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




Tweet