Sega Genesis Programming Part 16: Scripted Events


Scripted events

One of the last capabilities needed for this little demo is scripted event processing. A "scripted event" being a time in the game when the player loses control of their sprite and it, or an NPC sprite, walks along a fixed pattern while potentially saying a few lines of dialog. Just about every RPG does this at some point. The first 18 hours of Final Fantasy IV are mostly this as I recall.

I'm being a little careful here because I don't want to directly rip-off how this was done in Phantasy Star III. I obviously know exactly how it's implemented there since I wrote an editor for it:

Aridia - scripted event editing

General game concepts aren't subject to copyright but specific implementations are. I wrote my code from scratch but have to admit some concepts carried over like storing the script as a series of pairs where one half of the pair is a direction and the other half is a number of steps. There aren't a lot of other ways to tackle this problem.

I suppose most modern games use algorithms like "walk to this point" or "walk to this object". Neither of those would be super-easy to code on the Genesis. OK, "walk to this point" is easy if there are no objects in the way. If you have to figure out how to walk around objects it's a tad more difficult. With the approach I'm using the sprite will walk along a known clear path to a destination. If the player, or another sprite, is in the way we can work around that (this will be explained a little farther down). Freezing all other sprite movement during a scripted event makes life easier of course.

One difference between what I came up with and Phantasy Star III is in my implementation the sprite ID is stored with the scripted event. That might be something I regret later because presumably they had a good reason to not do that. I think it will be helpful if/when I want to extend this to store multiple sprite movements in the same event. The dialog pointers are also handled differently. I am storing the full address of the text location and Phantasy Star III uses relative addresses. Perhaps later on I'll run into a massive problem that makes me realize why they chose that.

Enough talking, let's get on to the implementation...

The code

First off there are a couple new variables. One tracks the address of the scripted event being run. This is incremented each time a step in the event changes. The next variable stores the current action which will either be a direction or value indicating a dialog should be displayed. The third variable is used when the action is a direction, it tracks how many steps the sprite still needs to move. The last variable tracks which sprite is moving.


MEM_SCRIPTED_EVENT_ADDR=$FFFF002C ; pointer to scripted event running
MEM_SCRIPTED_EVENT_ACTION=$FFFF0030 ; current scripted event action
MEM_SCRIPTED_EVENT_STEPS=$FFFF0032 ; steps remaining in current action
MEM_SCRIPTED_SPRITE_ADDR=$FFFF0034 ; which sprite to move

Since we're going to test movement and dialog, we need some dummy text:


DialogTextTestScriptIntro:
 dc.b "Testing scripted event",LF
 dc.b "processing...",ETX
DialogTextTestScript0:
 dc.b "`Some dialog after",LF
 dc.b "walking around a bit.'",ETX
DialogTextTestScript1:
 dc.b "`I guess I'm done",LF
 dc.b "walking for now.'",ETX

The data for the scripted event is, as previously mentioned, just a set of key-values. In this demo the sprite will walk in a couple directions and say a couple lines. I'm keeping it simple just to prove it works:


SCRIPTED_EVENT_END=$FFFF
SCRIPTED_EVENT_DIALOG=$EEEE
[...]
ScriptedEventTest0:
 dc.w $0002 ; NPC0
 dc.w SCRIPTED_EVENT_DIALOG
 dc.l DialogTextTestScriptIntro
 dc.w DIRECTION_DOWN
 dc.w $0008
 dc.w DIRECTION_LEFT
 dc.w $0010
 dc.w SCRIPTED_EVENT_DIALOG
 dc.l DialogTextTestScript0
 dc.w DIRECTION_LEFT
 dc.w $0010
 dc.w DIRECTION_UP
 dc.w $0020
 dc.w SCRIPTED_EVENT_DIALOG
 dc.l DialogTextTestScript1
 dc.w SCRIPTED_EVENT_END

The directions were chosen to avoid colliding with the player sprite. If the player is standing to the left of the NPC the 8 steps down will walk past them. If the player is standing below the NPC, the NPC will appear to stand still for a second then start walking left. In reality they're blocked from walking down and once those 8 blocked steps are complete they move to the next step.

I'm temporarily hijacking the response to talking to the first NPC to test this out:


Day00Scene00Action01: ; ACTION_USE_TALK
 move.l (MEM_DAY_EVENT_FLAGS),d7 ; copy event flags for the day to d7
 move.w (MEM_ACTION_TARGET_OBJID),d6 ; copy action target to d6
 cmpi.w #OBJ_NPC_DANI,d6 ; test target
 bne.s .2 ; branch to next NPC
 ;----------------------------
 ; handle dialog with NPC Dani
 ;----------------------------
 btst.l #$1,d7 ; test if flag 1 is set
 bne .1 ; branch if it is
 bset.l #$1,d7 ; set flag 1
 move.l d7,(MEM_DAY_EVENT_FLAGS) ; save updated event flags for the day
 lea DialogTextDaniScene0Day0Flag0,a6 ; load dialog text
