两小时搞定Flash转换

发布时间:2015-10-30

  本篇会就如何使用LayaFlash IDE将一个AS3游戏转换成一个H5游戏而叙述整个LayaFlash使用过程。开始前,请先了解LayaFlash IDE的安装、编译项目的方法,请确保安装了适合自己AS3开发IDE。更适合于熟悉AS3、JS等脚本语言、或掌握一门其他语言的开发者,学习如何初步掌握LayaFlash的开发方式。

1.新建项目
  我们讲解例子是一个用FLash CS开发的AS3熊猫跑酷游戏。
  案例源码下载(包含转换前后的素材与源码):layaFlashGame.rar

  游戏规则是在屏幕上点击鼠标进行操作,在屏幕上点击一下,熊猫就跳跃,点第二下,熊猫进入二段跳跃,再点第三下一直按着鼠标不动,熊猫就以消耗体力为代价在空中旋转缓慢下降。避免撞到敌人,否则会被扣除体力,体力被扣光后游戏结束。途中搜集到各种道具,获得的竹笋能作为子弹攻击敌人。
  我们先看看游戏原始项目的目录结构,它们被保存在“pandaRunSource”文件夹里:

1.png

  原项目是使用Flash CS系列软件开发的游戏项目,Game.as是文档类文件,游戏素材以链接类的形式保存在pandaRun.fla文件中。pandaRun.fla是Flash素材源文件。当然,使用FlashDevelop也是很好的选择,更直接,速度更快,但由于FlashBuilder界面更细致清晰,我们选用FlashBuilder 4.7讲解本例

  先创建一个新的AS3项目,命名为“pandaRun”(此项目也在教程提供的项目源码中)。添加Laya.swc作为项目的swc文件,Laya.swc位于LayaFlash IDE的安装目录下的“resources\app\swc”目录。修改入口类名为“Game.as”、输出目录为“bin”:
2c.png

  然后把原始项目中的AS3代码文件(.as文件)连同所在目录都复制到新项目里的src目录下,覆盖新建项目的Game.as文件,使用原始项目的代码作为入口类的代码,覆盖后src目录里的文件内容
3.png

  添加初始化代码,对Game.as类的构造函数作如下修改:

public function Game()
{

    IFlash.setSize(800, 500); //设置场景尺寸
    IFlash.setOrientationEx(1); //是否为横屏模式
    IFlash.setBgcolor("#83e8ff"); //背景色
    IFlash.showInfo(true); //是否显示帧率
    super();
    
    if (stage)
        init(null);
    else
        addEventListener(Event.ADDED_TO_STAGE, init);
}


  添加的Event.ADDED_TO_STAGE事件用来确保需要获取stage属性时此属性不为空。新定义一个initGame方法,除了构造函数里的super()语句,原来的其他代码都放到新定义的initGame方法中:

private function init(event:Event = null):void
{
    initGame();
}

