チュートリアル 4:60 分でゲームを作成する
チュートリアル 4:60 分でゲームを作成する
このチュートリアルでは、ゲーム作成のプロセスについて学びながら、ほぼ完全なゲームを記述できるようにします。このチュートリアルのコードは、このチュートリアルの本文で説明する手法を実際に示したものです。このチュートリアルの完全なサンプル コードをダウンロードできます。ダウンロード内容には、完全なソース コードと、このサンプルに必要な追加のサポート ファイルが含まれます。
初心者のゲーム プログラマーにとっては、ゲーム コーディングの基本を理解することが最も難しいステップになります。完成したゲームを示すサンプル、ヒント、特定のテクニックの使用方法を示すチュートリアルなどを見つけることは簡単ですが、そこにはゲーム作成のプロセスについての情報はほとんど示されていません。このチュートリアルの目的は、ゲーム作成のプロセスについて学びながら、ほぼ完全なゲームを記述できるようにすることです。さらに、このチュートリアルでは完全なサンプル ファイル (GoingBeyond4_Tutorial_Sample.zip) に収録されているアセットだけを使用するため、追加のコンテンツをインストールする必要がありません。ここでサンプル ファイルをダウンロードし、その内容をローカル ドライブのディレクトリに抽出してください。
これから作成するゲームは、よく知られている Atari® の Asteroids® ゲームの簡易版です。ビデオ ゲームの歴史における Asteroids の地位はよく知られています。ウィキペディア (Wikipedia) でこのゲームの興味深い歴史を読むことをお勧めします。このチュートリアルでは、Asteroids ゲームの動作について一般的な知識があることを前提としています。
このチュートリアルの初期作業の多くは、既に実行済みです。つまり、このチュートリアルは、「チュートリアル 3:XNA Game Studio によるサウンドの作成」チュートリアルの最後の仕上げとして収録されています。「新たなステップ : 3D の XNA Game Studio」の最初の 3 つのチュートリアルを完了した時点で、移動可能な宇宙船がサウンド付きで 3D 空間にレンダリングされていることになります。さらに 60 ~ 90 分のコーディング時間で、ほぼ完全な Asteroids 形式のゲームができあがります。
「新たなステップ : 3D の XNA Game Studio」の最初の 3 つのチュートリアルで、3D でレンダリングされる単一の操作可能オブジェクトの基本について説明しました。しかし、実際のゲームでは、2 つ以上のオブジェクトが必要です。このチュートリアルで実際のゲームを作成するための最初のステップは、ゲームで複数のオブジェクトの追跡とレンダリングを行う準備をすることです。
画面上の宇宙船について考えてみましょう。宇宙船は Model
クラスを使用して描画され、Vector3
で位置が追跡され、もう 1 つの Vector3
で速度が追跡されます。さらに、float
で回転角度が追跡されます。これらの各データ タイプは、コード パスに沿ってさまざまな場所で変更または確認されます。ユーザーに表示される最終結果は良好に見えますが、同類のデータを必要とする別のオブジェクトを含めるようにゲームプレイを拡張しようとする場合には難点があります。
たとえば、画面上に描画する 2 番目の宇宙船を追加し、移動や回転ができるようにする場合は、最初の宇宙船で使用していた各変数のコピーを作成する必要があります。各変数の確認および変更を行うために記述したコードも複製する必要があります。コピーした各行は、新しい変数に対して動作するという点を除いては、コピー元の行とほとんど同じです。
描画して動き回らせるオブジェクトの数が最終的には 1 ダースを超えるようなゲームの場合、この作業は実行不可能です。複製したコードは、わかりにくく、修正しづらいものになります。しかし、良い方法があります。3D オブジェクトの描画と移動を行うための共通変数を保持するコード オブジェクトを作成し、それらのオブジェクトのリストを維持すれば、同じコードを使用してすべてのオブジェクトを一緒に描画および移動することができます。このプロセスをカプセル化と呼びます。これはオブジェクト指向プログラミングの初歩ですが、作成するゲームが大きくなるほど、この手法は重要になります。
まず、ソリューション エクスプローラーでプロジェクトを右クリックし、[追加]、[クラス] の順に選択します。[名前] ボックスに「Ship.cs
」と入力し、[追加] をクリックします。
新しいファイルを追加すると、そのファイルがコード ウィンドウに表示されます。この新しいファイルはクラス、つまりコード オブジェクトを表します。この特別なクラスは Ship
という名前になります。この時点ではほとんど何も記述されていませんが、これを、次に示されているように変更します。
using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace GoingBeyond4 { class Ship { public Model Model; public Matrix[] Transforms; //Position of the model in world space public Vector3 Position = Vector3.Zero; //Velocity of the model, applied each frame to the model's position public Vector3 Velocity = Vector3.Zero; public Matrix RotationMatrix = Matrix.Identity; private float rotation; public float Rotation { get { return rotation; } set { float newVal = value; while (newVal >= MathHelper.TwoPi) { newVal -= MathHelper.TwoPi; } while (newVal < 0) { newVal += MathHelper.TwoPi; } if (rotation != newVal) { rotation = newVal; RotationMatrix = Matrix.CreateRotationY(rotation); } } } public void Update(GamePadState controllerState) { // Rotate the model using the left thumbstick, and scale it down. Rotation -= controllerState.ThumbSticks.Left.X * 0.10f; // Finally, add this vector to our velocity. Velocity += RotationMatrix.Forward * 1.0f * controllerState.Triggers.Right; } } }
これで、Ship
クラスは多くの操作を行うようになりました。このクラスでは、宇宙船の位置、速度、回転、および 3D モデルを保持し、専用の Update
メソッドで宇宙船を動かします。
Ship
クラスを作成したら、次に、この新しくカプセル化したデータを利用するように Game1.cs コード ファイルのコードを変更する必要があります。ソリューション エクスプローラーで [Game1.cs] をダブルクリックします。
宇宙船のモデルの描画から始めます。元の描画コードは Draw
メソッド内にありましたが、それでは複数のオブジェクトへの拡張はうまくいきません。Model
オブジェクトを画面に描画するので、選択された Model
を描画するメソッドを作成します。Draw
メソッドの下に、次のように、DrawModel
という新しいメソッドを追加します。
public static void DrawModel(Model model, Matrix modelTransform, Matrix[] absoluteBoneTransforms) { //Draw the model, a model can have multiple meshes, so loop foreach (ModelMesh mesh in model.Meshes) { //This is where the mesh orientation is set foreach (BasicEffect effect in mesh.Effects) { effect.World = absoluteBoneTransforms[mesh.ParentBone.Index] * modelTransform; } //Draw the mesh, will use the effects set above. mesh.Draw(); } }
この DrawModel
メソッドでは、モデル描画アルゴリズムを取得し、渡された Model
オブジェクトにそのアルゴリズムを適用して、Model
を画面に描画します。次に、Draw
呼び出しを変更して、この新しいメソッドを呼び出すようにします。
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); Matrix shipTransformMatrix = ship.RotationMatrix * Matrix.CreateTranslation(ship.Position); DrawModel(ship.Model, shipTransformMatrix, ship.Transforms); base.Draw(gameTime); }
前のチュートリアルからのコードには、Draw
呼び出しの前に modelPosition
値および modelRotation
値の宣言が含まれていました。これらの宣言は、今後は不要になるので削除します。cameraPosition
変数も削除します。これは後で作成し直します。
次に、Update
メソッドと UpdateInput
メソッドを変更して、次のように新しい Ship
クラスの値を使用するようにします。
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // Get some input. UpdateInput(); // Add velocity to the current position. ship.Position += ship.Velocity; // Bleed off velocity over time. ship.Velocity *= 0.95f; base.Update(gameTime); } protected void UpdateInput() { // Get the game pad state. GamePadState currentState = GamePad.GetState(PlayerIndex.One); if (currentState.IsConnected) { ship.Update(currentState); //Play engine sound only when the engine is on. if (currentState.Triggers.Right > 0) { if (soundEngineInstance.State == SoundState.Stopped) { soundEngineInstance.Volume = 0.75f; soundEngineInstance.IsLooped = true; soundEngineInstance.Play(); } else soundEngineInstance.Resume(); } else if (currentState.Triggers.Right == 0) { if (soundEngineInstance.State == SoundState.Playing) soundEngineInstance.Pause(); } // In case you get lost, press A to warp back to the center. if (currentState.Buttons.A == ButtonState.Pressed) { ship.Position = Vector3.Zero; ship.Velocity = Vector3.Zero; ship.Rotation = 0.0f; soundHyperspaceActivation.Play(); } } }
UpdateInput
メソッドの上の、Update
の上にある modelVelocity
変数を削除します。これはもう必要ありません。
最後に、初期化およびコンテンツ読み込みの処理方法に変更を加える必要があります。Game
クラスの最上部から Update
の呼び出しのすぐ上までのコードを次のように変更します。
GraphicsDeviceManager graphics; //Camera/View information Vector3 cameraPosition = new Vector3(0.0f, 0.0f, -5000.0f); Matrix projectionMatrix; Matrix viewMatrix; //Audio Components SoundEffect soundEngine; SoundEffectInstance soundEngineInstance; SoundEffect soundHyperspaceActivation; //Visual components Ship ship = new Ship(); public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } ////// Allows the game to perform any initialization it needs to before /// starting to run. This is where it can query for any required /// services and load any non-graphic related content. /// Calling base.Initialize will enumerate through any components /// and initialize them as well. /// protected override void Initialize() { projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians(45.0f), GraphicsDevice.DisplayMode.AspectRatio, 1.0f, 10000.0f); viewMatrix = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up); base.Initialize(); } private Matrix[] SetupEffectDefaults(Model myModel) { Matrix[] absoluteTransforms = new Matrix[myModel.Bones.Count]; myModel.CopyAbsoluteBoneTransformsTo(absoluteTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.Projection = projectionMatrix; effect.View = viewMatrix; } } return absoluteTransforms; } protected override void LoadContent() { ship.Model = Content.Load ("Models/p1_wedge"); ship.Transforms = SetupEffectDefaults(ship.Model); soundEngine = Content.Load ("Audio/Waves/engine_2"); soundEngineInstance = soundEngine.CreateInstance(); soundHyperspaceActivation = Content.Load ("Audio/Waves/hyperspace_activate"); } /// /// UnloadContent will be called once per game and is the place to unload /// all content. /// protected override void UnloadContent() { }
たいへんな作業のように思えるかもしれませんが、変更後のコードは、カプセル化の良い例として、ゲームの開発時に役に立ちます。
宇宙船のオブジェクトが準備できたので、次に、上から見下ろす視点で宇宙船が画面上を飛び回るようにします。これは、カメラのアングルと距離を変更するだけで実現できます。最後に、ユーザー入力に対する回転の方法を、必要な動作に合うように調整します。
z 軸に沿ってカメラの位置を逆転させるには、値を負の 5000 から正の 25000 に変更するだけです。cameraPosition
メンバーは、Game1 クラスの先頭近くに宣言されています。cameraPosition 宣言は次のようになります。
Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 25000.0f);
残念ながら、この変更のみでチュートリアルを実行しても、宇宙船は表示されません。これは、カメラの "射影行列" が正しくないためです。この問題を説明する正式な用語は "境界錐台カリング" ("視錐台カリング" とも呼ばれる) です。BoundingFrustum クラスについては、XNA Game Studio のドキュメントを参照してください。ここには、錐台に関する詳細およびカメラとの関連を理解できるキー ダイアグラムが記載されています。カメラの最も近い面および最も遠い面は、通常はパフォーマンスに関する問題に対処するために特有の方法で設定されます。ここでは、元のカメラの最も近い面は 1 で、最も遠い面は 10,000 にあります。図 1 のように、カメラが 5,000 単位の位置に設定されていれば、宇宙船はカメラのビュー空間内にあることになります。
Figure 1. 元のカメラ設定とビュー空間
カメラから 5,000 単位離れた位置に宇宙船がある場合に、最上の精度になります。しかし、カメラを最初の点から 25,000 に移動した場合は、図 2 のように、カメラのビュー空間は適切ではなくなります (遠く離れすぎていて宇宙船が見えません)。
Figure 2. 新しいカメラ位置による不適切なビュー空間
ここで、ビュー空間の問題を修正します。Game1
クラスの Initialize
メソッド内に、projectionMatrix
を作成するメソッドがあります。
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians(45.0f), GraphicsDevice.DisplayMode.AspectRatio, 1.0f, 10000.0f);
宇宙船がビュー空間に再び表示されるように、錐台の近くのクリップ面と遠くのクリップ面を変更する必要があります。簡単な計算によって、近くのクリップ面と遠くのクリップ面を決定します。カメラは宇宙船から 25,000 単位離れているので、次のように、近くの面がカメラに 5,000 単位 "近づく" ように、そして、遠くの面は 5,000 単位 "遠のく" ように設定します。
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians(45.0f), GraphicsDevice.DisplayMode.AspectRatio, 20000.0f, 30000.0f);
これで、図 3 のように、宇宙船が画面内に表示されるようにビュー空間が修正されます。
Figure 3. 修正されたビュー空間
ここでプログラムを実行すると、船首ではなく船尾に面するビューが表示されます。宇宙船を自分の方に向かって、または自分から遠ざかるように飛ばすと、宇宙船は数秒後に錐台の外部に移動して、見えなくなります。次に、宇宙船の向きと、入力に対する応答を修正します。
Ship
クラスで、上から見下ろす視点から始まるように、宇宙船の既定の向きを変更します。ソリューション エクスプローラーで [Ship.cs] をダブルクリックします。
最初に宇宙船が自分から見て反対方向に向いている場合は、x 軸に沿って 90 度回転させると、上から見下ろすビューになります。z 軸に沿って宇宙船を見ていることを忘れないでください。その視点からは、x の変更は "左右"、y の変更は "上下" の移動になります。したがって、x 軸上で宇宙船を回転させると、飛行中に旋回しているように、宇宙船はぐるりと 180 度反転します。XNA Framework では、角度の測定値はラジアンで示されるので、宇宙船を Pi/2 ラジアン回転させることになります。
RotationMatrix
の既存の宣言を次のコードに置き換えます。
public Matrix RotationMatrix = Matrix.CreateRotationX(MathHelper.PiOver2);
今度は、宇宙船の回転 (宇宙船の Rotation
プロパティの "set"メソッド) を変更するときは常に、この既定の回転に加えて、プレイヤーのコントローラーから提供される z 軸に沿った回転量が含まれるように、回転行列を変更します。次の条件を満たしていれば、他のどの軸に沿っても同じように回転させることができます。
- カメラを正しく配置する。
- 正しい軸を基準として移動および回転の計算を行う。
移動および回転の動きの適切な計算に失敗すると、予想外の結果になることがあります。これを回避するには、Rotation プロパティの set メソッドを変更します。既存の if
句を次のように変更します。
if (rotation != value) { rotation = value; RotationMatrix = Matrix.CreateRotationX(MathHelper.PiOver2) * Matrix.CreateRotationZ(rotation); }
今度は、宇宙船がゆっくりと飛んでいるように見えます。これは、ビューが以前よりもさらに遠く離れているためです。Velocity
の宣言のすぐ下に、宇宙船の速度調整に使用できる浮動小数点定数を追加します。
//amplifies controller speed input private const float VelocityScale = 5.0f;
宇宙船の Update
メソッドの最後で、宇宙船の速度を少し上げるため、VelocityScale
値を使用するように Velocity の現在の計算を変更します (より正確には、ゲームのフレームあたりの単位数を増やします)。
Velocity += RotationMatrix.Forward * VelocityScale * controllerState.Triggers.Right;
これらの変更を行ったうえで実行すると、宇宙船を上から見下ろすビューが表示され、画面上を宇宙船が飛び回るようになります。画面から飛び去ってしまった場合は、ワープ ボタンを押します。後のステップで [A] ボタンを射撃に使用するので、[A] ボタンの元の用途を別のボタンに変更しておくことをお勧めします。
宇宙船ができたので、次に小惑星をゲームに追加します。簡素化するために、各小惑星の位置、方向、および速度のみを追跡することにします。その 3 つのメンバーのみを持つ単純なクラスを作成します。ソリューション エクスプローラーで [GoingBeyond4Windows] プロジェクトを右クリックし、[追加]、[クラス] の順にクリックします。ファイルに「Asteroid.cs
」という名前を付けます (必ず、Microsoft.Xna.Framework のステートメントを使用して追加してください)。このクラスは "軽量" であるため、クラスから構造体に変更します (ファイル内で "class" の文字を "struct" に変更します)。構造体 (C# の語法では "値型" と呼ばれる) を使用する場合と使用しない場合とでは、微妙な違いが多数あります。このドキュメントでは説明しませんが、問題の多くはパフォーマンスおよびガベージ コレクション (GC) に関係するものです。Compact Framework チームによるブログの投稿 ( で、値型について次のように述べられています。
- 「ゲームには、通常、ゲームの状態を表す小さなオブジェクトが多数含まれています。これを最適化する明白な方法は、ライブ オブジェクトの数を減らすことです。これを行うには、それらのデータ構造を構造体 (より一般的な用語を使用すれば値型) として定義します。値型は GC ヒープの対象外です。もちろん、構造体がオブジェクト内でボックス化されていないことが前提となります。GC ヒープはコード内で気付かないうちに発生していることがよくあります。」
ここでは、Asteroid に値型を使用して (後で弾丸にも使用します)、ガベージ コレクション イベントを減らすと同時に、実装が複雑にならないようにします。
構造体に次の 3 つのメンバーを追加します。
public Vector3 position; public Vector3 direction; public float speed;
Game1.cs ファイルをダブルクリックします。.Game1
クラス内に、小惑星を格納する単純な配列を作成します。小惑星をレンダリングするために、いくつかのメンバーを Game1
クラスに追加します。宇宙船の宣言 (Ship ship = new Ship();)
の後に、次のコードを追加します。
Model asteroidModel; Matrix[] asteroidTransforms; Asteroid[] asteroidList = new Asteroid[GameConstants.NumAsteroids]; Random random = new Random();
4 行のそれぞれに新しい内容があります。1 行ずつ見ていきましょう。1 行目は、Content Pipeline プロセッサによって読み込まれた実際の小惑星モデルを記述する一連の情報を保持するオブジェクトです。これはすぐにできます。2 行目は、小惑星に対する個別のライティングおよびエフェクト変換に関するプレゼンス情報を保持します。特殊なライティング エフェクトは一切追加しませんので、モデルに既定のエフェクトを設定するだけです。3 行目は、小惑星の単純な配列ですが、GameConstants
クラスが使用されています。このクラスでは、通常、ゲームの開発とテストを行っているときに変更する可能性がある値を保持します。これについては、もっと短時間でできます。最後の行は、乱数ジェネレーターを作成します。これは、ゲーム内でいくつかの目的のために使用します。
この新しい GameConstants
クラスについて簡単に説明します。このような単純なゲームでの設計上のヒントの 1 つは、カスタマイズするゲーム パラメーターを 1 か所に集めることです。まず、そのためのクラスを作成します。[追加]、[クラス] の順にクリックします。そのファイルに GameConstants.cs
という名前を付けます。ファイルが開いたら、そのクラスに次の定数を追加します (PlayfieldSize
定数は後で使用します)。
//camera constants public const float CameraHeight = 25000.0f; public const float PlayfieldSizeX = 16000f; public const float PlayfieldSizeY = 12500f; //asteroid constants public const int NumAsteroids = 10;
カメラ定数を追加したことから推測されるとおり、Game1
クラスの CameraPosition
宣言を次のように変更します。
Vector3 cameraPosition = new Vector3(0.0f, 0.0f, GameConstants.CameraHeight);
そして、projectionMatrix
(Initialize
メソッドにあります) の初期化を次のように変更します。
projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians(45.0f), GraphicsDevice.DisplayMode.AspectRatio, GameConstants.CameraHeight - 1000.0f, GameConstants.CameraHeight + 1000.0f);
もう一度 Asteroid
構造体に注目してください。小惑星をレンダリングするには、小惑星モデルを Content Pipeline に追加する必要があります。このゲームには既に Content\Models ディレクトリがあり、そこに宇宙船のモデルが保存されています。そのディレクトリを右クリックし、[追加]、[既存の項目] の順にクリックして、"asteroid1.x" モデルをそのディレクトリに追加します。次に、サンプル ファイルの内容を抽出したパスに移動します (「新たなステップ : 3D の XNA Game Studio」の「チュートリアル 1 : 画面での 3D モデルの表示」を実行したときに、この作業を行う必要があったことを思い出してください)。Content\Models ディレクトリから asteroid1.x ファイル (このファイルを表示するには [ファイルの種類] で [コンテンツ パイプライン ファイル] を選択する必要がある場合があります) を選択し、このファイルを Models ディレクトリに追加します。このモデルを追加する他に、小惑星のテクスチャー ファイル asteroid1.tga を、サンプルの Content\Textures ディレクトリからゲーム プロジェクト フォルダーの Content\Textures サブフォルダーに手動でコピーする必要もあります。単に手動でコピーし、[追加]、[既存の項目] の順に選択する方法は使用しないでください。また、コピー作業にも十分に注意を払ってください。初心者にありがちなミスは、テクスチャー ファイルを Model ディレクトリにコピーしてしまうことです。これは適切な方法ではありません。
今度は、Game1
クラスの LoadContent
メソッドに注目します。このメソッドでは、先ほど追加した小惑星のメッシュ モデルを読み込みます。p1_wedge モデルを追加した行のすぐ下で、小惑星モデルを読み込んで変換します。
asteroidModel = Content.Load("Models/asteroid1"); asteroidTransforms = SetupEffectDefaults(asteroidModel);
次に、asteroidList
に小惑星をいくつか格納するメソッドが必要になります。このメソッドは、Game1
クラスの Initialize
メソッドの最後 (base.Initialize()
呼び出しの前) に呼び出されます。小惑星を作成するときは、開始速度とランダム方向を設定します。この時点では、小惑星を画面の中央からスタートさせます。
小惑星のリストを格納するための、ResetAsteroids
という別のメソッドを作成します。
private void ResetAsteroids() { for (int i = 0; i < GameConstants.NumAsteroids; i++) { asteroidList[i].position = Vector3.Zero; double angle = random.NextDouble() * 2 * Math.PI; asteroidList[i].direction.X = -(float)Math.Sin(angle); asteroidList[i].direction.Y = (float)Math.Cos(angle); asteroidList[i].speed = GameConstants.AsteroidMinSpeed + (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed; } }
public const float AsteroidMinSpeed = 100.0f; public const float AsteroidMaxSpeed = 300.0f;
次に、Initialize
メソッドの base.Initialize()
呼び出しの直前に、ResetAsteroids()
の呼び出しを追加します。
小惑星の方向値では、基本的な三角関数を使用することにより、開始角度に基づいて、方向の x 成分と y 成分を決定しています。このゲームは 2 次元でのみプレイするので、z 値は変更しないでください。
小惑星を作成したら、それらをレンダリングする必要があります。これには、Draw()
メソッドを使用します。実際、asteroidList
に目を通し、宇宙船と同じ方法で各小惑星をレンダリングするだけです。そのために、Draw()
メソッドで宇宙船のレンダリングが完了した後に、次のコードを追加します。
for (int i = 0; i < GameConstants.NumAsteroids; i++) { Matrix asteroidTransform = Matrix.CreateTranslation(asteroidList[i].position); DrawModel(asteroidModel, asteroidTransform, asteroidTransforms); }
このコードをこのまま実行すると、宇宙船と 1 個の小惑星が中央にレンダリングされます。実際には 10 個の小惑星があるのですが、すべてが重なって描画されるので 1 個に見えます。
次のステップでは、これらの小惑星に動きを与えます。これを行うには、Update()
メソッドでリストの繰り返し処理を行い、それぞれの位置を更新します。宇宙船の速度を更新した直後にこれを実行します。
for (int i = 0; i < GameConstants.NumAsteroids; i++) { asteroidList[i].Update(timeDelta); }
追加するものの 1 つに、時間差があります。これは、小さいながら効果的な技です。プロパティを繰り返し呼び出して経過秒数の合計を確認するのではなく、更新するたびに 1 回ずつ timeDelta
値を計算します。これが、Game1
クラスの Update()
メソッドの最初の行になります。
float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
このループ内で各小惑星の Update()
メソッドを呼び出しています。そのため、このメソッドを Asteroid
構造体 (構造体の中かっこの外側ではなく内側) に追加する必要があります。豊富な XNA Framework 数値演算ライブラリーのおかげで、非常に簡単な方法でこれを記述できます。
public void Update(float delta) { position += direction * speed * GameConstants.AsteroidSpeedAdjustment * delta; }
すべてうまくいった場合は、すべての小惑星がランダムな方向に向かって宇宙船から遠ざかり、画面から見えなくなるまで飛んでいきます。
この画像のどこが問題なのでしょうか。
ゲーム内に小惑星を存在させ続けるには、画面の周囲で小惑星をラップ アラウンド (はみ出した部分を反対側に回り込ませて表示) させます。これを実現するには、小惑星が画面から外れてドリフトできるようにし、画面から消えたらすぐに反対側に移動させます。プレイ領域のサイズ定数の値は、実際のビュー空間に基づいた大まか数字から作成されています。本格的に設計されるゲームでは、表示領域の範囲を入念に計算し、小惑星モデルのサイズやその他のパラメーターに基づいて境界を決定します。ここでは、単純な方法で PlayfieldSize
定数を使用して、"ラップ アラウンド" をトリガーする領域を決定します。Asteroid
クラスの Update
メソッドで小惑星の位置を更新した後に、その小惑星を動かす必要があるかどうかを判断します。
if (position.X > GameConstants.PlayfieldSizeX) position.X -= 2 * GameConstants.PlayfieldSizeX; if (position.X < -GameConstants.PlayfieldSizeX) position.X += 2 * GameConstants.PlayfieldSizeX; if (position.Y > GameConstants.PlayfieldSizeY) position.Y -= 2 * GameConstants.PlayfieldSizeY; if (position.Y < -GameConstants.PlayfieldSizeY) position.Y += 2 * GameConstants.PlayfieldSizeY;
これで小惑星は、空間をよぎるように、画面の周囲で問題なくラップ アラウンドします。これでうまく行きました。あと少しです。このゲームでは、すべての小惑星を中央からスタートさせるかどうかはあまり重要ではありません。いずれにしても宇宙船と衝突することになるからです。画面の左端または右端から小惑星をスタートさせる場合は、コードを少し追加する必要があります。
小惑星をスタートさせる場所を選択するときには、少し注意が必要です。小惑星の位置の x 値については、まず、画面の左側または右側からスタートするように選択する必要があります。乱数ジェネレーターを使用して、0 か 1 のどちらかが選択されるようにします。0 の場合は左からスタートし、1 の場合は右からスタートします。これを行うには、random.Next(2)
を呼び出します。このメソッドは、0 から ( ) 内の値 (その値を含まない) までの数を生成するため、この場合は 0 か 1 のみが返されます。小惑星の位置の y 値については、プレイ領域の y の範囲内にあるランダムな数を選択するだけです。つまり、小惑星の位置を Vector3.Zero
の値に指定する行を変更します。最後のメソッドは次のようになります。
private void ResetAsteroids() { float xStart; float yStart; for (int i = 0; i < GameConstants.NumAsteroids; i++) { if (random.Next(2) == 0) { xStart = (float)-GameConstants.PlayfieldSizeX; } else { xStart = (float)GameConstants.PlayfieldSizeX; } yStart = (float)random.NextDouble() * GameConstants.PlayfieldSizeY; asteroidList[i].position = new Vector3(xStart, yStart, 0.0f); double angle = random.NextDouble() * 2 * Math.PI; asteroidList[i].direction.X = -(float)Math.Sin(angle); asteroidList[i].direction.Y = (float)Math.Cos(angle); asteroidList[i].speed = GameConstants.AsteroidMinSpeed + (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed; } }
少しわかりにくいかもしれませんが、数値計算に数回目を通せば、どうなっているのかがわかるようになります。この時点では、宇宙船は画面の中央にあり、いくつかの小惑星が左右両側からスタートしてランダムな方向と速度で移動します。
次に、ステップ 3 および 4 で使用するいくつかのコンテンツ アイテムをゲームに追加してみましょう。1 つのモデルと 3 つのサウンドをゲームに追加します。
-
プロジェクトの Models セクションに "pea_proj.x" (弾丸モデル) を追加します。これを行うには、[Models] を右クリックし、[追加]、[既存の項目] の順にクリックします。ここでも、[ファイルの種類] を [コンテンツ パイプライン ファイル] に変更する必要がある場合がありますので注意してください。
このモデルは、ダウンロードしたサンプルのディレクトリの Content\Models の下 (小惑星モデルと同じ場所) にあります。さらに、Content\Texture からこのゲームの Content\Textures に、pea_proj.tga ファイルをコピーする必要があります。ここでも、[追加]、[既存の項目] の順に選択する方法は使用しないでください。
-
「チュートリアル 3 : XNA Game Studio によるサウンドの作成」での作業と同様に、ダウンロードしたサンプルのディレクトリである Content\Audio\Waves ディレクトリに移動し、weapons\explosion3.wav、explosions\explosion2.wav、および weapons\tx0_fire1.wav を、このゲームの Content\Audio\Waves ディレクトリにコピーします。
既存のサウンド エフェクト変数の直後に、追加したばかりのサウンド エフェクトを格納する新しい変数を 3 つ追加します。
SoundEffect soundExplosion2; SoundEffect soundExplosion3; SoundEffect soundWeaponsFire;
ここで、新しいサウンド エフェクトをロードするように LoadContent
メソッドを変更します。
soundExplosion2 = Content.Load("Audio/Waves/explosion2"); soundExplosion3 = Content.Load ("Audio/Waves/explosion3"); soundWeaponsFire = Content.Load ("Audio/Waves/tx0_fire1");
これで、ゲーム プレイ中の必要なときに、新しい爆発と武器の火炎エフェクトを使用する準備ができました。
これで、映像的におもしろくなりました。動き回らせることができる、サウンド効果付きの宇宙船ができました。画面上を飛び回る小惑星もうまくできました。ただし、残念ながら、小惑星を射撃することができません。一方で、小惑星も宇宙船を傷つけることがまだできません。次は、宇宙船と小惑星の衝突の検出を追加します。後のステップで、宇宙船からも攻撃できるようにします。
XNA Framework を使用すれば、単純な衝突の検出は容易です。このステップでは、BoundingSphere を使用します。これは、ターゲット モデルを囲むことができる最小サイズの球 (デフォルト値) を作成するオブジェクトです。BoundingSphere には、さまざまな種類の交差テストが含まれており、面、光線、ボックスとの衝突や、もちろん他の球との衝突も検出できます。これを使用して、テストする各オブジェクトの周りを非表示の球で囲い、互いに交差するかどうかを判定します。
ゲームプレイで忘れてはならないテクニックの 1 つは、コンテキストに応じて衝突のさまざまなルールを検討することです。ここでは、宇宙船を囲む球として、わざと宇宙船より小さい境界球を作成します。これは、ゲーム プログラミングにおけるちょっとした秘訣です。ほとんどのモデルはシェイプが不均一ですが、BoundingSphere では、球の半径を作成するときに、モデルの中心から最も遠い点が基準になります。このため、プレイヤーの宇宙船の近くで衝突が起きているように見えない結果になることがよくあります。少し小さい球を作成すると、これを解決できることに加えて、プレイヤーが小惑星に近づきすぎた場合に、少し "大目に見る" という効用もあります。そこで、GameConstants
クラスに、小惑星と宇宙船の境界球のサイズを設定する 2 つの定数を作成します。
public const float AsteroidBoundingSphereScale = 0.95f; //95% size public const float ShipBoundingSphereScale = 0.5f; //50% size
次に、Game1
クラスの Update
メソッドで小惑星の位置を更新した直後に、宇宙船を囲む実際の境界球を作成します。さらに、各小惑星を調べるループを作成します。このループ内で、小惑星を囲む一時的な境界球を作成して、宇宙船と小惑星の球が交差するかどうかを判定します。2 つの球が交差する場合は、爆発音を再生してループから抜けます。
//ship-asteroid collision check BoundingSphere shipSphere = new BoundingSphere( ship.Position, ship.Model.Meshes[0].BoundingSphere.Radius * GameConstants.ShipBoundingSphereScale); for (int i = 0; i < asteroidList.Length; i++) { BoundingSphere b = new BoundingSphere(asteroidList[i].position, asteroidModel.Meshes[0].BoundingSphere.Radius * GameConstants.AsteroidBoundingSphereScale); if (b.Intersects(shipSphere)) { //blow up ship soundExplosion3.Play(); break; //exit the loop } }
この時点でプログラムを実行すると、いくつかの重要なフィードバックが得られます。まず、衝突検査は十分に機能しているようです。2 番目に、衝突音が聞こえます。3 番目に、衝突音が適切ではないようです。これは、小惑星と宇宙船が互いに突き進むときに、フレームごとに常に衝突検査が行われるためであり、XNA Framework はフレームごとに爆発音を再生しようとするので、おかしなサウンドになります。衝突しているオブジェクトを更新およびレンダリングの対象から除けば、この問題を解決できます。実際のゲームでは、これは、宇宙船が爆発してプレイヤーは命を失うことを意味します。このチュートリアルでは、宇宙船と、宇宙船を爆発させた小惑星をディスプレイから削除してから更新します。この機能は次のステップで追加します。
今度のコードは少し複雑になりますが、XNA Game Studio の便利な機能を利用すれば簡単です。最初に、宇宙船が生きているか死んでいるかを知らせるブール フラグを作成する必要があります。これには、VelocityScale
の宣言の直後に Ship
クラスを使用します。
public bool isActive = true;
宇宙船と小惑星の衝突を検査する前に、isActive
フラグが true かどうかを確認する必要があります。これを行うには、既に記述した衝突コードを if ステートメントでラップします。これは、XNA Game Studio では簡単に行えます。衝突検査を行うコード ブロック全体 (BoundingSphere 宣言およびその直後のループ) を強調表示させ、選択したコードを右クリックして [ブロックの挿入] をクリックし、一覧から if ステートメント (#if ステートメントではない) を選択します。これで、選択したコードは if ステートメントでラップされ、ブール条件を待ち受けます。あとは true を ship.isActive
に置き換えるだけです。最後に、爆発音を再生した後に ship.isActive
を false に設定します。
これで爆発音は修正されますが、宇宙船および宇宙船を襲った小惑星は、ゲーム内にまだ表示されています。最初に宇宙船を削除します。Update()
メソッドでフラグを設定しましたが、宇宙船をそれ以降は描画しないようにする作業がまだ残っていました。そこで、もう一度、今度は Draw()
メソッド内のコード ブロックを if ステートメントでラップします。コードのどの部分で宇宙船を描画しているかは、もうおわかりになるでしょう。宇宙船を描画するコード行を選択して右クリックし、[ブロックの挿入] をクリックして、if (ship.isActive)
テストを挿入します。
このコードを実行すると、宇宙船を小惑星にうまく激突させることができます。このとき、爆発と共に宇宙船は消えます。
最後に、衝突した小惑星を削除する必要があります。これには、宇宙船とまったく同じフラグが必要です。小惑星ごとに、その小惑星を描画または更新するかどうかを知らせる isActive
フラグが必要です。この作業は、次の 5 つの手順で行います。実際に試してみてください。
- 宇宙船で行ったのと同じように、
Asteroid
クラス内にisActive
フラグを作成します。 -
Game1
クラスのResetAsteroids
メソッドで各小惑星を作成するときに、isActive
フラグを true に設定します。 - 小惑星を描画するコード内で、描画コードを if ステートメントでラップします。これは、各小惑星の繰り返し処理を行うループ内で発生します。
- 同様に、更新セクションでも同じ作業を行って、宇宙船との衝突検査を実行する前に小惑星がアクティブであるかどうかを確認する必要があります。
- 宇宙船が小惑星と衝突した場合は、爆発音の再生後すぐに、小惑星のアクティブ状態を false に設定します。
すべての手順を正しく実行した場合は、ほぼ実用可能なゲームができています。衝突、サウンド、宇宙船の動きがすべてうまく連動し始めます。ここで次の疑問が生じます。宇宙船を爆発させた後はどうすればよいのでしょうか。これは簡単です。ワープ ボタンを押してください。「チュートリアル 2:入力を使用してモデルを移動」で、宇宙船を中央に戻すコードを記述しました。これをそのまま利用できます (ただし、[A] ボタンであったという点は除きます)。今度は、[B] ボタンを押したときのコード ブロックに ship.isActive = true
ステートメントを追加します。(ヒント : Game1
クラスの UpdateInput
メソッドに注目してください)。また、ワープ ボタンを [A] から [B] に変更していない場合は、ここでその変更も行ってください。これで、宇宙船は瞬時に復活します。
次のステップでは、ゲームに弾丸を追加して、自ら攻撃できるようにします。これまで行ってきた作業のおかげで、弾丸の作業は簡単に思えることでしょう。
ゲームの中の弾丸は、多くの点で小惑星に似ています。一定の方向に進み、そこにあるものと衝突します。ただし、弾丸は少し違った方法で処理します。このプロセスではゲームを少し細かく調整します。
さいわい、Bullet
構造体は、Asteroid
構造体とまったく同じですので、Asteroid
の実装ファイルをコピーして、ファイル名を Bullet.cs に変更し、構造体名を Bullet
に変更するだけで済みます。さらに、次のような新しい定数を GameConstants
クラスに追加します。これらは後で使用します。
public const int NumBullets = 30; public const float BulletSpeedAdjustment = 100.0f;
ここで、次の点について前もって少し検討しておいてください。空間で弾丸が飛ぶ時間はどれくらいにするか。画面で弾丸をラップ アラウンドさせるか。一定の秒数間だけ存続させるのか、一定の距離を進むようにするのか。小惑星と宇宙船の両方と衝突できるようにするのか。こうしたすべての問題に取り組むことが、ゲームの物理的動作を作り上げるための正統な方法です。ただし、この場合、弾丸は画面の外に出たら消えるだけです。このため、Bullet
クラスの Update()
メソッドでは、弾丸がビューの外に出たら非アクティブのフラグが設定されるようにします。
この単純な検査は、Asteroid
構造体で行ったものと同様です。
public void Update(float delta) { position += direction * speed * GameConstants.BulletSpeedAdjustment * delta; if (position.X > GameConstants.PlayfieldSizeX || position.X < -GameConstants.PlayfieldSizeX || position.Y > GameConstants.PlayfieldSizeY || position.Y < -GameConstants.PlayfieldSizeY) isActive = false; }
Asteroid
構造体と同様に、Bullet
構造体の変更もこれで完了です。ただし、ゲーム内で弾丸を実際に動かすための作業をいくつか行う必要があります。この作業は以前に小惑星で行ったものと同じですが、基本的な手順を復習しましょう。
- モデルを Content Pipeline に読み込んで、エフェクト変換を設定します。
- ゲーム内のすべての弾丸を追跡するためのリストを作成します。
- 弾丸を作成し、プレイヤーが特定のボタンを押したときの発射音を作成します。
- 飛行中の弾丸を描画します。
- 小惑星と弾丸の衝突を検査します。衝突した場合は、爆発音を鳴らし、衝突した弾丸と小惑星を削除します。
必要なインスタンス変数の作成から始めます。asteroidList
変数および asteroidModel
変数を作成した場所の下に、弾丸を保持するリストと、弾丸のシェイプを保持するモデルを作成します。
Model bulletModel; Matrix[] bulletTransforms; Bullet[] bulletList = new Bullet[GameConstants.NumBullets];
次に、LoadContent()
メソッドで、pea_proj
モデルを bulletModel
に割り当てます。以前に、pea_proj.x を Content\Models ディレクトリに追加したことを思い出してください。
bulletModel = Content.Load("Models/pea_proj"); bulletTransforms = SetupEffectDefaults(bulletModel);
小惑星とは異なり、弾丸は Initialize
内には作成しません。代わりに、ユーザーがコントローラーの [A] ボタンを押すたびに弾丸を作成します。UpdateInput()
メソッドの一番最後に新しい条件を追加します。
//攻撃しますか。if (ship.isActive && currentState.Buttons.A == ButtonState.Pressed) { //別の弾丸を追加します。非アクティブの弾丸スロットを見つけて、使用します//弾丸スロットがすべて使用されている場合、ユーザー入力を無視します for (int i = 0; i < GameConstants.NumBullets; i++) { if (!bulletList[i].isActive) { bulletList[i].direction = ship.RotationMatrix.Forward; bulletList[i].speed = GameConstants.BulletSpeedAdjustment; bulletList[i].position = ship.Position + (200 * bulletList[i].direction); bulletList[i].isActive = true; soundWeaponsFire.Play(); score -= GameConstants.ShotPenalty; break; //ループを終了します } } }
このコードには、説明が必要な興味深い技が含まれています。弾丸の最初の位置を計算するときに、弾丸が船首から発射されるように見えるようにします。したがって、コードでは、まず、弾丸のスタート位置を決めます。これは宇宙船の中心にします。次に、弾丸を船首の方向に 200 単位移動させます (200 というのは、宇宙船の中心から船首までのおおよその距離です)。
この種の "モーション オフセット" は、ゲーム開発で非常によく使用されます。ここでは説明しませんが、宇宙船の現在の速度を弾丸の速度に追加することもできます。
これで実際にゲームを実行し、発射 ([A]) ボタンを押すことができますが、弾丸はまだ見えません (描画していないため)。発射ボタン ([A] ボタン) を押すと、宇宙船と小惑星による爆発時と同じように、サウンドに問題があることに気が付くでしょう。これは、サウンドをトリガーする回数が多すぎるためです。実際、発射ボタンを押したままにしていると、次々と弾丸が発射されます (弾丸の "スロット" が使い尽くされるまで)。ボタンが押されるたびに 1 回だけ弾丸を発射するには、UpdateInput()
メソッドに簡単な修正を加えます。
UpdateInput
の問題点は、ユーザーの以前の入力状態を追跡できないことです。これを行う変数を作成します。GraphicsDeviceManager
宣言 (Game1
クラスの先頭近くにある) の直後に、この変数を追加します。
GamePadState lastState = GamePad.GetState(PlayerIndex.One);
次に、UpdateInput
メソッドの最後で、ユーザーのゲーム パッドの状態を保存します。
lastState = currentState;
最後に、"発射" エフェクトの if ステートメントを変更して、このコードによる前回の更新時にボタンが押されていなかったことを確認するようにします。
if (ship.isActive && currentState.Buttons.A == ButtonState.Pressed && lastState.Buttons.A == ButtonState.Released)
プログラムを実行すると、[A] ボタンを 1 回押すたびに発射音が聞こえるようになっています。方法がわかったところで、一貫性を持たせるため、ハイパースペース ボタンに同じチェック処理を追加します。次のステップでは、画面上で飛んでいる弾丸を描画します。さいわい、このコードは、Draw
メソッドで "asteroid" を "bullet" に置き換える以外は、小惑星を描画するコードとまったく同じです。
for (int i = 0; i < GameConstants.NumBullets; i++) { if (bulletList[i].isActive) { Matrix bulletTransform = Matrix.CreateTranslation(bulletList[i].position); DrawModel(bulletModel, bulletTransform, bulletTransforms); } }
次に、Update
メソッドについてもまったく同じ作業を繰り返します。小惑星の位置を更新する部分の直後 (小惑星と宇宙船の衝突検査の前) に、弾丸を更新するコードを追加します。
for (int i = 0; i < GameConstants.NumBullets; i++) { if (bulletList[i].isActive) { bulletList[i].Update(timeDelta); } }
この時点でコードを実行すると、実際に、ほぼ実用的なゲームになっています。残っているのは、弾丸と小惑星の衝突の検査だけです。この処理は非常に簡単です。必要なのは、小惑星ごとにループ処理を実行し、それらに弾丸が衝突しているかどうかを調べることだけです。衝突している場合は、その弾丸と小惑星の両方を非アクティブにし、これを小惑星のリストの最後まで繰り返します。このコードは、if (shipAlive)
の代わりに各小惑星に対してループ処理を実行する以外は、宇宙船と小惑星の衝突コードとまったく同じです。注意することが 1 つあります。この衝突検査は、宇宙船が小惑星と衝突するかどうかを確認する前に行ってください。そうすれば、プレイヤーは破壊される前に相手を "仕留める" チャンスが与えられます。
//bullet-asteroid collision check for (int i = 0; i < asteroidList.Length; i++) { if (asteroidList[i].isActive) { BoundingSphere asteroidSphere = new BoundingSphere(asteroidList[i].position, asteroidModel.Meshes[0].BoundingSphere.Radius * GameConstants.AsteroidBoundingSphereScale); for (int j = 0; j < bulletList.Length; j++) { if (bulletList[j].isActive) { BoundingSphere bulletSphere = new BoundingSphere( bulletList[j].position, bulletModel.Meshes[0].BoundingSphere.Radius); if (asteroidSphere.Intersects(bulletSphere)) { soundExplosion2.Play(); asteroidList[i].isActive = false; bulletList[j].isActive = false; break; //no need to check other bullets } } } } }
すべてうまくいった場合は、宇宙船を飛び回らせたり、小惑星を撃ったり、小惑星と衝突させたりすることができます。おめでとうございます。これで、初めての XNA Framework ゲームができました。ただ、青色の背景では、Asteroids ゲームらしくありません。宇宙空間の背景が必要です。それと、もちろん、スコアを記録することも必要です。それが最後のステップになります。
最後のステップでは、見た目を魅力的にし、ゲームの雰囲気をもっと味わえるように、ゲームの最終仕上げを行います。この作業は、2 つのパートに分けて行います。最初のパートでは、2D のバックグラウンド テクスチャーをゲームに追加して、宇宙空間らしくします。2 番目のパートでは、単純なスコア記録メカニズムをゲームに追加します。どちらの手順を実行する場合も、すべての 2D アイテムはスプライトとして描画されるということを忘れないでください。背景とスコアは、描画方法に関しては違いはありませんが、いつ描画するかが重要になります。
最初の手順では、星空の背景のテクスチャーを作成する必要があります。まず、Asteroid
モデルおよび Bullet
モデルを宣言した同じ場所に、stars
Texture2D
オブジェクトを追加します。
bulletModel
オブジェクトおよび bulletTransforms
オブジェクトを作成した直後に、そのテクスチャーを読み込みます。
stars = Content.Load("Textures/B1_stars");
最後に、Draw()
メソッドの始めの部分で、グラフィック デバイスに対する Clear
呼び出しの直後に星空の背景を描画します。背景は、最後ではなく、最初に描画することが重要です。そうしないと、先に描画されたオブジェクトの上に背景が描画されるので、先に描画されたもの (小惑星など) がすべて覆い隠されてしまいます。
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None); spriteBatch.Draw(stars, new Rectangle(0, 0, 800, 600), Color.White); spriteBatch.End();
次に、B1_stars.tga ファイルをプロジェクトの Content\Textures ディレクトリに追加します ([テクスチャー] を右クリックし、[追加]、[既存の項目] の順にクリックします)。次に、抽出したサンプルの Content\Textures フォルダーに移動し、B1_stars.tga ファイルを選択します。ここでゲームを実行すると、背景には美しい星が広がり、すべてのゲームプレイがその前面に表示されます。
最後に残っているのは、ゲームのスコアを記録することです。これは、次に示すいくつかの簡単な手順で実現できます。
- スプライト フォントを作成し、それを Content Pipeline 処理に追加します。
- その他のコンテンツと共にスプライト フォントを読み込みます。
- 表示文字列を設定し、
DrawString
メソッドを呼び出します。
スプライト フォントの作成は簡単です。まず、Content フォルダーの下に Fonts という名前の新しいフォルダーを作成します。次に、このフォルダーを右クリックし、[追加]、[新しい項目] の順にクリックします。メニューから [スプライト フォント] を選択します。このファイルの既定の名前は SpriteFont1.spritefont です。既定の名前のままでもかまいませんが、ここでは、使用するフォントと同じ名前にします。Kootenay フォントを使用しているので、ファイルに Kootenay.spritefont という名前を付けます。この手順を覚えたら、後でさまざまなフォントを使って自由に試してみてください。ファイルを作成すると、そのファイルが開いて、各種のフォント パラメーターを編集できるようになります。ここでは、既定の設定のままにしてファイルを閉じます。
スプライト フォントを作成したら、Game1
クラスにコードを追加して、文字を表示できるようにします。stars
オブジェクトの宣言の直後に、さらにいくつかの宣言を追加します。
SpriteFont kootenay; int score; Vector2 scorePosition = new Vector2(100, 50);
最初の宣言では、スプライト フォントを保持します。2 番目の宣言はスコア用の単純なカウンターです。最後の scorePosition
オブジェクトでは、画面上の座標にスコアを配置します。scorePosition
を GameConstants
クラスに移動することもできますが、Vector2
クラスのコンパイル ルールにより、それを const 値にすることはできません。
スプライト フォントを読み込むには、LoadContent
メソッドの最後に次の 1 行を追加します。
kootenay = Content.Load("Fonts/Kootenay");
あとは画面にスコアを表示するだけです。これは、描画順序のルールに注意しさえすれば、とても簡単です。Draw
メソッドには、非常に明確な 4 つの描画ステップがあります。背景を描画し、次にゲーム要素を描画します (宇宙船、小惑星、弾丸の順)。前に述べたように、ゲーム要素の後に背景を描画すると、最後に描画する星空によって画面全体が覆われてしまうため、星空しか見えなくなります。これと同じ問題が、ゲーム スコアにも当てはまります。ゲーム スコアは最後に描画して、他のゲーム要素の上に表示されるようにします。
もう既におわかりのように、Draw
メソッドで base.Draw
を呼び出す直前にスコアを描画します。文字列を描画する実際のコードでは、スプライト バッチの Begin/End
ペアの間で DrawString
を呼び出します。
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.None); spriteBatch.DrawString(kootenay, "Score: " + score, scorePosition, Color.LightGreen); spriteBatch.End();
ここでゲームを実行すると、左上隅にスコアが表示されます。また、スコアの背後でもゲーム要素が描画され、指定したエフェクトが適用されているのがわかります。次に、ゲームの得点方法について考えてみます。
"動かして撃つ" だけでは良いゲームプレイとは言えません。1 つ以上の目標を達成するためにプレイヤーに意思決定と得失評価を行わせることが大事です。このゲームでは、プレイヤーが 1 回撃つたびに減点し (攻撃行為による損失が発生する)、ワープ ボタンを押したときにも減点します (防御行為による損失が発生する)。さらに、プレイヤーが死んだときにも減点します。ほとんどのビデオ ゲームでは、限られた数の生命を与えられ、プレイヤーのアバターが破壊されると 1 つの "生命" が差し引かれます。ただし、このゲームでは、複数の生命を与えるシステムは実装されていないので (後で独力で実装してみてください)、単に点数を引くだけです。小惑星を破壊した場合は、そのたびに得点を与えます。まず、GameConstants
クラスでスコア値を設定します。
public const int ShotPenalty = 1; public const int DeathPenalty = 100; public const int WarpPenalty = 50; public const int KillBonus = 25;
次に、適切な場所でスコアを変更します。たとえば、射撃による減点は UpdateInput
メソッドに追加します。プレイヤーが弾丸を発射したことを記録した直後、ここでは soundWeaponsFire.Play();
行の直後が最も適切です。
score -= GameConstants.ShotPenalty;
他の 3 か所で同様の作業を行う必要があります。次の場所になりますので、自分でやってみてください。
- 宇宙船が小惑星と衝突したと判定されたとき (score から DeathPenalty を引く)。
- 弾丸が小惑星と衝突したと判定されたとき (score に KillBonus をたす)。
- プレイヤーがワープ ボタンを押したとき (score から WarpPenalty を引く)。
このチュートリアルの最初の目標は、ゲームを記述するためのツール、素材、および知識をすぐに活用できることを示し、初めてのゲームを手順に従って記述してもらうことでした。このチュートリアルでは学んだ内容は次のとおりです。
- カメラ ビューを変更して異なるレンダリング視点を実現する。
- 単純な衝突検出ルーチンを記述する。
- 多くの事象が一度に発生しているように見えるゲーム環境を作成する。
- 2D と 3D のレンダリングを統合する。
- ゲーム内にテキストをレンダリングする。
- 得点するのも失点するのもプレイヤー自身の決断しだいという "ゲームプレイ" 感を作り出す。
ゲームを作成するプロセスも楽しんでいただけたならさいわいです。ゲームを作成することはゲームをプレイすることと同じくらいおもしろいものです。でも、これはほんの手始めにすぎません。ここで作成したゲームはおもしろいものですが、さらに魅力的でおもしろいゲームにするためにできることは、まだたくさんあります。ゲームをレベル アップする方法についてのいくつかの提案 (全部ではありません) を次に示します。
- 画面上で宇宙船をラップ アラウンドさせる。
- 宇宙船が小惑星と衝突したときにコントローラーを振動させる。
- 大きい小惑星を、小さい小惑星に分裂させる。
- 弾丸が小惑星に当たったときに爆発エフェクトを追加する。
- 宇宙船が飛ぶときのエンジン パーティクル エフェクトを追加する。
- プレイヤーの宇宙船を攻撃する悪賢い "UFO" を追加する。
- ゲームに "ハイ スコア" 機能を追加する。
- プレイ場面がクリアされたことを確認して、新しいレベル (小惑星の数と速度が増すなど) を開始する。
この時点で、ゲームの構築に必要な基本要素 (グラフィック、入力、およびサウンド) の多くを作成しました。それでも、まだ「どのようにしてゲームを構築するのか」という疑問があるかもしれません。
ゲームは表現的なプロセスであり、創造的な問題の解決に多くの労力が費やされます。ゲームを作成する 1 つの正しい方法があるわけではありません。ここで作成したサンプルでは、まだ多くの要素が不足しています。宇宙船はどのような相手とやり取りするのか。ゴールはあるのか。宇宙船がゴールに到達するのを阻む障害物は何か。
これらの質問に答えることにより、ゲームを定義し、独自のゲームを構築することができます。興味をひかれたゲームをプレイし、XNA クリエーターズ クラブ オンライン をチェックし、『プログラミング ガイド』を読み込み、XNA Framework を研究して、自分だけのゲームを構築することを楽しんでくださいXNA Game Studio を活用されることを願っています。
0 コメント:
コメントを投稿