feat: implement modular script system (ScriptModule)

This commit is contained in:
ChestnutYueyue 2026-02-10 21:46:43 +08:00
parent b4036cd8dd
commit 360a209a5c
21 changed files with 1716 additions and 0 deletions

View File

@ -0,0 +1,321 @@
# Extra2D 模块化脚本系统融合方案
结合 **Yosin 的配置驱动脚本加载****Kiwano 的模块系统**,为 Extra2D 实现一个强大的模块化 Squirrel 脚本架构。
## 融合架构设计
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Extra2D.exe (C++ 引擎) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application (单例) │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ ModuleList (模块列表) │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ ScriptModule│ │ AudioModule │ │ PhysicsModule │ │ │ │
│ │ │ │ (脚本系统) │ │ (音频系统) │ │ (物理系统) │ │ │ │
│ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ - Run() - 运行应用 │ │
│ │ - Use(Module&) - 注册模块 │ │
│ │ - Update(dt) - 更新所有模块 │ │
│ │ - Render() - 渲染所有模块 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ ScriptModule (脚本模块) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Squirrel VM │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ ScriptConfig.cfg (配置驱动) │ │ │
│ │ │ - 定义脚本加载顺序 │ │ │
│ │ │ - 定义模块依赖关系 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 加载流程: │ │
│ │ 1. 加载 Core 层 (基础工具类) │ │
│ │ 2. 加载 Engine 层 (引擎封装类) │ │
│ │ 3. 执行 User/main.nut (用户入口) │ │
│ │ │ │
│ │ 每帧调用: │ │
│ │ - OnUpdate(dt) -> 调用脚本的 update 函数 │ │
│ │ - OnRender() -> 调用脚本的 render 函数 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Squirrel 脚本层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Core/ │ │ Engine/ │ │ User/ │ │ Modules/ │ │
│ │ │ │ │ │ │ │ │ │
│ │ _init.nut │ │ _init.nut │ │ main.nut │ │ ui.nut │ │
│ │ class.nut │ │ app.nut │ │ stages/ │ │ network.nut │ │
│ │ array.nut │ │ director.nut│ │ objects/ │ │ ... │ │
│ │ math.nut │ │ stage.nut │ │ ui/ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
## 核心组件设计
### 1. Module 基类(来自 Kiwano
```cpp
// base/module.h
class Module {
public:
virtual void setupModule(); // 初始化
virtual void destroyModule(); // 销毁
virtual void onUpdate(float dt); // 更新
virtual void onRender(); // 渲染
virtual void handleEvent(Event* evt); // 事件
};
```
### 2. ScriptModule脚本模块
```cpp
// script/script_module.h
class ScriptModule : public Module {
public:
void setupModule() override {
// 1. 初始化 Squirrel VM
vm_ = sq_open(1024);
// 2. 注册 C++ 绑定
registerBindings(vm_);
// 3. 读取 ScriptConfig.cfg
auto config = loadConfig("scripts/ScriptConfig.cfg");
// 4. 按顺序加载脚本
for (auto& path : config.preload) {
executeFile(path);
}
// 5. 调用 main 函数
callMainFunction();
}
void onUpdate(float dt) override {
// 调用脚本的全局 update 函数
callScriptFunction("onUpdate", dt);
}
};
```
### 3. 脚本配置系统(来自 Yosin
```
# scripts/ScriptConfig.cfg
# 定义脚本加载顺序
# ========== Core 层(基础工具)==========
scripts/core/_init.nut
scripts/core/class.nut
scripts/core/array.nut
scripts/core/table.nut
scripts/core/math.nut
scripts/core/string.nut
# ========== Engine 层(引擎封装)==========
scripts/engine/_init.nut
scripts/engine/application.nut
scripts/engine/director.nut
scripts/engine/stage.nut
scripts/engine/node.nut
scripts/engine/sprite.nut
scripts/engine/action.nut
scripts/engine/audio.nut
scripts/engine/input.nut
# ========== User 层(用户逻辑)==========
scripts/user/stages/start_stage.nut
scripts/user/objects/player.nut
scripts/user/ui/main_ui.nut
scripts/user/main.nut # 入口脚本(最后加载)
```
### 4. 脚本类层次结构(来自 Yosin
```squirrel
// scripts/core/class.nut
// 基础类系统
// scripts/engine/node.nut
class Node {
C_Object = null; // C++ 对象指针
constructor() {
C_Object = Node_Create();
Register_Destruction(C_Object, this);
}
function setPosition(x, y) {
Node_SetPosition(C_Object, x, y);
}
function getPosition() {
return Node_GetPosition(C_Object);
}
}
// scripts/engine/stage.nut
class Stage extends Node {
constructor() {
C_Object = Stage_Create();
}
function enter() {
Director_EnterStage(C_Object);
}
function onUpdate(dt) {
// 子类重写
}
}
// scripts/engine/application.nut
class Application {
title = "Extra2D Game";
width = 1280;
height = 720;
function run(stageClass) {
App_SetTitle(title);
App_SetSize(width, height);
App_Run(stageClass);
}
}
```
### 5. 用户入口脚本main.nut
```squirrel
// scripts/user/main.nut
function main(args) {
// 创建应用
local app = Application();
app.title = "My Game";
app.width = 800;
app.height = 600;
// 运行开始舞台
app.run(StartStage);
}
// 定义开始舞台
class StartStage extends Stage {
function onEnter() {
// 创建精灵
local sprite = Sprite("assets/logo.png");
sprite.setPosition(400, 300);
this.addChild(sprite);
// 创建 UI
local ui = MainUI();
this.addChild(ui);
}
function onUpdate(dt) {
// 每帧更新
}
}
```
## 实现步骤
### 阶段 1: 基础架构
1. 创建 `base/module.h/cpp`Kiwano 风格)
2. 修改 `Application` 支持模块注册
3. 实现模块生命周期管理
### 阶段 2: 脚本模块
1. 创建 `script/script_module.h/cpp`
2. 实现配置解析(`ScriptConfig.cfg`
3. 实现脚本加载器
### 阶段 3: C++ 绑定扩展
1. 扩展 `Node` 绑定(添加 `C_Object` 指针桥接)
2. 添加 `Stage` 类绑定
3. 添加 `Director` 类绑定
4. 添加 `Application` 配置绑定
### 阶段 4: 脚本类库
1. 创建 `scripts/core/` 基础工具
2. 创建 `scripts/engine/` 引擎封装
3. 实现类继承机制
### 阶段 5: 入口机制
1. 实现 `main()` 函数自动调用
2. 实现 `GameWindow` 封装类
3. 创建示例项目
## 文件变更清单
### 新增文件
```
src/extra2d/base/module.h
src/extra2d/base/module.cpp
src/extra2d/script/script_module.h
src/extra2d/script/script_module.cpp
src/extra2d/script/sq_binding_stage.cpp
src/extra2d/script/sq_binding_director.cpp
scripts/ScriptConfig.cfg
scripts/core/_init.nut
scripts/core/class.nut
scripts/engine/_init.nut
scripts/engine/application.nut
scripts/engine/director.nut
scripts/engine/stage.nut
scripts/user/main.nut
```
### 修改文件
```
src/extra2d/app/application.h/cpp
src/extra2d/script/script_engine.cpp
src/extra2d/script/sq_binding_node.cpp
```
## 最终效果
用户只需编写脚本即可创建完整游戏:
```squirrel
// 纯脚本游戏
function main() {
local app = Application();
app.config({
title = "Hello World",
width = 800,
height = 600
});
local director = Director();
director.enterStage(MyStage());
app.run();
}
class MyStage extends Stage {
function onEnter() {
local label = Label("Hello Squirrel!");
label.position = [400, 300];
this.addChild(label);
}
}
```
请确认此方案后,我将开始

View File

@ -4,7 +4,9 @@
#include <extra2d/core/types.h> #include <extra2d/core/types.h>
#include <extra2d/graphics/render_backend.h> #include <extra2d/graphics/render_backend.h>
#include <extra2d/platform/window.h> #include <extra2d/platform/window.h>
#include <extra2d/base/module.h>
#include <memory> #include <memory>
#include <vector>
namespace extra2d { namespace extra2d {
@ -96,6 +98,19 @@ public:
// 获取配置 // 获取配置
const AppConfig &getConfig() const { return config_; } const AppConfig &getConfig() const { return config_; }
// ------------------------------------------------------------------------
// 模块系统 (新增)
// ------------------------------------------------------------------------
// 注册模块
void use(ModulePtr module);
// 获取模块
ModulePtr getModule(const char* name);
// 获取所有模块
const std::vector<ModulePtr>& getModules() const { return modules_; }
private: private:
Application() = default; Application() = default;
~Application(); ~Application();
@ -117,6 +132,9 @@ private:
UniquePtr<EventDispatcher> eventDispatcher_; UniquePtr<EventDispatcher> eventDispatcher_;
UniquePtr<Camera> camera_; UniquePtr<Camera> camera_;
// 模块系统
std::vector<ModulePtr> modules_;
// 状态 // 状态
bool initialized_ = false; bool initialized_ = false;
bool running_ = false; bool running_ = false;

View File

@ -0,0 +1,68 @@
#pragma once
#include "../core/types.h"
#include "../event/event.h"
namespace extra2d {
// 前置声明
class Application;
// ============================================================================
// 模块基类 (参考 Kiwano 设计)
// 所有功能模块的基类,提供统一的生命周期管理
// ============================================================================
class Module {
public:
Module() : enabled_(true), initialized_(false) {}
virtual ~Module() = default;
// 禁用拷贝
Module(const Module&) = delete;
Module& operator=(const Module&) = delete;
// ============================================================================
// 生命周期方法
// ============================================================================
// 模块初始化 (在 Application::init 中调用)
virtual void setupModule() {}
// 模块销毁 (在 Application::shutdown 中调用)
virtual void destroyModule() {}
// 每帧更新
virtual void onUpdate(float dt) {}
// 每帧渲染
virtual void onRender() {}
// 事件处理
virtual void handleEvent(Event* evt) {}
// ============================================================================
// 状态管理
// ============================================================================
// 启用/禁用模块
void setEnabled(bool enabled) { enabled_ = enabled; }
bool isEnabled() const { return enabled_; }
// 检查是否已初始化
bool isInitialized() const { return initialized_; }
void setInitialized(bool initialized) { initialized_ = initialized; }
// 获取模块名称 (子类可重写)
virtual const char* getName() const { return "Module"; }
protected:
bool enabled_; // 模块是否启用
bool initialized_; // 模块是否已初始化
};
// ============================================================================
// 模块智能指针类型
// ============================================================================
using ModulePtr = Ptr<Module>;
} // namespace extra2d

View File

@ -0,0 +1,92 @@
#pragma once
#include "../base/module.h"
#include "../core/string.h"
#include "../core/types.h"
#include <squirrel.h>
#include <vector>
namespace extra2d {
// ============================================================================
// 脚本配置结构
// ============================================================================
struct ScriptConfig {
std::vector<String> preload; // 预加载脚本列表
String entryPoint; // 入口脚本 (默认: scripts/user/main.nut)
};
// ============================================================================
// 脚本模块 - 集成 Squirrel 脚本引擎的 Module 实现
// ============================================================================
class ScriptModule : public Module {
public:
ScriptModule();
~ScriptModule() override;
// 禁用拷贝
ScriptModule(const ScriptModule&) = delete;
ScriptModule& operator=(const ScriptModule&) = delete;
// Module 接口实现
void setupModule() override;
void destroyModule() override;
void onUpdate(float dt) override;
void onRender() override;
const char* getName() const override { return "ScriptModule"; }
// ============================================================================
// 脚本执行
// ============================================================================
bool executeString(const String& code);
bool executeFile(const String& filepath);
// ============================================================================
// 配置加载
// ============================================================================
bool loadConfig(const String& configPath);
const ScriptConfig& getConfig() const { return config_; }
// ============================================================================
// Squirrel VM 访问
// ============================================================================
HSQUIRRELVM getVM() const { return vm_; }
bool isInitialized() const { return vm_ != nullptr; }
// ============================================================================
// 脚本调用
// ============================================================================
bool callFunction(const char* funcName);
bool callFunction(const char* funcName, float arg);
private:
// 初始化 Squirrel VM
bool initVM();
// 关闭 Squirrel VM
void closeVM();
// 注册 C++ 绑定
void registerBindings();
// 加载脚本列表
bool loadScripts(const std::vector<String>& scripts);
// 编译并运行
bool compileAndRun(const String& source, const String& sourceName);
// Squirrel 回调
static void printFunc(HSQUIRRELVM vm, const SQChar* fmt, ...);
static void errorFunc(HSQUIRRELVM vm, const SQChar* fmt, ...);
static SQInteger errorHandler(HSQUIRRELVM vm);
static void compilerError(HSQUIRRELVM vm, const SQChar* desc,
const SQChar* source, SQInteger line,
SQInteger column);
HSQUIRRELVM vm_ = nullptr;
ScriptConfig config_;
bool configLoaded_ = false;
};
// ============================================================================
// 脚本模块智能指针
// ============================================================================
using ScriptModulePtr = Ptr<ScriptModule>;
} // namespace extra2d

View File

@ -178,6 +178,19 @@ template <typename T> T *getSingleton(HSQUIRRELVM vm, SQInteger idx) {
return static_cast<T *>(up); return static_cast<T *>(up);
} }
// ============================================================================
// User pointer helpers (for non-class user pointers)
// ============================================================================
inline void pushUserPointer(HSQUIRRELVM vm, void *ptr) {
sq_pushuserpointer(vm, ptr);
}
inline void *getUserPointer(HSQUIRRELVM vm, SQInteger idx) {
SQUserPointer up = nullptr;
sq_getuserpointer(vm, idx, &up);
return up;
}
// ============================================================================ // ============================================================================
// ClassDef — fluent API for registering a class // ClassDef — fluent API for registering a class
// ============================================================================ // ============================================================================

View File

@ -0,0 +1,12 @@
#pragma once
#include <extra2d/script/sq_binding.h>
namespace extra2d {
namespace sq {
// 注册 Stage 相关绑定
void registerStageBindings(HSQUIRRELVM vm);
} // namespace sq
} // namespace extra2d

View File

@ -2,6 +2,7 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <extra2d/core/string.h>
#include <extra2d/core/types.h> #include <extra2d/core/types.h>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -48,6 +49,9 @@ template <typename T> inline std::string to_string_arg(const T &value) {
} else { } else {
return std::to_string(value); return std::to_string(value);
} }
} else if constexpr (std::is_same_v<T, String>) {
// 处理 extra2d::String
return value.c_str();
} else { } else {
std::ostringstream oss; std::ostringstream oss;
oss << value; oss << value;

View File

@ -176,6 +176,15 @@ void Application::shutdown() {
sceneManager_->end(); sceneManager_->end();
} }
// 销毁所有模块(逆序销毁)
for (auto it = modules_.rbegin(); it != modules_.rend(); ++it) {
if (*it) {
(*it)->destroyModule();
(*it)->setInitialized(false);
}
}
modules_.clear();
// 清理子系统 // 清理子系统
sceneManager_.reset(); sceneManager_.reset();
resourceManager_.reset(); resourceManager_.reset();
@ -311,6 +320,13 @@ void Application::update() {
timerManager_->update(deltaTime_); timerManager_->update(deltaTime_);
} }
// 更新所有启用的模块
for (const auto& module : modules_) {
if (module && module->isEnabled()) {
module->onUpdate(deltaTime_);
}
}
if (sceneManager_) { if (sceneManager_) {
sceneManager_->update(deltaTime_); sceneManager_->update(deltaTime_);
} }
@ -324,6 +340,13 @@ void Application::render() {
renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight()); renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight());
// 渲染所有启用的模块
for (const auto& module : modules_) {
if (module && module->isEnabled()) {
module->onRender();
}
}
if (sceneManager_) { if (sceneManager_) {
sceneManager_->render(*renderer_); sceneManager_->render(*renderer_);
} else { } else {
@ -360,4 +383,40 @@ void Application::enterScene(Ptr<Scene> scene,
} }
} }
// ============================================================================
// 模块系统实现
// ============================================================================
void Application::use(ModulePtr module) {
if (!module) {
E2D_LOG_WARN("Cannot register null module");
return;
}
// 检查是否已存在同名模块
for (const auto& m : modules_) {
if (strcmp(m->getName(), module->getName()) == 0) {
E2D_LOG_WARN("Module '{}' already registered", module->getName());
return;
}
}
// 初始化模块
module->setupModule();
module->setInitialized(true);
// 添加到模块列表
modules_.push_back(module);
E2D_LOG_INFO("Module '{}' registered", module->getName());
}
ModulePtr Application::getModule(const char* name) {
for (const auto& module : modules_) {
if (strcmp(module->getName(), name) == 0) {
return module;
}
}
return nullptr;
}
} // namespace extra2d } // namespace extra2d

