2-幀動畫-從零開始寫一個武俠冒險遊戲
從零開始寫一個武俠冒險遊戲-2-幀動畫
---- 用基本繪圖函式實現幀動畫
本章寫作思路:
說明幀動畫的原理->取得素材->把素材整個顯示->擷取子畫面->顯示子畫面->討論在迴圈顯示函式 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
本文涉及的 Codea 基本繪圖函式
函式 sprite(圖片, x, y, width, height)
函式說明:
引數說明:
- 圖片: 圖片物件
- x: 左下角(或中心點)橫座標
- y: 左下角(或中心點)縱座標
- width: 顯示出來的影像的寬度
- height: 顯示出來的影像的高度
函式 image:copy(x,y,width,height)
函式說明:
引數說明:
- x: 左下角橫座標
- y: 左下角縱座標
- width: 要拷貝影像區域的寬度
- height: 要拷貝影像區域的高度
更多的函式說明文件請參考:
繪製一個子幀
很好, 現在我們從素材中取出第一個子畫面, 也就是左下角座標為(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
, 所以一旦左右移動背景超過了這個長寬範圍,就沒有影像了,因此,我們可以有這麼幾種解決思路:
- 1 預先準備一個大於螢幕尺寸的大背景圖;
- 2 使用兩個繪圖語句,一個以
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)
這兩種方式各有利弊, 我們可以根據實際需要進行選擇.
跟第一章整合
相關文章
- 從零開始:用REACT寫一個格鬥遊戲(一)React遊戲
- 從零開始:用REACT寫一個格鬥遊戲(二)React遊戲
- 動作冒險遊戲俠盜獵車手遊戲
- 從零開始寫一個ExporterExport
- 從零開始仿寫一個抖音App——開始APP
- 從零開始的 Flutter 動畫Flutter動畫
- 從零開始寫一個node爬蟲(一)爬蟲
- 從零開始實現放置遊戲(一)遊戲
- 冒險遊戲已逝?冒險遊戲萬歲!遊戲
- 從零開始編寫一個babel外掛Babel
- 從零開始寫一個Javascript解析器JavaScript
- 從3A遊戲中看國產武俠遊戲
- 從零開始做一個SLG遊戲(七):遊戲系統以及配置表遊戲
- 從零開始做一個SLG遊戲(一):六邊形網格遊戲
- Cursor 寫一個 Flutter Unsplash 桌布工具 | 從零開始Flutter
- 從零開始寫一個微前端框架-沙箱篇前端框架
- 武俠遊戲,江湖告急遊戲
- 從0開始用python寫一個命令列小遊戲(二)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(十)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(六)Python命令列遊戲
- 從零開始做一個SLG遊戲(八):配置表載入元件遊戲元件
- 武俠遊戲演變史:從“俠客英雄傳”到“只狼”遊戲
- 從零開始手寫一個微前端框架-渲染篇前端框架
- 暗黑+仙俠+動作冒險,這款遊戲是如何將它們融合的?遊戲
- 從零開始開發一個 WebpackWeb
- 從零開始構建一個vue專案 --- webpack歷險記VueWeb
- 從零開始做一個SLG遊戲(六):UI系統擴充套件遊戲UI套件
- 從零開始做一個SLG遊戲(三):用unity繪製圖形遊戲Unity
- 從零開始做一個SLG遊戲(四):UI系統之主介面搭建遊戲UI
- 從零開始,如何用puppeteer寫一個爬蟲指令碼爬蟲指令碼
- 從零開始編寫一個 Python 非同步 ASGI WEB 框架Python非同步Web框架
- phaser3入門教程-從零開始開發一個打磚塊遊戲遊戲
- 為冒險解密遊戲設計一個好謎題解密遊戲設計
- 從零開始實現一個RPC框架(零)RPC框架
- “俠”已成痴:一個西北漢子的硬派武俠遊戲夢與他的江湖遊戲
- 從零開始實現放置遊戲(一):整體框架搭建遊戲框架
- 位元組跳動的遊戲大冒險遊戲
- 【從零開始擼一個App】PKCEAPP
- 【從零開始擼一個App】KotlinAPPKotlin