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#
125 lines
6.5 KiB
C#
1 week 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;
|
||
|
}
|
||
|
}
|
||
|
}
|