Corona中文站

强大、易学的跨平台(iOS/Android)开发框架。QQ群1:74390406(满) 群2:221929599

导航

五分钟学会Corona(五) - DisplayObject和Stage
所有发生在屏幕上的绘制活动,都通过创建DisplayObject来完成。实际上,任何在屏幕上显示的东西,都是一个DisplayObject的实例。

创建Display Objects

实际上,你并不直接创建这些对象。而是,你创建一些特定类型的DisplayObject,例如 rectangles, circles, images, text, groups, 等等。

这些对象都是第一类。你可以修改他们的位置、旋转他们、启用动画,把他们当作按钮,等。

所有这些对象共有一些常规的属性和方法,在Corona SDK语言和API参考的 Display Objects 一章里可以看到。

所有DisplayObject对象实例可以,象普通的lua table一样对待。这意味着你可以给这些对象添加你自己的属性,只要他们不和DisplayObject自己预定义的属性和方法冲突就行。有一个例外,就是你不能象一个数组一样使用数字索引来操作DisplayObject。

绘画者模型

通过绘画者模型,DisplayObject被绘制到屏幕上。理解这个模型最容易的办法,就是想象一个真实的绘画过程。这里,你一开始画上的内容会在底下,而之后画上去的会在上面。每个后续的画刷笔触,遮盖住之前的。

你可以认为DisplayObject有点类似与一个画刷笔触。当你创建一个DisplayObject,就表示你在一个已经存在的显示对象上,“画”一个新的对象。当你向屏幕上画上更多的对象,你后画上去的对象将会遮盖住前面画上去的。

显示层级

为了管理画上去的DisplayObjects的顺序,DisplayObject被组织在一个层级当中。这个层级确定了什么对象在什么对象之上。

Group Objects

组对象的存在,使得层级成为可能。组对象是一种特别的DisplayObject,它可以包含子对象。组对象使得组织你的图案成为可能,因此你可以在对象间建立关系。一个组对象的例子是,弹出式菜单的上拉和下拉。这个“菜单”组将包括图像、文本和按钮--组的所有子成员。

你可以把任何DisplayObject变成一个组的子对象。这个子对象被组织在一个数组里,所以第一个子成员(索引1)在下一个子成员的下面,以此类推;最后的子成员,始终在所有它的兄弟的最上面。你可以通过group:insert()插入一个对象到组中,并且可以通过整数索引(例如group[1])访问组的某一个成员。

local square = display.newRect( 0, 0, 100, 100 )

local rect = display.newRect( 0, 0, 100, 100 )

local group = display.newGroup()

group:insert( square )

group:insert( rect )

assert( (group[1] == square) and (group[2] == rect) )


Stage Objects

无论何时你创建一个新对象,都有隐式地加入到一个特别的组对象中,它是所有层级顶层。这个组对象,被成为stage object(通常表示当前的场景)。每次你创建一个DisplayObject,这个对象都会被隐式添加到这个stage对象里。默认情况下,它会添加那个对象到自己的子对象数组的末端,从而显示在所有子显示对象的最上面。(当前,Corona中只有一个stage,这个stage表示整个屏幕)。一旦一个对象被创建,它可以被使用as-is或者移动到另一个组。

向前向后移动对象

不象现实中的绘画,显示对象的次序不是一成不变的。你可以改变对象之间的相对次序。组对象中的子对象的次序,是由自对象数组的顺序决定的。使用 group:insert()对象方法,你可以重分配一个对象在父组中的位置。概念上说,你可以采用把对象再插入到同样的父组中的方法。

local square = display.newRect( 0, 0, 100, 100 ) -- Red square is

square:setFillColor( 255, 0, 0 ) -- at the bottom.

local circle = display.newCircle( 80, 120, 50 ) -- Green circle is

circle:setFillColor( 0, 255, 0 ) -- in the middle.

local rect = display.newRect( 0, 0, 100, 100 ) -- Blue rect is

rect:setFillColor( 0, 0, 255 ) -- at the top.



-- square,circle,rect all have same parent

local parent = square.parent



-- 移动到顶层,插到数组最后

parent:insert( square ) -- same as parent:insert( parent.length, square)



-- 移动到最前,插入到数组最前

parent:insert( 1, circle )


对象也可以通过使用 object:toBack 和 object:toFront 在组内进行移动。

绘制循环

基本的绘图模型包含一个循环,循环中执行lua代码、遍历渲染当前场景对象中的显示树上的渲染对象。在循环期内,之后当显示树上的对象改变时,屏幕才会更新。这些更新发生在添加、删除或修改子DisplayObject的属性时。

这个循环每秒会发生30-60次,次数依赖于在config.lua (参见 Frame rate control) 中的刷新率设置。在每次循环的开头,一个“enterFrame”事件会被分发到所有用lua编写的注册listener中。一旦所有的listener完成了执行,屏幕将会更新。

屏幕更新

当你的lua代码块执行时,屏幕完全没有更新。因此,如果你在一个代码块中(例如x坐标属性)多次修改一个display object,只有最后一次改变(x最后的值)将会反应到屏幕更新上去。

坐标和转换

