unity检测周围物体的方法总结

说明

检测的方法会有很多,关键在于分析出什么时候用什么方法最合适(效果好,性能高)

1.碰撞检测

(1)碰撞检测的条件:
二者均有碰撞组件,运动的物体具有刚体组件,且其中至少一个碰撞器附加非动力学刚体。
(2)碰撞检测的方法:
在这些方法中做想做的事即可,这些方法会在unity脚本生命周期中自动调用,不用我们操心。

  • 当前collider/rigidbody开始碰到另一个rigidbody/collider时OnCollisionEnter被调用。
void OnCollisionEnter(Collision collision)
  • 每当该collider/rigidbody碰到其他rigidbody/collider,将在每帧调用OnCollisionStay。通俗的说,一个碰撞器或刚体碰到另一个刚体或碰撞器,在每帧都会调用OnCollisionStay,直到它们之间离开不接触。
void OnCollisionStay(Collision collisionInfo)
  • 当前collider/rigidbody停止碰撞另一个 rigidbody/collider时,OnCollisionExit被调用。
void OnCollisionExit(Collision collisionInfo)

这几个方法中的参数即为与当前碰撞体发生碰撞的Collision,而触发器检测到的是当前碰撞体发生碰撞的Collider,Collision中有collider和rigidbody属性,并且Collision中有一个至关重要的属性:contacts,碰撞点集合,类型为ContactPoint[],ContactPoint类也有很多常用的属性和方法,去文档看!!!我们可以由contacts[0]得到第一个碰撞点contacts[0].point,在那里搞一些事情,比如碰撞的火花… …,还可以由contacts[0].normal得到接触面的法线(碰撞方向就得到了)。

2.触发器检测

触发器就是带碰撞器组件且勾选Is Trigger的物体,没有碰撞效果仅用来检测碰撞(相交)。
(1)触发条件:
两者均有碰撞器,至少一个勾选Trigger。
(2)触发方法:
在这些方法中做想做的事即可,这些方法会在unity脚本生命周期中自动调用,不用我们操心。

  • 当碰撞器进入触发器时OnTriggerEnter被调用。
void OnTriggerEnter(Collider other)
  • 每当碰撞器从进入触发器,几乎每帧调用OnTriggerStay。
    注意:OnTriggerStay函数是基于物理计时器,因此它未必每帧都运行。
    也就是说OnTriggerStay是在每一个Time.fixedDeltaTime的时间节点上运行,不是Time.deltaTime的时间节点上运行
void OnTriggerStay(Collider other)
  • 当该碰撞器停止接触触发器时,OnTriggerExit被调用。也就是说,当碰撞器离开触发器时,调用OnTriggerExit。
void OnTriggerExit(Collider other)

注意这里的参数就是与当前碰撞器接触的Collider了。

3.射线检测

前两种方法在一些细致的地方需要我们对物理引擎有很好的了解才能做的更真实,设置刚体的属性一直是我最恶心的事情,所以我宁愿用角色控制器也不会自己去给模型配置刚体、碰撞器(当然这只是对角色,对地形(死物)用前两种还是很方便的)。
那么有时候我们也必须舍弃前两种方法,尽管它们的事件响应方式很香,比如快速飞行的物体(如子弹)可能会在一帧之内穿过,而前两种方法一帧检测一次,很容易出现物体穿过但检测不到的情况,这是我们就必须给快速飞行的物体做射线检测。

(1)射线检测需要用到的类/结构:

又很多现成的属性和方法供我们很方便的进行这些事情。

i.RaycastHit 射线投射碰撞信息:

重要属性:

  • collider 碰到的碰撞器。
  • distance 从射线的原点到触碰点的距离。
  • normal 射线触碰表面的法线。
  • point 在世界坐标空间,射线碰到碰撞器的接触点。

ii.Ray

  • origin 射线的原点。
  • direction 射线的方向。
  • Ray(Vector3 origin, Vector3 direction); 构造器。

(2)用Physics中的众多射线检测方法:

Physics类中又很多不同形式的射线检测,这里总结我常用的几个以及例子:
注:下面这些方法会有很多重载,但大多都是功能性和表达上的一些区别,根据需要看官方文档去选择合适的重载。

i.线投射:适用于有检测距离的方向性检测

public static bool Linecast(Vector3 start, Vector3 end, out RaycastHit hitInfo, int layerMask = DefaultRaycastLayers);
在线的开始和结束位置之间,如果与任何碰撞器相交返回真,hitInfo存储相交碰撞器的信息,Layer mask是用来指定投射的层。

ii.射线投射:在场景中投下可与所有碰撞器碰撞的一条光线,并返回碰撞的细节信息。

public static bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
参数
origin 在世界坐标,射线的起始点。
direction 射线的方向
hitInfo 将包含碰到碰撞器的更多信息。
distance 射线的长度。
layermask 选择投射的层蒙版。
queryTriggerInteraction 指定是否查询碰到触发器。

iii.射线投射列表:在场景投射一条射线并返回所有碰撞。注意不保证顺序。

public static RaycastHit[] RaycastAll(Ray ray, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
参数
origin射线的开始点。
direction射线的方向。
maxDistance 从射线开始允许投射的最大距离。
layermask 选择投射的层蒙版。
queryTriggerInteraction指定是否查询碰到触发器。

iv.球形射线:返回球内或与之接触的所有碰撞器。

我常用它去检测周围的各种物体。比如施放技能打到的物体,敌人巡逻区域发现玩家… …
public static Collider[] OverlapSphere(Vector3 position, float radius, int layerMask = AllLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
参数
position球体中心。
radius 球的半径。
layerMask 选择投射的层蒙版。
queryTriggerInteraction 指定是否查询碰到触发器。

v.球形投射列表

这个和上面的区别就是这个返回所有球形扫描到的碰撞信息。 功能更强大。
public static RaycastHit[] SphereCastAll(Vector3 origin, float radius, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers);
origin 扫描开始的球形中心点。
radius 球的半径。
direction 球形扫描的方向
distance 扫描的长度。
layerMask 仅扫描指定层的碰撞器。

(3)实例:

发射子弹看看是否击中游戏对象,以及击中的是什么游戏对象,其实这和子弹就没关系了,我们只需要对枪口做射线检测,根据不同的需求去选择合适的重载方法,这里我们就是单纯的去检测:下面是项目中的子弹类(开始位置就是枪口位置)

/// 
/// 子弹,定义子弹共有行为
/// 
public class Bullet : MonoBehaviour
{
    //计算目标点(用前面讲的射线检测)
    protected RaycastHit hit;
    //攻击距离
    public float attackMaxDistance = 200f;
    //打哪些层
    public LayerMask mask;
    //目标点
    private Vector3 target;
    //移动速度
    public float moveSpeed = 200;
    //攻击力
    [HideInInspector]//在编译器中隐藏
    public float atk;
    /// 
    /// 计算目标点
    /// 
    private void CalculateTargetPoint()
    {
        if(Physics.Raycast(transform.position, transform.forward,out hit,attackMaxDistance, mask))
        {
            target = hit.point;
        }
        else
        {
            target = transform.position + transform.forward * attackMaxDistance;
        }
    }
    private void Awake()
    {
        CalculateTargetPoint();
    }
    //移动
    private void Update()
    {
        Movement();
        if ((transform.position - target).sqrMagnitude < 0.1f)
        {
            Destroy(this.gameObject);
            GenerateContactEffect();
        }
    }
    private void Movement()
    {
        transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
    }
    //达到目标点:摧毁、创建相关特效(根据目标点的便签(在射线检测中的信息有碰撞器组件collider.tag))
    //创建相关特效(根据目标点的便签(在射线检测中的信息有碰撞器组件collider.tag))
    private void GenerateContactEffect()
    {
        if (hit.collider == null)//没打到东西就没效果
        {
            return;
        }
        //switch (hit.collider.tag)
        //{
        //    case "":

        //        break;
        //}
        //对于资源较多时通过代码读取(资源必须放在Resources文件夹下)[以后会用对象池替代]
        GameObject prefabGo = Resources.Load<GameObject>("ContactEffects/Effects"+hit.collider.tag);
        if (prefabGo)
            //创建资源(应朝向目标点的法线并向法线方向向上移动一点)
            Instantiate(prefabGo, target+hit.normal*0.01f, Quaternion.LookRotation(hit.normal));
    }
}

4.使用代码(如标签)

很多时候我们对周围物体的检测有各种各样的要求,比如在攻击选择时,我们只想选取攻击范围内且离我们最近、血量大于0、给的金币最多…的小怪,这样不太好用射线了,而且射线消耗性能要远高于我们去通过标签找到想要选择的游戏对象类型,我们就需要自己写代码了。这里主要通过项目中的例子来体现:
eg:技能的攻击选择方法

namespace ARPGDemo.Skills
{
    /// 
    /// 圆形攻击选择类:选择圆形区域中的敌人作为攻击目标
    /// 
    public class CircleAttackSelector: IAttackSelector
    {
        /// 
        /// 选择目标方法
        /// 
        /// 技能对象,选择目标取决于它
        /// 选择时的参考点(一般就是以主角自身(技能拥有者)呗)
        /// 
        public GameObject[] SelectTarget(SkillData skillData, Transform ownerTransform)
        {
            //1.通过射线找:在物体没有tag标记时这样找
            //2.有tag标记通过tag找性能高
            //在这通过射线查找(攻击距离为半径),在扇形中用tag
            var colliders = Physics.OverlapSphere(ownerTransform.position, skillData.attackDistance);
            if (colliders == null || colliders.Length == 0)
            {
                return null;
            }
            //找出标记为xx(在attackTargetTags中的)的所有物体
            //并且是活着的(Hp>0)
            var allCanBeSelected = ArrayHelper.FindAll(colliders, 
                c => (Array.IndexOf(skillData.attackTargetTags, c.tag) >= 0) 
                && (c.gameObject.GetComponent<CharacterStatus>().HP > 0));
            if (allCanBeSelected == null || allCanBeSelected.Count == 0)
            {
                return null;
            }
            //根本不用这样,列表有ToArray方法
            //因为数组助手类的参数都是数组,而tmp从FindAll中的返回类型是List,所以要转为数组
            //Collider[] allCanBeSelected = new Collider[tmp.Count];
            //for (int i = 0; i < tmp.Count; i++)
            //{
            //    allCanBeSelected[i] = tmp[i];

            //}
            //根据技能攻击类型确定返回单个或多个目标
            switch (skillData.attackType)
            {
                case SkillAttackType.Group://群攻就都选
                    return ArrayHelper.Select(allCanBeSelected.ToArray(), a => a.gameObject);
                    break;
                case SkillAttackType.Single://单攻就选最近的
                    var collider = ArrayHelper.Min(allCanBeSelected.ToArray(), 
                        a => Vector3.Distance(ownerTransform.position, a.transform.position));
                    return new GameObject[] { collider.gameObject };
                    break;
            }
            return null;
        }

    }
}

扇形选择就是多了角度的限制:

			//再找范围以内的
            //并且是活着的(Hp>0)
            //并且有角度
            var allCanBeSelected = allTargets.FindAll(
                a => Vector3.Distance(ownerTransform.position, a.transform.position) <= skillData.attackDistance
                && a.GetComponent<CharacterStatus>().HP > 0
                && TransformHelper.RangeFind(a.transform, ownerTransform, skillData.attackAngle));

这里面是lambda表达式,非常方便,不过主要还是用代码体现检测的思想。

5.角色控制器CharacterController

角色控制器让你在受制于碰撞的情况下很容易的进行运动,而不用处理刚体。
这简直是我的最爱了,就是因为他不用为角色配置复杂的刚体属性和碰撞器,却能通过void OnControllerColliderHit(ControllerColliderHit hit)检测到与之碰撞的物体。
ControllerColliderHit中也有如collider、controller、normal、point、rigidbody这些重要的属性来帮助我们获取碰撞体的信息,具体看官方手册。
unity检测周围物体的方法总结_第1张图片

你可能感兴趣的:(Unity,开发)