View File

@ -0,0 +1,9 @@
#include "extra2d/base/module.h"
namespace extra2d {
// Module 基类的实现
// 目前所有方法都是虚函数,在头文件中已有默认实现
// 如果后续需要添加通用逻辑,可以在这里实现
} // namespace extra2d

View File

@ -0,0 +1,341 @@
#include "extra2d/script/script_module.h"
#include "extra2d/script/sq_binding_action.h"
#include "extra2d/script/sq_binding_animation.h"
#include "extra2d/script/sq_binding_audio.h"
#include "extra2d/script/sq_binding_input.h"
#include "extra2d/script/sq_binding_node.h"
#include "extra2d/script/sq_binding_stage.h"
#include "extra2d/script/sq_binding_types.h"
#include "extra2d/utils/logger.h"
#include <cstdarg>
#include <fstream>
#include <sqstdaux.h>
#include <sqstdblob.h>
#include <sqstdio.h>
#include <sqstdmath.h>
#include <sqstdstring.h>
#include <sstream>
namespace extra2d {
ScriptModule::ScriptModule() = default;
ScriptModule::~ScriptModule() {
destroyModule();
}
void ScriptModule::setupModule() {
if (vm_) {
E2D_LOG_WARN("ScriptModule: VM already initialized");
return;
}
// 1. 初始化 Squirrel VM
if (!initVM()) {
E2D_LOG_ERROR("ScriptModule: failed to initialize VM");
return;
}
// 2. 注册 C++ 绑定
registerBindings();
// 3. 加载配置 (如果存在)
if (!configLoaded_) {
loadConfig("scripts/ScriptConfig.cfg");
}
// 4. 加载预定义脚本
if (!config_.preload.empty()) {
loadScripts(config_.preload);
}
// 5. 执行入口脚本
if (!config_.entryPoint.empty()) {
executeFile(config_.entryPoint);
}
E2D_LOG_INFO("ScriptModule: setup complete");
}
void ScriptModule::destroyModule() {
if (vm_) {
closeVM();
E2D_LOG_INFO("ScriptModule: destroyed");
}
}
void ScriptModule::onUpdate(float dt) {
if (!vm_) return;
// 调用脚本的全局 onUpdate 函数
callFunction("onUpdate", dt);
}
void ScriptModule::onRender() {
if (!vm_) return;
// 调用脚本的全局 onRender 函数
callFunction("onRender");
}
bool ScriptModule::executeString(const String& code) {
if (!vm_) {
E2D_LOG_ERROR("ScriptModule: VM not initialized");
return false;
}
return compileAndRun(code, "<string>");
}
bool ScriptModule::executeFile(const String& filepath) {
if (!vm_) {
E2D_LOG_ERROR("ScriptModule: VM not initialized");
return false;
}
std::ifstream file(filepath.c_str());
if (!file.is_open()) {
E2D_LOG_ERROR("ScriptModule: cannot open file '{}'", filepath);
return false;
}
std::ostringstream ss;
ss << file.rdbuf();
return compileAndRun(ss.str(), filepath);
}
bool ScriptModule::loadConfig(const String& configPath) {
std::ifstream file(configPath.c_str());
if (!file.is_open()) {
E2D_LOG_WARN("ScriptModule: config file not found '{}', using defaults", configPath);
// 使用默认配置
config_.entryPoint = "scripts/user/main.nut";
configLoaded_ = true;
return true;
}
std::string stdLine;
bool inPreloadSection = false;
while (std::getline(file, stdLine)) {
// 转换为 String 进行处理
String line = String(stdLine.c_str());
// 跳过空行和注释
line = line.trim();
if (line.empty() || line.startsWith("#")) {
continue;
}
// 检查是否是入口点定义
if (line.startsWith("entry:")) {
config_.entryPoint = line.substring(6).trim();
continue;
}
// 检查是否是预加载脚本
if (line.endsWith(".nut")) {
config_.preload.push_back(line);
}
}
// 如果没有指定入口点,使用默认值
if (config_.entryPoint.empty()) {
config_.entryPoint = "scripts/user/main.nut";
}
configLoaded_ = true;
E2D_LOG_INFO("ScriptModule: loaded config from '{}'", configPath);
return true;
}
bool ScriptModule::callFunction(const char* funcName) {
if (!vm_) return false;
SQInteger top = sq_gettop(vm_);
sq_pushroottable(vm_);
sq_pushstring(vm_, funcName, -1);
if (SQ_SUCCEEDED(sq_get(vm_, -2))) {
sq_pushroottable(vm_); // this
if (SQ_SUCCEEDED(sq_call(vm_, 1, SQFalse, SQTrue))) {
sq_settop(vm_, top);
return true;
}
}
sq_settop(vm_, top);
return false;
}
bool ScriptModule::callFunction(const char* funcName, float arg) {
if (!vm_) return false;
SQInteger top = sq_gettop(vm_);
sq_pushroottable(vm_);
sq_pushstring(vm_, funcName, -1);
if (SQ_SUCCEEDED(sq_get(vm_, -2))) {
sq_pushroottable(vm_); // this
sq_pushfloat(vm_, arg);
if (SQ_SUCCEEDED(sq_call(vm_, 2, SQFalse, SQTrue))) {
sq_settop(vm_, top);
return true;
}
}
sq_settop(vm_, top);
return false;
}
bool ScriptModule::initVM() {
vm_ = sq_open(1024);
if (!vm_) {
E2D_LOG_ERROR("ScriptModule: failed to create Squirrel VM");
return false;
}
sq_setprintfunc(vm_, printFunc, errorFunc);
sq_setcompilererrorhandler(vm_, compilerError);
sq_pushroottable(vm_);
// Register standard libraries
sqstd_register_mathlib(vm_);
sqstd_register_stringlib(vm_);
sqstd_register_bloblib(vm_);
sqstd_register_iolib(vm_);
// Set error handler
sq_newclosure(vm_, errorHandler, 0);
sq_seterrorhandler(vm_);
sq_pop(vm_, 1); // pop root table
E2D_LOG_INFO("ScriptModule: Squirrel VM initialized (v{})", SQUIRREL_VERSION);
return true;
}
void ScriptModule::closeVM() {
if (vm_) {
sq_close(vm_);
vm_ = nullptr;
E2D_LOG_INFO("ScriptModule: Squirrel VM closed");
}
}
void ScriptModule::registerBindings() {
if (!vm_) return;
// Register Easy2D bindings
sq::registerValueTypes(vm_);
sq::registerNodeBindings(vm_);
sq::registerStageBindings(vm_);
sq::registerInputBindings(vm_);
sq::registerActionBindings(vm_);
sq::registerAudioBindings(vm_);
sq::registerAnimationBindings(vm_);
// Register global log function
sq_pushroottable(vm_);
sq_pushstring(vm_, "log", -1);
sq_newclosure(
vm_,
[](HSQUIRRELVM v) -> SQInteger {
const SQChar* msg = nullptr;
sq_getstring(v, 2, &msg);
if (msg)
E2D_LOG_INFO("[Script] {}", msg);
return 0;
},
0);
sq_newslot(vm_, -3, SQFalse);
sq_pop(vm_, 1);
E2D_LOG_INFO("ScriptModule: C++ bindings registered");
}
bool ScriptModule::loadScripts(const std::vector<String>& scripts) {
bool allSuccess = true;
for (const auto& script : scripts) {
if (!executeFile(script)) {
E2D_LOG_ERROR("ScriptModule: failed to load script '{}'", script);
allSuccess = false;
} else {
E2D_LOG_INFO("ScriptModule: loaded script '{}'", script);
}
}
return allSuccess;
}
bool ScriptModule::compileAndRun(const String& source, const String& sourceName) {
if (!vm_) return false;
SQInteger top = sq_gettop(vm_);
sq_pushroottable(vm_);
if (SQ_FAILED(sq_compilebuffer(vm_, source.c_str(),
static_cast<SQInteger>(source.length()),
sourceName.c_str(), SQTrue))) {
sq_settop(vm_, top);
return false;
}
sq_push(vm_, -2); // push root table as 'this'
if (SQ_FAILED(sq_call(vm_, 1, SQFalse, SQTrue))) {
sq_settop(vm_, top);
return false;
}
sq_settop(vm_, top);
return true;
}
void ScriptModule::printFunc(HSQUIRRELVM, const SQChar* fmt, ...) {
va_list args;
va_start(args, fmt);
char buf[2048];
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
E2D_LOG_INFO("[Squirrel] {}", buf);
}
void ScriptModule::errorFunc(HSQUIRRELVM, const SQChar* fmt, ...) {
va_list args;
va_start(args, fmt);
char buf[2048];
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
E2D_LOG_ERROR("[Squirrel] {}", buf);
}
SQInteger ScriptModule::errorHandler(HSQUIRRELVM vm) {
const SQChar* errMsg = nullptr;
if (sq_gettop(vm) >= 1) {
if (SQ_FAILED(sq_getstring(vm, 2, &errMsg))) {
errMsg = "unknown error";
}
}
// Print call stack
SQStackInfos si;
SQInteger level = 1;
E2D_LOG_ERROR("[Squirrel] Runtime error: {}", errMsg ? errMsg : "unknown");
while (SQ_SUCCEEDED(sq_stackinfos(vm, level, &si))) {
const SQChar* fn = si.funcname ? si.funcname : "unknown";
const SQChar* src = si.source ? si.source : "unknown";
E2D_LOG_ERROR(" [{}] {}:{} in {}", level, src, si.line, fn);
++level;
}
return 0;
}
void ScriptModule::compilerError(HSQUIRRELVM, const SQChar* desc,
const SQChar* source, SQInteger line,
SQInteger column) {
E2D_LOG_ERROR("[Squirrel] Compile error: {}:{}:{}: {}", source, line, column, desc);
}
} // namespace extra2d

