using UnityEngine;
namespace BossRoom.Server
{
///
/// Action that represents a swing of a melee weapon. It is not explicitly targeted, but rather detects the foe that was hit with a physics check.
///
///
/// Q: Why do we DetectFoe twice, once in Start, once when we actually connect?
/// A: The weapon swing doesn't happen instantaneously. We want to broadcast the action to other clients as fast as possible to minimize latency,
/// but this poses a conundrum. At the moment the swing starts, you don't know for sure if you've hit anybody yet. There are a few possible resolutions to this:
/// 1. Do the DetectFoe operation once--in Start.
/// Pros: Simple! Only one physics cast per swing--saves on perf.
/// Cons: Is unfair. You can step out of the swing of an attack, but no matter how far you go, you'll still be hit. The reverse is also true--you can
/// "step into an attack", and it won't affect you. This will feel terrible to the attacker.
/// 2. Do the DetectFoe operation once--in Update. Send a separate RPC to the targeted entity telling it to play its hit react.
/// Pros: Always shows the correct behavior. The entity that gets hit plays its hit react (if any).
/// Cons: You need another RPC. Adds code complexity and bandwidth. You also don't have enough information when you start visualizing the swing on
/// the client to do any intelligent animation handshaking. If your server->client latency is even a little uneven, your "attack" animation
/// won't line up correctly with the hit react, making combat look floaty and disjointed.
/// 3. Do the DetectFoe operation twice, once in Start and once in Update.
/// Pros: Is fair--you do the hit-detect at the moment of the swing striking home. And will generally play the hit react on the right target.
/// Cons: Requires more complicated visualization logic. The initial broadcast foe can only ever be treated as a "hint". The graphics logic
/// needs to do its own range checking to pick the best candidate to play the hit react on.
///
/// As so often happens in networked games (and games in general), there's no perfect solution--just sets of tradeoffs. For our example, we're showing option "3".
///
public class MeleeAction : Action
{
private bool m_ExecFired;
//cache Physics Cast hits, to minimize allocs.
private static RaycastHit[] s_Hits;
private const int k_MaxDetects = 4;
private ulong m_ProvisionalTarget;
public MeleeAction(ServerCharacter parent, ref ActionRequestData data, int level) : base(parent, ref data, level)
{
if (s_Hits == null)
{
s_Hits = new RaycastHit[k_MaxDetects];
}
}
public override bool Start()
{
ServerCharacter foe = DetectFoe();
if (foe != null)
{
m_ProvisionalTarget = foe.NetworkId;
Data.TargetIds = new ulong[] { foe.NetworkId };
}
m_Parent.NetState.ServerBroadcastAction(ref Data);
return true;
}
public override bool Update()
{
if (!m_ExecFired && (Time.time - TimeStarted) >= Description.ExecTime_s)
{
m_ExecFired = true;
var foe = DetectFoe(m_ProvisionalTarget);
if (foe != null)
{
foe.RecieveHP(this.m_Parent, -Description.Amount);
}
}
return true;
}
///
/// Returns the ServerCharacter of the foe we hit, or null if none found.
///
///
private ServerCharacter DetectFoe(ulong foeHint = 0)
{
//this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack.
var myBounds = this.m_Parent.GetComponent().bounds;
//NPCs (monsters) can hit PCs, and vice versa. No friendly fire allowed on either side.
int mask = LayerMask.GetMask(m_Parent.IsNPC ? "PCs" : "NPCs");
int numResults = Physics.BoxCastNonAlloc(m_Parent.transform.position, myBounds.extents,
m_Parent.transform.forward, s_Hits, Quaternion.identity, Description.Range, mask);
if (numResults == 0) { return null; }
//everything that passes the mask should have a ServerCharacter component.
ServerCharacter foundFoe = s_Hits[0].collider.GetComponent();
//we always prefer the hinted foe. If he's still in range, he should take the damage, because he's who the client visualization
//system will play the hit-react on (in case there's any ambiguity).
for (int i = 0; i < numResults; i++)
{
var serverChar = s_Hits[i].collider.GetComponent();
if (serverChar.NetworkId == foeHint)
{
foundFoe = serverChar;
break;
}
}
return foundFoe;
}
}
}