Brendan Keesing   

FOMOGRAPHY     Projects     Blog     About


Ways To Make Cutscenes

April 2, 2021

Cutscenes are one of those things that are important to a lot of games but you’ll rarely (if ever) hear game developers discussing solutions to the issue. Just slapping legions of text on the screen is simple enough, yet anything more may send even a seasoned programmer into a spiral of over-complexity. The best solution really comes down to your requirements.

Here, we’ll discuss the various options often used to solve the task.

Data-Based Scripts

Suppose your cutscene consists of basic dialog, where each character takes it in turn to display some text on the screen and maybe a portrait of the character is shown. This can be solved with a simple data-based script. A conversation between two characters might consist of a plain text file with a format like this:

1
2
3
Johnny;Have you seen the dragon that lives at the mountain top?
Mary;No, but I have heard the stories. Is it really as dangerous as they say?
Johnny;No one has returned from it’s lair, so we can only assume the worst.

In this example, each line from a character is shown on a separate line, with the character’s ID first (Johnny and Mary), with the text to display after a semicolon.

You might want to fit in some more data, such as the path to an audio file to play, or an image for a character portrait, coordinates of where to display the character portrait on screen etc.

You can write your own parser for this quite easily, but if you want something well established and versatile, there are no shortages of options. Some from the top of my head are CSV, JSON, XML, YAML and INI.

The real benefit of this approach is simplicity. There’s no need to make fancy editors or complex rules, it’s as simple as opening up a text editor and punching in some data.

This can be extended to be much more complex. It is not too difficult to transform it into a command list with parameters. Suppose we want to make our cutscene more complex by providing animation. Perhaps we could put in the data like this:

1
2
3
4
5
6
MOVE;Johnny;12;-64
DIALOGUE;Johnny;Have you seen the dragon that lives at the mountain top?
ANIMATION;Mary;shake_head
DIALOGUE;Mary;No, but I have heard the stories. Is it really as dangerous as they say?
ANIMATION;Johnny;shrug
DIALOGUE;Johnny;No one has returned from it’s lair, so we can only assume the worst.

Each line now represents an action. The action will execute, and only when it’s finished will it continue to the next line. You might think this is starting to look like a regular script, yet you’ll find this has significant limitations.

For example, how can we do branching, like an if statement for a while loop? Or assigning variables and math functions? These are not impossible to do. You can make a GOTO command to jump to a specific line, make an ASSIGN command to create variables, and a GREATER_THAN command to compare variables.

Another limitation is that you can’t do multiple things at once. What if we want Johnny and Mary to animate at the same time? Once again, there may be ways around this, but the more we extend it, the more frustrating it is to use.

As you may be thinking, this is getting very tedious and will ultimately leave you thinking “Surely there’s a better way.”

Pros:

  • Trivial to parse
  • No custom editors required
  • Easy to extend with more commands
  • Non-programmer friendly

Cons

  • Branching and variables can get very tedious and hard to visualize the flow
  • Can’t parallelize easily

Node-Based Scripts

You may have seen these cool node-based editors, such as Unreal’s blueprints or Unity’s Bolt. These are traditionally called Flow Graphs, and give a non-programmer-friendly interpretation of program instructions.

Nodes are a simple concept and can be extended to some crazy complex things, such as managing variables, directing execution flow, embedded flow graphs, branching, real-time debugging and more.

Another thing you may have seen are the nightmarish spaghetti graphs.

HSV

Just like text programming, nodes are very susceptible to mess and require a lot of care to keep clean. It’s important to be aware of the types of things that can introduce this mess into your graphs. Flow graphs are usually really keeping complex math clean. The main idea should always be to keep your graphs as small and focused as possible. Experienced programmers may know this intuitively, but most others will not. Be prepared to deal with that.

Pros:

  • Non-programmer friendly
  • Easily supports branching

Cons

  • A lot of work to get an editor up and running
  • Hard to keep clean with more complex graphs (especially when using math)
  • Can’t parallelize

Coroutine Scripts

Why not just use a scripting language for cutscenes? This will give us full control over anything we could ever want! Let’s see what it looks like:

