Home Unity Tutorials

How to Tell a Joke With Unity Timeline

Writing jokes is easy. Telling jokes is hard. In this tutorial, you’ll learn about Unity’s Timeline and how to create and deliver your very own punchlines!

5/5 4 Ratings

Version

  • C# 7.3, Unity 2020.1, Unity

Games are full of dialog, and how that dialog is delivered is often more important than the writing. After all, writing jokes is easy, telling jokes is hard. That’s why stand-up comedians spend night after night trying the same material in different ways to get the biggest laugh.

In this tutorial, you’ll learn how to use Unity’s Timeline to create and deliver your very own punchlines with perfect comedic timing. Timeline will take your dialog system to the next level, helping you retain user attention and get even bigger laughs.

In this tutorial, you’ll learn:

  1. What Timeline is.
  2. How to trigger custom events with Timeline.
  3. How to implement a custom dialog system from scratch.

Comedian telling a joke

Note: This tutorial assumes you know the basics of development in Unity. If you’re new to the subject, check out our Unity for Beginners series first. Also, read our Intro to Unity Timeline.

For this tutorial, you’ll need Unity 2020.1.6f1 or later.

Getting Started

Download the project by clicking the Download Materials button at the top or bottom of the tutorial.

Open the starter project, which was created using the 2D template, and look at the folder structure in the Project window.

Folder setup in the Project window

In Assets ▸ RW, you’ll find everything you need for the project:

  1. Scenes: Contains the JokeMachine scene.
  2. Scripts: An empty folder for the scripts you’ll create.
  3. Sprites: Contains sample art for you to use.
  4. Timeline: Anmpty folder for the Timeline Asset you’ll create.

Now, you’re ready to start cracking… jokes! :]

Setting the Scene

For your first step, you’ll set up a simple scene containing a comedian telling a joke. Later, you’ll use Timeline to make the joke appear with perfect comedic timing.

Adding the Text Bubble

Open the JokeMachine scene inside the Scenes folder. Inside, you’ll find a very basic scene with a camera.

Start by creating a bubble for the dialog. Add a UI ▸ Image GameObject and call it JokeWindow. This automatically adds some other components, including a Canvas and an EventSystem.

For the Source Image, select ChatBubble from the Sprites folder. Set the Color to something funny, like purple. Next, set the Width to 1000 and the Height to 600. Finally, set Pos X to 90 and Pos Y to 160. This should make the bubble fill the top right of the screen.

Add a UI ▸ Text – TextMeshPro GameObject as a child of the JokeWindow and call it JokeText.

You might see a pop-up to install the TextMeshPro package; if so, do it. Open Package Manager to confirm that you’ve installed TextMeshPro.

Package Manager showing that you have TextMeshPro instealled

Next, set the JokeText‘s Width to 600 and the Height to 200, with the default centered anchor. In the Scene View, the outline of the box should fit nicely within the bubble.

Set Font Size in the TextMeshPro component to 50, then center the text vertically and make sure the Vertex Color is different from the bubble color you selected. The default white color is fine.

In the text field, insert the following joke across three lines:

  • There’s 10 types of people in the world.
  • Those who know binary,
  • and those who don’t.

Funny, right?

Adding the Comedian

Now that you’ve set up your text bubble, you need someone to tell the joke.

Add another UI ▸ Image GameObject as a child of Canvas for the main character. Call it Comedian.

A suitable character is waiting for you in the downloaded materials. Make it appear by going to the Source Image and selecting Comedian from the Sprites folder.

To position the Comedian, set the Width to 400 and the Height to 400. Then, adjust the RectTransform to place the character to the lower left of the bubble by setting Pos X to -390 and Pos Y to -160.

In the Canvas GameObject, change the default Canvas Scalar component to Scale with Screen Size and the Reference Resolution to 1280 x 720. Finally, set Screen Match Mode to Expand. This will ensure that your friendly comedian makes full use of the stage — regardless of screen size or aspect ratio — without being clipped.

Canvas Scalar with the values listed above entered in it

The UI is now complete. Click Play and you’ll see a joke onscreen… but you’re probably not laughing. Seeing the text all at once is a great way to ruin the punchline. Instead, it would be better for the text to appear gradually.

Comedian with a purple text bubble saying the joke all at once

Revealing Text Over Time

Ever since the invention of the typewriter, words have appeared one letter at a time. To do that in Unity, you need a script to modify the TextMeshPro component.

Inside Scripts, create a new C# script named Typewriter. Attach that script to the JokeText GameObject.

Now, you’re ready to set your bubble text to gradually reveal itself.

To start, open the Typewriter script in your favorite code editor and add this declaration to the script, above the class declaration:

using TMPro;    //for TextMeshPro

This will let you access TextMeshPro features.

Next, just inside the class declaration, add these variables:

//1
private TMP_Text textBox;
//2
private float timePerCharacter = 0.05f;

Here’s what this code does:

  1. Creates a private reference to TMP_Text that you can use throughout the script.
  2. Declares the variable, timePerCharacter, and set it to 0.05f.

Next, you’ll use Awake to set up the TMP_Text component:

void Awake()
{
    //1
    textBox = GetComponent<TMP_Text>();
    //2
    textBox.enableWordWrapping = true;
    //3
    textBox.maxVisibleCharacters = 0;
}

In this code, you:

  1. Cache the reference to the attached GameObject.
  2. Configure enableWordWrapping to true to have multiple lines in the text box.
  3. Initialize the variable maxVisibleCharacters to 0 so the bubble looks empty to start.

To reveal one character at a time, add the following code:

public IEnumerator Reveal(int startLetterIndex)
{
    //1
    textBox.ForceMeshUpdate();

    //2
    int totalVisibleCharacters = textBox.textInfo.characterCount;

    //3
    for (int i = startLetterIndex; i < totalVisibleCharacters; i++)
    {
        textBox.maxVisibleCharacters = i + 1;

        yield return new WaitForSeconds(timePerCharacter);
    }

    //4
    textBox.maxVisibleCharacters = totalVisibleCharacters;
    yield break;
}

And here's what this does:

  1. Forces an update of the mesh to get up-to-date information about the text component.
  2. Gets the number of visible characters in the text object.
  3. Reveals one letter at a time with a pause after each letter.
  4. For insurance, shows all the possible characters at the end of the coroutine.

Now, you need something that will start the Typewriter. So, for testing purposes, add the following code:

private void OnEnable()
{
    StartCoroutine(Reveal(0));
}

OnEnable is called when the GameObject becomes enabled in the scene. So, when the text box appears on screen, it will start the coroutine Reveal.

Save the script and return to the editor. Click Play and watch the joke reveal one character at a time.

But... well, it's still not very funny. Before implementing an even better way to deliver the joke, take a moment to consider why jokes are funny.

The text revealing one character at a time, in linear time.

Understanding Comic Timing

The world is full of jokes in many forms. But ask yourself this: Do you laugh more reading a joke, or when you hear a professional tell it?

Try this at home with your favorite comedian on a streaming service. Watch with subtitles on and the sound off, and then watch with the sound on and the subtitles off. Which is funnier?

Spoken jokes are more popular than written jokes because timing is critical to getting a laugh. Rush it and you ruin it. Wait too long, and the audience fills in the gap. Even cartoon strips use the three-box format to control the delivery of the joke.

Those three parts are: the Setup, the Turn, and the Punchline. The length of each is important, the pace of each is important and the pause between each is important.

Professional stand-up comedians rehearse those timings over and over until they get it just right. But how do you control all that timing in Unity? The answer is: Timeline!

Understanding Timeline

Unity's Timeline infrastructure is powerful and flexible. It allows you to create cutscenes, cinematics and gameplay sequences by visually arranging tracks and clips linked to GameObjects in your scene.

