c语言学习-3

前两波我们学习完了c语言的基础语法,接下来我们用一个项目来深入理解所学知识。

项目地址:https://github.com/Sakjijdidji55/Racing-Car/tree/master

项目简介

本项目是一个赛车游戏,玩家通过控制赛车躲避障碍物,最终到达终点。

项目结构

项目包含以下文件:

(注:后缀名.cpp是因为项目需引入easyx库文件,.c无法使用这个文件,本项目语法均为c语言风格语法,笔者希望这个项目可以对你有所帮助)

  • main.cpp:主程序文件,只包含main函数。
  • Body.cpp: 程序主体,包含赛车、障碍物、背景等对象及其处理。
  • Car.cpp: 赛车对象及其处理。
  • Coin.cpp: 金币对象及其处理。
  • RoadLine.cpp: 路线对象及其处理。
  • Queue.cpp: 自实现循环队列。
  • image文件夹:存放游戏所需图片资源。
  • music文件夹:存放游戏所需音乐资源。

游戏原理

如何实现主角也就是我们自己操作的对象的移动效果

1, 路线以及游戏的各种资源向下移动
2, 赛车向上移动
(注:移动的原理就是相对坐标的改变)

碰撞检测

1, 赛车与金币碰撞检测
2, 赛车与障碍物碰撞检测
3, 赛车与终点碰撞检测
(注:碰撞检测的原理就是两个对象的相对坐标是否重合,重合则判定为碰撞)

游戏结束条件

1, 血量清零

easyx.h库与graphics.h库的使用

1
2
#include <graphics.h> 
#include <easyx.h>

首先导包

在这个项目我们用到了以下函数与变量类型

easyx.h库函数:

  • int putimage(int x, int y, IMAGE *pDstImg, int op); // 在指定位置绘制图像
  • void loadimage(IMAGE *pDstImg, LPCTSTR pImgFile, int nWidth = 0, int nHeight = 0, bool bResize = false); // 加载图像
  • void setlinestyle(int style, int thickness = 1, const DWORD *puserstyle = NULL, DWORD userstylecount = 0); // 设置线条样式
  • void setlinecolor(COLORREF color); // 设置线条颜色
  • void line(int x1, int y1, int x2, int y2); // 绘制线条
  • HWND initgraph(int width, int height, int flag = 0); // 初始化图形窗口
  • void BeginBatchDraw(); // 开始批量绘制
  • void FlushBatchDraw(); // 批量绘制
  • void EndBatchDraw(); // 结束批量绘制

graphics.h库函数:

  • bool MouseHit();
  • MOUSEMSG GetMouseMsg();

类型:

  • IMAGE // 图像类型
  • MOUSEMSG // 鼠标消息类型

具体函数作用后文会将以及自己根据函数名理会

项目实现

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include"Body.hpp"
// 定义帧间隔时间
int frameInterval = 1000 / FPS;
// 定义开始时间和结束时间
clock_t startTime, endTime;
// 设置游戏帧率
void setFPSonTheGame(int diff) {
if (diff < frameInterval) {
Sleep( frameInterval - diff );
}
}
int main() {
// 初始化游戏
initGame();
// 用户开始
UserStart();
// 开始游戏
StartGame();
while (true) {
// 记录开始时间
startTime = clock();
// 更新游戏
update();
// 判断游戏是否结束
isGameOver();
// 记录结束时间
endTime = clock();
// 计算时间差
int DiffofTime = endTime - startTime;
// 设置游戏帧率
setFPSonTheGame(DiffofTime);
}
// 关闭游戏
CloseGame();
return 0;
}

你看,我们的main.cpp文件就只有main函数,是不是很简单,这里就只是作为项目的入口

Body.hpp 与 Body.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#ifndef BODY_HPP
#define BODY_HPP

#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include"Car.hpp"
#include"RoadLine.hpp"
#include"Queue.hpp"
#include"Coin.hpp"
#include<conio.h>
#include<time.h>
#include <graphics.h>

// 定义图片大小
#define IMG_SIZE 128
// 定义行数
#define ROW 6
// 定义列数
#define COL 4
// 定义道路线数量
#define CountOfLines 18
// 定义最大速度
#define MaxSpeed 24
// 定义默认速度
#define DefaultSpeed 8
// 定义汽车数量
#define CountofCars 3
// 定义帧率
#define FPS 60
// 定义常量a
#define a 1
// 定义探索状态时间
#define EXPLORESTATETIME 0.1
// 定义金币数量
#define CountofCoins 6
// 引入winmm库
#pragma comment(lib, "winmm.lib")

// 初始化游戏
void initGame();
// 用户开始
void UserStart();
// 开始游戏
void StartGame();
// 更新游戏状态
void update();
// 判断游戏是否结束
void isGameOver();
// 关闭游戏
void CloseGame();
#endif
  • define 是宏定义,用于定义常量,换句话说,就是把原本没有意义的数字或者字符串赋予一个有意义的名字,提高代码可读性
  • pragma comment(lib, “winmm.lib”) 是引入winmm库,用于播放音乐

我们在Body.cpp中定义了许多变量如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include"Body.hpp"//引入头文件

