C++實現(xiàn)三國殺核心邏輯:事件驅動架構與面向對象設計實踐
1. 項目概述為什么選擇C重寫三國殺作為一個玩了十幾年桌游、也寫了十幾年代碼的老程序員我一直對《三國殺》這款游戲情有獨鐘。它的魅力在于將武將技能、卡牌效果和玩家決策精巧地編織在一起形成了一個動態(tài)、復雜的博弈系統(tǒng)。市面上雖然有各種現(xiàn)成的客戶端但作為一個技術人總想親手“拆開”看看里面的齒輪是怎么咬合的。用C來實現(xiàn)是我深思熟慮后的選擇。首先C的“零成本抽象”特性讓我們在構建游戲核心邏輯這種對性能有要求的系統(tǒng)時既能保持代碼的結構清晰又不用擔心像某些高級語言那樣引入過多的運行時開銷。游戲的核心循環(huán)——判定、響應、結算——每秒鐘可能發(fā)生成百上千次C的高效執(zhí)行至關重要。其次面向對象的設計模式OOP與卡牌游戲的“萬物皆對象”理念天然契合。一張“殺”牌、一個“關羽”武將、一次“閃”的響應都可以被優(yōu)雅地抽象為類通過繼承和多態(tài)來管理它們之間錯綜復雜的關系。最后這是一次絕佳的練兵機會能深入實踐設計模式、狀態(tài)機、事件驅動等中大型項目必備的架構思想。這個項目我們將完全從零開始不依賴任何游戲引擎僅使用標準庫和少量第三方庫如用于網絡通信的asio但本文核心聚焦單機邏輯目標是構建一個可運行、可擴展、代碼結構清晰的三國殺核心邏輯框架。無論你是想深入學習C面向對象設計還是對游戲邏輯實現(xiàn)感興趣亦或是想挑戰(zhàn)一個中等復雜度的綜合項目這篇文章都將為你提供一條清晰的路徑。2. 核心架構設計如何用C抽象游戲世界在動手寫第一行代碼之前我們必須先搭好骨架。一個混亂的架構會讓后續(xù)的卡牌效果、技能聯(lián)動變成一場災難。我的設計核心是“事件驅動”和“組件化”。2.1 核心類關系圖與職責劃分整個游戲世界由幾個核心類構成它們的關系可以用以下方式理解Game游戲控制器單例模式的最佳實踐場景。它是游戲的總指揮持有游戲狀態(tài)回合、階段、玩家列表并驅動整個游戲循環(huán)Loop。它也是事件總線Event Bus的中心所有游戲內動作都轉化為事件Event由它派發(fā)。Player玩家代表一名游戲參與者。它應該是一個“瘦”對象主要包含身份、血量、手牌區(qū)、裝備區(qū)、判定區(qū)等數(shù)據(jù)。其行為如出牌、發(fā)動技能大多通過持有的PlayerController玩家控制器來觸發(fā)這樣便于實現(xiàn)AI將Controller替換為AI邏輯和網絡層將Controller替換為網絡消息處理器的擴展。Card卡牌所有卡牌的基類。它包含卡牌的基本屬性花色、點數(shù)、名稱、類型基本牌、錦囊牌、裝備牌。這里的關鍵是使用多態(tài)。我們定義一個虛函數(shù)bool use(Game* game, Player* source, std::vectorPlayer* targets)。那么“殺”、“桃”、“過河拆橋”這些具體卡牌類都繼承自Card并重寫use方法來實現(xiàn)各自的效果邏輯。Effect效果/技能這是實現(xiàn)復雜邏輯的“魔法”。我們將游戲內發(fā)生的任何事情都視為“效果”例如“造成1點傷害”、“摸兩張牌”、“獲得一個技能”。Effect是一個可執(zhí)行、可逆轉為了處理“無懈可擊”等的動作單元。武將技能和卡牌效果在觸發(fā)時都會生成相應的Effect實例提交給Game去執(zhí)行和結算。Event事件游戲內狀態(tài)變化的通知。例如CardUsedEvent卡牌使用、DamageEvent傷害造成前/后、PhaseEvent階段開始/結束時。采用觀察者模式任何對象如武將技能都可以監(jiān)聽Subscribe感興趣的事件并在事件發(fā)生時觸發(fā)對應的技能邏輯。這是實現(xiàn)技能聯(lián)動的基石。設計心得千萬不要讓Player類變成一個“上帝類”。我曾見過新手實現(xiàn)時把洗牌、判定、傷害計算等所有邏輯都塞進Player里導致類膨脹到幾千行難以維護。務必遵循單一職責原則。2.2 關鍵數(shù)據(jù)結構選型C標準庫為我們提供了強大的容器選擇合適的數(shù)據(jù)結構能事半功倍。牌堆、棄牌堆、手牌使用std::vectorCard*或std::dequeCard*。vector連續(xù)內存隨機訪問如“觀星”看牌堆頂?shù)腘張牌效率高但中間插入刪除慢??紤]到主要操作是牌堆頂?shù)拿坪蜅壟贫秧數(shù)奶砑觗eque雙端隊列在頭尾操作上性能更優(yōu)且也支持一定程度的隨機訪問是更合適的選擇。必須注意內存管理。由于存放的是指針我們需要在析構函數(shù)或游戲結束時手動遍歷容器并delete每一張牌或者更現(xiàn)代地使用std::vectorstd::unique_ptrCard利用智能指針自動管理生命周期。玩家列表使用std::vectorPlayer*或std::listPlayer*。游戲過程中玩家順序座次是固定的但經常需要按順序遍歷如從當前回合玩家開始逆時針結算。vector的遍歷緩存友好效率極高。雖然玩家陣亡需要“移除”但我們通常不真正刪除元素而是將其標記為“死亡”is_alive狀態(tài)置為false在遍歷時跳過即可。因此vector是首選。技能/事件映射使用std::unordered_map。為了快速根據(jù)事件類型找到所有監(jiān)聽它的技能我們需要一個映射std::unordered_mapEventType, std::vectorSkill*。unordered_map基于哈希表平均O(1)的查找速度非常適合這種高頻查詢。// 示例事件系統(tǒng)核心結構簡析 class EventBus { private: std::unordered_mapEventType, std::vectorstd::functionvoid(const Event) listeners_; public: void subscribe(EventType type, std::functionvoid(const Event) handler) { listeners_[type].push_back(handler); } void publish(const Event event) { auto it listeners_.find(event.type()); if (it ! listeners_.end()) { for (auto handler : it-second) { handler(event); // 觸發(fā)所有監(jiān)聽該事件的處理器 } } } };3. 核心流程實現(xiàn)游戲循環(huán)與回合引擎游戲的核心是一個狀態(tài)機驅動著“回合-階段-操作”的循環(huán)。這個循環(huán)必須穩(wěn)定、清晰且易于調試。3.1 游戲主循環(huán)與狀態(tài)遷移主循環(huán)不是簡單的while(1)它必須能優(yōu)雅地處理等待玩家輸入、網絡延遲、動畫播放如果以后有UI等情況。一個基于狀態(tài)的設計如下class Game { public: enum class State { INIT, PLAYING, ROUND_START, TURN_START, ... , GAME_OVER }; void run() { state_ State::INIT; initializeGame(); // 初始化身份、發(fā)牌等 state_ State::PLAYING; while (state_ ! State::GAME_OVER) { switch (state_) { case State::ROUND_START: onRoundStart(); state_ State::TURN_START; break; case State::TURN_START: onTurnStart(current_player_); state_ State::PHASE_JUDGE; // 進入判定階段 break; case State::PHASE_JUDGE: resolveJudgement(current_player_); state_ State::PHASE_DRAW; break; case State::PHASE_DRAW: current_player_-drawCards(2); state_ State::PHASE_PLAY; break; case State::PHASE_PLAY: // 這里是關鍵進入玩家自由出牌階段。 // 我們需要一個子狀態(tài)機或者通過異步回調來處理玩家的每張牌。 waitForPlayerAction(current_player_); // 當玩家點擊“結束出牌”或無法/不想再出牌時跳出此階段 state_ State::PHASE_DISCARD; break; case State::PHASE_DISCARD: checkHandCardLimit(current_player_); state_ State::TURN_END; break; case State::TURN_END: onTurnEnd(current_player_); moveToNextPlayer(); state_ State::TURN_START; break; } checkGameOver(); // 每輪循環(huán)后檢查游戲是否結束 } } private: State state_; Player* current_player_; // ... 其他成員 };避坑指南PHASE_PLAY出牌階段的實現(xiàn)是最復雜的。它不是一個瞬間完成的狀態(tài)而是一個等待期。在控制臺版本我們可以用阻塞式輸入但在未來面向事件驅動的UI或網絡版中這里必須改為非阻塞的。我的做法是在進入PHASE_PLAY時向該玩家的PlayerController發(fā)送一個“請開始你的回合”的事件。然后游戲主循環(huán)或另一個事件循環(huán)繼續(xù)運行等待PlayerController提交一個“結束出牌”的動作請求。這需要將游戲狀態(tài)機設計為可被事件中斷和驅動的。3.2 卡牌使用與結算鏈這是游戲邏輯的精華所在。以一張【殺】為例其使用和結算是一個嚴謹?shù)逆湕l合法性檢查來源玩家是否處于“出牌階段”是否已有“殺”的使用限制是否在攻擊范圍內目標是否合法例如是否有“空城”技能聲明使用通過Game::useCard(Card* card, Player* source, std::vectorPlayer* targets)函數(shù)。此函數(shù)會創(chuàng)建一個CardUseEvent并發(fā)布。響應時機牌堆頂在卡牌效果生效前插入一個“響應時機”。其他玩家可以打出【閃】或者使用【無懈可擊】響應某些錦囊。這通過監(jiān)聽CardUseEvent的技能來實現(xiàn)。例如一個“護駕”技能可以在此刻被觸發(fā)。效果生效如果無人響應或響應無效則執(zhí)行卡牌真正的Effect。對于【殺】就是創(chuàng)建一個DamageEffect傷害效果。傷害結算DamageEffect執(zhí)行時會先發(fā)布一個DamageEvent造成傷害時觸發(fā)諸如“奸雄”、“反饋”這類技能。然后實際扣減目標體力。扣減后再發(fā)布一個DamagedEvent受到傷害后觸發(fā)“遺計”、“剛烈”等技能。后續(xù)處理將使用的卡牌移入棄牌堆。// 簡化的卡牌使用函數(shù)核心邏輯 bool Game::useCard(Card* card, Player* source, std::vectorPlayer* targets) { // 1. 合法性檢查 if (!card-isTargetValid(source, targets)) return false; // 2. 創(chuàng)建使用事件 CardUseEvent useEvent(card, source, targets); eventBus_.publish(useEvent); // 3. 檢查是否被“無懈可擊”等技能抵消 if (useEvent.isCanceled()) { moveCardToDiscard(card); return true; // 使用行為發(fā)生但被抵消 } // 4. 執(zhí)行卡牌效果 std::unique_ptrEffect effect card-createEffect(source, targets); if (effect) { effect-apply(this); // apply內部會處理傷害、摸牌等所有結算 } // 5. 移動卡牌到棄牌堆 moveCardToDiscard(card); return true; }4. 高級特性實現(xiàn)技能系統(tǒng)與狀態(tài)管理武將技能是三國殺的靈魂。一個靈活的技能系統(tǒng)能讓我們用最少的代碼添加新武將。4.1 基于事件的技能觸發(fā)機制我為每個技能定義一個類繼承自Skill基類。Skill的核心是trigger方法和一個events_列表標明這個技能監(jiān)聽哪些事件。class Skill { public: virtual std::string name() const 0; virtual std::vectorEventType triggerEvents() const 0; virtual void onTrigger(const Event event, Game* game, Player* owner) 0; // ... 其他如是否鎖定技、限定技等屬性 }; // 具體技能示例“奸雄”曹操 class JianXiong : public Skill { public: std::string name() const override { return 奸雄; } std::vectorEventType triggerEvents() const override { return {EventType::Damaged}; // 監(jiān)聽受到傷害后的事件 } void onTrigger(const Event event, Game* game, Player* owner) override { auto damagedEvent dynamic_castconst DamagedEvent(event); // 獲取造成傷害的牌 Card* damageCard damagedEvent.damageCard; if (damageCard) { // 將牌加入自己的手牌 game-obtainCard(owner, damageCard, CardArea::HAND); game-broadcastMessage(owner-name() 發(fā)動了【奸雄】獲得了 damageCard-name()); } } };在游戲初始化時將武將對應的技能實例綁定到玩家身上并將其注冊到事件總線。當事件發(fā)生時事件總線會自動調用所有監(jiān)聽該事件的技能的onTrigger方法。4.2 復雜狀態(tài)與標記系統(tǒng)很多技能會產生持續(xù)性的狀態(tài)或標記例如“樂不思蜀”、“閃電”、“連環(huán)”。狀態(tài)State通常有明確的開始和結束時機如一個回合內。可以用一個std::mapstd::string, int或更復雜的State類附加在Player上。例如“跳過出牌階段”可以是一個狀態(tài)。標記Mark更通用可能沒有明確時限用于記錄信息。例如“連環(huán)”標記可以用一個布爾值表示。更復雜的如“伏兵”標記可能需要記錄額外的數(shù)據(jù)如埋伏的牌。我設計了一個Mark類包含類型和任意數(shù)據(jù)的容器如std::any。class Player { // ... private: std::unordered_mapstd::string, std::shared_ptrMark marks_; std::vectorstd::unique_ptrState states_; public: void addMark(const std::string key, std::shared_ptrMark mark) { marks_[key] mark; } bool hasMark(const std::string key) const { return marks_.find(key) ! marks_.end(); } // 狀態(tài)管理類似但通常有生命周期檢查如回合結束時移除 };處理“閃電”這種需要傳遞的判定牌可以將其作為一個特殊的標記掛在當前判定玩家身上在判定階段觸發(fā)。5. 代碼組織、調試與擴展建議當項目代碼量達到數(shù)千行時良好的組織至關重要。5.1 模塊化與目錄結構我建議的目錄結構如下SGS-Core/ ├── core/ │ ├── game.{hpp, cpp} // Game類主循環(huán) │ ├── player.{hpp, cpp} // Player類 │ ├── card.{hpp, cpp} // Card基類及派生類 │ ├── effect.{hpp, cpp} // Effect基類及各種效果 │ ├── event.{hpp, cpp} // Event基類及各種事件 │ └── skill.{hpp, cpp} // Skill基類及具體技能 ├── engine/ │ ├── event_bus.{hpp, cpp} // 事件總線實現(xiàn) │ └── state_machine.{hpp, cpp} // 可選狀態(tài)機引擎 ├── utils/ │ ├── random.{hpp, cpp} // 隨機數(shù)生成器洗牌、判定 │ └── logger.{hpp, cpp} // 日志系統(tǒng)調試必備 ├── data/ │ └── card_data.json // 卡牌數(shù)據(jù)可JSON定義運行時加載 └── main.cpp使用#pragma once或標準的#ifndef防衛(wèi)式頭文件聲明避免重復包含。在Card和Skill的具體實現(xiàn)中大量使用工廠模式通過字符串名稱來創(chuàng)建對象便于從數(shù)據(jù)文件加載。5.2 調試技巧與日志輸出在沒有圖形界面的核心邏輯開發(fā)階段一個強大的日志系統(tǒng)是你的眼睛。class Logger { public: enum Level { DEBUG, INFO, WARN, ERROR }; static Logger instance() { static Logger logger; return logger; } void log(Level level, const std::string message) { if (level currentLevel_) { // currentLevel_可配置 std::cout [ levelToString(level) ] message std::endl; // 也可以輸出到文件 } } private: Level currentLevel_ DEBUG; // ... }; // 在代碼中大量使用 Logger::instance().log(Logger::INFO, 玩家 player-name() 使用了【殺】目標 target-name()); Logger::instance().log(Logger::DEBUG, 進入判定階段判定牌是 card-toString());通過日志你可以清晰地看到游戲流程回合開始 - 判定階段 - 摸牌階段 - 出牌階段玩家使用殺 - 觸發(fā)技能... - 棄牌階段 - 回合結束。當技能結算出現(xiàn)問題時通過對比日志和規(guī)則手冊能快速定位BUG所在。5.3 擴展方向AI、網絡與GUI當核心邏輯穩(wěn)定后你可以考慮以下擴展AI人工智能實現(xiàn)一個AIController類繼承自PlayerController。AI的決策可以非常簡單隨機出牌也可以非常復雜基于狀態(tài)評估的博弈樹搜索。一個簡單的規(guī)則型AI就可以大大提升測試效率。網絡對戰(zhàn)將Game類作為服務器權威邏輯。PlayerController變?yōu)榫W絡適配器接收客戶端消息轉換為游戲動作并將游戲狀態(tài)廣播給所有客戶端??梢允褂肂oost.Asio或ENet庫。關鍵點所有隨機數(shù)洗牌、判定必須在服務器端生成客戶端只負責表現(xiàn)。圖形界面GUI使用如SFML、SDL2或Qt等庫。GUI層只負責渲染和輸入采集將所有游戲邏輯調用轉發(fā)給核心的Game實例。這就是典型的MVC模型-視圖-控制器架構核心邏輯是模型Model完全獨立于視圖View。6. 常見問題與實戰(zhàn)排坑記錄在開發(fā)過程中我踩過不少坑這里分享幾個最具代表性的問題一循環(huán)引用導致的內存泄漏技能持有玩家的指針玩家又持有技能的集合容易形成循環(huán)引用。如果使用std::shared_ptr會導致對象永遠無法釋放。解決方案明確所有權關系。玩家“擁有”其技能std::vectorstd::unique_ptrSkill。技能如果需要反向引用玩家使用原始指針Player*或弱引用std::weak_ptrPlayer因為技能的生命周期絕不會長于玩家。問題二事件處理的順序問題當多個技能同時監(jiān)聽同一個事件例如“受到傷害時”誰先觸發(fā)這直接影響游戲平衡。解決方案為技能引入“優(yōu)先級”或“時機”概念。在事件總線內部對監(jiān)聽同一事件的處理器進行排序。通常按照“回合角色”優(yōu)先、主動技能優(yōu)先等規(guī)則。可以在Skill類中增加一個int priority()方法在注冊時排序。問題三“鎖定技”與“非鎖定技”的混淆鎖定技必須發(fā)動非鎖定技可以選擇發(fā)動。在代碼中如果簡單調用onTrigger就變成了強制發(fā)動。解決方案在Skill::onTrigger中對于非鎖定技首先要向玩家或AI提供一個“是否發(fā)動”的查詢接口。這可以通過一個回調函數(shù)或一個額外的canTrigger詢問階段來實現(xiàn)。問題四結算嵌套與棧溢出例如A對B使用【殺】B觸發(fā)“反饋”技能傷害AA又觸發(fā)“剛烈”技能……如果遞歸調用處理不當可能導致調用棧溢出。解決方案將結算過程“扁平化”。使用一個結算隊列Effect Queue。當發(fā)生嵌套結算時不立即執(zhí)行新效果而是將其插入隊列尾部。游戲主循環(huán)或一個專門的結算器會不斷從隊列頭部取出效果執(zhí)行直到隊列為空。這保證了結算順序的清晰和棧的深度可控。問題五隨機性測試與復現(xiàn)卡牌游戲充滿隨機性一個BUG可能很難穩(wěn)定復現(xiàn)。解決方案實現(xiàn)一個可播種Seed的偽隨機數(shù)生成器。在測試時使用固定的種子這樣每次運行的“隨機”洗牌和判定順序都是一樣的可以完美復現(xiàn)BUG場景。在Logger中輸出關鍵隨機數(shù)種子便于記錄和回溯。從零實現(xiàn)《三國殺》是一個龐大的工程但將其分解為“核心架構-流程引擎-技能系統(tǒng)”這幾個模塊后就變得清晰可行。這個過程不僅是對C面向對象和設計模式的深度實踐更是對復雜邏輯抽象和系統(tǒng)架構能力的一次極佳鍛煉。當你看到自己編寫的程序能夠正確運行一局包含【閃電】、【樂不思蜀】和多個武將技能互動的對局時那種成就感是無與倫比的。最重要的是這個核心框架像樂高底座一樣穩(wěn)固未來你想添加新武將、新卡牌甚至創(chuàng)造自己的擴展包都會變得非常輕松。

