用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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM