Unity 2021.3.21 实战:从零搭建带AI巡逻的第三人称射击Demo(上篇)
在独立游戏开发领域,快速验证核心玩法是项目成功的关键。本文将带你使用Unity 2021.3.21版本,从零开始构建一个具备基础AI功能的第三人称射击游戏原型。不同于简单的跟随教程,我们将深入探讨如何将游戏设计文档中的概念转化为可运行的代码实现,特别聚焦于AI巡逻与警戒系统的构建。
1. 项目初始化与环境配置
开始前,我们需要确保开发环境正确配置。Unity 2021.3.21作为长期支持版本(LTS),提供了稳定的开发基础。建议使用VS Code作为代码编辑器,通过以下步骤配置:
- 安装Unity 2021.3.21f1版本
- 在VS Code中安装C#扩展包
- 在Unity Editor中设置外部工具:
- 菜单栏选择Edit > Preferences
- 在External Tools选项卡中设置External Script Editor为VS Code
- 确保.NET版本与Unity兼容
提示:使用LTS版本可避免因引擎更新导致的兼容性问题,特别适合长期项目开发。
2. 游戏设计文档(GDD)核心要素
一个完整的GDD应包含以下关键部分,我们将其精简为射击游戏Demo所需的核心内容:
| 设计要素 | 具体实现 | 技术要点 |
|---|---|---|
| 核心概念 | 躲避巡逻敌人并射击 | AI状态机、碰撞检测 |
| 移动机制 | WASD控制角色移动 | Rigidbody物理系统 |
| 视角控制 | 鼠标控制第三人称相机 | 摄像机跟随算法 |
| 战斗系统 | 左键射击、生命值管理 | 对象实例化与销毁 |
| AI行为 | 巡逻路线与警戒范围 | NavMesh与触发器 |
关键决策点:选择第三人称而非第一人称视角,可更好地展示角色动画与场景交互,同时降低摄像机控制复杂度。
3. 场景搭建与资源管理
3.1 基础场景构建
使用Unity原生几何体快速搭建游戏场景:
// 示例:通过代码创建基础地形 void CreateBaseTerrain() { GameObject ground = GameObject.CreatePrimitive(PrimitiveType.Plane); ground.transform.localScale = new Vector3(5, 1, 5); ground.name = "Ground"; ground.tag = "Environment"; }3.2 预制件系统实践
掩体等重复元素应使用预制件(Prefab):
- 创建基础立方体组合作为掩体原型
- 拖拽到Project视图创建预制件
- 通过代码实例化:
public GameObject coverPrefab; void SpawnCovers() { for(int i=0; i<4; i++) { Vector3 pos = new Vector3(i*10, 0, i*10); Instantiate(coverPrefab, pos, Quaternion.identity); } }3.3 动态元素添加
为场景增加视觉反馈:
- 创建旋转的拾取物品:
- 添加Animation组件
- 设置Y轴旋转关键帧
- 粒子系统效果:
- 使用Particle System组件
- 调整发射器形状与生命周期
4. 角色控制系统实现
4.1 物理移动方案对比
我们对比两种移动实现方式:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Transform | 简单直接 | 忽略物理交互 | 简单原型 |
| Rigidbody | 真实物理反馈 | 实现复杂 | 正式项目 |
推荐方案:采用Rigidbody物理移动,为后续碰撞检测和AI交互奠定基础。
4.2 完整角色控制器
[RequireComponent(typeof(Rigidbody))] public class PlayerController : MonoBehaviour { public float moveSpeed = 5f; public float rotateSpeed = 180f; public float jumpForce = 7f; public LayerMask groundLayer; private Rigidbody rb; private CapsuleCollider col; private bool isGrounded; void Start() { rb = GetComponent<Rigidbody>(); col = GetComponent<CapsuleCollider>(); } void Update() { HandleJumpInput(); } void FixedUpdate() { HandleMovement(); CheckGrounded(); } void HandleMovement() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 movement = transform.forward * v * moveSpeed; Vector3 rotation = Vector3.up * h * rotateSpeed; rb.MovePosition(rb.position + movement * Time.fixedDeltaTime); rb.MoveRotation(rb.rotation * Quaternion.Euler(rotation * Time.fixedDeltaTime)); } void HandleJumpInput() { if(Input.GetKeyDown(KeyCode.Space) && isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } void CheckGrounded() { float rayLength = col.bounds.extents.y + 0.1f; isGrounded = Physics.Raycast(transform.position, Vector3.down, rayLength, groundLayer); } }4.3 摄像机跟随系统
第三人称摄像机需要解决的主要问题:
- 避免穿墙
- 平滑跟随
- 鼠标控制灵敏度
public class ThirdPersonCamera : MonoBehaviour { public Transform target; public Vector3 offset = new Vector3(0, 2, -3); public float smoothSpeed = 0.125f; public float mouseSensitivity = 2f; private float currentX = 0f; private float currentY = 0f; void LateUpdate() { currentX += Input.GetAxis("Mouse X") * mouseSensitivity; currentY -= Input.GetAxis("Mouse Y") * mouseSensitivity; currentY = Mathf.Clamp(currentY, -30, 70); Quaternion rotation = Quaternion.Euler(currentY, currentX, 0); Vector3 desiredPosition = target.position + rotation * offset; // 防止穿墙 RaycastHit hit; if(Physics.Linecast(target.position, desiredPosition, out hit)) { desiredPosition = hit.point - offset.normalized; } transform.position = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed); transform.LookAt(target); } }5. 基础AI系统实现
5.1 巡逻状态设计
AI敌人需要具备三种基本状态:
- 巡逻状态:沿预定路径移动
- 警戒状态:发现玩家后追踪
- 攻击状态:进入攻击范围后行动
public enum AIState { Patrol, Alert, Attack }5.2 巡逻路径系统
创建可复用的巡逻路径方案:
- 使用空对象作为路径点
- 通过脚本管理路径点序列
public class PatrolPath : MonoBehaviour { public List<Transform> waypoints = new List<Transform>(); public bool isCircular = true; void OnDrawGizmos() { if(waypoints.Count > 1) { for(int i=1; i<waypoints.Count; i++) { Gizmos.DrawLine(waypoints[i-1].position, waypoints[i].position); } if(isCircular) { Gizmos.DrawLine(waypoints[waypoints.Count-1].position, waypoints[0].position); } } } }5.3 基础AI控制器
public class AIController : MonoBehaviour { public AIState currentState = AIState.Patrol; public PatrolPath patrolPath; public float moveSpeed = 3f; public float detectionRange = 8f; private int currentWaypoint = 0; private Transform player; void Start() { player = GameObject.FindGameObjectWithTag("Player").transform; } void Update() { switch(currentState) { case AIState.Patrol: PatrolBehavior(); CheckForPlayer(); break; case AIState.Alert: ChasePlayer(); break; } } void PatrolBehavior() { if(patrolPath.waypoints.Count == 0) return; Transform target = patrolPath.waypoints[currentWaypoint]; Vector3 direction = (target.position - transform.position).normalized; transform.position += direction * moveSpeed * Time.deltaTime; if(Vector3.Distance(transform.position, target.position) < 0.5f) { currentWaypoint = (currentWaypoint + 1) % patrolPath.waypoints.Count; } } void CheckForPlayer() { if(Vector3.Distance(transform.position, player.position) < detectionRange) { currentState = AIState.Alert; } } void ChasePlayer() { Vector3 direction = (player.position - transform.position).normalized; transform.position += direction * moveSpeed * Time.deltaTime; if(Vector3.Distance(transform.position, player.position) > detectionRange * 1.5f) { currentState = AIState.Patrol; } } }5.4 视觉反馈增强
为AI状态变化添加视觉提示:
public class AIVisualFeedback : MonoBehaviour { public Material patrolMaterial; public Material alertMaterial; private Renderer renderer; private AIController aiController; void Start() { renderer = GetComponent<Renderer>(); aiController = GetComponent<AIController>(); } void Update() { renderer.material = aiController.currentState == AIState.Patrol ? patrolMaterial : alertMaterial; } }6. 射击系统实现
6.1 子弹物理系统
创建子弹预制件需注意:
- 添加Rigidbody组件
- 设置适当的碰撞体
- 配置物理材质减少弹跳
public class Bullet : MonoBehaviour { public float speed = 50f; public float lifetime = 3f; public int damage = 10; void Start() { Destroy(gameObject, lifetime); } void FixedUpdate() { transform.position += transform.forward * speed * Time.fixedDeltaTime; } void OnCollisionEnter(Collision collision) { if(collision.gameObject.CompareTag("Enemy")) { collision.gameObject.GetComponent<AIHealth>().TakeDamage(damage); } Destroy(gameObject); } }6.2 武器射击逻辑
public class Weapon : MonoBehaviour { public GameObject bulletPrefab; public Transform firePoint; public float fireRate = 0.2f; private float nextFireTime = 0f; void Update() { if(Input.GetMouseButton(0) && Time.time >= nextFireTime) { Shoot(); nextFireTime = Time.time + fireRate; } } void Shoot() { GameObject bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); bullet.GetComponent<Rigidbody>().velocity = firePoint.forward * bulletPrefab.GetComponent<Bullet>().speed; } }6.3 射击反馈优化
提升射击体验的关键细节:
- 添加枪口闪光粒子效果
- 实现后坐力动画
- 添加射击音效
public class WeaponEffects : MonoBehaviour { public ParticleSystem muzzleFlash; public AudioClip shootSound; void PlayShootEffects() { muzzleFlash.Play(); AudioSource.PlayClipAtPoint(shootSound, transform.position); } }7. 碰撞检测与交互系统
7.1 触发器与碰撞器区别
| 特性 | 碰撞器(Collider) | 触发器(Trigger) |
|---|---|---|
| 物理反应 | 有 | 无 |
| 触发事件 | OnCollisionEnter | OnTriggerEnter |
| 性能消耗 | 较高 | 较低 |
| 典型用途 | 物理交互 | 检测区域 |
7.2 拾取物品实现
public class PickupItem : MonoBehaviour { public enum ItemType { Health, Ammo, Powerup } public ItemType type; public int value = 10; void OnTriggerEnter(Collider other) { if(other.CompareTag("Player")) { other.GetComponent<PlayerInventory>().AddItem(type, value); Destroy(gameObject); } } }7.3 玩家库存系统
public class PlayerInventory : MonoBehaviour { public int health = 100; public int ammo = 30; public void AddItem(PickupItem.ItemType type, int value) { switch(type) { case PickupItem.ItemType.Health: health = Mathf.Min(health + value, 100); break; case PickupItem.ItemType.Ammo: ammo += value; break; } } }在开发过程中,我发现AI巡逻系统最容易出现的问题是路径点索引越界。解决方案是使用模运算确保索引始终在有效范围内,如currentWaypoint = (currentWaypoint + 1) % waypoints.Count。另一个常见问题是角色控制器在斜坡上滑动,这需要通过调整物理材质或增加额外的地面检测射线来解决。