☰
C++宠物小精灵游戏课设:面向对象编程与C/S对战全解析
2026/9/28 5:01:29 网站建设 项目流程

简介:这是一套基于C++实现的宠物小精灵对战游戏项目,源于面向对象课程设计,适合高校学生完成大作业、课程设计或工程实训。内容从基础实验到综合项目层层递进,覆盖矩阵运算、圆形相交判断、形状类层次设计、猜价格I/O流实验,以及虚函数与运算符重载等知识点;综合部分则实现精灵对战,涉及C/S模式通信、用户注册登录登出、精灵升级与技能释放等完整功能。

压缩包共63个文件,以cpp源文件、vcxproj工程配置、h头文件、sln解决方案为主,另含docx课程设计报告、pdf实验说明、txt与md说明文档,整体仅1.23MB,结构清晰便于按模块查阅。目前已有115人学习下载,适合作为面向对象编程的完整参考。通过本项目可同时获得可直接编译运行的源码与配套设计文档,既能对照实验要求验证编码思路,也能根据报告说明理解模块划分与通信流程,是入门C++面向对象开发的实用素材。

1. 基于 C++ 的宠物小精灵游戏:一份能照跑的面向对象课程设计全套

面向对象课设最纠结的不是代码,是选题——做个计算器显得单薄,做个完整 C/S 对战项目又怕时间不够。这份基于 C++ 实现的宠物小精灵游戏课设,刚好卡在两者中间:五个基础实验把类、继承、重载、I/O 流逐个过掉,综合实验用 client/server 模式把用户注册登录、精灵对战、升级释放技能串成一个完整的 C++ 小游戏。我拆过不少同类资源,这套的完整度属于“拿到手可以直接照着跑”的级别。

它适合三类人:正在准备 C++ 课程设计、大作业但还没定方向的学生;面向对象语法看过但不知道类之间怎么分职责的初学者;以及想找一份类宝可梦游戏源码做参考、顺便学 C/S 通信的人。资源里除了源码,还有实验指导 PDF、课程设计报告 docx,后面几章我会把每个实验对应的文件、编译顺序和最容易翻车的位置全部过一遍。

2. 五个基础实验打磨语法:矩阵类、圆形相交、形状派生和运算符重载该怎么写

很多同学拿到这份资源会先看综合实验里的精灵对战,我的建议是反过来:先把五个基础实验的源码按顺序过一遍。综合实验的代码量大概在千行以上,里面大量用到类的组合、继承和运算符重载,如果基础实验里的写法没吃透,综合部分读起来会很吃力。资源目录里的基础1、基础2-1、基础2-2、基础3、基础4、基础5-1-1、基础5-1-2、基础5-2,就是这一章的落地素材,我下面按知识点分类讲。

2.1 矩阵类相加减:为什么用 std::vector 而不是二维数组

基础实验的第一题是矩阵相加减,第二题里又出现了一次“使用类编写矩阵类”,说明这个题在课程里被反复练。最省事的写法是定义一个 Matrix 类,内部用std::vector<std::vector<int>>存数据,而不是用int data[10][10]这种固定大小的二维数组。原因很简单:vector 可以动态 resize,构造函数里传行列数就行,不用预先猜一个最大尺寸;内存由 vector 自己管理,也不需要手写析构函数。

#include <iostream> #include <vector> class Matrix { private: std::vector<std::vector<int>> data; int rows; int cols; public: Matrix(int r, int c) : rows(r), cols(c) { data.resize(r, std::vector<int>(c, 0)); } bool add(const Matrix &other) { if (rows != other.rows || cols != other.cols) { std::cerr << "矩阵维度不一致,不能相加" << std::endl; return false; } for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { data[i][j] += other.data[i][j]; } } return true; } void print() const { for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { std::cout << data[i][j] << " "; } std::cout << std::endl; } } };

resize(r, std::vector<int>(c, 0))的意思是先把外层扩张成 r 行,每一行初始化为 c 个 0,这样构造函数执行完,矩阵里每个元素都有确定值。add返回 bool 而不是 void,是给调用方一个信号:如果矩阵维度对不上,操作失败,程序不用直接崩溃。很多课程设计里这道题被扣分,就是因为维度不一致时直接越界访问,程序当场闪退。我一般会在写这类类的时候坚持“操作失败要能返回错误状态”,后面综合实验里的技能释放、精灵交易也沿用这个思路,会省很多调试时间。

