这是Cocos2D-X砖块地图教程系列,你将在此创造一款有关沙漠中的忍者寻找美味的西瓜的简单游戏。
需要注意的是该教程是关于Cocos2D-X,即Cocos2D-iPhone的跨平台C++移植。所以你在此编写的代码将适用于iPhone,Android和更多平台上!
在本系列文章的第一部分中,你将学习如何添加砖块地图到游戏中,跟着玩家滚动地图,并使用对象图层。你将学到如何使用地图编辑器去创造砖块地图本身。
而第二部分是关于如何在地图上创造碰撞领域,如何使用砖块属性,如何创造可收集的道具并动态地修改地图,以及如何确保你的忍者不会吃太多东西。
注:本篇教程类似于Cocos2D-iPhone教程。
让我们开始创造砖块地图吧!
开始
对于这一教程,你需要安装最新的Cocos2D-X版本(游戏邦注:在写本篇教程的时候更新到2.1.4)。如果你还未拥有最新版本的Cocos2D-X,先下载它并在终端运行如下命令去安装模版:
- cd ~/Downloads/cocos2d-x-2.1.4
- ./install-templates-xcode.sh -f -u
然后使用iOS\cocos2d-x\cocos2dx模版在Xcode创造一个新项目。点击Next,将项目命名为TileGame,将项目设置为Universal,点击Next然后点击Create。
你将在这一项目中使用ARC,所以如果这是你第一次听到ARC,我会鼓励你先了解下它。模版并不是默认使用ARC,但幸运的是,我们能够轻松地进行 修改。前往Edit\Refactor\Convert to Objective-C ARC。往下拉并只选择文件main.m, AppDelegate.cpp, HelloWorldScene.cpp,然后点击Check并完成向导的步骤。
创建并运行,然后确保一切都还正常运行—-你应该能够看到标准的“你好世界”屏幕。
接下来下载游戏资源的压缩文件。压缩文件包含如下内容:
你将面向玩家对象使用的精灵。
一些伴随着cfxr效用所创造的音效(你将会在教程中用到)。
一些伴随着Garage Band所创造的背景音乐。
你将用到的一些砖块设置—-这将伴随着你将使用的地图编辑器,但我认为我们能够更轻松地将其与其它内容包含在一起。
一些额外的“特别”砖块,将在之后进行详细解释。
当你下载了资源后,打开它并将TileGameResources文件夹拖到项目的Resources群组中。在项目菜单里,右击 Resources群组,并选择Add Files to “TileGame”…选择Resources/TileGameResources文件夹,核实选中了Copy items into destination group’s folder (if needed)以及Create groups for any added folders,然后点击完成。
如果一切顺利的画,所有的文件都将出现在你的项目中。
你的项目应该如下:
现在我们将开始创造地图!
创造地图
Cocos2D-X支持基于开放源Tiled Map Editor去创造地图并将其以TMX格式进行保存。
下载Tiled Map Editor。在编写本篇教程的时候,其最新版本是0.9.0。
然后运行Tiled,前往File\New,并如下填写对话内容:
在定向区域中,你可以在Orthogonal或Isometric间做出选择。在此你将选择Orthogonal。
接下来你将设置地图的大小。记住这是在砖块中,而不是像素中。你将创造一个较小的地图,所以在此你应该选择50×50。Tiled将基于像素呈现给 你总体地图的大小,即在New Map对话的最底部。这是在长度和宽度的基础上将地图大小(50个砖块)乘以砖块的大小(32像素)所计算出来的。
最后,你将明确宽度和高度。你在此所选择的是取决于美术人员所设置的砖块。对于本篇教程,你将使用一些伴随着Tiled编辑器的样本砖块,即32×32规格,选择它便点击OK。
接下来你必须添加砖块设置去绘制你的地图。在菜单栏上点击Map,然后关掉New Tileset…,并如下填写对话框内容:
为了获得图像,点击Browse并导航至你自己的TileGame/Resources/TileGameResources文件夹,然后选择你之前从资源压缩中下载的tmw_desert_spacing.png文件,并将其添加到项目中。它将自动根据文件名填写名字。
你可以将宽度和高度设置为32×32,因为这也是砖块的大小。对于边缘和间隔:
边缘是关于在Tiled开始寻找真正的砖块像素前应该为当前的砖块略过多少多少像素(包括宽度和高度)。
间隔是关于Tiled在明确了实际砖块像素并转向下一个砖块数据之后应该前进多少像素(包括宽度和高度)。
如果你着眼于tmw_desert_spacing.png,你将发现每个砖块都围绕着一个1像素的黑色边缘,这也解释了边缘和间隔为1的设置。
当你点击OK时,你将看到砖块呈现在Tilesets窗口中。现在你可以开始绘制了。点击工具栏的Stamp Brush图标,然后点击地图上的任何一个位置去放置一个砖块。
所以继续绘制地图—-尽可能发挥创造性!确保添加至少一些建筑到地图上,因为你在之后将需要一些碰撞内容。
为了更轻松地绘制内容,你可以着眼于一些快捷方法。以下是最常用到的一些方法:
你可以在Tileset选择器中围绕着一系列砖块拖曳一个盒子,并同时放下多个相邻的砖块。
你可以通过View\Zoom In和View\Zoom Out进行放大和缩小。
z键将在基于Stamp Brush工具编辑地图时进行旋转。
在一些新功能中你可能会注意到Mini-map。这是一个很棒的功能,它让你能够看到一个迷你地图!着眼于我在Mini-map最下方的迷宫中的糟糕尝试。红色盒子代表你在主要编辑窗口中看到的区域。
当你在阅读下一个区域中的滚动时牢牢记住这一Mini-map视图。
需要注意的是这一教程的资源是出现在地图前的——所以如果你很懒的话便可以直接利用它。如果你这么做,你应该在Tiled打开地图并明确它是如何设置的。
当你完成地图的绘制时,在Layers视图中双击Tile Layer,将名字改为Background。然后点击File\Save,并将文件保存到TileGame项目中的TileGame \Resources\TileGameResources,将文件命名为TileMap.tmx,并覆盖现有的文件。
你将在之后使用Tiled做其它事,但是现在让我们将这一地图带进游戏中!
添加Tiled地图到Cocos2D-X场景中
打开HelloWorldScene.h,在#include “cocos2d.h”之后添加如下内容:
- using namespace cocos2d;
这能指导编辑器去使用cocos2d命名空间,所以你不需要为所有内容加上cocos2d的前缀。
然后添加以下内容到类定义中,即在花括号之后:
- private:
- CCTMXTiledMap *_tileMap;
- CCTMXLayer *_background;
这创造了一个实例变量去追踪砖块地图本身,并创造了另一个实例变量去追踪地图的背景层。你将在之后学到更多有关砖块地图层面的内容。
接下来,用如下内容换掉HelloWorldScene.cpp:
- CCTMXObjectGroup *objectGroup = _tileMap->objectGroupNamed(“Objects”);
- if(objectGroup == NULL){
- CCLog(“tile map has no objects object layer”);
- return false;
- }
- CCDictionary *spawnPoint = objectGroup->objectNamed(“SpawnPoint”);
- int x = ((CCString)*spawnPoint->valueForKey(“x”)).intValue();
- int y = ((CCString)*spawnPoint->valueForKey(“y”)).intValue();
- _player = new CCSprite();
- _player->initWithFile(“Player.png”);
- _player->setPosition(ccp(x,y));
- this->addChild(_player);
- this->setViewPointCenter(_player->getPosition());
最后一行有个预兆——但不要担心,你很快就能到达那里。
让我们暂停一会并解释对象层面和对象群组。首先注意你是通过在CCTMXTiledMap对象中(而不是layerNamed)通过objectGroupNamed方法检索对象层面。它返回了一个特殊的CCTMXObjectGroup对象。
然后objectGroup调用了objectNamed方法去获得一个CCDictionary,并包含了一些有关对象的有用信息,如x和y轴,宽度和高度。在教程的这一部分,你需要关心的便是x和y轴,将其设置为玩家精灵的位置。
在代码块的最后你设置了视图去明确玩家的位置。所以现在添加如下内容到HelloWorldScene.h中:
- // In the public section
- void setViewPointCenter(CCPoint position);
并添加一个新方法到HelloWorldScene.cpp(在文件的最下方最好):
- void HelloWorld::setViewPointCenter(CCPoint position) {
- CCSize winSize = CCDirector::sharedDirector()->getWinSize();
- int x = MAX(position.x, winSize.width/2);
- int y = MAX(position.y, winSize.height/2);
- x = MIN(x, (_tileMap->getMapSize().width * this->_tileMap->getTileSize().width) – winSize.width / 2);
- y = MIN(y, (_tileMap->getMapSize().height * _tileMap->getTileSize().height) – winSize.height/2);
- CCPoint actualPosition = ccp(x, y);
- CCPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
- CCPoint viewPoint = ccpSub(centerOfView, actualPosition);
- this->setPosition(viewPoint);
- }
这是关于砖块的解释。想象这一函数设置了摄像机的中心位置。它让用户能够进入地图中x,y轴的任何位置—-但是你有可能不想呈现出某些点,如你可能不想要屏幕超过地图的边缘(那么它便只会呈现出黑边!)。
如下图:
如果摄像机的中心小于winSize.width/2或winSize.height/2,那么部分视角是否会脱离屏幕?同样的,检查最上方的界限也很重要,这也是setViewPointCenter所做的。
到目前为止这一函数被当成设置了摄像机所面对的中心位置。然而,这并不是它真正做的。这是在Cocos2D-X中操控CCNode的摄像机的一种方法,但使用它会比你将使用的解决方法(移动整个层面)更复杂。
着眼于这一图解:
diagram(from raywenderlich)想象一个大世界,你将着眼于坐标轴,即从0到winSize.height/width。你的视图的中心是 centerOfView,你便能清楚自己想要以哪里为中心(actualPosition)。所以为了用实际位置去匹配视图中心位置,你需要做的便是向 下倾斜地图!
通过从视图中心减去实际位置你便能够做到这点,然后将HelloWorld层面设为该位置。
说了这么多理论,是时候执行它们了!创建并运行项目,如果一切运行正常,你将在屏幕上看到忍者,并且视图会不断移动去呈现他的行动。
让忍者移动
这是个好的开始,但是你的忍者还只是站在那里!这并不像真正的忍者。你将朝着用户敲打的方向移动忍者而让他动起来。添加如下代码到HelloWorldScene.h的公共部分:
- void registerWithTouchDispatcher();
- void setPlayerPosition(CCPoint position);
- bool ccTouchBegan(CCTouch *touch, CCEvent *event);
- void ccTouchEnded(CCTouch *touch, CCEvent *event);
然后打开HelloWorldScene.cpp并将如下代码添加到init:
- this->setTouchEnabled(true);
这将层面设置为可碰触的,所以它将关注于碰触事件。接下来添加如下方法到文件最底端:
- #pragma mark – handle touches
- void HelloWorld::registerWithTouchDispatcher() {
- CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, 0, true);
- }
- bool HelloWorld::ccTouchBegan(CCTouch *touch, CCEvent *event)
- {
- return true;
- }
- void HelloWorld::setPlayerPosition(CCPoint position) {
- _player->setPosition(position);
- }
- void HelloWorld::ccTouchEnded(CCTouch *touch, CCEvent *event)
- {
- CCPoint touchLocation = touch->getLocationInView();
- touchLocation = CCDirector::sharedDirector()->convertToGL(touchLocation);
- touchLocation = this->convertToNodeSpace(touchLocation);
- CCPoint playerPos = _player->getPosition();
- CCPoint diff = ccpSub(touchLocation, playerPos);
- if ( abs(diff.x) > abs(diff.y) ) {
- if (diff.x > 0) {
- playerPos.x += _tileMap->getTileSize().width;
- } else {
- playerPos.x -= _tileMap->getTileSize().width;
- }
- } else {
- if (diff.y > 0) {
- playerPos.y += _tileMap->getTileSize().height;
- } else {
- playerPos.y -= _tileMap->getTileSize().height;
- }
- }
- // safety check on the bounds of the map
- if (playerPos.x <= (_tileMap->getMapSize().width * _tileMap->getTileSize().width) &&
- playerPos.y <= (_tileMap->getMapSize().height * _tileMap->getTileSize().height) &&
- playerPos.y >= 0 &&
- playerPos.x >= 0 )
- {
- this->setPlayerPosition(playerPos);
- }
- this->setViewPointCenter(_player->getPosition());
- }
在此你覆盖了registerWithTouchDispatcher方法去处理目标碰触事件。这将导致ccTouchBegan/ccTouchEnded方法(单数情况)被调用,而不是ccTouchesBegan/ccTouchesEnded方法(复数情况)。
你可能会好奇单数情况和复数情况有什么区别。不过在这种情况下我们没有必要去弄清楚这些问题。但是我还是想向所有人介绍这一方法,因为它带有2个主要优势:
“你不需要处理NSSets,调度程序能够区分它们。每次调用你将获得一个UITouch。”
“你可以通过在ccTouchBegan返回YES而要求一个UITouch。要求碰触的更新只会被发送到要求它们的委托中。所以如果你删除/结束/取消更新,你就需要确保它是你的碰触。这将让你无需在执行多点碰触时做各种检查。”
不管怎样,在你的ccTouchEnded位置上,你像往常那样将位置转换成视图坐标轴,然后再转换成GL坐标轴。而新任务便是你调用了this->convertToNodeSpace(touchLocation)。
这是因为碰触位置将提供给你用户在视口中轻敲的坐标轴(例如100,100)。但是你可能已经滚动了地图,所以它将匹配(800,800)的位置。所以调用这一方法将基于你如何移动层面而抵消碰触。
接下来你将明确碰触点和玩家位置的区别。你将基于碰触选择一个方向,所以首先你应该决定是上下移动还是左右移动。然后你将判断是正数还是复数而进行上下移动。
你将相对地调整玩家位置,然后将视图中心设置为玩家的位置,这是你在上部分便写下的内容!
注意你必须添加一个安全检查以确保不会将玩家带离地图外部!
所以创建并运行项目,然后尝试它!现在你应该能够轻敲屏幕去移动忍者了!
最后
这时候你已经知道如何创造地图并将其整合到游戏中了。而在第二部分教程中你将进一步学习如何添加碰撞检测到地图中以避免忍者能够轻松地穿越墙壁。