Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Can I do frame-perfect scripting without animation events?

Discussion in 'Scripting' started by TabuuForteAkugun, Oct 22, 2021.

  1. TabuuForteAkugun

    TabuuForteAkugun

    Joined:
    Sep 28, 2015
    Posts:
    56
    Hi!

    I'm making a fan-made Smash Brothers platform fighter in Unity. I have a lot of stuff working: like how to play animations at the right times with the right button combinations, basic collisions for ground, hovering platforms, ceilings and walls (using a custom physics system and not Rigidbody). I can move, jump, taunt, "attack" (with no hitboxes yet), crouch, move through hovering platforms, etc. Basically, I can control and move my fighters around with almost nary an issue (except for my controller being not quite complete yet.)

    I'm developing something called AnimCommand, a custom scripting "language" / system just for this game: a scripting system for characters' move sets where scripts for each relevant animation are stored in a ScriptableObject, where the execution is done by the system's own associated C# interpreter MonoBehaviour. It can play sounds, wait (with varied results upon repeating the animations), stop sounds, play particle effects, change script upon changing animator controller states, manipulate variables, and slow down/speed up animations for attacks to sort of "create" ending/recovery lag. Problem is, I can't seem to get the frame timing frame-perfect, or consistent, to my liking. This poses problems for both particles and hitboxes, making attacks all wonky.

    What I mean is that, if I: say, play a Particle Effect. It'll play at different times, at different locations, etc. I wait, say, for frame number 5 in the current animation. The effect never plays at the right time, or never does it consistently. I want to avoid having to turn to animation events due to certain limitations (event functions only using one parameter, for example, while my hitbox command has over 20). I'm using a coroutine to read through each script line by line, and have it yield a new WaitForSeconds(framesToSeconds) when a Timer (wait) command is reached, after which it starts over each time a script re-starts or a new one is loaded.

    But that
    Code (CSharp):
    1. yield new WaitForSeconds(framesToSeconds)
    is where the problem lies. It waits for varying numbers of seconds each time the animation runs. So it could be called on frame 5 like before, or frame 4 or 6 or even 7. Here's the algorithm I'd come up with. I need help figuring out consistent, frame-dependent timing for AnimCommand, and how on Earth to handle frame stutter: ie lag or picking up frames. I even get performance variations in the Editor play mode VS a development build of the same project.

    Current code:

    Code (CSharp):
    1. yield return new WaitForSeconds(acmd_Effect.syncFramesToWait / motion.GetFrameRate());
    Where acmd_XXX.syncFrameToWait = the amount of animation frames to wait for as an Int, divided by the animation's current float frame rate(60fps in most cases). So say I want an effect to play at frame 5 again, it's 5/currentFrameRate (if 60, that's 0.83333...... seconds) But that will cause events to fire off at random times. I know I'm missing something, but what that is completely escapes me. I'd post the whole Coroutine, but the timing is the issue, everything else works perfect, so I posted the WaitForSeconds.

    What I need is to know how to convert a positive integer amount of frames within the currently-playing animation state to a float timeInSeconds for use in yield new WaitForSeconds(). so that the timing will be the EXACT SAME on each animation play, bar for performance drops/spikes (which should still be a very similar timing thing) A former acquaintance of mine who RE's game console stuff tried explaining this to me, but the actual info passed right over my head as he preferred me to figure this out myself. I wanted to, I really did, but I just can't do that. It's kinda embarrassing for me to admit that I've hit such a possibly simple snag in my project. I'll bet if I get the answer, I'll be like "DUH, that shoulda been obvious lol"

    Thank you for reading this wall of text, and I hope it's understood! haha
     
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    The most simple explanation possible:
    - there is no precise WaitForSeconds() call everything depends on the frame-timing and the elapsed time (WaitForSeconds counts elapsed time and when the current time has passed the target time, you will get the control back in the next frame)
    -
    yield return null;
    will yield for the engine until the next frame, you can build your own counter on this one and it will be guaranteed that frame-precise (but not time-precise, since frames aren't time-precise)
    - oh, and I really recommend using animation event callbacks instead of anything else
     
    TabuuForteAkugun likes this.
  3. TabuuForteAkugun

    TabuuForteAkugun

    Joined:
    Sep 28, 2015
    Posts:
    56
    Okay. But I'm looking to control in animation frames. Not in Unity Update() frames, which updates at very irregular intervals from what I've gathered.

    And what're those callbacks? Never used that before!
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,749
    Here's the starting point to reason about this stuff:

    https://docs.unity3d.com/Manual/ExecutionOrder.html

    For my KurtMaster2D game package, I ported old MS-DOS, PalmOS and TRS-80 games to run their native code and present it in Unity, so I wanted to have nice slow 12fps-18fps frame play (or maybe as high as 30fps), which is how these games were all integer-framecount-tuned.

    I use Unity Update() so I never miss things like GetKeyDown() or touches, then halfway through the update that gathers all the input, I ask if it is even time to call the underlying native layer to advance the next integer game frame.

    This is the full logic... it's that simple:

    Code (csharp):
    1. // handle all Unity input, etc.
    2.  
    3.         float OneFrameTime = 1.0f / dispatcher1.currentframerate;
    4.         gameTime += Time.deltaTime;
    5.         if (gameTime < OneFrameTime)
    6.         {
    7.             return;
    8.         }
    9.         gameTime -= OneFrameTime;
    10.  
    11. // pump the native code and re-render the surface
    dispatcher1.currentframerate
    is an integer returned by each game saying how many frames per second it wants to be pumped.

    That way it will automatically adjust back and forth within a given frame so it never lags or leads by more than one Unity update frame, and I can count on a certain FPS at the native internal code layer.

    Apple iTunes: https://itunes.apple.com/us/app/kurtmaster2d/id1015692678
    Google Play (including TV): https://play.google.com/store/apps/details?id=com.plbm.plbm1
     
  5. TabuuForteAkugun

    TabuuForteAkugun

    Joined:
    Sep 28, 2015
    Posts:
    56
    So say I make my game at 60 fps, which is what I'm trying to do. In that case, "dispatcher1.currentframerate" would be replaced by 60, or the game's current fps (the one shown in the profiler)?

    This makes sense. I'll try it out.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,749
    Note that my code fails when I am not getting Update() called at least as often as I need it, and I don't need it very fast.

    I decided on modern hardware that was unlikely to be a case to hassle with, such as by double-pumping in a given frame, but at 60fps you might need to consider it. I would at least find OUT if you're dropping frames at 60fps.
     
  7. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    But you were talking about coroutines. Coroutines are tied to the engine framerate. That's why I'm recommending animation events, those can be called at a specific moment during the animation playback (and if you don't put too much in those callbacks, you can get away with a lot). And if I remember correctly they can handle objects as parameter, so the "only takes one argument" is kind of moot. Just pack your parameters together.
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,749
    To be clear they are still tied to Update() (like all scripting in Unity), but AnimationEvents will guaranteed be fired before the next Update() after where you put them in the Animation, so you can control precisely where in the anim the code gets called. See the timing link above.
     
    Lurking-Ninja likes this.
  9. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    Yeah I should have worded my answer better. You're obviously right and my answer sounds like they weren't in the Update loop. Sorry about that.
     
  10. TabuuForteAkugun

    TabuuForteAkugun

    Joined:
    Sep 28, 2015
    Posts:
    56
    Yikes so it would seem I don't have much of a choice but to use the events.
     
  11. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    I mean you could start a coroutine for the animation and
    yield return null
    every frame and calculate the elapsed time in there and call a callback when your event should happen, but you would only do the same thing what the animation system is already doing and you would lose the possibility to edit it in the editor visually (you would need to write your own custom editor to edit your stuff visually if you wish so).
    I really don't see the advantage over the existing animation events.
     
  12. TabuuForteAkugun

    TabuuForteAkugun

    Joined:
    Sep 28, 2015
    Posts:
    56
    I apologize for my bad wording about coroutines, I just want to time it similarly to the animation system: that system gives out consistent results every frame.