【英文论坛获奖教程】从零开始做一款你自己的拼图游戏

从零开始做一款你自己的拼图游戏

原作者:
ruben1

英文论坛原帖:
http://www.cocos2d-x.org/forums/6/topics/54780?r=55414

源码
你能在GitHub3中下载我们的项目:
https://github.com/MandukaGames/tutorial-puzzle

使用Cocos2d-x v3制作一个简易的拼图
在这篇教程中我们将从头开始制作一个非常简单的拼图。我们的拼图有四块,但如果你对它的工作方式感到着迷并且想要制作你自己的小实验,你会发现扩充它并且添加新的功能将会非常简单。让我们能够了解你对这篇教程的印象以及这篇教程对你创作自己的游戏有着何种启发。
搭建起开发坏境
新版本的Coco2d-x (v3)是一个巨大的飞跃。如果你还在使用v2的版本,那么是时候考虑尽快换版本了。
在这篇教程中我们假设你已经 下载了Cocos2d-x v3在你的电脑上并且安装了新的控制台工具。如果你还没有完成这一步,你应该先移步到cocos2d-x的官方GitHub上official github2你将会找到每一步的说明。(https://github.com/cocos2d/cocos2d-x)
我们将会使用OSX Maverick来完成这篇教程并且在Mac上编译。但唯一需要这样做的原因只是因为这样会更快并且你也不需要其他设备。记住,我们在这里写下的每一个代码将会在其他的平台上运行。我们已经在一台iPad上测试过了,如果你想在Android上运行也应该是可以的。
开始动工。
用Cocos2dx v3创建一个新的项目
我们首先需要做的就是创建一个新的项目。在这个例子中我会将它创建在我Mac的桌面上。
Open the Terminal and create a new project with the console tool of Cocos.
打开终端并且使用Cocos的控制台工具创建一个新的项目。
MacBook-Pro:bin Ruben$ cocos new PuzzleTutorial -p com.mandukagames.puzzle -l cpp -d /Users/Ruben/Desktop
Runing command: new

Copy template into /Users/Ruben/Desktop/PuzzleTutorial
Copying cocos2d-x files…
Rename project name from ‘HelloCpp’ to ‘PuzzleTutorial’
Replace the project name from ‘HelloCpp’ to ‘PuzzleTutorial’
Replace the project package name from ‘org.cocos2dx.hellocpp’ to ‘com.mandukagames.puzzle’
如果所有步骤没有出错的话,那么你现在就应该有一个名为"PuzzleTutorial"的新项目在你的桌面上。现在打开proj.iosmac_文件夹并开启程序PuzzleTutorial.xcodeproj

1119x424 154 KB

最好的方法检验去否有差错就是运行程序。打开Xcode,在开始/停止按钮旁play/stop buttons,选择目标项目PuzzleTutorial Mac 和My Mac 作为设备。一个新的OSX窗口应该打开并执行项目。

228 KB

定义新的项目,并创建新的类
我们现在就可以开始编码了。但是我就是不喜欢有个名叫HelloWorldScene的文档在项目里(不爽你来打我啊!)。所以首先我们要将它更名为GamePlayScene这样看起来更恰当。如果你并不觉得有所不舒服的话那么你可以跳过这一步。 .
我们继续把类HelloWorld重命名为GamePlay这样足够在代码中做出足够的改动。最简单的方法就是用Xcode的查找Find和替换replace功能并将所有的"HelloWorld"改成"GamePlay"。一旦完成后就按下cmd+b进行编译确认没有步骤出错。
我们的拼图会在一张图层上合成,我们把这图层当做画布"canvas"使用并且四张图片需要用户将其移动到正确的位置。
Piece将会成为我们拼图里代表图块的类,在它确认被用来表示图片的时候,我们就将它作为一个精灵的子类。只要打开Xcode点击添加文件,选择C++类文档并接受。之后我们会回来处理这个文档。
Now we will add to the project the resources for the canvas and the pieces.
现在我们为画布和图片添加项目的资源。
我们用一幅码头的图片(我们的导演 和吉祥物Manduki)创建了一个小拼图。你能够在这里找到资源https://github.com/MandukaGames/tutorial-puzzle/tree/master/Resources/puzzle
Xcode的检视窗口应该是这个样子的:

编码代码的核心
删除掉不需要的文件
我们将要删除掉GamePlay里不需要的代码。我们的init方法应该是长这样的。
// on “init” you need to initialize your instance
bool GamePlay::init()
{
if ( !Layer::init() )
{
return false;
}

Size visibleSize = Director::getInstance()->getVisibleSize();
Vector2 origin = Director::getInstance()->getVisibleOrigin();
return true;

}
我习惯把变量visibleSize 和 origin留住,因为到最后处于各种原因,你都会感觉到你需要他们。
你可以把方法menuCloseCallback送去见鬼了,在头文件中相应的声明也可以删去了,毕竟这个方法在这个教程里不需要。
提醒:如果你想要留住这个方法,那就不要把这个精灵的png资源删掉,不然这回引起崩溃。
添加背景图片并改变背景颜色。
为了向用户展示这些图片的正确去向我们将添加一张图片到场景中。我们将它作为背景并放在中心。
那些需要添加进init方法的那些几行新代码我们已经标记了"+"符号
// on “init” you need to initialize your instance
bool GamePlay::init()
{
//////////////////////////////
// 1. super init first
if ( !Layer::init() )
{
return false;
}

Size visibleSize = Director::getInstance()->getVisibleSize();
Vector2 origin = Director::getInstance()->getVisibleOrigin();

+// Add Background shape
+Sprite * background = Sprite::create("background.png");
+background->setPosition(Vector2(this->getContentSize().width/2, this->getContentSize().height/2));
+this->addChild(background);

return true;

}
其中有一件事不是必要但我们仍会做的就是改变场景中背景的颜色。改变类的继承从Layer 到 LayerColor

class GamePlay : public cocos2d::LayerColor
现在改变init方法使得他们能够改变颜色。

  • if (!Layer::init())
  • if (!LayerColor::initWithColor(Color4B(255, 255, 255, 255)))
    给图块增加坐标
    我们接下来要把图块放置在屏幕上。
    如果你真的喜欢这篇教程并且最终从这里开始开发自己的游戏这就要求你创造一些更为精细的代码来调整图块在屏幕中最终位置。比如,你需要创造一个基于.plist的格式来创造一个图块的数组并关联到一个等级。这将允许你创造更多的拼图和更简单的添加等级。
    但这篇教程的目的并不是讨论游戏引擎和游戏动态,所以我们只是为每一图块硬编码了init位置和goal位置。
    (如果有足够多的人对此感兴趣的话那就来这Manduka Games1找我们吧。我们很乐意制作这篇教程的第二部制作另一个主题的游戏)。
    Init位置
    从背景开始再来一点基本的几何学知识我们就能正确的计算拼图的位置。基本上我们做的就是创建一个Vector2——也就是之前的CCPoint代表每个图块的中心位置。
    // Position of pieces
    // 图块的位置
    Vector2 positionPieceTopLeft = Vector2(background->getPositionX()- background->getContentSize().width/4,
    background->getPositionY()+ background->getContentSize().height/4);

Vector2 positionPieceTopRight = Vector2(background->getPositionX()+ background->getContentSize().width/4,
background->getPositionY()+ background->getContentSize().height/4);

Vector2 positionPieceBottomLeft = Vector2(background->getPositionX()- background->getContentSize().width/4,
background->getPositionY()- background->getContentSize().height/4);

Vector2 positionPieceBottomRight = Vector2(background->getPositionX()+ background->getContentSize().width/4,
background->getPositionY()- background->getContentSize().height/4);
图块piece的类
之前我们已经创建了类Piece现在是时间再次编辑它了。
我们从类定义开始。只要打开Piece.h并黏贴一下下一行代码。如果代码中你有一些不太明白的没关系,接下来我们就要来了解一下他的意义。
#include “cocos2d.h”

USING_NS_CC;

class Piece: public Sprite
{
bool _actived;
Vector2 _targetPosition;

void setActived(bool active);

public:

// Constructor
static PLPiece* create(const std::string &filename);
virtual bool init(const std::string &filename);

void setTargetPosition(Vector2 targetPosition);
bool currentLocationSuccess();

};

正如你所看到的,我们的图块都是精灵sprite。我们会自定义函数的create和init的功能来初始化想要的图片(这些我们会将它当做参数来传递)。
我们已经定义了两个实例变量actived_ 和 targetPosition_
• ___actived__将会用来判断图块是否在需要的时候是否已经激活。如果现在用户在某一特定的时刻点击了这个图块,就会使用这个变量来获知是否应该反馈给用户。比如,如果用户正在触摸图块我们能使图片稍微变大或者改动颜色。

• 在___targetPosition__里我们存储这张图片应该的被存放的正确位置。我们还会定义一个公共setter给这个变量。

最后,我们要定义一个方法来查询拼图在特定时刻它的位置是否是良好的。这个方法在我们编写拼图的逻辑时非常有用。
构造函数

让我们从create和init这两个基本的实现方法开始吧。

这些方法是Cocos2d-x的一个标准的模式。如果你是Cocos2d-x的老鸟那你应该对他们非常熟悉了,就算不是,那也很快就是了~如果你计划着开发游戏,那你将会经常使用到他们。
如果你想知道他们是如何工作的,你可以浏览Cocos2d-x(http://www.cocos2d-x.org/wiki)的Wiki网页。那里面有一个区是关于内存管理的,还有继承自Objective-C的保存和继承模式。
把这代码添加进Piece.m
Piece* Piece::create(const std::string &filename)
{
Piece *pRet = new Piece();
if (pRet && pRet->init(filename))
{
pRet->autorelease();
}
else
{
delete pRet;
pRet = NULL;
}
return pRet;

}

bool Piece::init(const std::string &filename)
{
if (!Sprite::initWithFile(filename)) {
return false;
}

return true;

}

void Piece::setTargetPosition(cocos2d::math::Vector2 targetPosition)
{
this->_targetPosition = targetPosition;
}
在这个关键点注释一下我们还没有完成的方法,然后按cmd+r开始编译和运行,确认一切进展顺利。
把拼图添加到场景
回到GamePlay类里面找到我们添加拼图位置的地方。直到我们有四个变量来保存在屏幕上的位置。
接下来我们创建拼图并给每个不同的拼图分配位置。为了避免重复编码四次,这里我们将使用for循环和一个数组。
把位置矢量 Vector2插入数组中
Vector2 positionsArray;
positionsArray = positionPieceTopLeft;
positionsArray = positionPieceTopRight;
positionsArray = positionPieceBottomLeft;
positionsArray = positionPieceBottomRight;
创建并给每个位置添加拼图。
for (int i = 0; i < 4; i++)
{
// Add piece
Piece * piece = Piece::create(“piece_0”+std::to_string(i)+".png");
piece->setTargetPosition(positionsArray*);
this->addChild(piece);
}
到这一步我们开始编译运行的话会看到拼图已经添加在坐标(0, 0)上,锚点是(0.5, 0.5)。

1.02 MB

我们应该保持一个对拼图的引用,使用Vector很可能是个好办法。
在Cocos2d-x 2点几的版本中我们倾向于使用基于Objective-C的基础函数NSArray的CCArray,但这个类已经被废弃了。现在你应该开始使用新的基于C++标准数据库的类Vector而且它有着比CCArray更强的优势。你能找到关于这个类的较为详细的解释从这篇Wayne A.Lee的文章this article(http://dev.bunnyhero.org/2014/01/cocos2d-x-30-beta-the-new-vector-class/)。或者你能信任本教程并直接继续
在GamePlay.h里创建一个新的私有变量private variable
private:
Vector<Piece > puzzlePieces;
Add the line with the character “+” to the loop.
for (int i = 0; i < 4; i++)
{
// Add piece
Piece * piece = Piece::create(“piece_0”+std::to_string(i)+".png");
piece->setTargetPosition(positionsArray
);
+ this->puzzlePieces.pushBack(piece);
this->addChild(piece);
}
现在我们已经有了对拼图的关联,所以我们返回到Piece的类并添加用户互动了。
与拼图开始互动
Cocos2d-x v3有介绍过对时间管理的若干个改进。我们建议你浏览关于时间管理的官方文档event handling. (http://www.cocos2d-x.org/docs/manual/framework/native/input/event-dispatcher/en*)
在Piece.h里面声明返回函数callbakcs:
bool onTouchBegan(Touch*, Event*);
void onTouchMoved(Touch*, Event*);
void onTouchEnded(Touch*, Event*);
void onTouchCancel(Touch*, Event*);
我依然保持着这种声明方法方法使之规范,但自从cocos v3后你能自己改名或者甚至是在内部声明他们。不过还是要记住参数类型和返回值。
在init里面的完成方法中我们要添加下一行代码用于注册一个新的监听事件来检测触摸和关联我们之前刚声明的方法。
auto listener = EventListenerTouchOneByOne::create();

listener->onTouchBegan = CC_CALLBACK_2(Piece::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(Piece::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(Piece::onTouchEnded, this);
listener->onTouchCancelled = CC_CALLBACK_2(Piece::onTouchCancel, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
是时候编辑游戏逻辑了
当用户接触式我们要激活一块拼图。如果接触的地方不是拼图而是拼图以外的地方我们就要使用callback。
bool Piece::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event)
{
if (this->getBoundingBox().containsPoint(touch->getLocation()))
{
this->setActived(true);
}

return true;

}
当用户离开触摸屏时拼图就不在启动。
void Piece::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event)
{
this->setActived(false);
}
如果用户移动指尖,那么拼图也应该跟着移动。
void Piece::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event)
{
if (this->_actived)
{
this->setPosition(Vector2(this->getPositionX()+touch->getDelta().x, this->getPositionY()+touch->getDelta().y));
}
}
编译和执行

959 KB

如果你这个时候按住拼图并拖动的话,你会看到他们全部都在动,好像是一张和在一起的图片。
冷静点,这些拼图都已经被放置进窗口了,只是在你手指或者是鼠标移动的时候这些图片同时都感应到并且开始移动。接触事件传递到了后面的拼图应用在了被一个监听器上。
为了避免这一行为,我们会在onTouchBegin上做点改进。
bool Piece::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event)
{
if (this->getBoundingBox().containsPoint(touch->getLocation()))
{
this->listener->setSwallowTouches(true);
this->setActived(true);
}
else
{
this->listener->setSwallowTouches(false);
}

return true;

}
现在我们只需要当用户碰触到拼图外的时候传递接触事件。当用户按住拼图的时候,拼图会吞下swallow所有的触摸时间并不会影响到其他图层。
再次编译和执行。现在一切都应该OK了并且能够移动每一个图片。

959 KB

一点点小魔法
为你能坚持到现在感到骄傲吧。你很坚强,所以别放弃我们就快成功了。
重述要点:
给点用户反馈来改进用户体验会是非常好的一件事。
接下来将使拼图在被点击到的时候它的大小尺寸会稍有变化。
我们使用方法setActived并且稍作修改:
void Piece::setActived(bool active)
{
_actived = active;
Action *scale;
if (_actived)
{
scale = ScaleTo::create(0.2, 1.1);
}
else
{
scale = ScaleTo::create(0.2, 1);
}
this->runAction(scale);
}
所以现在当我们每碰到一块拼图,它的尺寸会增大10%,每次释放接触的时候它都会变回原来的尺寸。开始编译并开始试验吧~
最后。。。
回到方法中
bool currentLocationSuccess
现在是时候使得:当你释放拼图的时候,如果它非常接近于目标地点,它会移动到它的准确位置上。所以首先我们就是要完成确认拼图是否进入正确位置的实现方法。
bool Piece::currentLocationSuccess()
{
Vector2 currentPosition = this->getPosition();
int deltaX, deltaY;
deltaX = _targetPosition.x - currentPosition.x;
deltaY = _targetPosition.y - currentPosition.y;

// Current piece is inside the desired range.
if ((abs(deltaX) < LOCATION_ACCURACY)&&(abs(deltaY) < LOCATION_ACCURACY))
{
    Action* moveToTargetPosition = MoveTo::create(0.2, _targetPosition);
    this->runAction(moveToTargetPosition);
    std::cout << "pieze is now in the correct place";
    return true;
}
return false;

}
在接触事件结束时启动这个方法
void Piece::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event)
{
this->setActived(false);
this->currentLocationSuccess();
}
你应该也注意到了我们使用了常量"LOCATION_ACCURACY",这你应该在之前就定义。
#define LOCATION_ACCURACY 100
这个常量,可想而知,这是我们想要拼图接受某个位置为满足要求的位置时使用的参数。对于这个教程”100”这个数值就是一个可以接受的值。当然你还是可以自己修改它。数值越低越不容易释放图片。

495 KB

就是这样啦!你已经完成了教程。
所以现在呢?
Improving our puzzle
改进我们的拼图
We propose you a list of improvements to practice. If you decide to make someone let us now and send us your feedback and your improvements via pull request to github3 or email to developer@mandukagames.com. We will be very happy to add your improvements to the tutorial.
我们打算给你们一清单的改进建议来练习
• Make a bigger puzzle.
• Position the pieces randomly in the screen to force user to find them.
• A popup congratulating the user when he finish the puzzle.
• A reset button to disorder the pieces over the screen (animations?).
• A counter to punctuate the user by the time he needs to solve the puzzle.
• An init menu with the Play and Menu option to choose the number of pieces and difficulty ot the puzzle.
• 制作更大拼图。
• 让拼图随机出现在屏幕上使用户自己去找。
• 当用户完成了拼图后出现提示成功的弹出窗口。
• 设置一个重置按钮来打乱屏幕上的拼图(使用动画效果?)
• 制作一个计时器给用户限定完成时间。
• 一个初始化菜单有着播放和菜单选项来选择拼图的数量和难度。
We hope you have enjoyed this tutorial! Don’t forget to leave us your feedback in mandukagames.com.
我们希望你能喜欢这篇教程!别忘记到我们的网站mandukagames.com给我们反馈啊~*

1赞

您的文章已被推荐到CocoaChina首页热门文章精选,感谢您的分享。

楼主辛苦了,谢谢

Vector2 哪来的:2:

Point也可以的