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#

3 weeks ago
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);
}
}
}
}
}