從零開始寫一個武俠冒險遊戲-3-地圖生成

從零開始寫一個武俠冒險遊戲-3-地圖生成

概述

前面兩章咱們設計了角色的狀態, 繪製出了角色, 而且賦予角色動做, 如今是時候爲咱們的角色創造一個舞臺了, 那就是遊戲地圖(咱們目前作的是一個2D 遊戲, 所以叫地圖, 若是是 3D, 則叫地形).數據庫

地圖生成也是遊戲開發的一項基本技術, 涉及到方方面面的技能, 並且地圖的數據結構要考慮到遊戲裏的其餘景物跟角色的顯示和交互, 對於整個遊戲程序的效率起着決定性的影響, 不過咱們這裏先解決有沒有的問題, 目標不過高, 能流暢運行就能夠了.數據結構

最簡原型

跟咱們一貫提倡的大思路一致, 一切從簡出發, 先弄個原型跑起來再說.框架

經驗之談: 不少開發過程當中的難題都是由於咱們一開始就引入了過於複雜的問題, 制定了太大的目標, 試圖一開始就把方方面面都考慮到, 結果無形中就增長了難度, 不得不認可, 這種頂層設計的思路是不太符合事物發展的規律的, 也不符合生物的進化規律, 因此實現起來就比較困難, 若是咱們遵循從簡單到複雜, 從原型到成品的開發思路, 就會發現開發過程變得順利不少.dom

遊戲地圖原理

簡單說來, 遊戲地圖有兩個層面, 一個是顯示到屏幕上的圖形圖像, 一個是隱藏在圖像後面的數據結構, 前者是遊戲跟玩家交互的界面, 後者是遊戲中繪製出來的各類對象跟程序交互的接口.函數

好比玩家操縱一個遊戲角色從左邊一個位置走到右邊一個位置, 玩家看到的是屏幕上角色的移動過程, 而程序在後面要記錄玩家每時每刻的座標, 以及該座標在地圖上對應的位置.性能

若是玩家看到地圖上某個位置有一個能夠操做的物體, 好比一個箱子, 玩家的角色想要靠近這個箱子而後打開它, 那麼後臺的地圖數據庫裏首先要在地圖的某個位置上有一個箱子, 而後再判斷角色距離箱子的距離, 若是小於某個值, 那麼就說明容許操做, 玩家開過箱子後, 還要把箱子的當前狀態(已開啓)再寫回到數據庫裏, 等等諸如此類.動畫

最簡單的地圖

最簡單的地圖就是一張事先畫好的圖, 角色在這張圖上移來移去, 這個功能咱們在第2章就已經實現了, 可是按照這種方法實現的地圖角色很難跟地圖上的物體進行交互, 並且使用事先畫好的圖作地圖還有一個問題就是若是整個遊戲場景比較大的話就須要不少畫預先存儲到遊戲中, 這樣會致使較大的體積..net

因此, 咱們採起另外一種作法, 由於遊戲場景中不少物體對象都是能夠重複使用的, 好比樹木, 岩石等等, 因此咱們能夠把這些基本對象提取出來事先繪製好, 或者使用預先作好的素材, 這樣咱們須要事先存儲的內容就大大減小了, 而後再根據實際須要動態繪製上去, 這就是隨機生成場景地圖的作法.設計

剛好我以前寫過一個簡單的隨機地圖生成器, 雖然比較簡陋, 不過爲了減小工做量, 仍是能夠拿來用用的, 固然, 直接用是不行的, 主要是以它作一個基礎來進行改寫.code

原型目標

首先明確一下咱們這個地圖原型的基本需求點:

  • 能夠靈活調整地圖大小
  • 能夠隨機插入樹木/礦物/建築等固定物體
  • 角色能夠跟地圖上的這些物體交互

這是三個最基本的需求, 咱們一步一步來實現這三個需求.

格子地面地圖

綜合性能和實現難度方面的考慮, 咱們的地圖以網格的形式進行繪製和保存, 也就是以咱們以前寫好的那個隨機地圖生成器爲基本原型, 這樣一方面能夠靈活控制數據表的大小, 數據表中存儲的最小單位就是一個預先設定好大小的格子, 另外一方面寫起來也比較簡單, 還有不錯的效率表現.

首先肯定咱們的初始化參數和數據結構, 用這個函數來實現:

function initParams()
    print("Simple Map Sample!!")
    textMode(CORNER)
    spriteMode(CORNER)

    --[[
    gridCount:網格數目,範圍:1~100,例如,設爲3則生成3*3的地圖,設爲100,則生成100*100的地圖。
    scaleX:單位網格大小比例,範圍:1~100,該值越小,則單位網格越小;該值越大,則單位網格越大。
    scaleY:同上,若與scaleX相同則單位網格是正方形格子。
    plantSeed:植物生成概率,範圍:大於4的數,該值越小,生成的植物越多;該值越大,生成的植物越少。
    minerialSeed:礦物生成概率,範圍:大於3的數,該值越小,生成的礦物越多;該值越大,生成的礦物越少。
    --]]
    gridCount = 50
    scaleX = 50
    scaleY = 50
    plantSeed = 20.0
    minerialSeed = 50.0

    -- 根據地圖大小申請圖像
    local w,h = (gridCount+1)*scaleX, (gridCount+1)*scaleY
    imgMap = image(w,h)

    -- 整個地圖使用的全局數據表
    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")

    -- 存放物體: 名稱,圖像
    itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}

    -- 3*3 
    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}}

