Complete Examples

Player Controller

WASD movement with physics, jumping, and animation switching:

#include "Core/Behavior.h"
#include "Core/Components.h"
#include "Core/Engine.h"
#include <GLFW/glfw3.h>

namespace SafiEngine {

class PlayerBehavior : public Behavior {
public:
  void OnStart(flecs::entity e, Engine &engine) override {
    if (auto *anim = e.get_mut<Animator>()) {
      anim->clipIndex = m_IdleClip;
    }
  }

  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    auto &input = engine.GetInput();
    float speed = 6.0f;
    float vx = 0.0f, vz = 0.0f;

    if (input.IsKeyDown(GLFW_KEY_W)) vz -= speed;
    if (input.IsKeyDown(GLFW_KEY_S)) vz += speed;
    if (input.IsKeyDown(GLFW_KEY_A)) vx -= speed;
    if (input.IsKeyDown(GLFW_KEY_D)) vx += speed;

    bool moving = (vx != 0.0f || vz != 0.0f);

    // Animation
    if (auto *anim = e.get_mut<Animator>()) {
      int desired = moving ? m_WalkClip : m_IdleClip;
      if (anim->clipIndex != desired) {
        anim->clipIndex = desired;
        anim->time = 0.0f;
        anim->playing = true;
      }
    }

    // Physics-based movement (preserves gravity)
    engine.GetPhysicsWorld().SetHorizontalVelocity(e.id(), vx, vz);

    // Jump
    if (input.IsKeyPressed(GLFW_KEY_SPACE)) {
      engine.GetPhysicsWorld().SetLinearVelocity(e.id(), vx, 8.0f, vz);
    }
  }

private:
  int m_IdleClip = 0;
  int m_WalkClip = 1;
};

void RegisterPlayerBehavior() {
  BehaviorRegistry::Register(
      "Player", [] { return std::make_shared<PlayerBehavior>(); });
}

} // namespace SafiEngine

Follow Camera

Configurable camera that follows any named entity using CameraUtils:

#include "Core/Behavior.h"
#include "Core/Components.h"
#include "Core/Engine.h"
#include <cstdlib>
#include <glm/glm.hpp>

namespace SafiEngine {

class FollowCameraBehavior : public Behavior {
public:
  void OnStart(flecs::entity e, Engine &engine) override {
    const auto *ns = e.get<NativeScript>();
    if (!ns) return;

    auto it = ns->properties.find("target");
    if (it != ns->properties.end() && !it->second.empty())
      m_TargetName = it->second;

    it = ns->properties.find("offsetX");
    if (it != ns->properties.end()) m_Offset.x = std::atof(it->second.c_str());
    it = ns->properties.find("offsetY");
    if (it != ns->properties.end()) m_Offset.y = std::atof(it->second.c_str());
    it = ns->properties.find("offsetZ");
    if (it != ns->properties.end()) m_Offset.z = std::atof(it->second.c_str());
    it = ns->properties.find("smoothing");
    if (it != ns->properties.end()) m_Smoothing = std::atof(it->second.c_str());
  }

  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    auto target = engine.GetWorld().lookup(m_TargetName.c_str());
    if (!target.is_valid() || !target.has<Transform>()) return;

    auto *cam = e.get_mut<Transform>();
    if (!cam) return;
    const auto *tgt = target.get<Transform>();
    auto &utils = engine.GetCameraUtils();

    glm::vec3 desired = tgt->position + m_Offset;
    utils.SmoothFollow(*cam, desired, m_Smoothing, dt);
    utils.SmoothLookAt(*cam, tgt->position, m_Smoothing * 2.0f, dt);
  }

private:
  std::string m_TargetName = "Player";
  glm::vec3 m_Offset{0.0f, 5.0f, 8.0f};
  float m_Smoothing = 5.0f;
};

void RegisterFollowCameraBehavior() {
  BehaviorRegistry::Register(
      "FollowCamera", [] { return std::make_shared<FollowCameraBehavior>(); });
}

} // namespace SafiEngine

Properties (set in Inspector):

  • target - entity name to follow (default: "Player")
  • offsetX, offsetY, offsetZ - camera offset
  • smoothing - follow speed

Rotating Object

Spin an entity continuously:

class SpinBehavior : public Behavior {
public:
  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    auto *t = e.get_mut<Transform>();
    if (!t) return;
    t->rotation.y += 90.0f * dt; // 90 degrees per second
  }
};

Light Flicker

Make a point light flicker like a candle:

class FlickerBehavior : public Behavior {
public:
  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    auto *pl = e.get_mut<PointLight>();
    if (!pl) return;

    m_Timer += dt;
    float noise = std::sin(m_Timer * 15.0f) * 0.3f
                + std::sin(m_Timer * 7.3f) * 0.2f;
    pl->intensity = m_BaseIntensity + noise;
  }

private:
  float m_Timer = 0.0f;
  float m_BaseIntensity = 1.5f;
};

Cinematic Camera Switcher

Switch between cameras on a timer:

class CinematicDirector : public Behavior {
public:
  void OnStart(flecs::entity e, Engine &engine) override {
    const auto *ns = e.get<NativeScript>();
    if (ns) {
      auto it = ns->properties.find("cam1");
      if (it != ns->properties.end()) m_CamNames[0] = it->second;
      it = ns->properties.find("cam2");
      if (it != ns->properties.end()) m_CamNames[1] = it->second;
      it = ns->properties.find("cam3");
      if (it != ns->properties.end()) m_CamNames[2] = it->second;
      it = ns->properties.find("switchTime");
      if (it != ns->properties.end()) m_SwitchTime = std::atof(it->second.c_str());
    }

    SwitchToCamera(engine, 0);
  }

  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    m_Timer += dt;
    if (m_Timer > m_SwitchTime) {
      m_Timer = 0.0f;
      m_CurrentCam = (m_CurrentCam + 1) % 3;
      SwitchToCamera(engine, m_CurrentCam);
    }
  }

private:
  void SwitchToCamera(Engine &engine, int index) {
    if (m_CamNames[index].empty()) return;
    auto cam = engine.GetWorld().lookup(m_CamNames[index].c_str());
    if (cam.is_valid()) engine.SetMainCamera(cam);
  }

  std::string m_CamNames[3] = {"CineCam1", "CineCam2", "CineCam3"};
  float m_SwitchTime = 5.0f;
  float m_Timer = 0.0f;
  int m_CurrentCam = 0;
};

Properties: cam1, cam2, cam3 (camera entity names), switchTime (seconds between switches).