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

05 / 03
Sat

毎週土曜日更新にしようかと迷っている Ziphil です。

Ruby でメソッドの返り値を明示したかったら return foo とか書きますよね。 まあ、 Ruby は最後に評価された値をそのまま返すので、 return 文はいらないんですが。 では、 ブロックの返り値を明示したい場合はどうしましょうか。 return foo と書くと、 ブロックだけでなくメソッドまで抜けちゃいます。 調べるとどうやら next foo と書けば良いらしいです。

え、 わざわざ next 文を使う必要ないじゃないかって? ・・・ええ、 その通りですよ。


スポンサーサイト
comment ×0
03 / 30
Sun

引っ越しが終わった Ziphil です。

私のプログラムでは、 アイテムなどのデータベースは、 アイテム ID をキーとしたハッシュで管理しています。 そこで、 すべてのデータベースを合わせた 1 つのハッシュを作ろうとして、 hash1 + has2 のように書いたら、 Hash#+ がないと怒られました。 ・・・ないの?

どうやら、 こういう場合は Hash#merge を使うみたいです。 調べてみると、 ただ 2 つのハッシュを混合したハッシュを作るだけでなく、 キーが重複した場合の処理を指定できるみたいです。

hash1 = {:a => 2, :b => 4, :c => 9}
hash2 = {:b => 5, :c => 6, :d => 7}
p hash1.merge(hash2){|key, value1, value2| value1 + value2}    #=> {:a => 2, :b => 9, :c => 15, :d => 7}

次です。 Object#tap というメソッドを知りました。 これは、 self をブロックの引数として評価した後、 self そのものを返すメソッドです。 で、 こんなメソッドが何の役に立つかというと、 メソッドチェーンの途中経過を覗けるんです。

x = [1, 3, 5, 6, 8, 10, 13, 16, 18, 19, 21, 22, 24]
y = x.select(&:even?).map(&:succ)

こんな風に、 配列から偶数だけを取り出して、 それぞれに 1 をたした配列を作りたいとします。

x = [1, 3, 5, 6, 8, 10, 13, 16, 18, 19, 21, 22, 24]
y = x.select(&:even?).tap{|t| p t}.map(&:succ)    # => [6, 8, 10, 16, 18, 22, 24]

こんな感じに途中に tap をはさめば、 select メソッド実行後の途中経過を見ることができるわけです。

デバッグっぽい使い方以外にも、 使い道がありますよ。

x = [2, 5, 8]
p x.inject({}){|h, t| h[t] = t ** 2; h}        # => {2 => 4, 5 => 10, 8 => 16}
p x.inject({}){|h, t| h.tap{h[t] = t ** 2}}    # => {2 => 4, 5 => 10, 8 => 16}

こんな感じに、 配列の各値をキーとして、 その 2 乗を値とするハッシュを作ろうとするわけです。 普通なら 2 行目みたいに inject 使えばいいんですが、 どうも ; h ってのが美しくない。 そこで、 tap の出番です。 多少冗長にはなりますが、 見た目が美しくなります。 効果には個人差があります。

まあ、 何をしているかというと、 破壊的メソッドを実行したときに自分自身が返るようにしているわけです。 要するにかっこつけです。 はい。

とまあ、 Ruby の豆知識でした。


comment ×0
03 / 13
Thu

PHP もやってみたいと思い始めた Ziphil です。

Ruby で開発をするとき、 私は基本的に 1 つのファイルに 1 つのクラスを書くようにしています。 あるクラスだけで使用される補助的なクラスだけは、 例外として、 用いられるクラスが書かれているファイルと同じファイルに書きます。 これは Java からの遺産です。

このとき、 MainCanvas.rb のようにファイル名をクラス名そのままにしてたんですが、 全部小文字が主流のようですね。 そういえば、 ライブラリ名は全部小文字だった。 別に何かこだわりがあってキャメルケースのファイル名にしてるわけではないので、 ここは慣習に乗ろうと思っています。 でも修正面倒だなぁ・・・。

