☰
C++课程设计实战:三国游戏开发中的面向对象与设计模式
2026/10/14 19:24:01 网站建设 项目流程

1. 从一道实验题说起:为什么“三国游戏”是C++课程设计的经典命题

但凡学过C++的人,大概率在课程设计或者实验环节碰到过“三国游戏”这个题目。我第一次见到它的时候,心里想的是“不就是个文字游戏吗”,结果真正动手写起来才发现,这个题目几乎把面向对象编程的核心知识点全部串了一遍——类与对象、继承与多态、STL容器、运算符重载、文件读写、随机数生成、状态机设计,一个都跑不掉。

这个项目的核心目标很明确:用C++实现一个以三国时代为背景的策略类小游戏。玩家可以招募武将、组建队伍、进行回合制战斗,武将之间有不同的属性(武力、智力、统率、兵力等),战斗过程需要模拟伤害计算、技能触发、胜负判定。听起来简单,但当你真正开始设计类的层次结构时,问题就来了:武将和谋士要不要分成两个类?技能系统怎么设计才能方便扩展?战斗流程怎么组织才不会写成一坨面条代码?

这篇文章面向的是正在做C++课程设计的学生,以及想通过一个完整项目来巩固C++知识的自学者。我会从整体架构设计讲到具体代码实现,把我在做这个项目时踩过的坑、想过的方案、最终的选择和理由全部摊开来讲。代码量大概在800到1200行左右,适合作为一周左右的课程设计工作量。

需要说明的是,下面的内容是基于常见的C++课程设计实践来展开的,不同学校的具体要求可能有差异,但核心思路是通用的。

2. 整体架构设计:先想清楚“有哪些东西”再动手写代码

2.1 核心需求拆解与功能模块划分

拿到“三国游戏”这个题目,第一步不是打开IDE开始写,而是拿出一张纸,把游戏需要哪些功能列清楚。根据常见的课程设计要求,这个游戏至少需要包含以下几个模块:

  • 武将管理模块:武将的创建、属性展示、状态维护(存活/阵亡/血量)
  • 队伍编成模块:玩家从可选武将池中挑选若干武将组成出战队伍
  • 战斗系统模块:回合制战斗流程控制、伤害计算、技能释放、胜负判定
  • 交互界面模块:菜单显示、玩家输入处理、战斗过程输出
  • 数据持久化模块:武将数据的文件读写(可选但加分)

这五个模块不是孤立的,它们之间有明确的依赖关系。武将管理是基础,队伍编成依赖武将管理,战斗系统依赖队伍编成,交互界面贯穿始终,数据持久化则是锦上添花。

我见过不少同学一上来就开始写main函数,边写边想,结果写到一半发现类的关系理不清,只能推倒重来。所以我的建议是:先用注释把每个模块的核心类和接口写出来,确认逻辑通顺之后再填充实现。这个习惯能帮你省下大量返工时间。

2.2 类的层次结构设计:继承还是组合

这是整个项目最关键的架构决策。三国游戏里有很多“角色”,武将、谋士、君主,他们有一些共同属性(名字、血量、等级),又有各自特有的属性(武将偏重武力,谋士偏重智力)。

方案一:用继承。定义一个基类Character,然后Warrior、Strategist、Lord分别继承它。这是教科书式的做法,也是很多老师期望看到的。

方案二:用组合。只定义一个Hero类,用一个Role枚举来区分类型,不同属性用同一个字段表示,只是数值倾向不同。

我最终选择了继承方案,理由有三:第一,课程设计的评分标准通常明确要求体现继承和多态,这是得分点;第二,继承结构更符合现实世界的分类逻辑,代码可读性更好;第三,后续如果要扩展新角色类型(比如加入“刺客”),继承方案更容易扩展。

但继承方案也有坑。最大的坑是菱形继承——如果你设计了一个Fighter类和一个Advisor类,然后想让某个角色同时继承两者(比如诸葛亮既能打仗又能出谋划策),就会遇到二义性问题。我的处理方式是:不搞多重继承,用虚函数和接口类来替代。具体来说,定义一个ISkill纯虚接口,谁有技能谁就实现这个接口,而不是通过多重继承来获得能力。

