chore/refactor #2

Merged
nick merged 2 commits from chore/refactor into main 2025-11-05 10:37:06 +00:00
12 changed files with 447 additions and 161 deletions

View File

@@ -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

View File

@@ -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

27
include/Application.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include "ScriptEngine.h"
#include "HotReload.h"
class Application {
public:
Application();
~Application();
bool Initialize();
void Run();
void Shutdown();
private:
ScriptEngine scriptEngine;
HotReload* hotReload;
bool scriptCompilationError;
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();
};

18
include/HotReload.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <chrono>
#include <filesystem>
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);
};

12
include/ScriptBindings.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include "angelscript.h"
#include <string>
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);
};

30
include/ScriptEngine.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include "angelscript.h"
#include <string>
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;
asIScriptModule* currentModule;
bool hasValidScript;
static void MessageCallback(const asSMessageInfo* msg, void* param);
std::string ReadFile(const std::string& filename);
void ClearCachedFunctions();
};

168
main.cpp
View File

@@ -1,162 +1,16 @@
#include "Application.h"
#include <iostream>
#include <filesystem>
#include <chrono>
#include <thread>
#include "raylib.h"
#include "angelscript.h"
#include "scriptstdstring.h"
#include <fstream>
#include <sstream>
#include <assert.h>
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<std::chrono::system_clock::duration>(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;
}

View File

@@ -2,10 +2,10 @@ float x = 50;
float y = 100;
void Update(float dt) {
x += 50 * dt; // move text horizontally
x += 50 * dt;
if (x > 800) x = 0;
}
void Draw() {
DrawText("Hello from AngelScript!", int(x), int(y), 20, 0xFF0000FF);
}
DrawText("Hello from AngelScript - Working perfectly!", int(x), int(y), 20, 0xFF0000FF);
}

88
src/Application.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "Application.h"
#include "raylib.h"
#include <iostream>
#include <chrono>
#include <thread>
const char* Application::WINDOW_TITLE = "Raylib + AngelScript";
const char* Application::SCRIPT_FILE = "scripts/test.as";
Application::Application() : hotReload(nullptr), scriptCompilationError(false) {
}
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
scriptCompilationError = !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()) {
bool success = scriptEngine.CompileScript(SCRIPT_FILE);
scriptCompilationError = !success;
if (!success) {
std::cout << "Script compilation failed - keeping previous version\n";
}
}
// Call script Update function
scriptEngine.CallScriptFunction(scriptEngine.GetUpdateFunction(), deltaTime);
}
void Application::Draw() {
BeginDrawing();
ClearBackground(RAYWHITE);
// Show script error status in top left if there's an error, otherwise show normal message
if (scriptCompilationError) {
DrawText("SCRIPT ERROR - Check console for details", 10, 10, 16, RED);
} else {
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();
}

32
src/HotReload.cpp Normal file
View File

@@ -0,0 +1,32 @@
#include "HotReload.h"
#include <iostream>
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<std::chrono::system_clock::duration>(
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;
}
}

33
src/ScriptBindings.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "ScriptBindings.h"
#include "raylib.h"
#include <iostream>
#include <assert.h>
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));
}

155
src/ScriptEngine.cpp Normal file
View File

@@ -0,0 +1,155 @@
#include "ScriptEngine.h"
#include "ScriptBindings.h"
#include "scriptstdstring.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <assert.h>
ScriptEngine::ScriptEngine() : engine(nullptr), updateFunc(nullptr), drawFunc(nullptr), currentModule(nullptr), hasValidScript(false) {
}
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() {
ClearCachedFunctions();
if (engine) {
engine->ShutDownAndRelease();
engine = nullptr;
}
currentModule = nullptr;
hasValidScript = false;
}
bool ScriptEngine::CompileScript(const std::string& filename) {
if (!engine) return false;
std::string code = ReadFile(filename);
if (code.empty()) {
std::cerr << "Failed to read script file: " << filename << "\n";
return false;
}
// Try to compile in a temporary module first
asIScriptModule* tempMod = engine->GetModule("temp", asGM_ALWAYS_CREATE);
int r = tempMod->AddScriptSection(filename.c_str(), code.c_str());
if (r < 0) {
std::cerr << "Failed to add script section for: " << filename << "\n";
engine->DiscardModule("temp");
return false;
}
r = tempMod->Build();
if (r < 0) {
std::cerr << "Failed to build script: " << filename << "\n";
engine->DiscardModule("temp");
return false;
}
// If we get here, compilation succeeded
// Now safely replace the main module
if (currentModule) {
engine->DiscardModule("main");
}
// Create new main module with the working code
currentModule = engine->GetModule("main", asGM_ALWAYS_CREATE);
r = currentModule->AddScriptSection(filename.c_str(), code.c_str());
if (r >= 0) {
r = currentModule->Build();
}
// Clean up temp module
engine->DiscardModule("temp");
if (r < 0) {
// This shouldn't happen since we already tested compilation
std::cerr << "Unexpected error when creating main module\n";
ClearCachedFunctions();
return false;
}
// Cache new functions
updateFunc = currentModule->GetFunctionByName("Update");
drawFunc = currentModule->GetFunctionByName("Draw");
hasValidScript = true;
std::cout << "Script compiled and cached: " << filename << "\n";
return true;
}
void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) {
if (!func || !engine || !hasValidScript) return;
asIScriptContext* ctx = engine->CreateContext();
if (!ctx) return;
int r = ctx->Prepare(func);
if (r < 0) {
ctx->Release();
return;
}
if (func->GetParamCount() == 1) {
ctx->SetArgFloat(0, dt);
}
r = ctx->Execute();
if (r != asEXECUTION_FINISHED) {
if (r == asEXECUTION_EXCEPTION) {
std::cerr << "Script exception: " << ctx->GetExceptionString() << "\n";
}
}
ctx->Release();
}
void ScriptEngine::ClearCachedFunctions() {
updateFunc = nullptr;
drawFunc = nullptr;
hasValidScript = false;
}
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();
}