Scenes

A scene is a group of entities that load and unload together: a menu, a level, an arena. Several scenes can be loaded at once, and several copies of the same one.

tide
scene Arena
{
    int size = 20;
}

A loaded scene is an entity like any other. The declaration is its component, and its fields are the scene's state, so a system that takes Arena runs once per loaded arena.

Authored prefabs

An authored .tprefab file describes a reusable rooted entity tree. The same tree can be spawned as an entity or loaded as a scene:

tide
// Enemy.tprefab
prefab Enemy
{
    Transform;
    Health { value = 100 };
    Target { weapon = Weapon };
    entity Weapon
    {
        Transform { localPosition = float3(0, 1, 0) };
    }
}

Declare Health and Target in ordinary .tide code. A reference field can use [SceneRequired] Entity weapon; to require a named entity in authored prefabs. The compiler checks unused prefabs too; missing components, fields, references and recursive inclusion are errors. Ordinary code-driven construction can still leave these fields unset and initialize them later.

tide
var enemy = Spawn(Enemy, Health { value = 50 });
var level = Scene.Load(Level, LevelSettings { difficulty = 2 });
Scene.Unload(level);

Settings are normal root components. Component arguments override only the fields they name and can add root components. A bare component argument resets that component to its declared defaults. Match scene visibility comes last by name: Scene.Load(Level, visibility: SceneVisibility.Private).

All entities, component values, references and parenting exist before the instance's Spawned handlers run, root-first. Nested children move with their parents and are destroyed with them. entity Weapon : Pistol { ... } reuses another named prefab; fields in its block override that prefab's root. Paths such as Weapon.Muzzle resolve through nested instances, and omitted values use the authored prefab and component defaults.

local Spawn(Enemy) creates local entities, and Scene.Load from local code loads into the local world. References must use the corresponding Entity or LocalEntity type. Prefab names are factories, so filters use explicit root components. Keep scene Main in .tide for startup. The initial limits are 1024 entities per instance and 128 nesting levels. Use tide open Level.tprefab for the native scene editor.

Main

The game starts in the scene called Main, and there's exactly one. It sets up the first scene, not the game loop: the engine runs the tick.

tide
scene Main { }

event(Spawned) Setup(with Main)
{
    Spawn(Transform { localScale = float3(1, 1, 1) }, Player);
}

A match Main, like this one, starts a single-player match with it right away. A local Main starts the program outside any match, usually in a menu, which starts one later (see Local state and Matches).

Loading and unloading

tide
var arena = Scene.Load(Arena { size = 30 });
Scene.Unload(arena);

Scene.Load loads a scene into the current world and returns its entity. Scene.Unload unloads it, destroying every entity it owns, and so does Destroy on a scene's entity. Scene.Unload of an entity that isn't a scene does nothing. Spawn of a scene is an error that says to use Scene.Load.

Loading and unloading happen at the end of the tick, like Spawn and Destroy. A scene is set up by Spawned handlers, and its entities get Destroyed when it unloads, both within that tick.

Main is the fallback. When the last scene of its world unloads, Main loads again at the end of that tick, set up by its Spawned handlers as at the start: a match Main in the match, a local one in the local state. Unloading one scene and loading another in the same tick never brings it back, and entities in no scene don't count. Main comes back at most once a tick: one that unloads itself as it loads leaves the world without a scene until the next.

A match can't load a local Main, so when Main is local, a match whose last scene unloads ends there. Every machine in it goes back offline, to its Main, and gets Disconnected with the reason Ended (see Matches). Local state carries on as it was.

tide
// Sets up each arena. The floor joins that arena.
event(Spawned) SetupArena(Arena arena)
{
    Spawn(Floor { size = arena.size });
}

system Collapse(Arena arena)
{
    if (arena.size <= 0) Scene.Unload(this);
}

Each button below loads a Room, or unloads the one that's there. A Spawned handler furnishes a room with four balls as it loads. They're spawned for the room, so they join it, and unloading it takes them with it.

Rooms that load and unloadLive Tide example, tap it to play
Open separately
tide
settings { title = "Scenes"; tickRate = 30; maxPlayers = 1; }

scene Main { }

// A scene's fields are its state. Each loaded Room is an entity with a Room.
scene Room
{
    int slot;
}

