6-用GPU提升效能(2)-從零開始寫一個武俠冒險遊戲

自由布魯斯發表於2016-06-21

從零開始寫一個武俠冒險遊戲-6-用GPU提升效能(2)

概述

mesh 改寫地圖類, 帶來的一大好處是控制邏輯可以變得非常簡單, 作為一個地圖類, 最基本的控制邏輯就是顯示哪一部分和地圖如何捲動, 而這兩點可以通過 mesh 的紋理貼圖非常容易地解決, 因為在 OpenGL ES 2.0/3.0 中, 可以通過設定紋理座標來決定如何在地圖上顯示紋理貼圖, 而這些控制邏輯如果不用 mesh, 自己去寫, 就有些繁瑣了, 不信你可以試試.

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

基於 OpenGL ES 2.0/3.0 的紋理貼圖特性, 我們既可以使用一塊很小的紋理, 然後用拼圖的方式把大螢幕鋪滿, 也可以使用一塊很大的超出螢幕範圍的圖片做紋理, 然後選擇其中一個尺寸跟螢幕尺寸相當的區域來顯示.

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

用 mesh 改寫地圖類

整體思路

地圖類的處理相對來說複雜一些, 正如我們在 概述 中提到的, 要在兩個層面使用 mesh, 第一層是用小素材紋理通過拼圖的方式生成一張超過螢幕尺寸的大地圖圖片, 第二層是把這張大地圖圖片作為紋理素材, 通過紋理座標的設定來從大地圖圖片素材中選擇一個尺寸剛好是螢幕大小的區域, 然後把它顯示在螢幕上.

先改寫第二層面

因為我們是前面的基礎上改寫, 也就是說用來生成大地圖圖片的程式碼已經寫好了, 所以我們可以選擇先從簡單的開始, 那就是先實現第二層面: 用大圖片作為紋理貼圖, 利用 mesh 的紋理座標來實現顯示小區域和地圖捲動等功能.

具體實現方法

具體辦法就是先在初始化函式 Maps:init() 中用 mesh:addRect() 新建一個螢幕大小的矩形, 然後載入已經生成的大地圖圖片作為紋理貼圖, 再通過設定紋理座標 mesh:setRectTex(i, x, y, w, t) 取得對應於紋理貼圖上的一塊螢幕大小的區域; 然後再在 Maps:drawMap() 函式中根據角色移動來判斷是否需要捲動地圖, 以及如果需要捲動向哪個方向捲動, 最後在 Maps:touched(touch) 函式中把紋理座標的 (x, y) 跟觸控資料關聯起來, 這樣我們螢幕上顯示的地圖就會隨著角色移動到螢幕邊緣而自動把新地圖平移過來.

程式碼說明

在初始化函式 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

現在我們可以方便地更換地面紋理貼圖, 看看這兩個不同的貼圖效果:

截圖1]

截圖2

在地圖上用 shader 增加特效

到目前為止, 我們對地圖類的改寫基本完成, 除錯通過後, 剩下的就是利用 shader 來為地圖增加一些特效了.

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

氣候變化

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

季節變化

春夏秋冬四季變化

晝夜變化

光線隨時間改變明暗程度

流動的河流

讓河流動起來

波光粼粼的湖泊

湖泊表面閃爍

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

用廣告牌實現的樹木

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

讓地面產生動態陰影變化

天空盒子(3D)

搞一個立方體紋理特貼圖

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

本章小結

本章成功實現瞭如下目標:

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

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

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

所有章節連結

Github專案地址

Github專案地址, 原始碼放在 src/ 目錄下, 圖片素材放在 assets/ 目錄下, 整個專案檔案結構如下:

Air:Write-A-Adventure-Game-From-Zero admin$ tree
.
├── README.md
├── Vim 列編輯功能詳細講解.md
├── assets
│   ├── IMG_0097.PNG
│   ├── IMG_0099.JPG
│   ├── IMG_0100.PNG
│   ├── c04.mp4
│   ├── cat.JPG
│   └── runner.png
├── src
│   ├── c01.lua
│   ├── c02.lua
│   ├── c03.lua
│   ├── c04.lua
│   ├── c05.lua
│   ├── c06-01.lua
│   ├── c06-02.lua
│   └── c06.lua
├── 從零開始寫一個武俠冒險遊戲-0-開發框架Codea簡介.md
├── 從零開始寫一個武俠冒險遊戲-1-狀態原型.md
├── 從零開始寫一個武俠冒險遊戲-2-幀動畫.md
├── 從零開始寫一個武俠冒險遊戲-3-地圖生成.md
├── 從零開始寫一個武俠冒險遊戲-4-第一次整合.md
├── 從零開始寫一個武俠冒險遊戲-5-使用協程.md
├── 從零開始寫一個武俠冒險遊戲-6-用GPU提升效能(1).md
└── 從零開始寫一個武俠冒險遊戲-6-用GPU提升效能(2).md

2 directories, 24 files
Air:Write-A-Adventure-Game-From-Zero admin$ 

開源中國專案文件連結

從零開始寫一個武俠冒險遊戲-1-狀態原型
從零開始寫一個武俠冒險遊戲-2-幀動畫
從零開始寫一個武俠冒險遊戲-3-地圖生成
從零開始寫一個武俠冒險遊戲-4-第一次整合
從零開始寫一個武俠冒險遊戲-5-使用協程
從零開始寫一個武俠冒險遊戲-6-用GPU提升效能(1)
從零開始寫一個武俠冒險遊戲-6-用GPU提升效能(2)

相關文章