اصنع لعبة متعددة اللاعبين في Unity باستخدام PUN 2

هل تساءلت يومًا ما الذي يتطلبه إنشاء لعبة متعددة اللاعبين داخل Unity؟

على عكس الألعاب الفردية، تتطلب الألعاب متعددة اللاعبين خادمًا بعيدًا يلعب دور الجسر، مما يسمح لعملاء اللعبة بالتواصل مع بعضهم البعض.

في الوقت الحاضر، تهتم العديد من الخدمات باستضافة الخادم. إحدى هذه الخدمات هي Photon Network، والتي سنستخدمها في هذا البرنامج التعليمي.

PUN 2 هو أحدث إصدار من واجهة برمجة التطبيقات (API) الخاصة بهم والتي تم تحسينها بشكل كبير مقارنة بالإصدار القديم.

في هذه المقالة، سنبدأ بتنزيل الملفات الضرورية، وإعداد Photon AppID، وبرمجة مثال بسيط للعب الجماعي.

Unity الإصدار المستخدم في هذا البرنامج التعليمي: Unity 2018.3.0f2 (64 بت)

الجزء 1: إعداد PUN 2

الخطوة الأولى هي تنزيل حزمة PUN 2 من ملف Asset Store. أنه يحتوي على كافة البرامج النصية والملفات المطلوبة للتكامل متعددة اللاعبين.

  • افتح مشروعك Unity ثم انتقل إلى Asset Store: (نافذة -> عام -> AssetStore) أو اضغط على Ctrl+9
  • ابحث عن "PUN 2- Free" ثم اضغط على النتيجة الأولى أو اضغط هنا
  • قم باستيراد حزمة PUN 2 بعد انتهاء التنزيل

  • بعد استيراد الحزمة، تحتاج إلى إنشاء معرف تطبيق Photon، ويتم ذلك على موقع الويب الخاص بهم: https://www.photonengine.com/
  • إنشاء حساب جديد (أو تسجيل الدخول إلى حسابك الحالي)
  • انتقل إلى صفحة التطبيقات بالضغط على أيقونة الملف الشخصي ثم "Your Applications" أو اتبع هذا الرابط: https://dashboard.photonengine.com/en-US/PublicCloud
  • في صفحة التطبيقات، انقر فوق "Create new app"

  • في صفحة الإنشاء، بالنسبة لـ Photon Type، حدد "Photon Realtime" وبالنسبة للاسم، اكتب أي اسم ثم انقر فوق "Create"

كما ترون، يتم تعيين التطبيق افتراضيًا على الخطة المجانية. يمكنك قراءة المزيد عن خطط التسعير هنا

  • بمجرد إنشاء التطبيق، انسخ معرف التطبيق الموجود أسفل اسم التطبيق

  • ارجع إلى مشروعك Unity ثم انتقل إلى Window -> Photon Unity Networking -> PUN Wizard
  • في معالج PUN، انقر فوق "Setup Project"، ثم الصق معرف التطبيق الخاص بك، ثم انقر فوق "Setup Project"

  • PUN 2 جاهز الآن!

الجزء الثاني: إنشاء لعبة متعددة اللاعبين

الآن دعنا ننتقل إلى الجزء الذي نقوم فيه بالفعل بإنشاء لعبة متعددة اللاعبين.

طريقة التعامل مع اللاعبين المتعددين في PUN 2 هي:

  • أولاً، نتصل بمنطقة الفوتون (مثل شرق الولايات المتحدة الأمريكية وأوروبا وآسيا وما إلى ذلك) والتي تُعرف أيضًا باسم الردهة.
  • بمجرد وصولنا إلى الردهة، نطلب جميع الغرف التي تم إنشاؤها في المنطقة، وبعد ذلك يمكننا إما الانضمام إلى إحدى الغرف أو إنشاء غرفتنا الخاصة.
  • بعد الانضمام إلى الغرفة، نطلب قائمة باللاعبين المتصلين بالغرفة وننشئ مثيلات اللاعب الخاصة بهم، والتي تتم بعد ذلك مزامنتها مع مثيلاتهم المحلية من خلال PhotonView.
  • عندما يغادر شخص ما الغرفة، يتم تدمير مثيله وإزالته من قائمة اللاعبين.

