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框架中的大体使用样例。
功能:
用户输入账号密码–>点击登录–>请求服务器–>服务器返回数据–>显示玩家信息。
代码:
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