从零开始学AS3游戏开发【三】 向敌人开火!

注:本系列教程每周一篇,旨在引导刚刚接触FLASH的新手通过实例进行游戏开发的学习。在过程中逐步说明涉及到的类及对应的使用方法。从一个光秃秃的方块开始,根据不同的控制方式、玩法产生不同的分支,最终完善成一个个可玩的游戏。希望对各位入门的朋友有所帮助!在教程涉及的各种处理方法,可能不够完善,也希望各位高手指正:)

转载请注名来源于天地会

前篇勘误:gameScene.as的public function removeObject中,(48行)if (id != -1) return;应为if (id == -1) return;特此更正。请各位谅解。本篇源码已修正本错误。

第三篇 向敌人开火!

上一篇教程中,我们向游戏场景中增加了随机移动的3个敌人(当然,你也可以改成更多),同时,我们的角色也可以在场景中进行移动。但是,并没有产生攻击。在这次的教程中,我们将让自己的角色可以发射子弹,并攻击敌人。

为了实现这个功能,我们必须逐一实现以下内容:
1.构建子弹类
2.修改控制器,让自己可以射击
3.在子弹类中控制子弹的飞行
4.检测子弹是否击中敌人或飞出屏幕
5.新增敌人和自己的HP属性,当被子弹击中后,HP减少
6.敌人死亡

下面,我们来逐一进行实现。

0.一些必要的结构调整。

可以预见的,子弹的飞行方向应该与角色的移动方向相符。但在目前我们的程序中,ActionObject具备方向这个属性,但是它除了控制方向外,还肩负了是否产生运动的判断。(当walkDirection为0的时候,不执行move方法)。这样,当角色处于停止状态(walkDirection为0),我们就不好判断子弹的发射方向了。因此,必须进行一点小的修改。

在ActionObject中,把walkDirection的默认值改为1(向上),增加一个属性_isWalk

  1. /**
  2.                  * 移动方向
  3.                  */
  4.                 protected var walkDirection:uint = 1;
  5.                 
  6.                 /**
  7.                  * 是否移动
  8.                  */
  9.                 protected var _isWalk:Boolean = false;
  10.                 ...
  11.                 public function set isWalk(val:Boolean):void
  12.                 {
  13.                         _isWalk = val;
  14.                 }
复制代码

修改Do方法为根据_isWalk属性来判断是否移动

  1.                 /**
  2.                  * 覆盖父类的Do方法
  3.                  */
  4.                 override public function Do():void
  5.                 {
  6.                         if (_isWalk != 0) move();
  7.                         super.Do();
  8.                 }
复制代码

最后,修改KeyController的按键侦听函数:

  1.                 /**
  2.                  * 当按键按下时触发
  3.                  * @param        e
  4.                  */
  5.                 protected function onKeyDown(e:KeyboardEvent):void
  6.                 {
  7.                         var me:Player = _target as Player; // 将me修改为Player类,方便后面的射击实现
  8.                         switch(e.keyCode)
  9.                         {
  10.                                 case 38:
  11.                                         me.direction = ActionObject.UP;
  12.                                         me.isWalk = true;// 增加了这一句,将isWalk设置为true,进入行走状态
  13.                                         break;
  14.                                 case 40:
  15.                                         me.direction = ActionObject.DOWN;
  16.                                         me.isWalk = true;// 增加了这一句,将isWalk设置为true,进入行走状态
  17.                                         break;
  18.                                 case 37:
  19.                                         me.direction = ActionObject.LEFT;
  20.                                         me.isWalk = true;// 增加了这一句,将isWalk设置为true,进入行走状态
  21.                                         break;
  22.                                 case 39:
  23.                                         me.direction = ActionObject.RIGHT;
  24.                                         me.isWalk = true;// 增加了这一句,将isWalk设置为true,进入行走状态
  25.                                         break;
  26.                                 default:trace(e.keyCode); break;
  27.                         }
  28.                 }
  29.                 
  30.                 /**
  31.                  * 当按键弹起时触发
  32.                  * @param        e
  33.                  */
  34.                 protected function onKeyUp(e:KeyboardEvent):void
  35.                 {
  36.                         var me:ActionObject = _target as ActionObject;
  37.                         var active:Array = new Array(37,38,39,40);
  38.                         if (active.indexOf(e.keyCode) != -1)
  39.                         {
  40.                                 me.isWalk = false; // 增加了这一句,恢复isWalk为false
  41.                         }
  42.                 }
复制代码

同理,修改MonsterController的changeDir方法:

  1.                  /**
  2.                  * 随机修改方向
  3.                  */
  4.                 private function changeDir():void
  5.                 {
  6.                         var me:ActionObject = _target as ActionObject;
  7.                         me.direction = 1 + int(Math.random() * 4);
  8.                         me.isWalk = true; // 增加了这一句
  9.                 }
复制代码

1.构建子弹类。

首先简单分析一下子弹:它的计算比较简单,可以不需要控制器(当然,如果你喜欢,你可以给它加一个控制器),因此,我们直接从GameObject继承即可。子弹可以自己飞行,并会击中目标。我们需要在Do方法里增加飞行控制。子弹有外观,我们现在可以暂时用绘图函数来自己绘制。同时,我们不难想到,子弹的飞行是有方向的。而这个方向和角色面对的方向有关。在我们目前的代码中,ActionObject是具备方向属性的。最后,子弹必须可以准确击中敌人,而不是自己。因此,子弹有必要记录发射者是谁,方便识别敌友。

我们逐一来实现功能。首先,当然是构造函数,我们需要在子弹被声明的时候就确认它是由谁发射出来的,因此,子弹首先需要有发射人这个属性,并在构造函数中予以声明。同时,子弹在发出的瞬间,应该获得发射人的方向,之后,不管发射人如何运动,子弹的运动轨迹不受影响。最后,通过绘图函数绘制出子弹:

  1.                 /**
  2.                  * 子弹发射人
  3.                  */
  4.                 protected var _shooter:gameObject;
  5.                 /**
  6.                  * 子弹的飞行方向
  7.                  */
  8.                 protected var _direction:uint;
  9.                 public function BulletObject(shooter:gameObject) 
  10.                 {
  11.                         _shooter = shooter;
  12.                         _direction = _shooter.direction;
  13.                         graphics.beginFill(0x000000);// 用黑色开始填充
  14.                         graphics.drawCircle( -1, -1, 1);// 绘制一个半径为1的圆,为了保证圆心在正中,我们把圆从-1,-1这个坐标开始绘制
  15.                         graphics.endFill();// 结束绘制
  16.                 }
复制代码

graphics是一个Graphics类的实例。是Sprite的一个属性。我们可以通过他来绘制矢量图形。详细的方法可以参阅Graphics类的API文档

子弹的飞行需要速度,因此,子弹必须有速度这个属性,然后,通过发射人的方向来判断子弹的飞行方向,并做出处理:

  1.                  /**
  2.                  * 子弹飞行速度
  3.                  */
  4.                 protected var _speed:Number = 10.2;
  5.                  override public function Do():void
  6.                 {
  7.                         switch(_direction)
  8.                         {
  9.                                 case ActionObject.UP:
  10.                                         y -= _speed;
  11.                                         break;
  12.                                 case ActionObject.DOWN:
  13.                                         y += _speed;
  14.                                         break;
  15.                                 case ActionObject.LEFT:
  16.                                         x -= _speed;
  17.                                         break;
  18.                                 case ActionObject.RIGHT:
  19.                                         x += _speed;
  20.                                         break;
  21.                                 default:break;
  22.                         }
  23.                 }
复制代码

最后,子弹击中目标后,应该消失。因为我们通过graphics绘制了图形,所以,一个好的习惯就是在它消失前,把graphics清理掉

  1.                 override public function die():void
  2.                 {
  3.                         graphics.clear();
  4.                 }
复制代码

到这里,子弹类的结构就搭建好了。我们将在后面再根据实际的需求逐步对子弹类进行完善。

2.修改控制器,让自己可以射击

首先,我们要让角色可以射击。我们可以预知,除了自己可以射击以外,敌人也应该可以射击(虽然在本次的教程中,我们暂时只实现了自己的角色进行射击)。所以,“射击”这个动作应该是一个统一的标准。因此,我们通过接口来实现它。

下面,我们来定义射击接口。
新建一个包MyInterface,在文件夹上点右键,选择New Interface选项
1.jpg 

属性文件名IShoot.as,使用I作为接口的命名,是一个很好的习惯。以区分它和其他文件。

接口不实现任何方法,也不能包含任何属性。他只不过是一个标准的定制。射击接口的代码如下:

  1. package D5Power.MyInterface
  2. {
  3.         
  4.         /**
  5.          * 射击接口
  6.          * @author D5Power
  7.          */
  8.         public interface IShoot 
  9.         {
  10.                 /**
  11.                  * 射击方法
  12.                  */
  13.                 function Shoot():void;
  14.                 
  15.         }
  16. }
复制代码

在这个接口中,我们只规定了一个Shoot方法。随着游戏的扩展,可以丰富这个接口,让它去实现更多的功能。

标准定义好了,我们需要在实际操作中来让相关人等遵守这个标准。修改Player类:

  1. ...
  2. import D5Power.MyInterface.IShoot;
  3. ...
  4. public class Player extends FaceObject implements IShoot
复制代码

