Skip to content

Releases: deprimus/Tale

Bloom Update

11 Nov 17:38
dcc2cf2

Choose a tag to compare

Hello!

After 5 years of work, Tale has reached the first major milestone:

Bloom 🎉

Tale was originally created due to my early frustration with the Unity animator. I wanted to create games that focused on the story instead of gameplay. Needing a dialog system too among other things, I decided to create something which allowed me to not only show dialog, but also manipulate objects via code. Something like DOTween (which I didn't know at the time) mixed with other features.

After many years, I realize my frustration was that I didn't take the time to properly learn the animator. Oh well, I'm glad I started the project anyway, because I couldn't have imagined how big it would become when I started.

Tale is now the core of every game that I make, including those that focus on gameplay.

Release Notes

I'll be listing the major features and improvements added throughout the years since the first public version (dubbed 'classic' or 'old').

Automatic Master object

Tale requires a single object to be present in the scene. This is called Tale Master and it's responsible for all the runtime logic, like executing the actions every frame.

Until now, you had to manually add a prefab instance to every scene. However, that's no longer the case.

Tale will now automatically instantiate the object at runtime, so you no longer need to add it by hand.

Editor Tools

When Tale first came out, you had to set it up by hand. This meant you had to create all of the objects, animation transitions, and so on. This took hours.

Now, Tale automatically creates the master object, complete with default animations. All you have to do is to click one button :)

You can even remove or add components as you wish. This feature greatly improved the user experience.

image

There are also tools for quickly adding transitions and splash scenes. Speaking of the latter, there is now support for adding multiple sound variants to a splash scene. Tale will choose and play one of them during the splash, so it's different every time!

image

New Action API

Originally, Tale made use of action constructors to initialize actions. However, this posed several problems:

  • Actions could not be reset, unless the constructor was somehow called again on the same object.
  • Pooling would require parameter packing in order to call constructors with different params. As you know, C#'s generics are very underpowered, and parameter packing isn't really possible without the use of reflection.
  • The action would have to directly use a static reference to the master object. Injecting it in the constructor would've been the proper way to do it, but that would've added extra boilerplate.

Constructors were dropped. Instead, there is a dedicated function which every action defines. For the built-in actions, Init takes this role, and returns the object itself in order to allow chaining. Behind the scenes, the master injects itself into the action when creating or retrieving it from the pool.

When defining custom actions, you can choose to name this function something else. However, you need to call it before placing the action on the queue. Keep in mind that it can be called multiple times during the action's lifecycle. However, once it's called, it won't be called again until Run returns true, or the action was Interrupt-ed. That's when it returns to the pool and can be retrieved again.

Actions can also report if they work with subactions. This allows Tale to do a lot of work behind the scenes instead of the programmer. Action interruption and delta callbacks are automatically propagated.

The new API looks like this:

public class MyAction : Action {
    public MyAction Init(/*...*/) {
        // ...
        return this;
    }

// Required -------------
    protected override bool Run() {
        // ...
    }
    
// Optional -------------
    public override IEnumerable<Action> GetSubactions() {
        // ...
    }
    
    protected override void OnInterrupt() {
        // ...
    }
    
    public override string ToString() {
        // ...
    }
}

Magic Stack Traces

Tale actions execute in the span of multiple frames. If an action throws an exception, you see the runtime stack trace which points to Tale Master. This is because the master is responsible for executing those actions. Due to this, it's very hard to trace where exactly the faulty action was enqueued.

Well, it used to be hard.

Tale will now automatically keep track of where each action was created, catch exceptions, and correlate the runtime stack with the creation stack. This magic stack that points to the creation of the action. Irrelevant frames, such as internal logic, are discarded. This is a big step forward for debugging.

image

You can see the magic stack first, and the real runtime stack second (which is provided by Unity itself).

Realtime Queue Visualization

Until now, the build-in debug info was fairly limited: it only showed the current action on the queue.

However, with the introduction of the new action API, the whole queue can be visualized in real time. This includes all subactions as well.

queue2

It's very pretty.

Scene Selector

This is a huge one. I've always wanted to add a scene selector to my games, but found it annoying to have to list every scene by hand. Not to mention that just listing them in plain text isn't that appealing for the player.

The scene selector was created to solve this issue. It automatically lists the scenes during runtime, so you don't have to do anything. You can even blacklist scenes that you don't want to appear.

A big addition was in the form of thumbnails. With the press of a button, Tale can automatically generate a thumbnail for each scene. If you want a better-looking thumbnail, you can play the specific scene and press F11 at the right time. This will generate a thumbnail on the disk. For most use cases, this is good enough. However, since the thumbnails are written to the disk, you can even modify them by hand!

image

Input Abstraction

I always found it annoying to have to change code depending on if I used the legacy Input Manager or the new Input System. Tale now has an input system abstraction, which is similar to the old Input Manager:

TaleUtil.Input.GetKeyDown(KeyCode.Space);

It works with both input systems; Tale handles the implementation, and you get to use this simple abstraction :)

Action Pool

Actions are the most important part of Tale. Lots of them are created and destroyed. The old version didn't really care to reuse them, but
now there is an action pool that takes care of this. Actions are automatically returned to the pool after they have finished executing. Depending on how you use Tale, the benefits could vary from mild to a lot.

You can also set a limit for the action pool in case you want to reduce memory usage. Sure, even in the worst cases, the memory usage is negligible. Let's assume that an action takes up 128 bytes: storing 10,000 actions in the pool would take up only 1.22MB. But, Tale plays nice and allows you to control even this aspect.

Specialized data structures

IRDeque

Tale was originally created during college, and so the code wasn't great. One of the core components, the queue, used a linked list behind the scenes. It worked fine, but it wasn't really ideal. As you know, linked lists have horrible cache locality unless they are implemented by using contiguous memory under the hood. Another issue was that Tale requires the ability to remove the last X actions from the back in order to do some black magic. However, the last node in the list wasn't cached, and so the removal of such elements was very inefficient.

Now, Tale uses an input-restricted deque, which supports fast removal from both the front and back in O(1), as well as back insertion in amortized O(1).

The real impact isn't huge, due to the fact that our games usually used ~1000 Tale actions in total. However, under stress tests (~20,000 actions), the new IRDeque blows the old queue out of the water, which feels good.

FastUnorderedList

The parallel list was also changed to use a fast unordered list, which supports O(1) arbitrary removal. Again, the real performance gain isn't that large, but it's nice.

Vacuum

If the queue and parallel list grow too much, their capacity will automatically be reduced between scenes so they consume less memory. In real cases, this could maybe save around 1MB in very extreme cases. So yeah, this definitely might be over-engineered, but Tale plays nice and does its part in not hogging memory, even if it's so little. You can configure when you want this operation to happen, and you can even disable it.

Built-in actions

Many built-in actions have been added and modified. For example, say you want to play a video and do something at the 2.5 second time mark. Until now, you had to use this strange 'Detach' API:

Tale.Cinema.VideoPlay("video", 2.5f, Tale.Cinema.VideoDetachType.FIXED);
// ...

It would block the queue execution until the specified time had passed.

This has been changed to mimic the sound action's Sync API:

Tale.Cinema.Video.Play("video");
Tale.C...
Read more