diff --git a/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset b/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset
index da63bc034..f6450fbec 100644
--- a/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset
+++ b/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5ba7dbcb8c32a01bdacdf8a5591b7a17bf0d68547e61b0376533608123a31b26
+oid sha256:f1181a174a045df822dd160e56a2603edbd3148f9f8f29c202e37aa76afd11a2
size 864
diff --git a/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController b/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController
index 4a8195ae1..30d8e82a4 100644
--- a/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController
+++ b/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController
@@ -21,3 +21,5 @@ AnimatorOverrideController:
m_OverrideClip: {fileID: -6239216113759591374, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
- m_OriginalClip: {fileID: -5612658629409835226, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
m_OverrideClip: {fileID: -5612658629409835226, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
+ - m_OriginalClip: {fileID: -4428684883894617094, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
+ m_OverrideClip: {fileID: -3419257869308726280, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs
index 2bc8f15ad..a4218f680 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs
@@ -13,6 +13,11 @@ public abstract class ActionFX : ActionBase
///
public const string k_DefaultHitReact = "HitReact1";
+ ///
+ /// True if this actionFX began running immediately, prior to getting a confirmation from the server.
+ ///
+ public bool Anticipated { get; protected set; }
+
public ActionFX(ref ActionRequestData data, ClientCharacterVisualization parent) : base(ref data)
{
m_Parent = parent;
@@ -21,8 +26,16 @@ public ActionFX(ref ActionRequestData data, ClientCharacterVisualization parent)
///
/// Starts the ActionFX. Derived classes may return false if they wish to end immediately without their Update being called.
///
+ ///
+ /// Derived class should be sure to call base.Start() in their implementation, but note that this resets "Anticipated" to false.
+ ///
/// true to play, false to be immediately cleaned up.
- public abstract bool Start();
+ public virtual bool Start()
+ {
+ Anticipated = false; //once you start for real you are no longer an anticipated action.
+ TimeStarted = UnityEngine.Time.time;
+ return true;
+ }
public abstract bool Update();
@@ -54,17 +67,65 @@ public static ActionFX MakeActionFX(ref ActionRequestData data, ClientCharacterV
case ActionLogic.RangedFXTargeted: return new FXProjectileTargetedActionFX(ref data, parent);
case ActionLogic.Trample: return new TrampleActionFX(ref data, parent);
case ActionLogic.AoE: return new AoeActionFX(ref data, parent);
- case ActionLogic.Stunned: return new AnimationOnlyActionFX(ref data, parent);
case ActionLogic.Target: return new TargetActionFX(ref data, parent);
+
case ActionLogic.ChargedShield:
case ActionLogic.ChargedLaunchProjectile: return new ChargedActionFX(ref data, parent);
+
case ActionLogic.StealthMode: return new StealthModeActionFX(ref data, parent);
+
+ case ActionLogic.Stunned:
+ case ActionLogic.LaunchProjectile:
+ case ActionLogic.Revive:
+ case ActionLogic.Emote: return new AnimationOnlyActionFX(ref data, parent);
+
default: throw new System.NotImplementedException();
}
}
+ ///
+ /// Should this ActionFX be created anticipatively on the owning client?
+ ///
+ /// The ActionVisualization that would be playing this ActionFX.
+ /// The request being sent to the server
+ /// If true ActionVisualization should pre-emptively create the ActionFX on the owning client, before hearing back from the server.
+ public static bool ShouldAnticipate(ActionVisualization parent, ref ActionRequestData data)
+ {
+ if( !parent.Parent.CanPerformActions ) { return false; }
+
+ var actionDescription = GameDataSource.Instance.ActionDataByType[data.ActionTypeEnum];
+
+ //for actions with ShouldClose set, we check our range locally. If we are out of range, we shouldn't anticipate, as we will
+ //need to execute a ChaseAction (synthesized on the server) prior to actually playing the skill.
+ bool isTargetEligible = true;
+ if( data.ShouldClose == true )
+ {
+ ulong targetId = (data.TargetIds != null && data.TargetIds.Length > 0) ? data.TargetIds[0] : 0;
+ if( MLAPI.Spawning.NetworkSpawnManager.SpawnedObjects.TryGetValue(targetId, out MLAPI.NetworkObject networkObject ) )
+ {
+ float rangeSquared = actionDescription.Range * actionDescription.Range;
+ isTargetEligible = (networkObject.transform.position - parent.Parent.transform.position).sqrMagnitude < rangeSquared;
+ }
+ }
+
+ //at present all Actionts anticipate except for the Target action, which runs a single instance on the client and is
+ //responsible for action anticipation on its own.
+ return isTargetEligible && actionDescription.Logic != ActionLogic.Target;
+ }
+
public virtual void OnAnimEvent(string id) { }
public virtual void OnStoppedChargingUp() { }
+
+ ///
+ /// Called when the action is being "anticipated" on the client. For example, if you are the owner of a tank and you swing your hammer,
+ /// you get this call immediately on the client, before the server round-trip.
+ /// Overriders should always call the base class in their implementation!
+ ///
+ public virtual void AnticipateAction()
+ {
+ Anticipated = true;
+ TimeStarted = UnityEngine.Time.time;
+ }
}
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs
index 602ea6138..dabf0fcbc 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs
@@ -9,14 +9,19 @@ namespace BossRoom.Visual
///
public class ActionVisualization
{
- private List m_PlayingActions;
+ private List m_PlayingActions = new List();
+
+ ///
+ /// Don't let anticipated actionFXs persist longer than this. This is a safeguard against scenarios
+ /// where we never get a confirmed action for an action we anticipated.
+ ///
+ private const float k_AnticipationTimeoutSeconds = 1;
public ClientCharacterVisualization Parent { get; private set; }
public ActionVisualization(ClientCharacterVisualization parent)
{
Parent = parent;
- m_PlayingActions = new List();
}
public void Update()
@@ -27,15 +32,24 @@ public void Update()
var action = m_PlayingActions[i];
bool keepGoing = action.Update();
bool expirable = action.Description.DurationSeconds > 0f; //non-positive value is a sentinel indicating the duration is indefinite.
- bool timeExpired = expirable && (Time.time - action.TimeStarted) >= action.Description.DurationSeconds;
- if (!keepGoing || timeExpired)
+ bool timeExpired = expirable && action.TimeRunning >= action.Description.DurationSeconds;
+ bool timedOut = action.Anticipated && action.TimeRunning >= k_AnticipationTimeoutSeconds;
+ if (!keepGoing || timeExpired || timedOut)
{
- action.End();
+ if (timedOut) { action.Cancel(); } //an anticipated action that timed out shouldn't get its End called. It is canceled instead.
+ else { action.End(); }
+
m_PlayingActions.RemoveAt(i);
}
}
}
+ //helper wrapper for a FindIndex call on m_PlayingActions.
+ private int FindAction(ActionType action, bool anticipatedOnly )
+ {
+ return m_PlayingActions.FindIndex(a => a.Description.ActionTypeEnum == action && (!anticipatedOnly || a.Anticipated));
+ }
+
public void OnAnimEvent(string id)
{
foreach (var actionFX in m_PlayingActions)
@@ -52,22 +66,55 @@ public void OnStoppedChargingUp()
}
}
- public void PlayAction(ref ActionRequestData data)
+ ///
+ /// Called on the client that owns the Character when the player triggers an action. This allows actions to immediately start playing feedback.
+ ///
+ ///
+ ///
+ /// What is Action Anticipation and what problem does it solve? In short, it lets Actions run logic the moment the input event that triggers them
+ /// is detected on the local client. The purpose of this is to help mask latency. Because this demo is server authoritative, the default behavior is
+ /// to only see feedback for your input after a server-client roundtrip. Somewhere over 200ms of round-trip latency, this starts to feel oppressively sluggish.
+ /// To combat this, you can play visual effects immediately. For example, MeleeActionFX plays both its weapon swing and applies a hit react to the target,
+ /// without waiting to hear from the server. This can lead to discrepancies when the server doesn't think the target was hit, but on the net, will feel
+ /// more responsive.
+ ///
+ /// An important concept of Action Anticipation is that it is opportunistic--it doesn't make any strong guarantees. You don't get an anticipated
+ /// action animation if you are already animating in some way, as one example. Another complexity is that you don't know if the server will actually
+ /// let you play all the actions that you've requested--some may get thrown away, e.g. because you have too many actions in your queue. What this means
+ /// is that Anticipated Actions (actions that have been constructed but not started) won't match up perfectly with actual approved delivered actions from
+ /// the server. For that reason, it must always be fine to receive PlayAction and not have an anticipated action already started (this is true for playback
+ /// Characters belonging to the server and other characters anyway). It also means we need to handle the case where we created an Anticipated Action, but
+ /// never got a confirmation--actions like that need to eventually get discarded.
+ ///
+ /// Another important aspect of Anticipated Actions is that they are an "opt-in" system. You must call base.Start in your Start implementation, but other than
+ /// that, if you don't have a good way to implement an Anticipation for your action, you don't have to do anything. In this case, that action will play
+ /// "normally" (with visual feedback starting when the server's action broadcast reaches the client). Every action type will have its own particular set of
+ /// problems to solve to sell the anticipation effect. For example, in this demo, the mage base attack (FXProjectileTargetedActionFX) just plays the attack animation
+ /// anticipatively, but it could be revised to create and drive the mage bolt effect as well--leaving only damage to arrive in true server time.
+ ///
+ /// How to implement your own Anticipation logic:
+ /// 1. Isolate the visual feedback you want play anticipatively in a private helper method on your ActionFX, like "PlayAttackAnim".
+ /// 2. Override ActionFX.AnticipateAction. Be sure to call base.AnticipateAction, as well as play your visual logic (like PlayAttackAnim).
+ /// 3. In your Start method, be sure to call base.Start (note that this will reset the "Anticipated" field to false).
+ /// 4. In Start, check if the action was Anticipated. If NOT, then play call your PlayAttackAnim method.
+ ///
+ ///
+ /// The Action that is being requested.
+ public void AnticipateAction(ref ActionRequestData data)
{
- ActionDescription actionDesc = GameDataSource.Instance.ActionDataByType[data.ActionTypeEnum];
-
- //Do Trivial Actions (actions that just require playing a single animation, and don't require any state tracking).
- switch (actionDesc.Logic)
+ if (!Parent.IsAnimating && ActionFX.ShouldAnticipate(this, ref data))
{
- case ActionLogic.LaunchProjectile:
- case ActionLogic.Revive:
- case ActionLogic.Emote:
- Parent.OurAnimator.SetTrigger(actionDesc.Anim);
- return;
+ var actionFX = ActionFX.MakeActionFX(ref data, Parent);
+ actionFX.AnticipateAction();
+ m_PlayingActions.Add(actionFX);
}
+ }
- var actionFX = ActionFX.MakeActionFX(ref data, Parent);
- actionFX.TimeStarted = Time.time;
+ public void PlayAction(ref ActionRequestData data)
+ {
+ var anticipatedActionIndex = FindAction(data.ActionTypeEnum, true);
+
+ var actionFX = anticipatedActionIndex>=0 ? m_PlayingActions[anticipatedActionIndex] : ActionFX.MakeActionFX(ref data, Parent);
if (actionFX.Start())
{
m_PlayingActions.Add(actionFX);
@@ -85,7 +132,7 @@ public void CancelAllActions()
public void CancelAllActionsOfType(ActionType actionType)
{
- for (int i = m_PlayingActions.Count-1; i >=0; --i)
+ for (int i = m_PlayingActions.Count - 1; i >= 0; --i)
{
if (m_PlayingActions[i].Description.ActionTypeEnum == actionType)
{
@@ -100,7 +147,7 @@ public void CancelAllActionsOfType(ActionType actionType)
///
public void CancelAll()
{
- foreach( var action in m_PlayingActions )
+ foreach (var action in m_PlayingActions)
{
action.Cancel();
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs
index 5840262de..d25db4b7c 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs
@@ -13,16 +13,32 @@ public AnimationOnlyActionFX(ref ActionRequestData data, ClientCharacterVisualiz
public override bool Start()
{
- m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ if( !Anticipated )
+ {
+ PlayStartAnim();
+ }
+
+ base.Start();
return true;
}
+ private void PlayStartAnim()
+ {
+ m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ }
+
+ public override void AnticipateAction()
+ {
+ base.AnticipateAction();
+ PlayStartAnim();
+ }
+
public override bool Update()
{
return ActionConclusion.Continue;
}
- public override void End()
+ public override void Cancel()
{
if (!string.IsNullOrEmpty(Description.Anim2))
{
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs
index 8e6f27ec4..d13eae627 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs
@@ -11,6 +11,7 @@ public AoeActionFX(ref ActionRequestData data, ClientCharacterVisualization pare
public override bool Start()
{
+ base.Start();
m_Parent.OurAnimator.SetTrigger(Description.Anim);
GameObject.Instantiate(Description.Spawns[0], m_Data.Position, Quaternion.identity);
return ActionConclusion.Stop;
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs
index 96aeaf7cd..744c23628 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs
@@ -27,6 +27,7 @@ public ChargedActionFX(ref ActionRequestData data, ClientCharacterVisualization
public override bool Start()
{
+ base.Start();
m_Parent.OurAnimator.SetTrigger(Description.Anim);
if (Description.Spawns.Length > 0)
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs
index 8905a42fd..5867e151c 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs
@@ -25,6 +25,8 @@ public FXProjectileTargetedActionFX(ref ActionRequestData data, ClientCharacterV
public override bool Start()
{
+ bool wasAnticipated = Anticipated;
+ base.Start();
m_Target = GetTarget();
if (HasTarget() && m_Target == null)
{
@@ -44,10 +46,19 @@ public override bool Start()
m_Projectile = SpawnAndInitializeProjectile();
// animate shooting the projectile
- m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ if( !wasAnticipated )
+ {
+ PlayFireAnim();
+ }
+
return true;
}
+ private void PlayFireAnim()
+ {
+ m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ }
+
public override bool Update()
{
// we keep going until the projectile's duration ends
@@ -128,5 +139,11 @@ private FXProjectile SpawnAndInitializeProjectile()
projectile.Initialize(m_Parent.transform.position, m_Target?.transform, m_Data.Position, Description.ExecTimeSeconds, m_ProjectileDuration);
return projectile;
}
+
+ public override void AnticipateAction()
+ {
+ base.AnticipateAction();
+ PlayFireAnim();
+ }
}
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs
index 259cee892..2c11cae1b 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs
@@ -21,7 +21,12 @@ public MeleeActionFX(ref ActionRequestData data, ClientCharacterVisualization pa
public override bool Start()
{
- m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ if( !Anticipated)
+ {
+ PlayAnim();
+ }
+
+ base.Start();
return true;
}
@@ -45,6 +50,11 @@ public override void End()
PlayHitReact();
}
+ private void PlayAnim()
+ {
+ m_Parent.OurAnimator.SetTrigger(Description.Anim);
+ }
+
private void PlayHitReact()
{
if (m_ImpactPlayed) { return; }
@@ -74,5 +84,14 @@ private void PlayHitReact()
//in the future we may do another physics check to handle the case where a target "ran under our weapon".
//But for now, if the original target is no longer present, then we just don't play our hit react on anything.
}
+
+ public override void AnticipateAction()
+ {
+ base.AnticipateAction();
+
+ //note: because the hit-react is driven from the animation, this means we can anticipatively trigger a hit-react too. That
+ //will make combat feel responsive, but of course the actual damage won't be applied until the server tells us about it.
+ PlayAnim();
+ }
}
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs
index 2f76ba430..917eae680 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs
@@ -27,6 +27,7 @@ public StealthModeActionFX(ref ActionRequestData data, ClientCharacterVisualizat
public override bool Start()
{
+ base.Start();
m_Parent.OurAnimator.SetTrigger(Description.Anim);
return true;
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs
index e3a081ec5..bb6a94f28 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs
@@ -13,6 +13,7 @@ public class TargetActionFX : ActionFX
{
private GameObject m_TargetReticule;
private ulong m_CurrentTarget;
+ private ulong m_NewTarget;
private NetworkCharacterState m_ParentState;
private const float k_ReticuleGroundHeight = 0.2f;
@@ -23,15 +24,34 @@ public TargetActionFX(ref ActionRequestData data, ClientCharacterVisualization p
public override bool Start()
{
+ base.Start();
m_ParentState = m_Parent.Parent.GetComponent();
+
+ m_ParentState.TargetId.OnValueChanged += OnTargetChanged;
+ m_ParentState.GetComponent().ActionInputEvent += OnActionInput;
+
return true;
}
+ private void OnTargetChanged(ulong oldTarget, ulong newTarget )
+ {
+ m_NewTarget = newTarget;
+ }
+
+ private void OnActionInput(ActionRequestData data )
+ {
+ //this method runs on the owning client, and allows us to anticipate our new target for purposes of FX visualization.
+ if( data.ActionTypeEnum == ActionType.GeneralTarget )
+ {
+ m_NewTarget = data.TargetIds[0];
+ }
+ }
+
public override bool Update()
{
- if( m_CurrentTarget != m_ParentState.TargetId.Value )
+ if( m_CurrentTarget != m_NewTarget )
{
- m_CurrentTarget = m_ParentState.TargetId.Value;
+ m_CurrentTarget = m_NewTarget;
if (NetworkSpawnManager.SpawnedObjects.TryGetValue(m_CurrentTarget, out NetworkObject targetObject ) )
{
@@ -89,6 +109,12 @@ private void ValidateReticule(NetworkObject targetObject)
public override void Cancel()
{
GameObject.Destroy(m_TargetReticule);
+
+ m_ParentState.TargetId.OnValueChanged -= OnTargetChanged;
+ if( m_ParentState.TryGetComponent(out Client.ClientInputSender inputSender))
+ {
+ inputSender.ActionInputEvent -= OnActionInput;
+ }
}
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs
index def39ff0d..bfd36086c 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs
@@ -37,6 +37,7 @@ public TrampleActionFX(ref ActionRequestData data, ClientCharacterVisualization
public override bool Start()
{
+ base.Start();
m_Parent.OurAnimator.SetTrigger(Description.Anim);
return true;
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs b/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs
index ad6bc8159..67384ca08 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs
@@ -51,6 +51,8 @@ public class ClientCharacterVisualization : NetworkBehaviour
event Action Destroyed;
+ public bool CanPerformActions { get { return m_NetState.CanPerformActions; } }
+
///
public override void NetworkStart()
{
@@ -120,6 +122,7 @@ public override void NetworkStart()
if( Parent.TryGetComponent(out ClientInputSender inputSender))
{
+ inputSender.ActionInputEvent += OnActionInput;
inputSender.ClientMoveEvent += OnMoveInput;
}
}
@@ -143,6 +146,11 @@ public override void NetworkStart()
}
}
+ private void OnActionInput(ActionRequestData data)
+ {
+ m_ActionViz.AnticipateAction(ref data);
+ }
+
private void OnMoveInput(Vector3 position)
{
if( !IsAnimating )
@@ -182,6 +190,7 @@ private void OnDestroy()
if (Parent != null && Parent.TryGetComponent(out ClientInputSender sender))
{
+ sender.ActionInputEvent -= OnActionInput;
sender.ClientMoveEvent -= OnMoveInput;
}
}
diff --git a/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs b/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs
index 0a415344d..fedd459d3 100644
--- a/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs
+++ b/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs
@@ -20,6 +20,7 @@ public class ClientInputSender : NetworkBehaviour
//upstream network conservation. This matters when holding down your mouse button to move.
private const float k_MoveSendRateSeconds = 0.05f; //20 fps.
+
private const float k_TargetMoveTimeout = 0.45f; //prevent moves for this long after targeting someone (helps prevent walking to the guy you clicked).
private float m_LastSentMove;
@@ -33,6 +34,11 @@ public class ClientInputSender : NetworkBehaviour
private NetworkCharacterState m_NetworkCharacter;
+ ///
+ /// This event fires at the time when an action request is sent to the server.
+ ///
+ public Action ActionInputEvent;
+
///
/// This describes how a skill was requested. Skills requested via mouse click will do raycasts to determine their target; skills requested
/// in other matters will use the stateful target stored in NetworkCharacterState.
@@ -216,6 +222,7 @@ private void PerformSkill(ActionType actionType, SkillTriggerStyle triggerStyle,
//Don't trigger our move logic for another 500ms. This protects us from moving just because we clicked on them to target them.
m_LastSentMove = Time.time + k_TargetMoveTimeout;
+ ActionInputEvent?.Invoke(playerAction);
m_NetworkCharacter.RecvDoActionServerRPC(playerAction);
}
else if(actionType != ActionType.GeneralTarget )
@@ -223,7 +230,10 @@ private void PerformSkill(ActionType actionType, SkillTriggerStyle triggerStyle,
// clicked on nothing... perform a "miss" attack on the spot they clicked on
var data = new ActionRequestData();
PopulateSkillRequest(k_CachedHit[0].point, actionType, ref data);
+
+ ActionInputEvent?.Invoke(data);
m_NetworkCharacter.RecvDoActionServerRPC(data);
+
}
}
diff --git a/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs b/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs
index e63394a58..2314999ee 100644
--- a/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs
+++ b/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs
@@ -99,6 +99,18 @@ public int HitPoints
public bool IsValidTarget { get { return NetworkLifeState.Value != LifeState.Dead; } }
+ ///
+ /// Returns true if the Character is currently in a state where it can play actions, false otherwise.
+ ///
+ public bool CanPerformActions
+ {
+ get
+ {
+ //TODO: stun state should be tracked in NetworkCharacterState, and then also checked here.
+ return NetworkLifeState.Value == LifeState.Alive;
+ }
+ }
+
///
/// The CharacterData object associated with this Character. This is the static game data that defines its attack skills, HP, etc.
///