Featured image of post 【第1回】弾を1000発撃ちたい!メモリの限界

【第1回】弾を1000発撃ちたい!メモリの限界

弾幕シューティングで弾を1000発撃ちたい!でも素朴にBulletオブジェクトを1000個作ると、メモリ使用量が爆発。この問題をどう解決するか、一緒に考えてみましょう。

弾幕シューティングゲームを作りたい!画面いっぱいに弾が飛び交うあの世界を、Perlで再現してみましょう。

メモリオーバーフローの問題

この記事でやること

  • 弾を表すクラス Bullet を作成する
  • 100発の弾オブジェクトを生成する
  • メモリ使用量を確認し、問題を発見する

対象読者

この記事は、次のような方を想定しています:

  • 「Mooで覚えるオブジェクト指向プログラミング」シリーズを読了した方
  • Mooの基本的な使い方(hassubnew)を理解している方
  • Perl v5.36以降の環境をお持ちの方

前提知識

この記事では、前シリーズ「Mooで覚えるオブジェクト指向プログラミング」で学んだ以下の知識を活用します:

  • hassubでクラスを定義する方法
  • newでオブジェクトを生成する方法
  • requiredで必須属性を指定する方法

弾幕シューティングの世界

弾幕シューティングとは、画面上に大量の弾が飛び交うシューティングゲームのジャンルです。有名なタイトルでは、東方Projectシリーズがあります。

画面いっぱいに広がる美しい弾幕。あの世界を実現するには、数百〜数千発の弾を同時に管理する必要があります。

でも、ちょっと待ってください。数千個のオブジェクトを生成するって、大丈夫なのでしょうか?

まずは素朴に作ってみよう

弾を表す Bullet クラスを作ってみましょう。弾には以下の情報が必要です:

  • 形状(丸、星、レーザーなど)
  • 色(赤、青、緑など)
  • サイズ
  • 位置(X座標、Y座標)
  • 速度(X方向、Y方向)
 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
#!/usr/bin/env perl
use v5.36;

package Bullet {
    use Moo;

    # 弾の見た目
    has shape => (is => 'ro', required => 1);  # 形状
    has color => (is => 'ro', required => 1);  # 色
    has size  => (is => 'ro', required => 1);  # サイズ

    # 弾の位置と速度
    has x  => (is => 'rw', required => 1);  # X座標
    has y  => (is => 'rw', required => 1);  # Y座標
    has vx => (is => 'ro', required => 1);  # X方向速度
    has vy => (is => 'ro', required => 1);  # Y方向速度

    sub move($self) {
        $self->x($self->x + $self->vx);
        $self->y($self->y + $self->vy);
    }

    sub render($self) {
        my $shape = $self->shape;
        my $color = $self->color;
        my $x = $self->x;
        my $y = $self->y;
        say "[$color $shape] at ($x, $y)";
    }
}

# 100発の弾を生成
my @bullets;
for my $i (0 .. 99) {
    my $bullet = Bullet->new(
        shape => 'circle',
        color => 'red',
        size  => 8,
        x     => 100 + $i,
        y     => 200,
        vx    => 0,
        vy    => 5,
    );
    push @bullets, $bullet;
}

say "弾の数: " . scalar(@bullets);

# 最初の3発を表示
for my $bullet (@bullets[0..2]) {
    $bullet->render;
}

実行してみましょう:

1
2
3
4
弾の数: 100
[red circle] at (100, 200)
[red circle] at (101, 200)
[red circle] at (102, 200)

動きました!でも、ここで気づくことがあります。

すべて同じ「赤い丸」なのに…

100発の弾を作りましたが、よく見てください:

  • 形状: すべて circle
  • 色: すべて red
  • サイズ: すべて 8

見た目の情報は全部同じなのに、100個のオブジェクトそれぞれに shapecolorsize という属性を持たせています。

これ、無駄じゃないですか?

メモリ使用量を確認してみよう

実際にどれくらいメモリを使っているか確認してみましょう。Devel::Size モジュールを使います:

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

