Vulkan Schnee 0.0.1
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...)
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 <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,...
A scene is the overarching structure which can spawn, contain and destroy actors or entities.
Definition Scene.h:56
#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. Pass the scene from the actor when adding the component.

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
namespace AirHockey
{
class HealthComponent;
class Enemy : public Engine::Entities::Actor
{
public:
Enemy(
const std::shared_ptr<Engine::Entities::SceneNode>& node,
);
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:20
#include "AirHockey/Actors/Enemy.h"
#include "AirHockey/Components/HealthComponent.h"
namespace AirHockey
{
Enemy::Enemy(
const std::shared_ptr<Engine::Entities::SceneNode>& node,
) : Actor(node, owningScene)
{
healthComponent_ = addComponent<HealthComponent>(owningScene, 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::LocalTransform enemyTransform;
enemyTransform.setPosition({ 0.0f, 0.0f, -4.0f });
auto* enemy = spawnActor<AirHockey::Enemy>(enemyTransform);
enemy->applyDamage(25.0f);
}

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

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() 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.