Search Unity

Simple sliding door animation: speeds up and finishes current clip before starting next one

Discussion in 'Animation' started by chris_gamedev, May 18, 2019.

  1. chris_gamedev

    chris_gamedev

    Joined:
    Feb 10, 2018
    Posts:
    18
    My noob question of the day and the first day I'm playing with animations in Unity.

    Fairly simple set up, I have a parent game object which has a cube as a child. The parent has an animator and controller attached to it.

    I have an Open clip which transforms the child position X from 0 to -1. Similarly, I have a Close clip which transforms X from -1 to 0. With these transitions:

    upload_2019-5-18_2-26-7.png

    I have a script which triggers an Open trigger or Close trigger depending on the starting state when a keyboard key is pressed.

    For the most part, it works fine. The door slides to open when the key is pressed, and then slides to close again when the key is pressed.

    The problem occurs if the key is pressed while it is in the middle of the clip running.

    I have "Has Exit Time" toggled off for all of the transitions, but despite this, the clip completes before starting the next one. The weird thing is, it speeds up to finish the existing clip very quickly before starting the next one at normal speed.

    I'm not observing the same behaviour in tutorials I've seen.

    Actually, just created a video to demonstrate it, and actually the full transform isn't completing when it is interrupted so maybe I've done something wrong with that, though they are fairly simple with only two key frames.

    https://imgur.com/W1ap0y8

    Thanks for reading :)
     
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    502
    I'm currently working on an update for my Animancer plugin which includes a new example scene that handles this exact problem. It uses only one animation and simply controls the speed to play it forwards or backwards so it can swap directions at any time.
     
    chris_gamedev likes this.
  3. chris_gamedev

    chris_gamedev

    Joined:
    Feb 10, 2018
    Posts:
    18
    Thanks, this gave me an idea which involved simply modifying a multiplier speed parameter in code, though perhaps my understanding of the magic going on in your plugin (I haven't actually downloaded it, yet) was a bit naive.
    It kind of works. Previously I had calls to
    SetTrigger()
    one that triggered the Open clip, and another that triggered Close. The script now looks like:
    Code (CSharp):
    1. public void Interact()
    2. {
    3.     animator.SetTrigger("Open");
    4.  
    5.     if (isOpen)
    6.     {
    7.         isOpen = false;
    8.         animator.SetFloat("Speed", -1f);
    9.     }
    10.     else
    11.     {
    12.         isOpen = true;
    13.         animator.SetFloat("Speed", 1f);
    14.     }
    15. }
    As far as I can tell, it's almost as if the animation never stops when it gets to the first frame or the last frame.

    Because if you open the door fully (3 seconds) then wait 3 seconds, and then interact with the door again nothing happens for about 6 seconds. I might be reading that wrong as it doesn't always seem to be the case but that's roughly what's happening.

    In better news, if I do not allow the animation finish, it does now interrupt it correctly, so there is that :) But either the approach is fundamentally flawed or I've missed something crucial.
     
  4. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    502
    That's correct. Regardless of whether the animation is looping or not, the time continues to increase after it reaches the end, it simply decides whether to clamp or wrap the output.

    In my example I solve that by pausing the PlayableGraph when the animation ends which causes it to stop evaluating entirely, so the time stops increasing and you save some performance too. I'm not sure what would be the best way to achieve the same thing in Mecanim because it doesn't have a simple OnEnd callback. Setting the speed to 0 would do it, but you would need to check the animation time every frame to wait until it is done.
     
    chris_gamedev likes this.
  5. chris_gamedev

    chris_gamedev

    Joined:
    Feb 10, 2018
    Posts:
    18
    Thanks for that tip.

    As a result, I've come up with something that works, and doesn't require checking every frame. I'm not sure how much I like this, yet, but it does have the bonus of actually working, and consistently, so it can't be too bad ;)

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [RequireComponent(typeof(Animator))]
    6. public class BaseDoor : MonoBehaviour, IInteractable
    7. {
    8.     private bool isOpen = false;
    9.     private Animator animator;
    10.  
    11.     private void Awake()
    12.     {
    13.         animator = GetComponent<Animator>();
    14.     }
    15.  
    16.     public void Interact()
    17.     {
    18.         animator.SetTrigger("Open");
    19.  
    20.         if (isOpen)
    21.         {
    22.             isOpen = false;
    23.             animator.SetFloat("Speed", -1f);
    24.         }
    25.         else
    26.         {
    27.             isOpen = true;
    28.             animator.SetFloat("Speed", 1f);
    29.         }
    30.     }
    31.  
    32.     public void OnAnimationEnd(string position)
    33.     {
    34.         bool setSpeed = false;
    35.  
    36.         if (position == "start")
    37.         {
    38.             setSpeed = (animator.GetFloat("Speed") < 0);
    39.         }
    40.         else
    41.         {
    42.             setSpeed = (animator.GetFloat("Speed") > 0);
    43.         }
    44.  
    45.         if (setSpeed)
    46.         {
    47.             animator.SetFloat("Speed", 0f);
    48.         }
    49.     }
    50. }
    51.  
    52.  
    It requires an animation event at the start and stop of the animation which I'm not 100% keen on, but it does work and is fairly clear what is going on. And, as I'm trying to re-use as much logic as possible across many different door types, the events here may actually be useful extension points.

    My next job is to figure out a better way of triggering this. Currently firing a ray to the door to open it. That's fine for a normal pivoting swing door, but much harder to trigger with a sliding door which only has a few pixels showing after it has opened...

    Thank you @SilentSin for all of your help.
     
  6. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    502
    Give the door a collider as usual, but also put a trigger in the whole doorway (or just a collider on a layer that doesn't collide with anything if you don't have "raycasts hit triggers" enabled) and allow interactions with either one.
     
    chris_gamedev likes this.
  7. chris_gamedev

    chris_gamedev

    Joined:
    Feb 10, 2018
    Posts:
    18
    Thanks again :)

    At the time I was thinking there would be a way to collide with a trigger, otherwise I would have to do some sort of sphere cast or something.

    As it happens, Physics.raycast has an optional sixth argument, so you can conditionally allow raycasts to hit triggers, rather than changing it globally:
    Code (CSharp):
    1. Physics.Raycast(origin, mainCamera.transform.forward, out hit, maxDistance, interactionLayer, QueryTriggerInteraction.Collide);
     
  8. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    502
    Nice.

    It would probably still be more efficient to use layers though, so it doesn't need to check for intersections between the trigger and other objects if you aren't actually doing anything with them.
     
    chris_gamedev likes this.