从零实现C++与Qt养猪模拟游戏:课设项目实战指南
简介这份基于C与QT实现的养猪模拟游戏课程设计项目围绕养猪场管理场景完整覆盖猪圈分配、品种隔离、出栏补栏、体重增长模拟、文件存取与猪瘟传播等核心功能适合高校计算机相关专业学生作为课设、大作业或期末项目参考。压缩包共45个文件大小7.66MB主要包含14个cpp源码、13个h头文件、2个ui界面文件、2个qrc资源文件及png/wav素材并附有项目说明文档结构清晰便于直接导入Qt工程运行与二次修改。设计上使用链表管理猪圈生猪自动分配猪圈与统计售价支持任意查询猪圈猪只状态及近五年的销售、购入记录并提供各品种数量、体重及饲养时间分布图猪瘟传播模块以概率模型模拟同圈与相邻传染可作为算法可视化与交互设计的示范。已有117人学习对正在完成相关课设或想扩展Qt项目功能的学习者具有直接借鉴价值。1. 为什么是“养猪模拟游戏”一门课设如何把 C 和 Qt 用到实处每年课程设计选题季总有一批同学对着题目列表纠结管理系统太土、算法题太卷、网络编程又怕自己调不通。如果你手里正好握着“基于C和QT开发的养猪模拟游戏”那这其实是个被低估的宝藏题目。它表面是个游戏内核却是一个完整的“面向对象设计 GUI 事件驱动 数据持久化”综合训练C 负责把养殖逻辑写成类Qt 负责把状态画成界面二者各司其职天然适合拿来当作课程设计的主项目。这类项目的核心诉求很直接把猪的生长过程抽象成数据模型让玩家通过喂食、清洁、防疫等操作影响猪的健康和体重再用可视化界面实时展示状态。对于学生来说它比“图书馆管理系统”更能展示设计能力对于想补项目经验的开发者来说它麻雀虽小、五脏俱全QTimer 驱动的模拟循环、信号槽解耦、序列化存档都是以后做正式产品会反复用到的东西。这篇文章不假设你已经有现成代码而是按“从零把课设立起来”的顺序讲清楚怎么组织工程、怎么写核心逻辑、怎么把界面做像样、以及哪些坑必须提前避开。2. 从空工程到可运行骨架项目结构、构建方式与 C/Qt 的版本选型2.1 为什么用 Qt Widgets 而不是 QML选型先想清楚“QT开发”在热词里经常跟着 Qt Designer、Qt Creator 一起出现不少新手第一步就会纠结界面用 Widgets 还是 QML。对于课程设计我的建议是默认选 Qt Widgets。原因不是 QML 不好而是 QML 的 QML/JS 混合编程会引入第二套语法体系课设答辩时老师更想看到你用 C 类组织逻辑而不是把业务规则写在 QML 的 JavaScript 函数里。Qt Widgets 方案的典型分层是这样的视图层放 QMainWindow 和自定义 QWidget控制层用信号槽连接按钮点击与数据变更模型层写 Pig、Farm、GameEngine 等纯 C 类。这套结构和你以后写 Qt 桌面工具、甚至 Qt 嵌入式应用的思路完全一致。提示如果所在学校指定了 Qt 版本优先按学校要求来如果自己下载建议选 Qt 5.15.2 LTS稳定、资料多、和 Qt Creator 的配合也最省心。Qt 6 也能做但一些第三方教程和旧代码里的写法需要微调。2.2 用 CMake 组织课设工程比 qmake 多一步但值得Qt Creator 默认支持 qmake 和 CMake 两套构建系统。qmake 的 .pro 文件确实简单但如果你以后想把这个项目迁移到 CI 环境或者别人用不同 IDE 打开CMake 的兼容性更好。下面给出一份可以直接用的 CMakeLists.txt 骨架。cmake_minimum_required(VERSION 3.16) project(PigFarmSimulator VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 自动包含 Qt 的 moc/uic/rcc 工具链 set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 5.15 REQUIRED COMPONENTS Widgets) set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) add_executable(PigFarmSimulator ${SOURCE_DIR}/main.cpp ${SOURCE_DIR}/Pig.cpp ${SOURCE_DIR}/GameEngine.cpp ${SOURCE_DIR}/MainWindow.cpp ${INCLUDE_DIR}/Pig.h ${INCLUDE_DIR}/GameEngine.h ${INCLUDE_DIR}/MainWindow.h ) target_include_directories(PigFarmSimulator PRIVATE ${INCLUDE_DIR}) target_link_libraries(PigFarmSimulator PRIVATE Qt5::Widgets)这段 CMake 配置里有三个关键点要理解AUTOMOC 开启后Qt 的信号槽元对象编译由 CMake 自动处理不需要手动运行 mocAUTOUIC 解决 .ui 文件到 ui_xxx.h 的转换find_package 里的 5.15 必须和系统里安装的 Qt 版本一致否则后面会报“fatal: cannot mix incompatible qt library”这类版本冲突错误这个坑我们在第五章还会展开。2.3 类的划分从“猪”这个对象开始课程设计源码里最重要的不是界面好不好看而是类的职责是否清晰。以养猪模拟为例最起码要拆出四个类Pig单头猪的数据模型包括体重、健康值、饱食度、清洁度、成长阶段。GameEngine模拟引擎负责时间推进、随机事件生成、状态迭代。MainWindow主窗口承载所有 UI 控件转发用户操作。辅助结构体或类存档数据包用于序列化和读档。这里给出 Pig 类的头文件示例关键成员全部用私有封装对外只暴露接口。#ifndef PIG_H #define PIG_H #include QString #include QDateTime class Pig { public: enum class GrowthStage { Piglet, // 仔猪 Growing, // 中猪 Adult, // 成猪 Ready // 出栏 }; Pig(const QString name); void feed(double amountKg); void clean(); void vaccinate(); void updateDaily(); double weight() const; double health() const; double satiety() const; double cleanliness() const; GrowthStage stage() const; int ageDays() const; private: QString m_name; double m_weightKg 5.0; double m_health 100.0; double m_satiety 80.0; double m_cleanliness 100.0; int m_ageDays 0; void recalcStage(); }; #endif // PIG_H这个头文件展示了一个课设项目应有的“数据边界”界面代码永远不直接操作 m_weightKg而是调用 feed 或 updateDaily。这不仅是面向对象封装的要求更是后面做存档读写时能省力的前提。updateDaily 里要放每天的状态衰减和成长计算feed 里要处理“吃太多反而生病”的平衡逻辑这些细节直接决定游戏好不好玩也是答辩时能讲出内容的地方。2.4 main.cpp让 QApplication 成为程序起点C 的 Qt 程序入口和普通控制台程序不同必须创建 QApplication 实例然后进入事件循环。这个机制是 Qt 一切信号槽和定时器的基础也是初学者最容易忽略“为什么窗口一闪而过”的根本原因。#include QApplication #include MainWindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }app.exec() 启动事件循环后程序才会持续响应按钮点击、定时器超时和窗口重绘。如果把 window.show() 写成了 window.showWindow()编译会立刻报错但如果你忘了调用 show程序不会报错只会出现一个“幽灵进程”——窗口不出现但进程不退出。这类问题的排查思路是先看 main 函数是不是正确进入了事件循环再去看控件是否被添加到了布局里。3. 把“养猪”做成模拟器状态系统、定时器循环与随机事件3.1 设计一天的时间粒度Tick 还是按秒模拟模拟游戏的“时间感”是靠定时器驱动的Qt 里最常用的是 QTimer。常见做法是设定一个游戏内 tick 等于现实时间的 1 秒也就是每个 QTimer 超时信号代表游戏中过去了 1 天。这里的关键参数是 setInterval(1000)1000 毫秒触发一次 timeout 信号。在 GameEngine 里通常会有这样的循环更新逻辑对每一头猪依次执行 updateDaily再检查是否触发随机事件最后发出一个 stateUpdated 信号通知界面刷新。这套流程和实时策略游戏的“心跳逻辑”是同构的只是规模小得多。void GameEngine::onTick() { m_dayCounter; for (auto pig : m_pigs) { pig.updateDaily(); addRandomEventsFor(pig); } emit stateUpdated(m_dayCounter, m_pigs); }这个函数写在 GameEngine 内部通过 connect 把它连到 QTimer 的 timeout 信号上。关键点在于不要在 onTick 里直接操作 QLabel 或 QProgressBar因为引擎层根本不认识界面控件。它只负责改变数据然后发信号由 MainWindow 里面的槽函数把数据映射到控件上。这样做的好处是把“游戏规则”和“UI 表现”拆开后续无论改成命令行版还是网页版核心逻辑都能复用。3.2 体重增长与健康值用分段公式别用一条直线课设答辩时老师常问的一个问题是“你的游戏平衡性怎么设计的”。如果你说“每天体重加 1 公斤健康值减 2”那就太单薄了。更合理的做法是按成长阶段分段设定增长速率同时引入健康值对增长速度的修正。void Pig::updateDaily() { m_ageDays; // 饱食度每日自然消耗 m_satiety qMax(0.0, m_satiety - 8.0); // 清洁度每日自然下降 m_cleanliness qMax(0.0, m_cleanliness - 5.0); // 健康不独立变化而是根据前两项推导 if (m_satiety 20.0 || m_cleanliness 30.0) { m_health qMax(0.0, m_health - 5.0); } else { m_health qMin(100.0, m_health 2.0); } // 体重增长受健康系数和阶段影响 double healthFactor m_health / 100.0; double dailyGain 0.0; switch (m_stage) { case GrowthStage::Piglet: dailyGain 0.6; break; case GrowthStage::Growing: dailyGain 1.2; break; case GrowthStage::Adult: dailyGain 0.8; break; case GrowthStage::Ready: dailyGain 0.2; break; } m_weightKg dailyGain * healthFactor; recalcStage(); }这个 updateDaily 里的减法用了 qMax 带下限保护防止数值变成负数。体重增长用的是“阶段系数 * 健康系数”的乘法模型比固定增量更接近真实养殖规律——猪在成长期长得最快成熟后增速放缓。这正是答辩时能拉开差距的设计细节。健康值不是独立随机数而是饱食度和清洁度的衍生值这会让玩家感受到“操作影响状态状态影响结果”的因果关系。3.3 随机事件把课设从“展示品”变成“可玩项目”一个没有随机事件的养猪模拟玩起来就像进度条玩家点几下就腻了。加入随机事件是提升可玩性的最便宜手段。常见事件包括猪突然食欲下降、天气变化导致清洁度降低、发现猪腹泻需要治疗、市场价格波动影响出栏收益。随机事件在 Qt 里只需要用 QRandomGenerator这是 Qt 5.10 之后推荐的随机数接口。void GameEngine::addRandomEventsFor(Pig pig) { auto* rng QRandomGenerator::global(); // 5% 概率触发食欲下降事件 if (rng-bounded(100) 5) { emit eventTriggered(pig.name(), 食欲下降); pig.adjustSatiety(-15); } // 3% 概率触发天气事件影响清洁度 if (rng-bounded(100) 3) { emit eventTriggered(pig.name(), 暴雨); pig.adjustCleanliness(-10); } }QRandomGenerator::global() 是全局线程安全的随机数生成器bounded(100) 返回 0 到 99 的整数所以 5% 概率就是小于 5。注意这里不要自己用 rand() 和 srand()因为 Qt 的事件循环和渲染线程可能对全局状态有干扰用 Qt 自带的生成器更稳妥。事件触发后通过 eventTriggered 信号把文本消息推给界面让 UI 显示一条日志这样玩家就知道发生了什么。3.4 存档与读档用 QJsonDocument 做序列化课程设计的加分项之一就是“关闭程序再打开游戏还在”。实现存档最可靠的方式是 JSON 格式Qt 的 QJsonDocument 可以方便地把嵌套结构写成文件。你可能听说过 QSettings 也能做但 QSettings 更适合存配置而存档这种结构化的数据用 JSON 更清晰。bool GameEngine::saveGame(const QString filePath) { QJsonObject root; root[day] m_dayCounter; QJsonArray pigArray; for (const auto pig : m_pigs) { QJsonObject pigObj; pigObj[name] pig.name(); pigObj[weight] pig.weight(); pigObj[health] pig.health(); pigObj[satiety] pig.satiety(); pigObj[cleanliness] pig.cleanliness(); pigObj[age] pig.ageDays(); pigArray.append(pigObj); } root[pigs] pigArray; QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(QJsonDocument(root).toJson()); file.close(); return true; }方法里用 QJsonObject 组装数据结构再用 QJsonArray 保存猪的列表最后通过 QJsonDocument::toJson 一次性写成带缩进的文本文件。读档的过程基本就是反向操作唯一要注意的是 QJsonValue 的类型转换如果读出来是空值就说明 JSON 字段名对不上这种问题在后续维护里最容易遇到调试时可以先把文档 qDebug 出来看看。4. 让界面“活”起来Qt Designer 布局、状态刷新与用户交互4.1 用 Qt Designer 搭静态骨架别用手写代码堆控件如果你还在用代码逐个 new QLabel 和 QPushButton那效率太低了。Qt Designer 的 .ui 文件可以直接拖拽布局生成后 AUTOUIC 会自动编译到项目里。课程设计里我一般建议手动把整体框架分成左、中、右三块左侧放猪的信息面板中间放主视图右侧放操作按钮和事件日志。在设计器里要注意的一个坐标参数是 sizePolicy它会决定控件在窗口缩放时的行为。猪的信息面板建议设置成 Expanding操作按钮设置成 Preferred这样窗口拉大时信息面板会摊开按钮区保持紧凑。这个细节在平时看不出差别但放到高分屏上或者答辩投影时观感差距会非常明显。4.2 进度条与状态面板把 Pig 的数据映射到 UI界面的核心作用是把 Pig 对象的四个关键数值——体重、健康、饱食度、清洁度——实时呈现出来。QProgressBar 非常适合表现 0 到 100 的状态值而体重需要转换成整数再显示成文本。void MainWindow::onStateUpdated(int day, const QVectorPig pigs) { m_dayLabel-setText(QString(第 %1 天).arg(day)); const Pig pig pigs.first(); m_weightLabel-setText(QString(%1 kg).arg(pig.weight(), 0, f, 1)); m_healthBar-setValue(static_castint(pig.health())); m_satietyBar-setValue(static_castint(pig.satiety())); m_cleanBar-setValue(static_castint(pig.cleanliness())); // 根据健康值调整进度条颜色低于 30 显示红色 if (pig.health() 30.0) { m_healthBar-setStyleSheet(QProgressBar::chunk { background: #e74c3c; }); } else { m_healthBar-setStyleSheet(); } }arg 的第二个参数 0 表示最小宽度为 0f 表示浮点数格式1 表示保留一位小数。把 double 转成 int 再传给 setValue 是因为进度条的 value 是整数类型。颜色变化用 setStyleSheet 动态调整这里不推荐直接在构建时写死样式因为状态不同时期显示不同的颜色才能给玩家直观的预警感。这段槽函数通过 connect 连接到 GameEngine 的 stateUpdated 信号就完成了数据到界面的单向流动。4.3 用户操作按钮如何最终改变猪的状态用户点击“喂食”按钮后事件链路是这样的按钮的 clicked 信号触发 MainWindow 里的一个私有槽槽函数调用 GameEngine 的 feedCurrentPig 方法GameEngine 修改 Pig 对象然后立刻发出 stateUpdated 信号界面随之刷新。这中间没有哪一行代码是“直接改控件”的。void MainWindow::onFeedButtonClicked() { double amount m_feedAmountSpin-value(); m_engine-feedCurrentPig(amount); appendLog(QString(你给 %1 喂了 %2 kg 饲料) .arg(m_engine-currentPigName()) .arg(amount)); }这里引入 QDoubleSpinBox 作为喂食量输入控件而不是用固定的按钮点击一次加 10 克能让玩家对“操作力度”有感知。appendLog 是自定义的日志函数往 QPlainTextEdit 里追加一行并将光标移到底部这样玩家能一直看到最新的反馈日志。这种“操作后立刻通过信号刷新全部状态”的模式比手动逐个更新控件代码量少得多而且结构上不容易漏刷新。4.4 状态刷新方案一次性全量更新还是增量更新有的课设会把刷新做成增量式比如只更新被修改过的猪。但在这个项目里猪的数量通常小于 10 头全量更新完全够用。我建议直接每次发送整个 pigs 向量界面遍历刷新所有控件。这个选择能让代码更简单可靠把精力省下来去处理更有价值的问题。如果你打算做多猪同场养殖那就要注意全量刷新时不能重建整个布局或删除所有控件。正确做法是维护一个猪信息卡片列表按索引去更新已有卡片的 QLabel 文本而不是 clearLayout 再重建。后者会造成界面闪烁而且不停地 new 对象会带来内存压力。5. 避坑清单课设里最容易翻车的 6 个“玄学”问题5.1 Qt 版本混用导致链接失败现象在别人电脑上能编译到自己电脑上报错fatal: cannot mix incompatible Qt library (version ex50601) with this library或者一连串 undefined reference。原因最常见的是系统里装了多个 Qt 版本CMake 的 find_package 找到了一个版本而编译器实际链接的是另一个版本。还有就是 Qt 5.15 与 Qt 6 混用它们虽是相似接口但底层 ABI 不同绝对不能混链。解决先彻底卸载多余版本或用 Qt 自带的 MaintenanceTool 清理。然后在 CMakeLists 中明确写 find_package(Qt5 5.15 REQUIRED)并在 Qt Creator 的构建套件Kit里指定同一个 Qt 版本的 qmake 路径。检查构建输出里 qmake 的路径确保和 CMake 找到的路径一致这个检查能节省大量排查时间。5.2 中文路径导致构建失败或运行时找不到插件现象项目目录放在 D:\课程设计\PigFarm编译时出现无法找到文件或运行时报 could not find the qt platform plugin windows。原因Qt 的构建工具链对中文路径支持不是完美无缺尤其是某些老版本 MinGW 在解析带中文的路径时会出乱码导致临时文件无法生成。运行时找不到 platform plugin 也有可能是环境变量 QT_QPA_PLATFORM_PLUGIN_PATH 指向了错误的插件目录。解决课程设计项目目录全用英文。这里想确认一下你刚才提到的目录结构是直接把整个项目放在 D:\ 下面还是在 D:\ 下建了一个英文文件夹再放项目通常建议是 D:\PigFarm 或 C:\workspace\PigFarm。如果是在中文目录下开发的先把整个项目复制到英文路径再重新编译。同时在 Qt Creator 的“运行”设置里检查工作目录是否含中文很多隐藏的启动失败都源于此。5.3 QTimer 回调里操作已销毁的控件导致崩溃现象游戏运行过程中点击关闭窗口程序崩溃断点定位在某个 setText 调用附近。原因窗口关闭时QTimer 没有停止定时器回调还在触发而回调函数里访问的控件已经被销毁。这是 Qt 事件循环一个典型的使用误区——对象销毁顺序没处理好。解决在 MainWindow 的析构函数中显式调用 m_timer-stop()或者更好的是利用 QObject 父子机制把 QTimer 的 parent 设为 MainWindow但析构时也要显式停掉因为 stop 和 delete 是两回事。更稳妥的做法是设置窗口关闭时退出事件循环。MainWindow::~MainWindow() { m_timer-stop(); }这个析构函数写在 MainWindow 里建议在窗口关闭前就把定时器停掉。注意如果你在 closeEvent 里 deleteLater 了某些控件也要保证不会再有信号触发它们的方法。5.4 中文字符串在界面上显示为乱码现象窗口标题、按钮文字都是问号或乱码但控制台输出正常。原因代码文件本身保存编码和编译器默认执行字符集不一致。Qt 5 默认假定源码是 UTF-8但 MSVC 编译器在没有 /utf-8 选项时会把源码按本地代码页GBK解析。解决在 CMakeLists 中加入针对 MSVC 的编译选项。if(MSVC) add_compile_options(/utf-8) endif()这个是 CMake 的标准写法通过判断编译器版本为 MSVC 环境添加 UTF-8 编译参数。如果你用的是 MinGW一般不会出现这个乱码问题因为 MinGW 默认就是 UTF-8。另一个小技巧是所有界面字符串都放在 QStringLiteral 或 tr() 里不要直接用 char* 拼接中文。5.5 高 DPI 缩放下界面模糊或按钮变形现象在 2K 或 4K 屏幕上窗口显示出来字很小图片模糊布局挤压。原因Qt 5.6 后默认支持高 DPI但如果你没有设置 setAttribute(Qt::AA_EnableHighDpiScaling)在部分平台上仍会按 96 DPI 来渲染。解决在 main.cpp 的 QApplication 创建之前设置属性标志。这是 Qt 官方支持的写法可以让程序在 Windows 和 Linux 上自动适配缩放。注意要在创建 QApplication 之前设置否则不会生效。如果你开发环境下一切正常到答辩机器上变模糊优先检查这条。5.6 存档文件打不开或读出来是空数据现象saveGame 返回 true但 loadGame 之后所有数值都是 0。原因多因为 JSON 字段名拼写不一致。比如保存时写的是 weight读取时用的是 weightKgQJsonValue 读不到 key 时返回的是空值而空值转 double 就是 0。解决把保存和读取的 JSON 字段名统一抽成常量不要在两个函数里各写一遍字符串。更简单的方法是保存时直接用结构体转 QJsonObject读取时也按同一个结构体反向解析。调试时在加载函数最后用 qDebug() QJsonDocument(root).toJson() 打印一次看看内容是否完整。6. 收尾技巧从“能跑”到“值得展示”的测试、数据可视化与答辩加分项目代码都正常跑通之后还有几件事能显著拉开和其他课设的差距。第一件是主动写测试用例虽然很多人觉得课设不需要测试但只要有几个断言来验证核心逻辑比如“喂食后饱食度不超过 100”“健康值低于 20 时体重停止增长”就能证明你的代码是可验证的这在答辩时很加分。第二件是把数据可视化做得更有说服力。我建议在 UI 里加入一个简单的折线图显示猪的体重随天数增长的趋势。Qt 里可以引入 Qt Charts 模块或者自己用 QPainter 画一个二维坐标折线。如果不想引额外模块用 QCustomPlot 也可以但那是第三方库需要额外说明来源。手动画线的成本其实很低用 QPainter 的 drawLine 把历史体重逐点连接起来再加两条辅助线标记出栏标准三五十行代码就能完成。第三件事是准备一份项目说明文档重点写清楚类的职责划分、信号槽的通信链路、模拟引擎的时间循环、存档格式设计。文档里不要大段贴代码而是画清楚调用关系。比如“点击喂食按钮后 clicked 信号触发 MainWindow::onFeedButtonClicked该方法调用 GameEngine::feedCurrentPig进而修改 Pig 对象并触发 stateUpdated 信号最终刷新 UI”这段链路写明白答辩基本就能立于不败之地。最后提醒一下工作目录和发布路径的匹配问题。我见过不少项目在开发环境跑得好好的拷到别的机器上就报“could not find the qt platform plugin”错误这是因为 Qt 的发布需要带上对应的 plugins 目录。如果你用 windeployqt 发布注意它生成的是针对当前构建套件的插件版本用的是 debug 库打包就会缺失 release 插件。发布给老师看的时候建议打一个 release 包再用 windeployqt 部署到单独的目录不要直接拷贝整个构建目录。这些技巧里我印象最深的是那次因为 MSVC 编码问题导致整个界面的中文全部变成了问号当时以为是 Qt 安装坏了重装了两次 Qt 才反应过来是编译参数的问题。后来养成习惯新项目第一件事就是把 /utf-8 选项写进 CMakeLists。希望这个习惯也能帮到你如果你的课设正在赶工有一条路径值得优先走先把纯逻辑代码写完并用 qDebug 验证再套上 Qt Widgets 界面。祝你的养猪场早日出栏。本文还有配套的精品资源点击获取