本手册旨在广泛介绍 Starling 框架. Starling是一个可用于各种应用程序, 特别关注2D游戏的ActionScript 3跨平台引擎.
免费下载此文档的 PDF 版本. |
在本手册中你将学习:
-
Starling使用的技术及其遵循的原则.
-
如何选择IDE并建立第一个项目.
-
显示列表,事件和动画系统等基本概念.
-
如何挖掘片段和顶点程序的潜力等进阶技术.
-
如何发挥框架的最佳性能.
-
如何将游戏运行在手机和平板电脑上.
继续深入阅读!
1. 开始
本章将对Starling所基于的技术进行概述. 首先简要介绍一下Flash和AIR的历史,以及Starling是如何诞生的. 然后介绍使用Starling开发时所需要的工具和资源. 在本章的最后,运行第一个“Hello World”示例程序.
1.1. 简介
首先:欢迎来到Starling社区! 自2011年首次发布以来,得益于小红鸟的帮助,来自世界各地的开发人员创造了大量令人惊奇的游戏和应用程序. 太棒了,欢迎加入我们!
你找对地方了:本手册将教你所需要知道的. 我们将涵盖从"A"(Asset management资源管理)到"Z",额,Zen-like编码.
我们将从头开始,从一个小游戏入手来对框架有一个初步的感觉. 然后我们将详细讨论所有的概念,包括显示列表架构和Starling的动画系统. 后面的章节包含了Starling高级用户的技能,例如如何编写自定义渲染代码. 在这之后你将玩转Starling!
但是,有一个小前提条件:您应该对ActionScript 3(AS3)有基本的了解. 不要担心:如果你使用过任何其他面向对象的语言,你会快速的掌握它. 有很多书和教程可以带你飞.
如果一定要推荐一本书的话,我会推荐Colin Moock的"Essential ActionScript 3". 当我开始使用AS3,它教我所有重要事项(特别是AS3古怪的基于E4X标准的 XML语法处理)。它一直在我书架上伴随着我.
1.2. 什么是Adobe AIR?
就在几年前,Adobe的Flash Player在web浏览器界如日中天,无处不在. 当时,如果你想创建具有互动和动画内容的网页,它基本上是唯一的选择. 浏览器在视频,声音和动画方面的能力非常有限; 并且开发者经常因为少数功能不被浏览器兼容所困扰. 总之:情况是一团糟.
这就是为什么Adobe Flash如此受欢迎的原因. [1] Adobe Flash允许设计师和开发人员在直观的创作工具(现在称为Adobe Animate)中创建多媒体内容,并确保它在所有平台上看起来一样. Flash使用ActionScript 3编程,它是一门易于学习, 并且非常强大的编程语言.
基于其平台的普及,Adobe意识到,需要使用相同的技术,使得应用程序运行在浏览器之外. 这就是Adobe AIR运行时的目标. 使用AIR SDK构建的独立应用程序可以运行在桌面平台(Windows,macOS)或移动平台(Android,iOS)之上. AIR的标准库是Flash的一个超集,任何你能在Flash中做到的事情,在AIR中同样可以做到;不仅如此,AIR还为文件系统访问或窗口管理等功能提供了大量额外的API.
当然,当你想创建一个桌面应用程序时,你需要一种方法来创建一个图形用户界面,对吧? 由于标准的Flash不是非常适合这个任务,所以这项任务交给了另一个SDK来承担:Flex(现在的Apache Flex). Flex还引入了基于XML的标记语言(称为MXML)来定义用户界面布局.
对于Starling,您不需要Flex SDK,只需要AIR SDK. |
1.2.1. Flash和AIR的现状
AIR是"富互联网应用"(RIA)这个术语的一部分 - 这是一个流行语,在2000年末曾风行一时. 曾经,Adobe的AIR和Microsoft的Silverlight(以及Sun的JavaFX)之间存在激烈的竞争. 那么到今天,当热潮已经退去,结果又如何呢. 显然, 获胜者是HTML5 / JavaScript,它是使用Web技术构建应用程序时最受欢迎的技术. 事实上Adobe也遵循这一趋势,并且为其产品增加了越来越多的HTML5支持.
当谈到软件开发时,不要陷入人云亦云的陷阱. 对于每个问题,会有多个解决方案; 也许其中一些不适合别人,但更适合你. 选择你最觉得最顺手的工具和方式; 让自己专注于想要创建的软件. |
即使AIR / Flash可能不再是被众人所羡慕的那个"别人家的孩子", 但它仍然是一个用于构建软件非常有吸引力的平台.
-
相比HTML5的各自为政的分散现象,以及比Lady Gaga的裙子变得更快的类库来说,AIR / Flash是非常成熟和易于使用的技术平台.
-
它带有一个广泛的标准库,提供日常开发所需的所有工具.
-
Flash插件虽然在一般网站上使用频率表现为衰退,但仍是网页游戏的标准. 例如,Facebook上的大多数游戏仍然使用Flash构建.
-
特别是Flash与Starling和Feathers相结合,它为真正的跨平台开发提供了最平滑的途径之一(以同一份代码为基础,可运行于所有主要桌面和移动平台).
谈到Starling …它是如何对应这张图片的?
1.3. 什么是Starling?
Starling 框架允许您在ActionScript 3中创建硬件加速应用程序. 主要目标是创建2D游戏,但Starling可用于任何图形应用程序. 借助于Adobe AIR,基于Starling的应用程序可以部署到所有主要的移动和桌面平台.
虽然Starling模拟了Adobe AIR / Flash的经典显示列表架构,但它提供了更好的性能:所有对象都由GPU直接渲染(使用Stage3D API). Starling完成了一整套架构设计,能更好的与GPU协同工作,集成了对常用的游戏开发任务的核心支持. Starling对开发人员隐藏了Stage3D内部接口,但是对于需要完全性能和灵活性的用户来说,它也很容易访问.
1.3.1. 为什么要另一个显示API?
如上所述,Starling的API与本机Flash API非常相似,即:flash.display软件包. 所以你可能会问:为什么要花费巨大的努力来重建Flash内部结构…,这样做真的好吗?
原因是:原生的flash.display API,它灵活的显示列表,矢量能力,文本呈现,滤镜等等,都是针对台式计算机时代设计的. 这些台式计算机拥有强大的CPU和非常原始的图形硬件(相对现代标准). 但是,今天的移动硬件具有几乎颠倒的设置:拥有非常先进的图形芯片和相对较弱的CPU(即节省电池的).
那么问题来了:如何让原有针对纯CPU渲染设计的API能够快速有效地利用GPU,显然,这是是非常困难的(即使有可能的话). [2] 这种尝试失败了,因此,Flash插件也一直未能在手机和平板电脑上的浏览器中出现.
值得称道的是,Adobe非常清楚这个问题. 这就是为什么,在2011年,他们推出了一个称为Stage3D的低级图形API. 这个API确实是够低级的; 它基本上是一个OpenGL和DirectX的简单包装,允许开发人员访问GPU的原始功能而已.
那么问题又来了:这样一个低级的API并没能帮助用户利用上已有的经典显示列表,至少不能方便的使用. 正因为Stage3D API是如此低级,所以它不是一个典型的开发者可以(或应该)用来直接创建应用程序或游戏的API. [3] 显然,Adobe需要一个建立在Stage3D之上,但是像’flash.display’一样容易使用的更高级的API.
嗯…是时候了,Starling进入舞台了(一语双关)! 它是基于Stage3D同时尽可能模仿经典的Flash API而设计的. 这使得它既有可能充分利用现代强大的图形硬件,又可以使用无数的开发人员已经熟悉的概念.
Adobe当然可以自己制作这样的API. 然而,由大公司构建的API具有变得膨胀和不灵活的毛病. 一个小型的开源项目,加上一个公平开放、交流活跃的开发者的社区,可以更迅速地达成目的. Flash和AIR平台产品经理Thibault Imbert洞悉了这一切, 2011年,Starling项目在他的带领下由此开启.
直到今天,它仍由Adobe资助和支持.
1.3.2. Starling的核心观念
Starling的核心目标之一是使其尽可能轻量和易于使用. 在我看来,开源库不应该只是易于使用 - 它也应该鼓励开发人员深入阅读代码. 我希望开发人员能够了解幕后的情况; 只有这样,他们才能够扩展和修改它,直到它完全满足需求.
所以Starling的源文件都有据可查,简明扼要. 由于只有大约15000行代码,它可能比大多数游戏源码更小!
我真的想强调:如果你有一天卡住或困惑为什么你的代码不能正常工作,不要犹豫,阅读Starling的源代码. 通常,你会很快看到发生了什么问题,你会更好地理解它的内部实现机制. |
Starling的另一个重要目标当然是它尽量亲近本机显示列表架构. 这不仅是因为我真的很喜欢显示列表背后的整个思路,还使开发人员能够轻松地过渡到Starling.
然而,我从来没有试图创建一个完美的复制品. 针对GPU编程需要具体的概念,那些概念应该被突出! 例如,纹理和网格的概念旨在与原始API无缝融合,就像它一直是为GPU设计的.
1.4. 选择IDE
正如你刚刚读到的,Starling应用程序和游戏是使用Adobe AIR SDK构建的. 从技术上讲,您可以使用文本编辑器和命令行来编译和部署代码,但不建议这样做. 相反,你肯定要使用集成开发环境(IDE). 这将使调试,重构和部署更容易. 幸运的是,有几个集成开发环境可供选择. 让我们看看这些方案!
1.4.1. Adobe Flash Builder
以前称为Flex Builder,这是由Adobe创建的IDE. 您可以将其作为独立版本(标准版和高级版)购买,也可以将其作为Creative Cloud订阅的一部分进行购买.
它建立在Eclipse之上,是一个非常强大的软件,包含了你想要的所有功能,如移动调试和重构. 高级版甚至包括一个非常有用的性能分析器.
就个人而言,我使用Flash Builder很长时间,Starling下载包中甚至附带了合适的项目文件. 但是,有一条消息你需要知道:Flash Builder显然已被Adobe放弃,它最后一次发布更新(4.7版)是在2012年年底. 它不是特别稳定;并且这种状况可能会一直持续.
因此,我只是推荐它,如果你拥有一个旧许可证,或者如果你是一个Creative Cloud用户(反正因为那样,你会免费得到它). 不要误会我,我推荐它是因为它有一系列非常棒的功能,你将得益于它从而完成你的工作. 如果你不在意偶尔遇到崩溃,或者觉得更新AIR SDK会很麻烦的话.
-
平台:Windows,macOS
-
价格:249美元(标准版),699美元(高级版)
1.4.2. IntelliJ IDEA
下一个候选者可能被称为"IDE终结者", 因为IDEA支持大量的语言和平台. AIR通过安装"Flash/Flex支持"插件来搭建适合的开发环境.
我使用IDEA有很长一段时间,我真的很喜欢它(特别是对于强大的重构功能). 所有重要的部分都很到位。感觉它就像天生是为AS3而存在的.
与Flash Builder不同,此IDE会定期接收更新. 不幸的是,Flash插件不是这样. 特别是有一些(次要的)问题等待相当一段时间仍未修复.
这些都是小问题,尽管如此. 它仍然是一个很好的IDE,如果你正在使用macOS做开发的话,建议使用它. 唯一的坏消息可能是JetBrains最近切换到基于订阅的定价模型,这可能不是每个人都喜欢.
还有一个免费的社区版IDEA,但不幸的是它尚未集成Flash / Flex插件.
-
平台:Windows,macOS
-
价格: 499美元(第一年),399美元(第二年),299美元(第三年)
订阅模式包含所谓的“永久备用许可证”,这意味着在12个月后,即使您取消订阅,您仍然可以保留当前版本而继续使用IDEA. 就个人而言,我认为这弥补了订阅模式的缺点. |
1.4.3. FlashDevelop
我几乎都在MacOS上工作,这里有一个我羡慕Windows用户的地方: 作为Starling开发者,他们可以使用一个优秀免费(开源)的IDE:FlashDevelop. 它自2005年以来一直存在,仍然定期看到更新。 如果你使用Haxe,它一样也适用.
由于我主要使用macOS,我没有很多第一手FlashDevelop的实践经验; 但通过Starling论坛的无数帖子,我看到的都是说好的. 有些人甚至通过虚拟机,如Parallels(译注:一款虚拟机软件,可以让用户在Mac上安装Windows10/8/7/XP、Linux 等操作系统及应用)在Mac上使用它.
-
平台:仅Windows
-
价格:免费并且开源
1.4.4. PowerFlasher FDT
就像Flash Builder一样, FDT也是基于Eclipse平台构建. 因此,当你不再使用Flash Builder时,它是一个很好的选择,因为它看起来和Flash Builder非常相似. 您甚至可以导入所有Flash Builder项目.
FDT相对Flash Builder在多个方面有所改进; 例如,您可以轻松地将项目从Flash工程切换到AIR工程 - 这在Flash Builder中是不可能的. 它还支持多种其他语言,如HTML5 / JavaScript,Haxe和PHP.
总而言之,它是一个非常可靠的IDE. 如果你喜欢Eclipse,你不能错过FDT!
FDT甚至推出了一个免费版本,这是一个极好的开始。 它不仅可以用于页面产品开发,您还可以将其用于移动AIR开发. |
-
平台:Windows,macOS
-
价格:每月25美元至55美元(取决于合同时长)。 学生和教师可以申请特惠条款.
1.4.5. Adobe Animate
如果你是一个使用Flash很长时间的设计师或开发人员,你可能会想知道Adobe Flash Professional在这个IDE列表中的位置. 好吧,这里就是! 如果你不认识它,这是因为Adobe最近把它重命名为Adobe Animate. 这实际上有重要意义,因为新的名称反映了Adobe战略思路的重大变化. Animate现在成了一个通用的动画工具,不仅支持将动画输出成Flash,还支持HTML5,WebGL和SVG等其它格式.
W虽然你可以使用Animate来编写Starling程序,但我不推荐它. 这是一个提供给设计师的神奇工具,它并不是为编写代码而存在的. 你使用它只是单纯的为了更好的设计图形,编写代码还是建议用上述其它IDE.
-
平台:Windows,macOS
-
价格:对Creative Cloud订阅者免费
1.5. 资源
我们当然已经准备好了所有的资源,万事俱备,只欠东风。 现在让我们看看怎么才能开始第一个项目,不是吗?
1.5.1. AIR SDK
如果您选择并安装了IDE,下一步是下载最新版本的AIR SDK. Adobe每三个月发布一个新的稳定版本,所以一定要保持最新! 每个新版本通常修复了几个重要的错误,这对于保持与最新移动操作系统的兼容性尤为重要. 你也将看到其团队不断发布新的功能,而我努力让Starling跟上步伐.
最新版本可以从这里下载: Download Adobe AIR SDK
Starling 2需要AIR 19及以上. |
1.5.2. Flash Player Projector
如果你的项目打算在Flash Player中运行,我建议你下载它的独立版本,称为Projector(包含Debug和Release版本). Projector的优点是方便调试. 当然,你也可以通过浏览器调试(安装调试版插件后) - 但个人而言,我觉得那样非常麻烦. 独立版本播放器启动要快得多,您无需配置任何HTML文件就可以使用.
此页面包含适合开发人员的所有Flash Player版本的列表。 打开页面后查找 "projector content debugger": Adobe Flash Player Debug Downloads
调试版运行速度明显比标准版本慢。 在进行性能优化时,请记住这一点. |
同样,你的IDE可能需要知道如何链接到特定的播放器. 例如,在IDEA中,此设置是调试配置的一部分; 其他IDE可能只使用系统默认值. 在任何情况下,务必保证播放器版本不低于AIR SDK编译SWF时指定其运行所需的最低版本.
1.5.3. Starling
到目前为止,还需要下载的只有Starling本身了. 你有两种方式可以选择.
-
从以下地址下载最新版本的zip文件: gamua.com/starling/download.
-
克隆Starling的Git存储库.
前者的优点是它附带了一个预编译的SWC文件(位于文件夹starling / bin中),该文件很容易链接到您的项目. 但是,你使用的是本地稳定版本,这意味着你无法使用包含了新功能或修复了某些bug的热更新版本! 因此,我推荐使用Git.
假如你报告了一个错误,这个错误在几天后被修复了(是的,确实发生过!),你必须等待我修复其它一些问题,直到我创建一个新的稳定版本,提供新的标准库swc下载,这可能需要一段时间. 但是如果你使用Git仓库,错误会立即得到修复. |
深入了解Git不是本手册要讲述的内容,网上有很多关于它的教程. 安装Git后,可以使用以下命令将完整的Starling存储库下载到本地磁盘:
git clone https://github.com/Gamua/Starling-Framework.git
Starling框架将存储在’Starling-Framework’文件夹下面.
查找实际源代码子文件夹 starling/src
.
所有IDE都支持将此文件夹指定为项目的源路径,完成后Starling将成为目标工程的一部分.
这不会比链接到SWC文件更复杂, 并且还有另外一个好处,你可以在调试中进入Starling源代码.
这将非常容易就把Starling更新到最新版本,这是最好的办法. 只需定位到存储库的根目录并执行’pull’操作:
cd Starling-Framework git pull
这比打开浏览器和手动下载新版本简单得多,不是吗?
给高级Git用户的一些附加信息:
|
1.5.4. 获得帮助
常在江湖走,哪有不湿鞋. 你可能会因为Starling中的一个错误而碰壁 ,或者因为遇到一个似乎不可能解决的问题而困顿. 无论哪种情况,Starling社区不会让你一个人孤单寂寞冷! 此时,这里有一些资源,会对你有所帮助.
- Starling 论坛
-
这是Starling社区的最重要的阵地. 每天有几十个新帖子,您的问题很可能之前已被问过,所以一定要利用搜索功能寻求答案. 如果这没有帮助,请随时注册一个帐户,并发帖求助. 这会是您在网上能找到的最友好和最有耐心的社区之一!
→ http://forum.starling-framework.org - Starling 手册
-
您正在阅读的这本在线手册. 我尽力保持与Starling新版本同步更新.
→ http://manual.starling-framework.org - Starling Wiki
-
维基包含Starling各相关主题的链接和文章,最重要的是:Starling扩展列表. 我们将在后面讨论一些.
→ http://wiki.starling-framework.org - API 参考
-
不要忘记查阅Starling API参考,了解有关所有类和方法的详细信息.
→ http://doc.starling-framework.org - Gamua Blog
-
通过Gamua博客了解Starling的最新消息. 当谈到写博客文章,我必须承认我有点懒,但每个Starling版本总是至少有一篇.
→ http://gamua.com/blog -
我使用了几个社交网络,最好的方式是通过@Gamua与我保持连接. 跟踪这个帐户以获得关于Starling的最新进展或链接到其他基于Starling技术创作的游戏.
→ https://twitter.com/Gamua
1.6. Hello World
好了,说了这么多背景信息. 是时候开始干活了! 没有比一个经典的“Hello World”程序更管用的开始方式. 本手册如果没有一个岂不是不完整?
1.6.1. 清单
以下是您应该已经准备好的快速摘要:
-
选择并下载IDE.
-
下载了AIR SDK的最新版本.
-
下载最新版本的Flash Player独立版播放器.
-
下载最新版本的Starling.
-
配置您的IDE以适用正确的SDK和播放器.
配置IDE和设置项目对每个IDE来说都略有不同. 为了帮助您,我在Starling Wiki中的为每个IDE创建了一个特定的操作方法: Starling Wiki. 请按照相应的教程,然后继续.
诚然,这些设置流程都是那么的让人讨厌. 但是相信我:你需要做的其实很少. |
1.6.2. 启动代码
在IDE中创建一个新项目或模块; 建议您创建一个Flash Player项目(目标平台: Web),并且命名为 "Hello World". 作为初始化过程的一部分,IDE还将为您设置一个最小的启动类. 让我们打开并修改它,如下所示. (通常,该类的命名类似于您的项目名,因此请使用合适的类名替换下面的类名.)
package
{
import flash.display.Sprite;
import starling.core.Starling;
[SWF(width="400", height="300", frameRate="60", backgroundColor="#808080")]
public class HelloWorld extends Sprite
{
private var _starling:Starling;
public function HelloWorld()
{
_starling = new Starling(Game, stage);
_starling.start();
}
}
}
此代码创建了一个Starling实例并立即启动它. 注意,我们将“Game”类的引用传递给Starling构造函数. Starling将在准备好之后实例化该类. (这样做的原因是你不必考虑代码运行的先后顺序。)
当然, 那个类还是需要先写. 向您的项目添加一个名为Game的新类,并添加以下代码:
package
{
import starling.display.Quad;
import starling.display.Sprite;
import starling.utils.Color;
public class Game extends Sprite
{
public function Game()
{
var quad:Quad = new Quad(200, 200, Color.RED);
quad.x = 100;
quad.y = 50;
addChild(quad);
}
}
}
类仅仅显示一个简单的红色矩形,以检测我们是否配置好基本的开发和运行环境.
注意Game类扩展自starling.display.Sprite,而不是flash.display.Sprite! 这是至关重要的,因为我们现在处于Starling世界. 它完全独立于flash.display软件包. |
1.6.3. 首次运行
现在启动项目. 可能有一些人会遇到出人意料的结果,因为你看到一个错误消息,像这样:
这种情况很可能是因为打开了浏览器,而不是独立的Flash Player. 检查运行/调试配置,并确保使用Flash Player Projector(调试版本),而不是浏览器. 这个问题应该会被解决.
2. 基本概念
Starling可以说是一个紧凑的框架,但它仍然包含大量的包和类. 它是围绕以下几个基本概念构建的,它们旨在互补和利于扩展. 他们组织在一起共同为您提供一套工具,使您能够创建你能想象的任何应用程序.
- 显示编程
-
在屏幕上呈现的每个对象都是显示对象,被组织在显示列表中.
- 纹理和图像
-
为了将像素,表单和颜色带到屏幕,你将学习使用纹理和图像类.
- 动态文本
-
动态文本的渲染几乎是每个应用程序的基本任务.
- 事件处理
-
通信是关键! 你的显示对象需要彼此通信,他们可以通过Starling强大的事件系统来做到这一点.
- 动画
-
给图形带来一些缓动! 有不同的策略来缓动你的显示对象.
- 资源管理
-
了解如何加载和组织各种资源(如纹理和声音).
- 特殊效果
-
效果和过滤器,将使你的图形质量脱颖而出.
- 实用工具
-
一些帮助工具类,使你在创作程序时更轻松.
我们的征途是星辰大海! 所以,用超级马里奥的话说: "Lets-a-go!"
2.1. 配置Starling
每个Starling应用程序的第一步:创建一个Starling类的实例(包名starling.core). 下面是Starling的构造函数的完整声明:
public function Starling(
rootClass:Class,
stage:Stage,
viewPort:Rectangle = null,
stage3D:Stage3D = null,
renderMode:String = auto,
profile:Object = auto);
- rootClass
-
一旦Stage3D完成初始化,必须实例化的类。 根类是以下类的一个子类
starling.display.DisplayObject
. - stage
-
传统的Flash通过此类承接Starling的内容。 这就是Starling和Flash显示列表相互连接的关键所在.
- viewPort
-
定义Starling渲染的一个区域(Flash Stage中)。由于这通常是完整的大小,您可以忽略此参数(即传递null)以使用完整区域.
- stage3D
-
用于渲染的Stage3D实例。每个Flash阶段可能包含多个Stage3D实例,您可以选择其中任何一个。然而,默认参数(null)将足以满足大多数需求.
- renderMode
-
Stage3D背后的整体理念是提供硬件加速渲染。但是,还有一种软件回退模式;它可能会通过传递Context3DRenderMode.SOFTWARE来强制。建议使用默认(自动),但这意味着软件渲染仅在没有其他选项时使用.
- profile
-
Stage3D提供了一组分组到不同配置文件(定义为Context3DProfile中的常量)的功能。应用程序运行的硬件越好,可用配置文件越好。默认(自动)是选择最好的可用配置文件.
大多数这些参数具有正确的默认值,因此您可能不需要指定所有这些值。下面的代码显示了启动Starling的最直接的方法。这是Flash Player或AIR项目的Main Class.
package
{
import flash.display.Sprite;
import starling.core.Starling;
[SWF(width="640", height="480",
backgroundColor="#808080",
frameRate="60")]
public class Main extends Sprite
{
private var _starling:Starling;
public function Main()
{
_starling = new Starling(Game, stage);
_starling.start();
}
}
}
注意类扩展自flash.display.Sprite,而不是Starling变量. 这是AS3中所有Main类的必要条件. 然而,一旦Starling完成启动,逻辑就转移到Game类,它构建了我们与starling.display世界的链接.
配置帧速率
有些设置位于类前面的“SWF”MetaData标签中. 帧速率是其中之一. Starling没有类似设置:它总是简单地使用flash本身的frameRate. 在运行时更改它,请访问nativeStage属性:
|
Starling的设置过程是异步的. 这意味着您将无法在Main方法结束时访问Game实例. 但是,可以侦听ROOT_CREATED事件,以在类实例化完成后收到通知.
public function Main()
{
_starling = new Starling(Game, stage);
_starling.addEventListener(Event.ROOT_CREATED, onRootCreated);
_starling.start();
}
private function onRootCreated(event:Event, root:Game):void
{
root.start(); // 'start' needs to be defined in the 'Game' class
}
2.1.1. The ViewPort
Stage3D为Starling提供了一个矩形区域. 该区域可以在Flash舞台的任何位置:Flash Player或应用程序(如果是AIR项目)窗口区域内的任何位置.
在Starling中,该区域称为viewPort. 大多数时候,你使用区域将是整个Flash舞台的尺寸,但有时,限制渲染到某个局部区域是有意义的.
想象一个设计的长宽比为4:3的游戏,运行在16:9的屏幕上. 通过在屏幕上居中4:3 viewPort,你会得到一个“letterboxed”游戏,在顶部和底部有黑色条.
离开Starling的stage谈论viewPort是没有意义的. 默认情况下,stage尺寸将与viewPort的大小完全相同. 这是很有道理的:一个显示器大小为1024×768像素的设备应该有一个相同大小的舞台.
你可以自定义舞台大小. 这可以通过stage.stageWidth和stage.stageHeight属性来设置:
stage.stageWidth = 1024;
stage.stageHeight = 768;
这是什么意思? 绘图区域的大小现在是由viewPort还是stage大小定义的?
不要担心,该区域仍然只由viewPort设置,如上所述. 修改stageWidth和stageHeight不会改变绘图区域的大小; 舞台总是会覆盖整个viewPort. 你改变stageWidth和stageHeight,实际上改变的只是舞台坐标系的大小.
这意味着,当舞台宽度为1024时,x坐标为1000的对象将接近舞台的右边缘;无论viewPort的宽度值是512,1024,还是2048像素.
这在开发HiDPI屏幕上的应用程序时特别有用. 例如,苹果的iPad存在于普通和“视网膜”显示屏版本,后者像素行和列的数量翻倍(产生四倍的像素总量). 在这种屏幕上,界面元素不应变得更小;相反,它们应该更清晰.
通过举例来区分viewPort和舞台大小,这种例子很容易在Starling中出现. 在以上两种设备类型上,设备显示屏大小相同,其舞台大小都是1024×768;但是,viewPort将以像素为单位反映屏幕的分辨率. 优点:您可以在显示屏尺寸相同的设备上为显示对象使用相同的坐标,而不管运行应用程序的设备配置了哪种显示屏.
点 vs. 像素
如果你理解这一点,你会看到在这样的视网膜设备上,x坐标为1的对象实际上离原点两个像素. 换句话说,用于测量的单位已经改变. 我们不再谈论’像素',而是谈论’点'! 在低分辨率屏幕上,一点等于一个像素; 在HiDPI屏幕上,一点等于两个像素(或更多,取决于设备). |
要找出点的实际宽度(以像素为单位),您可以简单地将`viewPort.width`除以`stage.stageWidth`. 或者你使用Starling的`contentScaleFactor`属性,就像这样:
starling.viewPort.width = 2048;
starling.stage.stageWidth = 1024;
trace(starling.contentScaleFactor); // -> 2.0
在“移动开发”一章,我会告诉你如何充分利用这个概念.
2.1.2. Context3D Profiles
Starling运行的平台具有各种图形处理器. 当然,那些图形处理器(GPU)具有不同的能力. 问题是:如何在运行时区分这些能力?
这就是Context3D profiles(也称为render profiles)的用途.
什么是Context3D?
当使用Stage3D时,您不可避免的将与渲染管道打交道,而渲染管道具有许多设置和属性. context(设置上下文)就是封装这些管道的对象. 创建纹理,上传着色器,渲染三角形 - 这一切都是通过context完成的. |
实际上,Starling尽力隐藏任何配置文件限制. 为了确保最广泛的覆盖范围,它被设计成默认尝试使用最低可用配置文件. 同时,当运行在含有更高的配置文件环境时,它会自动适配最佳的配置文件.
然而,了解它们的基本特征终归是有用的. 以下是每个配置文件的概述,从最低的开始.
BASELINE_CONSTRAINED
-
如果设备完全支持Stage3D,它必须支持此配置文件。它有几个限制,例如:它只支持边长为2的幂的纹理,并且着色器的长度非常有限。该配置文件主要出现在旧台式计算机上.
BASELINE
-
在移动设备上出现的最低配置文件。 Starling在此配置文件下运行良好;去除了纹理边长必须是2的幂限制;允许更有效率的使用存储器,并且着色器程序的长度往往满足其需求.
BASLINE_EXTENDED
-
将最大纹理尺寸从“2048x2048”提高到“4096x4096”像素,这对高分辨率设备至关重要.
STANDARD_CONSTRAINED
,STANDARD
,STANDARD_EXTENDED
-
Starling当前不需要使用这些配置文件中的任何功能。它们提供额外的着色器命令和其他低级控制能力.
我的建议:让Starling自动选择最好的可用配置文件(auto
),让它自行处理.
最大纹理尺寸
有一件事你自己必须小心:确保你的纹理不是太大. 最大纹理尺寸可以通过属性“Texture.maxSize”访问,前提是在Starling已经完成初始化之后. |
2.1.3. 原生叠加
Starling背后的主要目标是使用Stage3D驱动的API来加速渲染. 然而,不可否认的是:经典的显示列表有许多功能,Starling无法提供. 因此,提供一个简单的方法来综合Starling和经典Flash的功能是有意义的.
`nativeOverlay`属性是最简单的方法. 这是一个常规的“flash.display.Sprite”,它直接位于Starling层的顶部,使用时请将viewPort和contentScaleFactor因素加以考虑. 如果您需要使用原生Flash对象,请将它们添加到此叠加层.
但要注意,在Stage3D之上的原生Flash内容可能导致某些(移动)平台的性能损失。 因此,当您不再需要它们时,始终从叠加层中删除所有对象.
假如你有疑问:不,你不能添加任何原生显示对象到Starling显示对象之下. 是的,Stage3D表面总是位于flash底部; 没有办法越过这道槛. |
2.1.4. 跳过不变帧
在应用程序或游戏中经常出现几个帧完全相同的情形. 例如,应用可能正在呈现静态画面或等待用户输入. 那么有必要在这些情况下重新绘制舞台吗?
这正是’skipUnchangedFrames`属性的要点所在. 如果启用,静态画面会被自动识别,以便后台缓冲区保留原样. 在移动设备上,此性能的提升虽不能被高估. 但没有比这更好的方法来提高电池寿命了!
我已经听到你的反对意见:如果这个功能是如此有用,为什么不是默认激活? 这里有一个坑,对吧?
确实有:它不能很好地与Render和VideoTextures同时工作. 这些纹理的变化根本不会被呈现. 不过这很容易解决:暂时禁用`skipUnchangedFrames`,或每当它们的内容有改变时调用`stage.setRequiresRedraw()`.
现在你知道了这个功能,那么总是激活它,使它成为一个习惯吧! 在此期间,我希望我可以在未来的Starling版本中解决上述问题.
在移动平台上,您还应注意另一个限制:只要本机(Flash)舞台上有任何原生显示对象(例如通过Starling的“nativeOverlay”添加的),Starling就不能跳过任何帧. 这是Stage3D限制的结果. |
2.1.5. 统计显示
在开发应用程序时,您需要尽可能多的信息来了解发生了什么. 这样,你能够早点发现问题,并可能由此避免进入死胡同. 统计显示就是用来干这件事的.
_starling.showStats = true;
这些值的含义是什么?
-
framerate (帧率)意义应该是相当明确的: Starling统计到在前一秒内渲染的帧数.
-
Standard memory 是内存, 总而言之, 你使用的 AS3 对象, 无论它是 String, Sprite, Bitmap, 或者 Function: 所有对象都需要一些内存. 单位是兆字节(Mb).
-
GPU memory 表示显存,这分几种情况. 纹理存储在图形存储器(显存)中,顶点缓冲和着色器程序也是如此. 大多数时候,纹理占据了绝大部分.
-
draw calls 表示绘制调用次数,即每帧向GPU发送多少个单独的“绘制”命令. 通常,当绘制调用较少时,场景渲染会更快. 当我们谈论[性能优化]时,我们将详细解读这个值.
您可能会注意到统计显示的背景颜色在黑色和深绿色之间交替. 这是一个微妙的线索,指的是`skipUnchangedFrames`属性: 当大部分帧可以跳过时,该框变为绿色. 每当画面静止时,确保它保持绿色; 如果没有, 一些逻辑导致了Starling不能进入跳帧操作.
您可以通过`showStatsAt`方法在屏幕上自定义统计显示的位置. |
2.2. 显示编程
完成了所有的设置过程,我们可以开始实际把一些内容到显示到屏幕上!
在您创建的每个应用程序中,您的主要任务之一是将其拆分为多个逻辑单元。 通常,这些单元将具有可视化表示形式。 换句话说:每个单元将是一个显示对象。
2.2.1. 显示对象
屏幕上显示的所有元素其类型都是显示对象。 `starling.display`包包括抽象DisplayObject类;它提供了许多不同类型的基本显示对象,例如图像,影片剪辑和文本等等。
DisplayObject类提供了所有显示对象共有的方法和属性。 例如,以下属性用于配置对象在屏幕上的位置:
-
x
,y
:当前坐标系中的位置。 -
width
,height
:对象的大小(以点为单位)。 -
scaleX
,scaleY
:另一种方式来查看对象大小; “1.0”表示未缩放,“2.0”将尺寸加倍等。 -
rotation
:对象绕其原点旋转(以弧度表示)。 -
skewX
,skewY
:水平和垂直倾斜(以弧度表示)。
其他属性修改像素在屏幕上的显示方式:
-
blendMode
:确定对象的像素与下面的像素混合。 -
filter
:修改对象外观的特殊GPU程序(着色器)。过滤器可以模糊对象或添加投影。 -
mask
:mask删除某个区域外的所有部分。 -
alpha
:对象的不透明度,从“0”(不可见)到“1”(完全不透明)。 -
visible
:如果为false,则对象将完全隐藏。
以上这些是每个显示对象必须支持的基本属性。 下面让我们看看Starling关于这方面的API架构:
你会注意到,图表分成两个主要的子分支。
第一个分支,有一些类扩展自`Mesh`:例如`Quad`,Image`和`MovieClip
。
网格是Starling的渲染架构的基本组成部分。 实际上一切被绘制到屏幕上的对象都是网格! Stage3D不能绘制任何东西,除了三角形,一个网格其实是一系列的点,这些点按一定的顺序组成三角形。
第二个分支,你会发现一些扩展自DisplayObjectContainer的类。 顾名思义,这个类充当其他显示对象的容器。 它可以将显示对象组织成逻辑系统 - display list(显示列表)。
2.2.2. 显示列表
被渲染的所有显示对象的层次结构称为display list。 Stage是显示列表的根容器。 把"stage"(舞台)按字面上的意思理解:你的用户(观众)只会看到已经进入舞台的对象(演员)。 当您开始创建Starling时,舞台将自动被创建。 连接到舞台(直接或间接)的所有内容都将被渲染。
当我说“连接到”时,我的意思是需要一个parent-child关系。 为了使一个对象出现在屏幕上,你应该把它添加(addChild)到舞台,或任何其他连接到舞台的DisplayObjectContainer上。
舞台的第一个(通常是唯一的)子对象是application root:这是你传递给Starling构造函数的类。 就像舞台,它可能也是一个DisplayObjectContainer。 你将通过它来接管显示列表!
您将创建容器,其中容器将包含其他容器和网格(例如图片)。 在树型的显示列表中,这些网格构成叶子:它们不能再包含任何子对象。
这一切听起来似乎很抽象,让我们看一个具体的例子:一个聊天气泡。 要创建聊天气泡,您需要一个图像(对于气泡)和一些文本(对于讲话内容)。
这两个对象应该作为一个整体:当移动时,图像和文本应该跟随。 同样的,应该一起适应大小,缩放,旋转等的变化。 我们可以把这些对象放在一个非常轻量级的DisplayObjectContainer:Sprite中来实现。
DisplayObjectContainer vs. Sprite
DisplayObjectContainer和Sprite可以几乎具有相同的意义。 这两个类之间的唯一区别是,一个(DisplayObjectContainer)是抽象的,而另一个(Sprite)不是。 因此,您可以使用Sprite将对象分组在一起,而不需要再创建一个扩展自DisplayObjectContainer的子类。 可以看出Sprite的另一个优点:它是更快捷的类型。 通常,这是我更喜欢它的主要原因。 像大多数程序员一样,我也是一个懒人! |
因此,要将文本和图像组合在一起,您可以创建一个sprite,并将文本和图像添加为children:
var sprite:Sprite = new Sprite(); (1)
var image:Image = new Image(texture);
var textField:TextField = new TextField(200, 50, "Ay caramba!");
sprite.addChild(image); (2)
sprite.addChild(textField); (3)
1 | 创建sprite。 |
2 | 向sprite中添加一个Image。 |
3 | 向sprite中添加一个TextField。 |
添加子项的顺序很重要 - 它们按照放置顺序来决定显示层次。 这里,`textField`将出现在`image`前面。
现在那些对象被分组到同一个sprite里了,你可以把sprite当成一个整体来使用。
var numChildren:int = sprite.numChildren; (1)
var totalWidth:Number = sprite.width; (2)
sprite.x += 50; (3)
sprite.rotation = deg2rad(90); (4)
1 | 查询子对象个数。 这里,结果将是“2”。 |
2 | `width`和`height`计算了子对象的大小和位置。 |
3 | 向右移动50点。 |
4 | 将组旋转90度(Starling始终使用弧度)。 |
实际上,DisplayObjectContainer定义了许多方法来帮助你操纵它的子对象:
function addChild(child:DisplayObject):void;
function addChildAt(child:DisplayObject, index:int):void;
function contains(child:DisplayObject):Boolean;
function getChildAt(index:int):DisplayObject;
function getChildIndex(child:DisplayObject):int;
function removeChild(child:DisplayObject, dispose:Boolean=false):void;
function removeChildAt(index:int, dispose:Boolean=false):void;
function swapChildren(child1:DisplayObject, child2:DisplayObject):void;
function swapChildrenAt(index1:int, index2:int):void;
2.2.3. 坐标系
每个显示对象都有自己的坐标系。 例如,显示对象的`x`和`y`属性并不是直接针对屏幕坐标系而言,它们总是取决于当前的坐标系。 该坐标系又取决于对象在显示列表层次结构中的位置。
为了理解这一点,想象将纸片固定在书签板上。 每张纸版都表示有水平x轴和垂直y轴的坐标系。 你插入钉针的位置就是该坐标系的原点。
现在,当您旋转纸张时,绘制在其上的所有内容(例如图像和文本)将随其一起旋转,如同旋转了它的x和y轴一样。 然而,坐标系的原点(针)仍保持在它的位置。
因此,针的位置实际上是纸张相对于其母坐标系统(书签板)所确定的x坐标和y坐标。
在创建显示层次结构时,请牢记书签板的例子。 当采用Starling开发时这是一个你需要理解的非常重要的概念。
2.2.4. 自定义显示对象
我已经提到过:当你创建一个应用程序时,你把它拆分成不同的逻辑部分。 一个简单的国际象棋游戏可能包含棋盘,棋子,暂停按钮和消息框等部分。 所有这些对象将显示在屏幕上 - 因此,每个对象都将由从DisplayObject派生的类来表示。
以一个简单的消息框为例。
这实际上与我们刚刚创建的讲话泡泡非常相似; 除了背景图像和文本,它还包含两个按钮。
这一次,我们不是仅仅将对象组合在一个sprite中,而是将它封装成一个方便的类,并且隐藏任何实现细节。
为了实现这一点,我们创建一个继承自DisplayObjectContainer的新类。 在其构造函数中,我们创建构成消息框的所有内容:
public class MessageBox extends DisplayObjectContainer
{
[Embed(source = "background.png")]
private static const BackgroundBmp:Class;
[Embed(source = "button.png")]
private static const ButtonBmp:Class;
private var _background:Image;
private var _textField:TextField;
private var _yesButton:Button;
private var _noButton:Button;
public function MessageBox(text:String)
{
var bgTexture:Texture = Texture.fromEmbeddedAsset(BackgroundBmp);
var buttonTexture:Texture = Texture.fromEmbeddedAsset(ButtonBmp);
_background = new Image(bgTexture);
_textField = new TextField(100, 20, text);
_yesButton = new Button(buttonTexture, "yes");
_noButton = new Button(buttonTexture, "no");
_yesButton.x = 10;
_yesButton.y = 20;
_noButton.x = 60;
_noButton.y = 20;
addChild(_background);
addChild(_textField);
addChild(_yesButton);
addChild(_noButton);
}
}
现在你有一个简单的类,包含一个背景图像,两个按钮和一些文本。 要使用它,只需创建一个MessageBox的实例并将其添加到显示列表:
var msgBox:MessageBox = new MessageBox("Really exit?");
addChild(msgBox);
您可以向类添加其它方法(如fadeIn和fadeOut),以及在用户单击其中一个按钮时触发的代码。 这是使用Starling的事件机制来完成的,这将在后面的章节中讲到。
2.2.5. 移除显示对象
当您不想再显示某个对象时,只需将其从其父级中删除即可。 例如通过调用`removeFromParent()`。 当然,这种情况下该对象仍然会存在,如果你想复用的话,你可以将它添加到另一个显示对象容器中。 然而,很多时候,该对象没有继续存在的必要。 在这种情况下,销毁它是一个好的做法。
msgBox.removeFromParent();
msgBox.dispose();
当销毁显示对象时,它们将释放其本身(及任何子级)占用的所有资源。 这很重要,因为许多Stage3D相关的数据是无法被垃圾回收器回收的。 如果您没有销毁这些数据,它们将一直被保留在内存中,这意味着应用程序迟早会耗尽内存资源而崩溃。
为了处理起来更简单,`removeFromParent()`接受一个可选地布尔参数来决定是否销毁该DisplayObject。 这样,上面的代码可以简化为这一行:
msgBox.removeFromParent(true);
2.2.6. 枢轴点
枢轴点是在传统显示列表中找不到的功能。 在Starling中,显示对象包含两个附加属性:pivotX和pivotY。 对象的枢轴点(也称为_origin_,root_或_anchor)定义其坐标系的根。
默认情况下,枢轴点在(0,0)`; 在图像中,即左上角位置。 大多数时候,这是适合需求的。 然而,有时,你想要在不同的位置 - 例如。 当您想要围绕其中心旋转图像时。
你必须将对象包装在容器中,以便使用枢轴点:
var image:Image = new Image(texture);
var sprite:Sprite = new Sprite(); (1)
image.x = -image.width / 2.0;
image.y = -image.height / 2.0;
sprite.addChild(image); (2)
sprite.rotation = deg2rad(45); (3)
1 | 创建sprite。 |
2 | 添加一个图像,使其中心正好在sprite的原点上。 |
3 | 旋转sprite会使图像围绕其中心旋转。 |
大多数有长期Flash开发经验的人员都会知道这个技巧; 并且会定期的使用它。 然而,有人可能会说,做这样一个很简单的事情也需要写不少代码。 借助于枢轴点,代码将缩减为以下形式:
var image:Image = new Image(texture);
image.pivotX = image.width / 2.0; (1)
image.pivotY = image.height / 2.0; (2)
image.rotation = deg2rad(45); (3)
1 | 将pivotX移动到图像的水平中心。 |
2 | 将`pivotY`移动到图像的垂直中心。 |
3 | 绕中心旋转。 |
不需要创建多余的容器! 延续前面章节中使用的类比:枢轴点定义当您将其附加到父对象时,钉子穿过的位置。 上面的代码将枢轴点移动到对象的中心。
现在你已经学会了如何单独控制枢轴点坐标,让我们来看看“alignPivot()”方法。 它允许我们只用一行代码将枢轴点移动到对象的中心:
var image:Image = new Image(texture);
image.alignPivot();
image.rotation = deg2rad(45);
方便吗?
此外,如果我们想要把枢轴点置于其它地方(例如,在右下角),我们可以给该方法传递对齐参数:
var image:Image = new Image(texture);
image.alignPivot(Align.RIGHT, Align.BOTTOM);
image.rotation = deg2rad(45);
该代码使图像围绕其右下角旋转。
窍门
注意:枢轴点总是在对象的本地坐标系中给出。 这与’width’和’height’属性不同,它们实际上是相对于parent坐标系。 当枢轴点与对象的缩放或旋转等属性同时被设置时,结果会使人诧异。
例如,想象一个100像素宽,缩放到200%的图像(image.scaleX = 2.0
)。
该图像现在返回一个宽度为200像素(宽度的两倍)的宽度。
但是,要将水平枢轴点居中,您仍然将`pivotX`设置为`50`,而不是`100`!
因为在本地坐标系中,图像仍然是100像素宽 - 它只是在parent坐标系中显得更宽。
当你回顾本节开头的代码时,我们可能会更容易理解,我们将图像集中在一个父容器中。 如果你改变了sprite的`scale`会发生什么? 这是否意味着您必须更新图像的位置以保持其居中? 当然不是。 scale不影响容器内部发生的事情,只是它从外部看起来变化了。 从这个方面讲它与枢轴点相同。
如果你仍然头痛(实际上发生在我身上),只要记住在改变对象的scale或rotation属性之前先设置好枢轴点。 这将避免所有问题。 |
2.3. 纹理和图像
我们几次碰到过图像(Image)和纹理(Texture)类,确实如此:它们是Starling中最有用的类。 但是如何使用它们,两者之间有什么区别呢?
2.3.1. 纹理
纹理只是描述图像的数据 - 好比保存在数码相机上的文件。 你无法直接看到数据文件所记录的图像:毕竟它只是一系列的0和1。 您需要一个图像查看器来查看它,或将其发送到打印机。
纹理直接存储在GPU内存中,这意味着它们可以在渲染期间被高效的访问。 我们通常是直接嵌入(embed)到类或通过加载文件来得到纹理。您可以选择以下文件格式之一作为纹理格式:
- PNG
-
最通用的文件格式。其无损压缩特别适用于具有大面积纯色的图像。建议作为默认纹理格式。
- JPG
-
由于使用有损压缩算法,对于摄影(和照片)图像,文件占用字节数比PNG格式小。然而,缺少alpha通道严重限制了其适用性。仅推荐照片和大背景图片使用这种格式。
- ATF
-
特别为Stage3D定制的格式。 ATF纹理需要很少的纹理内存并且具有非常快的加载速度;然而,它们也是有损压缩的,不能完全适合于所有类型的图像。我们将在后面的章节中更详细地讨论ATF纹理(参见“ATF纹理”)。
starling.textures.Texture
类包含了许多用于实例化纹理的工厂方法。
下面列举了几个工厂方法(为了读起来更清楚,我省略了参数)。
public class Texture
{
static function fromColor():Texture;
static function fromBitmap():Texture;
static function fromBitmapData():Texture;
static function fromEmbeddedAsset():Texture;
static function fromCamera():Texture;
static function fromNetStream():Texture;
static function fromTexture():Texture;
}
可能最常见的任务是通过位图创建纹理。 这似乎并不容易:
var bitmap:Bitmap = getBitmap();
var texture:Texture = Texture.fromBitmap(bitmap);
通过嵌入位图创建纹理也很常见。 这可以用跟上述相同的方式完成:
[Embed(source="mushroom.png")] (1)
public static const Mushroom:Class;
var bitmap:Bitmap = new Mushroom(); (2)
var texture:Texture = Texture.fromBitmap(bitmap); (3)
1 | 嵌入位图。 |
2 | 实例化位图。 |
3 | 从位图创建纹理。 |
然而,有一个捷径,使工作进一步简化:
[Embed(source="mushroom.png")] (1)
public static const Mushroom:Class;
var texture:Texture = Texture.fromEmbeddedAsset(Mushroom); (2)
1 | 嵌入位图。 |
2 | 直接从存储嵌入资源的类创建纹理。 |
提示
这不仅节省代码,还能节省内存! 该`fromEmbeddedAsset`方法通过幕后的一些黑科技来减少性能损失,比传统的`fromBitmap`方法更加高效。 我们稍后将回到这个主题,但现在,只记得这是从嵌入式位图创建纹理的首选方法。 |
Texture类的另一个特性隐藏在不显眼的`fromTexture`方法中。 它允许您使用另一个纹理中指定区域的局部纹理。
是什么原因让这个特性很有用?事实上,在这个过程中没有任何像素被复制。 相反,创建的SubTexture仅存储对其父纹理的引用。 这种方案非常高效!
var texture:Texture = getTexture();
var subTexture:Texture = Texture.fromTexture(
texture, new Rectangle(10, 10, 41, 47));
很快,你会知道TextureAtlas类; 它基本上围绕这个功能构建。
2.3.2. 图像
我们现在有了一些纹理,但我们仍然不知道如何在屏幕上显示它们。 最简单的方法是使用Image类或其直系亲属之一。
让我们聚焦此族谱中的这一部分。
-
Mesh是三角形的平面集合(记住,GPU只能绘制三角形)。
-
一个Quad是一个至少有两个三角形的集合,它构成一个四边形。
-
一个Image只是一个有着方便的构造函数和一些额外方法的四边形。
-
MovieClip是一种随时间切换纹理的图像。
虽然所有这些类都自带运用纹理的方法,但是你经常使用的可能是Image类。 这是因为矩形纹理是最常见的,而Image类是使用它们的最简便的方式。
为了演示,让我告诉你如何通过Quad和Image显示一个纹理。
var texture:Texture = Texture.fromBitmap(...);
var quad:Quad = new Quad(texture.width, texture.height); (1)
quad.texture = texture;
addChild(quad);
var image:Image = new Image(texture); (2)
addChild(image);
1 | 创建具有适当大小的四边形并分配纹理,或: |
2 | 使用其标准构造函数创建图像。 |
虽然在上述两种情况下,幕后发生的事情是完全相同的。 就个人而言,我总是选择能够节省一些代码的方法。
2.3.3. 一个纹理,多个图像
需要强调的是一个纹理可以映射到任意数量的图像(网格)。 事实上,这正是你应该做的:加载一个纹理,然后在应用程序的整个生命周期重复使用它。
// 不要这样做!!
var image1:Image = new Image(Texture.fromEmbeddedAsset(Mushroom));
var image2:Image = new Image(Texture.fromEmbeddedAsset(Mushroom));
var image3:Image = new Image(Texture.fromEmbeddedAsset(Mushroom));
// 正确的做法是,创建一次纹理,保留引用并重复使用:
var texture:Texture = Texture.fromEmbeddedAsset(Mushroom));
var image1:Image = new Image(texture);
var image2:Image = new Image(texture);
var image3:Image = new Image(texture);
几乎所有的内存占用都来自纹理;如果你浪费了纹理内存,你将很快耗尽RAM。
2.3.4. 纹理图集
在之前所有的示例中,我们都是分别加载每个纹理。 然而,真正的应用程序实际上不应该这样做。 这是为什么呢?
-
对于高效的GPU渲染,Starling将渲染的Meshes一起进行批处理。但是,当纹理改变时,批处理被中断。
-
在某些情况下,Stage3D需要纹理的宽度和高度是2的幂。 Starling规避了这个限制,但是如果你不遵守这个规则,你会使用更多的内存。
通过使用纹理图集,您可以避免纹理切换和二次方的限制。 所有纹理都在一个大的“超级纹理”内,而Starling会关注显示这个纹理的正确部分。
诀窍是让Stage3D使用这个大纹理而不是小的纹理,并且只将大纹理的一部分映射到对应的每个四边形。 这将非常有效的节省内存使用,浪费尽可能小的空间。 (一些其他框架调用此功能,例如Sprite Sheets。)
“Texture Packer”的团队实际上创建了一个关于sprite sheet的介绍视频。 在这里观看: Sprite Sheet |
创建纹理图集
每个SubTexture在XML文件中像这样定义:
<TextureAtlas imagePath="atlas.png">
<SubTexture name="moon" x="0" y="0" width="30" height="30"/>;
<SubTexture name="jupiter" x="30" y="0" width="65" height="78"/>;
...
</TextureAtlas>;
如你所见,XML引用一个大纹理并定义多个SubTextures,每个SubTexture指向该纹理内的一个区域。 在运行时,你可以通过他们的名字引用这些SubTextures,就像它们本来就是独立的纹理一样。
但是怎么将所有的纹理合并成这样的图集? 幸运的是,你不必手动做; 有很多工具,将帮助你完成这项任务。 这里有两个候选人,但Google会带来更多。
-
TexturePacker 是我个人的最爱。 你不会找到任何工具,能够这么完善的控制你的精灵图集,同时对Starling的支持是这么友好(ATF纹理,还有其它工具吗?)。
-
Shoebox 是一个用AIR构建的免费工具。 虽然它没有TexturePacker一样多的选项来创建纹理图集,但它包含许多相关功能,如位图字体创建和精灵提取。
使用纹理图集
好吧:你现在有一个纹理图集。 但是你怎么用呢? 让我们从嵌入纹理和XML数据开始。
[Embed(source="atlas.xml", mimeType="application/octet-stream")] (1)
public static const AtlasXml:Class;
[Embed(source="atlas.png")] (2)
public static const AtlasTexture:Class;
1 | 嵌入图集描述文件XML。 不要忘记指定mimeType。 |
2 | 嵌入图集纹理。 |
或者,您也可以从URL或磁盘加载这些文件(如果我们在谈论AIR)。 当我们讨论Starling的AssetManager时,我们将详细讨论。 |
有了这两个对象,我们可以创建一个新的TextureAtlas实例,并通过getTexture()方法访问所有SubTextures。 在游戏初始化时创建一次atlas对象,并在其生命周期中引用它。
var texture:Texture = Texture.fromEmbeddedAsset(AtlasTexture); (1)
var xml:XML = XML(new AtlasXml());
var atlas:TextureAtlas = new TextureAtlas(texture, xml);
var moonTexture:Texture = atlas.getTexture("moon"); (2)
var moonImage:Image = new Image(moonTexture);
1 | 创建图集。 |
2 | 显示子纹理(SubTexture). |
就这么简单!
2.3.5. 渲染纹理
RenderTexture类允许动态创建纹理。 把它想象成一个画布,你可以在其上绘制任何显示对象。
创建渲染纹理后,只需调用`drawObject`方法将对象直接渲染到纹理上。 对象将被绘制到当前位置的纹理上,并附带其当前旋转,缩放和透明度等属性。
var renderTexture:RenderTexture = new RenderTexture(512, 512); (1)
var brush:Sprite = getBrush(); (2)
brush.x = 40;
brush.y = 120;
brush.rotation = 1.41;
renderTexture.draw(brush); (3)
1 | 创建一个具有给定大小(以点为单位)的新RenderTexture。 它将用完全透明的像素初始化。 |
2 | 在这个示例中,我们引用了一个描述画笔的显示对象。 我们将它移动到某个位置。 |
3 | 画刷对象将以其当前的位置和方向被绘制到纹理中。 |
绘图过程非常高效,因为它直接发生在图形存储器中。 在将对象绘制到渲染纹理上之后,其性能将像普通纹理一样 - 无论您绘制多少对象。
var image:Image = new Image(renderTexture);
addChild(image); (1)
1 | 此纹理可以像任何其他纹理一样使用。 |
如果你一次绘制大量对象,建议通过`drawBundled`方法将绘图调用捆绑在一个块中,如下所示。 这使得Starling可以跳过几个相当昂贵的操作,极大地加快了这个过程。
renderTexture.drawBundled(function():void (1)
{
for (var i:int=0; i<numDrawings; ++i)
{
image.rotation = (2 * Math.PI / numDrawings) * i;
renderTexture.draw(image); (2)
}
});
1 | 通过将所有绘图调用封装在一个函数中来激活绑定的绘图。 |
2 | 在此函数内部,像以前一样调用`draw`来绘制对象。 |
要擦除渲染纹理的某些部分,可以使用任何显示对象(如“橡皮擦”),通过设置其混合模式为BlendMode.ERASE来达到目的。
brush.blendMode = BlendMode.ERASE;
renderTexture.draw(brush);
要彻底擦除它,使用`clear`方法。
设备丢失
不幸的是,渲染纹理有一个很大的缺点:当渲染设备丢失时,它们会丢失所有内容。 [设备丢失] 将在后面的章节中详细讨论; 简而言之,这意味着Stage3D在某些情况下可能会丢失其缓冲区的所有内容。 (是的,虽然听起来令人讨厌。) 因此,如果纹理的内容是持久的(即它不只是过眼云烟),这个问题真的很重要,你将需要做一些额外的处理。 我们将在上述章节中探讨可能的策略 - 在这里我只是想提到这个事实,碰见它时才不会措手不及。 |
2.4. 动态文本
文本是每个应用程序的重要组成部分。 你用图像传达的信息很有限; 一些事情只能依靠运行时动态地用语言描述。
2.4.1. TextFields
Starling可以方便地显示动态文本。 TextField类字面意思应该非常明确!
var textField:TextField = new TextField(100, 20, "text"); (1)
textField.format.setTo("Arial", 12, Color.RED); (2)
textField.format.horizontalAlign = Align.RIGHT; (3)
textField.border = true; (4)
1 | 创建一个大小为100×20点的TextField,显示文本“text”。 |
2 | 我们将格式设置为“Arial”,大小为12点,红色。 |
3 | 文本向右对齐。 |
4 | border属性在主要开发阶段有用:它将显示TextField的边界。 |
注意,文本的样式是通过`format`属性设置的,它指向`starling.text.TextFormat`的一个实例。 |
一旦创建TextField,您可以像使用图像(Image)或四边形(Quad)一样使用它。
2.4.2. TrueType字体
默认情况下,Starling将使用系统字体来渲染文本。 例如,如果设置TextField使用“Arial”字体,它将使用系统上安装的(如果系统中包含)。
然而,该方法的渲染质量不是最优的; 例如,可以在没有抗锯齿的情况下呈现字体。
为了获得更好的输出,应该将TrueType字体直接嵌入到SWF文件中。 使用以下代码:
[Embed(source="Arial.ttf", embedAsCFF="false", fontFamily="Arial")]
private static const Arial:Class; (1)
[Embed(source="Arial Bold.ttf", embedAsCFF="false", fontFamily="Arial", fontWeight="bold")]
private static const ArialBold:Class; (2)
[Embed(source="Arial Italic.ttf", embedAsCFF="false", fontFamily="Arial", fontStyle="italic")]
private static const ArialItalic:Class; (3)
[Embed(source="Arial.ttf", embedAsCFF="false", fontFamily="Arial", unicodeRange = "U+0020-U+007e")]
private static const ArialJustLatin:Class; (4)
1 | 嵌入标准Arial字体。注意embedAsCFF部分:不要跳过!否则,字体根本不会显示。 |
2 | 粗体和斜体样式必须单独嵌入。注意fontWeight属性在这里, |
3 | fontStyle属性也在这里。 |
4 | 当您不需要所有Unicode字符时,您还可以定义要包括哪些字形,这对于大字符集语言字体库很有用。 此处显示的范围是基本拉丁语(大写和小写字符,数字和公共符号/标点符号)。 |
嵌入字体后,所有使用相应字体名称(字体系列)和权重设置的TextField都将自动使用嵌入字体。 除此之外不需要其他设置或配置。
当嵌入字体的所有字形时,当心占用过大的空间。 上面显示的“unicodeRange”减轻了这个问题。您可以使用例如 Unicode Range Generator. |
如果您的文本被裁剪或出现在错误的位置,请查看您当前的“stage.quality”设置。 低品质通常导致Flash / AIR返回不正确的文本边界值,而Starling在绘制文本时取决于这些值。 (在这里谈论的是指Flash原生stage,以上只适用于TrueType字体。) |
2.4.3. 位图字体
使用如上所示的TrueType字体适用于不经常更改的文本。 但是,如果您的TextField不断更改其内容,或者如果要显示TrueType格式不可用的花哨字体,则应使用位图字体。
位图字体是包含要显示的所有字符的纹理。 与TextureAtlas类似,XML文件存储着字形处于纹理中的位置。
这是Starling渲染的位图字体的所有必要条件。 要创建所需的文件,有几个途径:
-
Littera, 一个功能齐全的免费在线位图字体生成器。
-
Bitmap Font Generator, 一个由AngelCode提供的工具,它允许您使用任何TrueType字体创建位图字体。但是,它仅适用于Windows。
-
Glyph Designer 为macOS,一个优秀的工具,允许你添加漂亮的特殊效果到您的字体。
-
bmGlyph, 也专用于macOS,在App Store上可用。
这些工具使用方法都类似,允许您选择一个系统字体并可选地应用一些特殊效果。 导出文件时,有几件事要注意:
-
Starling需要“.fnt”格式的XML变体。
-
确保选择正确的字形集;否则,你的字体纹理可能会变得非常大。
结果是“.fnt”文件和包含字符的关联纹理。
要使这样的字体可用于Starling,您可以将其嵌入到SWF中并将其注册到TextField类。
[Embed(source="font.png")]
public static const FontTexture:Class;
[Embed(source="font.fnt", mimeType="application/octet-stream")]
public static const FontXml:Class;
var texture:Texture = Texture.fromEmbeddedAsset(FontTexture);
var xml:XML = XML(new FontXml());
var font:BitmapFont = new BitmapFont(texture, xml); (1)
TextField.registerCompositor(font); (2)
1 | 创建BitmapFont类的实例。 |
2 | 在TextField类中注册字体。 |
一旦位图字体实例在TextField类中完成注册,则无需再进一步处理它。 Starling将在遇到使用具有该名称的字体的TextField时,简单地拾取该字体。 像这样:
var textField:TextField = new TextField(100, 20, "Hello World");
textField.format.font = "fontName"; (1)
textField.format.fontSize = BitmapFont.NATIVE_SIZE; (2)
1 | 要使用字体,只需引用字体名称。默认情况下,名称存储在XML文件的`face`属性中。 |
2 | 当字体大小与创建字体纹理时的实际字体大小一致时,位图字体看起来最好。你可以手动分配这个大小 - 但更好的做法是通过`NATIVE_SIZE`常量来设置。 |
窍门
还有一件事你需要知道:如果你的位图字体只使用一种颜色(像一个正常的TrueType字体,没有任何颜色效果),你的字形需要导出为纯白色。 然后,通过设置TextField的`format.color`属性可以用来在运行时将字体颜色调整为任意颜色(简单地通过与纹理的RGB通道相乘)。
另一方面,如果你的字体包含颜色(像上面的示例图像),需要将TextField的`format.color`属性设置为白色(Color.WHITE
)。
这样,TextField的颜色着色将不会影响纹理颜色。
为了获得最佳性能,您甚至可以向纹理图集中添加字体纹理! 这样,您的文本可以与常规图像一起批处理,减少drawCall的调用。 |
MINI字体
Starling实际上带有一个非常轻量级的位图字体。 它可能不会赢得任何选美比赛 - 但当你仅仅需要显示最基本的文本,或者作为一些调试输出时,它是最合适的。
当我说轻便的,我的意思是:每个字母只有5像素高。 有一个窍门,但是,它会扩展到正好200%的原生大小。
var textField:TextField = new TextField(100, 10, "The quick brown fox ...");
textField.format.font = BitmapFont.MINI; (1)
textField.format.fontSize = BitmapFont.NATIVE_SIZE * 2; (2)
1 | 使用MINI字体 |
2 | 使用原始大小的两倍。 由于字体使用最近邻的缩放,它仍然保持清晰! |
2.5. 事件处理
您可以将事件视为您作为程序员感兴趣的任何类型的事件。
-
例如,移动应用可能会通知您设备方向已更改,或者用户刚刚触摸了屏幕。
-
最基本的,可能表示一个按钮被触发,或一个骑士,他已经用尽了生命值。
这就是Starling的事件机制。
2.5.1. 动机
事件机制是Starling架构的一个关键特性。 简而言之,事件允许对象彼此进行通信。
你可能会想:我们已经有了一个机制 - 方法! 确实如此,但方法只能在同一个线性方向上工作。 例如,查看包含Button的MessageBox。
消息框本身包含了按钮这个对象,因此可以直接使用其方法和属性,例如:
public class MessageBox extends DisplayObjectContainer
{
private var _yesButton:Button;
private function disableButton():void
{
_yesButton.enabled = false; (1)
}
}
1 | 通过属性与Button通信。 |
另一方面,Button实例没有对消息框的引用。 毕竟,一个按钮可以被任何组件使用 - 它完全独立于MessageBox类。 这是一件好事,否则,你只能在消息框中使用按钮,而在其他地方就只能呵呵了!
进一步说,按钮这样设计是有原因的 - 如果触发,它需要告诉别人! 换句话说:按钮需要能够发送消息给它的所有者,无论谁。
2.5.2. Event & EventDispatcher
有件事我得承认:当我向你展示Starling的显示对象的类层次结构时,我省略了实际的基类:EventDispatcher。
该类赋予所有显示对象分派和处理事件的能力。 所有显示对象继承自EventDispatcher,这不是巧合; 在Starling中,事件系统与显示列表紧密集成。 这有一些优点,我们将在后面看到。
最好通过看一个例子来描述事件。
想象一下,你有一只狗; 让我们叫他Einstein。 每天几次,Einstein会告诉你,他想出去散步。 他是通过吠叫来做到的。
class Dog extends Sprite
{
function advanceTime():void
{
if (timeToPee)
{
var event:Event = new Event("bark"); (1)
dispatchEvent(event); (2)
}
}
}
var einstein:Dog = new Dog();
einstein.addEventListener("bark", onBark); (3)
function onBark(event:Event):void (4)
{
einstein.walk();
}
1 | 字符串`bark`将标识事件。它封装在Event实例中。 |
2 | 调度`事件’将发送给订阅`bark`事件的每个人。 |
3 | 这里,我们通过调用`addEventListener`来订阅。第一个参数是事件`type`,第二个是`listener`(一个函数)。 |
4 | 当狗吠时,这个方法将调用事件作为参数。 |
你看到了事件机制的三个主要组件:
-
事件封装在* Event *类(或其子类)的实例中。
-
要分派事件,发送者调用* dispatchEvent *,传递Event实例。
-
要监听事件,客户端调用* addEventListener *,指示他感兴趣的事件类型以及要调用的函数或方法。
有时,你的阿姨会照顾狗。 当这种情况发生时,你不用关心狗吠 - 因为你的阿姨知道她要干什么! 所以你删除事件监听器,这是一个不仅对狗主人,对Starling开发者来说也是一个好的做法。
einstein.removeEventListener("bark", onBark); (1)
einstein.removeEventListeners("bark"); (2)
1 | 这删除了特定的`onBark`监听器。 |
2 | 这将删除该类型的所有侦听器。 |
这么多的`bark`事件。 当然,Einstein可以分派几种不同的事件类型,例如`howl`或`growl`事件。 建议将这样的字符串存储在静态常量中,例如。 右边的“狗”类。
class Dog extends Sprite
{
public static const BARK:String = "bark";
public static const HOWL:String = "howl";
public static const GROWL:String = "growl";
}
einstein.addEventListener(Dog.GROWL, burglar.escape);
einstein.addEventListener(Dog.HOWL, neighbor.complain);
Starling在Event类中预定义了几个非常有用的事件类型。 这里有一些最受欢迎的类型:
-
* Event.TRIGGERED:*触发了一个按钮
-
* Event.ADDED:*显示对象已添加到容器
-
* Event.ADDED_TO_STAGE:*显示对象已添加到连接到舞台的容器
-
* Event.REMOVED:*从容器中删除显示对象
-
* Event.REMOVED_FROM_STAGE:*显示对象失去其与舞台的连接
-
* Event.ENTER_FRAME:*一段时间过去了,一个新的帧被渲染(我们将在后面谈到)
-
* Event.COMPLETE:*某事(像一个MovieClip实例)刚刚完成
2.5.3. 自定义事件
狗吠的原因不尽相同,对吧? Einstein可能表达他想撒尿,或者他饿了。 它也可能是一种方式告诉一只猫,是离开此地的时候了。
懂狗的人可能会听到差异(我不会,因为我是一个只懂猫的人)。 这是因为聪明的狗设置了一个BarkEvent来存储他们的意图。
public class BarkEvent extends Event
{
public static const BARK:String = "bark"; (1)
private var _reason:String; (2)
public function BarkEvent(type:String, reason:String, bubbles:Boolean=false)
{
super(type, bubbles); (3)
_reason = reason;
}
public function get reason():Boolean { return _reason; } (4)
}
1 | 在自定义事件类中存储事件类型是一个很好的做法。 |
2 | 创建自定义事件的原因:我们要存储一些信息。 这里,这是“reason”字符串。 |
3 | 在构造函数中调用超类。 (我们将很快看看“冒泡”的意思。) |
4 | 通过属性访问“reason”。 |
狗现在可以使用这个自定义事件时吠叫:
class Dog extends Sprite
{
function advanceTime():void
{
var reason:String = this.hungry ? "hungry" : "pee";
var event:BarkEvent = new BarkEvent(BarkEvent.BARK, reason);
dispatchEvent(event);
}
}
var einstein:Dog = new Dog();
einstein.addEventListener(BarkEvent.BARK, onBark);
function onBark(event:BarkEvent):void (1)
{
if (event.reason == "hungry") (2)
einstein.feed();
else
einstein.walk();
}
1 | 请注意,参数是事件类型“BarkEvent”。 |
2 | 这就是为什么我们现在可以访问`reason`属性并执行相应地操作。 |
这样,任何熟悉BarkEvent的狗主人终将能够真正了解他们的狗。 实在是一项成就!
2.5.4. 简化
正如你所见:创建这个额外的类只是为了能传递那个“reason”字符串有点麻烦。 毕竟,它通常只是我们感兴趣的一个单一的信息。 不得不为这样简单的机制创建额外的类感觉有点低效。
这就是为什么你实际上不需要经常使用子类方法。 相反,你可以使用Event类的`data`属性,它可以存储任意引用(它是泛类型:Object)。
将BarkEvent逻辑替换为:
// create & dispatch event
var event:Event = new Event(Dog.BARK);
event.data = "hungry"; (1)
dispatchEvent(event);
// listen to event
einstein.addEventListener(Dog.BARK, onBark);
function onBark(event:Event):void
{
trace("reason: " + event.data as String); (2)
}
1 | 将reason存储在`data`属性中。 |
2 | 为了回到原因,将`data`转换为String。 |
这种方法的缺点是我们失去一些类型安全。 但在我看来,与其实现一个完整的类,我宁愿使用类型转换。
此外,Starling还有几个快捷方法,可以进一步简化此代码! 看这个:
// create & dispatch event
dispatchEventWith(Dog.BARK, false, "hungry"); (1)
// listen to event
einstein.addEventListener(Dog.BARK, onBark);
function onBark(event:Event, reason:String):void
{
trace("reason: " + reason); (2)
}
1 | 创建类型为“Dog.BARK”的事件,填充“data”属性,并调度事件 - 一行代码完成。 |
2 | `data`属性被传递给事件处理程序的(可选)第二个参数。 |
我们摆脱了相当多的笨重的代码! 当然,即使您不需要任何自定义数据,您也可以使用相同的机制。 让我们看看最简单的事件交互:
// create & dispatch event
dispatchEventWith(Dog.HOWL); (1)
// listen to event
dog.addEventListener(Dog.HOWL, onHowl);
function onHowl():void (2)
{
trace("hoooh!");
}
1 | 通过仅指定事件的类型来分派事件。 |
2 | 注意,此函数不包含任何参数!如果您不需要它们,则无需指定它们。 |
注意:简化的`dispatchEventWith`调用实际上甚至更高的内存效率,因为Starling将在后台复用Event对象。
2.5.5. 冒泡
在我们前面的例子中,事件分派器和事件监听器通过`addEventListener`方法直接连接。 但有时,这不是你想要的。
让我们假设你创建了一个带有深层显示列表的复杂游戏。 在这个列表的某个分支上,Einstein(这个游戏的主角 - 狗)陷入陷阱。 他在痛苦中嚎叫,在他的最后呼吸中,发出一个“GAME_OVER”事件。
不幸的是,这些信息需要在显示列表中,在游戏的根类中 在这样的事件中,它通常重置等级并将狗返回到其最近一个数据备份点。 将这个事件从狗身上通过多个显示对象,直到到达游戏根为止,这将是非常麻烦的。
这是一个非常常见的要求 - 这就是事件系统支持向上冒泡(bubbling)的原因。
想象一个真正的树(它是你的显示列表),并将其转动180度,使树干指向上方。 树干,这是你的舞台,树的叶子是你的显示对象。 现在,如果一个叶子创造了一个冒泡事件,那么该事件将像一杯苏打水中的气泡一样向上移动,从一个分支到另一个分支(从父母到另一个),直到它最终到达树干。
沿此路由的任何显示对象都可以侦听此事件。 它甚至可以弹出泡沫,阻止它进一步旅行。所有这一切都需要设置一个事件的`bubbles`属性为true。
// classic approach:
var event:Event = new Event("gameOver", true); (1)
dispatchEvent(event);
// one-line alternative:
dispatchEventWith("gameOver", true); (2)
1 | 传递`true`作为Event构造函数的第二个参数激活冒泡。 |
2 | 或者,`dispatchEventWith`使用完全相同的参数。 |
在任何地方沿着它的路径,你可以听这个事件,例如。 对狗,其父母或阶段:
dog.addEventListener("gameOver", onGameOver);
dog.parent.addEventListener("gameOver", onGameOver);
stage.addEventListener("gameOver", onGameOver);
这个功能在许多情况下都很方便; 尤其是当通过鼠标或触摸屏进行用户输入时。
2.5.6. 触摸事件
虽然典型的台式计算机由鼠标控制,但大多数移动设备(如智能手机或平板电脑)都是用手指控制的。
Starling统一了这些输入方式,将所有“指向设备”输入视为“TouchEvent”。 这样,你不必关心游戏控制的实际输入方式。 输入设备是鼠标,手写笔还是手指:Starling将始终分派触摸事件。
如果你想支持多点触摸,第一件事:请确保在创建你的Starling实例之前启用它。
Starling.multitouchEnabled = true;
var starling:Starling = new Starling(Game, stage);
starling.simulateMultitouch = true;
注意属性`simulateMultitouch`。 如果启用它,您可以使用鼠标在开发计算机上模拟多点触摸输入。 当您移动鼠标光标时,按住键盘的[Ctrl]键(Windows)或键盘的[Cmd]键(Mac)即可试用。 再按住键盘的[Shift]以更改替代光标的移动方式。
要对触摸事件(真实或模拟)做出反应,您需要侦听类型为“TouchEvent.TOUCH”的事件。
sprite.addEventListener(TouchEvent.TOUCH, onTouch);
您可能已经注意到,我刚刚将事件侦听器添加到Sprite实例。 但是Sprite是一个容器类;它没有任何有形的可见表面。 仍然能触摸它吗?
是的,能 - 感谢bubbling。
要理解这个,先回想一下我们之前创建的MessageBox类。 当用户点击其文本字段时,任何监听文本字段上的触摸事件的人都必须得到通知 - 到目前为止,显而易见。 但是对于在消息框本身上监听触摸事件的人也是如此;毕竟文本字段是消息框的一部分。 即使有人监听舞台上的触摸事件,他应该也得到通知。 触摸显示列表中的任何对象同时意味着触摸舞台!
由于冒泡事件,Starling可以轻松地代表这种类型的交互。 当它检测到屏幕上的触摸时,它计算出哪个叶节点对象被触摸。 它创建一个TouchEvent并在该对象上调度它。 从那里,它将沿着显示列表冒泡,一直到根。
Touch Phases
Time to look at an actual event listener:
private function onTouch(event:TouchEvent):void
{
var touch:Touch = event.getTouch(this, TouchPhase.BEGAN);
if (touch)
{
var localPos:Point = touch.getLocation(this);
trace("Touched object at position: " + localPos);
}
}
这是最基本的情况:找出是否有人触摸屏幕并描绘出坐标。 `getTouch`方法由TouchEvent类提供,并帮助您找到感兴趣的触摸。
注意:Touch类封装了单次触摸的所有信息:发生的地方,它在前一帧中的位置等。
作为第一个参数,我们将`this`传递给`getTouch`方法。 因此,我们要求它返回在’this`或其children上发生的任何触摸。
触摸在他们的生命周期内经历了一些阶段:
TouchPhase.HOVER
|
仅用于鼠标输入;当鼠标按钮up状态时移动到对象上时调度。 |
TouchPhase.BEGAN
|
手指刚刚触摸屏幕,或者鼠标按钮被按下。 |
TouchPhase.MOVED
|
手指在屏幕上移动,或者按下按钮时鼠标移动。 |
TouchPhase.STATIONARY
|
手指或鼠标(按下按钮)自最后一帧以来未移动。 |
TouchPhase.ENDED
|
手指从屏幕提起或鼠标按钮弹起。 |
因此,上面的示例(寻找开始阶段’BEGAN')将在手指触摸屏幕的准确时刻写入跟踪输出,而不是在移动或离开屏幕时。
多点触控
在上面的示例中,我们只监听单次触摸(即只有一个手指)。 多点触摸的处理非常相似;唯一的区别是需要调用`touchEvent.getTouches`(注意复数)。
var touches:Vector.<Touch> = event.getTouches(this, TouchPhase.MOVED);
if (touches.length == 1)
{
// one finger touching (or mouse input)
var touch:Touch = touches[0];
var movement:Point = touch.getMovement(this);
}
else if (touches.length >= 2)
{
// two or more fingers touching
var touch1:Touch = touches[0];
var touch2:Touch = touches[1];
// ...
}
`getTouches`方法返回一个接触者向量。 我们可以基于该向量的长度和内容来确定逻辑。
-
在第一个if子句中,屏幕上只有一个手指。通过`getMovement`,我们可以实现拖动手势。
-
在else子句中,两个手指在屏幕上。通过访问两个触摸对象,我们可以实施捏合手势。
注意:作为Starling下载一部分的演示应用程序包含 TouchSheet 类,该类使用在Multitouch场景中。 它显示了允许拖动,旋转和缩放sprite的触摸处理程序的示例实现。
2.6. 动画
动画不仅是任何游戏的基本组成部分;也为现代商业应用程序提供平滑且生动的过渡。 显然,提供一套能快速响应设置且直观的接口对控制动画大有裨益。 正因为如此,Starling内置了一个非常灵活的动画引擎。
如你所想,动画可以简单分为两种类型。
-
第一种,动画变化非常动态,你无法预知会发生什么。比如有一个向玩家移动的敌人:它的方向和速度需要根据环境每一帧更新。每个额外的力量或碰撞都可能改变移动轨迹。
-
另一种,预定义动画,这种动画遵循一定的规律;你从开始就知道会发生什么。比如消息框中淡出,或者从一个屏幕过渡到另一个屏幕。
我们将在以下部分示例这两种类型。
2.6.1. 帧事件
在一些游戏引擎中,有一个叫做run-loop的机制。 这是一个无限循环,不断更新场景的所有元素。
在Starling中,由于显示列表架构,这样的循环没有什么意义。 我们已经将游戏分成许多不同的自定义显示对象,每个人都应该知道哪段时间该做什么。
这正是帧事件(EnterFrameEvent)的要点:允许显示对象随时间更新自身。 每帧,该事件被分派给位于显示列表中的所有显示对象。 下面讲如何使用它:
public function CustomObject()
{
addEventListener(Event.ENTER_FRAME, onEnterFrame); (1)
}
private function onEnterFrame(event:Event, passedTime:Number):void (2)
{
trace("Time passed since last frame: " + passedTime);
bird.advanceTime(passedTime);
}
1 | 您可以在任何地方向此事件添加侦听器,但构造函数是一个很好的选择。 |
2 | 这是对应的事件侦听器的样子。 |
方法`onEnterFrame`每帧被调用一次,并且传递自上一帧以来已经过去的时间参数。 有了这些信息,你可以移动你的敌人,更新太阳的高度,或做任何其他需要。
这个事件背后的蕴含力量是,每次事件发生时你可以做完全不同的事情。 你可以动态地响应游戏的当前状态。
例如,你可以让敌人向玩家迈进一步;甚至一个简单形式的敌人AI,如果你愿意!
2.6.2. Tweens
现在到预定义的动画。
它们非常常见,例如movement,scale,fade等。
Starling处理这些动画的方法很简单 - 但同时非常灵活。
基本上,你可以缓动任何对象的任何属性,只要它是数字(Number
,int
,uint
)。
这些动画描述被封装在称为Tween的对象中。
术语“Tween”来自手工绘制的动画,其中铅笔插画将绘制重要的关键帧,而团队的其余人员绘制关键帧之间(in-between)的其它帧。 |
了解了这些理论,接下来我们举个例子:
var tween:Tween = new Tween(ball, 0.5);
tween.animate("x", 20);
tween.animate("scale", 2.0);
tween.animate("alpha", 0.0);
这个补间描述了一个动画,将“ball”对象移动到“x = 20”,将其放大两倍,并降低其不透明度,直到它不可见。 所有这些变化将在半秒内同时进行。 起始值仅仅是指定属性的当前值。
这个示例告诉我们:
-
你可以对一个对象的任意属性进行动画处理
-
您可以在一个补间对象中组合多个动画。
Apropos: 由于缩放,淡入淡出和移动的频率很高,所以Tween类也提供了更为具体的方法。 你可以这样写:
tween.moveTo(20, 0); // animate "x" and "y"
tween.scaleTo(2); // animate "scale"
tween.fadeTo(0); // animate "alpha"
补间动画的一个有趣的功能是,你可以改变动画的执行方式,例如。 让它先慢后快。 这是通过指定过渡类型做到的。
以下示例显示如何指定这样的转换,并介绍此类的更多功能。
var tween:Tween = new Tween(ball, 0.5, Transitions.EASE_IN); (1)
tween.onStart = function():void { /* ... */ };
tween.onUpdate = function():void { /* ... */ }; (2)
tween.onComplete = function():void { /* ... */ };
tween.delay = 2; (3)
tween.repeatCount = 3; (4)
tween.reverse = true;
tween.nextTween = explode; (5)
1 | 通过第三个构造函数参数指定过渡类型。 |
2 | 这些回调在补间启动时,每帧更新时或完成时执行。 |
3 | 延迟两秒钟再执行动画。 |
4 | 重复播放动画三次,可选择以yoyo风格(reverse )。 如果将`repeatCount`设置为零,动画将被无限重复。 |
5 | 指定下一个动画补间,在此动画完成后立即开始。 |
我们刚刚创建和配置了一个补间动画 - 但什么也没有发生。 一个tween对象描述了动画如何运行,但它并未开始运行。
你可以通过tweens`advanceTime`方法手动执行:
ball.x = 0;
tween = new Tween(ball, 1.0);
tween.animate("x", 100);
tween.advanceTime(0.25); // -> ball.x = 25
tween.advanceTime(0.25); // -> ball.x = 50
tween.advanceTime(0.25); // -> ball.x = 75
tween.advanceTime(0.25); // -> ball.x = 100
嗯,它们正常工作了,但这样编码有点麻烦,不是吗? 虽然可以在一个`ENTER_FRAME`事件响应函数中调用`advanceTime`,但是只要你有多个动画,它仍然一定会变得繁琐。
别担心:我知道有个好家伙。 他真的善于处理这样的事情。
2.6.3. Juggler
Juggler(魔术师)接受并执行任意数量的动画对象。 像任何真正的艺术家一样,它将以饱满的热情坚定地追求其生命价值:连续调用`advanceTime`。
在活动Starling实例上总是有一个默认的juggler。 执行动画的最简单的方法是通过下面这行代码 - 只要将动画分配给默认的魔术师,你就完成了。
Starling.juggler.add(tween);
当动画完成后,它将被自动释放,无需后续设置。 在许多情况下,这种简单的方法将是你需要的!
在某些情况下,你需要更多的控制。 假设你的舞台包含一个主要动作发生的游戏区域。 当用户点击暂停按钮时,您想要暂停游戏并显示一个消息框(消息框本身也有动画),也许再提供一个选项返回到游戏菜单。
当这种情况发生时,游戏应该完全冻结:它的动画不应该再继续。 问题是消息框本身也使用一些动画,所以我们不能只让默认的魔术师停止工作。
在这种情况下,给游戏区域额外分配一个魔术师就变得有必要。 一旦按下退出按钮,这个魔术师应该停止一切工作。 游戏将冻结在当前画面,而消息框(使用默认的魔术师,或者可能是另一个)动画仍然在工作。
当你创建一个自定义的魔术师,你所要做的就是在每一帧中调用它的“advanceTime”方法。 我建议通过以下方式使用jugglers:
public class Game (1)
{
private var _gameArea:GameArea;
private function onEnterFrame(event:Event, passedTime:Number):void
{
if (activeMsgBox)
trace("waiting for user input");
else
_gameArea.advanceTime(passedTime); (2)
}
}
public class GameArea
{
private var _juggler:Juggler; (3)
public function advanceTime(passedTime:Number):void
{
_juggler.advanceTime(passedTime); (4)
}
}
1 | 在你的游戏的根类,监听`Event.ENTER_FRAME`。 |
2 | 仅当没有活动消息框时才推进`gameArea'。 |
3 | GameArea包含自己的魔术师。 它将管理所有的游戏内动画。 |
4 | 玩家在其“advanceTime”方法(由Game调用)中前进。 |
这样,你已经完美地分离了游戏的动画和消息框。
顺便说一下:魔术师不限于Tweens家族。 只要一个类实现了“IAnimatable”接口,就可以将它列入到魔术师阵营。 该接口只有一个方法:
function advanceTime(time:Number):void;
通过实现此接口,您可以 自己创建一个简单的MovieClip类。 在它的“advanceTime”方法中,它会不断地改变显示的纹理。 要启动影片剪辑,您只需将其分配给魔术师。
这留下一个问题,那就是:什么时候并且如何从魔术师中删除一个动画对象?
停止动画
当动画完成后,它会自动移除。 如果要在动画完成之前中止动画,则只需从魔术师中删除这个动画。
让我们看看一个例子,创建一个动画:球移动,并将它分配给默认魔术师:
tween:Tween = new Tween(ball, 1.5);
tween.moveTo(x, y);
Starling.juggler.add(tween);
下面列举了几种可以中止该动画方法。 你只需选择最适合您的当前游戏逻辑的一个。
var animID:uint = juggler.add(tween);
Starling.juggler.remove(tween); (1)
Starling.juggler.removeTweens(ball); (2)
Starling.juggler.removeByID(animID); (3)
Starling.juggler.purge(); (4)
1 | 直接删除动画。 这适用于任何`IAnimatable`对象。 |
2 | 删除影响球的所有动画。 只适用于tweens! |
3 | 通过其ID删除动画。 当您无法访问Tween实例时很有用。 |
4 | 如果你想中止一切,让魔术师停止工作。 |
但是有一点小心,“purge”的方法,会使你的所有依赖默认魔术师的动画嘎然而止,显得很突兀。 我建议你只在自定义的魔术师身上使用“purge”方法。
自动删除
你可能已经问自己,如何知道Tween类在动画完成后自动从运行时中删除了? 这可以通过`REMOVE_FROM_JUGGLER`事件来获取通知。
实现“IAnimatable”的任何对象都可以调度这样的事件; 玩家监听这些事件并相应地删除对象。
public class MyAnimation extends EventDispatcher implements IAnimatable
{
public function stop():void
{
dispatchEventWith(Event.REMOVE_FROM_JUGGLER);
}
}
单命令Tweens
虽然tween和juggler的分离是非常强大的功能,但它有时这个分离反而迫使你为简单的任务写了很多代码。 这就是为什么在魔术师身上有一个方便的方法,允许您使用单个命令创建和执行补间。 下面是一个示例:
juggler.tween(msgBox, 0.5, {
transition: Transitions.EASE_IN,
onComplete: function():void { button.enabled = true; },
x: 300,
rotation: deg2rad(90)
});
这将为`msgBox`对象创建一个动画,持续时间为0.5秒,同时为`x`和`rotation`属性生成动画。
正如你所看到的,{}
-parameter用来列出你想要动画的所有属性,以及Tween本身的属性。
非常节省时间!
2.6.4. 延迟调用
从技术上讲,我们现在已经涵盖了Starling支持的所有动画类型。 然而,实际上还有另一个概念与这个主题密切相关。
记住Einstein,给我们介绍事件系统的那位智能狗? 最后一次我们看到他,他刚刚失去了所有的健康点,并打算叫“gameOver”。 但等待:不要立即调用该方法 - 这将突然结束游戏。 相反,调用它的延迟,例如,两秒钟(足够的时间,让玩家实现正在展开的剧情)。
要实现该延迟,您可以使用flash自带的Timer或`setTimeout`方法。 然而,你也可以使用魔术师,这有一个巨大的优势:你完全掌控一切。
当你想象玩家现在点击“暂停”按钮,在这两秒过去之前变得很明显。 在这种情况下,你不仅想要停止游戏区的动画;你希望这个延迟的`gameOver`调用被延迟更多。
为此,请进行如下操作:
juggler.delayCall(gameOver, 2);
`gameOver`函数将从现在开始调用两秒钟(如果魔术师工作被中断,则更长时间)。 也可以给这个函数传递一些参数。 想要派发事件吗?
juggler.delayCall(dispatchEventWith, 2, "gameOver");
使用延迟调用的另一种方便的方法是执行周期性操作。 想象你想要每三秒产生一个新的敌人。
juggler.repeatCall(spawnEnemy, 3);
在幕后,`delayCall`和`repeatCall`都创建一个DelayedCall类型的对象。 就像juggler.tween方法是使用tweens的快捷方式,这些方法是创建延迟调用的快捷方式。 |
要中止延迟调用,请使用以下方法之一:
var animID:uint = juggler.delayCall(gameOver, 2);
juggler.removeByID(animID);
juggler.removeDelayedCalls(gameOver);
2.6.5. 影片剪辑
当我们查看关于Mesh的类图的时候,你可能已经注意到MovieClip类。 事实如此:MovieClip实际上只是Image的一个子类,随着时间的推移改变其纹理。 可以认为他是Starling中的一个GIF动画!
获取纹理
建议您的影片剪辑的所有帧都来自一个纹理图集,并且所有帧都具有相同的大小(如果没有,它们将被拉伸到第一帧的大小)。 您可以使用像Adobe Animate这样的工具来创建这样的动画; 它可以直接导出为Starling的纹理图集格式。
这是包含影片剪辑的帧的纹理图集的示例。 首先,看看带有帧坐标的XML。 请注意,每个帧都以前缀`flight_`开头。
<TextureAtlas imagePath="atlas.png">
<SubTexture name="flight_00" x="0" y="0" width="50" height="50" />
<SubTexture name="flight_01" x="50" y="0" width="50" height="50" />
<SubTexture name="flight_02" x="100" y="0" width="50" height="50" />
<SubTexture name="flight_03" x="150" y="0" width="50" height="50" />
<!-- ... -->
</TextureAtlas>
这里是相应的纹理:
创建影片剪辑
现在让我们创建影片剪辑。 假设“atlas”变量指向包含上述影片剪辑所有帧的TextureAtlas,这很容易。
var frames:Vector.<Texture> = atlas.getTextures("flight_"); (1)
var movie:MovieClip = new MovieClip(frames, 10); (2)
addChild(movie);
movie.play();
movie.pause(); (3)
movie.stop();
Starling.juggler.add(movie); (4)
1 | `getTextures`方法返回从给定前缀开始的所有纹理,按字母顺序排序。 |
2 | 这是我们MovieClip的理想选择,因为我们可以将这些纹理传递给它的构造函数。第二个参数描述了每秒播放多少帧。 |
3 | 这些是控制剪辑的回放的方法。默认情况下,它将处于“播放”模式。 |
4 | 重要提示:就像Starling中的任何其他动画一样,影片剪辑需要分配给魔术师! |
你注意到我们通过他们的前缀`flight_`引用了图集的纹理吗? 这允许您创建包含其他影片剪辑和纹理的混合图集。 要将一个剪辑的帧组合在一起,您只需对所有剪辑使用相同的前缀。
该类还支持在达到某个帧时执行声音或任意回调。 请务必查看其API参考,了解可能的情况!
2.7. 资源管理
现在应该弄明白一件事:纹理构成了每个应用程序资源的重要组成部分。 特别是游戏,需要很多图形;从用户界面到字符,列表细项,背景等 这还不是全部,您可能还需要管理声音和配置文件。
要引用这些资源,你有几个选择:
-
将它们嵌入到应用程序(通过`[Embed]'元数据)。
-
从磁盘加载它们(仅适用于AIR应用程序)。
-
从URL加载它们,例如来自网络服务器。
由于每种访问方式都需要编写不同的代码(取决于资源类型和加载机制),所以很难以统一的方式访问资源。 幸运的是,Starling包含一个能解决这个问题的帮助类:AssetManager。
它支持以下类型的资源:
-
纹理(来自位图或ATF数据)
-
纹理图集
-
位图字体
-
声音
-
XML数据
-
JSON数据
-
字节数组
为了实现这一点,AssetManager使用三步法:
-
将资源的"指针"添加到队列中,例如`File`对象或URL。
-
告诉AssetManager处理队列。
-
一旦队列处理完成,您可以使用相应的“get”方法访问所有资源。
AssetManager包含一个`verbose`属性。 如果启用,则排队和加载过程的所有步骤都将输出到控制台。 这对调试非常有用,例如当您不明白为什么某些资源没有正常显示的时候! 因此,最新的Starling版本已默认启用。 |
2.7.1. 把资源加入到队列
第一步是将您要使用的所有资源加入到队列。 如何加入完全取决于每个资源的类型和来源。
来自磁盘或网络的资源
把磁盘或远程服务器的文件添加到队列相当简单:
// 从远程URL加载资源
assets.enqueue("http://gamua.com/img/starling.jpg");
// 从磁盘加载资源(仅限AIR)
var appDir:File = File.applicationDirectory;
assets.enqueue(appDir.resolvePath("sounds/music.mp3"));
// 递归查找此目录下的所有文件并加入到队列(仅限AIR)。
assets.enqueue(appDir.resolvePath("textures"));
要加载纹理图集 ,只需将其XML文件和相应的纹理添加到队列。 需要确保XML文件中的“imagePath”属性包含正确的文件名,因为在以后创建图集时,AssetManager将会查找它。
assets.enqueue(appDir.resolvePath("textures/atlas.xml"));
assets.enqueue(appDir.resolvePath("textures/atlas.png"));
位图字体 加载方法跟上述图集类似。 不过在这种情况下,您需要确保的是XML中的`file`属性(`.fnt`文件)设置正确。
assets.enqueue(appDir.resolvePath("fonts/desyrel.fnt"));
assets.enqueue(appDir.resolvePath("fonts/desyrel.png"));
嵌入资源
对于嵌入资源,我建议您将所有嵌入语句放入一个专用类中。 将它们声明为“public static const”,并遵循以下命名约定:
-
嵌入图像的类别应该与文件具有完全相同的名称,而不需要扩展名。 这是必需的,来自XML(图集,位图字体)的引用因此才不会被打断。
-
图集XML和字体XML文件可以有任意的名称,因为它们的文件名不会被引用。
Here’s a sample of such a class:
public class EmbeddedAssets
{
/* PNG纹理 */
[Embed(source = "/textures/bird.png")]
public static const bird:Class;
/* ATF纹理 */
[Embed(source = "textures/1x/atlas.atf",
mimeType = "application/octet-stream")]
public static const atlas:Class;
/* XML文件 */
[Embed(source = "textures/1x/atlas.xml",
mimeType = "application/octet-stream")]
public static const atlas_xml:Class;
/* MP3声音 */
[Embed(source = "/audio/explosion.mp3")]
public static const explosion:Class;
}
当您将该类添加到任务队列后,资源管理器将稍后实例化所有嵌入其中的资源。
var assets:AssetManager = new AssetManager();
assets.enqueue(EmbeddedAssets); (1)
1 | 将“鸟”纹理,“爆炸声”和纹理图集添加到加载队列。 |
单项资源配置
当您手动创建纹理(通过“Texture.from …()”工厂方法)时,您有机会微调它的创建方式。 例如,您可以决定纹理格式或比例因子。
这些设置的存在一个需要注意的问题:创建纹理后,您无法再更改它们。 因此,您需要确保在创建纹理时设置正确。 资源管理器也支持这种配置:
var assets:AssetManager = new AssetManager();
assets.textureFormat = Context3DTextureFormat.BGRA_PACKED;
assets.scaleFactor = 2;
assets.enqueue(EmbeddedAssets);
资源管理器将遵从这些设置创建所有纹理。 但是,这似乎只允许统一设置属性然后加载纹理,对吧? 实际上不是这样的,你可以在分开的几个步骤中,在每次调用enqueue之前分配正确的设置。
assets.scaleFactor = 1;
assets.enqueue(appDir.resolvePath("textures/1x"));
assets.scaleFactor = 2;
assets.enqueue(appDir.resolvePath("textures/2x"));
这将使来自“1x”和“2x”文件夹的纹理分别使用1和2的缩放因子。
2.7.2. 加载资源
现在资源队列已经添加完成了,您可以一次加载所有资源。 根据您正在加载的资源的数量和大小,这可能需要一段时间。 因此,最好向用户展示某种进度条或加载指示符。
assets.loadQueue(function(ratio:Number):void
{
trace("Loading assets, progress:", ratio);
// when the ratio equals '1', we are finished.
if (ratio == 1.0)
startGame();
});
请注意,“startGame”方法是您必须自己实现的;这时你就可以隐藏加载画面并开始实际的游戏。
如果启用了“verbose”属性,您将看到可供访问的资源名称:
[AssetManager] Adding sound 'explosion' [AssetManager] Adding texture 'bird' [AssetManager] Adding texture 'atlas' [AssetManager] Adding texture atlas 'atlas' [AssetManager] Removing texture 'atlas'
你注意到了吗? 在最后一行,在创建纹理图集之后,实际上删除了“atlas”纹理。 这是为什么? 一旦创建了图集,您就不再需要对图集纹理感兴趣,因为它们已经被包含在子纹理中了。 因为另外提供了访问纹理的入口,因此可以去除内存中实际的图集纹理。 位图字体也是如此。 |
2.7.3. 访问资源
现在队列完成处理了,您可以使用AssetManager的各种“get …”方法访问您的资源。 每个资源都由一个名称来引用,它是资源的文件名(不带扩展名)或嵌入对象的类名。
var texture:Texture = assets.getTexture("bird"); (1)
var textures:Vector.<Texture> = assets.getTextures("animation"); (2)
var explosion:SoundChannel = assets.playSound("explosion"); (3)
1 | 首先搜索独立命名的纹理,然后搜索图集纹理。 |
2 | 搜索与上述相同,但返回从给定的字符串开始的所有(子)纹理。 |
3 | 播放声音并返回控制它的SoundChannel。 |
如果您之前已经将位图字体加入到队列中了,那么队列完成后它将被注册并可以使用。
在我的游戏中,我通常会在我的根类上存储资源管理器的引用,并且可以通过`static`属性访问。 这使得我非常容易从游戏中的任何地方访问资源,只需通过调用“Game.assets.get …()”(假定根类称为“Game”)。 |
2.8. 片段过滤器
到目前为止,我们所有的渲染都是映射到包含(或不包含)贴图的网格。 你可以移动网格,缩放它们,旋转它们,也许以不同的颜色着色。 总而言之,可能性是相当有限的 - 游戏的外观完全决定于纹理。
在某些时候,你会遇到这种方法的极限; 也许你需要一个图像的变体,多种颜色,模糊或阴影。 如果将所有这些变体添加到纹理图集中,您将很快耗尽内存。
Fragment filters(片段过滤器)可以帮助你解决上述问题。 过滤器可以附加到任何显示对象(包括容器),并且可以完全改变其外观。
例如,假设您要向对象添加高斯模糊:
var filter:BlurFilter = new BlurFilter(); (1)
object.filter = filter; (2)
1 | 创建并配置所需滤镜的实例。 |
2 | 将滤镜分配给显示对象的`filter`属性。 |
使用分配的滤镜,显示对象的呈现将被修改为这样:
-
每帧,目标对象都呈现为一个纹理。
-
纹理由片段着色器(直接在GPU上)处理。
-
一些滤镜使用多遍,即一个着色器的输出作为下一次的输入。
-
最后一个着色器的输出被绘制到后台缓冲区。
这种方法非常灵活,允许产生各种不同的效果(我们将很快看到)。 此外,它极大地利用了GPU的并行处理能力;所有昂贵的逐像素处理逻辑都在图形芯片上执行。
也就是说:滤镜会中断批处理,每个滤镜工作时需要单独的绘制调用。 无论是内存使用和还是渲染性能,他们消耗都不低。 所以要小心,明智地使用它们。
2.8.1. 展示
Starling有一些非常有用且易用的滤镜。
模糊滤镜
对显示对象应用高斯模糊。可以分别对x轴和y轴设置模糊的强度。
-
每个模糊方向,滤镜需要至少一个渲染过程(绘制调用)。
-
每个强度单位,滤镜需要一次pass(1的强度需要一次pass,2的强度通过2次pass)。
-
通过降低滤波器分辨率,而不是提高模糊强度, 来达到相似的模糊效果,这样会节省更多的性能。
颜色矩阵滤镜
动态地更改对象的颜色。更改对象的亮度,饱和度,色调或将其完全反转。
该滤镜将每个像素的颜色和α值乘以4×5矩阵。 这是一个非常灵活的设计,但是设置相应的矩阵也很麻烦。 为此,该类包含几个方法,帮助您设置想要实现效果的矩阵(例如,更改色调或饱和度)。
-
您可以在一个滤镜实例中组合多个颜色转换。 例如,要更改亮度和饱和度,请调用滤镜内置的相应方法。
-
此滤镜总是需要一次pass。
2.8.2. 性能提示
我上面提到它:虽然GPU处理部分是非常高效的,但额外的绘制调用使片段过滤器相当昂贵。 但是,Starling尽力优化滤镜性能。
-
当一个对象接连两帧不改变它相对于舞台的位置(或其他属性,比如尺度和颜色),Starling识别到这一点,会自动缓存过滤器输出。 这意味着滤镜将不再需要被处理;相反,它的行为就像一个单一的图像。
-
另一方面,当对象不断移动时,最后的滤镜总是通过后台缓冲区直接呈现而不是纹理。 这只有一次绘制调用。
-
如果你想继续使用滤镜输出,即使对象在移动,调用`filter.cache()
。 同样,这将使对象的行为就像一个静态图像。 但是,如果目标对象图像有任何更改,您必须再次调用`cache
(或`uncache`)。 -
为了节省内存,不妨试试’resolution’和’textureFormat’属性。 但这将降低图像质量。
2.8.3. 更多滤镜
您想知道如何创建自己的滤镜吗? 不要担心,我们将探讨该主题稍后…。
在此期间,您可以尝试使用由其他Starling开发人员创建的滤镜。 一个很好的例子是 filter collection by devon-o.
2.9. 网格
Mesh是所有有形显示对象的基本构建块。 “有形”是指显示列表中的一个“叶”对象:一个不是容器但直接呈现给后台缓冲区的对象。 因为它是如此重要,我想更详细地谈谈这个类。
简而言之,Mesh表示通过Stage3D渲染的三角形列表。 它已经被提到了几次,因为它是Quad和Image的基类。 作为提醒,先列出我们正在谈论的类层次结构:
Mesh不是一个抽象类; 没有什么阻止你直接实例化它。 就是这样:
var vertexData:VertexData = new VertexData();
vertexData.setPoint(0, "position", 0, 0);
vertexData.setPoint(1, "position", 10, 0);
vertexData.setPoint(2, "position", 0, 10);
var indexData:IndexData = new IndexData();
indexData.addTriangle(0, 1, 2);
var mesh:Mesh = new Mesh(vertexData, indexData);
addChild(mesh);
正如你所看到的,我们首先需要实例化两个类:VertexData和IndexData。 它们分别表示顶点和索引的集合。
-
VertexData有效地存储每个顶点的属性,例如。其位置和颜色。
-
IndexData存储这些顶点的索引。每三个指数将构成一个三角形。
这样,上面的代码创建了最基本的绘图单元:三角形。 我们通过定义三个顶点并顺时针引用它们来做到这一点。 毕竟,GPU非常擅长做这件事:绘制三角形 - 非常多的三角形。
2.9.1. 扩展网格
直接使用VertexData和IndexData将是相当麻烦的。 将所需要的设置代码封装在一个类中是有意义的。
为了说明如何创建自定义网格,我们现在将编写一个名为NGon的简单类。 它的任务:用自定义颜色渲染常规的n边形。
我们希望此类用起来像一个flash内置的显示对象。 您实例化它,将其移动到某个位置,然后将其添加到显示列表。
var ngon:NGon = new NGon(100, 5, Color.RED); (1)
ngon.x = 60;
ngon.y = 60;
addChild(ngon);
1 | 构造函数参数定义半径,边数和颜色。 |
让我们看看我们如何能够实现这个功绩。
2.9.2. 顶点设置
像所有其他形状,我们的正多边形也是由几个三角形构成。 下面是我们如何设置一个五边形的三角形(一个n边多边形,n = 5)。
五边形由六个顶点构成,横跨五个三角形。 我们给每个顶点一个在0和5之间的数字,5在中心。
如上所述,顶点存储在VertexData实例中。 VertexData为每个顶点定义一组属性。 在这个示例中,我们需要两个标准属性:
-
`position`存储一个二维点(x,y)。
-
`color`存储RGBA颜色值。
VertexData类定义了一些访问这些属性的方法。 这允许我们设置多边形的顶点。
创建一个名为NGon的新类,扩展自Mesh。 然后添加以下实例方法:
private function createVertexData(
radius:Number, numEdges:int, color:uint):VertexData
{
var vertexData:VertexData = new VertexData();
vertexData.setPoint(numEdges, "position", 0.0, 0.0); (1)
vertexData.setColor(numEdges, "color", color);
for (var i:int=0; i<numEdges; ++i) (2)
{
var edge:Point = Point.polar(radius, i*2*Math.PI / numEdges);
vertexData.setPoint(i, "position", edge.x, edge.y);
vertexData.setColor(i, "color", color);
}
return vertexData;
}
1 | 设置中心顶点(最后一个索引)。 |
2 | 设置边顶点。 |
由于我们的网格有一个统一的颜色,我们为每个顶点分配相同的颜色。 边缘顶点(拐角)的位置沿着具有给定半径的圆分布。
2.9.3. 索引设置
这么多的顶点。 现在,我们需要定义组成多边形的三角形。
Stage3D想要一个简单的列表,即索引列表,每三个连续的索引组成一个三角形。 顺时针参考指标是一个好的做法; 这个约定表示我们正在看三角形的前侧。 我们的五边形的列表将如下所示:
5, 0, 1, 5, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 0
在Starling中,IndexData类用于设置索引列表。 以下方法将使用适当的索引填充IndexData实例。
private function createIndexData(numEdges:int):IndexData
{
var indexData:IndexData = new IndexData();
for (var i:int=0; i<numEdges; ++i)
indexData.addTriangle(numEdges, i, (i+1) % numEdges);
return indexData;
}
2.9.4. NGon构造函数
这实际上是我们需要的NGon类! 现在我们只需要在构造函数中使用上面的方法。 显示对象的所有其他职责(碰撞测试,渲染,边界计算等)由超类处理。
public class NGon extends Mesh
{
public function NGon(
radius:Number, numEdges:int, color:uint=0xffffff)
{
var vertexData:VertexData = createVertexData(radius, numEdges, color);
var indexData:IndexData = createIndexData(numEdges);
super(vertexData, indexData);
}
// ...
}
这很直接,不是吗? 这种方法适用于你可以想到的任何形状。
当使用自定义网格时,还要查看Polygon类(在`starling.geom`包中)。 它有助于将任意封闭形状(由多个顶点定义)转换为三角形。 我们在<< 遮罩 >>部分更详细地讨论它。 |
2.9.5. 添加纹理
如果我们能够将纹理映射到这个多边形,不是很好吗? 基类Mesh已经定义了一个`texture`属性;我们只缺少所需的纹理坐标。
通过纹理坐标,您可以定义纹理的哪个部分映射到哪个顶点。 它们通常称为UV坐标,它是对通常用于其坐标轴(u和v)的名称的引用。 请注意,UV范围定义在0和1内,是一个比值,与实际纹理尺寸无关。
有了这些信息,我们可以相应地更新`createVertexData`方法。
function createVertexData(
radius:Number, numEdges:int, color:uint):VertexData
{
var vertexData:VertexData = new VertexData(null, numEdges + 1);
vertexData.setPoint(numEdges, "position", 0.0, 0.0);
vertexData.setColor(numEdges, "color", color);
vertexData.setPoint(numEdges, "texCoords", 0.5, 0.5); (1)
for (var i:int=0; i<numEdges; ++i)
{
var edge:Point = Point.polar(radius, i*2*Math.PI / numEdges);
vertexData.setPoint(i, "position", edge.x, edge.y);
vertexData.setColor(i, "color", color);
var u:Number = (edge.x + radius) / (2 * radius); (2)
var v:Number = (edge.y + radius) / (2 * radius);
vertexData.setPoint(i, "texCoords", u, v);
}
return vertexData;
}
1 | 中心顶点的纹理坐标:0.5,0.5 。 |
2 | 正多边形的原点在中心,但纹理坐标必须都为正。 因此,我们将顶点坐标向右移动(通过`radius`),并将它们除以`2 * radius`,结束在范围`0-1`内。 |
当分配纹理时,渲染代码将自动选取这些值。
var ngon:NGon = new NGon(100, 5);
ngon.texture = assets.getTexture("brick-wall");
addChild(ngon);
2.9.6. 抗锯齿
如果你仔细观察我们的正多边形的边缘,你会看到边缘锯齿相当严重。 这是因为GPU处理正多边形内部或外部的像素时 - 并没有平滑像素化边缘。 要解决这个问题,您可以启用反锯齿:在Starling类中有一个具有该名称的属性。
starling.antiAliasing = 4;
该值与Stage3D在渲染时使用的子样本数相关。 使用更多的子样本数需要执行更多的计算,使得抗锯齿可能成为非常昂贵的选项。 此外,Stage3D并非在所有平台上的都支持抗锯齿。
在移动设备上,目前抗锯齿仅在RenderTextures中有效。 |
因此,它不是一个理想的解决方案。 我可以提供的唯一的好消息:屏幕的像素密度不断上升。 在现代高端手机上,像素如此之小,以致于锯齿很少成为一个问题。
2.9.7. 网格样式
你现在知道该如何创建任意形状的纹理网格了。 如果仅仅如此,您使用Starling内置的标准渲染机制就可以了。
然而,如果你想自定义渲染过程呢? 虽然Mesh类的属性和方法提供了坚实的基础 - 但迟早,你会觉得这些还不够。
Starling的网格样式可以解决这种问题。
样式是Starling的新增内容(在2.0版中引入),是创建自定义高性能渲染代码的推荐方式。 事实上,Starling中的所有渲染都是通过网格样式完成的。
-
样式可以分配给任何网格物体(Mesh类或其子类的实例)。
-
默认情况下,每个网格的样式是基本MeshStyle类的实例。
-
后者提供了Starling的标准渲染功能:绘制彩色或有纹理映射的三角形。
要使用新的网格样式,你可以扩展MeshStyle。 这允许您为各种有趣的效果创建自定义着色器程序。 例如,您可以实现快速颜色转换或多纹理。
一个最令人印象深刻的样式之一是 动态照明扩展. 借助于法线贴图(针对表面法线编码的纹理),它可以提供逼真的实时照明效果。 一定要在Starling Wiki看看这个扩展! |
要使用样式,实例化它并将其分配给网格的“style”属性:
var image:Image = new Image(texture);
var lightStyle:LightStyle = new LightStyle(normalTexture);
image.style = lightStyle;
样式几乎无所不能;它们的应用无极限。 并且由于具有相同样式的网格可以分批在一起,您不必担心因此牺牲性能。 在这方面,它们比片段过滤器(它们具有类似的目的)更高效。
样式的主要缺点是它们只能被分配给一个网格物体(不是一个精灵),并且它们只能在实际的网格区域内作用(比如模糊效果就不能实现)。 此外,不能在一个网格上组合几个样式。
虽然如此,样式仍然是任何Starling开发人员都应该熟悉的一个强大的工具。 在“自定义样式,稍后部分”中,我将向您介绍如何从头开始创建自己的网格样式,着色器等等!敬请期待!
如果你对于Mesh和MeshStyle之间的区别仍然有点困惑,可以这样想: Mesh只是一个存储顶点列表,以及描述这些顶点如何构成三角形的对象。 MeshStyle则可以向每个顶点添加附加数据,并在渲染时使用它们。 标准MeshStyle提供颜色和纹理坐标; MultiTextureStyle可能会添加一组额外的纹理坐标等。 但是样式不应该修改对象的原始形状;它不会添加或删除顶点或更改其位置。 |
2.10. 遮罩
遮罩可以用于切除显示对象的一部分。 不妨将遮罩视为一个“洞”,透过它可以看到另一个显示对象的内容。 该“洞”可以具有任意形状。
如果你已经使用了经典显示列表的“mask”属性,你会觉得这个功能非常熟悉。 只需将一个显示对象分配给新的mask属性,如下所示。 任何显示对象可以用作掩码,并且它可以是或可以不是显示列表的一部分。
var sprite:Sprite = createSprite();
var mask:Quad = new Quad(100, 100);
mask.x = mask.y = 50;
sprite.mask = mask; // ← use the quad as a mask
这将产生以下结果:
遮罩的逻辑很简单:如果被遮罩对象的像素在遮罩的多边形内,会被绘制,否则不会被绘制。 遮罩的形状由它的多边形决定 - 而不是它的纹理!这是至关重要的! 因此,这样的遮罩是纯二元的:一个像素要么可见,要么不可见。
Masks and AIR
要使binary在AIR应用程序中工作,您需要在应用程序描述文件中激活模板缓冲区。 将以下设置添加到`initialWindow`元素:
但不要担心,如果您忘记这样做,Starling会向控制台打印警告。 |
2.10.1. 画布和多边形
“这个遮罩功能看起来真的很好”,你可能会问,“但是我怎么创造关于你刚说的任意形状?” 好,你问这个问题我很高兴!
事实上:由于遮罩完全依赖于几何体,而不是任何纹理,所以你需要一种绘制遮罩形状的方法。 恰巧有两个类可以帮助你完成这个任务:Canvas(画布)和Polygon(多边形)。 他们都与掩模板有密切的联系。
Canvas类的API类似于Flash的Graphics对象。 例如,画一个红圈:
var canvas:Canvas = new Canvas();
canvas.beginFill(0xff0000);
canvas.drawCircle(0, 0, 120);
canvas.endFill();
还有一些方法来绘制椭圆,矩形或任意多边形。
除了这些基本方法,Canvas类的功能是相当有限的; 不要期待一个完整的Graphics替代类,还没有。 这可能会在未来的版本中改变! |
我们来讲讲Polygon类。 Polygon(包`starling.geom`)描述由多个直线段定义的闭合形状。 它是Flash的Rectangle类的功能继承者,但是支持任意形状.[4]
而Canvas支持绘制多边形对象,它是Polygon的理想伴侣。 这两个类将解决所有与遮罩相关的需求。
var polygon:Polygon = new Polygon(); (1)
polygon.addVertices(0,0, 100,0, 0,100);
var canvas:Canvas = new Canvas();
canvas.beginFill(0xff0000);
canvas.drawPolygon(polygon); (2)
canvas.endFill();
1 | 此多边形描述一个三角形。 |
2 | 将三角形绘制到画布上。 |
我想提醒一下关于遮罩的几个注意事项:
- 是否可见
-
遮罩本身永远不可见。 你总是通过被遮罩的显示对象的效果间接地看到它。
- 位置
-
如果遮罩不是显示列表的一部分(即它没有父对象),则它将在被遮罩对象的局部坐标系中绘制:如果对象移动,遮罩将跟随。 如果遮罩是显示列表的一部分,其位置将像往常一样计算。
- 模板缓冲
-
在幕后,遮罩使用GPU的模板缓冲实现,它们非常轻量和快速。 一个遮罩需要两次绘制调用:一个将遮罩绘制到模板缓冲区中,一个在渲染完所有被遮罩内容后删除遮罩。
- 矩形裁剪
-
如果遮罩是与舞台坐标轴平行的无纹理四方形对象(Quad),Starling可以优化其渲染。 它将使用GPU的矩形裁剪,而不是模板缓冲, - 结果是仅有一次绘制调用。
- 纹理遮罩
-
如果一个简单的矢量形状并且不能裁剪,有一个扩展,允许你使用纹理的alpha通道作为模板掩码。 它叫做Texture Mask,查看这里: Starling Wiki.
2.11. Sprite3D
在前面的章节中我们看到的所有显示对象都是纯二维对象。 理所当然 - 毕竟Starling是一个2D引擎。 然而,即使在2D游戏中,有时也可以添加一个简单的3D效果,例如。用于在两个屏幕之间转换或显示纸牌的背面。
因此,Starling包含一个类,可以很容易地添加基本的3D功能,它就是:Sprite3D。 它允许您在三维空间中移动2D对象。
2.11.1. 基本属性
就像一个常规的Sprite,你可以在这个容器中添加和删除子对象,这允许你将多个显示对象组合在一起。 除此之外,Sprite3D还提供了几个有趣的属性:
-
z — 沿z轴(指向远离相机)移动精灵。
-
rotationX — 围绕x轴旋转精灵。
-
rotationY — 围绕y轴旋转精灵。
-
scaleZ — 沿z轴缩放精灵。
-
pivotZ — 沿z轴移动枢轴点。
在这些属性的帮助下,您可以将精灵及其所有的子对象放在3D世界中。
var sprite:Sprite3D = new Sprite3D(); (1)
sprite.addChild(image1); (2)
sprite.addChild(image2);
sprite.x = 50; (3)
sprite.y = 20;
sprite.z = 100;
sprite.rotationX = Math.PI / 4.0;
addChild(sprite); (4)
1 | 创建Sprite3D的实例。 |
2 | 向Sprite3D实例中添加一些常规2D对象。 |
3 | 在3D空间中设置对象的位置和方向。 |
4 | 像平常一样,将其添加到显示列表。 |
如你所见,使用Sprite3D并不困难:你只需要探索几个新的属性。 碰撞测试,动画,自定义渲染 - 一切都像你使用其他显示对象一样。
2.11.2. 相机设置
当然,如果你正在显示3D对象,你也希望能够设置这些对象的透视图。 这可以通过设置相机来达到目的;在Starling中,通过starling的舞台来设置相机。
以下舞台属性可设置相机:
-
fieldOfView — 指定视野(FOV)的角度(零和PI之间的弧度)。
-
focalLength — 舞台和相机之间的距离。
-
projectionOffset — 一个矢量,它将相机移动离开其默认位置,它在舞台中心的右边。
Starling将始终确保舞台填充整个视口。 如果更改视野,焦距也将被修改以遵守此约束,反之亦然。 换句话说:fieldOfView和focalLength只是相同属性的不同表示。
下面是一个例子,说明fieldOfView的值如何影响Starling演示中的立方体外观:
默认情况下,相机将始终指向舞台中心。 projectionOffset允许你切换成远离这一点的透视图;如果你想从另一个方向看你的对象,例如,从顶部或底部,可以使用projectionOffset。 还是这个立方体,这次针对`projectionOffset.y`使用不同的值:
2.12. 实用工具
`starling.utils`包包含几个有用的小助手,不应该被忽视。
2.12.1. 颜色
在常规Flash和Starling中,颜色以十六进制格式指定。 这里有几个例子:
// format: 0xRRGGBB
var red:Number = 0xff0000;
var green:Number = 0x00ff00; // or 0xff00
var blue:Number = 0x0000ff; // or 0xff
var white:Number = 0xffffff;
var black:Number = 0x000000; // or simply 0
Color类包含一些常用的颜色值的列表; 此外,您可以使用它来轻松访问各颜色通道数值。
var purple:uint = Color.PURPLE; (1)
var lime:uint = Color.LIME;
var yellow:uint = Color.YELLOW;
var color:uint = Color.rgb(64, 128, 192); (2)
var red:int = Color.getRed(color); // -> 64 (3)
var green:int = Color.getGreen(color); // -> 128
var blue:int = Color.getBlue(color); // -> 192
1 | 预定义几种常见的颜色。 |
2 | 可以使用此方法创建任何其他颜色。 只需将RGB值传递给此方法(范围:0 - 255)。 |
3 | 您还可以提取每个通道的整数值。 |
2.12.2. 角度
Starling期望所有角度以弧度表示(不同于Flash,在某些地方使用度数,在其他地方使用弧度)。 要在度数和弧度之间进行转换,可以使用以下简单函数。
var degrees:Number = rad2deg(Math.PI); // -> 180
var radians:Number = deg2rad(180); // -> PI
2.12.3. StringUtil
你可以使用`format`方法来格式化.Net/C#风格的字符串。
StringUtil.format("{0} plus {1} equals {2}", 4, 3, "seven");
// -> "4 plus 3 equals seven"
相同的类还包含从字符串的开头和结尾去除空格的方法 - 每当需要处理用户输入时进行频繁操作。
StringUtil.trim(" hello world\n"); // -> "hello world"
2.12.4. SystemUtil
了解应用或游戏当前执行环境的信息通常很有用。 SystemUtil包含一些帮助执行该任务的方法和属性。
SystemUtil.isAIR; // AIR or Flash?
SystemUtil.isDesktop; // desktop or mobile?
SystemUtil.isApplicationActive; // in use or minimized?
SystemUtil.platform; // WIN, MAC, LNX, IOS, AND
2.12.5. MathUtil
虽然该类主要是为了帮助处理一些几何问题,它还包含以下非常有用的帮助方法:
var min:Number = MathUtil.min(1, 10); (1)
var max:Number = MathUtil.max(1, 10); (2)
var inside:Number = MathUtil.clamp(-5, 1, 10); (3)
1 | 获取两个数字中最小的数字。结果:1 |
2 | 获取两个数字中最大的。结果:10 |
3 | 将数字(第一个参数)移动到特定范围。结果:1 |
如果你以前使用过AS3,你可能会想知道为什么我写了这些方法,提供了类似原生的数学类。
不幸的是,这些等价方法有副作用:每次您调用`Math.min`,它创建一个临时对象(至少当你为iOS编译你的应用程序时)。 这些替代品没有这种副作用,所以你应该总是喜欢他们。
2.12.6. 池
现在我们碰到了临时对象的主题,这是介绍你Pool类的最佳时机。
经验丰富的AS3开发人员知道任何对象分配都有一个代价:对象需要被垃圾回收。 这完全在幕后发生;你甚至大部分时间不会注意到它。
但是,当清理过程占用太多时间时,您的应用程序将卡住一段时间。 如果这种情况经常发生,它很快就会成为您的用户的一个烦恼。
避免这个问题的一个策略是回收您的对象并反复使用它们。 例如,像Point和Rectangle这样的类通常只需要很短的使用时间:创建它们,用一些数据填充它们,然后丢弃它们。
从现在开始,让Starling的Pool类处理这些对象。
var point:Point = Pool.getPoint(); (1)
doSomethingWithPoint(point);
Pool.putPoint(point); (2)
var rect:Rectangle = Pool.getRectangle(); (1)
doSomethingWithRectangle(rect);
Pool.putRectangle(rect); (2)
1 | 从池中获取对象。 它替换了类上调用’new'。 |
2 | 当你不再需要它时,把它放回池中。 |
该类还以类似的样式支持Vector3D,Matrix和Matrix3D。
始终确保get和put调用是平衡的。 如果你把太多的对象放入池中,从来不检取它们,它将随着时间的推移而填满,使用越来越多的内存。 |
3. 高级主题
通过我们在前几章中学到的所有内容,您已经能够在实际项目中开始使用Starling。 然而,在这样做的时候,你可能会遇到一些可能令人困惑的事情。 例如,
-
您的纹理快速消耗所有可用的内存。
-
你不时遇到设置丢失。
-
你实际上对你的应用程序的性能有点失望。 你想要更快的速度!
-
或者你可能是那些喜欢写自己的顶点和片段着色器,但不知道从哪里开始的受虐狂者之一。
有趣的是,这一章完全总结了这些问题。 扣上安全带,我们现在跳进一些高级话题!
3.1. ATF纹理
在常规Flash中,大多数开发人员使用PNG图像格式,如果不需要透明度,则使用JPG。 那些图像格式在Starling中也同样受欢迎。 但是,Stage3D提供了一个具有几个独特优点的替代方法:Adobe Texture Format,它可以存储compressed纹理。
-
压缩纹理占用更小的存储空间。
-
直接在GPU上完成解压缩。
-
上传到显存更快。
-
上传可以异步完成:您可以加载新的纹理,而不中断游戏.[5]
3.1.1. 显存
在我们继续之前,可能有兴趣了解一个纹理到底需要多少内存。
PNG图像为每个像素存储4个通道:red,green,blue和alpha,每个具有8位(每通道256个值)。 很容易计算512×512像素纹理占用的空间:
512×512 RGBA纹理的内存占用:+ 512×512像素×4字节= 1,048,576字节≈1MB
JPG图像基实也是类似的;不同的是它不需要alpha通道。
512×512 RGB纹理的内存占用:+ 512×512像素×3字节= 786,432字节≈768kB
非常多这样的小纹理,不是吗? 请注意,PNG和JPG的内置文件压缩对显存优化不起作用:因为图像必须在上传给显卡之前解压缩。 换句话说:纹理所占用的显存总是使用上面的公式计算,而与文件本身大小无关。
尽管如此:如果你的纹理很容易适应图形内存的存储方式 - 继续使用它们! 这些格式很容易使用,在许多情况下工作良好,尤其你的应用程序是针对桌面硬件的情况下。
但是,在开发阶段,您的内存消耗可能会比设备上可用的内存消耗更高。 现在,是时候来看看ATF格式了。
3.1.2. 压缩纹理
上面,我们了解到传统纹理文件大小与其占用内存之间的关系;大量压缩的JPG将与纯BMP格式占用一样多的空间。
这对于压缩纹理来说结果就不一样了:压缩纹理可以直接在GPU上压缩。 这意味着,根据压缩设置,您可以加载多达十倍的纹理。 惊呆了,对吧?
不幸的是,每个GPU供应商都认为自己可以比其他厂商做得更好,因此压缩纹理有几种不同的格式。 换句话说:根据你的游戏运行所依赖的硬件环境不同,它将需要一种不同格式的纹理。 你如何确定要包括哪种格式的文件呢?有点麻烦。
ATF出场了。 这是Adobe为Stage3D特别创建的格式;实际上,它是一个容器文件,可以包括多达四个不同版本的纹理。
-
PVRTC(PowerVR纹理压缩)用于PowerVR GPU。它支持所有的iPhone,iPod Touch和iPad。
-
DXT1/5 (S3纹理压缩)最初由S3 Graphics开发。它现在由Nvidia和AMD GPU支持,因此可在大多数台式计算机以及一些Android手机上使用。
-
ETC (爱立信纹理压缩)用于许多手机,最著名的是Android。
-
ETC2 提供更高质量的RGB和RGBA压缩。所有支持OpenGL ES 3的Android和iOS设备都支持它。
我之前写的ATF是一个container格式。这意味着它可以包括上述格式的任何组合。
当您包含所有格式(这是默认格式)时,纹理可以在任何支持Stage3D的设备上使用,无论应用程序所在的操作系统是在iOS,Android或桌面。 你完全不需要关心纹理内部是什么!
但是,如果您知道您的游戏只会部署到iOS设备,您可以忽略除PVRTC之外的所有格式。 或者,如果您只定位到高端移动设备(至少有OpenGL ES 3),则只需包含ETC2即可在Android和iOS上使用。 这样,你可以优化游戏安装包的下载体积。
DXT1和DXT5之间的区别只是后者支持alpha通道。 不过不要担心,ATF工具会自动选择正确的格式。 ETC1实际上不支持Alpha通道,但Stage3D通过在内部使用两个纹理来解决这个问题。再次,这完全发生在幕后。 |
3.1.3. 创建ATF纹理
Adobe提供了一组命令行工具,用于转换到ATF和从ATF转换,以及预览生成的文件。 它们是AIR SDK的一部分(查找`atftools`文件夹)。
最重要的工具可能是“png2atf”。 这里是一个基本的使用示例;输入png,输出压缩纹理atf,这种设置方法默认包含了所有可用的纹理格式。
png2atf -c -i texture.png -o texture.atf
如果您立即尝试,您可能会收到以下错误消息:
尺寸不是2的幂!
这是我还没有提到的限制:ATF纹理总是要求边长是2的幂。 这虽然有点讨厌,但它实际上并不是一个大问题,因为你几乎总是使用它们的atlas纹理图集。
提示:大多数图集生成器可以通过配置导出二次幂纹理。
当调用成功后,您可以查看ATFViewer中的输出。
在左侧的列表中,您可以选择要查看的内部格式。 此外,您看到,默认情况下,所有mipmap变体已创建。
我们将在“内存管理”一章中讨论mipmap。 |
你可能会注意到,图像质量已经遭受了一点压缩。 这是因为所有这些压缩格式都是lossy:质量降低的奖励便是带来了较小的内存。 质量损失严重程度取决于图像类型:有机、照片级的纹理压缩后看起来还不错,但具有硬边缘的漫画般的图像可能遭受相当大的影响。
当然,该工具提供了很多不同的选项。 例如。你可以让它只打包PVRTC格式(p),完美适配iOS:
png2atf -c p -i texture.png -o texture.atf
或者你可以为了节省内存告诉它忽略mipmaps(-n 0,0):
png2atf -c -n 0,0 -i texture.png -o texture.atf
另一个有用的实用程序叫“atfinfo”。 它能显示ATF文件数据详细信息,如包括的纹理格式,mipmap的数量等。
> atfinfo -i texture.atf 文件名 : texture.atf ATF版本 : 2 ATF文件类型 : RAW Compressed With Alpha (DXT5+ETC1/ETC1+PVRTV4bpp) 尺寸 : 256x256 立方体贴图 : 无 空Mipmap : 无 实际Mipmap : 1 嵌入级别 : X........ (256x256) AS3纹理类型 : Texture (flash.display3D.Texture) AS3纹理格式 : Context3DTextureFormat.COMPRESSED_ALPHA
3.1.4. 使用ATF纹理
在Starling中使用压缩纹理与其他任何纹理一样简单。 将带有文件内容的字节数组传递给工厂方法Texture.fromAtfData()。
var atfData:ByteArray = getATFBytes(); (1)
var texture:Texture = Texture.fromATFData(atfData); (2)
var image:Image = new Image(texture); (3)
1 | 获取原始数据。例如从文件获取。 |
2 | 创建ATF纹理。 |
3 | 像任何其他纹理一样使用它。 |
就像这样! 这种纹理可以像Starling中的任何其他纹理一样使用。 它也是你的图集纹理的完美候选人。
然而,上面的代码将同步上传纹理,即AS3执行将暂停,直到完成。 要以异步方式上传纹理,请将回调函数传递给方法:
Texture.fromATFData(atfData, 1, true,
function(texture:Texture):void
{
var image:Image = new Image(texture);
});
参数二和三分别控制缩放和是否应使用mipmaps。 第四个,如果传递一个回调,将触发异步加载:Starling将能够在这种情况下不中断其它渲染。 一旦回调被执行(不能更早!),纹理将可用。
当然,您也可以直接在AS3源码中嵌入ATF文件。
[Embed(source="texture.atf", mimeType="application/octet-stream")]
public static const CompressedData:Class;
var texture:Texture = Texture.fromEmbeddedAsset(CompressedData);
但请注意,在这种情况下,异步上传将不可用。
3.2. 上下文丢失
所有Stage3D渲染通过所谓的“渲染上下文”(Context3D类的一个实例)发生。 它是存储GPU的所有当前设置,如活动纹理列表,顶点数据的指针等的一个软件设备。所以渲染上下文丢失有时又称设备丢失。 渲染上下文是与GPU的连接 - 没有它,你不能做任何Stage3D渲染。
这里出现的问题是:这种软件设备有时会丢失。 这意味着您将丢失存储在图形内存中的所有数据的引用;最显著的是:纹理。
对于所有操作系统而言上下文丢失并不频繁;在iOS和macOS上很少出现;但Windows上经常发生的;Android情况呢(旋转屏幕?炸了!)。 所以没有办法:我们需要预期最坏的情况,并随时准备应付上下文丢失。
如何触发上下文丢失
有一个简单的方法来检查您的应用程序是否可以应付上下文丢失的异常情况:只需通过`Starling.context.dispose()`处理当前上下文。 它将立即重新创建,这是真正的设备丢失之后会发生的事情。 |
3.2.1. 默认行为
当Starling认识到当前渲染上下文已经丢失时,它启动以下过程:
-
Starling会自动创建一个新的上下文,并使用与之前相同的设置进行初始化。
-
所有顶点和索引缓冲区都将被恢复。
-
所有顶点和片段程序(着色器)将被重新编译。
-
纹理将通过任何可能的方式恢复(从内存/磁盘/等)
恢复缓冲区和程序没有问题;Starling拥有所需的所有数据,并不需要太多时间。 但是,恢复纹理是个头痛的问题。 为了说明这一点,我们来看一下最糟糕的例子:从嵌入式位图创建的纹理。
[Embed(source="hero.png")]
public static const Hero:Class;
var bitmap:Bitmap = new Hero();
var texture:Texture = Texture.fromBitmap(bitmap);
当您调用“Texture.fromBitmap”的时刻,位图被上传到GPU内存,这意味着它现在是上下文的一部分。 如果我们能够依靠永远处于激活状态的设备环境,我们现在就完成了所有的工作。
但是,现实是残酷的,我们不能依靠。纹理数据可能会随时丢失。 这就是为什么Starling将保留原始位图的副本的原因。 当最坏的情况发生时,它会使用它来重新创建纹理。 所有这一切都发生在幕后。
但是,你瞧! 这意味着纹理在内存中存在三份。
-
“英雄”类(常规内存)
-
备份位图(常规内存)
-
纹理(图形内存)
鉴于我们在移动设备上面临的严格的内存限制,这是一场灾难。 你绝对不希望发生这样的情况!
如果稍微更改代码,会变得更好一点
// 改用'fromEmbeddedAsset'方法
var texture:Texture = Texture.fromEmbeddedAsset(Hero);
这样,Starling可以直接从嵌入式类重新创建纹理(调用`new Hero()`),这意味着纹理在内存中只有两份。 对于嵌入式资源,这是您最好的选择。
理想情况下,我们希望纹理在内存中仅有一份。 为此,您不能嵌入资源;相反,您需要通过指向本地或远程的URL加载它。 这样,只需要存储URL;然后可以从原始位置重新加载实际数据。
有两种方法可以实现这一点:
-
使用AssetManager加载纹理。
-
手动恢复纹理。
我的建议是尽可能使用AssetManager。 它能应付上下文丢失而不浪费任何内存;并且您不必添加任何特殊的恢复逻辑。
不过,应该庆幸知道了幕后发生的事情。 天知道 - 你可能会遇到手动恢复是唯一的选择的情形。
3.2.2. 手动恢复
你可能会思考,“Texture.fromEmbeddedAsset()”内部是如何工作的。 我们来看一下该方法可能的实现:
public static function fromEmbeddedAsset(assetClass:Class):Texture
{
var texture:Texture = Texture.fromBitmap(new assetClass());
texture.root.onRestore = function():void
{
texture.root.uploadFromBitmap(new assetClass());
};
return texture;
}
你可以看到真正的技巧发生在`root.onRestore`回调中。 等一下:什么是“root”?
你可能不知道,当你有一个Texture实例时,实际上根本不是一个具体的纹理。 它可能只是一个指向另一个纹理的一部分(SubTexture)的一个指针。 即使`fromBitmap`调用也可以返回这样一个纹理! (解释背后的逻辑超出了本章的范围)。
在任何情况下,`texture.root`将始终返回ConcreteTexture对象,即是包含`onRestore`回调的对象。 这种回调将在上下文丢失之后直接执行,并且可以让您重新创建纹理。
在我们的例子中,该回调简单地实例化了位图,并将其上传到根纹理。 太好了,你瞧!纹理恢复了!
细节之处见功夫。 你必须非常仔细地构造你的`onRestore` - 回调,以确保不要存储另一个位图副本而不自知。 这是一个反面的示例,实际上不能这样用:
public static function fromEmbeddedAsset(assetClass:Class):Texture
{
// DO NOT use this code! BAD example.
var bitmap:Bitmap = new assetClass();
var texture:Texture = Texture.fromBitmap(bitmap);
texture.root.onRestore = function():void
{
texture.root.uploadFromBitmap(bitmap);
};
return texture;
}
你能发现错误吗?
问题在于该方法创建了一个Bitmap对象并在回调中使用它。 这个回调实际上是所谓的闭包;这是一个内联函数,它将与其中的一些变量一起存储。 换句话说,你有一个保持在内存中的函数对象,可以在上下文丢失时调用。 而且位图实例被存储在它里面,即使你没有明确地这样声明。 (嗯,其实你是通过在回调中使用`bitmap`来实现的。)
在原始代码中,位图不被引用,但在回调中是重新创建位图实例。 因此,没有“闭包”存储的“位图”实例。 回调中只引用`assetClass`对象,而且在内存中也是这样。
该技术适用于各种场景:
-
如果您的纹理来自URL,则只将该URL传递给onRestore回调方法并从那里重新加载。
-
对于ATF纹理,该过程是一样的,只是您需要改用`root.uploadATFData`上传数据。
-
对于包含传统显示对象渲染的位图,只需引用该显示对象并在回调中的重新绘制新位图后再次上传即可。 (就像Starling的TextField类所做的那样)
强调一下,AssetManager已经为您做了所有的这些恢复逻辑,之所以按着这个过程再走一遍,我只是想告诉你这是如何实现的。 |
3.2.3. 渲染纹理
上下文丢失导致的另一个特别令人讨厌的领域:渲染纹理。 就像其他纹理一样,它们将丢失所有的内容 - 但是没有简单的方法来恢复它们。 毕竟,他们的内容是任意数量的动态绘制操作的结果。
如果RenderTexture仅用于特殊效果(比如说雪地上的足迹),那么您可能只需要清除它即可 ,无伤大雅。 另一方面,如果其内容至关重要,则需要一个系统的解决方案。
没有其它办法:您需要手动重绘纹理的完整内容。 再一次,`onRestore`回调可能会拯救你:
renderTexture.root.onRestore = function():void
{
var contents:Sprite = getContents();
renderTexture.clear(); // 在纹理恢复时需要调用
renderTexture.draw(contents);
});
我仿佛听见你说:它可能不仅仅是一个对象,而是在更长的时间内执行了一堆绘图调用的综合结果。 例如,一个包含RenderTexture-canvas的绘图应用程序,包含数十个画笔笔画。
在这种情况下,您需要存储有关所有绘制命令的足够信息才能再现它们。
如果我们坚持使用渲染纹理绘制应用程序场景,您可能需要添加对 取消/重做 系统的支持。 这样的系统通常通过存储封装各个命令的对象的列表来实现。 在上下文丢失的情况下,您可以重新使用该系统来恢复所有绘图操作。
现在,在开始实施这个系统之前,还有一个需要注意的问题。 当执行`root.onRestore`回调时,很可能并不是所有的纹理都已经可用了。 毕竟,他们也需要恢复,这可能需要一些时间!
如果您使用AssetManager加载纹理,那么它已经包含了这个处理过程。 在这种情况下,您可以监听“TEXTURES_RESTORED”事件。 另外,确保使用`drawBundled`获得最佳性能。
assetManager.addEventListener(Event.TEXTURES_RESTORED, function():void
{
renderTexture.drawBundled(function():void
{
for each (var command:DrawCommand in listOfCommands)
command.redraw(); // 执行`renderTexture.draw()`
});
});
这一次,不需要调用clear,因为这只是`onRestore`的默认行为,不管如何—我们并没有修改它。
记住,我们在这里有一个不同的回调(Event.TEXTURES_RESTORED ),`onRestore`没有被修改为默认的实现。
|
3.3. 内存管理
许多Starling开发人员使用该框架为移动设备创建应用和游戏。 几乎所有这些开发人员迟早都会发现(以自己惨痛的经验)移动设备在内存上的总是感觉不够用。 这是为什么?
-
大多数移动设备具有极高分辨率的屏幕。
-
这样的设备的2D游戏需要同样高分辨率的纹理。
-
可用RAM太小,无法容纳所有纹理数据。
换句话说,一个真正的恶性组合。
如果内存不足时程序仍在运行,会发生什么? 大多数时候,你会得到着名的错误3691(超出此资源类型的资源限制),您的应用程序将崩溃。 以下提示将告诉您如何避免这种讨厌的错误!
3.3.1. 清除垃圾
当你不再需要一个对象时,别忘了在它上面调用`dispose`。 与传统的Flash对象不同,垃圾收集器不会清理任何Stage3D资源! 你自己负责管理这个内存。
纹理
纹理—这就是那些是你需要照顾的最重要的对象。 纹理总是占据你内存中最大的份额。
当然,Starling试图帮助你。 例如,当您从图集加载纹理时,您只需要处理图集,而不是实际的SubTextures。 只有图集才需要GPU内存,“子”纹理只会引用图集的纹理,不会实际占据GPU内存。
var atlas:TextureAtlas = ...;
var hero:Texture = atlas.getTexture("hero");
atlas.dispose(); // 将失效的“英雄”。
显示对象
虽然显示对象本身不需要很多显存(有些根本不需要),但失效时调用dispose也是一个很好的做法。 特别注意“重”对象,如TextFields。
显示对象容器将照顾所有的孩子,正如预期的那样。 当你处理一个容器时,所有的孩子将被自动处理。
var parent:Sprite = new Sprite();
var child1:Quad = new Quad(100, 100, Color.RED);
var child2:Quad = new Quad(100, 100, Color.GREEN);
parent.addChild(child1);
parent.addChild(child2);
parent.dispose(); // 子对象的dispose方法也将被自动调用
总而言之,最新的Starling版本在处理显示对象时变得更加宽容了。 大多数显示对象不再存储Stage3D资源,因此如果您忘记释放它们,也不会酿成灾难性的后果。
Images
这是第一个陷阱:释放图像将不会释放其纹理。
var texture:Texture = Texture.fromBitmap(/* ... */);
var image:Image = new Image(texture);
image.dispose(); // 不会释放纹理,即不会调用image.texture.dispose();
那是因为Starling不知道你是否在其他地方使用这种纹理! 毕竟,您其它的图像也可能使用了这种纹理。
另一方面,如果你知道纹理没有在别的地方使用,那就释放它。
image.texture.dispose();
image.dispose();
滤镜
滤镜也有点玄机。 当您释放对象时,其滤镜也将被释放:
var object:Sprite = createCoolSprite();
object.filter = new BlurFilter();
object.dispose(); // 此方法会将滤镜一并释放掉。
但请注意:以下类似的代码不会释放滤镜:
var object:Sprite = createCoolSprite();
object.filter = new BlurFilter();
object.filter = null; // 调用此方法并没有真正的释放掉滤镜
同样的原因,Starling不知道你是否想在其他地方使用这个滤镜对象。
但实际上这并不是一个问题。 滤镜没有释放,但Starling仍将清理所有资源。 所以这不会造成内存泄漏。
在之前的Starling版本(<2.0)中,上述做法会造成内存泄漏。 |
3.3.2. 不要嵌入纹理
ActionScript开发人员常常使用“Embed”元数据将其位图直接嵌入到SWF文件中。 这对网页来说非常棒,因为它允许您将所有游戏的数据合并到一个文件中。
我们已经在“设备丢失”部分看到,这种方法在Starling(或一般的Stage3D)中有一些严重的缺点。 归结为:纹理将占用两份内存:一份在常规内存中,一份在显示内存中。
[Embed(source="assets/textures/hero.png")]
private static var Hero:Class; (1)
var texture:Texture = Texture.fromEmbeddedAsset(Hero); (2)
1 | 该类存储在常规内存中。 |
2 | 纹理存储在图形存储器中。 |
请注意,此示例使用“Texture.fromEmbeddedAsset”加载纹理。 由于“设备丢失”中讨论的原因,替代方案(“Texture.fromBitmap”)会使用更多的内存。
保证纹理真的只存储在图形内存中的唯一方法是通过URL加载纹理。 如果你使用AssetManager来执行这个任务,那么需要做的工作不是很多。
var appDir:File = File.applicationDirectory;
var assets:AssetManager = new AssetManager();
assets.enqueue(appDir.resolvePath("assets/textures"));
assets.loadQueue(...);
var texture:Texture = assets.getTexture("hero");
3.3.3. 使用矩形纹理
Starling的Texture类实际上只是两个Stage3D相关类的包装器:
flash.display3D.textures.Texture
::可用于所有配置文件。 支持mipmaps和包装,但要求纹理边长是2的幂。
flash.display3D.textures.RectangleTexture
::可用于`BASELINE`开头的低配置文件。 没有mipmaps,没有包装,但支持任意边长。
前者(“纹理”)有一个奇怪的,鲜为人知的副作用:它将始终为mipmap分配内存,无论您是否需要它们。 这意味着你会浪费大约三分之一的纹理内存!
因此,最好使用替代(“RectangleTexture”)。 Starling将尽可能使用这种纹理类型。
但是,如果运行在仅仅包含“BASELINE”的配置文件环境中,并且如果您禁用mipmaps,那么它只能执行此操作。 可以通过选择最佳的Context3D配置文件来满足第一个要求。 如果您使用Starling的默认构造函数,则会自动完成这一切。
// init Starling like this:
... = new Starling(Game, stage);
// that's equivalent to this:
... = new Starling(Game, stage, null, null, "auto", "auto");
最后一个参数(auto
)会告诉Starling使用最好的配置文件。
这意味着如果设备支持RectangleTextures,Starling将会使用它们。
至于mipmap:只有在明确要求的时候才会创建它们。 一些“Texture.from …”工厂方法包含这样一个参数,而AssetManager具有一个`useMipMaps`属性。 默认情况下,它们始终处于禁用状态。
3.3.4. 使用ATF纹理
我们之前已经讨论过“ATF贴图”,但是在本节再次提及它们是更有意义的。 记住,GPU无法直接使用JPG或PNG压缩;这些文件始终在CPU阶段完成解压,并以未压缩形式上传到显卡。
但ATF纹理不是这样:它们可以直接以压缩形式上传给显卡并直接渲染,这样可以节省大量内存。 所以如果你跳过了ATF部分,我建议你回头再仔细看一下!
当然,ATF纹理的缺点是会降低图像质量。 但您可以尝试以下技巧,即使这并非适合所有类型的游戏:
-
创建您的纹理比实际需要的更大一点。
-
现在用ATF工具压缩它们。
-
在运行时,将它们缩小到原来的大小。
这样做将节省相当多的内存,并且压缩痕迹将变得不那么明显。
3.3.5. 使用16位纹理
如果您的应用程序使用漫画风格和有限的调色板,可能ATF纹理并不适合您。 不过仍有一个好消息:对于这些纹理,有一个不同的解决方案!
-
默认纹理格式(“Context3DTextureFormat.BGRA”)每像素使用32位(每个通道8位)。
-
有一个替代格式(“Context3DTextureFormat.BGRA_PACKED”),其中只有一半:每像素16位(每个通道4位)。
在Starling中,您可以通过`Texture.from …`方法的`format`参数,或通过AssetManager的`textureFormat`属性使用此格式。 这将节省50%的内存!
当然,代价是降低图像质量。 特别是如果您使用渐变,则16位纹理可能会变得相当难看。 但是,有一个解决方案:抖动!
为了使其更加明显,该示例中的渐变减少到仅仅16种颜色(4位)。 即使仅有这么少的颜色,抖动仍可以提供可接受的图像质量。
当您减少颜色深度时,大多数图像处理程序将自动使用抖动。 TexturePacker也有此涵盖。
AssetManager可以针对每个文件配置合适的颜色深度。
var assets:AssetManager = new AssetManager();
// enqueue 16 bit textures
assets.textureFormat = Context3DTextureFormat.BGRA_PACKED;
assets.enqueue(/* ... */);
// enqueue 32 bit textures
assets.textureFormat = Context3DTextureFormat.BGRA;
assets.enqueue(/* ... */);
// now start the loading process
assets.loadQueue(/* ... */);
3.3.6. 避免Mipmaps
Mipmap是您的纹理的低采样版本,旨在提高渲染速度并减少混叠效果。
自Starling2.0版本以来,默认情况下不会创建任何mipmap。 这是最好的默认值,因为没有mipmap:
-
纹理加载速度更快。
-
纹理需要较少的纹理内存(只是原始像素,没有mipmap)。
-
避免模糊的图像(mipmaps有时变得模糊)。
另一方面,当对象显著缩小时,激活它们将产生稍微更快的渲染速度,并且避免混叠效果(即与模糊相反的效果)。 要启用mipmap,请使用“Texture.from …”方法中的相应参数。
3.3.7. 使用位图字体
如前所述,TextField支持两种不同类型的字体:TrueType字体和位图字体。
虽然TrueType字体非常易于使用,但它们有一些缺点。
-
每当您更改文本时,都必须创建一个新的纹理并将其上传到图形内存。这很慢
-
如果您有很多TextFields或大的TextFields,这将需要大量的纹理内存。
另一方面,位图字体有一些特性:
-
非常快速地更新
-
只需要一个恒定的内存量(只是字形纹理)。
这使他们成为在Starling中显示文本的首选方式。 我的建议是尽可能使用它们!
位图字体纹理是16位纹理的一个很好的候选者,因为它们通常只是纯白色,在运行时被着色为实际的TextField颜色。 |
3.3.8. 优化您的纹理图集
首要任务是把你的纹理贴图尽可能地包装在一起。比如这个工具 TexturePacker 有几个可选的帮助选项:
-
修剪透明的边框。
-
如果有可能,将纹理旋转90度以更有效的节省空间。
-
降低颜色深度(见上文)。
-
删除重复纹理。
-
等等…
使用它! 将更多的纹理包装在一个图集中不仅可以降低整体内存消耗,还可以减少绘制调用(下一章更多介绍)。
3.3.9. 使用Adobe Scout
Adobe Scout 是一款用于ActionScript和Stage3D的轻量级但全面的概要分析工具。 任何Flash或AIR应用程序,无论是在移动设备上还是在浏览器中运行,都可以快速分析,而不会改变代码 - 而Adobe Scout可以快速有效地检测可能影响性能的问题。
使用Scout,您不仅可以在ActionScript代码中找到性能瓶颈,还可以在常规和图形内存方面随时随地找到内存消耗的详细内容。 这是无价的!
Adobe Scout是Adobe的Creative Cloud成员的免费版本的一部分。你不必成为CC的付费用户。 |
以下是Thibault Imbert的精彩教程,详细介绍了如何使用Adobe Scout: 开始使用Adobe Scout
3.3.10. 保持统计显示
统计显示(通过`starling.showStats`提供)包括有关传统内存和图形内存的信息。 在开发过程中,关注这些内容是很有价值的。
当然,传统的内存值常常是误导的 - 你永远不知道垃圾收集器何时运行。 但另一方面,图形内存值却非常准确。 创建纹理时,值会上升;当您释放纹理时,会立即减少。
实际上,当我将此功能添加到Starling后,大约花了五分钟,我已经发现了第一个内存泄漏 - 在Starling的演示程序中。 我使用以下方法:
-
在主菜单中,我记下了使用的GPU内存。
-
然后我一个接一个地进入演示场景。
-
每当我回到主菜单,我检查了GPU内存是否已经恢复到原来的值。
-
从一个场景返回后,该值没有恢复,确实:审查代码后我发现忘记了释放一个纹理。
Scout自然是提供了更多关于内存使用的细节。 但是统计显示总是用简单事实,让我们可以容易找到被忽视的东西。
3.4. 性能优化
虽然Starling模仿Flash的经典显示列表,但幕后的功能是完全不同的。 要实现最佳性能,您必须了解其架构的一些关键概念。 以下列表是您可以遵循的最佳做法,让您的游戏尽可能快地运行。
3.4.1. 普通AS3程序通用提示
总是构建发行版本
首先也是最重要的一条:在测试性能时始终构建发行(release)版本。 与传统的Flash项目不同,当您使用Stage3D框架时,发行版本会产生巨大的差异。 根据您正在开发的平台不同,运行时速度差异可能非常大;发行版帧率甚至可以轻松达到调试版帧率的倍数。
-
在Flash Builder中,通过单击菜单:工程 [导出发行版本] 构建发布版本。
-
在Flash Develop中,选择“发布”配置并构建项目; 然后在执行“PackageApp.bat”脚本时选择“ipa-ad-hoc”或“ipa-app-store”选项。
-
在IntelliJ IDEA中,选择菜单:构建 [AIR应用程序包] ;为Android选择“发布”,iOS选择“临时发行”。 对于非AIR项目,请取消选择模块编译器选项中的“生成可调试SWF”。
-
如果您从命令行构建Starling项目,请确保`-optimize`为true,`-debug`为false。
检查你的硬件
确保Starling确实使用GPU进行渲染。 这很容易检查:如果“Starling.current.context.driverInfo”包含字符串“software”,则Stage3D处于软件回退模式,否则使用GPU。
此外,一些移动设备可以在省电模式中运行。 进行性能测试时,请务必将其关闭。
设置帧速率
无论您怎么优化,您的应用程序仍然以每秒24帧运行? 那么你可能从来没有设置你想要的帧速率,你看到只是Flash Player的默认设置。
要改变这一点,可以在启动类中使用相应的元数据标签,或者在Flash运行阶段手动设置帧速率。
[SWF(frameRate="60", backgroundColor="#000000")]
public class Startup extends Sprite
{ /* ... */ }
// 或其他任何地方
Starling.current.nativeStage.frameRate = 60;
使用Adobe Scout
Adobe Scout不仅对“memory_management_scout,内存分析”有用;它在性能分析方面同样强大。
它允许您实际查看每个ActionScript(和Starling)方法中花费了多少时间。 这是非常有用的,因为它显示了您可以从任何优化中获得最大收益的位置。 没有它,你最终优化的代码可能会在实际上与帧速率无关的区域!
记住,过早优化是所有不幸的根源! |
与传统的分析器相比,它的优点在于,它还可以在发布模式下运行,所有优化都已做到位。 这确保其输出非常准确。
异步解码加载的图像
默认情况下,如果您使用Loader加载PNG或JPEG图像,则图像数据不会立即解码,而是首次使用时。 这种情况发生在主线程上,可能会导致应用程序在纹理创建时表现卡顿。 为了避免这种情况,请将图像解码策略标志设置为“ON_LOAD”。 这将导致图像在Loader的后台线程中直接解码。
loaderContext.imageDecodingPolicy = ImageDecodingPolicy.ON_LOAD;
loader.load(url, loaderContext);
另一方面,您可能正在使用Starling的AssetManager加载纹理,不是吗? 在这种情况下,不要担心,它内部已经采用这种做法。
避免 "for each"
当使用经常重复或深入嵌套的循环时,最好避免“for each”而改用经典的“for i”优化性能: better 。 此外,请注意,每次循环都会执行循环条件一次,因此将其保存到一个额外的变量中速度更快。
// 慢:
for each (var item:Object in array) { ... }
// 更快:
for (var i:int=0; i<array.length; ++i) { ... }
// 最快:
var length:int = array.length;
for (var i:int=0; i<length; ++i) { ... }
避免分配内存
避免创建大量临时对象。 它们占用内存,需要被垃圾收集器清理,这可能会在运行时产生小的卡顿。
// 较差:
for (var i:int=0; i<10; ++i)
{
var point:Point = new Point(i, 2*i);
doSomethingWith(point);
}
// 更好:
var point:Point = new Point();
for (var i:int=0; i<10; ++i)
{
point.setTo(i, 2*i);
doSomethingWith(point);
}
实际上,Starling包含一个帮助类:Pool。 它提供了对象池,能存取经常需要的对象,如Point,Rectangle和Matrix。 您可以从该池中“借”对象,并在完成后返还到池中。
// best:
var point:Point = Pool.getPoint();
for (var i:int=0; i<10; ++i)
{
point.setTo(i, 2*i);
doSomethingWith(point);
}
Pool.putPoint(point); // 不要忘记这个!
3.4.2. Starling特性提示
最大限度地减少状态变化
你已经知道,Starling使用Stage3D渲染显示列表。 这意味着所有绘图都是由GPU完成的。
现在,Starling可以一个接一个地将四边形发送到GPU,逐一绘制。 事实上,最初的Starling发行版就是这样工作的! 但是,为了获得最佳性能,GPU更适合获得一大堆数据,同时绘制所有数据。
这就是为什么更新的Starling版本在将它们发送到GPU之前,将尽可能多的四边形拼凑在一起的原因。 不过,它只能批量渲染具有相似属性的四边形。 每当遇到具有不同“状态”的四边形时,GPU将发生“状态改变”,并绘制当前批次的四边形。
我在本节中使用四边形(Quad)和图像(Image)同义。 记住,Image只是Quad的一个子类,只是它添加了几个方法。 此外,Quad扩展网格(Mesh),并且以下内容也适用于Mesh。 |
这些是构成状态的关键属性:
-
纹理 (不同的纹理来自同一图集)
-
显示对象的blendMode(混合模式)
-
Mesh/Quad/Image的textureSmoothing(平滑)值
-
Mesh/Quad/Image的textureRepeat(重复填充)值
如果您以尽可能少的更改状态的制作方式设置场景,则您的渲染性能将获得巨大的提升。
Starling的统计显示又提供了有用的数据。 它显示每帧执行多少次绘制调用。 应用程序状态更改次数越多,这个数字就越高。
统计显示小界面本身也会引发绘制调用。 但是,Starling已经将这个因素考虑在内,自身显示导致的绘制计数也一并统计,以得出最终明确的结论。 |
您的目标应始终保持绘制调用次数尽可能低。 以下提示将告诉你如何做到。
画家算法
要了解如何将状态更改次数降到最低,您需要知道Starling处理对象的逻辑顺序。
像Flash一样,Starling使用画家算法来处理显示列表。 这意味着它像画家一样绘制您的场景:从底层的对象(例如背景图像)开始并向上移动,在先前的对象之上绘制新对象。
如果您在Starling中设置了这样一个场景,您创建了三个精灵:一个包含远处的山脉,一个包含地面,另一个包含植被。 山脉处于底部(在_children中下标为0),植被处于顶部(下标2)。 每个精灵都将包含实际图像。
在渲染时,Starling将从左侧开始,以“山 1”开始,向右移动,直到达到“树 2”。 如果所有这些对象具有不同的状态,这意味着会产生六次绘制调用。 如果每个对象的纹理都是从单独的位图加载就一定会发生这种情况。
纹理图集
如果所有这些纹理都是从同一个图集中加载,Starling将能够一次绘制所有对象! 这就是为什么纹理图集是如此重要的原因之一。 (上面列出的影响状态的属性不改变的情况下)
这个实验结果指出我们应该总是把纹理封装到一个图集后使用。 这里,每个图像来源于同一个图集(由具有相同颜色描绘的所有节点)。
有时候,并不是所有的纹理都适合存放于同一图集。 因为纹理图集的尺寸是有上限的,所以你迟早会耗尽空间。 但这并不是问题,只要你以一种聪明的方式安排你的纹理。
这两个例子都使用两个图集(每个图集一个颜色)。 但是,当左侧的显示列表强制对每个对象进行状态更改时,右侧的版本将能够在两个批次中完成所有对象的绘制。
使用MeshBatch类
一次绘制大量四边形或其他网格的最快方法是使用MeshBatch类。 这是Starling内部所有渲染都会使用的类,所以它是经过重度优化的。.[6] 它的工作原理如下:
var meshBatch:MeshBatch = new MeshBatch();
var image:Image = new Image(texture);
for (var i:int=0; i<100; ++i)
{
meshBatch.addMesh(image);
image.x += 10;
}
addChild(meshBatch);
你注意到了吗?您可以随意添加相同的图像! 此外,添加对象是一个非常快的操作;例如不会调度任何事件(而将对象添加到容器时则会派发事件)。
正如预想的那样,这个机制也存在一些缺点:
-
您添加的所有对象必须具有相同的状态(例如:使用来自同一图集的纹理)。 您添加到MeshBatch的第一张图像将决定其状态。 您以后不能更改状态,除非将其完全重置。
-
您只能添加Mesh类或其子类(包括Quad,Image,甚至MeshBatch)的实例。
-
对象删除非常棘手:只能通过修剪MeshBatch内部的顶点和索引数来删除网格。 但是,您也可以在特定索引处覆盖网格。
由于这些原因,它仅适用于非常具体的用例(例如,BitmapFont类,其内部就使用了MeshBatch)。 在这些情况下,这绝对是最快的选择。您在Starling中找不到更有效的渲染大量物件的方式。
TextFields批处理
默认情况下,即使您的字形纹理是主要纹理图集的一部分,TextField也需要单独的一次绘图调用。 这是因为长文本需要大量的CPU计算以达到批次的目的;这种情况下只需简单地绘制它们即可(不需要将它们复制到MeshBatch)。
但是,如果文本字段只包含少数几个字符(经验法则:16个字符以下),则可以在TextField上启用`batchable`属性。 启用该功能后,您的文本将像其他显示对象一样能进行合并批渲染。
使用BlendMode.NONE
如果您有完全不透明的矩形纹理,可以通过禁用这些纹理的混合功能来节省GPU消耗。 这对于大背景图像特别有用。
backgroundImage.blendMode = BlendMode.NONE;
当然这也意味着产生了一个额外的状态变化,即多一次绘制调用,所以不要过度使用这种技术。 对于小图像,它可能不值得的这样做(除非它们由于其他原因导致状态已经改变)。
使用stage.color
通常情况下,您的游戏中看不到舞台的实际颜色,因为舞台上总是有图像或网格。
在这种情况下,始终将其设置为黑色(“0x0”)或白色(“0xffffff”)。 因为在某些移动硬件上调用“context.clear”,对于全部为1或全部为0的情况,似乎存在一个快速的硬件优化途径。 一些开发人员测试对比帧使用时间,发现即使这样一个简单的改变也有一个非常好的增益!
[SWF(backgroundColor="#0")]
public class Startup extends Sprite
{
// ...
}
另一方面,如果您的游戏背景是纯色,您也可以利用这一点:只需将舞台颜色设置为该值,而不是显示图像或彩色四边形。 任何情况下,Starling必须每帧清除一次舞台 - 因此,如果您更改舞台颜色,则该操作则不会有任何消耗。
[SWF(backgroundColor="#ff2255")]
public class Startup extends Sprite
{
// ...
}
避免查询宽度和高度
查询“宽”和“高”属性比你想象的更耗费性能,特别是在精灵上查询这些属性。 必须计算矩阵,并且每个子对象的每个顶点将与该矩阵相乘。
为此,请避免重复访问它们。例如,在一个循环中反复访问它们。 在某些情况下,使用常量值更有价值。
// 较差:
for (var i:int=0; i<numChildren; ++i)
{
var child:DisplayObject = getChildAt(i);
if (child.x > wall.width)
child.removeFromParent();
}
// 较好:
var wallWidth:Number = wall.width;
for (var i:int=0; i<numChildren; ++i)
{
var child:DisplayObject = getChildAt(i);
if (child.x > wallWidth)
child.removeFromParent();
}
禁用容器touchable功能
当您将鼠标/手指移动到屏幕上时,Starling必须找出哪个对象被选中。 这可能是一个非常耗性能的操作,因为它需要对每个显示对象进行碰撞测试(在最坏的情况下)。
因此,无论如何,如果你不关心他们被选中,禁用其touchable功能将有助于提高运行时性能。 最好在完整的容器上禁用touchable:因为这样,Starling也就不必对其孩子进行迭代。
// 好:
for (var i:int=0; i<container.numChildren; ++i)
container.getChildAt(i).touchable = false;
// 更好:
container.touchable = false;
隐藏在舞台边界之外的对象
Starling将发送显示列表的所有对象到GPU。 即使此对象在舞台界限之外,也是如此!
你可能会想:为什么Starling不自动忽略那些看不见的物体? 原因是以普通的方式检查可视性(CPU实时计算)是相当昂贵的。 事实上,与其这样做昂贵的CPU操作,不如将对象发送到GPU,GPU剪辑速度更快。 GPU实际上是非常高效的,如果对象在屏幕边界之外,则很快就会中止整个渲染管道。
但是,上传数据还需要时间,您可以避免这种情况的话更好。 在应用程序具体模块内或游戏内部逻辑中,通常更容易进行可见性检查(例如,您仅仅将x / y坐标与常量进行比较就可以做到)。 如果你有很多对象在这些边界之外,这是值得的。 从显示列表上移除这些元素,或者将它们的`visible`属性设置为`false`。
3.5. 自定义滤镜
你准备好开始行动了吗? 现在,我们正在进入自定义渲染代码的领域,让我们从一个简单的片段过滤器开始。
是的,这将涉及一些低级代码; 嘿嘿,你甚至需要写几行汇编程序! 但不要害怕,这不是火箭科学。 正如我的老数学老师曾经说过的:一个经过训练的猴子都能做到!
记住:滤镜是针对显示对象的每一个像素工作(换个说法:像素级)。 已过滤的对象将被渲染到纹理,然后通过自定义片段着色器(因此名称为fragment filter)进行处理。 |
3.5.1. 目标
我们选择一个尽量简单的目标,但它至少应该是一个有用的,对吧? 那就让我们创建一个ColorOffsetFilter(颜色偏移滤镜)吧。
你可能知道,可以通过给一个网格的任何顶点指定一种颜色来达到目的。 在渲染时,颜色将与纹理颜色相乘,这提供了一种非常简单(和快速)的方式来修改纹理的颜色。
var image:Image = new Image(texture);
image.color = 0x808080; // R = G = B = 0.5
背后的数学非常简单:在GPU上,每个颜色通道(红色,绿色,蓝色)由0和1之间的值表示。 例如纯红色表示如下:
R = 1, G = 0, B = 0
在渲染时,该颜色将乘以纹理的每个像素的颜色(也称为“纹素”)。 图像颜色的默认值为纯白色,意味着所有通道值都是1。 因此,纹理颜色看起来不变(与`1`的乘法结果是无操作)。 当你分配一个不同的颜色,经过乘法计算后将产生一种新的颜色,例如。
R = 1, G = 0.8, B = 0.6 × B = 0.5, G = 0.5, B = 0.5 ------------------------- R = 0.5, G = 0.4, B = 0.3
这里有问题:这只会使对象变得更暗,而不是更亮。 这是因为我们只能乘以0和1之间的值;0意味着结果将是黑色的,而1意味着它保持不变。
接下来我们想要解决滤镜的这个问题! 我们将在公式中包含offset。 (在经典Flash中,您可以使用 ColorTransform.)
-
新红色值=(旧红色值×红色倍数)+ 红色偏移量
-
新绿色值=(旧绿色值×绿色倍数)+ 绿色偏移量
-
新蓝色值=(旧蓝色值×蓝色倍数)+ 蓝色偏移量
-
新透明值=(旧透明值×透明倍数)+ 透明偏移量
我们已经有了倍增器,因为它是在基本Mesh类中处理的;我们的滤镜只需要添加偏移量即可。
现在进入正题,好吗?!
3.5.2. 扩展片段滤镜
所有的滤镜扩展自类’starling.filters.FragmentFilter`,这一个也不例外。 嘿,抓住了!我现在要扔给你一个完整的ColorOffsetFilter类;这不是代码片段,而是最终的完整代码! 我们不会再修改它。
public class ColorOffsetFilter extends FragmentFilter
{
public function ColorOffsetFilter(
redOffset:Number=0, greenOffset:Number=0,
blueOffset:Number=0, alphaOffset:Number=0):void
{
colorOffsetEffect.redOffset = redOffset;
colorOffsetEffect.greenOffset = greenOffset;
colorOffsetEffect.blueOffset = blueOffset;
colorOffsetEffect.alphaOffset = alphaOffset;
}
override protected function createEffect():FilterEffect
{
return new ColorOffsetEffect();
}
private function get colorOffsetEffect():ColorOffsetEffect
{
return effect as ColorOffsetEffect;
}
public function get redOffset():Number
{
return colorOffsetEffect.redOffset;
}
public function set redOffset(value:Number):void
{
colorOffsetEffect.redOffset = value;
setRequiresRedraw();
}
// the other offset properties need to be implemented accordingly.
public function get/set greenOffset():Number;
public function get/set blueOffset():Number;
public function get/set alphaOffset():Number;
}
至目前为止,以上代码还是非常紧凑的,对吧? 但是我不得不承认:这个故事并没有讲完,因为我们也要写另一个类,来做实际的颜色处理工作。 无论如何,分析上面所看到的代码是非常值得的。
该类扩展自FragmentFilter,它覆盖了一个方法:createEffect
。
以前你可能还没有遇到过`starling.rendering.Effect`类,因为只有低级渲染阶段才需要它。
通过阅读API文档可以得知:
Effect类封装了Stage3D绘图操作的所有步骤: 它配置渲染上下文,并设置着色器程序以及索引和顶点缓冲区,从而提供所有低级渲染的基本机制。
FragmentFilter类使用这个Effect类,或者Effect的子类FilterEffect。 对于这个简单的滤镜,我们只需要提供一个自定义的效果配置,我们通过重写“createEffect()”方法来实现。 它只是通过属性来配置效果,并不做其它的工作。 在渲染时,基类将自动使用配置的效果来呈现滤镜。 仅此而已!
如果您想知道什么是“colorOffsetEffect”属性:它其实相当于一个快捷方式,能够访问配置的相关属性,而不会持续将其转换为ColorOffsetEffect。 基类也提供了一个`effect`属性,但它会返回一个类型为’FilterEffect`的对象 - 但我们需要完整的类型ColorOffsetEffect来访问“offset”属性。 |
更复杂的滤镜可能需要重写“process”方法;例如需要创建多通道滤镜。 但对于我们目前讨论的示例滤镜,不必重写此方法。
最后,请注意调用“setRequiresRedraw”:它确保在更改设置后重新呈现效果。 否则,Starling不会重绘对象。
3.5.3. 扩展FilterEffect
我们来做点实际的工作吧 FilterEffect子类是实现这个滤镜的重头戏。 这并不意味着它非常复杂,耐着性子听我讲完还是很容易理解的。
我们从一个尚未完成的代码开始:
public class ColorOffsetEffect extends FilterEffect
{
private var _offsets:Vector.<Number>;
public function ColorOffsetEffect()
{
_offsets = new Vector.<Number>(4, true);
}
override protected function createProgram():Program
{
// TODO
}
override protected function beforeDraw(context:Context3D):void
{
// TODO
}
public function get redOffset():Number { return _offsets[0]; }
public function set redOffset(value:Number):void { _offsets[0] = value; }
public function get greenOffset():Number { return _offsets[1]; }
public function set greenOffset(value:Number):void { _offsets[1] = value; }
public function get blueOffset():Number { return _offsets[2]; }
public function set blueOffset(value:Number):void { _offsets[2] = value; }
public function get alphaOffset():Number { return _offsets[3]; }
public function set alphaOffset(value:Number):void { _offsets[3] = value; }
}
请注意,我们将偏移量存储在Vector中,因为这样可以轻松将其上传到GPU。 `offset’属性从该向量读取并写入该向量。 就是这么简单。
当我们看看这两个重写的方法时,它会变得更有趣。
createProgram
该方法支持创建实际的Stage3D着色器代码。
我将向您展示Stage3D的基础知识,但彻底解释Stage3D却超出了本手册的范围。 要想深入了解该主题,您可以随时查看以下教程: |
所有Stage3D渲染都是通过顶点着色器和片段着色器完成的。 这些是由GPU直接执行的小程序,它们有两种风格:
-
*顶点着色器*对于每个顶点执行一次。 他们的输入通常由“VertexData”类设置的顶点属性组成;它们的输出是屏幕坐标中顶点的位置。
-
*片段着色器*对于每个像素(片段)执行一次。 它们的输入由三角形的三个顶点的interpolated属性组成;输出便是具体像素的颜色。
-
一个片段着色器和一个顶点着色器共同组成一个*程序*。
着色语言由一种被成称为AGAL的汇编语言来编写。 (是的,你没看错!这是一种低级别的语言。) 幸运的是,典型的AGAL程序非常短,所以实际情况并不是像你想的那么糟糕。
有一个好消息:我们仅需要编写一个片段着色器。 因为顶点着色器相对于大多数片段着色器来说是相同的,所以Starling提供了一个标准的实现。 我们来看看代码:
override protected function createProgram():Program
{
var vertexShader:String = STD_VERTEX_SHADER;
var fragmentShader:String =
"tex ft0, v0, fs0 <2d, linear> \n" +
"add oc, ft0, fc0";
return Program.fromSource(vertexShader, fragmentShader);
}
如上所述事实并不难,顶点着色器取自常数;片段着色器只是两行代码。 两者组合成一个Program实例,并由一个方法返回。
当然,片段着色器还需要进一步阐述。
AGAL概述
在AGAL中,每行包含一个简单的方法调用。
[操作码] [结果],[参数 1],([可选参数 2])
-
前三个字母是操作码的名称(例如`tex`,
add
)。 -
下一个参数定义了操作结果的保存位置。
-
其他参数是方法的实际参数。
-
所有数据存储在预定义的寄存器中;将它们视为Vector3D实例(具有x,y,z和w的属性)。
有几种不同类型的寄存器,例如用于常量,临时数据或着色器的输出。 在这些着色器中,其中一些已经包含数据;他们是通过滤镜的其他方法设置的(稍后会介绍)。
-
`v0’包含当前纹理坐标(变量寄存器0)
-
`fs0’指向输入纹理(纹理样本寄存器0)
-
`fc0`包含这个颜色偏移量(片段常量寄存器0)
片段着色器的结果始终是一种颜色;该颜色将被存储在“oc”寄存器中。
代码审查
让我们回到片段着色器的实际代码。 *第一行*从纹理读取颜色:
tex ft0, v0, fs0 <2d, linear>
我们正在通过寄存器`v0’指定的纹理坐标和其它一些选项(2d,linear
)来读取纹理`fs0`。
纹理坐标在“v0”中的原因只是因为标准顶点着色器(“STD_VERTEX_SHADER”)将它们存储在那里;在编写着色程序时记住这一点。
最后将结果存储在临时寄存器`ft0`(记住:在AGAL中,结果总是存储在操作码后的第一个参数中)。
到目前为止我们还没有创建任何纹理,对吧?这是什么? 如上所述,片段着色器始终在像素级别工作;其输入是原始对象,输出结果呈现为纹理。 我们的基础类(FilterEffect)为我们设定了这个规则:当程序运行时,可以确保纹理采样器`fs0`将指向当前正在被过滤对象的像素。 |
你知道原因,其实我可能需要改写这一行。 您可能会注意到最后的可选参数,指定如何解释纹理数据。 事实证明如何编写这些可选参数取决于我们正在访问的纹理类型。 为了确保代码适用于每个纹理,我们统一使用一个帮助方法来写入该AGAL操作。
tex("ft0", "v0", 0, this.texture)
实际结果是一样的(该方法返回一个AGAL字符串),但我们不再需要关心选项了。 始终使用此方法访问纹理;它会让你在晚上睡得更安稳。
*第二行*干的事情是这样的:它将颜色偏移添加到纹素颜色。 偏移量存储在`fc0`中,我们稍后会看看;这被添加到`ft0`寄存器(我们刚读取的纹素颜色)并存储在输出寄存器('oc`)中。
add oc, ft0, fc0
这就是AGAL现在做的事情。 我们来看看其他被重写的方法。
beforeDraw
`beforeDraw`方法在着色器执行之前执行。我们可以使用它们来设置我们的着色器所需的所有数据。
override protected function beforeDraw(context:Context3D):void
{
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _offsets);
super.beforeDraw(context);
}
这是我们将偏移值传递给片段着色器的地方。
第二个参数“0”定义了数据将要结束的寄存器。
如果你回头看一下实际的着色器代码,你会看到我们读取’fc0’的偏移量,这正是我们在这里填补的:fragment constant 0
。
超类方法完成所有其余的设置,例如分配纹理(fs0
)和纹理坐标。
正如你想要询问的那样,确实还有一个`afterDraw()`方法,通常用于清理资源。 但是对于常量来说,这并不是必需的,所以我们可以在这个滤镜中忽略它。 |
3.5.4. 尝试一下
我们的滤镜已经准备好了,实际上(下载完整的代码 here)! 是时候跑一跑看看实际效果了.
var image:Image = new Image(texture);
var filter:ColorOffsetFilter = new ColorOffsetFilter();
filter.redOffset = 0.5;
image.filter = filter;
addChild(image);
老天! 红色的值肯定是更高的,但为什么它现在超出了鸟的面积呢? 毕竟,我们并没有改变alpha值!
不要惊慌。 你刚刚创建了你的第一个滤镜,它并没有打击到你,对吧? 这一定是值得的。 只是需要一些微调。
事实证明,我们忘了考虑“前乘alpha”(PMA)。 所有常规纹理都以其alpha通道预先乘以RGB通道进行存储。 所以,一个红色与50%的alpha,像这样:
R = 1, G = 0, B = 0, A = 0.5
实际上会像这样存储:
R = 0.5, G = 0, B = 0, A = 0.5
我们没有考虑到这一点。 接下来需要需要做的是将偏移值与当前像素的alpha值相乘,然后再将其输出。 这是第一种方法:
tex("ft0", "v0", 0, texture) // 从纹理获取颜色
mov ft1, fc0 // 将完整的偏移量复制到ft1
mul ft1.xyz, fc0.xyz, ft0.www // 将颜色偏移量的rbg通道值分别乘以alpha(pma!)
add oc, ft0, ft1 // 添加偏移量后将结果复制到输出寄存器
您可以看到,我们可以访问寄存器的“xyzw”属性来访问各个颜色通道(它们对应于我们的“rgba”通道)。
如果纹理不与PMA一起存储怎么办? `tex`方法确保我们总是使用PMA接收值,所以不用担心。 |
第二种尝试
当您再次尝试滤镜时(完整的代码: ColorOffsetFilter.as), 您会看到正确的Alpha值:
恭喜! 你刚刚完成了第一个滤镜,它工作的相当完美。 (是的,你可以刚刚使用Starling的“ColorMatrixFilter”,但是,这个比较快一点点,所以很值得一试。)
如果你内心坚定勇敢,那么现在可以尝试用网格样式来实现这个效果。 我承诺你的收获会大有不同!
3.6. 自定义样式
现在我们已经发掘了Stage3D的原始力量,让我们继续沿着这条道路前进! 在本节中,我们将编写一个简单的网格样式。 在Starling 2中,所有渲染都通过样式完成;通过创建自己的网格样式,您可以创建特殊效果,而不必担心带来任何性能损失。
在继续之前,请确保您已经阅读过[自定义滤镜]那一节内容。 滤镜和样式存在许多类似的概念,所以从两个更简单的例子开始是有意义的。 下面,我假设你已经熟悉在相关章节介绍过的所有内容。 |
3.6.1. 目标
目标是:最终效果和我们用ColorOffsetFilter得到的一样!我们希望为每个像素的颜色值添加一个偏移量。 只是这一次,我们用网格样式来达到目的!我们将它称为… ColorOffsetStyle。
在继续之前,了解滤镜和样式之间的区别至关重要。
滤镜与样式
如前所述,滤镜是像素级的工作:对象被渲染为纹理,然后滤镜以某种方式处理该纹理。 另一方面,样式可以访问对象的所有原始几何数据,或者更精确地说:访问对象的顶点。
虽然样式在某些方面受到了限制(例如,你不能通过样式实现模糊效果),但样式工作起来更高效。 首先,因为你不需要将对象绘制到纹理这一步。 第二,更重要的是:网格样式可以被批次渲染。
如你所知,保持较低的绘制调用的次数对于高帧率运行是非常重要的。 为了确保发生这种情况,Starling在绘制之前将尽可能多的把对象集中在一起。 问题是,如何决定把哪些对象分配到同一个批次? 这也是网格样式发挥作用的地方:只有具有相同样式的对象可以分批在一起。
如果您向舞台添加了三个应用了ColorOffsetFilter的图片,您将看到至少三次绘图调用。 同样是这三个对象,而改用ColorOffsetStyle,就只需要一次绘制调用。 虽然网格样式写起来并不是那么简便 - 但这个努力是值得付出的!
3.6.2. 扩展网格样式
所有网格样式的基类是`starling.styles.MeshStyle`。 这个类提供了我们需要的所有基础功能。让我们先看一个示例:
public class ColorOffsetStyle extends MeshStyle
{
public static const VERTEX_FORMAT:VertexDataFormat =
MeshStyle.VERTEX_FORMAT.extend("offset:float4");
private var _offsets:Vector.<Number>;
public function ColorOffsetStyle(
redOffset:Number=0, greenOffset:Number=0,
blueOffset:Number=0, alphaOffset:Number=0):void
{
_offsets = new Vector.<Number>(4, true);
setTo(redOffset, greenOffset, blueOffset, alphaOffset);
}
public function setTo(
redOffset:Number=0, greenOffset:Number=0,
blueOffset:Number=0, alphaOffset:Number=0):void
{
_offsets[0] = redOffset;
_offsets[1] = greenOffset;
_offsets[2] = blueOffset;
_offsets[3] = alphaOffset;
updateVertices();
}
override public function copyFrom(meshStyle:MeshStyle):void
{
// 待实现
}
override public function createEffect():MeshEffect
{
return new ColorOffsetEffect();
}
override protected function onTargetAssigned(target:Mesh):void
{
updateVertices();
}
override public function get vertexFormat():VertexDataFormat
{
return VERTEX_FORMAT;
}
private function updateVertices():void
{
// 待实现
}
public function get redOffset():Number { return _offsets[0]; }
public function set redOffset(value:Number):void
{
_offsets[0] = value;
updateVertices();
}
// 其他偏移属性需要相应地实现。
public function get/set greenOffset():Number;
public function get/set blueOffset():Number;
public function get/set alphaOffset():Number;
}
这是我们的出发点。 你会看到,已经比我们上次看到的最后一个初始滤镜类的例子多出了很多代码。 让我们来看看代码的各个部分。
顶点格式
第一件值得注意的事:位于类顶部的顶点格式常量。 我已经提到样式是顶点级别的工作,让您访问对象的所有几何数据。 VertexData类存储该几何数据,但我们从未实际讨论过该类具体把几何数据存放在哪里,以及是如何存放的。 这是由VertexDataFormat定义的。
MeshStyle使用的默认格式如下:
position:float2, texCoords:float2, color:bytes4
这个字符串的语法看起来应该是熟悉的;它是具有某些数据的属性列表
-
`position`属性存储两个浮点(顶点的x和y坐标)。
-
`texCoords`属性存储两个浮点数(顶点的纹理坐标)。
-
`color`属性存储顶点颜色的四个字节(每个通道一个字节)。
具有此格式的VertexData实例将为网格的每个顶点存储这些属性,属性存放的顺序与上述字符串显示的完全一致。 这意味着每个顶点将占用20个字节(8 + 8 + 4)。
当创建网格并且没有特别指定任何样式时,将交由默认的MeshStyle来渲染,顶点格式则采用上述标准格式。 你绘制纹理或彩色网格,所有必要的信息都存放在这里了。
但是对于我们的ColorOffsetStyle,这还不够:我们还需要存储我们的颜色偏移。 因此,我们需要定义一个新的格式,它添加一个由四个浮点值组成的“offset”属性。
MeshStyle.VERTEX_FORMAT.extend("offset:float4");
// => position:float2, texCoords:float2, color:bytes4, offset:float4
现在,你可能会问:为什么需要这么做?滤镜没有自定义顶点格式,不也是能正常工作吗?
这是一个很好的问题,我很高兴你问! 答案在于Starling的批处理代码。 当我们将样式分配给一些后续的网格时,它们将被分组在一起批处理 - 这是我们做这个努力的全部原因,对吧?
但是批处理是什么意思? 它意味着我们将所有单个网格的顶点复制到一个更大的网格并渲染。 在Starling的渲染内部,你会发现看起来像这样的代码:
var batch:Mesh = new Mesh();
batch.add(meshA);
batch.add(meshB);
batch.add(meshC);
batch.style = meshA.style; // ← !!!
batch.render();
你看到了的问题的实质吗?大网格(“batch”)的网格样式,即是第一个被添加的网格样式的副本! 虽然这三种样式可能会使用不同的设置。 如果这些设置只存储在style中,除了一个之外的所有设置将在渲染时丢失。 所以,必须将样式数据存储在其目标网格的VertexData中! 只有这样,大的batch网格才能单独接收所有偏移量。
由于这一点是如此重要!我再复述一遍: 样式的设置必须始终存储在目标网格的顶点数据中。 |
按照惯例,顶点格式总是作为样式类中的静态常量,访问时由`vertexFormat`属性返回。 当样式分配给网格时,其顶点将自动适应该新格式。
当你理解了这个概念,你就能理解这一切。 剩下的就是更新代码,以便从顶点数据而不是片段常量读取偏移量。
我讲的可能有些过头了。
成员变量
你会注意到,即使我坚持要将所有数据存储在顶点中,仍然有一组偏移存储在成员变量中:
private var _offsets:Vector.<Number>;
这是因为我们希望开发人员能够在将样式分配给网格之前对其进行配置。 即便没有目标对象,没有顶点数据,我们也能存储这些偏移,对吧? 所以我们使用这个向量。 一旦分配了目标,值就被复制到目标的顶点数据(参见`onTargetAssigned`)。
copyFrom
在批处理期间,样式有时必须从一个实例复制到另一个实例(主要是为了能够重用它们而不会给垃圾回收器带来压力)。 因此,有必要重写`copyFrom`方法。 我们这样做:
override public function copyFrom(meshStyle:MeshStyle):void
{
var colorOffsetStyle:ColorOffsetStyle = meshStyle as ColorOffsetStyle;
if (colorOffsetStyle)
{
for (var i:int=0; i<4; ++i)
_offsets[i] = colorOffsetStyle._offsets[i];
}
super.copyFrom(meshStyle);
}
直接了当:我们只是检查复制的样式是否类型正确,然后在当前实例上复制它的所有偏移。 剩下的事都交给超类完成。
createEffect
这看起来很熟悉,对吧?
override public function createEffect():MeshEffect
{
return new ColorOffsetEffect();
}
它的工作原理跟滤镜类相似;我们返回稍后将要创建的ColorOffsetEffect。 但是,它与滤镜使用方式不同(因为偏移值是从顶点读取的),但是可以创建一个适用于两者的效果。
onTargetAssigned
如上所述,我们需要将偏移存储在目标网格的顶点数据中。 是的,这意味着偏移值在每个顶点中都存在一份拷贝,即使这似乎有点浪费。 但这是保证样式支持批处理的唯一方法。
当滤镜被分配一个目标时,这个回调将被执行 - 根据这条线索我们知道顶点被更新了。 同样的工作我们可能需要在别处再做一次,所以我把实际的处理过程封装到`updateVertices`方法中。
override protected function onTargetAssigned(target:Mesh):void
{
updateVertices();
}
private function updateVertices():void
{
if (target)
{
var numVertices:int = vertexData.numVertices;
for (var i:int=0; i<numVertices; ++i)
vertexData.setPoint4D(i, "offset",
_offsets[0], _offsets[1], _offsets[2], _offsets[3]);
setRequiresRedraw();
}
}
你可能想知道`vertexData`对象来自哪里。 一旦目标被分配,`vertexData`属性将引用目标的顶点(样式本身从不拥有任何顶点)。 因此,上面的代码循环遍历目标网格的所有顶点,并分配正确的偏移值,准备在渲染期间使用。
3.6.3. 扩展MeshEffect
现在我们已经完成了样式类 - 是时候讨论效果了,这是实际渲染发生的地方。 这一次,我们要扩展MeshEffect类。 记住,效果类简化了低级渲染代码的编写。 我实际上谈论一组具有以下继承关系的类:
基类(Effect)简洁到了极致:它只绘制白色三角形。 FilterEffect添加了对纹理的支持,MeshEffect用于颜色和alpha。
这两个类也可以命名为TexturedEffect和ColoredTexturedEffect,但我选择关于用他们用法的命名方式。 如果你创建一个自定义滤镜,你需要扩展FilterEffect;如果你创建一个自定义网格样式,则需要扩展MeshEffect。 |
让我们来看一下ColorOffsetEffect的设置,我们稍后会介绍一些代码片断。
class ColorOffsetEffect extends MeshEffect
{
public static const VERTEX_FORMAT:VertexDataFormat =
ColorOffsetStyle.VERTEX_FORMAT;
public function ColorOffsetEffect()
{ }
override protected function createProgram():Program
{
// TODO
}
override public function get vertexFormat():VertexDataFormat
{
return VERTEX_FORMAT;
}
override protected function beforeDraw(context:Context3D):void
{
super.beforeDraw(context);
vertexFormat.setVertexBufferAt(3, vertexBuffer, "offset");
}
override protected function afterDraw(context:Context3D):void
{
context.setVertexBufferAt(3, null);
super.afterDraw(context);
}
}
如果你把它和上一个教程的模拟滤镜效果类进行比较,你会看到所有的’offset`属性被删除了;而我们现在覆盖`vertexFormat`,它确保我们使用的格式与指定样式相同,准备好每个顶点存储我们的偏移值。
beforeDraw & afterDraw
beforeDraw`和`afterDraw
-方法现在配置上下文,以便我们可以从着色器中读取offset属性为`va3`(vertex属性序号为3)。
让我们来看看`beforeDraw`中的那行:
vertexFormat.setVertexBufferAt(3, vertexBuffer, "offset");
这等价于以下这行代码:
context.setVertexBufferAt(3, vertexBuffer, 5, "float4");
第三个参数(5→bufferOffset
)表示顶点格式中颜色偏移的位置,最后一个参数(float4→format
)是该属性的格式。
所以我们不必计算和记住这些值,我们可以要求`vertexFormat`对象为我们设置这个属性。
这样,如果格式发生变化,我们就会在偏移之前添加另一个属性,代码将继续工作。
顶点缓冲区属性应该在绘制完成时始终清除,因为以下绘制调用可能使用不同的格式。 这就是我们在`afterDraw`方法中所做的。
createProgram
是时候介绍样式底层最核心的部分了;实际渲染的AGAL代码。 这一次,我们还要实现顶点着色器;它不会使用标准实现,因为我们需要添加一些自定义逻辑。 但是,片段着色器几乎与我们为滤镜写的一样。 让我们来看看!
override protected function createProgram():Program
{
var vertexShader:String = [
"m44 op, va0, vc0", // 将4x4变换矩阵输出到剪裁空间
"mov v0, va1 ", // 将纹理坐标传递给片段程序
"mul v1, va2, vc4", // 将alpha(vc4)与颜色(va2)相乘,并将结果复制到v1
"mov v2, va3 " // 将颜色偏移量复制到v2
].join("\n");
var fragmentShader:String = [
tex("ft0", "v0", 0, texture) + // 从纹理获取颜色
"mul ft0, ft0, v1", // 将颜色与纹理颜色相乘
"mov ft1, v2", // 将完整偏移量复制到ft1
"mul ft1.xyz, v2.xyz, ft0.www", // 将颜色偏移的rgb分量与alpha相乘 (pma!)
"add oc, ft0, ft1" // 将纹理颜色值与偏移量相加,并将计算结果输出到oc
].join("\n");
return Program.fromSource(vertexShader, fragmentShader);
}
要理解顶点着色器正在做什么,你首先要了解它的输入工作方式。
-
va
(vertex attribute 缩写,即顶点属性)寄存器包含当前顶点的属性,取自顶点缓冲区。 它们的排序与顶点格式中的属性排序完全一样,我们在前面已设置:`va0`是顶点位置,`va1`是纹理坐标,`va2`是颜色,`va3`是偏移量。 -
对于所有的顶点,两个常量是相同的:`vc0-3`包含modelview-projection矩阵,`vc4`是当前的alpha值。
任何顶点着色器的主要任务是将顶点位置移动到所谓的“剪裁空间”。 这是通过将顶点位置乘以`mvp矩阵`(modelview-projection matrix)来实现的。 顶点着色器第一行就是做这个事情的,在Starling中,无论在哪一个顶点着色器,你都能够发现它。 它负责找出顶点在屏幕上的位置。
另一方面,我们或多或少会通过变量寄存器‘v0’ - ‘v2’ 转移数据到片段着色器。
片段着色器几乎是与滤镜类相同的复制品。 你能找到区别吗? 我们正是从这种寄存器中读取颜色偏移量的。 之前,它存储在一个常数中,而现在,存储在变量寄存器`v2`中。
3.6.4. 尝试
有了它,我们几乎完成了我们的样式! 让我们来测试一下。 一个真正大胆的举动,我们立即把它应用在两个对象上,看看批处理是否正确工作。
var image:Image = new Image(texture);
var style:ColorOffsetStyle = new ColorOffsetStyle();
style.redOffset = 0.5;
image.style = style;
addChild(image);
var image2:Image = new Image(texture);
image2.x = image.width;
var style2:ColorOffsetStyle = new ColorOffsetStyle();
style2.blueOffset = 0.5;
image2.style = style2;
addChild(image2);
天啦噜,它正确的工作了! 一定要看看左上角的绘制调用(drawCall),一个诚实和恒定的“1”。
有一个小小的事情还要做;我们创建了上面的着色器,是假定总是可以通过某一个纹理来读取颜色数据。 然而,样式也可能被分配给一个不使用任何纹理的网格,所以我们必须为这种情况编写一些特定的代码(这是很简单,我不打算再详细说明)。
完整的类,包括解决了上述最后一个问题的参考示例,可以在这里找到: ColorOffsetStyle.as.
3.7. 距离场渲染
如前所述,位图字体是在Starling中渲染文本的最快方式。 但是,如果您需要显示多种大小的文本,您很快就会发现位图字体无法很好地缩放。 放大使它们模糊,缩小引入混叠问题。 因此,为了获得最佳结果,必须字体嵌入应用程序中,字体需要包含应用程序需要的全部尺寸。
Distance Field Rendering解决了这个问题:它允许GPU对位图字体和其他单色形状绘制出无锯齿边缘,即使在高放大倍率时仍不失真。 该技术首先由Valve Software在SIGGRAPH文章中介绍: http://tinyurl.com/AlphaTestedMagnification. Starling包含一个MeshStyle,支持将此功能添加到Starling。
为了理解它的工作原理,我将首先展示如何在单个图像上使用它。 例如,您希望在整个应用程序中使用的图标。
3.7.1. 呈现单个图像
我们在这本手册中已经遇到了很多鸟,这一次我们去捕食一只! 我的猫能胜任这项工作。 我用一个黑色的矢量轮廓作为她的肖像,用于这个示例简直完美。
不幸的是,Starling不能显示矢量图像;我们需要Seven的位图纹理(PNG格式)。
只要我们想显示原始大小的猫(scale == 1
),这个方法能很好的工作。
然而,当我们放大图像,它很快变得模糊。
我们可以通过将这个图像转换为距离场纹理来避免模糊。 Starling实际上包含一个方便的小工具,处理这个转换过程。 它被称为“Field Agent”,可以在Starling存储库的`util`目录中找到。
您需要安装Ruby和ImageMagick才能使用Field Agent。 查看随附的“README”文件,了解这些依赖项以便正确安装。 该工具既可以Windows上工作,也可以在macOS工作。 |
我从一个高分辨率PNG版本的猫开始,并传递给Field Agent。
ruby field_agent.rb cat.png cat-df.png --scale 0.25 --auto-size
这将创建一个具有原始大小的25%的距离场纹理。 如果你给它一个高分辨率的纹理,让它缩小,Field Agent工作效果最好。 Field Agent编码形状的细节,因此它可以比输入纹理小得多。
猫原来尖锐的轮廓已被替换为模糊渐变。 这就是距离场:它编码每个像素到原始形状中最接近边缘的距离。
这个纹理实际上是在透明背景上的纯白色;我在此把背景着成黑色,是为了让你看到更好的效果。 |
模糊量称为spread。 Field Agent默认使用八个像素,但您可以自定义。 较高的模糊量允许更好的缩放,并且更容易添加特殊效果(稍后更多),但是其可能的范围取决于输入图像。 如果输入包含非常细的线,则没有足够的空间用于高模糊量。
要在Starling中显示此纹理,我们只需加载纹理并将其分配给Image即可。 分配DistanceFieldStyle将使Starling切换到distance field rendering。
var texture:Texture = assets.getTexture("cat-df");
var image:Image = new Image(texture);
image.style = new DistanceFieldStyle();
image.color = 0x0; // we want a black cat
addChild(image);
应用此样式,纹理保持完美清晰,即使具有高缩放值。 你能看到周围非常细小的区域的细节(如Seven的发型)。
根据创建纹理时使用的“spread”,您可能需要更新“softness”参数以获得您想要的锐度/平滑度。 这是样式的构造函数的第一个参数。
经验法则:`softness = 1.0 / spread'。 |
渲染模式
这实际上只是距离场纹理最基本的应用。 距离场样式支持几种不同的渲染模式;即阴影、轮廓和发光。 这些效果都在特定的片段着色器中渲染,这意味着它们不需要任何额外的绘制调用。 换句话说,这些效果基本上是免费的,性能优越的!
var style:DistanceFieldStyle = new DistanceFieldStyle();
style.setupDropShadow(); // or
style.setupOutline(); // or
style.setupGlow();
很酷,是不是?
注意:唯一的限制:您不能组合两种模式,例如。同时有轮廓和阴影。 你仍然可以通过片段过滤器实现,如果有需要的话。
3.7.2. 距离场字体
距离场渲染的特点使其非常适合文本。 好消息:Starling的标准位图字体类与距离场样式非常吻合。 不过创建实际的字体纹理,恐怕有点麻烦。
记住,位图字体包含一个包含所有字形的图集纹理,以及一个描述每个字形属性的XML文件。 你不能简单地使用Field Agent处理转换纹理(至少不容易),因为每个字形需要一些填充来弥补模糊量spread。
因此,最好使用支持距离场纹理的位图字体工具。 以下列举了一些可供选择的方式:
就个人而言,我使用Hiero获得了最好的结果,虽然它的用户界面并不友好。 我希望未来的产品会有所改善。
无论使用什么工具或过程:最后,你将有一个纹理和一个`.fnt`文件,就像往常一样。 小小提示,下面是创建和注册位图字体的代码:
[Embed(source="font.fnt", mimeType="application/octet-stream")]
public static const FontXml:Class;
[Embed(source="font.png")]
public static const FontTexture:Class;
var texture:Texture = Texture.fromEmbeddedAsset(FontTexture);
var xml:XML = XML(new FontXml());
var font:BitmapFont = new BitmapFont(texture, xml)
TextField.registerCompositor(font);
var textField:TextField = new TextField(200, 50, "I love Starling");
textField.format.setTo(font.name, BitmapFont.NATIVE_SIZE);
addChild(textField);
一直到这里,都没有什么新的东西需要处理,不过,马上有了。 要切换到距离场渲染,我们将适当的样式附加到TextField。
var style:DistanceFieldStyle = new DistanceFieldStyle();
textField.style = style;
作为以上辛勤工作的奖励:这种字体现在可以适应任何缩放值,并可以应用之前所述所有灵活的渲染模式。
3.8. 总结
为自己加油:我们刚刚讨论了很多相当高级的话题。
-
您现在熟悉ATF纹理,它不仅是存储效率高,加载速度也比标准的PNG要快。
-
你知道如何从设备丢失中恢复:通过依靠AssetManager或自己提供恢复代码。
-
你找到感觉了,如何确保你不浪费任何内存,以及如何避免和发现内存泄漏。
-
当性能成为一个问题,你的第一个看法是观察drawCall计数。 你知道如何确保批处理不会中断。
-
原来你想起来都可怕的低级渲染代码。见鬼,现会竟然会编写自己的滤镜和风格!
-
距离场渲染是一个有用的技术,以用于可缩放字体或其他单色形状。
这些知识将为您在任何即将到来的项目中节省大量的时间和麻烦。 我打赌他们中的一些将要在移动硬件上运行,对吗?
4. 移动开发
Adobe AIR是当今面向跨平台开发的最强大的解决方案之一。 当某人现在说“跨平台”时,通常意味着:iOS和Android。
开发这样的移动平台可能是非常具有挑战性的:围绕着各种不同的设备类型,其屏幕分辨率范围从insult到eye到sanely high,以及纵横比都不符合任何逻辑。 雪上加霜就,其中一些设备配置的CPU,设计上只为袖珍计算器功能考虑,并没有意图提供任何其他功能。
作为开发者,你只能耸耸肩膀,卷起你的袖子,直接跳到这个坑里去。 至少你知道名利和财富就在坑的另一边!![7]
4.1. 多分辨率开发
嗯,我们在单一屏幕上开发游戏是什么时候的事了? 那时,我们在HTML页面中有一个小的矩形区域,那就是我们放置我们的精灵,文本和图像的地方。 一个单一的解决方案 - 不是很好吗?
唉…是改变的时候了! 手机具有各种类型和尺寸,甚至台式电脑和笔记本电脑都具有高密度显示器。 这对我们的消费者而言是一个好消息,但它并不能让我们这些开发者的开发生涯变得更容易,这是肯定的!
但不要放弃治疗:你可以搞定这个。 通过一系列需要预先考虑的事件,和利用Starling提供的一些简单的机制。
问题在于,万事开头难。 我们将在一个个小步骤中完成这个工作 - 我们将从2007年开始。
是的,你听到了:进入DeLorean,启动Flux Capacitor(TM),并按每小时八十英里的速度前进。
4.1.1. iPhone
iPhone可以说是休闲游戏最受欢迎的平台。 早在2007年,它也是你可以轻松开发的唯一一个平台。 这是大型App Store淘金热的时候!
其固定分辨率为320×480像素,第一款iPhone超级容易开发。 当然,Starling如果那时候就有了,你会这样启动它:
var screenWidth:int = stage.fullScreenWidth;
var screenHeight:int = stage.fullScreenHeight;
var viewPort:Rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
starling = new Starling(Game, stage, viewPort);
我们将viewPort设置为屏幕的全尺寸:320×480像素。 默认情况下,舞台将具有完全相同的尺寸。
到目前为止,就这么简单:这就像在浏览器中的游戏一样。 (那会是Internet Explorer 6,对吧?)
下一站:2010。
4.1.2. iPhone视网膜屏幕
我们将DeLorean停在旧苹果校园的拐角处,查看App Store图表。 欢呼!显然,我们的游戏在2007年取得了巨大的成功,而且还在前十名! 没时间了,我们输不起:我们必须确保在几周内iPhone 4上看起来很好。
由于我们来自未来,我们知道其主要创新点:被苹果营销团队称为“视网膜显示”的高分辨率屏幕。 我们从2007年开始我们的游戏,并启动了这个尚待发布的设备。
可惜,游戏现在只占了四分之一的屏幕! 这是为什么?
如果你回顾我们在2007年写的代码,你会看到我们使viewPort和屏幕一样大。
使用iPhone 4,这些值翻了一番:其屏幕有640×960像素。
在舞台上放置显示对象的代码预计只有320×480的坐标系。
所以放在右边的东西(x = 320
)现在突然在中心了。
这很容易解决。 记住:Starling的“viewPort”和“stageWidth / Height”属性可以独立设置。
-
ViewPort(视口)决定了Starling显示的画面在屏幕的哪个区域。 它总是以像素为单位指定。
-
舞台尺寸决定在该视口中显示的坐标系的大小。 当您的舞台宽度为320时,任何x坐标在0和320之间的对象都将处于舞台中,无论viewPort的大小如何。
有了这个知识,升级之事就是小菜一碟:
var screenWidth:int = stage.fullScreenWidth;
var screenHeight:int = stage.fullScreenHeight;
var viewPort:Rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
starling = new Starling(Game, stage, viewPort);
starling.stage.stageWidth = 320;
starling.stage.stageHeight = 480;
viewPort仍然是动态的,具体取决于游戏开始的设备;但是,我们在底部添加了两行,将舞台大小硬编码为固定值。
由于这些值不再指示像素,我们现在称呼它们为:点:我们的舞台大小现在是320×480点。 |
在iPhone 4上,游戏现在看起来像这样:
这更好:我们现在正在使用全屏尺寸。 但是,它也有点模糊。 我们并没有真正使用大屏幕。 我可以看到糟糕的评论进来了…我们需要解决这个问题!
高清纹理
该问题的解决方案是提供高分辨率的特殊纹理。 根据像素密度,我们将使用低或高分辨率纹理集。 优点:除了选择纹理的逻辑,我们不需要更改我们的任何代码。
但是,仅仅加载一组不同的文件还不够。 毕竟,较大的纹理将为width和height返回更大的值。 我们固定的舞台宽度为320点。
-
宽度为160像素的SD纹理将填充舞台的一半;
-
相应的高清纹理(宽度:320像素)将填满整个舞台。
我们想要的是使HD纹理返回与SD纹理相同的大小,但提供更多的细节。
这时候,Starling的contentScaleFactor就派上用场了。 当我们配置Starling的stage和viewPort大小时,我们隐式地设置它。 使用上述设置,在iPhone 4上运行以下代码:
trace(starling.contentScaleFactor); // → 2
contentScaleFactor返回viewPort width/stage width。 在视网膜设备上,将为“2”; 在非视网膜设备上,将为“1”。 这告诉我们在运行时加载哪些纹理。
contentScaleFactor是一个整数不是巧合。 苹果公司每行/每列的像素数量翻倍,以尽可能避免混叠问题。 |
纹理类有一个类似的属性,简称为“scale”。 当正确设置时,纹理将按我们预想的那样工作。
var scale:Number = starling.contentScaleFactor; (1)
var texturePath:String = "textures/" + scale + "x"; (2)
var appDir:File = File.applicationDirectory;
assetManager.scaleFactor = scale; (3)
assetManager.enqueue(appDir.resolvePath(texturePath));
assetManager.loadQueue(...);
var texture:Texture = assetManager.getTexture("penguin"); (4)
trace(texture.scale); // → Either '1' or '2' (5)
1 | 从Starling实例获取`contentScaleFactor`。 |
2 | 根据比例因子,纹理将从“1x”或“2x”目录加载。 |
3 | 通过将相同的比例因子分配给AssetManager,所有纹理将使用该值进行初始化。 |
4 | 访问纹理时,您不需要关注比例因子。 |
5 | 然而,您可以随时通过`scale`属性找出纹理的比例。 |
不使用AssetManager? 别担心,所有的“Texture.from …”方法都包含一个额外的比例因子参数。 创建纹理时必须正确配置; 该值不能稍后更改。 |
当您查询纹理宽度或高度时,现在需要考虑scale factor。 例如,假如这是游戏全屏背景纹理,将会发生什么。
File | Size in Pixels | Scale Factor | Size in Points |
---|---|---|---|
textures/1x/bg.jpg |
320×480 |
1.0 |
320×480 |
textures/2x/bg.jpg |
640×960 |
2.0 |
320×480 |
现在我们拥有所需的所有工具!
-
我们在后座的平面设计师(称他为Biff)以高分辨率创建所有纹理(理想情况下,作为矢量图形)。
-
在预处理步骤中,纹理将转换成我们要支持的实际分辨率(“1x”,“2x”)。
-
在运行时,我们检查Starling的contentScaleFactor并加载对应的纹理。
现在我们有一个清晰的视网膜游戏了! 我们的玩家将会喜欢它的,我确信这一点。
使用像 TexturePacker 这样的工具让事情变得非常简单. 给他们提供所有的纹理(最高分辨率),并让它们创建多个纹理图集,即每个缩放因子对应一个图集。 |
我们在Redwood的 bar 上庆祝我们的成功,喝一两杯啤酒然后继续前进。
4.1.3. iPhone 5
2012年,iPhone对我们来说又有一个惊喜:苹果改变了屏幕的纵横比。 在水平方面,它仍然是640像素宽;但垂直方面,现在已经有点长了(1136像素)。 它仍然是一个视网膜屏幕,当然,我们的新的逻辑分辨率变成了320×568点。
作为一个快速的解决方案,我们只需将我们的舞台放在viewPort上,并且让它伴随顶部和底部的黑色条纹一起呈现。
var offsetY:int = (1136 - 960) / 2;
var viewPort:Rectangle = new Rectangle(0, offsetY, 640, 960);
嗯, 似乎正常工作了! 对于在这个时间线上开始出现的所有Android智能手机来说,这是一个公平的策略。 是的,我们的游戏在某些设备上可能看起来有点模糊,但这并不太糟糕:图像质量仍然令人惊喜。 大多数用户不会注意到。
我称之为*黑边模式*。
-
以固定的舞台大小开发游戏(如320×480点)。
-
根据比例因子(例如“1x”,“2x”,“3x”)添加几组资源。
-
然后,您可以扩展应用程序,使其填充屏幕而不会发生任何失真。
这可能是最务实的解决方案。 它允许您的游戏在所有可用的显示分辨率上以可接受的质量运行,除了将viewPort设置为正确的大小外,您不必进行任何额外的工作。
顺便说一句,当您使用Starling附带的RectangleUtil时,事情变得非常容易处理。 要“放大”您的viewPort,只需使用以下代码创建它:
const stageWidth:int = 320; // points
const stageHeight:int = 480;
const screenWidth:int = stage.fullScreenWidth; // pixels
const screenHeight:int = stage.fullScreenHeight;
var viewPort:Rectangle = RectangleUtil.fit(
new Rectangle(0, 0, stageWidth, stageHeight),
new Rectangle(0, 0, screenWidth, screenHeight),
ScaleMode.SHOW_ALL);
简单高效! 我们乘坐时光机再来一次旅行。 出发!
4.1.4. iPhone 6 and Android
我们现在在2014年…天哪! 检查“苹果商店年鉴”,我们发现在上次更新后,我们的销售情况并不好。 显然,苹果对我们的黑边方法并不满意,这次并没有提供这个功能给我们。 可恶。
嗯,我想我们现在别无选择了,让我们咬住子弹,并利用这个额外的屏幕空间。 是告别硬编码坐标的时候了,我们竟然用了这么久! 从现在开始,我们需要为所有的显示对象使用相对位置。
我会称这个策略*智能对象布局*。 启动代码仍然非常相似:
var viewPort:Rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
starling = new Starling(Game, stage, viewPort);
starling.stage.stageWidth = 320;
starling.stage.stageHeight = isIPhone5() ? 568 : 480;
是的,我也感觉到哪里不对。 根据我们正在运行的设备硬编码舞台高度…这不是一个非常聪明的想法。 我承诺,我们即将解决这个问题。
到现在为止,它能正常的工作,毕竟:viewPort和stage都有正确的大小。 但是我们该怎么做显得更聪明呢? 我们现在来看看Game类,这个类将作为Starling的根类。
public class Game extends Sprite
{
public function Game()
{
addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); (1)
}
private function onAddedToStage():void
{
setup(stage.stageWidth, stage.stageHeight); (2)
}
private function setup(width:Number, height:Number):void
{
// ...
var lifeBar:LifeBar = new LifeBar(width); (3)
lifeBar.y = height - lifeBar.height;
addChild(lifeBar);
// ...
}
}
1 | 当游戏的构造函数被调用时,它尚未连接到舞台。所以我们推迟初始化,直到连接到舞台。 |
2 | 我们调用自定义的“setup”方法,并传入舞台大小。 |
3 | 示例,我们在屏幕底部创建一个LifeBar实例(一个自定义的用户界面类)。 |
总而言之,这不是太难,对吧? 诀窍是始终考虑舞台尺寸。 在这里,如果您在干净的组件中创建游戏,并且单独的类负责不同的界面元素,这将是有益的。 对于任何有意义的元素,您可以传递大小(如上面的LifeBar构造函数),并使其相应地执行。
这在iPhone 5上非常出色。 我们应该在2012年做到这一点,而不是2014年! 在2014年,事情变得更复杂了。
-
Android正在迅速获得市场份额,手机具有不同的尺寸和分辨率。
-
苹果公司也推出了更大的屏幕 - iPhone 6和iPhone 6 Plus。
-
我是否提到平板电脑?
我们通过组织相对于舞台尺寸的显示对象,已经奠定了解决这个问题的基础。 我们的游戏几乎能运行任何尺寸的舞台之上。
剩下的问题是使用一个值-舞台大小和内容的比例因子。 看看我们必须处理的屏幕区域,这似乎是一项艰巨的任务!
Device | Screen Size | Screen Density | Resolution |
---|---|---|---|
iPhone 3 |
3,50" |
163 dpi |
320×480 |
iPhone 4 |
3,50" |
326 dpi |
640×960 |
iPhone 5 |
4,00" |
326 dpi |
640×1136 |
iPhone 6 |
4,70" |
326 dpi |
750×1334 |
iPhone 6 Plus |
5,50" |
401 dpi |
1080×1920 |
Galaxy S1 |
4,00" |
233 dpi |
480×800 |
Galaxy S3 |
4,80" |
306 dpi |
720×1280 |
Galaxy S5 |
5,10" |
432 dpi |
1080×1920 |
Galaxy S7 |
5,10" |
577 dpi |
1440×2560 |
确定比例因子的关键是考虑到屏幕的密度。
-
密度越高,比例因子越高。 换句话说:我们可以从密度推断出比例因子。
-
从比例因子我们可以计算适当的舞台大小。 基本上我们扭转了我们以前的做法。
原始iPhone的屏幕密度约为160 dpi。 我们将其作为我们计算的基础:对于任何设备,我们将密度除以160,并将结果取为大于或等于当前值的最近一个整数。 下面对这种做法进行细致的检查。
Device | Screen Size | Screen Density | Scale Factor | Stage Size |
---|---|---|---|---|
iPhone 3 |
3,50" |
163 dpi |
1.0 |
320×480 |
iPhone 4 |
3,50" |
326 dpi |
2.0 |
320×480 |
iPhone 5 |
4,00" |
326 dpi |
2.0 |
320×568 |
iPhone 6 |
4,70" |
326 dpi |
2.0 |
375×667 |
iPhone 6 Plus |
5,50" |
401 dpi |
3.0 |
414×736 |
Galaxy S1 |
4,00" |
233 dpi |
1.5 |
320×533 |
Galaxy S3 |
4,80" |
306 dpi |
2.0 |
360×640 |
Galaxy S5 |
5,10" |
432 dpi |
3.0 |
360×640 |
Galaxy S7 |
5,10" |
577 dpi |
4.0 |
360×640 |
看看计算出来的舞台的大小:现在它们从320×480到414×736点。 这是一个适度的范围,这也是有道理的:一个物理尺寸更大的屏幕料想会有更大的舞台。 重要的是,通过选择适当的比例因子,我们得到了合理的坐标系。 这是我们可以依据的尺寸范围!
您可能已经注意到Galaxy S1的比例因子不是整数值。 这是必要的,以达到可接受的舞台大小。 |
我们来看看我是如何得到这些比例因子的。 创建一个名为“ScreenSetup”的类,并从以下内容开始:
public class ScreenSetup
{
private var _stageWidth:Number;
private var _stageHeight:Number;
private var _viewPort:Rectangle;
private var _scale:Number;
private var _assetScale:Number;
public function ScreenSetup(
fullScreenWidth:uint, fullScreenHeight:uint,
assetScales:Array=null, screenDPI:Number=-1)
{
// ...
}
public function get stageWidth():Number { return _stageWidth; }
public function get stageHeight():Number { return _stageHeight; }
public function get viewPort():Rectangle { return _viewPort; }
public function get scale():Number { return _scale; }
public function get assetScale():Number { return _assetScale; }
}
这个类将会找出Starling如何合理配置viewPort和stage size。 大多数属性应该是其意自见的 - 或许`assetScale`除外。
上表显示,我们将以“1”到“4”的比例因子结束。 但是,我们可能不想为所有尺寸都创建匹配的纹理。 反正最密集的屏幕的像素很小,你的眼睛不可能区分它们。 因此,您通常只会为这些比例因子的一部分(例如1-2或1-3)提供资源。
-
构造函数中的`assetScales`参数应该是一个填充了您创建纹理的比例因子的数组。
-
“assetScale”属性将告诉您需要加载哪些资源图集。
如今,应用程序要求比例因子“1”的情况已经是罕见的。 但是,在开发过程中,该尺寸非常方便,因为您可以预览界面而不需要极大的电脑屏幕。 |
我们来看看那个构造函数的实现。
public function ScreenSetup(
fullScreenWidth:uint, fullScreenHeight:uint,
assetScales:Array=null, screenDPI:Number=-1)
{
if (screenDPI <= 0) screenDPI = Capabilities.screenDPI;
if (assetScales == null || assetScales.length == 0) assetScales = [1];
var iPad:Boolean = Capabilities.os.indexOf("iPad") != -1; (1)
var baseDPI:Number = iPad ? 130 : 160; (2)
var exactScale:Number = screenDPI / baseDPI;
if (exactScale < 1.25) _scale = 1.0; (3)
else if (exactScale < 1.75) _scale = 1.5;
else _scale = Math.round(exactScale);
_stageWidth = int(fullScreenWidth / _scale); (4)
_stageHeight = int(fullScreenHeight / _scale);
assetScales.sort(Array.NUMERIC | Array.DESCENDING);
_assetScale = assetScales[0];
for (var i:int=0; i<assetScales.length; ++i) (5)
if (assetScales[i] >= _scale) _assetScale = assetScales[i];
_viewPort = new Rectangle(0, 0, _stageWidth * _scale, _stageHeight * _scale);
}
1 | 我们需要为Apple iPad添加一个小的解决方法。 我们希望它使用与iOS上本机相同的比例因子。 |
2 | 我们的基本密度为160 dpi(iPad上为130 dpi)。 具有这种密度的设备将使用比例因子“1”。 |
3 | 我们的比例因子应该是整数值或“1.5”。 该代码选择最接近的一个。 |
4 | 在这里,我们选择要加载的资源集。 |
如果要查看此代码在上表设备上运行的结果,请参阅此 Gist. 您可以轻松地在此列表中添加更多设备,并查看您是否对结果感到满意。 |
现在,一切都到位,我们可以调整Starling的启动代码。 此代码假定您提供的比例因子为“1”和“2”。
var screen:ScreenSetup = new ScreenSetup(
stage.fullScreenWidth, stage.fullScreenHeight, [1, 2]);
_starling = new Starling(Root, stage, screen.viewPort);
_starling.stage.stageWidth = screen.stageWidth;
_starling.stage.stageHeight = screen.stageHeight;
在加载资源时,使用`assetScale`属性。
var scale:Number = screen.assetScale;
var texturePath:String = "textures/" + scale + "x";
var appDir:File = File.applicationDirectory;
assetManager.scaleFactor = scale;
assetManager.enqueue(appDir.resolvePath(texturePath));
assetManager.loadQueue(...);
就是这样! 您仍然需要时刻考虑舞台大小以设置您的用户界面与之相适应,但这绝对是可以做到的。
Starling存储库包含一个名为Mobile Scaffold的项目,其中包含所有这些代码。 这是任何移动应用程序的完美起点。 (如果您在下载中找不到ScreenSetup类,请查看GitHub项目的最新版本) |
如果您使用Feathers,类ScreenDensityScaleFactorManager将执行我们上面写的ScreenSetup类所做的工作。 事实上,这里描述的逻辑很多的灵感来自于该类。 |
4.1.5. iPad和其他平板电脑
回过头来看, 我们开始怀疑将游戏移植到平板电脑是否有意义。 上面的代码在平板电脑上工作得很好,然而我们将面临更大的舞台,更多的内容空间。 如何处理这取决于您正在创建的应用程序。
4.2. 设备旋转
今天智能手机和平板电脑的一个非常酷的功能就是它们识别出物理世界中设备的方向,并可以相应地更新用户界面。
要检测Starling中的方向更改,您首先需要更新应用程序的AIR配置文件。 确保它包括以下设置:
<aspectRatio>any</aspectRatio> (1)
<autoOrients>true</autoOrients> (2)
1 | 初始宽高比(“portrait”,“landscape”或“any”)。 |
2 | 表示应用程序在启动时是否开始自动定向。 |
当这样做的时候,你可以在Starling舞台上监听“RESIZE”事件。 每当方向发生变化时这个事件都会被调度。 毕竟,方向变化总是导致舞台大小改变(交换width和height)。
在相应的事件处理程序中更新Starling viewPort和stage的尺寸。
stage.addEventListener(Event.RESIZE, onResize);
private function onResize(event:ResizeEvent):void (1)
{
updateViewPort(event.width, event.height);
updatePositions(event.width, event.height);
}
private function updateViewPort(width:int, height:int):void (2)
{
var current:Starling = Starling.current;
var scale:Number = current.contentScaleFactor;
stage.stageWidth = width / scale;
stage.stageHeight = height / scale;
current.viewPort.width = stage.stageWidth * scale;
current.viewPort.height = stage.stageHeight * scale;
}
private function updatePositions(width:int, height:int):void (3)
{
// Update the positions of the objects that make up your game.
}
1 | 设备旋转时调用此事件处理程序。 |
2 | 根据当前的屏幕尺寸(像素)更新舞台和viewPort的大小。 |
3 | 更新您的用户界面,使其符合新的方向。 |
请注意,我们必须在事件侦听器中手动更新viewPort和stage size。 默认情况下,它们将保持不变,这意味着您的应用程序将出现裁剪。 上面的代码修复了这个问题; 它现在能适用于不同的缩放因子。
最后一部分会变得更加困难:更新您的用户界面,使其适应新的舞台维度。 这并非对所有游戏都有意义 - 但如果有必要的话,你应该考虑额外的自适应工作。 您的用户会感受到更好的体验!
Starling附带的Scaffold项目包含了此功能的可能实现。 |
5. 结束语
5.2. 获得帮助
想分享你刚刚获得的知识吗? 或者您有任何尚未回答的问题吗? Starling社区渴望你的到来! 欢迎访问官方 Starling 论坛.
5.3. 想了解更多?
如果你觉得关于Starling这还不够,不要忘了http://eepurl.com/chQGpD[报名预约] Starling指南的新书发布! 除了本手册的所有内容,它还提供了易于使用的技巧,你可以放在自己的项目中使用。 同时也更好的支持Starling持续发展!