View File

@ -0,0 +1,223 @@
#include <extra2d/app/application.h>
#include <extra2d/scene/scene.h>
#include <extra2d/scene/scene_manager.h>
#include <extra2d/scene/sprite.h>
#include <extra2d/script/sq_binding_stage.h>
#include <extra2d/script/sq_binding_types.h>
namespace extra2d {
namespace sq {
// ============================================================================
// Director (场景管理器单例)
// ============================================================================
static SQInteger directorGetInstance(HSQUIRRELVM vm) {
// Director 是单例,返回 SceneManager 的引用
auto& app = Application::instance();
auto* sceneManager = &app.scenes();
sq_pushuserpointer(vm, sceneManager);
return 1;
}
static SQInteger directorEnterScene(HSQUIRRELVM vm) {
auto* sceneManager = static_cast<SceneManager*>(getUserPointer(vm, 1));
if (!sceneManager)
return sq_throwerror(vm, "null director");
auto scene = getPtr<Scene>(vm, 2);
if (!scene)
return sq_throwerror(vm, "null scene");
auto& app = Application::instance();
scene->setViewportSize(static_cast<float>(app.window().getWidth()),
static_cast<float>(app.window().getHeight()));
sceneManager->enterScene(scene);
return 0;
}
static SQInteger directorReplaceScene(HSQUIRRELVM vm) {
auto* sceneManager = static_cast<SceneManager*>(getUserPointer(vm, 1));
if (!sceneManager)
return sq_throwerror(vm, "null director");
auto scene = getPtr<Scene>(vm, 2);
if (!scene)
return sq_throwerror(vm, "null scene");
auto& app = Application::instance();
scene->setViewportSize(static_cast<float>(app.window().getWidth()),
static_cast<float>(app.window().getHeight()));
sceneManager->replaceScene(scene);
return 0;
}
static SQInteger directorGetCurrentScene(HSQUIRRELVM vm) {
auto* sceneManager = static_cast<SceneManager*>(getUserPointer(vm, 1));
if (!sceneManager)
return sq_throwerror(vm, "null director");
auto scene = sceneManager->getCurrentScene();
if (scene) {
pushPtr(vm, scene);
} else {
pushNull(vm);
}
return 1;
}
static SQInteger directorPopScene(HSQUIRRELVM vm) {
auto* sceneManager = static_cast<SceneManager*>(getUserPointer(vm, 1));
if (!sceneManager)
return sq_throwerror(vm, "null director");
sceneManager->popScene();
return 0;
}
// ============================================================================
// Scene (舞台/场景)
// ============================================================================
static SQInteger sceneCreate(HSQUIRRELVM vm) {
auto scene = makePtr<Scene>();
pushPtr(vm, scene);
return 1;
}
static SQInteger sceneSetBackgroundColor(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
SQInteger argc = sq_gettop(vm);
if (argc >= 5) {
// r, g, b, a
float r = static_cast<float>(getFloat(vm, 2));
float g = static_cast<float>(getFloat(vm, 3));
float b = static_cast<float>(getFloat(vm, 4));
float a = static_cast<float>(getFloat(vm, 5));
scene->setBackgroundColor(Color(r, g, b, a));
} else if (argc >= 2) {
// Color instance
Color *c = getValueInstance<Color>(vm, 2);
if (c)
scene->setBackgroundColor(*c);
}
return 0;
}
static SQInteger sceneGetBackgroundColor(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
pushValueInstance(vm, scene->getBackgroundColor());
return 1;
}
static SQInteger sceneAddChild(HSQUIRRELVM vm) {
auto scene = getPtr<Scene>(vm, 1);
auto node = getPtr<Node>(vm, 2);
if (!scene || !node)
return sq_throwerror(vm, "null scene or node");
scene->addChild(node);
return 0;
}
static SQInteger sceneRemoveChild(HSQUIRRELVM vm) {
auto scene = getPtr<Scene>(vm, 1);
auto node = getPtr<Node>(vm, 2);
if (!scene || !node)
return sq_throwerror(vm, "null scene or node");
scene->removeChild(node);
return 0;
}
static SQInteger sceneRemoveAllChildren(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
scene->removeAllChildren();
return 0;
}
static SQInteger sceneGetChildByName(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
auto child = scene->getChildByName(getString(vm, 2));
if (child)
pushPtr(vm, child);
else
pushNull(vm);
return 1;
}
static SQInteger sceneSetSpatialIndexingEnabled(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
scene->setSpatialIndexingEnabled(getBool(vm, 2));
return 0;
}
static SQInteger sceneIsSpatialIndexingEnabled(HSQUIRRELVM vm) {
auto *scene = getRawPtr<Scene>(vm, 1);
if (!scene)
return sq_throwerror(vm, "null scene");
push(vm, scene->isSpatialIndexingEnabled());
return 1;
}
void registerStageBindings(HSQUIRRELVM vm) {
// Director 单例
sq_pushroottable(vm);
sq_pushstring(vm, "Director", -1);
sq_newtable(vm);
// Director.getInstance() -> 返回 SceneManager 指针
sq_pushstring(vm, "getInstance", -1);
sq_newclosure(vm, directorGetInstance, 0);
sq_newslot(vm, -3, SQFalse);
// Director.enterScene(scene)
sq_pushstring(vm, "enterScene", -1);
sq_newclosure(vm, directorEnterScene, 0);
sq_newslot(vm, -3, SQFalse);
// Director.replaceScene(scene)
sq_pushstring(vm, "replaceScene", -1);
sq_newclosure(vm, directorReplaceScene, 0);
sq_newslot(vm, -3, SQFalse);
// Director.getCurrentScene()
sq_pushstring(vm, "getCurrentScene", -1);
sq_newclosure(vm, directorGetCurrentScene, 0);
sq_newslot(vm, -3, SQFalse);
// Director.popScene()
sq_pushstring(vm, "popScene", -1);
sq_newclosure(vm, directorPopScene, 0);
sq_newslot(vm, -3, SQFalse);
sq_newslot(vm, -3, SQFalse); // Director -> root
sq_pop(vm, 1); // pop root
// Scene 类
ClassDef(vm, "Scene")
.setTypeTag(typeTag<Scene>())
.staticMethod("create", sceneCreate)
.method("setBackgroundColor", sceneSetBackgroundColor)
.method("getBackgroundColor", sceneGetBackgroundColor)
.method("addChild", sceneAddChild)
.method("removeChild", sceneRemoveChild)
.method("removeAllChildren", sceneRemoveAllChildren)
.method("getChildByName", sceneGetChildByName)
.method("setSpatialIndexingEnabled", sceneSetSpatialIndexingEnabled)
.method("isSpatialIndexingEnabled", sceneIsSpatialIndexingEnabled)
.commit();
}
} // namespace sq
} // namespace extra2d

