1. 先搞清楚这个实战项目到底要解决什么问题如果你刚开始接触 Unity或者想从 2D 小游戏转向 3D 动作 RPG这个“骑士战斗实战”项目是个非常直接的切入点。它要解决的核心问题不是让你从零开始学 C# 语法而是让你理解一个 3D 动作 RPG 里玩家控制、敌人 AI 和战斗逻辑这三者是怎么协同工作的。很多人学 Unity 卡在“知识点都会但不知道怎么拼成一个能玩的游戏”这一步这个项目就是帮你跨过这道坎。最值得关注的不是 Unity 6 的新特性列表而是如何用最基础的组件Character Controller、Animator、NavMeshAgent和清晰的代码结构搭出一个手感尚可、逻辑完整的战斗原型。这意味着即使你机器配置一般也能跟着做出来并且能清楚地知道每一行代码在游戏运行中扮演什么角色。2. 动手前的环境与思路准备别急着写代码在打开 Unity Hub 之前先明确两件事项目范围和学习路径。这不是一个商业级游戏而是一个教学原型。我们的目标是实现玩家骑士可以移动、攻击敌人能自动寻路、发现玩家、进入战斗状态并进行攻击双方有简单的生命值HP和伤害计算。环境准备清单Unity 版本标题提到 Unity 6但目前撰写时Unity 6 可能仍处于预览或早期发布阶段。对于学习稳定性优先我建议使用Unity 2022 LTS或Unity 2023的最新稳定版本。核心逻辑C# 脚本、物理、动画系统在这些版本中通用不影响学习。确保安装时勾选Windows/Mac IL2CPP Build Support和Visual Studio Community或Rider作为代码编辑器。项目模板新建项目时选择3D (Core)模板即可。不需要 URP 或 HDRP 这些高级渲染管线它们会增加不必要的复杂度。资源准备去 Unity Asset Store 或一些免费的 3D 模型网站如 Mixamo找一个带骨骼动画的骑士Knight模型和一个怪物如骷髅、兽人模型。确保它们有 Idle待机、Run奔跑、Attack攻击、Take Damage受击、Death死亡这几个基础动画片段。这是动作 RPG 的“原材料”。思路准备把游戏拆解成几个独立又关联的模块输入模块处理键盘WASD和鼠标输入控制玩家移动和攻击指令。角色控制模块包含玩家控制器和敌人控制器。负责移动、转向、动画播放。战斗模块管理攻击判定、伤害计算、生命值HP变化、战斗状态是否在攻击中、是否可被攻击。AI 模块让敌人能巡逻、发现玩家、追逐、攻击决策。状态模块一个简单的状态机管理角色的“闲置”、“移动”、“攻击”、“受伤”、“死亡”等状态。先在大脑里搭好这个架子写代码时就不会东一榔头西一棒子。3. 搭建战场场景、角色与基础移动3.1 创建基础场景与玩家角色在场景中创建一个 Plane 或 Terrain 作为地面。导入你的骑士模型拖入场景重命名为Player。为Player对象添加Character Controller组件。这是一个比 Rigidbody 更适合 RPG 角色移动的组件它自带胶囊碰撞体并处理了与地面的交互但将物理模拟的控制权交给了脚本。调整Character Controller的Center和Height使其匹配模型大小。为Player添加Animator组件。随后创建一个 Animator Controller如Player_AC并双击打开 Animator 窗口。3.2 实现玩家移动控制创建一个 C# 脚本PlayerController.cs并挂载到Player上。using UnityEngine; public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 5f; public float rotationSpeed 10f; [Header(组件引用)] private CharacterController characterController; private Animator animator; private Camera mainCamera; private Vector3 moveDirection; private bool isMoving; void Start() { characterController GetComponentCharacterController(); animator GetComponentAnimator(); mainCamera Camera.main; // 获取主摄像机用于将输入转换为世界方向 } void Update() { HandleMovement(); UpdateAnimator(); } void HandleMovement() { // 获取原始输入 float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 inputDir new Vector3(horizontal, 0, vertical).normalized; if (inputDir.magnitude 0.1f) { // 将输入方向相对于摄像机旋转进行转换 float targetAngle Mathf.Atan2(inputDir.x, inputDir.z) * Mathf.Rad2Deg mainCamera.transform.eulerAngles.y; Vector3 worldMoveDir Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward; // 移动角色控制器 characterController.Move(worldMoveDir.normalized * moveSpeed * Time.deltaTime); // 平滑转向至移动方向 Quaternion targetRotation Quaternion.LookRotation(worldMoveDir); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); isMoving true; } else { isMoving false; } } void UpdateAnimator() { // 将移动状态传递给 Animator animator.SetBool(IsMoving, isMoving); } }关键点解释CharacterController.Move()这是移动角色的核心方法。它比直接修改Transform.position更好因为它会考虑碰撞。摄像机相对移动mainCamera.transform.eulerAngles.y用于将基于键盘的输入总是相对于世界坐标转换为玩家视角下的方向。这是第三人称游戏的常见做法。Animator.SetBool(“IsMoving”)我们在代码里设置一个布尔参数然后在 Animator Controller 里用这个参数来切换 Idle 和 Run 动画状态。3.3 配置玩家动画状态机在Player_AC中创建两个状态Idle和Run。创建从Any State到Idle的入口以及Idle和Run之间的双向转换Transitions。为转换设置条件Conditions。例如从Idle到Run的条件是IsMoving为 true从Run到Idle的条件是IsMoving为 false。将对应的动画片段Idle, Run拖拽到各自的状态上。现在运行游戏你应该能用 WASD 控制骑士移动并看到移动和待机动画的切换。4. 构建战斗系统的核心攻击与伤害动作 RPG 的战斗手感很重要。我们需要实现攻击动画播放、攻击范围判定、伤害应用。4.1 扩展玩家控制器加入攻击逻辑修改PlayerController.cs增加攻击相关部分。public class PlayerController : MonoBehaviour { // ... 之前的变量 ... [Header(战斗参数)] public float attackRange 2f; public int attackDamage 10; public float attackCooldown 1f; private float lastAttackTime -Mathf.Infinity; // 初始化为一个很早的时间使第一次攻击可以立即进行 public LayerMask enemyLayer; // 在 Inspector 中设置为 Enemy 所在的层 void Update() { HandleMovement(); HandleAttack(); // 新增攻击处理 UpdateAnimator(); } void HandleAttack() { // 例如用鼠标左键攻击 if (Input.GetMouseButtonDown(0) Time.time lastAttackTime attackCooldown) { // 触发攻击动画 animator.SetTrigger(Attack); lastAttackTime Time.time; // 注意伤害判定不应该立即执行而应该与动画事件Animation Event或特定帧同步 // 这里我们先写一个立即判定的简单版本后面会优化。 PerformAttackDetection(); } } void PerformAttackDetection() { // 简单的球形检测前方敌人 Collider[] hitEnemies Physics.OverlapSphere(transform.position transform.forward, attackRange, enemyLayer); foreach (Collider enemy in hitEnemies) { // 假设敌人身上有一个 EnemyHealth 脚本 EnemyHealth enemyHealth enemy.GetComponentEnemyHealth(); if (enemyHealth ! null) { enemyHealth.TakeDamage(attackDamage); } } } // 这是一个可视化攻击范围的方法便于调试 void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position transform.forward, attackRange); } }4.2 创建敌人的生命值脚本创建一个EnemyHealth.cs脚本未来挂载到敌人对象上。using UnityEngine; public class EnemyHealth : MonoBehaviour { public int maxHealth 30; private int currentHealth; void Start() { currentHealth maxHealth; } public void TakeDamage(int damage) { currentHealth - damage; Debug.Log(gameObject.name 受到 damage 点伤害剩余生命: currentHealth); // 触发受击动画如果有 Animator anim GetComponentAnimator(); if (anim ! null) { anim.SetTrigger(TakeDamage); } if (currentHealth 0) { Die(); } } void Die() { Debug.Log(gameObject.name 死亡); // 触发死亡动画 Animator anim GetComponentAnimator(); if (anim ! null) { anim.SetBool(IsDead, true); } // 禁用碰撞体和控制器防止死后还能被攻击或移动 Collider col GetComponentCollider(); if (col ! null) col.enabled false; // 例如5秒后销毁对象 Destroy(gameObject, 5f); } }4.3 优化攻击判定使用动画事件直接在Update里检测攻击范围手感很差因为攻击动作有前摇。更专业的做法是使用动画事件。在玩家模型的 Attack 动画片段上在武器挥出、应该造成伤害的那一帧添加一个事件。在 Unity 动画窗口选中 Attack 片段点击 “Add Event” 按钮。将事件函数命名为OnAttackHit。在PlayerController.cs中添加一个公有方法OnAttackHit将PerformAttackDetection()的调用移动到这个方法里。这样伤害判定就与动画帧精确同步了。现在战斗流程是玩家按下攻击键 - 播放攻击动画 - 动画播放到特定帧触发事件 - 执行攻击检测逻辑 - 对范围内敌人造成伤害。5. 赋予敌人“灵魂”基础 AI 与状态机敌人不能只是个木桩。我们需要一个简单的 AI 控制器让它能巡逻、发现玩家、追逐并攻击。5.1 创建敌人 AI 控制器创建EnemyAIController.cs脚本。using UnityEngine; using UnityEngine.AI; public class EnemyAIController : MonoBehaviour { public enum AIState { Idle, Patrol, Chase, Attack } public AIState currentState AIState.Patrol; [Header(巡逻参数)] public Transform[] patrolPoints; private int currentPatrolIndex 0; public float patrolWaitTime 2f; private float waitTimer 0f; [Header(追逐与攻击参数)] public float sightRange 10f; public float attackRange 2f; public float chaseSpeed 3.5f; public float patrolSpeed 2f; private Transform playerTarget; [Header(组件)] private NavMeshAgent navMeshAgent; private Animator animator; private EnemyHealth enemyHealth; void Start() { navMeshAgent GetComponentNavMeshAgent(); animator GetComponentAnimator(); enemyHealth GetComponentEnemyHealth(); playerTarget GameObject.FindGameObjectWithTag(Player).transform; // 确保玩家对象有“Player”标签 if (patrolPoints.Length 0) { Debug.LogWarning(未设置巡逻点敌人将停留在原地。); currentState AIState.Idle; } } void Update() { if (enemyHealth ! null enemyHealth.currentHealth 0) return; // 如果死亡停止AI float distanceToPlayer Vector3.Distance(transform.position, playerTarget.position); // 状态决策 switch (currentState) { case AIState.Idle: // 可以什么都不做或者加入一个发呆后继续巡逻的逻辑 break; case AIState.Patrol: PatrolBehavior(distanceToPlayer); break; case AIState.Chase: ChaseBehavior(distanceToPlayer); break; case AIState.Attack: AttackBehavior(distanceToPlayer); break; } UpdateAnimator(); } void PatrolBehavior(float distToPlayer) { // 如果发现玩家进入追逐状态 if (distToPlayer sightRange) { currentState AIState.Chase; return; } // 巡逻逻辑 if (patrolPoints.Length 0) { if (!navMeshAgent.pathPending navMeshAgent.remainingDistance navMeshAgent.stoppingDistance) { // 到达巡逻点等待一段时间 waitTimer Time.deltaTime; if (waitTimer patrolWaitTime) { waitTimer 0f; currentPatrolIndex (currentPatrolIndex 1) % patrolPoints.Length; navMeshAgent.SetDestination(patrolPoints[currentPatrolIndex].position); } } navMeshAgent.speed patrolSpeed; } } void ChaseBehavior(float distToPlayer) { // 如果玩家进入攻击范围转为攻击 if (distToPlayer attackRange) { currentState AIState.Attack; navMeshAgent.isStopped true; // 停止寻路准备攻击 return; } // 如果玩家跑出视野范围回到巡逻 else if (distToPlayer sightRange * 1.2f) // 加一个缓冲防止在边界反复横跳 { currentState AIState.Patrol; navMeshAgent.isStopped false; return; } // 追逐玩家 navMeshAgent.isStopped false; navMeshAgent.SetDestination(playerTarget.position); navMeshAgent.speed chaseSpeed; } void AttackBehavior(float distToPlayer) { // 面向玩家 Vector3 directionToPlayer (playerTarget.position - transform.position).normalized; directionToPlayer.y 0; Quaternion lookRotation Quaternion.LookRotation(directionToPlayer); transform.rotation Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 10f); // 检查是否应该攻击这里简化可以加入攻击冷却 animator.SetTrigger(Attack); // 触发敌人攻击动画 // 如果玩家跑出攻击范围重新追逐 if (distToPlayer attackRange * 1.2f) { currentState AIState.Chase; navMeshAgent.isStopped false; } } void UpdateAnimator() { // 根据 NavMeshAgent 的速度设置移动动画 float speed navMeshAgent.velocity.magnitude / navMeshAgent.speed; animator.SetFloat(Speed, speed); // 设置状态参数用于动画机切换如 Attack, TakeDamage 等 animator.SetInteger(AIState, (int)currentState); } // 敌人攻击动画事件调用的方法用于对玩家造成伤害 public void OnEnemyAttackHit() { // 检测前方扇形或球形区域内的玩家 Collider[] hitPlayers Physics.OverlapSphere(transform.position transform.forward, attackRange - 0.5f); foreach (Collider col in hitPlayers) { if (col.CompareTag(Player)) { PlayerHealth playerHealth col.GetComponentPlayerHealth(); if (playerHealth ! null) { playerHealth.TakeDamage(10); // 敌人攻击力 } } } } }5.2 设置敌人与导航网格将敌人模型拖入场景挂载NavMeshAgent、Animator、EnemyHealth和EnemyAIController脚本。为敌人对象设置一个 Layer比如 “Enemy”并在玩家攻击检测的enemyLayer中勾选此层。为玩家对象设置 Tag 为 “Player”。在场景中放置几个空物体作为patrolPoints并拖拽到敌人控制器的对应数组里。最重要的一步烘焙导航网格NavMesh。顶部菜单栏Window - AI - Navigation。在 Navigation 窗口的 Bake 页签确保你的地面Plane/Terrain是 Walkable。点击 “Bake” 按钮。烘焙后地面会变成蓝色表示敌人可以在此区域移动。在NavMeshAgent组件上可以调整 Speed、Angular Speed、Stopping Distance 等参数来微调移动手感。现在运行游戏敌人应该会在巡逻点之间移动。当你控制的骑士进入其sightRange它会开始追逐进入attackRange它会停止并播放攻击动画。6. 收尾与打磨让战斗更像样基础功能都有了但还很粗糙。我们需要把它们连接得更顺畅并解决一些明显的问题。6.1 创建玩家生命值系统创建一个PlayerHealth.cs脚本逻辑与EnemyHealth类似挂载到玩家对象上。这样敌人攻击时才能对玩家造成伤害。using UnityEngine; using UnityEngine.UI; // 如果需要 UI public class PlayerHealth : MonoBehaviour { public int maxHealth 100; private int currentHealth; public Slider healthSlider; // 可选的 UI 血条 void Start() { currentHealth maxHealth; if (healthSlider ! null) { healthSlider.maxValue maxHealth; healthSlider.value currentHealth; } } public void TakeDamage(int damage) { currentHealth - damage; Debug.Log(玩家受到 damage 点伤害剩余生命: currentHealth); // 更新 UI if (healthSlider ! null) healthSlider.value currentHealth; // 触发玩家受击动画或屏幕特效 Animator anim GetComponentAnimator(); if (anim ! null) anim.SetTrigger(TakeDamage); if (currentHealth 0) { Die(); } } void Die() { Debug.Log(玩家死亡); // 触发死亡动画禁用控制 Animator anim GetComponentAnimator(); if (anim ! null) anim.SetBool(IsDead, true); PlayerController pc GetComponentPlayerController(); if (pc ! null) pc.enabled false; // 游戏结束逻辑... } }6.2 解决战斗中的“粘滞”问题目前玩家和敌人进入攻击范围后攻击动画会不断触发在 Update 里每帧判断。这看起来不自然。我们需要引入攻击冷却和攻击状态。在PlayerController和EnemyAIController中我们已经有了attackCooldown和lastAttackTime。确保在攻击后只有冷却结束才能进行下一次攻击。在 Animator Controller 中为攻击动画创建一个独立的 “Attack” 状态并设置其 “Has Exit Time” 为 true或者使用一个 “IsAttacking” 的布尔参数来控制确保一次攻击动画完整播放期间不能打断或有限打断为其他动作。6.3 添加视觉反馈受击反馈当角色受到伤害时除了播放动画可以短暂地将模型材质颜色变红使用协程StartCoroutine(FlashRed())或者显示伤害数字UI Text。攻击范围可视化如前所述使用OnDrawGizmosSelected绘制攻击范围的线框对于调试非常有帮助。敌人视野可视化可以在敌人前方绘制一个扇形 Gizmo 来表示sightRange和视野角度。6.4 性能与代码结构优化GameObject.FindGameObjectWithTag和GetComponent在 Update 中频繁调用有开销。在Start或Awake中缓存这些引用。敌人的状态检测如距离计算不需要每帧都做可以每 0.2-0.5 秒进行一次使用InvokeRepeating或一个计时器这是一种简单的性能优化。考虑将通用的战斗逻辑如伤害检测、生命值管理提取到基类CharacterCombat中让PlayerController和EnemyAIController继承它减少重复代码。7. 从原型到可玩下一步可以做什么当你完成了以上所有步骤一个最基础的 3D 动作 RPG 战斗循环就实现了。但这只是个起点。要让游戏更有趣你可以按以下优先级继续深化动画融合与根运动目前移动是直接由代码控制位置动画是“播放”上去的可能会滑步。研究 Animator 中的 “Root Motion” 选项让角色的移动完全由动画本身驱动手感会更扎实。更复杂的 AI 行为树对于多个敌人或更复杂的行为逃跑、呼叫同伴、使用技能简单的 switch-case 状态机会变得难以维护。可以了解行为树Behavior Tree插件或设计模式。技能系统将攻击抽象为 “Skill” 或 “Ability” 类包含冷却时间、伤害、效果范围、动画、粒子特效等。这样便于扩展火球、治疗等技能。装备与属性系统创建Item、Weapon、Armor类以及CharacterStats类来管理攻击力、防御力、生命值等属性。攻击伤害计算将基于这些属性。UI 系统用 Unity 的 UGUI 制作更精美的血条、技能图标、背包界面。音效与音乐为攻击、受击、死亡、脚步声添加音效大幅提升沉浸感。关卡设计利用 ProBuilder 或外部建模工具设计更有层次感的战斗场景而不仅仅是一个平面。最后也是最关键的建议不要试图一次性实现所有功能。像搭积木一样每完成一个可运行的小模块比如一个完整的攻击流程就测试一下确保它工作正常再进入下一个。遇到 Bug优先使用Debug.Log输出关键变量值并利用 Unity 的Console 窗口和Profiler 窗口来定位性能瓶颈。这个实战项目的价值就在于让你亲身体验一个看似复杂的游戏系统是如何由一个个简单的、可理解的模块组合而成的。