用四叉树加速碰撞检测

本文介绍了四叉树的基本概念及其在碰撞检测中的应用,通过四叉树的空间划分,将原本复杂度为O(n^2)的碰撞检测降低到O(n log n)。在Unity中实现四叉树时,文章讨论了边界问题、动态更新和单个与集体插入的策略,并提出了创建、检索、删除和插入四叉树节点的思路。虽然存在一些优化挑战,如动态更新时的效率问题,但四叉树仍然是提高大规模碰撞检测效率的有效工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

长话短说

1.Define Quad Tree

四叉树的作用就是从原点开始根据一定的范围画出周围的四个象限,然后每个子象限继续划分为四个孙子象限,以此类推,到达递归深度最大值时停止。
在这里插入图片描述

2.在碰撞检测中的作用

如果场景里有1万个障碍物和2个子弹,那么子弹需要和每个障碍物进行一次碰撞检测,复杂度Omn。而我们进行了空间划分之后,只需要将子弹与其所在的区域内的障碍物进行检测即可,复杂度Omlogn。
相当于将碰撞检测分成了两部分,第一部分(粗测)尽可能将不会碰到的物体扔掉,从所有的物体中筛选出与子弹最接近最可能碰到的物体,第二部分(精测)使用尽可能精确的碰撞检测公式去检测是否碰撞,简单的有几行代码就能写出来的简单几何体碰撞,复杂的有GJK算法等。

3.Unity中的实现

如果把二分查找的每个步骤都存储起来,那么最终的结果看起来就像是一个二叉树,那么可以参考二分查找的步骤去创建。

3.1.设计

类名 作用
Obstacle 障碍物
QTree 四叉树集合的封装类
QNode 四叉树的每个节点
Bound 包围盒,定义物体的碰撞体积,这里用的无旋转矩形

需要的初始化参数:

参数
每个节点内最大物体数量
四叉树坐标和大小
四叉树的最大递归深度

3.2 坑

3.2.1 边界问题

在这里插入图片描述

如果一个物体正好脚踩N个象限,如何对敌?
两种方法,1.脚踩的每个象限都存储该物体的索引,好处是分的清楚,检测的效率高,但缺点是不好管理,因为这样会产生一大堆同样的索引,占内存。2.将该物体交给能完整的包住该物体的父节点,优点是号管理,每个物体都只属于一个节点,但缺点是,举个例子,如果有个比较小的物体正好在四叉树根节点的中心位置,那么任何要进行碰撞检测的物体都要与该物体检测,增加了许多无用的检测,效率低。
我选的第二个方法,因为我觉得游戏中需要移动的碰撞体相比静态物体还是少数,可以通过提前手动修改静态物体位置的方法尽量减少效率低的问题。

3.2.2 动态更新

假设最坏的情况,场景中的每个碰撞体都是移动的,那么问题来了,怎么更新呢?很好理解,删除+插入。但是每一帧重建整个四叉树显然是不现实的,所以这里面有很多优化的trick,比如只更新可能移动的物体;比如只搜索原物体附近的区域,进行删除和插入,但是这样解决不了闪现的问题;或者将物体新坐标与旧坐标检测是否在同一区域的方法来剪枝,等等。
本文暂时不考虑优化。

3.2.3 单个插入和集体插入的不一致

这个是我自己造的词和坑。
举个例子,假如有100个障碍物,每个节点最多存1个,如果是一起插入四叉树,那么根节点大概率是不存物体的,因为有100个障碍物都可以插入根节点,早就超出单个节点的最大容量了。而单个插入就不同了,你在插入时并不知道其他节点的信息,那么第一个插入的物体一定就在根节点。
不过我感觉这两者区别的影响也并不是很大。

3.3 思路

3.3.1 创建四叉树

拿到要插入的物体的集合后
(1).计算该节点四个象限内和跨象限的物体
(2).(1)如果全部节点个数小于节点最大物体个数或者到达最大递归深度,直接全部存到该节点中
(2).(2)否则跨象限的物体存到该节点中,而四个象限的物体传入四个子节点的构造函数中。

3.3.2 检索

(1).遍历节点内的所有物体,进行碰撞检测
(2).如果节点为空,检查该物体在该节点的哪个象限,递归的去找该象限的所属节点

3.3.2 删除

(1).从物体的所属节点中删除该物体
(2).如果该物体的所属节点内没有物体且该节点没有子节点,则删除该节点。
(3).递归的检查该节点的父节点是否需要删除

3.3.3 插入

(1).(1)遇到最大深度的节点或者该节点内的物体未到达最大容量,直接插入该节点
(1).(2)否则,仿照创建四叉树的套路

3.4 代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public enum QTNodeType
{
   
    /// <summary>
    /// 左上
    /// </summary>
    LT = 0,
    /// <summary>
    /// 右上
    /// </summary>
    RT = 1,
    /// <summary>
    /// 右下
    /// </summary>
    RB = 2,
    /// <summary>
    /// 左下
    /// </summary>
    LB = 3,
    /// <summary>
    /// 根节点
    /// </summary>
    Root = 4,
}

public class Bound
{
   
    public float X;
    public float Y;
    public float Width;
    public float Height;

    public Bound(float x, float y, float w,float h)
    {
   
        X = x;
        Y = y;
        Width = w;
        Height = h;
    }

    public static bool CheckCollision(Bound b1, Bound b2)
    {
   
        float[] rec1 = {
    b1.X - b1.Width / 2, b1.Y - b1.Height / 2, b1.X + b1.Width / 2, b1.Y + b1.Height / 2, };
        float[] rec2 = {
    b2.X - b2.Width / 2, b2.Y - b2.Height / 2, b2.X + b2.Width / 2, b2.Y + b2.Height / 2, };
        return !(rec1[2] <= rec2[0] || rec2[2] <= rec1[0] || rec1[3] <= rec2[1] || rec2[3] <= rec1[1]);
    }
}

public class QTree
{
   
    public int MaxObjCnt;
    public int MaxDepth;
    public QNode Root;
    public Bound Bound;
    public List<Obstacle> ObjList;

    public QTree(List<Obstacle> objList,Bound bound,int maxObjCnt,int maxDepth)
    {
   
        this.MaxObjCnt = maxObjCnt;
        this.ObjList = objList;
        this.MaxDepth = maxDepth;
        Bound = bound;
        Root = new QNode(objList,Bound, 0,Root, this,QTNodeType.Root);
    }
    public void RenderTree()
    {
   
        Root.RenderNode();
    }
    public void SearchNode(Bullet bullet)
    {
   
        Root.SearchNode(bullet);
    }
    public void InsertObj(Obstacle obj)
    {
   
        Root.InsertObj(obj);
    }
    public void DeleteObj(Obstacle obj)
    {
   
        obj.BelongedNode.DeleteObj(obj);
    }
    public void UpdateObj(Obstacle obj)
    {
   
        // 删除后重新插入
        DeleteObj(obj
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值