Sprite (Tutorial)

From MZXWiki
Jump to navigation Jump to search

Despite the mystery and trepidation that generally surrounds them, sprites are not that hard to work with and are designed to be easy to use, with the right program model. Two common and effective models, which can be used together or alone depending on the requirements of the engine, are the sprite-layer model and the sprite-object model. The structure of your code will depend on the application and the model being used, but there are some basics to cover first.


Sprite Tutorial

1 The Basics: Drawing Sprites
Exercise 1.1: Making a sprite based player
Exercise 1.2: Improving and generalizing the code
1a The Not So Basics: Sprite Collision
Exercise 1.3: Adding basic collision detection
2 The Sprite-Object Model: Active Collision
Exercise 2.1: Keys and doors with sprites
Exercise 2.2: Keeping track of sprites on the board
Exercise 2.3: Moving the player sprite between boards
Exercise 2.4: Finishing touches, fixing the scrolling
3 The Sprite-Layer Model: Working With Layers
Exercise 3.1: Creating a status bar
Exercise 3.2: Bullets as a layer (framework)
Exercise 3.3: Bullets as a layer (implementation)
Exercise 3.4: Interacting with the rest of the game

External Links

The Basics: Drawing Sprites

All sprites, regardless of their function, require some basic initialization and setup before they can be used. First, you need to set aside space on the board (or the Vlayer) for the sprite source image. You don't even have to put anything there yet, some advanced techniques construct the image data dynamically. But you need a block of space that is going to be used for sprite data. Next, all sprites need initialization code somewhere that designates this area.

set "spr#_refx" to XCOORD      These counters specify the x coordinate, y coordinate,
set "spr#_refy" to YCOORD      width, and height of the bounding rectangle for the source image
set "spr#_width" to WIDTH      Replace # with the number of the sprite, from 0 to 255.
set "spr#_height" to HEIGHT    Counter interpolation is acceptable and in fact common for this.

Finally, all sprites need to be placed somewhere in order to be viewed. While "spr#_x" and "spr#_y" counters do exist, and they can be written to place and move the sprite, we recommend you treat them as read-only and use the "put" command for readability and clarity of purpose:

put c?? Sprite # at X Y        # is the number of the sprite to be placed, as a parameter.  Can be a counter.

Exercise 1.1: Making a sprite based player

A common application for sprites is to have all actors in the game, including the player, be represented by sprites. This lends itself well to a sprite-object model, but we'll come to that later. Our first exercise will be to make a player sprite that can be moved around with the arrow keys.

  1. First, start editing a new world, and draw a customblock smiley face on the board. It doesn't really matter what it looks like, or where it is, but for the sake of example put it at (80,0) off the right edge of the screen, and make it 3x3 characters large.
  2. Create a new robot called "sprite" to handle the sprite drawing and moving code. I like to put my control robots in a horizontal line starting at (1,0), but again, it really doesn't matter. The very first line of the robot should be "lockplayer", just to keep the player from moving around.
  3. Let's use sprite 0 for our player. So, the next 4 commands should be:
    set "spr0_refx" to 80
    set "spr0_refy" to 0
    set "spr0_width" to 3
    set "spr0_height" to 3
  4. Now we need a control loop that draws the sprite and handles input.
    : "drawloop"
    wait for 1
    put c?? Sprite p00 at "x" "y"
    if uppressed then "up"
    if leftpressed then "left"
    if rightpressed then "right"
    if downpressed then "down"
    goto "drawloop"
    Note that x and y will default to 0 since they haven't been used yet. It's generally a good idea to keep this information in a pair of local counters and initialize them along with the rest of the sprite data.
  5. Now, you just need code to modify the values of "x" and "y" for each label "up", "left", "right", and "down".
    : "up"
    dec "y" by 1
    goto "drawloop"
    And the rest should be obvious: inc "y" by 1 for "down", dec "x" by 1 for "left", and inc "x" by 1 for "right".

Example 1.1 Code

Exercise 1.2: Improving and generalizing the code

That's it, you can now test your world and move the sprite around. You'll notice that the sprite gets "stuck" if you move it off the top or left of the board, and disappears off the bottom right. That's because we didn't do any bounds checking. In fact, there are a lot of improvements we can make to this code before we move on.

  1. First let's make the sprite initialization dynamic. This may seem like more work now, but it'll make things much easier when you want to play with a lot of objects later, and decide that you need to reallocate sprite numbers. We'll declare local counters for the sprite draw location while we're at it.
    set "local" to 0
    set "local2" to 5
    set "local3" to 5
    set "spr&local&_refx" to 80
    set "spr&local&_refy" to 0
    set "spr&local&_width" to 3
    set "spr&local&_height" to 3
    : "drawloop"
    wait for 1
    put c?? Sprite "local" at "local2" "local3"
  2. We also need some bounds checking. I prefer to define variables for zone boundaries, but we'll use constants with expressions for now.
    : "up"
    if "local3" <= 0 then "drawloop"
    dec "local3" by 1
    goto "drawloop"
    : "down"
    if "local3" >= "(25-'spr&local&_height')" then "drawloop"
    inc "local3" by 1
    goto "drawloop"
    Remember that "local3" is "y".
  3. You also probably noticed that the previous movement routine favored certain directions over others; you can fix this by turning the label calls into subroutines (with diagonal movement as a free bonus!)
    if uppressed then "#up"
    : "#up"
    if "local3" <= 0 then "#return"
    dec "local3" by 1
    goto "#return"
  4. One pitfall that comes with using subroutines for movement like this is that it's possible to get the sprite stuck on corners as you move it. This is because the sprite position is not updated after the sprite counters are changed, but only after every move direction has been checked. One way to fix this is to just use the spr#_x and spr#_y counters instead of local counters, since those are directly tied to sprite position. But for reasons we'll discuss later, I recommend against this, or at least keeping copies of them in local counters. The other way is to make sure the sprite is updated (i.e. drawn) after each move. This is accomplished most easily through subroutines as well:
    : "#draw"
    put c?? Sprite "local" at "local2" "local3"
    goto "#return"
    Then, for every location where the sprite needs updating, that is at the end of all movement subroutines and within the main loop, call the subroutine "#draw" with a goto. In fact, it's not really even necessary to call this in every iteration of the main loop if it's called on movement. It only needs to be called once after all the sprite counters have been initialized.
  5. Since this is our player sprite, it would be nice if we could get the screen scrolling to act like it. Fortunately there's a really easy way to do this without having to work out a bunch of bothersome math. Just put the following functional counter in your #draw subroutine, after you draw the sprite:
    set "spr&local&_setview" to 1
    Really simple, and another good reason to have a separate subroutine for the drawing code, since that makes it easy to update and augment. You can now expand the board area and bounds checking beyond the confines of the viewport.

This leaves us with this final code:

Example 1.2 Code

The Not So Basics: Sprite Collision

So we can now draw a sprite and move it around the screen. This is great, but except for some very limited cases is not particularly useful for gameplay. Simple bounds checking is easy enough to implement, but what if we want to have our player sprite move around inside a defined terrain, with walls and solid objects and impassible barriers? Worse, what if we want to interact with the environment, or with other sprites? Designing this from scratch would involve doing a lot of checking for customblocks, a way to figure out which sprite is at a specific location, and depending on the size of the sprite being moved, would require checking multiple locations each time in order to ensure consistency. It would be difficult to make the system general enough to be transplantable from game to game, and if you did manage it it would be hard to read and understand the code.

Fortunately, MZX provides a way to do all that work with one statement:

if c?? Sprite_colliding p## at X Y then LABEL   p## is the number of the sprite you want to move, X and Y are relative coordinates.

One of the reasons people find this simple command so difficult to use is that they don't understand what it actually does. The first important requirement is that the sprite being moved define a collision rectangle. This should be done along with the other sprite initialization counters like so:

set "spr#_cx" to X             These X and Y coordinates are relative values.
set "spr#_cy" to Y             That means you set them relative to (0,0) as the top left corner of the sprite.
set "spr#_cwidth" to WIDTH     So if you want the collision rectangle to be the same size and area as the sprite itself,
set "spr#_cheight" to HEIGHT   cx and cy should both be 0, and cwidth and cheight should be the same as width and height.

