第11章 Unity AI进阶实战:对象管理、随机处理与游戏系统优化

11.1 概述与引言

在Unity引擎中,人工智能编程不仅是实现非玩家角色行为的基础,更是提升游戏体验和商业项目成功的关键因素。本章基于《Unity人工智能实战(原书第2版)》第10章的内容,深入探讨AI编程的高级主题,涵盖可编写脚本对象的管理、随机数处理的优化、游戏特定AI系统的构建以及难度调整机制的实现。这些主题在商业游戏开发中广泛应用,能够帮助开发者创建更智能、更动态的游戏世界。通过理论与实践的结合,本章将提供详细的代码实例,确保在Unity 2021.3.8f1c1、VS2022和VSCODE环境中可运行,使用Allman风格代码和驼峰命名法,以通俗易懂的语言展开讲解。

在商业项目中,AI系统往往需要兼顾性能、可维护性和可扩展性。例如,可编写脚本的对象允许开发者将AI行为数据化,便于团队协作和快速迭代;随机数处理则影响游戏的不可预测性和公平性;而游戏特定AI如空气曲棍球对手或竞速游戏架构,直接关系到核心玩法体验。此外,橡皮筋系统作为动态难度调整工具,能够平衡不同技能水平玩家的挑战性。本章将从这些角度出发,结合实例代码,详细解析如何在实际项目中应用这些技术。

11.2 可脚本化对象的创建与管理策略

在Unity AI编程中,可编写脚本的对象(ScriptableObject)是一种强大的数据容器,允许开发者创建不依赖于游戏场景实例的资产,用于存储和管理AI行为配置、状态数据或决策树。这种设计模式促进了代码与数据的分离,提高了项目的模块化和可维护性。

理论知识与应用场景

ScriptableObject继承自UnityEngine.Object,但不像MonoBehaviour那样需要附加到游戏对象上。它在内存中作为资产存在,可用于存储AI参数如移动速度、攻击范围或行为权重。在商业项目中,使用ScriptableObject可以实现以下优势:

  • 数据驱动设计:AI行为可以通过外部资产配置,无需修改代码即可调整,便于测试和平衡。
  • 资源重用:多个AI实体可以共享同一个ScriptableObject实例,减少内存占用。
  • 团队协作:策划人员可以通过Unity编辑器直接修改AI参数,无需程序员介入。

在AI系统中,ScriptableObject常用于定义状态机、行为树或决策系统的数据。例如,一个敌人的AI配置可能包括巡逻路径、检测半径和攻击冷却时间,所有这些都可以存储在ScriptableObject中。

实例:创建可脚本化的AI行为配置

以下实例展示如何在Unity中创建一个ScriptableObject来管理AI行为参数,并在游戏中使用它。首先,定义一个ScriptableObject类用于存储AI数据。

在Unity项目中,创建一个脚本文件命名为AiBehaviorConfig.cs,使用Allman风格和驼峰命名法。

using UnityEngine;

// 定义AI行为配置的ScriptableObject类
[CreateAssetMenu(fileName = "NewAiBehaviorConfig", menuName = "AI/Behavior Config")]
public class AiBehaviorConfig : ScriptableObject
{
    // 使用驼峰命名法定义变量
    [Header("Movement Settings")]
    [SerializeField]
    private float moveSpeed = 5.0f;

    [SerializeField]
    private float rotationSpeed = 120.0f;

    [Header("Detection Settings")]
    [SerializeField]
    private float detectionRadius = 10.0f;

    [SerializeField]
    private LayerMask targetLayer;

    [Header("Behavior Weights")]
    [SerializeField]
    [Range(0, 1)]
    private float aggressionWeight = 0.5f;

    [SerializeField]
    [Range(0, 1)]
    private float cautionWeight = 0.5f;

    // 公开属性以便其他脚本访问
    public float MoveSpeed
    {
        get { return moveSpeed; }
        set { moveSpeed = value; }
    }

    public float RotationSpeed
    {
        get { return rotationSpeed; }
        set { rotationSpeed = value; }
    }

    public float DetectionRadius
    {
        get { return detectionRadius; }
        set { detectionRadius = value; }
    }

    public LayerMask TargetLayer
    {
        get { return targetLayer; }
        set { targetLayer = value; }
    }

    public float AggressionWeight
    {
        get { return aggressionWeight; }
        set { aggressionWeight = Mathf.Clamp01(value); }
    }

    public float CautionWeight
    {
        get { return cautionWeight; }
        set { cautionWeight = Mathf.Clamp01(value); }
    }

    // 方法:计算行为决策分数
    public float CalculateBehaviorScore(float distanceToTarget)
    {
        if (distanceToTarget > detectionRadius)
        {
            return 0.0f;
        }

        float normalizedDistance = distanceToTarget / detectionRadius;
        float aggressionScore = aggressionWeight * (1 - normalizedDistance);
        float cautionScore = cautionWeight * normalizedDistance;
        return aggressionScore - cautionScore;
    }
}

接下来,创建一个MonoBehaviour脚本,在游戏对象上使用这个ScriptableObject。命名为AiController.cs。

using UnityEngine;

public class AiController : MonoBehaviour
{
    // 引用ScriptableObject配置
    [SerializeField]
    private AiBehaviorConfig behaviorConfig;

    // 内部变量
    private Transform playerTarget;
    private float currentSpeed;

    void Start()
    {
        // 查找玩家目标(假设玩家标签为"Player")
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null)
        {
            playerTarget = player.transform;
        }
        else
        {
            Debug.LogWarning("Player target not found. AI may not behave correctly.");
        }

