The Enigma Of Cutscenes

Post Reply
Bucket Mouse
Posts: 88
Joined: Wed Mar 07, 2018 2:25 am

The Enigma Of Cutscenes

Post by Bucket Mouse » Tue Apr 03, 2018 8:00 am

It appears in just a few short days I'll have access to the NES Maker beta. Though the proper introduction would be to take things slowly and work within the limits of its capabilities.....I know myself, and it won't be long before my impatient self tries to figure out how to make cutscenes.

When I say cutscenes I don't mean giant still drawings accompanied with text -- graphical data is at a premium and you can only have a scarce few of those, best saved for the opening and ending scenes. Also, the tool to make them is already in NES Maker anyway. Instead of dedicated drawings, many games in the NES's era told their narratives by creating scenes where the sprites moved and changed position on their own, speaking with text boxes.

I've brought this up several times with the NES Maker Makers and they've implied they have no interest in automating cutscenes, and that it won't be a feature unless someone else figures it out and creates some kind of plug-in.

Great. Assembly is totally foreign to me. Everything I know about programming, I learned in BASIC. This is how I would set up a cutscene to trigger itself in that language:

CUT=1
First I would set up a variable and turn it on at the appropriate point. This would appear in the code after the player performed a certain unrepeatable action.

IF CUT=1 THEN GOSUB 65000
Then I would set up a door or a tile with this command. If this flag is up when the player steps on the tile or opens the door, the program subroutines to where the cutscene data is.

X=20: FOR A=1 TO 15: X=X+1: DRAW (SPRITE1) AT 30,X: XDRAW (SPRITE A) AT 30,X: NEXT A
Here is how I would make the sprites move in the cutscene. X is the variable representing the vertical coordinate. I create a loop with A that repeats itself 15 times. Outside of the loop I define X as 20. Within the loop the program adds 1 to that and continues to add 1 with each loop-around, making the sprite move down 15 pixels by itself.

I already know at least one of these things is different on NES. On the Apple II you had to undraw a sprite from a frame before you drew it again. That creates a flickering effect, but in BASIC it was the only way. (You could code in Assembly on Apple to avoid that -- it used the same 6502 processor that the NES did.)

As for the text....simple. "PRINT "BLAH BLAH BLAH DIALOGUE." That was easy with BASIC.

CUT=0: RETURN

After the scene finishes, the final bit of code before leaving the subroutine sets CUT back to 0, ensuring the scene will not repeat every time the player steps on that tile or opens that door.

****

So....how do I translate this to something the NES can understand? After hours of poring over Nerdy Nights, I'm going to take a stab at cobbling it together. I'm under no delusion that any of this is correct. You can tell me where I've got it wrong.

First off....how to set the flag.

LDA #$07

This flag should only go up once in the game. I'm not sure yet how to make flags unrepeatable in Assembly.
It's my understanding that the digit being loaded here could be anything because it's intended as a flag. I've made it a 7.

STA $802C

It won't be long before that byte of memory is rewritten with something else, so we should save it. I'll stick it here and remember that address later.

Now the player has opened the door, or stepped on the tile. We need an IF-THEN-ELSE argument.

LDA $802C

Before we begin let's load the number back in.

CMP #$07

CMP means "Compare," assigned to a value of 7. If A equals 7.....

BEQ $FF00

BEQ means "Branch If Equal." CMP and BEQ are used together. If A equals 7 THEN jump to $FF00, the address in the ROM where the cutscene data is.

Like we did in BASIC, the last thing we should do in that data is establish a value of 0 at $802C. The next time the player performs the same action, the game will read that address and not get a 7, thereby refusing to trigger the cutscene.

But how to make the cutscene itself? This requires more thought, and it is getting late.

In the meantime, someone who knows Assembly better than me can point out my mistakes and suggest corrections.
User avatar
Kasumi
Posts: 99
Joined: Fri Mar 09, 2018 11:13 pm

Re: The Enigma Of Cutscenes

Post by Kasumi » Tue Apr 03, 2018 11:19 am

Some good news. You can name stuff.

lda #$07? How about
CUTSCENEFLAG = 7

Then in your code you can just do
lda #CUTSCENEFLAG
and
CMP #CUTSCENEFLAG


BEQ $FF00? How about:
BEQ cutscenestart

lda $802C?
How about
cutsceneState = $FF
and then
lda cutsceneState?