// 基类定义 class Character { protected: std::string name; int maxHP; int currentHP; int level; bool alive; public: Character(const std::string& n, int hp, int lv) : name(n), maxHP(hp), currentHP(hp), level(lv), alive(true) {} virtual ~Character() = default; virtual int attack() const = 0; // 纯虚函数,子类必须实现 virtual std::string getType() const = 0; void takeDamage(int dmg) { currentHP -= dmg; if (currentHP <= 0) { currentHP = 0; alive = false; } } bool isAlive() const { return alive; } std::string getName() const { return name; } int getHP() const { return currentHP; } int getMaxHP() const { return maxHP; } };

这里attack()和getType()是纯虚函数,基类不提供实现,强制子类根据自己的特点来实现。这就是多态的核心——通过基类指针调用,实际执行的是子类的方法。

2.3 战斗系统的设计思路:回合制状态机

战斗系统是整个游戏的核心玩法。我采用的是回合制状态机模型:每个回合分为“玩家行动阶段”和“敌方行动阶段”,每个阶段内按照速度属性决定行动顺序。

为什么用状态机而不是简单的循环?因为战斗过程中有很多状态需要维护:当前是第几回合、轮到谁行动、是否有武将阵亡需要跳过、是否触发了特殊技能需要额外处理。如果全部用if-else堆在循环里,代码会变得非常难以维护。用状态机把每个阶段的行为封装成独立的函数,主循环只负责状态切换,逻辑就清晰多了。

enum class BattleState { PLAYER_TURN, ENEMY_TURN, CHECK_RESULT, BATTLE_OVER }; void battleLoop(Team& playerTeam, Team& enemyTeam) { BattleState state = BattleState::PLAYER_TURN; int round = 1; while (state != BattleState::BATTLE_OVER) { switch (state) { case BattleState::PLAYER_TURN: executeTurn(playerTeam, enemyTeam); state = BattleState::ENEMY_TURN; break; case BattleState::ENEMY_TURN: executeTurn(enemyTeam, playerTeam); state = BattleState::CHECK_RESULT; break; case BattleState::CHECK_RESULT: if (checkBattleEnd(playerTeam, enemyTeam)) { state = BattleState::BATTLE_OVER; } else { round++; state = BattleState::PLAYER_TURN; } break; default: break; } } }

这个结构看起来简单,但它把复杂的战斗流程拆成了几个独立的状态,每个状态只关心自己的事情。后面要加新功能(比如“每三回合触发一次天气效果”),只需要在CHECK_RESULT里加判断就行,不会影响其他部分。

3. 核心细节解析:那些教程里不会告诉你的关键点

3.1 武将属性系统的数值设计

很多同学做这个项目时,属性数值是拍脑袋定的——武力80、智力60,然后伤害公式随便写个damage = attack - defense。这样做的结果是游戏平衡性极差,要么一刀秒杀,要么打半天不掉血。

我在设计数值时参考了一个基本原则:让战斗持续3到5个回合。这意味着一个满血武将大概需要被攻击4到6次才会阵亡。基于这个目标,我反推了属性范围:

属性范围说明
武力40-100影响物理攻击力
智力30-100影响技能效果和策略防御
统率50-95影响防御力和兵力上限
速度30-90决定行动顺序
兵力(HP)800-1500基础生命值

伤害计算公式我用了这个:

基础伤害 = (攻击方武力 * 0.6 + 攻击方等级 * 5) - (防御方统率 * 0.4) 最终伤害 = 基础伤害 * 随机系数(0.85 ~ 1.15)

随机系数的引入是为了避免“计算结果完全确定”导致游戏变得可预测。但随机范围不能太大,否则玩家会觉得“全靠运气”。0.85到1.15这个区间是我测试下来比较合适的——既保留了不确定性,又不会让策略完全失效。

