UE 面试题库 · 整理版

第12章 算法面试题

本章共 27 道题,覆盖:排序、链表与哈希、树与二叉树、图与寻路、AI 决策算法、游戏数学与图形算法、碰撞检测与物理模拟、空间数据结构与渲染算法、并发与调度、常用编程题与设计模式等算法核心知识。

一、排序与查找

1.1 实现快速排序

★★ 中等

考点: 分治、基准值选择、排序优化

参考答案:

  1. 快速排序是分治思想的经典应用:选基准值 → 分区(小于基准在左、大于在右)→ 递归排序两侧;
  2. 优化点:
    • 三数取中选择基准值,避免已排序数组退化到 O(n²) 最坏情况;
    • 小数组(长度 < 10)改用插入排序,减少递归开销;
    • 尾递归优化(只递归小的一侧);相同元素聚集优化(三路快排)。
  3. 平均 O(n log n),最坏 O(n²),空间 O(log n)(递归栈);不稳定排序;
  4. 代码示例:
template<typename T>
void QuickSort(TArray<T>& Array, int32 Left, int32 Right) {
    if (Left >= Right) return;
    if (Right - Left < 10) { InsertionSort(Array, Left, Right); return; }  // 小数组
    T Pivot = Array[Left + (Right - Left) / 2];   // 三数取中
    int32 i = Left, j = Right;
    while (i <= j) {
        while (Array[i] < Pivot) i++;
        while (Array[j] > Pivot) j--;
        if (i <= j) { Swap(Array[i], Array[j]); i++; j--; }
    }
    QuickSort(Array, Left, j);
    QuickSort(Array, i, Right);
}
  1. UE 的 TArray::Sort() 基于 Introsort(快排 + 堆排 + 插入排序混合),可自定义比较器。

二、链表与哈希

2.1 实现 LRU 缓存

★★★ 困难

考点: 哈希表、双向链表、缓存淘汰策略

参考答案:

  1. LRU(Least Recently Used):淘汰最久未使用的数据,get/put 均要求 O(1);
  2. 数据结构:哈希表 + 双向链表
    • TMap 存 key → 链表节点指针,O(1) 定位;
    • 双向链表维护访问顺序,头部最近使用、尾部最久未使用;
    • 带头尾哨兵节点,简化边界处理。
  3. 核心操作:
    • Get:命中则移到链表头部并返回值;
    • Put:已存在则更新值并移到头部;不存在则插入头部,超容量时删除尾节点并同步移除哈希项。
  4. 代码示例(核心逻辑):
template<typename KeyType, typename ValueType>
class TLRUCache {
    struct Node { KeyType Key; ValueType Value; Node* Prev; Node* Next; };
    int32 Capacity;
    TMap<KeyType, Node*> Map;
    Node* Head; Node* Tail;   // 哨兵节点

    void AddToFront(Node* N) {
        N->Next = Head->Next; N->Prev = Head;
        Head->Next->Prev = N; Head->Next = N;
    }
    void RemoveNode(Node* N) { N->Prev->Next = N->Next; N->Next->Prev = N->Prev; }
    void MoveToFront(Node* N) { RemoveNode(N); AddToFront(N); }

public:
    TLRUCache(int32 Cap) : Capacity(Cap) {
        Head = new Node(); Tail = new Node();
        Head->Next = Tail; Tail->Prev = Head;
    }
    ValueType* Get(const KeyType& Key) {
        if (Node** Found = Map.Find(Key)) { MoveToFront(*Found); return &(*Found)->Value; }
        return nullptr;
    }
    void Put(const KeyType& Key, const ValueType& Value) {
        if (Node** Found = Map.Find(Key)) { (*Found)->Value = Value; MoveToFront(*Found); }
        else {
            if (Map.Num() >= Capacity) {            // 淘汰尾部
                Node* TailNode = Tail->Prev;
                RemoveNode(TailNode);
                Map.Remove(TailNode->Key);
                delete TailNode;
            }
            Node* NewNode = new Node{ Key, Value, nullptr, nullptr };
            AddToFront(NewNode);
            Map.Add(Key, NewNode);
        }
    }
};
  1. 易错点:命中后必须移动节点;删除节点要同步更新哈希表;用哨兵避免空链表判断。

三、树与二叉树

3.1 实现二叉树的层序遍历

★★ 中等

考点: BFS、队列、按层输出

参考答案:

  1. 层序遍历即 BFS:用队列逐层访问节点,输出每层的节点值;
  2. 按层分组关键:每轮先取当前队列长度 LevelSize,只处理本层节点,子节点入队留到下一轮;
  3. 也可用 DFS 递归(记录 level 深度)实现等价结果,但 BFS 更直观;
  4. 代码示例:
struct TreeNode {
    int32 Value; TreeNode* Left; TreeNode* Right;
    TreeNode(int32 Val) : Value(Val), Left(nullptr), Right(nullptr) {}
};

TArray<TArray<int32>> LevelOrder(TreeNode* Root) {
    TArray<TArray<int32>> Result;
    if (!Root) return Result;
    TQueue<TreeNode*> Queue;
    Queue.Enqueue(Root);
    while (!Queue.IsEmpty()) {
        int32 LevelSize = Queue.Num();
        TArray<int32> Level;
        for (int32 i = 0; i < LevelSize; i++) {
            TreeNode* Node;
            Queue.Dequeue(Node);
            Level.Add(Node->Value);
            if (Node->Left) Queue.Enqueue(Node->Left);
            if (Node->Right) Queue.Enqueue(Node->Right);
        }
        Result.Add(Level);
    }
    return Result;
}
  1. 时间复杂度 O(n),空间 O(n);变体:锯齿形(之字形)层序遍历需双端队列或按层翻转。

四、图与寻路

4.1 A* 寻路算法实现与优化

★★ 中等

考点: f(n)=g(n)+h(n)、启发式函数、开放/关闭列表、寻路优化

参考答案:

  1. 核心公式 f(n) = g(n) + h(n):g 为起点到当前的实际代价,h 为到目标的启发式估计,每次扩展 f 最小的节点;
  2. 启发式函数选择:
    • 曼哈顿距离(仅 4 方向移动);
    • 对角线距离 / Octile(8 方向,√2 加权);
    • 欧几里得距离(任意方向,估算偏低、扩展较多)。
    • 关键性质:h 可采纳(不高估)保证最优解;h 越小搜索空间越大。
  3. 数据结构:开放列表用最小堆(按 F 排序)+ TMap 快速查找;关闭列表用 TSet;
  4. 注意点:
    • 对角线移动要检查相邻格都可行走,防止穿墙;
    • 单独维护 CameFrom 表回溯路径(勿复用开放表);回溯后逆序输出;
  5. 优化策略:分层寻路(粗粒度 Cluster 图 + 局部细化)、JPS(见 4.2)、D Lite*(增量重算,适合动态障碍)、String Pulling 拉绳法平滑路径;
  6. 代码示例(核心函数):
