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

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

----把地圖處理放在GPU上

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

概述

mesh 改寫地圖類, 帶來的一大好處是控制邏輯能夠變得很是簡單, 做爲一個地圖類, 最基本的控制邏輯就是顯示哪一部分和地圖如何捲動, 而這兩點能夠經過 mesh 的紋理貼圖很是容易地解決, 由於在 OpenGL ES 2.0/3.0 中, 能夠經過設置紋理座標來決定如何在地圖上顯示紋理貼圖, 而這些控制邏輯若是不用 mesh, 本身去寫, 就有些繁瑣了, 不信你能夠試試.git

另外咱們以前實現的地圖類的地圖繪製是極其簡陋的, 好比地面就是一些單色的矩形塊, 本章咱們將會把很小的紋理貼圖素材拼接起來生成更具表現力和真實感的地面.github

基於 OpenGL ES 2.0/3.0 的紋理貼圖特性, 咱們既可使用一塊很小的紋理, 而後用拼圖的方式把大屏幕鋪滿, 也可使用一塊很大的超出屏幕範圍的圖片作紋理, 而後選擇其中一個尺寸跟屏幕尺寸至關的區域來顯示.編程

在本章中, 這兩種方法都會用到, 前者用來生成一張大大的地圖, 後者用來顯示這塊大地圖的局部區域.數組

用 mesh 改寫地圖類

總體思路

地圖類的處理相對來講複雜一些, 正如咱們在 概述 中提到的, 要在兩個層面使用 mesh, 第一層是用小素材紋理經過拼圖的方式生成一張超過屏幕尺寸的大地圖圖片, 第二層是把這張大地圖圖片做爲紋理素材, 經過紋理座標的設置來從大地圖圖片素材中選擇一個尺寸恰好是屏幕大小的區域, 而後把它顯示在屏幕上.xcode

先改寫第二層面

由於咱們是前面的基礎上改寫, 也就是說用來生成大地圖圖片的代碼已經寫好了, 因此咱們能夠選擇先從簡單的開始, 那就是先實現第二層面: 用大圖片做爲紋理貼圖, 利用 mesh 的紋理座標來實現顯示小區域和地圖捲動等功能.框架

具體實現方法

具體辦法就是先在初始化函數 Maps:init() 中用 mesh:addRect() 新建一個屏幕大小的矩形, 而後加載已經生成的大地圖圖片做爲紋理貼圖, 再經過設置紋理座標 mesh:setRectTex(i, x, y, w, t) 取得對應於紋理貼圖上的一塊屏幕大小的區域; 而後再在 Maps:drawMap() 函數中根據角色移動來判斷是否須要捲動地圖, 以及若是須要捲動向哪一個方向捲動, 最後在 Maps:touched(touch) 函數中把紋理座標的 (x, y) 跟觸摸數據關聯起來, 這樣咱們屏幕上顯示的地圖就會隨着角色移動到屏幕邊緣而自動把新地圖平移過來.dom

代碼說明

在初始化函數 Maps:init() 中主要是這些處理:函數

  • 先根據咱們設置的地圖參數計算出整個大地圖的尺寸 w,h,
  • 再申請一個這麼大的圖形對象 self.imgMap, 咱們的大地圖就要繪製在這個圖形對象上,
  • 接着把屏幕放在大地圖中央,計算出屏幕左下角在大地圖上的絕對座標值 self.x, self.y, 這裏把大地圖的左下角座標設爲 (0,0),
  • 而後建立一個 mesh 對象 self.m,
  • 再在 self.m 上新增一個矩形, 該矩形中心座標爲 (WIDTH/2, HEIGHT/2), 寬度爲 WIDTH, 高度爲 HEIGHT, 也就是一個跟屏幕同樣大的矩形,
  • 把大地圖 self.imgMap 設爲 self.m 的紋理貼圖,
  • 由於咱們的紋理貼圖大於屏幕, 因此須要設置紋理座標來映射紋理上的一塊區域, 再次提醒, 紋理座標大範圍是 [0,1], 因此須要咱們把座標的絕對數值轉換爲 [0,1] 區間內的相對數值, 也就是用屏幕寬高除以大地圖的寬高 local u,v = WIDTH/w, HEIGHT/h
  • 最後把這些計算好的變量用 mesh:setRectTex() 設置進去

就是下面這些代碼:性能

...
    -- 根據地圖大小申請圖像
    local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY
    self.imgMap = image(w,h)
    
    -- 使用 mesh 繪製地圖
    -- 設置當前位置爲矩形中心點的絕對數值,分別除以 w, h 能夠獲得相對數值
    self.x, self.y = w/2-WIDTH/2, h/2-HEIGHT/2
    self.m = mesh()
    self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    self.m.texture = self.imgMap
    -- 利用紋理座標設置顯示區域,根據中心點座標計算出左下角座標,除以紋理寬度獲得相對值,w h 使用固定值(小於1)
    local u,v = WIDTH/w, HEIGHT/h
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)
    ...