注意:数值设计没有标准答案,关键是你要能说清楚“为什么选这个数”。课程设计答辩时老师很可能会问这个问题,如果你能说出“我通过反推战斗回合数来确定数值范围”,这比“我随便写的”要高好几个档次。

3.2 技能系统的可扩展设计

技能系统是最容易写乱的部分。我见过有同学用一大堆if-else来判断“如果武将是关羽就放青龙偃月,如果是张飞就放咆哮”,这样写出来的代码,加一个新技能就要改十几个地方。

我的做法是用策略模式:把每个技能封装成一个独立的类,所有技能类实现同一个接口。战斗时只需要调用接口的execute方法,不需要知道具体是哪个技能。

class ISkill { public: virtual ~ISkill() = default; virtual std::string getName() const = 0; virtual int getCost() const = 0; // 技能消耗(怒气值) virtual void execute(Character* caster, Character* target) = 0; }; class HeavyStrike : public ISkill { public: std::string getName() const override { return "重击"; } int getCost() const override { return 30; } void execute(Character* caster, Character* target) override { int dmg = static_cast<int>(caster->attack() * 1.8); target->takeDamage(dmg); std::cout << caster->getName() << " 发动重击,造成 " << dmg << " 点伤害!\n"; } };

这样设计的好处是:加新技能只需要新建一个类,不用改任何现有代码。这就是开闭原则——对扩展开放,对修改关闭。答辩时如果你能提到这个设计原则,绝对是加分项。

每个武将可以持有一个std::vector<std::shared_ptr<ISkill>>来管理自己的技能列表。用shared_ptr而不是裸指针,是为了避免内存泄漏——这是C++课程设计里最常见的扣分点之一。

3.3 文件读写与数据持久化

课程设计通常要求把武将数据保存到文件,下次打开程序还能读取。这个功能看起来简单,但有几个细节容易翻车。

首先是文件格式的选择。纯文本格式(比如CSV)可读性好,方便调试;二进制格式读写快,但出了问题不好排查。我建议用文本格式,因为课程设计的重点不是性能,而是逻辑正确性。调试阶段你能直接用记事本打开文件看数据对不对,这比什么都重要。

其次是读写时的错误处理。很多同学写文件读取时不检查文件是否存在,结果程序直接崩溃。正确的做法是:

std::ifstream fin("heroes.txt"); if (!fin.is_open()) { std::cerr << "数据文件不存在,将使用默认武将数据。\n"; loadDefaultHeroes(); return; }

还有一个容易忽略的点:写入时的分隔符选择。如果你的武将名字里可能包含空格(比如“赵 云”),用空格做分隔符就会出问题。我建议用竖线|或者制表符\t作为分隔符,并且在读取时做相应的解析处理。

实操心得:每次修改文件读写逻辑后,一定要做一次“写入-关闭-重新打开-读取”的完整测试。我当初就是只测试了写入没测试读取,结果答辩演示时发现读出来的数据全是乱的,场面非常尴尬。

4. 完整实操流程:从零开始搭建这个项目

4.1 开发环境准备与项目结构

工欲善其事,必先利其器。C++开发环境的选择直接影响你的开发效率。

