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é)到了多少。