第四話 群體控制

第四話 群體控制

各位學員,又到了上課的時間拉。真是非常的抱歉,因為最近忙著參加十二夜App遊戲開發大賽,導致開課時間延誤了一些,請大家原諒老師。

(望向教室後排空蕩蕩的課桌椅,好像大家都翹光了,沒差 =.=)

咳~咳~ ,哪怕只有三個個學員來聽課,我還是要保持最大的熱誠來教課的。^^

回到課程,今天我們來討論一個很有用的課題好嗎,就是群體控制!

想像一下,我們為何要學習程式開發呢?因為我們很聰明(我絕對不會說其實是因為我們很懶,懶得動),懂得讓電腦來幫我們做那些無聊又重複的工作。

因此,當我們想要對一群元件做相同的操作時,例如想要將一堆圖文元件都向右移動一點或者是隱藏起來。你當然可以一個一個去指定,但是如何透過一個操作就完成,這就是群體控制所要探討的作法啦。

阿福:這太簡單了,只要透過For迴圈搭配陣列就可以一次對大量元件做操作了啊,程式也不會變很多。

大雄:迴圈那是什麼?陣列?老師好像都沒教過呀。

(這時候身為助教的小叮噹似乎是發現了什麼,興奮地跳了出來)

小叮噹:For迴圈我知道,我來我來。所謂的迴圈就是被用來執行大量的重複性事務。基本區分為有指定次數和無指定次數兩種。所謂有指定次數就像是印出十行hello,world這類的工作就是,而無指定次數則是一直執行直到某種條件發生才結束,事前開發者的我們並不知道應該要執行幾次喔。

大雄:那lua的迴圈要怎麼寫勒?

小叮噹:我用下圖來解釋一下吧,下面是指定次數迴圈的寫法。會搭配一個指標變數來使用,這個i是一個區域變數,不一定要用i,也可以自己取(PS:不過用i是慣例,如果為二維或三維迴圈就會命名到j.k等等)。像下面這個例子,第一次會印出i:1,結束時i+2,第二次就印出i:3,接著i再加+2,第三次印出i:5,最後要再加2之前檢查發現已經超過5了,因此迴圈就結束啦。

大雄:喔喔,好像很厲害。那陣列又是啥呢?

小叮噹:這個嘛~

(望向大家似乎好像有點陷入困惑中...,我決定收回發言權)

Lua裏頭的陣列是用table來實作的,它是Lua裡面唯一也最好用的一個資料結構,因為要說的內容需要整整一堂課的時間,因此還是讓我們之後再來細細說明吧。

說回群體控制,阿福所說的迴圈加陣列的確是一種解決的思路,不過卻還是有不少缺點。比如每次都要寫一個迴圈造成程式碼看起來顯得複雜;再來像是所有元件要根據使用者的手指來加以移動的話,每次手指頭一動就要跑一次迴圈,效率上也顯得不是太好...

宜靜:Zack老師,你的意思是,應該有方法可以將這些元件放進一個類似容器的東西,之後直接控制容器就等於控制所有元件,而不需要透過迴圈是嗎?

不愧是班上最聰明的宜靜(望向又進入冥想狀態的技安...),一下就猜出我的解決方案,我要介紹的group就是這樣的一個東西。之前介紹過的ImageRect和Text都是顯示元件(Display Object),而group也是Display Object的一種。

它跟座標系統跟之前相同,向右向下為正

group這個Display Object所不同在於它沒有邊界(因此你不需要去指定它的width.height),且可以裝入其他Display Object。你不妨將其想成是一張圖畫紙,可以在上面畫圖案(ImageRect)或者是寫字(Text)。當你移動這張圖畫紙時,所有的圖案跟文字也跟著移動相同的距離;而當你將整張圖畫紙給蓋起來的時候,所有元件也都消失囉。

比如上圖,當設定group的alpha為0.5(也就是一半)時,所有元件都變半透明了

在之前的課程當中,雖然你們都沒寫過group,但是其實group早就存在於你們的程式當中,因為有Stage。

Corona的SDK當中,有一個舞台(Stage)的概念,所有元件都要在舞台中才得以顯現,而它其實就是一個group,也是最上層的group,不需要你建立就已經存在,你可以透過display.currentStage來得到它。

就像是之前學到的Display Object都有原點(Original Point),或稱為錨點(Anchor Point),group也有。但是之前的Object圓點預設都在中心點對吧,而group則是在所包含內容範圍的左上角。

用一個例子來說明會比較清楚,黑色部分是我們生成的myGroup元件,而紅色方塊則是另一個長方形元件(同樣也是Display Object)。注意看可以發現,紅色方塊的原點座標為100,100,也就是用藍線交叉的地方,指向的就是正中央。而myGroup的原點座標0,0,則是橘色線交叉的地方,指向在左上角。


