TETRISiON 〜 もし水口哲也がテトリスを作ったら(原題)


SiON TETRISizer | Si+(wonderfl.net)
ものすごく久々にゲーム作った,といってもテトリスだけど.
なんだかんだSiONの更新には半年以上費やしてしまっていて,その間にゲーム作りたい衝動がかなり高まってた.wonderflではscoreapiとか導入されたりしてるし.今回はとりあえずリハビリのつもりでテトリスにしたんだけど,連鎖の概念を導入しようとするとフィールドに落とした後もブロックの形状を覚えとかなくちゃならないとか,せっかくだからARIKAテトリスみたいにぬるぬる動かそうとするとブロック移動判定が全然変わるとか,いろいろやってたら結構面倒だった.
製作期間はテトリス本体1日+音楽1日+バランス調整2日+演出関係2日で大体1週間.意外に全盛期のスピードをいまだに保ってて自分でもびっくりした.もっともベースがテトリスルミネスって最初から決まってて,企画やバランス調整がかなり楽だったのはある.
ゲームバランスの面では,横八列にすることで隙間を空きやすくして偶発連鎖を起き易くする.mixiテトリスのパクリのストックブロックで連鎖構築をしやくする.全部で32回しか消せない制限で一回のブロック消去で消すライン数の重要度を上げる.連鎖数と消去ライン数の点数上での重みを同じにする.と連鎖をメインのチューニングにした.


といってもこれはあくまでもゲーマー視点であって,スコアを気にしなければ連鎖なんてどうでも良くなるので,一般人からすれば単なる音カジュゲー.
[id:ABA:20100512]にもあるけど,ベッタベタのゲーマーはカジュアルゲームをどう調整していいのかさっぱり分からない.テトリスは,落ちモノゲーマーからすると,別ウィンドで動画を見ながらでもできるくらいの超カジュアルゲー.なので,今回のTETRISizerのチューニングは,自分の中では"だいたい聴いてるだけゲー"のつもりだった,だけど,スコア見てると何かかなり認識が間違ってるっぽい.一般人からするとテトリスはかなり高難易度なアクションパズルゲームに分類されるんじゃないかと思った.
そう考えると Rez, SpaceChannel5, Luminesって,ゲーマーにとっては"だいたい聴いてるだけゲー"だったけど,そうは言ってもそんなには簡単ではなかったような.音ゲーは,シューティングと同様,深刻な難易度インフレがおきてるジャンルのひとつなので,ここら辺の"音に乗せるゲー"も音ゲーのインフレの影響で,いつの間にか難易度が釣り上がってしまう傾向があるのかもしれない.
もっともっと簡単にしてこそ,真の"聴いてるだけカジュアルゲー"が見えてくるのかな.
そいえば,((♪)) | Si+のマウスオペレーションって,結構,真の"聴いてるだけカジュアルゲー"に近いかも?と思った.Puppyish Pentatonicism | Si+までいってしまうと,プレイヤーの介入する余地がほとんどなくなるので,どうかと思うけど.でも,これだってテンポと音が変えられるわけで,りっぱなカジュアルゲームなのかなー...んー.

SiON version0.60 SoundObject Quartet


SiON SoundObject Quartet | Si+(wonderfl.net)
ひっさびさに全力でwonderflってます.もともとデバッグ用に使ってたUIを遊べる感じに修正してアップしました.788行のほとんどがUIコーディングで,カスタムコンポーネントも3つばかり作成しています.
で,SiON も半年ぶりにアップデートです.年頭(一つ前の記事)で表明していたように,SiON を一般化すべく,ずっとリファクタリングしてました.脱MMLを目標に SoundObject という概念を導入してみました.詳細はまた改めて書きますが,上記のデモのコードに結構がんばってコメントを付けてみたので参考にしてみてください.もしくは,JActionScriptersの方にそれなりにがんばっててんぱってる感じの英語で解説を書いてますので,良かったら読んで下さい.