1. إعداد اللوبي

لنبدأ بإنشاء مشهد الردهة الذي سيحتوي على منطق الردهة (استعراض الغرف الموجودة، وإنشاء غرف جديدة، وما إلى ذلك):

  • أنشئ نصًا جديدًا لـ C# وأطلق عليه اسم PUN2_GameLobby
  • قم بإنشاء مشهد جديد وسميه "GameLobby"
  • في مشهد GameLobby، قم بإنشاء GameObject جديد. أطلق عليه اسم "_GameLobby" وقم بتعيين البرنامج النصي PUN2_GameLobby إليه

الآن افتح البرنامج النصي PUN2_GameLobby:

أولاً، نقوم باستيراد مساحات أسماء الفوتون عن طريق إضافة الأسطر أدناه في بداية البرنامج النصي:

using Photon.Pun;
using Photon.Realtime;

قبل المتابعة أيضًا، نحتاج إلى استبدال MonoBehaviour الافتراضي بـ MonoBehaviourPunCallbacks. هذه الخطوة ضرورية لتتمكن من استخدام عمليات الاسترجاعات الخاصة بالفوتون:

public class PUN2_GameLobby : MonoBehaviourPunCallbacks

بعد ذلك نقوم بإنشاء المتغيرات الضرورية:

    //Our player name
    string playerName = "Player 1";
    //Users are separated from each other by gameversion (which allows you to make breaking changes).
    string gameVersion = "0.9";
    //The list of created rooms
    List<RoomInfo> createdRooms = new List<RoomInfo>();
    //Use this name when creating a Room
    string roomName = "Room 1";
    Vector2 roomListScroll = Vector2.zero;
    bool joiningRoom = false;

ثم نستدعي ConnectUsingSettings() في الفراغ Start(). وهذا يعني أنه بمجرد فتح اللعبة، فإنها تتصل بخادم Photon Server:

    // Use this for initialization
    void Start()
    {
        //This makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically
        PhotonNetwork.AutomaticallySyncScene = true;

        if (!PhotonNetwork.IsConnected)
        {
            //Set the App version before connecting
            PhotonNetwork.PhotonServerSettings.AppSettings.AppVersion = gameVersion;
            // Connect to the photon master-server. We use the settings saved in PhotonServerSettings (a .asset file in this project)
            PhotonNetwork.ConnectUsingSettings();
        }
    }

لمعرفة ما إذا كان الاتصال بـ Photon ناجحًا، نحتاج إلى تنفيذ عمليات رد الاتصال هذه: OnDisconnected(DisconnectCause Cause), OnConnectedToMaster(), OnRoomListUpdate(List<RoomInfo>roomList)

    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.Log("OnFailedToConnectToPhoton. StatusCode: " + cause.ToString() + " ServerAddress: " + PhotonNetwork.ServerAddress);
    }

    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");
        //After we connected to Master server, join the Lobby
        PhotonNetwork.JoinLobby(TypedLobby.Default);
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Debug.Log("We have received the Room list");
        //After this callback, update the room list
        createdRooms = roomList;
    }

