The Enigma Of Cutscenes II: The Next Generation

Bucket Mouse

Active member
I hope the mods don't mind my making a second topic, but a lot has changed since we last discussed how to do this. In the original topic, we didn't know exactly how the NESMaker code would be written regarding movement. Now we do. I feel with the information given in the latest batch of tutorials, we're super-close to coding out scenes where the characters move on their own.

We now know the specific names of the variables used:

LDX player1_object
INC Object_x_hi,x

This moves the player's character right. The same code but with DEC instead of INC moves it left.

ChangeObjectState #$01

This changes the player's animation to a walk cycle. Specific animation for cutscenes could be created in the Animations tool and called up with different numbers when needed.

Creating the movement for cutscenes should be as simple as creating a new variable for the number of frames you want the movement to take, as well as the speed. Four subroutines for Up, Down, Left and Right would allow us to do pretty much everything (except jumping).

There would also need to be a line of code that would shut off every input button except Start (because people will still want to pause the game), and possibly A (which could skip the cutscene). Those lines could be two subroutines, one to shut off input and one to turn it back on.

Nor for what I still don't know....how to get these things into the game! Currently, the only tool in NESMaker that lets you apply scripts is the Input screen. If the Map tool would let us apply scripts directly to a tile on the screen, the cutscene could activate when a character walks across it. Or if a cutscene needs to happen as soon as a screen is loaded, a menu could let us load that script into the screen, and there'd be a checkbox for whether the script was a one-time event or meant to happen each time.

Those things would be nice to have. Hint hint.

PS: if the reply is "not enough memory per frame," think about replacing that one function that separates everything into Night and Day. Very few projects are actually going to use that.
 

dale_coop

Moderator
Staff member
Yeah, I thought about that too after watched the last tutorial.

