ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++实战项目:手把手教你用面向对象写一个王者荣耀极简版小游戏

C++实战项目:手把手教你用面向对象写一个王者荣耀极简版小游戏 如果你把C基础语法啃完了指针、类、继承这些概念都见过但总觉得缺一个能把它们串起来的实战项目那我强烈建议你亲手写一个“C小游戏王者荣耀极简版”。这项目听着有点蹭热度的意思但实际是练习C综合能力性价比极高的一条路游戏逻辑里天然需要面向对象设计、指针和引用传参、容器管理、排序算法比如按距离排攻击目标、循环与状态机基本语法书里的重点都能在几百行代码里碰到。我带过几个新手认真写完这个项目的人回头再看指针、虚函数、vector这些内容心态完全不一样。这个极简王者能做到什么程度别想着完整峡谷、野区、5v5团战那是团队几百号人干的事。我的目标很清楚单机人机或双人对战一个英雄一条中路双方各有防御塔和水晶能走位、能放技能、能清兵推塔最终推掉对方水晶就算赢。如果你学完C基础语法但缺项目经验这个项目就是把你从“会语法”推向“能写程序”的那座桥。适合谁来写刚学完C基础、想用项目练手的新人学校课程设计要做小游戏的同学以及那些学完指针和类一直没找到应用场景的人。下面我按自己实操的流程把极简王者的拆解思路、核心实现和踩坑记录完整写出来代码框架可以直接抄思路部分建议仔细看这才是真正值钱的地方。1. 项目整体设计与思路拆解1.1 为什么选C来做“极简王者”游戏行业里C是客户端底层的主力语言大型商业手游的引擎层、战斗逻辑层大量使用C。学C写游戏天然对胃口。虽然用Python也能做小游戏但C在内存控制、对象管理、性能调优上的底层层面优势很明显。做MOBA这种需要同时处理大量单位实时运算的项目时C能让你真正理解“性能”是怎么一回事——当屏幕上单位数量上百时每帧遍历一遍容器语言本身的开销立刻见分晓。用C语言也能写但表达面向对象时比较痛苦。比如定义英雄、小兵、防御塔C语言只能靠结构体加函数指针模拟后期加技能、加英雄会非常别扭。Python写起来快但运行慢也是一个隐患而且容易让人陷入“调库”而不是“理解底层”的舒适区。C正好卡在中间既能用类把角色抽象好又能手动管理内存从而感受到底层细节。1.2 极简版到底该保留哪些玩法原版王者荣耀系统庞杂全部复刻不现实。我的取舍思路是保留“MOBA核心循环”即可。这个循环是——对线、清兵、推塔、决胜。玩家操作一个英雄从小兵对拼中获得经济和经验配合防御塔一步步推掉对方水晶。原版功能极简版处理理由5v5对局1v1人机或双人同屏逻辑复杂度直接减半多地图/野区单一路线战斗聚焦核心战斗逻辑4~5个技能2~3个技能减少状态管理负担装备系统按击杀数自动成长先不碰商店逻辑小兵系统定时生成自动推进核心乐趣之一必须要有防御塔自动攻击最近敌人简化仇恨机制这个表基本定了整个项目的范围。我写代码之前会先画一张“功能边界表”防止写着写着开始加功能。做小游戏最大的坑不是不会写而是没完没了地加需求——加一个野区就想加BUFF加完BUFF又想加装备最后项目烂尾。极简项目就要有“抛开完整还原”的决心。1.3 开发环境与工具选型开发环境上我首选VSCode装上C/C插件和Code Runner编译器用MinGW-w64里的GCCWindows下。有些人喜欢Visual Studio完全可以就是项目配置会多几道手续。VSCode配MinGW是当前最轻量、最适合小游戏项目的组合。VSCode配置C/C环境核心就三步:安装VSCode扩展里搜“C/C”安装Microsoft官方插件再装一个“Code Runner”方便直接跑单文件。下载MinGW-w64解压后把bin目录路径加到系统环境变量PATH里。装完后打开cmd输入g --version能输出版本号就说明编译器OK。生成tasks.json和launch.json用于F5一键编译调试。VSCode菜单栏“运行→添加配置→C(GDB/LLDB)”会自动生成模板把program参数改成你的exe输出路径即可。配置这块我在第4章还会专门讲坑。现在记住一句话不要花超过半小时在环境折腾上能编译运行就行主战场永远是代码逻辑。2. 核心模块拆解与实现原理2.1 游戏主循环一切游戏的心脏不管做控制台小游戏还是图形界面游戏所有游戏的核心都是主循环。它做的事情就三件处理输入、更新逻辑、渲染画面。再加上一个帧率控制防止循环跑太快。while (running) { processInput(); // 读取键盘/鼠标输入 update(); // 更新所有对象状态 render(); // 输出画面 sleep(); // 控制帧率简单用 Sleep(50) }这个循环看似简单实际上整个项目的骨架。我见过不少新手把逻辑写成一坨顺序代码从程序启动一路执行到底结果退出都不知道怎么退出。用主循环有个明显好处每一帧都是独立的。你可以随时暂停、继续、调试逻辑因为游戏状态是逐帧推进的而不是一条流水线走到黑。MOBA类游戏对帧率尤其敏感。原版《王者荣耀》追求高帧率就是为了让操作“跟手”。极简版虽然是控制台/2D简化版但主循环的节奏必须设计好。我的做法是固定延时Sleep(50)也就是每秒大约20帧。这个速度下控制台画面不会闪得太厉害操作响应也够用。如果你后期接上图形库就需要用上一帧与下一帧的时间差deltaTime来驱动位移和技能冷却而不是写死每次移动10像素——否则不同性能的电脑上游戏速度会完全不一样。时间驱动这一点很多新手一开始意识不到。我见过有人在循环里直接用x 5结果在配置好的电脑上角色快得像瞬移在垃圾电脑上又慢吞吞。正确做法是记录每帧时间变化位移乘上时间差。控制台版因为帧率固定还好图形版一定要养成用deltaTime的习惯。2.2 角色类设计与指针/引用实战极简王者里至少有三种角色英雄玩家操作、小兵自动推进、防御塔固定位置攻击。这三种角色有共性都有位置、有血量、有攻击力、每帧要更新、要渲染。所以最合理的做法是定义一个基类GameObject然后用继承去扩展。class GameObject { public: int x, y; // 坐标 int hp, maxHp; // 血量 int atk; // 攻击力 int team; // 阵营 0或1 bool alive; public: virtual void update(vectorGameObject* objs) 0; virtual void render() 0; virtual ~GameObject() {} };Hero类继承GameObject重写update和render再加移动方法、技能方法。Minion类和Tower类也继承GameObject各自实现自动行为。这里最大的亮点是游戏里所有对象统一用vectorGameObject*存起来每帧遍历这个容器调用每个对象的update和render。这就是多态的实际应用——基类指针调用虚函数实际执行的是子类的方法。指针在这个项目里的应用非常典型GameObject*指针容器管理所有单位实现多态遍历。攻击目标时通过指针访问目标对象的hp并修改。vector里存的是指针对象本身在堆上new出来这就逼着你处理释放问题——正好练习delete的使用。引用也有用武之地。写函数的时候比如findNearestTarget(GameObject self, vectorGameObject* objs)用引用避免拷贝同时保证修改能作用到原对象。新手最常见的困惑“指针和引用到底什么区别”在做这个项目的过程中会自动理解——指针可以被赋值为nullptr可以重新指向别的对象而引用一旦绑定就不能改绑语义上有明显区别。2.3 地图、移动与碰撞检测的简化艺术地图我建议直接用二维数组或者干脆全是自由坐标。原版王者的地图是精美的3D峡谷极简版在地图上花的精力越少越好。我用的是200x50的字符画平面图横坐标x范围0~79纵坐标y范围0~24在控制台上刚好能放下。双方基地分别在两侧中间一路直线作为“中路”。移动实现很简单键盘方向键改变英雄坐标x和y加减移动速度。真正的难点是碰撞检测——怎么判断英雄的攻击命中了小兵或防御塔MOBA的碰撞检测不需要物理级的精确度。我的做法是简化成圆距判断如果两个单位坐标的欧几里得距离小于攻击范围就视为命中。更简单的可以用曼哈顿距离反正极简版对精度要求不高。核心代码如下bool inAttackRange(GameObject a, GameObject b, int range) { int dx a.x - b.x; int dy a.y - b.y; return (dx * dx dy * dy) range * range; }用平方距离比较省去开根号运算。这个优化虽然在这点单位数量下看不出差别但养成习惯是好事。游戏开发里能省则省积少成多就是性能差距。防御塔的仇恨逻辑也可以简化每帧扫描所有敌方单位找距离最近的作为攻击目标。注意这里必须按距离排序就需要用到排序算法——C标准库的sort加自定义比较器就能实现这也是我特意保留的逻辑点让读者明白“排序算法在哪里才会被真正用到”。2.4 技能、冷却与状态机的组合玩法技能系统是游戏最有“手感”的部分。原版游戏的技能包括伤害、控制、位移、护盾等。极简版我做了三个技能一技能直线飞箭对路径上第一个敌方目标造成伤害。二技能范围斩击对周围所有敌人造成伤害冷却时间稍长。三技能大招强化普攻短暂增加攻击力和移速。每个技能要管理冷却状态。我不用复杂的状态机直接在Hero类里存两个成员int skill1Cooldown; int skill2Cooldown;。每次用技能就把对应冷却设为固定值比如5每帧递减到0才允许再次使用。设置冷却时间的计算逻辑很简单// 技能按键 J if (key j skill1Cooldown 0) { castSkill1(); skill1Cooldown 5; // 5秒后可用 } // 每帧更新 void Hero::update(...) { if (skill1Cooldown 0) skill1Cooldown--; if (skill2Cooldown 0) skill2Cooldown--; if (ultimateCooldown 0) ultimateCooldown--; }这套时间计数方式极其直观。等以后做更复杂的项目你会发现技能冷却只是状态机的一个简单特例状态、条件迁移。状态机更大的应用在于AI控制——小兵的状态只有两个移动中、攻击中。防御塔更简单没有敌人时睡觉有敌人时打人。先写技能冷却再写小兵AI状态机的概念就自然打通了。3. 实操记录从零开始搭“极简峡谷”3.1 第一步搭建工程骨架我习惯新建一个文件夹结构src源码、include头文件、build编译产物。刚开始不用搞太复杂直接一个main.cpp加几个头文件就够了。工程骨架的核心是三个文件GameObject.h定义基类和各子类。Game.h定义游戏管理器包含主循环和对象容器。main.cpp程序入口初始化游戏启动主循环。写Game类的思路是这样Game持有vectorGameObject* objects负责创建英雄、防御塔、定期生成小兵并提供每帧update全部对象的方法。为什么要有Game这个类因为如果主循环里的update逻辑全堆在main函数里代码很快会失控。把“游戏世界”抽象成一个类所有对象都归它管逻辑清晰后续加功能也好扩展。3.2 第二步实现英雄移动与技能英雄类是玩家直接操作的对象先写这个最直观。定义的Hero类成员class Hero : public GameObject { public: string name; int speed; int skill1Cooldown; // 一技能CD int skill2Cooldown; // 二技能CD int ultimateCooldown; // 大招CD int killCount; // 击杀数用于属性成长 public: void update(vectorGameObject* objs) override; void render() override; void move(int dx, int dy); void castSkill1(vectorGameObject* objs); void castSkill2(vectorGameObject* objs); void castUltimate(); };移动那块直接绑定键盘。控制台游戏用_getch()Windows下需要conio.h读键盘。注意_getch()会阻塞等待输入如果不做处理游戏会在等待键盘时卡住——所以需要配合_kbhit()判断是否有按键输入if (_kbhit()) { int key _getch(); switch (key) { case w: case W: hero.move(0, -1); break; case s: case S: hero.move(0, 1); break; case a: case A: hero.move(-1, 0); break; case d: case D: hero.move(1, 0); break; case j: hero.castSkill1(gameObjects); break; case k: hero.castSkill2(gameObjects); break; case l: hero.castUltimate(); break; } }使用_kbhit()的意义是每帧先看键盘有没有输入有才处理没有就直接进入更新逻辑。这样游戏画面会持续刷新角色一直能动而不是按一个键动一下。你试过就会明白这基本上是控制台游戏手感好坏的分水岭。技能的实现以范围斩击为例遍历所有敌人检查距离在攻击范围内的就扣血。这里需要固定冷却值否则会出现“按住按键疯狂放技能”的BUG。3.3 第三步加入小兵与推塔判定小兵系统是极简王者最有成就感的点。每隔5秒双方基地各生成一个小兵小兵沿着中路向对方基地移动。遇到敌方单位英雄或小兵就停下来攻击没有敌方单位就继续走。小兵update逻辑的伪代码void Minion::update(vectorGameObject* objs) { if (hp 0) { alive false; return; } GameObject* target findNearestEnemy(objs); if (target inAttackRange(*this, *target, attackRange)) { attack(target); } else { move(team 0 ? 1 : -1, 0); // 队伍0向右推进队伍1向左推进 } }这一小段逻辑包含了AI的入门思想感知找目标、决策打还是走、行动攻击或移动。别看简单所有AI游戏角色的基础都是这个模式。防御塔更简单。防御塔是固定不动的每帧扫描范围内最近的敌方单位开火。塔的数值要比小兵高一大截不然游戏就失去推塔的压力感。双方防御塔各两个放在离基地不同距离的位置敌方英雄要一路拆过去才能打水晶。这样“推塔”就成了整个游戏的核心节奏先清兵、顶着塔伤打塔、塔快破了就撤、再来一波推掉。这条循环做到位极简版就已经很“王者”了。3.4 第四步胜负判定与UI显示胜负判定就两件事某一方水晶血量小于等于0时对方获胜。极简版的王者在setup里生成两队各一个水晶一队两个防御塔一队小兵生成器。水晶类继承GameObjectupdate里什么都不用做只要继承一个alive标志主循环每帧检查即可。渲染方面控制台游戏我建议用“字符画”方式清屏、逐行打印字符。void render() { system(cls); // Windows下清屏 for (int row 0; row mapHeight; row) { for (int col 0; col mapWidth; col) { char ch .; // 根据坐标匹配对象确定该位置显示什么字符 for (auto obj : objects) { if (obj-alive obj-x col obj-y row) { ch obj-getChar(); } } cout ch; } cout endl; } // 显示英雄状态 cout Hero HP: hero.hp / hero.maxHp endl; cout Skill1 CD: hero.skill1Cooldown Skill2 CD: hero.skill2Cooldown endl; cout Kills: hero.killCount endl; }注意system(cls)省事但效率低游戏画面大时会出现闪屏。想改善可以换用光标定位Windows下SetConsoleCursorPosition把绘制起点移回左上角而不是整个清屏。这一步对观感提升十分明显建议做完基础版后顺手优化一下。3.5 完整代码结构参考我给出一个可以参考的文件划分// GameObject.h #pragma once #include vector using namespace std; class GameObject { public: int x, y; int hp, maxHp; int atk; int team; bool alive; public: GameObject(int tx, int ty, int thp, int tAtk, int tteam) : x(tx), y(ty), hp(thp), maxHp(thp), atk(tAtk), team(tteam), alive(true) {} virtual ~GameObject() {} virtual void update(vectorGameObject* objs) 0; virtual char getChar() 0; virtual bool isHero() { return false; } virtual bool isMinion() { return false; } virtual bool isTower() { return false; } virtual bool isCrystal() { return false; } };// Hero.h #pragma once #include GameObject.h class Hero : public GameObject { public: string name; int speed; int skill1Cooldown, skill2Cooldown, ultimateCooldown; int killCount; public: Hero(string tName); void update(vectorGameObject* objs) override; char getChar() override { return team 0 ? H : h; } bool isHero() override { return true; } void move(int dx, int dy); void castSkill1(vectorGameObject* objs); void castSkill2(vectorGameObject* objs); void castUltimate(); };// main.cpp #include iostream #include vector #include conio.h #include windows.h #include Hero.h // ... 其他头文件 int main() { Game game; game.init(); game.run(); return 0; }这个骨架维持了清晰的面向对象结构。真正上手写时建议按“一个类一个文件”的节奏来编译报错时也容易定位。控制台下一个文件全梭哈也能跑但工程一旦超过500行拆文件的好处就开始体现了。4. 常见问题与排查技巧实录4.1 VSCode配置C/C环境时的典型报错很多人在配置环境这步就被劝退我整理几个高频错误g 不是内部或外部命令MinGW的bin目录没加到PATH里或者加了之后没重启终端。验证方法新开一个cmd输入g --version。未找到任务buildtasks.json没生成或格式错了。直接在VSCode里按CtrlShiftP搜“C/C: 生成任务”选“g 生成活动文件”。launch.json 中 program 路径错误调试时要让VSCode知道exe在哪。我的配置是program: ${fileDirname}\\${fileBasenameNoExtension}.exe简单直接。另外提一个高频到不能再高频的报错error: Microsoft Visual C 14.0 is required。这个错误其实不是VSCode或g的问题而是安装Python某些包比如pycocotools、lxml等时MSVC环境缺失导致的。解决办法是按提示安装Visual Studio Build Tools选“使用C的桌面开发”工作负载。这个小问题为什么要提因为很多同时折腾C和Python的读者会在这卡一整天希望你能快速绕开。4.2 控制台输出乱码问题控制台输出中文乱码是Windows上的经典问题。原因很简单源码文件是UTF-8编码而控制台默认用GBK代码页936解析。解决方法有几个最简单代码开头加system(chcp 65001);把控制台切到UTF-8代码页。一劳永逸在main开头加SetConsoleOutputCP(CP_UTF8);这是Windows API调用效果等同但更干净。另一个角度把源文件另存为GBK编码但我不推荐因为会让代码在Git等工具下产生大量乱码diff。我自己的习惯是源文件统一UTF-8main函数第一行加SetConsoleOutputCP(CP_UTF8);从此告别乱码。4.3 游戏卡顿与帧率不稳定控制台游戏的卡顿来源大多是这几种system(cls)全屏清屏太慢。换成光标定位可以明显改善。Sleep()值设得太小比如0或1毫秒导致CPU占用飙升。控制在20~50毫秒之间既流畅又不飙CPU。对象遍历里做了太多重复计算。极简版对象数量撑死几十个cpu完全没压力但如果每帧都重新new和delete对象会产生大量内存碎片也会拖慢。我建议对象池化——小兵死了不是释放内存而是打标记待复用。这是游戏开发里非常基础实用的技巧。输出时每帧重复构造string拼接。可以用ostringstream一次拼好再cout明显比反复cout 快。4.4 指针误用和内存泄漏这个项目里大量使用指针新手最容易犯的错是容器里存了new出来的对象但忘记delete。我建议两条路优先用智能指针把vectorGameObject*换成vectorunique_ptrGameObject让对象生命周期自动管理。C11之后这是最优解。如果坚持裸指针就一定要在游戏结束时手动清理for (auto obj : objects) delete obj;。我见过有人写小游戏每次退出都内存泄漏程序不崩溃但属于不良习惯。还有野指针问题小兵被击杀后会标记alive false但对象还在vector里。如果更新逻辑里再对这个对象取坐标容易访问已释放内存。我的惯例是死亡对象不立即delete而是标记后在下一次遍历时统一清除这样就不会出现迭代器失效或野指针的问题了。// 安全的清理写法 for (auto it objects.begin(); it ! objects.end(); ) { if (!(*it)-alive) { delete *it; it objects.erase(it); } else { it; } }这套写法记得背下来几乎每个写C游戏的人都会遇到。4.5 一个调试小技巧开启监视窗口VSCode的调试模式里可以看容器内容。比如悬停objects变量会看到vector里所有元素的地址和属性。调试游戏逻辑时这一步价值极高——你可以实时看到每个小兵的hp、坐标、阵营状态而不必在控制台列表里猜来猜去。如果不想用IDE调试也可以用临时调试输出在update函数里加一行cout hero pos: hero.x , hero.y endl;跑完删掉。这种“printf调试法”在游戏开发里看起来很土但面对逻辑BUG时反而效率最高因为你能看到每一帧的实际状态流转。我个人在实际操作中的最大体会是这个小项目写完你对C的理解会从“会语法”变成“能干活”。尤其是指针和容器那一块语法书里看不明白的细节在写游戏时会主动弄明白——比如为什么函数参数要用引用、为什么容器的迭代器会失效、为什么多态虚函数能统一接口。这些知识靠刷题很难体会但做成一个看得见、摸得着的小游戏理解就自然到位了。最后再分享一个小技巧极简王者跑通之后你可以试着把地图从字符画换成Windows API的图形绘制SetPixel画点Rectangle画方块或者接上SDL2做真正的图形版本。核心的Hero类、小兵逻辑、碰撞检测几乎不用大改因为设计时就已经把表现层和逻辑层分开了。这一步做完你的C游戏开发就算正式入门了。
返回列表