用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 EntityType{
Player = 1,Enemy = 2
};
//实体类型
struct Entity{
Vec2 position; //位置
Vec2 velocity; //速度
EntityType type; //玩家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;
//地面也是一种障碍,高度为0
for (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].type= 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->type = Player;
scene->player->active = true;
scene->player->grounded = false;
//设置重力
scene->gravity = -29.8f;
}
游戏显示
为了让控制台画面不断刷新,我们在游戏循环里加入绘制显示的函数,用以每帧调用。
该函数使用system("cls");来清理屏幕,然后通过printf再次输出要显示的内容。
控制台输出其实是显示1个控制台屏幕缓冲区的内容。
我们可以先把要输出的字符,存进我们自己定义的字符缓冲区。
然后再将字符缓冲区的内容写入到控制台屏幕缓冲区。
#define BUFFER_WIDTH 50
#define BUFFER_HEIGTH 15
struct ViewBuffer {
char buffer[BUFFER_WIDTH][BUFFER_HEIGTH]; //自己定义的字符缓冲区
};
但是很容易发现,画面会有频繁的闪烁:
这是因为上面的操作无论是清理还是输出都是对唯一一个屏幕缓冲区进行操作。
这就导致:可能会高频地出现未完全或者空的画面(发生在屏幕缓冲区清理时或清理后还没显示完内容的短暂时刻)。
双缓冲区技术
解决闪屏问题,只需要准备2个控制台屏幕缓冲区:
当写入其中一个缓冲区时,显示另一个缓冲区。这样就避免了显示不完全的缓冲区,也就解决了闪屏现象。
(上面两幅图显示了两个缓冲区交替使用)
但是因为printf,getch等都是用默认的1个缓冲区,所以我们得另寻其他API,所以下面将会出现一些陌生的输出函数。
首先要先定义两个控制台屏幕缓冲区:
#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] = ' ';
else
vb->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 = 1s
dt /= 1000.0f;
//更新怪物AI
updateAI(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].type == 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].type == Player && scene->eneities[j].type == 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].type == Enemy && scene->eneities[j].type == 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 EntityType {
Player = 1, Enemy = 2
};
//实体类型
struct Entity {
Vec2 position; //位置
Vec2 velocity; //速度
EntityType type; //玩家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;
//地面也是一种障碍,高度为0
for (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].type = 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->type = 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] = ' ';
else
vb->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].type == 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].type == Player && scene->eneities[j].type == 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].type == Enemy && scene->eneities[j].type == 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 = 1s
dt /= 1000.0f;
//更新怪物AI
updateAI(scene, dt);
//更新物理和碰撞
//拆分10次模拟
updatePhysics(scene, dt ,10);
}
int main() {
//限制帧数的循环 <60fps
double 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;
}
用C语言做一个横板过关类型的控制台游戏的更多相关文章
- C语言做一个通讯录程序(在console里面运行)
最近复习C语言的时候看到网上有个C语言通讯录的小项目,于是看了下那个程序实现的大概的功能,然后自己也跟着做了个.代码还算简洁,贴上来给有需要的人. // // main.m // AdressBook ...
- 做一个 App 前需要考虑的几件事
做一个 App 前需要考虑的几件事 来源:limboy的博客 随着工具链的完善,语言的升级以及各种优质教程的涌现,做一个 App 的成本也越来越低了.尽管如此,有些事情最好前期就做起来,避免当 ...
- 有了lisk,为什么我们还要做一个Asch?
0 前言 首先要声明一点,我们和我们的一些朋友都是lisk的投资人和支持者,我们也相信lisk会成功. 事实上,lisk已经成功了一半,目前在区块链领域融资金额排行第二,仅次于以太坊. 那为什么我们还 ...
- 做一个App前需要考虑的几件事
本文转载于文章原文链接,版本归原作者所有! 随着工具链的完善,语言的升级以及各种优质教程的涌现,做一个 App 的成本也越来越低了.尽管如此,有些事情最好前期就做起来,避免当 App 有了一定规模后, ...
- [转载]做一个 App 前需要考虑的几件事
本文转自http://limboy.me/tech/2016/07/06/starting-an-app.html ========================================= ...
- NetAnalyzer笔记 之 三. 用C++做一个抓包程序
[创建时间:2015-08-27 22:15:17] NetAnalyzer下载地址 经过前两篇的瞎扯,你是不是已经厌倦了呢,那么这篇让我们来点有意思的吧,什么,用C#.不,这篇我们先来C++的 Wi ...
- .Net MVC&&datatables.js&&bootstrap做一个界面的CRUD有多简单
我们在项目开发中,做得最多的可能就是CRUD,那么我们如何在ASP.NET MVC中来做CRUD呢?如果说只是单纯实现功能,那自然是再简单不过了,可是我们要考虑如何来做得比较好维护比较好扩展,如何做得 ...
- c语言基础学习09_复合类型
=============================================================================涉及到的知识点有:一.结构体1.定义结构体st ...
- OpenGL ES 2.0 Shader 调试新思路(二): 做一个可用的原型
OpenGL ES 2.0 Shader 调试新思路(二): 做一个可用的原型 目录 背景介绍 请参考前文OpenGL ES 2.0 Shader 调试新思路(一): 改变提问方式 优化 ledCha ...
随机推荐
- Hibernate二级缓存简述及基于Spring4,Hibernate5,Ehcache3的二级缓存配置
Hibernate L2缓存 缓存的分类 L2缓存工作原理 放入二级缓存的数据 Ehcache 依赖 ehcache.xml 常用的memoryStoreEvictionPolicy(缓存算法) eh ...
- TCP的流量控制和拥塞处理
1. 利用滑动窗口实现流量控制 如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失.所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收. 利用滑动窗口机制可以 ...
- springboot: thymeleaf 使用详解
springboot:thymeleaf,这篇文章将更加全面详细的介绍thymeleaf的使用.thymeleaf 是新一代的模板引擎,在spring4.0中推荐使用thymeleaf来做前端模版引擎 ...
- sqlilabs 5
第一个1不断返回true,2可以进行更改?id=-1' union select 1,2,3 and '1?id=-1' union select 1,2,3 and 1='1 ?id=-1' uni ...
- 云计算大数据:Xen、KVM、VMware、hyper-v等虚拟化技术的比较
1.Xen.KVM.VMware.hyper-v等虚拟化技术的比较,xen和kvm,是开源免费的虚拟化软件. vmware是付费的虚拟化软件. hyper-v比较特别,是微软windows 2008 ...
- Struts标签库详解【2】
==================================================================== 需要在head中用<s:head />标签 一.控 ...
- SSM-Spring-02:Spring的DI初步加俩个实例
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- DI:依赖注入 第一个DEMO:域属性注入 java类:(Car类和Stu类,学生有一辆小汽车) packag ...
- MySQL事务原理&实战【官方精译】
事务隔离级别 事务隔离是数据库处理的基础之一.隔离是I中的首字母 ACID ; 隔离级别是在多个事务同时进行更改和执行查询时,对结果的性能和可靠性,一致性和可重复性之间的平衡进行微调的设置. Inno ...
- sql server 高可用故障转移(上)
群集准备工作 个人电脑 内存12G,处理器 AMD A6-3650CPU主频2.6GHz 虚拟机 VMware Workstation 12 数据库 sql server 2008 r2 三台虚拟服 ...
- 解决WordPress无法上传媒体文件以及无法下载和安装主题与插件的问题
前言: 我的个人博客网站荒原之梦在安装成功WordPress之后本来是可以上传媒体文件,安装主题和插件的,但是后来不知道怎么回事就出了问题:不能上传媒体文件也不能安装主题和插件了.出现这个问题后我尝试 ...