Most people understand this much. What often gets confused is that the Sprite_colliding object in the if statement is NOT this collision rectangle. Nor does it directly represent the collision rectangle of another sprite, though it is necessary for other sprites to have collision rectangles in order for collision to work. But there isn't an actual sprite_colliding object anywhere on the board, this is simply the syntax used to call collision detection for a sprite in advance of movement. The command says "if I hypothetically move this sprite (specified by the parameter) X by Y from its current location, will it collide with anything." And then it branches depending on whether the answer is true or false.

The two key points about X and Y are that they are relative to the sprite's current location, and they specify the movement of the sprite, not the position of something else. The color term is co-opted to perform a non-intuitive task of specifying relative versus absolute movement. c?? means that X and Y are relative values, and is what you will normally want to use. c00 (or any other absolute color) make X and Y absolute coordinates, but again the statement checks to see what would happen if you put the sprite at that location, not for the presence of some other object at that location. This is vital to understand, since without it you will probably try to do much more work than you need to do, and will probably achieve unexpected and incorrect results for your efforts.

Exercise 1.3: Adding basic collision detection

Let's see if we can't improve our player sprite code and turn it into something you might actually want to use in a game.

  1. First, take that board you were working on and draw some walls on it. Sprite collision detection only works with customblocks and other sprites, so in any game where you want to use sprite collision, all of your collidable scenery should be customblock. But if you're really interested in advanced MZX programming and artwork, you should already be doing this anyway.
  2. In order to create the illusion of depth and a 3/4 camera angle, we'll define the collision rectangle as being only the bottom row of the sprite. The sprite dimensions are 3x3, so that means:
    set "spr&local&_cx" to 0
    set "spr&local&_cy" to 2
    set "spr&local&_cwidth" to 3
    set "spr&local&_cheight" to 1
  3. Update each of your movement subroutines to include a collision check along with the bounds check:
    : "#up"
    if "local3" <= 0 then "#return"
    if c?? Sprite_colliding "local" at 0 -1 then "#return"
    dec "local3" by 1
    goto "#return"
  4. Finally, going along with that 3/4 camera thing, you will generally want game sprites farther down the screen to appear "in front" of sprites above them. Sprites have a globally applicable drawing mode to handle this very thing:
    set "spr_yorder" to 1
    Note that this counter is global to all sprites, and is not a per-sprite counter. You can stick it anywhere you like, but it makes the most sense to put it in the global robot. We will discuss how to use layer-like sprites with yorder later. For now, all you need to know is that this makes sprites draw in order of position from the top of the screen to the bottom, instead of in order of their sprite numbers.

And that's it! That's really all you have to do!

Example 1.3 Code

The Sprite-Object Model: Active Collision

It's wonderful and all that we can make sprites not do something (i.e. move) if they collide. But what if we want to make them do something else instead? What if we want that thing to be different depending on the object of the collision? When you perform a collision check with "if sprite_colliding", it has the side effect of populating an array-like construct that details what, if anything, was collided with. This array is called "spr_clist", its length is stored in the counter "spr_collisions", and it is accessed as pseudo-arrays usually are in MZX, through counter interpolation. Here is a typical and flexible collision handler:

: "collision"
loop start
goto "#collide('spr_clist&loopcount&')"        This will only goto labels that actually exist, making it easily pluggable.
send "spr('spr_clist&loopcount&')" to "touch"  This will send to a robot named with the same number as the colliding sprite.
loop for "('spr_collisions'-1)"                Don't forget about the loop termination quirk, where the terminator is inclusive.
goto "#return"                                 Short circuits whatever else would have been done had collision not happened.
: "#collide-1"                                 -1 is the background, meaning the sprite collided with a customblock.
* "~fOuch, I bumped into a wall!"              Normally you wouldn't bother with this label though, since a wall is a wall.
goto "#return"                                 Continues collision handling.  Remember, subroutines go on a call stack.

The send command is the beginning of understanding sprites with an object model, where each sprite is bound to code in a robot specific to that sprite, so that everything relevant to that sprite (including counters you may create) can be accessed with a single number. Here, we use that number to send the sprite (and I find it useful to make no distinction between the sprite and the robot controlling it) a touch label.

You should devise and maintain a convention for what robots that control sprites should be called. "sprite#" or "spr#" are normal, or you could just reference them by number. In order to make things as code driven as possible, so that you only have to change a single line to reassign a sprite number in a robot, you should do this dynamically in your robot setup with a rename command. And there's some other stuff you can do to make it easy to move sprites around just by moving the robots that control them.

set "local" to "robot_id"   This makes things very dynamic, since you can copy the same robot around the board with no changes
set "local2" to "thisx"     and make multiple copies of the sprite.  Setting local2 and local3 based on the robot's position
set "local3" to "thisy"     means that you don't have to configure the initial sprite placement, either.  Then you can move the
gotoxy "local" 0            robot into a robot bank at the top of the board, and the unique value of local ensures you won't
. "@spr&local&"             overwrite anything.  But this is not always appropriate, sometimes you'll want to choose a specific ID

Exercise 2.1: Keys and doors with sprites

Now that we have an idea of how to make our sprite player touch things, let's create some things for him to touch.

  1. First, the player needs a collision handler. You can use the one we just discussed with no modification (though you may want to take out the #collide-1 label since that's just to demonstrate how to do something special based on what you collided with). You'll also need to change the target label for the collision check to "collision":
    if c?? Sprite_colliding "local" at 0 -1 then "collision"
    And so on.
  2. You need to create some key objects and some door objects, at least one of each, but creating more than one will be really, really easy. Each of these will be sprites, and each of them will have a robot in control. First, draw some graphics over with the player source you had before. Remember what their parameters are. If you want to do something REALLY cool, create a list of constants and put it in the global robot to keep track of these numbers for you. This way, if you ever change them, or do something like making your sprites vlayer based, you'll have a much easier time of things since you'll only have to change the global robot. In any case, use what you've learned about sprites so far to create a robot called "key" with a dynamic initialization routine:
    set "local" to "robot_id"
    set "local2" to "thisx"
    set "local3" to "thisy"
    set "local4" to "this_color"
    gotoxy "local" 0
    . "@spr&local&"
    set "spr&local&_refx" to XCOORD
    set "spr&local&_refy" to YCOORD
    set "spr&local&_width" to WIDTH
    set "spr&local&_height" to HEIGHT
    set "spr&local&_cx" to CX
    set "spr&local&_cy" to CY
    set "spr&local&_cwidth" to CWIDTH
    set "spr&local&_cheight" to CHEIGHT
    put "local4" Sprite "local" at "local2" "local3"
    Of course you'll need to provide your own values for those counters, and as I've said, I really recommend assigning them to constant counters whose values you set all in one place. It makes things much easier later, when you want to change the way things are done. Also notice that in addition to reading the coordinates of the key based on where you put the robot, it also reads the color of the key and draws the sprite accordingly. This means we can use exactly the same robot code for different colored keys, without changing a thing except the color of the robot itself.
  3. Now you need a ":touch" label. In the case of a key, if you touch it you just collect the key and it disappears. So we need a counter to keep track of the key, and we need to make the sprite disappear.
    : "touch"
    inc "key_&local4&" by 1
    set "spr&local&_off" to 1
    What we've done here is increment a counter that is unique to the color of the key. This means we can collect more than one of the same color key, and open just as many doors. The spr#_off counter is a special functional counter which turns off sprite drawing and sprite collision when it is set to something. It does NOT set the sprite's counters to zero, though.
  4. Now, copy that key robot and make a door robot out of it. I'm serious, the changes you'll be making are so slight that they don't warrant starting from scratch. Just change the values of the sprite initialization to suit, and change the behavior in the touch routine:
    : "touch"
    if "key_&local4& <= 0 then "end"
    dec "key_&local4&" by 1
    set "spr&local&_off" to 1
    : "end"
    We perform a simple check to make sure that the player collected a key that is the same color as this door. Other than that, it's a mirror image of the key's touch routine.
  5. Now, copy those two robots all over the board. You may actually want to do this with copyrobot commands, so that you only have to change the code in one place, when you need to change it. Or, if you want to be clever, save the robot code into a file and load from that. Whatever you do, put a bunch of copies of the key and door code onto your board, using as many different colors as you like. Now play around with it.

Example 2.1 Code

Exercise 2.2: Keeping track of sprites on the board

So far, the only time we've needed to have sprites talk to other sprites or refer to them by ID is when they collided, and then the numbers are provided for us. But suppose we want to know which sprite is the player and where it is, so that we can allow another robot controlling another sprite (like an enemy) to target it? Or what if we want to have the same sprite run around the board stealing keys (we'll actually do this in an upcoming example)? It'll need to know where they are too.

  1. For the player, this is simple enough, we can just have the following line in our initialization:
    set "playersprite" to "local"
    Can't get much easier than that. But for keys and doors, we need to be a little bit more clever, since there can be arbitrarily many of those. Just using their color (local4) as an identifier isn't enough, since that might not be unique on the board, and besides that we'd like a solution to enumerate a list of things.
  2. First, in the global robot, initialize a couple of counters to keep track of the number of keys and doors on the board:
    set "num_keys" to 0
    set "num_doors" to 0
  3. Then in the key init code (and similarly for the door code), add each key to the end of a list and advance the counter:
    set "spr&local&_lpos" to "num_keys"
    set "keysprite('spr&local&_lpos')" to "local"
    inc "num_keys" by 1
    We also store the sprite's position in the list and attach it to the sprite, double linking them.
  4. Why bother spending overhead on this, and making this counter public? So that we can easily prune the list when the player collects a key or opens a door. Here's a neat trick to quickly remove an item from an array without leaving a hole:
    dec "num_keys" by 1
    set "spr('keysprite&num_keys&')_lpos" to "spr&local&_lpos"
    set "keysprite('spr&local&_lpos')" to "keysprite&num_keys&"
    set "spr&local&_off" 1

