第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行为逻辑。

行为树的基本组成节点包括:

  1. 控制节点:决定子节点的执行顺序,包括选择节点、序列节点和平行节点
  2. 条件节点:检查特定条件是否满足,返回成功或失败
  3. 动作节点:执行具体行为,如移动、攻击、躲避等

在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的核心组成部分。一个良好的掩体系统应该提供:

  1. 掩体点自动生成
  2. 掩体质量评估
  3. 掩体间移动路径
  4. 掩体射击位置计算
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系统提供了坚实的起点。

Logo

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

更多推荐