在開發 CHIP-8 emulator 前,我們除了背景知識外,還需要先進行架構規劃以及引用需要的函式庫。除了專案架構應該大同小異外,這次專案引入 Simple DirectMedia Layer,能讓開發者輕鬆建立視窗並將像素繪製到畫面上,以達到顯示遊戲畫面之目的。
以下說明是建立在開發環境作業系統是 Windows 11 之下,其他的系統有些許差異。
專案架構以及目錄
目錄結構
先簡單拉個基礎架構,建立以下資料夾來妥善分類檔案:
bin:用來存放最終編譯出來的執行檔二進位檔案。build:存放編譯過程中產生的物件檔(Object Files)。include:放置所有的標頭檔。src:放置 C 語言原始碼 (如main.c)。lib:用來放置外部函式庫檔案。
看起來跟一般的 C 語言專案差不多吧XDD 所以才說大同小異。
在程式碼架構方面,我們會在 include 內建立一個 chip8.h 檔案,用來定義代表 CHIP-8 狀態的資料結構(包含模擬器的記憶體、暫存器、螢幕像素與鍵盤等)。
CHIP-8 硬體規格與設定檔管理
還記得為了這個專案我們先認識了 CHIP-8 硬體規格嗎?
為了避免將寫死的數值散落在程式碼各處,我們會建立一個 config.h 設定檔統一管理靜態資訊。關於這塊的詳細背景資訊可以依前篇文章《》規格進行設計。
以下用記憶體以及顯示螢幕舉例:
- 記憶體:CHIP-8 語言最多可存取 4KB 的 RAM,位址範圍從
0x000到0xFFF。我們可以在設定檔中定義記憶體大小常數為 4096。 - 顯示螢幕:CHIP-8 的原生顯示解析度極低,只有 64×32 像素。如果將原生尺寸直接呈現在現代桌面螢幕上,畫面會小到幾乎看不見。為了解決這個問題,我們必須設定一個「視窗放大倍率」(Window Multiplier),例如設為 10。透過將寬高乘以倍率,我們能將視窗放大 10 倍,最終得到一個
640x320像素的視窗,這就像放大圖片一樣,確保玩家能清楚看到畫面。設定檔中也能順便定義視窗的標題,讓後續修改更乾淨俐落。
諸如這些資料就會被寫在 config.h 內。
SDL 函式庫整合與編譯環境設定
SDL(Simple DirectMedia Layer)是一個強大的圖形函式庫,能讓開發者將像素繪製到畫面上,非常符合模擬器開發的需求。
進入官網下載 SDL 開發者版本(Development Libraries)後,需將函式庫整合進專案。
- 將 SDL 的標頭檔 (
include) 複製到專案的include目錄。 - 將
.lib或.a等函式庫檔案複製到專案的lib目錄。 - 確保動態連結檔
SDL2.dll放置於bin資料夾內,也就是執行檔所在的工作目錄中。
整合完的目錄長類似這樣:
Chip-8-Emulator/
├── Makefile
│
├── bin/
│ ├── SDL2.dll
│ └── main.exe
│
├── include/
│ └── SDL2/
│ ├── SDL.h
│ ├── SDL_main.h
│ ├── SDL_events.h
│ ├── SDL_render.h
│ ├── SDL_rect.h
│ ├── SDL_timer.h
│ ├── SDL_video.h
│ └── ...
│
├── lib/
│ ├── libSDL2.a
│ ├── libSDL2.dll.a
│ ├── libSDL2.la
│ ├── libSDL2_test.a
│ ├── libSDL2_test.la
│ ├── libSDL2main.a
│ └── libSDL2main.la
│
└── src/
└── main.c
在編譯階段(例如透過 MinGW32 自行撰寫 Makefile),必須透過 -L./lib 告訴編譯器尋找函式庫的路徑,並且使用 -lmingw32 -lSDL2main -lSDL2 的指令來正確連結 SDL 函式庫。
INCLUDES= -I ./include
FLAGS= -g
all:
gcc ${FLAGS} ${INCLUDES} ./src/main.c -L ./lib -lmingw32 -lSDL2main -lSDL2 -o ./bin/main
小試一下是否能正常運作
事件處理與資源管理
視窗建立後如果沒有持續處理操作,程式會立刻結束或是造成視窗沒有回應。因此,我們必須撰寫一個無窮迴圈 while(1) 來維持程式運行,也就是我們現代軟體開發熟悉的 Event Polling。
在迴圈內,我們呼叫 SDL_PollEvent 持續捕捉系統事件(例如滑鼠點擊或鍵盤按壓)。其中最重要的是處理關閉視窗的事件(SDL_QUIT)。
while(1)
{
SDL_Event event;
while(SDL_PollEvent(&event))
{
if(event.type == SDL_QUIT)
{
goto out;
}
}
// 保留後續補充
}
當使用者點擊視窗上的 X 按鈕時,觸發此事件,我們可以使用 C 語言的 goto 語法俐落地跳出迴圈。在程式結束前,務必呼叫 SDL_DestroyWindow 釋放視窗與記憶體資源,確保系統維持穩定。
初始化視窗與繪製圖形
在 main.c 檔案中,首先呼叫 SDL_Init(SDL_INIT_EVERYTHING) 初始化 SDL 的所有子系統。 接著使用 SDL_CreateWindow 函式,並帶入先前設定的標題與放大後的寬高尺寸(640×320)來建立視窗。
為了在畫面上繪製圖形,需要呼叫 SDL_CreateRenderer 建立渲染器(Renderer)。繪製的流程如下:
- 清空畫面:使用
SDL_SetRenderDrawColor將顏色設為黑色(RGB皆為 0),並透過SDL_RenderClear塗滿整個視窗背景。 - 繪製矩形:定義一個
SDL_Rect指定繪製座標與寬高。將渲染顏色更改為綠色或白色以免圖形融入黑色背景中,接著呼叫SDL_RenderFillRect畫出實心矩形。 - 呈現畫面:最後,必須呼叫
SDL_RenderPresent將這些修改真實呈現到使用者的視窗上。
主程式類似這樣:
#include <stdio.h>
#include "SDL2/SDL.h"
#include "SDL2/SDL_main.h"
int main(int argc, char** argv)
{
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Window* window = SDL_CreateWindow(
"Chip8 Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640, 320,
SDL_WINDOW_SHOWN
);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
while(1)
{
SDL_Event event;
while(SDL_PollEvent(&event))
{
if(event.type == SDL_QUIT)
{
goto out;
}
}
// 清空畫面(用黑色) -> 繪製矩形 -> 呈現畫面
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 0);
SDL_Rect r;
r.x = 0;
r.y = 0;
r.w = 40;
r.h = 40;
SDL_RenderFillRect(renderer, &r);
SDL_RenderPresent(renderer);
}
out:
SDL_DestroyWindow(window);
return 0;
}
後記
留給自己看的修訂紀錄:
原本在 renderer 初始化那段是 SDL_CreateRenderer(window, -1, SDL_TEXTUREACCESS_TARGET);
然而 SDL_TEXTUREACCESS_TARGET 是建立 Texture 時使用的存取模式,不是 Renderer 的建立旗標。這裡後來改成 SDL_RENDERER_ACCELERATED,雖然最後都只是整數常數,因此編譯器不會報錯,但語意上來說應該放錯地方了。
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
而 SDL_TEXTUREACCESS_TARGET 的意思是建立一張可以被當成繪圖目標的 Texture。
例如先把多個圖形畫進 Texture,再一次把整張 Texture 畫到視窗:
SDL_Texture *texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
640,
320
);
SDL_SetRenderTarget(renderer, texture);
/* 後續繪圖會畫到 texture,而不是直接畫到視窗。 */
SDL_SetRenderTarget(renderer, NULL);
/* 再將 texture 畫到視窗。 */
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);






