11RIA 闪客社区 - 最赞 Animate Flash 论坛

搜索
楼主: general_clarke
上一主题 下一主题

[交流 & 讨论] AS3转码其他语言(本帖以 C#+Unity 举例),工具开发记录

[复制链接] TA的其它主题
 楼主| 发表于 2018-11-21 18:37:28 | 显示全部楼层
本帖最后由 general_clarke 于 2018-11-23 14:57 编辑

【五】

AS3的特性不太适合描述一个异步过程。
比如我要描述以下游戏常见的新手引导步骤:

1,一个手形图标在20帧内缓动到(10,20)位置,
2,播放对话
3,(对话结束后)手形图标在20帧内缓动到(50,50)位置
4,弹出提示框
5,(提示框关闭后)开始游戏的战斗逻辑

用AS3编写的话有两种简单做法。
例1,分多个函数,每个设置回调。

  1. function main():void{
  2.   移动手形1()
  3. }
  4. function 移动手形1():void{
  5.   通过ENTER_FRAME执行异步逻辑,在执行时间结束后调用"播放对话()"
  6. }
  7. function 播放对话():void{
  8.   根据玩家操作处理,在执行完毕后调用"移动手形2"
  9. }
  10. function 移动手形2():void{
  11.   通过ENTER_FRAME执行异步逻辑,在执行时间结束后调用"弹出提示框()"
  12. }
  13. function 弹出提示框():void{
  14.   监听鼠标事件,关闭面板时调用"开始战斗()"
  15. }
复制代码

例1中,对当前步骤的下一步调用直接写在当前步骤中,每个步骤一定需要一个函数,不能简单复用。
如出现10次移动手形,那么就要写10个内容差不多的函数。
使用一些技巧可以让函数复用,比如闭包,但这并不能提升程序可读性和耐改能力。


例2,简单状态机,ENTER_FRAME检查当前在第几步,经switch语句执行对应逻辑,每步单独设置计时器。

  1. public var state:int = 0;
  2. public var nextX:Number;
  3. public var nextY:Number;
  4. public var timer:int;
  5. public var maxTimer:int;
  6. function enterFrame(e:Event):void{
  7.   switch(state){
  8.     case 0:
  9.     case 2:
  10.       移动手形();
  11.       timer++;
  12.       if(timer == maxTimer){
  13.         timer = 0;
  14.         state++;
  15.       }
  16.       break;
  17.     case 1:
  18.       播放对话(2);//播放对话模块结束时设置state = 2
  19.       break;
  20.     case 3:
  21.       弹出提示();//之后进入战斗不需要状态。
  22.       break
  23.   }
  24. }
复制代码
此例在策划增加步骤时稍微好改一点,只要不出低级错误,涉及的代码和修改速度尚可。
为了应付策划可能插入步骤,从开始状态的编号就不是1,2,3,4而是10,20,30,40
但是所有代码写在一个函数中,在逻辑变复杂后单个函数数百上千行,可读性变差,这是面向过程不耐改的典型案例。


实际上如果as3有个sleep或别的语句阻塞当前的逻辑线程(非主线程)
那么上面提到的功能可以大幅缩减代码数量并提高可读性。

为此我设计了一种简单脚本

  1. tweenHand 20 10 20
  2. playDrama 剧情名称
  3. tweenHand 20 50 50
  4. popAlert 弹框名称 弹框文字
  5. startBattle
复制代码

脚本的格式就是用行分隔语句,同行用空格分隔命令和参数。
脚本就是一段字符串,保存在TXT文档,使用如下办法解释执行。

  1. public var arr:Array;
  2. public var ptr:int;
  3. function main():void{
  4.   var str:String = 上述脚本;
  5.   arr = str.split("\r\n");
  6.   ptr = -1;
  7.   runNextCommand();
  8. }
  9. function runNextCommand():void{
  10.   ptr++;
  11.   var line:Array = arr[ptr].split(" ");
  12.   switch(line[0]){
  13.      xxxxx
  14.   }
  15. }
复制代码


这样策划一旦加步骤,或者调整步骤顺序,
如果是老的逻辑,比如播放剧情移动手形图标,那么策划你自己改TXT就行了。
如果是新的逻辑,比如新增一个“按钮发光”命令,程序扩展解释器增加一条命令,策划你还是改TXT就行了。
回复

使用道具 举报

 楼主| 发表于 2018-11-21 18:54:33 | 显示全部楼层
本帖最后由 general_clarke 于 2018-11-26 19:09 编辑

这个简单脚本后来发扬光大,追加命令,并出现在我司游戏的调试控制台上。

我希望输出Starling舞台上某对象的x坐标,代码相对简单,这个不注释了。

  1. getClass starling.core.Starling
  2. getProperty current
  3. callGameFunction getChildAt 0
  4. callGameFunction getChildByName 我的对象
  5. trace
复制代码


因为对IOS设备编译AIR程序过程缓慢,
此控制台调试方案在IOS设备上用来输出一些变量尤其好用。

经过多次扩展,这套基于AS3解释器的脚本现有300多条命令,
支持创建对象、反射调用、常规运算、循环、判断分支、sleep暂停执行脚本、对脚本模拟多线程等功能
概括说来,做as3能做的所有事。

我叫General_Clarke,所以这套脚本就叫GeneralScript,简称GS。

众所周知IOS平台无法更新SWF,不能更新SWF不表示游戏没bug。
此脚本后主要用于迅速解决已上线游戏的bug,也就是Unity们常用LUA干的事。
就几年运营的经验,说“GS脚本从紧急bug中拯救了我们公司”一点也不为过。
而且不止一次。

还是举例来说,游戏对外时发现充值后台URL错写成了测试用的URL,
这是低级而严重的错误。
修复是很快的,改个字符串。
但是必须后台停止充值、发公告告知玩家,迅速提交用于苹果平台审核的新版本。
之后焦急地等待苹果审核通过或者因为另一个低级错误被拒绝,
还要把写错东西的策划臭骂一顿。

通过脚本这个字符串被修改掉了,所以上面这些都没有了。
策划也只被口头批评,下次该犯还犯。

能进行脚本更新是重要的,这也需要程序员来编写。
GS脚本毕竟是汇编一类的二代语言,除了不支持三代语言的特性外,甚至无法支持括号。
加上命令数量多不易记忆,编写GS脚本是个困难和缓慢的工作。
出现紧急bug我也无法将编写GS脚本紧急修复工作交付给其他同僚。


因为我和同僚都会写as3,
那么,
将as3转换成GS脚本是否可行?



回复

使用道具 举报

 楼主| 发表于 2018-11-23 14:54:42 | 显示全部楼层
本帖最后由 general_clarke 于 2018-11-26 19:11 编辑

从三代语言转换为二代语言实际是完成三代语言的编译过程。

希望将
  1. a = b*2-c/3;
复制代码

转换为
  1. mul $b 2//将b和2相乘
  2. set $1//设置为临时变量1
  3. div $c 3//将c和3相除
  4. set $2//设置为临时变量2
  5. sub $1 $2//$reg始终表示上一步结果。此句用c减上一步结果
  6. set $a//将上一步结果设置为$a
复制代码


GS脚本实际描述了AS语句的执行过程。
首先将AS代码解析成语法树。
  1. [AS3Value] a
  2.         [object AS3Atom<symbol> @151] a
  3.         [object AS3Atom<oper> @153] =
  4.         [AS3Value]
  5.                 [AS3Value] b
  6.                         [object AS3Atom<symbol> @155] b
  7.                         [object AS3Atom<oper> @156] *
  8.                         [object AS3Atom<number> @157] 2
  9.                 [object AS3Atom<oper> @158] -
  10.                 [AS3Value] c
  11.                         [object AS3Atom<symbol> @159] c
  12.                         [object AS3Atom<oper> @160] /
  13.                         [object AS3Atom<number> @161] 3
复制代码

因为语法树已经过了后缀式分析,其递归关系已经可以正确地表示代码执行顺序。

设转换结果放置于rt:Array中。
将语法树转换为GS脚本的方法为convertToGS(root:AS3ElementBase, rt:Array)


就二元运算来说,其逻辑是
  1. public var auto_id:int = 1;//用来创建临时变量
  2. convertToGS(root:AS3ElementBase, rt:Array):String{
  3.   if(root is AS3Value){
  4.     var value:AS3Value = root as AS3Value;
  5.     var ls:String = convertToGS(value.left);//左右侧分别转换并且转换
  6.     var rs:String = convertToGS(value.right);
  7.     rt.push(运算符对应字符串(value.oper)+" "+ls+" "+rs);
  8.     return "$"+(auto_id++);//返回临时变量
  9.   }else if(root is AS3AtomSymbol){
  10.     //符号增加$标识。
  11.     return "$"+root.toString();
  12.   }else{
  13.     //数字等直接返回其文字。
  14.     return root.toString();
  15.   }
  16. }
复制代码


对上述语法树根节点调用convertToGS后,得到结果rt,rt.join("\r\n")结果为
  1. mul $b 2
  2. set $1
  3. div $c 3
  4. set $2
  5. sub $1 $2
  6. set $3
  7. set $a $3
复制代码


此脚本已可正确执行,但比手写版本糟糕一点,多出了一个临时变量。
因为解释器使用循环逐行解释,行数越多,执行越慢。

于是多制作了若干条用于优化快速执行的命令,从rt开始进行优化合并,缩减rt的行数,
此脚本最后变成了
  1. divAndSet $1 $c 3
  2. mul $b 2
  3. subAndSet $a $1
复制代码


二元运算的转换是个成功,只有二元运算是不行的。
AS转换GS脚本需要处理的语法较多,
语法树此时还是摸着石头过河的版本,不时出些问题。

磕磕绊绊,一边制作编译工具一边优化语法树。
约有一个半月时间后,终于可以有九成九的把握将一个函数不出错地转换为脚本
优化结果比人力手写节约1/4~1/2的行数。

然而这个转码工具实际很少被使用,
因为开发太久,同僚们已能比较熟练手写GS脚本,
以及作为补丁的程序比起运行速度更注重可读可维护性。

无论如何,至少从技术上,这是一个货真价实的“转码”成功案例。
也为日后做AS3到C#转换奠定了信心。
回复

使用道具 举报

发表于 2018-11-25 22:51:39 | 显示全部楼层
这真是历害了
回复

使用道具 举报

 楼主| 发表于 2018-11-26 19:53:08 | 显示全部楼层
本帖最后由 general_clarke 于 2018-12-1 20:55 编辑

http://www.11ria.com/forum.php?mod=viewthread&tid=1377&page=1

这个是前几层提到的翻译软件,AS3转GS,相当于解释AS3
回复

使用道具 举报

发表于 2018-12-5 10:56:14 | 显示全部楼层
很好的东西
回复

使用道具 举报

 楼主| 发表于 2018-12-5 17:10:36 | 显示全部楼层
【六】

转眼4年。
某天,我发现公司变大了。
公司的同事不知哪天已经多到认不全。
曾在夏天会进水的平房,也变成了正对购物中心的高层写字楼。

随着公司规模的扩大,
我们有了足够的资金和人力开设第二个项目,
为了公司进一步稳定发展也必须这么做。

正本文序言所言,新项目遇到adobe技术的一系列利空。

对比之后,
既然AS3可以转GS,没有理由不能转为C#。
我设计了AS3到C#,AIR到Unity的转码方案。
回复

使用道具 举报

 楼主| 发表于 2018-12-5 17:24:02 | 显示全部楼层
本帖最后由 general_clarke 于 2018-12-7 00:55 编辑

我希望做出AIR相同的效果,同时尽可能少学Unity

这个工作的第一步是——下载Unity。Unity软件最低授权是免费的。

不得不承认,Unity就制作游戏的功能性上比Adobe系列专精太多。
它提供了一整套编辑器,在不经过额外的架构设计情况下,便能做出数值分离和视图分离,这能为多人协作省下很多功夫
运行时支持比起AIR也是只强不弱。

VisualStudio从头到尾散发着比FlashBuilder更加快捷方便的信息。
这些都让作为AS程序员的我感觉到压力,
质疑是否自己驾轻就熟的技术早已过期。

我需要研究Unity
做出最简单的构建一个2D项目、显示PNG图、显示文本、一个小球从屏幕左边滚动到右面等。

后来结合下载的例子,成功地利用Canvas和RawImage显示了一张图片。
以及使用主类的update方法使其进行移动。
我希望转换后的代码也能实现相同的功能。



回复

使用道具 举报

 楼主| 发表于 2018-12-6 21:13:24 | 显示全部楼层
本帖最后由 general_clarke 于 2018-12-7 00:55 编辑

因代码转换会涉及大量同名类问题,
下文中,flash/AIR运行时下的类会用"as3::"或"flash::"表示
用C#重写实现的DisplayObject会用"C#::"或"Unity::"表示
如"C#::as3.flash.display.Sprite"表示用C#编写的包名是"as3.flash.display"的类名为"Sprite"的类。



为了编写C#::as3.flash.display.Bitmap
先写了C#::as3.FlashObject类作为所有转码的基类。
这个类实现用中括号通过反射读写属性的逻辑,来支持flash常见的this["abc"] = 100;

再写C#::as3.flash.events.EventDispatcher类。
这个类的逻辑比较难,参考Starling的写法,花了一点时间,
做到调用其dispatchEvent能正确从字典逐个执行注册好的回调方法。

DisplayObject还继承IEventDispatcher接口,
从实际使用,我认为IEventDispatcher接口没有足够重要性,跳过了。

之后才开始编写C#::as3.flash.display.DisplayObject类。
每个此类对象包含一个Unity的GameObject对象。
GameObject是Unity的最小单元
编写新类使用getter/setter设置x,y等,实际设置内部包含的gameObject的x,y等

然后少不了C#::as3.flash.display.DisplayObjectContainer
基本上照搬Starling的做法,实现对子项的管理。

为了能让对象被显示,制作C#::as3.flash.display.Stage
此类通引用一张Unity::Canvas的办法,
此Canvas作为显示文本、图片的画布。

最后是C#::as3.flash.display.Bitmap类。
其中内置一个Unity::RawImage作为显示对象,
内置一个Unity::Texture2D充当原本的as3::BitmapData对象

使用如下代码作为入口,已经相当接近于AS



  1. using System;

  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using as3;
  6. using as3.flash.display;

  7. public class Main : MonoBehaviour {
  8.         Bitmap bmp;
  9.         void Awake()
  10.         {
  11.                 initFlashGlobal();

  12.                 Texture2D texture = Resources.Load<Texture2D>("textures/abc");//图片是从外部加载的
  13.                 bmp = new Bitmap(texture);
  14.                 bmp.y = 100;
  15.                 FlashCore.stage = new Stage;//FlashCore用静态属性来保存一些单例
  16.                 FlashCore.stage.addChild(bmp);
  17.                
  18.         }
  19.         private void Update()
  20.         {
  21.                 bmp.x = bmp.x + 1 ;
  22.         }
  23.         private void initFlashGlobal()
  24.         {
  25.                 Application.targetFrameRate = 30;
  26.         }
  27. }

复制代码


好消息是图片显示出来了
坏消息是显示的位置不大对。

Unity的(0,0)原点位置不同,以及y坐标轴与flash相反,
于是在做C#::Stage时,把Stage的坐标改到屏幕左上角并且y轴取反。
这会导致所有图片显示成上下颠倒。

于是让所有非容器的的DisplayObject再上下颠倒一次将其抵消。



Unity::GameObject的基础坐标是全局的,即其x,y属性表示剪辑在舞台上的全局位置
而as3::DisplayObject的x,y是相对父级的位置。

利用Unity::GameObject设置localPosition、localRotation、localScale的办法可以设置显示对象相对父级的坐标。
从个人经验认为上述几个接口是低效方法,
于是仿照starling的写法,每个C#::DisplayObject缓存所有的对x,y,rotation等的赋值,
只在每帧调用的update时一次性设置localPosition、localRotation、localScale


回复

使用道具 举报

 楼主| 发表于 2018-12-13 15:49:52 | 显示全部楼层
另利用C#底层类,简单实现了Array和Number等,
结合转码工具的成功,我认为转码方案已随时可以启动了。

我目前是只使用了七天Unity的新手,
C#的编写速度也只有AS3的两成。
那么下一步要做的是招聘一名C#程序员。

感叹技术冷热的差异,
不同于AS3招聘启示的门可罗雀,在我们发布C#招聘广告后简历涌入没有停过。

我们并没有作出太多对比,因为首位面试者便是靠谱的C#牛人Unity主程。

强手加盟使这个转换的成功率进一步提高。

项目立项后的前台配置是含本人在内两名AS程序员,一名C#程序员,一名LUA程序员。
AS负责战斗逻辑编写
C#负责UI底层逻辑以及AS对接
LUA负责UI业务
另UI编辑器也能在策划师操作下一定程度生成基于C#/LUA的面板和其它控件。

如下图所示。
QQ截图20181213154509.png
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐 上一条 /1 下一条

感谢所有支持论坛的朋友:下面展示最新的5位赞助和充值的朋友……更多赞助和充值朋友的信息,请查看:永远的感谢名单

SGlW(66139)、 anghuo(841)、 whdsyes(255)、 longxia(60904)、 囫囵吞澡(58054)

下面展示总排行榜的前3名(T1-T3)和今年排行榜的前3名的朋友(C1-C3)……更多信息,请查看:总排行榜今年排行榜

T1. fhqu1462(969)、 T2. lwlpluto(14232)、 T3. 1367926921(962)  |  C1. anghuo(147)、 C2. fdisker(27945)、 C3. 囫囵吞澡(58054)



快速回复 返回顶部 返回列表