        // 初始化速度
        if (behaviorConfig != null)
        {
            currentSpeed = behaviorConfig.MoveSpeed;
        }
    }

    void Update()
    {
        if (behaviorConfig == null || playerTarget == null)
        {
            return;
        }

        // 计算到目标的距离
        float distanceToTarget = Vector3.Distance(transform.position, playerTarget.position);

        // 使用配置计算行为分数
        float behaviorScore = behaviorConfig.CalculateBehaviorScore(distanceToTarget);

        // 根据分数决定行为:正分数表示攻击,负分数表示撤退
        if (behaviorScore > 0)
        {
            MoveTowardsTarget();
        }
        else
        {
            RetreatFromTarget();
        }
    }

    private void MoveTowardsTarget()
    {
        Vector3 direction = (playerTarget.position - transform.position).normalized;
        transform.position += direction * currentSpeed * Time.deltaTime;

        // 旋转面向目标
        Quaternion targetRotation = Quaternion.LookRotation(direction);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, behaviorConfig.RotationSpeed * Time.deltaTime);
    }

    private void RetreatFromTarget()
    {
        Vector3 direction = (transform.position - playerTarget.position).normalized;
        transform.position += direction * currentSpeed * Time.deltaTime;

        // 旋转背向目标
        Quaternion targetRotation = Quaternion.LookRotation(-direction);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, behaviorConfig.RotationSpeed * Time.deltaTime);
    }

    // 在编辑器中可视化检测半径
    void OnDrawGizmosSelected()
    {
        if (behaviorConfig != null)
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(transform.position, behaviorConfig.DetectionRadius);
        }
    }
}

在Unity编辑器中,可以通过Assets菜单创建AiBehaviorConfig资产:点击Assets > Create > AI > Behavior Config,然后调整参数。将资产拖拽到AiController脚本的Behavior Config字段中。运行游戏,AI将根据配置的行为权重移动。

这个实例展示了如何使用ScriptableObject管理AI数据,实现数据驱动的行为决策。在商业项目中,可以扩展此类配置,包括更多参数如动画状态、声音事件或特效触发,从而构建复杂的AI系统。

11.3 随机数生成的高级技巧

随机数在游戏AI中扮演着关键角色,用于模拟不确定性、增加游戏可玩性或平衡难度。然而,Unity默认的Random类可能不满足所有需求,尤其是在需要可控随机性或可重复性的场景中。本节探讨随机数生成的高级技巧,包括种子控制、分布调整和性能优化。

理论知识与应用场景

在AI编程中,随机数常用于以下方面:

  • 决策多样化:例如,AI在多个行动中选择一个,带有随机权重。
  • 行为变化:如移动路径的微小偏移,使AI看起来更自然。
  • 难度调整:通过随机数控制AI的失误率,适应玩家水平。

Unity的Random类提供基本随机功能,但它使用全局状态,可能导致不可预测的结果,特别是在多线程环境中。此外,商业项目可能需要特定分布(如正态分布)的随机数,或需要保存随机状态以实现回放功能。

一个常见问题是随机数的“可重复性”。通过设置种子,开发者可以生成相同的随机序列,便于调试和测试。另外,使用System.Random类可以提供更多控制,但需注意线程安全。

实例:实现可控随机数生成器

以下实例展示如何创建一个自定义随机数生成器,支持种子设置、不同分布和性能优化。首先,定义一个RandomUtility类,封装随机数生成逻辑。

创建脚本RandomUtility.cs。

using UnityEngine;
using System;

public class RandomUtility : MonoBehaviour
{
    // 单例实例,便于全局访问
    private static RandomUtility instance;

    private System.Random systemRandom;
    private uint seed;

    public static RandomUtility Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("RandomUtility");
                instance = go.AddComponent<RandomUtility>();
                DontDestroyOnLoad(go);
            }
            return instance;
        }
    }

    void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this;
        DontDestroyOnLoad(gameObject);

        // 默认使用时间相关的种子
        SetSeed((uint)DateTime.Now.Ticks);
    }

    // 设置种子并初始化随机数生成器
    public void SetSeed(uint newSeed)
    {
        seed = newSeed;
        systemRandom = new System.Random((int)seed);
        UnityEngine.Random.InitState((int)seed);
    }

    // 获取当前种子
    public uint GetSeed()
    {
        return seed;
    }

    // 生成浮点数范围 [min, max]
    public float Range(float min, float max)
    {
        if (min > max)
        {
            float temp = min;
            min = max;
            max = temp;
        }
        double range = max - min;
        double sample = systemRandom.NextDouble();
        double scaled = (sample * range) + min;
        return (float)scaled;
    }

    // 生成整数范围 [min, max]
    public int Range(int min, int max)
    {
        return systemRandom.Next(min, max);
    }

    // 生成正态分布随机数(Box-Muller变换)
    public float NextGaussian(float mean, float standardDeviation)
    {
        float u1 = 1.0f - (float)systemRandom.NextDouble();
        float u2 = (float)systemRandom.NextDouble();
        float randStdNormal = Mathf.Sqrt(-2.0f * Mathf.Log(u1)) * Mathf.Sin(2.0f * Mathf.PI * u2);
        return mean + standardDeviation * randStdNormal;
    }

    // 加权随机选择:根据权重数组返回索引
    public int WeightedRandom(int[] weights)
    {
        if (weights == null || weights.Length == 0)
        {
            return -1;
        }

        int totalWeight = 0;
        foreach (int weight in weights)
        {
            totalWeight += weight;
        }

        int randomValue = systemRandom.Next(0, totalWeight);
        int currentSum = 0;

        for (int i = 0; i < weights.Length; i++)
        {
            currentSum += weights[i];
            if (randomValue < currentSum)
            {
                return i;
            }
        }

        return weights.Length - 1;
    }
}

