游戏技能系统开发探索2

昨天简单的设计了一个可供主角方便使用的技能容器,今天进一步研究这个容器如何设计才可以变成公用型容器。
先把昨天的容器代码拿来看一下

class HeroSkillList
{
public:
    //获取单例实例方法
    static HeroSkillList& Instance(){}
    std::shared_ptr<SkillBase> FindSkill(std::string){}
private:
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;
    HeroSkillList(){}
    //省略拷贝构造函数,重载赋值运算符等
}

现在我把它变成一个供敌人使用的容器,看看能否顺利使用

class EnemySkillList
{
public:
    //获取单例实例方法
    static EnemySkillList& Instance(){}
    std::shared_ptr<SkillBase> FindSkill(std::string){}
private:
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;
    EnemySkillList()
    {
        //将敌人能使用的技能加入列表
    }
    //省略拷贝构造函数,重载赋值运算符等
}

直接这样敌人能不能直接使用?大概是可以的,敌人的代码可以向这样

class Enemy:public GameObject
{
public:
    void UseSkill()
    {
        EnemySkillList::Instance().FindSkill(/*技能名*/)->Use();
    }
}

完全照搬主角类的模式来做就可以很方便的使用技能。但是这种做法我认为并不是非常好的做法。因为一个游戏中并不止有一种敌人,可能很多的敌人都会使用技能,假设有10种敌人,Enemy1,Enemy2….Enemy10;那么对应的EnemySkillList列表可能就有10种。那么EnemySkillList的很多代码都会被重复编写10遍,这不是我们所希望的。这个List里,至少

    static EnemySkillList& Instance(){}
    std::shared_ptr<SkillBase> FindSkill(std::string){}
private:
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;

这些代码都是被重复编写的,可能还有其他方法也是要被重复写好多遍的,我希望能够有效的重用这些代码,我采取的方法是把这些代码放进一个类中,即SkillListBase,代码如下

template <typename T>
class SkillListBase
{
public:
    static T& Instance()
    {
        static T instance;
        return instance;
    }
    std::shared_ptr<SkillBase> FindSkill(std::string){}
protected:
    SkillListBase(){}
    ~SkillListBase(){}
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;
}

这里我直接把技能列表基类做成了单例基类,至于具体为什么单例基类这么设计,网上一搜一大把,这里不多解释。这样可以使代码复用效率大大提升。在一个特定的技能列表中应该这么做,以主角类举例

class HeroSkillList : public SkillListBase<HeroSkillList>
{
private:
    friend SkillListBase<HeroSkillList>;
    HeroSkillList()
    {
        //添加技能的代码,可能有100行
    }
}

这时这个主角技能列表编写时需要注意三点,第一是继承基类时的写法

class HeroSkillList : public SkillListBase<HeroSkillList>

因为基类时使用泛型的,所以这里需要给出确定的类型,否则编译器会罢工。
第二点是在子类中指定友元

    friend SkillListBase<HeroSkillList>;

如果这里不再子类中将基类指定为友元,在基类的Instance方法中,编译器会认为HeroSkillList这个类的构造函数是不可访问的,会拒绝执行代码。
第三点是子类的构造函数,拷贝构造函数,赋值运算符等应声明为私有成员。这样做可以确保外界无法声明一个子类实例,也无法通过继承来创建一个实例。
做出这样的修改后,主角想要使用一个技能应该怎么做?代码大概像这样

class Hero:public GameObject
{
public:
    void UseSkill()
    {
        HeroSkillList::Instance().FindSkill(/*技能名*/)->Use();
    }
}

和之前没有变化。那么敌人类呢?

class Enemy:public GameObject
{
public:
    void UseSkill()
    {
        EnemySkillList::Instance().FindSkill(/*技能名*/)->Use();
    }
}

敌人类也没有变化。变化的部分在于下面代码

class HeroSkillList : public SkillListBase<HeroSkillList>
{
private:
    friend SkillListBase<HeroSkillList>;
    HeroSkillList()
    {
        //添加技能的代码,可能有100行
    }
}
class EnemySkillList : public SkillListBase<EnemySkillList>
{
private:
    friend SkillListBase<EnemySkillList>;
    EnemySkillList()
    {
        //添加技能的代码,可能有100行
    }
}

