Unity手游開發(fā)札記——Lua語言集成

0. 前言

2016年8月從網(wǎng)易“畢業(yè)”河闰,在新的公司開始新的工作,這其中的波折與故事暫時不提伟叛,等以后有時間的時候再另開文章總結(jié)回顧私痹。全新的手游項目從零開始,使用Unity引擎開發(fā)一款手游項目统刮,本系列札記主要針對開發(fā)過程中重要的部分進(jìn)行記錄和總結(jié)紊遵,一方面方便自己日后回顧,也希望可以給遇到類似問題的朋友一些提醒和啟發(fā)侥蒙。當(dāng)然暗膜,自己在Unity引擎和Lua語言方面都是新手,更加希望拋磚引玉鞭衩,針對遇到的問題進(jìn)行更廣泛的討論和更多大牛的提點学搜。
具體到本篇文章的主題娃善,主要是Lua語言和Unity引擎的集成。這是我最近一個月左右的時間一直在嘗試解決的問題瑞佩,本文主要記錄方案的選擇和對比聚磺,以及針對選擇的方案自己進(jìn)行的一些改造和思考。

1. 為什么要集成Lua語言

有句從知乎開始發(fā)展起來的名言叫做——“先問是不是炬丸,再問為什么”瘫寝,類似地,在做一個技術(shù)方案的時候稠炬,“先問為什么焕阿,再考慮如何做”。那我們第一個問題就是要解決這個項目“為什么要集成Lua語言”首启?
在網(wǎng)易內(nèi)部暮屡,一向遵守的傳統(tǒng)是邏輯用腳本來做,比如Python毅桃、Lua等褒纲,好處主要有如下幾點:

  • 利用腳本語言的動態(tài)特性,客戶端可以做Hotfix疾嗅,服務(wù)端可以做Refresh外厂,無論在運營還是開發(fā)期這一特性都很有用;
  • 腳本語言運行在虛擬機中代承,它把游戲進(jìn)程搞掛的概率相比C/C++等靜態(tài)語言要低;
  • 腳本語言相對好學(xué)習(xí)一些渐扮,對于新手來說上手難度較低论悴,比如Python,當(dāng)然要精通也需要時間和經(jīng)驗的積累墓律;

當(dāng)然還有其他的優(yōu)點膀估,對應(yīng)的缺點就在于運行效率比C/C++低不少,相對于靜態(tài)語言在編譯器有完備的語法檢查耻讽,動態(tài)語言更容易出一些運行時的錯誤稻爬,調(diào)試難度相對大一些宁炫。
而對于Unity引擎,因為它已經(jīng)選擇了C#作為對應(yīng)的腳本語言,因此再集成一門Lua語言顯得有些多余束倍。核心的原因還是在IOS設(shè)備上因為使用了IL2CPP,無法實現(xiàn)像Android上面那樣直接替換DLL的方式來進(jìn)行更新台汇,這導(dǎo)致游戲邏輯如果出現(xiàn)錯誤砾淌,不但無法Hotfix修復(fù),甚至連Patch都不能修復(fù)具帮,只能重新提包博肋。雖然APP Store現(xiàn)在對于應(yīng)用的審核速度已經(jīng)變快低斋, 但是仍然需要2-3天以上的時間,這對于需要快速反應(yīng)的商業(yè)游戲來說是無法容忍的匪凡。
目前了解到的業(yè)內(nèi)常用的做法主要有如下幾種:

  • 純C#開發(fā)的方式膊畴,比如騰訊這種大廠,某些工作室的做法就是完全使用C#來進(jìn)行開發(fā)病游,盡量做到功能邏輯可配置唇跨,這樣出現(xiàn)某些重大問題可以通過更新數(shù)據(jù)的方式把邏輯暫時關(guān)閉掉。邏輯的更新安卓使用替換DLL的方式礁遵,IOS使用重新提包的方式轻绞。對外測試以安卓為主,并且大廠有比較好的QA團(tuán)隊進(jìn)行質(zhì)量保證佣耐,因此可以做到IOS最終上線的品質(zhì)和bug都是相對少的政勃。
  • C#做核心邏輯,Lua做UI和活動玩法等執(zhí)行頻率低兼砖,需求變動較大的部分奸远。這是目前了解到的一些創(chuàng)業(yè)團(tuán)隊使用得比較多的做法,在效率和可更新性之間的一個折中讽挟。
  • 以Lua為主的方式懒叛。也了解到一些公司的團(tuán)隊,包括網(wǎng)易內(nèi)部的一些項目耽梅,使用邏輯都以Lua語言來寫的方式進(jìn)行開發(fā)薛窥。從網(wǎng)易之前的經(jīng)驗來看,邏輯使用純腳本的方式并不會有太大問題眼姐。

我們要開發(fā)的產(chǎn)品是一款商業(yè)游戲诅迷,對于出現(xiàn)問題快速響應(yīng)的需求相對強烈,因此在Unity中使用Lua語言是必不可少的众旗,至于多大范圍地應(yīng)用它罢杉,初步是計劃大部分功能都是用Lua語言來開發(fā),并制定每隔一段時間周期進(jìn)行性能測試和評估的方式來確保性能可以滿足需求贡歧。

2. 怎樣集成Lua語言

在決定要使用Lua語言之后滩租,要面臨的問題就是如何在Unity中去集成它±洌可選的方案有很多律想,各種方案的實現(xiàn)原理也不盡相同,早期有各種在C#語言內(nèi)部實現(xiàn)Lua虛擬機的哗咆,也有利用反射動態(tài)查找腳本的蜘欲,但是目前比較主流的兩種方案是ToLua#SLua這兩種方案。

2.1 性能對比測試

這兩個方案的原理都相似晌柬,基于LUAInterface姥份,在開發(fā)時將C#的接口導(dǎo)出為Lua的版本郭脂,通過LuaState的棧結(jié)構(gòu)來進(jìn)行兩種語言之間方法調(diào)用。這兩個開源項目針對性能對比在網(wǎng)上打了不少口水仗澈歉,到底誰更優(yōu)秀很難公允地評價展鸡,因為作為一個中間件性質(zhì)的開源項目,除了性能之外還有生態(tài)圈埃难、易用性等各個方面的問題需要考量莹弊。網(wǎng)上有不少對比的帖子可以自己搜索一下,這里不進(jìn)行詳述了涡尘,以免引起論戰(zhàn)忍弛。
在這一部分我們最終選擇了ToLua#,原因我是自己在安卓設(shè)備上進(jìn)行測試了結(jié)果考抄。錢康來前段時間發(fā)了一個帖子來對比幾款Unity中Lua集成方案的性能细疚,Unity常見lua解決方案性能比較,這篇文章也整理投稿到了UWA博客中川梅,我自己基于測試用例在錘子T2上進(jìn)行了簡單的性能測試疯兼,結(jié)論和這篇博客中的基本一致,未整理的數(shù)據(jù)如下表所示贫途。

框架 test1 test2 test3 test4 test5 test6
SLua 755.004 623.619 34.126 6812.41 1648.68 0.6352
ToLua# 634 871.2 297.8 3056.2 1139.4 1.206

數(shù)據(jù)的單位是毫秒吧彪,測試是進(jìn)行五次測試的平均值,使用錘子T2進(jìn)行丢早。這次測試并不嚴(yán)謹(jǐn)姨裸,只是為了親自驗證一下兩者之間的性能差異到底是什么樣子的。每一個測試用例的代碼可以參考前文提到文章怨酝,這里只簡單進(jìn)行說明:

  • test1是簡單的屬性操作啦扬;
  • test2和test3是向量的操作;
  • test4是GameObject的創(chuàng)建凫碌;
  • test5是創(chuàng)建GameObject并進(jìn)行一些屬性操作;
  • test6是對四元數(shù)進(jìn)行操作胃榕。

2.2 性能差異的可能原因之一

個人感覺ToLua#在屬性操作方面性能較好盛险,而Vector的向量操作,因為可能會有Lua層的優(yōu)化勋又,即在Lua層完全實現(xiàn)了對應(yīng)的操作苦掘,因此需要針對源碼進(jìn)行詳細(xì)的對比。至于性能差異的原因楔壤,我沒有從Lua虛擬機的實現(xiàn)部分分析鹤啡,只是查看兩種生成Warp后的接口進(jìn)行一個簡單的猜想。
選取同一個接口進(jìn)行對比蹲嚣,UnityEngine.Animator的GetFloat接口递瑰,ToLua#的實現(xiàn)如下:

    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static int GetFloat(IntPtr L)
    {
        try
        {
            int count = LuaDLL.lua_gettop(L);

            if (count == 2 && TypeChecker.CheckTypes(L, 1, typeof(UnityEngine.Animator), typeof(int)))
            {
                UnityEngine.Animator obj = (UnityEngine.Animator)ToLua.ToObject(L, 1);
                int arg0 = (int)LuaDLL.lua_tonumber(L, 2);
                float o = obj.GetFloat(arg0);
                LuaDLL.lua_pushnumber(L, o);
                return 1;
            }
            //此處省略另一個重載接口
            else
            {
                return LuaDLL.luaL_throw(L, "invalid arguments to method: UnityEngine.Animator.GetFloat");
            }
        }
        catch(Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }

SLua生成的代碼如下:

    [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
    static public int GetFloat(IntPtr l) {
        try {
            int argc = LuaDLL.lua_gettop(l);
            if(matchType(l,argc,2,typeof(int))){
                UnityEngine.Animator self=(UnityEngine.Animator)checkSelf(l);
                System.Int32 a1;
                checkType(l,2,out a1);
                var ret=self.GetFloat(a1);
                pushValue(l,true);
                pushValue(l,ret);
                return 2;
            }
            //此處省略另一個重載接口
            pushValue(l,false);
            LuaDLL.lua_pushstring(l,"No matched override function to call");
            return 2;
        }
        catch(Exception e) {
            return error(l,e);
        }
    }

我們注意到祟牲,這一函數(shù)只需要一個返回值的,但是SLua往棧里pushValue了兩個值抖部,然后返回2说贝,第一個值是一個bool值,它應(yīng)該是用于標(biāo)識函數(shù)調(diào)用是否成功慎颗。在不了解其他地方是否有性能差別的情況下乡恕,這里應(yīng)該是ToLus#和SLua在簡單的接口調(diào)用上的性能差別的原因之一。SLua使用一個單獨的值來表示函數(shù)運行結(jié)果俯萎,這對于錯誤可以進(jìn)行更好的處理傲宜,但是多出的壓棧和出棧操作有額外的性能消耗。

2.3 導(dǎo)出方式對比

ToLua#導(dǎo)出使用的是白名單的方式夫啊,在CustomeSettings.cs文件中定義的接口才會導(dǎo)出函卒,也提供了導(dǎo)出引擎所有的接口的功能;而SLua是以黑名單的方式進(jìn)行涮母,默認(rèn)提供的功能是導(dǎo)出除了黑名單中的所有模塊接口谆趾,也提供了一個導(dǎo)出最簡接口的方式。
從使用角度來看叛本,SLua黑名單的方式在開發(fā)期比較方便沪蓬,默認(rèn)會導(dǎo)出所有接口,因此不需要每次想要增加一個已經(jīng)存在的類的Lua接口都要自己定義然后重新導(dǎo)出来候,發(fā)布的時候也可以使用最簡接口的方式導(dǎo)出跷叉。維護(hù)起來ToLua#因為所有的導(dǎo)出類都是我們自己定義的,因此更加清晰明確营搅。
鑒于這部分內(nèi)容有源碼可以進(jìn)行修改云挟,因此不是一個核心需要考慮的內(nèi)容,兩種方式各有利弊转质。

2.4 我們的選擇

至于這一點是否是性能差別的主要原因园欣,因為沒有時間和精力閱讀其他部分的源碼,暫時也不太好進(jìn)行對比和評價休蟹。出于性能的考慮沸枯,我們項目決定使用ToLua#作為Lua部分集成的方案,并且以接口的形式進(jìn)行封裝赂弓,來保證后面替換的可能性绑榴。

3. 如何使用Lua語言

在進(jìn)行了初步集成之后,怎樣讓開發(fā)人員可以更好地使用Lua語言是接下來要面臨的問題盈魁。ToLua#對應(yīng)有一套之前ulua作者開發(fā)的LuaFramework翔怎,這一個框架集成了腳本打包和二進(jìn)制腳本讀取、UI制作流程等多個功能,但是也如作者自己所說赤套,這一框架最初源自一個示例形式的Demo飘痛,因此其中代碼有很多部分是和示例寫死綁定的邏輯,比如啟動邏輯于毙、Lua二進(jìn)制腳本的加載需要手動指定等等敦冬。
相對應(yīng)的,SLua也有多套已經(jīng)開源的框架唯沮,其中最為完善的是KSFramwork脖旱,這套框架集成了資源打包、導(dǎo)表介蛉、Lua熱重載在內(nèi)的多個功能萌庆,而且代碼質(zhì)量初步看起來還不錯,因此最終我們決定把KSFramwork中的SLua部分替換成ToLua#的部分來結(jié)合使用币旧。
改造的過程還比較簡單践险,由于該部分使用Lua耦合的只有兩塊內(nèi)容,一是UIControler部分吹菱,二是LuaBehavior部分巍虫,所有的接口都由LuaModule模塊提供。因此改造的過程也就比較明確了:

  1. 刪除源代碼中的SLua部分鳍刷,接入ToLua#的部分占遥;
  2. 使用ToLua#重寫LuaModule的實現(xiàn);
  3. 改造LuaUIController输瓜,使用新的LuaModule接口實現(xiàn)之前的功能瓦胎;
  4. 改造LUABehavior模塊。

代碼刪除和LuaModule模塊的重新實現(xiàn)都比較簡單尤揣,著重介紹一下LuaUIController和LUABehavior模塊的改造搔啊。

3.1 改造初衷

之前的KSFramwork還是一個核心邏輯在C#,Lua只承載UI等邏輯的模塊北戏,這與我之前從網(wǎng)易“繼承”的“輕引擎负芋,重腳本”的思路并不契合。在這一思路下嗜愈,引擎可以看做渲染示罗、資源加載、音效等功能的提供者芝硬,腳本邏輯負(fù)責(zé)使用這些功能構(gòu)建游戲內(nèi)容。那這樣大部分與邏輯相關(guān)的控制權(quán)就應(yīng)該從引擎交給腳本部分來進(jìn)行轧房。Unity作為一個比較特殊的例子拌阴,雖然對于它來說,C#部分已經(jīng)是腳本了奶镶,但是對于希望著重使用Lua腳本的我們來說迟赃,因為C#不可更新陪拘,因此被視作了引擎部分。
最為簡單的設(shè)計就是當(dāng)引擎初始化完畢之后纤壁,通過一個接口調(diào)用把后續(xù)的邏輯都交由腳本來控制左刽,大部分與游戲玩法相關(guān)的模型加載、聲音播放酌媒、特效播放欠痴、動畫播放等由腳本來控制。tick邏輯為了減少調(diào)用次數(shù)秒咨,每幀也由引擎調(diào)用注冊的一個腳本接口進(jìn)行統(tǒng)一調(diào)用喇辽,腳本層自己做分發(fā)。

3.2 LuaUIController的改造

LuaUIController原始的方式是在C#層通過ui模塊的名稱加載對應(yīng)的一個lua文件雨席,獲取一個lua table進(jìn)行緩存菩咨,在比如OnInit等需要接口調(diào)用的地方查找這個table中對應(yīng)的函數(shù)進(jìn)行調(diào)用。這種方式的界面是由C#層的邏輯來驅(qū)動加載和顯示的陡厘,而且在加載過程中要有文件的搜索和檢查過程抽米。
這樣會存在一個問題,就是腳本層的邏輯無法或者很難去控制界面對象的生命周期糙置。針對資源的生命周期云茸,“誰創(chuàng)建誰管理”的策略不再可以很方便地來明確責(zé)任的劃分,因此要進(jìn)行改造罢低。
改造的方向很簡單查辩,將界面加載和顯示的接口開放到Lua層,然后在創(chuàng)建的時候由lua層傳遞一個table對象進(jìn)來网持,C#中進(jìn)行緩存宜岛,當(dāng)界面資源異步加載完畢,需要進(jìn)行接口調(diào)用的地方的實現(xiàn)與之前保存一致功舀。這樣萍倡,界面資源的生命周期全部交由腳本層來管理,在腳本構(gòu)建一個結(jié)構(gòu)合理功能齊全的UIManager來進(jìn)行一些功能的封裝辟汰,就可以滿足大部分的需求列敲。

3.3 LuaBehavior的改造

MonoBehavior是Unity為了放便開發(fā)而提供的一個很好的功能,腳本以組件的方式掛接在GameObject身上帖汞,就可以在Awake戴而、Start、Update等接口中處理想要的邏輯翩蘸。為了能夠繼續(xù)使用Unity的這一特性所意,在Lua層也實現(xiàn)了一個簡單的LuaBehavior封裝。
KSFramwork中的思路非常簡單,同樣根據(jù)名稱來把一個LuaBehavior和一個Lua腳本進(jìn)行綁定扶踊,在對應(yīng)的邏輯中調(diào)用與之對應(yīng)的接口就可以了泄鹏。比如Awake接口的實現(xiàn)如下:

        protected virtual void Awake()
        {
            if (!string.IsNullOrEmpty(LuaPath))
            {
                Init();
                CallLuaFunction("Awake");
            } // else Null Lua Path, pass Awake!
        }

CallLuaFunction的實現(xiàn)也很明確,從緩存的lua table中獲取名稱為Awake的function進(jìn)行調(diào)用秧耗。這種方式?jīng)]有問題备籽,但是當(dāng)場景中掛載了LuaBehavior的GameObject很多的時候,每一幀都會有非常多次的update方法調(diào)用分井,這個調(diào)用從C#層傳遞給Lua層车猬,有很多額外的性能消耗。
前文也提到了杂抽,比較好的方式是每幀只有一個C#到Lua層的Update方法調(diào)用诈唬,然后腳本層自己做分發(fā)。因此缩麸,針對這一需求铸磅,我們使用ToLua#自帶的LuaLooper來實現(xiàn)這一功能。
LuaLooper是全局只創(chuàng)建一個的MonoBehaviour杭朱,注意這里只創(chuàng)建一個是由邏輯來決定的阅仔,而不是一個單例模式。這里針對單例模式適用場合的討論不再展開弧械,此處由邏輯來保證只有一個Looper存在是一件比較合理的事情八酒,預(yù)留了一些擴(kuò)展的可能。
LuaLooper以事件的方式將三種Update分發(fā)出去:Update刃唐、LateUpdate羞迷、FixedUpdate,它在自己對應(yīng)的函數(shù)中調(diào)用luaState的對應(yīng)函數(shù)來將事件告知腳本画饥,腳本中需要的模塊向分發(fā)模塊注冊回調(diào)來監(jiān)聽事件衔瓮,就可以做到每幀只有一次Update調(diào)用了。
具體的代碼實現(xiàn)可以去看ToLua#中的LupLooper.cs的類實現(xiàn)抖甘。

注意 這里有一個需要小心的點是當(dāng)事件在腳本層分發(fā)的時候热鞍,要注意執(zhí)行時序問題的影響,最好能夠保證任意的執(zhí)行順序都可以不影響游戲邏輯的結(jié)果衔彻,否則可能會出現(xiàn)很難查的詭異bug薇宠。

對于Awake、Start等一次性調(diào)用的函數(shù)艰额,由于不是頻繁的邏輯澄港,因此保留了原始的實現(xiàn)方式,這樣可以讓Lua層對應(yīng)的代碼實現(xiàn)更加簡潔柄沮。而使用事件注冊的方式慢睡,讓不需要update邏輯的腳本沒有任何額外的性能消耗逐工。

4. 結(jié)語

只有上述的這些部分,對于開發(fā)一款商業(yè)游戲來說還遠(yuǎn)遠(yuǎn)不夠漂辐,但是通過導(dǎo)出的接口和對于KSFramwork的一些改進(jìn),已經(jīng)可以實現(xiàn)一個簡單的由Lua層來驅(qū)動的Demo了棕硫,它可以加載場景髓涯,打開一個打包成AssetBundle的界面,設(shè)置界面上的控件屬性哈扮,為按鈕添加一些回調(diào)時間纬纪,然后切換場景,加載一些打包在AssetBudnle中的Prefab模型滑肉。
這是Lua初步集成的結(jié)束包各,也是在這款游戲中創(chuàng)造萬物的開始。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末靶庙,一起剝皮案震驚了整個濱河市问畅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌六荒,老刑警劉巖护姆,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掏击,居然都是意外死亡卵皂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門砚亭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灯变,“玉大人,你說我怎么就攤上這事捅膘√砘觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵篓跛,是天一觀的道長膝捞。 經(jīng)常有香客問我,道長愧沟,這世上最難降的妖魔是什么蔬咬? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮沐寺,結(jié)果婚禮上林艘,老公的妹妹穿的比我還像新娘。我一直安慰自己混坞,他們只是感情好狐援,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布钢坦。 她就那樣靜靜地躺著,像睡著了一般啥酱。 火紅的嫁衣襯著肌膚如雪爹凹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天镶殷,我揣著相機與錄音禾酱,去河邊找鬼。 笑死绘趋,一個胖子當(dāng)著我的面吹牛颤陶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陷遮,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼滓走,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了帽馋?” 一聲冷哼從身側(cè)響起搅方,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茬斧,沒想到半個月后腰懂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡项秉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年绣溜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娄蔼。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡怖喻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岁诉,到底是詐尸還是另有隱情锚沸,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布涕癣,位于F島的核電站哗蜈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坠韩。R本人自食惡果不足惜距潘,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望只搁。 院中可真熱鬧音比,春花似錦、人聲如沸氢惋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至骚亿,卻和暖如春已亥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背来屠。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工陷猫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人的妖。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像足陨,于是被迫代替她去往敵國和親嫂粟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容