第一讲的内容过于简单,就当是熟悉一下Unity开发基础好了。这次正式发车。
前言、AI行为综述
作为一个称职的游戏AI,要具有以下自我修养,可以不包含全部:
- 明白自己能干什么——目前所有可以做的行为,即可能性图Possibility Map。
- 认识到当前状况,对一般的AI来说,直接读取游戏数据即可;高级AI有视觉、听觉来感知到当前状况。
- 理解目标,分解目标,决策具体行动并执行。寻路是一种典型的决策问题。
- 了解环境,思考互动策略,比如推箱子、触发机关给玩家制造麻烦。
- 群体交互。理解其他伙伴的信息和队伍的整体策略,做出配合、防止冲突。在复数敌人的游戏中多有体现,在足球游戏中更是体现得淋漓尽致。
AI的层次有高有低,但是行为层次的高低与编程难度有时并不成正比。比如AI的视觉、听觉系统就属于高级行为,但是在Unity中实现并不难;而如果现在就写基本的可能性图系统,你会发现NPC的基本移动、攻击功能还没实现,最后只能写出伪代码而无法真正实现一个功能。
作为一篇想要尽可能浅显的文章,我们从视觉系统出发,把复杂问题解构,目的是让大家有一种“不过如此”的感觉。( ̄y▽ ̄)~*
1、模拟视觉系统——原理和例子
人类的视觉系统有几个特点,比如:
- 近的看得清,远的看不清。
- 视角大约90度,视线正前方信息丰富(色彩和细节),视线外侧的部分只有轮廓和运动信息。
- 注意力有限,当关注于某个具体的方位或者物体时,其它部分被忽略(比如魔术中的障眼法对绝大多数人有效)。
作为一个AI,可以模拟这种视觉系统,有助于干掉玩家或者……取悦玩家ㄟ( ▔, ▔ )ㄏ 。
上图是潜入类游戏里程碑式的作品《盟军敢死队》,红圈标出的是一个敌方德国兵。这个游戏是俯视的,玩家具有上帝视角。玩家在游戏中可以随意查看敌人的视野范围(虽然这有点不符合实际),敌人的视野是一个巨大的三角形,视线角度约90度;视野分为两段,近处是亮绿色,远处是暗绿色,在亮绿色范围内一定会引起注意,而暗绿色的部分由于敌人看不太清楚,所以我方的人员只要趴下就不会引起注意。
由于敌人众多,视线错综复杂,这个游戏的难度颇高,后面几关我实在打不过去(╯‵□′)╯︵┻━┻ 。
不说老古董了,举个更有人气的例子:《合金装备》系列。
合金装备2中,室内场景更多一些,敌人视角也更窄,但是从技术面分析,敌人视野的实现方式和《盟军敢死队》并没有什么不同。上图中的主角正在利用墙角进行隐蔽,等待敌人转过去时伺机击杀他。
可以说所有潜入类游戏的AI都要依赖于视觉系统,在Unity中实现这个效果并不难,我们来尝试一下。
2、模拟视觉系统——实现
拿出前面一节做的小例子,换位思考一下——我们做的例子里的Player就是敌人。
1、先制造一点氛围,把主光源Directional Light的强度调低,让场景昏暗下来。
2、给Player加上一个探照灯。右键点击Player,Light > Spotlight。
3、以上两步应该已经能看到效果了。下面调整一下探照灯的远近、角度范围、光线强度。让它和人物的视野大概一致。
4、开始写代码实现视野。我们用射线来模拟视野。先看最终效果,再来解释代码。
如图,我们要发射一系列射线,从角色身上开始,发射到远端,形成扇形分布。使用Debug.DrawLine函数显示的射线只会出现在编辑窗口里,而不出现在Game窗口。像我这样把两个窗口并列排布可以很方便的看到效果。
给Player脚本增加两个变量:
public float viewRadius = 8.0f; // 代表视野最远的距离
public float viewAngleStep = 30; // 射线数量,越大就越密集,效果更好但硬件耗费越大。
增加一个函数:void DrawFieldOfView(),并在Update函数的最后面一句调用它。函数内容如下:
void DrawFieldOfView()
{
// 获得最左边那条射线的向量,相对正前方,角度是-45
Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
// 依次处理每一条射线
for (int i = 0; i <= viewAngleStep; i++)
{
// 每条射线都在forward_left的基础上偏转一点,最后一个正好偏转90度到视线最右侧
Vector3 v = Quaternion.Euler(0, (90.0f/viewAngleStep) * i, 0) * forward_left;
// Player位置加v,就是射线终点pos
Vector3 pos = transform.position + v;
// 从玩家位置到pos画线段,只会在编辑器里看到
Debug.DrawLine(transform.position, pos, Color.red);
}
}
执行游戏,已经可以看到效果了,截图在上面可以返回去对比一下。
5、上面的射线在遇到盒子后,会传过去。现在处理一下,让视线被物体、敌人阻挡,而不会穿透。
添加两个Layer,一个是Enemy层,一个是Obstacle层。将那几个大方块设置为Obstacle层也就是障碍物层,敌人物体我们还没做。前面介绍鼠标点击地面的时候已经说明了添加、设置Layer的方法,不再赘述。
修改脚本,实际发出Ray与障碍物和敌人碰撞。
void DrawFieldOfView()
{
// 获得最左边那条射线的向量,相对正前方,角度是-45
Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
// 依次处理每一条射线
for (int i = 0; i <= viewAngleStep; i++)
{
// 每条射线都在forward_left的基础上偏转一点,最后一个正好偏转90度到视线最右侧
Vector3 v = Quaternion.Euler(0, (90.0f / viewAngleStep) * i, 0) * forward_left; ;
// 创建射线
Ray ray = new Ray(transform.position, v);
RaycastHit hitt = new RaycastHit();
// 射线只与两种层碰撞,注意名字和你添加的layer一致,其他层忽略
int mask = LayerMask.GetMask("Obstacle", "Enemy");
Physics.Raycast(ray, out hitt, viewRadius, mask);
// Player位置加v,就是射线终点pos
Vector3 pos = transform.position + v;
if (hitt.transform != null)
{
// 如果碰撞到什么东西,射线终点就变为碰撞的点了
pos = hitt.point;
}
// 从玩家位置到pos画线段,只会在编辑器里看到
Debug.DrawLine(transform.position, pos, Color.red); ;
// 如果真的碰撞到敌人,进一步处理
if (hitt.transform!=null && hitt.transform.gameObject.layer == LayerMask.NameToLayer("Enemy"))
{
//OnEnemySpotted(hitt.transform.gameObject);
}
}
}
成功的话应该是如下效果,视线被障碍物挡住了:
到这里效果已经有点像是盟军敢死队了……如果你觉得效果并不好,那只能先忍耐一下了。做游戏就是这样,我们看到的成品游戏都是深入优化的结果,无论程序还是美术都要做到80分才能得到好的结果。这里我们还是继续研究视野问题本身吧。
6、这个例子最后一大步是添加虚拟的敌人。其实我们控制的Player是敌人,准确来说现在要添几个虚拟的玩家,我们要去发现他们。别忘了,角色互换 (♥◠‿◠)ノ ʅ(‾◡◝)
如上图,添加几个敌人,可以在障碍前、障碍物后面都放一个,方便测试。注意圆柱体要有一定高度,也要粗一些。因为我们的射线是有高度的,我一开始放的圆柱体很矮,导致射线打不到它。另外如果圆柱太细,会从射线之间漏过去,也不好。
把这些敌人的Layer设置为Enemy层,以便和射线碰撞。
播放游戏。如图,确保射线与圆柱体也能碰撞。
7、还没完,我们要做出一种效果,让敌人被看到时才显形,平时没被发现时是隐形的。先给敌人添加脚本,内容如下:
public class Enemy : MonoBehaviour {
MeshRenderer meshRenderer;
// 代表被发现时的帧数(这里用帧数代表时间)
public int spottedFrame = -100;
void Start () {
meshRenderer = GetComponent<MeshRenderer>();
}
void Update () {
// 通过设置 spottedFrame,就可以实现隐藏或显现
if (spottedFrame >= Time.frameCount-10)
{
meshRenderer.enabled = true;
}
else
{
meshRenderer.enabled = false;
}
}
}
如上图,敌人只有两个属性,meshRenderer和spottedFrame,看注释可以大致理解spottedFrame的用途,不理解没关系,我们先做完。修改Player的脚本,刚才Update最后面射线碰撞到敌人的部分我们注释掉了,把注释去掉并添加函数OnEnemySpotted
// Player.cs的Update函数……省略上面的代码
// 如果真的碰撞到敌人,进一步处理
if (hitt.transform!=null && hitt.transform.gameObject.layer == LayerMask.NameToLayer("Enemy"))
{
OnEnemySpotted(hitt.transform.gameObject);
}
}
}
void OnEnemySpotted(GameObject enemy)
{
enemy.GetComponent<Enemy>().spottedFrame = Time.frameCount;
}
对照这里spottedFrame 的设置方法,理解一下。当敌人被发现的时候,他会保持显形10帧,一直在视线内就一直显形。一旦它离开视线,10帧之后他就会再次隐形。
8、完成!接下来测试和修正问题吧。
总结
本专栏受到一本书《Practical Game AI Programming》的影响,例子会讲的很浅显易懂。我觉得游戏教程就应该如此,新手能跟着一步一步学习,老手可以看个思路。希望我能和大家一起实践,对游戏AI有更系统更深入的理解,肯定可以超出那本书所讲的范畴。(有一句话偷偷说:AI是国产游戏的明显短板 (/ω\)。)
注意本文最后的控制敌人隐形、显形的算法。AI实现时会有很多游戏特有的小算法,初学者学习时要注意思考哦。这些小算法属于编程的核心能力,而核心能力在AI编程中极其重要,这也是为什么国外的游戏制作团队会非常重视AI设计、重视培养AI设计师的原因。
————————————————————————————————————
对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 线下教育自不必说,还会定期更新各种教程,干货。在你学习进步的路上,有皮皮关陪你!~
我们的官网地址:http://levelpp.com/
我们的游戏开发技术交流群:610475807
我们的微信公众号:皮皮关
暂无关于此日志的评论。