(前置聲明: 本隨筆圖片資源 選自 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 書上介紹過).