4.音と映像とインターフェイスの連携

前章3. SiON と DisplayObject の連携の続きです.後半では,インタラクティブな音生成Flashにおいて重要になると考えられる,音の遅延といかに上手に付き合うか,について少し考えてみようと思います.

インターフェイスとの連携

クリックやキー入力といったユーザーアクションに反応して音を鳴らす場合,delayとquantize を0に設定して最速で音を生成します.音の合成は関数呼び出し後の最初のストリーミング時に行われるため,発音の遅延は一定ではありません.
ユーザーアクションに対応して"音楽と同期した"音を鳴らす場合は,delay と quantize で同期タイミングを設定して呼び出します(Synchronized Sequence).この場合,音楽に同期させるための遅延が余計に発生します.この"音楽に同期させるための遅延"は,SiMMLTrack::trackStartDelay プロパティで取得できます.
これらの遅延された発音のタイミングは Event Trigger によって取得します.NOTE_ON_STREAM イベントハンドラで SiONTrackEvent::frameTriggerDelay プロパティを参照するか,NOTE_ON_FRAME イベントハンドラを使用してください.
ユーザーアクションによる音と音楽と映像を連動させる場合は,ユーザーアクション→音楽と同期して音生成→映像リアクションと3段階の遅延が発生します.SiON では,Synchronized Sequence と Event Trigger によってこれらの遅延を処理します.

var driver:SiONDriver = new SiONDriver();
driver.addEventListener(SiONTrackEvent.NOTE_ON_FRAME, _noteOnFrame);
addEventListener(Event.CLICK, _onClick);
driver.play();
...

// クリックで音生成,
private function _onClick(event:Event) : void {
    // delay=0(第4), quant=4(第5) で四分音符に同期 (Synchronized Sequence).
    // eventTriggerID=100 (第7),noteOnType=1 (第8)で,NOTE_ON_FRAME イベントを発行(Event Trigger).
    driver.noteOn(60, null, 4,  0, 4,  0,  100, 1, 0);
}

// 音が発音されるタイミングでNOTE_ON_FRAMEイベントが発生.
private function _noteOnFrame(trackEvent:SiONTrackEvent) : void {
    // eventTriggerIDで発行元をチェック
    if (trackEvent.eventTriggerID == 100) {
        (音に合わせた演出)
    }
}

視覚刺激と聴覚刺激

一般的に視覚は聴覚に比べ時間分解能が低い事が知られています.このため,視覚刺激が聴覚刺激に対して近いタイミングで起きると,視覚刺激は聴覚刺激に引きずられて同じタイミングだと錯覚します(感覚的な問題で文献によっても様々ですが約100ms程度は吸収される?).
SiON における DisplayObject との同期は不完全で,理論上約±20ms(60fpsで2フレーム程度)のバラつきが発生します.このバラつきを補正する事は技術的に可能ですが,そのために一律50msの遅延が必要になるため,現状では入れていません.
ビートに対して音を同期させる場合は厳密に合わせる必要がありますが,絵とビートの同期はそれ程しっかり合わせる必要はありません.この事を念頭に置いておくと,どうしても発生してしまう音の遅延と上手に付き合えるんじゃないかなと思います.

遅延描画と先行判定