In this tutorial, you'll use it to make a dialog system. The infrastructure is made of several parts that all need to work in harmony:

  1. Timeline Asset: An arrangement of tracks you manage in the Timeline Window.
  2. Timeline Track: A sequence of animation clips of a similar type.
  3. Playable Director GameObject: Associates a Timeline Asset with a GameObject in the scene.
  4. Signal Emitter: Contains a reference to a Signal Asset, which it visually represents visually with a marker on the Timeline.
  5. Signal Receiver: A component with a list of reactions linked to a Signal Asset.
  6. Signal Asset: The association between a Signal Emitter and a Signal Receiver.

This puzzle has a lot of pieces, but don't worry, they all fit together quite easily.

Prepping Assets

Create an empty GameObject called Timeline. The Hierarchy should now look like this:

Final Scene Hierarchy

Navigate to Window ▸ Sequencing ▸ Timeline to add an empty Timeline Window to the Editor workspace. Dock this tab to the bottom so you can easily see it and the Game view at the same time. The editor layout should now look like this:

Hierarchy with the Game view in a large panel and the Timeline window below

With the Timeline GameObject selected in the Hierarchy, click the Create button in the Timeline Window. This starts the process of creating and setting up the Timeline Asset.

When prompted, save a new Timeline Asset to the Timeline folder and name it JokeTimeline. This step automatically adds a Playable Director component to the Timeline GameObject and assigns the newly-created JokeTimeline asset as the Playable.

Finally, toggle Play On Awake to True in the Inspector.

Now that all the pieces are in place, it's time to connect them.

Activating GameObjects

Timeline can control many things, but one of its simplest uses is to enable and disable GameObjects.

First, select the Timeline GameObject in the Hierarchy and make sure the Timeline Window is visible. Then, drag the Comedian GameObject to the left panel of the Timeline Window.

At this point, you'll see a prompt for the type of track you want. Choose Add Activation Track. This populates a clip in the Timeline and associates the Comedian GameObject with that clip.

When the Timeline Marker is over the clip, the Comedian is active; when the marker is not over the clip, the Comedian is inactive.

You can change the duration of the clip and move it anywhere you like on the Timeline. You can view the Timeline either in seconds or frames.

Click the Gear icon in the Timeline Window to set your preference to Seconds. For the Comedian clip, start the clip at 0:00 and stretch it to 20:00.

Do this again for the JokeWindow GameObject. Drag it to the Timeline Window to create an Activation Clip. Start the clip at 2:00 and stretch it to 18:00. This makes the JokeWindow appear after the Comedian and disappear before the Comedian.

In the Game View, make sure you haven't selected Maximize On Play so you can see both the Game View and the Timeline Window simultaneously.

Click Play and watch the sequence play both in the Game View and the Timeline Window. In Edit Mode, scroll the mouse wheel while over the Timeline Window to see more or less of the overall Timeline. Then, scrub the Timeline Marker back and forth to watch the clips activate and deactivate their associated GameObjects.

GameObjects appearing and disappearing as you scrub the Timeline

Sending Signals

Timeline tells GameObjects what to do using Signals. Signals consist of three parts: the Signal Emitter, the Signal Receiver, and the Signal Asset. The Signal Emitters are placed on the Timeline and Signal Receivers are placed on GameObjects in the scene that you want to control. During playback, whenever Timeline passes a Signal Emitter, it sends the Signal Asset to the Signal Receiver.

Before writing code to send or receive a signal, it's best to define the elements that should be in the signal. There are many things you can include, but for this app, you'll pass four pieces of data with the signal.

To help manage that data, return to the Typewriter script and, at the very bottom, add a simple class with the following code:

public class Dialog
{
    //1
    public string Quote;
    //2
    public float PausePerLetter;
    //3
    public bool NewPage;
    //4
    public bool NewLine;
}

With this code, you're declaring four variables:

  1. Quote: The text that you'll send — in this case, the joke itself.
  2. PausePerLetter: Controls the speed of the text reveal.
  3. NewPage: Tells the Typewriter whether or not to start a new page.
  4. NewLine: Instructs the Typewriter whether or not to start a new line.

Now that you've created a form for the signal, create a new script called JokeMarker to send it. Add these declarations to the script, above the class declaration:

using UnityEngine.Playables;
using UnityEngine.Timeline;

These are necessary to access the Timeline API.

Instead of the usual Monobehaviour, this script needs to derive from Marker and implement INotification. So replace the default class definition with:

public class JokeMarker : Marker, INotification

You need to send four pieces of information in the signal, which can be private and visible in the Inspector via the SerializeField attribute:

    //1
    [SerializeField]
    private string quote = "";
    //2
    [SerializeField]
    private float pausePerLetter = 0.1f;
    //3
    [SerializeField]
    private bool startNewPage;
    //4
    [SerializeField]
    private bool startNewLine;

Here, you declare:

  1. quote and set it to be empty.
  2. pausePerLetter and set it to 0.1f.
  3. startNewPage, which defaults to false.
  4. startNewLine, which also defaults to false.

Next, add the following code, which uses the variables you just declared:

    public string Quote => quote;

    public float PausePerLetter => pausePerLetter;

    public bool StartNewPage => startNewPage;

    public bool StartNewLine => startNewLine;

This exposes read-only access for your four private variables. Using [SerializeField], private and read-only properties together is a good safety pattern. It ensures that other parts of your code can't randomly change the values you set in the Inspector.

Finally, implement the INotification interface by adding the following line:

    public PropertyName id => new PropertyName();

This is a necessary identifier for notifications. As your use of notifications grows, you can use this id to identify notification channels or even individual notifications.

Save the script and return to the editor.

Now that you've created the JokeMarker, you need to create a script that listens for it. You'll do that next.

Receiving Signals

Create JokeReceiver and add the Playables declaration to the script, above the class declaration:

using UnityEngine.Playables;

In addition to being a MonoBehaviour, this class also needs to implement INotificationReceiver, like so:

public class JokeReceiver : MonoBehaviour, INotificationReceiver

Now, create a reference to the Typewriter in the scene:

[SerializeField]
private Typewriter dialogAnimator;

Since the JokeReceiver passes the content of the message to the Typewriter, you need to create a stub function in the Typewriter script to process that message. To do this, switch to the Typewriter script and add:

public void AddDialog(Dialog message)
{

}

You'll populate this function later in the tutorial, but it's helpful to have the stub in place before finishing the JokeReceiver script.

Return to the JokeReceiver script and implement the INotificationReceiver interface with the following code:

//1
public void OnNotify(Playable origin, INotification notification, object context)
{
    //2
    if (notification is JokeMarker dialog && dialogAnimator != null)
    {
        //3
        var newdialog = new Dialog
        {
            //4
            Quote = dialog.Quote,
            PausePerLetter = dialog.PausePerLetter,
            NewPage = dialog.StartNewPage,
            NewLine = dialog.StartNewLine
        };

        //5
        dialogAnimator.AddDialog(newdialog);
    }
}

Here, you:

  1. Implement OnNotify with the necessary parameters.
  2. Check to make sure the notification is the correct type and not null.
  3. Create a temporary variable newdialog to store the content of the notification.
  4. Assign the corresponding elements of the notification to the newdialog.
  5. Forward newdialog to the stub function in the Typewriter.

Save the script and return to the editor. Attach the JokeReceiver script to the JokeText GameObject, then assign the Typewriter component on the same GameObject to the Dialog Animator slot in the Inspector for the Joke Receiver component.

As the message moves from the JokeMarker to the Typewriter, the final step is for the Typewriter to do something with the message.

Reacting to Signals

Return to the Typewriter script and fill out the AddDialog stub function:

public void AddDialog(Dialog message)
{
    //1
    timePerCharacter = message.PausePerLetter;

    //2
    //clear and start new
    if (message.NewPage)
    {
        textBox.maxVisibleCharacters = 0;
        textBox.text = message.Quote;
        StartCoroutine(Reveal(0));
    }

    //3
    //append to existing
    else
    {
        //4
        int currentLetterIndex = textBox.maxVisibleCharacters;

        //5
        if (message.NewLine)
        {
            textBox.text = string.Concat(textBox.text, "\n", message.Quote);
        }
        else
        {
            textBox.text = string.Concat(textBox.text, " ", message.Quote);
        }

        //6
        StartCoroutine(Reveal(currentLetterIndex));
    }
}

