はじめに
先日、ダンジョンマップをランダム生成する記事を書きました。今回の記事は、その続きの内容となっています。
概略
今回の内容に入る前に、前回の記事の概略を説明します。前回の記事では、マップをグリッドに分割し、それを深さ優先探索することで、マップを生成しました。生成されるマップは以下のような見た目です。

スタート地点からゴール地点までのルート自体はランダムに生成されていますが、グリッドは正方形の形を取るので、規則正しさが出てしまっています。今回は、生成後にこれを「歪める」ことによって、よりそれっぽいマップを作っていきます。
手法
では歪め方について説明します。マップのタイル情報は、二次元リストに格納しているので、以下のようにすることで、縦横にずらすことができます。
column = [row[x] for row in map_data] # x列を抽出
shifted_column = column[n % len(column):] + column[:n % len(column)] # 縦にn個シフト
for i in range(len(map_data)):
map_data[i][x] = shifted_column[i] # 元の列を更新
row = map_data[y] # y行を抽出
shifted_row = row[n % len(row):] + row[:n % len(row)] # 横にn個シフト
map_data[y] = shifted_row # 元の行を更新
このずらし方を上手く組み合わせることで、地形っぽさを出すことができます。どんな地形にするかによってn
の決め方を工夫することで、ある程度調整が可能です。
テクニック
ただランダムにn
を選ぶだけでは、あまり地形っぽさが出ないので、2つテクニックを紹介します。
一段ずつずらす
a = min(1, n + 1)
b = max(-1, n - 1)
n = random.choice([a, b])
純粋に-1
~1
のランダムから選ぶよりも、隣の列とのズレは1になるように調整することで、より地形っぽさが出ます。
複数行をまとめてずらす
for y in range(0, len(map_data)-m, m):
n = pyxel.rndi(0,1)
for i in range(m):
row = map_data[y+i]
shifted_row = row[n % len(row):] + row[:n % len(row)]
map_data[y+i] = shifted_row
for
で行を取り出す時点でm行ごとに取り出し、起点からm行を同じ方向にずらすことで、縦と横の両方を毎行ずらすよりも、地形っぽさが出ます。
以上のテクニックを組み合わせて出来たマップは、こんな感じの見た目になります。

利点
生成する段階でランダム性を持たせるよりも、ある程度大枠を作ってから歪める方が、実装が簡単でした。また、大枠を生成する段階で、一番遠い行き止まりをゴールとするようにしてあるので、歪めた後の一見ランダムなマップでも、ある程度の長さが担保されます。当然、ゴールにたどり着けることも保証できます(歪め方には限度がありますが)。また、自分で調整しながら歪められるので、複数のタイルで壁の模様を構成するような場合にも適用が可能です。例えば森のステージなどを作る際に、2x2の木を歪めずにランダムなマップを一から生成するのは困難に思うので、この点は大きい利点だと考えています。
最後に
ここまで読んで下さりありがとうございました。今回考えた方法以外にも、地形らしさを出しながら、マップを自動生成する方法はたくさんあると思います。アプローチの一つとして捉えていただければと思います。