;**************************
; TESTING SCRIPTED MOVEMENT
;**************************
 lea ScriptedEventTest0,a6
 bsr.w QueueScriptedEvent 
 rts
;******************************
; END TESTING SCRIPTED MOVEMENT
;******************************
 bra.s .5 ; branch to setup dialog display
[...]
.5
 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT
ExitDay00Scene00Action01:
 rts

That last snippet calls a new subroutine - QueueScriptedEvent. This sets up everything so the event starts next time the main loop is executed. It does so by setting a state flag, saving the address of the sprite to move, and loading the first action into memory:


; a6 = address of event to queue
; a5 and d7 are modified
QueueScriptedEvent:
 ;---------------
 ; set state flag
 ;---------------
 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7
 bset.l #STATE_FLAG_SCRIPTED_EVENT,d7 ; set the scripted event flag
 move.l d7,(MEM_GAME_STATE) ; save it back
 ;---------------------------------------
 ; determine which sprite this applies to
 ;---------------------------------------
 move.w (a6)+,d7 ; copy to d7 for next operations
 cmpi.w #$0000,d7 ; this the player sprite?
 bne.s .1 ; branch if not player
 lea MEM_PLAYER_SPRITE_ID,a5 ; point a5 to player
 bra.s .2 ; branch
.1
 subq #$0002,d7 ; decrement to account for 0 indexing & sprite zero
 lea MEM_NPC0_SPRITE_ID,a5 ; point a5 to first NPC 
 mulu.w #NPC_RECORD_SIZE,d7 ; multiply to get offset
 adda.w d7,a5 ; move to NPC being modified
.2
 move.l a5,(MEM_SCRIPTED_SPRITE_ADDR) ; save address
 ;---------------------
 ; setup scripted event
 ;---------------------
 move.w (a6)+,(MEM_SCRIPTED_EVENT_ACTION) ; save first action
 move.l a6,(MEM_SCRIPTED_EVENT_ADDR) ; save address of first step
 ; set steps to 0 to trigger next action start in main loop
 move.w #$0000,(MEM_SCRIPTED_EVENT_STEPS)
ExitQueueScriptedEvent:
 rts

In the main loop we now need to test if a scripted event is running. If so we need to determine whether to start the first/the next step in the event or continue running the current one. Player input is only read when the dialog is displaying (except for pause/unpause which trumps all other game states).


[...]
TestScriptedEvent:
 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7
 btst.l #STATE_FLAG_SCRIPTED_EVENT,d7 ; test game state
 beq.w TestDialog ; not in a scripted event, move to next test
 btst.l #STATE_FLAG_DIALOG,d7 ; test if we need to wait for a dialog
 bne.w TestDialogUpdateFrequency ; wait for dialog interaction to finish
 ;-------------------------------------------------------------------
 ; dialog is not displaying, test if it is time to start a new action
 ;-------------------------------------------------------------------
 cmpi.w #$0000,(MEM_SCRIPTED_EVENT_STEPS) ; test if 0
 beq.s NextScriptedEventAction ; if 0 then start next action
 ;---------------------------
 ; continue moving the sprite
 ;---------------------------
 cmpi.w #SPRITE_MOVE_FREQUENCY,(MEM_FRAME_COUNTER); is it time to move?
 blt.w MainGameLoopEnd ; exit if it's not time to move
 move.w #$0000,(MEM_FRAME_COUNTER) ; reset counter to 0
ScriptedEventMoveSprite:
 subq #$1,(MEM_SCRIPTED_EVENT_STEPS) ; decrement step counter
 move.l (MEM_SCRIPTED_SPRITE_ADDR),a6 ; point a6 to sprite to move
 bsr.w MoveSprite ; branch to MoveSprite
 bra.w MainGameLoop ; return to start of game loop
