给猫看的游戏AI实战(二)视觉感知初步

作者:Levelplay皮皮关游戏教育
2017-10-18
18 17 3

作者注

应 indienova 邀请,转自本人的知乎专栏,此文为 AI 系列的第二篇。如有任何疑问欢迎各位朋友指出。

前言、AI行为综述

第一讲的内容过于简单,就当是熟悉一下 Unity 开发基础好了。这次正式发车。

作为一个称职的游戏AI,要具有以下自我修养,可以不包含全部:

  1. 明白自己能干什么——目前所有可以做的行为,即可能性图 Possibility Map。
  2. 认识到当前状况,对一般的 AI 来说,直接读取游戏数据即可;高级AI有视觉、听觉来感知到当前状况。
  3. 理解目标,分解目标,决策具体行动并执行。寻路是一种典型的决策问题。
  4. 了解环境,思考互动策略,比如推箱子、触发机关给玩家制造麻烦。
  5. 群体交互。理解其他伙伴的信息和队伍的整体策略,做出配合、防止冲突。在复数敌人的游戏中多有体现,在足球游戏中更是体现得淋漓尽致。

AI 的层次有高有低,但是行为层次的高低与编程难度有时并不成正比。比如 AI 的视觉、听觉系统就属于高级行为,但是在 Unity 中实现并不难;而如果现在就写基本的可能性图系统,你会发现 NPC 的基本移动、攻击功能还没实现,最后只能写出伪代码而无法真正实现一个功能。

作为一篇想要尽可能浅显的文章,我们从视觉系统出发,把复杂问题解构,目的是让大家有一种“不过如此”的感觉。( ̄y▽ ̄)~*

一、模拟视觉系统——原理和例子

人类的视觉系统有几个特点,比如:

  1. 近的看得清,远的看不清。
  2. 视角大约90度,视线正前方信息丰富(色彩和细节),视线外侧的部分只有轮廓和运动信息。
  3. 注意力有限,当关注于某个具体的方位或者物体时,其它部分被忽略(比如魔术中的障眼法对绝大多数人有效)。

作为一个 AI,可以模拟这种视觉系统,有助于干掉玩家或者……取悦玩家ㄟ( ▔, ▔ )ㄏ 。

1

上图是潜入类游戏里程碑式的作品《盟军敢死队》,红圈标出的是一个敌方德国兵。这个游戏是俯视的,玩家具有上帝视角。玩家在游戏中可以随意查看敌人的视野范围(虽然这有点不符合实际),敌人的视野是一个巨大的三角形,视线角度约90度;视野分为两段,近处是亮绿色,远处是暗绿色,在亮绿色范围内一定会引起注意,而暗绿色的部分由于敌人看不太清楚,所以我方的人员只要趴下就不会引起注意。

由于敌人众多,视线错综复杂,这个游戏的难度颇高,后面几关我实在打不过去(╯‵□′)╯︵┻━┻ 。

不说老古董了,举个更有人气的例子:《合金装备》系列。

2

合金装备2中,室内场景更多一些,敌人视角也更窄,但是从技术面分析,敌人视野的实现方式和《盟军敢死队》并没有什么不同。上图中的主角正在利用墙角进行隐蔽,等待敌人转过去时伺机击杀他。

可以说所有潜入类游戏的AI都要依赖于视觉系统,在 Unity 中实现这个效果并不难,我们来尝试一下。

二、模拟视觉系统——实现

拿出前面一节做的小例子,换位思考一下——我们做的例子里的 Player 就是敌人。

1、先制造一点氛围,把主光源 Directional Light 的强度调低,让场景昏暗下来。

3

2、给 Player 加上一个探照灯。右键点击 PlayerLight > Spotlight

4

3、以上两步应该已经能看到效果了。下面调整一下探照灯的远近、角度范围、光线强度。让它和人物的视野大概一致。

5

4、开始写代码实现视野。我们用射线来模拟视野。先看最终效果,再来解释代码。

6

如图,我们要发射一系列射线,从角色身上开始,发射到远端,形成扇形分布。使用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);
            }
        }
    }

成功的话应该是如下效果,视线被障碍物挡住了:

7

到这里效果已经有点像是盟军敢死队了……如果你觉得效果并不好,那只能先忍耐一下了。做游戏就是这样,我们看到的成品游戏都是深入优化的结果,无论程序还是美术都要做到80分才能得到好的结果。这里我们还是继续研究视野问题本身吧。

6、这个例子最后一大步是添加虚拟的敌人。其实我们控制的Player是敌人,准确来说现在要添几个虚拟的玩家,我们要去发现他们。别忘了,角色互换 (♥◠‿◠)ノ ʅ(‾◡◝)

8

如上图,添加几个敌人,可以在障碍前、障碍物后面都放一个,方便测试。注意圆柱体要有一定高度,也要粗一些。因为我们的射线是有高度的,我一开始放的圆柱体很矮,导致射线打不到它。另外如果圆柱太细,会从射线之间漏过去,也不好。

把这些敌人的 Layer 设置为 Enemy 层,以便和射线碰撞。

9

播放游戏。如图,确保射线与圆柱体也能碰撞。

7、还没完,我们要做出一种效果,让敌人被看到时才显形,平时没被发现时是隐形的。先给敌人添加脚本,内容如下:

 public class Enemy : MonoBehaviour {

    MeshRenderer meshRenderer;
    // 代表被发现时的帧数(这里用帧数代表时间)
    public int spottedFrame = -100;
    void Start () {
        meshRenderer = GetComponent();
    }
    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().spottedFrame = Time.frameCount;
    }

对照这里 spottedFrame 的设置方法,理解一下。当敌人被发现的时候,他会保持显形10帧,一直在视线内就一直显形。一旦它离开视线,10帧之后他就会再次隐形。

8、完成!接下来测试和修正问题吧。

10

总结

本专栏受到一本书《Practical Game AI Programming》的影响,例子会讲的很浅显易懂。我觉得游戏教程就应该如此,新手能跟着一步一步学习,老手可以看个思路。希望我能和大家一起实践,对游戏AI有更系统更深入的理解,肯定可以超出那本书所讲的范畴。(有一句话偷偷说:AI是国产游戏的明显短板 (/ω\)。)

注意本文最后的控制敌人隐形、显形的算法。AI实现时会有很多游戏特有的小算法,初学者学习时要注意思考哦。这些小算法属于编程的核心能力,而核心能力在AI编程中极其重要,这也是为什么国外的游戏制作团队会非常重视AI设计、重视培养AI设计师的原因。

  • 官网
  • 游戏开发技术交流群:610475807
  • 微信公众号:皮皮关

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. byzod 2017-10-19

    射线的解决方法, 对于远处的小物件, 只能大幅度提高射线密度来解决吗?
    考虑到实际游戏中必须还原3D的视野, 无论是模仿电视的线扫描还是直接用射线填满空间角, 要让AI能够在远处发现一个手雷的射线密度必定非常高, 这部分的性能开销不知道能不能忽视

    • mayao11 2017-10-19

      @byzod:你好我是原作者。真做项目的时候,千万别这么干……确实效率很低的。
      正确做法是,先用其它方法找到附近一个矩形区域或者圆形区域内的所有人,然后再判断它们和主角的角度关系。
      简单来说是
      对方向量 = 对方位置 - 主角位置。
      然后判断对方向量与"主角正前方向量"的夹角。

  2. smileroy 2017-10-24

    这个扇形视角范围的计算,用向量的点乘来计算方位,再结合距离 来判断诶。
    这么密集的射线。。。好可怕。。。

您需要登录或者注册后才能发表评论

登录/注册