用游戏编程实例进行C++“多态”概念教学

时间:2022-10-19 08:01:50

用游戏编程实例进行C++“多态”概念教学

摘要:“多态”是面向对象程序设计方法中的重要概念,也是提高程序可扩充性的重要手段。然而初学面向对象编程的学生往往难以真正体会到其作用。文章介绍一个在教学中沿用多年,能够生动而充分地展示多态的作用,并在教学比赛中获奖的游戏编程教学案例,供大家参考。

关键词:多态;可扩充性;虚函数;抽象类

1问题的提出

面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。

而“多态”(Polymorphism),可以分为编译时的多态和运行时的多态。

编译时的多态,主要指的是运算符的重载和函数的重载。这部分内容,比较简单,易于理解,本文并不打算讨论。

运行时的多态,指的是以下机制(本文以后提到的“多态”,都指的是运行时的多态):

对于通过基类指针,调用基类和派生类中都有的同名、同参数表的虚函数这样的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果该基类指针指向的是一个基类对象,则基类的虚函数被执行,如果该基类指针指向的是一个派生类对象,则派生类的虚函数被执行(将上面表述中的“指针”换成“引用”,同样成立)。

多态可以简单地理解成同一条函数调用语句能调用不同的函数,或者说,对不同对象发送同一消息,使得不同对象有各自不同的行为。

多态在面向对象的程序设计语言中是如此重要,以至于有类和对象的概念,但是不支持多态的语言,只能被称作“基于对象的程序设计语言”,而不能被称为“面向对象的程序设计语言”。如Visual Basic就是“基于对象的程序设计语言”。

让学生掌握多态的语法规则并不难,难的是让他们深刻理解多态到底有什么用处。实际上,在面向对象的编程中使用多态,能够有效地提高程序的可扩充性,这就是多态最大的作用。

所谓一个程序的可扩性好,指的就是当该程序的功能需要增加或修改时,只需改动或增加比较少的代码就能实现。往往,一个程序员只有在编写了一定规模的程序,并且等到其程序真正需要添加新功能的时候,才能切身体会到程序的可扩充性是多么重要。那么,怎样才能让没有多少编程经历的低年级学生,不需要编写大规模的程序就能体会到多态在提高程序可扩充性方面的作用呢?这就是本文要探讨的问题。

2问题的现状

笔者查阅多本流行的c++教材,这些教材和讲义大多对多态提高程序的可扩充性这个作用未能充分展示。这些教材在阐述多态时,所举的例子一般都是这样的:

开设一个基类指针数组,该数组里的指针,有的指向基类对象,有的指向派生类对象。在此种情况下,遍历该数组,对每个数组元素,均通过它去调用基类和派生类里都有的同名虚函数,这就达到了在每个对象上都执行它自己的虚函数的目的[2]。例如,一个几何形体演示程序,有基类Shape,还有Rectangle,Triangle和Circle等Shape的派生类,这些类都有虚函数double Area()用以计算图形的面积。那么要计算所有几何图形的面积,只需用一个Shape * 类型的数组,存放所有几何图形对象的地址,然后遍历该数组,对每个元素(即类型为Shape * 的变量)均通过它去调用Area()虚函数,那么多态机制就能确保每个几何图形的面积都是用正确的Area()函数计算出来的[3]。

这样的例子,说明使用多态能够某种程度上精简程序的代码,但不能很好地说明多态在增强可扩充性方面的作用。比较好的例子应该是用多态和非多态的方法各写一段程序,然后要求对该程序进行功能上的扩充,此时再来看这两段程序各要做多大的改动――这才能够充分体现多态的优势。

笔者看到的教材里,只有一部采用了这样的写法[1]。该书举了一个异质链表(同一链表里存放不同类型的对象)的例子。该例子能够充分说明多态的优点,但是略显冗长,不够生动有趣,也不像实践中的例子。

那么软件开发的实践中,能否找到生动有趣而又不冗长的例子,来充分说明多态在程序可扩充性方面的作用呢?答案是肯定的,那就是到游戏开发中去寻找案例。

3问题的解决

游戏软件的开发,是最能体现面向对象设计方法的优势的。游戏中的人物、道具、建筑物、场景,都是很直观的对象,游戏运行的过程,就是这些对象相互作用的过程。每个对象都有自己的属性和方法,不同对象又可能有共同的属性和方法,特别适合使用继承、多态等面向对象的机制。而且,游戏本来就是学生所津津乐道的,在课堂的PPT里放几张游戏的截图,学生精神就会为之一振,兴趣大增。因此,笔者在讲述“多态”这一概念的时候,以“魔法门之英雄无敌”游戏的开发为例,充分论述了多态在提高程序可扩充性方面的作用,让同学们不但能学得明白,还能学得有趣。

“魔法门”游戏中有各种各样的怪物,如骑士、天使、狼,鬼,等等。每个怪物都有生命力、攻击力这两种属性。怪物能够互相攻击,一个怪物攻击另一个怪物时,会使被攻击者受伤;同时被攻击者会反击,使得攻击者也受伤。但是一个怪物反击的力量较弱,只是其自身攻击力的1/2。

怪物主动攻击、被敌人攻击和实施反击时都有相应的动作。比如骑士攻击时的动作就是挥舞宝剑,而火龙的攻击动作就是喷火;怪物受到攻击会嚎叫和受伤流血,如果受伤过重,生命力被减为0,则怪物就会倒地死去…….

针对这个游戏,教师提出的问题是:该如何编写程序,才能使得游戏版本升级,要增加新的怪物时,原有的程序改动尽可能少 。换句话说,就是怎样才能使程序的可扩充性更好。

显然,不论是否使用多态,均应使每种怪物都有一个类与之对应,每个怪物就是一个对象。而且,怪物的攻击、反击和受伤等动作,都是通过对象的成员函数实现的,因此为每个类都需要编写Attack、FightBack和 Hurted成员函数

Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的 Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。

Hurted函数减少自身生命值,并表现受伤动作。

FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。

接下来就是对比使用多态和不使用多态两种写法,来体现多态在提高程序可扩充性方面的作用。

先看不用多态的写法。假定用“CDragon”类表示火龙,用“CWolf”类表示狼,用“CGhost”类表示鬼,则“CDragon”类写法大致如下(其他类的写法也类似):

class CDragon

{

private:

int m_nPower ; //攻击力

int m_nLifeValue ; //生命值

public:

//攻击“狼”的成员函数

void Attack(CWolf * p);

//攻击“鬼”的成员函数

void Attack(CGhost * p);

//......其他Attack重载函数

//表现受伤的成员函数

void Hurted( int nPower);

//反击“狼”的成员函数

void FightBack(CWolf * p);

//反击“鬼”的成员函数

void FightBack(CGhost * p);

//......其他FightBack重载函数

};

接下来再看各成员函数的写法:

1. void CDragon::Attack(CWolf * p)

2. {

3. p->Hurted(m_nPower);

4. p->FightBack(this);

5. }

6. void CDragon::Attack(CGhost * p)

7. {

8. p->Hurted(m_nPower);

9. p->FightBack(this);

10. }

11. void CDragon::Hurted(int nPower)

12. {

13. m_nLifeValue -= nPower;

14. }

15. void CDragon::FightBack(CWolf * p)

16. {

17. p->Hurted(m_nPower/2);

18. }

19. void CDragon::FightBack(CGhost * p)

20. {

21. p->Hurted(m_nPower/2);

22. }

在上面带行号的程序中:

第1行,Attack函数的参数p,指向被攻击的CWolf对象。

第3行,在p所指向的对象上面执行Hurted成员函数,使被攻击的“狼”对象受伤。调用Hurted时,参数是攻击者“龙”对象的攻击力。

第4行,以指向攻击者自身的this指针为参数,调用被攻击者的FightBack成员函数,接受被攻击者的反击。

显然,在真实的游戏程序中,CDragon类的Attack成员函数中还应包含表现火龙在攻击时的动作和声音的代码。

第13行,一个对象的Hurted成员函数被调用会导致该对象的生命值减少,减少的量等于攻击者的攻击力。当然,真实的程序中,Hurted函数还应包含表现受伤时动作的代码,以及生命力如果减至小于等于零,则倒地死去的代码。

第17行,p指向的是实施攻击者,对攻击者进行反击,实际上就是调用攻击者的Hurted成员函数使其受伤。其受到的伤害的大小,等于实施反击者的攻击力的一半(反击的力量不如主动攻击大)。当然,FightBack函数中其实也应包含表现反击动作的代码。

实际上,如果游戏中有n种怪物,CDragon 类中就会有n个Attack成员函数,用于攻击n种怪物。当然,也会有n个FightBack成员函数(这里我们假设两条龙也能互相攻击)。对于其他类,比如CWolf等,也是这样

以上为非多态的实现方法。如果游戏版本升级,增加了新的怪物雷鸟,假设其类名为CThunderBird, 则程序需要做哪些改动呢?

显然,除了新写一个CThunderBird类外,所有的类都需要增加以下两个成员函数,用以对雷鸟实施攻击,以及在被雷鸟攻击时对其进行反击:

void Attack( CThunderBird * p) ;

void FightBack( CThunderBird * p) ;

这样,在怪物种类多的时候,工作量就较大。

实际上,非多态实现中,代码更精简的做法是将CDragon,CWolf等类的共同特点抽取出来,形成一个CCreature类,然后再从CCreature类派生出CDragon、CWolf等类。但是由于每种怪物进行攻击、反击和受伤时的表现动作不同,CDragon、CWolf这些类还是要实现各自的Hurted成员函数,以及一系列Attack、FightBack成员函数。所以只要没有利用多态机制,那么即便引入基类CCreature,对程序的可扩充性也无帮助。