在繪製函數 Maps:drawMap() 中要作這些處理:測試

  • 首先判斷大地圖有沒有變化, 好比某個位置的某棵樹是否是被玩家角色給砍掉了, 等等, 若是有就從新生成, 從新設置一遍,
  • 檢查玩家角色 myS 當前所在的座標 (myS.x, myS.y) 是否是已經處於地圖邊緣, 若是是則開始切換地圖(也就是把地圖捲動過來), 切換的辦法就是給地圖的紋理座標的起始點一個增量操做,
  • 若是走到屏幕左邊緣, 則須要地圖向右平移, self.x = self.x - WIDTH/1000,
  • 若是走到屏幕右邊緣, 則須要地圖向左平移, self.x = self.x + WIDTH/1000,
  • 若是走到屏幕上邊緣, 則須要地圖向下平移, self.y = self.y + HEIGHT/1000,
  • 若是走到屏幕下邊緣, 則須要地圖向上平移, self.y = self.y - HEIGHT/1000,
  • 而後把這些數據所有除以 w,h 獲得位於 [0,1] 區間內的座標的相對值,
  • 用這些座標相對值做爲函數 self.m:setRectTex() 的參數.

代碼是這些:

...
    -- 更新紋理貼圖, --若是地圖上的物體有了變化
    self.m.texture = self.imgMap
    local w,h = self.imgMap.width, self.imgMap.height
    local u,v = WIDTH/w, HEIGHT/h
    -- 增長判斷,若角色移動到邊緣則切換地圖:經過修改貼圖座標來實現
    print(self.x,self.y)
    local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10
    local ss = 800
    if myS.x <= left then self.x= self.x - WIDTH/ss end
    if myS.x >= right then self.x= self.x + WIDTH/ss end
    if myS.y <= bottom then self.y = self.y - HEIGHT/ss end
    if myS.y >= top then self.y = self.y + HEIGHT/ss end
    
    -- 根據計算獲得的數據從新設置紋理座標
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)  
    ...

另外, 咱們使用了一個局部變量 local ss = 800 來控制屏幕捲動的速度, 由於考慮到玩家角色可能行走, 也可能奔跑, 而咱們這是一個武俠遊戲, 可能會設置 輕功 之類的技能, 這樣當角色以不一樣速度運動到屏幕邊緣時, 地圖捲動的速度也各不相同, 看起來真實感更強一些.

補充說明一點, 爲方便編程, 咱們使用的 self.x, self.y 都用了絕對數值, 可是在函數 self.m:setRectTex() 中須要的是相對數值, 因此做爲參數使用時都須要除以 w, h, 這裏我在調程序的時候也犯過幾回暈.

在函數 Maps:touched(touch) 中, 把觸摸位置座標 (touch.x, touch.y) 跟玩家角色座標 (myS.x, myS.y) 創建關聯, 這裏這麼寫主要是爲了方便咱們如今調試用.

代碼很簡單:

if touch.state == BEGAN then
        myS.x, myS.y = touch.x, touch.y
    end

另外還須要在 setup() 函數中設置一下 (myS.x, myS.y) 的初值, 讓它們位於屏幕中央就能夠了.

myS.x, myS.y = WIDTH/2, HEIGHT/2

修改後代碼

完整代碼以下:

-- c06-02.lua

-- 主程序框架
function setup()
    displayMode(OVERLAY)

    myS = {}
    myS.x, myS.y = WIDTH/2, HEIGHT/2
    myMap = Maps()
    myMap:createMapTable()
end

function draw()
    background(40, 40, 50)    
    
    -- 繪製地圖
    myMap:drawMap()
    sysInfo()
end

function touched(touch)
    myMap:touched(touch)
end


-- 使用 mesh() 繪製地圖
Maps = class()

