Featured image of post 【第3回】BulletFactoryで弾を使い回そう

【第3回】BulletFactoryで弾を使い回そう

同じ種類の弾は1つのオブジェクトを共有しよう!BulletFactoryでオブジェクトプールを管理し、「1000発の弾を5オブジェクトで」を実現します。

前回、弾の「種類」と「位置」を分離することを学びました。でも、弾の種類を毎回手動で作成するのは面倒ですし、同じ種類の弾を間違えて2回 new してしまうかもしれません。

今回は、BulletFactory クラスを作って、この問題を解決しましょう!

BulletFactoryによるオブジェクトプール

前回の振り返り

前回達成したこと:

  • BulletType クラスで「内部状態」(形状、色、サイズ)を管理
  • 位置情報(外部状態)は弾ごとにハッシュで管理
  • メモリ使用量を約60%削減できた

残っている問題:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 毎回手動で種類を管理している
my $red_circle = BulletType->new(
    shape => 'circle', color => 'red', size => 8, char => '●',
);

# 間違えて同じ種類を2回作ってしまうかも?
my $red_circle2 = BulletType->new(
    shape => 'circle', color => 'red', size => 8, char => '●',
);
# $red_circle と $red_circle2 は別オブジェクト!(メモリの無駄)

BulletFactoryの役割

BulletFactory クラスを作り、以下の役割を持たせます:

  1. 弾の種類を一元管理:「赤い丸」「青い星」などの種類を管理
  2. 同じ種類は同じオブジェクトを返す:キャッシュ機構で重複を防ぐ
  3. 種類の取得を簡単にする:キー(例:red_circle)で取得できる

キャッシュ機構を実装する

Perlには、「なければ作って返す」を簡潔に書ける //= 演算子があります:

1
2
3
4
my %cache;

# キャッシュになければ新規作成、あればキャッシュから返す
$cache{$key} //= create_something($key);

この演算子を使って、BulletFactory を実装しましょう:

 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
#!/usr/bin/env perl
use v5.36;

package BulletType {
    use Moo;

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

    sub render($self, $x, $y) {
        my $char = $self->char;
        say "$char at ($x, $y)";
    }
}

package BulletFactory {
    use Moo;

    # 内部キャッシュ(先頭の _ は「外から触らないで」の慣習)
    has _cache => (
        is      => 'ro',
        default => sub { {} },
    );

    # 弾の種類定義(キー → 属性のマッピング)
    has _definitions => (
        is      => 'ro',
        default => sub {
            {
                red_circle   => { shape => 'circle', color => 'red',    size => 8,  char => '●' },
                blue_star    => { shape => 'star',   color => 'blue',   size => 12, char => '★' },
                green_laser  => { shape => 'laser',  color => 'green',  size => 4,  char => '|' },
                yellow_arrow => { shape => 'arrow',  color => 'yellow', size => 6,  char => '→' },
                purple_wave  => { shape => 'wave',   color => 'purple', size => 10, char => '〜' },
            }
        },
    );

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

        # キャッシュになければ新規作成
        $cache->{$type_key} //= do {
            my $def = $defs->{$type_key}
                or die "Unknown bullet type: $type_key";
            BulletType->new(%$def);
        };

        return $cache->{$type_key};
    }

    sub cache_stats($self) {
        my $cache = $self->_cache;
        my @keys = keys %$cache;
        return {
            count => scalar(@keys),
            types => \@keys,
        };
    }
}

# Factoryを作成
my $factory = BulletFactory->new;

# 同じ種類を何度取得しても、同じオブジェクトが返る
my $bullet1 = $factory->get('red_circle');
my $bullet2 = $factory->get('red_circle');
my $bullet3 = $factory->get('blue_star');

say "bullet1とbullet2は同じオブジェクト?";
say $bullet1 == $bullet2 ? "  → はい!同じオブジェクトです" : "  → いいえ、別オブジェクトです";

say "";
say "キャッシュの状態:";
my $stats = $factory->cache_stats;
say "  キャッシュ数: " . $stats->{count};
say "  キャッシュ内容: " . join(", ", @{$stats->{types}});

実行結果:

1
2
3
4
5
6
bullet1とbullet2は同じオブジェクト?
  → はい!同じオブジェクトです

キャッシュの状態:
  キャッシュ数: 2
  キャッシュ内容: red_circle, blue_star

red_circle を2回取得しても、作られる BulletType オブジェクトは1つだけです!

1000発の弾を5オブジェクトで管理

いよいよ、弾幕シューティングらしいコードを書いてみましょう:

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

package BulletType {
    use Moo;

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

    sub render($self, $x, $y) {
        my $char = $self->char;
        print "$char";
    }
}

