赞
踩
在实际游戏开发时,不可避免地要⽤到各种射线检测。即便是⼀个不怎么⽤到物理系统的游戏,也很可能要⽤到射线检测机制。换句话说,射线检测在现代游戏开发中应⽤得⾮常⼴泛,超越了物理游戏的范围。下⾯简单举⼏个例⼦。
(1)游戏中有单击地⾯的操作,因此要发射射线以确定是否点中了可单击区域和单击位置的坐标。
(2)在判定⼦弹或技能是否击中⽬标时,如果采⽤碰撞体需要考虑⼦弹速度,且存在穿透问题,⽽射线是没有速度的(瞬时发⽣),不仅易于使⽤,⽽且综合效率更⾼。
(3)在3D动作游戏或2D动作游戏中,判断玩家是否落地时,可以向⾓⾊脚下发射射线;判断玩家是否接触墙壁时,可以往左右两侧发射射线;判断玩家是否需要低头时,可以往头顶发射射线;判断玩家是否需要攀爬时,同样也可以采⽤射线检测的⽅法。
(4)因为射线与视线⼀样会被障碍物阻挡,所以在游戏AI设计中,可以⽤射线模拟AI⾓⾊的视线。
注意,上所述的各种射线检测都是以物理系统为基础的。射线需要与碰撞体和触发器配合才能发挥出作⽤。
下⾯来介绍⼀下射线编程⽅法。
常⽤的直线型射线⽤类型Ray表⽰。Ray包含了origin(起点)和direction(⽅向)的定义,起点和⽅向都⽤Vector3类型表⽰,前者是⼀个坐标,后者是⼀个表⽰⽅向的向量。
有很多⽅法可以在游戏世界中发射⼀条射线,最常⽤的⽅法是Physics.Raycast()和Physics.RaycastAll()。由于实践中有各式各样的具体应⽤场景,因此Physics.Raycast()⽅法的重载有10种以上,不过实际⼤同⼩异,例如以下3种。
- bool Raycast(Vector3 origin, Vector3 direction);
- bool Raycast(Vector3 origin, Vector3 direction, float maxDistance);
- bool Raycast(Vector3 origin, Vector3 direction, float maxDistance, int
- layerMask);
以上3个函数共同的参数都是发射点坐标和⽅向向量,返回值都是是否击中了某个碰撞体或触发器。
第3个参数maxDistance的作⽤是指定射线的最⼤⻓度。虽然名字叫作“射线”,但与⼏何中的射线不同,这⾥的“射线”更多是“发射”的意思。例如游戏中经常通过往⾓⾊脚下发射很短的射线(0.01,代表1厘⽶)来判断⾓⾊是否站在地上。
除了指定⽅向和位置的射线以外,以下还有⼀类很常⽤的重载形式。
- bool Raycast(Ray ray, out RaycastHit hitInfo);
- bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance);
- bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int
- layerMask);
这种形式的射线检测⽤了⼀种常⽤结构体Ray(射线),它只是将射线数据对象先单独创建出来,并没有实际区别。Ray对象有多种创建⽅法,例如以下⽅法。
- // 创建从原点向上的射线
- Ray ray = new Ray(Vector3.zero, Vector3.up);// 获得当前⿏标指针在屏幕上的位置(单位是像素)
- Vector2 mousePos = Input.mousePosition;
- // 创建⼀条射线,起点是摄像机位置,⽅向指向⿏标指针所在的点(隐含了从屏幕到世界的坐标转
- 换)
- Ray ray2 = Camera.main.ScreenPointToRay(mousePos);
- // 之后可以将ray或ray2发射出去,例如:
- Physics.Raycast(ray, 10000, LayerMask.GetMask("Default"));
这些重载形式的第2个参数,即类型为RaycastHit的参数hitInfo也很有⽤,它保存着详细的碰撞信息,如碰撞点的配置、法线等。碰撞信息会在第3.2.6⼩节重点详细讲解。
很多时候,需要射线仅被某些物体阻挡,例如希望检测地⾯的射线只检测地⾯,⽽不要检测其他东⻄,也就是说应当穿过地⾯以外的东⻄。那么这⾥就要⽤到Layer和Layer Mask(层遮罩)的概念了。
“层”的概念让物理系统变得更加好⽤和实⽤。例如⼀条⼦弹射线,仅让它碰到Ground(地⾯)、Player(玩家⾓⾊)和Obstacle(障碍物)这3个层,⽽不会和其他层的物体碰撞,其编写代码如下。
- int mask = LayerMask.GetMask("Ground", "Player", "Obstacle");
- if (Physics.Raycast(transform.position, Vector3.forward, mask))
- {
- // 碰到了物体
- }
某些读者可能会很好奇,“与某3层碰撞”这⼀条件竟然⽤⼀个int就能表⽰。这其实是⼀种⼆进制的妙⽤,⽤⼀个int最多可以表⽰32个层的遮罩,Layer和Tag最多也只有32个,这不是巧合。
如果让mask表⽰这3层以外的所有层,则⽤⼀个⼆进制的取反运算即可,其⽅法如下。
- mask = ~mask; // 英⽂波浪线,代表⼆进制取反
- 有时需要改变物体所在的层,如将⼀个物体设置在Default层上,其⽅法如下。
- gameObject.layer = LayerMask.NameToLayer("Default");
可以通过函数LayerMask.NameToLayer()将层名称转化为整数表⽰的层,也可以⽤函数LayerMask.LayerToName()将表⽰层的整数转化为层名字。
1. 射线碰撞信息
前⽂举例的函数的返回值仅仅是“是否碰到了物体”,⽽⽆法确定碰撞点是哪⾥,也不知道碰到的物体是哪⼀个。射线检测其实有着丰富的碰撞信息,如可以获取到碰撞点坐标、被碰撞物体的所有信息,甚⾄可以获取到碰撞点的法线(碰撞点所在物体平⾯的朝向)。这些丰富的碰撞信息,都被保存在RaycastHit结构体中。例如,以下⼏个Raycast()函数的重载可以获取到碰撞信息。
- bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo,
- float
- maxDistance);
- bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo,
- float
- maxDistance, int layerMask);
- bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int
- layerMask);
- 综合⽤法演⽰如下。
- private void TestRay()
- {
- // 声明变量,⽤于保存碰撞信息
- RaycastHit hitInfo;
- // 发射射线,起点是当前物体的位置,⽅向是世界前⽅
- if (Physics.Raycast(transform.position, Vector3.forward, out
- hitInfo))
- {
- // 如果确实碰到物体,会运⾏到这⾥。没碰到物体就不会
- // 获取碰撞点的坐标(世界坐标)
- Vector3 point = hitInfo.point;
- // 获取对⽅的碰撞体组件
- Collider coll = hitInfo.collider;
- // 获取对⽅的Transform组件
- Transform trans = hitInfo.transform;
- // 获取对⽅的物体名称string name = coll.gameObject.name;
- // 获取碰撞点的法线向量
- Vector3 normal = hitInfo.normal;
- }
以上例⼦基本涵盖了能从hitInfo中获取到的信息,更多碰撞信息可以查阅Raycastlift结构体的定义。
2. 其他形状的射线
射线不仅可以有⻓度,还可以有粗细和形状。除了前⾯所提到的直线射线,还有球形射线、盒⼦射线和㬵囊体射线,如图3-7所⽰。
- 与发射射线类似,各种形状的射线也有很多种函数重载,以下是⼏种常⽤的重载形
- 式。
- // 球形射线:
- bool SphereCast(Ray ray, float radius);
- bool SphereCast(Ray ray, float radius, out RaycastHit hitInfo);
- // 盒⼦射线:
- bool BoxCast(Vector3 center, Vector3 halfExtents, Vector3 direction);
- bool BoxCast(Vector3 center, Vector3 halfExtents, Vector3 direction, out
- RaycastHit hitInfo, Quaternion orientation);
- // 㬵囊体射线:
- bool CapsuleCast(Vector3 point1, Vector3 point2, float radius, Vector3
- direction);
- bool CapsuleCast(Vector3 point1, Vector3 point2, float radius, Vector3
- direction,
- out RaycastHit hitInfo, float maxDistance);
可以看出,球形射线、盒⼦射线和㬵囊体射线的发射函数与直线型射线是类似的。区别在于,球形射线需要指定球的半径;盒⼦射线需要指定盒⼦的中⼼点和盒⼦的半边⻓(边⻓的⼀半),如果有必要再加上盒⼦的朝向;㬵囊体的形状更为复杂,需要⽤point1、point2和radius(半径)这3个参数指定㬵囊体的起点和形状。
在实践中有各种不同的需求和情况,在必要时可以进⼀步查阅相关资料,并对参数的⽤法做实际的试验。本⼩节的最后还会介绍射线调试的⼀些技巧。
3. 穿过多个物体的射线
有时需要射线在遇到第⼀个物体时不停⽌,继续前进,最终穿过多个物体。使⽤Physics.RaycastAll()函数可以获取到射线沿途碰到的所有碰撞信息,该函数的返回值是RaycastHit数组。
- RaycastHit[] RaycastAll(Ray ray, float maxDistance);
- RaycastHit[] RaycastAll(Vector3 origin, Vector3 direction, float
- maxDistance);
- RaycastHit[] RaycastAll(Ray ray, float maxDistance, int layerMask);
- RaycastHit[] RaycastAll(Ray ray);
同样,也有球形穿越射线、盒⼦穿越射线和㬵囊体穿越射线,函数名称分别为SpherecastAll、BoxcastAll和CapsulecastAll。
4. 区域覆盖型射线(Overlap)
有时需要检测⼀个空间范围,例如炸弹爆炸时,范围10⽶之内的物体都会受到波及,那么这⾥需要的就不是⼀条射线,⽽是⼀个半径为10⽶的球形区域。物理系统也提供了这类函数,它们均以Physics.Overlap开头,列举如下。
- Collider[] OverlapBox(Vector3 center, Vector3 halfExtents, Quaternion
- orientation, int layerMask);
- Collider[] OverlapCapsule(Vector3 point0, Vector3 point1, float radius, int
- layerMask);
- Collider[] OverlapSphere(Vector3 position, float radius, int layerMask);
以球形覆盖检测OverlapSphere()为例,调⽤该函数时,会返回原点为position、半径为radius的球体内,满⾜⼀定条件的碰撞体集合(以数组表⽰),⽽这个球体称为“3D相交球”。
5. 射线调试技巧
射线检测函数类型多、重载多、参数多,可能会让读者看得⼀头雾⽔。在实际游戏开发中,虽然这些参数不容易填写正确,但也有很好的⽅法可以提⾼编程的效率。这个⽅法就是使⽤Debug.DrawLine()函数和Debug.DrawRay()函数,将看不⻅的射线以可视化的形式表现出来,⽅便查看参数是否正确。Debug.DrawLine()函数和Debug.DrawRay()函数的常⽤形式如下。
- void DrawLine(Vector3 start, Vector3 end, Color color);
- void DrawLine(Vector3 start, Vector3 end, Color color, float duration);
- void DrawRay(Vector3 start, Vector3 dir, Color color);
- void DrawRay(Vector3 start, Vector3 dir, Color color, float duration);
Debug.DrawLine()函数通过指定线段的起点、终点和颜⾊(默认红⾊),绘制⼀条线段;Debug.DrawRay函数则是通过指定起点和⽅向向量,绘制⼀条射线。两者的⽤法是相似的。
使⽤时要注意,发射射线时,参数通常为起点、⽅向向量和⻓度,⽽DrawLine()⽅法⽤的是起点和终点。应正确使⽤向量加法,避免看到的线条与实际射线不⼀致。下⾯举个例⼦以供读者参考。
- // 以⼀个简单的射线为例
- Raycast(起点, ⽅向向量, ⻓度);
- // 对应的可视化线条DrawLine(起点, 起点+⽅向向量.normalized * ⻓度, Color.red);
- // 其中nomalized是将向量标准化,即⽅向不变⻓度变为1
需要说明的是,这种绘制⽅法仅在开发期⽣效,不会出现在最终的游戏发布版中。在默认情况下,该辅助线仅在编辑器的场景窗⼝中可⻅。如果要在Game窗⼝中看到它,则需要单击Game窗⼝右上⾓的Gizmos(辅助线框)按钮,⽽且⽆论怎么设置,它都不会出现在最终的游戏发布版中。
以上函数的最后⼀个参数,即持续时间(duration)可以省略,省略后这条参考线只出现⼀帧。如果在代码中每帧都绘制线条,那么就可以省略该参数。如果这个线条只出现⼀帧且看不清,则可以填写⼀个较⼤的持续时间(单位是秒),让射线停留在屏幕上⽅以便查看。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。