场景分区

场景分区一般用于开放大世界,是流式加载,实现动态加载或者卸载的做法,避免一次性就要加载整个世界场景
ai真是世界上最伟大的发明,以下都是AI教给我的

为什么要分区:
1.内存需要,避免一次性加载太大的场景或者里面的事物,导致内存满了
2.为了加载速度,避免卡顿
3.性能要求,场景太大,每帧更新的东西变多了,容易掉帧

分区的几种方法:
1.手动分区
人工在场景上划定区域边界,一般使用体积触发器
优势是完全可控,可以人为界定边界形状
缺点是人工耗费大,不够灵活
2.标准网格划分
按照固定大小将2D场景或者3D场景用长方形或者长方体来进行网格划分
优势是实现简单,逻辑清晰,空间查询效率高
缺点是有些物体被网格切割,需要特殊处理,过于标准化,有的地方密集,有的地方稀疏。
3.四叉树,八叉树划分
2D用四叉树,3D用八叉树。
先把整个3D场景划分为八叉树,设定一个值即每个分区不得多于这个值X,然后计算各个分区中的物体个数,如果高于X,则将这个分区继续划分八叉树,直到小于这个值为止。
优点:
自适应: 密集区域自动细分,稀疏区域保持大块,内存利用更高效。
高效的空间查询: 快速进行视锥体裁剪、光线检测、邻近搜索。
缺点:
树结构构建和维护稍复杂。
物体跨节点边界时,需要存储在多节点中(或使用松散边界)。

树的构建过程:
四叉树(Quadtree)和八叉树(Octree)是空间划分数据结构的核心实现,用于高效管理2D/3D场景中的对象。以下是它们的实现逻辑详解,包含核心步骤、伪代码和关键优化:


一、四叉树(Quadtree)实现逻辑

适用场景:2D平面游戏(如策略游戏、2D开放世界)、地形管理、碰撞检测、光照计算。

1. 数据结构定义

class QuadtreeNode {
    Rectangle bounds;      // 当前节点覆盖的矩形区域
    int capacity;          // 节点容纳对象的最大数量(分裂前)
    List<GameObject> objects; // 当前节点存储的对象
    QuadtreeNode[] children; // 四个子节点 [NW, NE, SW, SE]
    bool isDivided = false;  // 是否已分裂
}

2. 核心操作流程

(1) 插入对象

def insert(node, object):
    if object not in node.bounds: 
        return False  # 对象不在节点范围内
    
    if not node.isDivided and len(node.objects) < node.capacity:
        node.objects.append(object)  # 存入当前节点
        return True
    
    if not node.isDivided:
        node.split()  # 触发分裂
        
    # 递归插入到子节点
    for child in node.children:
        if child.bounds.contains(object):
            child.insert(object)
            return True
    
    # 若对象跨越多个子节点,仍存于父节点
    node.objects.append(object)
    return True

(2) 分裂节点

def split(node):
    x = node.bounds.x
    y = node.bounds.y
    w = node.bounds.width / 2
    h = node.bounds.height / 2

    # 创建四个子区域
    nw = Rectangle(x, y, w, h)
    ne = Rectangle(x + w, y, w, h)
    sw = Rectangle(x, y + h, w, h)
    se = Rectangle(x + w, y + h, w, h)

    node.children = [
        QuadtreeNode(bounds=nw, capacity=node.capacity),
        QuadtreeNode(bounds=ne, capacity=node.capacity),
        QuadtreeNode(bounds=sw, capacity=node.capacity),
        QuadtreeNode(bounds=se, capacity=node.capacity)
    ]
    node.isDivided = True

    # 将父节点对象重新分配到子节点
    for obj in node.objects:
        for child in node.children:
            if child.bounds.contains(obj):
                child.insert(obj)
    node.objects.clear()  # 父节点不再存储对象

(3) 查询对象

