Unity联网对战游戏小Demo

前言

开发3D游戏听起来门槛很高,但是Unity的出现让门槛大大降低。开发联网实时对战的3D游戏门槛就更高,因为即便熟悉掌握了Unity的开发技术,联网的游戏还要涉及到熟悉网络协议栈、掌握后端知识以及面对服务器带来的高额成本。但是Bmob最近在内测一款游戏sdk,让普通开发者开发一款联网实时对战游戏这个梦想变得触手可及。


第一步,准备一个单机的Unity游戏

访问 Unity 的 Asset Store下载游戏项目,并且Import到 Unity 内。

我选择了一款可爱的射击打怪游戏:Survival Shooter Tutorial

游戏图片

项目导入后是这个样子的:
项目图片

下面简要介绍项目结构:

  • 场景上的摆设物体都包括在Environment上,比如图上的闹钟、柜子等,它们的Layer都设置为了Shootable字段,代码在玩家开激光枪、激光碰撞检测时检测到Shootable为Layer的物体才会触发碰撞事件。

  • 怪物的生成由EnemyManage来控制,能够管理何种怪物在哪个出生点按什么时间间隔出生。

  • 怪物主要由三个脚本来管理,分别是EnemyHealth.cs(管理怪物的hp,被玩家射击到时扣血,血量小于等于0时死掉)、EnemyMovement.cs(管理怪物的移动,用UnityEngine.AI.NavMeshAgent,把玩家的坐标设为目的地,这样怪物会按设置的行走速度自动走向玩家位置)和EnemyAttack.cs(管理怪物的自动攻击,按一定的时间间隔进行发招)。

  • 玩家也主要由三个脚本来管理,分别是PlayerHealth.cs(管理玩家的hp,主要是收到怪物攻击时掉血)、PlayerMovement.cs(管理玩家的移动,当键盘按wsad时上下左右的移动)和PlayerShooting.cs(管理玩家的射击动作,当鼠标左键点击时对玩家枪口面对的方向发出激光Ray,如果在范围内碰撞检测到了怪物则对怪物进行扣血)。

这里不说太详细,想学习更多细节的童鞋可以去看Unity官方的教程


第二步,开始改造

将玩家角色(和角色控制器)克隆一份,去掉主动操作行为,添加被动展现方法。

可以在上一步骤中看到项目中只有一个玩家Player,要改造成联网的游戏就需要多个玩家,所以我把场景中的Player物体克隆一份,命名为Player2,当然控制它的脚本也不能少,克隆克隆克隆!

  1. Player2Health.cs:
    因为Player2Health控制的是其他玩家的血量,所以把玩家收到怪物攻击时减的血量设为0,让其他玩家的血量不受本地控制。
  2. Player2Movement.cs:
    删掉根据键盘的操作引起的移动,加上传入数据时的操作角色移动方法。

    //  移动到相应坐标
    public void MoveTo(float x, float z){
        playerRigidbody.MovePosition (new Vector3(x, playerRigidbody.position.y,z));
        Animating (x, z);
    }
    
    //  旋转到相应角度
    public void TurnTo(float y){
        transform.eulerAngles = new Vector3 (0, y, 0);
    }
  3. Player2Shooting.cs:
    删掉根据鼠标的操作引起的射击,加上传入射击指令时的方法,考虑到网络延时原因,是否射中怪物的判断不由这里判断,这个射击方法给怪物的伤害要设置为0,仅显示出UI效果。

以上改好后再把对应的脚本替换掉原脚本放到Player2物体上,将物体拖进Project一栏中,这样物体就变成预设物体,可以随时调用啦。除了克隆、修改玩家之外,还需要修改一些细节:

  • 更改怪物的移动方式:
    上面我们有提到怪物是根据玩家的位置来自动寻路的,那现在有两个玩家了怎么办呢?根据玩游戏的经验告诉我们,怪物会跟着离他更近的玩家走哟。下面贴出代码:
// UnityEngine.AI.NavMeshAgent nav;
// nav = GetComponent <UnityEngine.AI.NavMeshAgent> ();
void Update ()
{
    // If the enemy and the player have health left...
    if(enemyHealth.currentHealth > 0)
    {
        Vector3 enemyPosition = transform.position, 
                tempPosition;
        float minDist = float.MaxValue, 
                tempFloat;
        Vector3 target = Vector3.zero;
        for (int i = 0; i < targetHealths.Length; i++) {
            if (targetHealths [i].currentHealth > 0) {
                tempPosition = trackTargets [i].position;
                tempFloat = Vector3.Distance (enemyPosition, tempPosition);
                if (tempFloat < minDist) {
                    minDist = tempFloat;
                    target = tempPosition;
                }
            }
        }
        if (minDist != float.MaxValue) {
            // ... set the destination of the nav mesh agent to the player.
            nav.SetDestination (target);
        }
    }
    // Otherwise...
    else
    {
        // ... disable the nav mesh agent.
        nav.enabled = false;
    }
}
  • 更改怪物受到伤害减血的触发方式:
    上面提到,其他玩家射击到怪物的事件不能在我这里减血,那么怪物的血量怎么控制呢,我把EnemyManager.cs的脚本改了下,把生成的每个怪物都命名,当检测到射击时,把我伤害的怪物的名称发送给其他玩家,就能同步好每个怪物的血量了。

第三步,结合Bmob Game Sdk

  1. 访问 BGS官网,注册账号并下载 Unity SDK、GameCloud SDK;
  2. 将 BmobGame_UnitySDK_vx.x.x_xxxxxx.unitypackage Import 到Unity内;
  3. 修改SDK,将游戏开始跳转的 Scene 改为本游戏的场景”_Complete-Game/_Complete-Game”;

    SceneManager.LoadSceneAsync ("_Complete-Game/_Complete-Game");
  4. 在 Demo Scene 进行SDK的初始化,绑定 delegate 用于处理各种通知;

  5. 将处理事件转发的脚本绑定给本地角色Player,将Player的移动、旋转、hp等数据,调用SDK接口同步到服务器;

    void Update ()
    {
        BmobGame.UpdateFrame ();
        if (isOver)
            return;
        Vector3 position = transform.position;
    
        BmobGame.EditMyStatus ("position", new float[]{ position.x, position.z });
        BmobGame.EditMyStatus ("rotation", transform.eulerAngles.y);
        BmobGame.EditMyStatus ("hp", GetComponent<PlayerHealthBase>().currentHealth<0?0:GetComponent<PlayerHealthBase>().currentHealth);
    }
  6. 将 Player 的瞬时动作射击、射中的怪物名通过transfer接口直接发送到其他玩家;

    // Game_BmobSDKTest里面SendFireEvent和SendDamageEvent方法,
    // 都是把传来的参数转成byte数组(数组第一位设为事件类别),
    // 通过transfer接口传递数组给其他玩家:BmobGame.SendTransferToAllExceptSelf (notify);
    
    // Game_BmobSDKTest mBGS;
    // mBGS = GetComponentInParent<Game_BmobSDKTest> ();
    
    // 把发射起点、角度、长度,用transfer接口传
    mBGS.SendFireEvent (transform.position.x, transform.position.z, transform.eulerAngles.y, range);
    
    // 把射中的怪物名发送出去,用transfer接口传
    mBGS.SendDamageEvent (shootHit.collider.name);
  7. 读取服务器同步的数据,渲染其它玩家的位置、角度。获取其它玩家直接发送的瞬时动作,作出射击和射中某个怪物的处理;

    //对收到其他玩家信息的处理
    void OnOthersGameStatus (int no, ArrayList attrNames, Hashtable status)
    {
        Debug.Log ("Player[" + no + "] game status is changed: " + status.Count);
    
        if(attrNames.Contains("position")){
            float[] position = status ["position"] as float[];
            mOtherPlayers [no].GetComponent<Player2Movement> ().MoveTo (position [0], position [1]) ;
        }
        if(attrNames.Contains("rotation")){
            float y = (float)(status ["rotation"]);
            mOtherPlayers [no].GetComponent<Player2Movement> ().TurnTo (y) ;
        }
        if(attrNames.Contains("hp")){
            int hp = (int)(status ["hp"]);
            mOtherPlayers [no].GetComponent<Player2Health> ().currentHealth = hp;
        }
    }
    
    //对收到transfer接口的信息的处理
    void OnTransfer (int fromNo, byte[] data)
    {
        Debug.Log ("Get transfer data flag = " + data[0] + " & len = " + data.Length + " & from: " + fromNo);
        switch(data[0]){
        case 1:
            ReceiveFireEvent (fromNo, data);//开火事件
            break;
        case 2:
            ReceiveDamageEvent (fromNo, data);//击中怪物事件
            break;
        }
        Debug.Log ("Player[" + fromNo + "] transfer: " + data [0] + ", len = " + data.Length);
    }
    
    //对收到云端通知的处理
    void OnCloudNotifyJson(string jsonStr){
        Debug.Log ("Handle cloud notify: " + jsonStr);
        JSONNode json = JSON.Parse (jsonStr);
        if (json == null) {
            return;
        }
        string a = json ["action"];
        if (a == null || a.Length == 0) {
            return;
        }
        if ("gameover".Equals (a)) {
            // 游戏结束,3秒后回到房间
            Invoke ("BackToRoom", 3);
        }
    }
  8. 在 BGS官网 登录管理后台,创建游戏,修改服务器运行配置,包括:每秒帧率(默认60Hz)、房间最多玩家数 (2个或以上);

  9. 修改 玩家属性配置,设置各个属性的名称、类型、长度、值域、由云端/客户端编辑、其它玩家是否可见等。我这里仅有hp、position和rotation。