修改之前的代码长这样

class HeroSkillList
{
public:
    //获取单例实例方法
    static HeroSkillList& Instance(){}
    std::shared_ptr<SkillBase> FindSkill(std::string){}
private:
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;
    HeroSkillList()
    {
        //将主角能使用的技能加入列表,可能有100行
    }
    //省略拷贝构造函数,重载赋值运算符等
}

class EnemySkillList
{
public:
    //获取单例实例方法
    static EnemySkillList& Instance(){}
    std::shared_ptr<SkillBase> FindSkill(std::string){}
private:
    std::map<std::string,std::shared_ptr<SkillBase>> skillList;
    EnemySkillList()
    {
        //将敌人能使用的技能加入列表,可能有100行
    }
    //省略拷贝构造函数,重载赋值运算符等
}

如果需要很多的skilllist,这样节省的代码量还是很可观的。
程序员有时候是不知道满足的生物,这样的设计放到这里,我会想如果我需要很多的skilllist,那我还需要些好多的skilllist类,而每个skilllist类都只做一点简单的add操作,我感觉这样很浪费,要这样的add操作可能只需要一个函数,在函数里做这些工作不就好了,为什么非要多去开发一个类?我觉得应该有更好的设计,但是网上怎么搜都搜不到一个成型的框架结构,所以我只好自己去想办法设计。最后也就做成了这样的东西。如果以后想到什么能够更加简化设计的方案,我应该会再写一篇文章来记录的吧。


总之,这个系统开发探索进入下一个主题了。首先回到上一篇文章,看一下上一篇文章里遗留下来的一个问题
假设我现在有一个技能drugattack,已经加入到了主角技能列表中,并且再假设这个技能名就叫“drugattack”,接下来我们的主角要是用这个技能攻击一个敌人了,代码像这样

class Hero:public GameObject
{
public:
    void UseSkill()
    {
        HeroSkillList::Instance().FindSkill("drugattack")->Use();
    }
}
int main()
{
    Hero h;
    Enemy e1;
    h.UseSkill();
    return 0;
}

啊,啊咧?我有没有打到e1啊?程序这么走那妥妥是在搞笑吧。没错,现在这样的技能系统有一个极其严重的问题,技能使用与游戏对象间的交互问题该怎么解决,也就是具体的技能设计问题。没错,下一步要研究激动人心的具体技能实现问题了。
解决方案,像drugattack技能,他要首先打出伤害,是对某个对象造成的伤害,然后给这个对象加一个buff,那么很容易想到的解决方案是在use时给这个函数传个参数,drugattack的use方法看起来像这样

void DrugAttack::Use(Enemy e)
{
    skill1.Use(e);
    skill2.Use(e);
}

那么HurtHP和AddDrug里的代码就需要这么修改

void HurtHP::Use(Enemy e)
{
    cout<<"对"<<e.GetName()/*假设有这样的方法*/<<"造成伤害"<<endl;
}
void AddDrug::Use(Enemy e)
{
    cout<<"对"<<e.GetName()<<"添加毒buff"<<endl;
}

然后还要在hero里把代码改成这样


void Hero::UseSkill(Enemy victimer)
{
    HeroSkillList::Instance().FindSkill("drugattack")->Use(victimer);
}

主函数里要这么调

int main()
{
    Hero h;
    Enemy e1;
    h.UseSkill(e1);
    return 0;
}

这样设计有什么问题都不用我多说,问题大了去了,我甚至应该问怎么会有这么垃圾的代码存在。
这样的设计最主要的问题在于hero和enemy的强耦合,我想要使用技能必须知道存在一个敌人类。但是如果我这个技能打出去能打碎一块石头,又该如何处理呢?难道要把石头当成敌人类来处理吗?还有具体的技能设计中,如果说drugattack是主角专属技能,那么接受一个敌人类做参数可能还能说得通,但是属于基础技能类的HurtHP和AddDrug接受一个敌人类型的参数就绝对不合理了。如果有个技能是敌人专属的,用来打主角的,那么我根本不能使用这个基础技能。就算稍微修改一下基础技能类的设计,像下面这样