float Heuristic(const FIntVector& A, const FIntVector& B, bool bDiag) {
    int DX = FMath::Abs(A.X - B.X), DY = FMath::Abs(A.Y - B.Y);
    return bDiag ? FMath::Max(DX, DY) + (FMath::SQRT2 - 1.0f) * FMath::Min(DX, DY)
                 : (float)(DX + DY);
}
// 主循环:堆弹出最小 F 节点 -> 加入 ClosedSet -> 扩展邻居(计算 NewG,
// 已在开放表且 G 更小则跳过,否则更新/插入并记录 Parent)
  1. UE5 的 UNavigationSystemV1 使用 NavMesh + A* 组合;Flow Field 流场寻路适合大量单位同时寻路(RTS)。

4.2 JPS 跳点搜索优化

★★★ 困难

考点: 网格对称性、强迫邻居、剪枝策略

参考答案:

  1. 核心思想:在均匀代价网格上,A* 会扩展大量对称等价路径;JPS 通过“跳跃”跳过中间节点,只在**跳点(Jump Point)**处扩展,大幅减少节点数;
  2. 关键概念:
    • 自然邻居(Natural Neighbors):沿当前方向继续移动会经过的格子;
    • 强迫邻居(Forced Neighbor):当前节点某侧被障碍阻挡,迫使路径必须拐弯经过该格时才产生;
  3. 跳跃规则:
    • 水平/垂直移动:检查两侧是否有强迫邻居,有则当前格是跳点,否则继续向前;
    • 对角线移动:递归检查水平、垂直两个正交方向是否也能找到跳点;
    • 到达目标格也是跳点。
  4. 剪枝策略:从父节点到当前节点只保留自然邻居 + 强迫邻居方向,其它方向剪掉;起始节点考虑全部 8 方向;
  5. 代码示例(跳跃函数核心):
FIntVector Jump(FIntVector Current, int DirX, int DirY) {
    FIntVector Next(Current.X + DirX, Current.Y + DirY, 0);
    if (!IsWalkable(Next)) return FIntVector(-1, -1, -1);  // 无效
    if (Next == Goal) return Next;                          // 到达目标
    if (DirX != 0 && DirY != 0) {                           // 对角线移动
        if ((!IsWalkable(FIntVector(Next.X - DirX, Next.Y, 0)) &&
              IsWalkable(FIntVector(Next.X - DirX, Next.Y + DirY, 0))) ||
            (!IsWalkable(FIntVector(Next.X, Next.Y - DirY, 0)) &&
              IsWalkable(FIntVector(Next.X + DirX, Next.Y - DirY, 0))))
            return Next;                                    // 有强迫邻居,是跳点
        if (Jump(Next, DirX, 0).X >= 0) return Next;        // 水平方向有跳点
        if (Jump(Next, 0, DirY).X >= 0) return Next;        // 垂直方向有跳点
    } else { /* 水平/垂直移动:检查两侧强迫邻居,命中即返回 Next */ }
    return Jump(Next, DirX, DirY);                          // 继续跳跃
}
  1. 限制:JPS 仅适用于均匀代价网格;JPS+ 预计算查找表支持非均匀代价;NavMesh 图上对称性少,通常不需要 JPS。

4.3 NavMesh 导航网格生成算法

★★★ 困难

考点: Recast 流程、体素化、轮廓提取、三角化

参考答案:

  1. NavMesh(导航网格)把可行走表面表示为一组多边形,寻路在多边形上进行,比网格寻路效率高、路径更自然;
  2. 生成流程(Recast 库核心步骤):
    1. 体素化(Voxelization):把场景三角形栅格化为高度场(Heightfield),每列存跨度(Span);
    2. 区域标记:检查跨度上方空间是否足够 Agent 通过,标记可行走区域;
    3. 区域生成:合并相邻可行走跨度成区域(Region);
    4. 轮廓提取:用 Marching Squares 变体提取区域边界,再用 Douglas-Peucker 简化;
    5. 多边形网格化:对轮廓做约束三角化(Constrained Delaunay)生成多边形;
    6. 细节网格:在多边形内采样高度场生成更密三角形,提高寻路精度。
  3. Agent 参数影响生成:半径(走廊宽度)、高度(净空)、最大坡度、最大台阶高度(AgentClimb);
  4. 体素化核心逻辑:
// 三角形转体素:先算三角形 AABB,投影到体素格子范围,
// 对每个被覆盖的列做三角形-体素求交得到 y 区间,写入跨度链表
void RasterizeTriangle(const FVector& V0, const FVector& V1, const FVector& V2) {
    // 计算 MinX/MaxX/MinZ/MaxZ 并映射到格子索引 X0..X1, Z0..Z1
    for (int z = Z0; z <= Z1; z++)
        for (int x = X0; x <= X1; x++)
            if (TriangleAABBIntersection(V0, V1, V2, x, z, SpanMinY, SpanMaxY))
                AddSpan(x, z, SpanMinY, SpanMaxY);
}
  1. 动态修改:UE5 用 UNavModifierVolume / 运行时修改 API 更新局部 NavMesh;
  2. UE5 使用 Recast/Detour 库生成和查询,支持多种导航代理配置(多层 NavMesh)。

五、AI 决策算法

5.1 行为树节点评估算法

★★ 中等

考点: Selector/Sequence/Decorator、节点状态、黑板、Observer Aborts

参考答案:

  1. 节点类型:
    • Selector(选择器):按顺序执行子节点,有一个成功即成功(OR 语义);
    • Sequence(序列):依次执行,全部成功才成功,任一失败即失败(AND 语义);
    • Decorator(装饰器):附加在组合/任务节点上,检查条件或控制运行;
    • Task(任务):叶子节点,实际执行行为。
  2. 节点状态:Running(下帧继续)、Success、Failure;组合节点需记住 CurrentChild 支持 Running 续跑;
  3. 黑板(Blackboard):全局共享数据空间,Decorator 通过黑板键读条件值,实现决策数据解耦;
  4. 核心执行流程(简化):
enum class EBTNodeResult : uint8 { InProgress, Succeeded, Failed };

