本篇文章我们着重讲解本系列课程final大作业的前置作业之二:魔兽世界之二:装备。
1. 前置知识
对于本题所需要的前置知识,大家需要对前几章的知识点有一定的认识,不太明白的童鞋,可以参考前几章编程习题的讲解文章:
- 运算符重载
- 继承
- 多态
- 输入输出和模板
- 标准模板库STL
2. 思路解析:数据结构
首先,推荐大家阅读上一篇习题的分析,循序渐进地构建这个final大作业的代码和思路。这题和前一题的区别主要是介绍了Warrior和Weapon的特点,但是为了给final大作业做铺垫,这里我把Weapon具体的特点也融合到本题之中:
sword的攻击力是使用者当前攻击力的20%(去尾取整)。
bomb的攻击力是使用者当前攻击力的40%(去尾取整),但是也会导致使用者受到攻击,对使用者的攻击力是对敌人取整后的攻击力的1/2(去尾取整)。Bomb一旦使用就没了。
arrow的攻击力是使用者当前攻击力的30%(去尾取整)。一个arrow用两次就没了。
所以如果只针对这题,没必要引入Weapon类,大家可以参考这个版本的代码:wow_weapons_submit_ver1.cpp。但为了更好的练习,文章后面都把Weapon的攻击力特点作为题目要求之一。总的来说,这题相比前一题来说,难度并不大,仅仅只是增加了Warrior持有Weapon的情况:
dragon 可以拥有一件武器。编号为n的dragon降生时即获得编号为 n%3 的武器。dragon还有“士气”这个属性,是个浮点数,其值为它降生后其司令部剩余生命元的数量除以造dragon所需的生命元数量。
ninja可以拥有两件武器。编号为n的ninja降生时即获得编号为 n%3 和 (n+1)%3的武器。
iceman有一件武器。编号为n的iceman降生时即获得编号为 n%3 的武器。
lion 有“忠诚度”这个属性,其值等于它降生后其司令部剩余生命元的数目。
wolf没特点。
请注意,在以后的题目里,武士的士气,生命值,忠诚度在其生存期间都可能发生变化,都有作用,武士手中的武器随着使用攻击力也会发生变化。
所以对比上题,我们首先需要新增Warrior类用来表示战士这个属性,其他具体的类,比如dragon,ninjia作为派生类继承Warrior类。另外就是Weapon类,同样作为所有武器的父类,其他武器类,比如sword都是其子类。到这里,我们分析了Warrior和Weapon类的继承关系,剩下的就是他们所需要存储的数据域。
对于Warrior类,它应该存储所有战士共有的成员属性:
- 编号 - id
- 类型 - t
- 战斗力 - p
- 生命值 - m
- 初始武器库 - w_init
对于每个Warrior的派生类,这里就不再赘述,大家可以通过源码来看看他们各自有哪些成员变量。相似地,对于Weapon类和其派生类,大家同样可以源码来进行理解如何构建它们的数据结构。这里需要注意一点,我们不需要存储每个武器的伤害,因为武器的伤害是根据使用者的攻击力进行实时计算的。
3. 代码解析:数据结构
首先,对于Commander和WorldWarcraft类,整体的数据结构没有太大变化,这里就skip了,详细情况大家可以看看源码。对于Warrior类,大家可以看看这里成员变量的类型,这里需要注意武器库存储的是Weapon的指针,而不是Weapon实例,后面我们会讲解为什么会这样做:
※ 为了方便理解整体的代码思路,本篇讲解的代码尽量和final的保持一致,或指出两者的不同之处,大家可以比对两个版本的源码来观察代码构建过程的取舍思路。
// 源码中还有一个额外的成员变量 - W_lib,这个是final战斗中才会用到,大家可以skip掉
class Warrior {
protected:
const size_t id; // ID
const Warrior_enum t; // Type
const size_t p; // power
size_t m; // Life points.
// https://www.geeksforgeeks.org/pointer-array-array-pointer/
Weapon* (*W_init) = nullptr; // Initialized weapons.
// 方法略
};
// 这里每个Warrior派生类的OUT_FORMAT是用来输出printf的字符串格式,比如dragon的:
// "It has a %s,and its m is %ld, p is %ld\n";
class Dragon: public Warrior {
const static char* OUT_FORMAT;
double morale;
// 方法略
};
class Ninjia: public Warrior {
const static char* OUT_FORMAT;
// 方法略
};
class Iceman: public Warrior {
const static char* OUT_FORMAT;
// 方法略
};
class Lion: public Warrior {
const static char* OUT_FORMAT;
size_t loyalty;
// 方法略
};
// 注意Wolf类没有任何成员变量,主要因为它没有初始武器,也没有特殊的数值单位
class Wolf: public Warrior {
// 方法略
};
而对于Weapon类的数据结构相对简单,这里也不再赘述:
// 这里注意武器耐久度的类型和其表示的范围,和相应的耐久度数值
// 注意这一版的源码有damage成员变量的,但是根据后续的题目,这个变量是不需要的
class Weapon {
protected:
const Weapon_enum t; // id
int durability; // -1 no durability, 0 broken, >= 1 normal.
// 方法略
};
class Sword : public Weapon {
// 方法略
};
class Bomb : public Weapon {
// 方法略
};
class Arrow : public Weapon {
// 方法略
};
3. 思路解析:算法/成员函数
我们首先来分析一下Weapon的成员函数,因为只需要实现一个功能:当Warrior使用某种武器时的伤害,根据之前题意的描述,我们只需要把Warrior类的攻击力当作参数传入伤害计算函数即可,就是需要注意计算误差,比如取攻击力的40%,不能写成 p * 0.4,需要用分数进行表示:p * 4 / 10.0,之后就是每种武器都有自己的伤害计算成员函数,所以Warrior类里面需要定义一个相关的虚函数,即下面的每个的派生类对其进行具体的实现。
分析完Weapon类需要的成员函数,接下来就是Warrior类所需要的成员函数。针对本题,Warrior类有两个特点:
- 每个Warrior生成时,会有初始武器库;
- 生成后,Warrior需要报告相关的属性和武器;
所以,在构造函数里面我们需要初始化武器库,然后也需要一个生成信息输出虚函数,之后每个派生类进行重写。
4. 代码解析:算法/成员函数
同样我们先来看看Weapon类的代码:
class Weapon {
// 成员变量略
public:
Weapon( Weapon_enum t_, size_t dur_ ) : t( t_ ), durability( dur_ ) {}
virtual ~Weapon() {}
// -------------------------------------------------------
// getter and setter
// -------------------------------------------------------
virtual size_t getDamage( size_t p_ ) = 0;
};
除了一些基本的构造和析构函数,我们看到这里定义了一个计算伤害的虚函数:
virtual size_t getDamage( size_t p_ ) = 0;
所以我们需要在派生类里面实现这个方法,比如sword:
class Sword : public Weapon {
public:
Sword() : Weapon( sword, -1 ) {}
size_t getDamage( size_t p_ ) override;
};
// sword的攻击力是使用者当前攻击力的20%(去尾取整)。
size_t Sword::getDamage( size_t p_ ) {
return std::floor( p_ * 2 / 10.0 );
}
分析完Weapon类的代码实现,接下来就是Warrior类:
class Warrior {
// 成员变量略
Weapon* get_weapon( Weapon_enum t_, size_t p_ );
public:
virtual ~Warrior() {
// 注意收回W_init分配的内存空间,防止内存溢出
delete [] W_init;
}
virtual void print() = 0;
};
同样,这里有输出生成信息的虚函数:
virtual void print() = 0;
那么同样地,派生类必须实现这个方法,同时我们需要在构造函数里面初始化武器库,比如dragon:
class Dragon: public Warrior {
const static char* OUT_FORMAT;
double morale;
public:
Dragon( size_t id_, size_t m_, size_t p_, size_t n_, size_t m_remained )
: Warrior( id_, dragon, p_, m_ ), morale( ( double ) m_remained / m_ ) {
// dragon 可以拥有一件武器。编号为n的dragon降生时即获得编号为 n%3 的武器。
// 分配一个长度的Weapon指针数组
W_init = new Weapon*[ 1 ];
// 获取n%3的武器,这里的get_weapon()大家可以看成一个API,传入需要武器的类型,获取该武器的一个实例对象
// 参数p_在final版源码是不需要的
W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % 3 ), p_ );
}
// 生成输出函数
void print() override;
};
const char* Dragon::OUT_FORMAT = "It has a %s,and it's morale is %.2f\n";
void Dragon::print() {
printf(
OUT_FORMAT,
WEAPON_NAMES[ W_init[ 0 ]->get_type() ],
morale
);
}
另外,Ninja使用bomb不会受到反噬这个机制,笔者选择在战斗逻辑里面实现,不过大家也可以放到Ninja类里面实现,就看大家怎么选择了。
5. 项目代码
※ 以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))。
※ 因为本题是课程final大作业的前置作业,所以最终代码会针对后面题目的要求进行修改。之所以这么做是让大家能从一个大项目的角度来看待整个作业,而不是仅仅是一个作业,所以我们会着重于模块化,可读性,可维护性,因此那种把整个代码放在一个文件的情况在这个作业是不会出现的。但是为了应对作业提交的要求(提交只能是一个文件),这里都会提供一个可供提交的版本,大家可以对比提交版本和最终版本的区别,看看我们究竟是从一个小项目一步步创建一个大型项目的,这个能力在后期大家项目中也是非常有用的能力之一。
※ 由于篇幅所限,这里只会给到Warrior和Weapon类、成员变量/函数的定义,具体实现请见github相关文件。
// -------------------------------------------------------
// Weapon class
// -------------------------------------------------------
class Weapon {
protected:
const Weapon_enum t; // Type id
int durability; // -1 no durability, 0 broken, >= 1 normal.
public:
Weapon( Weapon_enum t_, size_t dur_ ) : t( t_ ), durability( dur_ ) {}
virtual ~Weapon() {}
Weapon_enum getType() const;
// -------------------------------------------------------
// getter and setter
// -------------------------------------------------------
virtual size_t getDamage( size_t p_ ) = 0;
};
class Sword : public Weapon {
public:
Sword() : Weapon( sword, -1 ) {}
size_t getDamage( size_t p_ ) override;
};
class Bomb : public Weapon {
public:
Bomb() : Weapon( bomb, 1 ) {}
size_t getDamage( size_t p_ ) override;
};
class Arrow : public Weapon {
public:
Arrow() : Weapon( arrow, 2 ) {}
size_t getDamage( size_t p_ ) override;
};
// -------------------------------------------------------
// Warrior class
// -------------------------------------------------------
// https://en.cppreference.com/w/cpp/language/abstract_class
class Warrior {
protected:
const size_t id; // ID
const Warrior_enum t; // Type
const size_t p; // power
size_t m; // Life points.
// https://www.geeksforgeeks.org/pointer-array-array-pointer/
Weapon* (*W_init) = nullptr; // Initialized weapons.
Warrior( size_t id_, Warrior_enum t_, size_t p_, size_t m_ ) : id( id_ ), t( t_ ), p( p_ ), m( m_ ) {}
Weapon* get_weapon( Weapon_enum t_, size_t p_ );
public:
virtual ~Warrior() {
delete [] W_init;
}
virtual void print() = 0;
};
class Dragon: public Warrior {
const static char* OUT_FORMAT;
double morale;
public:
Dragon( size_t id_, size_t m_, size_t p_, size_t n_, size_t m_remained )
: Warrior( id_, dragon, p_, m_ ), morale( ( double ) m_remained / m_ ) {
W_init = new Weapon*[ 1 ];
W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % 3 ), p_ );
W_lib.push( W_init[ 0 ] );
}
void print() override;
};
class Ninjia: public Warrior {
const static char* OUT_FORMAT;
public:
Ninjia( size_t id_, size_t m_, size_t p_, size_t n_ )
: Warrior( id_, ninja, p_, m_ ) {
W_init = new Weapon*[ 2 ];
W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % WEAPON_NUM ), p_ );
W_init[ 1 ] = get_weapon( static_cast<Weapon_enum>( ( n_ + 1 ) % WEAPON_NUM ), p_ );
W_lib.push( W_init[ 0 ] );
W_lib.push( W_init[ 1 ] );
}
void print() override;
};
class Iceman: public Warrior {
const static char* OUT_FORMAT;
public:
Iceman( size_t id_, size_t m_, size_t p_, size_t n_ )
: Warrior( id_, iceman, p_, m_ ) {
W_init = new Weapon*[ 1 ];
W_init[ 0 ] = get_weapon( static_cast<Weapon_enum>( n_ % WEAPON_NUM ), p_ );
W_lib.push( W_init[ 0 ] );
}
void print() override;
};
class Lion: public Warrior {
const static char* OUT_FORMAT;
size_t loyalty;
public:
Lion( size_t id_, size_t m_, size_t p_, size_t m_remained )
: Warrior( id_, lion, p_, m_ ), loyalty( m_remained ) {}
void print() override;
};
class Wolf: public Warrior {
public:
Wolf( size_t id_, size_t m_, size_t p_ ) : Warrior( id_, wolf, p_, m_ ) {}
void print() override;
};
上一章:标准模板库STL(二)
下一章:魔兽世界之三:开战
6. 参考资料
- C++程序设计
- pixiv illustration: メイド服を着せられるお嬢様
7. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;