Cafe capybara TECH-BLOG

PHPな会社でゴリゴリしてるあかいいぬの技術ブログです

FPSゲームの作り方 2.プレイヤーと敵キャラクターを作る

銃を撃つ前に、自分と敵がいないとダメでした。ということで先にキャラクタを作成します。

今回は、敵キャラクタは最もシンプルに「その場で動かずに、視線に入った自キャラを攻撃する」というAIを実装することにします。

抽象クラスを継承する

このタイトルだけ見てもなんのことやらさっぱりですね。

「敵キャラクター」と「自キャラクター」には、共通点があります。

キャラクタのモデルがあって、銃を持っていて、どちらかの方向を向いていて...のような所が同じです。

同じものを持っているのに、全く別々の箱を作ってしまうと、その同一部分に編集を加えようとする時、その両方を変えないといけません。

さらに、「味方キャラクター」のように似たようなものを追加する時もソースコードをコピーしてとさらにめんどくさくなります。

そういう時に使えるのが、オブジェクト指向の特徴でもある「継承」です。

色々な言語解説のサイトでは、継承の書き方は書いてありますが、実際に使う時はどうすればいいのかよくわからなかったりしますので、ここでは実際のコードを書いてみます。

(Character.cs)
	/// <summary>
	/// キャラクタの抽象クラス
	/// </summary>
	public abstract class Character
	{
		/// <summary>
		/// キャラクタのモデル
		/// </summary>
		protected Model ModelData
		{ get; set; }

		/// <summary>
		/// キャラクタの色
		/// </summary>
		protected Color Diffuse
		{ get; set; }

		/// <summary>
		/// キャラクタの座標
		/// </summary>
		protected Vector3 Position
		{ get; set; }

		/// <summary>
		/// キャラクタの横回転
		/// </summary>
		protected float Yaw
		{ get; set; }

		/// <summary>
		/// キャラクタの縦回転
		/// </summary>
		protected float Pitch
		{ get; set; }

		public Character(Model modelData)
		{
			ModelData = modelData;
			Diffuse = Color.White;
		}

		/// <summary>
		/// 更新処理を書くメソッド
		/// (書かないとエラーが出る)
		/// </summary>
		public abstract void Update(GameTime gameTime);
		/// <summary>
		/// 描画処理を書くメソッド
		/// (書かなければ下の処理だけ行われる)
		/// </summary>
		/// <param name="view">カメラのビュー行列</param>
		/// <param name="projection">カメラの射影行列</param>
		public virtual void Draw(GameTime gameTime, Matrix view, Matrix projection)
		{
			// モデルデータを描画
			foreach (var mesh in ModelData.Meshes)
			{
				foreach (BasicEffect e in mesh.Effects)
				{
					// モデルを回転してから移動
					e.World = Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0) *
						Matrix.CreateTranslation(Position);
					// モデルに色付け
					e.DiffuseColor = Diffuse.ToVector3();
					e.View = view;
					e.Projection = projection;
					e.EnableDefaultLighting();
				}
				mesh.Draw();
			}
		}
	}

これが抽象クラスです。一番最初に「public abstract class Character」という宣言部分がありますが、この「abstract」というものが抽象クラスを表します。

別にこれを書かなくても継承は出来ます。これは「継承しないとこのクラスは使えない」という意味になります。

モデル、色、場所、縦回転と横回転、それとモデルの描画処理を加えました。では、継承したクラスも見てみましょう。

(MyCharacter.cs)
	/// <summary>
	/// 自キャラクター
	/// </summary>
	public class MyCharacter : Character
	{
		public MyCharacter(Model modelData)
			: base(modelData)
		{
			Diffuse = Color.Blue;
		}

		public override void Update(GameTime gameTime)
		{
			// キャラクタを移動させる
			Yaw += 0.02f;
			Position += Vector3.TransformNormal(Vector3.Forward,
				Matrix.CreateRotationY(Yaw));
		}
	}

