浏览代码

ActionVisualization and MeleeActionFX (#40)

-- ActionVisualization and ActionFX base classes added.
-- MeleeActionFX logic added, and some logic to only play the hit react if the target is still in range.
-- folded "Level" into ActionRequestData. (The server takes care to sanitize the field if a request comes from the client).
-- hit detection logic has moved to a shared ActionUtils class. I wound up not making use of it in this change, but I still think it's likely this logic will need to be shared between server and client, or between different actions on the server. (It will also likely grow more complex).
-- Added a BrainEnabled switch. This is mainly intended for debugging, although it's possible it could be part of a "Stun" effect. Switch it to false to turn monsters into helpless punching bags.
-- split off some of Action into a shared ActionBase class. This is because Action and ActionFX ended up sharing several concepts.
-- Changed server-duration to match the animat...
/main
GitHub 4 年前
当前提交
645f009c
共有 28 个文件被更改,包括 589 次插入227 次删除
  1. 25
      Assets/BossRoom/Models/BossSetController.controller
  2. 27
      Assets/BossRoom/Models/CharacterSet.fbx.meta
  3. 41
      Assets/BossRoom/Models/CharacterSetController.controller
  4. 3
      Assets/BossRoom/Prefabs/Character/Boss.prefab
  5. 3
      Assets/BossRoom/Prefabs/Player.prefab
  6. 73
      Assets/BossRoom/Scripts/Client/ClientCharacterVisualization.cs
  7. 9
      Assets/BossRoom/Scripts/Client/ClientInputSender.cs
  8. 23
      Assets/BossRoom/Scripts/Server/Game/Action/Action.cs
  9. 52
      Assets/BossRoom/Scripts/Server/Game/Action/ActionPlayer.cs
  10. 2
      Assets/BossRoom/Scripts/Server/Game/Action/ChaseAction.cs
  11. 35
      Assets/BossRoom/Scripts/Server/Game/Action/MeleeAction.cs
  12. 6
      Assets/BossRoom/Scripts/Server/Game/Action/ReviveAction.cs
  13. 56
      Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs
  14. 81
      Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs
  15. 15
      Assets/BossRoom/Scripts/Server/ServerCharacterMovement.cs
  16. 17
      Assets/BossRoom/Scripts/Shared/Game/Action/ActionRequestData.cs
  17. 8
      Assets/BossRoom/Scripts/Client/Game/Action.meta
  18. 42
      Assets/BossRoom/Scripts/Shared/Game/Action/ActionBase.cs
  19. 11
      Assets/BossRoom/Scripts/Shared/Game/Action/ActionBase.cs.meta
  20. 40
      Assets/BossRoom/Scripts/Shared/Game/Action/ActionUtils.cs
  21. 11
      Assets/BossRoom/Scripts/Shared/Game/Action/ActionUtils.cs.meta
  22. 66
      Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs
  23. 11
      Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs.meta
  24. 67
      Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs
  25. 11
      Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs.meta
  26. 70
      Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs
  27. 11
      Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs.meta

25
Assets/BossRoom/Models/BossSetController.controller


m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 6
m_ConditionEvent: AttackID
m_EventTreshold: 1
m_ConditionEvent: BeginAttack
m_ConditionEvent: Attack1
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 6299315480180736668}

serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionDuration: 0
m_ExitTime: 0.75
m_ExitTime: 0.0000000010521221
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0

m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 9100000}
m_Controller: {fileID: 0}
m_Controller: {fileID: 9100000}
- m_Name: AttackID
m_Type: 3
m_Controller: {fileID: 0}
- m_Name: Attack1
m_Type: 9
m_Controller: {fileID: 9100000}
- m_Name: BeginAttack
m_Controller: {fileID: 0}
- m_Name: HitReact1
m_Controller: {fileID: 9100000}
m_Controller: {fileID: 0}
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer

m_Position: {x: 240, y: 90, z: 0}
- serializedVersion: 1
m_State: {fileID: 6299315480180736668}
m_Position: {x: 40, y: 220, z: 0}
m_Position: {x: 240, y: 260, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []

27
Assets/BossRoom/Models/CharacterSet.fbx.meta


mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
events:
- time: 0.40376437
functionName: OnAnimEvent
data: impact
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}

mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
events:
- time: 0.53248686
functionName: OnAnimEvent
data: impact
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}

mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
events:
- time: 0.40753868
functionName: OnAnimEvent
data: impact
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}

41
Assets/BossRoom/Models/CharacterSetController.controller


m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: HitReact
m_Name: HitReact1
m_Speed: 1
m_CycleOffset: 0
m_Transitions:

m_Name:
m_Conditions:
- m_ConditionMode: 1
m_ConditionEvent: BeginHitReact
m_ConditionEvent: HitReact1
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: -7345246596832709857}

m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 2.5632382e-10
m_HasExitTime: 1
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1

