Jump to content

Photo

[Kinda Tutorial?] Understanding Waitframe()


  • Please log in to reply
9 replies to this topic

#1 Mitchfork

Mitchfork

    no fun. not ever.

  • Contributors
  • Real Name:Mitch
  • Location:Alabama

Posted 08 July 2021 - 02:36 PM

Originally, this post was a reply to this thread but it quickly got out of hand so I split it into its own topic, since some of this information may be useful to some people that are still learning about ZScript and how to conceptualize the Waitframe() function.
 

The whole need for Waitframe() has always confused me. Does not having it mean that Demonlink's code snippet runs 120 times in a single frame instead of 1 time over 120 frames?

 
Yeah, conceptualizing Waitframe() was hard for me too to be honest.  Here's a way of thinking about it that unlocked it for me:
 
In ZC (and I guess programs more generally) the program creates the illusion that it does a lot of things at once but in reality everything that needs to be calculated, moved, or adjusted is done sequentially, one at a time.  For example, let's say you have a screen that has four FFC scripts running, and you also have a global script loop. ZC has to do the following:

  • Perform all of the normal engine behaviors that occur before scripts execute.
  • Perform the global active script.
  • Perform FFC script 1.
  • Perform FFC script 2.
  • Perform FFC script 3.
  • Perform FFC script 4.
  • Perform all the normal engine behaviors that occur after scripts execute.
  • Draw the screen on your monitor, then loop back around to number 1.  This is 1 frame.  Because this display is "instant" it makes it appear that all of the scripts and engine stuff happened at the same time.

(Note this is a heavily simplified but basically correct order - you can see the precise order that ZC handles things in zscript.txt under "Instruction Processing Order and General Timing")
 
Here's the thing though - how does ZC know that FFC script 1 is "done"? Consider if we assigned the following script to FFC 1:
 

ffc script drawOverLink {
    void run() {
        while(true) {
            Screen->FastTile(4,Link->X,Link->Y,100,2,OP_OPAQUE);
        }
    }
}

This is a perfectly legal FFC script that will compile properly.  It will draw tile 100 on layer 4, at Link's position, then loop back around and do it again.  When you enter the screen...

  • ZC will do all it's pre-script engine stuff, go through the global, then start the drawOverLink script since that's FFC 1.
  • Since the script is just starting, it begins with the run() function, then checks the while loop.
  • Yup, true is still true, so we will enter the loop.
  • We'll tell the engine to draw that tile on layer 4 when it draws the screen next time.
  • We reach the end of the while loop and check the condition again.  True is true, let's do it again.
  • We'll tell the engine to draw that tile on layer 4 when it draws the screen next time.
  • We reach the end of the while loop and... hey, wait a minute, we already did all this!

So now ZC is stuck executing this loop, forever, and there's no way to get out of it.  Now let's add a Waitframe() like we should have:

ffc script drawOverLink {
    void run() {
        while(true) {
            Screen->FastTile(4,Link->X,Link->Y,100,2,OP_OPAQUE);
            Waitframe();
        }
    }
}

Now this is what ZC will do when we enter the screen:

  • ZC will do all it's pre-script engine stuff, go through the global, then start the drawOverLink script since that's FFC 1.
  • Since the script is just starting, it begins with the run() function, then checks the while loop.
  • Yup, true is still true, so we will enter the loop.
  • We'll tell the engine to draw that tile on layer 4 when it draws the screen next time.
  • We hit the Waitframe() function, so now ZC will bookmark where it was on FFC 1's script, then go to the next script that needs to execute.
  • ZC will execute FFC 2, 3, and 4 normally - when those hit their Waitframes or otherwise end, they'll progress to the next script.
  • ZC finishes all the script handling, then does its normal post-script engine stuff, then draws the screen.  This is 1 frame.
  • ZC loops back around, does more engine stuff, executes the global, then enters FFC 1's script.
  • ZC remembers the Waitframe() that we ended at last time and cuts in right there.
  • Now we're at the end of that while loop, so we check the condition (true is true) and restart.
  • We'll tell the engine to draw that tile on layer 4 when it draws the screen next time.
  • We hit the Waitframe() function, so now ZC will bookmark where it was on FFC 1's script, then go to the next script that needs to execute.

And you can follow it out from here.
 
This is why ZC will freeze if you forget to put a Waitframe() inside a while(true) loop in an FFC script.  ZC just doesn't know to stop executing a script unless you tell it to - and that's what Waitframe() does.  The reason why Demonlink's original script does not completely freeze is that it's inside a non-infinite for loop which can only run 120 times - so essentially you're telling ZC to draw that combo 120 times, then it exits the for loop, then it reaches the end of the FFC script (which automatically goes to the next script in ZC's list).
 
This is also why functions added to your global active loop shouldn't use Waitframe() functions except for some very rare exceptions - you want the global active script to execute all of those functions every frame, so you just put one Waitframe() in in the global active loop, and structure all the function calls from global so that they only run once and then end.
 
Usually, for most FFC scripts that need to run for more than one frame, you will be structuring your scripts with one Waitframe() at a single looping point.  However, there's no reason that you have to use this structure, and for certain applications it will make it much harder to do so.  Consider this script, which makes an FFC vibrate on screen:
 

ffc script vibrate {
    void run() {
        while(true) {
            int angle = Rand(360);          //Calculate a random angle...
            this->X += VectorX(2,angle);    //Move slightly in that direction.
            this->Y += VectorY(2,angle);
            Waitframes(2);                  //Wait for 2 frames.
            this->X -= VectorX(4,angle);    //Move twice as far in the opposite direction.
            this->Y -= VectorY(4,angle);
            Waitframes(2);                  //Wait 2 more frames.
            this->X += VectorX(2,angle);    //Move slightly in the original direction - this will reset to the original position 
            this->Y += VectorY(2,angle);
        }
    }
}

