読者です 読者をやめる 読者になる 読者になる

雑念日記

主に技術的なことをつらとらと(書ければいいな)。

役に立ちそうで役に立たない少し役に立つかも知れないDXRubyローグライクのノウハウ

とうに旬が過ぎたネタ改変をタイトルに掲げた本投稿はDXRuby Advent Calendar 2014 22日目の記事です。

21日目はあおいたくさんの「DXRubyユーザーのための標準添付ライブラリの紹介(前編)」でした。
「こういうのあると便利なんだけどなー」なんて思ったクラスやメソッドなんかは大体用意されてたり、あるいは誰かが作って公開してくれてたりするんですが、その存在自体を知らないと利用しようがないです。今回のAdvent Calendarであおたくさんは8日目の記事も合わせて便利ライブラリを横断的に紹介してくれていてとても助かりますね!

さてそれでは僕の記事の内容ですが、これまでの記事達ですっかりハードルが上がってる上に、内容が被るのも避けたいトコロ。(次の機会があったら早めの日程にしよう...。)うんうん頭を捻っていたわけですが、そもそも参加のキッカケが、DXRubyでローグライク不思議のダンジョンライク)を作っていたところAdvent Calendarにお誘いをいただいたというもので。


DXRuby/ruby-processingでローグライクゲーム その1 - 雑念日記

ならばこの不思議のダンジョンライクゲームに関する記事にしようではないかと。完成していない・誰も触れないものについての記事を描くのは若干気が引けますが、タイトルの通り、役に立つかどうかわからない自前不思議のダンジョンライク実装のTipsです。Tipsというか「ここをこうしてみたら捗ったぞよ」的な。

まだゲーム自体のコードを公開できていないので断片的な情報となってしまい(そのため実際のコードと細部が異なる点も多々あり)ますが、もし参考にしていただけたり、あるいは「もっといい方法があるよ!」と指摘をいただけたりすると嬉しいです。

目次

あんまりダラダラ書いてもアレがナニなので絞って以下。

  • ダンジョン生成
  • テキストの辞書化
  • パラメータのDSL
  • キーコンフィグ
  • おわりに

ダンジョン生成

f:id:hoshi_sano:20141210101721j:plain

ダンジョン生成のアルゴリズムにはよくあるこういうやつを使っています。

f:id:hoshi_sano:20141214203852g:plain

ここでは特に詳細について書きませんが、「ローグライク 生成」とかでググるとたくさん参考情報が出てくるので気になる方は調べてみてください。

前述のようにゲームのソースコードは公開できていませんが、ダンジョン生成部分だけはGithubに置いてあります。


hoshi-sano/meiro · GitHub

DXRubyに依存していないためどの環境でもお試しいただけるかと思いますが、ドキュメント等々を全然整備していないので誰も使い方がわからないと思います。よろしくないですね。

生成されたダンジョンには敵キャラやアイテムをこれまたランダムで設置します。マップが自動生成されると、自分で定義することがグッと減るから楽でいいですよね。ただし何か問題があった場合に再現できないと困るので、ゲーム全体で単一共通の乱数生成器を使う点に気をつけています。そのあたりは昨年のAdvent Calendarでmieki256さんが詳しく書いてくださっています。

mieki256's diary - DXRubyと疑似乱数

テキストの辞書化

f:id:hoshi_sano:20141210101733j:plain

ゲームを作っているとアイテム名やメッセージなどを表示する機会が頻繁にあるため、文字列をよく使います。
例えば単純に作るとこんな感じになるかと思います。

# 薬草
class Yakusou < Item
  NAME = "薬草"
  NOTE = "使うとHPが少し回復するよ"
end

# いやし草
class Iyashisou < Item
  NAME = "いやし草"
  NOTE = "使うとMPが少し回復するよ"
end

# おっさん
class Ossan < Character
  def talk
    show_message("武器や防具は装備しないと意味がないぞ!")
  end
end

# お姉さん
class Onesan < Character
  def talk
    show_message("あらあなた素敵ね。ぱふぱふしない?")
  end
end

でもこれだとあとから「アイテム説明文が馴れ馴れしいよ!丁寧語にしろ丁寧語に!」みたいな横断的な変更があった場合に、複数のクラス(≒ファイル)にまたがって変更しなければならなかったり、デバッグ中に誤字を見つけた時に修正対象のメッセージがどこにあるかわかりづらくなったりしちゃいます。そもそもの話、ロジック(コード)とデータは分離してまとめておきたいですよね。

ので、yamlにまとめました。

:items:
  :yakusou:
    :name: "薬草"
    :note: |-
      薬草【やくそう】
      使うとHPが少し回復するよ
  :iyashisou:
    :name: "いやし草"
    :note: |-
      いやし草【いやしそう】
      使うとMPが少し回復するよ
