Cafe capybara TECH-BLOG

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

FPSゲームの作り方 3.銃を撃つ

キャラクターも出てきたところで今回は、シューティングの肝である、「銃を撃つ」というアクションを実装します。

まだゲーム性(勝ったり負けたりがある)は出てきませんが、FPSっぽくはなります!

銃弾を実装する

左クリックをすると、視点の中心から向いている方向に銃弾が発射されるという流れです。

まず最初に、銃弾のクラスを実装します。

	/// <summary>
	/// 銃弾を表すクラス
	/// </summary>
	public class Bullet
	{
		/// <summary>
		/// 銃弾の速度
		/// </summary>
		public static readonly float Speed = 10f;

		/// <summary>
		/// 発射位置
		/// </summary>
		public Vector3 BasePosition
		{ get; private set; }

		/// <summary>
		/// 発射方向
		/// </summary>
		public Vector3 Direction
		{ get; private set; }

		/// <summary>
		/// 発射したキャラクタ
		/// </summary>
		public Character Shooter
		{ get; private set; }

		private float _distance;
		private float _distance_prev;

		public Bullet(Vector3 basePosition, Vector3 direction, Character shooter)
		{
			BasePosition = basePosition;
			Direction = direction;
			Shooter = shooter;
		}

		/// <summary>
		/// 銃弾の場所を更新する。
		/// trueを返した場合、その銃弾は削除される。
		/// </summary>
		public bool Update(GameTime gameTime)
		{
			_distance_prev = _distance;
			_distance += Speed;

			return _distance > 10000;
		}
		public void Draw(GraphicsDevice device)
		{
			var vertices = new VertexPositionColor[] {
					new VertexPositionColor(BasePosition + Direction * _distance_prev, Color.Yellow),
					new VertexPositionColor(BasePosition + Direction * _distance, Color.Yellow)
			};
			device.DrawUserPrimitives<VertexPositionColor>(
				PrimitiveType.LineList, vertices, 0, 1);
		}
	}

これ単体は1つの銃弾を表します。

敵の銃弾も自分の銃弾も、同じこのクラスを使います。

shooterというのは、誰が撃ったかを表すもので、これを使ってダメージの有無を判断します。

さらに、この銃弾を管理するクラスを1つ用意します。

	/// <summary>
	/// 銃弾を管理するクラス
	/// </summary>
	public class BulletManager : DrawableGameComponent
	{
		private Camera _camera;
		private BasicEffect _basicEffect;
		private List<Bullet> _bulletList;

		public BulletManager(Game game, Camera camera)
			:base(game)
		{
			_camera = camera;
			_bulletList = new List<Bullet>();

			// ゲームのコンポーネントに登録する
			game.Components.Add(this);
			game.Services.AddService(typeof(BulletManager), this);
		}

		public override void Initialize()
		{
			base.Initialize();

			// ゲームコンポーネント内でGraphicsDeviceを使用する際はbase.Initialize()の後で
			_basicEffect = new BasicEffect(GraphicsDevice);
			_basicEffect.VertexColorEnabled = true;
		}

		/// <summary>
		/// 銃弾を発射する
		/// </summary>
		public void Shoot(Vector3 basePosition, Vector3 direction, Character shooter)
		{
			_bulletList.Add(new Bullet(basePosition, direction, shooter));
		}

		public override void Update(GameTime gameTime)
		{
			for (int i = 0; i < _bulletList.Count; i++)
			{
				// trueなら削除する
				if (_bulletList[i].Update(gameTime))
				{
					_bulletList.RemoveAt(i);
					i--;
				}
			}

			base.Update(gameTime);
		}

		public override void Draw(GameTime gameTime)
		{
			_basicEffect.View = _camera.View;
			_basicEffect.Projection = _camera.Projection;
			_basicEffect.CurrentTechnique.Passes[0].Apply();

			foreach (var b in _bulletList)
			{
				b.Draw(GraphicsDevice);
			}

			base.Update(gameTime);
		}
	}

