您的位置:首页 > 编程语言 > C语言/C++

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的技能会被删除,这个在游戏中一般是不允许的。想想你捡到一本技能书,点击使用后却发现槽位满了。这个时候不仅技能没有学到书也不见了。唯一做的处理就是保存这个技能到技能背包中待玩家使用,或者是发出提示消息并将该技能书返还回玩家,这个需要读者自己去摸索实现了。

感谢您的阅读,希望对您有帮助。谢谢^_^.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息