int carsSpeed[TypeofCars] = { 0, 4, 6, 4, 6, 4 };
int speed = DefaultSpeed;
int NitrogenState = 0;
int Test[4][ROW * IMG_SIZE];
int PlayerBlood;
int PlayerScore;
int CanDown = 1;
int Enemy_x[4] = { 0 };
int UserState = 1;
int rushMusic = 0;
int lookSpeed = 0;
int increasespeed = 0;
int maxScore = 0;

Queue* ExplodeTime = initQueue();
IMAGE bk[2];
IMAGE explode, money, startgame_image, loser;
IMAGE one, two, three, four;
RoadLine lines[CountOfLines];
Car player;
Car enemy[CountofCars];
COLORREF transparentColor;
Coin coins[CountofCoins];
MOUSEMSG msg;

这些变量在后面的代码中会用到,这里就不一一介绍了

我们通过在Body.hpp中提供的函数接口,介绍Body.cpp中实现的这些函数,从而实现游戏功能

函数 void initGame()

1
2
3
4
5
6
7
8
9
10
11
void initGame() {
initgraph(IMG_SIZE * COL, IMG_SIZE * ROW);
srand(time(0));
LoadSource();
LoadMusic();
initLines();
initPlayer(0);
initEnemy();
initCoins();
BeginBatchDraw();
}
  • initgraph(IMG_SIZE * COL, IMG_SIZE * ROW) 是初始化图形窗口,参数为窗口宽度和高度,这里我们设置宽度1284和高度为1286,前面介绍过这个内置函数。
  • srand(time(0)) 是设置随机数种子,用于生成随机数,在游戏里随机化是相当重要的。
LoadSource() 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 加载资源函数
void LoadSource() {
// 将Test数组清零
memset(Test, 0, sizeof(Test));
// 加载背景
LoadBackGround();
// 加载汽车
LoadCars();
// 加载爆炸图片
loadimage(&explode, _T("./image/explore.png"), Car_WEIGHT + 20, Car_WEIGHT + 20);
// 加载金币图片
loadimage(&money, _T("./image/OIP-C.png"), Car_WEIGHT, Car_WEIGHT);
// 加载开始游戏图片
loadimage(&startgame_image, _T("./image/StartGame.png"), 16 * 20, 9 * 20);
// 加载失败图片
loadimage(&loser, _T("./image/lose.png"), 16 * 20, 9 * 20);
// 加载疯这个字的图片
loadimage(&one, _T("./image/1.png"), IMG_SIZE - 20, IMG_SIZE - 20);
// 加载数字狂这个字的图片
loadimage(&two, _T("./image/2.png"), IMG_SIZE - 20, IMG_SIZE - 20);
// 加载数字赛这个字的图片
loadimage(&three, _T("./image/3.png"), IMG_SIZE - 20, IMG_SIZE - 20);
// 加载数字车这个字的图片
loadimage(&four, _T("./image/4.png"), IMG_SIZE - 20, IMG_SIZE - 20);
// 设置玩家血量为10
PlayerBlood = 10;
// 设置玩家分数为0
PlayerScore = 0;
// 设置玩家是否可以下落
CanDown = 1;
}

第一个不用说,基础函数,你要自己练

LoadBackGround() 函数

1
2
3
4
5
// 加载背景
void LoadBackGround() {
loadimage(&bk[0], _T("./image/grass.png"), IMG_SIZE, IMG_SIZE);
loadimage(&bk[1], _T("./image/road.png"), IMG_SIZE, IMG_SIZE);
}

这里要注意_T(“./image/grass.png”),_T是强制转换字符串为宽字符,因为Windows.h中定义的函数都是宽字符,所以我们需要将字符串转换为宽字符,才能正确加载图片。后面的路径是相对路径,表示图片在当前目录下的image文件夹中。

LoadCars() 函数

1
2
3
4
5
6
7
void LoadCars() {
for (int i = 0; i < TypeofCars; i++) {
WCHAR path[20] = { 0 };
wsprintf(path, _T("./image/car%d.png"), i);
loadimage(&cars_img[i], path, Car_WEIGHT, Car_LENGTH);
}
}

WCHAR 是宽字符类型,wsprintf 是格式化字符串函数,path 是一个宽字符数组,用于存储图片路径,i 是循环变量,用于遍历所有汽车类型。
这里wsprintf(path, _T(“./image/car%d.png”), i); 事实上就是理解为printf把打印在控制台上的东西,打印到path这个数组中,_T(“./image/car%d.png”) 是格式化字符串,%d 是占位符,表示一个整数,i 是整数,表示汽车类型。

  • 后面的三个变量
  • PlayerBlood = 10;
  • // 设置玩家分数为0
  • PlayerScore = 0;
  • // 设置玩家是否可以下落
  • CanDown = 1;
    都是初始化变量,设置玩家血量为10,玩家分数为0,玩家是否可以下落为1。
LoadMusic() 函数
1
2
3
4
5
6
7
void LoadMusic() {
mciSendString(L"open music/bk.mp3 alias bgm", NULL, 0, NULL);
mciSendString(L"set bgm volume to 500", NULL, 0, NULL);
mciSendString(L"open music/begin.mp3 alias beg", NULL, 0, NULL);
mciSendString(L"open music/exp.mp3 alias exp", NULL, 0, NULL);
mciSendString(L"open music/eat.mp3 alias eat", NULL, 0, NULL);
}

