用C语言做一个横板过关类型的控制台游戏
前言:本教程是写给刚学会C语言基本语法不久的新生们。
因为在学习C语言途中,往往只能写控制台代码,而还能没接触到图形,也就基本碰不到游戏开发。
所以本教程希望可以给仍在学习C语言的新生们能提前感受到游戏开发技术的魅力和乐趣。
先来看看本次教程程序大概的运行画面:
游戏循环机制
下面是一个简单而熟悉的C程序。
#include <stdio.h>int main() {.... //做一些东西return 0;
}
大部分常见的程序,基本是一套流程下来(典型的流程:输入,输出,结束)
而对于游戏程序来说,往往是一直在运行(很多游戏,即使你不动,整个游戏场景也在一直模拟着)。
因此自然而然想到用循环来实现游戏程序主体——游戏循环机制
一个简单的循环机制
#include <stdio.h>int main() {while (1) {.... //运算(场景数据模拟,更新等).... //渲染(显示场景画面)};return 0;
}
这样的循环机制存在一定问题:程序有时候运算量大,有时候运算量少。造成游戏帧率有时很高,有时很慢。
帧率:每秒的帧数(fps)或者说帧率表示图形处理器处理场时每秒钟能够更新的次数。帧率越高,就越流畅。
- 这就导致有时候程序时而十分快速(动作过于顺畅),有时候就比较慢。即使慢的时候fps有30~60,而在玩家看来,这种对比会造成一种卡顿感。
- 有时候游戏帧率过高是没必要的(例如高于屏幕刷新率或者高于人眼觉得流畅的频率),而且要消耗着更多的运行资源。
限制帧数的循环机制
为了避免帧率过高带来的不好因素,一种妥当的策略是限制帧数。
#include <stdio.h>
#include <windows.h> //有关获取windows系统时间的函数在这个库int main() {double TimePerFrame = 1000.0f/60;//每帧固定的时间差,此处限制fps为60帧每秒//记录上一帧的时间点DWORD lastTime = GetTickCount();while (1) {DWORD nowTime = GetTickCount(); //获得当前帧的时间点DWORD deltaTime = nowTime - lastTime; //计算这一帧与上一帧的时间差lastTime = nowTime; //更新上一帧的时间点.... //运算(场景数据模拟,更新等).... //渲染(显示场景画面)//若 实际时间差 少于 每帧固定时间差,则让机器休眠 少于的部分时间。if (deltaTime <= TimePerFrame)Sleep(TimePerFrame - deltaTime);};return 0;
}
DWORD——unsigned long类型,本文是用来存储毫秒数。属于<windows.h>
Sleep(DWORD ms);——函数作用:让程序休眠ms毫秒。属于<windows.h>
GetTickCount();——函数作用:获取当前时间点(以毫秒为单位),通常利用两个时间点相减来计算时间差。属于<windows.h>
这种循环机制利用时间差的计算,让每帧之间的时间限制在自己想要的固定值。
这样我们就可以利用每帧是固定时间差的原理,实现一些根据每帧时间差来做一些运算操作。
//例如:我们想让一个实体在每1000毫秒20米的速度移动
void update() {//有一个速度float speed = 20.0f / 1000.0f;//因为每帧耗费的时间是TimePerFrame,所以我们让它移动TimePerFrame*speed米。entity->move(TimePerFrame * speed);
}
然后主函数里每帧调用更新(update)函数:
while (1) {DWORD nowTime = GetTickCount();DWORD deltaTime = nowTime - lastTime;lastTime = nowTime;update();.... //渲染(显示场景画面)if (deltaTime <= TimePerFrame)Sleep(TimePerFrame - deltaTime);
};
看起来可行,然而事实上这是真正固定的时间差?
- 并不是。当机器是低性能的时候,处理每帧的时间大于固定时间差时,游戏运行就会变得‘缓慢’。
例如正常运行来说,现实1000毫秒能让游戏更新60次,而60次更新能让人物移动20米。
但是由于某些机器性能低执行缓慢,1000毫秒只能让游戏更新30次,而30次更新只能让人物移动10米。
这在一些要求同步的游戏(例如网络游戏),这种情况是不应发生的,否则会造成两个玩家因为机器性能差
而看到游戏数据的不一致(例如我明明看到某个东西在A点,别人却看到在B点)。
也就是说这个循环机制:
对于过高的帧率,可以限制帧率。
对于低帧率情况,则束手无策,会导致时间不同步。
可变时长的循环机制
要解决时间不同步的问题,其实只需要改一点东西即可解决。
对于更新函数,我们要求一个时间差参数。
//例如:我们想让一个实体在每1000毫秒20米的速度移动
void update(float deltaTime) {//有一个速度float speed = 20.0f / 1000.0f;//因为每帧之间实际耗费的时间是deltaTime,所以我们让它移动deltaTime*speed米。entity->move(deltaTime * speed);
}
给更新(update)等函数传入实际的时间差:
while (1) {DWORD nowTime = GetTickCount();DWORD deltaTime = nowTime - lastTime;....update(deltaTime); //传入实际的时间差....
};
是的,就这样解决了。
即使是低性能的机器,画面卡顿,但是能看到的数据信息也是根据实际运行时间来同步的。
游戏场景
有场景才有万物。自然而然想到第一个事情是如何构建场景。
我们设定,这是一个长为250,高为15的带重力的世界,有1X1大小的障碍物,
里面有10个怪物+1个玩家(总共11个实体)。(PS:一个更好的做法是用链表来存储实体数据,这样可以方便做到动态生成或删除实体)
#define MAP_WIDTH 250
#define MAP_HEIGTH 15
#define ENEMYS_NUM 10
#define ENTITYS_NUM (ENEMYS_NUM+1)//....待补充的类型声明struct Scene{Entity eneities[ENTITYS_NUM]; //场景里的所有实体bool barrier[MAP_WIDTH][MAP_HEIGTH]; //障碍:我们规定假如值为false,则没有障碍。//假如值为true,则有障碍。Entity* player; //提供玩家实体的指针,方便访问玩家float gravity; //重力
};
根据初步设定的场景,我们要补充相应的类型声明。
//二维坐标/向量类型
struct Vec2{float x;float y;
};//区分玩家和敌人的枚举类型
enum EntityTpye{Player = 1,Enemy = 2
};//实体类型
struct Entity{Vec2 position; //位置Vec2 velocity; //速度EntityTpye tpye; //玩家or敌人char texture; //纹理(要显示的图形)bool grounded; //是否在地面上(用于判断跳跃)bool active; //是否存活
};
然后先写好一个初始化场景的函数:
void initScene(Scene* scene){//障碍初始化bool(*barr)[15] = scene->barrier;//所有地方初始化为无障碍for (int i = 0; i < MAP_WIDTH; ++i)for (int j = 0; j < MAP_HEIGTH; ++j)barr[i][j] = false;//地面也是一种障碍,高度为0for (int i = 0; i < MAP_WIDTH; ++i)barr[i][0] = true;//自定义障碍barr[4][1] = barr[4][2] = barr[4][3] = barr[5][1] = barr[5][2]= barr[6][1]= barr[51][3] = barr[52][3] = barr[53][3] = barr[54][3] = barr[55][3] = barr[56][3]= barr[57][3]= true;//敌人初始化for (int i = 0; i < ENTITYS_NUM-1; ++i) {scene->eneities[i].position.x = 5.0f + rand()%(MAP_WIDTH-5);scene->eneities[i].position.y = 10;scene->eneities[i].velocity.x = 0;scene->eneities[i].velocity.y = 0;scene->eneities[i].texture = '#';scene->eneities[i].tpye = Enemy;scene->eneities[i].grounded = false;scene->eneities[i].active = true;}//玩家初始化scene->player = &scene->eneities[ENTITYS_NUM-1];scene->player->position.x = 0;scene->player->position.y = 15;scene->player->velocity.x = 0;scene->player->velocity.y = 0;scene->player->texture = '@';scene->player->tpye = Player;scene->player->active = true;scene->player->grounded = false;//设置重力scene->gravity = -29.8f;
}
游戏显示
为了让控制台画面不断刷新,我们在游戏循环里加入绘制显示的函数,用以每帧调用。
该函数使用system("cls");来清理屏幕,然后通过printf再次输出要显示的内容。
控制台输出其实是显示1个控制台屏幕缓冲区的内容。
我们可以先把要输出的字符,存进我们自己定义的字符缓冲区。
然后再将字符缓冲区的内容写入到控制台屏幕缓冲区。
#define BUFFER_WIDTH 50
#define BUFFER_HEIGTH 15struct ViewBuffer {char buffer[BUFFER_WIDTH][BUFFER_HEIGTH]; //自己定义的字符缓冲区
};
但是很容易发现,画面会有频繁的闪烁:
这是因为上面的操作无论是清理还是输出都是对唯一一个屏幕缓冲区进行操作。
这就导致:可能会高频地出现未完全或者空的画面(发生在屏幕缓冲区清理时或清理后还没显示完内容的短暂时刻)。
双缓冲区技术
解决闪屏问题,只需要准备2个控制台屏幕缓冲区:
当写入其中一个缓冲区时,显示另一个缓冲区。这样就避免了显示不完全的缓冲区,也就解决了闪屏现象。
(上面两幅图显示了两个缓冲区交替使用)
但是因为printf,getch等都是用默认的1个缓冲区,所以我们得另寻其他API,所以下面将会出现一些陌生的输出函数。
首先要先定义两个控制台屏幕缓冲区:
#define BUFFER_WIDTH 50
#define BUFFER_HEIGTH 15struct ViewBuffer {char buffer[BUFFER_WIDTH][BUFFER_HEIGTH]; //字符缓冲区HANDLE hOutBuf[2]; //2个控制台屏幕缓冲区
};
配上一个初始化缓冲区的函数
void initViewBuffer(ViewBuffer * vb) {//初始化字符缓冲区for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j)vb->buffer[i][j] = ' ';//初始化2个控制台屏幕缓冲区vb->hOutBuf[0] = CreateConsoleScreenBuffer(GENERIC_WRITE,//定义进程可以往缓冲区写数据FILE_SHARE_WRITE,//定义缓冲区可共享写权限NULL,CONSOLE_TEXTMODE_BUFFER,NULL);vb->hOutBuf[1] = CreateConsoleScreenBuffer(GENERIC_WRITE,//定义进程可以往缓冲区写数据FILE_SHARE_WRITE,//定义缓冲区可共享写权限NULL,CONSOLE_TEXTMODE_BUFFER,NULL);//隐藏2个控制台屏幕缓冲区的光标CONSOLE_CURSOR_INFO cci;cci.bVisible = 0;cci.dwSize = 1;SetConsoleCursorInfo(vb->hOutBuf[0], &cci);SetConsoleCursorInfo(vb->hOutBuf[1], &cci);}
每帧更新字符缓冲区函数和显示屏幕缓冲区函数
void updateViewBuffer(Scene* scene, ViewBuffer * vb) {//更新BUFFER中的地面+障碍物int playerX = scene->player->position.x + 0.5f;int offsetX = min(max(0, playerX - BUFFER_WIDTH / 2), MAP_WIDTH - BUFFER_WIDTH - 1);for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j){if (scene->barrier[i + offsetX][j] == false)vb->buffer[i][j] = ' ';elsevb->buffer[i][j] = '=';}//更新BUFFER中的实体for (int i = 0; i < ENTITYS_NUM; ++i) {int x = scene->eneities[i].position.x + 0.5f - offsetX;int y = scene->eneities[i].position.y + 0.5f;if (scene->eneities[i].active == true && 0 <= x && x < BUFFER_WIDTH&& 0 <= y && y < BUFFER_HEIGTH) {vb->buffer[x][y] = scene->eneities[i].texture;}}
}void drawViewBuffer(Scene* scene ,ViewBuffer * vb) {//先根据场景数据,更新字符缓冲区数据updateViewBuffer(scene,vb);//再将字符缓冲区的内容写入其中一个屏幕缓冲区static int buffer_index = 0;COORD coord = { 0,0 };DWORD bytes = 0;for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j){coord.X = i;coord.Y = BUFFER_HEIGTH - 1 - j;WriteConsoleOutputCharacterA(vb->hOutBuf[buffer_index], &vb->buffer[i][j],1, coord, &bytes);}//显示 写入完成的缓冲区SetConsoleActiveScreenBuffer(vb->hOutBuf[buffer_index]);//下一次将使用另一个缓冲区buffer_index = !buffer_index;
}
游戏输入
常见的C输入函数scanf,getch等都是属于阻塞形输入,即没有输入则代码不会继续往下执行。
但在游戏程序里几乎见不到阻塞形输入,因为即使玩家不输入,游戏也得继续运行。
这时候我们可能需要一些即使没有输入,代码也会往下执行的函数。
异步键盘输入
异步键盘输入函数是<windows.h>提供的。
它在相应按键按下时,第15位设为1;若抬起,则设为0。
利用判断该函数返还值 & 0x8000的值 是不是为真,来判断当前帧有没有按下按键。
示例用法 :
if (GetAsyncKeyState(VK_UP) & 0x8000) {...}
//VK_UP可改成其他VK_XX代表键盘的按键
下面是本文游戏的输入处理函数:
//处理输入
void handleInput(Scene* scene) {//如果玩家死亡,则不能操作if (scene->player->active != true)return;//控制跳跃if (GetAsyncKeyState(VK_UP) & 0x8000) {if (scene->player->grounded)scene->player->velocity.y = 15.0f;}//控制左右移动bool haveMoved = false;if (GetAsyncKeyState(VK_LEFT) & 0x8000) {scene->player->velocity.x = -5.0f;haveMoved = true;}if (GetAsyncKeyState(VK_RIGHT) & 0x8000) {scene->player->velocity.x = 5.0f;haveMoved = true;}//若没有移动,则速度停顿下来if (haveMoved != true) {scene->player->velocity.x = max(0,scene->player->velocity.x * 0.5f);//使用线性速度的渐进减速}
}
所谓的控制移动,其实就是根据输入来给玩家设置x轴和y轴上的速度。
游戏更新
我们知道一个游戏循环内,一般都是先游戏数据更新,然后根据数据显示相应的画面。
所以说游戏更新是一个很重要的内容,由于篇幅有限,本文游戏更新只包含3个内容。
void updateScene(Scene* scene, float dt) {//缩小时间尺度为秒单位,1000ms = 1sdt /= 1000.0f;//更新怪物AIupdateAI(scene,dt);//更新物理和碰撞updatePhysics(scene,dt);
}
简单的游戏AI
void updateAI(Scene* scene, float dt) {//简单计时器static float timeCounter = 0.0f;timeCounter += dt;//每2秒更改一次方向(随机方向,可能方向不变)if (timeCounter >= 2.0f) {timeCounter = 0.0f;for (int i = 0; i < ENTITYS_NUM; ++i) {//存活着的怪物才能被AI操控着移动if (scene->eneities[i].active == true && scene->eneities[i].tpye == Enemy) {scene->eneities[i].velocity.x = 3.0f * (1-2*(rand()%2));//(1-2*(rand()%1)要不是 -1要不是1}}}
}
物理模拟&碰撞检测
物理模拟:预测一个物体dt时间后的位置,若该位置碰到其他物体,则说明该物体将会碰到东西
,然后就使该物体位置不变。否则没碰到,就更新物体的新位置。
碰撞检测:实体碰撞这里用的是简单粗暴的,逐个实体比较,若两个实体之间的距离小于1(本文用的是
自己写的distanceSq()函数,返还两点之间的距离的平方,这样运算不需用开方的开销),则断定
该两个实体互相碰撞,然后将他们的索引(在实体数组的第n个位置)交给处理碰撞事件的函数。
//更新物理&碰撞
void updatePhysics(Scene* scene, float dt) {//更新实体for (int i = 0; i < ENTITYS_NUM; ++i) {//若实体死亡,则无需更新if (scene->eneities[i].active != true)continue;//记录原实体位置float x0f = scene->eneities[i].position.x;float y0f = scene->eneities[i].position.y;int x0 = x0f + 0.5f;int y0 = y0f + 0.5f;//记录模拟后的实体位置float x1f = min(max(scene->eneities[i].position.x + dt * scene->eneities[i].velocity.x, 0.0f), MAP_WIDTH - 1);float y1f = min(max(scene->eneities[i].position.y + dt * scene->eneities[i].velocity.y, 1.0f), MAP_HEIGTH - 1);int x1 = x1f + 0.5f;int y1 = y1f + 0.5f;//判断障碍碰撞if (scene->barrier[x0][y1] == true) {scene->eneities[i].velocity.y = 0;y1 = y0;y1f = y0f;}if (scene->barrier[x1][y1] == true) {scene->eneities[i].velocity.x = 0;x1 = x0;x1f = x0f;}//判断实体碰撞for (int j = i + 1; j < ENTITYS_NUM; ++j) {//若实体死亡,则无需判定if (scene->eneities[j].active != true)continue;float disSq = distanceSq(scene->eneities[i].position, scene->eneities[j].position);if (disSq <= 1 * 1) {//若发生碰撞,则处理该碰撞事件handleCollision(scene, i, j, disSq);}}//判断是否踩到地面(位置的下一格),用于处理跳跃if (scene->barrier[x1][max(y1 - 1, 0)] == true) {scene->eneities[i].grounded = true;}else {scene->eneities[i].velocity.y += dt * scene->gravity;scene->eneities[i].grounded = false;}//更新实体位置(可能是旧位置也可能是新位置)scene->eneities[i].position.x = x1f;scene->eneities[i].position.y = y1f;
}
一切看起来很好,但是实际运行的时候发生了物理穿模现象(即物体穿过了模型)。
- 原因:时间dt*速度的值太大,结果预测位置越过了障碍位置,且预测位置处没有障碍,然后判定这次预测移动成功。
- 解决方案:将模拟的时间段dt拆分成更小段,从而模拟多次,每次模拟改变的位置值也就减少,减少穿模的可能性。
(如图,一次模拟拆分成5次,然后在第三次模拟中发现碰到了障碍,也就阻止了物体穿模。)
这是物理引擎的固有缺点,许多游戏都可能发生穿模现象(育碧现象),特别是高速移动的物体。所以常见的手法还有
对高速移动物体进行更多拆分模拟(例如子弹的运动模拟)。
改进后的物理模拟代码,这样我们可以指定stepNum来决定这个dt时间段拆分成多少个小时间段:
//更新物理&碰撞
void updatePhysics(Scene* scene, float dt, int stepNum) {dt /= stepNum;for (int i = 0; i < stepNum; ++i) {//更新实体for (int i = 0; i < ENTITYS_NUM; ++i) {//若实体死亡,则无需更新if (scene->eneities[i].active != true)continue;//记录原实体位置float x0f = scene->eneities[i].position.x;float y0f = scene->eneities[i].position.y;int x0 = x0f + 0.5f;int y0 = y0f + 0.5f;//记录模拟后的实体位置float x1f = min(max(scene->eneities[i].position.x + dt * scene->eneities[i].velocity.x, 0.0f), MAP_WIDTH - 1);float y1f = min(max(scene->eneities[i].position.y + dt * scene->eneities[i].velocity.y, 1.0f), MAP_HEIGTH - 1);int x1 = x1f + 0.5f;int y1 = y1f + 0.5f;//判断障碍碰撞if (scene->barrier[x0][y1] == true) {scene->eneities[i].velocity.y = 0;y1 = y0;y1f = y0f;}if (scene->barrier[x1][y1] == true) {scene->eneities[i].velocity.x = 0;x1 = x0;x1f = x0f;}//判断实体碰撞for (int j = i + 1; j < ENTITYS_NUM; ++j) {//若实体死亡,则无需判定if (scene->eneities[j].active != true)continue;float disSq = distanceSq(scene->eneities[i].position, scene->eneities[j].position);if (disSq <= 1 * 1) {//若发生碰撞,则处理该碰撞事件handleCollision(scene, i, j, disSq);}}//判断是否踩到地面if (scene->barrier[x1][max(y1 - 1, 0)] == true) {scene->eneities[i].grounded = true;}else {scene->eneities[i].velocity.y += dt * scene->gravity;scene->eneities[i].grounded = false;}scene->eneities[i].position.x = x1f;scene->eneities[i].position.y = y1f;}}
}
接下来就是处理碰撞事件了,本文选择模仿超级马里奥的效果:
当玩家和怪物互相碰撞时,若玩家踩到怪物头上,怪物死亡。否则玩家死亡。
//实体死亡函数
void entityDie(Scene* scene,int entityIndex) {scene->eneities[entityIndex].active = false;scene->eneities[entityIndex].velocity.x = 0;scene->eneities[entityIndex].velocity.y = 0;
}//处理碰撞事件
void handleCollision(Scene* scene, int i,int j,float disSq) {//若玩家碰到怪物if (scene->eneities[i].tpye == Player && scene->eneities[j].tpye == Enemy) {//若玩家高度高于怪物0.3,则证明玩家踩在怪物头上,怪物死亡。if (scene->eneities[i].position.y - 0.3f > scene->eneities[j].position.y) {entityDie(scene,j);}//否则玩家死亡else {entityDie(scene,i);}}//若怪物碰到玩家if (scene->eneities[i].tpye == Enemy && scene->eneities[j].tpye == Player) {//若玩家高度高于怪物0.3,则证明玩家踩在怪物头上,怪物死亡。if (scene->eneities[j].position.y - 0.3f > scene->eneities[i].position.y) {entityDie(scene, i);}//否则玩家死亡else {entityDie(scene, j);}}
}
总结
这里已经包含了很多内容,想必大家应该对游戏开发有一些认识了,
然而这个游戏还未能达到真正完整的程度,但是基本的游戏框架已经成型,
要扩展成为一个完整的横板游戏(开始界面,结束条件,奖励,更多敌人/技能等)这些内容就不再
多讲,可以课余尝试自己去实现。
完整源代码(为了方便copy,于是没有分多文件):
#include <stdio.h>
#include <Windows.h>
#include <math.h>
#include <stdlib.h>//限制帧数:围绕固定时间差(限制上限的时间差)来编写
//限制帧数+可变时长:围绕现实/实际时间差 来编写#define MAP_WIDTH 250
#define MAP_HEIGTH 15
#define ENTITYS_NUM 11//二维坐标/向量类型
struct Vec2 {float x;float y;
};//区分玩家和敌人的枚举类型
enum EntityTpye {Player = 1, Enemy = 2
};//实体类型
struct Entity {Vec2 position; //位置Vec2 velocity; //速度EntityTpye tpye; //玩家or敌人char texture; //纹理(要显示的图形)bool grounded; //是否在地面上(用于判断跳跃)bool active; //是否存活
};//场景类型
struct Scene {Entity eneities[ENTITYS_NUM]; //场景里的所有实体bool barrier[MAP_WIDTH][MAP_HEIGTH]; //障碍:我们规定假如值为false,则没有障碍。//假如值为true,则有障碍。Entity* player; //提供玩家实体的指针,方便访问玩家float gravity; //重力 -1119.8f
};//初始化场景函数
void initScene(Scene* scene) {//-----------------------------障碍初始化bool(*barr)[15] = scene->barrier;//所有地方初始化为无障碍for (int i = 0; i < MAP_WIDTH; ++i)for (int j = 0; j < MAP_HEIGTH; ++j)barr[i][j] = false;//地面也是一种障碍,高度为0for (int i = 0; i < MAP_WIDTH; ++i)barr[i][0] = true;//自定义障碍barr[4][1] = barr[4][2] = barr[4][3] = barr[5][1] = barr[5][2] = barr[6][1]= barr[51][3] = barr[52][3] = barr[53][3] = barr[54][3] = barr[55][3] = barr[56][3] = barr[57][3]= true;//-----------------------------实体初始化//敌人初始化for (int i = 0; i < ENTITYS_NUM - 1; ++i) {scene->eneities[i].position.x = 5.0f + rand() % (MAP_WIDTH - 5);scene->eneities[i].position.y = 10;scene->eneities[i].velocity.x = 0;scene->eneities[i].velocity.y = 0;scene->eneities[i].texture = '#';scene->eneities[i].tpye = Enemy;scene->eneities[i].grounded = false;scene->eneities[i].active = true;}//玩家初始化scene->player = &scene->eneities[ENTITYS_NUM - 1];scene->player->position.x = 0;scene->player->position.y = 15;scene->player->velocity.x = 0;scene->player->velocity.y = 0;scene->player->texture = '@';scene->player->tpye = Player;scene->player->active = true;scene->player->grounded = false;//---------------设置重力scene->gravity = -29.8f;
}#define BUFFER_WIDTH 50
#define BUFFER_HEIGTH 15//显示用的辅助工具
struct ViewBuffer {char buffer[BUFFER_WIDTH][BUFFER_HEIGTH]; //自己定义的字符缓冲区HANDLE hOutBuf[2]; //2个控制台屏幕缓冲区
};//初始化显示
void initViewBuffer(ViewBuffer * vb) {//初始化字符缓冲区for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j)vb->buffer[i][j] = ' ';//初始化2个控制台屏幕缓冲区vb->hOutBuf[0] = CreateConsoleScreenBuffer(GENERIC_WRITE,//定义进程可以往缓冲区写数据FILE_SHARE_WRITE,//定义缓冲区可共享写权限NULL,CONSOLE_TEXTMODE_BUFFER,NULL);vb->hOutBuf[1] = CreateConsoleScreenBuffer(GENERIC_WRITE,//定义进程可以往缓冲区写数据FILE_SHARE_WRITE,//定义缓冲区可共享写权限NULL,CONSOLE_TEXTMODE_BUFFER,NULL);//隐藏2个控制台屏幕缓冲区的光标CONSOLE_CURSOR_INFO cci;cci.bVisible = 0;cci.dwSize = 1;SetConsoleCursorInfo(vb->hOutBuf[0], &cci);SetConsoleCursorInfo(vb->hOutBuf[1], &cci);
}//每帧 根据场景数据 更新 显示缓冲区
void updateViewBuffer(Scene* scene, ViewBuffer * vb) {//更新BUFFER中的地面+障碍物int playerX = scene->player->position.x + 0.5f;int offsetX = min(max(0, playerX - BUFFER_WIDTH / 2), MAP_WIDTH - BUFFER_WIDTH - 1);for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j){if (scene->barrier[i + offsetX][j] == false)vb->buffer[i][j] = ' ';elsevb->buffer[i][j] = '=';}//更新BUFFER中的实体for (int i = 0; i < ENTITYS_NUM; ++i) {int x = scene->eneities[i].position.x + 0.5f - offsetX;int y = scene->eneities[i].position.y + 0.5f;if (scene->eneities[i].active == true&& 0 <= x && x < BUFFER_WIDTH&& 0 <= y && y < BUFFER_HEIGTH) {vb->buffer[x][y] = scene->eneities[i].texture;}}
}//每帧 根据显示缓冲区 显示画面
void drawViewBuffer(ViewBuffer * vb) {//再将字符缓冲区的内容写入其中一个屏幕缓冲区static int buffer_index = 0;COORD coord = { 0,0 };DWORD bytes = 0;for (int i = 0; i < BUFFER_WIDTH; ++i)for (int j = 0; j < BUFFER_HEIGTH; ++j){coord.X = i;coord.Y = BUFFER_HEIGTH - 1 - j;WriteConsoleOutputCharacterA(vb->hOutBuf[buffer_index], &vb->buffer[i][j], 1, coord, &bytes);}//显示 写入完成的缓冲区SetConsoleActiveScreenBuffer(vb->hOutBuf[buffer_index]);//下一次将使用另一个缓冲区buffer_index = !buffer_index;//!1 = 0 !0 = 1
}//处理输入
void handleInput(Scene* scene) {//如果玩家死亡,则不能操作if (scene->player->active != true)return;//控制跳跃if (GetAsyncKeyState(VK_UP) & 0x8000) {if (scene->player->grounded)scene->player->velocity.y = 15.0f;}//控制左右移动bool haveMoved = false;if (GetAsyncKeyState(VK_LEFT) & 0x8000) {scene->player->velocity.x = -5.0f;haveMoved = true;}if (GetAsyncKeyState(VK_RIGHT) & 0x8000) {scene->player->velocity.x = 5.0f;haveMoved = true;}//若没有移动,则速度停顿下来if (haveMoved != true) {scene->player->velocity.x = max(0, scene->player->velocity.x * 0.5f);//使用线性速度的渐进减速}
}//更新怪物AI
void updateAI(Scene* scene, float dt) {//简单计时器static float timeCounter = 0.0f;timeCounter += dt;//每2秒更改一次方向(随机方向,可能方向不变)if (timeCounter >= 2.0f) {timeCounter = 0.0f;//改变方向的代码for (int i = 0; i < ENTITYS_NUM; ++i) {//存活着的怪物才能被AI操控着移动if (scene->eneities[i].active == true && scene->eneities[i].tpye == Enemy) {scene->eneities[i].velocity.x = 3.0f * (1 - 2 * (rand() % 2));//(1-2*(rand()%1)要不是 -1要不是1}}}
}//计算距离的平方
float distanceSq(Vec2 a1, Vec2 a2) {float dx = a1.x - a2.x;float dy = a1.y - a2.y;return dx * dx + dy * dy;
}//某个实体死亡
void entityDie(Scene* scene, int entityIndex) {scene->eneities[entityIndex].active = false;scene->eneities[entityIndex].velocity.x = 0;scene->eneities[entityIndex].velocity.y = 0;
}//处理碰撞事件
void handleCollision(Scene* scene, int i, int j, float disSq) {//若玩家碰到怪物if (scene->eneities[i].tpye == Player && scene->eneities[j].tpye == Enemy) {//若玩家高度高于怪物0.3,则证明玩家踩在怪物头上,怪物死亡。if (scene->eneities[i].position.y - 0.3f > scene->eneities[j].position.y) { entityDie(scene, j); }//否则玩家死亡else { entityDie(scene, i); }}//若怪物碰到玩家if (scene->eneities[i].tpye == Enemy && scene->eneities[j].tpye == Player) {//若玩家高度高于怪物0.3,则证明玩家踩在怪物头上,怪物死亡。if (scene->eneities[j].position.y - 0.3f > scene->eneities[i].position.y) { entityDie(scene, i); }//否则玩家死亡else { entityDie(scene, j); }}
}//更新物理&碰撞
void updatePhysics(Scene* scene, float dt,int stepNum) {dt /= stepNum;for (int i = 0; i < stepNum; ++i){//更新实体for (int i = 0; i < ENTITYS_NUM; ++i) {//若实体死亡,则无需更新if (scene->eneities[i].active != true)continue;//记录原实体位置float x0f = scene->eneities[i].position.x;float y0f = scene->eneities[i].position.y;int x0 = x0f + 0.5f;int y0 = y0f + 0.5f;//记录模拟后的实体位置//旧位置 + 时间×速度 = 新位置float x1f = min(max(scene->eneities[i].position.x + dt * scene->eneities[i].velocity.x, 0.0f), MAP_WIDTH - 1);float y1f = min(max(scene->eneities[i].position.y + dt * scene->eneities[i].velocity.y, 1.0f), MAP_HEIGTH - 1);int x1 = x1f + 0.5f;int y1 = y1f + 0.5f;//判断障碍碰撞if (scene->barrier[x0][y1] == true) {scene->eneities[i].velocity.y = 0;y1 = y0;y1f = y0f;}if (scene->barrier[x1][y1] == true) {scene->eneities[i].velocity.x = 0;x1 = x0;x1f = x0f;}//判断是否踩到地面(位置的下一格),用于处理跳跃if (scene->barrier[x1][max(y1 - 1, 0)] == true) {scene->eneities[i].grounded = true;}else {// 增加的速度大小 = 时间*(重力/质量)scene->eneities[i].velocity.y += dt * (scene->gravity / 1.0f);scene->eneities[i].grounded = false;}//判断实体碰撞for (int j = i + 1; j < ENTITYS_NUM; ++j) {//若实体死亡,则无需判定if (scene->eneities[j].active != true)continue;float disSq = distanceSq(scene->eneities[i].position, scene->eneities[j].position);if (disSq < 1 * 1) {//若发生碰撞,则处理该碰撞事件handleCollision(scene, i, j, disSq);}}//更新实体位置(可能是旧位置也可能是新位置)scene->eneities[i].position.x = x1f;scene->eneities[i].position.y = y1f;}}
}//更新场景数据
void updateScene(Scene* scene, float dt) {//缩小时间尺度为秒单位,1000ms = 1sdt /= 1000.0f;//更新怪物AIupdateAI(scene, dt);//更新物理和碰撞//拆分10次模拟updatePhysics(scene, dt ,10);
}int main() {//限制帧数的循环 <60fpsdouble TimePerFrame = 1000.0f / 60;//每帧固定的时间差,此处限制fps为60帧每秒//记录上一帧的时间点DWORD lastTime = GetTickCount();//显示缓冲区ViewBuffer vb;initViewBuffer(&vb);//场景Scene sc;initScene(&sc);while (1) {DWORD nowTime = GetTickCount(); //获得当前帧的时间点DWORD deltaTime = nowTime - lastTime; //计算这一帧与上一帧的时间差lastTime = nowTime; //更新上一帧的时间点handleInput(&sc);//处理输入updateScene(&sc,deltaTime);//更新场景数据updateViewBuffer(&sc, &vb);//更新显示区drawViewBuffer(&vb);//渲染(显示)//若 实际时间差 少于 每帧固定时间差,则让机器休眠 少于的部分时间。if (deltaTime <= TimePerFrame)Sleep(TimePerFrame - deltaTime);}return 0;
}
转载于:https://www.cnblogs.com/KillerAery/p/9747957.html
用C语言做一个横板过关类型的控制台游戏相关推荐
- 用C语言做一个迷宫小游戏
用C语言做一个迷宫小游戏,以下是代码段 这个迷宫游戏使用了递归回溯算法来寻找通往出口的路径.迷宫中的墙用'#'表示,路径用空格表示,入口和出口分别用'S'和'E'表示,已走过的路径用'*'表示.在生成 ...
- 做一个像植物大战僵尸的Flash游戏1
http://bbs.9ria.com/thread-80527-1-6.html 这个教程是我翻译的..不是我原创的..原来是发表在新闻资讯版块,后来被移到了游戏编程版块,所以关于原作者的一些信息等 ...
- 用cocos2dx做一个简单的单机捕鱼达人游戏(1)
用cocos2dx做一个简单的单机捕鱼达人游戏(1) 我使用了cocos2dx 3.9版本和vs2017来开发 今天先做游戏开始界面 开始界面很简单,一个背景图,一个logo,3个button(三种登 ...
- 可以帮我做一个python的3D飞机小游戏吗
当然可以!我很乐意帮助你做一个 Python 的 3D 飞机小游戏. 如果你是 Python 初学者,我建议你先了解一些 Python 的基础知识,包括变量.数据类型.流程控制语句.函数等.这些知识都 ...
- 用c++做一个简单的打飞机小游戏(详细说明与注释)
用c++做一个简单的打飞机小游戏(详细说明与注释) 说明: 代码长度5k多,行数200多行. 不仅没有压行,反而为了条理清晰一点所以很多中间加空换行,把很多可以写在一起的分割成了几个函数. 为了不会忘 ...
- 程序员带你回味童年,一起用C语言做一个“推箱子”玩!【文末源码】
这篇文章是用C语言做了一个推箱子小游戏,实现起来比较简单,和大家一起回味一下童年捧着按键机玩推箱子的日子!文末附带万字源码! 目录 一.写在前面 二.设计思路 1.主界面函数介绍 2.选择界面函数 3 ...
- 语言做一个自动售货机软件_软件开发手机app系统软件高端定制做一个app软件要多少钱...
软件开发手机app系统软件高端定制-做一个app软件要多少钱 APP开发分原生APP开发和在线制作,我们来看下这两种都需要多少费用吧. 1.原生APP开发(定制开发) 互联网是个神奇的大网,大数据开发 ...
- 利用Python做一个简单的对战小游戏
利用Python做一个简单的文字对战小游戏 一.游戏介绍 1.大体介绍:文字版的对战小游戏,可以利用Python随机生成两个角色,角色带有各自的血量和攻击值两个指标.两人在对战时同时攻击对方,同时造成 ...
- 从零开始制作一个飞机大战类型的射击游戏
射击类游戏是极为经典的游戏系列之一,它往往有着精美绚丽的画面,高度有趣的音效,为玩家呈现不一样射击体验,让人流连忘返. 今天,我们用scratch从零开始制作了的一个射击类型的小游戏,适合学习者进行学 ...
最新文章
- php获取等于符号后面的参数,php获取URL中带#号等特殊符号参数的解决方法
- 【PAT乙级】1074 宇宙无敌加法器 (20 分)
- WIX(20121031) 应用设置默认变量
- 鸿蒙系统开发资金,华为终于动手,将拿出超十亿资金,开发者们有福了
- 这帮吃货程序猿,给阿里食堂来了一波骚操作
- 应用交付脚本工具在HTTP服务中的应用
- ASP.NET 2.0的编译行为
- 如何使用 iCloud 钥匙串从 macOS Monterey 导入和导出密码?
- 软件评測师真题考试分析-5
- clearcase命令
- pytorch Resnet 网络结构
- PHP自学笔记 ---李炎恢老师PHP第一季 TestGuest0.8
- 拆装智伴机器人_智伴机器人软件下载-智伴下载 v4.2.8-pc6智能硬件网
- 苹果手机咋用计算机,苹果手机怎么通过usb连接电脑上网
- ABE或IBE中属性撤销的寻找最小覆盖集的基本算法
- LiDAR-based Panoptic Segmentation via Dynamic Shifting Network(论文阅读笔记)
- 防微信聊天气泡图片实现
- 安 卓APP隐私政策检测自查评估
- python机器人编程与操作_机器人Python极客编程入门与实战 PDF 完整目录版
- 基于Pytorch的强化学习(DQN)之 Experience Replay