Skip to content

Latest commit

 

History

History
950 lines (667 loc) · 27.9 KB

File metadata and controls

950 lines (667 loc) · 27.9 KB

Outline | Previous: Character Animations | Next: Level Won Timeline

12) Intro Timeline

Create an animated sequence for the intro to level 1.

12.1) Intro Animation

YouTube | Source before | Source after

Create an animation for the cloud entrance at the start of the level.

Create a timeline which enables the LevelController and Hammers after the intro is complete.

How

Intro animation:

  • Create an animation for the EvilCloud's sprite Animations/CloudLevel1Entrance.anim
    • Click record:
      • Start by moving the cloud off-screen.
      • Then over time, modify its position to create a dramatic entrance.
      • The timeline should end with the cloud at position 0 (the starting position at the top left).
  • Select the file Animations/CloudLevel1Entrance:
    • In the Inspector uncheck 'Loop Time'.


Intro timeline:

  • Open menu Window -> Timeline Editor.
  • Select the EvilCloud's sprite.
    • Click 'Create'. Save as Animations/Level1Entrance.playable
    • Select 'Add from Animation Clip' and select CloudLevel1Entrance.
  • Select the clip just added and in the Inspector:
    • Speed: .1
    • Hit play in the Timeline Editor and adjust the speed for your animation.
  • Drag the parent Hammers GameObject (which holds all the hammers) onto the timeline and select Activation Track.
    • Move the box for the script so that it starts after the cloud animation completes.
      • The start of the box represents when it will be enabled.
      • The end must align with the end of the time timeline to prevent it from being disabled.
  • Repeat, creating activation tracks for the LevelController and the Ladders.
  • Close the Timeline Editor window.
  • Disable the GameObjects: LevelController, Ladders, and Hammers.


Test:

  • The cloud's animation should complete and then the ladders, hammers, and character fade in.
    • Enemies will spawn before the intro completes, this will be fixed next.


What is a Unity Timeline / Activation Track?

Timeline is a new feature released with Unity 2017. It's a higher level component than the Animator Controller, used to coordinate animations and trigger events across several objects in the scene with an interface that resembles the Animation timeline.

Previously, achieving similiar results would have required a script. Now you can manage the sequence visually if you prefer.

'Add Animation From Clip' plays an animation during the timeframe specified, overriding what the Animator controller for that object would have done.

Activation Tracks are one of several ways that you trigger behaviour with the Timeline. An activation track will enable a GameObject where the track begins in the timeline, and disable it again where it ends. If the activation track ends at the very end of the entire timeline then it will remain active after the timeline completes.


How might we do this without using the Timeline Editor?

There are always alternative ways to achieve a goal, particularly true in this case since the Timeline Editor is brand new.

An alternative solution might be something like this:

  • For the EvilCloud, simply play the intro animation with a default state in the Animator Controller.
  • Add a 'InvisibleFor' value to the FadeInThenEnable script, and time that to coordinate with the intro.
  • Add an initial sleep time to the spawner to align with the intro animation.

The advantage to using the Timeline is as you make adjustments to the sequence, you can make those changes visually and aligning the time between objects may be easier.

<hr


12.2) Start Spawners After Intro

YouTube | Source before | Source after

Disable the spawners and create a script to later enable them when the level intro completes.

How

Create EnableComponentsOnTimelineEvent:

using UnityEngine;

public class EnableComponentsOnTimelineEvent : MonoBehaviour
{
  [SerializeField]
  TimelineEventPlayable.EventType eventType;

  [SerializeField]
  Behaviour[] componentListToEnable;

  public void OnEvent(
    TimelineEventPlayable.EventType currentEventType)
  {
    if(currentEventType == eventType)
    {
      for(int i = 0; i < componentListToEnable.Length; i++)
      {
        Behaviour component = componentListToEnable[i];
        component.enabled = true;
      }
    }
  }
}


Create TimelineEventPlayable:

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

public class TimelineEventPlayable : BasicPlayableBehaviour
{
  public enum EventType
  {
    AlmostAtStart, Start, End
  }

  [SerializeField]
  EventType eventType;

  public override void OnBehaviourPlay(
    Playable playable,
    FrameData info)
  {
    base.OnBehaviourPlay(playable, info);

    EnableComponentsOnTimelineEvent[] componentList
      = GameObject.FindObjectsOfType<EnableComponentsOnTimelineEvent>();

    for(int i = 0; i < componentList.Length; i++)
    {
      EnableComponentsOnTimelineEvent component = componentList[i];
      component.OnEvent(eventType);
    }
  }
}


