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.

245 lines
9.0 KiB
C#

3 weeks ago
using System;
using System.Collections.Generic;
using Unity.BossRoom.Gameplay.Actions;
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
using Unity.BossRoom.Utils;
using Unity.BossRoom.VisualEffects;
using Unity.Netcode;
using UnityEngine;
namespace Unity.BossRoom.Gameplay.GameplayObjects
{
/// <summary>
/// Logic that handles a physics-based projectile with a collider
/// </summary>
public class PhysicsProjectile : NetworkBehaviour
{
bool m_Started;
[SerializeField]
SphereCollider m_OurCollider;
/// <summary>
/// The character that created us. Can be 0 to signal that we were created generically by the server.
/// </summary>
ulong m_SpawnerId;
/// <summary>
/// The data for our projectile. Indicates speed, damage, etc.
/// </summary>
ProjectileInfo m_ProjectileInfo;
const int k_MaxCollisions = 4;
const float k_WallLingerSec = 2f; //time in seconds that arrows linger after hitting a target.
const float k_EnemyLingerSec = 0.2f; //time after hitting an enemy that we persist.
Collider[] m_CollisionCache = new Collider[k_MaxCollisions];
/// <summary>
/// Time when we should destroy this arrow, in Time.time seconds.
/// </summary>
float m_DestroyAtSec;
int m_CollisionMask; //mask containing everything we test for while moving
int m_BlockerMask; //physics mask for things that block the arrow's flight.
int m_NpcLayer;
/// <summary>
/// List of everyone we've hit and dealt damage to.
/// </summary>
/// <remarks>
/// Note that it's possible for entries in this list to become null if they're Destroyed post-impact.
/// But that's fine by us! We use <c>m_HitTargets.Count</c> to tell us how many total enemies we've hit,
/// so those nulls still count as hits.
/// </remarks>
List<GameObject> m_HitTargets = new List<GameObject>();
/// <summary>
/// Are we done moving?
/// </summary>
bool m_IsDead;
[SerializeField]
[Tooltip("Explosion prefab used when projectile hits enemy. This should have a fixed duration.")]
SpecialFXGraphic m_OnHitParticlePrefab;
[SerializeField]
TrailRenderer m_TrailRenderer;
[SerializeField]
Transform m_Visualization;
const float k_LerpTime = 0.1f;
PositionLerper m_PositionLerper;
/// <summary>
/// Set everything up based on provided projectile information.
/// (Note that this is called before OnNetworkSpawn(), so don't try to do any network stuff here.)
/// </summary>
public void Initialize(ulong creatorsNetworkObjectId, in ProjectileInfo projectileInfo)
{
m_SpawnerId = creatorsNetworkObjectId;
m_ProjectileInfo = projectileInfo;
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
m_Started = true;
m_HitTargets = new List<GameObject>();
m_IsDead = false;
m_DestroyAtSec = Time.fixedTime + (m_ProjectileInfo.Range / m_ProjectileInfo.Speed_m_s);
m_CollisionMask = LayerMask.GetMask(new[] { "NPCs", "Default", "Environment" });
m_BlockerMask = LayerMask.GetMask(new[] { "Default", "Environment" });
m_NpcLayer = LayerMask.NameToLayer("NPCs");
}
if (IsClient)
{
m_TrailRenderer.Clear();
m_Visualization.parent = null;
m_PositionLerper = new PositionLerper(transform.position, k_LerpTime);
m_Visualization.transform.rotation = transform.rotation;
}
}
public override void OnNetworkDespawn()
{
if (IsServer)
{
m_Started = false;
}
if (IsClient)
{
m_TrailRenderer.Clear();
m_Visualization.parent = transform;
}
}
void FixedUpdate()
{
if (!m_Started || !IsServer)
{
return; //don't do anything before OnNetworkSpawn has run.
}
if (m_DestroyAtSec < Time.fixedTime)
{
// Time to return to the pool from whence it came.
var networkObject = gameObject.GetComponent<NetworkObject>();
networkObject.Despawn();
return;
}
var displacement = transform.forward * (m_ProjectileInfo.Speed_m_s * Time.fixedDeltaTime);
transform.position += displacement;
if (!m_IsDead)
{
DetectCollisions();
}
}
void Update()
{
if (IsClient)
{
// One thing to note: this graphics GameObject is detached from its parent on OnNetworkSpawn. On the host,
// the m_Parent Transform is translated via PhysicsProjectile's FixedUpdate method. On all other
// clients, m_Parent's NetworkTransform handles syncing and interpolating the m_Parent Transform. Thus, to
// eliminate any visual jitter on the host, this GameObject is positionally smoothed over time. On all other
// clients, no positional smoothing is required, since m_Parent's NetworkTransform will perform
// positional interpolation on its Update method, and so this position is simply matched 1:1 with m_Parent.
if (IsHost)
{
m_Visualization.position = m_PositionLerper.LerpPosition(m_Visualization.position,
transform.position);
}
else
{
m_Visualization.position = transform.position;
}
}
}
void DetectCollisions()
{
var position = transform.localToWorldMatrix.MultiplyPoint(m_OurCollider.center);
var numCollisions = Physics.OverlapSphereNonAlloc(position, m_OurCollider.radius, m_CollisionCache, m_CollisionMask);
for (int i = 0; i < numCollisions; i++)
{
int layerTest = 1 << m_CollisionCache[i].gameObject.layer;
if ((layerTest & m_BlockerMask) != 0)
{
//hit a wall; leave it for a couple of seconds.
m_ProjectileInfo.Speed_m_s = 0;
m_IsDead = true;
m_DestroyAtSec = Time.fixedTime + k_WallLingerSec;
return;
}
if (m_CollisionCache[i].gameObject.layer == m_NpcLayer && !m_HitTargets.Contains(m_CollisionCache[i].gameObject))
{
m_HitTargets.Add(m_CollisionCache[i].gameObject);
if (m_HitTargets.Count >= m_ProjectileInfo.MaxVictims)
{
// we've hit all the enemies we're allowed to! So we're done
m_DestroyAtSec = Time.fixedTime + k_EnemyLingerSec;
m_IsDead = true;
}
//all NPC layer entities should have one of these.
var targetNetObj = m_CollisionCache[i].GetComponentInParent<NetworkObject>();
if (targetNetObj)
{
ClientHitEnemyRpc(targetNetObj.NetworkObjectId);
//retrieve the person that created us, if he's still around.
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(m_SpawnerId, out var spawnerNet);
var spawnerObj = spawnerNet != null ? spawnerNet.GetComponent<ServerCharacter>() : null;
if (m_CollisionCache[i].TryGetComponent(out IDamageable damageable))
{
damageable.ReceiveHP(spawnerObj, -m_ProjectileInfo.Damage);
}
}
if (m_IsDead)
{
return; // don't keep examining collisions since we can't damage anybody else
}
}
}
}
[Rpc(SendTo.ClientsAndHost)]
private void ClientHitEnemyRpc(ulong enemyId)
{
//in the future we could do quite fancy things, like deparenting the Graphics Arrow and parenting it to the target.
//For the moment we play some particles (optionally), and cause the target to animate a hit-react.
NetworkObject targetNetObject;
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(enemyId, out targetNetObject))
{
if (m_OnHitParticlePrefab)
{
// show an impact graphic
Instantiate(m_OnHitParticlePrefab.gameObject, transform.position, transform.rotation);
}
}
}
}
}