Photon Fusion for UnityのProjectilesサンプルを改造して弾のバリエーションを増やす

# はじめに

Photon Fusionアンバサダーのニム式です。

シューター系のゲームを作る場合、ベーシックな銃弾だけではなく、追尾したり跳弾したりと色々な種類の弾を実装したくなるかと思います。

オンラインゲームの場合、オフラインと違い**弾自身も一つの同期オブジェクトとなる**都合上、弾の特性はその同期方法に大きく依存することになります。

以前の記事では公式のProjectilesサンプルに実装されている同期方法の解説と、簡単な改造例の紹介をしました。

https://qiita.com/nimushiki/items/8f51d5e20038b2329b26

本記事では、引き続きProjectilesサンプルをベースに、より**特徴のある弾の実装例を紹介**します。

### 前提記事

Photon Fusionの基本的な解説は以下の記事で行っていますので、そちらを参照下さい。

https://qiita.com/nimushiki/items/d6fe4f0cbe0437677fa8

### 動作確認環境

Windows 11 Home 22H2
Unity 2022.3.2f1
Fusion SDK 1.1.8 F Build 725

# 実装例～基本性能の変更

射撃性能の変更をする簡単な方法は、武器の性能を決めるWeaponComponentか弾の性能を決めるProjectileを継承したコンポーネントの数値を調整することです。

新しい武器や弾のプレハブを作った場合、AgentプレハブのWeapons・ProjectileManagerコンポーネントに登録する必要があります。これは武器や弾の生成管理は武器ではなくプレイヤーが行うためです。

![武器や弾の追加方法](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/7a4abbe3-fc4e-076b-11e1-ade02f9ad792.png)

## 武器の性能を変更する

射撃性能のうち**武器の性能を決めるコンポーネントにはWeaponComponentが継承**されており、WeaponBarrel、WeaponBeam、WeaponMagazine、WeaponTriggerの4つが実装されています。

例えば本記事の後半ではPulseGunを取り上げますが、PrimaryActionオブジェクトのWeaponMagazineでは以下の性能が調整できます。

- Initial Ammo
    - 初期装填数
- Has Magazine
    - マガジン単位で弾の管理をするか
- Max Magazine Ammo
    - リロード時の追加弾数
- Max Weapon Ammo
    - 武器に保持できる弾数
- Has Unlimited Ammo
    - Has Magazineがfalseなら無限に撃てる
- Reload Time
    - リロード時間

![WeaponMagazineコンポーネント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/07e6fc7e-9ed4-dc41-122e-f8130ec6ae20.png)

## 弾の性能を変更する

射撃性能のうち**弾の性能を決めるコンポーネントにはProjectileが継承**されており、大まかにHitscan、Kinematic、Standaloneの3系統があります。

例えば本記事で取り上げるHomingProjectileでは以下のような調整ができます。

- Length
    - 弾の長さ
- Min Dot
    - 進行方向と誘導先ベクトルの内積の許容値
    - 1なら正面のみ、-1なら真後ろまでカバー
- Predict Target Position
    - 追尾対象の先読み具合

![HomingProjectileコンポーネント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d11b43a1-6708-3a51-9d84-f6451553f1b7.png)

## 動的な変更

上記の変更はエディタ上での変更であり、ゲームとしては初期設定にあたります。ゲーム中の動的な変更については、以前の記事でマガジンの拡張を行えるように改造した例を紹介していますので、そちらを参照ください。

https://qiita.com/nimushiki/items/8f51d5e20038b2329b26

# 実装例～基本性能の拡張

次はコードに改良を加え、射撃性能にバリエーションを追加する例です。

## 発射体を飛ばし方を変える

まずはメイン武器としてAssets/Prefabs/Weapons/PulseGunのPrimaryActionを改造します。

### 連続で打ち出すバースト

バーストは一定個数の弾を連続で打ち出す方式です。

これはWeaponMagazineとWeaponTriggerコンポーネントの調整、具体的には**装填数と発射レートを小さくする**ことで実現可能です。

![WeaponMagazineコンポーネント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/25ec8abc-8d4d-c2f0-aa58-876ca3ba6b8f.png)

![WeaponTriggerコンポーネント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d9c24ce7-3592-dd7b-082e-3b9c355188c9.png)
  
![バースト発射デモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/9ed1a7d0-740c-396f-dfd0-2418f656986c.gif)


### 同時に複数方向へ打ち出すWAY

2Dシューティングゲームによくある、一つの発射点から同一平面上の扇形に複数弾を発射する方式です。3Dシューター系の場合はショットガンのような3次元的な放射状のものが多いですが、サンプルにあるため割愛します。

WAY方式はWeaponBarrelコンポーネントを改良し、**発射角`angleSpread`を同時発射数`_projectilesPerShot`で割って等間隔に発射**するようにします。

