VDP Sprite Collision


Still Learning

Sometime recently I decided to continue on with this journey to learn Genesis programming by developing another demo. This isn't an official announcement but it's almost certainly going to be called Retail Clerk '90 and is (obviously) a sequel to Retail Clerk '89.

As I noted last time I'm not trying to make a good demo (mission accomplished by the way), I'm really interested in all the meta stuff around it. When I'm done I hope to have a generic-ish game framework and some code generation tools. If I pull that off then building for multiple platforms is my stretch goal. I haven't decided which platforms exactly, only that they need to be something that was still viable in 1990.

Along the way I'm trying to learn as much about the Sega Genesis hardware as I can. Today I feel like trying out a debatably useless feature - the VDP sprite collision flag. The Genesis VDP has a feature that tells you whether two sprites have collided recently. Technically it's something like "flag that indicates any sprites have non-transparent overlapping pixels" but let's stick with the shorter description.

I assume this is something fairly well documented:

Google results for Sega Genesis sprite collision

I AM SO INCREDIBLY SCREWED!

Note to Google - I am not an expert on anything.

OK, so I guess we'll look into this via some trial and error.

Just to backtrack for a minute... I wrote sprite collision detection a while ago, was unhappy with the results, and removed it. In that article I used a collision map that was updated every time a sprite moved. The final implementation made it kind of hard to move around the map and I scrapped it. Instead I opted for this Persona 5 like experience where you simply walk through anyone in your way. I had not played Persona 5 yet though. The first time I played it was after I came up with the rough story line for Retail Clerk '90. Among the various things I wanted to try was building a very (very) simple bonding system inspired by Trails of Cold Steel / Tokyo Xanadu. I heard those were based off Persona so I gave it a whirl.

It turns out they are not similar games at all. Persona & Trails are both turn-based RPGs with character-specific bonding events and that's about all they have in common. The battle system are completely different, I'm partial to the Trails one but won't argue with people who prefer Persona's. The function and purpose of the bonding systems aren't comparable really. In Trails it's used to explore the backstory of characters you find interesting. There might be some slight stat increases along the way. In Persona the bonding system doubles as a way to gain new abilities. It's a core part of developing your character vs a collection of side-stories. I don't see them as being much alike at all. Tokyo Xanadu has roughly the same setting as Persona 5 (and 1000 other RPGs or anime series) with even less in common otherwise. The extremely crude thing I'm thinking of building is closer to the Trails games.

One thing I found interesting in Persona 5 is how you hear conversations by walking next to people. That's what got me thinking about sprite collision detection again. Like would it be possible for NPCs to say something to you as you walked by or even change direction to face you? Of course it's possible, the question is whether using the VDP collision flag is a good way to do it?

According to Sega's documentation you can query the VDP control port for VDP status. It returns a word with the bits defined as:

As I started poking around with this, I found there's a Schrödinger's cat problem where the value of the VDP control changes after you read it. Let's say that during each vblank you do this:

VDP_CONTROL=$00C00004

MEM_DEBUG_1=$FFFF0000

MEM_DEBUG_2=$FFFF0002

[...]

VBlank:

 move.w (VDP_CONTROL),(MEM_DEBUG_1)

 and.w #%0000001111111111,(MEM_DEBUG_1)

 move.w (VDP_CONTROL),(MEM_DEBUG_2)

 and.w #%0000001111111111,(MEM_DEBUG_2)

You might expect that those consecutive reads produce the same value, they do not. Let's start with a scene where no sprites are colliding:

No sprite collision

This is also your sneak preview of a Retail Clerk '90 lead character (there are two again). Let's see what the VDP values are:

No sprite collision - debug

So after a read it changed from:

000000 10 1000 1000

to:

000000 01 1000 1000

Bits 8 and 9 flipped, why? No clue. Not the point of this article to find out either. Let's see how it looks if two sprites are overlapping:

Sprite collision

And the values are:

Sprite collision - debug

So after a read it changed from:

000000 01 1010 1000

to:

000000 10 1000 1000

Bits 8 and 9 flipped again, in the other direction. The more interesting thing for now is how bit 5 changed after being read. If you're going to rely on this bit to tell you a collision happened you better read it before doing anything else in that frame.

OK, we at least know it's possible to find out if a collision occurred from the VDP. Let's try this thing out.

Putting Things Together

First we'll add a few more sprites to the scene:

Scene with more sprites

Now two constants for memory addresses we'll be using. One to store the VDP status read and another to store the sprite ID the player sprite is overlapping with. The values here are not important.

MEM_VDP_STATUS=$FFFF03C4

MEM_SPRITE_OVERLAP_ID=$FFFF03C6

Two quick noteworthy things:

  1. I only care about the player sprite overlapping with another sprite. I will not be checking for NPC sprite overlaps.
  2. The sprite collision flag is set on every frame where there is a collision (or so it seems). The ID of the last sprite colliding with the player sprite is saved to avoid firing the same event repeatedly.

From here there's a lot of code from Retail Clerk '89 (https://github.com/huguesjohnson/RetailClerk89) that is reusable.

[...]

move.w (VDP_CONTROL),(MEM_VDP_STATUS) ; save vdp status

and.w #%0000000000100000,(MEM_VDP_STATUS) ; clear the bits we don't care about

beq.s CarryOn ; exit if no vdp collision flag

bsr.w BuildNPCObjectList ; update the location of NPCs

bsr.w FindActionTarget ; find the target of the player's action

cmpi.w #OBJ_NOTHING,(MEM_ACTION_TARGET_OBJID) ; is the target nothing?

beq.s NoCollisionActionTarget ; branch if no target

; compare the sprite ids

move.w (MEM_ACTION_TARGET_OBJID),d0

move.w (MEM_SPRITE_OVERLAP_ID),d1

cmp.w d0,d1

beq.s CarryOn ; if they are the same then exit

move.w d0,(MEM_SPRITE_OVERLAP_ID) ; save id of sprite for next time through

; treating this like the player pressed the A button for the purposes of this demo

move.w #ACTION_INTERACT,(MEM_ACTION_ID) ; setup call to ProcessAction

bsr.w ProcessAction ; call ProcessAction

bra.s CarryOn ; exit

NoCollisionActionTarget:

move.w #$0000,(MEM_SPRITE_OVERLAP_ID) ; clear the overlap id

CarryOn:

[...]

Again the code is deceptively small because of things that already work:

Now when the player first hits a sprite there's a dialog. The dialog doesn't display as they pass through. It returns after they stop colliding and start colliding again:

Demo screens of interaction on sprite collision

It works for all the other sprites in the scene too:

More demo screens of interaction on sprite collision

So that's neat. This works about how I expect and took a very small amount of new code to implement. I will probably not use this in the final game demo but it's good to know I can. The behavior demo'd here can be accomplished without ever checking the VDP sprite collision flag. The only way it helps is by indicating whether it's worth checking at all.

What's Next?

I guess if I'm actually serious about making a Retail Clerk '90 I should just do that. I'm still sort of curious about how to support the Team Player and/or 4 Way Play. I have some bad trivia game ideas that I'd like to try someday. So if I write another Genesis programming article, and I'm sure I will, then something about supporting 4 players seems likely. Knowing me, the next one will be something completely different though.




Related