Featured image of post 【第4回】外部状態を渡して弾幕を描こう

【第4回】外部状態を渡して弾幕を描こう

renderメソッドに位置を引数で渡して描画。共有オブジェクトと外部状態を組み合わせて、弾幕を効率的に描画する方法を学びます。

前回、BulletFactory で弾の種類を効率的に管理できるようになりました。今回は、実際に弾を動かして描画する方法を学びます。

ポイントは「外部状態を渡す操作」です。共有オブジェクト(BulletType)に、描画時に位置情報を渡して描画します。

外部状態を渡して描画

前回の振り返り

前回達成したこと:

  • BulletFactory クラスでオブジェクトプールを実装
  • //= 演算子でキャッシュ機構を構築
  • 1000発の弾を5つの BulletType オブジェクトで管理

今回やること:

  • render($x, $y) メソッドで外部状態を受け取る
  • 弾を動かしてアニメーション風に描画
  • 弾幕らしい見た目を実現

外部状態を渡すとは?

これまでのコードでも、すでに外部状態を渡していました:

1
2
3
# BulletType自体は位置を知らない
# 描画時に「外から」位置を渡す
$bullet_type->render($x, $y);

この設計のメリットは何でしょうか?

設計メリットデメリット
BulletTypeが位置を持つシンプルオブジェクトを共有できない
位置を外から渡すオブジェクトを共有できる呼び出し時に位置を管理する必要がある

位置を外から渡すことで、同じ BulletType オブジェクトを複数の弾で共有できるのです。

弾を動かす

弾には速度(vx, vy)があります。毎フレーム、位置を更新して描画してみましょう:

 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
#!/usr/bin/env perl
use v5.36;
use Time::HiRes qw(sleep);

package BulletType {
    use Moo;

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

    sub render($self, $x, $y, $width, $height) {
        # 画面外なら描画しない
        return if $x < 0 || $x >= $width || $y < 0 || $y >= $height;
        my $char = $self->char;
        print "\e[${y};${x}H$char";  # ANSIエスケープで位置指定
    }
}