2.2 圆形相交与形状派生类:封装、继承和抽象类的边界

圆形相交这道题考察的是类的封装:把圆心和半径放在 Circle 类内部,对外只暴露构造函数和判断函数。判断两个圆是否相交,数学上就是算圆心距d,然后和半径和、半径差比较。这里有个很多人第一次写都会踩的坑:直接用==判断相切。浮点数运算之后,两个本应相切的圆,圆心距可能算出4.0000000001,直接比较就误判成不相交。

#include <cmath> struct Point { double x; double y; }; class Circle { private: Point center; double radius; public: Circle(Point c, double r) : center(c), radius(r) {} bool isIntersect(const Circle &other) const { double dx = center.x - other.center.x; double dy = center.y - other.center.y; double dist = std::sqrt(dx * dx + dy * dy); const double eps = 1e-9; double sum = radius + other.radius; double diff = std::fabs(radius - other.radius); // 严格相交:d < R + r 且 d > |R - r| return dist < sum - eps && dist > diff + eps; } };

eps是误差容忍值,1e-9对课程设计里的距离量级够用。把相切情况排除掉还是包含进去,取决于题目要求,但不管哪种处理,都要显式写出来,而不是留给浮点数误差决定,这是这个实验的考核点之一。资源里基础2-1对应圆形类,基础2-2对应矩阵类,两个工程可以对照着看同一道实验题两种不同的封装粒度。

形状及其派生类、虚函数那两道题,考的是继承和多态。我的建议是直接按抽象类的标准写法来:基类只定义纯虚函数,派生类必须全部实现,这样编译器能在编译期帮你检查有没有漏掉某个方法。

class Shape { public: virtual double area() const = 0; virtual void show() const = 0; virtual ~Shape() {} }; class CircleShape : public Shape { private: double radius; public: explicit CircleShape(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } void show() const override { std::cout << "Circle, radius = " << radius << std::endl; } }; class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; } void show() const override { std::cout << "Rectangle, area = " << area() << std::endl; } };

基类构造函数里不写=0的虚函数实现,析构函数一定要声明为 virtual,否则通过 Shape 指针 delete 派生类对象时,只会调用基类析构,派生类里如果有动态申请的资源就会泄漏。这个细节在很多 C++ 八股题里都会出现,放在课设里就是答辩老师最爱问的一个点。

2.3 运算符重载与随机数:Point 的 ++、-- 和猜价格游戏

Point类重载++和--是在练运算符重载的两种形式:前置和后置。前置返回引用,后置返回旧值的一个副本,靠一个不具名的int参数区分。这里很容易写反,后置版本如果也返回引用,会对局部对象取引用,造成悬垂引用。