接下来,创建一个测试脚本RandomAiBehavior.cs,演示如何在AI中使用这个随机数生成器。

using UnityEngine;

public class RandomAiBehavior : MonoBehaviour
{
    // AI行为选项
    public string[] actions = { "Attack", "Defend", "Move", "Idle" };
    public int[] actionWeights = { 30, 20, 40, 10 }; // 对应行动的权重

    private float decisionCooldown = 2.0f;
    private float timeSinceLastDecision = 0.0f;

    void Start()
    {
        // 设置随机种子,确保可重复性(在商业项目中,种子可能来自游戏设置)
        RandomUtility.Instance.SetSeed(12345);
    }

    void Update()
    {
        timeSinceLastDecision += Time.deltaTime;
        if (timeSinceLastDecision >= decisionCooldown)
        {
            MakeDecision();
            timeSinceLastDecision = 0.0f;
        }
    }

    private void MakeDecision()
    {
        // 使用加权随机选择行动
        int actionIndex = RandomUtility.Instance.WeightedRandom(actionWeights);
        if (actionIndex >= 0 && actionIndex < actions.Length)
        {
            string chosenAction = actions[actionIndex];
            Debug.Log(gameObject.name + " chooses to: " + chosenAction);

            // 根据行动执行AI行为
            ExecuteAction(chosenAction);
        }
    }

    private void ExecuteAction(string action)
    {
        switch (action)
        {
            case "Attack":
                // 模拟攻击行为:移动向目标,带随机偏移
                Vector3 attackDirection = GetTargetDirection() + GetRandomOffset(0.5f);
                MoveInDirection(attackDirection);
                break;
            case "Defend":
                // 防御行为:后退并减少速度
                Vector3 defendDirection = -GetTargetDirection();
                MoveInDirection(defendDirection * 0.5f);
                break;
            case "Move":
                // 移动行为:随机方向
                Vector3 randomDirection = new Vector3(
                    RandomUtility.Instance.Range(-1.0f, 1.0f),
                    0,
                    RandomUtility.Instance.Range(-1.0f, 1.0f)
                ).normalized;
                MoveInDirection(randomDirection);
                break;
            case "Idle":
                // 闲置:不移动
                Debug.Log(gameObject.name + " is idling.");
                break;
        }
    }

    private Vector3 GetTargetDirection()
    {
        // 假设目标为玩家
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null)
        {
            return (player.transform.position - transform.position).normalized;
        }
        return Vector3.zero;
    }

    private Vector3 GetRandomOffset(float maxOffset)
    {
        float offsetX = RandomUtility.Instance.Range(-maxOffset, maxOffset);
        float offsetZ = RandomUtility.Instance.Range(-maxOffset, maxOffset);
        return new Vector3(offsetX, 0, offsetZ);
    }

    private void MoveInDirection(Vector3 direction)
    {
        float speed = 3.0f;
        transform.position += direction * speed * Time.deltaTime;
        if (direction != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(direction);
        }
    }
}

在Unity场景中,创建一个立方体作为AI对象,附加RandomAiBehavior脚本,并设置玩家标签为"Player"。运行游戏,AI将每2秒根据权重随机选择行动,并使用可控随机数生成偏移。

这个实例展示了如何超越Unity默认随机功能,实现种子控制、加权随机和正态分布。在商业项目中,这种可控性对于调试、平衡和玩家体验至关重要。例如,在竞速游戏中,AI车辆的失误率可以用正态分布随机数模拟,使表现更真实。

11.4 设计智能空气曲棍球对手AI

空气曲棍球是一种快节奏的体育游戏,对手AI需要实时反应、预测玩家动作并做出智能决策。本节探讨如何构建一个空气曲棍球游戏对手AI,涵盖移动策略、击球逻辑和难度调整。

理论知识与应用场景

在空气曲棍球中,AI对手的核心任务是防守己方球门并进攻玩家球门。这需要以下AI组件:

  • 球轨迹预测:基于球的当前位置和速度,计算其未来位置,以便提前移动。
  • 移动控制:AI球拍应平滑移动,避免不自然瞬移,同时保持反应速度。
  • 击球策略:AI需要决定击球方向和力度,可能包括防御性回击或进攻性射门。

商业项目中,AI难度通常通过调整预测精度、反应时间或移动速度来实现。例如,简单AI可能只反应当前球位置,而困难AI可能预测反弹并设置陷阱。

物理在空气曲棍球中至关重要。Unity的物理引擎可用于模拟球和球拍的碰撞,但AI需要理解物理结果以做出决策。此外,AI应避免“完美”表现,通过引入误差模拟人类失误,增强游戏可玩性。

实例:构建空气曲棍球AI对手

以下实例展示如何创建一个空气曲棍球AI对手,包括球预测、移动和击球逻辑。首先,设置游戏场景:一个平面作为球场,两个球拍(玩家和AI),一个球。所有对象使用刚体组件。

创建脚本AirHockeyAi.cs,附加到AI球拍上。

using UnityEngine;

public class AirHockeyAi : MonoBehaviour
{
    // 引用对象
    [SerializeField]
    private GameObject ball;

