從零開始寫一個武俠冒險遊戲-8-用GPU提高性能(3)

從零開始寫一個武俠冒險遊戲-8-用GPU提高性能(3)

----解決因繪製雷達圖致使的幀速降低問題

  • 做者:FreeBlues
  • 修訂記錄
    • 2016.06.23 初稿完成.
    • 2016.08.07 增長對 XCode 項目文件的說明.

概述

如今輪到用 mesh 改寫那個給性能帶來巨大影響的狀態類了, 分析一下不難發現主要是那個實時繪製而且不停旋轉的雷達圖拖累了幀速, 那麼咱們就先從雷達圖入手.git

開始我感受這個雷達圖改寫起來會比較複雜, 由於畢竟在狀態類的代碼中, 雷達圖就佔據了一多半, 並且又有實時繪製, 又要旋轉, 想一想都以爲麻煩, 因此就把它放到最後面來實現.github

不過如今正式開始考慮時, 才發現, 其實想多了, 並且又犯了個特別容易犯的毛病: 一次性考慮全部的問題, 因而問題天然就變複雜了, 那麼咱們繼續遵循最初的原型開發原則, 先提取核心需求, 從簡單入手, 一步一步來, 一次只考慮一個問題, 這樣把整個問題分解開發就發現其實也沒多難.數組

用 mesh 改寫狀態類

總體思路

改寫工做的核心就是先畫個大六邊形做爲雷達圖的背景, 再根據角色的 6 個屬性值畫一個小多邊形(可能會凹進去), 最後讓它旋轉, 其中涉及的實時計算所有放到 shader 中.xcode

還有要作的就是在六邊形頂點處顯示屬性名稱, 最後把狀態欄也用 mesh 繪製出來.框架

改寫雷達圖

具體來講就是兩部分工做:函數

  • 繪製雷達圖背景:大六邊形
  • 繪製技能線:小多邊形

咱們前面也用過 mesh 繪圖, 使用了函數 addRect(), 由於咱們當時繪製的是一個方形區域, 如今要繪製六邊形, 可使用 mesh 的另外一種繪圖方式: 爲其提供多邊形的頂點, 這些頂點用於組成一個個的三角形, 使用屬性 mesh.vertices 來傳遞頂點, 形如:性能

mesh.vertices = {vec2(x1,y1), vec2(x2,y2), vec2(x3,y3), ...}

或者:優化

mesh.vertices = {vec2(x1,y1,z1), vec2(x2,y2,z2), vec2(x3,y3,z3), ...}

這種繪圖方式最靈活, 不過也比較麻煩, 由於要計算好各個三角形的位置, 這些三角形還要設置好順序, 不然就容易畫錯, 好在 Codea 還提供了一個把多邊形拆分爲三角形的函數 triangulate()(實際是封裝了 OpenGL ES 2.0/3.0 的函數), 只要給出多邊形的頂點座標, 就能夠返回拼接成多邊形的多個三角形的頂點座標.動畫

先試試再說, 爲避免影響已有代碼, 咱們在 Status 類中單獨寫一個新函數 Status:radarGraphMesh(), 在這個函數裏進行咱們的改寫工做, 代碼以下:lua

