Featured image of post 最初のダンジョンを生成しよう - PerlとMooでダンジョンジェネレーター入門【第1回】

最初のダンジョンを生成しよう - PerlとMooでダンジョンジェネレーター入門【第1回】

PerlとMooで初めてのダンジョンを生成。二次元配列とASCII artを使った洞窟ダンジョンの描画方法を学び、ローグライク風のマップを自動生成する基礎を習得します。

PerlとMooで「ランダムダンジョンジェネレーター」を作る連載の第1回です。 ローグライク風のダンジョンを自動生成するエンジンを7回に分けて開発していきます。

ダンジョン生成への第一歩

この連載では、シンプルなダンジョン生成から始めて、段階的に機能を拡張していきます。 その過程で「クラス爆発」という設計上の問題にぶつかり、Bridgeパターンで優雅に解決する体験をします。

この連載で作るもの

完成すると、以下のようなASCII artでダンジョンマップを表示できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
####################################
#....######........................#
#....######........................#
#....######........................#
########..........................##
#.................................##
#.................................##
#........................##########
#........................##########
####################################

# は壁、. は床を表しています。 ローグライクゲームでおなじみの表現方法ですね。

対象読者と前提知識

この連載は以下の方を対象としています。

  • Perl入学式を卒業した方
  • 「Mooで覚えるオブジェクト指向プログラミング」シリーズを読了した方
  • デザインパターンに興味があるが、退屈な例題は避けたい方

前提として、Perl v5.36以降の機能(signaturespostfix dereference)と、Mooの基本的な使い方(haswithなど)を理解している必要があります。

二次元配列でダンジョンを表現する

ダンジョンの構造は二次元配列で表現します。 各セルは「壁」か「床」のどちらかです。

二次元配列とは、配列の中に配列が入った構造のことです。 今回の場合、縦方向(Y軸)の配列があり、その各要素が横方向(X軸)の配列になっています。

1
2
3
4
5
     X=0  X=1  X=2  X=3  ...
 Y=0  #    #    #    #
 Y=1  #    .    .    #
 Y=2  #    .    .    #
 Y=3  #    #    #    #

このように、map[Y][X] の形式で各セルにアクセスできます。

まずはDungeon.pmモジュールを作成しましょう。

 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
# Dungeon.pm - ダンジョンの基本構造
package Dungeon;
use v5.36;
use Moo;

# ダンジョンのサイズ
has width  => ( is => 'ro', default => 40 );
has height => ( is => 'ro', default => 10 );

# マップデータ(二次元配列への参照)
has map => (
    is      => 'rw',
    lazy    => 1,
    builder => '_build_map',
);

# 初期状態:すべて壁で埋める
sub _build_map ($self) {
    my @map;
    for my $y ( 0 .. $self->height - 1 ) {
        my @row;
        for my $x ( 0 .. $self->width - 1 ) {
            push @row, '#';    # 壁
        }
        push @map, \@row;
    }
    return \@map;
}

1;

map属性はlazybuilderを組み合わせています。

  • lazy => 1: オブジェクト生成時ではなく、最初にアクセスされたときに初期化する
  • builder => '_build_map': 初期化時に呼ばれるメソッド名を指定

この組み合わせにより、$dungeon->mapと呼び出した瞬間に_build_mapが実行され、二次元配列が作られます。 何度呼び出しても_build_mapは最初の1回だけ実行され、以降は同じ配列が返されます。

