dMZX Forums: Dynamic Lists - dMZX Forums

Jump to content

Page 1 of 1

Dynamic Lists

#1 User is offline   CJA 

  • «≡larch bucket≡»
  • PipPipPipPipPipPip
  • Group: Members
  • Posts: 3,262
  • Joined: 23-June 05
  • Gender:Male
  • Location:......@.c....

Posted 04 August 2017 - 03:48 AM

Hello, all! When you're programming a more advanced game, you might realize that using Robots to represent things in your game isn't always the best option. This might be true for party members in an RPG, which are not just mere Robots, but groups of statistics that have to exist between boards. Or maybe you want particles for fireworks, gunfire, or leaves floating on the wind that can't just be solid Robots.

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? Believe it or not, that long variable name can actually slow down MZX compared to a short one like, say, &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". There's one difference, though! Remember that old quirk with the old loops? While the old loop would run 11 times (0 through 10), this loop doesn't run the 11th time; it runs from 0 to 9. We got rid of the quirk!

But this is important: We'll need to remember that the raindrops are stored from 0 to 9 (in other words, raindrops minus 1). This is why we subtract 1 in loop for "('raindrops'-1)". We don't want to go all the way from 0 to raindrops (10), just 0 to 9!

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 and sorted, 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. If you were brave enough to make a lot of raindrops over and over and leave MZX on overnight, you might see that you've consumed a massive amount of counters and 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.

---------------

This is an early draft of this guide and discussion is welcome. I wanted to write this for my own future reference and figured sharing it would be best. I'll be editing to refine some of this.

This post has been edited by CJA: 19 September 2023 - 04:16 AM

1

#2 User is offline   Graham 

  • . "@Master Procrastinator"
  • PipPipPipPip
  • Group: Members
  • Posts: 625
  • Joined: 28-December 12
  • Gender:Male
  • Location:Oregon

Posted 05 August 2017 - 07:55 PM

Great tutorial CJA, thanks! This actually helps a lot being able to learn how to create a dynamic list in a tutorial.
Currently working on Servo for MegaZeux, I hope to complete it by the middle of 2015? Who knows...

"Before you criticize someone, you should walk a mile in their shoes. That way, when you criticize them, you’re a mile away and you have their shoes."
-Jack Handey
0

#3 User is offline   Bramble 

  • Veteran Member
  • PipPipPipPipPip
  • Group: Members
  • Posts: 2,869
  • Joined: 19-March 04
  • Gender:Not Telling

Posted 14 August 2017 - 10:41 PM

Daaaaamn!
0

#4 User is offline   Graham 

  • . "@Master Procrastinator"
  • PipPipPipPip
  • Group: Members
  • Posts: 625
  • Joined: 28-December 12
  • Gender:Male
  • Location:Oregon

Posted 28 August 2017 - 09:23 PM

Hi guys and gals, I haven't had internet at my Apt. for a while so I'm uploading these videos and posting this at work. I wanted to share the work I did in my game involving dynamic lists. This is perhaps one of the coolest changes to my game in some time and along with MegaZeux 2.90 has reinvigorated my desire to continue working on it.
This is me following CJA's tutorial. I added the ability to vary the rain intensity as well as displaying some of the counters along the right side of the screen because, well, it looks neat.

This is my implementation of dynamic lists to handle pushing objects around.

This is another implementation of dynamic lists to lift objects.

Hope you enjoy! and Thanks CJA!
Currently working on Servo for MegaZeux, I hope to complete it by the middle of 2015? Who knows...

"Before you criticize someone, you should walk a mile in their shoes. That way, when you criticize them, you’re a mile away and you have their shoes."
-Jack Handey
1

Share this topic:


Page 1 of 1


Fast Reply

  

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users