このチュートリアルのコードは、このチュートリアルの本文で説明する手法を実際に示したものです。このチュートリアルの完全なサンプル コードをダウンロードできます。ダウンロード内容には、完全なソース コードと、このサンプルに必要な追加のサポート ファイルが含まれます。
初心者のゲーム プログラマーにとっては、ゲーム コーディングの基本を理解することが最も難しいステップになります。完成したゲームを示すサンプル、ヒント、特定のテクニックの使用方法を示すチュートリアルなどを見つけることは簡単ですが、そこにはゲーム作成のプロセスについての情報はほとんど示されていません。このチュートリアルの目的は、ゲーム作成のプロセスについて学びながら、ほぼ完全なゲームを記述できるようにすることです。さらに、このチュートリアルでは完全なサンプル ファイル (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 宣言は次のようになります。