本文转载自 :碧俐千仞的博客http://blog.sina.com.cn/peiyul
1、安装框架
只要在http://www.ulua.org/index.html下载LuaFramework,然后用Unity3D打开,这里用的是LuaFramework_UGUI-1.0.4.109版本以及Unity3D 5.2,其他版本理应相似。打开之后需要点击lua菜单里面的Generate All和LuaFramework菜单里Build XXX Resources,以生成一些必要的文件。
安装过程可以参见http://pan.baidu.com/s/1gd8fG4N里面的01_uLua_Windows.avi和02_SimpleFramework_UGUI_Windows.avi两个视频(如果在windows系统下)。框架结构请参见http://doc.ulua.org/article/ngui/simpleframework_base1.html,这里不再复述。
若运行后能够弹出示范界面,证明安装成功,可以进入下一步。
成功运行示范界面(可要客户端能够运行起来就行)
2、运行Lua代码
这一步的目标很简单,就是让框架运行我们自己写的lua代码,显示一句“helloWorld”。下一步再考虑代码的热更新问题。
1)新建场景
在任意物体中添加Main组件。其实Main组件里面只是调用了AppFacade.Instance.StartUp(),这是框架的起点。框架将会自动完成资源加载、热更新等等事项。
添加Main组件
2)删掉示例的调用
现在不需要框架自带的示例了,需要删掉一些代码,使框架只运行我们编写的lua文件。打开Assets\LuaFramework\Scripts\Manager\GameManager.cs,将OnInitalize修改成下图这个样子。这是lua的入口,框架会调用Main.lua的Main方法。
修改GameManager的lua入口
3)编写lua代码
打开Assets\LuaFramework\Lua\main.lua,编写lua代码。这里只添加一句“LuaFramework.Util.Log("HelloWorld");”(如下所示),它的功能相当于Debug.Log("HelloWorld")。
--主入口函数。从这里开始lua逻辑
function Main()
LuaFramework.Util.Log("HelloWorld");
end
“LuaFramework.Util.Log("HelloWorld")”中的Util是c#里定义的类,供lua中调用。可以打开Assets\LuaFramework\Editor\CustomSettings.cs看到所有可以供lua调用的类,如下图是CustomSettings.cs的部分语句。
CustomSettings.cs的部分语句
再由具体的类可以查找所有的API(参见下面两个图),如下图是Util类的部分语句。
Util类(Assets\LuaFramework\Scripts\Utility\Util.cs)的部分语句
4)运行游戏
点击菜单栏中LuaFramework→Build Windows Resource,生成资源文件。然后运行游戏,即可在控制台中看到打印出的HelloWorld。
生成资源文件
运行结果
按照默认的设置,每更改一次lua代码,都需要执行Build XXX Resource才能生效。读者可以将Assets\LuaFramework\Scripts\ConstDefine\AppConst.cs中的LuaBundleMode修改为false,这样代码文件便不会以AssetBundle模式读取,会直接生效,以方便调试。
设置LuaBundleMode
3、热更新的原理
接下来便要尝试代码热更新,让程序下载服务器上的lua文件,然后运行它。在说明热更新之前,需要先看看Unity3D热更新的一般方法。如下图所示,Unity3D的热更新会涉及3个目录。
热更新的过程
游戏资源目录:里面包含Unity3D工程中StreamingAssets文件夹下的文件。安装游戏之后,这些文件将会被一字不差地复制到目标机器上的特定文件夹里,不同平台的文件夹不同,如下所示(上图以windows平台为例)
Mac OS或Windows:Application.dataPath + "/StreamingAssets";
IOS: Application.dataPath + "/Raw";
Android:jar:file://" + Application.dataPath + "!/assets/";
数据目录:由于“游戏资源目录”在Android和IOS上是只读的,不能把网上的下载的资源放到里面,所以需要建立一个“数据目录”,该目录可读可写。第一次开启游戏后,程序将“游戏资源目录”的内容复制到“数据目录中”(步骤1,这个步骤只会执行一次,下次再打开游戏就不复制了)。游戏过程中的资源加载,都是从“数据目录”中获取、解包(步骤3)。不同平台下,“数据目录”的地址也不同,LuaFramework的定义如下:
Android或IOS:Application.persistentDataPath + "/LuaFramework"
Mac OS或Windows:c:/LuaFramework/
调试模式下:Application.dataPath + "/StreamingAssets/"
注:”LuaFramework”和”StreamingAssets”由配置决定,这里取默认值
网络资源地址:存放游戏资源的网址,游戏开启后,程序会从网络资源地址下载一些更新的文件到数据目录。
这些目录包含着不同版本的资源文件,以及用于版本控制的files.txt。Files.txt的内容如下图所示,里面存放着资源文件的名称和md5码。程序会先下载“网络资源地址”上的files.txt,然后与“数据目录”中文件的md5码做比较,更新有变化的文件(步骤2)。
files.txt
LuaFramework的热更新代码定义在Assets\LuaFramework\Scripts\Manager\GameManager.cs,真正用到项目时可能还需少许改动。
4、开始热更新代码吧!
那么开始测试热更新代码的功能吧!热更上述实现的“HelloWorld”。
1)修改配置
框架的默认配置是从本地加载文件,需要打开AppConst.cs将UpdateMode设置为true(才会执行步骤2),将LuaBundleMode设置为true,将WebUrl设置成服务器地址。如下图所示。
AppConst的配置
2)配置“网络资源”
笔者使用iis开启本地服务器,然后将StreamingAssets里面的所有内容复制到服务器上面。必要时要配置一些权限,让所有文件都都可以下载。
通过网络访问文件
3)测试热更新
改一下Lua脚本(如将HelloWorld改为Hello Lpy2),点击Build Windows Resource,将“工程目录/StreamingAssets”里面的文件复制到服务器上。再将脚本改成其他内容,然后Build Windows Resource,覆盖掉本地资源。运行游戏,如果程序显示“Hello Lpy2”的代码,证明成功从网上拉取了文件。
代码热更新
热更新涉及资源热更新和代码热更新(其实lua代码也是资源),那接下来看看如何动态加载一个模型,然后热更成其他素材。这一部分涉及资源打包、动态创建资源等内容。
1、创建物体
为了调试的方便,笔者先将框架配置为本地模式,待测试热更新时再改成更新模式。
配置为本地模式
先测试个简单的创建物体,新建一个名为go的物体,然后设置它的坐标为(1,1,1)。这段代码虽然不涉及资源加载,但能展示“把物体添加到场景中”的过程。Main.lua的代码如下:
-
function Main()
-
local go = UnityEngine.GameObject ('go')
-
go.transform.position = Vector3.one
end
动态创建一个名为go的空物体
要热更新资源,便需要制作资源。这里制作一个名为tankPrefab的坦克模型预设,然后存到Assets/Tank目录下。接下来对它做打包,然后动态加载。
坦克预设
广告时间:这个坦克模型是来自笔者即将出版的一本书《Unity3D网络游戏实战》。该书通过一个完整的多人坦克对战实例,详细介绍网络游戏开发过程中涉及到的知识和技巧。书中还介绍了服务端框架、客户端网络模块、UI系统的架构等内容。相信透过本书,读者能够掌握Unity3D网络游戏开发的大部分知识,也能够从框架设计中了解商业游戏的设计思路,感谢大家支持。
2、资源打包
LuaFramework在打包方面并没有做太多的工作,我们需要手动打包。打开Assets/LuaFramework/Editor/Packager.cs,按照示例的写法,加上下面这一行:将Assets/Tank目录下的所有预设(.prefab)打包成名为tank的包。
修改打包代码
点击“Build Windows Resource”,即可在StreamingAssets中看到打包好的文件。
如下图所示,Unity3D资源包里面包含多个资源,就像一个压缩文件一样。在动态加载的时候,便需要有加载包文件、或取包中的资源两步操作(框架已经帮我们做好了这部分工作,直接调用API即可)。
Unity3D的资源包
3、动态加载模型
编写如下lua代码(main.lua),使用框架提供的资源管理器(resMgr)加载tank包的TankPrefab文件,加载完成后回调OnLoadFinish方法。在OnLoadFinish中使用Instantiate实例化对象。
-
--主入口函数。从这里开始lua逻辑
-
function Main()
-
LuaHelper = LuaFramework.LuaHelper;
-
resMgr = LuaHelper.GetResManager();
-
resMgr:LoadPrefab('tank', { 'TankPrefab' }, OnLoadFinish);
-
end
-
--加载完成后的回调--
-
function OnLoadFinish(objs)
-
local go = UnityEngine.GameObject.Instantiate(objs[0]);
-
LuaFramework.Util.Log("Finish");
end
完成后运行游戏,即可看到动态加载出来的模型。
动态加载出来的模型
4、加载资源的过程
只有理解了动态加载,即LoadPrefab的过程,才能算是真正的理解了热更新。LoadPrefab为ResourceManager中定义的方法,在Assets\LuaFramework\Scripts\Manager\ResourceManager.cs中实现,建议配合代码看下面的解释。
LoadPrefab的流程如下所示,先是判定当前是否正在加载该资源包,如果没有则调用OnLoadAsset加载资源包、然后解包获取资源、调用回调函数。
LoadPrefab的流程
ResourceManager类定义了m_AssetBundleManifest、m_Dependencies、m_LoadedAssetBundles、m_LoadRequests这4个变量,只要理解了这几个变量的用途,也就能够理解了资源加载的全过程了。这4个变量的类型如下:
ResourceManager定义的几个变量
m_AssetBundleManifest
理解m_AssetBundleManifest之前,需要先理解Unity3D的依赖打包。前面的tank.unity3D中,坦克的预设、坦克的贴图等资源都被打包到一起,没有依赖关系(只要打包时不给贴图单独打包,Unity3D会自动将预设相关的资源都打包进来)。如下图所示。
前面加载坦克制作的资源包
假如有两个坦克预设共用一套贴图,如果像上面那样打包,每个坦克预设各自包含一份贴图,资源会比较大。更好的办法是将公共贴图放到一个包里,每个坦克预设不再包含贴图(如下图)。这种打包方式下,加载TankPrefab前,需要先加载依赖包common.unity3D,坦克预设才能找到贴图。
依赖打包
打包后,Unity3D会产生一个名为AssetBundle.manifest的文件(框架会将该文件放在StreamingAssets中),该文件包含所有包的依赖信息。所以在加载资源前需要先加载这个文件,m_AssetBundleManifest便是指向这个包的变量。相关代码如下:
m_AssetBundleManifest便是指向AssetBundle.manifest的变量
加载这个包后,便可以使用下面的语句获取某个包所依赖的所有包名,然后加载它们。
string[] dependencies = m_AssetBundleManifest.GetAllDependencies(包名);
注:更多Unity3D的依赖打包的解释,可以参见这篇文章:
http://liweizhaolili.blog.163.com/blog/static/16230744201541410275298/
m_LoadedAssetBundles
字典类型的m_LoadedAssetBundles保存了所有已经加载资源包。如果某个包已经被加载过,那下次需要用到它时,直接从字典中取出即可,减少重复加载。简化后的代码如下:
-
IEnumerator OnLoadAsset(XXX)
-
{
-
AssetBundleInfo bundle = GetLoadedAssetBundle(XXX);
-
if(!bundle)
-
bundle = OnLoadAssetBundle(名字);
-
加载资源
-
回调函数处理
-
}
其中GetLoadedAssetBundle方法会判断资源包是否存在于m_LoadedAssetBundles中,并返回资源包。OnLoadAssetBundle为重新加载资源包的方法。
加载资源包后,只需通过 bundle.LoadAssetAsync(资源名,类型)便可加载所需的资源。
m_Dependencies
m_Dependencies记录了所有已加载资源的依赖包,以便在GetLoadedAssetBundle方法中判断资源包是否被完全加载(主体包和所有依赖包都被加载才算完成加载)。简化后的代码如下:
-
IEnumerator OnLoadAssetBundle(包名)
-
{
-
//获取依赖包
-
string[] dependencies = m_AssetBundleManifest.GetAllDependencies(abName);
-
m_Dependencies.Add(abName, dependencies); //更新依赖表
-
//加载依赖包
-
for (int i = 0; i < dependencies.Length; i++)
-
OnLoadAssetBundle(XXX)
-
//然后加载主体资源
-
download = http://WWW.LoadFromCacheOrDownload(包名)
-
//更新加载表
-
m_LoadedAssetBundles.Add(XXX)
-
}
-
AssetBundleInfo GetLoadedAssetBundle(包名)
-
{
-
//判断加载表
-
AssetBundleInfo bundle = m_LoadedAssetBundles[包名];
-
if (bundle == null) return null;
-
//判断依赖包
-
foreach (string 依赖包名 in m_Dependencies[包名])
-
{
-
if (m_LoadedAssetBundles[依赖包名]== null)
-
return null;
-
}
-
return bundle;
}
m_LoadRequests
m_LoadRequests是一个>类型的字典,LoadAssetRequest的定义如下,它用于异步加载后的回调。填写图片摘要(选填)
LoadAssetRequest
由于采用异步加载,加载资源的过程中,程序有可能发起同一个请求,多次加载似乎有些浪费,如下图所示。
两次加载同一资源
更好的办法是,在收到第2次请求时先做判断,如果该资源正在加载,那先记录请求2的回调函数,待资源加载完成,调用所有请求该资源的回调函数,如下图所示。m_LoadRequests便记录每种资源的请求,使程序可以判断该资源是否正在加载,并从m_LoadRequests获取各个资源请求的回调函数。
记录请求2的回调函数
简化后的代码如下:
-
void LoadAsset(包名)
-
{
-
If(m_LoadRequests[abName] == null)
-
{
-
m_LoadRequests[包名].Add(回调函数等);
-
OnLoadAsset();
-
}
-
else
-
{
-
m_LoadRequests[包名].Add(回调函数等);
-
}
-
}
-
IEnumerator OnLoadAsset(XXX)
-
{
-
加载包
-
加载资源
-
foreach( request in m_LoadRequests[包名] )
-
{
-
Request.回调函数();
-
}
}
为实现代码热更新,在Unity3D中使用lua,然而为此也需付出不少代价。其一,使代码结构混乱(尽管可以优化),其二降低了运行速度,其三增加学习成本(还要多学一门语言)。为了热更新,所有的逻辑都要用lua编写,那么怎样用lua编写游戏逻辑呢?
By 罗培羽 (知乎 @罗培羽)
1、Lua的Update方法
第一篇“代码热更新”演示了用lua打印HelloWorld的方法,第二篇“资源热更新”演示了加载坦克模型的方法。这一篇要把两者结合起来,用lua实现“用键盘控制坦克移动”的功能。用Lua和用c#编写的Unity3D程序大同小异,只需正确使用API即可,Lua语言的知识请参见《programing in lua》这本书。
1)Update方法
出于效率的考虑,tolua提供了名为UpdateBeat的对象,在LuaFramework中,只需给UpdateBeat添加回调函数,该函数便会每帧执行,相当于Monobehaviour的Update方法。Lua代码如下所示:
-
function Main()
-
UpdateBeat:Add(Update, self)
-
end
-
function Update()
-
LuaFramework.Util.Log("每帧执行一次");
end
除了UpdateBeat,tolua还提供了LateUpdateBeat和FixedUpdateBeat,对应于Monobehaviour中的LateUpdate和FixedUpdate。
2)控制坦克
现在编写“用键盘控制坦克移动”的lua代码,加载坦克模型后,使用UpdateBeat注册每帧执行的Update方法,然后在Update方法中调用UnityEngine.Input等API实现功能。代码如下:
local go;
--加载的坦克模型
--主入口函数。从这里开始lua逻辑
-
function Main()
-
LuaHelper = LuaFramework.LuaHelper;
-
resMgr = LuaHelper.GetResManager();
-
resMgr:LoadPrefab('tank', { 'TankPrefab' }, OnLoadFinish);
-
end
--加载完成后的回调--
-
function OnLoadFinish(objs)
-
go = UnityEngine.GameObject.Instantiate(objs[0]);
-
LuaFramework.Util.Log("LoadFinish");
-
UpdateBeat:Add(Update, self)
-
end
--每帧执行
-
function Update()
-
LuaFramework.Util.Log("每帧执行");
-
local Input = UnityEngine.Input;
-
local horizontal = Input.GetAxis("Horizontal");
-
local verticla = Input.GetAxis("Vertical");
-
local x = go.transform.position.x + horizontal
-
local z = go.transform.position.z + verticla
-
go.transform.position = Vector3.New(x,0,z)
end
运行游戏,即可用键盘的控制坦克移动。
用键盘的控制坦克移动
2、自定义API
框架中提供了数十个可供lua调用的c#类,但这些往往不够用,需要自己添加,本节将介绍添加自定义API的方法。
1)编写c#类
例如,编写TestLuaFun.类,它包含一个静态方法Log,会打印出“《Unity3D网络游戏实战》是一本好书!”和“《手把手教你用c#制作rpg游戏》也是一本好书!”两行文本。
-
using UnityEngine;
-
using System.Collections;
-
public class TestLuaFun
-
{
-
public static void Log()
-
{
-
Debug.Log("《Unity3D网络游戏实战》是一本好书!");
-
Debug.Log("《手把手教你用c#制作rpg游戏》也是一本好书!");
-
}
}
2)修改CustomSetting
打开CustomSetting.cs,在customTypeList中添加一句“_GT(typeof(TestLuaFun))”。
修改CustomSetting
3)生成wrap文
点击菜单栏的Lua→Clear wrap files和Lua→Generate All,重新生成wrap文件。由于刚刚在customTypeList添加了类,所以会生成TestLuaFun类的wrap文件TestLuaFunWrap.cs。
打开TestLuaFunWrap.cs,可以看到TestLuaFun注册了Log方法。关于static int Log(IntPtr L)的具体含义,请参见《programing in lua》中lua与c++交互的章节。
TestLuaFunWrap
4)测试API
修改main.lua,调用TestLuaFun.Log() ,即可看到效果。
--主入口函数。从这里开始lua逻辑
-
function Main()
-
TestLuaFun.Log()
end
调用自定义API
3、原理
tolua实现了LuaInterface,抛开luaFramework,只需创建lua虚拟机,便能在c#中调用lua代码,如下所示。
-
using UnityEngine;
-
using System.Collections;
-
using LuaInterface;
-
public class test : MonoBehaviour
-
{
-
void Start ()
-
{
//初始化
-
LuaState lua = new LuaState();
-
LuaBinder.Bind(lua);
//lua代码
-
string luaStr =
-
@"
-
print('hello tolua#, 广告招租')
-
LuaFramework.Util.Log('HelloWorld');
-
TestLuaFun.Log()
-
";
//执行lua脚本
-
lua.DoString(luaStr);
-
}
}
创建lua虚拟机,执行lua代码
实际上LuaFramework也是用了相似的方法,框架启动后,会创建LuaManager、LuaLooper的实例。LuaManager创建lua虚拟机并调用Main.lua的Main方法,LuaLooper处理了UpdateBeat相关的事情。如下所示:
LuaManager的启动过程
基于组件的编程模式是Unity3D的核心思想之一,然而使用纯lua编程,基本就破坏了这一模式。那么有没有办法做一些封装,让Lua脚本也能挂载到游戏物体上,作为组件呢?
1、设计思想
在需要添加Lua组件的游戏物体上添加一个LuaComponent组件,LuaComponent引用一个lua表,这个lua表包含lua组件的各种属性以及Awake、Start等函数,由LuaComponent适时调用Lua表所包含的函数。
下面列举lua组件的文件格式,它包含一个表(如Component),这个表包含property1 、property2 等属性,包含Awake、Start等方法。表中必须包含用于派生对象的New方法,它会创建一个继承自Component的表o,供LuaComponent调用。
Component= --组件表
{
property1 = 100,
property2 = “helloWorld”
}
function Component:Awake()
print("TankCmp Awake name = "..self.name );
end
function Component:Start()
print("TankCmp Start name = "..self.name );
End
--更多方法略
function Component:New(obj)
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
2、LuaComponent组件
LuaComponent主要有Get和Add两个静态方法,其中Get相当于UnityEngine中的GetComponent方法,Add相当于AddComponent方法,只不过这里添加的是lua组件不是c#组件。每个LuaComponent拥有一个LuaTable(lua表)类型的变量table,它既引用上述的Component表。
Add方法使用AddComponent添加LuaComponent,调用参数中lua表的New方法,将其返回的表赋予table。
Get方法使用GetComponents获取游戏对象上的所有LuaComponent(一个游戏对象可能包含多个lua组件,由参数table决定需要获取哪一个),通过元表地址找到对应的LuaComponent,返回lua表。代码如下:
using UnityEngine;
using System.Collections;
using LuaInterface;
using LuaFramework;
public class LuaComponent : MonoBehaviour
{
//Lua表
public LuaTable table;
//添加LUA组件
public static LuaTable Add(GameObject go, LuaTable tableClass)
{
LuaFunction fun = tableClass.GetLuaFunction("New");
if (fun == null)
return null;
object[] rets = fun.Call (tableClass);
if (rets.Length != 1)
return null;
LuaComponent cmp = go.AddComponent();
cmp.table = (LuaTable)rets[0];
cmp.CallAwake ();
return cmp.table;
}
//获取lua组件
public static LuaTable Get(GameObject go,LuaTable table)
{
LuaComponent[] cmps = go.GetComponents();
foreach (LuaComponent cmp in cmps)
{
string mat1 = table.ToString();
string mat2 = cmp.table.GetMetaTable().ToString();
if(mat1 == mat2)
{
return cmp.table;
}
}
return null;
}
//删除LUA组件的方法略,调用Destory()即可
void CallAwake ()
{
LuaFunction fun = table.GetLuaFunction("Awake");
if (fun != null)
fun.Call (table, gameObject);
}
void Start ()
{
LuaFunction fun = table.GetLuaFunction("Start");
if (fun != null)
fun.Call (table, gameObject);
}
void Update ()
{
//效率问题有待测试和优化
//可在lua中调用UpdateBeat替代
LuaFunction fun = table.GetLuaFunction("Update");
if (fun != null)
fun.Call (table, gameObject);
}
void OnCollisionEnter(Collision collisionInfo)
{
//略
}
//更多函数略
}
3、调试LuaCompomemt
现在编写名为TankCmp的lua组件,测试LuaCompomemt的功能,TankCmp会在Awake、Start和Update打印出属性name。TankCmp.lua的代码如下:
TankCmp =
{
--里面可以放一些属性
Hp = 100,
att = 50,
name = "good tank",
}
function TankCmp:Awake()
print("TankCmp Awake name = "..self.name );
end
function TankCmp:Start()
print("TankCmp Start name = "..self.name );
end
function TankCmp:Update()
print("TankCmp Update name = "..self.name );
end
--创建对象
function TankCmp:New(obj)
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
编写Main.lua,给游戏对象添加lua组件。
require "TankCmp"
--主入口函数。从这里开始lua逻辑
function Main()
--组件1
local go = UnityEngine.GameObject ('go')
local tankCmp1 = LuaComponent.Add(go,TankCmp)
tankCmp1.name = "Tank1"
--组件2
local go2 = UnityEngine.GameObject ('go2')
LuaComponent.Add(go2,TankCmp)
local tankCmp2 = LuaComponent.Get(go2,TankCmp)
tankCmp2.name = "Tank2"
end
运行游戏,即可看到lua组件的运行结果:
程序运行结果
程序运行结果
4、坦克组件
下面代码演示用lua组件实现“用键盘控制坦克移动”的功能,TankCmp.lua的代码如下:
TankCmp =
{
name = "good tank",
}
function TankCmp:Update(gameObject)
print("TankCmp Update name = "..self.name );
local Input = UnityEngine.Input;
local horizontal = Input.GetAxis("Horizontal");
local verticla = Input.GetAxis("Vertical");
local x = gameObject.transform.position.x + horizontal
local z = gameObject.transform.position.z + verticla
gameObject.transform.position = Vector3.New(x,0,z)
end
--创建对象
function TankCmp:New(obj)
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
Main.lua先加载坦克模型,然后给他添加lua组件,代码如下:
require "TankCmp"
--主入口函数。从这里开始lua逻辑
function Main()
LuaHelper = LuaFramework.LuaHelper;
resMgr = LuaHelper.GetResManager();
resMgr:LoadPrefab('tank', { 'TankPrefab' }, OnLoadFinish);
end
--加载完成后的回调--
function OnLoadFinish(objs)
go = UnityEngine.GameObject.Instantiate(objs[0]);
LuaComponent.Add(go,TankCmp)
end
运行游戏,即可用键盘的控制坦克移动。
坦克组件运行结果
界面系统在游戏中占据重要地位。游戏界面是否友好,很大程度上决定了玩家的体验;界面开发是否便利,也影响着游戏的开发进度。Unity3D 的UGUI系统,使用户可以“可视化地”开发界面,那么怎样用Lua去调用UGUI呢?
1、显示UI界面
下面演示如何显示一个UI界面。由于UI界面也是一种资源,使用第二篇“资源热更新”的方法即可。这个例子中,制作一个含有按钮的界面,然后组成名为Panel1的UI预设,存放到Tank目录下。
图:Panel1
前面(第二篇)已在Packager类HandleExampleBundle方法中添加了一句“AddBuildMap("tank" + AppConst.ExtName, "*.prefab", "Assets/Tank");”(当然也可以添加到其他地方),它会把Tank目录下的所有预设打包成名为tank的资源包。故而点击“Build xxx Resource”后,Panel1也会被打包到tank资源包中。
修改Lua入口函数Main.lua中的Main方法,在加载资源后把panel1放到Canvas下(需要在场景中添加画布),然后调整它的位置和大小。
--主入口函数。从这里开始lua逻辑
function Main()
LuaHelper = LuaFramework.LuaHelper;
resMgr = LuaHelper.GetResManager();
resMgr:LoadPrefab('tank', { 'Panel1' }, OnLoadFinish);
end
--加载完成后的回调--
function OnLoadFinish(objs)
--显示面板
go = UnityEngine.GameObject.Instantiate(objs[0]);
local parent = UnityEngine.GameObject.Find("Canvas")
go.transform:SetParent(parent.transform);
go.transform.localScale = Vector3.one;
go.transform.localPosition = Vector3.zero;
end
运行游戏,即可看到加载出来的界面。
图:加载出来的界面
2、事件响应
c#中可以使用事件监听的方法给UI组件添加事件。例如,添加按钮点击事件的方法如下:
Button btn = go.GetComponent ();
btn.onClick.AddListener
(
delegate()
{
this.OnClick(go)
}
);
然而在LuaFramework的API中,没能找到合适的方法,只能根据第三篇中“自定义API”的方法,自己编写一套了。编写UIEvent类,它包含用于添加监听事件的AdonClick和清除监听事件的ClearButtonClick方法,代码如下所示(完成后记得要“修改CustomSetting”和“生成wrap文件”)。
using UnityEngine;
using System.Collections;
using LuaInterface;
using UnityEngine.UI;
public class UIEvent
{
//添加监听
public static void AdonClick(GameObject go, LuaFunction luafunc)
{
if (go == null || luafunc == null)
return;
Button btn = go.GetComponent ();
if (btn == null)
return;
btn.onClick.AddListener
(
delegate()
{
luafunc.Call(go);
}
);
}
//清除监听
public static void ClearButtonClick(GameObject go)
{
if (go == null)
return;
Button btn = go.GetComponent ();
if (btn == null)
return;
btn.onClick.RemoveAllListeners();
}
}
接下来测试下这套API,修改Main.lua,代码如下:
--主入口函数。从这里开始lua逻辑
function Main()
略
end
--加载完成后的回调--
function OnLoadFinish(objs)
--显示面板
略
--事件处理
local btn = go.transform:FindChild("Button").gameObject
UIEvent.AdonClick(btn, OnClick)
end
function OnClick()
print("触发按钮事件")
end
运行游戏,点击按钮,OnClick方法即被调用。
图:按钮的事件响应
读者可以使用相似的方法监听其他UI组件,这里就只演示按钮事件了。
3、界面管理器
LuaFramework提供了一套简单的(不完善的)界面管理器,具体代码请参见PanelManager类。PanelManager类的CreatePanel方法完成异步加载资源,在加载完成后,会设置面板的大小和位置,然后调用回调函数。与上面用lua加载界面的方法完全一样。
图:PanelManager的CreatePanel方法
LuaFramework会给每个界面添加名为LuaBehaviour的组件,它拥有用于添加按钮监听的AddClick方法,相关代码如下,与UIEvent的AdonClick方法相似。
图:LuaBehaviour的AddClick方法
在LuaFramework的PureMVC架构中,如果要添加一个界面,需要编写对应的Controller、View,以及修改3个框架自带的lua文件,比较繁琐。因此在实际项目中有必要重写PanelManager,由它实现界面的加载及事件处理。
LuaFramework内置了网络模块(NetworkManager、SocketClient、ByteBuffer、Converter、Protocal),本篇将会介绍该模块的调用方法以及其原理。
1、发起连接
发起连接是客户端网络通信的第一步,LuaFramewor中,只需通过LuaFramework.AppConst.SocketAddress和LuaFramework.AppConst.SocketPort设置ip和端口,然后调用NetworkManager的SendConnect方法即可发起连接。Main.lua的代码如下:
require "Network"
--主入口函数。从这里开始lua逻辑
function Main()
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
local AppConst = LuaFramework.AppConst
AppConst.SocketPort = 1234;
AppConst.SocketAddress = "127.0.0.1";
networkMgr:SendConnect();
end
在收到服务端回应后,LuaFramework会调用Network的OnSocket方法(写死)。新建名为Network.lua的文件,处理消息回调。在如下的代码中,Protocal代表协议号,比如“连接服务器”(Protocal.Connect)的协议号是101,在OnSocket的参数中,key便是收到的协议号,data是收到的数据。
Network = {};
--协议
Protocal = {
Connect = '101'; --连接服务器
Exception = '102'; --异常掉线
Disconnect = '103'; --正常断线
Message = '104'; --接收消息
}
--Socket消息--
function Network.OnSocket(key, data)
if key == 101 then
LuaFramework.Util.Log('OnSocket Connect');
else
LuaFramework.Util.Log('OnSocket Other');
end
end
为了测试网络功能,需要编写服务端,这里使用c#编写一套简单的服务端程序,仅为调试使用,代码如下:
using System;
using System.Net;
using System.Net.Sockets;
using System.Linq;
class MainClass
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
//Socket
Socket listenfd = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 1234);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
while (true)
{
//Accept
Socket connfd = listenfd.Accept();
Console.WriteLine("[服务器]Accept");
}
}
}
运行服务端和客户端,客户端会发起连接,服务端accept该连接后回应,客户端会显示“OnSocket Connect”
图:服务端
图:客户端
此时把服务端关掉(断开连接),客户端会收到协议号为102的消息,即异常掉线(Exception)。
图:异常掉线
调用NetworkManager.SendConnect实际是调用BeginConnect发起连接。连接之后,回调OnConnect方法。
图:连接过程
OnConnect方法调用NetworkManager.AddEvent,排除设计模式的内容,相当于调用Network.lua的OnSocket方法。传入OnSocket的第1个参数为101(Protocal.Connect),指代协议名,第2个参数是空的字节流。网络模块中定义了101、102、103这3个固定的协议号,分别代表连接服务器、异常断线和正常断线。
图:连接回调
2、发送和接收
接下来尝试发送和接收数据。LuaFramework默认(如果不去改它的代码)使用的协议格式如下图所示,前面的2个字节为消息长度,用于处理沾包分包,随后的2个字节代表协议号(如上面的101、102、103),最后才是消息的内容。
图:协议
修改Network.lua,在连接成功后(OnSocket方法的101协议),调用send发送一串协议号为104的数据。服务端收到数据后回射给客户端,客户端在收到回应后(OnSocket方法的104协议),读取并显示出来。
send方法中新建了一个buffer,然后往buffer中添加协议号(104)和协议内容(字符串:《Unity3D网络游戏实战》是一本好书!),最后调用networkMgr:SendMessage()发送数据。networkMgr:SendMessage()会自动计算协议长度,并附加到buffer上发送出去。
--Socket消息--
function Network.OnSocket(key, data)
if key == 101 then
LuaFramework.Util.Log('OnSocket Connect');
Send()
elseif key == 104 then
LuaFramework.Util.Log('OnSocket Message ');
local str = data:ReadString();
LuaFramework.Util.Log('收到的字符串:'..str);
else
LuaFramework.Util.Log('OnSocket Other '..key);
end
end
function Send()
--组装数据
local buffer = LuaFramework.ByteBuffer.New();
buffer:WriteShort(Protocal.Message);
buffer:WriteString("《Unity3D网络游戏实战》是一本好书!");
--发送
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
networkMgr:SendMessage(buffer);
LuaFramework.Util.Log('数据发送完毕');
end
修改服务端程序,读出接收到的内容,并echo回去。
public static void Main(string[] args)
{
略,没有改动
while (true)
{
//Accept
Socket connfd = listenfd.Accept();
Console.WriteLine("[服务器]Accept");
//Recv 不考虑各种意外,只做测试
byte[] readBuff = new byte[100];
int count = connfd.Receive(readBuff);
//显示字节流
string showStr = "";
for (int i = 0; i < count; i++)
{
int b = (int)readBuff[i];
showStr += b.ToString() + " ";
}
Console.WriteLine("[服务器接收]字节流:"+ showStr);
//解析协议
Int16 messageLen = BitConverter.ToInt16(readBuff,0);
Int16 protocal = BitConverter.ToInt16(readBuff,2);
Int16 strLen = BitConverter.ToInt16(readBuff,4);
string str = System.Text.Encoding.UTF8.GetString(readBuff, 6, strLen);
Console.WriteLine("[服务器接收] 长度:" + messageLen);
Console.WriteLine("[服务器接收] 协议号:" + protocal);
Console.WriteLine("[服务器接收] 字符串:" + str);
//Send(echo)
byte[] writeBuff = new byte[count];
Array.Copy(readBuff,writeBuff,count);
connfd.Send(writeBuff);
}
}
运行游戏,可以看到服务端收到的如图所示的信息。字节流的前两位“53 0”表示消息长度为53字节,紧跟着的“104 0”代表协议号104。在字符串的封装中(buffer:WriteString),程序会先在buffer中添加字符串的长度,最后才是字符串的内容。“49 0”即表示“《Unity3D网络游戏实战》是一本好书!”占用49个字节(14个中文符号,每个3字节,7个英文符号,每个1字节)。协议长度53字节 = 协议号2个字节 + 字符串长度2字节 + 字符串内容49字节。
图:服务端收到的信息
客户端收到服务端回射的消息后,也会显示出来,如下图所示。
图:客户端收到的消息
在lua中调用networkMgr:SendMessage(buffer)时,实际上相当于调用了SocketClient的WriteMessage方法,该方法会计算协议的长度,然后将长度和内容组装在一起,调用BeginWrite发送数据。
图:发送数据
在建立连接后,SocketClient会调用BeginRead,当收到服务端的消息时,回调OnRead方法。OnRead又调用了OnReceive方法。
图:接收数据过程
OnReceive方法完成沾包分包处理,然后调用AddEvent方法分发消息(相当于调用了lua中NetWork表的OnSocket方法)。
图:解析数据过程
关于BeginRead、BeginConnect等方法的介绍,读者可以查看c#网络编程的资料或参照《Unity3D网络游戏实战》第6章“网络基础”。
3、消息分发
一款游戏往往涉及很多条网络通信协议,在Network.OnSocket中,如果只用ifelse语句处理不同协议,代码往往会混乱不堪。LuaFramework集成了消息分发的方法,用法如下所示。
1、引用LuaFramework\Lua\events.lua,然后使用Event.AddListener添加监听,例如“Event.AddListener(Protocal.Connect, Network.OnConnect); ”表示当收到101协议(Protocal.Connect)时,回调Network.OnConnect方法。Main.lua代码如下:
require "Network"
Event = require 'events'
--主入口函数。从这里开始lua逻辑
function Main()
local LuaHelper = LuaFramework.LuaHelper
local networkMgr = LuaHelper.GetNetManager()
local AppConst = LuaFramework.AppConst
AppConst.SocketPort = 1234;
AppConst.SocketAddress = "127.0.0.1";
Event.AddListener(Protocal.Connect, Network.OnConnect);
Event.AddListener(Protocal.Message, Network.OnMessage);
networkMgr:SendConnect();
end
2、在需要分发消息的地方调用Event.Brocast,然后编写相应的回调函数。Network.lua的部分代码如下:
--Socket消息--
function Network.OnSocket(key, data)
LuaFramework.Util.Log('OnSocket 消息分发:'..key);
Event.Brocast(tostring(key), data);
end
function Network.OnConnect(data)
LuaFramework.Util.Log('Network.OnConnect');
Send()
end
function Network.OnMessage(data)
LuaFramework.Util.Log('Network.OnMessage');
local str = data:ReadString();
LuaFramework.Util.Log('收到的字符串:'..str);
end
运行游戏,可以看到消息分发的结果。
图:消息分发
调用Event.AddListener,实际上是在一个表中添加数据,把某个协议号对应于某个方法的信息记录起来。
图:AddListener的过程
当调用Event.Brocast时,程序会查找这份表,然后执行回调方法。这里使用了协程来调用回调函数。使用协程的目的应该是不让回调逻辑阻碍主体逻辑,然而由于协程是单线程的,这点不起作用。除非回调函数也使用协程,相互配合。所以这里应该可以不用协程的。
图:Brocast的过程
LuaFramework使用了PureMVC框架。百度百科上说:“PureMVC是在基于模型、视图和控制器MVC模式建立的一个轻量级的应用框架”。PureMVC框架可以做到较好的解耦,减少游戏代码的相互调用。然而LuaFramework整合PureMVC属于“杀鸡用牛刀”,实质上只用到了事件分发(也可能是我理解得不够透彻)。如果单纯写一套事件分发系统,可能不到100行代码就能完成。
1、解耦的好处
如果没有很好的解耦设计,游戏功能越多,代码就越乱,最后没人敢改动。举个例子,假如游戏中背包(item)和成就(Achieve)两项功能,各用一个类实现。当玩家获得100个经验豆(一种道具)时,会获得“拥有100个经验豆”的成就;当成就点数达到300时,会获得道具奖励。一种常见的实现方法是调用对方的public函数,代码如下所示。然而如果一款游戏有几百上千个类,之间又相互调用,如果某些功能需要大改(例如删掉成就功能),那其他的类也得改动。
Class Item
{
public AddItem()
{
if(经验豆 > 100)
achieve.AddAchieve(“拥有100个经验豆”)
}
}
Class Achieve
{
public AddAchieve()
{
成就点数 + 10
if(成就点数 > 300)
item.AddItem(宝石)
}
}
如果使用事件分发,各个类之间的联系就减弱了。如下所示的代码中背包类(Item)监听了消息“添加道具”,成就类(Achieve)监听了消息“添加成就”。如果达成成就需要添加奖励,只需派发“添加道具”这条消息,由背包类去执行。这样类与类之间不存在相互调用,就算大改功能甚至删掉功能,其他类都受到的影响比较小。
Class Item
{
Start()
{
监听(“添加道具”,AddItem)
}
private AddItem()
{
if(经验豆 > 100)
分发(“添加成就”,“拥有100个经验豆”)
}
}
Class Achieve
{
Start()
{
监听(“添加成就”,AddAchieve)
}
private AddAchieve()
{
成就点数 + 10
If(成就点数 > 300)
分发(“添加道具”, 宝石)
}
}
2、MVC的使用方法
LuaFramework中的Framwork目录存放着PureMVC框架的代码,个人认为在LuaFramework中属于过度设计(毕竟从其他地方拷过来的)。它的原理并不复杂,用一个列表把监听信息保存起来,在派发消息时,查找对应的监听表,找到需要回调的对象。
PureMVC框架便是实现了“注册/分发”模式(发布/订阅、观察者模式),可以调用RegisterCommand注册消息(命令),调用SendMessageCommand方法分发消息。RegisterCommand方法可以把某个继承ControllerCommand 的类注册到指定的消息下,在事件分发时调用该类的Execute方法。
例如新建一个名为TestCommand的类,让它继承ControllerCommand,然后编写Execute方法处理具体事务。
using UnityEngine;
using System.Collections;
public class TestCommand : ControllerCommand
{
public override void Execute(IMessage message)
{
Debug.Log("name=" + message.Name);
Debug.Log("type=" + message.Type);
}
}
接着,编写另一个类来处理消息。这个类先调用AppFacade.Instance.RegisterCommand()将TestCommand类注册到“TestMessage”消息下。然后使用SendMessageCommand()派发“TestMessage”消息。框架将会创建一个TestCommand实例,并调用它的Execute方法。
public class Main : MonoBehaviour
{
void Start()
{
AppFacade.Instance.RegisterCommand ("TestMessage",
typeof(TestCommand));
AppFacade.Instance.SendMessageCommand ("TestMessage");
}
}
运行结果如下所示,可以看到分发消息后,TestCommand的Execute方法被调用。
Execute方法的参数message包含了Name,Body,Type三个成员(如下图所示)。其中Name是命令名,Body是一个任意类型的参数。
如下代码所示,在SendMessageCommand中可以给消息的Body传值,相应的Execute方法便可以获取它。
void Start()
{
AppFacade.Instance.RegisterCommand ("TestMessage",
typeof(TestCommand));
AppFacade.Instance.SendMessageCommand ("TestMessage", "这是字符串");
}
运行结果如下图所示。
总而言之,LuaFramework中所谓的pureMVC只是一套“注册/分发”机制,完全可以用c#的事件来实现。另《Unity3D网络游戏实战》中的客户端网络模块部分也使用的“注册/分发”机制,有兴趣的读者可以看看。
3、MVC与Unity3D组件的结合
pureMVC与Unity3D组件之间有一些封装,只要让组件继承View类(View类继承MonoBehavior),即使用pureMVC框架的RegisterMessage和SendMessageComman方法实现“注册/分发”机制。
例如,新建一个继承自View的TestManage组件,在Start 方法中它注册了“msg1”、“msg2”、“msg3”三个消息的监听。在Update方法中,当按下空格键时,分发消息“msg1”。
当接收到消息后,指定对象(这里指定this)的OnMessage方法会被调用,参数message里面包含了命令名、Body等信息。代码如下所示。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class TestManage : View
{
// Use this for initialization
void Start ()
{
List<string> regList = new List<string>();
regList.Add("msg1");
regList.Add("msg2");
regList.Add("msg3");
RegisterMessage(this,regList);
}
// Update is called once per frame
void Update ()
{
if (Input.GetKeyUp (KeyCode.Space))
{
facade.SendMessageCommand("msg1", null);
}
}
public override void OnMessage(IMessage message)
{
Debug.Log ("OnMessage " + message.Name);
}
}
此外LuaFramework的各个Manager(如GameManager,LuaManager,SoundManager等)也都继承自View类,可以使用“注册/分发”机制。
LuaFramework内置的管理器包括GameManager(处理热更新)、luaManager(lua脚本管理器)、PanelManager(界面管理器)、NetworkManager(网络管理器)、ResourceManager(资源管理器)、TimerManager(时间管理器)、线程管理器(ThreadManager)和SoundManager(声音管理器)。其中GameManager、luaManager、PanelManager、NetworkManager、ResourceManager在前面的文章中已经有过介绍,这一篇讲讲讲播放声音相关的SoundManager。
1、使用方法
SoundManager估计是从其他地方拷过来的,并不能很好的与框架结合,这里我们先看看原来的SoundManager的使用方法,再介绍它的不足之处及改进方法。虽然SoundManager定义了好几个方法,但能直接在lua中使用的只有用于播放背景音乐的PlayBacksound。
编写播放声音的代码前,需要在GameManager上挂载AudioSource组件,以播放背景音乐。
把声音文件放到Resource目录下,由于SoundManager使用Resources.Load加载声音文件,声音文件必须放到这个目录下。如下图所示。
然后编写lua代码,调用soundMgr:PlayBacksound即可,它的第一个参数指明Resource目录下的文件名,第二个参数为true表示开始播放,false表示停止播放。
--主入口函数。从这里开始lua逻辑
function Main()
LuaHelper = LuaFramework.LuaHelper;
soundMgr = LuaHelper.GetSoundManager();
soundMgr:PlayBacksound("motor", true)
end
运行游戏,即可听到音效。关于Unity3D播放声音的内容,大家也可以参考《Unity3D网络游戏实战》哦!
2、代码解析
SoundManager的示意代码如下,实际上是使用Resources.Load来加载资源的,所以声音文件必须放在Resources目录下。
这种用法违背了热更新框架的设计,因为在Resources目录下的文件并不能热更。
3、改进的声音管理器
这一部分我们需要改进声音管理器,实现这么几个功能:
1)从本地“数据目录”读取打包后的声音文件,使它支持热更新;
2)添加播放/停止背景音乐的PlayBackSound/StopBackSound和播放音效的PlaySound方法;
3)使用缓存存储加载后的声音文件,以提高运行效率。
为了支持加载AudioClip的加载,在ResourceManager中添加LoadAudioClip方法,该方法将会加载资源包abName的资源assetName,加载完AudioClip资源后调用回调函数func,代码如下。
//载入音效资源
public void LoadAudioClip(string abName,
string assetName, Action<UObject[]> func)
{
LoadAsset<AudioClip>(abName, new string[] { assetName }, func);
}
修改SoundManager,使用Hashtable类型的sounds存储加载后的声音,包含PlayBackSound、StopBackSound和PlaySound三个API。代码如下所示。
先是整体kuan
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace LuaFramework
{
public class SoundManager : Manager
{
private AudioSource audio;
private Hashtable sounds = new Hashtable();
string backSoundKey = "";
void Start()
{
audio = GetComponent<AudioSource>();
if (audio == null)
gameObject.AddComponent<AudioSource> ();
}
//回调函数原型
private delegate void GetBack(AudioClip clip, string key);
//获取声音资源
private void Get(string abName, string assetName, GetBack cb)
{
string key = abName + "." + assetName;
if(sounds [key] == null)
{
ResManager.LoadAudioClip(abName, assetName, (objs)=>
{
if(objs == null || objs[0] == null)
{
Debug.Log("PlayBackSound fail);
cb(null,key);
return;
}
else
{
sounds.Add(key, objs[0]);
cb(objs[0] as AudioClip ,key);
return;
}
});
}
else
{
cb(sounds [key] as AudioClip,key);
return;
}
}
}
}
PlayBackSound:
//播放背景音乐
public void PlayBackSound(string abName, string assetName)
{
backSoundKey = abName + "." + assetName;
Get(abName, assetName,(clip, key)=>
{
if(clip == null)
return;
if(key != backSoundKey)
return;
audio.loop = true;
audio.clip = clip;
audio.Play();
});
}
StopBackSound:
//停止背景音乐
public void StopBackSound()
{
backSoundKey = "";
audio.Stop ();
}
//播放音效
public void PlaySound(string abName, string assetName)
{
Get(abName, assetName,(clip, key)=>
{
if(clip == null)
return;
if(Camera.main == null)
return;
AudioSource.PlayClipAtPoint(clip,
Camera.main.transform.position);
});
}
修改代码后,需要重新生成wrap文件(点击菜单栏的Lua→Clear wrap files和Lua→Generate All)。然后编写lua代码调试它吧!这里演示的是先播放背景音乐,3秒后停止播放,每隔0.3秒播放一次音效的功能。
--主入口函数。从这里开始lua逻辑
function Main()
LuaHelper = LuaFramework.LuaHelper;
soundMgr = LuaHelper.GetSoundManager();
soundMgr:PlayBackSound("sound", "motor")
UpdateBeat:Add(Update, self)
end
local lastTime = 0
function Update()
if Time.time > 3 then
soundMgr:StopBackSound();
end
if Time.time - lastTime > 0.3 then
soundMgr:PlaySound("sound", "shoot")
lastTime = Time.time
end
end
LuaFramework内置了线程管理器ThreadManager,一开始我以为这是个创建线程、终止线程等方法的封装。然而不是,它是热更新时使用线程下载资源的具体实现。那让我们来看看线程管理器的工作原理吧。
@罗培羽
1、GameManager的调用
那么先看看在热更新过程中哪些地方调用到ThreadManager。热更新由GameManager执行(相关代码如下图所示),它在对比本地文件和网络资源的差异后,将需要下载的文件名存放到列表中,然后遍历列表,调用BeginDownload下载。从代码可以看出,它通过ISDownOK判断该文件是否下载完成,然后下载下一个文件,一个个的下载文件。
BeginDownload(代码如下所示)便调用ThreadManager的AddEvent方法。ThreadManager并不是真正意义上的线程管理器,它只管理一条“下载线程”,通过AddEvent将要下载的文件名放到“代办列表”中,该线程依次下载它们。其中的OnThreadCompleted是“回调函数”,在下载该文件后,会通过消息的方式回调它。
在“下载线程”下载完一个文件后,它以通知的形式调用“回调函数”OnThreadCompleted(代码如下所示),该方法将会设置“下载完成列表”downloadFiles。
再看看IsDownOk(代码如下所示)方法,当“下载完成列表”包含该文件时,说明下载已经完成,可以进行下一个文件的下载。
2、ThreadManager的启动
ThreadManager启动时,开启一个线程“下载线程”,相关代码如下所示。由此ThreadManager仅仅是管理一条线程,而不是真正意义的线程管理器。
3、AddEvent方法
AddEvent是给线程添加任务的方法,代码如下,其实就是给events队列添加一个值。
Events的定义如下所示:
ThreadEvent包含事件名key和参数evParams,代码如下所示。
3、下载过程
“下载线程”执行了OnUpdate方法(代码如下所示),它调用OnDownloadFile下载文件。
OnDownloadFile(代码如下所示)又调用了DownloadFileAsync下载文件,下载文件过程中ProgressChanged方法会被调用。
ProgressChanged方法记录了下载进度,当进度为100%时,使用m_SyncEvent发送通知,相当于调用“回调函数”OnThreadCompleted。
4、改进
这套线程管理器依然有“杀鸡用牛刀”之嫌,“任务列表”并没有实际作用(因为GameManager控制了下载进度,一个个下载),消息分发部分也太复杂,实际上只用回调函数之类的方法便能够实现。
个人认为线程管理器应当提供线程调度的方法,具体的下载逻辑可在GameManager中实现。而且下载功能不一定非要用线程,协程也能够解决,而且更简单。代码如下所示。
由于热更新需要下载不少的文件,一个个下载实在太慢。如果能开启多个线程,同时下载,可在一定程度上提高下载速度。
框架并没有处理下载失败的情况,一般情况下,当一个文件下载失败,应当重试,在重试多次依然无法下载时,才弹出错误。
终于到了本系列完结的时候了。
现在,大家对LuaFramework有个全方位的理解了吧!接下来通过一个例子总结ulua,作为“lua逻辑”的延伸,说明lua的写法。这个例子中玩家能够控制2D游戏角色走动,并且发射炮弹。
1、目标
制作如图所示的游戏,玩家可以通过键盘控制角色上下左右移动,角色有4个面向,走动过程中会播放行走动画。当玩家点击鼠标左键,角色会发射一颗炮弹。
2、游戏资源
使用下图所示的图片作为游戏角色(该图片来自rpg maker),在导入Unity后将它切割成12张小图。
使用如下图所示的图片作为炮弹。
在游戏场景中新建画布,画布下摆放一个名为Panel的面板,代表游戏场景。面板下有res和map两个子物体,map(Image)为一张场景图,role(Image)为游戏中的角色,bullet(Image)为游戏中的子弹(同一时间只能发射一颗子弹)。
Res子物体存放12张角色图片(Image),之后会使用这些资源替换map.role的图片,以实现动画效果。
然后将Panel做成预设,存放到SimpleGame目录下。并且在GameManager空物体上添加Game组件,以启动框架。
修改Packager.cs,在HandleExampleBundle添加如下代码,将SimpleGame目录下的预设打包。然后点击LuaFramework→Build Windows Resource打包。
//小游戏
AddBuildMap("SimpleGame" + AppConst.ExtName, "*.prefab", "Assets/SimpleGame");
具体的框架设置请参加第一篇和第二篇,这里仅做简单描述。
3、编写行走代码
游戏使用UI组件,在CustomSettings.cs中添加如下两行,使tolua生成Image和Sprite相关的调用。
_GT(typeof(Image)),
_GT(typeof(Sprite)),
打开main.lua(如何运行main.lua请参见第一篇)开始编写代码。程序从Main方法开始执行,使用LoadPrefab(请参见第二篇)加载之前打包的资源文件Panel。这里还定义几个变量,其中map代表游戏场景(panel.map),role代表游戏角色(panel.map.role),roleImage是游戏角色中的图片组件,roleRes代表各个面向的角色图片,比如roleRes[“UP”]将会包含panel.res中3张角色朝上的图。roleAnm代表当前角色的动画,每个面向有3个动画,对应于不同的图片。lastAnmTime代表展现角色动画帧的时间,用于控制动画播放速度。
--主入口函数。从这里开始lua逻辑
function Main()
LuaHelper = LuaFramework.LuaHelper;
resMgr = LuaHelper.GetResManager();
resMgr:LoadPrefab('SimpleGame', { 'Panel' }, OnLoadFinish);
end
local map
local role
local roleImage
local roleRes = {
["UP"] = {},
["DOWN"] = {},
["LEFT"] = {},
["RIGHT"] = {},
}
local roleAnm = 1;
local lastAnmTime = 0;
--加载完成后的回调--
function OnLoadFinish(objs)
--暂略
end
接着编写加载完成的回调方法OnLoadFinish,它处理下面几件事情。
1、使用Instantiate实例化面板,并且设置面板的坐标,具体请参见第5篇。
2、获取面板中的部件,给map、role、roleImage赋值。
3、获取素材res中的图片,赋值给roleRes,之后roleRes ["UP"],roleRes ["DOWN"],roleRes ["LEFT"],roleRes ["RIGHT"]都包含3张同面向不同动画的图片。
4、使用UpdateBeat:Add()初始化Update方法(具体参照第三篇)。
function OnLoadFinish(objs)
--显示面板
go = UnityEngine.GameObject.Instantiate(objs[0])
local parent = UnityEngine.GameObject.Find("Canvas")
go.transform:SetParent(parent.transform, false)
--获取元素
map = go.transform:FindChild("map").gameObject
role = map.transform:FindChild("role").gameObject
roleImage = role:GetComponent("Image")
--获取素材
local res = go.transform:FindChild("res").gameObject
roleRes["DOWN"][1] = res.transform:FindChild("role (0)").gameObject:GetComponent("Image").sprite
roleRes["DOWN"][2] = res.transform:FindChild("role (1)").gameObject:GetComponent("Image").sprite
roleRes["DOWN"][3] = res.transform:FindChild("role (2)").gameObject:GetComponent("Image").sprite
roleRes["LEFT"][1] = res.transform:FindChild("role (3)").gameObject:GetComponent("Image").sprite
roleRes["LEFT"][2] = res.transform:FindChild("role (4)").gameObject:GetComponent("Image").sprite
roleRes["LEFT"][3] = res.transform:FindChild("role (5)").gameObject:GetComponent("Image").sprite
roleRes["RIGHT"][1] = res.transform:FindChild("role (6)").gameObject:GetComponent("Image").sprite
roleRes["RIGHT"][2] = res.transform:FindChild("role (7)").gameObject:GetComponent("Image").sprite
roleRes["RIGHT"][3] = res.transform:FindChild("role (8)").gameObject:GetComponent("Image").sprite
roleRes["UP"][1] = res.transform:FindChild("role (9)").gameObject:GetComponent("Image").sprite
roleRes["UP"][2] = res.transform:FindChild("role (10)").gameObject:GetComponent("Image").sprite
roleRes["UP"][3] = res.transform:FindChild("role (11)").gameObject:GetComponent("Image").sprite
--UpdateBeat
UpdateBeat:Add(Update, self)
end
编写Update方法,它根据用户输入改变坐标(具体参见第三篇),并且根据不同的移动方向设置角色图片素材,将roleImage.sprite替换成roleRes[方向][动画索引]。最后判断“if Time.time - lastAnmTime > 0.1 then”,每隔0.1秒切换一次动画。
--每帧执行
function Update()
--移动
local Input = UnityEngine.Input;
local horizontal = Input.GetAxis("Horizontal");
local verticla = Input.GetAxis("Vertical");
local x = role.transform.position.x + horizontal
local y = role.transform.position.y + verticla
role.transform.position = Vector3.New(x,y,0)
--转向
if horizontal < 0 then
roleImage.sprite = roleRes["LEFT"][roleAnm]
elseif horizontal > 0 then
roleImage.sprite = roleRes["RIGHT"][roleAnm]
elseif verticla > 0 then
roleImage.sprite = roleRes["UP"][roleAnm]
elseif verticla < 0 then
roleImage.sprite = roleRes["DOWN"][roleAnm]
end
--步伐(动画索引)
if Time.time - lastAnmTime > 0.1 then
roleAnm = roleAnm+1
if roleAnm > 3 then roleAnm = 1 end
lastAnmTime = Time.time
end
end
运行游戏,玩家可以通过键盘的方向键控制角色移动。
3、编写射击代码
在main.lua中添加炮弹相关的变量,如下所示。其中bullet代表炮弹元件(panel.map.bullet),lastShootTime 代表上一次发射炮弹的时间,bulletSpeedX代表炮弹的水平移动速度,bulletSpeedY代表炮弹的垂直移动速度,roleFace代表角色的面向。然后在OnLoadFinish中给bullet赋值。
local bullet
local lastShootTime = -100
local bulletSpeedX = 0
local bulletSpeedY = 0
local roleFace = 0
function OnLoadFinish(objs)
……
--子弹元素
bullet = map.transform:FindChild("bullet").gameObject
End
在Update中给roleFace赋值。
--每帧执行
function Update()
--移动
……
--转向
if horizontal < 0 then
roleImage.sprite = roleRes["LEFT"][roleAnm]
roleFace = 1
elseif horizontal > 0 then
roleImage.sprite = roleRes["RIGHT"][roleAnm]
roleFace = 2
elseif verticla > 0 then
roleImage.sprite = roleRes["UP"][roleAnm]
roleFace = 3
elseif verticla < 0 then
roleImage.sprite = roleRes["DOWN"][roleAnm]
roleFace = 4
end
--步伐
……
end
在Update中添加处理炮弹的代码,它处理如下几件事情。
1、炮弹在飞行1.2秒后,燃尽消失;
2、当玩家按下鼠标左键时,发射炮弹,根据角色面向,bulletSpeedX和bulletSpeedY会有不同的值。
3、根据bulletSpeedX和bulletSpeedY移动炮弹。
--每帧执行
function Update()
……
--子弹
if Time.time - lastShootTime > 1.2 then
--消失
if bullet.transform.position.x ~= -999 then
bullet.transform.position = Vector3.New(-999,-999,0)
end
--发射
if Input.GetMouseButton(0) then
bullet.transform.position = Vector3.New(x,y,0)
if roleFace == 1 then
bulletSpeedX = -10
bulletSpeedY = 0
elseif roleFace == 2 then
bulletSpeedX = 10
bulletSpeedY = 0
elseif roleFace == 3 then
bulletSpeedX = 0
bulletSpeedY = 10
elseif roleFace == 4 then
bulletSpeedX = 0
bulletSpeedY = -10
end
lastShootTime = Time.time
end
else
--运动
local x = bullet.transform.position.x + bulletSpeedX
local y = bullet.transform.position.y + bulletSpeedY
bullet.transform.position = Vector3.New(x,y,0)
end
end
运行游戏,点击鼠标左键,角色发射炮弹。另外也可以用基于组件的方法实现,这里就不展开了。