ここからはまた違う話ですが、 Ruby の for 文って each メソッドのシンタックスシュガーじゃないですか。 私はこの for 文を結構使うんですが、 なんだか each だけ特別扱いってのもどうかと思うんですよね。 ということで、 for 文使うのをやめようかと。

# for 文
for x in 1..100
  for y in 1..(x * 2)
    x.do_something
    y.do_something
  end
end
# each
(1..100).each do |x|
  (1..(x * 2)).each do |y|
    x.do_something
    y.do_something
  end
end

これも、 正直どっちでも良いんですよね。 私が for 文をよく使うのは、 HSP とか Java とかの遺産だと思います。 これらの言語には each なんてないので。

それと、 1..100 っていうのは良いんですが、 1..(x * 2) って少し気持ち悪いんですよね。 あ、 別にカッコは不要で 1..x * 2 でも問題ないんですけど、 これはこれで * 2 が離れちゃって微妙。 じゃ、 Range.new(1, x * 2) で! いや、 いっそのこと、 each なんてやめて Numeric#step を使うとか。 この辺は、 個人の印象によりますね。

次行きましょう。 私のコードにこんな部分があります。

def load_short_weapon_file
  regexp = /(\d+);\s*(.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+)/
  open("../data/data/database/item-1.txt") do |file|
    while line = file.gets
      if line =~ regexp
        # 省略
        data_string.split(/\s*,\s*/).each_with_index do |data, i|
          case keys[i][1]
            # 省略
          end
        end
      end
    end
  end
end   

最後に end が連なってますね。 Ruby の構文からして連なるのは仕方ないんですが、 一部の Rubyist には 「END HELL」 とまで言われて嫌われているそうです。 私は別に気にしませんけどね。

で、 ある人は言いました。 こうすれば良いではないか、 と。

def load_short_weapon_file
  regexp = /(\d+);\s*(.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+,\s*.+)/
  open("../data/data/database/item-1.txt") do |file|
    while line = file.gets
      if line =~ regexp
        # 省略
        data_string.split(/\s*,\s*/).each_with_index do |data, i|
          case keys[i][1]
            # 省略
          ennnnnnd    # ←これ

変態的です。 嫌いじゃないですけど。 しかも、 これ parse.y とか書き換えれば実装できるんですよ。 いや、 でもこれインデントどうなるんだよ。

そうそう、 Python はインデントでブロックを明示するので、 そもそも end を書かないんですよね。 そのアイデアは良いと思うんですが、 閉じがないのでアンバランスな気がしないでもないです。

また話が変わります。 私、 RPG 開発などは JRuby でやってますが、 ちょっとした小物を作るときは生の Ruby を使ってます。 バージョンは 1.9.1 とすでに一昔前のものを使ってますが、 これはもうすぐパソコンを買い換えるので、 最新版をインストールするのが面倒なだけです。 CGI なんかは 1.8.7 で作ってます。 サポートはとっくに終わってることは知ってるんですが、 借りているサーバーが 1.8.7 なので仕方ない。

で、 最近というわけではもうなくなってますが、 Ruby 2.0 って何ができるの? いや、 最近は Ruby 2.1 が出てるみたいで、 それでは何ができるの? ・・・ということです。

一番の変化は、 やっぱりキーワード引数でしょうね。 1.9 でも最後の引数がハッシュの場合は、 ハッシュの波カッコを省略できるようになっていて、 これによってキーワード引数っぽいことができました。 2.0 は、 キーワード引数が正式な構文になったようです。 私はあまり使わないと思います。

2.1 からの新機能ですが、 どれを取り上げれば良いのか分かりませんが、 数値リテラルが増えました。 r をつけると Rational 型に、 i をつけると Complex 型に。 まあ、 これは良いんですが、 私は f がほしいです。 2.0 なんて書くより 2f の方がきれいだと思います。

後は、 def の返り値ですかね。 メソッド名の Symbol インスタンスを返します。 これによって、 private def ~ という書き方が可能になりました。 便利だと思います。 でも、 Ruby を使うときは、 私はあまり private とか public とか気にしたことがありません。

とまあ、 Ruby の雑多な話題でした。


