机器人Bling的实现
下面我们要来制作机器人Bling。很显然,由于Bling可以有各种不同的属性取值,我们处理方法与Kaiju一样,先将所有Bling的共性抽象成一个“机器人类”。
依据习俗约定,我们将机器人类命名为“bling_t”。
Code (c):
class bling_t{
};
有了刚才制作怪兽类的经验,我们很快就能写出bling_t的大体框架。
Code (c):
class bling_t{
private:
double crit; //暴击属性
double strength; //力量属性
double power; //当前剩余能量
static const double power_max; //能量最大值,是静态常数。
public:
bling_t(double str, double c) : crit(c), strength(str), power(power_max){} //输入属性(力量和暴击)来构造一只Bling。
};
const double bling_t::power_max = 100.0; //给能量最大值赋初值。
公共冷却是与每一只Bling相关的,它也应该属于机器人类的一个属性。
我们如何表达公共冷却?记录公共冷却的结束时刻就可以了。我们对公共冷却何时起始不感兴趣,我们只关心它何时结束以便使用下一个技能。
时间类型time_t在C语言头文件“time.h”中有定义,它们将time_t定义为了内置类型long。我们不妨使用它。
Code (c):
#include <time.h> //从time.h头文件中获取time_t类型的定义。
class bling_t{
private:
double crit;
double strength;
double power;
static const double power_max;
time_t gcd_expire; //使用time_t类型记录公共冷却的结束时刻,我们以毫秒为单位。
public:
bling_t(double str, double c) : crit(c), strength(str), power(power_max), gcd_expire(0) {} //别忘了将它也加入成员初值列一起初始化。
};
const double bling_t::power_max = 100.0;
我们记录了公共冷却的结束时间,那么如何表示当前时间?
同样,你也可以使用Singleton设计模式,建立一个函数,每次调用返回一个指向时间的引用。
这里time_t的定义并不复杂,它只是一个整数而已,所以如果你直接使用一个全局变量来表示它,也是非常不错的。
我们还是使用Singleton来表示,因为这种手法可扩展,有保障,在time_t稍复杂一些的模拟器中还应该使用它。
使用Singleton还有一个好处是,即使是对于基本类型,它也能保证被正确赋予初值。被声明为static的时间,无需显式赋初值,在程序启动时会自动初始化为0。
如果这里你使用非static的变量来表示,不要忘了给它赋初值。
Code (c):
#include <time.h>
time_t& now(){ //一个Singleton函数,
static time_t t; //包含了一个静态的时间,
return t; //每次调用now(),就返回指向它的引用。
}
class bling_t{
private:
double crit;
double strength;
double power;
static const double power_max;
time_t gcd_expire;
public:
bool is_gcd_expired() const{ //一个成员函数,判断公共冷却是否已经结束。
return gcd_expire<=now(); //就这样判断。
}
void trigger_gcd(time_t duration){ //另一个成员函数,触发长度为duration的公共冷却。
gcd_expire=now()+duration; //记录公共冷却的结束时刻,同样使用到了now()。
}
bling_t(double str, double c) : crit(c), strength(str), power(power_max), gcd_expire(0){}
};
const double bling_t::power_max = 100.0;
另外,我们需要添加一系列的成员方法,供后面设计技能使用。包括返回各项属性值、消耗/恢复能量等等。
Code (c):
class bling_t{
private:
double crit;
double strength;
double power;
static const double power_max;
time_t gcd_expire;
public:
double get_crit() const{ //返回暴击
return crit;
}
double get_str() const{ //返回力量
return strength;
}
bool has_such_power(double p) const{ //是否有足够的能量
return power>=p;
}
bool is_gcd_expired() const{ //公冷是否结束
return gcd_expire<=now();
}
void consume_power(double p){ //消耗能量
power-=p;
if (power<0) power=0; //避免出现负能量。
}
void resume_power(double p){ //恢复能量
power+=p;
if (power>power_max) power=power_max; //不能超过最大值。
}
void trigger_gcd(time_t duration){ //触发公冷
gcd_expire=now()+duration;
}
bling_t(double str, double c) : crit(c), strength(str), power(power_max), gcd_expire(0){}
};
机器人类大体就这样了。
现在我们试试描述Bling的两个技能。
在第一部里我们已经详细描述了使用“继承”来描绘技能的方式,这里我们就使用它。
首先着手新建一个技能类“bling_spell_t”。技能类需要描绘出一个最平凡的技能是如何工作的。
Code (c):
class bling_spell_t{ //布林技能类
protected: //不同于“公有”和“私有”,这里使用一种特殊的类型,称为“保护”。
bling_t* bling_ptr; //一个指针,通过它访问技能的持有者Bling。
private: //私有属性
time_t cd; //冷却长度
time_t gcd; //公共冷却长度
time_t cd_expire; //记录这个技能的冷却结束时间。
double power_consume; //消耗的能量
double power_resume; //恢复的能量
public:
bling_spell_t(bling_t* ptr, time_t _cd, time_t _gcd, double _power_consume, double _power_resume) : //构造函数
bling_ptr(ptr), //要想构造一个布林的技能,你必须提供所有这些属性的值。
cd(_cd), //包括:一个指向布林的指针“bling_ptr”,技能的冷却长度“cd”,公共冷却长度“gcd”,
gcd(_gcd), //技能消耗和恢复的能量“power_consume”和“power_resume”。
cd_expire(0), //另外,初始时刻技能处于冷却完成状态,将cd_expire初始化为0
power_consume(_power_consume),
power_resume(_power_resume)
{};
virtual bool execute(){ //方法:执行
if (gcd>0&&!bling_ptr->is_gcd_expired()) return false; //检查公共冷却
if (cd>0&&cd_expire>now()) return false; //检查冷却
if (power_consume>0&&!bling_ptr->has_such_power(power_consume)) return false; //检查能量
if (gcd>0) bling_ptr->trigger_gcd(gcd); //如果都过关,继续执行。触发公共冷却。
if (cd>0) cd_expire=now()+cd; //触发冷却。
if (power_consume>0) bling_ptr->consume_power(power_consume); //消耗能量
if (power_resume>0) bling_ptr->resume_power(power_resume); //恢复能量
return true; //返回执行成功。
};
};
我们见到了一个新鲜名词“protected”。对,在C++中,除了私有和公有之外,还有一种成员类型叫做“保护”。保护是介于公有和私有之间的一种类型,它可以被自身所处的类所看见,可以被它的派生类所看见,但不能被派生类以外的位置看见。一个表格可以说明得更清楚:
由于我们的派生类也需要访问这个成员“bling_ptr”,通过它才能找到使用此技能的布林是哪一只。private将权限收得太紧,public又将权限放得太松,所以我们选择protected。
将一个方法声明为virtual,意为“虚函数”,它可以被派生类继承,也可以被派生类所覆写。
这里execute()方法就是一个虚函数。如果某个技能继承自“bling_spell_t”,那么这个技能会自动获得一份execute方法的默认实现,也就是我们上面所写的那些“检查公冷、检查冷却、……”;如果该技能决定自己写一份实现,那么它可以覆写这段代码,程序会转而采用它所写的代码。
我们采用的是第一部中提到的“继承”设计法。由于这个基类“bling_spell_t”需要被各个技能类“继承”,各个技能类对同一种方法可以有不同的实现,我们说“bling_spell_t”是多态的。
什么叫多态?举个例子。猫头鹰是一种鸟类,企鹅也是一种鸟类,我们把猫头鹰和企鹅都叫做鸟类的派生类,把鸟类叫做它们的基类。
如果我提到“一只鸟”,那么你就明白,我可能说的是一只猫头鹰,也有可能说的是一只企鹅。
猫头鹰会孵蛋,企鹅也会孵蛋。但是猫头鹰由雌性孵蛋,企鹅是由雄性孵蛋的。“一只鸟”有至少两种可能的孵蛋方法,鸟类所具有的这种性质叫做多态。
这里,“打脸.EXE类”将会是“布林技能类”的派生类,“蓝屏类”也将会是“布林技能类”的派生类。它们的执行方法各有不同,所以我们说“布林技能类”是多态的。
简单的多态判定,就是看一个基类中是否有虚函数成员。
为什么要突然插入这么一段话来讲解什么叫多态?因为这里涉及到一项原则:如果一个基类是多态的,你必须声明一个virtual析构函数。
Code (c):
class bling_spell_t{
protected:
bling_t* bling_ptr;
private:
time_t cd;
time_t gcd;
time_t cd_expire;
double power_consume;
double power_resume;
public:
bling_spell_t(bling_t* ptr, time_t _cd, time_t _gcd, double _power_consume, double _power_resume) :
bling_ptr(ptr),
cd(_cd),
gcd(_gcd),
cd_expire(0),
power_consume(_power_consume),
power_resume(_power_resume)
{};
virtual bool execute(){ //有虚函数成员,所以是多态的。
if (gcd>0&&!bling_ptr->is_gcd_expired()) return false;
if (cd>0&&cd_expire>now()) return false;
if (power_consume>0&&!bling_ptr->has_such_power(power_consume)) return false;
if (gcd>0) bling_ptr->trigger_gcd(gcd);
if (cd>0) cd_expire=now()+cd;
if (power_consume>0) bling_ptr->consume_power(power_consume);
if (power_resume>0) bling_ptr->resume_power(power_resume);
return true;
};
virtual ~bling_spell_t(){}; //这就叫virtual析构函数。没什么必须delete掉的成员所以具体实现是空的,但必须声明一下让它是virtual才行。
};
基类写好,我们开始写派生类。我们先挑软柿子捏,蓝屏这个技能看起来简单一些,我们把它实现。
Code (c):
class bsod_t : public bling_spell_t{ //后边这个public是什么意思不用管,只要知道这个冒号加public是“继承自”的意思就行了。
public:
bsod_t(bling_t* ptr) : bling_spell_t(ptr, 6000, 1500, 0, 50) {} //构造函数,接受一个指向布林的指针,然后把属性设置好。
}; //呃,就完了。
真的就结束了……恢复能量已经在基类中实现,我们只要提供恢复的具体数值“50”就可以了。
另一个技能是打脸。
Code (c):
class smackthat_dot_exe_t : public bling_spell_t{
public:
smackthat_dot_exe_t(bling_t* ptr) : bling_spell_t(ptr, 0, 1500, 30, 0) {} //和蓝屏一样的构造函数,就是改改数字。
virtual bool execute(){ //覆写虚函数“execute”
if (!bling_spell_t::execute()) return false; //条件检查都在基类的“execute”中,这里调用一下
//执行基类提供的“execute”之后,还不够,
double d = 236.0; //计算伤害,基础伤害236
d += bling_ptr->get_str(); //每点力量增加1点伤害
double c = 0.0; //还有可能会暴击,基础暴击率0%
c += bling_ptr->get_crit(); //再加上暴击属性
if ( static_cast<double>(rand())/RAND_MAX < c ) //进行一次Roll点,如果Roll出暴击来,
d *= 2.0; //伤害就翻倍。
enemy().take_damage(d,DAMAGE_MECHANICAL); //对敌人造成这么多的机械伤害。
return true; //返回执行成功。
};
};
“static_cast<double>(rand())/RAND_MAX”在上一部讲到随机数发生器的时候提到了。rand产生的是随机整数,我们这样把它转换成小数。
调整代码顺序
要使用rand(),你需要将C函数库头文件“stdlib.h”包含进来,rand()在其中有定义。
在代码的顶端写上:
Code (c):
#include <stdlib.h>
呃,刚才是不是包含过一个“time.h”?
把它也挪到顶端去,两个头文件写在一起比较好看。
Code (c):
#include <stdlib.h>
#include <time.h>
//后边是代码
在C++中,你在使用一个东西之前,必须能够在其上文找到这个东西的声明式或者定义式。
Code (c):
//这是一个例子。
class some_class_t; //这是一个类的声明式,但只声明了类本身,没有声明其中的任何成员。
some_class_t some_object; //这是一个对象的定义式,其中使用了some_class_t,而且前面已经有了some_class_t的声明,所以没问题。
some_object.some_method(); //尝试使用对象some_object的方法some_method(),前者能够找到定义式,没问题;
//但是后者既找不到定义式也找不到声明式,不行。
其他代码都好办,我们依次顺序写下来的。但是在布林这里出现了一些问题。
施放技能需要能够获知布林的属性,而布林本身又需要能够使用技能。这是一个环状依赖,无论把技能和布林两者中的哪一个放在前面,都无法通过编译。
这时,我们采用“内部类”的办法来解决。我们将“技能”的所有代码放置在“布林”类的内部。形如:
Code (c):
class bling_t{ //外层是布林。
private:
double power; //布林的私有量
public:
class bling_spell_t{ //内层是技能。
};
bling_spell_t smackthat; //布林和技能的关系:“布林有技能”。
bling_spell_t bsod;
bling_spell_t some_other_abilities;
};
内部类并不比其他普普通通的类具有更多的访问权限,所以内层的bling_spell_t仍然无法访问布林的私有量“power”(即使在某些编译器中能访问,也请千万不要尝试使用它)。
只是现在我们解决了循环依赖的问题。
布林和技能之间的关系叫做“布林有技能”,我们使用复合来描绘这种关系。
所谓复合,就是在一个类中,包含了另一个类的实例作为其成员。例如这里,布林类包含了技能类的三个实例作为成员:smackthat、bsod、some_other_abilities。
调整后的bling_t应该是这样:
Code (c):
class bling_t{
private:
double crit;
double strength;
double power;
static const double power_max;
time_t gcd_expire;
public:
double get_crit() const{ //各种方法在先,因为技能需要使用它们。
return crit;
}
double get_str() const{
return strength;
}
bool has_such_power(double p) const{
return power>=p;
}
bool is_gcd_expired() const{
return gcd_expire<=now();
}
void consume_power(double p){
power-=p;
if (power<0) power=0;
}
void resume_power(double p){
power+=p;
if (power>power_max) power=power_max;
}
void trigger_gcd(time_t duration){
gcd_expire=now()+duration;
}
class bling_spell_t{ //然后定义技能类
protected:
bling_t* bling_ptr;
private:
time_t cd;
time_t gcd;
time_t cd_expire;
double power_consume;
double power_resume;
public:
bling_spell_t(bling_t* ptr, time_t _cd, time_t _gcd, double _power_consume, double _power_resume) :
bling_ptr(ptr),
cd(_cd),
gcd(_gcd),
cd_expire(0),
power_consume(_power_consume),
power_resume(_power_resume)
{};
virtual ~bling_spell_t(){};
virtual bool execute(){
if (gcd>0&&!bling_ptr->is_gcd_expired()) return false;
if (cd>0&&cd_expire>now()) return false;
if (power_consume>0&&!bling_ptr->has_such_power(power_consume)) return false;
if (gcd>0) bling_ptr->trigger_gcd(gcd);
if (cd>0) cd_expire=now()+cd;
if (power_consume>0) bling_ptr->consume_power(power_consume);
if (power_resume>0) bling_ptr->resume_power(power_resume);
return true;
};
};
class bsod_t : public bling_spell_t{ //技能类的派生类
public:
bsod_t(bling_t* ptr) : bling_spell_t(ptr, 6000, 1500, 0, 50) {}
};
class smackthat_dot_exe_t : public bling_spell_t{ //派生类
public:
smackthat_dot_exe_t(bling_t* ptr) : bling_spell_t(ptr, 0, 1500, 30, 0) {}
virtual bool execute(){
if (!bling_spell_t::execute()) return false;
double d = 236.0;
d += bling_ptr->get_str();
double c = 0.0;
c += bling_ptr->get_crit();
if ( static_cast<double>(rand())/RAND_MAX < c )
d *= 2.0;
enemy().take_damage(d,DAMAGE_MECHANICAL);
return true;
};
};
bling_spell_t* smackthat_dot_exe; //布林有两个技能,
bling_spell_t* bsod; //我们使用指针类型,是为了方便在不销毁布林的情况下将技能状态重置。
bling_t(double str, double c) :
crit(c),
strength(str),
power(power_max),
gcd_expire(0){
smackthat_dot_exe = new smackthat_dot_exe_t(this); //在构造布林时,将两个技能也构造好。
bsod = new bsod_t(this);
}
~bling_t(){
delete smackthat_dot_exe; //在销毁布林时,将两个技能也销毁掉。
delete bsod;
}
};
我们使用new来构造一个实例,但千万记住一个原则:每次使用new,都必须有一个delete与之对应。
所有对象都有生命周期,有生,就有死;有构造,就有销毁;有new,就有delete。
如果你只负责不断地new、new、new,从来也不管delete,就会有许多本应被销毁掉的对象未被正确销毁,而积压在内存里。你会发现程序的内存占用一直在上涨、上涨、上涨……
这种现象称为“内存泄漏(memory leak)”,对一个模拟器来说,少量的内存泄漏或许影响并不大,但对一个需要长时间保持运行的系统而言,稍有内存泄漏,最终结果就将是内存被积压至满而崩溃。泄漏量大小都一样,只要泄漏了,剩下的就是崩溃时间早晚的问题。
为什么系统使用时间长了性能会下降,需要重启一下才能恢复,其实就是这个道理。你们如果在用大脚插件,有兴趣可以在游戏里观察一下大脚基本库“BigFoot”的内存占用,这就属于典型的内存泄漏,而且泄漏量还不小。在WoW中输入这个命令:
Code (c):
/run collectgarbage("collect");
这是lua语言的显示内存回收命令。使用它,lua可以找到大部分本应被销毁、但未被正确销毁的僵尸对象,然后将它们从内存中释放掉。你会发现每次大脚的内存占用升高了,使用这个命令,它的内存占用就会突然下降一截。大脚会在某些情况下自动调用它来清理自己泄漏掉的内存。
与怪兽一样,我们给布林也增加一个重置状态的方法respawn。
Code (c):
void respawn(){ //无参数的respawn
delete smackthat_dot_exe; //销毁旧技能
delete bsod;
power=power_max; //重设能量为满
gcd_expire=0; //重设公冷完成时间为0
smackthat_dot_exe = new smackthat_dot_exe_t(this); //重新构建新技能
bsod = new bsod_t(this);
}
void respawn(double str, double c){ //有参数的respawn重载
respawn(); //除了执行无参数respawn的工作以外,
strength=str; //将力量和暴击设置为新值。
crit=c;
}
制作一个布林作为模拟主人公
与怪兽一样,布林也需要实例化成为一个可用的对象。
我们这里仍然使用Singleton函数完成实例化。
Code (c):
bling_t& bling(){
static bling_t blington3k(500.0, 0.3);//布林顿3000,默认带有500力量和30%暴击。
return blington3k;
}