$802C can't be RAM. $8000-$FFFF are ROM in mapper 30. (And writing to $802C would cause a "bank switch" which you probably do not want to do without some sort of NES Maker built in subroutine.) You'd actually also want to be careful with something like cutsceneState = $FF because NES Maker might use that RAM. I'm sure they'll cover how to avoid using the same RAM as NES Maker.
I'm not sure yet how to make flags unrepeatable in Assembly.
You make sure the CPU never hits the code that initializes it again.

Code: Select all

lda #CUTSCENEFLAG
sta cutsceneState
cutscenestart:
;Code here
jmp cutscenestart
That code sets cutsceneState (which is a byte of RAM) to CUTSCENEFLAG. Then it continues down (as code often does). The jmp tells the CPU to continue from cutscenestart. Since cutscenestart is below lda #CUTSCENEFLAG and sta cutsceneState, those instructions aren't run again.

As far as how to make a loop in ASM:

Code: Select all

ldx #7;Load X (or Y) with the number of iterations
looplabel:;Where the loop should restart. Like the example cutscenestart code above, 
;you don't want to reload X during the loop, so we put the label below it.

;Now comes the code for the thing you want to do 7 times
lda source,x;We load a value to copy.
sta destination,x;And store it.
dex;This subtracts one from X
bne looplabel;If the result of the above subtract was not zero, we branch (or jump) to looplabel.
So...
X is 7. load source, store destination. subtract one from X. X is now 6. 6 isn't 0. Go to looplabel.
X is 6. load, store, subtract. X is now 5. 5 isn't 0. Go to looplabel.
X is 5. load, store, subtract. X is now 4. 4 isn't 0. Go to looplabel.
X is 4. load, store, subtract. X is now 3. 3 isn't 0. Go to looplabel.
X is 3. load, store, subtract. X is now 2. 2 isn't 0. Go to looplabel.
X is 2. load, store, subtract. X is now 1. 1 isn't 0. Go to looplabel.
X is 1. load, store, subtract. X is now 0. 0 is 0, so continue downward. (Don't branch)

You can also count upwards.

Code: Select all

ldx #1;Where you want to start.
looplabel2:
lda source,x
sta destination,x
inx;Adds one to X. The opposite of dex.
cpx #8
bne looplabel
Note that it's cpx #8, not cpx #7. Because the cpx is directly after the inx, if we compare X to 7 to stop, we never end up doing 7!

I have no idea how NES Maker is going to work. In my game engine, I have levels and objects in the levels. (An object can be an enemy, or the player, or the camera. Anything interactable, basically.) My level load code will automatically load any object in the level at the starting position. All objects have code that is run every frame and they all have some amount of RAM allocated to them.

So I'd create a new level for my cutscene. I'd place an object in it that controls the cutscene, and then all the code I need is added to the cutscene object. Say I want to steal control of the player for the cutscene. I would make my cutscene object set RAM just like you described. Then the player object would compare that RAM to a certain value. If it's that value, it means the player can't control the character. Instead the cutscene object should. So the cutscene object would say... count down a timer in its own RAM for 15 frames (like your example) and feed joypad movement to the player. Then, it would set the cutscene RAM the player checks to something that would give control back to the player and destroy itself.

Indivisible doesn't have too much in the way of cutscenes, but at the end of the game (when the boss is beaten, and she's running toward the exit) an object sets RAM that forces her to move right (and also makes it impossible for the player to make her do anything else like jump) much like I've described here.

In fact, this is what the code looks like (with an edit or two for clarity):

Code: Select all

	jsr isAjnaCloseEnough;Check if Ajna is close enough to trigger the end of the game
	bcc roti.toscared;If she is, start us running away scared, and trigger the end game cutscene
	jmp roti.collisionloop;Otherwise, go here instead (which basically ejects roti from walls)
roti.toscared:
	lda ajnadashextra
	ora #%10000000;Force Ajna to Heruka Dash by setting the highest bit of this RAM
	sta ajnadashextra
	
	jmp roti.startrun;Not shown, but code that makes the tapir start running
And then in Ajna's (the player object's) code I have

Code: Select all

lda ajnadashextra
bpl ajna.normal;If the highest bit of the RAM is not set, Ajna should be controlled as normal
;If here, Ajna should be forced into the Heruka Dash
;The code that does this is a bit complicated (checks for death and such as well), so it's not shown
ajna.normal:
You can see it in action here:
Image
There's input display at the bottom. Note that after Roti starts running, the player's button input no longer has any effect.

Roti also ends the game when she gets to the right of the screen. But instead of writing to a location in RAM, it just jmps to the end game code. Now, Roti is a visible object, and cutscenes are usually invisible objects. But it doesn't really make a difference in how you'd program it.

