C# Scripting
Gameplay in Lumina is written in C#. A script is a C# class that attaches to an entity and runs that entity’s behavior. Scripts compile inside the editor on save, so changing a file updates the running editor with no rebuild and no restart.
This page covers how a script is shaped and how it runs. The rest of the section documents the API surface.
- Entities & Components, working with this entity and its components.
- The World API, spawning, finding, and moving entities.
- World Systems, world-level systems that run a rule across many entities.
- Physics, forces, velocities, queries, and collision events.
- Input, actions, keys, and the mouse.
- User Interface, screen-space RmlUi documents driven from C#.
- Low-Level Rendering (RHI), custom GPU work: compute dispatch, textures, and the bindless heap.
- Events, the gameplay message bus and component signals.
- Parallel Work, running heavy compute across worker threads.
- Networking, roles and replication.
- Reference, types, math, and the global API.
The C# language
Section titled “The C# language”These docs cover Lumina’s API, not the language itself. For C# syntax, the type
system, and the standard library, see the official
C# documentation. Lumina hosts
.NET 10 / CoreCLR, so the full base class library (System.*, LINQ, collections,
MathF) is available to a script, though for hot per-frame code you’ll lean on
the engine’s value types and avoid per-frame allocation.
Where scripts live
Section titled “Where scripts live”Scripts are .cs files in your project’s Game/Scripts/ folder. The editor
compiles every script in the project into one assembly; you attach a script to an
entity by its class name (for example GameScripts.Player), not by file path.
Anatomy of a script
Section titled “Anatomy of a script”A script is a class that derives from EntityScript and overrides the lifecycle
hooks it needs. This complete script orbits its entity around its starting point.
using System;using LuminaSharp;using Lumina;
namespace GameScripts;
public sealed class Orbit : EntityScript{ [Property(Tooltip = "Orbit radius in meters.")] public float Radius = 4.0f;
[Property(Tooltip = "Revolutions per second.")] public float Speed = 0.25f;
private float _Time; private FVector3 _Origin;
public override void OnReady() { _Origin = Transform.GetLocalLocation(); }
public override void OnUpdate(float DeltaTime) { _Time += DeltaTime * Speed * MathF.Tau; float X = _Origin.X + MathF.Sin(_Time) * Radius; float Z = _Origin.Z + MathF.Cos(_Time) * Radius; Transform.SetLocalLocation(new FVector3(X, _Origin.Y, Z)); }}[Property] exposes a field in the editor, and its initializer is the field’s
default: Radius starts at 4 on every new instance, and the Details panel’s
reset returns to it. See
Editable properties
for the types a property can be.
EntityScript gives every script a few members, ready to use before the first
hook runs.
| Member | What it is |
|---|---|
Entity | This entity’s handle (an Entity, wrapping its id). |
World | The world this entity lives in. |
Registry | The world’s component store (World.Registry). |
Transform | This entity’s STransformComponent, re-resolved on every access. |
DestroyToken | A CancellationToken canceled when the script is detached, to pass to GameTask awaits. |
EnableInput() / DisableInput() | Add or remove this entity’s SInputComponent, which is what makes OnInput and the input queries fire. |
A typed, reflection-driven API
Section titled “A typed, reflection-driven API”Through the reflection system, every component type you
define in C++ is exposed to C# by its name, with no binding code to write. You
refer to a component by its C++ name (STransformComponent,
SRigidBodyComponent, SStaticMeshComponent) as a generic type argument, and
read or write its members directly.
SRigidBodyComponent Body = Registry.Get<SRigidBodyComponent>(Entity);Body.Mass = 5.0f;The component types, the math types (FVector3, FQuat), and the event types
(SCollisionEvent, SInputEvent) all live in the Lumina namespace; the
scripting surface (EntityScript, Entity, Registry, attributes, Physics,
Net) lives in LuminaSharp. Most scripts open both.
using LuminaSharp;using Lumina;Entity is this entity, World is everything else
Section titled “Entity is this entity, World is everything else”The API has one rule that keeps it clean.
Entity/Transformare this entity. Use them for this entity’s transform, components, and identity.Worldis everything else, including other entities, physics, navigation, networking, and global helpers.
World exposes its subsystems as properties (World.Physics, World.Draw,
World.Net, World.Navigation, World.Messages, and more) covered on the pages
that follow.
Lifecycle hooks
Section titled “Lifecycle hooks”Override only the ones you need. A hook you don’t override costs nothing. An
entity with no OnUpdate is never ticked.
| Method | When it runs |
|---|---|
OnAttach() | Once, when the instance is attached to its entity. The earliest hook. |
OnReady() | Once, after OnAttach, before the first OnUpdate (all siblings are attached). Cache things and look up other entities here. |
OnUpdate(float DeltaTime) | Every frame, while the entity is enabled. DeltaTime is seconds. |
OnFixedUpdate(float FixedDeltaTime) | At the fixed physics rate, 0..N times per frame. Use it for forces and movement. |
OnDetach() | Once, when the entity is destroyed or the script is removed. |
OnInput(SInputEvent Event) | A keyboard or mouse event happened, while the entity is receiving input. See Input. |
OnContactBegin(SCollisionEvent) / OnContactEnd(SCollisionEvent) | This entity’s body started or stopped touching another. See Collisions. |
OnOverlapBegin(SCollisionEvent) / OnOverlapEnd(SCollisionEvent) | The same, for a trigger volume. |
OnTargetPerceived(SPerceptionEvent) / OnTargetLost(SPerceptionEvent) | One of this entity’s senses acquired or lost a target. See Perception. |
That is the whole set. Overriding one sets its bit on the script’s class, so the engine only crosses into C# for the callbacks a script actually implements.
For the full picture (when each runs, the physics phase it runs in, what is per-entity, and how threading works) see Entity Systems. For what survives when you save a script while the editor is running, see Hot Reload.
Attaching a script
Section titled “Attaching a script”In the editor, add an Entity Script component to an entity, then use Add
Script… and pick your script’s type (for example GameScripts.Player). An entity can
carry several scripts at once, each with its own properties; they run
independently. Press Play or Simulate to run them.
Attach, read, and remove scripts at runtime too. From inside a script these act on its own entity:
Weapon weapon = AddScript<Weapon>(); // attach a new script, returns the instanceWeapon? w = GetScript<Weapon>(); // the first Weapon on this entity, or nullList<Weapon> all = GetScripts<Weapon>(); // every Weapon on this entityRemoveScript<Weapon>(); // remove the first WeaponThe same calls exist on the registry for any entity, e.g.
World.Registry.AddScript<Weapon>(entity) or
World.Registry.GetScript<Weapon>(entity).