第8章 Unity引擎中第三人称射击游戏AI系统构建详解
本章详细介绍了Unity引擎中第三人称射击游戏(TPS)AI系统的构建方法。主要内容包括:1)TPS游戏AI系统架构设计,采用分层结构(感知、决策、执行系统);2)敌人AI行为树逻辑构建,包含感知模块(视觉/听觉检测)、决策模块(战术选择)和移动模块(路径规划与掩体利用);3)Unity NavMesh系统在AI导航中的应用;4)代码示例展示了AI感知系统(CanSeePlayer/CanHear
第8章 Unity引擎中第三人称射击游戏AI系统构建详解
8.1 TPS游戏AI架构规划与设计原理
8.1.1 第三人称射击游戏AI系统概述
第三人称射击游戏(Third-Person Shooter,简称TPS)是一种玩家角色在游戏世界中以第三视角呈现的射击游戏类型。在这种游戏模式中,人工智能系统的设计需要特别关注角色行为的可视性和战术合理性。与第一人称射击游戏相比,TPS游戏的AI角色需要更加注重移动路径的可观察性和战术掩体的利用。
人工智能在TPS游戏中的核心作用主要体现在敌人角色的自主决策能力上。一个优秀的TPS游戏AI系统应当具备以下特征:战术意识、环境感知、自适应行为和玩家互动反馈。战术意识使AI角色能够理解战场局势并做出相应决策;环境感知让AI能够识别掩体、危险区域和战术位置;自适应行为确保AI能根据玩家行为调整策略;玩家互动反馈则保证游戏体验的动态性和挑战性。
在Unity引擎中实现TPS游戏AI,通常采用分层架构设计。最底层是感知系统,负责收集环境信息;中间层是决策系统,基于行为树或状态机做出判断;最高层是执行系统,控制角色的具体动作。这种分层设计提高了代码的模块化和可维护性。
8.1.2 敌人AI角色行为树逻辑构建
行为树(Behavior Tree)是一种用于控制AI决策的树状结构模型,在游戏AI开发中广泛应用。与有限状态机相比,行为树更具可扩展性和可读性,特别适合复杂的AI行为逻辑。
行为树的基本组成节点包括:
- 控制节点:决定子节点的执行顺序,包括选择节点、序列节点和平行节点
- 条件节点:检查特定条件是否满足,返回成功或失败
- 动作节点:执行具体行为,如移动、攻击、躲避等
在TPS游戏中,敌人AI的行为树设计需要考虑以下核心行为模块:
感知模块:负责检测玩家位置、判断视线遮挡、听觉感知等。这个模块需要高效地处理空间查询和物理检测。
using UnityEngine;
using UnityEngine.AI;
public class AIPerceptionSystem : MonoBehaviour
{
[Header("视觉设置")]
[SerializeField] private float visionRange = 20f;
[SerializeField] private float fieldOfView = 90f;
[SerializeField] private LayerMask visionBlockingLayers;
[Header("听觉设置")]
[SerializeField] private float hearingRange = 15f;
private Transform playerTransform;
private NavMeshAgent navAgent;
private void Start()
{
navAgent = GetComponent<NavMeshAgent>();
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
}
public bool CanSeePlayer()
{
if (playerTransform == null)
{
return false;
}
Vector3 directionToPlayer = playerTransform.position - transform.position;
float distanceToPlayer = directionToPlayer.magnitude;
if (distanceToPlayer > visionRange)
{
return false;
}
float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
if (angleToPlayer > fieldOfView / 2f)
{
return false;
}
RaycastHit hit;
if (Physics.Raycast(transform.position, directionToPlayer.normalized,
out hit, distanceToPlayer, visionBlockingLayers))
{
if (hit.transform.CompareTag("Player"))
{
return true;
}
}
return false;
}
public bool CanHearPlayer(Vector3 playerNoisePosition)
{
float distanceToNoise = Vector3.Distance(transform.position, playerNoisePosition);
return distanceToNoise <= hearingRange;
}
public float GetDistanceToPlayer()
{
if (playerTransform == null)
{
return Mathf.Infinity;
}
return Vector3.Distance(transform.position, playerTransform.position);
}
}
决策模块:基于当前游戏状态和感知信息,选择最合适的战术行为。在TPS游戏中,常见的决策包括选择攻击位置、决定是否寻找掩体、评估撤退时机等。
移动模块:控制AI角色的导航和移动,包括路径规划、障碍物回避和掩体利用。Unity的NavMesh系统为这一模块提供了强大支持。
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
public class AIMovementController : MonoBehaviour
{
private NavMeshAgent agent;
private Animator animator;
private Vector3 currentDestination;
[Header("移动参数")]
[SerializeField] private float normalSpeed = 3.5f;
[SerializeField] private float combatSpeed = 2.5f;
[SerializeField] private float rotationSpeed = 10f;
[Header("掩体参数")]
[SerializeField] private float coverCheckRadius = 1f;
[SerializeField] private LayerMask coverLayerMask;
private List<Vector3> nearbyCoverPositions = new List<Vector3>();
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
}
private void Start()
{
agent.speed = normalSpeed;
agent.angularSpeed = rotationSpeed;
}
public bool MoveToPosition(Vector3 position)
{
if (agent.isActiveAndEnabled && agent.isOnNavMesh)
{
currentDestination = position;
agent.SetDestination(position);
return true;
}
return false;
}
public bool FindAndMoveToCover(Vector3 threatPosition)
{
FindNearbyCoverPositions();
if (nearbyCoverPositions.Count == 0)
{
return false;
}
Vector3 bestCover = FindBestCoverPosition(threatPosition);
if (bestCover != Vector3.zero)
{
MoveToPosition(bestCover);
return true;
}
return false;
}
private void FindNearbyCoverPositions()
{
nearbyCoverPositions.Clear();
Collider[] coverColliders = Physics.OverlapSphere(
transform.position, 15f, coverLayerMask);
foreach (Collider cover in coverColliders)
{
Vector3 coverPosition = FindCoverPoint(cover);
if (coverPosition != Vector3.zero)
{
nearbyCoverPositions.Add(coverPosition);
}
}
}
private Vector3 FindCoverPoint(Collider coverCollider)
{
Vector3[] potentialPoints = new Vector3[]
{
coverCollider.transform.position + coverCollider.transform.forward * 2f,
coverCollider.transform.position - coverCollider.transform.forward * 2f,
coverCollider.transform.position + coverCollider.transform.right * 2f,
coverCollider.transform.position - coverCollider.transform.right * 2f
};
foreach (Vector3 point in potentialPoints)
{
NavMeshHit hit;
if (NavMesh.SamplePosition(point, out hit, 2f, NavMesh.AllAreas))
{
if (!Physics.Linecast(transform.position, hit.position,
visionBlockingLayers))
{
return hit.position;
}
}
}
return Vector3.zero;
}
private Vector3 FindBestCoverPosition(Vector3 threatPosition)
{
Vector3 bestCover = Vector3.zero;
float bestScore = -Mathf.Infinity;
foreach (Vector3 coverPosition in nearbyCoverPositions)
{
float score = EvaluateCoverPosition(coverPosition, threatPosition);
if (score > bestScore)
{
bestScore = score;
bestCover = coverPosition;
}
}
return bestCover;
}
private float EvaluateCoverPosition(Vector3 coverPosition, Vector3 threatPosition)
{
float score = 0f;
float distanceToThreat = Vector3.Distance(coverPosition, threatPosition);
score += Mathf.Clamp(10f - distanceToThreat, 0f, 10f);
float distanceToCurrent = Vector3.Distance(transform.position, coverPosition);
score -= distanceToCurrent * 0.2f;
Vector3 coverToThreat = threatPosition - coverPosition;
RaycastHit hit;
if (Physics.Raycast(coverPosition, coverToThreat.normalized,
out hit, distanceToThreat, coverLayerMask))
{
score += 15f;
}
return score;
}
public void SetMovementSpeed(bool inCombat)
{
agent.speed = inCombat ? combatSpeed : normalSpeed;
}
private void Update()
{
if (animator != null)
{
float speed = agent.velocity.magnitude / agent.speed;
animator.SetFloat("Speed", speed);
if (agent.hasPath && agent.remainingDistance > agent.stoppingDistance)
{
Vector3 direction = agent.steeringTarget - transform.position;
direction.y = 0;
if (direction.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
}
}
}
}
}
战斗模块:处理射击逻辑、瞄准精度、弹药管理和攻击节奏控制。这个模块需要与动画系统和武器系统紧密配合。
8.2 TPS游戏场景构建与环境设置
8.2.1 游戏战场环境搭建
创建适合TPS游戏的场景需要考虑多个因素:视线遮挡物分布、移动路径复杂度、战术位置多样性以及性能优化。在Unity中构建TPS场景时,建议采用模块化设计方法,使用预制体组合来创建多样化的环境。
首先需要设置合适的导航网格(NavMesh)。导航网格是AI角色移动的基础,定义了可行走区域和障碍物位置。在Unity 2021.3.8f1c1中,使用NavMesh Components包来创建和管理导航网格:
using UnityEngine;
using UnityEngine.AI;
public class NavigationBaker : MonoBehaviour
{
[Header("导航区域设置")]
[SerializeField] private NavMeshSurface navMeshSurface;
[SerializeField] private LayerMask includeLayers;
[Header("导航网格参数")]
[SerializeField] private float agentRadius = 0.5f;
[SerializeField] private float agentHeight = 2f;
[SerializeField] private float maxSlope = 45f;
[SerializeField] private float stepHeight = 0.3f;
private void Start()
{
BakeNavigationMesh();
}
public void BakeNavigationMesh()
{
if (navMeshSurface == null)
{
navMeshSurface = GetComponent<NavMeshSurface>();
}
navMeshSurface.BuildNavMesh();
}
private void OnValidate()
{
if (navMeshSurface != null)
{
navMeshSurface.agentTypeID = GetAgentTypeSettings();
}
}
private int GetAgentTypeSettings()
{
NavMeshBuildSettings settings = new NavMeshBuildSettings();
settings.agentRadius = agentRadius;
settings.agentHeight = agentHeight;
settings.agentSlope = maxSlope;
settings.agentClimb = stepHeight;
return NavMesh.GetSettingsByIndex(0).agentTypeID;
}
}
场景中的碰撞体设置对AI行为有重要影响。静态障碍物应使用Mesh Collider或Box Collider,并标记为Navigation Static。动态障碍物需要专门处理,确保AI能够识别和回避。
8.2.2 战术掩体系统配置
掩体系统是TPS游戏AI的核心组成部分。一个良好的掩体系统应该提供:
- 掩体点自动生成
- 掩体质量评估
- 掩体间移动路径
- 掩体射击位置计算
using UnityEngine;
using System.Collections.Generic;
public class CoverSystem : MonoBehaviour
{
[System.Serializable]
public class CoverPoint
{
public Vector3 position;
public Vector3 normal;
public CoverType type;
public float safetyRating;
public bool isOccupied;
public float lastOccupiedTime;
}
public enum CoverType
{
LowWall,
HighWall,
Corner,
Window
}
[Header("掩体生成参数")]
[SerializeField] private float coverGenerationDistance = 20f;
[SerializeField] private LayerMask coverDetectionLayers;
private List<CoverPoint> allCoverPoints = new List<CoverPoint>();
private Dictionary<Collider, List<CoverPoint>> coverDictionary =
new Dictionary<Collider, List<CoverPoint>>();
private void Start()
{
GenerateCoverPoints();
}
private void GenerateCoverPoints()
{
Collider[] potentialCovers = Physics.OverlapSphere(
transform.position, coverGenerationDistance, coverDetectionLayers);
foreach (Collider coverCollider in potentialCovers)
{
if (IsValidCover(coverCollider))
{
AnalyzeCoverObject(coverCollider);
}
}
}
private bool IsValidCover(Collider coverCollider)
{
if (coverCollider.isTrigger)
{
return false;
}
if (coverCollider.gameObject.CompareTag("Cover"))
{
return true;
}
MeshRenderer renderer = coverCollider.GetComponent<MeshRenderer>();
if (renderer != null && renderer.bounds.size.y > 0.5f)
{
return true;
}
return false;
}
private void AnalyzeCoverObject(Collider coverCollider)
{
Bounds bounds = coverCollider.bounds;
Vector3[] testDirections = new Vector3[]
{
Vector3.forward,
Vector3.back,
Vector3.right,
Vector3.left
};
List<CoverPoint> pointsForThisCover = new List<CoverPoint>();
foreach (Vector3 direction in testDirections)
{
Vector3 testPosition = bounds.center + direction * bounds.extents.magnitude;
Ray ray = new Ray(testPosition + Vector3.up * 2f, Vector3.down);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 3f, coverDetectionLayers))
{
if (hit.collider == coverCollider)
{
CoverPoint newPoint = new CoverPoint();
newPoint.position = hit.point;
newPoint.normal = -direction;
newPoint.type = DetermineCoverType(coverCollider);
newPoint.safetyRating = EvaluateCoverSafety(hit.point, -direction);
newPoint.isOccupied = false;
newPoint.lastOccupiedTime = -Mathf.Infinity;
pointsForThisCover.Add(newPoint);
allCoverPoints.Add(newPoint);
}
}
}
if (pointsForThisCover.Count > 0)
{
coverDictionary[coverCollider] = pointsForThisCover;
}
}
private CoverType DetermineCoverType(Collider coverCollider)
{
Bounds bounds = coverCollider.bounds;
if (bounds.size.y < 1.2f)
{
return CoverType.LowWall;
}
else if (bounds.size.y > 2f)
{
return CoverType.HighWall;
}
else
{
return Random.value > 0.5f ? CoverType.Corner : CoverType.Window;
}
}
private float EvaluateCoverSafety(Vector3 position, Vector3 coverNormal)
{
float safetyScore = 0f;
RaycastHit hit;
Vector3[] threatDirections = new Vector3[]
{
Vector3.forward,
Vector3.back,
Vector3.right,
Vector3.left
};
foreach (Vector3 direction in threatDirections)
{
if (Vector3.Dot(direction, coverNormal) < 0.3f)
{
if (Physics.Raycast(position + Vector3.up, direction,
out hit, 10f, coverDetectionLayers))
{
safetyScore += 2f;
}
else
{
safetyScore -= 1f;
}
}
}
return safetyScore;
}
public CoverPoint GetBestCoverPoint(Vector3 threatPosition,
Vector3 requesterPosition)
{
CoverPoint bestPoint = null;
float bestScore = -Mathf.Infinity;
foreach (CoverPoint point in allCoverPoints)
{
if (point.isOccupied &&
Time.time - point.lastOccupiedTime < 5f)
{
continue;
}
float score = CalculateCoverScore(point,
threatPosition, requesterPosition);
if (score > bestScore)
{
bestScore = score;
bestPoint = point;
}
}
if (bestPoint != null)
{
bestPoint.isOccupied = true;
bestPoint.lastOccupiedTime = Time.time;
}
return bestPoint;
}
private float CalculateCoverScore(CoverPoint point,
Vector3 threatPosition, Vector3 requesterPosition)
{
float score = point.safetyRating;
float distanceToThreat = Vector3.Distance(point.position, threatPosition);
score += Mathf.Clamp(15f - distanceToThreat, 0f, 15f);
float distanceToRequester = Vector3.Distance(
point.position, requesterPosition);
score -= distanceToRequester * 0.3f;
Vector3 toThreat = threatPosition - point.position;
if (Vector3.Dot(point.normal, toThreat.normalized) < -0.7f)
{
score += 10f;
}
return score;
}
public void ReleaseCoverPoint(CoverPoint point)
{
if (point != null)
{
point.isOccupied = false;
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
foreach (CoverPoint point in allCoverPoints)
{
if (!point.isOccupied)
{
Gizmos.DrawSphere(point.position, 0.2f);
Gizmos.DrawLine(point.position,
point.position + point.normal);
}
else
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(point.position, 0.25f);
Gizmos.color = Color.green;
}
}
}
}
8.3 武器与弹道系统实现
8.3.1 子弹物理与命中检测
在TPS游戏中,子弹的物理模拟和命中检测需要平衡真实性和游戏性。常见的实现方式包括射线检测法和物理投射法。
using UnityEngine;
using System.Collections.Generic;
public class Projectile : MonoBehaviour
{
[System.Serializable]
public class DamageProfile
{
public float baseDamage = 25f;
public float headshotMultiplier = 2.5f;
public float limbMultiplier = 0.7f;
public float armorPenetration = 0.5f;
}
[Header("弹道参数")]
[SerializeField] private float initialSpeed = 100f;
[SerializeField] private float maxDistance = 100f;
[SerializeField] private float gravityScale = 0.5f;
[SerializeField] private LayerMask hitLayers;
[Header("伤害配置")]
[SerializeField] private DamageProfile damageProfile;
[Header("视觉效果")]
[SerializeField] private GameObject impactEffect;
[SerializeField] private GameObject trailEffect;
private Vector3 initialPosition;
private Vector3 velocity;
private bool isActive = true;
private float traveledDistance = 0f;
private TrailRenderer trailRenderer;
private GameObject trailInstance;
private void Start()
{
initialPosition = transform.position;
velocity = transform.forward * initialSpeed;
if (trailEffect != null)
{
trailInstance = Instantiate(trailEffect, transform.position,
Quaternion.identity, transform);
trailRenderer = trailInstance.GetComponent<TrailRenderer>();
}
}
private void Update()
{
if (!isActive)
{
return;
}
float deltaTime = Time.deltaTime;
Vector3 newPosition = transform.position + velocity * deltaTime;
velocity += Physics.gravity * gravityScale * deltaTime;
traveledDistance = Vector3.Distance(initialPosition, newPosition);
if (traveledDistance >= maxDistance)
{
DestroyProjectile();
return;
}
CheckCollision(newPosition);
if (isActive)
{
transform.position = newPosition;
transform.rotation = Quaternion.LookRotation(velocity.normalized);
}
}
private void CheckCollision(Vector3 newPosition)
{
Vector3 direction = newPosition - transform.position;
float distance = direction.magnitude;
if (distance > 0)
{
RaycastHit[] hits = Physics.RaycastAll(transform.position,
direction.normalized, distance, hitLayers);
System.Array.Sort(hits, (a, b) =>
a.distance.CompareTo(b.distance));
foreach (RaycastHit hit in hits)
{
if (hit.collider.isTrigger)
{
continue;
}
if (ProcessHit(hit))
{
HandleImpact(hit);
break;
}
}
}
}
private bool ProcessHit(RaycastHit hit)
{
Damageable damageable = hit.collider.GetComponent<Damageable>();
if (damageable != null)
{
float damage = CalculateDamage(hit);
damageable.TakeDamage(damage, hit.point, hit.normal);
return true;
}
return true;
}
private float CalculateDamage(RaycastHit hit)
{
float damage = damageProfile.baseDamage;
string hitArea = DetermineHitArea(hit.collider);
switch (hitArea)
{
case "Head":
damage *= damageProfile.headshotMultiplier;
break;
case "Limb":
damage *= damageProfile.limbMultiplier;
break;
}
Armor armor = hit.collider.GetComponent<Armor>();
if (armor != null)
{
damage *= (1f - armor.damageReduction *
(1f - damageProfile.armorPenetration));
}
return Mathf.Max(damage, 0f);
}
private string DetermineHitArea(Collider collider)
{
if (collider.CompareTag("Head"))
{
return "Head";
}
else if (collider.CompareTag("Limb"))
{
return "Limb";
}
return "Body";
}
private void HandleImpact(RaycastHit hit)
{
if (impactEffect != null)
{
GameObject impact = Instantiate(impactEffect, hit.point,
Quaternion.LookRotation(hit.normal));
Destroy(impact, 2f);
}
DestroyProjectile();
}
private void DestroyProjectile()
{
isActive = false;
if (trailRenderer != null)
{
trailRenderer.transform.parent = null;
trailRenderer.autodestruct = true;
}
Destroy(gameObject);
}
public void SetInitialParameters(Vector3 direction, float speedMultiplier)
{
velocity = direction.normalized * initialSpeed * speedMultiplier;
}
}
8.3.2 武器控制系统开发
武器系统需要管理射击逻辑、弹药系统、后坐力控制和动画同步。
using UnityEngine;
using System.Collections;
public class WeaponController : MonoBehaviour
{
[System.Serializable]
public class WeaponStats
{
public float fireRate = 0.15f;
public int magazineSize = 30;
public float reloadTime = 2f;
public float damage = 25f;
public float range = 100f;
public float spread = 0.05f;
public int pelletsPerShot = 1;
}
[System.Serializable]
public class RecoilProfile
{
public Vector3 positionalRecoil = new Vector3(0.02f, 0.05f, -0.1f);
public Vector3 rotationalRecoil = new Vector3(2f, 0.5f, 0.5f);
public float recoilRecoverySpeed = 5f;
public float snappiness = 8f;
public float returnSpeed = 15f;
}
[Header("武器属性")]
[SerializeField] private WeaponStats weaponStats;
[SerializeField] private RecoilProfile recoilProfile;
[Header("引用组件")]
[SerializeField] private Transform muzzleTransform;
[SerializeField] private GameObject projectilePrefab;
[SerializeField] private Animator weaponAnimator;
[SerializeField] private ParticleSystem muzzleFlash;
[Header("音频组件")]
[SerializeField] private AudioSource fireAudioSource;
[SerializeField] private AudioClip fireSound;
[SerializeField] private AudioClip reloadSound;
[SerializeField] private AudioClip emptySound;
private int currentAmmo;
private bool isReloading = false;
private bool isFiring = false;
private float nextFireTime = 0f;
private Vector3 currentPositionRecoil;
private Vector3 currentRotationRecoil;
private Vector3 rotationalRecoilVelocity;
private Transform weaponTransform;
private void Awake()
{
weaponTransform = transform;
currentAmmo = weaponStats.magazineSize;
}
private void Update()
{
UpdateRecoil();
if (!isReloading && currentAmmo <= 0)
{
StartCoroutine(Reload());
}
}
public bool AttemptFire(Vector3 targetPosition)
{
if (Time.time < nextFireTime || isReloading || currentAmmo <= 0)
{
if (currentAmmo <= 0 && !isReloading)
{
PlayEmptySound();
}
return false;
}
FireWeapon(targetPosition);
return true;
}
private void FireWeapon(Vector3 targetPosition)
{
currentAmmo--;
nextFireTime = Time.time + weaponStats.fireRate;
Vector3 fireDirection = CalculateFireDirection(targetPosition);
for (int i = 0; i < weaponStats.pelletsPerShot; i++)
{
Vector3 spreadDirection = ApplySpread(fireDirection);
FireProjectile(spreadDirection);
}
ApplyRecoil();
PlayFireEffects();
if (weaponAnimator != null)
{
weaponAnimator.SetTrigger("Fire");
}
}
private Vector3 CalculateFireDirection(Vector3 targetPosition)
{
Vector3 direction = (targetPosition - muzzleTransform.position).normalized;
return direction;
}
private Vector3 ApplySpread(Vector3 direction)
{
if (weaponStats.spread <= 0)
{
return direction;
}
float spread = weaponStats.spread;
Vector3 spreadVector = new Vector3(
Random.Range(-spread, spread),
Random.Range(-spread, spread),
Random.Range(-spread, spread)
);
return (direction + spreadVector).normalized;
}
private void FireProjectile(Vector3 direction)
{
if (projectilePrefab == null)
{
Debug.LogError("Projectile prefab not assigned!");
return;
}
GameObject projectile = Instantiate(projectilePrefab,
muzzleTransform.position,
Quaternion.LookRotation(direction));
Projectile projComponent = projectile.GetComponent<Projectile>();
if (projComponent != null)
{
float speedMultiplier = 1f + Random.Range(-0.1f, 0.1f);
projComponent.SetInitialParameters(direction, speedMultiplier);
}
}
private void ApplyRecoil()
{
currentPositionRecoil += new Vector3(
Random.Range(-recoilProfile.positionalRecoil.x,
recoilProfile.positionalRecoil.x),
Random.Range(-recoilProfile.positionalRecoil.y,
recoilProfile.positionalRecoil.y),
recoilProfile.positionalRecoil.z
);
currentRotationRecoil += new Vector3(
-recoilProfile.rotationalRecoil.x,
Random.Range(-recoilProfile.rotationalRecoil.y,
recoilProfile.rotationalRecoil.y),
Random.Range(-recoilProfile.rotationalRecoil.z,
recoilProfile.rotationalRecoil.z)
);
}
private void UpdateRecoil()
{
currentPositionRecoil = Vector3.Lerp(
currentPositionRecoil, Vector3.zero,
recoilProfile.recoilRecoverySpeed * Time.deltaTime);
currentRotationRecoil = Vector3.Lerp(
currentRotationRecoil, Vector3.zero,
recoilProfile.recoilRecoverySpeed * Time.deltaTime);
rotationalRecoilVelocity = Vector3.Lerp(
rotationalRecoilVelocity, currentRotationRecoil,
recoilProfile.snappiness * Time.deltaTime);
weaponTransform.localPosition = currentPositionRecoil;
weaponTransform.localRotation = Quaternion.Euler(rotationalRecoilVelocity);
}
private void PlayFireEffects()
{
if (muzzleFlash != null)
{
muzzleFlash.Play();
}
if (fireAudioSource != null && fireSound != null)
{
fireAudioSource.pitch = Random.Range(0.95f, 1.05f);
fireAudioSource.PlayOneShot(fireSound);
}
}
private void PlayEmptySound()
{
if (fireAudioSource != null && emptySound != null)
{
fireAudioSource.PlayOneShot(emptySound);
}
}
public IEnumerator Reload()
{
if (isReloading || currentAmmo == weaponStats.magazineSize)
{
yield break;
}
isReloading = true;
if (weaponAnimator != null)
{
weaponAnimator.SetTrigger("Reload");
}
if (fireAudioSource != null && reloadSound != null)
{
fireAudioSource.PlayOneShot(reloadSound);
}
yield return new WaitForSeconds(weaponStats.reloadTime);
currentAmmo = weaponStats.magazineSize;
isReloading = false;
}
public int GetCurrentAmmo()
{
return currentAmmo;
}
public bool IsReloading()
{
return isReloading;
}
}
8.4 玩家角色控制系统实现
8.4.1 角色移动与相机控制
TPS游戏的玩家控制需要同时处理角色移动和相机控制,确保操作流畅且视角舒适。
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[Header("移动参数")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float sprintSpeed = 8f;
[SerializeField] private float rotationSpeed = 10f;
[SerializeField] private float jumpForce = 5f;
[SerializeField] private float groundCheckDistance = 0.2f;
[Header("相机参数")]
[SerializeField] private Transform cameraTransform;
[SerializeField] private float cameraDistance = 5f;
[SerializeField] private float cameraHeight = 2f;
[SerializeField] private float cameraSensitivity = 2f;
[SerializeField] private float cameraMinAngle = -30f;
[SerializeField] private float cameraMaxAngle = 70f;
[Header("物理参数")]
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float gravityMultiplier = 2f;
private CharacterController characterController;
private Animator animator;
private Vector3 moveDirection;
private float verticalVelocity;
private bool isGrounded;
private bool isSprinting;
private float cameraPitch = 0f;
private float cameraYaw = 0f;
private Vector3 cameraOffset;
private float currentCameraDistance;
private void Awake()
{
characterController = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
currentCameraDistance = cameraDistance;
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
HandleGroundCheck();
HandleMovement();
HandleJump();
HandleCamera();
UpdateAnimator();
HandleWeaponInput();
}
private void HandleGroundCheck()
{
isGrounded = characterController.isGrounded ||
Physics.Raycast(transform.position + Vector3.up * 0.1f,
Vector3.down, groundCheckDistance + 0.1f, groundLayer);
if (isGrounded && verticalVelocity < 0)
{
verticalVelocity = -2f;
}
}
private void HandleMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
isSprinting = Input.GetKey(KeyCode.LeftShift) && vertical > 0;
float currentSpeed = isSprinting ? sprintSpeed : moveSpeed;
Vector3 forward = Vector3.ProjectOnPlane(
cameraTransform.forward, Vector3.up).normalized;
Vector3 right = Vector3.ProjectOnPlane(
cameraTransform.right, Vector3.up).normalized;
Vector3 moveInput = (forward * vertical + right * horizontal).normalized;
if (moveInput.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveInput);
transform.rotation = Quaternion.Slerp(
transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
}
moveDirection = moveInput * currentSpeed;
verticalVelocity += Physics.gravity.y *
gravityMultiplier * Time.deltaTime;
Vector3 finalMove = moveDirection + Vector3.up * verticalVelocity;
characterController.Move(finalMove * Time.deltaTime);
}
private void HandleJump()
{
if (Input.GetButtonDown("Jump") && isGrounded)
{
verticalVelocity = Mathf.Sqrt(jumpForce * -2f *
Physics.gravity.y * gravityMultiplier);
if (animator != null)
{
animator.SetTrigger("Jump");
}
}
}
private void HandleCamera()
{
float mouseX = Input.GetAxis("Mouse X") * cameraSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * cameraSensitivity;
cameraYaw += mouseX;
cameraPitch -= mouseY;
cameraPitch = Mathf.Clamp(cameraPitch, cameraMinAngle, cameraMaxAngle);
Quaternion cameraRotation = Quaternion.Euler(cameraPitch, cameraYaw, 0);
Vector3 targetPosition = transform.position +
Vector3.up * cameraHeight -
cameraRotation * Vector3.forward * currentCameraDistance;
RaycastHit hit;
if (Physics.Linecast(transform.position + Vector3.up * cameraHeight,
targetPosition, out hit))
{
currentCameraDistance = Mathf.Min(
cameraDistance, hit.distance * 0.9f);
}
else
{
currentCameraDistance = cameraDistance;
}
cameraTransform.position = transform.position +
Vector3.up * cameraHeight -
cameraRotation * Vector3.forward * currentCameraDistance;
cameraTransform.rotation = cameraRotation;
}
private void UpdateAnimator()
{
if (animator == null)
{
return;
}
float speed = moveDirection.magnitude / moveSpeed;
animator.SetFloat("Speed", speed);
animator.SetBool("IsSprinting", isSprinting);
animator.SetBool("IsGrounded", isGrounded);
animator.SetFloat("VerticalVelocity", verticalVelocity);
}
private void HandleWeaponInput()
{
if (Input.GetButtonDown("Fire1"))
{
animator.SetBool("IsAiming", true);
}
if (Input.GetButtonUp("Fire1"))
{
animator.SetBool("IsAiming", false);
}
if (Input.GetKeyDown(KeyCode.R))
{
animator.SetTrigger("Reload");
}
}
public Transform GetCameraTransform()
{
return cameraTransform;
}
public bool IsAiming()
{
return animator != null &&
animator.GetBool("IsAiming");
}
}
8.5 智能敌人行为系统构建
8.5.1 行为树可视化设计与实现
使用行为树控制AI决策时,可视化设计工具能极大提高开发效率。以下是基于Unity的可视化行为树编辑器核心组件:
using UnityEngine;
using System.Collections.Generic;
public class BehaviorTreeNode : MonoBehaviour
{
public enum NodeType
{
Selector,
Sequence,
Parallel,
Condition,
Action,
Decorator
}
public enum NodeStatus
{
Success,
Failure,
Running
}
[Header("节点配置")]
public NodeType nodeType;
public string nodeName;
public int priority = 0;
[Header("子节点引用")]
public List<BehaviorTreeNode> children = new List<BehaviorTreeNode>();
protected BehaviorTreeExecutor executor;
protected AIController aiController;
private NodeStatus currentStatus = NodeStatus.Failure;
private int currentChildIndex = 0;
public virtual void Initialize(BehaviorTreeExecutor executor,
AIController controller)
{
this.executor = executor;
this.aiController = controller;
foreach (BehaviorTreeNode child in children)
{
if (child != null)
{
child.Initialize(executor, controller);
}
}
}
public NodeStatus Execute()
{
switch (nodeType)
{
case NodeType.Selector:
return ExecuteSelector();
case NodeType.Sequence:
return ExecuteSequence();
case NodeType.Parallel:
return ExecuteParallel();
case NodeType.Condition:
return ExecuteCondition();
case NodeType.Action:
return ExecuteAction();
case NodeType.Decorator:
return ExecuteDecorator();
default:
return NodeStatus.Failure;
}
}
protected virtual NodeStatus ExecuteSelector()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
NodeStatus childStatus = children[i].Execute();
if (childStatus == NodeStatus.Running)
{
currentChildIndex = i;
currentStatus = NodeStatus.Running;
return currentStatus;
}
else if (childStatus == NodeStatus.Success)
{
ResetChildren();
currentStatus = NodeStatus.Success;
return currentStatus;
}
}
ResetChildren();
currentStatus = NodeStatus.Failure;
return currentStatus;
}
protected virtual NodeStatus ExecuteSequence()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
NodeStatus childStatus = children[i].Execute();
if (childStatus == NodeStatus.Running)
{
currentChildIndex = i;
currentStatus = NodeStatus.Running;
return currentStatus;
}
else if (childStatus == NodeStatus.Failure)
{
ResetChildren();
currentStatus = NodeStatus.Failure;
return currentStatus;
}
}
ResetChildren();
currentStatus = NodeStatus.Success;
return currentStatus;
}
protected virtual NodeStatus ExecuteParallel()
{
int successCount = 0;
int failureCount = 0;
foreach (BehaviorTreeNode child in children)
{
NodeStatus childStatus = child.Execute();
if (childStatus == NodeStatus.Success)
{
successCount++;
}
else if (childStatus == NodeStatus.Failure)
{
failureCount++;
}
}
if (successCount >= children.Count)
{
currentStatus = NodeStatus.Success;
}
else if (failureCount >= children.Count)
{
currentStatus = NodeStatus.Failure;
}
else
{
currentStatus = NodeStatus.Running;
}
return currentStatus;
}
protected virtual NodeStatus ExecuteCondition()
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
protected virtual NodeStatus ExecuteAction()
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
protected virtual NodeStatus ExecuteDecorator()
{
if (children.Count > 0)
{
currentStatus = children[0].Execute();
}
return currentStatus;
}
public virtual void OnEnter()
{
currentChildIndex = 0;
}
public virtual void OnExit()
{
ResetChildren();
}
protected void ResetChildren()
{
currentChildIndex = 0;
foreach (BehaviorTreeNode child in children)
{
if (child != null)
{
child.OnExit();
}
}
}
public NodeStatus GetCurrentStatus()
{
return currentStatus;
}
public void Abort()
{
OnExit();
currentStatus = NodeStatus.Failure;
}
}
public class BehaviorTreeExecutor : MonoBehaviour
{
[Header("行为树配置")]
[SerializeField] private BehaviorTreeNode rootNode;
[SerializeField] private float updateInterval = 0.1f;
private AIController aiController;
private float lastUpdateTime = 0f;
private void Awake()
{
aiController = GetComponent<AIController>();
}
private void Start()
{
if (rootNode != null)
{
rootNode.Initialize(this, aiController);
}
}
private void Update()
{
if (Time.time - lastUpdateTime >= updateInterval)
{
ExecuteBehaviorTree();
lastUpdateTime = Time.time;
}
}
private void ExecuteBehaviorTree()
{
if (rootNode != null)
{
rootNode.Execute();
}
}
public void SetRootNode(BehaviorTreeNode node)
{
if (rootNode != null)
{
rootNode.Abort();
}
rootNode = node;
if (rootNode != null)
{
rootNode.Initialize(this, aiController);
}
}
public AIController GetAIController()
{
return aiController;
}
}
8.5.2 具体行为节点实现
以下是TPS游戏中常用的具体行为节点实现:
using UnityEngine;
// 条件节点:检查是否看到玩家
public class CanSeePlayerNode : BehaviorTreeNode
{
[Header("视觉参数")]
[SerializeField] private float visionRange = 20f;
[SerializeField] private float fieldOfView = 90f;
protected override NodeStatus ExecuteCondition()
{
if (aiController == null)
{
return NodeStatus.Failure;
}
bool canSeePlayer = aiController.CanSeePlayer(visionRange, fieldOfView);
currentStatus = canSeePlayer ? NodeStatus.Success : NodeStatus.Failure;
return currentStatus;
}
}
// 条件节点:检查玩家是否在攻击范围内
public class InAttackRangeNode : BehaviorTreeNode
{
[SerializeField] private float attackRange = 15f;
protected override NodeStatus ExecuteCondition()
{
if (aiController == null)
{
return NodeStatus.Failure;
}
float distanceToPlayer = aiController.GetDistanceToPlayer();
currentStatus = (distanceToPlayer <= attackRange) ?
NodeStatus.Success : NodeStatus.Failure;
return currentStatus;
}
}
// 条件节点:检查是否需要寻找掩体
public class NeedsCoverNode : BehaviorTreeNode
{
[SerializeField] private float healthThreshold = 0.5f;
[SerializeField] private float coverCooldown = 10f;
private float lastCoverTime = -Mathf.Infinity;
protected override NodeStatus ExecuteCondition()
{
if (aiController == null)
{
return NodeStatus.Failure;
}
if (Time.time - lastCoverTime < coverCooldown)
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
bool needsCover = aiController.GetHealthPercentage() < healthThreshold ||
aiController.IsUnderHeavyFire();
currentStatus = needsCover ? NodeStatus.Success : NodeStatus.Failure;
if (currentStatus == NodeStatus.Success)
{
lastCoverTime = Time.time;
}
return currentStatus;
}
}
// 动作节点:移动到玩家位置
public class MoveToPlayerNode : BehaviorTreeNode
{
[Header("移动参数")]
[SerializeField] private float stoppingDistance = 2f;
private bool isMoving = false;
public override void OnEnter()
{
base.OnEnter();
isMoving = false;
}
protected override NodeStatus ExecuteAction()
{
if (aiController == null)
{
return NodeStatus.Failure;
}
Vector3 playerPosition = aiController.GetPlayerPosition();
if (playerPosition == Vector3.zero)
{
return NodeStatus.Failure;
}
float distanceToPlayer = Vector3.Distance(
aiController.transform.position, playerPosition);
if (distanceToPlayer <= stoppingDistance)
{
aiController.StopMovement();
currentStatus = NodeStatus.Success;
return currentStatus;
}
if (!isMoving)
{
aiController.MoveToPosition(playerPosition);
isMoving = true;
}
if (aiController.IsPathComplete())
{
currentStatus = NodeStatus.Success;
}
else
{
currentStatus = NodeStatus.Running;
}
return currentStatus;
}
public override void OnExit()
{
base.OnExit();
aiController.StopMovement();
isMoving = false;
}
}
// 动作节点:寻找并使用掩体
public class TakeCoverNode : BehaviorTreeNode
{
[Header("掩体参数")]
[SerializeField] private float minCoverTime = 3f;
[SerializeField] private float maxCoverTime = 8f;
private CoverSystem.CoverPoint currentCover;
private float coverEndTime;
private bool inCover = false;
public override void OnEnter()
{
base.OnEnter();
inCover = false;
if (aiController != null)
{
Vector3 threatPosition = aiController.GetPlayerPosition();
Vector3 aiPosition = aiController.transform.position;
CoverSystem coverSystem = FindObjectOfType<CoverSystem>();
if (coverSystem != null)
{
currentCover = coverSystem.GetBestCoverPoint(
threatPosition, aiPosition);
if (currentCover != null)
{
aiController.MoveToPosition(currentCover.position);
coverEndTime = Time.time +
Random.Range(minCoverTime, maxCoverTime);
}
}
}
}
protected override NodeStatus ExecuteAction()
{
if (currentCover == null)
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
if (!inCover)
{
float distanceToCover = Vector3.Distance(
aiController.transform.position, currentCover.position);
if (distanceToCover < 1f)
{
aiController.StopMovement();
aiController.LookAtDirection(currentCover.normal);
inCover = true;
}
else if (aiController.IsPathComplete())
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
currentStatus = NodeStatus.Running;
}
else
{
if (Time.time >= coverEndTime ||
!aiController.IsCoverEffective(currentCover.position))
{
currentStatus = NodeStatus.Success;
}
else
{
currentStatus = NodeStatus.Running;
}
}
return currentStatus;
}
public override void OnExit()
{
base.OnExit();
CoverSystem coverSystem = FindObjectOfType<CoverSystem>();
if (coverSystem != null && currentCover != null)
{
coverSystem.ReleaseCoverPoint(currentCover);
}
aiController.StopMovement();
inCover = false;
}
}
// 动作节点:攻击玩家
public class AttackPlayerNode : BehaviorTreeNode
{
[Header("攻击参数")]
[SerializeField] private float attackCooldown = 1f;
[SerializeField] private float aimTime = 0.5f;
private float lastAttackTime = -Mathf.Infinity;
private float aimStartTime;
private bool isAiming = false;
public override void OnEnter()
{
base.OnEnter();
isAiming = false;
}
protected override NodeStatus ExecuteAction()
{
if (aiController == null)
{
return NodeStatus.Failure;
}
if (Time.time - lastAttackTime < attackCooldown)
{
currentStatus = NodeStatus.Running;
return currentStatus;
}
Vector3 playerPosition = aiController.GetPlayerPosition();
if (!isAiming)
{
aiController.LookAtPosition(playerPosition);
aimStartTime = Time.time;
isAiming = true;
}
if (Time.time - aimStartTime >= aimTime)
{
bool attackSuccess = aiController.AttackPlayer(playerPosition);
if (attackSuccess)
{
lastAttackTime = Time.time;
isAiming = false;
currentStatus = NodeStatus.Success;
}
else
{
currentStatus = NodeStatus.Failure;
}
}
else
{
currentStatus = NodeStatus.Running;
}
return currentStatus;
}
public override void OnExit()
{
base.OnExit();
isAiming = false;
}
}
8.5.3 敌人AI主控制器
这是整合所有AI系统的核心控制器:
using UnityEngine;
using UnityEngine.AI;
public class AIController : MonoBehaviour
{
[Header("组件引用")]
[SerializeField] private NavMeshAgent navMeshAgent;
[SerializeField] private Animator animator;
[SerializeField] private WeaponController weaponController;
[SerializeField] private AIPerceptionSystem perceptionSystem;
[Header("AI参数")]
[SerializeField] private float maxHealth = 100f;
[SerializeField] private float aimAccuracy = 0.8f;
private float currentHealth;
private Transform playerTransform;
private CoverSystem coverSystem;
private bool isUnderFire = false;
private float lastHitTime = 0f;
private Vector3 lastKnownPlayerPosition;
private void Awake()
{
if (navMeshAgent == null)
{
navMeshAgent = GetComponent<NavMeshAgent>();
}
if (animator == null)
{
animator = GetComponent<Animator>();
}
if (perceptionSystem == null)
{
perceptionSystem = GetComponent<AIPerceptionSystem>();
}
coverSystem = FindObjectOfType<CoverSystem>();
}
private void Start()
{
currentHealth = maxHealth;
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
InitializeNavigation();
}
private void InitializeNavigation()
{
if (navMeshAgent != null)
{
navMeshAgent.acceleration = 8f;
navMeshAgent.angularSpeed = 360f;
navMeshAgent.stoppingDistance = 1f;
navMeshAgent.autoBraking = true;
}
}
public bool CanSeePlayer(float range, float fov)
{
if (perceptionSystem != null)
{
return perceptionSystem.CanSeePlayer();
}
if (playerTransform == null)
{
return false;
}
Vector3 directionToPlayer = playerTransform.position - transform.position;
float distance = directionToPlayer.magnitude;
if (distance > range)
{
return false;
}
float angle = Vector3.Angle(transform.forward, directionToPlayer);
if (angle > fov / 2f)
{
return false;
}
RaycastHit hit;
if (Physics.Raycast(transform.position + Vector3.up,
directionToPlayer.normalized, out hit, distance))
{
if (hit.transform.CompareTag("Player"))
{
lastKnownPlayerPosition = playerTransform.position;
return true;
}
}
return false;
}
public float GetDistanceToPlayer()
{
if (playerTransform == null)
{
return Mathf.Infinity;
}
return Vector3.Distance(transform.position, playerTransform.position);
}
public Vector3 GetPlayerPosition()
{
if (playerTransform != null)
{
return playerTransform.position;
}
return lastKnownPlayerPosition;
}
public void MoveToPosition(Vector3 position)
{
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
{
navMeshAgent.SetDestination(position);
if (animator != null)
{
animator.SetBool("IsMoving", true);
}
}
}
public void StopMovement()
{
if (navMeshAgent != null)
{
navMeshAgent.ResetPath();
if (animator != null)
{
animator.SetBool("IsMoving", false);
}
}
}
public bool IsPathComplete()
{
if (navMeshAgent == null)
{
return true;
}
return !navMeshAgent.pathPending &&
navMeshAgent.remainingDistance <= navMeshAgent.stoppingDistance &&
(!navMeshAgent.hasPath || navMeshAgent.velocity.sqrMagnitude == 0f);
}
public void LookAtPosition(Vector3 position)
{
Vector3 direction = position - transform.position;
direction.y = 0;
if (direction.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation,
targetRotation, Time.deltaTime * 5f);
}
}
public void LookAtDirection(Vector3 direction)
{
direction.y = 0;
if (direction.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation,
targetRotation, Time.deltaTime * 5f);
}
}
public bool AttackPlayer(Vector3 targetPosition)
{
if (weaponController == null)
{
return false;
}
Vector3 aimPosition = targetPosition;
if (aimAccuracy < 1f)
{
float inaccuracy = (1f - aimAccuracy) * 2f;
aimPosition += new Vector3(
Random.Range(-inaccuracy, inaccuracy),
Random.Range(-inaccuracy, inaccuracy),
Random.Range(-inaccuracy, inaccuracy)
);
}
bool attackSuccess = weaponController.AttemptFire(aimPosition);
if (attackSuccess && animator != null)
{
animator.SetTrigger("Attack");
}
return attackSuccess;
}
public float GetHealthPercentage()
{
return currentHealth / maxHealth;
}
public bool IsUnderHeavyFire()
{
return isUnderFire && Time.time - lastHitTime < 3f;
}
public bool IsCoverEffective(Vector3 coverPosition)
{
if (playerTransform == null)
{
return true;
}
Vector3 directionToPlayer = playerTransform.position - coverPosition;
RaycastHit hit;
if (Physics.Raycast(coverPosition + Vector3.up,
directionToPlayer.normalized, out hit))
{
if (hit.collider.CompareTag("Cover") ||
hit.collider.gameObject == gameObject)
{
return true;
}
}
return false;
}
public void TakeDamage(float damage, Vector3 hitPoint, Vector3 hitNormal)
{
currentHealth -= damage;
lastHitTime = Time.time;
isUnderFire = true;
if (currentHealth <= 0)
{
Die();
}
else if (animator != null)
{
animator.SetTrigger("Hit");
}
}
private void Die()
{
if (navMeshAgent != null)
{
navMeshAgent.isStopped = true;
}
if (animator != null)
{
animator.SetTrigger("Die");
}
Destroy(gameObject, 3f);
enabled = false;
if (navMeshAgent != null) navMeshAgent.enabled = false;
if (weaponController != null) weaponController.enabled = false;
}
private void Update()
{
if (Time.time - lastHitTime > 5f)
{
isUnderFire = false;
}
UpdateAnimator();
}
private void UpdateAnimator()
{
if (animator == null)
{
return;
}
if (navMeshAgent != null)
{
float speed = navMeshAgent.velocity.magnitude /
navMeshAgent.speed;
animator.SetFloat("Speed", speed);
}
animator.SetBool("IsAiming", weaponController != null);
}
}
8.6 游戏界面与反馈系统
8.6.1 用户界面设计实现
TPS游戏的用户界面需要提供清晰的游戏状态反馈,包括生命值、弹药、任务目标等信息。
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class GameUIManager : MonoBehaviour
{
[Header("玩家状态UI")]
[SerializeField] private Slider healthSlider;
[SerializeField] private Text healthText;
[SerializeField] private Image healthFillImage;
[SerializeField] private Gradient healthGradient;
[Header("武器状态UI")]
[SerializeField] private Text ammoText;
[SerializeField] private Text reserveAmmoText;
[SerializeField] private Image crosshairImage;
[SerializeField] private RectTransform crosshairRect;
[Header("敌人指示器")]
[SerializeField] private GameObject enemyIndicatorPrefab;
[SerializeField] private RectTransform indicatorContainer;
[Header("任务提示")]
[SerializeField] private Text missionText;
[SerializeField] private CanvasGroup missionCanvasGroup;
private PlayerController playerController;
private WeaponController currentWeapon;
private Camera mainCamera;
private float missionTextDuration = 0f;
private Coroutine missionTextCoroutine;
private void Awake()
{
mainCamera = Camera.main;
}
private void Start()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerController = player.GetComponent<PlayerController>();
}
if (healthSlider != null)
{
healthSlider.maxValue = 100f;
healthSlider.value = 100f;
}
UpdateHealthUI(100f);
StartCoroutine(UpdateEnemyIndicators());
}
private void Update()
{
UpdateWeaponUI();
UpdateCrosshair();
if (missionTextDuration > 0f)
{
missionTextDuration -= Time.deltaTime;
if (missionTextDuration <= 0f)
{
HideMissionText();
}
}
}
public void UpdateHealthUI(float currentHealth)
{
if (healthSlider != null)
{
healthSlider.value = currentHealth;
if (healthFillImage != null)
{
float normalizedHealth = currentHealth / healthSlider.maxValue;
healthFillImage.color = healthGradient.Evaluate(normalizedHealth);
}
}
if (healthText != null)
{
healthText.text = $"HP: {Mathf.CeilToInt(currentHealth)}";
}
}
private void UpdateWeaponUI()
{
if (currentWeapon == null)
{
FindCurrentWeapon();
}
if (currentWeapon != null)
{
if (ammoText != null)
{
ammoText.text = currentWeapon.GetCurrentAmmo().ToString();
}
if (reserveAmmoText != null)
{
reserveAmmoText.text = "∞";
}
}
}
private void FindCurrentWeapon()
{
if (playerController != null)
{
currentWeapon = playerController.GetComponentInChildren<WeaponController>();
}
}
private void UpdateCrosshair()
{
if (crosshairRect == null || playerController == null)
{
return;
}
if (playerController.IsAiming())
{
crosshairRect.localScale = Vector3.one * 0.5f;
if (currentWeapon != null && currentWeapon.IsReloading())
{
crosshairImage.color = Color.yellow;
}
else
{
crosshairImage.color = Color.white;
}
}
else
{
crosshairRect.localScale = Vector3.one * 1f;
crosshairImage.color = new Color(1f, 1f, 1f, 0.5f);
}
}
private IEnumerator UpdateEnemyIndicators()
{
while (true)
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
foreach (Transform child in indicatorContainer)
{
Destroy(child.gameObject);
}
foreach (GameObject enemy in enemies)
{
Vector3 screenPos = mainCamera.WorldToScreenPoint(enemy.transform.position);
if (screenPos.z > 0 &&
screenPos.x > 0 && screenPos.x < Screen.width &&
screenPos.y > 0 && screenPos.y < Screen.height)
{
GameObject indicator = Instantiate(
enemyIndicatorPrefab, indicatorContainer);
RectTransform rectTransform = indicator.GetComponent<RectTransform>();
rectTransform.anchoredPosition = new Vector2(
screenPos.x - Screen.width / 2f,
screenPos.y - Screen.height / 2f);
float distance = Vector3.Distance(
playerController.transform.position,
enemy.transform.position);
indicator.GetComponentInChildren<Text>().text =
$"{Mathf.RoundToInt(distance)}m";
}
}
yield return new WaitForSeconds(0.5f);
}
}
public void ShowMissionText(string text, float duration = 3f)
{
if (missionText != null)
{
missionText.text = text;
}
if (missionCanvasGroup != null)
{
missionCanvasGroup.alpha = 1f;
}
missionTextDuration = duration;
if (missionTextCoroutine != null)
{
StopCoroutine(missionTextCoroutine);
}
missionTextCoroutine = StartCoroutine(FadeMissionText());
}
private void HideMissionText()
{
if (missionCanvasGroup != null)
{
missionCanvasGroup.alpha = 0f;
}
}
private IEnumerator FadeMissionText()
{
yield return new WaitForSeconds(missionTextDuration - 1f);
float fadeDuration = 1f;
float elapsedTime = 0f;
while (elapsedTime < fadeDuration)
{
if (missionCanvasGroup != null)
{
missionCanvasGroup.alpha = 1f - (elapsedTime / fadeDuration);
}
elapsedTime += Time.deltaTime;
yield return null;
}
HideMissionText();
}
public void ShowDamageIndicator(Vector3 damageSource)
{
StartCoroutine(DamageIndicatorCoroutine(damageSource));
}
private IEnumerator DamageIndicatorCoroutine(Vector3 damageSource)
{
Vector3 direction = (damageSource - playerController.transform.position).normalized;
float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
GameObject indicator = new GameObject("DamageIndicator");
indicator.transform.SetParent(transform);
Image image = indicator.AddComponent<Image>();
image.color = new Color(1f, 0f, 0f, 0.5f);
RectTransform rectTransform = indicator.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(20f, 100f);
rectTransform.anchoredPosition = Vector2.zero;
rectTransform.localRotation = Quaternion.Euler(0, 0, -angle);
float duration = 1f;
float elapsedTime = 0f;
while (elapsedTime < duration)
{
float alpha = 0.5f * (1f - elapsedTime / duration);
image.color = new Color(1f, 0f, 0f, alpha);
elapsedTime += Time.deltaTime;
yield return null;
}
Destroy(indicator);
}
}
8.7 游戏系统整合与优化
8.7.1 游戏管理器实现
游戏管理器负责协调所有子系统,管理游戏状态和提供全局访问点。
using UnityEngine;
using System.Collections.Generic;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("游戏设置")]
[SerializeField] private int targetFrameRate = 60;
[SerializeField] private bool cursorLocked = true;
[Header("玩家引用")]
[SerializeField] private GameObject playerPrefab;
[SerializeField] private Transform playerSpawnPoint;
[Header("敌人设置")]
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private List<Transform> enemySpawnPoints;
[SerializeField] private int maxEnemies = 10;
[SerializeField] private float enemySpawnInterval = 5f;
private GameObject currentPlayer;
private List<GameObject> activeEnemies = new List<GameObject>();
private float nextEnemySpawnTime;
private int enemiesDefeated = 0;
private int totalEnemiesSpawned = 0;
private GameUIManager uiManager;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
Application.targetFrameRate = targetFrameRate;
if (cursorLocked)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
private void Start()
{
uiManager = FindObjectOfType<GameUIManager>();
SpawnPlayer();
InitializeEnemySpawning();
if (uiManager != null)
{
uiManager.ShowMissionText("清除所有敌人", 5f);
}
}
private void Update()
{
if (Time.time >= nextEnemySpawnTime &&
activeEnemies.Count < maxEnemies)
{
SpawnEnemy();
nextEnemySpawnTime = Time.time + enemySpawnInterval;
}
CheckGameCompletion();
}
private void SpawnPlayer()
{
if (playerPrefab != null && playerSpawnPoint != null)
{
currentPlayer = Instantiate(playerPrefab,
playerSpawnPoint.position,
playerSpawnPoint.rotation);
PlayerHealth playerHealth = currentPlayer.GetComponent<PlayerHealth>();
if (playerHealth != null)
{
playerHealth.OnHealthChanged.AddListener(OnPlayerHealthChanged);
playerHealth.OnDeath.AddListener(OnPlayerDeath);
}
}
}
private void InitializeEnemySpawning()
{
nextEnemySpawnTime = Time.time + 2f;
for (int i = 0; i < Mathf.Min(3, maxEnemies); i++)
{
SpawnEnemy();
}
}
private void SpawnEnemy()
{
if (enemyPrefab == null || enemySpawnPoints.Count == 0)
{
return;
}
Transform spawnPoint = enemySpawnPoints[Random.Range(0, enemySpawnPoints.Count)];
GameObject enemy = Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
AIController aiController = enemy.GetComponent<AIController>();
if (aiController != null)
{
aiController.OnDeath.AddListener(OnEnemyDefeated);
}
activeEnemies.Add(enemy);
totalEnemiesSpawned++;
if (uiManager != null)
{
uiManager.ShowMissionText($"敌人出现: {activeEnemies.Count} 活跃", 2f);
}
}
private void OnPlayerHealthChanged(float currentHealth, float maxHealth)
{
if (uiManager != null)
{
uiManager.UpdateHealthUI(currentHealth);
}
if (currentHealth < maxHealth * 0.3f)
{
if (uiManager != null)
{
uiManager.ShowMissionText("警告: 生命值过低", 2f);
}
}
}
private void OnPlayerDeath()
{
if (uiManager != null)
{
uiManager.ShowMissionText("任务失败", 10f);
}
Time.timeScale = 0.5f;
Invoke("ShowGameOver", 2f);
}
private void OnEnemyDefeated(GameObject enemy)
{
activeEnemies.Remove(enemy);
enemiesDefeated++;
if (uiManager != null)
{
uiManager.ShowMissionText(
$"敌人击败: {enemiesDefeated} / {totalEnemiesSpawned}", 2f);
Vector3 enemyPosition = enemy.transform.position;
uiManager.ShowDamageIndicator(enemyPosition);
}
}
private void CheckGameCompletion()
{
if (enemiesDefeated >= 20)
{
if (uiManager != null)
{
uiManager.ShowMissionText("任务完成!", 10f);
}
Time.timeScale = 0.5f;
Invoke("ShowVictoryScreen", 2f);
}
}
private void ShowGameOver()
{
Time.timeScale = 0f;
if (uiManager != null)
{
uiManager.ShowMissionText("游戏结束 - 按R键重新开始", 999f);
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
private void ShowVictoryScreen()
{
Time.timeScale = 0f;
if (uiManager != null)
{
uiManager.ShowMissionText("胜利! - 按R键重新开始", 999f);
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
private void OnApplicationFocus(bool hasFocus)
{
if (hasFocus && cursorLocked)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
public GameObject GetPlayer()
{
return currentPlayer;
}
public Transform GetPlayerTransform()
{
return currentPlayer != null ? currentPlayer.transform : null;
}
public int GetEnemiesRemaining()
{
return activeEnemies.Count;
}
}
8.7.2 性能优化与调试工具
在开发TPS游戏AI系统时,性能优化和调试工具至关重要。
using UnityEngine;
using System.Diagnostics;
using System.Collections.Generic;
public class AIDebugManager : MonoBehaviour
{
[System.Serializable]
public class DebugSettings
{
public bool showVisionCones = true;
public bool showNavigationPaths = true;
public bool showCoverPoints = true;
public bool showBehaviorTree = true;
public Color visionConeColor = new Color(1f, 0f, 0f, 0.2f);
public Color pathColor = Color.yellow;
public Color coverColor = Color.blue;
}
[Header("调试设置")]
[SerializeField] private DebugSettings debugSettings;
[Header("性能监控")]
[SerializeField] private bool enablePerformanceMonitoring = true;
[SerializeField] private float performanceUpdateInterval = 1f;
private Dictionary<string, Stopwatch> performanceTimers =
new Dictionary<string, Stopwatch>();
private Dictionary<string, float> averageTimes =
new Dictionary<string, float>();
private Dictionary<string, int> callCounts =
new Dictionary<string, int>();
private float lastPerformanceUpdate;
private void Update()
{
if (enablePerformanceMonitoring &&
Time.time - lastPerformanceUpdate >= performanceUpdateInterval)
{
UpdatePerformanceDisplay();
lastPerformanceUpdate = Time.time;
}
}
public void StartTimer(string timerName)
{
if (!performanceTimers.ContainsKey(timerName))
{
performanceTimers[timerName] = new Stopwatch();
averageTimes[timerName] = 0f;
callCounts[timerName] = 0;
}
performanceTimers[timerName].Restart();
}
public void StopTimer(string timerName)
{
if (performanceTimers.ContainsKey(timerName))
{
performanceTimers[timerName].Stop();
float elapsedMs = performanceTimers[timerName].ElapsedMilliseconds;
callCounts[timerName]++;
averageTimes[timerName] = (averageTimes[timerName] *
(callCounts[timerName] - 1) + elapsedMs) / callCounts[timerName];
}
}
private void UpdatePerformanceDisplay()
{
string debugText = "AI性能监控:\n";
foreach (var kvp in averageTimes)
{
debugText += $"{kvp.Key}: {kvp.Value:F2}ms (calls: {callCounts[kvp.Key]})\n";
}
UnityEngine.Debug.Log(debugText);
}
private void OnDrawGizmos()
{
if (!Application.isPlaying)
{
return;
}
DrawAIDebugInfo();
}
private void DrawAIDebugInfo()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject enemy in enemies)
{
AIController aiController = enemy.GetComponent<AIController>();
if (aiController != null)
{
if (debugSettings.showVisionCones)
{
DrawVisionCone(aiController);
}
if (debugSettings.showNavigationPaths)
{
DrawNavigationPath(aiController);
}
}
}
if (debugSettings.showCoverPoints)
{
CoverSystem coverSystem = FindObjectOfType<CoverSystem>();
if (coverSystem != null)
{
DrawCoverPoints();
}
}
}
private void DrawVisionCone(AIController aiController)
{
float visionRange = 20f;
float fieldOfView = 90f;
Vector3 position = aiController.transform.position + Vector3.up;
Vector3 forward = aiController.transform.forward;
Gizmos.color = debugSettings.visionConeColor;
int segments = 20;
float step = fieldOfView / segments;
float halfFov = fieldOfView / 2f;
Vector3 previousPoint = position +
Quaternion.Euler(0, -halfFov, 0) * forward * visionRange;
for (int i = 0; i <= segments; i++)
{
float angle = -halfFov + step * i;
Vector3 direction = Quaternion.Euler(0, angle, 0) * forward;
Vector3 point = position + direction * visionRange;
Gizmos.DrawLine(position, point);
if (i > 0)
{
Gizmos.DrawLine(previousPoint, point);
}
previousPoint = point;
}
Gizmos.DrawLine(position, previousPoint);
}
private void DrawNavigationPath(AIController aiController)
{
NavMeshAgent agent = aiController.GetComponent<NavMeshAgent>();
if (agent != null && agent.hasPath)
{
Gizmos.color = debugSettings.pathColor;
Vector3[] pathCorners = agent.path.corners;
for (int i = 0; i < pathCorners.Length - 1; i++)
{
Gizmos.DrawLine(pathCorners[i], pathCorners[i + 1]);
Gizmos.DrawSphere(pathCorners[i], 0.1f);
}
if (pathCorners.Length > 0)
{
Gizmos.DrawSphere(pathCorners[pathCorners.Length - 1], 0.1f);
}
}
}
private void DrawCoverPoints()
{
CoverSystem coverSystem = FindObjectOfType<CoverSystem>();
if (coverSystem != null)
{
Gizmos.color = debugSettings.coverColor;
// 这里需要根据实际的CoverSystem实现来绘制掩体点
// 假设CoverSystem有一个方法可以获取所有掩体点
}
}
public void LogAIEvent(string eventName, string details)
{
string logMessage = $"[AI Event] {eventName}: {details}";
UnityEngine.Debug.Log(logMessage);
}
public void DrawLine(Vector3 from, Vector3 to, Color color, float duration = 0.1f)
{
UnityEngine.Debug.DrawLine(from, to, color, duration);
}
public void DrawSphere(Vector3 position, float radius, Color color, float duration = 0.1f)
{
for (int i = 0; i < 10; i++)
{
float angle = i * Mathf.PI * 2f / 10;
float nextAngle = (i + 1) * Mathf.PI * 2f / 10;
Vector3 point1 = position + new Vector3(
Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Vector3 point2 = position + new Vector3(
Mathf.Cos(nextAngle) * radius, 0, Mathf.Sin(nextAngle) * radius);
UnityEngine.Debug.DrawLine(point1, point2, color, duration);
}
}
}
通过以上完整的系统构建,我们实现了一个具有高度智能的第三人称射击游戏AI系统。这个系统包含了行为树决策、环境感知、战术移动、战斗系统、用户界面和调试工具等多个模块。每个模块都遵循了良好的软件设计原则,确保代码的可维护性和可扩展性。
在实际开发中,还需要根据具体游戏需求调整参数、优化性能,并添加更多高级功能,如机器学习行为适应、动态难度调整和更复杂的团队协作AI等。这个基础框架为开发商业级TPS游戏的AI系统提供了坚实的起点。
更多推荐


所有评论(0)