日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

(二)PUN 2基本教程

發(fā)布時間:2024/1/8 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 (二)PUN 2基本教程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

一、介紹

PUN 基礎教程是一個基于 Unity 的教程。我們將使用 Photon Cloud 開發(fā)第一個簡單的 PUN 2 多人游戲。目標是同步每個玩家的動畫角色、健康值和基本光線投射。

1.概述

本教程將從一個空項目開始,逐步指導您完成整個創(chuàng)建過程。在此過程中,將解釋概念,以及網(wǎng)絡游戲的常見陷阱和設計注意事項。
我們將實現(xiàn)一個簡單的 UI 來輸入昵稱,我們將向用戶顯示連接進度。
該游戲最多可容納 4 名玩家,并根據(jù)房間內的玩家數(shù)量自定義大小的競技場。這主要是為了展示有關同步場景的幾個概念:加載不同場景時如何處理玩家以及這樣做時可能出現(xiàn)的問題:)
為了不只是讓玩家四處走動而無所事事,我們將實施一個基本的射擊系統(tǒng)以及玩家的健康管理。到那時,我們將學習如何跨網(wǎng)絡同步變量。
當你的健康為 0 時,游戲結束,你離開競技場。然后您會再次看到介紹屏幕,如果需要,您可以開始新游戲。

2.你需要知道的

本教程僅假定使用 Unity 編輯器和編程的基礎知識。但是,最好具備良好的游戲創(chuàng)建知識和一些經(jīng)驗,以便專注于 Photon Unity Networking 引入的新概念。
示例代碼是用 C# 編寫的。

3.導入 PUN 和設置

確保您使用等于或高于 2017.4 的 Unity 版本(不推薦測試版)。創(chuàng)建一個新項目,一般在處理教程時建議這樣做。
打開資產(chǎn)商店并找到 PUN 2 資產(chǎn)并下載/安裝它。導入所有資產(chǎn)后,讓 Unity 重新編譯。
PUN 設置向導可以幫助您進行網(wǎng)絡設置,并提供一種方便的方式來開始我們的多人游戲:光子云!
云?是的,云。這是我們可以在游戲中使用的一組 Photon 服務器。我們稍后會解釋。
通過“免費計劃”使用云是免費的,沒有義務,所以現(xiàn)在我們只需輸入我們的郵件地址,向導就會發(fā)揮它的魔力。

新帳戶立即獲得“AppId”。如果您的郵件地址已經(jīng)注冊,系統(tǒng)會要求您打開儀表板。登錄并獲取“Ap??pId”以將其粘貼到輸入字段中。
保存 AppId 后,我們就完成了這一步。

?那么,這個“光子云”到底有什么作用呢?!
基本上,它是一堆機器,上面運行著 Photon 服務器。這個“云”服務器由 Exit Games 維護,并為您的多人游戲提供無憂服務。服務器按需添加,因此可以處理任意數(shù)量的玩家。
盡管 Photon Cloud 并非完全免費,但成本很低,尤其是與常規(guī)托管相比。
Photon Unity Networking 將為您處理 Photon Cloud,但簡而言之,這是內部發(fā)生的事情:
每個人都首先連接到“名稱服務器”。它檢查客戶端想要使用哪個應用程序(帶有 AppId)和哪個區(qū)域。然后它將客戶端轉發(fā)到主服務器。
主服務器是一堆區(qū)域服務器的樞紐。它知道該區(qū)域的所有房間。任何時候創(chuàng)建或加入房間(比賽/游戲)時,客戶端都會被轉發(fā)到另一臺稱為“游戲服務器”的機器。
PUN 中的設置非常簡單,您不必關心托管成本、性能或維護。不止一次。

4.應用程序 ID 和游戲版本

由于每個人都連接到相同的服務器,因此必須有一種方法將您的玩家與其他人的玩家分開。
每個標題(如在游戲、應用程序中)在云中都有自己的“AppId”。玩家只會遇到具有相同“AppId”的其他玩家。
還有一個“游戲版”。這是一個您可以編輯的字符串,它將把擁有老客戶的玩家與擁有新客戶的玩家區(qū)分開來。

5.地區(qū)

Photon Cloud 在全球不同的區(qū)域組織,以實現(xiàn)玩家之間的最佳連接。
每個區(qū)域都與所有其他區(qū)域分開,在與分布在不同區(qū)域的遠程團隊合作時記住這一點很重要。確保您最終位于同一地區(qū)。
PUN 2 通過確定一個“開發(fā)區(qū)域”來幫助您,該區(qū)域用于所有開發(fā)構建。

6.房間

Photon Cloud 是為“基于房間的游戲”而構建的,這意味著每場比賽的玩家數(shù)量有限(比如說:最多 16 人),與其他人分開。在一個房間里,每個人都會收到其他人發(fā)送的任何信息(除非您向特定玩家發(fā)送消息)。在房間外,玩家無法交流,所以我們總是希望他們盡快進入房間。
進入房間的最佳方式是使用隨機匹配??:只需向服務器詢問任何房間或指定玩家期望的一些屬性。
所有房間都有一個名稱作為標識符。除非房間已滿或關閉,否則我們可以通過名字加入。方便的是,主服務器可以為我們的應用程序提供房間列表。

7.大廳

您的應用程序的大廳存在于主服務器上,用于列出您的游戲的房間。它不能讓玩家相互交流!
在我們的示例中,我們不會使用大廳,而是簡單地加入一個隨機房間(如果有可用房間),或者如果沒有現(xiàn)有房間可以加入則創(chuàng)建一個新房間(房間可以有最大容量,因此它們可能是全部滿的)。

?8.開發(fā)

本教程的每個部分都涵蓋了項目開發(fā)階段的特定部分。對腳本和 Photon 知識的假設水平逐漸增加。在最好的情況下,按順序完成它們。

  • 創(chuàng)建基本的大廳場景。
  • 使用用戶界面 (ui) 改善大廳場景。
  • 創(chuàng)建游戲場景。
  • 實施關卡加載。
  • 創(chuàng)建基本的播放器預制件。
  • 讓相機跟隨你的玩家。
  • 修改播放器預制件以添加網(wǎng)絡功能。
  • 播放器實例化和場景切換。
  • 播放器用戶界面 (ui)。
  • 9.教程之外

    當然,要創(chuàng)建一個完整的游戲還有很多工作要做,但這只是建立在我們在這里介紹的基礎之上。
    請務必閱讀“入門”部分。
    保持好奇,瀏覽文檔和 API 參考只是為了全面了解可用的內容。您可能不會立即需要所有內容,但是當您需要它或實現(xiàn)新功能時,它會重新出現(xiàn)在您的記憶中。您會記得某些方法或屬性是相關的,因此是時候正確了解它們了。
    利用論壇,不要猶豫,分享您的問題、問題,甚至挫敗感 :) 重要的是您不要被問題困住。通過寫下來讓其他人理解你的問題,你會在你的大腦之外制定它,這有助于解決問題。沒有愚蠢的問題,這完全取決于您的專業(yè)水平以及您對 Unity 和 PUN 的學習/掌握程度。

    二、大廳

    1.連接到服務器、房間訪問和創(chuàng)建

    讓我們首先解決本教程的核心問題,能夠連接到 Photon Cloud 服務器并加入一個房間或在必要時創(chuàng)建一個房間。

  • 創(chuàng)建一個新場景,并將其保存為 Launcher.unity。
  • 創(chuàng)建一個新的 C# 腳本啟動器。
  • 在層次結構中創(chuàng)建一個名為 Launcher 的空游戲對象。
  • 將 C# 腳本啟動器附加到游戲對象啟動器。
  • 編輯 C# 腳本啟動器使其內容如下:
  • 保存 C# 腳本啟動器。
  • using UnityEngine; using Photon.Pun;namespace Com.MyCompany.MyGame {public class Launcher : MonoBehaviour{#region Private Serializable Fields#endregion#region Private Fields/// <summary>/// This client's version number. Users are separated from each other by gameVersion (which allows you to make breaking changes)./// </summary>string gameVersion = "1";#endregion#region MonoBehaviour CallBacks/// <summary>/// MonoBehaviour method called on GameObject by Unity during early initialization phase./// </summary>void Awake(){// #Critical// this makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automaticallyPhotonNetwork.AutomaticallySyncScene = true;}/// <summary>/// MonoBehaviour method called on GameObject by Unity during initialization phase./// </summary>void Start(){Connect();}#endregion#region Public Methods/// <summary>/// Start the connection process./// - If already connected, we attempt joining a random room/// - if not yet connected, Connect this application instance to Photon Cloud Network/// </summary>public void Connect(){// we check if we are connected or not, we join if we are , else we initiate the connection to the server.if (PhotonNetwork.IsConnected){// #Critical we need at this point to attempt joining a Random Room. If it fails, we'll get notified in OnJoinRandomFailed() and we'll create one.PhotonNetwork.JoinRandomRoom();}else{// #Critical, we must first and foremost connect to Photon Online Server.PhotonNetwork.ConnectUsingSettings();PhotonNetwork.GameVersion = gameVersion;}}#endregion} }

    編碼提示:不要復制/粘貼代碼,您應該自己輸入所有內容,因為您可能會更好地記住它。編寫注釋非常簡單,在方法或屬性上方的行中鍵入 ///,您將讓腳本編輯器自動創(chuàng)建結構化注釋,例如帶有 <summary> 標記。

    讓我們回顧一下到目前為止該腳本中的內容,首先是從一般的 Unity 角度,然后是我們進行的 PUN 特定調用。

    (1)命名空間

    雖然不是強制性的,但為您的腳本提供適當?shù)拿臻g可以防止與其他資產(chǎn)和開發(fā)人員發(fā)生沖突。如果另一個開發(fā)人員也創(chuàng)建了一個類 Launcher 怎么辦? Unity 會抱怨,您或那個開發(fā)人員將不得不為 Unity 重命名該類以允許執(zhí)行項目。如果沖突來自您從資產(chǎn)商店下載的資產(chǎn),這可能會很棘手。現(xiàn)在,Launcher 類實際上是 Com.MyCompany.MyGame.Launcher 實際上,其他人不太可能擁有完全相同的命名空間,因為您擁有該域,因此使用反向域約定作為命名空間可以使您的工作安全且井井有條。 Com.MyCompany.MyGame 應該替換為您自己的反向域名和游戲名稱,這是一個很好的慣例。

    (2)MonoBehaviour 類

    請注意,我們從 MonoBehaviour 派生我們的類,這實質上將我們的類變成了一個 Unity 組件,然后我們可以將其放到 GameObject 或 Prefab 上。擴展 MonoBehaviour 的類可以訪問許多非常重要的方法和屬性。在您的例子中,我們將使用兩種回調方法,Awake() 和 Start()。

    (3)PhotonNetwork.GameVersion

    請注意代表您的游戲版本的 gameVersion 變量。您應該將其保留為“1”,直到您需要對已經(jīng)上線的項目進行重大更改。

    (4)PhotonNetwork.ConnectUsingSettings()

    在 Start() 期間,我們調用此方法的公共函數(shù) Connect()。這里要記住的重要信息是,此方法是連接到 Photon Cloud 的起點。

    (5)PhotonNetwork.AutomaticallySyncScene

    我們的游戲將有一個可根據(jù)玩家數(shù)量調整大小的競技場,并確保加載的場景對于每個連接的玩家都是相同的,我們將利用 Photon 提供的非常方便的功能:PhotonNetwork.AutomaticallySyncScene 當這是是的,masterclient 可以調用 PhotonNetwork.LoadLevel() 并且所有連接的玩家將自動加載相同的級別。

    此時,可以保存Launcher Scene,打開PhotonServerSettings(在Unity菜單Window/Photon Unity Networking/Highlight Photon Server Settings中選擇),我們需要將PUN Logging設置為“Full”:

    ?編碼時要養(yǎng)成的一個好習慣是始終測試潛在的失敗。這里我們假設電腦是聯(lián)網(wǎng)的,但是如果電腦沒有聯(lián)網(wǎng)會怎樣呢?讓我們找出來。關閉計算機上的互聯(lián)網(wǎng)并播放場景。您應該會在 Unity 控制臺中看到此錯誤:

    Connect() to 'ns.exitgames.com' failed: System.Net.Sockets.SocketException: No such host is known.

    ?理想情況下,我們的腳本應該意識到這個問題,并對這些情況作出優(yōu)雅的反應,并提出響應式體驗,無論出現(xiàn)什么情況或問題。
    現(xiàn)在讓我們處理這兩種情況,并在我們的 Launcher 腳本中獲知我們確實連接或未連接到 Photon Cloud。這將是對 PUN 回調的完美介紹。

    2.PUN 回調

    PUN 在回調方面非常靈活,并提供兩種不同的實現(xiàn)。為了學習,讓我們涵蓋所有方法,我們將根據(jù)情況選擇最適合的方法。

    3.實現(xiàn)回調接口

    PUN 提供了您可以在類中實現(xiàn)的 C# 接口:

    IConnectionCallbacks:連接相關的回調。
    IInRoomCallbacks:房間內發(fā)生的回調。
    ILobbyCallbacks:大廳相關回調。
    IMatchmakingCallbacks:匹配相關回調。
    IOnEventCallback:任何接收到的事件的單一回調。這等效于 C# 事件 OnEventReceived。
    IWebRpcCallback:接收WebRPC操作響應的單一回調。
    IPunInstantiateMagicCallback:實例化 PUN 預制件的單個回調。
    IPunObservable:PhotonView 序列化回調。
    IPunOwnershipCallbacks:PUN 所有權轉移回調。

    回調接口必須注冊和注銷。調用 PhotonNetwork.AddCallbackTarget(this) 和 PhotonNetwork.RemoveCallbackTarget(this)(可能分別在 OnEnable() 和 OnDisable() 中)

    這是確保類符合所有接口但強制開發(fā)人員實現(xiàn)所有接口聲明的一種非常安全的方法。大多數(shù)優(yōu)秀的 IDE 將使這項任務變得非常容易。然而,腳本最終可能會包含許多可能什么都不做的方法,但必須實現(xiàn)所有方法才能使 Unity 編譯器滿意。因此,這確實是您的腳本將大量使用所有或大部分 PUN 功能的時候。

    我們確實要使用 IPunObservable,在本教程的后面進行數(shù)據(jù)序列化。

    4.擴展 MonoBehaviourPunCallbacks

    我們將經(jīng)常使用的另一種技術是最方便的。我們將從 MonoBehaviourPunCallbacks 派生類,而不是創(chuàng)建派生自 MonoBehaviour 的類,因為它公開了特定的屬性和虛方法,供我們在方便時使用和覆蓋。這非常實用,因為我們可以確定我們沒有任何錯別字,我們不需要實現(xiàn)所有方法。
    注意:覆蓋時,大多數(shù) IDE 默認會執(zhí)行一個堿基調用并自動為您填充。在我們的例子中,我們不需要,因此作為 MonoBehaviourPunCallbacks 的一般規(guī)則,永遠不要調用基本方法,除非您覆蓋 OnEnable() 或 OnDisable()。如果您覆蓋 OnEnable() 和 OnDisable(),請始終調用基類方法。

    因此,讓我們通過 OnConnectedToMaster() 和 OnDisconnected() PUN 回調將其付諸實踐 :

    (1)編輯 C# 腳本啟動器

    (2)將基類從 MonoBehaviour 修改為 MonoBehaviourPunCallbacks

    public class Launcher : MonoBehaviourPunCallbacks {

    (3)使用 Photon.Realtime 添加;在類定義之前的文件頂部。

    (4)為清楚起見,在類的末尾添加以下兩個方法,在 MonoBehaviourPunCallbacks 回調區(qū)域內。

    #region MonoBehaviourPunCallbacks Callbackspublic override void OnConnectedToMaster() {Debug.Log("PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN"); }public override void OnDisconnected(DisconnectCause cause) {Debug.LogWarningFormat("PUN Basics Tutorial/Launcher: OnDisconnected() was called by PUN with reason {0}", cause); }#endregion

    (5)保存啟動器腳本。

    現(xiàn)在,如果我們在有或沒有互聯(lián)網(wǎng)的情況下播放這個場景,我們可以采取適當?shù)牟襟E來通知播放器和/或進一步處理邏輯。當我們開始構建 UI 時,我們將在下一節(jié)中處理這個問題。現(xiàn)在我們將處理成功的連接:

    因此,我們將以下調用附加到 OnConnectedToMaster() 方法:

    // #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed() PhotonNetwork.JoinRandomRoom();

    正如評論所說,如果嘗試加入隨機房間失敗,我們需要得到通知,在這種情況下我們需要實際創(chuàng)建一個房間,因此我們在腳本中實現(xiàn) OnJoinRandomFailed() PUN 回調并使用 PhotonNetwork.CreateRoom 創(chuàng)建一個房間() 并且,您已經(jīng)猜到了,相關的 PUN 回調 OnJoinedRoom() 將在我們有效加入房間時通知您的腳本:

    public override void OnJoinRandomFailed(short returnCode, string message) {Debug.Log("PUN Basics Tutorial/Launcher:OnJoinRandomFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom");// #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.PhotonNetwork.CreateRoom(null, new RoomOptions()); }public override void OnJoinedRoom() {Debug.Log("PUN Basics Tutorial/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room."); }

    現(xiàn)在,如果您運行該場景,您應該按照連接到 PUN 的邏輯順序結束,嘗試加入現(xiàn)有房間,否則創(chuàng)建一個房間并加入新創(chuàng)建的房間。
    在本教程的這一點上,由于我們現(xiàn)在已經(jīng)涵蓋了連接和加入房間的關鍵方面,所以有一些事情不是很方便,但他們需要盡快解決。這些與學習PUN并無太大關系,但從整體角度來看卻很重要。

    5.在 Unity Inspector 中公開字段

    您可能已經(jīng)知道這一點,但如果您不知道,MonoBehaviours 可以自動將字段公開給 Unity Inspector。默認情況下,所有公共字段都是公開的,除非它們被標記為 [HideInInspector]。如果我們想公開非公共字段,我們可以使用屬性 [SerializeField]。這是 Unity 中一個非常重要的概念,在我們的例子中,我們將修改每個房間的最大玩家數(shù)量并將其顯示在檢查器中,以便我們可以在不觸及代碼本身的情況下進行設置。

    我們將對每個房間的最大玩家數(shù)量做同樣的事情。在代碼中對此進行硬編碼并不是最佳做法,相反,讓我們將其作為公共變量,以便我們稍后可以決定并使用該數(shù)字,而無需重新編譯。

    在類聲明的開頭,在 Private Serializable Fields 區(qū)域內讓我們添加:

    /// <summary> /// The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created. /// </summary> [Tooltip("The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created")] [SerializeField] private byte maxPlayersPerRoom = 4;

    然后我們修改 PhotonNetwork.CreateRoom() 調用并使用這個新字段而不是我們之前使用的硬編碼數(shù)字。

    // #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room. PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = maxPlayersPerRoom });

    ?所以,現(xiàn)在我們不強制腳本使用靜態(tài) MaxPlayers 值,我們只需要在 Unity 檢查器中設置它,然后點擊運行,不需要打開腳本,編輯它,保存它,等待 Unity 重新編譯最后運行。這種方式的效率和靈活性要高得多。

    ?三、大廳UI

    本部分將重點介紹為大廳創(chuàng)建用戶界面 (UI)。它將保持非常基礎,因為它與網(wǎng)絡本身并沒有真正的關系。

    1.Play按鈕

    目前我們的大廳自動將我們連接到一個房間,這對早期測試很有幫助,但實際上我們想讓用戶選擇是否以及何時開始游戲。因此,我們只需為此提供一個按鈕。

  • 打開場景啟動器。
  • 使用 Unity 菜單“GameObject/UI/Button”創(chuàng)建一個 UI Button,將該按鈕命名為 Play Button,請注意,它在場景層次結構中創(chuàng)建了一個 Canvas 和一個 EventSystem GameObject,所以我們不必這樣做,很好 :)
  • 將播放按鈕的子文本值編輯為“播放”
  • 選擇 Play Button 并找到 Button 組件內的 On Click () 部分
  • 單擊小“+”以添加條目
  • 將 Launcher GameObject 從 Hierarchy 拖到 Field 中
  • 在下拉菜單中選擇 Launcher.Connect() 我們現(xiàn)在已將 Button 與我們的 Launcher Script 連接起來,這樣當用戶按下該 Button 時,它將從我們的 Launcher Script 調用方法“Connect()”。
  • 打開腳本啟動器。
  • 在 Start() 中刪除我們調用 Connect() 的行
  • 保存腳本啟動器并保存場景。
  • ?如果您現(xiàn)在點擊播放,請注意在您點擊按鈕之前您不會連接。

    2.玩家名稱

    典型游戲的另一個重要的最低要求是讓用戶輸入他們的名字,這樣其他玩家就知道他們在和誰玩。我們將通過使用 PlayerPrefs 來記住名稱的值,從而為這個簡單的任務添加一個轉折點,以便當用戶再次打開游戲時,我們可以恢復名稱。這是一個非常方便且非常重要的功能,可以在您的游戲的許多區(qū)域實現(xiàn),以獲得出色的用戶體驗。
    讓我們首先創(chuàng)建將管理和記住玩家名稱的腳本,然后創(chuàng)建相關的 UI。

    3.創(chuàng)建 PlayerNameInputField

    (1)創(chuàng)建一個新的 C# 腳本,將其命名為 PlayerNameInputField

    (2)這是它的全部內容。相應地編輯并保存 PlayerNameInputField 腳本

    using UnityEngine; using UnityEngine.UI;using Photon.Pun; using Photon.Realtime;using System.Collections;namespace Com.MyCompany.MyGame {/// <summary>/// Player name input field. Let the user input his name, will appear above the player in the game./// </summary>[RequireComponent(typeof(InputField))]public class PlayerNameInputField : MonoBehaviour{#region Private Constants// Store the PlayerPref Key to avoid typosconst string playerNamePrefKey = "PlayerName";#endregion#region MonoBehaviour CallBacks/// <summary>/// MonoBehaviour method called on GameObject by Unity during initialization phase./// </summary>void Start () {string defaultName = string.Empty;InputField _inputField = this.GetComponent<InputField>();if (_inputField!=null){if (PlayerPrefs.HasKey(playerNamePrefKey)){defaultName = PlayerPrefs.GetString(playerNamePrefKey);_inputField.text = defaultName;}}PhotonNetwork.NickName = defaultName;}#endregion#region Public Methods/// <summary>/// Sets the name of the player, and save it in the PlayerPrefs for future sessions./// </summary>/// <param name="value">The name of the Player</param>public void SetPlayerName(string value){// #Importantif (string.IsNullOrEmpty(value)){Debug.LogError("Player Name is null or empty");return;}PhotonNetwork.NickName = value;PlayerPrefs.SetString(playerNamePrefKey,value);}#endregion} }

    讓我們分析一下這個腳本:

    RequireComponent(typeof(InputField)):
    我們首先確保此腳本強制執(zhí)行 InputField,因為我們需要它,這是保證此腳本無故障使用的非常方便快捷的方法。

    PlayerPrefs.HasKey(),?PlayerPrefs.GetString()?and?PlayerPrefs.SetString():

    PlayerPrefs 是一個簡單的配對條目查找列表(就像一個有兩列的 excel 表),一列是鍵,一列是值。 Key 是一個字符串,完全是任意的,你決定如何命名,你需要在整個開發(fā)過程中堅持使用它。因此,始終將 PlayerPrefs Key 存儲在一個地方是有意義的,一種方便的方法是使用 [Static|變量聲明,因為它在游戲過程中不會隨時間改變,每次都是一樣的。可以一直將其聲明為 const,但隨著您使用 C# 獲得越來越多的經(jīng)驗,您將會了解這一點,這只是在此處戲弄 C# 的可能性范圍。

    因此,邏輯非常簡單。如果 PlayerPrefs 有一個給定的鍵,我們可以在啟動該功能時獲取它并直接注入該值,在我們的例子中,我們在啟動時用這個填充 InputField,在編輯期間,我們將 PlayerPref Key 設置為當前InputField 的值,然后我們確定它已本地存儲在用戶設備上以供以后檢索(用戶下次打開該游戲時)。?

    PhotonNetwork.NickName:

    這是此腳本的要點,通過網(wǎng)絡設置播放器的名稱。該腳本在兩個地方使用它,一次是在檢查名稱是否存儲在 PlayerPrefs 之后的 Start() 期間,一次是在公共方法 SetPlayerName() 中。現(xiàn)在,沒有任何東西調用這個方法,我們需要綁定 InputField OnValueChange() 來調用 SetPlayerName() 以便每次用戶編輯 InputField 時,我們都會記錄它。我們只能在用戶按下播放鍵時執(zhí)行此操作,這取決于您,但是這更多涉及腳本方面的知識,因此為了清楚起見,讓我們保持簡單。這也意味著無論用戶將做什么,輸入都會被記住,這通常是期望的行為。

    4.為玩家的名字創(chuàng)建 UI

  • 確保您仍在場景啟動器中。
  • 使用 Unity 菜單“GameObject/UI/InputField”創(chuàng)建一個 UI InputField,將其命名為 GameObject Name InputField
  • 將 RectTransform 中的 PosY 值設置為 35,使其位于播放按鈕上方
  • 找到 Name InputField 的 PlaceHolder 子項并將其文本值設置為“輸入您的姓名...”
  • 選擇名稱 InputField GameObject
  • 添加我們剛剛創(chuàng)建的 PlayerNameInputField 腳本
  • 在 InputField 組件內找到 On Value Change (String) 部分
  • 單擊小“+”以添加條目
  • 將附加到同一 GameObject 的 PlayerNameInputField 組件拖到該字段中
  • 在下拉菜單中選擇 Dynamic String 部分下的 PlayerNameInputField.SetPlayerName
  • 保存場景。
  • 現(xiàn)在你可以點擊播放,輸入你的名字,然后停止播放,再次點擊播放,你輸入的內容就會出現(xiàn)。
    我們正在取得進展,但就用戶體驗而言,我們缺少有關連接進度的反饋,以及連接和加入房間時出現(xiàn)問題的反饋。

    ?5.連接進度

    我們將在這里保持簡單,隱藏名稱字段和播放按鈕,并在連接期間將其替換為簡單的文本“正在連接...”,并在需要時將其切換回來。
    為此,我們將對播放按鈕進行分組并命名為 Field,這樣我們就可以簡單地激活和停用該組。稍后可以將更多功能添加到組中,這不會影響我們的邏輯。

  • 確保您仍在場景啟動器中。
  • 使用 Unity 菜單“GameObject/UI/Panel”創(chuàng)建一個 UI 面板,將其命名為 GameObject Control Panel
  • 從控制面板中刪除圖像和畫布渲染器組件,我們不需要這個面板的任何視覺效果,我們只關心它的內容。
  • 將播放按鈕和名稱輸入字段拖放到控制面板上
  • 使用 Unity 菜單“GameObject/UI/Text”創(chuàng)建一個 UI Text,命名為 GameObject Progress Label 不要擔心它會干擾視覺,我們將在運行時相應地激活/停用它們。
  • 選擇進度標簽的文本組件
  • 設置Alignment為居中對齊和中間對齊
  • 將文本值設置為“正在連接...”
  • 將顏色設置為白色或任何從背景中脫穎而出的顏色。
  • 保存場景
  • ?此時,為了進行測試,您可以簡單地啟用/禁用控制面板和進度標簽,以查看各個連接階段的情況。現(xiàn)在讓我們編輯腳本來控制這兩個游戲對象的激活。

    (1)編輯腳本Launcher

    (2)在 Public Fields 區(qū)域內添加以下兩個屬性

    [Tooltip("The Ui Panel to let the user enter name, connect and play")] [SerializeField] private GameObject controlPanel; [Tooltip("The UI Label to inform the user that the connection is in progress")] [SerializeField] private GameObject progressLabel;

    (3)在 Start() 方法中添加以下內容

    progressLabel.SetActive(false); controlPanel.SetActive(true);

    (4)在 Connect() 方法的開頭添加以下內容

    progressLabel.SetActive(true); controlPanel.SetActive(false);

    (5)將以下內容添加到 OnDisconnected() 方法的開頭

    progressLabel.SetActive(false); controlPanel.SetActive(true);

    (6)保存 Script Launcher 并等待 Unity 完成編譯

    (7)確保您仍在場景Launcher中。

    (8)在層次結構中選擇 GameObject Launcher

    (9)從層次結構Control Panel和Progress Label拖放到Launcher組件中的相應字段

    (10)保存場景

    現(xiàn)在,如果您播放場景,您將只看到控制面板,可見并且只要您單擊播放,就會顯示進度標簽。
    現(xiàn)在,我們對大廳部分很好。為了進一步向大廳添加功能,我們需要切換到游戲本身,并創(chuàng)建各種場景,以便我們最終可以在加入房間時加載正確的級別。我們將在下一節(jié)中完成,之后,我們將最終完成大廳系統(tǒng)。

    四、游戲場景

    本節(jié)介紹玩家將要玩的各種場景的創(chuàng)建。
    每個場景都將專供特定數(shù)量的玩家使用,場景會越來越大以適應所有玩家,并為他們提供足夠的移動空間。
    在本教程的后續(xù)部分,我們將實現(xiàn)根據(jù)玩家數(shù)量加載正確關卡的邏輯,為此我們將使用一個約定,即每個關卡將使用以下格式命名:“Room for X”,其中 X將代表玩家的數(shù)量。

    1.第一個房間創(chuàng)建

  • 創(chuàng)建一個新場景,保存并命名為 Room for 1。
  • 創(chuàng)建一個立方體并將其命名為 floor。
  • 將其定位在 0,0,0。這很重要,因為我們的邏輯系統(tǒng)會在中心 (0,x,??0) 上方生成玩家。
  • 將地板縮放到 20、1、20。
  • 這對于一個可玩的關卡來說已經(jīng)足夠了,但是一些墻會讓玩家保持在地板區(qū)域內。只需創(chuàng)建更多立方體并定位、旋轉和縮放它們即可充當墻壁。這是所有四面墻的位置和比例,以匹配物體floor

    ?此時不要忘記保存 Room For 1 Scene。

    2.游戲管理器預制件

    在所有情況下,用戶界面的最低要求是能夠退出房間。為此,我們需要一個 UI 按鈕,但我們還需要一個腳本來調用 Photon 讓本地玩家離開房間,所以讓我們從創(chuàng)建我們稱之為游戲管理器預制件開始,第一個它將處理退出本地玩家當前所在房間的任務。

  • ?創(chuàng)建一個新的 c# 腳本 GameManager
  • 在場景中創(chuàng)建一個空的 GameObject,將其命名為 Game Manager
  • 將 GameManager 腳本放到 GameObject 游戲管理器上
  • 通過將游戲管理器從場景層次結構拖到資產(chǎn)瀏覽器,將游戲管理器變成預制件,它將在層次結構中變?yōu)樗{色。
  • 編輯 GameManager 腳本
  • 替換為以下內容:
  • 保存場景
  • using System; using System.Collections;using UnityEngine; using UnityEngine.SceneManagement;using Photon.Pun; using Photon.Realtime;namespace Com.MyCompany.MyGame {public class GameManager : MonoBehaviourPunCallbacks{#region Photon Callbacks/// <summary>/// Called when the local player left the room. We need to load the launcher scene./// </summary>public override void OnLeftRoom(){SceneManager.LoadScene(0);}#endregion#region Public Methodspublic void LeaveRoom(){PhotonNetwork.LeaveRoom();}#endregion} }

    因此,我們創(chuàng)建了一個公共方法 LeaveRoom()。它所做的是明確讓本地玩家離開 Photon Network 房間。我們將其包裝在我們自己的公共抽象方法周圍。我們可能希望在稍后階段實現(xiàn)更多功能,例如保存數(shù)據(jù),或插入用戶將離開游戲的確認步驟等。
    根據(jù)我們的游戲要求,如果我們不在房間里,我們需要顯示 Launcher 場景,所以我們將監(jiān)聽 OnLeftRoom() Photon Callback 并加載大廳場景 Launcher,它在 Build settings 場景的列表中索引為 0 ,我們將在此頁面的“構建設置場景列表”部分中進行設置。
    但是為什么我們要用這個做一個預制件呢?因為我們的游戲需求意味著同一個游戲有多個場景,所以我們需要重用這個游戲管理器。在 Unity 中,重用游戲對象的最佳方式是將它們變成預制件。
    接下來,讓我們創(chuàng)建將調用 GameManager 的 LeaveRoom() 方法的 UI 按鈕。

    3.退出房間按鈕預制體

    同樣,就像游戲管理器一樣,從我們將有許多不同場景需要此功能的角度來看,提前計劃并將按鈕制作成預制件是有意義的,這樣我們就可以重用它并僅在一個地方修改它我們需要在未來。

  • 確保您在 Scene Room 中 1
  • 使用 Unity 菜單“GameObject/UI/Panel”創(chuàng)建一個 UI 面板,將該面板命名為 Top Panel。
  • 移除 Image 和 Canvas Renderer 組件以清除此面板。如果您覺得它更好,請保留它,這很美觀。
  • 將“錨定預設”設置為頂部并將錨定預設設置為在按住 Shift 和 Alt 的同時拉伸。 RectTransform Anchors 需要一些經(jīng)驗才能習慣,但這是值得的。
  • 將 RectTransform 高度設置為 50。
  • 右鍵單擊 Panel GameObject Top Panel 并添加一個 UI/Button,將其命名為 Leave Button
  • 選擇 Leave Button 的 Text Child,并將其文本設置為 Leave Game。
  • 將 OnClick 按鈕的事件連接到層次結構中的游戲管理器實例以調用 LeaveRoom()。
  • 將“離開按鈕”從場景層次結構拖到資源瀏覽器中,將其變成預制件,它將在層次結構中變?yōu)樗{色。
  • 保存場景,保存Project
  • 4.其他房間創(chuàng)建

    現(xiàn)在我們已經(jīng)正確地完成了一個房間,讓我們將其復制 3 次,并適當?shù)孛鼈?#xff08;當您復制它們時,它們應該已經(jīng)由 Unity 命名):

    • Room for 2
    • Room for 3
    • Room for 4

    在下面找到位置、旋轉和比例的變化以加速這個重復過程。(略)

    5.構建Build Settings列表

    對于項目在編輯和發(fā)布時的良好運行至關重要,我們需要在構建設置中添加所有這些場景,以便 Unity 在構建應用程序時包含它們。

    (1)通過菜單“File/Build Settings”打開構建設置
    (2)拖放所有場景,Launcher場景必須保持在第一個,因為默認情況下 Unity 將加載并向玩家顯示該列表中的第一個場景

    ?現(xiàn)在我們已經(jīng)完成了基本的場景設置,我們終于可以開始連接所有東西了。讓我們在下一節(jié)中執(zhí)行此操作。

    五、Game Manager & Levels

    本節(jié)介紹了根據(jù)當前在房間中玩游戲的玩家數(shù)量來處理各種關卡加載的功能。

    1.加載競技場例程

    我們創(chuàng)建了 4 個不同的房間,并且按照約定最后一個字符是玩家人數(shù)來命名它們,因此現(xiàn)在可以很容易地綁定房間中當前的玩家人數(shù)和相關場景。這是一種非常有效的技術,稱為“約定優(yōu)于配置”。例如,基于“配置”的方法會為房間中給定數(shù)量的玩家維護場景名稱的查找表列表。然后我們的腳本會查看該列表并返回一個名稱根本無關緊要的場景。 “配置”通常需要更多的腳本,這就是為什么我們會在這里選擇“約定”,它可以讓我們更快地工作代碼,而不會用不相關的功能污染我們的代碼。

    (1)打開 GameManager 腳本
    (2)讓我們在專用于我們將為該場合創(chuàng)建的私有方法的新區(qū)域中添加一個新方法。不要忘記保存 GameManager 腳本。?

    #region Private Methodsvoid LoadArena() {if (!PhotonNetwork.IsMasterClient){Debug.LogError("PhotonNetwork : Trying to Load a level but we are not the master Client");return;}Debug.LogFormat("PhotonNetwork : Loading Level : {0}", PhotonNetwork.CurrentRoom.PlayerCount);PhotonNetwork.LoadLevel("Room for " + PhotonNetwork.CurrentRoom.PlayerCount); }#endregion

    當我們調用此方法時,我們將根據(jù)我們所在房間的 PlayerCount 屬性加載適當?shù)姆块g。

    ?這里有兩點需要注意,非常重要:

    (1)PhotonNetwork.LoadLevel() 只應在我們是 MasterClient 時調用。所以我們首先使用 PhotonNetwork.IsMasterClient 檢查我們是 MasterClient。調用者也有責任檢查這一點,我們將在本節(jié)的下一部分中介紹。

    (2)我們使用 PhotonNetwork.LoadLevel() 來加載我們想要的級別,我們不直接使用 Unity,因為我們希望依靠 Photon 在房間中所有連接的客戶端上加載這個級別,因為我們已經(jīng)啟用了 PhotonNetwork.AutomaticallySyncScene這個游戲。我們使用 PhotonNetwork.LoadLevel() 來加載我們想要的級別,我們不直接使用 Unity,因為我們希望依靠 Photon 在房間中所有連接的客戶端上加載這個級別,因為我們已經(jīng)啟用了 PhotonNetwork.AutomaticallySyncScene這個游戲。

    現(xiàn)在我們有了加載正確關卡的函數(shù),讓我們將其與連接和斷開連接的玩家綁定。

    2.觀看玩家連接

    我們已經(jīng)在教程的前一部分研究了獲取 Photon Callbacks 的各種方法,現(xiàn)在 GameManager 需要監(jiān)聽玩家的連接和斷開連接。讓我們來實現(xiàn)它。

    (1)打開 GameManager 腳本
    (2)添加以下 Photon 回調并保存 GameManager 腳本

    #region Photon Callbackspublic override void OnPlayerEnteredRoom(Player other) {Debug.LogFormat("OnPlayerEnteredRoom() {0}", other.NickName); // not seen if you're the player connectingif (PhotonNetwork.IsMasterClient){Debug.LogFormat("OnPlayerEnteredRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoomLoadArena();} }public override void OnPlayerLeftRoom(Player other) {Debug.LogFormat("OnPlayerLeftRoom() {0}", other.NickName); // seen when other disconnectsif (PhotonNetwork.IsMasterClient){Debug.LogFormat("OnPlayerLeftRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoomLoadArena();} }#endregion

    現(xiàn)在,我們有一個完整的設置。每次玩家加入或離開房間時,我們都會收到通知,我們會調用之前實現(xiàn)的 LoadArena() 方法。但是,只有當我們是使用 PhotonNetwork.IsMasterClient 的 MasterClient 時,我們才會調用 LoadArena()。
    現(xiàn)在讓我們回到大廳,最終能夠在加入房間時加載正確的場景。

    3.從大廳loading Arena

    (1)編輯腳本Launcher。
    (2)將以下內容附加到 OnJoinedRoom() 方法

    // #Critical: We only load if we are the first player, else we rely on `PhotonNetwork.AutomaticallySyncScene` to sync our instance scene. if (PhotonNetwork.CurrentRoom.PlayerCount == 1) {Debug.Log("We load the 'Room for 1' ");// #Critical// Load the Room Level.PhotonNetwork.LoadLevel("Room for 1"); }

    讓我們測試一下,打開場景Launcher,然后運行它。單擊“播放”,讓系統(tǒng)連接并加入房間。就是這樣,現(xiàn)在我們的大廳開始工作了。但是如果你離開房間,你會注意到當回到大廳時,它會自動重新加入……哎呀,讓我們來解決這個問題。
    如果您還不知道為什么會發(fā)生這種情況,請“簡單地”分析日志。我只是簡單地引用一下,因為需要實踐和經(jīng)驗才能獲得自動性來概述問題并知道在哪里查看以及如何調試它。

    現(xiàn)在嘗試一下,如果您仍然無法找到問題的根源,讓我們一起來解決這個問題。

  • 運行Launcher場景
  • 點擊“播放”按鈕,等待您加入房間并加載“Room for 1”
  • 清除 Unity 控制臺
  • 點擊“離開房間”
  • 研究 Unity 控制臺,注意記錄“PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN”
  • 停止Launcher場景
  • 雙擊日志條目“PUN Basics Tutoria/Launcher:OnConnectedToMaster() was called by PUN”腳本將被加載并指向調試調用行。
  • 嗯……所以,每次我們被告知我們已連接時,我們都會自動加入一個隨機房間,這不是我們想要的。
  • 要解決這個問題,我們需要了解上下文。當用戶點擊“播放”按鈕時,我們應該舉起一個標志來知道連接過程是由用戶發(fā)起的。然后我們可以檢查此標志以在各種光子回調中相應地采取行動。

    (1)編輯腳本啟動器
    (2)在 Private Fields 區(qū)域內創(chuàng)建新屬性

    /// <summary> /// Keep track of the current process. Since connection is asynchronous and is based on several callbacks from Photon, /// we need to keep track of this to properly adjust the behavior when we receive call back by Photon. /// Typically this is used for the OnConnectedToMaster() callback. /// </summary> bool isConnecting;

    (3)在 Connect() 方法內部將 isConnecting 設置為 PhotonNetwork.ConnectUsingSettings() 方法的返回值,如下所示:

    // keep track of the will to join a room, because when we come back from the game we will get a callback that we are connected, so we need to know what to do then isConnecting = PhotonNetwork.ConnectUsingSettings();

    結果:

    public void Connect() {progressLabel.SetActive(true);controlPanel.SetActive(false);if (PhotonNetwork.IsConnected){PhotonNetwork.JoinRandomRoom();}else{isConnecting = PhotonNetwork.ConnectUsingSettings();PhotonNetwork.GameVersion = gameVersion;} }

    (4)在 OnConnectedToMaster() 方法中,用 if 語句包圍 PhotonNetwork.JoinRandomRoom() 如下:

    // we don't want to do anything if we are not attempting to join a room. // this case where isConnecting is false is typically when you lost or quit the game, when this level is loaded, OnConnectedToMaster will be called, in that case // we don't want to do anything. if (isConnecting) {// #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed()PhotonNetwork.JoinRandomRoom();isConnecting = false; }

    (5)在 OnDisconnected 方法中,將 isConnecting 設置為 false

    現(xiàn)在,如果我們再次測試并運行 Launcher Scene,并在 Lobby 和 Game 之間來回切換,一切都很好 :) 為了測試場景的自動同步,您需要發(fā)布應用程序(為桌面發(fā)布,它是運行測試最快的),并與 Unity 一起運行,因此您實際上有兩個玩家將連接并加入一個房間。如果 Unity 編輯器首先創(chuàng)建房間,它將是 MasterClient,您將能夠在 Unity 控制臺中驗證您在連接時獲得“PhotonNetwork:加載級別:1”和后來的“PhotonNetwork:加載級別:2”與已發(fā)布的實例。
    好的!我們已經(jīng)介紹了很多,但這只是工作的一半……:) 我們需要自己解決玩家問題,所以讓我們在下一節(jié)中解決這個問題。不要忘記不時離開計算機休息一下,以便更有效地吸收所解釋的各種概念。

    六、構建玩家

    本節(jié)將指導您從頭開始創(chuàng)建將在本教程中使用的Player預制件,因此我們將涵蓋創(chuàng)建過程的每個步驟。
    嘗試創(chuàng)建一個可以在沒有連接 PUN 的情況下工作的Player預制件始終是一個好方法,這樣可以輕松快速測試、調試并確保一切至少在沒有任何網(wǎng)絡功能的情況下工作。然后,您可以緩慢而穩(wěn)妥地構建和修改每個功能,使其成為網(wǎng)絡兼容的角色。通常,用戶輸入只能在玩家擁有的實例上激活,而不應在其他玩家的計算機上激活。我們將在下面詳細介紹。

    1.預制基礎知識

    要了解 PUN 的第一個也是重要的規(guī)則是,應該通過網(wǎng)絡實例化的預制件必須位于 Resources 文件夾中。

    在 Resources 文件夾中包含預制件的第二個重要副作用是您需要注意它們的名稱。您的資產(chǎn)資源路徑下不應有兩個名稱相同的預制件,因為 Unity 會選擇它找到的第一個。因此,請始終確保在您的項目資源中,資源文件夾路徑中沒有兩個同名的預制件。我們很快就會談到這一點。

    我們將使用 Unity 作為免費資產(chǎn)提供的 Kyle 機器人。它以 Fbx 文件的形式出現(xiàn),該文件是使用 3ds Max、Maya、cinema 4D 等 3d 軟件創(chuàng)建的。使用這些軟件創(chuàng)建網(wǎng)格和動畫超出了本教程的范圍。這個機器人 Kyle.fbx 位于“\Assets\Photon\PhotonUnityNetworking\Demos\Shared Assets\Models”。

    這是開始為您的Player使用“Kyle Robot.fbx”的一種方法:

    (1)在你的“項目瀏覽器”中,在某處創(chuàng)建一個名為“Resources”的文件夾,通常建議你組織你的內容,所以可以有類似“PunBasics_tutorial\Resources”的東西

    (2)創(chuàng)建一個新的空場景,并將其保存為“PunBasics_tutorial\Scenes”中的 Kyle Test。 “Kyle Test”場景的目的僅僅是創(chuàng)建預制件并進行設置。一旦完成,您就可以擺脫現(xiàn)場。

    (3)將 Robot Kyle 拖放到“Scene Hierarchy”上。

    (4)將剛剛在層次結構中創(chuàng)建的 GameObject 重命名為 My Robot Kyle

    (5)將我的機器人 Kyle 拖放到“PunBasics_tutorial\Resources”

    (6)我們現(xiàn)在已經(jīng)創(chuàng)建了一個基于 Kyle Robot Fbx 資產(chǎn)的預制件,并且我們在場景 Kyle Test 的層次結構中有一個它的實例。現(xiàn)在我們可以開始使用它了。

    2.CharacterController

    (1)這個Component是Unity提供的一個非常方便的Standard Asset,可以讓我們使用Animator更快地制作出典型的角色,所以讓我們利用這些Unity的強大功能吧。

    (2)雙擊 My Kyle Robot 放大場景視圖。注意以腳為中心的“Capsule Collider”;我們實際上需要“Capsule Collider”來正確匹配角色。

    (3)在 CharacterController 組件中,將 Center.y 屬性更改為 1(其 Height 屬性的一半)。

    ?(4)點擊“Apply”以影響我們所做的更改。這是非常重要的一步,因為我們已經(jīng)編輯了預制件 My Kyle Robot 的一個實例,但我們希望這些更改適用于每個實例,而不僅僅是這個實例,所以我們點擊“Apply”。

    3.分配Animator Controller

    Kyle Robot Fbx 資產(chǎn)需要由 Animator Graph 控制。我們不會在本教程中介紹此圖的創(chuàng)建,因此我們?yōu)榇颂峁┝艘粋€控制器,位于 \Assets\Photon\PhotonUnityNetworking/Demos/PunBasics-Tutorial/Animator/ 下的項目資產(chǎn)中,名為 Kyle Robot

    ?要將此 Kyle 機器人控制器分配給我們的預制件,只需將 Animator 組件的屬性 Controller 設置為指向 Kyle Robot

    ?不要忘記,如果您在 My Kyle Robot 的實例上執(zhí)行此操作,則需要點擊預制件本身的“Apply”以合并這些更改。

    4.使用控制器參數(shù)

    理解動畫控制器的關鍵特征是動畫參數(shù)。我們正在使用這些,通過腳本控制我們的動畫。在我們的例子中,我們有 Speed、Direction、Jump、Hi 等參數(shù)。

    Animator 組件的一大特色是能夠根據(jù)動畫實際移動角色。此功能稱為 Root Motion,Animator Component 上有一個 Apply Root Motion 屬性,默認情況下為 true,所以我們可以開始了。
    因此,實際上,要讓角色行走,我們只需將速度動畫參數(shù)設置為正值,它就會開始行走并向前移動。我們開工吧!

    5.Animator 管理腳本

    讓我們創(chuàng)建一個新腳本,我們將在其中根據(jù)用戶的輸入控制角色。

  • 創(chuàng)建一個名為 PlayerAnimatorManager 的新 C# 腳本。
  • 將此腳本附加到 Prefab My Robot Kyle。
  • 用您的命名空間 Com.MyCompany.MyGame 包圍類,如下所示。
  • 為了清晰起見,用區(qū)域 MonoBehaviour CallBacks 包圍 Start() 和 Update()。
  • using UnityEngine; using System.Collections;namespace Com.MyCompany.MyGame {public class PlayerAnimatorManager : MonoBehaviour{#region MonoBehaviour Callbacks// Use this for initializationvoid Start(){}// Update is called once per framevoid Update(){}#endregion} }

    ?6.動畫管理:速度控制

    我們需要編碼的第一件事是獲取 Animator 組件,以便我們可以控制它。

    (1)確保您正在編輯腳本 PlayerAnimatorManager

    (2)創(chuàng)建類型為 Animator 的私有變量動畫器

    (3)將 Animator 組件存儲在 Start() 方法中的這個變量中

    private Animator animator; // Use this for initialization void Start() {animator = GetComponent<Animator>();if (!animator){Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);} }

    請注意,由于我們需要一個 Animator 組件,如果我們沒有得到一個,我們會記錄一個錯誤,這樣它就不會被忽視并立即被開發(fā)人員解決。您應該始終編寫代碼,就好像它會被其他人使用 :) 這很乏味,但從長遠來看是值得的。

    (4)現(xiàn)在讓我們聽取用戶輸入并控制速度動畫參數(shù)。然后保存腳本PlayerAnimatorManager。

    // Update is called once per frame void Update() {if (!animator){return;}float h = Input.GetAxis("Horizontal");float v = Input.GetAxis("Vertical");if (v < 0){v = 0;}animator.SetFloat("Speed", h * h + v * v); }

    讓我們研究一下這個腳本在做什么:
    由于我們的游戲不允許后退,因此我們確保 v 小于 0。如果用戶按下“向下箭頭”或“s”鍵(垂直軸的默認設置),我們不允許這樣做并強制值為 0。

    您還會注意到我們對兩個輸入進行了平方。為什么?所以它總是一個正的絕對值以及添加一些緩和。不錯的技巧就在這里。您也可以使用 Mathf.Abs() ,那會很好。
    我們還添加了兩個輸入來控制速度,這樣當只按下左或右輸入時,我們仍然會在轉彎時獲得一些速度。
    當然,所有這些都非常針對我們的角色設計,根據(jù)您的游戲邏輯,您可能希望角色原地轉彎,或者能夠向后退。動畫參數(shù)的控制總是非常特定于游戲。

    7. 測試,測試,1 2 3 ...

    讓我們驗證一下我們到目前為止所做的。確保您已打開 Kyle Test 場景。目前,在這個場景中,我們只有一個攝像頭和 Kyle 機器人實例,場景中缺少機器人站立的地面,如果你現(xiàn)在跑到場景中,Kyle 機器人就會倒下。此外,我們不會關心場景中的燈光或任何花哨的東西,我們想測試和驗證我們的角色和腳本是否正常工作。

  • ?在場景中添加一個“Cube”。因為立方體默認有一個“Box Collider”,所以我們最好將它用作地板。
  • 將它定位在 0,-0.5,0 因為立方體的高度是 1。我們希望Cube的頂面是地板。
  • 將立方體縮放到 30、1、30,以便我們有空間進行實驗
  • 選擇相機并將其移開以獲得良好的概覽。一個不錯的技巧是在“SceneView”中獲得您喜歡的視圖,選擇相機并轉到菜單“GameObject/Align With View”,相機將匹配場景視圖。
  • 最后一步,將 My Robot Kyle 向上移動 0.1,否則在開始時會錯過碰撞并且角色會穿過地板,因此始終在碰撞器之間留出一些物理空間以便模擬創(chuàng)建接觸。
  • 播放場景,然后按“向上箭頭”或“a”鍵,角色正在行走!您可以使用所有鍵進行測試以進行驗證。
  • 這很好,但我們還有很多工作要做,相機需要跟上,我們還不能轉向......

    如果您現(xiàn)在想在相機上工作,請轉到專用部分,此頁面的其余部分將完成 Animator 控件并實現(xiàn)旋轉。

    8.Animator Manager 腳本:方向控制

    控制旋轉會稍微復雜一些;我們不希望我們的角色在按下左右鍵時突然旋轉。我們想要柔和平滑的旋轉。幸運的是,可以使用一些阻尼來設置動畫參數(shù)

    (1)確保您正在編輯 Script PlayerAnimatorManager

    (2)在新區(qū)域“Private Fields”區(qū)域內創(chuàng)建一個公共浮動變量 directionDampTime。

    #region Private Fields[SerializeField] private float directionDampTime = 0.25f;#endregion

    ?(3)在 Update() 函數(shù)的末尾,添加

    animator.SetFloat("Direction", h, directionDampTime, Time.deltaTime);

    所以我們馬上注意到 animator.SetFloat() 有不同的簽名。我們用來控制 Speed 的參數(shù)很簡單,但是這個參數(shù)需要兩個參數(shù),一個是阻尼時間,一個是 deltaTime。阻尼時間是有道理的:它需要多長時間才能達到所需的值,但是 deltaTime?。它本質上允許您編寫與幀速率無關的代碼,因為 Update() 取決于幀速率,我們需要使用 deltaTime 來解決這個問題。盡可能多地閱讀有關該主題的內容以及在網(wǎng)絡上搜索此內容時您會找到的內容。理解這個概念后,您將能夠充分利用 Unity 的許多功能,包括動畫和隨時間推移對值的一致控制。

    ?(4)播放您的場景,并使用所有箭頭查看您的角色行走和轉身的情況

    (5)測試directionDampTime的效果:比如1,然后5,看看達到最大轉彎能力需要多長時間。您會看到轉彎半徑隨著 directionDampTime 的增加而增加。

    9.Animator Manager腳本:跳躍

    對于跳躍,由于兩個因素,我們需要做更多的工作。第一,我們不希望玩家在不跑的情況下跳躍,第二,我們不希望跳躍是循環(huán)的。

    確保您正在編輯 Script PlayerAnimatorManager
    在我們在 Update() 方法中捕獲用戶輸入之前插入它

    // deal with Jumping AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); // only allow jumping if we are running. if (stateInfo.IsName("Base Layer.Run")) {// When using trigger parameterif (Input.GetButtonDown("Fire2")){animator.SetTrigger("Jump");} }

    測試。開始奔跑并按下“alt”鍵或鼠標右鍵,Kyle?就會跳起來。

    好的,那么首先要了解我們如何知道Animator是否正在運行。我們使用 stateInfo.IsName("Base Layer.Run") 執(zhí)行此操作。我們只是詢問 Animator 的當前活動狀態(tài)是否為運行。我們必須附加 Base Layer,因為 Run 狀態(tài)在 Base Layer 中。

    如果我們處于 Run 狀態(tài),那么我們將監(jiān)聽 Fire2 Input 并在必要時觸發(fā) Jump 觸發(fā)器。
    所以,這是到目前為止完整的 PlayerAnimatorManager 腳本:

    using UnityEngine; using System.Collections;namespace Com.MyCompany.MyGame {public class PlayerAnimatorManager : MonoBehaviour{#region Private Fields[SerializeField]private float directionDampTime = .25f;private Animator animator;#endregion#region MonoBehaviour CallBacks// Use this for initializationvoid Start(){animator = GetComponent<Animator>();if (!animator){Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);}}// Update is called once per framevoid Update(){if (!animator){return;}// deal with JumpingAnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);// only allow jumping if we are running.if (stateInfo.IsName("Base Layer.Run")){// When using trigger parameterif (Input.GetButtonDown("Fire2")){animator.SetTrigger("Jump");}}float h = Input.GetAxis("Horizontal");float v = Input.GetAxis("Vertical");if (v < 0){v = 0;}animator.SetFloat("Speed", h * h + v * v);animator.SetFloat("Direction", h, directionDampTime, Time.deltaTime);}#endregion} }

    當您考慮它在場景中實現(xiàn)的目標時,對于幾行代碼來說還不錯。現(xiàn)在讓我們處理相機工作,因為我們能夠在我們的世界中進化,我們需要適當?shù)南鄼C行為來跟進。

    ?10.相機設置

    在本節(jié)中,我們將使用 CameraWork 腳本。如果您想從頭開始編寫 CameraWork,請轉到下一部分,完成后返回此處。

  • 將組件 CameraWork 添加到 My Kyle Robot 預制件
  • 啟用屬性 Follow on Start,這實際上使相機立即跟隨角色。當我們開始網(wǎng)絡實施時,我們將關閉它。
  • 將屬性 Center Offset 設置為 0,4,0,這會使相機看起來更高,從而提供比相機直視玩家更好的環(huán)境視角,我們會白白看到太多地面。
  • 播放場景 Kyle Test,并四處移動角色以驗證相機是否正確跟隨角色。
  • 11.PhotonView 組件

    我們需要將一個 PhotonView 組件附加到我們的預制件上。 PhotonView 將每臺計算機上的各種實例連接在一起,并定義要觀察的組件以及如何觀察這些組件。

  • 將 PhotonView 組件添加到我的機器人 Kyle
  • 將 Observe 選項設置為 Unreliable On Change
  • 注意 PhotonView 警告您需要觀察一些東西才能產(chǎn)生任何效果。您現(xiàn)在可以忽略它,因為這些觀察到的組件將在本教程的后面設置。?
  • 12.Beams設置

    我們的機器人角色仍然缺少他的武器,讓我們創(chuàng)建一些從他的眼睛中射出的激光束。

    13.添加Beams模型

    為了簡單起見,我們將使用簡單的立方體并將它們縮放到非常細和長。有一些技巧可以快速完成此操作:不要直接將立方體添加為頭部的子項,而是創(chuàng)建它移動它并自行放大,然后將其附加到頭部,這將防止猜測正確的旋轉值讓你的光束與眼睛對齊。
    另一個重要的技巧是對兩個光束只使用一個對撞機。這是為了讓物理引擎更好地工作,薄對撞機從來都不是一個好主意,它不可靠,所以我們將制作一個大盒子對撞機,以便我們確保可靠地擊中目標。

    (1)打開 Kyle 測試場景

    (2)在場景中添加一個立方體,將其命名為 Beam Left

    (3)修改它看起來像一根長光束,并正確定位在左眼上

    (4)在層次結構中選擇 My Kyle Robot 實例

    (5)找到Head child

    ?(6)添加一個空游戲對象作為“Head”游戲對象的子對象,將其命名為“Beams” 7. 將“Beam Left”拖放到“Beams”內 8.復制“Beams Left”,將其命名為“Beams Right” 9.將其定位使其與右眼對齊 10. 從 `Beams Right` 中移除 Box Collider 組件 11. 調整 `Beams Left` 的“Box Collider”中心和大小以封裝兩個光束 12. 轉動 `Beams Left` 的“Box Collider” `IsTrigger` 屬性設置為 `True`,我們只想知道光束接觸玩家,而不是碰撞。 13. 創(chuàng)建一個新材料,將其命名為“Red Beam” 14. 將“Red Beam”材料分配給兩個梁 15. 將更改應用回預制件
    注意:激光束應該在角色的碰撞器之外,以免傷到自己。
    你現(xiàn)在應該有這樣的東西:

    ?

    ?14.使用用戶輸入控制光束

    好的,現(xiàn)在我們有了光束,讓我們插入 Fire1 輸入來觸發(fā)它們。
    讓我們創(chuàng)建一個新的 C# 腳本,稱為 PlayerManager。下面是讓光束工作的第一個版本的完整代碼。

    using UnityEngine; using UnityEngine.EventSystems;using Photon.Pun;using System.Collections;namespace Com.MyCompany.MyGame {/// <summary>/// Player manager./// Handles fire Input and Beams./// </summary>public class PlayerManager : MonoBehaviourPunCallbacks{#region Private Fields[Tooltip("The Beams GameObject to control")][SerializeField]private GameObject beams;//True, when the user is firingbool IsFiring;#endregion#region MonoBehaviour CallBacks/// <summary>/// MonoBehaviour method called on GameObject by Unity during early initialization phase./// </summary>void Awake(){if (beams == null){Debug.LogError("<Color=Red><a>Missing</a></Color> Beams Reference.", this);}else{beams.SetActive(false);}}/// <summary>/// MonoBehaviour method called on GameObject by Unity on every frame./// </summary>void Update(){ProcessInputs();// trigger Beams active stateif (beams != null && IsFiring != beams.activeInHierarchy){beams.SetActive(IsFiring);}}#endregion#region Custom/// <summary>/// Processes the inputs. Maintain a flag representing when the user is pressing Fire./// </summary>void ProcessInputs(){if (Input.GetButtonDown("Fire1")){if (!IsFiring){IsFiring = true;}}if (Input.GetButtonUp("Fire1")){if (IsFiring){IsFiring = false;}}}#endregion} }

    此腳本在此階段的要點是激活或停用光束。激活后,光束將在與其他模型發(fā)生碰撞時有效觸發(fā)。我們稍后會抓住這些觸發(fā)器來影響每個角色的健康。
    我們還公開了一個公共屬性 Beams,它可以讓我們在 My Kyle Robot Prefab 的層次結構中引用確切的游戲對象。讓我們看看我們需要如何工作才能連接梁,因為這在預制件中很棘手,因為在資產(chǎn)瀏覽器中,預制件只公開第一個孩子,而不是子孩子,而且我們的梁確實埋在預制件層次結構中,所以我們需要從場景中的一個實例中做到這一點,然后將其應用回預制件本身。

  • ?打開 Kyle 測試場景
  • 在場景層次結構中選擇 My Kyle Robot
  • 將 PlayerManager 組件添加到我的 Kyle 機器人
  • 將 My Kyle Robot/Root/Ribs/Neck/Head/Beams 拖放到檢查器中的 PlayerManager Beams 屬性中
  • 將實例中的更改應用回預制件
  • 如果你點擊播放,然后按下 Fire1 輸入(默認情況下是鼠標左鍵或左 ctrl 鍵),光束將出現(xiàn),并在你松開時立即隱藏。

    15.健康值設置

    讓我們實現(xiàn)一個非常簡單的健康系統(tǒng),當光束擊中玩家時,它會減少。由于它不是子彈,而是源源不斷的能量流,我們需要以兩種方式考慮健康損害,當我們被光束擊中時,以及在光束擊中我們的整個過程中。

    (1)打開 PlayerManager 腳本
    (2)在 Public Fields 區(qū)域內添加Public Fields屬性

    [Tooltip("The current Health of our player")] public float Health = 1f;

    (3)將以下兩個方法添加到 MonoBehaviour 回調區(qū)域。然后保存 PlayerManager 腳本。

    /// <summary> /// MonoBehaviour method called when the Collider 'other' enters the trigger. /// Affect Health of the Player if the collider is a beam /// Note: when jumping and firing at the same, you'll find that the player's own beam intersects with itself /// One could move the collider further away to prevent this or check if the beam belongs to the player. /// </summary> void OnTriggerEnter(Collider other) {if (!photonView.IsMine){return;}// We are only interested in Beamers// we should be using tags but for the sake of distribution, let's simply check by name.if (!other.name.Contains("Beam")){return;}Health -= 0.1f; } /// <summary> /// MonoBehaviour method called once per frame for every Collider 'other' that is touching the trigger. /// We're going to affect health while the beams are touching the player /// </summary> /// <param name="other">Other.</param> void OnTriggerStay(Collider other) {// we dont' do anything if we are not the local player.if (!photonView.IsMine){return;}// We are only interested in Beamers// we should be using tags but for the sake of distribution, let's simply check by name.if (!other.name.Contains("Beam")){return;}// we slowly affect health when beam is constantly hitting us, so player has to move to prevent death.Health -= 0.1f * Time.deltaTime; }

    PlayerManager 擴展了 MonoBehaviourPunCallbacks。 MonoBehaviourPunCallbacks 擴展了 MonoBehaviourPun。 MonoBehaviourPun 有一個帶有“延遲初始化”的 photonView 屬性。這就是 photonView 最終出現(xiàn)在 PlayerManager 中的方式。

    首先,這兩種方法幾乎相同,唯一的區(qū)別是我們在 TriggerStay 期間使用 Time.deltaTime 減少健康,減少的速度不依賴于幀率。這是一個通常適用于動畫的重要概念,但在這里,我們也需要這個,我們希望所有設備的健康狀況以可預測的方式下降,如果在更快的計算機上,你的健康狀況下降得更快是不公平的:) Time.deltaTime 是為了保證一致性。如果您有任何問題,請回復我們并通過搜索 Unity 社區(qū)了解 Time.deltaTime,直到您完全理解這個概念,這是必不可少的。
    第二個重要方面,現(xiàn)在應該明白的是,我們只影響本地玩家的健康,這就是為什么如果 PhotonView 不是我的,我們會提前退出該方法。
    最后,如果擊中我們的物體是光束,我們只想影響健康。
    為了便于調試,我們將 Health 浮點數(shù)設置為公共浮點數(shù),以便在等待構建 UI 時輕松檢查其值。
    好的,看起來一切都完成了嗎?嗯...如果不考慮玩家的游戲結束狀態(tài),健康系統(tǒng)是不完整的,當健康達到 0 時發(fā)生,讓我們現(xiàn)在開始吧。

    16.健康值檢查游戲結束

    為簡單起見,當玩家的生命值達到 0 時,我們只需離開房間,如果您還記得的話,我們已經(jīng)在 GameManager 腳本中創(chuàng)建了一個離開房間的方法。如果我們可以重用此方法而不是對相同的功能進行兩次編碼,那就太好了。您應該不惜一切代價避免針對相同結果的重復代碼。這也是介紹一個非常方便的編程概念“Singleton”的好時機。雖然這個主題本身可以填滿幾個教程,但我們只會做“單例”的最小實現(xiàn)。了解單例、它們在 Unity 上下文中的變體以及它們如何幫助創(chuàng)建強大的功能非常重要,并且會為您省去很多麻煩。因此,請不要猶豫,抽出本教程的時間來了解更多信息。

    (1)打開 GameManager 腳本
    (2)在 Public Fields 區(qū)域添加這個變量

    public static GameManager Instance;

    (3)添加 Start() 方法,如下所示

    void Start() {Instance = this; }

    請注意,我們用 [static] 關鍵字修飾了 Instance 變量,這意味著該變量無需保存指向 GameManager 實例的指針即可使用,因此您可以在代碼的任何位置簡單地執(zhí)行 GameManager.Instance.xxx() .真的很實用!讓我們看看它如何適合我們的邏輯管理游戲

    (1)打開 PlayerManager 腳本
    (2)在 Update() 方法中,在我們檢查 photonView.IsMine 的 if 語句中,添加它并保存 PlayerManager 腳本

    if (photonView.IsMine) {ProcessInputs();if (Health <= 0f){GameManager.Instance.LeaveRoom();} }

    注意:我們考慮到健康可能是負值,因為激光束造成的損害強度不同。
    注意:我們到達 GameManager 實例的 LeaveRoom() 公共方法而無需實際獲取 Component 或任何東西,我們只依賴于假設 GameManager 組件位于當前場景中的 GameObject 上這一事實。

    ?好的,現(xiàn)在我們進入網(wǎng)絡!

    七、構建玩家相機

    本節(jié)將指導您創(chuàng)建 CameraWork 腳本,以便在您玩此游戲時跟隨您的玩家。
    本節(jié)與網(wǎng)絡無關,因此將保持簡短。

    1.創(chuàng)建 CameraWork 腳本

    (1)創(chuàng)建一個名為 CameraWork 的新 C# 腳本
    (2)將 CameraWork 的內容替換為以下內容:

    // -------------------------------------------------------------------------------------------------------------------- // <copyright file="CameraWork.cs" company="Exit Games GmbH"> // Part of: Photon Unity Networking Demos // </copyright> // <summary> // Used in PUN Basics Tutorial to deal with the Camera work to follow the player // </summary> // <author>developer@exitgames.com</author> // --------------------------------------------------------------------------------------------------------------------using UnityEngine;namespace Photon.Pun.Demo.PunBasics {/// <summary>/// Camera work. Follow a target/// </summary>public class CameraWork : MonoBehaviour{#region Private Fields[Tooltip("The distance in the local x-z plane to the target")][SerializeField]private float distance = 7.0f;[Tooltip("The height we want the camera to be above the target")][SerializeField]private float height = 3.0f;[Tooltip("Allow the camera to be offseted vertically from the target, for example giving more view of the sceneray and less ground.")][SerializeField]private Vector3 centerOffset = Vector3.zero;[Tooltip("Set this as false if a component of a prefab being instanciated by Photon Network, and manually call OnStartFollowing() when and if needed.")][SerializeField]private bool followOnStart = false;[Tooltip("The Smoothing for the camera to follow the target")][SerializeField]private float smoothSpeed = 0.125f;// cached transform of the targetTransform cameraTransform;// maintain a flag internally to reconnect if target is lost or camera is switchedbool isFollowing;// Cache for camera offsetVector3 cameraOffset = Vector3.zero;#endregion#region MonoBehaviour Callbacks/// <summary>/// MonoBehaviour method called on GameObject by Unity during initialization phase/// </summary>void Start(){// Start following the target if wanted.if (followOnStart){OnStartFollowing();}}void LateUpdate(){// The transform target may not destroy on level load,// so we need to cover corner cases where the Main Camera is different everytime we load a new scene, and reconnect when that happensif (cameraTransform == null && isFollowing){OnStartFollowing();}// only follow is explicitly declaredif (isFollowing) {Follow ();}}#endregion#region Public Methods/// <summary>/// Raises the start following event./// Use this when you don't know at the time of editing what to follow, typically instances managed by the photon network./// </summary>public void OnStartFollowing(){cameraTransform = Camera.main.transform;isFollowing = true;// we don't smooth anything, we go straight to the right camera shotCut();}#endregion#region Private Methods/// <summary>/// Follow the target smoothly/// </summary>void Follow(){cameraOffset.z = -distance;cameraOffset.y = height;cameraTransform.position = Vector3.Lerp(cameraTransform.position, this.transform.position +this.transform.TransformVector(cameraOffset), smoothSpeed*Time.deltaTime);cameraTransform.LookAt(this.transform.position + centerOffset);}void Cut(){cameraOffset.z = -distance;cameraOffset.y = height;cameraTransform.position = this.transform.position + this.transform.TransformVector(cameraOffset);cameraTransform.LookAt(this.transform.position + centerOffset);}#endregion} }

    跟隨玩家背后的邏輯很簡單。我們使用距離計算所需的相機位置,并添加偏移量以落后于使用高度。然后它使用 Lerping 來平滑運動以趕上所需的位置,最后,一個簡單的 LookAt 讓相機始終指向玩家。
    除了 Camera 工作本身,還設置了一些重要的東西;控制行為何時應該主動跟隨玩家的能力。理解這一點很重要:我們什么時候想讓攝像機跟隨玩家?

    通常,讓我們想象一下如果它始終跟隨玩家會發(fā)生什么。當你連接到一個滿是玩家的房間時,其他玩家實例上的每個 CameraWork 腳本都會爭先恐后地控制“主攝像機”,以便它看著它的玩家......好吧,我們不想那樣,我們只想跟隨代表計算機后面的用戶的本地Player。
    一旦我們定義了我們只有一個相機但有多個玩家實例的問題,我們就可以輕松找到多種方法來解決這個問題。

    (1)僅在本地Player上附加 CameraWork 腳本。
    (2)通過關閉和打開 CameraWork 行為來控制它,具體取決于它必須跟隨的玩家是否是本地玩家。
    (3)將 CameraWork 連接到相機并注意場景中何時有本地Player實例并僅關注該Player實例。

    這 3 個選項并不詳盡,可以找到更多方法,但在這 3 個中,我們將任意選擇第二個。以上選項都沒有好壞之分,但這是可能需要最少編碼量并且最靈活的選項......“有趣......”我聽到你說:)

    (1)我們暴露了一個字段 followOnStart 如果我們想在非網(wǎng)絡環(huán)境中使用它,我們可以將其設置為 true,例如在我們的測試場景中,或者在完全不同的場景中

    (2)在基于網(wǎng)絡的游戲中運行時,當我們檢測到玩家是本地玩家時,我們將調用公共方法 OnStartFollowing()。這將在播放器預制網(wǎng)絡一章中創(chuàng)建和解釋的腳本 PlayerManager 中完成

    ?八、Player Networking

    本節(jié)將指導您修改“Player”預制件。我們首先創(chuàng)建了一個按原樣工作的Player,但現(xiàn)在我們將對其進行修改,以便在我們在 PUN 環(huán)境中使用它時它可以工作并符合要求。修改非常輕,但概念很關鍵。所以這個部分確實非常重要。

    1.Transform同步

    我們想要同步的明顯特征是角色的位置和旋轉,這樣當玩家四處移動時,角色在其他玩家的游戲實例中的行為方式相似。
    您可以在自己的 Script 中手動觀察 Transform 組件,但是由于網(wǎng)絡延遲和同步數(shù)據(jù)的有效性,您會遇到很多麻煩。幸運的是,為了簡化這項常見任務,我們將使用 PhotonTransformView 組件。基本上,此組件已為您完成所有艱苦的工作。

  • 將 PhotonTransformView 添加到“My Robot Kyle”預制件
  • 將 PhotonTransformView 從其標題標題拖到 PhotonView 組件上的第一個可觀察組件條目上
  • 現(xiàn)在,檢查 PhotonTransformView 中的Synchronize Position
  • 檢查Synchronize Rotation
  • 2.Animator同步

    PhotonAnimatorView 還使網(wǎng)絡設置變得輕而易舉,將為您節(jié)省大量時間和麻煩。它允許您定義要同步的圖層權重和參數(shù)。只有當層權重在游戲過程中發(fā)生變化時才需要同步,并且可能根本不同步它們就可以逃脫。參數(shù)也是如此。有時可以從其他因素中得出動畫值。速度值就是一個很好的例子,您不一定需要完全同步該值,但您可以使用同步位置更新來估計其值。如果可能,請嘗試同步盡可能少的參數(shù)。

  • 將 PhotonAnimatorView 添加到我的機器人 Kyle 預制件
  • 將 PhotonAnimatorView 從其標題標題拖到 PhotonView 組件中的新可觀察組件條目上
  • 現(xiàn)在,在同步參數(shù)中,將速度Speed設置為Discrete(離散)
  • 將方向Direction設置為Discrete離散
  • 將跳轉Jump設置為Discrete離散
  • 將 Hi 設置為Disabled禁用
  • 每個值都可以被Disabled禁用,或者Discrete離散地或Continuous連續(xù)地同步。在我們的例子中,因為我們沒有使用 Hi 參數(shù),所以我們將禁用它并節(jié)省流量。

    Discrete離散同步意味著默認情況下每秒發(fā)送 10 次值(在 OnPhotonSerializeView 中)。接收客戶端將值傳遞給他們本地的 Animator。?

    Continuous連續(xù)同步意味著 PhotonAnimatorView 運行每一幀。當調用 OnPhotonSerializeView 時(默認情況下每秒 10 次),自上次調用以來記錄的值將一起發(fā)送。接收客戶端然后按順序應用這些值以保持平滑過渡。這種模式雖然更流暢,但也會發(fā)送更多的數(shù)據(jù)來達到這種效果。

    ?3.用戶輸入管理

    用戶對網(wǎng)絡的控制的一個關鍵方面是相同的預制件將為所有玩家實例化,但其中只有一個代表用戶實際在計算機前玩,所有其他實例代表其他用戶,在其他計算機上玩。因此,考慮到這一點的第一個障礙是"Input Management”。我們如何在一個實例上啟用輸入而不在其他實例上啟用輸入以及如何知道哪個是正確的?輸入 IsMine 概念。

    讓我們編輯之前創(chuàng)建的 PlayerAnimatorManager 腳本。在當前形式下,此腳本不知道這種區(qū)別,讓我們來實現(xiàn)它。

  • 打開腳本PlayerAnimatorManager
  • 將 PlayerAnimatorManager 類從 MonoBehaviour 轉換為 MonoBehaviourPun,這樣可以方便地公開 PhotonView 組件。
  • 在 Update() 調用中,在最開始插入
  • if (photonView.IsMine == false && PhotonNetwork.IsConnected == true) {return; }

    好的,如果實例由“客戶端”應用程序控制,則 photonView.IsMine 將為真,這意味著該實例代表在此應用程序中在此計算機上玩游戲的自然人。因此,如果它為 false,我們不想做任何事情,只依賴 PhotonView 組件來同步我們之前設置的Transform和Animator組件。
    但是,為什么還要在我們的 if 語句中強制執(zhí)行 PhotonNetwork.IsConnected == true 呢?嗯嗯:) 因為在開發(fā)過程中,我們可能想在沒有連接的情況下測試這個預制件。例如,在虛擬場景中,僅創(chuàng)建和驗證與網(wǎng)絡功能本身無關的代碼。因此,通過這個附加表達式,我們將允許在未連接時使用輸入。這是一個非常簡單的技巧,將大大改善您在開發(fā)過程中的工作流程。

    4.相機控制

    它與輸入相同,玩家只有一個游戲視圖,因此我們需要 CameraWork 腳本只跟隨本地玩家,而不是其他玩家。這就是 CameraWork 腳本具有定義何時跟隨的能力的原因。
    讓我們修改 PlayerManager 腳本來控制 CameraWork 組件。

    在 Awake() 和 Update() 方法之間插入下面的代碼:

    /// <summary> /// MonoBehaviour method called on GameObject by Unity during initialization phase. /// </summary> void Start() {CameraWork _cameraWork = this.gameObject.GetComponent<CameraWork>();if (_cameraWork != null){if (photonView.IsMine){_cameraWork.OnStartFollowing();}}else{Debug.LogError("<Color=Red><a>Missing</a></Color> CameraWork Component on playerPrefab.", this);} }

    首先,它獲取 CameraWork 組件,這是我們所期望的,所以如果我們沒有找到它,我們會記錄一個錯誤。然后,如果 photonView.IsMine 為真,則意味著我們需要跟隨此實例,因此我們調用 _cameraWork.OnStartFollowing(),這會有效地使相機跟隨場景中的那個實例。
    所有其他Player實例都將其 photonView.IsMine 設置為 false,因此它們各自的 _cameraWork 將不執(zhí)行任何操作。
    使這項工作有效的最后一項更改:

    在預制件 My Robot Kyle 的 CameraWork 組件上禁用 Follow on Start

    現(xiàn)在,這實際上將跟隨玩家的邏輯移交給了將調用 _cameraWork.OnStartFollowing() 的腳本 PlayerManager,如上所述。

    ?5.Beams Fire 控制

    觸發(fā)也遵循上面暴露的輸入原則,它只需要在 photonView.IsMine 為 true 時工作
    打開腳本PlayerManager
    用 if 語句包圍輸入處理調用:

    if (photonView.IsMine) {ProcessInputs (); }

    然而,在測試時,我們只看到本地玩家開火。我們還需要查看另一個實例何時觸發(fā)。我們需要一種在網(wǎng)絡上同步觸發(fā)的機制。為此,我們將手動同步 IsFiring 布爾值,到目前為止,我們使用 PhotonTransformView 和 PhotonAnimatorView 為我們完成變量的所有內部同步,我們只需要調整通過 Unity 方便地暴露給我們的內容Inspector,但是這里我們需要的是非常針對您的游戲的,因此我們需要手動執(zhí)行此操作。

    打開腳本PlayerManager
    實施 IPunObservable:

    public class PlayerManager : MonoBehaviourPunCallbacks, IPunObservable {#region IPunObservable implementationpublic void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info){}#endregion }

    在 IPunObservable.OnPhotonSerializeView 中添加以下代碼:

    if (stream.IsWriting) {// We own this player: send the others our datastream.SendNext(IsFiring); } else {// Network player, receive datathis.IsFiring = (bool)stream.ReceiveNext(); }

    回到 Unity 編輯器,在你的資源中選擇 My Robot Kyle prefab,在 PhotonView 組件上添加一個 observe entry 并將 PlayerManager 組件拖到它上面

    ?如果沒有這最后一步,IPunObservable.OnPhotonSerializeView 永遠不會被調用,因為它不會被 PhotonView 觀察到。
    在這個 IPunObservable.OnPhotonSerializeView 方法中,我們得到一個變量流,這是將要通過網(wǎng)絡發(fā)送的內容,這個調用是我們讀取和寫入數(shù)據(jù)的機會。我們只能在我們是本地玩家時寫入(photonView.IsMine == true),否則我們讀取。
    由于 stream 類有 helpers 知道該做什么,我們簡單地依賴 stream.isWriting 來知道在當前實例情況下預期的是什么。
    如果我們需要寫入數(shù)據(jù),我們會使用 stream.SendNext() 將 IsFiring 值附加到數(shù)據(jù)流中,這是一種非常方便的方法,可以隱藏數(shù)據(jù)序列化的所有艱苦工作。如果我們需要讀取,我們使用 stream.ReceiveNext()。

    6.生命值同步

    好的,為了完成網(wǎng)絡更新Player功能,我們將同步生命值,以便Player的每個實例都具有正確的生命值。這與我們剛才介紹的 IsFiring 值的原理完全相同。

    打開腳本播放器管理器
    在 SendNext 和 ReceiveNext IsFiring 變量之后,在 IPunObservable.OnPhotonSerializeView 中,對 Health 做同樣的事情:

    if (stream.IsWriting) {// We own this player: send the others our datastream.SendNext(IsFiring);stream.SendNext(Health); } else {// Network player, receive datathis.IsFiring = (bool)stream.ReceiveNext();this.Health = (float)stream.ReceiveNext(); }

    這就是在這個場景中同步 Health 變量所需要的全部內容。

    九、Player Instantiation

    本節(jié)將介紹網(wǎng)絡上的“Player”預制實例化,并實現(xiàn)播放時適應自動場景切換所需的各種功能。

    1.實例化Player

    實例化我們的“Player”預制件實際上非常容易。我們需要在剛進入房間時實例化它,我們可以依賴 GameManager Script Start() 消息,這將指示我們已經(jīng)加載了Arena,這意味著我們的設計表明我們在房間中。

    打開 GameManager 腳本
    在 Public Fields 區(qū)域添加以下變量:

    [Tooltip("The prefab to use for representing the player")] public GameObject playerPrefab;

    在 Start() 方法中,添加以下內容

    if (playerPrefab == null) {Debug.LogError("<Color=Red><a>Missing</a></Color> playerPrefab Reference. Please set it up in GameObject 'Game Manager'",this); } else {Debug.LogFormat("We are Instantiating LocalPlayer from {0}", Application.loadedLevelName);// we're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.InstantiatePhotonNetwork.Instantiate(this.playerPrefab.name, new Vector3(0f,5f,0f), Quaternion.identity, 0); }

    這會公開一個公共字段供您引用“Player”預制件。這很方便,因為在這種特殊情況下,我們可以直接拖放到“GameManager”預制件中,而不是在每個場景中,因為“Player”預制件是一種資產(chǎn),因此引用將保持完整(而不是引用層次結構中的游戲對象,預制件只能在同一場景中實例化時執(zhí)行)。

    ?警告:始終確保應該通過網(wǎng)絡實例化的預制件位于 Resources 文件夾中,這是 Photon 的要求。

    ?

    然后,在 Start() 中,我們實例化它(在檢查我們是否正確引用了“Player”預制件之后)。
    請注意,我們在地板上方實例化(5 個單位以上,而玩家只有 2 個單位高)。這是在新玩家加入房間時防止碰撞的眾多方法之一,玩家可能已經(jīng)在競技場中心移動,因此它避免了突然碰撞。 “掉落”的玩家也是游戲中一個新實體的清晰指示和介紹。
    然而,這對我們的情況來說還不夠,我們有一個轉折:)當其他玩家加入時,將加載不同的場景,我們希望保持一致性,而不是僅僅因為其中一個玩家離開而破壞現(xiàn)有玩家。所以我們需要告訴 Unity 不要銷毀我們創(chuàng)建的實例,這反過來意味著我們現(xiàn)在需要檢查在加載場景時是否需要實例化。

    2.跟蹤玩家實例

    打開 PlayerManager 腳本
    在“Public Fields”區(qū)域中,添加以下內容:

    [Tooltip("The local player instance. Use this to know if the local player is represented in the Scene")] public static GameObject LocalPlayerInstance;

    ?在 Awake() 方法中,添加以下內容:

    // #Important // used in GameManager.cs: we keep track of the localPlayer instance to prevent instantiation when levels are synchronized if (photonView.IsMine) {PlayerManager.LocalPlayerInstance = this.gameObject; } // #Critical // we flag as don't destroy on load so that instance survives level synchronization, thus giving a seamless experience when levels load. DontDestroyOnLoad(this.gameObject);

    通過這些修改,我們可以在 GameManager 腳本中執(zhí)行檢查以僅在必要時實例化。

    打開 GameManager 腳本
    用 if 條件包圍實例化調用:

    if (PlayerManager.LocalPlayerInstance == null) {Debug.LogFormat("We are Instantiating LocalPlayer from {0}", SceneManagerHelper.ActiveSceneName);// we're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.InstantiatePhotonNetwork.Instantiate(this.playerPrefab.name, new Vector3(0f, 5f, 0f), Quaternion.identity, 0); } else {Debug.LogFormat("Ignoring scene load for {0}", SceneManagerHelper.ActiveSceneName); }

    有了這個,我們現(xiàn)在只在 PlayerManager 沒有對 localPlayer 的現(xiàn)有實例的引用時實例化。

    3.在競技場外管理Player位置?

    我們還有一件事需要注意。競技場的大小根據(jù)玩家的數(shù)量而變化,這意味著如果一個玩家離開而其他玩家在當前競技場大小的邊界附近,他們會發(fā)現(xiàn)自己在較小的競技場之外它會加載,我們需要考慮到這一點,在這種情況下只需將玩家重新定位回競技場的中心。這是您的游戲玩法和關卡設計中的一個問題。
    目前增加了復雜性,因為 Unity 改進了“場景管理”并且 Unity 5.4 棄用了一些回調,這使得創(chuàng)建適用于所有 Unity 版本(從 Unity 5.3.7 到最新版本)的代碼稍微復雜一些。所以我們需要基于 Unity 版本的不同代碼。它與 Photon Unity Networking 無關,但掌握它對于您的項目在更新中生存很重要。

    打開 PlayerManager 腳本
    在“私有方法”區(qū)域中添加一個新方法:

    #if UNITY_5_4_OR_NEWER void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode loadingMode) {this.CalledOnLevelWasLoaded(scene.buildIndex); } #endif

    在 Start() 方法的末尾,添加以下代碼

    #if UNITY_5_4_OR_NEWER // Unity 5.4 has a new scene management. register a method to call CalledOnLevelWasLoaded. UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; #endif

    在“MonoBehaviour Callbacks”區(qū)域中添加以下兩個方法

    #if !UNITY_5_4_OR_NEWER /// <summary>See CalledOnLevelWasLoaded. Outdated in Unity 5.4.</summary> void OnLevelWasLoaded(int level) {this.CalledOnLevelWasLoaded(level); } #endifvoid CalledOnLevelWasLoaded(int level) {// check if we are outside the Arena and if it's the case, spawn around the center of the arena in a safe zoneif (!Physics.Raycast(transform.position, -Vector3.up, 5f)){transform.position = new Vector3(0f, 5f, 0f);} }

    重寫 OnDisable 方法如下:

    #if UNITY_5_4_OR_NEWER public override void OnDisable() {// Always call the base to remove callbacksbase.OnDisable ();UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; } #endif

    這段新代碼所做的是觀察正在加載的關卡,并向下投射當前玩家的位置以查看我們是否擊中了任何東西。如果我們不這樣做,這意味著我們不在競技場的地面之上,我們需要重新定位回到中心,就像我們第一次進入房間時一樣。
    如果您使用的 Unity 版本低于 Unity 5.4,我們將使用 Unity 的回調 OnLevelWasLoaded。如果您使用的是 Unity 5.4 或更高版本,則 OnLevelWasLoaded 不再可用,您必須使用新的 SceneManagement 系統(tǒng)。最后,為了避免重復代碼,我們只需要一個 CalledOnLevelWasLoaded 方法,該方法將從 OnLevelWasLoaded 或 SceneManager.sceneLoaded 回調中調用。

    十、Player UI Prefab

    本節(jié)將指導您創(chuàng)建 Player UI 系統(tǒng)。我們需要顯示玩家的姓名及其當前健康狀況。我們還需要管理 UI 位置以跟隨周圍的玩家。
    本節(jié)與網(wǎng)絡本身無關。但是,它提出了一些非常重要的設計模式,以提供一些圍繞網(wǎng)絡及其在開發(fā)中引入的約束的高級功能。
    所以,UI 不會聯(lián)網(wǎng),只是因為我們不需要,還有很多其他方法可以解決這個問題并避免占用流量。這總是值得努力的事情,如果你能擺脫一個不聯(lián)網(wǎng)的功能,那就太好了。
    現(xiàn)在的合理問題是:我們如何為每個聯(lián)網(wǎng)Player提供一個 UI?
    我們將擁有一個帶有專用 PlayerUI 腳本的 UI Prefab。我們的 PlayerManager 腳本將保存此 UI Prefab 的引用,并在 PlayerManager 啟動時簡單地實例化此 UI Prefab,并告訴該預制件跟隨那個玩家。

    1.創(chuàng)建 UI 預制件

  • 打開任意一個有 UI 畫布的場景
  • 將 Slider UI 游戲對象添加到畫布,將其命名為 Player UI
  • 將 Rect Transform vertical anchor 設置為 Middle,Horizo??ntal anchor 設置為 center
  • 將 Rect Transform 寬度設置為 80,將高度設置為 15
  • 選擇背景子項,將其圖像組件顏色設置為紅色
  • 選擇子“填充區(qū)域/填充”,將其圖像顏色設置為綠色
  • 添加一個 Text UI GameObject 作為 Player UI 的子對象,將其命名為 Player Name Text
  • 將 CanvasGroup 組件添加到播放器 UI
  • 在該 CanvasGroup 組件上將 Interactable 和 Blocks Raycast 屬性設置為 false
  • 將 Player UI 從層次結構拖到資產(chǎn)中的預制文件夾中,你知道有一個預制件
  • 刪除場景中的實例,我們不再需要它了。?
  • ?2.PlayerUI 腳本基礎

    創(chuàng)建一個新的 C# 腳本,并將其命名為 PlayerUI
    這是基本的腳本結構,相應地編輯和保存 PlayerUI 腳本:

    using UnityEngine; using UnityEngine.UI;using System.Collections;namespace Com.MyCompany.MyGame {public class PlayerUI : MonoBehaviour{#region Private Fields[Tooltip("UI Text to display Player's Name")][SerializeField]private Text playerNameText;[Tooltip("UI Slider to display Player's Health")][SerializeField]private Slider playerHealthSlider;#endregion#region MonoBehaviour Callbacks#endregion#region Public Methods#endregion} }

    現(xiàn)在讓我們創(chuàng)建預制件本身。

  • 將 PlayerUI 腳本添加到 Prefab PlayerUI
  • 將子游戲對象“Player Name Text”拖放到公共字段 PlayerNameText 中
  • 將滑塊組件拖放到公共字段 PlayerHealthSlider
  • 3.PlayerUI 與 Player 綁定

    PlayerUI 腳本需要知道它代表哪個玩家,其中一個原因是:能夠顯示其健康狀況和名稱,讓我們創(chuàng)建一個公共方法來實現(xiàn)此綁定。

    打開腳本 PlayerUI
    在“私有字段”區(qū)域添加私有屬性:

    private PlayerManager target;

    我們需要提前考慮,我們會定期查看健康狀況,因此緩存 PlayerManager 的引用以提高效率是有意義的。

    ?在“公共方法”區(qū)域添加這個公共方法:

    public void SetTarget(PlayerManager _target) {if (_target == null){Debug.LogError("<Color=Red><a>Missing</a></Color> PlayMakerManager target for PlayerUI.SetTarget.", this);return;}// Cache references for efficiencytarget = _target;if (playerNameText != null){playerNameText.text = target.photonView.Owner.NickName;} }

    在“MonoBehaviour Callbacks”區(qū)域中添加此方法:

    void Update() {// Reflect the Player Healthif (playerHealthSlider != null){playerHealthSlider.value = target.Health;} }

    有了這個,我們就有了 UI 來顯示目標玩家的名字和生命值。

    4.實例化

    好的,所以我們已經(jīng)知道我們想要如何實例化這個預制件,每次我們實例化一個玩家預制件。最好的方法是在初始化期間在 PlayerManager 中執(zhí)行此操作。

    ?打開腳本PlayerManager
    添加一個公共字段來保存對 Player UI 預制件的引用,如下所示:

    [Tooltip("The Player's UI GameObject Prefab")] [SerializeField] public GameObject PlayerUiPrefab;

    在 Start() 方法中添加此代碼:

    if (PlayerUiPrefab != null) {GameObject _uiGo = Instantiate(PlayerUiPrefab);_uiGo.SendMessage ("SetTarget", this, SendMessageOptions.RequireReceiver); } else {Debug.LogWarning("<Color=Red><a>Missing</a></Color> PlayerUiPrefab reference on player Prefab.", this); }

    所有這些都是標準的 Unity 編碼。但是請注意,我們正在向剛剛創(chuàng)建的實例發(fā)送一條消息。我們需要一個接收器,這意味著如果 SetTarget 沒有找到響應它的組件,我們將收到警報。另一種方法是從實例中獲取 PlayerUI 組件,然后直接調用 SetTarget。通常建議直接使用組件,但知道您可以通過多種方式實現(xiàn)相同的目的也很好。
    然而這還遠遠不夠,我們需要處理玩家的刪除,我們當然不希望場景中到處都是孤立的 UI 實例,所以我們需要在發(fā)現(xiàn)目標時銷毀 UI 實例它已經(jīng)被分配了。

    打開 PlayerUI 腳本
    將此添加到 Update() 函數(shù):

    // Destroy itself if the target is null, It's a fail safe when Photon is destroying Instances of a Player over the network if (target == null) {Destroy(this.gameObject);return; }

    保存 PlayerUI 腳本這段代碼雖然簡單,但實際上非常方便。由于 Photon 刪除聯(lián)網(wǎng)實例的方式,如果目標引用為空,UI 實例更容易銷毀自身。這樣就避免了很多潛在的問題,而且非常安全,不管是什么原因丟失了一個target,相關的UI也會自動銷毀,非常方便快捷。但是等一下……當一個新關卡被加載時,UI 被破壞但我們的播放器仍然存在……所以我們也需要在我們知道一個關卡被加載時實例化它,讓我們這樣做:

    打開腳本PlayerManager
    在 CalledOnLevelWasLoaded() 方法中添加此代碼:

    GameObject _uiGo = Instantiate(this.PlayerUiPrefab); _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver);

    請注意,有更復雜/更強大的方法來處理這個問題,UI 可以用單例來制作,但它很快就會變得復雜,因為其他加入和離開房間的玩家也需要處理他們的 UI。在我們的實現(xiàn)中,這是直截了當?shù)?#xff0c;代價是重復我們實例化 UI 預制件的位置。作為一個簡單的練習,您可以創(chuàng)建一個私有方法來實例化并發(fā)送“SetTarget”消息,然后從不同的地方調用該方法而不是復制代碼。

    5.Parenting To UI Canvas

    ?Unity UI 系統(tǒng)的一個非常重要的約束是任何 UI 元素都必須放置在 Canvas GameObject 中,因此我們需要在實例化 PlayerUI Prefab 時處理這個問題,我們將在 PlayerUI 腳本的初始化期間執(zhí)行此操作。

    打開腳本 PlayerUI
    在“MonoBehaviour Callbacks”區(qū)域內添加此方法:

    void Awake() {this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false); }

    保存 PlayerUI 腳本 為什么要用蠻力以這種方式找到 Canvas?因為當場景要加載和卸載時,我們的 Prefab 也是如此,而 Canvas 每次都會不同。為了避免更復雜的代碼結構,我們將采用最快的方法。但是真的不推薦使用“Find”,因為這是一個緩慢的操作。這超出了本教程的范圍,無法實現(xiàn)對此類情況的更復雜處理,但是當您對 Unity 和腳本感到滿意時,這是一個很好的練習,可以找到編碼更好地管理需要加載的 Canvas 元素引用的方法并考慮卸載。

    6.跟隨目標玩家?

    這是一個有趣的部分,我們需要讓玩家 UI 在屏幕上跟隨玩家目標。這意味著要解決幾個小問題:

    UI 是一個 2D 元素,Player是一個 3D 對象。在這種情況下我們如何匹配位置?
    我們不希望 UI 稍微高于Player,我們如何在屏幕上從播放器位置偏移?

    ?打開 PlayerUI 腳本
    在“公共字段”區(qū)域內添加此公共屬性:

    [Tooltip("Pixel offset from the player target")] [SerializeField] private Vector3 screenOffset = new Vector3(0f,30f,0f);

    將這四個字段添加到“私有字段”區(qū)域:

    float characterControllerHeight = 0f; Transform targetTransform; Renderer targetRenderer; CanvasGroup _canvasGroup; Vector3 targetPosition;

    將其添加到 Awake Method 區(qū)域內:

    _canvasGroup = this.GetComponent<CanvasGroup>();

    在設置 _target 后將以下代碼附加到 SetTarget() 方法:

    targetTransform = this.target.GetComponent<Transform>(); targetRenderer = this.target.GetComponent<Renderer>(); CharacterController characterController = _target.GetComponent<CharacterController> (); // Get data from the Player that won't change during the lifetime of this Component if (characterController != null) {characterControllerHeight = characterController.height; }

    我們知道我們的Player基于具有高度屬性的 CharacterController,我們需要它來對Player上方的 UI 元素進行適當?shù)钠啤?/p>

    在“MonoBehaviour Callbacks”區(qū)域添加這個公共方法 :

    void LateUpdate() {// Do not show the UI if we are not visible to the camera, thus avoid potential bugs with seeing the UI, but not the player itself.if (targetRenderer!=null){this._canvasGroup.alpha = targetRenderer.isVisible ? 1f : 0f;}// #Critical// Follow the Target GameObject on screen.if (targetTransform != null){targetPosition = targetTransform.position;targetPosition.y += characterControllerHeight;this.transform.position = Camera.main.WorldToScreenPoint (targetPosition) + screenOffset;} }

    ?因此,將 2D 位置與 3D 位置匹配的技巧是使用攝像機的 WorldToScreenPoint 函數(shù),由于我們的游戲中只有一個,我們可以依賴于訪問主攝像機,這是 Unity 場景的默認設置。
    請注意我們是如何分幾步設置偏移量的:首先我們獲取目標的實際位置,然后添加 characterControllerHeight,最后,在推斷出 Player 頂部的屏幕位置后,我們添加屏幕偏移量。

    ?

    Package Demos

    1.Asteroids

    小行星演示是將 Unity 的 NetworkMeteoroid 演示從 uNet 移植到 PUN 2 的結果。在這個演示中,1 到 8 個玩家可以競爭摧毀小行星。得分最多的玩家贏得游戲。
    如果您想了解更多關于從 uNet 到 PUN 的移植過程,您可以查看演示的文檔頁面。
    演示位置:/Photon/PhotonUnityNetworking/Demos/DemoAsteroids/

    ?

    (1)從 UNet 移植到 PUN

    此頁面基于 Unity 的 NetworkMeteoroid 演示描述了從 uNet 到 PUN 的移植過程,您可以通過訪問資產(chǎn)商店的鏈接查看該演示。該頁面顯示了該過程的某些方面,這些方面非常容易處理,而其他一些方面則處理起來稍微復雜一些,以便最終獲得令人信服的結果。因此,該頁面被細分為不同的部分,以涵蓋所有必要和重要的步驟。這些步驟(或多或少)按難度升序排序。但是,成功移植游戲并不一定要遵循此順序。

    還請記住,以下描述不是您可以用來將現(xiàn)有應用程序從 uNet 移植到 PUN 的通用方法,但它應該讓您了解哪些步驟是必要的以及您可能必須處理哪些問題.

    (2)重新導入資產(chǎn),重建預制件和場景

    當開始將現(xiàn)有項目從 uNet 移植到 PUN 時,您基本上可以使用現(xiàn)有項目并將所有 uNET 的網(wǎng)絡邏輯替換為 PUN 的網(wǎng)絡邏輯。由于我們正在處理一個我們不熟悉的演示,因此我們決定從一個新項目開始并重建演示的大部分內容。這樣我們也可以從一開始就展示整個移植過程。實際上沒有必要這樣做,但這樣我們也能夠看到原始演示和我們移植的演示之間的所有差異 - 至少在源代碼中。如果您認為這會導致比僅使用現(xiàn)有的 NetworkMeteoroid 演示更多的工作,那么您可能是對的,但否則我們也必須做很多特別次要的工作。因此,我們可以預計,就此演示而言,兩種移植過程的工作量或多或少是相同的。與項目的復雜性相關,這種體驗肯定會改變。
    首先,我們在 Unity 中創(chuàng)建了一個新項目,并開始(或多或少)重新創(chuàng)建原始演示的文件夾結構,并重新導入必要的資產(chǎn)(模型和紋理)。完成后,我們開始重建其他所需的資產(chǎn),例如材料和預制件以及場景。在特別重建“大廳”場景時,我們確保沒有重新創(chuàng)建以后不需要的部分,例如專用服務器選項。我們還確保它的外觀和感覺適合 PUN 的演示中心。

    (3)對游戲邏輯進行細微調整

    對應用程序的源代碼進行修改時,您需要一個起點。在這種情況下,我們決定從 NetworkMeteoroid 演示的游戲邏輯開始 - 特別是因為沒有那么多事情要做。當然,我們不能一對一地重用整個代碼,但我們可以使用演示的源代碼作為模式,并簡單地對其進行修改。因此,我們可以復制整個代碼并在之后修改它,或者從一開始就應用修改重新編寫它。對于這個演示,我們結合使用了這兩種方式。最后,游戲邏輯本身很可能與原始演示中的相同,除了一些特別適用于網(wǎng)絡相關源代碼的小改動。
    這里的一個例子是小行星在原始演示中是如何產(chǎn)生的。在我們的修改版本中,它基本上以相同的方式工作(使用一個“游戲管理器”和一個只要游戲運行就運行的協(xié)程),只是對網(wǎng)絡相關代碼進行了一些小的調整。在這個具體的例子中,我們只是用 PUN 的房間對象實例化調用 PhotonNetwork.InstantiateRoomObject(...) 替換了 uNet 的實例化調用 NetworkServer.Spawn(...)。使用此調用,我們可以添加 InstantiationData,例如,我們使用它來共享有關小行星剛體的其他詳細信息。這還有一個好處,就是我們不必使用單獨的 RPC 或 RaiseEvent 調用來同步此類信息。
    但也有部分源代碼根本沒有修改。例如,處理玩家輸入的方式與原始演示中的方式完全相同,因為它已經(jīng)運行良好,根本不需要任何修改。

    (4)對網(wǎng)絡邏輯進行重大調整

    在這部分,事情(終于)變得有趣了,因為我們必須對演示的源代碼進行一些重大修改,以便用 PUN 的網(wǎng)絡邏輯替換所有 uNet 的網(wǎng)絡邏輯。提醒:遺憾的是,在嘗試將現(xiàn)有應用程序從 uNet 移植到 PUN 時,沒有可以遵循的通用方法。所以你不能一般地說某個 uNET 屬性(例如 [ClientRpc])可以一直映射到某個 PUN 屬性(在這個例子中是 [PunRPC]),因為在PUN 中根本不存在網(wǎng)絡邏輯或屬性本身。這意味著您必須考慮對每一行代碼的網(wǎng)絡相關源代碼的哪些段應用了哪些修改。
    由于我們不出于此演示的目的使用服務器端邏輯,因此我們還必須就如何處理模擬做出另一個重要決定,因為它由原始演示中的服務器控制。在沒有自定義服務器端邏輯的情況下使用 PUN 時,我們唯一的可能是使用所有客戶端或僅使用一個客戶端來處理模擬。在我們的例子中,我們選擇了第二個選項,并決定使用 MasterClient 來運行和控制模擬。這意味著它是唯一允許實例化小行星并處理與玩家宇宙飛船發(fā)射的子彈的碰撞檢測的客戶端。此外,這些小行星被實例化為場景對象,其好處是如果 MasterClient 在游戲運行時斷開連接,它們不會被破壞。相反,只要有另一個客戶端可以接管這個角色,模擬的控制就會傳遞給新的 MasterClient。
    網(wǎng)絡邏輯的另一個重要方面是前面提到的小行星和玩家飛船的同步。為了獲得令人信服的結果,我們決定實現(xiàn)一個自定義的 OnPhotonSerializeView 函數(shù),該函數(shù)處理所有必要數(shù)據(jù)的發(fā)送和接收。這些包括剛體的位置、旋轉和速度。隨著對它的進一步修改,這個自定義解決方案后來變成了新的 PhotonRigidbodyView 組件。

    (5)通過添加滯后補償解決同步問題

    在設置模擬并在多個客戶端上運行它之后,我們很快發(fā)現(xiàn)我們有明顯的同步問題,當至少有兩個游戲窗口彼此相鄰運行時,這會導致視覺上令人失望的結果。一個例子是宇宙飛船在兩個不同屏幕上的位置位移。這是由滯后引起的,并導致整個同步的進一步問題:在某些情況下,玩家的宇宙飛船在一個客戶端的視圖中撞上了一顆小行星,但在另一個客戶端的視圖中卻沒有。這進一步迫使 MasterClient(記住他控制模擬和碰撞檢測)有時會引爆另一個玩家的宇宙飛船,因為他的物理系統(tǒng)檢測到碰撞,而這在其他客戶端上根本不可見。這些問題對于任何多人游戲的玩法來說都是致命的。
    為了擺脫這些同步問題,我們決定為小行星、宇宙飛船和它們發(fā)射的子彈添加滯后補償。在我們的例子中,滯后補償意味著接收到同步對象信息的客戶端試圖在先前接收到的信息的幫助下計算出更準確和更新的數(shù)據(jù)。一個例子:每當客戶端收到另一艘宇宙飛船的信息時,他使用接收到的位置和速度值以及消息的時間戳和他當前的時間戳來計算另一艘宇宙飛船的最新位置。計算出這個更準確的位置后,我們使用 Unity 的 FixedUpdate 函數(shù)實際將宇宙飛船一步一步地移近它的“真實”位置——至少移動到我們認為這是物體“真實”位置的位置。為清楚起見,您可以查看下面的代碼片段,其中顯示了上述功能的實現(xiàn)。

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {if (stream.IsWriting){stream.SendNext(rigidbody.position);stream.SendNext(rigidbody.rotation);stream.SendNext(rigidbody.velocity);}else{networkPosition = (Vector3)stream.ReceiveNext();networkRotation = (Quaternion)stream.ReceiveNext();rigidbody.velocity = (Vector3)stream.ReceiveNext();float lag = Mathf.Abs((float)(PhotonNetwork.Time - info.timestamp));networkPosition += (rigidbody.velocity * lag);} }

    Owner只發(fā)送飛船的位置、旋轉和速度等重要信息。接收者使用此信息更新其本地存儲的值并對位置應用滯后補償......?

    public void FixedUpdate() {if (!photonView.IsMine){rigidbody.position = Vector3.MoveTowards(rigidbody.position, networkPosition, Time.fixedDeltaTime);rigidbody.rotation = Quaternion.RotateTowards(rigidbody.rotation, networkRotation, Time.fixedDeltaTime * 100.0f);} }

    ?...在使用 FixedUpdate 函數(shù)中本地存儲的數(shù)據(jù)更新遠程對象之前。

    請注意:上面顯示的代碼片段并不代表 PUN 包中的實際 PhotonRigidbodyView 組件。

    最后,為移植的演示添加滯后補償使其變得更好。這包括令人信服的視覺表現(xiàn)以及具有改進的網(wǎng)絡同步的整體令人滿意的游戲玩法。

    ?2.Procedural

    程序演示展示了在使用 Photon Cloud 時如何處理程序生成的世界。因此,該演示的重點是生成世界和在多個客戶端上同步應用到它的修改。您可以在演示的文檔頁面上相關信息。
    演示位置:/Photon/PhotonUnityNetworking/Demos/DemoProcedural/

    ?

    (1)介紹

    在此演示中,我們想演示如何使用 Photon Cloud 處理程序生成的世界和應用于它們的修改。因此 PUN 2 包包含一個演示,我們可以在其中創(chuàng)建一個由立方體/塊組成的世界。對于創(chuàng)建過程本身,我們有不同的選項,我們可以選擇這些選項來創(chuàng)建不同的世界。
    此文檔頁面描述了演示和世界生成的工作原理以及應用的修改如何在所有客戶端之間同步。它還顯示了在使用 Photon Cloud 和一般情況下創(chuàng)建程序生成的世界時所犯的一些最常見的錯誤。

    (2)這個演示是如何工作的

    當連接到房間時,MasterClient 可以使用控制面板控制確定性世界生成器。下一章將介紹控制面板及其相關選項。當世界生成器運行時,它會創(chuàng)建多個集群,每個集群包含多個構成世界的塊。將世界劃分為不同的集群,有助于稍后在對其應用修改時對其進行同步。同步的工作原理也是本文檔頁面描述的一部分。要對世界應用修改,任何客戶端都可以左鍵單擊某個塊以降低其高度或右鍵單擊以提高其高度。
    您可能想知道現(xiàn)在是否降低或提高塊的高度。我們決定通過在 y 軸上使用塊的比例來描述生成世界的不同高度級別。這有一些優(yōu)點:首先是我們在場景中沒有那么多的游戲對象,這在性能方面是好的——Unity(以及其他引擎)顯然無法處理幾乎無限數(shù)量的對象。所以為了這個演示的目的,我們對我們的實施沒有問題。另一方面是我們必須以某種方式存儲應用的修改,以便所有客戶端都可以使用它們。由于我們在 Photon Cloud 上沒有可用的自定義服務器端邏輯(除非我們使用企業(yè)云),我們決定使用自定義房間屬性來存儲應用的修改。最后一點是,實施的解決方案比更復雜且可能“生產(chǎn)就緒”的解決方案更容易理解。然而,這個演示仍然展示了我們在開發(fā)程序生成游戲時使用 Photon Cloud 的可能性。

    (3)生成一個世界

    啟動演示時,您可能會注意到游戲窗口左上角的控制面板。該面板只能由 MasterClient 使用,因為它控制著世界生成器。在這個演示中,我們有四個選項可以影響世界生成器。

    其中一個選項是種子,在此演示中用數(shù)字表示。種子至少需要一位數(shù)字,最多可以有十位數(shù)字,結果是從 0 到 9,999,999,999 的區(qū)間。另一個選項描述了世界的整體大小。對于這個演示,我們可以創(chuàng)建尺寸從 16 x 16 塊到 128 x 128 塊的世界。請注意,就此演示而言,生成的世界不是無限的。第三個選項描述了集群的大小。一個簇最多可以包含 64 個塊。創(chuàng)建多少集群主要取決于這個值和前面提到的世界大小。擁有包含大量塊的集群將加速生成世界(由于我們的實施)。另一個選項描述了對生成關卡的外觀有影響的世界類型。該演示包含三個不同的選項,主要影響生成過程中塊的最大高度。
    每當 MasterClient 單擊控制面板上的確認按鈕時,所選選項將存儲在自定義房間屬性中,以便同一房間中的所有客戶端都可以使用它們。每當客戶端收到這些更新時,他將在本地(重新)運行他的世界生成器以創(chuàng)建新世界。由于這些選項是同步的并且沒有使用任何隨機功能,因此每個客戶端將生成相同的世界。

    為了生成一個世界,我們使用 Simplex Noise 算法,這是一個噪聲函數(shù)。為此,我們將每個實例化塊的 x 和 z 坐標傳遞給它的 CalcPixel2D 函數(shù)。除了 2D 功能外,Benjamin Ward 使用的 Simplex Noise 實現(xiàn)也提供了 1D 和 3D 功能。這些功能中的每一個都有兩個重要的共同點。第一個是,這些函數(shù)總是返回一個介于 -1.0f 和 1.0f 之間的值。我們使用這個值來計算每個塊的高度(y 尺度)。高度主要取決于這個值和使用的世界類型。第二個方面是,只要輸入保持不變,該函數(shù)的輸出就始終相同。換句話說:更改輸入?yún)?shù)(例如種子)時會得到不同的結果。這主要是我們必須同步使用的種子和世界生成器的其他選項的原因。

    ?(4)同步修改

    如前所述,應用于世界的修改存儲在自定義房間屬性中。這有一個主要好處:每個客戶都將自動收到最新的自定義房間屬性,包括所有修改;我們只需要在客戶端處理它們。這也讓后來加入的客戶更容易,因為我們不必考慮如何與他們分享世界的最新狀態(tài),它只是“自動”發(fā)生。
    為了將修改后的世界數(shù)據(jù)存儲在自定義房間屬性中,每個集群都會為其添加一個唯一標識符和一個字典。字典本身包含某些塊的唯一標識符及其相關高度(y 尺度)。這里的重要方面是,只有修改過的塊才會存儲在自定義房間屬性中。未修改的塊仍然由之前解釋的世界生成器設置描述,不需要存儲在這里。

    注意:我們在此演示中使用字典,因為它比更復雜的解決方案更易于使用且更容易理解。當然還有其他可能的表示來描述對世界所做的修改。

    就本演示而言,這非常有效。但是,如果您想要創(chuàng)建一個具有“無限”世界的更大規(guī)模的游戲,您必須考慮將其托管在企業(yè)云或自托管的 Photon 服務器上。這兩個選項提供了通過使用插件或通過自己實現(xiàn)服務器應用程序來運行自定義服務器端邏輯的可能性。這對您的游戲來說可能是必不可少的,因為您會繞過一些限制,例如在加入游戲時的最大世界大小或加載時間方面。

    (5)最常見的錯誤

    本章介紹了在開發(fā)程序生成的網(wǎng)絡游戲時可能犯的一些最常見的錯誤。
    你可能犯的第一個錯誤是試圖“網(wǎng)絡實例化”一切。假設您想創(chuàng)建一個由幾堵墻組成的迷宮。就 PUN 而言,一種簡單的方法是為這面墻創(chuàng)建一個預制件,將一個 PhotonView 組件附加到它上面,然后在使用 Unity Editor 時將其直接放置在場景中,或者在運行時使用 PhotonNetwork.Instantiate 或 PhotonNetwork 對其進行實例化。實例化房間對象。這實際上可能適用于一定數(shù)量的對象,但是不推薦這樣做的一個原因是每個客戶端的 ViewID 有限制。此限制適用于用戶以及場景對象。由于此限制,生成的迷宮也可能受到其大小或復雜性的限制。
    另一個常見錯誤是在每個客戶端上分別使用來自 Unity 或 System 命名空間的 Random 類。 Random 類的問題是,只要不使用相同的種子,就會在不同的機器上得到不同的結果。結果是,不同的客戶端會生成不同的世界,這在多人游戲方面確實很糟糕。如果您現(xiàn)在仍然考慮將 Random 類與同步種子一起使用,還有另一個主要缺點:您很可能不會獲得視覺上令人滿意的結果。正如所描述的,噪聲算法創(chuàng)建了某種高度圖,它具有 - 取決于生成它的設置 - 在不同高度級別之間的過渡。使用 Random 類時,很可能在不同高度級別之間不會有任何良好的過渡。取而代之的是,會有很多拼湊而成的結果在視覺上令人失望。
    由于我們已經(jīng)看到了一種通過使用自定義房間屬性來存儲一些數(shù)據(jù)的方法,您可能會考慮使用它們來存儲整個生成的世界。當世界變得太大時,這可能會在一定程度上發(fā)揮作用。然而,問題是,加入房間需要很長時間,因為必須將大量數(shù)據(jù)傳輸?shù)娇蛻舳恕T谶@種情況下的解決方案是添加服務器端邏輯,以便服務器可以決定哪些數(shù)據(jù)需要發(fā)送給客戶端。因此,服務器不會立即發(fā)送整個世界狀態(tài),而是只發(fā)送世界的那些部分,客戶端當前需要并將在之后按需更新他。

    ?3.老虎機賽車

    ?在 Slot Racer 演示中,1 到 4 名玩家可以在賽道上駕駛他們的老虎機。演示沒有使用“經(jīng)典”位置同步,而是使用驅動距離來同步軌道上玩家的老虎機。
    演示位置:/Photon/PhotonUnityNetworking/Demos/DemoSlotRacer/

    ?4.PUN Cockpit

    Cockpit 演示提供了區(qū)域 ping、連接過程、房間創(chuàng)建和房間管理的可視化方法,并完美嵌入到 Slot Racer 演示中。要使用它,您必須將 PunCockpit-Scene 和 SlotCar-Scene 添加到構建設置中并啟動 Slot Racer 演示。
    演示位置:/Photon/PhotonUnityNetworking/Demos/PunCockpit/

    ?

    ?5.LoadBalancing

    ?LoadBalancing 演示展示了如何直接使用實時 API。
    演示位置:/Photon/PhotonRealtime/Demos/DemoLoadBalancing/

    ?6.Chat

    從 PUN Classic 接管的聊天演示通過使用聊天 API 顯示了一個簡單的聊天室。
    演示位置:/Photon/PhotonChat/Demos/DemoChat/

    總結

    以上是生活随笔為你收集整理的(二)PUN 2基本教程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。

    91精品免费在线 | 欧美日韩高清一区二区 国产亚洲免费看 | 久久久国产一区二区三区四区小说 | 中文理论片 | 国产精品日韩在线播放 | 国产精品2区 | 婷婷激情在线 | 美女av免费看 | 国产欧美精品一区二区三区 | 在线有码中文字幕 | 免费在线视频一区二区 | 91完整版在线观看 | 国产精品igao视频网网址 | 亚洲精品一区二区18漫画 | 在线免费观看视频一区 | 成人a在线观看高清电影 | 久久久久夜色 | 欧美一性一交一乱 | 成人一级免费电影 | 国产视频在线看 | 中文字幕日韩免费视频 | 免费看污片 | 久草在线免费播放 | 久久手机精品视频 | 中午字幕在线观看 | 成人app在线播放 | 一区二区欧美在线观看 | 国产一区二三区好的 | 亚洲综合色播 | 波多野结衣在线播放一区 | 在线一级片| 亚洲精品免费观看视频 | 2019中文最近的2019中文在线 | 国产午夜精品免费一区二区三区视频 | 亚洲激情 欧美激情 | 午夜久久福利影院 | 亚洲精品国偷拍自产在线观看蜜桃 | 国产精品 国内视频 | 午夜视频免费播放 | 成人亚洲精品久久久久 | 98福利在线 | 97视频亚洲 | 在线免费视频一区 | 日本久久久亚洲精品 | 成人久久精品视频 | 欧美另类xxx | 高清av不卡 | 99热这里是精品 | 91久久精品一区二区二区 | 丁香av | 色五月情 | 日韩精品在线观看视频 | 中文字幕专区高清在线观看 | 啪啪资源 | 天天天天综合 | 99riav1国产精品视频 | 免费看黄20分钟 | av福利电影 | 日韩视频三区 | 韩国av免费在线 | 在线日韩中文 | 黄色动态图xx | 久久艹艹 | 久久精品99国产精品亚洲最刺激 | 久久精品中文字幕免费mv | 三级午夜片 | 中文字幕在线观看免费高清完整版 | www99精品 | 91免费版在线观看 | av中文天堂在线 | 天天av天天| 国产高清小视频 | 国产一二三四在线观看视频 | 国产精品福利一区 | 日日操日日插 | 国产日韩在线一区 | 亚洲色图av | 久久综合免费视频 | 99热在线网站| 四虎在线免费观看 | 亚洲欧洲一区二区在线观看 | 亚洲三级黄色 | 国产一区二区在线播放视频 | 天天射天天干天天操 | 国产免费av一区二区三区 | www.久草视频| 91丨九色丨国产在线 | 九九热在线观看 | 中文字幕资源网在线观看 | 偷拍福利视频一区二区三区 | 日韩一区在线播放 | 精品福利网站 | 日韩av一区二区三区四区 | 正在播放国产一区二区 | 亚洲精品久久久久999中文字幕 | 婷婷激情欧美 | 成人欧美在线 | 国产在线欧美在线 | 免费看一级 | 中文字幕一区二区三区久久蜜桃 | 久久一区二区三区国产精品 | 免费一级片在线观看 | 欧美精品资源 | 亚洲精品一区二区三区在线观看 | 99久久精品免费看国产四区 | 久久av中文字幕片 | 91中文字幕永久在线 | 中文字幕影片免费在线观看 | 首页中文字幕 | 亚洲精品中文字幕在线观看 | 国产123av| 丝袜+亚洲+另类+欧美+变态 | 黄色日视频| 国产成人免费 | 中文字幕在线免费观看 | 国产探花在线看 | 一区二区三区久久 | 中文有码在线 | 91精品国产欧美一区二区 | 免费69视频 | 成人av手机在线 | 久久国产精彩视频 | 成人av免费网站 | 亚洲高清资源 | 天天干亚洲 | 亚洲特级片 | 狠狠狠色丁香婷婷综合久久88 | 999国产在线 | 日韩综合在线观看 | 啪啪免费试看 | 在线观看视频中文字幕 | 天天玩天天干天天操 | 天天操夜夜叫 | 波多野结依在线观看 | 久久伦理网 | www.久久婷婷 | 亚洲精品av在线 | 91九色国产在线 | 日韩免费一区二区 | 九九九九九精品 | 色婷婷综合久久久 | 在线观看国产一区二区 | 黄色小说视频在线 | 国产精品久久久久久a | 丁香在线观看完整电影视频 | 在线免费观看一区二区三区 | 国产一二区在线观看 | 狠狠综合久久 | 国产最新精品视频 | 亚洲有 在线 | 西西444www大胆高清视频 | 亚洲视频99 | 久久久久久久久久久福利 | 99免费精品 | 狠狠干,狠狠操 | 夜夜视频欧洲 | 国产精品第一页在线 | 亚洲激情网站免费观看 | 精品综合久久久 | 一本一道波多野毛片中文在线 | 久草免费手机视频 | 久久精品视频2 | 超碰人人做| 国产粉嫩在线观看 | 91亚洲精 | 午夜精品福利一区二区三区蜜桃 | 最近中文字幕免费av | 久草青青在线观看 | 色综合中文综合网 | 国产成人精品一区二区三区福利 | 九九热99视频 | 国产 字幕 制服 中文 在线 | 国产在线观看地址 | 丁香视频免费观看 | 成人在线观看你懂的 | 一区二区三区视频在线 | 日韩精品综合在线 | 国产69精品久久久久久久久久 | 日韩av图片 | 免费高清无人区完整版 | 97色噜噜 | 中文字幕在线乱 | 国产午夜精品免费一区二区三区视频 | 99se视频在线观看 | 久久人人爽 | 天天操天天舔天天爽 | 国产一区二区高清 | 日韩av手机在线观看 | 国产999免费视频 | 91视频啊啊啊 | 亚洲免费精品一区二区 | 夜夜骑日日操 | 亚洲影音先锋 | 91视频xxxx| 天天摸天天操天天舔 | 乱男乱女www7788 | 91在线精品一区二区 | 日韩激情第一页 | 人人爱天天操 | av网址aaa | 日日骑| 91久久国产自产拍夜夜嗨 | 黄色软件网站在线观看 | 永久免费的av电影 | 亚洲精品一区二区三区新线路 | 久久精品—区二区三区 | 日韩高清 一区 | 91黄在线看 | 天天摸夜夜操 | 国产人成看黄久久久久久久久 | 国产高清免费 | 亚洲激情校园春色 | 久久精品www人人爽人人 | 色综合久久五月 | 国产精品成久久久久三级 | 在线黄色av电影 | 欧美analxxxx | 成年人在线免费看视频 | 97看片 | 99成人免费视频 | 成人免费视频免费观看 | 国产精品久久久久久久久婷婷 | 99色人| 一级片观看 | 久久精彩视频 | 99久视频 | 视频在线观看亚洲 | 中文字幕一区二区在线播放 | 久久激情五月激情 | 久久黄色影院 | 99产精品成人啪免费网站 | 一区二区中文字幕在线观看 | 亚洲精品国产精品久久99 | 午夜一级免费电影 | 国产精品6 | 天天天天射 | 精品日韩在线 | 久久av黄色| 精品国产一区二区三区四区vr | 欧美另类高潮 | 香蕉视频在线观看免费 | www国产亚洲精品 | 色搞搞 | 日日干天夜夜 | 激情综合啪啪 | 精品久久1 | 五月天亚洲精品 | 亚洲精品美女 | 开心综合网 | 亚洲区精品视频 | 日韩区欧美久久久无人区 | 国产成人三级在线播放 | 日本91在线 | 超碰资源在线 | 国产精品乱码久久久久久1区2区 | 免费91在线观看 | 亚洲电影成人 | 国产精品久久久久永久免费观看 | 97中文字幕 | 91亚洲精品久久久蜜桃网站 | 欧美日韩国产成人 | 美女免费黄视频网站 | 日韩欧美电影在线观看 | 亚洲va男人天堂 | 狠狠色噜噜狠狠 | 成人黄色av网站 | 91精品国产91 | 亚洲综合在线一区二区三区 | 人人添人人澡人人澡人人人爽 | 欧美极品少妇xbxb性爽爽视频 | 人人澡人 | 免费成人av在线看 | 久久久久亚洲精品国产 | 91色欧美 | 在线一区观看 | 中文在线字幕免费观 | 一色av| 在线视频中文字幕一区 | 国产三级精品三级在线观看 | 91爱在线| 精品一二三区视频 | 欧美日韩亚洲在线观看 | 国产在线观看午夜 | 日韩伦理片hd | 色成人亚洲网 | 97超碰国产在线 | 精品一区二区免费在线观看 | 久久99精品久久久久久清纯直播 | 手机看片| 一区二区三区国产精品 | 中国一级片在线播放 | 美女视频免费精品 | 在线高清av| 黄色小说在线观看视频 | 久久久国产99久久国产一 | 日日插日日干 | 五月天久久综合 | 中文字幕在线久一本久 | 亚洲一区二区精品3399 | 丝袜美腿在线 | 国产成人精品一区二区三区在线观看 | 久久久久久久久久久综合 | av免费看网站| 天天天天爱天天躁 | 国产精品午夜久久 | 亚洲激情校园春色 | 一区av在线播放 | 国产精品一区免费在线观看 | 五月天久久| 久久夜av | 亚洲成a人片综合在线 | 亚洲精品456在线播放第一页 | 亚洲成人影音 | 国产精品视频 | 91亚洲精品乱码久久久久久蜜桃 | a色网站| 99亚洲天堂| 国产精久久久久久妇女av | 免费一级特黄录像 | 日韩在线第一 | 国产极品尤物在线 | 国产最新在线 | 一本一道久久a久久精品蜜桃 | 国产精品久久久久久久久岛 | 婷婷久久一区二区三区 | 狠狠色狠狠色合久久伊人 | 在线观看免费av网 | 久久国色夜色精品国产 | 欧美成天堂网地址 | av高清在线| 手机av看片 | 天天色天天综合网 | 国内精品久久影院 | 久草久视频 | 午夜少妇一区二区三区 | 久久不色 | 一区二区三区四区在线免费观看 | 99精品欧美一区二区 | 色婷婷视频在线观看 | 久久久五月天 | 免费观看丰满少妇做爰 | av大全在线看 | 天天草天天干天天射 | 欧美成人手机版 | 黄色在线免费观看网址 | av在线看网站 | 欧美巨乳网 | 国产中文字幕av | 日韩亚洲精品电影 | 久久精品老司机 | 81国产精品久久久久久久久久 | 精品一区二区三区四区在线 | 91免费看黄 | 中文乱码视频在线观看 | 国产免费av一区二区三区 | 91桃花视频| 夜夜躁狠狠燥 | 在线免费高清一区二区三区 | 日韩字幕在线 | 丁香六月欧美 | 国产专区视频 | 国产一区二区三区四区在线 | www.av免费观看| 91资源在线| 亚洲一区二区三区在线看 | 色综合天天做天天爱 | 国产尤物在线视频 | 久久久免费看片 | 天天操,夜夜操 | 婷婷久久一区 | 亚洲高清不卡av | 国产精品成人av久久 | 九九综合九九 | 日韩精品五月天 | 99久久99精品 | 天天婷婷 | 久久99热这里只有精品 | 999日韩| 久久久久久网址 | 欧美性高跟鞋xxxxhd | 91视频a| avove黑丝| 天天草天天摸 | 五月婷婷视频 | 色综合久久网 | 少妇bbw搡bbbb搡bbb | se视频网址 | 精品在线观看一区二区 | 国产视频一区二区在线 | 天天干干| 成人app在线免费观看 | 一区二区三区免费在线 | 伊人婷婷在线 | 成人亚洲欧美 | 91精品国产成人观看 | 992tv在线| 日韩欧美在线观看一区二区三区 | 四虎在线永久免费观看 | www.国产在线 | 日本精品久久 | 免费福利片2019潦草影视午夜 | 国产999精品久久久 免费a网站 | 一区二区三区高清不卡 | 色播99| 欧洲精品久久久久毛片完整版 | 国产资源免费 | 国产一级免费视频 | 高清视频一区 | 特级西西444www高清大视频 | 国产精品初高中精品久久 | 免费在线观看91 | 欧美精品久久久久久久久免 | 亚洲视频每日更新 | 免费在线观看成年人视频 | 日韩在线电影观看 | 日韩成人欧美 | 在线视频手机国产 | 91av蜜桃| 在线观看亚洲精品视频 | 草久久久久 | 色综合久久精品 | 成人av免费在线观看 | 国产精品美女久久久 | 最近中文字幕mv | 成人综合婷婷国产精品久久免费 | 免费无遮挡动漫网站 | 色婷婷综合久久久 | 中文字幕黄色 | 久久精品视频一 | 国产一区二区在线免费播放 | 成人黄色在线播放 | 久二影院| 亚洲欧美国产精品久久久久 | 精品夜夜嗨av一区二区三区 | 91免费黄视频| 国产一二三四在线观看视频 | 2020天天干夜夜爽 | 久久在草| 99精品视频在线观看 | 99色婷婷 | 久久超碰99 | 一区二区中文字幕在线播放 | 久久精品综合视频 | 草在线视频 | 国产 日韩 欧美 在线 | 国产精品色 | 免费福利在线观看 | 亚洲经典视频 | 久久精品观看 | av一级在线 | 色综合久久网 | 伊人天天狠天天添日日拍 | 91传媒在线观看 | 狠狠操.com| 久久手机在线视频 | 久久免费视频国产 | 国产精品都在这里 | 操碰av| www久| 日韩高清成人在线 | 在线精品亚洲 | 日韩精品一区二区三区免费视频观看 | 97视频入口免费观看 | 国产五月色婷婷六月丁香视频 | 久草视频首页 | 中文字幕免费看 | 国产黄色特级片 | 日韩区在线观看 | 久久av免费电影 | 激情综合狠狠 | 国产精品一区免费在线观看 | 欧美激情精品 | 伊人天天狠天天添日日拍 | 最新国产精品视频 | 国产精品久久一区二区三区, | 精品中文字幕在线观看 | 日韩动漫免费观看高清完整版在线观看 | 国产精品久久一卡二卡 | 视频一区亚洲 | 超碰在线99 | 日韩免费网址 | 色噜噜色噜噜 | 国产精品日韩在线播放 | 精品视频免费观看 | 久久爱www.| 911免费视频 | 亚洲精品久久激情国产片 | 嫩草av在线 | 亚洲精品成人av在线 | 久久乱码卡一卡2卡三卡四 五月婷婷久 | 久久精品高清视频 | 久久亚洲热 | 欧美精品久久99 | 免费裸体视频网 | 免费视频一区二区 | 婷婷av综合| 伊人丁香 | 人人狠 | 天堂视频中文在线 | 91pony九色丨交换 | 国产午夜激情视频 | 精品一区二区免费视频 | 久久精品高清视频 | 婷婷5月激情5月 | 午夜精品视频一区 | 在线观看亚洲电影 | 日日精品 | 99久久精品国产一区二区成人 | 久久综合久久综合九色 | 正在播放 久久 | 深爱开心激情网 | 亚洲伊人av | 99re中文字幕 | 久久激情影院 | av电影免费 | 天天干天天干天天射 | 亚洲成人二区 | 91视频专区| 91视频在线观看下载 | 亚洲影音先锋 | 激情五月婷婷综合网 | 亚洲高清国产视频 | 美女网站在线观看 | 三级av黄色 | 国产精品理论片 | 精品国产免费久久 | 日韩艹 | 国产精品网站 | 国产精品久久久久久久免费大片 | 91福利视频久久久久 | 狠狠色伊人亚洲综合网站野外 | 久久午夜免费视频 | 欧美日韩在线免费观看视频 | 国产在线更新 | 97电院网手机版 | 国产网站色 | 久久激情视频 久久 | 亚洲一级免费电影 | 婷婷午夜天 | 欧美福利网址 | 看全黄大色黄大片 | 美女精品网站 | 亚洲丝袜一区二区 | 国产在线观看av | 久久久久在线视频 | 亚洲激情校园春色 | 欧美激情另类 | 成人av免费在线看 | 视频在线观看亚洲 | 亚洲欧美视频一区二区三区 | 国产综合香蕉五月婷在线 | 精品国产一区二区三区久久久 | 人人舔人人舔 | 亚洲精品国产成人 | 天天操天天操天天操天天操天天操天天操 | 久久精品看| 国产精品v欧美精品v日韩 | 大胆欧美gogo免费视频一二区 | 亚洲视频www | 国产一区二区精 | 久草| 久久久久成| 中文在线字幕免费观 | 久久99精品久久久久久三级 | 亚洲精品中文在线观看 | 日韩欧美在线一区二区 | 夜夜躁狠狠燥 | 波多野结衣在线中文字幕 | 一区视频在线 | 在线播放视频一区 | 人人爱爱人人 | 国产精品区免费视频 | 久久久久久久久久久久av | 美女免费网视频 | 国产成人精品免高潮在线观看 | 99久热精品 | 人人射 | 久草在线资源视频 | 蜜臀av性久久久久蜜臀aⅴ四虎 | 国产成人在线一区 | 成人免费观看在线视频 | 日日躁夜夜躁xxxxaaaa | 日韩精品免费在线观看 | 日韩中文字幕视频在线观看 | 免费成人在线观看 | 国产精品久久久免费 | 久人人| 亚洲黄色片一级 | 亚洲最新av网址 | 国内精品在线看 | 日日操夜 | 91精品国产乱码在线观看 | 国产精品视频久久久 | 日韩黄色在线电影 | 免费看国产精品 | av在线电影网站 | 亚洲精品国产精品99久久 | 久久免费看a级毛毛片 | 美女av免费看 | 亚洲一级免费观看 | 在线va网站 | 亚洲欧洲成人 | 日本在线中文在线 | 在线www色 | 国产精品久久久久久久久久99 | 在线观看色网 | 日韩a级黄色 | 日本精品中文字幕在线观看 | 国产亚洲精品久久网站 | www.看片网站 | 国产免费专区 | 久久国产经典视频 | 久久精品在线免费观看 | 亚洲欧美日韩国产一区二区三区 | 一级做a爱片性色毛片www | 国产精品入口66mio女同 | 午夜免费福利视频 | 欧美视屏一区二区 | 国产精品h在线观看 | 色资源中文字幕 | 不卡电影免费在线播放一区 | 美女网站视频免费黄 | 最近中文字幕在线中文高清版 | 亚洲美女视频在线观看 | 成人动图 | 久久久久久久国产精品影院 | 永久精品视频 | 中文字幕亚洲欧美日韩 | 欧美日韩精品免费观看视频 | 中文字幕亚洲欧美 | 波多野结衣一区 | 丁香婷五月 | 国产第一页在线播放 | 日韩高清精品免费观看 | 欧美久久久久久久久中文字幕 | 国产精品麻豆一区二区三区 | 奇米影视999 | 波多野结衣精品视频 | 国产区精品视频 | 成人av在线亚洲 | 天堂av官网 | 欧美一区二区三区特黄 | 91在线观 | 国产丝袜一区二区三区 | 伊人亚洲精品 | 97精品在线 | 超碰在线最新网址 | 草免费视频 | 成人在线观看影院 | 国产午夜精品一区二区三区欧美 | 日韩大片免费观看 | 国产精品一区二区久久久久 | 中文字幕精品三区 | 狠狠干网站 | 国产精选在线观看 | 香蕉成人在线视频 | 久久久久综合视频 | 国产精品成人国产乱 | 视频在线观看入口黄最新永久免费国产 | 欧美日韩在线免费观看视频 | 日韩国产精品久久久久久亚洲 | 国产精品青草综合久久久久99 | 91试看 | 精品在线观看视频 | 久久精品99久久 | 六月丁香激情网 | www.福利视频 | 国产破处在线播放 | www天天干com | 国产精品影音先锋 | 国产精品av免费在线观看 | 人成免费网站 | 久久视频这里有精品 | 伊人激情综合 | 丁香视频五月 | 免费观看mv大片高清 | 国产精品精品视频 | 首页av在线 | 91视频网址入口 | 国产黄色av网站 | 成人高清在线观看 | 精品久久综合 | 久久免费视频7 | 综合国产在线 | 天天天插 | 狠狠地操 | 夜夜操夜夜干 | 免费日韩一区二区三区 | 91九色视频观看 | 日韩在线小视频 | 日韩精品久久久久久中文字幕8 | 久久经典国产 | 国产一级91 | 国产视频在线一区二区 | 国产精品理论片 | 国内综合精品午夜久久资源 | 欧美资源在线观看 | a在线观看视频 | 人人澡人人爱 | 婷婷在线视频观看 | 欧美性色xo影院 | 狠狠的操你 | 色婷婷视频在线 | 日韩久久精品一区 | 免费在线播放黄色 | 激情久久久久久久久久久久久久久久 | 国产一区二区三区在线免费观看 | 久章草在线观看 | 国产精品18久久久久久vr | 国产日韩欧美视频在线观看 | 亚洲精品乱码白浆高清久久久久久 | 97激情影院 | 国产日产精品一区二区三区四区的观看方式 | 一区二区三区在线不卡 | www在线观看视频 | 中文字幕在线视频一区二区三区 | 九九综合久久 | www.色婷婷.com | 久久国产亚洲精品 | 成人影视片| 天天色天天上天天操 | 色婷av | 一区二区成人国产精品 | 91自拍视频在线观看 | 97精品伊人 | 久久久久久国产精品999 | 曰韩精品 | 一级黄色免费网站 | 中文字幕一区二区三区四区视频 | 操操操综合| 日韩欧美一区二区在线播放 | 欧美精品xxx | 在线精品视频免费播放 | 在线播放 日韩专区 | 国产精品1区2区3区在线观看 | 夜夜躁狠狠躁 | 久久午夜色播影院免费高清 | 99精品一级欧美片免费播放 | 日韩性久久 | 亚洲夜夜网| h视频日本 | 五月婷婷视频在线观看 | 亚洲最快最全在线视频 | 国产成人精品一区二区在线观看 | 五月天久久婷婷 | 国产黄色一级大片 | www.国产高清 | 日韩一二三 | 国产电影一区二区三区四区 | 精品一区91 | 天堂av中文字幕 | 91桃花视频 | 亚洲高清免费在线 | 国产精品美女网站 | 超碰在线观看97 | 国产无套精品久久久久久 | 99在线视频精品 | 欧美精品v国产精品v日韩精品 | 2019中文字幕第一页 | 亚洲乱亚洲乱妇 | 青青草华人在线视频 | www.91成人 | 波多野结衣最新 | 亚洲欧洲xxxx| 又黄又刺激 | 国产69熟 | 国产91在线免费视频 | 免费看在线看www777 | 特级片免费看 | 91色在线观看视频 | 亚洲精品午夜久久久久久久 | a天堂一码二码专区 | 最新91在线视频 | 午夜视频久久久 | 久久96国产精品久久99漫画 | 国产一区在线免费观看视频 | 国产免费久久精品 | 国产亚洲视频在线 | 亚洲三级视频 | 国产成人免费网站 | 99久久精品免费视频 | 97超碰人人| 日本乱码在线 | 婷婷婷国产在线视频 | 亚洲精品动漫久久久久 | 成人午夜精品久久久久久久3d | 99爱在线| 免费看一级特黄a大片 | 久久系列| 国产精品免费在线观看视频 | 91精品视频导航 | 亚洲无线视频 | 最新国产精品拍自在线播放 | 国产不卡一 | 日批视频在线观看免费 | 国产在线黄色 | 婷婷亚洲五月 | 欧美人交a欧美精品 | 超碰在线观看av.com | 9ⅰ精品久久久久久久久中文字幕 | av网站免费在线 | 亚洲日本va中文字幕 | 亚洲全部视频 | 天天玩天天干 | 青青久草在线视频 | 中文字幕国产亚洲 | 在线播放精品一区二区三区 | 成人免费视频网站在线观看 | 国产精品久久久久久久久久白浆 | 国产美女久久久 | 国产成人精品免高潮在线观看 | 一区二区三区四区五区在线 | 九色91福利 | 日韩免费av片 | 国产精品日韩精品 | 玖玖玖国产精品 | 日韩中文在线观看 | 久久久精品国产一区二区 | 欧美精品久久久久久久久久 | 久久人网 | 国产精品久久久久久久久软件 | 中文字幕在线观看网站 | 国产精品中文字幕在线 | 亚洲高清网站 | 亚洲成人一二三 | 国产精品99精品 | 丁香综合激情 | 久久成人精品电影 | av综合 日韩 | 一区二区三区免费在线观看视频 | 日韩超碰在线 | 国产精品美女久久久久久久 | 少妇性aaaaaaaaa视频 | 亚洲区另类春色综合小说 | 日韩天堂在线观看 | 国产精品理论片 | 黄色片免费看 | 久草免费看 | 91高清免费在线观看 | 五月激情丁香图片 | 综合色中色 | 91大神精品视频在线观看 | 精品久久精品 | 九九热中文字幕 | 欧美日韩不卡一区二区 | 欧美一级乱黄 | 亚洲一本视频 | 国产视频91在线 | av中文字幕在线观看网站 | 久久免费电影网 | 毛片在线播放网址 | 欧美日本高清视频 | www.夜色321.com| 丁香六月婷婷开心婷婷网 | 欧美福利网站 | 国产欧美精品一区aⅴ影院 99视频国产精品免费观看 | 国产一区免费 | 国产五月色婷婷六月丁香视频 | 欧美日韩性视频 | 18+视频网站链接 | 久久免费精品国产 | 免费黄色av电影 | 五月婷婷六月丁香激情 | 亚洲 欧美 综合 在线 精品 | www.夜夜草| 国产精品美女久久久久久免费 | 日批视频在线 | 免费看的黄色 | 蜜桃视频日韩 | 日韩欧美电影在线 | 五月天激情视频在线观看 | 精品久久久久国产免费第一页 | 99视频在线观看视频 | 久久女教师| 国产在线观看污片 | 欧美精品一区二区三区一线天视频 | 在线视频麻豆 | 亚洲黄色激情小说 | 亚洲一区二区精品在线 | 五月精品 | 热re99久久精品国产66热 | 亚洲涩涩涩涩涩涩 | 国产成人亚洲精品自产在线 | 国产精品毛片一区二区在线 | 久久这里只有精品视频99 | 在线视频免费观看 | 人人澡人人干 | 99久久精品久久久久久动态片 | 91片在线观看 | 亚洲久草视频 | 中文字幕4| 日韩精品在线视频 | 中文在线a在线 | 成人四虎影院 | 国产精品久久久久毛片大屁完整版 | 亚洲国产片色 | 黄色片免费看 | 日日干天天爽 | 精品国产诱惑 | 日韩免费视频 | 99亚洲精品在线 | 国产精品亚洲片在线播放 | 午夜精品视频免费在线观看 | 在线免费精品视频 | av免费在线观看网站 | 91片在线观看 | 在线免费观看国产黄色 | 日韩在线免费 | 99久久精品国 | 美女网站色 | 五月婷婷爱 | 在线观看视频黄色 | 91成人免费视频 | 国产裸体视频bbbbb | 99久久久久成人国产免费 | 国产成人精品一区二区在线观看 | 久久99国产一区二区三区 | 亚洲国产福利视频 | a级国产乱理伦片在线播放 久久久久国产精品一区 | 欧美另类69 | 在线播放 日韩专区 | 婷婷四房综合激情五月 | 日韩色高清| 久操视频在线免费看 | 在线视频黄| 亚洲精品在线网站 | 欧美日韩在线免费观看 | 国产免费片 | 亚洲人人精品 | 亚洲少妇xxxx| 中文字幕一区二区三 | 插久久 | 韩日视频在线 | 欧美日比视频 | 亚洲国产wwwccc36天堂 | 91色在线观看视频 | 国产精品对白一区二区三区 | 激情在线免费视频 | 国产精品免费看久久久8精臀av | 91色国产在线 | 99精品在线免费观看 | 成人久久网 | 超碰免费在线公开 | 免费在线成人 | 国产福利专区 | 亚洲a网 | 久久久久影视 | 欧美午夜激情网 | 中文字幕久久网 | 国产免费作爱视频 | 国产又粗又猛又色又黄视频 | 欧美老人xxxx18 | 五月天激情综合网 | 五月天亚洲综合 | 日韩在线观看不卡 | 免费观看全黄做爰大片国产 | 日韩在线观看网站 | 亚洲黄色软件 | 久久这里精品视频 | 中文日韩在线视频 | 西西www444 | 一区免费视频 | 青青草国产免费 | 久久久国产一区二区三区四区小说 | 久草在线观看视频免费 | 亚洲高清91| 日韩欧三级 | 亚洲综合精品在线 | 亚洲 欧美 日韩 综合 | 成年免费在线视频 | 国产精品免费视频观看 | 四虎国产精品免费观看视频优播 | 日日夜夜av | 国产精品久久久久永久免费 | 伊人色综合久久天天网 | 黄色综合 | 一区二区三区免费在线观看视频 | 久草在线资源观看 | 日韩在线观看视频网站 | 中文字幕在线播放日韩 | 婷婷午夜天 | 国产精品嫩草影院99网站 | 亚洲精品视频在线播放 | 亚洲天天看 | 亚洲日本国产精品 | 狠狠色丁香九九婷婷综合五月 | 狠狠色噜噜狠狠 | 91香蕉视频污在线 | 91精品视频免费在线观看 | 久久色亚洲 | 在线观看一 | 婷婷丁香av| 区一区二区三区中文字幕 | 午夜美女福利 | 黄色一级大片在线免费看产 | 少妇bbbb搡bbbb桶 | 日韩国产精品久久久久久亚洲 | 免费看的黄色的网站 |