    [SerializeField]
    private GameObject playerMallet;

    [SerializeField]
    private GameObject aiGoal;

    [SerializeField]
    private GameObject playerGoal;

    // AI参数
    [SerializeField]
    private float moveSpeed = 10.0f;

    [SerializeField]
    private float predictionTime = 0.5f; // 预测球未来位置的时间

    [SerializeField]
    private float reactionDelay = 0.1f; // AI反应延迟,模拟人类反应

    [SerializeField]
    private float errorMargin = 0.2f; // 击球误差,增加随机性

    // 内部变量
    private Rigidbody ballRigidbody;
    private Rigidbody aiRigidbody;
    private Vector3 targetPosition;
    private float timeSinceLastReaction;
    private bool isAttacking;

    void Start()
    {
        if (ball != null)
        {
            ballRigidbody = ball.GetComponent<Rigidbody>();
        }
        aiRigidbody = GetComponent<Rigidbody>();
        timeSinceLastReaction = 0.0f;
        isAttacking = false;
    }

    void Update()
    {
        timeSinceLastReaction += Time.deltaTime;
        if (timeSinceLastReaction >= reactionDelay)
        {
            UpdateTargetPosition();
            timeSinceLastReaction = 0.0f;
        }
    }

    void FixedUpdate()
    {
        MoveTowardsTarget();
        AdjustStrikeStrategy();
    }

    private void UpdateTargetPosition()
    {
        if (ballRigidbody == null)
        {
            return;
        }

        // 预测球未来位置
        Vector3 ballVelocity = ballRigidbody.velocity;
        Vector3 predictedPosition = ball.transform.position + (ballVelocity * predictionTime);

        // 限制预测位置在球场内(假设球场在XZ平面,范围-10到10)
        predictedPosition.x = Mathf.Clamp(predictedPosition.x, -9.0f, 9.0f);
        predictedPosition.z = Mathf.Clamp(predictedPosition.z, -4.0f, 4.0f);

        // 如果球朝向AI球门,优先防御
        float ballToAiGoalDirection = Vector3.Dot(ballVelocity, (aiGoal.transform.position - ball.transform.position).normalized);
        if (ballToAiGoalDirection > 0)
        {
            // 球飞向AI球门:防御模式
            isAttacking = false;
            // 目标位置为预测位置和球门之间的拦截点
            targetPosition = Vector3.Lerp(predictedPosition, aiGoal.transform.position, 0.3f);
        }
        else
        {
            // 球飞向玩家球门:进攻模式
            isAttacking = true;
            // 目标位置为预测位置,但偏向玩家球门
            targetPosition = Vector3.Lerp(predictedPosition, playerGoal.transform.position, 0.2f);
        }

        // 添加随机误差,模拟AI不完美
        targetPosition += new Vector3(
            Random.Range(-errorMargin, errorMargin),
            0,
            Random.Range(-errorMargin, errorMargin)
        );

        // 确保目标位置在AI半场(假设AI在负Z侧)
        targetPosition.z = Mathf.Clamp(targetPosition.z, -4.0f, 0.0f);
    }

    private void MoveTowardsTarget()
    {
        if (aiRigidbody == null)
        {
            return;
        }

        Vector3 direction = (targetPosition - transform.position).normalized;
        Vector3 movement = direction * moveSpeed * Time.fixedDeltaTime;

        // 使用刚体移动以兼容物理
        aiRigidbody.MovePosition(transform.position + movement);
    }

    private void AdjustStrikeStrategy()
    {
        // 如果球在击球范围内,施加击球力
        float distanceToBall = Vector3.Distance(transform.position, ball.transform.position);
        float strikeRadius = 1.5f; // 球拍击球半径

        if (distanceToBall < strikeRadius && ballRigidbody != null)
        {
            Vector3 strikeDirection;

            if (isAttacking)
            {
                // 进攻击球:朝向玩家球门,带随机角度
                strikeDirection = (playerGoal.transform.position - ball.transform.position).normalized;
                strikeDirection += new Vector3(Random.Range(-0.3f, 0.3f), 0, Random.Range(0.0f, 0.5f));
            }
            else
            {
                // 防御击球:将球击离AI球门
                strikeDirection = (ball.transform.position - aiGoal.transform.position).normalized;
                strikeDirection += new Vector3(Random.Range(-0.2f, 0.2f), 0, Random.Range(0.0f, 0.3f));
            }

            strikeDirection.Normalize();
            float strikeForce = isAttacking ? 15.0f : 10.0f; // 进攻时更用力
            ballRigidbody.AddForce(strikeDirection * strikeForce, ForceMode.Impulse);
        }
    }

    void OnDrawGizmosSelected()
    {
        // 可视化目标位置
        Gizmos.color = Color.red;
        Gizmos.DrawSphere(targetPosition, 0.2f);
    }
}

创建脚本BallReset.cs,用于重置球位置,附加到球上。

using UnityEngine;

public class BallReset : MonoBehaviour
{
    private Vector3 initialPosition;

    void Start()
    {
        initialPosition = transform.position;
    }

    void OnTriggerEnter(Collider other)
    {
        // 假设球门有触发器,球进入后重置
        if (other.CompareTag("Goal"))
        {
            ResetBall();
        }
    }

    private void ResetBall()
    {
        GetComponent<Rigidbody>().velocity = Vector3.zero;
        transform.position = initialPosition;
    }
}