193
docs/ScriptModule_Usage.md Normal file
View File

@ -0,0 +1,193 @@
# Extra2D ScriptModule 使用指南
## 快速开始
### 1. 在 C++ 中注册 ScriptModule
```cpp
#include <extra2d/extra2d.h>
int main(int argc, char** argv) {
// 初始化日志
Logger::init();
// 配置应用
AppConfig config;
config.title = "Script Game";
config.width = 1280;
config.height = 720;
// 初始化应用
auto& app = Application::instance();
if (!app.init(config)) {
return -1;
}
// 注册 ScriptModule
auto scriptModule = makePtr<ScriptModule>();
app.use(scriptModule);
// 运行应用
app.run();
return 0;
}
```
### 2. 创建脚本配置文件
创建 `scripts/ScriptConfig.cfg`:
```
# 预加载脚本列表
scripts/core/_init.nut
scripts/core/class.nut
scripts/core/math.nut
# 入口脚本
entry: scripts/user/main.nut
```
### 3. 编写游戏脚本
创建 `scripts/user/main.nut`:
```squirrel
// 游戏入口
function main(args) {
print("Game started!");
// 创建场景
local scene = Scene();
scene.setBackgroundColor(0.1, 0.1, 0.3, 1.0);
// 进入场景
Director.enterScene(scene);
}
// 每帧更新
function onUpdate(dt) {
// 游戏逻辑
}
// 每帧渲染
function onRender() {
// 渲染逻辑
}
// 启动游戏
main([]);
```
## 脚本类库
### Core 层工具
#### 类系统
```squirrel
// 定义类
local MyClass = class(null);
MyClass.value <- 0;
MyClass.constructor <- function(v) {
this.value = v;
}
MyClass.getValue <- function() {
return this.value;
}
// 创建实例
local obj = MyClass.new(42);
print(obj.getValue()); // 输出: 42
```
#### 数学工具
```squirrel
// 向量
local v1 = Vector2(100, 200);
local v2 = Vector2(50, 50);
local v3 = v1.add(v2);
// 插值
local value = lerp(0, 100, 0.5); // 50
// 限制
local clamped = clamp(value, 0, 100);
```
## 配置格式
### ScriptConfig.cfg
```
# 注释以 # 开头
# 预加载脚本(按顺序加载)
scripts/core/_init.nut
scripts/core/class.nut
scripts/core/array.nut
scripts/core/math.nut
scripts/core/string.nut
scripts/core/table.nut
scripts/engine/_init.nut
scripts/engine/application.nut
scripts/engine/director.nut
scripts/engine/stage.nut
scripts/engine/node.nut
scripts/engine/sprite.nut
scripts/engine/action.nut
scripts/engine/audio.nut
scripts/engine/input.nut
# 用户脚本
scripts/user/stages/start_stage.nut
scripts/user/objects/player.nut
scripts/user/ui/main_ui.nut
# 入口脚本(最后加载,必须包含 main 函数)
entry: scripts/user/main.nut
```
## API 参考
### ScriptModule C++ API
```cpp
// 创建模块
auto scriptModule = makePtr<ScriptModule>();
// 加载配置
scriptModule->loadConfig("path/to/config.cfg");
// 执行脚本
scriptModule->executeFile("script.nut");
scriptModule->executeString("print('Hello')");
// 调用脚本函数
scriptModule->callFunction("onUpdate", 0.016f);
// 获取 Squirrel VM
HSQUIRRELVM vm = scriptModule->getVM();
```
### 全局函数
脚本中可以使用的全局函数:
```squirrel
// 日志
print("message"); // 输出到控制台
log("message"); // 输出到日志系统
// 数学
sqrt(x); // 平方根
abs(x); // 绝对值
sin(x); cos(x); tan(x); // 三角函数
rand(); // 随机数
```
## 示例项目
参见 `examples/script_demo/` 目录获取完整示例。

