將Unity官方射擊游戲(Space Shooter)改為實時對戰(zhàn)小游戲钥屈,使用天梯實時對戰(zhàn)服務(NanoLink)
- io 類型游戲如此熱門晨炕,有沒有蠢蠢欲動务豺?
- 如何讓自己的游戲快速實現(xiàn)可實時聯(lián)機對戰(zhàn)驻售?
- 開發(fā)實時對戰(zhàn)游戲的時間成本?人力成本库北?
經朋友介紹了解到天梯的實時對戰(zhàn)服務(NanoLink),NanoLink 基于 UDP 的低延遲可靠連接们陆,確保發(fā)送端數(shù)據(jù)盡快到達接收端寒瓦。
下面用 Unity官方實例 Space Shooter ,改成聯(lián)機對戰(zhàn)看看效果坪仇。
對戰(zhàn)截圖:
下面簡單介紹下實現(xiàn)過程杂腰,有什么疑問大家可以一起探討,歡迎蒞臨騷擾椅文。
感謝
感謝天梯實時對戰(zhàn)服務的商務小伙伴的熱情幫忙及聯(lián)機服務的支持喂很。
具體接口情況還是聯(lián)系天梯服務商吧(QQ:2803816871),不多介紹皆刺。
涉及同步數(shù)據(jù)
- 玩家飛機位置同步
- 掉落障礙物(石頭少辣,攻擊性敵機)同步
- 分值同步
- 對戰(zhàn)結束勝負同步
- 存檔回放(新功能)
基本 UI 修改
(先介紹一種數(shù)據(jù)同步 “玩家飛機位置同步“ 簡要代碼和邏輯吧,其它數(shù)據(jù)同步都是類似的芹橡,主要就是看游戲本身的數(shù)據(jù)同步點毒坛。大家有什么問題也可以留言,一起探討林说,握手)
主要就是:
1煎殷,復制一架新的戰(zhàn)機,分別命名為 Player-0, Player-1腿箩。這樣命名也是為了跟 NanoLink 實時對戰(zhàn)服務返回的玩家索引 getClientIndex 一致豪直,方便控制當前玩家的戰(zhàn)機。
2珠移,添加 UI 對象弓乙,也就是 UI -> Canvas末融。
添加 4個 Button 入口,“等級匹配", "局域網匹配"暇韧, "房間號匹配"勾习。這也是 NanoLink 實時對戰(zhàn)服務 支持的 3中匹配連接方式。
玩家飛機位置同步
鼠標點擊 Hierarchy 窗口的 Player-0 對象懈玻,可以看到 Space Shooter 戰(zhàn)機控制腳本為 Done_PlayerController巧婶,下面重點看下這個腳本。( 簡要代碼 )
1涂乌,2個玩家匹配成功后艺栈,射擊。
2湾盒,玩家控制戰(zhàn)機的位置湿右,發(fā)送數(shù)據(jù)到 NanoLink 協(xié)議
Done_PlayerController.cs
void Update () {
// if (Input.GetButton("Fire1") && Time.time > nextFire)
// 連接上后自動開火
if (MyClient.isConnected() && Time.time > nextFire) {
nextFire = Time.time + fireRate;
// Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
GameObject obj = GameObject.Instantiate(shot, shotSpawn.position, shotSpawn.rotation) as GameObject;
obj.name = "Bolt-Player-" + playerIndex; // 當前飛機的id
GetComponent<AudioSource>().Play ();
}
// XXX 原 FixedUpdate
if(playerIndex != NanoClient.getInt ("client-index")) {
return;
}
bool bMoved = false;
// moveHorizontal = Input.GetAxis ("Horizontal");
// moveVertical = Input.GetAxis ("Vertical");
#region KeyboardEvents
float deltaLR = 0;
float deltaUD = 0;
// 鍵盤操作,用于電腦上調試
if (Input.GetKey (KeyCode.LeftArrow)) {
deltaLR = -DELTA;
} else if (Input.GetKey (KeyCode.DownArrow)) {
deltaUD = -DELTA;
} else if (Input.GetKey (KeyCode.RightArrow)) {
deltaLR = DELTA;
} else if (Input.GetKey (KeyCode.UpArrow)) {
deltaUD = DELTA;
}
if(deltaLR != 0 || deltaUD != 0) {
targetPosition = transform.position + new Vector3 (deltaLR, 0, deltaUD);
bMoved = true;
}
#endregion
#region TouchEvents
if(Input.touchCount == 1) {
Touch touch =Input.touches[0];
if(touch.phase == TouchPhase.Moved) {
Vector3 touchPosition = Camera.main.ScreenToWorldPoint (new Vector3 (touch.position.x, touch.position.y, 0));
float diffTime = Time.realtimeSinceStartup - lastMove;
float diffX = Mathf.Abs(touchPosition.x - targetPosition.x);
float diffZ = Mathf.Abs(touchPosition.z - targetPosition.z);
// 頻率控制
if(((diffX >= 0.25f || diffZ >= 0.25f) && diffTime >= 0.05f) || ((diffX > 0.1f || diffZ > 0.1) && diffTime > 0.1f)) {
targetPosition = new Vector3 (touchPosition.x, transform.position.y, touchPosition.z);
bMoved = true;
}
// moveHorizontal = Input.GetAxis("Mouse X");
// moveVertical = Input.GetAxis("Mouse Y");
}
}
#endregion
if (bMoved) {
lastMove = Time.realtimeSinceStartup;
fireEvent ();
}
}
void FixedUpdate () {
transform.position = Vector3.LerpUnclamped (transform.position, targetPosition, Time.deltaTime * speed); // 10
}
public void fireEvent() {
Hashtable values = new Hashtable ();
values.Add ("name", "move");
// 位置 position
values.Add ("x", targetPosition.x);
values.Add ("z", targetPosition.z);
MyClient.send (GameSerialize.toBytes(values));
}
3罚勾,新版Nanolink SDK 優(yōu)化毅人,調整
新版本SDK 對 NanoClient.cs 進行了分裝優(yōu)化,開發(fā)者只需要實現(xiàn)自己的 MyClient.cs 繼承 NanoClient荧库。
然后堰塌,開發(fā)者只需要在代碼中實現(xiàn) 已封裝好的虛擬函數(shù) (onMessage, onStatusChanged, onConnected, onDisconnected, onEvent )即可赵刑。
看了下新版本 SDK分衫, 理論上只需要 實現(xiàn) onMessage, onEvent兩個函數(shù)即可,比上一個版本方便多啦般此。
MyClient.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Nanolink;
// 游戲聯(lián)網服務
public class MyClient : NanoClient {
protected override void onMessage(byte[] data, byte fromIndex) {
Hashtable values = GameSerialize.fromBytes (data);
// 事件處理
onEvent (values, fromIndex);
}
protected override void onStatusChanged(string newStatus, string oldStatus) {
Debug.Log ("狀態(tài)發(fā)生改變, newStatus:" + newStatus + "; oldStatus:" + oldStatus);
}
protected override void onConnected() {
Debug.Log ("連接成功蚪战, playerIndex:" + getInt("client-index") + "; serverId:" + getString("server-id"));
// 連接上后,開火
}
protected override void onDisconnected(int error) {
if (error == 0) {
if (disconnectedBySelf)
Debug.Log ("主動斷開");
else
Debug.Log ("對方斷開");
} else {
// 錯誤代碼具體參考 "Nanolink SDK 接口說明" 中 lastError 定義
if (error == 501) {
if (getInt ("last-time", -2) < 2000)
Debug.Log ("超時斷開, 可能是對方原因");
else
Debug.Log ("超時斷開, 可能是己方原因");
}
}
// XXX 斷開連接后自動保存 存檔铐懊,用于 “回放上局”
if(getInt("mode") != 0) {
string archivesFilePath = "";
string archivesFileName = "archives_file.dat";
#if UNITY_EDITOR
archivesFilePath = Application.dataPath;
#else
archivesFilePath = Application.persistentDataPath;
#endif
save (archivesFilePath + "/" + archivesFileName);
}
// 斷開連接 重新reload 當前關卡
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
protected override void onResync(byte fromIndex) {
Debug.Log ("同步數(shù)據(jù)");
GameObject gameObj = GameObject.Find ("Player-" + getInt ("client-index"));
if(gameObj != null) {
Done_PlayerController player = gameObj.GetComponent<Done_PlayerController> ();
player.fireEvent ();
}
// 連接(或者 重新連接)后邀桑,發(fā)送隨機函數(shù)數(shù)據(jù)
// 主機發(fā)送
if(fromIndex == 0) {
Hashtable values = new Hashtable ();
values.Add ("name", "seed");
values.Add ("value", NanoRandom.Seed);
send (GameSerialize.toBytes(values));
}
}
void onEvent(Hashtable values, byte playerIndex) {
string name = (string)values["name"];
switch(name) {
case "move":
{
// int playerIndex = (NanoClient.getClientIndex () + 1) % 2;
GameObject gameObj = GameObject.Find ("Player-" + playerIndex);
if (gameObj != null) {
Done_PlayerController player = gameObj.GetComponent<Done_PlayerController> ();
player.onEvent (values);
}
}
break;
case "die":
{
GameObject gameObj = GameObject.Find ("Player-" + values["player"]);
if (gameObj != null) {
Done_PlayerController player = gameObj.GetComponent<Done_PlayerController> ();
player.onEvent (values);
}
}
break;
case "score":
{
GameObject gameControllerObj = GameObject.FindGameObjectWithTag ("GameController");
if (gameControllerObj == null)
return;
Done_GameController gameController = gameControllerObj.GetComponent<Done_GameController> ();
if (gameController == null)
return;
if(getInt("mode") == 0) {
// 當前數(shù)據(jù)玩家索引 == clientIndex,為當前玩家的數(shù)據(jù)
if((int)playerIndex == getInt("client-index")) {
gameController.AddScore ((int)values["score"]);
} else {
gameController.AddScore2 ((int)values["score"]);
}
} else {
gameController.AddScore2 ((int)values["score"]);
}
}
break;
case "seed":
// 處理接收到的 同步隨機數(shù)種子命令
// 主要是 客機響應
if(NanoClient.getInt ("client-index") == 1) {
NanoRandom.Seed = (long)values["value"];
}
break;
case "hazard":
{
GameObject gameControllerObj = GameObject.FindGameObjectWithTag ("GameController");
if (gameControllerObj == null)
return;
Done_GameController gameController = gameControllerObj.GetComponent<Done_GameController> ();
if (gameController == null)
return;
gameController.onEvent (values);
}
break;
default:
Debug.Log ("無效事件");
break;
}
}
4科乎,新功能:存檔回放
新版本SDK 支持存檔回放功能壁畸,很贊,聯(lián)機對戰(zhàn)存檔文件每分鐘才幾K茅茂,才幾K捏萍,對才幾K 。空闲。令杈。
這樣游戲實現(xiàn)存檔回放簡直太容易啦。下面分享的 Space Shooter 源碼中也實現(xiàn)了 回放的功能碴倾。
最后逗噩,展示幾張實際聯(lián)機對戰(zhàn)截圖掉丽,把實際發(fā)送的基本數(shù)據(jù)和實際的延遲展示在左上角,可以留意下异雁。我這邊顯示延遲有時會在10毫秒左右捶障,很夸張。做到這點纲刀,簡直 666
后續(xù)
身為技術宅的你残邀,如果也對實時對戰(zhàn)感興趣的話,我們可以一起探討交流(本人QQ:836667502)柑蛇,非程勿擾芥挣。
決定研究 NanoLink 的另一個重要的原因
NanoLink 實時對戰(zhàn)服務 有完整的數(shù)據(jù)統(tǒng)計后臺,可以觀察游戲的 當前在線人數(shù)耻台,匹配連接人次空免,流量,具體某個地區(qū)數(shù)據(jù)盆耽,而且支持全球區(qū)服匹配蹋砚,地區(qū)數(shù)據(jù)支持更詳細的次均時長,延遲分布摄杂,流量分布等等 一系列的數(shù)據(jù)指標坝咐。還是上圖吧:
下面提供一個聯(lián)機版本的Space Shooter 源碼。 直接下載 .unitypackage 即可查看析恢。
(注意:有開發(fā)者跟我說下載.unitypackage墨坚,運行不能聯(lián)機,這是因為之前分享的包映挂,我去掉了個人的appKey, 應要求重新分享一個新版本泽篮,直接運行就可以聯(lián)機啦。)
(另外:編譯到設備時柑船,需要在 Unity 編輯器中配置 "File" - "Build Settings" - "Player Settings" - "Other Settings" - "Internet Access" 改為 Require帽撑;)
Unity 工程包:
鏈接: https://pan.baidu.com/s/1GGTi7lfy3izLSig64iVJtQ 提取碼: ejnx
安卓.apk包下載(兩臺安卓設備直接對戰(zhàn)即可):
鏈接: https://pan.baidu.com/s/1bpeuEjP 密碼: syq6