例えば Box2D などの物理エンジンで衝突判定に対して音をつける事を考えます.基本的にはユーザーアクションと同様,衝突時に最速で合成させます.この遅延は,一応,上述の錯覚の範囲内に入ります.ただし,バッファサイズ=2048では計算上70±20ms程度の遅延が考えられるためギリギリと言えます.
この音に Synchronized Sequence を設定して音楽に同期させます.すると,上述のように視覚が聴覚刺激に引きずられるため,ボールが壁に当たるタイミングがあたかも音楽にあってるかのように見えます.しかし,この場合の遅延は,錯覚で吸収される範囲を超える事があり,音が遅れて聞こえてしまいます.
● - wonderfl build flash online
このデモは SiON の実装実験中に作ったもので SiON も Box2D も使っていませんが,ランダムなタイミングに発生する床とボールの衝突音をビートに同期させています.クリックでボールを1つづつ落とすと,時々床衝突に対して音が遅れて感じる事があると思います.これは音のズレが錯覚の範囲を超えているためと考えられます.
このデモでは最大200ms程度の遅延が起きていると考えられます.この 200ms という数字は,70±20ms の最速でも存在する遅延と,最大110msのビート同期による遅延を足した大体の値です.つまり 200ms = 50msの固定遅延+150msの変動遅延 と捕らえる事ができます.
もし,この変動遅延150msの中心のタイミングで映像上ボールが床に衝突すれば,バラつきは±75msとなり,理論上,音が衝突と一緒に起きていると錯覚できる範囲に収まることになります.このようなタイミングに衝突を合わせるは,50+75=125ms前に衝突が分かっていれば良い事になります.
これは60fpsで7フレーム程度レンダリングしておいて遅れて映像を出す,または現在の速度と位置から衝突予想時間を概算する事で実現できます.ユーザーアクションに対する反応はこうは行きませんが,あらかじめある程度決まった物理法則などの場合,こういった方法で遅延を打ち消す方法があります.
((♪)) - wonderfl build flash online
このデモは,上述のように音の生成を床との衝突より数フレーム前に行っています.実際にはある程度の速度である程度床に近づいたら音を生成する,という非常に適当な近似を行っているのですが,前のデモよりも音の遅延が少なくなったと感じれると思います.
現実に125ms先読みを行うと,音が前のめり過ぎて違和感を感じます.映像より若干音が遅いほうが自然に感じるようです.ここら辺は個人の感覚で微調整するのが良いと思います.

遅延演出