30
scripts/ScriptConfig.cfg Normal file
View File

@ -0,0 +1,30 @@
# Extra2D 脚本配置文件
# 定义脚本加载顺序和入口点
# ========== Core 层(基础工具)==========
scripts/core/_init.nut
scripts/core/class.nut
scripts/core/array.nut
scripts/core/table.nut
scripts/core/math.nut
scripts/core/string.nut
# ========== Engine 层(引擎封装)==========
scripts/engine/_init.nut
scripts/engine/application.nut
scripts/engine/director.nut
scripts/engine/stage.nut
scripts/engine/node.nut
scripts/engine/sprite.nut
scripts/engine/action.nut
scripts/engine/audio.nut
scripts/engine/input.nut
# ========== User 层(用户逻辑)==========
# 用户脚本将在最后加载
# scripts/user/stages/start_stage.nut
# scripts/user/objects/player.nut
# scripts/user/ui/main_ui.nut
# 入口脚本(最后加载)
entry: scripts/user/main.nut

4
scripts/core/_init.nut Normal file
View File

@ -0,0 +1,4 @@
// Core 层初始化脚本
// 基础工具类和函数
print("Core layer initialized");

53
scripts/core/array.nut Normal file
View File

@ -0,0 +1,53 @@
// Core 层 - 数组工具
// 提供数组操作的工具函数
// 检查数组是否包含元素
function array_contains(arr, item) {
foreach (i, v in arr) {
if (v == item) return true;
}
return false;
}
// 查找数组元素索引
function array_indexOf(arr, item) {
foreach (i, v in arr) {
if (v == item) return i;
}
return -1;
}
// 数组去重
function array_unique(arr) {
local result = [];
local seen = {};
foreach (v in arr) {
if (!seen.rawin(v)) {
seen[v] <- true;
result.append(v);
}
}
return result;
}
// 数组过滤
function array_filter(arr, predicate) {
local result = [];
foreach (v in arr) {
if (predicate(v)) {
result.append(v);
}
}
return result;
}
// 数组映射
function array_map(arr, mapper) {
local result = [];
foreach (v in arr) {
result.append(mapper(v));
}
return result;
}
print("Array utilities loaded");