在Unity中,设置场景:

  1. 创建一个平面作为球场,缩放至合适大小。
  2. 创建两个球拍(立方体),一个标记为"Player",另一个附加AirHockeyAi脚本。
  3. 创建一个球(球体),附加刚体和BallReset脚本。
  4. 创建两个球门(立方体),标记为"Goal",并设置触发器碰撞器。
  5. 在AirHockeyAi脚本中,分配ball、playerMallet、aiGoal和playerGoal引用。

运行游戏,AI将根据球的位置和速度移动,并击球。调整predictionTime、reactionDelay和errorMargin参数可以改变AI难度。

这个实例演示了如何结合物理预测和策略决策构建游戏特定AI。在商业空气曲棍球游戏中,可以扩展此AI,包括学习玩家模式或自适应难度,以提升长期可玩性。

11.5 竞速游戏AI架构实现

竞速游戏中的AI对手需要模拟真实赛车行为,包括路径跟踪、超车决策和适应性速度控制。本节探讨竞速游戏AI架构,涵盖航点系统、状态机和物理集成。

理论知识与应用场景

竞速游戏AI通常基于以下组件:

  • 航点系统:AI车辆沿着预设路径移动,航点定义赛道中心线或最佳赛车线。
  • 速度管理:AI根据弯道、玩家位置和难度设置调整速度。
  • 决策逻辑:包括超车、防守或失误,以增加比赛不确定性。

商业竞速游戏如《极限竞速》或《跑跑卡丁车》使用复杂AI系统,平衡挑战性和公平性。AI不应完美,而应模拟人类车手的行为,包括错误和风格变化。

Unity中,AI架构可以使用有限状态机(FSM)管理不同驾驶状态(如加速、刹车、漂移)。此外,物理集成是关键,因为车辆通常使用轮式碰撞器或刚体模拟。AI需要理解车辆动力学以做出合理决策。

实例:实现基本竞速AI架构

以下实例展示如何创建一个竞速AI系统,包括航点跟踪、速度控制和简单决策。首先,设置赛道:创建一系列空对象作为航点,形成一个闭环路径。

创建脚本WaypointPath.cs,管理航点。

using UnityEngine;
using System.Collections.Generic;

public class WaypointPath : MonoBehaviour
{
    public List<Transform> waypoints = new List<Transform>();
    public bool isLoop = true;

    void OnDrawGizmos()
    {
        if (waypoints == null || waypoints.Count < 2)
        {
            return;
        }

        Gizmos.color = Color.green;
        for (int i = 0; i < waypoints.Count; i++)
        {
            if (waypoints[i] == null)
            {
                continue;
            }

            Gizmos.DrawSphere(waypoints[i].position, 0.5f);

            if (i < waypoints.Count - 1 && waypoints[i + 1] != null)
            {
                Gizmos.DrawLine(waypoints[i].position, waypoints[i + 1].position);
            }
        }

        if (isLoop && waypoints.Count > 1 && waypoints[0] != null && waypoints[waypoints.Count - 1] != null)
        {
            Gizmos.DrawLine(waypoints[waypoints.Count - 1].position, waypoints[0].position);
        }
    }

    public Transform GetNextWaypoint(int currentIndex)
    {
        if (waypoints.Count == 0)
        {
            return null;
        }

        int nextIndex = currentIndex + 1;
        if (nextIndex >= waypoints.Count)
        {
            if (isLoop)
            {
                nextIndex = 0;
            }
            else
            {
                return null;
            }
        }

        return waypoints[nextIndex];
    }
}

创建脚本RacingAiController.cs,附加到AI车辆上。

using UnityEngine;

public enum AiDriveState
{
    Accelerating,
    Braking,
    Cornering,
    Overtaking
}

public class RacingAiController : MonoBehaviour
{
    // 引用
    [SerializeField]
    private WaypointPath waypointPath;

    [SerializeField]
    private GameObject playerVehicle;

    // AI参数
    [SerializeField]
    private float maxSpeed = 20.0f;

    [SerializeField]
    private float acceleration = 10.0f;

    [SerializeField]
    private float brakingForce = 15.0f;

    [SerializeField]
    private float corneringSpeedFactor = 0.7f; // 弯道减速因子

    [SerializeField]
    private float waypointReachDistance = 2.0f;

    [SerializeField]
    private float overtakingDistance = 5.0f;

    // 内部变量
    private Rigidbody vehicleRigidbody;
    private int currentWaypointIndex;
    private AiDriveState currentState;
    private float currentSpeed;
    private float stateTimer;

    void Start()
    {
        vehicleRigidbody = GetComponent<Rigidbody>();
        currentWaypointIndex = 0;
        currentState = AiDriveState.Accelerating;
        currentSpeed = 0.0f;
        stateTimer = 0.0f;

        if (waypointPath == null)
        {
            Debug.LogError("WaypointPath not assigned to RacingAiController.");
        }
    }

    void Update()
    {
        stateTimer += Time.deltaTime;
        UpdateState();
    }

    void FixedUpdate()
    {
        Drive();
    }

