Unity3D熱更新技術點——ToLua(下)

作者:朔宇
在上一篇文章中我們通過一個小的案例,介紹了ToLua在Unity中的基本使用方法,而這次,我們將通過一個更為復雜的例子,繼續(xù)深入了解ToLua的使用方法及其原理。
上篇傳送門:Unity3D熱更新技術點——ToLua(上)
ToLua文件目錄
我們首先來了解一下ToLua的文件目錄。
Tolua集成主要分為兩部分,一部分是運行時需要的代碼包括一些手寫的和自動生成的綁定代碼,另一部分是編輯器相關代碼,主要提供代碼生成、編譯lua文件等操作,具體就是Unity編輯器中提供的功能。
接下來我們具體介紹一下Tolua文件列表中文件的用途:
1.Editor
Editor下Custom/CustomSettings.cs 自定義配置文件,用于定義哪些類作為靜態(tài)類型、哪些類需要導出、哪些附加委托需要導出等
我們需要注冊到Lua中的類型也都需要在這里導入,在Tolua中已經(jīng)為我們提供了Unity大部分基礎類型,若我們需要導入自己的類型或Tolua沒有導入的類型可以在其中添加,如下圖所示:?

2.Source
在Source文件夾中有Generate文件夾及LuaConst.cs腳本,Generate中主要是生成用于交互的綁定代碼wrap腳本,LuaConst.cs是一些lua路徑等配置文件。 若在CustomSettings中做了修改,需要在菜單欄的Lua選項中,重新生成的Wrap文件,當重新生成Wrap文件后,會發(fā)現(xiàn)我們新添加類型也生成了相應的Wrap文件,如下圖所示:


3.ToLua
Tolua文件夾中有如下文件
1)BaseType: 一些基礎類型的綁定代碼
2)Core: 提供的一些核心功能,包括封裝的「LuaFunction」「LuaTable」 「LuaThread」「LuaState」「LuaEvent」、調(diào)用tolua原生代碼等等。
3)Examples: Tolua示例
4)Misc: 雜項,包含LuaClient,LuaCoroutine(協(xié)程),LuaLooper(用于tick),LuaResLoader(用于加載lua文件)
5)Reflection: 反射相關
我們這里只了解一下Tolua中的文件結(jié)構(gòu)及相關文件的作用,具體的腳本綁定及生成流程我們不做過多贅述,如需要了解可以查詢相關資料,若有較多反饋,在之后我們可以開一篇新的文章,具體介紹tolua?
Tolua跳一跳
現(xiàn)在我們已經(jīng)大致了解了Tolua這個方案,接下來我們通過一個Demo,來看在Unity中,我們?nèi)绾问褂肨olua開發(fā)項目。
本文中以一個仿照微信跳一跳的小游戲作為案例來講解,案例非常簡單,但希望讀者有unity基礎,本文主要講解Tolua的用法,代碼邏輯方面的講解可能會相對偏少?

一.開發(fā)前準備
在之前,我們已經(jīng)導入了Tolua資源。在這個項目中,我們需要使用到DoTween插件,可以在Asset Store自行下載。