function Maps:init()
    
    self.gridCount = 100
    self.scaleX = 40
    self.scaleY = 40
    self.plantSeed = 20.0
    self.minerialSeed = 50.0
    
    -- 根據地圖大小申請圖像
    local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY
    -- print(w,h)
    self.imgMap = image(w,h)
    
    -- 使用 mesh 繪製地圖
    -- 設置當前位置爲矩形中心點的絕對數值,分別除以 w, h 能夠獲得相對數值
    self.x, self.y = (w/2-WIDTH/2), (h/2-HEIGHT/2)
    self.m = mesh()
    self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    self.m.texture = self.imgMap
    -- 利用紋理座標設置顯示區域,根據中心點座標計算出左下角座標,除以紋理寬度獲得相對值,w h 使用固定值(小於1)
    local u,v = WIDTH/w, HEIGHT/h
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)
    
    -- 整個地圖使用的全局數據表
    self.mapTable = {}
        
    -- 設置物體名稱
    tree1,tree2,tree3 = "松樹", "楊樹", "小草"    
    mine1,mine2 = "鐵礦", "銅礦"
    
    -- 後續改用表保存物體名稱
    self.trees = {"松樹", "楊樹", "小草"}
    self.mines = {"鐵礦", "銅礦"}
        
    -- 設置物體圖像
    imgTree1 = readImage("Planet Cute:Tree Short")
    imgTree2 = readImage("Planet Cute:Tree Tall")
    imgTree3 = readImage("Platformer Art:Grass")
    imgMine1 = readImage("Platformer Art:Mushroom")
    imgMine2 = readImage("Small World:Treasure")
    
    -- 存放物體: 名稱,圖像
    self.itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}
       
    -- 尺寸爲 3*3 的數據表示例
    self.mapTable = {{pos=vec2(1,1),plant=nil,mineral=mine1},{pos=vec2(1,2),plant=nil,mineral=nil},
                {pos=vec2(1,3),plant=tree3,mineral=nil},{pos=vec2(2,1),plant=tree1,mineral=nil},
                {pos=vec2(2,2),plant=tree2,mineral=mine2},{pos=vec2(2,3),plant=nil,mineral=nil},
                {pos=vec2(3,1),plant=nil,mineral=nil},{pos=vec2(3,2),plant=nil,mineral=mine2},
                {pos=vec2(3,3),plant=tree3,mineral=nil}}
    
    print("地圖初始化開始...")
    -- 根據初始參數值新建地圖
    -- self:createMapTable()
end

function Maps:drawMap() 
    -- sprite(self.imgMap,-self.scaleX,-self.scaleY)
    -- sprite(self.imgMap,0,0)
    
    -- 更新紋理貼圖, --若是地圖上的物體有了變化
    self.m.texture = self.imgMap
    local w,h = self.imgMap.width, self.imgMap.height
    local u,v = WIDTH/w, HEIGHT/h
    -- 增長判斷,若角色移動到邊緣則切換地圖:經過修改貼圖座標來實現
    -- print(self.x,self.y)
    local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10
    local ss = 800
    if myS.x <= left then self.x= self.x - WIDTH/ss end
    if myS.x >= right then self.x= self.x + WIDTH/ss end
    if myS.y <= bottom then self.y = self.y - HEIGHT/ss end
    if myS.y >= top then self.y = self.y + HEIGHT/ss end
    
    -- 根據計算獲得的數據從新設置紋理座標
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)
    
    -- self:updateMap()
    self.m:draw()
end

function Maps:touched(touch)
    if touch.state == BEGAN then
        myS.x, myS.y = touch.x, touch.y
    end
end

-- 新建地圖數據表, 插入地圖上每一個格子裏的物體數據
function Maps:createMapTable()
    --local mapTable = {}
    for i=1,self.gridCount,1 do
        for j=1,self.gridCount,1 do
            self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()}
            --self.mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(self.mapTable, self.mapItem)
            -- myT:switchPoint(myT.taskID)
        end
    end
    print("OK, 地圖初始化完成! ")
    self:updateMap()
end

-- 根據地圖數據表, 刷新地圖,比較耗時,能夠考慮使用協程,每 1 秒內花 1/60 秒來執行它;
-- 協程還可用來實現時間系統,氣候變化,植物生長,它賦予咱們操縱遊戲世界運行流程的能力(至關於控制時間變化)
-- 或者不用循環,只執行改變的物體,傳入網格座標
function Maps:updateMap()
    setContext(self.imgMap)   
    for i = 1,self.gridCount*self.gridCount,1 do
        local pos = self.mapTable[i].pos
        local plant = self.mapTable[i].plant
        local mineral = self.mapTable[i].mineral
        -- 繪製地面
        self:drawGround(pos)
        -- 繪製植物和礦物
        if plant ~= nil then self:drawTree(pos, plant) end
        if mineral ~= nil then self:drawMineral(pos, mineral) end
    end
    setContext()
end

function Maps:touched(touch)
    if touch.state == BEGAN then
        myS.x, myS.y = touch.x, touch.y
    end
end

-- 根據像素座標值計算所處網格的 i,j 值
function Maps:where(x,y)
    local i = math.ceil((x+self.scaleX) / self.scaleX)
    local j = math.ceil((y+self.scaleY) / self.scaleY)
    return i,j
end

-- 隨機生成植物,返回值是表明植物名稱的字符串
function Maps:randomPlant()
    local seed = math.random(1.0, self.plantSeed)
    local result = nil
    
    if seed >= 1 and seed < 2 then result = tree1
    elseif seed >= 2 and seed < 3 then result = tree2
    elseif seed >= 3 and seed < 4 then result = tree3
    elseif seed >= 4 and seed <= self.plantSeed then result = nil end
    
    return result
end

