本文给出的是一个基于状态枚举的AI代码,代码由两部分组成。第一部分EnemyAIController负责控制敌人的逻辑,第二部分RotationMap负责敌人空闲状态的路径算法。先给出Inspector面板中各个值的说明,最后上代码。
EnemyAIController.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
public enum EnemyState
{
Idle, TurnAround, Walk, Run, Attack, Dead
}
public delegate void ThinkMethod();
[RequireComponent(typeof(RotationMap))]
public class EnemyAIController : MonoBehaviour
{
[Header("速度")]
public float walkSpeed;
public float runSpeed;
public float rotateSpeed;
[Header("时间")]
public float thinkTimeOfIdle;
public float thinkTimeOfAttack;
public float animationDampTime;
public float destroyDelayTime;
[Header("距离")]
public float inspectDistance;
public float standAttackDistance;
[Header("属性")]
public EnemyState enemyState;
public string playerTag;
public float maxHealth;
public float causeDamage;
private float health;
private bool resetMap;
private GameObject player;
private Quaternion targetRotation;
private const int RotateAngle = 45;
void Start()
{
health = maxHealth;
player = GameObject.FindGameObjectWithTag(playerTag);
}
void Update()
{
ControlSlider();
ControlRotationMap();
if (enemyState != EnemyState.Dead)
{
ControlState();
ControlPosition();
ControlRotation();
}
ControlAnimation();
}
void ControlSlider()
{
Debug.DrawRay(transform.position, transform.up * 5, Color.red, 500);
Slider slider = GetComponentInChildren<Slider>();
slider.value = (float)health / maxHealth;
}
void ControlRotationMap()
{
switch (enemyState)
{
case EnemyState.Run:
RotationMap map = GetComponent<RotationMap>();
if(!resetMap)
{
map.ResetMap();
resetMap = true;
}
break;
default:
resetMap = false;
break;
}
}
void ControlState()
{
float distance = Vector3.Distance(transform.position, player.transform.position);
if (distance > inspectDistance)
{
InvokeThinkMethod(ThinkAttackState, ThinkIdleState, thinkTimeOfIdle);
}
else
{
if (distance > standAttackDistance)
InvokeThinkMethod(ThinkIdleState,ThinkAttackState, thinkTimeOfAttack);
else
enemyState = EnemyState.Attack;
}
}
void InvokeThinkMethod(ThinkMethod cancelMethod, ThinkMethod invokeMethod, float delay)
{
CancelInvoke(GetMethodName(cancelMethod));
if (!IsInvoking(GetMethodName(invokeMethod)))
Invoke(GetMethodName(invokeMethod), delay);
}
string GetMethodName(ThinkMethod th) { return th.Method.Name; }
void ThinkIdleState()
{
enemyState = (EnemyState)Random.Range((int)EnemyState.Idle, (int)EnemyState.Walk);
targetRotation = (enemyState == EnemyState.TurnAround) ? ThinkOfRotation() : targetRotation;
if (enemyState == EnemyState.TurnAround)
StartCoroutine(ChangeToWalkState());
}
void ThinkAttackState() { enemyState = EnemyState.Run; }
IEnumerator ChangeToWalkState()
{
while (true)
{
float current = transform.rotation.eulerAngles.y;
float target = targetRotation.eulerAngles.y;
if (Mathf.RoundToInt(current) == Mathf.RoundToInt(target))
{
string mName = GetMethodName(ThinkIdleState);
int resultIndex = (int)(targetRotation.eulerAngles.y / RotateAngle);
float delay = (resultIndex % 2 == 0) ? thinkTimeOfIdle : thinkTimeOfIdle * Mathf.Sqrt(2);
CancelInvoke(mName);
Invoke(mName, delay);
enemyState = EnemyState.Walk;
break;
}
yield return null;
}
}
void ControlAnimation()
{
Animator animator = GetComponent<Animator>();
switch (enemyState)
{
case EnemyState.Idle:
animator.SetFloat(Animator.StringToHash("speed"), 1, animationDampTime, Time.deltaTime);
animator.SetBool(Animator.StringToHash("attack"), false);
break;
case EnemyState.TurnAround:
case EnemyState.Walk:
animator.SetFloat(Animator.StringToHash("speed"), 2, animationDampTime, Time.deltaTime);
animator.SetBool(Animator.StringToHash("attack"), false);
break;
case EnemyState.Run:
animator.SetFloat(Animator.StringToHash("speed"), 3, animationDampTime, Time.deltaTime);
animator.SetBool(Animator.StringToHash("attack"), false);
break;
case EnemyState.Attack:
animator.SetBool(Animator.StringToHash("attack"), true);
animator.SetFloat(Animator.StringToHash("speed"), 0);
break;
case EnemyState.Dead:
animator.SetBool(Animator.StringToHash("dead"), true);
animator.SetFloat(Animator.StringToHash("speed"), 0);
break;
}
}
void ControlPosition()
{
CharacterController charaController = GetComponent<CharacterController>();
switch (enemyState)
{
case EnemyState.Walk:
charaController.SimpleMove(transform.forward * Time.deltaTime * walkSpeed);
break;
case EnemyState.Run:
charaController.SimpleMove(transform.forward * Time.deltaTime * runSpeed);
break;
}
}
void ControlRotation()
{
switch (enemyState)
{
case EnemyState.TurnAround:
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotateSpeed);
break;
case EnemyState.Run:
case EnemyState.Attack:
Vector3 center = new Vector3(player.transform.position.x, transform.position.y, player.transform.position.z);
transform.LookAt(center);
break;
}
}
Quaternion ThinkOfRotation()
{
RotationMap map = GetComponent<RotationMap>();
List<int> array = map.GetArrayFromMap();
int index = array[Random.Range(0, array.Count)];
map.SetPositionToNext(index);
Vector3 result = Vector3.zero;
result.y+=index * RotateAngle;
return Quaternion.Euler(result);
}
public void GetDamage(int damage)
{
health -= damage;
if (health <= 0)
{
enemyState = EnemyState.Dead;
Destroy(GetComponent<CharacterController>());
Destroy(gameObject, destroyDelayTime);
transform.position = new Vector3(transform.position.x, -1, transform.position.z);
}
}
}
RotationMap.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class Boolean2D
{
[HideInInspector]
public bool[] array;
[HideInInspector]
public int oneSideSize;
public Boolean2D(int size)
{
array = new bool[size * size];
oneSideSize = size;
}
public Boolean2D(Boolean2D copy)
{
array = new bool[copy.oneSideSize * copy.oneSideSize];
oneSideSize = copy.oneSideSize;
for (int i = 0; i < array.Length; i++)
array[i] = copy.array[i];
}
public bool IsEmpty()
{
int count = 0;
foreach(var value in array)
{
if (value)
count++;
if (count > 1)
return false;
}
return true;
}
public bool SetElement(int i, int j, bool value)
{
if (i >= 0 && i < oneSideSize && j >= 0 && j < oneSideSize)
{
array[i * oneSideSize + j] = value;
return true;
}
else
return false;
}
public bool GetElement(int i, int j, out bool value)
{
if (i >= 0 && i < oneSideSize && j >= 0 && j < oneSideSize)
{
value = array[i * oneSideSize + j];
return true;
}
else
{
value = false;
return false;
}
}
}
[System.Serializable]
public class RotationMap : MonoBehaviour
{
public Vector2 currentPosition;
public int mapSize;
public bool useObliqueLine;
public bool canWalkBack;
[HideInInspector]
public int backUpSize;
[HideInInspector]
public Boolean2D pointArray;
private Boolean2D _pointArray;
private List<List<int>> _mapList;
private Vector2 _position = Vector2.down;
void Awake()
{
ResetMap();
if (!canWalkBack)
Debug.Log("canWalkBack取消时,请确保无孤立点且路径唯一,否则将出错!");
}
public void ResetMap()
{
if (_position == Vector2.down)
_position = currentPosition;
else
currentPosition = _position;
_pointArray = new Boolean2D(pointArray);
_mapList = GetListFromBoolean2D(pointArray);
}
public List<int> GetArrayFromMap()
{
if (_pointArray.IsEmpty())
_pointArray = new Boolean2D(pointArray);
List<List<int>> mapList = canWalkBack ? _mapList : GetListFromBoolean2D(_pointArray);
if (!canWalkBack)
_pointArray.SetElement((int)currentPosition.x, (int)currentPosition.y, false);
int index = (int)(currentPosition.x * mapSize + currentPosition.y);
if (mapList[index] == null)
{
Debug.LogError("请将PositionInMap的初始值设在路径上!");
return null;
}
return mapList[index];
}
public void SetPositionToNext(int index)
{
switch (index)
{
case 0:
currentPosition.x--;
break;
case 1:
currentPosition.x--;
currentPosition.y++;
break;
case 2:
currentPosition.y++;
break;
case 3:
currentPosition.x++;
currentPosition.y++;
break;
case 4:
currentPosition.x++;
break;
case 5:
currentPosition.x++;
currentPosition.y--;
break;
case 6:
currentPosition.y--;
break;
case 7:
currentPosition.x--;
currentPosition.y--;
break;
}
}
List<List<int>> GetListFromBoolean2D(Boolean2D pArray)
{
List<List<int>> mapList = new List<List<int>>();
for (int i = 0; i < mapSize; i++)
{
for (int j = 0; j < mapSize; j++)
{
bool value;
if (pArray.GetElement(i, j, out value) && !value)
{
mapList.Add(null);
continue;
}
List<int> resultList = new List<int>();
AddResult(i - 1, j, resultList,pArray, 0);
AddResult(i, j + 1, resultList,pArray, 2);
AddResult(i + 1, j, resultList,pArray, 4);
AddResult(i, j - 1, resultList,pArray, 6);
if(useObliqueLine)
{
AddResult(i - 1, j + 1, resultList,pArray, 1);
AddResult(i + 1, j + 1, resultList,pArray, 3);
AddResult(i + 1, j - 1, resultList,pArray, 5);
AddResult(i - 1, j - 1, resultList,pArray, 7);
}
mapList.Add(resultList);
}
}
return mapList;
}
void AddResult(int i, int j, List<int> list,Boolean2D pArray, int result)
{
bool value;
if (pArray.GetElement(i, j, out value) && value)
list.Add(result);
}
}
RotationMapWindow.cs(放在Editor目录下)
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(RotationMap))]
public class RotationMapWindow : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
RotationMap map = (RotationMap)target;
if (map.backUpSize != map.mapSize)
{
map.pointArray = new Boolean2D(map.mapSize);
map.backUpSize = map.mapSize;
}
if (map.pointArray == null) return;
GUILayout.BeginVertical();
for (int i = 0; i < map.mapSize; i++)
{
GUILayout.BeginHorizontal();
for (int j = 0; j < map.mapSize; j++)
{
bool value;
if (map.pointArray.GetElement(i, j, out value))
map.pointArray.SetElement(i, j, EditorGUILayout.Toggle(value, GUILayout.MaxWidth(15)));
}
GUILayout.EndHorizontal();
}
GUILayout.EndVertical();
}
}