Twitter @HigenekoTwitter #XNA
XNAはShader Model 1.1以上のビデオカード向けに設計されていて、固定シェーダーが使えません。これは、昨今のゲームのグラフィクスにはシェーダープログラムが欠かせなくなっていること、DirectX 10でも固定シェーダーが廃止されたことが主な理由です。また、仮に固定シェーダーをサポートした場合、設計思考が違うものを一緒にすることによるライブラリの複雑化、マルチテクスチャの細かい部分の振る舞いがGPUによって違い、しかもその情報量が少ないという問題もあります。
以上の理由から、XNAではシェーダプログラムが使える事が前提となっています。しかし、いきなりシェーダープログラムを書くというのでは敷居が高すぎるので、固定シェーダーの代表的な機能を簡単に使えるようにしたのがBasicEffectクラスです。
BasicEffectには以下の機能があります。
これらの機能をコントロールするために、BasicEffectにはLightingEnabled、TextureEnabled、VertexColorEnabled、そしてFogEnabledプロパティがあります。ここで重要なのは、これらのプロパティを変更した場合、BasicEffectは使用するシェーダーを切り替え、特に最初の3つプロパティによって頂点シェーダー内で参照する頂点データが変化するということです。以下は各プロパティと頂点データの対応表です。
頂点データに対応するデータがない場合、例えばLightingEnabledをtrueにしたのに指定した頂点データにNormalが含まれていない場合の動作は未定義となっています。特にDirectX SDKを入れてデバッグモードにしている環境では例外処理が発生するので、自分のところでは動いていても、友達のPCだと動かなかった、なんてことになってしまうので注意が必要です。
これらのプロパティは全て既定値がfalseとなっています。ですから、BasicEffect.Textureにテクスチャをセットしただけではテクスチャが表示されないので、テクスチャを使う時はBasicEffect.TextureEnabledをtrueにすることでテクスチャが表示されます。
XNAで初めて3Dプログラムを始める人にはもちろん、3Dプログラムに慣れている人でもライティング設定は面倒な作業です。この作業を軽減する為に、BasicEffect.EnableDefaultLightingというメソッドがあります。このメソッドを呼ぶことで以下のライティング設定が適用されます。
このキー、フィルそしてバックライトの3つの光源を使うのがライティングの基礎になり、実際に映画やドラマなどではこれらを基本としたライティングが使われています。暗いシーンで効いてくるのが、被写体の輪郭を背景から浮かび上がらせる効果のあるバックライトです。以下の2枚のスクリーンショットは実際にGSEで撮ったもので、左がキーライトのみ、右がキー、フィル、そしてバックライトの3つを使ったものです。
左側のスクリーンショットでは、機体の下の部分が背景に溶けこんでしまっているのに対して、右側ではシルエットがハッキリとしていて、機体の丸みが判り、より立体的に見えます。
ライティングはそれだけで本が書けるくらい奥が深いのですが、EnableDefaultLightingを使うことで簡単にライティング効果を得ることができるので、試しに使ってみるのはどうでしょうか?
SpriteBatch.Beginメソッドにはブレンドモード、ソートモード、そしてステートモードの3つ引数を渡すメソッド以外に、引数を省略できる2つのオーバーライドがあります。引数を省略した場合、ブレンドモードはSpriteBlendMode.AlphaBlend、ソートモードはSpriteSortMode.Deferred、そしてステートモードはSaveStateMode.Noneとなります。
ここで重要なのはステートモードがSaveStateMode.Noneということで、SpriteBatch.Begin()、またはSpriteBatch.Begin(SpriteBlendMode blendMode)を呼んだ場合は、レンダーステートが変更されてしまうということです。変更されるレンダーステートは指定するSpriteBlendModeによって変わりますが、共通して以下のレンダーステートを変更します。
GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace; GraphicsDevice.RenderState.DepthBufferEnable = false; GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Clamp; GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Clamp; GraphicsDevice.SamplerStates[0].MagFilter = TextureFilter.Linear; GraphicsDevice.SamplerStates[0].MinFilter = TextureFilter.Linear; GraphicsDevice.SamplerStates[0].MipFilter = TextureFilter.Linear; GraphicsDevice.SamplerStates[0].MipMapLevelOfDetailBias = 0.0f; GraphicsDevice.SamplerStates[0].MaxMipLevel = 0;
これ以外にも、GraphicsDeviceのVertices、Indices、VertexDeclaration、VertexShader、そしてPIxelShaderが変更されます。
そして、以上に加えて、SpriteStateMode.Noneでは以下のレンダーステートが変更されます。
GraphicsDevice.RenderState.AlphaBlendEnable = false; GraphicsDevice.RenderState.AlphaTestEnable = false;
SpriteStateMode.AlphaBlendの場合は以下のレンダーステートが変更されます。
GraphicsDevice.RenderState.AlphaBlendEnable = true; GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add; GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha; GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha; GraphicsDevice.RenderState.SeparateAlphaBlendEnabled= false; GraphicsDevice.RenderState.AlphaTestEnable = true; GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Greater; GraphicsDevice.RenderState.ReferenceAlpha = 0;
SpriteStateMode.Addtiveの場合は以下のレンダーステートが変更されます。SpriteStateMode.AlphaBlendの時と一緒ですが、RenderState.DestinationBlendに設定される値が違います。
GraphicsDevice.RenderState.AlphaBlendEnable = true; GraphicsDevice.RenderState.AlphaBlendOperation = BlendFunction.Add; GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha; GraphicsDevice.RenderState.DestinationBlend = Blend.One; GraphicsDevice.RenderState.SeparateAlphaBlendEnabled= false; GraphicsDevice.RenderState.AlphaTestEnable = true; GraphicsDevice.RenderState.AlphaFunction = CompareFunction.Greater; GraphicsDevice.RenderState.ReferenceAlpha = 0;
ここで注目すべきは、どの設定でもDepthBufferEnableがfalseに設定されることです。特に3Dモデルを描いた後にSpriteBatchを使ってステータス画面やスコアを表示したりすると、深度バッファが効かない状態になってしまい、次のフレーム以降の3Dモデルの描画がおかしくなってしまいます。
この問題を解決するには、SpritaBatch.Endメソッドを呼んだ後に、以下のレンダーステートを設定しなおすと良いでしょう。
GraphicsDevice.RenderState.DepthBufferEnable= true; GraphicsDevice.RenderState.AlphaBlendEnable = false; GraphicsDevice.RenderState.AlphaTestEnable = false;
また、必要に応じて以下のテクスチャアドレッシングのステートも設定しなおします。
GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap; GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
もちろん、SpriteBatch.Beginメソッドを呼ぶときにSaveStateMode.SaveStateを指定して元のレンダーステートに戻すこともできます。ただし、私個人しては以下の理由から、あまりお勧めできません。
SpriteBatchはSaveStateMode.SaveStateが指定されたときにStateBlockクラスを使ってレンダーステートを保存します。この時の処理に時間が掛かる、特にXbox 360側でのStateBlockのパフォーマンスはWindowsに比べて著しく低いです。とは言っても、Begin、Endの呼び出しが少ないうちは全体的パフォーマンスへの影響は無視できる範囲です。
パフォーマンスよりも問題なのは、レンダーステートの管理が面倒だからといって、至る所でStateBlockを使ってしまうことが習慣化してしまうことです。StateBlockがDirect Xでサポートされる以前に同じような仕組み作ったことがあるのですが、出来上がった時には便利なものができたと自負していました。グラフィクス・プログラマーの人達も便利だと言って使ってくれてたのですが、だんだんと彼らはレンダーステートがどうなっているかに無関心になっていき、とにかくレンダーステートを保存しておけば大丈夫というのが習慣化してしまいました。そして遂にはレンダーステート保存する部分のコードがゲーム全体のパフォーマンスに影響を与えるまでに乱用されるまでになってしまいました。
実際にゲームを作る上で、必要なレンダーステートは比較的簡単に分類化でき、その数も容易に管理できる範囲内で
と、いった感じです。これらの描画に必要なレンダーステートを、あらかじめ決めておいてメソッドとして呼び出すだけでレンダーステートを変更するようにしておくと、常にどんなレンダーステートを使用しているかが把握しやすくなります。
SpriteBatchクラスはWindows/Xbox360上で効率的に2D描画をするために設計されています。SpriteBatchで使える機能としては以下のものがあります。
スプライトの回転とスケーリングを使う場合に気をつける点はOriginパラメーターを指定しない場合は、回転、スケーリングはスプライトの左上を原点として扱います。
上図は、原点の違いによる回転結果の違いです。キャラクターなどをスプライトを使って描画する場合、特に多間接キャラクタなどを表現する場合はOriginを指定することが多いと思います。
また、テクスチャの一部分の矩形を指定できるので、一枚のテクスチャに複数のキャラクタのアニメーションを描いたり、ビットマップフォントなどで指定の文字の部分を描くといったことができます。
カラーモジュレーションはColorパラメーターで指定したカラーとテクスチャカラーを乗算した結果が描画されます。テクスチャのそのままの色を使う場合はColor.Whiteを使います。使用例としては夕焼けのシーンで赤い色をColorに指定したり、キャラクターが現れる時にカラーのアルファ値を時間と共に変更することで徐々に現れたり、消えたりといった演出にも使えます。
バッチ処理とソーティング
現在のGPUは、大量のポリゴンを描画するように設計されていますが、そのパフォーマンスを得るためにはテクスチャ変更といったレンダーステートの変更を極力少なくする必要があります。例えば木と岩の2枚のテクスチャがあった場合、木のテクスチャのスプライトを100枚続けて描いた後に岩のテクスチャのスプライトを100枚描くのと、岩と木のテクスチャを交互に切り替えて10枚(書き間違ではない)描いた方が遅いなんていうこともあります。
見下ろし型の2Dベースのゲームを作る場合、地面があり、その上に岩や木といったオブジェクトがあり、更にその上に半透明の雲があるようなシーンを作る場合、以下のようにSpriteBatchを使うことで実現できます。
地面部分を描く場合、その殆どがアルファブレンディングの必要がないので、SpriteSortMode.Textureを設定します。この時、SpriteBatchクラスはDrawで呼ばれたパラメーターを内部バッファに保持しておき、SpriteBatch.Endが呼ばれた時に使われているテクスチャの順に並び替えてから、まとめて描画します。テクスチャ変更の回数を少なくすることによって、GPUが効率的に描画することができます。
次に、木や岩といったオブジェクトを描く場合に重要なのは、オブジェクトの前後関係です。この場合、SpriteSortMode.BackToFront、またはSpriteSortMode.FrontToBackを使用します。スプライトはDrawメソッドのdepthLayer引数の値によって並び替えられた後に描画されます。depthLayer引数は0~1までの浮動小数点の値で、0が手前側(Front)、1が奥側(Back)になります。ですから、通常はSpriteSortMode.BackToFrontを指定します。
最後に雲の描画になるわけですが、これも単純にSpriteSortMode.BackFrontを使って、雲の高さをdepthLayerに指定することで実現できます。
// 背景を描く spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Texture, SaveStateMode.None); spriteBatch.Draw(...); spriteBatch.End(); // 木や岩といったオブジェクトを描く spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None); spriteBatch.Draw(...); spriteBatch.End(); // 雲を描く spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None); spriteBatch.Draw(...); spriteBatch.End();
まとめると以上のようなコードになります。このようなケースではSpriteSortModeを指定するだけで実現できることが多いですが、ゲーム側で描画の順番などを制御したい場合はSpriteSortMode.Immediateを使います。SpriteSortMode.Immediateを使った場合、SpriteBatchは並び替えの処理などをせずにDrawを呼んだ順にスプライトを描きます。この場合でも、同じテクスチャが続けて使われている場合はバッチ処理の対象となるので、呼び出し側で調整することでパフォーマンスを得ることができます。
また、SpriteSortMode.Immediateと他のソートモードとの違いとして、スプライトを描画するのに必要なレンダーステートは、他のソートモードではSpriteBatch.End内で設定されるのに対して、SpriteSortMode.Immediateの場合はSpriteBatch.Begin内で設定されます。ですから、SpriteBlendModeで用意されているブレンドモード以外のものを使いたい場合などにSpriteSortMode.Immediateを設定することができます。
SpriteBatchは描画するのに必要なレンダーステートを設定しますが、その事については次回で詳しく説明します。
XNA Game Studio Expressアップデートが4月からダウンロードできるようになります。この更新は主にフィードバックを元にした変更になっています。詳細は以下の通り。
以上の殆どがユーザーからのフィードバックを元にしています。これからもXNAを良くするために皆さんからのご意見ご希望などをXNAチームメンバ一同待っています。
12/27/2008 XNA GS 2.0の以降の振る舞いに合わせて「固定更新、高リフレッシュレート環境」部分を更新
Gameクラスには、Updateの呼ばれ方の振る舞いを変更するIsFixedTimeStepというプロパティがあります。この値がtrue(既定値)の場合、Game.TargetElapsedTime(規定値は16.6ms)で指定された間隔でUpdateメソッドが呼ばれます。これを仮に固定更新と呼びます。
この値がfalseの場合は、GameクラスはUpdate、Drawを交互に呼び続けるだけです。ただし、GraphicsDeviceManager.SynchronizeWithVerticalRetraceの既定値がtrueなので、GameクラスがDrawを呼んだ後に、IGraphicsDeviceManager.EndDrawメソッド内でデバイスのPresentを呼んだ時にV-Sync(垂直同期)待ちがが発生するので、通常は使っているモニタのリフレッシュレートになります。これを可変更新と呼びます。
上の図はモニタのリフレッシュレートが60Hz(一秒間に60回)環境で、Update、Drawの処理時間が16.6ms以下の場合の様子です。
この場合は、固定、可変のどちらの場合も同じ動作をし、Update、Drawの引数であるGameTime.ElapsedGameTimeの値は常に16.6msを示します。常にこの状態でゲームが動作するのが好ましいのですが、画面に沢山のキャラクターを表示したりして処理が16.6msに間に合わない、つまり処理落ちの状態では固定、可変更新での振る舞いは違います。
上の図は、IsFixedTimeStepがfalseの場合で、処理落ちした状態での様子です。この場合、フレーム1以降でUpdateとDrawに渡されるGameTime.ElapsedGameTimeの値は大体23~25msと処理状態によって変化します。また、処理落ちが発生した場合はGraphicsDevice.PresentはV-Syncと同期しません。
上の図は、IsFixedTimeStepがtrueの場合で、処理落ちした状態での様子です。この状態でもUpdateとDrawに渡されるGameTime.ElapsedGameTimeの値はGame.TargetElapsedTimeと同じものです。
ここで違うのは、フレーム3の時にUpdateが2回呼ばれるということです。固定更新の場合、Gmaeクラスは各フレームで経過時間によってUpdateを呼ぶ回数を変化させます。上の場合は、フレーム3の時にフレーム1で更新するべき時間の16.6msから、大体34ms以上の時間が経過しているので、Gameクラスは実時間とゲーム内でのシミュレーション時間を合わせるために、34ms/16.6ms=2フレーム分のUpdateを呼ぶわけです。
結果的にDrawを呼ぶ回数が減るので、次のフレームでは処理落ちが回避できる可能性が高くなりますが、Updateの処理時間がDrawの処理時間より掛かる場合はいつまで経っても実時間に追いつかないケースが発生します。この状態に陥るのを防ぐ為にGameクラスは500msを経過時間の上限としていいます。また、Updateが複数回呼ばれる状況になったときにはUpdate、Drawに渡されるGameTime.IsRunningSlowlyの値がtrueになるので、この値によってゲーム側で余計な処理を省いたりすることで、処理落ちを防ぐことができます。
PCのモニタ、特にCRTモニタの場合だとLCDモニタに比べて高いリフレッシュレートのモニタがあります。この場合、可変更新の場合は単にGame.TargetElapsedTimeがリフレッシュレートの間隔になるのに比べて、固定更新の場合は上の図のように、フレーム1、3、そして6のようにUpdateとDrawがフレームの途中で呼び出されるフレームが存在します。XNA GS 2.0以降ではUpdateが呼ばれない場合は、Drawも呼ばれず、単に次の更新時間になるまでループするようになっています。XNA GSE 1.0ではUpdateが呼ばれない場合でもDrawが呼ばれるので、通常はそこで垂直帰線期間待ちが発生するようになっていました。
これは、GameクラスはUpdateを呼ぶに足る経過時間が経った場合にのみUpdateを呼ぶという処理の仕方からくる現象です。この振る舞いを言葉で伝えると、大抵の人は変な振る舞いだと思ってしまいますが、上の図の50msの時点を見てみると60Hz、100Hzどちらの場合もUpdateが3回呼ばれているということが分かると思います。つまり表示される時間に多少の違いがあるものの、ゲームシミュレーションとしては常に一定(Deterministic)であるというわけです。
実はこれに近い仕組みは、皆さんの身の回りで良く目にすることができます。それはTVで映画を観る時です。秒間24フレームの映画を秒間30フレームのTVで見るために2-3プルダウンという手法が使われています。
以上のことをまとめると以下のようになります。
Xbox360などのコンシューマーゲーム機では、固定更新を使っているゲームが殆どです。PCのゲームでは以前は可変更新をするゲームが主流でしたが、最近は固定更新をするゲームが増えてきました。
固定更新をするゲームず増えてきた要因は、ゲーム内で物理シミュレーションが本格的に使われるようになったのと、ネットワーク対応のゲームが増えてきたことです。
物理シミュレーションでは物同士が衝突した瞬間を計算するわけですが、この衝突した瞬間というのが更新時間によって左右される場合が多く、更新時間の違いによってシミュレーション結果が変わってしまうという問題があります。
ネットワークゲームの場合は相手側に情報を伝える時間は人間にとっては一瞬でも、プログラムにとっては充分に長い時間なので、常に次の動きを予想しながら動作しています。このときに可変更新にしてしまうと、こちら側と向こう側のシミュレーション結果を一致させるのに多大な努力と、ネットワーク帯域の消費してしまうという問題があります。
これらの理由から、XNAの既定値は1/60秒固定更新になっています。多くのケースに対応するように設計されているわけですが、最大の利点としては、そんなことを一切気にせずともWindows/Xbox360上で同じように動作するゲームが作れてしまうということです。
前回は、XNA Game Studio Expressでゲームプロジェクトを新規作成したときに生成されるテンプレートコードの中身の説明をしました。今回は、ゲームプログラムの基本となる以下のものを実装、つまりコーディングをします。
今回は、簡単な説明に留めますが、次回からはそれぞれについて詳しく説明していきます。
コンテントの読み込み
XNAにはビットマップ、3Dモデル、オーディオといったゲームを作る上で必要なコンテントを簡単に、効率よく使うためのコンテント・パイプラインというものがあります。全てのコンテントは、プロジェクトをビルドした時に、コンテント・パイプラインによって処理され、Windows/Xbox360上で効率良く使えるデータ形式に変換されるようになっています。また、それぞれのゲームで独自形式のフォーマットを取り扱えるようにカスタマイズできる機能を持っています。
今回使うコンテントは、左の飛行機の絵が一枚だけです。XNAでは全ての画像データはテクスチャとして変換されます。コンテントのプロジェクトへの追加はソートコードの追加と同じで、ソリューション エクスプローラからコンテントを追加するプロジェクトを右クリックしてメニューを開き、その中の追加/既存の項目を選びます。 ダイアログボックスが開いたら、ファイルの種類をContent Pipeline Filesにしてから追加したいコンテントを選びます。
GSE 1.0では以下のファイルフォーマット形式がサポートされています。
プロジェクトディレクトリ直下に直接コンテントを追加してもいいのですが、コンテントとコードの区別がつくようにプロジェクトディレクトリの下にContentというディレクトリを作っておくと便利です。また、この場合、ContentManagerの初期化の時(Initializeメソッド内)にコンテントのルートディレクトリを指定しておくと、コンテントの読み込みの時に"Content/アセット名"と書かずに済みます。
// コンテントのルートディレクトリを"Content"に設定する content = new ContentManager(Services, "Content");
実際のコンテントの読み込みをする為には以下のようなコードをLoadGraphicsContentメソッド内に書きます。
// 飛行機用のテクスチャを読み込む airPlaneTexture = content.Load<Texture2D>("air-plane");
ContentManagerクラスにはLoad<T>という、ジェネリクスメソッドがあり、どのような型のコンテントでも同じようにして読み込むことができます。.Net 2.0ならではの機能であるジェネリクスを使うことによって、今までのようにLoadTexture2D、LoadMeshFromFileといった、長い関数名を覚える必要もなく簡単にコンテントを読み込むことができます。
スプライトの描画
XNAでは簡単にテクスチャイメージを描画する為のSpriteBatchというクラスがあります。なぜ単にSpriteではなく、SpriteBatchという名前になっているかというと、このクラスは複数のスプライトをまとめて処理(バッチ処理)する機能をもっているからです。詳細については別の投稿で説明しますが、簡単に言うとWindows/Xbox360の違いを気にせずに、高パフォーマンスを実現しながらも、簡単に使える便利なものといった感じです。
// SpriteBatchに描画開始を伝える spriteBatch.Begin(); // 飛行機のテクスチャを指定した座標に表示する spriteBatch.Draw(airPlaneTexture, airPlanePos, Color.White); // SpriteBatchに描画終了を伝える spriteBatch.End();
以上のコードをDrawメソッド内に書き加えます。SpriteBatch.BeginとSpriteBatch.Endの間にSpriteBatch.Drawメソッドを書くようにします。BeginとEndの間には何回でもDrawを呼ぶことができます。SpriteBatch.Beginを呼ぶ前にDrawを呼んだり、SpriteBatch.Beginを続けて呼んだりすると、実行時に例外が発生します。
スプライトは回転、スケールといった機能もサポートしていますが、ここでは単にテクスチャを指定した座標に描画しています。座標を表すのがVector2構造体なので、2D描画に浮動小数点を使って大丈夫なの?と思う人もいるかもしれませんが、最近のGPUの殆どがピクセル以下の描画をサポートしていて、画面をゆっくりとスクロールさせる時などの滑らかさに違いが現れます。
ですから、XNAでゲームを作った場合は、昔の日本製パソコンで出てたシューティングゲームのように「脅威の0.5ドットスクロール」と宣伝することが奨励されています(ウソです)。
コントローラーによる移動
ここまでで、画面には飛行機が表示されるようになっていますが、ここではその飛行機をコントローラーによって動かすコードを実装します。
// 飛行機の座標をコントローラー及び、キーボードの入力によって変更する Vector2 dir; if (ProcessKeyboardInput(out dir) == false) { // キー入力が無かったので、コントローラーの左スティックの状態を取得する dir = GamePad.GetState(PlayerIndex.One).ThumbSticks.Left; } dir.Y = -dir.Y; // スクリーン座標のY方向とキー入力の上下を合わせる const float speed = 400.0f; // ピクセル/秒の速度 airPlanePos += dir * (float)gameTime.ElapsedGameTime.TotalSeconds * speed;
上のコードをUpdateメソッド内に書き加えます。日本では北米程にXbox360が普及していない、つまりXbox360コントローラーを持っている人が少ないと思われるので、ProcessKeyboardInputというメソッドでキーボード入力をサポートしましたが、ここでは割愛します。
ゲームコントローラーの入力は、自由な時にGamePad.GeStateを使うことによって、その状態を知ることができます。コントローラーが繋がっていない場合でも、プログラム自体は問題なく動作するので上のように非常に簡単にコントローラーの入力を処理することができます。
ここではThumbSticks.Leftという、コントローラーの左スティックの状態を取得しています。ThumbSticks.LeftはVector2構造体で、スティックの左右はVector2.Xに格納され、左が-1、右の状態が1の値を示し、スティックの上下はVector2.Yに格納され、上が1、下が-1の値を示します。アナログスティックなので傾け方によって、-1から1までの値に変化します。
コントローラーから、移動したい方向を取得した後は、その方向への移動量を現在の座標に加えることによって移動します。ここでは単に一定量の速度で移動させるのではなく、GameTime.ElapsedGameTimeを使うことで、Game.IsFixedTimeStepがfalseの時にフレームレートが変化した場合でも、飛行機が同じ速度で移動するようになっています。Game.IsFixedTimeStepによるゲーム更新の仕方の違いは次の投稿で詳しく説明します。
ゲームの基本ができた
ここまででゲーム製作の基本である、コンテントの読み込み、メインループでの更新、描画、そしてコントローラーの入力といった処理のXNA上でのやり方を紹介してきました。これだけでも、簡単な2Dベースのゲームを作る下地はできたのではないでしょうか?
今回、実装したコードをアップしておきます。下に表示されているAttachmentのSample01.zipをクリックすることでダウンロードできます。Windows用とXbox360用のソリューションファイルが入っているので、どちらのプラットフォームでも動作します。
XNAチームブログでXNAに関する幾つかのアナウンスがありました。
Creators Clubのオンライン・コミュニティのサイトが開設!
http://creators.xna.com/では以下のコンテントが提供されています。
Dream-Build-Playのメインコンテスト開催
優勝者には1万ドルの賞金、AMD 64 FX-62搭載のPCにSoftimage、3ds Max、Mayaのいずれかの3Dツールといったものに加え、作った作品がXbox Liveアーケードで配信できる権利が授与されます。期限は北米時間の7月2日までとなっています。詳しくはhttp://www.dreambuildplay.comに書いてあります。
Creators Club年会費を払う価値
現在、Xbox360上でCreators Club会員になっている人は、Creators Club プレミアム会員と呼ばれるようになり、前述のCreators Clubサイトでの特典が得られる他に、5月以降からは更に以下の特典があります。
これでCreators Club年会費は、Xbox 360上で実行する為の料金以上の意味を持つことになるわけですね。
これまで概要的な話をしてきましたが、今回からは実際のプログラミングについての話をしていこうと思います。
今回は以下の動作を実装します。
DirectXやMDXで以上の作業をするのには、かなりの量のコードを書く必要があり、特にデバイスロストの管理は複雑で、ウィンドウのリサイズ時はもちろん、マルチモニタでのモニタ間のウィンドウ移動、フルスクリーンへの切り替え、スクリーンセーバーが立ち上がったとき、PCがスリープ状態に入ったときなどに対応するには、少なくとも1,000行以上のプログラムを書かないといけません。
DirectXでゲームを作りたい!と思った人が、いざプログラムを始めようとすると、いきなり最初からこんな難関を乗り越えないといけないのは大きな障害以外のなにものでもありません。確かにプロジェクトテンプレートを使えば以上のことはコーディングなしで実現できますが、やはり大量のコードを管理するというのは面倒なことだし、ゲーム製作自体には関係のないことです。
XNAでは、これらの問題に加えて、PC/Xbox360の両プラットフォームでのゲーム開発を容易にするために、アプリケーションモデルがあります。XNAのアプリケーションモデルでは、今回の実装を実現するのに100行程度のコードで、テンプレートも用意されているので実際にコーディングする必要はありません。
以下のコードはXNA Game Studio Express上から、ファイル/新しいプロジェクトを選んだ後にWindows GameかXbox 360 Gameを選ぶことで自動的に生成されるコードです。実際には英語のコメントですが、ここでは翻訳されています。PCとXbox360のコードは完全に同じものです。
/// <summary> /// ゲームのメインクラス /// </summary> public class SampleGame01 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; public SampleGame01() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); } /// <summary> /// ゲームの実行する前に初期化必要なものを処理する。 /// ここで必要なサービスの検索や、非グラフィクス系のコンテントの読み込みをする。 /// base.Initializeを呼ぶことで登録されたコンポーネントの初期化する。 /// </summary> protected override void Initialize() { // TODO: ここに初期化用コードを追加 base.Initialize(); } /// <summary> /// グラフィクスコンテントの読み込み。loadAllContentがtrueの場合、 /// ResourceManagementModeがAutomatic、Manualの両方のコンテントを読み込み、 /// falseの場合、ResourceManagementMode.Manualのコンテントを読み込む。 /// </summary> /// <param name="loadAllContent">読み込むコンテントの種類</param> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { // TODO: ResourceManagementMode.Automaticのコンテントを読み込む } // TODO: ResourceManagementMode.Manualのコンテントを読み込む } /// <summary> /// グラフィクスコンテントの廃棄。unloadAllContentがtrueの場合、 /// ResourceManagementModeがAutomatic、Manualの両方のコンテントを /// 破棄する必要があり、falseの場合、ResourceManagementMode.Manualの /// コンテントを破棄する。 /// デバイスリセット時にManualコンテントはGraphicsDeviceによって /// 自動的にDisposeされる。 /// </summary> /// <param name="unloadAllContent">破棄するコンテントの種類</param> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// ゲームの更新(衝突判定、入力処理、オーディオの再生など) /// </summary> /// <param name="gameTime">タイミング値のスナップショット</param> protected override void Update(GameTime gameTime) { // Xbox360とWindowsの既定ゲーム終了処理 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // TODO: ここにゲーム更新のロジックを追加する // 登録されたGameComponentを更新する base.Update(gameTime); } /// <summary> /// ゲームの描画。必要な時に呼び出される。 /// </summary> /// <param name="gameTime">タイミング値のスナップショット</param> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: ここに描画用のコードを追加する // 登録されたDrawableGameComponentを描画する base.Draw(gameTime); // TODO: ここにも描画用のコードを追加したりもする(ポストプロセスとか) } }
アプリケーションモデルの核となるのはGameクラスで、このクラスから派生したクラスを定義することで基本的なゲームの初期化やメインループといったものを実現できます。
以上のコードをまとめると、以下の3種類のメソッドが重要な役割を果たします。
これら3種類の関数内にコードを追加するのが、XNA上でのゲームプログラミングの第一歩になります。
Initializeメソッド
このメソッド内ではゲームコンフィグなどといった非グラフィクスのデータの読み込みや、ゲームの部品になるゲームコンポーネント(後の投稿で説明します)の登録などをします。
LoadGraphicsContentメソッド
このメソッドは、グラフィクスデバイスが作られたり、グラフィクスデバイスのリセットやロストの後にGameクラスから呼ばれます。ここでは、グラフィクス関連のコンテントなどを読み込み処理をするのが適しています。グラフィクスコンテントにはデバイスリセット後に自動的にその内容を元に戻してくれるResourceManagementMode.Automaticと、デバイスリセット後には内容が消えてしまうResourceManagementMode.Manialの2種類があります。殆どの場合はAutomaticのグラフィクスコンテントを使うだけで済みます。
UnloadGraphicsContentメソッド
このメソッドはデバイスリセットやデバイスロストの前にGameクラスから呼ばれます。XNAの全てのグラフィクスリソースはグラフィクスデバイスのリセットやロスト時に自動的にDisposeされるので、ゲーム内で不必要になった時以外に明示的に呼ぶ必要がありません。ですから、通常はなにもする必要がありませんが、例えばLoadGraphicsContentメソッド内で生成したグラフィクスリソースをリストに追加するといった処理をしている場合は、UnloadGraphicsContentメソッド内でリストから削除する処理をするのに適しています。
Updateメソッド
このメソッド内には非グラフィクスのゲームロジックを更新するコードを記述します。既定では、Gameクラスから16.6ms毎、つまり秒間60回呼ばれるようになっています。この更新の間隔はGame.TargetElapsedTimeを変更することで変えることができます。またGame.IsFixedTimeStepの値を既定値のtrueからfalseに変えることによって、GameクラスがTargetElapsedTimeの値を無視ししてUpdate、Drawメソッドを常に交互に呼ぶように振る舞いを変えることができます。ただし、この場合はUpdateメソッドに渡されるgameTimeの内容は変化します。
Drawメソッド
このメソッド内では各フレームでの画面を描画するための処理をします。このメソッドはGameクラスから呼び出されます。
以上がアプリケーションモデルの核であるGameクラスについての簡単な説明です。アプリケーションモデルには、他にGameComponentというのがありますが、これは画面に何かを表示させてからの方が理解しやすいと思うので、次回はテクスチャの読み込みとSpriteBatchでの描画をやりたいと思います。
前回は、XNAの概要について書きました。これからはXNA上でゲームを製作する時に注意する点や、知ってもらいたい事などを開発者という立場から、書いていこうと思います。
と、その前に今回はXNAでゲームを作る為に現時点で必要なものを記します。
ここまで読んで「え?プログラミングができないとだめなの?」と思った人もいると思います。どうしても会社としてアナウンスするときには「ゲームがすっげぇ簡単に作れる」といったキャッチフレーズばかりが先行して、実際にXNAってのはなんなの?というのが置き去りにされてしまいがちです。実際、北米でベータ板を公開した頃はXNAというのはアンリアル・エンジンのようなゲームエンジンだと思われたり、日本で言うところのRPGツクールのようなものだと思った人達が沢山いました。
そう思った方々には残念ですが、現時点ではXNAでゲームを作る際にはある程度のプログラミング知識が必要になります。もちろん、実際にXNAを使ってくれる人達の要望によってXNAはひにけに(日ごとに)変わっていくので、ご意見ご要望があればフォーラム等に気軽に投稿してくれるとうれしいです。