在实际游戏开发中,防御塔和抽卡机制是两种非常流行且能有效提升游戏可玩性的设计元素。防御塔游戏考验玩家的策略布局和资源管理能力,而抽卡机制则通过随机性和收集要素极大地增强了游戏的长期吸引力和商业潜力。将两者结合,可以构建一个既有策略深度又充满收集乐趣的移动端游戏项目。
本文旨在从零开始,带领你使用 Unity 3D 引擎,结合内置渲染管线,打造一个包含防御塔建造、升级、战斗以及抽卡收集角色/塔楼等核心玩法的完整移动游戏原型。无论你是刚接触 Unity 的初学者,还是希望系统学习特定机制整合的开发者,都能通过本文获得从项目搭建、核心逻辑实现到移动端适配的完整实践路径。我们将从最基础的 Unity 工程创建开始,逐步实现敌人寻路、塔防攻击、抽卡 UI 与逻辑、数据持久化等模块,最终形成一个可在手机上运行测试的完整项目闭环。
1. 理解项目核心机制与架构设计
在动手写代码之前,理清防御塔和抽卡两大核心机制如何协同工作,是避免后续开发混乱的关键。一个典型的结合了这两种机制的游戏流程可能是:玩家通过关卡战斗获得游戏货币,使用货币进行抽卡,获得新的防御塔或塔的升级部件,然后用这些新单位去挑战更难的关卡。
1.1 防御塔机制的核心组件
防御塔机制通常包含以下几个相互关联的模块:
- 敌人系统 (Enemy System):负责生成敌人,并让敌人沿着预设路径移动。每个敌人拥有生命值、移动速度、奖励金币等属性。
- 路径系统 (Path System):定义关卡中敌人的移动路线,通常由一系列路径点 (Waypoints) 组成。
- 防御塔系统 (Tower System):这是核心。一个塔需要具备:
- 攻击范围 (Attack Range):一个圆形碰撞体或通过距离计算来检测敌人。
- 目标选择策略 (Targeting Strategy):如何从范围内的多个敌人中选择攻击目标(如最近、最强、最先进入等)。
- 攻击行为 (Attack Behavior):发射子弹、造成伤害、应用减速等效果。
- 升级逻辑 (Upgrade Logic):消耗资源提升攻击力、射程、攻击速度等属性。
- 关卡与波次管理 (Wave Management):控制敌人生成的节奏、波次数量和强度。
1.2 抽卡机制的核心组件
抽卡机制则更侧重于 UI、数据和随机性:
- 卡池数据 (Card Pool Data):定义所有可抽取的“卡牌”对象,每个卡牌对应一种防御塔或升级道具,包含其名称、图标、稀有度、属性、获取权重等。
- 抽卡逻辑 (Gacha Logic):实现随机算法。最简单的可能是根据权重随机,更复杂的会包含“保底”机制。
- 资源管理 (Resource Management):管理用于抽卡的货币(如钻石、金币),以及抽卡结果的存储。
- 用户界面 (UI):抽卡按钮、动画、结果展示面板、卡牌仓库界面等。
1.3 项目架构设计思路
为了保持代码清晰,我们采用模块化的设计。整个项目可以划分为几个相对独立的“领域”,通过定义良好的接口(如事件、单例管理器)进行通信。
- 游戏管理器 (GameManager):作为单例,管理游戏全局状态,如当前关卡、玩家金币、生命值、游戏暂停/结束逻辑。
- 敌人管理器 (EnemyManager):负责根据波次数据生成和回收敌人。
- 塔管理器 (TowerManager):管理所有已建造的塔,可能处理塔的建造、升级、出售。
- 抽卡管理器 (GachaManager):作为单例,管理卡池数据,执行抽卡逻辑,并将结果通知其他系统(如为玩家解锁新塔)。
- UI 管理器 (UIManager):控制所有游戏界面的显示与隐藏,更新金币、生命值等显示。
- 数据持久化 (Data Persistence):使用
PlayerPrefs或JSON文件保存玩家的金币数量、已解锁的卡牌、关卡进度等。
这种设计使得抽卡系统和塔防战斗系统既能独立开发和测试,又能通过GameManager或事件系统进行数据交换(例如,抽卡获得新塔后,塔的建造菜单中应出现该塔的选项)。
2. 环境准备与 Unity 项目初始化
工欲善其事,必先利其器。确保你的开发环境配置正确,是项目顺利推进的第一步。
2.1 软件与工具准备
- Unity Hub & Unity Editor:从 Unity 官网下载并安装 Unity Hub。通过 Hub 安装一个长期支持 (LTS) 版本,例如2022.3.x。在安装时,确保勾选“Android Build Support”(如果你要发布到安卓)或“iOS Build Support”(针对苹果设备)。对于本教程,使用内置渲染管线 (Built-in Render Pipeline) 即可,它足够满足我们的需求且更简单。
- 代码编辑器:推荐使用Visual Studio(Windows)或Visual Studio for Mac,并确保在 Unity Hub 的安装设置中关联好。Rider 也是一个强大的选择。
- 目标平台 SDK:
- Android:需要安装 Java JDK 和 Android SDK & NDK。Unity Hub 通常可以帮你一键安装。
- iOS:需要在 macOS 上安装 Xcode。
2.2 创建新项目与初始设置
- 打开 Unity Hub,点击“新建项目”。
- 选择“3D (Core)”模板。将项目命名为“TowerDefenseGacha”,并选择一个合适的存储位置。
- 点击“创建项目”。等待 Unity 初始化完毕。
- 项目创建后,首先进行一些基础设置:
- 设置目标平台:点击菜单栏
File -> Build Settings。选择Android或iOS,然后点击Switch Platform。这个过程可能需要一些时间。 - 创建文件夹结构:在 Project 窗口的 Assets 文件夹下,创建以下文件夹来组织资源,这是一个良好的实践:
_Scripts(脚本)Prefabs(预制体)Scenes(场景)Materials(材质)Textures(贴图)UI(用户界面素材)Audio(音效)
- 设置目标平台:点击菜单栏
- 保存初始场景:将默认的
SampleScene重命名为MainGame,并拖入Assets/Scenes文件夹。
3. 构建基础塔防游戏框架
我们先实现一个最基础的塔防玩法,这是整个项目的战斗核心。
3.1 创建敌人与路径系统
- 设计路径:在场景中创建一个空物体,命名为
Path。为其创建几个子空物体,命名为Waypoint_1,Waypoint_2... 这些子物体的位置就是敌人的移动路径点。你可以将它们排列成一条曲折的路线。 - 创建敌人预制体:
- 在场景中创建一个
3D Object -> Capsule,命名为Enemy。缩放至合适大小(如(0.8, 0.8, 0.8))。 - 为其添加一个材质,比如设为红色以便区分。
- 为其添加一个
Rigidbody组件,并勾选Is Kinematic(我们通过脚本控制移动,而非物理模拟)。 - 创建一个 C# 脚本
Enemy.cs,拖到敌人上。然后将这个敌人从 Hierarchy 拖到Assets/Prefabs文件夹,创建为预制体。最后删除场景中的实例。
- 在场景中创建一个
- 编写敌人移动脚本(
Enemy.cs):
using UnityEngine; using System.Collections.Generic; public class Enemy : MonoBehaviour { public float startHealth = 100f; public float moveSpeed = 5f; public int goldReward = 10; private float currentHealth; private int waypointIndex = 0; private List<Transform> waypoints; void Start() { currentHealth = startHealth; // 假设路径点的父物体叫“Path” GameObject pathObject = GameObject.Find("Path"); if (pathObject != null) { waypoints = new List<Transform>(); foreach (Transform child in pathObject.transform) { waypoints.Add(child); } } // 初始位置设为第一个路径点 if (waypoints.Count > 0) transform.position = waypoints[0].position; } void Update() { Move(); } void Move() { if (waypoints == null || waypointIndex >= waypoints.Count) { ReachDestination(); return; } Vector3 targetPos = waypoints[waypointIndex].position; Vector3 dir = (targetPos - transform.position).normalized; transform.Translate(dir * moveSpeed * Time.deltaTime, Space.World); // 让敌人面朝移动方向 if (dir != Vector3.zero) transform.rotation = Quaternion.LookRotation(dir); if (Vector3.Distance(transform.position, targetPos) < 0.1f) { waypointIndex++; } } public void TakeDamage(float damage) { currentHealth -= damage; if (currentHealth <= 0) { Die(); } } void Die() { // 通知游戏管理器增加金币 GameManager.Instance.AddGold(goldReward); Destroy(gameObject); } void ReachDestination() { // 敌人到达终点,扣减玩家生命值 GameManager.Instance.TakeLifeDamage(1); Destroy(gameObject); } }- 创建敌人生成器:创建一个空物体
EnemySpawner,挂载脚本EnemySpawner.cs。这个脚本将根据波次数据,在路径起点(第一个路径点)生成敌人。
using UnityEngine; using System.Collections; public class EnemySpawner : MonoBehaviour { public GameObject enemyPrefab; public float timeBetweenWaves = 5f; public int[] enemiesPerWave; // 每波敌人的数量,在Inspector中设置 private int currentWave = 0; private bool isSpawning = false; void Start() { StartCoroutine(StartNextWave()); } IEnumerator StartNextWave() { yield return new WaitForSeconds(timeBetweenWaves); if (currentWave < enemiesPerWave.Length) { StartCoroutine(SpawnWave(enemiesPerWave[currentWave])); currentWave++; } else { Debug.Log("所有波次结束!"); // 可以触发关卡胜利逻辑 } } IEnumerator SpawnWave(int enemyCount) { isSpawning = true; for (int i = 0; i < enemyCount; i++) { SpawnEnemy(); yield return new WaitForSeconds(1f); // 每个敌人间隔1秒 } isSpawning = false; // 等待本波所有敌人被消灭或到达终点后,开始下一波 StartCoroutine(StartNextWave()); } void SpawnEnemy() { if (enemyPrefab != null) { Instantiate(enemyPrefab, transform.position, Quaternion.identity); } } }3.2 创建防御塔系统
- 创建塔预制体:
- 创建一个
3D Object -> Cylinder作为塔身,命名为BasicTower。 - 为其创建一个子物体(如一个 Cube 或 Sphere)作为炮塔旋转部分,命名为
TurretHead。 - 在
TurretHead下再创建一个子物体(如一个细长的 Cube),作为炮管,命名为Barrel,用于指示攻击方向。 - 为塔身添加一个材质(如蓝色)。
- 为塔添加一个
Sphere Collider,将其Is Trigger设为true,并调整半径作为攻击范围。取消勾选Rigidbody(如果需要的话)。 - 将整个
BasicTower拖成预制体。
- 创建一个
- 编写基础塔脚本(
Tower.cs):
using UnityEngine; using System.Collections.Generic; public class Tower : MonoBehaviour { public float attackRange = 5f; public float attackRate = 1f; // 每秒攻击次数 public float attackDamage = 20f; public GameObject bulletPrefab; public Transform firePoint; // 子弹发射点,可以拖拽Barrel的末端 private float attackCountdown = 0f; private Transform target; private List<GameObject> enemiesInRange = new List<GameObject>(); void Update() { UpdateTarget(); if (target != null) { // 让炮塔旋转部分朝向目标 Vector3 dir = target.position - transform.position; Quaternion lookRotation = Quaternion.LookRotation(dir); Vector3 rotation = lookRotation.eulerAngles; // 假设TurretHead是旋转部分 Transform turretHead = transform.Find("TurretHead"); if (turretHead != null) turretHead.rotation = Quaternion.Euler(0f, rotation.y, 0f); if (attackCountdown <= 0f) { Attack(); attackCountdown = 1f / attackRate; } attackCountdown -= Time.deltaTime; } } void UpdateTarget() { // 简单的目标选择:选择列表中的第一个敌人(最先进入范围) if (enemiesInRange.Count > 0) { // 可以在这里实现更复杂的逻辑,如选择最近的敌人 target = enemiesInRange[0].transform; } else { target = null; } } void Attack() { if (bulletPrefab != null && firePoint != null) { GameObject bulletGO = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); Bullet bullet = bulletGO.GetComponent<Bullet>(); if (bullet != null) { bullet.Seek(target); bullet.damage = attackDamage; } } } void OnTriggerEnter(Collider other) { if (other.CompareTag("Enemy")) { enemiesInRange.Add(other.gameObject); } } void OnTriggerExit(Collider other) { if (other.CompareTag("Enemy")) { enemiesInRange.Remove(other.gameObject); } } // 在Unity编辑器中可视化攻击范围 void OnDrawGizmosSelected() { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); } }- 创建子弹脚本与预制体:
- 创建一个 Sphere 作为子弹,添加刚体 (
Rigidbody) 并勾选Is Kinematic。创建脚本Bullet.cs并挂载。 - 将其拖成预制体
BulletPrefab,然后在 Tower 的 Inspector 中赋值。
- 创建一个 Sphere 作为子弹,添加刚体 (
using UnityEngine; public class Bullet : MonoBehaviour { public float speed = 30f; public float explosionRadius = 0f; // 用于未来实现范围伤害 public float damage = 20f; private Transform target; public void Seek(Transform _target) { target = _target; } void Update() { if (target == null) { Destroy(gameObject); return; } Vector3 dir = target.position - transform.position; float distanceThisFrame = speed * Time.deltaTime; // 如果这一帧会击中目标 if (dir.magnitude <= distanceThisFrame) { HitTarget(); return; } transform.Translate(dir.normalized * distanceThisFrame, Space.World); transform.LookAt(target); } void HitTarget() { // 简单伤害逻辑 Enemy enemy = target.GetComponent<Enemy>(); if (enemy != null) { enemy.TakeDamage(damage); } Destroy(gameObject); } }3.3 实现游戏管理器与基础 UI
- 创建游戏管理器(
GameManager.cs):这是一个单例,管理游戏状态。
using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { public static GameManager Instance; public int playerGold = 100; public int playerLives = 20; public Text goldText; public Text livesText; void Awake() { if (Instance == null) Instance = this; else if (Instance != this) Destroy(gameObject); } void Start() { UpdateUI(); } public void AddGold(int amount) { playerGold += amount; UpdateUI(); } public bool SpendGold(int amount) { if (playerGold >= amount) { playerGold -= amount; UpdateUI(); return true; } else { Debug.Log("金币不足!"); return false; } } public void TakeLifeDamage(int damage) { playerLives -= damage; UpdateUI(); if (playerLives <= 0) { GameOver(); } } void UpdateUI() { if (goldText != null) goldText.text = "金币: " + playerGold; if (livesText != null) livesText.text = "生命: " + playerLives; } void GameOver() { Debug.Log("游戏结束!"); Time.timeScale = 0f; // 暂停游戏 // 可以在这里显示游戏结束UI } }- 创建基础 UI:
- 在 Hierarchy 中右键 ->
UI -> Canvas。将 Canvas 的Render Mode设为Screen Space - Overlay。 - 在 Canvas 下创建两个
UI -> Text,分别命名为GoldText和LivesText。调整它们的位置(如屏幕左上角)。 - 将这两个 Text 组件分别拖拽到
GameManager脚本的对应字段中。
- 在 Hierarchy 中右键 ->
至此,一个最基础的塔防框架已经完成。你可以运行游戏,看到敌人生成并沿着路径移动,塔会自动攻击进入范围的敌人,敌人死亡会奖励金币,到达终点会扣减生命值。
4. 实现抽卡机制与系统集成
现在,我们将抽卡系统集成到塔防框架中。假设抽卡可以获得新的防御塔类型。
4.1 设计卡牌数据与卡池
- 创建卡牌数据类:在
_Scripts下创建文件夹Data,新建一个 C# 脚本CardData.cs。这是一个纯粹的 数据容器 ,不继承MonoBehaviour。
[System.Serializable] // 使其可序列化,方便在Inspector中编辑和保存 public class CardData { public string cardId; // 卡牌唯一标识 public string cardName; // 卡牌名称 public string description; // 描述 public Sprite icon; // UI图标 public TowerType towerType; // 关联的塔类型(枚举) public int rarity; // 稀有度,1普通,2稀有,3史诗... public int weight; // 抽卡权重,权重越高越容易抽到 } // 定义塔类型枚举 public enum TowerType { Basic, Cannon, Slow, Splash // 可以继续扩展 }- 创建卡池管理器(
CardPoolManager.cs):这是一个单例,负责加载和管理所有卡牌数据。
using UnityEngine; using System.Collections.Generic; public class CardPoolManager : MonoBehaviour { public static CardPoolManager Instance; public List<CardData> allCards = new List<CardData>(); // 在Inspector中手动配置或从JSON加载 void Awake() { if (Instance == null) Instance = this; else if (Instance != this) Destroy(gameObject); DontDestroyOnLoad(gameObject); // 跨场景不销毁 } // 根据权重随机抽取一张卡 public CardData DrawRandomCard() { int totalWeight = 0; foreach (CardData card in allCards) { totalWeight += card.weight; } int randomPoint = Random.Range(0, totalWeight); int currentWeight = 0; foreach (CardData card in allCards) { currentWeight += card.weight; if (randomPoint < currentWeight) { return card; } } // 理论上不会走到这里 return null; } // 十连抽 public List<CardData> DrawTenCards() { List<CardData> results = new List<CardData>(); for (int i = 0; i < 10; i++) { results.Add(DrawRandomCard()); } return results; } }- 配置卡池:在场景中创建一个空物体
CardPoolManager,挂载上述脚本。在 Inspector 中,展开All Cards列表,点击“+”号添加条目,并为每个条目填写卡牌信息(需要提前准备好图标 Sprite)。
4.2 创建抽卡 UI 与逻辑
- 创建抽卡 UI:
- 在 Canvas 下创建一个
Panel,命名为GachaPanel,作为抽卡的主界面。初始状态设为Active为false。 - 在
GachaPanel下创建:- 一个
Button作为单抽按钮 (DrawOneButton)。 - 一个
Button作为十连抽按钮 (DrawTenButton)。 - 一个
Text显示抽卡消耗 (CostText)。 - 一个
Image作为结果显示区域 (ResultDisplay)。 - 一个
Button作为关闭按钮 (CloseButton)。
- 一个
- 在 Canvas 下创建一个
- 编写抽卡 UI 控制器(
GachaUIController.cs):
using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; public class GachaUIController : MonoBehaviour { public GameObject gachaPanel; // 抽卡面板 public Button drawOneButton; public Button drawTenButton; public Text costText; public Image resultDisplayImage; // 用于显示抽到卡牌的图标 public Text resultNameText; // 显示抽到卡牌的名字 public int drawOneCost = 100; public int drawTenCost = 900; // 通常有折扣 void Start() { drawOneButton.onClick.AddListener(OnDrawOneClicked); drawTenButton.onClick.AddListener(OnDrawTenClicked); UpdateCostDisplay(); // 默认隐藏面板 if (gachaPanel != null) gachaPanel.SetActive(false); } void UpdateCostDisplay() { costText.text = $"单抽: {drawOneCost}金币\n十连: {drawTenCost}金币"; } // 打开抽卡界面(可以由主UI的某个按钮触发) public void OpenGachaPanel() { if (gachaPanel != null) gachaPanel.SetActive(true); } public void CloseGachaPanel() { if (gachaPanel != null) gachaPanel.SetActive(false); } void OnDrawOneClicked() { if (GameManager.Instance.SpendGold(drawOneCost)) { CardData drawnCard = CardPoolManager.Instance.DrawRandomCard(); ShowResult(drawnCard); // 重要:解锁这张卡对应的塔 UnlockTower(drawnCard); } } void OnDrawTenClicked() { if (GameManager.Instance.SpendGold(drawTenCost)) { List<CardData> drawnCards = CardPoolManager.Instance.DrawTenCards(); // 简单处理:只显示最后一张,实际项目需要更华丽的展示 if (drawnCards.Count > 0) { ShowResult(drawnCards[drawnCards.Count - 1]); foreach (var card in drawnCards) { UnlockTower(card); } } } } void ShowResult(CardData card) { if (card != null) { resultDisplayImage.sprite = card.icon; resultNameText.text = card.cardName; // 可以在这里播放特效或动画 Debug.Log($"抽到了: {card.cardName} (稀有度: {card.rarity})"); } } void UnlockTower(CardData card) { // 这里需要将解锁的塔类型信息传递给“塔建造UI”或“塔管理器” // 例如,可以触发一个事件,或者直接调用建造UI的解锁方法 TowerBuildManager.Instance.UnlockTowerType(card.towerType); Debug.Log($"已解锁塔类型: {card.towerType}"); } }4.3 集成抽卡与塔防:解锁新塔
抽卡的目的是解锁新的塔。我们需要一个中心来管理玩家已解锁的塔类型,并更新建造菜单。
- 创建塔建造管理器(
TowerBuildManager.cs):
using UnityEngine; using System.Collections.Generic; public class TowerBuildManager : MonoBehaviour { public static TowerBuildManager Instance; // 塔类型与预制体的映射 public Dictionary<TowerType, GameObject> towerPrefabDict = new Dictionary<TowerType, GameObject>(); // 玩家已解锁的塔类型列表 public List<TowerType> unlockedTowerTypes = new List<TowerType>(); // 在Inspector中预先关联好预制体 public GameObject basicTowerPrefab; public GameObject cannonTowerPrefab; // 等等 void Awake() { if (Instance == null) Instance = this; else if (Instance != this) Destroy(gameObject); // 初始化字典 towerPrefabDict[TowerType.Basic] = basicTowerPrefab; towerPrefabDict[TowerType.Cannon] = cannonTowerPrefab; // ... 初始化其他类型 // 初始解锁基础塔 unlockedTowerTypes.Add(TowerType.Basic); } // 由GachaUIController调用,解锁新塔 public void UnlockTowerType(TowerType type) { if (!unlockedTowerTypes.Contains(type)) { unlockedTowerTypes.Add(type); // 通知UI更新(例如,在建造按钮栏显示新塔的按钮) UIManager.Instance.UpdateTowerBuildButtons(); } } // 获取当前选中的塔类型(由UI按钮设置) private TowerType selectedTowerType = TowerType.Basic; public void SelectTowerType(TowerType type) { if (unlockedTowerTypes.Contains(type)) { selectedTowerType = type; } } // 在指定位置建造选中的塔 public void BuildTowerAt(Vector3 position) { if (towerPrefabDict.ContainsKey(selectedTowerType)) { GameObject towerToBuild = towerPrefabDict[selectedTowerType]; // 这里应该检查金币是否足够,位置是否合法等 Instantiate(towerToBuild, position, Quaternion.identity); } } }- 更新 UI 管理器(
UIManager.cs):创建一个简单的 UI 管理器,负责根据unlockedTowerTypes动态生成或激活建造按钮。
现在,游戏流程已经串联起来:玩家通过战斗获得金币 -> 打开抽卡界面消耗金币抽卡 -> 抽到新塔类型并解锁 -> 在塔建造菜单中可以使用新类型的塔进行建造。
5. 移动端适配、数据持久化与常见问题
5.1 移动端输入与 UI 适配
在移动设备上,我们需要将 PC 端的鼠标点击操作替换为触摸操作。
- 塔的建造:修改塔建造逻辑,通过射线检测触摸点来确定建造位置。
// 在 TowerBuildManager 或一个专门的 InputHandler 中 void Update() { if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began) { // 检查是否点击了UI元素,如果是则忽略 if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) return; Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(0).position); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { // 假设地面有一个“Ground”层 if (hit.collider.gameObject.layer == LayerMask.NameToLayer("Ground")) { BuildTowerAt(hit.point); } } } // PC端备用,方便测试 if (Input.GetMouseButtonDown(0)) { if (EventSystem.current.IsPointerOverGameObject()) return; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { if (hit.collider.gameObject.layer == LayerMask.NameToLayer("Ground")) { BuildTowerAt(hit.point); } } } }- UI 缩放:在 Canvas Scaler 组件上,将
UI Scale Mode设置为Scale With Screen Size,并设定一个参考分辨率(如 1920x1080)。确保按钮和文字在不同屏幕尺寸下清晰可点。
5.2 数据持久化:保存游戏进度
我们需要保存玩家的金币、已解锁的塔和关卡进度。这里使用简单的PlayerPrefs。
using UnityEngine; using System.Collections.Generic; public class SaveSystem : MonoBehaviour { private const string GOLD_KEY = "PlayerGold"; private const string UNLOCKED_TOWERS_KEY = "UnlockedTowers"; // 存储为逗号分隔的字符串,如 "Basic,Cannon" public void SaveGame() { PlayerPrefs.SetInt(GOLD_KEY, GameManager.Instance.playerGold); // 保存已解锁的塔 string unlockedTowersString = string.Join(",", TowerBuildManager.Instance.unlockedTowerTypes); PlayerPrefs.SetString(UNLOCKED_TOWERS_KEY, unlockedTowersString); PlayerPrefs.Save(); Debug.Log("游戏已保存"); } public void LoadGame() { if (PlayerPrefs.HasKey(GOLD_KEY)) { GameManager.Instance.playerGold = PlayerPrefs.GetInt(GOLD_KEY); GameManager.Instance.UpdateUI(); } if (PlayerPrefs.HasKey(UNLOCKED_TOWERS_KEY)) { string unlockedTowersString = PlayerPrefs.GetString(UNLOCKED_TOWERS_KEY); string[] towerNames = unlockedTowersString.Split(','); TowerBuildManager.Instance.unlockedTowerTypes.Clear(); foreach (string name in towerNames) { if (System.Enum.TryParse(name, out TowerType type)) { TowerBuildManager.Instance.unlockedTowerTypes.Add(type); } } UIManager.Instance.UpdateTowerBuildButtons(); } Debug.Log("游戏已加载"); } // 可以在游戏退出、切换场景或特定时机调用SaveGame void OnApplicationQuit() { SaveGame(); } }注意:
PlayerPrefs适合存储简单数据。对于更复杂的结构(如卡牌收集进度、多个存档),建议使用JSON或XML序列化到文件中。
5.3 常见问题与排查
在开发过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 检查与解决思路 |
|---|---|---|
| 敌人不移动或移动方向错误 | 1.Enemy.cs脚本中waypoints列表为空或未正确获取。2. 路径点 Path物体的名称或层级不对。3. 敌人预制体上的 Enemy脚本未启用。 | 1. 在Enemy.Start()中打印waypoints.Count检查。2. 确认场景中存在名为 Path的 GameObject,且其子物体是路径点。3. 检查预制体及其场景实例。 |
| 塔不攻击敌人 | 1. 敌人没有Tag设为 “Enemy”。2. 塔的 Sphere Collider的Is Trigger未勾选。3. 子弹预制体未正确赋值给 Tower脚本。4. firePoint未赋值。 | 1. 检查敌人预制体的 Tag。 2. 检查塔的碰撞体设置。 3. 在 Inspector 中确认 bulletPrefab和firePoint字段已拖拽赋值。4. 使用 Debug.DrawRay在Update中绘制攻击射线辅助调试。 |
| 抽卡 UI 按钮点击无反应 | 1. UI 按钮的OnClick()事件未绑定方法。2. GachaUIController脚本未挂载或未激活。3. 金币不足,但 SpendGold的失败提示未显示。 | 1. 在 Inspector 中检查按钮的事件列表。 2. 确认脚本所在 GameObject 是激活的。 3. 在 SpendGold方法中添加更明显的反馈,如 UI 飘字。 |
| 建造塔时金币被扣除但塔未出现 | 1.BuildTowerAt方法中的位置无效(如碰撞检测失败)。2. towerPrefabDict中对应TowerType的预制体为null。3. 实例化位置被其他物体遮挡。 | 1. 在BuildTowerAt中打印position和selectedTowerType进行调试。2. 检查 TowerBuildManager的 Inspector,确保所有预制体已赋值。3. 确保建造位置(地面)有正确的 Layer 并被射线检测到。 |
| 游戏打包到手机后运行崩溃或黑屏 | 1. 脚本存在平台相关代码(如System.IO路径问题)。2. 资源未正确包含在构建中。 3. 移动端输入处理有问题。 | 1. 使用Application.persistentDataPath替代硬编码路径。2. 在 Build Settings -> Player Settings中检查图标、分辨率等设置。3. 在 Unity Editor 中使用 Remote 真机调试功能连接手机查看日志。 |
5.4 性能优化与最佳实践建议
- 对象池:敌人和子弹频繁创建和销毁会产生垃圾回收压力。务必实现简单的对象池来复用它们。
- 事件系统:使用 C# 事件或 UnityEvent 来解耦模块。例如,敌人死亡时发布一个事件,
GameManager和成就系统监听该事件,而不是直接调用GameManager.Instance.AddGold()。 - 配置数据外置:将敌人属性、波次信息、塔属性、卡牌数据等存储在
ScriptableObject或 JSON/XML 文件中,便于策划调整而无需修改代码。 - 状态管理:明确游戏状态(如菜单、战斗中、暂停、抽卡中),并防止状态冲突(如在抽卡动画中不能建造塔)。
- 音频与粒子:为攻击、抽卡、UI 点击等添加音效和粒子效果,能极大提升游戏体验。
通过以上步骤,你已经完成了一个融合防御塔与抽卡机制的 Unity 3D 移动游戏核心原型。这个项目涵盖了从基础框架搭建、核心玩法实现、系统集成到移动端适配的完整流程。你可以在此基础上继续扩展,例如加入更多类型的塔和敌人、设计更丰富的卡牌和保底机制、制作关卡选择界面、添加音效和粒子特效、接入 SDK 实现广告与内购等,逐步将其打磨成一个完整的可发布游戏。