ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cocos Creator塔防游戏源码深度解析与二次开发实战指南

Cocos Creator塔防游戏源码深度解析与二次开发实战指南 1. 项目概述与核心价值最近在Cocos中文社区翻到一个老帖子一个基于Cocos Creator 2.1.3的精品塔防游戏源码讨论热度不低。这个项目有意思的地方在于它不是一个简单的Demo而是拥有相对完整的战斗逻辑、关卡设计和UI系统更重要的是它使用了经典塔防游戏《王国保卫战》Kingdom Rush的美术资源并且原作者和社区版主已经确认获得了Iron Hide Games的许可解决了开源项目中最头疼的版权问题。对于想入门Cocos Creator游戏开发特别是对塔防这个经久不衰的类型感兴趣的朋友来说这无疑是一个极佳的“脚手架”和“活教材”。这个源码的价值远不止于“能跑起来”。它像一本打开的教科书清晰地展示了如何用Cocos Creator的组件化、数据驱动思想去构建一个中等复杂度的游戏系统。你可以看到敌人波次如何生成、塔的寻敌与攻击逻辑如何实现、技能效果如何触发、金币和经验系统如何运转。对于新手你可以通过修改数值、调整关卡来快速获得正反馈对于有一定经验的开发者你可以深入其架构学习如何组织一个可维护、易扩展的项目代码结构。无论是想快速做出一个塔防原型验证想法还是希望深入学习Cocos Creator在实战中的应用这份源码都是一个非常扎实的起点。接下来我就带大家深入这个项目的“五脏六腑”看看一个精品塔防是如何被构建出来的并分享如何基于它进行二次开发和深度定制。2. 源码结构与核心模块拆解拿到源码第一件事不是急着点“运行”而是先理清它的目录结构和核心脚本的职责。一个清晰的结构是理解项目、后续进行高效开发的基础。这个塔防项目的结构遵循了Cocos Creator常见的模块化思想。2.1 资源与场景组织在assets目录下你会看到典型的资源分类textures存放所有图片资源塔、敌人、地图格子、UI图标等prefabs预制体目录则包含了游戏中的可复用对象如不同类型的防御塔Tower_Archer.prefab,Tower_Mage.prefab、敌人Enemy_Orc.prefab,Enemy_Goblin.prefab以及子弹、技能特效等。scenes目录下是游戏的主场景如Main.scene和各个关卡场景如Level1.scene。特别需要注意的是由于使用了《王国保卫战》的资源其美术风格统一动画帧命名规范这为我们替换或扩充美术资源提供了很好的参考。2.2 核心脚本模块解析脚本是游戏的灵魂。这个项目的脚本通常放在assets/scripts目录下我们可以将其分为几个核心模块游戏管理模块GameManager这是一个单例脚本堪称游戏的大脑。它负责全局状态的管理例如当前游戏状态准备、进行中、胜利、失败、玩家金币和生命值、当前关卡信息、游戏暂停与继续等。它通常作为常驻节点挂载其他模块通过它来访问和修改全局数据。关卡管理模块LevelManager负责具体关卡的逻辑。包括解析关卡数据如敌人波次配置、路径点信息、控制敌人波次的生成时机和顺序、判定关卡胜利或失败的条件。它和GameManager紧密协作GameManager告诉它关卡开始它则负责具体的战斗流程。实体控制模块包括Enemy敌人和Tower防御塔的基类及各种子类脚本。Enemy脚本控制敌人的移动沿路径点、生命值、受到伤害、死亡奖励金币、经验等逻辑。不同的敌人类型如步兵、骑兵、飞行单位会继承自Enemy并重写部分行为比如飞行单位可能无视地面路径。Tower脚本这是塔防的核心。它负责塔的放置、攻击范围检测、目标选择策略最近、最强、最先进入、攻击冷却、生成并发射子弹或释放技能。Tower通常是一个基类ArcherTower、MageTower、ArtilleryTower等继承它实现各自独特的攻击方式和特效。UI控制模块包括HUDController游戏内UI如金币/生命显示、BuildMenu建塔菜单、UpgradeMenu升级菜单、GameOverPanel游戏结束面板等。这些脚本处理用户的点击、拖拽操作并更新UI元素的显示。工具与配置模块包括DataManager可能用于加载JSON格式的关卡配置、塔属性配置、PoolManager对象池用于高效管理频繁创建销毁的子弹和敌人这是性能优化的关键、AudioManager音效管理等。注意在分析源码时不要被复杂的细节一开始就吓到。建议先从GameManager和主场景的入口脚本看起顺着游戏启动、关卡加载、第一波敌人生成的流程一步步理清各个模块是如何被调用和协作的。用调试模式运行在关键函数处打上断点观察变量的变化是理解代码执行流最有效的方法。3. 核心游戏逻辑实现深度剖析理解了结构我们深入到塔防游戏最核心的几个逻辑的实现细节。这些是任何塔防游戏的通用“套路”掌握它们你就能举一反三。3.1 敌人路径与移动系统塔防地图的本质是一张网格Grid或由一系列路径点Waypoints构成的图。在这个项目中路径系统很可能通过以下方式实现路径点设置在关卡场景中会有一组空节点通常命名为Waypoint1,Waypoint2, ...作为路径点。它们被按顺序放置在场景中形成一个敌人行进的路线。敌人移动逻辑在Enemy脚本的update函数中每一帧会计算敌人当前位置与下一个目标路径点之间的方向向量然后根据敌人的移动速度moveSpeed朝着该方向移动。当敌人到达某个路径点的一定距离内例如小于0.1个单位就将下一个路径点设置为目标。伪代码如下// Enemy.js 的 update 函数片段 update(dt) { if (!this.targetWaypoint) { this.targetWaypoint this.waypoints[this.currentWaypointIndex]; } let direction this.targetWaypoint.position.sub(this.node.position).normalize(); this.node.position this.node.position.add(direction.mul(this.moveSpeed * dt)); // 判断是否到达路径点 if (this.node.position.sub(this.targetWaypoint.position).mag() 0.1) { this.currentWaypointIndex; if (this.currentWaypointIndex this.waypoints.length) { // 到达终点扣减玩家生命值并销毁自己 this.reachEnd(); return; } this.targetWaypoint this.waypoints[this.currentWaypointIndex]; } }路径数据传递LevelManager在初始化时会收集场景中所有的路径点按顺序存入一个数组然后在生成敌人时将这个路径点数组传递给每一个Enemy实例。实操心得路径点的编辑最好在编辑器中可视化完成。你可以创建一个专门的“Path”节点下面挂载所有路径点子节点。这样在LevelManager中只需要通过this.node.parent.getChildByName(“Path”).children就能一次性获取所有有序的路径点非常方便。对于更复杂的地图如分支路径可以在路径点数据中加入权重或概率信息让敌人随机选择分支。3.2 防御塔的寻敌与攻击机制这是塔防游戏的战斗核心。一个健壮的塔攻击系统需要考虑效率性能和策略性。攻击范围检测每座塔都有一个attackRange属性。在塔的update或一个自定义计时器中它需要检测范围内的敌人。最直接但低效的方法是每帧遍历场景中所有存活的敌人计算距离。高性能的做法是使用节点分组Group和空间划分。Cocos Creator本身没有内置的物理引擎用于此类检测但我们可以利用其Node的position属性和简单的距离计算结合对象池或敌人管理器提供的敌人列表来进行优化。例如EnemyManager维护一个所有活动敌人的数组塔只遍历这个数组。// Tower.js 寻找目标函数 findTarget() { let enemies GameManager.instance.enemyManager.activeEnemies; let shortestDistance this.attackRange; let target null; for (let enemy of enemies) { let dist this.node.position.sub(enemy.node.position).mag(); if (dist this.attackRange dist shortestDistance) { shortestDistance dist; target enemy; // 这里实现的是“最近”策略 } } this.currentTarget target; }目标选择策略上述代码实现了“攻击最近敌人”的策略。其他常见策略包括最强血量最高比较敌人的hp属性。最先进入范围需要为敌人添加一个enterTime时间戳。最弱血量最低比较敌人的hp属性取最小值。 可以在塔的属性中增加一个targetingStrategy字段在findTarget函数中使用switch语句或策略模式来实现不同的选择逻辑增加游戏深度。攻击执行与冷却一旦锁定目标塔进入攻击状态。它需要一个attackCooldown攻击间隔计时器。在update中如果currentTarget有效且处于攻击范围内则累积攻击时间currentCooldown。当currentCooldown attackCooldown时执行攻击如创建子弹、播放攻击动画、调用目标受伤函数然后重置currentCooldown。子弹与伤害计算对于需要发射子弹的塔如弓箭塔、炮塔攻击时会实例化一个子弹预制体。子弹脚本Bullet会初始化目标target并在update中朝目标移动。击中后调用target.takeDamage(this.attackDamage)。伤害计算可能涉及护甲穿透、暴击等复杂公式这些通常在takeDamage函数中处理。避坑指南不要在每座塔的每一帧都进行全图敌人遍历这是新手最容易犯的性能错误。对于敌人数量较多50的场景这会迅速消耗CPU。优化方案a) 使用一个全局的EnemyManager统一管理敌人列表塔只遍历这个列表。b) 将地图进行网格划分塔只检测所在网格及相邻网格的敌人四叉树/网格空间划分思想。c) 降低检测频率例如每0.2秒检测一次而不是每帧检测。3.3 经济与升级系统设计塔防游戏的策略性很大程度上体现在资源管理和塔的成长路线上。金币系统金币是核心资源来源于击杀敌人奖励、关卡初始赠送、可能还有时间奖励或技能。在GameManager中维护一个gold变量。任何消耗金币的行为建塔、升级前都要检查gold cost。金币的增减需要通过事件或直接调用GameManager的方法并即时更新UI显示。建塔流程玩家点击地图上的可建造区域一个空的“塔基”节点弹出BuildMenu。BuildMenu显示当前可建造的塔类型及其价格。玩家点击一个塔图标如果金币足够则在塔基位置实例化对应的塔预制体并扣除金币。塔基节点可能被禁用或隐藏表示该位置已被占用。升级系统塔的升级通常是一个树状结构。点击已建造的塔弹出UpgradeMenu。升级选项可能包括提升伤害/攻击速度/范围直接修改塔的attackDamage,attackCooldown,attackRange属性。解锁特殊能力例如弓箭塔升级为“火矢”附加持续伤害法师塔升级为“霜冻”减速效果。这通常通过为塔添加新的组件如FireArrowEffect,FrostEffect或在攻击逻辑中增加新的判断分支来实现。改变攻击模式例如从单体攻击变为小范围溅射攻击。这需要修改塔的findTarget逻辑和伤害施加逻辑。 升级数据每级消耗、属性提升值最好用JSON或ScriptableObjectCocos Creator中类似的是cc.Asset来配置便于平衡性调整。设计技巧不要让升级只是简单的数值叠加。为每种塔设计1-2条有特色的升级分支让玩家在“伤害”和“功能”之间做出选择能极大提升游戏的重玩价值。例如炮塔可以升级为“高爆弹”大范围低伤或“穿甲弹”小范围高伤带破甲。4. 基于源码的二次开发与功能扩展读懂了源码下一步就是动手改造把它变成你自己的游戏。这里提供几个有代表性的扩展方向。4.1 添加新的敌人类型假设我们要添加一个会治疗的“巫医”敌人它不仅能自己前进还能治疗周围的友军。创建预制体复制一个已有的敌人预制体如Enemy_Orc.prefab重命名为Enemy_WitchDoctor.prefab替换其精灵Sprite为巫医的图片。创建脚本新建脚本EnemyWitchDoctor.js继承自Enemy基类如果原项目有基类或复制Enemy脚本修改。// EnemyWitchDoctor.js properties: { healRange: 100, // 治疗范围 healPower: 5, // 每次治疗量 healInterval: 2.0, // 治疗间隔秒 }, onLoad() { // 调用父类初始化 this._super(); // 初始化治疗计时器 this.healTimer 0; }, update(dt) { // 先执行原有的移动逻辑 this._super.update(dt); // 治疗逻辑 this.healTimer dt; if (this.healTimer this.healInterval) { this.healTimer 0; this.tryHealAllies(); } }, tryHealAllies() { let allies GameManager.instance.enemyManager.activeEnemies; for (let ally of allies) { // 不治疗自己且盟友在治疗范围内 if (ally ! this this.node.position.sub(ally.node.position).mag() this.healRange) { ally.takeHeal(this.healPower); // 需要在Enemy基类中添加takeHeal方法 } } }挂载脚本将EnemyWitchDoctor.js挂载到Enemy_WitchDoctor.prefab上。配置关卡在关卡数据可能是JSON或LevelManager中的一个数组中在某一波次加入{type: “WitchDoctor”, count: 2, interval: 1}这样的配置。修改生成逻辑在敌人生成器可能在LevelManager或单独的Spawner脚本中根据type字段实例化对应的预制体。4.2 实现英雄单位系统许多现代塔防如《王国保卫战》本身都有英雄单位他们可以在地图上自由移动、攻击并拥有强大的技能。创建英雄实体创建一个Hero预制体包含移动、攻击、技能等组件。Hero脚本同样需要寻敌和攻击逻辑但移动是玩家控制的通过点击地面。玩家控制监听屏幕的触摸/点击事件。当玩家点击非UI区域的地图位置时通过射线检测或坐标转换判断点击的是可行走的地面然后命令英雄移动至该点。这需要实现一个简单的点击移动Click-to-Move系统。技能系统为英雄设计2-4个技能如范围伤害、治疗、召唤物、增益光环。每个技能是一个独立的脚本包含冷却时间、消耗魔法值或时间、效果逻辑。在UI上添加技能按钮点击按钮后进入技能释放状态如光标变成范围指示器再次点击地面释放技能。英雄成长英雄可以通过杀敌获得经验升级后提升基础属性或解锁新技能。这需要一套独立的经验值、等级和技能树系统。扩展思路英雄系统可以做得非常复杂。你可以引入装备系统为英雄穿戴不同的武器、护甲影响属性、天赋树让玩家选择不同的成长方向、甚至多个英雄切换。从最小可行产品MVP开始先实现一个能移动和普攻的英雄再逐步添加技能。4.3 设计多路径与动态关卡原版源码很可能是单一路径。我们可以将其扩展为多路径甚至动态生成的路径增加策略深度。多路径数据在关卡编辑时定义多条路径点序列如PathA,PathB。在敌人波次配置中为每个敌人类别指定它走哪条路径path: “A”。敌人路径分配在生成敌人时根据配置将其路径点数组设置为对应的路径。动态路径/可建造墙更高级的玩法是允许玩家在战斗中建造“墙”或“路障”来改变敌人路径。这需要实现一套“可放置障碍物”的系统定义地图网格每个格子有“可通行”、“不可通行”、“可建造”等状态。敌人使用A*等寻路算法实时计算从出生点到终点的最短路径。玩家花费资源在“可建造”格子上放置障碍物该格子状态变为“不可通行”。敌人会动态地重新寻路绕过障碍物。这涉及到实时寻路性能是关键需要谨慎实现可能需要对敌人进行分批寻路或使用简化网格。5. 性能优化与项目工程化实践当你的游戏内容越来越丰富性能问题和代码维护难度就会凸显。以下是一些针对Cocos Creator塔防项目的优化和工程化建议。5.1 对象池的深度应用对象池是游戏开发中减少内存分配、避免GC卡顿的利器。原项目可能已经用于子弹和敌人但我们可以更彻底。通用对象池管理器实现一个PoolManager它可以管理多种类型的预制体。// PoolManager.js 简化示例 let poolDict {}; function getPool(prefab) { let key prefab.uuid || prefab.name; if (!poolDict[key]) { poolDict[key] new cc.NodePool(); // 可以预创建一些实例 for (let i 0; i 10; i) { let node cc.instantiate(prefab); poolDict[key].put(node); } } return poolDict[key]; } function getNode(prefab, parentNode) { let pool getPool(prefab); let node pool.size() 0 ? pool.get() : cc.instantiate(prefab); parentNode.addChild(node); return node; } function putNode(prefab, node) { let pool getPool(prefab); pool.put(node); }应用场景敌人一波敌人被全部击杀或到达终点后不是调用node.destroy()而是调用PoolManager.putNode(enemyPrefab, enemyNode)。下一波生成时从池中getNode。子弹同上击中目标或飞出屏幕后回池。UI特效如伤害数字、建造特效等。塔的升级/出售当塔被出售时其模型和特效也可以回池。重要提示对象池中的节点在被get出来后必须重置其所有状态包括位置、旋转、缩放、脚本中的计时器、血量等所有属性。最好在节点的根脚本上提供一个reset()方法在从池中取出和放回时调用。忘记重置状态是使用对象池最常见的Bug来源。5.2 渲染与Draw Call优化塔防游戏单位多Draw Call容易升高。Cocos Creator使用自动合批但需要满足条件相同图集、相同材质等。使用纹理图集Auto Atlas将游戏中所有的小图片UI图标、塔和敌人的不同状态图打包成少数几个大图集。在Cocos Creator的项目设置中开启“Auto Atlas”功能并合理配置。这是降低Draw Call最有效的手段。静态合批对于场景中永远不会移动的背景元素、地图装饰物可以将它们合并成一个静态节点。在Cocos Creator中可以通过将多个精灵的cc.Sprite组件设置为相同的渲染组件类型如simple并使用相同图集来促进合批。动态合批限制了解动态合批的限制。节点变换位置、旋转、缩放不同、材质实例属性不同都会打断合批。对于大量相同但位置不同的单位如同一种小兵如果它们来自同一图集且材质相同引擎会尝试动态合批但仍需注意性能。5.3 代码架构与数据驱动随着功能增加避免代码变成“意大利面条”。良好的架构能让后续开发事半功倍。使用配置表JSON/CSV将游戏平衡性数据彻底从代码中剥离。例如towerConfig.json: 定义所有塔的基础属性、升级消耗、每级提升。enemyConfig.json: 定义所有敌人的血量、速度、金币奖励。levelConfig.json: 定义所有关卡的敌人波次、初始金币、地图背景等。 在游戏启动时加载这些配置脚本通过ID或名称读取。调整游戏数值只需要改配置文件无需重新编译代码。事件驱动通信减少脚本间的直接耦合。使用Cocos Creator内置的cc.systemEvent或自己实现一个简单的事件中心。// EventManager.js const EventManager { _events: {}, on(event, callback, target) { /*...*/ }, off(event, callback, target) { /*...*/ }, emit(event, ...args) { /*...*/ } }; // 在 Tower 攻击时 EventManager.emit(TOWER_FIRED, this, this.currentTarget); // 在 UI 或成就系统中监听 EventManager.on(TOWER_FIRED, (tower, target) { // 更新数据或播放音效 }, this);这样Tower不需要知道谁关心它开火只需要发出事件。成就系统、音效系统、数据统计系统可以独立地监听它们感兴趣的事件。状态管理对于复杂的游戏状态如当前选中的塔、游戏是否暂停、是否在释放技能可以集中到一个GameState对象或使用类似Flux/Vuex的状态管理思路虽然对于小游戏可能过重避免状态散落在各处难以调试。6. 从开发到发布的完整流程与避坑指南当你基于这份源码完成了自己的创意改造下一步就是把它打包发布让更多人玩到。这个过程也有不少需要注意的地方。6.1 跨平台适配与测试Cocos Creator的优势之一就是一次开发多平台发布。但在发布前必须进行充分的平台测试。分辨率与适配在Canvas组件上设置好设计分辨率如1920x1080并选择合适的适配策略Fit Height,Fit Width,Show All等。在编辑器里多切换几种预览分辨率如全面屏手机、iPad、PC宽屏检查UI是否错位、场景是否显示完整。输入差异Web/PC主要是鼠标事件cc.Node.EventType.MOUSE_DOWN等。Mobile主要是触摸事件cc.Node.EventType.TOUCH_START等。确保你的点击建塔、英雄移动等操作在触摸屏上同样流畅。注意移动端可能存在“点按”和“拖拽”的冲突需要精细处理事件冒泡。性能 profiling使用Chrome DevTools对于Web平台或Cocos Creator自带的Profiler工具在真机特别是低端安卓机上运行游戏查看CPU、内存、Draw Call、渲染时间Frame Time。重点观察敌人数量多、特效密集时的帧率。针对瓶颈进行优化如减少活动粒子数量、使用更简单的碰撞检测、合并Draw Call。音频处理移动端浏览器对音频播放有严格限制通常需要用户交互后才能播放。使用Cocos Creator的cc.audioEngine并确保背景音乐、音效在适当的时机如点击开始按钮后加载和播放。考虑提供静音开关。6.2 资源管理与包体优化游戏最终要下载到用户设备包体大小直接影响首次加载速度和用户流失率。纹理压缩对于不同平台使用合适的纹理压缩格式如Web平台用PVRTC、ETC、ASTC注意兼容性。在Cocos Creator的资产属性面板中可以为每个纹理设置压缩格式。剔除未使用资源在构建发布时Cocos Creator默认会只打包被场景或资源依赖引用的资源。但仍需手动检查assets目录删除那些在旧版本中创建但已不再使用的图片、预制体、声音文件。代码分包与动态加载如果游戏内容很多可以考虑将部分资源如后续关卡的场景和资源放在远程服务器在需要时通过cc.assetManager.loadBundle动态加载。这能显著减少初始包体大小。小图合并如前所述使用纹理图集能减少网络请求数量和内存碎片。6.3 常见问题排查与调试技巧开发过程中你一定会遇到各种奇奇怪怪的问题。这里记录几个典型场景和解决思路。问题现象可能原因排查步骤与解决方案点击建塔按钮没反应1. 按钮事件未绑定或绑定错误。2. 按钮节点或被其父节点设置了active: false。3. 有其他UI元素如全屏遮罩拦截了点击事件。1. 检查按钮的cc.Button组件Click Events列表是否添加了正确的事件处理函数。2. 在编辑器的场景树中检查按钮及其父链节点的激活状态小眼睛图标。3. 检查层级关系确保按钮在可点击的最上层。可以使用编辑器中的“调试”模式查看节点层级和包围盒。敌人不移动或移动诡异1. 路径点数组waypoints为空或顺序错误。2. 敌人的移动速度speed为0或极小。3.update函数未被调用脚本未启用或节点未激活。4. 坐标计算错误世界坐标与本地坐标混淆。1. 在Enemy的start或onLoad函数中打印this.waypoints的长度和位置确认数据正确传入。2. 检查Enemy脚本中moveSpeed属性的值。3. 确认敌人节点和Enemy脚本组件是否激活勾选框。4. 确保所有位置计算都在同一坐标系下。使用node.convertToWorldSpaceAR或node.parent.convertToNodeSpaceAR进行必要的坐标转换。游戏运行一段时间后越来越卡1. 内存泄漏节点未正确销毁持续增加。2. 对象池未正确使用节点不断被创建且未销毁。3. 存在未清理的计时器this.schedule或事件监听。1. 使用Chrome DevTools的Memory面板定期进行堆快照Heap Snapshot比较快照查看cc.Node等对象数量是否持续增长。2. 检查子弹、敌人、特效的生成和回收逻辑确保不用时是放回对象池或destroy。3. 在节点的onDestroy生命周期函数中取消所有由该节点注册的计时器和事件监听。塔的攻击范围显示错乱1. 范围指示器一个圆形Sprite的缩放scale未根据attackRange正确设置。2. 范围指示器的锚点anchor不在中心导致显示偏移。1. 范围指示器的大小应该与attackRange数值匹配。如果指示器图片的原始半径是R像素设计分辨率下1单位对应N像素那么缩放应为(attackRange * 2 * N) / (R * 2)实际上更简单的方法是将指示器节点作为一个子节点挂在塔下根据attackRange动态设置其scale。indicatorNode.scale attackRange / baseRadius。2. 将范围指示器Sprite的锚点Anchor设置为(0.5, 0.5)确保它以中心点缩放。调试心法善用Cocos Creator编辑器的“调试”模式可以实时查看节点属性、修改变量值。对于复杂逻辑多使用cc.log或console.log输出关键变量的中间状态。对于性能问题Profiler是你的最佳伙伴。记住遇到问题先定位是逻辑错误、数据错误还是性能问题再分析最后解决。盲目修改代码只会让问题更复杂。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进