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

この記事でやること
- 弾を表すクラス
Bullet を作成する - 100発の弾オブジェクトを生成する
- メモリ使用量を確認し、問題を発見する
対象読者
この記事は、次のような方を想定しています:
- 「Mooで覚えるオブジェクト指向プログラミング」シリーズを読了した方
- Mooの基本的な使い方(
has、sub、new)を理解している方 - Perl v5.36以降の環境をお持ちの方
前提知識
この記事では、前シリーズ「Mooで覚えるオブジェクト指向プログラミング」で学んだ以下の知識を活用します:
hasとsubでクラスを定義する方法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個のオブジェクトそれぞれに shape、color、size という属性を持たせています。
これ、無駄じゃないですか?
メモリ使用量を確認してみよう
実際にどれくらいメモリを使っているか確認してみましょう。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 "---";
}
|