我们通过implement关键字,让Player类实现了IShoot接口。之后,必须实现接口所规定的全部标准(方法)。在IShoot接口中,我们只写了Shoot一个方法,因此,Player既然已经声明支持IShoot接口,它就必须要有Shoot方法,否则FLASH将提出严正抗议(编译报错。。。)

  1. ...
  2. public function Shoot():void
  3. {
  4. ...
  5. }
  6. ...
复制代码

只有在实际实现接口的类的对应方法中,才能有函数结构({}里的内容),并声明函数的作用域(public,protected,或其他)。而在接口的声明(IShoot.as)里,是不允许这样做的。关于接口的详细介绍,可以参考《Flash ActionScript 3 殿堂之路》的相关章节:)这里不再过多描述。

考虑一下Shoot方法的内容。无非就是声明一个新的子弹类,然后把它加到场景里面去,也就是说,需要使用gameScene.addObject方法,但是。我们目前没有任何位置可以访问到gameScene,因此,我们需要修改Global.as:

  1.                 /**
  2.                  * 直接调用游戏场景
  3.                  */
  4.                 public static var scene:gameScene;
复制代码

而把gameScene的构造函数,修改为以下内容:

  1. /**
  2.                  * 创建游戏基本场景需要传递基本舞台这个参数
  3.                  * @param        _stage        舞台
  4.                  */
  5.                 public function gameScene(_stage:Stage) 
  6.                 {
  7.                         Global.stage = _stage;
  8.                         Global.scene = this; // 增加了这句
  9.                         objectList = new Array();
  10.                         Global.stage.addEventListener(Event.ENTER_FRAME, render);
  11.                 }
复制代码

做了这两项准备后,我们可以编写Player的Shoot方法了:

  1.                 /**
  2.                  * 发射子弹
  3.                  */
  4.                 public function Shoot():void
  5.                 {
  6.                         var b:BulletObject = new BulletObject(this);
  7.                         b.x = x+width/2;
  8.                         b.y = y+height/2;
  9.                         Global.scene.addObject(b);
  10.                 }
复制代码

从代码中我们可以看到,我们声明了一个新的子弹,并把子弹移动到当前自己角色的坐标的位置(我们根据角色的宽度和高度调整了子弹的位置,你可以考虑一下,这是为什么),并向游戏场景添加了这个对象。

最后,我们只要修改控制器,当按下合适的按键时,触发这个射击方法就OK了。我们现在选择了空格作为发射键,ASCII码为32,因此,修改KeyController的onKeyDown代码:

  1. /**
  2.                  * 当按键按下时触发
  3.                  * @param        e
  4.                  */
  5.                 protected function onKeyDown(e:KeyboardEvent):void
  6.                 {
  7.                         var me:Player = _target as Player;
  8.                         switch(e.keyCode)
  9.                         {
  10.                                 case 38:
  11.                                         me.direction = ActionObject.UP;
  12.                                         me.isWalk = true;
  13.                                         break;
  14.                                 case 40:
  15.                                         me.direction = ActionObject.DOWN;
  16.                                         me.isWalk = true;
  17.                                         break;
  18.                                 case 37:
  19.                                         me.direction = ActionObject.LEFT;
  20.                                         me.isWalk = true;
  21.                                         break;
  22.                                 case 39:
  23.                                         me.direction = ActionObject.RIGHT;
  24.                                         me.isWalk = true;
  25.                                         break;
  26.                                 case 32:
  27.                                         // 空格射击
  28.                                         me.Shoot();
  29.                                         break;
  30.                                 default:trace(e.keyCode); break;
  31.                         }
  32.                 }
复制代码

发布测试一下,我们已经可以看到,在按下空格后,我们的角色在发射子弹了。不过,子弹不会击中敌人,超出屏幕范围后,也没有做对应的删除操作。OK,我们继续来实现其他功能。

3-4 子弹的飞行及碰撞检测

我们知道,在场景中的所有游戏对象都记录在对象列表gameScene.objectList中,因此,只要逐个判断是否与列表中的对象发生碰撞,就知道子弹击中哪个对象了。当然,不能击中自己,呵呵。在此之前,我们没有从外部调用过objectList,而它是一个protected的变量,从外部是不能访问的,因此,我们来写一个get方法来让外部可以对他进行读取。
  1.                 /**
  2.                  * 游戏对象列表
  3.                  */
  4.                 public function get AllObject():Array
  5.                 {
  6.                         return objectList;
  7.                 }
复制代码
可以考虑一下为什么不把objectList设置为public,这样就不用单独写方法了。这是因为,游戏场景必须严格的通过addObject和removeObject来进行游戏对象的增加与删除,如果objectList可以随意修改,那么就会出现对象漏删等情况,引起程序错误。因此,objectList对于外部来讲,是只读的,所以,必须通过protected或者private来对他进行保护,而只通过get函数来获取值,将它形成一个只读变量。

