Native Scripting

Overview

SafiEngine uses a native C++ behavior system for gameplay logic. Instead of external scripting languages, you write behaviors as C++ classes that extend the Behavior base class. Behaviors have full access to the engine, input, physics, audio, and all ECS components.

For comprehensive scripting tutorials, see the Scripting Guide.

Architecture

ConceptDescription
BehaviorBase class with virtual OnStart and OnUpdate methods
NativeScriptECS component that stores a behavior name, properties, and runtime instance
BehaviorRegistryGlobal registry mapping behavior names to factory functions

Creating a Behavior

  1. Create a .cpp file in Core/src/behaviors/
  2. Define a class extending Behavior
  3. Export a registration function
  4. Add it to BehaviorRegistry::RegisterAll() in Behavior.cpp
  5. Add the file to Core/CMakeLists.txt

Example: Player Controller

#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 {
    // Called once when play mode starts
  }

  void OnUpdate(flecs::entity e, Engine &engine, float dt) override {
    auto &input = engine.GetInput();
    float speed = 4.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;

    engine.GetPhysicsWorld().SetLinearVelocity(e.id(), vx, 0.0f, vz);
  }
};

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

} // namespace SafiEngine

Example: Follow Camera (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;
  }

  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>();
    const auto *tgt = target.get<Transform>();
    auto &utils = engine.GetCameraUtils();

    glm::vec3 desired = tgt->position + glm::vec3(0.0f, 5.0f, 8.0f);
    utils.SmoothFollow(*cam, desired, 5.0f, dt);
    utils.SmoothLookAt(*cam, tgt->position, 10.0f, dt);
  }

private:
  std::string m_TargetName = "Player";
};

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

} // namespace SafiEngine

Attaching to an Entity

  1. Select an entity in the Scene Hierarchy
  2. Click Add Component > NativeScript
  3. Select a behavior from the Behavior dropdown
  4. Press Play to test

The behavior's OnStart is called once when play mode begins. OnUpdate is called every frame with the delta time.

Script Properties

Behaviors can read configurable properties set in the Inspector. Properties are string key-value pairs stored on the NativeScript component:

  • Type a property name and click + Add in the Inspector
  • Read them in OnStart via e.get<NativeScript>()->properties
  • Remove a property by right-clicking it

Properties are saved with the scene and allow you to configure behavior parameters without recompiling.

See Script Properties for details.

Available Engine APIs

Inside OnUpdate, you have full access to:

APIAccess
Input (keyboard, mouse)engine.GetInput()
Physics (velocity, queries)engine.GetPhysicsWorld()
Audioengine.GetAudioEngine()
ECS Worldengine.GetWorld()
Asset Managerengine.GetAssetManager()
Debug Drawengine.GetDebugDraw()
Event Busengine.GetEventBus()
Camera (main)engine.GetMainCamera() / engine.SetMainCamera(entity)
Delta Timedt parameter or engine.GetDeltaTime()

You can also read and modify any ECS component on the entity:

auto *transform = e.get_mut<Transform>();
auto *animator = e.get_mut<Animator>();
auto *camera = e.get_mut<Camera>();

Editor Integration

The New Behavior button in the NativeScript inspector creates a template .cpp file in your project's assets/scripts/ directory. After writing the behavior logic, add the file to CMake and rebuild.

Integration Notes

  • Behaviors are instantiated when play mode starts and destroyed when it stops
  • Scene snapshot/restore handles behaviorName and properties; the runtime instance is recreated fresh each play session
  • Each entity gets its own behavior instance -- state is per-entity
  • New behaviors require a rebuild since they are compiled C++