(EnemyCharacter.cs)
	/// <summary>
	/// 敵キャラクター
	/// </summary>
	public class EnemyCharacter : Character
	{
		public EnemyCharacter(Model modelData, Random r)
			: base(modelData)
		{
			Diffuse = Color.Red;
			
			// 場所をランダムに
			Position = new Vector3(
				r.Next(-512, 512),
				0,
				r.Next(-512, 512));
		}

		public override void Update(GameTime gameTime)
		{
			// 回転
			Yaw -= 0.05f;
		}
	}

クラス宣言時に、後ろにコロン+継承するクラス名を書けば実装できます。

初期化時点で色を変え、敵キャラクターは場所をランダムで配置し、更新部分で異なる処理をしています。

このような動きになる予定です。メインクラスをいじっていないので、現段階ではまだ何も表示されません。

キャラクタのモデルはメタセコイアで簡単に作って、ひげねこさんのメタセコイアプラグイン(
メタセコイアのモデルファイルをXNAで直接読み込む「メタセコイア・パイプライン」 - ひにけにXNA - Site Home - MSDN Blogs
)を使わせてもらって読み込みました。

f:id:expert88:20120401051553j:plain

インターフェースで自キャラでの視点とフリーの視点を導入する

さて、青い自分のキャラクターが、勝手に動いています。もはや自分じゃありません

視点とキャラクタモデルが同期されていないのが問題ですね。

しかし自由に動けなくなるのも面倒なので、両方を行き来出来るようなシステムにしましょう。

ここでは、「カメラを移動させることが出来る」という機能を持つ、と明示させる「インターフェース」を作ります。

インターフェースを使うと、特定のメソッド・プロパティを持つクラスや構造体を表現することが出来るようになります。

実際私もこのあたりは微妙なのですが...。

(CanOperateCamera.cs)
	public interface CanOperateCamera
	{
		void OperateCamera(Camera camera);
	}

これがインターフェースです。超シンプルですね。

Cameraというクラスを作って、それを引数に使いました。このクラスも実装してみます。

(Camera.cs)
	/// <summary>
	/// カメラ情報を保持するクラス
	/// </summary>
	public class Camera
	{
		public Matrix View
		{ get; set; }

		public Matrix Projection
		{ get; set; }

		public Viewport Viewport
		{ get; set; }

		public Camera(Matrix projection, Viewport viewport)
		{
			Projection = projection;
			Viewport = viewport;
		}
	}

ビュー行列と射影行列を保持するだけの簡単なクラスです。射影行列は通常ゲーム中に変化しないので、宣言時に設定してしまいます。

また、Viewportクラスでマウスの移動等を行うので、これも保持しておきます。

さて、先程作ったインターフェースを使って、カメラ移動のメソッドを作ります。

まずは、今までのフリー移動のカメラから。新しいクラスを作ります。

(FreeCamera.cs)
	public class FreeCamera : CanOperateCamera
	{
		float _yaw;
		float _pitch;
		Vector3 _position;

		public void OperateCamera(Camera camera)
		{
			// マウスが画面中央からどれだけ移動したかを取得し、値を変化させる
			// YawはY軸を中心とした横回転
			_yaw -= (Mouse.GetState().X - camera.Viewport.Width / 2) / 1000f;
			// PitchはX軸を中心とした縦回転
			_pitch -= (Mouse.GetState().Y - camera.Viewport.Height / 2) / 1000f;
			// Pitchに上下の制限をかける
			_pitch = MathHelper.Clamp(_pitch,
				-MathHelper.PiOver2 + 0.01f, MathHelper.PiOver2 - 0.01f);

			// マウスを画面の中心に置く
			Mouse.SetPosition(
				camera.Viewport.Width / 2,
				camera.Viewport.Height / 2);
			// YawとPitchから前方の座標と左側、上側の座標を取得
			Vector3 forward =
				Vector3.TransformNormal(Vector3.Forward,
				Matrix.CreateFromYawPitchRoll(_yaw, _pitch, 0));
			Vector3 left =
				Vector3.TransformNormal(Vector3.Left,
				Matrix.CreateFromYawPitchRoll(_yaw, _pitch, 0));
			Vector3 up =
				Vector3.TransformNormal(Vector3.Up,
				Matrix.CreateFromYawPitchRoll(_yaw, _pitch, 0));

			// キー入力情報を取得
			KeyboardState key = Keyboard.GetState();
			if (key.IsKeyDown(Keys.W))
				_position += forward;
			if (key.IsKeyDown(Keys.S))
				_position -= forward;
			if (key.IsKeyDown(Keys.A))
				_position += left;
			if (key.IsKeyDown(Keys.D))
				_position -= left;

			// ビュー行列を作成
			camera.View = Matrix.CreateLookAt(_position, _position + forward, up);
		}
	}