التالي هو جزء واجهة المستخدم، حيث يتم تصفح الغرفة وإنشاء الغرفة:

    void OnGUI()
    {
        GUI.Window(0, new Rect(Screen.width / 2 - 450, Screen.height / 2 - 200, 900, 400), LobbyWindow, "Lobby");
    }

    void LobbyWindow(int index)
    {
        //Connection Status and Room creation Button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Status: " + PhotonNetwork.NetworkClientState);

        if (joiningRoom || !PhotonNetwork.IsConnected || PhotonNetwork.NetworkClientState != ClientState.JoinedLobby)
        {
            GUI.enabled = false;
        }

        GUILayout.FlexibleSpace();

        //Room name text field
        roomName = GUILayout.TextField(roomName, GUILayout.Width(250));

        if (GUILayout.Button("Create Room", GUILayout.Width(125)))
        {
            if (roomName != "")
            {
                joiningRoom = true;

                RoomOptions roomOptions = new RoomOptions();
                roomOptions.IsOpen = true;
                roomOptions.IsVisible = true;
                roomOptions.MaxPlayers = (byte)10; //Set any number

                PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
            }
        }

        GUILayout.EndHorizontal();

        //Scroll through available rooms
        roomListScroll = GUILayout.BeginScrollView(roomListScroll, true, true);

        if (createdRooms.Count == 0)
        {
            GUILayout.Label("No Rooms were created yet...");
        }
        else
        {
            for (int i = 0; i < createdRooms.Count; i++)
            {
                GUILayout.BeginHorizontal("box");
                GUILayout.Label(createdRooms[i].Name, GUILayout.Width(400));
                GUILayout.Label(createdRooms[i].PlayerCount + "/" + createdRooms[i].MaxPlayers);

                GUILayout.FlexibleSpace();

                if (GUILayout.Button("Join Room"))
                {
                    joiningRoom = true;

                    //Set our Player name
                    PhotonNetwork.NickName = playerName;

                    //Join the Room
                    PhotonNetwork.JoinRoom(createdRooms[i].Name);
                }
                GUILayout.EndHorizontal();
            }
        }

        GUILayout.EndScrollView();

        //Set player name and Refresh Room button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Player Name: ", GUILayout.Width(85));
        //Player name text field
        playerName = GUILayout.TextField(playerName, GUILayout.Width(250));

        GUILayout.FlexibleSpace();

        GUI.enabled = (PhotonNetwork.NetworkClientState == ClientState.JoinedLobby || PhotonNetwork.NetworkClientState == ClientState.Disconnected) && !joiningRoom;
        if (GUILayout.Button("Refresh", GUILayout.Width(100)))
        {
            if (PhotonNetwork.IsConnected)
            {
                //Re-join Lobby to get the latest Room list
                PhotonNetwork.JoinLobby(TypedLobby.Default);
            }
            else
            {
                //We are not connected, estabilish a new connection
                PhotonNetwork.ConnectUsingSettings();
            }
        }

        GUILayout.EndHorizontal();

        if (joiningRoom)
        {
            GUI.enabled = true;
            GUI.Label(new Rect(900 / 2 - 50, 400 / 2 - 10, 100, 20), "Connecting...");
        }
    }

وأخيرًا، قمنا بتنفيذ 4 عمليات رد اتصال أخرى: OnCreateRoomFailed(short returnCode, string message), OnJoinRoomFailed(short returnCode, string message), OnCreatedRoom(), و OnJoinedRoom().

تُستخدم عمليات رد الاتصال هذه لتحديد ما إذا كنا قد انضممنا/أنشأنا الغرفة أو إذا كانت هناك أية مشكلات أثناء الاتصال.

إليك البرنامج النصي النهائي PUN2_GameLobby.cs:

using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class PUN2_GameLobby : MonoBehaviourPunCallbacks
{

    //Our player name
    string playerName = "Player 1";
    //Users are separated from each other by gameversion (which allows you to make breaking changes).
    string gameVersion = "0.9";
    //The list of created rooms
    List<RoomInfo> createdRooms = new List<RoomInfo>();
    //Use this name when creating a Room
    string roomName = "Room 1";
    Vector2 roomListScroll = Vector2.zero;
    bool joiningRoom = false;

    // Use this for initialization
    void Start()
    {
        //This makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically
        PhotonNetwork.AutomaticallySyncScene = true;

        if (!PhotonNetwork.IsConnected)
        {
            //Set the App version before connecting
            PhotonNetwork.PhotonServerSettings.AppSettings.AppVersion = gameVersion;
            // Connect to the photon master-server. We use the settings saved in PhotonServerSettings (a .asset file in this project)
            PhotonNetwork.ConnectUsingSettings();
        }
    }

    public override void OnDisconnected(DisconnectCause cause)
    {
        Debug.Log("OnFailedToConnectToPhoton. StatusCode: " + cause.ToString() + " ServerAddress: " + PhotonNetwork.ServerAddress);
    }

    public override void OnConnectedToMaster()
    {
        Debug.Log("OnConnectedToMaster");
        //After we connected to Master server, join the Lobby
        PhotonNetwork.JoinLobby(TypedLobby.Default);
    }

    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Debug.Log("We have received the Room list");
        //After this callback, update the room list
        createdRooms = roomList;
    }

    void OnGUI()
    {
        GUI.Window(0, new Rect(Screen.width / 2 - 450, Screen.height / 2 - 200, 900, 400), LobbyWindow, "Lobby");
    }

    void LobbyWindow(int index)
    {
        //Connection Status and Room creation Button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Status: " + PhotonNetwork.NetworkClientState);

        if (joiningRoom || !PhotonNetwork.IsConnected || PhotonNetwork.NetworkClientState != ClientState.JoinedLobby)
        {
            GUI.enabled = false;
        }

        GUILayout.FlexibleSpace();

        //Room name text field
        roomName = GUILayout.TextField(roomName, GUILayout.Width(250));

        if (GUILayout.Button("Create Room", GUILayout.Width(125)))
        {
            if (roomName != "")
            {
                joiningRoom = true;

                RoomOptions roomOptions = new RoomOptions();
                roomOptions.IsOpen = true;
                roomOptions.IsVisible = true;
                roomOptions.MaxPlayers = (byte)10; //Set any number

                PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
            }
        }

        GUILayout.EndHorizontal();

        //Scroll through available rooms
        roomListScroll = GUILayout.BeginScrollView(roomListScroll, true, true);

        if (createdRooms.Count == 0)
        {
            GUILayout.Label("No Rooms were created yet...");
        }
        else
        {
            for (int i = 0; i < createdRooms.Count; i++)
            {
                GUILayout.BeginHorizontal("box");
                GUILayout.Label(createdRooms[i].Name, GUILayout.Width(400));
                GUILayout.Label(createdRooms[i].PlayerCount + "/" + createdRooms[i].MaxPlayers);

                GUILayout.FlexibleSpace();

                if (GUILayout.Button("Join Room"))
                {
                    joiningRoom = true;

                    //Set our Player name
                    PhotonNetwork.NickName = playerName;

                    //Join the Room
                    PhotonNetwork.JoinRoom(createdRooms[i].Name);
                }
                GUILayout.EndHorizontal();
            }
        }

        GUILayout.EndScrollView();

        //Set player name and Refresh Room button
        GUILayout.BeginHorizontal();

        GUILayout.Label("Player Name: ", GUILayout.Width(85));
        //Player name text field
        playerName = GUILayout.TextField(playerName, GUILayout.Width(250));

        GUILayout.FlexibleSpace();

        GUI.enabled = (PhotonNetwork.NetworkClientState == ClientState.JoinedLobby || PhotonNetwork.NetworkClientState == ClientState.Disconnected) && !joiningRoom;
        if (GUILayout.Button("Refresh", GUILayout.Width(100)))
        {
            if (PhotonNetwork.IsConnected)
            {
                //Re-join Lobby to get the latest Room list
                PhotonNetwork.JoinLobby(TypedLobby.Default);
            }
            else
            {
                //We are not connected, estabilish a new connection
                PhotonNetwork.ConnectUsingSettings();
            }
        }

        GUILayout.EndHorizontal();

        if (joiningRoom)
        {
            GUI.enabled = true;
            GUI.Label(new Rect(900 / 2 - 50, 400 / 2 - 10, 100, 20), "Connecting...");
        }
    }

    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Debug.Log("OnCreateRoomFailed got called. This can happen if the room exists (even if not visible). Try another room name.");
        joiningRoom = false;
    }

    public override void OnJoinRoomFailed(short returnCode, string message)
    {
        Debug.Log("OnJoinRoomFailed got called. This can happen if the room is not existing or full or closed.");
        joiningRoom = false;
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.Log("OnJoinRandomFailed got called. This can happen if the room is not existing or full or closed.");
        joiningRoom = false;
    }

    public override void OnCreatedRoom()
    {
        Debug.Log("OnCreatedRoom");
        //Set our player name
        PhotonNetwork.NickName = playerName;
        //Load the Scene called GameLevel (Make sure it's added to build settings)
        PhotonNetwork.LoadLevel("GameLevel");
    }

    public override void OnJoinedRoom()
    {
        Debug.Log("OnJoinedRoom");
    }
}

