从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)
從零開始寫一個武俠冒險游戲-6-用GPU提升性能(1)
----把幀動畫的實現放在GPU上
- 作者:FreeBlues
- 修訂記錄
- 2016.06.19 初稿完成.
- 2016.08.05 增加對 XCode 項目文件的說明.
概述
我們之前所有的繪圖工作都是直接使用基本繪圖函數來繪制的, 這樣寫出來的代碼容易理解, 不過這些代碼基本都是由 CPU 來執行的, 沒怎么發揮出 GPU 的作用, 實際上現在的移動設備都有著功能不弱的 GPU(一般都支持 OpenGL ES 2.0/3.0), 本章的目標就是把我們游戲中繪圖相關的大部分工作都轉移到 GPU 上, 這樣既可以解決我們代碼目前存在的一些小問題, 同時也會帶來很多額外好處:
- 首先是性能可以得到很大提升, 我們現在的幀速是40左右, 主要是雷達圖的實時繪制拖慢了幀速;
- 方便在地圖類上實現各種功能, 如大地圖的局部顯示, 地圖平滑卷動;
- 保證地圖上的物體狀態更新后重繪地圖時的效率;
- 幀動畫每次起步時速度忽然加快的問題, 反向移動時角色動作顯示為倒退, 需要鏡像翻轉;
- 狀態欄可以通過 紋理貼圖 來使用各種中文字體(Codea不支持中文字體);
- 最大的好處是: 可以通過 shader 來自己編寫各種圖形特效.
在 Codea 里使用 GPU 的方法就是用 mesh 和 shader 來繪圖, 而 mesh 本身就是一種內置 shader. 還有一個很吸引人的地方就是: 使用 mesh 后續可以很容易地把我們的 2D 游戲改寫為 3D 游戲, 這也是我們這個游戲的一個嘗試: 玩家可以自由地在 2D 和 3D 之間轉換.
基于以上種種理由, 我們后續會把游戲中大部分圖形繪制工作都放到 GPU 上, CPU 只負責處理耗費資源很少的菜單選項等 UI 繪制.
本章先簡單介紹一下 Codea 中的 mesh 和 shader, 接著按照從易到難的順序, 依次把 幀動畫類, 地圖類 和 狀態類 改寫為用 GPU 繪制(也就是用 mesh 繪制)
這部分內容稍微深入一些, 需要讀者對 OpenGL ES 2.0 中的坐標系統有一點了解, 另外對于著色器語言 shader language 也要有一定了解, 這樣讀起來不會太吃力, 不過沒有這方面背景也不要緊, 多讀幾遍, 上機跑幾遍例程, 再自己胡亂修改修改看看是什么效果, 這么折騰一番也差不多會了.
因為一方面本章內容稍微難一些, 另一方面本章的篇幅也比較長, 因此本章將拆分為兩個或者三個子章節.
Codea 中的 mesh + shader 介紹
簡單介紹 mesh
mesh 是 Codea 中的一個用來繪圖的類, 用來實現一些高級繪圖, 用法也簡單, 先新建一個 mesh 實例, 接著設置它的各項屬性, 諸如設置頂點 m.vertices, 設置紋理貼圖 m.texture, 設置紋理坐標 m.texCoords, 設置著色器 m.shader= shader(...) 等等, 最后就是用它的 draw() 方法來繪制, 如果有觸摸事件需要處理, 那就寫一下它的 touched(touch) 函數, 最簡單例程如下:
function setup()m = mesh() mi = m:addRect(x, y, WIDTH/10, HEIGHT/10)m.texture = readImage("Documents:catRunning")m.shader = shader(shaders["sprites"].vs,shaders["sprites"].fs)m:setRectTex(mi, s, t, w,h) endfunction draw()m:draw() end簡單介紹 shader
shader 是 OpenGL 中的概念, 我們在移動設備上使用的 OpenGL 版本是 OpenGL ES 2.0/3.0, shader 是其中的著色器, 用于在管線渲染的兩個環節通過用戶的自定義編程實行人工干預, 這兩個環節一個是 頂點著色-vertex, 一個是 片段(像素)著色-fragment, 也就是說實際上它就是針對 vertex 和 fragment 的兩段程序, 它的最簡單例程如下:
shaders = { sprites = { vs=[[ //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;vTexCoord = texCoord;gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;gl_FragColor = col; } ]]} }為方便調用,我們把它們寫在兩段字符串中, 然后放到一個表里.
Codea 中的 mesh 和 shader 可以到官網查看手冊, 或者看看 Codea App 的內置手冊(中文版), 還算全面.
大致介紹了 mesh 和 shader 之后, 就要開始我們的改寫工作了, 先從 幀動畫類開始.
用 mesh 改寫幀動畫類
思路
仔細分析之后, 發現用 mesh 去實現幀動畫, 簡直是最合適不過了, 只要充分利用好它的紋理貼圖和紋理坐標屬性, 就可以很方便地從一張大圖上取得一幅幅小圖, 而且動畫顯示速度控制也很好寫, 我們先用 mesh 創建一個矩形, 把整副幀動畫素材圖作為它的紋理貼圖, 這樣我們就可以通過設置不同的紋理坐標來取得不同的子幀, 而且它的紋理坐標的參數特別適合描述子幀: 左下角 x, 左下角 y, 寬度, 高度, 注意, 紋理坐標的范圍是 [0,1].
補充說明一下, 鑒于目前并非所有的設備都支持 3.0, 所以我們這里使用的都是 OpenGL ES 2.0, 在 3.0 中, 新增了一種紋理類型 2Dw紋理數組-2DSampleArray, 就是專門用于實現幀動畫的, 用起來應該更方便.
結合具體代碼進行說明
下面看看代碼:
function setup()...-- 新建一個矩形, 保存它的標識索引 mimi = m:addRect(self.x, self.y,WIDTH/10,HEIGHT/10)-- 把整副幀動畫素材設置為紋理貼圖m.texture = readImage("Documents:catRunning")-- 計算出各子幀的紋理坐標存入表中coords = {{0,3/4,1/2,1/4}, {1/2,3/4,1/2,1/4}, {0,2/4,1/2,1/4}, {1/2,2/4,1/2,1/4}, {0,1/4,1/2,1/4}, {1/2,1/4,1/2,1/4}, {0,0,1/2,1/4}, {1/2,0,1/2,1/4}}-- 把第一幅子幀設置為它的紋理坐標m:setRectTex(mi, self.coords[1][1], self.coords[1][2] ,self.coords[1][3], self.coords[1][4])... end因為我們這幅素材圖分 2 列, 4 行, 共有 8 副子幀, 第一幅子幀在左上角, 所以第一幅子幀對應的紋理坐標就是 {0, 3/4, 1/2, 1/4}, 其余以此類推, 我們把所有子幀的紋理坐標按顯示順序依次存放在一個表中, 后續可以方便地過遞增索引來循環顯示.
先在 setup() 中設置好 time 和 speed 的值, 接著在 draw() 中可以通過這段代碼來控制每幀的顯示時間:
function draw()...-- 如果停留時長超過 speed,則使用下一幀if os.clock() - time >= speed theni = i + 1time = os.clock()end... end我們一般用幀動畫來表現玩家控制的角色, 需要移動它的顯示位置, 可以在 draw() 中用這條語句實現:
-- 根據 x, y 重新設置顯示位置m:setRect(mi, x, y, w, h)目前我們的代碼需要每副子幀的尺寸一樣大, 如果子幀尺寸不一樣大的話, 就需要做一個轉換, 我們決定讓屬性紋理坐標表仍然使用真實坐標, 新增一個類方法來把它轉換成范圍為 [0,1] 的表, 如下:
-- 原始輸入為形如的表: 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}}-- 把絕對坐標值轉換為相對坐標值 function convert(coords)local w, h = m.texture.width, m.texture.heightlocal n = #coordsfor i = 1, n docoords[i][1], coords[i][2] = coords[i][1]/w, coords[i][2]/hcoords[i][3], coords[i][4] = coords[i][3]/w, coords[i][4]/hend end用 shader 實現鏡像翻轉
現在還有一個問題, 就是當角色先向右移動, 然后改為向左移動時, 角色的臉仍然朝向右邊, 看起來就像是倒著走一樣, 因為我們的幀動畫素材中橘色就是臉朝右的, 該怎么辦呢? 有種辦法是做多個方向的幀動畫素材, 比如向左, 向右, 向前, 向后, 這貌似是通用的解決方案, 不過我們這里有一種辦法可以通過 shader 實現左右鏡像翻轉, 然后根據移動方向來決定是否調用翻轉 shader.
因為我們只是左右翻轉, 可以這樣想象: 在圖像中心垂直畫一條中線, 把中線左邊的點翻到中線右邊, 把中線右邊的點翻到中線左邊, 也就是每個點只改變它的 x 值, 假設一個點原來的坐標為 (x, y), 翻轉后它的坐標就變成了 (1.0-x, y), 注意, 此處因為是紋理坐標, 所以該點坐標范圍仍然是 [0,1], 這次變化只涉及頂點, 所以我們只需要修改 vertex shader, 代碼如下:
void main() {vColor = color;// vTexCoord = texCoord;vTexCoord = vec2(1.0-texCoord.x, texCoord.y);gl_Position = modelViewProjection * position; }不過這樣處理在每個子幀的尺寸有差異時會出現顯示上的問題, 因為我們的紋理坐標是手工計算出來的, 它所確定的子幀不是嚴格對稱的, 解決辦法就是給出一個精確左右對稱的紋理坐標, 這樣弄起來也挺麻煩, 其實最簡單的解決辦法是把素材處理一下, 讓每副子幀的尺寸相同就好了.
用 shader 去掉素材白色背景
在使用 runner 素材時, 因為它的背景是白色, 需要處理成透明, 之前我們專門寫了一個函數 Sprites:deal() 預先對圖像做了處理, 現在我們換一種方式, 直接在 shader 里處理, 也很簡單, 就是在用取樣函數得到當前像素的顏色時, 看看它是不是白色,若是則使用 shader 內置函數 discard 將其丟棄, 注意, 這里的顏色值必須寫成帶小數點的形式, 因為它是一個浮點類型, 對應的 fragment shader 代碼如下:
// 定義一個用于比較的最小 alpha 值, 由用戶自行控制 uniform vec4 maxWhite;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;if ( col.r > maxWhite.x && col.g > maxWhite.y && col.b > maxWhite.z) discard;else gl_FragColor = col; }試著執行一下, 發現效果還不錯.
發現還有個小問題, 就是修改了 self.w 和 self.h 后, 顯示的區域出現了錯誤, 看了代碼, 需要在 Sprites:init() 中修改一下, 修改前為:
self.mi = self.m:addRect(self.x, self.y, self.w, self.h)修改后為:
self.mi = self.m:addRect(self.x, self.y, w, h)另外, 在移動角色時加了一個判斷, 避免它走出屏幕范圍, 代碼如下:
-- 根據 self.x, self.y 重新設置整幅圖的顯示位置,走到邊緣則不再前進local l,r,b,t = WIDTH/16,WIDTH*15/16,HEIGHT/16,HEIGHT*15/16if self.x >= l and self.x <= r and self.y >= b and self.y <= t thenself.m:setRect(self.mi, self.x, self.y, self.w, self.h)end不過有些切換不太靈活, 后續需要找一下原因.
完整代碼
寫成類的完整代碼如下:
-- c06.lua--# Shaders -- 用 mesh/shader 實現幀動畫,把運算量轉移到 GPU 上,可用 shader 實現各種特殊效果 Sprites = class()function Sprites:init()self.m = mesh()self.m.texture = readImage("Documents:catRunning")self.m.shader = shader(shaders["sprites"].vs,shaders["sprites"].fs)self.coords = {{0,3/4,1/2,1/4}, {1/2,3/4,1/2,1/4}, {0,2/4,1/2,1/4}, {1/2,2/4,1/2,1/4}, {0,1/4,1/2,1/4}, {1/2,1/4,1/2,1/4}, {0,0,1/2,1/4}, {1/2,0,1/2,1/4}}self.i = 1local w,h = self.m.texture.width, self.m.texture.heightlocal ws,hs = WIDTH/w, HEIGHT/hself.x, self.y = w/2, h/2self.w, self.h = WIDTH/10, HEIGHT/10self.mi = self.m:addRect(self.x, self.y, self.w, self.h)self.speed = 1/30self.time = os.clock() endfunction Sprites:convert()local w, h = self.m.texture.width, self.m.texture.heightlocal n = #self.coordsfor i = 1, n doself.coords[i][1], self.coords[i][2] = self.coords[i][1]/w, self.coords[i][2]/hself.coords[i][3], self.coords[i][4] = self.coords[i][3]/w, self.coords[i][4]/hend endfunction Sprites:draw()-- 依次改變貼圖坐標,取得不同的子幀self.m:setRectTex(self.mi, self.coords[(self.i-1)%8+1][1], self.coords[(self.i-1)%8+1][2], self.coords[(self.i-1)%8+1][3], self.coords[(self.i-1)%8+1][4])-- 根據 self.x, self.y 重新設置整幅圖的顯示位置,走到邊緣則不再前進local l,r,b,t = WIDTH/16,WIDTH*15/16,HEIGHT/16,HEIGHT*15/16if self.x >= l and self.x <= r and self.y >= b and self.y <= t thenself.m:setRect(self.mi, self.x, self.y, self.w, self.h)end-- 如果停留時長超過 self.speed,則使用下一幀if os.clock() - self.time >= self.speed thenself.i = self.i + 1self.time = os.clock()endself.m:draw() endfunction Sprites:touched(touch)self.x, self.y = touch.x, touch.y end-- 游戲主程序框架 function setup()displayMode(OVERLAY)-- 幀動畫素材1img1 = readImage("Documents:runner")pos1 = {{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}}-- 幀動畫素材2 img2 = readImage("Documents:catRunning")local w,h = 1024,1024pos2 = {{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}}-- 開始初始化幀動畫類 myS = Sprites()myS.m.texture = img1myS.coords = pos1-- 若紋理坐標為絕對數值, 而非相對數值(即范圍在[0,1]之間), 則需將其顯式轉換為相對數值myS:convert()-- 使用自定義 shadermyS.m.shader = shader(shaders["sprites1"].vs,shaders["sprites1"].fs)-- 設置 maxWhitemyS.m.shader.maxWhite = 0.8-- 設置速度myS.speed = 1/20myS.x = 500 endfunction draw()background(39, 31, 31, 255)-- 繪制 meshmyS:draw()sysInfo() endfunction touched(touch)myS:touched(touch) end-- 系統信息: 顯示FPS和內存使用情況 function sysInfo()pushStyle()fill(255, 255, 255, 255)-- 根據 DeltaTime 計算 fps, 根據 collectgarbage("count") 計算內存占用local fps = math.floor(1/DeltaTime)local mem = math.floor(collectgarbage("count"))text("FPS: "..fps.." Mem:"..mem.." KB",650,740)popStyle() end-- Shader shaders = {sprites = { vs=[[ // 左右翻轉著色器 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;// vTexCoord = texCoord;vTexCoord = vec2(1.0-texCoord.x, texCoord.y);gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;gl_FragColor = col; } ]]},sprites1 = { vs=[[ // 把白色背景轉換為透明著色器 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;vTexCoord = texCoord;gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;// 定義一個用于比較的最小 alpha 值, 由用戶自行控制 uniform float maxWhite;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;if ( col.r > maxWhite.x && col.g > maxWhite.y && col.b > maxWhite.z) discard;else gl_FragColor = col; } ]]} }回頭來看, 就發現用 mesh 改寫后的幀動畫類既簡單又高效. 而且有了 shader 這個大殺器, 我們可以非常方便地為角色添加各種特效, 上面用過的 鏡像 和 去素材白色背景 就是兩種比較簡單的特效, 我們在下面介紹幾種其他特效.
在幀動畫角色上用 shader 增加特效
角色灰化
一些游戲, 比如 魔獸世界, 在玩家控制的角色死亡時, 會進入靈魂狀態, 這時所有的畫面全部變為灰色, 我們也可以在這里寫一段 shader 來實現這個效果, 不過我們打算稍作修改, 只把玩家角色變為灰色, 屏幕上的其余部分都保持原色.
先寫一個從彩色到灰度的轉換函數, 這個函數要在 fragment shader 中使用:
float intensity(vec4 col) {// 計算像素點的灰度值return 0.3*col.x + 0.59*col.y + 0.11*col.z; }然后修改片段著色代碼:
void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;col.rgb = vec3(intensity(col));gl_FragColor = col; }如果我們希望在灰化的同時實現虛化, 也就是讓角色變淡, 可以連 alpha 一起修改, 這種淡化特效可以用于角色使用了隱匿技能后的顯示, 代碼如下:
void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;col.rgba = vec4(intensity(col));gl_FragColor = col; }效果很不錯, 完全達到了我們的預定目標.
中毒狀態
很多游戲中, 角色如果中毒了, 會在兩個地方顯示出來, 一個是狀態欄, 一個是角色本身, 比如 仙劍奇俠傳 中會給角色渲染一層深綠色, 我們用 shader 實現的話, 只需要把取樣得到的像素點顏色乘以一個指定的顏色值(綠色或其他), 該指定顏色可隨時間變化而變深, 也可以因為吃了解毒藥而逐漸變淺(在我們的設定里不存在一吃藥就變好的情況, 只能慢慢好), 這部分處理可以充分利用 mesh 的一個方法 setRectColor() 來實現, 代碼如下:
function setup()...myS.m:setRectColor(myS.mi, 0, 255,0,255)...shader 中只需要把取樣點的顏色跟該顏色vColor相乘即可, 我們的模板代碼就是這樣的:
void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;gl_FragColor = col;所以我們只需要在 setup() 中設置一下, 然后調用名為 sprites 的 shader 即可.
效果完美, 后面可以根據游戲需要再加一個根據時間流逝綠色變淡或者變深的處理.
角色的其他狀態, 例如受傷出血也可以通過類似的方法實現(把 vColor 改為紅色即可), 可自行試驗.
角色光粒子化
實際上, 我們上面實現的幾種特效都是比較簡單的, 最后我們來一個復雜點的, 角色升華, 變成光粒子消散在空中, 當然這種特效也可以放在 NPC 身上, 代碼如下:
---后續補充本章用到的 shader 代碼
下面列出我們在這里用于實行各種特性的 shader 代碼:
-- Shader shaders = {sprites = { vs=[[ // 左右翻轉著色器 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;// vTexCoord = texCoord;vTexCoord = vec2(1.0-texCoord.x, texCoord.y);gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;gl_FragColor = col; } ]]},sprites1 = { vs=[[ // 把白色背景轉換為透明著色器 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;vTexCoord = texCoord;gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;// 定義一個用于比較的最小 alpha 值, 由用戶自行控制 uniform vec4 maxWhite;void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;if ( col.r > maxWhite.x && col.g > maxWhite.y && col.b > maxWhite.z) discard;else gl_FragColor = col; } ]]},sprites2 = { vs=[[ // 局部變灰 //--------vertex shader--------- attribute vec4 position; attribute vec4 color; attribute vec2 texCoord;varying vec2 vTexCoord; varying vec4 vColor;uniform mat4 modelViewProjection;void main() {vColor = color;vTexCoord = texCoord;gl_Position = modelViewProjection * position; } ]], fs=[[ //---------Fragment shader------------ //Default precision qualifier precision highp float;varying vec2 vTexCoord; varying vec4 vColor;// 紋理貼圖 uniform sampler2D texture;float intensity(vec4 col) {// 計算像素點的灰度值return 0.3*col.x + 0.59*col.y + 0.11*col.z; }void main() {// 取得像素點的紋理采樣lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;col.rgba = vec4(intensity(col));gl_FragColor = col; } ]]} }本章小結
使用 mesh繪圖時, 可以選擇不加載 shader, 如果需要自定義修改圖像中的某些顯示效果, 就要選擇加載 shader 了.
關于幀動畫類的 GPU 改造暫時就寫這么多, 下一節準備說說如何用 mesh 來改寫地圖類.
所有章節鏈接
Github項目地址
Github項目地址, 源代碼放在 src/ 目錄下, 圖片素材放在 assets/ 目錄下, XCode項目文件放在 MyAdventureGame 目錄下, 整個項目文件結構如下:
Air:Write-A-Adventure-Game-From-Zero admin$ tree . ├── MyAdventureGame │?? ├── Assets │?? │?? ├── ... │?? ├── Libs │?? │?? ├── ... │?? ├── MyAdventureGame │?? │?? ├──... │?? ├── MyAdventureGame.codea │?? │?? ├──... │?? ├── MyAdventureGame.xcodeproj │?? │?? ├──... │?? └── libversion ├── README.md ├── Vim 列編輯功能詳細講解.md ├── assets │?? ├── ... │?? └── runner.png ├── src │?? ├── c01.lua │?? ├── c02.lua │?? ├── c03.lua │?? ├── c04.lua │?? ├── c05.lua │?? ├── c06-01.lua │?? ├── c06-02.lua │?? ├── c06-03.lua │?? └── c06.lua ├── 從零開始寫一個武俠冒險游戲-0-開發框架Codea簡介.md ├── 從零開始寫一個武俠冒險游戲-1-狀態原型.md ├── 從零開始寫一個武俠冒險游戲-2-幀動畫.md ├── 從零開始寫一個武俠冒險游戲-3-地圖生成.md ├── 從零開始寫一個武俠冒險游戲-4-第一次整合.md ├── 從零開始寫一個武俠冒險游戲-5-使用協程.md ├── 從零開始寫一個武俠冒險游戲-6-用GPU提升性能(1).md ├── 從零開始寫一個武俠冒險游戲-6-用GPU提升性能(2).md └── 從零開始寫一個武俠冒險游戲-6-用GPU提升性能(3).md2 directories, 26 files Air:Write-A-Adventure-Game-From-Zero admin$轉載于:https://www.cnblogs.com/freeblues/p/5739821.html
總結
以上是生活随笔為你收集整理的从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kvm虚拟化管理平台WebVirtMgr
- 下一篇: Part5核心初始化_lesson3--