private function initGame():void 
{
    var i:* = 0;
    stage.scaleMode = StageScaleMode.EXACT_FIT;
    Board.init();
    Panda.init();
    Food.init();
    Enemy.init();
    Smoke.init();
    Help.init();
    ……


  Game方法的代码包含了初始化舞台尺寸和背景色等项目基本信息。舞台尺寸是根据项目原来的尺寸设置的:

IFlash.setSize(800, 500); //设置场景尺寸
IFlash.setOrientationEx(1); //是否为横屏模式
IFlash.setBgcolor("#83e8ff"); //背景色
IFlash.showInfo(true); //是否显示帧率

  如果原始项目用到了基于Stage3D开发的框架(例如Starling框架)则需将IFlash.setSize()语句用下面这段代码替换掉, 也可以将这段代码封装到一个函数里再由init方法调用。这段代码能确保基于Stage3D的游戏项目的舞台缩放不出问题:

var stageW:int = 0;
var stageH:int = 0;
__JS__( 'stageW = Laya.window.innerWidth;' );
__JS__( 'stageH = Laya.window.innerHeight;' );
(stageW > 0) && IFlash.setSize(stageW, stageH);

  __JS__()方法是LayaFlash提供的方法,使用它可以实现AS3代码和JS代码的混合编写。


2.LayaFlash编译JS

  然后进入LayaFlash的安装路径,打开LayaFlash.exe:
0.png

  点击LayaFlash IDE菜单栏中“项目”菜单下的“打开”选项,在弹出的文件浏览器中定位到我们刚才修改过的游戏项目的项目文件“.actionScriptProperties”,IDE就开始编译项目了。等待编译进度完成:

21.png

  编译进度读取至100%后,LayaFlash便会自动运行,并在IDE窗口中显示编译后的画面:

5.png

  我们这就成功地用AS3项目编译出了一个H5项目,第一步走得不错。但是因为我们目前的代码只设置了项目的背景和尺寸,所以画面中只有背景颜色和显示在窗口左上角的帧率信息。打开AS3项目输出目录(/bin),LayaFlash自动在此目录里生成了一个“h5”文件夹,这个文件夹就是我们的H5输出目录了。目录下经LayaFlash编译出现了一个和AS3项目入口类同名的JS文件game.max.js和html文件:

5_1.png

  接下来我们继续做后续的转换步骤。


3.AS3素材转换
3.1.图片资源

  记得原始项目目录里有一个pandaRun.fla文件吗?那里面保存了游戏项目需要的大量素材。原始项目是一个Flash CS项目,是在Flash CS软件中开发的,可以直接在AS3代码通过元件的链接类获取素材。我们已将项目源码转到了Flash Builder 4.7中,这意味着我们要把原有的素材打包成swf文件,加载到项目中使用。由于H5使用的素材不能是Flash软件发布的swf字节码文件,要用LayaFlash IDE提供的素材转换工具做一次转换,一起看看转换的具体步骤吧。
  首先
去掉pandaRun.fla的文档类设置,导出一个pandaRun.swf文件:

4c.png

  在Game.as类中的init方法添加加载新导出的pandaRun.swf文件的代码,在新建的AS3项目的输出目录下新建“assets”目录,把pandaRun.swf文件复制到里面,这个是AS3项目运行所需的素材:

        注意:由于源资源文件夹内包含链接类,导致直接打开源fla文件后导出SWF文件,出现大量报错和资源转换不完整,使用者需将fla文件放置到其他位置重新导出SWF文件,可避免该错误的发生!

4_1.png

  然后修改initGame方法,增加一个事件参数,让它变成素材加载完成事件的处理函数,完成新项目初始代码改造:

private function init(event:Event = null):void
{
    IFlash.setSize(800, 500); //设置场景尺寸
    IFlash.setOrientationEx(1); //是否为横屏模式
    IFlash.setBgcolor("#83e8ff"); //背景色
    IFlash.showInfo(true); //是否显示帧率
    var loader:Loader = new Loader();
    loader.contentLoaderInfo.addEventListener(Event.COMPLETE, initGame);
    loader.load(new URLRequest("assets/pandaRun.swf"), 
    new LoaderContext(false, ApplicationDomain.currentDomain));
    
}

private function initGame(event:Event):void {
    var loaderInfo:LoaderInfo = event.target as LoaderInfo;
    loaderInfo.removeEventListener(Event.COMPLETE, initGame);
    
    ……

  然后在LayaFlash IDE里依次点击菜单栏里的“工具”|“资源转换工具”打开资源转换工具:

6.png

  将刚导出的pandaRun.swf文件拖入资源转换工具弹窗,swf文件被拖入弹窗区域后会提示swf的来源路径和输出路径,输出目录默认设置到了H5项目的输出目录下,并包含了被转换文件的上层路径(“assets\”)

7.png

  点击开始转换,看到界面提示“转换成功”,素材转换就完成。这样,H5游戏项目也有素材了
 8.png


3.2.声音资源
  有了图片,还需要处理游戏声音。
注意观察pandaRun.fla源文件的元件库就会发现,里面包含了各种声音素材:
10.png

  如果项目的声音素材是像这样被集成到fla文件中的话必须将它们从fla文件中分离出来,成为独立的声音文件。LayaFlash推荐使用mp3和wav格式文件作为声音文件,mp3文件用于播放背景音乐,wav文件用于播放声效。

  以上的声音文件都保存在示例项目“pandaRunSource\素材源文件\声音原始”目录里,分别在AS3项目素材目录(“bin\assets\”)和H5项目素材目录(bin\h5\assets\)下新建一个“sound”文件夹,把声音文件都复制进来

9_1.png9.png

  为了不改动项目代码,我们需要为这些声音文件按照fla中定义的链接类创建对应的类文件,包名均为“sound”,他们都继承自Sound类。为节省开发者时间,这些类已经存放在了示例项目中的“pandaRunSource\sound”目录已经准备了这些类,这些类在此前覆盖新建项目代码时已被复制到新项目的“src\sound\”目录里,打开其中一个文件BgSound.as可以看到以下代码:

package sound
{
    import flash.media.Sound;
    import flash.net.URLRequest;
    
    public dynamic class BgSound extends Sound
    {
        
        public function BgSound()
        {
            super(new URLRequest("sound/FlySound.mp3")); 
            //add by LayaFlash, ch.ji
        }
    }
}

  构造函数里加入了对应的声音文件路径,其他的声音素材链接类代码类似。

4.修改AS3代码,为H5项目转换做准备
  我们试运行一下AS3项目:

9_2.png

  没有通过编译,查看问题面板,发生了2个错误和1个警告项。别着急,这是意料之中的情况。既然有报错,就查看是导致错误的原因,对症下药。FlashBuilder的错误面板能显示两个错误分别和NativeApplication类和BitmapData类有关,双击这些报错定位到出错的代码行上:
9_3.png

  引发这个错误是由于使用了Laya.swc未定义的方法。如果引发“未定义”错误的方法是AS3原有的方法,通常是LayaFlash不支持的API。因为引入的Laya.swc文件包含了经LayaFlash重定义的AS3方法,FlashBuilder或其他“AS3开发IDE”会根据Laya.swc文件检查项目代码,如果是LayaFlash不支持的方法就会提示出来。

  编译错误中提示的NativeApplication类是调用移动设备本机扩展功能的类,在H5中我们的支付功能及其他需要调用本机扩展的功能都会经过LayaPlayer运行器(Layabox家族产品的H5浏览器Runtime解决方案)封装处理,开发者可以从ANE的开发中解脱出来,因此将项目里用到这个类的代码都注释掉就行了。推荐在修改过的代码附近加上简略的修改原因和修改人的注释,今后查找当时修改代码的原因。解决后的代码如下:

private function keyCtrl(e:KeyboardEvent):void
{
    if (e.keyCode == Keyboard.HOME)
    {
        this.pause();
    }
    else if (e.keyCode == Keyboard.BACK)
    {
        this.pause();
        //NativeApplication.nativeApplication.exit();//fix by layaFlash
    }
    else
    {
        this.panda.b();
    }
}

  另一处错误也是类似的原因,将报错的代码注释掉即可:
9_4.png

  解决后的代码:

private function buildBitmap(w:int) : void
{
    this.dpo.bitmapData = _bitmapData[w];
    var bitmapData:BitmapData = this.dpo.bitmapData;
    /*if(bitmapData.getPixel(0,0) == 0)//fix by layaFlash
    {
        return;
    }*/
    ……
}

  经过资源转换工具的的处理,原来AS3项目显示的swf元件都被转换成了加载HTML的Image对象的操作,为了保证性能,没有提供获取像素方面的操作,因此LayaFlash中getPixel方法不可用,要将代码用到getPixel方法的地方去掉。

  剩下的是警告信息,尽管一般情况下我们可以不处理警告,但是为了防止与转换相关的警告混在其中影响转换结果的准确性,我们要尽量处理发出警告的代码。经过检查和修改,引发这些警告的AS3代码都是编写不严谨或项目文件未找到引起的,修改完成后,进入调试项目的运行时报错阶段,运行项目可以看到以下报错信息: 

9_3.png

  报错提示引发这个问题的原因是传入的参数为空导致的,查找调试面板的调用堆栈,跟踪到flyEffSrc的赋值位置:

private static var FlyEffBmp:Class = Panda_FlyEffBmp;
public static const flyEffSrc:BitmapData = new FlyEffBmp().bitmapData;

  进一步判断,是被初始化的flyEffSrc的bitmapData属性为空导致出错了,这个位图数据对象有可能会使用的外部位图数据,属于外部加载的素材,为了验证,打开pandaRun.fla源文件:

9_6.png

  库中确实存在这样一个链接类“Panda_FlyEffBmp”。打开链接类Panda_FlyEffBmp.as,代码如下:

package game
{
    import mx.core.BitmapAsset;
    
    public class Panda_FlyEffBmp extends BitmapAsset
    {
        
        public function Panda_FlyEffBmp()
        {
            super();
        }
    }
}

  AS3项目编译的时候是可以将swf里的元件资源与一个AS3类文件关联到一起的,项目中这样的使用方法并没有问题,如果确定此问题不是因为非法使用AS3导致的就可以跳过这个问题了。

  再次用LayaFlash IDE的编译功能,编译完成后,窗口中看到的不再只是空背景,点击中央三角形开始的游戏按钮,就能看到一个胖乎乎的熊猫开始飞奔了

23.png


5.H5游戏调试
  LayaFlash IDE的菜单栏下,有一个“调试窗口”按钮
,点击它可以打开LayaFlash IDE的调试工具面板

9_7c.png

  调试工具面板从左到右三个区域的作用依次是:
  A、查看H5页面包含的文件目录。
  B、查看源文件内容(JS代码和HTML源码)。
  C、查看调试堆栈和变量表达式。
  三个区域的下方则是用于打印输出信息:

2c.png

  调试项目问题的时候可以按一定顺序逐步测试游戏功能,通常按游戏界面顺序排除。我们就从游戏的开始界面入手吧。开始前,先勾选调试面板的调试堆栈区域里的“Pause On Caught Exceptions”选项:

4c.png

  回到游戏画面,点击游戏界面下方最右边的“帮助”按钮:

9_8c.png

  点击后游戏被打断:

26.png

  鼠标悬停在这行代码的变量上,能看到此时变量内部各个属性的值。this的值为Window:

27.png

  JS中运行的时候this关键字的值和AS3中运行时的值不一样,AS3会自动将定义方法的父对象绑定到this上。这是JS的语言特性决定的,也是JS和AS3存在差异的一个地方,随后说明这个差异。解决这个问题我们需要先了解这个方法是从什么地方运行进来的。

  调试面板右侧显示代码的调用堆栈,可在这找到程序运行错误语句之前都调用了哪些方法,点击其中的堆栈项目就能直接定位到代码位置。鼠标悬停在代码的this可以知道代码片段出自什么类文件。顺带提示一下,此时按下F10单步跳过这个断点即可看到具体的报错信息,为了能清晰的看到程序在报错前的调用顺序,对因出错而自动出现的断点不要使用单步跳过,这会中断我们对代码的跟踪。

  下图是按下F10的调试面板,面板中右侧的调用堆栈消失,影响正常跟踪代码
9_9.png

  继续讨论没按下F10的情况。回到调试堆栈面板,沿着调用堆栈列表中的项按顺序向下点击,就能看到有一个调用与SetIntervalTimer类有关:

9_10c.png

  SetIntervalTimer对应AS3的setInterval方法,用于设置一定时间间隔后调用指定方法,类似定时器的作用。看来出问题的代码是从一个与定时器类似的功能执行进来的。打开项目的入口类Game.as,找到下面的代码,代码调用了游戏开始界面Welcome.as类:

private function initGame(event:Event):void {
    ……
    this.welcome = new Welcome(this.goPlay, this.goRank, this.goStore, this.goHelp, this.goAbout, this.goAcv);
    ……
}

  作为参数传入Welcome构造函数的goHelp方法就是点击“帮助”按钮后的回调函数,它的代码如下:

private function goHelp():void
{
    this.achieve.close();
    this.help.start();
    this.gameStart();
    setTimeout(stage.addEventListener, 100, MouseEvent.MOUSE_UP, this.help.next);
}

        里边用了一个setTimeout函数,这个是AS3提供的一个延迟一定时间后调用指定函数的全局方法,
尝试在这里调试看是否可以得到解决问题的线索。

  在调试工具面板中按下“ctrl+F”可以在源代码中搜索想要查看的代码。我们输入“/game.as”,即AS3类完全小写形式的文件名。按下回车键就能在源代码中搜索到AS3源代码对应JS中Game类所在的位置了:

9_11c.png

  项目的其他类也可以使用这样的方式搜索代码。类里指定的方法也可以通过这种搜索功能找到。如同下图那样在搜索输入框里输入方法名“goHelp=”,再按回车键搜索即可:

9_12c.png

  找到方法代码以后,单击代码行对应的行号即可设置断点:

9_13c.png

  重新运行游戏,点击右边的“帮助”按钮,游戏就在刚才设置了断点的地方停下来了,Bingo!这说明点击“帮助”按钮确实执行了goHelp的逻辑。

  代码看上去完全符合AS3的语法规则,是什么地方的问题?其实问题在传入setTimeout的参数上。先来简单了解下AS3和JS函数作用域的差别,这也是为什么之前刚才在调试工具里查看报错代码时this的值会是Window的原因。

  函数的作用域界定了函数能调用哪些函数和变量,当函数的作用域不在当前运行的代码作用范围的时候,这个函数就无法调用当前代码定义的方法和变量。AS3函数被作为参数传递到其他对象前由于已经绑定了作用域,即使在另一个对象的作用域下被调用也可以访问到原来的对象里的方法和变量,但是JS函数没有绑定作用域的操作,在另一个对象里被调用时作用域会被设定为JS的全局对象Window。
  遇到这样的情况,要对AS3
按照LayaFlash的开发规则做些改动:调用LayaFlash提供的bind()方法绑定函数作用域。
  goHelp方法原来的语句:

setTimeout(stage.addEventListener, 100, MouseEvent.MOUSE_UP, this.help.next);

  goHelp方法里调用bind()方法后的语句:

setTimeout(bind(stage, stage.addEventListener), 100, MouseEvent.MOUSE_UP, 
bind(help, this.help.next));//add by LayaFlash, 绑定函数作用域

  这里使用bind方法绑定了两个函数的作用域,它们是stage对象和stage的addEventListener方法绑定,help对象和help的next方法绑定:

9_14c.png

  修改后运行游戏,再点击“帮助”按钮,问题解决了。排除开始界面的问题,我们继续调试游戏其他功能。

  进到游戏,界面初看是正确的,但控制熊猫跳跃动作的时候熊猫图像怎么消失了?

  控制熊猫动作和显示的AS3代码类是Panda.as类,这个类包含了控制熊猫的各种动作的代码,jump方法则是跳跃的方法。从这里入手查找问题。jump方法代码如下:

public function jump():void
{
    jumpSound.play();
    var power:int = 0;
    if (gameTime - this.strongJump < 5000)
    {
        power = -200;
    }
    this.physicsHiter = this.fallHiter;
    this.speed.y = power - 880;
    this.startTime = gameTime;
    this.updateBitmapData = this.jumpFrame;
    this.smoke.show(this.rect.x, this.rect.bottom);
}

  代码片段设置熊猫跳跃时的力量power,以及此力量计算出的跳跃速度,分别用了jumpFrame方法和fallHiter方法。熊猫动画受到一系列位图列表影响,位图列表中保存了熊猫跳跃等动作所需的位图序列,按照一定的时间间隔轮换显示图片就形成了动画。下面几段关键代码说明 Panda 类的updateBitmapData方法被用作熊猫动作画面的定时更新:

  Game.as类部分代码:

private function initGame(event:Event):void {
    ……
    this.core = new <ISteper>[this.panda, this.scene, this.enemy, this.board, 
    this.food, this.shoots, this.ground, this.gui, this.smoke, this.help, 
    this.achieve];
    ……
    this.engine = new FpsGame(this, this);
    ……
}
……
public function step():void
{
    var i:* = 0;
    var o:ISteper = null;
    frameDelay = this.engine.delay / 1000;
    gameTime = this.engine.currTime;
    var len:int = this.core.length;
    for each (o in this.core)
    {
        o.step();
    }
    if (this.panda.rect.y > 680)
    {
        this.gameOver();
    }
}

  FpsGame.as类部分代码:

public function FpsGame(steper:ISteper, ctrl:ICtrl)
{
    super();
    this.steper = steper;
    this.ctrl = ctrl;
}

private function step(e:Event = null):void
{
    ……
    this.steper.step();
}

public function start():void
{
    this._e.addEventListener(Event.ENTER_FRAME, this.step);
    ……
}

  Panda.as类部分代码:

public function step():void
{
    this.running();
    this.physicsHiter();
    this.foodHiter();
    this.enemyHiter();
    this.updateView();
    this.effView();
}
……
private function updateView():void
{
    this.updateBitmapData();
    this.dpo.x = this.rect.x;
    this.dpo.y = this.rect.y;
}
……
private function jumpFrame():void
{
    var frame:int = (gameTime - this.startTime) / 83.3;
    frame = Math.max(0, Math.min(frame, 7));
    this.dpo.bitmapData = jumpBmpds[frame];
}
……
public function jump():void
{
    jumpSound.play();
    var power:int = 0;
    if (gameTime - this.strongJump < 5000)
    {
        power = -200;
    }
    this.physicsHiter = this.fallHiter;
    this.speed.y = power - 880;
    this.startTime = gameTime;
    this.updateBitmapData = this.jumpFrame;
    this.smoke.show(this.rect.x, this.rect.bottom);
}

  core中包含一个Panda类型的属性:panda。step方法遍历了core中的所有对象,并调用他们的step方法。

  Game类被作为参数传入FpsGame类的对象,FpsGame对象持有Game对象引用,内部使用帧事件每帧调用Game类的step方法,从而驱动所有core中所有对象更新各自的画面。对于Panda来说,每帧被调用的就是updateBitmapData函数。

  当Panda的jump方法被调用,就会把updateBitmapData设置成 jumpFrame方法,从而更新熊猫的人物画面。

  按照之前介绍的搜索代码方法,搜索代码关键字“jumpFrame=”, 找到jumpFrame方法定义的地方,点击代码行对应的行号,打下调试断点,开始游戏,点屏幕让熊猫跳跃,程序停在刚才设置断点的代码行,说明之前的代码分析正确。用鼠标查看一下代码的frame值:

9_15c.png

  AS3中本该是整数的frame变量在JS中居然是浮点数,简直不可原谅啊!有木有?但是这在弱类型的JS语言里是难以避免的,这个差异会让frame的无法获取到Panda.jumbBmpds存储的熊猫动作位图。解决办法是在用到整型变量的地方强制转换:

private function jumpFrame():void
{
    var frame:int = (gameTime - this.startTime) / 83.3;
    //fix by LayaFlash 增加整数类型的强制转换
    frame = Math.max(0, Math.min(int(frame), 7));
    this.dpo.bitmapData = jumpBmpds[frame];
}

  继续在LayaFlash IDE里编译、运行、点击开始游戏、控制熊猫起跳,熊猫兄的跳跃动作出现了!

  运用LayaFlash开发规则和与AS3的差异点,按照以上的规律和步骤逐步调试游戏的功能,加上LayaFlash提供的强大编译功能和成套技术解决方案,就能轻松将AS3游戏项目转换为H5游戏项目。

  好了,让我们就此一起在H5游戏的新领域里遨游吧!