component Ball
{
    float2 position;
    float2 velocity;
    int slot;
}

singleton Slots { List<Entity> rooms = [default, default, default]; }
command Toggle { int slot; }

float2 Center(int slot)
{
    return float2((slot - 1) * 1.3, 0);
}

command(Toggle toggle, PlayerID sender) LoadOrUnload(mut Slots slots)
{
    if (toggle.slot < 0 || toggle.slot > 2) return;
    if (slots.rooms[toggle.slot] == default)
        slots.rooms[toggle.slot] = Scene.Load(Room { slot = toggle.slot });
    else
    {
        // Unloading destroys the room and every entity spawned into it.
        Scene.Unload(slots.rooms[toggle.slot]);
        slots.rooms[toggle.slot] = default;
    }
}

// Sets each room up as it loads. These spawns join that room.
event(Spawned) Furnish(Room room)
{
    for (var i = 0; i < 4; i++)
    {
        Spawn(Ball { position = Center(room.slot), slot = room.slot,
                     velocity = float2(Math.Cos(i * 1.9 + room.slot), Math.Sin(i * 1.9 + room.slot)) * 0.9 });
    }
}

system Bounce(mut Ball ball, Time time)
{
    ball.position += ball.velocity * time.dt;
    var offset = ball.position - Center(ball.slot);
    if (Math.Abs(offset.x) > 0.4) ball.velocity.x = -Math.Sign(offset.x) * Math.Abs(ball.velocity.x);
    if (Math.Abs(offset.y) > 1) ball.velocity.y = -Math.Sign(offset.y) * Math.Abs(ball.velocity.y);
}

local singleton Count { int balls; }

view Buttons(Slots slots, mut Count count)
{
    Draw.Clear(Color(0.086, 0.086, 0.094));
    Draw.SetCamera(float2(0, 0.2), 2.2);
    count.balls = 0;
    GUI.SetTheme(GUITheme { radius = 6 });
    for (var slot = 0; slot < 3; slot++)
    {
        var loaded = slots.rooms[slot] != default;
        Draw.WireRoundRect(Center(slot), float2(1.1, 2.3), 0.1, 0.03, loaded ? Color(0.98, 0.75, 0.14) : Color(0.24, 0.25, 0.27));
        var corner = Draw.WorldToScreen(Center(slot) + float2(-0.55, -1.3));
        var width = Draw.WorldToScreen(Center(slot) + float2(0.55, 0)).x - corner.x;
        if (GUI.Button(Rect(corner.x, corner.y, width, 32), loaded ? "Unload" : "Load")) Send(Toggle { slot = slot });
    }
}

view Balls(Ball ball, mut Count count)
{
    Draw.Circle(ball.position, 0.1, Color(0.18, 0.83, 0.75));
    count.balls += 1;
}

view Total(Count count)
{
    Draw.SetScreen();
    Draw.Text($"{count.balls} balls, in the rooms that are loaded", float2(16, 14), 16, Color.white);
}

Which scene owns what

A spawn joins the scene of the entity the code is running for. In a handler, that's the entity the event was sent to, so the floor above joins the arena it was spawned for. A spawn into a scene that's unloaded by the spawn's turn is never made, like the loot an enemy drops while its arena unloads. Code that isn't running for an entity, like a system that runs once per tick, spawns into no scene, and those entities live until they're destroyed.

A scene itself is never owned: it lives until it's unloaded, whoever loaded it.

Loaded scenes share their world: the same systems, singletons and Time.

Private scenes

Scenes are public by default. SceneVisibility.Private and Scene.AddPlayer restrict a scene and its entities to the chosen players:

tide
scene Hand
{
    PlayerID player;
}

event(PlayerJoined joined) DealIn()
{
    var hand = Scene.Load(Hand { player = joined.player }, SceneVisibility.Private);
    Scene.AddPlayer(hand, joined.player);
}

Scene.AddPlayer(scene, player) and Scene.RemovePlayer(scene, player) change that membership. It is part of the match, so only match code changes it.

A private scene with no members stays on the server. Adding a player supplies its current permitted state; removing one conceals it. Entity and component policies can restrict this audience further, using the Visibility component and shared groups. The server simulates the complete match and corrects clients where hidden simulation affects their permitted state.