m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 6
m_ConditionEvent: AttackID
m_EventTreshold: 1
m_ConditionEvent: BeginAttack
m_ConditionEvent: Attack1
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 6299315480180736668}

serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionDuration: 0.0000000037252903
m_ExitTime: 0.75
m_ExitTime: 0.016478343
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0

m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 0}
- m_Name: AttackID
m_Type: 3
- m_Name: Attack1
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 0}
- m_Name: HitReact1
m_Type: 9
- m_Name: BeginAttack
- m_Name: FallDown
- m_Name: BeginHitReact
- m_Name: BeginRevive
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 0}
- m_Name: Dead
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0
m_DefaultBool: 0
m_Controller: {fileID: 0}
- m_Name: StandUp
m_Type: 9
m_DefaultFloat: 0
m_DefaultInt: 0

3
Assets/BossRoom/Prefabs/Character/Boss.prefab


m_GameObject: {fileID: 6839301660383890230}
m_Enabled: 1
m_Avatar: {fileID: 9000000, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3}
m_Controller: {fileID: 9100000, guid: 259c0c92d6badb54bb032df749609d27, type: 2}
m_Controller: {fileID: 9100000, guid: 81ba9d484ee6d174a8aeb3af411babc4, type: 2}
m_CullingMode: 1
m_UpdateMode: 0
m_ApplyRootMotion: 0

m_Script: {fileID: 11500000, guid: 9520a47fc61d5ab4ca99cdac2d574909, type: 3}
m_Name:
m_EditorClassIdentifier:
m_ClientVisualsAnimator: {fileID: 0}
MinZoomDistance: 3
MaxZoomDistance: 30
ZoomSpeed: 3

3
Assets/BossRoom/Prefabs/Player.prefab


m_Name:
m_EditorClassIdentifier:
HitPoints:
InternalValue: 1000000
InternalValue: 250
Mana:
InternalValue: 10
--- !u!114 &4600110157238723776

m_EditorClassIdentifier:
IsNPC: 0
DetectRange: 10
BrainEnabled: 1
--- !u!114 &7690172137830037487
MonoBehaviour:
m_ObjectHideFlags: 0

73
Assets/BossRoom/Scripts/Client/ClientCharacterVisualization.cs


using System;
using System;
using UnityEngine;
namespace BossRoom.Visual

public Animator OurAnimator { get { return m_ClientVisualsAnimator; } }
private ActionVisualization m_ActionViz;
private Transform m_Parent;
public Transform Parent { get; private set; }
public float MinZoomDistance = 3;
public float MaxZoomDistance = 30;

private const float x_MaxRotSpeed = 280; //max angular speed at which we will rotate, in degrees/second.
private const float k_MaxRotSpeed = 280; //max angular speed at which we will rotate, in degrees/second.
public void Start()
{
m_ActionViz = new ActionVisualization(this);
}
/// <inheritdoc />
public override void NetworkStart()

m_NetState = this.transform.parent.gameObject.GetComponent<NetworkCharacterState>();
m_NetState.DoActionEventClient += this.PerformActionFX;
m_NetState.NetworkLifeState.OnValueChanged += OnLifeStateChanged;
m_Parent = transform.parent;
m_Parent.GetComponent<BossRoom.Client.ClientCharacter>().ChildVizObject = this;
Parent = transform.parent;
Parent.GetComponent<BossRoom.Client.ClientCharacter>().ChildVizObject = this;
if (IsLocalPlayer)
{
AttachCamera();

private void PerformActionFX(ActionRequestData data)
{
//TODO: [GOMPS-13] break this method out into its own class, so we can drive multi-frame graphical effects.
//FIXME: [GOMPS-13] hook this up to information in the ActionDescription.
switch (data.ActionTypeEnum)
{
case ActionType.TANK_BASEATTACK:
m_ClientVisualsAnimator.SetInteger("AttackID", 1);
m_ClientVisualsAnimator.SetTrigger("BeginAttack");
if (data.TargetIds != null && data.TargetIds.Length > 0)
{
NetworkedObject targetObject = MLAPI.Spawning.SpawnManager.SpawnedObjects[data.TargetIds[0]];
if (targetObject != null)
{
var targetAnimator = targetObject.GetComponent<BossRoom.Client.ClientCharacter>().ChildVizObject.OurAnimator;
if (targetAnimator != null)
{
targetAnimator.SetTrigger("BeginHitReact");
}
}
}
break;
case ActionType.ARCHER_BASEATTACK:
break;
case ActionType.GENERAL_CHASE:
break;
case ActionType.GENERAL_REVIVE:
m_ClientVisualsAnimator.SetTrigger("BeginRevive");
break;
default:
throw new ArgumentOutOfRangeException();
}
m_ActionViz.PlayAction(ref data);
}
private void OnLifeStateChanged(LifeState previousValue, LifeState newValue)

void Update()
{
if (m_Parent == null)
if (Parent == null)
{
//since we aren't in the transform hierarchy, we have to explicitly die when our parent dies.
GameObject.Destroy(this.gameObject);

m_ClientVisualsAnimator.SetFloat("Speed", m_NetState.NetworkMovementSpeed.Value);
}
m_ActionViz.Update();
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (scroll != 0 && m_MainCamera)
{

}
public void OnAnimEvent(string id)
{
//if you are trying to figure out who calls this method, it's "magic". The Unity Animation Event system takes method names as strings,
//and calls a method of the same name on a component on the same GameObject as the Animator. See the "attack1" Animation Clip as one
//example of where this is configured.
m_ActionViz.OnAnimEvent(id);
}
var posDiff = m_Parent.transform.position - transform.position;
var angleDiff = Quaternion.Angle(m_Parent.transform.rotation, transform.rotation);
var posDiff = Parent.transform.position - transform.position;
var angleDiff = Quaternion.Angle(Parent.transform.rotation, transform.rotation);
float timeDelta = Time.deltaTime;

if (angleDiff > 0)
{
float maxAngleMove = timeDelta * x_MaxRotSpeed;
float maxAngleMove = timeDelta * k_MaxRotSpeed;
transform.rotation = Quaternion.Slerp(transform.rotation, m_Parent.transform.rotation, t);
transform.rotation = Quaternion.Slerp(transform.rotation, Parent.transform.rotation, t);
}
}

9
Assets/BossRoom/Scripts/Client/ClientInputSender.cs


void Awake()
{
m_NpcLayerMask = LayerMask.GetMask("NPCs");
m_NpcLayerMask = LayerMask.NameToLayer("NPCs");
m_NetworkCharacter = GetComponent<NetworkCharacterState>();
}

var revive_data = new ActionRequestData();
revive_data.ShouldQueue = true;
revive_data.ActionTypeEnum = ActionType.GENERAL_REVIVE;
revive_data.TargetIds = new [] {GetTargetObject(ref hit)};
revive_data.TargetIds = new[] { GetTargetObject(ref hit) };
m_NetworkCharacter.ClientSendActionRequest(ref revive_data);
}
}