二.Lua虛擬機管理器
我們需要用C#腳本來開啟Lua虛擬機并調(diào)用Lua模塊,那么不同的邏輯就會有不同的C#腳本來開啟虛擬機并調(diào)用Lua模塊,這無疑是很耗費性能且繁瑣的,所以我們可以自己做一些封裝,先將C#腳本中所必須的方法做一個緩存,如下代碼所示: LuaManager.cs?
public class LuaManager : MonoBehaviour {
??? private static LuaManager _instance;
??? public static LuaManager Instance{
??????? get{
??????????? return _instance;
????? ??}
??? }
??? private LuaClient _luaClient;
??? public LuaClient LuaClient
??? {
??????? get
??????? {
??????????? return _luaClient;
??????? }
??? }
??? void Awake () {
??????? _instance = this;
??????? //跨場景不銷毀
??????? DontDestroyOnLoad(this.gameObject);
??????? _luaClient = this.gameObject.AddComponent<LuaClient>();
??? }
}
?
在代碼中,我們直接使用LuaClient,LuaClient我們可以理解成是ToLua內(nèi)部對自己的一種封裝,可以視為tolua環(huán)境的一個啟動。我們需要將LuaClient中的protected LuaState luaState = null;
改為public,同時我們可以在LuaClient中再封裝一個調(diào)用Lua模塊函數(shù)的方法。
public virtual void CallFunc(string func, GameObject obj)
??? {
??????? LuaFunction luaFunc = luaState.GetFunction(func);
??????? luaFunc.Call(obj);
??????? luaFunc.Dispose();
?????? ?luaFunc = null;
??? }
?
然后我們在場景中創(chuàng)建一個空物體,添加LuaManager.cs腳本

三.自建C#方法工具類
在使用Tolua開發(fā)中,ToLua提供的方法有限,有時我們可能找不到很好的方法來代替C#中的功能,或者不清楚某個功能的使用方法,這時候我們可以在C#中封裝好一些功能,然后導入到Lua中,便可以直接在Lua中使用。這里可以創(chuàng)建一個C#腳本,我們命名為Util.cs,因為Lua中數(shù)值只存在number類型,如果需要我們可以封裝int和float類型
public static int Int(object o)
??? {
??????? return Convert.ToInt32(o);
??? }
public static float Float(object o)
??? {
??????? return (float)Math.Round(Convert.ToSingle(o), 2);
??? }
??
同時還有我們可能需要使用到的Dotween的部分方法
? ? public static void DoMove(GameObject obj, Vector3 vec, float time)
??? {
??????? obj.transform.DOMove(vec, time);
??? }
?
??? public static void DoScale(GameObject obj, float f, float time)
??? {
??????? obj.transform.DOScale(f, time);
??? }
?
??? public static void DoScale(GameObject obj, Vector3 vec, float time)
??? {
??????? obj.transform.DOScale(vec, time);
??? }
?
之前我們介紹過,如果需要添加導入lua的類型,需要在CustomSettings.cs中添加

可以看到我們在其中加入了 _GT(typeof(Util)),然后重新生成wrap文件?

