-- / --
--
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

03 / 02
Sun

これといってやることがない Ziphil です。

プログラミングのリハビリがてら、 ランダムダンジョンを生成するプログラムを作ってみましょうか。 方法としては、 以下の通りです。 一般的ですね。

  • 境界線を作成し、マップ全体を複数の領域に分割する。
  • それぞれの領域に 1 つ部屋を作成する。
  • 部屋と境界線をつなぐ道を作成する。
  • 不要な境界線を削除する。

じゃ、 順にコードを組んでみましょうか。 ちなみに、 Ruby (JRuby) を使います。

まずは、 ダンジョン生成用のクラス AutoTilemap と、 分割された領域を管理するクラス AutoTilemapRoom を定義します。 @tiles はマップデータを格納する 2 次元配列、@rooms は分割された領域を格納する配列です。 attr_reader ~ は省略してあります。

class AutoTilemap
  def initialize(width, height)
    @width = width
    @height = height
    @tiles = Array.new(height){Array.new(width, 0)}  # 0 なら壁, 1 以上なら通過可能領域
    @rooms = [AutoTilemapRoom.new(0, 0, width, height, nil)]  # 最初はマップ全体を 1 つの領域とする
  end
end
class AutoTilemapRoom
  def initialize(x, y, width, height, direction)
    @x = x
    @y = y
    @width = width
    @height = height
    @room_x = 0
    @room_y = 0
    @room_width = 0
    @room_height = 0
    @direction = direction
  end

イメージとしてはこんな感じです。 最初ですから、 壁を表す 0 で埋め尽くされています。

770_ランダムダンジョン1

では、 最初のステップとして、 複数の領域に分割していきます。 @rooms の要素から適当に 1 つ取り出し、 縦に分割するか横に分割するか決め、 分割します。 この分割を何回か繰り返します。 この分割回数は後で変えられるように引数にしておきましょうか。

class AutoTilemap
  def devide(time)
    time.times do
      i = rand(@rooms.size)
      room = @rooms[i]
      if rand(2) == 0
        if room.width >= 11
          bx = rand(room.width - 10) + 5
          room.height.times do |j|
            @tiles[room.y + j][room.x + bx] = 1
          end 
          @rooms[i..i] = room.devide(bx, 0)
        end
      else
        if room.height >= 11
          by = rand(room.height - 10) + 5
          room.width.times do |j|
            @tiles[room.y + by][room.x + j] = 1
          end 
          @rooms[i..i] = room.devide(by, 1)
        end
      end
    end
  end
end

まず、 rand(@rooms.size) で既存の領域を適当に取り出します。 rand(2) で縦に分割するか横に分割するか決めます。 0 なら縦分割、 1 なら横分割です。 縦分割をするなら、 まず分割するだけの幅かあるかどうかを room.width >= 11 によって判断します。 今回は、 最低限 5 マスの幅は確保したかったので、 幅 11 マス以上を条件としています。 条件を満たしていれば、 bx = rand(room.width - 10) + 5 で分割する (領域内での) x 座標を bx に格納します。 その後、 room.height.times do |j| ~ end の部分で、 分割境界のマップデータを 1 に書き換え、 続いて @rooms[i..i] = room.devide(bx, 0) で、 分割前の領域を分割後の領域に置き換えます。

さて、 ここで未知の関数 AutoTilemapRoom#devide が出てきましたが、 内容はこんな感じです。

class AutoTilemapRoom
  def devide(c, direction)
    if direction == 0
      return [AutoTilemapRoom.new(@x, @y, c, @height, 6), AutoTilemapRoom.new(@x + c + 1, @y, @width - c - 1, @height, 4)]
    else
      return [AutoTilemapRoom.new(@x, @y, @width, c, 2), AutoTilemapRoom.new(@x, @y + c + 1, @width, @height - c - 1, 8)]
    end
  end
end

c が分割する座標になっています。 分割後の領域を表す AutoTilemapRoom インスタンスの配列を返します。

AutoTilemapRoom#initialize に direction という引数がありますが、 これは後で部屋から道を作るときに、 どの方向に道を伸ばせば良いかを記憶しておくためのものです。 2, 4, 6, 8 がそれぞれ上, 左, 右, 下になっています。 例えば、 縦に分割したときの右側の領域では、 道を左に向かって伸ばせば境界にぶつかって道がつながるので、 ここには 4 が指定されることになります。

さて、 領域分割のアルゴリズムはこんな感じです。 1 回分割すると、 イメージとしてこうなります。 赤い枠が分割された領域です。

770_ランダムダンジョン2

この分割をマップの大きさに応じて適当な回数繰り返します。

770_ランダムダンジョン3

では、 2 番目のステップとして、 領域に部屋を作っていきましょう。 @rooms のそれぞれの要素の対して、 内部に部屋を作ります。

class AutoTilemap
  def create_room
    @rooms.each do |i|
      width = rand(i.width / 2 - 2) + i.width / 2
      height = rand(i.height / 2 - 2) + i.height / 2
      x = rand(i.width - width - 2) + 1
      y = rand(i.height - height - 2) + 1
      i.set_room_status(x, y, width, height)
      for j in 0...width
        for k in 0...height
          @tiles[i.y + y + k][i.x + x + j] = 2
        end
      end
    end
  end
end

部屋が小さすぎても困るので、 部屋の幅と高さは、 最低でもそれぞれ領域の幅と高さの半分は確保することにしました。 また、 領域の境界と部屋が接してしまっても困るので、 rand(i.width / 2 - 2) とか rand(i.width - width - 2) + 1 とかで調整しています。 i.set_room_status(x, y, width, height) で部屋の情報を設定して、 続く for 文の @tiles[i.y + y + k][i.x + x + j] = 2 で部屋のマップデータを 2 に書き換えています。

AutoTilemapRoom#set_room_status ですが、 こうなってます。 インスタンス変数に値を代入してるだけです。 後で使うので。

class AutoTilemapRoom
  def set_room_status(x, y, width, height)
    @room_x = x
    @room_y = y
    @room_width = width
    @room_height = height
  end
end

この処理が終わると、 マップはこうなります。

770_ランダムダンジョン4

では引き続き、 3 番目のステップです。 部屋と境界線をつなぎます。

class AutoTilemap
  def create_path
    @rooms.each do |i|
      case i.direction
      when 2
        px = rand(i.room_width) + i.room_x + i.x
        j = 0
        until @tiles[i.room_y + i.y + i.room_height + j][px] == 1
          @tiles[i.room_y + i.y + i.room_height + j][px] = 3
          j += 1
        end
      when 4
        py = rand(i.room_height) + i.room_y + i.y
        j = 0
        until @tiles[py][i.room_x + i.x + j] == 1
          @tiles[py][i.room_x + i.x + j] = 3
          j -= 1
        end
      when 6
        py = rand(i.room_height) + i.room_y + i.y
        j = 0
        until @tiles[py][i.room_x + i.x + i.room_width + j] == 1
          @tiles[py][i.room_x + i.x + i.room_width + j] = 3
          j += 1
        end
      when 8
        px = rand(i.room_width) + i.room_x + i.x
        j = 0
        until @tiles[i.room_y + i.y + j][px] == 1
          @tiles[i.room_y + i.y + j][px] = 3
          j -= 1
        end
      end
    end
  end
end

道を伸ばす方向によって処理が微妙に異なりますから、 ここでは方向が 2 (下) の場合を見てみましょう。 まず、 px = rand(i.room_width) + i.room_x + i.x で道の x 座標を決めます。 続いて until 文で、 道が領域の境界にぶつかるまで、 マップデータを 3 に書き換えています。

この処理が終わると、 こんな感じです。

770_ランダムダンジョン5

さて、 これで全ての部屋が道でつながり、 ダンジョンは完成したわけですが、 まだ不要な通路がありますね。 マップの外側にまでつながってしまっている通路です。 最後のステップとして、 この不要な通路を削除しましょう。

class AutoTilemap
  def delete_path
    for x in 0...@width
      if @tiles[0][x] == 1
        y = 0
        until y >= @height || @tiles[y][x + 1] >= 1 || @tiles[y][x - 1] >= 1
          @tiles[y][x] = 0
          y += 1
        end
      end
      if @tiles[@height - 1][x] == 1
        y = @height - 1
        until y < 0 || @tiles[y][x + 1] >= 1 || @tiles[y][x - 1] >= 1
          @tiles[y][x] = 0
          y -= 1
        end
      end
    end
    for y in 0...@height
      if @tiles[y][0] == 1
        x = 0
        until x >= @width || @tiles[y + 1][x] >= 1 || @tiles[y - 1][x] >= 1 
          @tiles[y][x] = 0
          x += 1
        end
      end
      if @tiles[y][@width - 1] == 1
        x = @width - 1
        until x < 0 || @tiles[y + 1][x] >= 1 || @tiles[y - 1][x] >= 1
          @tiles[y][x] = 0
          x -= 1
        end
      end
    end
  end
end

どのように不要な道を削除するかですが、 まず、 マップの一番上 (y=0) を左から順に調べます。 そこでマップデータが 1 以上、 すなわち通過可能領域だった場合、 今度は下に向かってその通過可能領域を調べていきます。 このとき、 通過可能領域のマップデータを順に 0 に戻していき、 通路を削除していきます。 そして、 通路の両側のどちらか一方に他の通路が現れたところで、 通路の削除をやめます。 この処理を、 マップの一番下でも行い、 さらにマップの一番右側を下方向に調べ、 最後にマップの一番左側も調べます。

これが終わると、 ランダムダンジョンの完成です。

770_ランダムダンジョン6

最後に、 これまでのメソッドをまとめたメソッドを追加しておきましょう。 なお、 分割の回数ですが、 1 回の分割で領域の長さは 1/2 になりますから、 マップの幅の平方根とマップの高さの平方根の和に比例させるのが良い気がします。 この辺は、 生成したい部屋の大きさに応じて変えましょう。

class AutoTilemap
  def generate_tilemap
    devide(Math.sqrt(@width).to_i + Math.sqrt(@height).to_i)
    create_room
    create_path
    delete_path
  end
end

さて、 ざっと解説しましたが、 このアルゴリズムはまだ改善の余地があります。 まず、 部屋が必ず長方形になりますから、 洞窟などの自然物のダンジョンには向いていません。 さらに、 マップ全体をうまく使えなかったり、 空間がない無駄な部分ができてしまったり、 いろいろあります。

また、 このアルゴリズム自体はよくある手法で有名なんですが、 Ruby のコードは全て私が書いたものなので、 コード自体にも改善すべき点があるかもしれません。 その点は、 指摘していただけるとありがたいです。

スポンサーサイト

comment ×0
コメント
管理者にだけ表示を許可する
 
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。