继续来实现碰撞检测的代码,可想而知,我们应该把检测代码写到子弹类的onDo函数里。这样在子弹飞行的时候可以随时判断他是不是发生了碰撞:
  1.                         for each(var obj:gameObject in Global.scene.AllObject)
  2.                         {
  3.                                 if (obj.hitTestPoint(x, y, true) && obj!=_shooter)
  4.                                 {
  5.                                         // 击中目标
  6.                                         // 这里编写伤害敌人的代码
  7.                                         break;
  8.                                 }
  9.                         }
复制代码
第一次接触到for each语句,来解释一下。for each即循环每一个,这条语句将逐个循环Global.scene.AllObject所返回的数组,并把数组内容转换为gameObject类型的变量obj,然后,我们就可以用obj来进行判断了。hitTestPoint是继承自Sprite的方法,作用是检测与一个点的碰撞情况。我们基本认为子弹就是一个点。x,y分别为点的坐标,而true则指定了,我们采用形状来进行碰撞检测,而不是外框。由于我们绘制的是一个正方形,因此外框和形状实际是一样的。但是在游戏中,我们可能应用到很多图形,这些不一定是规则的。如果希望对这些图形进行比较精确的碰撞检测,请把最后一个参数设置为true。当然,子弹不能击中自己,我们增加了一个条件:obj!=_shooter,检测目标不能是射击者自己。在循环的过程中,使用break;关键字,将导致跳出循环。当我们找到一个符合条件的对象,子弹显然不用再找其他目标了,因此,我们使用break跳出了循环。

另外,子弹飞出屏幕范围后应该被清除。因此,我们有必要判断子弹的目前坐标是否符合要求,代码比较简单:
  1. if (x<0 || x>Global.stage.stageWidth || y<0 || y>Global.stage.stageHeight) die();
复制代码
5-6 增加HP属性,并增加伤害方法直至死亡。

我们的需求写到了,给敌人和自己增加HP属性。或许你会想到,那就直接给gameObject增加HP属性就行了。然后它的子子孙孙就都有HP属性了。这样显然是不合理的。很简单的一个例子:子弹也是gameObject,但是子弹显然不应该有HP。我们选择了FaceObject来增加HP属性,你也可以选择ActionObject来增加。根据自己的实际情况来判断:)
  1.                 protected var _hp:uint = 100;
  2.                 /**
  3.                  * 获取HP
  4.                  */
  5.                 public function get HP():uint
  6.                 {
  7.                         return _hp;
  8.                 }
复制代码
另外,我们需要一个伤害方法,传递入一个伤害值,之后减少对应的HP。
  1. public function Hurt(val:uint):void
  2.                 {
  3.                         _hp -= val;
  4.                         if (_hp <= 0) die();
  5.                 }
复制代码
当HP的值小于等于0的时候,我们认为主角死亡了。同时,完善了一下ActionObject的die函数:
  1. override public function die():void
  2.                 {
  3.                         Global.scene.removeObject(this);
  4.                 }
复制代码
在死亡函数die中,我们移除了目前在使用的皮肤,同时从游戏场景的对象列表中,把自己删除。

最后,当然是把这些补充到我们刚才的“// 这里编写伤害敌人的代码”的位置。这段代码现在看起来应该是这样的:
  1.                         for each(var obj:gameObject in Global.scene.AllObject)
  2.                         {
  3.                                 if (obj.hitTestPoint(x, y, true) && obj!=_shooter && obj!=this)
  4.                                 {
  5.                                         // 击中目标
  6.                                         if (!obj.hasOwnProperty('Hurt')) continue;
  7.                                         (obj as FaceObject).Hurt(20);
  8.                                         die();
  9.                                         break;
  10.                                 }
  11.                         }
复制代码
首先,可以被击中的目标必须要有Hurt方法,如果没有,则跳过程序继续循环下一个(continue),之后,我们要把符合条件的obj转换为FaceObject,并调用Hurt方法产生20的伤害值。最后,子弹击中目标,应该被删除掉。

这里,我们的伤害值是固定的,如果你想通过道具或其他途径,改变伤害值。可以把伤害值写成一个属性,具体的实现方法各位可以自己去思考:)

现在,我们黑方块已经可以发射子弹攻击红色方块了。在下一篇教程中,我们将使敌方角色也发射子弹,并增加基地对象,并使敌人优先攻击基地。

本篇最终效果如下:
 main.swf (3.3 KB) 
本篇的最终源码如下:

Teach.rar (16.73 KB)


原文地址:https://www.cnblogs.com/keng333/p/2304956.html