Featured image of post 【第5回】完成!弾幕シューティングエンジン

【第5回】完成!弾幕シューティングエンジン

BattleFieldクラスで弾の位置を管理し、毎フレーム更新・描画。完成した弾幕シューティングエンジンの全体像をお見せします。

いよいよ最終段階です!これまで作ってきたクラスを統合して、「弾幕シューティングエンジン」を完成させましょう。

完成した弾幕シューティングエンジン

これまでの振り返り

作ったもの役割
第1回Bullet クラス(素朴版)問題発見:メモリ爆発
第2回BulletType クラス内部状態(形状、色、サイズ)を管理
第3回BulletFactory クラスオブジェクトプール管理
第4回render($x, $y) メソッド外部状態を渡して描画

今回作るもの:

  • BattleField クラス:弾幕全体を管理するクラス
  • 弾の生成、移動、描画を一元管理
  • 完成した弾幕シューティングエンジン

BattleFieldクラスの設計

BattleField クラスは、弾幕シューティングの「戦場」を表します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package BattleField {
    use Moo;

    has width   => (is => 'ro', required => 1);  # 画面幅
    has height  => (is => 'ro', required => 1);  # 画面高さ
    has factory => (is => 'ro', required => 1);  # BulletFactory
    has bullets => (is => 'ro', default => sub { [] });  # 弾の配列

    sub spawn { ... }   # 弾を生成
    sub update { ... }  # 弾を移動
    sub render { ... }  # 弾を描画
    sub run { ... }     # ゲームループ
}

完成版コード

それでは、完成版のコードをお見せします:

  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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env perl
use v5.36;
use Time::HiRes qw(sleep);

# ============================================================
# BulletType: 弾の種類(内部状態)
# ============================================================
package BulletType {
    use Moo;

    has name  => (is => 'ro', required => 1);
    has char  => (is => 'ro', required => 1);
    has color => (is => 'ro', default => 'white');

    sub render($self, $x, $y, $screen) {
        my $ix = int($x);
        my $iy = int($y);
        return if $iy < 0 || $iy >= @$screen;
        return if $ix < 0 || $ix >= length($screen->[$iy]);
        substr($screen->[$iy], $ix, 1) = $self->char;
    }
}

# ============================================================
# BulletFactory: 弾の種類を管理(オブジェクトプール)
# ============================================================
package BulletFactory {
    use Moo;

    has _cache => (is => 'ro', default => sub { {} });

    has _definitions => (
        is => 'ro',
        default => sub {
            {
                circle => { name => 'circle', char => '●' },
                star   => { name => 'star',   char => '★' },
                dot    => { name => 'dot',    char => '・' },
                arrow  => { name => 'arrow',  char => '→' },
                wave   => { name => 'wave',   char => '〜' },
            }
        },
    );

    sub get($self, $key) {
        my $cache = $self->_cache;
        my $defs  = $self->_definitions;
        $cache->{$key} //= BulletType->new(%{$defs->{$key}});
    }

    sub count($self) {
        scalar keys %{$self->_cache};
    }

    sub types($self) {
        sort keys %{$self->_cache};
    }
}

# ============================================================
# BattleField: 戦場を管理(弾の生成・移動・描画)
# ============================================================
package BattleField {
    use Moo;

    has width   => (is => 'ro', required => 1);
    has height  => (is => 'ro', required => 1);
    has factory => (is => 'ro', required => 1);
    has bullets => (is => 'ro', default => sub { [] });

    # 弾を生成
    sub spawn($self, $type_key, $x, $y, $vx, $vy) {
        push @{$self->bullets}, {
            type => $self->factory->get($type_key),
            x    => $x,
            y    => $y,
            vx   => $vx,
            vy   => $vy,
        };
    }

    # 弾幕パターン: 放射状
    sub spawn_radial($self, $type_key, $cx, $cy, $count, $speed) {
        for my $i (0 .. $count - 1) {
            my $angle = $i * (360 / $count) * 3.14159 / 180;
            $self->spawn(
                $type_key,
                $cx, $cy,
                cos($angle) * $speed,
                sin($angle) * $speed * 0.5,
            );
        }
    }

    # 弾幕パターン: 螺旋
    sub spawn_spiral($self, $type_key, $cx, $cy, $waves, $per_wave, $speed) {
        for my $wave (0 .. $waves - 1) {
            for my $i (0 .. $per_wave - 1) {
                my $angle = ($i * (360 / $per_wave) + $wave * 15) * 3.14159 / 180;
                push @{$self->bullets}, {
                    type => $self->factory->get($type_key),
                    x    => $cx,
                    y    => $cy,
                    vx   => cos($angle) * $speed,
                    vy   => sin($angle) * $speed * 0.5,
                    born => $wave * 2,  # 遅延生成
                };
            }
        }
    }

    # 弾を移動
    sub update($self, $frame) {
        my @alive;
        for my $b (@{$self->bullets}) {
            # 遅延生成チェック
            next if defined $b->{born} && $frame < $b->{born};

            # 移動
            $b->{x} += $b->{vx};
            $b->{y} += $b->{vy};

            # 画面内なら生存
            if ($b->{x} >= -5 && $b->{x} < $self->width + 5 &&
                $b->{y} >= -5 && $b->{y} < $self->height + 5) {
                push @alive, $b;
            }
        }
        @{$self->bullets} = @alive;
    }

    # 描画
    sub render($self, $frame) {
        my @screen = map { " " x $self->width } (1 .. $self->height);

        for my $b (@{$self->bullets}) {
            next if defined $b->{born} && $frame < $b->{born};
            $b->{type}->render($b->{x}, $b->{y}, \@screen);
        }

        return \@screen;
    }

    # 統計を表示
    sub stats($self) {
        my $bullet_count = scalar @{$self->bullets};
        my $type_count = $self->factory->count;
        return "弾: $bullet_count 発 / BulletType: $type_count 種類";
    }
}

