找回密碼
 注冊帳號

掃一掃,訪問微社區

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

9
回復
1857
查看
打印 上一主題 下一主題
[ 復制鏈接 ]
排名
1
昨日變化

8029

主題

8587

帖子

3萬

積分

Rank: 16

UID
1231
好友
186
蠻牛幣
12029
威望
30
注冊時間
2013-7-29
在線時間
4101 小時
最后登錄
2019-8-9

活力之星原創精華達人突出貢獻獎財富之證游戲蠻牛QQ群會員蠻牛妹VIP

馬上注冊,結交更多好友,享用更多功能,讓你輕松玩轉社區。

您需要 登錄 才可以下載或查看,沒有帳號?注冊帳號

x
上一篇文章中我們通過一個小的案例,介紹了ToLua在Unity中的基本使用方法,而這次,我們將通過一個更為復雜的例子,繼續深入了解ToLua的使用方法及其原理。


ToLua文件目錄
我們首先來了解一下ToLua的文件目錄。
Tolua集成主要分為兩部分,一部分是運行時需要的代碼包括一些手寫的和自動生成的綁定代碼,另一部分是編輯器相關代碼,主要提供代碼生成、編譯lua文件等操作,具體就是Unity編輯器中提供的功能。


接下來我們具體介紹一下Tolua文件列表中文件的用途:


1.Editor
Editor下Custom/CustomSettings.cs 自定義配置文件,用于定義哪些類作為靜態類型、哪些類需要導出、哪些附加委托需要導出等

我們需要注冊到Lua中的類型也都需要在這里導入,在Tolua中已經為我們提供了Unity大部分基礎類型,若我們需要導入自己的類型或Tolua沒有導入的類型可以在其中添加,如下圖所示:



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



[size=0.9em]Clear wrap files后會自動重新生成wrap文件


3.ToLua
Tolua文件夾中有如下文件

1)BaseType: 一些基礎類型的綁定代碼
2)Core: 提供的一些核心功能,包括封裝的「LuaFunction」「LuaTable」 「LuaThread」「LuaState」「LuaEvent」、調用tolua原生代碼等等。
3)Examples: Tolua示例
4)Misc: 雜項,包含LuaClient,LuaCoroutine(協程),LuaLooper(用于tick),LuaResLoader(用于加載lua文件)
5)Reflection: 反射相關

我們這里只了解一下Tolua中的文件結構及相關文件的作用,具體的腳本綁定及生成流程我們不做過多贅述,如需要了解可以查詢相關資料,若有較多反饋,在之后我們可以開一篇新的文章,具體介紹tolua


Tolua跳一跳
現在我們已經大致了解了Tolua這個方案,接下來我們通過一個Demo,來看在Unity中,我們如何使用Tolua開發項目。
本文中以一個仿照微信跳一跳的小游戲作為案例來講解,案例非常簡單,但希望讀者有unity基礎,本文主要講解Tolua的用法,代碼邏輯方面的講解可能會相對偏少



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


二.Lua虛擬機管理器
  • 我們需要用C#腳本來開啟Lua虛擬機并調用Lua模塊,那么不同的邏輯就會有不同的C#腳本來開啟虛擬機并調用Lua模塊,這無疑是很耗費性能且繁瑣的,所以我們可以自己做一些封裝,先將C#腳本中所必須的方法做一個緩存,如下代碼所示: LuaManager.cs



[AppleScript] 純文本查看 復制代碼
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內部對自己的一種封裝,可以視為tolua環境的一個啟動。我們需要將LuaClient中的protected LuaState luaState = null;改為public,同時我們可以在LuaClient中再封裝一個調用Lua模塊函數的方法。

[AppleScript] 純文本查看 復制代碼
public virtual void CallFunc(string func, GameObject obj)
    {
        LuaFunction luaFunc = luaState.GetFunction(func);
        luaFunc.Call(obj);
        luaFunc.Dispose();
        luaFunc = null;
    }


然后我們在場景中創建一個空物體,添加LuaManager.cs腳本




三.自建C#方法工具類
  • 在使用Tolua開發中,ToLua提供的方法有限,有時我們可能找不到很好的方法來代替C#中的功能,或者不清楚某個功能的使用方法,這時候我們可以在C#中封裝好一些功能,然后導入到Lua中,便可以直接在Lua中使用。這里可以創建一個C#腳本,我們命名為Util.cs,因為Lua中數值只存在number類型,如果需要我們可以封裝int和float類型



[AppleScript] 純文本查看 復制代碼
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的部分方法

   
[AppleScript] 純文本查看 復制代碼
 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文件



