拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity塔防+抽卡游戏开发实战:从零构建移动端核心玩法

在移动游戏开发领域塔防与抽卡养成是两类极具吸引力的玩法。当我们将两者结合构建一个玩家需要部署防御塔抵御敌人同时通过抽卡来获取和升级塔楼与技能的游戏时就创造了一个兼具策略深度与收集乐趣的复合体验。然而从零开始实现这样一个完整的Unity 3D移动游戏项目涉及从场景搭建、核心玩法逻辑到UI交互、数据管理的全链路挑战新手开发者往往在模块衔接和性能优化上遇到瓶颈。本文旨在提供一个系统化的实战指南手把手带你从零开始使用Unity 2022.3 LTS版本基于内置渲染管线Built-in Render Pipeline打造一个包含防御塔建造、升级、攻击和抽卡获取新塔、技能等核心机制的完整移动端游戏Demo。无论你是刚接触Unity的初学者还是希望系统学习游戏架构的中级开发者都能通过本文理清开发脉络掌握可复用于其他项目的模块化设计思想。1. 项目核心概念与设计思路在动手写代码之前明确游戏的核心循环和各个模块的职责至关重要。这能帮助我们构建清晰、可维护的代码结构。1.1 游戏核心玩法拆解我们的游戏可以概括为“塔防卡牌收集”。其核心循环如下准备阶段玩家在关卡开始前或游戏主界面使用游戏内货币进行“抽卡”。抽卡机制从预设的卡池中随机抽取“防御塔卡牌”或“技能卡牌”。每张卡牌代表一个可部署的塔楼或一次可使用的技能。布防阶段进入关卡后玩家将拥有的卡牌拖拽到地图上指定的建造点消耗资源建造对应的防御塔。战斗阶段敌人沿路径进攻防御塔自动攻击进入射程的敌人。玩家可手动释放技能卡牌辅助战斗。成长循环通过关卡获得货币和抽卡资源抽取更强力的卡牌升级现有塔牌挑战更高难度的关卡。1.2 关键技术模块划分基于上述玩法我们将项目划分为以下几个相对独立的模块场景与基础系统3D场景搭建、寻路系统NavMesh、游戏管理器GameManager负责全局状态如金币、生命值、游戏状态。敌人系统敌人单位Enemy的生成、移动沿路径或使用NavMesh、生命值管理、死亡逻辑。防御塔系统塔的基类TowerBase定义攻击、索敌、射击等通用行为。不同塔类型如箭塔、炮塔、魔法塔继承基类并实现特殊效果。抽卡系统卡池CardPool数据配置、抽卡逻辑随机算法、卡牌UICardUI的展示与交互。建造与拖拽系统处理卡牌拖拽、建造点BuildNode检测、塔的实例化与放置。UI系统游戏内HUD金币、生命值显示、抽卡界面、塔升级界面、关卡选择界面。数据管理使用ScriptableObject或JSON配置卡牌数据、敌人波次数据、关卡数据实现数据与逻辑分离。这种模块化设计使得每个部分可以独立开发和测试最后通过游戏管理器进行整合。2. 开发环境与项目初始化工欲善其事必先利其器。一个稳定的开发环境是项目顺利推进的基础。2.1 环境准备与版本说明Unity Hub Unity Editor本文使用Unity 2022.3.25f1 LTS版本。LTS长期支持版本稳定性高适合项目开发。请通过Unity Hub安装此版本。渲染管线项目使用Built-in Render Pipeline内置渲染管线。对于移动端和入门项目内置管线足够使用且学习曲线平缓。在创建项目时请确保模板选择“3D (Core)”而不是URP或HDRP。目标平台Android或iOS。本文示例将兼顾两者但主要设置以Android为例。你需要提前安装对应平台的SDK/NDKAndroid或切换到iOS构建目标需Mac电脑。IDEVisual Studio 2022 或 JetBrains Rider并确保已安装Unity开发所需的插件包。2.2 创建新项目与基础设置打开Unity Hub点击“新建项目”。选择“3D (Core)”模板输入项目名称如“TowerDefenseCardGame”选择存储位置点击“创建项目”。项目创建后进行一些基础设置以优化移动端体验File - Build Settings切换目标平台为“Android”或“iOS”。对于Android点击“Player Settings...”。Player Settings 关键配置Company Name和Product Name填写你的游戏名称。Resolution and Presentation将“Default Orientation”设置为“Portrait”或“Landscape”根据你的游戏设计决定。通常塔防游戏更适合横屏Landscape。Other SettingsBundle Identifier设置为唯一的反向域名格式如com.YourCompany.YourGameName。Minimum API Level根据你的目标用户设备选择例如 Android 8.0 (API Level 26) 是一个较安全的起点。Quality Settings针对移动端可以适当降低渲染质量。在Project窗口搜索“Quality”打开设置面板为Android平台选择较低的等级如“Simple”关闭抗锯齿Anti Aliasing或使用较低级别。3. 构建游戏场景与基础环境一个直观的场景是游戏的舞台。我们先搭建一个简单的测试关卡。3.1 创建基础地形与路径在Hierarchy面板右键 - 3D Object - Plane命名为“Ground”。这作为地面。为地面创建一个简单的材质。在Project窗口右键 - Create - Material命名为“GroundMat”。将其Albedo颜色调为灰色或绿色然后拖拽到场景中的“Ground”物体上。创建敌人行进路径。最简单的方法是使用空物体GameObject - Create Empty作为路径点Waypoint。创建一个空物体命名为“Path”。将其作为所有路径点的父物体。在“Path”下创建多个空物体命名为“Waypoint_01”, “Waypoint_02”… 将它们按你希望的敌人移动路线排列在场景中例如从屏幕左侧蜿蜒到右侧的基地。创建敌人生成点Spawn Point和终点End Point/Base。在路径起点和终点位置分别创建空物体命名为“EnemySpawner”和“Base”。3.2 设置导航网格NavMesh寻路对于更复杂的自由移动如敌人寻找最短路径绕过障碍我们需要使用Unity的导航系统。将“Ground”标记为可导航区域。选中“Ground”在Inspector面板点击右上角的“Static”下拉框勾选“Navigation Static”。打开导航网格烘焙窗口Window - AI - Navigation。在“Navigation”窗口的“Bake”页签保持默认参数Agent Radius, Height等可根据敌人大小调整点击右下角的“Bake”按钮。烘焙完成后场景地面会覆盖一层蓝色的导航网格。此时我们可以通过代码让敌人利用NavMeshAgent组件从生成点移动到终点。这是一种更动态的寻路方式适合复杂地图。对于简单的固定路径直接让敌人按顺序移动至路径点数组即可。本文后续将采用路径点数组的方式因其更简单可控是塔防游戏的经典实现。3.3 创建建造点Build Nodes防御塔只能建造在特定的位置。在场景中合适的位置如道路两旁创建一些Cube3D Object - Cube命名为“BuildNode”。缩放Cube到一个合适的大小如扁平的正方形作为建造点的视觉标识。为每个BuildNode创建一个材质如半透明的蓝色以便区分。为每个BuildNode添加一个自定义脚本组件稍后创建用于处理塔的建造逻辑。同时可以添加一个Box Collider用于检测拖拽的卡牌。至此一个包含地面、路径、建造点的基本场景就搭建完成了。接下来我们将开始编写核心的游戏逻辑代码。4. 核心系统实现敌人与防御塔游戏的核心交互是敌人移动防御塔攻击。我们首先实现这两个系统。4.1 敌人Enemy系统实现敌人需要沿着路径移动并拥有生命值受到攻击后会死亡。创建Enemy脚本在Project窗口的Assets/Scripts文件夹下若无则创建右键 - Create - C# Script命名为Enemy。// 文件路径Assets/Scripts/Enemy.cs using UnityEngine; using System.Collections; public class Enemy : MonoBehaviour { [Header(属性)] public float startSpeed 10f; // 初始速度 [HideInInspector] // 在Inspector中隐藏因为速度可能会被减速效果改变 public float speed; public float startHealth 100f; // 初始生命值 private float health; public int worth 50; // 击败后获得的金币 [Header(Unity Setup Fields)] public GameObject deathEffect; // 死亡特效预制体 private bool isDead false; void Start() { speed startSpeed; health startHealth; } // 受到伤害的方法由防御塔调用 public void TakeDamage(float amount) { health - amount; if (health 0 !isDead) { Die(); } } // 减速效果可选用于某些塔的特殊能力 public void Slow(float slowPercentage) { speed startSpeed * (1f - slowPercentage); // 通常减速效果会持续一段时间这里需要协程或状态管理为简化先直接设置 // 实际项目中应使用协程在减速时间后恢复速度 } void Die() { isDead true; // 奖励金币给玩家 PlayerStats.Money worth; // 实例化死亡特效 if (deathEffect ! null) { GameObject effect Instantiate(deathEffect, transform.position, Quaternion.identity); Destroy(effect, 5f); // 5秒后销毁特效 } // 从WaveSpawner的存活敌人列表中移除如果存在 WaveSpawner.EnemiesAlive--; // 销毁敌人自身 Destroy(gameObject); } }创建敌人移动脚本敌人需要沿着预设的路径点移动。创建脚本EnemyMovement。// 文件路径Assets/Scripts/EnemyMovement.cs using UnityEngine; using System.Collections; public class EnemyMovement : MonoBehaviour { [Header(路径设置)] public Transform[] waypoints; // 通过Inspector或代码赋值路径点数组 private int waypointIndex 0; private Enemy enemy; // 引用Enemy脚本以获取速度 void Start() { enemy GetComponentEnemy(); // 注意这里waypoints需要从GameManager或专门的PathFinder获取 // 为了示例我们假设通过Tag查找一个包含所有路径点的父物体 GameObject pathObject GameObject.FindGameObjectWithTag(Path); if (pathObject ! null) { waypoints new Transform[pathObject.transform.childCount]; for (int i 0; i waypoints.Length; i) { waypoints[i] pathObject.transform.GetChild(i); } } // 初始位置设为第一个路径点生成点 if (waypoints.Length 0) transform.position waypoints[0].position; } void Update() { if (waypoints null || waypointIndex waypoints.Length) { EndPath(); return; } Vector3 dir waypoints[waypointIndex].position - transform.position; transform.Translate(dir.normalized * enemy.speed * Time.deltaTime, Space.World); // 使用距离平方判断效率更高 if (Vector3.SqrMagnitude(dir) 0.1f * 0.1f) // 如果距离小于0.1单位 { waypointIndex; } // 简单的面向移动方向可选 if (dir ! Vector3.zero) { Quaternion lookRotation Quaternion.LookRotation(dir); Vector3 rotation Quaternion.Lerp(transform.rotation, lookRotation, Time.deltaTime * 10f).eulerAngles; transform.rotation Quaternion.Euler(0f, rotation.y, 0f); } } void EndPath() { // 敌人到达终点玩家扣血 PlayerStats.Lives--; WaveSpawner.EnemiesAlive--; Destroy(gameObject); } }创建敌人预制体Prefab在场景中创建一个3D物体如Capsule作为敌人模型命名为“EnemyPrefab”。将Enemy和EnemyMovement脚本挂载上去。为它添加一个Rigidbody取消Use Gravity防止下落和一个Capsule Collider用于触发检测。将这个物体从Hierarchy拖拽到Project窗口的Assets/Prefabs/Enemies文件夹中生成预制体。然后删除场景中的实例。4.2 防御塔Tower系统实现防御塔是游戏的核心我们需要一个灵活的基类系统。创建塔基类脚本TowerBase定义所有塔的通用行为索敌、攻击、冷却。// 文件路径Assets/Scripts/Towers/TowerBase.cs using UnityEngine; using System.Collections; public abstract class TowerBase : MonoBehaviour { [Header(通用塔属性)] public float range 15f; // 攻击范围 public float fireRate 1f; // 每秒攻击次数 private float fireCountdown 0f; public int damage 10; public GameObject bulletPrefab; // 子弹预制体 public Transform firePoint; // 子弹发射点 [Header(目标锁定)] public Transform target; // 当前锁定的敌人 public string enemyTag Enemy; // 敌人标签 void Start() { // 每0.5秒调用一次更新目标避免每帧调用 InvokeRepeating(UpdateTarget, 0f, 0.5f); } void Update() { if (target null) return; // 攻击冷却 if (fireCountdown 0f) { Shoot(); fireCountdown 1f / fireRate; } fireCountdown - Time.deltaTime; } // 更新目标寻找范围内最近的敌人 void UpdateTarget() { GameObject[] enemies GameObject.FindGameObjectsWithTag(enemyTag); float shortestDistance Mathf.Infinity; GameObject nearestEnemy null; foreach (GameObject enemy in enemies) { float distanceToEnemy Vector3.Distance(transform.position, enemy.transform.position); if (distanceToEnemy shortestDistance) { shortestDistance distanceToEnemy; nearestEnemy enemy; } } if (nearestEnemy ! null shortestDistance range) { target nearestEnemy.transform; } else { target null; } } // 抽象方法具体攻击方式由子类实现 protected abstract void Shoot(); // 在Scene视图中绘制攻击范围便于调试 void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, range); } }创建具体塔类型标准攻击塔StandardTower继承基类实现发射子弹的逻辑。// 文件路径Assets/Scripts/Towers/StandardTower.cs using UnityEngine; using System.Collections; public class StandardTower : TowerBase { protected override void Shoot() { if (bulletPrefab null || firePoint null || target null) return; // 实例化子弹 GameObject bulletGO Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); Bullet bullet bulletGO.GetComponentBullet(); if (bullet ! null) { // 设置子弹的目标和伤害 bullet.Seek(target); bullet.damage damage; } } }创建子弹脚本Bullet处理子弹的飞行和命中逻辑。// 文件路径Assets/Scripts/Bullet.cs using UnityEngine; using System.Collections; public class Bullet : MonoBehaviour { public float speed 70f; public int damage 10; public GameObject impactEffect; 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() { GameObject effectIns Instantiate(impactEffect, transform.position, transform.rotation); Destroy(effectIns, 2f); // 2秒后销毁特效 // 对敌人造成伤害 Enemy enemy target.GetComponentEnemy(); if (enemy ! null) { enemy.TakeDamage(damage); } Destroy(gameObject); // 销毁子弹自身 } }创建塔预制体创建一个Cube或导入一个塔的3D模型命名为“StandardTowerPrefab”。挂载StandardTower脚本。创建一个空物体作为子物体命名为“FirePoint”放置在塔的枪口位置并将其拖拽到脚本的Fire Point字段。创建一个Sphere作为子弹并为其创建Bullet脚本和预制体。将子弹预制体拖拽到塔脚本的Bullet Prefab字段。添加一个Sphere Collider用于范围检测可选因为索敌逻辑在代码中。将塔预制体保存到Assets/Prefabs/Towers。5. 游戏管理与资源系统我们需要一个中心管理器来协调游戏状态如金币、生命值、波次生成等。5.1 玩家状态与游戏管理器创建PlayerStats静态类或单例GameManager来管理全局资源。// 文件路径Assets/Scripts/Managers/PlayerStats.cs using UnityEngine; public class PlayerStats : MonoBehaviour { // 使用单例模式便于全局访问 public static PlayerStats Instance; [Header(玩家资源)] public int startMoney 400; private int money; public int startLives 20; private int lives; public int Money { get { return money; } set { money value; // 当金钱变化时可以触发UI更新事件 OnMoneyChanged?.Invoke(money); } } public int Lives { get { return lives; } set { lives value; // 当生命变化时触发UI更新和游戏结束检查 OnLivesChanged?.Invoke(lives); if (lives 0) { GameOver(); } } } // 定义事件用于UI更新 public delegate void OnMoneyChangedDelegate(int newMoney); public event OnMoneyChangedDelegate OnMoneyChanged; public delegate void OnLivesChangedDelegate(int newLives); public event OnLivesChangedDelegate OnLivesChanged; void Awake() { if (Instance ! null) { Debug.LogError(More than one PlayerStats in scene!); return; } Instance this; } void Start() { Money startMoney; Lives startLives; } void GameOver() { Debug.Log(Game Over!); // 这里可以触发游戏结束UI、停止生成敌人等 Time.timeScale 0f; // 暂停游戏 } }5.2 敌人波次生成器创建WaveSpawner脚本控制敌人一波一波地生成。// 文件路径Assets/Scripts/WaveSpawner.cs using UnityEngine; using System.Collections; using System.Collections.Generic; [System.Serializable] public class Wave { public string waveName; public GameObject enemyPrefab; public int count; // 该波次敌人数量 public float rate; // 生成间隔秒 } public class WaveSpawner : MonoBehaviour { public static int EnemiesAlive 0; // 静态变量记录存活敌人数量 public ListWave waves; public Transform spawnPoint; private int waveIndex 0; private bool spawningWave false; void Update() { // 如果当前没有敌人生成且存活敌人数为0则生成下一波 if (!spawningWave EnemiesAlive 0) { if (waveIndex waves.Count) { StartCoroutine(SpawnWave()); } else { // 所有波次结束游戏胜利 Debug.Log(All waves cleared! Level Complete!); // 触发胜利逻辑 } } } IEnumerator SpawnWave() { spawningWave true; Wave wave waves[waveIndex]; Debug.Log(Spawning Wave: wave.waveName); for (int i 0; i wave.count; i) { SpawnEnemy(wave.enemyPrefab); yield return new WaitForSeconds(1f / wave.rate); } waveIndex; spawningWave false; } void SpawnEnemy(GameObject enemyPrefab) { if (spawnPoint null) { Debug.LogError(No spawn point assigned!); return; } GameObject enemy Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation); EnemiesAlive; } }将WaveSpawner脚本挂载到场景中的一个空物体如“WaveManager”上并将之前创建的敌人预制体拖拽到Waves列表中配置数量和生成速率。6. 建造系统实现拖拽与放置这是连接抽卡系统和场景交互的关键。我们需要实现从UI卡牌拖拽到建造点放置塔的逻辑。6.1 建造点BuildNode脚本建造点需要检测拖拽过来的卡牌并实例化对应的塔。// 文件路径Assets/Scripts/BuildSystem/BuildNode.cs using UnityEngine; using UnityEngine.EventSystems; public class BuildNode : MonoBehaviour { public Vector3 positionOffset; // 塔生成的位置偏移避免塔嵌在地里 [HideInInspector] public GameObject towerBuilt; // 当前节点上已建造的塔 [HideInInspector] public TowerBlueprint towerBlueprint; // 当前节点选择的塔蓝图 private Renderer rend; private Color startColor; void Start() { rend GetComponentRenderer(); startColor rend.material.color; } // 当鼠标悬停在建造点上时高亮 void OnMouseEnter() { if (EventSystem.current.IsPointerOverGameObject()) // 防止穿透UI return; if (towerBuilt ! null) // 如果已经有塔则不显示高亮 return; rend.material.color Color.yellow; } void OnMouseExit() { rend.material.color startColor; } // 当鼠标点击建造点时 void OnMouseDown() { if (EventSystem.current.IsPointerOverGameObject()) return; // 如果该点已有塔则显示升级/出售UI而不是建造 if (towerBuilt ! null) { // 调用BuildManager来显示该塔的升级界面 BuildManager.instance.SelectNode(this); return; } // 如果未选择要建的塔则返回 if (!BuildManager.instance.HasTowerToBuild) return; // 调用BuildManager在此节点建造塔 BuildManager.instance.BuildTowerOn(this); } }6.2 建造管理器BuildManager这是一个单例负责管理当前选中的塔、处理建造逻辑和金钱扣除。// 文件路径Assets/Scripts/BuildSystem/BuildManager.cs using UnityEngine; [System.Serializable] public class TowerBlueprint { public GameObject prefab; // 塔的预制体 public int cost; // 建造花费 public GameObject upgradedPrefab; // 升级后的预制体后续实现 public int upgradeCost; // 升级花费 } public class BuildManager : MonoBehaviour { public static BuildManager instance; // 单例 void Awake() { if (instance ! null) { Debug.LogError(More than one BuildManager in scene!); return; } instance this; } private TowerBlueprint towerToBuild; // 当前准备建造的塔蓝图 private BuildNode selectedNode; // 当前选中的节点用于升级 // 属性是否有塔准备建造 public bool HasTowerToBuild { get { return towerToBuild ! null; } } // 属性获取当前要建的塔 public TowerBlueprint GetTowerToBuild() { return towerToBuild; } // 选择要建造的塔由UI卡牌按钮调用 public void SelectTowerToBuild(TowerBlueprint blueprint) { towerToBuild blueprint; // 这里可以取消之前选中的节点 DeselectNode(); } // 在指定节点上建造塔 public void BuildTowerOn(BuildNode node) { if (PlayerStats.Instance.Money towerToBuild.cost) { Debug.Log(Not enough money to build that!); return; } PlayerStats.Instance.Money - towerToBuild.cost; GameObject tower Instantiate(towerToBuild.prefab, node.transform.position node.positionOffset, Quaternion.identity); node.towerBuilt tower; node.towerBlueprint towerToBuild; Debug.Log(Tower built! Money left: PlayerStats.Instance.Money); // 建造后清空选择 towerToBuild null; } // 选中一个节点用于显示升级UI public void SelectNode(BuildNode node) { if (selectedNode node) { DeselectNode(); return; } selectedNode node; towerToBuild null; // 选择节点时清空待建的塔选择 // 这里应该调用UI管理器来显示该节点的升级/出售面板 // UIManager.instance.DisplayNodeUI(node); } public void DeselectNode() { selectedNode null; // 隐藏升级UI // UIManager.instance.HideNodeUI(); } }6.3 卡牌UI与拖拽这是抽卡系统的前端表现。我们需要创建卡牌的UI预制体并为其添加拖拽功能。创建卡牌UI预制体在Canvas下创建一个Image作为卡牌背景添加Button组件。在卡牌上添加Image显示塔的图标Text显示塔的名称和花费。为这个卡牌UI创建一个脚本TowerCardUI。// 文件路径Assets/Scripts/UI/TowerCardUI.cs using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; public class TowerCardUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { public TowerBlueprint towerBlueprint; // 此卡牌对应的塔蓝图 [Header(UI References)] public Image towerIcon; public Text towerNameText; public Text costText; private Transform originalParent; private GameObject dragIcon; // 拖拽时跟随鼠标的图标 void Start() { // 初始化UI显示 if (towerBlueprint ! null towerBlueprint.prefab ! null) { // 这里假设塔的预制体上有一个SpriteRenderer或我们有一个图标资源字典 // towerIcon.sprite ...; towerNameText.text towerBlueprint.prefab.name; costText.text $ towerBlueprint.cost.ToString(); } } // 当卡牌被点击时备用方案如果不用拖拽可以用点击 public void OnCardClicked() { BuildManager.instance.SelectTowerToBuild(towerBlueprint); Debug.Log(Selected: towerBlueprint.prefab.name); } // ---- 拖拽接口实现 ---- public void OnBeginDrag(PointerEventData eventData) { originalParent transform.parent; // 创建一个临时图标跟随鼠标 dragIcon new GameObject(DragIcon); dragIcon.transform.SetParent(GetComponentInParentCanvas().transform, false); dragIcon.transform.SetAsLastSibling(); // 确保在最上层 Image image dragIcon.AddComponentImage(); image.sprite towerIcon.sprite; image.raycastTarget false; // 防止拖拽图标挡住射线检测 RectTransform rt dragIcon.GetComponentRectTransform(); rt.sizeDelta new Vector2(50, 50); // 设置拖拽图标大小 BuildManager.instance.SelectTowerToBuild(towerBlueprint); // 选择要建造的塔 } public void OnDrag(PointerEventData eventData) { if (dragIcon ! null) { // 将屏幕坐标转换为Canvas下的局部坐标 RectTransformUtility.ScreenPointToLocalPointInRectangle( dragIcon.transform.parent.GetComponentRectTransform(), eventData.position, eventData.pressEventCamera, out Vector2 localPos); dragIcon.transform.localPosition localPos; } } public void OnEndDrag(PointerEventData eventData) { Destroy(dragIcon); // 检查是否拖拽到了建造点上通过射线检测 Ray ray Camera.main.ScreenPointToRay(eventData.position); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { BuildNode node hit.collider.GetComponentBuildNode(); if (node ! null) { // 在节点上建造塔 BuildManager.instance.BuildTowerOn(node); } } // 无论是否建造成功都清空当前选择 BuildManager.instance.DeselectNode(); } }至此我们已经实现了从卡牌拖拽到建造塔的完整流程。玩家可以将卡牌拖到建造点上如果金钱足够就会自动建造对应的防御塔。由于篇幅限制本文分为上下两部分。上半部分我们完成了从项目初始化、场景搭建到核心的敌人、防御塔、游戏管理、建造系统的实现。在下半部分我们将重点实现抽卡系统、卡池数据管理、UI系统整合、移动端适配与优化以及项目构建发布。这将使我们的游戏从一个可运行的Demo成长为一个功能完整的移动游戏项目。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门