In this case you might want to use counters, rather than Robots, to represent the existence of things. These can be concrete things, like a flurry of snow; abstract things, like a list of enemy statistics for an RPG; or something in-between, such as items lying in a player's inventory.
The one thing in common with all of these examples is that the list is dynamic. It can change in length. Sometimes there may be handfuls of raindrops on the board, or sometimes there may be just a couple. You may not know exactly how many items are in the player's inventory, or perhaps their backpack size can vary. Because of these uncertain lengths, the list is not able to be allocated all at once.
Hello, raindrops
Let's start with a simple example. We want rain that falls down. This rain is an object that will be drawn on the board, so it will need an X and a Y position.
Let's try to simulate a rain particle.
. "@HelloRaindrops" . "Initialize the X and Y." set "rainX" to random 0 to 79 set "rainY" to 0 . "Main loop" : "rain" . "Clear the raindrop" put c07 ' ' overlay to "rainx" "rainy" . "Increase Y by 1. Move the drop down." inc "rainY" by 1 . "Draw the raindrop" put c09 '|' overlay to "rainx" "rainy" . "Wait to avoid a busyloop, and repeat." cycle 1 goto "rain"
The raindrop picks a point on the top row between 0 and 79 (the default visible area on a standard empty board). Then we enter a loop where we Clear, Move, Draw. This basic loop structure is great for many engines that involve moving particles and objects.
This is great... for one raindrop. We want more! Well, we can do that, but we're going to need to use more counters. Let's keep them seperated using numbers in the counter names. Maybe an example will make more sense:
. "@ThreeRaindrops" set "rain0X" to random 0 to 79 set "rain0Y" to 0 set "rain1X" to random 0 to 79 set "rain1Y" to 0 set "rain2X" to random 0 to 79 set "rain2Y" to 0 : "rain" . "Clear!" put c07 ' ' overlay to "rain0x" "rain0y" put c07 ' ' overlay to "rain1x" "rain1y" put c07 ' ' overlay to "rain2x" "rain2y" . "Move!" inc "rain0Y" by 1 inc "rain1Y" by 1 inc "rain2Y" by 1 . "Draw!" put c09 '|' overlay to "rain0x" "rain0y" put c09 '|' overlay to "rain1x" "rain1y" put c09 '|' overlay to "rain2x" "rain2y" cycle 1 goto "rain"
Great, now we have three raindrops. But what if we want more...? We'll have to make this code longer and longer. What a chore! Let's make it easier on ourselves and do this using loops.
Let's first look at the LOOP START / LOOP FOR # command. This is a good way to get a very quick loop going without needing a label, GOTO, and variable. Let's condense!
. "@FactoredRaindrops" loop start set "rain&loopcount&X" to random 0 to 79 set "rain&loopcount&Y" to 0 loop for 2 : "rain" loop start put c07 ' ' overlay to "rain&loopcount&x" "rain&loopcount&y" inc "rain&loopcount&Y" by 1 put c09 '|' overlay to "rain&loopcount&x" "rain&loopcount&y" loop for 2 cycle 1 goto "rain"
We looped the initialization AND the Clear, Move, Draw loop. Now they each run three times and use the proper three numbers to refer to our different raindrops! But wait, loop for 2? Why 2 and not 3? One weird quirk about MZX loops is that the loop starts at 0, and the loop ends at the number you provide. So in the above example, loopcount will be 0, then 1, then 2, and then the loop ends. Which means there are really three passes!
Dynamic rain
Let's say we want more or less raindrops than 3. Maybe 10? Let's make a counter to store it! We'll set COMMANDS to a high value, set raindrops to 10, and replace the loop for 2 with loop for "raindrops":
. "@SomeRaindrops" set "COMMANDS" to 32767 set "raindrops" to 10 loop start set "rain&loopcount&X" to random 0 to 79 set "rain&loopcount&Y" to 0 loop for "raindrops" : "rain" loop start put c07 ' ' overlay to "rain&loopcount&x" "rain&loopcount&y" inc "rain&loopcount&Y" by 1 put c09 '|' overlay to "rain&loopcount&x" "rain&loopcount&y" loop for "raindrops" cycle 1 goto "rain"
It's getting a bit tiring to write &loopcount& over and over, huh? Let's switch to a loop using a counter like &i&. Let's leave our init part alone, but turn our loop into a proper label and goto loop:
set "commands" to 32767 set "raindrops" to 10 loop start set "rain&loopcount&X" to random 0 to 79 set "rain&loopcount&Y" to 0 loop for "('raindrops'-1)" : "rain" set "i" to 0 : "loop" put c07 ' ' overlay to "rain&i&x" "rain&i&y" inc "rain&i&Y" by 1 put c09 '|' overlay to "rain&i&x" "rain&i&y" inc "i" by 1 if "i" < "raindrops" then "loop" cycle 1 goto "rain"
In this case, the set "i" to 0 and : "loop" are functionally the same as loop start, and the inc "i" by 1 and the if statement are the same as loop for "raindrops". Also, keep in mind that we still retain the "quirk" where we stop when "i" is equal to raindrops: we process from 0 to (raindrops-1). That's accomplished because we use < and not <= when we check at the end of our loop.
OK, that's great. But rain doesn't work like this, it keeps coming! We need to program in a way to make more raindrops.
Allocation
Let's leave our rain engine robot behind and make another one. This one will MAKE rain drops every so often!
. "@RainMaker" : "l" wait for 3 set "rain&raindrops&X" to random 0 to 79 set "rain&raindrops&Y" to 0 inc "raindrops" by 1 goto "l"
This looks a lot like our initialization code, doesn't it? It does the exact same thing, but it uses "raindrops" as the variable.
Let's take a closer peek at our rain counters.
+---------+---------------------------------------------+ | Counter | 0 1 2 3 4 5 6 7 8 9 10 | | X | 42 76 12 18 37 72 75 51 11 48 | | Y | 1 1 1 1 1 1 1 1 1 1 | +---------+---------------------------------------------+
So our rain counters 0 through 9 are set, but 10 is not set. That's where we want to put our next drop. And it just so happens that, because we started at 0, the raindrops counter that tells us how many drops we have is exactly 10!
Important fact: As long as we keep our list compact, the next free list position will always equal the number of list items. So rain&raindrops&X and rain&raindrops&Y will always refer to the item after the last item in the list. Of course, the item after the last item never exists, and thus, is where we would want to create a new item.
Now that we know that, all we need to do is set rain&raindrops&X and rain&raindrops&Y to the values we want, and presto, we have a new raindrop! But we need to remember that we did just make one, so we have to inc "raindrops" by 1!
Destruction
So far we've made the rain engine that draws and moves the drops, as well as an allocator that makes new raindrops. So everything's fine, right? Well... not quite. Without removing raindrops over time, you might consume way too many counters and eventually, memory! The last piece to the dynamic list puzzle is destruction.
What will destroy raindrops? Hitting the ground will suffice (while not totally accurate). Let's make it so that when a raindrop hits a CustomBlock, it dies for good! If you're following along at home, go ahead and draw a CustomBlock terrain on your testing board for this part!
Since our engine touches every rain drop, it might as well do the check. Let's add the CustomBlock check into the loop.
. "@DynamicRaindrops" set "commands" to 32767 set "raindrops" to 10 loop start set "rain&loopcount&X" to random 0 to 79 set "rain&loopcount&Y" to 0 loop for "('raindrops'-1)" : "rain" set "i" to 0 : "loop" put c07 ' ' overlay to "rain&i&x" "rain&i&y" inc "rain&i&Y" by 1 if c?? CustomBlock p?? at "rain&i&X" "rain&i&Y" then "hit" put c09 '|' overlay to "rain&i&x" "rain&i&y" inc "i" by 1 if "i" < "raindrops" then "loop" cycle 1 goto "rain"
Now let's add our destruction code to the : "hit" label:
: "hit" dec "raindrops" by 1 set "rain&i&X" to "rain&raindrops&X" set "rain&i&Y" to "rain&raindrops&Y" goto "loop"
Let's look at our counters again. Let's say we have our 10 raindrops, stored in 0 to 9. Let's say raindrop number 5 hits a CustomBlock. It goes to the : "hit" label. Here's where the true magic happens: First, we want to keep our list compact. The easiest way to do this is to take the last item in the list and copy it here, and then shrink the list. We could do this in order but it just makes more sense to shrink the list to "0 to 8", so that raindrop number 9 is no longer in the list. But raindrop number 9's X and Y are still there, so we copy them over! Raindrop number 9 is effectively destroyed (moved out of the list), but raindrop number 5 is an exact clone of raindrop number 9 -- so really, 9 is still fine, and 5 is being destroyed, as it should be!
Let's think about the counters one more time. We have moved raindrop number 9 to raindrop number 5's position, but raindrop number 5 has already had its "turn" in the loop! We need to make the raindrop go again. This is why we use goto "loop" instead of returning from a subroutine. That label re-starts the Clear, Move, Draw loop so that the re-positioned raindrop still gets its chance this cycle!
Destruction sanity checks
One more little quirk: what happens when there are no raindrops? Ideally, the engine should just rest for a cycle. Also, when we are destroying the last raindrop in the list, the list should shrink, but nothing should copy (and the main loop should restart). Let's make sure all that happens.
So, here's your full rain engine, with the : "norain" label for when there are 0 raindrops or when the last raindrop is destroyed. Let's also remove the initialization and let our rain maker make all the rain!
. "@DynamicRaindrops" set "commands" to 32767 : "rain" set "i" to 0 if "raindrops" <= 0 then "norain" : "loop" put c07 ' ' overlay to "rain&i&x" "rain&i&y" inc "rain&i&Y" by 1 if c?? CustomBlock p?? at "rain&i&X" "rain&i&Y" then "hit" put c09 '|' overlay to "rain&i&x" "rain&i&y" inc "i" by 1 if "i" < "raindrops" then "loop" : "norain" cycle 1 goto "rain" : "hit" dec "raindrops" by 1 if "i" >= "raindrops" then "norain" set "rain&i&X" to "rain&raindrops&X" set "rain&i&Y" to "rain&raindrops&Y" if "raindrops" > 0 then "loop" goto "norain"
. "@RainMaker" : "l" wait for 3 set "rain&raindrops&X" to random 0 to 79 set "rain&raindrops&Y" to 0 inc "raindrops" by 1 goto "l"
Of course, there's much more you can do here. Here are some exercises to test your knowledge:
- How would you make sure that if a raindrop falls off the board, it is removed? Spoiler
- What counter contains the Y coordinate of the last raindrop in the list? Spoiler
- How would you make multi-colored rain? Spoiler
- What are some ways you track or debug the raindrops? Spoiler
- Want some sprite practice? Try doing this with sprites. Make sure your rain maker sets up the sprite (refx, refy, width, and height) and puts the sprite on the board. Use the sprite counters, like spr&i&_x and so on, to refer to your items, and make sure your destroy/copy code copies all of the relevant counters.
This is one of the simplest examples of using dynamic lists. You could make a particle engine from here by adding counters like "dx" and "dy" to represent horizontal and vertical motion. You can make the character and color into variables as well. You can even use sprites! As long as you make sure that your objects are properly allocated and destroyed, you'll have smooth sailing when you use dynamic lists!
--------------
Inventory system
Raindrops are nice but how about an inventory system? It's just a matter of adapting the above code into an event based structure, rather than a loop based one. Let's say, first, we have some sort of basic inventory game. Items are unique and you pick them up and drop them off. We'll use a string to represent items for our own ease. Maybe just $item0 through $item...whatever.
. "@Inventory" set "commands" to 32767 end : "keyi" set "i" to 0 if "items" <= 0 then "noitems" change overlay c0f to c00 : "loop" write overlay c0f "&$item('i')&" at 0 "i" inc "i" by 1 if "i" < "items" then "loop" end : "noitems" write overlay c07 "No items " at 0 0 end
How is this different? First, there is no Clear, Move, Draw loop because these are not concrete objects. The change overlay command is just a quick way to clear the overlay (and doesn't quite work--it just makes the text black--but it's good enough for this tutorial). Items are simply drawn on the overlay, one after the other, when the I key is pressed. The loop simply draws all of the items in the list and ends.
Allocating items
So how do we get items? Enter the free chicken giver:
. "@FreeChickens" end : "touch" & "Here, have a rubber chicken!" set "$item&items&" to "Rubber chicken" inc "items" by 1 end
This is almost exactly like the Rain Maker from earlier. The chicken giver sets the next open item's slot to "Rubber chicken" and increases the total items by 1. Try touching the chicken giver a few times and pressing "I" and you'll see your list of rubber chickens!
Destroying items
Just like earlier, taking away (destroying) items is a bit tougher. In order to find a particular item, though, we're going to need to loop through all of our items and find one that matches! Here's the Chicken Collector that will happily take away all your rubber chickens:
. "@ChickenCollector" end : "touch" if "items" = 0 then "noitems" loop start if "$item&loopcount&" = "Rubber chicken" then "thatone" loop for "('items'-1)" : "noitems" & "I need a rubber chicken!" end : "thatone" & "Oh boy, a rubber chicken. Thanks!" dec "items" by 1 if "loopcount" >= "items" then "end" set "$item&loopcount&" to "&$item('items')&" : "end" end
Notice that the code in the : "touch" event scans your items and finds out whether you have the item in question. It makes sense to first check that the player has items at all before doing anything else. When it does hit upon a rubber chicken it goes to : "thatone", with the loopcount counter already pointing to the correct position to destroy. Notice that it decreases items, does a check to see this is the last item ([font="Courier New"]if "loopcount" >= "items" then "end"), then proceeds to deallocate by copying the last item in the list to the destroyed item's position.
---------------
Edited 6/30/24 to remove a few misconceptions, for the most part this still holds up though!
This post has been edited by CJA: 30 June 2024 - 06:29 PM