/// <summary>
/// Gets the Target NetworkId from the Raycast hit, or 0 if Raycast didn't contact a Networked Object.
/// </summary>
private ulong GetTargetObject(ref RaycastHit hit )
private ulong GetTargetObject(ref RaycastHit hit)
if (targetObj == null) { return 0; }
if (targetObj == null) { return 0; }
return targetObj.NetworkId;
}

23
Assets/BossRoom/Scripts/Server/Game/Action/Action.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BossRoom.Server

{
protected ServerCharacter m_Parent;
/// <summary>
/// The level this action plays back at. e.g. a weak "level 0" melee attack, vs a strong "level 3" melee attack.
/// </summary>
protected int m_Level;
protected ActionRequestData m_Data;
/// <summary>

get
{
var list = ActionData.ActionDescriptions[Data.ActionTypeEnum];
int level = Mathf.Min(m_Level, list.Count - 1); //if we don't go up to the requested level, just cap at the max level.
int level = Mathf.Min(Data.Level, list.Count - 1); //if we don't go up to the requested level, just cap at the max level.
return list[level];
}
}

/// </summary>
public Action(ServerCharacter parent, ref ActionRequestData data, int level)
public Action(ServerCharacter parent, ref ActionRequestData data)
m_Level = level;
m_Data.Level = level;
}
/// <summary>

/// <param name="data">the data to instantiate this skill from. </param>
/// <param name="level">the level to play the skill at. </param>
/// <returns>the newly created action. </returns>
public static Action MakeAction(ServerCharacter parent, ref ActionRequestData data, int level )
public static Action MakeAction(ServerCharacter parent, ref ActionRequestData data)
switch(logic)
switch (logic)
case ActionLogic.MELEE: return new MeleeAction(parent, ref data, level);
case ActionLogic.CHASE: return new ChaseAction(parent, ref data, level);
case ActionLogic.REVIVE: return new ReviveAction(parent, ref data, level);
case ActionLogic.MELEE: return new MeleeAction(parent, ref data);
case ActionLogic.CHASE: return new ChaseAction(parent, ref data);
case ActionLogic.REVIVE: return new ReviveAction(parent, ref data);
default: throw new System.NotImplementedException();
}
}

