程序截图:
这是一张完整的图片:
这是一张打乱了的图片:
)制作这种类型的游戏,最大的好处就是可以为我们接下来学习和理解Tiled
那么,为了制作一个这样的游戏,我们需要做哪些事情呢?下面就是制作这个滑动图片游戏的步骤列表:
- 创建一个“Tile”类,它包含sprite,position(x,y)和value这些实例变量。
- 创建一个管理类,它负责创建所有的Tile,同时可以追踪所有的Tile的状态。
- 添加touch组件,这样玩家可以交换两个tile的位置。
- 添加一些代码来随机加载图片,这样游戏可以有更多的花样。
就这么多,当我把步骤写出来的时候,是不是觉得很简单?(译者:补充一点,看我教程的朋友,不要仅仅局限于具体的技术细节,要多想想游戏创作的步骤,这个游戏有4步,你的游戏需要几步呢?多思考,这样就不会看着教程能做,没教程就无从下手了)接下来,我会一步步实现所有的功能,你会发现其实真的也很简单。
说真的,这个简单的游戏例子,能给你很多启发。。。今后,我将会基于这个系列的教程,给你们介绍更多的这种类型的游戏(译者:比如拼图啦,华容道啦,推箱子啦,连连看啦)
不管怎么说,先让我们实现这个游戏再说!为了完成了这个艰巨的任务,我们需要一些辅助类。这些类是Tile.h, Tile.cpp, Box.h, Box.cpp。
如果你看了本站其它教程的话,你肯定已经知道“SceneManager”和"PlayLayer“类了,这里就不再啰嗦了。如果还没有的话,请参考《cocos2d菜单教程》和《cocos2d精灵教程》。
首先,先让我们看看Tile类的实现。
Tile.h:
```cpp
#include "cocos2d.h"
#include "Constants.h"
class TileElem :
public cocos2d::Object
{
public:
TileElem();
~TileElem();
bool initWithPos(int posX, int posY);
static TileElem* create(int posX, int posY);
bool nearTile(TileElem *otherTile);
void trade(TileElem *otherTile);
cocos2d::Point pixPosition();
CC_SYNTHESIZE_READONLY(int, _x, PosX);
CC_SYNTHESIZE_READONLY(int, _y, PosY);
CC_SYNTHESIZE_RETAIN(cocos2d::Sprite*, _sprite, Sprite);
CC_SYNTHESIZE(int, _value, Value);
};
```
tile类主要就是代表从一张大图里面提取出来的一小块内容,有X,Y位置(注意,这里的x,y坐标不等于精灵的位置坐标,精灵的位置坐标是sprite->getPosition()),有精灵,有value。这里的value可以是任何值 ,比如可以代表每个tile在原图中的位置(这样的话,我们就可以用这些位置来判断玩家是否正确拼成完整的图了),在这个版本中,我们暂时不会使用这个value;
现在,让我们看看其具体实现:
Tile.cpp:
```cpp
#include "Tile.h"
USING_NS_CC;
TileElem::TileElem()
{
}
TileElem::~TileElem()
{
CC_SAFE_RELEASE_NULL(_sprite);
}
TileElem* TileElem::create(int posX, int posY)
{
TileElem *pRet = new TileElem();
if (pRet && pRet->initWithPos(posX, posY))
{
pRet->autorelease();
return pRet;
}
else
{
delete pRet;
pRet = NULL;
return NULL;
}
}
bool TileElem::initWithPos(int posX, int posY)
{
_x = posX;
_y = posY;
_value = 0;
_sprite = Sprite::create();
CC_SAFE_RETAIN(_sprite);
return true;
}
bool TileElem::nearTile(TileElem *otherTile)
{
return (_x == otherTile->getPosX() &&
abs(_y - otherTile->getPosY()) == 1) ||
(_y == otherTile->getPosY() &&
abs(_x - otherTile->getPosX()) == 1);
}
void TileElem::trade(TileElem *otherTile)
{
Sprite *tempSprite = Sprite::createWithTexture(_sprite->getTexture());
tempSprite->setPosition(_sprite->getPosition());
int tempValue = _value;
this->setSprite(otherTile->getSprite());
this->setValue(otherTile->getValue());
otherTile->setSprite(tempSprite);
otherTile->setValue(tempValue);
}
Point TileElem::pixPosition()
{
return Point(kStartX + _x * kTileSize + kTileSize / 2.0f, kStartY + _y * kTileSize + kTileSize / 2.0f);
}
```
大部分内容一看就能明白--我们实现了四个方法 “initWithPos”, “nearTile”, “trade” and “pixPosition”.
“initWithPos”方法,从名字就可以看出来它是做什么的---它是Tile类的初始化代码,它Tile类被初始化的时候被调用。它接收一个x,y值,这两个值和世界坐标无关,而是与包含它们的Box类有关。举个例子,我们使用 initWithPos(3,4),假如我们的box是7*7的话,那么,这个Tile被放置在3,4号位置)我们可以用pixPosition函数来计算每个Tile的精灵在屏幕上的位置。
“nearTile”接收一个Tile类型的参数,判断两个Tile是否是邻居,如果是,就返回true,否则返回false。
“trade”就是把两个Tile的精灵交换一下。交换两个变量,相信学过C语言的都会,定义一个临时变量temp,然后temp = a; a=b; b=temp;
最后,“pixPosition”计算得到每个Tile的精灵在屏幕上的正确的坐标位置---你将会在后面看到这个函数的特殊用途。
Box类的主要功能就是处理所有单个Tile类的创建,加载相应精灵,以及把它们放置在屏幕上的正确位置。
Box.h:
```cpp
#include "Tile.h"
USING_NS_CC;
class Box :
public Object
{
public:
Box();
~Box();
bool initWithSize(Size aSize, int aImgValue);
TileElem* objectAtPos(int posX, int posY);
bool check();
static Box* create(Size size, int factor);
CC_SYNTHESIZE_READONLY(Size, _size, Size);
CC_SYNTHESIZE(Layer*, _layer, Layer);
CC_SYNTHESIZE(bool, lock, Lock);
public:
std::vector<vector> contentVec;
Vector readyToRemoveTilesVec;
TileElem* OutBorderTile;
int imgValue;
};
```
单看头文件,有些内容你也可以猜到它的作用了。。。size就是我们将要创建的网格的大小(3*3, 4*5, 5*3,7*7等等)
Box类最主要的两个变量就是“contentVec”和“readyToRemoveTilesVec”。这里说明一下cocos2d自带有一种Vector,但是编者在做得时候发现cocos2d的Vector不能存Vector,所以我用得是C++标准的std::vector来存储。实现二维数组。
contentVec变量实际上是一个多维数组,至少也是一维(如果SIZE为1*1的话)。我们将创建一个std::vector<vector>,然后会在每一列中再加入一个Vector作为一行。我们可以使用 “return contentVec.at(posY).at(posX);”来得到正确的Tile。
readyToRemove变量,在这个教程中,只是初始化了,但是,今后,我会介绍另一个游戏,在那里面我会大量使用这个变量----在这个教程中,我将使用它加载所有新创建的精灵。
接下来,让我们看看Box类的具体实现:
Box.cpp:
```cpp
#include "Box.h"
Box::Box()
{
}
Box::~Box()
{
}
Box* Box::create(Size aSize, int aImgValue)
{
Box *pRet = new Box();
if (pRet && pRet->initWithSize(aSize, aImgValue))
{
pRet->autorelease();
return pRet;
}
else
{
delete pRet;
pRet = NULL;
return NULL;
}
}
bool Box::initWithSize(Size aSize, int aImgValue)
{
imgValue = aImgValue;
_size = aSize;
OutBorderTile = TileElem::create(-1, -1);
for (int y = 0; y < _size.height; y++) {
Vector rowContentVec;
for (int x = 0; x < _size.width; x++) {
TileElem *tile = TileElem::create(x, y);
rowContentVec.pushBack(tile);
readyToRemoveTilesVec.pushBack(tile);
}
contentVec.push_back(rowContentVec);
}
return true;
}
TileElem* Box::objectAtPos(int posX, int posY)
{
if (posX < 0 || posX >= kBoxWidth || posY < 0 || posY >= kBoxHeight) {
return OutBorderTile;
}
return contentVec.at(posY).at(posX);
}
bool Box::check()
{
int countTile = readyToRemoveTilesVec.size();
if (0 == countTile){
return false;
}
for (int i = 0; i < countTile; i++) {
TileElem *tile = readyToRemoveTilesVec.at(i);
tile->setValue(0);
if (tile->getSprite()) {
_layer->removeChild(tile->getSprite());
}
}
readyToRemoveTilesVec.clear();
char name[20] = { 0 };
sprintf(name, "%d.png", imgValue);
Texture2D * texture = Director::getInstance()->getTextureCache()->addImage(name);
Vector imgFrames;
for (int i = 0; i < 7; i++) {
for (int j = 6; j >= 0; j--) {
SpriteFrame *imgFrame = SpriteFrame::createWithTexture(texture, Rect(i * 40, j * 40, 40, 40));
imgFrames.pushBack(imgFrame);
}
}
for (int x = 0; x < _size.width; x++) {
int extension = 0;
for (int y = 0; y < _size.height; y++) {
TileElem *tile = TileElem::create(x, y);
if (tile->getValue() == 0){
extension++;
} else if(extension == 0) {
}
}
for (int i = 0; i < extension; i++) {
TileElem *destTile = this->objectAtPos(x, kBoxHeight - extension + i);
SpriteFrame * img = imgFrames.at(0);
Sprite *sprite = Sprite::createWithSpriteFrame(img);
imgFrames.eraseObject(img);
sprite->setPosition(kStartX + x * kTileSize + kTileSize / 2, kStartY + (kBoxHeight + i) * kTileSize + kTileSize / 2 - kTileSize * extension);
_
layer->addChild(sprite); destTile->setValue(imgValue); destTile->setSprite(sprite); } } return true; } ```
那么,这个类到底做了些什么事呢---我认为"initWithSize" 和 "objectAtPos" 这两个方法已经很清楚了。。这里就不再啰嗦了。
因此,最重要的方法---check。首先,我们判断readyToRemoveTilesVec中是否包含任何Tiles。。在我们这个例子中,所有的Tile都在里面。。。非常好!然后我们遍历这个数组里面所有的元素,把其中的精灵一个个全部清除掉,最后,我们把整个数组里的元素全部清空。现在,你可能知道了,当制作一个游戏的多个关卡的时候,如何做好清理化工作了。
接下来,我们从资源文件中加载纹理(比如1.png, 2.png, 3.png等等),然后把它们存储在一个Texture2D对象中,之后我们会从Texture2D对象来构建所有的精灵帧。我们将创建49个精灵帧。因为我们的图片大小是280*280,所以每个CCSpriteFrame大小就是40*40。这里面用了一个双重for循环。
```cpp
for (int x = 0; x < _size.width; x++) {
int extension = 0;
for (int y = 0; y < _size.height; y++) {
TileElem *tile = TileElem::create(x, y);
if (tile->getValue() == 0){
extension++;
} else if(extension == 0) {
}
}
```
上面这部分代码,对于本教程来说,其实并不是必须的,但是,它可以用来判断有多少Tile需要替换图片。这种机制非常好,尤其是当你制作一个Tile drop游戏(比如宝石迷阵)的时候,因为你不想一次替换掉所有的Tile。。。在本例中,我们将检测Tile的value属性是否为0,然后用extension变量来追踪有多少个value为0的Tile。在本例中,由于所有的Tile都存在了readyToRemoveTilesVec中,所以extension变量永远都是7。
下面,我们得到第一个Y坐标为(kBoxHeight-extension+i)的Tile。。因为,在我们的例子中, kBoxHeight = 7,而extension总是为7,所以,我们实际上只需要关心变量i就可以了。i会从0一直递增到6.好了,你可能会问,为什么我要这样?因为,我会在其实游戏中使用到,如果现在我们就熟悉了的话,以后的工作会很轻松:)。接下来,看看精灵是如何工作的吧。。。
```cpp
for (int i = 0; i < extension; i++) {
TileElem *destTile = this->objectAtPos(x, kBoxHeight - extension + i);
SpriteFrame * img = imgFrames.at(0);
Sprite *sprite = Sprite::createWithSpriteFrame(img);
imgFrames.eraseObject(img);
sprite->setPosition(kStartX + x * kTileSize + kTileSize / 2, kStartY + (kBoxHeight + i) * kTileSize + kTileSize / 2 - kTileSize * extension);
_layer->addChild(sprite);
destTile->setValue(imgValue);
destTile->setSprite(sprite);
}
```
然后,我们创建了新的Sprite,通过给定的SpriteFrame,并且把精灵放置在box的合适的位置。我们把所有的Tile的imgValue都设置成一样的。
最后,看看PlayLayer类,我们应该很熟悉了。。。它将会处理touches,以及box类的初始化工作。我们需要追踪前面两次touch--“selectedTile”变量指代玩家当前选择的Tile,这时候,如果玩家选择另外一个Tile的时候,就会和前面的Tile进行交换。接下来,看看具体实现吧。
PlayLayer.h:
```cpp
#include "cocos2d.h"
#include "Box.h"
#include "Constants.h"
USING_NS_CC;
class PlayLayer :
public Layer
{
public:
PlayLayer();
~PlayLayer();
bool init();
void check(Object* pSender, void* data);
void changeWithTileA(TileElem* a, TileElem* b);
void onTouchesBegan(const std::vector& touches, Event *unused_event);
CREATE_FUNC(PlayLayer);
public:
Box* box;
TileElem* selectedTile;
int value;
};
```
接下来是其实现:
PlayLayer.cpp:
```cpp
#include "PlayLayer.h"
PlayLayer::PlayLayer()
{
}
PlayLayer::~PlayLayer()
{
CC_SAFE_RELEASE_NULL(box);
}
bool PlayLayer::init()
{
if (!Layer::init())
{
return false;
}
value = CCRANDOM_0_1()*kKindCount + 1;
box = Box::create(Size(kBoxWidth, kBoxHeight), value);
CC_SAFE_RETAIN(box);
box->setLayer(this);
box->setLock(true);
box->check();
auto listener = EventListenerTouchAllAtOnce::create();
listener->onTouchesBegan = CC_CALLBACK_2(PlayLayer::onTouchesBegan, this);
this->_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
selectedTile = NULL;
return true;
}
void PlayLayer::onTouchesBegan(const std::vector& touches, Event *unused_event)
{
CCLOG("touched");
auto touch = touches.at(0);
auto location = touch->getLocation();
if (location.y < kStartY ||
location.y >(kStartY + (kTileSize * kBoxHeight)) ||
location.x < kStartX ||
location.x >(kStartX + (kTileSize * kBoxWidth)))
{
return;
}
int x = (location.x - kStartX) / kTileSize;
int y = (location.y - kStartY) / kTileSize;
if (selectedTile && selectedTile->getPosX() == x && selectedTile->getPosY() == y) {
selectedTile = NULL;
return;
}
TileElem *tile = box->objectAtPos(x, y);
if (tile->getPosX() >= 0 && tile->getPosY() >= 0) {
if (selectedTile && selectedTile->nearTile(tile)) {
box->setLock(true);
this->changeWithTileA(selectedTile, tile);
selectedTile = NULL;
}
else {
if (selectedTile) {
if (selectedTile->getPosX() == x && selectedTile->getPosY() == y) {
selectedTile = NULL;
}
}
selectedTile = tile;
}
}
}
void PlayLayer::check(Object* pSender, void* data)
{
CCLOG("PlayLayer::check has been called");
}
void PlayLayer::changeWithTileA(TileElem* a, TileElem* b)
{
CCLOG("PlayLayer::changeWithTileA has been called");
Action *actionA = Sequence::create(
MoveTo::create(kMoveTileTime, b->pixPosition()),
CallFuncN::create(CC_CALLBACK_1(PlayLayer::check, this, (void*)a)),
NULL);
Action *actionB = Sequence::create(
MoveTo::create(kMoveTileTime, a->pixPosition()),
CallFuncN::create(CC_CALLBACK_1(PlayLayer::check, this, (void*)b)),
NULL);
a->getSprite()->runAction(actionA);
b->getSprite()->runAction(actionB);
a->trade(b);
}
```
这个类中主要有3个方法---init方法用来初始化box类,onTouchesBegan方法,决定哪个Tile被用户选择,如果新选择的Tile等于原Tile的话就直接返回;如果是邻近的Tile的话,就交换;否则,就不做任何事情。交换Tile的时候,调用Sequence 和 MoveTo来展示交换的动画。实际上,我们真正交换的代码只有a->trade(b)。