The title screen cutscene is also handled with RAM Ajna checks. At the start of the game, a value in RAM is set that makes her play the dress wind animation when a level is loaded. When the player presses a button (after a timer expires), the Ajna object changes that RAM and since it's never changed again, she'll never load again in the dress wind state. But it's totally still possible to see with debug codes.
Image
Since you can switch levels with a debug command before the timer expires, she'll load in the dress wind state again. But that wouldn't happen in a regular playthrough and you can also see that once the player has gotten out of that state, it can't be triggered again.

Anyway: Your BASIC experience will probably get you pretty far with ASM.
User avatar
FrankenGraphics
Posts: 42
Joined: Wed Mar 07, 2018 11:36 am

Re: The Enigma Of Cutscenes

Post by FrankenGraphics » Wed Apr 04, 2018 10:00 am

BASIC is in a sense pretty close to 650x assembly, if you think about it. All a BASIC interpreter did back in the day was calling a bunch of of micro subroutines. The one thing that confused me most in the beginning is that the systemic flow of the program sometimes feel wrangled inside out. In BASIC, you'd just say IF statement GOTO program line and don't think about it twice. In assembly, there's a few things to keep in mind.

One is that branches not taken are quicker to execute than branches that are taken, so ideally you want to structure your program so that the paths the Program Counter most frequently takes is the natural incrementing order, and branches are the occasional exceptions. This is noncritical most of the time, but you may want to keep an eye on loops with many/frequent iterations each frame. In a longer segment of code, you won't have this option because a branch destination must be within a signed 256-byte range . Else, you must use jmp or jsr. If your choice is between a jmp or jsr on one hand and a branch on the other that is circumvening the jmp/jsr, you want the most frequently threaded path to be the branch.

In my personal projects template, at the very start of main, if a zeropage-allocated variable called GameMode is set to 0 (the most common gamemode at runtime, ie gameplay is running), it goes about its business as usual by branching past the actual mode selection routine. if not 0 the PC steps into a pointer-loading routine selector with up to 127 choices other than the case of a "normal game mode". (Actually, the choices are 128, but to access the 0st of them you need to load 0 into GameMode then explicitly access this routine from elsewhere. Not very useful, but it's there).

I tend to create more labels than seemingly needed because it
a)makes the code more readable
b)cheap local labels (for example @loop or @skip), ie. labels that can be reused, are only visible between the scope of two global labels.

the start of my main as-is:

Code: Select all

Main:

SelectMode:	
	bit gameMode 
	;lda gameMode - if accumulator can't be assumed to be 0, use this line instead
	beq NormalGameOn ;0 = normal, else specials
NormalGameOff:	
	lda gameMode	
	asl a ;*2 - this is so we can reuse gameMode as an index with even increments.
	tax
	lda .hibyte ModeTable
	sta ptr
	lda .lobyte ModeTable,x
	sta ptr+1
	jmp (ptr)
NormalGameOn:
	; first of all, check if pause
	
	; In BASIC the following had been IF foo = 1 GOSUB TogglePause.
	; But rather, this translates to IF foo = 0 DO NOT GOSUB TogglePause. This is the inversion that may be confusing coming from BASIC. 
	; I sometimes confuse this and do the inverse of what i want to do when not keeping my wits with me, leading to little "gotcha" bugs between builds. 
	
	lda Joy1Diff
	and #BUTTON_START ;filter out all other presses except start.
	beq @skip 
		jsr TogglePause
	@skip
SomeOtherRoutine:
[the rest of the main code here]
TogglePause:
	lda GamePaused
	eor #$01
	sta GamePaused
	jsr SFX_gamePause
	rts
ModeTable:
.word NormalGameOn, InventoryMode, MaybeTitle, MaybeCredits, MaybeEnding, DoThis, DoThat, SomeOtherThing, HurryUp, OneMoreMode ;.word same as .addr in ca65

 
Now the hierarchy says that the game can only map pause function to the start button if gameMode is 0, if another gamemode jumps back to gameMode 0 when done, or if the toggle code is repeated elsewhere as a subroutine.

Pause is independent from GameMode because there might be several cases where i want to decide wether pause is legit on a mode-for-mode basis.

the NESmaker template codebase likely has a few routine-selecting jumptables of its own. There are more ways to construct these routines, this is just one way. I'm guessing that if you want cutscenes, you may either hack into a routine selector like that if it is called at the appropriate place, or you create a wholly new one which trigs on a variable you make up, like maybe CutscenePending.
www.frankengraphics.com - NES homebrew blog
User avatar
Kasumi
Posts: 99
Joined: Fri Mar 09, 2018 11:13 pm

