1、场景设置
场景中央为可供移动的主角(黑色箭头)和四处巡逻的AI(蓝色圆形),四周为黑色墙体。
墙体碰撞器为EdgeCollider2D
AI拥有一个子物体,子物体上挂一个Circle Collider2D,用于表示AI的听力范围
2、脚本编写
1、AI的脚本
首先,我们可以针对AI所拥有的状态,创建对应的枚举类型
public enum AvoidSceneState
{
Idle,
Track,
AvoidWall
}
在这个简单巡逻场景中,AI需要三个状态:巡逻、追击、避墙
巡逻:
一种比较简单的巡逻实现方式,可以设置一个间隔时间,每经过一次间隔时间,生成一个随机数,随机数取值范围为[0,7]的整数,根据产生的随机数,来确定AI的移动方向(八方向)
private void Idleing(int num)
{
//if(info.collider.)
lastState = AvoidSceneState.Idle;
//八方向随机移动
moveDir = new Vector2(Mathf.Cos((0 - num) * 45 * Mathf.Deg2Rad), Mathf.Sin((0 - num) * 45 * Mathf.Deg2Rad));
this.transform.Translate(moveDir.normalized * Time.deltaTime * idleSpeed, Space.World);
}
追击:
private void Tracking(GameObject player)
{
lastState = AvoidSceneState.Track;
moveDir = (player.transform.position - this.transform.position);
//有一定距离,则向主角移动
if (moveDir.magnitude >= 0.5f)
{
this.transform.Translate(moveDir.normalized * Time.deltaTime * trackSpeed, Space.World);
}
}
避墙:
2d场景中,想要让AI在碰到墙前提前改变运动方向,可以使用叉乘。具体做法如下:
由AI当前位置加上碰撞器在运动方向上的矢量的一半(避免射线与AI碰撞器本身进行交互)作为起始位置,射出一条单位长度的射线矢量,每帧检测射线是否与墙壁有碰撞。当发生碰撞时,因为墙体采用的是线碰撞器,可以通过线碰撞器两个端点的减法,来计算墙体矢量。将射线与墙体矢量进行叉乘,计算出2d平面的z轴基矢量,再将射线与z轴基矢量进行叉乘,可计算出基于射线与墙体交点的一条与射线垂直的矢量,将矢量与运动方向矢量相加,获取新的运动方向。
如上图所示:蓝线为运动方向矢量(射线),黄线为墙体矢量,黑线为垂线矢量,红线为新的运动方向矢量。
private void RaysDetection(Vector2 moveDir)
{
//射线检测
ray = new Ray2D(new Vector2(this.transform.position.x, this.transform.position.y) + moveDir.normalized * boxCollider2D.size.x * Mathf.Sqrt(2) / 2, moveDir.normalized);
UnityEngine.Debug.DrawRay(ray.origin, ray.direction * 1, Color.blue);
info = Physics2D.Raycast(ray.origin, ray.direction, 1f, mask);
//UnityEngine.Debug.DrawRay(this.transform.position, moveDir, Color.red);
if (info && info.collider.tag == "Wall")
{
//rotateTime = 0;
canSrand = true;
state = AvoidSceneState.AvoidWall;
curMoveDir = moveDir;
Vector3 dir = new Vector3(ray.direction.x, ray.direction.y, 0);
Vector3 wall = new Vector3((info.collider.gameObject.GetComponent<EdgeCollider2D>().points[0] - info.collider.gameObject.GetComponent<EdgeCollider2D>().points[1]).x,
(info.collider.gameObject.GetComponent<EdgeCollider2D>().points[0] - info.collider.gameObject.GetComponent<EdgeCollider2D>().points[1]).y, 0);
//运动方向和墙叉乘出z轴
Vector3 normal = Vector3.Cross(curMoveDir, wall);
//Debug.Log(Vector3.Dot(curMoveDir, wall));
if (Vector3.Dot(curMoveDir, wall) <= -0.01f)
{
//运动方向和z轴叉乘出运动方向关于交点的垂线矢量
tangent = Vector3.Cross(dir, normal).normalized * 2;
}
if (Vector3.Dot(curMoveDir, wall) >= 0.01f)
{
tangent = Vector3.Cross(dir, normal).normalized * (-2);
}
if (Vector3.Dot(curMoveDir, wall) < 0.001f && Vector3.Dot(curMoveDir, wall) > -0.001f)
{
tangent = -2 * curMoveDir.normalized;
}
//Debug.Log(tangent + "tangent");
UnityEngine.Debug.DrawRay(info.point, wall, Color.yellow);
UnityEngine.Debug.DrawRay(info.point, tangent, Color.black);
curMoveDir = new Vector2(tangent.x, tangent.y) + curMoveDir;
UnityEngine.Debug.DrawRay(this.transform.position, curMoveDir, Color.red);
}
}
private void AvoidWalling()
{
lastState = AvoidSceneState.AvoidWall;
moveDir = curMoveDir;
this.transform.Translate(curMoveDir.normalized * Time.deltaTime * idleSpeed, Space.World);
}
针对状态之间的切换
switch (state)
{
case AvoidSceneState.Idle:
//恢复听力
//this.transform.GetChild(0).GetComponent<CircleCollider2D>().enabled = true;
text.text = "AI正常情况下会四处巡逻";
idletimer += Time.deltaTime;
if (idletimer > moveTime || canSrand)
{
//八方向随机移动
randNum = Random.Range(0, 8);
idletimer = 0;
avoidtimer = 0;
canSrand = false;
}
Idleing(randNum);
break;
case AvoidSceneState.Track:
text.text = "主角进入一定范围,AI会追击主角";
idletimer = 0;
avoidtimer = 0;
Tracking(player);
break;
case AvoidSceneState.AvoidWall:
text.text = "AI快要碰壁时,暂停追踪一段时间\n若主角再次进入范围,会被再次追击";
text.text = text.text.Replace("\\n", "\n");
//快要碰壁
if (lastState == AvoidSceneState.Track)
{
hearingTimer = 0;
this.transform.GetChild(0).GetComponent<CircleCollider2D>().enabled = false;
}
avoidtimer += Time.deltaTime;
//碰壁改变方向后迅速回到Idle状态
if (avoidtimer > 0.5)
{
state = AvoidSceneState.Idle;
avoidtimer = 0;
idletimer = 0;
}
//Debug.Log("avoidWall");
AvoidWalling();
break;
}
3、效果展示
项目文件
链接:https://pan.baidu.com/s/1vqZiW7QBd8Sa1TlcdNlcEw
提取码:ojuy