將Unity官方射擊游戲 Space Shooter 改為實時聯(lián)機對戰(zhàn)小游戲

將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)截圖:

對戰(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中匹配連接方式。

Space Shooter UI 部分修改

玩家飛機位置同步

鼠標點擊 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ù)指標坝咐。還是上圖吧:


Nanolink 數(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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鞍时,隨后出現(xiàn)的幾起案子亏拉,更是在濱河造成了極大的恐慌,老刑警劉巖逆巍,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件及塘,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒸苇,警方通過查閱死者的電腦和手機磷蛹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來溪烤,“玉大人味咳,你說我怎么就攤上這事庇勃。” “怎么了槽驶?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵责嚷,是天一觀的道長。 經常有香客問我掂铐,道長罕拂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任全陨,我火速辦了婚禮爆班,結果婚禮上,老公的妹妹穿的比我還像新娘辱姨。我一直安慰自己柿菩,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布雨涛。 她就那樣靜靜地躺著枢舶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪替久。 梳的紋絲不亂的頭發(fā)上凉泄,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音蚯根,去河邊找鬼后众。 笑死,一個胖子當著我的面吹牛稼锅,可吹牛的內容都是我干的吼具。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矩距,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了怖竭?” 一聲冷哼從身側響起锥债,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痊臭,沒想到半個月后哮肚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡广匙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年允趟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸦致。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡潮剪,死狀恐怖涣楷,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情抗碰,我是刑警寧澤狮斗,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站弧蝇,受9級特大地震影響碳褒,放射性物質發(fā)生泄漏。R本人自食惡果不足惜看疗,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一沙峻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧两芳,春花似錦专酗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疗隶,卻和暖如春佑笋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斑鼻。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工蒋纬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坚弱。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓蜀备,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荒叶。 傳聞我的和親對象是個殘疾皇子碾阁,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容

  • This article is a record of my journey to learn Game Deve...
    蔡子聰閱讀 3,781評論 0 9
  • 111. [動畫系統(tǒng)]如何將其他類型的動畫轉換成關鍵幀動畫? 動畫->點緩存->關鍵幀 112. [動畫]Unit...
    胤醚貔貅閱讀 13,014評論 3 90
  • 有些公司規(guī)定些楣,員工的工資必須保密脂凶,如果有誰要是泄露自己的工資給別人,也算是違反了員工條例愁茁,可能會受到處罰蚕钦。其實在某...
    楓丹白露蘇眉魚閱讀 288評論 0 3
  • 這幾年嘶居,運動健身成了娛樂圈明星們的又一圈粉手段,畢竟看到那些養(yǎng)眼的身材促煮,也給我了們跟隨偶像變得更好的動力邮屁。 你以為...
    辣八閱讀 851評論 1 2
  • 眾生抬頭向天望整袁,你在穹頂望蒼生 我造的詞,給很牛的一枚牛媧塑造的樱报。 對己葬项,細思量,不自量迹蛤,這么多年民珍,確實總是可以站...
    平行夏閱讀 217評論 1 0