Skip to content

Entities & Components

Entity is the entity the script is attached to, and Registry is the world’s component store. This page covers working with this entity. To work with other entities, see The World API.

Component types come from C++ through reflection, so you refer to them by name (STransformComponent, SRigidBodyComponent, and so on) as a generic type argument. You reach a component through Registry, passing the entity it lives on.

// Read this entity's rigid body, if it has one.
SRigidBodyComponent? Body = Registry.TryGet<SRigidBodyComponent>(Entity);
if (Body != null)
{
Body.Mass = 5.0f;
}
// Add a mesh component and configure it in place.
SStaticMeshComponent Mesh = Registry.Emplace<SStaticMeshComponent>(Entity)!;
Registry.Remove<SBillboardComponent>(Entity);
MethodReturns
Registry.Get<T>(Entity)The component; throws if absent
Registry.TryGet<T>(Entity)The component, or null
Registry.Has<T>(Entity)bool
Registry.Emplace<T>(Entity)Adds the component if missing and returns it (idempotent). Add<T> is an alias
Registry.GetOrAdd<T>(Entity)The component, adding a default one first if absent
Registry.Remove<T>(Entity)bool (whether one was removed)

Emplace and GetOrAdd return T?, and the result is null for a tag, a component with no fields (SDisabledTag and friends). Presence is the whole value there, so test it with Has<T>.

The returned wrapper points at the live component, writing its fields writes through to the entity’s data. A component’s own methods and fields depend on its type; see Entities & Components for the catalog.

A component wrapper is a pointer into the registry’s storage, and that storage moves. Adding or removing any component on any entity can reallocate or swap the array a wrapper points into, so a wrapper you stored in a field last frame may address the wrong entity, or freed memory, this frame.

Resolve inside the callback that uses it, and hold it only for that call.

public sealed class Mover : EntityScript
{
public override void OnUpdate(float DeltaTime)
{
SRigidBodyComponent? Body = Registry.TryGet<SRigidBodyComponent>(Entity);
if (Body is not null)
{
Body.LinearDamping = 0.1f;
}
}
}

Transform follows the same rule: it is not a cached field, it re-resolves on every access, which is exactly why reading it is always safe.

What you can cache is anything that is a value rather than a view: an Entity handle, a script instance from GetScript<T>(), an asset reference, or the numbers you read out of a component.

MemberWhat it is
Entity.IdThis entity’s raw id (a uint)
Entity.IsNulltrue for the null handle
World.GetEntityName(Entity)This entity’s name
World.DestroyEntity(Entity)Removes this entity
World.DuplicateEntity(Entity)Deep-copies it, returns the new entity

Transform is the live STransformComponent. Most methods work in local space (relative to the parent); the World variants resolve through the parent chain. Getters return FVector3 / FQuat.

FVector3 Here = Transform.GetLocalLocation(); // local-space position
Transform.SetLocalLocation(new FVector3(0, 2, 0)); // local-space
FVector3 World = Transform.GetWorldLocation(); // resolved world position
Transform.Translate(new FVector3(0, 0, 1));
Transform.AddYaw(90.0f); // degrees
Transform.SetLocalRotationFromEuler(new FVector3(0, 90, 0));
MethodSpaceReturns
GetLocalLocation() / SetLocalLocation(v)localFVector3
GetLocalRotation() / SetLocalRotation(q)localFQuat
GetLocalScale() / SetLocalScale(v)localFVector3
GetWorldLocation() / GetWorldRotation() / GetWorldScale()world
GetLocalRotationAsEuler() / SetLocalRotationFromEuler(e)localdegrees
AddLocalRotationFromEuler(e)localdegrees
Translate(delta)localFVector3
AddYaw(deg) / AddRoll(deg)local
AddPitch(deg, clampMin, clampMax)localThe clamps have C++ defaults of -89.9 and 89.9; a generated C# binding carries no defaults, so pass all three.
GetForward() / GetRight() / GetUp()worldFVector3
SetWorldTransform(t)world

Parent and child links live on World, keyed by entity.

Entity Parent = World.GetParent(Entity); // Entity.Null if none
World.SetParent(Child, Entity); // reparent, preserving world transform
World.DetachFromParent(Entity); // detach to the world root
Entity Root = World.GetRootEntity(Entity); // top of this entity's tree