例えば ホーミングレーザーを撃って敵に着弾し爆発する際の音をつける事を考えます(つまりRez).この爆発音を音楽に同期させる場合,ホーミングレーザーの着弾時間を微調整する事で遅延を消す事ができます.
具体的には,ホーミングレーザーを撃った時点でホーミングレーザーの着弾時間分のdelayと同期 quantize を設定しておいて爆発音を生成してしまいます.この際,実際に生じる遅延時間をSiMMLTrack::trackStartDelayで取得してホーミングレーザーの軌道を計算する事で,爆発音が遅延なしに音楽に同期しているように見せることができます.
また,シューティングゲームで機体の爆発を音楽に同期させるような場合は,例えば小爆発してからタイムラグがあって破壊,のような演出を行えば,遅延をそのままタイムラグに当てはめればごまかす事ができます.これは拙作ゲーム"Quantized Blaze"で使った同期方法です(http://www.vector.co.jp/soft/dl/win95/game/se397592.html).自機被弾の演出が比較的分かりやすいと思います.
このように,遅延を積極的に演出に組み込む事でも,遅延を感じさせないインタラクティブな音表現が可能となると思います.

遅延を緩和するユーザーインタフェイス

例えば,キーボードで鍵盤楽器を模した場合,遅延の影響で打鍵から発音にはタイムラグが生じます.実際の鍵盤楽器はキーを押した瞬間に音がなりますから,このタイムラグは鍵盤楽器を弾いたことのある人なら不快なものとなる可能性があります.
一方,マウスのクリックで音が鳴る場合,同様にタイムラグが発生しますが,クリックはボタンを離した瞬間に決定される場合も多いです.このためタイムラグはそれほど気にならないかもしれません.
このように遅延はそのユーザーアクションの種類によっても感じ方が大きく変わってきます.映像リアクションの見せ方次第でも感じ方は大きく変わってくると思います.

3.SiONとDisplayObjectの連携

Flashでは(というか特殊なハードを介さない普通のPCアプリケーションでは),映像は描画したフレームで即出力されるのに対し,音は実際の発音より早いタイミングで合成しておく必要があります.このため,音を合成してから音の出力までには必ず遅延(レイテンシ)が発生します.
この遅延のため,ユーザーのアクションに反応する音は遅れ,音と同時に映像を処理すると映像が早くなります.現バージョンの Flash では理論上最短でも50ms程度の遅延が発生するため(実際にはもっと長い),早い反応が要求される音ゲのようなアプリケーションは,残念ながら作る事はできません.
このように,音の"合成"と"発音"は別々のタイミングで実行されるため,同期させるには何らかのギミックが必要になります.SiON では,"Synchronized Sequence", "Event Trigger" という2つの方法によって同期を行います.

Synchronized Sequence (DisplayObject に SiON を同期させる)

DisplayObject -> SiON の同期は,任意のタイミングで呼び出された発音コマンドについて,演奏中のビートと同期するように遅延して合成する事で実現します.この同期は,SiONDriver::noteOn(), SiONDriver::sequenceOn() の第4, 5引数の delay と quantize によって制御します.以下再掲.

  • 第4引数;delay:Number = 0; 音が出るまでの遅延を16分音符単位で指定します.第4引数と同様4分音符なら4,全音符なら16,32分音符なら0.5です.
  • 第5引数;quant:Number = 0; 音が出るタイミングをどの拍子にシンクロさせるかを16分音符単位で指定します.例えば4(4分音符)を指定すると4分拍子のタイミングに合わせて音を鳴らします.この時delay=2を指定すると4分拍子の8分音符後(つまり8beatの裏拍)になります.0を指定すると拍に関係なく最速で出音します.

これらはSiONDriver::play()で演奏しているシーケンスに同期します.また,null渡しでplay()を呼び出した場合は,SiONDriver::bpm プロパティで指定したテンポに同期します.

Event Trigger (SiON に DisplayObject を同期させる)

SiON では,発音のタイミングでイベントを発行する事で SiON -> DisplayObject の同期を行います.具体的には SiONDriver は ノート"オン"/"オフ"時に,音が"発音"/"合成"されるタイミングで,それぞれSiONTrackEventを発行する事ができます.つまり1つのノートに対して最大4種類のイベントが発行されます.MML演奏などに対して全イベントを生成させると大量にイベントが発行されパフォーマンスの低下を招きます.発行するイベントの種類はトラック毎に選択可能ですのでよく吟味して選択してください.

  • SiONTrackEvent.NOTE_ON_FRAME はノートが発音されるタイミングで発行されます.このイベントのリスナーで映像を操作すると発音と同期する事ができます.ハンドラに渡される SiONTrackEvent の note プロパティで発音する音階を参照できます.track プロパティで発音しているトラックを参照できますが,このイベントが発行される時点で音の合成はすでに終了しているため,このプロパティを使って操作する事はできません.また,値を参照する場合は遅延分未来の値が入っていると考えて下さい(厳密には違います).
  • SiONTrackEvent.NOTE_ON_STREAM はノートが合成されるタイミングで発行されます.このイベントは発音より早いタイミングで発行されるため,このイベントのハンドラ内で映像を操作すると音より早いタイミングで映像が変化してしまいます.しかし,このイベントは音の合成前に発行されるため,渡される SiONTrackEvent の track プロパティによるトラックの操作や,preventDefault()による発音のキャンセルが可能です.また SiONTrackEvent::frameTriggerDelay プロパティを参照すると実際に発音されるまでの遅延を[ms]単位で取得する事が出来ます.尚,このハンドラ内で track プロパティの keyOn() を呼び出すと再帰呼出でスタックオーバーフローエラーとなります.
  • SiONTrackEvent.NOTE_OFF_FRAME, NOTE_OFF_STREAM は,上記イベントのノートオフ版です.スラーやピッチベンドで音が繋がっている場合でも,次発音の直前にイベントが発行されます.

これら SiONTrackEvent を発行させるには,MML 内に"%t"又は"%e"コマンドを仕込んでおくか,SiDriver::noteOn()の第7-9引数を指定するか,SiMMLTrack::setEventTrigger() 関数を呼びます.尚,noteOn() は内部でノートオンイベントが発行されるため,返値で渡された SiMMLTrack の setEventTrigger() を呼び出してもノートオンイベントは発行されません(ノートオフイベントは発行される).第7-9引数で指定して下さい.一方,sequenceOn() はストリーミング時にイベントが発行されるため,返された SiMMLTrack の setEventTrigger() が有効です.
ここでは最も代表的な使い方である"%t"MMLコマンドについて説明します.他の使用方法でも基本的には同じで下記 3引数で設定します.

  • 第1引数;eventTriggerID; ハンドラに渡される SiONTrackEvent の eventTriggerID プロパティで参照する値を指定します.このIDを用いて,どこからイベントが発行されたかを特定する事ができます.値は任意です.
  • 第2引数;noteOnTrigger; ノートオン時のSiONTrackEventを発行するかをフラグで指定します.0なら発行しません.1なら発音のタイミングでSiONTrackEv ent.NOTE_ON_FRAMEを発行します.2なら合成開始のタイミングでSiONTrackEvent.NOTE_ON_STREAMを発行します.3なら両方発行します.
  • 第3引数;noteOffTrigger; ノートオフ時のSiONTrackEventを発行するかをフラグで指定します.0なら発行しません.1なら消音のタイミングでSiONTrackEvent.NOTE_OFF_FRAMEを発行します.2なら合成終了のタイミングでSiONTrackEvent.NOTE_OFF_STREAMを発行します.3なら両方発行します.

また"%e"コマンドは,ノートが無くても強制的にノートオンイベントを発行します.SiMMLTrack::dispatchNoteOnEvent() でも同じ挙動です.これらによってノートオンイベントは発行されますが実際に発音されません.これらのコマンド/メソッドでは,第三引数はありません.

SiONTrackEvent.BEAT/SiONDriver::setTimerInterruption()

BEATイベント (SiONTrackEvent.BEAT) と タイマ割り込み(SiONDriver::setTimerInterruption()) は,拍子に対する Event Trigger に相当します.これらによりシンプルな SiON -> DisplayObject の同期が可能です.
SiONTrackEvent.BEAT イベントは,SiONDriver::play()で演奏しているシーケンス(null渡しでplay()を呼び出した場合は,SiONDriver::bpm プロパティで指定したテンポ) の拍子が"発音されるタイミング"で発行されます.デフォルトでは 4Beat毎に発行され SiONDriver::setBeatCallbackInterval() によりイベント間隔の設定を行います.このイベントのハンドラ内で処理を行う事で拍子に合わせた映像操作が可能になります.尚,ハンドラに渡される SiONTrackEvent の eventTriggerID プロパティには,ストリーミング開始を0とした16ビートカウンタが渡され,note プロパティには 0 が渡されます.
一方,SiONDriver::setTimerInterruption() は,第二引数で渡した function を割り込み関数として,拍子が"合成されるタイミング"で呼び出します.この割り込み関数内で,SiMMLTrack::keyOn() や SiONDriver::noteOn(), sequenceOn() 等を呼び出す事で拍子に合わせた発音が可能となります.また,第一引数では割り込み間隔を16ビートカウント単位で指定します.この値は小数を指定する事もできるため,例えば bpm=120(125[msec/16beat]) で 0.1333333333(=0.125/60) を指定すると60[fps]のタイマ割込が可能です.ただし,割り込みの前に一旦全てのトラックのレンダリングを終えてから処理を返すため,高頻度の割り込みはパフォーマンスの低下を招きます.留意して下さい.
SiON Tenorion - wonderfl build flash online
BEATイベントとタイマ割り込みについては Tenorion デモが良い例になると思います.