// Selector:任一子节点成功即成功
while (CurrentChild < Children.Num()) {
    switch (Children[CurrentChild]->Execute(OwnerComp)) {
    case EBTNodeResult::Succeeded: CurrentChild = 0; return Succeeded;
    case EBTNodeResult::Failed:    CurrentChild++; break;
    case EBTNodeResult::InProgress: return InProgress;
    }
}
CurrentChild = 0; return Failed;
  1. Observer Aborts 中断机制(UE5 行为树精髓):
    • ObserverAborts_Self:条件不满足时中止自己所在分支;
    • ObserverAborts_LowerPriority:高优先级条件变 true 时,中止低优先级分支重新评估。
  2. 对比状态机:行为树更易扩展、可读性高,适合复杂 AI;状态机更轻量实时。

5.2 RVO/ORCA 群体避障算法

★★★ 困难

考点: 速度障碍(VO)、对称分担、线性规划求最优速度

参考答案:

  1. 速度障碍(Velocity Obstacle,VO):速度空间中,使两个智能体在时间窗内相撞的所有相对速度集合;
  2. RVO(Reciprocal VO):双方各承担一半避让责任(对称分担),避免“振荡”;
  3. ORCA(Optimal Reciprocal Collision Avoidance):把 RVO 的圆锥约束近似为半平面约束,每个智能体被其他智能体与障碍物施加若干半平面,在所有半平面的交集中线性规划出最接近期望速度的最优速度;
  4. 求解流程:为每个智能体收集所有邻居的 ORCA 约束 → 2D 线性规划(逐条检查当前速度是否在半平面内,越界则投影到边界与已有约束的交点);
  5. 核心逻辑:
// 计算单条 ORCA 约束:基于相对位置/速度、合并半径 R、时间窗 T
FVector2D W = RelativeVel - RelativePos / TimeHorizon;   // 所需速度变化
// 若 W 指向 VO 的扇形区域 -> 沿边界切线调整,否则投影到圆弧边界
Line.Point = Agent.Velocity + U * 0.5f;   // 对称分担 50%
Line.Direction = /* 半平面法线,约束区域在其左侧 */;

// 线性规划:Result = PreferredVel,逐约束检查 det(Direction, Point - Result) < 0
// 越界则 Result = ProjectOnLine(...) 投影到最近边界
  1. 局限性:高密度场景可能振荡;UE5 的 Recast/Detour Crowd 内置避障;更高级方案有 Continuum Crowds 流体模拟、Social Force Model、Flocking(分离/对齐/聚合)。

5.3 效用 AI 评分系统

★★ 中等

考点: 效用分数、响应曲线、多因素加权、评分组合

参考答案:

  1. 核心思想:为每个行为(Action)定义评分函数,每帧计算各行为得分,选最高分执行,比行为树更适合连续、可量化的决策;
  2. 响应曲线(Response Curve):把原始输入归一化到 [0,1] 后映射到 [0,1],常用类型:Linear 线性、Exponential 指数(后期增长快)、Logistic S 形(阈值效果)、Logit 反 S 形、Sine 周期;
  3. 每个行为由若干**考虑因素(Consideration)**组成:因素 = 输入函数 × 权重 × 响应曲线;
  4. 评分组合策略:
    • Average:平均,因素可互相弥补;
    • Multiply:乘积,所有因素都重要(常见默认);
    • MinValue:木桶效应,最差因素决定;
    • MaxValue:最优因素决定。
  5. 代码示例(核心逻辑):
struct FConsideration {
    float Weight;
    EResponseCurve CurveType;
    float Slope = 1.0f, Exponent = 2.0f;
    TFunction<float(const FAgentContext&)> InputFunction;

    float Evaluate(const FAgentContext& Ctx) const {
        return ResponseCurve(InputFunction(Ctx), CurveType, Slope, Exponent) * Weight;
    }
};

// 行为评分:默认 Multiply(乘积),任一因素接近 0 则整分趋近 0
for (const auto& C : Considerations) { Score *= C.Evaluate(Context); }
// 决策:遍历所有 Action 取最高分,可加小幅随机扰动避免行为过于机械
  1. 应用案例:《模拟人生》《文明》;可与行为树互补(效用 AI 做高层决策、行为树做低层执行);可加“兴奋度衰减”防止行为频繁切换。

六、游戏数学与图形算法

6.1 四元数旋转与 Slerp 插值

★★★ 困难

考点: 四元数、万向锁、Slerp 球面插值、Nlerp 近似

参考答案:

  1. 四元数 q = (w, x, y, z),单位四元数表示旋转;绕单位轴 n̂ 旋转 θ:q = (cos(θ/2), sin(θ/2)·n̂);
  2. UE5 中 FRotator::Quaternion() 转四元数,FQuat::RotateVector() 旋转向量;组合旋转 Q2 * Q1 表示先 Q1 后 Q2;
  3. 万向锁(Gimbal Lock):欧拉角在 Pitch 接近 ±90° 时 Yaw 与 Roll 轴重合,丢失一个自由度;四元数无此奇异性,故 FTransform 内部用四元数存储旋转;
  4. Slerp 实现要点:
    • 计算点积 Dot,若为负则对 B 取反(q 与 -q 表示同一旋转,选最短路径);
    • 角度极小时退化为线性插值并归一化(避免除零);
    • 一般公式:Theta0 = acos(Dot); Theta = Theta0 * t,Result = (A * (cos(Theta) - Dot·sin(Theta)/sin(Theta0)) + B * (sin(Theta)/sin(Theta0))).GetNormalized()。
  5. 代码示例:
FQuat Slerp(const FQuat& A, const FQuat& B, float t) {
    float Dot = A | B;
    FQuat BAdj = B;
    if (Dot < 0.0f) { Dot = -Dot; BAdj = -B; }        // 取最短路径
    if (Dot > 0.9995f) {                              // 小角度退化为 Nlerp
        return FQuat(FMath::Lerp(A.X, BAdj.X, t),
                     FMath::Lerp(A.Y, BAdj.Y, t),
                     FMath::Lerp(A.Z, BAdj.Z, t),
                     FMath::Lerp(A.W, BAdj.W, t)).GetNormalized();
    }
    float Theta0 = FMath::Acos(Dot), Theta = Theta0 * t;
    float SinT = FMath::Sin(Theta), SinT0 = FMath::Sin(Theta0);
    float Sa = FMath::Cos(Theta) - Dot * SinT / SinT0;
    float Sb = SinT / SinT0;
    return (A * Sa + BAdj * Sb).GetNormalized();
}
// UE5 内置:FQuat::Slerp(A, B, t) / FMath::QInterpTo()
  1. Nlerp vs Slerp:Nlerp 是“线性插值再归一化”,快 3-4 倍但角速度不均匀;角度差 < 30° 或动画融合用 Nlerp 即可,需匀速旋转用 Slerp;
  2. 延伸:双四元数用于蒙皮动画减少体积损失;Squad 用于多四元数平滑插值。