void AddDrug::Use(GameObject* object){}

也不是一个很合适的处理方式。因为总不可能每次遇到问题后再来修改这些基础的东西。想要实际解决问题,还应该从问题根源下手。

所以在解决技能具体设计之前,需要考虑一下,在一个技能使用过程成计算机需要做出那些处理:

  • 某个对象使用一个技能,一个技能声明周期开始
  • 使用技能对使用者有什么影响?比如会不会消耗mp值?技能有没有冷却时间等等
  • 这个技能如何找到它的作用对象?如果是指向型的技能可能在释放技能时就已经知道他要作用的对象是谁;如果是轨迹型的技能可能需要在技能运行轨迹上查看是不是和某个目标对象发生了碰撞,之后才确定自己的目标对象;如果是范围伤害可能要实时关注自己的作用范围内是否存在某对象,存在的话才会把他作为作用对象。或者其他寻找作用对象的方式去寻找
  • 找到作用对象后,该对对象做什么处理?如果是伤害型的技能,可能要通过某公式去计算具体的伤害数值,如果是加buff类的技能可能需要把某种buff加到对象身上
  • 知道如何处理目标对象后,应该如何将处理结果反馈给目标对象?比如如何将具体的伤害值发送给目标对象?如何将某个具体的buff信息发送给目标对象
  • 执行完上面操作后该如何结束技能的生命周期?或者技能打空后该何时结束技能的生命周期?

我们需要根据上面假想的计算机执行过程来设计一个具体技能。假设现在主角使用了一个技能

hero.UseSkill();

接下来drugattack可能要发挥它的本领了。首先可能需要对它的使用者做一些工作,比如消耗一些MP,它的代码可能像这样

void DrugSkill::Use(GameObject* user)
{
    //判断使用者是否有足够的mp,如果有足够的MP则可以发动技能
    if(user->UseMP(useMPValue))
    {
        //做一些操作
    }
}

这段代码里有两个需要注意的地方,一个是use方法接受的参数,一个技能对他的使用者进行一些了解是有必要的。不止是使用者,有时有的技能还需要对它的目标对象也要有一定的了解。第二个需要注意的是useMPValue变量,之前从来没有提到过这个变量,是因为之前没有提到这个变量的必要。这个变量指示这个技能会消耗使用者多少mp,声明应该声明为类成员变量。可以声明在具体技能类中,也可以声明在技能基类中。具体声明在哪里应该以大部分技能需不需要消耗使用者mp为准则。如果大部分技能需要消耗使用者mp,那么就应该声明在基类中。即使某个技能不需要消耗mp,也可以简单的将这个变量赋值为0来解决。下面给出一个基类代码的范本

class SkillBase
{
public:
    //在基类中将mp消耗量初始化为0
    SkillBase():useMPValue(0){}
    virtual void Use(GameObject* user) = 0;
    std::string GetSkillName(){return skillName;}
protected:
    int useMPValue;
    std::string skillName;
}

然后再来考虑一个问题,如果技能有cd应该如何处理?首先考虑cd时间应该放到哪里去存储?如果是主角的话,并且是单机游戏,一个主角的话,技能只有一套,把技能cd直接放到技能类中也没有问题。但是敌人的技能怎么办?敌人可能有好多,放到技能类中会造成一个时间段内只有一个敌人能使用技能,一个敌人使用技能后所有敌人都在这个技能冷却中。这就是比较滑稽的事情了。所以我认为技能cd应该放在游戏对象中,由每一个游戏对象实例自己去管理自己的cd时间。至于怎么管理,这可能需要一个专门的文章才能讲清楚。这里我就不管了。我们需要假设的是游戏对象有IsInCD和SetSkillCD两个方法,然后一个具体技能中可能需要这样处理代码

void DrugSkill::Use(GameObject* user)
{
    //判断使用者是否有足够的mp,并且这个技能不在技能cd中。如果两个条件都满足则可以发动技能
    if(user->UseMP(useMPValue) && (user->IsInCD(this->skillName))!=true)
    {
        //使用技能后,这个技能就处于cd了,所以需要set
        user->SetSkillCD(this->skillName,CDTime);
        //做一些操作
    }
}