"player": {                         // 玩家的相关信息
    "attributes": {                 // 玩家在游戏内的属性,下面的都是示例,实际情况由开发者自定义
        "hp": {                     // 玩家的HP    
            "type": "int",          // HP属性类型为数字
            "max": 101              // HP的上限,int类型的属性,都可以设置其max,设置得越紧密,运行效率越高
        },
        "position": {
            "type": "float[]",
            "count": 2,
            "editable": true,
            "export": true
        },
        "rotation": {
            "type": "float",
            "editable": true,
            "export": true
        }
    }
}

10 . 打开 Eclipse 或 Android Studio,创建Java项目,导入 BmobGame_JavaCloud_vx.x.x_xxxxxx.jar,并创建 Player.java 和 Room.java,分别继承自 PlayerBase.class 和 RoomBase.class 后,编写游戏逻辑代码。

Player.java :

package cn.bmob.gamesdk.server.been;

import cn.bmob.gamesdk.server.api.BmobGameSDKHook;
import cn.bmob.gamesdk.server.api.JSON;
import cn.bmob.gamesdk.server.api.PlayerBase;

public class Player extends PlayerBase {
    @BmobGameSDKHook
    public native int getHp();

    @BmobGameSDKHook
    public strictfp void onUpdate_Hp() {
        for (Player p : roommates)
            if (p.getHp() != 0)
                return;
        gameOver();
    }

    private final void gameOver() {
        sendToAll(JSON.toJson("action", "gameover").toString().getBytes());
        room.dispatchGameOver();
    }
}

Room.java :

package cn.bmob.gamesdk.server.been;

import cn.bmob.gamesdk.server.api.RoomBase;

public class Room extends RoomBase{
}

11 . 打包运行游戏,就可以多人同时在线对战啦~

运行效果

Demo测试运行视频 (B站无广告传送门)

怎么样,会了吗?
不服来战,不懂来问!
内测中,感兴趣加官方群:726133616

其他教程:

放大招!!!落地成盒?教你开发自己的联网”吃鸡”游戏
如何实现各种游戏的思路杂想

