首页 > 代码库 > cocos2d-x之道~制作第一款文字游戏(二)

cocos2d-x之道~制作第一款文字游戏(二)

在 cocos2d-x之道~制作第一款文字游戏(一)中,使用cocos2d-x把主界面显示出来,分别有每个级别提供的初始短语TileView,和目标短语TargetView。初步接触了cocos2d-x的基本概念和基础用法。这篇博客将会基本实现游戏的逻辑,完成游戏的主体部分。采用以下步骤:

使TileView可拖动

捕获TileView停止移动的事件

分析TileView是否放在正确的位置上

创建与原来Layer区分的层,放置按钮、菜单和分数等等。

添加计时和分数

现在开始,继续cocos2d-x之道!


1) 拖放TileView

在TileView的initWithLetter函数中,其实还有一部分工作没完成。现在要实现拖放效果,就得让TileView处理手势事件,在函数的最后添加下面代码

CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(tile,0,true);

通过CCDirector类,添加TileView为手势处理代理。要处理手势事件,TileView还需要继承CCTargetedTouchDelegate,如代码所示

class TileView: public CCNode, public CCTargetedTouchDelegate

然后在类里添加以下变量

	int xOffset;
	int yOffset;
	bool isControl;

以及重写CCTargetedTouchDelegate继承来的函数

        bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
	void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
	void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
	void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);

ccTouchBegan是点击开始,ccTouchMoved是手指在屏幕上移动,ccTouchEnded是手指离开屏幕以及ccTouchCancelled是被其他情况打断

实现这几个重要函数前,先把isControl初始化,同样是在initWithLetter的最后添加

tile->isControl = false;

这个变量的作用在后面再解释,先把非常重要的手势事件给实现

bool TileView::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent){
	if(!containTouchLocation(pTouch)){
		return false;
	}

	isControl = true;
	this->setZOrder(999);

	CCPoint point = pTouch->getLocationInView();
	CCPoint touchPoint = CCDirector::sharedDirector()->convertToGL(point);
	this->xOffset = touchPoint.x - this->getPositionX();
	this->yOffset = touchPoint.y - this->getPositionY();

	return true;
}

void TileView::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent){
	if(!isControl){
		return;
	}

	CCPoint point = pTouch->getLocationInView();
	CCPoint touchPoint = CCDirector::sharedDirector()->convertToGL(point);
	float x = touchPoint.x - this->xOffset;
	float y = touchPoint.y - this->yOffset;

	this->setPosition(ccp(x,y));
}

void TileView::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent){
	CCPoint point = pTouch->getLocationInView();
	CCPoint touchPoint = CCDirector::sharedDirector()->convertToGL(point);

	if(isControl){
		isControl = false;
	}	
}

void TileView::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent){
	ccTouchEnded(pTouch, pEvent);
}

一步步来解释,在ccTouchBegan中,containTouchLocation是判断手指的落点是否在TileView内,只有落点是在TileView内,才进行处理,否则直接返回。然后设置isControl为true,且把拖动的TileView置顶,使用xOffset和yOffset来记录触摸点坐标和TileView坐标的偏移量。在每个手势事件函数里,都有convertToGL这个函数,它的作用是把触摸的坐标转化成游戏世界里的坐标(非常重要)。

ccTouchMoved就是在拖动过程中,不断的改变TileView的坐标,记得把偏移量减掉。

ccTouchEnded就是手指离开屏幕了,把isControl重新设为false。可以看出,isControl的作用就是控制在整个手势过程中,标记TileView是否被拖动着。当ccTouchEnded或者ccTouchCancelled后,就需要复原。

运行一下,是否可以拖动了?神奇吧。


2) 放下TileView

拖动TileView后,总会有放下的一刻。当放下TileView时,我们就要判断它是否被放在正确的目标TargetView上。这些判断需要MainScene去处理,所以得建立一个TileDropDelegate

class TileDropDelegate{
public:
	virtual void dropToPoint(TileView * tile,cocos2d::CCPoint point) = 0;
};

用MainScene继承这个类

class MainScene : public cocos2d::CCLayer, public TileDropDelegate

实现dropToPoint方法

void MainScene::dropToPoint(TileView * tile,CCPoint point){
	TargetView * target = NULL;
	CCObject * tv;
	CCARRAY_FOREACH(pTargets,tv){
		TargetView* pTarget = (TargetView*) tv;
		if(pTarget->containPoint(point)){
			target = pTarget;
			break;
		}
	}
}

遍历存放TargetView的pTargets,检查是否点point在TargetView的frame里,如果发现TileView最后是落在TargetView上,则可以进行结果判断,继续在dropToPoint函数的尾部添加

if(target != NULL){
		if(target->getLetter() == tile->getLetter()){
			// 1 单词匹配
		}else{
			// 2 单词不匹配
		}
	}
1.检查拖动的TileView所代表的字母与TargetView的字母(不可见)是匹配

2.两个字母是不匹配的,需要进行后续工作

实现了回调函数,还得设置进去给TileView,在dealRandomAnagram中的 tile->randomize(); 这句后面添加