-- 隨機生成礦物,返回值是表明礦物名稱的字符串
function Maps:randomMinerial()
    local seed = math.random(1.0, self.minerialSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = mine1
    elseif seed >= 2 and seed < 3 then result = mine2
    elseif seed >= 3 and seed <= self.minerialSeed then result = nil end
    
    return result
end

function Maps:getImg(name)
    return self.itemTable[name]
end

-- 重置  
function Maps:resetMapTable()
    self.mapTable = self:createMapTable()
end

-- 繪製單位格子地面
function Maps:drawGround(position)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    strokeWidth(1)
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,self.scaleX,self.scaleY)
    --sprite("Documents:3D-Wall",x,y,scaleX,scaleY)
    popMatrix()
end

-- 繪製單位格子內的植物
function Maps:drawTree(position,plant)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    -- 繪製植物圖像
    sprite(self.itemTable[plant],x,y,self.scaleX*6/10,self.scaleY)
    
    --fill(100,100,200,255)
    --text(plant,x,y)
    popMatrix()
end

-- 繪製單位格子內的礦物
function Maps:drawMineral(position,mineral)
    local x,y = self.scaleX * position.x, self.scaleY * position.y
    pushMatrix()
    -- 繪製礦物圖像
    sprite(self.itemTable[mineral],x+self.scaleX/2,y,self.scaleX/2,self.scaleX/2)

    --fill(100,100,200,255)
    --text(mineral,x+self.scaleX/2,y)
    popMatrix()
end

再改寫第一層面

如今開始把第一層面改寫爲用 mesh 繪圖, 也就是說以 mesh 方式來生成大地圖, 具體來講就是改寫這些函數:

  • Maps:updateMap() 負責把全部的繪製函數整合起來, 繪製出整副地圖
  • Maps:drawGround() 負責繪製單位格子地面
  • Maps:drawTree() 負責繪製單位格子內的植物
  • Maps:drawMineral() 負責繪製單位格子內的礦物

這裏稍微麻煩一些, 由於咱們打算用小紋理貼圖來拼接, 因此一旦小紋理肯定, 那麼這些屬性就不須要顯式指定了:

  • self.scaleX = 40
  • self.scaleY = 40

它們實際上就是小紋理貼圖的 寬度高度, 假設使用名爲 tex 的小紋理, 那麼這兩個值就分別是 tex.widthtex.height, 雖然咱們通常提倡使用正方形的紋理, 不過這裏仍是區分了 寬度高度.

而矩形的大小, 則能夠經過屬性 self.gridCount = 100 來設定須要用到多少塊小紋理, 這裏設置的是 100, 表示橫向使用 100 塊小紋理, 縱向使用 100 塊小紋理.

看起來此次改寫涉及的地方比較多.

具體實現方法

這裏仍是經過 mesh 的紋理貼圖功能來實現, 不過跟在第一層面的用法不一樣, 這裏咱們會使用很小的紋理貼圖, 好比大小爲 50*50 像素單位, 經過紋理座標的設置和 shader 把它們拼接起來鋪滿整個地圖, 之因此要用到 shader, 是由於在這裏, 咱們提供紋理座標的取值大於 [0,1] 的範圍, 必須在 shader 中對紋理座標作一個轉換, 讓它們從新落回到 [0,1] 的區間.

好比假設咱們程序提供的紋理座標是 (23.4, 20.8), 前面的整數部分 (23, 20) 表明的都是整塊的紋理圖, 至關於橫向有 23 個貼圖, 縱向有 20 個貼圖, 那麼剩下的小數部分 (0.4, 0.8) 就會落在一塊小紋理素材圖內, 這個 (0.4, 0.8) 纔是咱們真正要取的點.

繪製地面

咱們先從地面開始, 先新建一個名爲 m1mesh, 接着在這個 mesh 上新建一個大大的矩形, 簡單來講就是跟咱們的地圖同樣大, 再加載一個尺寸較小的地面紋理貼圖, 經過紋理座標的設置和 shader 的處理把它以拼圖的方式鋪滿整個矩形, 最後用函數 m1:draw() 把它繪製到 self.img 上, 不過爲方便調試, 咱們先臨時增長一個屬性 self.img1, 全部改寫部分先在它上面繪製, 調試無誤後再繪製到 self.imgMap1 上.

初始化函數 Maps:init() 中須要增長的代碼

-- 使用 mesh 繪製第一層面的地圖 
    self.m1 = mesh()
    self.m1.texture = readImage("Documents:3D-Wall")
    local tw,th = self.m1.texture.width, self.m1.texture.height
    local mw,mh = (self.gridCount+1)*tw, (self.gridCount+1)*th
    -- 臨時調試用, 調試經過後刪除
    self.imgMap1 = image(mw, mh)
    -- local ws,hs = WIDTH/tw, HEIGHT/th
    local ws,hs = mw/tw, mh/th
    print(ws,hs)
    self.m1i = self.m1:addRect(mw/2, mh/2, mw, mh)
    self.m1:setRectTex(self.m1i, 1/2, 1/2, ws, hs)
    -- 使用拼圖 shader
    self.m1.shader = shader(shaders["maps"].vs,shaders["maps"].fs)

由於須要修改的地方較多, 爲避免引入新問題, 因此保留原來的處理, 臨時增長几個函數, 專門用於調試:

-- 臨時調試用
function Maps:updateMap1()
    setContext(self.imgMap)   
    m1:draw()
    setContext()
end

另外須要在增長一個專門用於拼圖的 shader, 把小塊紋理圖拼接起來鋪滿:

-- Shader
shaders = {

maps = { 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()
{
    vec4 col = texture2D(texture,vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));
    gl_FragColor = vColor * col;
}
]]}
}

修改 mapTable 的結構

原來咱們的 mapTable 是一個一維數組, 如今把它改成二維數組, 這樣在知道一個網格的座標 i, j 後能夠很快地查找出該網格在數據表中的信息 mapTable[i][j], 很是方便對地圖中的物體(植物/礦物)進行操做, 首先是改寫地圖數據表生成函數 Maps:createMapTable(), 這裏須要注意的一點是 用 Luatable 實現二維數組時, 須要顯示地建立每一行, 改成以下:

function Maps:createMapTable()
    --local mapTable = {}
    for i=1,self.gridCount,1 do
        self.mapTable[i] = {}
        for j=1,self.gridCount,1 do
            self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()}
            table.insert(self.mapTable[i], self.mapItem)
            -- self.mapTable[i][j] = self.mapItem
            -- myT:switchPoint(myT.taskID)
        end
    end
    print("OK, 地圖初始化完成! ")  
    self:updateMap1()
end

也能夠這樣 self.mapTable[i][j] = self.mapItem 來爲數組的每一個位置賦值.

修改了數據表結構後, 不少針對數據表的相關操做也要作對應修改, 如 Maps:updateMap() 函數:

function Maps:updateMap()
    setContext(self.imgMap)   
    -- 用 mesh 繪製地面
    self.m1:draw()
    -- 用 sprite 繪製植物,礦物,建築
    for i = 1,self.gridCount,1 do
        for j=1,self.gridCount,1 do
            local pos = self.mapTable[i][j].pos
            local plant = self.mapTable[i][j].plant
            local mineral = self.mapTable[i][j].mineral
            -- 繪製植物和礦物
            if plant ~= nil then self:drawTree(pos, plant) end
            if mineral ~= nil then self:drawMineral(pos, mineral) end
        end
    end
    setContext()
end

還有其餘幾個函數就不一一列舉了, 由於修改的地方很清晰.

增長一些用於交互的函數

這個遊戲程序寫了這麼久了, 玩家控制的角色尚未真正對地圖上的物體作過交互, 這裏咱們增長几個用於操做地圖上物體的函數:

首先提供一個查看對應網格信息的函數 Maps:showGridInfo():

function Maps:showGridInfo(i,j)
    local item = self.mapTable[i][j]    
    print(item.pos, item.tree, item.mineral)
    if item.tree ~= nil then 
        fill(0,255,0,255)
        text(item.pos.."位置處有: "..item.tree.." 和 ..", 500,200)
    end
end

而後是一個刪除物體的函數 Maps:removeMapObject():

function Maps:removeMapObject(i,j)
    local item = self.mapTable[i][j] 
    if item.pos == vec2(i,j) then 
        item.plant = nil 
        item.mineral = nil 
    end
end

咱們以前寫過一個根據座標數值換算對應網格座標的函數 ``, 如今須要改寫一下, 把計算單位換成小紋理貼圖的寬度和高度:

function Maps:where(x,y)
    local w, h = self.m1.texture.width, self.m1.texture.height
    local i, j = math.ceil(x/w), math.ceil(y/h)
    return i,j
end

還存在點小問題, 精度須要提高, 後續改進.

繪製植物

要修改函數 Maps:drawTree(), 原來是根據 self.scaleX, self.scaleY 和網格座標 i, j 來計算繪製到哪一個格子上的, 如今由於地面改用 mesh 的紋理貼圖繪製, 因此就要用地面紋理貼圖的 width, height 來計算了.

-- 臨時調試用
function Maps:drawTree(position,plant) 
    local w, h = self.m1.texture.width, self.m1.texture.height
    local x, y =  w * position.x, h * position.y
    print("tree:"..x..y)
    pushMatrix()
    -- 繪製植物圖像
    sprite(self.itemTable[plant],x,y,w*6/10,h)
    popMatrix()
end

繪製礦物

一樣須要修改的還有 Maps:drawMineral() 函數:

function Maps:drawMineral(position,mineral)
    local w, h = self.m1.texture.width, self.m1.texture.height
    local x, y = w * position.x, h * position.y
    pushMatrix()
    -- 繪製礦物圖像
    sprite(self.itemTable[mineral], x+w/2, y , w/2, h/2)
    --fill(100,100,200,255)
    --text(mineral,x+self.scaleX/2,y)
    popMatrix()
end

通過上面這些改動, 基本上是完成了, 不過刪除地圖上的物體後, 須要重繪地圖, 若是把數據表 mapTable 全都遍歷一遍, 至關於整副地圖都重繪一遍, 顯然沒這個必要, 因此咱們打算只重繪那些被刪除了物體的網格, 由於知道確切座標, 因此咱們能夠用這樣一個函數來實現:

--局部重繪函數
function Maps:updateItem(i,j)
    setContext(self.imgMap)
    local x,y = i * self.m1.texture.width, j * self.m1.texture.height
    sprite(self.m1.texture, x, y)
    setContext()
    self.m.texture = self.imgMap
end

完整代碼

-- c06-02.lua

-- 主程序框架
function setup()
    displayMode(OVERLAY)

    -- 角色位置,用於調試
    myS = {}
    myS.x, myS.y = WIDTH/2, HEIGHT/2
    
    -- 生成地圖
    myMap = Maps()
    myMap:createMapTable()
    print("左下角在地圖的座標:"..myMap.x,myMap.y)
    local i,j = myMap:where(myMap.x,myMap.y)
    print("左下角對應網格座標:"..i.." : "..j)
    -- print(myMap.mapTable[9][10].pos, myMap.mapTable[9][10].plant)
    -- 測試格子座標計算
    ss = ""
end

function draw()
    background(40, 40, 50)    
    
    -- 繪製地圖
    myMap:drawMap()
    sysInfo()  
    
    -- 顯示點擊處的格子座標
    fill(255, 0, 14, 255)
    -- text(ss,500,100)
end

function touched(touch)
    myMap:touched(touch)
    
    if touch.state == ENDED then
    c1,c2 = myMap:where(myMap.x + touch.x, myMap.y + touch.y)
    myMap:showGridInfo(c1,c2)
    myMap:removeMapObject(c1,c2)
    print("點擊處的座標絕對值:", (myMap.x + touch.x)/200, (myMap.y + touch.y)/200)
    print("c1:c2 "..c1.." : "..c2) 
    
    ss = c1.." : "..c2
    end
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


-- 使用 mesh() 繪製地圖
Maps = class()

function Maps:init()
    
    self.gridCount = 20
    self.scaleX = 200
    self.scaleY = 200
    self.plantSeed = 20.0
    self.minerialSeed = 50.0
    
    -- 根據地圖大小申請圖像,scaleX 可實現縮放物體
    --local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY
    local w,h = (self.gridCount+0)*self.scaleX, (self.gridCount+0)*self.scaleY
    print("大地圖尺寸: ",w,h)
    self.imgMap = image(w,h)
    
    -- 使用 mesh 繪製第一層面的地圖地面  
    self.m1 = mesh()
    self.m1.texture = readImage("Documents:hm1")
    local tw,th = self.m1.texture.width, self.m1.texture.height
    local mw,mh = (self.gridCount+1)*tw, (self.gridCount+1)*th
    -- 臨時調試用, 調試經過後刪除
    self.imgMap1 = image(mw, mh)
    -- local ws,hs = WIDTH/tw, HEIGHT/th
    local ws,hs = mw/tw, mh/th
    print("網格數目: ",ws,hs)
    self.m1i = self.m1:addRect(mw/2, mh/2, mw, mh)
    self.m1:setRectTex(self.m1i, 1/2, 1/2, ws, hs)
    -- 使用拼圖 shader
    self.m1.shader = shader(shaders["maps"].vs,shaders["maps"].fs)
    
    -- 使用 mesh 繪製第二層面的地圖
    -- 屏幕左下角(0,0)在大地圖上對應的座標值(1488, 1616)
    -- 設置屏幕當前位置爲矩形中心點的絕對數值,分別除以 w, h 能夠獲得相對數值
    self.x, self.y = (w/2-WIDTH/2), (h/2-HEIGHT/2)
    self.m = mesh()
    self.mi = self.m:addRect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    self.m.texture = self.imgMap
    -- 利用紋理座標設置顯示區域,根據中心點座標計算出左下角座標,除以紋理寬度獲得相對值,w h 使用固定值(小於1)
    -- 這裏計算獲得的是大地圖中心點處的座標,是遊戲剛開始運行的座標
    local u,v = WIDTH/w, HEIGHT/h
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)
    
    -- 整個地圖使用的全局數據表
    self.mapTable = {}
        
    -- 設置物體名稱
    tree1,tree2,tree3 = "松樹", "楊樹", "小草"    
    mine1,mine2 = "鐵礦", "銅礦"
    
    imgTree1 = readImage("Planet Cute:Tree Short")
    imgTree2 = readImage("Planet Cute:Tree Tall")
    imgTree3 = readImage("Platformer Art:Grass")
    imgMine1 = readImage("Platformer Art:Mushroom")
    imgMine2 = readImage("Small World:Treasure")
                 
    -- 後續改用表保存物體名稱
    self.trees = {"松樹", "楊樹", "小草"}
    self.mines = {"鐵礦", "銅礦"}
        
    -- 設置物體圖像  
    self.items = {imgTree1 = readImage("Planet Cute:Tree Short"),
                 imgTree2 = readImage("Planet Cute:Tree Tall"),
                 imgTree3 = readImage("Platformer Art:Grass"),
                 imgMine1 = readImage("Platformer Art:Mushroom"),
                 imgMine2 = readImage("Small World:Treasure")}
    
    -- 存放物體: 名稱,圖像
    self.itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}
    
    --[=[
    self.itemTable = {[self.trees[1]].self.items["imgTree1"],[self.trees[2]].self.items["imgTree2"],
                      [self.trees[3]].self.items["imgTree3"],[self.mines[1]].self.items["imgMine1"],
                      [self.mines[3]].self.items["imgMine2"]}   
    --]=]   
    
    --[[ 尺寸爲 3*3 的數據表示例,連續
    self.mapTable = {{{pos=vec2(1,1),plant=nil,mineral=mine1},{pos=vec2(1,2),plant=nil,mineral=nil},
                     {pos=vec2(1,3),plant=tree3,mineral=nil}},{{pos=vec2(2,1),plant=tree1,mineral=nil},
                     {pos=vec2(2,2),plant=tree2,mineral=mine2},{pos=vec2(2,3),plant=nil,mineral=nil}},
                     {{pos=vec2(3,1),plant=nil,mineral=nil},{pos=vec2(3,2),plant=nil,mineral=mine2},
                     {pos=vec2(3,3),plant=tree3,mineral=nil}}}
    --]]
    
    print("地圖初始化開始...")
    -- 根據初始參數值新建地圖
    -- self:createMapTable()
