自定义游戏脚本语言的解析与执行器设计
一、引言
在大型游戏项目中,我们常常需要灵活控制角色行为、任务逻辑、剧情流程等。此时,硬编码显得繁琐且缺乏扩展性。自定义脚本语言的引入可以解决:
- 游戏策划可编写/调试行为逻辑
- 逻辑热更新(无需重新编译)
- 支持表达式、控制流与函数调用
本篇将从底层出发,用 C++ 构建一个简单的 解释型脚本语言,具备 词法分析、语法解析、字节码生成、虚拟机执行 四个核心部分,并以 RPG 任务系统为例进行实践。
二、目标语言功能设定
我们构建的脚本语言支持如下功能:
function attack(target) {
if (target.hp > 0) {
say("攻击 " + target.name);
target.hp = target.hp - 10;
} else {
say("敌人已被击败!");
}
}
支持:
- 变量定义 / 修改
- 表达式运算
- 条件分支 / 循环
- 函数定义与调用
- 内建函数(如 say())
三、架构设计概览
四、词法分析器(Lexer)
将脚本文本拆分为 Token 流:
enum class TokenType {
Identifier, Number, String,
Function, If, Else, Return,
LeftParen, RightParen, Plus, Minus, Equal,
Eof
};
struct Token {
TokenType type;
std::string value;
};
示例源码:
attack("Orc")
词法输出:
[
{ "Identifier": "attack" },
{ "LeftParen": "(" },
{ "String": "Orc" },
{ "RightParen": ")" }
]
五、语法分析器(Parser)
将 Token 序列转为 AST:
struct ASTNode {
virtual ~ASTNode() = default;
};
struct FunctionCall : ASTNode {
std::string name;
std::vector<std::shared_ptr<ASTNode>> args;
};
struct IfStatement : ASTNode {
std::shared_ptr<ASTNode> condition;
std::vector<std::shared_ptr<ASTNode>> thenBlock;
std::vector<std::shared_ptr<ASTNode>> elseBlock;
};
语法树结构图:
六、字节码生成器
AST 转换为栈式字节码:
enum class OpCode {
PushConst, LoadVar, StoreVar,
Add, Sub, Equal,
JumpIfFalse, Jump, CallFunc, Return
};
struct Instruction {
OpCode opcode;
std::vector<std::string> operands;
};
示例代码:
say("敌人已被击败")
字节码指令:
PushConst "敌人已被击败"
CallFunc say 1
七、虚拟机执行器(VM)
解释执行字节码指令:
class VM {
std::vector<Value> stack;
std::unordered_map<std::string, Value> vars;
public:
void run(const std::vector<Instruction>& code) {
for (size_t ip = 0; ip < code.size(); ++ip) {
const auto& instr = code[ip];
switch (instr.opcode) {
case OpCode::PushConst:
stack.push_back(Value(instr.operands[0]));
break;
case OpCode::CallFunc:
if (instr.operands[0] == "say") {
std::cout << ">> " << stack.back().toString() << std::endl;
stack.pop_back();
}
break;
}
}
}
};
执行结果:
>> 敌人已被击败
八、运行时变量与作用域
支持函数作用域、全局变量与栈帧:
九、示例:RPG 战斗逻辑脚本
脚本:
function battle(target) {
if (target.hp > 0) {
say("攻击 " + target.name);
target.hp = target.hp - 5;
if (target.hp <= 0) {
say("击败敌人!");
}
}
}
编译后字节码:
LoadVar target.hp
PushConst 0
Greater
JumpIfFalse L1
LoadVar target.name
PushConst "攻击 "
Add
CallFunc say 1
LoadVar target.hp
PushConst 5
Sub
StoreVar target.hp
LoadVar target.hp
PushConst 0
LessEqual
JumpIfFalse L2
PushConst "击败敌人!"
CallFunc say 1
L2:
L1:
十、高级功能扩展建议
功能 | 说明 |
---|---|
支持数组与字典类型 | 脚本中访问复杂数据结构 |
热重载与调试支持 | 实时修改脚本,断点单步调试 |
支持 async/await | 实现协程式脚本逻辑控制 |
脚本与 C++ 互操作 | 内建函数可调用游戏 C++ 方法 |
十一、脚本语言与游戏引擎集成
双向通信接口:
void RegisterFunction(const std::string& name, std::function<void(Value)> func);
RegisterFunction("say", [](Value v) {
std::cout << "角色说:" << v.toString() << std::endl;
});
脚本控制游戏事件:
VM vm;
vm.run(loadScript("battle.txt"));
十二、总结与展望
我们构建了一个拥有完整语法解析、字节码编译、虚拟机执行能力的脚本语言框架,它可以:
- 轻量快速
- 嵌入游戏引擎
- 适配任务系统、NPC行为、战斗 AI 等场景