Aren't you afraid that NES Maker would just bring lazy noobs

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Bananmos
Posts: 524
Joined: Wed Mar 09, 2005 9:08 am
Contact:

Re: Aren't you afraid that NES Maker would just bring lazy noobs

Post by Bananmos » Tue May 12, 2020 9:15 am

gauauu wrote:
Tue May 12, 2020 7:10 am
I've found that C can be great for writing a main character's logic, as that often has a lot of complexity of different moves/attacks/etc, and isn't as likely to iterate through a loop of pointers/arrays. C is a lot worse for iterating over your enemies or other entities, as it does a terrible job of preserving X or Y, so wastes a ton of time reloading them.
Personally, I actually found that the greatest improvement in simplifying game logic came from implementing a "yield" statement, allowing my actors to "wait" for a frame / multiple frames, returning to the same place. I find that at least for some game logic, this implicit state through execution location is a lot easier to reason about than a single-entry-point function that has to start with a big switch statement depending on an explicit game state.

The yield statement had to have a compromise to save on stack memory: Only allowing the yield to happen at the top AI function and not in any called subroutines. But even with this limitation, I found that combined with the ca65 HLA macros by Movax, it made for some quite readable code. Here's a snippet from a cloud boss circling around and attacking the player with lightning (which brings the chaotically moving boss to a standstill and is a good opportunity for the player to attack using steam):

Code: Select all