NextScriptedEventAction:
 move.l (MEM_SCRIPTED_SPRITE_ADDR),a6 ; point a6 to sprite
 bsr.w StopSprite ; stop moving the sprite
 move.l (MEM_SCRIPTED_EVENT_ADDR),a6 ; point a6 to next step
 move.w (MEM_SCRIPTED_EVENT_ACTION),d7 ; copy action to d7
 cmpi.w #SCRIPTED_EVENT_END,d7 ; are we at the end?
 beq.s ScriptedEventEnd ; if so branch
 cmpi.w #SCRIPTED_EVENT_DIALOG,d7 ; display a dialog?
 beq.s ScriptedEventDialog ; if so branch
 ;------------------------
 ; start moving the sprite
 ;------------------------
 move.l (MEM_SCRIPTED_SPRITE_ADDR),a5 ; point a5 to sprite to update
 ; copy direction to sprite direction 
 adda.w #STRUCT_SPRITE_DIRECTION,a5 ; move to direction
 move.w (MEM_SCRIPTED_EVENT_ACTION),(a5) ; copy action to direction
 move.w (a6)+,(MEM_SCRIPTED_EVENT_STEPS) ; copy steps in action
 ; save state of scripted event and loop back to main
 move.w (a6)+,(MEM_SCRIPTED_EVENT_ACTION) ; save next action
 move.l a6,(MEM_SCRIPTED_EVENT_ADDR) ; save address of next step
 bra.w MainGameLoop ; return to start of game loop
ScriptedEventEnd:
 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7
 bclr.l #STATE_FLAG_SCRIPTED_EVENT,d7 ; clear the scripted event flag
 move.l d7,(MEM_GAME_STATE) ; save it back
 bra.w MainGameLoop ; return to start of game loop
ScriptedEventDialog:
 move.l (a6)+,(MEM_DIALOG_TEXT) ; copy value at a6 to MEM_DIALOG_TEXT
 move.w (a6)+,(MEM_SCRIPTED_EVENT_ACTION) ; save next action
 move.l a6,(MEM_SCRIPTED_EVENT_ADDR) ; save address of next step
 move.w #$0000,(MEM_SCRIPTED_EVENT_STEPS) ; reset step counter
 ; set dialog flags to display the dialog
 move.l (MEM_DIALOG_FLAGS),d7 ; copy current dialog state to d7
 bset.l #DIALOG_FLAG_TEXT_OPENING,d7 ; change state to opening
 bset.l #DIALOG_FLAG_STYLE_SIMPLE_TEXT,d7 ; set style to overworld menu
 move.l d7,(MEM_DIALOG_FLAGS) ; save changes made to the game state
 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
 bra.w MainGameLoop ; return to start of game loop
[...]

This was not much code in the end. I was sort of expecting it to be a little bigger. Maybe it doesn't work? Let's find out...

Results

First the player talks to the NPC and a dialog is displayed:

Step 1 - dialog

Then the NPC starts walking left:

Step 2 - walking

Now they talk again, accidentally covering themselves in the process:

Step 3 - second dialog

After that dialog closes they resume walking:

Step 4 - more walking

Then they say their last line and stop walking:

Step 5 - ending dialog

So there it is, one of the last pieces needed for this short demo idea.

What's next?

Looking at the "plan" from four articles ago:

1) Dialog between characters - meaning an NPC says something to you and you respond. Maybe you respond with more dialog, maybe you give them an item. That leads to...

2) Inventory management - along with basic stuff like taking and giving items.

3) Game event tracking - we need a way to say "if [X] happened and you talk to character [Y] then do [Z]", I'll probably over-complicate this. I won't call this 100% complete but it's functional enough.

4) Scripted sprite movements - if the ultimate goal is to get a customer to leave a store then there needs to be an animation where they walk away.

5) Adding & removing NPCs from the current scene - the one NPC is hard-coded, there needs to be a way to move NPCs in and out of scenes. The removing part hasn't been needed yet but it's small. I should have done it in this article but didn't.

6) To make this look like a real demo I should really create a title screen and ending message.

7) Dozens of things I didn't think of that will all be painful to work out.

With all the basic mechanics minimally functional this will be my last article for a while. Scripting out the rest of the demo won't be too rough but it will take me some time to create new artwork & music for a title screen. Plus I think I want to add one more NPC to the mix which means more art. None of these additions warrant a new article.

Once this demo is behind me I'll (probably) go to work on adding new scenes and characters. It seems like a good time to start a project page for this on my site too which will also be time consuming. Part 17 of this series, if there is one, won't appear until there's a new feature I need to add. I'd like to add a full-screen menu but that's really just an expansion on the dialog idea so that won't be next. Eventually I'll likely need data compression for images and collision data. That seems article-worthy. If I get around to adding a password or save feature that would also warrant a new article. Both of these are quite a while away. Given the amount of free time I have to work on this it could easily be a year.

It's possible that I'll write another Genesis programming article that's completely unrelated to this one. Like one day when I'm suffering from a poor attention span I try to figure out some random thing. There's also a small chance that I'll post a tutorial explaining how to do various visual effects I accidentally implemented over the past year.

So until next time, thanks for reading these. I hope they've been a little educational for you, they definitely have been for me.

Download

Download the latest source code on GitHub




Related