# ============================================================
# メイン処理
# ============================================================
my $WIDTH  = 60;
my $HEIGHT = 25;
my $FRAMES = 20;

my $factory = BulletFactory->new;
my $field = BattleField->new(
    width   => $WIDTH,
    height  => $HEIGHT,
    factory => $factory,
);

# 弾幕を生成
my $cx = $WIDTH / 2;
my $cy = $HEIGHT / 2;

# 3種類の弾幕パターンを重ねる
$field->spawn_spiral('circle', $cx, $cy, 3, 12, 1.5);
$field->spawn_spiral('star',   $cx, $cy, 2, 8,  1.2);
$field->spawn_radial('dot',    $cx, $cy, 16, 2.0);

# 初期統計
say "=== 弾幕シューティングエンジン ===";
say $field->stats;
say "";
say "使用中のBulletType: " . join(", ", $factory->types);
say "";
say "アニメーション開始...";
sleep(1);

# ゲームループ
print "\e[2J";
for my $frame (0 .. $FRAMES) {
    my $screen = $field->render($frame);

    print "\e[H";
    for my $row (@$screen) {
        say $row;
    }

    say "-" x $WIDTH;
    say "Frame: $frame / $FRAMES | " . $field->stats;

    $field->update($frame);
    sleep(0.15);
}

# 最終統計
print "\e[" . ($HEIGHT + 3) . ";1H";
say "";
say "=== 完成! ===";
say "弾幕シューティングエンジンが動作しました。";
say "";
say "ポイント:";
say "  ✓ 大量の弾を少数のBulletTypeオブジェクトで管理";
say "  ✓ BulletFactoryでオブジェクトプールを実現";
say "  ✓ BattleFieldで弾の生成・移動・描画を一元管理";

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
=== 弾幕シューティングエンジン ===
弾: 68 発 / BulletType: 3 種類

使用中のBulletType: circle, dot, star

アニメーション開始...

(アニメーションが表示される)

=== 完成! ===
弾幕シューティングエンジンが動作しました。

ポイント:
  ✓ 大量の弾を少数のBulletTypeオブジェクトで管理
  ✓ BulletFactoryでオブジェクトプールを実現
  ✓ BattleFieldで弾の生成・移動・描画を一元管理

クラス構成図

完成したシステムの全体像をまとめます:

	block-beta
    columns 1
    block:battlefield["BattleField"]
        columns 1
        bullets["bullets: [{type, x, y, vx, vy}, ...]<br>外部状態(位置・速度)の配列"]
        space
        block:factory["BulletFactory"]
            columns 1
            cache["_cache: {circle => ●, star => ★, dot => ・}<br>内部状態のオブジェクトプール"]
        end
        space
        block:types
            columns 3
            circle["● circle<br>共有!"]
            star["★ star<br>共有!"]
            dot["・ dot<br>共有!"]
        end
        typelabel["BulletType(3つだけ)"]
    end
    bullets --> factory
    factory --> types

次回予告

弾幕シューティングエンジンが完成しました!

次回(最終回)では、私たちが作ってきたこのシステムが、実は有名なデザインパターンの一つだったことを明かします。そのパターンの名前は…?

お楽しみに!

今回のまとめ

  • BattleField クラスで弾幕全体を管理
  • spawn_radialspawn_spiral で弾幕パターンを生成
  • update で弾を移動、render で描画
  • 68発の弾を3つの BulletType オブジェクトで管理
  • 完成した弾幕シューティングエンジン!
  • 次回:このパターンの正体を明かす

今回の完成コード

(上記の完成版コードと同じ)

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