ファイナルファンタジーやドラゴンクエストのようなターン制RPG。子どもの頃から親しんできたこのシステムを、自分で作ってみたいと思ったことはありませんか。

このシリーズでは、PerlとMooを使ってテキストベースのRPG戦闘エンジンを構築していきます。最初はシンプルな殴り合いから始めて、少しずつ機能を追加していき、最終的には攻撃・防御・魔法・アイテムを使いこなす本格的な戦闘システムを完成させます。
全10回のシリーズを通じて、実践的なオブジェクト指向設計と、GoFデザインパターンの活用方法を体験していただきます。
対象読者
- Perl入学式を卒業したレベルの方
- 「Mooで覚えるオブジェクト指向プログラミング」シリーズを読み終えた方
- RPGやゲーム開発に興味がある方
完成イメージ
最終的には、以下のような対話型のバトルシステムを作ります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| === 戦闘開始! ===
勇者 HP: 100 vs スライム HP: 30
あなたのターンです。
1: 攻撃 2: 防御 3: 魔法 4: アイテム
> 1
勇者の攻撃! スライムに 15 のダメージ!
スライム HP: 15
スライムの攻撃! 勇者に 5 のダメージ!
勇者 HP: 95
あなたのターンです。
...
|
今回はその第一歩として、最もシンプルな「殴り合い」から始めましょう。
Characterクラスを作る
まずは戦闘に参加するキャラクターを表現するクラスを作ります。RPGのキャラクターに必要な最低限の情報を考えてみましょう。
- 名前(name)
- HP(体力)
- 攻撃力(attack_power)
これらをMooで表現します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package Character;
use v5.36;
use Moo;
has name => (
is => 'ro',
required => 1,
);
has hp => (
is => 'rw',
default => 100,
);
has attack_power => (
is => 'ro',
default => 10,
);
|
classDiagram
class Character {
+name : String
+hp : Int
+attack_power : Int
+attack(target)
+is_alive() Boolean
}
nameは必須属性、hpとattack_powerはデフォルト値を持つ属性として定義しました。hpだけは戦闘中に変化するためrw(読み書き可能)としています。
攻撃メソッドを追加する
次に、相手を攻撃するメソッドを追加します。攻撃すると、相手のHPが自分の攻撃力分だけ減少します。
1
2
3
4
5
6
7
| sub attack($self, $target) {
my $damage = $self->attack_power;
my $new_hp = $target->hp - $damage;
$target->hp($new_hp < 0 ? 0 : $new_hp);
say $self->name . "の攻撃! " . $target->name . "に " . $damage . " のダメージ!";
}
|
attackメソッドは攻撃対象($target)を引数に取り、ダメージを与えます。HPがマイナスにならないよう、0以下になった場合は0に補正しています。
生存判定メソッド
キャラクターがまだ生きているかどうかを判定するメソッドも必要です。
1
2
3
| sub is_alive($self) {
return $self->hp > 0;
}
|
HPが0より大きければ生存、そうでなければ戦闘不能です。
戦闘ループを作る
キャラクターができたら、次は戦闘を進行させるループを作ります。ターン制RPGの基本は以下の流れです。
- プレイヤーのターン(攻撃)
- 敵のターン(攻撃)
- どちらかのHPが0になるまで繰り返す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| sub battle($player, $enemy) {
say "=== 戦闘開始! ===";
say $player->name . " HP: " . $player->hp . " vs " . $enemy->name . " HP: " . $enemy->hp;
say "";
while ($player->is_alive && $enemy->is_alive) {
# プレイヤーのターン
$player->attack($enemy);
last unless $enemy->is_alive;
# 敵のターン
$enemy->attack($player);
}
say "";
if ($player->is_alive) {
say $enemy->name . "を倒した!";
} else {
say $player->name . "は倒れた...";
}
}
|
flowchart TD
Start([戦闘開始]) --> CheckAlive{生存確認?}
CheckAlive -- Both Alive --> PlayerTurn[プレイヤーの攻撃]
PlayerTurn --> EnemyAlive{敵生存?}
EnemyAlive -- Yes --> EnemyTurn[敵の攻撃]
EnemyTurn --> CheckAlive
EnemyAlive -- No --> PlayerWin([プレイヤー勝利])
CheckAlive -- Player Dead --> EnemyWin([敵勝利])
last unless $enemy->is_aliveの行がポイントです。プレイヤーの攻撃で敵が倒れた場合、敵のターンをスキップして戦闘を終了します。
完成コード
ここまでの内容をまとめた完成コードです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| #!/usr/bin/env perl
use v5.36;
package Character {
use Moo;
has name => (
is => 'ro',
required => 1,
);
has hp => (
is => 'rw',
default => 100,
);
has attack_power => (
is => 'ro',
default => 10,
);
sub attack($self, $target) {
my $damage = $self->attack_power;
my $new_hp = $target->hp - $damage;
$target->hp($new_hp < 0 ? 0 : $new_hp);
say $self->name . "の攻撃! " . $target->name . "に " . $damage . " のダメージ!";
}
sub is_alive($self) {
return $self->hp > 0;
}
}
sub battle($player, $enemy) {
say "=== 戦闘開始! ===";
say $player->name . " HP: " . $player->hp . " vs " . $enemy->name . " HP: " . $enemy->hp;
say "";
while ($player->is_alive && $enemy->is_alive) {
$player->attack($enemy);
last unless $enemy->is_alive;
$enemy->attack($player);
}
say "";
if ($player->is_alive) {
say $enemy->name . "を倒した!";
} else {
say $player->name . "は倒れた...";
}
}
# メイン処理
my $hero = Character->new(
name => '勇者',
hp => 100,
attack_power => 15,
);
my $slime = Character->new(
name => 'スライム',
hp => 30,
attack_power => 5,
);
battle($hero, $slime);
|
実行結果
1
2
3
4
5
6
7
8
| === 戦闘開始! ===
勇者 HP: 100 vs スライム HP: 30
勇者の攻撃! スライムに 15 のダメージ!
スライムの攻撃! 勇者に 5 のダメージ!
勇者の攻撃! スライムに 15 のダメージ!
スライムを倒した!
|
最小限ですが、ちゃんとターン制の戦闘が動いています。勇者は2ターンでスライムを倒し、自身は5ダメージを受けただけで済みました。
今回のポイント
今回は以下の要素を実装しました。
- Characterクラス: 名前、HP、攻撃力を持つ
- attackメソッド: 相手にダメージを与える
- is_aliveメソッド: 生存判定
- battle関数: プレイヤーと敵が交互に攻撃する戦闘ループ
この時点でのコードはとてもシンプルです。しかし、実際のRPGには攻撃以外にも防御、魔法、アイテム使用など、多くの行動があります。
次回は、この「行動の種類を増やしたい」という要求に応えていきます。行動ごとにクラスを分離し、より柔軟な設計を目指しましょう。
次回: