尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从类图到战斗循环:宠物小精灵游戏的C++面向对象设计

发布时间:2026/9/28 2:58:15

资讯中心
01
ARTICLE

从类图到战斗循环:宠物小精灵游戏的C++面向对象设计

从类图到战斗循环:宠物小精灵游戏的C++面向对象设计
简介这是一份基于C完成的宠物小精灵对战游戏课程设计资料适合高校学生用于面向对象课程设计、大作业或项目入门。资源共63个文件包含10个cpp源码、4个h头文件以及配套工程文件vcxproj/sln另有课程设计报告docx和实验说明pdf压缩包大小1.23MB结构清晰便于查阅。项目由基础实验和综合实验组成基础部分涵盖矩阵运算、形状继承、虚函数与抽象类、运算符重载、I/O流等典型知识点综合实验实现宠物小精灵对战涉及C/S模式通信、用户注册登录、精灵获取/送出、升级与技能释放等完整功能逻辑。报告中对实验设计、代码实现和运行效果进行了说明适合对照源码理解面向对象三大特性及实际工程组织方式。该资源已有115人学习下载可作为快速上手C课程设计的参考模板。1. 课程设计里的宠物小精灵游戏为什么我要劝你把类图画在写代码之前这道题拆开看其实是两层东西一层是“宠物小精灵游戏”这个壳另一层是“面向对象课程设计”这个魂。哪怕你把画面做成黑白字符界面只要用封装、继承、多态把精灵、技能、战斗系统组织清楚老师照样给高分反过来如果你用三个大全局数组硬凑出一个能跑的 C 游戏那这分基本就白丢了。做这类课程设计最常见的翻车方式是拿到题目就开写 main 函数写到一半发现精灵种类和技能逻辑像蜘蛛网一样缠在一起。我的建议正好相反先把类和类之间的关系画明白再动手写代码。这也是这篇文章想讲清楚的事——把一份“宠物小精灵游戏”的 C 面向对象设计从类图到战斗循环拆开给你看最后聊几个让报告和答辩都体面的细节。适合人群很明确正在做 C 课程设计、想借游戏练面向对象手感或者拿到一份源码但不知道怎么给老师讲清楚的同学。2. 用继承和多态把精灵世界立起来类的设计决定你后面能少改多少代码2.1 为什么这个项目天然适合继承is-a 关系一眼就能看明白宠物小精灵游戏里最核心的领域概念是“精灵”。皮卡丘是一只精灵妙蛙种子是一只精灵小火龙也是一只精灵。“皮卡丘 is-a 精灵”这句话在 C 里翻译过来就是皮卡丘这个类公有继承精灵这个基类。这就是课程设计里最想看到的继承用法——不是为了用继承而用继承而是业务模型本来就有清晰的层级。基类放什么派生类放什么这是设计的第一道分水岭。我一般把所有精灵都共有的东西放进基类名字、生命值、攻击力、防御力、速度、属性火水电草以及受伤、判断存活、复位这些基础操作。每个精灵不一样的地方——技能名称、伤害算法——放进各自的派生类。这样当你加一个新精灵时不需要改动任何战斗逻辑只需新增一个派生类用 override 把技能行为写掉即可。这就是面向对象课程设计报告里最值得写的一段话开闭原则在游戏里的落地。很多同学会把属性表哪个精灵克哪个属性做成基类里的一个巨型 switch这也能跑但派生类的存在感会大幅下降。答辩时老师通常会问“你这个多态体现在哪”如果你指着 switch 说是多态那基本会被追问到说不出话。2.2 头文件就是给老师看的类图最小可用的精灵类怎么写先给出一份精简但完整的头文件。通常在课程设计里我会拆成 pokemon.h 和 pokemon.cpp这里先看声明部分。// pokemon.h #pragma once #include iostream #include string #include cstdlib #include ctime using namespace std; // 属性枚举克制关系在战斗模块里单独查表 enum class Element { NONE, FIRE, WATER, GRASS, ELECTRIC }; // 精灵基类所有派生类共有的数据和操作都放在这里 class Pokemon { protected: string name; int hp; int maxHp; int atk; int def; int speed; Element element; public: Pokemon(const string name, int hp, int atk, int def, int speed, Element element) : name(name), hp(hp), maxHp(hp), atk(atk), def(def), speed(speed), element(element) {} virtual ~Pokemon() {} // 基类析构必须虚否则派生类资源无法释放 virtual string skillName() const 0; // 每个精灵技能名不同 virtual int calcDamage() const 0; // 每个精灵伤害算法不同 void takeDamage(int dmg) { hp - dmg; if (hp 0) hp 0; } bool isAlive() const { return hp 0; } string getName() const { return name; } int getHp() const { return hp; } int getSpeed() const { return speed; } Element getElement() const { return element; } // 一局结束后把精灵状态复位方便再来一局 void reset() { hp maxHp; } };这里的关键点是 protected 区域。为什么选 protected 而不是 private因为派生类要直接访问 atk、def 这些成员来写自己的技能算法但如果全部 public外部代码就可以随手把血量改成 9999封装就形同虚设。protected 是“只让子类碰”的边界。再一个是两个纯虚函数。加了 0之后 Pokemon 就变成了抽象类不能直接创建“一只精灵”对象只能拿它做指针、做引用。这是刻意的现实中不存在一只没有属性的泛化精灵你创建的必然是电气鼠、火焰犬这类具体精灵。同时纯虚函数也强制每个派生类必须实现自己的技能逻辑漏写一个都会编译报错。课堂上学“抽象类不能实例化”可能没什么感觉在这里一下就明白了这个设计约束防止你写出半吊子的派生类。2.3 用派生类表现精灵差异多态到底在哪里触发基类定好接口之后派生类的工作就非常机械了。常见做法是每个派生类只写自己的构造函数和两个 override 函数。下面以两只精灵为例你可以看到它们的行为差异完全被封装在类内部战斗模块完全不知道技能的具体实现。// pokemon.cpp 的一部分 #include pokemon.h // 电气鼠高速度技能是十万伏特伤害带随机波动 class ElectricRat : public Pokemon { public: ElectricRat() : Pokemon(电气鼠, 120, 32, 18, 40, Element::ELECTRIC) {} string skillName() const override { return 十万伏特; } int calcDamage() const override { // 基础攻击力 0~9 的随机浮动体现技能不稳定 return atk rand() % 10; } }; // 火焰犬高攻击血薄技能是烈焰冲撞 class FireDog : public Pokemon { public: FireDog() : Pokemon(火焰犬, 100, 40, 15, 30, Element::FIRE) {} string skillName() const override { return 烈焰冲撞; } int calcDamage() const override { // 大招伤害高但有概率烧到自己用随机数模拟副作用 if (rand() % 100 15) { return atk * 2 - 10; } return atk; } };这两只精灵的攻击逻辑完全不同一个稳定但有波动一个偶尔爆发但可能自伤。但这些差异对调用方完全透明。战斗系统里只需要拿到基类的引用调用calcDamage()C 会在运行期根据对象真实类型去找到对应的函数版本这就是运行时多态。我记得第一次在课程设计里看到这种写法时挺震撼的同样的三行代码结果却因为对象不同而不同这就是虚函数表在做的事。答辩的时候你可以把这句话说出来比空谈“多态很有用”有说服力得多。2.4 封装边界私有、保护和友元别乱用再往前一步讨论封装边界。很多初学者拿到类就喜欢全部 public或者全部 private前者丧失保护后者让你写出大量 getter 和 setter代码瞬间膨胀看起来像 C 语言里的一堆全局函数。我的经验是三条规则第一基础属性、状态数据一律放在 protected 或 private 下只暴露行为。第二对外提供给用户在这里就是 main 函数和战斗模块的操作全部控制访问比如血量只允许减少不允许外部直接赋值。第三友元尽量别用尤其是为了“方便测试”把战斗函数声明成友元这种设计答辩时会被老师点名问“友元破坏了什么”你就只能支支吾吾了。标准回答是友元让外部函数可以访问私有成员破坏了封装边界如果真需要批量操作请改成类内部的公有接口。3. 回合制战斗与游戏主循环把状态机写清楚代码路径才不怕绕3.1 游戏主循环只用状态驱动菜单、选精灵、战斗、结算宠物小精灵这种回合制游戏主线程不需要搞什么高性能渲染循环一个 while switch 状态机就够了关键是你得先把有哪些状态列出来。游戏过程可以切分成四个阶段主菜单、选择精灵、战斗阶段、战斗结算顺便问一句还玩不玩。这一步想不清楚后面很容易写出互相 goto 的逻辑。// main.cpp 的主循环骨架 enum class GameState { MENU, // 主菜单 SELECT_POKEMON, // 玩家选择精灵 BATTLE, // 战斗进行中 RESULT, // 战斗结束显示胜负 QUIT // 退出游戏 }; int main() { srand(static_castunsigned(time(nullptr))); // 随机种子后面详谈 GameState state GameState::MENU; Pokemon* playerPokemon nullptr; Pokemon* enemyPokemon nullptr; while (state ! GameState::QUIT) { switch (state) { case GameState::MENU: { // 打印菜单读取输入把状态切到 SELECT_POKEMON state GameState::SELECT_POKEMON; break; } case GameState::SELECT_POKEMON: { // 玩家挑选自己的精灵敌方精灵随机生成 // playerPokemon 和 enemyPokemon 在 new 出来之后 // 以基类指针的形式交给战斗模块 state GameState::BATTLE; break; } case GameState::BATTLE: { // 调用战斗函数输出战斗过程日志 state GameState::RESULT; break; } case GameState::RESULT: { // 判断胜负问玩家再玩一次还是退出 // 如果是再玩一次记得把精灵 reset state GameState::SELECT_POKEMON; break; } } } delete playerPokemon; delete enemyPokemon; return 0; }这段代码在面向对象层面有两点值得注意。一是精灵对象用基类指针Pokemon*持有指向 new 出来的派生类对象这正是多态发挥作用的前提二是在退出时统一 delete再把析构函数设计成 virtual形成一个完整的资源释放链路。如果你把精灵声明成栈上对象ElectricRat playerPokemon也不是不行但你将失去多态的灵活度——后面想加入“场地对某种属性加成”的功能时会需要一堆类型判断非常痛苦。参数说明状态枚举的值本身没有特殊含义但要注意每个 case 里在切换状态前把该做的输出都做完防止进入下一状态时界面信息缺失。很多同学在这里写乱就是因为一个 case 里既处理输入又处理逻辑还负责切状态导致代码高耦合。其实这个骨架唯一的职责就是“按状态分发”具体动作都拆到函数里这个习惯对后面扩展存档功能也很友好。3.2 伤害计算公式与随机数参数怎么定才既有手感又不失衡战斗系统是玩家体验的核心。伤害公式没有必要照搬宝可梦官方数值作为课程设计能做出一套自洽、可见、可控的公式就够了。我常用的公式是基础伤害 攻击技能的结算值 × 100 / (100 防御 × 2)这个公式的好处是防御堆得再高也不会减伤到溢出且攻防差值不那么容易一击秒杀。攻击技能的结算值来自派生类的calcDamage()而这只是基础值最后还要做属性克制修正。电气鼠打水系精灵时伤害翻倍打草系时减半这些规则用一个独立的克制判断函数查表即可。// battle.cpp 中的关键逻辑 #include pokemon.h // 属性克制表被克制的属性造成 2 倍伤害抵抗则 0.5 倍 bool isSuperEffective(Element atk, Element def) { return (atk Element::FIRE def Element::GRASS) || (atk Element::WATER def Element::FIRE) || (atk Element::GRASS def Element::WATER) || (atk Element::ELECTRIC def Element::WATER); } int calcRealDamage(const Pokemon attacker, const Pokemon defender) { // 从派生类多态获取技能基础伤害 int base attacker.calcDamage() * 100 / (100 defender.def * 2); if (isSuperEffective(attacker.getElement(), defender.getElement())) { base base * 2; // 克制翻倍 } else if (isSuperEffective(defender.getElement(), attacker.getElement())) { base base / 2; // 被克减半 } return base; } // 一回合的对打速度快的先出手每方只打一次 void fightRound(Pokemon a, Pokemon b) { Pokemon* first (a.getSpeed() b.getSpeed()) ? a : b; Pokemon* second (first a) ? b : a; int dmg calcRealDamage(*first, *second); second-takeDamage(dmg); // 输出second 受到 dmg 点伤害剩余 HP xxx if (second-isAlive()) { dmg calcRealDamage(*second, *first); first-takeDamage(dmg); // 输出反击造成的伤害 } }随机数这块有个细节特别容易翻车。rand()生成的是一个伪随机序列如果你不在程序入口调用srand(time(nullptr))播种那么每次运行游戏第一场战斗的伤害序列完全一样。我见过同学在同一台机器上演示三次三次都是“火焰犬暴击 15 点”老师当场就问是不是写死了。正确做法就是main一开头调用一次srand(static_castunsigned(time(nullptr)))。注意是播一次种不是每回合都种——每回合播种反而会让随机性变差因为 time 返回秒级精度循环内多次调用可能得到同一秒值从而出现大量相同结果。参数怎么调我一般先定精灵总血量在 100 到 200 之间攻击力在 30 到 50 之间防御在 10 到 25。这样一场战斗大约在 3 到 6 回合结束观感不拖沓。如果你的公式导致总是 10 回合以上说明防御系数偏高把 100 / (100 def * 2) 改成 100 / (100 def) 或者直接降低防御数值即可。这个调节过程完全可以写进课程设计报告的“参数调优”小节老师看到的是工程思维。3.3 菜单与输入处理面向对象封装的一个隐藏考点战斗模块做完之后菜单和输入看起来是体力活其实这里藏着一个很容易被忽略的问题菜单的输入读取方式会影响你后面战斗指令的输入方式。很多课程设计都是先在菜单用cin choice到了战斗回合想用getline读取玩家指令然后发现输入流里残留的回车把第三次读值直接跳过了。我的做法是统一封装一个输入工具函数内部清空输入缓冲再读取。这也是面向对象的体现——调用方不需要知道输入流里有什么脏数据工具函数负责处理。// input_util.h #pragma once #include iostream #include limits // 读取一个整数选项失败时返回 fallback int readChoice(int fallback 0) { int value; if (!(std::cin value)) { std::cin.clear(); std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); return fallback; } return value; }这个工具有两个关键点cin.clear()清除错误状态cin.ignore(...)把残留的坏数据全部扔掉避免影响下一次输入。你在战斗回合里读“攻击/防御/逃”这类字符串指令时同样走这个函数只是把解析动作换成对字符串的判断。把输入全部集中到一个模块后期做存档读档也不用在大大小小的 case 里到处打补丁。战斗状态本身可以用一个局部变量记录回合数达到上限平局收场。这些细节都在报告的“功能设计”小节里提一下观感会好很多——毕竟课程设计看重的不只是能不能玩而是你有没有把循环、输入、状态这三样东西理清楚。4. 课程设计最容易踩的五个坑从编译闪退到报告被杀4.1 析构函数不写 virtual内存泄漏直接让答辩翻车现象程序跑完没报错但关闭终端时明显卡一下甚至出现一个隐晦的调试断言窗口。原因你通过Pokemon*删除一个派生类对象时如果基类析构函数不是 virtualC 就只调用基类的析构。这里如果派生类持有堆内存比如火焰犬内部 new 了一个技能特效对象那么它的析构函数不会被调用那块内存就泄漏了。更麻烦的是这种问题在 Debug 版里会触发 _CrtDumpMemoryLeaks 报告答辩现场弹出来简直是灾难现场。解决基类析构函数写上virtual ~Pokemon() {}一行一劳永逸。平时写带继承关系的类基类析构一律虚析构别问“有没有必要”这是 C 的默认素养。也可以在报告里专门写一小段“为什么只有基类需要虚析构”这是面向对象里既基础又容易被追问的点答上就是加分项。4.2 srand 没播时间种子每次开局的伤害序列完全一样现象运行两次游戏第一回合伤害数字一模一样或者切换到某个精灵后暴击触发时机完全复现像按剧本演一样。原因rand()的随机性建立在种子上不调用srand(time(NULL))时种子默认是 1每次程序的伪随机序列当然完全相同。而如果多人把srand写在战斗函数里由于 time 粒度是秒同一秒内多次调用种子没变化随机数反而被“固化了”。解决在main入口处调用一次srand(static_castunsigned(time(nullptr)))。如果要更精细的随机体验从 C11 开始可以使用random库std::mt19937配合std::uniform_int_distribution是更现代的做法。课程设计用rand()能讲清楚原理即可但如果你想让报告有点“技术感”把选mt19937的理由写成“伪随机质量更好、周期更长”是完全没有问题的。4.3 中文全部乱码GBK、UTF-8 与 MinGW 的编码冲突现象在家用 Visual Studio 编译控制台输出“电气鼠”正常换到 Dev-C 或者 VS Code 的 MinGW 环境全部变成“娑 饱”之类的乱码字符。原因Visual Studio 在国内默认把源文件按 GBK 保存而 MinGW 默认按 UTF-8 读取源码。源码里字符串字面量的编码和编译器读取它的编码不一致输出自然乱套。这不是游戏逻辑的 bug但老师不同环境下编译一跑印象分直接打七折。解决在项目根目录放一个README写明“统一用 UTF-8 编码保存源码GBK 用户请先另存为”。如果你不写中文输出纯英文界面可以彻底规避这个问题。我平时更推荐后者——英文菜单并不跌份反而显得清爽而且避免了答辩时字体问题。如果题目要求中文界面那就坚持用 UTF-8 with BOM这是 GCC 和 MSVC 都能比较友好处理的格式在 VS Code 右下角可以直接切换文件编码。4.4 菜单输入被跳过cin 残留回车坑了 getline现象进入战斗回合后输入“攻击”选项程序不读你的指令直接跳过一回合或者连续输入两次才有反应。原因cin choice读走某个整数后回车键产生的换行符还残留在输入缓冲区里。接下来如果用getline(cin, command)读取一行字符串它读到的就是一个空串整个回合就被闪掉了。解决如 3.3 节所说统一用readChoice处理数字输入并在读字符串输入前清空缓冲区。讲到具体原理残留的换行符是因为格式化输入和行输入混用导致的cin.ignore()可以固定清掉一个字符最稳妥的是用cin.ignore(numeric_limitsstreamsize::max(), \n)把所有残留内容全冲掉再读后续字符串。4.5 报告堆功能截图但类图画不清现象报告的“系统实现”章节里贴了十几个运行截图但问到你“你的精灵继承关系图呢”的时候PPT 上却没有一张清晰的类图只能现场在白板上画几个方框越画越紧张。原因这是大多数课程设计报告的通病——把精力花在截图和排版技巧上忽略了核心的设计表达。课程设计报告讲的是“设计”不是“展示”。老师想看你对面向对象几个特性的理解而不是看你游戏画面做得多花哨。解决报告第二章放一张严谨的类图标注成员访问权限和三处多态关系。类图可以用 Windows 自带的绘图或在线画图工具完成不必追求专业 UML 工具。画图的核心是画清楚三层关系Pokemon 和 ElectricRat 之间的继承连线战斗模块到 Pokemon 之间的依赖以及基类里哪些是纯虚函数、哪些是虚析构函数。这张图占的篇幅不大却是整份报告的门面。答辩时直接指图说“这里就是多态触发点”比临时组织语言强太多。5. 把课程设计做到“能演示、能答辩、能加点”一个面向报告的技巧很多同学做完游戏就松口气觉得代码能跑万事大吉。真正的分水岭在最后两步能不能把代码拆成清晰的文件结构以及能不能在答辩现场快速演示“帮你拿分的点”。我自己的习惯是代码写完之后单独给 main 加一个隐藏启动参数--demo运行后自动走一遍“玩家选择电气鼠 → 与火焰犬战斗 → 输出胜负”的完整流程并且每一回合都打印攻防双方的姓名、技能、伤害、剩余血量。这个小小的演示函数答辩时非常管用你不需要在老师面前紧张地按菜单点击一句“下面我用 demo 模式展示完整流程”三秒进入战斗全程输出尽收眼底。配合这个演示我还会把战斗过程的输出同时写入battle_log.txt。原理上没有新东西就是把每次战斗的结果追加写入一个文件同时保留控制台输出。这个设计在答辩时是很好的“过程测试”证据——老师可以直接翻开日志文件看到一整场战斗的完整数据流而不是只看到一张结算界面。更重要的是写文件这个动作迫使你思考“什么数据需要留下来”这会引导你把精灵信息、伤害数据、回合结果这些概念进一步结构化而不是全塞在 main 函数里。很多高分课设的差距就在这里别人交的是“能玩的代码”你交的是“能回溯、能验证、能扩展的系统”。如果你还剩一到两天我劝你把时间优先投在三个地方第一把报告里的类图画规范哪怕是手画后拍照也比子虚乌有的截图有价值第二补一份简单的“文件说明清单”列出每个头文件和源文件的职责方便老师快速读懂你的工程第三在战斗结算界面加一句“本场战斗日志已保存”让演示链路看上去是完整的。千万不要因为追求华丽动画去动 UI课程设计评审的关注点是设计思维不是画质。这个方向值不值得投入取决于你缺的其实不是游戏性而是把类和对象关系讲透的能力——而宠物小精灵游戏恰好是练这一趟最省力的题材我当年吃过“重实现、轻设计”的亏后来学乖了凡是交出去的代码先按类图走一遍再写报告这个习惯一直用到现在。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。