今回は「DrawableGameComponent」というXNAの機能を使いました。

この抽象クラスを継承すると、メインクラス(Gameクラス)と同じメソッドの動きをし、ゲームのコンポーネントとして登録することで、自動でUpdate、Drawなどおこなってくれます。
(正確には、Gameクラスのbase.Update(),base.Draw()などの部分で行われています)

銃弾を発射する際はShootメソッドを呼び出すだけですね。

Character抽象クラスがこのBulletManagerを保持するようにちょっといじります。

		/// <summary>
		/// 銃弾管理クラス
		/// </summary>
		protected BulletManager BulletManager
		{ get; set; }

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

コンストラクタの部分に変数の宣言と、引数からの代入を加えました。

		public MyCharacter(Model modelData, BulletManager bulletManager)
			: base(modelData, bulletManager)

		public EnemyCharacter(Model modelData, BulletManager bulletManager, Random r)
			: base(modelData, bulletManager)

MyCharacterとEnemyCharacterの宣言部分もこのように書き加えます。

さらにMyCharacterのUpdateメソッドに銃を撃つ処理を加えます。

		public override void Update(GameTime gameTime)
		{
			// 左クリックしてたら弾を撃つ
			if (Mouse.GetState().LeftButton == ButtonState.Pressed)
			{
				Vector3 dir = Vector3.TransformNormal(Vector3.Forward,
					Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0));
				BulletManager.Shoot(Position, dir, this);
			}
		}

BulletManagerをメインのクラスに追加します。

	public class Game1 : Microsoft.Xna.Framework.Game
	{
		// ~中略~

		BulletManager _bulletManager;

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

		protected override void Initialize()
		{
			// エフェクトの作成
			_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());

			// 銃弾管理クラスを作成
			_bulletManager = new BulletManager(this, _camera);

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

			base.Initialize();
		}

		protected override void LoadContent()
		{
			// 自キャラクラスを作成
			MyCharacter myChar = new MyCharacter(Content.Load<Model>("CharCube"), _bulletManager);
			_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"), _bulletManager, r));
		}

初期化を全てLoadContentメソッド内で行なっていましたが、ちゃんとInitializeとLoadContentに分けました。

LoadContentではContentを利用するものを、Initializeではそれ以外を初期化するためにあります。

Initializeメソッド内でContentを使おうとすると、エラーが起きます(base.Initialize()後ならいいですが...)。

現時点でこんな感じになりました。

あれ...なんかレーザービーム(CV.柳生比呂士)みたいになってますね。弾速が遅いのもありますが、とにかく絶えず銃弾が出ているのでこんなことになっているようです。

間隔を空けて撃つ

ある程度間隔を空けて撃つようにしましょう。これはMyCharacterをいじるだけで出来ます。

		private readonly float _shootInterval = 80;
		private float _shootIntervalNow;

		public MyCharacter(Model modelData, BulletManager bulletManager)
			: base(modelData, bulletManager)
		{
			Diffuse = Color.Blue;
		}

		public override void Update(GameTime gameTime)
		{
			// 左クリックしてたら弾を撃つ
			if (Mouse.GetState().LeftButton == ButtonState.Pressed)
			{
				_shootIntervalNow += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

				if (_shootIntervalNow > _shootInterval)
				{
					Vector3 dir = Vector3.TransformNormal(Vector3.Forward,
						Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0));
					BulletManager.Shoot(Position, dir, this);
					_shootIntervalNow = 0;
				}
			}
			else _shootIntervalNow = 1000;
		}

float _shootIntervalNowという変数を用意して、クリックしている間だけ、フレーム分の経過時間を追加します。

ある程度まで時間が経過したら実際に発射し、数値を0に戻す。これを繰り返すだけです。

左クリックしていない間は数値を大きくしておきます。これで一発目がクリックした瞬間に撃たれるようになります。

だいぶ銃っぽくなってきました。

第四回では、ついに「当たり判定」と戦ってみたいと思います。

自分の撃った弾が敵に当たると、敵を倒せる。これだけでもうゲームですね。