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

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 / 21
Fri

パソコンよりスマートフォンの方が速度が速い Ziphil です。 ・・・早く新しいパソコンにしたい。

さて、 防具のデータベース (名前だけ) ができました。 42 種類あります。 正直、 これだけ探すのには苦労しました。 「重~」 「ヘビー~」 で水増ししているのもありますが・・・。

789_防具リスト

この 42 種類の防具のステータスを決める方が大変ですけどね。

首装備と指装備は、 基本的に補助用で、 各種ステータスの上昇用にしようと思っています。 それ以外は、 軽装備用と重装備用で同じ数ずつ用意してあります。

プログラミングをしようにも、 データベースが定まってないとできない状況に来てしまってるんですよね・・・。 防具と遠距離武器の設定を、 さっさと済ませておかないとですね。


03/22 よくよく見てみたら、 「鎧」 っていう全般的な名称と 「板金鎧」 とかいう具体的な名称が別々になってますね・・・。 「板金鎧」 だって 「鎧」 だろ! ・・・みたいなツッコミはなしの方向で。


comment ×0
03 / 19
Wed

スマートフォンの画質に感動する Ziphil です。

RPG などで、 ゲームを進んでいるとよく完全上位互換の武器とかが手に入りますよね。 自分が今持っている武器より、 どのステータスから見ても強い武器のことです。 こういうのが頻発するゲームはあまり好きではありません。 これまでの武器を売って新しい武器を手に入れるだけで、 武器を選択する楽しみがなくなります。

私個人の意見ですが、 過去に用いた武器や防具を頻繁に売ることになる RPG は微妙です。 ゲームを進めると、 いろいろな武器が手に入り、 迫り来る敵にはどれが最も適切かを考えるのが楽しいんです。

ということで、 武器の設定は並列に行うようにしています。 いろいろな特徴をもった武器が何種類もあるみたいな感じです。 まあ、 データベースの設定はその分大変になりますけどね。


comment ×0
03 / 18
Tue

引っ越しの処理が大変なことになりそうな Ziphil です。

さて、 プログラミングから離れ、 データベースの設定といきましょうか。 RPG を作成するときに最も重要で最も面倒な部分です。

以前、 データベースの一部と仕組みについて書きましたが、 これを変更しました。 これまでは 「2d9611」 という方式で武器などの威力を表現していましたが、 これだとちょっと分かりにくい。 ということで、 「123±110」 という方式で表現することにしました。 こうすれば、 最大値, 最小値, 平均値がわりとすぐに分かり、 便利だと思うんですよ。

ということで、 近接武器のデータベースを書き換えて、 今は防具とかのデータベースを設定しているところです。 これが武器以上に厄介なんです。 各部位に 6 種類くらい必要だと思っているんですが、 そんなに種類が思いつかないです。 いや本当はあるかもしれないんですけど、 私には無理でした。

・・・ということで、 いろいろな RPG から情報収集しないとです。 んー、 大変です。


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 / 08
Sat

Lisp が気になる Ziphil です。

ランダムダンジョン関係の処理が一段落したので、 タイトル画面を作ってみました。 ちなみに、 考えた結果、 ランダムダンジョンの生成アルゴリズムは 4 種類で十分だということになりました。

776A_製作過程A

あれ・・・、 ドット絵・・・?

ドット絵ではないです。 つまりどういうことかというと、 私にはこんなに大きいドット絵は描けないということです。 背景画像は CLOSET 様からお借りしました。

やっぱり青はきれいです。


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 / 07
Fri

RGB 色空間がますます使いにくく感じていく Ziphil です。

Maze 型のダンジョン生成プログラムができました。

775_製作過程A

通路の幅 が 2 マス、 壁の幅が 1 マスになるように作ってあります。 使用したアルゴリズムですが、 よくある 「穴掘り法」 ってやつです。 たぶん。 正直、 穴掘り法の解説を真面目に読んでないので、 本当に私の実装したアルゴリズムが本当に穴掘り法かどうかは分かりませんが。 穴は掘ってますけどね。

そういえば、 プログラムを書きながら、 地味にマップチップを追加してます。 今回のスクリーンショットのマップチップは全部新作ですし、 前の洞窟のチップは壁だけ新作です。 まあ、 プログラムが主だったので、 ちょっと手抜きだったりしなくもないですけど・・・。

にしても、 ドット絵うまくなりたい。


comment ×0
03 / 06
Thu

この欄に書くことが思いつかない Ziphil です。

さて、 これまで Temple 型, Wilderness 型, Cavern 型 の 3 つのランダムダンジョン生成アルゴリズムを作りました。 名前は今適当につけました。