If this entity has a camera, you can read and tune it through its component.

SCameraComponent Camera = Registry.Get<SCameraComponent>(Entity);
Camera.SetFOV(70.0f);

World.GetActiveCamera() returns the world’s current view camera. To make a camera follow another entity, add an SCameraFollowComponent and set its target (see Cameras).

Expose a field to the editor with the [Property] attribute. It appears in the entity’s Entity Script section in the Details panel, and you read or write it like any field. The field’s type picks the widget: a numeric drag, a vector or color picker, an enum dropdown, an asset or entity picker, a list.

[Property(Category = "Movement", Min = 0, Max = 20, Units = "m/s", Tooltip = "Top speed.")]
public float Speed = 5.0f;
[Property(Color = true)] public FVector3 Tint = new(1, 1, 1);
// Typed asset and entity references draw a searchable picker from their type.
[Property] public TSoftObjectPtr<CStaticMesh> Mesh;
[Property] public Entity Target;

= 5.0f is not just a starting value for one instance, it is the property’s default. The engine records it once per script type, and every new instance of that script starts from it. The Details panel’s reset control returns the field to exactly that value.

Change a default and existing entities keep whatever they were authored with. Only entities that never overrode the field pick up the new value.

A script property’s value lives in native memory, not in a C# field. The engine gives your script type a real reflected property, and the [Property] field you wrote becomes an accessor over it.

You do not have to do anything about this, and reading or writing the field is an ordinary field access. It is worth knowing because it is why a script property needs no save or sync code: the inspector, scene saving, undo, prefab overrides, and network replication all read the same storage your script does, so they always agree.

Two consequences show up in what you can declare.

The C# types carry the same names as their C++ counterparts, so a TVector<T> here is the engine’s TVector<T> there.

TypeNotes
float, double, bool, int, uint, long, byte, …Every numeric type, plus bool.
An enumDraws a dropdown.
stringStored as an engine FString.
FStringThe same storage as string, spelled as the engine type. Use this one inside a container, where string cannot go.
FNameAn interned name. Compared by id rather than by text, and case insensitive.
FVector2 / FVector3 / FVector4 / FQuat / FTransformAny engine math type.
ColorRGBA, stored as an FVector4. Pair it with Color = true for the picker.
A struct you declareGrouped members, drawn as a nested section. See Your own struct.
EntityDraws an entity picker.
FSoftObjectPath, TSoftObjectPtr<T>Soft asset references, resolved on demand. See Asset references.
TObjectPtr<T>A hard reference to a live object, which keeps it alive.
A class deriving NativeObjectA direct reference to a CObject, for example CTexture.
TVector<T>A list. See Lists and maps for what T may be.
THashMap<K, V>A key/value map. K and V are plain values.
SInputAction / SInputAxisAn action binding, drawn as a dropdown of the project’s actions. The only property kind that stays a managed field. See Input.

Anything else is a build error naming the field, rather than a property that silently never appears.

Group related settings in a struct and use it as a property. Its members are minted as a nested struct and drawn as their own section in the Details panel.

public struct AimSettings
{
[Property] public float Spread;
[Property] public bool Auto;
[Property] public FVector3 Offset;
[Serialize] public double Recoil;
}
public sealed class Weapon : EntityScript
{
[Property] public AimSettings Aim;
}

It nests and it goes in a list, so TVector<AimSettings> and a struct holding another struct both work. Assign it as a whole value, the way you would any struct.

AimSettings V = Aim;
V.Spread = 2.5f;
Aim = V;

Three rules apply, and breaking one is a build error naming the field.

  • Every field must be marked [Property] or [Serialize]. The struct is the value native stores, so an unmarked field would leave the two sides disagreeing about its size. Use [Serialize] for a member you want stored but not shown.
  • Every field must itself be storable as raw bytes: numbers, bool, engine math types, or another such struct. A string, a list, or a reference makes the struct managed and it cannot back a property.
  • Enums are fine, at any underlying width. The native slot is the C# underlying type’s width, so byte, int, and long enums all pack as they do in C#.

The engine checks the minted layout against the managed size when it builds the class, so a disagreement is reported and the property dropped rather than read out of bounds.

[Instanced], which would let a property hold a polymorphic subclass picked in the inspector, is not reachable from a script. The attribute exists for native use; a script field marked with it is rejected at compile time.

