尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++游戏开发实战:从零构建迷你农场RPG,掌握面向对象与游戏循环

C++游戏开发实战:从零构建迷你农场RPG,掌握面向对象与游戏循环 最近在尝试用 C 开发一个轻量级的游戏项目发现很多教程要么过于简单比如只有一个控制台贪吃蛇要么直接跳到复杂的图形引擎中间缺少一个平滑的过渡。为了填补这个空白我决定动手实现一个“迷你农场 RPG”它融合了角色扮演的成长系统和农场模拟的经营玩法非常适合用来学习 C 面向对象设计、游戏循环、状态管理和简单的资源管理。本文将手把手带你从零搭建这个项目涵盖核心的游戏循环、玩家交互、作物生长系统、背包系统等模块并提供完整的、可运行的代码示例。无论你是想巩固 C 基础还是对游戏开发感兴趣这篇文章都能为你提供一个清晰的实战路径。1. 项目概述与核心概念1.1 什么是迷你农场RPG“迷你农场 RPG”是一个结合了角色扮演游戏RPG元素和农场模拟经营玩法的游戏原型。在游戏中玩家扮演一个农场主核心玩法包括探索与交互在固定的地图网格上移动与场景元素如土地、商店、仓库互动。种植与收获购买种子在开垦的土地上种植作物会随着时间游戏内时间生长最终成熟可收获。资源管理拥有一个背包系统用于存放种子、工具和收获的作物。金钱是主要资源用于购买物品。简易任务系统通过完成特定目标如收获一定数量的某种作物来推动游戏进程或获得奖励。这个项目不依赖复杂的图形库如 Unity、Unreal而是使用控制台或极简的图形库如 SFML 的简单图形模式来呈现确保我们能把重点放在 C 游戏逻辑的构建上。1.2 为什么用 C 开发C 以其高性能和对底层资源的精细控制能力一直是游戏开发特别是游戏引擎和核心逻辑层的首选语言之一。通过这个项目你可以深入理解面向对象编程OOP如何用类来抽象游戏中的实体如Player、Crop、Inventory。游戏主循环Game Loop理解处理输入 - 更新状态 - 渲染输出这一核心模式。状态管理管理游戏的不同状态如菜单、游玩、暂停以及实体自身的状态如作物生长阶段。数据组织使用 STL 容器如std::vector,std::map高效地管理游戏对象集合。时间与事件模拟游戏内时间流逝并触发相应事件如作物生长。1.3 技术选型与项目结构我们将采用以下技术栈语言C17/20编译环境GCC/Clang 或 MSVC构建工具CMake推荐或直接使用 IDE 项目可选图形库SFMLSimple and Fast Multimedia Library用于简单的窗口、事件和图形渲染。如果只想专注于逻辑纯控制台Console也可以。项目结构清晰的目录划分有助于管理。MiniFarmRPG/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── Game.cpp │ ├── Game.h │ ├── Player.cpp │ ├── Player.h │ ├── Map.cpp │ ├── Map.h │ ├── Crop.cpp │ ├── Crop.h │ ├── Inventory.cpp │ ├── Inventory.h │ └── Item.h ├── assets/ (可选存放字体、图片) └── README.md2. 环境准备与项目搭建2.1 开发环境配置安装 C 编译器Windows推荐安装 MinGW-w64 或使用 Visual Studio 2022 并勾选“使用 C 的桌面开发”。Linux/macOS系统通常自带 GCC 或 Clang可通过终端命令g --version或clang --version检查。安装 CMake可选但推荐从 CMake官网 下载并安装。它可以帮助我们跨平台构建项目。安装 SFML如果使用图形界面Windows从 SFML官网 下载对应编译器版本的预编译库解压后记住路径。Linux使用包管理器安装例如 Ubuntu/Debiansudo apt-get install libsfml-devmacOS使用 Homebrewbrew install sfml2.2 创建基础项目我们首先创建一个纯控制台版本以确保核心逻辑正确。创建项目目录MiniFarmRPG。创建CMakeLists.txt如果使用 CMakecmake_minimum_required(VERSION 3.10) project(MiniFarmRPG) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 如果是纯控制台项目 add_executable(MiniFarmRPG src/main.cpp src/Game.cpp src/Player.cpp src/Map.cpp src/Crop.cpp src/Inventory.cpp) # 如果后续要链接 SFML取消注释并修改路径 # set(SFML_DIR path/to/your/SFML/lib/cmake/SFML) # find_package(SFML 2.5 COMPONENTS graphics window system REQUIRED) # target_link_libraries(MiniFarmRPG SFML::Graphics SFML::Window SFML::System)创建基础头文件src/Item.h定义游戏中所有物品的枚举和基础信息。// src/Item.h #ifndef ITEM_H #define ITEM_H #include string // 物品类型枚举 enum class ItemType { SEED, CROP, TOOL, MONEY // 特殊物品代表金钱 }; // 物品子类型例如具体是哪种种子 enum class SeedType { WHEAT, CORN, CARROT, NONE }; enum class CropType { WHEAT, CORN, CARROT, NONE }; enum class ToolType { HOE, WATERING_CAN, NONE }; struct Item { ItemType type; union { SeedType seed; CropType crop; ToolType tool; } subtype; std::string name; int buyPrice; // 购买价格 int sellPrice; // 出售价格 int growthTime; // 生长时间仅对种子有效单位游戏刻 // 构造函数 Item(ItemType t ItemType::SEED, SeedType s SeedType::NONE, const std::string n , int bp 0, int sp 0, int gt 0) : type(t), name(n), buyPrice(bp), sellPrice(sp), growthTime(gt) { subtype.seed s; } // 可以根据需要添加其他构造函数 }; #endif // ITEM_H3. 核心类设计与实现3.1 作物类 (Crop)作物是游戏的核心实体之一它有生长状态和生命周期。// src/Crop.h #ifndef CROP_H #define CROP_H #include Item.h #include string class Crop { public: Crop(SeedType seedType, int plantTime); ~Crop() default; // 更新作物状态根据当前游戏时间 void update(int currentGameTime); // 检查是否可收获 bool isHarvestable() const; // 收获作物返回对应的物品 Item harvest(); // 获取信息 std::string getName() const; CropType getType() const; int getGrowthStage() const; // 0:种子, 1:幼苗, 2:成熟 int getTimePlanted() const; private: CropType m_type; std::string m_name; int m_timePlanted; // 种植时的游戏时间 int m_growthTime; // 总生长所需时间 int m_growthStage; // 当前生长阶段 bool m_harvestable; }; #endif // CROP_H// src/Crop.cpp #include Crop.h #include iostream Crop::Crop(SeedType seedType, int plantTime) : m_timePlanted(plantTime), m_growthStage(0), m_harvestable(false) { switch (seedType) { case SeedType::WHEAT: m_type CropType::WHEAT; m_name 小麦; m_growthTime 10; // 假设10个游戏刻成熟 break; case SeedType::CORN: m_type CropType::CORN; m_name 玉米; m_growthTime 15; break; case SeedType::CARROT: m_type CropType::CARROT; m_name 胡萝卜; m_growthTime 8; break; default: m_type CropType::NONE; m_name 未知作物; m_growthTime 0; } } void Crop::update(int currentGameTime) { if (m_harvestable || m_type CropType::NONE) return; int timeElapsed currentGameTime - m_timePlanted; if (timeElapsed m_growthTime) { m_growthStage 2; m_harvestable true; std::cout m_name 已经成熟可以收获了\n; } else if (timeElapsed m_growthTime / 2) { m_growthStage 1; // 进入生长中期 } // 否则保持为0幼苗期 } bool Crop::isHarvestable() const { return m_harvestable; } Item Crop::harvest() { if (!m_harvestable) { return Item(ItemType::CROP, CropType::NONE, 空气, 0, 0, 0); } // 收获后作物消失返回对应的农产品物品 Item harvestedItem(ItemType::CROP); harvestedItem.subtype.crop m_type; harvestedItem.name m_name (收获); harvestedItem.sellPrice 15; // 示例售价 return harvestedItem; } // 其他getter方法实现略...3.2 背包类 (Inventory)背包用于管理玩家持有的物品需要支持添加、移除、查找和显示。// src/Inventory.h #ifndef INVENTORY_H #define INVENTORY_H #include Item.h #include vector #include map class Inventory { public: Inventory(); ~Inventory() default; bool addItem(const Item item); bool removeItem(const std::string itemName, int count 1); const Item* findItem(const std::string itemName) const; void display() const; int getMoney() const { return m_money; } void addMoney(int amount) { m_money amount; } bool spendMoney(int amount); // 花费金钱钱不够返回false private: std::vectorItem m_items; int m_money; // 可以使用map来优化查找这里用vector简化 }; #endif // INVENTORY_H// src/Inventory.cpp #include Inventory.h #include iostream #include algorithm Inventory::Inventory() : m_money(100) { // 初始资金100 // 可以初始化一些起始物品 m_items.push_back(Item(ItemType::TOOL, ToolType::HOE, 锄头, 0, 5, 0)); m_items.push_back(Item(ItemType::SEED, SeedType::WHEAT, 小麦种子, 5, 2, 10)); } bool Inventory::addItem(const Item item) { // 简单实现直接加入背包 m_items.push_back(item); std::cout 获得物品: item.name \n; return true; } bool Inventory::removeItem(const std::string itemName, int count) { int removed 0; auto it m_items.begin(); while (it ! m_items.end() removed count) { if (it-name itemName) { it m_items.erase(it); removed; } else { it; } } if (removed 0) { std::cout 移除了 removed 个 itemName \n; return true; } std::cout 背包中没有找到: itemName \n; return false; } void Inventory::display() const { std::cout 背包 \n; std::cout 金钱: m_money G\n; std::cout 物品列表:\n; std::mapstd::string, int itemCounts; for (const auto item : m_items) { itemCounts[item.name]; } for (const auto pair : itemCounts) { std::cout pair.first x pair.second \n; } std::cout \n; } bool Inventory::spendMoney(int amount) { if (m_money amount) { m_money - amount; std::cout 花费了 amount G 剩余 m_money G\n; return true; } std::cout 金钱不足需要 amount G 但只有 m_money G\n; return false; }3.3 地图与玩家类 (Map Player)地图管理游戏世界的网格玩家则在地图上移动并与地图元素交互。// src/Map.h #ifndef MAP_H #define MAP_H #include Crop.h #include vector #include memory // for std::unique_ptr enum class TileType { GRASS, // 草地可开垦 SOIL, // 已开垦的土地可种植 WATER, // 水不可通过 ROCK, // 岩石不可通过 SHOP, // 商店 HOME // 家仓库 }; struct Tile { TileType type; std::unique_ptrCrop crop; // 该地块上种植的作物如果有 // 可以添加其他属性如装饰物 }; class Map { public: Map(int width, int height); ~Map() default; void render() const; // 渲染地图控制台版 Tile* getTile(int x, int y); const Tile* getTile(int x, int y) const; bool isPassable(int x, int y) const; bool plantCrop(int x, int y, SeedType seedType, int currentTime); Item harvestCrop(int x, int y); int getWidth() const { return m_width; } int getHeight() const { return m_height; } private: int m_width; int m_height; std::vectorstd::vectorTile m_grid; }; #endif // MAP_H// src/Player.h #ifndef PLAYER_H #define PLAYER_H #include Inventory.h #include Map.h #include utility // for std::pair class Player { public: Player(int startX, int startY); ~Player() default; bool move(int dx, int dy, const Map map); // 移动玩家 void interact(Map map); // 与当前所在格子交互 void update(int currentGameTime, Map map); // 更新状态例如检查周围作物 std::pairint, int getPosition() const { return {m_x, m_y}; } Inventory getInventory() { return m_inventory; } private: int m_x, m_y; Inventory m_inventory; }; #endif // PLAYER_H由于篇幅Map.cpp和Player.cpp的实现代码将整合到下一节的游戏主循环中展示关键部分4. 游戏主循环与核心逻辑实现4.1 游戏类 (Game) 与主循环Game类是游戏的总控制器负责初始化、运行主循环和处理游戏状态。// src/Game.h #ifndef GAME_H #define GAME_H #include Player.h #include Map.h class Game { public: Game(); ~Game() default; void run(); // 启动游戏主循环 private: void processInput(); // 处理用户输入 void update(); // 更新游戏状态 void render(); // 渲染当前帧 bool m_isRunning; // 游戏运行标志 int m_gameTime; // 游戏内时间刻 Player m_player; Map m_map; }; #endif // GAME_H// src/Game.cpp #include Game.h #include iostream #include thread #include chrono #ifdef _WIN32 #include conio.h // Windows 下用于_getch() #else #include termios.h #include unistd.h #include stdio.h int getch() { // Linux/macOS 简易实现 struct termios oldt, newt; int ch; tcgetattr(STDIN_FILENO, oldt); newt oldt; newt.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, newt); ch getchar(); tcsetattr(STDIN_FILENO, TCSANOW, oldt); return ch; } #endif Game::Game() : m_isRunning(true), m_gameTime(0), m_player(1, 1), m_map(10, 10) { // 初始化地图设置一些障碍物和商店 for (int y 0; y 10; y) { for (int x 0; x 10; x) { auto* tile m_map.getTile(x, y); if (x 0 || y 0 || x 9 || y 9) { tile-type TileType::ROCK; } else { tile-type TileType::GRASS; } } } m_map.getTile(5, 5)-type TileType::SHOP; m_map.getTile(8, 8)-type TileType::HOME; std::cout 迷你农场 RPG 启动\n; std::cout 控制WASD移动I打开背包E交互Q退出\n; } void Game::run() { while (m_isRunning) { processInput(); update(); render(); // 简单控制帧率避免循环过快 std::this_thread::sleep_for(std::chrono::milliseconds(100)); m_gameTime; // 游戏时间前进 } } void Game::processInput() { char input; #ifdef _WIN32 if (_kbhit()) { input _getch(); #else // 非阻塞输入检查简化版 fd_set set; struct timeval timeout; FD_ZERO(set); FD_SET(STDIN_FILENO, set); timeout.tv_sec 0; timeout.tv_usec 0; if (select(1, set, NULL, NULL, timeout) 0) { input getch(); #endif switch (input) { case w: case W: m_player.move(0, -1, m_map); break; case s: case S: m_player.move(0, 1, m_map); break; case a: case A: m_player.move(-1, 0, m_map); break; case d: case D: m_player.move(1, 0, m_map); break; case i: case I: m_player.getInventory().display(); break; case e: case E: m_player.interact(m_map); break; case q: case Q: m_isRunning false; std::cout 游戏结束。\n; break; default: break; } } } void Game::update() { // 更新地图上所有作物的状态 for (int y 0; y m_map.getHeight(); y) { for (int x 0; x m_map.getWidth(); x) { auto* tile m_map.getTile(x, y); if (tile-crop) { tile-crop-update(m_gameTime); } } } // 可以在这里添加其他更新逻辑如NPC行为、事件触发等 } void Game::render() { system(cls); // Windows 清屏Linux/macOS 可替换为 system(clear); std::cout 游戏时间: m_gameTime 刻\n; m_map.render(); auto [px, py] m_player.getPosition(); std::cout 玩家位置: ( px , py )\n; std::cout 控制WASD移动I打开背包E交互Q退出\n; }Map.cpp的关键渲染和交互实现// src/Map.cpp (部分关键函数) #include Map.h #include iostream Map::Map(int width, int height) : m_width(width), m_height(height) { m_grid.resize(height, std::vectorTile(width)); for (int y 0; y height; y) { for (int x 0; x width; x) { m_grid[y][x].type TileType::GRASS; m_grid[y][x].crop.reset(); } } } void Map::render() const { for (int y 0; y m_height; y) { for (int x 0; x m_width; x) { char displayChar .; switch (m_grid[y][x].type) { case TileType::GRASS: displayChar .; break; case TileType::SOIL: displayChar ; break; case TileType::WATER: displayChar ~; break; case TileType::ROCK: displayChar #; break; case TileType::SHOP: displayChar S; break; case TileType::HOME: displayChar H; break; } if (m_grid[y][x].crop) { switch (m_grid[y][x].crop-getGrowthStage()) { case 0: displayChar ,; break; // 幼苗 case 1: displayChar i; break; // 生长中 case 2: displayChar Y; break; // 成熟 } } std::cout displayChar ; } std::cout \n; } } bool Map::plantCrop(int x, int y, SeedType seedType, int currentTime) { if (x 0 || x m_width || y 0 || y m_height) return false; Tile tile m_grid[y][x]; if (tile.type ! TileType::SOIL) { std::cout 这里不是已开垦的土地\n; return false; } if (tile.crop) { std::cout 这块地已经有作物了\n; return false; } tile.crop std::make_uniqueCrop(seedType, currentTime); std::cout 成功种植了作物。\n; return true; }4.2 玩家交互逻辑实现// src/Player.cpp #include Player.h #include iostream Player::Player(int startX, int startY) : m_x(startX), m_y(startY) {} bool Player::move(int dx, int dy, const Map map) { int newX m_x dx; int newY m_y dy; if (map.isPassable(newX, newY)) { m_x newX; m_y newY; return true; } else { std::cout 无法移动到那个位置。\n; return false; } } void Player::interact(Map map) { Tile* currentTile map.getTile(m_x, m_y); if (!currentTile) return; switch (currentTile-type) { case TileType::GRASS: { // 使用锄头开垦土地 const Item* hoe m_inventory.findItem(锄头); if (hoe) { currentTile-type TileType::SOIL; std::cout 使用锄头开垦了土地。\n; } else { std::cout 你需要一把锄头来开垦土地。\n; } break; } case TileType::SOIL: { // 种植或收获 if (currentTile-crop) { if (currentTile-crop-isHarvestable()) { Item harvested map.harvestCrop(m_x, m_y); m_inventory.addItem(harvested); } else { std::cout 作物还没成熟呢。\n; } } else { // 尝试种植 const Item* seed m_inventory.findItem(小麦种子); // 简化固定种小麦 if (seed m_inventory.removeItem(小麦种子, 1)) { // 这里需要获取当前游戏时间需要Game类传递进来为了简化我们先假设一个时间 map.plantCrop(m_x, m_y, SeedType::WHEAT, 0); // 时间参数应来自Game } else { std::cout 你没有小麦种子。\n; } } break; } case TileType::SHOP: { std::cout 欢迎来到商店\n; std::cout 1. 购买小麦种子 (5G)\n; std::cout 2. 离开\n; // 这里可以扩展一个商店菜单系统 break; } case TileType::HOME: { std::cout 回到家了。可以在这里保存游戏或休息。\n; break; } default: std::cout 这里没什么可做的。\n; } }4.3 主函数入口// src/main.cpp #include Game.h int main() { Game game; game.run(); // 进入游戏主循环 return 0; }5. 编译与运行5.1 使用 CMake 构建推荐在项目根目录MiniFarmRPG/下执行mkdir build cd build cmake .. cmake --build . # 在Windows下可能会生成.sln用VS打开编译或使用 cmake --build . --config Release编译成功后在build/Debug/或build/目录下找到可执行文件MiniFarmRPG并运行。5.2 直接使用命令行编译简易进入src/目录使用 g 编译所有.cpp文件g -stdc17 -o MiniFarmRPG.exe main.cpp Game.cpp Player.cpp Map.cpp Crop.cpp Inventory.cpp -I.然后在命令行中运行./MiniFarmRPG.exeWindows或./MiniFarmRPGLinux/macOS。5.3 运行效果运行后你将看到一个简单的字符网格地图玩家由表示需要在Map::render中根据玩家位置特殊标记。通过WASD移动走到草地上按E使用锄头开垦走到开垦的土地上按E种植如果你有种子等时间过去作物成熟后再次按E收获。按I查看背包和金钱。6. 功能扩展与优化建议目前的版本是一个极其简化的原型。你可以在此基础上进行大量扩展使其更像一个完整的游戏6.1 扩展游戏功能完整的商店系统实现一个Shop类管理商品列表、购买/出售逻辑。任务系统设计一个Quest类包含任务目标、奖励和状态。游戏初始化时加载任务玩家完成后获得奖励。更丰富的作物系统为作物添加水分、肥料需求不同生长阶段需要不同照料。时间系统引入昼夜循环和季节影响作物生长速度和商店物价。NPC 与对话添加非玩家角色玩家可以与之对话获取信息或任务。数据持久化将游戏状态玩家数据、地图、背包保存到文件下次启动可以加载。6.2 代码结构优化使用智能指针管理资源地图中的作物已经使用了std::unique_ptr可以扩展到其他动态创建的对象。引入事件系统将玩家交互、作物成熟等定义为事件通过事件总线解耦各个系统。状态模式管理游戏状态将游戏的不同状态主菜单、游玩、暂停、商店用单独的类管理使主循环更清晰。配置文件将作物属性、物品价格、地图初始布局等数据移到外部配置文件如 JSON便于修改和平衡。6.3 引入图形界面使用 SFML如果你想摆脱控制台可以集成 SFML。步骤大致如下修改CMakeLists.txt链接 SFML 库。创建一个RenderSystem类负责加载纹理、精灵并绘制地图、玩家和 UI。将Game::processInput改为处理 SFML 的事件sf::Event。在Game::render中调用RenderSystem的绘制方法。用图片替代字符使游戏画面更美观。7. 常见问题与调试技巧7.1 编译错误undefined reference to ...通常是因为没有在编译命令中链接所有.cpp文件或者 CMake 中add_executable没有包含该文件。检查CMakeLists.txt或命令行。sfml相关错误确保 SFML 库路径正确并且编译器版本与 SFML 库版本匹配。C标准不匹配在CMakeLists.txt中明确设置set(CMAKE_CXX_STANDARD 17)。7.2 运行时问题控制台闪烁或输入无响应控制台清屏命令system(“cls”)在某些 IDE 的终端中可能表现不佳。可以考虑使用更稳定的控制台库如ncurses用于 Linux/macOS或直接切换到 SFML 图形窗口。移动或交互无效检查Map::isPassable和Player::interact中的逻辑判断条件。添加一些调试输出打印玩家位置和目标格子的类型。内存泄漏确保所有new都有对应的delete或者优先使用智能指针和 STL 容器。本项目中使用std::unique_ptr管理Crop避免了手动内存管理。7.3 游戏逻辑调试使用日志在关键函数入口添加std::cout输出跟踪程序执行流。检查数据状态定期调用m_player.getInventory().display()和输出地图信息确保游戏内部状态符合预期。简化复现如果遇到一个 bug尝试创建一个最小的、可复现的测试用例这能帮助你快速定位问题根源。8. 总结与学习路线通过这个“迷你农场 RPG”项目我们实践了 C 游戏开发的核心流程从需求分析、类设计、到具体实现和测试。你不仅复习了 C 的类、STL、指针等基础知识更重要的是理解了游戏循环、状态更新、对象交互等游戏编程的核心概念。下一步可以深入学习的方向设计模式学习如何在游戏中使用状态模式、观察者模式、工厂模式等让代码更优雅、易扩展。实体组件系统ECS这是现代游戏引擎常用的架构可以极大地提升复杂游戏项目的性能和可维护性。图形与动画深入学习 SFML 或 SDL实现精灵动画、粒子效果、摄像机等。物理与碰撞为游戏角色和物体添加简单的物理运动和碰撞检测。音频与音乐集成背景音乐和音效提升游戏体验。跨平台与发布学习如何将你的游戏打包发布到不同的平台。游戏开发是一个庞大的领域但万丈高楼平地起。从这个简单的项目出发不断添加新功能、重构代码、学习新知识你就能逐步构建出更复杂、更精彩的游戏世界。希望这个项目能成为你 C 游戏开发之旅的一块坚实垫脚石。如果在实现过程中遇到任何问题欢迎在评论区交流讨论。
返回列表