package BulletFactory {
    use Moo;

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

    has _definitions => (
        is      => 'ro',
        default => sub {
            {
                red_circle   => { shape => 'circle', color => 'red',    size => 8,  char => '●' },
                blue_star    => { shape => 'star',   color => 'blue',   size => 12, char => '★' },
                green_laser  => { shape => 'laser',  color => 'green',  size => 4,  char => '|' },
                yellow_arrow => { shape => 'arrow',  color => 'yellow', size => 6,  char => '→' },
                purple_wave  => { shape => 'wave',   color => 'purple', size => 10, char => '〜' },
            }
        },
    );

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

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

        return $cache->{$type_key};
    }

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

# 弾幕を生成
my $factory = BulletFactory->new;
my @type_keys = qw(red_circle blue_star green_laser yellow_arrow purple_wave);

my @bullets;
for my $i (0 .. 999) {
    # 5種類の弾をランダムに配置
    my $type_key = $type_keys[$i % 5];
    push @bullets, {
        type => $factory->get($type_key),
        x    => $i % 50,
        y    => int($i / 50),
        vx   => 0,
        vy   => 1,
    };
}

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

# メモリ使用量を確認
my $bullets_size = total_size(\@bullets);
my $cache_size = total_size($factory->_cache);

say "=== メモリ使用量 ===";
say "弾1000発分: " . sprintf("%.1f", $bullets_size / 1024) . "KB";
say "BulletType 5つ分: " . sprintf("%.1f", $cache_size / 1024) . "KB";
say "";

# 弾幕を20x10の範囲で描画(一部だけ)
say "=== 弾幕プレビュー(先頭200発)===";
for my $row (0 .. 3) {
    for my $col (0 .. 49) {
        my $idx = $row * 50 + $col;
        my $bullet = $bullets[$idx];
        $bullet->{type}->render($bullet->{x}, $bullet->{y});
    }
    say "";
}

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
=== 弾幕の統計 ===
弾の総数: 1000
BulletTypeオブジェクトの数: 5

=== メモリ使用量 ===
弾1000発分: 245.8KB
BulletType 5つ分: 3.5KB

=== 弾幕プレビュー(先頭200発)===
●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜
●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜
●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜
●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜●★|→〜

1000発の弾を、たった5つの BulletType オブジェクトで管理できました!

オブジェクトプールの威力

前回との比較をしてみましょう:

項目第1回(分離なし)第2回(手動分離)今回(Factory)
弾1000発のメモリ約600KB約250KB約250KB
BulletType管理なし手動自動(キャッシュ)
同じ種類の重複防げない注意が必要防げる

BulletFactory の導入により、弾の種類の管理が自動化され、重複オブジェクトの生成を防ぐことができました。

次回予告

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

render メソッドに外部状態(位置)を渡して、弾幕を描画してみましょう!

今回のまとめ

  • BulletFactory クラスで弾の種類を一元管理
  • //= 演算子でキャッシュ機構を実装
  • 同じ種類の弾は同じオブジェクトを返す(重複防止)
  • 1000発の弾を5つの 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
#!/usr/bin/env perl
use v5.36;
use Devel::Size qw(total_size);

package BulletType {
    use Moo;

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

    sub render($self, $x, $y) {
        my $char = $self->char;
        print "$char";
    }

    sub describe($self) {
        my $shape = $self->shape;
        my $color = $self->color;
        return "[$color $shape]";
    }
}

package BulletFactory {
    use Moo;

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

    has _definitions => (
        is      => 'ro',
        default => sub {
            {
                red_circle   => { shape => 'circle', color => 'red',    size => 8,  char => '●' },
                blue_star    => { shape => 'star',   color => 'blue',   size => 12, char => '★' },
                green_laser  => { shape => 'laser',  color => 'green',  size => 4,  char => '|' },
                yellow_arrow => { shape => 'arrow',  color => 'yellow', size => 6,  char => '→' },
                purple_wave  => { shape => 'wave',   color => 'purple', size => 10, char => '〜' },
            }
        },
    );

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

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

        return $cache->{$type_key};
    }

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

    sub list_cached($self) {
        return keys %{$self->_cache};
    }
}

# メイン処理
my $factory = BulletFactory->new;
my @type_keys = qw(red_circle blue_star green_laser yellow_arrow purple_wave);

# 1000発の弾を生成
my @bullets;
for my $i (0 .. 999) {
    my $type_key = $type_keys[$i % 5];
    push @bullets, {
        type => $factory->get($type_key),
        x    => $i % 50,
        y    => int($i / 50),
        vx   => 0,
        vy   => 1,
    };
}

say "=== 弾幕シューティングエンジン ===";
say "弾の総数: " . scalar(@bullets);
say "BulletTypeオブジェクト数: " . $factory->cache_count;
say "キャッシュ内容: " . join(", ", $factory->list_cached);
say "";

# メモリ使用量
my $size = total_size(\@bullets);
say "メモリ使用量: " . sprintf("%.1f", $size / 1024) . "KB";
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。