52
Assets/BossRoom/Scripts/Server/Game/Action/ActionPlayer.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// </summary>
public class ActionPlayer
{
private ServerCharacter m_parent;
private ServerCharacter m_Parent;
private List<Action> m_queue;
private List<Action> m_Queue;
public ActionPlayer(ServerCharacter parent )
public ActionPlayer(ServerCharacter parent)
m_parent = parent;
m_queue = new List<Action>();
m_Parent = parent;
m_Queue = new List<Action>();
public void PlayAction(ref ActionRequestData data )
public void PlayAction(ref ActionRequestData data)
if( !data.ShouldQueue )
if (!data.ShouldQueue)
int level = 0; //todo, get this from parent's networked vars, maybe.
var new_action = Action.MakeAction(m_parent, ref data, level);
var new_action = Action.MakeAction(m_Parent, ref data);
bool was_empty = m_queue.Count == 0;
m_queue.Add(new_action);
if( was_empty )
bool wasEmpty = m_Queue.Count == 0;
m_Queue.Add(new_action);
if (wasEmpty)
{
AdvanceQueue(false);
}

{
if( m_queue.Count > 0 )
if (m_Queue.Count > 0)
m_queue[0].Cancel();
m_Queue[0].Cancel();
m_queue.Clear();
m_Queue.Clear();
}
/// <summary>

{
if (m_queue.Count > 0)
if (m_Queue.Count > 0)
data = m_queue[ 0 ].Data;
data = m_Queue[0].Data;
return true;
}
else

/// <param name="expireFirstElement">Pass true to remove the first element and advance to the next element. Pass false to "advance" to the 0th element</param>
private void AdvanceQueue(bool expireFirstElement)
{
if( expireFirstElement && m_queue.Count > 0 )
if (expireFirstElement && m_Queue.Count > 0)
m_queue.RemoveAt(0);
m_Queue.RemoveAt(0);
if( m_queue.Count > 0 )
if (m_Queue.Count > 0)
m_queue[0].TimeStarted = Time.time;
bool play = m_queue[0].Start();
if( !play )
m_Queue[0].TimeStarted = Time.time;
bool play = m_Queue[0].Start();
if (!play)
{
AdvanceQueue(true);
}

public void Update()
{
if( this.m_queue.Count > 0 )
if (m_Queue.Count > 0)
Action runningAction = m_queue[0]; //action at the front of the queue is the one that is actively running.
Action runningAction = m_Queue[0]; //action at the front of the queue is the one that is actively running.
if ( !keepGoing || timeExpired )
if (!keepGoing || timeExpired)
{
AdvanceQueue(true);
}

2
Assets/BossRoom/Scripts/Server/Game/Action/ChaseAction.cs


private Vector3 m_CurrentTargetPos;
public ChaseAction(ServerCharacter parent, ref ActionRequestData data, int level) : base(parent, ref data, level)
public ChaseAction(ServerCharacter parent, ref ActionRequestData data) : base(parent, ref data)
{
}

35
Assets/BossRoom/Scripts/Server/Game/Action/MeleeAction.cs


/// </remarks>
public class MeleeAction : Action
{
private bool m_ExecFired;
private bool m_ExecutionFired;
private ulong m_ProvisionalTarget;
private static RaycastHit[] s_Hits;
private const int k_MaxDetects = 4;
private ulong m_ProvisionalTarget;
public MeleeAction(ServerCharacter parent, ref ActionRequestData data, int level) : base(parent, ref data, level)
public MeleeAction(ServerCharacter parent, ref ActionRequestData data) : base(parent, ref data)
if (s_Hits == null)
{
s_Hits = new RaycastHit[k_MaxDetects];
}
}
public override bool Start()

public override bool Update()
{
if (!m_ExecFired && (Time.time - TimeStarted) >= Description.ExecTime_s)
if (!m_ExecutionFired && (Time.time - TimeStarted) >= Description.ExecTime_s)
m_ExecFired = true;
m_ExecutionFired = true;
foe.RecieveHP(this.m_Parent, -Description.Amount);
foe.ReceiveHP(this.m_Parent, -Description.Amount);
/// <summary>

private ServerCharacter DetectFoe(ulong foeHint = 0)
{
//this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack.
//this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack.
var myBounds = this.m_Parent.GetComponent<Collider>().bounds;
//NPCs (monsters) can hit PCs, and vice versa. No friendly fire allowed on either side.
int mask = LayerMask.GetMask(m_Parent.IsNPC ? "PCs" : "NPCs");
int numResults = Physics.BoxCastNonAlloc(m_Parent.transform.position, myBounds.extents,
m_Parent.transform.forward, s_Hits, Quaternion.identity, Description.Range, mask);
RaycastHit[] results;
int numResults = ActionUtils.DetectMeleeFoe(m_Parent.IsNPC, m_Parent.GetComponent<Collider>(), Description, out results);
ServerCharacter foundFoe = s_Hits[0].collider.GetComponent<ServerCharacter>();
ServerCharacter foundFoe = results[0].collider.GetComponent<ServerCharacter>();
var serverChar = s_Hits[i].collider.GetComponent<ServerCharacter>();
var serverChar = results[i].collider.GetComponent<ServerCharacter>();
if (serverChar.NetworkId == foeHint)
{
foundFoe = serverChar;

6
Assets/BossRoom/Scripts/Server/Game/Action/ReviveAction.cs


private bool m_ExecFired;
private ServerCharacter m_TargetCharacter;
public ReviveAction(ServerCharacter parent, ref ActionRequestData data, int level) : base(parent, ref data, level)
public ReviveAction(ServerCharacter parent, ref ActionRequestData data) : base(parent, ref data)
{
}

if (m_TargetCharacter.NetState.NetworkLifeState.Value == LifeState.FAINTED)
{
m_TargetCharacter.Revive(m_Parent, (int) m_Data.Amount);
m_TargetCharacter.Revive(m_Parent, (int)m_Data.Amount);
}
else
{

return true;
}
}
}
}

56
Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

IDLE,
}
private ServerCharacter m_serverCharacter;
private ActionPlayer m_actionPlayer;
private AIStateType m_currentState;
private Dictionary<AIStateType, AIState> m_logics;
private List<ServerCharacter> m_hatedEnemies;
private ServerCharacter m_ServerCharacter;
private ActionPlayer m_ActionPlayer;
private AIStateType m_CurrentState;
private Dictionary<AIStateType, AIState> m_Logics;
private List<ServerCharacter> m_HatedEnemies;
m_serverCharacter = me;
m_actionPlayer = myActionPlayer;
m_ServerCharacter = me;
m_ActionPlayer = myActionPlayer;
m_logics = new Dictionary<AIStateType, AIState>
m_Logics = new Dictionary<AIStateType, AIState>
[ AIStateType.IDLE ] = new IdleAIState(this),
[AIStateType.IDLE] = new IdleAIState(this),
[ AIStateType.ATTACK ] = new AttackAIState(this, m_actionPlayer),
[AIStateType.ATTACK] = new AttackAIState(this, m_ActionPlayer),
m_hatedEnemies = new List<ServerCharacter>();
m_currentState = AIStateType.IDLE;
m_HatedEnemies = new List<ServerCharacter>();
m_CurrentState = AIStateType.IDLE;
}
/// <summary>

{
AIStateType newState = FindBestEligibleAIState();
if (m_currentState != newState)
if (m_CurrentState != newState)
m_logics[ newState ].Initialize();
m_Logics[newState].Initialize();
m_currentState = newState;
m_logics[ m_currentState ].Update();
m_CurrentState = newState;
m_Logics[m_CurrentState].Update();
}
private AIStateType FindBestEligibleAIState()

foreach (AIStateType type in Enum.GetValues(typeof(AIStateType)))
{
if (m_logics[ type ].IsEligible())
if (m_Logics[type].IsEligible())
{
return type;
}

}
#region Functions for AIStateLogics
if (potentialFoe == null || potentialFoe.IsNPC)
if (potentialFoe == null || potentialFoe.IsNPC || potentialFoe.NetState.NetworkLifeState.Value != LifeState.ALIVE)
// FIXME: check for dead!
// Also, we could use NavMesh.Raycast() to see if we have line of sight to foe?
return true;
}