package BulletFactory {
    use Moo;

    has _cache => (is => 'ro', default => sub { {} });
    has _definitions => (
        is => 'ro',
        default => sub {
            {
                red_circle  => { char => '●', color => 'red' },
                blue_star   => { char => '★', color => 'blue' },
                green_laser => { char => '|', color => 'green' },
            }
        },
    );

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

# 設定
my $WIDTH  = 40;
my $HEIGHT = 15;
my $FRAMES = 10;

my $factory = BulletFactory->new;

# 弾を生成(放射状に飛ぶ)
my @bullets;
for my $i (0 .. 11) {
    my $angle = $i * 30 * 3.14159 / 180;  # 30度ずつ
    push @bullets, {
        type => $factory->get('red_circle'),
        x    => $WIDTH / 2,
        y    => $HEIGHT / 2,
        vx   => cos($angle) * 2,
        vy   => sin($angle),
    };
}

# アニメーションループ
print "\e[2J";  # 画面クリア
for my $frame (0 .. $FRAMES) {
    print "\e[H";  # カーソルを左上に

    # 背景を描画
    for my $row (1 .. $HEIGHT) {
        say "." x $WIDTH;
    }

    # 弾を描画
    for my $bullet (@bullets) {
        my $type = $bullet->{type};
        my $x = int($bullet->{x});
        my $y = int($bullet->{y});
        $type->render($x, $y, $WIDTH, $HEIGHT);
    }

    # 弾を移動
    for my $bullet (@bullets) {
        $bullet->{x} += $bullet->{vx};
        $bullet->{y} += $bullet->{vy};
    }

    print "\e[" . ($HEIGHT + 1) . ";1H";
    say "Frame: $frame / $FRAMES";

    sleep(0.2);
}
print "\e[" . ($HEIGHT + 2) . ";1H";
say "Animation complete!";

このコードを実行すると、弾が放射状に飛んでいくアニメーションが見られます。

弾幕パターンを作る

もう少し複雑な弾幕パターンを作ってみましょう。螺旋状に弾を発射します:

 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
#!/usr/bin/env perl
use v5.36;
use Time::HiRes qw(sleep);

package BulletType {
    use Moo;
    has char => (is => 'ro', required => 1);

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

package BulletFactory {
    use Moo;
    has _cache => (is => 'ro', default => sub { {} });
    has _defs => (
        is => 'ro',
        default => sub {
            {
                circle => { char => '●' },
                star   => { char => '★' },
                dot    => { char => '・' },
            }
        },
    );

    sub get($self, $key) {
        $self->_cache->{$key} //= BulletType->new(%{$self->_defs->{$key}});
    }
}

my $WIDTH  = 50;
my $HEIGHT = 20;
my $factory = BulletFactory->new;

my @bullets;
my $center_x = $WIDTH / 2;
my $center_y = $HEIGHT / 2;

# 螺旋状に弾を生成
for my $wave (0 .. 2) {
    for my $i (0 .. 23) {
        my $angle = ($i * 15 + $wave * 5) * 3.14159 / 180;
        my $type_key = qw(circle star dot)[$wave];
        push @bullets, {
            type    => $factory->get($type_key),
            x       => $center_x,
            y       => $center_y,
            vx      => cos($angle) * (1 + $wave * 0.3),
            vy      => sin($angle) * 0.5,
            created => $wave * 3,  # 生成タイミング
        };
    }
}

say "=== 弾幕シューティング ===";
say "弾の総数: " . scalar(@bullets);
say "BulletTypeオブジェクト数: 3(共有)";
say "";
say "アニメーション開始...";
sleep(1);

print "\e[2J";
for my $frame (0 .. 15) {
    # 画面バッファを作成
    my @screen = map { " " x $WIDTH } (1 .. $HEIGHT);

    # 弾を描画
    for my $bullet (@bullets) {
        next if $frame < $bullet->{created};  # まだ生成されていない
        my $type = $bullet->{type};
        $type->render($bullet->{x}, $bullet->{y}, \@screen);
    }

    # 画面を出力
    print "\e[H";
    for my $row (@screen) {
        say $row;
    }
    say "Frame: $frame";

    # 弾を移動
    for my $bullet (@bullets) {
        next if $frame < $bullet->{created};
        $bullet->{x} += $bullet->{vx};
        $bullet->{y} += $bullet->{vy};
    }

    sleep(0.15);
}

say "";
say "=== 結果 ===";
say "72発の弾を3つのBulletTypeオブジェクトで描画しました!";

外部状態を渡すメリット

今回の実装で、外部状態を渡すことのメリットが明確になりました:

  1. BulletType は位置を知らない

    • 形状と見た目だけを担当
    • 描画時に位置を受け取る
  2. 同じ BulletType を何度でも使える

    • 72発の弾に対して3つのオブジェクト
    • メモリ効率が良い
  3. 弾の管理がシンプル

    • 弾ごとの情報(位置、速度)はハッシュで管理
    • BulletType は共有

次回予告

弾の描画ができるようになりました!次回は、これまでのコードを統合して「弾幕シューティングエンジン」を完成させます。

BattleField クラスで弾の管理を一元化し、より本格的な弾幕システムを構築しましょう。

今回のまとめ

  • render($x, $y) で外部状態(位置)を受け取る
  • BulletType は位置を知らず、描画時に渡される
  • この設計により、同じ BulletType を複数の弾で共有できる
  • 螺旋状の弾幕パターンを実装
  • 次回:全体を統合して完成へ

今回の完成コード

  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
#!/usr/bin/env perl
use v5.36;
use Time::HiRes qw(sleep);

package BulletType {
    use Moo;

    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;
    }
}

package BulletFactory {
    use Moo;

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

    has _definitions => (
        is => 'ro',
        default => sub {
            {
                circle => { char => '●', color => 'red' },
                star   => { char => '★', color => 'blue' },
                dot    => { char => '・', color => 'green' },
                arrow  => { char => '→', color => 'yellow' },
            }
        },
    );

    sub get($self, $key) {
        my $cache = $self->_cache;
        my $defs  = $self->_definitions;

        $cache->{$key} //= do {
            my $def = $defs->{$key} or die "Unknown: $key";
            BulletType->new(%$def);
        };
    }

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

# メイン処理
my $WIDTH  = 50;
my $HEIGHT = 20;
my $factory = BulletFactory->new;

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

for my $wave (0 .. 2) {
    for my $i (0 .. 11) {
        my $angle = ($i * 30 + $wave * 10) * 3.14159 / 180;
        my $type = qw(circle star dot)[$wave];
        push @bullets, {
            type => $factory->get($type),
            x    => $cx,
            y    => $cy,
            vx   => cos($angle) * (1.5 - $wave * 0.2),
            vy   => sin($angle) * 0.7,
            born => $wave * 2,
        };
    }
}

say "弾の総数: " . scalar(@bullets);
say "BulletTypeオブジェクト数: " . $factory->count;
say "";

# アニメーション
print "\e[2J";
for my $frame (0 .. 12) {
    my @screen = map { " " x $WIDTH } (1 .. $HEIGHT);

    for my $b (@bullets) {
        next if $frame < $b->{born};
        $b->{type}->render($b->{x}, $b->{y}, \@screen);
        $b->{x} += $b->{vx};
        $b->{y} += $b->{vy};
    }

    print "\e[H";
    say $_ for @screen;
    say "Frame $frame - 外部状態を渡して描画中...";
    sleep(0.2);
}

say "\n完了!";
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。