回合制自己写的 战斗DEMO 加动画状态 Q键移动并攻击。 代码很简单。不要抱有太大希望 。作为新手学习使用。 public enum HeroStatus { idle = 0, //空闲 attack, //攻击 other, //其他 hit, //受击 die, //死亡 defense, //防御 cast, //施法 miss, //闪避 seriousInjury, //重伤 move, //移动 exit, //获取下一个状态 MAX, } public class hero : MonoBehaviour { public UISpriteAnimation m_spriteAnimation = null; public HeroStatus m_staus = HeroStatus.exit; //身体碰撞盒 public BoxCollider m_bodyBC = null; public string m_name = ""; public AttackCrash m_attackCrash = null; public byte m_posIndex = 0; void Awake() { m_spriteAnimation = transform.GetComponent(); m_bodyBC = transform.GetComponent(); } // Use this for initialization void Start () { m_spriteAnimation.AddFrameCallBack("attack", 1, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 4, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 7, AttackCallBack); } public float m_speed = 1f; public float m_offset = 0.5f; public Vector3 m_destination = Vector3.zero; public List m_actionList = new List(); // Update is called once per frame void Update () { switch (m_staus) { case HeroStatus.idle: //空闲状态 if (m_actionList.Count > 0) m_staus = HeroStatus.exit; break; case HeroStatus.attack: if (!m_spriteAnimation.isPlaying) { //攻击动画播放完毕 m_staus = HeroStatus.exit; } break; case HeroStatus.defense: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.hit: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.die: break; case HeroStatus.cast: break; case HeroStatus.miss: break; case HeroStatus.seriousInjury: break; case HeroStatus.move: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.exit: //获取下一个状态 if (m_actionList.Count > 0) { string str = "idle"; switch(m_actionList[0].status) { case HeroStatus.move: str = "idle"; break; default: str = Enum.GetName(typeof(HeroStatus), m_actionList[0].status); break; } m_spriteAnimation.namePrefix = str; m_spriteAnimation.loop = m_actionList[0].loop; m_destination = m_actionList[0].destinationMove; m_speed = m_actionList[0].speed; m_spriteAnimation.ResetToBeginning(); m_staus = m_actionList[0].status; m_actionList.RemoveAt(0); } else { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } break; // case HeroStatus.moveback: // //transform.position = Vector3.SmoothDamp(transform.position, destinationMove, ref cameraVelocity, smoothTime); // transform.position = Vector3.MoveTowards(transform.position, destinationMove, m_speed * Time.deltaTime); // if (transform.position == destinationMove) // m_staus = HeroStatus.idle; // break; } } public void SetPosition(byte pos, float x, float y) { m_posIndex = pos; transform.localPosition = new Vector3(x, y); } public void AttackCallBack() { //创建攻击特效 A攻击B B掉血222 B反击A闪避 UnityEngine.Object sourceObj = Resources.Load("AttackCrash"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(-70, 0, 0); } private void OnCollisionEnter(Collision co) { //进入碰撞 Debug.Log("进入碰撞!"); UnityEngine.Object sourceObj = Resources.Load("Effect"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(0, 0, 0); //Defense(); Hit(); } public void Attack(GameObject aims) { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.move; tmpAD.loop = true; UISprite tmpS = transform.GetComponent(); tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(aims.transform.localPosition.x + (tmpS.width/2), aims.transform.localPosition.y)); tmpAD.speed = Vector3.Distance(transform.position, tmpAD.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.attack; tmpAD1.loop = false; tmpAD1.destinationMove = Vector3.zero; tmpAD1.speed = 0; m_actionList.Add(tmpAD1); ActionData tmpAD2 = new ActionData(); tmpAD2.status = HeroStatus.move; tmpAD2.loop = true; tmpAD2.destinationMove = transform.position; tmpAD2.speed = Vector3.Distance(tmpAD.destinationMove, tmpAD2.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD2); } public void Defense() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.defense; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.25f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Hit() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.hit; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.3f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Idle() { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } public void Move() { // //transform.GetComponent().depth = 99; // GameObject go = GameObject.Find("enemy").gameObject; // destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(go.transform.localPosition.x + 98,go.transform.localPosition.y,go.transform.localPosition.z)); // m_speed = Vector3.Distance(transform.position, destinationMove) * 4; //4/1秒到达目的地 // m_staus = HeroStatus.moveto; // m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); // m_spriteAnimation.loop = true; // m_spriteAnimation.ResetToBeginning(); }
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值