/// <param name="character"></param>
public void Hate(ServerCharacter character)
{
if (!m_hatedEnemies.Contains(character))
if (!m_HatedEnemies.Contains(character))
m_hatedEnemies.Add(character);
m_HatedEnemies.Add(character);
}
}

public List<ServerCharacter> GetHatedEnemies()
{
// first we clean the list -- remove any enemies that have disappeared (became null), are dead, etc.
m_hatedEnemies.RemoveAll(enemy => !IsAppropriateFoe(enemy));
return m_hatedEnemies;
m_HatedEnemies.RemoveAll(enemy => !IsAppropriateFoe(enemy));
return m_HatedEnemies;
}
/// <summary>

public ServerCharacter GetMyServerCharacter()
{
return m_serverCharacter;
return m_ServerCharacter;
#endregion
}
}

81
Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs


using System;
using System.Collections;
[RequireComponent(typeof(NetworkCharacterState))]
[RequireComponent(typeof(ServerCharacterMovement), typeof(NetworkCharacterState))]
public class ServerCharacter : MLAPI.NetworkedBehaviour
{
public NetworkCharacterState NetState { get; private set; }

[Tooltip("If IsNPC, this is how far the npc can detect others (in meters)")]
public float DetectRange = 10;
private ActionPlayer m_actionPlayer;
private AIBrain m_aiBrain;
[SerializeField]
[Tooltip("If set to false, an NPC character will be denied its brain (won't attack or chase players)")]
private bool m_BrainEnabled = true;
private ActionPlayer m_ActionPlayer;
private AIBrain m_AIBrain;
private static List<ServerCharacter> g_activeServerCharacters = new List<ServerCharacter>();
private static List<ServerCharacter> s_ActiveServerCharacters = new List<ServerCharacter>();
g_activeServerCharacters.Add(this);
s_ActiveServerCharacters.Add(this);
g_activeServerCharacters.Remove(this);
s_ActiveServerCharacters.Remove(this);
return g_activeServerCharacters;
return s_ActiveServerCharacters;
// Start is called before the first frame update
m_actionPlayer = new ActionPlayer(this);
m_ActionPlayer = new ActionPlayer(this);
m_aiBrain = new AIBrain(this, m_actionPlayer);
m_AIBrain = new AIBrain(this, m_ActionPlayer);
if (!IsServer) { this.enabled = false; }
if (!IsServer) { enabled = false; }
this.NetState = GetComponent<NetworkCharacterState>();
this.NetState.DoActionEventServer += this.OnActionPlayRequest;
NetState = GetComponent<NetworkCharacterState>();
NetState.DoActionEventServer += OnActionPlayRequest;
NetState.OnReceivedClientInput += OnClientMoveRequest;
NetState.NetworkLifeState.OnValueChanged += OnLifeStateChanged;
}
}