end

-- 新建地圖數據表, 插入地圖上每一個格子裏的物體數據
function Maps:createMapTable()
    --local mapTable = {}
    for i=1,self.gridCount,1 do
        self.mapTable[i] = {}
        for j=1,self.gridCount,1 do
            self.mapItem = {pos=vec2(i,j), plant=self:randomPlant(), mineral=self:randomMinerial()}
            table.insert(self.mapTable[i], self.mapItem)
            -- self.mapTable[i][j] = self.mapItem
            -- myT:switchPoint(myT.taskID)
        end
    end
    print("OK, 地圖初始化完成! ")  
    self:updateMap()
end

-- 更新整副地圖:繪製地面, 繪製植物, 繪製礦物
function Maps:updateMap()
    setContext(self.imgMap)   
    -- 用 mesh 繪製地面
    self.m1:draw()
    -- 用 sprite 繪製植物,礦物,建築
    for i = 1,self.gridCount,1 do
        for j=1,self.gridCount,1 do
            local pos = self.mapTable[i][j].pos
            local plant = self.mapTable[i][j].plant
            local mineral = self.mapTable[i][j].mineral
            -- 繪製植物和礦物
            if plant ~= nil then self:drawTree(pos, plant) end
            if mineral ~= nil then self:drawMineral(pos, mineral) end
        end
    end
    setContext()
end

function Maps:drawMap() 
    -- 更新紋理貼圖, --若是地圖上的物體有了變化
    self.m.texture = self.imgMap
    local w,h = self.imgMap.width, self.imgMap.height
    local u,v = WIDTH/w, HEIGHT/h
    -- 增長判斷,若角色移動到邊緣則切換地圖:經過修改貼圖座標來實現
    -- print(self.x,self.y)
    local left,right,top,bottom = WIDTH/10, WIDTH*9/10, HEIGHT/10, HEIGHT*9/10
    local ss = 800
    if myS.x <= left then self.x= self.x - WIDTH/ss end
    if myS.x >= right then self.x= self.x + WIDTH/ss end
    if myS.y <= bottom then self.y = self.y - HEIGHT/ss end
    if myS.y >= top then self.y = self.y + HEIGHT/ss end
    
    -- 根據計算獲得的數據從新設置紋理座標
    self.m:setRectTex(self.mi, self.x/w, self.y/h, u, v)
    
    -- self:updateMap()
    self.m:draw()
end

function Maps:touched(touch)
    if touch.state == BEGAN then
        myS.x, myS.y = touch.x, touch.y
    end
end

--局部重繪函數
function Maps:updateItem(i,j)
    setContext(self.imgMap)
    local x,y = i * self.m1.texture.width, j * self.m1.texture.height
    sprite(self.m1.texture, x, y)
    setContext()
    self.m.texture = self.imgMap