s

坐标空间定义所有绘制发生的参考位置。这个屏幕代表了基本的坐标系统。所有的内容都会用相对于屏幕原点的位置。


通常,采用屏幕坐标形式描述所有东西,是不明智的。因此,我们介绍一下本地坐标的概念。每个显示对象运行在他们自己的本地坐标系当中。如果我们自己处理在显示对象本地坐标系和显示器坐标系之间的转换,将是一个很重的负担。


通过数学变换或转换操作,本地坐标系和屏幕坐标系的转换处理成为了可能。转换可以把坐标从一个空间转到另一个空间。


坐标

笛卡尔坐标系统(众所周知的标准坐标系统)被用于定义位置。和笛卡尔坐标系统不同的是,屏幕的原点位于左上角,所以正y坐标值向下(正x坐标值向右)。所有的屏幕坐标,都是相对于这个原点的。

本地坐标允许你操作显示对象的几何属性,例如更加符合直觉的旋转。每个显示对象都有一个原点,相对于它的父对象。这个原点本质上,定义了显示对象相对于其父对象的位置。例如,如果变量r是一个矩形,那么它的原点/本地位置,将是(r.xOrigin, r.yOrigin)。

当一个对象被创建,它的坐标是相对于当前场景(主屏幕)。为这个对象增加一个触摸监听器将返回触摸位置,这个位置对应于屏幕上对象的位置。当这个对象被添加到一个组里(非当前场景),对象的坐标是相对于这个组,而不是屏幕的。通过对象监听器返回的触摸位置不再对应于该被触摸对象的x和y属性。object.contentBounds API将会把这个对象的坐标映射回屏幕坐标系。

每个对象也有一个本地参考点,用于例如旋转转换发生。这个参考点,被定义为两个数字(在矩形r中,有r.xReference,r.yReference),指明了相对于于本地原点的参考点的位置。默认情况下,参考点和本地原点一样。通常参考点作为=(0,0)

改变对象的位置

为了改变显示对象的位置,你可以修改(xOrigin, yOrigin)属性或(x,y)属性 -通常你会选择后者,因为在缩放或旋转发生的时候它们是同一点。

(xReference,yReference)属性不会影响到一个显示对象的位置。相反参考点的位置,会影响到缩放和旋转。

当一个显示对象被创建, origin 处于该对象左上角。当对象创建完成之后,参考点处于对象的中心。有一个例外是display.newCircle,它将会创建一个中心 origin。

你通过下面的方法把参考点移回左上角:

object:setReferencePoint( display.TopLeftReferencePoint )

如果你读取参考点的x,y,它们的值是相对于Origin点的x和y值(对象的中心)。当参考点在中心的时候,参考点为(0,0)。当参考点在左上角,参考点坐标(-w/2,-h/2)(对象宽度和高度的一半)。

转换

通常,跟踪转换是一个非常容易出错而且需要技巧的活。这是因为几何转换的次序会决定对象的最终位置。例如,旋转一个对象,然后伸缩它(非对称)将会和先伸缩后旋转,得到完全不同的结果。

为了简化这件事,我们定义转换对象时的操作次序。这些操作都是相对于display object的参考点的。在这种方法里,你可以自由改变对象的位置,旋转它,以及伸缩之,并按照任何你想要的顺序,其结果一致。

要应用在几何体上的操作的转换,按照如下顺序进行计算:

1. 使用 ( object.xScale, object.yScale ),基于对象的参考点,伸缩显示对象

2. 围绕显示对象的参考点旋转object.rotation个角度.

3. 在本地坐标系里,通过 ( object.x, object.y ) ,相对父对象移动对象的origin (非参考点) 。

注:这些方法 object:scale(), object:rotate(), 和 object:translate() 只是改变了基本几何性质的值。你调用它们的顺序,完全不影响到最终的结果,所以你不应该把它们当成矩阵变换。

对象引用

因为在层级中对象可以被重新排序,所以通过整数索引来访问组的子对象是脆弱的。如果你移动一个子对象到其兄弟对象的前面,所有的整数索引都会被更新,一个简单的方案是把子显示对象存储为父组对象的属性,这将会让之后访问这些对象更加容易一点。

让我们考虑一种情况:在我们的太阳系里,我们有一个太阳和一些星球图片。我们想把它们都放在一个组的下面。在这个例子中,我们有一个table列出所有的文件名,我们创建一个组来存储我们的图片对象。

local planetFiles = { sun="sun.png", mercury="mercury.png",

venus="venus.png", earth="earth.png", mars="mars.png",

jupiter="jupiter.png", saturn="saturn.png", neptune="neptune.png",

uranus="uranus.png", pluto="pluto.png" }



local solarSystem = display.newGroup()


下一步就是,通过迭代整个planetFiles table来创建image对象。我们使用一个迭带器 ipairs来返回存储在 planetFiles中的属性名和存储在属性中的图片文件名。我们使用文件名来加载图片;我们使用属性名来获取table中的值比数组通过索引来获得更加有效。

-- Loop through all the files, load the image, assign property in the group

for key,file in pairs( planetFiles ) do

