Runtime Reflection in C++

Inspecting and manipulating types at runtime from unity-like editor.

Why reflection?

We wanted a way to easily change the game components parameters, almost like you would do in a professional game engine, allowing designers and artists to iterate on gameplay values without needing to touch source code or recompiling the engine.

To achieve this we built a custom runtime reflection system that scans through all the source files at compile-time, identifying classes and structs that contain reflection attributes. These attributes—such as [serializefield] or [hideineditor] serve as markers that tell the system which members should be exposed to the editor. The system then generates a .cpp file which includes the metadata for the class its currently reflecting.

And what this system also allows is the implementation of custom attributes, such as [ReplicateField] for networking.

Iteration

I developed two reflection systems. The first used a templatized class to store reflected values, which meant every instance of a class containing reflected variables also carried redundant metadata - type names, offset information, and validation flags - duplicated across hundreds or thousands of objects. That scrap data should have been stored in a central type registry, not per-instance.

The newer system fixes this by moving to compile-time reflection. All metadata is generated once per type during static initialization and shared across all instances. The per-object footprint is now just the raw data - no overhead, no duplication.

Difference

The old system:

class ObstacleSpawner : public Eclipse::Component
{
	COMPONENT_BASE_2(ObstacleSpawner, 3)

public:
	void Start() override;
	void Update() override;

private:
	SERIALIZED_FIELD(Eclipse::Assets::Prefab, ObstaclePrefab);
	SERIALIZED_FIELD(float, ObstacleSpawnInterval, 2);
	float obstacleSpawnTimer = 0.f;
};

SERIALIZED_FIELD macro expanded:

Eclipse::Reflection::SerializedVariable ObstaclePrefab{ "ObstaclePrefab", this, true, 0.1f };

Everything reflected is stored locally in the variable, which means every instance of this component will cause duplication of data.


The new system:

CLS
class Player : public Eclipse::Component
{
public:
	void Move();

private:
	[[serializefield]]
	float moveSpeed = 2.4f;
};
END_CLS

Build time code generation (via lexer), static/shared reflection data per class.

How It Works

1. Scan

Lexer scans all source files for attributes

2. Generate

Creates .cpp files with TypeInfo<T> specializations

3. Register

TypeRegistrar registers each type during static initialization

4. Runtime Usage

Engine queries metadata for UI, serialization, networking

Key Takeaway

The shift from per-instance macro metadata to build-time code generation proved to be dramatically more efficient. In the old system, each reflected variable carried a 100-200 byte SerializedVariable object meaning a component with 5 reflected fields added over 500 bytes of duplicated metadata per instance. For 10,000 objects, that's 5+ MB of wasted memory.

The new system eliminates this entirely. Reflection data is generated once per type and stored in static memory. Each instance now pays only for its actual data - no overhead, no duplication, just efficiency.