用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;
}
相關文章
相關標籤/搜索