Javaにおけるバイトコードパターン:カスタム仮想マシンによる命令の解釈
バイトコードデザインパターンの意図
Javaにおけるバイトコードデザインパターンは、振る舞いを仮想マシンへの命令としてエンコードすることを可能にし、ゲーム開発やその他のアプリケーションにおいて強力なツールとなります。
実際の例を用いたバイトコードパターンの詳細な説明
実際の例
バイトコードデザインパターンの現実世界の類似例は、本を複数の言語に翻訳するプロセスに見ることができます。元の言語から直接すべての他の言語に本を翻訳するのではなく、まず本をエスペラントのような共通の中間言語に翻訳します。この中間バージョンは、よりシンプルで構造化されているため、翻訳が容易です。次に、各ターゲット言語の翻訳者がエスペラントからそれぞれの特定の言語に翻訳します。このアプローチは、一貫性を確保し、エラーを減らし、翻訳プロセスを簡素化します。これは、バイトコードが、異なるプラットフォーム間で高水準プログラミング言語の実行を最適化および容易にするための中間表現として機能するのと同様です。
平易な言葉で言うと
バイトコードパターンは、コードではなくデータによって駆動される振る舞いを可能にします。
gameprogrammingpatterns.comのドキュメントには次のように書かれています。
命令セットは、実行できる低レベルの操作を定義します。一連の命令は、バイトのシーケンスとしてエンコードされます。仮想マシンは、中間値にスタックを使用して、これらの命令を一度に1つずつ実行します。命令を組み合わせることで、複雑な高レベルの振る舞いを定義できます。
Javaにおけるバイトコードパターンのプログラム例
このプログラム例では、Javaにおけるバイトコードパターンが、明確に定義された一連の操作を通じて、複雑な仮想マシン命令の実行をどのように簡略化できるかを示します。この現実世界の例は、Javaにおけるバイトコードデザインパターンが、バイトコード命令を通じてウィザードの振る舞いを簡単に調整できるようにすることで、ゲームプログラミングをどのように効率化できるかを示しています。
チームは、ウィザード同士が戦う新しいゲームに取り組んでいます。ウィザードの振る舞いは、プレイテストを通じて慎重に調整し、何度も繰り返す必要があります。ゲームデザイナーが振る舞いを変更したいときに毎回プログラマーに変更を依頼するのは最適ではないため、ウィザードの振る舞いはデータ駆動の仮想マシンとして実装されます。
最も重要なゲームオブジェクトの1つは、Wizard
クラスです。
@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++;
}
}
次に、仮想マシンで使用できる命令を示します。各命令には、スタックデータでどのように動作するかに関する独自のセマンティクスがあります。たとえば、ADD命令はスタックから上位2つの項目を取得し、それらを加算して結果をスタックにプッシュします。
@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
// Other properties and methods...
}
この例の中心となるのは、VirtualMachine
クラスです。このクラスは、命令を入力として受け取り、それらを実行してゲームオブジェクトの振る舞いを提供します。
@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 properties and methods...
}
これで、仮想マシンを利用した完全な例を示すことができます。
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(String.format(HEALTH_PATTERN, "GET")));
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(String.format(HEALTH_PATTERN, "SET")));
}
以下はコンソール出力です。
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 []
Javaにおけるバイトコードデザインパターンを使用すると、仮想マシンベースのアプリケーションの柔軟性と保守性を大幅に向上させることができます。
Javaでバイトコードパターンを使用する場合
以下の場合にバイトコードパターンを使用します。定義する必要のある動作がたくさんあり、ゲームの実装言語が適切でない場合。
- プログラミングするには低レベルすぎるため、退屈またはエラーが発生しやすい。
- コンパイル時間が遅いなどのツール関連の問題により、反復に時間がかかりすぎる。
- 信頼性が高すぎる。定義されている振る舞いがゲームを壊さないようにしたい場合は、コードベースの残りの部分からサンドボックス化する必要があります。
Javaにおけるバイトコードパターンの実際の応用
- Java仮想マシン(JVM)は、JVMがインストールされているあらゆるデバイスでJavaプログラムを実行できるようにバイトコードを使用します。
- Pythonはスクリプトをバイトコードにコンパイルし、それをPython仮想マシンが解釈します。
- .NET Frameworkは、Microsoft Intermediate Language(MSIL)と呼ばれるバイトコードの形式を使用します。
バイトコードパターンの利点とトレードオフ
利点
- 移植性:互換性のあるVMがあるプラットフォームであれば、どこでもプログラムを実行できます。
- セキュリティ:VMはバイトコードに対してセキュリティチェックを強制できます。
- パフォーマンス:JITコンパイラーは、実行時にバイトコードを最適化できるため、解釈されたコードよりもパフォーマンスが向上する可能性があります。
トレードオフ
- オーバーヘッド:バイトコードの実行には、通常、ネイティブコードの実行よりも多くのオーバーヘッドが発生し、パフォーマンスに影響を与える可能性があります。
- 複雑さ:VMを実装および保守すると、システムの複雑さが増します。
関連するJavaのデザインパターン
- Interpreterは、バイトコード命令を解釈するためにVMの実装内でよく使用されます。
- Command:バイトコード命令は、VMによって実行されるコマンドと見なすことができます。
- Factory Method:VMは、バイトコードで定義された操作または命令をインスタンス化するためにファクトリーメソッドを使用する場合があります。