Putting [Property] on a plain class of your own fails differently from a struct: the compiler rewrites those fields into accessors over native storage that only an EntityScript has, so the build stops with CS0103: The name 'HasNativeStorage' does not exist in the current context pointing at your helper class. Use a struct.

A list is a TVector<T> and a map is a THashMap<K, V>. Both are views over the container the engine owns, so you declare them without an initializer and use them like an IList<T> and an IDictionary<K, V>.

[Property] public TVector<float> Cooldowns;
[Property] public TVector<FString> Tags;
[Property] public TVector<FName> Slots;
[Property] public THashMap<int, float> WeightByTier;
public override void OnReady()
{
Cooldowns.Clear();
Cooldowns.Add(1.5f);
Cooldowns[0] = 2.0f;
Tags.Add("boss");
Tags.Set(0, "elite");
string First = Tags.Get(0);
Slots.Add(new FName("Head"));
WeightByTier.Set(1, 0.5f);
if (WeightByTier.TryGetValue(1, out float Weight))
{
// ...
}
foreach (var Pair in WeightByTier)
{
// Pair.Key, Pair.Value
}
}

A TVector<T> element is stored in the engine’s own buffer, so T has to be something that can live there.

ElementAllowedWhy
A number, bool, Entity, or a math type like FVector3YesThe value is its bytes.
FNameYesAn interned id, so it is a plain value too.
FStringYesThe list reads and writes each slot through the engine’s string, not by copying bytes.
TObjectPtr<T>YesThe list assigns each slot through the engine, so the reference count stays right.
stringNoA managed reference cannot live in native memory. Use FString.
A class deriving NativeObject, such as CTextureNoNot a storable reference. Use TObjectPtr<CTexture>.
An enumYesThe slot is the underlying type’s width, so it packs like the number it is.
FSoftObjectPath, TSoftObjectPtr<T>NoAn asset reference is stored as a path, not as bytes.
Another TVector<T> or THashMap<K, V>NoThere is no nested container property. Give the elements a struct type instead.

A THashMap<K, V> is stricter: its key and value must both be plain values.

Each rejection is a build error naming the field and the element type, and it names the type to write instead.

[Property] public TVector<TObjectPtr<CTexture>> Layers;
public override void OnReady()
{
Layers.Add(new TObjectPtr<CTexture>(SomeTexture));
Layers.Set(0, new TObjectPtr<CTexture>(OtherTexture));
CTexture? First = Layers.Get(0).Value;
}

In the Details panel a list adds a numbered row per element, and a map adds one row per entry with the key on the left and the value on the right, each with Add, Clear, and per-row remove controls.

Because the container is a view, assigning the property itself is meaningless and the compiler rejects both = new TVector<float>() and a later assignment. Add to it, clear it, or remove from it instead.

Assigning through a property that returns a struct does not compile, so how you write an element depends on whether the C# value is the stored bytes.

ContainerWriting an element
TVector<T> of a plain valueList[i] = value works. The element is a T in native memory, so the list hands it back by reference.
TVector<FString>, TVector<TObjectPtr<T>>Use List.Set(i, value) and List.Get(i). The indexer throws for these, because a reference into the slot would let a plain assignment copy a string’s buffer pointer or store an object pointer without taking a reference.
THashMap<K, V>Use Map.Set(key, value). A by-reference indexer would have to insert on a miss, which would make reading an absent key add it.

One script declaring one of everything, as a reference to copy from.

The engine types (FVector3, FString, FName, TVector<T>, and the rest) live in the Lumina namespace, so a script in a namespace of your own needs using Lumina;. Scripts are compiled with implicit usings off, so nothing adds it for you.