1
2
3
4
5
6
Johnny.MoveTo(12, -64);
Johnny.Talk(Have you seen the dragon that lives at the mountain top?);
Mary.PlayAnimation(shake_head);
Mary.Talk(No, but I have heard the stories. Is it really as dangerous as they say?);
Johnny.PlayAnimation(shrug);
Johnny.Talk(No one as returned from its lair, so we can only assume the worst.);

Looks great, right? If you try to implement it, you will run into trouble because almost all programming languages will execute the next statement immediately, instead of waiting for the last one to finish. In the above example, Talk() will be called immediately after MoveTo(), resulting in everything happening at once.

This is where coroutines come in. Coroutines allow the code to wait until the code completes before moving to the next line. This may look something like this:

1
2
3
4
5
6
yield Johnny.MoveTo(12, -64);
yield Johnny.Talk(Have you seen the dragon that lives at the mountain top?);
Mary.PlayAnimation(shake_head);
yield Mary.Talk(No, but I have heard the stories. Is it really as dangerous as they say?);
Johnny.PlayAnimation(shrug);
yield Johnny.Talk(No one as returned from its lair, so we can only assume the worst.);

In this example, using the yield keyword will wait for the statement to complete before moving on. This also opens opportunities to parallelize the cutscene. In the above example, the animation will start playing but immediately move to the next line, resulting in the animation playing while the character is talking.

So how can we implement this? We could use an external scripting language (such as Lua, Python, Javascript) or simply use whatever language the game is coded in. Some languages, such as C#, support coroutines, but other, such as C and C++ (before C++20), do not.

Pros:

  • Easily extendable
  • Lots of options for scripting languages
  • Can be parallelized

Cons:

  • More difficult for non-programmers to learn
  • Localization needs to use IDs
  • Can be a nightmare to roll your own scripting language

Timeline Editor

Timeline editors are often used in animation and video editing software. They present a series of tracks, where each track can have clips and events placed at specific times. This is a very animation-focused tool, giving complete frame-by-frame control over parallelized actions. On top of that, it also provides instant playback so you can see exactly what it will look like in the game without having to recompile or even play the game.

HSV

As great as timeline editors are for animation, they fall flat in two major areas: interactivity and automation.

Suppose a dialog box appears and presents the player with an option. How will the cutscene wait for the action to complete? If there is a dynamic wait time in the timeline, all of the timeline times will be messed up. We could just pause the timeline, but then we won’t be able to animate anything while we’re waiting for the player to perform their action.

And how will the action know what to do depending on the player’s choice? We will want the cinematic to branch down multiple paths. So should we do something tricky like disabling certain tracks under specific conditions?

You might have a consistent style for displaying dialogue boxes, so you’d like to wrap up all of the functionality for dialogue boxes into a single automated flow. How will this fit in the timeline?

These problems may or may not be solvable, but it’s clear that any solution will be hacky and unintuitive.

Pros:

  • Great for animation
  • Instant playback (WYSIWYG)

Cons:

  • No branching
  • No pausing to wait for interactions
  • Can be complex to create your own editor

Hybrid (Using the right tool for the job)

If you’re like me, you might find that none of these solutions tick all the boxes. Cutscenes are difficult, dynamic and varied, and it’s hard to predict what you’ll need it to. That’s why I would recommend using a hybrid of the above.

For example, in my upcoming game, I use coroutines for the majority of cutscenes, but sometimes I need something more fancy for complex animations. I solved this by having the coroutines able to play timeline clips.

1
2
yield DisplayDialog(fisherman_hello);
yield PlayTimeline(timelineClip);

This means that we can get the best of both worlds. Don’t be scared to mix and match to suit your needs because you’ll often find that forcing a system to do something it wasn’t designed to do will have catastrophic long-term results. Pick the right tool for the job.

Localization

One thing that we’ve ignored in all of this is localization. Localization is the system that replaces the dialogue text depending on which language the player has selected. Ideally, we should be able to make all of our cutscenes in our preferred language and then have an automated process that can generate localization IDs that can then be passed to the translation team. This can be quite trivial with most forms of cutscenes, but you will run into a lot of trouble with scripting languages. What if the text is being passed in as a variable instead of a hard-coded string? This can be damn near impossible to detect, making automation ultimately futile. This just means that localization IDs must be maintained elsewhere.



Twitter YouTube GitHub Email RSS