如圖所示,Source/Generate中生成了UtilWrap文件


四.開始界面
再之前的動圖中可以看到我們的項目中的開始界面,只搭建了背景及一個開始按鈕,讀者也可以自行擴展。 * 這里我們需要用到Button事件,我們同樣可以通過封裝一個C#腳本給lua提供一個按鈕事件


BtnEvent.cs
[AppleScript] 純文本查看 復制代碼
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中的調用了。 我們可以在Project面板中找到lua文件夾,我們可以把我們的Lua腳本文件放在這個文件夾下(當然,也可以根據自己的習慣修改Lua文件夾,但我們查找Lua文件的路徑就需要修改),文件夾下有名為Main.lua的Lua文件,在這個腳本中,我們可以定義我們所需的全局類型:


[AppleScript] 純文本查看 復制代碼
--主入口函數。從這里開始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


  • 然后我們創建Login.lua,我們的開始界面邏輯將會寫在這個腳本中:

[AppleScript] 純文本查看 復制代碼
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)--開啟協程
    ui = object
    local loginBtn = ui.transform : Find("Login").gameObject
    UIEvent.AddButtonOnClick(loginBtn, LoginOnClick)
end

function LoginOnClick()
      --場景切換
    SceneManagement.SceneManager.LoadScene("Jump")

end



在這個lua腳本中,可以看出,其實Lua中調用unity的方法和C#十分相似,想必有Unity基礎的讀者很容易看明白以上的代碼。
其中要注意的是,unity中的物體無法綁定lua腳本,所以無法通過如c#中定義public的值可以在Inspector面板進行賦值,所以代碼中我們必須通過GameObject.Find()或者Transform:Find()來找到物體

還有一個重要的地方,需要注意我們調用類的方法、屬性、字段時「.」和「:」的區別

  • 我們希望在游戲中有背景音樂,所以在這里,我們使用協程來下載一首音樂,并掛之前創建的在Manager上

[AppleScript] 純文本查看 復制代碼
--協程下載
--這里使用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#中調用剛才寫好的lua模塊:

[AppleScript] 純文本查看 復制代碼
public class Login : MonoBehaviour {
    void Start () {
        LuaManager.Instance.LuaClient.luaState.DoFile("Login.lua");
        LuaManager.Instance.LuaClient.CallFunc("Login.Awake", this.gameObject);
    }

}

四.角色邏輯
開始游戲界面完成后,我們就著手于游戲主場景,首先我們可以新建場景,使用plane、cube等基礎物體,搭建一個簡單的跳一跳場景。



  • 接下來,我們用Lua實現角色的跳躍,這里我們可以使用剛體來讓角色實現向前跳躍的動作:

[AppleScript] 純文本查看 復制代碼
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 


同樣的,首先我們要找到角色,然后獲取其中的剛體組建。

  • 在跳一跳游戲中,我們通過某一個按鍵(這里我們使用空格鍵)讓角色開始跳躍,角色跳躍的力度是更具按下屏幕或者按鈕的時間來決定的,所以這里我們需要獲取到我們按下按鈕的時間

[AppleScript] 純文本查看 復制代碼
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


然后實現角色在蓄力(也就是按住按鈕)時的動作,以及粒子效果,如下圖所示


  • 我們的角色是由一個圓柱體和一個球體組成,所以在蓄力時,我們需要壓縮圓柱體同時球體的位置也要下移:

[AppleScript] 純文本查看 復制代碼
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()
我們應該在按下空格時就開啟粒子,所以這里應該放在判斷按下空格中

  • 當我們松開空格時,角色需要恢復之前的大小及位置,粒子效果也需要停止,同時角色跳躍。

[AppleScript] 純文本查看 復制代碼
--DoTween恢復角色
Util.DoScale(body, 0.1, 0.5)
Util.DoLocalMoveY(head, 0.8, 0.5)
particle : GetComponent('ParticleSystem') : Stop()


這里我們用DoTween來恢復角色大小。

  • 關于角色的邏輯,最后一步就是判定角色是否跳到了下一個盒子

[AppleScript] 純文本查看 復制代碼
--如果跳到此盒子,便給該盒子添加腳本,并移動攝像機
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界面,并調用一個叫Continue的模塊


Continue.lua
[AppleScript] 純文本查看 復制代碼
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

[AppleScript] 純文本查看 復制代碼
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);
}

