【C語言程序設(shè)計】小游戲之俄羅斯方塊(二)!適合初學(xué)者上手、練手!

1. 容器的表示

大方塊的實現(xiàn)涉及到位運(yùn)算,而容器同樣如此。容器顯示的部分是由 10 * 20 個小方塊構(gòu)成的矩形,如果我們將每個小方塊用一個比特來表示,則一行只需要 10 比特,C語言中可以用 unsigned short 表示,不過這里我們?yōu)榱撕笃跀U(kuò)展,選用了 unsigned long 類型。

unsigned long blockContainer[TETRIS_CONTAINER_HEIGHT];

blockContainer 變量代表整個容器,TETRIS_CONTAINER_HEIGHT 代表容器的高度。這里需要注意常量 TETRIS_CONTAINER_HEIGHT 并沒有定義為 20,而是定義為 25,容器的每行我們用了 12 位比特表示,并沒有用 10 位表示,這所以這樣,其實是為了碰撞檢測帶來方便,其中容器的寬高定義如下:

//俄羅斯方塊容器寬高

#define TETRIS_CONTAINER_WIDTH (1 + 10 + 1)

#define TETRIS_CONTAINER_HEIGHT (BLOCK_HEIGHT + 20 + 1)

這里我們用容器的示意圖表示一下這樣定義的好處:

上圖是俄羅斯方塊真正的容器區(qū)域,其中游戲界面顯示的僅僅是其中的藍(lán)色顯示區(qū)域,而綠色隱藏區(qū)域用來放置準(zhǔn)備下落的大方塊,而灰色是用來碰撞檢測的隔離區(qū)域。

因為 Windows 窗口的縱坐標(biāo)是從上到下的,所以我們顯示的時候也是從上到下,最上邊是容器的第 0 行,最下邊是容器的 24 行,這一行會用來兜底,防止大方塊在下落的過程中越界。


2. 大方塊的表示

Windows 窗口的橫坐標(biāo)是從左到右,所以左邊是第 0 行,最右邊是第 11 行。這里需要注意這和默認(rèn)大方塊表示的二進(jìn)制順序并不一樣:

事實上,前臺顯示的畫面左邊是二進(jìn)制的低位,右邊是二進(jìn)制的高位,所以大方塊真正表示的二進(jìn)制是和顯示的畫面水平方向正好是相反的。


3. 結(jié)構(gòu)定義

明白了上面的介紹,接下來我們就可以定義俄羅斯方塊的數(shù)據(jù)結(jié)構(gòu):

//俄羅斯方塊

typedef struct Tetris

{

unsigned long blockContainer[TETRIS_CONTAINER_HEIGHT]; // 容器

int blockIndex; // 當(dāng)前塊索引

Block blocks[TETRIS_BLOCK_NUM]; // 多個塊(前后臺)

//......

} Tetris;

俄羅斯方塊中成員很多,但最重要的就是容器和兩個方塊的表示,之所以是兩個方塊是因為一個是當(dāng)前下落的前臺方塊,另一個是下輪下落的后臺方塊,這里用數(shù)組表示,然后增加一個方塊索引,用來循環(huán)使用。


4. 初始化

有了數(shù)據(jù)結(jié)構(gòu)之后,接下來可以實現(xiàn)俄羅斯方塊的基本操作了。首先當(dāng)然是初始化操作:

//初始化容器

for (int i = 0; i < TETRIS_CONTAINER_HEIGHT; i++) {

tetris->blockContainer[i] = EMPTY_LINE;

}

tetris->blockContainer[TETRIS_CONTAINER_HEIGHT - 1] = 0xFFFF;

//初始化方塊

tetris->blockIndex = 0;

for (int i = 0; i < TETRIS_BLOCK_NUM; i++) {

initRandBlock(&(tetris->blocks[i]), BLOCK_INIT_POSY, BLOCK_INIT_POSX);

}

代碼中邏輯就是將容器初始化為前面的示意圖狀態(tài),其中定義了三個常量:

const int BLOCK_INIT_POSX = (TETRIS_CONTAINER_WIDTH - BLOCK_WIDTH) / 2;

const int BLOCK_INIT_POSY = 2;

const unsigned long EMPTY_LINE = 0x0801;

前兩個用來表示方塊初始化的位置,后面則是值容器中空行的數(shù)值。


5. 碰撞合并

初始化完成之后,我們接下來實現(xiàn)大方塊和容器的碰撞操作以及大方塊和容器發(fā)生碰撞后的合并操作。首先是碰撞操作:

//碰撞測試

int hitTest(const Block* block, const Tetris* tetris)

{

? ? unsigned short blk = gBlockList[block->type][block->state];

? ? for (int i = 0; i < BLOCK_HEIGHT; i++)

? ? {

? ? ? ? unsigned short bits = ((blk >> (i * BLOCK_WIDTH)) & 0x000F);

? ? ? ? //block->col 可能為負(fù)數(shù)

? ? ? ? if (block->col < 0) {

? ? ? ? ? ? bits >>= (-block->col);

? ? ? ? } else {

? ? ? ? ? ? bits <<= block->col;

? ? ? ? }

? ? ? ? if(tetris->blockContainer[block->row + i] & bits)

? ? ? ? {

? ? ? ? ? ? return 1;

? ? ? ? }

? ? }

? ? return 0;

}

碰撞測試中首先獲取當(dāng)前大方塊,然后根據(jù)大方塊的位置,查看大方塊和容器是否有重合的地方,邏輯上就是檢測容器和大方塊相同的位置比特位是否同時為 1。這里有個地方需要注意,大方塊的水平位置可能為負(fù),例如下面這種情況:

上圖是 I 形的大方塊,在豎起的狀態(tài)下可能呈現(xiàn)出上面的效果,當(dāng)前這個方塊的列為 -1。事實上你可以通過規(guī)劃大方塊的形狀和位置來避免這類問題,只不過這里沒有這樣做,而是直接將負(fù)數(shù)列作為正常的情況之一。

接下來是碰撞后的合并,操作很簡單就是直接將大方塊的比特位復(fù)印到容器內(nèi)即可,在位運(yùn)算上可以使用或運(yùn)算實現(xiàn)。

//合并

void merge(Block* block, Tetris* tetris)

{

unsigned short blk = gBlockList[block->type][block->state];

for (int i = 0; i < BLOCK_HEIGHT; i++)

{

unsigned short bits = ((blk >> (i * BLOCK_WIDTH)) & 0x000F);

//block->col 可能為負(fù)數(shù)

if (block->col < 0) {

bits >>= (-block->col);

}

else {

bits <<= block->col;

}

? ? ? ? tetris->blockContainer[block->row + i] |= bits;

}

}


6. 操控大方塊

接下來實現(xiàn)大方塊的操控函數(shù),主要有左移、右移、下移、旋轉(zhuǎn)以及掉落。這些其實以及在上一篇文章講過了,這次做的是加上碰撞邏輯,例如當(dāng)左移動的時候:

//左移

int moveLeftBlock(Tetris* tetris)

{

if (!tetris) {

return -1;

}

//當(dāng)前方塊

Block* currBlock = &(tetris->blocks[tetris->blockIndex]);

//移動后的狀態(tài)

Block next = *currBlock;

moveLeft(&next);

//檢測下一狀態(tài)的方塊會發(fā)生碰撞,則取消移動

if (hitTest(&next, tetris)) {

return 0;

}

//沒發(fā)生碰撞,完成移動

moveLeft(currBlock);

return 0;

}

我們首先獲取大方塊的狀態(tài),然后模擬出大方塊左移后的效果,用左移后的方塊做碰撞檢測,如果發(fā)生碰撞,則直接返回,否則將當(dāng)前的方塊左移,整個過程有點類似于投石問路的效果。

其它的操作和左移基本類似,除了下移操作需要在發(fā)生碰撞的時候進(jìn)行合并操作并生成新的方塊:

//下移

int moveDownBlock(Tetris* tetris)

{

if (!tetris) {

return -1;

}

//當(dāng)前方塊

Block* currBlock = &(tetris->blocks[tetris->blockIndex]);

if (moveDownTest(tetris, currBlock))

{

//如果底部碰撞,則將方塊合并到容器中

merge(currBlock, tetris);

//消行

eraseLines(tetris);

//重新生成方塊,并切換當(dāng)前方塊

initRandBlock(currBlock, BLOCK_INIT_POSY, BLOCK_INIT_POSX);

tetris->blockIndex = (tetris->blockIndex + 1) % TETRIS_BLOCK_NUM;

return 1;

}

//沒發(fā)生碰撞,完成移動

moveDown(currBlock);

return 0;

}

這里面還有一個上面沒有說的消行函數(shù),消行本身非常簡單,只需要檢測當(dāng)前容器行是否滿足“滿行”即可,如果滿足,則刪除該行,讓容器其它行依次移動到這里:

//消減行

static void eraseLines(Tetris* tetris)

