Featured image of post 【第1回】まずは殴り合いから - PerlとMooでテキストRPG戦闘エンジンを作ろう

【第1回】まずは殴り合いから - PerlとMooでテキストRPG戦闘エンジンを作ろう

PerlとMooでテキストRPG戦闘エンジンを作成するシリーズ第1回。プレイヤーと敵が交互に攻撃する最小限の戦闘ループを実装します。Characterクラスを定義し、ターン制バトルの基礎を学びましょう。

ファイナルファンタジーやドラゴンクエストのようなターン制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は必須属性、hpattack_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の基本は以下の流れです。

  1. プレイヤーのターン(攻撃)
  2. 敵のターン(攻撃)
  3. どちらかの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には攻撃以外にも防御、魔法、アイテム使用など、多くの行動があります。

次回は、この「行動の種類を増やしたい」という要求に応えていきます。行動ごとにクラスを分離し、より柔軟な設計を目指しましょう。


次回:

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。