Tuesday 2 May 2017

Animation sequence

I have been trying for a while now to create a sequence of animation. By that I mean a chain of animations that play without the user input. Very much like a cutscene or a tutorial, that can be played out to the user without interaction.

After looking up for tutorials I couldn't find anything relevant, so I came up with my own implementation.

I have also played around with the Unity Timeline feature, which is still in experimental mode, and while I was able to create some sequences, I still could not play animations the way I wanted, such as, having the player character move around, shoot, jump and do other things.

In the end I found a solution which, although not optimal, works pretty well and allows me to create scripted animations for each GameObject in the current scene.

I'm still using this  assent, just like the previous posts.

The way I had it in my mind was to have all my sequences as asset files, which I could then plug int a sequence player in the scene.

Firstly, let's take a look at the Sequence script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public abstract class Sequence :  ScriptableObject {

 public bool playOnce;
 public bool hasBeenPlayed = false;
 public abstract void OnSequenceEnd();
 public abstract IEnumerator sequence(GameObject[] actors);

 protected void CloseAllDialogs(GameObject[] actors)
 {
  for(int i=0; i<actors.Length;i++)
  {
   actors [i].GetComponent<Speak> ().CloseComic ();
  }
 }

}

This is nothing but a base class which will use to create our sequences asset files. As you can see, it's a ScriptableObject, which allows us to create an asset out of it.

The animation sequence itself is going to be a coroutine, which is the IEnumerator on line 6, which will be differet for each animation sequence we wish to write.

The CloseAllDialogs method is strictly related to my current project and should not be relevant for this example.

One limitation I had to work around was the inability of ScriptableObjects to reference objects in the scene. It would have been much easier and neat to have all the GameObjects I wanted to animate in my ScriptableObject file, but that is simply not possible. Instead, the SequencePlayer will be able to "select" objects form the scene and pass them to the scriptable object, and that is the actors GameObject array you see being passed to our co routine on line 6.

This is the SequencePlayer script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SequencePlayer : Interactable {

 public GameObject[] actors;
 public Sequence sequence;
 public bool playOnAwake = false;
 public bool playOnCollision = false;
 public float startDelay = 0;

 void Start()
 {
  if (playOnAwake)
   Invoke ("PlaySequence", startDelay);
 
 }

 public void PlaySequence()
 {

   GameController.instance.currentLevel.SetCurrentState (new PlayableLevel_ConversationState ());
  StartCoroutine (sequence.sequence (actors)); 
 
 }

 override public void OnInteract()
 {
  if (playOnCollision) {
  
   Invoke ("PlaySequence", startDelay);
   GetComponent<Collider2D> ().enabled = false;
  
  }
 }

}

This is, in fact, a Monobehavior script. You can see it inherits from a class called Interactable, which is simply a base class I'm using in my project that represents objects that can interact with the player.

This script will be placed on an empty GameObject in the scene, which should have a collider attached to detect the presence of the player character. I decided to do it this way because I want my animations to be triggered by two main factors: either the scene starts or the player has interacted with our SequencePlayer GameObject, which means has reached an area where an animation sequence should be played.

The 2 bool variables on line 5 and 6 determine which one is the trigger method. If the sequence should be played on awake, as soon as the script is initialized the animation is triggered, with a delay if necessary (line 12). Otherwise, will be triggered once the collision has been detected (line 24 - 31).

The GameObject array actors will need to be pre filled with all the objects we wish to animate. This array is then passed to our ScriptableObject sequence, which is going to do all the magic for us.

Let's have a quick look at our scene now:


We only have the player character on the right. At the moment it is off camera, in our animation we will have it walking in the viewport, greet the player and leave.

An empty GameObject is present in the scene, called Sequencer, and has the SequencePlayer script attached. The inspector will look like this:


As you can see, I passed the Player object to the actors array, and I plugged a Sequence file called ExmapleSequence into the Sequence public field (which is a ScriptableObject).

This is the ExampleSequence scriptable object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[CreateAssetMenu(fileName="ExampleSequence",menuName="AnimationSequence/ExampleSequence")]
public class ExampleSequence : Sequence {

 override public IEnumerator sequence(GameObject[] actors)
 {
  GameObject playerObj = actors [0];

  playerObj.GetComponent<Rigidbody2D> ().isKinematic = false;

  while (playerObj.transform.position.x < 0) {
   playerObj.GetComponent<CharController> ().Movement (new Vector2 (1, 0));

   yield return null;
  }

  playerObj.GetComponent<CharController> ().Movement (new Vector2 (0, 0));

  yield return new WaitForSeconds (2);

  playerObj.GetComponent<Speak> ().Say ("Greetings!");

  yield return new WaitForSeconds (2);

  playerObj.GetComponent<Speak> ().CloseComic ();

  while (playerObj.transform.position.x >-15) {
   playerObj.GetComponent<CharController> ().Movement (new Vector2 (-1, 0));

   yield return null;
  } 

  OnSequenceEnd ();

 }

 override public void OnSequenceEnd()
 {
  //Currently not used
 }

}

The very first line is used to create the actual asset file. In the editor, if we now navigate to Asset->Create we can see our ExampleSequence asset, which can be instantiated as an asset file in our project folder:


Then, we proceed creating the actual sequence, in the IEnumerator.

In order to move the character around and speak I have created a character controller script, which I am not going to display here as it is quite extensive. However, there are many tutorials on how to create a character controller, but really it's just a script that enables our character movement and apply animations through an AnimatorController.

On line 6 we grab the reference to our player object.

We then proceed to set its RigidBody2D to respond to physics. The while loop will call the Movement(,,,) method of the CharController script which simply moves the object to a certain point, checking the X value of the position. This method also applies animation with the AnimatioController whithin the CharController script.

We then stop the movement (line 16), we wait for a couple of seconds and we get the player to "say" something. This is done very simply with the use of a Canvas in world space. Finally, we close the comic and send the player back the way he came.

And that is it.

This what it looks like in the end:


Clearly, this is not optimized. Each time we need to grab components from the player object, and that is not ideal. To improve on this, we could cache all the components at the beginning of the script and call them accordingly. For this example, this is acceptable.

Conclusion

This simple method is a good starting point to create scripted animations. It can be a little time consuming having to write a complete sequence, but it allows for great control over all the objects we wish to animate. I'm also using this technique to create tutorial sequences for the player, in which the animation sequence is not controller by time, rather, by the user input this time. Before proceeding to the next instruction, instead of using WaitForSeconds(..) we can pass in a while loop, like so:

1
2
3
   do {
    yield return null;
   } while (!Input.GetMouseButtonDown (0));

This way, the script will resume only after the user has tapped the screen.

I think a lot can be done to optimize the scripts, however, depending on your project, this could be a decent solution for cutscenes and scripted animations.