Skip to content

Reflection

Reflection is the system that makes Lumina’s C++ types known at runtime. You annotate a class, struct, enum, property, or function in C++, and a build-time tool generates the metadata that describes it. That metadata is the foundation the editor, serialization, networking, and C# scripting are all built on. Write your type once, and every one of those systems understands it with no extra glue.

Three macros do the work.

  • REFLECT(...) annotates a class, struct, or enum for reflection.
  • GENERATED_BODY() goes inside the type and is replaced with the generated boilerplate (its StaticClass()/StaticStruct() accessor, constructors, and so on).
  • The header includes its own *.generated.h as the last include.

A reflected component struct looks like this.

#pragma once
#include "Core/Object/ObjectMacros.h"
#include "HealthComponent.generated.h" // always last
namespace Lumina
{
REFLECT(Component, Category = "Gameplay")
struct RUNTIME_API SHealthComponent
{
GENERATED_BODY()
PROPERTY(Script, Editable, Category = "Health", ClampMin = 0)
float Max = 100.0f;
PROPERTY(Script, ReadOnly)
float Current = 100.0f;
};
}

A reflected class derives from CObject (these are the C-prefixed asset and object types).

REFLECT()
class RUNTIME_API CMyAsset : public CObject
{
GENERATED_BODY()
public:
PROPERTY(Editable)
FString DisplayName;
};

Enums reflect too, REFLECT() for a plain enum, REFLECT(BitMask) for a flags enum so the editor shows checkboxes.

REFLECT()
enum class EDoorState : uint8 { Closed, Opening, Open };

PROPERTY(...) exposes a field. The specifiers control what each system may do with it.

SpecifierEffect
EditableShown and editable in the Details panel; serialized.
ReadOnlyShown in the Details panel but not editable; serialized.
ScriptExposed to C# (readable/writable per the editor flags above).
ReplicatedParticipates in network replication.
EditorOnlyKept for editor tooling; stripped from cooked/packaged builds.
NoSerializeNot saved or loaded.
ScriptReadOnly / ScriptWritable / ScriptHiddenShape the C# wrapper independently of the editor flags. Make an editor-hidden field writable from script, or hide a public field from C#.

A specifier can take a value, and any Key = Value the macro doesn’t recognize is stored as metadata the editor reads. The common editor hints follow.

MetadataEffect
Category = "..."Group the property under a header in the Details panel.
ClampMin / ClampMaxNumeric bounds on the drag/slider.
Units = "..."Unit suffix shown after the value (e.g. "m/s").
ColorDraw a color picker for a vector value.
PROPERTY(Editable, Category = "Movement", ClampMin = 0, Units = "m/s")
float MoveSpeed = 5.0f;
PROPERTY(Editable, Color)
FVector4 Tint = FVector4(1.0f);

A TInstancedStruct<TBase> property owns a struct instance whose concrete type you pick in the Details panel, from TBase or any reflected struct deriving from it. The picker sits on the property’s row, and the chosen struct’s own properties edit inline beneath it. It is the value-type form of an instanced object: store a different behavior struct per instance and edit it in place.

#include "Core/Object/InstancedStruct.h"
REFLECT()
struct RUNTIME_API SCommand
{
GENERATED_BODY()
};
REFLECT()
struct RUNTIME_API SWaitCommand : public SCommand
{
GENERATED_BODY()
PROPERTY(Editable, ClampMin = 0, Units = "s")
float Seconds = 1.0f;
};
REFLECT(Component)
struct RUNTIME_API SAIComponent
{
GENERATED_BODY()
// The picker offers SCommand and every struct derived from it.
PROPERTY(Editable)
TInstancedStruct<SCommand> Command;
};

Read the stored value with Command.GetPtr<SWaitCommand>() (null unless the stored type is SWaitCommand or derived), and replace it with Command.InitializeAs<SWaitCommand>(). The value serializes inline by the chosen struct’s name. The base SCommand only needs REFLECT() + GENERATED_BODY(). The same workflow is available to C# scripts through the [Instanced] attribute; see Instanced properties.

FUNCTION(Script) exposes a member function to C#, so a script can call it on the reflected type’s wrapper (this is how Transform.AddYaw(...) or Controller.Jump() reach native code, no binding written by hand).

REFLECT(Component, Category = "Gameplay")
struct RUNTIME_API SDoorComponent
{
GENERATED_BODY()
PROPERTY(Editable)
bool bOpen = false;
FUNCTION(Script)
void Toggle() { bOpen = !bOpen; }
};

FUNCTION(Script, NoSuppressGCTransition) is a variant for a function that may exceed the fast managed→native budget (e.g. one that walks a hierarchy).

REFLECT(...) itself takes specifiers that classify the type.

SpecifierMeaning
ComponentThe struct is an ECS component (shows up in the Add Component menu and gets a C# wrapper).
System / EventRegister the struct as an ECS system or an event type.
Category = "..."The component’s group in the Add Component menu.
BitMask(Enums) treat the enum as flags.
MinimalAPIExport only the reflection plumbing across modules, not the whole type.

There are also opt-outs for the C# layer (NoCSharp / ManualStub) for types whose bindings are hand-written instead of generated (the core math types use these).

Reflection is also why Lumina’s type names carry a one-letter prefix.

PrefixMeaningExample
CA reflected class (a CObject, usually an asset or object type)CMaterial, CPrefab
SA reflected struct, including all componentsSTransformComponent, SRigidBodyComponent
FA plain (non-reflected) engine typeFVector3, FName

When you see an S- or C-prefixed name in the editor or a script, it is a reflected type, and the same name works in C#.

You never write the metadata by hand. A build-time tool, the Reflector (Engine/Applications/Reflector), parses your headers with libclang (the REFLECT/PROPERTY/FUNCTION macros expand to clang annotations it reads) and emits two things per module.

  • the *.generated.h files each header includes, and
  • a generated source file that registers every type’s CClass/CStruct at module load, plus the C# wrappers for Script-flagged members.

This runs as a prebuild step before each module compiles, so the generated code is always in sync with your headers. The output lands in Intermediates/Reflection/. It is generated, so don’t edit it.

Alongside its properties, every reflected struct gets a small table of operations called FStructOps. The codegen wires each struct’s StructOps to MakeStructOps<T>(), which inspects the type at compile time and fills in only the operations the type actually supports. You register nothing. If your struct defines one of these functions, the reflection system finds it and uses it.

These are the functions it looks for, each detected by whether your type provides it.

If your struct definesFStructOps fills inUsed for
bool Serialize(FArchive&)SerializeDisk serialization, replacing the default per-property path
void NetSerialize(FNetArchive&)NetSerializeNetwork/wire serialization, so a type can quantize itself
void CopyFrom(const T&)CopyCopying the value
operator==EqualsEquality and editor diffing
FString ToString() constToStringText display
operator<LessThanSorting

Two more come straight from the type. A Construct is filled in when the struct is default-constructible, and a Destruct from its destructor, so the engine can build and tear down instances (for example the default instance the editor diffs against when you reset a property to its default).

When a struct provides none of the optional functions, the reflection system falls back to its default behavior and walks the struct’s reflected properties one by one. So a custom Serialize or NetSerialize is an opt-in fast path or special case, not a requirement. The quantized math types, for instance, define NetSerialize to pack themselves tightly on the wire.

SystemHow it uses reflection
EditorThe Details panel builds itself from a type’s Editable/ReadOnly properties and their metadata, with no hand-written UI per component.
SerializationReflected properties are what gets written into worlds, prefabs, and assets; EditorOnly properties are stripped when cooking.
NetworkingReplicated properties are collected and sent server→client, see Networking.
ScriptingEvery Component struct and Script property/function is exposed to C# by name, see C# Scripting.
Object systemCClass/CStruct, StaticClass(), type-safe casts, and object construction all run on the generated type info.