在上一节中,我们介绍了如何构建我们小小的90度角RPG游戏的背景,在这一节中我将为列位带来重头戏部分,隆重介绍我们的主角及NPC登场,噔噔噔噔……掌声在哪里?!
额,没听到掌声,罢了,直接开场吧。
本章源码下载:www.iamsevent.com/zb_users/UPLOAD/AS3Coder5/AS3Coder5_src2.rar
构造人物
如果你仔细阅读过《Starling介绍》,那么在书中介绍MovieClip类的章节中作者提到过,Starling中的MovieClip不是容器,无法向其中添加子对象,那么要实现多状态的动画就必须使用setFrameTexture、addFrameAt等一系列MovieClip的API来做到动态更换帧。像我们这个小游戏中,人物存在上、下、左、右四个方向的动画状态,因此我不得不创建一个多状态的MovieClip类来实现之,我称之为MutiStateMovieClip,在介绍该类之前,先介绍一下与之配套的数据类。如果把动画的每个状态称作“关键帧”。下图显示了一个具有四个关键帧的动画:
我们知道,每一个关键帧都由一组纹理组成,那么我们就需要一个数据类来记录关键帧所需的信息,KeyFrame类就是为此而生:
package com.iamsevent.vo { import starling.textures.Texture; /** * KeyFrame对象为MutiStateMovieClip提供关键帧信息的存储 * @author S_eVent * */ public class KeyFrame { /** 关键帧名 */ public var name:String; /** 关键帧所用纹理序列 */ public var textures:Vector.<Texture>; public function KeyFrame(name:String=null, textures:Vector.<Texture>=null) { this.name = name; this.textures = textures; } } }
有了它之后我们就可以创建出MutiStateMovieClip类,该类的代码可能会有一些复杂,还请仔细揣摩:
package com.iamsevent.view { import com.iamsevent.manager.AssetswManager; import com.iamsevent.vo.KeyFrame; import starling.display.MovieClip; import starling.textures.Texture; /** * 多状态MovieClip。你可以通过setKeyFrame和gotoAndPlay方法来设置/跳转到某个关键帧 * @author S_eVent * */ public class MutiStateMovieClip extends MovieClip { /** 关键帧存放于此 */ protected var keyFrames:Vector.<KeyFrame> = new Vector.<KeyFrame>(); /** 当前播放关键帧 */ protected var currentKeyFrame:KeyFrame; /** * @param keyFrams 关键帧列表。MutiStateMovieClip将使用向量首个元素作为当前播放帧 * @param fps 帧频 */ public function MutiStateMovieClip(keyFrames:Vector.<KeyFrame>=null, fps:Number=12) { //默认外观 var defaultTextures:Vector.<Texture> = new Vector.<Texture>(); defaultTextures.push( AssetswManager.instance.getTexture("UnknownIcon") ); super(defaultTextures, fps); if( keyFrames ) { resetKeyFrames( keyFrames ); } } /** * 添加一个关键帧 * @param frame 欲添加的关键帧对象 * */ public function addKeyFrame(frame:KeyFrame):void { if( keyFrames.indexOf(frame) == -1 ) keyFrames.push(frame); } /** * 移除某帧 * @param frame 欲移除关键帧对象 * */ public function removeKeyFrame(frame:KeyFrame):void { var index:int = keyFrames.indexOf(frame); if( index != -1 ) { //若欲移除关键帧正在播放,则调整正在播放关键帧为移除帧前面那个关键帧 if( currentKeyFrame == frame ) { var successor:int = index - 1 < 0 ? index + 1 : index - 1; gotoAndPlay(keyFrames[successor].name); } keyFrames.splice(index, 1); } } /** 重置全部关键帧 */ public function resetKeyFrames(keyFrames:Vector.<KeyFrame>):void { this.keyFrames = keyFrames; currentKeyFrame = keyFrames[0]; changeTextures(currentKeyFrame.textures); } /** * 设置某个关键帧的动画 * @param frameName 关键帧名称 * @param textures 将替换的纹理序列 * */ public function setKeyFrameTexture(frameName:String, textures:Vector.<Texture>):KeyFrame { var frame:KeyFrame = getKeyFrameByName(frameName); if( frame ) { //若设置的是当前播放的关键帧,则立马更换纹理 if( currentKeyFrame == frame ) changeTextures(textures); frame.textures = textures; } return frame; } /** 根据名字查找关键帧 */ public function getKeyFrameByName(frameName:String):KeyFrame { for each(var frame:KeyFrame in keyFrames) { if( frame.name == frameName ) return frame; } return null; } /** * 跳到某个关键帧 * @param frameName 关键帧名称 */ public function gotoAndPlay(frameName:String):void { var frame:KeyFrame = getKeyFrameByName(frameName); if( frame && currentKeyFrame != frame ) { changeTextures( frame.textures ); currentKeyFrame = frame; } else { trace("指定名称的帧不存在或者当前播放帧已是指定帧"); } } /** 改变当前MovieClip所用动画纹理序列 */ protected function changeTextures(texturs:Vector.<Texture>):void { stop(); var len:int = texturs.length; for(var i:int=0; i<len; i++) { if( i < numFrames ) { texture = texturs[i];//立马改变纹理 width = texture.width; height = texture.height; setFrameTexture(i, texturs[i]);//改变动画中该帧纹理 } else { addFrameAt(i, texturs[i]); } } //始终保持注册点为中心 pivotX = this.width >> 1; pivotY = this.height >> 1; play(); } } }
在构造函数中,若是一开始没有传入keyFrames参数,则MutiStateMovieClip对象则会取AssetManager中嵌入的默认图片资源作为默认外观,你可以修改AssetManager的代码来更改此默认图片。当在构造函数中传入keyFrames参数或在任何时候调用resetKeyFrames方法都会重设全部的关键帧,即改变keyFrames这个关键帧列表及currentKeyFrame变量的值。之后,MutiStateMovieClip会使用currentKeyFrame变量的textures属性去调用changeTextures方法来产生其当前播放动画的实际外观改变。
我们的游戏中,所有角色都拥有四个方向的动画,即四个关键帧。若是直接让角色视图类使用MutiStateMovieClip类的话,用起来感觉还是有点麻烦。为此,我又创建了一个RoleAppearance类来将角色四个方向的动画改变逻辑封装起来,这样在外部使用起来就会显得非常方便了。
/** * 角色外观类。它带有四个方向的行动动画 * @author S_eVent */ public class RoleAppearance extends MutiStateMovieClip { private var _roleFrames:Object = {}; private var _currentDirection:String; public function RoleAppearance(fps:Number=12) { super(null, fps); } /** * 设置某个方向上所用的动画 * @param direction 欲设置的角色方向。可选值为RoleState类中定义的以DIRECTION开头的四个常量 * @param textures 组成动画的纹理序列 * */ public function setDirectionMC( direction:String, textures:Vector.<Texture> ):void { if( direction != RoleState.DIRECTION_DOWN && direction != RoleState.DIRECTION_UP && direction != RoleState.DIRECTION_LEFT && direction != RoleState.DIRECTION_RIGHT ) { trace("setDirectionMC方法direction参数不合法!"); } else { var frame:KeyFrame = setKeyFrameTexture(direction, textures); // 若setKeyFrame返回值为空,则表示该对象中没有direction指定名字的关键 // 帧,此时需要创建一个关键帧并插入 if( !frame ) { addKeyFrame( new KeyFrame(direction, textures) ); } } } /** 调整人物朝向。 * @param direction 欲设置的朝向。可选值为RoleState类中定义的以DIRECTION开头的四个常量*/ public function turnDirection(direction:String):void { if( direction == _currentDirection )return; if( direction != RoleState.DIRECTION_DOWN && direction != RoleState.DIRECTION_UP && direction != RoleState.DIRECTION_LEFT && direction != RoleState.DIRECTION_RIGHT ) { trace("setDirectionMC方法direction参数不合法!"); } else { gotoAndPlay(direction); _currentDirection = direction; } } /** 重置四方向外观 */ public function resetTextrues(downFrame:Vector.<Texture>, leftFrame:Vector.<Texture>, rightFrame:Vector.<Texture>, upFrame:Vector.<Texture>):void { var frames:Vector.<KeyFrame> = new Vector.<KeyFrame>(); frames[0] = new KeyFrame(RoleState.DIRECTION_DOWN, downFrame); frames[1] = new KeyFrame(RoleState.DIRECTION_LEFT, leftFrame); frames[2] = new KeyFrame(RoleState.DIRECTION_RIGHT, rightFrame); frames[3] = new KeyFrame(RoleState.DIRECTION_UP, upFrame); resetKeyFrames(frames); _currentDirection = RoleState.DIRECTION_DOWN; } }
出现在上述代码中的RoleState类中定义了代表角色四方向的共有常量:
package com.iamsevent.view { /** * RoleState定义了RoleAppearence的状态信息定义 * @author S_eVent * */ public class RoleState { /** 角色面朝上的动画状态 */ public static const DIRECTION_UP:String = "directionUp"; /** 角色面朝下的动画状态 */ public static const DIRECTION_DOWN:String = "directionDown"; /** 角色面朝左的动画状态 */ public static const DIRECTION_LEFT:String = "directionLeft"; /** 角色面朝右的动画状态 */ public static const DIRECTION_RIGHT:String = "directionRight"; } }
有了RoleAppearance类之后,我们就可以使用其提供的几个API来很轻松地改变一个角色的四方向动画了。但是RoleAppearance类终究只是一个MovieClip的子类罢了,它不是容器,这就决定了它无法担任除播放动画之外的其他任务了。因此我们还需要创建一个继承自Sprite的容器类来负责将角色动画、名字等东西集中起来的类。在该案例中,我创建了RoleView类来扮演此角色,在该类里面,除了负责将所有必需的显示对象集成起来之外,还负责人物移动等功能的实现。按照我以往的习惯,总会安排一个VO数据对象类来与View视图类进行配套,为此我创建了RoleVO类:
package com.iamsevent.vo { /** * RoleVO为RoleView视图对象及其子类提供数据存储 * @author S_eVent * */ public class RoleVO { /** 角色名称 */ public var name:String; /** 角色外观素材。格式应保持为0_1_2_3这样,下划线分隔了角色下、左、右、上方向所使用的素材的组序号 */ public var textureString:String; /** 角色运动速度。单位:像素/秒 */ public var speed:Number; /** 拷贝一个副本 */ public function clone():RoleVO { var c:RoleVO = new RoleVO(); c.name = this.name; c.textureString = this.textureString; c.speed = this.speed; return c; } } }
接下来一起看RoleView的全部代码:
/** * RoleView用于显示人物角色 * @author S_eVent * */ public class RoleView extends Sprite { /** 人物当前是否正在移动。设置此标记为true是让人物产生移动的前提条件 */ public var isWalking:Boolean = false; /** 当前运动方向 */ public var currentDirection:String; /** 可行走区域 */ public var walkableArea:Rectangle; /** 当抵达目的地后按原路返回。若此属性为true,则在调用walkTo方法时,若已抵达目的地,则会按原路返回。默认值为false。 */ public var backWhenArrive:Boolean = false; /** 该类角色所用素材所在图片名 */ public var textureAtlasName:String = Config.ROLE_TEXTURE_IMAGE_NAME; private var _juggler:Juggler; private var _roleVO:RoleVO = new RoleVO(); private var _path:Vector.<Point>; /** 当前已运动到的路径节点索引 */ private var _currentPosition:uint = 0; /** 当backWhenArrive为true时,此属性用以指示当前行走的方向为顺序还是逆序 */ private var _reverse:Boolean = false; private var _appearence:RoleAppearance;//外观 private var _nameTF:TextField;//姓名 /** * * @param fps 帧频 * @param juggler 该类角色所用Juggler对象,若不设置juggler属性,则角色外观动画不会播放 * */ public function RoleView(fps:Number=12, juggler:Juggler=null) { super(); _appearence = new RoleAppearance(fps); addChild(_appearence); this.juggler = juggler; _nameTF = new TextField(10,20, ""); _nameTF.autoScale = true; addChild(_nameTF); } /** * 行走至某处。请在调用此方法前先设置path属性 * @param time 经过时间 * @param autoChangeDirection 是否开启自动调整方向功能。默认为true * */ public function walkTo(time:Number, autoChangeDirection:Boolean=true):void { if( !roleVO )return; if( !path || path.length == 0 ) { trace("未设置路径,无法行走"); return; } var currentDestination:Point = path[_currentPosition]; var distX:Number = currentDestination.x - this.x; var distY:Number = currentDestination.y - this.y; var actualSpeed:Number = roleVO.speed * time; var isArrival:Boolean = Math.sqrt(distX * distX + distY * distY) < actualSpeed; if( isArrival ) { //到达后,若接下来还有路径节点要走,则以下一个节点作为目的地行进 if( (_reverse && _currentPosition > 0) || (!_reverse && _currentPosition < path.length - 1) ) { _currentPosition = _reverse ? _currentPosition - 1 : _currentPosition + 1; walkTo(time); } //若没有路要走,则派发到达事件 else { this.x = currentDestination.x; this.y = currentDestination.y; if( backWhenArrive ) { _reverse = !_reverse;//反置标记 } else { stopWalk(); } dispatchEvent(new GameEvent(GameEvent.MOVE_ARRIVAL, true)); } } else { var angle:Number = Math.atan2(distY, distX); var speedX:Number = Math.cos(angle) * actualSpeed; var speedY:Number = Math.sin(angle) * actualSpeed; this.x += speedX; this.y += speedY; if( autoChangeDirection ) { //判断与目的地的横距离是否大于纵距离 var xGTy:Boolean = Math.abs(distX) >= Math.abs(distY); var direction:String; //若横距离大于纵距离,一定是朝向左或右了 if( xGTy ) { direction = distX > 0 ? RoleState.DIRECTION_RIGHT : RoleState.DIRECTION_LEFT; } //否则,朝向上或下 else { direction = distY > 0 ? RoleState.DIRECTION_DOWN : RoleState.DIRECTION_UP; } _appearence.turnDirection(direction); } } } /** 向某方向一直走。请在调用此方法前设置currentDirection,如要限制行走范围,则设置walkableArea * @param time 经过时间 * @return 角色移动的距离*/ public function walkToward( time:Number ):Point { var moveDistance:Point = new Point(); if( roleVO == null || currentDirection == null )return moveDistance ; var speedX:Number = 0;//实际横坐标速度 var speedY:Number = 0;//实际纵坐标速度 var actualSpeed:Number = roleVO.speed * time;//实际速度 switch(currentDirection) { case RoleState.DIRECTION_DOWN: speedY = actualSpeed; break; case RoleState.DIRECTION_LEFT: speedX = -actualSpeed; break; case RoleState.DIRECTION_RIGHT: speedX = actualSpeed; break; case RoleState.DIRECTION_UP: speedY = -actualSpeed; break; default: trace("[RoleView.walkToward参数direction不符合规范]"); return moveDistance; } this.x += speedX; this.y += speedY; _appearence.turnDirection( currentDirection ); moveDistance.x = speedX; moveDistance.y = speedY; if( walkableArea != null ) { if( this.x < walkableArea.x ) { this.x = walkableArea.x; moveDistance.x = 0; stopWalk(); } else if( this.x > walkableArea.right ) { this.x = walkableArea.right; moveDistance.x = 0; stopWalk(); } if( this.y < walkableArea.y ) { this.y = walkableArea.y; moveDistance.y = 0; stopWalk(); } else if( this.y > walkableArea.bottom ) { this.y = walkableArea.bottom; moveDistance.y = 0; stopWalk(); } } return moveDistance; } /** 停止行走 */ public function stopWalk():void { isWalking = false; } /** 当设置roleVO时,会调用此方法。默认行为是以roleVO.textureSting设置角色四方向动画。 * 如想改变默认行为,则可以重载此方法 */ protected function onRoleVOChanged( ):void { //更新外观 if( roleVO.textureString ) { var t:Vector.<Vector.<Texture>> = AssetswManager.instance.getTexturesByString( textureAtlasName, roleVO.textureString ); if( t.length < 4 ) { trace("textureString提供帧数不足4"); } else { _appearence.resetTextrues(t[0], t[1], t[2], t[3]); } _appearence.x = -_appearence.width >> 1; _appearence.y = -_appearence.height >> 1; } //更新姓名 if( roleVO.name ) { _nameTF.text = roleVO.name; _nameTF.width = _appearence.width * 2; _nameTF.x = -_nameTF.width >> 1; _nameTF.y = _appearence.y - _appearence.height / 2 - _nameTF.height; } } //------------------------------------------get / set functions-------------------------------------// /** RoleBase对象配套VO */ public function get roleVO():RoleVO { return _roleVO; } public function set roleVO(value:RoleVO):void { if( _roleVO != value ) { _roleVO = value; onRoleVOChanged( ); } } /** 设置角色运动路径 */ public function get path():Vector.<Point> { return _path; } public function set path(value:Vector.<Point>):void { _path = value; _currentPosition = 0;//重置当前已运动到的路径节点索引 } /** 负责调度RoleView角色外观动画的Juggler对象 */ public function get juggler():Juggler { return _juggler; } public function set juggler(value:Juggler):void { if( _juggler != value ) { _juggler = value; if( _juggler ) _juggler.add(_appearence); } } }
roleVO:当设置一个roleView对象的roleVO后,会根据该RoleView对象的textureAtlasName属性及roleVO的textureString属性去获取对应的外观资源,前者指定了素材取自于哪一张SpriteSheet图片,后者指定了动画的各关键帧(对于角色对象来说,它有四个关键帧,分别代表上、下、左、右方向行走动画)所在位置。我将角色所有可用的SpriteSheet图片名存放在Config类中,该类中还可以存放很多全局的配置常量,如该游戏的版本号,所用语言包等等:
package { /** * Config保存了游戏的配置信息 * @author S_eVent * */ public class Config { /** 人物角色所用素材取自于的图片名字 */ public static const ROLE_TEXTURE_IMAGE_NAME:String = "Role"; /** NPC所用素材取自于的图片名字 */ public static const NPC_TEXTURE_IMAGE_NAME:String = "NPC"; } }
通过对roleVO属性的设置还可以导致RoleView对象中姓名文本的改变,由于在构造函数中将textfield对象的autoScale属性设为了true,所以不必担心姓名文字出现显示不完全的情况。
juggler:我们知道,用来显示角色外观的对象_appearence是一个RoleAppearance类型的对象,它是由MovieClip继承过来的,因此要让它播放必须得将其添加到一个juggler对象中去。为了节省资源,我不会为每个RoleView对象都单独创建一个juggler对象,而是在所有RoleView的共有父类(在本例中是一个World对象)中使用唯一的一个juggler对象来统一管理这些RoleView的动画。你可以在构造函数中传入juggler参数或在任意时间使用RoleVIew的juggler属性进行设置。
walk:使用walkTo或walkToward方法可以让角色产生移动,前者可以让角色沿着固定的路径走,若设置其autoChangeDirection参数为true(默认值也为true),则角色在行走过程中会根据下一个目的地相对于其的位置进行方向调整。而且如果你设置了backWhenArrive属性为true,则角色在运动到路径的终点后会原路返回,如此往返。我将用此方法来控制NPC的往返运动。后者则用在控制主角运动上面,按下键盘方向键时主角会沿着一个方向一直走,在调用此方法前,需要设置currentDirection来决定人物行走的方向,如果有需要,可以设置walkableArea属性来约束人物的行走范围。这两个方法中的具体实现逻辑我在此就不分析了。不过这两个行走方法都是基于时间,而非帧,如果简简单单地通过侦听一个ENTER_FRAME事件来让人物移动的话会造成不同步的情况,就是帧频越高人物移动越快。我不希望发生这种事,我只希望在调高帧频的时候仅仅是让动画的播放更加流畅而已,而不应该让人物移动速度有所变化。列位道友只需要记住,在Starling中使用时间来控制动画的播放非常方便,我们只需要侦听EnterFrameEvent.ENTER_FRAME事件,然后在事件处理函数中使用EnterFrameEvent对象的passedTime属性来作为时间参数即可,该属性记录着当前帧与上一帧间隔的时间。用此参数传入到所有PanelBase极其子类对象的advancedTime方法中即可。
出现吧,凡人们!
OK,OK,OK,准备工作已经就绪,接下来就是fuck up的时刻了。我们将利用RoleView类来进行主角及NPC的创建。首先,自然是在AssetManager中嵌入所有需要用到的资源:
public class AssetswManager { //-----------------------------------------默认图标-----------------------------------------------------// [Embed(source="/assets/character/unknown.jpg")] public var UnknownIcon:Class; //-----------------------------------------角色素材-----------------------------------------------------// [Embed(source="/assets/character/Role.png")] public var RoleTexture:Class; [Embed(source="/assets/data/Role.xml", mimeType="application/octet-stream")] public var RoleData:Class; …… //-----------------------------------------NPC素材-----------------------------------------------------// [Embed(source="/assets/character/NPC.png")] public var NPCTexture:Class; [Embed(source="/assets/data/NPC.xml", mimeType="application/octet-stream")] public var NPCData:Class; [Embed(source="/assets/data/NPCInfo.xml", mimeType="application/octet-stream")] public var NPCInfo:Class; private var _textureAtlasFactory:Object = {};//纹理集缓存 private var _npcInfo:XML;//NPC描述信息 private static var _instance:AssetswManager; public function AssetswManager(s:Singleton) { _npcInfo = XML( new NPCInfo() ); } public static function get instance():AssetswManager { if( _instance == null ) _instance = new AssetswManager(new Singleton()); return _instance; } …… /** * 根据一个ID来获取NPC信息 * @param id * @return * */ public function getNPCByID( id:String ):RoleVO { var result:RoleVO; var x:XMLList = _npcInfo.npc.(@id == id); if( x.length() > 0 ) { var roleXML:XML = x[0]; result = new RoleVO(); result.textureString = roleXML.@texture; } return result; } }
细心的道友应该发现了这里出现了一个不速之客:NPCInfo,这是一个XML对象。事实上,为了生成随机的NPC,我把每个NPC所要用到的纹理素材及ID都定义在了一个NPCInfo.xml文件中了,一起来看下:
<?xml version="1.0" encoding="UTF-8"?>
<!--此配置表设置了全部的防御塔信息-->
<npcInfo>
<!--
texture:素材。下划线分隔了怪物下、左、右、上方向所使用的素材的组序号
id:NPC外观ID
-->
<npc texture="0_4_8_12" id="0"/>
<npc texture="1_5_9_13" id="1"/>
<npc texture="2_6_10_14" id="2"/>
<npc texture="3_7_11_15" id="3"/>
<npc texture="16_20_24_28" id="4"/>
<npc texture="17_21_25_29" id="5"/>
<npc texture="18_22_26_30" id="6"/>
<npc texture="19_23_27_31" id="7"/>
</npcInfo>
我将此XML嵌在AssetManager中,并创建getNPCByID方法,之后我就可以随机出一个0-7的数字,然后将此数字传入getNPCByID中来获得一个NPC的数据。
一切就绪之后,我们把创建主角及NPC的工作在Wolrd类中实现:
public class World extends PanelBase { private var _sceneXML:XML;//场景信息 private var _sceneWidth:Number;//场景宽度 private var _sceneHeight:Number;//场景高度 private var _background:BackgroundImage;//背景 private var _player:RoleView;//主角 private var _npcCount:int = 100; private var _npcList:Vector.<RoleView> = new Vector.<RoleView>(); public function World() { } /** * 进入新的一个场景 * @param sceneXML 场景信息XML * */ public function enterScene( sceneXML:XML ):void { _sceneXML = sceneXML; _sceneWidth = _sceneXML.sceneWidth; _sceneHeight = _sceneXML.sceneHeight; //设置背景 var groundTexture:Texture = AssetswManager.instance.getMCTextures("Tile", _sceneXML.groundImage)[0]; if( !_background ) _background = new BackgroundImage(groundTexture, _sceneWidth, _sceneHeight); else _background.redraw(groundTexture, _sceneWidth, _sceneHeight); if( !this.contains(_background) ) addChild( _background ); //设置主角 if( !_player ) _player = new RoleView(10, juggler); var vo:RoleVO = new RoleVO(); vo.name = "小臭脚"; vo.speed = 100; vo.textureString = "0_4_8_12"; _player.roleVO = vo; _player.walkableArea = new Rectangle(0, 0, _sceneWidth, _sceneHeight); _player.x = _player.y = 200; if( !this.contains(_player) ) addChild(_player); //设置NPC for each(var view:RoleView in _npcList) { view.removeFromParent(true); } _npcList = new Vector.<RoleView>(); var randomID:String; for(var i:int=0; i<_npcCount; i++) { randomID = int(Math.random() * 8).toString(); vo = AssetswManager.instance.getNPCByID(randomID); vo.speed = 30 + Math.random() * 50;//速度在30-80之间随机一个值 view = new RoleView(10, juggler); view.textureAtlasName = Config.NPC_TEXTURE_IMAGE_NAME; view.roleVO = vo; _npcList[i] = view; view.x = Math.random() * _sceneWidth; view.y = Math.random() * _sceneHeight; view.backWhenArrive = true; view.path = generateRandomPath(view.x, view.y, 2); addChild(view); } } override public function advanceTime(time:Number):void { super.advanceTime(time); if( _player.isWalking ) { //移动主角 var moveDistance:Point = _player.walkToward( time ); //判断场景是否需要移动 var halfW:Number = stage.stageWidth / 2; var layerMove:Boolean = _player.x > halfW && _player.x < _sceneWidth - halfW; if( layerMove ) { this.x -= moveDistance.x; } } for each(var npc:RoleView in _npcList) { npc.walkTo( time ); } } /** * 让主角开始行走 * @param direction 行走方向 * */ public function startMoving(direction:String):void { _player.currentDirection = direction; _player.isWalking = true; } /** * 让主角停止行走 * */ public function stopMoving():void { _player.stopWalk(); } /** * 生成随机路径 * @param x 路径起点x坐标 * @param y 路径起点y坐标 * @param pathLength 路径长度(包含起点) * @return * */ private function generateRandomPath(x:Number, y:Number, pathLength:int):Vector.<Point> { var randomPath:Vector.<Point> = new Vector.<Point>(); var node:Point = new Point(); node.x = x; node.y = y; randomPath[0] = node; var w:Number = _sceneWidth; var h:Number = _sceneHeight; for(var i:int=1; i<pathLength; i++) { node = new Point(); node.x = Math.random() * w; node.y = Math.random() * h; randomPath[i] = node; } return randomPath; } }
主角的信息我暂时写死,你也可以抽出主角信息将其放入一个XML文件中。NPC的外观、速度、路径都是随机的,并设置其backWhenArrive属性为true,让他们能够往返地不间断地走动。注意到我在World类中重载了advancedTime方法,让我们的World类可以在调度动画播放的同时实现所有角色的行走逻辑。主角的走动有时会带动场景的移动,这一部分的原理可参考我的【AS3 Coder】任务一:制作2DRPG游戏场景。当然,主角并不是无时无刻都在行走的,只有当其isWalking属性为true时才会行走,需要更改该属性,可以通过对startMoving(调用的同时为主角设定行走方向)及stopMoving方法的调用来做到。
最后一步,就是在游戏主类Game中添加事件侦听器了:
public class Game extends Sprite { [Embed(source="/assets/data/scene.xml", mimeType="application/octet-stream")] public var Scene:Class; private var _sceneXML:XML;//场景信息 private var _world:World; public function Game() { this.addEventListener(Event.ADDED_TO_STAGE, onAdded); } protected function onAdded( e:Event ):void { //初始化数据 _sceneXML = XML( new Scene() ); _world = new World(); addChild( _world ); _world.enterScene( _sceneXML ); _world.start(); _pausePanel = new GamePausePanel(); this.addEventListener(EnterFrameEvent.ENTER_FRAME, onEF); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); } private function onKeyDown( e:KeyboardEvent ):void { var direction:String; //方向键控制人物行走 switch(e.keyCode) { case Keyboard.LEFT: direction = RoleState.DIRECTION_LEFT; break; case Keyboard.RIGHT: direction = RoleState.DIRECTION_RIGHT; break; case Keyboard.UP: direction = RoleState.DIRECTION_UP; break; case Keyboard.DOWN: direction = RoleState.DIRECTION_DOWN; break; } _world.startMoving( direction ); } private function onKeyUp( e:KeyboardEvent ):void { _world.stopMoving(); } private function onEF( e:EnterFrameEvent ):void { if( _world.paused ) { } else { _world.advanceTime( e.passedTime ); } } }
运行这些代码,可以看到如下结果(点击图片进行试玩,试玩需求Flash Player版本为11.1以上):
如果你试玩了,你会发现帧频会比较稳定地维持在60FPS这样,不会掉得太厉害,人物、场景移动比较流畅。是不是找到一点硬件加速的快感呢?你可以下载了完整源码,然后修改World类中的_npcCount属性,试着往场景中多加一些NPC观察一下性能及CPU使用情况。
在下一章中,我们将为该游戏做另一个界面——游戏暂停界面,并试着把玩一下粒子效果