ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity第三人称射击游戏开发:从核心框架到性能优化实战

Unity第三人称射击游戏开发:从核心框架到性能优化实战 1. 项目概述从零到一构建一个可玩的TPS核心框架最近在整理硬盘翻出来一个几年前做的Unity第三人称射击游戏Demo。当时是为了验证一套快速搭建TPS核心玩法的框架从场景搭建、角色控制到敌人AI和武器系统都走了一遍。虽然项目不大但麻雀虽小五脏俱全涵盖了从输入处理、动画状态机、物理交互到资源管理的多个核心模块。今天我就把这个项目的核心实现思路和关键代码拆解出来希望能给想入门Unity游戏开发特别是对动作射击类游戏感兴趣的朋友一个清晰的路线图。这个演示项目不追求华丽的画面和复杂的剧情重点在于构建一个稳定、可扩展且手感扎实的第三人称射击游戏基础框架让你理解每个功能模块是如何串联起来的。很多人一上来就想做3A大作结果在复杂的动画融合、高级渲染效果上卡住反而忽略了游戏最基础的“可玩性”是如何实现的。我这个项目就是从“可玩性”出发优先解决“移动-瞄准-射击-受击”这个核心循环。无论你是想学习Unity还是准备面试毕竟Unity面试题里状态机、对象池、寻路AI都是高频考点或者单纯想了解一个完整游戏项目是如何组织代码和资源的接下来的内容都会对你有所帮助。我会尽量用通俗的语言解释原理并提供可以直接“抄作业”的代码片段和设置方法。2. 核心系统设计与架构思路一个第三人称射击游戏可以拆解成几个相对独立又紧密协作的系统。在项目初期清晰的架构设计能避免后期代码和资源的混乱。我的设计思路是采用一种基于组件的模块化架构每个核心功能由一个或多个MonoBehaviour脚本组件负责通过GameObject进行组装。2.1 玩家控制系统输入、移动与摄像机玩家控制是TPS的基石手感好坏直接决定游戏体验。我将其分解为三个部分输入处理、角色移动和摄像机跟随。输入处理早期我使用Unity旧的Input Manager但后来全面转向了新的Input System。新的Input System支持多设备、重绑定并且更易于管理复杂的输入组合。我创建了一个PlayerInputActions的Input Action Asset在里面定义“Move”WASD/摇杆、“Look”鼠标/右摇杆、“Fire”鼠标左键/RT、“Aim”鼠标右键/LT、“Reload”R等动作。在代码中通过PlayerInput组件或InputAction的C#接口来读取这些输入值。注意使用Input System时记得在Project Settings的Player设置中将“Active Input Handling”设置为“Both”或“Input System Package (New)”并在代码中启用对应的输入处理模式否则可能接收不到输入事件。角色移动移动逻辑写在PlayerMovement组件中。它接收“Move”输入的二维向量一个方向结合角色当前面向通常由摄像机或瞄准方向决定计算出世界空间下的移动方向。移动本身我使用了CharacterController组件因为它内置了与地形和其他CharacterController的简单碰撞处理比刚体物理更稳定更适合需要精确控制的玩家角色。代码中会调用CharacterController.SimpleMove或Move方法并处理重力Physics.gravity和跳跃逻辑。public class PlayerMovement : MonoBehaviour { public float moveSpeed 5f; public float sprintSpeed 8f; public float rotationSpeed 10f; private CharacterController controller; private Vector3 moveDirection; private float verticalVelocity 0f; public float gravity -9.81f; private bool isSprinting false; void Start() { controller GetComponentCharacterController(); } void Update() { // 从Input System获取输入 Vector2 input playerInput.actions[Move].ReadValueVector2(); isSprinting playerInput.actions[Sprint].IsPressed(); float currentSpeed isSprinting ? sprintSpeed : moveSpeed; // 计算移动方向基于摄像机朝向 Vector3 camForward mainCamera.transform.forward; Vector3 camRight mainCamera.transform.right; camForward.y 0; camRight.y 0; camForward.Normalize(); camRight.Normalize(); moveDirection (camForward * input.y camRight * input.x).normalized; // 处理重力 if (controller.isGrounded) { verticalVelocity -0.5f; // 给一个小的向下的力保持贴地 if (playerInput.actions[Jump].triggered) { verticalVelocity Mathf.Sqrt(jumpHeight * -2f * gravity); } } else { verticalVelocity gravity * Time.deltaTime; } Vector3 velocity moveDirection * currentSpeed Vector3.up * verticalVelocity; controller.Move(velocity * Time.deltaTime); // 角色朝向移动方向平滑旋转 if (moveDirection ! Vector3.zero) { Quaternion targetRotation Quaternion.LookRotation(moveDirection); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } } }摄像机跟随这是实现第三人称视角的关键。我使用了一个经典的“弹簧臂Spring Arm”模式即摄像机挂载在一个空物体CameraRig下这个空物体再跟随玩家。摄像机本身通过Cinemachine这个官方插件来实现它极大地简化了复杂摄像机行为的配置。我创建了一个CinemachineFreeLook虚拟相机将其Follow目标设为玩家Look At目标可以设为玩家或玩家前方的某个点。通过调整其轨道Orbits半径、高度和阻尼Damping设置可以轻松实现平滑的跟随、镜头拉近拉远瞄准时以及避免穿墙的碰撞检测Cinemachine Collider扩展。2.2 武器与射击系统从数据到表现武器系统是TPS的乐趣核心。我设计了一个Weapon基类然后派生出Rifle、Pistol等具体武器类。武器数据如伤害、射速、弹匣容量、扩散、后坐力模式使用ScriptableObject来创建这样策划可以方便地配置和平衡而无需修改代码。射击逻辑当按下开火键时Weapon脚本会检查冷却时间、弹药数量然后执行射击。射击的核心是射线检测Raycast。从摄像机中心屏幕中心或枪口发射一条射线检测击中的物体。如果是敌人则调用其受伤接口。为了表现弹道我会同时实例化一个子弹轨迹特效LineRenderer或子弹预制体。这里对象池Object Pool是必须的频繁实例化销毁子弹和特效是性能杀手。public class Weapon : MonoBehaviour { public WeaponData weaponData; // ScriptableObject public Transform muzzleTransform; public ParticleSystem muzzleFlash; private float nextFireTime 0f; private int currentAmmo; void Start() { currentAmmo weaponData.magazineCapacity; } public bool TryShoot() { if (Time.time nextFireTime) return false; if (currentAmmo 0) { // 触发空枪音效或UI提示 return false; } nextFireTime Time.time weaponData.fireRate; currentAmmo--; // 播放枪口火焰、音效 muzzleFlash.Play(); AudioManager.Instance.PlaySound(weaponData.fireSound); // 射线检测 Ray ray Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); RaycastHit hit; if (Physics.Raycast(ray, out hit, weaponData.range, weaponData.hitLayer)) { // 命中处理 IDamageable damageable hit.collider.GetComponentIDamageable(); if (damageable ! null) { damageable.TakeDamage(weaponData.damage, hit.point); } // 在命中点生成命中特效血花、火花 ObjectPool.Instance.SpawnFromPool(HitEffect, hit.point, Quaternion.LookRotation(hit.normal)); } // 后坐力可以通过抖动摄像机或武器模型来实现 ApplyRecoil(); return true; } public void Reload() { // 播放换弹动画和音效动画事件在末尾调用实际装填弹药的方法 StartCoroutine(ReloadCoroutine()); } IEnumerator ReloadCoroutine() { // 等待换弹动画时间 yield return new WaitForSeconds(weaponData.reloadTime); currentAmmo weaponData.magazineCapacity; // 更新UI UIManager.Instance.UpdateAmmoUI(currentAmmo, weaponData.magazineCapacity); } }后坐力与扩散为了增加真实感和操作难度我实现了简单的后坐力系统。每次射击后会给摄像机一个向上的力垂直后坐力和一个随机的水平力。通过一个Lerp或SmoothDamp函数随时间恢复就能模拟出枪口上跳和回复的过程。扩散则通过给射线起点添加一个随机的球面偏移来实现扩散大小与角色移动、连续射击次数相关。2.3 敌人AI感知、决策与行为没有敌人的射击游戏是没有灵魂的。我使用Unity自带的NavMesh系统来处理敌人的寻路。首先在场景中烘焙导航网格NavMesh然后为敌人添加NavMeshAgent组件。敌人的AI逻辑采用一个简单的有限状态机FSM来实现通常包含以下几个状态巡逻Patrol在预设的几个点之间随机或顺序移动。追击Chase当玩家进入警戒范围时切换到该状态使用NavMeshAgent.SetDestination()追逐玩家。攻击Attack当玩家进入攻击范围且视线无遮挡时停止移动播放攻击动画并调用攻击逻辑如发射子弹或近战伤害判定。死亡Die生命值归零后播放死亡动画禁用NavMeshAgent和碰撞体一段时间后销毁或回收到对象池。感知系统发现玩家可以通过在敌人身上挂载一个球体碰撞体作为“视觉范围”触发器或者在Update中使用Physics.OverlapSphere检测并结合Raycast检查视线是否被遮挡。public class EnemyAI : MonoBehaviour { public enum AIState { Patrol, Chase, Attack, Dead } public AIState currentState AIState.Patrol; public Transform[] patrolPoints; private int currentPatrolIndex 0; private NavMeshAgent agent; public float sightRange 10f; public float attackRange 3f; private Transform playerTarget; void Start() { agent GetComponentNavMeshAgent(); playerTarget GameObject.FindGameObjectWithTag(Player).transform; GoToNextPatrolPoint(); } void Update() { if (currentState AIState.Dead) return; float distanceToPlayer Vector3.Distance(transform.position, playerTarget.position); switch (currentState) { case AIState.Patrol: // 检查是否发现玩家 if (distanceToPlayer sightRange HasLineOfSight()) { currentState AIState.Chase; break; } // 检查是否到达巡逻点 if (!agent.pathPending agent.remainingDistance 0.5f) { GoToNextPatrolPoint(); } break; case AIState.Chase: agent.SetDestination(playerTarget.position); if (distanceToPlayer attackRange) { currentState AIState.Attack; agent.isStopped true; // 停止移动准备攻击 } else if (distanceToPlayer sightRange * 1.2f) // 加入一点滞后防止频繁切换 { currentState AIState.Patrol; GoToNextPatrolPoint(); } break; case AIState.Attack: // 面向玩家 Vector3 lookPos playerTarget.position - transform.position; lookPos.y 0; transform.rotation Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(lookPos), Time.deltaTime * 5f); // 执行攻击逻辑例如通过动画事件触发 // ... if (distanceToPlayer attackRange) { currentState AIState.Chase; agent.isStopped false; } break; } } bool HasLineOfSight() { RaycastHit hit; Vector3 direction (playerTarget.position - transform.position).normalized; if (Physics.Raycast(transform.position Vector3.up, direction, out hit, sightRange)) { if (hit.transform.CompareTag(Player)) { return true; } } return false; } void GoToNextPatrolPoint() { if (patrolPoints.Length 0) return; agent.SetDestination(patrolPoints[currentPatrolIndex].position); currentPatrolIndex (currentPatrolIndex 1) % patrolPoints.Length; } public void TakeDamage(float damage) { // 处理受伤可能触发被击动画或状态切换 // 生命值降至0时切换到Dead状态 // currentState AIState.Dead; // agent.isStopped true; // PlayDeathAnimation(); } }2.4 动画状态机与角色表现无论是玩家还是敌人流畅的动画都是沉浸感的关键。Unity的Animator Controller和动画状态机是管理复杂动画逻辑的利器。对于玩家角色我创建了一个Animator Controller包含以下主要状态Idle待机Walk行走Run奔跑Aim瞄准- 通常是一个Layer用于混合上半身瞄准动画Shoot射击- 也是一个Layer或Sub-State Machine触发单次射击动画Reload换弹Jump跳跃及其落地动画通过脚本如PlayerMovement、Weapon设置Animator的Parameters如Speed、IsGrounded、IsAiming、TriggerReload来控制状态切换。动画层Layers和遮罩Avatar Masks在这里非常有用可以让我们实现“下半身移动上半身瞄准射击”的混合效果。对于敌人动画状态机通常与AI状态机对应有Idle、Walk、Run、Attack、HitReact、Die等状态。实操心得在设置动画过渡时合理使用“Exit Time”和“Fixed Duration”可以让过渡更自然。对于需要立即响应的动作如受击、死亡使用Trigger参数并设置过渡时间为0。另外善用动画事件Animation Events可以在动画的特定帧触发游戏逻辑比如在射击动画的某一帧实际生成子弹在换弹动画末尾完成弹药装填这比用计时器更精确。3. 关键实现细节与性能优化当核心系统跑通后项目的稳定性和流畅度就成为了重点。以下是我在项目中处理的一些关键细节和优化点。3.1 资源管理与对象池射击游戏会频繁生成子弹、弹壳、命中特效、敌人等对象。如果每次都使用Instantiate和Destroy会产生大量的内存分配和垃圾回收GC导致游戏卡顿。对象池Object Pool是解决这个问题的标准方案。我实现了一个简单的泛型对象池管理器。它的核心是一个QueueGameObject或ListGameObject用于存放已创建但未使用的对象。当需要生成对象时先从池子里取如果池子不为空取出的对象设为Active并放到指定位置当对象需要“销毁”时如子弹命中后并不真正Destroy它而是将其设为Inactive并放回池子。public class ObjectPool : MonoBehaviour { public static ObjectPool Instance; [System.Serializable] public class Pool { public string tag; public GameObject prefab; public int size; } public ListPool pools; private Dictionarystring, QueueGameObject poolDictionary; void Awake() { Instance this; poolDictionary new Dictionarystring, QueueGameObject(); foreach (Pool pool in pools) { QueueGameObject objectPool new QueueGameObject(); for (int i 0; i pool.size; i) { GameObject obj Instantiate(pool.prefab); obj.SetActive(false); objectPool.Enqueue(obj); } poolDictionary.Add(pool.tag, objectPool); } } public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogWarning(Pool with tag tag doesnt exist.); return null; } GameObject objectToSpawn; if (poolDictionary[tag].Count 0) { objectToSpawn poolDictionary[tag].Dequeue(); } else { // 如果池子空了动态扩容实例化一个新的 Pool pool pools.Find(p p.tag tag); objectToSpawn Instantiate(pool.prefab); } objectToSpawn.SetActive(true); objectToSpawn.transform.position position; objectToSpawn.transform.rotation rotation; IPooledObject pooledObj objectToSpawn.GetComponentIPooledObject(); if (pooledObj ! null) { pooledObj.OnObjectSpawn(); // 对象被取出时的初始化 } return objectToSpawn; } public void ReturnToPool(string tag, GameObject objectToReturn) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogWarning(Pool with tag tag doesnt exist.); Destroy(objectToReturn); return; } objectToReturn.SetActive(false); poolDictionary[tag].Enqueue(objectToReturn); } } // 被池化对象需要实现的接口 public interface IPooledObject { void OnObjectSpawn(); }对于子弹我让子弹预制体挂载一个脚本在OnEnable从池中取出时初始化速度和方向在碰撞或飞行一定时间后调用ObjectPool.Instance.ReturnToPool将自己放回池中而不是Destroy。3.2 UI系统生命值、弹药与准星清晰的UI是玩家获取游戏信息的关键。我使用Unity的UGUI系统来构建HUD。生命值/护甲条使用Slider组件将其value与玩家的当前生命值绑定。为了平滑变化我通常会用协程Coroutine或Mathf.SmoothDamp来过渡而不是直接设置。弹药显示在屏幕角落显示当前弹匣弹药和总剩余弹药。当武器开火或换弹时通过事件通知UIManager更新文本。准星准星不仅仅是屏幕中心的一个十字。我实现了动态准星当玩家移动、跳跃或连续射击时准星的几个部分上下左右四个线段或点会向外扩散当玩家静止或瞄准时准星会收缩。这通过调整几个UI Image的rectTransform.anchoredPosition来实现扩散程度与一个“当前扩散值”变量相关这个变量由玩家的状态移动速度、是否开火等计算得出。敌人血条这是世界空间UI。为每个敌人创建一个CanvasRender Mode设置为“World Space”下面放一个Slider作为血条。将这个Canvas作为敌人的子物体并写一个脚本EnemyHealthBar在Update中让血条始终面向摄像机transform.LookAt(Camera.main.transform)并根据敌人的生命值更新Slider的value。3.3 音效与视觉反馈良好的视听反馈能极大提升游戏手感。音效管理我创建了一个AudioManager单例它管理多个AudioSource并提供了播放音效的静态方法如AudioManager.Instance.PlaySound(Gunshot, transform.position)。音效文件使用AudioClip数组可以随机播放数组中的一个避免同一音效重复播放显得单调。对于背景音乐和需要循环的音效如脚步声使用独立的AudioSource。屏幕特效当玩家受伤时屏幕边缘会泛红使用一个全屏的UI Image调整其Color和Alpha。这可以通过一个Post-Processing的Vignette效果或者更简单的用一个覆盖全屏的红色半透明图片在受伤时短暂显示并淡出来实现。击中反馈子弹击中不同材质敌人、墙壁、金属时播放不同的音效和粒子特效血花、火花、尘土。这可以通过在射线检测命中后检查被击中物体的Tag或Layer然后从对应的特效池中取出特效来播放。3.4 性能分析与优化实战在项目后期我使用Unity Profiler进行了性能分析并针对几个常见瓶颈做了优化Draw Call优化这是渲染性能的关键。我使用了静态合批Static Batching来处理场景中不会移动的物体如建筑、地面。在Player Settings中勾选“Static Batching”并将静态物体的Static标志勾选上。对于大量重复的物体如草丛、碎石使用GPU Instancing如果材质支持能大幅降低Draw Call。在材质的Inspector中勾选“Enable GPU Instancing”即可。物理开销不必要的Rigidbody和复杂的碰撞体是性能杀手。确保只有需要物理模拟的物体才有Rigidbody。对于静态碰撞体使用简单的几何体Box, Sphere, Capsule组合来代替复杂的Mesh Collider。对于子弹这种高速移动的物体我使用射线检测而非刚体碰撞性能更好。脚本效率避免在Update中做昂贵的操作。例如查找玩家对象GameObject.FindWithTag(Player)应该只在Start或Awake中执行一次然后缓存结果。对于敌人的感知检测如Physics.OverlapSphere不要每帧都对所有敌人执行可以降低检测频率如每0.2秒一次或者使用Physics.SphereCastNonAlloc等非分配内存的物理函数。资源内存注意纹理、音频文件的大小。使用合适的压缩格式如ASTC、ETC2。对于不再需要的资源及时使用Resources.UnloadUnusedAssets()或通过Addressable Asset System进行加载和卸载管理避免内存泄漏。4. 项目构建、发布与常见问题当游戏开发完成最后一步就是构建和发布。根据目标平台PC、WebGL、移动端的不同需要注意的细节也不同。4.1 针对不同平台的构建设置PC独立平台在Build Settings中选择“PC, Mac Linux Standalone”。需要注意屏幕分辨率、窗口模式全屏/窗口化的设置。如果使用了新的Input System确保在Player Settings的“Configuration”中选择了正确的“Active Input Handling”。WebGL这是将游戏发布到网页的平台。Unity WebGL初始化很久是一个经典问题。优化方法包括在Player Settings - Publishing Settings中启用压缩Compression Format为Brotli比Gzip更好。使用Addressables进行资源分包和按需加载减少初始加载体积。在脚本编译设置Player Settings - Other Settings中将“Code Optimization”设置为Size或Speed并考虑启用“Strip Engine Code”来移除未使用的引擎代码。将WebGL Memory Size适当调大如256MB或512MB但需注意用户浏览器内存限制。Android/iOS移动端构建需要更多优化。降低图形质量使用更简单的Shader、降低分辨率缩放、严格控制Draw Call和三角形数量。对于Android注意处理不同的屏幕比例和虚拟按键。对于iOS需要配置正确的证书和描述文件。4.2 开发中遇到的典型问题与解决方案在开发这个TPS Demo的过程中我踩过不少坑这里记录几个有代表性的问题一角色在斜坡上行走抖动或滑落现象使用CharacterController时角色在斜坡上移动不顺畅有时会轻微抖动或向下滑动。原因CharacterController的SimpleMove方法虽然自动处理了重力但其移动逻辑在斜坡上可能不够精确。Move方法更灵活但需要手动处理重力。解决方案改用CharacterController.Move()并自行计算包含重力的速度向量。同时在移动前通过Physics.SphereCast检查角色前方是否有斜坡并调整移动方向使其贴合斜坡表面即沿斜坡的切线方向移动这能有效解决抖动和滑落问题。问题二敌人AI在拐角处“卡住”或抖动现象使用NavMeshAgent的敌人在追逐玩家到墙角或狭窄通道时可能会在原地快速旋转或轻微抖动无法顺利通过。原因NavMeshAgent的路径在拐角处可能计算不够平滑或者Agent的半径Radius与拐角空间匹配不佳。解决方案适当减小NavMeshAgent的Radius并增加Height。调整NavMeshAgent的Steering参数提高Speed和Angular Speed降低Stopping Distance在追击状态下可以设为0。在烘焙NavMesh时确保拐角处的可行走区域Walkable足够宽。可以适当增加烘焙代理的Radius这样烘焙出的网格在拐角处会更“圆滑”。代码层面可以在敌人接近拐角或目标点时临时提高其转向速度。问题三射击手感“飘”或不跟手现象鼠标控制摄像机旋转时感觉有延迟或者瞄准不精准。原因输入处理、摄像机旋转和角色旋转的更新顺序或平滑处理不当。解决方案输入采样确保在Update中读取鼠标输入因为Update与帧率同步而FixedUpdate是固定物理时间步长不适合处理实时输入。摄像机平滑使用Cinemachine时调整Free Look Camera的Damping时间。时间太短会抖动太长会有延迟。通常X轴水平阻尼可以小一些如0.1-0.3Y轴垂直阻尼可以大一些如0.2-0.4以获得更稳定的跟随。鼠标灵敏度将鼠标的delta每帧移动量乘以一个可配置的灵敏度系数并考虑帧率补偿乘以Time.deltaTime但注意对于直接旋转摄像机的操作有时不乘Time.deltaTime反而更跟手这需要实测调整。帧率锁定将游戏帧率锁定在一个稳定值如60 FPS可以避免因帧率波动导致的手感不一致。在代码开始处使用Application.targetFrameRate 60;。问题四使用Addressables打包后TextMeshProTMP材质变紫现象当使用Addressable Asset System打包和加载UI或3D文本时原本正常的TMP文本材质变成了洋红色紫色。原因这是Unity资源依赖关系没有正确打包的经典问题。TMP的字体材质Material和纹理Texture Atlas是分离的资源如果只打包了字体Asset而没有将其依赖的材质和纹理图集一起打包运行时加载字体时就会找不到依赖资源导致材质丢失显示为紫色。解决方案在Addressables Groups窗口找到你的TMP字体资产。在它的Inspector面板中查看“Dependencies”列表。确保字体所依赖的Material和Texture都被包含在了同一个或可被加载到的Addressables Group中。最可靠的方法是将TMP字体及其所有直接依赖Material, Texture打到一个AssetBundle即一个Addressables Group中。你可以创建一个新的Group把这些资源都拖进去或者使用Addressables的“Analyze”工具来检查缺失的依赖。构建完成后使用Addressables的“Check for Content Update Restrictions”工具来验证依赖关系。问题五如何实现“华佗热更新”或资源热更需求在不重新发布游戏包的情况下更新游戏内的场景、模型、配置表等资源。方案Unity官方推荐的现代资源管理方案是Addressable Asset System。它完美支持热更新。标记资源将需要热更的资源Prefabs、Scenes、ScriptableObjects等标记为Addressable并分配一个唯一的地址Key。远程部署在Addressables Group的设置中将“Build Load Paths”设置为从远程服务器加载如https://your-cdn.com/addressables/[BuildTarget]。构建与上传构建Addressables时它会生成一个资源目录Catalog和一堆资源包Bundles。将这些文件上传到你的CDN服务器。客户端检查更新游戏启动时使用Addressables.CheckForCatalogUpdates()检查远程目录是否有新版本。如果有调用Addressables.UpdateCatalogs()来下载更新的Catalog和变化的资源包。加载资源之后在游戏中使用Addressables.LoadAssetAsyncGameObject(YourAssetKey)来异步加载资源系统会自动从本地缓存或远程服务器获取。注意事项热更代码C# DLL在大多数平台如iOS、主机受到严格限制通常只能热更资源。Addressables主要解决的就是资源热更问题。对于配置表如Excel转成的Json或ScriptableObject非常适合用此方案更新。这个第三人称射击游戏Demo虽然功能基础但它完整地串联起了从输入、角色控制、战斗、AI到UI、音效和资源管理的整个开发链条。每一个模块深入下去都有大量的优化和扩展空间比如更复杂的武器系统榴弹、狙击镜、更智能的敌人AI使用行为树、更炫酷的技能和特效、联机多人对战等等。但无论如何理解并实现好这个基础框架是迈向更复杂游戏开发最坚实的一步。希望这个拆解能帮你理清思路少走一些弯路。如果在实现过程中遇到其他具体问题比如如何做更流畅的动画融合、如何优化URP下的渲染那又是另一个可以深入探讨的话题了。
返回列表