2. إنشاء لاعب الجاهزة

في الألعاب متعددة اللاعبين، يكون لمثيل اللاعب وجهان: محلي وعن بعد

يتم التحكم في المثيل المحلي محليًا (بواسطتنا).

ومن ناحية أخرى، فإن المثيل البعيد هو تمثيل محلي لما يفعله اللاعب الآخر. يجب أن لا يتأثر بمدخلاتنا.

لتحديد ما إذا كان المثيل محليًا أم بعيدًا، نستخدم مكون PhotonView.

يعمل PhotonView بمثابة برنامج مراسلة يستقبل ويرسل القيم التي تحتاج إلى المزامنة، على سبيل المثال، الموضع والتدوير.

فلنبدأ بإنشاء مثيل المشغل (إذا كان لديك بالفعل مثيل المشغل الخاص بك جاهزًا، فيمكنك تخطي هذه الخطوة).

في حالتي، سيكون مثيل المشغل عبارة عن مكعب بسيط يتم نقله باستخدام مفتاحي W وS وتدويره باستخدام مفتاحي A وD.

فيما يلي نص تحكم بسيط:

SimplePlayerController.cs

using UnityEngine;

public class SimplePlayerController : MonoBehaviour
{

    // Update is called once per frame
    void Update()
    {
        //Move Front/Back
        if (Input.GetKey(KeyCode.W))
        {
            transform.Translate(transform.forward * Time.deltaTime * 2.45f, Space.World);
        }
        else if (Input.GetKey(KeyCode.S))
        {
            transform.Translate(-transform.forward * Time.deltaTime * 2.45f, Space.World);
        }

        //Rotate Left/Right
        if (Input.GetKey(KeyCode.A))
        {
            transform.Rotate(new Vector3(0, -14, 0) * Time.deltaTime * 4.5f, Space.Self);
        }
        else if (Input.GetKey(KeyCode.D))
        {
            transform.Rotate(new Vector3(0, 14, 0) * Time.deltaTime * 4.5f, Space.Self);
        }
    }
}

الخطوة التالية هي إضافة مكون PhotonView.

  • أضف مكون PhotonView إلى مثيل المشغل.
  • قم بإنشاء برنامج نصي جديد لـ C#، وأطلق عليه اسم PUN2_PlayerSync (سيتم استخدام هذا البرنامج النصي للتواصل من خلال PhotonView).

افتح البرنامج النصي PUN2_PlayerSync:

في PUN2_PlayerSync أول شيء يتعين علينا القيام به هو إضافة مساحة اسم Photon.Pun واستبدال MonoBehaviour بـ MonoBehaviourPun وإضافة واجهة IPunObservable أيضًا.

يعد MonoBehaviourPun ضروريًا لتتمكن من استخدام متغير photonView المخزن مؤقتًا، بدلاً من استخدام GetComponent<PhotonView>().

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable

بعد ذلك يمكننا الانتقال إلى إنشاء جميع المتغيرات الضرورية:

    //List of the scripts that should only be active for the local player (ex. PlayerController, MouseLook etc.)
    public MonoBehaviour[] localScripts;
    //List of the GameObjects that should only be active for the local player (ex. Camera, AudioListener etc.)
    public GameObject[] localObjects;
    //Values that will be synced over network
    Vector3 latestPos;
    Quaternion latestRot;

ثم في الفراغ Start()، نتحقق مما إذا كان اللاعب محليًا أم بعيدًا باستخدام photonView.IsMine:

    // Use this for initialization
    void Start()
    {
        if (photonView.IsMine)
        {
            //Player is local
        }
        else
        {
            //Player is Remote, deactivate the scripts and object that should only be enabled for the local player
            for (int i = 0; i < localScripts.Length; i++)
            {
                localScripts[i].enabled = false;
            }
            for (int i = 0; i < localObjects.Length; i++)
            {
                localObjects[i].SetActive(false);
            }
        }
    }