local myGroup = display.newGroup()
local myBox = display.newRect( 100, 100, 80, 80 )
myBox:setFillColor( 1, 0, 0, 0.8 )
myGroup:insert( myBox )

技安,你來給老師說說,為什麼紅色方塊會位在100,100的座標上面?

(技安睡到口水在桌面上淹成一座小湖,聽到我叫他,突然驚醒,結果攪亂了桌面上的一湖口水)

技安:那...那是因為紅色方塊設定的座標就是100,100啊,所以就放在全域座標點100,100的位置上啊

錯很大!技安你給我站著上課,不然等會恐怕會發生海嘯了...

各位同學,這裏有個很重要的細節要注意,當某個元件(Display Object)被放進group之後,其所擺放位置將不再是其原點座標相對於全域座標系統的位置,而是相對於Group座標系統的位置。只是因為目前的myGroup位置為0,0,所以才會跟全域座標點100,100相同。

事實上,從更高的角度看,剛才提過的Stage本身就是一個group,當我們新建立一個圖文元件卻沒有將其放入我們所建立的group當中時,其實它也是被放入Stage這個group,我們所謂的全域座標系統其實也就是相對於Stage這個座標系統,所以我可以說所有元件都是受到group的座標系統所影響。

就像可以對圖片設定x.y座標來變更位置,因為group也是Display Oject的關係,我們當然也能對group做相同的操作,結果是group和其所包含的元件都跟著移動啦~~~ 


myGroup.x = 50
myGroup.y = 50

現在看起來,雖然紅色方塊的原點座標為100,100,但是全域座標顯然不是,那麼我們要如何知道其對應的全域座標點為多少?很簡單...

呼叫localToContent( x, y )函式,透過可將子元件的相對x.y座標值,變更為全域座標值。比方說,把x.y換作是0,0,就可以將原點的區域座標轉換為對應的全域座標囉,再利用取得多個回傳值的技巧來得到座標值。

大雄:Zack老師,一般程式不是都有回傳值只能有一個的限制嗎?

沒錯,但是lua卻是個例外,它是少數支援多回傳值程式的一種,我們可以利用以下的語法來得到多個回傳值喔!

local actual_x , actual_y = btn:localToContent( 0, 0 )

group身為Display Object,當然也有width和height,為了方便說明先讓我們再加一個藍色方塊上去吧。 


local myGroup = display.newGroup()
local myBox = display.newRect( 100, 100, 80, 80 )
myBox:setFillColor( 1, 0, 0, 0.8 )
myGroup:insert( myBox )

myGroup.x = 50
myGroup.y = 50

--Add a smaller blue box
local blueBox = display.newRect( 50, 50, 50, 50 )
blueBox:setFillColor( 0, 0, 1, 0.8 )
myGroup:insert( blueBox )

之前有說過group是沒有邊界的,因此概念上不希望你去設定其寬高(但是其實你可以這麼做=.=),不過其實它同樣有寬跟高的,請看下圖灰色的部分就是myGroyp現在的寬高範圍,也就是能包含所有子元件的最小範圍。

設定myGroup的錨點之後,發現好像沒有變化,這是因為group有個anchorChildren屬性,用來設定group的範圍是否要依附在錨點上,其預設值為false


myGroup.anchorX = 0
myGroup.anchorY = 0

將group的anchorChildren屬性設為true,有沒發現灰色區域現在就靠在了錨點上,而子元件的位置也跟著產生了變化。

有趣的是,還記得之前紅色方塊所指定的x座標為100,可是目前看起來它的群組的位置相對於group錨點的位置不到100,這表示錨點的優先級是高於座標指定的。 

myGroup.anchorChildren = true

現在,試看看把myGroup的x座標設為150試看看,可以發現所有元件都往右移動了。

myGroup.x = 150

當然,我們也可以變更錨點,將其設定為右上角,也就是將anchorX設為1,透過這樣的方式同樣也是達到了讓所屬子元件移動的目的。


myGroup.anchorX = 1
myGroup.anchorY = 0

接著來聊聊前後景的問題,圖文元件有先建立的先畫因此排後面的特性,group當然也不例外,因此在製作遊戲時通常會有前中後三景的呈現需求,我們可以這樣做如下,先把三個景的Group依照順序生成出來,接下來再根據圖文元件需於那一層再分別的放進對應Group即可。


local farBackground = display.newGroup()
local nearBackground = display.newGroup()  --this will overlay 'farBackground'
local foreground = display.newGroup()  --and this will overlay 'nearBackground'

如果需要清除一個圖文元件,我們一般是用removeSelf()這個function,group也是相同的作法,唯一需要說明的是當呼叫這個function時,其子元件也會一併被清除,有時我們也可利用這個特性來清除大量的元件。不過話說回來,清除後再新增是比較耗效能的做法,因此如果某些元件需要重複使用的話,也許使用pool設計模式會是較好的做法,這又是一個很大的Topic,將來有機會再說。現在同學們,盡量先用隱藏來取代清除的作法來做即可。

理論說的這麼多,小叮噹顯然有些無聊(他正在把手放進口袋中,不知道正在找啥道具..)

那麼,就請小叮噹寫個範例來總結今天所教授的東西吧...(他眼睛一亮,似乎很激動..) 


display.setStatusBar( display.HiddenStatusBar )
-- 載入widget Library
local widget = require ("widget")
--=======================================================================================
--宣告各種變數
local numOfBtn = 5
local startOfX = 0
local startOfY = 10
local distanceBetweenBtns = 70
local btns

local initial
--=======================================================================================
--宣告各種全域變數
_SCREEN = {
    WIDTH = display.contentWidth,
    HEIGHT = display.contentHeight
}
_SCREEN.CENTER = {
    X = display.contentCenterX,
    Y = display.contentCenterY
}
--=======================================================================================
--宣告各種區域變數
local numOfBtn = 5
local startOfX = 40
local startOfY = 40
local distanceBetweenBtns = 70
local btns
--宣告各種區域函式
local initial
--=======================================================================================
--宣告與定義main()函式
--=======================================================================================
local main = function (  )
    --生成一個新的子Group
    -- 建立一個子群組
    btns = display.newGroup( )
    initial(btns)
end
--=======================================================================================
--定義其他函式
--=======================================================================================
--初始化函式,一般用於建立元件,設定初始值,部署元件等工作...
initial = function (group)
    group.x = startOfX
    group.y = startOfY

    --Group同樣可以設錨點,但需要使用anchorChildren來觸發
    group.anchorX = 0
    group.anchorY = 0
    group.anchorChildren = true
    for i=1,numOfBtn do
        local btn = widget.newButton{
            width = 120,
            height = 60,
            defaultFile = "images/BackBtn.png",
            overFile = "images/BackBtnPressed.png",
        }
        btn.anchorX = 0
        btn.anchorY = 0
        btn.x = 0
        btn.y = (i - 1) * distanceBetweenBtns

        --將生成的按鈕放進group群組當中
        group:insert( btn )

        --取得群組的子元件的全域座標
        local actual_x , actual_y = btn:localToContent( 0, 0 )
        print( 'actual_x:' .. actual_x ..",actual_y:" .. actual_y )
    end

     --逐一取出群組裡的子元件,先進先出
    for i=1,group.numChildren do
        print('btn y:' .. group[i].y)
    end
end
--=======================================================================================
--呼叫主函式
--=======================================================================================
-- 先完成所有的宣告以及賦值,最後才執行main方法啟動工作
main()

在main方法裡,我們先是創建了一個group群組,命名為btns,接著將其傳入初始化函式initial裡頭。接著在initial函式內設定了原點座標.錨點.開啟anchorChildren屬性,再接著用迴圈生成多個按鈕並將其加入btns群組中。

PS:如果想要了解某個群組裡頭有多少個子元件,可以透過其numChildren來得到,而不是用#。至於要取得某個元件的話,就使用btns[index]即可。

(技安站著似乎急著想要下課,都開始做起體操了)

喔喔,One More Thing,還有一件事。其實還有一個容器很group很類似,事實上他本來就是一種特殊的group,他的名字叫做container。

container和group使用上幾乎大同小異,主要差別在於group是無邊界的但是container則有,而group的anchorChildren屬性預設為false,而container則是為true,詳細比較見下圖。

大雄:Zack老師,所謂的遮罩功能是啥意思勒?

就是只顯示部分內容的功能之意,沒有顯示的部分被遮住了,因此稱為遮罩,如果還是不理解,請看下圖...

那麼,今天這堂課就用Container作為結尾吧,container的原始碼我就不再說明,放在附件作為回家功課請同學回去研究囉。

小叮噹,我們今天上到這裡。

小叮噹:起立,敬禮,謝謝Zack老師!

分享這篇文章:

發佈自 林品爵

林品爵
一個堅信程式教育能夠改變下一代未來的狂熱者,青少年揚帆計畫的推動者。十年以上全端程式開發,上百場程式教學經驗,累積學生數達百人以上。 成大學士畢業,使用過J2EE.PHP.MySQL.CoronaSDK.Android.Objective-C.Swift.Python等技術知識,歷任友訊科技總部Java工程師首席.黑快馬股份有限公司研發經理.諾亞數碼娛樂執行長等職位,現任哥布林程式教育學苑創辦人,所開發過專案涵括產品管理系統.POS系統.拍賣平台.擴增實境.問券.社區等等。因觀察到學員英文不好產生挫折發展出布林程式教育系統。

關聯文章:

留下留言