三消是消除游戏里面的经典玩法,看起来虽然简单,其实里面的逻辑一点都不简单,通过一个基础的范例来对经典三消游戏一探究竟

ps:所有素材都来自于互联网,仅供学习和参考

预览

工程结构

环境

  • win10
  • vs2015
  • cocos2dx3.16

代码目录

游戏架构

主要有以下场景

  • 欢迎场景
  • 游戏场景(三消界面)

步骤

欢迎场景

只是用于转场,为了简便,这个demo里面没有预加载和缓存

bool MenuScene::init()
{if (!Scene::init())return false;   // 获得屏幕尺寸常量(必须在类函数里获取)const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();// 加载菜单页面背景Sprite *menu_background = Sprite::create("images/menu_bg.jpg");menu_background->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);addChild(menu_background, 0);// 添加开始菜单Label *start_label = Label::createWithTTF("Start Game", "fonts/Marker Felt.ttf", 35);start_label->setTextColor(cocos2d::Color4B::RED);// 用lambda表达式作为菜单回调MenuItemLabel *start_menu_item = MenuItemLabel::create(start_label, [&](Ref *sender) {CCLOG("click start game"); // 注意,只有debug模式才会输出CCLOG// 转场到游戏主界面Scene *main_game_scene = GameScene::createScene();TransitionScene *transition = TransitionFade::create(0.5f, main_game_scene, Color3B(255, 255, 255));Director::getInstance()->replaceScene(transition);});start_menu_item->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);Menu *menu = Menu::createWithItem(start_menu_item);menu->setPosition(Vec2::ZERO);addChild(menu, 1);return true;
}

游戏场景

游戏主场景里面就是内容最丰富的三消界面了,所有的游戏逻辑和相关动画都写在里面

数据结构

每个可消除元素是一个精灵,具有类型、标记、坐标、名称,以及出现动画和消失动画等信息,整个游戏地图是个一个二维矩阵

// 精灵的行列值结构体
struct ElementPos
{int row;int col;// fixme: the constructor will not compile success in coco2dx//ElementPos(int _row, int _col): row(_row), col(_col)//{}
};// 逻辑精灵结构体
struct ElementProto
{int type;bool marked;
};
bool Element::init()
{if (!Sprite::init())return false;// 初始化element_type = -1;return true;
}void Element::appear()
{// 延时显示特效再出现setVisible(false);scheduleOnce(schedule_selector(Element::appearSchedule), 0.3);
}void Element::appearSchedule(float dt)
{setVisible(true);setScale(0.5);ScaleTo *scale_to = ScaleTo::create(0.2, 1.0);runAction(scale_to);
}void Element::vanish()
{// 延时显示特效再消失ScaleTo *scale_to = ScaleTo::create(0.2, 0.5);CallFunc *funcall = CallFunc::create(this, callfunc_selector(Element::vanishCallback));Sequence *sequence = Sequence::create(DelayTime::create(0.2), scale_to, funcall, NULL);runAction(sequence);
}void Element::vanishCallback()
{removeFromParent();
}

全局定义

// 场景中的层次,数字大的在上层
const int kBackGroundLevel = 0; // 背景层
const int kGameBoardLevel = 1;  // 实际的游戏精灵层
const int kFlashLevel = 3; // 显示combo的弹层
const int kMenuLevel = 5; // 菜单层// 精灵纹理文件,索引值就是类型
const std::vector<std::string> kElementImgArray{"images/diamond_red.png","images/diamond_green.png","images/diamond_blue.png","images/candy_red.png","images/candy_green.png","images/candy_blue.png"
};// combo标语
const std::vector<std::string> kComboTextArray{"Good","Great","Unbelievable"
};// 声音文件
const std::string kBackgourndMusic = "sounds/background.mp3";
const std::string kWelcomeEffect = "sounds/welcome.mp3";
const std::string kPopEffect = "sounds/pop.mp3";
const std::string kUnbelievableEffect = "sounds/unbelievable.mp3";// 消除分数单位
const int kScoreUnit = 10;// 消除时候类型和纹理
const int kElementEliminateType = 10;
const std::string kEliminateStartImg = "images/star.png";// 界面边距
const float kLeftMargin = 20;
const float kRightMargin = 20;
const float kBottonMargin = 70;// 精灵矩阵行列数
const int kRowNum = 8;
const int kColNum = 8;// 可消除状态枚举
const int kEliminateInitFlag = 0;
const int kEliminateOneReadyFlag = 1;
const int KEliminateTwoReadyFlag = 2;

初始化

初始化的时候场景需要做几件事情

  • 生成并绘制游戏格子地图
  • 初始化分数、进度条、音效、combo文字等辅助元素
  • 添加触摸监听
  • 启动渲染计时器
  • 设置条件变量