-- 用 mesh 繪製雷達圖
function Status:raderGraphMesh()
    -- 雷達圖底部大六邊形背景
    self.m = mesh()
    -- 雷達圖中心座標,半徑,角度
    local x0,y0,r,a,s = 250,330,500,360/6,4
    -- 計算右上方斜線的座標
    local x,y = r* math.cos(math.rad(30)), r* math.sin(math.rad(30))
    -- 六邊形 6 個頂點座標,從正上方開始,逆時針方向
    local points = triangulate({vec2(0,r/s),vec2(-x/s,y/s),vec2(-x/s,-y/s),
                                vec2(0,-r/s),vec2(x/s,-y/s),vec2(x/s,y/s)})
    print(#points, points[1], points[2],points[3])
    self.m.vertices = points
    local c1 = color(0, 255, 121, 123)
    self.m:setColors(c1)    
end


-- main 主程序框架
function setup()
    displayMode(OVERLAY)
    myStatus = Status()
    myStatus:raderGraphMesh()
end

function draw()
    background(32, 29, 29, 255)
    
    translate(650,300)
    myStatus.m:draw()
    
    sysInfo()
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

截圖以下:

看起來不錯, 再在這個大六邊形上面畫一個小六邊形, 小六邊形的頂點須要根據屬性值(體力,內力,精力,智力,,)來計算, 還好咱們前面寫過一個計算函數 linesDynamic(t,n,j,z,q,x), 把它改個名字, 再稍做改動, 讓它返回計算出來的頂點, 而後再新建一個名爲 m1mesh, 用來繪製表明屬性值的小六邊形, 代碼以下:

-- 用 mesh 繪製雷達圖
function Status:raderGraphMesh()
    ...
    -- 實時繪製頂點位置,根據各狀態屬性值,實時計算頂點位置
    local function axisDynamic()
        local t,n,j,z,q,x = self.tili, self.neili, self.jingli,self.zhili, self.qi, self.xue
        local c,s = math.cos(math.rad(30)), math.sin(math.rad(30))
        local points = triangulate({vec2(0,t),vec2(-n*c,n*s),vec2(-j*c,-j*s),
                                    vec2(0,-z),vec2(q*c,-q*s),vec2(x*c,x*s)})
        return points
    end
    
    -- 繪製表明屬性值的小六邊形
    self.m1 = mesh()
    self.m1.vertices = axisDynamic()
    local c = color(0, 255, 121, 123)
    self.m1:setColors(c)
    ...
end

在主程序的 draw() 中增長一句 myStatus.m1:draw() 就能夠了, 看看運行截圖:

很好, 很是符合咱們的要求, 不過有一點就是做爲背景的大六邊形的對角的線沒有畫出來, 如今須要處理一下, 實際上用 shader 最適合畫的圖形就是三角形, 直線有點麻煩(雖然 OpenGL ES 2.0 支持 三角形, 直線 三種基本圖形繪製, 不過我在 Codea 中沒找到直線的函數), 固然, 咱們也能夠用兩個狹長的三角形拼成一個細長的矩形來模擬直線, 不過這樣比較麻煩, 因此咱們打算改用另一種方法來實現: 把組成六邊形的三角形的頂點設置不一樣的顏色, 這樣相鄰兩個三角形之間那條公共邊就被突出了.

mesh 中, 能夠用這個函數來設定頂點的顏色 mesh:color(i, color), 第一個參數 i 是頂點在頂點數組中的索引值, 從 1 開始, 貌似咱們的六邊形總共生成了 12 個頂點(感受好像有些不對), 每 3 個頂點組成一個三角形, 先隨便改改看看是什麼效果, 就修改其中 1,5,9 號頂點好了, 立刻試驗:

...
    local c1,c2 = color(0, 255, 121, 123),color(255, 57, 0, 123)
    self.m:setColors(c2)
    self.m:color(1,c1)
    self.m:color(5,c1)
    self.m:color(9,c1)
    ...

看看截圖:

果真, 徹底不是咱們想象中的六個小三角形, 原來出於優化的緣由, 函數 triangulate() 會生成儘可能少的三角形, 咱們的六邊形只須要 4 個三角形就能夠了, 因此它返回 12 個頂點, 看來想達到咱們的效果, 還得手動設定頂點, 好在咱們的圖形比較規則, 只須要再加一箇中心的座標就夠了, 而咱們中心點的座標頗有先見之明地被設置爲了 vec2(0,0), 代碼以下:

...
    -- 手動定義組成六邊形的6個三角形的頂點
    local points = {vec2(0,r/s), vec2(-x/s,y/s), vec2(0,0),
                    vec2(-x/s,y/s), vec2(-x/s,-y/s), vec2(0,0),
                    vec2(-x/s,-y/s), vec2(0,-r/s), vec2(0,0),
                    vec2(0,-r/s), vec2(x/s,-y/s), vec2(0,0),
                    vec2(x/s,-y/s), vec2(x/s,y/s), vec2(0,0),
                    vec2(x/s,y/s), vec2(0,r/s), vec2(0,0)} 
    self.m.vertices = points
    
    local c1,c2 = color(186, 255, 0, 123),color(25, 235, 178, 123)
    self.m:setColors(c2)
    self.m:color(1,c1)
    self.m:color(4,c1)
    self.m:color(7,c1)
    self.m:color(10,c1)
    self.m:color(13,c1)
    self.m:color(16,c1)
    
    ...

截圖:

再看看效果, 還能夠, 好, 就按這個方式寫了.

如今須要處理的是這個用於實時計算屬性值頂點的函數 axisDynamic(), 認真分析一下, 就會發現, 其實咱們不須要實時計算, 由於屬性值並非實時更新的, 它應該是隨着角色的活動而變化, 角色有活動它纔會變, 固然這也取決於咱們的設定, 若是咱們設定說角色只要有動做就會耗費體力, 哪怕角色坐着不動, 只要時間流逝它也會變的話, 那麼它就須要實時繪製了, 咱們先按實時繪製來實現. 既然是實時計算, 那咱們但願把這部分計算處理也放到 GPU 中處理, 也就是說須要在 shader 中實現這個函數.

另外就是目前只用一個函數 radarGraphMesh() 來實現雷達圖的繪製, 有些結構不合理, 一些初始化的工做在每次繪製時都要作, 因此打算把它拆分紅三個個函數, 函數 radarGraphInit() 用來負責初始化一些頂點數據, 函數 radarGraphVertex() 用來根據屬性值實時計算頂點座標, 函數 radarGraphDraw() 用來執行繪圖操做, 以下:

function Status:radarGraphInit()
    -- 雷達圖底部六邊形背景
    self.m = mesh()
    p = {"體力","內力","精力","智力","氣","血"}
    -- 中心座標,半徑,角度,縮放比例
    local x0,y0,r,a,s = 150,230,50,360/6,1
    -- 計算右上方斜線的座標
    local x,y = r* math.cos(math.rad(30)), r* math.sin(math.rad(30))
    -- 六邊形 6 個頂點座標,從正上方開始,逆時針方向
    local points = triangulate({vec2(0,r/s),vec2(-x/s,y/s),vec2(-x/s,-y/s),
                                vec2(0,-r/s),vec2(x/s,-y/s),vec2(x/s,y/s)})
    print(#points, points[1], points[2],points[3])
    -- 手動定義組成六邊形的6個三角形的頂點
    local points = {vec2(0,r/s), vec2(-x/s,y/s), vec2(0,0),
                    vec2(-x/s,y/s), vec2(-x/s,-y/s), vec2(0,0),
                    vec2(-x/s,-y/s), vec2(0,-r/s), vec2(0,0),
                    vec2(0,-r/s), vec2(x/s,-y/s), vec2(0,0),
                    vec2(x/s,-y/s), vec2(x/s,y/s), vec2(0,0),
                    vec2(x/s,y/s), vec2(0,r/s), vec2(0,0)} 
    self.m.vertices = points
    
    local c1,c2 = color(186, 255, 0, 123),color(25, 235, 178, 123)
    self.m:setColors(c2)
    self.m:color(1,c1)
    self.m:color(4,c1)
    self.m:color(7,c1)
    self.m:color(10,c1)
    self.m:color(13,c1)
    self.m:color(16,c1)
    
    
    -- 繪製表明屬性值的小六邊形
    self.m1 = mesh()
    self.m1.vertices = self:radarGraphVertex()
    local c = color(221, 105, 55, 123)
    self.m1:setColors(c)
    
end

-- 實時繪製頂點位置,根據各狀態屬性值,實時計算頂點位置
function Status:radarGraphVertex()
    local l = 4
    local t,n,j,z,q,x = self.tili/l, self.neili/l, self.jingli/l,self.zhili/l, self.qi/l, self.xue/l
    local c,s = math.cos(math.rad(30)), math.sin(math.rad(30))
    local points = triangulate({vec2(0,t),vec2(-n*c,n*s),vec2(-j*c,-j*s),
                                    vec2(0,-z),vec2(q*c,-q*s),vec2(x*c,x*s)})
    return points
end

function Status:radarGraphDraw()
    setContext(self.img)
    pushMatrix()
    pushStyle()
    -- 平移到中心 (x0,y0), 方便以此爲中心旋轉
    translate(x0,y0)
    -- 圍繞中心點勻速旋轉
    rotate(30+ElapsedTime*10)
    
    self.m:draw()
    self.m1:draw()

    
    strokeWidth(2)    
    -- noSmooth()
    stroke(21, 42, 227, 255)
    fill(79, 229, 28, 255)
    -- 繪製雷達圖相對頂點之間的連線
    for i=1,6 do
        -- print(i)
        text(p[i],0,45)
        -- line(0,0,0,r)
        rotate(a)
    end

    popStyle()
    popMatrix()
    setContext()    
end

再把 Status:radarGraphInit() 放到 Status:init() 中, 把 Status:radarGraphDraw() 放到 Status:drawUI() 中, 以下:

function Status:init() 
    ...
        
    -- 初始化雷達圖
    self:radarGraphInit()
end

function Status:drawUI()
    ...
    
    self:radarGraphDraw()
    sprite(self.img, 400,300)
end

運行發現幀速大幅提高, 基本在 60 左右, 看來以前拖累性能的緣由是不合理的程序結構(把全部工做都放到一個函數 Status:radarGraph() 中去繪製雷達圖), 真是歪打正着, 這麼看來, 這裏僅僅作完這兩點:

  • 把繪圖方式改寫爲 mesh;
  • 修改不合理的程序結構.

就已經把性能大幅度提高了, 也就不必再用 shader 來改寫了.

剩下的就是一些收尾工做, 好比把一些調試時使用的全局變量改寫爲類屬性什麼的, 完成後的完整狀態類以下:

-- 用 mesh 繪製,先繪製背景六邊形,再繪製技能六邊形,再繪製動態技能,最後再考慮旋轉
-- 角色狀態類
Status = class()

function Status:init() 
    -- 體力,內力,精力,智力,氣,血
    self.tili = 100
    self.neili = 30
    self.jingli = 70
    self.zhili = 100
    self.qi = 100
    self.xue = 100
    self.gongfa = {t={},n={},j={},z={}}
    self.img = image(200, 300)
    -- 初始化雷達圖
    self:radarGraphInit()
end

function Status:update()
    -- 更新狀態:自我修煉,平常休息,戰鬥
    self.neili = self.neili + 1
    self:xiulian()
end

function Status:drawUI()
    setContext(self.img)
    background(119, 121, 72, 255)
    pushStyle()
    fill(35, 112, 111, 114)
    rect(5,5,200-10,300-10)
    fill(70, 255, 0, 255)
    textAlign(RIGHT)
    local w,h = textSize("體力: ")
    text("體力: ",30,280) 
    text(math.floor(self.tili), 30 + w, 280)
    text("內力: ",30,260) 
    text(math.floor(self.neili),  30 + w, 260)
    text("精力: ",30,240) 
    text(math.floor(self.jingli), 30 + w, 240)
    text("智力: ",30,220) 
    text(math.floor(self.zhili), 30 + w, 220)
    text("氣    : ",30,200) 
    text(math.floor(self.qi), 30 + w, 200)
    text("血    : ",30,180) 
    text(math.floor(self.xue), 30 + w, 180)
    -- 繪製狀態欄繪製的角色
    sprite("Documents:B1", 100,90)
    popStyle()
    setContext()
    
    self:radarGraphDraw()
    sprite(self.img, 400,300)
end

function Status:xiulian()
    -- 修煉基本內功先判斷是否知足修煉條件: 體力,精力大於50,修煉一次要消耗一些
    if self.tili >= 50 and self.jingli >= 50 then
        self.neili = self.neili * (1+.005)
        self.tili = self.tili * (1-.001)
        self.jingli = self.jingli * (1-.001)
    end
end

-- 用 mesh 繪製, 改寫爲3個函數
function Status:radarGraphInit()
    -- 雷達圖底部六邊形背景
    self.m = mesh()
    p = {"體力","內力","精力","智力","氣","血"}
    -- 中心座標,半徑,角度,縮放比例
    self.x0, self.y0, self.rr, self.ra, self.rs = 150,230,40,360/6,1
    local x0,y0,r,a,s = self.x0, self.y0, self.rr, self.ra, self.rs
    -- 計算右上方斜線的座標
    local x,y = r* math.cos(math.rad(30)), r* math.sin(math.rad(30))
    -- 六邊形 6 個頂點座標,從正上方開始,逆時針方向
    local points = triangulate({vec2(0,r/s),vec2(-x/s,y/s),vec2(-x/s,-y/s),
                                vec2(0,-r/s),vec2(x/s,-y/s),vec2(x/s,y/s)})
    print(#points, points[1], points[2],points[3])
    -- 手動定義組成六邊形的6個三角形的頂點
    local points = {vec2(0,r/s), vec2(-x/s,y/s), vec2(0,0),
                    vec2(-x/s,y/s), vec2(-x/s,-y/s), vec2(0,0),
                    vec2(-x/s,-y/s), vec2(0,-r/s), vec2(0,0),
                    vec2(0,-r/s), vec2(x/s,-y/s), vec2(0,0),
                    vec2(x/s,-y/s), vec2(x/s,y/s), vec2(0,0),
                    vec2(x/s,y/s), vec2(0,r/s), vec2(0,0)} 
    self.m.vertices = points
    
    local c1,c2 = color(186, 255, 0, 123),color(25, 235, 178, 123)
    self.m:setColors(c2)
    self.m:color(1,c1)
    self.m:color(4,c1)
    self.m:color(7,c1)
    self.m:color(10,c1)
    self.m:color(13,c1)
    self.m:color(16,c1)
    
    
    -- 繪製表明屬性值的小六邊形
    self.m1 = mesh()
    self.m1.vertices = self:radarGraphVertex()
    local c = color(221, 105, 55, 123)
    self.m1:setColors(c)
    
end

-- 實時繪製頂點位置,根據各狀態屬性值,實時計算頂點位置
function Status:radarGraphVertex()
    local l = 4
    -- 中心座標,半徑,角度,縮放比例
    local x0,y0,r,a,s = self.x0, self.y0, self.rr, self.ra, self.rs
    local t,n,j,z,q,x = self.tili/l, self.neili/l, self.jingli/l,self.zhili/l, self.qi/l, self.xue/l
    local c,s = math.cos(math.rad(30)), math.sin(math.rad(30))
    local points = triangulate({vec2(0,t),vec2(-n*c,n*s),vec2(-j*c,-j*s),
                                    vec2(0,-z),vec2(q*c,-q*s),vec2(x*c,x*s)})
    return points
end

function Status:radarGraphDraw()
    setContext(self.img)
    pushMatrix()
    pushStyle()
    
    -- 中心座標,半徑,角度
    local x0,y0,r,a,s = self.x0, self.y0, self.rr, self.ra, self.rs
    -- 平移到中心 (x0,y0), 方便以此爲中心旋轉
    translate(x0,y0)
    -- 圍繞中心點勻速旋轉
    rotate(30+ElapsedTime*10)
    
    self.m:draw()

    strokeWidth(2)    
    -- Smooth()
    stroke(21, 42, 227, 255)
    fill(79, 229, 128, 255)
    -- 繪製雷達圖相對頂點之間的連線
    for i=1,6 do
        text(p[i],0,r+15)
        -- line(0,0,0,49)
        rotate(a)
    end
    self.m1.vertices = self:radarGraphVertex()
    self.m1:draw()

    popStyle()
    popMatrix()
    setContext()    
end


-- main 主程序框架
function setup()
    displayMode(OVERLAY)
    myStatus = Status()
end

function draw()
    background(32, 29, 29, 255)    
    myStatus:drawUI()
    sysInfo()
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
shadersStatus = {
status = { vs=[[
// 雷達圖着色器: 用 shader 繪製雷達圖
//--------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()
{
    // vec4 col = texture2D(texture,vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));
    vec4 col = texture2D(texture,vTexCoord);
    gl_FragColor = vColor * col;
}
]]}
}

發現改寫爲 mesh, 再把原來的一個函數拆分紅三個後, 不只性能提高了, 並且代碼也沒那麼多了, 更重要的是讀起來很清晰.

本章小結

如今, 咱們已經用 mesh 完成 幀動畫, 地圖類狀態類 的改寫, 並且效果還不錯, 幀速也提高到了 60 左右, 既然達到了起初的目標, 那麼剩下的就是再次把這幾個改寫後的模塊整合到一塊兒, 整合後的代碼在這裏: c06.lua.

全部章節連接

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
├── 從零開始寫一個武俠冒險遊戲-7-用GPU提高性能(2).md
└── 從零開始寫一個武俠冒險遊戲-8-用GPU提高性能(3).md

2 directories, 26 files
Air:Write-A-Adventure-Game-From-Zero admin$
相關文章
相關標籤/搜索