6.2 贝塞尔曲线与样条插值

★★ 中等

考点: De Casteljau 算法、Catmull-Rom、弧长参数化

参考答案:

  1. 贝塞尔曲线:由控制点定义的参数曲线,不一定经过中间控制点;De Casteljau 算法通过逐层线性插值求曲线上点;
  2. 三次贝塞尔(4 控制点)公式:B(t) = (1-t)³P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3,导数用于求切线/速度;
  3. Catmull-Rom 样条:经过所有控制点的插值样条,每段由 4 个点 P(i-1)..P(i+2) 决定,在 P(i) 到 P(i+1) 之间插值;
  4. 弧长参数化(Arc-Length Parameterization):t 均匀变化时曲线上速度不均匀;预计算“累积弧长表”,用距离反查 t(二分查找 + 线性插值),实现沿曲线匀速运动;
  5. 代码示例(De Casteljau + 弧长表核心):
FVector BezierPoint(const TArray<FVector>& ControlPoints, float t) {
    TArray<FVector> Temp = ControlPoints;
    for (int Level = Temp.Num() - 1; Level > 0; Level--)
        for (int i = 0; i < Level; i++)
            Temp[i] = FMath::Lerp(Temp[i], Temp[i + 1], t);
    return Temp[0];
}

// 弧长表:预先采样累积弧长,GetT(DistanceRatio) 二分查找反解 t
float GetT(float DistanceRatio) const { /* 二分查找 Lengths,段内 Lerp Params */ }
  1. 应用:弹道、相机轨道、UI 动画;UE5 USplineComponent 使用 Hermite 样条(支持切线控制);B-Spline/NURBS 在建模中更常用。

6.3 模型-视图-投影矩阵推导

★★★ 困难

考点: 齐次坐标、LookAt、透视投影、NDC 与透视除法

参考答案:

  1. 齐次坐标:3D 点用 4 维向量 (x,y,z,w) 表示;w=1 是点、w=0 是方向(不受平移影响);用矩阵统一表示平移/旋转/缩放/透视变换;
  2. 模型矩阵:本地→世界,按 TRS 顺序组合:Model = Translation * Rotation * Scale;
  3. 视图矩阵:世界→相机空间,本质是相机变换的逆;LookAt 先计算相机三轴(Forward = (Target-Eye) 归一化,Right = Up×Forward,CamUp = Forward×Right),旋转部分用正交矩阵转置,再平移把 Eye 移到原点;
  4. 投影矩阵:透视投影把视锥映射到裁剪空间,w 分量携带深度信息;关键参数:FOV、Aspect、Near、Far;
  5. 完整管线:世界空间 →(View)→ 观察空间 →(Proj)→ 裁剪空间 →(透视除法 ÷w)→ NDC →(视口变换)→ 屏幕空间;屏幕 Y 轴向下需翻转;
  6. 代码示例(核心变换):
// 透视投影矩阵(UE5 列主序)
FPlane(1/(Aspect*TanHalfFoV), 0, 0, 0)
FPlane(0, 1/TanHalfFoV, 0, 0)
FPlane(0, 0, Far/(Far-Near), 1)
FPlane(0, 0, -Near*Far/(Far-Near), 0)

// 世界 -> 屏幕:View -> Proj -> 透视除法(Clip/W) -> 视口映射
float ScreenX = (NDC.X * 0.5f + 0.5f) * ViewportWidth;
float ScreenY = (1.0f - (NDC.Y * 0.5f + 0.5f)) * ViewportHeight;
  1. 延伸:UE5 使用反向 Z(Near=1, Far=0)提高远处深度精度;剔除发生在裁剪空间(对 x/y/z/w 做 ±w 比较);NDC 范围 OpenGL 为 [-1,1]、D3D/Vulkan 为 [0,1]。

七、碰撞检测与物理模拟

7.1 AABB 与球体碰撞检测

★★ 中等

考点: AABB 表示与相交、球-球相交、球-AABB 相交、穿透信息

参考答案:

  1. AABB(轴对齐包围盒):用 Min/Max 表示,合法条件 Min <= Max;两 AABB 相交判断:任意轴上不重叠即分离(3 个坐标轴分别比较);
  2. 球-球相交:两球心距离平方 ≤ (半径和)²,用平方距离避免开方(经典优化);
  3. 球-AABB 相交:先求 AABB 上离球心最近的点(各分量 Clamp 到 [Min,Max]),再判断该点到球心距离 ≤ 半径;
  4. 特殊情况:球心在 AABB 内部时,按穿透最浅的轴取法线(比较到各面距离);
  5. 代码示例(核心):
bool AABBvsAABB(const FAABB& A, const FAABB& B) {
    if (A.Max.X < B.Min.X || A.Min.X > B.Max.X) return false;
    if (A.Max.Y < B.Min.Y || A.Min.Y > B.Max.Y) return false;
    if (A.Max.Z < B.Min.Z || A.Min.Z > B.Max.Z) return false;
    return true;
}

bool SphereVsSphere(const FSphere& A, const FSphere& B) {
    float DistSq = FVector::DistSquared(A.Center, B.Center);
    float RSum = A.Radius + B.Radius;
    return DistSq <= RSum * RSum;
}

bool SphereVsAABB(const FSphere& S, const FAABB& Box) {
    FVector Closest = FVector(
        FMath::Clamp(S.Center.X, Box.Min.X, Box.Max.X),
        FMath::Clamp(S.Center.Y, Box.Min.Y, Box.Max.Y),
        FMath::Clamp(S.Center.Z, Box.Min.Z, Box.Max.Z));
    return FVector::DistSquared(S.Center, Closest) <= S.Radius * S.Radius;
}
  1. 延伸:UE5 的 FBox 就是 AABB,FBoxSphereBounds 同时存 AABB 与包围球;BVH 用 AABB 构建加速结构;Sweep 扫描检测用于连续碰撞(CCD);GJK 可处理任意凸体。

7.2 OBB 碰撞检测与 SAT 分离轴定理

★★★ 困难

考点: OBB 表示、SAT 定理、15 条分离轴、穿透深度

参考答案:

  1. OBB(有向包围盒):用中心点 + 3 个局部轴(单位向量)+ 3 个方向半尺寸表示,支持任意旋转;
  2. SAT(分离轴定理):两个凸体不相交,当且仅当存在一条轴使它们在轴上的投影不重叠;对 OBB,潜在分离轴共 15 条:
    • A 的 3 条局部轴 + B 的 3 条局部轴;
    • 两两叉积 9 条(A0×B0, A0×B1, …)。
  3. 投影半径:R = HalfExtent.X·|Dot(A0, Axis)| + HalfExtent.Y·|Dot(A1, Axis)| + HalfExtent.Z·|Dot(A2, Axis)|;
  4. 判定:两中心连线在轴上的投影距离 ≤ RA + RB 才可能有交点,重叠量 = RA + RB - Distance;所有 15 条轴都重叠则相交,找到分离轴即可提前返回;
  5. 细节:平行轴叉积接近零向量时跳过(退化轴);计算穿透深度需追踪最小重叠量的轴;法线应指向从 A 到 B 的方向;
  6. 代码示例(单轴测试):
