Game as a Service —— 開源云游戲搭載WebRTC

image

軟件即服務(wù),基礎(chǔ)架構(gòu)即服務(wù)丁频,平臺即服務(wù)杉允,通信平臺即服務(wù),視頻會議即服務(wù)席里,那么叔磷,游戲即服務(wù)(Game as a Service)如何呢?已經(jīng)有不少科技公司試水云游戲奖磁,最著名的要數(shù)Google的Stadia改基。對WebRTC來說,Stadia已經(jīng)算是老朋友了咖为,但是其他云游戲也能以同樣的方式運用WebRTC嗎秕狰?

Thanh Nguyen研究了他自己的開源項目CloudRetro在這方面是否可行。CloudRetro基于很受歡迎的WebRTC的pion庫躁染。在這篇文章中鸣哀,Thanh對他如何構(gòu)建項目進行了框架性的回顧與思考,以及他在此過程中遇到的一些挑戰(zhàn)吞彤。

image

loudretro.io中megaman x4的屏幕截圖

簡介 去年我衬,谷歌發(fā)布了Stadia,這個想法的獨特性和創(chuàng)新性顛覆了我的認(rèn)知饰恕。我一直在質(zhì)疑當(dāng)前的技術(shù)狀態(tài)怎么可能支持Stadia挠羔。為了揭開Stadia的神秘面紗,我創(chuàng)建了Cloud Gaming的開源版本埋嵌。我想在接下來的文章中分享有關(guān)自己開發(fā)Cloud Gaming的一年時間的大冒險破加。

為什么云游戲才是未來 我相信云游戲不僅將很快成為新一代游戲,而且還將成為新一代的計算機科學(xué)甚至其他領(lǐng)域雹嗦。云游戲是客戶端/服務(wù)器模型的頂峰范舀。通過將游戲邏輯放在遠(yuǎn)程服務(wù)器上并將圖像/音頻流傳輸?shù)娇蛻舳耍梢宰畲蠡蠖丝刂撇⒆钚』岸斯ぷ骼S纱耍?wù)器將負(fù)責(zé)處理繁重的任務(wù)端仰,而客戶端將不再受硬件限制捶惜。就Google Stadia而言,它實際上使用戶可以在YouTube等界面上玩3A游戲荔烧≈ㄆ撸可以將相同的方法應(yīng)用于其他繁重的脫機應(yīng)用程序汽久,例如操作系統(tǒng)或2D / 3D圖形設(shè)計等,以便我們可以跨平臺在低規(guī)格設(shè)備上一致地運行它們踊餐。
image

可能的未來:你能想象在Chrome瀏覽器上運行Microsoft Windows 10嗎景醇? 云游戲仍然面臨技術(shù)挑戰(zhàn) 游戲是少數(shù)需要用戶持續(xù)且快速反應(yīng)的應(yīng)用之一。如果我們單擊頁面時出現(xiàn)2秒鐘的延遲吝岭,這是可以接受的三痰。直播視頻流通常會延遲很多秒,但仍然具有可用性窜管。但是散劫,如果游戲頻繁延遲500毫秒,該游戲?qū)o法播放幕帆。
當(dāng)前的目標(biāo)是實現(xiàn)極低的延遲获搏,以確保游戲輸入與媒體之間的gap盡可能小。因此失乾,傳統(tǒng)的視頻流傳輸方法不適用于將圖像/音頻流傳輸?shù)娇蛻舳说那闆r常熙。

image

云游戲運作原理 開源項目CloudRetro 我決定創(chuàng)建一個云游戲的POC,這樣我就可以驗證在這些嚴(yán)格的網(wǎng)絡(luò)限制下是否仍有可能實現(xiàn)以上所說的低延遲碱茁。我選擇了Golang作為我的POC裸卫,因為這是我最熟悉的語言,當(dāng)然也因為它便于運作且開發(fā)速度快早芭。當(dāng)處理并發(fā)和流操作時彼城,Go通道也非常有用。
這個項目就是CloudRetro.io:針對懷舊游戲的基于Web的云游戲服務(wù)開源項目退个。我的目標(biāo)是希望帶來最舒適的游戲體驗募壕,并將在線多人游戲等網(wǎng)絡(luò)游戲引入傳統(tǒng)的復(fù)古游戲。你可以引用整個項目庫:https://github.com/giongto35/cloud-game CloudRetro的功能性 CloudRetro使用Retro游戲演示了Cloud Gaming的強大功能并帶來了許多獨特的游戲體驗语盈。

  • 便攜式游戲體驗

  • 即點即玩舱馅,無需下載安裝

  • 在瀏覽器上運行,無需任何軟件即可啟動

  • 游戲會話可以在多個設(shè)備之間共享刀荒,并存儲在云中方便下次游戲

  • 游戲可播可玩代嗤,并且多個用戶可以加入同一游戲

  • 類似于TwitchPlayPokemon的人群播放,但更實時缠借,更無縫

  • 在線多人游戲干毅,無需網(wǎng)絡(luò)設(shè)置即可進行離線游戲。現(xiàn)在可以在CloudRetro上通過網(wǎng)絡(luò)與2位玩家一起玩《武士對決》

image

在不同設(shè)備上進行多人線上游戲的demo 架構(gòu) 要求與技術(shù)配置 以下是我在開始這個項目前列出的一些要求泼返。
單人游戲
這項要求聽起來并不相關(guān)且非常直接硝逢,但這是我的主要發(fā)現(xiàn)之一,它使云游戲擺脫了傳統(tǒng)的流媒體服務(wù)。如果我們專注于單人游戲渠鸽,就可以擺脫集中式服務(wù)器或CDN叫乌,因為我們不需要將會話流分配給大量用戶。該服務(wù)不是通過將流上傳到攝取服務(wù)器或?qū)?shù)據(jù)包傳遞到集中式WebSocket服務(wù)器徽缚,而是通過WebRTC對等連接直接流向用戶憨奸。
低延遲媒體流當(dāng)我研究Stadia時,有些文章提到了WebRTC的應(yīng)用凿试。我發(fā)現(xiàn)WebRTC是一項非凡的技術(shù)排宰,而且非常適合云游戲。
WebRTC是一個通過簡單的API為Web瀏覽器和移動應(yīng)用程序提供實時通信的項目红省。它支持對等通信额各,并針對媒體進行了優(yōu)化,并具有內(nèi)置的標(biāo)準(zhǔn)編解碼器吧恃,例如VP8和H264虾啦。我優(yōu)先考慮為用戶提供最流暢的體驗,而不是保留高質(zhì)量的圖形痕寓。該算法中有一些損失是可接受的傲醉。在Google Stadia上,還有一個步驟來減小服務(wù)器上的圖像大小呻率,并且圖像幀在渲染給對等對象之前被重新縮放為更高的質(zhì)量硬毕。具有地理路由的分布式架構(gòu)****無論壓縮算法和代碼如何優(yōu)化,網(wǎng)絡(luò)仍然是導(dǎo)致延遲最關(guān)鍵的因素礼仗。該體系結(jié)構(gòu)需要一種將最近的服務(wù)器與用戶配對的機制吐咳,以減少往返時間(RTT)。這樣的體系結(jié)構(gòu)包含單個協(xié)調(diào)器和分布在世界各地的多個流服務(wù)器:美國西部元践,美國東部韭脊,歐洲,新加坡单旁,中國沪羔。所有流服務(wù)器完全被隔離開來了。當(dāng)服務(wù)器加入或離開網(wǎng)絡(luò)時象浑,系統(tǒng)可以調(diào)整其分布蔫饰。因此,在超高流量下愉豺,添加更多服務(wù)器可實現(xiàn)水平擴展篓吁。
瀏覽器兼容
在用戶需求極少的情況下,云游戲的表現(xiàn)是最好的蚪拦。這也意味著能夠在瀏覽器上運行杖剪。瀏覽器通過刪除軟件和硬件安裝為用戶帶來最舒適的游戲體驗节腐,同時,它還有助于在移動設(shè)備和臺式機之間提供跨平臺的靈活性摘盆。幸運的是,WebRTC在不同的瀏覽器中都具有出色的支持能力饱苟。
明確劃分游戲界面及服務(wù)我將云游戲服務(wù)看作是一個平臺孩擂,一個能夠?qū)⑷魏尾寮迦氲钠脚_。目前箱熬,我將LibRetro(https://www.libretro.com/)與云游服務(wù)集成在一起类垦,因為LibRetro為SNES,GBA城须,PS等復(fù)古游戲提供了美觀的游戲蚤认。
為多人、群體游戲和深層游戲鏈接服務(wù)的基于房間的機制CloudRetro支持許多新穎的游戲玩法糕伐,例如用于復(fù)古游戲的CrowdPlay和線上多人游戲砰琢。如果多個用戶在不同計算機上打開相同的深層鏈接,他們將看到的正在運行的游戲與視頻流相同良瞧,而且他們可以像視頻中任何一個玩家一樣加入游戲陪汽。
此外,游戲狀態(tài)存儲在云中使用戶可以隨時在任何其他設(shè)備上繼續(xù)他們的游戲褥蚯。水平縮放像今天的每一個SAAS一樣挚冤,云游戲必須被設(shè)計為可水平擴展。協(xié)調(diào)器-工作器的設(shè)計允許增加更多的工作器以服務(wù)更多的流量赞庶。
不可知的云CloudRetro的基礎(chǔ)架構(gòu)托管在各種云提供商(Digital Ocean训挡,阿里巴巴,定制提供商)上歧强,以對標(biāo)不同的區(qū)域澜薄。我通過bash腳本對基礎(chǔ)架構(gòu)進行了dockerize和配置網(wǎng)絡(luò)設(shè)置,以避免依賴任何一個云提供商誊锭。結(jié)合使用WebRTC的NAT遍歷表悬,我們可以靈活地將CloudRetro部署在任何云平臺甚至任何用戶的計算機上。
架構(gòu)設(shè)計 worker:(或者是上面提到的流服務(wù)器)生成游戲丧靡、運行編碼管道蟆沫、并將編碼的媒體流傳輸給用戶。Worker分布在世界各地温治,每一個都可以同時處理多個用戶會話饭庞。
Coordinator:負(fù)責(zé)將新用戶與最適合的Worker配對并進行流傳輸,通過WebSocket與worker進行交互熬荆。Game state storage:所有游戲狀態(tài)的中央遠(yuǎn)程存儲舟山。該存儲實現(xiàn)了一些基本功能,例如遠(yuǎn)程保存/加載。

image

CloudRetro高級架構(gòu) 用戶流 當(dāng)新用戶在下圖所示的步驟1和2中打開CloudRetro時累盗,協(xié)調(diào)器將被要求提供前端頁面以及可用Worker列表寒矿。之后,在第3步若债,客戶端使用HTTP ping請求計算所有候選者的延遲符相。此延遲列表隨后發(fā)送回協(xié)調(diào)器,以便它可以確定最適合為用戶服務(wù)的worker蠢琳。在下面的步驟4中啊终,游戲生成。WebRTC流連接是在用戶和指定worker之間建立的傲须。


image

CloudRetro 在獲得后的用戶流 Inside the worker 在worker內(nèi)部藕赞,游戲和流管道保持隔離狀態(tài)文捶,并通過接口交換信息。當(dāng)前,該通信是通過Golang通道上的內(nèi)存?zhèn)鬏斶^程完成的丘逸。下一個目標(biāo)是進一步隔離–即以不同的過程獨立運行游戲犁功。


