Featured image of post 【第2回】行動の種類を増やしたい - PerlとMooでテキストRPG戦闘エンジンを作ろう

【第2回】行動の種類を増やしたい - PerlとMooでテキストRPG戦闘エンジンを作ろう

RPG戦闘エンジン第2回。攻撃だけでなく防御やアイテム使用など、行動の種類を増やします。Actionクラスを設計し、各行動を独立したクラスとして分離する方法を学びます。

前回は、プレイヤーと敵が交互に攻撃するシンプルな戦闘ループを作りました。しかし実際のRPGでは、攻撃以外にもさまざまな行動があります。

  • 防御: 次のターン、受けるダメージを半減させる
  • アイテム使用: ポーションでHPを回復する
  • 魔法: MPを消費して強力な攻撃を放つ

今回は、これらの行動をどう実装するか考えていきましょう。

行動をクラスとして分離するイメージ

行動クラスを分離する

現在のattackメソッドはCharacterクラスの中に直接書かれています。新しい行動を追加するたびにCharacterクラスにメソッドを追加していくと、クラスが肥大化してしまいます。

そこで、行動を表す専用のクラスを作ることにします。まずは基本となるActionクラスを定義しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package Action;
use v5.36;
use Moo;

has actor => (
    is       => 'ro',
    required => 1,
);

sub execute($self, $target) {
    die "execute() must be implemented by subclass";
}

actorは行動を実行するキャラクター、executeは実際の行動を実行するメソッドです。executeは基底クラスでは実装せず、サブクラスで具体的な処理を定義します。

攻撃アクションを作る

Actionを継承して攻撃アクションを作ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package AttackAction;
use v5.36;
use Moo;

extends 'Action';

sub execute($self, $target) {
    my $actor = $self->actor;
    my $damage = $actor->attack_power;
    my $new_hp = $target->hp - $damage;
    $target->hp($new_hp < 0 ? 0 : $new_hp);
    
    say $actor->name . "の攻撃! " . $target->name . "に " . $damage . " のダメージ!";
}

前回Characterクラスにあったattackメソッドの処理を、AttackActionクラスのexecuteメソッドに移動しました。

防御アクションを作る

次に防御アクションを作ります。防御すると、次に受けるダメージが半減します。この状態を表現するために、Characterクラスにis_defendingフラグを追加します。

また、回復量を安全に上限でクリップできるよう、max_hpも持たせておきます。

 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
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,
);

has max_hp => (
    is      => 'ro',
    default => 100,
);

has is_defending => (
    is      => 'rw',
    default => 0,
);

sub is_alive($self) {
    return $self->hp > 0;
}

sub take_damage($self, $damage) {
    my $actual_damage = $self->is_defending ? int($damage / 2) : $damage;
    my $new_hp = $self->hp - $actual_damage;
    $self->hp($new_hp < 0 ? 0 : $new_hp);
    $self->is_defending(0);  # 防御状態をリセット
    return $actual_damage;
}

take_damageメソッドを追加し、ダメージ計算を一箇所にまとめました。防御中の場合はダメージを半減し、ダメージを受けた後は防御状態をリセットします。

HPを回復するときの上限値としてmax_hpを使うことで、魔法の数字(ハードコードされた100)を避けています。

防御アクションは以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package DefendAction;
use v5.36;
use Moo;

extends 'Action';

sub execute($self, $target) {
    my $actor = $self->actor;
    $actor->is_defending(1);
    say $actor->name . "は防御の構えをとった!";
}

DefendActionexecuteでは、ターゲットではなく自分自身の防御フラグを立てます。

アイテム使用アクションを作る

HPを回復するポーションを使うアクションも作ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package ItemAction;
use v5.36;
use Moo;

extends 'Action';

has item_name => (
    is      => 'ro',
    default => 'ポーション',
);

has heal_amount => (
    is      => 'ro',
    default => 30,
);

sub execute($self, $target) {
    my $actor = $self->actor;
    my $max_hp = 100;  # 最大HPを仮定
    my $new_hp = $actor->hp + $self->heal_amount;
    $actor->hp($new_hp > $max_hp ? $max_hp : $new_hp);
    
    say $actor->name . "は" . $self->item_name . "を使った! HPが " . $self->heal_amount . " 回復!";
}

ItemActionにはitem_nameheal_amount属性を追加し、アイテムの種類と回復量を設定できるようにしました。

max_hpを参照するので、キャラクターごとにHP上限を変えてもコード変更は不要です。

攻撃アクションの修正

AttackActiontake_damageメソッドを使うように修正しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package AttackAction;
use v5.36;
use Moo;

extends 'Action';