bool SATTestAxis(const FOBB& A, const FOBB& B, const FVector& Axis,
                 float& OutOverlap, FVector& OutAxis) {
    if (Axis.IsNearlyZero()) return true;   // 退化轴跳过
    FVector N = Axis.GetSafeNormal();
    float Dist = FMath::Abs(FVector::DotProduct(B.Center - A.Center, N));
    float RA = OBBProjectionRadius(A, N), RB = OBBProjectionRadius(B, N);
    float Overlap = (RA + RB) - Dist;
    if (Overlap <= 0.0f) return false;      // 分离!
    if (Overlap < OutOverlap) { OutOverlap = Overlap; OutAxis = N; }
    return true;
}
  1. 局限性:SAT 只适用于凸体;更通用的 GJK(O(n))+ EPA(算穿透深度)是 UE5 Chaos 物理引擎的内部方案。

7.3 空间哈希 Broad Phase 碰撞检测

★★ 中等

考点: Broad Phase、空间哈希、扫描剪枝、方案对比

参考答案:

  1. Broad Phase(粗检测)目的:快速排除不可能碰撞的对象对,只把潜在碰撞对交给 Narrow Phase 精确检测,避免 O(n²) 两两检测;
  2. 空间哈希(Spatial Hashing):
    • 把空间划分为固定大小的网格单元(CellSize),每个物体根据 AABB 跨越的格子插入对应桶;
    • 哈希键用大素数混合坐标:X*92837111 ^ Y*689287499 ^ Z*283923481;
    • 潜在碰撞对 = 同一格子内物体两两组合(用 TSet 去重,物体跨多格会重复配对);
    • 构建 O(n)、查询近似 O(1)/对,适合均匀分布、大量物体。
  3. 扫描剪枝(Sweep and Prune):沿一个轴维护所有物体端点的有序列表,用插入排序(物体运动不剧烈时几乎有序)增量更新,min/max 端交替时记录潜在碰撞对;
  4. 方案对比:
方案 构建 查询 适用场景
空间哈希 O(n) O(1)/对 均匀分布、大量物体
扫描剪枝 O(n log n) O(n+k) 物体运动不剧烈
八叉树 O(n log n) O(log n) 非均匀分布
BVH O(n log n) O(log n) 动态物体频繁重建
  1. 易错点:CellSize 太大会导致单元格内物体过多,太小则跨格物体多;UE5 Chaos 物理引擎使用多层加速结构(Sweep and Prune + BVH 组合)。

7.4 Verlet 积分与约束求解

★★ 中等

考点: 积分稳定性、位置约束、迭代求解、布料/绳索

参考答案:

  1. 对比:显式欧拉积分 v += a·dt; x += v·dt 是一阶精度,能量不守恒易爆炸;Verlet 积分 x(t+dt) = 2x(t) - x(t-dt) + a·dt² 是二阶精度、更稳定,且无需显式存储速度(速度隐含在位置差中),天然适合约束求解;
  2. 施加外力时更新公式:newPos = pos + (pos - oldPos)·Damping + accel·dt²,再交换 pos/oldPos;
  3. 距离约束(Distance Constraint):保持两点间距为 RestLength;误差 Correction = Delta·(1 - RestLength/CurrentLength)·Stiffness,两点各移动一半(固定点不移动);
  4. 迭代求解:每帧对全部约束重复多次(Gauss-Seidel 方式,迭代 10 次左右),迭代次数越多越稳定但越贵;
  5. 代码示例(核心):
struct FVerletPoint { FVector Position, OldPosition; bool bPinned; };

void Integrate(float DeltaTime) {
    for (auto& P : Points) {
        if (P.bPinned) continue;
        FVector Vel = (P.Position - P.OldPosition) * Damping;
        FVector Acc = FVector(0, 0, -980.0f);        // 重力
        P.OldPosition = P.Position;
        P.Position += Vel + Acc * DeltaTime * DeltaTime;
    }
}

// 距离约束
FVector Delta = B.Position - A.Position;
float Cur = Delta.Size();
FVector Correction = Delta * (1.0f - RestLength / Cur) * Stiffness;
A.Position += Correction * 0.5f;   // 都未固定时各移一半
B.Position -= Correction * 0.5f;
  1. 应用:绳索、布料网格;UE5 Chaos Cloth 使用 PBD(Position-Based Dynamics),XPBD 改进刚度参数与迭代次数的关系。

7.5 质点-弹簧布料模拟

★★ 中等

考点: 三类弹簧、风力、数值稳定性、碰撞

参考答案:

  1. 布料用质点网格表示,质点之间用弹簧连接,三种弹簧各司其职:
    • 结构弹簧(Structural):连接相邻网格(横/竖),防止拉伸;
    • 剪切弹簧(Shear):对角线连接,防止对角变形;
    • 弯曲弹簧(Bend):跨一格连接,防止过度折叠。
  2. 刚度通常不同:结构 1.0、剪切 0.8、弯曲 0.3,模拟真实布料的抗弯特性;
  3. 模拟流程(每帧):
    1. 计算顶点法线(三角形面法线累加后归一化),用于风力和碰撞;
    2. 施加外力:Verlet 积分隐含速度,pos += vel + (Gravity + WindForce/Mass)·dt²;风力与法线点积(正面迎风效果更强);
    3. 约束求解:对全部弹簧做多次迭代的位置修正;
    4. 碰撞约束:地面/球体等简单碰撞。
  4. 代码示例(约束求解核心):
void SolveConstraint(const FSpring& Spring) {
    FVector Delta = Particles[Spring.B].Position - Particles[Spring.A].Position;
    float Cur = Delta.Size();
    if (Cur < KINDA_SMALL_NUMBER) return;
    float Diff = (Cur - Spring.RestLength) / Cur;
    FVector Correction = Delta * Diff * 0.5f;

    // 不同弹簧类型刚度不同(结构1.0 / 剪切0.8 / 弯曲0.3)
    Correction *= Stiffness;

    if (!A_Pinned && !B_Pinned) { A += Correction; B -= Correction; }
    else if (!A_Pinned) { A += Correction * 2.0f; }
    else if (!B_Pinned) { B -= Correction * 2.0f; }
}
  1. 延伸:UE5 Chaos Cloth 与角色骨骼碰撞、自碰撞检测是难点、碰撞厚度参数重要;可用 Compute Shader GPU 并行加速;远处用简化模型或关闭模拟(LOD)。

八、空间数据结构与渲染算法

8.1 BVH 构建与光线-三角形求交

★★★ 困难

考点: BVH 构建、SAH 启发式、Slab Method、Möller–Trumbore

参考答案:

  1. BVH(Bounding Volume Hierarchy):把图元按包围体组织成层次树,光线求交时快速跳过无关子树,构建 O(n log n)、查询 O(log n);
  2. SAH(Surface Area Heuristic):按子节点表面积加权评估划分代价,Cost = Ct + (A_L/A_T·N_L + A_R/A_T·N_R)·Ci,取代价最小的分割轴与分割位置;
  3. 光线-AABB 求交(Slab Method):对三个轴分别算光线进入/离开的参数区间,取交集;若 TMax < TMin 则不相交;
  4. 光线-三角形求交(Möller–Trumbore):用重心坐标 + 行列式一次性求解 t、u、v,避免预计算平面方程;
  5. 遍历优化:用栈做非递归遍历,先访问较近的子节点(T 更小的优先);
  6. 代码示例(核心):
// 光线-AABB 求交(Slab Method)
for (int i = 0; i < 3; i++) {
    float InvD = 1.0f / RayDir[i];
    float T0 = (Box.Min[i] - RayOrigin[i]) * InvD;
    float T1 = (Box.Max[i] - RayOrigin[i]) * InvD;
    if (InvD < 0.0f) Swap(T0, T1);
    OutTMin = FMath::Max(OutTMin, T0);
    OutTMax = FMath::Min(OutTMax, T1);
    if (OutTMax < OutTMin) return false;
}

// 光线-三角形求交(Möller–Trumbore)
FVector P = Cross(RayDir, Edge2);
float Det = Dot(Edge1, P);
if (FMath::Abs(Det) < EPSILON) return false;     // 平行
float InvDet = 1.0f / Det;
FVector T = RayOrigin - V0;
OutU = Dot(T, P) * InvDet;
if (OutU < 0 || OutU > 1) return false;
FVector Q = Cross(T, Edge1);
OutV = Dot(RayDir, Q) * InvDet;
if (OutV < 0 || OutU + OutV > 1) return false;
OutT = Dot(Edge2, Q) * InvDet;
  1. 延伸:UE5 路径追踪器用 BVH;Lumen 使用 Mesh SDF / Global SDF 近似全局光照;Embree(Intel)是业界标准 BVH 实现;DXR / Vulkan RayTracing 用 GPU 硬件加速 BVH 遍历。

8.2 八叉树空间分区与查询

★★ 中等

考点: 构建与分裂、区域查询、K 最近邻查询、应用场景

参考答案:

  1. 八叉树把空间递归 8 等分,适合非均匀分布的场景对象管理;
  2. 分裂条件:节点项数 > MaxItemsPerNode 且深度 < MaxDepth 时分裂;子节点用位标记定位(X 高半 1 位、Y 2 位、Z 4 位);
  3. 插入:从根向下定位到所属子节点;区域查询(RangeQuery/SphereQuery):先做节点包围体与查询体相交测试剪枝,再检查项;
  4. K 最近邻(KNN):用最大堆维护 Top-K,优先访问更近的子节点,当前节点最近距离超过堆内最远距离时剪枝;
  5. 代码示例(核心):
// 点落在父节点的哪个八分象限(0-7)
int GetChildIndex(const FAABB& PB, const FVector& P) {
    FVector C = PB.GetCenter();
    int Child = 0;
    if (P.X >= C.X) Child |= 1;
    if (P.Y >= C.Y) Child |= 2;
    if (P.Z >= C.Z) Child |= 4;
    return Child;
}

// 分裂:创建 8 个子节点,把现有项按象限重分配
if (Node.Items.Num() > MaxItemsPerNode && Node.Depth < MaxDepth) { Split(NodeIdx); }

// 区域查询:节点包围体与查询 AABB 不相交则整棵子树剪掉
if (!AABBvsAABB(Node.Bounds, QueryBounds)) return;
// KNN 剪枝:节点最近距离超过堆内最远距离则跳过
if (BestK.Num() == K && MinDistToPoint(Node.Bounds, Q) > BestK[0].Value) return;
  1. 应用:场景管理、空间查询、Broad Phase 的替代方案;UE5 的 World Partition 用空间分区管理流式加载;2D 下退化为四叉树;均匀分布下空间哈希比八叉树更高效。

8.3 Cascaded Shadow Maps 实现原理

★★★ 困难

考点: Shadow Map 原理、视锥分割、Shadow Acne、PCF 软阴影

参考答案:

  1. Shadow Map 原理:从光源视角渲染一张深度图,着色时比较片段与光源的距离判断是否在阴影中;单张 Shadow Map 分辨率不够,远处浪费、近处模糊;
  2. CSM:把视锥按距离切分成多段(级联),每段单独渲染一张较高分辨率的阴影贴图,近处更清晰;分割策略 = Lambda·Log + (1-Lambda)·Linear 混合(对数分配远处更均匀,线性分配近处更均匀);
  3. 稳定性优化:用视锥包围球(而非 AABB)避免相机旋转抖动,并把中心 Snap 到纹素边界;每个级联生成独立 LightViewProj 矩阵;
  4. Shadow Acne(自阴影瑕疵):深度精度不足导致表面“自己遮挡自己”;解决:深度偏移(Bias)、法线偏移、只渲染背面到阴影图;
  5. PCF 软阴影:在邻域内多次采样阴影深度取平均,实现边缘柔化;
  6. 代码示例(核心):
// 视锥分割:Split = Lambda * Log + (1-Lambda) * Linear
float LogSplit = Near * FMath::Pow(Far / Near, (float)i / NumCascades);
float LinearSplit = Near + (Far - Near) * (float)i / NumCascades;
SplitDist = Lambda * LogSplit + (1.0f - Lambda) * LinearSplit;

// PCF:邻域多次采样,深度比较带 Bias
float Bias = 0.005f;
Shadow += (ShadowZ - Bias > ShadowDepth) ? 0.0f : 1.0f;   // 求和取平均
  1. 延伸:UE5 默认使用 Virtual Shadow Maps(VSM) 按需分配分辨率;EVSM / MSM 减少 Light Bleeding;平行光用 CSM、点光源用 Cubemap Shadow、聚光灯用单张 Shadow Map。

8.4 时域抗锯齿 TAA 算法

★★★ 困难

考点: 锯齿成因、抖动采样、重投影、方差钳制、重影

参考答案:

  1. 锯齿成因:几何边缘欠采样(高频信息超过像素分辨率);MSAA 只对几何边缘超采样,无法处理着色/特效锯齿,且开销高;
  2. TAA 核心思想:利用前一帧信息,通过混合多帧实现超采样效果(等价于亚像素累积);
  3. 关键步骤:
    • 抖动采样:每帧用 Halton 序列生成亚像素偏移加入投影矩阵,采样点逐帧不同;
    • 重投影(Reprojection):用前一帧的 ViewProj 矩阵把当前像素的世界位置映射到上一帧屏幕坐标,双线性采样历史颜色;
    • 时域混合:当前帧颜色与历史颜色按比例混合(如 0.1);
    • 方差钳制(Variance Clamping):把历史颜色钳制到当前像素 3×3 邻域的均值±方差范围内,防止残影。
  4. 运动区域处理:运动向量越大,混合因子越接近 1(当前帧占主导),减少重影;
  5. 代码示例(核心):
// Halton 序列生成低差异抖动(每帧不同子像素偏移)
float Halton(int Index, int Base) {
    float Result = 0.0f, Frac = 1.0f / Base;
    while (Index > 0) { Result += (Index % Base) * Frac; Index /= Base; Frac /= Base; }
    return Result;
}

// 方差钳制:History = Clamp(History, NeighborMean - K*Var, NeighborMean + K*Var)
// 时域混合:Blend 随运动向量增大而增大
float MotionLen = (PrevUV - FVector2D(x, y)).Size();
Blend = FMath::Lerp(Blend, 1.0f, FMath::Clamp(MotionLen / 10.0f, 0.0f, 1.0f));
Output = Lerp(Current, History, Blend);
  1. 局限:高运动场景模糊与重影;UE5 的 UTemporalUpscaler 接口支持 FSR2、DLSS、TAAU 等;TAAU 在时域抗锯齿中同时实现上采样。

8.5 Hi-Z 遮挡剔除算法

★★★ 困难

考点: 遮挡剔除、Hi-Z Mipmap 链、GPU 驱动剔除、软件光栅化

参考答案:

  1. 遮挡剔除目的:跳过被其他物体完全挡住的物体,减少 Draw Call 与像素开销;
  2. Hi-Z Buffer(层次 Z 缓冲):把深度缓冲生成 Mipmap 链,每级取 4 个像素中最远深度(Max,深度越大越远,保守性保证不误剔);
  3. GPU 驱动剔除流程:
    • Compute Shader 中把物体包围盒变换到裁剪空间 → 透视除法 → 屏幕空间 AABB;
    • 按屏幕尺寸选择对应 Mip 级别,采样该级深度与物体最近深度比较;
    • 被遮挡则丢弃,可见的用 InterlockedAdd 原子写入可见列表 → Indirect Draw 渲染。
  4. 软件光栅化(Nanite 核心思想之一):对遮挡物用 CPU/GPU 低分辨率光栅化出深度,再做遮挡测试;
  5. 代码示例(核心逻辑):
// GPU Shader 生成 Hi-Z Mip:4 个像素取最远深度
float4 Depths = InputDepth.Gather(LinearSampler, DTid.xy * 2);
float MaxDepth = max(max(Depths.x, Depths.y), max(Depths.z, Depths.w));
OutputDepth[DTid.xy] = MaxDepth;

// GPU 剔除:根据屏幕尺寸选 MipLevel,深度比较决定是否可见
int MipLevel = (int)ceil(log2(max(ScreenSize.x, ScreenSize.y)));
if (ObjectNearestDepth < HiZDepth) { InterlockedAdd(Count, 1, Idx); VisibleList[Idx] = Obj.ID; }
  1. 延伸:UE5 Nanite 使用 GPU 驱动多级遮挡剔除(Instance → Cluster → Triangle);Pre-Z Pass 先渲染深度再做遮挡查询;硬件遮挡查询适合动态物体但有 GPU-CPU 同步延迟;Occlusion Culling 与 Frustum Culling 的区别要分清。

九、并发与调度

9.1 时间分片算法

★★ 中等

考点: 帧预算、任务优先级、中断恢复、异步任务

参考答案:

  1. 为什么需要:大型计算(如生成 NavMesh、加载资产)一次做完会撑爆单帧(60fps = 16.67ms),导致卡顿;时间分片把长任务拆成小块,每帧只执行预算内的时间,剩余下帧继续;
  2. 帧预算(Frame Budget):每帧给物理/AI/渲染/加载各分配毫秒预算,超时即暂停;
  3. 任务优先级:高优先级任务优先执行;可被中断的任务记录进度,下帧恢复;
  4. 状态管理:用阶段机(Phase)表示长流程(如 NavMesh 的体素化 → 区域 → 轮廓 → 网格化),每阶段处理一批数据(Batch)返回是否完成;
  5. 代码示例(核心):
// 时间分片调度器:每帧在预算内执行任务,完成则移除
void Tick(float DeltaTime) {
    double Start = FPlatformTime::Seconds();
    double Budget = FrameBudgetMs / 1000.0;
    for (int i = TaskQueue.Num() - 1; i >= 0; i--) {
        if (FPlatformTime::Seconds() - Start >= Budget) break;  // 超出帧预算
        float TaskBudget = FMath::Min(TaskQueue[i].TimeBudget / 1000.0f,
                                      (float)(Budget - (FPlatformTime::Seconds() - Start)));
        bool bDone = TaskQueue[i].Execute(TaskBudget);
        if (bDone) TaskQueue.RemoveAt(i);
    }
}

// 分帧生成 NavMesh:每个阶段处理 BatchSize 个三角形,未完成下帧继续
while (FPlatformTime::Seconds() - StartTime < BudgetSeconds) {
    switch (CurrentPhase) {
    case EPhase::Voxelization:
        if (!VoxelizationChunk()) CurrentPhase = EPhase::RegionGeneration;
        break;
    // ... 各阶段逐帧推进,DetailMesh 完成后返回 true
    }
}
  1. UE5 异步:FAsyncTask / FAutoDeleteAsyncTask 在线程池后台执行,FAsyncTask<FMyTask>::StartBackgroundTask();FStreamableManager 用时间分片加载资产;LOD 本身也是一种时间分片思路。

9.2 异步任务图调度算法

★★★ 困难

考点: DAG 依赖、拓扑排序、并行调度、负载均衡