Idle:
    if bossState = #SHRINKING
        jsr UpdatePalette
        ; When eyes are closed, do explosion
        dec bossTimer
        if zero
            jsr SpawnExplosion
            inc bossHitCount
            jsr ShowCloudBG
            WAIT_UNTIL_ANIMATION_DONE_AND_UPDATE_PALETTE
            ACTOR_SET_ANIMATION #ANIMATION_CLOUDEYES_LOOKAROUND
            WAIT_UNTIL_ANIMATION_DONE_AND_UPDATE_PALETTE
            mb { bossState := #CRUISING }
            ACTOR_SET_ANIMATION #ANIMATION_CLOUDEYES_DIRECTIONAL
        else
            YIELD
        fi
        
    else        
        jsr UpdateTarget
        jsr GetInput
        jsr UpdateSpeed
        jsr DropWater
        jsr SpawnSteam

        ; Do state-specific animation/spawn lightning
        if bossState = #PREYING && ReachedTarget
            mb { bossState := #CHARGING }
            mb { bossTimer := #100 }
        elseif bossState = #CHARGING
            ; Keep looking down for awhile, then switch to charging animation
            if bossTimer > #50
                ACTOR_SET_ANIMATION #ANIMATION_CLOUDEYES_DIRECTIONAL
                mb { bossAnimationPos := #(4*7+4) }
                mb { bossFrameDelay := #255 }
            elseif bossTimer = #50
                ACTOR_SET_ANIMATION #ANIMATION_CLOUDEYES_CHARGING
            elseif bossTimer < #50
                ; Make sure to diminish speed whilst charging, to make circling around target less chaotic
                mb { y := bossTimer + #77 }
                jsr DiminishSpeed
            fi
            
            ; Spawn lightning when actorTimer = 0
            dec bossTimer
            if zero
                jsr SpawnLightning
                do
                    ; Goto frame 4 in animation
                    mb { bossAnimationPos := #(4*4+4) }
                    jsr UpdatePalette
                    YIELD
                while LightningPresent
                ; Goto frame 5 in animation
                mb { bossAnimationPos := #(5*4+4) }
                mb { bossFrameDelay := #3 }
                mb { bossState := #CRUISING }
                WAIT_UNTIL_ANIMATION_DONE_AND_UPDATE_PALETTE
                ACTOR_SET_ANIMATION #ANIMATION_CLOUDEYES_DIRECTIONAL
            fi
        fi
    
        jsr SetEyesDirection
        jsr UpdatePos
        jsr UpdatePalette
        jsr CheckCollisionWithSteam
        if bossHitCount = #3
            jmp DoEndSequence
        fi
        YIELD
    fi
    jmp Idle
The above code does still have some explicit top-level states, but using yield avoids having to treat every sequential operation as an explicit state... arguably the top-level state could also be avoided altogether by jumping to another block of code, simplifying the logic even more if the top-level states need distinguishing more. The "WAIT_UNTIL_ANIMATION_DONE_..." macros are also implemented through a yield, which feels much simpler to me than having a new explicit state when you want to play a pre-authored animation with little game logic involved.

I think it also very much depends on the game genre how useful the yield statement really is. The game I was working on (put on the back burner for too long) was a puzzler with a lot of complicated sequential logic, where trying to design an explicit state machine would have been a nightmare to even conceptualize. But I guess for other genres, a state machine might be less limiting / more appropriate.

Still, coroutines / actors is one of those things that high-level scripting languages for game logic (like the previously mentioned Lua) provide out-of-the-box, and where hacking this high-level language feature in assembly was surprisingly easy to do.
By contrast I wouldn't even know how to start doing this with cc65... as there's no obvious way in the language to save the current address within a function to return to the same point of execution in the next frame - the C language was never designed for lightweight threading of concurrent actors. Though there might be some obscure C feature that could make it possible? You could also do this with your own pre-processor that creates explicit states for every yield... but then you're already in the realm of designing your own C-like language which ain't really C. (though you could argue the high-level macros do the same)

User avatar
rainwarrior
Posts: 7822
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: Aren't you afraid that NES Maker would just bring lazy noobs

Post by rainwarrior » Tue May 12, 2020 10:51 am

Bananmos wrote:
Tue May 12, 2020 9:15 am
By contrast I wouldn't even know how to start doing this with cc65... as there's no obvious way in the language to save the current address within a function to return to the same point of execution in the next frame - the C language was never designed for lightweight threading of concurrent actors. Though there might be some obscure C feature that could make it possible?
Well there's setjmp in the standard library which kinda does this, but it's a bit limited and not a lot of people use it. cc65 does implement it though.

Bananmos
Posts: 524
Joined: Wed Mar 09, 2005 9:08 am
Contact:

Re: Aren't you afraid that NES Maker would just bring lazy noobs

Post by Bananmos » Wed May 13, 2020 4:16 am

rainwarrior wrote:
Tue May 12, 2020 10:51 am
Bananmos wrote:
Tue May 12, 2020 9:15 am
By contrast I wouldn't even know how to start doing this with cc65... as there's no obvious way in the language to save the current address within a function to return to the same point of execution in the next frame - the C language was never designed for lightweight threading of concurrent actors. Though there might be some obscure C feature that could make it possible?
Well there's setjmp in the standard library which kinda does this, but it's a bit limited and not a lot of people use it. cc65 does implement it though.
Ah, I stand corrected. So there *is* a rather obscure C feature that could enable coroutines in cc65... that's interesting to know. I knew setjmp / longjmp could be used for exception handling but didn't consider this use. :)

Existing examples seem quite grotty though:
https://www.embeddedrelated.com/showarticle/455.php
http://dotat.at/cgi/git/picoro.git
https://fanf.livejournal.com/105413.html

But could be a useful starting point for someone trying this out in cc65, if you fix the parts that allocate 64kB of stack to each coroutine, just because there's no portable way of figuring out the right stack size...

Worth saying it's actually not that much of a problem to yield at any level of an actor - you would just need to make sure the actor gets its own dedicated space on the stack and does a txs / tsx as part of yield:ing to save contents of its own local stack and lookup the top-of-its-own-stack to know where it should return. I just never added it because it was a lot of extra overhead / complexity for marginal gain, but chose to keep it simple. I also needed the stack space for other algorithms which used it heavily, so couldn't have used it for persistent storage of actor execution state anyway.

Post Reply