美文网首页
将Unity官方射击游戏 Space Shooter 改为实时联

将Unity官方射击游戏 Space Shooter 改为实时联

作者: 小5_小武君 | 来源:发表于2017-08-29 12:48 被阅读0次

    将Unity官方射击游戏(Space Shooter)改为实时对战小游戏,使用天梯实时对战服务(NanoLink)

    • io 类型游戏如此热门,有没有蠢蠢欲动?
    • 如何让自己的游戏快速实现可实时联机对战?
    • 开发实时对战游戏的时间成本?人力成本?

    经朋友介绍了解到天梯的实时对战服务(NanoLink),NanoLink 基于 UDP 的低延迟可靠连接,确保发送端数据尽快到达接收端。

    下面用 Unity官方实例 Space Shooter ,改成联机对战看看效果。
    对战截图:

    对战界面

    下面简单介绍下实现过程,有什么疑问大家可以一起探讨,欢迎莅临骚扰。


    感谢

    感谢天梯实时对战服务的商务小伙伴的热情帮忙及联机服务的支持。
    具体接口情况还是联系天梯服务商吧(QQ:2803816871),不多介绍。


    涉及同步数据

    • 玩家飞机位置同步
    • 掉落障碍物(石头,攻击性敌机)同步
    • 分值同步
    • 对战结束胜负同步
    • 存档回放(新功能)

    基本 UI 修改

    (先介绍一种数据同步 “玩家飞机位置同步“ 简要代码和逻辑吧,其它数据同步都是类似的,主要就是看游戏本身的数据同步点。大家有什么问题也可以留言,一起探讨,握手)

    主要就是:
    1,复制一架新的战机,分别命名为 Player-0, Player-1。这样命名也是为了跟 NanoLink 实时对战服务返回的玩家索引 getClientIndex 一致,方便控制当前玩家的战机。

    2,添加 UI 对象,也就是 UI -> Canvas。
    添加 4个 Button 入口,“等级匹配", "局域网匹配", "房间号匹配"。这也是 NanoLink 实时对战服务 支持的 3中匹配连接方式。

    Space Shooter UI 部分修改

    玩家飞机位置同步

    鼠标点击 Hierarchy 窗口的 Player-0 对象,可以看到 Space Shooter 战机控制脚本为 Done_PlayerController,下面重点看下这个脚本。( 简要代码 )

    1,2个玩家匹配成功后,射击。
    2,玩家控制战机的位置,发送数据到 NanoLink 协议

    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 优化,调整
    新版本SDK 对 NanoClient.cs 进行了分装优化,开发者只需要实现自己的 MyClient.cs 继承 NanoClient。
    然后,开发者只需要在代码中实现 已封装好的虚拟函数 (onMessage, onStatusChanged, onConnected, onDisconnected, onEvent )即可。
    看了下新版本 SDK, 理论上只需要 实现 onMessage, onEvent两个函数即可,比上一个版本方便多啦。

    MyClient.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    using Nanolink;
    
    // 游戏联网服务
    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 ("状态发生改变, 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 ("同步数据");
    
                    GameObject gameObj = GameObject.Find ("Player-" + getInt ("client-index"));
                    if(gameObj != null) {
                            Done_PlayerController player = gameObj.GetComponent<Done_PlayerController> ();
                            player.fireEvent ();
                    }
    
                    // 连接(或者 重新连接)后,发送随机函数数据
                    // 主机发送
                    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) {
                                            // 当前数据玩家索引 == clientIndex,为当前玩家的数据
                                            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":
                            // 处理接收到的 同步随机数种子命令
                            // 主要是  客机响应
                            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 支持存档回放功能,很赞,联机对战存档文件每分钟才几K,才几K,对才几K 。。。
    这样游戏实现存档回放简直太容易啦。下面分享的 Space Shooter 源码中也实现了 回放的功能。

    最后,展示几张实际联机对战截图,把实际发送的基本数据和实际的延迟展示在左上角,可以留意下。我这边显示延迟有时会在10毫秒左右,很夸张。做到这点,简直 666


    后续

    身为技术宅的你,如果也对实时对战感兴趣的话,我们可以一起探讨交流(本人QQ:836667502),非程勿扰。

    决定研究 NanoLink 的另一个重要的原因

    NanoLink 实时对战服务 有完整的数据统计后台,可以观察游戏的 当前在线人数,匹配连接人次,流量,具体某个地区数据,而且支持全球区服匹配,地区数据支持更详细的次均时长,延迟分布,流量分布等等 一系列的数据指标。还是上图吧:


    Nanolink 数据后台

    下面提供一个联机版本的Space Shooter 源码。 直接下载 .unitypackage 即可查看。

    (注意:有开发者跟我说下载.unitypackage,运行不能联机,这是因为之前分享的包,我去掉了个人的appKey, 应要求重新分享一个新版本,直接运行就可以联机啦。)
    (另外:编译到设备时,需要在 Unity 编辑器中配置 "File" - "Build Settings" - "Player Settings" - "Other Settings" - "Internet Access" 改为 Require;)

    Unity 工程包:
    链接: https://pan.baidu.com/s/1GGTi7lfy3izLSig64iVJtQ 提取码: ejnx

    安卓.apk包下载(两台安卓设备直接对战即可):
    链接: https://pan.baidu.com/s/1bpeuEjP 密码: syq6

    相关文章

      网友评论

          本文标题:将Unity官方射击游戏 Space Shooter 改为实时联

          本文链接:https://www.haomeiwen.com/subject/wpjodxtx.html