Unity3D学习(五):实现一个简单的视觉感知
前言
在很多第一人称或者第三人称射击游戏的单人模式中,玩家的乐趣往往来源于和各式各样的AI敌人的战斗。而战斗的爆发很多时候是因为这些AI在“看见”玩家后就会立即做出反应,比如开火、呼叫同伴、躲藏或者逃跑等。
所以这些AI到底是如何探测,或者说”看到“玩家位置的?
别人的例子
参考了知乎 给猫看的游戏AI实战(二)视觉感知初步 这篇文章。
这篇文章中,原作者让玩家站在敌人的角度来探测目标,它通过向正前方一定扇形区域发射一堆射线来探测目标的位置,如下图:

这种方法虽然实现起来比较简单,但它主要有两个弊端:
1.同一时间内发射大量的射线,对游戏本身的优化来说很不好,容易造成卡顿。
2.如果要探测的物体比较小,甚至比两条射线之间的间隔还小,那么射线是无法探测到这个物体的。
另一种解决思路
原文的评论中,有人提到可以基于探测者自身构建一个球体来探测周围的物体。
因此我们可以用Unity自带的Sphere触发器或者Physics里的OverLaps来构建一个球体探测区域,如下图:

这里我用的是Overlaps,代码如下:
玩家,即探测者
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class Player : MonoBehaviour {
public float moveSpeed; //移动速度
public float EyeViewDistance; //视野距离
public float viewAngle = 120f; //视野角度 private Rigidbody rb;
private Collider[] SpottedEnemies; //附近的敌人
// Use this for initialization
void Start () {
rb = GetComponent<Rigidbody>();
} private void FixedUpdate()
{
DetectEnemy();
} // Update is called once per frame
void Update ()
{
//AutoMove();
MoveAndTurn();
Debug.DrawLine(transform.position, transform.forward * , Color.red); //红色射线,面对的方向
} void AutoMove() //向面对的方向自动移动
{
transform.position += transform.forward * moveSpeed * Time.deltaTime;
} void MoveAndTurn() //玩家移动
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo = new RaycastHit();
//shoot a ray from cam to mouse position which is only detected by gameobject with "Plane" layer.
Physics.Raycast(ray, out hitInfo, , LayerMask.GetMask("Plane"));
if (hitInfo.collider != null)
{
transform.LookAt(new Vector3(hitInfo.point.x, transform.position.y, hitInfo.point.z));
}
rb.velocity = new Vector3(Input.GetAxisRaw("Horizontal"), , Input.GetAxisRaw("Vertical")).normalized * moveSpeed;
} void DetectEnemy() //探测敌人
{
//OverlapSphere内的敌人
SpottedEnemies = Physics.OverlapSphere(transform.position, EyeViewDistance, LayerMask.GetMask("Enemy"));
for(int i = ;i < SpottedEnemies.Length;i++) //检测每一个敌人是否在视野区中
{
Vector3 EnemyPosition = SpottedEnemies[i].transform.position; //敌人的位置 //Debug.Log(transform.forward + " 面对的方向");
//Debug.Log("夹角为:" + Vector3.Angle(transform.forward, EnemyPosition - transform.position)); Debug.DrawRay(transform.position, EnemyPosition - transform.position, Color.yellow); //玩家位置到敌人位置的向量
if (Vector3.Angle(transform.forward, EnemyPosition - transform.position) <= viewAngle/) //这个敌人是否在视野内
{
//如果在视野内
RaycastHit info = new RaycastHit();
int layermask = LayerMask.GetMask("Enemy", "Obstacles"); //指定射线碰撞的对象
Physics.Raycast(transform.position, EnemyPosition - transform.position, out info,EyeViewDistance,layermask); //向敌人位置发射射线
Debug.Log(info.collider.gameObject.name);
if(info.collider == SpottedEnemies[i]) //如果途中无其他障碍物,那么射线就会碰撞到敌人
{
DiscoveredEnemy(SpottedEnemies[i]);
}
}
}
} void DiscoveredEnemy(Collider Enemy) //发现敌人
{
//Do something
Debug.Log("发现敌军:" + Enemy.gameObject.name);
Enemy.GetComponent<Enemy>().BeDiscovered();
}
}
SpottedEnemies是一个Collider数组,我用它来保存这一帧当中处于OverlapSphere形成的球体区域的所有敌人对象,(LayerMask可以让Overlaps的球体只和指定layer的对象发生交互)。然后计算玩家面对的方向和探测到的目标方向的夹角Vector3.Angle(transform.forward, EnemyPosition - transform.position),如下图:

forwar向量代表玩家面朝的方向,v1代表探测到的物体相对于玩家位置的方向,红色扇形区域代表玩家的视野范围。那么计算这两个向量的夹角,然后判断下这个夹角是否小于扇形区夹角的一半(即探测目标是否在玩家视野内)就行了。如果夹角小于视野夹角的一半,那么我们再向目标位置发射一根射线,然后看下射线碰撞到的物体是否是目标对象就行,因为如果玩家和目标之间有障碍物的话,那么射线是会被障碍物挡下来的(也就是说玩家的"视野"被障碍物"遮挡"了)。
Enemy,被探测的目标
这里设定被探测的目标脱离玩家视野一定时间后重新进入隐形状态。
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class Enemy : MonoBehaviour {
public float HideCoolDown = 0.3f; //隐藏自己的冷却时间
public float AppearTime; //最后一次被发现的时刻
private MeshRenderer mr;
// Use this for initialization
void Awake()
{
mr = GetComponent<MeshRenderer>();
} private void Start()
{
mr.enabled = false;
} // Update is called once per frame
void Update ()
{
if (Time.time - AppearTime < HideCoolDown)
return;
if (mr.enabled)
mr.enabled = false;
} public void BeDiscovered() //被发现了
{
mr.enabled = true;
AppearTime = Time.time;
}
}
最后实现的效果图如下:

这种方法就不用发射大量的射线,而且只会对进入球形探测区域且处于视野范围内的物体发射射线,并且也避免了小物体无法被探测的bug。
参考资料
知乎:U3d开发中大部分事件都是用数学进行计算判定的吗? 最高票答案 作者:Meta42
猫看的游戏AI实战(二)视觉感知初步 作者:马遥
Unity3D学习(五):实现一个简单的视觉感知的更多相关文章
- LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS
LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163. ...
- Linux第三周学习总结——构造一个简单的Linux系统MenuOS
第三周学习总结--构造一个简单的Linux系统MenuOS 作者:刘浩晨 [原创作品转载请注明出处] <Linux内核分析>MOOC课程http://mooc.study.163.com/ ...
- PureMVC和Unity3D的UGUI制作一个简单的员工管理系统实例
前言: 1.关于PureMVC: MVC框架在很多项目当中拥有广泛的应用,很多时候做项目前人开坑开了一半就消失了,后人为了填补各种的坑就遭殃的不得了.嘛,程序猿大家都不喜欢像文案策划一样组织文字写东西 ...
- django学习-11.开发一个简单的醉得意菜单和人均支付金额查询页面
1.前言 刚好最近跟技术部门的[产品人员+UI人员+测试人员],组成了一桌可以去公司楼下醉得意餐厅吃饭的小team. 所以为了实现这些主要点餐功能: 提高每天中午点餐效率,把点餐时间由20分钟优化为1 ...
- pipelinewise 学习二 创建一个简单的pipeline
pipelinewise 提供了方便的创建简单pipeline的命令,可以简化pipeline 的创建,同时也可以帮我们学习 生成demo pipeline pipelinewise init --n ...
- 用IntelliJ IDEA学习Spring--创建一个简单的项目
这段时间想学习一下Spring,其实之前学过Spring,只是有些忘记了.而且之前学的时候是适用eclipse学习的,现在好像对IntelliJ这个工具使用挺多的,现在就学习一下这个工具的用法,顺便复 ...
- java学习之—实现一个简单的ArrayList
package thread1; /** * 实现一个简单的ArrayList * * @Title: uminton */ public class SimpleArrayList<T> ...
- 《Linux内核分析》第三周学习小结 构造一个简单的Linux系统OS
郝智宇 无转载 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第三周 构造一个简单的Linux系统Me ...
- 容器基础(五): 实现一个简单容器sdocker
在前面几部分的基础上, 我们更新一下代码,实现一个简单容器 sdocker. sdocker目录构成 linux: # tree . ├── Makefile ├── cpu-test.c # 由cp ...
随机推荐
- python函数参数是值传递还是引用传递(以及变量间复制后是否保持一致):取决于对象内容可变不可变
函数参数传递本质上和变量整体复制一样,只是两个变量分别为形参a和实参b.那么,a=b后,a变了,b值是否跟着变呢?这取决于对象内容可变不可变 首先解释一下,什么是python对象的内容可变不可变? p ...
- C语言中的位运算和逻辑运算
这篇文章来自:http://blog.csdn.net/qp120291570/article/details/8708286 位运算 C语言中的位运算包括与(&),或(|),亦或(^),非( ...
- Android SDK工具(谷歌提供的16个工具)简介
Android SDK包含了许多可以帮助你开发Android平台应用的工具.这些工具分为两类:一是SDK工具:而是平台工具.SDK工具独立于平台,任何开发Android应用的平台都需要配置.平台工具是 ...
- 【60】Spring总结之基础架构(1)
为什么用Spring? Spring一直贯彻并遵守"好的设计优于具体实现,代码应易于测试",这一理念,并最终带给我们一个易于开发.便于测试而又功能齐全的开发框架.概括起来Sprin ...
- AngularJS进阶(十二)AngularJS常用知识汇总(不断更新中....)
AngularJS常用知识汇总(不断更新中....) 注:请点击此处进行充电! app.controller('editCtrl',['$http','$location','$rootScope', ...
- hibernate链接数据库链接池c3p0配置
[html] view plain copy <bean id="dataSourceLocal" name="dataSource" class=&qu ...
- rails将类常量重构到数据库对应的表中之三
经过博文之一和之二的重构,貌似代码表现的还不错,正常运行和test都通过鸟,但是,感觉告诉我们还是有什么地方不对劲啊!究竟是哪里不对劲呢?我们再来好好看一下. 我们把数据库表中的支付方式集合直接放在实 ...
- ELF 动态链接 so的动态符号表(.dynsym)
静态链接中有一个专门的段叫符号表 -- ".symtab"(Symbol Table), 里面保存了所有关于该目标文件的符号的定义和引用. 动态链接中同样有一个段叫 动态符号表 - ...
- ASP.NET平台下从浏览器地址栏输入之后发生的事
浏览器一般内嵌两个模块: Socket通信模块 → 浏览器将地址栏的数据及其他的数据放入http协议的请求头文件中,Socket将此http请求数据发送到远程服务器端 浏览器引擎渲染模块 → 浏览器接 ...
- windows from 手风琴
public class OutlookBar : Panel { private int SelectedBandHeight { get; set; } public int ButtonHeig ...