feat: example entity script
CI / build-and-test (push) Successful in 2m13s
Sync Docs to Gitea Wiki / Sync docs to Gitea wiki (push) Successful in 10s

This commit is contained in:
2026-03-08 21:48:40 +13:00
parent 7a3810805e
commit d6f2cc1ea3
14 changed files with 588 additions and 35 deletions
+12 -16
View File
@@ -12,8 +12,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history
- name: Start SSH agent
uses: webfactory/ssh-agent@v0.9.0
@@ -29,27 +27,25 @@ jobs:
env:
WIKI_REPO: git@gitea.appstack.me:nick/simian.wiki.git
run: |
sudo apt-get update -qq
sudo apt-get install -y rsync
git config --global user.name "Simian CI"
git config --global user.email "ci@simian.local"
# Clone wiki to TEMP dir (not workspace)
TEMP_WIKI=$(mktemp -d)
git clone "$WIKI_REPO" "$TEMP_WIKI"
# Copy docs into wiki clone
rsync -av --delete docs/ "$TEMP_WIKI/"
cd "$TEMP_WIKI"
git checkout main 2>/dev/null || git checkout -b main
git add -A
if ! git diff --staged --quiet; then
# Git-safe clean: delete everything except .git
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
# Copy docs files
cp -r ../docs/* . 2>/dev/null || true
git add -A
if git diff --staged --quiet; then
echo "No wiki changes"
else
git commit -m "Update wiki from docs (CI) [skip ci]"
git push origin main
echo "✅ Pushed to simian.wiki main"
else
echo "No wiki changes"
fi
echo "✅ Synced to simian.wiki main"
fi
+15
View File
@@ -53,6 +53,7 @@ This document outlines the in-app editor UI, its panels, and common workflows.
- Transform: position, scale, rotation (degrees in UI).
- Velocity: linear and angular velocity.
- Sprite: model id, shader id, color, outline.
- Script: per-entity script path and enabled toggle.
- Camera: position, target, up, FOV, projection, helper actions.
- Light: direction, color, intensity, helper presets.
- Material: material id.
@@ -76,3 +77,17 @@ This document outlines the in-app editor UI, its panels, and common workflows.
- Scene Save and Load use file path text fields.
- Component IDs are numeric and map to runtime resource managers.
- World Transform is computed from hierarchy and local transforms and is read-only.
## Scene Scripts
- Scene scripts are loaded by the scene system and run once per scene.
- The editor lets you set a scene-level script path in the Scene panel.
- Preferred scene callbacks: `SceneInit()`, `SceneUpdate(float dt)`, `SceneShutdown()`.
- Legacy callbacks still work: `Init()`, `Update(float dt)`, `Shutdown()`.
## ScriptComponent
- ScriptComponent runs a per-entity script module.
- Set the script path in the Inspector and toggle enabled.
- Preferred callbacks: `EntityInit(uint entity)`, `EntityUpdate(uint entity, float dt)`, `EntityShutdown(uint entity)`.
- Legacy callbacks still work: `Init(uint entity)`, `Update(uint entity, float dt)`, `Shutdown(uint entity)`.
+2 -2
View File
@@ -26,8 +26,8 @@ Size=225,63
Collapsed=0
[Window][##TOAST1]
Pos=1704,835
Size=196,63
Pos=1035,564
Size=225,63
Collapsed=0
[Window][##TOAST2]
+20
View File
@@ -8,6 +8,8 @@
#include "MaterialManager.h"
#include "InputManager.h"
#include <entt.hpp>
#include <string>
#include <unordered_map>
class Application {
public:
@@ -24,6 +26,16 @@ public:
entt::registry& GetRegistry() { return registry; }
private:
struct EntityScriptModule {
std::string path;
std::string moduleName;
asIScriptModule* module = nullptr;
asIScriptFunction* initFunc = nullptr;
asIScriptFunction* updateFunc = nullptr;
asIScriptFunction* shutdownFunc = nullptr;
bool valid = false;
};
ScriptEngine scriptEngine;
HotReload* hotReload;
bool scriptCompilationError;
@@ -41,6 +53,10 @@ private:
ModelManager modelManager;
MaterialManager materialManager;
InputManager inputManager;
std::unordered_map<std::string, EntityScriptModule> entityScriptModules;
std::unordered_map<entt::entity, std::string> entityScriptBindings;
unsigned int entityScriptModuleCounter = 0;
std::string currentSceneScriptPath;
static const int WINDOW_WIDTH = 1280;
static const int WINDOW_HEIGHT = 720;
@@ -50,6 +66,10 @@ private:
void Update(float deltaTime);
void UpdateSystems(float deltaTime);
void UpdateEntityScripts(float deltaTime);
void ShutdownEntityScripts();
void ProcessSceneScriptChanges();
EntityScriptModule* GetEntityScriptModule(const std::string& path);
void RenderScene();
void Draw();
};
+7
View File
@@ -56,6 +56,13 @@ struct Tag {
Tag(const std::string& n) : name(n) {}
};
// ScriptComponent - per-entity script reference
struct ScriptComponent {
std::string scriptPath;
bool enabled = true;
bool started = false;
};
// SceneEntity component - marks entities owned by the scene
struct SceneEntity {
bool persistent = false;
+5
View File
@@ -18,3 +18,8 @@ bool SaveSceneToFile(const std::string& path);
// Clear all entities from the registry
void ClearScene();
// Scene script path helpers
void SetSceneScriptPath(const std::string& path);
const std::string& GetSceneScriptPath();
bool ConsumeSceneScriptPath(std::string& outPath);
+6
View File
@@ -6,6 +6,7 @@
#include "gui/ImGuiNotify.hpp"
#include "LogWindow.h" // Include the new LogWindow class
#include <entt.hpp>
#include <string>
class Application;
@@ -30,6 +31,7 @@ private:
bool IsEntityAncestor(entt::entity ancestor, entt::entity entity) const;
const char* GetEntityLabel(entt::entity entity);
void SyncTagBuffer(entt::entity entity);
void SyncScriptPathBuffer(entt::entity entity);
bool showLogWindow = true;
bool showSceneWindow = true;
@@ -39,6 +41,10 @@ private:
entt::entity selectedEntity = entt::null;
entt::entity tagBufferEntity = entt::null;
char tagBuffer[128] = {0};
entt::entity scriptPathEntity = entt::null;
char scriptPathBuffer[260] = {0};
char sceneScriptPathBuffer[260] = {0};
std::string lastSceneScriptPath;
ImVec2 desiredRenderSize = ImVec2(0.0f, 0.0f);
bool requestSceneNew = false;
bool requestSceneLoad = false;
+2
View File
@@ -12,7 +12,9 @@ public:
bool Initialize();
void Shutdown();
bool CompileScript(const std::string& filename);
bool CompileModule(const std::string& moduleName, const std::string& filename, asIScriptModule** outModule);
void CallScriptFunction(asIScriptFunction* func, float dt = 0.0f);
void CallScriptFunction(asIScriptFunction* func, unsigned int entityId, float dt);
void GarbageCollect();
// Add a path used to resolve #include directives in scripts
+1
View File
@@ -50,4 +50,5 @@ color = 0x4523BAFF
outline_size = 0.000
shader_vs = "shaders/toon.vs"
shader_fs = "shaders/toon.fs"
script = "scripts/ball.as"
+104
View File
@@ -0,0 +1,104 @@
// ScriptComponent example: vertical sine movement
array<uint> trackedEntities;
array<float> baseX;
array<float> baseY;
array<float> baseZ;
array<float> phase;
float amplitude = 0.5f;
float speed = 1.5f;
int FindEntityIndex(uint entity)
{
for (uint i = 0; i < trackedEntities.length(); ++i)
{
if (trackedEntities[i] == entity)
{
return int(i);
}
}
return -1;
}
void TrackEntity(uint entity)
{
if (FindEntityIndex(entity) != -1)
{
return;
}
if (!ECS::HasTransform(entity))
{
ECS::AddTransform(entity, 0.0f, 0.0f, 0.0f);
}
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
ECS::GetPosition(entity, x, y, z);
trackedEntities.insertLast(entity);
baseX.insertLast(x);
baseY.insertLast(y);
baseZ.insertLast(z);
phase.insertLast(0.0f);
}
void UntrackEntity(uint entity)
{
int idx = FindEntityIndex(entity);
if (idx < 0)
{
return;
}
trackedEntities.removeAt(idx);
baseX.removeAt(idx);
baseY.removeAt(idx);
baseZ.removeAt(idx);
phase.removeAt(idx);
}
void EntityInit(uint entity)
{
if (!ECS::IsValid(entity))
{
return;
}
TrackEntity(entity);
}
void EntityUpdate(uint entity, float dt)
{
if (!ECS::IsValid(entity))
{
return;
}
int idx = FindEntityIndex(entity);
if (idx < 0)
{
TrackEntity(entity);
idx = FindEntityIndex(entity);
if (idx < 0)
{
return;
}
}
if (!ECS::HasTransform(entity))
{
return;
}
phase[idx] += dt;
float offset = Math::Sin(phase[idx] * speed + float(entity) * 0.1f) * amplitude;
ECS::SetPosition(entity, baseX[idx], baseY[idx] + offset, baseZ[idx]);
}
void EntityShutdown(uint entity)
{
UntrackEntity(entity);
}
+168 -1
View File
@@ -107,6 +107,7 @@ bool Application::Initialize(int argc, char *argv[])
// Compile initial script
scriptCompilationError = !scriptEngine.CompileScript(SCRIPT_FILE);
currentSceneScriptPath = SCRIPT_FILE;
if (enableEditor)
{
@@ -134,10 +135,12 @@ void Application::Run()
void Application::Update(float deltaTime)
{
ProcessSceneScriptChanges();
// Check for hot reload
if (hotReload && hotReload->CheckForChanges())
{
bool success = scriptEngine.CompileScript(SCRIPT_FILE);
bool success = scriptEngine.CompileScript(currentSceneScriptPath);
scriptCompilationError = !success;
if (!success)
{
@@ -155,6 +158,8 @@ void Application::Update(float deltaTime)
// Call script Update function
scriptEngine.CallScriptFunction(scriptEngine.GetUpdateFunction(), deltaTime);
UpdateEntityScripts(deltaTime);
// Update ECS systems
UpdateSystems(deltaTime);
}
@@ -176,6 +181,167 @@ void Application::UpdateSystems(float deltaTime)
UpdateWorldTransforms(registry);
}
Application::EntityScriptModule* Application::GetEntityScriptModule(const std::string& path)
{
if (path.empty()) {
return nullptr;
}
auto it = entityScriptModules.find(path);
if (it != entityScriptModules.end()) {
return &it->second;
}
EntityScriptModule moduleInfo;
moduleInfo.path = path;
moduleInfo.moduleName = "entity_script_" + std::to_string(entityScriptModuleCounter++);
if (!scriptEngine.CompileModule(moduleInfo.moduleName, path, &moduleInfo.module)) {
log_warn("Failed to compile entity script: %s", path.c_str());
entityScriptModules.emplace(path, moduleInfo);
return &entityScriptModules[path];
}
if (moduleInfo.module) {
moduleInfo.initFunc = moduleInfo.module->GetFunctionByDecl("void EntityInit(uint)");
if (!moduleInfo.initFunc) {
moduleInfo.initFunc = moduleInfo.module->GetFunctionByDecl("void Init(uint)");
}
moduleInfo.updateFunc = moduleInfo.module->GetFunctionByDecl("void EntityUpdate(uint, float)");
if (!moduleInfo.updateFunc) {
moduleInfo.updateFunc = moduleInfo.module->GetFunctionByDecl("void Update(uint, float)");
}
moduleInfo.shutdownFunc = moduleInfo.module->GetFunctionByDecl("void EntityShutdown(uint)");
if (!moduleInfo.shutdownFunc) {
moduleInfo.shutdownFunc = moduleInfo.module->GetFunctionByDecl("void Shutdown(uint)");
}
moduleInfo.valid = true;
}
entityScriptModules.emplace(path, moduleInfo);
return &entityScriptModules[path];
}
void Application::UpdateEntityScripts(float deltaTime)
{
auto view = registry.view<ScriptComponent>();
std::vector<entt::entity> invalidBindings;
for (auto entity : view) {
auto& script = view.get<ScriptComponent>(entity);
if (!script.enabled || script.scriptPath.empty()) {
auto existingBinding = entityScriptBindings.find(entity);
if (script.started && existingBinding != entityScriptBindings.end()) {
auto* moduleInfo = GetEntityScriptModule(existingBinding->second);
if (moduleInfo && moduleInfo->valid && moduleInfo->shutdownFunc) {
scriptEngine.CallScriptFunction(moduleInfo->shutdownFunc, static_cast<unsigned int>(entity), 0.0f);
}
}
script.started = false;
entityScriptBindings.erase(entity);
continue;
}
std::string previousPath;
auto existingBinding = entityScriptBindings.find(entity);
if (existingBinding != entityScriptBindings.end()) {
previousPath = existingBinding->second;
}
if (previousPath != script.scriptPath) {
if (!previousPath.empty()) {
auto* oldModule = GetEntityScriptModule(previousPath);
if (oldModule && oldModule->valid && oldModule->shutdownFunc) {
scriptEngine.CallScriptFunction(oldModule->shutdownFunc, static_cast<unsigned int>(entity), 0.0f);
}
}
entityScriptBindings[entity] = script.scriptPath;
script.started = false;
}
auto* moduleInfo = GetEntityScriptModule(script.scriptPath);
if (!moduleInfo || !moduleInfo->valid) {
continue;
}
if (!script.started) {
if (moduleInfo->initFunc) {
scriptEngine.CallScriptFunction(moduleInfo->initFunc, static_cast<unsigned int>(entity), 0.0f);
}
script.started = true;
}
if (moduleInfo->updateFunc) {
scriptEngine.CallScriptFunction(moduleInfo->updateFunc, static_cast<unsigned int>(entity), deltaTime);
}
}
for (const auto& [entity, path] : entityScriptBindings) {
if (!registry.valid(entity)) {
invalidBindings.push_back(entity);
}
}
for (auto entity : invalidBindings) {
entityScriptBindings.erase(entity);
}
}
void Application::ShutdownEntityScripts()
{
auto view = registry.view<ScriptComponent>();
for (auto entity : view) {
auto& script = view.get<ScriptComponent>(entity);
if (!script.started || script.scriptPath.empty()) {
continue;
}
auto* moduleInfo = GetEntityScriptModule(script.scriptPath);
if (moduleInfo && moduleInfo->valid && moduleInfo->shutdownFunc) {
scriptEngine.CallScriptFunction(moduleInfo->shutdownFunc, static_cast<unsigned int>(entity), 0.0f);
}
script.started = false;
}
}
void Application::ProcessSceneScriptChanges()
{
std::string sceneScriptPath;
if (!ConsumeSceneScriptPath(sceneScriptPath)) {
return;
}
if (sceneScriptPath.empty() || sceneScriptPath == currentSceneScriptPath) {
return;
}
currentSceneScriptPath = sceneScriptPath;
bool success = scriptEngine.CompileScript(currentSceneScriptPath);
scriptCompilationError = !success;
if (!success) {
log_warn("Scene script compilation failed - keeping previous version");
Toast::Warning("Scene script compilation failed - check console for details.");
return;
}
if (hotReload) {
delete hotReload;
hotReload = nullptr;
}
std::filesystem::path p(currentSceneScriptPath);
std::string watchPath = p.parent_path().string();
if (watchPath.empty()) {
watchPath = ".";
}
hotReload = new HotReload(watchPath);
scriptEngine.CallScriptFunction(scriptEngine.GetInitFunction());
}
void Application::RenderScene()
{
// Query for active camera (first one found with Camera3DComponent)
@@ -305,6 +471,7 @@ void Application::Shutdown()
}
ShutdownEntityScripts();
scriptEngine.Shutdown();
if (enableEditor)
{
+89 -1
View File
@@ -17,6 +17,8 @@
static entt::registry* g_sceneRegistry = nullptr;
static ModelManager* g_sceneModelManager = nullptr;
static ShaderManager* g_sceneShaderManager = nullptr;
static std::string g_sceneScriptPath;
static bool g_sceneScriptDirty = false;
void SetSceneContext(entt::registry* registry, ModelManager* modelManager, ShaderManager* shaderManager) {
g_sceneRegistry = registry;
@@ -30,6 +32,26 @@ void ClearScene() {
}
}
void SetSceneScriptPath(const std::string& path) {
if (g_sceneScriptPath != path) {
g_sceneScriptPath = path;
g_sceneScriptDirty = true;
}
}
const std::string& GetSceneScriptPath() {
return g_sceneScriptPath;
}
bool ConsumeSceneScriptPath(std::string& outPath) {
if (!g_sceneScriptDirty) {
return false;
}
outPath = g_sceneScriptPath;
g_sceneScriptDirty = false;
return true;
}
struct SceneEntityDef {
std::string id;
std::string parent;
@@ -53,6 +75,10 @@ struct SceneEntityDef {
std::string shaderFs;
unsigned int shaderId = 0;
bool hasShaderId = false;
bool hasScript = false;
std::string scriptPath;
bool scriptEnabled = true;
};
static std::string Trim(const std::string& value) {
@@ -131,6 +157,24 @@ static bool ParseString(const std::string& value, std::string& out) {
return !out.empty();
}
static bool ParseBool(const std::string& value, bool& out) {
std::string trimmed = Trim(value);
std::string lower;
lower.reserve(trimmed.size());
for (char c : trimmed) {
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
}
if (lower == "true" || lower == "1") {
out = true;
return true;
}
if (lower == "false" || lower == "0") {
out = false;
return true;
}
return false;
}
static std::string EscapeString(const std::string& value) {
std::string out;
out.reserve(value.size());
@@ -251,7 +295,23 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) {
}
if (!current) {
log_warn("Scene parse: key/value outside entity section at line %d", lineNumber);
size_t eq = line.find('=');
if (eq == std::string::npos) {
log_warn("Scene parse: invalid line %d", lineNumber);
continue;
}
std::string key = Trim(line.substr(0, eq));
std::string value = Trim(line.substr(eq + 1));
if (key == "scene_script") {
std::string sceneScript;
if (ParseString(value, sceneScript)) {
SetSceneScriptPath(sceneScript);
}
} else {
log_warn("Scene parse: key/value outside entity section at line %d", lineNumber);
}
continue;
}
@@ -308,6 +368,14 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) {
current->shaderId = static_cast<unsigned int>(std::stoul(value, nullptr, 0));
current->hasShaderId = true;
current->hasSprite = true;
} else if (key == "script") {
if (ParseString(value, current->scriptPath)) {
current->hasScript = true;
}
} else if (key == "script_enabled") {
if (ParseBool(value, current->scriptEnabled)) {
current->hasScript = true;
}
} else {
log_warn("Scene parse: unknown key '%s' at line %d", key.c_str(), lineNumber);
}
@@ -377,6 +445,13 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) {
}
g_sceneRegistry->emplace<Sprite>(entity, static_cast<int>(modelId), col, def.outlineSize, shaderId);
}
if (def.hasScript || !def.scriptPath.empty()) {
auto& script = g_sceneRegistry->emplace<ScriptComponent>(entity);
script.scriptPath = def.scriptPath;
script.enabled = def.scriptEnabled;
script.started = false;
}
}
for (size_t i = 0; i < defs.size(); ++i) {
@@ -412,6 +487,10 @@ bool SaveSceneToFile(const std::string& path) {
return false;
}
if (!g_sceneScriptPath.empty()) {
file << "scene_script = \"" << EscapeString(g_sceneScriptPath) << "\"\n\n";
}
auto& registry = *g_sceneRegistry;
std::vector<entt::entity> entities;
auto& storage = registry.storage<entt::entity>();
@@ -533,6 +612,15 @@ bool SaveSceneToFile(const std::string& path) {
}
}
if (auto* script = registry.try_get<ScriptComponent>(entity)) {
if (!script->scriptPath.empty()) {
file << "script = \"" << EscapeString(script->scriptPath) << "\"\n";
}
if (!script->enabled) {
file << "script_enabled = false\n";
}
}
file << "\n";
}
+76 -11
View File
@@ -328,6 +328,19 @@ void GuiManager::SyncTagBuffer(entt::entity entity)
}
}
void GuiManager::SyncScriptPathBuffer(entt::entity entity)
{
if (!app) return;
if (entity == scriptPathEntity) return;
scriptPathEntity = entity;
scriptPathBuffer[0] = '\0';
auto& registry = app->GetRegistry();
if (auto* script = registry.try_get<ScriptComponent>(entity)) {
std::snprintf(scriptPathBuffer, sizeof(scriptPathBuffer), "%s", script->scriptPath.c_str());
}
}
void GuiManager::RenderEntityNode(entt::entity entity)
{
if (!app) return;
@@ -395,6 +408,24 @@ void GuiManager::RenderSceneWindow()
auto& registry = app->GetRegistry();
const std::string& sceneScriptPath = GetSceneScriptPath();
if (sceneScriptPath != lastSceneScriptPath) {
std::snprintf(sceneScriptPathBuffer, sizeof(sceneScriptPathBuffer), "%s", sceneScriptPath.c_str());
lastSceneScriptPath = sceneScriptPath;
}
ImGui::Text("Scene Script");
ImGui::SameLine();
ImGui::SetNextItemWidth(240.0f);
ImGui::InputText("##SceneScriptPath", sceneScriptPathBuffer, sizeof(sceneScriptPathBuffer));
ImGui::SameLine();
if (ImGui::Button("Apply")) {
SetSceneScriptPath(sceneScriptPathBuffer);
lastSceneScriptPath = sceneScriptPathBuffer;
}
ImGui::Separator();
if (ImGui::Button("Create Entity")) {
entt::entity entity = registry.create();
std::string name = "Entity " + std::to_string(static_cast<unsigned int>(ToId(entity)));
@@ -492,10 +523,11 @@ void GuiManager::RenderInspectorWindow()
{"Transform", 1},
{"Velocity", 2},
{"Sprite", 3},
{"Camera", 4},
{"Light", 5},
{"Material", 6},
{"Render Pass", 7}
{"Script", 4},
{"Camera", 5},
{"Light", 6},
{"Material", 7},
{"Render Pass", 8}
};
auto hasComponent = [&](int id) {
@@ -504,10 +536,11 @@ void GuiManager::RenderInspectorWindow()
case 1: return registry.any_of<ECSTransform>(selectedEntity);
case 2: return registry.any_of<Velocity>(selectedEntity);
case 3: return registry.any_of<Sprite>(selectedEntity);
case 4: return registry.any_of<Camera3DComponent>(selectedEntity);
case 5: return registry.any_of<LightComponent>(selectedEntity);
case 6: return registry.any_of<MaterialComponent>(selectedEntity);
case 7: return registry.any_of<RenderPassComponent>(selectedEntity);
case 4: return registry.any_of<ScriptComponent>(selectedEntity);
case 5: return registry.any_of<Camera3DComponent>(selectedEntity);
case 6: return registry.any_of<LightComponent>(selectedEntity);
case 7: return registry.any_of<MaterialComponent>(selectedEntity);
case 8: return registry.any_of<RenderPassComponent>(selectedEntity);
default: return false;
}
};
@@ -560,15 +593,19 @@ void GuiManager::RenderInspectorWindow()
registry.emplace<Sprite>(selectedEntity, 0, WHITE, 0.0f, 0);
break;
case 4:
registry.emplace<Camera3DComponent>(selectedEntity);
registry.emplace<ScriptComponent>(selectedEntity);
SyncScriptPathBuffer(selectedEntity);
break;
case 5:
registry.emplace<LightComponent>(selectedEntity);
registry.emplace<Camera3DComponent>(selectedEntity);
break;
case 6:
registry.emplace<MaterialComponent>(selectedEntity);
registry.emplace<LightComponent>(selectedEntity);
break;
case 7:
registry.emplace<MaterialComponent>(selectedEntity);
break;
case 8:
registry.emplace<RenderPassComponent>(selectedEntity);
break;
default:
@@ -705,6 +742,34 @@ void GuiManager::RenderInspectorWindow()
}
}
// Script
if (registry.any_of<ScriptComponent>(selectedEntity)) {
auto& script = registry.get<ScriptComponent>(selectedEntity);
ImGui::Separator();
ImGui::Text("Script");
ImGui::PushID("ScriptComponent");
SyncScriptPathBuffer(selectedEntity);
if (ImGui::InputText("Path", scriptPathBuffer, sizeof(scriptPathBuffer))) {
script.scriptPath = scriptPathBuffer;
script.started = false;
}
if (ImGui::Checkbox("Enabled", &script.enabled)) {
if (!script.enabled) {
script.started = false;
}
}
if (ImGui::Button("Remove Script")) {
registry.remove<ScriptComponent>(selectedEntity);
scriptPathEntity = entt::null;
scriptPathBuffer[0] = '\0';
}
ImGui::PopID();
}
// Camera
if (registry.any_of<Camera3DComponent>(selectedEntity)) {
ImGui::PushID("Camera");
+81 -4
View File
@@ -165,15 +165,62 @@ bool ScriptEngine::CompileScript(const std::string& filename) {
// Get the newly created module
currentModule = engine->GetModule("main");
// Cache new functions
initFunc = currentModule->GetFunctionByName("Init");
updateFunc = currentModule->GetFunctionByName("Update");
shutdownFunc = currentModule->GetFunctionByName("Shutdown");
// Cache new functions (scene-specific names preferred)
initFunc = currentModule->GetFunctionByDecl("void SceneInit()");
if (!initFunc) {
initFunc = currentModule->GetFunctionByDecl("void Init()");
}
updateFunc = currentModule->GetFunctionByDecl("void SceneUpdate(float)");
if (!updateFunc) {
updateFunc = currentModule->GetFunctionByDecl("void Update(float)");
}
shutdownFunc = currentModule->GetFunctionByDecl("void SceneShutdown()");
if (!shutdownFunc) {
shutdownFunc = currentModule->GetFunctionByDecl("void Shutdown()");
}
hasValidScript = true;
log_info("Script compiled and cached: %s", filename.c_str());
return true;
}
bool ScriptEngine::CompileModule(const std::string& moduleName, const std::string& filename, asIScriptModule** outModule) {
if (!engine) return false;
if (engine->GetModule(moduleName.c_str())) {
engine->DiscardModule(moduleName.c_str());
}
CScriptBuilder builder;
builder.SetIncludeCallback(IncludeCallback, nullptr);
int r = builder.StartNewModule(engine, moduleName.c_str());
if (r < 0) {
log_error("Failed to start module: %s", moduleName.c_str());
return false;
}
r = builder.AddSectionFromFile(filename.c_str());
if (r < 0) {
log_error("Failed to add script section from file: %s", filename.c_str());
engine->DiscardModule(moduleName.c_str());
return false;
}
r = builder.BuildModule();
if (r < 0) {
log_error("Failed to build module: %s", moduleName.c_str());
engine->DiscardModule(moduleName.c_str());
return false;
}
if (outModule) {
*outModule = engine->GetModule(moduleName.c_str());
}
return true;
}
void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) {
if (!func || !engine || !hasValidScript) return;
@@ -200,6 +247,36 @@ void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) {
ctx->Release();
}
void ScriptEngine::CallScriptFunction(asIScriptFunction* func, unsigned int entityId, float dt) {
if (!func || !engine) return;
asIScriptContext* ctx = engine->CreateContext();
if (!ctx) return;
int r = ctx->Prepare(func);
if (r < 0) {
ctx->Release();
return;
}
const asUINT paramCount = func->GetParamCount();
if (paramCount >= 1) {
ctx->SetArgDWord(0, entityId);
}
if (paramCount >= 2) {
ctx->SetArgFloat(1, dt);
}
r = ctx->Execute();
if (r != asEXECUTION_FINISHED) {
if (r == asEXECUTION_EXCEPTION) {
log_error("Script exception: %s", ctx->GetExceptionString());
}
}
ctx->Release();
}
void ScriptEngine::ClearCachedFunctions() {
updateFunc = nullptr;
hasValidScript = false;