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 #define a 1 #define EXPLORESTATETIME 0.1 #define CountofCoins 6 #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和高度为128 6,前面介绍过这个内置函数。
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 () { 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 ); PlayerBlood = 10 ; 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.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]); } 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 void PutAlphaImg (int x, int y, IMAGE* src) { DWORD* pwin = GetImageBuffer(); DWORD* psrc = GetImageBuffer(src); int win_w = getwidth(); int win_h = getheight(); 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; if (x < 0 ) { psrc += -x; real_w -= -x; x = 0 ; } if (y < 0 ) { psrc += (src_w * -y); real_h -= -y; y = 0 ; } pwin += (win_w * y + x); for (int iy = 0 ; iy < real_h; iy++) { for (int ix = 0 ; ix < real_w; ix++) { byte t = (byte)(psrc[ix] >> 24 ); if (t > 100 ) { pwin[ix] = psrc[ix]; } } pwin += win_w; 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,j IMG_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 ; Enemy_x[origin_x] = 0 ; enemy[i].y = -Car_LENGTH; int x = rand() % 4 ; while (EnemynotVaild(x)) { x = rand() % 4 ; } Enemy_x[x] = 1 ; enemy[i].x = IMG_SIZE + (IMG_SIZE / 2 ) * x + 12 ; 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(); coins[i].x = IMG_SIZE + (IMG_SIZE / 2 ) * (rand() % 4 ) + 12 ; 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 ]; _stprintf_s(score_text, sizeof (score_text) / sizeof (wchar_t ), L"当前分数: %d" , PlayerScore * 10 ); settextcolor(WHITE); 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 ]; _stprintf_s(score_text, sizeof (score_text) / sizeof (wchar_t ), L"当前分数: %d" , maxScore * 10 ); settextcolor(WHITE); settextstyle(15 , 0 , _T("幼圆" )); setbkmode(TRANSPARENT); outtextxy(3 * IMG_SIZE + 5 , 90 , score_text); } void WriteBlood () { wchar_t score_text[20 ]; _stprintf_s(score_text, sizeof (score_text) / sizeof (wchar_t ), L"当前血量: %d" , PlayerBlood * 10 ); settextcolor(WHITE); 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); 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)) { if (player.y > 0 ) { player.y -= increasespeed; if (increasespeed < DefaultSpeed) { increasespeed++; } } else { player.y = 0 ; increasespeed = 0 ; } } if (GetAsyncKeyState(VK_DOWN) && CanDown) { if (player.y < getheight() - Car_LENGTH - 16 ) { player.y += DefaultSpeed + speed; } else { player.y = getheight() - Car_LENGTH - 16 ; } } if (GetAsyncKeyState(VK_LEFT)) { if (player.x > IMG_SIZE) { player.x -= DefaultSpeed; } else { player.x = IMG_SIZE; } } if (GetAsyncKeyState(VK_RIGHT)) { if (player.x < 3 * IMG_SIZE - Car_WEIGHT) { player.x += DefaultSpeed; } else { player.x = 3 * IMG_SIZE - Car_WEIGHT; } } if (GetAsyncKeyState(VK_SPACE)) { if (speed < MaxSpeed) { speed += a; } CanDown = 0 ; } else { if (speed > DefaultSpeed && !NitrogenState) { speed = DefaultSpeed; } CanDown = 1 ; } if (lookSpeed < (speed + increasespeed) * 20 ) { lookSpeed++; } else if (lookSpeed > (speed + increasespeed) * 20 ) { lookSpeed -= 2 ; } } void DrawEnemy () { for (int i = 0 ; i < CountofCars; i++) { DrawCar(enemy[i]); } } void MoveEnemy () { for (int i = 0 ; i < CountofCars; i++) { enemy[i].y += carsSpeed[enemy[i].id] + speed; if (enemy[i].y > getheight() - Car_LENGTH) { enemy[i].y = -Car_LENGTH; int origin_x = (enemy[i].x - 12 - IMG_SIZE) / (IMG_SIZE / 2 ); origin_x %= 4 ; Enemy_x[origin_x] = 0 ; int x = rand() % 4 ; while (EnemynotVaild(x)) { x = rand() % 4 ; } Enemy_x[x] = 1 ; enemy[i].x = IMG_SIZE + (IMG_SIZE / 2 ) * x + 12 ; enemy[i].id = rand() % 5 + 1 ; } } } void MoveCoins () { for (int i = CountofCoins - 1 ; i >= 0 ; i--) { coins[i].y += speed; if (coins[i].y >= getheight() - Car_WEIGHT) { coins[i].x = IMG_SIZE + (IMG_SIZE / 2 ) * (rand() % 4 ) + 12 ; 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 ); } }
如果时间差小于帧率间隔,则休眠,否则不休眠,这样就可以保证游戏的帧率稳定
项目讲解完毕,感谢观看