Let's pause here and explain what we've just done in more detail. First, we create a list of sprite/robot ids (they're the same in our system, remember). We populate this list by first setting the length of the list to zero, and adding each item to the end of the list as its robot gets executed. We also know that these items are subject to being removed from the list of active sprites in certain game situations. Now we can set things up such that the list will be automatically regenerated if we leave the board and come back (more on that in the next example). But when we start using lists to keep track of lots of objects that get added to and deleted from the list a lot (e.g. bullets), we'll really appreciate having a way to perform those add and delete operations in constant time, while limiting the time to iterate over the list down to the number of currently active items.
The key observation that makes the above trick work is that the list doesn't need to be sorted. So all we need to do to deal with the hole left by a deleted list item is find another item to put in the hole. And there's always an item at the end of the list, even if it's the very item we're deleting. We tell the sprite at the end of the list that it's list position (spr#_lpos) is now the position of the item we're deleting (this is why that counter needed to be public). And then we tell the list that the id at that position is now the id at the end of the list. Finally we decrease the length of the list (it's safe to do this first as shown in the code because the data doesn't actually go anywhere), effectively removing the last item on the list.

  1. This exercise has largely been in preparation for things to come. But in order to demonstrate its usefulness quickly, let's create a simple debug robot to print the status of all of our sprites on the board.
    : "keyt"
    [ "The player is sprite &playersprite&, located at (('spr&playersprite&_x'), ('spr&playersprite&_y'))."
    set "local" to 0
    : "keyloop"
    if "num_keys" <= 0 then "doorloop"
    [ "Key sprite #&local& is located at (('spr('keysprite&local&')_x'), ('spr('keysprite&local&')_y'))."
    inc "local" by 1
    if "local" < "num_keys" then "keyloop"
    set "local" to 0
    : "doorloop"
    if "num_doors" <= 0 then "end"
    [ "Door sprite #&local& is located at (('spr('doorsprite&local&')_x'), ('spr('doorsprite&local&')_y'))."
    inc "local" by 1
    if "local" < "num_doors" then "doorloop"
    : "end"
    Just copy this into a new robot (put it somewhere other than the top row of the board) and press 'T' to access it.

Example 2.2 Code

Exercise 2.3: Moving the player sprite between boards

Almost any real game is going to involve more than one board (single board content management via MZMs aside). However, successfully managing sprites across multiple boards requires some overhead and foresight. Before we go any further, we need to address one of the major gotchas of sprites, which is that you have 256 of them to use in the entire GAME, not per board. This means that, especially for a dynamic sprite system, you need a way to ensure that sprites are kept consistent and in the same places across boards.

  1. What this generally means is that we want to have the sprite initialization happen every time you load the board, implying the use of :justentered. But there are also parts of our sprite initialization code that we want to happen only one time, like setting the initial sprite position by using the initial robot position and then moving the robot. Fortunately there's a simple trick for pulling this off:
    . "One time execution code goes here."
    restore "justentered" 1
    | "justentered"
    . "Static initialization code goes here (sprite ref and collision box among others)."
    . "Fallthrough to main program loop."
    If you remember your robotic, a pipe is a pre-zapped label. This should be applied to the global robot as well, for sprite list setup.
  2. Of course there are things, especially for sprites that are going to move around, that must be preserved on a board change but which are subject to change. This is the reason I suggested the use of "local2" and "local3" to keep track of sprite position, instead of "spr#_x" and "spr#_y". But the player needs a system that doesn't simply preserve its position on each board, but which accurately sets its position based on where it was on the previous board. This means that the player sprite needs to use global counters instead of local counters. So do a search and replace on "local2" and "local3" in the player robot (CTRL+R is your friend) for counters like "player_x" and "player_y". Then add this code to the one time execution segment of the global robot:
    set "local2" to "playerx"
    set "local3" to "playery"
    The reason for the locals as temps will become clear in a moment. Now add this code to the board init section:
    set "player_x" to "local2"
    set "player_y" to "local3"
    put player 0 0
    Now you can us the player object itself to mark the start position of the player sprite, and be able to test from that position on any board. We lock scrolling because of what we do with the player in the next step, to prevent the viewport from jumping around. It no longer really matters where you put the player robot, as long as you don't put it in the top row (for safety reasons, since all the sprite robots move to that line and it might get overwritten before it moves). Also note the use of "player_x" instead of "playerx", since "playerx" and "playery" are built-in and read-only.
  3. Now we need to know when the sprite moves off the board. Fortunately bounds checking allows us to do just that. For each movement routine, add a sprite counter that tells which direction the sprite last moved, and then route the bounds check to a label that sends the global robot a message, like so:
    : "#up"
    set "spr&local&_lastmove" to 0
    if "player_y" <= "bminy" then "edgecollide"
    : "edgecollide"
    send "global" to "edge&spr('local')_lastmove&"
    goto "#return"
  4. Now we're going to use a nice little shortcut so that we can keep track of board adjacencies with the standard MZX system instead of writing another data structure to do it. All it requires is that the four corners of each board you use it on be empty. That's because we're going to use the player object itself to move between boards, instead of using teleport player. Write each edge# method similar to this:
    : "edge0"
    . "North"
    set "local2" to "player_x"
    set "local3" to "('bmaxy'-'spr_p_h')"
    . "That is, the bottom of the sprite playing field minus the height of the player sprite."
    put player 0 0
    . "Use board_w-1 and board_h-1 to move south and east."
    move player NORTH
    The idea here is to set temp counters to the expected location of the player sprite on the next board, then move the actual player to that board. We don't use the actual counters so that if the move fails (there is no board in that direction), nothing happens to the sprite. But if the move succeeds, the justentered label will be triggered and the values will be committed.
  5. Obviously for this to work right, those "bminy" and "bmaxy" counters need to actually exist. They also need to be the same across all boards; if the boards need to have different dimensions you will need a more complex system. But for this simple case, just add those to the one-time code of the global.
    set "bminx" to 0
    set "bminy" to 0
    set "bmaxx" to 80
    set "bmaxy" to 25
    You can use whatever values you like, of course, at this point it's more up to the requirements of your game than anything else. You can and should replace all hard-coded values in the player bounds checking with these counters, as seen above.
  6. One final thing remains to be done, and that's to make sure all the sprites being used at the time of the board transition are turned OFF. We may have done a lot of work to make sure sprites can initialize properly each time the board loads, but if we don't destroy them before moving to another board then any sprites that don't get reinitialized will hang around. Now it would be possible to just loop through all 256 sprites and turn them all off to be safe, but that may not always be optimal, and besides that this is a perfect application for those lists we made earlier.
    set "spr&playersprite&_off" to 1
    loop start
    set "spr('keysprite&loopcount&')_off" to 1
    loop for "('num_keys'-1)"
    loop start
    set "spr('doorsprite&loopcount&')_off" to 1
    loop for "('num_doors'-1)"
    Stick this at the front of the global board init, and it'll be the first thing executed when a board loads.

That's really all that needs to be done at this point. Test it out by creating a few interlinked boards and spread your doors and keys across them. You'll need to copy the sprite source data between the boards too. (Writing an MZM loader for this into the global robot is left as an exercise to the reader. I actually recommend using the vlayer to store them though, and we'll do this in the next step, so brush up on the vlayer tutorial for more info.)

Example 2.3 Code

Exercise 2.4: Finishing touches, fixing the scrolling

Before we wrap up this section for good, there's a bit of cleanup work we need to do. Most pressing, and something you will have noticed by now unless your boards are all the same size as the bounding area for your sprite playing field, is that the simple directions I gave you for scrolling the viewport don't work right, or not the way you'd really want them to. If your sprite images are still on the board, you probably want them to not be visible; you'd like the "board" that the player sprite moves on to stop before you get to things that are off the screen. There are really only two ways to handle this: either put nothing on the board that is not intended to be seen (i.e. use the vlayer and some other tricks), or use complicated math to clamp the viewport to the edges of the playing area, to prevent the player from seeing things that are not intended to be seen.

I wrestled with how to present this material for close to a year, since there are just so many problems with it. One problem is that it's mostly just necessary busywork, tying off loose ends so that the engine functions more smoothly; it doesn't really fit in with the flow of the tutorial, but it really needs to be done now before we proceed to the next major section. Another problem is that doing things in an easily pluggable way that only involves adding a few lines in key places requires some very complex expression hacking that just isn't appropriate to teach in a tutorial about sprites; while doing it in a way that is easy to understand requires more additions and modifications than I'd really like. So ultimately I've decided to give you as many options as possible. If you want to skip as much of this nonsense as possible and get right on to the sprite-layer model, just follow step 1 to get your sprites on the vlayer, and step 1a to deal with the details of only doing that. If you want to go the whole nine yards, the rest of the steps will show you, in order of necessity, how to tweak the engine to handle scrolling inside of a bounded area. You can stop following them whenever you're satisfied with the results. And if you're interested in my preferred method for doing this, the last step will show you the expressions for compressing 50 lines of code into 5.

  1. First things first, one thing that absolutely must happen before we move on to sprite layers is that we start using the vlayer for sprites. If nothing else, we don't want to have to set aside broad swaths of space on each board for sprites that are as large as the playing area itself. So, export your sprite image data to something like "sprites.mzm", and add the following line to the one-time code of the global robot:
    put "@sprites.mzm" Image_file p02 at 0 0"
    You'll also need to adjust all the coordinate constants for the sprites appropriately, by subtracting 80 from the refx coordinates. So for example:
    set "spr_p_x" to 0
    set "spr_d_x" to 3
    set "spr_k_x" to 6
    Finally, all sprite robots need a line in their board init to reference them from the vlayer:
    set "spr&local&_vlayer" to 1
    1. If this is the only step you want to take, there are a few more small things to do. First, make sure the board size, and the dimensions specified by bminx, bmaxx, bminy, and bmaxy, are the same. The easiest way to do this is to set them in the global robot directly based on the board settings:
      set "bminx" to 0
      set "bminy" to 0
      set "bmaxx" to "board_w"
      set "bmaxy" to "board_h"
      You will then need to draw overlay artwork on the areas of the board where the robots will reside (i.e. the top row) and in the top-left and bottom-right corners where the player can be. Finally, to accomodate this, all sprites that might move into these areas (notably the player) need to have this line added to their setup:
      set "spr&local&_overlaid" to 1
      This will ensure that the sprites appear over the overlay you've drawn, with the drawback that you won't be able to draw overlay to appear above them.
  2. Now we need to correct the scrolling. The easiest way to conceptualize this is to create a subroutine for each boundary that resets the viewport to line up with that edge, and then to call those subroutines each time the current scrolling code causes the viewport to cross any boundary. The subroutines will all go at the end the player robot, and will be called conditionally inside of the "#draw" subroutine, right after the spr#_setview line. So for example, the left edge works like this:
    if "scrolledx" < "bminx" then "#fixleft"
    : "#fixleft"
    scrollview position "bminx" "scrolledy"
    goto "#return"
    The right edge requires a little bit more, since we need to take the width of the viewport into account:
    if "scrolledx" > "('bmaxx'-'vw')" then "#fixright"
    : "#fixright"
    scrollview position "('bmaxx'-'vw')" "scrolledy"
    goto "#return"
    The other two edges are basically the same, with different counters in the appropriate places. Of course, the counter vw, and its brother vh (for viewport width and viewport height) need to exist. MZX doesn't have built-in counters to read these values, so we need to set them ourselves, in the global robot one-time code:
    set "vw" to 80
    set "vh" to 25
    This allows them to be easily changed later, in case we decide we want the viewport to be smaller than the full screen. As a final step, so that the player and robots don't have to be covered up with overlay, we want to make sure the playing area for the sprites has at least a character of margin around it, for the player and robots to live in and move around. This means that bminx and bminy should both be changed to 1, at least, and bmaxx and bmaxy should both be at least one less than the width and height of the board.
  3. There will be a minor problem with a visible scroll snap when you move from board to board. This is because the board draws before any of the robots on it run, so the scroll correction code in the player robot will only take effect after the board has been drawn once. To fix this, we'll put some corrective code into the only robot that runs before the screen is drawn: the global robot. The basic idea here is that, similar to how we move the player to the opposite side of the playing field every time we change boards, we'll do the same with the viewport. We'll use "local4" and "local5" as carry-over counters for this purpose. First, in the board init, right after the lockscroll command:
    scrollview position "local4" "local5"
    Then, for each of the :edge# routines, we set these counters, similarly to the way the clamping routines worked in the last step. So for moving north, we want the screen to end up on the bottom edge:
    : "edge0"
    set "local4" to "scrolledx"
    set "local5" to "('bmaxy'-'vh')"
    Similar additions apply to the other routines. Finally, we want local4 and local5 to have values on the first board. We could perform the same complicated scroll correction routine the player robot uses to make sure the viewport is inside the playing area, but that's really a waste of code for something that's only ever going to be used once at the very beginning of the game. So we'll just set them in the one-time code based on the initial scroll location:
    set "local4" to "scrolledx"
    set "local5" to "scrolledy"
  4. The put player command is an interesting one, since it consumes a cycle only if it actually causes the player to move. But this does mean that there will be an extra cycle of delay whenever we move south or east, since then the player DOES have to move to the opposite end of the board. This isn't that important, but as long as we're cleaning up loose ends, we might as well handle this. Similar to the scroll correction we did before, we will set up subroutines to move the real player object to the correct edge of the board, based on the location of the player sprite, and then call them conditionally in a loop. This way, the player will always be at the correct location in advance. So, after the global board setup, instead of just ending the program, create a loop:
    : "playerloop"
    wait for 1
    goto "playerloop"
    Then create the subroutines and if calls. To check if the player is on the left side of the board:
    if "player_x" < "('bminx'+'bmaxx'/2)" then "#playerleft"
    : "#playerleft"
    put player at 0 "playery"
    goto "#return"
    Or for the bottom of the board:
    if "player_y" > "('bminy'+'bmaxy'/2)" then "#playerdown"
    : "#playerdown"
    put player at "playerx" "('board_h'-1)"
    goto "#return"
    The expression used in the comparison is a simple midpoint calculation for the board playing area. All you have to do now is remove the put player commands from the edge handlers, since the player will already be in the right place to move when they're called.
  5. The subroutine system is pretty easy to understand and works just fine, but I personally don't like to use it when I don't have to, just to save on commands. Instead, when I have a situation that involves setting one value to a choice of two or three values depending on some condition, I like to cram all the logic into an expression that will execute in one command. This is purely a preference on my part, it doesn't make the code that much more efficient and it certainly doesn't make it more readable. But it does make it shorter. So in the case of scroll correction, this is what I do:
    set "local2" to "(('scrolledx'<'bminx'*('bminx'-'scrolledx'))+('scrolledx'+'vw'>'bmaxx'*('bmaxx'-'vw'-'scrolledx'))+'scrolledx')"
    set "local3" to "(('scrolledy'<'bminy'*('bminy'-'scrolledy'))+('scrolledy'+'vh'>'bmaxy'*('bmaxy'-'vh'-'scrolledy'))+'scrolledy')"
    scrollview position "local2" "local3"
    This turns what was 16 lines into three; it would be one, but the expressions are so complicated that they won't fit side by side in one line. The first two lines there can also be applied at the beginning of the global robot to local4 and local5, instead of simply "scrolledx" and "scrolledy", to more accurately set the initial location of the viewport.
    Then, for moving the player around, I use this construct:
    put player at "('bminx'+'bmaxx'/2<'player_x'*('board_w'-1))" "('bminy'+'bmaxy'/2<'player_y'*('board_h'-1))"
    This turns those 16 lines into a single line, without the need for any intermediate steps.

This concludes the section on sprites as objects. It got a little sidetracked at the end, but this is all necessary maintenance work if you want to use sprites as objects in a full-fledged game.

Example 2.4 Code

The Sprite-Layer Model: Working With Layers

Understanding sprites as layers is not really that complex a concept to grasp. The basic idea dates back to MZX feature requests for having multiple overlays or underlays in the game, so that you could use different layers to handle different tasks. The feature was rejected as is, but sprites can fill this role just fine. Up till now we've thought of sprites as relatively small objects linked to specific actors in the game. Now, we'll be using sprites as very large objects that can cover the whole screen or even the whole board. Instead of placing a small sprite at a location on the board to change what the player sees, we will paint image data onto a large sprite to accomplish the same thing.

There are a few useful status commands for working with layer-like sprites

set "spr#_vlayer" to 1    Sets the sprite to get its image data from the vlayer instead of the board.
set "spr#_overlaid" to 1  Makes the sprite appear over the overlay. Ideal if using the overlay in a traditional way.
set "spr#_static" to 1    Makes the sprite display relative to the viewport, rather than the board, like a static overlay.

Using the vlayer is especially important for layer sprites. At this point it becomes completely impractical to set aside as much empty space on every board in the game as is necessary to accomodate all of the graphical data used by them. Painting the sprite on top of the overlay can also be useful if you are using the overlay to draw static things on top of the normal game action, like trees or houses, but you want something special to appear above everything, like a status bar or a message box. And very often, it is worth combining this with the static command, so that you don't have to figure out where to redraw the sprite every time the screen moves.

There are also a few important things to consider when working with large sprites, and when combining them with the gameplay engine we're building. The first is that character 32, the space, is not drawn in a sprite, just like it isn't drawn on the overlay. This is pretty essential if you're going to have a sprite that covers the whole screen, space-wise, and still want to see what's beneath it. Another is that sprites have an absolute maximum width and height of 255. If you try to make a sprite larger than that, the practical dimensions will just wrap back around to 0, modulo 256. Because we're using layer sprites as a window into the vlayer, we never actually NEED a sprite to be larger than 80x25 characters, and there are plenty of ways to work with them to stay inside this constraint. But since it's often easier, for a layer that covers the entire board, to just place the layer on the board, we'll assume for the purposes of this tutorial that you won't be using a board or sprites larger than 255 in either dimension.

Finally, one important catch that comes up when we try to use a layer sprite in conjunction with a system that uses spr_yorder to draw game objects correctly, is how to get the layer to appear on top of all the other sprites in the game, or to change its order relative to other layers. After all, a sprite drawn over the whole screen is going to have a y-coordinate lower than any sprite object in the game, right? Fortunately for us, spr_yorder doesn't calculate its draw order based on spr#_y values, but on spr#_cy values. Since we almost never want sprites that are suppose to appear above all game objects to collide with those game objects, this means that we can co-opt this counter as a view order marker. Setting it to extreme negative values will make it appear below every other sprite in the game. Setting it to extreme positive ones will make it appear above everything. It may require some tweaking to get working exactly right when combined with other layers, since the values are still taken as offsets from the sprite's position on the board, but it's a serviceable workaround.

Exercise 3.1: Creating a status bar

For our first foray into layer sprites, let's do something fairly simple to create, but that epitomizes the most basic purpose of layers: a status bar.

  1. Before anything else, we need to draw the base template for the status bar. This can be done mostly however you like, but it should at least have locations to draw health, ammo, and collected keys, and the locations and sizes of the places for drawing to these areas should be recorded as constants in the global robot. Here are the constants and sizes I'll be using:
    set "lyr_s_x" to 0 meaning layer-statusbar-refx
    set "lyr_s_y" to 3 layer-statusbar-refy
    set "lyr_s_w" to 50 layer-statusbar-width
    set "lyr_s_h" to 2 layer-statusbar-height
    set "lyr_sh_x" to 10 layer-statusbar-health-refx
    set "lyr_sh_y" to 3 layer-statusbar-health-refy
    set "lyr_sh_w" to 25 layer-statusbar-health-width
    set "lyr_sh_h" to 1 layer-statusbar-health-height
    set "lyr_sa_x" to 43 layer-statusbar-ammo-refx
    set "lyr_sa_y" to 3 layer-statusbar-ammo-refy
    set "lyr_sa_w" to 5 layer-statusbar-ammo-width
    set "lyr_sa_h" to 1 layer-statusbar-ammo-height
    set "lyr_sk_x" to 8 layer-statusbar-keys-refx
    set "lyr_sk_y" to 4 layer-statusbar-keys-refy
    set "lyr_sk_w" to 40 layer-statusbar-keys-width
    set "lyr_sk_h" to 1 layer-statusbar-keys-height
    The final result should look something like this, 50 characters wide, 2 high, with room for labels and a 2 character margin on each side:
    | Health: =========================  Ammo: 00000 |
    | Keys: ________________________________________ |

    Save it as a new MZM, statusbar.mzm, and then have the global robot load it along with sprites.mzm:

    put "@statusbar.mzm" Image_file p02 at "lyr_s_x" "lyr_s_y"
  2. Now we need a robot to handle drawing our layer. This starts out in the same basic way as all sprite robots have so far:
    set "local" to "robot_id"
    set "local2" to "('vw'-'lyr_s_w'/2)"
    set "local3" to 0
    . "These are the initial x/y coordinates for the sprite, though they'll change later."
    . "The expression for the x coordinate is a midpoint formula that places the sprite in the center of the viewport"
    . "@spr&local&"
    gotoxy "local" 0
    | "justentered"
    restore "justentered" 1
    set "statusbar" to "local"
    set "spr&local&_vlayer" to 1
    set "spr&local&_static" to 1
    set "spr&local&_refx" to "lyr_s_x"
    set "spr&local&_refy" to "lyr_s_y"
    set "spr&local&_width" to "lyr_s_w"
    set "spr&local&_height" to "lyr_s_h"
    set "spr&local&_cx" to 0
    set "spr&local&_cy" to 500
    set "spr&local&_cwidth" to 0
    set "spr&local&_cheight" to 0
    put c?? Sprite "local" at "local2" "local3"
    All of the principles used here, including the new ones like spr&local&_static and using spr&local&_cy to fix the display order, should already be familiar to you if you've been following the tutorial. We set the other collision counters to 0 in order to make sure they're off; remember, this sprite number could have belonged to something else on another board. By the same token, don't forget to add a line to turn off the statusbar sprite on board transitions, in the global robot.
  3. That got the sprite drawing, but now we need it to display something meaningful, instead of just looking pretty. First, let's set up a main program loop:
    : "drawloop"
    put "@statusbar.mzm" Image_file p02 at "lyr_s_x" "lyr_s_y"
    . "The easiest way to redraw the status bar is to reload the base image from the MZM."
    goto "#dohealth"
    goto "#doammo"
    goto "#dokeys"
    wait for 1
    goto "drawloop"
    Then we need to flesh out each of those subroutines. Health is the easiest to do: we need a bar of filled health as long as the space in the status bar, so draw a line of red 25 characters long next to the other sprites, and re-export sprites.mzm. Make sure to add constants for this:
    set "spr_sh_x" to 8 meaning sprite-statusbar-health-refx, for the sprite image to copy onto the layer
    set "spr_sh_y" to 0 sprite-statusbar-health-refy
    To draw the health, we'll simply copy a portion of this bar based on the value of 'health' and 'maxhealth' (you'll have to set maxhealth to something yourself). This leads to:
    : "#dohealth"
    copy block "#&spr_sh_x&" "#&spr_sh_y&" for "('health'*'lyr_sh_w'/'maxhealth')" "lyr_sh_h" to "#&lyr_sh_x&" "#&lyr_sh_y&"
    goto "#return"
  4. Now for the ammo. The easiest way to read this out of a counter and onto the vlayer is to convert the counter into a string. MZX will do this for you without having to write a conversion routine. We would like to write that string directly to the vlayer, but unfortunately MZX doesn't support that operation in a convenient way right now (though it may in the future). We could write it to some off-screen space on the overlay and then copy it, but depending on how you set up the board scrolling in the last section, you may not have any off-screen space. So we'll just have to loop through the string.
    : "#doammo"
    set "$ammo" to "&ammo&"
    . "Ampersands are required, so that we don't just draw 'ammo' on the screen."
    loop start
    set "vch('lyr_sa_x'+'loopcount'),('lyr_sa_y')" to "$ammo.&loopcount&"
    loop for "('$ammo.length'-1)"
    goto "#return"
    Notice that we don't do any checking for the length of the ammo string. We could do this, but it would involve adding some complex expressions that are outside of this tutorial's scope, and is left as an exercise to the reader. Instead, simply take care not to let the player's ammo exceed 99999.
  5. Finally, the keys. Remember that the information for these is not stored in the built-in MZX key handling, but in counters we defined ourselves, 'key_#' for each color. To draw the keys, we'll loop through these counters and test whether they're set or not. This is a fairly complicated routine, and involves keeping track of the key color and the current draw location, and aborting the loop before we draw too many keys to fit on the status bar.
    : "#dokeys"
    set "local4" to 0
    set "local5" to 0
    : "keyloop"
    if "key_&local4&" <= 0 then "keynext"
    loop start
    if "local5" >= "lyr_sk_w" then "#return"
    set "vch('lyr_sk_x'+'local5'),('lyr_sk_y')" to 12
    set "vco('lyr_sk_x'+'local5'),('lyr_sk_y')" to "local4"
    inc "local5" by 1
    loop for "('key_&local4&'-1)"
    : "keynext"
    inc "local4" by 1
    if "local4" < 256 then "keyloop"
    goto "#return"
    Remember that 12 is the character of the key graphic; this could easily be changed. Also understand that this is a fairly inefficient routine, and will take around 1000 commands to execute. We could create some data structures to greatly improve on this time, but that would involve maintenance and a lot of extra effort. Instead, this is the time to go ahead and set the 'commands' counter to something reasonably high, in the global robot.
  6. Two last things before we're done: the status bar necessarily covers up parts of the playing area that we might like to see. There are two things we can do to help with this. One is to move the status bar to the opposite side of the screen if the player gets too close to it. The other is to allow the player to toggle the display on and off. Neither of these is that hard to do, so we'll take them both in order. To move the sprite, create a #draw subroutine in the main loop:
    goto "#draw"
    : "#draw"
    set "local3" to 0
    if "('spr&playersprite&_y'-'scrolledy')" >= "('vh'-'spr_p_h'/2)" then "drawnext"
    set "local3" to "('vh'-'lyr_s_h')"
    : "drawnext"
    put c?? Sprite "local" at "local2" "local3"
    goto "#return"
    Basically, as long as the player remains in the lower half of the screen, the status bar will be on top. If the player moves into the top of the screen, the status bar will move to the bottom.
  7. To turn the status bar on and off, we'll simply use a classic zap/restore toggle with the :keyenter label. This is very basic stuff that should be familiar to most people already. We'll have the sprite start in the off state each time the board initializes. So instead of placing the sprite and going to the draw loop, do this:
    restore "keyenter" 255
    goto "keyenter"
    : "keyenter"
    zap "keyenter" 1
    set "spr&local&_off" to 1
    : "keyenter"
    restore "keyenter" 1
    goto "drawloop"
    Since the status bar is now replacing the default enter menu, make sure to have the global robot turn that off by setting 'enter_menu' to 0.

That does it for our status bar. You'll want to add in some test code and make use of the counter debugger to see that the various elements are reporting their numbers correctly, but you can take this robot and drop it on every screen on which you want to have a status bar.

Example 3.1 Code

Exercise 3.2: Bullets as a layer (framework)

Our next exercise is the most complicated individual piece of the engine yet: adding bullets and shooting to the game. There's no easy way to break it into pieces, but it'll really take at least two exercises to fully explain. So I'll do the best I can and break it into a framework phase, where we'll construct the basic skeleton for the code and explain what each part is going to do, and an implementation phase, where we'll implement each of the main subroutines and explain how they work.

  1. First things first, we need our basic sprite handler robot preamble. You've written this thing several times already, but let's go over it again. We need a one-time execution section that sets a counter based on the robot id, renames the robot based on it, and moves the robot to the robot bank (this part is not strictly necessary but we'll do it anyway):
    set "local" to "robot_id"
    . "@spr&local&"
    gotoxy "local" 0
    We need a separator to prevent this code from happening more than once, and have the code after it happen every time the board loads:
    restore "justentered" 1
    | "justentered"
    And we need some basic sprite initialization and placement:
    set "playerbullets" to "local"
    set "spr&local&_vlayer" to 1
    set "spr&local&_refx" to "lyr_pb_x"
    set "spr&local&_refy" to "lyr_pb_y"
    set "spr&local&_width" to "lyr_pb_w"
    set "spr&local&_height" to "lyr_pb_h"
    put c?? Sprite "local" "bminx" "bminy"
    We'll get to collision in a little bit, since it's going to be done a little bit differently than normal. For now, we want to set up for a layer sprite that's going to fit snugly on top of the board's playing area. And of course we make sure to create a reference counter for the sprite (playerbullets).
  2. Of course we're going to need to set up the global robot to manage some of these counters, which are going to correspond to areas on the vlayer. One thing we're going to need is an actual image for the player's bullets, separate from the layer the bullets get drawn on. So add that to sprites.mzm and note its location, and then add some counters to global:
    set "spr_pb_x" to 9
    set "spr_pb_y" to 0
    set "spr_pb_w" to 1
    set "spr_pb_h" to 1
    set "lyr_pb_x" to 0
    set "lyr_pb_y" to 5
    set "lyr_pb_w" to "('bmaxx'-'bminx')"
    set "lyr_pb_h" to "('bmaxy'-'bminy')"
    Another important thing we're going to need is a blank MZM the size of the layer, to use as a way to clear all of the bullets and redraw them. You could go to the trouble of exporting this by hand, or you could do it the smart way and just have the global robot make it for you. The vlayer consists of empty space by default, so we can just export an appropriately sized piece of it to make a blank MZM:
    copy block at "#&lyr_pb_x&" "#&lyr_pb_y&" for "lyr_pb_w" "lyr_pb_h" to "@blank.mzm" 1
    Finally, we want to make sure the bullet sprite gets turned off between boards, so that its number can be properly reassigned if it changes. Remember where the global robot section that performs this task is?
    set "spr&playerbullets&_off" to 1
  3. Now that we've got that out of the way, we need to figure out how bullets are going to work. The idea is to have them all drawn on a layer that fits over the board, and to keep track of important stats for each bullet in an array. For now these are basically position and velocity, where velocity will be effectively limited to moving in one of the four cardinal directions. Our engine will work in a loop that will:
    1. Add any new bullets to the layer
    2. Move all of the bullets on the layer
    3. Check for bullet collisions with other sprites or walls
    4. Remove all bullets that have collided or moved off the screen
    5. Redraw the layer
    As we've discussed before, the easiest way to tackle something like this is to break it down into component tasks. So we'll make a loop that executes a bunch of subroutines in order:
    : "drawloop"
    goto "#add"
    goto "#move"
    goto "#check"
    goto "#remove"
    goto "#draw"
    wait for 1
    goto "drawloop"
    This loop will change somewhat when we actually implement these routines, but this is the basic idea. Go ahead and add the label stubs for the subroutines, too. For example:
    : "#add"
    . "TODO"
    goto "#return"
  4. One final step: we're going to need to maintain a list of bullets on the layer, and we're going to need an interface to add to and remove from that list. The easiest way to do this is to have a list of bullets to add and another list of bullets to remove, which are processed by each subroutine. The player robot will add bullets to the add list when the player fires, and the bullet robot itself will add them to the remove list when they collide with something or move off the board. The implementation is a little complicated and we'll discuss it in the next exercise, but for now, we should make sure the length of each list is set to 0 each time the board loads, so that we can start fresh.
    set "pbullets" to 0
    set "pbullets_add" to 0
    set "pbullets_remove" to 0

This is all the set up we really need to do. Running the code at this point won't do a single thing, but the framework is now in place for implementation.
Example 3.2 Code

Exercise 3.3: Bullets as a layer (implementation)

Now we come to the meat of the engine. We can't really build this incrementally, but we can take each part individually since we've divided it up into distinct subroutines. So that's just what we'll do.

  1. First, the #add routine. We're going to add a flag to a list (pbullets_add) each time the player fires a bullet, and then this routine will process that list every cycle, adding a set of bullet parameters based on the flag. The flag we're going to use is the ID of the sprite that fired it (i.e. the player), and you might ask why we would even bother with a list at all and not just have a "bulletfired" flag that the player engine sets when it wants to shoot, or even a subroutine of its own to send. The reason is because I prefer to generalize engines whenever possible; this engine in particular will be easy to use to handle multiple bullets from multiple enemies simultaneously with no problem at all. Regardless, at this point we're not really concerned with how bullets get added to the add list, just with processing that list. The list is of the form pbullets_add# and has a length of pbullets_add, and all of these values will be correct at the beginning of the routine. So our subroutine is primarily a loop that processes this list:
    : "#add"
    loop start
    loop for "('pbullets_add'-1)"
    set "pbullets_add" to 0
    goto "#return"
    Notice that we clear the add list once we're done, to prevent from processing the same stuff again next cycle. New bullets will get added on top of the old.
    Now, each value in the list is a sprite ID (all the same ID in fact, but this will be a general case for later). This means we have access to a host of information about a particular sprite, thanks to the object-oriented approach we've been following before with counter names, and the default sprite counters in general. Notably, we have access to a set of coordinates and dimensions for the sprite (to help us decide where to put the bullet), and in the case of the player access to the last direction it moved (to help us decide which way the bullet should be firing). We'll capture this reference out of the list into a temporary local counter, for easier access:
    set "local2" to "pbullets_add&loopcount&"
    We also have a list of bullets containing various stats, which is referenced using the form pbullets#_stat, and is pbullets in length. With zero indexing, this means we can add on to the end of the list by using pbullets as an index. Or more to the point:
    set "pbullets&pbullets&_x" to "('spr&local2&_width'/2 + 'spr&local2&_x' - 'bminx')"
    set "pbullets&pbullets&_y" to "('spr&local2&_height'/2 + 'spr&local2&_y' - 'bminy')"
    The expressions are a bit scary, but the basic idea is to put the bullet at the x/y coordinate of the player sprite, plus an offset to put it into the center of the sprite (half of the dimensions), and minus the offset for the location of the playing area relative to the actual board. The reason for the last part is so that the bullets' x/y coordinates treat the 0,0 as the top-left of the playing area, since that's how the bullet sprite is placed. The velocities are trickier: we want to translate a direction code (0 = north, 1 = south, 2 = west, 3 = east) into an x and y velocity component (we'll stick with -1, 0, and 1 for now, no need to get too fancy). Remember that the counter for this is spr#_lastmove. So the easiest way to do this is with a case structure, as follows:
    goto "#add('spr&local2&_lastmove')"
    : "#add0"
    set "pbullets&pbullets&_vx" to 0
    set "pbullest&pbullets&_vy" to -1
    goto "#return"
    Make sure to add four corresponding subroutines to the end of the program, out of the way of other code execution. For your edification, I've left my two-line expression solution to this in the example code.
    Finally, we need to make sure the length of the bullet list is updated.
    inc "pbullets" by 1
    And we're done with this subroutine. The game can now process a list of bullets and add them to our list.
  2. Next, the move routine. Moving is fairly simple in concept, except for one catch that we'll get to momentarily. The gist is to process each bullet in the bullet list and add its velocity to its current location.
    : "#move"
    loop start
    inc "pbullets&loopcount&_x" by "pbullets&loopcount&_vx"
    inc "pbullets&loopcount&_y" by "pbullets&loopcount&_vy"
    loop for "('pbullets'-1)"
    goto "#return"
    The catch is that bullets can move all the way off the playing field, in which case they need to be removed, or they'll end up being drawn on parts of the vlayer we'd like not to draw on. We could handle this check in the #check routine along with collision, but it's still going to need handling, so we'll just do it here, next to the stuff it relates to.
    if "(('pbullets&loopcount&_x'>=0)a('pbullets&loopcount&_y'>=0)a('pbullets&loopcount&_x'<'lyr_pb_w')a('pbullets&loopcount&_y'<'lyr_pb_h'))" = 1 then "movenext"
    set "pbullets_remove&pbullets_remove&" to "loopcount"
    inc "pbullets_remove" by 1
    : "movenext"
    This block goes right before the end of the loop, after the bullet location counters have been updated. The expression looks long and scary but this is actually an expression usage worth learning, since all it is is a bunch of conditional statements joined together with the AND operator. Basically, if the bullet is still inside the defined playing area, the commands to remove it are skipped. Removal is going to work much the same way adding does, with a list of bullets to remove. Except in this case, the list contains indexes into the main bullet list. In any case, that's all for the movement routine.
  3. If it seems that all we've been doing so far is a bunch of abstract bookkeeping on numbers, the #check routine actually gets us back into the realm of sprites. Here, we'll use the collision functionality provided by sprites to check each bullet and get a collision list for message sending with a minimum of fuss. That means that the collision values need to be set for the layer first, for each bullet:
    : "#check"
    loop start
    set "spr&local&_cx" to "pbullets&loopcount&_x"
    set "spr&local&_cy" to "pbullets&loopcount&_y"
    set "spr&local&_cwidth" to "spr_pb_w"
    set "spr&local&_cheight" to "spr_pb_h"
    loop for "('pbullets'-1)"
    goto "#return"
    This makes sure that we're dealing with a collision rectangle for each bullet on the layer, and can process them in turn. Then, the actual collision is triggered:
    set "spr&local&_clist" to 1
    if "spr_collisions" <= 0 then "checknext"
    set "pbullets_remove&pbullets_remove&" to "loopcount"
    inc "pbullets_remove" by 1
    : "checknext"
    Here, I've decided to do things inline and short-circuit to the end of the loop, since I plan to use that label for something else in a moment. Collision could also be done with a subroutine, of course, this is just how it happened to fall out when I originally wrote the engine. Notice also that we're using the same removal method as before.
    We would be done with this here, except that we also need to do something about the sprite or sprites the bullet is colliding with. This is just our basic collision handler loop, stuck in the middle:
    set "local2" to 0
    : "collide"
    if "spr_clist&local2&" = "playersprite" then "checknext"
    send "spr('spr_clist&local2&')" to "playershot"
    inc "local2" by 1
    if "local2" < "spr_collisions" then "collide"
    Unfortunately we can't use a default loop structure since we're already in the middle of one, so we continue using local2 as a temp counter. Notice in particular the breakout condition for when the bullet is colliding with the player. This is done specifically to deal with the fact that each player bullet starts its life inside of the player, according to our #add routine. We could do this differently, but this is an easy hack to get around that and prevent removing the bullet prematurely.
  4. The #remove routine functions on a list of references to bullets to remove, as we've already seen. This is to avoid the perils of deleting items from the bullet list in place, while we're iterating through it. I have actually thought of an interesting way to do just that without negative side effects, but I'm not sure enough of it yet to use it in this tutorial, so I'm sticking with something I know will work. In any case, removing a bullet from the bullet list will work much the same way as removing a key from the list of key sprites: we grab the bullet from the end of the list and put it into the location of the bullet to be removed. In order to do this without risking serious malfunction though, we need to process the bullet list backwards. That is, we need to remove bullets starting from the same end of the list that we're replacing them from, or we'll risk having a reference to a bullet that is no longer the same, or does not even exist, by the time we've gotten to the end. Since replacing from the front is prohibitively complicated and costly to do, the easiest solution is to process the remove list backwards, since it's already sorted in ascending order.
    : "#remove"
    loop start
    dec "pbullets" by 1
    set "local2" to "pbullets_remove('pbullets_remove'-'loopcount'-1)"
    loop for "('pbullets_remove'-1)"
    goto "#return"
    If you remember how we did the keys, we decrease the size of the list at the beginning so that we can have an easy reference to the end of the list, instead of the end of the list plus one. The numbers don't actually go anywhere, so it doesn't matter if we decrease the length at the beginning or the end of the removal. And as you can see, local2 is set to count backwards through the values in the remove list, which are indexes into the main bullet list.
    set "pbullets&local2&_x" to "pbullets&pbullets&_x"
    set "pbullets&local2&_y" to "pbullets&pbullets&_y"
    set "pbullets&local2&_vx" to "pbullets&pbullets&_vx"
    set "pbullets&local2&_vy" to "pbullets&pbullets&_vy"
    This is the actual removal step, and as before simply involves some counters into others. The only difference here is that there are more of them.
  5. Very near the end now, all we have left is the #draw routine. Drawing is going to take care of all of the things necessary to repaint the layer, based on the numbers we've spent so much time setting up. The first task is to wipe the layer clean with the blank MZM:
    : "#draw"
    put "@blank.mzm" Image_file p02 at "lyr_pb_x" "lyr_pb_y"
    Then, a loop for each of the bullets, to move the bullet image onto the layer:
    loop start
    copy block at "#&spr_pb_x&" "#&spr_pby" for "spr_pb_w" "spr_pb_h" to "#('lyr_pb_x'+'pbullets&loopcount&_x')" "#('lyr_pb_y'+'pbullets&loopcount&_y')"
    loop for "('pbullets'-1)
    Finally, the end of the drawing, which for display purposes involves setting the spr#_cy value so that bullets appear under everything:
    set "spr&local&_cy" to -1
    goto "#return"
    You may wonder what this means for collision, if drawing happens afterwards, and whether collision will happen a cycle late. But the default behavior for sprites is to collide with anything overlapping the collision rectangle, regardless of whether there is actually anything in it. This can be changed with the spr#_ccheck counter so that either character 32 (setting 1) or any empty character (setting 2) does NOT cause a collision, but for our purposes here that's not what we want.
  6. One final step remains, and that's to revisit the main loop that actually calls all of the subroutines. The astute reader will have noticed that the remove routine relies on having a sorted list, which the iterative approach in the move and check routines provides; but that there are two iterations to take into account, one for moving and one for collision checking, and so the list will not necessarily be completely sorted. The simple way around this is to call #remove twice, once after #move and once after #check.
    The other snag is that the nature of the MZX loop structure always performs the loop once, even if there's nothing inside that we want to loop through. This means that in the cases of empty lists, we don't want to call the subroutines at all. Fortunately this is fairly easy to guard against:
    : "drawloop"
    if "pbullets_add" > 0 then "#add"
    if "pbullets" > 0 then "#move"
    if "pbullets_remove" > 0 then "#remove"
    if "pbullets" > 0 then "#check"
    if "pbullets_remove" > 0 then "#remove"
    if "pbullets" > 0 then "#draw"
    wait 1
    goto "drawloop"
    There is, however, one thing that we do want to happen no matter what, and that's for the layer to be cleared and the collision set off the board when the last bullet disappears. With that in mind, grab both of those commands out of the draw routine, and put them in the main loop instead:
    : "drawloop"
    put "@blank.mzm" Image_file p02 at "lyr_pb_x" "lyr_pb_y"
    if "pbullets" > 0 then "#draw"
    set "spr&local&_cy" to -1
    wait for 1
    goto "drawloop"

And that's it. The engine still won't be doing anything, but it's no longer because there isn't any code behind it. Instead, it's because there's nothing using the engine yet. We'll cover that next in the section wrap up.
Example 3.3 Code

Exercise 3.4: Interacting with the rest of the game

After all of that setup, actually integrating the engine into the game is so simple it barely merits an exercise of its own. In fact I'm considering moving all of this back into the framework exercise, since it would fit there and only take up a couple steps. But for now, let's just finish this out.

  1. The first thing we need is a way to let the player shoot something. Going with the standard MZX gameplay mechanic, this means the player robot needs to handle spacebar when an arrow key is pressed.. Simply add this line to each of the movement subroutines (#up, #down, etc.):
    if spacepressed then "shoot"
    Remember that the add routine is going to use the spr#_lastmove counter to determine which way the bullet should fire, so make sure this check happens after that gets set. Then, set up the :shoot branch:
    : "shoot"
    set "pbullets_add&pbullets_add&" to "local"
    inc "pbullets_add" by 1
    goto "#return"
    This short-circuits the rest of the movement routine; the player can't move when spacebar is pressed now, only shoot. Note that we don't have to send the bullet robot any sort of label, we just add an item to the list that we know will get processed.
  2. That gets the bullet engine rolling on the input side, and you can actually use it now and see it in action. Output is even simpler: much like the player engine sends other sprites to :touch labels when it collides with them, the bullet engine sends them a :playershot label. To see this in action, open up your key or door robot and add something like this:
    : "playershot"
    set "local5" to "('local4'%16)"
    * "~&+local5&Ouch, you shot me!"
    The extra step is an easy way to use the door's or key's color for the message, interpolating &+counter& returns a hexadecimal value. Remember that local4 is the color of the key or door. Unfortunately this can't be used with expressions, hence the need for an extra temp counter to extract the foreground color from the background.
  3. Finally, to round it all off, let's actually tie the shooting to the value of the ammo counter. That way, we can watch the ammo count down on the status bar when we shoot, and prevent from shooting when we're out of ammo. Don't underestimate the usefulness of MZX's more specialized commands; here, the take command provides exactly the functionality we need:
    : "shoot"
    take 1 AMMOS else "#return"
    Don't forget to have the global robot give the player some ammo up front, or this will stop you from shooting anything.

And that's all for now. We're starting to have the beginnings of a really solid engine, all it really needs now are some enemies. So we'll focus on that (and some other things) in the next section.

Example 3.4 Code

External Links

Saike's Sprite Tutorial - Slightly out of date with regards to other MZX features.