sub execute($self, $target) {
    my $actor = $self->actor;
    my $damage = $actor->attack_power;
    my $actual = $target->take_damage($damage);
    
    my $msg = $actor->name . "の攻撃! " . $target->name . "に " . $actual . " のダメージ!";
    $msg .= "(防御で軽減)" if $actual < $damage;
    say $msg;
}

防御によってダメージが軽減された場合、その旨を表示するようにしました。

行動を使って戦闘する

これらのアクションを使って戦闘を進めてみましょう。

1
2
3
4
5
6
7
# プレイヤーは攻撃
my $player_action = AttackAction->new(actor => $hero);
$player_action->execute($slime);

# 敵も攻撃
my $enemy_action = AttackAction->new(actor => $slime);
$enemy_action->execute($hero);

行動をオブジェクトとして扱うことで、どの行動を実行するかを柔軟に切り替えられるようになりました。

完成コード

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/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,
    );
    
    has is_defending => (
        is      => 'rw',
        default => 0,
    );
    
    sub is_alive($self) {
        return $self->hp > 0;
    }
    
    sub take_damage($self, $damage) {
        my $actual_damage = $self->is_defending ? int($damage / 2) : $damage;
        my $new_hp = $self->hp - $actual_damage;
        $self->hp($new_hp < 0 ? 0 : $new_hp);
        $self->is_defending(0);
        return $actual_damage;
    }
}

package Action {
    use Moo;
    
    has actor => (
        is       => 'ro',
        required => 1,
    );
    
    sub execute($self, $target) {
        die "execute() must be implemented by subclass";
    }
}

package AttackAction {
    use Moo;
    extends 'Action';
    
    sub execute($self, $target) {
        my $actor = $self->actor;
        my $damage = $actor->attack_power;
        my $actual = $target->take_damage($damage);
        
        my $msg = $actor->name . "の攻撃! " . $target->name . "に " . $actual . " のダメージ!";
        $msg .= "(防御で軽減)" if $actual < $damage;
        say $msg;
    }
}

package DefendAction {
    use Moo;
    extends 'Action';
    
    sub execute($self, $target) {
        my $actor = $self->actor;
        $actor->is_defending(1);
        say $actor->name . "は防御の構えをとった!";
    }
}

package ItemAction {
    use Moo;
    extends 'Action';
    
    has item_name => (
        is      => 'ro',
        default => 'ポーション',
    );
    
    has heal_amount => (
        is      => 'ro',
        default => 30,
    );
    
    sub execute($self, $target) {
        my $actor = $self->actor;
        my $new_hp = $actor->hp + $self->heal_amount;
        $actor->hp($new_hp > $actor->max_hp ? $actor->max_hp : $new_hp);
        
        say $actor->name . "は" . $self->item_name . "を使った! HPが " . $self->heal_amount . " 回復!";
    }
}

# メイン処理
my $hero = Character->new(
    name         => '勇者',
    hp           => 100,
    attack_power => 15,
);

my $slime = Character->new(
    name         => 'スライム',
    hp           => 50,
    attack_power => 10,
);

say "=== 戦闘デモ ===";
say "勇者 HP: " . $hero->hp . "  スライム HP: " . $slime->hp;
say "";

# ターン1: 勇者は攻撃、スライムは防御
AttackAction->new(actor => $hero)->execute($slime);
DefendAction->new(actor => $slime)->execute($slime);

say "";

# ターン2: 勇者は攻撃(スライムは防御中)、スライムはアイテム
AttackAction->new(actor => $hero)->execute($slime);
ItemAction->new(actor => $slime, heal_amount => 20)->execute($slime);

say "";
say "最終状態: 勇者 HP: " . $hero->hp . "  スライム HP: " . $slime->hp;

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
=== 戦闘デモ ===
勇者 HP: 100  スライム HP: 50

勇者の攻撃! スライムに 15 のダメージ!
スライムは防御の構えをとった!

勇者の攻撃! スライムに 7 のダメージ!(防御で軽減)
スライムはポーションを使った! HPが 20 回復!

最終状態: 勇者 HP: 100  スライム HP: 48

防御によってダメージが15から7に軽減されていること、アイテムでHPが回復していることが確認できます。

今回のポイント

  • 行動をActionクラスとして分離した
  • AttackActionDefendActionItemActionを作成した
  • Characterにダメージ処理を集約するtake_damageメソッドを追加した
  • 防御状態をis_defendingフラグで管理した

行動がクラスとして分離されたことで、新しい行動を追加しやすくなりました。しかし、現在の実装ではどの行動を実行するかをコードに直接書いています。

次回は、プレイヤーの入力に応じて行動を切り替える処理を追加してみましょう。そこで見えてくる問題点と、その解決策について考えていきます。


前回:

次回:

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