end

接下來是繪製地面單位格子的函數, 如今是在每一個格子上繪製一個矩形, 參數 position 是一個二維向量, 形如 vec(1,2) 則表示該格子位於第1行, 第2列, 代碼以下:

-- 繪製單位格子地面
function drawUnitGround(position)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    -- 網格線寬度
    strokeWidth(1)
    -- 地面顏色
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,scaleX,scaleY)
    popMatrix()
end

用這兩個函數來調用它:

-- 新建地圖數據表, 插入地圖上每一個格子裏的物體數據
function createMapTable()
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 更新地圖
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        -- 繪製地面
        drawUnitGround(pos)
    end
    setContext()
end

-- 繪製地圖
function drawMap() 
    -- 繪製地圖
    sprite(imgMap,-scaleX,-scaleY)
end

最基本原型的完整代碼

下面咱們把實現這個最基本原型的完整代碼列出來:

-- MapSample

-- 初始化地圖參數
function initParams()
    print("地圖初始化開始...")
    textMode(CORNER)
    spriteMode(CORNER)

    --[[ 參數說明:
    gridCount:網格數目,範圍:1~100,例如,設爲3則生成3*3的地圖,設爲100,則生成100*100的地圖。
    scaleX:單位網格大小比例,範圍:1~100,該值越小,則單位網格越小;該值越大,則單位網格越大。
    scaleY:同上,若與scaleX相同則單位網格是正方形格子。
    plantSeed:植物生成概率,範圍:大於4的數,該值越小,生成的植物越多;該值越大,生成的植物越少。
    minerialSeed:礦物生成概率,範圍:大於3的數,該值越小,生成的礦物越多;該值越大,生成的礦物越少。
    --]]
    gridCount = 50
    scaleX = 50
    scaleY = 50
    plantSeed = 20.0
    minerialSeed = 50.0

    -- 根據地圖大小申請圖像
    local w,h = (gridCount+1)*scaleX, (gridCount+1)*scaleY
    imgMap = image(w,h)

    -- 整個地圖使用的全局數據表
    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")

    -- 存放物體: 名稱,圖像
    itemTable = {[tree1]=imgTree1,[tree2]=imgTree2,[tree3]=imgTree3,[mine1]=imgMine1,[mine2]=imgMine2}

    -- 3*3 
    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}}

end

-- 新建地圖數據表, 插入地圖上每一個格子裏的物體數據
function createMapTable()
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 跟據地圖數據表, 刷新地圖
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        -- 繪製地面
        drawUnitGround(pos)
    end
    setContext()
end

-- 繪製單位格子地面
function drawUnitGround(position)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    stroke(99, 94, 94, 255)
    -- 網格線寬度
    strokeWidth(1)
    -- 地面顏色
    fill(5,155,40,255)
    -- fill(5,155,240,255)
    rect(x,y,scaleX,scaleY)
    popMatrix()
end

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

    initParams()
end

function draw()
    background(40, 40, 50)    

    -- 繪製地圖
    drawMap()
end

看看截圖:

只有地面的地圖原型

很好, 基本的格子地圖寫好了, 接着咱們來解決在格子地圖上隨機插入樹木/礦物/建築等固定物體的功能.

插入物體

由於咱們已經在設計數據表時就考慮到了要插入固定物體, 因此如今須要作的就是寫幾個相關的函數, 首先是兩個隨機選取物體名字的函數:

-- 隨機生成植物
function randomPlant()
    local seed = math.random(1.0, 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 <= plantSeed then result = nil end

    -- 返回隨機選取的物體名字
    return result
end

-- 隨機生成礦物
function randomMinerial()
    local seed = math.random(1.0, 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 <= minerialSeed then result = nil end

    -- 返回隨機選取的物體名字
    return result
end

而後增長兩個繪製函數, 來繪製出物體的圖像:

-- 繪製單位格子內的植物
function drawUnitTree(position,plant)
    local x,y = scaleX * position.x, scaleY * position.y
    pushMatrix()
    -- 繪製植物圖像
    sprite(itemTable[plant], x, y, scaleX*6/10,scaleY)

    --fill(100,100,200,255)
    --text(plant,x,y)
    popMatrix()
end

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

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

最後須要修改函數 createMapTable()updateMap(), 在其中增長對 plantmineral 的處理, 修改後的代碼以下:

-- 新建地圖數據表, 插入地圖上每一個格子裏的物體數據, 目前爲 plant  和 mineral 爲空
function createMapTable()
    --local mapTable = {}
    for i=1,gridCount,1 do
        for j=1,gridCount,1 do
            mapItem = {pos=vec2(i,j), plant=randomPlant(), mineral=randomMinerial()}
            --mapItem = {pos=vec2(i,j), plant=nil, mineral=nil}
            table.insert(mapTable, mapItem)
        end
    end
    updateMap()
end

-- 跟據地圖數據表, 刷新地圖
function updateMap()
    setContext(imgMap)   
    for i = 1,gridCount*gridCount,1 do
        local pos = mapTable[i].pos
        local plant = mapTable[i].plant
        local mineral = mapTable[i].mineral
        -- 繪製地面
        drawUnitGround(pos)
        -- 繪製植物和礦物
        if plant ~= nil then drawUnitTree(pos, plant) end
        if mineral ~= nil then drawUnitMineral(pos, mineral) end
    end
    setContext()
end

很是好, 第二個基本目標也完成了, 截個圖:

插入植物礦物的完整地圖

看看如今的截圖效果, 是否是感受咱們的原型正在一步步走向完善? 緊接着就要想辦法實現角色跟地圖上物體的交互了, 想作到這一點, 首先須要創建角色跟地圖在地圖數據表中的數據關聯.

創建角色跟地圖的關聯

如今地圖繪製好了, 角色也能夠自由地在地圖上活動了, 不過這只是咱們看到的表面現象, 實際在隱藏於屏幕後面的程序代碼中, 角色的位置跟地圖的座標(方格)並無創建任何關聯.

例如, 角色在地圖上看到一棵樹, 他想要對這棵樹作一些動做(觀察/澆水/砍伐 等)進行交互, 若是角色選擇了砍伐樹, 那麼最終樹被砍倒以後咱們還須要更新地圖數據表, 把對應位置的樹的圖片更換成樹根, 而實現角色跟樹的交互, 就須要根據角色位置座標跟樹的位置座標進行判斷.

咱們知道樹的位置座標已經保存在地圖的數據表中了, 可是角色的座標跟地圖的數據表尚未任何關係, 由於角色常常移動, 因此咱們能夠寫一個函數, 根據角色的屏幕像素點座標來計算所處的地圖方格座標, 代碼以下:

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

有了這個函數, 咱們只要把角色當前位置的像素點座標輸入, 就能夠獲得它所處網格的座標, 這樣就把角色跟地圖從數據層面創建了關聯. 後續就能夠方便地經過這個接口來處理他們之間的交互了.

爲方便後續代碼維護, 咱們要把上述代碼改寫爲一個地圖生成類, 改寫後的完整代碼以下:

-- MapSample

Maps = class()

function Maps:init()
    --[[
    gridCount:網格數目,範圍:1~100,例如,設爲3則生成3*3的地圖,設爲100,則生成100*100的地圖。
    scaleX:單位網格大小比例,範圍:1~100,該值越小,則單位網格越小;該值越大,則單位網格越大。
    scaleY:同上,若與scaleX相同則單位網格是正方形格子。
    plantSeed:植物生成概率,範圍:大於4的數,該值越小,生成的植物越多;該值越大,生成的植物越少。
    minerialSeed:礦物生成概率,範圍:大於3的數,該值越小,生成的礦物越多;該值越大,生成的礦物越少。
    --]]
    self.gridCount = 50
    self.scaleX = 50
    self.scaleY = 50
    self.plantSeed = 20.0
    self.minerialSeed = 50.0

    -- 根據地圖大小申請圖像
    local w,h = (self.gridCount+1)*self.scaleX, (self.gridCount+1)*self.scaleY
    self.imgMap = image(w,h)

    -- 整個地圖使用的全局數據表
    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.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()
    print("OK, 地圖初始化完成! ")
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)
        end
    end
    self:updateMap()
end

-- 根據地圖數據表, 刷新地圖
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:drawMap() 
    sprite(self.imgMap,-self.scaleX,-self.scaleY)
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

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

    myMap = Maps()
end

function draw()
    background(40, 40, 50)    

    -- 繪製地圖
    myMap:drawMap()
end

到目前爲止, 咱們在地圖生成原型章節的目標基本完成, 下一章咱們會嘗試把 狀態 , 幀動畫地圖生成 這三個模塊整合起來, 通常來講事物發展到 的階段會由量變觸發質變, 咱們這個程序也同樣, 會在此次整合以後, 從一個個零散簡陋的原型, 一躍而成一個還能看得過去的基本框架, 是否是很期待?

激動人心的新起點

事實上, 把角色屏幕位置跟地圖數據表創建關聯以後, 咱們的角色就真正存在於這個遊戲世界中了, 它能夠自由地跟地圖上的每個物體進行交互, 這意味着一個全新的激動人心的開始! 到如今爲止, 咱們遊戲世界的基本框架已經搭建起來了, 咱們能夠在這個框架上試驗本身對於武俠冒險遊戲的各類新想法.

全部章節連接

從零開始寫一個武俠練功遊戲-1-狀態原型
從零開始寫一個武俠練功遊戲-2-幀動畫
從零開始寫一個武俠練功遊戲-3-地圖生成

相關文章
相關標籤/搜索