四.開始界面
再之前的動圖中可以看到我們的項目中的開始界面,只搭建了背景及一個開始按鈕,讀者也可以自行擴展。 * 這里我們需要用到Button事件,我們同樣可以通過封裝一個C#腳本給lua提供一個按鈕事件
BtnEvent.cs?
?
public class UIEvent : MonoBehaviour {
??? public static void AddButtonOnClick(GameObject game, LuaFunction function)
??? {
??????? if (game == null)
??????????? return;
??????? Button btn = game.GetComponent<Button>();
??????? btn.onClick.AddListener(
??????????? delegate () {
??????????????? function.Call(game);
??????????? }
??????? );
??? }
}
?
接下來就是在lua中的調(diào)用了。 我們可以在Project面板中找到lua文件夾,我們可以把我們的Lua腳本文件放在這個文件夾下(當然,也可以根據(jù)自己的習慣修改Lua文件夾,但我們查找Lua文件的路徑就需要修改),文件夾下有名為Main.lua的Lua文件,在這個腳本中,我們可以定義我們所需的全局類型:
--主入口函數(shù)。從這里開始lua邏輯
--這里定義我們所需的全局類型
function Main()
??? GameObject = UnityEngine.GameObject
??? Transform = UnityEngine.Transform
??? ParticleSystem = UnityEngine.ParticleSystem
??? Color = UnityEngine.Color
??? Util = Util.New()
??? SceneManagement = UnityEngine.SceneManagement
??? Input = UnityEngine.Input
??? KeyCode = UnityEngine.KeyCode
??? Time = UnityEngine.Time
??? Camera = UnityEngine.Camera
??? AudioSource = UnityEngine.AudioSource
??? Resources = UnityEngine.Resources
??? www = UnityEngine.WWW
??? print("logic start")
end
?
--場景切換通知
function OnLevelWasLoaded(level)
??? collectgarbage("collect")
??? Time.timeSinceLevelLoad = 0
end
?
function OnApplicationQuit()
end
?
然后我們創(chuàng)建Login.lua,我們的開始界面邏輯將會寫在這個腳本中:
Login = {}--定義Login類
local this = Login
require('Music')--加載Music模塊
local ui
local manager
function this.Awake(object)
??? manager = GameObject.Find('Manager')
??? manager : AddComponent(typeof(AudioSource))
??? coroutine.start(Music.PlaySound)--開啟協(xié)程
??? ui = object
??? local loginBtn = ui.transform : Find("Login").gameObject
??? UIEvent.AddButtonOnClick(loginBtn, LoginOnClick)
end
?
function LoginOnClick()
????? --場景切換
??? SceneManagement.SceneManager.LoadScene("Jump")
?
end
在這個lua腳本中,可以看出,其實Lua中調(diào)用unity的方法和C#十分相似,想必有Unity基礎的讀者很容易看明白以上的代碼。?
其中要注意的是,unity中的物體無法綁定lua腳本,所以無法通過如c#中定義public的值可以在Inspector面板進行賦值,所以代碼中我們必須通過GameObject.Find()或者Transform:Find()來找到物體?
還有一個重要的地方,需要注意我們調(diào)用類的方法、屬性、字段時「.」和「:」的區(qū)別
我們希望在游戲中有背景音樂,所以在這里,我們使用協(xié)程來下載一首音樂,并掛之前創(chuàng)建的在Manager上
--協(xié)程下載
--這里使用Tolua中提供的coroutine.www
Music = {}
local this = Music
function this.PlaySound()
??? local audio = GameObject.Find('Manager') : GetComponent('AudioSource')
??? local url = www('https://etnly.oss-cn-shanghai.aliyuncs.com/%E5%B2%A1%E9%83%A8%E5%95%93%E4%B8%80%20-%20%E9%81%BA%E3%82%B5%E3%83%AC%E3%82%BF%E5%A0%B4%E6%89%80%EF%BC%8F%E6%96%9C%E5%85%89.ogg')
??? coroutine.www(url)
??? audio.clip = url : GetAudioClip()
??? audio : Play()
end
?
接下來我們來看如何在C#中調(diào)用剛才寫好的lua模塊:
public class Login : MonoBehaviour {
??? void Start () {
??????? LuaManager.Instance.LuaClient.luaState.DoFile("Login.lua");
??????? LuaManager.Instance.LuaClient.CallFunc("Login.Awake", this.gameObject);
??? }
?
}
?
四.角色邏輯
開始游戲界面完成后,我們就著手于游戲主場景,首先我們可以新建場景,使用plane、cube等基礎物體,搭建一個簡單的跳一跳場景。

接下來,我們用Lua實現(xiàn)角色的跳躍,這里我們可以使用剛體來讓角色實現(xiàn)向前跳躍的動作:
local player
local rigidbody
function this.Awake(obj)
??? player = obj
??? rigidbody = player : GetComponent('Rigidbody')
end
function this.StartJump(time)
??? --跳躍邏輯,這里的time可以理解為我們按下按鈕的時間
??? rigidbody : AddForce(Vector3(1, 1, 0) * time * 7,
??? UnityEngine.ForceMode.Impulse)
end
?
同樣的,首先我們要找到角色,然后獲取其中的剛體組建。?
在跳一跳游戲中,我們通過某一個按鍵(這里我們使用空格鍵)讓角色開始跳躍,角色跳躍的力度是更具按下屏幕或者按鈕的時間來決定的,所以這里我們需要獲取到我們按下按鈕的時間
function this.Update()
??? if Input.GetKeyDown(KeyCode.Space) then
??????? startTime = Time.time--獲取按下空格時的時間
??? end
??? if Input.GetKeyUp(KeyCode.Space) then
??????? endTime = Time.time - startTime--計算按下空格至松開的時間
??????? this.StartJump(endTime)
??? end
end
?
然后實現(xiàn)角色在蓄力(也就是按住按鈕)時的動作,以及粒子效果,如下圖所示

