Tutorial: Layering (Generative Art 1)
You can find the code to copy and paste from this gist.
Generative Art 1
Let's take a look at the code I've written for this assignment. Remember that the code was (obviously) not written in the order it is presented here: It comes together in bits and pieces, some working and others broken. This is one of problems with so many tutorials as they exist on YouTube, for example: Instructors spend a few hours building the code in haphazard and unpredictable ways -- then they present it as though they're writing it 'live', giving the impression that they just start coding with line one at the top and move to the bottom, like an essay or a research article. But if a program has more than, say, five lines of code, the odds are very good that they were not written in the order 1,2,3,4 and 5: We make mistakes, we change our minds, we hit upon different ways to solve problems half-way through. "Writing" code is almost nothing like "writing" an essay.
In the version below, I put the code into three different TABS, one for each of PICO-8's main Functions. (You can put more than one function on a single tab, but you should never try to spread one function across two tabs.)
function _init() is where we start, in this case by defining all of the variables that I want to have ready access to later on. I've commented the variables in the code, below.
As I suggested above,
function _init() tends to grow and shrink throughout the process: Whenever I decide to add a new feature, etc., I almost always have to go back and create new GLOBAL variables by defining these inside
function _init(). By the same token, when I remove code that just didn't work, I'll usually need to go back to
function _init() and get rid of those variables that I don't need any more.
function _init() cls(1) counter = 0 updatescreen = true colqty = 9 rowqty = 9 colwidth = 128/colqty rowheight = 128/rowqty layers=3 end
counter = 0
As before, I'll use a counter variable to slow down my code.
updatescreen = true
This time, however, I'm spreading the "slow down!" feature across two variables, which you'll see in the
function _update() section.
updatescreen is a BOOLEAN variable (as in Boolean algebra), sometimes called a FLAG: It is always either TRUE or FALSE (YES or NO, 1 or 0). What I'm trying to do here is break my code into discreet little parts. You can think of flags like
updatescreen like lightswitches in a house.
layers = 3
My big challenge is to figure out how to put more than one shape inside each quadrant: e.g., sometimes my XY coordinates move, but sometimes they stay the same. If there are four quadrants, and two shapes per quadrant, that's a total of eight different shapes -- which is manageable by hand, I guess. But at, say, 4x4 sections, with three shapes per cell, that would be 16x3=48 separate shapes, which becomes hard to manage.
My solution (one of many possible!) is to think in terms of layers: I draw my biggest shapes on the bottom layer, then I start a new layer, drawing smaller shapes, then a last layer, drawing smaller shapes still. Computers never tire of repeating the same series of actions again and again: Just like before, I'm using
for / do loops to count over
rows, but now I've put that inside a bigger loop: This one counts through each of my
function _update() function is where I put the code that will slow my program down. The
counter variable gets increased with every loop, but that increase only matters once it hits a multiple of 120 (e.g., 120, 240, 360, etc.).
I chose to set the timer's trigger at about four seconds (an arbitrary choice on my part). At 30 frames per second, 4 seconds is about 120 cycles or repetitions. So when
counter bumps up to a perfect multiple of 120 (
counter % 120 == 0), the conditional statement inside this function suddenly evaluates as TRUE:
counter divided by 120 suddenly gives us an even number without a remainder! For 119 cycles -- because of lots of ugly remainders -- we'd be consigned to the
else part of the conditional: Setting
false, over and over.
counter % 120 == 0 is true, so the computer flows into the previously off-limits section of the conditional: It sets the
updatescreen variable to
true. Of course, as soon as we come back to this function, and add 1 to
counter, those remainders will pop up again, and we'll be back in the
else part of the conditional, setting
false again. The question is: What happens elsewhere in the code during this brief, shining moment (1/30th of a second, or 33.3 milliseconds)1 when
updatescreen actually equals
Note that I don't have to stick with only one variable like
updatescreen. In games, I'll typically use a lot of these "flags":
show_title_screen, and so on. Because they are BOOLEAN VARIABLES (only TRUE or FALSE) they take up no practically no memory, are fast to read and write, and help me simplify my code.
function _update() counter=counter+1 if (counter%100==0) then updatescreen=true else updatescreen=false end end
Here we are, at the start of the biggest
function. It is hard to talk about it usefully without breaking it up somewhat, so the format will change a bit below. I'll put a link to the code after all these comments. Let's go!
function _draw() if (updatescreen==true) then cls(1)
Here's the fruition of that 2-part approach we discussed earlier. See how I'm not asking about
counter at all, but instead I'm interested in whether we
updatescreen or not? That makes my code MUCH easier to read and understand, I think: We'll improve further on this approach later in the semester.
For now, notice that everything that happens in this
function depends on
updatescreen being set to
true. We don't even clear the screen (
cls(1)) until we know for sure that this is the one frame out of every 120 where we get to make a new piece of art. (See sections above for information about 120 frames, etc.).
(function _draw continues) radius = flr(colwidth/2) for pass=layers,1,-1 do scale=pass/layers
In the code above, I implement my layers approach (which took a little bit of trial and error, FWIW). You can think of it this way: If I had been using columns and rows to put stuff on the screen (that is, using X and Y), then with layers, I just added a Z-axis: Up and down.
Before, it looked a bit like this:
Count X from 1 to 4: Count Y from 1 to 4: Calculate exact position Draw something End of Y Loop End of X Loop
Remember that if we were to peek in at how the computer handled this code, we'd see something like this:
X1 Y1 X1 Y2 X1 Y3 X1 Y4 X2 Y1 X2 Y2 X2 Y3 X2 Y4 X3 Y1 X3 Y2 X3 Y3 X3 Y4 X4 Y1 X4 Y2 X4 Y3 X4 Y4
Clearly, the outermost loop, in this case the
X loop (width, columns, etc.) has a much easier job: It only runs four times. But the interior
Y loop (height, rows, etc.) ends up running a total of 16 times. NOW we're going to just add one more loop to our project (one more Matroska doll). While some approaches are better than others, we could probably do it any way we pleased: We could put the
layers loop on the top, so that X and Y are inside it, OR we could put the
layers loop on the bottom, so that it is actually inside both X and Y. In effect, we're asking: which of these two approaches should I take?
Should I do it like this?
- Find the specific X, Y coordinates of the cell you're working on;
- Now at that position, draw 3 different shapes, each a bit smaller than the one before.
Or should we go this route?
- Get ready to draw shapes of size S:
- Counting X by Y, draw shapes of size S centered in every quadrant;
- Now reduce the size of S. Go back and do everything again.
(Note that the 'pseudocode' here doesn't present us with an exact parallel.)
Here's a more abstracted, esoteric, but perhaps more satisfying version of the question:
Columns x Rows x Layers?
Layers x Columns x Rows?
For our purposes? Six of one, half-dozen of the other: They're basically the same at this point. In the end, I chose to put the layer-counting loop as the outer-most loop, which means that it repeats just a couple of times, while X works harder than before, and Y (the inner-most loop) works hardest of all.
BONUS: One perk of doing these shapes by looping through them with a
for / doloop? I can actually turn them off without causing trouble. I can do this by setting
function _init()) to 1, so that (in effect) my outermost loop ends up counting from 1 to 1. It never ends up running that loop more than once.
OK. That's a lot of info. I'm going to add just a word or two more, and then post this. We can talk more about things as you like.
Remember that since I've added the
layers loop, all of this stuff is inside it. The most important part here is a new variable that I introduced just inside the
layers loop, called
scale. Remember how that loop looked:
for pass = layers, 1, -1 do scale = pass / layers
So I'm inventing a variable here called
pass, which keeps track of which
pass I'm doing over the screen (e.g., first pass, second pass, etc.). The syntax that I've used, though, means that
pass doesn't start from 1, like I normally would: It starts from whatever the value of
layers is (which was set in the
_init() function). That third argument, "-1", is not usually there, but that's because we're normally counting from 1 to n, rather than from n to 1. "-1" lets us count backwards ("decrement by 1"). So if I have asked for 4 layers total,
pass will be 4, 3, 2, and then 1.
Why? Look at the next line. Again, millions of approaches to this, but this is a simple one: In order to resize the shapes on each subsequent layer, I compute
scale. If there is only one layer total, then scale is always 1.0 (i.e., 100%). Two layers? 100% and 50%. Three? 100%, 66%, and 33%. See how that works? I then use
scale to adjust the size of each rectangle, and/or each circle, in turn.
So why count backwards? Because layers can only work if the biggest is on the bottom -- otherwise they'll obscure subsequent layers. It works out nicely, as each subsequent layer is a smaller and smaller portion of 100% (the full cell).
for col=0,colqty-1 do for row=0,rowqty-1 do x1=col * colwidth x2=x1+colwidth-1 xcenter=(x1+x2)/2 y1=row * rowheight y2=y1+rowheight-1 ycenter = (y1+y2)/2 mycolor=rnd(1)*15+1 mycolor=flr(mycolor) mycolor=mycolor%6+1 -- pick a shape shape=rnd(1) if (shape>0.49) then xdist=(colwidth)/2 ydist=(rowheight)/2 xdist=xdist-1 ydist=ydist-1 xdist=xdist*scale ydist=ydist*scale x1=xcenter-xdist x2=xcenter+xdist y1=ycenter-ydist y2=ycenter+ydist rectfill(x1, y1, x2, y2, mycolor) else circfill(xcenter, ycenter, radius * scale, mycolor) end end end end end
Did you spot this in the code?
mycolor = mycolor % 6 + 1
There are a lot of choices we could talk about. The program runs fine without this little bit of extra code. But what does it change, and why?
Completing your code is always (ALWAYS) a process of returning to it and tweaking a line here, fixing the spacing there, renaming a variable throughout, etc. It is gradual, and accomplished over time. If you are new to programming: The fact that it works (or almost works!) is good enough for now.
(Actually, 33.3 milliseconds is a pretty long time. But we'll worry about that later!) ↩