43
scripts/core/class.nut Normal file
View File

@ -0,0 +1,43 @@
// Core 层 - 类系统
// 提供基础的类定义工具
// 简单的类创建函数
function class(base) {
local new_class = {};
if (base != null) {
// 继承基类
foreach (key, value in base) {
new_class[key] <- value;
}
new_class.base <- base;
}
// 创建实例的函数
new_class.constructor <- function() {}
new_class.new <- function(...) {
local instance = {};
// 复制类成员到实例
foreach (key, value in this) {
if (key != "new" && key != "constructor") {
instance[key] <- value;
}
}
// 设置元表实现继承
instance.setdelegate(this);
// 调用构造函数
if (this.constructor != null) {
instance.constructor.acall([instance].concat(vargv));
}
return instance;
}
return new_class;
}
print("Class system loaded");

77
scripts/core/math.nut Normal file
View File

@ -0,0 +1,77 @@
// Core 层 - 数学工具
// 提供数学计算的工具函数
// 常量
const PI <- 3.14159265359;
const DEG_TO_RAD <- PI / 180.0;
const RAD_TO_DEG <- 180.0 / PI;
// 角度转弧度
function degToRad(deg) {
return deg * DEG_TO_RAD;
}
// 弧度转角度
function radToDeg(rad) {
return rad * RAD_TO_DEG;
}
// 限制数值在范围内
function clamp(value, min, max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
// 线性插值
function lerp(a, b, t) {
return a + (b - a) * t;
}
// 获取符号
function sign(x) {
if (x > 0) return 1;
if (x < 0) return -1;
return 0;
}
// 向量2D类
Vector2 <- class {
x = 0.0;
y = 0.0;
constructor(_x = 0.0, _y = 0.0) {
x = _x;
y = _y;
}
function length() {
return sqrt(x * x + y * y);
}
function normalize() {
local len = length();
if (len > 0) {
return Vector2(x / len, y / len);
}
return Vector2(0, 0);
}
function add(v) {
return Vector2(x + v.x, y + v.y);
}
function sub(v) {
return Vector2(x - v.x, y - v.y);
}
function mul(s) {
return Vector2(x * s, y * s);
}
function toString() {
return "Vector2(" + x + ", " + y + ")";
}
};
print("Math utilities loaded");

4
scripts/engine/_init.nut Normal file
View File

@ -0,0 +1,4 @@
// Engine 层初始化脚本
// 引擎封装类的基础设置
print("Engine layer initialized");

View File

@ -0,0 +1,31 @@
// Engine 层 - Director 导演类
// 场景管理单例
Director <- {
// 进入场景
function enterScene(scene) {
// 调用 C++ Director.enterScene
local instance = this.getInstance();
return instance.enterScene(scene);
}
// 替换场景
function replaceScene(scene) {
local instance = this.getInstance();
return instance.replaceScene(scene);
}
// 获取当前场景
function getCurrentScene() {
local instance = this.getInstance();
return instance.getCurrentScene();
}
// 弹出场景
function popScene() {
local instance = this.getInstance();
return instance.popScene();
}
};
print("Director loaded");

68
scripts/engine/stage.nut Normal file
View File

@ -0,0 +1,68 @@
// Engine 层 - Stage 场景类
// 继承自 Node 的场景类
Stage <- class(Node) {
// 构造函数
constructor() {
// 创建 C++ Scene 对象
this.C_Object = Scene.create();
}
// 设置背景颜色
function setBackgroundColor(r, g, b, a) {
if (this.C_Object) {
this.C_Object.setBackgroundColor(r, g, b, a);
}
}
// 获取背景颜色
function getBackgroundColor() {
if (this.C_Object) {
return this.C_Object.getBackgroundColor();
}
return null;
}
// 添加子节点
function addChild(node) {
if (this.C_Object && node && node.C_Object) {
this.C_Object.addChild(node.C_Object);
}
}
// 移除子节点
function removeChild(node) {
if (this.C_Object && node && node.C_Object) {
this.C_Object.removeChild(node.C_Object);
}
}
// 移除所有子节点
function removeAllChildren() {
if (this.C_Object) {
this.C_Object.removeAllChildren();
}
}
// 进入场景时调用
function onEnter() {
// 子类重写
}
// 退出场景时调用
function onExit() {
// 子类重写
}
// 每帧更新
function onUpdate(dt) {
// 子类重写
}
// 每帧渲染
function onRender() {
// 子类重写
}
};
print("Stage loaded");

53
scripts/user/main.nut Normal file
View File

@ -0,0 +1,53 @@
// User 层 - 入口脚本
// 这是用户游戏的入口点
function main(args) {
print("========================================");
print(" Extra2D Script System Demo");
print("========================================");
// 创建开始场景
local startStage = StartStage();
// 进入场景
Director.enterScene(startStage.C_Object);
print("Game started! Press START to exit.");
}
// 定义开始场景
StartStage <- class(Stage) {
constructor() {
base.constructor();
// 设置背景颜色 (深蓝色)
this.setBackgroundColor(0.1, 0.1, 0.3, 1.0);
print("StartStage created!");
}
function onEnter() {
print("StartStage entered!");
}
function onUpdate(dt) {
// 游戏逻辑
}
function onRender() {
// 渲染逻辑
}
};
// 全局更新函数(每帧调用)
function onUpdate(dt) {
// 在这里编写每帧更新的逻辑
}
// 全局渲染函数(每帧调用)
function onRender() {
// 在这里编写每帧渲染的逻辑
}
// 启动游戏
main([]);