  • 编译器:推荐GCC 9.0以上或Clang 10以上,确保支持C++17标准。Visual Studio 2019以上也可以,但要注意有些语法在MSVC和GCC上行为不同。
  • IDE:Visual Studio Code配合C/C++插件,或者CLion。不建议用Dev-C++,它的调试功能太弱,出了问题很难排查。
  • 构建工具:项目文件不多的话,直接写Makefile或者用IDE的默认构建就行,不需要上CMake。

项目文件结构我建议这样组织:

三国游戏/ ├── include/ │ ├── Character.h │ ├── Warrior.h │ ├── Strategist.h │ ├── Skill.h │ ├── Team.h │ └── BattleSystem.h ├── src/ │ ├── Character.cpp │ ├── Warrior.cpp │ ├── Strategist.cpp │ ├── Skill.cpp │ ├── Team.cpp │ ├── BattleSystem.cpp │ └── main.cpp ├── data/ │ └── heroes.txt └── Makefile

头文件和源文件分离是C++项目的基本规范。头文件放类的声明,源文件放实现。这样做的好处是编译速度快(修改一个.cpp文件不需要重新编译所有文件),而且代码结构清晰。

4.2 武将类的完整实现

先来看Warrior(武将)类的完整实现。这个类继承自Character,增加了武力、统率等属性,以及技能列表。

// Warrior.h #pragma once #include "Character.h" #include "Skill.h" #include <vector> #include <memory> class Warrior : public Character { private: int strength; // 武力 int leadership; // 统率 int speed; // 速度 int rage; // 怒气值 int maxRage; std::vector<std::shared_ptr<ISkill>> skills; public: Warrior(const std::string& n, int hp, int lv, int str, int lead, int spd); int attack() const override; std::string getType() const override { return "武将"; } void addSkill(std::shared_ptr<ISkill> skill); bool canUseSkill(int index) const; void useSkill(int index, Character* target); void gainRage(int amount); void resetRage() { rage = 0; } int getStrength() const { return strength; } int getLeadership() const { return leadership; } int getSpeed() const { return speed; } int getRage() const { return rage; } const std::vector<std::shared_ptr<ISkill>>& getSkills() const { return skills; } };
// Warrior.cpp #include "Warrior.h" #include <algorithm> Warrior::Warrior(const std::string& n, int hp, int lv, int str, int lead, int spd) : Character(n, hp, lv), strength(str), leadership(lead), speed(spd), rage(0), maxRage(100) {} int Warrior::attack() const { // 基础攻击力 = 武力 * 0.6 + 等级 * 5 return static_cast<int>(strength * 0.6 + level * 5); } void Warrior::addSkill(std::shared_ptr<ISkill> skill) { skills.push_back(skill); } bool Warrior::canUseSkill(int index) const { if (index < 0 || index >= static_cast<int>(skills.size())) return false; return rage >= skills[index]->getCost(); } void Warrior::useSkill(int index, Character* target) { if (!canUseSkill(index)) return; rage -= skills[index]->getCost(); skills[index]->execute(this, target); } void Warrior::gainRage(int amount) { rage = std::min(rage + amount, maxRage); }

这里有几个设计细节值得展开说。怒气值系统是我额外加的,目的是让技能释放有节奏感——不能每回合都放技能,需要积累怒气。每次普通攻击获得20点怒气,受到伤害获得10点怒气,技能消耗30到50点不等。这样设计的结果是:大概每2到3回合可以放一次技能,战斗节奏比较舒服。

attack()方法返回的是攻击力而不是直接造成伤害,这是因为伤害计算需要结合攻防双方的属性,放在战斗系统里统一处理更合理。如果让attack()直接修改目标血量,那这个方法就同时承担了“计算”和“执行”两个职责,违反了单一职责原则。

4.3 战斗流程的完整代码实现

战斗系统是整个项目中最复杂的部分。我把战斗流程拆成了几个关键函数:

// BattleSystem.h #pragma once #include "Team.h" #include <functional> class BattleSystem { private: Team& playerTeam; Team& enemyTeam; int round; std::vector<std::string> battleLog; public: BattleSystem(Team& p, Team& e) : playerTeam(p), enemyTeam(e), round(1) {} void run(); bool executePlayerTurn(); void executeEnemyTurn(); bool checkBattleEnd(); void printBattleStatus() const; void addLog(const std::string& msg); void printLog() const; };
// BattleSystem.cpp #include "BattleSystem.h" #include <iostream> #include <random> #include <algorithm> static std::mt19937 gen(std::random_device{}()); int calculateDamage(const Character* attacker, const Character* defender) { int baseDmg = attacker->attack() - static_cast<int>( dynamic_cast<const Warrior*>(defender)->getLeadership() * 0.4); baseDmg = std::max(baseDmg, 10); // 最低伤害保底 std::uniform_real_distribution<double> dist(0.85, 1.15); return static_cast<int>(baseDmg * dist(gen)); } void BattleSystem::run() { std::cout << "\n========== 战斗开始 ==========\n"; while (!checkBattleEnd()) { std::cout << "\n--- 第 " << round << " 回合 ---\n"; printBattleStatus(); if (!executePlayerTurn()) break; if (checkBattleEnd()) break; executeEnemyTurn(); round++; } printLog(); } bool BattleSystem::executePlayerTurn() { auto& aliveHeroes = playerTeam.getAliveHeroes(); if (aliveHeroes.empty()) return false; // 按速度排序,速度高的先行动 std::sort(aliveHeroes.begin(), aliveHeroes.end(), [](Character* a, Character* b) { return dynamic_cast<Warrior*>(a)->getSpeed() > dynamic_cast<Warrior*>(b)->getSpeed(); }); for (auto* hero : aliveHeroes) { if (!hero->isAlive()) continue; auto& enemies = enemyTeam.getAliveHeroes(); if (enemies.empty()) break; // 简单AI:选择血量最低的敌人攻击 auto* target = *std::min_element(enemies.begin(), enemies.end(), [](Character* a, Character* b) { return a->getHP() < b->getHP(); }); int dmg = calculateDamage(hero, target); target->takeDamage(dmg); std::string msg = hero->getName() + " 攻击 " + target->getName() + ",造成 " + std::to_string(dmg) + " 点伤害"; if (!target->isAlive()) msg += "(" + target->getName() + " 阵亡)"; addLog(msg); std::cout << msg << "\n"; // 攻击后获得怒气 dynamic_cast<Warrior*>(hero)->gainRage(20); } return true; }

这段代码里有几个值得注意的设计决策。目标选择策略我用了“攻击血量最低的敌人”,这是一种简单但有效的AI策略——优先击杀残血目标,让敌方减员。如果你想让游戏更有挑战性,可以改成“攻击攻击力最高的敌人”或者“随机选择目标”。

速度排序决定了行动顺序。这里我用了std::sort配合lambda表达式,这是C++11之后的标准写法。需要注意的是,getAliveHeroes()返回的是一个副本而不是引用,否则排序会打乱队伍中的原始顺序。

伤害保底设置为10点,这是为了防止出现“攻击力低于防御力导致伤害为0或负数”的情况。如果没有这个保底,高防御低攻击的对局可能永远打不完。

4.4 主程序与游戏流程控制

main.cpp负责把各个模块串联起来,控制整个游戏的流程:

// main.cpp #include "Warrior.h" #include "Strategist.h" #include "Team.h" #include "BattleSystem.h" #include "Skill.h" #include <iostream> #include <fstream> #include <sstream> std::vector<std::shared_ptr<Character>> loadHeroes(const std::string& filename) { std::vector<std::shared_ptr<Character>> heroes; std::ifstream fin(filename); if (!fin.is_open()) { std::cerr << "无法打开 " << filename << ",使用默认数据。\n"; // 创建默认武将 heroes.push_back(std::make_shared<Warrior>("关羽", 1200, 5, 95, 80, 70)); heroes.push_back(std::make_shared<Warrior>("张飞", 1300, 5, 98, 75, 65)); heroes.push_back(std::make_shared<Strategist>("诸葛亮", 900, 5, 40, 100, 85)); heroes.push_back(std::make_shared<Warrior>("赵云", 1100, 5, 92, 78, 88)); heroes.push_back(std::make_shared<Strategist>("周瑜", 950, 5, 55, 95, 80)); return heroes; } std::string line; while (std::getline(fin, line)) { if (line.empty() || line[0] == '#') continue; std::istringstream iss(line); std::string type, name; int hp, lv, attr1, attr2, attr3; std::getline(iss, type, '|'); std::getline(iss, name, '|'); iss >> hp >> lv >> attr1 >> attr2 >> attr3; if (type == "Warrior") { heroes.push_back(std::make_shared<Warrior>(name, hp, lv, attr1, attr2, attr3)); } else if (type == "Strategist") { heroes.push_back(std::make_shared<Strategist>(name, hp, lv, attr1, attr2, attr3)); } } return heroes; } int main() { std::cout << "========================================\n"; std::cout << " 三国群英传 - C++课程设计\n"; std::cout << "========================================\n\n"; auto allHeroes = loadHeroes("data/heroes.txt"); // 展示可选武将 std::cout << "可选武将列表:\n"; for (size_t i = 0; i < allHeroes.size(); ++i) { std::cout << i + 1 << ". " << allHeroes[i]->getName() << " [" << allHeroes[i]->getType() << "]" << " HP:" << allHeroes[i]->getMaxHP() << " 攻击:" << allHeroes[i]->attack() << "\n"; } // 玩家选择出战武将 Team playerTeam("玩家队伍"); std::cout << "\n请选择3名武将出战(输入编号,空格分隔):"; for (int i = 0; i < 3; ++i) { int choice; std::cin >> choice; if (choice >= 1 && choice <= static_cast<int>(allHeroes.size())) { playerTeam.addHero(allHeroes[choice - 1]); } } // 敌方队伍(随机选择3名) Team enemyTeam("敌方队伍"); std::shuffle(allHeroes.begin(), allHeroes.end(), gen); for (int i = 0; i < 3 && i < static_cast<int>(allHeroes.size()); ++i) { enemyTeam.addHero(allHeroes[i]); } // 开始战斗 BattleSystem battle(playerTeam, enemyTeam); battle.run(); return 0; }

主程序的逻辑很直白:加载武将数据、让玩家选择出战阵容、随机生成敌方阵容、进入战斗。这里我用了std::shared_ptr来管理武将对象,因为同一个武将可能同时被玩家队伍和敌方队伍引用(虽然在这个简化版本里不会发生,但用智能指针是更好的习惯)。

实操心得:main函数里尽量不要写太多业务逻辑,把功能封装到各个类的方法里。我见过有同学的main函数写了300多行,所有逻辑都堆在一起,最后调试时根本找不到问题出在哪里。记住一个原则:main函数只负责“串流程”,不负责“做事情”。

5. 常见问题与排查技巧实录

5.1 编译与链接错误速查

C++项目最常见的翻车现场就是编译不过。下面这张表整理了我在做这个项目时遇到过的典型错误和解决方法:

错误信息原因解决方法
undefined reference to 'Warrior::attack()'头文件声明了但.cpp没实现,或者Makefile没把.cpp加入编译检查.cpp文件是否实现,检查构建配置
multiple definition of 'calculateDamage'函数定义在头文件里被多个.cpp包含把函数实现移到.cpp,头文件只留声明;或加inline
cannot dynamic_cast 'defender' to 'class Warrior*'基类指针指向的不是Warrior对象检查对象实际类型,或改用虚函数避免dynamic_cast
no matching function for call to 'std::sort'排序的比较函数签名不对确保lambda参数类型和容器元素类型一致
segmentation fault运行时崩溃空指针解引用或数组越界用gdb或IDE调试器定位崩溃行,检查指针是否为空

其中dynamic_cast相关的问题特别值得说。我在战斗系统里用dynamic_cast<Warrior*>(defender)来获取防御方的统率值,但如果防御方是Strategist类型(没有统率属性),这个转换会返回nullptr,后续访问就会崩溃。

解决方案有两种:一是在基类Character里加一个虚函数getDefense(),让每个子类自己实现;二是用dynamic_cast之后检查返回值是否为nullptr。我最终选择了第一种方案,因为它更符合面向对象的设计原则——基类定义接口,子类提供实现。

// 在Character基类中添加 virtual int getDefense() const = 0; // Warrior中实现 int getDefense() const override { return static_cast<int>(leadership * 0.4); } // Strategist中实现 int getDefense() const override { return static_cast<int>(intelligence * 0.3); }

这样战斗系统就不需要知道具体是什么类型的角色,直接调用getDefense()就行。这就是多态的价值所在。

5.2 内存管理与智能指针的坑

课程设计中另一个高频扣分点是内存泄漏。如果你用new创建了对象但忘记delete,或者用new[]创建数组但用delete释放,都会导致内存问题。

我的建议是:尽量用智能指针,避免手动管理内存。具体来说:

  • 用std::shared_ptr管理需要共享所有权的对象(比如武将对象可能被多个队伍引用)
  • 用std::unique_ptr管理独占所有权的对象(比如一个队伍拥有的技能列表)
  • 用std::vector替代动态数组,它自动管理内存

但智能指针也有坑。最常见的是循环引用:A持有B的shared_ptr,B也持有A的shared_ptr,导致两个对象永远不会被释放。解决方法是把其中一方改成weak_ptr。不过在这个项目里,队伍和武将之间不太可能出现循环引用,所以暂时不需要担心。

还有一个坑是在构造函数里调用虚函数。C++在构造基类部分时,虚函数表还没有完全建立,此时调用虚函数不会触发多态,而是调用基类的版本。如果你在Character的构造函数里调用了getType(),它不会返回子类的类型字符串。解决方法很简单:不要在构造函数和析构函数里调用虚函数。

5.3 游戏平衡性调试技巧

代码写完了,能跑起来了,但游戏好不好玩是另一回事。我第一版做出来的时候,战斗基本上是两个回合就结束——攻击力太高,血量太低。调整平衡性花了整整一个下午。

我的调试方法是:写一个自动战斗函数,让两个随机队伍对战100次,统计平均回合数和胜率。如果平均回合数低于3,说明伤害太高,需要降低攻击系数或提高血量;如果高于10,说明伤害太低,需要反向调整。

void balanceTest(int iterations) { int totalRounds = 0; int playerWins = 0; for (int i = 0; i < iterations; ++i) { auto heroes = generateRandomHeroes(); Team p("P"), e("E"); // ... 分配武将 BattleSystem battle(p, e); int rounds = battle.runSilent(); // 不输出日志的版本 totalRounds += rounds; if (battle.playerWon()) playerWins++; } std::cout << "平均回合数: " << totalRounds / iterations << "\n"; std::cout << "玩家胜率: " << (playerWins * 100 / iterations) << "%\n"; }

理想的结果是:平均回合数在4到6之间,玩家胜率在50%到60%之间(稍微偏向玩家,让游戏体验更好)。如果胜率低于40%,说明敌方太强,需要调整敌方属性或者降低敌方AI的智能程度。

注意:平衡性调整没有终点,差不多就行了。课程设计的重点是代码质量和设计思路,不是做成一个商业级游戏。我见过有同学花了三天时间调数值,结果代码写得一塌糊涂,得不偿失。

5.4 答辩演示的注意事项

最后说几个答辩时的实用技巧。老师看你的项目,第一眼看的是能不能跑起来,第二眼看的是代码结构清不清晰,第三眼才是功能完不完善。

所以演示之前一定要做这几件事:

  1. 准备一份干净的测试数据,确保演示时不会因为数据问题崩溃
  2. 把关键代码段打印出来,答辩时直接展示你的类设计和核心算法
  3. 准备一个“如果老师问为什么这样设计”的回答,比如“为什么用继承不用组合”“为什么用智能指针不用裸指针”
  4. 提前跑一遍完整流程,包括文件读写、战斗、胜负判定,确保没有遗漏

我当初答辩时被问到的第一个问题是“你这个项目里哪里体现了多态”,我直接打开BattleSystem.cpp,指着calculateDamage函数说“这里通过基类指针调用attack()和getDefense(),实际执行的是子类的实现”,老师点了点头就没再追问。所以提前准备好这些关键问题的答案,能让你在答辩时从容很多。

这个项目后续还可以继续扩展,比如加入装备系统(武器、防具、坐骑)、加入阵型系统(前排后排、克制关系)、加入剧情模式(按关卡推进)。但那是另一个话题了,先把基础版本做扎实,比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询