三消是消除游戏里面的经典玩法,看起来虽然简单,其实里面的逻辑一点都不简单,通过一个基础的范例来对经典三消游戏一探究竟
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 crash
if (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));
}
else
CCLOG("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并不会更新position
float 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);
}
else
break; // 只添加空白上方的部分精灵
}
// 只有中间有空缺才处理
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)
{
// 根据时间衰减进度条到0
if (_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实例开发之经典三消
发布评论