如果有一些其他需要处理的部分,大概思路也差不多。总的来说要处理个别问题还是要针对个别问题去处理代码的。

但是这里有另外一个问题。如果上面的代码拿到编译器上,大概编译时无法通过的。为什么呢?看这段代码

if(user->UseMP(useMPValue) && (user->IsInCD(this->skillName))!=true)

和GameObject的定义

class GameObject{}

是的,GameObject里并没有所谓的UseMP方法,也没有什么CD的方法,这段代码编译器当然不会通过。这里我们倒是可以把这些方法都加到基类里,但是有一些游戏对象并不会去使用技能,那么对于这一部分游戏物体,就会产生大量的无效代码。针对这个问题的处理,我才用的方法就是泛型处理。需要修改的代码是这样的

template <typename User_Type>
class SkillBase
{
public:
    //在基类中将mp消耗量初始化为0
    SkillBase():useMPValue(0){}
    virtual void Use(User_Type* user) = 0;
    std::string GetSkillName(){return skillName;}
protected:
    int useMPValue;
    clock_t CDTime;
    std::string skillName;
}

然后在Use方法里面就变成了这个样子

void DrugSkill::Use(User_Type* user)
{
    //判断使用者是否有足够的mp,并且这个技能不在技能cd中。如果两个条件都满足则可以发动技能
    if(user->UseMP(useMPValue) && (user->IsInCD(this->skillName))!=true)
    {
        //使用技能后,这个技能就处于cd了,所以需要set
        user->SetSkillCD(this->skillName,CDTime);
        //做一些操作
    }
}

当然,如果这个技能是主角专属的,那么代码其实还可以这么处理

class DrugAttack : public SkillBase<Hero>
{
public:
    virtual Use(Hero* user){}
}

如果这个技能可能有很多种类的游戏对象都可以使用,也可以这样做

template <typename User_Type>
class DrugAttack : public SkillBase<User_Type>
{
public:
    virtual Use(User_Type* user){}
}

Use方法里的代码不用做改动。这样的话在代码编辑过程中是不会报错的,但是在有代码段补全的编译器中,比如我用的vs2015里,写出user->的时候,如果user是确定的hero型,那么会提示出它能使用的方法,但如果是泛型的写法,那么编译器就不会提示它能使用的方法。在编译期间,如果user所代表的实际对象可以使用方法,比如可以使用UseMP方法的话,那么编译可以通过,但是如果user所代表的对象不能使用UseMP方法,则会在编译时报错。这个语法现象也很好理解。

接下来考虑第三个步骤应该如何处理,技能如何去捕捉它的作用对象。先分析一下技能会采取一些什么样的方式去捕捉作用对象

  • 范围伤害,俗称aoe,比如三国杀里的万箭齐发(额),就是会对一个范围内的敌人都早成伤害的技能
  • 轨迹型技能,释放后技能延某种轨迹运动,撞到某游戏对象后产生伤害。比如怪物猎人中火龙系的火球攻击。
  • 追踪型技能,释放后会追踪敌人进行运动的技能。其实这种技能也分两种类型,一种是技能放出时技能使用者不需要知道目标对象具体位置,是技能自己去找,比如英雄联盟里狐狸的e(还是哪个?反正是放出三个小火自动找人打那个),还有怪物猎人狱狼龙的龙珠,另一种是放出技能时使用者就指定了技能目标的。比如美国动作电影里常见的火箭筒(喂),这种追踪型技能也有可能打不到人,比如目标命中目标前被别的目标挡下来了,类似英雄联盟中女警的大可以被别人挡下来这样
  • 指定目标施法类的技能。比如rpg游戏中牧师回血,直接指定某一个游戏对象,直接施法就可以回血。使用技能前技能使用这可以明确的知道目标对象是谁,这时代码上可以直接把目标对象当做参数来处理(或者说不当做参数处理才比较麻烦)

