love2d 前端 聯合 c++ 服務端 的 遊戲開發(三)

(前置聲明: 本隨筆圖片資源 選自 http://opengameart.org)ide

這邊繼承上一篇隨筆的代碼, 修改後效果圖將如:函數

較之有如下改動:字體

1.使用簡潔的背景圖片
2.添加了調試文本輸出框(下方紅色框體)
3.添加了角色屬性查看藍(右上方帶背景的框體)
4.添加帶四個方向的2D角色模型
5.繪製角色模型的圖片sprite邊框體
5.鼠標控制移動角色模型

 

1.使用簡潔的背景圖片
將 welcome scene 的背景變動爲 黑色圖片:
function WelcomeScene:Initialize()
    
    self:SetDesc( "welcome scene")
    self:SetBackgroundImg( "img/black.jpg", g.width, g.height )

    ...
end


2.添加了調試文本輸出框(下方紅色框體)
新的類 MessagePanel (源自文件 panel/messagePanel.lua), 調試文本輸出框 主要由兩部分組成 : 紅色填充矩形 和 文本:
function MessagePanel:draw( )

    ... 

    --紅色填充矩形
    
    local r,g,b,b = love.graphics.getColor()
    love.graphics.setColor( 255, 0, 0, 128 )
    love.graphics.rectangle( "fill", self.x, self.y, self.width, self.height )
    love.graphics.setColor(  r,g,b,b)

    ...

    --繪製文本
    love.graphics.printf( 
        self.msgQueue[ msgs - i ], 
        self.x, 
        self.y + i * charHeight , 
        self.width 
    )


    ...
end
封裝成一個類, 主要是爲了複用 , 以及同步組織 紅色填充矩形 和 文本的位置大小信息
文本以 大小爲 10條消息的 隊列維護:
function MessagePanel:Add( message )
    -- body
    table.insert( self.msgQueue, 1, message)
    if #self.msgQueue > self.maxMsgs then 
        table.remove( self.msgQueue )
    end
end

隊列內容定時進行刪除過期消息:優化

function MessagePanel:update( dt )
    -- body
    self.delta = self.delta + dt 

    if self.delta >= self.timeout then 
        self.delta = self.delta - self.timeout

        table.remove( self.msgQueue )
    end
end

此外就是一些例如 繪製時 進行字體變動與恢復, 填充紅色時進行 存儲與恢復,總體 messagePanel.lua 內容有如ui

MessagePanel = {}

function MessagePanel:new()
    local o = {}
    setmetatable( o, MessagePanel)
    self.__index = self


    o:_Init()

    return o 
end

function MessagePanel:_Init( )
    -- body
    self.msgQueue = {}
    self.delta = 0 
    self.timeout = 10.0 --how long to clear one message
    self.maxMsgs = 10 -- max store 10 messages
    self.x = 20
    self.y = 20 
    self.width         = 256 
    self.height     = self.width * 1.2
    self.fontSize     = 9
    self.filled     = false

    self.font         = love.graphics.newFont( self.fontSize )
end

function MessagePanel:SetFill( filled )
    -- body
    self.filled = filled
end

function MessagePanel:SetFontSize( sz )
    -- body
    self.fontSize = sz 
end

function MessagePanel:SetMaxMessages( count )
    -- body
    self.maxMsgs = count
end

function MessagePanel:SetTimeout( tm )
    -- body
    self.timeout = tm 
end

function MessagePanel:GetWidth( )
    -- body
    return self.width
end

function MessagePanel:SetWidth( pixels )
    -- body
    self.width = pixels
end

function MessagePanel:SetHeight( pixels )
    -- body
    self.height = pixels

    self.maxMsgs = math.max( 1, self.height / self.font:getHeight() )
end

function MessagePanel:GetHeight( )
    -- body
    return self.height
end

function MessagePanel:SetPos( x, y )
    -- body
    self.x = x
    self.y = y 
end

function MessagePanel:update( dt )
    -- body
    self.delta = self.delta + dt 

    if self.delta >= self.timeout then 
        self.delta = self.delta - self.timeout

        table.remove( self.msgQueue )
    end
end

function MessagePanel:Add( message )
    -- body
    table.insert( self.msgQueue, 1, message)
    if #self.msgQueue > self.maxMsgs then 
        table.remove( self.msgQueue )
    end
end

function MessagePanel:Log( ... )
    -- body
    local msg = string.format( unpack(arg))
    self:Add( msg)
end

function MessagePanel:draw( )
    -- body
    local msgs = math.min( #self.msgQueue, self.maxMsgs )
    local charHeight = self.font:getHeight()


    if self.filled then
        local r,g,b,b = love.graphics.getColor()
        love.graphics.setColor( 255, 0, 0, 128 )
        love.graphics.rectangle( "fill", self.x, self.y, self.width, self.height )
        love.graphics.setColor(  r,g,b,b)
    end

    if msgs > 0 then 

        local oldFont = love.graphics.getFont()
        love.graphics.setFont( self.font )

        for i = 0, msgs - 1, 1 do

            love.graphics.printf( 
                self.msgQueue[ msgs - i ], 
                self.x, 
                self.y + i * charHeight , 
                self.width 
            )

        end

        love.graphics.setFont( oldFont )
    end

    
end

 使用方式:lua

    g.msgPanel = MessagePanel:new()
    g.msgPanel:SetFill( true)
    g.msgPanel:SetWidth( g.width)
    g.msgPanel:SetHeight( 50 )
    g.msgPanel:SetPos( 0, g.height - g.msgPanel:GetHeight() )


3.添加了角色屬性查看藍(右上方帶背景的框體) spa

若是MessagePanel類可以運行明白, ObjectInfoPanel類也就天然不過了, 由兩部分組成: 背景 和 文本輸出, 總體類詳細內容(panel/objectInfoPanel.lua )有如:3d

ObjectInfoPanel = {}

function ObjectInfoPanel:new()
    local o = {}
    setmetatable( o, ObjectInfoPanel)
    self.__index = self


    o:_Init()

    return o 
end

function ObjectInfoPanel:_Init( )
    -- body
    self.x = 0
    self.y = 0 
    self.width         = 256 
    self.height     = self.width * 1.2
    self.fontSize     = 9

    self.font         = love.graphics.newFont( self.fontSize )
    self.background = Background:new( "img/info_background.png", self.width, self.height )
    self:SetPos( 50, 50 )

    self.showable = false
end


function ObjectInfoPanel:SetFontSize( sz )
    -- body
    self.fontSize = sz 
end

function ObjectInfoPanel:SetWidth( pixels )
    -- body
    self.width = pixels
    self.background:SetWidth( self.width)
end

function ObjectInfoPanel:SetHeight( height )
    -- body
    self.height = height
    self.background:SetHeight( self.height)
end

function ObjectInfoPanel:SetPos( x, y )
    -- body
    self.x = x
    self.y = y 

    self.background:SetPos( self.x, self.y )
end

function ObjectInfoPanel:update( dt )

end

function ObjectInfoPanel:show( )
    -- body
    self.showable = true
end

function ObjectInfoPanel:hide( )
    -- body
    self.showable = false 
end

function ObjectInfoPanel:draw( )

    if self.showable then
        if self.background then
            self.background:draw()
        end

        local oldFont = love.graphics.getFont()
        love.graphics.setFont( self.font )

        love.graphics.printf( "hp : 100", self.x + 10 , self.y + 10, self.width ) 
        love.graphics.printf( "mp : 100", self.x + 10 , self.y + 20, self.width )

        love.graphics.setFont( oldFont )
    end
end

使用方式:調試

    g.objInfoPanel = ObjectInfoPanel:new()
    g.objInfoPanel:SetWidth( 125)
    g.objInfoPanel:SetHeight( 150)
    g.objInfoPanel:SetPos( g.width - 125, 0 )

當點擊了角色模型後, 就會顯示該屬性界面.code

 

4.添加帶四個方向的2D角色模型

玩家主控角色模型是 GamePlayer類(object/gamePlayer.lua), 總體代碼簡單有如:

require( "object/gameObject")

GamePlayer = GameObject:new()

function GamePlayer:new()
    local o = {}
    setmetatable( o, GamePlayer)
    self.__index = self 

    o:ChangeStateTo( STATE_IDLE_VANILLA)

    return o 
end

GamePlayer類的 目前主要工做僅僅是 設置出示 狀態: STATE_IDLE_VANILLIA, 即空閒狀態; 至關大一部分代碼由基類 GameObject 完成:

 

require( "position")
require( "sprite/sprite")

GameObject = {}

function GameObject:new( o)
    o = o or {}

    setmetatable( o, GameObject )
    self.__index = self

    o:_Init()

    return o
end

--內部信息初始化
function GameObject:_Init( )

end


--變動狀態
function GameObject:ChangeStateTo( stateid )
    
end

--變動位置
function GameObject:SetPos( pos, y )

end

--設置 GUID
function GameObject:SetGUID( guid )
    -- body
    self.guid = guid 
end

function GameObject:GetGUID()
    return self.guid 
end

--顯示可見性模型
function GameObject:draw()
    
end

--更新對象的各類狀態
function GameObject:update( dt )


end


--移動到鼠標所點擊的位置
function GameObject:MoveTo( x, y )
    
end

--檢測是否選擇了該對象
function GameObject:mousepressed( x, y, button )
    ...
    self:OnSelected()
    ...
end

--這就是 查看對象信息 事件了
function GameObject:OnSelected( )
    -- body
    g.objInfoPanel:show()
end

一個基本的對象 有如下幾個小部分組成:

1.位置信息 pos
2.可見性模型信息的 sprite
3.遊戲狀態 state, 到本隨筆爲止有 idle 和 moving 兩種狀態
4.被鼠標點選標記 hover 

function GameObject:_Init( )

    --位置信息
    self.pos     = Position:new()
    self.guid   = nil
    self.scene  = nil 

    --可見性模型 sprite
    self.sprite = Sprite:new()

    --顯示 sprite 邊框
    self.sprite:ShowBounder( true)
    
    --遊戲狀態
    self.state     = LoadState( STATE_IDLE_VANILLA )
    
    --移動路徑
    self.movePath = {}

    --移動方向, 將決定 sprite 選用 四個方向的哪一個
    self.moveDirect = Direction.DOWN

    --被鼠標點選相關屬性
    self.hover = false
end

兩個關鍵函數 GameObject:update(dt) 主要負責維護 對象的狀態, 即 idle 與 moving 的切換: moving停下來即進入 idle 狀態, 玩家點擊鼠標在合適位置則進入 moving狀態, 固然還包含如下例如位置的變動等等; GameObject:draw() 利用 sprite 顯示繪製 模型, sprite 會根據 對象的 state 狀態 和 移動方向進行圖片選取:

 

function GameObject:draw()
    self.sprite:Display( self.moveDirect, g_step, self.pos.x, self.pos.y )
end

function GameObject:update( dt )
    g_stepDelta = g_stepDelta + dt 
    g_moveDelta = g_moveDelta + dt 
    
    if g_stepDelta >= 0.25  then
        g_stepDelta = g_stepDelta - 0.25
        g_step = ( g_step  % 4 ) + 1    
    end
    
    if g_moveDelta >= 0.05  then
        g_moveDelta = g_moveDelta - 0.05
        if #self.movePath > 0 then 
            
            if self.pos.x == self.movePath[1] and self.pos.y == self.movePath[2] then 
                table.remove( self.movePath, 1)
                table.remove( self.movePath, 1)
            end
            
            if #self.movePath > 0 then
                self.moveDirect = FindDirection( self.pos.x, self.pos.y, self.movePath[1], self.movePath[2] )
                
                if self.pos.x ~= self.movePath[1] then 
                    if self.movePath[1] > self.pos.x then self.pos.x = self.pos.x + 1 else self.pos.x = self.pos.x - 1 end
                end
                
                if self.pos.y ~= self.movePath[2] then 
                    if self.movePath[2] > self.pos.y then self.pos.y = self.pos.y + 1 else self.pos.y = self.pos.y - 1 end
                end
            else
                --move to target pos 
                self:ChangeStateTo( STATE_IDLE_VANILLA )
            end
        end        
    end


    self.hover = self.sprite:isHovered( self.pos.x, self.pos.y )
    if self.hover then
        g.msgPanel:Log( "hovered")
    end

end

GameObject.pos 是 玩家正下方的中間的像素位置, isHovered() 函數根據這個 pos  和 sprite 單元的 大小, 肯定鼠標是否在 sprite 所可以表示的範圍內, 進而肯定"鼠標在對象正上方":

function Sprite:isHovered( obj_x, obj_y )
    -- body
    if g.mouse.x > obj_x - self._width_half
        and g.mouse.x < obj_x + self._width_half
        and g.mouse.y > obj_y - self._height
        and g.mouse.y < obj_y
    then
        return true
    end

    return false
end

兩個狀態 state 是很簡單的兩個類, 繼承自 State 基類:

 

STATE_IDLE_VANILLA = 1
STATE_WALK_VANILLA = 2


State = {}

function State:new()
    local o = {}

    setmetatable( o, State)
    self.__index = self

    o:_Init()
    
    return o
end

function State:_Init()
    self.desc = "state"
    self.delta = 0
    self.type = nil
end

function State:Initialize()

end

function State:draw()

end

function State:Type()
    return self.type 
end

function State:SetType( stateType )
    self.type = stateType
end

 

require( "state/state")

IdleVanillaState = State:new()

function IdleVanillaState:new( )
    -- body
    local o = {}
    setmetatable( o, IdleVanillaState)
    self.__index = self 

    o:Initialize()
    return o
end

function IdleVanillaState:Initialize()
    self:SetType( STATE_IDLE_VANILLA)
end

function IdleVanillaState:draw()
    
end

 

require( "state/state")

WalkVanillaState = State:new()

function WalkVanillaState:new(  )
    -- body

    local o = {}
    setmetatable( o, WalkVanillaState )
    self.__index = self 

    o:Initialize()
    return o 
end

function WalkVanillaState:Initialize()
    self:SetType( STATE_WALK_VANILLA )
end

function WalkVanillaState:draw()
    
end

兩個狀態 主要是維護了 本身的狀態 self.type 便是最大的不一樣(最大用途是 sprite 進行狀態判斷選擇圖片), 此外功能函數幾乎都同樣.


Sprite 類就比較爲有趣了. 對於移動狀態中的 對象, 可見性模型圖片須要從下圖中選取:

 

不一樣移動方向(或靜止時的朝向), 即 上下左右, 選取 四行中的一行, 而不一樣時序, 則選擇 某一行中的 4個 圖片中的一個.

Sprite = {}

function Sprite:new()
    local o = 
    {
        drawable = nil, --預先加載 某一狀態下 4 個方向 的 四個時序工 16 個小圖形組成的 一個 大圖

        row = 0, -- 4個方向
        col = 0, -- 4個時序

        MaxWidth = 0,     --大圖的 寬度
        MaxHeight = 0,     --大圖的 高度

        _width = 0,        -- 每一個小圖形的 寬度(每次繪製角色模型的 寬度)
        _height = 0,    -- 每一個小圖形的 高度(每次繪製角色模型的 高度)
        
        _width_half = 0,    -- 預處理用 的 半值
        _height_half = 0,    -- 預處理用 的 半值
        
        down     = {},    -- 向下 方向的 4個時序對應在 大圖中的 ( x, y ) 偏移量
        up         = {},    -- 向上 方向的 4個時序對應在 大圖中的 ( x, y ) 偏移量
        right     = {},    -- 向右 方向的 4個時序對應在 大圖中的 ( x, y ) 偏移量
        left     = {},    -- 向左 方向的 4個時序對應在 大圖中的 ( x, y ) 偏移量

        showBounder = false --是否顯示 邊框標記
    }

    o.direction =  --四個方向的 數值key 索引
    {
        [1] = o.up, 
        [2] = o.right,
        [3] = o.down,
        [4] = o.left,
    }

    o.quad =         --繪製用的 與生成對象
    {
        [1] = {},
        [2] = {},
        [3] = {}, 
        [4] = {},
    }


    setmetatable( o, Sprite)
    self.__index = self

    o:_Init()

    return o
end

function  Sprite:_Init(  )

end

function Sprite:ShowBounder( toSet )
    
end

function Sprite:SetAsset( filename, row, col )
    
end

function Sprite:Adjust()

end


function Sprite:Display( direction, step, x, y )

end

function Sprite:StateChanged( state )

end

function Sprite:isHovered( obj_x, obj_y )

end

當sprite 的圖片源文件變動, 或者 大小變動時, 都會從新生成 16 個小圖形的 偏移值:

function Sprite:Adjust()
    self.MaxWidth  = self.drawable:getWidth()
    self.MaxHeight = self.drawable:getHeight()

    self._width = self.MaxWidth / self.col
    self._height = self.MaxHeight / self.row
    
    self._width_half  = math.floor( self._width / 2   )
    self._height_half = math.floor( self._height / 2  )

    for w = 0, self.MaxWidth, self._width do 
        table.insert( self.down, {x=w, y=0} )
    end

    for w = 0, self.MaxWidth, self._width do 
        table.insert( self.up, {x=w, y=self._height} )
    end

    for w = 0, self.MaxWidth, self._width do 
        table.insert( self.right, {x=w, y=2*self._height} )
    end

    for w = 0, self.MaxWidth, self._width do 
        table.insert( self.left, {x=w, y=3*self._height} )
    end


    for dir = 1, 4, 1 do 
        for step = 1, self.col, 1 do 
            local offset = self.direction[ dir][ step]
            self.quad[ dir][ step] = love.graphics.newQuad( offset.x, offset.y, self._width, self._height, self.MaxWidth, self.MaxHeight )
        end
    end
end
--顯示 角色模型時, 根據 模型的朝向 direction 和 時序 step
--進行繪製
function Sprite:Display( direction, step, x, y )
    
    --這裏就是繪製邊框啦, 其實就是 描邊 的 矩形
    if self.showBounder then
        love.graphics.rectangle( "line", x - self._width_half, y - self._height, self._width, self._height )
    end
    

    --顯示 角色模型時, 根據 模型的朝向 direction 和 時序 step
    --進行繪製
    local quad = self.quad[ direction][ step]
    love.graphics.drawq( self.drawable, quad, x - self._width_half, y - self._height )

end

角色移動的 方向 和  時序其實都是在 GameObject:update(dt),

function GameObject:update( dt)
    ...
  self.moveDirect
= FindDirection( self.pos.x, self.pos.y, self.movePath[1], self.movePath[2] ) ...
end

FindDirection( x, y, x2, y2) 函數根據 目的地(x2, y2) 相對於 起點(x, y) 的方向:

local tryRight = function( y, y2 )
    if y2 > y then 
        return Direction.DOWN
    elseif y2 == y then
        return Direction.RIGHT
    else 
        return Direction.UP
    end
end

local tryLeft = function( y, y2 )

    if y2 > y then 
        return Direction.DOWN
    elseif y2 == y then
        return Direction.LEFT
    else 
        return Direction.UP
    end
end

local tryUP_DOWN = function( y, y2  )
    if y2 > y then 
        return Direction.DOWN
    elseif y2 == y then
        return Direction.LEFT
    else 
        return Direction.UP
    end
end

function FindDirection( x, y, x2, y2 )
    if x2 > x then
        return tryRight( y, y2)
    elseif x2 < x then
        return tryLeft( y, y2 )
    else
        return tryUP_DOWN( y, y2)
    end
end

時序step, 其實就是 循環在 1, 2, 3, 4 之間進行更換.

5.繪製角色模型的圖片sprite邊框體

  已講述

 

6.鼠標控制移動角色模型

關於這一點, 就得先說說 scene, Scene 有三大部份內容:

  • 子對象, 例如背景, 按鈕控件, 信息欄, 未來擴展的 菜單欄, 通常是人類玩家相關的操做元素;
  • 被容器管理的對象, 例如玩家, 怪物, 通常是遊戲世界內事物;
  • 鼠標,鍵盤 或者其餘響應(暫且重複 第一點的一些事項)

因此 scene 都會迭代的 調用 這三類的 update, draw 和 mousepressed 函數:

function Scene:draw()

    self.background:draw()

    for guid, obj in pairs( self.objlist) do 
        if obj.draw then 
            obj:draw()
        end
    end

    for _, child in pairs( self.children) do 
        if child.draw then 
            child:draw()
        end
    end
end

function Scene:update(dt)

    for guid, obj in pairs( self.objlist) do 
        if obj.update then 
            obj:update( dt)
        end
    end

    for _, child in pairs( self.children) do 
        if child.update then 
            child:update( dt)
        end
    end
end

function Scene:mousepressed(x, y, button)

    for guid, obj in pairs( self.objlist) do 

        if obj.mousepressed then 
            
            if obj:mousepressed( x, y, button ) then 
                -- game object selected
                g.msgPanel:Log( "game object selected")
                return true 
            end
        end
    end
    
    for _, child in pairs( self.children) do 

        if child.mousepressed then 
            if child:mousepressed( x, y, button ) then 
                -- child item selected
                g.msgPanel:Log( "child item selected")
                return true 
            end
        end
    end
    

    --default : make rgp move
    if button == "l" then
        g.player:MoveTo( x, y )
    end
end

對於 mousepressed 事件 迭代處理中, 是 假設若是 玩家點擊的對象不是子菜單, 也不是 點選對象, 就進行移動位置變動.每一個 GameObject 維護一個movePath table對象

GameObject.movePath = 
{
     [1] = 第一個拐點 x 座標,
     [2] = 第一個拐點 y 座標,
     [3] = 第二個拐點 x 座標,
     [4] = 第二個拐點 y 座標,
     ...
}

對於怪物, 在進行尋路時, 可能會產生 一系列的拐點, 而玩家角色, 我將維護兩種拐點使用方式:

第一, 人類玩家控制 角色模型時,  用以中途變動目的地, 只維護第一個拐點, 一旦目的地變動, 即刻爲 第一個拐點;
第二, 在進行自動掛機或尋路時,採用 和 怪物同樣的 拐點列表方式

 

這裏, 其實出現了兩個待優化問題:

1.移動時, 先八個方向走, 剩餘進行橫豎行走;

緣由是在 GameObject.update(dt)中 每次都是按照 一個像素進行 "朝着"目的地修正位置, 一旦 移動路徑的 橫豎座標份量差別較大時, 都會出現的:

function GameObject:update( dt)           
             ...            
            if #self.movePath > 0 then
                self.moveDirect = FindDirection( self.pos.x, self.pos.y, self.movePath[1], self.movePath[2] )
                
                if self.pos.x ~= self.movePath[1] then 
                    if self.movePath[1] > self.pos.x then self.pos.x = self.pos.x + 1 else self.pos.x = self.pos.x - 1 end
                end
                
                if self.pos.y ~= self.movePath[2] then 
                    if self.movePath[2] > self.pos.y then self.pos.y = self.pos.y + 1 else self.pos.y = self.pos.y - 1 end
                end
            else
                --move to target pos 
                self:ChangeStateTo( STATE_IDLE_VANILLA )
            end

            ...
end

一個解決方案是, 按照浮點數進行位置修正.

2.鼠標穿透不一樣 疊加了的控件.

會致使 鼠標選擇在正下方的 某個控件, 可是沒有選中 理應被選中的 在上方的控件.

一個解決方案是, 反序按照顯示順序進行迭代搜索( 記得 在 DirectX9 User interface design 書上介紹過).

 

源代碼:http://files.cnblogs.com/Wilson-Loo/XGame.0829.rar

相關文章
相關標籤/搜索