3 種類あれば十分かもしれませんが、 もう 2 種類くらいほしいなー、 と思うわけです。 具体的に言えば、 部屋と通路ではなく迷路のようになっている Maze 型と、 Temple 型 を自然物化させたような空洞と通路で構成される Cave 型です。

Maze 型は有名なアルゴリズムがありますから良いんです、 穴堀り法とか。 問題は Cave 型です。 Temple 型における部屋と通路の生成を、 より自然物っぽく曲線を含ませたものに改良すれば、 それで終わりな気がしますが、 せっかくだし違う方法を採ってみたいです。

穴掘り法っぽい方法で作れそうかなー、 と思うんです。 つまり、 適当な位置から適当に通路を掘っていって、 適当な位置で空洞を作り、 また適当に通路を作る、 みたいな。 このとき、 その場に本当に作れるか調査しならが通路や空洞を作っていけば、 それっぽくなる気がします。

もしくは、 空洞を先に適当に配置して、 適当に通路をつなぐ方法です。 ただ、 この通路をつなぐ処理が、 意外と面倒なんです。 孤立した空洞ができては困りますし、 かえってつなぎすぎも良くありません。

調べてみてもいるんですが、 なかなか良い方法は見つかりませんし。 こう、 エレガントな方法はないんでしょうか。


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 / 05
Wed

ワイエルシュトラス (Weierstrass) のカタカナ表記のブレが気になる Ziphil です。

いろいろあって、 自然物っぽいダンジョンの生成アルゴリズムが完成しました。 先日アルゴリズムの考察を投稿しましたが、 その中のどれとも違う方法で生成してます。 考察とは何だったのか・・・。

アルゴリズムの解説の前に、 完成品を見てもらいましょうか。 こんな感じの洞窟ができます。

773_製作過程A

ぐちゃぐちゃしてますね。 ちょっと荒れすぎな感じもしますが、 これ以上は私の力では無理でした。 全く違うアルゴリズムを考案する必要があるかもしれないですね。

では、 アルゴリズムの解説です。 ちょっと私のコードが煩雑になってしまったので、 今回はコードなしで、 概要だけ説明しますね。

まず、 2 点間を結ぶ通路を作るアルゴリズムを説明します。 この 2 点のうち、 片方を 「スタート地点」 と呼び、 もう片方を 「ゴール地点」 と呼ぶことにします。

マップのそれぞれのマスに、 状態とスコアの 2 つのデータをもたせます。 状態は 「open」 か 「closed」 のどちらかで、 スコアは 5 以下の整数とします。 スコアをもたないマスもあるとします。 初期状態は、 全て closed でスコアなしの状態です。

以下、 参考として図を乗せますが、 open なら明るい色、 closed なら暗い色でマス目を塗ることにしています。 マスの中の数字は、 そのマスのスコアを意味します。 また、 スタート地点を赤の枠で、 ゴール地点を緑の枠で示します。 したがって、 初期状態は以下のようになります。

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

スタート地点を、 状態 open, スコア 5 とします。

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

マップの中から状態が open であるマスを適当に選び出します。 そのマスの上下左右にあり、 スコアをまだもっていないマスをいくつか選び、 状態を open とします。 全方向を open でも良いですし、 1 個も open にしなくても構いません。 新たに open にしたマスのスコアは、 最初に選んだマスに比べてゴール地点に近づいたなら同じスコア、 遠ざかったなら 2~4 ほど減少させます。 その後、 最初に選んだマスの状態を closed にします。

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

上の処理を繰り返します。 なお、 処理の途中でスコアが 0 以下になってしまったマスは、 強制的に状態を closed にします。 ゴール地点にスコアが与えられた時点で、 繰り返しを終了します。

さて、 運が悪いと、 ゴール地点にスコアが与えられる前に、 open のマスがなくなってしまう場合があります。 その場合は、 スコアが与えられているマスのうち、 ゴール地点から最も近いマスの 1 つを open にして、 繰り返しを続行します。

最終的に、 ゴール地点にスコアが与えられると、 マップ全体はこんな感じになります。

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

最後に、 スコアが 0 以下のマスとスコアをもっていないマスを壁とし、 スコアが 1 以上のマスを通過可能領域とすれば、 2 点間を結ぶ通路が完成します。

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

さて、 後は、 マップ全体に何ヶ所かチェックポイントをとり、 そのチェックポイント同士を結ぶ通路を、 上のアルゴリズムを用いて作成すれば、 ダンジョンの完成です。

