消除类游戏是益智游戏的一种,玩家游戏过程中主要是将一定量相同的游戏元素,如水果、宝石、动物头像、积木麻将牌等,使它们彼此相邻配对消除来获胜。通常是将三个同样的元素配对消除,所以又称为三消类游戏。当然,三个以上的同样的元素也能消除。
对消除类游戏的设计与实现的各类游戏应用已经在20世纪80年代后期进入人们的日常休闲生活,随着各种网络以及计算机技术的发展,现如今消除类游戏已经成为各大游戏网站的热门下载。
游戏的主要任务是点击方块移动消除足够的方块获得得分,得分达到目标分数则可以通关。游戏的设计有:
界面部分:首先在界面中显示的是不同种类的图片作为消除的图片,在游戏图片消除以后的动画效果。最后还有分数的动态显示,动态效果统一使用SFML中的文本来实现。以及最后通关后游戏结束的界面。
游戏控制部分:在游戏控制部分中又分为三小块,第一是鼠标点击事件的处理,根据鼠标点击的图片判断前后两张图片是否可以交换;第二是两张图片交换后是否可以消除,如果可以消除则进行消除处理,如果不可以消除则将交换的图片恢复;第三是消除后的空白位置由以上图片填补,上面没有图片则随机生成图片。
消除游戏图片之后计分系统会根据消除图片的数量给予相应的得分,得分超过目标分即可通关。
在本次的游戏设计中消除算法整个游戏的重中之重,所以本次课题的研究方向主要是消除算法以及游戏中界面动画的设计与实现。
开发系统用的是windows系统中的visual studio2019,项目配置了第三方库SFML
消消乐项目课程设计个人独立开发完成,实现了游戏的所有算法函数和图片动态化的设计。
系统需要完成游戏背景的设置,所有方块初始化,方块可以被点击移动还原消除的动态效果等。游戏大致需要完成的功能有以下:
1.运行后是一个开始界面,开始界面有两个按键,点击开始游戏可以游玩游戏,点击退出可以关闭游戏。
2.点击两个方块后,方块会交换,交换后同行(同列)有三个以上相同的图片会消除,如果没有则会还原。
3.实现游玩中玩家得分的动态显示。
4.通关后跳出游戏结束界面,有分数的结算和游戏退出按键
系统需要处理好鼠标点击的坐标值以及所点击方块的行列信息,透明度。
系统中设计了Block类,即方块类。指游戏中需要玩家点击移动的小方块。这个类中需要有小方块的种类信息、位置信息、标记信息和方块的透明度(用于方块图片的消除消失实现)。
所以Block类中有整型的数据x,y记录方块的位置信息,整型数据row,col记录方块在数组中的位置信息,整型kind记录方块的种类信息,布尔型数据match为标记方块移动后是否满足消除条件,整型数据alpha用于记录方块的透明度。开始方块都为不满足消除条件且不透明,所以构造函数中match的初始值为false,alpha的初始值为255。
class Block
{public:
int x, y;//方块坐标值
int row, col;//在数组中第几行第几列
int kind;//方块种类
bool match;//是否周围有三个以上匹配方块
int alpha;//透明度
Block(){ //构造函数,数据初始化
match = false;
alpha = 255;
kind = -1;
}
}
方块的存放是放在类后设定的grid数组。方块的移动是先通过SFML库中的一个Mouse鼠标类来获取鼠标点击的位置,然后利用方块的大小计算出点击的方块在数组中的位置,即行和列,然后把两个对象的row,col数据互换,在check()函数中检查互换坐标后的方块与周围方块是否匹配。
如果匹配,则对匹配的方块的数据成员match加1。再到doMoving()函数把所点击的方块移动,移动后历遍数组把match值为1的方块的透明度逐步减一变为透明,然后进入updataGrid()更新函数,把match不为0的方块和同一列上方match为0的交换,再用随机函数把最上方match值不为0的方块更新为随机更新为新方块。
如果没有match为1的方块,则用huanyuan()还原函数把点击的方块的坐标再次交换,在下一次while循环的doMoving()函数中把方块还原回去。
玩家点击相邻两个方块会交换,如果交换后满足同种类方块三个或三个以上配对则方块消除,不匹配则还原所点击的两个方块。并且消除方块会获得相应分数,分数在界面下方。
点击开始游戏,玩家开始玩游戏,点击退出关闭窗口结束运行。
出现最终得分,点击退出结束游戏关闭窗口。
1.点击相邻两个方块后,方块无法互换
解决办法:doEvent()函数后包含交换条件的if语句移到记录第二次单击点击位置的if语句内。
Bug出现原因:在编码中出现了方块无法交换的问题,测试后发现在处理用户点击的函数中实现坐标交换后的click值对这个有影响,如果为零则无法交换,点击后的两个方块在不停的颤动但是不交换,如果为一,则方块可以完全交换但是又有新bug,点击了两个方块交换后,再次点击之前第一次点击的方块周围的方块就会立即交换,这是因为前一次交换后click值没清零,就一直标记着原方块为第一次点击方块,且都无法实现不匹配后还原的功能。
2.边界上出现两个素材中的第一个种类,即蓝色五角星时会相消
解决办法:把Block类中的数据成员kind初始值改为-1。
Bug出现原因:Block类中蓝色五角星的数据成员kind值为0,然后因为在定义数组时为防止方块溢出或者算法遗漏,所以数组各多了定义了两行两列,在挨近边界没有填充方块的对象中,数据成员都为初始化的0,与蓝色五角星的相等,所以会相消。
3.改变方块消失后更新方向
解决办法:需要改变更新函数updataGrid()中的for循环方向,然后颠倒所有循环中数组的行列,行变成列,列变成行(改变后的代码写在了源代码中)。
游戏运行后出现的是开始界面,有开始游戏和退出两个按键,点击开始游戏开始游玩,点击退出关闭游戏,开始游戏后,点击相邻两个方块即可交换,交换后如果方块满足匹配条件则可消除且获得得分,如果不满足则会还原,分数达到左下角设定的目标值,则通关,结束界面点击退出结束游戏。
注意事项:游戏需要的第三方库SFML的下载和配置可以参考以下两个网址
(11条消息) SFML+vs2019安装_Henry的博客-CSDN博客_sfml安装
SFML 2.5.1 (SFML / Download) (sfml-dev.org)
通过这次课设了解到了消除类游戏算法,把图形和数组结合起来,通过调整图片透明度处理图片消失动画。了解到了SFML库的一些基础用法。
SFML对游戏界面的处理和游戏按钮的处理太过麻烦,下次可以试试用其他工具写,而不用第三方库。系统设置还有些小bug,比如如果地图中没有可消除方块后会卡关,应该在check()函数中再多加一个for循环历遍数组检查无可消除方块则整个数组重新 随机加入方块。游戏设计方面还有所欠缺,参考市面上的消除类游戏,可以设计倒计时计分通关,或者设计必须消除满规定种类方块相应数量通关等很多玩法,目前设计的玩法太过简单且没有关卡选择,界面也不够美观。希望下次能做的更好。
附:系统的主要程序代码
class Block
{
public:
int x, y;//坐标值
int row, col;//第几行第几列
int kind;//方块种类
bool match;//是否成三
int alpha;//透明度
Block()//构造函数
{
match = false;
alpha = 255;
kind = -1;
}
}grid[ROWS_COUNT + 2][COLS_COUNT + 2];//左右多一行一列防止边界方块算法遗漏
void drawBlocks(Sprite* sprite, RenderWindow* window)
{
for (int i = 1; i <= ROWS_COUNT; i++)
{
for (int j = 1; j <= COLS_COUNT; j++)
{
Block p = grid[i][j];
sprite->setTextureRect(IntRect(p.kind * 50, 0, 50, 50));
//设置透明度
sprite->setColor(Color(255, 255, 255, p.alpha));
sprite->setPosition(p.x, p.y);
sprite->move(offset.x - ts, offset.y - ts);
window->draw(*sprite);
}
}
}
void drawfen(RenderWindow* window) {
Text text;
Font font;
font.loadFromFile("font/font.ttf");
String zfen ="Target:1000 Score:"+ std::to_string(fen);
text.setFont(font);
text.setString(zfen);
text.setFillColor(Color(255, 255, 255));
text.setCharacterSize(40);
text.setStyle(Text::Bold);
text.setPosition(40, 678);
window->draw(text);
}
//数组初始化
void initGrid()
{
for (int i = 1; i <= ROWS_COUNT; i++)
{
for (int j = 1; j <= COLS_COUNT; j++)
{
grid[i][j].kind = rand() % 7;
grid[i][j].col = j;
grid[i][j].row = i;
grid[i][j].x = j * ts;
grid[i][j].y = i * ts;
}
}
}
void swap(Block p1, Block p2)
{
std::swap(p1.col, p2.col);
std::swap(p1.row, p2.row);
grid[p1.row][p1.col] = p1;
grid[p2.row][p2.col] = p2;
}
void doEvent(RenderWindow* window)//判断用户点击为第一次还是第二次(点击两次后,两次点击的方块交换)
{
Event e;
while (window->pollEvent(e))
{
if (e.type == Event::Closed){
window->close();
}
if (e.type == Event::MouseButtonPressed){
if (e.key.code == Mouse::Left){
if (!isSwap && !isMoving)
click++;
pos = Mouse::getPosition(*window) - offset;
}
}
}
if (click == 1)//第一次单击记录点击位置{
posX1 = pos.x / ts + 1;
posY1 = pos.y / ts + 1;
}
if (click == 2)//第二次单击记录点击位置{
posX2 = pos.x / ts + 1;
posY2 = pos.y / ts + 1;
//判断是否相邻
if (abs(posX2 - posX1) + abs(posY2 - posY1) == 1){
//交换
swap(grid[posY1][posX1], grid[posY2][posX2]);
isSwap = true;
click = 0;
}
else{
click = 1;
}
}
}
void check()//检查是否匹配,或许可以把match记作分数
{
for (int i = 1; i <= ROWS_COUNT; i++) {
for (int j = 1; j <= COLS_COUNT; j++) {
if (grid[i][j].kind == grid[i + 1][j].kind && grid[i][j].kind == grid[i - 1][j].kind) {
grid[i - 1][j].match++;
grid[i][j].match++;
grid[i + 1][j].match++;
//for(int k=-1;k<=1;k++)grid[i+k][j].match++;
}
if (grid[i][j].kind == grid[i][j + 1].kind && grid[i][j].kind == grid[i][j - 1].kind) {
grid[i][j + 1].match++;
grid[i][j].match++;
grid[i][j - 1].match++;
//for(int k=-1;k<=1;k++)grid[i][j+k].match++;
}
fen = fen + grid[i][j].match;
}
}
}
void doMoving()
{
isMoving = false;
for (int i = 1; i <= ROWS_COUNT; i++) {
for (int j = 1; j <= COLS_COUNT; j++) {
Block& p = grid[i][j];
int dx, dy;
for (int k = 0; k < 4; k++) {
dx = p.x - p.col * ts;//计算标记坐标与实际坐标偏差
dy = p.y - p.row * ts;
if (dx){
p.x -= dx / abs(dx);//abs为取绝对值
}
if (dy){
p.y -= dy / abs(dy);
}
}
if (dx != 0 || dy != 0) {
isMoving = true;
}
}
}
}
void xiaochu()
{
for (int i = 1; i <= ROWS_COUNT; i++) {
for (int j = 1; j <= COLS_COUNT; j++) {
if (grid[i][j].match && grid[i][j].alpha > 10) {
grid[i][j].alpha -= 10;
isMoving = true;
}
}
}
}
void huanyuan()
{
if (isSwap && !isMoving) {
//如果不匹配,就还原
int score = 0;
for (int i = 1; i <= ROWS_COUNT; i++) {
for (int j = 1; j <= COLS_COUNT; j++) {
score += grid[i][j].match;
}
}
if (score == 0) {
swap(grid[posY1][posX1], grid[posY2][posX2]);
}
isSwap = false;
}
}
void updateGrid_down()//更新表格
{
for (int i = ROWS_COUNT; i > 0; i--) { //消掉之后往下更新
for (int j = 1; j <= COLS_COUNT; j++) {
if (grid[i][j].match) {
for (int k = i; k > 0; k--) {
if (grid[k][j].match == 0) {
swap(grid[k][j], grid[i][j]);
break;
}
}
}
}
}
for (int j = 1; j <= COLS_COUNT; j++) {
int n = 0;
for (int i = ROWS_COUNT; i > 0; i--) {
if (grid[i][j].match) {
grid[i][j].kind = rand() % 7;
grid[i][j].y = -ts * n;
n++;
grid[i][j].match = false;
grid[i][j].alpha = 255;
}
}
}
}
int main()
{
srand(time(0));
RenderWindow window(VideoMode(700, 800), "xiaoxiaole");
window.setFramerateLimit(60);
Texture t0, t1, t2, t3;
t0.loadFromFile("images/bg1.png");
t1.loadFromFile("images/bg2.png");
t2.loadFromFile("images/t4.png");
t3.loadFromFile("images/bg3.png");
Sprite spriteBegain(t0);//开始背景精灵
Sprite spriteBg(t1);//游戏背景精灵
Sprite spriteBlock(t2);//方块精灵
Sprite spriteEnd(t3);//结束背景精灵
initGrid();
while (window.isOpen())
{
//处理用户的点击事件
doEvent(&window);
window.draw(spriteBegain);
window.display();//draw配套使用
if (pos.x >= 100 && pos.x <= 450 && pos.y >= 315 && pos.y <= 426)
{
while (window.isOpen()){
doEvent(&window);
check();
doMoving();
if (!isMoving){
xiaochu();
}
huanyuan();
if (!isMoving){
updateGrid_down();
}
window.draw(spriteBg);
drawBlocks(&spriteBlock, &window);
drawfen(&window);
if (fen >= mubiao) {
window.draw(spriteEnd);
drawEndfen(&window);
if (pos.x >= 175 && pos.x <= 450 && pos.y >= 510 && pos.y <= 610) {
window.close();
}
}
window.display();
}
}
else if (pos.x >= 157 && pos.x <= 410 && pos.y >= 508 && pos.y <= 623) {
window.close();
}
}
return 0;
}