Skip to content

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.

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.

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.

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.

MemberWhat it is
EntityThis entity’s handle (an Entity, wrapping its id).
WorldThe world this entity lives in.
RegistryThe world’s component store (World.Registry).
TransformThis entity’s STransformComponent, re-resolved on every access.
DestroyTokenA 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.

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 / Transform are this entity. Use them for this entity’s transform, components, and identity.
  • World is 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.

Override only the ones you need. A hook you don’t override costs nothing. An entity with no OnUpdate is never ticked.

MethodWhen 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.

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 instance
Weapon? w = GetScript<Weapon>(); // the first Weapon on this entity, or null
List<Weapon> all = GetScripts<Weapon>(); // every Weapon on this entity
RemoveScript<Weapon>(); // remove the first Weapon

The same calls exist on the registry for any entity, e.g. World.Registry.AddScript<Weapon>(entity) or World.Registry.GetScript<Weapon>(entity).