WAYNETS.ORG

Game and Program

Steering Behavior

作者:

发表于

Context Polling System Post-Processing Renderer

1. 前言

在实时战略(RTS)、模拟经营或战术类游戏中,AI 单位的智能移动是核心功能之一。

传统的寻路(如 A*)可以计算出从起点到终点的路径,但它在动态环境下显得僵硬:当有移动障碍、其他单位或复杂的地形时,AI 往往会出现“卡死”或“频繁抖动”的问题。
为了解决这一问题,我们可以借助Steering Behavior(操舵行为)的思想,让单位像真实生物一样动态调整自己的移动方向,实现平滑、自然且可预测的移动效果。

本文基于Unity,结合Flow Field寻路,实现了一个具备目标追踪、避障、群体协同功能的简易 Steering Agent。

2. 功能概述

该系统的核心目标是让一个智能单位(Steering Agent)能够在以下场景中自然移动:

  1. 目标追踪
    • 接收目标点命令(MoveTo),自动移动到目标位置。
    • 靠近目标时自动停止(stoppingDistance 控制)。
  2. 避障行为(Obstacle Avoidance)
    • 检测前方一定范围内的障碍物。
    • 在多方向采样中选择最佳的可通行方向,保证单位不会卡住。
  3. 群体协作(Flocking Behavior)
    • 分离(Separation):避免与其他单位重叠。
    • 凝聚(Cohesion):向队伍中心靠拢(可选)。
  4. 平滑移动
    • 防止小幅度方向波动导致的抖动(方向阈值控制)。
    • 使用插值(Quaternion.Slerp)平滑旋转。

3. 系统核心结构

(1)主逻辑循环

核心思想:

  • 优先判断是否有移动目标。
  • 合成多个 steering 向量(路径指引、避障、分离等)。
  • 平滑更新方向,减少视觉抖动。
private void FixedUpdate()
{
    AlignToGround(); // 保持贴地

    if (targetPosition != Vector3.zero) // 有目标时
    {
        Vector3 toTarget = targetPosition - transform.position;
        toTarget.y = 0f;

        if (toTarget.magnitude > stoppingDistance) // 未到达
        {
            Vector3 newSteering = ComputeSteering(); // 计算合成方向
            float angle = Vector3.Angle(lastSteering, newSteering);

            // 避免小幅抖动
            if (angle > directionThreshold || lastSteering == Vector3.zero)
            {
                steering = newSteering;
                lastSteering = newSteering;
            }

            MoveAgent(steering);
            isMoving = true;
        }
        else // 到达
        {
            targetPosition = Vector3.zero;
            isMoving = false;
            OnDestinationReached();
        }
    }
}

(2)Steering向量合成逻辑

核心思想:

  • 基础方向:来源于流场寻路算法,确保全局寻路正确性。
  • 避障方向:防止与场景中的障碍物发生碰撞。
  • 分离方向:防止多个单位挤在一起。
private Vector3 ComputeSteering()
{
    Vector3 moveDir = GetFlowFieldDirection();     // 基础导航方向
    Vector3 avoidDir = ComputeObstacleAvoidance(); // 避障方向
    Vector3 separation = ComputeSeparation();      // 分离方向

    // 按权重合成
    Vector3 finalDir = moveDir 
                     + avoidDir * avoidStrength 
                     + separation * separationWeight;

    return finalDir.normalized;
}

(3)障碍物避让逻辑

通过方向采样 + 射线检测的方式,为单位寻找一条尽量畅通且接近目标方向的路径:

  • 方向采样
    • 将周围 360° 等分成若干个方向(代码中是 16 个方向),逐一检测每个方向的可行性。
  • 障碍检测
    • 对每个方向发射一个球形射线(SphereCast),检测该方向在给定半径和距离内是否有障碍物。
  • 评分与选择
    • 如果方向没有被阻挡,则计算该方向与目标方向的夹角余弦(Vector3.Dot),越接近目标方向,分数越高。
    • 选出得分最高的方向作为当前最佳避让方向。
  • 方向平滑
    • 最佳方向会与上一次避让方向进行插值(Lerp)平滑过渡,减少方向突变造成的抖动。
private Vector3 ComputeObstacleAvoidance()
{
    float castRadius = 0.5f;
    float castDistance = obstacleAvoidanceRadius;

    // 方向采样:16个方向更细致
    int numSamples = 16;
    Vector3 bestDir = Vector3.zero;
    float bestScore = float.MinValue;

    Vector3 toTarget = (targetPosition - transform.position).normalized;

    for (int i = 0; i < numSamples; i++)
    {
        float angle = i * 360f / numSamples;
        Vector3 dir = Quaternion.Euler(0, angle, 0) * Vector3.forward;

        Ray ray = new Ray(transform.position + Vector3.up * 0.5f, dir);

        bool blocked = Physics.SphereCast(ray, castRadius, castDistance, obstacleMask);

        if (!blocked)
        {
            // 越靠近目标方向分数越高
            float alignment = Vector3.Dot(dir, toTarget); 
            if (alignment > bestScore)
            {
                bestScore = alignment;
                bestDir = dir;
            }
        }
    }

    if (bestDir != Vector3.zero)
    {
        lastAvoidanceDir = Vector3.Lerp(lastAvoidanceDir, bestDir, 0.2f);
        return lastAvoidanceDir;
    }

    return Vector3.zero;
}

(4)单位间距控制逻辑

确保单位之间保持合理的距离,避免出现重叠或拥挤现象:

  • 邻居检测
    • 在一定感知半径内(neighborRadius)检测其他单位的碰撞体。
  • 分离力计算
    • 对每个邻居计算一个反向力(单位位置与邻居位置的差向量),并且距离越近,分离力越大(使用 1 / distance 衰减)。
  • 均值处理
    • 将所有邻居的分离力求平均,得到整体的分离趋势。
  • 方向归一化
    • 最终返回一个单位方向向量,用于在移动时与其他 steering 力结合,避免贴得过近。
private Vector3 ComputeSeparation()
{
    Vector3 separationForce = Vector3.zero;
    int neighborCount = 0;

    Collider[] neighbors = Physics.OverlapSphere(transform.position, neighborRadius, unitLayer);
    foreach (var neighbor in neighbors)
    {
        if (neighbor.gameObject == gameObject) continue;

        Vector3 away = transform.position - neighbor.transform.position;
        float distance = away.magnitude;

        if (distance > 0)
        {
            separationForce += away.normalized / distance; // 越近力越大
            neighborCount++;
        }
    }

    if (neighborCount > 0)
    {
        separationForce /= neighborCount;
    }

    return separationForce.normalized;
}

Leave a comment