// 初始化主场景
bool GameScene::init()
{if (!Layer::init())return false;// 获得屏幕尺寸常量(必须在类函数里获取)const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();// 加载游戏界面背景Sprite *game_background = Sprite::create("images/game_bg.jpg");game_background->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);addChild(game_background, kBackGroundLevel);// 初始化游戏地图for (int i = 0; i < kRowNum; i++){std::vector<ElementProto> line_elements;for (int j = 0; j < kRowNum; j++){ElementProto element_proto;element_proto.type = kElementEliminateType; // 初始化置成消除状态,便于后续生成element_proto.marked = false;line_elements.push_back(element_proto);}_game_board.push_back(line_elements);}// 绘制游戏地图drawGameBoard();// 初始游戏分数_score = 0;_animation_score = 0;_score_label = Label::createWithTTF(StringUtils::format("score: %d", _score), "fonts/Marker Felt.ttf", 20);_score_label->setTextColor(cocos2d::Color4B::YELLOW);_score_label->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height * 0.9);_score_label->setName("score");addChild(_score_label, kBackGroundLevel);// 初始触摸坐标_start_pos.row = -1;_start_pos.col = -1;_end_pos.row = -1;_end_pos.col = -1;// 初始移动状态_is_moving = false;_is_can_touch = true;_is_can_elimate = 0; // 0, 1, 2三个等级,0为初始,1表示一个精灵ready,2表示两个精灵ready,可以消除// 进度条_progress_timer = ProgressTimer::create(Sprite::create("images/progress_bar.png"));//创建一个进程条_progress_timer->setBarChangeRate(Point(1, 0));_progress_timer->setType(ProgressTimer::Type::BAR);_progress_timer->setMidpoint(Point(0, 1));_progress_timer->setPosition(Point(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height * 0.8));_progress_timer->setPercentage(100.0); // 初始为满addChild(_progress_timer, kBackGroundLevel);schedule(schedule_selector(GameScene::tickProgress), 1.0);// 播放音效SimpleAudioEngine::getInstance()->playBackgroundMusic(kBackgourndMusic.c_str(), true);SimpleAudioEngine::getInstance()->playEffect(kWelcomeEffect.c_str());// 添加combo标语label_combo_label = Label::createWithTTF(StringUtils::format("Ready Go"), "fonts/Marker Felt.ttf", 40);_combo_label->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);addChild(_combo_label, kFlashLevel);_combo_label->runAction(Sequence::create(DelayTime::create(0.8), MoveBy::create(0.3, Vec2(200, 0)), CallFunc::create([=]() {// 初始动画后隐藏,并重置位置_combo_label->setVisible(false);_combo_label->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);}), NULL));// 添加触摸事件监听EventListenerTouchOneByOne *touch_listener = EventListenerTouchOneByOne::create();touch_listener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan, this);touch_listener->onTouchMoved = CC_CALLBACK_2(GameScene::onTouchMoved, this);touch_listener->onTouchEnded = CC_CALLBACK_2(GameScene::onTouchEnded, this);_eventDispatcher->addEventListenerWithSceneGraphPriority(touch_listener, this); // 父类的 _eventDispatcher// 默认渲染循环调度器scheduleUpdate();return true;
}

生成和绘制游戏地图

游戏地图其实就是填满消除元素的矩阵,在初始生成的时候要考虑不会出现能够三个连起来消除的情况

基本思想是:遍历每个格子,随机填充一个类型,如果整个地图没有构成可消除,则向四个方向递归填充,直到所有格子被填充满为止

游戏逻辑是在后台内存里的,所有的矩阵变换都要反映到界面上,所以需要按照矩阵来绘制整个游戏格子地图

// 填充空白游戏地图,保证没有可消除的组合,(此算法目前是work的,但并不完美)
void GameScene::fillGameBoard(int row, int col)
{// 遇到边界则返回if (row == -1 || row == kRowNum || col == -1 || col == kColNum)return;// 随机生成类型int random_type = getRandomSpriteIndex(kElementImgArray.size());// 填充if (_game_board[row][col].type == kElementEliminateType){_game_board[row][col].type = random_type;// 如果没有消除则继续填充if (!hasEliminate()){// 四个方向递归填充fillGameBoard(row + 1, col);fillGameBoard(row - 1, col);fillGameBoard(row, col - 1);fillGameBoard(row, col + 1);}else_game_board[row][col].type = kElementEliminateType; // 还原}
}void GameScene::drawGameBoard()
{srand(unsigned(time(0))); // 初始化随机数发生器// 先在内存中生成,保证初始没有可消除的fillGameBoard(0, 0);// 如果生成不完美需要重新生成bool is_need_regenerate = false;for (int i = 0; i < kRowNum; i++){for (int j = 0; j < kColNum; j++){if (_game_board[i][j].type == kElementEliminateType){is_need_regenerate = true;}}if (is_need_regenerate)break;}// FIXME: sometime will crashif (is_need_regenerate){CCLOG("redraw game board");drawGameBoard();return;}// 获得屏幕尺寸常量(必须在类函数里获取)const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();// 添加消除对象矩阵,游戏逻辑与界面解耦float element_size = (kScreenSize.width - kLeftMargin - kRightMargin) / kColNum;for (int i = 0; i < kRowNum; i++){for (int j = 0; j < kColNum; j++){Element *element = Element::create();element->element_type = _game_board[i][j].type;element->setTexture(kElementImgArray[element->element_type]); // 添加随机纹理 element->setContentSize(Size(element_size, element_size)); // 在内部设置尺寸// 添加掉落特效Point init_position(kLeftMargin + (j + 0.5) * element_size, kBottonMargin + (i + 0.5) * element_size + 0.5 * element_size);element->setPosition(init_position);Point real_position(kLeftMargin + (j + 0.5) * element_size, kBottonMargin + (i + 0.5) * element_size);Sequence *sequence = Sequence::create(MoveTo::create(0.5, real_position), CallFunc::create([=]() {element->setPosition(real_position); // lambda回调,设置最终真实位置}), NULL);element->runAction(sequence);std::string elment_name = StringUtils::format("%d_%d", i, j);element->setName(elment_name); // 每个界面精灵给一个唯一的名字标号便于后续寻找addChild(element, kGameBoardLevel); }}
}

