Skip to content

Commit c413e09

Browse files
iluwatarohbus
andauthored
docs: iluwatar#590 add explanation for bytecode pattern (iluwatar#1687)
Type: docs and refactoring Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
1 parent 7ac468d commit c413e09

File tree

10 files changed

+277
-103
lines changed

10 files changed

+277
-103
lines changed

bytecode/README.md

Lines changed: 220 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,234 @@ tags:
99
---
1010

1111
## Intent
12-
Allows to encode behaviour as instructions for virtual machine.
12+
13+
Allows encoding behavior as instructions for a virtual machine.
14+
15+
## Explanation
16+
17+
Real world example
18+
19+
> A team is working on a new game where wizards battle against each other. The wizard behavior
20+
> needs to be carefully adjusted and iterated hundreds of times through playtesting. It's not
21+
> optimal to ask the programmer to make changes each time the game designer wants to vary the
22+
> behavior, so the wizard behavior is implemented as a data-driven virtual machine.
23+
24+
In plain words
25+
26+
> Bytecode pattern enables behavior driven by data instead of code.
27+
28+
[Gameprogrammingpatterns.com](https://gameprogrammingpatterns.com/bytecode.html) documentation
29+
states:
30+
31+
> An instruction set defines the low-level operations that can be performed. A series of
32+
> instructions is encoded as a sequence of bytes. A virtual machine executes these instructions one
33+
> at a time, using a stack for intermediate values. By combining instructions, complex high-level
34+
> behavior can be defined.
35+
36+
**Programmatic Example**
37+
38+
One of the most important game objects is the `Wizard` class.
39+
40+
```java
41+
@AllArgsConstructor
42+
@Setter
43+
@Getter
44+
@Slf4j
45+
public class Wizard {
46+
47+
private int health;
48+
private int agility;
49+
private int wisdom;
50+
private int numberOfPlayedSounds;
51+
private int numberOfSpawnedParticles;
52+
53+
public void playSound() {
54+
LOGGER.info("Playing sound");
55+
numberOfPlayedSounds++;
56+
}
57+
58+
public void spawnParticles() {
59+
LOGGER.info("Spawning particles");
60+
numberOfSpawnedParticles++;
61+
}
62+
}
63+
```
64+
65+
Next, we show the available instructions for our virtual machine. Each of the instructions has its
66+
own semantics on how it operates with the stack data. For example, the ADD instruction takes the top
67+
two items from the stack, adds them together and pushes the result to the stack.
68+
69+
```java
70+
@AllArgsConstructor
71+
@Getter
72+
public enum Instruction {
73+
74+
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
75+
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
76+
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
77+
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
78+
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
79+
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
80+
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
81+
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
82+
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
83+
ADD(10), // e.g. "ADD", pop 2 values, push their sum
84+
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
85+
// ...
86+
}
87+
```
88+
89+
At the heart of our example is the `VirtualMachine` class. It takes instructions as input and
90+
executes them to provide the game object behavior.
91+
92+
```java
93+
@Getter
94+
@Slf4j
95+
public class VirtualMachine {
96+
97+
private final Stack<Integer> stack = new Stack<>();
98+
99+
private final Wizard[] wizards = new Wizard[2];
100+
101+
public VirtualMachine() {
102+
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
103+
0, 0);
104+
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
105+
0, 0);
106+
}
107+
108+
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
109+
wizards[0] = wizard1;
110+
wizards[1] = wizard2;
111+
}
112+
113+
public void execute(int[] bytecode) {
114+
for (var i = 0; i < bytecode.length; i++) {
115+
Instruction instruction = Instruction.getInstruction(bytecode[i]);
116+
switch (instruction) {
117+
case LITERAL:
118+
// Read the next byte from the bytecode.
119+
int value = bytecode[++i];
120+
// Push the next value to stack
121+
stack.push(value);
122+
break;
123+
case SET_AGILITY:
124+
var amount = stack.pop();
125+
var wizard = stack.pop();
126+
setAgility(wizard, amount);
127+
break;
128+
case SET_WISDOM:
129+
amount = stack.pop();
130+
wizard = stack.pop();
131+
setWisdom(wizard, amount);
132+
break;
133+
case SET_HEALTH:
134+
amount = stack.pop();
135+
wizard = stack.pop();
136+
setHealth(wizard, amount);
137+
break;
138+
case GET_HEALTH:
139+
wizard = stack.pop();
140+
stack.push(getHealth(wizard));
141+
break;
142+
case GET_AGILITY:
143+
wizard = stack.pop();
144+
stack.push(getAgility(wizard));
145+
break;
146+
case GET_WISDOM:
147+
wizard = stack.pop();
148+
stack.push(getWisdom(wizard));
149+
break;
150+
case ADD:
151+
var a = stack.pop();
152+
var b = stack.pop();
153+
stack.push(a + b);
154+
break;
155+
case DIVIDE:
156+
a = stack.pop();
157+
b = stack.pop();
158+
stack.push(b / a);
159+
break;
160+
case PLAY_SOUND:
161+
wizard = stack.pop();
162+
getWizards()[wizard].playSound();
163+
break;
164+
case SPAWN_PARTICLES:
165+
wizard = stack.pop();
166+
getWizards()[wizard].spawnParticles();
167+
break;
168+
default:
169+
throw new IllegalArgumentException("Invalid instruction value");
170+
}
171+
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
172+
}
173+
}
174+
175+
public void setHealth(int wizard, int amount) {
176+
wizards[wizard].setHealth(amount);
177+
}
178+
// other setters ->
179+
// ...
180+
}
181+
```
182+
183+
Now we can show the full example utilizing the virtual machine.
184+
185+
```java
186+
public static void main(String[] args) {
187+
188+
var vm = new VirtualMachine(
189+
new Wizard(45, 7, 11, 0, 0),
190+
new Wizard(36, 18, 8, 0, 0));
191+
192+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
193+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
194+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH"));
195+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
196+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY"));
197+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
198+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM"));
199+
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
200+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2"));
201+
vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE"));
202+
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
203+
vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH"));
204+
}
205+
```
206+
207+
Here is the console output.
208+
209+
```
210+
16:20:10.193 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0]
211+
16:20:10.196 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 0]
212+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_HEALTH, Stack contains [0, 45]
213+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 0]
214+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_AGILITY, Stack contains [0, 45, 7]
215+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 7, 0]
216+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_WISDOM, Stack contains [0, 45, 7, 11]
217+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 45, 18]
218+
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 18, 2]
219+
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed DIVIDE, Stack contains [0, 45, 9]
220+
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 54]
221+
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed SET_HEALTH, Stack contains []
222+
```
13223

14224
## Class diagram
225+
15226
![alt text](./etc/bytecode.urm.png "Bytecode class diagram")
16227

17228
## Applicability
229+
18230
Use the Bytecode pattern when you have a lot of behavior you need to define and your
19231
game’s implementation language isn’t a good fit because:
20232

21-
* it’s too low-level, making it tedious or error-prone to program in.
22-
* iterating on it takes too long due to slow compile times or other tooling issues.
23-
* it has too much trust. If you want to ensure the behavior being defined can’t break the game, you need to sandbox it from the rest of the codebase.
233+
* It’s too low-level, making it tedious or error-prone to program in.
234+
* Iterating on it takes too long due to slow compile times or other tooling issues.
235+
* It has too much trust. If you want to ensure the behavior being defined can’t break the game, you need to sandbox it from the rest of the codebase.
236+
237+
## Related patterns
238+
239+
* [Interpreter](https://java-design-patterns.com/patterns/interpreter/)
24240

25241
## Credits
26242

bytecode/etc/bytecode.png

-19.4 KB
Binary file not shown.

bytecode/etc/bytecode.ucls

Lines changed: 0 additions & 49 deletions
This file was deleted.

bytecode/etc/bytecode.urm.png

17.4 KB
Loading

bytecode/etc/bytecode.urm.puml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.iluwatar.bytecode {
33
class App {
44
- LOGGER : Logger {static}
55
+ App()
6-
- interpretInstruction(instruction : String, vm : VirtualMachine) {static}
76
+ main(args : String[]) {static}
87
}
98
enum Instruction {
@@ -18,22 +17,25 @@ package com.iluwatar.bytecode {
1817
+ SET_HEALTH {static}
1918
+ SET_WISDOM {static}
2019
+ SPAWN_PARTICLES {static}
21-
- value : int
20+
- intValue : int
2221
+ getInstruction(value : int) : Instruction {static}
2322
+ getIntValue() : int
2423
+ valueOf(name : String) : Instruction {static}
2524
+ values() : Instruction[] {static}
2625
}
2726
class VirtualMachine {
27+
- LOGGER : Logger {static}
2828
- stack : Stack<Integer>
2929
- wizards : Wizard[]
3030
+ VirtualMachine()
31+
+ VirtualMachine(wizard1 : Wizard, wizard2 : Wizard)
3132
+ execute(bytecode : int[])
3233
+ getAgility(wizard : int) : int
3334
+ getHealth(wizard : int) : int
3435
+ getStack() : Stack<Integer>
3536
+ getWisdom(wizard : int) : int
3637
+ getWizards() : Wizard[]
38+
- randomInt(min : int, max : int) : int
3739
+ setAgility(wizard : int, amount : int)
3840
+ setHealth(wizard : int, amount : int)
3941
+ setWisdom(wizard : int, amount : int)
@@ -45,7 +47,7 @@ package com.iluwatar.bytecode {
4547
- numberOfPlayedSounds : int
4648
- numberOfSpawnedParticles : int
4749
- wisdom : int
48-
+ Wizard()
50+
+ Wizard(health : int, agility : int, wisdom : int, numberOfPlayedSounds : int, numberOfSpawnedParticles : int)
4951
+ getAgility() : int
5052
+ getHealth() : int
5153
+ getNumberOfPlayedSounds() : int
@@ -54,6 +56,8 @@ package com.iluwatar.bytecode {
5456
+ playSound()
5557
+ setAgility(agility : int)
5658
+ setHealth(health : int)
59+
+ setNumberOfPlayedSounds(numberOfPlayedSounds : int)
60+
+ setNumberOfSpawnedParticles(numberOfSpawnedParticles : int)
5761
+ setWisdom(wisdom : int)
5862
+ spawnParticles()
5963
}

bytecode/src/main/java/com/iluwatar/bytecode/App.java

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,33 +49,21 @@ public class App {
4949
*/
5050
public static void main(String[] args) {
5151

52-
var wizard = new Wizard();
53-
wizard.setHealth(45);
54-
wizard.setAgility(7);
55-
wizard.setWisdom(11);
52+
var vm = new VirtualMachine(
53+
new Wizard(45, 7, 11, 0, 0),
54+
new Wizard(36, 18, 8, 0, 0));
5655

57-
var vm = new VirtualMachine();
58-
vm.getWizards()[0] = wizard;
59-
60-
String literal = "LITERAL 0";
61-
62-
interpretInstruction(literal, vm);
63-
interpretInstruction(literal, vm);
64-
interpretInstruction("GET_HEALTH", vm);
65-
interpretInstruction(literal, vm);
66-
interpretInstruction("GET_AGILITY", vm);
67-
interpretInstruction(literal, vm);
68-
interpretInstruction("GET_WISDOM ", vm);
69-
interpretInstruction("ADD", vm);
70-
interpretInstruction("LITERAL 2", vm);
71-
interpretInstruction("DIVIDE", vm);
72-
interpretInstruction("ADD", vm);
73-
interpretInstruction("SET_HEALTH", vm);
74-
}
75-
76-
private static void interpretInstruction(String instruction, VirtualMachine vm) {
77-
vm.execute(InstructionConverterUtil.convertToByteCode(instruction));
78-
var stack = vm.getStack();
79-
LOGGER.info(instruction + String.format("%" + (12 - instruction.length()) + "s", "") + stack);
56+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
57+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
58+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH"));
59+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
60+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY"));
61+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
62+
vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM"));
63+
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
64+
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2"));
65+
vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE"));
66+
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
67+
vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH"));
8068
}
8169
}

0 commit comments

Comments
 (0)