第三章 游戏的框架(以贪吃蛇为例)
教学重难点:游戏的框架结构;理解一个概念,2D游戏、3D游戏、手游、页游、端游等不同类型及不同平台的不同游戏,它们的游戏框架万变不离其宗。
“第一节 游戏框架” 习题
1.游戏的框架由哪几部分构成?
2.游戏循环中的“绘制”、“输入”、“逻辑”3个模块执行完成的先后顺序如果发生调整,会有怎样的影响?
3.贪吃蛇的方向表示为什么采用枚举类型?它有什么优点?
4.本届代码中的舞台绘制,如果绘制出来的四道墙不能闭合,可能的原因是什么?
5.在贪吃蛇游戏中,游戏舞台的尺寸设定为高度height,宽度width,但本节案例的实际执行稍有偏差。以舞台的第i行为例,左墙的坐标为(0,i),右墙坐标为(width-1,i),实际的舞台宽度为width-2。该情况可能会为后续蛇的移动判定带来困扰。请尝试设计一种舞台的游戏数值表达方案,使得舞台左右墙体和贪吃蛇舞台坐标的表示,不会出现混乱。
游戏框架
游戏程序设计一般包含如下几个模块:
根据模块创建几个对应的函数:
首先进行游戏的初始化Initial,需要一个布尔变量判断游戏是否结束,然后创建一个游戏的循环体!gameOver,循环体中包含另外三个函数。另外,游戏的舞台尺寸也需要定义两个变量。因为从简单的贪吃蛇游戏入手,因此还需要定义这样几个变量。
然后对Initial函数进行完善
屏幕上的画面会动是因为每次都把屏幕清空然后绘制上新的内容,当新的内容与之前有变化就会觉得有东西在动,所以用system()清屏,贪吃蛇游戏的舞台由四道墙围成一个空间。先绘制上下两道墙,用#表示墙体,中间区域为空,空格键表示。
for (int i = 0; i < width; i++)
cout << "#";
cout << endl;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
cout << " ";
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width; i++)
cout << "#";
cout << endl;
运行会发现四道墙没有闭合,检查发现要添加if else做一下判断,现在游戏的舞台区域建立完毕。
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
else if (j == width - 1)
cout << "#";
else
cout << " ";
}
cout << endl;
}
需要思考一个问题,墙体在不在舞台上面?
如果墙体在舞台上面,那么蛇的坐标就不能出现在0和width-1上,这对于后面代码中上墙和下墙的头和尾要出状况,尤其是往舞台上放蛇(问题5)
但是发现蛇如果跑到(0,0)位置,就进入了墙体里面,因此我们对于墙体位置还要修改一下 再加入上节课学到的字体颜色的设置,将其运用起来 。
(很奇怪为啥又改回去了)(懂了!)
void Draw()
{
system("cls");
HANDLE h= GetStdHandle(STD_OUTPUT_HANDLE);
int textColer = 0XF3;
SetConsoleTextAttribute(h, textColer);
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
cout << " ";
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
}
然后把蛇和果子放进去(代码下方草稿一)
游戏模块
“第二节 游戏模块”习题
教学重难点
1.为什么本节输入模块中不采用直接侦测键盘硬件中断的GetAsyncKeyState()函数?本例中如果使用GetAsyncKetState()函数做键盘的输入接受,会有怎样的优弊?
2.贪吃蛇游戏的输入模块中采用switch()语句进行按键筛选,可能会存在怎样的弊端?
3.贪吃蛇游戏的输入模块,如果想采用键盘上的“←”、“→”、“↑”、“↓”左右上下四个方向键进行控制,代码需要如何改?如果“w”、“a”、“s”、“d”按键和方向键进行控制,又如何改?
4.全局变量的使用弊端请至少罗列三种。
5.游戏设计有哪4个组成要素?
6.贪吃蛇吃水果,在游戏画面上面如何表现或示意“水果”被吃没了? A。拿背景图将原来说过图案覆盖掉 B。把水果图案直接拿掉
7.在贪吃蛇游戏中,蛇每吃一个果子,蛇身会增长一节。为让游戏场景中的蛇身变长,应该在哪个函数里面实现比较合理 A。绘图函数,每增加一节蛇身绘图函数就多绘制一节;B。逻辑函数,每增加一节蛇身,逻辑函数就给蛇身链表增加一节蛇身数据。
8.如何让蛇身能够穿墙。即舌头进入右(上)墙,会从左(下)墙出来?条件应该怎样设定?
游戏输入
为完成输入部分,加入上一章学习的函数_kbhit()对键盘进行控制
接下来完善游戏的逻辑,可能会发现INPUT函数和Logic函数的内容可以并在一起写。但需要注意一下之前强调的框架的概念。
可以思考一下如果这个游戏还有鼠标或者其他输入设备,然后我们将两个函数并在一起,对于后续的一些行为是变简单还是复杂了
游戏逻辑
#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver;
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
enum eDirection {STOP=0,LEFT,RIGHT,UP,DOWN};
eDirection dir;
void Initial()
{
gameOver = false;
dir = STOP;
x = width / 2;
y = height / 2;
fruitX = rand() % width;
fruitY = rand() % height;//伪随机 后续扫雷那时间做随机种子
score = 0;
}
void Draw()
{
system("cls");
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
for (int i = 0; i < width+2; i++)
cout << "#";
cout << endl;
for (int i=0;i<height;i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
if (i == y && j == x)
{
int textColor = 0X8a;
SetConsoleTextAttribute(h, textColor);
cout << "0";
}
else if (i == fruitY && j == fruitX)
{
int textColor = 0X84;
SetConsoleTextAttribute(h, textColor);
cout << "F";
}
else
{
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
cout << " ";
}
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width+2; i++)
cout << "#";
cout << endl;
}
void Input()
{
/*if (_kbhit())
{
switch (_getch())
{
case 'a':
dir = LEFT;
break;
case 'd':
dir = RIGHT;
break;
case 'w':
dir = UP;
break;
case 's':
dir = DOWN;
break;
case 'x':
gameOver = true;
break;
default:
break;
}
}*/
}
void Logic()
{
/*switch (dir)
{
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
case UP:
y--;
break;
case DOWN:
y++;
break;
default:
break;
}*/
}
int main()
{
Initial();
while (!gameOver)
{
Draw();
Input();
Logic();
}
system("pause");
return 0;
}
贪吃蛇游戏的输入模块中采用switch()语句进行按键筛选,可能会存在怎样的弊端?
left和down同时按下的时候响应的是谁
游戏组成要素
我们运行程序能看到蛇头随着按键进行移动,我们再增加一个游戏结束的判定,这样当蛇头撞到墙壁时游戏就循环结束了
胜负判定
当蛇头遇到果子时,代码该如何完善
果子得分
在Draw里增加一个游戏分数的显示,测试看是否成功
下面完善蛇身部分,增加几个描述蛇身属性的变量
当蛇吃到果子的时候,蛇身长度要增加
贪吃蛇游戏的精华就在于蛇身用到链表结构,这里通过数组简单实现,就是将新增的蛇身坐标放在链表的最前面,原来的蛇身坐标依次往后挪,给人造成一种移动的感觉。其实就是把前面的数值存到后面去,挨个往后传。
下面我们把蛇身绘制出来
对代码段调整一下,让蛇头和蛇身的位置代码挨在一起。
为了方便测试,先把游戏结束代码注释掉。运行后会发现吃掉果子蛇身没有跟在蛇头后而是出现在屏幕左上角的同时同一行的墙体也出去了。
检查代码后做一些改进:
蛇身能跟着蛇头移动了 ,但墙体的问题仍然存在
在draw else代码里面,如果已经输出蛇身符号就不要再输出空格符号了
还有异常,此时再梳理一下代码,发现少了一对大括号
接下来是会穿墙的蛇:
再增加一个游戏结束的条件(当蛇头吃到蛇身时)
草稿一(仅有蛇头还没有蛇身)(可以吃果子显示得分)(蛇头移动减慢)
#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail;
enum eDirection { STOP = 0,LEFT,RIGHT,UP,DOWN};
eDirection dir;
void Initial()
{
gameOver = false;
dir = STOP;
x = width / 2;
y = height / 2;
fruitX = rand() % width;
fruitY = rand() % height;
score = 0;
}
void Draw()
{
system("cls");
HANDLE h= GetStdHandle(STD_OUTPUT_HANDLE);
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
if (i == y && j == x)
{
textColor = 0X8a;
SetConsoleTextAttribute(h, textColor);
cout << "0";
}
else if (i == fruitY && j == fruitX)
{
textColor = 0X84;
SetConsoleTextAttribute(h, textColor);
cout << "F";
}
else
{
textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
cout << " ";
}
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
cout << "游戏得分" << score << endl;
}
void Input()
{
if (_kbhit())
{
switch (_getch())
{
case 'a':
dir = LEFT;
break;
case 'd':
dir = RIGHT;
break;
case 'w':
dir = UP;
break;
case 's':
dir = DOWN;
break;
case 'x':
gameOver = true;
break;
default:
break;
}
}
}
void Logic()
{
switch (dir)
{
//case STOP:
//break; 为什么这两行不要
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
case UP: //向上居然是y--
y--;
break;
case DOWN:
y++;
break;
default:
break;
}
if (x>width||x<0||y>height||y<0) //蛇头遇到墙壁则停止
{
gameOver = true;
}
if ( x == fruitX && y==fruitY) //蛇头遇到果子
{
score += 10;
fruitX = rand() % width;
fruitY = rand() % height;
nTail++;
}
}
int main()
{
Initial();
while (!gameOver)
{
Draw();
Input();
Logic();
Sleep(100); // 100毫秒,可以根据需要调整速度
}
system("pause");
return 0;
}
链表 结构体 结点
草稿二(不仅有asdw还有上下左右键)(仅有蛇头还没有蛇身)(可以吃果子显示得分):
#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver;
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;
void Initial()
{
gameOver = false;
dir = STOP;
x = width / 2;
y = height / 2;
fruitX = rand() % width;
fruitY = rand() % height;
score = 0;
}
void Draw()
{
system("cls");
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
if (i == y && j == x)
{
int textColor = 0X8a;
SetConsoleTextAttribute(h, textColor);
cout << "0";
}
else if (i == fruitY && j == fruitX)
{
int textColor = 0X84;
SetConsoleTextAttribute(h, textColor);
cout << "F";
}
else
{
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
cout << " ";
}
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
}
void Input()
{
if (_kbhit())
{
int key = _getch();
switch (key)
{
case 75: // 左箭头键
case 'a':
dir = LEFT;
break;
case 77: // 右箭头键
case 'd':
dir = RIGHT;
break;
case 72: // 上箭头键
case 'w':
dir = UP;
break;
case 80: // 下箭头键
case 's':
dir = DOWN;
break;
case 'x':
gameOver = true;
break;
default:
break;
}
}
}
void Logic()
{
switch (dir)
{
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
case UP:
y--;
break;
case DOWN:
y++;
break;
default:
break;
}
}
int main()
{
Initial();
while (!gameOver)
{
Draw();
Input();
Logic();
}
system("pause");
return 0;
}
第三章完整代码
#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;
void Initial()
{
gameOver = false;
dir = STOP;
x = width / 2;
y = height / 2;
fruitX = rand() % width;
fruitY = rand() % height;
score = 0;
}
void Draw()
{
system("cls");
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
int textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
if (j == 0)
cout << "#";
if (i == fruitY && j == fruitX)
{
textColor = 0X84;
SetConsoleTextAttribute(h, textColor);
cout << "F";
}
else if (i == y && j == x)
{
textColor = 0X8a;
SetConsoleTextAttribute(h, textColor);
cout << "0";
}
else
{
bool flagPrint = false;
for (int k=0 ; k < nTail; k++)
{
if ((tailX[k] == j) && tailY[k] == i)
{
cout << "o";
flagPrint = true;
}
}
textColor = 0X86;
SetConsoleTextAttribute(h, textColor);
if(!flagPrint)
cout << " ";
}
if (j == width - 1)
cout << "#";
}
cout << endl;
}
for (int i = 0; i < width + 2; i++)
cout << "#";
cout << endl;
cout << "游戏得分" << score << endl;
}
void Input()
{
if (_kbhit())
{
switch (_getch())
{
case 'a':
dir = LEFT;
break;
case 'd':
dir = RIGHT;
break;
case 'w':
dir = UP;
break;
case 's':
dir = DOWN;
break;
case 'x':
gameOver = true;
break;
default:
break;
}
}
}
void Logic()
{
int prevX = tailX[0];
int prevY = tailY[0];
int prev2X, prev2Y;
tailX[0] = x;
tailY[0] = y;//把新增的蛇身坐标存放在0位
for (int i = 1; i < nTail; i++)
{
prev2X = tailX[i];
prev2Y = tailY[i];
tailX[i] = prevX;
tailY[i] = prevY;
prevX = prev2X;
prevY = prev2Y;
}
switch (dir)
{
//case STOP:
//break; 为什么这两行不要
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
case UP: //向上居然是y--
y--;
break;
case DOWN:
y++;
break;
default:
break;
}
//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
//{
// gameOver = true;
//}
for (int i=0; i < nTail;i++)
{
if (tailX[i] == x && tailY[i] == y)
gameOver == true;
}
if ( x >= width) x = 0; else if (x < 0)x = width - 1;
if (y >= height) y = 0; else if (y < 0)y = height - 1;
if (x == fruitX && y == fruitY) //蛇头遇到果子
{
score += 10;
fruitX = rand() % width;
fruitY = rand() % height;
nTail++;
}
}
int main()
{
Initial();
while (!gameOver)
{
Draw();
Input();
Logic();
Sleep(100); // 100毫秒,可以根据需要调整速度
}
system("pause");
return 0;
}