Instead of looping around a single Waitframe(), this uses two sequential Waitframes(int frames) functions. This allows it to easily reference the same "angle" variable across multiple frames and recalculate it every four frames without having to do any manual accounting for it. Note that there's no Waitframe() at the end of the while(true) loop. This script will reset the FFC's position then recalculate the "angle" variable and start the next vibration on the exact same frame.

Hopefully this helps some people understand when to use Waitframe() and how you can structure a script to take advantage of the different ways that you can use it.


Edited by Mitchfork, 13 July 2021 - 09:29 AM.

  • Anthus, Twilight Knight, Russ and 4 others like this

#2 Emily

Emily

    Scripter / Dev

  • ZC Developers

Posted 08 July 2021 - 04:39 PM

(Note this is a heavily simplified but basically correct order - you can see the precise order that ZC handles things in zscript.txt under "Instruction Processing Order and General Timing")

 

*docs/ZScript_Timing.txt, in 2.55


  • Mitchfork likes this

#3 Demonlink

Demonlink

    Lurking in the shadows...

  • Members
  • Real Name:Miguel
  • Location:Wouldn't you like to know?

Posted 08 July 2021 - 05:51 PM

So, in case of enemy spawn, I've read somewhere that it takes 4 frames for enemies to appear on screen. Thus, any scripts that need to check on screen enemies and stuff, should have a Waitframes(4) correct?

My question is more about the logic behind how this is processed. Let's say this is FFC Script 1, and I have a Waitframes(4); will ZC jump to the next FFC Script and not execute Script 1 until after 4 frames have happened? Or will nothing happen for 4 frames and thwn the next script is executed?

#4 Emily

Emily

    Scripter / Dev

  • ZC Developers

Posted 08 July 2021 - 06:20 PM

So, in case of enemy spawn, I've read somewhere that it takes 4 frames for enemies to appear on screen. Thus, any scripts that need to check on screen enemies and stuff, should have a Waitframes(4) correct?

My question is more about the logic behind how this is processed. Let's say this is FFC Script 1, and I have a Waitframes(4); will ZC jump to the next FFC Script and not execute Script 1 until after 4 frames have happened? Or will nothing happen for 4 frames and thwn the next script is executed?

'Waitframes()' is not an internal function, it's a function in 'std_functions.zh'

void Waitframes(int n) 
{
	while( n-- > 0) Waitframe();
}

so, it's just a loop with 'Waitframe();' in it. Thus, each frame, ZC will execute the script, hit the waitframe, and continue with the rest of the frame.

 

Also, fun fact, if you call 'Waitdraw()' twice in one frame, the second one acts like 'Waitframe()'. So, uh, don't do that.


  • Demonlink likes this

#5 Mitchfork

Mitchfork

    no fun. not ever.

  • Contributors
  • Real Name:Mitch
  • Location:Alabama

Posted 08 July 2021 - 09:55 PM

Yeah, there's no way to "skip" a script executing - frames are always waited on one at a time.



#6 Saffith

Saffith

    IPv7 user

  • ZC Developers

Posted 08 July 2021 - 11:52 PM

It makes more sense to think of it from the script's point of view rather than the engine's. A Waitframe call isn't telling the game to wait a frame; it indicates that the script will wait a frame before continuing.


  • Twilight Knight, Mani Kanina, Orithan and 2 others like this

#7 Matthew

Matthew

  • Administrators
  • Real Name:See above.
  • Pronouns:He / Him
  • Location:Ohio

Posted 09 July 2021 - 07:59 AM

This clears up a lot of confusion. Thanks for going so in-depth regarding this, Mitch. I had no idea my little query was worthy of such an explanation!  :shy:



#8 Evan20000

Evan20000

    P͏҉ę͟w͜� ̢͝!

  • Members
  • Real Name:B̵̴̡̕a҉̵̷ņ̢͘͢͜n̷̷ę́͢d̢̨͟͞
  • Location:B̕҉̶͘͝a̶̵҉͝ǹ̵̛͘n̵e̸͜͜͢d҉̶

Posted 12 July 2021 - 11:47 PM

You know this a good tutorial when it includes setting this->Y with VectorX instead of VectorY, a mistake that many a tired scripter has made before. :blah:

It's preparing you perfectly for the big leagues of spending half an hour wondering why your thing is moving to the wrong coords.


  • Mitchfork, Russ and Matthew like this

#9 Mitchfork

Mitchfork

    no fun. not ever.

  • Contributors
  • Real Name:Mitch
  • Location:Alabama

Posted 13 July 2021 - 09:29 AM

You know this a good tutorial when it includes setting this->Y with VectorX instead of VectorY, a mistake that many a tired scripter has made before. :blah:

It's preparing you perfectly for the big leagues of spending half an hour wondering why your thing is moving to the wrong coords.

I'm glad that I'm not the only one that this happens to, wow


  • Russ and Evan20000 like this

#10 Evan20000

Evan20000

    P͏҉ę͟w͜� ̢͝!

  • Members
  • Real Name:B̵̴̡̕a҉̵̷ņ̢͘͢͜n̷̷ę́͢d̢̨͟͞
  • Location:B̕҉̶͘͝a̶̵҉͝ǹ̵̛͘n̵e̸͜͜͢d҉̶

Posted 14 July 2021 - 03:21 PM

I'm glad that I'm not the only one that this happens to, wow

There's a funny progression of doing this a lot, to doing it rarely, to not doing it but spending 3 times as long writing every vector statement to make sure you didn't do it so I'm not really sure if this saved any time in the long run.


  • Mitchfork likes this


0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users