Configure spawners:

  • For both the cloud and door:
    • Disable the Spawner component.
    • Add EnableComponentsOnTimelineEvent.
      • Event Type: Start
      • Add the Spawner component to its list.


Update Timeline:

  • Open the Timeline Editor and select the EvilCloud's sprite.
  • Drag drop the TimelineEventPlayable script into the timeline.
    • Set the time to start a bit after the Hammers and such activate, and align the ends.
    • In the Inspector, change the 'Event Type' to 'Start'.
  • Drag the script in a second time and set the time to fire a bit before the animation ends.


Test:

  • Enemies should not spawn until after the intro animation completes.


Explain the code

EnableComponentsOnTimelineEvent:

'using' clauses at the top of a file brings APIs into scope. Used for:

  • UnityEngine.Behaviour
  • UnityEngine.MonoBehaviour
  • UnityEngine.SerializeFieldAttribute
using UnityEngine;

We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.

public is optional here. Used for consistency.

public class EnableComponentsOnTimelineEvent : MonoBehaviour
{

This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.

  [SerializeField]

This defines which Timeline event will trigger enabling components. Set in the Inspector.

  TimelineEventPlayable.EventType eventType;

This defines the list of components to enable when the Timeline event occurs. Set in the Inspector.

  [SerializeField]
  Behaviour[] componentListToEnable;

This is a public method which will be called by the TimelineEventPlayable when any of the Timeline events occur. It will pass in the current event type, and this method should ignore all but the event it's interested in.

  public void OnEvent(
    TimelineEventPlayable.EventType currentEventType)
  {

Check if this is the event this component is interested in.

    if(currentEventType == eventType)
    {

Loop over each of the components to enable.

      for(int i = 0; i < componentListToEnable.Length; i++)
      {
        Behaviour component = componentListToEnable[i];

Enable the component.

        component.enabled = true;
      }
    }
  }
}


TimelineEventPlayable:

using clauses at the top of a file brings APIs into scope. Used for:

  • UnityEngine.GameObject
  • UnityEngine.Playables.FrameData
  • UnityEngine.Playables.Playable
  • UnityEngine.SerializeFieldAttribute
  • UnityEngine.Timeline.BasicPlayableBehaviour
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

We inherit from BasicPlayableBehaviour which allows this script to be added to as an event in a Timeline.

public is optional here. Used for consistency.

public class TimelineEventPlayable : BasicPlayableBehaviour
{

This defines an enum of the possible event types this component may fire.

  public enum EventType
  {
    AlmostAtStart, Start, End
  }

This defines which event this instance in the Timeline will trigger. So you can this script to a Timeline and then in the Inspector change the event it will trigger.

  [SerializeField]
  EventType eventType;

OnBehaviourPlay is a Unity event which is called when this script begins in a Timeline.

We are going to ignore the parameters here.

  public override void OnBehaviourPlay(
    Playable playable,
    FrameData info)
  {

As a general best practice when override methods, we call the base first so that this script supplements instead of replaces any logic the basic class may have for this method.

    base.OnBehaviourPlay(playable, info);

Find all EnableComponentsOnTimelineEvent in the scene.

    EnableComponentsOnTimelineEvent[] componentList
      = GameObject.FindObjectsOfType<EnableComponentsOnTimelineEvent>();

Loop over each of the components found.

    for(int i = 0; i < componentList.Length; i++)
    {
      EnableComponentsOnTimelineEvent component = componentList[i];

Call the EnableComponentsOnTimelineEvent's OnEvent method, allowing that component to react to the event if it's the correct type.

      component.OnEvent(eventType);
    }
  }
}
What is a BasicPlayableBehaviour / when is OnBehaviourPlay called?

A BasicPlayableBehaviour is like a MonoBehaviour but for scripts to be used in the Timeline (vs on a GameObject directly).

OnBehaviourPlay is a Unity event called when the script begins on the timeline. Note that here Unity uses override instead of the reflection pattern used with MonoBehaviour events.


What's a C# 'enum'?

An enum is a set of named constants. The constants are by default type int and count sequentially starting from 0. For example:

enum Example 
{
  A, B, C
}

is similiar to

const int A = 0;
const int B = 1;
const int C = 2;