相關新聞

若依框架跨域問題解決方案與實戰(zhàn)配置

若依框架跨域問題解決方案與實戰(zhàn)配置

1. 若依框架跨域問題全景解析作為國內主流的企業(yè)級快速開發(fā)框架,若依(Ruoyi)在實際部署中經常面臨跨域訪問的挑戰(zhàn)。最近在技術社區(qū)看到不少開發(fā)者反饋:"前后端分離模式下,明明按照文檔配置了CORS,為什…

2026/8/2 8:54:42 閱讀更多
高校數(shù)字化通識教育的標準化配套路徑

高校數(shù)字化通識教育的標準化配套路徑

數(shù)字化崗位需求持續(xù)擴張背景下,高校通用AI教學資源供給不足的矛盾逐步凸顯。教育部2025年高校畢業(yè)生就業(yè)質量調研數(shù)據(jù)顯示,經管、文法、普通工科61.4%的應屆生崗位,明確要求求職者掌握基礎AI工具操作能力,但國內超七成本科院校未開…

2026/8/1 1:29:36 閱讀更多
I/O總線信號分線盒 M12/M8集線器解析!

I/O總線信號分線盒 M12/M8集線器解析!

在工業(yè)自動化現(xiàn)場,傳感器與執(zhí)行器星羅棋布。若每根線纜都直連控制柜,千平車間將變成線纜的海洋——布線耗時、故障難查、維護成本高昂。I/O總線信號分線盒(M12/M8集線器)正是為解決此痛點而生:它將分散的IO信號就近匯集…

2026/8/2 8:55:19 閱讀更多
3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過,那些年發(fā)過的QQ空間說說,那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南

3分鐘搞定!QQ空間歷史說說完整備份終極指南 【免費下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說說 項目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過,那些年發(fā)過的QQ空間說說,那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應用材料(Applied Materials)公司生產的一款用于半導體設備的I/O信號分配電路板。該型號(0100-02186)的核心特點如下:專用于Endura等半導體工藝腔室。集成信號路由與分配功能。連接控制…

2026/8/2 2:51:21 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動機是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機,適用于自動化設備及通用機械驅動。該型號(FFMN-32L-10-T0 40AX)的核心特點如下:三相交流異步電動機。額定…

2026/8/2 2:52:49 閱讀更多