image

Worker中各組件之間如何交互主要部分是:
WebRTC:面向客戶端的組件棵介,用戶輸入進入后雷,服務(wù)器的編碼媒體輸出。游戲模擬器:游戲組件咬最。借助Libretro庫翎嫡,該系統(tǒng)能夠在同一進程內(nèi)運行游戲,并在內(nèi)部掛鉤媒體和輸入流永乌。游戲中的幀被捕獲并被發(fā)送到編碼器惑申。圖像/音頻編碼器:編碼管道,它在其中接收媒體幀翅雏、在后臺進行編碼并輸出編碼的圖像/音頻圈驼。應(yīng)用CloudRetro依靠WebRTC作為骨干,因此在詳細(xì)介紹我在Golang中的實現(xiàn)之前望几,第一部分要專門介紹WebRTC技術(shù)绩脆。這是一項很棒的技術(shù),可以極大地幫助我實現(xiàn)亞秒級的延遲流橄抹。 WebRTC WebRTC旨在通過簡單的API在本機移動設(shè)備和瀏覽器上實現(xiàn)高質(zhì)量的對等連接靴迫。
NAT Traversal WebRTC以其NAT Traversal功能而聞名,它被設(shè)計用于對等通信楼誓,旨在找到最合適的直接路由玉锌,避免NAT網(wǎng)關(guān)和防火墻通過名為ICE的進程進行對等通信。作為此過程的一部分疟羹,WebRTC API使用STUN服務(wù)器找到您的公共IP地址主守,并在無法建立直接通信時回退到中繼服務(wù)器(TURN)禀倔。
但是,CloudRetro沒有充分利用此功能参淫。它的對等連接不是在用戶與用戶之間救湖,而是在用戶與云服務(wù)器之間。與典型的用戶設(shè)備相比涎才,該模型的服務(wù)器端對直接通信的限制較少捎谨。服務(wù)器不在NAT之后,可以進行預(yù)打開入站端口或直接使用公共IP地址等操作憔维。以前,我曾經(jīng)有讓這個項目成為云游戲分發(fā)平臺的野心畏邢。這個想法是想讓游戲創(chuàng)作者貢獻(xiàn)游戲和流媒體資源业扒,用戶將直接與游戲創(chuàng)作者的提供者配對。以這種分散的方式舒萎,CloudRetro只是將第三方流資源與用戶連接的一種媒介程储。因此當(dāng)托管的負(fù)擔(dān)不再依賴CloudRetro時,它會具有更高的可擴展性臂寝。WebRTC NAT Traversal在簡化第三方流資源上的對等連接初始化時將發(fā)揮重要作用章鲤,進而使創(chuàng)建者毫不費力地加入網(wǎng)絡(luò)。 視頻壓縮 視頻壓縮是管道中必不可少的部分咆贬,它極大地有助于流暢的流媒體體驗败徊。盡管不一定要完全了解VP8 / H264的所有視頻編碼細(xì)節(jié),但了解其概念有助于闡明流速度參數(shù)掏缎、調(diào)試意外行為并調(diào)整延遲皱蹦。用于流服務(wù)的視頻壓縮具有挑戰(zhàn)性,因為該算法需要確本祢冢總編碼時間+網(wǎng)絡(luò)傳輸+解碼時間的總和盡可能短沪哺。另外,編碼過程需要是連續(xù)并有次序的酌儒。某些傳統(tǒng)的編碼折衷方法并不適用–例如用較長的編碼時間換取較小的文件大小和解碼時間辜妓,或者是無序壓縮。視頻壓縮需要忽略不必要的信息忌怎,同時將保真度控制在保持用戶可以理解和接受的范圍籍滴。除了對單個靜態(tài)圖像幀進行編碼之外,該算法還根據(jù)先前和將來的幀對當(dāng)前幀進行了推斷榴啸,因此僅發(fā)送差異异逐。如同在下面的Pacman示例中看到的,僅有差分點被傳輸插掂。

image

以Pacman為例的視頻幀比對
音頻壓縮 同樣灰瞻,音頻壓縮算法會忽略人類無法感知的數(shù)據(jù)腥例。****目前性能最佳的音頻編解碼器是Opus。Opus旨在通過有序數(shù)據(jù)報協(xié)議(例如RTP實時傳輸協(xié)議)傳輸音頻波酝润。它比(mp3燎竖,aac)具有更高的質(zhì)量、產(chǎn)生更低的延遲(通常約為5?66.5 ms)Pion是一個將WebRTC引入Golang的開源項目要销。Pion不是簡單地包裝本機C ++ WebRTC庫构回,而是一種本機Golang實現(xiàn),可以實現(xiàn)更好的性能疏咐、更好的Golang集成以及對基本W(wǎng)ebRTC協(xié)議的版本控制纤掸。該庫還提供具有許多出色內(nèi)置功能的亞秒級延遲流。它具有自己的STUN浑塞,DTLS借跪,SCTP等實現(xiàn),以及QUIC和WebAssembly的一些實驗酌壕。這個開源庫本身是一個很好的學(xué)習(xí)資源掏愁,其中包含出色的文檔、網(wǎng)絡(luò)協(xié)議實現(xiàn)和示例卵牍。由非常熱情的創(chuàng)建者領(lǐng)導(dǎo)的Pion社區(qū)非彻郏活躍,并且對WebRTC進行了許多高質(zhì)量的討論糊昙。如果你對此技術(shù)感興趣辛掠,請加入http://pion.ly/slack –你將學(xué)到許多新東西。 Write CloudRetro in Golang

image

Go在worker內(nèi)的實施 Go Channel In Action 由于Go的通道設(shè)計精美释牺,事件流和并發(fā)問題得到了極大的簡化公浪。如圖所示,在不同的GoRoutine中有多個并行運行的組件船侧,每個組件管理自己的狀態(tài)并通過通道進行通信欠气。

Golang的select語句強制每個game tick都處理一個原子事件,這意味著此設(shè)計不需要鎖定镜撩。例如预柒,當(dāng)用戶保存時,需要一個完整的游戲狀態(tài)快照袁梗。該狀態(tài)需要通過運行輸入保持不間斷宜鸯,直到保存完成。在每個game tick中遮怜,后端只能處理保存操作或輸入操作淋袖,因此它也是同時安全的。
image

Fan-in / Fan-out 這個Golang模式與我的CrowdPlay和Multiple Player用例完全匹配锯梁。按照這種模式即碗,同一房間中的所有用戶輸入都扇入一個中央輸入通道焰情,然后將游戲媒體分發(fā)給同一房間中的所有用戶。因此剥懒,我們實現(xiàn)了來自不同用戶的多個游戲會話之間的游戲狀態(tài)共享内舟。