mciSendString 是 Windows API 函数,用于发送命令给多媒体控制接口,这里我们使用它来播放音乐。可以把他理解成用c语言对Windows操作系统进行操作,比如打开文件,关闭文件,播放音乐,暂停音乐等等。

  • L"open music/bk.mp3 alias bgm" 是打开音乐文件 bk.mp3,并给它一个别名 bgm。
  • L"set bgm volume to 500" 是设置 bgm 的音量为 500。
  • L"open music/begin.mp3 alias beg" 是打开音乐文件 begin.mp3,并给它一个别名 beg。
    以此类推。
函数 void initLines()
1
2
3
4
5
6
7
8
9
10
11
12
void initLines() {
for (int i = 0; i < CountOfLines; i++) {
lines[i].len = 48;
if (i % 2 == 0) {
lines[i].x = IMG_SIZE + IMG_SIZE / 2;
}
else {
lines[i].x = IMG_SIZE * 2 + IMG_SIZE / 2;
}
lines[i].y = i * lines[i].len;
}
}

这里我们就需要介绍RoadLine.hpp里面定义的结构体了

1
2
3
4
5
6
7
8
#ifndef ROADLINE_HPP
#define ROADLINE_HPP
typedef struct {
int x;
int y;
int len;
}RoadLine;
#endif

这里我得补充,#ifndef #endif 是一个预处理指令,用于防止头文件被重复包含。如果头文件没有被包含过,那么就执行 #ifndef 和 #endif 之间的代码,否则就跳过。这是为了防止头文件被重复包含,导致编译错误。说不重要也重要,在这里提出,可以顺手写,不写项目就没必要

  • RoadLine 是一个结构体,表示一条道路线,包含三个成员变量:x,y,len。
  • x 是道路线的 x 坐标,y 是道路线的 y 坐标,len 是道路线的长度。
  • lines 是一个 RoadLine 数组,表示所有的道路线。
  • initLines() 函数用于初始化所有的道路线,将它们的位置和长度设置好。

所以在循环里面,lines[i].len = 48; 是设置道路线的长度为 48。另外两个if条件用以设置左右马路中间线,将马路设置成四车道

  • RoadLine lines[CountOfLines];

这里就存线,可以说是表示赛车速度,至于坐标的设置,在这个环境里面,坐标零点是左上角,向右为x轴正方向,向下为y轴正方向。用数学知识算出坐标

函数 void initPlayer()
1
2
3
4
void initPlayer(int id) {
int x = rand() % 2 + 1;
initCar(&player, x * IMG_SIZE + 12, getheight() - 64 - 25, id);
}
  • Car player;

这里,介绍Car.hpp里面定义的结构体及其方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef CAR_HPP
#define CAR_HPP
#include<easyx.h>
#define TypeofCars 6
#define Car_WEIGHT 40
#define Car_LENGTH 64

typedef struct {
int x;
int y;
int id;
}Car;

void initCar(Car* car, int x, int y, int id);
void DrawCar(Car car);
void ChangeCar();
void LoadCars();
void PutAlphaImg(int x, int y, IMAGE* src);

#endif

在Car.cpp中的实现

定义了一个贴图数组

1
IMAGE cars_img[TypeofCars];

车辆的类型就是由这个数组实现

  • Car 是一个结构体,表示一辆车,包含三个成员变量:x,y,id。
  • x 是车的 x 坐标,y 是车的 y 坐标,id 是车的类型。
  • initCar() 函数用于初始化一辆车,将它的位置和类型设置好。
  • DrawCar() 函数用于绘制一辆车,将它的位置和类型设置好。
  • ChangeCar() 没有用,之前设计的时候觉得可能有用就写上了
  • LoadCars() 函数用于加载所有的车,将它们的位置和类型设置好。
  • PutAlphaImg() 函数用于绘制一个带透明度的图片,将它的位置和类型设置好。

initCar() 函数用于初始化一辆车,将它的位置和类型设置好。

1
2
3
4
5
6
7
8
9
10
11
12
13
void initCar(Car* car, int x, int y, int id) {
car->x = x;
car->y = y;
car->id = id;
}

void DrawCar(Car car) {
PutAlphaImg(car.x, car.y, &cars_img[car.id]); //为什么没有用putimage函数?
}

void ChangeCar() {

}

LoadCars() 函数用于加载所有的车,将它们的位置和类型设置好。

1
2
3
4
5
6
7
void LoadCars() {
for (int i = 0; i < TypeofCars; i++) {
WCHAR path[20] = { 0 };
wsprintf(path, _T("./image/car%d.png"), i);
loadimage(&cars_img[i], path, Car_WEIGHT, Car_LENGTH);
}
}

PutAlphaImg() 函数用于绘制一个带透明度的图片,将它的位置和类型设置好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 将图像src放置在窗口的(x, y)位置,并使用alpha通道进行透明处理
void PutAlphaImg(int x, int y, IMAGE* src) {
// 获取窗口的图像缓冲区
DWORD* pwin = GetImageBuffer();
// 获取图像src的图像缓冲区
DWORD* psrc = GetImageBuffer(src);
// 获取窗口的宽度和高度
int win_w = getwidth();
int win_h = getheight();
// 获取图像src的宽度和高度
int src_w = src->getwidth();
int src_h = src->getheight();

// 计算实际需要绘制的图像宽度
int real_w = (x + src_w > win_w) ? win_w - x : src_w;
// 计算实际需要绘制的图像高度
int real_h = (x + src_h > win_h) ? win_h - x : src_h;
// 如果x坐标小于0,则将图像src的图像缓冲区指针向后移动x个像素
if (x < 0) {
psrc += -x;
real_w -= -x;
x = 0;
}
// 如果y坐标小于0,则将图像src的图像缓冲区指针向下移动y个像素
if (y < 0) {
psrc += (src_w * -y);
real_h -= -y;
y = 0;
}

// 将窗口的图像缓冲区指针移动到(x, y)位置
pwin += (win_w * y + x);

// 遍历图像src的每个像素
for (int iy = 0; iy < real_h; iy++) {
for (int ix = 0; ix < real_w; ix++) {
// 获取图像src的alpha通道值
byte t = (byte)(psrc[ix] >> 24);
// 如果alpha通道值大于100,则将图像src的像素绘制到窗口的图像缓冲区中
if (t > 100) {
pwin[ix] = psrc[ix];
}
}
// 将窗口的图像缓冲区指针向下移动一行
pwin += win_w;
// 将图像src的图像缓冲区指针向下移动一行
psrc += src_w;
}
}

看不懂没关系,咱会ctrl + c 与 ctrl + v,不会出门转专业去

所以我们的initPlayer()函数就是初始化赛车,将赛车放在马路中间,并且选择赛车类型,我将它固定为第一种

函数 void initEnemy()
1
2
3
4
5
6
7
8
9
10
11
12
void initEnemy() {
for (int i = 0; i < CountofCars; i++) {
int id = rand() % 5 + 1;
int x = rand() % 4;
int y = (rand() % getheight()) / 2;
while (EnemynotVaild(x)) {
x = rand() % 4;
}
Enemy_x[x] = 1;
initCar(enemy + i, IMG_SIZE + (IMG_SIZE / 2) * x + 12, y, id);
}
}

-rand()内置函数,随机生成一个0到RAND_MAX之间的随机数,RAND_MAX是一个常量,而%num就是自己决定范围
-getheight()函数,获取窗口的高度
-EnemynotVaild(x)函数,判断敌人是否有效,如果有效则返回1,否则返回0

EnemynotVaild(x)函数,判断敌人是否有效,如果有效则返回1,否则返回0

1
2
3
int EnemynotVaild(int x) {
return Enemy_x[x] == 1;
}

四条跑道,三个敌人,我们保证敌人不会出现在同一行,所以用数组来记录敌人是否有效,給玩家留下一条生路

void initCoins() 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
void initCoins() {
for (int i = 0; i < CountofCoins; i++) {
int x = rand() % 4;
int y = (rand() % getheight()) / 2;
while (NotVaild(x, y)) {
x = rand() % 4;
y = (rand() % getheight()) / 2;
}
coins[i].x = IMG_SIZE + (IMG_SIZE / 2) * x + 12;
coins[i].y = y;
Test[x][y] = 1;
}
}

NotVaild(x, y)函数,判断金币是否有效,如果有效则返回1,否则返回0

1
2
3
4
5
6
7
8
int NotVaild(int x, int y) {
for (int i = y - Car_LENGTH - 10; i < y + Car_LENGTH + 10; i++) {
if (Test[x][i]) {
return 1;
}
}
return 0;
}

注意Car_LENGTH,金币不能出现在车的周围,所以用Test数组来记录金币是否有效,給玩家留下一条生路,但在游戏中效果并不好,懒得优化了

-最后用BeginBatchDraw(),开始缓冲区,以避免闪屏

函数 void UserStart()

1
2
3
4
5
6
7
8
9
10
11
12
void UserStart() {
while (UserState) {
DrawBackGround();
PutAlphaImg(IMG_SIZE / 2 - 20, IMG_SIZE / 2 + 5, &one);
PutAlphaImg(IMG_SIZE / 2 - 30 + IMG_SIZE, IMG_SIZE / 4, &two);
PutAlphaImg(IMG_SIZE / 2 - 40 + IMG_SIZE * 2, IMG_SIZE / 4, &three);
PutAlphaImg(IMG_SIZE / 2 - 50 + IMG_SIZE * 3, IMG_SIZE / 2, &four);
PutAlphaImg(IMG_SIZE - 20, 2 * IMG_SIZE, &startgame_image);
FlushBatchDraw();
isStart();
}
}

-FlushBatchDraw()函数,将缓冲区的内容绘制到窗口上,以避免闪屏

这个函数的作用是,在游戏开始前的一个开始游戏菜单,因为是一个小东西,时间也不够,没有写太复杂

  • PutAlphaImg(IMG_SIZE / 2 - 20, IMG_SIZE / 2 + 5, &one);
  • PutAlphaImg(IMG_SIZE / 2 - 30 + IMG_SIZE, IMG_SIZE / 4, &two);
  • PutAlphaImg(IMG_SIZE / 2 - 40 + IMG_SIZE * 2, IMG_SIZE / 4, &three);
  • PutAlphaImg(IMG_SIZE / 2 - 50 + IMG_SIZE * 3, IMG_SIZE / 2, &four);
  • PutAlphaImg(IMG_SIZE - 20, 2 * IMG_SIZE, &startgame_image);

你看到的疯狂赛车与开始游戏,就是这些图片。当然,坐标还是靠算和试

  • isStart()函数,判断玩家是否点击了开始游戏按钮,如果点击了,则将UserState设置为0,表示游戏开始
1
2
3
4
5
6
7
8
void isStart() {
if (MouseHit()) {
msg = GetMouseMsg();
if (msg.uMsg == WM_LBUTTONDOWN && msg.x >= IMG_SIZE - 20 && msg.x <= IMG_SIZE + 300 && msg.y >= 2 * IMG_SIZE && msg.y <= 2 * IMG_SIZE + 180) {
UserState = 0; // 开始游戏
}
}
}

检测鼠标是否点击,如果点击了,则判断鼠标点击的位置是否在开始游戏按钮上,如果是,则将UserState设置为0,表示游戏开始,while循环结束

函数 void StartGame()

1
2
3
4
5
void StartGame() {
mciSendString(L"play beg from 0", NULL, 0, NULL); // 发车音效
Sleep(200);
StartMusic();
}
  • mciSendString(L"play beg from 0", NULL, 0, NULL); // 发车音效
  • Sleep(200); // 等待200毫秒,让玩家听到发车音效,并给玩家反应时间
  • StartMusic(); // 开始游戏音乐

StartMusic() 函数,开始游戏音乐

1
2
3
void StartMusic() {
mciSendString(L"play bgm repeat from 0", NULL, 0, NULL);
}

函数 void update()

游戏最重要的函数,每一帧都会调用这个函数,用来更新游戏状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void update() {
DrawBackGround();
DrawLines();
ChangeLines();

EnemysisCrash();
CoinsisCrash();

WriteScore();
WriteBlood();
WriteSpeed();
WriteMaxScore();

MovePlayer();
DrawPlayer();
MoveEnemy();
DrawEnemy();
MoveCoins();
DrawCoins();

IsShouldExplore();
FlushBatchDraw();
}
函数 void DrawBackGround()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void DrawBackGround() {
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
if (j == 0 || j == 3) {
putimage(j * IMG_SIZE, i * IMG_SIZE, bk);
}
else {
putimage(j * IMG_SIZE, i * IMG_SIZE, bk + 1);
}
}
}
//美化背景
setlinestyle(PS_SOLID, 5);
setlinecolor(BLACK);
line(IMG_SIZE, 0, IMG_SIZE, getheight());
line(IMG_SIZE * 3, 0, IMG_SIZE * 3, getheight());
setlinecolor(WHITE);
setlinestyle(PS_SOLID, 3);
line(IMG_SIZE * 2, 0, IMG_SIZE * 2, getheight());
}

按照地图绘制背景

  • if 的条件作用,两边草坪,中间是马路,坐标用IMG_SIZE来计算,iIMG_SIZE,jIMG_SIZE,来计算地图的坐标
  • 美化背景,用setlinestyle()函数设置线型,setlinecolor()函数设置颜色,line()函数绘制线
函数 void DrawLines()
1
2
3
4
5
void DrawLines() {
for (int i = 0; i < CountOfLines; i++) {
line(lines[i].x, lines[i].y, lines[i].x, lines[i].y + lines[i].len);
}
}

直接画

函数 void ChangeLines()
1
2
3
4
5
6
7
8
9
void ChangeLines() {
for (int i = CountOfLines - 1; i >= 0; i--) {
lines[i].y += speed;
if (lines[i].y >= getheight()) {
int index = (i + 1) % CountOfLines;
lines[i].y = lines[index].y - lines[i].len;
}
}
}

这里是我自己想的一个小算法,从后往前遍历,出界了就把最后一个的坐标赋值给它,然后循环

函数 void EnemysisCrash() 和 void CoinsisCrash()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 判断玩家是否与敌人碰撞
void EnemysisCrash() {
for (int i = 0; i < CountofCars; i++) {
// 判断玩家是否与敌人碰撞
if (isInThisPlace(player.x, player.y, enemy[i].x, enemy[i].y, enemy[i].x + Car_WEIGHT, enemy[i].y + Car_LENGTH) || isInThisPlace(player.x + Car_WEIGHT, player.y, enemy[i].x, enemy[i].y, enemy[i].x + Car_WEIGHT, enemy[i].y + Car_LENGTH) || isInThisPlace(player.x, player.y + Car_LENGTH, enemy[i].x, enemy[i].y, enemy[i].x + Car_WEIGHT, enemy[i].y + Car_LENGTH) || isInThisPlace(player.x + Car_WEIGHT, player.y + Car_LENGTH, enemy[i].x, enemy[i].y, enemy[i].x + Car_WEIGHT, enemy[i].y + Car_LENGTH)) {
// 播放碰撞音效
mciSendString(L"play exp from 0", NULL, 0, NULL);
// 计算敌人图片的原始位置
int origin_x = (enemy[i].x - 12 - IMG_SIZE) / (IMG_SIZE / 2);
origin_x %= 4;
// 将敌人图片位置置为0
Enemy_x[origin_x] = 0;
// 将敌人y坐标置为-Car_LENGTH
enemy[i].y = -Car_LENGTH;
// 随机生成一个敌人图片位置
int x = rand() % 4;
// 判断敌人图片位置是否有效
while (EnemynotVaild(x)) {
x = rand() % 4;
}
// 将敌人图片位置置为1
Enemy_x[x] = 1;
// 设置敌人x坐标
enemy[i].x = IMG_SIZE + (IMG_SIZE / 2) * x + 12;
// 设置敌人id
enemy[i].id = rand() % 5 + 1;
// 将爆炸时间压入栈中
push(ExplodeTime, time(0), player.x - 10, player.y - 10);
// 玩家血量减少
PlayerBlood--;
}
}
}

// 判断玩家是否与金币碰撞
void CoinsisCrash() {
for (int i = 0; i < CountofCoins; i++) {
// 判断玩家是否与金币碰撞
if (isInThisPlace(player.x, player.y, coins[i].x, coins[i].y, coins[i].x + Car_WEIGHT, coins[i].y + Car_WEIGHT) || isInThisPlace(player.x + Car_WEIGHT, player.y, coins[i].x, coins[i].y, coins[i].x + Car_WEIGHT, coins[i].y + Car_WEIGHT) || isInThisPlace(player.x, player.y + Car_LENGTH, coins[i].x, coins[i].y, coins[i].x + Car_WEIGHT, coins[i].y + Car_WEIGHT) || isInThisPlace(player.x + Car_WEIGHT, player.y + Car_LENGTH, coins[i].x, coins[i].y, coins[i].x + Car_WEIGHT, coins[i].y + Car_WEIGHT)) {
// 吃金币
EatCoins();
// 随机生成金币x坐标
coins[i].x = IMG_SIZE + (IMG_SIZE / 2) * (rand() % 4) + 12;
// 随机生成金币y坐标
coins[i].y = -rand() % Car_WEIGHT - Car_WEIGHT;
}
}
}

二者算法相同,都是判断是否碰撞,如果碰撞,播放音效,然后改变车的位置,并且把车的id置为0,然后重新生成车的位置,并且重新生成车的id,并且把ExplodeTime的栈顶元素出栈,并且把player的blood减一

  • isInThisPlace()函数用来判断是否碰撞,如果碰撞,返回1,否则返回0
  • EnemynotVaild()函数用来判断车的位置是否合法,如果合法,返回1,否则返回0
  • EatCoins()函数用来吃金币,播放音效,并且把player的score加一,并且把player的blood加一,并且把player的speed加一

注意游戏里面撞到敌人是会有特效的,实际上就是图片,我们要控制爆炸时间,用队列来控制,代码里面有注释

isInThisPlace()

1
2
3
int isInThisPlace(int x, int y, int checkx1, int checky1, int checkx2, int checky2) {
return x >= checkx1 && x <= checkx2 && y >= checky1 && y <= checky2;
}

EatCoins()

1
2
3
4
void EatCoins() {
mciSendString(L"play eat from 0", NULL, 0, NULL);
PlayerScore++;
}
函数 void WriteBlood(), void WriteScore(), void WriteSpeed(), void WriteMaxScore()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 写入当前分数
void WriteScore() {
wchar_t score_text[20];
// 将当前分数乘以10,并格式化为字符串
_stprintf_s(score_text, sizeof(score_text) / sizeof(wchar_t), L"当前分数: %d", PlayerScore * 10);
// 设置文本颜色为白色
settextcolor(WHITE);
// 设置文本样式为15号字体,字体为“幼圆”
settextstyle(15, 0, _T("幼圆"));
// 设置背景模式为透明
setbkmode(TRANSPARENT);
// 在指定位置输出文本
outtextxy(3 * IMG_SIZE + 5, 15, score_text);
}

// 写入最高分数
void WriteMaxScore() {
// 更新最高分数
maxScore = max(maxScore, PlayerScore);
wchar_t score_text[20];
// 将最高分数乘以10,并格式化为字符串
_stprintf_s(score_text, sizeof(score_text) / sizeof(wchar_t), L"当前分数: %d", maxScore * 10);
// 设置文本颜色为白色
settextcolor(WHITE);
// 设置文本样式为15号字体,字体为“幼圆”
settextstyle(15, 0, _T("幼圆"));
// 设置背景模式为透明
setbkmode(TRANSPARENT);
// 在指定位置输出文本
outtextxy(3 * IMG_SIZE + 5, 90, score_text);
}

// 写入当前血量
void WriteBlood() {
wchar_t score_text[20];
// 将当前血量乘以10,并格式化为字符串
_stprintf_s(score_text, sizeof(score_text) / sizeof(wchar_t), L"当前血量: %d", PlayerBlood * 10);
// 设置文本颜色为白色
settextcolor(WHITE);
// 设置文本样式为15号字体,字体为“幼圆”
settextstyle(15, 0, _T("幼圆"));
// 设置背景模式为透明
setbkmode(TRANSPARENT);
// 在指定位置输出文本
outtextxy(3 * IMG_SIZE + 5, 40, score_text);
}

// 写入当前速度
void WriteSpeed() {
wchar_t Speed_text[20];
// 将当前速度格式化为字符串
_stprintf_s(Speed_text, sizeof(Speed_text) / sizeof(wchar_t), L"当前速度: %d", lookSpeed);
// 设置文本颜色为白色
settextcolor(WHITE);
// 设置文本样式为15号字体,字体为“幼圆”
settextstyle(15, 0, _T("幼圆"));
// 设置背景模式为透明
setbkmode(TRANSPARENT);
// 在指定位置输出文本
outtextxy(3 * IMG_SIZE + 5, 65, Speed_text);
}
  • WriteScore()函数用来写分数,把分数乘以10,然后转换为字符串,然后输出到屏幕上
  • WriteMaxScore()函数用来写最高分,把最高分乘以10,然后转换为字符串,然后输出到屏幕上
  • WriteBlood()函数用来写血量,把血量乘以10,然后转换为字符串,然后输出到屏幕上
  • WriteSpeed()函数用来写速度,把速度转换为字符串,然后输出到屏幕上
  • settextcolor()函数用来设置文本颜色
  • settextstyle()函数用来设置文本样式
  • setbkmode()函数用来设置背景模式
  • outtextxy()函数用来输出文本到屏幕上
  • _stprintf_s()函数用来格式化字符串,并且输入到text文本中
Move与Draw函数三件套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// 绘制玩家
void DrawPlayer() {
DrawCar(player);
}

// 移动玩家
void MovePlayer() {
// 如果按下向上键
if (GetAsyncKeyState(VK_UP)) {
// 如果玩家y坐标大于0
if (player.y > 0) {
// 玩家y坐标减少increasespeed
player.y -= increasespeed;
// 如果increasespeed小于默认速度
if (increasespeed < DefaultSpeed) {
// increasespeed增加
increasespeed++;
}
}
// 否则
else {
// 玩家y坐标为0
player.y = 0;
// increasespeed为0
increasespeed = 0;
}
}
// 如果按下向下键且CanDown为真
if (GetAsyncKeyState(VK_DOWN) && CanDown) {
// 如果玩家y坐标小于getheight() - Car_LENGTH - 16
if (player.y < getheight() - Car_LENGTH - 16) {
// 玩家y坐标增加DefaultSpeed + speed
player.y += DefaultSpeed + speed;
}
// 否则
else {
// 玩家y坐标为getheight() - Car_LENGTH - 16
player.y = getheight() - Car_LENGTH - 16;
}
}
// 如果按下向左键
if (GetAsyncKeyState(VK_LEFT)) {
// 如果玩家x坐标大于IMG_SIZE
if (player.x > IMG_SIZE) {
// 玩家x坐标减少DefaultSpeed
player.x -= DefaultSpeed;
}
// 否则
else {
// 玩家x坐标为IMG_SIZE
player.x = IMG_SIZE;
}
}
// 如果按下向右键
if (GetAsyncKeyState(VK_RIGHT)) {
// 如果玩家x坐标小于3 * IMG_SIZE - Car_WEIGHT
if (player.x < 3 * IMG_SIZE - Car_WEIGHT) {
// 玩家x坐标增加DefaultSpeed
player.x += DefaultSpeed;
}
// 否则
else {
// 玩家x坐标为3 * IMG_SIZE - Car_WEIGHT
player.x = 3 * IMG_SIZE - Car_WEIGHT;
}
}
// 如果按下空格键
if (GetAsyncKeyState(VK_SPACE)) {
// 如果速度小于最大速度
if (speed < MaxSpeed) {
// 速度增加a
speed += a;
}
// CanDown为假
CanDown = 0;
}
// 否则
else {
// 如果速度大于默认速度且NitrogenState为假
if (speed > DefaultSpeed && !NitrogenState) {
// 速度为默认速度
speed = DefaultSpeed;
}

// CanDown为真
CanDown = 1;
}

// 如果lookSpeed小于(speed + increasespeed) * 20
if (lookSpeed < (speed + increasespeed) * 20) {
// lookSpeed增加
lookSpeed++;
}
// 否则如果lookSpeed大于(speed + increasespeed) * 20
else if (lookSpeed > (speed + increasespeed) * 20) {
// lookSpeed减少2
lookSpeed -= 2;
}

}

// 绘制敌人
void DrawEnemy() {
// 遍历敌人数量
for (int i = 0; i < CountofCars; i++) {
// 绘制敌人
DrawCar(enemy[i]);
}
}

// 移动敌人
void MoveEnemy() {
// 遍历敌人数量
for (int i = 0; i < CountofCars; i++) {
// 敌人y坐标增加carsSpeed[enemy[i].id] + speed
enemy[i].y += carsSpeed[enemy[i].id] + speed;
// 如果敌人y坐标大于getheight() - Car_LENGTH
if (enemy[i].y > getheight() - Car_LENGTH) {
// 敌人y坐标为-Car_LENGTH
enemy[i].y = -Car_LENGTH;
// 计算敌人x坐标的原始位置
int origin_x = (enemy[i].x - 12 - IMG_SIZE) / (IMG_SIZE / 2);
origin_x %= 4;
// 将敌人x坐标的原始位置设为0
Enemy_x[origin_x] = 0;
// 生成随机数x
int x = rand() % 4;
// 如果x无效
while (EnemynotVaild(x)) {
// 生成新的随机数x
x = rand() % 4;
}
// 将敌人x坐标的位置设为1
Enemy_x[x] = 1;
// 敌人x坐标为IMG_SIZE + (IMG_SIZE / 2) * x + 12
enemy[i].x = IMG_SIZE + (IMG_SIZE / 2) * x + 12;
// 敌人id为随机数
enemy[i].id = rand() % 5 + 1;
}
}
}

// 移动金币
void MoveCoins() {
// 遍历金币数量
for (int i = CountofCoins - 1; i >= 0; i--) {
// 金币y坐标增加speed
coins[i].y += speed;
// 如果金币y坐标大于getheight() - Car_WEIGHT
if (coins[i].y >= getheight() - Car_WEIGHT) {
// 金币x坐标为IMG_SIZE + (IMG_SIZE / 2) * (rand() % 4) + 12
coins[i].x = IMG_SIZE + (IMG_SIZE / 2) * (rand() % 4) + 12;
// 金币y坐标为-rand() % Car_WEIGHT - Car_WEIGHT
coins[i].y = -rand() % Car_WEIGHT - Car_WEIGHT;
}
}
}

// 绘制金币
void DrawCoins() {
// 遍历金币数量
for (int i = 0; i < CountofCoins; i++) {
// 绘制金币
PutAlphaImg(coins[i].x, coins[i].y, &money);
}
}

这里是很主要的逻辑,但是很简单易懂,就是根据按键来移动玩家,然后根据玩家的移动来移动敌人,金币,然后绘制出来。不过多赘述,注释很清楚

void IsShouldExplore()
1
2
3
4
5
6
7
8
9
10
11
void IsShouldExplore() {
if (isEmpty(ExplodeTime)) {
return;
}
for (int i = ExplodeTime->front; i != ExplodeTime->end; i = (i + 1) % MAX_SIZE) {
if (time(0) - ExplodeTime->data[i] >= EXPLORESTATETIME) {
pop_front(ExplodeTime);
}
EXPlODE(ExplodeTime->x[i], ExplodeTime->y[i]);
}
}

这里是Queue.hpp中的函数,用于判断是否应该爆炸,如果队列不为空,则遍历队列,如果当前时间减去队列中的时间大于等于爆炸时间,则将队列中的元素弹出,然后调用EXPlODE函数,这个函数在Game.hpp中定义,用于绘制爆炸效果,这里就不多赘述了。

Queue.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef QUEUE_HPP
#define QUEUE_HPP
#define MAX_SIZE 10

typedef struct {
int front;
int end;
int* data;
int* x;
int* y;
} Queue;

Queue* initQueue();

int isEmpty(Queue* obj);

void push(Queue* obj, int val,int x,int y);

void pop_front(Queue* obj);

#endif

Queue.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "Queue.hpp"
#include<stdlib.h>

Queue* initQueue() {
Queue* obj = (Queue*)malloc(sizeof(Queue));
obj->end = 0;
obj->front = 0;
obj->data = (int*)malloc(MAX_SIZE * sizeof(int));
obj->x = (int*)malloc(MAX_SIZE * sizeof(int));
obj->y = (int*)malloc(MAX_SIZE * sizeof(int));;
return obj;
}

int isEmpty(Queue* obj) {
return obj->end == obj->front;
}

void push(Queue* obj, int val,int x,int y) {
obj->x[obj->end] = x;
obj->y[obj->end] = y;
obj->data[(obj->end)++] = val;
obj->end %= MAX_SIZE;
}

void pop_front(Queue* obj) {
obj->front++;
obj->front %= MAX_SIZE;
}

栈,队列等基础算法实现本节不讲,后面将

EXPlODE函数

1
2
3
void EXPlODE(int x, int y) {
PutAlphaImg(x, y, &explode);
}
  • FlushBatchDraw()函数 前面介绍过这个内置函数

函数 void isGameOver()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void isGameOver() {
if (PlayerBlood <= 0) {
UserState = 1;
CloseMusic();
while (UserState) {
DrawBackGround();
WriteScore();
WriteBlood();
WriteSpeed();
WriteMaxScore();
DrawCoins();
DrawPlayer();
DrawEnemy();
PutAlphaImg(IMG_SIZE - 20, IMG_SIZE - 20, &loser);
PutAlphaImg(IMG_SIZE - 20, 2 * IMG_SIZE, &startgame_image);
FlushBatchDraw();
isStart();
}
PlayerBlood = 10;
PlayerScore = 0;
CanDown = 1;
lookSpeed = 0;
increasespeed = 0;
player.x = (rand() % 2 + 1) * IMG_SIZE + 12;
player.y = getheight() - 64 - 25;
Sleep(500);
StartGame();
}
}

函数虽长,但是实际上就是之前的函数的复用,重置玩家状态,然后重新开始游戏

函数 void CloseGame()

1
2
3
4
5
6
void CloseGame() {
CloseMusic();
EndBatchDraw();
free(ExplodeTime->data);
free(ExplodeTime);
}

CloseMusic()函数

1
2
3
4
void CloseMusic() {
mciSendString(L"close bgm", NULL, 0, NULL);
mciSendString(L"open music/bk.mp3 alias bgm", NULL, 0, NULL);
}

关闭并且释放所有内存

项目里面Body的接口函数即实现都讲完,最后就是主函数控制帧率原理

1
2
3
4
5
6
7
8
9
10
11
12
13
while (true) {
startTime = clock();

update();

isGameOver();

endTime = clock();

int DiffofTime = endTime - startTime;

setFPSonTheGame(DiffofTime);
}
  • startTime = clock(); 获取当前时间
  • update(); 更新游戏状态
  • isGameOver(); 判断游戏是否结束
  • endTime = clock(); 获取当前时间
  • int DiffofTime = endTime - startTime; 计算时间差
  • setFPSonTheGame(DiffofTime); 设置帧率

setFPSonTheGame函数

1
2
3
4
5
void setFPSonTheGame(int diff) {
if (diff < frameInterval) {
Sleep( frameInterval - diff );
}
}

如果时间差小于帧率间隔,则休眠,否则不休眠,这样就可以保证游戏的帧率稳定

项目讲解完毕,感谢观看