ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于C++与Qt的养猪模拟游戏:状态机、信号槽与自定义绘图课设详解

基于C++与Qt的养猪模拟游戏:状态机、信号槽与自定义绘图课设详解 简介这是一份基于C与Qt开发的养猪模拟游戏课程设计资料包适合计算机相关专业学生完成C大作业、Qt项目实战或课设参考。项目实现了100个猪圈、每圈10头猪的完整养殖模拟涵盖黑猪、小花猪、大花白猪三种品种的分圈管理、猪崽自动购买与分配、体重增长刷新、出圈销售统计以及猪瘟传播模拟和近5年销售/购入记录查询界面基于Qt绘制并提供品种数量、体重、饲养时间分布图。资源共45个文件压缩包大小约7.66MB主体为14个cpp与13个h源码文件配合2个ui界面文件、2个qrc资源文件、2个wav音效及6张png图片另含项目说明md文档方便直接编译运行与二次开发。目前已有120人学习下载对于想系统掌握C链表管理、随机模拟算法与Qt界面设计的读者有较高借鉴价值可直接作为课程设计或期末大作业的完整范例。1. 落在课设周里的养猪模拟游戏C与Qt到底在考什么课程设计拿到“基于C和QT的养猪模拟游戏”这个题目时第一印象往往是个带画面和按钮的小demo窗口里放一头猪隔几秒喂一次猪长大就过关。真正动手你会发现这个课设把C的面向对象与状态机、Qt的信号槽、定时器、自定义绘图全部串在了一起难度刚好卡在“能跑”和“能讲清楚”之间。适合的读者是做CQt课设的学生或者是想快速验证自己Qt基础的在职开发。下面直接给出一份可复现的分层设计和关键代码先把猪的模型写对再把界面挂上去最后讲清楚说明书怎么写、翻车的坑在哪。2. 项目结构与Pig类设计把猪的状态机先跑在纯C里很多课设源码能跑但不好讲根源在于把界面代码和业务逻辑搅在一个槽函数里。按钮一多每个响应里都塞满了数值修改和判断答辩被问“假如要多加一头猪怎么办”就直接卡住。这一章先把项目拆干净让C核心逻辑不依赖Qt也能编译和测试界面只是挂在它上面的一层皮。2.1 先定文件布局业务逻辑不依赖Qt也能编译常见的课设目录会按功能分包。我习惯把源码根目录拆成四块核心模型、界面、资源、文档。这样做的好处是核心模型目录里可以单独写一个不含Qt头文件的控制台测试入口分分钟跑状态机验证逻辑不必每次改完代码都开整个GUI。项目结构参考如下PigFarm/ ├── Pig.pro # qmake 工程文件 ├── src/ │ ├── model/ │ │ ├── Pig.h │ │ ├── Pig.cpp │ │ └── main_console.cpp # 纯C的控制台自测入口 │ ├── ui/ │ │ ├── MainWindow.h │ │ ├── MainWindow.cpp │ │ └── main.cpp ├── resources/ │ └── images/ # 猪的位图或图标资源 └── docs/ ├── 项目说明.md └── 答辩演示脚本.md工程文件Pig.pro里需要把model和ui两个子目录都包含进HEADERS/SOURCES同时用QT widgets声明依赖Widgets模块。如果只想先验证核心逻辑而不开界面可以临时注释掉MainWindow相关文件仅保留model目录和main_console.cpp这样会连头文件里关于Qt的部分都没有链接时间极短。我一般会把这个控制台入口一直留在工程里它既是自测工具也是课设报告里写“单元测试做了什么”的实物证据。代码设计上Pig类只使用标准库不引用QObject保证纯C属性然后MainWindow通过普通getter去读写Pig的状态。这样模块边界清楚答辩时讲“我的模型和视图是分离的”才有底气。2.2 Pig类的状态机饥饿度、清洁度与健康值的联动养猪模拟的核心不是画图而是数值联动。我设计Pig时用了三个指标饥饿度hunger0~100越高越饿、清洁度hygiene0~100越高越干净、健康值health0~100归零则死亡。状态机则分为四个阶段幼仔、生长期、成熟、死亡。每天游戏时钟的每个tick各项指标按规则变化玩家通过喂食和清洁动作去干预。Pig.h的关键定义如下#ifndef PIG_H #define PIG_H enum class PigStage { Piglet, // 幼仔 Growing, // 生长期 Mature, // 已成熟可过关 Dead // 死亡 }; class Pig { public: Pig(); // 每个游戏tick调用一次推进时间 void tick(); // 玩家操作 void feed(); // 喂食降低饥饿度 void clean(); // 清洁提升清洁度 // 状态查询 PigStage stage() const; int hunger() const; int hygiene() const; int health() const; bool isWin() const; private: int m_hunger; int m_hygiene; int m_health; int m_growth; // 成长值累计到阈值升级 PigStage m_stage; void updateStage(); }; #endif // PIG_H数值用int而不是double是为了在课设演示时避免浮点误差带来的“喂一次饭饥饿值还是99”这种尴尬也方便在界面上直接显示整数。m_growth是隐藏的成长值每次tick加一点累计到阈值触发阶段迁移这个设计让“猪会长大”这个逻辑不再依赖定时器次数硬编码。Pig.cpp里的tick()是核心逻辑规则尽量直白#include Pig.h Pig::Pig() : m_hunger(20), m_hygiene(80), m_health(100), m_growth(0), m_stage(PigStage::Piglet) {} void Pig::tick() { // 先加饥饿、减清洁再判断健康影响 m_hunger qBound(0, m_hunger 5, 100); m_hygiene qBound(0, m_hygiene - 3, 100); if (m_hunger 80) m_health - 3; if (m_hygiene 30) m_health - 2; if (m_health 0) { m_health 0; m_stage PigStage::Dead; return; } m_growth 10; updateStage(); } void Pig::feed() { m_hunger qBound(0, m_hunger - 40, 100); } void Pig::clean() { m_hygiene qBound(0, m_hygiene 40, 100); } void Pig::updateStage() { if (m_stage PigStage::Dead) return; if (m_growth 300) m_stage PigStage::Mature; else if (m_growth 100) m_stage PigStage::Growing; } PigStage Pig::stage() const { return m_stage; } int Pig::hunger() const { return m_hunger; } int Pig::hygiene() const { return m_hygiene; } int Pig::health() const { return m_health; } bool Pig::isWin() const { return m_stage PigStage::Mature m_health 50; }这段逻辑里qBound是Qt提供的裁剪函数作用是把值限制在[0,100]区间比手写if判断干净得多。tick()里先用qBound更新两项指标再依据阈值扣健康值这个顺序很重要先让饥饿和清洁生效再算健康否则会出现“猪已经饿死了但界面显示还活着”的错位。updateStage()只在tick()内部调用不暴露成public防止外部任意改阶段。注意feed()直接把饥饿度减40这是因为游戏节奏要适配课设演示。如果间隔时间太长一次喂食减得太少会让人反复点按钮减太多又会让猪永远饿不死。这个40的数值属于游戏平衡参数最好提取成常量放到构造函数里后面用QSpinBox调的时候就不用改代码重编。2.3 用QTimer驱动主循环模拟时钟怎么走没有主循环的模拟游戏是假的。Qt里不需要自己写while(1)而是用QTimer定时触发Pig的tick()。我一般把定时器间隔设为1000毫秒对应游戏里的“一天”。在MainWindow构造函数里连接QTimer的timeout信号到更新方法#include QTimer #include MainWindow.h #include ui_MainWindow.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_pig(new Pig()) , m_timer(new QTimer(this)) { ui-setupUi(this); // 每1000ms触发一次游戏推进 m_timer-setInterval(1000); connect(m_timer, QTimer::timeout, this, MainWindow::onGameTick); m_timer-start(); updateStatusBar(); }这里的重点是把定时器生命周期挂在this之下写成new QTimer(this)而不是栈对象否则定时器在构造函数返回后就被销毁timeout再也不会触发。setInterval传入的是毫秒数1000就是一秒一次。如果想加快演示速度可以在界面上放一个“时间倍速”的下拉框比如500ms甚至200ms但要注意Pig类里的数值是绝对点数倍速越快游戏总时长越短。onGameTick()槽函数做的事情很纯粹调m_pig-tick()更新界面显示判断胜负。不要在这个槽函数里做任何绘图之外的重活。定时器驱动下的程序顺序是固定的timeout信号发出tick推进界面刷新玩家输入在两次tick之间生效。这跟游戏引擎里的帧循环是一回事只是把驱动源换成了Qt的事件循环。3. Qt界面从零搭起信号槽、绘制与点击喂食的完整联动有了纯C的Pig类界面层的任务只剩两件把三个指标画出来把玩家的按钮操作送回Pig。我会自定义一个QWidget作为游戏画布重写它的paintEvent来画猪和状态条按钮则通过信号槽直接操作Pig对象。整个界面层不需要自己管理状态机它只是“模型状态的投影”。3.1 自定义绘图paintEvent里画猪圈Qt Widgets的自定义绘图固定套路是继承QWidget重写paintEvent(QPaintEvent*)在事件里创建QPainter开始画。这个函数会在窗口初次显示、调用update()、尺寸变化时被自动触发不要在别的函数里直接调它。下面是一个极简GameCanvas的实现用几个几何图形代表猪圈和猪#include QPainter #include GameCanvas.h #include model/Pig.h GameCanvas::GameCanvas(QWidget *parent) : QWidget(parent), m_pig(nullptr) {} void GameCanvas::setPig(Pig *pig) { m_pig pig; update(); // 设置模型后立即重绘 } void GameCanvas::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 背景画一个浅绿色的猪圈 painter.fillRect(rect(), QColor(240, 230, 200)); if (!m_pig) return; // 根据健康值决定猪的颜色 QColor pigColor(255, 182, 193); // 默认粉红色 if (m_pig-health() 50) pigColor QColor(150, 150, 150); // 病态灰色 // 猪身体椭圆的中心放在画布中央 QRectF body( width()/2 - 100, height()/2 - 80, 200, 160 ); painter.setBrush(pigColor); painter.setPen(Qt::NoPen); painter.drawEllipse(body); // 猪眼睛两个小黑点 painter.setBrush(Qt::black); painter.drawEllipse(QPointF(body.left() 60, body.top() 60), 6, 6); painter.drawEllipse(QPointF(body.right() - 60, body.top() 60), 6, 6); // 画出饥饿度进度条贴在底部 painter.setBrush(Qt::red); painter.fillRect(10, height() - 30, width() - 20, 10, Qt::NoColor); painter.setBrush(Qt::red); painter.fillRect(10, height() - 30, (width() - 20) * m_pig-hunger() / 100, 10); }这里最关键的问题是坐标计算body的椭圆尺寸与窗口尺寸挂钩使用width()和height()而不是写死绝对值这样窗口拉伸时猪不会偏出视野。进度条长度也用百分比计算等于(width()-20)乘以hunger/100。setRenderHint(QPainter::Antialiasing, true)让椭圆边缘平滑课设截图会好看很多。paintEvent里不要尝试修改模型数据。它只读状态不做feed/clean。如果需要重绘在外部调用this-update()Qt会合并同一个事件循环里的多次重绘请求避免闪烁。另外要注意QPaintEvent参数在这次实现里没有用到可以忽略但不能删掉参数名否则编译对不上。3.2 鼠标点击与喂食信号槽的三种连接写法玩家操作按钮按钮把请求转给Pig。最直接的写法是在构造函数里connect按钮的clicked信号。Qt 5之后的语法推荐使用新式写法编译期就能检查信号与槽是否匹配比SIGNAL/SLOT宏字符串拼错后只在运行时报错安全得多。// 在MainWindow构造函数中 connect(ui-feedButton, QPushButton::clicked, this, [this]() { m_pig-feed(); updateStatusBar(); m_canvas-update(); }); connect(ui-cleanButton, QPushButton::clicked, this, MainWindow::onCleanClicked);lambda写法适合一行就能讲清楚的简单逻辑。这里用m_pig-feed()修改模型然后调用updateStatusBar()刷新QLabel再让m_canvas-update()触发重绘。注意m_pig是普通指针lambda捕获thisQt保证在发出clicked时MainWindow对象还存活所以不会悬空。如果槽函数名字有意义比如onCleanClicked直接在头文件里声明为public或private slot用成员函数写法更清晰。Qt 5新式connect里不需要写public slots关键字普通成员函数也可以作为槽。还有第三种常见做法把按钮命名成ui-feedButton然后在MainWindow里手写一个void MainWindow::on_feedButton_clicked()槽函数connect会根据命名约定自动连接前提是使用了Ui::MainWindow且按钮objectName正确。我一般不用这个隐式规则因为它是autoconnect黑匣子函数签名错一点就静默失效。3.3 状态反馈与QMessageBox游戏结束、生病的交互游戏需要给玩家反馈。我习惯在窗口底部放三个QLabel显示饥饿/清洁/健康数值同时在猪进入Mature状态且健康50时弹出胜利提示在猪死亡时弹出失败提示。注意QMessageBox::information是模态对话框会阻塞事件循环所以在弹出前要把定时器停掉否则玩家点掉对话框之前游戏还在后台跑。void MainWindow::onGameTick() { m_pig-tick(); updateStatusBar(); m_canvas-update(); if (m_pig-isWin()) { m_timer-stop(); QMessageBox::information(this, 通关, 猪已经健康长大恭喜过关点击重置开始新一局。); resetGame(); } else if (m_pig-stage() PigStage::Dead) { m_timer-stop(); QMessageBox::warning(this, 失败, 猪生病去世了。点击重置重新开始。); resetGame(); } }这里把QMessageBox的父对象设为this让弹窗随主窗口居中显示。重置游戏时调用resetGame()内部重新new一个Pig并清空canvas的模型指针。还有一个细节m_pig是堆对象在resetGame里要提前delete再new注意先delete旧对象再赋值新对象防止悬空指针被画布引用。如果对话框文字里有中文注意源文件编码这一块的坑在第5章单独讲。交互反馈的另一半是数值QLabel的刷新updateStatusBar()里用QString::number(m_pig-hunger())拼文本不要用sprintf和中文拼接因为QString和std::string混用会带来隐式编码转换问题。4. 课设说明书与答辩脚本把C源码讲成可验证的工程课设评分不只看程序跑多炫还看文档。评审老师通常先翻项目说明再问两三个“为什么这样设计”的问题。这一章讲说明书怎么写、状态迁移表怎么画、答辩演示按什么顺序讲让代码的亮点能被看见。4.1 项目说明文档的结构与核心图表文档不需要五十页但结构要完整。我常用下面这个六章结构直接映射到课设评分标准的各个条目。章节内容要求对应代码位置需求分析功能需求与非功能需求游戏规则描述总体设计模块划分图、Qt类关系图src/model与src/ui详细设计状态机、核心算法Pig.cpp、GameCanvas.cpp测试测试用例表、控制台自测结果main_console.cpp使用说明编译步骤、操作说明README截图总结遇到的坑、改进方向文档末尾总体设计里最难的是“类关系图”不要画复杂的UML画一个简单的方框连线即可Pig独立在右侧MainWindow持有Pig指针并连接QTimerGameCanvas通过setPig拿到只读引用。用Word的文本框和箭头就能完成重点标出“数据流是单向的”。详细设计里务必写清楚tick的时序逻辑。可以画一个状态迁移表这个表比五百字描述都有说服力。4.2 把tick机制和复杂度写进文档状态迁移表是答辩老师最容易认可的图表之一。按PigStage画四行三列当前状态迁移条件迁移结果Pigletm_growth 100GrowingGrowingm_growth 300Mature任意状态m_health 0DeadMaturem_health 50通关isWin同时要说明tick()里数值变化的公式比如hunger每tick加5hygiene每tick减3超过阈值对health的扣减。这些公式就是游戏平衡性的证明也是后续章节做危险动作的基本依据。复杂度方面tick()是O(1)时间整局游戏最多tick次数由成长阈值决定从0成长到300需要30次tick也就是30秒游戏时间。这个数字很有利于答辩演示因为3分钟内可以看到完整流程结束不用干等。文档里还可以单独写一节“异常处理”把QTimer停止、QMessageBox弹出、重置游戏这三个步骤的关系讲清楚。这里是一个很好的加分项当游戏结束定时器必须停止否则玩家在弹窗停留时模型还在变化。4.3 答辩演示脚本先操作后讲代码控制答辩时间演示顺序和代码讲解顺序不一定一致。我习惯先跑一遍完整游戏流程再回到代码里讲关键类。演示脚本可以按以下四步第一步启动程序展示主界面明确说出这是“基于Qt Widgets的自定义绘图界面数据模型与界面分离”。第二步先不操作让定时器走三个tick展示饥饿度从20涨到35指出数值变化规律。第三步点击喂食按钮饥饿度从35降到0同时指出feed()里qBound防溢出逻辑。第四步用调试器或输出日志展示状态迁移把猪养到Mature弹窗通关后讲解resetGame如何重置所有状态。这个流程控制在五分钟左右最合适。注意不要在演示时去改代码哪怕只是改一个颜色也要忍住。演示用的工程提前编译release版本答辩现场最怕的就是开机后重新构建等半天或者开发机临时弹插件缺失窗口。5. 常见问题排查从中文乱码到Qt崩溃的5个坑课设过程中最容易让时间流失的不是算法而是环境问题。我把自己踩过的坑按现象、原因、解决三条线列出来每一类都能在半小时内确诊。5.1 MSVC下中文全部变成乱码现象在Qt Creator里编译运行界面按钮上的“喂食”显示成“椋熼”或者QMessageBox里的提示文字全部变成问号。 原因MSVC编译器默认按本机ANSI代码页读取源文件而Qt Creator写代码默认保存UTF-8。当源文件里出现中文字符串编译器按GBK读取UTF-8字节流QString再从没有BOM的UTF-8解码时就乱套了。 解决在.pro工程文件里加一行QMAKE_CXXFLAGS /utf-8强制MSVC按UTF-8解读源文件同时把源文件重新保存为UTF-8编码。如果是Qt 6里的QString字面量可以在代码里改用QString::fromUtf8(u8喂食)作为兜底方案。5.2 信号槽连接失败槽函数没有被执行现象按钮点击完全没有反应输出窗口显示“No such slot MainWindow::onFeed(bool)”或者QObject::connect编译通过但运行时不报错。 原因常见的三种情况一是connect里使用了SIGNAL/SLOT宏拼错了槽函数名二是用重载信号时没有用函数指针指定具体版本三是把connect写在了窗口对象还没创建完成的构造函数里。 解决Qt 5之后一律用新式语法connect(button, QPushButton::clicked, this, MainWindow::onCleanClicked)重载信号比如QComboBox::currentIndexChanged用QOverload ::of显式指定参数。connect之后加一句if (!connect(...)) qWarning()把连接结果打出来是排查最快的手段。5.3 QTimer阻塞窗口无响应、猪“卡死”现象程序启动后界面还能显示但几秒后整个窗口变白标题栏提示“未响应”定时器不再刷新。 原因有人在timeout槽函数里放了一个QThread::sleep(2)模拟耗时处理或者写了while循环等待某个条件。GUI线程被阻塞事件循环停摆paintEvent和定时器都排不上队映射到界面上就是假死。 解决耗时操作不能放在GUI线程改成把工作交给QtConcurrent::run或QThread子线程。应付课设级别的逻辑最简单的是把耗时放回tick内部但必须保证每tick不超过几十毫秒如果确实需要模拟延迟用QTimer::singleShot(100, this, ...)来延迟执行下一步。5.4 绘图闪烁与坐标错乱现象点击喂食按钮后猪的位置突然跳到窗口角落或者窗口拉伸时猪被拉伸变形、出现残影闪动。 原因paintEvent里使用了固定坐标而不是相对坐标导致窗口尺寸变化后绘制内容错位。闪烁则是因为QWidget在每次重绘时先做背景清除再画前景清除和绘制之间有短暂时间差。 解决所有绘图坐标都基于size()计算禁用painter的坐标变换。用setAttribute(Qt::WA_OpaquePaintEvent)告诉Qt不需要先重绘背景然后自己用fillRect覆盖背景这样能明显减少闪烁。还有一种做法是让画布widget启用双缓冲Qt Widgets默认对QWidget开启了双缓冲不要手动关闭否则闪得厉害。5.5 换电脑运行报缺DLL与Qt崩溃现象在自己电脑上编译运行一切正常拷给同学后双击exe提示找不到Qt5Widgets.dll或者提示应用程序无法启动0xc000007b在vscode配置c/c环境下用MinGW编译的版本跑到另一台机器上直接崩溃。 原因Qt程序依赖大量动态库Debug版本依赖更多且体积大Release版本也需要Qt运行库、平台插件qwindows.dll和编译器运行时MSVC或MinGW。vscode里如果不走Qt Creator路径配置出错链接到的库版本混合也是崩溃常见来源。 解决不要拷贝Debug版exe。用Release构建后打开Qt命令行工具并切换到exe所在目录执行windeployqt 应用名.exe它会自动把依赖的Qt库和插件复制到同级目录MSVC编译后还需要把对应的VC运行库打进去或者让目标机器安装对应的Visual C Redistributable。6. 进阶验证用自动化测试和数据导出让课设多拿一个档课设做到能玩只能算及格要拉开差距可以从自动验证和数据化开始。我强烈建议保留main_console.cpp这样的控制台入口用它来跑核心逻辑的断言测试不用写Qt的QTest框架一个assert就能说明问题。先验证状态机关键路径#include cassert #include Pig.h int main() { Pig pig; // 模拟30个tick成长值满300进入Mature for (int i 0; i 30; i) { pig.tick(); if (pig.stage() PigStage::Dead) break; } assert(pig.stage() PigStage::Mature); assert(pig.isWin()); return 0; }这个测试跑通了说明没有中途死亡成长曲线符合设计。再做第二个测试连续10个tick不喂食猪应该死亡验证扣血规则。两个断言一正一反核心逻辑的正确性就有了书面证据写进项目说明“测试”章节比截图更有说服力。进阶的另一个方向是数据导出。在MainWindow里加一个“存档”按钮把Pig的四个数值序列化成QJsonObject配合QJsonDocument写入data/save.json。这样玩家退出后再启动可以恢复进度相当于引入了持久化。这部分代码量不大但让整个程序从“玩具demo”变成了“有状态的应用”。玩法扩展上可以加一个随机天气事件每天有10%概率触发“猪圈脏乱”清洁度额外下降5。实现方式是在tick()里用QRandomGenerator::global()-bounded(100)判断这个类在Qt 5.10之后都可用不需要引入C11的random组件。天气事件会破坏玩家节奏反而让游戏有策略深度。我自己课设时最大的教训是一开始把所有绘制代码写在了MainWindow::paintEvent里每次想加一个新状态就要连带改绘图段最后整个函数膨胀到两百行。后来习惯把画布单独拆成一个GameCanvas类只做表现逻辑Pig类只做数据迁移再用一个Controller类去握QTimer三层之间单向依赖。这个拆分经验救了我的答辩希望也能减少你深夜翻车的概率。希望帮到你。本文还有配套的精品资源点击获取
返回列表