تتم المزامنة الفعلية من خلال رد اتصال PhotonView: OnPhotonSerializeView(PhotonStreamstream, PhotonMessageInfo info):

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            //We own this player: send the others our data
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            //Network player, receive data
            latestPos = (Vector3)stream.ReceiveNext();
            latestRot = (Quaternion)stream.ReceiveNext();
        }
    }

في هذه الحالة، نرسل فقط موضع اللاعب ودورانه، ولكن يمكنك استخدام المثال أعلاه لإرسال أي قيمة مطلوبة لمزامنتها عبر الشبكة، بتردد عالٍ.

يتم بعد ذلك تطبيق القيم المستلمة في التحديث الفارغ ():

    // Update is called once per frame
    void Update()
    {
        if (!photonView.IsMine)
        {
            //Update remote player (smooth this, this looks good, at the cost of some accuracy)
            transform.position = Vector3.Lerp(transform.position, latestPos, Time.deltaTime * 5);
            transform.rotation = Quaternion.Lerp(transform.rotation, latestRot, Time.deltaTime * 5);
        }
    }
}

إليك البرنامج النصي النهائي PUN2_PlayerSync.cs:

using UnityEngine;
using Photon.Pun;

public class PUN2_PlayerSync : MonoBehaviourPun, IPunObservable
{

    //List of the scripts that should only be active for the local player (ex. PlayerController, MouseLook etc.)
    public MonoBehaviour[] localScripts;
    //List of the GameObjects that should only be active for the local player (ex. Camera, AudioListener etc.)
    public GameObject[] localObjects;
    //Values that will be synced over network
    Vector3 latestPos;
    Quaternion latestRot;

    // Use this for initialization
    void Start()
    {
        if (photonView.IsMine)
        {
            //Player is local
        }
        else
        {
            //Player is Remote, deactivate the scripts and object that should only be enabled for the local player
            for (int i = 0; i < localScripts.Length; i++)
            {
                localScripts[i].enabled = false;
            }
            for (int i = 0; i < localObjects.Length; i++)
            {
                localObjects[i].SetActive(false);
            }
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            //We own this player: send the others our data
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            //Network player, receive data
            latestPos = (Vector3)stream.ReceiveNext();
            latestRot = (Quaternion)stream.ReceiveNext();
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!photonView.IsMine)
        {
            //Update remote player (smooth this, this looks good, at the cost of some accuracy)
            transform.position = Vector3.Lerp(transform.position, latestPos, Time.deltaTime * 5);
            transform.rotation = Quaternion.Lerp(transform.rotation, latestRot, Time.deltaTime * 5);
        }
    }
}

لنقم الآن بتعيين برنامج نصي تم إنشاؤه حديثًا:

  • قم بإرفاق البرنامج النصي PUN2_PlayerSync إلى PlayerInstance.
  • قم بسحب وإسقاط PUN2_PlayerSync في مكونات PhotonView الملاحظة.
  • قم بتعيين SimplePlayerController إلى "Local Scripts" وقم بتعيين GameObjects (التي تريد إلغاء تنشيطها للاعبين عن بعد) إلى "Local Objects"

  • احفظ PlayerInstance في Prefab وانقله إلى المجلد المسمى Resources (إذا لم يكن هناك مثل هذا المجلد، فقم بإنشاء واحد). هذه الخطوة ضرورية لتتمكن من إنتاج كائنات متعددة اللاعبين عبر الشبكة.

3. إنشاء مستوى اللعبة

GameLevel هو مشهد يتم تحميله بعد الانضمام إلى الغرفة وهو المكان الذي تحدث فيه كل الأحداث.

  • أنشئ مشهدًا جديدًا وأطلق عليه اسم "GameLevel" (أو إذا كنت تريد الاحتفاظ باسم مختلف، فتأكد من تغيير الاسم في هذا السطر PhotonNetwork.LoadLevel("GameLevel"); على PUN2_GameLobby.cs).

في حالتي، سأستخدم مشهدًا بسيطًا بالطائرة:

  • الآن قم بإنشاء برنامج نصي جديد وأطلق عليه اسم PUN2_RoomController (سيتعامل هذا البرنامج النصي مع المنطق داخل الغرفة، مثل إنتاج اللاعبين، وإظهار قائمة اللاعبين، وما إلى ذلك).