我們的角色是由一個圓柱體和一個球體組成,所以在蓄力時,我們需要壓縮圓柱體同時球體的位置也要下移:
if Input.GetKey(KeyCode.Space) then
??? --角色壓縮效果
??? if body.transform.localScale.y < 0.11 and body.transform.localScale.y > 0.05 then
??????? body.transform.localScale = body.transform.localScale + Vector3(1, -1, 1) * 0.05 * Time.deltaTime
??????? head.transform.localPosition = head.transform.localPosition + Vector3(0, -1, 0) * 0.05 * Time.deltaTime
??? end
end
?
在這里,body時角色的身體,head時頭部,我們都需要先找到物體然后在對其進行操作?
蓄力時的粒子效果,讀者可以自行在unity中編輯,這里就不做過多贅述,相關代碼如下:
particle : GetComponent(‘ParticleSystem’) : Play()
我們應該在按下空格時就開啟粒子,所以這里應該放在判斷按下空格中?
當我們松開空格時,角色需要恢復之前的大小及位置,粒子效果也需要停止,同時角色跳躍。
--DoTween恢復角色
Util.DoScale(body, 0.1, 0.5)
Util.DoLocalMoveY(head, 0.8, 0.5)
particle : GetComponent('ParticleSystem') : Stop()
?
這里我們用DoTween來恢復角色大小。?
關于角色的邏輯,最后一步就是判定角色是否跳到了下一個盒子
--如果跳到此盒子,便給該盒子添加腳本,并移動攝像機
function this.OnCollisionStay(object)
??? if object.transform.tag == 'Cube' then
??????? if(object : GetComponent('BoxControl') == nil) then
??????????? this.CameraMove()
??????????? object : AddComponent(typeof(BoxControl))
??????? end
??? elseif object.transform.tag == 'Plane' then
??????? --重新開始游戲
??????? Time.timeScale = 0
??????? ui : SetActive(true)
??????? Continue.ReStart(ui)
??? end
?
end
?
function this.CameraMove()
??? --DoTween控制攝像機移動效果
??? Util.DoMove(Camera.main, (player.transform.position + cameraRelativePosition), 1)
end
?
在這里我們角色如果跳到了下一個盒子,就會給當前盒子添加一個腳本,并移動攝像機,如果沒有跳到則打開一個ui界面,并調(diào)用一個叫Continue的模塊?

Continue.lua?
Continue = {}
local this = Continue
function this.ReStart(obj)
??? local reStartBtn = obj.transform : Find("ReStart").gameObject
??? UIEvent.AddButtonOnClick(reStartBtn, ReStartOnClick)
end
?
function ReStartOnClick ()
??? SceneManagement.SceneManager.LoadScene("Jump")
end
?
最后就是角色控制的C#代碼,和之前開始界面類似,不過這里多了Update和OnCollisionStay
void Start () {????
??????? LuaManager.Instance.LuaClient.luaState.DoFile("Player.lua");
??????? LuaManager.Instance.LuaClient.CallFunc("Play.Awake", this.gameObject);
}
void Update () {
??????? LuaManager.Instance.LuaClient.CallFunc("Play.Update", gameObject);
}
private void OnCollisionStay(Collision collision)?? {???????
??????? LuaManager.Instance.LuaClient.CallFunc("Play.OnCollisionStay", collision.gameObject);
}
?
上面提到了會給跳到的盒子加入一個腳本,那么接下來我們就來看關于盒子的邏輯?
五.盒子邏輯
首先,當角色跳到當前盒子上,我們就應該把這個腳本綁定到該盒子,再之前角色的邏輯中已經(jīng)有說明。我們可以在圖中可以看到,當按下空格,角色壓縮是盒子也會同時被壓縮,松開空格后盒子復原