    private void UpdateState()
    {
        // 基于条件转换状态
        switch (currentState)
        {
            case AiDriveState.Accelerating:
                // 如果接近弯道,切换到Cornering
                if (IsCornerAhead())
                {
                    currentState = AiDriveState.Cornering;
                    stateTimer = 0.0f;
                }
                // 如果玩家在面前且接近,尝试超车
                else if (IsPlayerAhead() && Vector3.Distance(transform.position, playerVehicle.transform.position) < overtakingDistance)
                {
                    currentState = AiDriveState.Overtaking;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Cornering:
                // 弯道状态持续一段时间后恢复加速
                if (stateTimer > 1.5f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Overtaking:
                // 超车状态持续一段时间后恢复
                if (stateTimer > 3.0f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Braking:
                // 刹车后恢复加速
                if (currentSpeed < maxSpeed * 0.5f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;
        }
    }

    private void Drive()
    {
        if (waypointPath == null)
        {
            return;
        }

        // 获取当前目标航点
        Transform currentWaypoint = waypointPath.waypoints[currentWaypointIndex];
        if (currentWaypoint == null)
        {
            return;
        }

        // 计算朝向航点的方向
        Vector3 directionToWaypoint = (currentWaypoint.position - transform.position).normalized;
        directionToWaypoint.y = 0; // 保持水平移动

        // 根据状态调整速度
        float targetSpeed = maxSpeed;

        switch (currentState)
        {
            case AiDriveState.Accelerating:
                targetSpeed = maxSpeed;
                break;
            case AiDriveState.Braking:
                targetSpeed = maxSpeed * 0.3f;
                break;
            case AiDriveState.Cornering:
                targetSpeed = maxSpeed * corneringSpeedFactor;
                break;
            case AiDriveState.Overtaking:
                targetSpeed = maxSpeed * 1.2f; // 超车时稍微加速
                break;
        }

        // 应用速度控制
        float speedDifference = targetSpeed - currentSpeed;
        if (speedDifference > 0)
        {
            currentSpeed += acceleration * Time.fixedDeltaTime;
        }
        else
        {
            currentSpeed += brakingForce * Time.fixedDeltaTime * Mathf.Sign(speedDifference);
        }
        currentSpeed = Mathf.Clamp(currentSpeed, 0, targetSpeed);

        // 移动车辆
        Vector3 movement = transform.forward * currentSpeed * Time.fixedDeltaTime;
        vehicleRigidbody.MovePosition(transform.position + movement);

        // 旋转车辆朝向航点
        Quaternion targetRotation = Quaternion.LookRotation(directionToWaypoint);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.fixedDeltaTime * 2.0f);

        // 检查是否到达航点
        if (Vector3.Distance(transform.position, currentWaypoint.position) < waypointReachDistance)
        {
            currentWaypointIndex++;
            if (currentWaypointIndex >= waypointPath.waypoints.Count)
            {
                if (waypointPath.isLoop)
                {
                    currentWaypointIndex = 0;
                }
                else
                {
                    currentWaypointIndex = waypointPath.waypoints.Count - 1;
                }
            }
        }
    }

    private bool IsCornerAhead()
    {
        // 简单检测:如果下一个航点方向变化大,视为弯道
        if (waypointPath.waypoints.Count < 2 || currentWaypointIndex >= waypointPath.waypoints.Count - 1)
        {
            return false;
        }

        Transform currentWaypoint = waypointPath.waypoints[currentWaypointIndex];
        Transform nextWaypoint = waypointPath.GetNextWaypoint(currentWaypointIndex);

        if (currentWaypoint == null || nextWaypoint == null)
        {
            return false;
        }

        Vector3 currentDirection = (currentWaypoint.position - transform.position).normalized;
        Vector3 nextDirection = (nextWaypoint.position - currentWaypoint.position).normalized;

        float angle = Vector3.Angle(currentDirection, nextDirection);
        return angle > 30.0f; // 角度阈值
    }

    private bool IsPlayerAhead()
    {
        if (playerVehicle == null)
        {
            return false;
        }

        // 检查玩家是否在AI前方
        Vector3 toPlayer = playerVehicle.transform.position - transform.position;
        float dotProduct = Vector3.Dot(transform.forward, toPlayer.normalized);
        return dotProduct > 0.5f && toPlayer.magnitude < overtakingDistance * 2.0f;
    }

    void OnDrawGizmosSelected()
    {
        // 可视化当前目标航点
        if (waypointPath != null && currentWaypointIndex < waypointPath.waypoints.Count)
        {
            Transform waypoint = waypointPath.waypoints[currentWaypointIndex];
            if (waypoint != null)
            {
                Gizmos.color = Color.blue;
                Gizmos.DrawLine(transform.position, waypoint.position);
            }
        }
    }
}

在Unity中,设置场景:

  1. 创建一个空对象附加WaypointPath脚本,添加一系列子空对象作为航点,形成赛道。
  2. 创建AI车辆(立方体或导入模型),附加刚体和RacingAiController脚本。
  3. 创建玩家车辆,标记为"Player",并分配给AI脚本的playerVehicle字段。
  4. 调整AI参数如maxSpeed和acceleration。

运行游戏,AI将沿着航点移动,并根据状态调整行为。这个实例提供了竞速AI的基本架构,在商业项目中可以扩展,例如添加更多状态(如漂移)、动态航点生成或机器学习决策。

11.6 橡皮筋系统在竞速难度调整中的应用

橡皮筋系统是一种动态难度调整机制,在竞速游戏中用于平衡AI与玩家之间的差距,确保比赛紧张且公平。本节探讨橡皮筋系统的原理、实现方式及其在Unity中的集成。

理论知识与应用场景

橡皮筋系统通过实时调整AI车辆的性能(如速度、加速度或失误率),使AI始终接近玩家,无论玩家技能如何。这避免了AI领先太多或落后太远,提升游戏体验。系统名称源于橡皮筋的弹性:AI像被橡皮筋拴在玩家身上,距离越远,拉力越大。

在商业游戏中,橡皮筋系统常见于《马里奥赛车》等休闲竞速,但也用于模拟游戏以增加真实性。实现方式包括:

  • 速度调整:基于与玩家的距离,动态修改AI的最大速度。
  • 失误模拟:当AI领先时,增加刹车或转向错误。
  • 道具影响:在道具竞速中,调整AI获取道具的概率。

关键设计原则是隐蔽性:玩家不应明显察觉系统干预,否则会感觉不公平。因此,调整应平滑且基于游戏内因素(如赛道位置或车辆状态)。

实例:实现橡皮筋系统管理竞速难度

以下实例展示如何在竞速AI中集成橡皮筋系统,动态调整AI速度以保持接近玩家。基于11.5节的竞速AI架构,添加橡皮筋逻辑。

修改RacingAiController.cs,添加橡皮筋功能。

using UnityEngine;

public enum AiDriveState
{
    Accelerating,
    Braking,
    Cornering,
    Overtaking
}

public class RacingAiController : MonoBehaviour
{
    // 引用
    [SerializeField]
    private WaypointPath waypointPath;

    [SerializeField]
    private GameObject playerVehicle;

    // AI参数
    [SerializeField]
    private float baseMaxSpeed = 20.0f; // 基础最大速度

    [SerializeField]
    private float acceleration = 10.0f;

    [SerializeField]
    private float brakingForce = 15.0f;

    [SerializeField]
    private float corneringSpeedFactor = 0.7f;

    [SerializeField]
    private float waypointReachDistance = 2.0f;

    [SerializeField]
    private float overtakingDistance = 5.0f;

    // 橡皮筋参数
    [SerializeField]
    private float rubberBandStrength = 0.5f; // 强度:0为无,1为强

    [SerializeField]
    private float maxSpeedBoost = 5.0f; // 最大速度加成

    [SerializeField]
    private float minSpeedPenalty = 3.0f; // 最小速度惩罚

    // 内部变量
    private Rigidbody vehicleRigidbody;
    private int currentWaypointIndex;
    private AiDriveState currentState;
    private float currentSpeed;
    private float stateTimer;
    private float effectiveMaxSpeed; // 受橡皮筋影响的实际最大速度

    void Start()
    {
        vehicleRigidbody = GetComponent<Rigidbody>();
        currentWaypointIndex = 0;
        currentState = AiDriveState.Accelerating;
        currentSpeed = 0.0f;
        stateTimer = 0.0f;
        effectiveMaxSpeed = baseMaxSpeed;

        if (waypointPath == null)
        {
            Debug.LogError("WaypointPath not assigned to RacingAiController.");
        }
    }

    void Update()
    {
        stateTimer += Time.deltaTime;
        UpdateState();
        ApplyRubberBandEffect();
    }

    void FixedUpdate()
    {
        Drive();
    }

    private void UpdateState()
    {
        // 基于条件转换状态(同前,略作调整)
        switch (currentState)
        {
            case AiDriveState.Accelerating:
                if (IsCornerAhead())
                {
                    currentState = AiDriveState.Cornering;
                    stateTimer = 0.0f;
                }
                else if (IsPlayerAhead() && Vector3.Distance(transform.position, playerVehicle.transform.position) < overtakingDistance)
                {
                    currentState = AiDriveState.Overtaking;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Cornering:
                if (stateTimer > 1.5f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Overtaking:
                if (stateTimer > 3.0f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;

            case AiDriveState.Braking:
                if (currentSpeed < effectiveMaxSpeed * 0.5f)
                {
                    currentState = AiDriveState.Accelerating;
                    stateTimer = 0.0f;
                }
                break;
        }
    }

    private void Drive()
    {
        if (waypointPath == null)
        {
            return;
        }

        Transform currentWaypoint = waypointPath.waypoints[currentWaypointIndex];
        if (currentWaypoint == null)
        {
            return;
        }

        Vector3 directionToWaypoint = (currentWaypoint.position - transform.position).normalized;
        directionToWaypoint.y = 0;

        float targetSpeed = effectiveMaxSpeed;

        switch (currentState)
        {
            case AiDriveState.Accelerating:
                targetSpeed = effectiveMaxSpeed;
                break;
            case AiDriveState.Braking:
                targetSpeed = effectiveMaxSpeed * 0.3f;
                break;
            case AiDriveState.Cornering:
                targetSpeed = effectiveMaxSpeed * corneringSpeedFactor;
                break;
            case AiDriveState.Overtaking:
                targetSpeed = effectiveMaxSpeed * 1.2f;
                break;
        }

        float speedDifference = targetSpeed - currentSpeed;
        if (speedDifference > 0)
        {
            currentSpeed += acceleration * Time.fixedDeltaTime;
        }
        else
        {
            currentSpeed += brakingForce * Time.fixedDeltaTime * Mathf.Sign(speedDifference);
        }
        currentSpeed = Mathf.Clamp(currentSpeed, 0, targetSpeed);

        Vector3 movement = transform.forward * currentSpeed * Time.fixedDeltaTime;
        vehicleRigidbody.MovePosition(transform.position + movement);

        Quaternion targetRotation = Quaternion.LookRotation(directionToWaypoint);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.fixedDeltaTime * 2.0f);

        if (Vector3.Distance(transform.position, currentWaypoint.position) < waypointReachDistance)
        {
            currentWaypointIndex++;
            if (currentWaypointIndex >= waypointPath.waypoints.Count)
            {
                if (waypointPath.isLoop)
                {
                    currentWaypointIndex = 0;
                }
                else
                {
                    currentWaypointIndex = waypointPath.waypoints.Count - 1;
                }
            }
        }
    }

    private void ApplyRubberBandEffect()
    {
        if (playerVehicle == null)
        {
            effectiveMaxSpeed = baseMaxSpeed;
            return;
        }

        // 计算AI相对于玩家的位置(基于航点进度,简化:使用距离)
        float distanceToPlayer = Vector3.Distance(transform.position, playerVehicle.transform.position);
        float relativePosition = 0.0f;

        // 如果AI在玩家前面,distanceToPlayer为正,但需确定领先或落后
        // 简化:使用AI和玩家的Y坐标(假设赛道有高度变化)或航点索引
        // 这里使用航点索引作为进度估计
        int playerWaypointIndex = GetPlayerWaypointIndex();
        int progressDifference = currentWaypointIndex - playerWaypointIndex;

        // 调整effectiveMaxSpeed基于进度差
        float speedAdjustment = 0.0f;
        if (progressDifference > 0)
        {
            // AI领先:减速
            speedAdjustment = -Mathf.Clamp(progressDifference * rubberBandStrength, 0, minSpeedPenalty);
        }
        else if (progressDifference < 0)
        {
            // AI落后:加速
            speedAdjustment = Mathf.Clamp(-progressDifference * rubberBandStrength, 0, maxSpeedBoost);
        }
        else
        {
            // 并驾齐驱:小随机波动
            speedAdjustment = Random.Range(-0.5f, 0.5f);
        }

        effectiveMaxSpeed = baseMaxSpeed + speedAdjustment;
        effectiveMaxSpeed = Mathf.Clamp(effectiveMaxSpeed, baseMaxSpeed - minSpeedPenalty, baseMaxSpeed + maxSpeedBoost);
    }

    private int GetPlayerWaypointIndex()
    {
        // 简化:假设玩家有类似脚本。在实际项目中,需共享航点系统
        // 这里返回一个估计值
        if (playerVehicle != null)
        {
            // 可以添加玩家脚本跟踪航点,这里使用距离最近航点
            float minDistance = float.MaxValue;
            int nearestIndex = 0;
            for (int i = 0; i < waypointPath.waypoints.Count; i++)
            {
                float dist = Vector3.Distance(playerVehicle.transform.position, waypointPath.waypoints[i].position);
                if (dist < minDistance)
                {
                    minDistance = dist;
                    nearestIndex = i;
                }
            }
            return nearestIndex;
        }
        return currentWaypointIndex;
    }

    private bool IsCornerAhead()
    {
        if (waypointPath.waypoints.Count < 2 || currentWaypointIndex >= waypointPath.waypoints.Count - 1)
        {
            return false;
        }

        Transform currentWaypoint = waypointPath.waypoints[currentWaypointIndex];
        Transform nextWaypoint = waypointPath.GetNextWaypoint(currentWaypointIndex);

        if (currentWaypoint == null || nextWaypoint == null)
        {
            return false;
        }

        Vector3 currentDirection = (currentWaypoint.position - transform.position).normalized;
        Vector3 nextDirection = (nextWaypoint.position - currentWaypoint.position).normalized;

        float angle = Vector3.Angle(currentDirection, nextDirection);
        return angle > 30.0f;
    }

    private bool IsPlayerAhead()
    {
        if (playerVehicle == null)
        {
            return false;
        }

        Vector3 toPlayer = playerVehicle.transform.position - transform.position;
        float dotProduct = Vector3.Dot(transform.forward, toPlayer.normalized);
        return dotProduct > 0.5f && toPlayer.magnitude < overtakingDistance * 2.0f;
    }

    void OnDrawGizmosSelected()
    {
        if (waypointPath != null && currentWaypointIndex < waypointPath.waypoints.Count)
        {
            Transform waypoint = waypointPath.waypoints[currentWaypointIndex];
            if (waypoint != null)
            {
                Gizmos.color = Color.blue;
                Gizmos.DrawLine(transform.position, waypoint.position);
            }
        }
    }
}

为了完整,创建一个简单玩家控制器脚本PlayerVehicleController.cs,用于测试。

using UnityEngine;

public class PlayerVehicleController : MonoBehaviour
{
    public float speed = 15.0f;
    public float rotationSpeed = 100.0f;

    void Update()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");

        Vector3 movement = transform.forward * moveVertical * speed * Time.deltaTime;
        transform.position += movement;

        float rotation = moveHorizontal * rotationSpeed * Time.deltaTime;
        transform.Rotate(0, rotation, 0);
    }
}

在Unity中,设置玩家车辆附加PlayerVehicleController脚本,并确保AI脚本引用玩家。运行游戏,AI将根据相对位置动态调整速度:领先时减速,落后时加速。

这个实例展示了橡皮筋系统的基本实现。在商业项目中,可以扩展为多因素调整,如基于玩家表现、赛道段或游戏模式。关键是通过平滑过渡和合理参数隐藏调整,保持游戏沉浸感。

总结

本章深入探讨了Unity引擎中人工智能编程的高级主题,基于可编写脚本对象、随机数处理、游戏特定AI和动态难度系统。通过详细的理论讲解和实例代码,展示了如何在商业项目中应用这些技术,提升AI的智能性、可维护性和玩家体验。所有代码采用Allman风格和驼峰命名法,确保在Unity 2021.3.8f1c1、VS2022和VSCODE环境中可运行。开发者可以在此基础上扩展,构建更复杂的AI系统,满足现代游戏的需求。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