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.
Marking a type
Section titled “Marking a type”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 (itsStaticClass()/StaticStruct()accessor, constructors, and so on).- The header includes its own
*.generated.has 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 };Properties
Section titled “Properties”PROPERTY(...) exposes a field. The specifiers control what each system may do
with it.
| Specifier | Effect |
|---|---|
Editable | Shown and editable in the Details panel; serialized. |
ReadOnly | Shown in the Details panel but not editable; serialized. |
Script | Exposed to C# (readable/writable per the editor flags above). |
Replicated | Participates in network replication. |
EditorOnly | Kept for editor tooling; stripped from cooked/packaged builds. |
NoSerialize | Not saved or loaded. |
ScriptReadOnly / ScriptWritable / ScriptHidden | Shape 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.
| Metadata | Effect |
|---|---|
Category = "..." | Group the property under a header in the Details panel. |
ClampMin / ClampMax | Numeric bounds on the drag/slider. |
Units = "..." | Unit suffix shown after the value (e.g. "m/s"). |
Color | Draw 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);Instanced struct properties
Section titled “Instanced struct properties”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.
Functions
Section titled “Functions”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).
Type specifiers
Section titled “Type specifiers”REFLECT(...) itself takes specifiers that classify the type.
| Specifier | Meaning |
|---|---|
Component | The struct is an ECS component (shows up in the Add Component menu and gets a C# wrapper). |
System / Event | Register 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. |
MinimalAPI | Export 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).
Naming prefixes
Section titled “Naming prefixes”Reflection is also why Lumina’s type names carry a one-letter prefix.
| Prefix | Meaning | Example |
|---|---|---|
C | A reflected class (a CObject, usually an asset or object type) | CMaterial, CPrefab |
S | A reflected struct, including all components | STransformComponent, SRigidBodyComponent |
F | A plain (non-reflected) engine type | FVector3, 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#.
How the metadata is generated
Section titled “How the metadata is generated”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.hfiles each header includes, and - a generated source file that registers every type’s
CClass/CStructat module load, plus the C# wrappers forScript-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.
Struct operations
Section titled “Struct operations”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 defines | FStructOps fills in | Used for |
|---|---|---|
bool Serialize(FArchive&) | Serialize | Disk serialization, replacing the default per-property path |
void NetSerialize(FNetArchive&) | NetSerialize | Network/wire serialization, so a type can quantize itself |
void CopyFrom(const T&) | Copy | Copying the value |
operator== | Equals | Equality and editor diffing |
FString ToString() const | ToString | Text display |
operator< | LessThan | Sorting |
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.
What reflection powers
Section titled “What reflection powers”| System | How it uses reflection |
|---|---|
| Editor | The Details panel builds itself from a type’s Editable/ReadOnly properties and their metadata, with no hand-written UI per component. |
| Serialization | Reflected properties are what gets written into worlds, prefabs, and assets; EditorOnly properties are stripped when cooking. |
| Networking | Replicated properties are collected and sent server→client, see Networking. |
| Scripting | Every Component struct and Script property/function is exposed to C# by name, see C# Scripting. |
| Object system | CClass/CStruct, StaticClass(), type-safe casts, and object construction all run on the generated type info. |