インターフェースも継承と同じように、コロンの後に書いて実装します。

これは以前Updateメソッド内に書いたものとほぼ同一です。Pitch制限の追加と射影行列の設定の削除を行いました。

自キャラクターの方にも実装します。

(MyCharacter.cs)
	/// <summary>
	/// 自キャラクター
	/// </summary>
	public class MyCharacter : Character, CanOperateCamera
	{
		public MyCharacter(Model modelData)
			: base(modelData)
		{
			Diffuse = Color.Blue;
		}

		public override void Update(GameTime gameTime)
		{ }

		public void OperateCamera(Camera camera)
		{
			// マウスが画面中央からどれだけ移動したかを取得し、値を変化させる
			// YawはY軸を中心とした横回転
			Yaw -= (Mouse.GetState().X - camera.Viewport.Width / 2) / 1000f;
			// PitchはX軸を中心とした縦回転
			Pitch -= (Mouse.GetState().Y - camera.Viewport.Height / 2) / 1000f;
			// Pitchに上下の制限をかける
			Pitch = MathHelper.Clamp(Pitch,
				-MathHelper.PiOver2 + 0.01f, MathHelper.PiOver2 - 0.01f);

			// マウスを画面の中心に置く
			Mouse.SetPosition(
				camera.Viewport.Width / 2,
				camera.Viewport.Height / 2);
			// YawとPitchから前方の座標と左側、上側の座標を取得
			Vector3 forward =
				Vector3.TransformNormal(Vector3.Forward,
				Matrix.CreateFromYawPitchRoll(Yaw, 0, 0)); // Pitchを使わない
			Vector3 lookat =
				Vector3.TransformNormal(Vector3.Forward,
				Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0));
			Vector3 left =
				Vector3.TransformNormal(Vector3.Left,
				Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0));
			Vector3 up =
				Vector3.TransformNormal(Vector3.Up,
				Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0));

			// キー入力情報を取得
			KeyboardState key = Keyboard.GetState();
			if (key.IsKeyDown(Keys.W))
				Position += forward;
			if (key.IsKeyDown(Keys.S))
				Position -= forward;
			if (key.IsKeyDown(Keys.A))
				Position += left;
			if (key.IsKeyDown(Keys.D))
				Position -= left;

			// ビュー行列を作成
			camera.View = Matrix.CreateLookAt(Position, Position + lookat, up);
		}
	}

宣言の部分にCanOperateCameraを追加。2つ以上の継承項目がある場合はカンマで区切ります

※インターフェースは何個でも継承できますが、抽象クラスは1つしか継承出来ません。ので、抽象クラスを継承する場合はコロンの後一番最初に書く必要があります

forward変数(前方の向きベクトル)を算出する際にPitch(縦回転)を使わずに計算します。

これにより、上下の移動ができなくなるので、地面に沿った移動をするようになります。

ビュー行列には、フリー視点と同じように向いている視点を渡します。

後、Updateの中身を消しました。これで勝手にモデルが移動することがなくなります。

メインのクラスを更新する

準備が整ったので、メインのクラスに手を加えてカメラを変化させられるようにします。