افتح البرنامج النصي PUN2_RoomController:

تمامًا مثل PUN2_GameLobby، نبدأ بإضافة مساحات أسماء Photon واستبدال MonoBehaviour بـ MonoBehaviourPunCallbacks:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks

الآن دعونا نضيف المتغيرات الضرورية:

    //Player instance prefab, must be located in the Resources folder
    public GameObject playerPrefab;
    //Player spawn point
    public Transform spawnPoint;

لإنشاء مثيل للمشغل الجاهز، نستخدم PhotonNetwork.Instantiate:

    // Use this for initialization
    void Start()
    {
        //In case we started this demo with the wrong scene being active, simply load the menu scene
        if (PhotonNetwork.CurrentRoom == null)
        {
            Debug.Log("Is not in the room, returning back to Lobby");
            UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
            return;
        }

        //We're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, Quaternion.identity, 0);
    }

وواجهة مستخدم بسيطة تحتوي على زر "Leave Room" وبعض العناصر الإضافية مثل اسم الغرفة وقائمة اللاعبين المتصلين:

    void OnGUI()
    {
        if (PhotonNetwork.CurrentRoom == null)
            return;

        //Leave this Room
        if (GUI.Button(new Rect(5, 5, 125, 25), "Leave Room"))
        {
            PhotonNetwork.LeaveRoom();
        }

        //Show the Room name
        GUI.Label(new Rect(135, 5, 200, 25), PhotonNetwork.CurrentRoom.Name);

        //Show the list of the players connected to this Room
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            //Show if this player is a Master Client. There can only be one Master Client per Room so use this to define the authoritative logic etc.)
            string isMasterClient = (PhotonNetwork.PlayerList[i].IsMasterClient ? ": MasterClient" : "");
            GUI.Label(new Rect(5, 35 + 30 * i, 200, 25), PhotonNetwork.PlayerList[i].NickName + isMasterClient);
        }
    }

أخيرًا، قمنا بتنفيذ رد اتصال PhotonNetwork آخر يسمى OnLeftRoom() والذي يتم استدعاؤه عندما نغادر الغرفة:

    public override void OnLeftRoom()
    {
        //We have left the Room, return back to the GameLobby
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
    }

فيما يلي النص النهائي PUN2_RoomController.cs:

using UnityEngine;
using Photon.Pun;

public class PUN2_RoomController : MonoBehaviourPunCallbacks
{

    //Player instance prefab, must be located in the Resources folder
    public GameObject playerPrefab;
    //Player spawn point
    public Transform spawnPoint;

    // Use this for initialization
    void Start()
    {
        //In case we started this demo with the wrong scene being active, simply load the menu scene
        if (PhotonNetwork.CurrentRoom == null)
        {
            Debug.Log("Is not in the room, returning back to Lobby");
            UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
            return;
        }

        //We're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, Quaternion.identity, 0);
    }

    void OnGUI()
    {
        if (PhotonNetwork.CurrentRoom == null)
            return;

        //Leave this Room
        if (GUI.Button(new Rect(5, 5, 125, 25), "Leave Room"))
        {
            PhotonNetwork.LeaveRoom();
        }

        //Show the Room name
        GUI.Label(new Rect(135, 5, 200, 25), PhotonNetwork.CurrentRoom.Name);

        //Show the list of the players connected to this Room
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            //Show if this player is a Master Client. There can only be one Master Client per Room so use this to define the authoritative logic etc.)
            string isMasterClient = (PhotonNetwork.PlayerList[i].IsMasterClient ? ": MasterClient" : "");
            GUI.Label(new Rect(5, 35 + 30 * i, 200, 25), PhotonNetwork.PlayerList[i].NickName + isMasterClient);
        }
    }

    public override void OnLeftRoom()
    {
        //We have left the Room, return back to the GameLobby
        UnityEngine.SceneManagement.SceneManager.LoadScene("GameLobby");
    }
}
  • قم بإنشاء GameObject جديد في المشهد 'GameLevel' وقم بتسميته "_RoomController"
  • قم بإرفاق البرنامج النصي PUN2_RoomController بكائن _RoomController
  • قم بتعيين الإعداد الجاهز لـ PlayerInstance وتحويل SpawnPoint إليه ثم احفظ المشهد

  • أضف كلاً من MainMenu وGameLevel إلى إعدادات البناء.

