一、前言
近期好多人问起GMS1/2的texture page(以下简称纹理页面)与texture group(以下简称纹理组)方面的问题,这里我针对GMS精灵资源处理机制进行次系统性的讨论。
GMS编译的程序除included files和以stream方式读取的音频文件外,全部资源都会在程序执行时加载到内存里。所以很多人认为GMS只能做小游戏,实际情况并不是这样。
内存的占用绝大部分由精灵和音频这两部分组成。
音频方面上,GMS可以通过load/unload音效组或将音效资源以流读取方式(在IDE中将音效资源设置为stream类型)实现按需加载。需要注意的是,流读取方式相对较耗费CPU资源,所以建议只针对游戏背景音乐使用。而相对较短且使用频繁的音效资源(例如开枪音效),最好还是提前加载到内存里(非stream方式并在关卡开始时加载资源)以便提高执行效率。
二、精灵处理机制
关于精灵资源的处理上,YoYoGames结合了便捷、效率与资源等多方面因素进行了折中,选择将所有精灵资源整合成一张或多张纹理页面,然后再归纳到一个或多个纹理组的方式进行处理。其原理是将游戏中所涉及的所有精灵资源(非included files资源),尽可能紧凑有序的排序到一张或多张纹理页面里。目的是为了避免因GPU多次纹理页面swaps(纹理页面交换是要耗费GPU资源寻址的。而一个精灵无论其大小都相当于一张纹理页面,想想你游戏场景里会涉及多少张纹理页面)导致的效率问题。如果将多个精灵整合成一张或少数几张纹理页面的话,swap次数肯定会减少(甚至为1)。关于纹理页面的大小我们是可以设置的,从256x256到8192x8192(看平台支持情况,例如IOS最高只能支持到4096x4096)。这里可能有人会问纹理页面是越大越好啊,这样肯定会减少纹理页面swap的次数。这话说的没错,但纹理所占内存空间(即显存)同样需要注意。关于精灵所占用内存空间,下节会做详细的探讨。
这里要说明下,关于swap对性能的影响主要是在移动平台上,而对win平台direct11影响微乎其微(应该把重点放在像素填充率上)。
三、精灵资源空间
精灵资源内存空间的占用主要分为两部分:资源内存(暂且这么叫)和纹理内存(就是显存)。
1、资源内存方面,GMS是将纹理页面压缩成png格式存储到编译后的游戏执行文件里。就算在游戏过程中不加载纹理页面至显存(就是不绘制任何精灵),纹理页面.png文件也要加载到内存中。除非精灵资源是以included files形式存在(注意,外置精灵文件不会整合到纹理页面里)。
2、而显存方面,YoYoGames为了让GMS绘制无损的2D画面而将texture page解压为bitmap加载到显存中。
我举个简单点的例子。就精灵占用内存而言,比如某游戏只有一张2048x2048的texture page。经过GMS压缩成png后假设占用2MByte内存。当游戏运行开始时并没有绘制任何精灵,但此时游戏中精灵占用的2MByte内存是不会变的。当游戏绘制精灵时,程序会将这整张2048x2048的texture page解压成bitmap格式加载到显存中(即16MByte)。所以此时游戏中精灵所占的内存为2M+16M=18MByte。鉴于多数PC或移动平台的内存与显存都为共享式,所以你完全可以认为精灵占用的内存为这两部分的总和,即18MByte。
另外,目前版本的GMS在编译时可以通过一条简单命令gml_pragma("PNGCrush")来针对纹理页面png文件进行深度压缩,即减少精灵所占用的资源内存(显存还是以bitmap方式存在)。唯一的缺点是会增加工程的编译时间。所以大家在日常测试中可以关闭这个功能,等到在游戏正式发布时再打开也不迟。
关于pngcrush方面的技术细节,大家可以通过网站来了解:https://pmt.sourceforge.io/pngcrush/
四、纹理页面分配
之所以不建议在游戏中用included files直接加载并创建精灵的原因是,included files加载的精灵无论其大小,都会占用一张单独的纹理页面。这样肯定会增加 swaps而产生效率问题。试想你所设计的游戏中某一场景要涉及多少个精灵。主角、敌人、物品、GUI、背景等等,每个精灵的绘制就会产生一次纹理页面交换。而整合到纹理页面(系统自动分配)很大程度会大大减少swaps的数量,但也需要你做好规划与设计。千万不要认为两张纹理页面就只会出现2次swap,两张纹理页面出现的swap数量是无穷大的。举个例子,假设主角、敌人、粒子精灵属于纹理页面A;武器、道具、GUI页面、字库等属于纹理页面B。游戏中绘制顺序可能是:主角(A)->主角武器(B)->敌人(A)->敌人武器(B)->战斗的粒子效果(A)->物品(B)->天气等粒子效果(A)-> GUI页面绘制(B)->字库调用显示(B)。大家看看一帧里出现了多少次纹理页面交换,答案是8次swap。
当然这个例子比较极端,我只是想凸显下多个页面swap问题的严重性。其实我们完全可以通过合理的设计和分配来减少swap次数。总结起来需要做到两点:纹理页面大小与纹理组规划与设计。
注意:默认情况下我都认为一个纹理组里只有一张纹理页面。因为两张以上的话就会出现我刚才所提的情况,不可控也不合理。
1、关于纹理页面大小的设计相对简单,只要尽可能让关卡内所有精灵资源能放到一张或两张纹理页面中即可。但也要在满足你关卡需求的情况下,越小越好(降低显存的占用空间)。一般情况下2048x2048与4096x4096相对较主流。
2、而纹理组的设计方面就相对较复杂了,这里我先谈谈我的原则。我一般将纹理组分为两类:通用类与关卡类。
1)通用类的精灵资源主要为GUI界面,字库,主角以及相关精灵资源等等。总而言之,凡是所有关卡中重复出现的精灵资源全都到放到这个组里,即尽可能让一张纹理页面(一个组一张纸很重要)囊括所有资源。
2)关卡类顾名思义,是将关卡所特有的全资源放到关卡组里。不同特点的关卡或区域都可以设计成单独的纹理组。
听起来比较容易,但你可能没有仔细思考过。如果关卡1的敌人出现在关卡2或关卡3中会怎么样,如果某一关卡要出现前面几个关卡中敌人又如何。
暂且不谈swap对性能的影响,单显存占用就会让你的游戏轻而易举崩溃。要知道一张2048x2048的纹理页面要占用16MByte的显存空间。如果一个关卡中要加载10张纹理页面的话,显存空间就要占用160MByte(还没算上游戏所占的内存空间呢),这对于手游平台来说已经不算小了(例如IpadMini2只有1G内存空间,程序内存占用超过500MByte就会崩溃)。我想这个问题对于策划者来说绝对是头疼的,难道这个问题真的就没法解决吗?答案是否定的。这个问题从未是GMS引擎的障碍,以前不是,以后也绝对不是。
五、进阶处理方法
如果你只想做个小型游戏的话,以下章节可以直接越过。否则不但解决不了您的问题,可能还会增加疑惑。
通过咱们讨论完上述内容后,大家可能会觉得GMS对精灵资源的处理机制很不灵活。例如,工程中所有纹理页面不能按需加载。游戏过程中只能同步加载并创建精灵(因为同步的原因,会引起cpu中断导致游戏短暂卡顿)。本章节我们的目的就是如何避免这两个问题。
我们的目的很明确,就是如何在不影响效率的情况下尽可能占用少的内存资源。首先来说,纹理页面以bitmap的方式加载到显存中是无法避免的,这是GMS内部机制。当然这样做的目的是可以绘制出无损的2D精美画面。所以我们所能做只有按需加载纹理页面(不要对比3D引擎中对纹理的压缩处理,没可比性),以及如何在游戏过程中异步加载精灵资源这两方面了。
六、精灵按需加载
本章要解决的问题是纹理页面动态组合并加载。理想情况是在进入某关卡前,将本关卡的精灵资源通过included files的方式动态加载,随后通过某种算法将其所有精灵资源组整合成一张纹理页面中,这样我们就不用担心某关卡里避免出现其他关卡的精灵了。
实现方式很简单,15年时YoYoGames社区里一个叫Braffolk的朋友发布了一个Custom Sprite Framework插件。目前插件已发布在marketplace中,并且为免费。下载地址(需注册账户):https://marketplace.yoyogames.com/assets/4543/custom-sprite-framework
此插件的功能非常强大,在这里就不做详细介绍了。下面只举个简单的例子,便于大家认识与理解:
1、系统初始化。游戏执行中只需做一次。
image_system_init();
2、创建纹理页面组
image_group_create("main");
//创建页面组
image_stream_start("main",2048,2048,0);
//设置页面组大小为2048x2048,且精灵之间无间隔像素。
3、加载精灵资源
image_stream_add("main","player","player.png",subimg,xorg,yorg);
//读取精灵。其中subing为帧数,如果设置为2插件会将player.png图片横向等分切开,分成两帧存储。而xorg和yorg是精灵的中心。
image_stream_finish("main");
//读取结束并将所有精灵整合到纹理页面中。
4、存储精灵索引
img_player = image_group_find_image("main","player");
//提取纹理页面中精灵资源。
5、绘制精灵
draw_image(img_player,image_index,x,y);
//绘制精灵。
6、回收资源
image_group_clear("main");
//清除并不删除组。下次可以直接用组名加载精灵资源。
image_group_destroy("main");
//不用时可以删除组。下次需要重新创建。
七、精灵动态加载
当游戏背景不能通过tile展现时,异步加载背景就显得很重要了。再或者对于tile和个性化背景相结合的工程,我们不想在游戏开始时将整个关卡的背景资源一股脑的加载到内存里。不幸的是GMS的sprite_add函数为同步加载函数(只有在加载http资源时为异步),加载时会引擎短暂的卡顿。所以我们需要想办法来实现精灵的异步加载。
我直奔主题,简单介绍下目前可以解决的方法与流程:
1、首先将sprite精灵以bitmap的格式存储为bin文件,然后通过IDE放置到included files里作为外部精灵资源,以便动态加载。
2、当游戏过程中需要绘制外部精灵资源时,首先通过buffer_load_async异步读取资源到buffer里。
3、然后通过buffer_set_surface将buffer转换为surface。
4、最后再利用sprite_create_from_surface将surface转换为精灵。是否将表面转换为精灵看个人需求,也可直接绘制表面。
下面我再举个简单例子介绍下如何实现:
1)先对精灵进行二进制格式转换。
可以将这部分内容写成一个脚本scr_sprite_save(精灵索引,帧数,文件存储位置)。
var width = sprite_get_width(argument0);
var height = sprite_get_height(argument0);
var xoffset = sprite_get_xoffset(argument0);
var yoffset = sprite_get_yoffset(argument0);
var sur = surface_create(width, height);
surface_set_target(sur)
draw_sprite(argument0, argument1, -xoffset, -yoffset);
surface_reset_target()
buff = buffer_create(width * height * 4, buffer_fixed, 1);
buffer_get_surface(buff, sur, 0, 0, 0);
buffer_save(buff, argument2);
surface_free(sur);
buffer_delete(buff);
return buff;
2)异步加载精灵
surf = surface_create(1024,1024);
buff = buffer_create(4194304,buffer_fast,1);
loadid =buffer_load_async (buff,"sprite.bin",0,4194304);
3)生成精灵(异步Save/Load事件)
if ds_map_find_value(async_load, "id") == loadid
{
if ds_map_find_value(async_load, "status") == false
{
buffer_set_surface(buff,surf,0,0,0);
//经测试除WIN与PS4平台外IOS平台也可用(android未测试)
myspr = sprite_create_from_surface(surf,x,y,w,h,removeback,smooth,xorig,yorig);
}
}
感谢!
好文章, 解决了大问题