用C语言作一个横板过关类型的控制台游戏

前言:本教程是写给刚学会C语言基本语法不久的新生们。windows

由于在学习C语言途中,每每只能写控制台代码,而还能没接触到图形,也就基本碰不到游戏开发。数组

因此本教程但愿能够给仍在学习C语言的新生们能提早感觉到游戏开发技术的魅力和乐趣。网络

先来看看本次教程程序大概的运行画面:


框架

游戏循环机制

下面是一个简单而熟悉的C程序。异步

#include <stdio.h>

int main() {
    ....    //作一些东西
    return 0;
}

大部分常见的程序,基本是一套流程下来(典型的流程:输入,输出,结束)函数

而对于游戏程序来讲,每每是一直在运行(不少游戏,即便你不动,整个游戏场景也在一直模拟着)。
所以天然而然想到用循环来实现游戏程序主体——游戏循环机制工具

一个简单的循环机制

#include <stdio.h>

int main() {
    while (1) {
    ....    //运算(场景数据模拟,更新等)
    ....    //渲染(显示场景画面)
    };
    return 0;
}

这样的循环机制存在必定问题:程序有时候运算量大,有时候运算量少。形成游戏帧率有时很高,有时很慢。性能

帧率:每秒的帧数(fps)或者说帧率表示图形处理器处理场时每秒钟可以更新的次数。帧率越高,就越流畅。学习

  • 这就致使有时候程序时而十分快速(动做过于顺畅),有时候就比较慢。即便慢的时候fps有30~60,而在玩家看来,这种对比会形成一种卡顿感。
  • 有时候游戏帧率太高是不必的(例如高于屏幕刷新率或者高于人眼以为流畅的频率),并且要消耗着更多的运行资源。

限制帧数的循环机制

为了不帧率太高带来的很差因素,一种稳当的策略是限制帧数。3d

#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;
    //地面也是一种障碍,高度为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].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 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].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;
    //地面也是一种障碍,高度为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].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] = ' ';
            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].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 = 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;
}
相关文章
相关标签/搜索