news 2026/9/30 2:33:50

java-design-patterns 中的 Bytecode 模式:用自定义虚拟机以数据驱动游戏行为

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
java-design-patterns 中的 Bytecode 模式:用自定义虚拟机以数据驱动游戏行为
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

本文以开源仓库 java-design-patterns 中bytecode模块的官方文档(阿拉伯语版与 英语版)为核心,结合模块源码与测试,系统讲解 Bytecode(字节码)模式的设计意图、指令集定义、栈式虚拟机实现、文本指令转换工具,以及完整的可运行示例与验证方式。读完本文,你将掌握如何把"行为"编码为数据、由虚拟机逐条解释执行,从而在游戏开发等场景中实现数据驱动的行为定制。

一、模式意图:把行为编码为指令

Bytecode 模式的核心意图是:允许将行为编码为虚拟机可执行的指令。在传统开发中,行为以源码形式固化在代码里,每次调整都需要重新编译;而 Bytecode 模式把行为变成"数据",由运行时解释执行,从而让行为获得数据才有的灵活性——可以存储、传输、动态组合,甚至由非程序员(如游戏策划)通过文本指令来定义。

用一句话概括(文档原话):Bytecode 模式让行为由数据驱动,而非由代码驱动(Bytecode pattern enables behavior driven by data instead of code)。

现实世界的类比

一支团队正在开发一款巫师对战的新游戏。巫师的行为需要通过反复试玩调整、迭代数百次。如果每次游戏策划想调整行为都要请程序员改代码,那显然不理想。因此,巫师的行为被实现为一台"基于数据的虚拟机"。

这正是 Bytecode 模式的典型动机:把频繁变动的游戏逻辑从编译期代码中剥离,交给运行时解释的数据指令。

Game Programming Patterns 对 Bytecode 模式的定义(文档引用)是:

指令集(instruction set)定义了可执行的低层操作。一系列指令被编码为字节序列。虚拟机一条一条地执行这些指令,并用一个栈存放中间值。通过组合指令,可以定义出复杂的高层行为。

二、仓库模块结构与运行方式

本仓库的bytecode模块是一个独立的 Maven 子项目,代码结构如下:

  • bytecode/src/main/java/com/iluwatar/bytecode/:核心实现,包含Wizard、Instruction、VirtualMachine、App四个类;
  • bytecode/src/main/java/com/iluwatar/bytecode/util/InstructionConverterUtil.java:把文本形式的指令字符串转换为字节码数组的转换工具;
  • bytecode/src/test/java/com/iluwatar/bytecode/:JUnit 5 测试,覆盖虚拟机各指令行为与转换工具;
  • bytecode/etc/:类图(bytecode.urm.png)与序列图(bytecode-sequence-diagram.png)资源。

运行演示程序时,可直接执行App类的main方法(在模块根目录 bytecode/pom.xml 下通过 Maven 构建后运行),其输出即本文第四节展示的栈轨迹日志。

三、核心参与者:三个关键类

1. Wizard——被操作的游戏对象

Wizard.java 是游戏对象模型,持有三类可被指令修改的属性,以及两个副作用行为计数器:

@AllArgsConstructor @Setter @Getter @Slf4j public class Wizard { private int health; // 生命值 private int agility; // 敏捷 private int wisdom; // 智慧 private int numberOfPlayedSounds; // 已播放音效次数 private int numberOfSpawnedParticles; // 已生成粒子次数 public void playSound() { LOGGER.info("Playing sound"); numberOfPlayedSounds++; } public void spawnParticles() { LOGGER.info("Spawning particles"); numberOfSpawnedParticles++; } }

Wizard自身没有任何业务逻辑判断,它只是一个"数据容器 + 行为原语",完全被动地接受虚拟机的指令操作。

2. Instruction——指令集枚举

Instruction.java 用 Java 枚举定义了 11 条指令,每条指令绑定一个整数操作码。每一条指令都有自己操作栈数据的语义,例如ADD弹出栈顶两个值求和后压回栈。完整的指令码表如下:

操作码指令栈语义
1LITERAL读取 bytecode 中下一个值并压入栈(如LITERAL 0压入 0)
2SET_HEALTH弹出生命值数值与巫师编号,调用 set health
3SET_WISDOM弹出智慧值数值与巫师编号,调用 set wisdom
4SET_AGILITY弹出敏捷值数值与巫师编号,调用 set agility
5PLAY_SOUND弹出值作为巫师编号,调用 play sound
6SPAWN_PARTICLES弹出值作为巫师编号,调用 spawn particles
7GET_HEALTH弹出值作为巫师编号,将巫师生命值压入栈
8GET_AGILITY弹出值作为巫师编号,将巫师敏捷值压入栈
9GET_WISDOM弹出值作为巫师编号,将巫师智慧值压入栈
10ADD弹出两个值,压入它们的和
11DIVIDE弹出两个值,压入它们的商

枚举还提供了反向查找方法getInstruction(int value):遍历所有枚举项,找到intValue匹配的指令;找不到时抛出IllegalArgumentException("Invalid instruction value")。

3. VirtualMachine——解释执行的核心

VirtualMachine.java 是本模式的心脏:它持有一个操作数栈Stack<Integer>和一个长度为 2 的巫师数组Wizard[] wizards,并提供两个构造函数:

  • 无参构造:用ThreadLocalRandom在[3, 32]区间为两名巫师随机生成 health、agility、wisdom 属性;
  • 有参构造:直接接收两个Wizard实例(演示程序采用此方式,保证可复现)。

execute(int[] bytecode)方法是解释器的核心循环:逐条读取 bytecode 中的整数,通过Instruction.getInstruction解析出枚举值,再用switch分支执行对应的栈操作:

public void execute(int[] bytecode) { for (var i = 0; i < bytecode.length; i++) { Instruction instruction = Instruction.getInstruction(bytecode[i]); switch (instruction) { case LITERAL -> { // 读取下一个字节作为立即数 int value = bytecode[++i]; stack.push(value); } case SET_HEALTH -> { // 弹出数值与巫师编号,写入属性 var amount = stack.pop(); var wizard = stack.pop(); setHealth(wizard, amount); } case GET_HEALTH -> { // 弹出巫师编号,将属性压栈 var wizard = stack.pop(); stack.push(getHealth(wizard)); } case ADD -> { // 弹出两值求和压栈 var a = stack.pop(); var b = stack.pop(); stack.push(a + b); } case DIVIDE -> { // 弹出两值做除法压栈(注意 b / a) var a = stack.pop(); var b = stack.pop(); stack.push(b / a); } case PLAY_SOUND -> { // 弹出巫师编号,触发行为 var wizard = stack.pop(); getWizards()[wizard].playSound(); } // ... 其余指令分支 default -> throw new IllegalArgumentException("Invalid instruction value"); } LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack()); } }

从源码可看出两点关键设计:其一,栈是后进先出(LIFO)的,SET_*类指令先弹数值再弹巫师编号,对应文本指令中的书写顺序;其二,DIVIDE的分支是stack.push(b / a),即先弹出的作为除数、后弹出的作为被除数,这也解释了演示程序里"先压入 11 再压入 2"能得到 11 ÷ 2 = 5(整数除法向下取整为 5,但示例中 18 ÷ 2 = 9)。每执行一条指令,虚拟机都会打印一次当前栈内容,方便逐条追踪。

4. InstructionConverterUtil——文本指令到字节码的桥梁

为了让"数据驱动的行为"真正可写、可读,仓库提供了一个转换工具 InstructionConverterUtil.java。convertToByteCode(String instructions)把空格分隔的文本指令串转换为int[]字节码数组:

  • 输入为null或空白串时返回空数组;
  • 对每个分词,先尝试按Instruction.valueOf解析为指令名(合法则取其intValue);
  • 再尝试Integer.parseInt解析为立即数(如LITERAL 0中的0);
  • 两者都不匹配时抛出IllegalArgumentException("Invalid instruction or number: " + token)。

例如"LITERAL 0 GET_HEALTH"会被转换为[1, 0, 7]。该行为由 InstructionConverterUtilTest.java 验证:testInstructions断言"LITERAL 35 SET_HEALTH ..."转换出的数组长度与各位置操作码完全符合预期;testEmptyInstruction断言空串转换结果长度为 0。

四、完整可运行示例与栈轨迹解析

以下完整示例来自 App.java(文档亦收录):

public static void main(String[] args) { var vm = new VirtualMachine( new Wizard(45, 7, 11, 0, 0), // 0 号巫师:health=45, agility=7, wisdom=11 new Wizard(36, 18, 8, 0, 0)); // 1 号巫师:health=36, agility=18, wisdom=8 vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM")); vm.execute(InstructionConverterUtil.convertToByteCode("ADD")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2")); vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE")); vm.execute(InstructionConverterUtil.convertToByteCode("ADD")); vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH")); }

这段程序计算的是0 号巫师的综合属性均值:(health + agility + wisdom) / 2 = (45 + 7 + 11) / 2 = 31.5,由于是整数除法(栈上先压 11 后压 2,DIVIDE计算b / a),得到63 / 2 = 31,最终再加回生命值得到新值45 + 31 = 76,写入 0 号巫师的 health。控制台输出(文档原样收录)逐条展示了每一步的栈变化:

16:20:10.193 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0] 16:20:10.196 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_HEALTH, Stack contains [0, 45] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_AGILITY, Stack contains [0, 45, 7] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 7, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_WISDOM, Stack contains [0, 45, 7, 11] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 45, 18] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 18, 2] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed DIVIDE, Stack contains [0, 45, 9] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 54] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed SET_HEALTH, Stack contains []

逐行解读这段输出,可以帮助你理解栈式虚拟机的完整执行过程:

  1. LITERAL 0压入巫师编号 0 → 栈[0];
  2. LITERAL 0压入第一个操作数 0(占位)→ 栈[0, 0];
  3. GET_HEALTH弹出 0 号巫师,压入其 health 45 → 栈[0, 45];
  4. LITERAL 0再次压入巫师编号 → 栈[0, 45, 0];
  5. GET_AGILITY弹出 0 号巫师,压入 agility 7 → 栈[0, 45, 7];
  6. LITERAL 0压入巫师编号 → 栈[0, 45, 7, 0];
  7. GET_WISDOM弹出 0 号巫师,压入 wisdom 11 → 栈[0, 45, 7, 11];
  8. ADD弹出 11 与 7,压入 18 → 栈[0, 45, 18];
  9. LITERAL 2压入除数 2 → 栈[0, 45, 18, 2];
  10. DIVIDE弹出 2 与 18,压入 18/2=9 → 栈[0, 45, 9];
  11. ADD弹出 9 与 45,压入 54 → 栈[0, 54];
  12. SET_HEALTH弹出 54 与 0,执行setHealth(0, 54)→ 栈清空[]。

五、类图与执行时序

模块的类图(bytecode/etc/bytecode.urm.png)展示了App、VirtualMachine、Wizard、Instruction、InstructionConverterUtil之间的协作关系:App依赖VirtualMachine与InstructionConverterUtil;VirtualMachine持有一个Stack<Integer>与Wizard[]数组,并依赖Instruction枚举。

时序图(bytecode/etc/bytecode-sequence-diagram.png)则描绘了运行时消息流:App将文本指令交给InstructionConverterUtil转换为字节码数组,VirtualMachine.execute逐条解析并操作Wizard的属性。

六、什么时候使用 Bytecode 模式

文档指出,当你需要定义大量行为,而游戏所使用的实现语言不适合直接编写这些行为时,应当考虑 Bytecode 模式,典型触发条件如下:

  • 语言层级过低:直接用实现语言编写行为过于繁琐、容易出错;
  • 迭代成本过高:由于编译速度慢或其他工具链问题,反复修改行为需要太长时间;
  • 信任度问题:如果希望确保被定义的行为不可能破坏游戏(沙箱隔离),需要将行为代码与其余代码库隔离。

这三个条件正好对应游戏策划高频调参、需要热更新行为、且希望行为运行在受控环境中的场景。

七、已知的真实应用

文档列举了三种广为人知的 Bytecode 使用案例:

  • Java 虚拟机(JVM):使用 bytecode 让 Java 程序可以在任何安装了 JVM 的设备上运行(这正是"可移植性"的教科书级例子);
  • Python:将脚本编译为 bytecode,再由 Python 虚拟机解释执行;
  • .NET Framework:使用名为 Microsoft Intermediate Language(MSIL)的字节码形式。

这些案例共同印证了 Bytecode 模式"一次编码、处处解释"的价值。

八、后果分析:优点与代价

优点

  • 可移植性(Portability):程序可以在任何装有兼容虚拟机的平台上运行;
  • 安全性(Security):虚拟机可以在解释执行时对字节码实施安全检查(沙箱化);
  • 性能潜力(Performance):JIT(即时编译器)可以在运行时优化字节码,相较纯解释执行可能带来性能提升。

缺点

  • 开销(Overhead):运行字节码通常比运行原生代码有更多额外开销,可能影响性能;
  • 复杂度(Complexity):实现和维护一个虚拟机为系统增加了额外复杂度。

这两点与"数据驱动灵活性"构成了经典的权衡:灵活与安全来自 VM 这一层间接,而其代价正是间接层的开销与维护成本。

九、相关设计模式

文档明确指出 Bytecode 模式与以下三个模式存在紧密联系,本仓库均有对应模块:

  • 解释器(Interpreter):虚拟机实现内部通常使用解释器模式来解析并执行字节码指令——本模块VirtualMachine.execute对Instruction的switch分发就是解释执行的具体形态;
  • 命令(Command):每条字节码指令都可以看作一个由虚拟机执行的动作命令,指令即命令的编码形式;
  • 工厂方法(Factory Method):虚拟机可能使用工厂方法来实例化字节码中定义的操作或指令。

十、测试验证:行为与转换的双重保障

模块的测试代码是理解本模式正确性的最佳佐证,位于 bytecode/src/test/java/com/iluwatar/bytecode/:

  • VirtualMachineTest.java:逐条验证虚拟机行为,例如testLiteral断言执行[LITERAL, 10]后栈中弹出 10;testSetHealth用[LITERAL, 0, LITERAL, 50, SET_HEALTH]断言 0 号巫师 health 变为 50;testPlaySound断言执行后numberOfPlayedSounds递增为 1 且栈被清空;testInvalidInstruction断言操作码 999 触发IllegalArgumentException;
  • InstructionConverterUtilTest.java:验证文本到字节码的转换正确性与空输入处理;
  • AppTest.java:验证主程序可正常启动。

结语

Bytecode 模式的价值在于把行为从"代码"降维成"数据":指令集是语法的边界,栈式虚拟机是执行语义的载体,文本转换工具则是人与机器之间的翻译层。在 java-design-patterns 的bytecode模块中,你可以通过 Wizard.java、Instruction.java、VirtualMachine.java 三份源码完整复现这套机制,再结合测试用例理解其行为契约。当你在游戏或规则引擎项目中遇到"行为需要频繁调整、又希望隔离风险"的需求时,这套数据驱动的虚拟机思路将是一个值得借鉴的架构选择。

  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

相关推荐

上一篇:WordPress 块编辑器 BackgroundImageControl 组件:背景图选择、焦点定位与尺寸控制实战指南
下一篇:WordPress Gutenberg base-styles 包完全指南:从安装用法到样式演进与迁移实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 2:32:50

嵌入式驱动开发实战:从寄存器到Linux内核的完整技术栈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 2:32:18

华为敏捷数据中心双活网络架构实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 2:30:42

轻羽大师:纯本地运行的微信自动化定时群发工具

轻羽大师&#xff1a;纯本地运行的微信自动化定时群发工具你有没有算过&#xff0c;每天有多少时间浪费在重复的微信操作上&#xff1f;复制粘贴同一条消息发给几十个人&#xff0c;手动转发通知到各个群&#xff0c;定好闹钟提醒自己发朋友圈。这些看似不起眼的操作&#xff0…

作者头像 李华
网站建设 2026/9/30 2:30:16

工业视觉实战 + YOLO避坑

YOLOv11 四卡训练卡住不动&#xff0c;切换单卡却正常&#xff1a;一次多 GPU DDP 训练排查记录 一、问题现象 最近在云训练机上训练 YOLOv11 自定义工业缺陷数据集时&#xff0c;我原本准备使用四张 GPU 加快训练&#xff0c;训练指令大致如下&#xff1a; yolo detect tra…

作者头像 李华