Làm quen với Unity Networking API
Bài viết này sẽ cung cấp những kiến thức cơ bản về Unity Networking qua việc tạo 1 game nhỏ - Pong Game Source Project ở đây: https://github.com/TienHP/TechBlogSeptember.git I > Những kiến thức cơ bản Unity Networking API là bộ thư viện có sẵn của Unity hỗ trợ cho việc tạo game nhiều người ...
Bài viết này sẽ cung cấp những kiến thức cơ bản về Unity Networking qua việc tạo 1 game nhỏ - Pong Game Source Project ở đây: https://github.com/TienHP/TechBlogSeptember.git
I > Những kiến thức cơ bản
- Unity Networking API là bộ thư viện có sẵn của Unity hỗ trợ cho việc tạo game nhiều người chơi (Multiplayer), UNA xây dựng dựa trên Raknet - 1 networking engine cho game. Giao thức mà UNA hỗ trợ là UDP.
- Pong Game là 1 thể loại game kinh điển, cách chơi của nó gần giống như tennis hoặc bóng bàn hiện nay, Pong có cách chơi, đồ hoạ đơn giản, có thể chơi một hoặc 2 người.
pong
II> Các bước cần thực hiện để tạo game
- Mô hình của game sẽ là client - server, người chơi đầu tiên sẽ đóng vai trò là server, người chơi thứ 2 sẽ kết nối với địa chỉ ip của người chơi thứ nhất. Như vậy ta cần tạo 1 scene dạng lobby nơi mà người chơi sẽ host game hoặc join game. Để đơn giản hoá chúng ta tạo 1 empty object và add đoạn script sau:
public class ConnectToGame : MonoBehaviour { private string ip = ""; private int port = 25005; void OnGUI(){ if (isConnected) return; //let the user enter IP address GUILayout.Label("IP Address"); ip = GUILayout.TextField(ip, GUILayout.Width(200f)); //let the user enter port number GUILayout.Label("Port"); string port_str = GUILayout.TextField(port.ToString(), GUILayout.Width(100f)); int port_number = port; if (int.TryParse(port_str, out port_number)) port = port_number; //connect to the IP and port if (GUILayout.Button("Connect", GUILayout.Width(100f))){ Network.Connect(ip, port); }//end if //host a server on the given port, only allow 1 incoming if (GUILayout.Button("Host", GUILayout.Width(100.0f))){ Network.InitializeServer( 2, port, true); }//end if }//end method private bool isConnected = false; void OnConnectedToServer(){ Debug.Log("connected to server: "+Network.player.ipAddress); isConnected = true; NetworkLevelLoader.Instance.LoadLevel("Game"); }//end method void OnServerInitialized(){ Debug.Log("server initialized"); Debug.Log("connected to server: "+Network.player.ipAddress); NetworkLevelLoader.Instance.LoadLevel("Game"); }//end method void OnFailedToConnect(NetworkConnectionError error) { Debug.Log("Could not connect to server: " + error); }//end method }//end class
script này sẽ tạo 1 gui đơn giản cho phép người chơi host hoặc connect vào game, trong trường hợp muốn connect cần biết địa chỉ ip của máy host. Khi nhấn vào button host hoặc connect chúng ta thực hiện hành động 'NetworkLevelLoader.LoadLevel('Game')' method này được viết như sau:
IEnumerator doLoadLevel( string name, int prefix ) { Network.SetSendingEnabled( 0, false ); Network.isMessageQueueRunning = false; Network.SetLevelPrefix( prefix ); Application.LoadLevel( name ); yield return null; yield return null; Network.isMessageQueueRunning = true; Network.SetSendingEnabled( 0, true ); }
với nhiệm vụ là load level nhưng thay đổi Network.isMessageQueueRunning = false để disable việc xử lí các network message đến khi level mới load xong. OK, bước 1 với lobby đơn giản được tạo xong!
- Chúng ta sẽ tạo ra scene 'Game' để chơi game, những đối tượng tham gia vào game đó là : paddle(vợt), ball (bóng), goal (khung thành), wall (tường) và score (điểm số), với logic đơn giản như sau:
Người chơi sẽ điều khiển vợt lên trên hoặc xuống dưới sử dụng phím mũi tên trái phải. Bóng (ở giữa màn hình) -----di chuyển ngẫu nhiên---->-----chạm tường----->nảy theo hướng vuông góc ------chạm vợt-------->nảy theo hướng ngược lại ------chạm khung thành-------->tính điểm cho người chơi
một scene đơn giản được tạo như sau:
Screen Shot 2014-09-29 at 3.15.06 PM
- Giả sử 2 người chơi đã kết nối được với nhau và scene 'Game' đã được load, các đối tượng 'goal' 'ball' và 'wall' 'score' là cố định và được tạo từ trước. Công việc của chúng ta trước hết khởi tạo 'paddle' tại các vị trí cố định ở 2 đầu sân chơi.
void Start(){ if (Network.isServer){ Network.Instantiate(paddlePrefab, spawnP1.position, Quaternion.identity, 0); //nobody has joined yet, display "Waiting ..." for player 2 player2ScoreDisplay.text = Network.player.ipAddress; }//end if }//end method void OnPlayerConnected(NetworkPlayer player){ //when a player joins, tell them to spawn networkView.RPC("net_DoSpawn", player, spawnP2.position); //change player 2's score display from "waiting ..." to "0" player2ScoreDisplay.text = "0"; }//end method [RPC] void net_DoSpawn(Vector3 position){ Network.Instantiate(paddlePrefab, position, Quaternion.identity, 0); }//end method
Có 2 điểm cần chú ý đó là hàm Network.Instantiate khởi tạo 1 object trên tất cả các máy trong mạng, và hàm OnPlayerConnected() gọi tự động khi 1 player connect với máy host, nó đã được delay do việc set isQueueMessaging = false ở trên! Chú ý thêm rằng câu lệnh networkView chỉ chạy được nếu như object được gắn component NetworkView đây là component được xây dựng sẵn làm nhiệm vụ chính là đồng bộ hoá 1 component trên tất cả các máy.
Công đoạn tiếp theo là tạo script điều khiển paddle, mỗi khi người chơi connect vào thì có 1 paddle được tạo ra trên tất cả các máy do lệnh Network.Instantiate(..) do đó sẽ có paddle của người chơi và của người chơi khác trên cùng 1 máy, chúng ta cần xác định paddle nào sẽ nhận input:
void Start(){ ... acceptsInput = networkView.isMine; networkView.observed = this; ... }//end method
acceptsInput là biến bool để nhận biết paddle đó có điều khiển được hay không, tất nhiên các paddle cũng cần phải gắn NetworkView, networkview.ismine sẽ cho ta biết object gắn networkview có thực sự được tạo ra trên máy hiện tai hay không.
void Update(){ if (!acceptsInput){ transform.position = Vector3.Lerp(transform.position, readNetworkPos, 10.0f * Time.deltaTime); //dont use player input return ; }//end if //get user input float input = Input.GetAxis("Vertical"); //move the paddle Vector3 pos = transform.position; pos.z += input * moveSpeed * Time.deltaTime; //set position transform.position = pos; }//end method void OnSerializeNetworkView (BitStream stream, NetworkMessageInfo info){ if (stream.isWriting){ Vector3 pos = transform.position; stream.Serialize(ref pos); }//end if else{ Vector3 pos = Vector3.zero; stream.Serialize(ref pos); readNetworkPos = pos; }//end else }//end method
Hàm OnSerializeNetworkView(..) được gọi tự động bởi networkview để chạy trên tất cả các component có cùng 1 viewID, bản chất networkview sẽ theo dõi 1 component được gán là 'observed', các properties của component đó sẽ giống nhau trên tất cả các máy, ở đây chúng ta cần đồng bộ position nên có thể lựa chọn observed là transform, nhưng đó không phải lựa chọn tốt vì networkview không có thuộc tính lerp vị trí nên tốt hơn chúng ta đồng bộ MonoBehavior này và chạy hàm OnSerializedView để lấy vị trí và làm mượt như ở hàm update trên. Như vậy người chơi đã có thể điều khiển lên xuống bằng các phím lên xuống, ta có thể thấy vị trí các paddle thay đổi trên tất cả các máy.
- Công việc tiếp theo đó là tạo script di chuyển cho quả bóng
void Update(){ //dont move the ball if it's resetting if (resetting) return; //don't move the ball if there's nobody to play with if (Network.connections.Length == 0){ return; }//end if //move the ball in the direction Vector2 moveDir = currentDir * currentSpeed * Time.deltaTime; transform.Translate(new Vector3( moveDir.x, 0.0f, moveDir.y )); }//end method void OnTriggerEnter( Collider other ){ Debug.Log("in trigger :" + other.gameObject.tag); if (other.tag.ToLower() == "wall"){ currentDir.y *= -1; }//end if else if (other.tag.ToLower() == "player"){ currentDir.x *= -1; }//end else else if (other.tag.ToLower() == "goal"){ StartCoroutine( ResetBall() ); other.SendMessage("GetPoint", SendMessageOptions.DontRequireReceiver); }//end else //increase speed currentSpeed += speedInscrease; //clamp speed to maximum currentSpeed = Mathf.Clamp(currentSpeed, startSpeed, maxSpeed); }//end method void OnSerializeNetworkView( BitStream stream, NetworkMessageInfo info ){ if (stream.isWriting){ Vector3 pos = transform.position; Vector3 dir = currentDir; float speed = currentSpeed; stream.Serialize(ref pos); stream.Serialize(ref dir); stream.Serialize(ref speed); }//end if else{ Vector3 pos = Vector3.zero; Vector3 dir = Vector3.zero; float speed = 0f; stream.Serialize(ref pos); stream.Serialize(ref dir); stream.Serialize(ref speed); transform.position = pos; currentDir = dir; currentSpeed = speed; }//end else }//end method
Script làm nhiệm vụ xử lí di chuyển cho quả bóng, ta sử dụng 2 biến direction và speed, xử lí direction và speed không có gì đặc biệt, phụ thuộc vào đối tượng quả bóng chạm vào. Về công việc đồng bộ chúng ta cần đồng bộ vị trí, hướng và speed hiện tại là các thuộc tính thay đổi, về network không có gì đặc biệt. Xử lí vấn đề va chạm trong networking trong trường hợp này chúng ta sử dụng OnTriggerEnter, va chạm xảy ra trên tất cả các máy nên ta có thể update điểm số 1 cách độc lập như sau:
public void AddScore( int player ){ //networkView.RPC("net_AddScore", RPCMode.All, player); net_AddScore(player); }//end method public void net_AddScore( int player ){ if (player == 1){ p1Score++; } else if (player == 2){ p2Score++; } if (p1Score >= scoreLimit || p2Score >= scoreLimit){ if (p1Score > p2Score){ Debug.Log("player 1 wins"); } else if (p2Score > p1Score) Debug.Log("player 2 wins"); else Debug.Log("players are tied"); p1Score = 0; p2Score = 0; }//end if player1ScoreDisplay.text = p1Score.ToString(); player2ScoreDisplay.text = p2Score.ToString(); }//end method
Điểm mấu chốt ở đây là va chạm xảy ra trên tất cả các máy nên mỗi máy sẽ chịu trách nhiệm update kết quả trên máy đó, không hề có sự đồng bộ về điểm số trên các máy, ưu điểm của cách làm này là xử lí nhanh trong các trường hợp các đối tượng vật lý dễ kiểm soát, trong nhiều trường hợp chúng ta cần 1 cơ sở dữ liệu chung giữa các máy mới có thể cho kết quả chính xác.
III> Build và chạy thử Trước hết chúng ta build project trên platform PC/Mac, copy bản build ra 2 máy cùng trong 1 mạng Local Chạy bản build trên máy thứ nhất Screen Shot 2014-09-29 at 3.20.57 PM --> click chọn Host Game Screen Shot 2014-09-29 at 3.22.32 PM
địa chỉ IP của host sẽ được show ra chạy bản build ở máy thứ 2 nhập IP vào khung Ip Address và nhấn connect hình ảnh ở cả 2 máy như sau:
Screen Shot 2014-09-29 at 3.26.41 PM
Sử dụng các phím mũi tên lên xuông hoặc w, s để chơi game.