/// <param name="data">Contains all data necessary to create the action</param>
public void PlayAction(ref ActionRequestData data )
public void PlayAction(ref ActionRequestData data)
if (!IsNPC)
{
//Can't trust the client! If this was a human request, make sure the Level of the skill being played is correct.
data.Level = 0;
}
this.m_actionPlayer.PlayAction(ref data);
//Can't trust the client! If this was a human request, make sure the Level of the skill being played is correct.
this.m_ActionPlayer.PlayAction(ref data);
}
}
private void OnClientMoveRequest(Vector3 targetPosition)
{
if (NetState.NetworkLifeState.Value == LifeState.ALIVE)
{
ClearActions();
GetComponent<ServerCharacterMovement>().SetMovementTarget(targetPosition);
}
}
private void OnLifeStateChanged(LifeState prevLifeState, LifeState lifeState)
{
if (lifeState != LifeState.ALIVE)
{
ClearActions();
GetComponent<ServerCharacterMovement>().CancelMove();
}
}

public void ClearActions()
{
this.m_actionPlayer.ClearActions();
this.m_ActionPlayer.ClearActions();
private void OnActionPlayRequest( ActionRequestData data )
private void OnActionPlayRequest(ActionRequestData data)
{
this.PlayAction(ref data);
}

/// </summary>
/// <param name="Inflicter">Person dishing out this damage/healing. Can be null. </param>
/// <param name="HP">The HP to receive. Positive value is healing. Negative is damage. </param>
public void RecieveHP( ServerCharacter inflicter, int HP)
public void ReceiveHP(ServerCharacter inflicter, int HP)
{
//in a more complicated implementation, we might look up all sorts of effects from the inflicter, and compare them
//to our own effects, and modify the damage or healing as appropriate. But in this game, we just take it straight.

//we can't currently heal a dead character back to Alive state.
//that's handled by a separate function.
if( NetState.HitPoints.Value <= 0 )
if (NetState.HitPoints.Value <= 0)
if (IsNPC)
{
NetState.NetworkLifeState.Value = LifeState.DEAD;

NetState.NetworkLifeState.Value = LifeState.ALIVE;
}
}
// Update is called once per frame
m_actionPlayer.Update();
if (m_aiBrain != null && NetState.NetworkLifeState.Value == LifeState.ALIVE)
m_ActionPlayer.Update();
if (m_AIBrain != null && NetState.NetworkLifeState.Value == LifeState.ALIVE && m_BrainEnabled)
m_aiBrain.Update();
m_AIBrain.Update();
}
}
}

15
Assets/BossRoom/Scripts/Server/ServerCharacterMovement.cs


using MLAPI;
using System.Linq;
using MLAPI;
using UnityEngine;
using UnityEngine.AI;

private NavMeshPath m_DesiredMovementPath;
private MovementState m_MovementState;
private ServerCharacter m_CharLogic;
[SerializeField]
private float m_MovementSpeed; // TODO [GOMPS-86] this should be assigned based on character definition

m_NavMeshAgent = GetComponent<NavMeshAgent>();
m_NetworkCharacterState = GetComponent<NetworkCharacterState>();
m_CharLogic = GetComponent<ServerCharacter>();
m_Rigidbody = GetComponent<Rigidbody>();
}

// On the server enable navMeshAgent and initialize
m_NavMeshAgent.enabled = true;
m_NetworkCharacterState.OnReceivedClientInput += OnReceivedClientInput;
}
private void OnReceivedClientInput(Vector3 position )
{
m_CharLogic.ClearActions(); //a fresh movement request trumps whatever we were doing before.
SetMovementTarget(position);
}
/// <summary>

m_NavMeshAgent.CalculatePath(corners[corners.Length - 1], m_DesiredMovementPath);
}
}
}
}

17
Assets/BossRoom/Scripts/Shared/Game/Action/ActionRequestData.cs