comment ×0
03 / 09
Sun

IDE を NetBeans に変えてみた Ziphil です。

こんな風に定数を定義して。

BORDER = 1
ROOM = 2
PATH = 3
VERTICAL = 10
HORIZONTAL = 11

で、 こんな風に書けば、 定数を定義せずに数値を書くより、 何をしてるか分かりやすいじゃないですか。

if area.width >= minimum * 2 + 1
  bx = rand(area.width - minimum * 2) + minimum
  area.height.times do |j|
    @tiles[area.y + j][area.x + bx] = BORDER
  end 
  @areas[i..i] = area.devide(bx, VERTICAL)
end

正直、 定数の実際の値なんてどうでも良いじゃないですか。 ここでは、 VERTICAL の値を 10 にしてますが、 別に 100 でも 1000 でも良いんです。

数値である必要もなく、 VERTICAL = :vertical としても良いわけです。 しかし、 :vertical で具体的な意味が分かるので、 もはや定数を定義する意味がなくなります。 では、 定数の定義をやめてシンボルを使ってみると、 こうなります。

if area.width >= minimum * 2 + 1
  bx = rand(area.width - minimum * 2) + minimum
  area.height.times do |j|
    @tiles[area.y + j][area.x + bx] = :border
  end 
  @areas[i..i] = area.devide(bx, :vertical)
end

こうすれば、 定数の定義も必要なく、 かつ何をしているのか分かりやすい。 これでも・・・、良いんですよね?


03/09 リファレンスマニュアルを見てみると、 シンボルの用途に 「C の enum 的な使用」 というのがありました。 そもそも、 列挙型と定数って何が違うんでしょう。

例えば、 静的型付けの Java を例にとってみましょう。 定数を定義してみます。

public static final int VERTICAL = 10;
public static final int HORIZONTAL = 11;

このとき、 VERTICAL, HORIZONTAL はともにただの int 型の数値ですから、 VERTICAL もしくは HORIZONTAL を要求するメソッド引数に、 5 や 100 などの他の int 型数値を指定することができてしまいます。 型の保証が崩れます。 これを解消するものが列挙型だと思います。

public enum Direction {
  VERTICAL, HORIZONTAL
}

こうしておけば、 VERTICAL, HORIZONTAL は Direction 型ですから、 これらを要求するメソッドも Direction 型を要求するようにしておけば、 5 や 100 などの関係ない数値を指定するとエラーになってくれます。

と、 Java ではこのような恩恵が得られますが、 Ruby は動的型付けですから、 列挙型も定数も同じようなものになってしまう気がします。 ということで、 シンボルを列挙型の代用としても、 定数を使っても、 特に違いはないわけですから、 ここは好みの問題になるんでしょうか。


comment ×0
03 / 07
Fri

太ってきてやばいと感じる Ziphil です。

これまで 4 つ (Temple 型, Maze 型, Cavern 型, Wilderness 型) のダンジョン生成アルゴリズムを作ったので、 ちょっと速度比較をしてみましょうか。 マップのサイズは 50×50 としましょう。 こんな感じになりました。

                  user     system      total        real
temple:       0.007000   0.000000   0.007000 (  0.007000)
maze:         0.117000   0.000000   0.117000 (  0.117000)
cavern:       0.310000   0.000000   0.310000 (  0.310000)
wilderness:   0.375000   0.000000   0.375000 (  0.375000)

Temple 型だけ異様に速いですね。 まあ、 重い処理がありませんし、 当たり前と言えば当たり前ですが。

さて、 Cavern 型が重いのは、 セルオートマトンを実行する際に、 全マスの周囲の状態を調べる処理を 3 回も繰り返しているからでしょう。 Wilderness 型は、 2 点の通路作成が重いんだと思います。 ちなみに、 この通路作成はランダム要素が非常に強いので、 場合によっては Maze 型と同じくらいの速度でマップが生成されることもあります。

