多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity 3D对战游戏开发:战壕阵地战角色控制与战斗系统实战

Unity 3D对战游戏开发:战壕阵地战角色控制与战斗系统实战 最近在开发3D游戏时常常遇到一个核心挑战如何让玩家在复杂地形如战壕中既能体验到紧张刺激的阵地攻防又能感受到角色比如我们的“橙青球球”之间宿命对决的戏剧张力。这不仅仅是模型和动画的问题更涉及到游戏逻辑、物理交互、状态同步等一系列工程难题。本文将以一个虚构的3D对战游戏项目“Lost Marbles3D”第53季版本为背景深入拆解一套从场景搭建、角色控制到网络同步的完整实战方案。无论你是刚接触Unity/Unreal等3D引擎的初学者还是希望优化现有对战玩法的开发者都能从中获得可直接复用的代码模块和避坑指南。我们将重点关注战壕地形构建、双阵营球球角色控制、命中与伤害判定以及简单的状态同步思路。1. 项目背景与核心设计“Lost Marbles3D”是一个快节奏的3D弹射对战游戏概念。在第53季“战壕阵地战-橙青球球的宿命对决”中我们设定了以下核心玩法阵营与角色玩家分为“橙球”和“青球”两个阵营每个玩家控制一个具有弹射移动能力的球球角色。战场环境地图核心区域为复杂的“战壕”系统包含掩体、高低差、通道为阵地攻防提供战术空间。核心对决球球通过弹射撞击或发射“能量弹”攻击对手。被命中会积累“震荡值”值满则被“击飞”出战场短暂等待后重生。赛季目标在限定时间内击飞对方阵营成员次数多的队伍获胜。从技术角度看本项目主要涉及以下几个模块3D场景与地形使用ProBuilder或手动建模构建战壕。角色控制器实现基于物理Rigidbody的弹射、移动、跳跃。战斗系统碰撞检测、伤害计算、状态管理健康/震荡值。游戏管理器处理游戏规则、胜负判定、重生逻辑。基础网络功能可选为多人对战提供简单的状态同步示例。2. 环境准备与项目结构我们选择Unity 2022.3 LTS作为演示引擎因为其物理引擎和网络API较为成熟且资源丰富。当然核心逻辑在Unreal等引擎中也可以类似实现。所需环境Unity Hub及Unity Editor 2022.3.x或更高版本。Visual Studio 2022或JetBrains Rider作为代码编辑器。初始项目设置打开Unity Hub创建一个新的3D核心模板项目命名为LostMarbles3D_Season53。进入项目后先规划文件夹结构保持良好的工程习惯Assets/ ├── _Scripts/ │ ├── Characters/ │ │ ├── MarblePlayerController.cs │ │ ├── MarbleCombat.cs │ │ └── MarbleHealth.cs │ ├── Gameplay/ │ │ ├── GameManager.cs │ │ ├── SpawnPoint.cs │ │ └── Projectile.cs │ └── Utilities/ │ └── Singleton.cs ├── _Prefabs/ │ ├── PlayerMarble_Orange.prefab │ ├── PlayerMarble_Blue.prefab │ └── Projectile.prefab ├── _Scenes/ │ └── Main_TrenchWarfare.unity ├── _Materials/ ├── _Models/ ├── _Physics Materials/ └── _Audio/关键包导入通过Package ManagerProBuilder用于快速原型化战壕地形。Input System新的输入系统处理玩家操作更灵活。3. 战壕地形构建与场景搭建战壕是本次玩法的核心。我们使用ProBuilder进行快速构建。3.1 使用ProBuilder创建基础战壕在Unity编辑器中选择Window - Package Manager安装ProBuilder。安装后顶部菜单栏会出现Tools - ProBuilder。创建一个空GameObject命名为TrenchNetwork。选中它在ProBuilder窗口中选择Cube工具在场景中拖拽创建一个长方体。进入ProBuilder的编辑模式CtrlE选中长方体的顶面使用ExtrudeE工具向下挤压形成一条沟壑。通过移动顶点、边塑造出曲折、有掩体的战壕通道。重复此过程构建一个包含多条交错战壕、瞭望点、地下掩体的网络。记得为战壕底部和墙壁应用不同的材质以区分。3.2 添加物理与碰撞ProBuilder创建的模型默认带有Mesh Collider。对于复杂静态地形这可能会影响性能。一个优化方案是为TrenchNetwork对象添加Mesh Collider。在Inspector中考虑勾选Convex并尝试使用Cooking Options简化碰撞体或者更优的方法是使用简单的Box Collider和Capsule Collider组合来近似替代复杂地形的碰撞这对于仅由基础几何体构成的战壕是可行的。// 这是一个概念性步骤实际在编辑器内完成。 // 确保所有战壕部分都有合适的Collider并且Is Trigger为false。 // 为战壕边缘添加防止掉落的“隐形墙”使用带Collider的薄Cube。3.3 设置出生点与区域在战壕的两端以及地图中的安全区域创建空GameObject作为出生点SpawnPoint。创建空GameObject命名为Spawn_Orange_01为其添加一个脚本SpawnPoint.cs并设置阵营。将其Tag设置为Respawn或自定义如SpawnPoint方便GameManager查找。// File: Assets/_Scripts/Gameplay/SpawnPoint.cs using UnityEngine; public class SpawnPoint : MonoBehaviour { public enum Team { Orange, Blue } public Team teamAffiliation; // 可视化提示在Scene视图显示 void OnDrawGizmos() { Gizmos.color teamAffiliation Team.Orange ? Color.red : Color.blue; Gizmos.DrawWireSphere(transform.position, 1f); Gizmos.DrawIcon(transform.position, SpawnPoint, true); } }4. 球球角色控制器实现这是游戏操作感的核心。我们将实现一个基于物理力的弹射移动控制器。4.1 创建球球预制体在场景中创建一个Sphere球体命名为PlayerMarble_Orange。移除默认的Sphere Collider添加Capsule Collider更适合滚动物理或保留Sphere Collider并调整半径。添加Rigidbody组件。关键参数设置Mass: 5Drag: 1 (增加移动阻力防止无限滑动)Angular Drag: 5 (增加旋转阻力)Constraints: Freeze Rotation on X and Z (可选防止奇怪翻滚)创建材质赋予其橙色。将其拖入Assets/_Prefabs文件夹生成预制体。同理创建PlayerMarble_Blue。4.2 编写玩家控制脚本我们使用新的Input System。首先创建输入Actions。在Project窗口右键Create - Input Actions命名为PlayerControls。双击打开定义Action Maps和Actions。例如Action Map:PlayerActions:Move(Value, Vector2) - 绑定到WASD/左摇杆。Jump(Button) - 绑定到空格键/游戏手柄South按钮。ChargeShot(Button) - 绑定到鼠标左键/手柄RT。Look(Value, Vector2) - 绑定到鼠标Delta/右摇杆。保存并生成C#类在Asset Inspector中点击Generate C# Class。现在编写控制器脚本// File: Assets/_Scripts/Characters/MarblePlayerController.cs using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(Rigidbody))] public class MarblePlayerController : MonoBehaviour { [Header(Movement Settings)] public float moveForce 15f; public float jumpForce 10f; public float maxSpeed 10f; public float groundCheckDistance 0.6f; public LayerMask groundLayer; [Header(Charge Shot)] public GameObject projectilePrefab; public Transform shotSpawnPoint; public float minChargeTime 0.5f; public float maxChargeTime 2.0f; public float maxShotForce 30f; private float currentChargeTime 0f; private bool isCharging false; // Components private Rigidbody rb; private PlayerControls controls; private Vector2 moveInput; private bool isGrounded; void Awake() { rb GetComponentRigidbody(); controls new PlayerControls(); // 绑定输入回调 controls.Player.Move.performed ctx moveInput ctx.ReadValueVector2(); controls.Player.Move.canceled ctx moveInput Vector2.zero; controls.Player.Jump.performed ctx TryJump(); controls.Player.ChargeShot.performed ctx StartCharging(); controls.Player.ChargeShot.canceled ctx ReleaseShot(); } void OnEnable() controls.Player.Enable(); void OnDisable() controls.Player.Disable(); void FixedUpdate() { // 地面检测 isGrounded Physics.Raycast(transform.position, Vector3.down, groundCheckDistance, groundLayer); // 移动 Vector3 moveDirection new Vector3(moveInput.x, 0, moveInput.y); if (moveDirection.magnitude 0.1f isGrounded) { // 将输入方向转换到世界空间假设摄像机俯视或跟随 // 简单起见这里假设摄像机旋转为0 Vector3 worldMove transform.TransformDirection(moveDirection); rb.AddForce(worldMove * moveForce, ForceMode.Force); // 限制最大速度 Vector3 horizontalVel new Vector3(rb.velocity.x, 0, rb.velocity.z); if (horizontalVel.magnitude maxSpeed) { horizontalVel horizontalVel.normalized * maxSpeed; rb.velocity new Vector3(horizontalVel.x, rb.velocity.y, horizontalVel.z); } } // 蓄力过程 if (isCharging) { currentChargeTime Time.fixedDeltaTime; // 这里可以添加视觉反馈比如改变球体颜色或缩放 } } void TryJump() { if (isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } void StartCharging() { if (!isCharging) { isCharging true; currentChargeTime 0f; Debug.Log(开始蓄力...); } } void ReleaseShot() { if (isCharging projectilePrefab shotSpawnPoint) { isCharging false; float chargeRatio Mathf.Clamp01(currentChargeTime / maxChargeTime); if (chargeRatio * maxChargeTime minChargeTime) // 达到最小蓄力时间 { FireProjectile(chargeRatio); } currentChargeTime 0f; } } void FireProjectile(float chargeRatio) { GameObject proj Instantiate(projectilePrefab, shotSpawnPoint.position, shotSpawnPoint.rotation); Rigidbody projRb proj.GetComponentRigidbody(); if (projRb) { float force Mathf.Lerp(10f, maxShotForce, chargeRatio); // 蓄力越久力越大 projRb.AddForce(shotSpawnPoint.forward * force, ForceMode.Impulse); } // 可以在这里添加发射音效和后坐力效果 // rb.AddForce(-shotSpawnPoint.forward * force * 0.1f, ForceMode.Impulse); // 后坐力 } void OnDrawGizmosSelected() { Gizmos.color Color.green; Gizmos.DrawLine(transform.position, transform.position Vector3.down * groundCheckDistance); } }4.3 摄像机跟随创建一个简单的摄像机跟随脚本让摄像机平滑跟随球球。// File: Assets/_Scripts/Utilities/SimpleCameraFollow.cs using UnityEngine; public class SimpleCameraFollow : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 10, -10); // 俯视角偏移 public float smoothSpeed 0.125f; void LateUpdate() { if (target null) return; Vector3 desiredPosition target.position offset; Vector3 smoothedPosition Vector3.Lerp(transform.position, desiredPosition, smoothSpeed); transform.position smoothedPosition; transform.LookAt(target); } }将脚本挂载到主摄像机上并将球球预制体拖入Target字段。5. 战斗与生命值系统球球之间的对决需要伤害判定。我们为球球添加生命值震荡值组件。5.1 生命值震荡值组件// File: Assets/_Scripts/Characters/MarbleHealth.cs using UnityEngine; using UnityEngine.Events; public class MarbleHealth : MonoBehaviour { public enum Team { Orange, Blue } public Team myTeam; [Header(Health Settings)] public float maxShockValue 100f; private float currentShockValue; public float shockDecayRate 5f; // 每秒衰减值 [Header(Events)] public UnityEventfloat OnShockValueChanged; // 参数当前震荡值百分比 public UnityEvent OnShockFull; // 震荡值满时触发被击飞 public UnityEvent OnRespawn; // 重生时触发 private bool isAlive true; void Start() { currentShockValue 0f; isAlive true; } void Update() { // 非战斗状态下震荡值缓慢衰减 if (currentShockValue 0 isAlive) { currentShockValue - shockDecayRate * Time.deltaTime; currentShockValue Mathf.Max(currentShockValue, 0); OnShockValueChanged?.Invoke(currentShockValue / maxShockValue); } } // 被攻击时调用 public void TakeShock(float shockAmount, Vector3 hitDirection) { if (!isAlive) return; currentShockValue shockAmount; currentShockValue Mathf.Min(currentShockValue, maxShockValue); OnShockValueChanged?.Invoke(currentShockValue / maxShockValue); // 可以添加受击视觉效果如屏幕抖动、颜色闪烁 // 检查是否被击飞 if (currentShockValue maxShockValue) { GetKnockedOut(hitDirection); } } void GetKnockedOut(Vector3 direction) { isAlive false; Debug.Log(gameObject.name 被击飞); OnShockFull?.Invoke(); // 1. 禁用控制播放击飞动画/特效 var controller GetComponentMarblePlayerController(); if (controller) controller.enabled false; // 2. 施加一个击飞力 var rb GetComponentRigidbody(); if (rb) rb.AddForce(direction.normalized * 15f Vector3.up * 10f, ForceMode.Impulse); // 3. 通知GameManager安排重生 GameManager.Instance?.PlayerKnockedOut(this); } public void Respawn(Vector3 position) { currentShockValue 0f; isAlive true; OnShockValueChanged?.Invoke(0f); // 重置位置和物理状态 transform.position position; var rb GetComponentRigidbody(); if (rb) { rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; } // 重新启用控制器 var controller GetComponentMarblePlayerController(); if (controller) controller.enabled true; OnRespawn?.Invoke(); } }5.2 投射物与伤害触发创建子弹预制体Projectile并为其添加碰撞检测脚本。// File: Assets/_Scripts/Gameplay/Projectile.cs using UnityEngine; public class Projectile : MonoBehaviour { public float baseShockDamage 25f; public float lifeTime 5f; public GameObject hitEffectPrefab; private Team shooterTeam; // 需要由发射者设置 public void SetShooterTeam(Team team) { shooterTeam team; } void Start() { Destroy(gameObject, lifeTime); } void OnCollisionEnter(Collision collision) { // 检查是否击中玩家球球 MarbleHealth health collision.gameObject.GetComponentMarbleHealth(); if (health ! null) { // 避免击中队友可选规则 if (health.myTeam ! shooterTeam) { // 计算伤害这里简单使用基础值 Vector3 hitDirection (collision.transform.position - transform.position).normalized; health.TakeShock(baseShockDamage, hitDirection); } } // 播放击中特效 if (hitEffectPrefab) { Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); } // 销毁子弹 Destroy(gameObject); } }需要修改MarblePlayerController的FireProjectile方法在实例化子弹后设置发射者阵营。// 在FireProjectile方法内实例化子弹后添加 Projectile projScript proj.GetComponentProjectile(); if (projScript) { // 需要从MarbleHealth组件获取当前球球的阵营 MarbleHealth myHealth GetComponentMarbleHealth(); if (myHealth) { projScript.SetShooterTeam(myHealth.myTeam); } }6. 游戏逻辑管理器GameManager作为单例负责全局规则。// File: Assets/_Scripts/Gameplay/GameManager.cs using System.Collections.Generic; using UnityEngine; public class GameManager : SingletonGameManager { [System.Serializable] public class TeamInfo { public MarbleHealth.Team team; public int score; public ListMarbleHealth players new ListMarbleHealth(); } public ListTeamInfo teams new ListTeamInfo(); public float matchDuration 300f; // 5分钟 private float matchTimer; private bool matchActive false; public ListSpawnPoint spawnPoints new ListSpawnPoint(); protected override void Awake() { base.Awake(); // 初始化队伍信息 foreach (var team in System.Enum.GetValues(typeof(MarbleHealth.Team))) { teams.Add(new TeamInfo { team (MarbleHealth.Team)team, score 0 }); } } void Start() { FindAllSpawnPoints(); StartMatch(); } void FindAllSpawnPoints() { spawnPoints.Clear(); spawnPoints.AddRange(FindObjectsOfTypeSpawnPoint()); } void StartMatch() { matchTimer matchDuration; matchActive true; Debug.Log(比赛开始); // 这里可以初始化玩家分配出生点等 } void Update() { if (!matchActive) return; matchTimer - Time.deltaTime; if (matchTimer 0) { EndMatch(); } } public void PlayerKnockedOut(MarbleHealth player) { // 为对方队伍加分 TeamInfo opponentTeam teams.Find(t t.team ! player.myTeam); if (opponentTeam ! null) { opponentTeam.score; Debug.Log(${opponentTeam.team} 队得分当前比分: Orange {GetTeamScore(MarbleHealth.Team.Orange)} - Blue {GetTeamScore(MarbleHealth.Team.Blue)}); } // 安排重生 StartCoroutine(RespawnPlayerAfterDelay(player, 3f)); } private System.Collections.IEnumerator RespawnPlayerAfterDelay(MarbleHealth player, float delay) { yield return new WaitForSeconds(delay); SpawnPoint spawn GetRandomSpawnForTeam(player.myTeam); if (spawn ! null) { player.Respawn(spawn.transform.position); } } SpawnPoint GetRandomSpawnForTeam(MarbleHealth.Team team) { ListSpawnPoint teamSpawns spawnPoints.FindAll(s s.teamAffiliation.ToString() team.ToString()); if (teamSpawns.Count 0) { return teamSpawns[Random.Range(0, teamSpawns.Count)]; } return null; } int GetTeamScore(MarbleHealth.Team team) { TeamInfo teamInfo teams.Find(t t.team team); return teamInfo?.score ?? 0; } void EndMatch() { matchActive false; Debug.Log(比赛结束); // 判断胜负显示结算UI等 } } // 简单的单例基类 // File: Assets/_Scripts/Utilities/Singleton.cs public abstract class SingletonT : MonoBehaviour where T : MonoBehaviour { private static T instance; public static T Instance { get { if (instance null) { instance FindObjectOfTypeT(); if (instance null) { GameObject obj new GameObject(typeof(T).Name); instance obj.AddComponentT(); } } return instance; } } protected virtual void Awake() { if (instance null) { instance this as T; DontDestroyOnLoad(gameObject); } else if (instance ! this) { Destroy(gameObject); } } }7. 常见问题与排查思路在实现上述系统时你可能会遇到以下典型问题问题现象可能原因排查与解决思路球球移动滑动严重停不下来Rigidbody的Drag或Angular Drag值过低地面物理材质摩擦力太小。1. 增大Rigidbody的Drag如1-5。2. 检查地面Collider为其添加一个Physics Material调整Dynamic Friction和Static Friction。球球跳跃后在空中可以连续移动地面检测groundCheckDistance过小或groundLayer设置错误。1. 在OnDrawGizmosSelected中可视化检测射线确保长度合适。2. 确保球球和地面处于正确的Layer并在脚本的groundLayer中勾选该Layer。发射的子弹无法触发伤害Projectile脚本的OnCollisionEnter未被调用碰撞体设置问题。1. 确保子弹预制体有Rigidbody和Collider如Sphere Collider。2. 检查子弹的Rigidbody是否勾选Is Kinematic不应勾选。3. 确保MarbleHealth组件挂在玩家球球根物体上。被击飞后没有重生GameManager的单例未正确初始化SpawnPoint未找到或阵营不匹配。1. 检查场景中是否存在GameManager对象或Singleton的Awake逻辑。2. 在编辑器查看SpawnPoint的Gizmos图标确认其teamAffiliation设置正确。3. 在GetRandomSpawnForTeam方法中添加Debug.Log打印找到的出生点列表。输入无响应Input System的Action Map未启用输入绑定冲突。1. 在控制器脚本的OnEnable/OnDisable中确认已调用controls.Player.Enable()。2. 在Unity编辑器中打开PlayerControls资产检查Action的绑定路径是否正确。游戏运行卡顿每帧实例化/销毁大量子弹复杂的Mesh Collider。1. 为子弹实现对象池Object Pooling避免频繁的Instantiate/Destroy。2. 将战壕地形的Mesh Collider替换为简单的Box/Capsule Collider组合。8. 扩展思路与最佳实践以上实现了一个可运行的核心原型。要将其完善为一个真正的“第53季”版本可以考虑以下扩展和优化1. 网络同步多人对战使用Unity的Netcode for GameObjectsNGO或Photon PUN等网络库。将MarblePlayerController、MarbleHealth中的关键变量位置、旋转、速度、震荡值进行网络同步。射击动作和伤害判定需要在服务端进行权威验证防止作弊。2. 更丰富的战斗系统技能系统为橙球和青球设计独特的主动技能如瞬间加速、放置陷阱、治疗光环。装备系统允许玩家在战前选择不同的“弹射核心”影响移动力、伤害或防御。环境互动战壕中可放置临时屏障、触发地雷等。3. 视觉与音频增强角色特效蓄力时球体发光、受击时屏幕边缘泛红、被击飞时的拖尾和爆炸特效。音效设计移动摩擦声、蓄力音调升高、发射音效、击中不同材质的反馈音、被击飞的惊呼声。UI界面实时显示震荡值进度条、队伍比分、击杀提示、小地图显示战壕布局和队友位置。4. 性能优化对象池对子弹、击中特效、UI伤害数字等频繁生成销毁的对象务必使用对象池。遮挡剔除对于复杂的战壕地形合理设置遮挡区域减少渲染负担。物理优化将静态战壕地形设置为Static使用Physics Layers精细控制碰撞交互避免不必要的物理计算。5. 代码结构优化状态模式将玩家的“移动”、“蓄力”、“被击飞”、“重生”等行为用状态机管理使逻辑更清晰。事件系统使用UnityEvent或自定义事件总线Event Bus来解耦模块。例如得分、玩家死亡等事件通过事件广播UI、音效管理器监听这些事件并做出反应而不是直接调用。配置数据化将角色的移动速度、伤害值、技能冷却等平衡性数据放入ScriptableObject方便策划调整无需修改代码。通过以上步骤你不仅能够搭建出“Lost Marbles3D/战壕阵地战”的核心玩法更能掌握一套构建3D对战游戏的通用方法和工程化思维。从场景搭建、物理控制到游戏逻辑和状态管理每一个环节都是3D游戏开发中不可或缺的技能。动手尝试修改参数、添加新功能比如为青球设计一个“闪现”技能或者为橙球设计一个“滚石冲击”的大招真正创造出属于你的“宿命对决”。
返回列表