在完成 CHIP-8 的系統初始化與內建字元集後,接下來就要開始建立真正能顯示畫面的螢幕模組啦!這一階段會先使用布林二維陣列保存 64x32 的像素狀態,再加入像素設定、讀取功能。最後,透過 SDL 將開啟的像素依照視窗倍率放大並繪製到畫面上,讓模擬器首次具備完整的螢幕資料儲存與顯示能力。
螢幕資料結構
為了在模擬器中表示 CHIP-8 的螢幕,我們需要建立一個新的標頭檔 chip8_screen.h。
由於 CHIP-8 是單色顯示,螢幕上的像素點只有「開啟」與「關閉」兩種狀態,因此我們可以引入 <stdbool.h>,並在螢幕結構體中宣告一個布林二維陣列來代表像素。
陣列的寬度與高度,則直接引用設定檔中定義的 CHIP8_WIDTH 與 CHIP8_HEIGHT。
struct chip8_screen
{
bool pixels[CHIP8_HEIGHT][CHIP8_WIDTH];
};
這裡將高度放在第一個索引、寬度放在第二個索引,因此後續存取像素時會使用:screen->pixels[y][x]。
建立完成後,記得將螢幕結構加入主要的 chip8 結構體中:
struct chip8
{
...
struct chip8_screen screen;
};
實作像素存取與邊界防護
設定與讀取函式
建立好資料結構後,我們在 chip8_screen.c 中實作兩個核心函式:
- 設定像素:
chip8_screen_set接收 X 與 Y 座標,並將陣列中對應位置的像素設為開啟狀態,也就是true。 - 檢查狀態:
chip8_screen_is_set回傳指定 X 與 Y 座標的像素狀態,讓主程式判斷該位置是否有像素亮起。
#include "chip8_screen.h"
#include <assert.h>
void chip8_screen_set(struct chip8_screen* screen, int x, int y)
{
screen->pixels[y][x] = true;
}
bool chip8_screen_is_set(struct chip8_screen* screen, int x, int y)
{
return screen->pixels[y][x];
}
邊界檢查
和先前的記憶體與鍵盤模組相同,我們引入 <assert.h>,並建立一個名為 chip8_screen_check_bounds 的靜態函式。
static void chip8_screen_check_bounds(int x, int y)
{
assert(x >= 0 && x < CHIP8_WIDTH && y >= 0 && y < CHIP8_HEIGHT);
}
這個函式會檢查傳入的座標是否位於螢幕範圍內:
- X 必須大於或等於
0,且小於CHIP8_WIDTH。 - Y 必須大於或等於
0,且小於CHIP8_HEIGHT。
接著,將邊界檢查加入像素設定與讀取函式中,確保所有像素存取都位於合法範圍內。
#include "chip8_screen.h"
#include <assert.h>
void chip8_screen_set(struct chip8_screen* screen, int x, int y)
{
chip8_screen_check_bounds(x, y); // 加入檢查
screen->pixels[y][x] = true;
}
bool chip8_screen_is_set(struct chip8_screen* screen, int x, int y)
{
chip8_screen_check_bounds(x, y); // 加入檢查
return screen->pixels[y][x];
}
主程式整合與 SDL 渲染機制
完成螢幕模組並將其新增至編譯清單後,我們就可以回到主程式,改寫原本的 SDL 渲染邏輯。
每次準備繪製新的畫面時,都需要先清除上一幀的內容。因此,背景清除與繪製顏色設定應放在主迴圈內,但位於雙重像素迴圈之外。
基本流程如下:
清除上一幀畫面 -> 設定像素繪製顏色 -> 遍歷 CHIP-8 螢幕像素 -> 繪製開啟的像素 -> 呈現完整畫面
雙重迴圈走訪陣列
在主迴圈中,我們使用兩個 for 迴圈走訪整個螢幕陣列。
X 座標的範圍為 0 到 CHIP8_WIDTH - 1,Y 座標的範圍則為 0 到 CHIP8_HEIGHT - 1。
在迴圈內呼叫 chip8_screen_is_set。如果該位置的像素處於開啟狀態,就執行對應的繪製操作。
for (int x = 0; x < CHIP8_WIDTH; x++)
{
for (int y = 0; y < CHIP8_HEIGHT; y++)
{
if (chip8_screen_is_set(&chip8.screen, x, y))
{
...
}
}
}
視窗放大倍率(Multiplier)的應用
CHIP-8 的原始解析度只有 64x32。如果直接按照一比一的比例呈現在現代桌面螢幕上,畫面會非常小,幾乎無法辨識。
因此,我們需要使用先前設定的視窗放大倍率(Multiplier),將每個 CHIP-8 像素繪製成較大的矩形。
當放大倍率為 10 時,CHIP-8 的一個像素會被繪製成 10x10 的矩形,原始 X 與 Y 座標都需要乘上放大倍率,64x32 的顯示區域會被放大為 640x320。
完整邏輯如下:
for (int x = 0; x < CHIP8_WIDTH; x++)
{
for (int y = 0; y < CHIP8_HEIGHT; y++)
{
if (chip8_screen_is_set(&chip8.screen, x, y))
{
SDL_Rect r;
r.x = x * CHIP8_WINDOW_MULTIPLIER;
r.y = y * CHIP8_WINDOW_MULTIPLIER;
r.w = CHIP8_WINDOW_MULTIPLIER;
r.h = CHIP8_WINDOW_MULTIPLIER;
SDL_RenderFillRect(renderer, &r);
}
}
}
將清除畫面與呈現畫面的流程一併加入後,渲染部分大致如下:
...
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 0);
for (int x = 0; x < CHIP8_WIDTH; x++)
{
...
}
SDL_RenderPresent(renderer);
...
每次主迴圈更新畫面時,都會先清除上一幀,再根據 chip8.screen 中保存的像素狀態重新繪製畫面。
系統測試
為了驗證實作是否正確,我們可以在主程式中初始化 CHIP-8 實例後,手動呼叫 chip8_screen_set,將座標 (40, 20) 的像素設為開啟狀態。
int main(int argc, char** argv)
{
...
chip8_screen_set(&chip8.screen, 40, 20);
...
}
當放大倍率為 10 時,這個像素會被繪製在視窗座標 (400, 200),並顯示為一個 10x10 的矩形。

重新編譯並執行程式後,如果能在視窗中的正確位置看到放大後的像素方塊,就表示螢幕儲存陣列、像素存取函式與 SDL 渲染機制都已經成功整合,可以準備進入下一個階段了!