def query(node, search_area, results):
    if not node.bounds.intersects(search_area):
        return  # 无交集
    
    # 检查当前节点对象
    for obj in node.objects:
        if search_area.contains(obj):
            results.add(obj)
    
    # 递归查询子节点
    if node.isDivided:
        for child in node.children:
            child.query(search_area, results)

(4) 删除对象
类似插入过程,需遍历可能包含该对象的节点,删除后检查节点是否需要合并(见优化部分)。


二、八叉树(Octree)实现逻辑

适用场景:3D游戏(物理碰撞、渲染剔除)、点云管理、体素世界。

1. 数据结构定义

class OctreeNode {
    BoundingBox bounds;     // 3D包围盒
    int capacity;
    List<GameObject> objects;
    OctreeNode[] children;  // 8个子节点 [TopNW, TopNE, TopSW, TopSE, BottomNW, ...]
    bool isDivided = false;
}

2. 核心操作流程

(1) 插入对象
逻辑与四叉树相同,但需检查3D包围盒相交,并递归至8个子节点。

(2) 分裂节点

def split(node):
    center = node.bounds.center
    size = node.bounds.size / 2

    # 计算8个子立方体
    children_bounds = []
    for z in [0, 1]:        # 上下两层
        for y in [0, 1]:    # 南北
            for x in [0, 1]:# 东西
                offset = Vector3(x * size.x, y * size.y, z * size.z)
                child_bb = BoundingBox(
                    min = node.bounds.min + offset,
                    max = node.bounds.min + offset + size
                )
                children_bounds.append(child_bb)
    
    node.children = [OctreeNode(bounds=cb) for cb in children_bounds]
    node.isDivided = True
    # ...(对象重新分配逻辑同四叉树)

(3) 空间查询
支持多种查询类型:

def query(node, condition, results):
    if not node.bounds.intersects(condition):
        return
    
    for obj in node.objects:
        if condition.test(obj):  # 条件可能是:点在范围内、射线相交、球体碰撞等
            results.add(obj)
    
    if node.isDivided:
        for child in node.children:
            child.query(condition, results)

关键优化策略

  1. 松散边界(Loose Bounds)

    • 问题:物体位于边界时可能被多个节点存储,浪费内存。
    • 解决:扩大节点边界(如1.2倍),物体只存入唯一节点。
    // 创建节点时扩展边界
    looseBounds = bounds.Expand(1.2f);
    
  2. 节点合并(Merge)

    • 条件:当子节点总对象数 < 阈值时,合并子节点。
    def try_merge(node):
        if not node.isDivided: 
            return
        
        total_objects = len(node.objects)
        for child in node.children:
            total_objects += len(child.objects)
        
        if total_objects < MERGE_THRESHOLD:
            node.objects = collect_all_objects(node)  # 收集所有子节点对象
            node.children = []
            node.isDivided = False
    
  3. 动态物体处理

    • 移动时更新:物体位置变化后,检查是否跨越节点边界。
      void OnObjectMoved(GameObject obj) {
          oldNode.Remove(obj);
          rootNode.Insert(obj); // 重新从根节点插入
      }
      
    • 优化:记录物体所在节点,仅检查相邻节点。

4.BVH
原理: 基于场景中物体的包围盒(Bounding Volume)构建层次树,不依赖固定空间网格。
将场景中的对象或者对象组用包围体包裹起来,然后递归地将这些包围体分组,形成一棵树,树根包含整个场景,叶子节点包含单个对象或其几何元素。
构建策略:
1.自顶向下:从根节点开始,选择一个分割策略,将对象列表划分到左右子树,递归进行
2.自底向上:先将邻近的对象两两合并成小包围盒,再递归合并这些小包围盒形成更大的包围盒,直到根节点。
优点: 对物体分布极度不均匀的场景非常高效。
缺点: 树结构动态变化时更新开销较大(常用于静态或半静态场景)。
适用: 主要用于光线追踪加速、静态场景优化

5.BSP树
用平面分割空间
这个好难理解。。。。暂空着

分区的关键要素:
1.分区标识与邻居关系:
每个分区需要一个唯一ID
每个分区需要直到其相邻分区(上下左右前后),用于预测玩家移动方向,并预加载邻近分区。
2.分区加载/卸载范围:
活动集:围绕玩家当前位置的一组“活跃”分区(正在被更新和渲染)
加载范围:以玩家为中心,一定半径内的分区需要加载到内存(范围可动态调整)
卸载范围:超出玩家一定距离的分区被标记为可卸载
缓冲层:在活动集外增加一层“预加载”分区,确保玩家移动到边界时新分区已就绪
3.动态物体管理:
归属分区:每个动态物体(玩家,NPC,车辆)需要关联到一个“宿主”分区
跨分区移动:
物体移动时监测是否跨越分区边界
若跨越边界,需要将其从旧分区移除,添加到新分区,并通知相关系统(物理,AI)
全局物体:飞行物,远距离投射物等可能需要特殊处理
4.静态物体优化:
静态物体(建筑,地形)通常直接绑定到所属分区
使用LOD和遮挡剔除在分区内进一步优化渲染

流式加载
1.跟踪玩家位置
每帧更新玩家所在分区
2.计算需要加载卸载的分区
根据玩家当前位置,移动方向,速度预测未来可能进入的分区
结合加载范围和缓冲层策略生成待加载分区列表
标记超出卸载范围且未被引用的分区为待卸载
3.异步加载
在后台线程加载心分区的资源
显示加载进度条或提示
4.初始化分区
资源加载完成后,在主线程初始化分区:
实例化静态物体
激活预置的动态物体生成点
连接导航网格
注册触发器,事件
5.卸载分区
安全销毁或回收分区内的对象实例
卸载不再需要的资源
释放物理,导航等系统占用的资源

关键考虑因素与选择策略
不太容易理解,需要再多看看
1.场景静态 vs. 动态:
静态场景: KD-Tree, BSP, 离线构建的高质量八叉树/BVH 是很好的选择,构建一次后查询效率极高。
动态场景: 空间哈希、均匀网格、动态更新的 BVH(特别是 LBVH 等优化变体)、松散的八叉树(允许对象存在于父节点)更适合。需要权衡更新开销和查询效率。

2.查询类型:
点查询/小范围查询: 网格、空间哈希、所有树结构都表现良好。
大范围查询/区域查询: 树结构(BVH, 四/八叉树)通常比需要遍历大量网格单元或哈希桶的网格/哈希更优。
射线查询: BVH 和 KD-Tree 通常是最高效的选择。
最近邻查询: 通常需要专门结构如 k-d tree(用于点云)或结合 BVH/八叉树的算法。

3.对象大小与分布:
大小均匀且分布均匀:均匀网格简单高效。
大小差异大或分布不均:四/八叉树、BVH、KD-Tree 能更好地适应。
大量微小动态对象:空间哈希是强项。

4.内存限制:
网格、空间哈希内存通常可预测(固定网格大小或桶数)。
树结构(四/八叉树、BSP、BVH)内存占用取决于场景复杂度和树的深度/平衡性,可能变化较大。

5.实现复杂度:
均匀网格、空间哈希相对简单。
四叉树/八叉树中等。
BVH、KD-Tree、BSP 实现更复杂,尤其是高效的构建和更新算法。

6.混合策略:
现代游戏引擎通常不会只使用一种分区方案,而是根据子系统的需求选择最合适的,甚至组合使用:
用四叉树/八叉树管理大地形和静态对象。
用空间哈希或网格管理大量小动态对象(粒子、弹丸)。
用 BVH 管理需要光线追踪或复杂碰撞的静态/动态对象。
用单独的 BSP/K-D Tree 管理静态几何用于精确碰撞或特定 AI 路径。
在流式加载的大型开放世界中,外层用粗粒度的网格或四叉树管理区块加载,区块内部再用更细粒度的结构(如 BVH)管理对象。

posted @ 2025-06-18 12:08  木土无心  阅读(96)  评论(0)    收藏  举报