尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity第三人称射击与AI巡逻系统整合实战:从零搭建可玩Demo

Unity第三人称射击与AI巡逻系统整合实战:从零搭建可玩Demo 1. 项目概述与核心目标最近在社区里看到不少朋友对Unity的AI行为逻辑和第三人称射击TPS框架的整合很感兴趣但往往卡在如何将两者顺畅地结合形成一个有模有样的可玩Demo。正好我最近用Unity 2021.3.21 LTS版本完整地走了一遍从零搭建一个带基础AI巡逻功能的第三人称射击Demo的流程。这个版本稳定生态支持好是进行这类实战项目的绝佳选择。上篇我们将聚焦于最核心的“地基”部分项目初始化、基础场景与角色搭建、第三人称角色控制器实现以及AI巡逻系统的骨架。目标是让你看完就能动手做出一个角色能跑能跳能瞄准AI敌人能按预设路线巡逻并初步感知玩家的可交互场景。这不仅仅是功能的堆砌更重要的是理解每个模块背后的设计思路和Unity引擎的工作流为后续更复杂的战斗逻辑、状态机优化打下坚实基础。2. 环境准备与项目初始化2.1 Unity版本与模板选择选择Unity 2021.3.21f1c1这个LTS长期支持版本是经过考量的。LTS版本经过了更长时间的测试Bug相对较少第三方插件兼容性也最好适合用于需要稳定开发周期的项目。新建项目时我推荐使用“3D (URP)”模板。URPUniversal Render Pipeline通用渲染管线是Unity当前主推的轻量级、可编程渲染管线它在保持不错画质的同时性能开销比内置渲染管线更可控尤其适合移动端或需要较高运行效率的Demo项目。注意如果你之前主要使用内置渲染管线切换到URP初期可能会遇到材质“变紫”的问题。这是因为Shader不兼容。URP项目需要使用专为URP编写的Shader或使用URP自带的Lit/Unlit Shader Graph。在导入标准资产或第三方模型时记得使用Unity的“材质升级工具”进行转换。创建项目后第一件事是规划项目文件夹结构。一个清晰的结构能极大提升后期开发和团队协作的效率。我通常采用如下结构Assets/ ├── _Project项目专用如设置、场景 ├── Art美术资源按类型分Subfolder ├── Audio音效、音乐 ├── Prefabs预制体 ├── Scripts脚本按功能模块分Subfolder │ ├── Core游戏管理器、单例等 │ ├── Player玩家控制相关 │ ├── AI人工智能相关 │ ├── Weapons武器系统 │ └── Utilities工具类、扩展方法 ├── SettingsURP Asset、Input Asset等设置文件 └── UI用户界面在Package Manager中确保已经安装了“Input System”包。Unity的新输入系统更加强大和灵活支持跨平台输入重映射我们将使用它来处理玩家控制。2.2 核心系统设置输入与物理输入系统是交互的源头。在Project Settings Input System Package中创建或指定一个Input Actions Asset。我将其命名为“PlayerControls”。在这个Asset中我们需要定义几个Action Maps比如“Player”和“UI”。在“Player”下创建以下ActionsMove(Value Type: Vector2)绑定到键盘WASD或手柄左摇杆用于角色平面移动。Look(Value Type: Vector2)绑定到鼠标Delta或手柄右摇杆用于控制视角旋转。Jump(Button)空格键或手柄South按钮。Sprint(Button)左Shift或手柄左扳机用于冲刺。Fire(Button)鼠标左键或手柄Right Trigger用于开火。Aim(Button)鼠标右键或手柄Left Trigger用于进入瞄准状态。为每个Action设置合适的交互如Press、Hold并在脚本中通过PlayerInput组件或InputActionC# API来读取这些输入值。使用新输入系统的一个关键技巧是在脚本的OnEnable和OnDisable方法中分别调用输入Action的Enable()和Disable()以正确管理输入状态避免对象失活后输入残留。物理设置同样关键。检查Project Settings Physics和Physics 2D。对于第三人称射击我们关心的是3D物理。确保重力Gravity设置合理默认的-9.81在大多数情况下是合适的。层Layers和碰撞矩阵Layer Collision Matrix需要提前规划。我会创建诸如“Player”、“Enemy”、“Bullet”、“Environment”、“Pickup”等自定义层并在碰撞矩阵中精细控制谁和谁可以发生碰撞或触发检测。例如“Bullet”层应该与“Enemy”和“Environment”碰撞但不与“Player”或“Bullet”自身碰撞。这一步的预先规划能避免后期大量令人头疼的碰撞异常问题。3. 第三人称角色控制器深度实现3.1 角色移动与物理驱动第三人称角色的移动不是简单的Transform平移我们需要结合角色控制器CharacterController或刚体Rigidbody来模拟真实的物理运动。对于地面移动为主的TPSCharacterController是更简单高效的选择它提供了Move方法并自动处理了与带碰撞体的物体的互动但需要自己处理重力。而Rigidbody则提供了完全的物理模拟更真实但也更复杂容易产生“滑冰”感。我选择使用CharacterController配合新输入系统来实现移动。首先创建一个空物体作为玩家根节点如“Player”为其添加CharacterController组件。调整Center、Height和Radius以匹配你的角色模型胶囊体。然后创建一个子物体“Graphics”来放置角色模型和动画骨骼这样旋转模型不会影响控制器的碰撞体。移动脚本的核心逻辑在Update中读取输入从“Move” Action获取一个归一化的Vector2输入向量inputDirection。转换为世界方向输入是相对于玩家的局部方向前、后、左、右我们需要将其转换为世界空间的方向。这需要用到摄像机的前向和右向向量但通常要忽略摄像机的Y轴旋转只使用其在XZ平面上的投影以确保水平移动不受摄像机俯仰影响。Vector3 cameraForward Camera.main.transform.forward; cameraForward.y 0; cameraForward.Normalize(); Vector3 cameraRight Camera.main.transform.right; cameraRight.y 0; cameraRight.Normalize(); Vector3 moveDirection (cameraForward * inputDirection.y cameraRight * inputDirection.x).normalized;应用速度与重力将方向向量乘以移动速度speed得到水平速度horizontalVelocity。同时我们需要计算垂直速度verticalVelocity来处理跳跃和重力。如果角色在地面上且按下跳跃键则给verticalVelocity一个向上的初速度否则每一帧都应用重力加速度使其下降。if (characterController.isGrounded) { if (jumpInput) { verticalVelocity Mathf.Sqrt(jumpHeight * -2f * gravity); } else { verticalVelocity -0.5f; // 一个小值确保贴地 } } else { verticalVelocity gravity * Time.deltaTime; }执行移动将水平速度和垂直速度组合成最终的速度向量通过CharacterController.Move(finalVelocity * Time.deltaTime)来移动角色。Move方法内部会处理与环境的碰撞。实操心得处理斜坡移动时CharacterController的slopeLimit和stepOffset参数非常有用。但要注意在陡坡上单纯的水平移动向量可能会导致角色“爬坡”困难。一个常见的技巧是使用Physics.SphereCast或CharacterController的碰撞信息将移动方向稍微向斜坡的法线方向投影以获得更平滑的斜坡移动体验。3.2 摄像机跟随与视角控制流畅自然的摄像机是第三人称游戏体验的灵魂。我们将实现一个经典的“轨道摄像机”摄像机始终跟随在玩家身后一定距离和高度并可以围绕玩家旋转。创建一个独立的“CameraRig”空物体作为玩家对象的子物体。再创建“CameraPivot”和“Camera”作为其子物体。“CameraRig”控制基本的跟随和Y轴旋转左右看“CameraPivot”控制X轴旋转上下看和摄像机距离“Camera”就是实际的摄像机组件。脚本逻辑分解平滑跟随在LateUpdate中确保在玩家移动后执行使用Vector3.SmoothDamp让CameraRig的位置平滑地趋向于玩家位置。这避免了摄像机生硬的瞬移。处理旋转输入读取“Look” Action的输入。鼠标的输入值需要乘以一个灵敏度系数mouseSensitivity。将X输入左右移动累加到Y轴旋转角度yaw将Y输入上下移动累加到X轴旋转角度pitch。注意要对pitch进行限制如-30度到60度防止摄像机翻转或看到角色模型内部。应用旋转将计算好的yaw和pitch角度分别赋值给CameraRig的Y轴旋转和CameraPivot的X轴旋转。CameraRig.transform.rotation Quaternion.Euler(0f, currentYaw, 0f); CameraPivot.transform.localRotation Quaternion.Euler(currentPitch, 0f, 0f);处理摄像机碰撞为了避免摄像机穿墙需要从玩家角色或Pivot点向摄像机位置发射一条射线或使用SphereCast。如果检测到碰撞就将摄像机位置拉回到碰撞点前方一点的位置。这是一个细节但能极大提升体验。瞄准模式当玩家按下“Aim”键时通常需要摄像机拉近、视野变窄、移动速度变慢。这可以通过动态调整CameraPivot的本地位置Z轴负方向拉近和摄像机的Field of View来实现同时调整移动脚本中的速度系数。注意事项摄像机控制的代码对帧率敏感。务必使用Time.deltaTime来平滑与时间相关的计算如旋转累加。另外在处理鼠标输入时考虑锁定光标Cursor.lockState CursorLockMode.Locked;并隐藏它Cursor.visible false;以获得沉浸式的操控体验。别忘了在游戏暂停或打开菜单时解锁光标。4. AI巡逻系统骨架搭建4.1 导航网格烘焙与基础设置Unity的AI巡逻核心依赖于NavMesh导航网格系统。它允许我们“告诉”AI哪些区域是可以行走的。首先在场景中摆放好静态的环境物体地面、斜坡、台阶、障碍物。选中这些物体在Inspector窗口的Static下拉框中勾选“Navigation Static”。这标志着这些物体将参与导航网格的烘焙计算。然后打开Window AI Navigation窗口。在“Bake”标签页下有几个关键参数Agent RadiusAI代理的半径。这决定了路径的宽度AI无法通过比此半径更窄的通道。Agent HeightAI代理的高度。低于此高度的障碍物如门槛可以被跨越。Max SlopeAI可以爬上的最大斜坡角度。Step HeightAI可以一步迈上的最大高度差。根据你设计的AI角色尺寸通常是一个胶囊体来设置这些参数。设置好后点击“Bake”按钮。Unity会计算并生成一个蓝色的导航网格区域覆盖所有标记为Navigation Static的可行走表面。你可以在“Scene”视图的导航显示模式中查看它。实操心得烘焙导航网格是一个迭代过程。如果发现AI卡在某个地方或走不过去很可能是烘焙参数设置不当或者环境物体的碰撞体形状太复杂。对于复杂的斜坡或不规则地形可能需要手动放置多个NavMesh Agent或使用NavMesh Link组件来连接分离的网格区域比如跨越一个沟渠。4.2 巡逻状态机与路径点管理一个基础的AI巡逻逻辑可以看作一个简单的有限状态机FSM至少包含“巡逻Patrol”和“追击Chase”两种状态。在上篇我们先实现“巡逻”状态。创建一个AI敌人预制体。为其添加以下组件NavMeshAgent这是Unity内置的路径寻找和移动组件。设置其Speed、Angular Speed旋转速度、Acceleration加速度和Stopping Distance停止距离到达目标点多近才算到达。一个自定义的AIController脚本。在AIController脚本中我们需要定义巡逻点在场景中放置一系列空物体作为巡逻点Waypoints将它们拖拽到脚本的公共数组Transform[] patrolPoints中。或者可以在脚本中通过查找带有特定标签如“Waypoint”的游戏对象来动态获取。状态机逻辑在Update中根据当前状态执行不同的逻辑。public enum AIState { Patrol, Chase } private AIState currentState AIState.Patrol; private int currentWaypointIndex 0; private NavMeshAgent agent; void Start() { agent GetComponentNavMeshAgent(); if (patrolPoints.Length 0) agent.SetDestination(patrolPoints[currentWaypointIndex].position); } void Update() { switch (currentState) { case AIState.Patrol: PatrolUpdate(); break; case AIState.Chase: // 下篇实现 break; } } void PatrolUpdate() { // 检查是否到达当前巡逻点 if (!agent.pathPending agent.remainingDistance agent.stoppingDistance) { // 切换到下一个巡逻点循环 currentWaypointIndex (currentWaypointIndex 1) % patrolPoints.Length; agent.SetDestination(patrolPoints[currentWaypointIndex].position); } // 这里可以加入对玩家的检测逻辑如果检测到则切换到Chase状态 // if (DetectPlayer()) currentState AIState.Chase; }移动与旋转NavMeshAgent会自动处理朝向目标的移动和旋转。但有时默认的旋转行为可能显得生硬。你可以通过设置agent.updateRotation为true或false来控制。如果设为false就需要在脚本中手动使用Quaternion.LookRotation或Quaternion.Slerp来让AI平滑地转向移动方向这能实现更风格化的移动效果。4.3 基础视觉感知触发器在巡逻状态下AI需要有能力发现玩家。最基础的方法是使用触发器Trigger Collider结合射线检测Raycast。在AI预制体上添加一个球体碰撞体Sphere Collider并勾选“Is Trigger”。将其半径设置为AI的视觉范围如10米。在AIController脚本中实现OnTriggerEnter和OnTriggerStay方法。当玩家进入触发器范围时方向检查计算从AI位置指向玩家位置的向量。使用Vector3.Angle计算此向量与AI正前方向量的夹角。如果夹角小于某个视野角如60度说明玩家在AI的正面视野锥形区域内。视线遮挡检查即使角度符合中间也可能有墙壁遮挡。从AI的眼睛位置通常是一个子Transform如“EyeTransform”向玩家位置发射一条射线Physics.Raycast。设置LayerMask只检测“Environment”层墙壁等障碍物。如果射线没有击中任何东西或者击中的是玩家则说明视线无遮挡。状态切换如果以上两个条件都满足则将AI状态从Patrol切换到Chase并立即将玩家的当前位置设置为NavMeshAgent的新目标agent.SetDestination(player.position)。注意事项在OnTriggerStay中每帧都进行射线检测开销较大。一个优化技巧是使用协程Coroutine进行间隔检测比如每0.2秒检测一次。同时确保触发器的Layer设置正确只与“Player”层交互避免与其他物体误触发。这个基础的感知系统虽然简单但已经能实现“发现即追击”的核心反馈为下篇实现更复杂的战斗行为奠定了基础。5. 场景整合与初步调试5.1 玩家与AI的预制体化与场景布置当玩家控制器和AI巡逻逻辑的核心脚本都编写测试完毕后下一步就是将它们转化为可复用的预制体Prefab并布置到场景中。对于玩家将包含CharacterController、输入处理脚本、摄像机跟随脚本以及视觉模型的整个层级结构拖入Project视图的Prefabs文件夹创建一个“Player”预制体。对于AI同样将带有NavMeshAgent、AIController脚本、触发器碰撞体和模型的层级结构创建为“Enemy_Patrol”预制体。回到场景中删除测试用的临时对象。从Prefabs文件夹将“Player”预制体拖入场景放置在一个合适的起始位置。然后在场景中放置多个空物体作为AI的巡逻点Waypoints合理规划一条巡逻路径。最后将“Enemy_Patrol”预制体拖入场景并将巡逻点数组赋值给其AIController脚本的patrolPoints字段。可以复制多个AI实例并分配相同或不同的巡逻路径。此时运行游戏你应该能使用WASD移动鼠标控制视角并且AI会按照设定的路径点循环巡逻。当你操控玩家角色进入AI的触发范围和视野内时AI应立即停止巡逻转向并开始朝玩家移动追击状态虽然追击逻辑在下篇完善但目标切换应已生效。5.2 常见问题排查与性能初探在整合阶段你可能会遇到一些典型问题。这里有一个快速排查清单问题现象可能原因解决方案玩家移动时摄像机抖动或延迟大。摄像机跟随代码写在Update中而玩家移动也写在Update中执行顺序可能导致一帧内位置不同步。将摄像机跟随逻辑放在LateUpdate中执行确保它在所有Update逻辑之后运行。AI站在原地不动不前往巡逻点。1.NavMeshAgent组件未启用。2. 巡逻点未正确赋值或为空。3. 巡逻点的位置不在导航网格上。1. 检查Inspector中NavMeshAgent的启用复选框。2. 在运行时通过Debug.Log输出巡逻点数组长度和位置信息。3. 在Scene视图的导航显示模式下确认巡逻点位置有蓝色导航网格覆盖。AI发现玩家后不追击。1. 触发器未生效Layer设置错误或Is Trigger未勾选。2. 视线检测条件过于严格视野角太小或射线被误遮挡。3. 状态切换逻辑未执行或执行后目标设置失败。1. 检查碰撞体Layer和Is Trigger设置。2. 使用Debug.DrawRay可视化射线检查是否命中预期目标。3. 在DetectPlayer方法和状态切换处添加Debug.Log确认逻辑流程。玩家或AI移动时“穿墙”或卡住。1. 碰撞体Collider形状或大小设置不合理。2. 移动速度过快单帧位移过大导致穿透“子弹穿透”问题。3. 导航网格烘焙不准确存在缝隙或不可达区域。1. 调整CharacterController或模型碰撞体的尺寸使其紧密贴合模型。2. 对于高速移动对象考虑使用Raycast或SphereCast进行连续碰撞检测CCD或增加物理更新频率。3. 重新检查并烘焙导航网格确保可行走区域连续。性能方面在这个阶段主要关注两点Draw Calls和脚本效率。在Game视图的Stats面板中可以查看帧率(FPS)和批处理次数。如果Draw Calls过高比如超过1000需要考虑使用静态批处理Static Batching或GPU Instancing来合并相同材质的静态物体。对于脚本避免在Update中每帧进行昂贵的操作如FindGameObjectsWithTag、未缓存的GetComponent调用。对于AI的感知检测如前所述使用协程进行间隔检测是有效的优化手段。6. 工程结构与资源管理初步6.1 脚本架构与依赖管理随着功能增多良好的代码结构至关重要。避免将所有逻辑都塞进一个巨大的PlayerController或AIController里。我倾向于采用基于组件的职责分离模式。例如对于玩家PlayerInputHandler专门负责从Input System读取原始输入并将其转化为干净的事件或值供其他组件消费。PlayerLocomotion负责处理移动、跳跃、重力应用等物理运动逻辑与CharacterController交互。PlayerAim处理瞄准状态、摄像机拉近、视野变化等。PlayerHealth管理生命值、受伤、死亡逻辑。这些组件通过GetComponent在同一个GameObject上相互获取引用或者通过一个中心化的PlayerManager单例来协调。对于AI也是如此可以将感知(AISensor)、决策(AIStateMachine)、移动(AIMover)分离。依赖管理上尽量使用[SerializeField] private在Inspector中拖拽赋值而不是在代码中硬编码查找。这提高了脚本的复用性和场景配置的灵活性。对于必须动态查找的对象如玩家对象可以使用GameObject.FindWithTag(“Player”)配合缓存在Start或Awake中查找一次并存储引用。6.2 动画状态机与混合树入门虽然上篇不深入动画但为后续衔接需要建立基础。为玩家和AI模型创建Animator Controller。对于玩家基本的动画状态可能包括Idle待机、Walk行走、Run奔跑、Jump跳跃、Fall下落、Aim瞄准。使用Blend Trees混合树来平滑处理从Idle到Walk到Run的过渡参数可以使用速度大小。将Animator组件添加到模型上并关联创建好的Animator Controller。在移动脚本如PlayerLocomotion中根据当前速度、是否在地面、是否瞄准等条件设置Animator的参数animator.SetFloat(“Speed”, currentSpeed)。AI的动画更简单可能只需要Walk和Idle根据NavMeshAgent.velocity.magnitude是否大于一个阈值来切换。实操心得动画状态机的过渡条件要设置合理的时间Duration和退出时间Exit Time。避免使用“Has Exit Time”除非必要因为它会强制动画播放完才切换导致操控响应延迟。对于需要即时响应的动作如跳跃、射击使用布尔Bool或触发器Trigger参数并设置较短的固定过渡时长Fixed Duration。将动画片段导入设置中的“Loop Time”勾选对于循环动作走、跑至关重要。
返回列表