package Bullet {
    use Moo;

    has shape => (is => 'ro', required => 1);
    has color => (is => 'ro', required => 1);
    has size  => (is => 'ro', required => 1);
    has x     => (is => 'rw', required => 1);
    has y     => (is => 'rw', required => 1);
    has vx    => (is => 'ro', required => 1);
    has vy    => (is => 'ro', required => 1);
}

# 弾の数を変えて試す
for my $count (100, 1000, 10000) {
    my @bullets;
    for my $i (0 .. $count - 1) {
        push @bullets, Bullet->new(
            shape => 'circle',
            color => 'red',
            size  => 8,
            x     => 100 + $i,
            y     => 200,
            vx    => 0,
            vy    => 5,
        );
    }

    my $size = total_size(\@bullets);
    my $size_kb = sprintf("%.1f", $size / 1024);
    my $per_bullet = sprintf("%.0f", $size / $count);
    
    say "弾 $count 発: ${size_kb}KB (1発あたり ${per_bullet}バイト)";
}

実行結果(環境により異なります):

1
2
3
弾 100 発: 60.8KB (1発あたり 621バイト)
弾 1000 発: 596.6KB (1発あたり 610バイト)
弾 10000 発: 5.8MB (1発あたり 607バイト)

1発あたり約600バイト!10000発だと約6MB…。弾幕シューティングで本気の弾幕を作ろうとしたら、メモリがどんどん消費されてしまいます。

問題の整理

ここで問題を整理しましょう:

属性弾ごとに異なる?共有できる?
shape(形状)いいえ!同じ種類の弾は同じ形状できる!
color(色)いいえ!同じ種類の弾は同じ色できる!
size(サイズ)いいえ!同じ種類の弾は同じサイズできる!
x(X座標)はい!弾ごとに違うできない
y(Y座標)はい!弾ごとに違うできない
vx(X速度)はい!弾ごとに違うできない
vy(Y速度)はい!弾ごとに違うできない

「見た目」は共有できるのに、「位置」は弾ごとに違う!

同じ「赤い丸弾」なら、見た目の情報は1つだけ持って、それを共有すればいいのでは?

次回予告

同じ種類の弾は、見た目の情報を共有できそうです。でも、どうやって?

次回は、弾の「種類」と「位置」を分けて考える方法を紹介します。この発想が、メモリ効率化の鍵になります。

今回のまとめ

  • 弾幕シューティングでは大量の弾オブジェクトが必要
  • 素朴に全属性を持たせると、1発あたり約600バイトのメモリを消費
  • 10000発で約6MBのメモリが必要(問題!)
  • 「見た目」は共有できるのに、「位置」は弾ごとに違うという気づき
  • 次回:この気づきをもとに設計を改善する

今回の完成コード

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

package Bullet {
    use Moo;

    # 弾の見た目(共有可能…のはず)
    has shape => (is => 'ro', required => 1);
    has color => (is => 'ro', required => 1);
    has size  => (is => 'ro', required => 1);

    # 弾の位置と速度(弾ごとに異なる)
    has x  => (is => 'rw', required => 1);
    has y  => (is => 'rw', required => 1);
    has vx => (is => 'ro', required => 1);
    has vy => (is => 'ro', required => 1);

    sub move($self) {
        $self->x($self->x + $self->vx);
        $self->y($self->y + $self->vy);
    }

    sub render($self) {
        my $shape = $self->shape;
        my $color = $self->color;
        my $x = $self->x;
        my $y = $self->y;
        say "[$color $shape] at ($x, $y)";
    }
}

# メモリ使用量を計測
my @bullets;
for my $i (0 .. 99) {
    push @bullets, Bullet->new(
        shape => 'circle',
        color => 'red',
        size  => 8,
        x     => 100 + $i,
        y     => 200,
        vx    => 0,
        vy    => 5,
    );
}

say "弾の数: " . scalar(@bullets);

my $size = total_size(\@bullets);
my $size_kb = sprintf("%.1f", $size / 1024);
say "メモリ使用量: ${size_kb}KB";

# 弾を動かして表示
for my $bullet (@bullets[0..2]) {
    $bullet->render;
    $bullet->move;
    $bullet->render;
    say "---";
}
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。