With this code, you:

  1. Update timePerCharacter with the value in the message.
  2. Start a new page, if the message requests it.
  3. If not, add the content of the message to the existing page.
  4. Capture the current index of what's already been revealed.
  5. Start a new line, if the message requests it. If not, add the content of the message to the existing line.
  6. Begin revealing letters, starting at the index provided.

Save the script and return to the editor.

With the Timeline infrastructure in place, you no longer need the test OnEnable in the Typewriter script. Delete it or comment it out so JokeReceiver is the only thing that calls Reveal() on the Typewriter script.

Now that all the infrastructure is in place, it's time to tell a joke.

Creating Custom Tracks

Ha ha! The joke's on you. There's one more piece to the puzzle. To keep Timeline neat and tidy, your jokes deserve their own Track. So next, create one more script and name it JokeTrack. It's not much to look at, but it packs quite a punch(line). :]

Add the Timeline declaration to the script, above the class declaration:

using UnityEngine.Timeline;

Instead of the usual Monobehaviour, this script needs to derive from MarkerTrack. So replace the default class definition with:

public class JokeTrack : MarkerTrack

Finally, add two Attributes:

//1
[TrackBindingType(typeof(JokeReceiver))]
//2
[TrackColor(255f/255f, 140f/255f, 0f/255f)]
  1. Configure the track to work with JokeReceiver.
  2. Customize the track UI to any color you want.

Save the script and return to the editor. Now, you're ready to be funny in style. Seriously.

Telling a Joke

Select Timeline in the Hierarchy and make sure the Timeline Window is open so you can see the two Activation Tracks you added previously. Now, drag the JokeText GameObject to the left panel, then select Joke Track to add it to the Timeline.

Next, at the 3:00 second mark, right-click on the JokeTrack and select Add Joke Marker. In the Inspector, populate the content of the message as follows:

  • Quote There's 10 types of people in the world.
  • Pause Per Letter 0.05
  • Start New Page checked
  • Start New Line unchecked

Add a second JokeMarker at the 7:00 mark with the following content:

  • Quote Those who know binary,
  • Pause Per Letter 0.1
  • Start New Page unchecked
  • Start New Line checked

Add a third JokeMarker at the 11:00 mark with the following content:

  • Quote and those who don't!
  • Pause Per Letter 0.01
  • Start New Page unchecked
  • Start New Line checked

Finally, add a fourth JokeMarker at the 14:00 mark with the following content:

  • Quote 01 01 01 01 01 01 01 01
  • Pause Per Letter 0.03
  • Start New Page checked
  • Start New Line unchecked

After you've added all four markers, your Timeline will look like this:

Timeline window with jokes and markers added

Save the scene and the project.

Click Play and enjoy the show!

Completed Joke

Every comic genius has their own style and pace. Now, you have all the tools to create your own perfect delivery for maximum laughs. Try moving the markers around on the Timeline. Try different pause lengths for each part of the joke. Try different jokes!

Whatever you decide, your comedy career is ready to launch. :]

Where to Go From Here?

Great job on completing this tutorial! You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Feel free to tweak the project to your needs and turn it into something that gets you laughed at. :]

Besides the obvious use of creating different jokes or an entire set of them, you can use the infrastructure you just built for the JokeMachine for other types of machines, like:

  • Cutscene narratives.
  • A multi-track system for dialog between characters.
  • Adding Animation tracks to the Timeline to make the Comedian bounce up and down when it laughs.
  • Moving the Comedian's mouth in time with the Typewriter.
  • Even creating your own custom Marker/Receiver/Track system to design and easily tune obstacle patterns in an endless runner.

Timeline is flexible and powerful.

The Unity Timeline will continue to evolve, so follow the latest updates in the Unity Timeline official documentation.

For more examples of things you can do with Timeline, check out the Unity Blog.

I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments