How to Write a Story

Alex Anderson 🚀Alex Anderson 🚀, October 5, 2023

When Thorium Classic came out, mission timelines were one of the most impactful features. Before Thorium, a mission consisted of a binder, a DVD (or VHS back in the day!), a tactical stack (like a glorified PowerPoint presentation), and maybe some message or sensor presets built into the controls.

The Timeline changed all of that. It eliminated the need for all of those things by integrating them neatly into the simulation. Message sending, lighting and sound effects, the Viewscreen, sensor data, tactical targets... just about everything in the simulation could be automatically adjusted by the timeline at a press of a button from the Flight Director.

Everyone who switched to Thorium has told me how much they love the timeline. It's basically a table-stakes feature for this family of bridge simulators, so of course Thorium Nova will have something like it. But Nova's timeline will have to be significantly different. After all, without a Flight Director, who will advance the timeline?

Mission or timeline?

In Thorium Classic, Missions and Timelines are used interchangeably. In Thorium Nova, they mean distinct things.

Timelines are a sequence of steps that do stuff in a flight. Some timelines can be marked as "Missions" that let them be chosen when a flight is started.

What can a timeline do?

Consider a Flight Director operating a timeline. Typically what happens is the Flight Director recognizes the crew has done a necessary task, and clicks the button to advance the timeline so the mission can continue. To illustrate what kinds of things an automated timeline needs to be able to do, lets look at some examples.

  • When the player enters a certain solar system, they should get a long range message from headquarters (like Earth) warning them of danger.
  • Destroying the last enemy ship in the area should mark an objective as completed on the Captain's computer.
  • Sending the correct remote access code to an enemy ship should deactivate the enemy's shields.
  • A group of pirates is holding cargo ships hostage. Moving within 10,000km of the pirates will cause them to attack the hostages (not the player).
  • 20 minutes after the player sends a message to headquarters, the player gets a message from headquarters reminding them to send an update (but resets when the player sends a message).
  • Refugees from a planet are being brought aboard a fleet. When the number of refugees across all fleet ships reaches a certain number, send a message to the player ship.

Easy for a Flight Director, not so easy for a computer. These are just a few examples, but there are loads of others that you might want to implement in your timeline.

What makes these hard for a computer? The mission writer essentially has to program these cause and effect relationships into the mission before the mission is even run. I don't want to require mission writers to need to learn a programming language, so instead I built out a few components that provide all the power a mission writer might need.

Actions

Let's talk about the "effect" part first. I call these "Actions", both in Thorium Classic and Thorium Nova. Thorium gives you a list of all the possible ways you can change the simulation. You pick one, (such as "Change the alert condition on the ship"), set some parameters (alertLevel: 2), and when the action executes, it does what you told it to.

The action editor in Thorium Nova

This part is almost exactly the same as Thorium Classic, with two major differences:

  • In Thorium Classic, actions run in parallel and delays are built into the actions themselves. In Thorium Nova, actions run sequentially and Thorium: Delay is its own action. This makes it much easier to orchestrate timed actions one after another.
  • There can be more than one player ship in Thorium Nova, so you have to specifically pick which ship you want to perform actions on. Note in the screenshot the "Ship Id" parameter is blank, rendering this action useless.

So how do we fix this? How do we identify a specific ship when there might be several player ships in a flight? How do we identity a specific... anything?

I call the solution "Entity Queries", and it's my favorite part of the new timeline.

What is an Entity?

Pretty much anything in the flight is an entity. Ships are entities. Crew members are entities too. So are stars, planets, ship systems, messages, crew stations. Heck, even the flight itself is an entity!

Entities have a unique id and can each be assigned different components that represent different properties. Components include things like "isShip" or "isPlayerShip" for ships that are controlled by a player. Also, "heat", "size", "position", "isImpulseEngine", "efficiency", etc. Each of these components has different properties that define everything about that entity.

This whole setup is the database of the flight.

Entity Queries

See that sparkle button next to the "Ship Id" input in the image above? That's the "Entity Query" button. It lets you insert any value from any entity in the database into an action parameter. Think of it like a function, where you pass in some search parameters and it returns entity values. Handy!

Take a good look at the screenshot below and see if you can figure out what's going on.

The action editor with an entity query on the ship Id

It's overwhelming, I know, but it's also powerful. The top section, labeled "Entity Query" is how you pick the entity you want. First you choose a component from a list of options - components like "Is Player Ship" which is only present on ships controlled by the player. The "Tags" component lets you put tags on just about any entity, which makes it easy to select them later on. Here, we're checking the "tags" property of the "Tags" component to see if it contains the tag "player1". Clicking the "Add Filter" button underneath lets us add even more components to filter by.

So if we read this query, we're getting all player ships that have the tag "player1". This provides us with a list of those entities (if any).

Next we need to choose what value we want. That's what the "Entity Select" section is for. We can either choose "id" (as we're doing here) which gives us the unique ID of the entity, or we can choose any component's value. For example, we could choose to have it return the "name" property from the "Identity" component (likely something like "USS Voyager") but in this case that wouldn't be useful - we just need the id of the ship to send alert level command to.

Query Matches

Finally, we need to choose which of the matched entities we want the value from. If we only expect a single match, we choose "First Match". If we want the value from every match, we choose "All Matches". And if we only want the value from a random match, we choose "Random Match".

Suppose we choose "All Matches" and our query returns multiple entities - what happens then? Thorium will take the values that were selected and execute the action once for every permutation of all of the values. This makes it easy to run an action across a whole bunch of entities all at once.

For example, lets suppose our action is "Alert Level: Update" and our Entity Query selects all entities with the "pirate" tag - that would set the alert level of every pirate ships with that tag.

Nested Queries

Note also that the sparkle button is present on the entity queries too. This lets us nest entity queries within each other, so we can select entities based on other entities. Take this one for example:

A damage system action that selects the impulse engines on the player's ship

There's a lot going on here, but if you start from the center and work your way out, it makes sense. The inner-most Entity Query is just doing "Match All" on any "Is Player Ship" entities - so all player ships. That's being inserted into the "Ship Id" value of the "Is Ship System" component in the parent query - so we're getting all ship system entities associated with the player ships. And then another filter for the "Is Impulse Engine" component filters it to just Impulse Engine systems.

So this entity query gets the id of the first Impulse Engine system associated with any player ship. Makes sense, since this action is for setting the efficiency of a ship system.

Hopefully that gives a glimpse into how powerful the Entity Query system is. They'll be used in a lot of places in Thorium (we'll see another in just a moment), so get familiar with how they work. In the future, I'll add query presets, which let you save common queries so you don't have to build them from scratch every time. Any other suggestions for how to make this simpler, easier to use, or even more powerful are welcome.


That's Actions! Just like in Thorium Classic, we'll see actions pop up in a couple of places, but they'll all work pretty similar to this - pick an action, set the parameters, optionally use an entity query if the action needs it (most will).

And that's just the action part of timelines. What about the "cause" part of "cause and effect"? I call those "Triggers".

Triggers

In principle, Triggers perform Actions when "something" happens. You just have to define the "something", or "condition" as I call them, and the Actions.

There are (currently, I might add more later) three Trigger conditions:

  • Event Listener
  • Entity Match
  • Distance

Event Listeners

If you've used the Triggers feature in Thorium Classic, this one should be familiar to you. When a discrete action happens - like shields are raised, a message is sent, or a damage team is created - the condition passes and the trigger executes.

The condition editor, showing an "Alert Level: Update" condition

This might look like the same Action we configured earlier, but instead it configures a condition. More specifically, it configures what parameters should make the condition pass. If you leave both "Ship Id" and "Alert Level" blank, it will match whenever any ship changes its alert condition to any level. By setting both "Ship Id" and "Alert Level", we're saying "Only trigger when the player ship sets its alert level to 1".

This is by far the simplest of the conditions to configure - pick an event, add the matching parameters.

Entity Match

Yep, there's a Trigger condition based on entity queries. This condition checks whether an entity query matches a certain number of entities.

The condition editor, showing an entity match condition

By now you should be able to read this Entity Query. "Match any entity with the 'Is Player Ship' component, and the 'alertLevel' property of the 'Is Ship' component is set to 5." (Yep, the alert condition is stored on the "Is Ship" component - you'll have to spend some time learning what components there are and what data they have. Don't worry, they'll all be documented).

At the bottom, it lets you choose how many matches should activate the trigger. In this case, if there are one or more entities that match, the condition passes. You can also choose 0, 1, or a custom number.

This example especially might seem very similar to the Event Listeners - you might be wondering whether you'd need Entity Match conditions. Event Listener conditions pass when discrete events happen; Entity Match conditions pass when the state of the simulation hits a certain point. So Entity Match works well when:

  • You need a trigger to activate in response to simulator-driven changes, such as playing an alarm when a ship system heat is too high.
  • Some player actions don't have a corresponding events - entering or leaving a system doesn't trigger an event, but you can use an Entity Match to see if a player's ship is in a specific solar system.
  • You want to check a condition across many entities at once, instead of a distinct event that only applies to one entity.

At this point, I don't know what condition will be used more - Event Listeners or Entity Match. Both have uses, so it's good they're both options.

Distance

One thing that's difficult to do with Entity Match is to compare the distance between two entities, but it's crucial to certain situations I want to model with the timeline. So Distance is just that - pick two entities (yep, with entity queries), and measure whether the distance between them is greater than or less than some value.

The condition editor, showing a distance condition

As you can see, the two entities are a player ship and the planet named "Mars", and the distance match is less than 50,000 km — so if their distance is less than 50,000 km, the trigger will execute.

Multiple Conditions

There might be situations where you only want conditions to execute in certain circumstances. In other words, you want to put multiple conditions together. Triggers let you do that.

Trigger Action configuration window

This is what the full Trigger configuration looks like. If this looks like a funky action, you'd be right - Triggers are created through an action called "Triggers: Create". For your own convenience, you can label trigger with a name and add tags so you can query them later. (Yep, Triggers are entities too!)

There on the left are the conditions. This Trigger will only execute its actions when all of the conditions pass. So both the Distance and the Entity Match must pass before the trigger executes.

One interesting thing to consider here - a Trigger can only ever have one Event Listener condition, since the simulation can only process one event at a time. There will be ways to have an event happen first and then have another event happen which executes a Trigger, but we won't get into that today.


So, Triggers have conditions, and when all of those conditions pass it executes all of the Actions assigned to that trigger and deactivates the trigger so it can't repeatedly trigger. (Though in the future I'll probably make it so triggers with Event Listener conditions can be set to not automatically deactivate, so they will trigger any time the condition executes).

This is a feature that can be used across all flights, but it's also the primary means for advancing timelines.

Time for the main event - the timeline itself.

Timelines

The timeline editor showing a list of timelines, the selected timeline steps, and the timeline details

This should look familiar to anyone who's ever built a timeline in Thorium Classic. Timelines in Thorium Nova are still built around a sequence of steps. You can assign the timelines a name, description, and category for convenience in browsing timelines, and tags for accessing a timeline during a flight (since timelines are entities too!)

You can also toggle the "Mission" checkbox to make the timeline available when starting a flight. This also lets you add a cover image to let you show off the mission a bit.

Showing the timeline advance settings

When a timeline is created, a "Timeline Initialization" step is created which will run its Actions immediately when the timeline is activated. This is a great place to spawn any necessary entities and set up anything that the timeline needs later.

Timeline Steps

Also, every step includes a "Trigger: Advance Timeline" Action as a convenience. This Action creates a trigger that advances the timeline to the next step, though it doesn't include any conditions by default - you have to add those yourself. The "Timeline: Advance" Action that the trigger includes doesn't have to be configured, and will cause the timeline to activate the next step (and run that step's Actions) when it executes.

So without this trigger and "Timeline: Advance" action, the timeline will end, even if it has more steps. It needs something to make it move to the next step - whether that be manually by a Flight Director (which means the flight can't be fully automated), or another Trigger in some other timeline.

Note that there are two places you can put actions: on the timeline step, and on the trigger. This provide an implicit distinction between "before step activate" actions (in the trigger) and "after step activates" actions (in the step). I don't know if that distinction is helpful at all - as missions are written, we'll be able to see these patterns emerge. For now, I recommend putting them on the step for easier visibility.

Obviously this setup is more complex than Thorium Classic's timeline, but it allows for some interesting implications.

Advanced Timelines

Suppose you've organized your mission into a handful of timelines that join into each other. You're at the end of one timeline, and you want to activate another timeline. And suppose you put in two "Timeline: Activate" actions, each pointing to a different timeline. What happens?

Well... it activates two timelines! Parallel timelines, both running at the same time with their own triggers and actions. Yup. And there's no limit to how many timelines can be active at once. And, of course, you can make it so timelines converge - create a trigger with Entity Match conditions that check to see if all of the dependent timelines have completed before advancing the converged timeline.

These features are part of the reason Missions are still being written as sequential timelines (as opposed to other options that we've discussed). Timelines make it easy for a Flight Director to visualize what is happening during a flight. In this case, they can see a list of active timelines, and see which step each timeline is on. I might even make a triggers visualizer for seeing which triggers are active.

Parallel timelines are a highly advanced feature that I don't anticipate being used in many missions (maybe multi-bridge missions with two or more ships doing different things in different places). But there is one area this will definitely be used: Training.

That's right, there will be a training timeline. And not just any timeline - each station will have its own timeline specifically for that station. It will execute actions to send messages to the station telling the crew member what to do, and triggers to advance the timeline when the crew member accomplishes a task. The whole thing will be streamlined and automated and fully interactive.

This right here is the test case that I'm using for building the timeline system. If I can make multi-station automated training work, then I'm pretty sure I can make any mission scenario work too.


So, that's the whole timeline system. If you've read this far, congratulations! This is by far the longest blog post, but hopefully it's worth it and explains the vision for missions.

As always, feel free to hit me up in Discord or via email with any questions, suggestions, or comments.