-- key will be "sun", "mercury", etc.

-- file will be "sun.png", "mercury.png", etc.

local planet = display.newImage( file )

solarSystem:insert( planet )

solarSystem[key] = planet

end



-- Afterwards:

-- solarSystem.sun will refer to the image object for "sun.png",

-- solarSystem.mercury will refer to the image object "mercury.png",

-- etc.

如果你想要删除那些对象中的一个,切记你需要做两件事:第一,需要从显示层次中删除这个。第二,你需要把父组对象对应的属性为nil。(见变量引用)

正确删除对象

因为设备只有有限的资源,当你不再需要display object的时候,把对象从显示层次中删除是非常重要的。这有助于降低内存消耗(尤其是图片),并且减少不必要的绘图。

当你创建一个display object,默认的它会被添加到显示层次的根对象上。这个对象就是众所周知叫做stage对象的特殊的组对象。

为了正确的删除一个对象,以让它不再继续在屏幕上渲染,你需要显式的从它的父对象中删除这个对象。这会从显示层次中删除这个对象。可以通过一下两种方法做到这一点:

image.parent:remove( image ) -- remove image from hierarchy

-- 或 --

image:removeSelf( ) -- same as above

然而,这并不总是可以释放掉图片所占用的内存。为了确保image对象被正确的垃圾搜集掉,我们还需要消除所有变量对它的引用,这我们将在下一节给出解释。

注意:用 object:removSelf删除一个组,将会删除这个组对象以及里面所有的子对象。这将会释放掉所有的对象监听器,和被显示对象使用的纹理对象,并把现实对象变量放入简单的lua table里。当没有其他引用指向这个变量的时候,lua将会垃圾回收这个lua变量。(参看下一节关于变量引用)

变量引用

即使一个显示对象被从显示层次中删除,该对象继续存在的情况也是有的。在我们上面的例子中,父组对象 solarSystem 保存了planet的image对象的引用,作为属性。所以即便从显示层次中删除一个image对象以后,我们还需要确保 solarSystem不再引用到这个image。为了做到这一点,我们把属性设置为nil(我们叫做nil out这个属性)。

local sun = solarSystem.sun

sun.parent:remove( sun ) -- remove image from hierarchy

solarSystem.sun = nil -- remove sun as a property of solarSystem

一般来说,如果你把显示对象作为table元素插入(例如table的属性或作为array元素),这个显示对象将会保持存在,即便它没有显示在屏幕上(参看对象引用)。你必须象上面的例子一样nil out这个属性。

同样,如果有其他变量指向该显示对象,那些对象就需要继续存在,显示对象就不能被释放。例如如果有一个全局变量指向一个显示对象,那么全局变量将永远不能被释放,就算它不在显示层次内它还是要继续存在下去。这里,当你真的不再需要它的时候,你应该把全局变量设置为nil。

另一个微妙的地方是,当一个函数引用到一个作用域之外的本地变量:

local sun = solarSystem.sun



function dimSun()

sun.alpha = 0.5 -- sun was declared outside the function block

end

在这种情况下,这个函数中的image对象还有一个外部引用。因为这个函数是全局的,image对象必须保持存在。有两个解决方案:让函数变成非全局(例如local)或改变这个函数使得没有函数块以外的变量被引用。后者更可取,也更普遍,因为它可以被应用于任何显示对象:

local sun = solarSystem.sun



function dim( object )

object.alpha = 0.5

end


常见缺陷

一个常见的错误是不正确的从组中删除所有对象。这通常发生在你要从一个组中遍历,尝试删除显示层次中每个子元素。因为遍历一般很自然的是在组中向前遍历,然而这可能导致没有结束的混乱。

继续我们前面太阳系的例子,考虑下面我们试图(错误)从太阳系中删除所有的对象。

for i=1,solarSystem.numChildren do

local child = solarSystem[i]

child.parent:remove( child )

end

这个问题在于我们正在遍历一个集合的时候,却修改它(例如组子数组)。结果是我们删除的是其他子元素。用一个包含整数数组的例子,可以很容易说明这个问题:

local array = {1,2,3,4,5,6,7,8,9,10}

print( table.concat( array, " " ) ) --> 1 2 3 4 5 6 7 8 9 10



for i=1,#array do

table.remove( array, i ) -->i=2时,删的是3,原来2变成第一个元素了。

end



print( table.concat( array, " " ) ) --> 2 4 6 8 10

改成向后遍历:

for i=solarSystem.numChildren,1,-1 do

local child = solarSystem[i]

child.parent:remove( child )

end

当然,这只能确保所有的元素被从显示层次中删除;你仍然需要把所有这些显示对象的引用设置为nil。所以在这个例子中,我们只是想特别说明一下,同时遍历和修改组的子对象的缺陷。一个好的实现也要负责把 solarSystem中的属性设置为nil,以正常释放。
<< 五分钟学会Corona(四) - Images, Shapes, 和Text五分钟学会Corona(六) - Bitmap Mask >>

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

最近发表

Powered By Z-Blog 1.8 Walle Build 100427 Copyright 2011-2015 BuildApp.Net. All Rights Reserved.