本章寫做思路:編程
說明幀動畫的原理->取得素材->把素材整個顯示->截取子畫面->顯示子畫面->討論在循環顯示函數 draw() 中讓子畫面逐幀顯示->挖空素材背景(背景透明化)->讓角色再也不原地踏步(橫向或縱向跑起來)->增長背景圖->放大或者縮小角色(跑着跑着變瘦了-瘦身減肥效果)->背景圖動起來->改變角色角度數組
幀動畫是一種應用很是普遍的動畫技術,如今我打算藉助 Codea
很是友好的編程界面用最簡單的例子一步步教你學會幀動畫。網絡
看過網絡上很多關於幀動畫的教程, 大可能是經過引用這個類那個庫來實現的, 看起來很複雜的樣子, 不多有用基本繪圖語句直接寫的, 對於初學者來講, 理解幀動畫還得先去理解那些類庫, 無形中增長了學習難度, 本文嘗試一種新的講解方式, 所有采用 Codea
基本繪圖語句來演示幀動畫原理.數據結構
首先介紹一下幀動畫的原理: 簡單說就是把全部的動畫幀都集中放在一個圖片上(好處是能夠一次性加載到內存裏), 而後依次顯示每一幀, 這樣連續顯示起來的動畫幀利用視覺暫留效應就成功地實現了幀動畫.框架
具體來講須要這樣一個圖:函數
另外須要的就是每個子幀在整副圖片中的位置座標和長寬, 通常會用左下角座標和長度,寬度來肯定一個子幀, 咱們會用這樣一個表來存儲:學習
positon = {{x1,y,width,height}, {x2,y,width,height}, ... }
在這個數據結構中, 每副子幀的縱座標 y
, 寬度 width
, 高度 height
均可以保持同樣的值, 這樣處理起來最省事.測試
這個素材圖是每 3
副子幀完成一個動畫, 它的座標數據以下:優化
local x,y = 0,43 pos1 = {{0+x,0+y,32,43},{64+x,0+y,32,43},{96+x,0+y,32,43}}
不過有些幀動畫素材, 爲了節省那麼一丁點空間, 會根據子幀中角色的實際寬度來放置, 這樣就使得每副子幀的 寬度 width
不必定相同, 好比下面這個素材:動畫
它的座標數據就是這樣的:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}}
再看一張大貓圖:
它的座標數據就是這樣的了:
local w,h = 1024,1024 pos2 = {{0,h*3/4,w/2,h/4},{w/2,h*3/4,w/2,h/4},{0,h*2/4,w/2,h/4},{0,h*2/4,w/2,h/4}, {0,h*1/4,w/2,h/4},{w/2,h*1/4,w/2,h/4},{0,h*0/4,w/2,h/4},{0,h*0/4,w/2,h/4}}
明白了上述原理就好辦了.
首先,咱們準備好幀動畫素材,接着把素材讀入內存,而後試着把它直接顯示到屏幕上--記住,sprite
命令是後續幀動畫的基礎,代碼以下
function setup() displayMode(FULLSCREEN) -- 繪圖模式選 CENTER 能夠保證畫面中的動畫角色不會左右漂移 rectMode(CENTER) spriteMode(CENTER) -- 加載整副素材圖 img = readImage("Documents:runner") end function draw() sprite(img, x, y) end
函數說明:
參數說明:
函數說明:
參數說明:
更多的函數說明文檔請參考:
很好, 如今咱們從素材中取出第一個子畫面, 也就是左下角座標爲(0,0)
, 寬爲 110
, 高爲 120
的區域:
img1 = img:copy(0,0,110,120)
如今在 draw()
函數裏增長以下語句把它顯示到屏幕上
sprite(img1, x, y)
很是好,接下來咱們按照從左到右的順序依次取出各個子畫面,它們的座標能夠根據素材圖像的大小進行估算,好比第二個子畫面它的左下角座標的 x 值要在第一個子畫面左下角座標 x 值的基礎上加上第一個子畫面的寬度 110, 高度不變, 它本身的寬度咱們能夠大體估算爲70(根據實際狀況調整), 那麼以下
function setup() ... img2 = omg:copy(110, 0, 70, 120) ... end function draw() ... sprite(img2, x, y) ... end
有了這兩個基礎, 咱們就知道怎麼處理剩下的子畫面了, 爲了後續的操做方便,咱們把全部這些子畫面按順序放到一個表中
—- 新建一個空表 images = {} -- 分離各個子畫面,按順序存入表 imgs 中備用 imgs[1]=img:copy(0,0,110,120) imgs[2]=img:copy(110,0,70,120) imgs[3]=img:copy(180,0,70,120) imgs[4]=img:copy(250,0,70,120) imgs[5]=img:copy(320,0,105,120) imgs[6]=img:copy(423,0,80,120) imgs[7]=img:copy(500,0,70,120) imgs[8]=img:copy(570,0,70,120)
而後試着在屏幕上單獨顯示一個子畫面,檢驗一下咱們是否成功地從素材中取得了全部的子畫面, 代碼以下:
function draw() ... -- 分別顯示全部子畫面 sprite(imgs[1],120,200) sprite(imgs[2],130,400) sprite(imgs[3],120,600) sprite(imgs[4],300,600) sprite(imgs[5],320,400) sprite(imgs[6],330,200) sprite(imgs[7],620,200) sprite(imgs[8],630,400) end
接下來就是關鍵 ,把全部這些子畫面連續顯示到屏幕上, Codea
的 draw()
函數每秒鐘會自動執行60
次, 也就是說它每 1/60
秒(0.01666...秒)會在屏幕上繪製一次.
那麼咱們只要在同一個位置每隔一點時間按照子幀的順序顯示一個新的子幀, 就能夠造成動畫效果.
這裏咱們使用一個全局變量 q
來索引 imgs
中的子畫面, 首先在 setup()
函數中爲 q
賦初值 0
q =0
接着在 draw()
函數中設置爲遞增, 每執行一次 draw()
, q
就會加 1
q= q+1
而後在 draw()
中依次顯示 imgs[q]
,
sprite(imgs[q],120,200)
這時點擊運行就會出現一個錯誤, 提示數組越界, 怎麼回事呢?
由於咱們的 imgs
只有 8
個元素, 一旦 q
的值超過 8
就索引不到數據了, 也就是說咱們須要讓 imgs
的索引值保持在 1-8
的區間內.
解決辦法就是用取模函數來限定數組索引的範圍, 以下:
math.fmod(q, 8)
隨着 q
的不斷遞增, 它返回的值依次爲
0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7,0,1,2,3,4,5,6,7 …..
, 咱們只要給它加 1
就能保證正好索引到1到8之間, 試着運行一下:
sprite(imgs[math.fmod(q, 8)+1],120,200)
此次沒錯了, 不過新的問題出現了, 好像跑得有些快, 如何解決呢?
這是由於咱們的 draw()
默認每秒執行60次, 那麼咱們如今的子幀有 8
副圖, 按照咱們上面的代碼, 沒執行一次 draw()
就會繪製一幅子幀, 也就是說咱們的全部子幀會在 8/60
秒內就播放完.
如今咱們但願能調整一下幀速率, 須要用到 Codea
提供的一個全局變量(只讀) ElapsedTime
, 這個全局變量會實時返回程序執行時間, 咱們用這樣一個判斷來實現:
用一個 prevTime
記錄上一次執行時間, 在 setup()
中初始化(這時記錄的是第一次執行的時間)
prevTime =0
假設咱們但願子畫面每隔 0.1
秒更新一次, 也就是每隔 0.1
秒, imgs
的索引值增長1
, 在 draw()
裏增長這段代碼:
if ElapsedTime > prevTime + 0.1 then prevTime = prevTime + 0.1 k=math.fmod(i,8) i=i+1 end sprite(imgs[k+1],120,200)
看看如今的效果, 很是好, 如今的動畫速度差很少是原來的 1/6
, 並且這個值能夠根據須要進行調整.
首先是這段代碼:
— 新建一個空表 images = {} -- 分離各個子畫面,按順序存入表 imgs 中備用 imgs[1]=img:copy(0,0,110,120) imgs[2]=img:copy(110,0,70,120) imgs[3]=img:copy(180,0,70,120) imgs[4]=img:copy(250,0,70,120) imgs[5]=img:copy(320,0,105,120) imgs[6]=img:copy(423,0,80,120) imgs[7]=img:copy(500,0,70,120) imgs[8]=img:copy(570,0,70,120)
咱們能夠把位置座標提取出來集中放置到一個表中, 而後用循環來表示, 以下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}} for i = 1, 8 do imgs[i]=img:copy(pos[i][1], pos[i][2], pos[i][3], pos[i][4]) end
繼續用一個 table.unpack
語句來替換 pos[i][1], pos[i][2], pos[i][3], pos[i][4]
語句, 以下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}} for i = 1, 8 do imgs[i] = img:copy(table.unpack(pos[i])) end
不過不一樣的動畫素材使用的子幀數目也不必定都是 8
個, 因此這裏這個子幀數目能夠經過 #pos
(求 pos
的長度) 來靈活設置, 因此代碼以下:
pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}} for i = 1, #pos do imgs[i] = img:copy(table.unpack(pos[i])) end
爲了方便使用, 另外一方面也讓程序主框架看起來清爽一些, 咱們能夠把上述實現幀動畫的代碼封裝成一個類 Sprites
, 具體來講就是把初始化的代碼放在 Sprites:init()
函數中, 把實際繪製的代碼放在 Sprites:draw()
函數中, 代碼以下:
Sprites = class() function Sprites:init(x,y,img,pos) self.x = x self.y = y self.index = 1 self.img = img self.imgs = {} self.pos = pos self.i=0 self.k=1 self.q=0 self.prevTime =0 -- 使用循環,把各個子幀存入表中 for i=1,#self.imgs do -- imgs[i] = img:copy(startPos[i][1],startPos[i][2],startPos[i][3],startPos[i][4]) self.imgs[i] = self.img:copy(table.unpack(self.pos[i])) end print(#self.imgs) end function Sprites:draw() -- 肯定每幀子畫面在屏幕上停留的時間 if ElapsedTime > self.prevTime + 0.1 then self.prevTime = self.prevTime + 0.1 self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1 end self.q=self.q+1 -- rect(800,500,120,120) pushMatrix() rotate(30) -- sprite(self.imgs[self.k+1],self.i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) --sprite(imgs[math.fmod(q,8)+1],i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) sprite(self.imgs[self.k+1], self.x, self.y) popMatrix() -- sprite(imgs[self.index], self.x, self.y) end
使用方法也很簡單, 先在 setup()
中調用初始化函數, 而後在 draw()
中調用繪製函數:
function setup() displayMode(FULLSCREEN) -- 繪圖模式選 CENTER 能夠保證畫面中的動畫角色不會左右漂移 rectMode(CENTER) spriteMode(CENTER) fill(249, 249, 249, 255) imgs = {} pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}} img = readImage("Documents:runner") img1 = readImage("Documents:cats") pos1 = {{0,0,32,43},{0,0,64,43},{0,0,96,43}} -- 初始化 m = Sprites(600,400,img,startPos) -- m1 = Sprites(800,400,img1,pos1) end function draw() background(39, 44, 39, 255) m:draw() -- m1:draw() end
最新版本的代碼
-- 幀動畫對象類 Sprites = class() function Sprites:init(x,y,img,pos) self.x = x self.y = y -- self.index = 1 self.img = img self.imgs = {} self.pos = pos self.i=0 self.k=1 self.q=0 self.prevTime =0 -- 處理原圖,背景色變爲透明 self:deal() -- 使用循環,把各個子幀存入表中 for i=1,#self.pos do -- imgs[i] = img:copy(pos[i][1],pos[i][2],pos[i][3],pos[i][4]) self.imgs[i] = self.img:copy(table.unpack(self.pos[i])) end end function Sprites:deal() ---[[ 對原圖進行預處理,把背景修改成透明,現存問題:角色內部有白色也會被去掉 local v = 255 for x=1,self.img.width do for y =1, self.img.height do -- 取出全部像素的顏色值 local r,g,b,a = self.img:get(x,y) -- if r >= v and g >= v and b >= v then if r == v and g == v and b == v and a == v then self.img:set(x,y,r,g,b,0) end end end --]] end function Sprites:draw() -- 肯定每幀子畫面在屏幕上停留的時間 if ElapsedTime > self.prevTime + 0.08 then self.prevTime = self.prevTime + 0.08 self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1 end self.q=self.q+1 -- rect(800,500,120,120) pushMatrix() -- rotate(30) -- sprite(self.imgs[self.k+1],self.i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) --sprite(imgs[math.fmod(q,8)+1],i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) sprite(self.imgs[self.k+1], self.x, self.y,50,50) popMatrix() -- sprite(imgs[self.index], self.x, self.y) end -- Main function setup() displayMode(FULLSCREEN) -- 繪圖模式選 CENTER 能夠保證畫面中的動畫角色不會左右漂移 rectMode(CENTER) spriteMode(CENTER) fill(249, 249, 249, 255) imgs = {} pos = {{0,0,110,120},{110,0,70,120},{180,0,70,120},{250,0,70,120}, {320,0,105,120},{423,0,80,120},{500,0,70,120},{570,0,70,120}} img = readImage("Documents:runner") img1 = readImage("Documents:cats") local x,y = 128,43 pos1 = {{0+x,0+y,32,43},{64+x,0+y,32,43},{96+x,0+y,32,43}} img2 = readImage("Documents:catRunning") local w,h = 1024,1024 pos2 = {{0,h*3/4,w/2,h/4},{w/2,h*3/4,w/2,h/4},{0,h*2/4,w/2,h/4},{0,h*2/4,w/2,h/4}, {0,h*1/4,w/2,h/4},{w/2,h*1/4,w/2,h/4},{0,h*0/4,w/2,h/4},{0,h*0/4,w/2,h/4}} m = Sprites(600,400,img,pos) m1 = Sprites(500,400,img1,pos1) m2 = Sprites(500,200,img2,pos2) end function draw() background(39, 44, 39, 255) m:draw() m1:draw() m2:draw() end
幀動畫是一種應用場景很是普遍的基礎遊戲開發技術, 遊戲角色的大多數動做都是經過幀動畫來實現的, 例如角色平時的移動, 無聊時的各類小動做, 以及戰鬥時的各類技能釋放, 因此作遊戲開發必定要完全理解幀動畫的原理和實現, 這樣才能駕輕就熟地把它運用在開發中.
到目前爲止, 咱們的角色幀動畫已經作好了, 不過看起來不是很協調, 尤爲是動畫角色頂着一個白色矩形框, 這是由於素材沒有采用透明背景, 因此看起來感受不太好.
不過既然背景是單一的白色, 那麼咱們爲何不在動畫顯示前把它作一個預處理? 把它的白色背景改成透明? 這裏普及一下, 通常圖片素材都有4個顏色通道, 分別爲: r, g, b, a
, 前三個分別爲紅色
, 綠色
, 藍色
, 第四個 a
就是透明度, 它們的取值範圍都是 0~255
, 對於透明度來講, 0
表示透明, 255
表示不透明, 中間的值表示不一樣程度的透明.
那麼咱們的思路很簡單, 把每一個子畫面的每一個像素點都取出來,判斷它是否是白色(白色的r,g,b,a值分別爲255), 若是是, 咱們就認爲它是白色背景, 把它的 a
置爲 0
,而後寫回到原位置去, 若是不是背景則不作處理, 具體代碼在這裏:
--[[ 對每一個子畫面進行預處理,把背景修改成透明 for i=1,8 do for x=1,imgs[i].width do for y =1, imgs[i].height do r,g,b,a = imgs[i]:get(x,y) if r == 255 and g == 255 and b == 255 then imgs[i]:set(x,y,r,g,b,0) end end end end --]]
等等, 爲何不直接對整個素材圖像進行處理呢, 這樣還能夠少一個循環, 以下:
---[[ 對原圖進行預處理,把背景修改成透明 for x=1,img.width do for y =1, img.height do -- 取出全部像素的顏色值 r,g,b,a = img:get(x,y) -- if r >= 205 and g >= 205 and b >= 205 then if r == 255 and g == 255 and b == 255 then img:set(x,y,r,g,b,0) end end end --]]
這兩段代碼最終效果是同樣的, 不事後一種效率更高, 由於它集中處理整副素材圖, 少了一重循環.
由於這段代碼只須要執行一次便可, 因此咱們把它放在 setup()
函數中, 看看效果, 果真感受好多了, 雖然邊緣部分看起來清除得不是那麼好.
最終咱們會把這個函數整合到幀動畫類中, 做爲一個方法, 代碼以下:
function Sprites:deal() ---[[ 對原圖進行預處理,把背景修改成透明 for x=1,self.img.width do for y =1, self.img.height do -- 取出全部像素的顏色值 local r,g,b,a = self.img:get(x,y) -- if r >= 205 and g >= 205 and b >= 205 then if r == 255 and g == 255 and b == 255 then self.img:set(x,y,r,g,b,0) end end end end
能夠選擇把這個方法放在 Sprites:init()
中調用,
提醒
: 若是但願獲得更好的圖形效果, 能夠用修圖軟件手動修改素材, 這是隻是介紹一種簡單的用代碼處理圖像的思路, 並且正式的遊戲開發老是把能提早處理的步驟都儘可能提早處理, 實在沒辦法處理的才用代碼解決.如今的效果看起來是否是又有了一些改進? 沒錯, 好的軟件就是從一點一滴的細節改善中作出來的.
不過感受仍是有點美中不足, 首先黑黑的背景有些影響觀感, 那麼咱們增長一個非洲大草原的背景圖, 代碼以下:
sprite("Documents:bgGrass",(WIDTH/2),HEIGHT/2)
效果貌似稍微好了點, 感受仍是不太對, 如今雖然看起來角色雖然在跑,但是老是在原地踏步, 遊戲中的角色不能一直原地踏步不移動啊! 怎麼辦?
兩個辦法:
先說第一種, 很簡單, 修改這條顯示背景圖片的語句:
sprite("Documents:bgGrass",(WIDTH/2),HEIGHT/2)
讓它的 x 座標遞減便可, 剛好咱們有一個遞增的變量 i 能夠直接拿來用
sprite("Documents:bgGrass",(WIDTH/2-10*i),HEIGHT/2)
咱們發現背景動是動起來了, 但是隻要超過座標範圍就沒了, 看來還得增長相關處理, 先分析一下出現這種狀況的緣由, 由於咱們的背景圖的大小是 1024*768
, 因此一旦左右移動背景超過了這個長寬範圍,就沒有圖像了,所以,咱們能夠有這麼幾種解決思路:
0,0
爲左下角起點,另外一個以 1024,0
爲左下角起點(由於是左右移動,因此縱座標不須要修改),以下圖所示:這樣還有個問題,假設整個背景圖向左移動,那麼當移動到最右邊時,還會出現沒有圖像的狀況,這時咱們可使用一種前面用過的技術,那就是對橫座標取模,讓它們始終落在 0~1024
這個區間內, math.fmod(x,1024)
, 其中橫座標 x 是一個遞增的量.
這樣一來, 角色就能夠朝各個方向移動了, 現階段爲方便測試, 咱們能夠這麼設定:
要實現這個設定, 須要咱們在 setup
中增長一個全局變量 s
, 在 draw()
中增長繪製全屏背景圖的代碼, 在 touched
函數中增長一段代碼, 以下:
function setup() displayMode(OVERLAY) myStatus = Status() -- 如下爲幀動畫代碼 s = -1 ... end function draw() pushMatrix() pushStyle() -- spriteMode(CORNER) rectMode(CORNER) background(32, 29, 29, 255) -- 增長移動的背景圖: + 爲右移,- 爲左移 sprite("Documents:bgGrass",(WIDTH/2+10 * s * m.i)%(WIDTH),HEIGHT/2) sprite("Documents:bgGrass",(WIDTH+10 * s * m.i)%(WIDTH),HEIGHT/2) ... end function touched(touch) -- 用於測試修煉 if touch.x > WIDTH/2 and touch.state == ENDED then myStatus:update() end -- 用於測試移動方向:點擊左側向右平移,點擊右側向左平移 if touch.x > WIDTH/2 and touch.state == ENDED then s = -1 elseif touch.x < WIDTH/2 then s = 1 end end
如今遊戲還不完整, 因此咱們才經過一些設置好的變量來大體控制角色的移動--僅用於測試模塊功能, 等後面控制系統完成, 咱們會寫一些觸摸函數, 用它們來設置這些變量(m.i
, s
), 這樣咱們就能夠經過觸摸來精確控制角色的移動了.
再說第二種: 讓角色平行移動
明白了第一種讓背景平移的方法後, 第二種讓角色平移的方法就更容易理解了, 也就是在Sprites:draw()
函數中動態修改角色繪製語句
sprite(self.imgs[self.k+1], self.x, self.y,50,50)`
的 self.x
值, 結合第一種方法的具體實現, 基本上就是把 self.x
模仿 self.i
的處理方式處理一下就能夠了, 具體以下:
function Sprites:draw() -- 肯定每幀子畫面在屏幕上停留的時間 if ElapsedTime > self.prevTime + 0.08 then self.prevTime = self.prevTime + 0.08 self.k = math.fmod(self.i,#self.imgs) self.i = self.i + 1 self.x = self.x + s end ... sprite(self.imgs[self.k+1], self.x, self.y,50,50) end
其餘都沒必要變(固然爲了效果更明顯, 能夠把背景圖顯示改成固定位置繪製), 修改後代碼以下:
-- 增長移動的背景圖: + 爲右移,- 爲左移 -- sprite("Documents:bgGrass",(WIDTH/2+10 * s * m.i)%(WIDTH),HEIGHT/2) -- sprite("Documents:bgGrass",(WIDTH+10 * s * m.i)%(WIDTH),HEIGHT/2) sprite("Documents:bgGrass",WIDTH/2,HEIGHT/2)
這兩種方式各有利弊, 咱們能夠根據實際須要進行選擇.
從零開始寫一個武俠練功遊戲-1-狀態原型
從零開始寫一個武俠練功遊戲-2-幀動畫