tile->setTileDropDelegate(this);
记得在TileView类里添加setTileDropDelegate函数。

然后,就进行字母匹配后处理,需要将TileView覆盖到TargetView上,

void MainScene::placeTile(TileView * tile,TargetView * target){
	tile->setMatch(true);
	target->setMatch(true);

	tile->removeTouchDelegate();

	CCActionInterval * actionTo = CCMoveTo::create(0.35,ccp(target->getPositionX(),target->getPositionY()));
	tile->runAction(actionTo);
	tile->setRotation(0);
	target->setVisible(false);
}
代码中把tile和target设置成已匹配,去掉tile的拖放代理,使用CCMoveTo把tile移动到target位置上,并摆正

在1 单词匹配后添加以下语句

placeTile(tile,target);
在2 单词不匹配后添加

			tile->randomize();

			CCActionInterval * actionTo = CCMoveTo::create(0.35,
				ccp(tile->getPositionX() + Common::random(-20,20),
				tile->getPositionY() + Common::random(-20,30)));
			tile->runAction(actionTo);

主要作用是把tile偏离target,表示两者不匹配,如图所示


有了检测匹配函数,就可以判断是否成功完成游戏。在MainScene中添加

void MainScene::checkForSuccess(){
	CCObject * tv;
	CCARRAY_FOREACH(pTargets,tv){
		TargetView* pTarget = (TargetView*) tv;
		if(pTarget->getIsMatch() == false){
			return;
		}
	}
	
	CCLog("Game Over!");

}

遍历target,如果全部都匹配了,表示游戏成功,在dropToPoint最后添加

checkForSuccess();
运行游戏,就会发现当所有字母都匹配了,就能打印出Game Over消息

3) 创建HUD层

这是一种游戏设计理念,就是把游戏一些界面上的东西分离到HUD层上,这层的内容主要作用是辅助游戏内容,比如倒计时,按钮,结束界面等等

首先,我们来创建倒计时,类名为StopWatchView,头文件如下

class StopWatchView : public CCLabelAtlas{
public:
	StopWatchView(){}

	static StopWatchView * initView();
	void setSecond(int seconds);

	CREATE_FUNC(StopWatchView);
};
主体部分为setSecond是把秒数拆分成mm:ss形式

void StopWatchView::setSecond(int second){
	char strTime[50];
	float minutes = second / 60;
	minutes =  (minutes > 0.0) ? floor(minutes + 0.5) : ceil(minutes - 0.5); 
	int minu = (int)minutes;
	int sec = second % 60;
	sprintf(strTime," %02d:%02d",minu,sec);
	this->setString(strTime);
}
下面创建HUDView,头文件如下

class HUDView : public CCLayer{
public:
    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();  

	// implement the "static node()" method manually
    CREATE_FUNC(HUDView);

private:
	StopWatchView * pWatch;
	CounterPointView * pPointView;
	CCMenuItemImage * pButton;
	CCMenu * pMenu;
	CCLabelTTF * pLabel;
public:
	friend class MainScene;
};
其中,CounterPointView, CCmenuItemImage, CCMenu, CCLabelTTF这些是以后使用。friend class MainScene是让MainScene可以访问HUDView的成员变量。

HUDView.cpp 内只有一个函数,如

bool HUDView::init(){
	if(! CCLayer::init()){
		return false;
	}

	pWatch = StopWatchView::initView();
	pWatch->setSecond(0);
	pWatch->setPosition(ccp(Common::getCameraWith()*0.5,
		Common::getCameraHeight() * 0.95));
	pWatch->setScale(0.7);
	this->addChild(pWatch);
		
	return true;
}

把StopWatchView添加上去。

然后需要把HUDView添加到MainScene中,所以在MainScene.h里添加

HUDView * pHUD;
添加这个成员变量后,就在MainScene的init函数里,this->addChild(bg)这句后面添加

pHUD = HUDView::create();
this->addChild(pHUD);
然后运行游戏,可以看到如图所示


使用艺术字体

StopWatchView * StopWatchView::initView(){
	StopWatchView * p = (StopWatchView*)(CCLabelAtlas::create("11","fonts/tuffy_bold_italic-charmap.plist"));
	return p;
}
加载艺术字体后,如图所示



4) 添加Level计时器

在MainScene.h中添加

int mTimeLeft;
然后在MainScene 中 添加启动计时器,如

void MainScene::startStopWatch(){
	mTimeLeft = pLevel->mTimeToSovle;
	pHUD->pWatch->setSecond(mTimeLeft);

	schedule(schedule_selector(MainScene::downTime),1.0f);
}
使用schedule方法,每隔一秒执行一次downTime。有启动计时,就得停止计时

void MainScene::stopStopWatch(){
	unschedule(schedule_selector(MainScene::downTime));
}
unschedule后就不会再计时,下面实现downTime函数

void MainScene::downTime(float dt){
	mTimeLeft --;
	pHUD->pWatch->setSecond(mTimeLeft);

	if(mTimeLeft == 0){
		stopStopWatch();
	}
}
内容比较简单,就是实现mTimeLeft的递减和设置给TimeWatchView,并且在归0时停止计时

