Skip to content

Instantly share code, notes, and snippets.

@shinokaro
Last active December 6, 2015 11:51
Show Gist options
  • Save shinokaro/4997033b6d344d7d8b6e to your computer and use it in GitHub Desktop.
Save shinokaro/4997033b6d344d7d8b6e to your computer and use it in GitHub Desktop.
DXRuby Advent Calendar 2015 デジゲー博のレポートとその時使用したFiberテクニック

デジゲー博のレポートとその時使用したFiberテクニック

はじめに

DXRuby Advent Calendar 2015も6日目になりました。本日は、しのかろがお送りします。

前日はあおいたくさんの記事でした。あおいたくさんには昨年のDXRuby Advent Calendaで大変お世話になりました。(※詳細は今年の記事、8の項目参照)。あの時は私がGitの使い方に不慣れであった事もあり、ご迷惑をおかけしたと思います。リファクタリングのお付き合い、ありがとうございました。

デジゲー博2015参加レポート

デジゲー博2015について

デジゲー博とはインディー・ゲームの展示発表イベントです。インディー・ゲームとは自主制作ゲームのことです。開催日は11月15日、会場は秋葉原UDKです。

当日は朝から雨が降っていましたが、会場は入場開始から大入りでイベント閉会まで人がたくさんいました。内容としては今年も色々な自主制作ゲームが発表されていました。テーブルサイズの大型モニターを利用したゲームなんかもありました。特殊なデバイスを利用したゲームをプレイアブル展示ができるのがイベントの醍醐味ですね!

もちろん自分の所属するサークル「ばったりワークス」も昨年に引き続き、デモンストレーションの展示で参加しました。

展示内容

サークルの趣旨はRubyとDXRubyでゲームが作れるぞ! です。そして、今年の展示作品はオリジナルのパチスロ・ゲーム(※リンク先に当日の画像あり)です。

今回のデモンストレーション作成にあたって衝撃の紅さん、ヴェクトルさんの協力してもらいました。衝撃の紅さんは企画、キャラクターデザイン、演出、イラストを担当していただきました。ヴェクトルさんはキャラクターのセリフとパチスロについてレクチャーをしていただきました。

パチスロはスロットとは違い、日本独自のゲームシステムです。そのため、まずパチスロの仕組みやルールを調べることから始めました。製作前に実際に本物のパチスロを打ったり、法律を調べたりしました。

その成果により実物に近いリールの制御を行っています。分間80回転が法律の上限なので、45フレームで1回転、1回転あたり21絵柄で……と地道な設計からはじめました。実機と同じで停止ボタンを押したあと、すこし滑ります。法律の通りに内部抽選結果を全力で引き込むプログラムは苦労しました。

展示の反響

7名ほど、コントローラーを手にとってプレイしていただきました。一番受けたのは、やはりパチスロ経験者の方でした。リール停止時に僅かに絵柄がずれるところまで作りこんだ甲斐がありました。

ライブ作成

おかげさまで、当日に演出の実装が間に合いませんでした。しかし、PC持込でのデモンストレーションです。そこでイベント中にプログラムを実装しました。その場(ライブ)で演出コードが書けたのは後述する仕組みを完成させていたからです。

余談ですが当日、私はキーボードを叩きながら「なぜ、今、みんなプログラミングしないんだ!」と思ってしまいました。しかしそれはイベントの趣旨を履き違えでしたね。来年は展示物を完成させた上で、展示中にプログラミングしようかと考えています。

class Flow < Fiber

目的

ゲーム演出をRubyのコードで書ける様にする。それがFlowクラスとそのインスタンスの目的です。Flowインスタンスを使う理由は2つありあます。

最初に**「ゲームの進行とRubyコードの実行を一致させる」**そうすればRubyコードそのものがゲームのシナリオや演出のコードになります。

次に**「ゲーム内時間の進行を画面表示と同期させる」**これがないと勝手にゲームが進行してしまいます。

コード

require 'fiber'
require 'singleton'
require 'dxruby'

class Flows < Array
  include Singleton

  def update
    Sprite.clean  self
    Sprite.update self
  end
end

class Flow < Fiber

  def vanished?
    !alive?
  end

  alias update resume
end

解説

まずFlowsクラスから説明します。これはFlowインスタンスのマネージャークラスです。プログラムのどこからでも呼び出せるようにシングルトン・パターンを使って定義しています。レシーバー呼び出しはFlows.instanceです。Arrayクラスを継承していますので、Flows.instanceFlowインスタンスを追加したり削除したりする場合はArrayの操作と同じです。要は手早くマネージャー機能に必要なメソッドを手に入れているわけです。

注目点はupdateメソッドです。ここで、DXRubyのSpriteクラス・メソッドであるSprite.cleanSpritre.updateを使用しています。これは次のFlowクラスの説明で意味を持ちます。Sprite.cleanSprite.updateは入れ子になった配列も再帰的に走査してくれるので大変重宝します。

