Programming your game

Your first component

Create a .cs file anywhere under your project's Assets/ folder — for example Assets/Scripts/Rotator.cs:

namespace Usercode;

public class Rotator : Component
{
    /// <summary>Degrees per second.</summary>
    public float speed = 90f;

    public override void OnUpdate(float deltaTime) =>
        Node!.Transform.SetRotationY(Node.Transform.Rotation.Y + speed * deltaTime);
}

Save the file. Studio notices the change, recompiles your scripts in the background and swaps them in — compiler errors appear in the Output panel. Then select a node, click Add Component in the Inspector and pick Rotator, or drag the script from the Assets panel onto the node.

The first time Studio compiles a script it writes a Rotator.cs.meta beside it. That file holds the id scenes use to find the component, so keep it next to the script and commit it with it.

Rules of thumb:

  • One component class per file, named like the file.
  • Scripts are ordinary C# on .NET 10: use any language feature and the base class library.
  • New projects include Assets/Globals.cs, whose global using lines bring the engine (Turian.Engine.Core), its attributes (Turian), System.Numerics, logging and the input key codes into every script. Edit it freely.
  • Use Log.Logger (a Microsoft.Extensions.Logging logger) to write to the Output panel and the console.

Lifecycle callbacks

Override only the ones you need:

Callback Called when
OnAwake() Once, when the component is first attached — even if it is inactive.
OnEnable() The component becomes active (also right after OnAwake when active).
OnStart() Once, on the first frame, before the first OnUpdate.
OnUpdate(float deltaTime) Every frame while active; deltaTime is in seconds.
OnLateUpdate(float deltaTime) Every frame, after every OnUpdate.
OnFixedUpdate(float fixedDeltaTime) At a fixed timestep.
OnDisable() The component becomes inactive.
OnDestroy() The component is removed or its node destroyed.

See Components for the full model.

Example: an FPS counter

public class FpsCounter : Component
{
    float elapsed;
    int frames;

    public override void OnUpdate(float deltaTime)
    {
        elapsed += deltaTime;
        frames++;
        if (elapsed < 1f) return;

        Log.Logger.LogInformation("FPS: {Fps:F1}", frames / elapsed);
        elapsed = 0f;
        frames = 0;
    }
}

Editing values in the Inspector

Public fields and properties with a setter become Inspector controls, saved per node in the scene:

Type Inspector control
bool checkbox
integers, float, double, decimal number field — drag the label to scrub
string text field
enums dropdown
Vector2, Vector3, Vector4 one field per axis
List<T> resizable list
nested classes and structs foldout group
AssetReference<T> asset slot — drop an asset from the Assets panel
NodeRef<Node>, ComponentRef<T> scene slot — drop a node from the Scene Tree

Attributes refine how a member is shown:

public class Patrol : Component
{
    [Range(0, 20)] public float speed = 2.5f;          // clamped slider-style scrubbing
    public ComponentRef<CameraComponent>? watcher;      // drop a camera node here
    public AssetReference<Prefab>? loot;                // drop a .prefab here
    [ReadOnly] public int visits;                       // shown, not editable
    [Hide] public float internalTimer;          // public, but hidden

    [Button]                                             // drawn as a button in the Inspector
    public void ResetVisits() => visits = 0;
}

Private fields are hidden unless marked [Show]. A scene-reference field stores the target node's id; call Resolve(root) to get the live object:

var root = Node!;
while (root.Parent is not null) root = root.Parent;
var camera = watcher?.Resolve(root);

Where to go next

  • Input — keyboard, mouse, gamepad and action maps.
  • Scenes — loading and switching scenes.
  • Game UI — menus and HUDs with .ui documents.

← All docs Edit this page