Sega Genesis Programming Part 9: Object List



Where we left off

After 8 articles we have a sprite that can walk around and bump into things while music is playing. We can even pause the game just in case this is too much action for people to handle. Now let's figure out how we're going to let our sprite interact with their environment.

Let's get one thing out of the way first - in the last article I talked about fixing the sprite draw order in this one. I didn't do that. Perhaps I'll fix it next time, although it's more likely I'll continue to ignore it.

Object list

Before our sprite can interact with things in our little store we need to figure out how we want to model objects. The term "objects" is used to generically refer to anything in the room that isn't purely background scenery. NPCs are included in this umbrella.

The first part is figuring out how to store objects. Let's start by assuming all objects have a rectangle that defines their position and size. The reason is we need the ability to determine if a sprite is either looking at, or maybe even standing on, an object.

Hit test

The map we created is 512x512, even though we're only using 320x224 of it. So we have a couple ways to store an object rectangle:

Not compact:

  Word0=Object ID (0-65535)

  Word1=x0 (0-65535)

  Word2=y0 (0-65535)

  Word3=x1 (0-65535)

  Word4=y1 (0-65535)

Compact:

  Word0=Object ID (0-65535)

  Word1[0-8]=x0 (0-511)

  Word1[9-15]=width (0-127)

  Word2[0-8]=y0 (0-512)

  Word2[9-15]=height (0-127)

The 128x128 object size limit is not a huge issue because the display is 320x224, so one object can cover about a quarter of the screen. Objects that need to span more than that could be split into two objects with the same ID in the (unlikely) event they are needed.

We'll also need to create an array to store all these objects. For the sake of this article, the object list has 20 entries with 10 reserved for items in the store and 10 reserved for NPCs. This is more than we'll use for a while. The reason for having a division between items and NPCs is account for NPC values changing as they move. We don't want to update the object list every time an NPC moves, that's just a waste of instructions. Instead we're going to build the NPC list when the player presses a button to interact with whatever they're looking at. For the sake of convenience we'll reserve a spot in the list for NPCs.

Here are all the new constants created for this:

MEM_ACTION_TARGET_OBJID=$00FF0004C ; action target object id

MEM_OBJECT_LIST_OBJS=$00FF0004E ; list of objects in current map

MEM_OBJECT_LIST_NPCS=$00FF0008A ; list of npcs in current map

[...]

; object list constants

OBJ_LIST_LENGTH=$000A ; max of 10 items in the object list

NPC_LIST_LENGTH=$000A ; max of 10 items in the NPC list

OBJ_LIST_LOOP_CTRL=OBJ_LIST_LENGTH+NPC_LIST_LENGTH-1

OBJ_LIST_STRUCT_SIZE=$0006 ; size of the data structure for object list entries

[...]

; object IDs

OBJ_NOTHING=$00000000

;****************************************

; scenery

;****************************************

OBJ_SCENE_VB_8BIT=$1000

OBJ_SCENE_VB_HARDWARE=$1001

OBJ_SCENE_VB_16BIT=$1002

OBJ_SCENE_VB_MAGS=$1003

OBJ_SCENE_VB_COUNTER=$1004

OBJ_SCENE_VB_REGISTER=$1005

;****************************************

; NPCs

;****************************************

OBJ_NPC_DANI=$2000

Somewhere along the way I decided to name the NPC "Dani" which is short for "Danielle". There's not much of a story behind that name. Since the game is set in 1989 I looked at popular baby names in the 60s and 70s to pick some names that were common around 1968-1972. I don't know where this zany experiment will go, but wherever that is will be a place where people have average names.

Next we need to load the objects when the map is initialized. First let's look at the map and figure out where they should go:

Object layout

The code to load them goes like:

;----------------------------------------

; setup map and object data

;----------------------------------------

InitMap:

[...]

