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.
340 lines
17 KiB
C#
340 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Unity.BossRoom.Gameplay.GameplayObjects;
|
|
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
|
|
using Unity.BossRoom.VisualEffects;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
using BlockingMode = Unity.BossRoom.Gameplay.Actions.BlockingModeType;
|
|
|
|
namespace Unity.BossRoom.Gameplay.Actions
|
|
{
|
|
/// <summary>
|
|
/// The abstract parent class that all Actions derive from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The Action System is a generalized mechanism for Characters to "do stuff" in a networked way. Actions
|
|
/// include everything from your basic character attack, to a fancy skill like the Archer's Volley Shot, but also
|
|
/// include more mundane things like pulling a lever.
|
|
/// For every ActionLogic enum, there will be one specialization of this class.
|
|
/// There is only ever one active Action (also called the "blocking" action) at a time on a character, but multiple
|
|
/// Actions may exist at once, with subsequent Actions pending behind the currently active one, and possibly
|
|
/// "non-blocking" actions running in the background. See ActionPlayer.cs
|
|
///
|
|
/// The flow for Actions is:
|
|
/// Initially: Start()
|
|
/// Every frame: ShouldBecomeNonBlocking() (only if Action is blocking), then Update()
|
|
/// On shutdown: End() or Cancel()
|
|
/// After shutdown: ChainIntoNewAction() (only if Action was blocking, and only if End() was called, not Cancel())
|
|
///
|
|
/// Note also that if Start() returns false, no other functions are called on the Action, not even End().
|
|
///
|
|
/// This Action system has not been designed to be generic and extractable to be reused in other projects - keep that in mind when reading through this code.
|
|
/// A better action system would need to be more accessible and customizable by game designers and allow more design emergence. It'd have ways to define smaller atomic action steps and have a generic way to define and access character data. It would also need to be more performant, as actions would scale with your number of characters and concurrent actions.
|
|
/// </remarks>
|
|
public abstract class Action : ScriptableObject
|
|
{
|
|
/// <summary>
|
|
/// An index into the GameDataSource array of action prototypes. Set at runtime by GameDataSource class. If action is not itself a prototype - will contain the action id of the prototype reference.
|
|
/// This field is used to identify actions in a way that can be sent over the network.
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public ActionID ActionID;
|
|
|
|
/// <summary>
|
|
/// The default hit react animation; several different ActionFXs make use of this.
|
|
/// </summary>
|
|
public const string k_DefaultHitReact = "HitReact1";
|
|
|
|
|
|
protected ActionRequestData m_Data;
|
|
|
|
/// <summary>
|
|
/// Time when this Action was started (from Time.time) in seconds. Set by the ActionPlayer or ActionVisualization.
|
|
/// </summary>
|
|
public float TimeStarted { get; set; }
|
|
|
|
/// <summary>
|
|
/// How long the Action has been running (since its Start was called)--in seconds, measured via Time.time.
|
|
/// </summary>
|
|
public float TimeRunning { get { return (Time.time - TimeStarted); } }
|
|
|
|
/// <summary>
|
|
/// RequestData we were instantiated with. Value should be treated as readonly.
|
|
/// </summary>
|
|
public ref ActionRequestData Data => ref m_Data;
|
|
|
|
/// <summary>
|
|
/// Data Description for this action.
|
|
/// </summary>
|
|
public ActionConfig Config;
|
|
|
|
public bool IsChaseAction => ActionID == GameDataSource.Instance.GeneralChaseActionPrototype.ActionID;
|
|
public bool IsStunAction => ActionID == GameDataSource.Instance.StunnedActionPrototype.ActionID;
|
|
public bool IsGeneralTargetAction => ActionID == GameDataSource.Instance.GeneralTargetActionPrototype.ActionID;
|
|
|
|
/// <summary>
|
|
/// Constructor. The "data" parameter should not be retained after passing in to this method, because we take ownership of its internal memory.
|
|
/// Needs to be called by the ActionFactory.
|
|
/// </summary>
|
|
public void Initialize(ref ActionRequestData data)
|
|
{
|
|
m_Data = data;
|
|
ActionID = data.ActionID;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function resets the action before returning it to the pool
|
|
/// </summary>
|
|
public virtual void Reset()
|
|
{
|
|
m_Data = default;
|
|
ActionID = default;
|
|
TimeStarted = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the Action starts actually playing (which may be after it is created, because of queueing).
|
|
/// </summary>
|
|
/// <returns>false if the action decided it doesn't want to run after all, true otherwise. </returns>
|
|
public abstract bool OnStart(ServerCharacter serverCharacter);
|
|
|
|
|
|
/// <summary>
|
|
/// Called each frame while the action is running.
|
|
/// </summary>
|
|
/// <returns>true to keep running, false to stop. The Action will stop by default when its duration expires, if it has a duration set. </returns>
|
|
public abstract bool OnUpdate(ServerCharacter clientCharacter);
|
|
|
|
/// <summary>
|
|
/// Called each frame (before OnUpdate()) for the active ("blocking") Action, asking if it should become a background Action.
|
|
/// </summary>
|
|
/// <returns>true to become a non-blocking Action, false to remain a blocking Action</returns>
|
|
public virtual bool ShouldBecomeNonBlocking()
|
|
{
|
|
return Config.BlockingMode == BlockingModeType.OnlyDuringExecTime ? TimeRunning >= Config.ExecTimeSeconds : false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the Action ends naturally. By default just calls Cancel()
|
|
/// </summary>
|
|
public virtual void End(ServerCharacter serverCharacter)
|
|
{
|
|
Cancel(serverCharacter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will get called when the Action gets canceled. The Action should clean up any ongoing effects at this point.
|
|
/// (e.g. an Action that involves moving should cancel the current active move).
|
|
/// </summary>
|
|
public virtual void Cancel(ServerCharacter serverCharacter) { }
|
|
|
|
/// <summary>
|
|
/// Called *AFTER* End(). At this point, the Action has ended, meaning its Update() etc. functions will never be
|
|
/// called again. If the Action wants to immediately segue into a different Action, it can do so here. The new
|
|
/// Action will take effect in the next Update().
|
|
///
|
|
/// Note that this is not called on prematurely cancelled Actions, only on ones that have their End() called.
|
|
/// </summary>
|
|
/// <param name="newAction">the new Action to immediately transition to</param>
|
|
/// <returns>true if there's a new action, false otherwise</returns>
|
|
public virtual bool ChainIntoNewAction(ref ActionRequestData newAction) { return false; }
|
|
|
|
/// <summary>
|
|
/// Called on the active ("blocking") Action when this character collides with another.
|
|
/// </summary>
|
|
/// <param name="serverCharacter"></param>
|
|
/// <param name="collision"></param>
|
|
public virtual void CollisionEntered(ServerCharacter serverCharacter, Collision collision) { }
|
|
|
|
public enum BuffableValue
|
|
{
|
|
PercentHealingReceived, // unbuffed value is 1.0. Reducing to 0 would mean "no healing". 2 would mean "double healing"
|
|
PercentDamageReceived, // unbuffed value is 1.0. Reducing to 0 would mean "no damage". 2 would mean "double damage"
|
|
ChanceToStunTramplers, // unbuffed value is 0. If > 0, is the chance that someone trampling this character becomes stunned
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on all active Actions to give them a chance to alter the outcome of a gameplay calculation. Note
|
|
/// that this is used for both "buffs" (positive gameplay benefits) and "debuffs" (gameplay penalties).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// In a more complex game with lots of buffs and debuffs, this function might be replaced by a separate
|
|
/// BuffRegistry component. This would let you add fancier features, such as defining which effects
|
|
/// "stack" with other ones, and could provide a UI that lists which are affecting each character
|
|
/// and for how long.
|
|
/// </remarks>
|
|
/// <param name="buffType">Which gameplay variable being calculated</param>
|
|
/// <param name="orgValue">The original ("un-buffed") value</param>
|
|
/// <param name="buffedValue">The final ("buffed") value</param>
|
|
public virtual void BuffValue(BuffableValue buffType, ref float buffedValue) { }
|
|
|
|
/// <summary>
|
|
/// Static utility function that returns the default ("un-buffed") value for a BuffableValue.
|
|
/// (This just ensures that there's one place for all these constants.)
|
|
/// </summary>
|
|
public static float GetUnbuffedValue(Action.BuffableValue buffType)
|
|
{
|
|
switch (buffType)
|
|
{
|
|
case BuffableValue.PercentDamageReceived: return 1;
|
|
case BuffableValue.PercentHealingReceived: return 1;
|
|
case BuffableValue.ChanceToStunTramplers: return 0;
|
|
default: throw new System.Exception($"Unknown buff type {buffType}");
|
|
}
|
|
}
|
|
|
|
public enum GameplayActivity
|
|
{
|
|
AttackedByEnemy,
|
|
Healed,
|
|
StoppedChargingUp,
|
|
UsingAttackAction, // called immediately before we perform the attack Action
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on active Actions to let them know when a notable gameplay event happens.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When a GameplayActivity of AttackedByEnemy or Healed happens, OnGameplayAction() is called BEFORE BuffValue() is called.
|
|
/// </remarks>
|
|
/// <param name="serverCharacter"></param>
|
|
/// <param name="activityType"></param>
|
|
public virtual void OnGameplayActivity(ServerCharacter serverCharacter, GameplayActivity activityType) { }
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// True if this actionFX began running immediately, prior to getting a confirmation from the server.
|
|
/// </summary>
|
|
public bool AnticipatedClient { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Starts the ActionFX. Derived classes may return false if they wish to end immediately without their Update being called.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Derived class should be sure to call base.OnStart() in their implementation, but note that this resets "Anticipated" to false.
|
|
/// </remarks>
|
|
/// <returns>true to play, false to be immediately cleaned up.</returns>
|
|
public virtual bool OnStartClient(ClientCharacter clientCharacter)
|
|
{
|
|
AnticipatedClient = false; //once you start for real you are no longer an anticipated action.
|
|
TimeStarted = UnityEngine.Time.time;
|
|
return true;
|
|
}
|
|
|
|
public virtual bool OnUpdateClient(ClientCharacter clientCharacter)
|
|
{
|
|
return ActionConclusion.Continue;
|
|
}
|
|
/// <summary>
|
|
/// End is always called when the ActionFX finishes playing. This is a good place for derived classes to put
|
|
/// wrap-up logic (perhaps playing the "puff of smoke" that rises when a persistent fire AOE goes away). Derived
|
|
/// classes should aren't required to call base.End(); by default, the method just calls 'Cancel', to handle the
|
|
/// common case where Cancel and End do the same thing.
|
|
/// </summary>
|
|
public virtual void EndClient(ClientCharacter clientCharacter)
|
|
{
|
|
CancelClient(clientCharacter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel is called when an ActionFX is interrupted prematurely. It is kept logically distinct from End to allow
|
|
/// for the possibility that an Action might want to play something different if it is interrupted, rather than
|
|
/// completing. For example, a "ChargeShot" action might want to emit a projectile object in its End method, but
|
|
/// instead play a "Stagger" animation in its Cancel method.
|
|
/// </summary>
|
|
public virtual void CancelClient(ClientCharacter clientCharacter) { }
|
|
|
|
/// <summary>
|
|
/// Should this ActionFX be created anticipatively on the owning client?
|
|
/// </summary>
|
|
/// <param name="clientCharacter">The ActionVisualization that would be playing this ActionFX.</param>
|
|
/// <param name="data">The request being sent to the server</param>
|
|
/// <returns>If true ActionVisualization should pre-emptively create the ActionFX on the owning client, before hearing back from the server.</returns>
|
|
public static bool ShouldClientAnticipate(ClientCharacter clientCharacter, ref ActionRequestData data)
|
|
{
|
|
if (!clientCharacter.CanPerformActions) { return false; }
|
|
|
|
var actionDescription = GameDataSource.Instance.GetActionPrototypeByID(data.ActionID).Config;
|
|
|
|
//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 (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out NetworkObject networkObject))
|
|
{
|
|
float rangeSquared = actionDescription.Range * actionDescription.Range;
|
|
isTargetEligible = (networkObject.transform.position - clientCharacter.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the visualization receives an animation event.
|
|
/// </summary>
|
|
public virtual void OnAnimEventClient(ClientCharacter clientCharacter, string id) { }
|
|
|
|
/// <summary>
|
|
/// Called when this action has finished "charging up". (Which is only meaningful for a
|
|
/// few types of actions -- it is not called for other actions.)
|
|
/// </summary>
|
|
/// <param name="finalChargeUpPercentage"></param>
|
|
public virtual void OnStoppedChargingUpClient(ClientCharacter clientCharacter, float finalChargeUpPercentage) { }
|
|
|
|
/// <summary>
|
|
/// Utility function that instantiates all the graphics in the Spawns list.
|
|
/// If parentToOrigin is true, the new graphics are parented to the origin Transform.
|
|
/// If false, they are positioned/oriented the same way but are not parented.
|
|
/// </summary>
|
|
protected List<SpecialFXGraphic> InstantiateSpecialFXGraphics(Transform origin, bool parentToOrigin)
|
|
{
|
|
var returnList = new List<SpecialFXGraphic>();
|
|
foreach (var prefab in Config.Spawns)
|
|
{
|
|
if (!prefab) { continue; } // skip blank entries in our prefab list
|
|
returnList.Add(InstantiateSpecialFXGraphic(prefab, origin, parentToOrigin));
|
|
}
|
|
return returnList;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Utility function that instantiates one of the graphics from the Spawns list.
|
|
/// If parentToOrigin is true, the new graphics are parented to the origin Transform.
|
|
/// If false, they are positioned/oriented the same way but are not parented.
|
|
/// </summary>
|
|
protected SpecialFXGraphic InstantiateSpecialFXGraphic(GameObject prefab, Transform origin, bool parentToOrigin)
|
|
{
|
|
if (prefab.GetComponent<SpecialFXGraphic>() == null)
|
|
{
|
|
throw new System.Exception($"One of the Spawns on action {this.name} does not have a SpecialFXGraphic component and can't be instantiated!");
|
|
}
|
|
var graphicsGO = GameObject.Instantiate(prefab, origin.transform.position, origin.transform.rotation, (parentToOrigin ? origin.transform : null));
|
|
return graphicsGO.GetComponent<SpecialFXGraphic>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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!
|
|
/// </summary>
|
|
public virtual void AnticipateActionClient(ClientCharacter clientCharacter)
|
|
{
|
|
AnticipatedClient = true;
|
|
TimeStarted = UnityEngine.Time.time;
|
|
|
|
if (!string.IsNullOrEmpty(Config.AnimAnticipation))
|
|
{
|
|
clientCharacter.OurAnimator.SetTrigger(Config.AnimAnticipation);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|