第11章 Unity AI进阶实战:对象管理、随机处理与游戏系统优化
本章深入探讨Unity AI编程的高级应用,重点介绍可编写脚本对象(ScriptableObject)的管理、随机数处理优化、游戏特定AI系统构建及动态难度调整机制。通过创建可脚本化的AI行为配置,实现数据驱动设计,便于团队协作和快速迭代。内容包括:ScriptableObject的理论基础与应用场景;创建AI行为配置的具体实现代码;以及如何在游戏控制器中使用这些配置。实例展示了如何利用Scrip
第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中,设置场景:
- 创建一个平面作为球场,缩放至合适大小。
- 创建两个球拍(立方体),一个标记为"Player",另一个附加AirHockeyAi脚本。
- 创建一个球(球体),附加刚体和BallReset脚本。
- 创建两个球门(立方体),标记为"Goal",并设置触发器碰撞器。
- 在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中,设置场景:
- 创建一个空对象附加WaypointPath脚本,添加一系列子空对象作为航点,形成赛道。
- 创建AI车辆(立方体或导入模型),附加刚体和RacingAiController脚本。
- 创建玩家车辆,标记为"Player",并分配给AI脚本的playerVehicle字段。
- 调整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系统,满足现代游戏的需求。
更多推荐



所有评论(0)