```WeaponBarrel.cs

[SerializeField]
private bool _spread = false;
[SerializeField]
private float angleSpread;

public override void OnFixedUpdate(WeaponContext context, WeaponDesires desires)
{
	//略
	
	for (int i = 0; i < _projectilesPerShot; i++)
	{
		var projectileDirection = direction;

		if (_spread)
		{
			var unit = angleSpread / ( _projectilesPerShot + 1 );
			projectileDirection = Quaternion.Euler(0f,(unit * (i + 1) - angleSpread / 2f), 0f) * direction;
		}
		else
		{
			if (_dispersion > 0f)
			{
				// We use sphere on purpose -> non-uniform distribution (more projectiles in the center)
				var randomDispersion = Random.insideUnitSphere * _dispersion;
				projectileDirection = Quaternion.Euler(randomDispersion.x, randomDispersion.y, randomDispersion.z) * direction;
			}
		}

		//略
	}
}
```

![way発射デモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/a6e6eeb5-3ec2-d020-c85c-e6d77b0e19db.gif)


## 垂直ミサイルを作る

次はロボットゲームでよく見る垂直に発射し少し時間をおいて誘導を開始、上空から降り注ぐタイプの武器、通称**垂直ミサイル**です。

![垂直ミサイル発射デモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/89aab701-1b86-f329-4722-1adf366193ba.gif)

### バーストにする

これは先程と同じ内容になります。

![2023-10-19 10.47.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/38feafa7-0be5-94c4-4b04-879944228ff7.png)

### 上に打ち出す

デフォルトでは構えた武器からレティクルの方向に発射するようになっているため、上方に打ち出せるように武器オブジェクトを改造します。

- 位置を調整する

上方に打ち出せるような見た目を作るため、3Dオブジェクトを追加します。今回は元のオブジェクトを複製して使用します。

BarrelTransformは位置に加えこの**X軸方向が弾の発射基準になる**ため重要になっています。

![発射位置調整のための改造](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d4735df2-73a0-532a-f230-9d40c1c091bf.png)

- 向きを調整

向きはWeaponBarrelコンポーネントで決定していますが、デフォルトでは射出方向を示す`context.FireDirection`は発射地点からレティクルへ向かっています。

WeaponBarrelコンポーネントを改良し、**射出方向をバレルの向きに更新**できるようなオプションを追加します。

なお`BarrelTransform`は同一オブジェクトについているWeaonActionコンポーネントで設定します

```WeaponBarrel.cs

[SerializeField]
private bool _projectileFollowsBarrel = false;

public override void OnFixedUpdate(WeaponContext context, WeaponDesires desires)
{
	if (desires.HasFired == false)
		return;

	if (_dispersion > 0f)
	{
		Random.InitState(Runner.Tick * unchecked((int)Object.Id.Raw));
	}
	
	var direction = context.FireDirection;
	if (_projectileFollowsBarrel) direction = BarrelTransform.forward;

	//略
}
```

![改造後の設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/f75c38f5-aae1-40ae-9be0-c567d4f6e6de.png)

### 誘導性能の強化

誘導性能は武器ではなく弾オブジェクトが決定します。今回はAssets/Prefabs/Projectiles/HomingRocketプレハブのバリエーションとして作るためコピーして改造していきます。

HomingProjectileに誘導を開始するタイミングを変える仕組みはないため、弾が垂直で上っている間は誘導しないよう**誘導開始までのディレイを設定できるように改造**します。

それ以外についてはHomingProjectileコンポーネントに元からある設定を調整します。

- Max Seek Distance
    - 誘導範囲を広くする
- Turn Speed
    - 回頭速度を上げる
- Min Dot
    - 回頭範囲を決める
    - 進行方向と誘導先ベクトルの内積の許容値
    - 1なら正面のみ、-1なら真後ろまでカバー

![HomingProjectileコンポーネント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/d11b43a1-6708-3a51-9d84-f6451553f1b7.png)

```HomingProjectile.cs

[SerializeField]
private float _recalculateTargetDelay = 0f; //誘導開始までのディレイを設定する

public override ProjectileData GetFireData(NetworkRunner runner, Vector3 firePosition, Vector3 fireDirection)
{
	var data = base.GetFireData(runner, firePosition, fireDirection);

	//初動のターゲッティングを無効
	if(_recalculateTargetDelay <= 0f) data.Homing.Target = FindTarget(runner, firePosition, fireDirection); 
	data.Homing.Direction = fireDirection;
	data.Homing.Position = firePosition;

	return data;
}

private void TryRecalculateTarget(NetworkRunner runner, ref ProjectileData data, Vector3 position, Vector3 direction)
{
	if (_recalculateTargetAfterTime <= 0f)
		return;
	
	int recalculateTicks = Mathf.RoundToInt(_recalculateTargetAfterTime * runner.Simulation.Config.TickRate);
	//ディレイの設定を考慮するように修正
	int elapsedTicks = runner.Tick - data.FireTick - Mathf.RoundToInt(_recalculateTargetDelay * runner.Simulation.Config.TickRate);
	if (elapsedTicks < 0) return;
	
	if (elapsedTicks % recalculateTicks == 0)
	{
		data.Homing.Target = FindTarget(runner, position, direction);
	}
}
```