:messages:
  :pafu_pafu: "あらあなた素敵ね。ぱふぱふしない?"
  :recommend_equipping: "武器や防具は装備しないと意味がないぞ!"
  :level_up: "playerはレベルlevelに上がった。"
  :damage: "pointポイントのダメージを受けた。"
  :kill: "targetをやっつけた。"

yamlについては@vivit_jcさんが13日目の記事で書いてくださったので、そちらを読んでいただければこのセクションはだいたいOKです。


YAMLでゲーム内データを定義する - いつクリはてブロ

もう少し僕のゲーム内での使われ方について記述すると、以下のような感じで上記のyamlをロードして利用する辞書クラス(またはモジュール)を作ってしまえば、

require 'yaml'
require 'active_support/core_ext/hash/keys' # for stringify_keys

# 辞書用のモジュール
module Dictionary
  LIST = YAML.load_file('/path/to/dictionary.yml')

  module ModuleMethods
    # アイテム情報を返す
    def get_item_info(key)
      LIST[:items][key]
    end

    # メッセージを返す
    # optionsを指定した場合、キーに指定した文字列を値に指定した文字列で
    # 置換する
    def get_message(key, options = {})
      options.stringify_keys! # HashのキーをSymbolからStringに変換
      regexp = Regexp.union(*options.keys) # キーから正規表現を生成
      LIST[:messages][key].gsub(regexp, options) # 置換して返す
    end
  end

  extend ModuleMethods
end

↓こんな感じでテキストを取得できるので、

Dictionary.get_item_info(:yakusou)
#=> {:name=>"薬草", :note=>"薬草【やくそう】\n使うとHPが少し回復するよ"}

Dictionary.get_message(:pafu_pafu)
#=> "あらあなた素敵ね。ぱふぱふしない?"

個々のクラスにテキストをベタ書きする必要がなくなりますし、集約されているためラベルさえ変わらなければ文言の変更があっても辞書ファイルの中身を修正するだけで済みます。

あとこのあたり

    :level_up: "playerはレベルlevelに上がった。"

がミソで、「誰々が何々にほげほげのダメージを与えた」みたいな一部を変更して繰り返し使い回すメッセージは、正規表現などを使って置換できるようにしておけば、

Dictionary.get_message(:level_up, player: "勇者", level: 5)
#=> "勇者はレベル5に上がった。"
Dictionary.get_message(:level_up, player: "魔法使い", level: 3)
#=> "魔法使いはレベル3に上がった。"
Dictionary.get_message(:kill, target: "スライム")
#=> "スライムをやっつけた。"

↑こんな感じでオプションに渡す値を変えるだけで目的のテキストがスッキリ取得できますし、辞書ファイルを見に行かなくてもコードを見ただけで大体どんなメッセージが利用されているかが想像しやすいです。

パラメータのDSL

f:id:hoshi_sano:20141210101745j:plain

ゲームを作っているとキャラクターやアイテムにいろんなパラメータを設定しなければならないですよね。
STGだったら、スピードとか耐久力とか弾の種類とか。RPGならHPとかMPとか攻撃力とか防御力とか。
RPGローグライクのように同じ種類の敵キャラやアイテムを同じパラメータで使いまわす場合は、種別ごとにクラス化する感じの実装になるかと思います。

# スライム
class Slyme < Enemy
  LEVEL  = 1
  MAX_HP = 10
  MAX_MP = 0

  def initialize
    @level = LEVEL
    @hp = MAX_HP
    @mp = MAX_MP
    @max_hp = MAX_HP
    @max_mp = MAX_MP
  end
end

↑の例はちょっと極端ですが、ごちゃごちゃしちゃっていて、同じ感じでたくさん敵キャラクターを定義するのはしんどいです。もうちょっと楽ができつつ見通しが良いようにしました。
まずは上位のクラスでパラメータを簡単に記述できるような仕組みを用意します。例えばこんな感じ。

# 敵キャラクターのベースとなるクラス
class Enemy
  class << self
   # HPの設定
    def hp(value)
      @default_hp = value
    end

    # HPの取得
    def default_hp
      @default_hp || 1
    end
  end

  def initialize
    @hp     = self.class.default_hp
    @max_hp = self.class.default_hp
  end
end

パラメータはHPだけじゃないので黒魔術使ってもうちょっと増やします。やってることは↑と同じです。

