ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Cocos2d-x双人坦克大战Demo开发实战:碰撞检测与玩法验证

Cocos2d-x双人坦克大战Demo开发实战:碰撞检测与玩法验证 简介这是一份面向Cocos2d-x初学者与游戏开发实践者的双人坦克对战Demo项目旨在通过完整可运行的游戏实例帮助开发者掌握角色移动、攻击逻辑、碰撞检测等核心2D游戏机制并验证玩法可行性。资源共250个文件涵盖36个头文件h与25个源文件cpp构成主体逻辑75张PNG素材支撑视觉表现20个plist管理纹理图集辅以音效wav/mp3、场景配置tmx、构建脚本bat/sh及多平台工程文件vcxproj/sln/pbxproj压缩包仅2.62MB轻量易上手。已有36人下载学习适合用于课堂实验、自学练手或框架入门验证。读者可直接编译运行双人对战流程深入理解HelloWorldScene、GameScene、Tank等关键类的设计结构掌握Cocos2d-x的场景管理、动作调度、输入响应与物理交互实现路径。 做游戏开发这些年我见过不少人一上来就追着高大上的引擎特性跑结果项目烂尾。但“能用最简单的方式把核心玩法跑通”反而成了稀缺能力。这个cocos2d-x双人坦克大战demo就是一个特别典型的例子——它不是为了做产品而是为了回答两个问题“我想做的玩法到底好不好玩”以及“我到底熟不熟悉这个引擎”。这篇文章就围绕如何用cocos2d-x实现这个demo来聊从选型、地图逻辑、坦克操控、碰撞检测到双人对战体验设计把思路和踩过的坑一次性说清楚。适合正在用cocos2d-x练手、或者准备验证核心玩法的朋友参考。1. 项目定位与整体设计思路1.1 为什么选cocos2d-x做玩法验证很多人会问现在引擎这么多Unity、Godot、甚至H5的Phaser为什么还要折腾cocos2d-x我的观点是cocos2d-x在“验证玩法”这件事上有一个不可替代的优势它的逻辑层和渲染层分离得比较干净能让开发者强制性地去思考游戏对象之间的数据关系而不是被可视化编辑器的便利性带跑偏。尤其是C版本的cocos2d-xAPI设计比较直观Scene、Layer、Sprite、Action、EventListener这几个核心概念掌握之后整个游戏的基本骨架就出来了。写这个demo时我发现它迫使你把坦克、地图、子弹、碰撞这些模块都当成独立的类来设计而不是像在可视化编辑器里那样“拖拖拽拽就完事”。这对于验证玩法的帮助非常大因为你必须清晰地定义每个对象的属性和行为玩法逻辑才能跑起来。另外cocos2d-x的性能也足够支撑这类2D游戏即使不开高帧率在普通电脑上跑一个双人游戏也毫无压力。而且它的坐标系统、动作系统、调度器这些机制对于理解游戏循环的本质非常有帮助这些底层逻辑放到其他引擎里依然是通用的。1.2 双人同屏坦克大战的核心需求拆解这个demo要验证的玩法说直白点就是“双人同屏互相射击比谁先被干掉”。听起来简单但拆解下来涉及的模块并不少我列一下自己的需求清单模块核心需求预期实现方式地图固定墙壁布局坦克无法穿越瓦片地图或二维数组碰撞矩阵坦克双人分别控制移动射击键盘事件监听单例管理子弹飞行、碰撞墙壁/坦克后销毁对象池或动态创建碰撞检测碰撞坦克-墙壁、子弹-墙壁、子弹-坦克AABB包围盒碰撞检测游戏状态生命值、胜利判定、重新开始游戏状态机或简单的数据保持单例UI双方生命数、胜负提示Label 菜单刷新这个列表看起来好像没什么特别的但真正动手做的时候才发现每个模块都有不少细节要处理。比如地图如果用二维数组就要考虑坐标和数组下标的映射如果用瓦片地图就要处理动态加载和碰撞层坦克的转向和移动动画如何衔接子弹的出生点怎么算才能不卡在坦克模型里。一开始我采用的是最笨的办法——直接在一张TMXTiledMap上摆砖块然后通过getLayer(collision)来读取碰撞格子。后面考虑到demo的移植性和修改便利性改成了二维数组手动绘制这样以后换地图也好换直接改数组内容就行不用每次都用地图编辑器。1.3 这个demo能验证什么玩法玩法的核心是双人对抗那对抗的乐趣来自哪里我认为主要来自三件事地图的阻挡与视野、坦克的移动节奏、子弹的飞行速度与冷却。这三者能组合出很多策略空间比如地图改得复杂一些玩家就会更多地迂回而不是正面硬刚子弹改成三连发节奏就完全不一样。所以这个demo在实现时我特意把这些参数暴露在配置常量或UserDefaults里比如子弹速度、移动速度、射击CD、坦克血量这样后续调玩法时直接改数值就行不用动逻辑代码。这也是“验证玩法”的关键——能快速迭代数值才叫验证否则就是写死逻辑的自娱自乐。2. 核心技术点拆分与实现方案2.1 坐标系统与地图对齐cocos2d-x的坐标系统是OpenGL风格的原点在屏幕左下角x向右y向上。而瓦片地图的格子通常是从左上角开始编号。这个差异在第一版里坑了我不少时间。比如我在地图数组里定义map[10][10]想在第2行第3列放一堵墙直接按数组坐标换算成像素坐标时就会出现y方向颠倒。正确做法是确定好锚点和对齐方式。我采用的是// 把二维数组的 [row][col] 转成屏幕坐标 // tileSize 是瓦片像素尺寸mapRows 是总行数 float x col * tileSize tileSize / 2; float y (mapRows - row - 1) * tileSize tileSize / 2;这样数组的第0行就在地图顶部和人的直觉一致。如果是用cocos的TMX地图直接用getPositionAt(ccp(col, row))即可地图编辑器里从上到下编序号和数组下标天然对齐不用额外转换。但为了让后续换地图更灵活我最终还是选择了数组方式自己维护一个CollisionMatrix类。另外坐标对齐这件事还涉及坦克中心的定位。坦克精灵的锚点设置为(0.5, 0.5)后中心点就是位置点碰撞检测时直接用中心点和尺寸算AABB即可。但注意碰撞检测最好要同步考虑地图索引比如坦克位置在某两个格子的交界处这时就要把AABB和地图格子重叠的部分都检测出来否则会有穿墙的小缝隙。2.2 坦克对象的设计与移动坦克对象比想象中要稍微复杂一些因为它要同时考虑位置、方向、移动状态、射击冷却。在我这个demo里坦克类设计如下// Tank.h 核心接口 class Tank : public cocos2d::Sprite { public: enum class Direction { Up, Down, Left, Right }; void bindKeyboard(); void setMoveSpeed(float speed); void setBulletCD(float cd); void shoot(); void takeDamage(int dmg); bool isAlive() const; CREATE_FUNC(Tank); private: Direction _dir; float _speed; float _bulletCD; float _cdTimer; int _hp; bool _alive; };移动这里用了一个比较特殊的方式没有用MoveTo或MoveBy动作而是在update方法里每帧根据键盘状态修改位置。// 在 update 中根据键盘状态移动 if (_keys[EventKeyboard::KeyCode::KEY_LEFT_ARROW]) { _dir Direction::Left; this-setPositionX(getPositionX() - _speed * dt); } if (_keys[EventKeyboard::KeyCode::KEY_UP_ARROW]) { _dir Direction::Up; this-setPositionY(getPositionY() _speed * dt); }有人会觉得用Action不是更简单吗但这里有个关键问题——Action更适合单向的、预设好的动画而键盘操控是持续的、需要实时响应玩家输入的状态如果强行用Action会让代码变得非常绕。所以最稳妥的方案就是自己维护速度在update里逐帧计算。对移动类游戏来说这种方式才是符合游戏循环本质的。坦克在转向时需要同步改变精灵朝向最有效的方法是用setRotation或者替换不同的材质帧。我当时偷懒直接用了setRotation结果发现0度和180度之间旋转时坦克模型看起来不够自然。后来换成了四方向帧动画分别对应四个方向的坦克贴图效果立刻就好多了。2.3 子弹系统与对象池子弹是这个demo里最频繁创建和销毁的对象。如果每开一枪就new一个Sprite打几炮就退出或掉帧这肯定不行。子弹场景里通常不会同时存在太多子弹但为了养成好习惯我直接实现了对象池BulletPool。class BulletPool { public: static BulletPool* getInstance(); Bullet* getBullet(); // 从池中取没有则创建 void recycleBullet(Bullet* b); // 回收 private: std::vectorBullet* _pool; };子弹的飞行也用了类似坦克的方式通过scheduleUpdate每帧更新坐标并执行碰撞检测。void Bullet::update(float dt) { setPosition(getPosition() _velocity * dt); // 检查是否出界 if (getPositionX() 0 || getPositionX() visibleSize.width || getPositionY() 0 || getPositionY() visibleSize.height) { die(); } }子弹的_velocity就是direction * speed。方向由坦克发射时的朝向决定速度设置一个常量。这里要注意子弹速度如果太快一帧内可能穿过墙壁而检测不到碰撞。解决办法是分帧移动或增大碰撞检测的容错范围。我采用的是简化方案——限制子弹最大速度碰撞检测时把子弹上一帧位置和当前帧位置连成线段去做线段与矩形的相交检测。不过这个做法对初学者来说有点复杂如果你只是验证玩法限制速度就完全够用。还有一个坑是子弹从坦克身上飞出时容易碰到坦克自己。我在发射时把子弹的起始位置稍微往前进方向偏移了半个坦克宽度的距离同时在碰撞检测时忽略发射者本身这样就解决了误杀自己的问题。2.4 碰撞检测方案这个demo里碰撞的核心是AABB轴对齐包围盒。因为坦克、子弹、墙壁基本都是矩形直接用矩形相交判断即可。bool checkCollision(const Rect a, const Rect b) { return a.intersectsRect(b); }但在具体应用时还要区分碰撞类型。我有三个碰撞处理函数void handleBulletWall(Bullet* bullet); void handleBulletTank(Bullet* bullet, Tank* target); void handleTankWall(Tank* tank);坦克和墙壁的处理有点麻烦因为如果检测到坦克会和墙壁重叠你需要把坦克“推”回去或者干脆禁止这次移动。我用的方案是计算坦克移动后的新位置判断新位置的AABB是否与任何墙壁格子相交若相交则取消移动或只允许沿未碰撞方向的轴移动。// 简易做法分别检测X和Y方向 float newX tank-getPositionX() deltaX; float newY tank-getPositionY() deltaY; if (!checkTankMapCollision(newX, tank-getPositionY(), tank)) { tank-setPositionX(newX); } if (!checkTankMapCollision(tank-getPositionX(), newY, tank)) { tank-setPositionY(newY); }这个做法在大多数2D俯视角游戏中都适用好处是即使斜向移动坦克也能在碰到墙壁时沿墙滑动手感会非常好。2.5 双人操控与事件监听双人控制的实现其实不复杂核心就是分别监听两套按键。在cocos2d-x里EventListenerKeyboard可以同时监听多个键盘事件只要在回调里判断是哪个键就能实现区分。我的键位设定是玩家上下左右射击P1WSADJP2↑↓←→小键盘0或空格在onKeyPressed里记录按键状态在onKeyReleased里释放按键状态。注意键盘事件回调里的keyCode是cocos2d::EventKeyboard::KeyCode枚举判断时直接比较即可。auto listener EventListenerKeyboard::create(); listener-onKeyPressed [](EventKeyboard::KeyCode keyCode, Event* event) { if (keyCode EventKeyboard::KeyCode::KEY_A) { keys[0].left true; } // 其余按键类似 }; listener-onKeyReleased [](EventKeyboard::KeyCode keyCode, Event* event) { if (keyCode EventKeyboard::KeyCode::KEY_A) { keys[0].left false; } };然后坦克在update里根据keys数组的状态来移动。注意事件监听要在addEventListenerWithSceneGraphPriority中注册否则收不到消息。还有一个细节是如果你在onKeyPressed里直接触发射击会因为键盘重复触发机制导致连发频率不稳定。所以我射击也是通过状态位在update里检查_isFirePressed _cdTimer 0时才发射这样更加可控。3. 完整实现从建项目到双人开打3.1 创建工程与初始化Scene一开始用cocos new命令创建项目时我选择了带物理引擎的模板其实不需要cocos2d-x的物理引擎在这个demo里反而会带来额外复杂度纯手动AABB检测就足够了。cocos new TankBattle -p com.demo.tank -l cpp -d ./TankBattle创建之后开发流程是这样的在AppDelegate::applicationDidFinishLaunching里把mainScene从默认的HelloWorld::createScene()换成自己的GameScene::createScene()。在GameScene::init()里依次加载地图、创建坦克、创建UI。在GameScene::onEnter里注册键盘监听。在GameScene::update里进行游戏逻辑更新。上面这个顺序如果反了很可能出现监听器还没注册就开始update导致空指针的情况。所以建议严格按这个顺序执行。3.2 地图绘制与碰撞矩阵加载这里我的地图定义方式用了最简单的字符串数组const std::string mapData[13][13] { {0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,1,1,0,0,0,0,0,0,0,1,1,0}, // ... };其中0代表空地1代表墙壁。初始化时遍历数组如果是1就在对应位置创建一个Sprite加入场景。同时把墙壁矩形的信息存进一个std::vectorRect方便碰撞检测遍历。用二维数组的好处是简单直接改地图就改数组不用启动Tiled编辑器。坏处是如果地图很大内存占用会多点但demo阶段完全无所谓。墙面材质我用的是直接创建一个单色矩形auto wall Sprite::create(); wall-setTextureRect(Rect(0, 0, tileSize, tileSize)); wall-setColor(Color3B(169, 169, 169));这样连美术资源都不需要前期调试非常方便。等玩法验证满意后再贴图也不迟。3.3 游戏主循环与逻辑更新顺序在Scene里打开scheduleUpdate()然后在update(float dt)里按顺序执行void GameScene::update(float dt) { // 1. 处理输入 _tank1-handleInput(dt); _tank2-handleInput(dt); // 2. 更新子弹 _bulletPool-updateBullets(dt); // 3. 碰撞检测 checkBulletWallCollision(); checkBulletTankCollision(); checkTankWallCollision(); // 4. 更新UI updateHUD(); // 5. 胜负判断 checkWinCondition(); }这个顺序看起来简单但背后有很多细节。比如碰撞检测最好在位置更新之后、绘制之前执行否则玩家会看到坦克穿模一瞬间再比如胜负判断最好放在最后避免在胜负已分的情况下还让子弹继续飞。dt是每帧的间隔时间用它来乘以速度可以保证在不同帧率下运动距离一致。如果你的电脑跑在144Hz别人跑在60Hz相同逻辑下不会出现一快一慢的情况。3.4 完整的坦克移动实现这里我把坦克移动的完整代码贴出来这是在update中调用的核心部分void Tank::handleInput(float dt) { Vec2 moveDir Vec2::ZERO; if (_keyState.up) moveDir.y 1; if (_keyState.down) moveDir.y - 1; if (_keyState.left) moveDir.x - 1; if (_keyState.right) moveDir.x 1; // 归一化方向防止斜向移动速度变快 if (moveDir.length() 0) { moveDir.normalize(); setPosition(getPosition() moveDir * _speed * dt); updateDirection(moveDir); } // 射击逻辑 _cdTimer - dt; if (_keyState.fire _cdTimer 0) { shoot(); _cdTimer _bulletCD; } }关键点有两个一是归一化方向向量否则斜向移动时速度会是sqrt(2)倍二是射击冷却如果不加CD按住射击键就会像机关枪一样玩法体验会很差。我建议把移动和射击分离成不同的函数方便后续为AI或者手柄控制做扩展。只要传入相同的KeyState逻辑完全一致这样单人调试也会方便很多。3.5 UI与胜负显示UI层我用了一个Layer来承载包含左右上角各一个Label显示P1/P2生命数。中间偏上的提示Label显示胜利者。一个“重新开始”按钮点击后重载场景。注意Label在cocos2d-x里建议使用Label::createWithTTF或者Label::createWithSystemFont前者需要字体文件后者用系统字体更快。auto hpLabel Label::createWithSystemFont( Player1 HP: 3, Arial, 24); hpLabel-setPosition(Vec2(80, visibleSize.height - 40)); this-addChild(hpLabel, 10);当一方生命为0时显示“Player X Win!”然后停止更新逻辑只保留UI的点击响应。重新开始直接把Director::getInstance()-replaceScene(GameScene::createScene())就完事了。3.6 调试与环境配置的补充我在Visual Studio里开发的为了方便调试把窗口分辨率固定在了960x640这样地图可以设置为15x10个格子每个格子64像素。如果分辨率太高地图就要做大调试时窗口太大反而看不清边界。另外建议开一个调试开关把碰撞盒用DrawNode画出来这样能直观地看到坦克和墙壁的碰撞范围是否和视觉范围匹配。这个对排查穿墙和子弹穿模问题特别有用。#if COCOS2D_DEBUG 0 auto debugDraw DrawNode::create(); debugDraw-drawRect(tankRect.origin, tankRect.origin tankRect.size, Color4F(0, 1, 0, 1)); tank-addChild(debugDraw, 50); #endif4. 常见问题与排查技巧实录4.1 坦克穿墙问题实测中最常见的问题是坦克斜着移动时偶尔会卡进墙里。排查后发现是碰撞检测时只检测了坦克中心点所在的格子而没有检测整个坦克覆盖的格子范围。坦克宽高是40x40如果贴图64像素一格那么一个坦克可能同时跨4个格子只检测中心点必然会漏。修复方法算出坦克AABB覆盖的所有格子索引逐一检测是否有墙。bool GameScene::checkTankMapCollision(const Rect tankBox) { int startCol tankBox.getMinX() / tileSize; int endCol (tankBox.getMaxX() - 1) / tileSize; int startRow (mapRows - 1) - tankBox.getMaxY() / tileSize; int endRow (mapRows - 1) - tankBox.getMinY() / tileSize; for (int row startRow; row endRow; row) { for (int col startCol; col endCol; col) { if (mapData[row][col] 1) return true; } } return false; }这个方法对墙壁是规则矩形的情况非常好用对不规则墙体或者动态墙体就需要更精细的处理但对这个demo来说完全够用了。4.2 子弹穿过墙壁子弹速度太快时会出现上一帧还在墙左边、下一帧到了墙右边的情况根本检测不到碰撞。有三个方案方案优点缺点限制最大速度简单改动小子弹手感受限多段移动模拟连续运动性能略增逻辑稍复杂线段相交检测理论上可检测任意高速实现繁琐容易出bug我最终选了限制最大速度把子弹速度控制在500px/s在64像素格子下每帧最多移动8.3像素左右完全不会穿墙。如果你想做狙击枪这类高速弹道那就得用线段相交检测或者分段移动了。4.3 双人按键冲突这里说的冲突不是指物理上的键位冲突而是指事件监听器发生重复注册导致一个按键触发两次逻辑。有几次我在onEnter和init里各自注册了一次键盘监听结果坦克移动速度变成了两倍排查了半天才发现监听器重复了。建议在场景的init里统一注册监听不要分散到多个地方。另外如果是跨场景别忘了在监听器回调中event-stopPropagation()防止向下传递尤其是在有UI层的情况下。listener-onKeyPressed [](EventKeyboard::KeyCode keyCode, Event* event) { event-stopPropagation(); // 处理按键 };4.4 子弹误伤发射者坦克发射子弹后由于子弹初始位置和坦克重叠会在第一帧触发子弹与坦克的碰撞导致坦克自杀。解决方法是子弹初始化时设置一个shooter指针碰撞检测时跳过该指针指向的坦克。void GameScene::checkBulletTankCollision() { for (auto bullet : _bullets) { if (bullet-getShooter() ! _tank1 bullet-getBoundingBox().intersectsRect(_tank1-getBoundingBox())) { _tank1-takeDamage(1); bullet-die(); } // 同理检查坦克2 } }4.5 对象池回收时机子弹飞出边界或者碰撞后要立刻置为不可见并从活动列表中移除。如果不回收碰撞检测会遍历大量无效子弹性能下降非常明显。我使用一个std::vectorBullet*保存活着的子弹死亡时移动到_freeList中取用时再从_freeList弹出。这里注意在update时不要遍历删除元素否则会导致迭代器失效。正确做法是标记待删除统一在循环结束后处理或者用removeChild但先把子弹从数组摘出来。_bulletPool-updateBullets(dt); // 内部会收集待回收子弹 // 结束后统一回收 for (auto b : _bulletsToRecycle) { b-disable(); _bulletPool-recycle(b); } _bulletsToRecycle.clear();4.6 场景切换后监听器失效如果你在A场景注册了键盘监听切换场景后没有移除监听B场景的监听可能收不到事件甚至崩溃。cocos2d-x的EventListener默认绑定到当前场景的EventDispatcher如果场景被释放而监听器还挂在Director上就会野指针。建议在onExit里移除监听器清单void GameScene::onExit() { _eventDispatcher-removeEventListener(_keyListener); Layer::onExit(); }这个在反复切换场景时特别重要否则游戏玩到第三局就可能崩掉。5. 进阶优化与玩法扩展5.1 从demo到可玩demo数值与手感调优验证玩法的最终目的是把“手感”调出来。第一次写出来的坦克移动速度一快就容易过头一慢就感觉像在拖泥带水。我总结了一套调优流程先把坦克移动速度调到1000感觉明显太快之后逐渐降档。把子弹速度调高观察玩家是否能躲开如果不能适当降低或缩短坦克射击CD。把生命值从1调到3让一局比赛时间变长给玩家更多的试探机会。地图设计先简单后复杂确认双人能看到彼此的情况下对抗策略是什么再逐渐增加墙壁掩护。关于手感还有一个细节坦克转向时最好在转向瞬间消耗少量时间或者加一个最小转向帧否则游戏中会出现极高频的“转头射击”让操作者自己都看不清自己在哪。这个小调整能让对抗的节奏变慢策略感更强。5.2 增加道具系统验证更深玩法双人坦克大战最经典的扩展项就是道具。你可以快速做一个简单的道具系统地图上随机生成“护盾”、“双发”、“加速”三种道具坦克碰到后获得buff持续5秒。实现逻辑和碰撞检测完全一致只是多一个道具生成器class ItemBox : public cocos2d::Sprite { public: enum class ItemType { Shield, DoubleShot, SpeedUp }; ItemType getType() const { return _type; } private: ItemType _type; };道具的加入会极大地丰富对抗的变数也能帮你更全面地验证玩法机制。我强烈建议在基础demo跑通后优先加这个功能。5.3 后续扩展方向这个demo继续往下走的几个方向增加AI坦克做单人闯关模式。引入地图编辑器自动生成随机地图。双人联机把本地双人改成网络同步。增加更多坦克类型区分血量、速度、子弹形态。5.4 代码组织的经验总结写这个小demo让我最深刻的体会是游戏代码的模块化程度和玩法验证效率直接挂钩。如果你的子弹、地图、坦克都揉在GameScene一个类里改数值或者加功能会越来越痛苦。相反如果你一开始就按对象拆分后期每个功能模块都是独立的调试和扩展都会轻松很多。我一直保持着这样的习惯先画类图再写代码。即使是一个demo类图中的每个类都有明确的职责边界代码写起来会非常顺手。比如Tank只负责移动、射击、受伤不负责碰撞检测GameScene只负责组织和协调不负责坦克的具体行为。这种清晰的边界让整个项目即使代码只有几千行维护起来也不会手忙脚乱。6. 项目运行效果与实测体验这个demo实际跑起来是什么样的我描述一下两台坦克从地图两端出发中间有几堵墙挡着玩家1用WASD控制玩家2用方向键控制。一开始大家都会习惯性地向前冲然后撞墙然后慢慢学会绕路。当第一次子弹擦着墙边飞过去、命中对方坦克时那种爽快感真的能让人瞬间理解这款经典游戏为什么耐玩。我特意在墙边留下了几个窄通道玩家可以隔着墙对射也可以选择绕后偷袭。试玩几次下来发现地图上的墙布局直接决定了对抗的风格——墙多则偏策略墙少则偏反应。通过调整地图这个demo能模拟出两种完全不同的玩法方向这对验证需求来说实在太有价值了。性能方面在普通笔记本上稳定60帧没有任何压力CPU占用也不高。cocos2d-x在这类轻量2D游戏上的表现值得信赖。整个项目从建工程到跑通大概花了两天时间其中大部分时间花在碰撞调试和手感调优上。如果你也想用cocos2d-x验证一个玩法这个坦克大战demo绝对是个很好的起点——麻雀虽小五脏俱全几乎所有2D游戏要处理的模块它都覆盖了。最后再说一个小技巧把地图数据、速度参数、冷却时间这些抽取到独立配置文件里用FileUtils读取JSON或plist。这样你想调玩法时根本不用重新编译直接改配置就能跑起来看效果。这也是我后来做游戏验证时始终坚持的习惯在项目初期收益也许不明显但随着数值维度增多它节省的时间会非常可观。本文还有配套的精品资源点击获取
返回列表