言植物大戰(zhàn)僵尸源碼:可直接編譯的sln工程與游戲開發(fā)實(shí)戰(zhàn))
簡(jiǎn)介這是一份面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生與C語(yǔ)言初學(xué)者的EasyX圖形編程實(shí)戰(zhàn)項(xiàng)目用C語(yǔ)言配合EasyX圖形庫(kù)實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版“植物大戰(zhàn)僵尸”2D游戲適合課程設(shè)計(jì)、大作業(yè)、畢設(shè)演示或入門進(jìn)階練手。壓縮包共約2000個(gè)文件整體147.38MB其中png與gif構(gòu)成植物、僵尸、場(chǎng)景等圖形素材mp3與ogg提供背景音樂(lè)和音效psd為原始設(shè)計(jì)稿cpp、h與sln、vcxproj等源碼和工程文件可直接編譯運(yùn)行另有exe成品便于快速體驗(yàn)。項(xiàng)目完整實(shí)現(xiàn)了種植植物抵御僵尸、布置道具與技能消滅敵人的核心玩法代碼結(jié)構(gòu)清晰、功能驗(yàn)證穩(wěn)定。已有438人學(xué)習(xí)下載讀者可借此掌握EasyX繪圖、游戲循環(huán)、碰撞檢測(cè)與資源管理等知識(shí)點(diǎn)也可在此基礎(chǔ)上二次開發(fā)DIY出不同玩法與功能。1. 從一份能直接編譯的 EasyX 植物大戰(zhàn)僵尸源碼說(shuō)起很多人第一次接觸 C 語(yǔ)言做游戲卡住的不是語(yǔ)法而是環(huán)境。代碼抄了一堆graphics.h找不到鏈接報(bào)錯(cuò)窗口一閃而過(guò)最后只能放棄。這份「基于 EasyX 和 C 語(yǔ)言開發(fā)的簡(jiǎn)易植物大戰(zhàn)僵尸游戲源碼 sln 解決方案」解決的正是這個(gè)斷層它把圖形庫(kù)、工程配置、游戲主循環(huán)、資源加載全部打包成一個(gè)可以直接用 Visual Studio 打開的 sln 解決方案雙擊編譯就能跑起來(lái)。適合兩類人一類是剛學(xué)完 C 語(yǔ)言基礎(chǔ)、想找一個(gè)完整項(xiàng)目練手的入門者另一類是想用 EasyX 快速驗(yàn)證小游戲玩法、不想從零搭框架的開發(fā)者。它不追求商業(yè)級(jí)品質(zhì)但把「能跑起來(lái)」這件事做到了位而這恰恰是自學(xué)路上最稀缺的正反饋。2. EasyX 與 sln 工程為什么這套組合能省掉一半折騰2.1 EasyX 到底替 C 語(yǔ)言做了什么標(biāo)準(zhǔn) C 語(yǔ)言本身沒(méi)有圖形輸出能力printf只能往控制臺(tái)吐字符。想在 Windows 上畫一個(gè)豌豆射手要么直接調(diào) Win32 GDI要么用 DirectX前者代碼冗長(zhǎng)后者學(xué)習(xí)曲線陡峭。EasyX 是一層薄封裝把 GDI 的窗口創(chuàng)建、消息循環(huán)、繪圖接口壓縮成十幾個(gè)函數(shù)頭文件就是graphics.h鏈接庫(kù)是EasyXa.lib或EasyXw.lib。它保留了 C 語(yǔ)言的調(diào)用風(fēng)格沒(méi)有類、沒(méi)有模板函數(shù)名直白initgraph開窗口loadimage加載圖片putimage貼圖BeginBatchDraw和FlushBatchDraw做雙緩沖。對(duì)已經(jīng)學(xué)過(guò)c語(yǔ)言基礎(chǔ)知識(shí)、能寫數(shù)組和結(jié)構(gòu)體的人來(lái)說(shuō)上手成本幾乎為零。這份源碼里游戲窗口尺寸、幀率控制、鼠標(biāo)點(diǎn)擊檢測(cè)都依賴 EasyX 的 API。比如鼠標(biāo)事件不是通過(guò)回調(diào)而是用GetMouseMsg輪詢配合MOUSEMSG結(jié)構(gòu)體判斷左鍵按下和坐標(biāo)。這種同步輪詢的寫法在小型游戲里足夠用也避免了消息循環(huán)嵌套帶來(lái)的復(fù)雜度。2.2 sln 解決方案為什么比單文件更值得用單文件.c當(dāng)然也能編譯但一旦涉及圖片資源路徑、多文件拆分、庫(kù)目錄配置單文件就會(huì)變成災(zāi)難。sln 是 Visual Studio 的解決方案文件它把項(xiàng)目配置、包含目錄、庫(kù)目錄、鏈接器輸入全部固化下來(lái)。這份源碼附帶 sln意味著你不需要手動(dòng)去「項(xiàng)目屬性 → VC 目錄 → 包含目錄」里填 EasyX 的安裝路徑也不需要手動(dòng)指定EasyXa.lib。打開 sln確認(rèn)平臺(tái)工具集和 Windows SDK 版本匹配直接按 F5 就能生成并運(yùn)行。常見做法是先安裝 EasyX 庫(kù)到 Visual Studio再打開 sln。如果 EasyX 版本和 VS 版本不匹配sln 里的庫(kù)引用會(huì)失效這時(shí)候需要重新在項(xiàng)目屬性里指定庫(kù)文件。源碼包里的 sln 通常對(duì)應(yīng)某個(gè)特定 VS 版本比如 VS2019 或 VS2022用更高版本打開時(shí) VS 會(huì)提示「重定解決方案目標(biāo)」點(diǎn)確認(rèn)即可一般不影響編譯。2.3 把工程跑起來(lái)的最小操作步驟第一步確認(rèn)本機(jī)已安裝 Visual Studio并且勾選了「使用 C 的桌面開發(fā)」工作負(fù)載。第二步安裝 EasyX 庫(kù)安裝程序會(huì)自動(dòng)把graphics.h和庫(kù)文件復(fù)制到 VS 的默認(rèn)搜索路徑。第三步解壓源碼包雙擊.sln文件。第四步在解決方案資源管理器里確認(rèn)項(xiàng)目類型是「控制臺(tái)應(yīng)用」或「Windows 桌面應(yīng)用」字符集建議設(shè)為「使用多字節(jié)字符集」因?yàn)?EasyX 的loadimage和outtextxy在 Unicode 下需要寬字符而這份源碼大概率是按多字節(jié)寫的。# 如果你習(xí)慣用命令行編譯可以用 MSBuild 直接生成 sln # 先找到 VS 的開發(fā)者命令提示符然后進(jìn)入源碼目錄 cd /d D:\PVZ_EasyX msbuild PVZ.sln /p:ConfigurationRelease /p:Platformx86 # 生成成功后在 Release 目錄下找到 exe雙擊運(yùn)行這段命令的邏輯是msbuild讀取 sln 里的項(xiàng)目配置按 Release 模式編譯平臺(tái)選 x86 是因?yàn)?EasyX 的 32 位庫(kù)兼容性最好。參數(shù)/p:ConfigurationRelease會(huì)關(guān)閉調(diào)試符號(hào)并開啟優(yōu)化適合最終運(yùn)行如果你要斷點(diǎn)調(diào)試改成Debug。注意命令行編譯要求 EasyX 的庫(kù)路徑已經(jīng)寫入系統(tǒng)環(huán)境變量或 sln 里配置了絕對(duì)路徑否則會(huì)報(bào)LNK1104: 無(wú)法打開文件 EasyXa.lib。3. 游戲主循環(huán)與核心模塊從地圖繪制到僵尸波次3.1 主循環(huán)的骨架與幀率控制EasyX 游戲的主循環(huán)通常長(zhǎng)這樣一個(gè)while循環(huán)里面依次做「清屏或重繪背景 → 更新游戲邏輯 → 繪制所有對(duì)象 → 刷新屏幕」。這份源碼里幀率控制靠Sleep函數(shù)比如Sleep(16)大約對(duì)應(yīng) 60 幀每秒。更精細(xì)的做法是用GetTickCount計(jì)算時(shí)間差但簡(jiǎn)易項(xiàng)目用固定延時(shí)足夠。// 游戲主循環(huán)偽代碼基于 EasyX initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); // 創(chuàng)建游戲窗口 BeginBatchDraw(); // 開啟雙緩沖避免閃爍 while (gameRunning) { cleardevice(); // 清屏 drawBackground(); // 繪制草坪和卡片欄 updatePlants(); // 更新植物狀態(tài)冷卻、攻擊、產(chǎn)陽(yáng)光 updateZombies(); // 更新僵尸移動(dòng)、啃咬、死亡 updateBullets(); // 更新子彈位置與碰撞 drawAll(); // 統(tǒng)一繪制所有對(duì)象 FlushBatchDraw(); // 把緩沖區(qū)內(nèi)容一次性刷到屏幕 Sleep(16); // 控制幀率約 60 FPS } EndBatchDraw(); closegraph();邏輯說(shuō)明BeginBatchDraw和FlushBatchDraw必須成對(duì)使用否則畫面會(huì)嚴(yán)重閃爍。cleardevice每幀清屏如果背景是靜態(tài)圖片可以改成只重繪變化區(qū)域來(lái)省性能。Sleep(16)是簡(jiǎn)易做法實(shí)際幀間隔會(huì)略大于 16 毫秒因?yàn)槔L圖本身耗時(shí)。參數(shù)WINDOW_WIDTH和WINDOW_HEIGHT在源碼里通常是宏定義比如 900×600對(duì)應(yīng)經(jīng)典 PVZ 的橫向布局。3.2 植物、僵尸、子彈的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)C 語(yǔ)言做游戲結(jié)構(gòu)體是核心。這份源碼里植物、僵尸、子彈各用一個(gè)結(jié)構(gòu)體數(shù)組管理數(shù)組大小固定比如Plant plants[MAX_PLANTS]。每個(gè)結(jié)構(gòu)體包含坐標(biāo)、狀態(tài)、生命值、冷卻計(jì)時(shí)等字段。typedef struct { int x, y; // 格子坐標(biāo)或像素坐標(biāo) int type; // 植物類型0 向日葵1 豌豆射手2 堅(jiān)果墻 int hp; // 生命值 int cooldown; // 攻擊冷卻剩余幀數(shù) int active; // 是否存活 } Plant; typedef struct { int x, y; int hp; int speed; // 每幀移動(dòng)像素 int eating; // 是否正在啃咬植物 int active; } Zombie; typedef struct { int x, y; int row; // 所在行用于碰撞檢測(cè) int damage; int active; } Bullet;邏輯說(shuō)明用active標(biāo)記對(duì)象是否存活比動(dòng)態(tài)內(nèi)存分配簡(jiǎn)單也避免了c語(yǔ)言內(nèi)存管理里的釋放問(wèn)題。僵尸的row字段和植物的格子行對(duì)應(yīng)子彈碰撞檢測(cè)時(shí)只需比較行號(hào)和 x 坐標(biāo)范圍。參數(shù)MAX_PLANTS和MAX_ZOMBIES決定了游戲上限設(shè)太小會(huì)提前刷不出怪設(shè)太大浪費(fèi)內(nèi)存一般 50 到 100 足夠。3.3 陽(yáng)光收集與卡片冷卻的實(shí)現(xiàn)細(xì)節(jié)陽(yáng)光是 PVZ 的經(jīng)濟(jì)系統(tǒng)。源碼里陽(yáng)光以對(duì)象形式下落鼠標(biāo)點(diǎn)擊后增加sunCount??ㄆ鋮s用幀計(jì)數(shù)實(shí)現(xiàn)每張卡片有一個(gè)cooldown變量種下植物后設(shè)為最大冷卻幀數(shù)每幀減一減到零才能再次種植。// 陽(yáng)光點(diǎn)擊檢測(cè)與收集 MOUSEMSG msg; while (MouseHit()) { msg GetMouseMsg(); if (msg.uMsg WM_LBUTTONDOWN) { for (int i 0; i MAX_SUNS; i) { if (suns[i].active msg.x suns[i].x msg.x suns[i].x SUN_SIZE msg.y suns[i].y msg.y suns[i].y SUN_SIZE) { sunCount 25; suns[i].active 0; } } } }邏輯說(shuō)明MouseHit判斷是否有鼠標(biāo)消息GetMouseMsg取出消息。點(diǎn)擊檢測(cè)用矩形范圍判斷SUN_SIZE是陽(yáng)光圖標(biāo)的像素寬度。參數(shù)25是單個(gè)陽(yáng)光的價(jià)值源碼里可能定義成宏SUN_VALUE。注意這段代碼必須放在主循環(huán)里每幀調(diào)用否則鼠標(biāo)點(diǎn)擊會(huì)丟失。如果發(fā)現(xiàn)點(diǎn)擊沒(méi)反應(yīng)先檢查MouseHit是否被其他消息處理函數(shù)消耗了。4. 編譯運(yùn)行與資源加載那些讓你卡半天的坑4.1 圖片資源路徑與工作目錄EasyX 的loadimage默認(rèn)從當(dāng)前工作目錄找圖片。在 Visual Studio 里按 F5 運(yùn)行時(shí)工作目錄是項(xiàng)目目錄而不是 exe 所在目錄。所以源碼里的圖片文件夾通常放在項(xiàng)目根目錄下比如res/或images/。如果你直接雙擊 exe 運(yùn)行工作目錄變成 exe 所在目錄圖片就找不到了游戲會(huì)黑屏或報(bào)錯(cuò)。解決辦法有兩個(gè)一是在代碼里用絕對(duì)路徑但這樣換電腦就廢了二是用GetModuleFileName獲取 exe 路徑再拼接相對(duì)路徑。常見做法是后者但簡(jiǎn)易源碼往往直接用相對(duì)路徑所以建議在 VS 里運(yùn)行或者把圖片文件夾復(fù)制到 exe 旁邊。4.2 字符集與 Unicode 的翻車現(xiàn)場(chǎng)EasyX 的outtextxy和loadimage在 Unicode 字符集下需要寬字符串比如Lres/peashooter.png。如果源碼是按多字節(jié)寫的而你的 VS 項(xiàng)目默認(rèn)是 Unicode編譯會(huì)報(bào)「無(wú)法從 const char* 轉(zhuǎn)換為 LPCWSTR」。這時(shí)候去「項(xiàng)目屬性 → 高級(jí) → 字符集」改成「使用多字節(jié)字符集」即可。反過(guò)來(lái)如果源碼用了_T()宏兩種字符集都能兼容但簡(jiǎn)易項(xiàng)目很少這么講究。4.3 鏈接器報(bào)錯(cuò) LNK2019 與 LNK1104 的排查順序LNK1104: 無(wú)法打開文件 EasyXa.lib說(shuō)明庫(kù)文件沒(méi)找到。先確認(rèn) EasyX 是否安裝成功再檢查項(xiàng)目屬性里的「庫(kù)目錄」是否包含 EasyX 的 lib 路徑。LNK2019: 無(wú)法解析的外部符號(hào)通常是函數(shù)聲明了但沒(méi)鏈接庫(kù)比如用了initgraph卻沒(méi)加EasyXa.lib。在代碼里加#pragma comment(lib, EasyXa.lib)可以強(qiáng)制鏈接但前提是庫(kù)路徑正確。5. 避坑與常見問(wèn)題血淚經(jīng)驗(yàn)換來(lái)的五條記錄現(xiàn)象游戲窗口一閃而過(guò)什么都沒(méi)看到。原因initgraph之后沒(méi)有主循環(huán)或者主循環(huán)條件一開始就是 false。解決檢查gameRunning的初始值確保while循環(huán)能進(jìn)入并在循環(huán)末尾加Sleep防止 CPU 占滿。現(xiàn)象圖片加載失敗窗口全黑或只有色塊。原因工作目錄不對(duì)或者圖片格式不被 EasyX 支持。EasyX 支持 bmp、jpg、png、gif但 png 需要額外庫(kù)。解決把圖片轉(zhuǎn)成 bmp 格式或者確認(rèn) EasyX 版本支持 png。同時(shí)用GetWorkingDirectory打印當(dāng)前目錄確認(rèn)路徑?,F(xiàn)象鼠標(biāo)點(diǎn)擊沒(méi)反應(yīng)或者點(diǎn)擊位置偏移。原因GetMouseMsg的坐標(biāo)是相對(duì)于窗口客戶區(qū)的如果窗口有標(biāo)題欄和邊框坐標(biāo)原點(diǎn)在客戶區(qū)左上角不是屏幕左上角。解決用msg.x和msg.y直接判斷不要加窗口偏移。如果偏移檢查是否用了GetCursorPos而不是GetMouseMsg?,F(xiàn)象僵尸移動(dòng)速度忽快忽慢或者子彈穿模。原因幀率不穩(wěn)定Sleep(16)受系統(tǒng)調(diào)度影響。解決改用時(shí)間差計(jì)算移動(dòng)距離比如deltaTime GetTickCount() - lastTick然后x speed * deltaTime / 16。這樣即使幀率波動(dòng)移動(dòng)速度也一致?,F(xiàn)象編譯通過(guò)但運(yùn)行崩潰提示訪問(wèn)沖突。原因數(shù)組越界比如僵尸數(shù)量超過(guò)MAX_ZOMBIES還在往數(shù)組里寫。解決在生成僵尸前檢查active數(shù)量或者用動(dòng)態(tài)數(shù)組。簡(jiǎn)易項(xiàng)目里把MAX_ZOMBIES設(shè)大一點(diǎn)并在寫入前加if (index MAX_ZOMBIES)判斷。6. 進(jìn)階技巧用狀態(tài)機(jī)和對(duì)象池把簡(jiǎn)易框架撐大這份源碼的架構(gòu)是「數(shù)組 標(biāo)志位」夠用但不夠優(yōu)雅。如果你想在此基礎(chǔ)上加新植物、新僵尸或者做更復(fù)雜的關(guān)卡有兩個(gè)技巧值得試。第一個(gè)是狀態(tài)機(jī)。僵尸不只是「走」和「吃」還有「被冰凍」「被減速」「死亡動(dòng)畫」。用enum定義狀態(tài)在updateZombies里用switch分支處理比一堆if-else清晰得多。typedef enum { ZOMBIE_WALKING, ZOMBIE_EATING, ZOMBIE_FROZEN, ZOMBIE_DYING } ZombieState; void updateZombie(Zombie *z) { switch (z-state) { case ZOMBIE_WALKING: z-x - z-speed; if (checkCollisionWithPlant(z)) { z-state ZOMBIE_EATING; } break; case ZOMBIE_EATING: z-eatTimer; if (z-eatTimer EAT_INTERVAL) { damagePlant(z); z-eatTimer 0; } if (!checkCollisionWithPlant(z)) { z-state ZOMBIE_WALKING; } break; case ZOMBIE_FROZEN: z-freezeTimer--; if (z-freezeTimer 0) { z-state ZOMBIE_WALKING; } break; case ZOMBIE_DYING: z-deathTimer; if (z-deathTimer DEATH_ANIM_FRAMES) { z-active 0; } break; } }邏輯說(shuō)明每個(gè)狀態(tài)只關(guān)心自己的邏輯狀態(tài)切換由條件觸發(fā)。參數(shù)EAT_INTERVAL控制啃咬速度DEATH_ANIM_FRAMES控制死亡動(dòng)畫時(shí)長(zhǎng)。這種寫法比在一個(gè)大函數(shù)里堆if更容易擴(kuò)展。第二個(gè)是對(duì)象池。數(shù)組固定大小就是最簡(jiǎn)單的對(duì)象池但你可以把「查找空閑槽位」封裝成函數(shù)避免每次生成對(duì)象都寫一遍循環(huán)。int findFreeSlot(Plant plants[], int max) { for (int i 0; i max; i) { if (!plants[i].active) return i; } return -1; // 沒(méi)有空閑槽位 }調(diào)用時(shí)先int slot findFreeSlot(plants, MAX_PLANTS);如果返回 -1 就跳過(guò)生成。這樣代碼更干凈也方便以后換成動(dòng)態(tài)擴(kuò)容。驗(yàn)證方法很簡(jiǎn)單加一個(gè)新植物「寒冰射手」復(fù)用豌豆射手的結(jié)構(gòu)體在子彈命中僵尸時(shí)給僵尸加ZOMBIE_FROZEN狀態(tài)觀察僵尸是否減速。如果減速生效說(shuō)明狀態(tài)機(jī)和對(duì)象池都工作正常。我自己的習(xí)慣是每加一個(gè)新功能先跑一遍原有流程確認(rèn)沒(méi)破壞舊邏輯再提交。這樣即使翻車也能快速回滾。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取