(Game1.cs)
	public class Game1 : Microsoft.Xna.Framework.Game
	{
		GraphicsDeviceManager graphics;

		BasicEffect _basicEffect;
		VertexPositionColor[] _ground;

		// カメラを操作出来るクラスの一覧
		List<CanOperateCamera> _cameraOperator;
		Camera _camera;
		// 現在操作中のカメラ操作クラス番号
		int _cameraOperatorNumber;

		// キャラクターの一覧
		List<Character> _characters;

		public Game1()
		{
			graphics = new GraphicsDeviceManager(this);
			Content.RootDirectory = "Content";
		}

		protected override void LoadContent()
		{
			// エフェクトの作成
			_basicEffect = new BasicEffect(GraphicsDevice);
			_basicEffect.VertexColorEnabled = true;
			_basicEffect.TextureEnabled = false;
			_basicEffect.LightingEnabled = false;

			// 地面のポリゴン作成
			_ground = new VertexPositionColor[] {
				new VertexPositionColor(new Vector3(-512, -10, -512), Color.White),
				new VertexPositionColor(new Vector3( 512, -10, -512), Color.Red),
				new VertexPositionColor(new Vector3(-512, -10,  512), Color.Green),
				new VertexPositionColor(new Vector3( 512, -10,  512), Color.Blue),
			};

			// カメラ操作クラス一覧を作成
			_cameraOperator = new List<CanOperateCamera>();

			// カメラを作成して射影行列を設定
			_camera = new Camera(Matrix.CreatePerspectiveFieldOfView(
				MathHelper.PiOver2, GraphicsDevice.Viewport.AspectRatio, 1, 10000),
				GraphicsDevice.Viewport);

			// フリーカメラをカメラ操作クラス一覧に追加
			_cameraOperator.Add(new FreeCamera());

			// キャラクター一覧を作成
			_characters = new List<Character>();

			// 自キャラクラスを作成
			MyCharacter myChar = new MyCharacter(Content.Load<Model>("CharCube"));

			// 作った自キャラクラスをカメラ操作クラス一覧に追加
			_cameraOperator.Add(myChar);

			// 作った自キャラクラスをキャラクター一覧に追加
			_characters.Add(myChar);

			// 敵を10体キャラクター一覧に追加
			Random r = new Random();
			for (int i = 0; i < 10; i++)
				_characters.Add(new EnemyCharacter(Content.Load<Model>("CharCube"), r));
		}

		protected override void Update(GameTime gameTime)
		{
			// Escapeキーで終了
			if (Keyboard.GetState().IsKeyDown(Keys.Escape))
				this.Exit();

			// カメラ操作クラスを変更
			if (Keyboard.GetState().IsKeyDown(Keys.D1))
				_cameraOperatorNumber = 0;
			else if (Keyboard.GetState().IsKeyDown(Keys.D2))
				_cameraOperatorNumber = 1;

			// カメラを操作する
			_cameraOperator[_cameraOperatorNumber].OperateCamera(_camera);

			// キャラクタを更新する
			foreach (var character in _characters)
				character.Update(gameTime);

			base.Update(gameTime);
		}

		protected override void Draw(GameTime gameTime)
		{
			GraphicsDevice.Clear(Color.CornflowerBlue);

			_basicEffect.View = _camera.View;
			_basicEffect.Projection = _camera.Projection;
			_basicEffect.Techniques[0].Passes[0].Apply();

			GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
				PrimitiveType.TriangleStrip,
				_ground, 0, 2);

			// キャラクターを描画する
			foreach (var character in _characters)
				character.Draw(gameTime, _camera.View, _camera.Projection);
			
			base.Draw(gameTime);
		}
	}

List<CanOperateCamera> _cameraOperatorにカメラを操作出来るクラスを追加してリストにしています。

1キーを押すとフリー視点に、2キーを押すとキャラクタ視点になります。

自キャラと敵キャラ(10体)は、全てList<Character> _charactersのリストの中に入り、foreachで更新、描画されます。

で、こんな感じになると思います。ついでにEscapeキーで終了するのも加えました。(地面のサイズも1024x1024にアップ)


さて、3D空間に住人が住みつき、ようやくゲームの片鱗が見えてきました。次こそ、銃を撃てるようにしたいと思います。


ゲーム作成が初めての方は、言語に関する書き方や各用語の意味など、端折りすぎてわからない部分が多いと思います。コメントしていただくかTwitter @akai_inu にリプライを頂ければ対応いたしますので、なんでも質問してください。