大致上游戏中的技能也就有这些释放方式(我能想到这些)。对于前三种技能(准确来说前两种,第三种一部分需要分类在后一部分里),都是需要释放技能后,技能自己去寻找目标对象的,而后一种则可以在使用技能时就考虑捕捉目标对象。这个似乎比较好处理,我先处理这一种情况
这里我一开始想使用泛型去实现,但是实际上能够被选为技能目标的全都应该是游戏对象,即GameObject,而且实际使用泛型还发现有各种各样棘手的问题要处理。所以我这里的代码是这样处理的


template <typename User_Type>
class SkillBase
{
public:
    //在基类中将mp消耗量初始化为0
    SkillBase() :useMPValue(0), CDTime(0) {}
    virtual ~SkillBase() {}
    virtual void Use(User_Type* user, GameObject* victimer = nullptr) = 0;
    std::string GetSkillName() { return skillName; }
protected:
    int useMPValue;
    clock_t CDTime;
    std::string skillName;
};

Use方法的两个参数,第一个是使用者的指针,第二个是目标对象的指针。考虑到如果技能不是指定型的,第二个参数就用不到,所以给一个默认值为空。下面我们来做一个技能来看一下这个基类该如何使用
技能效果是指向某个特定的游戏物体,输出对他造成伤害,然后输出他被毒液感染。实际上就是把drugattack技能设计为指向性技能。我的代码应该这样处理


//实现伤害hp的基础技能
template <typename User_Type>
class HurtHP
{
public:
    void Use(User_Type* user, GameObject* victimer)
    {
        if (victimer != nullptr)
        {
            std::cout << victimer->name << "被攻击了" << std::endl;
        }
    }
};


//实现添加毒buff的基础技能
template <typename User_Type>
class AddDrug
{
public:
    void Use(User_Type* user, GameObject* victimer)
    {
        if (victimer != nullptr)
        {
            std::cout << victimer->name << "中毒了" << std::endl;
        }
    }
};

//主角能够使用的技能
class DrugAttack : public SkillBase<Hero>
{
public:
    DrugAttack();
    virtual ~DrugAttack();

    virtual void Use(Hero* user, GameObject* victimer = nullptr)
    {
        if (victimer != nullptr)
        {
            skill1.Use(user, victimer);
            skill2.Use(user, victimer);
        }
    }
private:
    HurtHP<Hero> skill1;
    AddDrug<Hero> skill2;
};

接下来要考虑一个问题,在主角那边负责管理技能列表的类该如何处理?原来的skilllistbase是在为考虑泛型的基础上设计的,现在技能基类已经变成了泛型设计,skilllistbase也应该做相应改动。具体代码可能像这样

template <typename T, typename User_Type>
class SkillListBase
{
public:
    static T& Instance()
    {
        static T instance;
        return instance;
    }
    std::shared_ptr<SkillBase<User_Type>> FindSkill(std::string skillName)
    {
        return skillList.find(skillName)->second;
    }
protected:
    SkillListBase() {}
    virtual ~SkillListBase() {}
    std::map<std::string, std::shared_ptr<SkillBase<User_Type>>> skillList;
};

接下来主角专用的技能列表大概像这个样子

class HeroSkillList : public SkillListBase<HeroSkillList, Hero>
{
public:
private:
    friend SkillListBase<HeroSkillList, Hero>;
    HeroSkillList()
    {
        //添加技能
        skillList.insert(std::pair<std::string, std::shared_ptr<SkillBase<Hero>>>
            ("drugattack",std::make_shared<DrugAttack>()));
    }
    virtual ~HeroSkillList() {}
};

添加具体技能的语法可能比较长,可以在基类中封装一个add方法简化这里的添加复杂度。比如这样

inline void SkillListBase::AddSkill(std::string skillName,std::shared_ptr<SkillBase<User_Type>> skillPointer)
{
        skillList.insert(std::pair<std::string, std::shared_ptr<SkillBase<User_Type>>>(skillName,skillPointer));
}

在HeroSkillList 中的语法就可以简化成

AddSkill("drugattack",std::make_shared<DrugAttack>());

然后顺便这里我的技能名是随便起的。实际上应该想办法获取到技能的name字段,这并不是很难的操作,所以我就不多说了
接下来主角类中就可以这样处理useskill方法

