Search Unity

Custom timers to trigger action vs animation events

Discussion in 'Scripting' started by mrCharli3, Dec 16, 2020.

  1. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    976
    I am building an RTS, so it's possible to have hundreds of units at once. Currently I handle a lot of actions using animation events, for example:

    In my attack animation - I deal damage to current target using an animation event triggered right when sword is at zenit.
    In my chop wood animation - I gather wood from current target using an animation event triggered right when axe is at zenit.

    And so on...

    The reason I did it using animation events is since it keeps the code very clean. However, I am starting to realize that I need to cull animations, maybe even use some crowd animation tech that uses shaders to animate.

    This means, if I rely on animation events, they won't trigger if I'm not looking at the animation.

    How do you handle these scenarios, is the best option to just use a custom timer, like so:

    Code (CSharp):
    1.  
    2. timer += Time.deltaTime;
    3.         if (timer >= attackSpeed && !animPlaying)
    4.         {
    5.             PlayAnim();
    6.             animPlaying = true;
    7.         }
    8.         else if (timer >= animEvent && animPlaying)
    9.         {
    10.             PlaySound();
    11.             DoDamage();
    12.         }
    13.  
    The above code is not complete but hopefully gives an idea of what I mean. Is there some better way to do this? The above code feels rly ugly and messy. But Im not sure how else I would get full control of when to play anim, and when to play animEvent.

    EDIT: Another way to frame the question; How can I trigger an action at a specific time of an animation, without relying on the animation running (since it might be culled).

    EDIT: Looking into curves looks interesting, but not sure how it works with culled animations: https://www.youtube.com/watch?t=28m18s&v=Xx21y9eJq1U&feature=youtu.be
     
    Last edited: Dec 16, 2020
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    Are you sure that culling prevents animation events? That may be worth testing.

    A while ago, I tried to use my own timers to do things synchronized to an animation (controlled by an Animator), and I discovered that my timing was off because (I think) the Animator's blend logic slightly changed the time it took to do things, and I had to switch to animation events in order to get proper synchronization with the visuals.

    But if you can't use animation events, I'm not sure what alternative you have.
     
  3. ensiferum888

    ensiferum888

    Joined:
    May 11, 2013
    Posts:
    317
    Culling absolutely kills the animation events. I had an issue like this and found two ways to go around it.

    The first one is selective culling, where I can know beforehand if an animation has events tied to it. My game ai is driven by a finite state machine but here's my PlayAnimAndWait state constructor:

    Code (CSharp):
    1. public PlayAnimAndWait(string name, CitizenAI _cs, bool frustrumCulled = true){
    2.     // cAnim is just the Animation component (legacy)
    3.     animName = name;
    4.     cs = _cs;
    5.     waitTime = cAnim[animName].length / cAnim[animName].speed;
    6.     if(frustrumCulled){
    7.         cAnim.cullingType = AnimationCullingType.BasedOnRenderers;
    8.     }
    9.     else{
    10.         cAnim.cullingType = AnimationCullingType.AlwaysAnimate;
    11.     }
    12. }
    And here's the update method for this state:
    Code (CSharp):
    1. public void StateUpdate(){
    2.     waitTime -= Time.deltaTime;
    3.     if(waitTime <= 0f){
    4.         Exit();
    5.     }
    6. }
    That way important animations will play and carry out their events.

    You could tinker with this method to add support for events in normalized time or something as well.

    The other solution that I opted was to simply slice my animations, so for example now there's an attack_before and attak_after hit animation for each attack. I can stack those two with whatever I want in between (deal dmg, heal, pick up something, etc).

    So for example now when my little guys fight I can leave the animations culled because the dammage is no longer tied to an animation event but its own state between the two animations:

    Code (CSharp):
    1. nextStates.Add(new PlayAnimAndWait(animPrefix + "_Before", cs));
    2. nextStates.Add(new Attack(cs, currentTarget, true));
    3. nextStates.Add(new PlayAnimAndWait(animPrefix + "_After", cs, false));
    4. nextStates.Add(new PlayAnim(idleAnim, cs));
     
  4. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    976
    Wow, amazing answer, 2 options I did not consider at all. I guess pretty much all my animations have events, except like idle and walking. How do you deal with the performance hit of animating so many units? Do u use some sort of crowd animator or shader? I've looked at how to make one and seen some assets as well. Or do u just use normal individual animators? I find they are by far the heaviest hit in my profiler.

    I also see you run your statemachine each frame, would it not be better to run it every .2 seconds or so? Not sure how many units u have so maybe its redundant, but all my units have an exposed value for how often to update their state machine, so I can have important units update quite often, and background units update less frequently.
     
    Last edited: Dec 17, 2020
  5. ensiferum888

    ensiferum888

    Joined:
    May 11, 2013
    Posts:
    317
    I'm using Animation (legacy) and not the Animator (mecanim) component. I understand there is a lot more overhead when using the mecanim rigs. And for my part no I can have about 400-500 units on the screen and still sit somewhere close to 70-80 FPS.

    My skeletons have a really low bone count, my skinned mesh have 1500-2500 verts and use a single material.

    The overhead from having the state machine run every frame is not noticeable at all, it eats about 5% of my total frame budget, animations and rendering will become a bottleneck before all the updates do.

    Of course heavy operations are done at intervals rather than every frame. For example all my units are represented in a QuadTree that's updated every 0.5 seconds. When they move they don't check the quadtree every frame, instead I query it every second and keep a local list of nearby units. Since the detection range is between 30 and 60 tiles and units move 3 units per second I'll always have the correct count of units around me.

    All states update each frame but some of the heavy lifting might be offset within them. So far it's been extremely fast, stable, reliable and super easy to build new behaviors as I don't need to modify a monolithic 3000 lines if/else function. I just create a new state pop it on the statemachine stack and done.
     
  6. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    976
    Very interesting, thanks for sharing! Like you I also get about 3-5% of my frame budget from scripts, rest is rendering and animators.

    Feels so wrong that a legacy tech is more performant, but I have read that in several other places too. I've looked a lot at crowd animator assets that seem to improve performance by several 100%, but think that would come at the cost of animation events.

    Anyway, I appriciate the help and really look forward to your game :)