触摸移动

监听屏幕触控,填充三个回调函数,在onTouchMoved函数里面判断是否有元素交换,从而做出执行后面的交换动画

  • 触摸开始,获取起始元素坐标
  • 触摸移动过程中,获取需要交换的元素坐标,注意只能是相邻的元素
  • 满足交换条件则执行元素的交换,并且在交换过程中禁止触摸
  • 交换后如果可消除,则执行消除,如果不可消除,则交换回来
  • 当交换结束,恢复可触摸状态
bool GameScene::onTouchBegan(Touch *touch, Event *event)
{//CCLOG("touch begin, x: %f, y: %f", touch->getLocation().x, touch->getLocation().y);// 只有在可触摸条件下才可以if (_is_can_touch){// 记录开始触摸的精灵坐标_start_pos = getElementPosByCoordinate(touch->getLocation().x, touch->getLocation().y);CCLOG("start pos, row: %d, col: %d", _start_pos.row, _start_pos.col);// 每次触碰算一次新的移动过程_is_moving = true;}return true;}void GameScene::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event)
{//CCLOG("touch moved, x: %f, y: %f", touch->getLocation().x, touch->getLocation().y);// 只有在可触摸条件下才可以if (_is_can_touch){// 根据触摸移动的方向来交换精灵(实际上还可以通过点击两个精灵来实现)// 计算相对位移,拖拽精灵,注意范围if (_start_pos.row > -1 && _start_pos.row < kRowNum&& _start_pos.col > -1 && _start_pos.col < kColNum){// 通过判断移动后触摸点的位置在哪个范围来决定移动的方向Vec2 cur_loacation = touch->getLocation();// 触摸点只获取一次,防止跨精灵互换if (_end_pos.row == -1 && _end_pos.col == -1|| _end_pos.row == _start_pos.row && _end_pos.col == _start_pos.col)_end_pos = getElementPosByCoordinate(cur_loacation.x, cur_loacation.y);if (_is_moving){// 根据偏移方向交换精灵bool is_need_swap = false;CCLOG("cur pos, row: %d, col: %d", _end_pos.row, _end_pos.col);if (_start_pos.col + 1 == _end_pos.col && _start_pos.row == _end_pos.row) // 水平向右is_need_swap = true;else if (_start_pos.col - 1 == _end_pos.col && _start_pos.row == _end_pos.row) // 水平向左is_need_swap = true;else if (_start_pos.row + 1 == _end_pos.row && _start_pos.col == _end_pos.col) // 竖直向上is_need_swap = true;else if (_start_pos.row - 1 == _end_pos.row && _start_pos.col == _end_pos.col) // 竖直向下is_need_swap = true;if (is_need_swap){// 执行交换swapElementPair(_start_pos, _end_pos, false);// 回归非移动状态_is_moving = false;}}}}
}void GameScene::onTouchEnded(Touch *touch, Event *event)
{//CCLOG("touch end, x: %f, y: %f", touch->getLocation().x, touch->getLocation().y);_is_moving = false;
}

循环渲染

在游戏的默认主loop中需要做一些每帧都更新的内容

  • 判断是否可消除
  • 判断是否僵局
  • 判断是否需要交换回来
void GameScene::update(float dt)
{// 需要确保标记清除if (_start_pos.row == -1 && _start_pos.col == -1&& _end_pos.row == -1 && _end_pos.col == -1)_is_can_elimate = kEliminateInitFlag;CCLOG("eliminate flag: %d", _is_can_elimate);// 每帧检查是否僵局,如果不是死局则显示当前提示点ElementPos game_hint_point = checkGameHint();if (game_hint_point.row == -1 && game_hint_point.col == -1){CCLOG("the game is dead");_combo_label->setString("dead game");_combo_label->setVisible(true);unschedule(schedule_selector(GameScene::tickProgress));}elseCCLOG("game hint point: row %d, col %d", game_hint_point.row, game_hint_point.col);// 交换动画后判断是否可以消除if (_is_can_elimate == KEliminateTwoReadyFlag){auto eliminate_set = getEliminateSet();if (!eliminate_set.empty()){batchEliminate(eliminate_set);// 消除完毕,还原标志位_is_can_elimate = kEliminateInitFlag; // 复位移动起始位置_start_pos.row = -1;_start_pos.col = -1;_end_pos.row = -1;_end_pos.col = -1;}else{// 没有可消除的,如果刚交换过,需要交换回来if (_start_pos.row >= 0 && _start_pos.row < kRowNum && _start_pos.col >= 0 && _start_pos.col < kColNum&&_end_pos.row >= 0 && _end_pos.row < kRowNum && _end_pos.row >= 0 && _start_pos.col < kColNum&& (_start_pos.row != _end_pos.row || _start_pos.col != _end_pos.col)){// 消除完毕,还原标志位,为反向交换准备_is_can_elimate = kEliminateInitFlag;swapElementPair(_start_pos, _end_pos, true);// 复位移动起始位置_start_pos.row = -1;_start_pos.col = -1;_end_pos.row = -1;_end_pos.col = -1;}}}
}

交换元素

交换元素是比较复杂的地方

  • 既要交换在内存中交换两个元素坐标,也要在界面将两个元素进行动画交换
  • 内存中交换,只需要根据坐标交换类型
  • 由于动画是异步的,并且动画的移动并不会改变元素的真正position,所以在动画的结束回调里面需要重设position,name
  • 在交换过程中,既不能触摸,也不能执行消除,必须等到交换动画结束之后才可以,所以需要设置两个标志位
  • 在交换结束后,如果不能消除,需要交换回来,所以要及时清除某些标志位
void GameScene::swapElementPair(ElementPos p1, ElementPos p2, bool is_reverse)
{// 交换时禁止可触摸状态_is_can_touch = false;const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();float element_size = (kScreenSize.width - kLeftMargin - kRightMargin) / kColNum;// 交换的逻辑,分3个层次// 内存,游戏精灵层,动画精灵层// 顺序需要根据反应速度由先到后,由同步到异步// 获得原始精灵相关信息std::string name1 = StringUtils::format("%d_%d", p1.row, p1.col);std::string name2 = StringUtils::format("%d_%d", p2.row, p2.col);Element *element1 = (Element *)getChildByName(name1);Element *element2 = (Element *)getChildByName(name2);Point position1 = element1->getPosition();Point position2 = element2->getPosition();int type1 = element1->element_type;int type2 = element2->element_type;CCLOG(is_reverse ? "==== reverse move ====" : "==== normal move ====");CCLOG("before move");CCLOG("p1 name: %s", element1->getName().c_str());CCLOG("p2 name: %s", element2->getName().c_str());CCLOG("position1, x: %f, y: %f", element1->getPosition().x, element1->getPosition().y);CCLOG("position2, x: %f, y: %f", element2->getPosition().x, element2->getPosition().y);// ---- 实际交换// 内存中交换精灵类型std::swap(_game_board[p1.row][p1.col], _game_board[p2.row][p2.col]);// 移动动画, move action并不会更新positionfloat delay_time = is_reverse ? 0.5 : 0;DelayTime *move_delay = DelayTime::create(delay_time); // 反向交换需要延时MoveTo *move_1to2 = MoveTo::create(0.2, position2);MoveTo *move_2to1 = MoveTo::create(0.2, position1);CCLOG("after move");element1->runAction(Sequence::create(move_delay, move_1to2, CallFunc::create([=]() {// lambda 表达式回调,注意要用 = 捕获外部指针// 重设位置,CCLOG("e1 moved");element1->setPosition(position2);// 交换名称element1->setName(name2);_is_can_elimate++;CCLOG("p1 name: %s", element1->getName().c_str());CCLOG("position1, x: %f, y: %f", element1->getPosition().x, element1->getPosition().y);}), NULL));element2->runAction(Sequence::create(move_delay, move_2to1, CallFunc::create([=]() {CCLOG("e2 moved");element2->setPosition(position1);element2->setName(name1);_is_can_elimate++;CCLOG("p2 name: %s", element2->getName().c_str());CCLOG("position2, x: %f, y: %f", element2->getPosition().x, element2->getPosition().y);}), NULL));// 恢复触摸状态_is_can_touch = true;
}

判断消除和执行消除

有两个地方用到了检验消除

基本思想是:遍历游戏地图,判断每个格子是否和上下或者左右形成三连,如果是就判断为有消除或者加入到列表,标记为marked

这里并没有采用递归的逻辑,因为遍历虽然有时间开销,但是逻辑较简单,也不会有堆栈溢出的风险

  • 生成游戏地图的时候,要保证每填充一格都不能消除
  • 交换完毕的时候,如果有可消除的元素,放到可消除列表
bool GameScene::hasEliminate()
{bool has_elminate = false;for (int i = 0; i < kRowNum; i++){for (int j = 0; j < kColNum; j++){// 要保证精灵和交换的精灵都不是标记为消除if (_game_board[i][j].type != kElementEliminateType){// 判断上下是否相同if (i - 1 >= 0&& _game_board[i - 1][j].type != kElementEliminateType&& _game_board[i - 1][j].type == _game_board[i][j].type&& i + 1 < kRowNum&& _game_board[i + 1][j].type != kElementEliminateType&& _game_board[i + 1][j].type == _game_board[i][j].type){has_elminate = true;break;}// 判断左右是否相同if (j - 1 >= 0&& _game_board[i][j - 1].type != kElementEliminateType&& _game_board[i][j - 1].type == _game_board[i][j].type&& j + 1 < kColNum&& _game_board[i][j - 1].type != kElementEliminateType&& _game_board[i][j + 1].type == _game_board[i][j].type){has_elminate = true;break;}}}if (has_elminate)break;}return has_elminate;
}// 全盘扫描检查可消除精灵,添加到可消除集合
std::vector<ElementPos> GameScene::getEliminateSet()
{std::vector<ElementPos> res_eliminate_list;// 采用简单的二维扫描来确定可以三消的结果集,横竖连着大于或等于3个就消除,不用递归for (int i = 0; i < kRowNum; i++)for (int j = 0; j < kColNum; j++){// 判断上下是否相同if (i - 1 >= 0&& _game_board[i - 1][j].type == _game_board[i][j].type&& i + 1 < kRowNum&& _game_board[i + 1][j].type == _game_board[i][j].type){// 添加连着的竖向三个,跳过已添加的和已消除的(虽然有填充,但是保险起见)if (!_game_board[i][j].marked && _game_board[i][j].type != kElementEliminateType){ElementPos pos;pos.row = i;pos.col = j;res_eliminate_list.push_back(pos);_game_board[i][j].marked = true;}if (!_game_board[i - 1][j].marked && _game_board[i - 1][j].type != kElementEliminateType){ElementPos pos;pos.row = i - 1;pos.col = j;res_eliminate_list.push_back(pos);_game_board[i - 1][j].marked = true;}if (!_game_board[i + 1][j].marked && _game_board[i + 1][j].type != kElementEliminateType){ElementPos pos;pos.row = i + 1;pos.col = j;res_eliminate_list.push_back(pos);_game_board[i + 1][j].marked = true;}}// 判断左右是否相同if (j - 1 >= 0&& _game_board[i][j - 1].type == _game_board[i][j].type&& j + 1 < kColNum&& _game_board[i][j + 1].type == _game_board[i][j].type){// 添加连着的横向三个,跳过已添加的if (!_game_board[i][j].marked && _game_board[i][j].type != kElementEliminateType){ElementPos pos;pos.row = i;pos.col = j;res_eliminate_list.push_back(pos);_game_board[i][j].marked = true;}if (!_game_board[i][j - 1].marked && _game_board[i][j - 1].type != kElementEliminateType){ElementPos pos;pos.row = i;pos.col = j - 1;res_eliminate_list.push_back(pos);_game_board[i][j - 1].marked = true;}if (!_game_board[i][j + 1].marked && _game_board[i][j + 1].type != kElementEliminateType){ElementPos pos;pos.row = i;pos.col = j + 1;res_eliminate_list.push_back(pos);_game_board[i][j + 1].marked = true;}}}return res_eliminate_list;
}

有了可消除列表之后,就执行消除,将内存中的类型标记为消除类型,播放消除动画

void GameScene::batchEliminate(const std::vector<ElementPos> &eliminate_list)
{// 播放消除音效SimpleAudioEngine::getInstance()->playEffect(kPopEffect.c_str());// 切换精灵图标并消失const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();float element_size = (kScreenSize.width - kLeftMargin - kRightMargin) / kColNum;for (auto &pos : eliminate_list){std::string elment_name = StringUtils::format("%d_%d", pos.row, pos.col);Element *element = (Element *)(getChildByName(elment_name));_game_board[pos.row][pos.col].type = kElementEliminateType; // 标记成消除类型element->setTexture(kEliminateStartImg); // 设置成消除纹理element->setContentSize(Size(element_size, element_size)); // 在内部设置尺寸element->vanish();}// combo标语std::string combo_text;int len = eliminate_list.size();if (len >= 4)SimpleAudioEngine::getInstance()->playEffect(kUnbelievableEffect.c_str());if (len == 4)combo_text = kComboTextArray[0];else if (len > 4 && len <= 6)combo_text = kComboTextArray[1];else if (len > 6)combo_text = kComboTextArray[2];_combo_label->setString(combo_text);_combo_label->setVisible(true);_combo_label->runAction(Sequence::create(MoveBy::create(0.5, Vec2(0, -50)), CallFunc::create([=]() {// 初始动画后隐藏并重置位置_combo_label->setVisible(false);_combo_label->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);}), NULL));// 修改分数addScore(kScoreUnit * eliminate_list.size());// 下降精灵scheduleOnce(schedule_selector(GameScene::dropElements), 0.5);
}

下降填充

精灵消除后会形成空白,上方的精灵依次下落填补空白

  • 下落的过程中,顶部又出现空白,需要随机生成并填补
  • 下落之后会伴随着连消,连消需要延迟执行
void GameScene::dropElements(float dt)
{_is_can_touch = false;// 获得屏幕尺寸常量(必须在类函数里获取)const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();float element_size = (kScreenSize.width - kLeftMargin - kRightMargin) / kColNum;// 精灵下降填补空白for (int j = 0; j < kColNum; j++){std::vector<Element *> elements;for (int i = kRowNum - 1; i >= 0; i--){if (_game_board[i][j].type != kElementEliminateType){std::string element_name = StringUtils::format("%d_%d", i, j);Element *element = (Element *)getChildByName(element_name);elements.push_back(element);}elsebreak; // 只添加空白上方的部分精灵}// 只有中间有空缺才处理if (elements.size() == kRowNum || elements.empty())continue;// 先反序一下std::reverse(elements.begin(), elements.end());// 每列下降int k = 0;int idx = 0;while (k < kRowNum){// 找到第一个空白的if (_game_board[k][j].type == kElementEliminateType)break;k++;}for (int idx = 0; idx < elements.size(); idx++){_game_board[k][j].type = elements[idx]->element_type;_game_board[k][j].marked = false;// 设置精灵位置和名称Point new_position(kLeftMargin + (j + 0.5) * element_size, kBottonMargin + (k + 0.5) * element_size);Sequence *sequence = Sequence::create(MoveTo::create(0.1, new_position), CallFunc::create([=]() {elements[idx]->setPosition(new_position); // lambda回调,设置最终真实位置}), NULL);elements[idx]->runAction(sequence);std::string new_name = StringUtils::format("%d_%d", k, j);elements[idx]->setName(new_name);k++;}while (k < kRowNum){_game_board[k][j].type = kElementEliminateType;_game_board[k][j].marked = true;k++;}}// 下降后填补顶部空白fillVacantElements();// 等空白精灵被填满后延迟消除scheduleOnce(schedule_selector(GameScene::delayBatchEliminate), 0.9);_is_can_touch = true;
}void GameScene::delayBatchEliminate(float dt)
{// 检验是否可连续消除auto eliminate_set = getEliminateSet();if (!eliminate_set.empty()){batchEliminate(eliminate_set);// 消除完毕,还原标志位_is_can_elimate = kEliminateInitFlag;// 复位移动起始位置_start_pos.row = -1;_start_pos.col = -1;_end_pos.row = -1;_end_pos.col = -1;}
}void GameScene::fillVacantElements()
{// 获得屏幕尺寸常量(必须在类函数里获取)const Size kScreenSize = Director::getInstance()->getVisibleSize();const Vec2 kScreenOrigin = Director::getInstance()->getVisibleOrigin();// 添加消除对象矩阵,游戏逻辑与界面解耦float element_size = (kScreenSize.width - kLeftMargin - kRightMargin) / kColNum;int len = kElementImgArray.size();srand(unsigned(time(0))); // 初始化随机数发生器// 先获取空白精灵集合for (int i = 0; i < kRowNum; i++)for (int j = 0; j < kColNum; j++){if (_game_board[i][j].type == kElementEliminateType){int random_type = getRandomSpriteIndex(len);_game_board[i][j].type = random_type;_game_board[i][j].marked = false;Element *element = Element::create();element->element_type = _game_board[i][j].type;element->setTexture(kElementImgArray[element->element_type]); // 添加随机纹理 element->setContentSize(Size(element_size, element_size)); // 在内部设置尺寸Point real_position(kLeftMargin + (j + 0.5) * element_size, kBottonMargin + (i + 0.5) * element_size);element->setPosition(real_position); // lambda回调,设置最终真实位置// 添加出现特效element->appear();std::string elment_name = StringUtils::format("%d_%d", i, j);element->setName(elment_name); // 每个界面精灵给一个唯一的名字标号便于后续寻找addChild(element, kGameBoardLevel);}}
}

判断僵局和获得提示

由于在游戏运行过程中,可能出现一个也消除不了情况,形成僵局

基本思想:遍历游戏地图,针对每个格子,尝试着往四个方向交换,如果能找到一个交换之后可消除的情况,则判断结束,不是僵局,获得该元素坐标作为提示,否则游戏形成僵局

  • 该函数既可以判断僵局,也可以用于获得提示
  • 获得提示是一个隐藏功能,没有往游戏界面上添加
ElementPos GameScene::checkGameHint()
{// 全盘扫描,尝试移动每个元素到四个方向,如果都没有可消除的,则游戏陷入僵局// 初始化提示点ElementPos game_hint_point;game_hint_point.row = -1;game_hint_point.col = -1;for (int i = 0; i < kRowNum; i++){for (int j = 0; j < kColNum; j++){// 上if (i < kRowNum - 1){// 交换后判断,然后再交换回来std::swap(_game_board[i][j], _game_board[i + 1][j]);if (hasEliminate()){game_hint_point.row = i;game_hint_point.col = j;// 注意这里虽然交换了内存数据,但是消除flag并不是可以动画的状态,所以不会影响到游戏std::swap(_game_board[i][j], _game_board[i + 1][j]);break;}std::swap(_game_board[i][j], _game_board[i + 1][j]);}// 下if (i > 0){std::swap(_game_board[i][j], _game_board[i - 1][j]);if (hasEliminate()){game_hint_point.row = i;game_hint_point.col = j;std::swap(_game_board[i][j], _game_board[i - 1][j]);break; // 找到一个点就跳出}std::swap(_game_board[i][j], _game_board[i - 1][j]);}// 左if (j > 0){std::swap(_game_board[i][j], _game_board[i][j - 1]);if (hasEliminate()){game_hint_point.row = i;game_hint_point.col = j;std::swap(_game_board[i][j], _game_board[i][j - 1]);break;}std::swap(_game_board[i][j], _game_board[i][j - 1]);}// 右 if (j < kColNum - 1){std::swap(_game_board[i][j], _game_board[i][j + 1]);if (hasEliminate()){game_hint_point.row = i;game_hint_point.col = j;std::swap(_game_board[i][j], _game_board[i][j + 1]);break;}std::swap(_game_board[i][j], _game_board[i][j + 1]);}}// 如果判断不是僵局,则跳出循环if (game_hint_point.row != -1 && game_hint_point.col != -1)break;}// 如果最后所有精灵都找不到可消除的return game_hint_point;
}

游戏分数

每次消除给游戏添加分数,分数增加有一个连续增长的特效动画,通过自定义计时器调度

void GameScene::addScoreCallback(float dt)
{_animation_score++;_score_label->setString(StringUtils::format("score: %d", _animation_score));// 加分到位了,停止计时器if (_animation_score == _score)unschedule(schedule_selector(GameScene::addScoreCallback));
}void GameScene::addScore(int delta_score)
{// 获得记分牌,更新分数和进度条_score += delta_score;_progress_timer->setPercentage(_progress_timer->getPercentage() + 3.0);if (_progress_timer->getPercentage() > 100.0)_progress_timer->setPercentage(100.0);// 进入计分加分动画schedule(schedule_selector(GameScene::addScoreCallback), 0.03);
}

combo效果和进度条

游戏辅助效果

  • 当出现连续消除数量较多时,增加一个combo特效
  • 时间进度条,进度条见到0则游戏结束,每次消除有时间奖励
// combo标语
std::string combo_text;
int len = eliminate_list.size();
if (len >= 4)SimpleAudioEngine::getInstance()->playEffect(kUnbelievableEffect.c_str());if (len == 4)combo_text = kComboTextArray[0];
else if (len > 4 && len <= 6)combo_text = kComboTextArray[1];
else if (len > 6)combo_text = kComboTextArray[2];
_combo_label->setString(combo_text);
_combo_label->setVisible(true);
_combo_label->runAction(Sequence::create(MoveBy::create(0.5, Vec2(0, -50)), CallFunc::create([=]() {// 初始动画后隐藏并重置位置_combo_label->setVisible(false);_combo_label->setPosition(kScreenOrigin.x + kScreenSize.width / 2, kScreenOrigin.y + kScreenSize.height / 2);
}), NULL));
void GameScene::tickProgress(float dt)
{// 根据时间衰减进度条到0if (_progress_timer->getPercentage() > 0.0)_progress_timer->setPercentage(_progress_timer->getPercentage() - 1.0);else{_combo_label->setString("game over");_combo_label->setVisible(true);unschedule(schedule_selector(GameScene::tickProgress));}}

音效

这个没有什么好说的,只需要在特定时刻播放音效或者音乐就好了

// 播放音效
SimpleAudioEngine::getInstance()->playBackgroundMusic(kBackgourndMusic.c_str(), true);
SimpleAudioEngine::getInstance()->playEffect(kWelcomeEffect.c_str());
// 播放消除音效
SimpleAudioEngine::getInstance()->playEffect(kPopEffect.c_str());
// combo音效
if (len >= 4)SimpleAudioEngine::getInstance()->playEffect(kUnbelievableEffect.c_str());

效果图



代码

csdn:三消
github:三消

cocos2dx实例开发之经典三消相关推荐

  1. cocos2dx实例开发之经典坦克

    小时候红白机上玩的的经典90坦克,看起来简单,做起来其实有点复杂,这里用原版素材还原了一个简版 预览 工程结构 游戏架构 包括场景: 欢迎界面,主菜单 游戏场景 步骤 菜单场景 对于图片,音乐,动画提 ...

  2. 2018年又传喜报!热烈祝贺王家林大师大数据经典著作《Spark SQL大数据实例开发教程》 畅销书籍 出版上市!

    2018年又传喜报!热烈祝贺王家林大师大数据经典著作<Spark SQL大数据实例开发教程> 畅销书籍 出版上市! 作者: 王家林 段智华  条码书号:9787111591979 出版日期 ...

  3. cocos2d-x游戏开发基础与实战 经典视频教程

    cocos2d-x游戏开发基础与实战 经典视频教程 cocos2d-x游戏开发工资高吗? 精通C/C++,熟练掌握Cocos2d-x引擎及其Cocos2d-x引擎周边开发工具,了解游戏开发常用的工具和 ...

  4. android入门经典dvd,Android开发入门经典实例 - My notes

    /* Android开发入门经典实例 */ My notes /* 安卓开发入门 */ 涉及到了Android开发中的一些关键知识,比如: 配置开发环境 App中一个屏幕的抽象: Activity 屏 ...

  5. 《实例妙解 Cocos2d-x 游戏开发》反馈勘误

    我的新书 <实例妙解 Cocos2d-x 游戏开发>已经上市了. 大家可以在书店或者网上购买: 部分购买地址: china-pub 当当网 京东 这个页面主要用于发布勘误.书中遇到了问题可 ...

  6. Android4开发入门经典 之 第七部分:数据存储

    数据存储基本知识 Android系统提供了多种数据存储的方式,如下: 1:Shared Preferences:用来存储私有的.原始类型的.简单的数据,通常是Key-value对 2:Internal ...

  7. Android开发的经典入门教材和学习…

    Android开发的经典入门教材和学习路线? 1.想利用寒假期间学习Android开发,了解到应该先学习Java,不知道选哪本书入门,学习Java和Android有什么经典教材,适合初学者.(有C++ ...

  8. java web开发实战经典 源码_李兴华 java_web开发实战经典 源码 完整版收集共享

    李兴华 java_web开发实战经典 源码 完整版收集共享 01f8a7  在  2018-11-07 20:41:33  上传  10.92 MB 第1章 JAVA WEB开发简介 1.1.WEB发 ...

  9. 李兴华java视频在线观看_李兴华Java开发实战经典视频教程_IT教程网

    资源名称:李兴华Java开发实战经典视频教程 资源目录: [IT教程网]010201_[第2章:简单Java程序]_简单Java程序 [IT教程网]010301_[第3章:Java基础程序设计]_Ja ...

最新文章

  1. PCB 使用Nginx让IIS7实现负载均衡
  2. 七年前将UC卖给马云,套现300亿的何小鹏,现今再创新奇迹?播报文章
  3. EasyUI中Messager消息框的简单使用
  4. DEBUG主要命令(转)
  5. oracle+mybatis查询遇到CHAR类型字段
  6. Java面试:Java面试总结PDF版
  7. 得到进程id_GDB调试多进程程序
  8. ADMM算法求解二次项目标函数+l1正则项问题
  9. Ubuntu安装配置sougou输入法
  10. Photoshop CS6 软件安装教程
  11. 《大数据之路:阿里巴巴大数据实践》-第3篇 数据管理篇 -第14章 存储和成本管理
  12. Excel中,编制卡方分布临界值表
  13. Windows 2016 server NVIDIA cuda toolkit11.3 pytorch-gpu 踩坑教程
  14. Windows Server - AD域 - 自动为域颁发证书
  15. PIP 更换国内安装源
  16. mysql 原子操作
  17. 第五人格调香师技能可以用几次?
  18. 风控中所涉及的重要指标全解析
  19. 手把手教学 | B端产品经理简历撰写指南(含专业话术+多套虚拟简历模板)
  20. matlab中怎样求峭度,【转】Matlab常用函数~

热门文章

  1. 计算机无法识别ipad,ipad连接电脑没反应怎么办 ipad air连接电脑无法识别解决办法...
  2. Java表的设计合同_java毕业设计_springboot框架的基于合同管理系统
  3. 一个网工的十年奋斗史 - 移民篇
  4. 有了这个抠图滤镜,设计师再也不怕扣头发婚纱了!
  5. java caller_callee和caller属性的区别
  6. html5 按钮css样式修改,css样式制作的漂亮按钮
  7. 6、域渗透中查询域用户对域成员机器关系
  8. 清华大学计算机研究生课程表
  9. 猜价格游戏c语言课程设计,肿么用C#编写一个猜价格的小程序?
  10. Java - Eclipse: Error notifying a preference change listener