初期状態ではすべてのセルが壁(#)で埋まっています。 ここから床(.)を掘り進めてダンジョンを作っていきます。

ランダムに床を配置する

最もシンプルな生成アルゴリズムとして、ランダムに床を配置する方法を実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Dungeon.pm に追加

# ランダムに床を配置
sub generate ($self) {
    my $map = $self->map;

    for my $y ( 1 .. $self->height - 2 ) {
        for my $x ( 1 .. $self->width - 2 ) {
            # 70%の確率で床にする
            if ( rand() < 0.7 ) {
                $map->[$y][$x] = '.';
            }
        }
    }
}

外周1セル分は壁のままにして、内側だけをランダムに掘っています。

なぜ 1 .. height - 2 という範囲なのでしょうか?

  • Y=0は最上段の壁(残したい)
  • Y=height-1は最下段の壁(残したい)
  • なので、Y=1からY=height-2の範囲を処理

X方向も同様です。 これにより、ダンジョンが壁で囲まれた状態を維持できます。

また、コメントでは「30%の確率で床にする」と書いていますが、実際のコードはrand() < 0.7なので「70%の確率で床にする」が正しいです。 必要に応じてこの値を調整することで、床と壁のバランスを変えられます。

ASCII artで表示する

ダンジョンを画面に表示するメソッドを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Dungeon.pm に追加

# ASCII artで表示
sub render ($self) {
    my $map = $self->map;
    my $output = '';

    for my $row ( $map->@* ) {
        $output .= join( '', $row->@* ) . "\n";
    }

    return $output;
}

postfix dereference$map->@*)を使って配列参照を展開しています。 Perl v5.36ではこの記法が標準で有効になっています。

$map->@*@{$map}と同じ意味です。 配列参照($map)が指している配列の全要素を展開します。 これにより、for my $row ( $map->@* )で各行を順番に処理できます。

実行スクリプトを作成する

ダンジョンを生成して表示するスクリプトを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env perl
# dungeon_demo.pl - ダンジョン生成デモ
use v5.36;
use lib '.';
use Dungeon;

# ダンジョンを生成
my $dungeon = Dungeon->new(
    width  => 40,
    height => 10,
);

$dungeon->generate;

# 表示
print $dungeon->render;

実行すると、以下のような出力が得られます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ perl dungeon_demo.pl
####################################
#....######........................#
#....######........................#
#....######........................#
########..........................##
#.................................##
#.................................##
#........................##########
#........................##########
####################################

実行するたびに異なるダンジョンが生成されます。 ランダム性があるので、毎回違う形になるのが楽しいですね。

完成コード

ここまでの内容をまとめた完成コードです。

 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
# Dungeon.pm - ダンジョンの基本構造(完成版)
package Dungeon;
use v5.36;
use Moo;

# ダンジョンのサイズ
has width  => ( is => 'ro', default => 40 );
has height => ( is => 'ro', default => 10 );

# マップデータ(二次元配列への参照)
has map => (
    is      => 'rw',
    lazy    => 1,
    builder => '_build_map',
);

# 初期状態:すべて壁で埋める
sub _build_map ($self) {
    my @map;
    for my $y ( 0 .. $self->height - 1 ) {
        my @row;
        for my $x ( 0 .. $self->width - 1 ) {
            push @row, '#';    # 壁
        }
        push @map, \@row;
    }
    return \@map;
}

# ランダムに床を配置
sub generate ($self) {
    my $map = $self->map;

    for my $y ( 1 .. $self->height - 2 ) {
        for my $x ( 1 .. $self->width - 2 ) {
            # 30%の確率で床にする
            if ( rand() < 0.7 ) {
                $map->[$y][$x] = '.';
            }
        }
    }
}

# ASCII artで表示
sub render ($self) {
    my $map = $self->map;
    my $output = '';

    for my $row ( $map->@* ) {
        $output .= join( '', $row->@* ) . "\n";
    }

    return $output;
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env perl
# dungeon_demo.pl - ダンジョン生成デモ(完成版)
use v5.36;
use lib '.';
use Dungeon;

# ダンジョンを生成
my $dungeon = Dungeon->new(
    width  => 40,
    height => 10,
);

$dungeon->generate;

# 表示
print $dungeon->render;

今回のまとめ

第1回では、ダンジョンジェネレーターの基礎を作りました。

  • 二次元配列でダンジョンのマップを表現
  • ランダムに床を配置するシンプルな生成アルゴリズム
  • ASCII artで画面に表示

現時点ではランダムに床を配置しているだけなので、見た目はあまり「ダンジョンらしく」ありません。

次回は、もっとダンジョンらしい見た目にするために、迷路型のアルゴリズムを追加します。 ちゃんとした通路がある迷路型のダンジョンが生成できるようになります!

ただし、アルゴリズムを追加する過程で、コードの重複という問題に直面することになります。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。