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.
165 lines
8.7 KiB
C#
165 lines
8.7 KiB
C#
using System.Collections.Generic;
|
|
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
|
|
|
|
namespace Unity.BossRoom.Gameplay.Actions
|
|
{
|
|
/// <summary>
|
|
/// This is a companion class to ClientCharacter that is specifically responsible for visualizing Actions. Action visualizations have lifetimes
|
|
/// and ongoing state, making this class closely analogous in spirit to the Unity.Multiplayer.Samples.BossRoom.Actions.ServerActionPlayer class.
|
|
/// </summary>
|
|
public sealed class ClientActionPlayer
|
|
{
|
|
private List<Action> m_PlayingActions = new List<Action>();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private const float k_AnticipationTimeoutSeconds = 1;
|
|
|
|
public ClientCharacter ClientCharacter { get; private set; }
|
|
|
|
public ClientActionPlayer(ClientCharacter clientCharacter)
|
|
{
|
|
ClientCharacter = clientCharacter;
|
|
}
|
|
|
|
public void OnUpdate()
|
|
{
|
|
//do a reverse-walk so we can safely remove inside the loop.
|
|
for (int i = m_PlayingActions.Count - 1; i >= 0; --i)
|
|
{
|
|
var action = m_PlayingActions[i];
|
|
bool keepGoing = action.AnticipatedClient || action.OnUpdateClient(ClientCharacter); // only call OnUpdate() on actions that are past anticipation
|
|
bool expirable = action.Config.DurationSeconds > 0f; //non-positive value is a sentinel indicating the duration is indefinite.
|
|
bool timeExpired = expirable && action.TimeRunning >= action.Config.DurationSeconds;
|
|
bool timedOut = action.AnticipatedClient && action.TimeRunning >= k_AnticipationTimeoutSeconds;
|
|
if (!keepGoing || timeExpired || timedOut)
|
|
{
|
|
if (timedOut) { action.CancelClient(ClientCharacter); } //an anticipated action that timed out shouldn't get its End called. It is canceled instead.
|
|
else { action.EndClient(ClientCharacter); }
|
|
|
|
m_PlayingActions.RemoveAt(i);
|
|
ActionFactory.ReturnAction(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
//helper wrapper for a FindIndex call on m_PlayingActions.
|
|
private int FindAction(ActionID actionID, bool anticipatedOnly)
|
|
{
|
|
return m_PlayingActions.FindIndex(a => a.ActionID == actionID && (!anticipatedOnly || a.AnticipatedClient));
|
|
}
|
|
|
|
public void OnAnimEvent(string id)
|
|
{
|
|
foreach (var actionFX in m_PlayingActions)
|
|
{
|
|
actionFX.OnAnimEventClient(ClientCharacter, id);
|
|
}
|
|
}
|
|
|
|
public void OnStoppedChargingUp(float finalChargeUpPercentage)
|
|
{
|
|
foreach (var actionFX in m_PlayingActions)
|
|
{
|
|
actionFX.OnStoppedChargingUpClient(ClientCharacter, finalChargeUpPercentage);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on the client that owns the Character when the player triggers an action. This allows actions to immediately start playing feedback.
|
|
/// </summary>
|
|
/// <remarks>
|
|
///
|
|
/// 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.
|
|
///
|
|
/// </remarks>
|
|
/// <param name="data">The Action that is being requested.</param>
|
|
public void AnticipateAction(ref ActionRequestData data)
|
|
{
|
|
if (!ClientCharacter.IsAnimating() && Action.ShouldClientAnticipate(ClientCharacter, ref data))
|
|
{
|
|
var actionFX = ActionFactory.CreateActionFromData(ref data);
|
|
actionFX.AnticipateActionClient(ClientCharacter);
|
|
m_PlayingActions.Add(actionFX);
|
|
}
|
|
}
|
|
|
|
public void PlayAction(ref ActionRequestData data)
|
|
{
|
|
var anticipatedActionIndex = FindAction(data.ActionID, true);
|
|
|
|
var actionFX = anticipatedActionIndex >= 0 ? m_PlayingActions[anticipatedActionIndex] : ActionFactory.CreateActionFromData(ref data);
|
|
if (actionFX.OnStartClient(ClientCharacter))
|
|
{
|
|
if (anticipatedActionIndex < 0)
|
|
{
|
|
m_PlayingActions.Add(actionFX);
|
|
}
|
|
//otherwise just let the action sit in it's existing slot
|
|
}
|
|
else if (anticipatedActionIndex >= 0)
|
|
{
|
|
var removedAction = m_PlayingActions[anticipatedActionIndex];
|
|
m_PlayingActions.RemoveAt(anticipatedActionIndex);
|
|
ActionFactory.ReturnAction(removedAction);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels all playing ActionFX.
|
|
/// </summary>
|
|
public void CancelAllActions()
|
|
{
|
|
foreach (var action in m_PlayingActions)
|
|
{
|
|
action.CancelClient(ClientCharacter);
|
|
ActionFactory.ReturnAction(action);
|
|
}
|
|
m_PlayingActions.Clear();
|
|
}
|
|
|
|
public void CancelAllActionsWithSamePrototypeID(ActionID actionID)
|
|
{
|
|
for (int i = m_PlayingActions.Count - 1; i >= 0; --i)
|
|
{
|
|
if (m_PlayingActions[i].ActionID == actionID)
|
|
{
|
|
var action = m_PlayingActions[i];
|
|
action.CancelClient(ClientCharacter);
|
|
m_PlayingActions.RemoveAt(i);
|
|
ActionFactory.ReturnAction(action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|