Programming tutorial: Part 10–Collisions

Atari Lynx programming tutorial series:

The 10th part in the tutorial series on Atari Lynx programming will go into collisions with sprites. For collisions we need to look at the basics to know how collision detection works, how to use it, but also at pens used for drawing as this impacts which parts of a sprite can collide.

Collision basics

The Lynx has some excellent collision detection support. The detection process will allow you to check whether sprites have collided with each other. This process is not enabled by default as it incurs additional overhead. The detection is implemented in the hardware of the Lynx. All you need to do is use the features, which we’ll discuss, and the device is going to do the hard work for you. The hard stuff you need to do is understand the way it all works and check the collisions.

Essentially, there are two ways to detect a collision of a sprite with another sprite. The first one is the easiest one. Every Sprite Control Block (SCB) can have a special byte indicating the sprite it has collided with. When you check this byte, you can tell whether a collision has occurred. The second way is to do manual checking at pixel level in a special collision buffer that represents the entire screen. This buffer is also maintained by the hardware, but offers no simple support for checking collisions. We will go into details on the first detection method first.

The collision depository, … say what?

The elevator version of the collision detection mechanism is this:

“Every sprite has a number that identifies it. When drawing sprites the hardware will remember where the sprite is drawn and what its collision number is. Sprites that are drawn later will be checked to collide with previously drawn sprites. If they do the later drawn sprite will indicate the sprite(s) it collided with by holding the highest collision number in its collision depository.”

Let’s run this again from the top.

In the required fields of a SCB you will find three bytes at the beginning, SPRCTL0, SPRCTL1 and SPRCOLL. This last one is used for collision detection. The byte SPRCOLL (for Sprite Collision I guess) is composed of two parts: the upper and lower nibble.

image

The lower nibble of bits 0 to 3 represents the collision number of the sprite. Every sprite has it and can use it. It is only relevant when the sprite is enabled for collision. That is controlled by bit 5, that controls whether this sprite will collide. You can turn collision detection for a sprite off by setting bit 5 to one (1). There is a define for this bit called NO_COLLIDE = 0x20. Bit 5 is the only significant bit of the upper nibble: the other bits need to be zero and don’t have meaning.

The next part in the things for collisions is the collision depository, also part of the SCB. This is a byte that holds the collision number of whatever other sprite the sprite defined in the SCB has collided with. In particular it holds the highest collision number of all previously drawn sprites it collided with.

The collision depository (or depository for short) is located at a relative position from the start of the SCB. It has no fixed position. It could be located anywhere in the SCB, e.g. after the fixed fields (from SPRCTL0 to VPOS) and optional fields. But, … because the relative location from the start of the SCB is the same for all SCBs it is unlikely it is located at the end of the fields. That would mean you need to have the same SCB structure for all your sprites, which is limiting or inefficient in most cases. The most common position for the depository is just before the required fields, at a relative position of –1 from the start of the SCB.

image

In the picture to the left you can see the required (blue) fields of a SCB and the optional ones (yellow). I am sure you can see how using more or less fields from SCB to SCB will make it impossible to specify a single relative value for the location of the depository. If not, assume we would set it to an offset of 11. That means the depository is located after SPRVSIZE. It also implies that the STRETCH field and following fields cannot be used or they will be overwritten. That excludes the use of collideable SCBs that use stretch, tilt and/or palette. OK, now assume we would set it to 18, right after the palette. Every SCB will need to allocate all bytes including the palette, whether they are used or not. That wastes quite a number of bytes, especially if the number of SCBs is significant. So, it is best to put it at the front of the SCB like the light blue field in the picture indicates. This is accomplished by the relative position of –1.

Turning on collision detection

Before anything will happen, you must turn on collision detection during the initialization of your game. Actually, a lot is already done by the initialization of the TGI driver.

The initialization will make sure the collision buffer is allocated, the proper values in relevant memory addresses are set and that the relative offset for sprite depositories is –1. In the next tutorial part you are going to go in depth on collision buffers.

All that is left is a call to a TGI function called tgi_setcollisiondetection to do the trick. You need to pass 1 (TRUE) to set collision detection on.

tgi_setcollisiondetection(1);

The function will turn on collision detection and do a little more. We will look at the details in the tutorial on pens.

After the preparation you are ready to start drawing sprites that can collide.

Drawing collideable sprites

You also need to prepare your SCB to use the collision depository. This requires a bit of work. Do you recognize this C struct?

typedef struct SCB_REHV_PAL {
  unsigned char sprctl0;
 
unsigned char
sprctl1;
 
unsigned char
sprcoll;
 
char
*next;
 
unsigned char
*data;
 
signed int
hpos;
 
signed int
vpos;
 
unsigned int
hsize;
 
unsigned int
vsize;
  unsigned char penpal[8];
} SCB_REHV_PAL;

It is the standard struct for a sizable sprite that is defined in _suzy.h. You should declare the following struct in your code:

typedef struct {
  byte depository;
  SCB_REHV_PAL scb;
} sprite_collideable;

The depository byte is located right before the sizable sprite structure in memory. You can declare a sprite like this:

sprite_collideable robot =  {
  0x0,  {
    BPP_4 | TYPE_NORMAL, REHV,
    0x01,
    0, robot,
    20, 50, 0x0100, 0x0100,
    { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }
  }
};

Notice the 0x0 value for the depository at the beginning, followed by the rest of the SCB data that you are probably familiar with by now. The value in bold for the SPRCOLL defines the collision value for this sprite.

The way you use this new structure is as follows:

tgi_sprite(&robot.scb);
itoa(robot.depository, text, 10);

Pay attention to the scb member of the collideable struct in bold. It is needed because the actual “normal” SCB data is located from that address.

After drawing your sprite you can evaluate the depository to check whether it has collided. The depository field of our composed struct contains the collision number of the sprite it may have collided with.

Putting it into practice

Let’s put all of this together in a sample. Imagine two walls that should not be touched by our hero, the robot.

image

The sample code will show which wall the robot collided with. The robot can move around and run into walls to see what happens. Here are some spectacular moments:

image
No collision yet. But this is about to go bad!
image
Ouch. Touched the wall and electric death is certain.
image
Oh no, two ways to die. But wall 8 is the winner here, because it is the highest collision number.

There is not a whole lot to do to make this work. Here are some steps that are required to get results:

  1. Turn on collision detection during initialization of your game

    That’s the easy part. All you need to do is call the TGI function we saw earlier: tgi_setcollisiondetection(1);

  2. Create two wall SCBs with a collision number of 8 and 6 respectively
    Even though you do not need to detect collision of the walls against other sprites themselves, they should be collideable sprite structures. Otherwise the wall sprites are not going to participate in the detection process. You are going to have to declare the sprites with a collision depository to avoid corrupting your memory.

    sprite_collideable wall =
    {
      0x0,
      {

        BPP_4 | TYPE_NORMAL,
       
    REHV, 
       
    0x8,
        0,
        singlepixel_data,
        50, 20, 0x0a00, 0x1a00,
        { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }
      }
    };
    This defines a wall with collision value 8. A similar SCB with 0x06 at the sprcoll value of the SCB will create a wall with a collision value of 6.
     
  3. Create a SCB for the robot and make it collideable as well.
    You saw the SCB definition earlier. It has a collision value of 0x01;
     
  4. Draw the walls first
    The drawing order is important as collisions can only be detected against previously drawn sprites. This goes as usual:

    tgi_sprite(&wall.scb); tgi_sprite(&wall2.scb);
  5. Draw the robot
    Because we already drew other objects in step 3, this will allow us to check for those objects that the robot may have collided with.

    tgi_sprite(&robot.scb);

  6. Check the collision depository of the robot
    The robot is drawn last, so its collision depository will be filled with the collision number of the sprites drawn earlier overlapping.

    if
    (robot.depository > 0) { tgi_outtextxy(0, 90, "Ouch!"); itoa(robot.depository, text, 10); tgi_outtextxy(50, 90, text); }

Moving sprites

So, the hardware is able to remember sprites it has previously drawn. But, for how long does it remember?

That’s up to you. The underlying collision buffer holds the sprites’ collision numbers until it gets reset to all zeros. You accomplish that with a call to tgi_clear. The latest versions of CC65 have a tgi_clear implementation that is collision detection aware. That means that whenever you call tgi_clear it will not only clear the screen, but also the collision buffer. It implies that the previously drawn sprites are “forgotten” and the detection starts fresh again.

Typically you will call tgi_clear at the beginning of your draw routine, so you can start afresh with both visuals and collisions. There are more advanced scenarios, where you would want only a partial screen and/or collision buffer clearing. We’ll get into the specifics in the next part.

For moving sprites, such as bullets you will definitely want to clear the collision buffer, so the depositories of the sprites will pick up current sprites, not old positions of danger objects, walls and what have you. That would make you die from bullets in old locations.

Selecting collision numbers

Because you have 4 bits to specify a collision number you can (only) pick values from 0 to 15. That probably means you cannot have a unique number per sprite, unless you have a small number of sprites. so, you need to carefully think about how to use the numbers for your sprites. Here are some ideas and hints:

  • Use a unique number per sprite type
  • Assign numbers according to visual depth. This allows for the creation of layers or planes in your game
  • Give a more important sprite(-type) a higher number, because only the highest will be remembered. Also, if a sprite collides with multiple other sprites, it will get the highest number as well.
  • Decide on who collides with who. You can have the hero collide with bullets, or the bullets collide with other things (hero, walls) or both.

Remember that there are other collision mechanisms, such as the use of hit boxes or evaluating the coordinates of sprites. This might be as useful as the hardware collision detection. The hardware helps you out at a price: more memory is allocated for the collision buffer and the drawing of sprites is slower than without collision detection. However, it does give you pixel precise collisions and your game might need it. Of course you can combine these techniques of coordinates, bounding box checks and hardware collisions.

Next time

The next part will take us deeper into the internals of collision detection. Right now you have seen the magic trick, but you will learn how it is all done.

Advertisements
This entry was posted in Tutorial. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s