class Point { private: int x; int y; public: Point(int px, int py) : x(px), y(py) {} // 前置 ++ Point &operator++() { ++x; ++y; return *this; } // 后置 ++ Point operator++(int) { Point temp = *this; ++(*this); return temp; } // 前置 -- 与后置 -- 同理 Point &operator--() { --x; --y; return *this; } };

这段代码的关键在后置版本:先拷贝当前状态到 temp,再调用前置版本完成真正的自增,最后返回 temp。因为返回的是临时对象,必须按值返回,不能返回引用。这也是++p和p++在语义上的核心差异,课设里只要把这一点讲清楚,答辩时基本不会被问倒。

猜价格游戏这道题把 I/O 流和 C++ 随机数结合起来:程序先rand()生成一个目标价格,玩家循环猜,猜对退出。这里真正的坑不在随机数,而在输入流的健壮性。如果用户输入的不是数字,std::cin >> guess会失败,同时流进入错误状态,之后的所有读取都会直接返回。

#include <cstdlib> #include <ctime> #include <iostream> #include <limits> int main() { std::srand(static_cast<unsigned>(std::time(nullptr))); int target = std::rand() % 100 + 1; int guess = 0; while (true) { std::cout << "猜一个 1~100 的价格: "; if (!(std::cin >> guess)) { // 清除错误状态并丢弃当前行的残余字符 std::cin.clear(); std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); std::cout << "输入无效,请重新输入" << std::endl; continue; } if (guess > target) { std::cout << "猜高了" << std::endl; } else if (guess < target) { std::cout << "猜低了" << std::endl; } else { std::cout << "猜对了" << std::endl; break; } } return 0; }

std::cin.clear()负责把错误状态位恢复,std::cin.ignore(...)负责把缓冲区里残留的非数字字符清掉。少了任何一行,输入一次非法字符之后整个循环就会陷入死循环。这道题在资源里的对应目录是基础4,代码量不大,但把清除输入流这招练熟,后面综合实验做用户注册输入校验时能直接复用。

2.4 基础实验代码怎么用:从“会写”到“敢写”

基础实验的源码基本是一个目录对应一个控制台工程,拿到资源后不要直接整个塞进 Visual Studio 的解决方案里,而是每个目录单独建一个空项目。原因很简单:五个实验的 main 函数各有各的入口,如果有两个实验文件放进了同一个可执行项目,编译器会报“main 函数重定义”的错误,这是最常出现的低级问题。

我一般会按这个顺序过一遍:先跑基础1的矩阵加减法,确认 vector 初始化没问题;再跑基础2-1的圆形相交,把 eps 的数值改大改小看判断结果怎么变化;接着跑基础3的形状派生类,试着自己加一个 Triangle 派生类,看编译器会不会强制要求实现area();最后跑基础4的猜价格游戏,故意输入abc验证输入流清理逻辑。

这一步的意义不在于“代码能跑就行”,而在于给综合实验做心理建设。直面对象课设的核心考核点是类的职责划分、继承关系的合理性、以及边界情况处理,这五个基础实验把以上三点都覆盖到了。等你把综合实验里的宠物对战代码打开,再回头看你跑过的这些 demo,会发现精灵对战里的人物、精灵、技能,本质上就是 Matrix、Circle、Shape 这些类的组合升级版,区别只在于数据成员更多、方法更复杂、还多了网络通信这个新维度。

3. 综合实验 C/S 框架拆解:注册登录、精灵对战与升级的模块化实现

综合实验的文件放在综合/client和综合/server两个目录下,说明它的架构是经典的 C/S 模式。第一个要建立的认知是:这不能让在一台机器上跑一个 main 函数解决问题,而是必须有两套独立的程序。server 端负责账号管理、精灵数据、战斗裁决,client 端负责用户输入、界面显示。两边通过 socket 通信,消息自己定义格式。下面按业务模块把这份类宝可梦游戏源码里最核心的四个部分梳理一遍。

3.1 通信模型先想清楚:消息格式是双方共同遵守的契约

课设阶段用 Winsock 的阻塞模式就够了,思路简单而且好调试。服务端创建监听 socket,accept()等待客户端连接,连上之后recv()读取客户端发来的消息,处理完send()回包。双向的通信消息必须有一套统一的格式,否则双方各说各话,解析必然错位。

#pragma pack(push, 1) struct MsgHeader { short type; // 消息类型:注册、登录、对战、技能等 short len; // 正文长度 }; #pragma pack(pop) int sendMsg(SOCKET s, short type, const char *body) { short len = static_cast<short>(strlen(body)); MsgHeader header; header.type = type; header.len = len; int ret = send(s, reinterpret_cast<const char *>(&header), sizeof(header), 0); if (ret == SOCKET_ERROR) { return -1; } return send(s, body, len, 0); }

头部用一个 4 字节的结构体:2 字节消息类型加 2 字节正文长度,#pragma pack(push, 1)是为了让结构体按 1 字节对齐,避免编译器在中间插入填充字节。这种“定长头部 + 变长正文”的格式在网络编程里属于基础操作,几乎所有课程设计都能套用。sendMsg返回时,可以简单用返回值是否等于SOCKET_ERROR判断发送是否失败。

很多第一次写 socket 的初学者会忽略字节序问题。如果消息只在 Windows 本机两个进程之间通信,不跨平台、不跨字节序,htons和ntohs可以省略;但一旦换到 Linux 或者 Mac 上编译,同样的代码就会因为大小端不同而解出错误长度。我一般在课设里也会顺手写上htons/ntohs转换,成本很低,却能让代码看起来更专业。资源里还有一份“实验(综合-宠物小精灵)-面向对象程序设计与实践-.pdf”,里面是题目的实验说明,消息类型的编号建议从中提取,而不是自己另起一套。

3.2 账号系统三步走:注册、登录、登出的状态管理

用户注册、登录、登出在资源里是明确列出的功能点。实现上不建议一开始就上数据库,用文件持久化就够。server 端维护一个用户表,注册时检查用户名是否已存在,登录时校验密码,登出时把用户状态改为离线。一个简单的做法是把用户信息顺序写入文本文件,每行一个用户,字段用|分隔。

struct UserInfo { char name[32]; char pass[32]; bool online; }; bool doRegister(const char *input) { UserInfo u; // 从 "REG|name|pass" 中解析出 u.name 和 u.pass if (strlen(u.name) == 0 || strlen(u.pass) == 0) { return false; } if (findUser(u.name) != nullptr) { return false; // 用户名已存在 } saveUser(u); return true; }

关键逻辑在findUser和saveUser这一读一写之间。如果课程设计里强调并发,需要给用户文件加线程锁,避免两个客户端同时注册时写入交错;如果只要求跑通,单线程顺序处理就能过关。很多实现把 online 状态放在内存里,用户表用std::vector<UserInfo>装载,启动时从文件加载,退出时写回,这种思路简单直接,也方便打印日志调试。

登录成功之后,server 端要记住“哪个 socket 对应哪个用户”。映射关系可以用std::map<SOCKET, std::string>维护,也可以直接在 UserInfo 里存一个 socket 字段。登出时除了关闭 socket,还要把 online 置为 false,否则下次登录会提示“该账号已在线”,这是一个很容易漏掉的细节。

3.3 回合制对战:技能释放和升级数值的设计方法

对战被拆成回合制是最明智的做法。每个精灵有攻击力、防御力、血量、魔法值、技能列表,技能有耗蓝和威力值。对战时双方轮流选择技能,造成伤害,血量归零的那一方判负,胜方精灵获得经验值。

struct Pet { char name[32]; int level; int hp; int maxHp; int mp; int atk; int def; int exp; }; struct Skill { const char *name; int mpCost; int power; }; int calcDamage(const Skill &skill, int atk, int def) { // 伤害 = 技能威力 * 攻击力 / 防御力 * 随机浮动 int base = skill.power * atk / (def > 0 ? def : 1); double rate = 0.9 + (rand() % 21) / 100.0; // 0.9 ~ 1.1 return static_cast<int>(base * rate); }

伤害公式里的def > 0 ? def : 1是为了防止防御力被配成 0 时出现除零崩溃。随机浮动幅度0.9 ~ 1.1用了rand() % 21配合除法,让每回合伤害不完全一样,观感上比较像游戏。课程设计阶段不需要严谨的数值平衡,但要注意基础数值不能太离谱——如果初始精灵攻击力 100、防御力 1,一回合就能秒杀对方,整个对战流程就失去了演示意义。

升级逻辑一般是经验值累积到某个阈值就升一级,并同步提升属性上限。简单做法:

void gainExp(Pet &pet, int exp) { pet.exp += exp; int newLevel = pet.exp / 100 + 1; if (newLevel > pet.level) { pet.level = newLevel; pet.maxHp = 60 + pet.level * 12; pet.hp = pet.maxHp; // 升级后回满血 pet.atk += 3; pet.def += 2; } }

阈值是每 100 经验升一级,等级越高属性线性增长。这里的核心设计思想是“把数值变化集中到一个函数里”,这样技能释放时调用calcDamage,回合结束时调用gainExp,整个战斗流程就非常清晰。对战过程本身是两方交互逻辑,代码里注意每个send之后要等对端的recv,顺序不能乱:A 客户端选择技能,server 收到后计算伤害,然后再把结果分发给 A 和 B。

3.4 精灵的获取与送出:文件存档和跨客户端转移

“获得精灵、送出精灵”这两个功能看起来是数据交换,本质上是 server 端对全局精灵数据的增删改。比如用户注册时系统随机送一只初始精灵,这是获取;用户把某只精灵送给另一个在线用户,这是送出。送出精灵时,server 端要从赠送者的精灵列表里删除该精灵,再添加到接收者的精灵列表里。

bool givePet(SOCKET fromUser, SOCKET toUser, const Pet &pet) { UserInfo *src = findUserBySocket(fromUser); UserInfo *dst = findUserBySocket(toUser); if (src == nullptr || dst == nullptr) { return false; } removePetFromList(src, pet); addPetToList(dst, pet); saveAllUsers(); // 写回文件 return true; }

单线程处理这条链路问题不大,但写回文件这一步必须放在最后统一做。有的实现会在删除精灵后立即写一次文件,添加精灵后再写一次,结果文件被打开了两次,中途程序崩溃时数据可能只存了一半。建议的做法是:在内存里完成所有用户数据的修改,最后一次性写回,既能保证一致性,也方便打印日志。

数据存档格式可以参照用户文件的方式,一行一个精灵,字段用,分隔:皮卡丘,5,120,120,40,40,20,300。saveAllUsers先写临时文件再改名替换,这样可以避免写文件中途崩溃导致原数据丢失——这个习惯在后面的排错章节里还会再提一次。

4. 让源码跑起来:编译顺序、目录对照与课程设计报告的产出路径

这一章解决一个实际问题:你下载了这个资源,从解压到能在自己电脑上跑起来,中间要经历什么。很多人卡住的不是代码,而是环境配置和文件组织。资源里带了一个 README.md,但课设资源常见的通病是 README 写得太简略,目录里还有.DS_Store这种 macOS 系统生成的隐藏文件,说明作者大概率是在 macOS 上整理又拷贝到 Windows 环境的。我把这套资源的结构和编译路径按经验重新梳理一遍。

4.1 目录结构对照:哪个文件夹对应哪道题

先建一张对照表,跑之前把每个目录的作用搞清楚,能省很多找错文件的时间。

目录 / 文件资源内容课程设计中的用途
课程设计报告.docx完整课设报告范本报告撰写参考
实验(综合-宠物小精灵)...pdf综合实验题目说明理解综合实验需求
基础1矩阵相加减基础实验 1
基础2-1圆形及相交判断基础实验 2 第一问
基础2-2矩阵类基础实验 2 第二问
基础3形状及派生类基础实验 3
基础4猜价格游戏基础实验 4
基础5-1-1、基础5-1-2虚函数、抽象类基础实验 5 第一问
基础5-2Point 重载 ++ / --基础实验 5 第二问
综合/client客户端工程综合实验前端
综合/server服务端工程综合实验后端

这个资源的地基非常扎实,基础实验部分几乎一道题对应一个独立目录,不存在多个实验挤在一个工程里的情况。特别注意基础5-1-1和基础5-1-2是两个文件,对应虚函数和抽象类两个递进步骤,写报告时可以描述成“先把虚函数定义好,运行确认多态效果后,再把基类改成抽象类”。

4.2 编译环境与编译顺序:先让基础实验全部通过

这套代码面向 Windows 的话,最顺手的工具是 Visual Studio 2022 或者 2019。新建控制台应用项目时,把对应目录下的.cpp文件加入源文件列表,.h文件会自动被识别。基础实验逐个编译通过之后,再碰综合实验。综合实验涉及 Winsock,需要额外链接ws2_32.lib,在 Visual Studio 里打开“项目属性 → 链接器 → 输入 → 附加依赖项”添加即可。

如果习惯用命令行,也可以用 g++ 直接编译:

g++ -std=c++11 server.cpp pet.cpp -o server -lws2_32 g++ -std=c++11 client.cpp pet.cpp -o client -lws2_32

-std=c++11保证代码里如果用了nullptr、override这些新特性不会被老编译器拒绝;-lws2_32是 Winsock 库的链接参数,Windows 下编译 socket 程序少了它就会出现一堆unresolved external symbol错误。先编译 server 端,再编译 client 端,两者可以复用同一个精灵数据类 pet.cpp,避免把同样的结构体写在两个工程里造成定义不一致。

4.3 client/server 联调细节:启动顺序与连接失败排查

联调时的铁律是:先启动 server,再启动 client。如果 client 先启动,它connect()的时候 server 端的listen()还没执行,连接会被拒绝,客户端直接退出。server 启动后会打印“等待连接”之类的提示,看到这个提示再开 client 窗口。

服务端如果在调试过程中频繁重启,经常会遇到bind失败,错误码是WSAEADDRINUSE。这是因为上一个 server 进程退出了,但 TCP 连接还处在TIME_WAIT状态,端口没有立即释放。解决方式是在 server 端创建 socket 后立刻设置地址重用:

int opt = 1; setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char *>(&opt), sizeof(opt));

如果 server 和 client 在同一台机器上,地址可以填127.0.0.1,端口随意指定一个大于 1024 的值,比如12345。在 Windows 上,如果 client 显示连接失败,先ipconfig看本机 IP,再确认 server 端防火墙有没有拦截端口。严格顺序排查思路是:先看 server 是否真的在监听,再看地址和端口是否匹配,最后看防火墙,而不是靠玄学重启。

4.4 课程设计报告怎么写:docx 怎么和源码互相引用

资源里的课程设计报告.docx是完整范本,建议不要直接改名上交,而是按它的章节骨架重写一遍自己的过程。一门面向对象课设,老师最想看到的是你能说清楚“每个类为什么这么设计”,而不是代码有多少行。报告的骨架一般分为需求分析、总体设计、详细设计、测试、总结五块。

需求分析里写清楚要实现的五个基础实验和综合实验的功能清单;总体设计放类图,可以从源码里抽取出 Pet、UserInfo、Skill、MessageHandler 几个类,用箭头标出组合和继承关系;详细设计挑两到三个重点函数展开,比如calcDamage、doRegister,把代码片段贴上并说明参数含义;测试部分用表格列出测试用例:注册重复用户名、登录错误密码、对战掉血是否符合预期、送出精灵后双方列表是否正确更新。报告的每个测试用例最好都能和源码里的具体函数对应上,答辩时老师问“这里怎么测的”,你就可以直接翻到对应代码现场演示。

5. 避坑记录:这份宠物小精灵课设最容易翻车的五个位置

课程设计的代码量到了一定规模后,真正的难点不是语法,而是一堆隐蔽的运行时问题。下面五条是这份宠物小精灵资源里最典型的五个坑,每一条我都按“现象 → 原因 → 解决”的顺序写清楚。

5.1 圆形相交判断结果飘忽不定

现象:两个半径和等于圆心距的圆,本应相切,程序有时判相交、有时判不相交,把半径改成整数就正确,改成小数就出错。

原因:浮点数运算有精度损失。圆心坐标如果是0.1、半径如果是2.3,算出来的sqrt(dx*dx + dy*dy)和真实几何距离存在约1e-10级误差,直接和radius + radius比较时,结果取决于舍入方向。

解决:在所有几何比较里引入eps容差,用dist < sum - eps && dist > diff + eps判断严格相交,相切情况单独用fabs(dist - sum) < eps判断。eps的量级至少要比浮点误差大一个数量级,1e-9对课程设计够用。

5.2 猜价格游戏输入乱套

现象:第一次输入正确数字能正常运行,第二次故意输入字母,程序开始疯狂打印“猜高了”或“猜低了”,不会停下来重新等待输入。

原因:std::cin >> guess读取数字失败后,流对象进入 fail 状态,后续所有读取操作直接跳过,循环变成死循环。

解决:每次读取后都检查std::cin状态,失败时先clear()恢复流状态,再ignore()清空缓冲区的非法字符。我见过不少实现只写clear()不写ignore(),结果下次读取依然失败,两个调用必须成对出现。

5.3 服务端重启时 bind 失败

现象:第一次运行 server 正常,Ctrl+C 强杀进程后再启动,提示“bind failed”,错误码10048。

原因:socket 处于TIME_WAIT状态,端口还没有完全释放。强杀进程时连接没有正常关闭,系统需要等约两分钟才能自动释放端口。

解决:创建监听 socket 后立即调用setsockopt设置SO_REUSEADDR。另外在 server 的退出逻辑里尽量走“退出循环 → closesocket → 退出”的干净流程,不要等到进程被强制终止。

5.4 客户端收到的精灵名字乱码

现象:客户端和服务端在同一台机器上运行,注册和登录都正常,一进入精灵列表,名字末尾出现乱码,有时名字和技能对不上。

原因:很可能是半包或粘包问题。send和recv是字节流接口,一次send的内容可能被拆成多次recv收到,也可能多个消息粘连在一次recv里一次性返回。如果只调用一次recv就按完整消息解析,自然会导致错位。

解决:收发都基于“固定头部 + 长度字段”做循环读取,直到收够sizeof(MsgHeader) + header.len字节为止。封装一个完整读取函数:

bool recvFull(SOCKET s, char *buf, int expect) { int total = 0; while (total < expect) { int n = recv(s, buf + total, expect - total, 0); if (n <= 0) { return false; // 连接断开或出错 } total += n; } return true; }

recvFull的核心是让每次接收都争取把剩余字节读满,total记录已收到的字节数,缓冲区指针跟着偏移,这样粘包和半包都能被消化,解析时永远从消息头部开始。

5.5 课程设计报告全是代码,缺少类图和测试

现象:报告写了 30 页,其中 25 页贴代码,类图和时序图一张没有,答辩时老师指着综合实验问“这两个类是什么关系”,答不上来。

原因:以为报告的评分标准是代码量。老师其实更关心你对代码的理解程度,贴图比贴代码更能体现你做了设计。

解决:用源码里的类结构画一张 UML 类图,画出 UserInfo、Pet、Skill、server 主逻辑类之间的依赖关系;再画一张对战流程的时序图,表示 clientA、server、clientB 三者之间的消息顺序。测试部分把 5.1 到 5.4 的复现和解决过程写进去,这个过程本身就是最有说服力的测试记录。

6. 进阶调试:用状态机重构回合战斗,并完成一次双端联调验证

综合实验的对战流程如果只用 if-else 嵌套实现,逻辑一旦复杂起来会非常难读。我一般会给课设里的回合制战斗加一层状态机:把对战的每个阶段定义成枚举值,主循环根据当前状态决定下一步动作。这样做的直接好处是流程清晰、测试时可以单步调试、答辩时还能作为多态之外的第二个设计亮点。

enum class BattleState { START, PLAYER_TURN, CHECK_PLAYER_DEAD, ENEMY_TURN, CHECK_ENEMY_DEAD, FINISH }; void runBattle(Pet &player, Pet &enemy) { BattleState state = BattleState::START; while (state != BattleState::FINISH) { switch (state) { case BattleState::START: // 发送对战开始消息,扣技能蓝量 state = BattleState::PLAYER_TURN; break; case BattleState::PLAYER_TURN: // recv 玩家选择的技能,计算伤害打到 enemy state = BattleState::CHECK_ENEMY_DEAD; break; case BattleState::CHECK_ENEMY_DEAD: if (enemy.hp <= 0) { state = BattleState::FINISH; } else { state = BattleState::ENEMY_TURN; } break; case BattleState::ENEMY_TURN: // 简单 AI:随机选技能打 player state = BattleState::CHECK_PLAYER_DEAD; break; case BattleState::CHECK_PLAYER_DEAD: if (player.hp <= 0) { state = BattleState::FINISH; } else { state = BattleState::PLAYER_TURN; } break; default: state = BattleState::FINISH; break; } } }

每个 case 分支里只做一件事,状态之间的跳转关系一目了然。测试时可以在每个状态切换处加日志,打印“当前状态 + 双方血量”,这样如果出 bug,直接看日志就知道是哪一步转错了。

联调验证的流程我建议固定成三步:第一,启动 server,确认监听成功;第二,启动两个 client,分别注册test_a和test_b两个账号,都登录上线;第三,test_a向test_b发起对战,打两个回合,确认掉血数值和升级经验值符合预期。验证送出精灵功能时,先让test_a送出精灵,再用test_b查看精灵列表,确认数量增加。这套流程跑通,说明通信链路、账号系统、战斗模块、存档模块全部正常工作。

我早期写过一版没有协议长度的 socket 小项目,联调时被半包问题折腾了一整晚,最后发现是recv少读了两个字节。从那以后,我每次写网络通信都强制自己先定好消息头部再动手写业务逻辑,哪怕只是课设级别的小项目也不会省略。这个习惯就是从这类 C++ 课程设计资源里磨出来的,希望这次拆解能帮你少走那段弯路,也希望你用这份资源做出来的课设,能在答辩时稳稳收尾。

本文还有配套的精品资源,点击获取

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

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

立即咨询