Compare commits

...

2 Commits

Author SHA1 Message Date
Subhrodip Mohanta
b978c83993
Merge branch 'master' into bytecode-explanation 2021-03-22 12:05:27 +05:30
Ilkka Seppälä
44dbe4374f
#590 add explanation for bytecode pattern 2021-03-21 16:39:56 +02:00
10 changed files with 277 additions and 103 deletions

View File

@ -9,18 +9,234 @@ tags:
---
## Intent
Allows to encode behaviour as instructions for virtual machine.
Allows encoding behavior as instructions for a virtual machine.
## Explanation
Real world example
> A team is working on a new game where wizards battle against each other. The wizard behavior
> needs to be carefully adjusted and iterated hundreds of times through playtesting. It's not
> optimal to ask the programmer to make changes each time the game designer wants to vary the
> behavior, so the wizard behavior is implemented as a data-driven virtual machine.
In plain words
> Bytecode pattern enables behavior driven by data instead of code.
[Gameprogrammingpatterns.com](https://gameprogrammingpatterns.com/bytecode.html) documentation
states:
> An instruction set defines the low-level operations that can be performed. A series of
> instructions is encoded as a sequence of bytes. A virtual machine executes these instructions one
> at a time, using a stack for intermediate values. By combining instructions, complex high-level
> behavior can be defined.
**Programmatic Example**
One of the most important game objects is the `Wizard` class.
```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++;
}
}
```
Next, we show the available instructions for our virtual machine. Each of the instructions has its
own semantics on how it operates with the stack data. For example, the ADD instruction takes the top
two items from the stack, adds them together and pushes the result to the stack.
```java
@AllArgsConstructor
@Getter
public enum Instruction {
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
ADD(10), // e.g. "ADD", pop 2 values, push their sum
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
// ...
}
```
At the heart of our example is the `VirtualMachine` class. It takes instructions as input and
executes them to provide the game object behavior.
```java
@Getter
@Slf4j
public class VirtualMachine {
private final Stack<Integer> stack = new Stack<>();
private final Wizard[] wizards = new Wizard[2];
public VirtualMachine() {
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
}
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
wizards[0] = wizard1;
wizards[1] = wizard2;
}
public void execute(int[] bytecode) {
for (var i = 0; i < bytecode.length; i++) {
Instruction instruction = Instruction.getInstruction(bytecode[i]);
switch (instruction) {
case LITERAL:
// Read the next byte from the bytecode.
int value = bytecode[++i];
// Push the next value to stack
stack.push(value);
break;
case SET_AGILITY:
var amount = stack.pop();
var wizard = stack.pop();
setAgility(wizard, amount);
break;
case SET_WISDOM:
amount = stack.pop();
wizard = stack.pop();
setWisdom(wizard, amount);
break;
case SET_HEALTH:
amount = stack.pop();
wizard = stack.pop();
setHealth(wizard, amount);
break;
case GET_HEALTH:
wizard = stack.pop();
stack.push(getHealth(wizard));
break;
case GET_AGILITY:
wizard = stack.pop();
stack.push(getAgility(wizard));
break;
case GET_WISDOM:
wizard = stack.pop();
stack.push(getWisdom(wizard));
break;
case ADD:
var a = stack.pop();
var b = stack.pop();
stack.push(a + b);
break;
case DIVIDE:
a = stack.pop();
b = stack.pop();
stack.push(b / a);
break;
case PLAY_SOUND:
wizard = stack.pop();
getWizards()[wizard].playSound();
break;
case SPAWN_PARTICLES:
wizard = stack.pop();
getWizards()[wizard].spawnParticles();
break;
default:
throw new IllegalArgumentException("Invalid instruction value");
}
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
}
}
public void setHealth(int wizard, int amount) {
wizards[wizard].setHealth(amount);
}
// other setters ->
// ...
}
```
Now we can show the full example utilizing the virtual machine.
```java
public static void main(String[] args) {
var vm = new VirtualMachine(
new Wizard(45, 7, 11, 0, 0),
new Wizard(36, 18, 8, 0, 0));
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"));
}
```
Here is the console output.
```
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 []
```
## Class diagram
![alt text](./etc/bytecode.urm.png "Bytecode class diagram")
## Applicability
Use the Bytecode pattern when you have a lot of behavior you need to define and your
games implementation language isnt a good fit because:
* its too low-level, making it tedious or error-prone to program in.
* iterating on it takes too long due to slow compile times or other tooling issues.
* it has too much trust. If you want to ensure the behavior being defined cant break the game, you need to sandbox it from the rest of the codebase.
* Its too low-level, making it tedious or error-prone to program in.
* Iterating on it takes too long due to slow compile times or other tooling issues.
* It has too much trust. If you want to ensure the behavior being defined cant break the game, you need to sandbox it from the rest of the codebase.
## Related patterns
* [Interpreter](https://java-design-patterns.com/patterns/interpreter/)
## Credits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.2.3" icons="true" always-add-relationships="false" generalizations="true" realizations="true"
associations="true" dependencies="false" nesting-relationships="true" router="FAN">
<class id="1" language="java" name="com.iluwatar.bytecode.VirtualMachine" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/VirtualMachine.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="455" y="173"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="2" language="java" name="com.iluwatar.bytecode.App" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/App.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="148" y="110"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="3" language="java" name="com.iluwatar.bytecode.Wizard" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="148" y="416"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<association id="4">
<end type="SOURCE" refId="1" navigable="false" variant="ASSOCIATION">
<attribute id="5" name="wizards">
<position height="18" width="48" x="296" y="291"/>
</attribute>
<multiplicity id="6" minimum="0" maximum="2147483647">
<position height="0" width="0" x="-327" y="-27"/>
</multiplicity>
</end>
<end type="TARGET" refId="3" navigable="true" variant="ASSOCIATION"/>
<display labels="true" multiplicity="true"/>
</association>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -3,7 +3,6 @@ package com.iluwatar.bytecode {
class App {
- LOGGER : Logger {static}
+ App()
- interpretInstruction(instruction : String, vm : VirtualMachine) {static}
+ main(args : String[]) {static}
}
enum Instruction {
@ -18,22 +17,25 @@ package com.iluwatar.bytecode {
+ SET_HEALTH {static}
+ SET_WISDOM {static}
+ SPAWN_PARTICLES {static}
- value : int
- intValue : int
+ getInstruction(value : int) : Instruction {static}
+ getIntValue() : int
+ valueOf(name : String) : Instruction {static}
+ values() : Instruction[] {static}
}
class VirtualMachine {
- LOGGER : Logger {static}
- stack : Stack<Integer>
- wizards : Wizard[]
+ VirtualMachine()
+ VirtualMachine(wizard1 : Wizard, wizard2 : Wizard)
+ execute(bytecode : int[])
+ getAgility(wizard : int) : int
+ getHealth(wizard : int) : int
+ getStack() : Stack<Integer>
+ getWisdom(wizard : int) : int
+ getWizards() : Wizard[]
- randomInt(min : int, max : int) : int
+ setAgility(wizard : int, amount : int)
+ setHealth(wizard : int, amount : int)
+ setWisdom(wizard : int, amount : int)
@ -45,7 +47,7 @@ package com.iluwatar.bytecode {
- numberOfPlayedSounds : int
- numberOfSpawnedParticles : int
- wisdom : int
+ Wizard()
+ Wizard(health : int, agility : int, wisdom : int, numberOfPlayedSounds : int, numberOfSpawnedParticles : int)
+ getAgility() : int
+ getHealth() : int
+ getNumberOfPlayedSounds() : int
@ -54,6 +56,8 @@ package com.iluwatar.bytecode {
+ playSound()
+ setAgility(agility : int)
+ setHealth(health : int)
+ setNumberOfPlayedSounds(numberOfPlayedSounds : int)
+ setNumberOfSpawnedParticles(numberOfSpawnedParticles : int)
+ setWisdom(wisdom : int)
+ spawnParticles()
}

View File

@ -49,33 +49,21 @@ public class App {
*/
public static void main(String[] args) {
var wizard = new Wizard();
wizard.setHealth(45);
wizard.setAgility(7);
wizard.setWisdom(11);
var vm = new VirtualMachine(
new Wizard(45, 7, 11, 0, 0),
new Wizard(36, 18, 8, 0, 0));
var vm = new VirtualMachine();
vm.getWizards()[0] = wizard;
String literal = "LITERAL 0";
interpretInstruction(literal, vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_HEALTH", vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_AGILITY", vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_WISDOM ", vm);
interpretInstruction("ADD", vm);
interpretInstruction("LITERAL 2", vm);
interpretInstruction("DIVIDE", vm);
interpretInstruction("ADD", vm);
interpretInstruction("SET_HEALTH", vm);
}
private static void interpretInstruction(String instruction, VirtualMachine vm) {
vm.execute(InstructionConverterUtil.convertToByteCode(instruction));
var stack = vm.getStack();
LOGGER.info(instruction + String.format("%" + (12 - instruction.length()) + "s", "") + stack);
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"));
}
}

View File

@ -33,17 +33,17 @@ import lombok.Getter;
@Getter
public enum Instruction {
LITERAL(1),
SET_HEALTH(2),
SET_WISDOM(3),
SET_AGILITY(4),
PLAY_SOUND(5),
SPAWN_PARTICLES(6),
GET_HEALTH(7),
GET_AGILITY(8),
GET_WISDOM(9),
ADD(10),
DIVIDE(11);
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
ADD(10), // e.g. "ADD", pop 2 values, push their sum
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
private final int intValue;

View File

@ -24,12 +24,15 @@
package com.iluwatar.bytecode;
import java.util.Stack;
import java.util.concurrent.ThreadLocalRandom;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Implementation of virtual machine.
*/
@Getter
@Slf4j
public class VirtualMachine {
private final Stack<Integer> stack = new Stack<>();
@ -37,12 +40,21 @@ public class VirtualMachine {
private final Wizard[] wizards = new Wizard[2];
/**
* Constructor.
* No-args constructor.
*/
public VirtualMachine() {
for (var i = 0; i < wizards.length; i++) {
wizards[i] = new Wizard();
}
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
}
/**
* Constructor taking the wizards as arguments.
*/
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
wizards[0] = wizard1;
wizards[1] = wizard2;
}
/**
@ -57,6 +69,7 @@ public class VirtualMachine {
case LITERAL:
// Read the next byte from the bytecode.
int value = bytecode[++i];
// Push the next value to stack
stack.push(value);
break;
case SET_AGILITY:
@ -107,6 +120,7 @@ public class VirtualMachine {
default:
throw new IllegalArgumentException("Invalid instruction value");
}
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
}
}
@ -133,4 +147,8 @@ public class VirtualMachine {
public int getAgility(int wizard) {
return wizards[wizard].getAgility();
}
private int randomInt(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
}

View File

@ -23,6 +23,7 @@
package com.iluwatar.bytecode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -31,16 +32,15 @@ import lombok.extern.slf4j.Slf4j;
* This class represent game objects which properties can be changed by instructions interpreted by
* virtual machine.
*/
@AllArgsConstructor
@Setter
@Getter
@Slf4j
public class Wizard {
private int health;
private int agility;
private int wisdom;
private int numberOfPlayedSounds;
private int numberOfSpawnedParticles;
@ -53,5 +53,4 @@ public class Wizard {
LOGGER.info("Spawning particles");
numberOfSpawnedParticles++;
}
}

View File

@ -73,6 +73,4 @@ public class InstructionConverterUtil {
return false;
}
}
}