You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
7.7 KiB
C#
254 lines
7.7 KiB
C#
using Fusion;
|
|
using UnityEngine;
|
|
|
|
namespace Projectiles
|
|
{
|
|
/// <summary>
|
|
/// Projectile that can move towards its target. It supports more advanced features like target movement prediction
|
|
/// and targeting target's ground position instead of body (rockets should try to hit ground beneath the target).
|
|
/// HomingProjectile needs to update its position in KinematicData and is therefore
|
|
/// the least network efficient projectile type in the sample.
|
|
/// </summary>
|
|
public class HomingProjectile : KinematicProjectile
|
|
{
|
|
// PRIVATE MEMBERS
|
|
|
|
[SerializeField]
|
|
private float _damage = 10f;
|
|
[SerializeField]
|
|
private EHomingPosition _homingPosition = EHomingPosition.Body;
|
|
[SerializeField]
|
|
private EHitType _hitType = EHitType.Projectile;
|
|
[SerializeField]
|
|
private LayerMask _hitMask;
|
|
[SerializeField]
|
|
private LayerMask _environmentCheckMask;
|
|
[SerializeField]
|
|
private float _maxSeekDistance = 50f;
|
|
[SerializeField, Tooltip("Specifies max angle between projectile forward and direction to target. "
|
|
+ "If exceeded, projectile will continue forward or look for other targets.")]
|
|
private float _maxAngleToTarget = 25f;
|
|
[SerializeField]
|
|
private float _distanceWeight = 1f;
|
|
[SerializeField]
|
|
private float _angleWeight = 1f;
|
|
[SerializeField]
|
|
private float _turnSpeed = 8f;
|
|
[SerializeField, Tooltip("0 = Never recalculate")]
|
|
private float _recalculateTargetAfterTime = 0f;
|
|
[SerializeField, Range(0f, 1f)]
|
|
private float _predictTargetPosition = 0f;
|
|
|
|
// KinematicProjectile INTERFACE
|
|
|
|
public override KinematicData GetFireData(Vector3 firePosition, Vector3 fireDirection)
|
|
{
|
|
var data = base.GetFireData(firePosition, fireDirection);
|
|
|
|
data.Velocity = fireDirection; // Homing projectiles use Velocity as direction
|
|
data.Homing.Target = FindTarget(firePosition, fireDirection);
|
|
|
|
return data;
|
|
}
|
|
|
|
public override void OnFixedUpdate(ref KinematicData data)
|
|
{
|
|
var previousPosition = data.Position;
|
|
var nextPosition = data.Position + (Vector3)data.Velocity * _startSpeed * Context.Runner.DeltaTime;
|
|
|
|
var direction = nextPosition - previousPosition;
|
|
float distance = direction.magnitude;
|
|
|
|
// Normalize
|
|
direction /= distance;
|
|
|
|
if (ProjectileUtility.ProjectileCast(Context.Runner, Context.Owner, previousPosition, direction, distance, _hitMask, out LagCompensatedHit hit) == true)
|
|
{
|
|
HitUtility.ProcessHit(Context.Owner, direction, hit, _damage, _hitType);
|
|
|
|
data.Position = hit.Point;
|
|
data.ImpactPosition = hit.Point;
|
|
data.ImpactNormal = (hit.Normal - direction) * 0.5f;
|
|
data.IsFinished = true;
|
|
|
|
SpawnImpact(data.ImpactPosition, data.ImpactNormal);
|
|
}
|
|
else
|
|
{
|
|
TryRecalculateTarget(ref data, nextPosition, direction);
|
|
UpdateDirection(ref data);
|
|
|
|
data.Position = nextPosition;
|
|
}
|
|
|
|
base.OnFixedUpdate(ref data);
|
|
}
|
|
|
|
protected override Vector3 GetRenderPosition(ref KinematicData data, ref KinematicData fromData, float alpha)
|
|
{
|
|
return Vector3.Lerp(fromData.Position, data.Position, alpha);
|
|
}
|
|
|
|
// PRIVATE MEMBERS
|
|
|
|
private NetworkId FindTarget(Vector3 firePosition, Vector3 fireDirection)
|
|
{
|
|
var targets = ListPool.Get<IHitTarget>(64);
|
|
|
|
HitUtility.GetAllTargets(Context.Runner, targets);
|
|
|
|
float bestValue = float.MinValue;
|
|
IHitTarget bestTarget = default;
|
|
|
|
float maxSqrDistance = _maxSeekDistance * _maxSeekDistance;
|
|
float minDot = Mathf.Cos(_maxAngleToTarget * Mathf.Deg2Rad);
|
|
|
|
var physicsScene = Context.Runner.GetPhysicsScene();
|
|
|
|
for (int i = 0; i < targets.Count; i++)
|
|
{
|
|
var target = targets[i];
|
|
var targetPosition = GetTargetPosition(target);
|
|
|
|
var direction = targetPosition - firePosition;
|
|
if (direction.sqrMagnitude > maxSqrDistance)
|
|
continue;
|
|
|
|
float distance = direction.magnitude;
|
|
direction /= distance; // Normalize
|
|
|
|
float dot = Vector3.Dot(fireDirection, direction);
|
|
|
|
if (dot < minDot)
|
|
continue;
|
|
|
|
if (physicsScene.Raycast(firePosition, direction, distance, _environmentCheckMask) == true)
|
|
continue; // View to the target is obstructed
|
|
|
|
float value = dot * 90f * _angleWeight + distance * -_distanceWeight;
|
|
|
|
if (value > bestValue)
|
|
{
|
|
bestValue = value;
|
|
bestTarget = target;
|
|
}
|
|
}
|
|
|
|
ListPool.Return(targets);
|
|
|
|
return bestTarget is NetworkBehaviour behaviour ? behaviour.Object.Id : default;
|
|
}
|
|
|
|
private void TryRecalculateTarget(ref KinematicData data, Vector3 position, Vector3 direction)
|
|
{
|
|
if (_recalculateTargetAfterTime <= 0f)
|
|
return;
|
|
|
|
int recalculateTicks = Mathf.RoundToInt(_recalculateTargetAfterTime * Context.Runner.TickRate);
|
|
int elapsedTicks = Context.Runner.Tick - data.FireTick;
|
|
|
|
if (elapsedTicks % recalculateTicks == 0)
|
|
{
|
|
data.Homing.Target = FindTarget(position, direction);
|
|
}
|
|
}
|
|
|
|
private void UpdateDirection(ref KinematicData data)
|
|
{
|
|
var targetObject = data.Homing.Target.IsValid == true ? Context.Runner.FindObject(data.Homing.Target) : null;
|
|
if (targetObject == null)
|
|
return; // No target, continue in current direction
|
|
|
|
var target = targetObject.GetComponent<IHitTarget>();
|
|
if (target.IsActive == false)
|
|
{
|
|
// Target is no longer active (= dead), forget this target
|
|
// and continue in current direction
|
|
data.Homing.Target = default;
|
|
data.Homing.TargetPosition = default;
|
|
return;
|
|
}
|
|
|
|
var targetPosition = GetTargetPosition(target);
|
|
|
|
var newDirection = (targetPosition - data.Position);
|
|
float distance = newDirection.magnitude;
|
|
|
|
newDirection /= distance; // Normalize
|
|
|
|
float minDot = Mathf.Cos(_maxAngleToTarget * Mathf.Deg2Rad);
|
|
|
|
if (Vector3.Dot(data.Velocity, newDirection) < minDot)
|
|
{
|
|
// Forget this target
|
|
data.Homing.Target = default;
|
|
data.Homing.TargetPosition = default;
|
|
return;
|
|
}
|
|
|
|
if (_predictTargetPosition > 0f)
|
|
{
|
|
var previousTargetPosition = (Vector3)data.Homing.TargetPosition;
|
|
data.Homing.TargetPosition = targetPosition;
|
|
|
|
if (previousTargetPosition != Vector3.zero)
|
|
{
|
|
var targetVelocity = (targetPosition - previousTargetPosition) * Context.Runner.TickRate;
|
|
float timeToTarget = distance / _startSpeed;
|
|
|
|
var predictedTargetPosition = targetPosition + (targetVelocity * timeToTarget * _predictTargetPosition);
|
|
newDirection = (predictedTargetPosition - data.Position).normalized;
|
|
}
|
|
}
|
|
|
|
float deltaTime = Context.Runner.DeltaTime;
|
|
data.Velocity = (data.Velocity + newDirection * deltaTime * _turnSpeed).normalized;
|
|
}
|
|
|
|
private Vector3 GetTargetPosition(IHitTarget target)
|
|
{
|
|
// For predicted targets without lag compensated hitbox we simply aim for target position on both server and clients
|
|
if (target.BodyHitbox == null)
|
|
{
|
|
switch (_homingPosition)
|
|
{
|
|
case EHomingPosition.Head:
|
|
return target.HeadPivot.position;
|
|
case EHomingPosition.Ground:
|
|
return target.GroundPivot.position;
|
|
default:
|
|
return target.BodyPivot.position;
|
|
}
|
|
}
|
|
|
|
// When target is lag compensated (e.g. players and lag comp. moving targets) we use lag compensation position.
|
|
// This ensures the same target position will be used on both local player client and server.
|
|
var positionOffset = Vector3.zero;
|
|
switch (_homingPosition)
|
|
{
|
|
case EHomingPosition.Head:
|
|
positionOffset = target.HeadPivot.position - target.BodyHitbox.Position;
|
|
break;
|
|
case EHomingPosition.Ground:
|
|
positionOffset = target.GroundPivot.position - target.BodyHitbox.Position;
|
|
break;
|
|
default:
|
|
positionOffset = target.BodyPivot.position - target.BodyHitbox.Position;
|
|
break;
|
|
}
|
|
|
|
Context.Runner.LagCompensation.PositionRotation(target.BodyHitbox, Context.Owner, out var compensatedPosition, out _, true);
|
|
return compensatedPosition + positionOffset;
|
|
}
|
|
|
|
// HELPERS
|
|
|
|
public enum EHomingPosition
|
|
{
|
|
Body = 1,
|
|
Head,
|
|
Ground,
|
|
}
|
|
}
|
|
}
|