启动计时的地方在dealRandomAnagram函数中,在最后处添加

startStopWatch();
如图所示,游戏开始时就进行倒计时


当然,游戏结束时别忘了要停止计时,在checkForSuccess的最后添加

stopStopWatch();

5)添加计分

游戏中计分是很重要的设定,有了计分,才有成就感。下面就开始为游戏添加计分功能

新增Data类,用来存放分数

class Data : public CCObject{
public:
	Data();
	virtual ~Data(){}

	void setPoint(int point);
	int getPoint();
	void addPoint(int point);
	void subtractPoint(int point);
	
private:
	int mPoints;
};
函数的实现如下

Data::Data(){
	mPoints = 0;
}

void Data::setPoint(int point){
	if(point > 0){
		mPoints = point;
	}else{
		mPoints = 0;
	}
}

int Data::getPoint(){
	return mPoints;
}

void Data::addPoint(int point){
	mPoints += point;
}

void Data::subtractPoint(int point){
	mPoints -= point;
}
当玩家拖放TileView,放到正确的位置上后,就添加分数,如果放错,则扣分。

在MainScene.h中添加变量

Data * pData;

在MainScene的init函数中,申请变量

pData = http://www.mamicode.com/new Data();

在dropToPoint中,如果匹配成功,则

pData->addPoint(pLevel->mPointPerTile);

如果匹配失败,则减一半的分

pData->subtractPoint(pLevel->mPointPerTile / 2);

6)在HUD层添加分数显示

分数应该实时显示给玩家看,需要显示则需要使用Label,所以新增CounterPointView

class CounterPointView: public CCLabelAtlas{
public:
	static CounterPointView* initWithValue(int v);

	void countTo(int p, float duration);
	void updateValueBy(float dt);

	void setValue(int v);

	CREATE_FUNC(CounterPointView);

private:
	CounterPointView(){};

	int value;
	float mDelta;
	int mEndValue;
	float mValueDelta;
};

value是指当前的分数值, mEndValue是新的分数值,下面先来初始化

CounterPointView* CounterPointView::initWithValue(int v){
	CounterPointView * p = (CounterPointView*)(CCLabelAtlas::create("11","fonts/tuffy_bold_italic-charmap.plist"));
	char strPoint[10];
	sprintf(strPoint,"%d",v);
	p->setString(strPoint);
	p->value = http://www.mamicode.com/v;>CCLabelAtlas类是能快速渲染的类,用来做变化的数字最合适(FPS的数字就是使用了CCLabelAtlas)

void CounterPointView::setValue(int v){
	this->value = http://www.mamicode.com/v;>设置分数

void CounterPointView::updateValueBy(float dt){
	this->value += (int)mValueDelta;

	if((int)mValueDelta > 0){
		if(this->value > mEndValue){
			this->value = http://www.mamicode.com/mEndValue;>
updateValueBy是递增(或递减)分数,每次都加上(或减去)mValueDelta,直到mEndValue,不然的话,每次都执行schedule函数

void CounterPointView::countTo(int to, float duration){
	this->mDelta = duration / (abs(to - value) + 1);
	if(mDelta < 0.05) mDelta = (float)0.05;

	mEndValue = http://www.mamicode.com/to;>
countTo函数是对外的接口,目的是在duration时间内达到to值,首先是计算间隔mDelta,然后是unscheduleAllSelectors,接着就开始递增(或递减)了。

在HUDView中添加变量CounterPointView后,在init函数中添加

pPointView = CounterPointView::initWithValue(0);
pPointView->setColor(ccc3(97,25,9));
pPointView->setPosition(ccp(Common::getCameraWith()*0.05,Common::getCameraHeight() * 0.9));
this->addChild(pPointView);

则可以如图所示,看到分数


回到MainScene.cpp中,在 pData->addPoint(pLevel->mPointPerTile); 这句后面添加

pHUD->pPointView->countTo(pData->getPoint(),1.5);

在 pData->subtractPoint(pLevel->mPointPerTile / 2); 这句后面添加

pHUD->pPointView->countTo(pData->getPoint(),0.75);

如图所示,当摆放正确后,分数递增,摆放错误则分数递减


7)结语

第二篇就到此结束了,因为时间实在是不够用,所以写得很慢而且很懒散,几乎是了了几句(掩面)。从文字上看,这实在不是一篇好教程,不过我觉得从中可以学习的是制作游戏过程的思路。

在开发过程中,不断的往主框架里添加新东西,然后让整个游戏变得充实丰满。主要的游戏逻辑在这篇已经完成了,剩下的就是一些优化方面的东西,还有就是添加一些炫的效果等。

使用cocos2d-x开发会很方便,也很有code的感觉,但是C++实在是门不容易驾驭的语言,要非常注意指针和内存方面的使用。不过也会收益匪浅,起码在code方面会提升。最后,放上AnagramPuzzle完整版的代码,欢迎大家拍砖。