注意:该项目源码开源,在本文章最底下链接处获取,源码中附有大量注释。
游戏展示
单人模式
斗蛇模式
闯关模式
闯关模式总地图界面: 闯关模式介绍信息:
地图C: 地图D: 地图B:
地图A: 由于该游戏是有点难度的(作者无法到达后续地图),所以本文章只展现四张游戏地图,后面还有四张地图,感兴趣的小伙伴可以试试通关哦!
项目代码解析
一、整体框架结构
二、界面代码解析
1.主程序
窗口参数为宏定义
int main() {
initgraph(WIDTH, HEIGHT, SHOWCONSOLE);
welcomeToGame();
return 0;
}
2.主菜单
主菜单背景绘制
IMAGE img;
loadimage(&img, "./image/snake.jpeg", 1020, 770);
putimage(0, 0, &img);
状态码设置
按钮具体用哪一套、音乐播放哪一个都是根据界面的状态码 进行判断的,按钮点亮是根据按钮状态码 的改变而设置的
void welcomeToGame() {
button_event = -1;
ui_event = 1;
...
}
音乐播放
根据音乐的状态码播放,播放前要先关闭之前的音乐,并且一次打开须对应一次关闭
if (music_status == 1)
{
mciSendString("close ./music/win.mp3", NULL, 0, NULL);
mciSendString("close ./music/score.mp3", NULL, 0, NULL);
mciSendString("close ./music/button.mp3", NULL, 0, NULL);
mciSendString("open ./music/bk.mp3", 0, 0, 0);
mciSendString("play ./music/bk.mp3 repeat", 0, 0, 0);
}
按钮设置
将按钮的区域进行了封装,便与以后使用保证代码形式简洁。
页面文字设置: 1.文字背景设置透明,否则文字所在的矩形有个白底 2.居中显示
RECT R1 = { r[0][0],r[0][1],r[0][2],r[0][3] };
RECT R2 = { r[1][0],r[1][1],r[1][2],r[1][3] };
RECT R3 = { r[2][0],r[2][1],r[2][2],r[2][3] };
RECT R4 = { r[3][0],r[3][1],r[3][2],r[3][3] };
RECT R5 = { r[4][0],r[4][1],r[4][2],r[4][3] };
IMAGE img;
loadimage(&img, "./image/snake.jpeg", 1020, 770);
putimage(0, 0, &img);
setbkmode(TRANSPARENT);
settextstyle(40, 20, _T("方正粗黑宋简体"));
settextcolor(RGB(255, 128, 64));
drawtext("开始游戏", &R1, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
settextcolor(RGB(0, 162, 232));
drawtext("排行榜", &R2, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
settextcolor(RGB(63, 72, 204));
drawtext("游戏设置", &R3, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
settextcolor(RGB(255, 0, 255));
drawtext("帮助", &R4, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
settextcolor(RGB(255, 242, 0));
drawtext("退出游戏", &R5, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
setbkmode(TRANSPARENT);
settextcolor(RGB(185, 122, 87));
settextstyle(40, 20, _T("方正粗黑宋简体"));
outtextxy(418, 90, "蛇蛇大作战");
获取鼠标信息
鼠标信息的数据类型ExMessage 只要页面不跳转就会以制获取,死循环套上
ExMessage m;
while (1) {
m = getmessage();
...
}
鼠标移动信息:
buttonJudge(m.x, m.y) 对当前鼠标位置信息进行判断,返回鼠标所在哪一按钮区域或不在按钮上。 button_event 记录当前按钮的状态,点亮与否
第一个if 是判断鼠标是否在按钮上,若是则进行第二次if 判断,判断是否是和上一轮为同一个按钮,如果同一个按钮不能再次点亮了,因为再次点亮会导致熄灭。如果不是和上一轮循环的同一个按钮,那就将当前按钮点亮。
另外,一旦鼠标离开了按钮,同时当前有某个按钮亮着,则要把按钮再次点亮即熄灭掉。执行一轮之后,刷新鼠标信息,再次判断。
case WM_MOUSEMOVE:
setrop2(R2_XORPEN);
setlinecolor(LIGHTRED);
setlinestyle(PS_SOLID, 3);
setfillcolor(WHITE);
if (buttonJudge(m.x, m.y) != -1) {
if (button_event != buttonJudge(m.x, m.y)) {
button_event = buttonJudge(m.x, m.y);
fillrectangle(r[button_event][0], r[button_event][1], r[button_event][2], r[button_event][3]);
}
}
else if (button_event != -1) {
fillrectangle(r[button_event][0], r[button_event][1], r[button_event][2], r[button_event][3]);
button_event = -1;
}
flushmessage(EM_MOUSE);
break;
鼠标左键信息
按下瞬间,用异或(画了存活20s就死)方式画环。
setrop2(R2_NOTXORPEN);
for (int i = 0; i <= 10; i++)
{
setlinecolor(RGB(25 * i, 25 * i, 25 * i));
circle(m.x, m.y, 2 * i);
Sleep(20);
circle(m.x, m.y, 2 * i);
}
注意事项:
- 播放某音乐之前一定要
先关掉 要播放的音乐,确保音乐可播放,这里先关一次,然后打开音乐->播放音乐 - 根据鼠标左键点击的位置跳转到相应的
界面函数 没点到按钮就清空鼠标信息再来
关键:音乐只有在处于关闭状态或从未打开过的状态下去打开才能正确播放,如果仅仅是一次播放完了,再次打开是不会播放的。另外对于模式难度的设置就是把速度值直接进行更改
排行榜、帮助、设置、选择游戏界面:
思路基本上和主菜单一致
退出按钮:
会调用```exit()```函数退出程序
游戏结束页面:
基本上也和上边一样,主要不同在于保存分数的设计
闯关模式界面和闯关模式里的详细信息界面、界面设置也都与前面设计一样
三、游戏代码解析
涉及到的对象:蛇,坐标,食物,障碍物,地图。
- 蛇类:
这里用链表储存蛇的每一节的坐标
class Snake {
public:
Snake(int length,char direction = left, int player = 0, int grade = 0,bool is_dead=false)
:length(length),direction(direction), player(player), grade(grade),is_dead(is_dead) {};
char direction;
std::list<coor*> xy;
int length;
int player;
int grade;
int is_dead;
};
- 食物类:用二维数组来表示位置
- 墙壁类:用二维数组表示位置
- 地图类:记录当前地图的下一地图属性
- 坐标类:两个整型记录坐标
涉及操作:
- 初始化蛇
根据游戏模式状态码 ,对蛇初始位置及蛇长度进行初始化
void initSnake(Snake& s)
{
if (game_status == 1) {
s.length = S_LEN;
for (int i = 0; i < s.length; ++i) {
s.xy.push_back(new coor(p11[i][0], p11[i][1]));
}
}
else if (game_status == 3) {
s.length = S_LEN_C;
for (int i = 0; i < S_LEN_C; ++i) {
coor* c = new coor(p31[i][0], p31[i][1]);
s.xy.push_back(c);
}
}
}
void game2InitSnake(Snake& s1, Snake& s2) {
if (!s1.xy.empty()) {
s1.xy.clear();
}
if (!s2.xy.empty()) {
s2.xy.clear();
}
for (int i = 0; i < S_LEN; ++i) {
s1.xy.push_back(new coor(p21[i][0], p21[i][1]));
s2.xy.push_back(new coor(p22[i][0], p22[i][1]));
}
}
- 初始化食物
初始化食物坐标,并改变坐标状态。同时需要对坐标进行位置判断,如果是障碍物、蛇或食物需要重新生成。
void initFood(Snake& s, Item& m_item)
{
if (game_status == 1) {
srand((unsigned)time(NULL));
int x = (rand() % ((WIDTH-1) / 10)) * CUBE;
int y = (rand() % ((HEIGHT-1) / 10)) * CUBE;
for (auto it = s.xy.begin(); it != s.xy.end(); ++it) {
if (x == (*it)->x || y == (*it)->y || item.food[x][y] == 1 || x == 0 || x == 1020 || y == 0 || y == 770) {
it = s.xy.begin();
x = (rand() % ((WIDTH - 1) / 10)) * CUBE;
y = (rand() % ((HEIGHT - 1) / 10)) * CUBE;
}
}
m_item.food[x][y] = 1;
}
else if (game_status == 3) {
...
}
}
- 画蛇
蛇头和身体分开画,遍历就完事了
void initSnake(Snake& s)
{
if (game_status == 1) {
s.length = S_LEN;
for (int i = 0; i < s.length; ++i) {
s.xy.push_back(new coor(p11[i][0], p11[i][1]));
}
}
else if (game_status == 3) {
s.length = S_LEN_C;
for (int i = 0; i < S_LEN_C; ++i) {
coor* c = new coor(p31[i][0], p31[i][1]);
s.xy.push_back(c);
}
}
}
- 画食物
遍历整个地图,如果当前状态有食物就画矩形。
void printFood(Item& m_item) {
for (int i = 1; i < 1020; i++) {
for (int j = 1; j < 770; j++)
{
if (m_item.food[i][j] == 1) {
setfillcolor(YELLOW);
fillrectangle(i, j, i + CUBE, j + CUBE);
}
}
}
}
- 蛇移动
首先是由键盘按键改变蛇的方向,再由方向进行移动。同时需要对蛇的下一个位置是否有食物进行判断,若吃到食物则更新相应分数长度数据,并将蛇链表伸长。
void snakeMove(Snake& s, Item& m_item)
{
int x, y;
coor* head = new coor();
switch (s.direction) {
case up:
x = s.xy.front()->x;
y = s.xy.front()->y - CUBE;
if (m_item.food[x][y] == 1) {
head->x = x;
head->y = y;
s.xy.push_front(head);
m_item.food[x][y] = 0;
s.length++;
if(game_status!=2)
s.grade++;
}
else {
head->x = x;
head->y = y;
s.xy.emplace_front(head);
s.xy.pop_back();
}
break;
case down:
...
break;
case left:
...
break;
case right:
...
break;
}
}
-
死亡判断 超越边界、撞墙:坐标判断 沿着墙走的地图:坐标判断 吃到自己:蛇头对蛇身进行一一比对,遍历看坐标 -
终点判断 坐标判断,并对界面进行切换:利用地图类切换地图,改变当前地图状态码。定方向和初始位置。 -
食物判断有无 对地图进行遍历,然后看该位置食物的状态。
bool ifHaveFood(Item& m_item)
{
for (int i = 0; i < 1021; i++) {
for (int j = 0; j < 771; j++) {
if (m_item.food[i][j] == 1) {
return true;
}
}
}
return false;
}
- 斗蛇胜负判断
和死亡判断一样,有一个死后,本轮结束,对分数进行更新。 直到有一个人分数超过5,整个游戏结束。 - 地图绘制
封装画图函数,然后根据传入坐标 ,画出对应的矩形 ,圆形 ,斜线 。 同时画之后要把相应坐标下的状态更改,以便于死亡判断和食物生存。 然后将画的所有地图封装到一个函数里。
四、游戏模式流程
1、不同游戏模式要设置模式的状态码 ,以便于根据状态码进行初始化,画图等操作。因为把接口都给统一了,需要通过状态码进行判断。 2、主体就是while循环 加上sleep 和不断的界面刷新实现游戏运行也称之为GameLoop(游戏循环)
项目源码下载:
源码下载
|