4-第一次整合-從零開始寫一個武俠冒險遊戲

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

從零開始寫一個武俠冒險遊戲-4-第一次整合

---- 把狀態,幀動畫地圖生成整合起來

概述

前面三章我們完成了遊戲開發中的 狀態原型,幀動畫原型地圖生成原型 這三個模組, 因為都是原型, 所以以上三個模組還有很多可以改進的地方, 這些細節我們會逐步完善, 現在讓我們把這三個模組整合到一起.

程式碼整合

因為我們是一個模組一個模組以類的形式進行開發的, 之前這些模組都試驗過可以正常跑起來, 而且現階段模組之間的耦合比較小, 所以我們的整合工作就比較順利, 需要修改的只是程式主框架, 也就是 setup()draw() 兩個函式.

另外我們在操作控制方面還沒怎麼投入, 之前僅僅是在 touched(touch) 函式中寫了一點簡單的測試用程式碼, 這些工作顯然是遠遠不夠的.

開源的操縱桿類

我們目前開發的遊戲是要執行在平板電腦上的, 玩家對角色的操作都通過觸控式螢幕進行, 所以我們需要寫一個操縱桿類來封裝那些觸控函式, 好訊息是已經有人寫好了, 並且公佈了原始碼, 所以我們可以直接使用, 只要說明版權資訊即可.

這個類寫得非常簡潔明瞭, 不過我還是加了一點註釋, 操縱桿類的程式碼如下:

--# Stick
-- 操縱桿類  作者: @Jaybob
Stick = class()

function Stick:init(ratio,x,y,b,s)
    self.ratio = ratio or 1
    self.i = vec2(x or 120,y or 120)
    self.v = vec2(0,0)
    self.b = b or 180   --大圓半徑
    self.s = s or 100   --小圓半徑
    self.d = d or 50
    self.a = 0
    self.touchId = nil
    self.x,self.y = 0,0
end

function Stick:draw()
    -- 沒有 touched 函式的 Stick 類是如何找到自己對應的觸控資料的?根據點選處座標跟操縱桿的距離來判斷
    if touches[self.touchId] == nil then
        -- 迴圈取出 touches 表內的資料,比較其座標跟操縱桿的距離,若小於半徑則說明是在點選操縱桿
        for i,t in pairs(touches) do
            if vec2(t.x,t.y):dist(self.i) < self.b/2 then self.touchId = i end
        end
        self.v = vec2(0,0)
    else
        -- 根據對應於操縱桿的觸控的xy座標設定 self.v,再根據它計算夾角 self.a
        self.v = vec2(touches[self.touchId].x,touches[self.touchId].y) - self.i
        self.a = math.deg(math.atan2(self.v.y, self.v.x))
    end
    -- 根據 self.v 和 self.b 計算得到 self.t
    self.t = math.min(self.b/2,self.v:len())

    if self.t >= self.b/2 then
        self.v = vec2(math.cos(math.rad(self.a))*self.b/2,math.sin(math.rad(self.a))*self.b/2)
    end

    pushMatrix()
    fill(127, 127, 127, 100)
    -- 分別繪製大圓,小圓
    ellipse(self.i.x, self.i.y, self.b)
    ellipse(self.i.x+self.v.x, self.i.y+self.v.y, self.s)
    --print(self.v.x, self.s)
    popMatrix()
    -- 根據 ratio 重新設定 self.v/self.t   
    self.v = self.v/(self.b/2)*self.ratio
    self.t = self.t/(self.b/2)*self.ratio
    -- 根據 self.v/self.t,重新設定 self.x/self.y
    self.x, self.y = self.v.x, self.v.y
end

第一次整合的程式碼

修改後的程式碼如下:

-- 主程式框架
function setup() 
    displayMode(OVERLAY)
    -- 初始化狀態
    myStatus = Status()

    -- 以下為幀動畫程式碼
    s = -1
    fill(249, 249, 249, 255)
    imgs = {}
    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}}

    img = readImage("Documents:runner")

    m = Sprites(600,400,img,pos)

    ---[[ 初始化觸控搖桿
    touches = {}
    -- cam = Camera(pos.x,pos.y,pos.z,pos.x+look.x,look.y,pos.z+look.z)
    ls,rs = Stick(20,WIDTH-300,200),Stick(2,WIDTH-120)
    -- ls,rs = Stick(1),Stick(3,WIDTH-120)

    -- 初始化地圖
    myMap = Maps()
    ss =""
end

function draw()
    pushMatrix()
    pushStyle()
    -- spriteMode(CORNER)
    rectMode(CORNER)
    background(32, 29, 29, 255)

    -- 增加移動的背景圖: + 為右移,- 為左移
    --sprite("Documents:bgGrass",(WIDTH/2+10*s*m.i)%(WIDTH),HEIGHT/2)
    --sprite("Documents:bgGrass",(WIDTH+10*s*m.i)%(WIDTH),HEIGHT/2)
    -- sprite("Documents:bgGrass",WIDTH/2,HEIGHT/2)
    ---[[
    if ls.x ~= 0 then
        step = 10 *m.i*ls.x/math.abs(ls.x)
    else
        step = 0
    end
    --]]
    --sprite("Documents:bgGrass",(WIDTH/2 - step)%(WIDTH),HEIGHT/2)
    --sprite("Documents:bgGrass",(WIDTH - step)%(WIDTH),HEIGHT/2)

    -- 繪製地圖
    myMap:drawMap()

    -- 繪製角色幀動畫
    m:draw(50,80)
    -- sysInfo()

    -- 繪製狀態列
    myStatus:drawUI()
    --myStatus:raderGraph()

    -- 繪製操縱桿
    ls:draw()
    rs:draw()

    -- 顯示角色所處網格座標
    fill(249, 7, 7, 255)
    text(ss, 500,100)

    --sysInfo()
    popStyle()
    popMatrix()

end

-- 處理玩家的觸控移動
function touched(touch)

    -- 連續的觸控資料放入 touches 表中
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        touches[touch.id] = touch
        -- for k,v in pairs(touches) do print(k,v) end
    end

    -- 用於測試修煉
    if touch.x > WIDTH/2 and touch.state == ENDED then myStatus:update() end

    -- 用於測試移動方向:點選左側向右平移,點選右側向左平移
    if touch.x > WIDTH/2 and touch.state == ENDED then 
        s = -1
    elseif touch.x < WIDTH/2 then 
        s = 1
    end

    -- c1,c2 = myMap:where(touch.x, touch.y)
    c1,c2 = myMap:where(m.x,m.y)
    -- 顯示角色所處網格座標
    ss = c1.." : "..c2

end

其他模組都不需要大改動, 除了 Sprites 類需要修改 draw() 裡的一點內容, 修改後程式碼為:

function Sprites:draw(w,h)
    ...
    -- 確定每幀子畫面在螢幕上停留的時間
    if ElapsedTime > self.prevTime + 0.08 then
        self.prevTime = self.prevTime + 0.08 
        self.k = math.fmod(self.i,#self.imgs) 
        self.i = self.i + 1    
        self.x = self.x + ls.x
        self.y = self.y + ls.y
    end
    ...
end

另外兩個模組直接複製過來就可以了, 執行截圖如下:

第一次整合

這裡還錄製了一段操作視訊, 看看是不是很流暢?

https://github.com/FreeBlues/Write-A-Adventure-Game-From-Zero/blob/master/assets/c04.mp4

另外再對狀態 類做一些小改進.

狀態原型的改進

發現文字沒有對齊, 先修改一下, 讓它們對齊, 修改後的程式碼如下:

function Status:drawUI()
    ...
    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)
    ...
end    

增加一個除錯函式 sysInfo

為了更精確地瞭解當前遊戲的幀速FPS和記憶體佔用情況(以便迅速發現記憶體洩漏), 我們寫一個小函式:

-- 系統資訊
function sysInfo()
    -- 顯示FPS和記憶體使用情況
    pushStyle()
    --fill(0,0,0,105)
    -- rect(650,740,220,30)
    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

它可以顯示當前的 FPS 和記憶體.

第一次整合後的完整程式碼如下:

-- c04.lua

function setup() 
    displayMode(OVERLAY)
    -- 初始化狀態
    myStatus = Status()

    -- 以下為幀動畫程式碼
    s = -1
    fill(249, 249, 249, 255)
    imgs = {}
    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}}

    img = readImage("Documents:runner")

    m = Sprites(600,400,img,pos)

    ---[[ 初始化觸控搖桿
    touches = {}
    -- cam = Camera(pos.x,pos.y,pos.z,pos.x+look.x,look.y,pos.z+look.z)
    ls,rs = Stick(20,WIDTH-300,200),Stick(2,WIDTH-120)
    -- ls,rs = Stick(1),Stick(3,WIDTH-120)

    -- 初始化地圖
    myMap = Maps()
    ss =""
end

function draw()
    pushMatrix()
    pushStyle()
    -- spriteMode(CORNER)
    rectMode(CORNER)
    background(32, 29, 29, 255)

    -- 增加移動的背景圖: + 為右移,- 為左移
    --sprite("Documents:bgGrass",(WIDTH/2+10*s*m.i)%(WIDTH),HEIGHT/2)
    --sprite("Documents:bgGrass",(WIDTH+10*s*m.i)%(WIDTH),HEIGHT/2)
    -- sprite("Documents:bgGrass",WIDTH/2,HEIGHT/2)
    ---[[
    if ls.x ~= 0 then
        step = 10 *m.i*ls.x/math.abs(ls.x)
    else
        step = 0
    end
    --]]
    --sprite("Documents:bgGrass",(WIDTH/2 - step)%(WIDTH),HEIGHT/2)
    --sprite("Documents:bgGrass",(WIDTH - step)%(WIDTH),HEIGHT/2)

    -- 繪製地圖
    myMap:drawMap()

    -- 繪製角色幀動畫
    m:draw(50,80)
    -- sysInfo()

    -- 繪製狀態列
    myStatus:drawUI()
    --myStatus:raderGraph()

    -- 繪製操縱桿
    ls:draw()
    rs:draw()
    fill(249, 7, 7, 255)
    text(ss, 500,100)

    sysInfo()
    popStyle()
    popMatrix()

end

-- 處理玩家的觸控移動
function touched(touch)

    -- 連續的觸控資料放入 touches 表中
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        touches[touch.id] = touch
        -- for k,v in pairs(touches) do print(k,v) end
    end

    -- 用於測試修煉
    if touch.x > WIDTH/2 and touch.state == ENDED then myStatus:update() end

    -- 用於測試移動方向:點選左側向右平移,點選右側向左平移
    if touch.x > WIDTH/2 and touch.state == ENDED then 
        s = -1
    elseif touch.x < WIDTH/2 then 
        s = 1
    end

    -- c1,c2 = myMap:where(touch.x, touch.y)
    c1,c2 = myMap:where(m.x,m.y)
    -- 顯示角色所處網格座標
    ss = c1.." : "..c2

end

-- 系統資訊
function sysInfo()
    -- 顯示FPS和記憶體使用情況
    pushStyle()
    --fill(0,0,0,105)
    -- rect(650,740,220,30)
    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

--# Status
-- 角色狀態類
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)
end

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

function Status:drawUI()
    pushMatrix()
    pushStyle()

    -- rectMode(CENTER)
    spriteMode(CENTER)
    textMode(CENTER)
    setContext(self.img)
    background(118, 120, 71, 109)

    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)


    -- m:draw(150,200)
    setContext()

    -- 在狀態列繪製雷達圖
    self:raderGraph()

    -- 繪製狀態列
    sprite(self.img, self.img.width/2,HEIGHT-self.img.height/2)


    ---[[ 測試程式碼
    fill(143, 255, 0, 255)
    rect(WIDTH*7/8,HEIGHT/2,100,80)
    fill(0, 55, 255, 255)
    text("修煉", WIDTH*7/8 +50,HEIGHT/2+40)
    --]]

    popStyle()
    popMatrix()
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

-- 角色技能雷達圖
function Status:raderGraph()
    pushMatrix()
    pushStyle()
    setContext(self.img)
    fill(60, 230, 30, 255)
    -- 中心座標,半徑,角度
    local x0,y0,r,a,s = 150,230,40,360/6,4
    -- 計算右上方斜線的座標
    local x,y = r* math.cos(math.rad(30)), r* math.sin(math.rad(30))
    p = {"體力","內力","精力","智力","氣","血"}
    axis = {t={vec2(0,r/s),vec2(0,r*2/s),vec2(0,r*3/s),vec2(0,r)},
            n={vec2(-x/s,y/s),vec2(-x*2/s,y*2/s),vec2(-x*3/s,y*3/s),vec2(-x,y)},
            j={vec2(-x/s,-y/s),vec2(-x*2/s,-y*2/s),vec2(-x*3/s,-y*3/s),vec2(-x,-y)},
            z={vec2(0,-r/s),vec2(0,-r*2/s),vec2(0,-r*3/s),vec2(0,-r)},
            q={vec2(x/s,-y/s),vec2(x*2/s,-y*2/s),vec2(x*3/s,-y*3/s),vec2(x,-y)},
            x={vec2(x/s,y/s),vec2(x*2/s,y*2/s),vec2(x*3/s,y*3/s),vec2(x,y)}}

    -- 用於繪製圈線的函式,固定 4 個點
    function lines(t,n,j,z,q,x)
        line(axis.n[n].x, axis.n[n].y, axis.t[t].x, axis.t[t].y)
        line(axis.n[n].x, axis.n[n].y, axis.j[j].x, axis.j[j].y)
        line(axis.x[x].x, axis.x[x].y, axis.t[t].x, axis.t[t].y)
        line(axis.z[z].x, axis.z[z].y, axis.j[j].x, axis.j[j].y)
        line(axis.x[x].x, axis.x[x].y, axis.q[q].x, axis.q[q].y)
        line(axis.z[z].x, axis.z[z].y, axis.q[q].x, axis.q[q].y)
        --print(axis.z[z].y)
    end

    -- 實時繪製位置,實時計算位置
    function linesDynamic(t,n,j,z,q,x)
        local t,n,j,z,q,x = self.tili, self.neili, self.jingli,self.zhili, self.qi, self.xue
        local fm = math.fmod
        -- t,n,j,z,q,x = fm(t,r),fm(n,r),fm(j,r),fm(z,r),fm(q,r),fm(x,r)
        -- print(t,n,j,z,q,x)
        local c,s = math.cos(math.rad(30)), math.sin(math.rad(30))
        line(0,t,-n*c,n*s)
        line(-n*c,n*s,-j*c,-j*s)
        line(0,-z,-j*c,-j*s)
        line(0,-z,q*c,-q*s)
        line(q*c,-q*s,x*c,x*s)
        line(0,t,x*c,x*s)
    end

    -- 平移到中心 (x0,y0), 方便以此為中心旋轉
    translate(x0,y0)
    -- 圍繞中心點勻速旋轉
    rotate(30+ElapsedTime*10)

    fill(57, 121, 189, 84)
    strokeWidth(0)
    ellipse(0,0,2*r/s)
    ellipse(0,0,4*r/s)
    ellipse(0,0,6*r/s)
    ellipse(0,0,r*2)

    strokeWidth(2)    
    -- noSmooth()
    stroke(93, 227, 22, 255)
    fill(60, 230, 30, 255)
    -- 繪製雷達圖
    for i=1,6 do
        text(p[i],0,45)
        line(0,0,0,r)
        rotate(a)
    end

    -- 繪製圈線
    stroke(255, 0, 0, 102)
    strokeWidth(2)
    for i = 1,4 do
        lines(i,i,i,i,i,i)
    end

    function values()
        local t,n,j,z,q,x = self.tili, self.neili, self.jingli,self.zhili, self.qi, self.xue
        local f = math.floor
        -- return math.floor(t/25),math.floor(t/25),math.floor(t/25),math.floor(t/25),math.floor(t/25),math.floor(t/25)
        return f(t/25),f((25+math.fmod(n,100))/25),f(j/25),f(z/25),f(q/25),f(x/25)
    end
    stroke(255, 32, 0, 255)
    strokeWidth(2)
    smooth()
    -- 設定當前各引數的值
    -- print(values())
    local t,n,j,z,q,x = 3,2,3,2,4,1
    local t,n,j,z,q,x = values()    
    -- local t,n,j,z,q,x = self.tili, self.neili, self.jingli,self.zhili, self.qi, self.xue
    lines(t,n,j,z,q,x)
    linesDynamic(t,n,j,z,q,x)

    setContext()
    popStyle()
    popMatrix()
end

--# Sprites
-- 幀動畫物件類
Sprites = class() 

function Sprites:init(x,y,img,pos)
    self.x = x
    self.y = y
    -- self.index = 1
    self.img = img
    self.imgs = {}
    self.pos = pos
    self.i=0
    self.k=1
    self.q=0
    self.prevTime =0

    -- 處理原圖,背景色變為透明
    self:deal()

    -- 使用迴圈,把各個子幀存入表中
    for i=1,#self.pos do
        -- imgs[i] = img:copy(pos[i][1],pos[i][2],pos[i][3],pos[i][4])
        self.imgs[i] = self.img:copy(table.unpack(self.pos[i]))
    end

end

function Sprites:deal()
    ---[[ 對原圖進行預處理,把背景修改為透明,現存問題:角色內部有白色也會被去掉
    local v = 255
    for x=1,self.img.width do
        for y =1, self.img.height do
            -- 取出所有畫素的顏色值
            local r,g,b,a = self.img:get(x,y)
            -- if r >= v and g >= v and b >= v then
            if r == v and g == v and b == v and a == v then
                self.img:set(x,y,r,g,b,0)
            end
        end
    end
    --]]
end

function Sprites:draw(w,h)
    pushMatrix()
    pushStyle()
    -- 繪圖模式選 CENTER 可以保證畫面中的動畫角色不會左右漂移
    rectMode(CENTER)
    spriteMode(CENTER)
    -- 確定每幀子畫面在螢幕上停留的時間
    if ElapsedTime > self.prevTime + 0.08 then
        self.prevTime = self.prevTime + 0.08 
        self.k = math.fmod(self.i,#self.imgs) 
        self.i = self.i + 1    
        self.x = self.x + ls.x
        self.y = self.y + ls.y
    end
    self.q=self.q+1
    -- rect(800,500,120,120) 

    -- rotate(30)
    -- sprite(self.imgs[self.k+1],self.i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) 
    --sprite(imgs[math.fmod(q,8)+1],i*10%WIDTH+100,HEIGHT/6,HEIGHT/8,HEIGHT/8) 
    -- sprite(self.imgs[self.k+1], self.x, self.y,150,200)
    sprite(self.imgs[self.k+1], self.x, self.y, w or 30, h or 50)
    popStyle()
    popMatrix()
    -- sprite(imgs[self.index], self.x, self.y)
end

--# Maps
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)
    sprite(self.imgMap,0,0)
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

--# Stick
-- 操縱桿類, 作者: @Jaybob
Stick = class()

function Stick:init(ratio,x,y,b,s)
    self.ratio = ratio or 1
    self.i = vec2(x or 120,y or 120)
    self.v = vec2(0,0)
    self.b = b or 180   --大圓半徑
    self.s = s or 100   --小圓半徑
    self.d = d or 50
    self.a = 0
    self.touchId = nil
    self.x,self.y = 0,0
end

function Stick:draw()
    -- 沒有 touched 函式的 Stick 類是如何找到自己對應的觸控資料的?根據點選處座標跟操縱桿的距離來判斷
    if touches[self.touchId] == nil then
        -- 迴圈取出 touches 表內的資料,比較其座標跟操縱桿的距離,若小於半徑則說明是在點選操縱桿
        for i,t in pairs(touches) do
            if vec2(t.x,t.y):dist(self.i) < self.b/2 then self.touchId = i end
        end
        self.v = vec2(0,0)
    else
        -- 根據對應於操縱桿的觸控的xy座標設定 self.v,再根據它計算夾角 self.a
        self.v = vec2(touches[self.touchId].x,touches[self.touchId].y) - self.i
        self.a = math.deg(math.atan2(self.v.y, self.v.x))
    end
    -- 根據 self.v 和 self.b 計算得到 self.t
    self.t = math.min(self.b/2,self.v:len())

    if self.t >= self.b/2 then
        self.v = vec2(math.cos(math.rad(self.a))*self.b/2,math.sin(math.rad(self.a))*self.b/2)
    end

    pushMatrix()
    fill(127, 127, 127, 100)
    -- 分別繪製大圓,小圓
    ellipse(self.i.x, self.i.y, self.b)
    ellipse(self.i.x+self.v.x, self.i.y+self.v.y, self.s)
    --print(self.v.x, self.s)
    popMatrix()
    -- 根據 ratio 重新設定 self.v/self.t   
    self.v = self.v/(self.b/2)*self.ratio
    self.t = self.t/(self.b/2)*self.ratio
    -- 根據 self.v/self.t,重新設定 self.x/self.y
    self.x, self.y = self.v.x, self.v.y
end

簡短的程式碼 VS. 強大的表現能力

看一看 Github 上對我們目前成果的程式碼行統計資料:

  • 563 lines (480 sloc) 16.7 KB

也就是說去掉註釋和空行的有效程式碼行是 480 行, 用這短短的不到 500 行的程式碼, 我們就搭建起一個武俠冒險遊戲的世界. 不得不說我們的開發工具 Codea 特別適合在 iPad 上做原型.

本章的內容比較少, 主要是把前面幾個模組整合到一起, 之所以專門用一章來寫這個, 原因是我需要思考一下後續的開發該怎麼做, 沒錯, 這個遊戲開發專案是我一時心血來潮開始寫的, 基本上沒有專門去做什麼需求分析, 概要設計, 詳細設計什麼的, 而是從想法出發, 從一個個最簡單的原型起步, 想到哪裡寫到哪裡, 於是這麼順順利利地就把一個小框架搭起來了.

需要說明的一點是, 這種原型開發法不太適合大型專案, 不過非常適合個人開發者或者超小型團隊(程式設計師<=2), 尤其適合那些有一個想法, 特別想做出個大概樣子來驗證驗證的開發者.

OK, 本章先寫這麼多, 我先去想想後面怎麼做, 再來繼續.

所有章節連結

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

相關文章