LoadObjectData:

 lea MEM_OBJECT_LIST_OBJS,a0 ; store address of object data

 ;--------------------------------------

 ; word0=Object ID (0-65535)

 ; word1[0-8]=x0 (0-511)

 ; word1[9-15]=width (0-127)

 ; word2[0-8]=y0 (0-512)

 ; word2[9-15]=height (0-127)

 ;--------------------------------------

 move.w #OBJ_SCENE_VB_8BIT,(a0)+

 ; x0=136 width=106 = 1101010 010001000 = D488

 move.w #$D488,(a0)+

 ; y0=136 height=26 = 0011010 010001000 = 3488

 move.w #$3488,(a0)+

 move.w #OBJ_SCENE_VB_HARDWARE,(a0)+

 ; x0=242 width=79 = 1001111 011110010 = 9EF2

 move.w #$9EF2,(a0)+

 ; y0=136 height=26 = 0011010 010001000 = 3488

 move.w #$3488,(a0)+

 move.w #OBJ_SCENE_VB_16BIT,(a0)+

 ; x0=351 width=90 = 1011010 101011111 = B55F

 move.w #$B55F,(a0)+

 ; y0=136 height=26 = 0011010 010001000 = 3488

 move.w #$3488,(a0)+

 move.w #OBJ_SCENE_VB_MAGS,(a0)+

 ; x0=240 width=114 = 1110010 011110000 = E4F0

 move.w #$E4F0,(a0)+

 ; y0=224 height=32 = 0100000 011100000 = 40E0

 move.w #$40E0,(a0)+

 move.w #OBJ_SCENE_VB_COUNTER,(a0)+

 ; x0=240 width=80 = 1010000 011110000 = A0F0

 move.w #$A0F0,(a0)+

 ; y0=200 height=16 = 0010000 011001000 = 20C8

 move.w #$20C8,(a0)+

 move.w #OBJ_SCENE_VB_REGISTER,(a0)+

 ; x0=320 width=16 = 0010000 101000000 = 2140

 move.w #$2140,(a0)+

 ; y0=162 height=32 = 0100000 010100010 = 40A2

 move.w #$40A2,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

LoadNPCData:

 bsr.w BuildNPCObjectList

Let's separate the code to update the NPC list because it needs to be called by another subroutine later on:

BuildNPCObjectList: ; note - d6 and d7 are used by caller

 lea MEM_OBJECT_LIST_NPCS,a0 ; store address of object data

 ;--------------------------------------

 ; word0=Object ID (0-65535)

 ; word1[0-8]=x0 (0-511)

 ; word1[9-15]=width (0-127)

 ; word2[0-8]=y0 (0-512)

 ; word2[9-15]=height (0-127)

 ;--------------------------------------

 move.w #OBJ_NPC_DANI,(a0)+

 move.w (MEM_NPC1_SPRITE_X),d5 ; store x position in d5

 subq #$0008,d5 ; buffer area around sprite

 add.w (MEM_MAP_POSITION_X),d5 ; adjust for map position

 or.w #%0100000000000000,d5 ; append sprite width+buffer to bits [9-15]

 move.w d5,(a0)+ ; store word 1

 move.w (MEM_NPC1_SPRITE_Y),d5 ; store y position in d5

 add.w (MEM_MAP_POSITION_Y),d5 ; adjust for map position

 subq #$0008,d5 ; buffer area around sprite

 or.w #%0110000000000000,d5 ; append sprite height+buffer to bits [9-15]

 move.w d5,(a0)+ ; store word 2

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 move.w #OBJ_NOTHING,(a0)+

 move.w #$0000,(a0)+

 move.w #$0000,(a0)+

 rts

Dialog processing stub

What we need to build is code that:

1) Responds to the A button press: The A button event should fire on the button press not button released. This is how all RPGs on the Genesis work, or at least all that I tried. It's also the recommendation in Sega's documentation that was sent to Genesis developers some 27 years ago.

2) Stops moving the sprites: The A button press should override the direction keys. Control shouldn't be released back to the player until the mode is changed back to exploring.

3) Determines what point on the map the player is looking at: This should be very similar to the code in MoveSprite that determines which cell the sprite is attempting to enter.

4) Checks the object and NPC list to see what, if anything, the player is looking at: This will be a simple brute-force search of the list. If the list was longer we could think about sorting it by something like X or Y position for faster searching.

5) Releases control back to the player when another button is pressed: As usual I'm going to use the original Phantasy Star series as a reference, although most 16-bit RPGs work roughly the same way. In Phantasy Star II the player must press A, B, or C to close a non-interactive dialog. Phantasy Star III improves on this by allowing direction keys to also close a non-interactive dialog. So if you're talking to an NPC and want to move along you can press and hold a direction to close the dialog and start walking. Phantasy Star III gets criticised a lot in comparison to II but there are many things it just plain does better. Anyway, once the dialog is open any button press except Start should close it. Luckily in the last article the pause functionality was added to the beginning of the main game loop and it is handled before entering the code to process dialogs.

Based on how we expect dialogs to work in an RPG, the lifecycle is:

Dialog states

Putting this all into code, we first need to update the main loop to capture the A button press:

MainGameLoop:

[...]

TestDialogOpened: ; test if the player is attempting to open a dialog

 move.b (MEM_CONTROL_PRESSED),d6 ; copy pressed buttons to d6

 andi.w #BUTTON_A_PRESSED,d6 ; test if the A button was pressed

 beq.s MainGameLoopUpdateSprites ; A button is not pressed

 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7

 bset.l #STATE_FLAG_DIALOG,d7 ; set the dialog bit

 move.l d7,(MEM_GAME_STATE) ; copy game state back to d7

 ; clear MEM_CONTROL_PRESSED to prevent dialog state from flipping in loop

 move.w #$0000,(MEM_CONTROL_PRESSED)

 bra.w MainGameLoop ; return to start of game loop

[...]

Now we need a stub for the dialog lifecycle. As you'll notice, there are a number of placeholders here:

ProcessDialog:

 move.w #$2700,sr ; disable interrupts while managing dialogs

 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7

ProcessDialogTestTextBuilt:

 btst.l #STATE_FLAG_DIALOG_TEXT_BUILT,d7 ; test game state

 bne.s ProcessDialogTestOpening ; text is built, move to next test

 bsr.w BuildNPCObjectList ; update the location of NPCs

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

 ; TODO - implement text building

 bset.l #STATE_FLAG_DIALOG_TEXT_BUILT,d7 ; flag that text is built

 bset.l #STATE_FLAG_DIALOG_TEXT_OPENING,d7 ; change state to opening

ProcessDialogTestOpening:

 btst.l #STATE_FLAG_DIALOG_TEXT_OPENING,d7 ; test if the dialog is opening

 bne.s ProcessDialogTestOpen ; dialog is not opening, move to next test

 ; TODO - implement dialog opening animation

 bset.l #STATE_FLAG_DIALOG_TEXT_OPEN,d7 ; change state to open when done

ProcessDialogTestOpen:

 btst.l #STATE_FLAG_DIALOG_TEXT_OPEN,d7 ; test if the dialog is opening

 bne.s ProcessDialogTestClosing ; dialog is not open, move to next test

 ; wait until a button is pressed to clear the dialog

 move.b (MEM_CONTROL_PRESSED),d6 ; copy pressed buttons to d6

 cmpi.w #$0000,d6 ; are any buttons pressed?

 beq.s ExitProcessDialog ; no buttons are pressed, exit

ProcessDialogTestClosing:

 btst.l #STATE_FLAG_DIALOG_TEXT_CLOSING,d7 ; test if the dialog is opening

 bne.s ProcessDialogClearFlags ; dialog is not closing, exit

 ; TODO - implement dialog closing animation

 nop

ProcessDialogClearFlags: ; clear flags when done

 bclr.l #STATE_FLAG_DIALOG,d7

 bclr.l #STATE_FLAG_DIALOG_TEXT_BUILT,d7

 bclr.l #STATE_FLAG_DIALOG_TEXT_CLOSING,d7

ExitProcessDialog:

 move.l d7,(MEM_GAME_STATE) ; save any changes made to the game state

 move.w #$2000,sr ; re-enable interrupts

 rts

The last part is writing code to search for the target object based on what the player sprite is looking at:

FindActionTarget: ; note - d6 and d7 are used by caller

 ; d4 - store map adjusted player sprite x

 move.w (MEM_PLAYER_SPRITE_X),d4 ; move base sprite x

 add.w (MEM_MAP_POSITION_X),d4 ; adjust for map position

 ; d5 - store map adjusted player sprite y

 move.w (MEM_PLAYER_SPRITE_Y),d5 ; move base sprite y

 add.w (MEM_MAP_POSITION_Y),d5 ; adjust for map position

 add.w #SPRITE_COLLISION_Y,d5 ; adjust for collision Y

 ; determine what the player is looking at

 move.w (MEM_PLAYER_SPRITE_DIRECTION),d3 ; copy direction

 cmpi.w #DIRECTION_UP,d3 ; test if sprite is facing up

 bne.s .1 ; branch if not

 subq #SPRITE_COLLISION_UP,d5 ; adjust y

 bra.w .4 ; branch to object list search

.1 ; down

 cmpi.w #DIRECTION_DOWN,d3 ; test if sprite is facing down

 bne.s .2 ; branch if not

 add.w #SPRITE_COLLISION_DOWN,d5 ; adjust y

 bra.w .4 ; branch to object list search

