新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++设计模式实战:大富翁游戏架构解析与工厂模式、状态模式应用

发布时间:2026/7/26 11:06:05
C++设计模式实战:大富翁游戏架构解析与工厂模式、状态模式应用
1. 项目概述与核心价值最近在整理大学时期的项目代码翻出来一个当年在吉林大学软件学院课程设计里做的“大富翁游戏”用C纯手工打造并且有意识地应用了多种经典的软件设计模式。这个项目不是简单的“能跑就行”而是我们几个同学花了小半个学期在理解设计模式精髓后刻意将其融入游戏逻辑的一次实践。现在回头看虽然图形界面用的是最原始的字符控制台但整个架构的设计思路对理解面向对象设计和设计模式的实际应用依然非常有价值。尤其是对于正在学习C、面临课程设计或者想深入理解设计模式如何落地到具体项目的朋友来说这个项目的完整代码和设计思路或许能提供一个非常直观的参考案例。简单来说这是一个在Windows/Linux控制台下运行的回合制大富翁游戏。玩家通过掷骰子在地图上移动可以购买土地、建造房屋、收取过路费并有机会触发“命运”和“机会”卡牌事件。游戏的核心乐趣和商业逻辑与大富翁经典玩法一致。而我们项目的重点和难点在于如何用良好的C面向对象编程OOP思想来组织代码并运用设计模式解决游戏开发中常见的“变化”问题比如如何优雅地添加新的地块类型、新的卡牌事件如何管理游戏中的各种状态等。最终我们实现了包括单例模式、工厂模式、观察者模式、状态模式、策略模式等在内的多种模式让代码的扩展性和维护性得到了质的提升。下面我就把这个项目的设计思路、关键实现、踩过的坑以及完整的代码结构分享出来希望能帮你少走弯路。2. 整体架构与设计模式选型思路做课程设计或者个人项目最怕的就是一开始代码写得飞起后期加功能时却发现“牵一发而动全身”改起来痛苦不堪。我们在大富翁项目立项时就定下了一个核心目标用设计模式构建一个易于扩展的游戏框架。这意味着未来如果想增加一种新的特殊地块比如“火车站”或者一种新的卡片效果应该只需要添加新的类修改极少的配置代码而不需要去改动那些已经稳定运行的核心逻辑。2.1 核心模块划分基于上述目标我们将游戏系统拆分为以下几个核心模块每个模块都承担明确的职责游戏引擎核心这是游戏的主循环和调度中心。它负责初始化游戏、管理当前玩家回合、处理掷骰子、移动、结算等一系列流程。这里我们主要运用了状态模式来管理游戏的不同阶段如掷骰子阶段、购买阶段、建筑阶段等使得状态转换清晰明了。地图与地块系统地图由一系列“地块”对象组成。我们为不同类型的地块普通地产、起点、监狱、机会、命运等设计了统一的基类接口然后派生出具体的子类。这里大量使用了工厂模式来创建地块对象使得地图的配置可以从文件如map.txt中读取程序只需要根据一个标识符如P代表普通地产调用工厂即可创建对应的对象极大提升了地图编辑的灵活性。玩家系统玩家类保存现金、资产、当前位置、是否在监狱中等状态。玩家与地图、卡牌系统的交互主要通过引擎核心来协调。卡牌系统“机会”和“命运”卡牌是游戏随机性的重要来源。我们将每张卡牌的效果封装成一个独立的“命令”对象。这里使用了命令模式将卡牌触发的事件如“获得200元”、“移动到指定位置”封装起来卡牌队列管理器只需要执行这些命令对象无需关心具体逻辑。添加新卡牌就是添加一个新的命令类。用户界面为了专注于逻辑我们采用了最简单的控制台文本交互。输入使用cin输出使用cout和cerr。虽然简陋但完全够用并且能将所有精力放在架构设计上。工具与管理器例如一个全局的单例模式的“游戏管理器”用于方便地访问游戏中的共享资源如随机数生成器、日志器等。还有观察者模式用于实现一些简单的消息通知比如当玩家资产变化时自动更新显示。2.2 为何选择这些设计模式工厂模式用于创建地块地图需要多种地块。如果在地图初始化代码里写满new Land(...),new Chance(...)那么增加一种新地块就要修改这段初始化代码违反了“开闭原则”。工厂模式将对象的创建逻辑封装起来客户端地图加载器只依赖一个抽象的创建接口具体创建什么对象由工厂类根据输入参数决定。这样新增地块类型只需扩展工厂类地图加载代码完全不用动。状态模式管理游戏流程游戏流程有明确的状态等待掷骰子、等待选择购买/建造、等待处理卡牌效果等。如果用一堆if-else或者switch-case来维护当前状态并决定下一个动作代码会非常臃肿且难以维护。状态模式将每个状态封装成一个类游戏引擎只持有当前状态对象的引用。当需要切换状态时比如掷完骰子当前状态对象自己决定下一个状态是什么并负责切换。这使得状态转移逻辑分散到各个状态类中结构清晰添加新状态也很方便。命令模式封装卡牌逻辑卡牌效果五花八门“前进3步”和“缴纳所得税50%”是截然不同的操作。如果用一个Card类里面用一个巨大的switch来执行不同效果同样是维护灾难。命令模式把每个操作如MoveCommand,PayCommand封装成独立对象卡牌只保存一个命令对象的引用。执行卡牌时就是调用这个命令对象的execute()方法。要加新效果新建一个命令类就行。单例模式管理全局资源像随机数生成器、游戏配置参数整个游戏只需要一个实例并且很多地方都需要访问。使用单例模式可以确保全局唯一访问点避免重复创建和传递引用的麻烦。但要注意单例不能滥用我们只在确有必要时使用。观察者模式实现松耦合通知比如玩家的现金显示需要实时更新。我们可以在玩家类中保存一个UI显示对象的引用每次现金变化就手动调用显示更新。但这将玩家和UI紧耦合在一起。使用观察者模式玩家作为“被观察者”现金显示组件作为“观察者”注册到玩家那里。玩家现金变化时只需通知所有观察者“我变了”观察者自己决定如何更新更新显示、播放音效等。这样玩家类完全不需要知道谁在关心它的现金变化。3. 核心模块实现细节与代码解析接下来我们深入到几个最关键模块的C实现细节中。我会贴出部分核心代码并解释其设计意图和注意事项。3.1 地图与地块系统的工厂模式实现首先定义所有地块的基类Block。// Block.h - 地块基类 #ifndef BLOCK_H #define BLOCK_H #include string #include memory class Player; // 前向声明 class Block { public: enum class Type { LAND, START, JAIL, CHANCE, FATE, TAX, FREE_PARKING }; Block(int id, const std::string name, Type type); virtual ~Block() default; int getId() const { return id_; } std::string getName() const { return name_; } Type getType() const { return type_; } // 关键虚函数当玩家停留在此地块时触发的动作 virtual void onPlayerStay(std::shared_ptrPlayer player) 0; // 虚函数当玩家经过此地块时触发的动作如起点经过给钱 virtual void onPlayerPass(std::shared_ptrPlayer player); protected: int id_; std::string name_; Type type_; }; #endif // BLOCK_H然后我们实现几个具体的地块。以普通地产LandBlock为例// LandBlock.h #ifndef LANDBLOCK_H #define LANDBLOCK_H #include Block.h class LandBlock : public Block { public: LandBlock(int id, const std::string name, int price, int baseRent); virtual ~LandBlock() default; void onPlayerStay(std::shared_ptrPlayer player) override; int getPrice() const { return price_; } int getBaseRent() const { return baseRent_; } // ... 其他方法如获取所有者、设置所有者、计算当前租金考虑房屋等级等 private: int price_; int baseRent_; std::weak_ptrPlayer owner_; // 使用weak_ptr避免循环引用 int houseLevel_{0}; };现在最关键的部分来了地块工厂BlockFactory。它的职责是根据一个类型标识符比如从配置文件中读取的字符创建对应的地块对象。// BlockFactory.h #ifndef BLOCKFACTORY_H #define BLOCKFACTORY_H #include memory #include string #include unordered_map #include functional #include Block.h class BlockFactory { public: using Creator std::functionstd::unique_ptrBlock(int id, const std::string name); // 获取工厂单例这里用了单例模式管理工厂 static BlockFactory getInstance(); // 注册地块创建器 void registerCreator(const std::string typeKey, Creator creator); // 根据类型标识符创建地块 std::unique_ptrBlock createBlock(const std::string typeKey, int id, const std::string name, const std::string extraArgs ); // 禁止拷贝和赋值 BlockFactory(const BlockFactory) delete; BlockFactory operator(const BlockFactory) delete; private: BlockFactory(); void registerDefaultCreators(); // 内部方法注册默认的地块创建器 std::unordered_mapstd::string, Creator creators_; }; #endif // BLOCKFACTORY_H// BlockFactory.cpp #include BlockFactory.h #include LandBlock.h #include StartBlock.h #include ChanceBlock.h // ... 其他地块头文件 BlockFactory::BlockFactory() { registerDefaultCreators(); } BlockFactory BlockFactory::getInstance() { static BlockFactory instance; // 局部静态变量实现单例 return instance; } void BlockFactory::registerDefaultCreators() { // 注册普通地产配置文件里用 L 表示后面可能跟价格和基础租金如 L:60:2 registerCreator(L, [](int id, const std::string name, const std::string args) - std::unique_ptrBlock { // 解析args例如60:2 size_t colon1 args.find(:); size_t colon2 args.rfind(:); if (colon1 std::string::npos) { // 参数错误返回默认或nullptr return nullptr; } int price std::stoi(args.substr(0, colon1)); int rent std::stoi(args.substr(colon1 1)); return std::make_uniqueLandBlock(id, name, price, rent); }); // 注册起点用 S 表示 registerCreator(S, [](int id, const std::string name, const std::string args) - std::unique_ptrBlock { return std::make_uniqueStartBlock(id, name); }); // 注册机会用 C 表示 registerCreator(C, [](int id, const std::string name, const std::string args) - std::unique_ptrBlock { return std::make_uniqueChanceBlock(id, name); }); // ... 注册其他地块类型 } std::unique_ptrBlock BlockFactory::createBlock(const std::string typeKey, int id, const std::string name, const std::string extraArgs) { auto it creators_.find(typeKey); if (it ! creators_.end() it-second) { return it-second(id, name, extraArgs); } // 处理未知类型可以返回一个默认地块或抛出异常 std::cerr Warning: Unknown block type key: typeKey . Creating a default placeholder block. std::endl; // 返回一个简单的、无特殊功能的占位地块 return std::make_uniqueBlock(id, name, Block::Type::LAND); // 注意这里需要Block有合适的构造函数 }设计要点与踩坑记录使用std::function和Lambda工厂的创建器使用std::function存储配合Lambda表达式使得注册新地块类型的代码非常集中和简洁都在registerDefaultCreators一个函数里完成。参数传递我们设计createBlock函数时除了typeKey还提供了一个extraArgs字符串。这是因为像普通地产L需要价格和租金这两个额外参数。我们在Lambda里解析这个字符串。这是一种简单的设计。更复杂的方案可以定义一个配置结构体或者使用更强大的配置文件格式如JSON、XML。错误处理工厂在找不到对应的创建器时不能简单地返回nullptr因为这可能导致程序在后续访问时崩溃。我们这里选择输出警告并返回一个无害的“占位符”地块保证游戏能继续运行尽管可能不符合预期。在生产环境中可能需要更严格的错误处理比如抛出异常。单例工厂将工厂设计为单例是合理的因为整个游戏只需要一个统一的地块创建入口。使用“局部静态变量”是实现单例线程安全C11以后且简单高效的方法。3.2 游戏流程的状态模式实现游戏主循环不应该是一锅粥的if-else。我们使用状态模式来梳理流程。首先定义状态接口GameState。// GameState.h #ifndef GAMESTATE_H #define GAMESTATE_H #include memory class GameEngine; // 前向声明状态需要操作引擎 class GameState { public: virtual ~GameState() default; // 处理当前状态下的输入/事件 virtual void handle(GameEngine* engine) 0; // 进入该状态时执行的操作 virtual void enter(GameEngine* engine) {} // 离开该状态时执行的操作 virtual void exit(GameEngine* engine) {} }; #endif // GAMESTATE_H然后实现几个具体的状态。例如PlayerTurnStartState玩家回合开始状态// PlayerTurnStartState.h #ifndef PLAYERTURNSTARTSTATE_H #define PLAYERTURNSTARTSTATE_H #include GameState.h class PlayerTurnStartState : public GameState { public: void handle(GameEngine* engine) override; void enter(GameEngine* engine) override; }; #endif // PLAYERTURNSTARTSTATE_H// PlayerTurnStartState.cpp #include PlayerTurnStartState.h #include GameEngine.h #include Player.h #include Dice.h #include iostream void PlayerTurnStartState::enter(GameEngine* engine) { auto currentPlayer engine-getCurrentPlayer(); std::cout \n--- currentPlayer-getName() s Turn --- std::endl; std::cout Cash: $ currentPlayer-getCash() std::endl; // 显示玩家资产等信息... } void PlayerTurnStartState::handle(GameEngine* engine) { std::cout Press Enter to roll the dice...; std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); // 清空输入缓冲区 std::cin.get(); Dice dice Dice::getInstance(); // 假设骰子也是单例 int steps dice.roll(); std::cout You rolled a steps ! std::endl; // 状态转换掷骰子后进入移动状态 engine-movePlayer(steps); engine-setState(std::make_uniquePlayerMovingState()); // 切换到移动状态 }再看PlayerMovingState玩家移动状态// PlayerMovingState.cpp #include PlayerMovingState.h #include GameEngine.h #include Player.h #include Map.h #include iostream void PlayerMovingState::handle(GameEngine* engine) { auto player engine-getCurrentPlayer(); auto map engine-getMap(); // 获取玩家即将到达的地块 int newPos (player-getPosition() engine-getPendingSteps()) % map.getBlockCount(); auto targetBlock map.getBlock(newPos); std::cout You moved to: targetBlock-getName() std::endl; // 触发“经过”事件比如经过起点 // 这里需要处理从地图尾部移动到头部时经过起点的逻辑 int oldPos player-getPosition(); if (newPos oldPos) { // 说明绕了一圈经过了起点 map.getBlock(0)-onPlayerPass(player); // 假设0号地块是起点 } player-setPosition(newPos); // 触发“停留”事件 targetBlock-onPlayerStay(player); // 状态转换移动并结算后进入回合结束或购买决策状态 // 这里需要判断比如如果触发了进监狱可能直接结束回合 if (player-isInJail()) { std::cout You are in jail! std::endl; engine-setState(std::make_uniquePlayerTurnEndState()); } else if (/* 地块可购买且玩家有钱且... */) { engine-setState(std::make_uniquePlayerBuyDecisionState()); } else { engine-setState(std::make_uniquePlayerTurnEndState()); } }设计要点与踩坑记录状态类持有引擎指针每个状态类都需要能够操作游戏引擎如获取当前玩家、地图、切换状态等。我们通过GameEngine*指针来实现。确保状态类不负责具体的游戏数据管理只负责流程控制。状态转换的决策点状态转换的逻辑写在当前状态的handle方法里。例如PlayerTurnStartState处理完掷骰子后自己决定下一个状态是PlayerMovingState。这样流程逻辑就分散到了各个状态类中非常清晰。如果要增加一个新状态比如“拍卖状态”只需要修改相关状态类的转换逻辑即可。使用std::unique_ptr管理状态GameEngine持有一个std::unique_ptrGameState来表示当前状态。切换状态时直接setState(std::make_uniqueNewState())。这保证了状态对象的唯一所有权和自动内存管理。enter和exit钩子这两个虚函数不是必须的但提供了很好的扩展点。比如在enter里显示提示信息在exit里清理临时数据。3.3 卡牌系统的命令模式实现命令模式将“请求”封装成对象。我们定义一个抽象的CardCommand接口。// CardCommand.h #ifndef CARDCOMMAND_H #define CARDCOMMAND_H #include memory class Player; class GameEngine; class CardCommand { public: virtual ~CardCommand() default; virtual void execute(std::shared_ptrPlayer target, GameEngine* engine) 0; virtual std::string getDescription() const 0; };然后实现具体的命令。例如MoveStepsCommand移动步数命令// MoveStepsCommand.h #ifndef MOVESTEPSCOMMAND_H #define MOVESTEPSCOMMAND_H #include CardCommand.h class MoveStepsCommand : public CardCommand { public: MoveStepsCommand(int steps); void execute(std::shared_ptrPlayer target, GameEngine* engine) override; std::string getDescription() const override; private: int steps_; };// MoveStepsCommand.cpp #include MoveStepsCommand.h #include Player.h #include GameEngine.h #include iostream MoveStepsCommand::MoveStepsCommand(int steps) : steps_(steps) {} void MoveStepsCommand::execute(std::shared_ptrPlayer target, GameEngine* engine) { std::cout Card Effect: Move steps_ steps. std::endl; // 这里需要小心直接移动玩家可能会干扰当前的状态机。 // 更好的方式是让命令产生一个“效果”由引擎在合适的时机处理。 // 我们修改一下设计命令不直接操作而是向引擎提交一个“延迟动作”。 engine-submitPendingMove(target, steps_); } std::string MoveStepsCommand::getDescription() const { return Move forward std::to_string(steps_) steps.; }再比如GainMoneyCommand获得金钱命令// GainMoneyCommand.cpp #include GainMoneyCommand.h #include Player.h #include iostream GainMoneyCommand::GainMoneyCommand(int amount) : amount_(amount) {} void GainMoneyCommand::execute(std::shared_ptrPlayer target, GameEngine* engine) { std::cout Card Effect: Gain $ amount_ . std::endl; target-gainCash(amount_); // 获得金钱是即时效果可以直接执行 } std::string GainMoneyCommand::getDescription() const { return Gain $ std::to_string(amount_) .; }卡牌类Card就非常简单了它只包含一个命令对象和描述。// Card.h #ifndef CARD_H #define CARD_H #include memory #include string #include CardCommand.h class Card { public: Card(std::unique_ptrCardCommand command, const std::string description); void execute(std::shared_ptrPlayer target, GameEngine* engine); std::string getDescription() const; private: std::unique_ptrCardCommand command_; std::string description_; };卡牌堆CardDeck负责管理一组卡牌并提供抽卡功能。// CardDeck.h #ifndef CARDDECK_H #define CARDDECK_H #include vector #include memory #include Card.h class CardDeck { public: CardDeck(); void shuffle(); std::shared_ptrCard drawCard(); // 抽一张牌 void returnCard(std::shared_ptrCard card); // 将牌放回牌堆底部 private: std::vectorstd::shared_ptrCard cards_; size_t currentIndex_{0}; };设计要点与踩坑记录命令的副作用这是命令模式在此场景下最需要注意的地方。有些命令如GainMoneyCommand可以立即执行。但有些命令如MoveStepsCommand会改变玩家位置进而可能触发新的地块事件这可能会与当前游戏状态机产生冲突。我们的解决方案是让命令产生“意图”而非直接执行。MoveStepsCommand的execute方法不再直接移动玩家而是调用engine-submitPendingMove(...)将移动请求提交给引擎。引擎会在当前状态处理完毕后的合适时机例如在PlayerTurnEndState之前统一处理这些 pending 的动作。这保证了游戏流程的确定性。卡牌的重用CardDeck在抽卡后通常会将卡牌放入弃牌堆当抽牌堆空时再洗回。我们这里简化了使用returnCard将牌放回底部。更完整的实现需要两个向量drawPile和discardPile。命令的创建和地块一样卡牌命令也可以使用工厂模式来创建从配置文件中读取。例如配置文件一行可能是MOVE:3或GAIN:200然后由一个CommandFactory来解析并创建对应的CardCommand对象。这能让卡牌系统的配置完全数据化。4. 项目构建、运行与配置详解4.1 开发环境与工具链这个项目是标准的C项目对跨平台支持友好。我们主要在以下环境开发和测试编译器g(MinGW-w64 或 Linux GCC) 或clang。确保支持 C11 或更高标准我们使用了std::unique_ptr,std::shared_ptr,std::function等特性。构建工具强烈推荐使用CMake。它可以帮助你轻松管理依赖、生成跨平台的构建文件如 Makefile 或 Visual Studio 项目。代码编辑器VSCode、CLion、Visual Studio均可。VSCode配置 C 环境需要安装C/C扩展并配置好c_cpp_properties.json,tasks.json,launch.json来支持编译和调试。版本控制使用Git。项目初期就应该建立仓库养成良好的提交习惯。4.2 使用CMake构建项目项目根目录下的CMakeLists.txt是关键。一个简化版本如下cmake_minimum_required(VERSION 3.10) project(MonopolyDesignPatterns VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将源代码文件分组 set(GAME_ENGINE_SOURCES src/main.cpp src/GameEngine.cpp src/Player.cpp src/Map.cpp src/Dice.cpp # ... 其他核心类cpp文件 ) set(BLOCK_SOURCES src/Block.cpp src/LandBlock.cpp src/StartBlock.cpp src/ChanceBlock.cpp src/BlockFactory.cpp # ... 其他地块cpp文件 ) set(STATE_SOURCES src/GameState.cpp src/PlayerTurnStartState.cpp src/PlayerMovingState.cpp # ... 其他状态cpp文件 ) set(COMMAND_SOURCES src/CardCommand.cpp src/MoveStepsCommand.cpp src/GainMoneyCommand.cpp src/Card.cpp src/CardDeck.cpp # ... 其他命令和卡牌cpp文件 ) # 将所有源文件合并 set(ALL_SOURCES ${GAME_ENGINE_SOURCES} ${BLOCK_SOURCES} ${STATE_SOURCES} ${COMMAND_SOURCES} ) # 创建可执行文件 add_executable(monopoly_dp ${ALL_SOURCES}) # 包含头文件目录 target_include_directories(monopoly_dp PRIVATE include) # 在Windows下如果是Visual Studio可能需要设置子系统为控制台 if(WIN32 AND MSVC) set_target_properties(monopoly_dp PROPERTIES LINK_FLAGS /SUBSYSTEM:CONSOLE) endif()构建步骤在项目根目录打开终端。mkdir build cd build创建一个构建目录保持源码干净。cmake ..生成构建文件。如果想指定生成器如cmake -G MinGW Makefiles ..。cmake --build .编译项目。在Linux/macOS下也可以直接make。编译完成后在build目录或build/Debug下会生成可执行文件monopoly_dp(或monopoly_dp.exe)。4.3 配置文件与数据驱动为了让游戏内容可配置我们将地图和卡牌数据放在外部文件中。地图配置文件map.txt:# 格式: 地块ID, 类型标识符, 地块名称, 额外参数 0,S,Start,GO 1,L,Mediterranean Avenue,60:2 2,C,Chance Chest, 3,L,Baltic Avenue,60:4 4,T,Income Tax,200:10% # 所得税200或10%取高 5,R,Reading Railroad,200 # 铁路特殊地块 ...游戏启动时Map类读取这个文件逐行解析并调用BlockFactory::createBlock(typeKey, id, name, extraArgs)来创建地块对象。卡牌配置文件chance.txt和fate.txt:# 格式: 命令类型, 参数, 描述文本 MOVE,3,Advance to Go. Collect $200. # 此命令需要特殊处理因为“前进到Go”是固定位置 GAIN,50,Bank pays you dividend of $50. PAY_EACH,-50,Pay poor tax of $50. # 负数表示支付 MOVE_TO,5,Take a trip to Reading Railroad. ...CardDeck初始化时读取对应文件利用一个CommandFactory其实现类似BlockFactory创建出具体的CardCommand对象然后组装成Card加入牌堆。这样做的好处你想修改游戏规则、调整地图、增加新卡牌完全不需要重新编译代码只需修改文本文件即可。这是设计模式带来的强大扩展性的直接体现。5. 常见问题、调试技巧与扩展方向5.1 编译与运行常见问题undefined reference to ...链接错误原因这是最常见的问题。说明你的.cpp源文件没有加入到编译列表中或者对应的.o文件没有被链接。解决检查CMakeLists.txt中的set(ALL_SOURCES ...)部分确保所有你实现的.cpp文件都列在了里面。特别是新添加的类一定要把它的.cpp文件加进去。‘unique_ptr’ in namespace ‘std’ did not name a template type原因编译器不支持 C11 或未启用 C11 标准。解决在CMakeLists.txt中确保设置了set(CMAKE_CXX_STANDARD 11)。如果使用 g 命令行添加-stdc11标志。控制台输出中文乱码原因Windows 控制台默认编码是 GBK而源代码文件是 UTF-8。解决Windows方案一将源代码文件保存为带 BOM 的 UTF-8 或 GBK 编码不推荐不利于跨平台。方案二在代码中输出中文时使用宽字符std::wstring和std::wcout改动较大。推荐方案三在程序启动时设置控制台代码页。在main()函数开头添加#ifdef _WIN32 #include windows.h #endif int main() { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); // 设置控制台输出为UTF-8 #endif // ... 游戏逻辑 }方案四在 VSCode 的集成终端中运行它通常能更好地处理 UTF-8。程序崩溃错误信息不明显解决使用调试器。在 VSCode 中配置好launch.json在关键代码处设置断点。在 CLion 或 Visual Studio 中直接使用内置调试器。这是定位指针错误、数组越界等问题的最有效方法。5.2 设计模式应用中的典型“坑”循环引用导致内存泄漏在地块系统中LandBlock拥有一个owner_类型是std::weak_ptrPlayer。为什么不用std::shared_ptrPlayer因为Player类可能也会持有其拥有的地产列表std::vectorstd::shared_ptrLandBlock。如果双方都用shared_ptr就会形成循环引用导致对象永远无法被释放。std::weak_ptr是一种“弱引用”它不会增加对象的引用计数打破了循环是处理这类问题的标准做法。状态机状态爆炸如果游戏逻辑非常复杂状态类可能会变得很多。这时候需要审视状态划分是否合理。有时可以将一些简单的、短暂的状态用枚举或标志位在同一个状态类内部处理而不是为每一个细微步骤都创建一个新的状态类。保持状态机的简洁和可理解性更重要。工厂模式中参数解析的复杂性我们用了简单的冒号分隔字符串来传递参数。当地块或命令的参数变得复杂时比如一个命令需要多个不同类型的参数这种格式会很难维护。进阶方案使用结构体来封装参数或者直接使用 JSON、YAML 等配置文件格式配合像nlohmann/json这样的库来解析可读性和可维护性会好很多。单例模式的滥用与测试困难单例模式让全局访问变得方便但也隐藏了依赖关系使得单元测试变得困难因为你很难模拟一个单例。在我们的项目中Dice骰子和BlockFactory使用单例是合理的因为它们是无状态的或确实是全局唯一的工具。但对于有状态的“游戏管理器”如果未来需要做网络版或AI测试单例可能会成为障碍。需要权衡利弊。5.3 项目扩展方向这个基础框架为扩展留下了很多空间图形化界面这是最直观的扩展。你可以用Qt、SFML或Dear ImGui等库替换掉控制台输出。模型我们的核心逻辑和视图GUI是分离的你只需要在现有状态类和命令类的相应位置将std::cout替换为向GUI发送消息或调用更新函数即可。设计模式保证了业务逻辑的独立性使得更换UI相对容易。网络对战将游戏引擎改造成服务器玩家客户端通过网络连接。状态模式在这里依然有用服务器端维护游戏状态机接收客户端指令如“掷骰子”、“购买”驱动状态转换并将结果广播给所有客户端。命令模式可以用来序列化和反序列化网络消息。AI玩家实现一个简单的AI并不难。AI在每个状态如PlayerBuyDecisionState需要做出决策。你可以为AI实现一套策略例如总是购买无人拥有的土地、当现金低于阈值时不购买、根据预期收益决定是否建造房屋等。AI的决策可以看作是在当前状态下对游戏引擎的一个“自动”输入。更复杂的经济系统引入银行贷款、股票市场、道具系统等。这些可以作为新的“命令”加入到卡牌系统或者作为新的“地块”类型如“股票交易所”地块通过扩展工厂和命令模式轻松集成。数据持久化实现存档/读档功能。你需要为每个可序列化的类如Player,LandBlock,Map实现toJson()和fromJson()方法将游戏状态保存到文件。设计模式让对象关系清晰序列化工作也会更有条理。回过头看这个项目最大的收获不是做出了一个多好玩的游戏而是在实践中真切地体会到了设计模式如何让代码应对变化。当你需要加一个新功能时不再是抱着头在一堆 spaghetti code 里寻找该改哪里而是思考“我应该新增一个类并在这里注册一下”。这种开发体验对于培养良好的软件工程思维至关重要。代码我整理后放在了GitHub上你可以直接下载、编译、运行更可以随意修改和扩展。希望这个来自吉大软院的课程设计项目能成为你学习C和设计模式的一个有价值的踏脚石。
网站建设 高端定制 企业官网