GameObject
A GameObject is the primary interface for interacting with entities in the helios ECS. It provides a convenient, type-safe wrapper around an EntityHandle and the EntityManager, enabling component manipulation without direct access to low-level ECS internals.
Overview
GameObject acts as a facade that combines:
- An
EntityHandleidentifying the entity - A pointer to the
EntityManagerthat owns the entity's data
This design keeps the handle lightweight while providing rich functionality through the manager.
// GameObject is typically obtained from GameWorld
auto player = gameWorld.addGameObject();
// Add components
player.add<TransformComponent>(glm::vec3{0.0f});
player.add<HealthComponent>(100.0f);
player.add<RenderableComponent>(mesh);
// Query components
if (player.has<HealthComponent>()) {
auto* health = player.get<HealthComponent>();
health->takeDamage(10.0f);
}
// Activate/deactivate
player.setActive(false); // Marks as inactive, calls onDeactivate()
player.setActive(true); // Marks as active, calls onActivate()
API Reference
Construction
GameObjects are created through GameWorld, not directly instantiated:
// Correct - obtain from GameWorld
auto entity = gameWorld.addGameObject();
// Correct - lookup existing
auto entity = gameWorld.gameObject(entityHandle);
Component Operations
| Method | Description |
|---|---|
add<T>(args...) | Constructs and attaches a component |
get<T>() | Returns pointer to component (or nullptr) |
getOrAdd<T>(args...) | Returns existing or creates new component |
has<T>() | Checks if component is attached |
has(typeId) | Checks by runtime type ID |
Adding Components
// Add with constructor arguments
auto& transform = player.add<TransformComponent>(position, rotation, scale);
// Add with default constructor
auto& health = player.add<HealthComponent>();
// Get existing or add new
auto& collider = player.getOrAdd<CollisionComponent>();
Retrieving Components
// Returns nullptr if not found
auto* transform = player.get<TransformComponent>();
if (transform) {
transform->setPosition(newPos);
}
// Const version for read-only access
const auto* health = player.get<HealthComponent>();
Activation State
GameObjects can be activated or deactivated, which propagates to all attached components:
// Deactivate - adds Inactive tag, calls onDeactivate()
player.setActive(false);
// Activate - adds Active tag, calls onActivate()
player.setActive(true);
// Query state
if (player.isActive()) {
// ...
}
When deactivated:
- An
Inactivetag component is added - The
Activetag component is removed onDeactivate()is called on components that support it
When activated:
- The
Inactivetag component is removed - An
Activetag component is added onActivate()is called on components that support it
Note:
setActive()does not callenable()/disable(). UseenableComponent()/disableComponent()for individual component toggling.
Lifecycle Hooks
GameObjects provide methods to trigger lifecycle hooks on all attached components:
// Called when acquired from object pool
player.onAcquire();
// Called when released back to pool
player.onRelease();
These methods iterate through all components and invoke the corresponding hooks if implemented.
Component Iteration
// Iterate over all component type IDs
for (auto typeId : player.componentTypeIds()) {
void* raw = player.raw(typeId);
// Process component...
}
Entity Handle Access
// Get the underlying handle
EntityHandle handle = player.entityHandle();
// Handles can be stored and compared
if (handle == otherHandle) {
// Same entity
}
Internal Structure
class GameObject {
EntityHandle entityHandle_; // 8 bytes (EntityId + VersionId)
EntityManager* entityManager_; // 8 bytes (pointer)
// ...methods delegate to entityManager_
};
The GameObject itself is:
- Lightweight (~16 bytes) - smaller than most smart pointers
- Copyable - multiple GameObjects can reference the same entity
- Non-owning - does not manage entity lifetime
- Pass-by-value friendly - should be passed by value, not by reference
Pass by Value
Due to its small size, GameObject should be passed by value rather than by reference or pointer:
// Correct - pass by value
void processEntity(GameObject entity) {
entity.get<HealthComponent>()->takeDamage(10.0f);
}
// Unnecessary - reference adds indirection for no benefit
void processEntity(GameObject& entity); // Avoid
void processEntity(const GameObject& entity); // Avoid
// Wrong - never use pointers for GameObject
void processEntity(GameObject* entity); // Never do this
Copying a GameObject is essentially copying two integers and a pointer - faster than dereferencing a reference in most cases.
Relationship with Other Classes
┌─────────────────────────────────────────────────────────────┐
│ GameWorld │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ EntityRegistry │◄───│ EntityManager │ │
│ │ (handle alloc) │ │ (component storage) │ │
│ └──────────────────┘ └────────────────────────────┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ EntityHandle │◄──────────│ GameObject │ │
│ │ (id+version) │ │ (facade) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Best Practices
-
Don't store GameObjects long-term - Store
EntityHandleinstead and resolve viaGameWorld::gameObject() -
Check component existence - Use
has<T>()beforeget<T>()or handle nullptr returns -
Use getOrAdd for optional components - Simplifies code when components may or may not exist
-
Prefer type-safe methods - Use
has<T>()overhas(typeId)when possible -
Leverage setActive for pooling - Deactivate instead of destroying for pooled objects
Example: Complete Entity Setup
auto enemy = gameWorld.addGameObject();
// Transform
enemy.add<ComposeTransformComponent>();
enemy.add<TranslationStateComponent>(spawnPosition);
enemy.add<ScaleStateComponent>(enemySize);
enemy.add<RotationStateComponent>();
// Rendering
auto& renderable = enemy.add<RenderableComponent>(mesh);
enemy.add<SceneNodeComponent>(sceneNode);
enemy.add<ModelAabbComponent>(renderable.localAABB());
// Physics
enemy.add<CollisionComponent>(CollisionLayer::Enemy);
enemy.add<AabbColliderComponent>();
enemy.add<Move2DComponent>(speed);
enemy.add<DirectionComponent>();
// Gameplay
enemy.add<HealthComponent>(100.0f);
enemy.add<DamageDealerComponent>();
// Pooling support
enemy.add<SpawnedByProfileComponent>();
See Also
- EntityManager - Low-level entity and component management
- EntityHandle - Versioned entity reference
- View - Component-based entity queries
- System - Game logic processors
- Component Lifecycle - Lifecycle hooks
- Component System - ECS architecture overview
- Sparse Set - Underlying component storage