end

-- 根據像素座標值計算所處網格的 i,j 值
function Maps:where(x,y)
    local w, h = self.m1.texture.width, self.m1.texture.height
    local i, j = math.ceil(x/w), math.ceil(y/h)
    return i, j
end

-- 角色跟地圖上物體的交互
function Maps:removeMapObject(i,j)
    local item = self.mapTable[i][j] 
    if item.pos == vec2(i,j) then 
        item.plant = nil 
        item.mineral = nil 
        self:updateItem(i,j)
    end
end

-- 顯示網格內的物體信息
function Maps:showGridInfo(i,j)
    local item = self.mapTable[i][j]
    
    print("showGridInfo: ", item.pos, item.tree, item.mineral)
    if item.tree ~= nil then 
        fill(0,255,0,255)
        text(item.pos.."位置處有: ", item.tree, 500,200)
    end
end

-- 隨機生成植物,返回值是表明植物名稱的字符串
function Maps:randomPlant()
    local seed = math.random(1.0, self.plantSeed)
    local result = nil
    
    if seed >= 1 and seed < 2 then result = tree1
    elseif seed >= 2 and seed < 3 then result = tree2
    elseif seed >= 3 and seed < 4 then result = tree3
    elseif seed >= 4 and seed <= self.plantSeed then result = nil end
    
    return result
end

-- 隨機生成礦物,返回值是表明礦物名稱的字符串
function Maps:randomMinerial()
    local seed = math.random(1.0, self.minerialSeed)
    local result = nil

    if seed >= 1 and seed < 2 then result = mine1
    elseif seed >= 2 and seed < 3 then result = mine2
    elseif seed >= 3 and seed <= self.minerialSeed then result = nil end
    
    return result
end

function Maps:getImg(name)
    return self.itemTable[name]
end

-- 重置  
function Maps:resetMapTable()
    self.mapTable = self:createMapTable()
end

-- 繪製單位格子內的植物
function Maps:drawTree(position,plant) 
    local w, h = self.m1.texture.width, self.m1.texture.height
    local x,y =  w * position.x, h * position.y
    -- print("tree:"..x.." : "..y)
    pushMatrix()
    -- 繪製植物圖像
    sprite(self.itemTable[plant], x, y, w*6/10, h)
    
    --fill(100,100,200,255)
    --text(plant,x,y)
    popMatrix()
end

-- 繪製單位格子內的礦物
function Maps:drawMineral(position,mineral)
    local w, h = self.m1.texture.width, self.m1.texture.height
    local x, y = w * position.x, h * position.y
    pushMatrix()
    -- 繪製礦物圖像
    sprite(self.itemTable[mineral], x+w/2, y , w/2, h/2)

    --fill(100,100,200,255)
    --text(mineral,x+self.scaleX/2,y)
    popMatrix()
end


-- Shader
shaders = {
maps = { 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()
{
    vec4 col = texture2D(texture,vec2(mod(vTexCoord.x,1.0), mod(vTexCoord.y,1.0)));
    gl_FragColor = vColor * col;
}
]]}
}

整合好的代碼

跟幀動畫整合在一塊兒的代碼在這裏: c06.lua

如今咱們能夠方便地更換地面紋理貼圖, 看看這兩個不一樣的貼圖效果:

在地圖上用 shader 增長特效

到目前爲止, 咱們對地圖類的改寫基本完成, 調試經過後, 剩下的就是利用 shader 來爲地圖增長一些特效了.

原本打算寫寫下面這些特效:

氣候變化

下雨,下雪,雷電,迷霧,狂風

季節變化

春夏秋冬四季變化

晝夜變化

光線隨時間改變明暗程度

流動的河流

讓河流動起來

波光粼粼的湖泊

湖泊表面閃爍

樹木(可以使用廣告牌-在3D階段實現)

用廣告牌實現的樹木

地面凹凸陰影(2D 和 3D)

讓地面產生動態陰影變化

天空盒子(3D)

搞一個立方體紋理特貼圖

可是一看本章已經寫了太長的篇幅了, 因此決定把這些內容放到後面單列一章, 所以本章到此結束.

本章小結

本章成功實現了以下目標:

  • mesh 繪製地圖, 用 mesh 顯示地圖
  • 利用 mesh 的紋理座標機制解決了地圖自動捲動
  • 增長了用戶跟地圖物體的交互處理
  • 爲後續的地圖特效提供了 shader.

臨時想到的問題, 後續解決:

  • 利用生命遊戲的規則, 讓隨機生成的植物演化一段時間, 以便造成更具真實感的羣落
  • 須要解決走到地圖盡頭的問題, 加一個處理, 讓圖片首尾銜接

全部章節連接

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$
相關文章
相關標籤/搜索