Featured image of post 【第2回】弾の種類と位置を分けて考えよう

【第2回】弾の種類と位置を分けて考えよう

弾の「種類」(形状・色)と「位置」(X, Y座標)を分けて考える。この発想がメモリ効率化の鍵。内部状態と外部状態の分離を学びます。

前回、大量の弾オブジェクトを生成すると、メモリ使用量が爆発することがわかりました。でも、よく考えると「見た目」は共有できるのに「位置」は弾ごとに違う…。

この気づきを設計に活かしてみましょう!

内部状態と外部状態の分離

前回の振り返り

前回発見した問題を整理します:

  • 100発の「赤い丸弾」を生成した
  • すべての弾が同じ shapecolorsize を持っている
  • でも、1発あたり約600バイトのメモリを消費
  • 見た目は共有できるはずなのに、全部別々に持っている

弾の「種類」と「個体」を分離する

ここで発想を変えてみましょう。

弾を「種類(BulletType)」と「個体(弾そのもの)」に分けて考えます:

分類内容弾ごとに異なる?
種類(BulletType)形状、色、サイズいいえ!同じ種類なら同じ
個体(位置情報)X座標、Y座標、速度はい!弾ごとに違う

「赤い丸弾」という種類は1つだけ作って、それを100発の弾で共有すればいいのです!

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
#!/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 describe($self) {
        my $shape = $self->shape;
        my $color = $self->color;
        my $size  = $self->size;
        return "[$color $shape, size=$size]";
    }
}

# 弾の種類を定義
my $red_circle = BulletType->new(
    shape => 'circle',
    color => 'red',
    size  => 8,
    char  => '●',
);

my $blue_star = BulletType->new(
    shape => 'star',
    color => 'blue',
    size  => 12,
    char  => '★',
);

my $green_laser = BulletType->new(
    shape => 'laser',
    color => 'green',
    size  => 4,
    char  => '|',
);

say "定義した弾の種類:";
say "  " . $red_circle->describe;
say "  " . $blue_star->describe;
say "  " . $green_laser->describe;

実行結果:

1
2
3
4
定義した弾の種類:
  [red circle, size=8]
  [blue star, size=12]
  [green laser, size=4]

これで、弾の「種類」を3つ定義できました。

位置情報を分離する

次に、「個体」としての位置情報を管理する方法を考えます。

前回は Bullet クラスに全部入れていましたが、今回は位置情報だけをハッシュで持つようにしてみましょう:

 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
#!/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;
        say "$char at ($x, $y)";
    }
}

# 弾の種類は3つだけ作る
my $red_circle = BulletType->new(
    shape => 'circle', color => 'red', size => 8, char => '●',
);
my $blue_star = BulletType->new(
    shape => 'star', color => 'blue', size => 12, char => '★',
);
my $green_laser = BulletType->new(
    shape => 'laser', color => 'green', size => 4, char => '|',
);

# 弾の位置情報を配列で管理
my @bullets;
for my $i (0 .. 99) {
    push @bullets, {
        type => $red_circle,  # 種類への参照(共有!)
        x    => 100 + $i,
        y    => 200,
        vx   => 0,
        vy   => 5,
    };
}

say "弾の数: " . scalar(@bullets);
say "BulletTypeオブジェクトの数: 3(共有)";

# 最初の3発を描画
for my $bullet (@bullets[0..2]) {
    my $type = $bullet->{type};
    $type->render($bullet->{x}, $bullet->{y});
}

# メモリ使用量を確認
my $bullets_size = total_size(\@bullets);
my $types_size = total_size([$red_circle, $blue_star, $green_laser]);

say "";
say "弾100発のメモリ: " . sprintf("%.1f", $bullets_size / 1024) . "KB";
say "BulletType 3つのメモリ: " . sprintf("%.1f", $types_size / 1024) . "KB";

実行結果:

1
2
3
4
5
6
7
8
弾の数: 100
BulletTypeオブジェクトの数: 3(共有)
● at (100, 200)
● at (101, 200)
● at (102, 200)

弾100発のメモリ: 25.2KB
BulletType 3つのメモリ: 2.1KB

前回との比較

項目前回(分離なし)今回(分離あり)
弾100発のメモリ60.8KB25.2KB
BulletTypeオブジェクト数なし(弾ごとに属性を持つ)3つ(共有)
メモリ削減率-約60%削減!

見た目の情報を共有するだけで、メモリ使用量が大幅に減りました!

「内部状態」と「外部状態」

ここで、専門用語を紹介します:

用語意味本シリーズでの例
内部状態(Intrinsic State)オブジェクト間で共有できる状態形状、色、サイズ(BulletType)
外部状態(Extrinsic State)オブジェクトごとに異なる状態位置、速度(弾ごとの情報)

「内部状態」を持つオブジェクトを共有し、「外部状態」は使用時に渡す。これがメモリ効率化の基本的な考え方です。

renderメソッドに位置を渡す

注目してほしいのは、render メソッドの呼び出し方です:

1
2
# BulletType自体は位置を知らない
$type->render($bullet->{x}, $bullet->{y});

BulletType オブジェクトは、自分の位置を知りません。描画するときに「外部から位置を受け取る」のです。

これが「外部状態を渡す操作」です。

次回予告

分離の考え方はわかりました。でも、毎回 BulletType->new(...) を呼び出すのは面倒ですし、同じ種類の弾を何度も new してしまう危険もあります。

次回は、BulletFactory というクラスを作って、同じ種類の弾は確実に同じオブジェクトを返すようにします。

今回のまとめ

  • 弾の「種類」と「位置」を分けて考える
  • 内部状態(形状、色、サイズ):共有可能 → BulletType クラス
  • 外部状態(位置、速度):弾ごとに異なる → 使用時に渡す
  • この分離により、メモリ使用量を約60%削減できた
  • 次回: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
#!/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;
        say "$char at ($x, $y)";
    }

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

# 弾の種類を定義(3種類だけ)
my $red_circle = BulletType->new(
    shape => 'circle', color => 'red', size => 8, char => '●',
);
my $blue_star = BulletType->new(
    shape => 'star', color => 'blue', size => 12, char => '★',
);
my $green_laser = BulletType->new(
    shape => 'laser', color => 'green', size => 4, char => '|',
);

say "弾の種類:";
say "  " . $red_circle->describe;
say "  " . $blue_star->describe;
say "  " . $green_laser->describe;
say "";

# 弾の位置情報(外部状態)を配列で管理
my @bullets;
for my $i (0 .. 99) {
    push @bullets, {
        type => $red_circle,  # 内部状態への参照(共有)
        x    => 100 + $i,     # 外部状態
        y    => 200,
        vx   => 0,
        vy   => 5,
    };
}

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

# 描画(外部状態を渡す)
say "最初の3発を描画:";
for my $bullet (@bullets[0..2]) {
    my $type = $bullet->{type};
    $type->render($bullet->{x}, $bullet->{y});
}

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