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.
179 lines
5.8 KiB
C#
179 lines
5.8 KiB
C#
3 weeks ago
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using Unity.BossRoom.Gameplay.Configuration;
|
||
|
using Unity.BossRoom.Gameplay.Actions;
|
||
|
using UnityEngine;
|
||
|
|
||
|
namespace Unity.BossRoom.Gameplay.GameplayObjects.Character.AI
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Handles enemy AI. Contains AIStateLogics that handle some of the details,
|
||
|
/// and has various utility functions that are called by those AIStateLogics
|
||
|
/// </summary>
|
||
|
public class AIBrain
|
||
|
{
|
||
|
private enum AIStateType
|
||
|
{
|
||
|
ATTACK,
|
||
|
//WANDER,
|
||
|
IDLE,
|
||
|
}
|
||
|
|
||
|
static readonly AIStateType[] k_AIStates = (AIStateType[])Enum.GetValues(typeof(AIStateType));
|
||
|
|
||
|
private ServerCharacter m_ServerCharacter;
|
||
|
private ServerActionPlayer m_ServerActionPlayer;
|
||
|
private AIStateType m_CurrentState;
|
||
|
private Dictionary<AIStateType, AIState> m_Logics;
|
||
|
private List<ServerCharacter> m_HatedEnemies;
|
||
|
|
||
|
/// <summary>
|
||
|
/// If we are created by a spawner, the spawner might override our detection radius
|
||
|
/// -1 is a sentinel value meaning "no override"
|
||
|
/// </summary>
|
||
|
private float m_DetectRangeOverride = -1;
|
||
|
|
||
|
public AIBrain(ServerCharacter me, ServerActionPlayer myServerActionPlayer)
|
||
|
{
|
||
|
m_ServerCharacter = me;
|
||
|
m_ServerActionPlayer = myServerActionPlayer;
|
||
|
|
||
|
m_Logics = new Dictionary<AIStateType, AIState>
|
||
|
{
|
||
|
[AIStateType.IDLE] = new IdleAIState(this),
|
||
|
//[ AIStateType.WANDER ] = new WanderAIState(this), // not written yet
|
||
|
[AIStateType.ATTACK] = new AttackAIState(this, m_ServerActionPlayer),
|
||
|
};
|
||
|
m_HatedEnemies = new List<ServerCharacter>();
|
||
|
m_CurrentState = AIStateType.IDLE;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Should be called by the AIBrain's owner each Update()
|
||
|
/// </summary>
|
||
|
public void Update()
|
||
|
{
|
||
|
AIStateType newState = FindBestEligibleAIState();
|
||
|
if (m_CurrentState != newState)
|
||
|
{
|
||
|
m_Logics[newState].Initialize();
|
||
|
}
|
||
|
m_CurrentState = newState;
|
||
|
m_Logics[m_CurrentState].Update();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called when we received some HP. Positive HP is healing, negative is damage.
|
||
|
/// </summary>
|
||
|
/// <param name="inflicter">The person who hurt or healed us. May be null. </param>
|
||
|
/// <param name="amount">The amount of HP received. Negative is damage. </param>
|
||
|
public void ReceiveHP(ServerCharacter inflicter, int amount)
|
||
|
{
|
||
|
if (inflicter != null && amount < 0)
|
||
|
{
|
||
|
Hate(inflicter);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private AIStateType FindBestEligibleAIState()
|
||
|
{
|
||
|
// for now we assume the AI states are in order of appropriateness,
|
||
|
// which may be nonsensical when there are more states
|
||
|
foreach (AIStateType aiStateType in k_AIStates)
|
||
|
{
|
||
|
if (m_Logics[aiStateType].IsEligible())
|
||
|
{
|
||
|
return aiStateType;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Debug.LogError("No AI states are valid!?!");
|
||
|
return AIStateType.IDLE;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns true if it be appropriate for us to murder this character, starting right now!
|
||
|
/// </summary>
|
||
|
public bool IsAppropriateFoe(ServerCharacter potentialFoe)
|
||
|
{
|
||
|
if (potentialFoe == null ||
|
||
|
potentialFoe.IsNpc ||
|
||
|
potentialFoe.LifeState != LifeState.Alive ||
|
||
|
potentialFoe.IsStealthy.Value)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Also, we could use NavMesh.Raycast() to see if we have line of sight to foe?
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Notify the AIBrain that we should consider this character an enemy.
|
||
|
/// </summary>
|
||
|
/// <param name="character"></param>
|
||
|
public void Hate(ServerCharacter character)
|
||
|
{
|
||
|
if (!m_HatedEnemies.Contains(character))
|
||
|
{
|
||
|
m_HatedEnemies.Add(character);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Return the raw list of hated enemies -- treat as read-only!
|
||
|
/// </summary>
|
||
|
public List<ServerCharacter> GetHatedEnemies()
|
||
|
{
|
||
|
// first we clean the list -- remove any enemies that have disappeared (became null), are dead, etc.
|
||
|
for (int i = m_HatedEnemies.Count - 1; i >= 0; i--)
|
||
|
{
|
||
|
if (!IsAppropriateFoe(m_HatedEnemies[i]))
|
||
|
{
|
||
|
m_HatedEnemies.RemoveAt(i);
|
||
|
}
|
||
|
}
|
||
|
return m_HatedEnemies;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Retrieve info about who we are. Treat as read-only!
|
||
|
/// </summary>
|
||
|
/// <returns></returns>
|
||
|
public ServerCharacter GetMyServerCharacter()
|
||
|
{
|
||
|
return m_ServerCharacter;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Convenience getter that returns the CharacterData associated with this creature.
|
||
|
/// </summary>
|
||
|
public CharacterClass CharacterData
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return GameDataSource.Instance.CharacterDataByType[m_ServerCharacter.CharacterType];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// The range at which this character can detect enemies, in meters.
|
||
|
/// This is usually the same value as is indicated by our game data, but it
|
||
|
/// can be dynamically overridden.
|
||
|
/// </summary>
|
||
|
public float DetectRange
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return (m_DetectRangeOverride == -1) ? CharacterData.DetectRange : m_DetectRangeOverride;
|
||
|
}
|
||
|
|
||
|
set
|
||
|
{
|
||
|
m_DetectRangeOverride = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|