简介:本资源为Unity引擎实现的《植物大战僵尸》塔防游戏完整源码项目,面向Unity初学者、游戏开发学习者及塔防类游戏机制研究者,旨在帮助读者深入理解策略游戏的核心逻辑与工程实践。项目包含多关卡设计、植物与僵尸对象管理、碰撞检测、动画状态机控制及游戏进度系统等关键模块,覆盖塔防游戏开发全流程。压缩包为RAR格式,大小127.12MB,虽文件总数未提供,但根据描述可确认含C#脚本(实现游戏逻辑与AI)、场景预制体(关卡布局)、动画控制器及资源引用配置等典型Unity工程结构。已有944人学习下载,适合通过可运行项目反向学习Unity物理引擎应用、MonoBehaviour事件驱动机制及资源管理规范。读者可直接导入Unity编辑器运行调试,快速掌握塔防游戏从对象行为建模到关卡流程编排的落地方法。
1. 植物大战僵尸源码用 Unity 实现:不是复刻原版,而是掌握塔防逻辑闭环的实战入口
你搜“植物大战僵尸源码 unity”,大概率不是想拿一份能直接上线的商业级复刻——那根本不存在(版权红线清晰),也不是为了跑通一个花哨但逻辑断裂的 Demo。你真正要的,是一套可调试、可打断点、可改规则、可加新植物/僵尸的塔防骨架:子弹怎么飞、阳光怎么生成、冷却怎么计时、碰撞怎么判定、波次怎么调度……这些在 Unity 里不是靠拖拽组件就能自然发生的黑匣子,而是必须亲手串起来的数据流与事件链。我带过 7 个实习生从零搭塔防框架,90% 的人卡在“为什么豌豆打不中僵尸”或“阳光一刷新就卡死”上,根源不是 C# 不熟,而是没把“游戏状态机 + 对象池 + 时间驱动”这三根柱子立稳。这篇笔记不讲美术资源怎么切图、不讲 Shader 怎么做卡通渲染,只聚焦于用 Unity 2021.3 LTS(LTS 版本最稳,避坑首选)+ C# 脚本 + 原生 UI 系统,从空项目开始,6 小时内跑通核心循环:阳光采集 → 种植植物 → 发射子弹 → 击中僵尸 → 僵尸移动 → 植物死亡 → 波次推进。适合刚写完“Hello World”脚本、想用真实项目倒逼工程能力的 Unity 新手;也适合做过简单小游戏、但没碰过“多对象生命周期管理”的进阶者——因为这里每个Destroy()、每个InvokeRepeating()、每个List<Zombie>的增删,都藏着你下次做 MMO 或开放世界时会踩的同款深坑。
2. 搭建最小可运行塔防骨架:从空场景到第一颗豌豆命中僵尸
2.1 创建基础场景与对象层级:别急着写代码,先画清数据边界
Unity 里塔防的混乱,80% 源于对象关系没理清。不要一上来就建 50 个 Prefab。按职责隔离原则,只建 4 个核心 GameObject:
GameController(空对象,挂GameController.cs):全局状态中枢,管阳光总数、当前波次、游戏暂停/结束。SunManager(空对象,挂SunManager.cs):只管阳光生成、收集、UI 更新,绝不碰植物逻辑。LaneManager(空对象,挂LaneManager.cs):管理 5 条横向车道(Y 轴位置固定),每条车道存一个List<Zombie>和List<Plant>,不存具体实例,只存引用。ObjectPool(空对象,挂ObjectPool.cs):预加载豌豆、阳光、爆炸特效等高频创建/销毁对象,用Dictionary<string, Queue<GameObject>>管理。
提示:所有 Manager 类都设为
DontDestroyOnLoad?错。只有GameController需要跨场景保留(比如加主菜单),SunManager和LaneManager必须随关卡场景销毁重建。否则下一关阳光数继承上一关,或者僵尸列表越积越多——这是新手最常翻车的玄学 Bug。
2.2 实现阳光系统:用协程控制生成节奏,用事件解耦收集逻辑
阳光不是“自动出现”,而是由SunManager按规则投放。关键不是Instantiate(),而是生成时机与收集反馈的分离:
// SunManager.cs public class SunManager : MonoBehaviour { public int initialSun = 50; public float sunSpawnInterval = 10f; // 秒 public int sunPerSpawn = 25; private int _currentSun = 0; private List<GameObject> _sunObjects = new List<GameObject>(); void Start() { _currentSun = initialSun; UpdateSunUI(); // 刷新 UI 文本 StartCoroutine(SpawnSunRoutine()); } IEnumerator SpawnSunRoutine() { while (true) { yield return new WaitForSeconds(sunSpawnInterval); if (_currentSun < 1000) // 阳光上限 { var sunObj = ObjectPool.Instance.GetPooledObject("Sun"); if (sunObj != null) { sunObj.transform.position = GetRandomSpawnPosition(); sunObj.SetActive(true); _sunObjects.Add(sunObj); } } } } // 收集阳光时调用(由 Sun.cs 脚本触发) public void CollectSun(int amount) { _currentSun += amount; UpdateSunUI(); // 发布事件,让 UI 或音效系统响应 EventManager.TriggerEvent("SunCollected", amount); } void UpdateSunUI() { // 假设 UI Text 组件挂在此对象下 if (sunText != null) sunText.text = _currentSun.ToString(); } }参数说明:
sunSpawnInterval = 10f:不是固定值,后期可按波次动态调整(第 3 波缩短到 7 秒);GetRandomSpawnPosition():返回屏幕顶部随机 X 坐标 + 固定 Y(如Camera.main.pixelHeight * 0.8f),确保阳光从天而降;ObjectPool.Instance.GetPooledObject("Sun"):避免Instantiate/Destroy频繁 GC,池子里预存 5 个 Sun Prefab;EventManager.TriggerEvent():用自定义事件总线(非 UnityEvent),解耦SunManager与AudioManager或VFXManager,后续加音效不用改阳光逻辑。
2.3 植物种植与冷却机制:用字典存冷却时间,用协程驱动倒计时
植物不能无限种,冷却不是“种完等 10 秒”,而是每个植物实例独立计时。PlantManager(挂载在GameController下)用Dictionary<PlantType, float>存类型级冷却,再用Dictionary<GameObject, float>存实例级剩余时间:
// PlantManager.cs public class PlantManager : MonoBehaviour { public Dictionary<PlantType, float> baseCooldown = new Dictionary<PlantType, float> { { PlantType.PeaShooter, 10f }, { PlantType.Sunflower, 15f } }; private Dictionary<GameObject, float> _instanceCooldown = new Dictionary<GameObject, float>(); private Dictionary<PlantType, bool> _isTypeAvailable = new Dictionary<PlantType, bool>(); void Start() { foreach (var kvp in baseCooldown) _isTypeAvailable[kvp.Key] = true; } // 点击植物图标时调用 public bool CanPlant(PlantType type, int cost) { return _isTypeAvailable[type] && SunManager.Instance.CurrentSun >= cost; } // 种植后立即调用 public void StartCooldown(GameObject plantInstance, PlantType type) { _isTypeAvailable[type] = false; _instanceCooldown[plantInstance] = baseCooldown[type]; // 启动实例倒计时协程 StartCoroutine(CooldownCoroutine(plantInstance, type)); } IEnumerator CooldownCoroutine(GameObject plant, PlantType type) { while (_instanceCooldown[plant] > 0) { _instanceCooldown[plant] -= Time.deltaTime; yield return null; } _instanceCooldown.Remove(plant); _isTypeAvailable[type] = true; // 触发 UI 更新:图标变亮 EventManager.TriggerEvent("PlantCooldownEnd", type); } }关键设计点:
baseCooldown是配置表,后期可导出为 ScriptableObject,美术改数值不用程序员改代码;_instanceCooldown用GameObject作 Key,而非Plant组件,因为植物可能被销毁(如被吃掉),Key 失效导致字典异常;- 协程
CooldownCoroutine里用Time.deltaTime而非WaitForSeconds,确保帧率波动时倒计时精准(60fps 和 30fps 下都是 10 秒)。
3. 僵尸与子弹的物理交互:绕开 Rigidbody 的性能陷阱,用射线检测替代碰撞体
3.1 僵尸移动:用 Transform.Translate + 状态机,禁用 Rigidbody
Unity 新手常给僵尸加Rigidbody再AddForce,结果 20 个僵尸同时跑,CPU 直接飙到 90%。塔防里僵尸移动是确定性路径(沿固定车道向左匀速走),完全不需要物理引擎:
// Zombie.cs public class Zombie : MonoBehaviour { public float moveSpeed = 0.5f; public int health = 10; private Vector3 _targetPosition; private bool _isAlive = true; void Update() { if (!_isAlive) return; // 沿 X 轴负方向移动(向左) transform.Translate(Vector3.left * moveSpeed * Time.deltaTime, Space.World); // 检查是否到达房子(X < -5f) if (transform.position.x < -5f) { GameController.Instance.GameOver(); Destroy(gameObject); } } public void TakeDamage(int damage) { health -= damage; if (health <= 0) { _isAlive = false; // 播放死亡动画、掉落阳光... Die(); } } void Die() { // 重置状态,归还对象池 ObjectPool.Instance.ReturnToPool(gameObject, "Zombie"); } }为什么不用 Rigidbody:
Rigidbody会触发 Physics.Update,每帧计算碰撞、重力,僵尸又不跳不摔;Transform.Translate是纯数学位移,CPU 开销近乎为 0;- 后续加“冰西瓜减速”效果,只需改
moveSpeed变量,无需处理物理材质摩擦力。
3.2 子弹发射与命中:用 SphereCast 检测前方僵尸,规避 Collider 重叠误判
豌豆不是“发射一个带 Collider 的预制体然后等 OnCollisionEnter”,而是每帧主动扫描前方是否有僵尸。这样既避免子弹穿模(僵尸移动快时 Collider 没撞上),又杜绝多颗豌豆同时击中同一僵尸的重复扣血:
// PeaShooter.cs public class PeaShooter : Plant { public float fireRate = 1.5f; // 秒/发 public float peaSpeed = 8f; public LayerMask zombieLayer; // 只检测 Zombie 层 private float _nextFireTime = 0f; void Update() { if (Time.time < _nextFireTime) return; // 向正前方发射射线(长度 10 单位) RaycastHit hit; if (Physics.SphereCast(transform.position, 0.2f, Vector3.right, out hit, 10f, zombieLayer)) { // 命中僵尸,直接扣血 var zombie = hit.collider.GetComponent<Zombie>(); if (zombie != null) { zombie.TakeDamage(1); _nextFireTime = Time.time + fireRate; // 播放射击音效、粒子特效 PlayShootEffect(); } } } void PlayShootEffect() { var effect = ObjectPool.Instance.GetPooledObject("PeaEffect"); if (effect != null) { effect.transform.position = transform.position + Vector3.right * 1.5f; effect.SetActive(true); } } }参数与技巧:
SphereCast比Raycast更鲁棒:豌豆有体积(0.2f 半径),能覆盖僵尸 Collider 的微小偏移;zombieLayer必须单独建层(如 “Zombie”),并在 Project Settings > Tags and Layers 中设置,不能用默认 “Everything”,否则射线会打中地面、房子甚至其他植物;fireRate = 1.5f是基础值,可被“樱桃炸弹”全局减速 Buff 修改,体现状态驱动设计。
3.3 僵尸与植物的碰撞逻辑:用 OnTriggerStay 替代 OnCollisionEnter,解决“贴脸不攻击”问题
僵尸走到植物面前,需要触发啃食。但OnCollisionEnter在僵尸刚接触植物 Collider 时触发一次,之后就没了。而OnTriggerStay每帧检测,适合持续伤害:
// Zombie.cs (续) private void OnTriggerStay(Collider other) { if (!other.CompareTag("Plant")) return; // 每 0.5 秒扣一次血(避免帧率高时扣血过快) if (Time.time - _lastBiteTime > 0.5f) { var plant = other.GetComponent<Plant>(); if (plant != null && plant.IsAlive) { plant.TakeDamage(1); _lastBiteTime = Time.time; // 播放啃食音效 AudioManager.Play("ZombieBite"); } } }注意:
- 植物和僵尸的 Collider 都需勾选
Is Trigger; Time.time - _lastBiteTime > 0.5f是硬性保护,否则 144fps 下每秒啃 288 次,植物瞬间消失;AudioManager.Play()用单例管理音效,避免每帧GetComponent<AudioSource>().Play()导致 GC。
4. 波次系统与对象池:用 ScriptableObject 配置关卡,用泛型池管理百个对象
4.1 用 ScriptableObject 定义波次:把“第 3 波出 5 个普通僵尸 + 1 个铁桶”写成数据
硬编码波次(if (wave == 3) SpawnZombie("Conehead", 1))会让关卡策划崩溃。正确做法是创建WaveData.asset:
// WaveData.cs [CreateAssetMenu(fileName = "NewWave", menuName = "TowerDefense/Wave Data")] public class WaveData : ScriptableObject { public int waveNumber; public float spawnInterval; // 该波内僵尸间隔秒数 public List<ZombieSpawnEntry> zombieSpawns; [System.Serializable] public class ZombieSpawnEntry { public ZombieType type; // 枚举:Normal, Conehead, Buckethead public int count; // 该类型出几个 public float delay; // 相对波次开始的延迟秒数 } }在 Unity 编辑器中右键 Create → TowerDefense → Wave Data,填入:
| waveNumber | spawnInterval | zombieSpawns.type | zombieSpawns.count | zombieSpawns.delay |
|---|---|---|---|---|
| 1 | 5.0 | Normal | 3 | 0 |
| 1 | 5.0 | Normal | 2 | 3 |
WaveManager.cs读取并执行:
// WaveManager.cs public class WaveManager : MonoBehaviour { public WaveData[] waveDatas; private int _currentWaveIndex = 0; private float _nextSpawnTime = 0f; private int _spawnedCount = 0; void Update() { if (_currentWaveIndex >= waveDatas.Length) return; var currentWave = waveDatas[_currentWaveIndex]; if (Time.time >= _nextSpawnTime && _spawnedCount < GetTotalZombiesInWave(currentWave)) { SpawnZombieFromWave(currentWave); _nextSpawnTime = Time.time + currentWave.spawnInterval; _spawnedCount++; } // 波次结束检查:该波僵尸全死亡 or 全进屋 if (_spawnedCount >= GetTotalZombiesInWave(currentWave) && LaneManager.Instance.GetAllZombies().Count == 0) { NextWave(); } } void SpawnZombieFromWave(WaveData wave) { var entry = wave.zombieSpawns[_spawnedCount % wave.zombieSpawns.Count]; var zombiePrefab = ZombiePrefabs.GetPrefab(entry.type); var zombie = Instantiate(zombiePrefab, GetSpawnPosition(), Quaternion.identity); // 设置初始血量、速度等(从 ScriptableObject 读取) zombie.GetComponent<Zombie>().health = ZombieStats.GetHealth(entry.type); } }优势:
- 策划双击
.asset文件即可改数值,无需程序员介入; GetTotalZombiesInWave()计算zombieSpawns.Sum(e => e.count),波次总僵尸数一目了然;SpawnZombieFromWave中entry.type是枚举,编译期校验,不怕字符串拼错。
4.2 对象池泛型化:一个池子管豌豆、阳光、爆炸,避免 10 个 Pool 脚本
ObjectPool.cs必须支持任意 Prefab,不能为每种对象写一个池:
// ObjectPool.cs public class ObjectPool : MonoBehaviour { public static ObjectPool Instance; [System.Serializable] public class Pool { public string tag; public GameObject prefab; public int size; } public List<Pool> pools; private Dictionary<string, Queue<GameObject>> _poolDictionary; void Awake() { Instance = this; _poolDictionary = new Dictionary<string, Queue<GameObject>>(); foreach (var pool in pools) { var queue = new Queue<GameObject>(); for (int i = 0; i < pool.size; i++) { var obj = Instantiate(pool.prefab); obj.SetActive(false); obj.transform.parent = transform; queue.Enqueue(obj); } _poolDictionary[pool.tag] = queue; } } public GameObject GetPooledObject(string tag) { if (!_poolDictionary.ContainsKey(tag)) return null; var queue = _poolDictionary[tag]; if (queue.Count == 0) return null; // 池空,可选择扩容或返回 null var obj = queue.Dequeue(); obj.SetActive(true); return obj; } public void ReturnToPool(GameObject obj, string tag) { if (!_poolDictionary.ContainsKey(tag)) return; obj.SetActive(false); _poolDictionary[tag].Enqueue(obj); } }配置方式:
- 在 Inspector 中给
pools添加 3 项:tag="Pea"、prefab=Pea.prefab、size=20;tag="Sun"、prefab=Sun.prefab、size=10;tag="Explosion"、prefab=Explosion.prefab、size=5; GetPooledObject("Pea")返回一个激活的豌豆,ReturnToPool(pea, "Pea")归还;- 关键:
obj.transform.parent = transform确保所有池对象挂载到ObjectPool下,场景视图不杂乱,且DontDestroyOnLoad时一并保留。
5. 避坑指南:那些让塔防项目卡在 90% 进度的致命细节
5.1 现象:阳光数字 UI 闪烁跳变,有时显示负数
原因:SunManager的CollectSun()和SpawnSun()并发修改_currentSun,未加锁;且UpdateSunUI()在Start()中调用,但 UI Text 组件尚未赋值。
解决:
_currentSun改为private int _currentSun = 0;,所有修改走CollectSun()和SpendSun()方法;SpendSun()返回bool表示是否扣款成功,植物种植前先调用if (!SunManager.Instance.SpendSun(cost)) return;;UpdateSunUI()前加空检查:if (sunText != null) sunText.text = _currentSun.ToString();,并在Awake()中用sunText = GetComponentInChildren<Text>();初始化。
5.2 现象:僵尸跑到植物面前不动,啃食音效不响
原因:僵尸和植物的 Collider 尺寸不匹配(如植物 Collider 高 1.5m,僵尸高 2m,垂直方向未重叠);或OnTriggerStay中other.CompareTag("Plant")的 Tag 未在 Inspector 中设置。
解决:
- 在 Scene 视图开启
Gizmos→Colliders,观察两者是否绿色重叠; - 植物 Collider 改用
BoxCollider,Size 设为(0.8f, 1.2f, 0.8f),Center(0, 0.6f, 0);僵尸 Collider Size(0.6f, 1.8f, 0.6f),Center(0, 0.9f, 0); - 选中植物 GameObject,在 Inspector 顶部 Tag 下拉框选
Plant(需先在Edit > Project Settings > Tags and Layers中添加该 Tag)。
5.3 现象:第 2 波僵尸刚出,第 1 波还没死完,LaneManager的List<Zombie>报NullReferenceException
原因:僵尸死亡时调用ObjectPool.Instance.ReturnToPool(gameObject, "Zombie"),但LaneManager仍持有已销毁对象的引用,foreach (var z in zombies)时z为 null。
解决:
LaneManager中存储List<Zombie>改为List<Zombie> _zombies = new List<Zombie>();;- 僵尸
Die()方法末尾加:LaneManager.Instance.RemoveZombie(this);; RemoveZombie(Zombie z)内部用zombies.Remove(z),并加空检查:if (zombies.Contains(z)) zombies.Remove(z);;- 血泪经验:永远不要在
Update()中遍历可能被外部修改的 List,改用for (int i = zombies.Count - 1; i >= 0; i--)倒序遍历删除。
5.4 现象:打包 WebGL 后,阳光生成停止,控制台报Cannot access a destroyed object
原因:WebGL 不支持DontDestroyOnLoad,SunManager被销毁后,SpawnSunRoutine()协程仍在运行,尝试访问已销毁对象。
解决:
- 所有 Manager 脚本
OnDisable()中加StopAllCoroutines();; SpawnSunRoutine()开头加if (this == null) yield break;;- 更彻底方案:用
SceneManager.sceneLoaded事件监听场景加载完成,再启动协程,确保对象存活。
5.5 现象:切换不同分辨率(如 1440x810)后,阳光从屏幕外生成,或僵尸移动速度忽快忽慢
原因:GetRandomSpawnPosition()用Camera.main.pixelHeight计算 Y 坐标,但未适配 Canvas 的Scale Factor;moveSpeed是绝对值,未乘Time.deltaTime。
解决:
GetRandomSpawnPosition()改用世界坐标:var screenPos = Camera.main.ViewportToWorldPoint(new Vector3(Random.value, 0.9f, 10f));;moveSpeed改为public float moveSpeed = 0.5f;,Update()中transform.Translate(Vector3.left * moveSpeed * Time.deltaTime, Space.World);;- Canvas 的 Render Mode 设为
Screen Space - Overlay,Canvas Scaler的 UI Scale Mode 设为Scale With Screen Size,Reference Resolution 设为1440x810。
6. 进阶验证与调试技巧:用编辑器扩展实时监控塔防状态
6.1 在 Scene 视图绘制调试信息:一眼看清僵尸血量与植物冷却
Unity 编辑器不显示运行时变量,但可以用OnDrawGizmos()在 Scene 视图画文字和线:
// Zombie.cs (编辑器扩展) #if UNITY_EDITOR using UnityEditor; #endif public class Zombie : MonoBehaviour { // ... 原有代码 ... #if UNITY_EDITOR void OnDrawGizmosSelected() { // 显示血量文本 Handles.Label(transform.position + Vector3.up * 1.2f, $"HP: {health}"); // 显示攻击范围(如果有的话) if (attackRange > 0) { Handles.color = Color.red; Handles.DrawWireDisc(transform.position, Vector3.forward, attackRange); } } #endif }效果:选中僵尸时,Scene 视图上方显示HP: 7,周围画红色圆圈表示攻击半径。同理,PeaShooter.cs中画绿色射线表示SphereCast方向与长度。
6.2 用 Profiler 定位性能瓶颈:重点看 GC Alloc 和 Physics.Processing
塔防性能杀手不是 Draw Call,而是 GC 和 Physics:
- GC Alloc 高:检查
List<T>.Add()是否在Update()中频繁调用(如每帧zombies.Add(z));改用List<T>.Clear()复用,或预分配容量new List<Zombie>(50); - Physics.Processing 高:确认所有
SphereCast/Raycast都指定了LayerMask,避免检测无关层;zombieLayer值用LayerMask.GetMask("Zombie")预计算,而非每帧LayerMask.NameToLayer("Zombie"); - MonoBehaviour.Update 高:给
Zombie.cs加[RequireComponent(typeof(Rigidbody))]?删掉!僵尸不用 Rigidbody;Plant.cs的Update()只做冷却检查,不执行逻辑。
6.3 用自定义 Inspector 简化波次配置:让策划 3 步配出新关卡
WaveData.cs默认 Inspector 是原始 JSON 风格。加一个编辑器脚本让它友好:
// WaveDataEditor.cs #if UNITY_EDITOR [CustomEditor(typeof(WaveData))] public class WaveDataEditor : Editor { public override void OnInspectorGUI() { var wave = (WaveData)target; EditorGUILayout.LabelField("波次配置", EditorStyles.boldLabel); wave.waveNumber = EditorGUILayout.IntField("波次数", wave.waveNumber); wave.spawnInterval = EditorGUILayout.FloatField("僵尸间隔(秒)", wave.spawnInterval); EditorGUILayout.LabelField("僵尸列表", EditorStyles.boldLabel); if (GUILayout.Button("添加僵尸")) { wave.zombieSpawns.Add(new WaveData.ZombieSpawnEntry()); } for (int i = 0; i < wave.zombieSpawns.Count; i++) { EditorGUILayout.BeginHorizontal(); wave.zombieSpawns[i].type = (ZombieType)EditorGUILayout.EnumPopup(wave.zombieSpawns[i].type); wave.zombieSpawns[i].count = EditorGUILayout.IntField(wave.zombieSpawns[i].count); wave.zombieSpawns[i].delay = EditorGUILayout.FloatField(wave.zombieSpawns[i].delay); if (GUILayout.Button("-", GUILayout.Width(20))) { wave.zombieSpawns.RemoveAt(i); break; } EditorGUILayout.EndHorizontal(); } if (GUILayout.Button("保存")) EditorUtility.SetDirty(target); } } #endif效果:策划在 Inspector 中看到“添加僵尸”按钮,点一下新增一行,下拉选僵尸类型、填数量、设延迟,比手敲 JSON 快 10 倍。
6.4 用 Addressable Assets 管理资源:为未来加 DLC 预留接口
现在所有 Prefab 都拖进Resources文件夹?立刻停手。Resources.Load()无法卸载,内存只增不减。改用 Addressables:
- Window → Package Manager → 安装
Addressable Asset System; - 右键
Pea.prefab→Addressable Assets → Set Address,填pea; ObjectPool.cs中GetPooledObject()改为:AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>(tag); handle.Completed += (op) => { /* 实例化并加入池 */ };
好处:
- 打包时资源可分组(
zombies组、plants组),热更新只替换一组; Addressables.Release(handle)可精确卸载,内存可控;- 后期加“植物大战僵尸 2”DLC,只需新地址
peashooter_v2,旧代码不用改。
我带团队做第一个塔防项目时,坚持用 Addressables,结果上线后第 3 个月接到需求:替换全部植物音效。运维同事 5 分钟上传新音频包,玩家重启游戏即生效,没动一行代码。这种“后悔药”能力,值得你在第一天就埋下。希望帮到你。
本文还有配套的精品资源,点击获取