Unity中使用lua來做UI部分開發時,如果項目規模較大的話,整一套合適的UI框架,提高共同開發效率和保證代碼質量,統一規範開發人員的代碼還是有必要的。避免各自關門按自己的風格行事,導致項目代碼風格各異,模塊功能代碼冗餘雜亂,詭異bug增加。
puremvc就是mvc框架中的一種,是前輩大佬們多年經驗的結果,也發展了很多個編程語言版本,但一直沒有lua版,所以使用lua按照puremvc框架思路實現了一番。
官網地址:https://www.baidu.com/link?url=1KDH29xY7ZKB2C_3p_8gxNp3FnevtWrq_TCbdk94apS&wd=&eqid=da63df06003d5a57000000035e00cc53
puremvc中使用了很多設計模式:
單例模式
觀察者模式
命令模式
中介模式
代理模式
外觀模式
框架大體設計目的直指向高複用模塊化,低耦合代碼目標。
puremvc文檔官網上已經有了,而且介紹的很詳細了,所以這裏不再介紹,主要是lua實現樣例
下面用Unity結合xlua實現了lua的puremvc UI框架。
但因爲lua動態編譯語言的特性,所以在lua版puremvc中定義接口意義不大,省略掉了接口的定義。
這裏只實現了簡要的功能,但也應用到了puremvc核心的功能,描述了puremvc在項目中UI框架中的大體使用樣例。
項目源碼地址:https://github.com/HengyuanLee/UnityLuaPuremvc.git
功能:
用戶輸入賬號密碼–>點擊登錄–>請求服務器–>服務器返回數據–>顯示玩家信息。
代碼:
Main.lua
require 'PureMVC/PureMVC'
require 'StartupMCmd'
require 'AppFacade'
require 'Network'
--遊戲lua入口函數
function Main()
print('start Main ...')
AppFacade:GetInstance():Startup()
end
AppFacade.lua
AppFacade = class('AppFacade',PureMVC.Facade)
AppFacade.APP_STARTUP = 'APP_STARTUP'
local module = AppFacade
--ctor等同於class的構造方法,在new()時會被調用。
function module:ctor()
self.super:ctor()
--註冊啓動命令StartupMCmd,這是個宏命令。
--註冊爲一個函數原因是實現命令的無狀態性,需要時才被實例化出來,省內存。
self:RegisterCommand(AppFacade.APP_STARTUP, function() return StartupMCmd.new() end)
end
--發出啓動遊戲命令。
function module:Startup()
self:SendNotification(AppFacade.APP_STARTUP)
end
StartupMCmd.lua
require 'Login/LoginInitCmd'
require 'PlayerInfo/PlayerInfoInitCmd'
StartupMCmd = class('StartupMCmd', PureMVC.MacroCommand)
--這個類以MCmd後綴結尾是一個MacroCommand,
--內部添加了多個子Cmd,按添加順序執行,
--宏Cmd一次性使用,執行完之後子Cmd就會被清空。
function StartupMCmd:InitializeMacroCommand()
--添加2個UI系統模塊的初始化Cmd
self:AddSubCommand(function() return LoginInitCmd.new() end)
self:AddSubCommand(function() return PlayerInfoInitCmd.new() end)
end
Network.lua
require 'PlayerInfo/LoginSuccessCmd'
--模擬與服務器交互
Network = {}
Network.CMD_LOGIN_SUCCESS = 'CMD_LOGIN_SUCCESS'
local module = Network
AppFacade:GetInstance():RegisterCommand(Network.CMD_LOGIN_SUCCESS, function() return LoginSuccessCmd.new() end)
--登錄
function module.Login(username, password)
if password == nil then
print(debug.traceback())
end
print('請求登陸信息 username: '..username..' password: '..password)
module.onLoginSuccess()
end
--模擬登陸成功
function module.onLoginSuccess()
--假設數據
local data = {
id = 4634536,
nickname = '小強',
guildName = '武當山',
level = 12,
vip = 1,
hp = 100,
mp = 100
}
--獲取服務器數據data成功後發出通知
AppFacade:GetInstance():SendNotification(module.CMD_LOGIN_SUCCESS, data)
end
Login/LoginInitCmd.lua
require 'Login/LoginPanel'
require 'Login/LoginMediator'
require 'Login/LoginClickCmd'
LoginInitCmd = class('LoginInitCmd', PureMVC.SimpleCommand)
--Login功能模塊初始化cmd,
--這裏是有view 的Mediator實例化,model的Proxy參考PlayerInfo系統模塊
function LoginInitCmd:Execute(notification)
local panel = LoginPanel.new(LoginPanel.NAME)
local mediator = LoginMediator.new(LoginPanel.NAME, panel)
AppFacade:GetInstance():RegisterMediator(mediator)
AppFacade:GetInstance():RegisterCommand(LoginClickCmd.CMD, function () return LoginClickCmd.new() end)
end
Login/LoginPanel.lua
--以Panel爲後綴的類名對應Unity 裏製作的UI預設
--如這裏對應LoginPanel.prefab
--panel類等同於一個view,panel裏只提供獲取控件的接口,供mediator訪問,panel內部不做複雜操作。
LoginPanel = class('LoginPanel')
LoginPanel.NAME = 'LoginPanel'
local module = LoginPanel
module.gameObject = nil
module.transform = nil
module.inputUsername = nil
module.inputPassword = nil
module.btnLogin = nil
function module:ctor(panelName)
self.panelName = panelName
self:Load(panelName)
end
--加載unity 裏的LoginPanel.prefab預設,
--並且獲取組件
function module:Load(panelName)
self.gameObject = CS.ResourcesLoader.Instance:LoadPanel(panelName)
self.transform = self.gameObject.transform
self.gameObject:SetActive(false)
self.inputUsername = self.transform:Find('InputUsername'):GetComponent(typeof(CS.UnityEngine.UI.InputField))
self.inputPassword = self.transform:Find('InputPassword'):GetComponent(typeof(CS.UnityEngine.UI.InputField))
self.btnLogin = self.transform:Find('BtnLogin'):GetComponent(typeof(CS.UnityEngine.UI.Button))
end
function module:Show()
self.gameObject:SetActive(true)
end
function module:Hide()
self.gameObject:SetActive(false)
end
Login/LoginMediator.lua
--用於對LoginPanel的各種操作,事件處理,數據填充等。
--PureMVC.Mediator同時繼承了Notifier類,所以能夠SendNotification
--PureMVC.Mediator實現了觀察者功能,提供ListNotificationInterests()方法來填寫感興趣的Cmd,並在HandleNotification()回調。
--這說明了Mediator其實也是一個Cmd,以觀察者模式集成了Cmd的功能。
LoginMediator = class('LoginMediator', PureMVC.Mediator)
local module = LoginMediator
function module:ctor(mediatorName, view)
--覆蓋了父類的構造方法,顯示調用父類構造方法,
--和其它語言有點不同,lua語法特性和class的定義決定了這樣寫
module.super.ctor(self, mediatorName, view)
self:init()
end
function module:init()
self.View:Show()
self.View.btnLogin.onClick:AddListener(
function()
self:onLoginClick()
end
)
end
--點擊登錄事件
function module:onLoginClick()
if self.View.inputUsername == nil then
print(debug.traceback())
print(self.View.__cname)
end
local username = self.View.inputUsername.text
local password = self.View.inputPassword.text
local body = {
username = username,
password = password
}
self:SendNotification(LoginClickCmd.CMD, body)
end
--指定方法ListNotificationInterests()中填寫感興趣的Cmd
function module:ListNotificationInterests()
return {
Network.CMD_LOGIN_SUCCESS
}
end
--感興趣的Cmd 在HandleNotification(notification)中收到回調
function module:HandleNotification(notification)
if notification.Name == Network.CMD_LOGIN_SUCCESS then
self.View:Hide()
end
end
LoginClickCmd.lua
LoginClickCmd = class('LoginClickCmd', PureMVC.SimpleCommand)
local module = LoginClickCmd
module.CMD = 'LoginClickCmd'
--處理點擊登錄界面“登錄”按鈕的事件,向服務器發送登錄消息。
function module:Execute(notification)
local username = notification.Body.username
local password = notification.Body.password
Network.Login(username, password)
end
PlayerInfo/LoginSuccessCmd.lua
LoginSuccessCmd = class('LoginSuccessCmd', PureMVC.SimpleCommand)
local module = LoginSuccessCmd
--有這個類的存在,只是爲了更新PlayerInfoProxy的數據,
--因爲proxy和mediator不一樣,數據模型proxy爲了解耦不接收任何通知,
--但UI mediator需要作爲觀察者接收通知與用戶進行互動。
function module:Execute(notification)
local gameInfoProxy = AppFacade:GetInstance():RetrieveProxy(PlayerInfoPanel.NAME)
gameInfoProxy:Refresh(notification.Body)
end
PlayerInfo/PlayerInitCmd.lua
require 'PlayerInfo/PlayerInfoPanel'
require 'PlayerInfo/PlayerInfoMediator'
require 'PlayerInfo/PlayerInfoProxy'
PlayerInfoInitCmd = class('PlayerInfoInitCmd', PureMVC.SimpleCommand)
local module = PlayerInfoInitCmd
--PlayerInfo系統模塊初始化,包括mediator和proxy。
function module:Execute(notification)
local panel = PlayerInfoPanel.new(PlayerInfoPanel.NAME)
local mediator = PlayerInfoMediator.new(PlayerInfoPanel.NAME, panel)
local proxy = PlayerInfoProxy.new(PlayerInfoPanel.NAME)
AppFacade:GetInstance():RegisterMediator(mediator)
AppFacade:GetInstance():RegisterProxy(proxy)
end
PlayerInfo/PlayerInfoMediator.lua
PlayerInfoMediator = class('PlayerInfoMediator', PureMVC.Mediator)
local module = PlayerInfoMediator
function module:ctor(mediatorName, View)
module.super.ctor(self, mediatorName, View)
self.View = View
end
function module:ListNotificationInterests()
return {
Network.CMD_LOGIN_SUCCESS,
PlayerInfoProxy.CMD_REFRESH
}
end
function module:HandleNotification(notification)
if notification.Name == Network.CMD_LOGIN_SUCCESS then
--登錄成功時顯示玩家信息
self.View:Show()
elseif notification.Name == PlayerInfoProxy.CMD_REFRESH then
--PlayerInfoProxy的數據刷新時,重新填充顯示的玩家數據
local proxy = AppFacade:GetInstance():RetrieveProxy(PlayerInfoPanel.NAME)
local data = proxy:GetData()
if data ~= nil then
local info = string.format(
" 登錄成功!\n 玩家信息 \n id: %d\n 暱稱:%s\n 公會:%s \n 等級:%d \n vip:%d \n hp:%s \n mp:%d",
data.id,
data.nickname,
data.guildName,
data.level,
data.vip,
data.hp,
data.mp)
self.View.text.text = info
end
end
end
PlayerInfo/PlayerInfoPanel.lua
PlayerInfoPanel = class('PlayerInfoPanel')
PlayerInfoPanel.NAME = 'PlayerInfoPanel'
local module = PlayerInfoPanel
function module:ctor(panelName)
self.panelName = panelName
self:Load(panelName)
end
function module:Load(panelName)
self.gameObject = CS.ResourcesLoader.Instance:LoadPanel(panelName)
self.transform = self.gameObject.transform
self.gameObject:SetActive(false)
self.text = self.transform:Find('Text'):GetComponent(typeof(CS.UnityEngine.UI.Text))
end
function module:Show()
self.gameObject:SetActive(true)
end
function module:Hide()
self.gameObject:SetActive(false)
end
PlayerInfo/PlayerInfoProxy.lua
--Proxy繼承了Notifer接口,可以發送命令SendNotification(),
--但不監聽任何命令。
PlayerInfoProxy = class('PlayerInfoProxy', PureMVC.Proxy)
PlayerInfoProxy.CMD_REFRESH = 'PlayerInfoProxy'
local module = PlayerInfoProxy
function module:GetData()
return self.Data
end
--提供修改數據的方法,只應該提供給服務器數據來時修改用。
function module:Refresh(data)
self.Data = data
self:SendNotification(PlayerInfoProxy.CMD_REFRESH)
end