で、 処理が重いのか軽いのかは分かりましたが、 正直あまり気にしてません。 もしこの処理が毎フレーム行われるのなら、 0.3 秒もかかる処理は致命的ですが、 これはマップ生成時に 1 度だけ実行されるものなので、 正直 0.5 秒以内なら許容範囲だと思ってます。 軽いに超したことはないんですが。 ・・・でも、 やっぱりマップ遷移のときにフリーズするのは良くないかなぁ・・・。


comment ×0
03 / 06
Thu

Ziphil です。

ランダムダンジョンの生成アルゴリズム、 3 つ目になります。 今回は、 セルオートマトンを使ってみました。

774_製作過程A

前のアルゴリズムに比べて荒れが少ないですね。 しかし、 これが簡単なセルオートマトンのルールを何回か適用しただけの結果だというのは驚きです。

簡単にアルゴリズムを説明しましょう。 今回は Ruby のコードつきです。 ちなみに、 今回のアルゴリズムは、 主にこのサイトを参考にしています。

まず、 セルオートマトンのルールです。 あるマス p から n 回以内の移動 (斜め移動なし) で進める範囲を An(p) とし、 範囲 An(p) の内部にある壁のマスの個数を Rn(p) とします。 つまり、 An(p) は p を中心とする 1 辺が 2n + 1 の正方形になります。

セルオートマトンのルールは、 R1(p) ≧ C1 または R2(p) ≦ C2 のときは p を壁にし、 そうでないときは p を通路にする、 というものです。 これを、 初期条件として W% が壁であるマップに対し、 何度か適用します。 W, C1, C2 はパラメータです。

上のスクリーンショットでは、 W = 40 とし、 C1 = 5, C2 = 2 のルールを 2 回適用し、 その後 C1 = 5, C2 = -1 のルールを 1 回適用しています。 この繰り返し回数と W, C1, C2 の値は、 各自で調整してください。

では、 プログラムを見てみましょう。

class AutoTilemap
  def initialize(width, height)
    @width = width
    @height = height
    @tiles = Array.new(height){Array.new(width, 0)}
  end
  def generate_tilemap
    create_noise
    apply_automaton
  end
  def create_noise
    for x in 1...(@width - 1)
      for y in 1...(@height - 1)
        @tiles[y][x] = (rand(100) < 40)? 0 : 1
      end
    end
  end
  def apply_automaton
    parameters = [[2, 5, 2], [1, 5, -1]]
    new_tiles = Array.new(@height){Array.new(@width, 0)}
    parameters.each do |parameter|
      parameter[0].times do 
        for x in 1...(@width - 1)
          for y in 1...(@height - 1)
            area_in = [x - 1, x, x + 1].product([y - 1, y, y + 1])
            area_out = [x - 2, x - 1, x, x + 1, x + 2].select{|i| i.between?(0, @width - 1)}.
                       product([y - 2, y - 1, y, y + 1, y + 2].select{|i| i.between?(0, @height - 1)})
            number_in = 9 - area_in.count{|i| @tiles[i[1]][i[0]] != 0}
            number_out = 25 - area_out.count{|i| @tiles[i[1]][i[0]] != 0}
            new_tiles[y][x] = (number_in >= parameter[1] || number_out <= parameter[2])? 0 : 1
          end
        end
      end
    end
    @tiles = new_tiles
  end
end

create_noise メソッドは、 W% が壁であるマップをランダムに生成しています。 今回は W = 40 としたので、 (rand(100) < 40)? 0 : 1 としてあります。 なお、 0 が壁で 1 が通路を表します。

apply_automaton メソッドで、 セルオートマトンのルールを適用しています。 変数 parameters で、 簡単にパラメータを変更できるようにしておきました。 繰り返し回数, C1, C2 を格納した配列の配列を指定してください。

・・・にしても、 セルオートマトンってこんなこともできるんですね。 簡単なルールを適用するだけで、 自然物が模倣できる・・・。 自然とは結構単純なのでしょうか。

ちなみに、 上の参考にしたサイトを読んでるときに 「cellular automata」 って automaton と何が違うのかなー、 とか考えたことは内緒です。 どう見てもただの複数形です。 本当にありがとうございました。


comment ×0
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
back-to-top
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。