I think something doable would be call a cutscene_subroutine when the player trigger a pickup object and or even a tile.
Then for the input scripts, you could check the state of a variable (for ex, initialized to #$00 and changed to #$01 in cutscene_subroutine) and do skip the input part of the script, when not equal to #$00.

Would be very basic but might be fun.
 

darkhog

New member
Well, you could use custom screens for tecmo theater-like cutscenes, though I'm not sure how would you go to/from those as needed.
 

Bucket Mouse

Active member
Presenting my FIRST attempt at independent movement! It didn't work, of course, but I wasn't expecting to get it right immediately. Hopefully with the help of the fine citizens of this forum, I can crack this.

I mapped this code to Button A in the main game. In theory, whenever I pushed button A, my little figure should've walked eight steps in a square shape. Instead the screen froze.

I'm sure there are a ton of rookie mistakes in here -- help me pick them out.

Code:
    ;; change to walking animation
    LDX player1_object 
    ChangeObjectState #$01, #$10

    StartMoving player1_object, MOVE_UP
    loophere:
    LDX player1_object
    LDA MOVE_UP
    ORA #DIR_UP
    STA Object_movement,x
    INX
    CPX #8
    BNE loophere

    StartMoving player1_object, MOVE_RIGHT
    loophere2:
    LDX player1_object
    LDA MOVE_RIGHT
    ORA #DIR_RIGHT
    STA Object_movement,x
    INX
    CPX #8
    BNE loophere2

    StartMoving player1_object, MOVE_DOWN
    loophere3:
    LDX player1_object
    LDA MOVE_DOWN
    ORA #DIR_DOWN
    STA Object_movement,x
    DEX
    CPX #1
    BNE loophere3

    StartMoving player1_object, MOVE_LEFT
    loophere4:
    LDX player1_object
    LDA MOVE_LEFT
    ORA #DIR_LEFT
    STA Object_movement,x
    DEX
    CPX #1
    BNE loophere4

    ;; change to idle animation
    LDX player1_object 
    ChangeObjectState #$00, #$02

    RTS
 

MistSonata

Moderator
I took your code and made it into something that would, in theory, work.

Code:
Dummy_Script:
    ;; change to walking animation
    LDX player1_object 
    ChangeObjectState #$01, #$10

    LDY #$08 ; how many pixels you want to travel
    LDA #UP 
    STA Object_movement,x
    
    charGoUpLoop:
    DEC Object_x_hi,x
    DEY
    CPY #$00
    BNE charGoUpLoop
    
    LDY #$08
    LDA #LEFT
    STA Object_movement,x
    
    charGoLeftLoop:
    DEC Object_y_hi,x
    DEY
    CPY #$00
    BNE charGoLeftLoop
    
    LDY #$08
    LDA #DOWN
    STA Object_movement,x
    
    charGoDownLoop:
    INC Object_x_hi,x
    DEY
    CPY #$00
    BNE charGoDownLoop
    
    LDY #$08
    LDA #RIGHT
    STA Object_movement,x
    
    charGoRightLoop:
    INC Object_y_hi,x
    DEY
    CPY #$00
    BNE charGoRightLoop
    
    ChangeObjectState #$00, #$00
    LDA #DOWN
    STA Object_movement,x
    
    RTS

And while this code does technically work, unfortunately there's a bigger problem with it. If you plug it in and test it, it will seem like it does nothing, and that's because it goes through the code too fast. If you go back into the code and change the numbers before each loop, your character will simply "teleport" to a different spot on the map. It changes the character's x and y position one pixel at a time, but it does it so fast that it's finished by the time the graphics have a chance to update.

You would need to implement some kind of timing, and apart from using your action steps, there's unfortunately no way to do that right now that I know of.

It's cool that you're experimenting with 6502, though! I'd highly recommend checking out things like Easy6502 and "Assembly in one step" to learn more, it will really help in the long run! :)
 

Kasumi

New member
if the value at RAM location player1_object isn't 7, you've created an endless loop
Code:
loophere:
    LDX player1_object;If the value stored here isn't 7, you've created an endless loop.
    LDA MOVE_UP
    ORA #DIR_UP
    STA Object_movement,x
    INX;Add one to X. 
    CPX #8;Compare that new value to 8. 
    BNE loophere;If it's not equal to 8, go to the label
Underneath the loophere label changes the value of X to player1_object. The inx adds 1 to this. But if the branch is taken, it just loops back to loophere which reloads X.

Suppose player1_object is 6. We copy it. We add one to the value of the copy. That's 7. We compare that value to 8. They're not equal, so we...
copy player1_object which is 6. We add one to the value of the copy. That's 7. We compare that value to 8. They're not equal so we...
copy player1_object which is 6. We add one to the value of the copy. That's 7. We compare that value to 8. They're not equal so we...

Even assuming you had no infinite loops here, it would not work like you're expecting. Storing a value to Object_movement,x doesn't actually move the object. It tells some code that runs later to move it. So you can store to it as many times as you like here, and it will only affect movement once.

As well, you do NOT want to modify X. The value of X is used to choose which object's data to store to.
Code:
ldx player1_object
sta Object_movement,x
would affect player1's RAM.
Code:
ldx monste1r_object;as an example, this isn't a real value I'm sure
sta Object_movement,x;would affect that monster's movement RAM.
But it probably don't even have that RAM, this is just an example of how "sta ram,x" is usually used. It's not meant as a NES Maker tip.

Video games work like this.

They display a frame. While that frame is being displayed, the CPU is running creating the data (positions for sprites, etc.) needed to display the next frame. You can't do the movement all at once in one script, because then all the movement would occur in one frame. NES runs at 60 frames per second.

That is to say that if you did successfully update the object positions 8 times to walk around in the square in this one script, the object would walk around in a square and be back where it started in less than 1/60th of a second. You wouldn't visibly see it move because all of that movement will have occurred in one frame.

You need to go pretty deep into the code to do what you want. At the very least you'd need to add a RAM definition to keep track of how many frames had passed, so you can decide which movement to do across the frames.

I really recommend reading easy6502 as MistSonata said: https://skilldrick.github.io/easy6502/
 

Kasumi

New member
Double post, because why not. MistSonata, you can almost always avoid comparing a number with zero.
Code:
ldy #8;Copy the value 8 to Y
loop:
dey;Subtract 1 from Y
cpy #0;Compare the value in Y to zero.
bne loop;If the result of the last operation was not zero, branch to loop.
Note that bne is "if the result of the last operation was not zero" and not "if the two numbers were not equal". So
Code:
ldy #8;Copy the value 8 to Y
loop:
dey;Subtract 1 from Y
bne loop;If the result of the last operation was not zero, branch to loop.
If Y is 1, dey makes Y equal to zero. Which means the result of the last operation was zero. Which means bne won't branch.

So you get the same result, with one fewer instruction, two fewer bytes, and faster.

Check out this page: http://www.obelisk.me.uk/6502/reference.html
It has a list of which flags are affected for each instruction. When the zero flag is affected, this means cmp/cpy/cpx is sort of implied, and you can use beq or bne to branch based on whether or not it was zero. But it's not ALWAYS affected. Here's an example.

Code:
loop:
lda #0
ldx #$FF
sta RAMLOCATION
bne loop
That bne would absolutely branch.
lda #0; This affects the zero flag. The result is zero, so beq would branch after it, bne would not.
ldx #$FF;This affects the zero flag. The result is NOT zero, so beq would NOT branch, and bne would.
sta RAMLOCATION;Even though 0 is in A, sta is an instruction that does NOT affect the zero flag. So it's unchanged from the above instruction. beq would NOT branch, and bne would.

One of the really key things to understand about branches in 6502. They only care about whatever most recently changed the flag. This reference guide is very helpful for seeing which flags get affected by which instructions and how: http://www.obelisk.me.uk/6502/reference.html
 

Bucket Mouse

Active member
MistSonata said:
I took your code and made it into something that would, in theory, work.

Code:
Dummy_Script:
    ;; change to walking animation
    LDX player1_object 
    ChangeObjectState #$01, #$10

    LDY #$08 ; how many pixels you want to travel
    LDA #UP 
    STA Object_movement,x
    
    charGoUpLoop:
    DEC Object_x_hi,x
    DEY
    CPY #$00
    BNE charGoUpLoop
    
    LDY #$08
    LDA #LEFT
    STA Object_movement,x
    
    charGoLeftLoop:
    DEC Object_y_hi,x
    DEY
    CPY #$00
    BNE charGoLeftLoop
    
    LDY #$08
    LDA #DOWN
    STA Object_movement,x
    
    charGoDownLoop:
    INC Object_x_hi,x
    DEY
    CPY #$00
    BNE charGoDownLoop
    
    LDY #$08
    LDA #RIGHT
    STA Object_movement,x
    
    charGoRightLoop:
    INC Object_y_hi,x
    DEY
    CPY #$00
    BNE charGoRightLoop
    
    ChangeObjectState #$00, #$00
    LDA #DOWN
    STA Object_movement,x
    
    RTS

And while this code does technically work, unfortunately there's a bigger problem with it. If you plug it in and test it, it will seem like it does nothing, and that's because it goes through the code too fast. If you go back into the code and change the numbers before each loop, your character will simply "teleport" to a different spot on the map. It changes the character's x and y position one pixel at a time, but it does it so fast that it's finished by the time the graphics have a chance to update.

You would need to implement some kind of timing, and apart from using your action steps, there's unfortunately no way to do that right now that I know of.

It's cool that you're experimenting with 6502, though! I'd highly recommend checking out things like Easy6502 and "Assembly in one step" to learn more, it will really help in the long run! :)
If this was Apple II, I would throw in a meaningless calculation to keep the computer busy between the desired amount of frames. FOR BUSY=1 TO 10000: NEXT BUSY But I'm guessing you can't really do that here.

So how does Mario move by himself into the pipe between levels 1-1 and 1-2? It doesn't happen in a split second. What's going on there? Maybe I'm using the wrong approach....

Maybe using the action steps IS the key. Maybe that subroutine can be temporarily hijacked to make a character move on their own. You'd redefine the movement speed, define the number of steps and the direction, send the NES over there, then restore the variable to its intended speed.....is that something that can be done?
 

Kasumi

New member
So how does Mario move by himself into the pipe between levels 1-1 and 1-2? It doesn't happen in a split second. What's going on there? Maybe I'm using the wrong approach....
Exactly as I described. Moving down a set number of pixels each frame. Say you have code like below that's run every frame as part of the Mario object's script.
Code:
lda marioisgoingdownpipe;If the cutscene has not been triggered
beq mariodone;don't run the pipe code.

pipedown:
lda pipeframesleft;Load how many frames Mario has left to go down the pipe
beq nextlevel;If there are no frames left, go to the code that start the next level
inc marioypos;Otherwise, move Mario a pixel down.
dec pipeframesleft
mariodone:
rts
So every frame, this code would check if the pipe cutscene has been triggered. It it hasn't, the Mario object is done for that frame.

Otherwise, it checks if he has frames left to move down a pipe. If this value is zero, he'll move to the next level start code. Otherwise it continues down and moves Mario a pixel down. Then subtracts from the number of frames that are left. Then returns and the Mario object is done for that frame.

Elsewhere you would have code that runs when Mario presses down on top of the pipe. This sets marioisgoingdownpipe to anything except zero and then sets pipeframesleft to however many frames he should move down the pipe.

This is what I meant when I said you would have to define RAM that keeps track of which step of movement you are on.
 

Bucket Mouse

Active member
Kasumi said:
This is what I meant when I said you would have to define RAM that keeps track of which step of movement you are on.

So how do you define that? Where do you define that?

All my programming experience is centered around being able to do subroutine loops without anything extra to worry about. I have no idea how to make the code do something once per FRAME.

I've looked around the existing NESMaker ASM, looking for some kind of clue as to how it's done, and I've come up with nothing. My head hurts. I need a bigger hint!
 

Kasumi

New member
I don't know how to do it in the context of NES Maker. There's not much official NES Maker documentation at the moment. The way I know how to do it would need to get updated every single time the the source files change in a NES Maker update. This is also why I held off on the DPCM safe controller reading.

Inside GameEngineData/Routines/Variables there is a file called UserVariables.asm. There's also a file called SystemVariables.asm, and ZP_and_vars.asm. The NES has 2KB of RAM from $0000 to $07FF. Open ZP_and_vars.asm.

The way NES Maker gives names to RAM is by setting the address where it wants the naming to start with .enum (address to start assigning names). Then it adds the name it wants to give the next X bytes, followed by .dsb (a number here) where the number is how many bytes of RAM are reserved by that name.

So, it starts at $0000 with .enum $0000. The next line is .include, which is a bit equivalent to taking the entire contents of the given file and pasting it in in place of the line the include is on. So let's open "Routines\System\ggsound_zp.inc". You'll see this:
Code:
sound_region: .dsb 1
sound_disable_update: .dsb 1
sound_local_byte_0: .dsb 1
sound_local_byte_1: .dsb 1
sound_local_byte_2: .dsb 1
What does this mean? sound_region starts at $0000 and is 1 byte long, so it also ends at $0000. ASM6 then adds 1 (or whatever number follows the dsb) to the invisible counter that was started with .enum $0000. $0000 +1 is $0001.

sound_disable_update starts at $0001 and is 1 byte long, so it also ends at $0001. Etc.

Let's say we changed
Code:
sound_region: .dsb 1
to
Code:
sound_region: .dsb 2
Now sound_region would still start at $0000 but it would be TWO bytes long, so it ends at $0001. ASM6 then added 2 to the invisible counter (which is currently at $0000) to get $0002. Then the sound_disable_update that follows would get assigned $0002. sound_local_byte_0 would get assigned $0003. Etc.

Anyway, GGSound uses about 70 bytes of RAM from a quick count. (It varies depending on whether DPCM is enabled.)

To name new RAM, you need to add nameofyourram .dsb (number of bytes for that RAM) after an enum like these. But there are caveats. If you add it after all the .dsb statements after the .enum $0000, but the total of bytes reserved is greater than 256, the variables will crash into ggsound's RAM. (ggsound has RAM in two places, because some things you can only do with RAM at $0000-$00FF. Putting ALL of its RAM there, though, would takes bytes away from the main program and that main program also wants as many bytes as possible to do that stuff.)

This gets us back to UserVariables.asm. This is where the engine wants you to put variables. The thing is that if you add a .dsb statement in here manually, it's likely going to get destroyed by the export process. And while you could add one in between two lines in ZP_and_vars.asm like this
Code:
.include "Routines\Variables\UserVariables.asm"
myramname .dsb 1
.ende
You'd still have to redo that each time you download a new NES Maker package. (And I mean actually redo. You can't just replace the new file with the older file with the stuff you added. If NES Maker adds new RAM in that file, the engine will expect that RAM to be defined and it won't be in your old file.)

In short: You can add new RAM to the engine by adding .dsb statements into GameEngineData/Routines/Variables/ZP_and_vars.asm like immediately above, but I REALLY don't recommend doing so. Look for how to add to GameEngineData/Routines/Variables/UserVariables.asm from within the tool itself which is much less likely to get broken all the time. I'll say I do wish there were more text documentation, scrubbing through hours of video content isn't quite my style. In the help file it does mention a UserVariables tab, but the text for it has not been written yet. So I similarly wouldn't add to it this way without documentation of the caveats.

Even shorter: I honestly wouldn't touch this right now.
 

Bucket Mouse

Active member
Kasumi said:
In short: You can add new RAM to the engine by adding .dsb statements into GameEngineData/Routines/Variables/ZP_and_vars.asm like immediately above, but I REALLY don't recommend doing so. Look for how to add to GameEngineData/Routines/Variables/UserVariables.asm from within the tool itself which is much less likely to get broken all the time. I'll say I do wish there were more text documentation, scrubbing through hours of video content isn't quite my style. In the help file it does mention a UserVariables tab, but the text for it has not been written yet. So I similarly wouldn't add to it this way without documentation of the caveats.


Right....We both know the tool itself isn't complete right now and a lot of options simply aren't wired up to work. Adding user variables to the tool is probably one of the last things they'll complete because the people who want it aren't numerous. The people who want the things that user variables can DO, however, are. Maybe we need to spread awareness...

Kasumi said:
Even shorter: I honestly wouldn't touch this right now.


You know how the protagonist of every shonen anime is a scratchy-voiced kid who has big dreams yet is very inexperienced? And everybody tells him "You'll never do it! Give up now!" but the kid clenches his fists and yells "Yes I will! I will win the Big Fat Epic Tournament and become Hokage and be a Pokemon Master and everything!" I feel a lot like that kid right now.

I have so many big ideas and for most of them, the ability to use cutscenes to tell the story is crucial to them. I want this. I NEED this. This is a hurdle I absolutely, positively must overcome, period.


Kasumi said:
You'd still have to redo that each time you download a new NES Maker package. (And I mean actually redo. You can't just replace the new file with the older file with the stuff you added. If NES Maker adds new RAM in that file, the engine will expect that RAM to be defined and it won't be in your old file.)


If it's, like, one line like you showed me, it wouldn't be that big a deal. Every time Wordpress updates itself on one of my websites it erases the custom color scheme I put there. I have to go in and re-add it every time, but that only takes a second.


Kasumi said:
To name new RAM, you need to add nameofyourram .dsb (number of bytes for that RAM) after an enum like these. But there are caveats. If you add it after all the .dsb statements after the .enum $0000, but the total of bytes reserved is greater than 256, the variables will crash into ggsound's RAM.


According to my count, the total number of bytes reserved by everything listed in ZP_and_vars.asm is 190. Then it gets confusing with the last two .dsb variables, collisionTable and collisionTable2, taking up 256 bytes each. That would....kinda put it over the limit? There's something here I don't understand.
 

Kasumi

New member
Bucket Mouse said:
I have so many big ideas and for most of them, the ability to use cutscenes to tell the story is crucial to them. I want this. I NEED this. This is a hurdle I absolutely, positively must overcome, period.
You're going about it the wrong way. Go to easy6502. Learn 6502.

If it's, like, one line like you showed me, it wouldn't be that big a deal. Every time Wordpress updates itself on one of my websites it erases the custom color scheme I put there. I have to go in and re-add it every time, but that only takes a second.
It's exactly one line like I showed you. Or, more specifically, it's one line for every byte of RAM you add.

According to my count, the total number of bytes reserved by everything listed in ZP_and_vars.asm is 190. Then it gets confusing with the last two .dsb variables, collisionTable and collisionTable2, taking up 256 bytes each. That would....kinda put it over the limit? There's something here I don't understand.
I said if you add it after all the .dsb statements after the .enum $0000. In ZP_and_vars.asm is .enum $0000 on line 1. On line 239 is .ende. This is the "ZP" in ZP_and_vars.asm. The Zero Page. $0000-$00FF are faster to access (and have a few other benefits.) If you add .dsb statements after .enum $0000, it will push everything else up. If you push the very last thing (SongToPlay) too much, it will be assigned a value greater than or equal to $0100 and be outside the zero page.

On line 242 is another .enum to start definitions at $0100. So there'd be a conflict. If you added the .dsb statement where I mentioned in the post, it'll be after an .enum statement that is outside the zero page.

In short: There are 2048 bytes total. The zero page is the first 256 of them.
 

SeaFoodAndButter

New member
So, I'm looking for a cut scene after the start menu. Something like Journey to Silius:
https://www.youtube.com/watch?v=QiwkOsUzrdQ&t=189s

Now, I asked Joe about this awhile ago and his response was basically, and I'm paraphrasing, "that took dozens of professional programmers months and it was a game that came out much later on in the NES's lifespan (1990). IF you want something like that, code it yourself." Now, there's no way I can code that because I don't know much about coding. However, I think there can be many ways to do something like the Journey to Silius's opening story cinematics.

If you look at it, it is awesome indeed, but it shouldn't really be that hard to recreate. What I see is basically 1) A start screen with music; 2) the player presses any button and it takes them to another "start screen" (basically) that switches between 2-3 palettes, then press any key and ; 3) you come to another "start screen" that has text, explaining the story; then 4) you come to another "start screen" essentially that has dialogue and more story; eventually, you come to the last "start screen" with a graphic and dialogue, press any key and start the game, the whole time the same track is playing. If you think about it, essentially these are multiple "start screens" that are simply stacked. Let me explain.

So I'm not a coding guy, but I have an idea and want to know if it could work, or is it just stupid and I'm a noob who doesn't understand?

OK, in NESMaker, your first screen is the "start screen." After you press any key, you go directly into the game. But I don't want that. I want to tell a bit of the story first. It seems kind of lame to go straight into the game like that. So, what if, you could stack "start screens"?

Basically, right now, when you fire up the game, you go to the first start screen that has graphics, music, and says press "start." Well, when you press start, you go directly to the screen you have your character starting on and begin the game. But what if, after you pressed start, you went to another "start screen" that showed a graphic and some dialogue, telling you to press, say, B, to continue. Then when you pressed B it took you to another "start screen" with graphics and more dialogue (all the while continuing the same music). Essentially, you could stack start screens as cinematics, and then finally start the game, after telling some story and stuff. Is this a dumb idea? Isn't this theasable? Logically, I don't see why you can't do this as long as memory allows for it...re-look at Journey to Silius and see that this looks essentially like what they did https://www.youtube.com/watch?v=QiwkOsUzrdQ&t=189s.
 

Bucket Mouse

Active member
SeaFoodAndButter said:
So, I'm looking for a cut scene after the start menu. Something like Journey to Silius:
https://www.youtube.com/watch?v=QiwkOsUzrdQ&t=189s

Now, I asked Joe about this awhile ago and his response was basically, and I'm paraphrasing, "that took dozens of professional programmers months and it was a game that came out much later on in the NES's lifespan (1990). IF you want something like that, code it yourself." Now, there's no way I can code that because I don't know much about coding. However, I think there can be many ways to do something like the Journey to Silius's opening story cinematics.


I was thinking about this last night. They said multiple times that it wouldn't be possible with NESMaker, but there could potentially be a way to use the resources available to "cheat" a scene like this.

You would make the graphics for the illustrations on one of the background graphics sheets, then use one of your screens to lay them out as a background. Below that you would have a text box tell the story (so text boxes would need to be functional).

There would be a little note that said "Press right to continue." Your character sprite would be at the bottom, but would be set on a completely black palette. Moving right would push your sprite right onto a warp tile that would take it to the next screen. The next drawing in the story would be on that screen, and more text. You could even use monster sprites to add animated effects.

Once the scene is done, you warp to the first playable screen, where your character's palette is swapped back to normal. That all sounds plausible to me.
 
Top Bottom