下面再来看看,如果使用多态机制来编写这个程序,在要新增CThunderBird类的时候,程序改动有多大。

多态的写法如下:

设置一个抽象类CCreature,概括了所有怪物的共同特点。然后,所有具体的怪物类,比如CDragon,CWolf,CGhost等,均从CCreature类派生而来。

下面是CCreature类的写法:

class CCreature{

protected :

int m_nLifeValue, m_nPower;

public:

virtual void Attack( CCreature * p) = 0;

virtual void Hurted( int nPower) = 0;

virtual void FightBack( CCreature * p) = 0;

};

我们看到,在基类CCreature类中,只有一个Attack 成员函数,也只有一个FightBadk成员函数。

实际上,所有CCreature 的派生类也都只有一个Attack成员函数和一个FightBack成员函数。例如,CDragon类的写法如下:

class CDragon : public CCreature

{

public:

virtual void Attack( CCreature * p) {

p->Hurted( m_nPower);

p->FightBack( this );

}

virtual intHurted( int nPower){

m_nLifeValue -= nPower;

}

virtual int FightBack( CCreature * p){

p->Hurted(m_nPower/2);

}

};

在CDragon类的成员函数中,略去了表现动作和声音的那部分代码。其他类的写法和CDragon类的写法类似,只是实现动作和声音的代码不同。当然,如何实现动画的动作和声音,就不是本课要讲的内容了。

在上述多态的写法中,当需要增加新怪物“雷鸟”的时候,只需要编写新类CThunderBird即可,不需要在已有的类里专门为新怪物增加void Attack(CThunder

Bird * p)和void FightBack(CThunderBird * p)这两个成员函数,也就是说,其他类根本不用修改。这和前面非多态的实现方法相比,程序的可扩充性当然大大提高了。实际上,即便不考虑可扩充的问题,程序本身也比非多态的写法大大精简了。

还需要向学生阐述,为什么CDragon等类只需要一个Attack函数,就能够实现对所有怪物的攻击。

假定有以下代码:

1. CDragon Dragon;

2. CWolfWolf;

3. CGhost Ghost;

4. CThunderBird Bird;

5. Dragon.Attack( & Wolf);

6. Dragon.Attack( & Ghost);

7. Dragon.Attack( & Bird);

根据赋值兼容规则,上面的5、6、7三行里的参数,都与基类指针类型CCreature * 匹配的,所以编译没有问题。而根据多态的规则,从5、6、7三行进入到CDragon::Attack函数后,执行p->Hurted (m_nPower)语句,分别调用的就是CWolf::Hurted、CGhost::Hurted 和CBird::Hurted 了。

关于FightBack函数的情况,和Attack类似,不再赘述。

至此,多态对提高程序可扩充性的作用,在这个游戏编程的实例中,得到了生动而充分的展示。再辅以在本文第二节中提到的那种例子,同学们对于多态的理解,就会非常深刻。

4结语

本文中的游戏编程实例,连续多个学年在北京大学信息学院主干基础课“程序设计实习”(其包含C++内容)的课堂上讲述,收到了很好的效果。同学们纷纷反映,很希望C++的其他内容的授课,也能像“多态”这一节那样精彩。笔者以此部分内容参加了北京大学青年教师教学基本功和现代教育技术应用演示竞赛,获得理工类三等奖;参加北京市“计算机技术教育课堂教学交流”学术年会,获得三等奖。

由此笔者得到一个启发:游戏开发是面向对象的程序设计方法的典型应用,学生们又对游戏如此喜闻乐见,那么,在面向对象程序设计语言的教学中,多引入一些游戏相关的例子程序,一定能广受学生欢迎。笔者据此思想改造了C++讲义,并且设计了一个需要熟练运用面向对象的各种机制,才能高效完成的期末大作业,一道类似游戏的模拟题――魔兽世界,放在了北京大学在线程序评测系统POJ上。

参考文献:

[1] 宛延]. C++语言面向对象程序设计[M]. 北京:清华大学出版社,1998:168-190.

[2] 郑莉,董渊,张瑞丰. C++语言程序设计[M]. 3版. 北京:清华大学出版社,2004:274-280.

[3] Harvey M. Deitel,Paul James Deitel. C++大学教程[M]. 2版. 邱仲潘,等译. 北京:电子工业出版社,2001:425-431.

Teaching the "Polymorphism" Conception by a Sample of Game Programming

GUO Wei

(School of Electronic Engineering and Computer Science, Peking University, Beijing 100871, China)

Abstract: Polymorphism is a very important concept in object oriented programming, and it's also a very important method to improve the program extensibility. But it's a little bit hard for the beginner to really understand the advantage of polymorphism. This article introduces a game programming sample, which can demonstrate the advantage of polymorphism vividly. And this teaching case won awards in some teaching contests.

Key words: polymorphism; extensibility; virtual function; abstract class

上一篇:“编译原理”全英文授课模式探索 下一篇:计算机网络协议分析与开发实验的一种新教学方...