Hướng dẫn làm game online multiplayer trong unity sử dụng unity Networking
Chào các bạn , hôm nay tôi sẽ chia sẻ cách làm một game online nhiều người chơi trong Unity . Có nhiều cách để làm game online trong Unity nhưng trong bài này tôi sẽ giới thiệu cách sử dụng Unity Networking để làm . Chúng ta tạo một project mới trong Unity. tạo một server : Trong project ...
Chào các bạn , hôm nay tôi sẽ chia sẻ cách làm một game online nhiều người chơi trong Unity . Có nhiều cách để làm game online trong Unity nhưng trong bài này tôi sẽ giới thiệu cách sử dụng Unity Networking để làm .
Chúng ta tạo một project mới trong Unity.
tạo một server :
Trong project unity vừa tạo ta tạo mới một file script C# với tên "NetworkManager" . Tiếp theo chúng ta tạo ra một scene mới và một đối tượng rỗng đặt tên là NetworkManager cho dễ nhớ sau đó áp dụng script NetworkManager.cs vừa tạo ra vào đối tượng này . Đối tượng NetworkManager sẽ có nhiệm vụ là một server hoặc kết nối tới một server .
Để tạo ra một server chúng ta phải khởi tạo nó trên mạng và đăng ký với một master server . Việc khởi tạo cần thông số về số lượng người chơi lớn nhất ở đây tôi khai báo là 4 và tên cổng ở đây là cổng 25000 . Tên server game đăng ký phải là duy nhất nếu không sẽ có vấn đề khi trùng tên với các server của game khác . Tên của room trong game thì có thể đặt bất kỳ .Thêm đoạn code sau vào file NetworkManager.cs
private const string typeName = "UniqueGameName"; private const string gameName = "RoomName"; private void StartServer() { Network.InitializeServer(4, 25000, !Network.HavePublicAddress()); MasterServer.RegisterHost(typeName, gameName); }
Nếu server khởi tạo thành công thì hàm sau sẽ được gọi , ở đây ta sẽ làm việc là thông báo khởi tạo server thành công :
void OnServerInitialized() { Debug.Log("Server Initializied"); }
Tiếp theo chúng ta sẽ tạo ra một nút Start Server trong scene , nút này chỉ xuất hiện khi ta chưa khởi tạo server và chưa joined vào một server nào có nghĩa là máy của ta chưa phải là máy khách cũng chưa phải là máy chủ .
void OnGUI() { if (!Network.isClient && !Network.isServer) { if (GUI.Button(new Rect(100, 100, 250, 100), "Start Server")) StartServer(); } }
Chú ý là MasterServer này được điều hành bởi Unity và nó có thể bị dỡ xuống để bảo trì . Bạn có thể tải về và chạy MasterServer của riêng bạn tại local . Thêm vào NetworkManager.cs dòng code sau :
MasterServer.ipAddress = “127.0.0.1″;
**Join Vào Server **
Bây giờ ta đã có thể tạo ra server nhưng chưa có chức năng tìm kiếm server hiện có hay tham gia vào một server nào đó. Để làm điều này chúng ta cần gửi một yêu cầu đến master server để lấy một danh sách các HostData . Cái này chứa tất cả các dữ liệu cần thiết để tham gia vào một server nào đó . Một khi ta nhận được danh sách máy chủ, một tin nhắn được gửi tới các game qua hàm OnMasterServerEvent () . Chúng ta kiểm tra lại bằng MasterServerEvent.HostListReceived nếu thoả mãn chúng ta sẽ lưu danh sách máy chủ lại .
private HostData[] hostList; private void RefreshHostList() { MasterServer.RequestHostList(typeName); } void OnMasterServerEvent(MasterServerEvent msEvent) { if (msEvent == MasterServerEvent.HostListReceived) hostList = MasterServer.PollHostList(); }
Để join vào một server ta dùng hàm sau
private void JoinServer(HostData hostData) { Network.Connect(hostData); }
Sau khi kết nối thành công sẽ gọi vào hàm
void OnConnectedToServer() { Debug.Log("Server Joined"); }
Ta tạo thêm một nút để lấy danh sách các server tên là Refresh Hot , Một nút Start Server , nếu lấy được danh sách server thì với mỗi server sẽ tạo tương ứng một nút để join vào nó .
void OnGUI() { if (!Network.isClient && !Network.isServer) { if (GUI.Button(new Rect(100, 100, 250, 100), "Start Server")) StartServer(); if (GUI.Button(new Rect(100, 250, 250, 100), "Refresh Hosts")) RefreshHostList(); if (hostList != null) { for (int i = 0; i < hostList.Length; i++) { if (GUI.Button(new Rect(400, 100 + (110 * i), 300, 100), hostList[i].gameName)) JoinServer(hostList[i]); } } } }
Tạo ra một đối tượng Player
Ta tạo cảnh game đơn giản là một mặt sàn , một nguồn sáng . Đối tượng Player của chúng ta sẽ là một khối hộp , ta sẽ áp dụng vật lý cho đối tượng này bằng cách thêm collider, rigigbody và ta sẽ không cho nó quay để tránh trường hợp khi di chuyển nó sẽ chạy lung tung .
Ta viết script điều khiển cho đối tượng player
public class Player : MonoBehaviour { public float speed = 10f; void Update() { InputMovement(); } void InputMovement() { if (Input.GetKey(KeyCode.W)) rigidbody.MovePosition(rigidbody.position + Vector3.forward * speed * Time.deltaTime); if (Input.GetKey(KeyCode.S)) rigidbody.MovePosition(rigidbody.position - Vector3.forward * speed * Time.deltaTime); if (Input.GetKey(KeyCode.D)) rigidbody.MovePosition(rigidbody.position + Vector3.right * speed * Time.deltaTime); if (Input.GetKey(KeyCode.A)) rigidbody.MovePosition(rigidbody.position - Vector3.right * speed * Time.deltaTime); } }
Tiếp theo thêm thành phần network view vào đối tượng player (Component> Miscellaneous> Network View) . Điều này cho phép ta gửi các gói dữ liệu qua mạng để đồng bộ các player . Các trường sẽ tự động đồng bộ thiết lập “reliable delta compressed”. Điều này nghĩa là dữ liệu được đồng bộ sẽ được gửi tự động nhưng chỉ khi giá trị của nó thay đổi . Vì vậy , ví dụ nếu bạn di chuyển player vị trí của bạn sẽ được đồng bộ trên server . Nếu bạn chọn "Off" là tắt tự đồng bộ đi thì tất cả bạn sẽ phải làm thủ công . Bây giờ ta chọn "reliable" sẽ giải thích sau . Ta lưu đối tượng player lại thành một prefab để sau này dùng .
Trong script NetworkManager thêm một biến đối tượng player , trong hàm SpawnPlayer ta sẽ khởi tạo prefab đó trên mạng , vì thế tất cả các máy khách sẽ nhìn thấy được đối tượng này trong màn hình game của họ một cách gần như lập tức.
Ta sẽ khởi tạo đối tượng player mới khi mà kết nối với server thành công hoặc khởi tạo server thành công:
public GameObject playerPrefab; void OnServerInitialized() { SpawnPlayer(); } void OnConnectedToServer() { SpawnPlayer(); } private void SpawnPlayer() { Network.Instantiate(playerPrefab, new Vector3(0f, 5f, 0f), Quaternion.identity, 0); }
Nếu lúc này chúng ta test game trên hai thiết bị thì có một vấn đề xảy ra là ta điều khiển được tất cả các đối tượng player .Vì vậy vấn đề cần giải quyết bây giờ là ai điều khiển đối tượng nào ?
Một cách để khắc phục vấn đề này là kiểm tra mã player vì nó chỉ nhận đầu vào từ người dùng mà nó khởi tạo . Bởi vì ta thiết lập đồng bộ hoá đáng tin cậy giữa các điểm mạng , dữ liệu được gửi tự động qua mạng và không có thông tin nào khác được yêu cầu . Để làm điều này ta kiểm tra xem đối tượng player thuộc tính "is mine" có nghĩa là có phải là tôi không trong hàm UpDate là được :
void Update() { if (networkView.isMine) { InputMovement(); } }
Nếu giờ test lại bạn sẽ thấy bạn chỉ điều khiển được một đối tượng .
Một giải pháp khác là có thể gửi tất cả các đầu vào cho các máy chủ, sau đó sẽ chuyển đổi dữ liệu của bạn để di chuyển vị trí thực tế và gửi trở lại vị trí mới của bạn với tất cả mọi người trên mạng. Ưu điểm là tất cả mọi thứ được đồng bộ trên máy chủ. Điều này ngăn cản các player gian lận từ local. Một bất lợi của phương pháp này là độ trễ giữa các máy khách và máy chủ, trong đó có thể dẫn đến việc người dùng phải chờ đợi để xem những hành động đang được thực hiện.
Đồng bộ State
Có hai phương pháp truyền thông mạng. Đầu tiên là State Synchronization cái nữa là Remote Procedure Call cái này sẽ được đề cập trong phần khác. State Synchronization liên tục cập nhật các giá trị qua mạng. Cách tiếp cận này là hữu ích cho các dữ liệu mà thay đổi thường xuyên, giống như di chuyển player. Trong hàm OnSerializeNetworkView () các biến thể được gửi hoặc nhận được và sẽ đồng bộ hóa chúng nhanh chóng và đơn giản. Để cho bạn thấy cách làm việc này, chúng ta sẽ viết code để đồng bộ vị trí của người chơi.
Chuyển đến phần network view trên prefab player. Các lĩnh vực quan sát có chứa các thành phần đó sẽ được đồng bộ. Các biến đổi được tự động thêm vào phần này, kết quả là các vị trí, góc quay và scale đang được cập nhật tùy thuộc vào sendrate.
Thêm hàm OnSerializeNetworkView () vào Player.cs. Chức năng này được gọi tự động mỗi khi nó có thể gửi hoặc nhận dữ liệu. Nếu người dùng đang viết các dòng, nó có nghĩa là anh ấy đang gửi dữ liệu. Bằng cách sử dụng stream.Serialize () biến sẽ được đăng và nhận bởi các khách hàng khác. Nếu người dùng nhận được các dữ liệu, hàm serialization được gọi và bây giờ có thể được thiết lập để lưu trữ dữ liệu tại local. Lưu ý rằng thứ tự của các biến cần được giữ như vậy cho việc gửi và nhận dữ liệu, nếu không thì giá trị này sẽ bị trộn lẫn.
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { Vector3 syncPosition = Vector3.zero; if (stream.isWriting) { syncPosition = rigidbody.position; stream.Serialize(ref syncPosition); } else { stream.Serialize(ref syncPosition); rigidbody.position = syncPosition; } }
interpolation
Bạn có thể thấy độ trễ giữa hai trường hợp do sendrate. Các thiết lập tiêu chuẩn trong Unity là một gói phần mềm đang được cố gắng để gửi 15 lần mỗi giây. Đối với mục đích thử nghiệm, ta sẽ thay đổi sendrate. Để làm điều này, đầu tiên ta đi đến các thiết lập network tại (Edit> Cài đặt Project> Network). Sau đó, thay đổi sendrate thành 5, kết quả là các gói dữ liệu được gửi đi ít hơn.
Để làm mịn quá trình di chuyển từ vị trí cũ đến vị trí mới và sửa chữa các vấn đề độ trễ, ta nên sử dụng nội suy.
OnSerializeNetworkView () cần phải được mở rộng với lưu trữ tất cả các dữ liệu cần thiết: các vị trí hiện tại, vị trí mới và sự chậm trễ giữa các lần update.
private float lastSynchronizationTime = 0f; private float syncDelay = 0f; private float syncTime = 0f; private Vector3 syncStartPosition = Vector3.zero; private Vector3 syncEndPosition = Vector3.zero; void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { Vector3 syncPosition = Vector3.zero; if (stream.isWriting) { syncPosition = rigidbody.position; stream.Serialize(ref syncPosition); } else { stream.Serialize(ref syncPosition); syncTime = 0f; syncDelay = Time.time - lastSynchronizationTime; lastSynchronizationTime = Time.time; syncStartPosition = rigidbody.position; syncEndPosition = syncPosition; } } void Update() { if (networkView.isMine) { InputMovement(); } else { SyncedMovement(); } } private void SyncedMovement() { syncTime += Time.deltaTime; rigidbody.position = Vector3.Lerp(syncStartPosition, syncEndPosition, syncTime / syncDelay); }
Prediction - dự đoán
Mặc dù quá trình di chuyển trông thì mịn, nhưng bạn sẽ nhận thấy một sự chậm trễ nhỏ giữa lúc ta ấn nút điều khiển so với di chuyển thực tế của đối tượng. Điều này là bởi vì vị trí được cập nhật sau khi dữ liệu mới được nhận. Cho đến khi chúng ta tìm ra được một cách hay hơn thì tất cả những gì chúng ta có thể làm là dự đoán những gì sẽ xảy ra dựa trên các dữ liệu cũ.
Một phương pháp để dự đoán vị trí tiếp theo là lấy vận tốc vào tài khoản. Một vị trí kết thúc chính xác hơn có thể được tính bằng cách cộng các vận tốc nhân với độ trễ.
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { Vector3 syncPosition = Vector3.zero; Vector3 syncVelocity = Vector3.zero; if (stream.isWriting) { syncPosition = rigidbody.position; stream.Serialize(ref syncPosition); syncVelocity = rigidbody.velocity; stream.Serialize(ref syncVelocity); } else { stream.Serialize(ref syncPosition); stream.Serialize(ref syncVelocity); syncTime = 0f; syncDelay = Time.time - lastSynchronizationTime; lastSynchronizationTime = Time.time; syncEndPosition = syncPosition + syncVelocity * syncDelay; syncStartPosition = rigidbody.position; } }
Remote Procedure Calls
Một phương pháp truyền thông mạng là cuộc gọi thủ tục từ xa (RPC), nó là hữu ích hơn cho dữ liệu không thay đổi liên tục. Một ví dụ tốt là những câu đàm thoại trong game. Trong đoạn này ta sẽ thay đổi màu sắc của một người chơi qua mạng.
RPC làm một cuộc gọi chức năng trên một thành phần Network view và thành phần này sẽ tìm kiếm các chức năng RPC tương ứng. Bằng cách thêm [RPC] ở phía trước của các hàm, nó có thể được gọi qua mạng. Cách tiếp cận này chỉ có thể gửi các số kiểu integers, floats, strings, networkViewIDs, vectors and quaternions. Vì vậy, không phải tất cả các tham số có thể được gửi đi, nhưng điều này có thể được giải quyết. Để gửi một đối tượng trò chơi, chúng ta nên thêm một thành phần network để xem đối tượng này vì vậy ta có thể sử dụng networkViewID của nó. Để gửi một màu sắc, chúng ta nên chuyển nó sang một vector hoặc quaternion.
một RPC được gửi bằng cách gọi hàm networkView.RPC (), trong đó bạn xác định tên hàm và tham số. Ngoài các chế độ RPC được yêu cầu: "Server" sẽ gửi dữ liệu đến máy chủ duy nhất, "Others " để tất cả mọi người trên máy chủ, ngoại trừ bản thân nó và "All" gửi nó đến tất cả mọi người. Hai cái cuối cùng có các chức năng để thiết lập được như bộ nhớ đệm, kết quả này trong các player mới được kết nối tiếp nhận tất cả các giá trị bộ nhớ đệm. Bởi vì ta gửi gói dữ liệu này mỗi sau khung hình, không có nhu cầu để lưu đệm nó.
Để tích hợp chức năng này vào trò chơi này hàm Update () cần phải gọi để kiểm tra đầu vào và thay đổi material cho một màu sắc ngẫu nhiên. Các hàm RPC thay đổi màu sắc dựa trên đầu vào và nếu đối tượng người chơi được điều khiển bởi người sử dụng, ta sẽ gửi một RPC cho tất cả những người khác trên mạng.
void Update() { if (networkView.isMine) { InputMovement(); InputColorChange(); } else { SyncedMovement(); } } private void InputColorChange() { if (Input.GetKeyDown(KeyCode.R)) ChangeColorTo(new Vector3(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f))); } [RPC] void ChangeColorTo(Vector3 color) { renderer.material.color = new Color(color.x, color.y, color.z, 1f); if (networkView.isMine) networkView.RPC("ChangeColorTo", RPCMode.OthersBuffered, color); }
OK tới đây là toàn bộ phần hướng dẫn các bạn tạo một game multiplayer .
Bạn có thể tải project tại đây về chạy thử
https://github.com/ngocdu/MulltiPlayerDemoUnity
Các tài liệu về làm game multiplayer các bạn có thể xem bên dươí:
http://forum.unity3d.com/threads/unity-5-unet-multiplayer-tutorials-making-a-basic-survival-co-op.325692/
https://www.youtube.com/watch?v=AIgwZK151-A&list=PLbghT7MmckI7BDIGqNl_TgizCpJiXy0n9&index=1