{
{ ActionType.TANK_BASEATTACK , new List<ActionDescription>
{
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=10, ManaCost=2, ExecTime_s=0.3f, Duration_s=0.5f, Range=2f, Anim="Todo" } }, //level 1
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=15, ManaCost=2, ExecTime_s=0.3f, Duration_s=0.5f, Range=2f, Anim="Todo" } }, //level 2
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=20, ManaCost=2, ExecTime_s=0.3f, Duration_s=0.5f, Range=2f, Anim="Todo" } }, //level 3
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=30, ManaCost=2, ExecTime_s=0.3f, Duration_s=1.2f, Range=2f, Anim="Attack1" } }, //level 1
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=40, ManaCost=2, ExecTime_s=0.3f, Duration_s=1.2f, Range=2f, Anim="Attack1" } }, //level 2
{new ActionDescription{Logic=ActionLogic.MELEE, Amount=50, ManaCost=2, ExecTime_s=0.3f, Duration_s=1.2f, Range=2f, Anim="Attack1" } }, //level 3
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=7, ManaCost=2, Duration_s=0.5f, Range=12f, Anim="Todo" } }, //Level 1
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=12, ManaCost=2, Duration_s=0.5f, Range=15f, Anim="Todo" } }, //Level 2
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=15, ManaCost=2, Duration_s=0.5f, Range=18f, Anim="Todo" } }, //Level 3
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=7, ManaCost=2, Duration_s=0.5f, Range=12f, Anim="Attack1" } }, //Level 1
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=12, ManaCost=2, Duration_s=0.5f, Range=15f, Anim="Attack1" } }, //Level 2
{new ActionDescription{Logic=ActionLogic.RANGED, Amount=15, ManaCost=2, Duration_s=0.5f, Range=18f, Anim="Attack1" } }, //Level 3
}
},

}
}
{ ActionType.GENERAL_REVIVE, new List<ActionDescription>
{ ActionType.GENERAL_REVIVE, new List<ActionDescription>
{
{new ActionDescription{Logic=ActionLogic.REVIVE, Amount=10, ExecTime_s=0.3f, Duration_s=0.5f, Anim="Todo" } }
}

8
Assets/BossRoom/Scripts/Client/Game/Action.meta


fileFormatVersion: 2
guid: 469848a43d22a88499451600cee68f03
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

42
Assets/BossRoom/Scripts/Shared/Game/Action/ActionBase.cs


using UnityEngine;
namespace BossRoom
{
/// <summary>
/// Abstract base class containing some common members shared by Action (server) and ActionFX (client visual)
/// </summary>
public abstract class ActionBase
{
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>
/// RequestData we were instantiated with. Value should be treated as readonly.
/// </summary>
public ref ActionRequestData Data { get { return ref m_Data; } }
/// <summary>
/// Data Description for this action.
/// </summary>
public ActionDescription Description
{
get
{
var list = ActionData.ActionDescriptions[Data.ActionTypeEnum];
int level = Mathf.Min(Data.Level, list.Count - 1); //if we don't go up to the requested level, just cap at the max level.
return list[level];
}
}
public ActionBase(ref ActionRequestData data)
{
m_Data = data;
}
}
}

11
Assets/BossRoom/Scripts/Shared/Game/Action/ActionBase.cs.meta


fileFormatVersion: 2
guid: 5b435f2cb67bfbd4da38c5a06e04a432
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

40
Assets/BossRoom/Scripts/Shared/Game/Action/ActionUtils.cs


using UnityEngine;
namespace BossRoom
{
public static class ActionUtils
{
//cache Physics Cast hits, to minimize allocs.
private static RaycastHit[] s_Hits = new RaycastHit[4];
/// <summary>
/// Does a melee foe hit detect.
/// </summary>
/// <param name="isNPC">true if the attacker is an NPC (and therefore should hit PCs). False for the reverse.</param>
/// <param name="attacker">The collider of the attacking GameObject.</param>
/// <param name="description">The Description of the Action being played (containing things like Range that control the physics query.</param>
/// <param name="results">Place an uninitialized RayCastHit[] ref in here. It will be set to the results array. </param>
/// <remarks>
/// This method does not alloc. It returns a maximum of 4 results. Consume the results immediately, as the array will be overwritten with
/// the next similar query.
/// </remarks>
/// <returns>Total number of foes encountered. </returns>
public static int DetectMeleeFoe(bool isNPC, Collider attacker, ActionDescription description, out RaycastHit[] results)
{
//this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack.
var myBounds = attacker.bounds;
//NPCs (monsters) can hit PCs, and vice versa. No friendly fire allowed on either side.
int mask = LayerMask.GetMask(isNPC ? "PCs" : "NPCs");
int numResults = Physics.BoxCastNonAlloc(attacker.transform.position, myBounds.extents,
attacker.transform.forward, s_Hits, Quaternion.identity, description.Range, mask);
results = s_Hits;
return numResults;
}
}
}

11
Assets/BossRoom/Scripts/Shared/Game/Action/ActionUtils.cs.meta


fileFormatVersion: 2
guid: da8e533df9f2e3d4f9498ba76434ff4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

66
Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs


namespace BossRoom.Visual
{
/// <summary>
/// Abstract base class for playing back the visual feedback of an Action.
/// </summary>
public abstract class ActionFX : ActionBase
{
protected ClientCharacterVisualization m_Parent;
public ActionFX(ref ActionRequestData data, ClientCharacterVisualization parent) : base(ref data)
{
m_Parent = parent;
}
public ActionLogic Logic
{
get
{
return ActionData.ActionDescriptions[Data.ActionTypeEnum][0].Logic;
}
}
/// <summary>
/// Starts the ActionFX. Derived classes may return false if they wish to end immediately without their Update being called.
/// </summary>
/// <returns>true to play, false to be immediately cleaned up.</returns>
public abstract bool Start();
public abstract bool Update();
/// <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 End()
{
Cancel();
}
/// <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>
protected virtual void Cancel() { }
public static ActionFX MakeActionFX(ref ActionRequestData data, ClientCharacterVisualization parent)
{
ActionLogic logic = ActionData.ActionDescriptions[data.ActionTypeEnum][0].Logic;
switch (logic)
{
case ActionLogic.MELEE: return new MeleeActionFX(ref data, parent);
default: throw new System.NotImplementedException();
}
}
public virtual void OnAnimEvent(string id) { }
}
}

11
Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs.meta


fileFormatVersion: 2
guid: bd5022fe94e2de7418ca7b00cb3431b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

67
Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs


using System.Collections.Generic;
using UnityEngine;
namespace BossRoom.Visual
{
/// <summary>
/// This is a companion class to ClientCharacterVisualization that is specifically responsible for visualizing Actions. Action visualizations have lifetimes
/// and ongoing state, making this class closely analogous in spirit to the BossRoom.Server.ActionPlayer class.
/// </summary>
public class ActionVisualization
{
private List<ActionFX> m_PlayingActions;
public ClientCharacterVisualization Parent { get; private set; }
public ActionVisualization(ClientCharacterVisualization parent)
{
Parent = parent;
m_PlayingActions = new List<ActionFX>();
}
public void Update()
{
//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.Update();
bool expirable = action.Description.Duration_s > 0f; //non-positive value is a sentinel indicating the duration is indefinite.
bool timeExpired = expirable && (Time.time - action.TimeStarted) >= action.Description.Duration_s;
if (!keepGoing || timeExpired)
{
action.End();
m_PlayingActions.RemoveAt(i);
}
}
}
public void OnAnimEvent(string id)
{
foreach (var action in m_PlayingActions)
{
action.OnAnimEvent(id);
}
}
public void PlayAction(ref ActionRequestData data)
{
//Do Trivial Actions (actions that just require playing a single animation, and don't require any state trackincg).
switch (data.ActionTypeEnum)
{
case ActionType.GENERAL_REVIVE:
Parent.OurAnimator.SetTrigger("BeginRevive");
return;
}
ActionFX action = ActionFX.MakeActionFX(ref data, Parent);
action.TimeStarted = Time.time;
if (action.Start())
{
m_PlayingActions.Add(action);
}
}
}
}

11
Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs.meta


fileFormatVersion: 2
guid: ac73f3a9b424550468f04fc5362275b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

70
Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs


using MLAPI;
using MLAPI.Spawning;
namespace BossRoom.Visual
{
/// <summary>
/// The visual part of a MeleeAction. See MeleeAction.cs for more about this action type.
/// </summary>
public class MeleeActionFX : ActionFX
{
public MeleeActionFX(ref ActionRequestData data, ClientCharacterVisualization parent) : base(ref data, parent) { }
//have we actually played an impact? This won't necessarily happen for all swings. Sometimes you're just swinging at space.
private bool m_ImpactPlayed;
/// <summary>
/// When we detect if our original target is still around, we use a bit of padding on the range check.
/// </summary>
private const float k_RangePadding = 3f;
public override bool Start()
{
m_Parent.OurAnimator.SetTrigger(Description.Anim);
return true;
}
public override bool Update()
{
return true;
}
public override void OnAnimEvent(string id)
{
if (id == "impact" && !m_ImpactPlayed)
{
PlayHitReact();
}
}
public override void End()
{
//if this didn't already happen, make sure it gets a chance to run. This could have failed to run because
//our animationclip didn't have the "impact" event properly configured (as one possibility).
PlayHitReact();
}
private void PlayHitReact()
{
if (m_ImpactPlayed) { return; }
m_ImpactPlayed = true;
//Is my original target still in range? Then definitely get him!
if (Data.TargetIds != null && Data.TargetIds.Length > 0 && SpawnManager.SpawnedObjects.ContainsKey(Data.TargetIds[0]))
{
NetworkedObject originalTarget = SpawnManager.SpawnedObjects[Data.TargetIds[0]];
float padRange = Description.Range + k_RangePadding;
if ((m_Parent.transform.position - originalTarget.transform.position).sqrMagnitude < (padRange * padRange))
{
ClientCharacterVisualization targetViz = originalTarget.GetComponent<Client.ClientCharacter>().ChildVizObject;
targetViz.OurAnimator.SetTrigger("HitReact1");
}
}
//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.
}
}
}

11
Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs.meta


fileFormatVersion: 2
guid: 87ac9a12dfd0b47478c2ce8ede685033
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存