if Input.GetKey(KeyCode.Space) then
??? --盒子壓縮效果
??? if currentBox.transform.localScale.y < 0.51 and currentBox.transform.localScale.y > 0.3 then
??????? currentBox.transform.localScale = currentBox.transform.localScale + Vector3(0, -1, 0) * 0.15 * Time.deltaTime
??????? currentBox.transform.localPosition = currentBox.transform.localPosition + Vector3(0, -1, 0) * 0.15 * Time.deltaTime
??? end
end
if Input.GetKeyUp(KeyCode.Space) then
??? --DoTween恢復盒子
??? Util.DoLocalMoveY(currentBox, 0.25, 0.2)
??? Util.DoScale(currentBox, Vector3(oldScale.x, oldScale.y, oldScale.z), 0.2)
end
?
然后當角色跳到新的盒子上,就應該根據(jù)當前位置,生成一個新的大小和顏色隨機的盒子,具體代碼邏輯如下
function this.GenerateBox()
??? boxPrefab = Resources.Load('Prefabs/Cube')
??? local newBox = GameObject.Instantiate(boxPrefab)
??? --盒子隨機位置、大小、顏色
??? randomScale = Util.Random(0.5, 1)
??? newBox.transform.position = currentBox.transform.position + Vector3(Util.Random(1.5, maxDistance), 0, 0)
??? plane.transform.localPosition = plane.transform.localPosition + Vector3(Util.Random(1.5, maxDistance), 0, 0)
??? newBox.transform.localScale = Vector3(randomScale, 0.5, randomScale)
??? newBox : GetComponent('Renderer').material.color = Color(Util.Random(0.0, 1.0), Util.Random(0.0, 1.0), Util.Random(0.0, 1.0))
end
?
最后一步,就是刪除之前的盒子,我們可以判斷盒子是否在攝像機范圍內(nèi)(在本項目中,我們攝像機范圍內(nèi)應該只有兩個盒子,當前和新生成的盒子),不在攝像機范圍內(nèi)的盒子,我們可以將其刪除
function this.Update(obj)
??? if this.IsInView(obj.transform.position) then
??????? GameObject.Destroy(obj, 1)
??? end
end
--判斷盒子是否在攝像機范圍內(nèi),如果不在,便將其銷毀
function this.IsInView(worldPos)
??? local cameraTrans = Camera.main.transform
??? local viewPos = Camera.main : WorldToViewportPoint(worldPos)
??? local dir = (worldPos - cameraTrans.position).normalized
??? local dot = Vector3.Dot(cameraTrans.forward, dir)
??? if dot > 0 and viewPos.x > 0 and viewPos.x < 1 and viewPos.y > 0 and viewPos.y < 1 then
??????? return false
??? end
??? return true
end
?
BoxControl.cs?
void Start () {??????
??????? LuaManager.Instance.LuaClient.luaState.DoFile("BoxControl.lua");
??????? LuaManager.Instance.LuaClient.CallFunc("Box.Awake", this.gameObject);
?
??? }
void Update () {
??????? LuaManager.Instance.LuaClient.CallFunc("Box.Update", this.gameObject);
??? }
?
到此為止,我們使用Tolua制作的跳一跳小游戲就完成了。項目雖小,但包含了Tolua的大部分功能的常見使用方法。
我們的項目工程會放在github中,大家可自行下載測試及擴展,也可以根據(jù)本篇文章制作其他項目來熟悉Tolua的使用。
完整工程在此 :https://github.com/Etnly/ToLuaDemo
想系統(tǒng)學習游戲開發(fā)的童鞋,歡迎訪問?http://levelpp.com/? ? ? ????
游戲開發(fā)攪基QQ群:869551769? ? ? ? ????
微信公眾號:皮皮關