Vulkan Schnee 0.0.1 (22840dc8)
High-performance rendering engine
Loading...
Searching...
No Matches
Custom Actor And Component Handbook

This page shows a small custom actor with a custom logic component. The example is a basic enemy that owns health and destroys itself when health reaches zero.

Use this pattern for gameplay state that belongs on an actor but should stay separate from movement, rendering, or input code.

APIs Used

Task API
Actor base class Engine::Entities::Actor
Logic component base class Engine::Components::Logic
Attach component Engine::Entities::Entity::addComponent<T>(args...)
Read attached component Engine::Entities::Entity::getComponent<T>()
Spawn actor Engine::Entities::Scene::spawnActor<T>(transform, args...)
Spawn attached actor Engine::Entities::Scene::spawnActorAttached<T>(localTransform, parent, args...)
Attach actor Engine::Entities::Actor::attachTo(parent, rule)
Destroy actor Engine::Entities::Scene::destroyActor(actor)

Health Component

HealthComponent stores health and invokes a death callback once when health reaches zero. It does not know about enemy behavior or actor destruction.

#pragma once
#include "Engine/Component/LogicComponent.h"
#include <functional>
namespace AirHockey
{
class HealthComponent : public Engine::Components::Logic
{
public:
static constexpr bool IsUnique = true;
HealthComponent(Engine::Entities::Scene* owningScene, float maxHealth);
void inflictDamage(float amount);
void setOnDeath(std::function<void()> callback);
[[nodiscard]] float getHealth() const;
[[nodiscard]] bool isDead() const;
private:
float health_ = 0.0f;
bool isDead_ = false;
std::function<void()> onDeath_;
};
}
Base class for all logic components that can be attached to an actor. Provides access to the scene,...
Owns scene entities, actor lifetime, and the scene graph root.
Definition Scene.h:84
#include "AirHockey/Components/HealthComponent.h"
#include <algorithm>
#include <utility>
namespace AirHockey
{
HealthComponent::HealthComponent(
float maxHealth
) : Logic(owningScene), health_(maxHealth)
{
}
void HealthComponent::inflictDamage(float amount)
{
if (isDead_ || amount <= 0.0f) {
return;
}
health_ = std::max(0.0f, health_ - amount);
if (health_ > 0.0f) {
return;
}
isDead_ = true;
if (onDeath_) {
onDeath_();
}
}
void HealthComponent::setOnDeath(std::function<void()> callback)
{
onDeath_ = std::move(callback);
}
float HealthComponent::getHealth() const
{
return health_;
}
bool HealthComponent::isDead() const
{
return isDead_;
}
}

Logic requires the owning scene in its constructor. Actor-owned components can receive it automatically through Actor::addComponent<T>(args...) when their constructor supports Scene*.

IsUnique = true makes addComponent<HealthComponent>() return the existing health component if one is already attached.

Enemy Actor

Create the component in the actor constructor. Scene::spawnActor<T>() calls the actor constructor, adds the actor to the scene, then calls beginPlay().

#pragma once
#include "Engine/Entity/Actor.h"
#include "Engine/Entity/SpawnContext.h"
namespace AirHockey
{
class HealthComponent;
class Enemy : public Engine::Entities::Actor
{
public:
explicit Enemy(const Engine::Entities::SpawnContext& context);
void applyDamage(float amount);
private:
HealthComponent* healthComponent_ = nullptr;
};
}
An Actor is similar to an Engine::Entities::Entity. An actor is an Entity with a transform.
Definition Actor.h:71
Scene ownership data passed to actor and component constructors during spawning.
#include "AirHockey/Actors/Enemy.h"
#include "AirHockey/Components/HealthComponent.h"
#include "Engine/Entity/Scene.h"
namespace AirHockey
{
Enemy::Enemy(const Engine::Entities::SpawnContext& context)
: Actor(context)
{
healthComponent_ = addComponent<HealthComponent>(100.0f);
healthComponent_->setOnDeath([this]() {
auto* scene = getOwningScene();
if (scene) {
scene->destroyActor(this);
}
});
}
void Enemy::applyDamage(float amount)
{
if (healthComponent_) {
healthComponent_->inflictDamage(amount);
}
}
}

Include the component header in the .cpp file before calling addComponent<HealthComponent>(). A forward declaration is enough in the actor header because the actor only stores a pointer.

The death callback owns the destruction policy. The health component only reports death.

Scene Setup

Spawn the enemy from a scene with the same spawnActor<T>() call used by other actors.

void CombatScene::loadContent()
{
Scene::loadContent();
Engine::Ecs::Transform enemyTransform;
enemyTransform.setPosition({ 0.0f, 0.0f, -4.0f });
auto* enemy = spawnActor<AirHockey::Enemy>(enemyTransform);
enemy->applyDamage(25.0f);
}
Local transform matrix and helpers for translation, rotation, and scale.
Definition EcsData.h:476
void setPosition(const glm::vec3 &newPosition)
Sets local position and preserves rotation and scale.

spawnActor<T>() returns a raw actor pointer owned by the scene. Do not delete it directly. Use Scene::destroyActor() when gameplay should remove it.

Use Scene::spawnActorAttached<T>() when the actor should start as a child of another actor or scene node. The transform passed to spawnActorAttached<T>() is local to the parent. See Actor Attachments for reparenting and transform-rule behavior.

Scene::destroyActor() queues destruction. Scene::flushPendingActorDestruction() later calls Scene::destroyActorImmediate(), which calls endPlay(), removes the actor from the scene, and deletes it. The engine flushes pending actor destruction after logic tick.

Damage Entry Points

Keep damage entry points on the actor when other systems should not access components directly.

auto* health = hitActor->getComponent<AirHockey::HealthComponent>();
if (health) {
health->inflictDamage(10.0f);
}

Direct component access is fine for small prototypes. Prefer an actor method when damage needs armor, hit reactions, score events, or team checks.

if (auto* enemy = dynamic_cast<AirHockey::Enemy*>(hitActor)) {
enemy->applyDamage(10.0f);
}

Lifecycle

Step What Happens
spawnActor<T>() Creates a SceneNode, constructs the actor, stores it in the scene, then calls beginPlay()
Actor constructor Attach components and set callbacks
Actor::beginPlay() Calls beginPlay() on all attached components
Actor::tick() Ticks components with canTick() == true
Scene::destroyActor() Queues the actor for destruction after logic tick
Scene::flushPendingActorDestruction() Calls destroyActorImmediate() for queued actors
Scene::destroyActorImmediate() Calls endPlay(), removes the actor from the scene, then deletes it

Health does not need ticking. Add ticking only for components that update every frame.

Notes

  • Derive custom actors from Engine::Entities::Actor.
  • Derive gameplay-only components from Engine::Components::Logic.
  • Pass owningScene into Logic(owningScene) from the component constructor.
  • Add components in the actor constructor so Actor::beginPlay() initializes them automatically.
  • Do not touch component state after a death callback destroys the owning actor.
  • Keep rendering, physics, and health as separate components unless the data must be owned together.