さて、 このアルゴリズムのパラメータは、 新たに隣接するマスを open にするときの確率と、 ゴール地点から遠ざかったときに減少させるスコアの量の 2 つです。 この 2 つを適当に調整すれば、 それっぽいダンジョンが作れます。

・・・でもやっぱり、 ちょっと形が荒れすぎですよね。 輪郭を滑らかにできるアルゴリズムをまた考えないといけませんね。


comment ×0
03 / 04
Tue

記事を連投する Ziphil です。

前に作ったランダムダンジョン生成アルゴリズムを微妙に改良しました。 領域の大きさ (幅+高さ) 上位 3 位までの中から適当に選んで、 分割することにしました。 これで、 不自然に大きな部屋が生成されたりすることもなくなり、 きれいな部屋割りのダンジョンができるようになりました。 こんな感じ。

772_製作過程A

それと、 マップの軽量化をしておきました。 どうやら、 オートタイルの処理が重かったみたいです。 マップの描画処理を毎フレーム行うのではなく、 マップ生成時にバッファに全て描画してしまって、 それを毎フレーム画面に表示する形にしました。 これで、 重かったオートタイルの処理を、 マップ生成時 1 回行うだけでよくなりました。 FPS も余裕で 40 をキープしてます。


comment ×0
03 / 04
Tue

絵がうまく描けるようになりたい Ziphil です。

Java のアプレットなんかでよくこんなコードを見ます。 途中省略してます。

public class TestApplet extends Applet {
  public void paint(Graphics graphics) {
    graphics.drawImage(image, 10, 10, this);
  }
}

こういうものなんだなーってことで、 これまで気にしてなかったんですが、 やっぱり気になります。 この第 4 引数って何ですか。 Java の API に聞いてみると 「イメージオブザーバ」 だと言われますが、 何ですかそれ。

簡単に言ってしまえば、 イメージを観察するオブジェクトみたいです。 image observer ですから、 そりゃそうですね。 じゃ、 具体的に何をしてるんでしょうか。

まず、 getImage メソッドなどによって、 変数に Image オブジェクトが格納されたとしましょう。 この時点では、 画像はまだ読み込まれません。 次に、 drawImage メソッドが呼ばれたとしましょう。 ここで初めて、 画像本体が読み込まれ始めます。 しかし、 drawImage メソッドが呼ばれたときから読み込みが始まってしまっては、 この時点で画像を描画することはできません。 そこで、 イメージオブザーバが逐一イメージの読み込み具合を観察して、 それに応じて何度も画像を描画してくれるわけです。

「イメージオブザーバなんて考えるの面倒だから null で良いじゃん」 ということで、 null を指定してみるとどうなるでしょう。 イメージオブザーバがありませんから、 イメージの読み込み具合に応じて再描画してくれなくなります。 drawImage メソッドが呼ばれた瞬間は、 まだイメージは読み込まれ始めたばかりですから、 この時点では何も描画できず、 結果として最後まで何も描画されません。

では、 ゲームなどのループ処理によって、 何度も drawImage が呼び出される場合はどうでしょう。 1 回目のループで drawImage が呼び出された時点では、 何も描画することはできませんが、 ループで何度も drawImage が呼び出されるので、 結果としてきちんと画像が表示されます。

まあ、 ざっとですがイメージオブザーバはこんなことをする便利なやつです。


comment ×0
03 / 03
Mon

Ziphil です。

アルゴリズム考察の続きです。 前回のエントリで、 曲線を使った方法を書きましたが、 もうちょっと具体的にしてみましょう。

まずは、 マップ全体を形作る閉曲線はどうやって描くか? 初期地点を決めて適当に曲げながら線分を描いていく方法を考えましたが、 これでは閉曲線になる保障がありません。 そこで、 最初から閉曲線を作っておいて、 それを曲げていく手法を採ってみようと思います。

  • マップ全体より少し小さめの長方形を作り、 その頂点に時計回りに 0, 1, 2, 3 のデータ値をもったノードを置く。
  • 数値が隣り合っている 2 つのノードを選び、 その中点から少しずれた位置に新たなノードを作成する。このノードは、 選んだ 2 つのノードのデータ値の相加平均のデータ値をもつ。
  • 上の処理を適当な回数繰り返す。

適当な線分描画アルゴリズム (ブレゼンハムのアルゴリズム) を使って、 隣り合うデータ値をもつノード間の線分を描画し、 通過領域とすれば、 洞窟ダンジョンの輪郭が決まります。 このとき、 輪郭の内部の座標を記憶しておきましょうか。

