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.

125 lines
6.5 KiB
C#

2 weeks ago
using System;
using Unity.BossRoom.Gameplay.GameplayObjects;
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
using UnityEngine;
namespace Unity.BossRoom.Gameplay.Actions
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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".
/// </remarks>
[CreateAssetMenu(menuName = "BossRoom/Actions/Melee Action")]
public partial class MeleeAction : Action
{
private bool m_ExecutionFired;
private ulong m_ProvisionalTarget;
public override bool OnStart(ServerCharacter serverCharacter)
{
ulong target = (Data.TargetIds != null && Data.TargetIds.Length > 0) ? Data.TargetIds[0] : serverCharacter.TargetId.Value;
IDamageable foe = DetectFoe(serverCharacter, target);
if (foe != null)
{
m_ProvisionalTarget = foe.NetworkObjectId;
Data.TargetIds = new ulong[] { foe.NetworkObjectId };
}
// snap to face the right direction
if (Data.Direction != Vector3.zero)
{
serverCharacter.physicsWrapper.Transform.forward = Data.Direction;
}
serverCharacter.serverAnimationHandler.NetworkAnimator.SetTrigger(Config.Anim);
serverCharacter.clientCharacter.ClientPlayActionRpc(Data);
return true;
}
public override void Reset()
{
base.Reset();
m_ExecutionFired = false;
m_ProvisionalTarget = 0;
m_ImpactPlayed = false;
m_SpawnedGraphics = null;
}
public override bool OnUpdate(ServerCharacter clientCharacter)
{
if (!m_ExecutionFired && (Time.time - TimeStarted) >= Config.ExecTimeSeconds)
{
m_ExecutionFired = true;
var foe = DetectFoe(clientCharacter, m_ProvisionalTarget);
if (foe != null)
{
foe.ReceiveHP(clientCharacter, -Config.Amount);
}
}
return true;
}
/// <summary>
/// Returns the ServerCharacter of the foe we hit, or null if none found.
/// </summary>
/// <returns></returns>
private IDamageable DetectFoe(ServerCharacter parent, ulong foeHint = 0)
{
return GetIdealMeleeFoe(Config.IsFriendly ^ parent.IsNpc, parent.physicsWrapper.DamageCollider, Config.Range, foeHint);
}
/// <summary>
/// Utility used by Actions to perform Melee attacks. Performs a melee hit-test
/// and then looks through the results to find an alive target, preferring the provided
/// enemy.
/// </summary>
/// <param name="isNPC">true if the attacker is an NPC (and therefore should hit PCs). False for the reverse.</param>
/// <param name="ourCollider">The collider of the attacking GameObject.</param>
/// <param name="meleeRange">The range in meters to check for foes.</param>
/// <param name="preferredTargetNetworkId">The NetworkObjectId of our preferred foe, or 0 if no preference</param>
/// <returns>ideal target's IDamageable, or null if no valid target found</returns>
public static IDamageable GetIdealMeleeFoe(bool isNPC, Collider ourCollider, float meleeRange, ulong preferredTargetNetworkId)
{
RaycastHit[] results;
int numResults = ActionUtils.DetectMeleeFoe(isNPC, ourCollider, meleeRange, out results);
IDamageable foundFoe = null;
//everything that got hit by the raycast should have an IDamageable component, so we can retrieve that and see if they're appropriate targets.
//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 damageable = results[i].collider.GetComponent<IDamageable>();
if (damageable != null && damageable.IsDamageable() &&
(damageable.NetworkObjectId == preferredTargetNetworkId || foundFoe == null))
{
foundFoe = damageable;
}
}
return foundFoe;
}
}
}