01-基于C++的简易技能系统实现
2017-02-28 22:34
661 查看
01-基于C++的简易技能系统实现
声明:
1、每天上班回来累成狗,码字不容易,请尊重笔者的付出,谢谢。2、未经允许请不要转载或抄袭笔者的劳动成果(呃虽然文章可能写的不好),谢谢。
3、笔者同意转载后,请注明文章出处,谢谢。
4、欢迎大家在评论区发表意见,欢迎指出文章的不足和一起探讨技术性问题。
前言:
笔者在写这篇文章时,实习生一枚,喜欢游戏开发,平时喜欢瞎捣鼓,文凭不高,人也菜。要是思路啊, 代码什么的有写的不对的地方,欢迎大家提出,一起交流。 今天给大家带来,游戏算法逻辑级的首篇文章,笔者没有参与过什么项目,纯粹是平时自己瞎捣鼓, 给入门新手提供一个思路,大神嘛。。。当玩笑看就行了0.0。 这次我们来探讨下,游戏中技能管理的实现,编写出一个利于维护的简易技能系统(我们只考虑整体 系统的实现思路,基于文章篇幅的原因,我们不会讨论具体技能的实现)....
思考:
对于刚刚入坑游戏编程的新人而言,你该怎么去设计技能系统呢? 你会问: 1、一个技能对应一个函数还是类? 2、技能跟英雄有什么关系? 3、除了英雄自带的技能以外,道具给予英雄的额外技能又该怎么处理?
对读者的要求:
1、后面代码是用C++实现的。笔者在此假设你对C++有一定的了解,如枚举、类、vector和纯虚函数等。 2、基本了解什么是面向对象,理解面向对象三大特征(封装、继承、多态)。 3、基本的指针操作。明白什么是“址传递”什么是“值传递”。
目标:
1、实现一个面向基类编程的易于后期维护的简易技能框架。 2、技能包括英雄技能和装备所附加在英雄身上的技能。 3、解决目标1的过程中,解答思考中的一系列问题。
正文:
注意:为了方便大家查看代码,类的实现被直接放在类的声明中。**思考第1问:** 思路1:笔者认为,如果你使用过面向过程语言(如C语言)来编写过游戏,你会倾向于实现一个 技能对应一个函数,如果每个角色都需要5个技能,那你就得为他们提供5个技能函数,然后再switch case 各种技能按键,然后对应case调用对应的技能函数。这样做没有错,还是可以实现出来的。但是代码灵活性不强, 纯粹函数实现技能容易被写死,后面不好维护管理,一维护就要改很多代码,打个比方,像一些大型游戏,一个角 色可能有几十或者几百个技能,那么你就要维护一个很长的switch case ,不怕一万就怕万一你给角色增删技 能的时候,删错了,添加错了,然后就等着DEBUG吧,这样的实现想想就头疼。 思路2:第二种就是笔者极力推荐的,用类来实现技能,一个技能对应一个类,但是这些技能类都要继承于 同一个技能基类,这样做的好处在于,我们是对基类的纯虚接口编程(需要掌握类继承、纯虚函数等知识),降 低对具体类的依赖,降低代码耦合,角色类只需要维护技能基类的“指针动态数组"就行了(为什么这里强调动态 数组?因为玩家可以拥有很多技能啊,你不能写死,要给后面留后路,码后好相见),便于后期维护。现在听不 懂?没关系!后面笔者会上代码解释这一思路。 **思考第2问** 技能跟英雄的最好关系是组合关系,就好想USB线跟电脑USB插口的关系一样,你USB拔出来不会影响到电脑, 同时你可以换用不同的厂家的USB线(相当于替换为其他技能),当然前面要求必须是标准USB接口,这个接口可以 理解为基类指针,也就是面向基佬编程。 **思考第3问** 关于物品自带技能这块,由WAR3编辑器的启发,个人认为物品技能跟人物技能实际上是同属一个基类,只是 将这个基类指针指向了某个专门为物品实现的技能,并将这个技能挂载到物品上而已。使用的时候直接调用这个物 品的使用接口,转而调用对应技能的使用接口。在本文中从物品添加的技能将会默认被添加到玩家技能列表中(有 点类似技能书的实现方式)。
实现代码
技能ID声明enum ESkill_ID { Skill_ID_FireBall, //火球术ID Skill_ID_SnowStorm, //暴风雪ID Skill_ID_HolyLight, //圣光术 /////// };
1、首先实现Property_Base(道具基类)并派生具体道具
//道具基类 class Property_Base { public: Property_Base() {} virtual ~Property_Base() {} virtual ESkill_ID GetSkillID() = 0; //作为接口用,获取该道具包含的技能ID }; //带有火球术技能的道具 class Property_FireBall : public Property_Base { public: Property_FireBall() { m_skill_ID = Skill_ID_FireBall; //初始化该道具特有技能的技能ID } virtual ~Property_FireBall() {} ESkill_ID GetSkillID() override //override基类接口 { return m_skill_ID; //返回道具所包含的技能ID } protected: ESkill_ID m_skill_ID; //技能ID };
2、然后实现Skill_Base(技能基类)并派生具体技能
//技能基类 class Skill_Base { public: Skill_Base(ESkill_ID id, unsigned level) { m_skill_id = id; m_skill_level = level; } ~Skill_Base() {} ////接口 virtual void InitMagic() = 0; //技能初始化函数 virtual void Use() = 0; //释放技能接口 ESkill_ID GetSkillID() { return m_skill_id; } //获取技能ID protected: virtual bool SelectedTarget() = 0; protected: ESkill_ID m_skill_id; unsigned m_skill_level; }; //基类派生出来的火球术技能 class Skill_FireBall : public Skill_Base { public: Skill_FireBall(ESkill_ID id, unsigned level) : Skill_Base(id, level) {} virtual ~Skill_FireBall() {} void InitMagic() override { cout << "initialized the FireBall" << endl; } void Use() override { if (SelectedTarget()) cout << "Emit the FireBall to the enemy" << endl; } protected: bool SelectedTarget() override { //技能寻找附近可攻击的敌人,有则true,没有则false //注意,实际上这里的函数应该是根据具体的技能类型 返回获取到的目标的Object, //例如火球术技能调用这个函数之后应该是返回玩家制定敌人单位的object而不是简单的返回bool值 //这里为了方便才返回bool,希望读者注意,介于篇幅的问题,不会过多介绍内部功能函数如何实现。 //毕竟本次我们只想了解大体框架 return true; //test always true } }; //基类派生出来的暴风雪技能 class Skill_SnowStorm : public Skill_Base { public: Skill_SnowStorm(ESkill_ID id, unsigned level) : Skill_Base(id, level) {} virtual ~Skill_SnowStorm() {} void InitMagic() override { cout << "initialized the SnowStorm" << endl; } void Use() override { if (SelectedTarget()) cout << "Use SnowStorm attack enemies" << endl; } protected: bool SelectedTarget() override { return true; //test always true } }; //基类派生出来的圣光术技能 class Skill_HolyLight : public Skill_Base { public: Skill_HolyLight(ESkill_ID id, unsigned level) : Skill_Base(id, level) {} virtual ~Skill_HolyLight() {} void InitMagic() override { cout << "initialized the HolyLight" << endl; } void Use() override { if (SelectedTarget()) cout << "Use HolyLigt to recover your ally health" << endl; } protected: bool SelectedTarget() override { //寻找最近一定范围内,生命值最低的友军单位 return true; //test always true } };
3、最后实现Unit_Base(单位基类)并派生具体单位
//技能----简单工厂,根据ID返回具体技能实例,此处为声明 Skill_Base* SkillFactory(ESkill_ID id); //单位基类 class Unit_Base { public: Unit_Base() {} virtual ~Unit_Base() {} //接口 virtual void GerProperty(Property_Base* _property) = 0; virtual void AddSkillWithID(ESkill_ID id) = 0; virtual void RemoveSkill(ESkill_ID id) = 0; virtual void Attack(unsigned slotID) = 0; }; //玩家类 class Unit_Player : public Unit_Base { public: Unit_Player(unsigned numSkillSlot) { m_skill_Slot.clear();//清空技能槽 m_skill_Slot.resize(numSkillSlot);//重置技能槽 for (auto& item : m_skill_Slot) item = nullptr; m_curNumOfEmptySlot = (unsigned)m_skill_Slot.size(); } virtual ~Unit_Player() {} public: ////重写基类接口 void GerProperty(Property_Base* _property) override { AddSkillWithID(_property->GetSkillID()); //这个函数用来获取道具,并获取道具中的对应技能ID } void RemoveSkill(ESkill_ID id) override { //vector<Skill_Base*>::iterator itr = m_skill_Slot.begin(); //while (itr != m_skill_Slot.end()) //遍历技能 //{ // if (*itr) // { // if ((*itr)->GetSkillID() == id)//删除对应ID的技能 // { // delete (*itr); // *itr = NULL; // m_curNumOfEmptySlot++; // break; // } // } // ++itr; //} for (auto& item : m_skill_Slot) { if (item) { if (item->GetSkillID() == id) { delete item; item = nullptr; m_curNumOfEmptySlot++; break; } } } } void Attack(unsigned slotID) override //玩家攻击,需要对应槽ID { //下面两条bool表示,当前空的技能槽数等于最大技能槽数 和 输入的槽ID索引大于最大槽ID索引 bool condition1 = m_curNumOfEmptySlot == m_skill_Slot.size(); bool condition2 = slotID > m_skill_Slot.size() - 1; if (condition1 || condition2) { cout << "warning : no skill in the slot, or the slotID is out of range of size of the m_skill_slot" << endl; return; } if (!m_skill_Slot[slotID]) { cout << "can not selected empty slot" << endl; return; } m_skill_Slot[slotID]->Use(); } protected: void AddSkillWithID(ESkill_ID id) { if (m_curNumOfEmptySlot == 0) { cout << "warning : no empty slot" << endl; return; } Skill_Base* newSkill = SkillFactory(id); if (!newSkill) return; bool findEmptySlot = false; //遍历技能槽,遍历中找到空的技能槽时插入技能,并返回 for (auto& item : m_skill_Slot) { if (!item) { item = newSkill; item->InitMagic(); //初始化技能 findEmptySlot = true; break; } } //没有成功插入到技能槽中去,因为是临时new的技能,没有插入成功的话要把这个失败的对象给消除。 if (!findEmptySlot) { delete newSkill; newSkill = nullptr; return; } m_curNumOfEmptySlot--;//空的技能槽数 -1 } protected: vector <Skill_Base*> m_skill_Slot; //技能槽列表 unsigned m_curNumOfEmptySlot; //空的(可用的)技能槽数 }; ////技能----简单工厂的具体函数实现,这里偷懒,直接写成一个函数了 Skill_Base* SkillFactory(ESkill_ID id) { Skill_Base* skill = nullptr; switch (id) { case Skill_ID_FireBall: skill = new Skill_FireBall(Skill_ID_FireBall, 0); break; case Skill_ID_SnowStorm: skill = new Skill_SnowStorm(Skill_ID_SnowStorm, 0); break; case Skill_ID_HolyLight: skill = new Skill_HolyLight(Skill_ID_HolyLight, 0); break; default: cout << "wrong ID" << endl; break; } return skill; }
测试结果:
情景1:玩家拥有2个技能槽,学会并使用圣光术,火球术,最后从技能槽中删除火球术再调用一次火球术,代码如下。int main() { //初始化玩家并设置技能槽数为2个 Unit_Base* Player = new Unit_Player(2); Player->AddSkillWithID(Skill_ID_HolyLight);//圣光 Player->AddSkillWithID(Skill_ID_FireBall); //火球 Player->Attack(0); //1号槽位对应圣光术 Player->Attack(1); //2号槽位对应火球术 Player->RemoveSkill(Skill_ID_FireBall);//删除火球术 Player->Attack(1); //再次使用2号槽位 getchar(); //卡程序专用 return 1; }
对应输出:
从图中可知输出顺序是:
1、先学习并初始化圣光术
2、然后再学习火球术。
3、使用圣光术
4、使用火球术
5、因为删除了火球术所以对应使用2号槽位技能时提示失效
情景2:玩家拥有4个技能槽,学会并使用圣光术,暴风雪,玩家获取到技能书“火球术”,最后从技能槽中删除圣光术、火球术,再调用一次圣光术、火球术、第4个空的技能槽以检验代码健壮性,代码如下。
int main() { //初始化玩家并设置技能槽数为4个 Unit_Base* Player = new Unit_Player(4); Property_Base* prop_FireBall = new Property_FireBall; Player->AddSkillWithID(Skill_ID_HolyLight);//学习圣光 Player->AddSkillWithID(Skill_ID_SnowStorm);//学习暴风雪 Player->GerProperty(prop_FireBall); //获取火球术技能书 Player->Attack(0); //1号技能槽位圣光术 Player->Attack(1); //2号技能槽位暴风雪 Player->Attack(2); //3号技能槽位火球术 Player->RemoveSkill(Skill_ID_HolyLight);//遗忘圣光 Player->RemoveSkill(Skill_ID_FireBall);//遗忘火球 Player->Attack(0); //再次使用圣光 Player->Attack(2); //再次使用火球术 Player->Attack(3); //使用4号位空技能槽检验是否有BUG。 getchar(); return 1; }
对应输出:
从图中可知输出顺序是
1、学习圣光术
2、学习暴风雪
3、获取技能书中的火球术技能并初始化
4、使用圣光术
5、使用暴风雪
6、使用火球术
7、删除圣光术(1号技能槽位)使用失效
8、删除火球术(3号技能槽位)使用失效
9、使用4号技能槽位(默认没有绑定任何技能)使用失效
总结
优点:1、技能槽使用vector来实现管理,优势在于vector跟数组一样支持随机存取,当玩家需要添加更多技能时可以pushback或者reset size.对应简单的技能系统而言更加方便,需要注意的是在操作vector时一定要注意迭代器失效的问题,并且删除掉的技能对应槽位指针一定要置空(NULL)。
2、基于基类编程,大大降低了模块间对具体类的依赖,降低耦合。使用基类指针来管理所有派生类(当然你可以写个管理器也行,这里不在赘述)。
3、技能(Skill_Base)跟玩家(Unit_Base)是一个组合关系,想要添加技能直接加入(ADD)进去、不想要就拔出来(Remove)。跟USB一样的道理。
4、当前代码比一个技能对应一个函数的C语言式的写法灵活性提高很多。并且利于后面维护。
缺点:
1、程序没有做到是否重复学习的判断,如果玩家同时学2次相同技能,调用一次remove会删除第一个遍历到的满足条件的技能。如果我想删除后面的怎么办?
2、真正的大型项目中所有的技能ID都是从配置文件中读取,大型游戏一般都会伴随有一整套的辅助工具,可以让策划可以更好的脱离程序工作,而不像本文一样直接写死在程序中。
3、这里有个细节,在AddSkillWithID函数中,如果槽位满了,那么当前new的技能会被删除,这个在游戏中一般是不允许的。想想你捡到一本技能书,点击使用后却发现槽位满了。这个时候不仅技能没有学到书也不见了。唯一做的处理就是保存这个技能到技能背包中待玩家使用,或者是发出提示消息并将该技能书返还回玩家,这个需要读者自己去摸索实现了。
感谢您的阅读,希望对您有帮助。谢谢^_^.
相关文章推荐
- 用 C++ 实现基于 session 的权限管理系统
- C++实现简易log日志系统
- 【推荐系统实战】:C++实现基于用户的协同过滤(UserCollaborativeFilter)
- 基于SSM实现的简易员工管理系统(基于阿里云的网站上线篇)
- 基于SSM实现的简易员工管理系统(基于阿里云的网站上线篇)
- 推荐系统之基于二部图的个性化推荐系统原理及C++实现
- C++实现简易通讯录系统
- 基于GUI的简易图像处理系统设计与实现
- “基于关键字匹配的文本过滤系统”配置文件的设计和实现(C/C++源码)
- C++实现简易log日志系统
- 基于RFID的简易图书管理系统设计与实现
- 基于LogAnalyzer实现简易日志系统
- 基于SSM实现的简易员工管理系统(基于阿里云的网站上线篇)
- 基于SSM实现的简易员工管理系统(基于阿里云的网站上线篇)
- c++链表实现学生成绩管理系统(简易版)
- 推荐系统之基于二部图的个性化推荐系统原理及C++实现
- 推荐系统之基于二部图的个性化推荐系统原理及C++实现
- C++实现的huffman与canonical huffman的压缩解压缩系统,支持基于单词的压缩解压缩
- 基于SSM实现的简易员工管理系统
- “基于关键字匹配的文本过滤系统”配置文件的设计和实现(C/C++源码)