Re: The Enigma Of Cutscenes

Post by Kasumi » Wed Apr 04, 2018 9:10 pm

I'd recommend not using bit there, FrankenGraphics. If you just start with lda gameMode, you can avoid the lda gameMode under the NormalGameOff label. It also avoids the state of A before this ever causing problems. Using bit that way, A has to be 255 for it to work, because BIT sets the zero flag as if an AND happened.

With bit, If A is 0 at SelectMode, the game will always go to NormalGameOn regardless of the value of gameMode because anything ANDed with zero is zero. But even A being something like 128 keeps it from working if gameMode is 1-127.

I don't use ca65 but the pointer logic also seems malformed. I'm under the impression from the docs that lda .hibyte ModeTable would give you a constant value of the high byte where the ModeTable label is rather than the high byte of any address in the actual table. Even if not, it's missing the ,x.

Even assuming directive is correct, the high byte is stored to ptr and the low byte is stored to ptr+1 when the low byte is required in the first pointer byte for jmp indirect to work on the 6502.

So a bit different:

Code: Select all

SelectMode:	
	lda gameMode
	beq NormalGameOn ;0 = normal, else specials
NormalGameOff:
	asl a ;*2 - this is so we can reuse gameMode as an index with even increments.
	tax
	lda ModeTable,x
	sta ptr
	inx
	lda ModeTable,x
	sta ptr+1
	jmp (ptr)
NormalGameOn:
User avatar
FrankenGraphics
Posts: 42
Joined: Wed Mar 07, 2018 11:36 am

Re: The Enigma Of Cutscenes

Post by FrankenGraphics » Thu Apr 05, 2018 2:11 am

You're absolutely right Kasumi! Thanks for pointing it out. I thought i had cleaned that experimental BIT out even before hitting post but apparently not. :o

I confused myself when writing the pointer lookup. The .hibyte ModeTable only works insofar that all Game mode-pointing symbols in that table are meant to have the same upper address byte. Which is pretty impractical (to fit into one and the same page of address space, the contents of each game mode label would in that case need consist of a series of jsr:s with almost no hardtyped code inbetween), + the way i set up the table, no space savings were done anyway. If it had been of any utility, the table could just consist of least significant bytes and we wouldn't need the ASL. What's worse, once the game mode symbols don't fit the same page, the build would break the game. Goes without saying, but this wasn't my intention. I just wanted to elaborate what the fault in my code does/doesn't.

Just having a fixed upper byte and fetching lowbytes might have its use (you get twice the range of options using similar simple code), but not for something like a game mode selector (i wouldn't be surprised if several games just have two coarse modes: title and running, in which case the first branch is enough).

Question is if i should edit the post so noone copies bad code by mistake or if i should keep it as is for the ease of following the discussion.

.hibyte and .lobyte are just written-out aliases for the > and < operators, respectively.
NESmaker users will want to know that .hibyte and .lowbyte aren't asm6 syntax, but > and < are. Examples later on in this post.

Looking at my other pointer based subroutines, they do it like this:

Code: Select all

WriteBulkPPUdata:
;===
;Bulk loader for data to be written to PPU VRAM, for use when rendering is turned off
;expects x (number of pages to write), y (nominally expecting 0, but can be other to skip portions of 1st page)
;relies on PPUADDR and the address latch being appropriately set before being called.
;For standard use, make sure y is = 0 before calling this sub. 
;overwrites x, y, ptr
;===

@loop:   
   lda (ptr), y
   sta PPUDATA ;each write to PPUDATA also increments the PPU address latch
   iny   
   bne loop    ; done a page yet?  (y reg)
   inc ptr+1    
   dex       
   bne loop    ; done all pages yet? (x reg)
rts
   
(A more user friendly version would ldy 0 before @loop at the cost of some flexibility. As is, though, the subroutine can be reused to copy a strip shorter than a page to the target ppuaddr. So even if it was written to load chunks of nametable/attribute data, it can update a chosen range of palettes too for example)

If pointers however need to be loaded with an absolute address (because a subroutine is expecting pointers maybe), the preparation would look something like this:

Code: Select all

lda #>mySymbol
sta ptr+1
lda #<mySymbol
sta ptr+0 
jsr MySubroutine

or if the subroutine expects two arguments to be passed in order to prepare a pointer internally, then:

Code: Select all

lda #>mySymbol
ldy #<mySymbol
jsr MySubRoutine
www.frankengraphics.com - NES homebrew blog
Post Reply