class Hero : public GameObject
{
public:
    Hero();
    ~Hero();

    void UseSkill(GameObject * victimer=nullptr)
    {
        HeroSkillList::Instance().FindSkill("drugattack")->Use(this, victimer);
    }
private:
};

然后在主函数中测试一下

int main()
{
    Hero h;
    Enemy e;
    h.UseSkill(&e);
    system("pause");
    return 1;
}

运行结果
这里写图片描述
这里我们可以攻击自己,改一下代码

    h.UseSkill(&h);

结果当然是这个样子
这里写图片描述
当然大多数游戏时不能攻击自己的,我的系统还没有做完,会出这种问题也很正常
然后到这一步,我想继续往下考虑一个问题,在这个技能中,想要给敌人造成一些伤害,但是我现在只是打印了一句敌人被攻击了,这当然不是实际技能想要的效果。想要真正达到效果,敌人首先需要有血量这个属性,然后还需要一些对血量操作的方法。比如get,hurt方法等。比如敌人类会受到伤害,可能会有下面这样的定义

class Enemy : public GameObject
{
public:
    int GetHP() { return HP; }
    void HurtHP(int value) { HP -= value; }
private:
    int HP;
};

这时我的HurtHP这个基础技能就想去对敌人造成一些伤害。Use方法里面可能想要这么写


    void Use(User_Type* user, GameObject* victimer)
    {
        if (victimer != nullptr)
        {
            std::cout << victimer->name << "被攻击了" << std::endl;
            victimer->HurtHP(10);
        }
    }

但是这样编译器一定是不会通过的。因为这里的victimer只是一个GameObjet型的指针,在这个类中并没有这个方法,所以编译器一定不会通过。 而这个情况正是我最开始想要使用泛型来处理技能设计的原因。不过在实际操作中使用泛型也会有各种各样的问题,所以我还是直接使用GameObject*作为参数来传递。而接下来处理这个问题的方法,有一点“黑魔法”的感觉,简单的说就是“歪门邪道”的做法。可能也并不好理解。但是我不知道更好的处理方法是怎样的,所以只好先用这种方法来处理一下了。
直接上代码,看我是如何在HurtHP的use方法中做处理的


    void Use(User_Type* user, GameObject* victimer)
    {
        if (victimer != nullptr && dynamic_cast<HPInterface*>(victimer) != nullptr)
        {
            auto victimer_pointer = dynamic_cast<HPInterface*>(victimer);
            std::cout << victimer->name << "被攻击了" << std::endl;
            victimer_pointer->HurtHP(10);
        }
    }

一行一行往下看,看我是如何做处理的
首先if语句,加了一个判断条件,判断这个传来的对象是否可以转型为HPInterface型的指针,如果能转型,则执行if语句中的内容。
下一行,将传进来的变量转型为HPInterface型的指针,变量名称为victimer_pointer
下一行,使用原来的指针获取作用对象的名字,输出出来
下一行,使用转化后的指针,调用HurtHP方法
那么这个HPInterface是什么东西呢?他的源码是这样的


class HPInterface
{
public:
    HPInterface() {}
    virtual ~HPInterface() {}

    virtual int GetHP() = 0;
    virtual void HurtHP(int value) = 0;
};

那么这个东西又有着什么样的作用呢?在解释这个问题前先来看一下enemy类需要做一些什么改动。enemy类代码


class Enemy : public GameObject,
    public HPInterface
{
public:
    Enemy();
    virtual ~Enemy();

    virtual int GetHP() { return HP; }
    virtual void HurtHP(int value) { HP -= value; }
private:
    int HP;
};

嗯,被c++程序员深恶痛绝的多继承出现了。额,在易拉罐,臭鸡蛋,玻璃瓶飞来之前,我还是要解释一下这样做的原因的。首先看一段代码,看完后分析一下这是那种语言的代码

public class Source {  

    public void method1() {  
        System.out.println("this is original method!");  
    }  
}  

public interface Targetable {  

    /* 与原类中的方法相同 */  
    public void method1();  

    /* 新类的方法 */  
    public void method2();  
}  

public class Adapter extends Source implements Targetable {  

