ゲーム開発入門の第 5 回にようこそ。ここまでで、機能する 3D 環境を作成し、カメラの向きと位置をキーボードとマウスを使用して操作できるようになっています。この記事では、事前定義のメッシュ ファイルを使用して 3D オブジェクトをゲームに追加し、単純なカリングを実装します。
コードのクリーンアップ
この記事のクリーンアップは、主にナビゲーション キーの修正、および不要になった項目の除去です。次の変更は、この記事に付属のコードに既に統合されています。
- Camera クラスのラジアンと度の変換メソッドを、Geometry クラスのユーティリティ クラスに置き換えました。
- GameEngine の CheckForInput メソッドのキー割り当てを次のように修正しました。
- W および S は、Z 軸を調整します。W は前方向 (プラス)、S は後方向 (マイナス) です。
- A および D は、X 軸を調整します。A は左方向 (マイナス)、D は右方向 (プラス) です。
- Q および Z は、Y 軸を調整します。Q は上方向 (プラス)、Z は下方向 (マイナス) です。ここでの戦車は飛ぶことができないため、Y 軸に沿っての移動は後で削除する予定です。
カメラの初期位置を調整し、Camera クラスの _z 変数を 10 に設定することで表面より上になるようにしました。これは戦車の視点を表すため、0 よりも高い必要があります。
Joystick クラスを除去しました。
August SDK を更新しました。
HiResTimer クラスの GetElapsedTime メソッドが float を戻すように変更しました。また、GameEngine の _deltaTime を float に変更しました。
HiResTimer.Reset を GameEngine Render メソッドから除去しました。HiResTimer.Start を GameEngine クラスのコンストラクタに移動しました。 IDispose
Keyboard クラス、Mouse クラスのようないくつかのクラスは、IDisposable インターフェイスを実装しています。これは、.NET Framework リファレンスのアンマネージ リソースをクリーンアップするための Finalize および Dispose の実装 (英語) で説明されている、.NET の Dispose パターンの実装です。
.NET の Dispose パターンは、プログラムが .NET ランタイムによって管理されないリソースを使用する場合を用途として想定しています。このような "アンマネージ" リソースは、決定的方法で確実に解放されるように特別な方法でクリーンアップする必要があります。.NET ガベージ コレクションは決定的ではないため、クリーンアップが正常に行われるようにするには特定の一連のステップを使用する必要があります。このステップが、Dispose パターンで定義されています。
ゲーム開発では多くのアンマネージ リソースを使用するため、DirectX、またはファイル リソースとやり取りするすべてのクラス (使用するクラスのほぼすべて) で Dispose パターンを実装することが最適です。これは、メモリ リークを防ぎ、ゲームのパフォーマンスを向上させることにつながります。
このパターン、および .NET のガベージ コレクションに関する基本的な情報については、関連の資料を参照してください。この記事のすべてのクラスには、Dispose パターンを追加しました。また、今後もそのようにします。
今回、BattleTank 2005 に必要となるものはユニットです。最初の記事 (英語) で示した当初のゲームのスクリーン ショットに戻ると、図形と相手の戦車が必要であることがわかります。図形は、プレイヤー、または敵の戦車が避難場所として使用できる障害物です。敵の戦車は、最終的に標的となるものです。これらのオブジェクトは、何もない景色の中を移動する際の手掛かりともなります。景色の中には参照ポイントがないため、移動していることを確認するためにカメラの位置をコンソールに出力する必要があったことを思い出してください。景色は、次の記事で地形を追加することにより完成させます。
ユニット
BattleTank 2005 には、2 種類の 3D オブジェクトがあります。障害物と戦車です。両者の主な違いは、戦車は動くことができ、障害物は動くことができないという点です。多数の障害物、および戦車を用意するため、それらを編成し、まとめて操作できるようにします。これは、障害物や戦車をコレクションに追加することにより実現します。
両方の種類のオブジェクトを単一のコレクションに追加することもできますが、種類ごとにコレクションを分ける方が適切です。このようにオブジェクトを分離することにより、1 つのグループに専念することができ、各ユニットでその種類を確認するというオーバーヘッドの発生を避けることができます。これは、パフォーマンスの向上にもつながります。さらに、移動ユニットの位置は更新できるようにします。静止ユニットでは、これは省略します。
ジェネリック
.NET の前までのバージョンでは、ユニットを保持するコレクション クラスを作成し、それを確実にタイプ セーフにすることは相当に複雑な作業でした。.NET 1.0、および 1.1 での 1 つのコレクションは、複数のオブジェクトの集合でした。これは、柔軟性が高く、どのような型の値、または参照でも受け入れることを意味します。しかしこれは、コレクションからオブジェクトを取得するときには必ず、オブジェクトを適切な型にキャストする必要があるということでもあります。つまり、オブジェクトのキャストが失敗する危険性が存在します。可能性のある例外を考慮し、キャストの前に各オブジェクトをテストすることで、オーバーヘッドは大きくなります。
.NET 2.0 では、ジェネリックを使用して、最小限の手間でタイプ セーフなコレクションを作成することができます。このようなコレクション クラスは、通常のコレクションよりも安全性もパフォーマンスも向上します。これは、ジェネリックの使用の可能性の 1 つに過ぎませんが、おそらく最もよく使用される方法です。
ジェネリックについては、MSDN の次の記事を参照してください。
すべてのユニットには、その目的に関係なく、共通の事項が多くあります。アプリケーションの保守性を高めるために、ユニットの共通する特性はすべて単一の基本クラスに分離します。すべてのユニットをこの基本クラスから派生させ、カスタム プロパティ、およびメソッドで拡張します。
基本クラス、および派生クラスを作成することにより、すべてのユニットをポリモーフィズムな形で扱うことを可能にするオブジェクト階層が作成されます。これは、オブジェクト指向プログラミングのとても強力な概念です。このような Visual Studio .NET のポリモーフィズムについては、MSDN の次のトピックを参照してください。
結果として作成される基本クラスは、次のようになります。
Visual C#
public abstract class UnitBase : IDisposable
{
public UnitBase (Device device, string meshFile,
Vector3 position, float scale )
public void Render ( Camera camera )
public bool IsCulled
public Vector3 Position
public float Radius
private void LoadMesh ( )
private void ComputeRadius ( )
private Vector3 _position;
private float _radius;
private bool _isCulled;
private Device _device;
private string _meshFile;
private float _scale;
private Mesh _mesh = null;
private Material[] _meshMaterials;
private Texture[] _meshTextures;
}
Visual Basic
Public MustInherit Class UnitBase
Implements IDisposable
Public Sub New(ByVal device As Device, _
ByVal meshFile As String, ByVal position As Vector3, _
ByVal scale As Single)
Public Sub Render(ByVal camera As Camera)
Public Property IsCulled() As Boolean
Public Property Position() As Vector3
Public Property X() As Single
Public Property Y() As Single
Public Property Z() As Single
Public ReadOnly Property Radius() As Single
Public Sub Dispose() Implements IDisposable.Dispose
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
Protected Overrides Sub Finalize()
Private _disposed As Boolean
Private Sub LoadMesh()
Private Sub ComputeRadius()
Private m_position As Vector3
Private m_radius As Single
Private m_isCulled As Boolean
Private m_device As Device
Private m_meshFile As String
Private m_scale As Single
Private m_mesh As Mesh = Nothing
Private m_meshMaterials As Material()
Private m_meshTextures As Texture()
End Class
このコードの大部分は理解できることと思いますが、メッシュ ファイルとは何か、およびなぜそれを読み込むのかについては疑問が残るかもしれません。
3D モデリング
複雑な 3D オブジェクトをコードで 1 行ずつ作成する方法には、いずれ疑問を感じるようになります。スカイボックスに作成した単純なキューブのみでも、約 200 行のコードになります。3D オブジェクトの作成には、より優れた方法があるはずです。
多くの 3D モデルは、Maya、または 3ds Max などの専用のモデリング プログラムを使用してアーティストによって作成されます。これらのプログラムは、独自のファイル形式 (Maya は iff、3DS Max は 3ds) でモデルに関する情報を保管します。DirectX は、これらのファイル形式を直接的に読み取ることはできませんが、X ファイルと呼ばれる形式は読み取ることができます。
X ファイル
DirectX は、X ファイル形式と呼ばれるファイル形式を定義します。これは、3D モデルの定義を含みます。このファイルを使用し、X ファイルから 3D モデルを読み込むことにより、3D モデルをゲームに読み込むために記述する必要のあるコードの量は激減します。DirectX の用語では、この X ファイルがメッシュ ファイルと呼ばれます。
メッシュ
前の記事で、メッシュは、3D 図形を記述するデータであると説明しました。メッシュ データには、図形を構成する頂点のリスト、それらの頂点が相互にどのように接続するかについての情報、およびすべての頂点についてのテクスチャ情報が含まれます。
また、3D オブジェクト ファイルを、他の形式から X ファイル形式に変換する変換プログラムも使用できます。DirectX SDK には、実際に、Maya、および 3ds Max 向けのプラグインが付属しています。これにより、これらのプログラムで作成されたファイルを X ファイル形式に変換することができます。
作業を始めるための X ファイルは、どこで入手できるでしょうか。DirectX SDK には、Samples フォルダの下に Media というフォルダがあります。ここには、使用できる X ファイルが数多く含まれています。DirectX SDK には、DirectX ビューア、および MeshViewer など、X ファイルの処理を可能にするユーティリティも多く含まれています。ユーティリティは、すべて Utilities フォルダにあります。手間と時間を節約するためにも、これらのユーティリティ、および SDK に付属の他のユーティリティを確認することをお勧めします。
無償の 3D モデル: グラフィックに関してあふれる才能を持ち合わせていない限り、ゲームのモデルの作成はアーティストに任せることが得策です。インターネット上には、アーティストがその技能を披露するために作成した大量の 3D モデルがあります。一般的に、アーティストは、個人的に楽しむことを目的に他の人が自身の作品を使用することは制限していません。ただし、商用のゲームでそれらを使用する場合は別の問題となります。各モデルについて、それを使用する前にその使用規則を読み、理解するようにしてください。http://www.3dcafe.com/ (英語) は、無償のモデルについての優れたサイトです。
X ファイル、および事前に定義されたモデルを使用することで、ゲームへの 3D オブジェクトの統合にはまったく新しい世界が開かれます。各オブジェクトの下位レベルの詳細を気にかける必要がなくなり、単純に、事前に作成されたメッシュ ファイルを読み込むことが可能になります。
スカイボックスの更新
この新たな知識を最初に使用する場所は、スカイボックス コードのクリーンアップです。SDK では、Samples\Media\Lobby フォルダに、現在スカイボックスに使用しているようなキューブを記述する lobby_skybox.x ファイルが含まれています。ここでは、このファイルを Resources フォルダに skybox.x としてコピーし、使用するテクスチャ ファイルの名前に一致するようにテクスチャ ファイルを更新しました。
テクスチャの変更: ほとんどの X ファイルは、単純なテキスト エディタで開くことができます。ファイルで、テクスチャ ファイルへの参照を検索し (TextureFilename を探します)、それを実際に使用する名前に置き換えます。また、メッシュを読み込むフェーズの中で、TextureLoader.FromFile メソッドにテクスチャ ファイルの名前を渡すことで、独自のテクスチャ ファイルに置き換えることもできます。より優れたものを表示したい場合は、SDK メディア フォルダから lobby_skybox.x ファイル、およびすべての JPG ファイルをリソース フォルダにコピーし、スカイボックスの LoadMesh メソッド内の X ファイルの名前をこの X ファイルに変更します。結果のスカイボックスは、マイクロソフトのすべてのゲーム開発者が勤務するビルのロビーです。
Skybox クラスで、SetupCubeFaces メソッド、Copy で始まる 6 つのメソッド (CopyLeftFaceVertexBuffer、CopyFrontFaceVertexBuffer など)、および RenderFace メソッドを除去します。また、クラスの最後で宣言されているプライベート変数も、Device 変数を除いて、すべて除去することができます。Render メソッドで、カメラの Pitch および Heading をチェックするコードの行を除去します。最後に、コンストラクタ内の SetupCubeFaces への呼び出しを、LoadMesh への呼び出しに置き換えます。
ここで、次の 3 つの変数宣言をクラスの最後に追加します。
Visual C#
private Mesh _mesh = null;
private Material[] _meshMaterials;
private Texture[] _meshTextures;
Visual Basic
Private m_mesh As Mesh = Nothing
Private m_meshMaterials As Material()
Private m_meshTextures As Texture()
既にメッシュ、およびテクスチャが何であるかは理解できたと思いますが、マテリアルとは何でしょうか。
マテリアル
マテリアルは、ポリゴンがアンビエントやディフューズ ライトをどのように反射するか、およびスペキュラ ハイライトに関する情報や、ポリゴンが光を発するように見えるかどうかについての情報を記述します。テクスチャがポリゴンの見え方を定義するのに対して、マテリアルは光の反射の仕方を定義することを理解しておくことが重要です。
メッシュの読み込み
メッシュをファイルから読み込むことはとても単純です。これは、Mesh クラスの FromFile メソッドを呼び出すだけで行えます。(Mesh クラスは、使用する主要なクラスの 1 つであるため、Mesh クラス、およびそのメソッドを理解するために少し時間をかけると良いでしょう。)
Skybox クラスで、次のコードを追加します。
Visual C#
private void LoadMesh ( )
{
ExtendedMaterial[] materials = null;
Directory.SetCurrentDirectory (
Application.StartupPath + @"\..\..\..\Resources\" );
_mesh = Mesh.FromFile (@"skybox.x",
MeshFlags.SystemMemory, _device, out materials);
if ( ( materials != null ) && ( materials.Length > 0 ) )
{
_meshTextures = new Texture[materials.Length];
_meshMaterials = new Material[materials.Length];
for ( int i = 0 ; i < materials.Length ; i++ )
{
_meshMaterials[i] = materials[i].Material3D;
_meshMaterials[i].Ambient = _meshMaterials[i].Diffuse;
if (materials[i].TextureFilename != null &&
(materials[i].TextureFilename != string.Empty))
_meshTextures[i] = TextureLoader.FromFile ( _device,
materials[i].TextureFilename );
}
}
}
Visual Basic
Private Sub LoadMesh()
Dim materials As ExtendedMaterial() = Nothing
Directory.SetCurrentDirectory(Application.StartupPath _
& "\..\..\..\Resources\")
m_mesh = Mesh.FromFile("skybox.x", MeshFlags.SystemMemory, _
m_device, materials)
If (Not (materials Is Nothing)) AndAlso _
(materials.Length > 0) Then
m_meshTextures = New Texture(materials.Length) {}
m_meshMaterials = New Material(materials.Length) {}
Dim i As Integer = 0
While i < materials.Length
m_meshMaterials(i) = materials(i).Material3D
m_meshMaterials(i).Ambient = m_meshMaterials(i).Diffuse
If Not (materials(i).TextureFilename Is Nothing) AndAlso _
(Not (materials(i).TextureFilename = String.Empty)) Then
m_meshTextures(i) = TextureLoader.FromFile( _
m_device, materials(i).TextureFilename)
End If
System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
End While
End If
End Sub
X ファイルを読み取り、それをオブジェクトに変換する処理 (つまり、頂点やインデックス バッファの作成など) のほとんどは、DirectX が自動的に行います。しかし、メッシュのマテリアルおよびテクスチャの読み込みは手動で管理する必要があります。
X ファイル、およびテクスチャを Skybox に統合する最後のステップとして、Skybox クラスの Render メソッドを変更します。_device.RenderState.CullMode = Microsoft.DirectX.Direct3D.Cull.None; の行の直後に、次のコードを追加します。
Visual C#
for ( int i = 0 ; i < _meshMaterials.Length ; i++ )
{
_device.Material = _meshMaterials[i];
_device.SetTexture ( 0, _meshTextures[i] );
_mesh.DrawSubset ( i );
}
Visual Basic
While i < m_meshMaterials.Length
m_device.Material = m_meshMaterials(i)
m_device.SetTexture(0, m_meshTextures(i))
m_mesh.DrawSubset(i)
System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
End While
meshMaterials を再び繰り返し、Mesh の DrawSubset メソッドを呼び出して各サブセットを順番に描画します。これで完了です。Skybox クラスの全体は、わずか約 80 行の長さとなり、読みやすくなりました。
UnitBase クラスに戻り、そこで確認する必要のある次の項目は、IsCulled フラグです。
カリング
カリングについては、第 3 回の記事で簡単に説明しました。カリングとは、景色からオブジェクト全体を単純に取り除き、それらをレンダリングしないようにすることです。どのオブジェクトを除去するかを判断するロジックには、ごく単純なものからとても複雑なものまでさまざまなものが使用されます。BattleTank 2005 では、景色の視錐台に含まれないすべてのオブジェクトをカリングします。
ユニットが視錐台の中にあるかを判断するために、Camera クラスを拡張し、現在の視錐台に関する情報を提供するようにして、各オブジェクトをチェックし、それが視錐台の内側にあるか、外側にあるかを知ることができるようにします。このチェックの実行には、ComputeRadius メソッドで計算される、UnitBase クラスの Radius プロパティを使用します。
Geometry クラスの BoundingSphere メソッド (ラジアンと度の変換に現在使用しているものと同じもの) が、メッシュの頂点データを使用してメッシュ内のすべてのポイントを完全に含む球を計算します。メッシュは、この情報を頂点バッファに含みます。このバッファにアクセスする場合は、アクセスする前にロックし、完了したらロックを解除することが最善の方法です。また、完了したら頂点バッファを確実に破棄する必要もあります。これを最も安全に行うには、行われる処理に関わらず Dispose が確実に呼び出される using ステートメントを使用します。
Visual C#
private void ComputeRadius ( )
{
using ( VertexBuffer vertexBuffer = _mesh.VertexBuffer )
{
GraphicsStream gStream = vertexBuffer.Lock ( 0, 0,
LockFlags.None );
Vector3 tempCenter;
_radius = Geometry.ComputeBoundingSphere (gStream,
_mesh.NumberVertices, _mesh.VertexFormat,
out tempCenter ) * _scale;
vertexBuffer.Unlock ( );
}
}
Visual Basic
Private Sub ComputeRadius()
Dim vertexBuffer As VertexBuffer = Nothing
Try
vertexBuffer = m_mesh.VertexBuffer
Dim gStream As GraphicsStream = _
vertexBuffer.Lock(0, 0, LockFlags.None)
Dim tempCenter As Vector3
m_radius = Geometry.ComputeBoundingSphere(gStream, _
m_mesh.NumberVertices, m_mesh.VertexFormat, tempCenter) * _
m_scale
Finally
vertexBuffer.Unlock()
vertexBuffer.Dispose()
End Try
End Sub
オブジェクトのカリングにオブジェクトの半径を使用することはとても大ざっぱな方法です。これは、オブジェクトが単純な図形の場合には、かなり正確で正常に機能しますが、図形が複雑になるほど境界となる球面はオブジェクトそのものよりも相当に大きくなります。
商用ゲームでは、カリング可能なオブジェクトを最大限まで除去するカリング ルーチンを作成するために多大な労力が投資されています。ここで高機能なルーチンを作成し、視錐台に一部分のみが含まれるオブジェクトを識別することも可能ですが、ユニットの一部分のみをレンダリングする機能がなければ時間の無駄です。ここでは、1 点でも視錐台に含まれるすべてのユニットは、視錐台に完全に含まれているものとして扱います。
これで、ユニットの半径は認識できるようになりました。次に、半径を視錐台に対してチェックできるように視錐台を認識する必要があります。
視錐台
視錐台の情報は、カメラに簡単に追加できます。最初に、視錐台に関する情報を保持するデータ構造体を追加します。前の記事で、視錐台は、ピラミッドの上部を切り取ったような形であると説明しました。つまり、上部と底部の四角形の角、および各側面の面を保管する必要があります。角には、よく使用している Vector3 構造体の配列を使用します。面には、DirectX が提供する便利な Plane 構造体を使用します。
Visual C#
private Vector3[] _frustumCorners;
private Plane[] _frustumPlanes;
Visual Basic
private Vector3[] _frustumCorners;
private Plane[] _frustumPlanes;
次に、コンストラクタでこれらの配列を初期化します (4 個の角を持つ四角形が 2 個で、頂点は 8 個です。また、ポリゴンの 4 個の側面と上部と底部で、面は 6 個になります)。
Visual C#
_frustumCorners = new Vector3[8];
_frustumPlanes = new Plane[6];
Visual Basic
m_frustumCorners = New Vector3(8) {}
m_frustumPlanes = New Plane(6) {}
次のステップで、現在の視点行列、および射影行列を使用して視錐台を計算します。
Visual C#
private void ComputeViewFrustum ( )
{
Matrix matrix = _viewMatrix * _perspectiveMatrix;
matrix.Invert ( );
_frustumCorners[0] = new Vector3 ( -1.0f, -1.0f, 0.0f ); // xyz
_frustumCorners[1] = new Vector3 ( 1.0f, -1.0f, 0.0f ); // Xyz
_frustumCorners[2] = new Vector3 ( -1.0f, 1.0f, 0.0f ); // xYz
_frustumCorners[3] = new Vector3 ( 1.0f, 1.0f, 0.0f ); // XYz
_frustumCorners[4] = new Vector3 ( -1.0f, -1.0f, 1.0f ); // xyZ
_frustumCorners[5] = new Vector3 ( 1.0f, -1.0f, 1.0f ); // XyZ
_frustumCorners[6] = new Vector3 ( -1.0f, 1.0f, 1.0f ); // xYZ
_frustumCorners[7] = new Vector3 ( 1.0f, 1.0f, 1.0f ); // XYZ
for ( int i = 0 ; i < _frustumCorners.Length ; i++ )
_frustumCorners[i] = Vector3.TransformCoordinate (
_frustumCorners[i], matrix );
// ここで、面を計算します。
_frustumPlanes[0] = Plane.FromPoints (
_frustumCorners[0],
_frustumCorners[1],
_frustumCorners[2] ); // 前方の面
_frustumPlanes[1] = Plane.FromPoints (
_frustumCorners[6],
_frustumCorners[7],
_frustumCorners[5] ); // 後方の面
_frustumPlanes[2] = Plane.FromPoints (
_frustumCorners[2],
_frustumCorners[6],
_frustumCorners[4] ); // 左の面
_frustumPlanes[3] = Plane.FromPoints (
_frustumCorners[7],
_frustumCorners[3],
_frustumCorners[5] ); // 右の面
_frustumPlanes[4] = Plane.FromPoints (
_frustumCorners[2],
_frustumCorners[3],
_frustumCorners[6] ); // 上の面
_frustumPlanes[5] = Plane.FromPoints (
_frustumCorners[1],
_frustumCorners[0],
_frustumCorners[4] ); // 下の面
}
Visual Basic
Private Sub ComputeViewFrustum()
Dim matrix As Matrix = m_viewMatrix * m_perspectiveMatrix
matrix.Invert()
m_frustumCorners(0) = New Vector3(-1.0F, -1.0F, 0.0F)
m_frustumCorners(1) = New Vector3(1.0F, -1.0F, 0.0F)
m_frustumCorners(2) = New Vector3(-1.0F, 1.0F, 0.0F)
m_frustumCorners(3) = New Vector3(1.0F, 1.0F, 0.0F)
m_frustumCorners(4) = New Vector3(-1.0F, -1.0F, 1.0F)
m_frustumCorners(5) = New Vector3(1.0F, -1.0F, 1.0F)
m_frustumCorners(6) = New Vector3(-1.0F, 1.0F, 1.0F)
m_frustumCorners(7) = New Vector3(1.0F, 1.0F, 1.0F)
Dim i As Integer = 0
While i < m_frustumCorners.Length
m_frustumCorners(i) = _
Vector3.TransformCoordinate(m_frustumCorners(i), matrix)
System.Math.Min( _
System.Threading.Interlocked.Increment(i), i - 1)
End While
m_frustumPlanes(0) = Plane.FromPoints(m_frustumCorners(0),
m_frustumCorners(1), m_frustumCorners(2))
m_frustumPlanes(1) = Plane.FromPoints( _
m_frustumCorners(6), _
m_frustumCorners(7), _
m_frustumCorners(5))
m_frustumPlanes(2) = Plane.FromPoints( _
m_frustumCorners(2), _
m_frustumCorners(6), _
m_frustumCorners(4))
m_frustumPlanes(3) = Plane.FromPoints( _
m_frustumCorners(7), _
m_frustumCorners(3), _
m_frustumCorners(5))
m_frustumPlanes(4) = Plane.FromPoints( _
m_frustumCorners(2), _
m_frustumCorners(3), _
m_frustumCorners(6))
m_frustumPlanes(5) = Plane.FromPoints( _
m_frustumCorners(1), _
m_frustumCorners(0), _
m_frustumCorners(4))
End Sub
最初に、視点行列と射影行列を乗算することで結合します。次に、視錐台の 8 個の角を、カメラのすぐ前にあるキューブとして初期化します。FromPoints メソッドを使用して、これらの角を変換して、6 個の面を作成します。
視錐台は、このクラスの初期化時、および各レンダリング ループで計算されます。ComputeViewFrustum( ) への呼び出しを Camera クラスのコンストラクタの最後、および Camera クラスの Render メソッドの最後に追加します。(これは、新しく計算された視点行列および射影行列を使用できるように、最後に追加する必要があります。) これで、計算された視錐台、および各ユニットの半径を使用して、そのユニットが一部でも視錐台に含まれているか、つまりレンダリングする必要があるかを判断できます。IsInViewFrustum メソッドは、ユニットが視錐台に含まれている場合は true を戻します。含まれていない場合は、false を戻します。
Visual C#
public bool IsInViewFrustum ( UnitBase unitToCheck )
{
foreach ( Plane plane in _frustumPlanes )
{
if ( plane.A * unitToCheck.Position.X + plane.B *
unitToCheck.Position.Y + plane.C * unitToCheck.Position.Z +
plane.D <= ( -unitToCheck.Radius ) )
return false;
}
return true;
}
Visual Basic
Public Function IsInViewFrustum(ByVal unitToCheck As UnitBase) As Boolean
For Each plane As Plane In m_frustumPlanes
If plane.A * unitToCheck.Position.X + plane.B * _
unitToCheck.Position.Y + plane.C * unitToCheck.Position.Z + _
plane.D <= (-unitToCheck.Radius) Then
Return False
End If
Next
Return True
End Function
カリングのプロセスは、ユニットがレンダリングされる前に行われる必要があります。これは、BaseUnit クラスの Render メソッドで、実際にメッシュをレンダリングする前にカメラの視錐台をチェックすることにより実現します。このチェックを実際の Render メソッドに配置することにより、コードの他の場所ではカリングの状態をチェックしなくて済むようにしています。
Visual C#
if (camera.IsInViewFrustum ( this ) == false )
return;
Visual Basic
If camera.IsInViewFrustum(Me) = False Then
Return
End If
これで、基本的なインフラストラクチャは完成しました。次にユニットを追加します。しかし、UnitBase クラスは抽象クラスであるため、これはインスタンス化できません。基本クラスの目的は、ユニットの共通のプロパティ、および機能性をまとめて保持することだけです。ここで、BattleTank 2005 で使用するオブジェクトを表すクラス、つまり障害物 (Obstacle) と戦車 (Tank) を作成します。
Visual C#
public class Obstacle : UnitBase
{
public Obstacle ( Device device, string meshFile,
Vector3 position, float scale )
: base ( device, meshFile, position, scale )
{
}
}
Visual Basic
Public Class Obstacle
Inherits UnitBase
Public Sub New(ByVal device As Device, ByVal meshFile As String, _
ByVal position As Vector3, ByVal scale As Single)
MyBase.New(device, meshFile, position, scale)
End Sub
End Class
Obstacle クラスでは、現在のところは、基本クラスを呼び出す以外のことは行いません。このクラスには後で手を加えます。
Visual C#
public class Tank : UnitBase
{
public Tank ( Device device, string meshFile,
Vector3 position, float scale )
: base ( device, meshFile, position, scale )
public void Update ( float deltaTime )
private float _speed = 10.0f;
}
Visual Basic
Public Class Tank
Inherits UnitBase
Public Sub New(ByVal device As Device, ByVal meshFile As String, _
ByVal position As Vector3, ByVal scale As Single)
MyBase.New(device, meshFile, position, scale)
End Sub
Public Sub Update(ByVal deltaTime As Single)
MyBase.Z -= (m_speed * deltaTime)
End Sub
Private m_speed As Single = 10.0F
End Class
Tank クラスでは、後で記述の必要な _speed プロパティ、および Update メソッドを追加します。
時間を使用しての移動のシミュレート
Update メソッドは、直前のレンダリング ループから経過した時間を秒単位で示す float を受け取ります。第 2 回の記事で、フレーム レートを計算するために使用する deltaTime 変数を追加しました。この値がこの後、移動オブジェクトの位置を計算するために使用する値です。ここでは、時間の原則を使用することで、コンピュータの速さに関係なくそれぞれのコンピュータで同様の動きが確保されるようにし、さらに動きが滑らかに見えるようにします。たとえば、レンダリング ループの 1 回ごとに、移動オブジェクトの位置を 1 インチずつ更新した場合、オブジェクトは高速なコンピュータではより速く移動します。これは、高速なコンピュータでは、レンダリング ループを速く計算できるためです。このような動作は、回転するキューブで実験することができます。もう 1 つの問題として、レンダリング ループの処理が行われる速さは、CPU が実行する他のオペレーションの影響を受けるため、毎回同じではありません。このため、移動が不規則になる可能性があります。(これを理解する最も簡単な方法も、実験してみることです。Z 軸の増分を、speed * time の式ではなく、1 に変更して、どのようになるかを確認してください。)
更新する必要があるのは、戦車のみです。障害物は移動しません。
Visual C#
foreach ( Tank tank in _tanks )
{
tank.Update ( _deltaTime );
}
Visual Basic
For Each tank As Tank In m_tanks
tank.Update(m_deltaTime)
Next
戦車の実際の Update メソッドは、現在のところは、事前に定義されている速さを使用して、原点に向かって単純に戦車を移動します。
Visual C#
public void Update ( float deltaTime )
{
base.Z -= ( _speed * deltaTime );
}
Visual Basic
Public Sub Update(ByVal deltaTime As Single)
MyBase.Z -= (m_speed * deltaTime)
End Sub
次に、GameEngine クラスに、移動ユニット、および静止ユニットを保持する 2 つのジェネリック コレクションを作成します。
Visual C#
private List<UnitBase> _obstacles;
private List<UnitBase> _tanks;
Visual Basic
private List<UnitBase> _obstacles;
private List<UnitBase> _tanks;
実際のユニットは、CreateObstacles メソッド、および CreateTanks メソッドでコレクションに追加します。
Visual C#
private void CreateObstacles ( )
{
_obstacles = new List<UnitBase> ( );
_obstacles.Add ( new Obstacle ( _device, @"car.x",
new Vector3 ( 0, 0, 200 ), 1f ) );
_obstacles.Add ( new Obstacle ( _device, @"car.x",
new Vector3 ( 60, 0, 100 ), 1f ) );
_obstacles.Add ( new Obstacle ( _device, @"car.x",
new Vector3 ( -60, 0, 150 ), 1f ) );
_obstacles.Add ( new Obstacle ( _device, @"car.x",
new Vector3 ( 60, 0, -100 ), 1f ) );
_obstacles.Add ( new Obstacle ( _device, @"car.x",
new Vector3 ( -60, 0, -150 ), 1f ) );
}
private void CreateTanks ( )
{
_tanks = new List<UnitBase> ( );
_tanks.Add ( new Tank (_device, @"bigship1.x",
new Vector3 ( 0, 0, 200 ), 1f ) );
_tanks.Add ( new Tank (_device, @"bigship1.x",
new Vector3 ( 100, 0, 300 ), 1f ) );
_tanks.Add ( new Tank (_device, @"bigship1.x",
new Vector3 ( -100, 0, 500 ), 1f ) );
_tanks.Add ( new Tank (_device, @"bigship1.x",
new Vector3 ( 100, 0, -200 ), 1f ) );
_tanks.Add ( new Tank (_device, @"bigship1.x",
new Vector3 ( -100, 0, -400 ), 1f ) );
}
Visual Basic
Private Sub CreateObstacles()
m_obstacles = New List(Of UnitBase)()
m_obstacles.Add(New Obstacle(m_device, "car.x", _
New Vector3(0, 0, 200), 1.0F))
m_obstacles.Add(New Obstacle(m_device, "car.x", _
New Vector3(60, 0, 100), 1.0F))
m_obstacles.Add(New Obstacle(m_device, "car.x", _
New Vector3(-60, 0, 150), 1.0F))
m_obstacles.Add(New Obstacle(m_device, "car.x", _
New Vector3(60, 0, -100), 1.0F))
m_obstacles.Add(New Obstacle(m_device, "car.x", _
New Vector3(-60, 0, -150), 1.0F))
End Sub
Private Sub CreateTanks()
m_tanks = New List(Of UnitBase)
m_tanks.Add(New Tank(m_device, "bigship1.x", _
New Vector3(0, 0, 200),1.0F))
m_tanks.Add(New Tank(m_device, "bigship1.x", _
New Vector3(100, 0, 300), 1.0F))
m_tanks.Add(New Tank(m_device, "bigship1.x", _
New Vector3(-100, 0, 500), 1.0F))
m_tanks.Add(New Tank(m_device, "bigship1.x", _
New Vector3(100, 0, -200), 1.0F))
m_tanks.Add(New Tank(m_device, "bigship1.x", _
New Vector3(-100, 0, -400), 1.0F))
End Sub
障害物、および戦車の座標を見ると、ここではわかりやすくするために、それらを同じような軸に沿って配置していることがわかります。これらのメソッドを変更して、ランダムな数の戦車、および障害物をランダムな座標に配置することもできます。ただし、オブジェクトが相互に重なり合ったり、原点に近くなりすぎ、カメラを隠したりすることを防止するロジックを追加してください。
レベル
柔軟性、および拡張性のより高いソリューションにする場合は、障害物、および戦車の個数、種類、および場所をファイルから読み取るようにします。このファイルにゲームのプレイ固有のその他の設定を含め、何らかの内部ロジックに基づいて自動的に読み込むようにすることも可能です。このようなシナリオは、レベルの事前定義に最もよく使用されます。プレイヤーは、終了基準を満たすと、より難しいレベルへと進むことができる場合があります。このシナリオにより、ランダムな配置では実現不可能な、変幻自在のプレイ体験、および各レベルに合わせて厳密に調整したプレイアビリティを簡単に作成することができます。この方法でゲームを設定すれば、プレイヤー自身がゲームをカスタマイズすることも可能になります。このような方法を採用する場合は、通常、レベル エディタを提供します。
最後のステップとして、これらのメソッドを呼び出します。これに最適な場所は、GameEngine クラスのコンストラクタの Camera クラスが作成された直後です。
Visual C#
public GameEngine ()
{
InitializeComponent ( );
this.SetStyle (ControlStyles.AllPaintingInWmPaint |
ControlStyles.Opaque, true );
ConfigureInputDevices ( );
ConfigureDevice ( );
_skyBox = new SkyBox ( this._device );
_camera = new Camera ( );
CreateObstacles ( );
CreateTanks ( );
this.Size = new Size ( 800, 600 );
HiResTimer.Start ( );
}
Visual Basic
Public Sub New()
InitializeComponent()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or _
ControlStyles.Opaque, True)
ConfigureInputDevices()
ConfigureDevice()
m_skyBox = New SkyBox(Me.m_device)
m_camera = New Camera
CreateObstacles()
CreateTanks()
Me.Size = New Size(800, 600)
HiResTimer.Start()
End Sub
各レンダリング ループで必要となることは、適切なコレクションを繰り返し、視錐台に含まれるものをレンダリングすることだけです。GameEngine クラスの Render メソッドで、スカイボックスをレンダリングした直後に RenderUnits メソッドを呼び出します。
Visual C#
private void RenderUnits ( )
{
foreach ( UnitBase ub in _obstacles )
{
ub.Render ( _camera );
}
foreach ( UnitBase ub in _tanks )
{
ub.Render ( _camera );
}
}
Visual Basic
Private Sub RenderUnits()
For Each ub As UnitBase In m_obstacles
ub.Render(m_camera)
Next
For Each ub As UnitBase In m_tanks
ub.Render(m_camera)
Next
End Sub
これで完成です。ユニットが作成されました。
まとめ
現時点でゲームを実行すると、次の 3 つのことに気付きます。
- ユニットは、空間に浮いているように見えます。
- ユニットは、すべて白色です。
- 各ユニットは、通り抜けることができます。
最初の問題は、ゲームに地形を追加し、Y 軸に沿って固体表面を表示することで解決できます。2 つ目の問題は、景色に光を追加することで解決できます。最後の問題は、衝突の検出をゲームに追加すれば解決されます。これらの 3 つの点以外にも、ゲームは、徐々にプレイできる形に近づいています。次の記事では、高さマップを使用してまだ欠けている地形を追加し、ユニットの色がわかるように光を追加します。また衝突の検出も追加します。
ゲーム内のさまざまなグラフィックは、記事ごとに変更しています (今後も、変更する予定です)。これは、独自のグラフィックを実装して、この基盤となるゲームを何かまったく異なるものに変化させることを推奨するために行っています。このゲームは、グラフィックを変更するだけで、宇宙にも、モーターボートのレースにも簡単に変化させることができます。ぜひ、お試しください。
いつものことですが、もう時間がなくなってしまいました。前回の記事では、HUD 背景を追加するとお約束していましたが、紙面が足りませんでした。これは、今後の記事で実現したいと思っています。 また、キーボードおよびマウスの入力の追跡方法を強化するアクション マッピングについても説明する予定でしたが、これも今後の記事で説明します。さらに、ゲームに単純なデバッグ コンソールを追加することも予定しています。
これらの機能を含めて、今後の記事では、敵の戦車に人工知能を追加し、ゲームがリアルな部隊となるようにします。さらに、ゲームをより楽しくする優れたサウンドも追加します。今後も、引き続き注目してください。
それまでは、コーディングを楽しんでください。