はじめに
Photon Fusionアンバサダーのニム式です。
シューター系のゲームを作る場合、ベーシックな銃弾だけではなく、追尾したり跳弾したりと色々な種類の弾を実装したくなるかと思います。
オンラインゲームの場合、オフラインと違い弾自身も一つの同期オブジェクトとなる都合上、弾の特性はその同期方法に大きく依存することになります。
以前の記事では公式のProjectilesサンプルに実装されている同期方法の解説と、簡単な改造例の紹介をしました。
本記事では、引き続きProjectilesサンプルをベースに、より特徴のある弾の実装例を紹介します。
前提記事
Photon Fusionの基本的な解説は以下の記事で行っていますので、そちらを参照下さい。
動作確認環境
Windows 11 Home 22H2
Unity 2022.3.2f1
Fusion SDK 1.1.8 F Build 725
実装例~基本性能の変更
射撃性能の変更をする簡単な方法は、武器の性能を決めるWeaponComponentか弾の性能を決めるProjectileを継承したコンポーネントの数値を調整することです。
新しい武器や弾のプレハブを作った場合、AgentプレハブのWeapons・ProjectileManagerコンポーネントに登録する必要があります。これは武器や弾の生成管理は武器ではなくプレイヤーが行うためです。
武器の性能を変更する
射撃性能のうち武器の性能を決めるコンポーネントには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
- リロード時間
弾の性能を変更する
射撃性能のうち弾の性能を決めるコンポーネントにはProjectileが継承されており、大まかにHitscan、Kinematic、Standaloneの3系統があります。
例えば本記事で取り上げるHomingProjectileでは以下のような調整ができます。
- Length
- 弾の長さ
- Min Dot
- 進行方向と誘導先ベクトルの内積の許容値
- 1なら正面のみ、-1なら真後ろまでカバー
- Predict Target Position
- 追尾対象の先読み具合
動的な変更
上記の変更はエディタ上での変更であり、ゲームとしては初期設定にあたります。ゲーム中の動的な変更については、以前の記事でマガジンの拡張を行えるように改造した例を紹介していますので、そちらを参照ください。
実装例~基本性能の拡張
次はコードに改良を加え、射撃性能にバリエーションを追加する例です。
発射体を飛ばし方を変える
まずはメイン武器としてAssets/Prefabs/Weapons/PulseGunのPrimaryActionを改造します。
連続で打ち出すバースト
バーストは一定個数の弾を連続で打ち出す方式です。
これはWeaponMagazineとWeaponTriggerコンポーネントの調整、具体的には装填数と発射レートを小さくすることで実現可能です。
同時に複数方向へ打ち出すWAY
2Dシューティングゲームによくある、一つの発射点から同一平面上の扇形に複数弾を発射する方式です。3Dシューター系の場合はショットガンのような3次元的な放射状のものが多いですが、サンプルにあるため割愛します。
WAY方式はWeaponBarrelコンポーネントを改良し、発射角angleSpread
を同時発射数_projectilesPerShot
で割って等間隔に発射するようにします。
[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;
}
}
//略
}
}
垂直ミサイルを作る
次はロボットゲームでよく見る垂直に発射し少し時間をおいて誘導を開始、上空から降り注ぐタイプの武器、通称垂直ミサイルです。
バーストにする
これは先程と同じ内容になります。
上に打ち出す
デフォルトでは構えた武器からレティクルの方向に発射するようになっているため、上方に打ち出せるように武器オブジェクトを改造します。
- 位置を調整する
上方に打ち出せるような見た目を作るため、3Dオブジェクトを追加します。今回は元のオブジェクトを複製して使用します。
BarrelTransformは位置に加えこのX軸方向が弾の発射基準になるため重要になっています。
- 向きを調整
向きはWeaponBarrelコンポーネントで決定していますが、デフォルトでは射出方向を示すcontext.FireDirection
は発射地点からレティクルへ向かっています。
WeaponBarrelコンポーネントを改良し、射出方向をバレルの向きに更新できるようなオプションを追加します。
なおBarrelTransform
は同一オブジェクトについているWeaonActionコンポーネントで設定します
[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;
//略
}
誘導性能の強化
誘導性能は武器ではなく弾オブジェクトが決定します。今回はAssets/Prefabs/Projectiles/HomingRocketプレハブのバリエーションとして作るためコピーして改造していきます。
HomingProjectileに誘導を開始するタイミングを変える仕組みはないため、弾が垂直で上っている間は誘導しないよう誘導開始までのディレイを設定できるように改造します。
それ以外についてはHomingProjectileコンポーネントに元からある設定を調整します。
- Max Seek Distance
- 誘導範囲を広くする
- Turn Speed
- 回頭速度を上げる
- Min Dot
- 回頭範囲を決める
- 進行方向と誘導先ベクトルの内積の許容値
- 1なら正面のみ、-1なら真後ろまでカバー
[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);
}
}