    @Override  
    public void method2() {  
        System.out.println("this is the targetable method!");  
    }  
}  

嗯,没错,这时java的一段源码,这段源码转自Java开发中的23种设计模式详解(转)。这是适配器模式的一段示例代码。我把它转过来是因为里面有这样的代码

public class Adapter extends Source implements Targetable 

这句代码的意思是Adapter继承Source,实现Targetable接口。在java中这种语法当然合法,而且使用十分广泛。但是剖其根源,实际上这就是另一种形式的多继承。在java中设计接口的原则是所有方法都是公有的,都是虚函数,且没有成员变量。c++没有“接口”这种语法,但是完全可以仿照java的接口的设计原则来模仿接口。而且c++的接口设计远比java的接口要来的自由的多。c++的接口里甚至也可以加成员变量。但是我并不建议哪个程序员在c++设计接口的时候往接口类中加一些数据成员进去。因为c++中的死亡钻石(菱形继承)究其原因就是因为基类中的非虚成员函数和成员变量所引起的,但是如果一个类中所有成员都是虚函数甚至是纯虚函数,就不必担心菱形继承造成的后果了。
所以在这里,我的HPInterface实际上就是接口。而且这里的HPInterface读作接口,写作class。没错,这个HPInterface是可以使用多态的,也就是一个enemy实例, 它既可以被一个GameObject* 所指向,也可以被一个HPInterface*所指向。那么这样一来,就可以解读HurtHP的use方法到底做了些什么了。
首先,一个enemy被传递过来,他是被一个GameObject* 所指向的对象,接下来dynamic_cast发现他可以向下转型为HPInterface* ,于是执行if内语句。这时首先定义了一个HPInterface* 来指向原来的对象,这样use方法里就有两个指针指向原来的enemy了,一个是HPInterface* ,一个是GameObject* 。接下来使用GameObject* 的指针去获取enemy的name字段。因为只有这个类型的指针知道enemy有name字段,而HPInterface* 并不知道enemy有这个字段,再下一步由HPInterface* 来调用enemy的HurtHP方法。因为只有HPInterface* 才知道enemy有这个方法。
这样在来试一下主角使用技能攻击敌人会有什么效果
这里写图片描述
接下来简单的实验一下给敌人设计一个技能,让他能指定的去攻击主角。为方便起见,敌人技能只有简单的伤血性能。下面是代码

//简单起见直接命名为skill1
class Skill1 : public SkillBase<Enemy>
{
public:
    Skill1();
    virtual ~Skill1();

    virtual void Use(Enemy* user, GameObject* victimer = nullptr);
private:
    HurtHP<Enemy> skill1;
};

//给敌人设计一个技能列表
class EnemySkillList : public SkillListBase<EnemySkillList, Enemy>
{
public:
private:
    friend SkillListBase<EnemySkillList, Enemy>;
    EnemySkillList()
    {
        //添加技能
        AddSkill("skill1", std::make_shared<Skill1>());

    }
    virtual ~EnemySkillList() {}
};

//敌人的使用技能方法
class Enemy : public GameObject,
    public HPInterface
{
public:
    Enemy();
    ~Enemy();

    void UseSkill(GameObject * victimer = nullptr)
    {
        EnemySkillList::Instance().FindSkill("skill1")->Use(this, victimer);
    }
    virtual int GetHP() { return HP; }
    virtual void HurtHP(int value) { HP -= value; }
private:
    int HP;

};

//主角也去实现HPInterface接口
class Hero : public GameObject,
    public HPInterface
{
public:
    Hero();
    ~Hero();

    void UseSkill(GameObject * victimer=nullptr)
    {
        HeroSkillList::Instance().FindSkill("drugattack")->Use(this, victimer);
    }
    virtual int GetHP() { return HP; }
    virtual void HurtHP(int value) { HP = HP - (2 * value); };
private:
    int HP = 120;
};

运行结果
这里写图片描述
这篇博客就先到这里,接下来要研究如何处理aoe型技能和轨迹型技能如何查找目标对象,以及技能系统接下来的各种问题

猜你喜欢

转载自blog.csdn.net/fsdafsagsadgas/article/details/69814640