Docs / Getting started

Writing scripts in 0.1 and 0.2

Tallow is released in two series while the ECS engine settles. 0.1 is the standard engine. 0.2 is the same editor on an entity-component-system (ECS) core, and it will become the main engine. Scripts are written in Beef in both, and they look alike, but 0.2 changes how the update callback works and adds new ways to work with data.

0.1 standard 0.2 ECS
Base class Component Script (Component still works)
Per-frame logic Update() with Time.DeltaTime Update(TickInput input, float deltaTime)
Networked logic NetworkScript.OnTick(TickInput, float) The same Update: per tick when the object has a Network Identity
Controls Input.GetKeyDown(.Space) input.Pressed(.Jump)
Local-only effects and UI Update, OnGUI OnLocalUpdate, LateUpdate, OnGUI
Bulk data Components and lists [Component] struct data, Query<...>, plain entities
API reference API → 0.1 API → 0.2

0.1: components with Update

A component is a class with public fields and callbacks. Update runs once a frame and reads the frame time and the keyboard directly.

using Tallow;
namespace MyGame;

[Component]
class Rotator : Component
{
    public Vector3 degreesPerSecond = .(0, 90, 0);
    public bool spinning = true;

    public override void Update()
    {
        if (Input.GetKeyDown(.Space))
            spinning = !spinning;
        if (spinning)
            Transform.Rotate(degreesPerSecond * Time.DeltaTime);
    }

    public override void OnGUI()
    {
        GUI.Label(20, 20, scope $"Spinning: {spinning}");
    }
}

The callbacks are Awake, OnEnable, Start, FixedUpdate (60 Hz), Update, LateUpdate, OnGUI, OnDisable and OnDestroy.

Networked players are written separately, as a NetworkScript whose OnTick runs once per network tick on the server and, predicted, on the player’s machine:

[Component]
class Hover : NetworkScript
{
    public float speed = 5;
    [Sync] public int32 score;

    public override void OnTick(TickInput input, float deltaTime)
    {
        Transform.Position += input.MoveDirection * speed * deltaTime;
        if (IsServer && input.Pressed(.Use))
            score++;
    }
}

0.2: one Update for everything

In 0.2 every script has a single Update(TickInput input, float deltaTime), and the engine decides when it runs. On an object with a Network Identity it runs once per network tick, predicted and replayed. On any other object it runs once a frame with this machine’s controls. The same script works in single player and multiplayer with no changes.

using Tallow;
namespace MyGame;

[Component]
class Rotator : Script
{
    public Vector3 degreesPerSecond = .(0, 90, 0);
    [Sync] public bool spinning = true;

    public override void Update(TickInput input, float deltaTime)
    {
        if (input.Pressed(.Jump))
            spinning = !spinning;
        if (spinning)
            Transform.Rotate(degreesPerSecond * deltaTime);
    }

    // IsMine: this machine controls the object.
    public override void OnGUI()
    {
        if (IsMine)
            GUI.LabelFormat(20, 20, "Spinning: {}", spinning);
    }
}

Three rules make this work:

  • Take controls from input, not from Input, and time from deltaTime, not from Time.DeltaTime. The Console underlines code that breaks this and links to the line.
  • Put work that is only for this machine (effects, sounds, cameras, UI) in OnLocalUpdate(), LateUpdate() or OnGUI(). These are never networked or replayed.
  • Update can run more than once for the same tick after a network correction, so do one-off things inside if (IsServer) or if (!IsReplaying).

NetworkScript still exists in 0.2: it adds the Network Identity for you, along with mouse-look settings and ReadInput. Its code goes in Update, the same as any other script.

0.2: data components

In 0.2 a GameObject is an ECS entity, and you can attach plain data to it: a struct marked [Component]. It shows in the Inspector, saves with the scene, and its [Sync] fields are networked when the object has a Network Identity.

[Component]
struct SpeedData
{
    [Sync] public float speed;
}

[Component]
class Accelerate : Script
{
    public override void Update(TickInput input, float deltaTime)
    {
        let speed = GetComponent<SpeedData>();
        if (speed != null)
            speed.speed += deltaTime;
    }
}

Keep values and engine references in data components, not objects the struct would own.

0.2: queries for many objects at once

When the profiler shows many objects doing the same work, a query walks their data in packed arrays instead of calling one script per object. Queries can be spread over the job threads.

[Component]
struct Boid
{
    public Vector3 position;
    public Vector3 velocity;
}

[Component]
class Swarm : Script
{
    public int32 count = 100000;
    List<Entity> mEntities = new .() ~ delete _;
    Query<Boid> mQuery ~ delete _;

    public override void Start()
    {
        Scene.World.CreateMany<Boid>(count, mEntities);   // plain entities: no GameObject each
        mQuery = new .(Scene.World);
    }

    public override void Update(TickInput input, float deltaTime)
    {
        // Spread over the job threads; each chunk is a packed array.
        mQuery.ParallelFor(scope (chunk) =>
        {
            let boids = chunk.Write<Boid>();
            for (int i < chunk.Count)
                boids[i].position += boids[i].velocity * deltaTime;
        });
    }
}
  • Scene.World.ObjectsWith<T>() is a ready-made cached query over scene GameObjects that have the data T. Don’t delete it.
  • A query you create belongs to one world: keep it while the world lives and delete it before the world goes.
  • Inside ParallelFor, only change the row you are given. Don’t create or destroy entities, add or remove components, or touch shared objects.
  • Table order can change, so don’t rely on it for gameplay order, spawning or random numbers.

See Advanced systems and ordinary scripts for ordering, World.AddStep and measuring.

Visual scripting

Graphs work in both series and compile to a Beef component. In 0.2 the graph search has an ECS category: Has, Add and Remove for data components, Get and Set for their fields, and a For Each loop over every object with a given data type.

Moving a 0.1 project to 0.2

  1. Change : Component to : Script on your scripts. This is optional, but it makes the intent clear.
  2. Replace public override void Update() with public override void Update(TickInput input, float deltaTime).
  3. Use deltaTime instead of Time.DeltaTime, and input (input.Pressed(.Jump), input.Move) instead of Input. Code that has to read the keyboard directly, such as menus and debug keys, moves to OnLocalUpdate().
  4. Rename NetworkScript.OnTick to Update.
  5. Build, then follow the Console’s determinism warnings: each one links to the line to change.

Scenes, prefabs, materials and assets open as they are.

Which should I use?

New projects can start on 0.2. That is where engine work is going, and its scripts run unchanged whether or not the game is networked. Choose 0.1 for an existing project you don’t want to change yet. 0.1 keeps getting fixes and the editor improvements that don’t change its API.