using Lumina;
using LuminaSharp;
namespace GameScripts;
public sealed class EveryPropertyType : EntityScript
{
public enum EMode { Off, Slow, Fast }
// Numbers and bool. The initializer is the default.
[Property] public float Speed = 3.5f;
[Property] public double Precise = -1.25;
[Property] public bool Enabled = true;
[Property] public sbyte Tiny = -3;
[Property] public short Small = -300;
[Property(Min = -100, Max = 100)]
public int Ranged = 7;
[Property] public long Big = 900000;
[Property] public byte Level = 200;
[Property] public ushort Count = 4000;
[Property] public uint Id = 70000;
[Property] public ulong Huge = 12345678901;
// An enum draws a dropdown.
[Property] public EMode Mode = EMode.Slow;
// Math types. Color = true swaps the drag fields for a color picker.
[Property] public FVector2 Offset = new FVector2(10, 20);
[Property(Color = true)]
public FVector3 Tint = new FVector3(0.25f, 0.5f, 0.75f);
[Property] public FVector4 Rect = new FVector4(1, 0, 0, 1);
[Property] public FTransform Anchor = FTransform.Identity;
// Text. FString is the same storage as string, and FName is an interned id.
[Property] public string Label = "declared default";
[Property] public FString Note = "also a native string";
[Property] public FName Slot = new FName("Head");
// An entity picker.
[Property] public Entity Target;
// References. Soft ones resolve on demand, the hard one keeps its object alive.
[Property] public FSoftObjectPath AnyAsset;
[Property] public TSoftObjectPtr<CTexture> Icon;
[Property] public TObjectPtr<CTexture> LoadedIcon;
[Property] public CTexture? Direct;
// Containers. No initializer: they are views over storage the engine owns.
[Property] public TVector<int> Steps;
[Property] public TVector<FVector3> Path;
[Property] public TVector<FString> Tags;
[Property] public TVector<FName> Slots;
[Property] public TVector<TObjectPtr<CTexture>> Layers;
[Property] public THashMap<int, float> WeightByTier;
public override void OnReady()
{
// Plain-value elements: the indexer hands back a reference.
Steps.Add(1);
Steps[0] = 2;
// Marshalled elements: Get and Set, because the C# value is not the bytes.
Tags.Add("boss");
Tags.Set(0, "elite");
string First = Tags.Get(0);
Slots.Add(new FName("Offhand"));
WeightByTier.Set(1, 0.5f);
}
}

Every [Property] key is optional.

KeyEffect
Category = "X"Groups the field under a collapsible header. Nest with "A|B".
Tooltip = "X"Hover help on the field.
Name = "X"Renames the field; this is both its inspector label and its saved key.
Min = n / Max = nClamp range for a numeric field.
Units = "X"Unit suffix after a numeric value, e.g. "m/s".
Color = trueDraws an RGBA color picker for an FVector3, FVector4, or Color instead of drag fields.

Related attributes control persistence and hot reload.

AttributeEffect
[Serialize]Persists the field with the entity without showing it in the inspector. It gets the same native storage a [Property] does, so the same type rules apply.
[Hide]Keeps the field from ever being serialized or shown.
[Alias("OldName")]A prior name, so the value survives a rename: both when loading a saved scene and across a live C# hot reload. Repeatable. Also valid on the script class, which carries attached scripts onto the renamed class.
[SkipHotReload]Resets the field to its default on a C# hot reload instead of carrying the old value. Also valid on the script class to reset all of its properties.

Two more attributes go on members other than a property field.

AttributeOnEffect
[Button("Label")]a parameterless methodDraws a button in the script’s inspector section that calls the method on the live instance. Only while the game is running; a method taking arguments is ignored with a warning.
[UpdatePhase(EScriptPhase.PostPhysics)]the classMoves this script’s OnUpdate to after the physics step. See Pre-physics and post-physics.
[Button("Reload")]
public void BeginReload()
{
Magazine = MagazineSize;
}

A hot reload picks up added, removed, and retyped properties on attached scripts: the engine rebuilds the class and replays your authored values.

The replay matches by name, so a rename looks like one property removed and another added, and the value is lost. [Alias] is what carries it across.

// Speed was renamed to Velocity. The old name keeps the authored value.
[Property, Alias("Speed")] public float Velocity = 5.0f;

The same applies to renaming the script class itself. Put [Alias] on the class and every attached script moves onto the new one, in a live reload and when loading a scene saved under the old name.

[Alias("GameScripts.OldPatrolScript")]
public sealed class Patrol : EntityScript { }

Without an alias the property or class is treated as new, so it starts at its default rather than picking up whatever happened to be in those bytes.

If a [Property] is rejected, the compile error says which field and why.

ErrorCause
LUM0101The field’s type cannot be a script property. See the table above.
LUM0102A TVector<T> or THashMap<K, V> was given an initializer. Fill it in OnReady instead.
LUM0103The member is a partial property. Declare it as a plain field.