一开始搞错规则了,表示非常伤心~~~~~~~~~~~所以决定好好写下教程,不是为了混福利了哦~
环境是cocos2dx 3.0rc0版本,rc1版本需要修改一点地方,下面会提到改动.
那么开始吧:
首先开始界面有一个标题
让我们来创建一个标题
void GameHome::createTitle()
{
auto title = Label::create("Greedy Snake","fonts/Marker Felt.ttf",50);
title->setAnchorPoint(Point::ANCHOR_MIDDLE);
title->setPosition(480,520);
title->enableShadow(Color3B::RED);
this->addChild(title);
}
```
这里用到3.0的一个新的Lable,具体请看论坛置顶帖,上面有仔细讲这个东西,我就不重复了,总之比以前的功能强大,可以描边,阴影,荧光.
我们这里用到了阴影,红色的阴影,然后锚点居中就OK了.
在rc1版本中这里是Color4B,使用rc1的童鞋,注意一下!其他下面用到enableShadow这个函数的地方同理.
接着我们需要一个开始游戏和关闭游戏的按钮
void GameHome::createMenu()
{
auto menu = Menu::create(
createMenuTTF("play",Point(0,30),CC_CALLBACK_1(GameHome::play,this)),
createMenuTTF("close",Point(0,-100),CC_CALLBACK_1(GameHome::close,this)),
nullptr);
this->addChild(menu);
}
MenuItemLabel* GameHome::createMenuTTF( const std::string& name,const Point& pos,const ccMenuCallback& callback )
{
auto closeLabel = Label::create(name,"fonts/Marker Felt.ttf",40);
closeLabel->enableShadow(Color3B::GREEN);
auto menuItemLabel = MenuItemLabel::create(closeLabel,callback);
menuItemLabel->setPosition(pos);
return menuItemLabel;
}
```
OK,这里我们也用到上面说的Lable,并且使用了全新的MenuItemLabel,同样我们使用了描边,让字体更加好看.
接下来我们跳转到游戏场景中
void Game::onEnter()
{
Scene::onEnter();
this->schedule(schedule_selector(Game::snakeMove),0.2f);
_istouchSnakeDirection = false;
_snakeDirection = RIGHT;
initSnake();
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = =](Touch* touch,Event*){ return true; };
listener->onTouchEnded = =](Touch* touch,Event*){ touchSnakeDirection(touch->getLocation()); };
this->_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,this);
createApple();
}
```
使用onEnter初始化,这里说一下为什么没有使用init,个人认为init适合一些基础数据的初始化,不适合创建精灵和各种节点,因为在场景跳转中,需要先执行新场景init,然后删除旧场景,会瞬间导致存在两个场景,内存暴涨的情况,而onEnter是在删除前一个场景之后调用,缓和了那一帧的计算量和内存,有一个缓冲的效果.
回到游戏中..
this->schedule(schedule_selector(Game::snakeMove),0.2f); 添加定时器0.2秒,这个也是蛇的移动速度,每0.2秒移动一次.
_snakeDirection = RIGHT; 初始移动向右
initSnake();初始化蛇
那一坨是添加touch事件
createApple();创建一个苹果...为什么取名字叫苹果呢..因为苹果被咬了~~囧
_snakeDirection一个东西是个枚举,结构是这样的,至于为什么是这样,后面会讲到...
enum SnakeDirection
{
UP_OR_DOWN = 1,
UP = 1,
DOWN = -1,
LEFT_OR_RIGHT = 2,
LEFT = 2,
RIGHT = -2
};
```
也就是4个方向.
void Game::initSnake()
{
_snake.clear();
for (int i = 0; i < 3;++i)
{
auto drawNode = createDrawNode(Point(0-kSnakeNodeSize.width*i,0)+Point(kSnakeNodeSize*12.5f));
_snake.push_back(drawNode);
}
}
```
初始化蛇,这里面有一个createDrawNode,让我们来看看这个函数
DrawNode* Game::createDrawNode( const Point& pos )
{
auto drawNode = DrawNode::create();
Size size(kSnakeNodeSize);
Point point = Point(-size.width/2,size.height/2);
Point points = {
point,
Point(point.x,point.y-size.height),
Point(point.x + size.width,point.y-size.height),
Point(point.x+size.width,point.y)};
drawNode->drawPolygon(points,4,Color4F::WHITE,1,Color4F::RED);
drawNode->setPosition(pos);
this->addChild(drawNode);
return drawNode;
}
```
这里面使用DrawNode画了一个四边形,并且有描边,里面是白色,描边是红色.
这里需要注意的一个地方是,画多边形的第一个点是左上角,然后向下逆时针旋转的顶点排序,这样的话会使得我们的锚点看起来像是左上角的样子,不符合我们通常的思维,
这里Point point = Point(-size.width/2,size.height/2);等于让我们看起来锚点是在中心了,当然其实锚点一直是中心,只不过画是从左上角开始画,所以我们需要一点调整.
kSnakeNodeSize这是一个常量,就是蛇一节一节的大小.
下面我们说一下创建一个苹果...
void Game::createApple()
{
Point pos;
bool b = false;
do
{
Size size = Director::getInstance()->getVisibleSize();
pos.x = ( (int)(CCRANDOM_0_1() * (size.width/kSnakeNodeSize.width-1)) + 0.5f ) * kSnakeNodeSize.width;
pos.y = ( (int)(CCRANDOM_0_1() * (size.height/kSnakeNodeSize.height-1)) + 0.5f ) * kSnakeNodeSize.height;
for (auto m : _snake)
{
if (m->getPosition().equals(pos))
{
b = true;
break;
}
else
{
b = false;
}
}
} while (b);
_apple = createDrawNode(pos);
}
```
这里用到了一个随机数,我没有使用种子,会导致每次开始游戏都在那个位置..懒~~~
这里需要满足两个条件随机的坐标正确,还有一个是不能随机在蛇的身上.
当然没有判断蛇吃满整个屏幕的情况,会导致永远随机不到合适的位置...
或者在蛇占屏幕的最后几个格子的时候,可能会导致要很久才能随机到合适的位置
其实这里应该改为,把所有的合理点放在一个容器里,在这个容器里去随机一个点,这样不仅效率高,而且正确..当然我懒...囧..
然后到我们的交互了,我们的touch做了什么呢?
void Game::touchSnakeDirection( const Point& tochPoint )
{
_istouchSnakeDirection = false;
if (abs(_snakeDirection) == UP_OR_DOWN)
{
if (tochPoint.x < _snake->getPosition().x)
{
_touchSnakeDirection = LEFT;
}
else
{
_touchSnakeDirection = RIGHT;
}
_istouchSnakeDirection = true;
}
else if (abs(_snakeDirection) == LEFT_OR_RIGHT)
{
if (tochPoint.y < _snake->getPosition().y)
{
_touchSnakeDirection = DOWN;
}
else
{
_touchSnakeDirection = UP;
}
_istouchSnakeDirection = true;
}
}
```
这里就用到了这个枚举的设计,如果这个时候蛇的方向是上下,那么点击蛇头的左边就是向左,点击蛇头的右边就是向右,当现在是方向是左右的时候同理.
枚举用了一个小技巧取绝对值,来判断当前的上下和左右.
然后根据现在蛇的方向与touch指出的方向
这里有个需要注意的问题,,这里我们明明已经判断出蛇的方向了,为什么还需要一个bool值来记录方向是否有改变,因为我们必须要在主循环中去改变当前的方向,因为如果我们在一次循环间隙中点击多次(这个游戏的主循环是0.2s很长,这种问题会非常明显,如果是1/60s,就比较难了,你需要在1/60秒之内点击2次以上,不要牺牲大量的效能来偷懒,在这里你不需要用到如此快的定时器,请记住,大多数情况你都不需要使用1/60s的定时器),就会导致你实际上改变了判断条件,条件是当前的方向,因为当前的方向已经被你上一次的touch改变了,但实际上你2次或者多次的改变都在这一次主循环中,要记住主循环是控制游戏正在变化,其他只是向主循环发出的更改请求,一次循环的其中一个属性只能改变一次,就比如你连续设置两次位置,你在视图是看不到两次位置的改变的.
这里我可能描述不是很清楚,但我表示已经江郎才尽了~~~
接下来就是我们蛇的移动了,也就是游戏的主循环
void Game::snakeMove( float )
{
if (_istouchSnakeDirection)
{
_snakeDirection = _touchSnakeDirection;
}
for (size_t i = _snake.size()-1; i > 0 ;--i)
{
auto pos = _snake*->getPosition();
_snake->setPosition(pos);
}
snakeHeadMove();
if (snakeIsDie())
{
snakeDie();
}
if (snakeIsAapple())
{
createSnakeNode();
}
}
```
判断布尔值,方向是否有发生改变,如果发生改变,则改变方向状态变量.
蛇的移动是从最后一个蛇节点向前面一个蛇节点移动,就是后面一个节点设置成前面一个节点的位置,从而达到移动的效果.
snakeHeadMove();蛇头的移动
void Game::snakeHeadMove()
{
switch (_snakeDirection)
{
case Game::UP:
_snake->setPositionY(_snake->getPositionY()+kSnakeNodeSize.height);
break;
case Game::DOWN:
_snake->setPositionY(_snake->getPositionY()-kSnakeNodeSize.height);
break;
case Game::LEFT:
_snake->setPositionX(_snake->getPositionX()-kSnakeNodeSize.width);
break;
case Game::RIGHT:
_snake->setPositionX(_snake->getPositionX()+kSnakeNodeSize.width);
break;
default:
break;
}
}
```
根据蛇方向移动蛇头.
snakeIsDie()判断蛇是否死亡
bool Game::snakeIsDie()
{
auto pos = _snake->getPosition();
for (size_t i = 1; i < _snake.size(); ++i)
{
if (_snake->getPosition().equals(pos))
{
return true;
}
}
auto size = Director::getInstance()->getVisibleSize();
Rect rect(kSnakeNodeSize.width/2,kSnakeNodeSize.height/2,
size.width - kSnakeNodeSize.width,size.height - kSnakeNodeSize.height);
if (!rect.containsPoint(pos))
{
return true;
}
return false;
}
```
蛇死亡有两种情况,一种是自己咬死自己了,一种是撞墙了.
这里大概就不细说了,说下注意的地方_snake->getPosition().equals(pos) 点之前的判断使用equals而非==,因为浮点精度问题,往往不相等,通常情况不要对浮点数使用==,通常是不相等的,虽然你看起来他们很相等~~~~
snakeDie();蛇死亡
void Game::snakeDie()
{
this->unscheduleAllSelectors();
auto title = Label::create("Game Over","fonts/Marker Felt.ttf",50);
title->setAnchorPoint(Point::ANCHOR_MIDDLE);
title->setPosition(480,520);
title->enableShadow(Color3B::RED);
this->addChild(title);
auto closeLabel = Label::create("Restart","fonts/Marker Felt.ttf",40);
closeLabel->enableShadow(Color3B::GREEN);
auto menuItemLabel = MenuItemLabel::create(closeLabel,](Ref*){
Director::getInstance()->replaceScene(Game::create()); });
auto menu = Menu::create(menuItemLabel,nullptr);
this->addChild(menu);
}
```
蛇死亡关闭主循环,跳出死亡界面.这里就没什么讲的了..
snakeIsAapple(); 蛇是否吃到苹果了
bool Game::snakeIsAapple()
{
auto posApple = _apple->getPosition();
auto posSnake = _snake->getPosition();
if (posApple.equals(posSnake))
{
_apple->removeFromParent();
createApple();
return true;
}
return false;
}
```
蛇吃到一个苹果,就删除这个苹果,然后创建一个新的苹果.OK
createSnakeNode(); 创建一个蛇节点,
void Game::createSnakeNode()
{
auto drawNode = createDrawNode(Point::ZERO);
_snake.push_back(drawNode);
}
```
其实非常简单,就是push一个NODE进去,位置都不用设置,由于主循环里面是会从最后一个点向前移动,所以就妥妥展开了...
好的,终于写完这个教程了,
原来已经写了这么..这么..这么长了.
可以向版主要福利了吗~~~呜呜~~~~(>_<)~~~~
*