上面提到了會給跳到的盒子加入一個腳本,那么接下來我們就來看關于盒子的邏輯
五.盒子邏輯
  • 首先,當角色跳到當前盒子上,我們就應該把這個腳本綁定到該盒子,再之前角色的邏輯中已經有說明。我們可以在圖中可以看到,當按下空格,角色壓縮是盒子也會同時被壓縮,松開空格后盒子復原






[AppleScript] 純文本查看 復制代碼
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


  • 然后當角色跳到新的盒子上,就應該根據當前位置,生成一個新的大小和顏色隨機的盒子,具體代碼邏輯如下

[AppleScript] 純文本查看 復制代碼
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



  • 最后一步,就是刪除之前的盒子,我們可以判斷盒子是否在攝像機范圍內(在本項目中,我們攝像機范圍內應該只有兩個盒子,當前和新生成的盒子),不在攝像機范圍內的盒子,我們可以將其刪除

[AppleScript] 純文本查看 復制代碼
function this.Update(obj)
    if this.IsInView(obj.transform.position) then
        GameObject.Destroy(obj, 1)
    end
end
--判斷盒子是否在攝像機范圍內,如果不在,便將其銷毀
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
[AppleScript] 純文本查看 復制代碼
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的大部分功能的常見使用方法。
感謝作者知乎@朔宇


回復

使用道具 舉報

7日久生情
2068/5000
排名
4092
昨日變化

0

主題

1350

帖子

2068

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
254705
好友
1
蠻牛幣
1883
威望
0
注冊時間
2017-11-16
在線時間
356 小時
最后登錄
2019-8-12
沙發
2019-5-8 14:48:22 只看該作者
6666666666666666666666666666666666666
回復 支持 反對

使用道具 舉報

7日久生情
4151/5000
排名
278
昨日變化

1

主題

146

帖子

4151

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
3603
好友
0
蠻牛幣
9920
威望
0
注冊時間
2013-9-10
在線時間
2062 小時
最后登錄
2019-8-12
板凳
2019-5-8 20:36:09 只看該作者
好,點贊一個
回復

使用道具 舉報

6蠻牛粉絲
1110/1500
排名
2236
昨日變化

1

主題

173

帖子

1110

積分

Rank: 6Rank: 6Rank: 6

UID
232255
好友
1
蠻牛幣
1631
威望
0
注冊時間
2017-7-15
在線時間
308 小時
最后登錄
2019-8-9
地板
2019-5-9 08:38:00 只看該作者
好,點贊一個
回復

使用道具 舉報

3偶爾光臨
183/300
排名
16955
昨日變化

1

主題

40

帖子

183

積分

Rank: 3Rank: 3Rank: 3

UID
173093
好友
0
蠻牛幣
106
威望
0
注冊時間
2016-9-30
在線時間
102 小時
最后登錄
2019-5-24
5#
2019-5-16 10:49:03 只看該作者
感謝分享
回復

使用道具 舉報

排名
317
昨日變化

15

主題

511

帖子

3274

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
8069
好友
31
蠻牛幣
7844
威望
0
注冊時間
2013-11-14
在線時間
940 小時
最后登錄
2019-8-9
QQ
6#
2019-5-17 12:34:28 只看該作者
期待
回復

使用道具 舉報

排名
317
昨日變化

15

主題

511

帖子

3274

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
8069
好友
31
蠻牛幣
7844
威望
0
注冊時間
2013-11-14
在線時間
940 小時
最后登錄
2019-8-9
QQ
7#
2019-5-17 12:34:30 只看該作者
期待
回復

使用道具 舉報

排名
34887
昨日變化

0

主題

17

帖子

42

積分

Rank: 1

UID
290237
好友
0
蠻牛幣
20
威望
0
注冊時間
2018-7-18
在線時間
17 小時
最后登錄
2019-5-28
8#
2019-5-22 14:44:24 只看該作者
感謝分享
回復

使用道具 舉報

5熟悉之中
635/1000
排名
5545
昨日變化

2

主題

54

帖子

635

積分

Rank: 5Rank: 5

UID
145209
好友
0
蠻牛幣
845
威望
0
注冊時間
2016-4-11
在線時間
323 小時
最后登錄
2019-7-4
9#
2019-5-29 09:49:19 只看該作者
tolua 可以斷點嗎?
回復 支持 反對

使用道具 舉報

0

主題

43

帖子

62

積分

Rank: 2Rank: 2

UID
327286
好友
0
蠻牛幣
30
威望
0
注冊時間
2019-7-16
在線時間
19 小時
最后登錄
2019-8-9
10#
2019-7-22 16:35:48 只看該作者
感謝樓主分享
回復

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 注冊帳號

本版積分規則

女校游泳队彩金