.2 ; left

 cmpi.w #DIRECTION_LEFT,d3 ; test if sprite is facing left

 bne.s .3 ; branch if not

 subq #SPRITE_COLLISION_LEFT,d4 ; adjust x

 bra.w .4 ; branch to object list search

.3 ; right

 cmpi.w #DIRECTION_RIGHT,d3 ; test if sprite is facing right

 bne.s .4 ; not reachable unless there's a bug in MovePlayer

 add.w #SPRITE_COLLISION_RIGHT,d4 ; adjust x

.4

 ; search object list

 lea MEM_OBJECT_LIST_OBJS,a0 ; point a0 to the object list

 move.w #OBJ_LIST_LOOP_CTRL,d3 ; use d3 for loop control

 ;--------------------------------------

 ; hit test = ((htx>=x1)&&(htx<=x0))&&((hty>y0)&&(hty

 ; where

 ; htx = sprite x adjusted for map scroll

 ; hty = sprite y adjusted for map scorll

 ; x0 = right edge of object rect

 ; x1 = left edge of object rect

 ; y0 = bottom edge of object rect

 ; y1 = top edge of object rect

 ;--------------------------------------

FindActionTargetObjectLoop:

 ;--------------------------------------

 ; a0 = word0=Object ID (0-65535)

 ; a0+2 = word1[0-8]=x0 (0-511) word1[9-15]=width (0-127)

 ; a0+4 = word2[0-8]=y0 (0-512) word2[9-15]=height (0-127)

 ;--------------------------------------

 move.w (a0),(MEM_ACTION_TARGET_OBJID) ; copy the current object id

 cmpi.w #OBJ_NOTHING,(MEM_ACTION_TARGET_OBJID) ; looking at nothing?

 beq.w FindActionTargetObjectLoopDbra ; if so loop to next object

 ; test if sprite x is between left and right edge

 move.w (2,a0),d2 ; copy word 1 (x and width)

 move.w d2,d1 ; use d1 for width

 and.w #%0000000111111111,d2 ; clear bits[9-15]

 cmp.w d2,d4 ; (htx>=x1)

 blt.w FindActionTargetObjectLoopDbra ; loop if sprite x < object left

 ; need to shift 9 bits right

 lsr.w #$08,d1 ; shift 8

 lsr.w #$01,d1 ; shift 1 more

 add.w d1,d2 ; add width to left edge to get right edge

 cmp.w d2,d4 ; (htx<=x0)

 bgt.w FindActionTargetObjectLoopDbra ; loop if sprite x > object right

 ; test if sprite y is between top and bottom edge

 move.w (4,a0),d2 ; copy word 2 (y and width)

 move.w d2,d1 ; use d1 for height

 and.w #%0000000111111111,d2 ; clear bits[9-15]

 cmp.w d2,d5 ; (hty>y0)

 blt.w FindActionTargetObjectLoopDbra ; loop if sprite y < object top

 ; need to shift 9 bits right

 lsr.w #$08,d1 ; shift 8

 lsr.w #$01,d1 ; shift 1 more

 add.w d1,d2 ; add height to top edge to get bottom edge

 cmp.w d2,d5 ; (hty

 blt.w ExitFindActionTarget ; if last test passes then we have a hit

FindActionTargetObjectLoopDbra:

 adda.w #OBJ_LIST_STRUCT_SIZE,a0 ; move to next object list entry

 dbra d3,FindActionTargetObjectLoop ; decrement and loop

ActionTargetNotFound:

 move.w #OBJ_NOTHING,(MEM_ACTION_TARGET_OBJID) ; nothing by default

ExitFindActionTarget:

 rts

Since we haven't created the dialogs yet, the only way to see if this works is by checking if MEM_ACTION_TARGET_OBJID ($00FF0004C) is updated with the expected value:

Memory debug

What's next?

The one really big thing that's missing from all this - a dialog that displays what the sprite is looking at. I think I'll save that for next time. I keep trying to bite off small chunks of code at time and dialogs will be the next chunk. Thinking through it there are a few things that need to happen:

1) Creating a font.

2) Some kind of rules to determine which string is displayed based on the object and other game variables.

3) The actual drawing of the dialog. Having them pop-up instantly in one frame would look awful so there needs to be code to expand & collapse them.

Sounds like we have something fun to work on next time...

Download

Download the latest source code on GitHub




Tweet