Enums are often used to bring a related set of constants together. They have some additional benefits over listing the constants individually such as:

  • You can iterate all possible values using System.Enum.GetValues.
  • You can use ToString to get the named value.
  • Clarifies intent, making it easier to know what values should be accepted.

Consider using an enum if the set of values is known at compile time.


12.3) Rotate Platforms

YouTube | Source before | Source after

Platforms start out straight and then when the intro animation is nearly complete, shake down into position.

How

Create RotateOvertimeToOriginal:

using System.Collections;
using UnityEngine;

public class RotateOvertimeToOriginal : MonoBehaviour
{
  [SerializeField]
  float rotationFactor = .25f;

  [SerializeField]
  float maxTimeBetweenRotations = .5f;

  Quaternion targetRotation;

  protected void Awake()
  {
    targetRotation = transform.rotation;
    transform.rotation = Quaternion.identity;
  }

  protected void Start()
  {
    StartCoroutine(AnimateRotation());
  }

  IEnumerator AnimateRotation()
  {
    float percentComplete = 0;
    float sleepTimeLastFrame = 0;
    while(true)
    {
      sleepTimeLastFrame 
        = UnityEngine.Random.Range(0, maxTimeBetweenRotations);
      yield return new WaitForSeconds(sleepTimeLastFrame);
      sleepTimeLastFrame = Mathf.Max(Time.deltaTime, sleepTimeLastFrame);

      float percentCompleteThisFrame = sleepTimeLastFrame * rotationFactor;
      percentCompleteThisFrame *= UnityEngine.Random.Range(0, 10);
      percentComplete += percentCompleteThisFrame;
      if(percentComplete >= 1)
      {
        transform.rotation = targetRotation;
        yield break;
      }

      transform.rotation = Quaternion.Lerp(
        Quaternion.identity, 
        targetRotation, 
        percentComplete);
    }
  }
}


Configure Platforms:

  • For each Platform:
    • Add RotateOvertimeToOriginal:
      • Disable the component.
    • Add EnableComponentsOnTimelineEvent:
      • Add RotateOvertimeToOriginal to the 'Components to enable on almost loaded'.


Test:

  • The platforms should shake into place when the intro is nearly complete.
    • Adjust the starting position for the Almost At Start event in the Timeline so the platforms shake at a good point in the animation.
    • Adjust the rotationFactor and maxTimeBetweenRotations to get the effect looking good with your intro animation.


Explain the code

'using' clauses at the top of a file brings APIs into scope. Used for:

  • System.Collections.IEnumerator
  • UnityEngine.Mathf
  • UnityEngine.MonoBehaviour
  • UnityEngine.Quaternion
  • UnityEngine.SerializeFieldAttribute
  • UnityEngine.WaitForSeconds
using System.Collections;
using UnityEngine;

We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.

public is optional here. Used for consistency.

public class RotateOvertimeToOriginal : MonoBehaviour
{

This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.

  [SerializeField]

This defines how much progress is made each iteration of the loop below.

  float rotationFactor = .25f;

This defines the max time to wait between each iteration of the loop below. Use wait for a random number of seconds from 0 to this value.

  [SerializeField]
  float maxTimeBetweenRotations = .5f;

This holds the original rotation for the GameObject, and is the rotation this will lerp towards in the loop below.

  Quaternion targetRotation;

Awake is a Unity method which is called once, the first time the GameObject is added to the scene.

protected is optional here. Used for consistency.

  protected void Awake()
  {

Here we store the original rotation for the GameObject.

    targetRotation = transform.rotation;

Then we set the rotation to Quaternion.identity, which is the same as having 0 rotation.

By doing this on Awake, we ensure that the platform has no rotation when the level starts.

    transform.rotation = Quaternion.identity;
  }

Start is a Unity method which is called once, the first time this component is enabled.

protected is optional here. Used for consistency.

  protected void Start()
  {

Here we start the coroutine below to rotate the GameObject overtime.

    StartCoroutine(AnimateRotation());
  }

This is the coroutine which will rotate the GameObject overtime.