4. إجراء اختبار البناء

حان الوقت الآن لإنشاء البنية واختبارها:

كل شيء يعمل كما هو متوقع!

علاوة

RPC

في PUN 2، يشير RPC إلى استدعاء الإجراء عن بعد، ويتم استخدامه لاستدعاء وظيفة على العملاء البعيدين الموجودين في نفس الغرفة (يمكنك قراءة المزيد عنها هنا).

تتمتع RPCs بالعديد من الاستخدامات، على سبيل المثال، لنفترض أنك بحاجة إلى إرسال رسالة دردشة إلى جميع اللاعبين في الغرفة. مع RPCs، من السهل القيام بذلك:

[PunRPC]
void ChatMessage(string senderName, string messageText)
{
    Debug.Log(string.Format("{0}: {1}", senderName, messageText));
}

لاحظ [PunRPC] قبل الوظيفة. تعد هذه السمة ضرورية إذا كنت تخطط لاستدعاء الوظيفة عبر RPCs.

لاستدعاء الوظائف التي تم وضع علامة RPC عليها، تحتاج إلى PhotonView. مثال على المكالمة:

PhotonView photonView = PhotonView.Get(this);
photonView.RPC("ChatMessage", RpcTarget.All, PhotonNetwork.playerName, "Some message");

نصيحة احترافية: إذا قمت باستبدال MonoBehaviour في البرنامج النصي الخاص بك بـ MonoBehaviourPun أو MonoBehaviourPunCallbacks، فيمكنك تخطي PhotonView.Get() واستخدام photonView.RPC() مباشرة.

خصائص مخصصة

في PUN 2، تعد الخصائص المخصصة عبارة عن جدول تجزئة يمكن تخصيصه للاعب أو للغرفة.

يكون هذا مفيدًا عندما تحتاج إلى تعيين بيانات ثابتة لا تحتاج إلى تغييرها بشكل متكرر (على سبيل المثال، اسم فريق اللاعب، وضع اللعب في الغرفة، وما إلى ذلك).

أولاً، يجب عليك تحديد Hashtable، ويتم ذلك عن طريق إضافة السطر أدناه في بداية البرنامج النصي:

//Replace default Hashtables with Photon hashtables
using Hashtable = ExitGames.Client.Photon.Hashtable;

يعين المثال أدناه خصائص الغرفة المسماة "GameMode" و "AnotherProperty":

        //Set Room properties (Only Master Client is allowed to set Room properties)
        if (PhotonNetwork.IsMasterClient)
        {
            Hashtable setRoomProperties = new Hashtable();
            setRoomProperties.Add("GameMode", "FFA");
            setRoomProperties.Add("AnotherProperty", "Test");
            PhotonNetwork.CurrentRoom.SetCustomProperties(setRoomProperties);
        }

        //Will print "FFA"
        print((string)PhotonNetwork.CurrentRoom.CustomProperties["GameMode"]);
        //Will print "Test"
        print((string)PhotonNetwork.CurrentRoom.CustomProperties["AnotherProperty"]);

يتم تعيين خصائص اللاعب بالمثل:

            Hashtable setPlayerProperties = new Hashtable();
            setPlayerProperties.Add("PlayerHP", (float)100);
            PhotonNetwork.LocalPlayer.SetCustomProperties(setPlayerProperties);

            print((float)PhotonNetwork.LocalPlayer.CustomProperties["PlayerHP"]);

لإزالة خاصية معينة، فقط قم بتعيين قيمتها على null.

            Hashtable setPlayerProperties = new Hashtable();
            setPlayerProperties.Add("PlayerHP", null);
            PhotonNetwork.LocalPlayer.SetCustomProperties(setPlayerProperties);

دروس إضافية:

مزامنة الأجسام الصلبة عبر الشبكة باستخدام PUN 2

PUN 2 إضافة غرفة الدردشة

مصدر
PUN2Guide.unitypackage14.00 MB