diff --git a/CMakeLists.txt b/CMakeLists.txt index 0493acb..0fa76cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,9 +28,16 @@ target_include_directories(scriptstdstring # ------------------------- # 4️⃣ Main executable # ------------------------- -add_executable(simian main.cpp) +add_executable(simian + main.cpp + src/Application.cpp + src/ScriptEngine.cpp + src/ScriptBindings.cpp + src/HotReload.cpp +) target_include_directories(simian PUBLIC + include external/raylib/src external/angelscript/sdk/angelscript/include external/angelscript/sdk/add_on/scriptstdstring diff --git a/README.md b/README.md index c4b474c..e7d6fca 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,36 @@ **Simian** is a Raylib + AngelScript test project on Windows and Linux. It demonstrates how to integrate **Raylib** for graphics and **AngelScript** for scripting, including the `scriptstdstring` add-on. +## Refactored Architecture + +The project has been refactored from a single `main.cpp` file into a more maintainable modular structure: + +### Core Components +- **Application** (`Application.h/cpp`) - Main application lifecycle and game loop +- **ScriptEngine** (`ScriptEngine.h/cpp`) - AngelScript engine management and script compilation +- **ScriptBindings** (`ScriptBindings.h/cpp`) - C++ to AngelScript function bindings +- **HotReload** (`HotReload.h/cpp`) - File monitoring for automatic script reloading +- **main.cpp** - Minimal entry point + +### Directory Structure +``` +Simian/ +├─ include/ # Header files +│ ├─ Application.h +│ ├─ ScriptEngine.h +│ ├─ ScriptBindings.h +│ └─ HotReload.h +├─ src/ # Source files +│ ├─ Application.cpp +│ ├─ ScriptEngine.cpp +│ ├─ ScriptBindings.cpp +│ └─ HotReload.cpp +├─ external/ # Dependencies +├─ scripts/ # AngelScript files +├─ main.cpp # Entry point +└─ CMakeLists.txt # Build configuration +``` + --- ## Requirements diff --git a/include/Application.h b/include/Application.h new file mode 100644 index 0000000..d524cdd --- /dev/null +++ b/include/Application.h @@ -0,0 +1,26 @@ +#pragma once +#include "ScriptEngine.h" +#include "HotReload.h" + +class Application { +public: + Application(); + ~Application(); + + bool Initialize(); + void Run(); + void Shutdown(); + +private: + ScriptEngine scriptEngine; + HotReload* hotReload; + + static const int WINDOW_WIDTH = 800; + static const int WINDOW_HEIGHT = 600; + static const int TARGET_FPS = 60; + static const char* WINDOW_TITLE; + static const char* SCRIPT_FILE; + + void Update(float deltaTime); + void Draw(); +}; \ No newline at end of file diff --git a/include/HotReload.h b/include/HotReload.h new file mode 100644 index 0000000..e8a15bb --- /dev/null +++ b/include/HotReload.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include +#include + +class HotReload { +public: + HotReload(const std::string& filename); + + bool CheckForChanges(); + void UpdateLastWriteTime(); + +private: + std::string scriptFile; + std::time_t lastWriteTime; + + std::time_t GetFileWriteTime(const std::string& filename); +}; \ No newline at end of file diff --git a/include/ScriptBindings.h b/include/ScriptBindings.h new file mode 100644 index 0000000..7a96483 --- /dev/null +++ b/include/ScriptBindings.h @@ -0,0 +1,12 @@ +#pragma once +#include "angelscript.h" +#include + +class ScriptBindings { +public: + static void RegisterAll(asIScriptEngine* engine); + +private: + static void Print(const std::string &msg); + static void AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color); +}; \ No newline at end of file diff --git a/include/ScriptEngine.h b/include/ScriptEngine.h new file mode 100644 index 0000000..f294d44 --- /dev/null +++ b/include/ScriptEngine.h @@ -0,0 +1,27 @@ +#pragma once +#include "angelscript.h" +#include + +class ScriptEngine { +public: + ScriptEngine(); + ~ScriptEngine(); + + bool Initialize(); + void Shutdown(); + bool CompileScript(const std::string& filename); + void CallScriptFunction(asIScriptFunction* func, float dt = 0.0f); + void GarbageCollect(); + + asIScriptEngine* GetEngine() const { return engine; } + asIScriptFunction* GetUpdateFunction() const { return updateFunc; } + asIScriptFunction* GetDrawFunction() const { return drawFunc; } + +private: + asIScriptEngine* engine; + asIScriptFunction* updateFunc; + asIScriptFunction* drawFunc; + + static void MessageCallback(const asSMessageInfo* msg, void* param); + std::string ReadFile(const std::string& filename); +}; \ No newline at end of file diff --git a/main.cpp b/main.cpp index 7ee0db7..ba8ba92 100644 --- a/main.cpp +++ b/main.cpp @@ -1,162 +1,16 @@ +#include "Application.h" #include -#include -#include -#include -#include "raylib.h" -#include "angelscript.h" -#include "scriptstdstring.h" -#include -#include -#include -namespace fs = std::filesystem; -asIScriptEngine* engine = nullptr; -std::string scriptFile = "scripts/test.as"; -std::time_t lastWriteTime = 0; - -// Cached script functions -asIScriptFunction* updateFunc = nullptr; -asIScriptFunction* drawFunc = nullptr; - -// ------------------------- -// Functions exposed to AngelScript -// ------------------------- -void Print(const std::string &msg) { - std::cout << "[Script] " << msg << std::endl; -} - -Color ColorFromUInt(unsigned int c) { - Color col; - col.r = (c >> 24) & 0xFF; - col.g = (c >> 16) & 0xFF; - col.b = (c >> 8) & 0xFF; - col.a = c & 0xFF; - return col; -} - -void AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color) { - DrawText(text.c_str(), x, y, fontSize, ColorFromUInt(color)); -} - -// ------------------------- -// AngelScript message callback -// ------------------------- -void AngelScriptMessageCallback(const asSMessageInfo* msg, void* param) { - std::cout << msg->section << " (" << msg->row << "): " << msg->message << std::endl; -} - -// ------------------------- -// Utility to read script files -// ------------------------- -std::string ReadFile(const std::string &filename) { - std::ifstream file(filename); - if (!file) return ""; - std::stringstream ss; - ss << file.rdbuf(); - return ss.str(); -} - -// ------------------------- -// Compile script and cache Update/Draw -// ------------------------- -bool compileScript(const std::string& filename) { - engine->GarbageCollect(); - asIScriptModule* mod = engine->GetModule("main", asGM_ALWAYS_CREATE); - - std::string code = ReadFile(filename); - if (code.empty()) { - std::cerr << "Failed to read script file: " << filename << "\n"; - return false; - } - - int r = mod->AddScriptSection(filename.c_str(), code.c_str()); - if (r < 0) return false; - - r = mod->Build(); - if (r < 0) return false; - - // Cache Update(float dt) and Draw() functions if they exist - updateFunc = mod->GetFunctionByName("Update"); - drawFunc = mod->GetFunctionByName("Draw"); - - std::cout << "Script compiled and cached: " << filename << "\n"; - return true; -} - -// ------------------------- -// Hot reload check -// ------------------------- -void checkHotReload() { - auto ftime = fs::last_write_time(scriptFile); - auto sctp = std::chrono::time_point_cast(ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); - std::time_t t = std::chrono::system_clock::to_time_t(sctp); - if (t != lastWriteTime) { - lastWriteTime = t; - compileScript(scriptFile); - } -} - -// ------------------------- -// Call a cached script function -// ------------------------- -void callScriptFunction(asIScriptFunction* func, float dt = 0.0f) { - if (!func) return; - - asIScriptContext* ctx = engine->CreateContext(); - ctx->Prepare(func); - - if (func->GetParamCount() == 1) { - ctx->SetArgFloat(0, dt); - } - - ctx->Execute(); - ctx->Release(); -} - -// ------------------------- -// Main -// ------------------------- int main() { - // Initialize Raylib - InitWindow(800, 600, "Raylib + AngelScript"); - SetTargetFPS(60); - - // Initialize AngelScript - engine = asCreateScriptEngine(); - assert(engine); - - RegisterStdString(engine); - engine->RegisterGlobalFunction("void DrawText(const string &in, int, int, int, uint)", - asFUNCTION(AS_DrawText), asCALL_CDECL); - - int r = engine->SetMessageCallback(asFUNCTION(AngelScriptMessageCallback), nullptr, asCALL_CDECL); - assert(r >= 0); - - r = engine->RegisterGlobalFunction("void Print(const string &in)", asFUNCTION(Print), asCALL_CDECL); - assert(r >= 0); - - compileScript(scriptFile); - - // Main loop - while (!WindowShouldClose()) { - float dt = GetFrameTime(); - - checkHotReload(); - - callScriptFunction(updateFunc, dt); - - BeginDrawing(); - ClearBackground(RAYWHITE); - - DrawText("Modify scripts/test.as to hot-reload!", 50, 50, 20, DARKGRAY); - - callScriptFunction(drawFunc); - - EndDrawing(); - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Application app; + + if (!app.Initialize()) { + std::cerr << "Failed to initialize application\n"; + return -1; } - - CloseWindow(); - engine->ShutDownAndRelease(); + + app.Run(); + app.Shutdown(); + + return 0; } diff --git a/scripts/test.as b/scripts/test.as index 077151a..c3b7c19 100644 --- a/scripts/test.as +++ b/scripts/test.as @@ -8,4 +8,5 @@ void Update(float dt) { void Draw() { DrawText("Hello from AngelScript!", int(x), int(y), 20, 0xFF0000FF); -} + +} \ No newline at end of file diff --git a/src/Application.cpp b/src/Application.cpp new file mode 100644 index 0000000..4816c42 --- /dev/null +++ b/src/Application.cpp @@ -0,0 +1,79 @@ +#include "Application.h" +#include "raylib.h" +#include +#include +#include + +const char* Application::WINDOW_TITLE = "Raylib + AngelScript"; +const char* Application::SCRIPT_FILE = "scripts/test.as"; + +Application::Application() : hotReload(nullptr) { +} + +Application::~Application() { + Shutdown(); +} + +bool Application::Initialize() { + // Initialize Raylib + InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); + SetTargetFPS(TARGET_FPS); + + // Initialize AngelScript + if (!scriptEngine.Initialize()) { + std::cerr << "Failed to initialize script engine\n"; + return false; + } + + // Initialize hot reload + hotReload = new HotReload(SCRIPT_FILE); + + // Compile initial script + scriptEngine.CompileScript(SCRIPT_FILE); + + return true; +} + +void Application::Run() { + while (!WindowShouldClose()) { + float deltaTime = GetFrameTime(); + + Update(deltaTime); + Draw(); + + // Small sleep to prevent excessive CPU usage + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} + +void Application::Update(float deltaTime) { + // Check for hot reload + if (hotReload && hotReload->CheckForChanges()) { + scriptEngine.CompileScript(SCRIPT_FILE); + } + + // Call script Update function + scriptEngine.CallScriptFunction(scriptEngine.GetUpdateFunction(), deltaTime); +} + +void Application::Draw() { + BeginDrawing(); + ClearBackground(RAYWHITE); + + DrawText("Modify scripts/test.as to hot-reload!", 50, 50, 20, DARKGRAY); + + // Call script Draw function + scriptEngine.CallScriptFunction(scriptEngine.GetDrawFunction()); + + EndDrawing(); +} + +void Application::Shutdown() { + if (hotReload) { + delete hotReload; + hotReload = nullptr; + } + + scriptEngine.Shutdown(); + CloseWindow(); +} \ No newline at end of file diff --git a/src/HotReload.cpp b/src/HotReload.cpp new file mode 100644 index 0000000..90ef84c --- /dev/null +++ b/src/HotReload.cpp @@ -0,0 +1,32 @@ +#include "HotReload.h" +#include + +namespace fs = std::filesystem; + +HotReload::HotReload(const std::string& filename) : scriptFile(filename), lastWriteTime(0) { + UpdateLastWriteTime(); +} + +bool HotReload::CheckForChanges() { + std::time_t currentWriteTime = GetFileWriteTime(scriptFile); + if (currentWriteTime != lastWriteTime) { + lastWriteTime = currentWriteTime; + return true; + } + return false; +} + +void HotReload::UpdateLastWriteTime() { + lastWriteTime = GetFileWriteTime(scriptFile); +} + +std::time_t HotReload::GetFileWriteTime(const std::string& filename) { + try { + auto ftime = fs::last_write_time(filename); + auto sctp = std::chrono::time_point_cast( + ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::system_clock::to_time_t(sctp); + } catch (const std::exception&) { + return 0; + } +} \ No newline at end of file diff --git a/src/ScriptBindings.cpp b/src/ScriptBindings.cpp new file mode 100644 index 0000000..d001629 --- /dev/null +++ b/src/ScriptBindings.cpp @@ -0,0 +1,33 @@ +#include "ScriptBindings.h" +#include "raylib.h" +#include +#include + +void ScriptBindings::RegisterAll(asIScriptEngine* engine) { + // Register Print function + int r = engine->RegisterGlobalFunction("void Print(const string &in)", + asFUNCTION(Print), asCALL_CDECL); + assert(r >= 0); + + // Register DrawText function + r = engine->RegisterGlobalFunction("void DrawText(const string &in, int, int, int, uint)", + asFUNCTION(AS_DrawText), asCALL_CDECL); + assert(r >= 0); +} + +void ScriptBindings::Print(const std::string &msg) { + std::cout << "[Script] " << msg << std::endl; +} + +Color ColorFromUInt(unsigned int c) { + Color col; + col.r = (c >> 24) & 0xFF; + col.g = (c >> 16) & 0xFF; + col.b = (c >> 8) & 0xFF; + col.a = c & 0xFF; + return col; +} + +void ScriptBindings::AS_DrawText(const std::string &text, int x, int y, int fontSize, unsigned int color) { + DrawText(text.c_str(), x, y, fontSize, ColorFromUInt(color)); +} \ No newline at end of file diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp new file mode 100644 index 0000000..224add8 --- /dev/null +++ b/src/ScriptEngine.cpp @@ -0,0 +1,104 @@ +#include "ScriptEngine.h" +#include "ScriptBindings.h" +#include "scriptstdstring.h" +#include +#include +#include +#include + +ScriptEngine::ScriptEngine() : engine(nullptr), updateFunc(nullptr), drawFunc(nullptr) { +} + +ScriptEngine::~ScriptEngine() { + Shutdown(); +} + +bool ScriptEngine::Initialize() { + engine = asCreateScriptEngine(); + if (!engine) { + std::cerr << "Failed to create AngelScript engine\n"; + return false; + } + + // Register std::string + RegisterStdString(engine); + + // Register script bindings + ScriptBindings::RegisterAll(engine); + + // Set message callback + int r = engine->SetMessageCallback(asFUNCTION(MessageCallback), nullptr, asCALL_CDECL); + if (r < 0) { + std::cerr << "Failed to set message callback\n"; + return false; + } + + return true; +} + +void ScriptEngine::Shutdown() { + if (engine) { + engine->ShutDownAndRelease(); + engine = nullptr; + } + updateFunc = nullptr; + drawFunc = nullptr; +} + +bool ScriptEngine::CompileScript(const std::string& filename) { + if (!engine) return false; + + engine->GarbageCollect(); + asIScriptModule* mod = engine->GetModule("main", asGM_ALWAYS_CREATE); + + std::string code = ReadFile(filename); + if (code.empty()) { + std::cerr << "Failed to read script file: " << filename << "\n"; + return false; + } + + int r = mod->AddScriptSection(filename.c_str(), code.c_str()); + if (r < 0) return false; + + r = mod->Build(); + if (r < 0) return false; + + // Cache Update(float dt) and Draw() functions if they exist + updateFunc = mod->GetFunctionByName("Update"); + drawFunc = mod->GetFunctionByName("Draw"); + + std::cout << "Script compiled and cached: " << filename << "\n"; + return true; +} + +void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) { + if (!func || !engine) return; + + asIScriptContext* ctx = engine->CreateContext(); + ctx->Prepare(func); + + if (func->GetParamCount() == 1) { + ctx->SetArgFloat(0, dt); + } + + ctx->Execute(); + ctx->Release(); +} + +void ScriptEngine::GarbageCollect() { + if (engine) { + engine->GarbageCollect(); + } +} + +void ScriptEngine::MessageCallback(const asSMessageInfo* msg, void*) { + std::cout << msg->section << " (" << msg->row << "): " << msg->message << std::endl; +} + +std::string ScriptEngine::ReadFile(const std::string& filename) { + std::ifstream file(filename); + if (!file) return ""; + std::stringstream ss; + ss << file.rdbuf(); + return ss.str(); +} \ No newline at end of file