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, whoseglobal usinglines 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(aMicrosoft.Extensions.Logginglogger) 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
.uidocuments.