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

标题: AS3转码其他语言(本帖以 C#+Unity 举例),工具开发记录 [打印本页]

作者: general_clarke    时间: 2018-11-9 12:17
标题: AS3转码其他语言(本帖以 C#+Unity 举例),工具开发记录
本帖最后由 TKCB 于 2020-1-27 09:56 编辑

【序】

楼主从FlashMX、AS2时开始使用flash解决方案,目前是杭州一家小型游戏开发公司的CTO。

在相当数量同业者追逐热门语言并为语言特性和编译工具头疼时,另一群人把时间和精力投入到算法和架构上。
有可能的话,我希望可以将一门语言一直专精下去。

我司长期专精于as3,从flash小游戏到AIR手游,业内长期唱衰flash并未影响到我司收入和规模扩张。
到发此帖时,AIR和adobe技术仍是我司赖以生存的资金来源。

直到两件事的发生让我意识到AIR虽然目前仍是稳定完善的解决方案,然为了公司长远发展,必须留一条后路才行。

一件是AIR28被苹果拒审。此问题是AIR运行时调用了苹果隐藏的API导致。
在这个问题上adobe在AIR29版本作出了不甚完美的修复,直到AIR30彻底解决花费了我们作为一款在线项目难以接受的时间。
我司有出色的工程师,手工修改了苹果AIR运行时库,比同业者提前成功上传,但是这让我们不得不考虑如果下次出现相同现象是否也能顺利解决。

第二件,我司位于杭州,在一个月前因希望扩张项目组招聘更多as程序员,
然即使在招聘网站发布广告、群发布广告(感谢T大设置了群公告)
实际数周内应聘者寥寥,少数的面试者水平也无法满足新项目需求。

因此,为了科学的可持续发展,比起老牌的C++/Cocos和新兴的H5,我看中了效率好功能强的C#+Unity解决方案。

此帖放在水区作为日记形式,记录从AS3转换C#的大事小事。此工具仍在制作,虽然楼主不认为此方案有什么问题,
如中途出了意外,这篇便是一篇探坑作。


因涉及一整个新项目,不出意外将不定期更新数个月到半年时间。
这个记录谈不上技术分享,因为想到哪写到哪,也有一些楼层水分含量较高,随时会编辑掉错字和查缺补漏。
如版主认为需要迁区请随意







作者: general_clarke    时间: 2018-11-9 12:58
本帖最后由 general_clarke 于 2018-11-10 12:07 编辑

方案是“代码转换”,不是“我要转行”
要被改变的只有as3,
从var a:int = 10;通过工具转变为int a = 10;

我们不算第一个吃螃蟹,诸位可能听过LayaFlash,这是个as3项目转换成H5项目的技术
那么,为什么不直接用其他语言重写一次而是搞转码工具?


as3项目是公司的流水项目。作为程序员可以通过跳槽放弃原有项目改变使用技术,作为公司不行。为了公司人的衣食住行,as3项目必须维护下去。
那么,
如果新项目直接使用C#编写,我们会面临以下问题

我们有不少旧库。
旧库是公司核心竞争力之一,包含我们积累下的优秀物理、美术、通信、调试代码、编辑器和工具链。


首先
如果旧库完全放弃,新项目老项目使用不同主程不同技术不同库,
因我们对新技术C#无积累,
那么将导致项目水平更大权重取决于空降主程、主程因故无法工作事其他语种程序员难以接手。
CTO因不能精通所有种类语言和解决方案,从倾向技术变为更多的倾向人事和产品策划方面管理、
要为每个项目单独配置和调试后台对接、美术格式、工具链等。

这个方案对于资金和人事实力雄厚、侧重管理而非技术、侧重运营而非策划、甚至侧重收购而非开发的大公司不成问题。
大公司同语种项目可能有不止两三个,能迅速地进行人员抽调、配置专门人力提供公用工具、甚至最差情况项目砍了算了。
以我司小公司实际情况,这个方案代价颇为沉重。


然后
将旧库完全手写成C#,旧库如出现修改,那么C#也同样要修改,用写代码常用的话就是出现了耦合。把一个我设计的类,聘用不同语种程序员重写,
因程序员本身的差异也会开销大量时间。重写者和之后的维护者也很可能不是一个人。
如之后公司另立项H5项目,那么一处修改将需要用三种语言各写一遍。


最后
制定的方案是游戏核心部分和基本类库从AS3经代码转换工具转变为C#,
C#+Unity部分负责提供运行库支持,即实现DisplayObject等AIR运行时必须类
在此方案下,
“底层算法”始终使用AS3开发维护,
每次修改重跑一次工具转换到C#就可以了。
对所有项目,素材、后台对接、编辑器、工具链可以使用相同的一套
关于DisplayObject等运行库,因为做好便不会修改,对每个非AIR解决方案这是一次性工作

说实在的,
身为一名小公司CTO,我无法放心把公司核心项目直接托付给对应程序员

能对所有项目底层技术进行把控,而不是干脆地专成管理位置,我认为这是公司目前长治久安的一个保证。
我司也好,业内也好,程序员离职、生病无可避免。
项目底层一定要有人负责,比起每个项目单独依赖别人,不如依赖我好了。



大道理就这么多,下次开始写实际的转换内容。




作者: general_clarke    时间: 2018-11-9 15:06
本帖最后由 general_clarke 于 2018-11-14 14:21 编辑

【一】

这个转换方案很大。我所知业内并没有成熟值得借鉴的方案,当然如果有我也没想拿来使用。随便能拿来用还没坑的方案,我们能用,别人也能,
成不了核心竞争力。

为了确认方案是否可行,首先将方案进行细化,有哪些工作要做,逐一确认可行性

从流程上
1,制定通用素材格式,这个比较简单,用PNG就好。
2,制作实例的AS代码
3,将AS3代码转换成C#
4,让这段C#在Unity下成功运行,效果和AS3内一致

从复杂程度上
1,转换trace("HelloWorld")
2,转换一个小球从屏幕左移动到屏幕右
3,转换文本框、声音和其他基本、必要的类
4,转换老项目框架和工具库,对其功能逐一进行尝试
5,转换实际项目业务代码
6,这个是扩展的,让转换成C#的代码支持热更。

总体来看思路清晰,工作量不少,不同步骤难度各异。

转换并不需要实现AIR运行时的所有类,一些类(比如Math)在C#也能比较简单找到对应
所以实际C#编写运行库所需的,只是编写
flash.display包
flash.event包
flash.text包
flash.media包
flash.system包
flash.utils包
其中的一部分常用类以及顶层类。

经过基本分析我认为这个方案是可行的,

那么,先订个小目标


var a:String = "HelloWorld";
trace(a);
转换成
string a = "HelloWorld";
System.Console.WriteLine(a);

简单语法是转换的基础,这个我们已实现功能,实际步骤比看上去的复杂,
下次写这个。


作者: phixcat    时间: 2018-11-9 16:15
关注。。
作者: general_clarke    时间: 2018-11-9 16:45
本帖最后由 general_clarke 于 2018-11-10 12:11 编辑

为了将
var a:String = "HelloWorld";
trace(a);
转换成
string a = "HelloWorld";
System.Console.WriteLine(a);
首先要让程序能读懂这句的单词和语法。

将上述代码当作字符串
使用for循环逐个charAt(i),制定规则断句
所谓断句的规则,
从0位置开始,
在当前位置读入一个字符,如果是英文,那么向后一直读到下个英数之外字符之前位置,根据内容分类成关键字或标识符。
当前位置读入的如果是数字,向后读到下个非数字的字符之前位置,读取到的内容作为数字
当前位置读入的如果是其他符号,向后读到下个空白区域或者英数之前位置,读取到的内容作为符号。
一些特例,比如0x开头的数字,或者写作英数的符号(is as in)因为是特例所以特殊判断。
再如"*"有时表示乘法,有时表示类型。
因为实际上特例颇多,这个字符串解析步骤我做了不少天才完成

之后得到了下面的数组。

[["关键字", var], ["空白", " "], ["标识符", "a"], ["符号",":"], ["标识符", "String"], ["空白", " "], ["符号", "="], ["空白", " "], ["字符串", "\"HelloWorld\""], ["符号", ";"]]
这样得到了as3原文每个“单词”的类型数组。

用数组表示“单词”不利于扩展,
设每个不能形成AS3Value但是有意义的”单词”为AS3Atom,根据单词的类型做AS3Atom的不同派生类
例如上面提到的["标识符", "a"],表示成new AS3AtomSymbol("a");
整个var a:String = "HelloWorld";被转换为
[new AS3AtomKey("var"), new AS3Space(" "), new AS3AtomSymbol("a"), new AS3AtomOper(":"), new AS3AtomSymbol("String"), new AS3AtomSpace(" "), new AS3AtomOper("="), new AS3AtomSpace(" "), new AS3AtomString("\"HelloWorld\""), new AS3AtomOper(";")]


同理获得到trace(a);的每个“单词”类型数组
有了单词,那么接着考虑的是如何来表示语法。






作者: general_clarke    时间: 2018-11-9 17:30
本帖最后由 general_clarke 于 2018-11-10 12:12 编辑

as3也好,其他语言也好。每句的语法是由关键字和运算符决定的。

例如关键字是if,那么语法是
if(【A】){【B】}
再如运算符是+=,那么语法是
【A】+=【B】


类似a = b,a-=3这样二元运算符决定的语句很多,决定先攻克这个语法
称其为AS3Value


一个普通的二元运算AS3Value有三个元素,即左元素,运算符和右元素,形如【A】【符号】【B】

其中的【A】和【B】可以是AS3Atom或其他AS3Value,
因此
令AS3Value和AS3Atom共同继承自最小单位AS3ElementBase。
令AS3Value的构造函数为public AS3Value(left:AS3ElementBase, oper:AS3AtomOper, right:AS3ElementBase)


举例:
a = 1+b-3*4;
根据运算符优先级会断句成如下形式,
(a = ((1+b)-(3*4)))每个括号中一定只有3个元素构成【A】【符号】【B】形式。(忽视空格和分号)
P.S.关于如何根据运算符优先级确定表达式计算顺序,可于网上搜索“前缀式”或“后缀式”来了解。


例子的
a = 1+b-3*4;
转换成单词数组为
[new AS3AtomSymbol("a"), new AS3AtomSpace(" "), new AS3AtomOper("="), new AS3AtomSpace(" "), new AS3AtomNumber("1"),new AS3AtomOper("+"),new AS3AtomSymbol("b"),new AS3AtomOper("-"),new AS3AtomNumber("3"),new AS3AtomOper("*"),new AS3AtomNumber("4"),new AS3AtomOper(";")]
根据运算符优先级断句后,变成
new AS3Value(new AS3AtomSymbol("a"), new AS3AtomOper("="), new AS3Value(new AS3Value(new AS3AtomNumber("1"), new AS3AtomOper("+"), new AS3AtomSymbol("b")), new AS3AtomOper("-"), new AS3Value(new AS3AtomNumber("3"), new AS3AtomOper("*"),new AS3AtomNumber("4"))));

写得清晰一点就是
[AS3Value] a
        [object AS3Atom<symbol @192>] a
        [object AS3Atom<oper> @194] =
        [AS3Value]
                [AS3Value] b
                        [object AS3Atom<number> @196] 1
                        [object AS3Atom<oper> @197] +
                        [object AS3Atom<symbol> @198] b
                [object AS3Atom<oper> @199] -
                [AS3Value]
                        [object AS3Atom<number> @200] 3
                        [object AS3Atom<oper> @201] *
                        [object AS3Atom<number> @202] 4



P.S.
@后的数字是Atom的生成顺序请忽视,下同

作者: general_clarke    时间: 2018-11-10 11:30
本帖最后由 general_clarke 于 2018-11-10 12:13 编辑

因为经过上述步骤,已经能根据as文件分析出所有的单词类型,也知道了简单的语法,
所以做了一个代码转换的副产品。
9ria的老坛友可能记得我发代码经常带着和论坛自带染色工具不同的颜色

package
{
        public class testClass extends Sprite
        {
                public var field:String;
                public function testClass():void{
                        var a:String = "HelloWorld";
                        trace(a);
                        var b:int = 1+2;
                        field = func1(a);
                }
                public function func(p:String):String{
                        return p;
                }
        }
}

经转换成
package {
        public class testClass extends Sprite
        {
                public var field:String;

                public function testClass ():void {
                        var a:String = "HelloWorld";
                        trace(a);
                        var b:int = 1 + 2;
                        field = func1(a);
                }//end of Function testClass

                public function func (p:String):String {
                        return p;
                }//end of Function func
        }
}






作者: general_clarke    时间: 2018-11-10 11:37
[ 本帖最后由 general_clarke 于 2018-11-10 12:00 编辑 ]\n\n此楼层禁用了Discuz!论坛编辑器代码。
楼上做的事情,
就是将var a:String = "HelloWorld";
转换成语法树
[AS3Var] a     
        [AS3Var] a            
                [object AS3Atom<key> @40] var
                [AS3Value] a
                        [object AS3Atom<symbol> @42] a
                        [object AS3Atom<oper> @43] :
                        [object AS3Atom<symbol> @44] String
                [object AS3Atom<oper> @46] =
                [object AS3Seg<string> @48]  "HelloWorld"

之后在语法树中递归地处理语法树每个节点,根据节点的不同类型前后插入Discuz!论坛编辑器代码,形成了
[color=6699cc]var[/color] a[color=000000]:[/color]String = [color=990000]"HelloWorld"[/color];
在不禁用论坛编辑器代码时即显示出代码染色
作者: general_clarke    时间: 2018-11-10 17:56
包括var语法、new,return,if,for,switch-case,do-while,
包头,类头,函数头,
匿名数组,匿名对象,匿名函数,

此工具能解析的语法足够支持一个项目的所有常用代码时,花费了一个半月时间。
作者: general_clarke    时间: 2018-11-12 11:53
本帖最后由 general_clarke 于 2018-11-14 14:21 编辑

【二】

因为使用adobe技术,我们的很多FLA文件需要使用JSFL处理。
编写JSFL使用JS,
一句
var a:int = 10
在JS中写作
var a = 10
即需要去掉类型。

那么之后尝试使用此工具完成这项任务。
难度并不是很大,只需要找出所有的var语句,输出时将var语句冒号后的内容去掉即可,
不到半天就写完了。

后来想到了其实
str = str.replace(/(var\s+[\w\)]+\s*)\:\w+/g,"$1");
就能解决问题,

这半天时间从结果上是做了白工。
从过程上,好歹是代码转换工具是真正地完成了从一门语言转换成另一门语言的过程。

好吧,虽然是转换,
AS3是JS的超集,这个转换太low了。

楼主做到这里的时候,cocos是热门的技术,而flash在被唱衰。
为公司长远发展保底,我们聘请了同时会用C和as3的程序员
希望编写对接库。

对接库实现最基本的flash数据结构,display、text、event、utils等重要包,
在代码转换成C语言后进行对接。
这不是重新编写一个AVM虚拟机,
例如,
AVM虚拟机会定义Number数据结构,运算经过复杂的判空容错,
但是我们转码不会,转码中Number会直接被转换成double或long,使用C语言自带运算。
所以虚拟机运行效率是C系语言的1/10以下,直接转码效率几乎等同于C系语言。


作者: general_clarke    时间: 2018-11-12 12:04
本帖最后由 general_clarke 于 2018-11-13 13:02 编辑

一个月后。

C语言程序员不干了。

这位同僚告诉我太难,搞不定。
我询问难在哪里,得到回复是“垃圾回收机制搞不定”

C语言并没有as3/java一样的垃圾回收机制,
这会导致不得不为对象设置手工释放。

我寻找迁就的方案,比如为每个类自动生成destroy方法,
这个工作在转成C语言前在AS端做完,工作量算我的。
同僚仍然表示不行,提出了很多其他问题,希望放弃这个项目,
准确地说,希望通过完成不了功能和提出难点让我主动放弃这个项目。

不为则易者亦难,
时值我司第一个手游项目《机战坦克》(现已停运)临近尾声,
继续做下去只会产生问题掣肘。
于是as3转c的工作暂停,
同僚改为负责新项目发布的工作。

作者: general_clarke    时间: 2018-11-13 13:09
本帖最后由 general_clarke 于 2018-11-14 14:21 编辑

【三】

AS3转换C语言已停止一段时间,
游戏上线工作如火如荼。

我们对素材图进行了一定的加密,代码也需要进行加密混淆。
破解无加密混淆的swf成本过低,
就算不担心有人私自架设服务器,对着清晰的代码可以轻易编写防不胜防的作弊软件。

我们出资数千购买国内最出名的SWF加密混淆工具。
工具虽好,无法满足我们的需求。
此工具对ANE和IOS发布支持不良。

我们另有一些基于反射的需求,故联系了工具作者,希望对工具添加定制的功能。
除额外的资金外,工具作者反馈添加支持的时间是两周,且“不确定”

我们可以理解工具作者也有自己的时间安排,不可能为每个购买者提供足够定制支持。
然一个即将上线公测的项目接受这个代价太难了。
这个“不确定”也让我们无法期待日后加密软件能得到良好维护。

将“AS3”转换为“加密后的AS3”,相当于代码转换,
求人不如求己,既然我们在做这个工具,
自己来做AS3混淆。



作者: general_clarke    时间: 2018-11-13 13:23
本帖最后由 general_clarke 于 2018-11-26 15:59 编辑

符号混淆没有性能代价。
这不能阻止反编译,但可以极大地增加破解成本,
难读代码能有效劝退以兴趣或小利为目的大多数破解者。

符号混淆,分为编译前对源码混淆,以及编译后解析SWF文件的Tags格式混淆其中的字符池。

var a:Sprite = new Sprite;
a.name = "A";
var b:ClassA = new ClassA;
b.name = "B";

应被混淆成
var _1_:Sprite = new Sprite;
a.name = "A";
var _2_:_3_ = new _3_
_2_._4_ = "B";

总结地说,
我们自行编写的类名和属性需要混淆,ClassA以及属于ClassA的"name"需要被混淆。AIR运行时和第三方类(主要是ANE对接类)则不混淆,Sprite的"name"属性是不能混淆的

为了完成上述工作,混淆工具必须了解每个类是否要加密,以及每个符号"name"的定义位置。
这比AS3转JS难了不少,只好投入额外的几周时间来升级。


作者: general_clarke    时间: 2018-11-13 15:38
本帖最后由 general_clarke 于 2018-11-26 16:00 编辑

几周后,方案完成了。
对文件的每个符号(AS3AtomSymbol)进行检查。
以符号a为例

如果在这个符号所在function里面找到了"var a"语句,那么它是局部变量。
如果在这个符号所在class里面找到了"public/protected/private var a"语句,它是成员变量。
如果在这个符号所在package的import语句导入类包含a类,它是类名。
如果a是一个顶层类,或当前文件同文件夹有其他as文件名字叫a.as,这个a也是类名。

在当前类找不到时递归去找其父类直到Object

诸如a.name这样的符号,用同上的办法检查到其中的a究竟是类名、是局部变量名还是成员变量名
并根据a的类型,是我们自行开发的还是官方或第三方类,决定name是否需要混淆。

经过这样的升级,
代码转换工具能成功地将AS转换成混淆AS。

比起第三方工具,
自制工具能随意地根据项目进行扩展,跳过应用反射的部分,
甚至可以在混淆前检查一遍代码或作出一些预处理。

为了使转换后的代码可读,我们实际加了注释

var a:ClassA = new ClassA;
a.name = "abc";
转换为
//var a:ClassA = new ClassA;
var v05234579:v024688945 = v024688945;
//a.name = "abc"
v05234579.v08343427 = "abc"

同时生成了一个字典文件,字典文件记录了符号与混淆后的符号对应关系。
于是,我们调试时和后台随时可以根据字典确定错误堆栈或者协议内正确的变量名,甚至可根据字典解除混淆,反编译成原始代码。

而破解者因为没有此字典和注释,阅读反编译代码事倍功半。





作者: general_clarke    时间: 2018-11-13 17:35
本帖最后由 general_clarke 于 2018-11-13 17:58 编辑

附一个类转换前后的效果,这个类制作的是弹出对话框右上角的关闭图标“X”。
看了一遍混淆后的代码,反正我自己读不懂,破解者不见得比我高明

  1. package tank2014x.mobile.panels.uiControls
  2. {
  3.         import flash.display.DisplayObject;
  4.         
  5.         import generalMVC.display.SOTSprite;
  6.         import generalMVC.ui.UIBase;
  7.         
  8.         import starling.display.STLSprite;
  9.         
  10.         public class UIClose extends UIBase
  11.         {
  12.                 public var bg:UI9GridGradual;
  13.                 public var fork:DisplayObject;
  14.                 public function UIClose(mc:*=null, initObj:Object=null)
  15.                 {
  16.                         if(!mc){
  17.                                 mc = new SOTSprite(new STLSprite);
  18.                         }
  19.                         super(mc, initObj);
  20.                         
  21.                         bg = new UI9GridGradual;
  22.                         bg.radius = 65;
  23.                         bg.setSize(130,130);
  24.                         bg.setColor(0xDEDEDE,0);
  25.                         //bg.visible = false;
  26.                         bg.x = bg.y = -65;
  27.                         bg.showOn(this.mc);
  28.                 }
  29.         }
  30. }
复制代码


  1. package v0326891.v0780146.v0264284.v0955421{
  2.         import flash.display.DisplayObject;
  3.         import v0263546.v0284825.v0284948;
  4.         import v0263546.v0264038.v0264161;
  5.         import starling.display.STLSprite;
  6.         public class v01167104 extends v0264161
  7.         {
  8.                 public var v0260225:v01061078;
  9.                 public var v01167227:DisplayObject;
  10.                 public function v01167104 (v0291590:* = null, v0480518:Object = null){
  11.                         if (!v0291590){
  12.                                 v0291590 = new v0284948(new STLSprite);
  13.                         }
  14.                         super(v0291590, v0480518);
  15.                         v0260225 = new v01061078;
  16.                         v0260225.v01063784 = 65;
  17.                         v0260225.v01063907(130, 130);
  18.                         v0260225.setColor(0xDEDEDE, 0);
  19.                         v0260225.x = v0260225.y = -65;
  20.                         v0260225.v0317543(this.v0291590);
  21.                 }
  22.         }
  23. }
复制代码

P.S.  cross写成fork,这个程序员晚上我去教育他。
作者: general_clarke    时间: 2018-11-14 16:25
本帖最后由 general_clarke 于 2018-11-14 17:24 编辑

【四】

和多数as3工程师一样,楼主平时在用FlashBuilder
有个很牛逼的编辑器叫做IntellIJ Idea,据说可以编辑as3,容我了解一下。

鼓吹Idea的同时一般会拉Eclipse出来批斗,
用词犀利颇有当年转cocos程序员批斗flash的架势。

这些多是跟风作,半桶水无有效内容的文章不少。
筛选阅读了约5篇有些营养文章后略有失望。

Idea编辑as3依赖于插件,此编辑器并未让我看到双眼一亮的功能。
比起Eclipse,Idea的核心优势似乎是调试输出变量的便利,以及更友好的代码提示。
还有相当数量的次要优势,例如对Git支持更好、支持javaScript边改边显示、更省力的代码模板、剪贴板能保存多次复制的内容等等。

同时我也搜索到了不少Idea编辑器编译和发布失败的例子。
对在线项目不值得冒险换编辑器。

对于优势这些,我认为只要为FlashBuilder/Eclipse编写插件可以实现。

Eclipse插件,届时我并没编写过这种东西。
FB用了这么久,是时候让它“升级”一下了。



作者: general_clarke    时间: 2018-11-14 16:44
本帖最后由 general_clarke 于 2018-11-14 17:17 编辑

因为插件是自己做的,所以一定符合自己的代码风格,也能针对项目架构表现出其他插件做不出智能。
此插件有且只有一个热键,这个热键的作用是“根据光标位置猜我要写什么,自动补出来”

那么先定几个小目标。
下面这些代码我们每天都在敲,浪费时间和脑力。

目标1:
光标在true上按某个键变false
光标在private上按某个键变public

目标2:
我希望在输入"mc.scaleX = 2;"之后按下某个键,程序能自动地补充后一句"mc.scaleY = 2;"
目标3:
光标在var i:int上按某个键变i(一段代码因复制或解除注释时出现两个以上var i,用于去除重复声明i的警告)

目标4:
func(123, "abc", a);
希望选中这一行之后按下某个键,在FB中产生模板,如下图
(, 下载次数: 160)