参考答案:

  1. 任务图:任务之间用**有向无环图(DAG)**表达依赖关系,A 完成后 B 才能开始;
  2. 拓扑排序:统计每个任务的入度(依赖数),把入度为 0 的任务加入就绪队列;任务完成时递减其依赖者的入度,减到 0 就加入就绪队列;用 PendingDeps 计数器避免每次重新计算;
  3. 并行调度:就绪队列按优先级排序,任务分发到线程池并行执行;无依赖的任务可同时运行(负载均衡交给线程池);
  4. 线程安全:就绪队列和计数器需加锁或原子操作;
  5. 代码示例(核心):
struct FTaskNode {
    TFunction<void()> Execute;
    TArray<int> Dependents;
    TAtomic<int> PendingDeps;   // 剩余未完成依赖数
    TAtomic<bool> bCompleted;
    int Priority;
};

// 添加依赖:B 依赖 A
void AddDependency(int A, int B) {
    Tasks[A].Dependents.Add(B);
    Tasks[B].PendingDeps++;
}

// 执行
void ExecuteTask(int TaskIdx) {
    Tasks[TaskIdx].Execute();
    Tasks[TaskIdx].bCompleted = true;
    for (int D : Tasks[TaskIdx].Dependents)
        if (Tasks[D].PendingDeps.Decrement() == 0)   // 依赖全部完成
            ReadyQueue.Add(D);                        // 加入就绪队列(需加锁)
}
  1. 应用:渲染管线各 Pass 依赖调度(Shadow → Lighting → PostProcess 等)、动画图;UE5 的 TaskGraph(TGraphTask + FGraphEvent + Prerequisites)是全局调度器,支持优先级与线程亲和性;UE5.1+ 引入 UE::Tasks 作为现代化替代;渲染线程与游戏线程同步用 ENQUEUE_RENDER_COMMAND。

十、常用编程题与设计模式

10.1 实现单例模式(线程安全)

★★ 中等

考点: 单例、线程安全、双重检查锁、局部静态变量

参考答案:

  1. 单例三要素:私有构造、删除拷贝/赋值、提供全局访问点;保证整个程序只有一个实例;
  2. 懒汉式 + 双重检查锁(DCLP):外层先判断避免每次加锁,内层再加锁二次判断,防止多线程同时创建;
  3. Meyers 单例(C++11 推荐):用函数内 static 局部变量,初始化由标准库保证线程安全,代码最简;
  4. 代码示例:
// 方案1:双重检查锁(C++11 前可用,注意内存序)
class Singleton {
    static Singleton* Instance;
    static std::mutex Mutex;
    Singleton() {}
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton* GetInstance() {
        if (Instance == nullptr) {
            std::lock_guard<std::mutex> Lock(Mutex);
            if (Instance == nullptr) Instance = new Singleton();
        }
        return Instance;
    }
};

// 方案2:Meyers 单例(C++11 起推荐,天然线程安全)
class Singleton {
    Singleton() {}
public:
    static Singleton& GetInstance() {
        static Singleton Instance;   // 首次调用时初始化,标准库保证线程安全
        return Instance;
    }
};
  1. UE 中通常用 Subsystem(UWorldSubsystem / UEngineSubsystem)替代传统单例,生命周期与引擎/世界绑定、自动销毁,更符合引擎规范。

10.2 实现一个简单的对象池

★★ 中等

考点: 对象复用、获取/回收、自动扩容

参考答案:

  1. 对象池:预创建一批对象缓存起来,需要时取出、用完回收复用,避免频繁 new/delete 造成的分配开销与 GC 压力;
  2. 关键操作:Acquire() 从空闲列表取,空闲不足且有扩容空间则新建;Release() 回收并重置状态;超上限返回 nullptr 或拒绝;
  3. ResetObject() 用虚函数让子类重置对象状态;
  4. 代码示例(核心):
template<typename ObjectType>
class TObjectPool {
    TArray<ObjectType*> AvailableObjects;
    TArray<ObjectType*> ActiveObjects;
    int32 MaxSize;
public:
    TObjectPool(int32 InitialSize = 10, int32 InMaxSize = 100) : MaxSize(InMaxSize) {
        for (int32 i = 0; i < InitialSize; i++) AvailableObjects.Add(new ObjectType());
    }
    ObjectType* Acquire() {
        ObjectType* Obj = nullptr;
        if (AvailableObjects.Num() > 0) Obj = AvailableObjects.Pop();
        else if (ActiveObjects.Num() < MaxSize) Obj = new ObjectType();
        else return nullptr;                       // 池满
        ActiveObjects.Add(Obj);
        return Obj;
    }
    void Release(ObjectType* Obj) {
        if (!Obj) return;
        if (ActiveObjects.Remove(Obj) > 0) {
            ResetObject(Obj);
            AvailableObjects.Add(Obj);
        }
    }
protected:
    virtual void ResetObject(ObjectType* Obj) {}   // 子类重写重置状态
};
  1. 使用场景:子弹/特效粒子、UI 列表项、AI 怪物生成与回收等高频创建销毁的对象。

10.3 实现一个简单的状态机

★★ 中等

考点: 状态模式、状态转换、Enter/Execute/Exit 回调

参考答案:

  1. 状态机:对象在不同状态间切换,每个状态独立处理自己的行为逻辑,适合角色 AI、动画状态、UI 流程等;
  2. 设计:状态基类定义 Enter / Execute / Exit 虚函数;状态机持有当前状态,按名称查找目标状态,转换时先 Exit 旧状态再 Enter 新状态;
  3. 每帧调用 Update() 执行当前状态的 Execute;
  4. 代码示例(核心):
template<typename OwnerType>
class TState {
public:
    virtual ~TState() {}
    virtual void Enter(OwnerType* Owner) {}
    virtual void Execute(OwnerType* Owner) {}
    virtual void Exit(OwnerType* Owner) {}
};

template<typename OwnerType>
class TStateMachine {
    OwnerType* Owner;
    TState<OwnerType>* CurrentState = nullptr;
    TMap<FName, TState<OwnerType>*> States;
public:
    TStateMachine(OwnerType* InOwner) : Owner(InOwner) {}
    void AddState(FName Name, TState<OwnerType>* State) { States.Add(Name, State); }
    void ChangeState(FName Name) {
        TState<OwnerType>* NewState = States.FindRef(Name);
        if (!NewState || NewState == CurrentState) return;
        if (CurrentState) CurrentState->Exit(Owner);
        CurrentState = NewState;
        CurrentState->Enter(Owner);
    }
    void Update() { if (CurrentState) CurrentState->Execute(Owner); }
};

// 使用:定义 Idle/Attack 等状态,SM->AddState("Idle", new FIdleState());
// SM->ChangeState("Idle"); 每帧 SM->Update();
  1. 与行为树的对比:状态机轻量、状态间转换明确,适合少量状态;行为树可扩展性强,适合复杂 AI 决策。