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.