{

//從下到上,逐步掃描

int line = TETRIS_CONTAINER_HEIGHT - 2;

int afterLine = line;

int eraseLine = 0;

while (line >= BLOCK_HEIGHT)

{

//如果當(dāng)前不滿行

if (0x0FFF != (tetris->blockContainer[line] & 0x0FFF))

{

afterLine--;

}

//記錄消行數(shù)

else

{

eraseLine++;

}

line--;

if (afterLine != line)

{

tetris->blockContainer[afterLine] = tetris->blockContainer[line];

}

}

//剩余設(shè)置為空

while (afterLine >= BLOCK_HEIGHT)

{

tetris->blockContainer[--afterLine] = EMPTY_LINE;

}

}

eraseLine 變量代表最終消行數(shù),你可以用這個值計算一些分?jǐn)?shù)等等。


7. 更新與繪制

完成了周邊的操作函數(shù),接下來就是讓程序自身動起來,這里直接在更新函數(shù)中增加一個不斷更新的下落操作就能實現(xiàn):

//處理游戲邏輯

while (tetris->tick >= tetris->speed) {

// 下落

moveDownBlock(tetris);

tetris->tick -= tetris->speed;

}

tick 變量代表游戲運(yùn)行中的滴答時間,單位是毫秒。而 speed 代表當(dāng)前的下落速度,這個單位也是毫秒,代表經(jīng)過多少毫秒下落一次,更新函數(shù)每次檢測當(dāng)前等待的時間是否大于下落速度,大于則執(zhí)行下落操作。

繪制操作很簡單,只是單純的調(diào)用 SDL 顯示數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)而已。下面是繪制容器的操作:

//繪制容器

for (int i = BLOCK_HEIGHT, r = 0; i < TETRIS_CONTAINER_HEIGHT-1; i++, r++)

{

for (int j = 1, c = 0; j < TETRIS_CONTAINER_WIDTH-1; j++, c++)

{

rtDst.x = c * BLOCK_IMAGE_WIDTH;

rtDst.y = r * BLOCK_IMAGE_HEIGHT;

rtDst.w = BLOCK_IMAGE_WIDTH;

rtDst.h = BLOCK_IMAGE_HEIGHT;

SDL_RenderCopy(pModule->pRenderer,

getResource(RES_TEXTURE), getTileRect(TT_BK), &rtDst);

//當(dāng)前位置有方塊(i,j)

if (tetris->blockContainer[i] & (1 << j))

{

SDL_RenderCopy(pModule->pRenderer,

getResource(RES_TEXTURE), getTileRect(TT_FK), &rtDst);

}

}

//繪制右側(cè)豎線

SDL_Rect rtLineSrc = {0, 0, 5, BLOCK_IMAGE_HEIGHT};

SDL_Rect rtLineDst = { (TETRIS_CONTAINER_WIDTH - 2)*BLOCK_IMAGE_WIDTH+3,

r * BLOCK_IMAGE_HEIGHT, 5, BLOCK_IMAGE_HEIGHT};

SDL_RenderCopy(pModule->pRenderer, getResource(RES_TEXTURE), &rtLineSrc, &rtLineDst);

}

下面是繪制大方塊的操作:

void renderBlock(Block* block, unsigned char alpha, SystemModule* pModule)

{

? ? SDL_Rect rt = { 0, 0, BLOCK_IMAGE_WIDTH, BLOCK_IMAGE_HEIGHT };

? ? for (int i = 0; i < BLOCK_HEIGHT; i++)

? ? {

? ? ? ? for (int j = 0; j < BLOCK_WIDTH; j++)

? ? ? ? {

? ? ? ? ? ? //如果當(dāng)前位置有方塊

? ? ? ? ? ? if ((1 << j << (i * BLOCK_WIDTH)) & (gBlockList[block->type][block->state]))

? ? ? ? ? ? {

rt.x = (block->col + j - 1) * BLOCK_IMAGE_WIDTH;

rt.y = (block->row + i - BLOCK_HEIGHT) * BLOCK_IMAGE_HEIGHT;

SDL_SetTextureAlphaMod(getResource(RES_TEXTURE), alpha);

SDL_RenderCopy(pModule->pRenderer, getResource(RES_TEXTURE), getTileRect(TT_FK), &rt);

SDL_SetTextureAlphaMod(getResource(RES_TEXTURE), 255);

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

整個俄羅斯方塊的基本邏輯就這些,最后再加上一點細(xì)節(jié),例如分?jǐn)?shù),等級、音樂等等。

這樣一個聯(lián)合前面那篇俄羅斯方塊的文章,一個完整的程序就這樣誕生了,你學(xué)會了嗎?沒學(xué)會,又學(xué)到了多少。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容