feat: example entity script
This commit is contained in:
@@ -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
|
||||
@@ -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)`.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user