IRCで議論したことと、私の思ったこと、それをまとめます。
これを叩き台にしていろいろ議論できたらなーと思ってます。
まず動機。私がRoguelikeゲームが好きなので、Roguelikeゲームをいつかは作ってやるぞーと思っています。
そこで気になるのは、「何をもってRoguelikeとするか」という点。言い換えると、Roguelikeの面白さの本質、ということになります。
これを明らかにすれば、Roguelikeの面白さをスポイルすることなく、様々なターゲットにプレイしてもらえる、いわゆる「面白い」ゲームを作ることが可能なのではないか、と思ったのです。
現在PCのRoguelikeは、DGD1モデルにおけるH1,H2層、いわゆる熱心なゲーマー層という、特定の層にしか響かないのが現状です。この層のみをターゲットすると、このプレイヤーはゲームに対して要求するレベルがどんどん上がっていくので、ゲーム内容が高度化し、新規参入者を締め出しがちです。対戦格闘や、シューティングを思い浮かべればわかりやすいでしょうか。(ゲームデザインによって改善可能ですが、今回の論点ではないので省略させてください)
そうならないように、間口が広いゲームを作りたい。そう思って議論しました。
さて、まずRoguelikeゲームとは、という問いに関しては、
「ランダムな状況に対応する戦術ゲーム」
が最小になりそうです。そしてRoguelikeゲームを構成する必須要素を議論しました。結果は以下。
・Roguelikeゲームはランダムな状況を生成する。
Roguelikeゲームは一定のロジックに基づいて、地形、アイテム、敵、その種類を生成します。これを一言で表すと、ランダムな状況を生成する、といえます。
・Roguelikeゲームはシームレスな戦闘方式を提供する。
ゲームの構成要素である葛藤を生み出す「障害」、その実現手段として、戦闘があります。その戦闘がシームレスである、ということです。
では、何に対してシームレスなのか。それは上に挙げた、ランダムな状況に対してです。いくらランダムな状況を作り出しても、それがゲームを構成する大きなパーツである「戦闘」とリンクしてなければ、全く無意味です。
Roguelikeゲームではない戦闘方式例として、ランダムエンカウントのコマンド式バトル(つまりドラクエ式の戦闘)が挙げられました。
いくら地形やアイテム、敵がランダムであっても、それと断絶されたコマンド式バトルでは、そのランダムな状況を生かしようが無いのです。
追記:
飛躍があるという指摘がありましたので訂正します。コマンドバトルに関しては、ランダムな状況を十分に使用したコマンドバトルも構築は可能かもしれません。それを意識してないコマンドバトルは、Roguelike的には価値が無い、としておきます。
この二つの構成要素を組み合わせることで、その状況において取りえる戦術の幅が膨大になります。
その状況状況に応じた戦術の取捨選択そして実行、フィードバック。これがRoguelikeゲームの面白さの本質である。ととりあえずまとまりました。
以下、必須ではないがRoguelikeにおいて重要な要素として挙げられました。
・鑑定
アイテムは手に入れただけではその機能は分からず、鑑定アイテムによってその機能を知ることができる。という要素です。
これをゲームデザインに組み込むことで、例えば未鑑定の杖をふってみるといった、取りえる戦術を増やすことが可能です。
存在感のある重要な要素ですが、これが必須か、と問われるとそうでもない、という意見が大半でした。
後半鑑定し放題になって鑑定が無意味化するHengbandですが、それでもRoguelikeの面白さを保ってます。
鑑定をゲームデザインに組み込まないRoguelike、これは実現可能であろうという点で、必須要素とはいえないという流れになりました。
・リソースの配分という戦略
持っているリソースを、いつ投入するのか、という戦略要素です。
最初私はこれを挙げていたのですが、そもそもこれは長期的に見てリソースが有限であるというゲームデザインにおいてのみ機能する戦略要素です。
例えばNethackはダンジョンが有限なので、この要素は光り輝いてます。ですがHengbandはダンジョンが無限なので、この要素は薄いのです。
かといって「リソースの配分」としてしまうと、これは色々なゲームにも当てはまるので、Roguelike特有のものではなくなってしまいます。
以上の点から、重要ではあるが、必須ではない、という結論になりました。
・キャラクタの成長
これについてはあまり深く議論されなかったのでとりあえずここにおきました。キャラクタのパラメータの成長もありますが、装備的な強化、新たな能力の獲得も成長の1カテゴリとしてまとめてます。例えばHengbandはLv50で打ち止めですが、装備で成長することが可能です。
以下、多くのRoguelikeが備えていますが、重要では無いと思われる要素です。
・キャラクタの死
これは慎重に取り扱う必要があります。というのも、これを好むプレイヤー層はH1,H2だけだからです。他のプレイヤー層は、死という大きなペナルティを乗り越えるのに多大なエネルギーを消費するので、ゲームを投げ出しかねません。死は無いならないで、そのようにバランスを取れれば良いと思われます。
・ターン制
多くのRoguelikeゲームは慣習的にターン制を採用している、というだけのように見えます。
Roguelikeアクションや、Roguelikeシューティングといったゲームもあってしかるべきでしょう。(実際存在します)
・文字による画面
味があるという意見には私も賛成しますが、文字と同程度にアイテム、敵を表せるアイコンを用意できるならば、そのほうが良いと思われます。
情報量が少ない、というならば、情報量が少なくてもよいようなゲームデザインにするだけです。Nethackをそのまま3Dにしたらもちろん失敗します。
2009年12月29日
2009年12月10日
360で動作確認
レイトレーシングの当たり判定のアルゴリズムを変えて、内部反射も取り扱うようにしました。意図した挙動により近づいて満足。
ただ、PCで60fpsで動いても、360実機で動かなければ話になりません。というわけで箱を5つ配置して、360で動かしてみました。
結果は…30fps。360のGPUが強力とはいえ、さすがに比較的最近のビデオカードの性能と同列とはいきませんでした。
実際のゲームに組み込むとなると、レイトレだけしてるわけにもいきませんから、レイ数を削減してそれを拡大するといった、品質を犠牲にした高速化をする必要もありそうです。
ただ、PCで60fpsで動いても、360実機で動かなければ話になりません。というわけで箱を5つ配置して、360で動かしてみました。
結果は…30fps。360のGPUが強力とはいえ、さすがに比較的最近のビデオカードの性能と同列とはいきませんでした。
実際のゲームに組み込むとなると、レイトレだけしてるわけにもいきませんから、レイ数を削減してそれを拡大するといった、品質を犠牲にした高速化をする必要もありそうです。
2009年12月08日
リアルタイムレイトレーシング
2009年12月06日
カメラの方向を向く板ポリ
XNAのゲームコンテストがあるらしいので、それに向かってゲームを作ってます。
タイトルだけ決まってます。CrystalChaser。CrystalSpear系譜のゲームで、三次元鬼ごっこをコンセプトにゲームデザインしてます。
さて、CrystalSpearの障害物として登場した半透明板ポリなんですが、今回三次元上を自由に動き回れるようにしたいため、普通に板ポリを配置すると(板なので当然ですが)方向によって見えなくなってしまいます。
そこでカメラの方向をむかせる必要があるわけです。
カメラの方向を向く板ポリの生成アルゴリズムなのですが、太さを持つラインとして処理しました。
まずラインの端点とカメラを結ぶベクトル、そしてラインの方向との外積を取り、結果のベクトルをラインの太さ分引き伸ばします。
そうするとラインを中心とする、カメラの方向を向いた板ポリを構成する頂点の位置のひとつが出来上がります。逆方向に太さ分伸ばすともうひとつ得られます。
逆の端点に対しても同じ処理を行うと、計四つの頂点が得られます。これを結べば板ポリの完成です。
ただ、これを数やるとCPUが悲鳴を上げるので、VertexShaderに行ってもらう事にしました。VertexShaderは頂点の生成は出来ないので、計算に必要な情報を持った4つの頂点を渡す形になります。
こうして板ポリの問題は片付いたのですが、ゲームデザイン上の問題が山積みです。先は長そうです。
タイトルだけ決まってます。CrystalChaser。CrystalSpear系譜のゲームで、三次元鬼ごっこをコンセプトにゲームデザインしてます。
さて、CrystalSpearの障害物として登場した半透明板ポリなんですが、今回三次元上を自由に動き回れるようにしたいため、普通に板ポリを配置すると(板なので当然ですが)方向によって見えなくなってしまいます。
そこでカメラの方向をむかせる必要があるわけです。
カメラの方向を向く板ポリの生成アルゴリズムなのですが、太さを持つラインとして処理しました。
まずラインの端点とカメラを結ぶベクトル、そしてラインの方向との外積を取り、結果のベクトルをラインの太さ分引き伸ばします。
そうするとラインを中心とする、カメラの方向を向いた板ポリを構成する頂点の位置のひとつが出来上がります。逆方向に太さ分伸ばすともうひとつ得られます。
逆の端点に対しても同じ処理を行うと、計四つの頂点が得られます。これを結べば板ポリの完成です。
ただ、これを数やるとCPUが悲鳴を上げるので、VertexShaderに行ってもらう事にしました。VertexShaderは頂点の生成は出来ないので、計算に必要な情報を持った4つの頂点を渡す形になります。
こうして板ポリの問題は片付いたのですが、ゲームデザイン上の問題が山積みです。先は長そうです。
2009年11月24日
ツールの扱いについて
またまた設計話です。
ツールとは、グラフィックスソフトウェアに良くある手のひらツールやペンツールなどと同じ意味のツールです。これをプログラムを組む視点で見ると、ウィジェット(GUI部品)の振る舞いを動的に変更している、と見ることが出来ます。
例えば、手のひらツールが選択されている場合は、ワークスペースをドラッグするとワークスペースのカメラが移動しますが、ペンツールが選択されている場合はドラッグの動作はワークスペースへの線の描画です。
ワークスペースが生成するドラッグというイベントに対して、振る舞いが変わっていることがわかります。
これはMVCの枠組みで実装すると、Controllerの振る舞いを変更することで実現できます。
ただし、振る舞いが変わるウィジェットがひとつならば、StateパターンなりなんなりでControllerの振る舞いを動的に変更すればよいですが、複数のウィジェットの振る舞いが変わる場合、先ほどのようにControllerの状態としてツールを表現するとまずいことになります。ツールを追加するたび、各ウィジェットのControllerに状態を追加しなければならないからです。これは凝集性が低いプログラムの典型です。
オブジェクトを横断して振る舞いを変える方法というとアスペクトが想起されますが、アスペクト指向プログラミング言語は一般的ではないので、オブジェクト指向の範囲内で出来る方法を考えます。
問題は、ツールの機能が各ウィジェットのControllerの状態として分配されている点です。なのでこれを一箇所に集めればよさそうです。これをそのままの名前、Toolとします。Controllerはそのツールの責務をToolに移譲します。移譲する際、メソッドのパラメータとしてControllerとView、Modelを渡します。
こうすることで、Toolを切り替えれば各ウィジェットの動作が切り替わります。ツールに依存しない振る舞いは、そのままControllerに残します。
どうも汚いと感じた原因はここにあったようで。ただし、GUIの自動化テストは基本的に難しいので、テスタビリティは低いです。テスタビリティを上げるためには、PresentationModelにこのToolの考え方を導入してみるとよいようです。
この場合、Toolに渡すパラメータはPresentationModelだけです。
ToolはControllerを兼ねたものになるので、Controllerを渡す必要は無く、Viewを変更するのに必要なデータはすべてPresentationModelが持っているので、Viewも渡す必要がないからです。
私はPresentationModelにToolを導入して実装してみます。何か分かったらまた書きます。
ツールとは、グラフィックスソフトウェアに良くある手のひらツールやペンツールなどと同じ意味のツールです。これをプログラムを組む視点で見ると、ウィジェット(GUI部品)の振る舞いを動的に変更している、と見ることが出来ます。
例えば、手のひらツールが選択されている場合は、ワークスペースをドラッグするとワークスペースのカメラが移動しますが、ペンツールが選択されている場合はドラッグの動作はワークスペースへの線の描画です。
ワークスペースが生成するドラッグというイベントに対して、振る舞いが変わっていることがわかります。
これはMVCの枠組みで実装すると、Controllerの振る舞いを変更することで実現できます。
ただし、振る舞いが変わるウィジェットがひとつならば、StateパターンなりなんなりでControllerの振る舞いを動的に変更すればよいですが、複数のウィジェットの振る舞いが変わる場合、先ほどのようにControllerの状態としてツールを表現するとまずいことになります。ツールを追加するたび、各ウィジェットのControllerに状態を追加しなければならないからです。これは凝集性が低いプログラムの典型です。
オブジェクトを横断して振る舞いを変える方法というとアスペクトが想起されますが、アスペクト指向プログラミング言語は一般的ではないので、オブジェクト指向の範囲内で出来る方法を考えます。
問題は、ツールの機能が各ウィジェットのControllerの状態として分配されている点です。なのでこれを一箇所に集めればよさそうです。これをそのままの名前、Toolとします。Controllerはそのツールの責務をToolに移譲します。移譲する際、メソッドのパラメータとしてControllerとView、Modelを渡します。
こうすることで、Toolを切り替えれば各ウィジェットの動作が切り替わります。ツールに依存しない振る舞いは、そのままControllerに残します。
どうも汚いと感じた原因はここにあったようで。ただし、GUIの自動化テストは基本的に難しいので、テスタビリティは低いです。テスタビリティを上げるためには、PresentationModelにこのToolの考え方を導入してみるとよいようです。
この場合、Toolに渡すパラメータはPresentationModelだけです。
ToolはControllerを兼ねたものになるので、Controllerを渡す必要は無く、Viewを変更するのに必要なデータはすべてPresentationModelが持っているので、Viewも渡す必要がないからです。
私はPresentationModelにToolを導入して実装してみます。何か分かったらまた書きます。
2009年11月23日
Presentation Model
ゲーム作成のためのツールの作成を行っているのですが、これは普通のGUIアプリケーションです。
最初古典的なMVCパターンで組んでいたのですが、次第に立ち行かなくなってきました。作ることは出来るんですが、すごく汚い。原因はViewが仕事しすぎな点にありました。
そこでMVCパターンのバリエーションのひとつである、Presentaiton Modelを導入してみることにします。
アーキテクチャの変更になるので、ほぼ書き直しです。(といっても2000行も書いてないのでたいした痛手ではないですが)さてどうなることやら。
最初古典的なMVCパターンで組んでいたのですが、次第に立ち行かなくなってきました。作ることは出来るんですが、すごく汚い。原因はViewが仕事しすぎな点にありました。
そこでMVCパターンのバリエーションのひとつである、Presentaiton Modelを導入してみることにします。
アーキテクチャの変更になるので、ほぼ書き直しです。(といっても2000行も書いてないのでたいした痛手ではないですが)さてどうなることやら。
2009年11月15日
大分間が空きました
ゲーム制作?進んでません…OTL
UOで絵を描くと約束された方々、今しばらくお待ちください。
UOで絵を描くと約束された方々、今しばらくお待ちください。
2009年09月09日
2009年09月04日
XNAでのGC対策
・XBOXのGC
GCにはGC回収時の動作で分けると、STW型GCとOTF型GCがあります。
STW型GCのSTWとは、Stop The Worldの略です。これはプログラムの動作を止めて行うGCです。
そしてOTF型GCはOn The Flyの略で、プログラムの動作を止めずに行うGCです。
止まらないなら後者のほうがいいじゃないかと思われますが、OTF型GCは一般的にスループットは悪いです。詳しく書くと長くなりすぎるので割愛しますが、かいつまんで書くと、GCの動作を細切れにして行うため、トータルの動作時間が長くなるのです。
そしてXBOXのGCはどうなのかというと、古典的なマーク&スイープGCだそうです。これはSTW型GCです。マルチスレッドに強いCPUなんだからMostly Concurrent GCにしてもよかったんじゃないかと思うんですが(これはマルチスレッド時に高いパフォーマンスを発揮するOTF型GC)無いものねだっても仕方ありません。ともかくSTW型GCなわけです。
・GC回避の手段、オブジェクトプーリングについて
STW型GCとリアルタイム系ゲームとの相性は悪いです。なんせゲームプレイ中に止まるわけですから。アクションゲームでフレーム落ちが頻繁に発生してしまうとがっかりです。
そこでGCを回避する必要があるわけですが、言語によっては唯一の、とある方法があります。(C#だともうひとつ手段があります。後述)それがオブジェクトプーリングです。
詳細を記述する前にひとつ断っておきます。これはGCを回避するためにしかたなく行うテクニックで、これを行うとGCのパフォーマンスが悪化します。あくまでもリアルタイム性が他の何よりも優先される場合のみに使うべきテクニックです。
と大げさな断りを入れましたが、実装は簡単です。
オブジェクトプールと呼ばれるオブジェクトを溜めておく池を用意して、プールにオブジェクトがあるならばそれを使い、無い場合は新規に作成します。オブジェクトの寿命がきたら、プールに送ります。
これだけです。
ただしこれも弱点があります。使用するクラスごとにプールを用意することになるので(一般的な言語では、オブジェクトが所属するクラスを変えることはできません)、例えばシューティングゲームで、弾丸Aを発射してたが、武器変更で弾丸Bを発射することになった場合、弾丸Aのオブジェクトプールは無駄になってしまいます。
使用するクラスが多く、ゲーム中で使用するクラスが動的に変更される場合、この弱点は顕著になります。
・C#だからできるGC回避方法
この問題をC#だと回避することができます。キーワードは構造体、そして共用体です。
C#ではFieldOffsetを同じ位置にすることで、C/C++における共用体を実現することが出来ます。これを利用します。
動的に生成されるすべての構造体を、共用体としてひとくくりにしてまとめるのです。
enum TypeID: int
{
TaskA = 0,
TaskB,
/* ... */
}
[StructLayout(LayoutKind.Explicit)]
public struct Task
{
[FieldOffset(0)]
public TypeID typeID;
[FieldOffset(sizeof(int))]
public TaskA taskA;
[FieldOffset(sizeof(int))]
public TaskB taskB;
/* ... */
}
こんな感じです。typeidは型番号です。この型番号を元に、呼び出す関数を決めます。
public CallTask(ref Task task)
{
switch(task.typeID)
{
case TypeID.TaskA:
TaskA.CallTask(ref task);
break;
case TypeID.TaskB;
TaskB.CallTask(ref task);
break;
/* ... */
}
}
実際にはタスクの死亡フラグや(他から参照されるならば)参照カウント等をTaskに追加する必要があると思いますが、雰囲気はつかめると思います。
注意点としては、参照型を含む構造体をFieldOffsetを使って同じ位置に配置すると、検証不能なコードになるという点です。マーク&スイープGCの動作原理を考えるに、クラッシュする可能性が高いです。
各タスク(前のコードでいうとTaskA、TaskB)の構造体中では、参照型を使わないようにしましょう。コンパイルエラーにならないので注意が必要です。
ひどく面倒な方法ですが、GC回避効果は高いです。
とはいえほとんどのゲームではオブジェクトプーリングで十分かと思われるので、こんな方法もある、程度にどうぞ。
GCにはGC回収時の動作で分けると、STW型GCとOTF型GCがあります。
STW型GCのSTWとは、Stop The Worldの略です。これはプログラムの動作を止めて行うGCです。
そしてOTF型GCはOn The Flyの略で、プログラムの動作を止めずに行うGCです。
止まらないなら後者のほうがいいじゃないかと思われますが、OTF型GCは一般的にスループットは悪いです。詳しく書くと長くなりすぎるので割愛しますが、かいつまんで書くと、GCの動作を細切れにして行うため、トータルの動作時間が長くなるのです。
そしてXBOXのGCはどうなのかというと、古典的なマーク&スイープGCだそうです。これはSTW型GCです。マルチスレッドに強いCPUなんだからMostly Concurrent GCにしてもよかったんじゃないかと思うんですが(これはマルチスレッド時に高いパフォーマンスを発揮するOTF型GC)無いものねだっても仕方ありません。ともかくSTW型GCなわけです。
・GC回避の手段、オブジェクトプーリングについて
STW型GCとリアルタイム系ゲームとの相性は悪いです。なんせゲームプレイ中に止まるわけですから。アクションゲームでフレーム落ちが頻繁に発生してしまうとがっかりです。
そこでGCを回避する必要があるわけですが、言語によっては唯一の、とある方法があります。(C#だともうひとつ手段があります。後述)それがオブジェクトプーリングです。
詳細を記述する前にひとつ断っておきます。これはGCを回避するためにしかたなく行うテクニックで、これを行うとGCのパフォーマンスが悪化します。あくまでもリアルタイム性が他の何よりも優先される場合のみに使うべきテクニックです。
と大げさな断りを入れましたが、実装は簡単です。
オブジェクトプールと呼ばれるオブジェクトを溜めておく池を用意して、プールにオブジェクトがあるならばそれを使い、無い場合は新規に作成します。オブジェクトの寿命がきたら、プールに送ります。
これだけです。
ただしこれも弱点があります。使用するクラスごとにプールを用意することになるので(一般的な言語では、オブジェクトが所属するクラスを変えることはできません)、例えばシューティングゲームで、弾丸Aを発射してたが、武器変更で弾丸Bを発射することになった場合、弾丸Aのオブジェクトプールは無駄になってしまいます。
使用するクラスが多く、ゲーム中で使用するクラスが動的に変更される場合、この弱点は顕著になります。
・C#だからできるGC回避方法
この問題をC#だと回避することができます。キーワードは構造体、そして共用体です。
C#ではFieldOffsetを同じ位置にすることで、C/C++における共用体を実現することが出来ます。これを利用します。
動的に生成されるすべての構造体を、共用体としてひとくくりにしてまとめるのです。
enum TypeID: int
{
TaskA = 0,
TaskB,
/* ... */
}
[StructLayout(LayoutKind.Explicit)]
public struct Task
{
[FieldOffset(0)]
public TypeID typeID;
[FieldOffset(sizeof(int))]
public TaskA taskA;
[FieldOffset(sizeof(int))]
public TaskB taskB;
/* ... */
}
こんな感じです。typeidは型番号です。この型番号を元に、呼び出す関数を決めます。
public CallTask(ref Task task)
{
switch(task.typeID)
{
case TypeID.TaskA:
TaskA.CallTask(ref task);
break;
case TypeID.TaskB;
TaskB.CallTask(ref task);
break;
/* ... */
}
}
実際にはタスクの死亡フラグや(他から参照されるならば)参照カウント等をTaskに追加する必要があると思いますが、雰囲気はつかめると思います。
注意点としては、参照型を含む構造体をFieldOffsetを使って同じ位置に配置すると、検証不能なコードになるという点です。マーク&スイープGCの動作原理を考えるに、クラッシュする可能性が高いです。
各タスク(前のコードでいうとTaskA、TaskB)の構造体中では、参照型を使わないようにしましょう。コンパイルエラーにならないので注意が必要です。
ひどく面倒な方法ですが、GC回避効果は高いです。
とはいえほとんどのゲームではオブジェクトプーリングで十分かと思われるので、こんな方法もある、程度にどうぞ。