文章写于2016-1-31,后有修改。
本文为本人原创,转载请注明。
以下为正文
……………………………………………………………………………………………………………………………………
游戏AI常常分为三大部分——感知层、分析层(包含记忆层)、决策层。
在早期的游戏中,AI只有分析层和决策层,虽然效果也是不错的,但是缺乏了真实感,直到《神偷》在感知层做出了巨大的突破后,游戏才逐渐变得真实有趣。
其中最有意思的是敌人视野的感知,比如说《盟军敢死队》。(童年回忆)
本文目的在于用Unity来实现一个观测敌人的炮台,做出类似盟军敢死队中敌人视野范围的效果(见下图):
视野范围感知的基本原理:
在Unity中,用一个Sphere Collider与角度差的计算来模拟敌人的视角。
当敌人进入Sphere Collider中后,进行角度差计算,如果目标与自己的角度差小于自己的视角,并且朝目标发射一条射线没有其他物体阻挡时,判断为看见敌人。完成后的场景图如下:
难点是射线碰撞的运用:
RaycastHit hitInfo;
if(Physics.Raycast(this.transform.position+Vector3.up,vec,out hitInfo,20)){
GameObject gameObj = hitInfo.collider.gameObject;
if(gameObj.tag == "Player"){
//相关操作
}
}
//若射线与物体产生碰撞,则整个表达式为true,否则为false
//起点为this.transform.position+Vector3.up
//方向为vec,这里的vec是指向目标的向量
//out hitInfo碰撞如果返回true,hitInfo将包含碰到器碰撞的更多信息。
//20为射线的长度
//注意:最后还有一个LayerMask参数未写进去,用于只选定Layermask层内的碰撞器,其它层内碰撞器忽略。
然后是敌人(炮台)的状态机设计图:
这里要注意的是,敌人有两种模式,正常模式和警戒模式。
警戒模式中,敌人的视野范围和视角都会变得更大,同时视角灯光会呈现黄色。
警戒模式在进入攻击模式时开启,再次回到正常模式时才会关闭。
由以上的情况,C#代码分为几大板块:
①角度差计算:计算目标与自身的角度差。
②目标是否可见的判断:判断目标是否在自身视野范围内。
③状态机:每种状态的切换以及相应要做的动作。
④目标进入与离开感知范围:捕捉目标GameObject或者释放目标GameObject。
接下来是完整的C#代码:(当时技术较菜,直接用 switch 来写了)
using UnityEngine;
using System.Collections;
public class Enemy01Rewrite : MonoBehaviour
{
//声音
AudioSource m_audio;
public AudioClip m_Shot;
public AudioClip m_EnterAlert;
public AudioClip m_ExitAlert;
public AudioClip m_ExitWait;
//功能
GameObject Target = null;
float DeltaShotTime = 0.5f;//攻击间隔
float AngleSpeed = 90f;//转动速度
float PerceiveRadius = 8f;//感知范围
float AlertTime = 10f;//警报时间
float WaitTime = 4f;//失去目标等待时间
float LightRange = 0f;//灯光距离(在Start()中根据SpotLight参数设定)
float LightIntensity = 0f;//灯光亮度(在Start()中根据SpotLight参数设定)
float ViewAngle = 60f;//视野角度
bool isInView = false;//是否在扇形视野范围内
bool isRayCast = false;//是否通过射线能看到
bool isAlert = false;//是否警报
bool isWait = false;//是否等待
float TargetAngle = 0;
float SelfAngle = 0;
float AngleDiff = 0;
float MinAngle = 3f;
public GameObject m_Light;
public GameObject m_Bullet;
public GameObject m_Launcher;
int state = 0;
float time_shot = 0;
float time_alert = 0;
float time_wait = 0;
//角度差计算函数
void CalculateAngle()
{
if (Target != null)
{
float AtanAngle = (Mathf.Atan((Target.transform.position.z - this.transform.position.z) /
(Target.transform.position.x - this.transform.position.x))
* 180.0f / 3.14159f);
//Debug.Log (this.transform.rotation.eulerAngles+" "+AtanAngle);
//1象限角度转换
if ((Target.transform.position.z - this.transform.position.z) > 0
&&
(Target.transform.position.x - this.transform.position.x) > 0
)
{
TargetAngle = 90f - AtanAngle;
//Debug.Log ("象限1 "+TargetAngle);
}
//2象限角度转换
if ((Target.transform.position.z - this.transform.position.z) <= 0
&&
(Target.transform.position.x - this.transform.position.x) > 0
)
{
TargetAngle = 90f + -AtanAngle;
//Debug.Log ("象限2 "+TargetAngle);
}
//3象限角度转换
if ((Target.transform.position.z - this.transform.position.z) <= 0
&&
(Target.transform.position.x - this.transform.position.x) <= 0
)
{
TargetAngle = 90f - AtanAngle + 180f;
//Debug.Log ("象限3 "+TargetAngle);
}
//4象限角度转换
if ((Target.transform.position.z - this.transform.position.z) > 0
&&
(Target.transform.position.x - this.transform.position.x) <= 0
)
{
TargetAngle = 270f + -AtanAngle;
//Debug.Log ("象限4 "+TargetAngle);
}
//调整TargetAngle
float OriginTargetAngle = TargetAngle;
if (Mathf.Abs(TargetAngle + 360 - this.transform.rotation.eulerAngles.y)
<
Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
)
{
TargetAngle += 360f;
}
if (Mathf.Abs(TargetAngle - 360 - this.transform.rotation.eulerAngles.y)
<
Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
)
{
TargetAngle -= 360f;
}
//输出角度差
AngleDiff = Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y);
Debug.Log("角度差:" + TargetAngle + "(" + OriginTargetAngle + ")-" + this.transform.rotation.eulerAngles.y + "=" + AngleDiff);
}
}
///
/// Judges the view.
///
//感知视野的相关计算 判断isRayCast和isInView
void JudgeView()
{
//感知角度相关计算
if (Target != null)
{
//指向玩家的向量计算
Vector3 vec = new Vector3(Target.transform.position.x - this.transform.position.x,
0f,
Target.transform.position.z - this.transform.position.z);
//射线碰撞判断
RaycastHit hitInfo;
if (Physics.Raycast(this.transform.position + Vector3.up, vec, out
hitInfo, 20))
{
GameObject gameObj = hitInfo.collider.gameObject;
//Debug.Log("Object name is " + gameObj.name);
if (gameObj.tag == "Player")//当射线碰撞目标为boot类型的物品 ,执行拾取操作
{
//Debug.Log("Seen!");
isRayCast = true;
}
else
{
isRayCast = false;
}
}
//画出碰撞线
Debug.DrawLine(this.transform.position, hitInfo.point, Color.red, 1);
//视野中的射线碰撞判断结束
//视野范围判断
//物体在范围角度内,警戒模式下范围为原来1.5倍
if (AngleDiff * 2 <
(isAlert ? ViewAngle * 1.5f : ViewAngle)
)
{
isInView = true;
}
else
{
isInView = false;
}
//Debug.Log ("角度差 "+AngleDiff);
}
}
// Use this for initialization
void Start()
{
LightRange = m_Light.GetComponent().range;
LightIntensity = m_Light.GetComponent().intensity;
this.GetComponent().radius = PerceiveRadius;
m_audio = this.GetComponent();
}
// Update is called once per frame
void Update()
{
//Debug.Log("state:" + state + " time_alert:" + time_alert);
if (isAlert)
{
//警戒模式
m_Light.GetComponent().range = LightRange * 2f;
m_Light.GetComponent().color = new Color(1f, 1f, 0);
m_Light.GetComponent().intensity = LightIntensity * 2f;
m_Light.GetComponent().spotAngle = ViewAngle * 1.5f;
this.GetComponent().radius = PerceiveRadius * 1.5f;
}
else
{
//正常模式
m_Light.GetComponent().range = LightRange;
m_Light.GetComponent().color = new Color(1f, 1f, 1f);
m_Light.GetComponent().intensity = LightIntensity;
m_Light.GetComponent().spotAngle = ViewAngle;
this.GetComponent().radius = PerceiveRadius;
}
//计算角度差
CalculateAngle();
//感知视野判断(判断isRayCast与isInView)
JudgeView();
//状态机 共4个状态
switch (state)
{
//正常模式——旋转扫视
case 0:
this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);
//发现敌人 进入攻击模式
if (isRayCast && isInView)
{
if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
isAlert = true;
time_wait = 0;
state = 1;
}
break;
//攻击模式——朝目标发射子弹
case 1:
//根据角度跟踪Target
if (this.transform.rotation.eulerAngles.y < TargetAngle)
{
if (AngleDiff >= MinAngle)
{
//顺时针旋转
this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);
}
}
else
{
if (AngleDiff >= MinAngle)
{
//逆时针旋转
this.transform.Rotate(new Vector3(0, -1f, 0f), Time.deltaTime * AngleSpeed);
}
}
//子弹发射计时 子弹发射间隔
time_shot += Time.deltaTime * 1;
if (time_shot >= DeltaShotTime)
{
Instantiate(m_Bullet, m_Launcher.transform.position, m_Launcher.transform.rotation);
time_shot = 0;
m_audio.PlayOneShot(m_Shot);
}
//敌人离开视野 进入等待模式
if (!isRayCast)
{
isWait = true;
time_wait = 0;
state = 2;
}
break;
//敌人离开可见区域——等待
case 2:
//进入旋转警戒模式 计时
time_wait += Time.deltaTime * 1;
if (time_wait >= WaitTime)
{
m_audio.PlayOneShot(m_ExitWait);
time_alert = 0;
isWait = false;
state = 3;
}
if (isRayCast && isInView)
{
if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
isAlert = true;
time_wait = 0;
state = 1;
}
break;
//警戒模式——旋转扫视
case 3:
//回到正常模式 计时
time_alert += Time.deltaTime * 1;
if (time_alert >= AlertTime)
{
m_audio.PlayOneShot(m_ExitAlert);
time_alert = 0;
isAlert = false;
state = 0;
}
//发现敌人 进入攻击模式
if (isRayCast && isInView)
{
if (!isAlert) m_audio.PlayOneShot(m_EnterAlert);
isAlert = true;
time_wait = 0;
state = 1;
}
this.transform.Rotate(new Vector3(0, 1f, 0f), Time.deltaTime * AngleSpeed);
break;
default:
break;
}
}
//玩家进入感知层
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
Target = other.gameObject;
//提前计算角度差
CalculateAngle();
time_shot = 0;
}
}
//玩家进入视野
void OnTriggerStay(Collider other)
{
if (other.gameObject.tag == "Player")
{
if (Target == null)
{
Target = other.gameObject;
}
}
}
//玩家离开感知层
void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Player")
{
Target = null;
isInView = false;
isRayCast = false;
}
}
}