383 lines
11 KiB
C#
383 lines
11 KiB
C#
using UnityEngine;
|
|
using Fusion;
|
|
using System.Collections.Generic;
|
|
|
|
namespace TPSBR
|
|
{
|
|
public class KinematicProjectile : Projectile
|
|
{
|
|
// PUBLIC MEMBERS
|
|
|
|
public float FireDespawnTime => _fireDespawnTime;
|
|
|
|
// PROTECTED MEMBERS
|
|
|
|
[Networked]
|
|
protected ProjectileData _data { get; private set; }
|
|
[Networked][OnChangedRender(nameof(OnBounceCountChanged))]
|
|
protected int _bounceCount { get; private set; }
|
|
|
|
// PRIVATE MEMBERS
|
|
|
|
[SerializeField]
|
|
private ProjectileDamage _damage;
|
|
[SerializeField]
|
|
private float _fireDespawnTime = 3f;
|
|
[SerializeField]
|
|
private float _impactDespawnTime = 1f;
|
|
[SerializeField]
|
|
private float _gravity = 20f;
|
|
[SerializeField]
|
|
private GameObject _impactEffect;
|
|
[SerializeField]
|
|
private ImpactSetup _impactSetup;
|
|
[SerializeField]
|
|
private float _impactPenetration = 0f;
|
|
[SerializeField]
|
|
private bool _spawnImpactEffectOnTimeout = false;
|
|
[SerializeField]
|
|
private bool _spawnImpactOnStaticHitOnly = true;
|
|
|
|
[SerializeField]
|
|
private float _showProjectileVisualAfterDistance = 0f;
|
|
[SerializeField]
|
|
private GameObject _projectileVisual;
|
|
[SerializeField]
|
|
private Transform _dummyRotationTarget;
|
|
[SerializeField]
|
|
private float _velocityToRotationMultiplier = 10f;
|
|
|
|
[Header("Bounce")]
|
|
[SerializeField]
|
|
private bool _canBounce = false;
|
|
[SerializeField]
|
|
private float _bounceVelocityMultiplier = 0.7f;
|
|
[SerializeField]
|
|
private float _bounceObjectRadius = 0.05f;
|
|
[SerializeField]
|
|
private AudioEffect _bounceEffect;
|
|
|
|
private float _maxBounceVolume;
|
|
|
|
private EHitType _hitType;
|
|
private LayerMask _hitMask;
|
|
private int _ownerObjectInstanceID;
|
|
private List<LagCompensatedHit> _validHits = new(16);
|
|
|
|
private TrailRenderer _trailRenderer;
|
|
private bool _hasImpactedVisual;
|
|
|
|
// PUBLIC METHODS
|
|
|
|
public override void Fire(NetworkObject owner, Vector3 firePosition, Vector3 initialVelocity, LayerMask hitMask, EHitType hitType)
|
|
{
|
|
ProjectileData data = default;
|
|
|
|
data.FirePosition = firePosition;
|
|
data.InitialVelocity = initialVelocity;
|
|
data.DespawnCooldown = TickTimer.CreateFromSeconds(Runner, _fireDespawnTime);
|
|
data.StartTick = Runner.Tick;
|
|
|
|
_hitMask = hitMask;
|
|
_hitType = hitType;
|
|
|
|
_ownerObjectInstanceID = owner != null ? owner.gameObject.GetInstanceID() : 0;
|
|
|
|
_data = data;
|
|
_bounceCount = default;
|
|
}
|
|
|
|
public void SetDespawnCooldown(float cooldown)
|
|
{
|
|
var data = _data;
|
|
data.DespawnCooldown = TickTimer.CreateFromSeconds(Runner, cooldown);
|
|
_data = data;
|
|
}
|
|
|
|
// NetworkBehaviour INTERFACE
|
|
|
|
public override void Spawned()
|
|
{
|
|
_projectileVisual.SetActiveSafe(_showProjectileVisualAfterDistance <= 0f);
|
|
|
|
_hasImpactedVisual = false;
|
|
|
|
if (_trailRenderer != null)
|
|
{
|
|
_trailRenderer.Clear();
|
|
}
|
|
}
|
|
|
|
public override void Despawned(NetworkRunner runner, bool hasState)
|
|
{
|
|
if (_dummyRotationTarget != null)
|
|
{
|
|
_dummyRotationTarget.rotation = Quaternion.identity;
|
|
}
|
|
}
|
|
|
|
public override void FixedUpdateNetwork()
|
|
{
|
|
var data = _data;
|
|
|
|
if (CalculateProjectile(ref data) == true)
|
|
{
|
|
_data = data;
|
|
}
|
|
}
|
|
|
|
public override void Render()
|
|
{
|
|
RenderProjectile(_data);
|
|
}
|
|
|
|
// MONOBEHAVIOUR
|
|
|
|
protected void Awake()
|
|
{
|
|
_trailRenderer = GetComponentInChildren<TrailRenderer>();
|
|
_maxBounceVolume = _bounceEffect != null ? _bounceEffect.DefaultSetup.Volume : 0f;
|
|
}
|
|
|
|
// PROTECTED METHODS
|
|
|
|
protected void OnBounceCountChanged()
|
|
{
|
|
if (_bounceEffect == null)
|
|
return;
|
|
|
|
var soundSetup = _bounceEffect.DefaultSetup;
|
|
soundSetup.Volume = Mathf.Lerp(0f, _maxBounceVolume, _data.InitialVelocity.magnitude / 10f);
|
|
|
|
_bounceEffect.Play(soundSetup, EForceBehaviour.ForceAny);
|
|
}
|
|
|
|
// PRIVATE METHODS
|
|
|
|
private void RenderProjectile(ProjectileData data)
|
|
{
|
|
// Spawn impact if not shown yet
|
|
if (data.HasImpacted == true && _hasImpactedVisual == false)
|
|
{
|
|
SpawnImpact(ref data, data.ImpactPosition, data.ImpactNormal, data.ImpactTagHash);
|
|
_projectileVisual.SetActiveSafe(false);
|
|
}
|
|
|
|
if (_trailRenderer != null)
|
|
{
|
|
_trailRenderer.emitting = data.IsFinished == false;
|
|
}
|
|
|
|
if (data.IsFinished == true)
|
|
{
|
|
transform.position = data.FinishedPosition;
|
|
return;
|
|
}
|
|
|
|
if (TryGetSnapshotsBuffers(out NetworkBehaviourBuffer from, out NetworkBehaviourBuffer to, out float alpha) == false)
|
|
return;
|
|
|
|
float renderTime = Object.IsProxy == true ? Runner.RemoteRenderTime : Runner.LocalRenderTime;
|
|
float floatTick = renderTime / Runner.DeltaTime;
|
|
|
|
transform.position = GetProjectilePosition(ref data, floatTick);
|
|
|
|
var direction = floatTick - 1f < data.StartTick ? data.InitialVelocity : transform.position - GetProjectilePosition(ref data, floatTick - 1f);
|
|
transform.rotation = Quaternion.LookRotation(direction.normalized);
|
|
|
|
if (_showProjectileVisualAfterDistance > 0f && Vector3.Distance(data.FirePosition, transform.position) > _showProjectileVisualAfterDistance)
|
|
{
|
|
// Delaying showing projectile visual is dummy approach for solving differences between
|
|
// weapon barrel position and actual fire position (near character shoulder).
|
|
// Check more elaborate approach to projectiles in Fusion Projectiles project.
|
|
_projectileVisual.SetActiveSafe(true);
|
|
}
|
|
|
|
if (_dummyRotationTarget != null)
|
|
{
|
|
var axis = Vector3.Cross(Vector3.up, data.InitialVelocity);
|
|
_dummyRotationTarget.Rotate(axis, Time.deltaTime * data.InitialVelocity.magnitude * _velocityToRotationMultiplier, Space.World);
|
|
}
|
|
}
|
|
|
|
private bool CalculateProjectile(ref ProjectileData data)
|
|
{
|
|
if (data.DespawnCooldown.Expired(Runner) == true)
|
|
{
|
|
if (data.HasStopped == false)
|
|
{
|
|
data.FinishedPosition = GetProjectilePosition(ref data, Runner.Tick);
|
|
data.HasStopped = true;
|
|
}
|
|
|
|
if (_spawnImpactEffectOnTimeout == true && data.HasImpacted == false)
|
|
{
|
|
SpawnImpact(ref data, data.FinishedPosition, Vector3.up, 0);
|
|
}
|
|
|
|
Runner.Despawn(Object);
|
|
return false;
|
|
}
|
|
|
|
if (data.IsFinished == true)
|
|
return true;
|
|
|
|
var newPosition = GetProjectilePosition(ref data, Runner.Tick);
|
|
var previousPosition = GetProjectilePosition(ref data, Runner.Tick - 1);
|
|
|
|
var direction = newPosition - previousPosition;
|
|
float distance = direction.magnitude;
|
|
|
|
if (distance <= 0f)
|
|
return true;
|
|
|
|
direction /= distance; // Normalize
|
|
|
|
if (ProjectileUtility.ProjectileCast(Runner, Object.InputAuthority, _ownerObjectInstanceID, previousPosition - direction * _bounceObjectRadius, direction, distance + 2 * _bounceObjectRadius, _hitMask, _validHits) == true)
|
|
{
|
|
if (_canBounce == true)
|
|
{
|
|
ProcessBounce(ref data, _validHits[0], direction, distance + _bounceObjectRadius * 2f);
|
|
}
|
|
else
|
|
{
|
|
ProcessHit(ref data, _validHits[0], direction);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ProcessHit(ref ProjectileData data, LagCompensatedHit hit, Vector3 direction)
|
|
{
|
|
data.FinishedPosition = hit.Point + direction * _impactPenetration;
|
|
|
|
float realDistance = Vector3.Distance(data.FirePosition, hit.Point);
|
|
float hitDamage = _damage.GetDamage(realDistance);
|
|
|
|
if (hitDamage > 0f)
|
|
{
|
|
var player = Context.NetworkGame.GetPlayer(Object.InputAuthority);
|
|
var owner = player != null ? player.ActiveAgent : null;
|
|
|
|
if (owner != null)
|
|
{
|
|
HitUtility.ProcessHit(owner.Object, direction, hit, hitDamage, _hitType, out HitData hitData);
|
|
}
|
|
else
|
|
{
|
|
HitUtility.ProcessHit(Object.InputAuthority, direction, hit, hitDamage, _hitType, out HitData hitData);
|
|
}
|
|
}
|
|
|
|
bool isDynamicTarget = hit.GameObject.layer == ObjectLayer.Agent || hit.GameObject.layer == ObjectLayer.Target;
|
|
|
|
if (_spawnImpactOnStaticHitOnly == false || isDynamicTarget == false)
|
|
{
|
|
SpawnImpact(ref data, hit.Point, (hit.Normal + -direction) * 0.5f, hit.GameObject.tag.GetHashCode());
|
|
}
|
|
|
|
data.HasStopped = true;
|
|
data.DespawnCooldown = TickTimer.CreateFromSeconds(Runner, isDynamicTarget == false ? _impactDespawnTime : 0.1f);
|
|
}
|
|
|
|
private void ProcessBounce(ref ProjectileData data, LagCompensatedHit hit, Vector3 direction, float distance)
|
|
{
|
|
float bounceMultiplier = Mathf.Lerp(_bounceVelocityMultiplier, 0.9f, _bounceCount / 8f);
|
|
|
|
// Stop bouncing when the velocity is small enough
|
|
if (distance * bounceMultiplier < _bounceObjectRadius * 2f)
|
|
{
|
|
data.HasStopped = true;
|
|
data.FinishedPosition = hit.Point + Vector3.Reflect(direction, hit.Normal) * _bounceObjectRadius;
|
|
return;
|
|
}
|
|
|
|
float distanceToHit = Vector3.Distance(hit.Point, transform.position);
|
|
float progressToHit = distanceToHit / distance;
|
|
|
|
var reflectedDirection = Vector3.Reflect(direction, hit.Normal);
|
|
|
|
data.FirePosition = hit.Point + reflectedDirection * _bounceObjectRadius;;
|
|
data.InitialVelocity = reflectedDirection * data.InitialVelocity.magnitude * bounceMultiplier;
|
|
|
|
// Simple trick to better align position with ticks. More precise solution would be to remember
|
|
// alpha between ticks (when the bounce happened) but it is good enough here.
|
|
data.StartTick = progressToHit > 0.5f ? Runner.Tick : Runner.Tick - 1;
|
|
|
|
_bounceCount++;
|
|
}
|
|
|
|
private void SpawnImpact(ref ProjectileData data, Vector3 position, Vector3 normal, int impactTagHash)
|
|
{
|
|
if (position == Vector3.zero)
|
|
return;
|
|
|
|
data.ImpactPosition = position;
|
|
data.ImpactNormal = normal;
|
|
data.ImpactTagHash = impactTagHash;
|
|
data.HasImpacted = true;
|
|
|
|
if (_impactEffect != null)
|
|
{
|
|
var networkBehaviour = _impactEffect.GetComponent<NetworkBehaviour>();
|
|
if (networkBehaviour != null)
|
|
{
|
|
if (HasStateAuthority == true)
|
|
{
|
|
Runner.Spawn(networkBehaviour, position, Quaternion.LookRotation(normal), Object.InputAuthority);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var effect = Context.ObjectCache.Get(_impactEffect);
|
|
effect.transform.SetPositionAndRotation(position, Quaternion.LookRotation(normal));
|
|
}
|
|
}
|
|
|
|
if (_impactSetup != null && impactTagHash != 0)
|
|
{
|
|
var impactParticle = Context.ObjectCache.Get(_impactSetup.GetImpact(impactTagHash));
|
|
Context.ObjectCache.ReturnDeferred(impactParticle, 5f);
|
|
Runner.MoveToRunnerSceneExtended(impactParticle);
|
|
|
|
impactParticle.transform.position = position;
|
|
impactParticle.transform.rotation = Quaternion.LookRotation(normal);
|
|
}
|
|
|
|
_hasImpactedVisual = true;
|
|
}
|
|
|
|
private Vector3 GetProjectilePosition(ref ProjectileData data, float tick)
|
|
{
|
|
float time = (tick - data.StartTick) * Runner.DeltaTime;
|
|
|
|
if (time <= 0f)
|
|
return data.FirePosition;
|
|
|
|
return data.FirePosition + data.InitialVelocity * time + new Vector3(0f, -_gravity, 0f) * time * time * 0.5f;
|
|
}
|
|
|
|
// CLASSES / STRUCTS
|
|
|
|
public struct ProjectileData : INetworkStruct
|
|
{
|
|
public bool IsFinished => HasImpacted || HasStopped;
|
|
public bool HasStopped { get { return State.IsBitSet(0); } set { State.SetBit(0, value); } }
|
|
public bool HasImpacted { get { return State.IsBitSet(1); } set { State.SetBit(1, value); } }
|
|
|
|
public byte State;
|
|
public int StartTick;
|
|
public TickTimer DespawnCooldown;
|
|
public Vector3 FirePosition;
|
|
public Vector3 InitialVelocity;
|
|
|
|
public Vector3 FinishedPosition;
|
|
|
|
public Vector3 ImpactPosition;
|
|
public Vector3 ImpactNormal;
|
|
public int ImpactTagHash;
|
|
}
|
|
}
|
|
}
|