Sprite (Tutorial)
Sprites were introduced to MegaZeux in version 2.65, primarily as a method of making it easier to implement large object representations in the game (for example, engines for handling a multiple character player or enemies). They include many features such as collision detection, easy drawing, and configurable draw order. Despite being designed to make coding easier, many MZXers avoid their use in favor of more traditional methods such as overlay buffering and hand-rolled collision routines. This is largely out of a perception that Sprites are difficult to use.
Sprite Tutorial
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.
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.
- 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.
- 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.
- 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
- Now we need a control loop that draws the sprite and handles input.
- : "drawloop"
- wait 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.
- 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".
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.
- 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 1
- put c?? Sprite "local" at "local2" "local3"
- 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".
- 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"
This leaves us with this final code:
lockplayer 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 1 put c?? Sprite "local" at "local2" "local3" if uppressed then "#up" if leftpressed then "#left" if rightpressed then "#right" if downpressed then "#down" goto "drawloop" : "#up" if "local3" <= 0 then "#return" dec "local3" by 1 goto "#return" : "#down" if "local3" >= "(25-'spr&local&_height')" then "#return" inc "local3" by 1 goto "#return" : "#left" if "local2" <= 0 then "#return" dec "local2" by 1 goto "#return" : "#right" if "local2" <= "(80-'spr&local&_width')" then "#return" inc "local2" by 1 goto "#return"
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 2: Adding basic collision detection
- 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.
- 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
- 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"
That's it! That's really all you have to do!
lockplayer 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 set "spr&local&_cx" to 0 set "spr&local&_cy" to 2 set "spr&local&_cwidth" to 3 set "spr&local&_cheight" to 1 : "drawloop" wait 1 put c?? Sprite "local" at "local2" "local3" if uppressed then "#up" if leftpressed then "#left" if rightpressed then "#right" if downpressed then "#down" goto "drawloop" : "#up" if "local3" <= 0 then "#return" if c?? Sprite_colliding "local" at 0 -1 then "#return" dec "local3" by 1 goto "#return" : "#down" if "local3" >= "(25-'spr&local&_height')" then "#return" if c?? Sprite_colliding "local" at 0 1 then "#return" inc "local3" by 1 goto "#return" : "#left" if "local2" <= 0 then "#return" if c?? Sprite_colliding "local" at -1 0 then "#return" dec "local2" by 1 goto "#return" : "#right" if "local2" <= "(80-'spr&local&_width')" then "#return" if c?? Sprite_colliding "local" at 1 0 then "#return" inc "local2" by 1 goto "#return"
(More to come...)
External Links
Saike's Sprite Tutorial - Slightly out of date with regards to other MZX features.