Flash游戏资源压缩方案(将SWF用作资源打包)
开发网页游戏画面效果和资源量是天生的死对头,如何平衡一个游戏的下载量和画面,减少游戏的等待时间页游生存的关键,所以一套合适的资源方案显得尤为重要.首先我们先来对比一下我们在开发网页游戏过程中常见的各种资源:
1.PNG
它的编码概要为在文件头中记录文件所用到的所有颜色值(调色板),再在后续的像素内容记录他们的颜色索引。选用该种图片格式的图片文件资源在后续的文件体积压缩优化上可选性较少,通常为减色处理,PNG支持3种颜色索引模式 PNG8 PNG24 PNG32分别代表最多所包含的颜色数,比如PNG8最多支持256色.
2.JPG
JPEG文件的特点主要集中在DCT变化与量化的过程,它是采用离散变化将图像空间域转化为频率域再根据能量集中在低频域与人眼对亮度变化敏感的特点对一些非关键信息进行忽略却又不易察觉的效果.
3.SWF:
如果把关键帧放在主时间轴上,加载完之后就可以直接ADD到场景上,这种资源加载同一个SWF文件,FLASH不会自动为我们共享其中的位图资源,所以每次使用Loader加载这个SWF文件的时候,实际上会创建出很多个位图实例,非常占用内存,所以一般我们不会把资源直接放在住时间轴上.而是把他放到库里,通过在库里为图片指定导出的AS3连接,在程序里通过new Background()的方式来创建对应的美术资源,自己定义动画的播放,不使用MovieClip,这样我们可以确保同一份位图资源在内存中只有一份副本.
以上三种文件格式的主要优缺点有
SWF 自由度高,出了问题难排查,难管理
PNG 高保真 支持透明通道 体积大
JPEG 可通过调节图片质量来控制压缩比 不支透明通道
那么我们能不能把他们的优点集中起来忽略他们的缺点?
答案是肯定的,我们要做的是二进制拼装一个SWF文件,让他支持JPG压缩(高压缩率),支持透明通道(我们游戏里的大多资源都是需要透明的),并且仅让SWF来进行打包,有连接名可以导出.
为什么要用SWF来打包资源,原因是首先SWF有一种标签,JPEG3标签支持带透明通道的JEPG格式,另外Flash虚拟机解析SWF文件的速度比我们自己解析自定义格式文件的速度快。
确定了方案后我们首先要了解SWF的格式,ADOBE官方公开的SWF文件格式文档Swffileformat.pdf.
第一个标签必须是文件属性FileAttributes标签,最后以END标签为结尾.SWF文件内容主要分为两部分,标签内容与ABC文件内容,可以理解为SWF文件是以标签-文件块(tag file)的形式组织起来的。每个标签文件的文件头标识了当前文件块的长度,以及是什么类型的文件块。早期的SWF文件仅有标签内容,他的组成结构如图所示,由一头部标签信息信息再加以其他特性标签所组成,它运行与早期的FLASH虚拟机AVM1上,类似于HTML标签语言。
而AS3类则对应着一个DoABC文件块,后来由于复杂逻辑实现与性能提升的需求ADOBE升级了FLASHPLAYER加入了第二代虚拟机AVM2,制定了新的编码文件格式ABC文件。为了与原有格式的兼容ADOBE通过将ABC文件放入新加个特性标签DoABCDefine来实现.
它包含的ABC文件的基础信息和AVM2指令执行字节码。
大体的了解完SWF文件结构我们先来看看我们要拼的一个SWF里面有哪些内容:
说到这里,我们先拿一个空的SWF的二进制用十六进制查看:
46 57 53 0f 36 00 00 00 78 00 05 5f 00 00 0f a0
00 00 18 01 00 44 11 08 00 00 00 43 02 ff ff ff
bf 15 0c 00 00 00 01 00 e5 9c ba e6 99 af 20 31
00 00 40 00 00 00
首先我们看一下Header,SWF文件头:
Signature | UI8 | Signature byte: “F” indicates uncompressed “C” indicates a zlib compressed SWF (SWF 6 and later only) “Z” indicates a LZMA compressed SWF (SWF 13 and later only) |
Signature | UI8 | Signature byte always “W” |
Signature | UI8 | Signature byte always “S” |
Version | UI8 | Single byte file version (for example, 0x06 for SWF 6) |
FileLength | UI32 | Length of entire file in bytes |
FrameSize | RECT | Frame size in twips |
FrameRate | UI16 | Frame delay in 8.8 fixed number of frames per second |
FrameCount | UI16 | Total number of frames in file |
前4个分别为UI8类型是一个8位无符号int数据,第一个是说明压缩类型,F代表未压缩,C代表使用zlib的压缩算法,Z是Lzma压缩.后面两个是57 = “W” 53 =”S”,没有什么意义.第四个是版本号0f=”15’.
接着读取一个32位的UI32文件长度,UI32是little-endian类型的 所以在解译前要先将其顺序调转过来36 00 00 00调转后即为00 00 00 36 即文件总长度为54(注意 这里的长度指得是解压后的长度,用以校验解压后SWF文件是否正确).
RECT
Nbits | UB[5] | Bits in each rect value field |
Xmin | SB[Nbits] | x minimum position for rect |
Xmax | SB[Nbits] | x maximum position for rect |
Ymin | SB[Nbits] | y minimum position for rect |
Ymax | SB[Nbits] | y maximum position for rect |
接着是一个默认显示尺寸的RECT它是一个Bit型数据所组成的,前5位表示后面每位占多少位. 78展开二进制为01111000余3位还要继续解 15 * 4 - 3=57位又因为接着的类型是UI16是位对其类型 所以最后一个字节要检查是否要补0,这里57 % 8 = 1 在第八位处补7个0 展开接着的8个字节 00 05 5f 00 00 0f a0 00 =
00000000 0000|0101 01011111 000|00000 00000000 00|001111 10100000 0|0000000
Xmin = 0;Xmax = 11000 (11000 / 20 = 550像素)
Ymin = 0;Ymax = 8000; (8000/ 20 = 400像素)
值得注意的是:在文件中不以像素记录位置尺寸信息,而是以(twips)1/20像素的度量单位来记录,所以我们会看到flash支持0.x像素内容的呈现.
接着是默认帧率 读取是按UI16读,但字段说明里指明这里特殊一下 按8.8定点数读所以00 18 = 18 00 前面8位代表整数后面8位代表小数 帧率位24
接着是舞台上的帧数 UI16类型 01 00 = 00 01 = 1
头部解出来就是下面的数据:
其他的TAG和上面的方法相同就不再详细赘述.
接下我们来看下DefineBitsJPEG3:
Field | Type | Comment |
Header | RECORDHEADER (long) | Tag type = 35. |
CharacterID | UI16 | ID for this character. |
AlphaDataOffset | UI32 | Count of bytes in ImageData. |
ImageData | UI8[data size] | Compressed image data in either JPEG, PNG, or GIF89a format |
BitmapAlphaData | UI8[alpha data size] | ZLIB compressed array of alpha data. Only supported when tag contains JPEG data. One byte per pixel. Total size after decompression must equal (width * height) of JPEG image. |
第一个是类型Tag type = 35就是DefineBitsJPEG3,第二个Length是整一个数据块的总长度,ImageSize是图片的长度,也就是说,从这个长度后面就是透明通道的数据.这里的Id是用来在SymbolClass中索引他的CLASSNAME的,比如:
讲到这里我们就要来说,AS3导出类了,因为在swf中的图片资源并不能直接使用,需要导出链接,所以每一张图片要对应一个AS3的导出类,这个类需要继承BitmapData.
我们先看下DoABC的格式:
Header | RECORDHEADER | Tag type = 82 |
Flags | UI32 | 是否载入就初始化 |
Name | STRING | 文件名 |
ABCData | BYTE[] | A block of .abc bytecode to be parsed by the ActionScript 3.0 virtual machine, up to the end of the tag. |
As3脚本文件都会被编译成abc文件然后再生成doABC标签,由Flash player直接解释执行的字节码.前3个值都没有什么太大的意义,主要是ABCDATA。
我们先来看看AVM2的运行机制:
在AVM2里函数句柄 和 函数体是分开定义的,函数句柄定义方法的基本信息比如返回值、参数、默认参数等等。函数体则记录函数被调用时所执行的操作,它有一组指令集合所组成。
在函数运行时虚拟机会位方法分配3个数据存储空间,一个叫操作堆(operand stack),一个叫域堆(scope stack), 一个叫本地寄存器(local registers),操作堆:存放指令集合与放置指令返回结果,域堆:存放函数所需要引用到的对象 ,本地寄存器:存放参数,函数变量等临时变量。AVM2用到的常量类型都会放在一个常量池里,后面用到的地方都是通过记录索引值来引用其常量值的,这些常量类型有以下几种:
Int,Uint,Double,String,Namespace,Null,Undefined. 存放整个运行过程中所用所有常量值。
指令可以在运行时创建新对象,而这些新对象将会放在堆(heap)中,只能通过访问对象的形式来访问堆(heap),在堆(heap)中的对象不再被引用时最终将会被虚拟机所回收。
运行时逻辑环境是由一系列的对象链表所组成,他们通过名字在运行进行定位查找,与栈类似,最后被压入栈顶的最先被搜索直到全局域.
接下来我们说一下,ABC文件定义:
abcFile
{
u16 minor_version 主版本号
u16 major_version 次版本号
cpool_info constant_pool 常量池
u30 method_count 函数句柄个数
method_info method[method_count] 函数句柄内容
u30 metadata_count 描述属性个数
metadata_info metadata[metadata_count] 描述属性内容
u30 class_count 类定义个数
instance_info instance[class_count] 类定义内容
class_info class[class_count] 类定义内容
一个类定义由两部分组成instance_info 与 class_info
u30 script_count script 定义script_info个数
script_info script[script_count] 内容
u30 method_body_count 定义函数结构体个数
method_body_info method_body[method_body_count] 内容
}
前面两个版本号没啥好说的,看下常量池的定义:
cpool_info
{
u30 int_count
s32 integer[int_count]//所有用到的int
u30 uint_count
u32 uinteger[uint_count]//所有用到的uint
u30 double_count
d64 double[double_count]//所有用到的double
u30 string_count
string_info string[string_count] //所有用到的字符串
u30 namespace_count
namespace_info namespace[namespace_count] // 命名空间
u30 ns_set_count
ns_set_info ns_set[ns_set_count] //命名空间集合
u30 multiname_count
multiname_info multiname[multiname_count]//命名空间集合的集合
}
常量池结构体展开如上,都是两个一组,以数量加内容的形式记录。唯一要注意得是各类型第一个值都是该类型在AS3中的默认值,比如int为0.
cpool_info 里面有u30 string_count,string_info string[string_count],放了所有字符串,
格式如下:
string_info
{
u30 size
u8 utf8[size]
}
打个比方:String[14],读出来Size = 7,那么Utf8[7] = 41 73 73 65 74 5F 30 = Asset_0.
在u30 namespace_count,namespace_info namespace[namespace_count],用来表示类的域的地方,格式如下:
namespace_info {
u8 kind //类型
u30 name //名称
}
类型表
CONSTANT_Namespace 0x08
CONSTANT_PackageNamespace 0x16
CONSTANT_PackageInternalNs 0x17
CONSTANT_ProtectedNamespace 0x18
CONSTANT_ExplicitNamespace 0x19
CONSTANT_StaticProtectedNs 0x1A
CONSTANT_PrivateNs 0x05
如:
namespace_info[7]
18 0E
Kind = CONSTANT_ProtectedNamespace 0x18
Name = 0E=14 = Asset_0,14到上面的String里面查就是Asset_0.
Namespace set 命名空间集,一个代表多个Namespace的集合结构体,由数字和一数组组成,前面一个数字代表多少个Namespace 后面数组里各数字代表引用常量池中Namespace的第几个对象,结构如下:
ns_set_info
{
u30 count
u30 ns[count]
}
Multiname由两字段组成,data[]字段具体内容与解析方式需要由kind的值来决定
multiname_info
{
u8 kind
u8 data[]
}
类型表
CONSTANT_QName 0x07
CONSTANT_QNameA 0x0D
CONSTANT_RTQName 0x0F
CONSTANT_RTQNameA 0x10
CONSTANT_RTQNameL 0x11
CONSTANT_RTQNameLA 0x12
CONSTANT_Multiname 0x09
CONSTANT_MultinameA 0x0E
CONSTANT_MultinameL 0x1B
CONSTANT_MultinameLA 0x1C
不同的类型决定的读取格式不一样,详细可以查一下avm2overview.pdf,这些类型决定了什么呢?举个例子:
QName 是编译时决定其值,比如:public var s : String;
RTQName 是namespace在运行时才能获知其值的类型 比如:
var ns = getANamespace();
x = ns::r;
RTQNameL 是名称和namespace都在运行时才知道其值的类型,比如:
var x = getAName();
var ns = getANamespace();
w = ns::[x];
在实际的字节码就是:
multiname_info[10]
07 01 0E
Kind = CONSTANT_QName 0x07
Ns = {
Kind = CONSTANT_PackageNamespace
Name = 空
}
Name = 0E = {Asset_0}
上面我们详细介绍了常量池的结果和各个参数的用处,接下来我们看下函数句柄method_info结构,由于篇幅问题就不再详细赘述,展开内容如下:
method_info
{
u30 param_count //指定函数的参数个数
u30 return_type //为常量池中multiname的索引值
u30 param_type[param_count] //为常量池中multiname的索引值
u30 name //函数名称指向常量池String的索引值
u8 flags //是一个标志位,用来决定一些类似于默认值,用到一些特殊指令之类
option_info //决定参数的类型
param_info param_names //参数的名字
}
method_body_info结构展开内容如下:
method_body_info
{
u30 method //一个指向method_info数组的索引值,用来指定该函数体对应的函数句柄
u30 max_stack //堆的上限值,表面执行过程中该函数用到的堆的个数
u30 local_count //指明本地寄存器的个数
u30 init_scope_depth //初始对象域深度
u30 max_scope_depth //最大对象域深度,本函数所使用对象域=max_scope_depth - init_scope_depth
u30 code_length
u8 code[code_length]//指令长度,指令集
u30 exception_count
exception_info exception[exception_count]//异常定义与异常信息
u30 trait_count
traits_info trait[trait_count]//特性列表
}
像我们资源里用到的类:
package
{
import flash.display.*;
public class Asset_0 extends flash.display.BitmapData
{
public function Asset_0()
{
super(0, 0);
return;
}
}
}
解析出来就是下面的样子:
method_body_info[3]
03 01 01 04 05 03 D0 30 47 00 00
Method = method_info[3]
max_stack 1 = 1
local_count = 1
init_scope_depth 4
max_scope_depth 5
code_length 3
codes [getlocal_0 pushscope returnvoid ]
Code[208, 48, 71]
exception_count 0
trait_count 0
method_body_info[4]
04 03 01 05 06 09 D0 30 D0 24 00 2A 49 02 47 00 00
Method = method_info[4]
max_stack 1 = 3
local_count = 1
init_scope_depth 5
max_scope_depth 6
code_length 9
Code[208, 48, 208, 36, 0, 42, 73, 2, 71]
codes [getlocal_0 pushscope getlocal_0 pushbyte byte{0} dup construct arg_count{2} returnvoid ]
exception_count 0
trait_count 0
上面的两个函数体都是类的默认构造函数我们不做特殊逻辑 所以根据默认格式写入即可,我们自定义图片类的主要数据包含在以下函数体中,它需要指定之前在外面JPEG3标签定义的资源名称以及他所依赖的一些类和父类信息压入scope中,在本例中我们自定义类型为Asset_0都是图片类所有继承关系是固定的 所以照着模版写入即可.
method_body_info[5]
05 02
01 01 04 13 D0 30 5D 0D 60 08 30 60 02 30 60 02
58 01 1D 1D 68 0A 47 00 00
Method = method_info[5]
max_stack 1 = 2
local_count = 1
init_scope_depth 1
max_scope_depth 4
code_length 19
Code[208, 48, 93, 13, 96, 8, 48, 96, 2, 48, 96, 2, 88, 1, 29, 29, 104, 10, 71]
codes [getlocal_0 pushscope findpropstrict { Asset_0 } getlex {DisplayObjectContainer} pushscope getlex {BitmapData} pushscope getlex {BitmapData} newclass {1} popscope popscope initproperty {Asset_0} returnvoid ]
exception_count 0
trait_count 0
好了,说到这里我们就可以自己用字节码拼出一个带可以导出连接的JPG3图片的SWF资源了,另外我们把角色的配置和一些编辑的信息(放大缩小变速等),也一起压缩放到SWF资源里,同样可以NEW出来,也就是说载完这个资源就什么都有了,便于管理.
压缩完:
压缩比接近50%,另外还有一个问题如何解决连接名同名的问题呢,在使用的时候可以把文件加载到单独的域里来确保连接名唯一.
最后讲在实现图片转换压入SWF时遇到的一个坑,一开始我们是把一张透明的PNG直接压缩成JPEG,_imageData = jpeg.encode(bmd2);放到SWF里发现有偏色,经过修改参数发现在转格式前先要将原始图片画成非透明再转才行.如下:
bmd2 = new BitmapData(_bmd.width, _bmd.height, false, 0x000000);
bmd2.copyPixels(_bmd, _bmd.rect, new Point());
_imageData = jpeg.encode(bmd2);
加上编辑器美术人员就可以编辑所需要的资源输出SWF文件了.