伤害类型的区分
我们将“机械伤害”与普通伤害区分开。虽然Bling只会造成机械伤害,但万一Bling某一天升级了,学会了造成其他种类伤害的技能呢?
我们需要给模拟器列举Blingdas世界中存在的所有伤害类型。在C++中,我们使用enum(枚举)来实现这一工作。
我们新建一个伤害类型枚举,按照习俗约定,将其命名为“damage_type_e”。
Code (c):
enum damage_type_e{
//这里列举伤害类型。
}; //不要忘了加上最后这个分号。
由于Blingdas世界中只存在机械伤害和普通伤害,所以我们的枚举只需要列举两项,即“普通伤害DAMAGE_NORMAL”和“机械伤害DAMAGE_MECHANICAL”。
普通伤害用0表示,机械伤害用1表示。
Code (c):
enum damage_type_e{
DAMAGE_NORMAL=0,
DAMAGE_MECHANICAL=1,
DAMAGE_TYPE_TOTAL=2,
}; //千万不要忘了加上最后这个分号。
将这个DAMAGE_TYPE_TOTAL顺序加在枚举的末尾,你会发现它的值正好等于枚举中拥有的项目数量。我们以此获取Blingdas世界中伤害类型的总数。
如果未来Bling学会了电击,可以用2来表示“电击伤害”,扩展非常简单。
Code (c):
enum damage_type_e{
DAMAGE_NORMAL=0,
DAMAGE_MECHANICAL=1,
DAMAGE_ELECTRIC=2,
DAMAGE_TYPE_TOTAL=3,
};
枚举了三种伤害类型,DAMAGE_TYPE_TOTAL所代表的值自然而然也随着增加到了3。
怪兽Kaiju的实现
我们首先来描述怪兽Kaiju。如果你看过模拟器组成原理第一部,你会明白什么叫做“类”。这里我们将每一只怪兽之间的所有共性抽出来,作为一个“怪兽类”。
按照习俗约定,我们将怪兽类命名为“kaiju_t”。
Code (c):
class kaiju_t{
//这里写代码,描述kaiju所拥有的属性和方法。
}; //小心分号。
属性和方法,都有私有和公有之分。私有属性和方法只有kaiju自己可以看得见,而公有属性和方法可以被任何人看见。
在C++中,这样区分私有和公有:
Code (c):
class kaiju_t{
private:
//这里是私有的属性和方法
public:
//这里则是公有的
}; //切记不要忘了最后这个分号。
在这个模拟器中,kaiju需要拥有多少种属性?
唔,kaiju有最大血量、当前剩余血量和免伤系数三个属性。我们分别将它们命名为“health_max”、“health”和“damage_reduction_coeff”。
Code (c):
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
}; //灰谷第一原则:如果你不知道在一段代码的末尾该不该加分号,你就把它加上。
“double”是一种内置数据类型“双精度浮点数”。你可以把它简单理解为“可以带有小数点的数”。呃?什么?小数?血量不是整数吗?其实,魔兽世界里血量可以是小数。在Blingdas中也一样。
“static”意思是静态的,“const”意思是常量。在Blingdas中,每只怪兽对各种类型的伤害减免系数都一样,所以我们使用“static”;怪兽对各种类型的伤害减免系数不会变化,所以我们使用“const”。
如果你将某个量声明为static,编译器会在编译期为它划分好内存空间。这意味着无论你创造kaiju_t类的多少个实例,它占用的内存空间也总是只有一份,所有的kaiju_t实例都将共享同一个值:一个kaiju将它的值改变了,其他所有的kaiju都会受到影响。
如果你将某个量声明为const,任何尝试改变其值的行为都会被阻止。也就是说,带有const属性的量,其取值在整个运行期间永远不会改变(除非你刻意想办法钻空子去改变它)。
在一个量的名字后面加上中括号“[]”,可以将其声明为数组。中括号里的值,就是数组内包含的成员的数目。
这里DAMAGE_TYPE_TOTAL正好就是Blingdas世界中伤害类型的数目,所以每一种伤害类型都可以对应damage_reduction_coeff中的一个值。
由于damage_reduction_coeff是一个常量,所以我们必须为它赋初值。
Code (c):
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
};
//对单个量赋初值,形式类似:
// const double some_constant = 0.7;
//对数组赋初值,只需用大括号括起来,然后依次列出初值,用逗号分隔。
//在类中已经定义为static,在这里就不需要static了。
const double kaiju_t::damage_reduction_coeff[] = {
1.0, //DAMAGE_NORMAL
0.7, //DAMAGE_MECHANICAL
1.3, //DAMAGE_ELECTRIC
}; //这也需要分号。
由于某些编译器不允许在类的声明中给常量赋初值,所以我们将它移到了类声明的下方。
在类声明之外,需要使用“类名::”缀在名字之前,以表示这是来自类中的成员。例如这里使用kaiju_t::damage_reduction_coeff,表示这是来自“kaiju_t”类中的成员“damage_reduction_coeff”。
可能会有人有疑问,血量怎么会是私有属性呢?难道血量只有Kaiju自己才能看得见,不许Bling看吗?
这里,将血量声明为私有,并不是为了防止Bling看见它,而是要防止Bling修改它。
如果我们将血量声明为公有,Bling不仅可以看见它,还能随意修改它。想象一下你在玩游戏的时候可以随意修改敌人的现有血量……那可真是一件糟糕的事情。
Code (c):
//这是一段可怕的代码,不要把它加入你的模拟器中!
void bling_t::cheat() { //Bling要作弊了!
if (enemy.health>0) //如果敌人还活着……
enemy.health=0; //直接将其秒杀!哈哈哈!
}
为了阻止上面这段代码真的生效,我们必须将血量设置为私有,别无选择。
要让Bling能够获知敌人剩余血量,我们通过一个方法来实现。
Code (c):
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const { //公有方法,所以Bling可以调用它。
return health; //告诉调用者这只Kaiju现在还剩多少血量。
}
};
current_health方法被声明为const,所以它承诺不会对Kaiju的任何属性造成任何改动。
现在任由Bling如何调用这个方法,Bling都无法随意修改Kaiju的剩余血量了。
Code (c):
//这是一段可怕的代码,但是它已经被阻止了。
void bling_t::cheat() { //Bling要作弊了!
if (enemy.current_health()>0) //如果敌人还活着……
enemy.current_health()=0; //直接将其秒杀……
}
这个Bling的cheat()方法仍然可以执行,但是不会造成任何实际效果。
请千万小心!将方法声明为const并不代表着一切都安全了,因为那只是Kaiju自己承诺不去做任何改动,而Bling没有对此事做出任何承诺。
假如我们将current_health()方法的返回类型由“double”改为“double&”:
Code (c):
double& kaiju_t::current_health() const { //现在它返回的是一个“引用”而不是值。
return health; //告诉调用者这只Kaiju现在还剩多少血量。
}
void bling_t::cheat() { //Bling要作弊了!
if (enemy.current_health()>0) //如果敌人还活着……
enemy.current_health()=0; //直接将其秒杀!哈哈哈!
}
“引用”类型的返回,将本应是Kaiju才能看见的私有属性health,引用给了Bling,导致了私有属性发生对外泄露。
现在Bling意外获得了对私有属性health的控制权。这个cheat()方法又可以将怪兽秒杀掉了。
如果因为某些原因,你必须使用引用类型的返回,那么在返回类型前面也加上“const”,以明确阻止Bling的犯规行为。
Code (c):
const double& kaiju_t::current_health() const { //现在它返回的是一个不可改动的引用。
return health; //告诉调用者这只Kaiju现在还剩多少血量。
}
Kaiju除了会告诉别人它所剩的血量外,还会承受伤害。
所以“承受伤害”也是Kaiju的方法之一,虽然听起来感觉很苦逼。
Code (c):
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const {
return health;
}
void take_damage(double damage, damage_type_e type){ //承受伤害,需要告知伤害的大小和类型。
health -= damage * damage_reduction_coeff[type]; //根据类型不同伤害减免也不同,所以乘上不同的减免系数,再从当前生命值中扣除。
}
};
“-=”符号就是“扣除”的意思啦。
这写起来很简单对吧。
我们最后需要一个构造函数,一个析构函数。
构造函数是在每只Kaiju被实例化(你可以理解为被“创造”出来)时自动调用的函数。而析构函数则是每只Kaiju被销毁(你可以理解为“戏份结束谢幕”)时自动调用的函数。
如果你不写,编译器就会替你写一个标准形式的。
这里析构函数我们可以让编译器代劳,但是我们需要自己来写构造函数。
Code (c):
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const {
return health;
}
void take_damage(double damage, damage_type_e type){
health -= damage * damage_reduction_coeff[type];
}
kaiju_t(double h) : //构造函数的名字就是类名,没有返回。
health_max(h), //冒号后面花括号前面的内容称为“成员初值列”,
health(h) //你可以在此设定任意属性的值。
{} //这里构造函数只需要用到成员初值列,函数本体没有任何内容。
};
另外,C++有一个特性称为“函数重载”。我们可以将同名函数定义多个不同的形态,他们可以共存。
比如我们刚刚定义了一个带有参数“h”的构造函数,我们接下来还可以再定义一个不带任何输入参数的构造函数,以备懒得提供参数时使用。
Code (c):
static const double health_max_default = 100000.0; //定义一个静态常量“默认最大生命值”。
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const {
return health;
}
void take_damage(double damage, damage_type_e type){
health -= damage * damage_reduction_coeff[type];
}
kaiju_t(double h) : health_max(h), health(h) {} //刚才定义的构造函数。
kaiju_t() : health_max(health_max_default), health(health_max_default) {} //新的重载,不提供参数则使用默认值。
};
同样,对于普通函数,也可以进行重载。
例如我们写一个respawn方法,在不销毁不重新构建的情况下将其恢复到初始状态。
Code (c):
static const double health_max_default = 100000.0;
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const {
return health;
}
void take_damage(double damage, damage_type_e type){
health -= damage * damage_reduction_coeff[type];
}
kaiju_t(double h) : health_max(h), health(h) {}
kaiju_t() : health_max(health_max_default), health(health_max_default) {}
void respawn(){ //不带参数的respawn
health_max=health_max_default;
health=health_max_default;
}
void respawn(double h){ //带参数的respawn
health_max=h;
health=h;
}
};
这样我们就已经实现了Kaiju怪兽类所需的全部内容。
制造一个怪兽作为敌人
现在我们有了怪兽类,却没有怪兽实例。要使用这个怪兽类,我们需要将它实例化。
Code (c):
kaiju_t& enemy(){ //通过这个函数获取你的敌人。
static kaiju_t knifehead; //一个静态的、使用无参数构造函数构造的怪兽对象:Knifehead。
return knifehead; //将指向Knifehead的引用返回给调用者。
}
这个函数内部有一个被声明为static的怪兽对象Knifehead。
static确保了这个函数无论何时被调用,返回的都是指向同一只怪兽的引用。整个程序运行开始到结束,这只Knifehead既不会被销毁也不会被重复构造。
这样我们就制造了一只怪兽实例,并且可以在程序中随时随地调用它获取指向这只怪兽的引用,通过引用对它进行任意的操作。
这种手法在设计模式中被称为“Singleton模式”或者“单例模式”。如果你需要构造一个永不销毁、唯一、随时随地可以调用的对象,使用这种手法会非常方便。
小结
如果你一直跟着我在做,现在你手里的代码应该像这样:
Code (c):
enum damage_type_e{
DAMAGE_NORMAL=0,
DAMAGE_MECHANICAL=1,
DAMAGE_ELECTRIC=2,
DAMAGE_TYPE_TOTAL=3,
};
static const double health_max_default = 100000.0;
class kaiju_t{
private:
double health_max;
double health;
static const double damage_reduction_coeff[DAMAGE_TYPE_TOTAL];
public:
double current_health() const {
return health;
}
void take_damage(double damage, damage_type_e type){
health -= damage * damage_reduction_coeff[type];
}
kaiju_t(double h) : health_max(h), health(h) {}
kaiju_t() : health_max(health_max_default), health(health_max_default) {}
void respawn(){
health_max=health_max_default;
health=health_max_default;
}
void respawn(double h){
health_max=h;
health=h;
}
};
const double kaiju_t::damage_reduction_coeff[] = {
1.0, //DAMAGE_NORMAL
0.7, //DAMAGE_MECHANICAL
1.3, //DAMAGE_ELECTRIC
};
kaiju_t& enemy(){
static kaiju_t knifehead;
return knifehead;
}