  IEnumerator AnimateRotation()
  {

Here we loop until reaching the yield break statement below, tracking the percentComplete along the way.

    float percentComplete = 0;
    float sleepTimeLastFrame = 0;
    while(true)
    {

This will select a random amount of time to wait for.

      sleepTimeLastFrame 
        = UnityEngine.Random.Range(0, maxTimeBetweenRotations);

Then we pause the coroutine, to be resumed after that time has passed.

      yield return new WaitForSeconds(sleepTimeLastFrame);

Here we calculate the amount of time that has passed, which will be at least 1 frame (represented by Time.deltaTime).

      sleepTimeLastFrame = Mathf.Max(Time.deltaTime, sleepTimeLastFrame);

Here we calculate the percent complete this frame by multiplying the time that has passed by the factor configured in the Inspector.

      float percentCompleteThisFrame = sleepTimeLastFrame * rotationFactor;

This adds some RNG to the value calculated above.

      percentCompleteThisFrame *= UnityEngine.Random.Range(0, 10);

Here we track progress for the effect and check if the effect is ready to complete.

      percentComplete += percentCompleteThisFrame;
      if(percentComplete >= 1)
      {

Set the rotation to the target / original rotation. We do this as opposed to just ending here to ensure that it ends at the exact target location.

        transform.rotation = targetRotation;

yield break in C# is how you can end an enumerator (e.g. a coroutine). The coroutine will not be resumed after this.

        yield break;
      }

This will update the rotation by using lerp. We lerp from Quaternion.identity, which is the first rotation of the GameObject a player will see, to the target rotation (which is the original rotation as layed out in the editor).

      transform.rotation = Quaternion.Lerp(
        Quaternion.identity, 
        targetRotation, 
        percentComplete);
    }
  }
}
What's C# yield break do?

Enumerators are methods which can 'yield return' and then later be resumed from where they left off. Coroutines in Unity are enumerators.

When working with enumerators, 'yield break' will return from the method and indicate that it's complete and cannot be resumed again.


12.4) Screen Shake

YouTube | Source before | Source after

Shake the screen when the platforms fall into place.

How

Create ScreenShake:

using System.Collections;
using UnityEngine;

public class ScreenShake : MonoBehaviour
{
  [SerializeField]
  float timeToShakeFor = 1.5f;

  [SerializeField]
  float maxTimeBetweenShakes = .2f;

  [SerializeField]
  float shakeMagnitude = 1;

  protected void Start()
  {
    StartCoroutine(ShakeCamera());
  }

  IEnumerator ShakeCamera()
  {
    Camera camera = Camera.main;
    Vector3 startingPosition = camera.transform.position;

    float timePassed = 0;
    while(timePassed < timeToShakeFor)
    {
      float percentComplete = timePassed / timeToShakeFor;
      percentComplete *= 2;
      if(percentComplete > 1)
      {
        percentComplete = 2 - percentComplete;
      }
      Vector2 deltaPosition 
        = UnityEngine.Random.insideUnitCircle * shakeMagnitude * percentComplete;
      camera.transform.position = startingPosition + (Vector3)deltaPosition;

      float maxTime = maxTimeBetweenShakes * (1 - percentComplete);
      float sleepTime 
        = UnityEngine.Random.Range(0, maxTime);
      yield return new WaitForSeconds(sleepTime);
      sleepTime = Mathf.Max(Time.deltaTime, sleepTime);
      timePassed += sleepTime;
    }

    camera.transform.position = startingPosition;
  }
}


Configure camera:

  • Select the Main Camera:
    • Add ScreenShake:
      • Disable the component.
    • Add EnableComponentsOnLevelLoad:
      • Event Type: Almost At Start
      • Add its ScreenShake component to the component list.


Test:

  • The camera should move up/down/left/right while the platforms are falling into place, creating the screen shake effect.


Explain the code

'using' clauses at the top of a file brings APIs into scope. Used for:

  • System.Collections.IEnumerator
  • UnityEngine.Camera
  • UnityEngine.Mathf
  • UnityEngine.MonoBehaviour
  • UnityEngine.SerializeFieldAttribute
  • UnityEngine.Vector2
  • UnityEngine.Vector3
  • UnityEngine.WaitForSeconds
using System.Collections;
using UnityEngine;

We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.

public is optional here. Used for consistency.

public class ScreenShake : MonoBehaviour
{

This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.

  [SerializeField]

This defines how long the effect should last for. You can change the default in the Inspector.

  float timeToShakeFor = 1.5f;

This defines the max amount of time between each move of the camera. You can change the default in the Inspector.

  [SerializeField]
  float maxTimeBetweenShakes = .2f;

This defines how much movement the camera will make. You can change the default in the Inspector.

  [SerializeField]
  float shakeMagnitude = 1;

Start is a Unity method which is called once, the first time this component is enabled.

protected in optional here. Used for consistency.