image

不同會話之間的同步
Golang的劣勢 Golang并不完美,它的通道緩慢初橘。與鎖定相比验游,Go通道只是處理并發(fā)和流事件的更簡單方法,但是通道并不能提供最佳性能保檐。通道下有一個復(fù)雜的鎖定邏輯耕蝉。因此,我通過在替換通道時重新應(yīng)用鎖定和原子值來對性能進行一些調(diào)整夜只,以優(yōu)化性能垒在。
此外,Golang垃圾收集器是無法控制的盐肃,因此有時會有一些可疑的長時間停頓。這極大地?fù)p害了該應(yīng)用程序流的實時性权悟。 CGO 該項目使用一些現(xiàn)有的Golang開源VP8 / H264庫進行媒體壓縮砸王,并使用Libretro作為游戲模擬器。所有這些庫都只是使用CGO在Go中對C庫的包裝峦阁。你可以參考Dave的這篇博客文章(https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface)谦铃。我現(xiàn)在面臨的問題是:

  • 即使使用Golang Recovery,也無法捕獲CGO的崩潰

  • 無法確定CGO下的細(xì)粒度問題就無法定義性能瓶頸

總結(jié) 我實現(xiàn)了揭開云游戲服務(wù)神秘面紗的目標(biāo)榔昔,并創(chuàng)建了一個平臺驹闰,可以幫助我和朋友們在線玩懷舊的復(fù)古游戲。沒有Pion庫和Pion社區(qū)的支持撒会,這個項目是不可能實現(xiàn)的嘹朗。我非常感謝Pion及其密集的開發(fā),WebRTC和Pion提供的簡單API也可以實現(xiàn)平穩(wěn)的集成诵肛。盡管集成起來很簡單屹培,但是P2P流媒體的確是計算機科學(xué)中一個非常具有挑戰(zhàn)性的領(lǐng)域。它必須處理IP和NAT等常年網(wǎng)絡(luò)架構(gòu)的復(fù)雜性才能創(chuàng)建對等會話怔檩。在從事此項目的過程中褪秀,我積累了許多有關(guān)網(wǎng)絡(luò)和性能優(yōu)化的寶貴知識,因此薛训,我建議所有人嘗試使用WebRTC構(gòu)建一些P2P產(chǎn)品媒吗。CloudRetro可滿足我作為復(fù)古游戲玩家的所有用例。但是乙埃,我認(rèn)為我可以改進項目中的許多方面闸英,例如使網(wǎng)絡(luò)更可靠锯岖、性能更高、提供更高圖形質(zhì)量的游戲或在用戶之間共享游戲自阱。我正在為此而努力嚎莉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沛豌,隨后出現(xiàn)的幾起案子趋箩,更是在濱河造成了極大的恐慌,老刑警劉巖加派,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叫确,死亡現(xiàn)場離奇詭異,居然都是意外死亡芍锦,警方通過查閱死者的電腦和手機竹勉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娄琉,“玉大人次乓,你說我怎么就攤上這事∧跛” “怎么了票腰?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長女气。 經(jīng)常有香客問我杏慰,道長,這世上最難降的妖魔是什么炼鞠? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任缘滥,我火速辦了婚禮,結(jié)果婚禮上谒主,老公的妹妹穿的比我還像新娘朝扼。我一直安慰自己,他們只是感情好霎肯,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布吟税。 她就那樣靜靜地躺著,像睡著了一般姿现。 火紅的嫁衣襯著肌膚如雪肠仪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天备典,我揣著相機與錄音异旧,去河邊找鬼提佣。 笑死吮蛹,一個胖子當(dāng)著我的面吹牛荤崇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播潮针,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼术荤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了每篷?” 一聲冷哼從身側(cè)響起瓣戚,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎焦读,沒想到半個月后子库,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡矗晃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年仑嗅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片张症。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡仓技,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俗他,到底是詐尸還是另有隱情脖捻,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布拯辙,位于F島的核電站郭变,受9級特大地震影響颜价,放射性物質(zhì)發(fā)生泄漏涯保。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一周伦、第九天 我趴在偏房一處隱蔽的房頂上張望夕春。 院中可真熱鬧,春花似錦专挪、人聲如沸及志。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽速侈。三九已至,卻和暖如春迫卢,著一層夾襖步出監(jiān)牢的瞬間倚搬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工乾蛤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留每界,地道東北人捅僵。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像眨层,于是被迫代替她去往敵國和親庙楚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348