# 敵キャラクターのベースとなるクラス
class Enemy
  class << self
    [
     :hp,      # HP
     :mp,      # MP
     :level,   # レベル
     :power,   # 攻撃力
     :defence, # 防御力
     :exp,     # 経験値
    ].each do |param_name|
      define_method(param_name) do |value|
        self.instance_variable_set("@default_#{param_name}", value)
      end

      define_method("default_#{param_name}") do
        self.instance_variable_get("@default_#{param_name}") || 1
      end
    end
  end

  def initialize
    @hp     = self.class.default_hp
    @max_hp = self.class.default_hp
    @mp     = self.class.default_mp
    @max_mp = self.class.default_mp
    @level  = self.class.default_level
    ...(省略)...
  end
end

上のEnemyクラスを継承すると、さっきのスライムの定義は以下のようになります。

# スライム
class Slyme < Enemy
  level    1
  hp      10
  mp       0
  power    3
  defence  2
  exp      5
end

Slyme.new
#=> #<Slyme:0x00000001591e78 @hp=10, @max_hp=10, @mp=0, @max_mp=0, @level=1, @power=3, @defence=2, @exp=5>

ドキュメントっぽいコードになって、敵キャラクターの能力が一目で見渡せます。これなら敵を増やすのも楽チン。
ここからは妄想の話で、ゆくゆくは攻撃パターンとかもタイプ化してラベルで指定できるようにすれば、個々の敵キャラクタークラスにコードを書く必要が無くなってこれ↓で完結しちゃったりして、

# スライム
class Slyme < Enemy
  level    1
  hp      10
  mp       0
  ...
  attack :normal # 通常攻撃
  weak   :fire   # 火が弱点
end

であれば最早クラス定義そのものもコードを書く必要はなくて、以下みたいなyamlから動的に敵キャラのクラスを定義できるようになるかもしれません(それが良いか悪いかは別として)。

:enemies:
  :slyme:
    :name: "スライム"
    :image_file: "images/enemies/slyme.png"
    :attack: :normal # 通常攻撃
    :weak:   :fire   # 火が弱点
    :status:
      :hp: 10
      :mp: 0
  :cerberus:
    :name: "ケルベロス"
    :image_file: "images/enemies/cerberus.png"
    :attack: :three_way # 3方向攻撃
    :status:
      :hp: 250
      :mp: 120

そうなるとだんだんフレームワークっぽい感じになってきて、最終的に「誰でもyamlを書くだけでオリジナルのローグライクゲームができちゃう!」みたいな夢がひろがりんぐ。
妄想終わり。

キーコンフィグ

f:id:hoshi_sano:20141210101757j:plain

操作性の悪いボタン割り当ては大変ストレスフルです。可能であれば自らが一番気持ちよく操作できるボタン配置でプレイしたいものです。

それなら、通常こんな感じで書くところを、

  # 攻撃ボタンが押されたら
  if Input.key_push?(K_Z)
    player.attack
  end

こんな感じで書いてあげるのはどうでしょうか。

  # デフォルトのキー割り当て
  DEFAULT_KEY_CONFIG = {
    attack: K_Z, # 攻撃
    menu:   K_X, # メニュー
    sort:   K_C, # アイテムの整理
  }.freeze

  # 既存のキー設定の読み込み
  # 無ければデフォルトの設定
  @key_config = load_key_config || DEFAULT_KEY_CONFIG.dup

  if Input.key_push?(@key_config[:attack])
    player.attack
  end

あとはキー割り当て用の画面を用意してあげて、@key_configの中身を変更可能にしてあげればキー設定が可能になります。上の例ではキー設定はただのHashなので、これもやっぱりyamlに吐いておいて、次の起動時にそのyamlを読み込むようにしておけば、変更したキー設定の永続化も可能です。

ただし、ゲームプレイ続行不可能になるようなキー設定にしたり、一つのボタンに複数の役割を割り当てたりができないように、設定内容を検証する処理を行わなければならない点に注意です。

おわりに

以上です。
あまりローグライクやDXRubyに特化した内容が書けなかったのが心残りですが、これら細々としたホニャララを積み重ねてDXRuby不思議のダンジョンライクは鋭意製作中です。
...嘘です。プライベートが何かと忙しかったりCrypt of the NecroDancerとかにハマったりしてしまって現在進捗はズタズタです。
いずれにせよ、何かひとつでも興味を持っていただけるTOPICがあったのであれば幸いです。

明日23日目はみれいゆーさんの「DXRubyでノベルADVエンジンを作った(仮)」です。
ノベルゲーム制作というのもかなり人気のあるジャンルですよね。僕なんかはゲーム作ろう!ってなると、剣と盾持ったキャラクターをぐりぐり動かして...という方向に進みがちなのですが。明日の記事に関心が高い人も多いのではないかと思います。乞うご期待!