  protected void Start()
  {

This starts the coroutine below to shake the camera over a period of time.

    StartCoroutine(ShakeCamera());
  }

This is the coroutine which will shake the camera over a period of time.

  IEnumerator ShakeCamera()
  {

Here we are grabbing a reference to the main camera, to use throughout this coroutine. Cached here for performance.

    Camera camera = Camera.main;

Before we begin moving the camera, we store the original position.

This position must be a Vector3 and not a Vector2. Camera is the one and only thing in a 2D game that should have a Z which is not 0.

    Vector3 startingPosition = camera.transform.position;

Loop until the desired amount of time for this effect has passed.

    float timePassed = 0;
    while(timePassed < timeToShakeFor)
    {

Here we calculate how close to complete this effect is.

      float percentComplete = timePassed / timeToShakeFor;

Above, percentComplete is a number from 0 to 1. We multiply by 2 to get 0 to 2.

      percentComplete *= 2;

If percentComplete is now in the range 1 to 2, we will modify it.

      if(percentComplete > 1)
      {

Here we take 2 minus the percentComplete (which is known to be 1 to 2). This gives us a value that goes from 1 to 0.

At this point percentComplete will smoothly transition from 0 to 1 and then back to 0.

        percentComplete = 2 - percentComplete;
      }

UnityEngine.Random.insideUnitCircle returns a Vector2 position which represents a random position on the circumference of a circle with radius 1. The value returned will always have a magnitude of 1.

To calculate the deltaPosition, which is how much to move the camera by, we take the random position and multiply it by the magnitude for the effect configured in the Inspector and by the percentComplete calculated above, ranging from 0 to 1 to 0.

This gives us a random delta position which is 0 at the start and end of the effect, and most dramatic half way through.

      Vector2 deltaPosition 
        = UnityEngine.Random.insideUnitCircle * shakeMagnitude * percentComplete;

Here we set the camera's position to its original plus the delta calculated above.

We cast deltaPosition to Vector3 to enable addition here, including the Z value in startingPosition.

      camera.transform.position = startingPosition + (Vector3)deltaPosition;

This calculates a max time to wait for, which will be used to select a random number below.

By multiplying the value configured in the Inspector by 1 - percentComplete, we are causing the maxTime to get smaller overtime. This will cause the effect to accelerate.

      float maxTime = maxTimeBetweenShakes * (1 - percentComplete);

This selects a random time to sleep for, between 0 and the max calculated above.

      float sleepTime 
        = UnityEngine.Random.Range(0, maxTime);

This pauses the coroutine, to be resumed after the sleepTime has passed.

      yield return new WaitForSeconds(sleepTime);

The amount of time actually slept for is at least one frame, represented by Time.deltaTime.

      sleepTime = Mathf.Max(Time.deltaTime, sleepTime);

This tracks how long the effect has been running for so far.

      timePassed += sleepTime;
    }

When the effect completes, this sets the position back to its original to ensure we don't leave the camera misplaced.

    camera.transform.position = startingPosition;
  }
}
What is the script doing with percent complete?

Our goal is to smoothly transition from 0 to 1 and then back to 0. We use this value as a multiple on how much we move the camera that frame - smoothing the start and end of the effect.

We do this by doubling the percent complete and then if greater than 1, use 2 - the value. This gives us the desired 0 -> 1 -> 0 curve.


What does Random.insideUnitCircle do?

Random.insideUnitCircle is a convenience method giving you a random point which falls on a circle with a radius of 1. We take that value and then multiple it by the desired magnitude, effectively giving us a random point on a larger, or smaller, circle; and then position that the camera that far from its original position.


What else could we add to the shake effect?

Here are a few ideas on how you might be able to make this effect even cooler:

  • Randomly change the z Rotation in addition to the position.
  • Randomly change the orthographic size, causing the camera to zoom in and out.
  • The current shake algorithm is uses a random offset from the camera's original position, you may be able to improve the effect by giving consideration to the camera position the previous frame.
  • Add a post processing effect such as blur. Post processing effects refer to scripts you can add to your camera, modifying the display to create an effect such as blur or bloom. Here are some post processing effects, free from Unity, you can use.

To Review

Testing / debugging tips
  • TODO

Up Next

Chapter 13 Level Won Timeline



Questions, issues, or suggestions? Please use the YouTube comments for the best fit section.

Support on Patreon, with Paypal, or by subscribing on Twitch (free with Amazon Prime).

License. Created live at twitch.tv/HardlyDifficult August 2017.