次はFlowクラスです。これはFiberを継承しています。目を引くのはvanished?メソッドの定義です。メソッド内部でFiber#alive?の結果を反転しています。ここで先ほどのSprite.cleanが意味を持ちます。Sprite.cleanvanished?メソッド呼び出しの結果がtrueの時、配列からそのオブジェクトを取り除いてくれます。

こうすると次のupdate呼び出し(Flowインスタンスではresumeのエイリアスとして定義)の時に終了したFiberを起こさないようにしています。終了したFiberを起こすと例外になります。

あとはWindow.loopからFlows.instance.updateを呼び出します。毎フレームごとにFlows.instanceに登録されたFlowインスタンスが実行されます。Flowインスタンスは自身のファイバーが終了すれば勝手にFlows.instanceから削除されます。

もちろんFlowインスタンスに定義するブロック内ではフレームごとにFiber.yieldで離脱する必要があります。一方で、1フレーム中に実行すべき処理を確実に行うことができます。この確実に、というのが重要です。

使用例

# 利便性のためにヘルパーメソッドを用意する。
module Kernel
  #
  # Flow生成とFlowsへの登録を行う。
  # ここではlambdaを受け取るように変更している。
  # ブロックではなくlambdaとした理由はreturn, break, nextを呼び出しても例外が発生しないから。
  #
  def flow(lambda_obj)
    Flows.instance << Flow.new(&lambda_obj)
  end
  #
  # 現在のフローからメインファイバーへ一時的に離脱する。
  #
  def wrap
    Flow.yield
  end
end
#
# ゲーム進行の記述
#
flow -> {
  #
  # サンプルのため画像を表示せず文字列を標準出力へ出力する。
  #
  puts "Push SPACE Key"

  until Input.key_push?(K_SPACE) do
    # ポーリング処理、ユーザのキー入力を待つ。
    # ループの終わりで現在のフローから抜け出しDXRubyへ制御を戻す。
    wrap
  end
  #
  # フロー中に新しいフローを作り出してよい。
  # 新しいフローは、次のフレームから動作する。
  #
  # 4つの独立したフローを生成
  #
  new_flows = 4.times do |i|
    #
    # フローを生成。ここでは乱数で決められたフレーム数だけ、自身の番号を出力する動作を行う。
    #
    flow -> {
      rand(60).times {
        puts i; # 自身の番号を出力
        wrap;   # フレームあたりの処理を終えたら離脱する
      }
    }
  end
  #
  # ゲーム本体のフローは60フレーム(1秒間)待機する。
  # ゲームが進行した際に、過去の演出フローを取り除く仕組みがあれば待つ必要はない。
  #
  60.times { wrap }
  #
  # 最初に戻る。無限ループ
  #
  redo
}
#
# DXRubyの制御ループ。ユーザのキー入力を受け取るためには、このループを回す必要がある。
#
Window.loop do
  Flows.instance.update
end

つまり、演出側から指示された順序通りにコードを記述すれば良いのです。これで、最初の目的は果たせました。

補足

今回の実装では二番目の目的も果たすことができています。しかし、なぜプログラムの動作が画面表示(フレーム)と同期する必要があるのでしょうか。

本来、コンピューター・ゲームは画面表示のタイミングと密接に関連して本体プログラムが動作するフレーム同期方式でした。最近では3Dゲームの出現により画面表示と関係なくゲーム本体のプログラムが動作するフレーム非同期方式が出てきているようです。

そこで私は、Ruby & DXRubyでゲームを作る場合はフレーム同期方式とフレーム非同期方式どちらが良いか検討しました。 その結論はフレーム同期方式です。なぜならコンピューター、OS、ライブラリー、言語のいずれも実時間を厳密に守れないからです。フレーム同期方式でプログラムを組めば演出時間が確実に計算可能になります。演出において時間の保証は必要条件です。

以上のことからThreadではダメな理由もわかります。ThreadはRubyが勝手にフローを離脱させてしまいます。そうさせない方法もありますが手間が掛かります。Fiberがあるならそっちを使おうということです。

おわりに

今回掲載したコードは自由に利用して構いません。コピペなり改造するなり自由です、私の名前を記載する必要もありません。

それから、もしも記事を書くことができればRubyとDXRubyについての記事を書きたいと思っています。といっても、予定は未定ですが。

次回

明日は土井ヴぃさんの「画像素材を用意せずにいきなり始めるゲーム開発PIZZA COOKER解説」です。 画像素材を用意するのは時間が掛かります。もし、画像素材がそろう前にゲーム開発ができるのならプロトタイプ開発も可能になります。そうすればゲーム製作のスピードも上がりますね!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment