学校怎么创建网站,产品销售类网站 模板,国外市场网站推广公司,成都的装修公司有哪些C游戏开发 文章目录 C游戏开发[TOC](文章目录) 前言一、逻辑分析1.1地图实现1.2人物的移动1.2.1小人移动1.2.2其他移动 1.3墙壁的碰撞1.4箱子的推动1.4.1什么时候推箱子1.4.2什么情况可以推箱子 1.5胜利的判断1.6卡关的处理1.7关卡的切换 二、DEMO代码2.1游戏框架2.2各功能函数…C游戏开发 文章目录 C游戏开发[TOC](文章目录) 前言一、逻辑分析1.1地图实现1.2人物的移动1.2.1小人移动1.2.2其他移动 1.3墙壁的碰撞1.4箱子的推动1.4.1什么时候推箱子1.4.2什么情况可以推箱子 1.5胜利的判断1.6卡关的处理1.7关卡的切换 二、DEMO代码2.1游戏框架2.2各功能函数的实现Init()Paint()Run()Close() 2.3额外添加的函数Move(char key)Check()main() 三、完整源代码四、总结
前言
推箱子为本系列的第一篇我个人认为这是游戏开发中最基础最简单的部分所以放在开篇程序有些入门适合初学者阅读。对于程序方面如果大家有更好的方案欢迎在评论区留言。那么首先为大家介绍一下推箱子的游戏规则 如上图所示为某一关推箱子的地图画面 其中黄色的圆点为目标点带有叉的方块是箱子玩家通过方向键控制小人移动在移动过程中如果遇到箱子可以推动箱子但不能拉。如下情况则不可以推动箱子 1.箱子遇到墙壁 2.两个箱子重叠推动两个箱子 如果将所有的箱子都放置在了目标点处则游戏获得胜利过关。 一、逻辑分析
所有的游戏开发其逻辑都基于游戏的规则因此完整的游戏规则是必不可少的。其中最重要的规则无外乎获胜条件、失败条件、积分规则等。 对于推箱子而言获胜规则显而易见是将所有的箱子都放置在目标点处。而本游戏并没有失败条件但是考虑到箱子可能会被推到墙壁处而不能通关出现卡关情况因此对于卡关也要做出处理。 本游戏中没有积分规则但是会有不同的关卡设计但其底层玩法包括人物的移动箱子的推动胜利的判断是没有区别的因此我们可以在不同的关卡中只更换游戏地图来实现关卡切换。 综上要实现的部分有
地图的实现人物的移动墙壁的碰撞箱子的推动胜利的判断卡关的处理关卡的切换
1.1地图实现
对于推箱子地图的实现我们可以用字符简单的绘制一下如下 ### # # # # #### ####### ###### ###### # # # ### 在上图中使用了字符#来模拟地图的墙壁当然只有地图是远远不够的还要有小人箱子目标点那么使用字符H来模拟小人使用字符O来模拟箱子字符*来模拟目标点得到如下的图 ### #*# #O# #### #######*O H O *######O###### # #*# ### 有了字符组成的地图那么显然我们可以使用C的数据结构二维数组来存储上述字符从而将其输出到命令行显示
//使用二维数组定义地图
char p_map[16][16] { , , ### , #*# , #O# , #### ###### , #*O H O *# , #####O##### , # # , #*# , ### , , , , , };void showMap(){//显示地图for(int i 0; i 16; i ){puts(p_map[i]);}
}由此我们得到了简易的地图显示。
1.2人物的移动
在讲到人物移动的逻辑之前我们先来聊一下视频动画是如何形成的。 如下图是一个摇头的向日葵 我们之所以能看到这个向日葵在摇摆是由下面一系列图片刷新显示当图片连续的刷新显示使人眼产生一种向日葵动起来的错觉这也是视频播放的原理。 每显示一张图片我们称作一帧而每秒钟刷新的次数显示的帧数称为帧率帧率越高动态效果就越真实。 而游戏肯定离不开动态因此几乎所有的游戏都要刷新的去显示形成所谓的动态效果。我们可以通过一个程序来实现一个简单的动画效果。
1.2.1小人移动
我们在命令行上输出一个小人
printf(O\nI\nH\n);如下 OIH若想让小人跑起来即向右移动 我们可以在字符OIH前分别加入一个空格使其右移一格 OIH若想连续的跑动则将程序放入循环中依次在每个字符左边多加入一个空格即可但是这样会出现以下的问题
for(int i 0; i 20 ; i){for(int j 0; j i; j ){//加入i个空格printf( );}printf( O\n);for(int j 0; j i; j ){printf( );}printf( I\n);for(int j 0; j i; j ){printf( );}printf( H\n);}小人没有像我们预期的一样跑动而是成了一条斜线。 这是因为我们只顾着去显示忘记了刷新屏幕上已显示过的小人因此我们需要每次将小人显示前清除上一次的显示结果从而实现刷新显示。 使用如下程序
system(cls);是C清空命令框的指令使用时需加上stdlib.h的头文件但此时运行会发行小人跑的太快了这也不符合我们的预期因此可以改变小人的刷新率帧率 使用如下程序
Sleep(n);其作用为让进程休眠n毫秒我们这里n取1000即帧率为1虽然很低但是为了看清小人移动的过程,使用时需加上windows.h的头文件
for(int i 0; i 20 ; i){system(cls);for(int j 0; j i; j ){//加入i个空格printf( );}printf( O\n);for(int j 0; j i; j ){printf( );}printf( I\n);for(int j 0; j i; j ){printf( );}printf( H\n);Sleep(1000);}这时就可以看到小人一步一步的移动啦
1.2.2其他移动
综上要显示移动效果需要如下步骤 1.画面的清除 2.新画面的显示 3.时间的控制用于控制刷新率、移动的快慢等 因此我们可以将以上封装为画面显示的函数paint()在每次画面更新的逻辑处理完后调用即可。
在推箱子中小人的移动就很简单了我们基于其在二维数组中的坐标x,y每次使用键盘输入后更改x,y的值并在地图中更新然后调用paint()函数即可。
char p_map[16][16] { , , , , , H , , , , , , , , , , };在上图中H的坐标为65我们将地图的打印、刷新、刷新率封装为一个函数
void paint(){system(cls);//清空上一次图片for(int i 0; i 16; i){//更新地图打印puts(p_map[i]);}Sleep(10);//10毫秒刷新
}使用getch()函数来获取键盘的输入该函数需要conio.h头文件 在循环中判断输入的键,对于不同方向做相应的处理更新小人坐标和地图
#includeiostream
#includestdio.h
#includestdlib.h
#includewindows.h
#includeconio.hvoid paint(){system(cls);//清空上一次图片for(int i 0; i 16; i){//更新地图打印puts(p_map[i]);}Sleep(10);//10毫秒刷新
}int main(){//记录坐标int x 6;int y 5;while(1){paint();char z getch();if(z w){//上p_map[y][x] ;//之前的位置更新为空格p_map[y-1][x] H;//移动后的位置变为小人后面同理y--;}else if(z a){//左p_map[y][x] ;//之前的位置更新为空格p_map[y][x-1] H;x--;}else if(z s){//下p_map[y][x] ;//之前的位置更新为空格p_map[y1][x] H;y;}else if(z d){//右p_map[y][x] ;//之前的位置更新为空格p_map[y][x1] H;x;}}system(pause);return 0;
}由此即实现了小人的移动但是由于没有边界限制条件因此当小人走出了数组的界限会因数组越界报错所以还需做更详细的处理。
1.3墙壁的碰撞
墙壁碰撞的逻辑就很简单了只要我们判断移动后的位置不是表示墙壁的字符即可如
if(z w p_map[y-1][x]!#){//不为墙壁即可向上移动p_map[y][x] ;//移动前的位置更新y--;//坐标更新p_map[y][x] H;//地图更新
}1.4箱子的推动
箱子的推动相比于小人移动的逻辑要复杂一点我们要明确两点 1.什么时候推箱子 2.什么情况可以推箱子
1.4.1什么时候推箱子
当小人移动后的位置如果为箱子的话此刻判断为推箱子如下四种情况
HO OH O HH O
1.4.2什么情况可以推箱子
当箱子移动后的位置如果是空位则可以推箱子不能撞墙不能重叠推两个及以上箱子 因此我们得到如下代码
if(z w p_map[y-1][x]O p_map[y-2][x] ){//移动的下一位为箱子箱子移动的下一位为空位p_map[y][x] ;//移动前的位置更新y--;//坐标更新p_map[y][x] H;//地图更新p_map[y-1][x] O;//更新箱子
}1.5胜利的判断
当所有箱子都处于目标点时即获得游戏胜利因此我们需要遍历所有的目标点这时要考虑对于不同的关卡目标点的个数也是不同的因此我们考虑两种解决办法 1.定义数组p_win[DEF_LENGTH],DEF_LENGTH要尽量大一些保证所有关卡的目标点数不超过它。 2.使用STL中的vector 上述两种方法皆可但如果使用方法1在传参时除了要传入数组指针还要传入目标点个数为了减少传参我们直接使用vector作为容器。
1.6卡关的处理
当箱子处于下面的情况时箱子将无法被推动到任何其他位置也没有处于目标点处这时既不会宣布游戏胜利也不会宣布游戏失败即出现了卡关。为了解决这一问题我们可以加入重置关卡的功能比如当我们按下R键所有箱子和小人的位置就会复原到初始位置这便解决了卡关的问题。
#######
#O H *#
#######1.7关卡的切换
为了方便关卡的切换我们将游戏的运行封装在一个函数中而每一关我们只需传入关卡地图、判断目标点的容器、小人起始坐标三个变量即可。这样每当通关后就自动切换到下一关。
以上就是推箱子游戏的基本逻辑下面我们来做出推箱子的简单DEMO
二、DEMO代码
2.1游戏框架
一般的游戏运行过程大致分为一下几步 1.游戏初始化 2.游戏运行 3.游戏画面显示 4.游戏结束 那么我们先简单定义一个游戏类的接口以便规范今后我们再去开发其他游戏。 定义抽象类GameFrame并给出四个纯虚函数
class GameFrame{
public:GameFrame(){}~GameFrame(){}virtual void Init() 0;//游戏初始化virtual void Close() 0;//游戏结束virtual void Paint() 0;//游戏画面绘制virtual void Run() 0;//游戏运行
};接下来定义坐标结构体方便后续使用
struct Point{int x;int y;void set(int px,int py){//给出设置坐标的set函数x px;y py;}
};//玩家坐标定义推箱子类并继承GameFrame
class PushBox : public GameFrame{
public:PushBox(){}~PushBox (){}void Init();void Close();void Paint();void Run();
};加入类成员变量
#includevectorusing namespace std;class PushBox : public GameFrame{
public:PushBox(){}~PushBox (){}void Init();void Close();void Paint();void Run();
private:int p_id;Point p_point;vectorPoint p_check;char p_map[16][16];
};其中p_id是用于分辨当前的关卡以及后续关卡切换的操作p_point是小人当前坐标p_check用于存储所有的目标点坐标以便遍历目标点判断是否获胜p_map是当前关卡地图在切换关卡和重置关卡时都要对其进行改变。
2.2各功能函数的实现
Init()
初始化函数的功能是设置小人初始坐标将目标点容器清空并填充并设置地图。为了后续的关卡切换功能我们在该函数中传入参数p_id通过判断其值来初始化不同的关卡
void Init(int id){switch (id){case 1:{p_check.clear();//容器清空char temp[16][16] { , , ### , #*# , # # , ####O###### , #*O H O *# , #####O##### , # # , #*# , ### , , , , , };for(int i 0; i 16;i ){//地图初始化for(int j 0; j 16; j ){p_map[i][j] temp[i][j];}}//填充四个目标点p_point.set(7,6);Point t_point;t_point.set(4,6);p_check.push_back(t_point);t_point.set(12,6);p_check.push_back(t_point);t_point.set(8,9);p_check.push_back(t_point);t_point.set(7,3);p_check.push_back(t_point);break;}case 2:{//TODO对第二关进行初始化设置}break;default:break;}
}//游戏初始化Paint()
该函数在1.2.2小结已经讲过主要作用就是刷新显示画面
void Paint() {//游戏画面绘制system(cls);//清空上一次图片for(int i 0; i 16; i){//更新地图打印puts(p_map[i]);}Sleep(10);//10毫秒刷新
}Run()
该函数是推箱子的主体运行函数为了使程序在通关前始终运行我们需要将进程卡在循环中而死循环可以使用while(true)的逻辑实现具体如下
void Run() {while(1){Paint();//刷新显示画面if(//判断是否通过){break;}char z getch();//读取当前输入的按键//小人的移动处理}
}//游戏运行Close()
close()函数的作用就是资源的回收处理和游戏结束处理工作由于本DEMO并没有使用到堆区的空间所以不需要在这部分进行资源回收只需要显示游戏结束即可。
void Close(){system(cls);coutyou win!endl;
}//游戏结束2.3额外添加的函数
接下来是我们游戏框架中没有体现的部分属于推箱子游戏特有的功能需要我们单独定义。
Move(char key)
该函数是负责判断键盘输入的按键来进行不同的处理包括上下左右控制小人移动按R建重置关卡此外也可以考虑按Q键退出该功能暂不实现可以自行考虑等等。 如下
void Move(char key){if(key w){if(p_map[p_point.y-1][p_point.x] ||p_map[p_point.y-1][p_point.x] *){//小人移动的判断p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y--;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新}else if(p_map[p_point.y-1][p_point.x]O (p_map[p_point.y-2][p_point.x] ||p_map[p_point.y-2][p_point.x]*)){//推箱子的判断p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y--;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新p_map[p_point.y-1][p_point.x] O;//更新箱子}}else if(key a){//左if(p_map[p_point.y][p_point.x-1] ||p_map[p_point.y][p_point.x-1] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x--;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y][p_point.x-1]O (p_map[p_point.y][p_point.x-2] ||p_map[p_point.y][p_point.x-2]*)){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x--;p_map[p_point.y][p_point.x] H;p_map[p_point.y][p_point.x-1] O;//更新箱子}}else if(key s){//下if(p_map[p_point.y1][p_point.x] ||p_map[p_point.y1][p_point.x] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.y;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y1][p_point.x]O (p_map[p_point.y2][p_point.x] ||p_map[p_point.y2][p_point.x]*)){p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新p_map[p_point.y1][p_point.x] O;//更新箱子}}else if(key d){//右if(p_map[p_point.y][p_point.x1] ||p_map[p_point.y][p_point.x1] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y][p_point.x1]O (p_map[p_point.y][p_point.x2] ||p_map[p_point.y][p_point.x2]*)){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x;p_map[p_point.y][p_point.x] H;p_map[p_point.y][p_point.x1] O;//更新箱子}}else if(key r){//重置Init(p_id);}}Check()
该函数是判断胜利的条件的关键我们在这一部分遍历所有的目标点位置如果所有的目标点位置都变成了箱子的字符则返回true否则返回false。另外我们还要处理一种情况如下:
########### ###########
#H * O*# - # * HO#
########### ###########如上所示我们控制小人经过一个目标点后将箱子推至最右侧目标点那么按照我们上述对小人移动的逻辑实现小人经过目标点后会将其“扫空”即当箱子和小人离开了目标点地图上将不会再显示因为已经将地图中目标点位置的字符换为了空格。为解决这一bug也要在本函数中作处理。 如下
bool Check(){//用于检测游戏胜利int len p_check.size();int flag 1;for(int i 0; i len; i ){if(p_map[p_check[i].y][p_check[i].x] ){//还原目标点p_map[p_check[i].y][p_check[i].x] *;}if(p_map[p_check[i].y][p_check[i].x] ! O){//判断是否为箱子flag 0;}}if(flag){return true;}else{return false;}
}main()
最后我们在主函数中直接定义推箱子对象并调用执行
int main(){PushBox* p_game new PushBox;p_game-Run();//游戏进行进程将在死循环中p_game-Close();//游戏结束delete p_game;//释放指针空间p_game nullptr;//指针置空防止野指针system(pause);return 0;
}三、完整源代码
#includeiostream
#includestdio.h
#includestdlib.h
#includewindows.h
#includevector
#includeconio.husing namespace std;class GameFrame{
public:GameFrame(){}~GameFrame(){}virtual void Init() 0;//游戏初始化virtual void Close() 0;//游戏结束virtual void Paint() 0;//游戏画面绘制virtual void Run() 0;//游戏运行
};struct Point{int x;int y;void set(int px,int py){x px;y py;}
};//玩家坐标class PushBox : public GameFrame{
public:PushBox(){p_id 1;Init(p_id);}~PushBox(){}void Init(){}void Init(int id){switch (id){case 1:{p_check.clear();char temp[16][16] { , , ### , #*# , # # , ####O###### , #*O H O *# , #####O##### , # # , #*# , ### , , , , , };for(int i 0; i 16;i ){//地图初始化for(int j 0; j 16; j ){p_map[i][j] temp[i][j];}}p_point.set(7,6);Point t_point;t_point.set(4,6);p_check.push_back(t_point);t_point.set(12,6);p_check.push_back(t_point);t_point.set(8,9);p_check.push_back(t_point);t_point.set(7,3);p_check.push_back(t_point);break;}default:break;}}//游戏初始化void Close(){system(cls);coutyou win!endl;}//游戏结束void Paint() {//游戏画面绘制system(cls);//清空上一次图片for(int i 0; i 16; i){//更新地图打印puts(p_map[i]);}Sleep(10);//10毫秒刷新}void Run() {while(1){Paint();if(Check()){break;}char z getch();Move(z);}}//游戏运行void Move(char key){if(key w){if(p_map[p_point.y-1][p_point.x] ||p_map[p_point.y-1][p_point.x] *){p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y--;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新}else if(p_map[p_point.y-1][p_point.x]O (p_map[p_point.y-2][p_point.x] ||p_map[p_point.y-2][p_point.x]*)){p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y--;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新p_map[p_point.y-1][p_point.x] O;//更新箱子}}else if(key a){//左if(p_map[p_point.y][p_point.x-1] ||p_map[p_point.y][p_point.x-1] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x--;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y][p_point.x-1]O (p_map[p_point.y][p_point.x-2] ||p_map[p_point.y][p_point.x-2]*)){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x--;p_map[p_point.y][p_point.x] H;p_map[p_point.y][p_point.x-1] O;//更新箱子}}else if(key s){//下if(p_map[p_point.y1][p_point.x] ||p_map[p_point.y1][p_point.x] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.y;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y1][p_point.x]O (p_map[p_point.y2][p_point.x] ||p_map[p_point.y2][p_point.x]*)){p_map[p_point.y][p_point.x] ;//移动前的位置更新p_point.y;//坐标更新p_map[p_point.y][p_point.x] H;//地图更新p_map[p_point.y1][p_point.x] O;//更新箱子}}else if(key d){//右if(p_map[p_point.y][p_point.x1] ||p_map[p_point.y][p_point.x1] *){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x;p_map[p_point.y][p_point.x] H;}else if(p_map[p_point.y][p_point.x1]O (p_map[p_point.y][p_point.x2] ||p_map[p_point.y][p_point.x2]*)){p_map[p_point.y][p_point.x] ;//之前的位置更新为空格p_point.x;p_map[p_point.y][p_point.x] H;p_map[p_point.y][p_point.x1] O;//更新箱子}}else if(key r){//重置Init(p_id);}}bool Check(){//用于检测游戏胜利int len p_check.size();int flag 1;for(int i 0; i len; i ){if(p_map[p_check[i].y][p_check[i].x] ){p_map[p_check[i].y][p_check[i].x] *;}if(p_map[p_check[i].y][p_check[i].x] ! O){flag 0;}}if(flag){return true;}else{return false;}}
private:int p_id;Point p_point;vectorPoint p_check;char p_map[16][16];//地图
};int main(){PushBox* p_game new PushBox;p_game-Run();p_game-Close();delete p_game;p_game nullptr;system(pause);return 0;
}四、总结
本程序只是实现推箱子逻辑的DEMO感兴趣的同学可以自己查找推箱子的其他关卡实现并完成关卡切换的部分。代码需改进的地方还有很多如 1.为了方便将源码一次性列出我没有定义头文件编写最好是将类以及全局变量的定义放在头文件中类成员函数在源文件中实现由main源文件调用头文件接口运行如下 pushbox.h -定义类、全局变量 pushbox.cpp -实现类的成员函数 main.cpp -引用头文件pushbox.h后只写main函数 2.if-else的过多使用这会使代码看起来很冗长对于这部分还有可优化的地方后续我会配合宏定义将这部分补充。 如果有其他优化的建议欢迎大家在我的评论区留言~ 下期我会将游戏框架的代码完整的进行封装并使用QT实现推箱子的图画版类似下面的样子也将可以实现敬请期待。 Codemon2024.02.02