次は、 この閉曲線内に適当に道を作ります。

  • 通過領域から適当に 1 点選び、 内部に向かって適当な長さの線分を引き、 通過領域に変える。
  • 線分の先の点から、 前回引いた線分とのなす角が 60° 未満になるように、 新たな線分を引き、 通過領域に変える。
  • 上の線分を引く処理を、 通過領域に衝突するまで続ける。
  • 最初の処理から順に同じ処理を適当な回数繰り返す。

これで、 輪郭がいくつかの領域に分けられるはずです。 後は、 部屋になる部分を決めます。

  • 輪郭の内部の通過不能領域である点を適当に選ぶ。
  • その点を含む領域を、 部屋として通過可能領域とする。
  • 以上の処理を適当な回数繰り返す。

これで部屋の完成し、 空洞つきの洞窟ダンジョンのマップが完成する・・・と思います。

このアルゴリズムの欠点ですが、 空洞の中央から通路が伸びるということがないことでしょうか。 まあ、 後から通路を追加すれば良いだけの話ですが。 詳しいことは、 実際にコードを書いて動かしてみないことには分かりませんね。 実装はまだ今度やろうと思います。


comment ×0
03 / 03
Mon

Ziphil です。

昨日はランダムダンジョンを自動生成するコードを書いたわけですが、 やっぱりあれを洞窟のダンジョンに使うには不自然。 ということで、 自然物のダンジョンをそれっぽく生成する方法を考えてみます。

昨日のアルゴリズムの何が不自然かと言うと、 一番は部屋の形でしょう。 長方形ですから、 自然物らしくありません。 そこで、 長方形の部屋の角や辺を少し削って、 部屋に丸みを帯びさせるというのが最も簡単です。

根本からアルゴリズムを見直してみましょう。 こんな生成方法が思いつきます。

  • 適当な位置を中心とし、 楕円状の通過領域を作る。
  • 適当な方向に移動し、 同じように楕円状の通過領域を作る。
  • 上の移動を何度か繰り返す。

楕円状の通過領域の形を毎回微妙に変えてやれば、 それっぽい洞窟の通路ができそうです。 まあ、 これだけでは一本道なので、 分岐を作ったりの処理も入れなければなりませんが。

こんな方法も思いつきました。

  • マップ全体に閉曲線の通過領域を描く。
  • 通過領域の適当な 2 点を結ぶ、 適当な曲線の通過領域を描く。
  • 上の曲線の描画を何回か繰り返す。
  • 閉曲線になっている部分をいくつか選び、 その内部を全て通過領域として部屋を作る。

直線要素がなくなるので自然物らしくはなりますが、 閉曲線を描くアルゴリズムが難しそうですね。

さて、 左上にミニマップそ表示するプログラムを追加したので、 昨日のアルゴリズムで生成したランダムダンジョンのサンプルを上げておきましょうか。 サイズは 80×80 です。

771_製作過程A

ちなみに、 変に細長い部屋が作られないように、 領域が縦長なら横に分割し、 領域が横長なら縦に分割するように、 昨日のプログラムを改良してあります。 また、 領域の最小の長さを、 5 から 10 に変更してあります。

上の画像ではよく分かりませんが、 実は FPS が設定値の 40 を下回っています。 やっぱり描画が遅いみたいで、 これは昔 StarRuby のときにやっていた軽量化処理を施さないといけないのでしょうか。


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
03 / 01
Sat

お久しぶりです、 Ziphil です。

いろいろあってゲーム製作をする時間がなく、 2 ヶ月ほどブログを放置してしまったわけですが、 生きてます。

正直いろいろなことを忘れてしまって、 ソースコードなどを見ても、 自分が何をやっていたか忘れてしまっている感じです。 ドット絵もしばらく描いてないんで、 どんな感じで描けば良いか分からなくなってます。 しばらくの間は、 ドット絵描いたりプログラムを書いたりして、 リハビリをしていこうと思っています。 そういえば、 途中からマップの 1 マスを 18px から 24px に変更したせいで、 武器のアイコンのサイズが微妙に小さかったんですよね。 前の絵をもとに描き直せば、 良いリハビリになりそうです。

開発は今のところ Eclipse 使ってるんですが、 個人的には IntelliJ IDEA の方が好みなんですよね。 ただ、 IntelliJ で Ruby 開発するには有料版が必要で、 仕方なく諦めたんです。 30 日間の無料体験版がありますし、 モチベーションを上げるためにもちょっと入れてみましょうかね。 Ruby の開発環境というと、 IntelliJ 系列で RubyMine があるんですが、 これは Ruby 専用なので IntelliJ の有料版を購入した方が融通は利くんでしょうか。

じゃ、 リハビリやりましょうかねー。


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