01-基于C++的简易技能系统实现

01-基于C++的简易技能系统实现


声明:

1、每天上班回来累成狗,码字不容易,请尊重笔者的付出,谢谢。
2、未经允许请不要转载或抄袭笔者的劳动成果(呃虽然文章可能写的不好),谢谢。
3、笔者同意转载后,请注明文章出处,谢谢。
4、欢迎大家在评论区发表意见,欢迎指出文章的不足和一起探讨技术性问题。

2018-09-20更新:
5、实际项目中的技能系统要复杂的很多,很多,这里只是说下基础


前言:

	笔者在写这篇文章时,实习生一枚,喜欢游戏开发,平时喜欢瞎捣鼓,文凭不高,人也菜。要是思路啊,
代码什么的有写的不对的地方,欢迎大家提出,一起交流。

	今天给大家带来,游戏算法逻辑级的首篇文章,笔者没有参与过什么项目,纯粹是平时自己瞎捣鼓,
给入门新手提供一个思路,大神嘛。。。当玩笑看就行了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输出
从图中可知输出顺序是:
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;
}

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

感谢您的阅读,希望对您有帮助。谢谢_.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值