《Unity网络游戏实战》终章: 坦克大战小游戏

1、介绍

在《Unity网络游戏实战》书的最后五个章节是制作一个小的多人对战游戏,坦克大战。这里就把东西都写在一起。做一个总结。

最后的五个章节是实现一个坦克大战游戏。游戏功能大致如下:
1、玩家注册游戏账号,登陆游戏。
2、进入游戏大厅,玩家可以创建房间,等待其他玩家加入房间。
3、玩家进入房间之后分配阵营,两边阵营人数相同并且最少有一人就可以开始游戏。
4、游戏过程很简单。地方坦克全灭就取得胜利。

因为章节太多,代码太长了。就不一一记录了。而且小游戏实现的功能确实比较简单。就只拿一下比较有用代码说一下。

2、大厅系统

大厅系统是玩家登录游戏之后进入游戏大厅,然后创建房间或加入已经存在的房间的操作。

客户端实现:
界面制作有大厅界面以及房间界面。
玩家登陆游戏之后,会向服务器发送MsgGetRoomList协议,获取当前存在的房间。客户端收到回应之后,就清空当前的所有RoomItem,根据服务器的回送创建新的RoomItem。

玩家点击进入房间,会向服务器发送MsgGetRoomInfo协议,获取当前进入的房间中所有玩家的信息,然后生成PlayerInfoItem。退出房间就向服务器发送MsgGetRoomList协议,显示房间列表。

客户端代码:
主要代码有:RoomListPanel和RoomPanel
RoomListPanel.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;


public class RoomListPanel : BasePanel {
    //账号文本
    private Text idText;
    //战绩文本
    private Text scoreText;
    //创建房间按钮
    private Button createButton;
    //刷新列表按钮
    private Button reflashButton;
    //列表容器
    private Transform content;
    //房间物体
    private GameObject roomObj;


    //初始化
    public override void OnInit() {
        skinPath = "RoomListPanel";
        layer = PanelManager.Layer.Panel;
    }

    //显示
    public override void OnShow(params object[] args) {
        //寻找组件
        idText = skin.transform.Find("InfoPanel/IdText").GetComponent();
        scoreText = skin.transform.Find("InfoPanel/ScoreText").GetComponent();
        createButton = skin.transform.Find("CtrlPanel/CreateButton").GetComponent

RoomPanel.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class RoomPanel : BasePanel {
    //开战按钮
    private Button startButton;
    //退出按钮
    private Button closeButton;
    //列表容器
    private Transform content;
    //玩家信息物体
    private GameObject playerObj;

    //初始化
    public override void OnInit() {
        skinPath = "RoomPanel";
        layer = PanelManager.Layer.Panel;
    }

    //显示
    public override void OnShow(params object[] args) {
        //寻找组件
        startButton = skin.transform.Find("CtrlPanel/StartButton").GetComponent

服务器主要是实现Room和RoomManager。这两个类在名字上就很清楚它们的功能是什么,比较简单。
Room.cs

using System;
using System.Collections.Generic;

public class Room {
    //id
    public int id = 0;
    //最大玩家数
    public int maxPlayer = 6;
    //玩家列表
    public Dictionary playerIds = new Dictionary();
    //房主id
    public string ownerId = "";
    //状态
    public enum Status {
        PREPARE = 0,
        FIGHT = 1 ,
    }
    public Status status = Status.PREPARE;


    //添加玩家
    public bool AddPlayer(string id){
        //获取玩家
        Player player = PlayerManager.GetPlayer(id);
        if(player == null){
            Console.WriteLine("room.AddPlayer fail, player is null");
            return false;
        }
        //房间人数
        if(playerIds.Count >= maxPlayer){
            Console.WriteLine("room.AddPlayer fail, reach maxPlayer");
            return false;
        }
        //准备状态才能加人
        if(status != Status.PREPARE){
            Console.WriteLine("room.AddPlayer fail, not PREPARE");
            return false;
        }
        //已经在房间里
        if(playerIds.ContainsKey(id)){
            Console.WriteLine("room.AddPlayer fail, already in this room");
            return false;
        }
        //加入列表
        playerIds[id] = true;
        //设置玩家数据
        player.camp = SwitchCamp();
        player.roomId = this.id;
        //设置房主
        if(ownerId == ""){
            ownerId = player.id;
        }
        //广播
        Broadcast(ToMsg());
        return true;
    }

    //分配阵营
    public int SwitchCamp() {
        //计数
        int count1 = 0;
        int count2 = 0;
        foreach(string id in playerIds.Keys) {
            Player player = PlayerManager.GetPlayer(id);
            if(player.camp == 1) {count1++;}
            if(player.camp == 2) {count2++;}
        }
        //选择
        if (count1 <= count2){
            return 1;
        }
        else{
            return 2;
        }
    }

    //是不是房主
    public bool isOwner(Player player){
        return player.id == ownerId;
    }

    //删除玩家
    public bool RemovePlayer(string id) {
        //获取玩家
        Player player = PlayerManager.GetPlayer(id);
        if(player == null){
            Console.WriteLine("room.RemovePlayer fail, player is null");
            return false;
        }
        //没有在房间里
        if(!playerIds.ContainsKey(id)){
            Console.WriteLine("room.RemovePlayer fail, not in this room");
            return false;
        }
        //删除列表
        playerIds.Remove(id);
        //设置玩家数据
        player.camp = 0;
        player.roomId = -1;
        //设置房主
        if(ownerId == player.id){
            ownerId = SwitchOwner();
        }
        //房间为空
        if(playerIds.Count == 0){
            RoomManager.RemoveRoom(this.id);
        }
        //广播
        Broadcast(ToMsg());
        return true;
    }

    //选择房主
    public string SwitchOwner() {
        //选择第一个玩家
        foreach(string id in playerIds.Keys) {
            return id;
        }
        //房间没人
        return "";
    }


    //广播消息
    public void Broadcast(MsgBase msg){
        foreach(string id in playerIds.Keys) {
            Player player = PlayerManager.GetPlayer(id);
            player.Send(msg);
        }
    }

    //生成MsgGetRoomInfo协议
    public MsgBase ToMsg(){
        MsgGetRoomInfo msg = new MsgGetRoomInfo();
        int count = playerIds.Count;
        msg.players = new PlayerInfo[count];
        //players
        int i = 0;
        foreach(string id in playerIds.Keys){
            Player player = PlayerManager.GetPlayer(id);
            PlayerInfo playerInfo = new PlayerInfo();
            //赋值
            playerInfo.id = player.id;
            playerInfo.camp = player.camp;
            playerInfo.win = player.data.win;
            playerInfo.lost = player.data.lost;
            playerInfo.isOwner = 0;
            if(isOwner(player)){
                playerInfo.isOwner = 1;
            }

            msg.players[i] = playerInfo;
            i++;
        }
        return msg;
    }
}


RoomManager.cs

using System;
using System.Collections.Generic;

public class RoomManager
{
    //最大id
    private static int maxId = 1;
    //房间列表
    public static Dictionary rooms = new Dictionary();

    //创建房间
    public static Room AddRoom(){
        maxId++;
        Room room = new Room();
        room.id = maxId;
        rooms.Add(room.id, room);
        return room;
    }

    //删除房间
    public static bool RemoveRoom(int id) {
        rooms.Remove(id);
        return true;
    }

    //获取房间
    public static Room GetRoom(int id) {
        if(rooms.ContainsKey(id)){
            return rooms[id];
        }
        return null;
    }

    //生成MsgGetRoomList协议
    public static MsgBase ToMsg(){
        MsgGetRoomList msg = new MsgGetRoomList();
        int count = rooms.Count;
        msg.rooms = new RoomInfo[count];
        //rooms
        int i = 0;
        foreach(Room room in rooms.Values){
            RoomInfo roomInfo = new RoomInfo();
            //赋值
            roomInfo.id = room.id;
            roomInfo.count = room.playerIds.Count;
            roomInfo.status = (int)room.status;

            msg.rooms[i] = roomInfo;
            i++;
        }
        return msg;
    }
}

RoomMsgHandle.cs

using System;


public partial class MsgHandler {
    
    //查询战绩
    public static void MsgGetAchieve(ClientState c, MsgBase msgBase){
        MsgGetAchieve msg = (MsgGetAchieve)msgBase;
        Player player = c.player;
        if(player == null) return;

        msg.win = player.data.win;
        msg.lost = player.data.lost;

        player.Send(msg);
    }


    //请求房间列表
    public static void MsgGetRoomList(ClientState c, MsgBase msgBase){
        MsgGetRoomList msg = (MsgGetRoomList)msgBase;
        Player player = c.player;
        if(player == null) return;

        player.Send(RoomManager.ToMsg());
    }

    //创建房间
    public static void MsgCreateRoom(ClientState c, MsgBase msgBase){
        MsgCreateRoom msg = (MsgCreateRoom)msgBase;
        Player player = c.player;
        if(player == null) return;
        //已经在房间里
        if(player.roomId >=0 ){
            msg.result = 1;
            player.Send(msg);
            return;
        }
        //创建
        Room room = RoomManager.AddRoom();
        room.AddPlayer(player.id);

        msg.result = 0;
        player.Send(msg);
    }

    //进入房间
    public static void MsgEnterRoom(ClientState c, MsgBase msgBase){
        MsgEnterRoom msg = (MsgEnterRoom)msgBase;
        Player player = c.player;
        if(player == null) return;
        //已经在房间里
        if(player.roomId >=0 ){
            msg.result = 1;
            player.Send(msg);
            return;
        }
        //获取房间
        Room room = RoomManager.GetRoom(msg.id);
        if(room == null){
            msg.result = 1;
            player.Send(msg);
            return;
        }
        //进入
        if(!room.AddPlayer(player.id)){
            msg.result = 1;
            player.Send(msg);
            return;
        }
        //返回协议  
        msg.result = 0;
        player.Send(msg);
    }


    //获取房间信息
    public static void MsgGetRoomInfo(ClientState c, MsgBase msgBase){
        MsgGetRoomInfo msg = (MsgGetRoomInfo)msgBase;
        Player player = c.player;
        if(player == null) return;

        Room room = RoomManager.GetRoom(player.roomId);
        if(room == null){
            player.Send(msg);
            return;
        }

        player.Send(room.ToMsg());
    }

    //离开房间
    public static void MsgLeaveRoom(ClientState c, MsgBase msgBase){
        MsgLeaveRoom msg = (MsgLeaveRoom)msgBase;
        Player player = c.player;
        if(player == null) return;

        Room room = RoomManager.GetRoom(player.roomId);
        if(room == null){
            msg.result = 1;
            player.Send(msg);
            return;
        }

        room.RemovePlayer(player.id);
        //返回协议
        msg.result = 0;
        player.Send(msg);
    }
}

如果是从头到尾实现一遍书上的内容的话,上面的代码还是比较简单的。

3、预测同步算法

在最后一章中作者介绍了服务器的同步算法。原理如下:
1、每个客户端每隔一段时间发送同步“帧”到服务器。
2、服务器更细玩家的位置、旋转信息并转发到各个客户端。
3、客户端收到同步信息之后,使用预测算法计算同步玩家的位置。

预测的方法也很简单。因为坦克是匀速运动的。因此预测位置只需要根据s = v * t就行了。
这里直接看代码就行,比较简单。
SyncMsg.cs

//同步坦克信息
public class MsgSyncTank:MsgBase {
    public MsgSyncTank() {protoName = "MsgSyncTank";}
    //位置、旋转、炮塔旋转
    public float x = 0f;        
    public float y = 0f;
    public float z = 0f;
    public float ex = 0f;       
    public float ey = 0f;
    public float ez = 0f;
    public float turretY = 0f;  
    //服务端补充
    public string id = "";      //哪个坦克
}

//开火
public class MsgFire:MsgBase {
    public MsgFire() {protoName = "MsgFire";}
    //炮弹初始位置、旋转
    public float x = 0f;        
    public float y = 0f;
    public float z = 0f;
    public float ex = 0f;
    public float ey = 0f;
    public float ez = 0f;
    //服务端补充
    public string id = "";      //哪个坦克
}

//击中
public class MsgHit:MsgBase {
    public MsgHit() {protoName = "MsgHit";}
    //击中谁
    public string targetId = "";
    //击中点   
    public float x = 0f;        
    public float y = 0f;
    public float z = 0f;
    //服务端补充
    public string id = "";      //哪个坦克
    public int hp = 0;          //被击中坦克血量
    public int damage = 0;      //受到的伤害
}

SyncTank.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SyncTank : BaseTank {
    //预测信息,哪个时间到达哪个位置
    private Vector3 lastPos;
    private Vector3 lastRot;
    private Vector3 forecastPos;
    private Vector3 forecastRot;
    private float forecastTime;

    //重写Init
    public new void Init(string skinPath){
        base.Init(skinPath);
        //不受物理运动影响
        rigidBody.constraints = RigidbodyConstraints.FreezeAll;
        rigidBody.useGravity = false;
        //初始化预测信息
        lastPos = transform.position;
        lastRot = transform.eulerAngles;
        forecastPos = transform.position;
        forecastRot = transform.eulerAngles;
        forecastTime = Time.time;
    }

    new void Update(){
        base.Update();
        //更新位置
        ForecastUpdate();
    }

    //移动同步
    public void SyncPos(MsgSyncTank msg){
        //预测位置
        Vector3 pos = new Vector3(msg.x, msg.y, msg.z);
        Vector3 rot = new Vector3(msg.ex, msg.ey, msg.ez);
        forecastPos = pos + 2*(pos - lastPos);
        forecastRot = rot + 2*(rot - lastRot);
        //更新
        lastPos = pos;
        lastRot = rot;
        forecastTime = Time.time;
        //炮塔
        Vector3 le = turret.localEulerAngles;
        le.y = msg.turretY;
        turret.localEulerAngles = le;
    }


    //更新位置
    public void ForecastUpdate(){
        //时间
        float t =  (Time.time - forecastTime)/CtrlTank.syncInterval;
        t = Mathf.Clamp(t, 0f, 1f);
        //位置
        Vector3 pos = transform.position;
        pos = Vector3.Lerp(pos, forecastPos, t);
        transform.position = pos;
        //旋转
        Quaternion quat = transform.rotation;
        Quaternion forcastQuat = Quaternion.Euler(forecastRot);
        quat = Quaternion.Lerp(quat, forcastQuat, t) ;
        transform.rotation = quat;
    }

    //开火
    public void SyncFire(MsgFire msg){
        Bullet bullet = Fire();
        //更新坐标
        Vector3 pos = new Vector3(msg.x, msg.y, msg.z);
        Vector3 rot = new Vector3(msg.ex, msg.ey, msg.ez);
        bullet.transform.position = pos;
        bullet.transform.eulerAngles = rot;
    }
}

战斗管理类中加入:

    //收到同步协议
    public static void OnMsgSyncTank(MsgBase msgBase){
        MsgSyncTank msg = (MsgSyncTank)msgBase;
        //不同步自己
        if(msg.id == GameMain.id){
            return;
        }
        //查找坦克
        SyncTank tank = (SyncTank)GetTank(msg.id);
        if(tank == null){
            return;
        }
        //移动同步
        tank.SyncPos(msg);
    }

    //收到开火协议
    public static void OnMsgFire(MsgBase msgBase){
        MsgFire msg = (MsgFire)msgBase;
        //不同步自己
        if(msg.id == GameMain.id){
            return;
        }
        //查找坦克
        SyncTank tank = (SyncTank)GetTank(msg.id);
        if(tank == null){
            return;
        }
        //开火
        tank.SyncFire(msg);
    }

    //收到击中协议
    public static void OnMsgHit(MsgBase msgBase){
        MsgHit msg = (MsgHit)msgBase;
        //查找坦克
        BaseTank tank = GetTank(msg.targetId);
        if(tank == null){
            return;
        }
        //被击中
        tank.Attacked(msg.damage);
    }

服务器主要做的就是转发功能。并没有做校验。相对简单。就不赘述了。

4、结语

原本打算花个十来天把这本书看完的。但是没想到花了差不多一个多月。《Unity3D网络游戏实战》这本书对于接触u3d的初学者还是很友好的,非常简单容易上手,代码也简单。最有用的应该是通用客户端网络框架和通用服务器框架那两章。对于网络通讯写的很好,清晰易懂。以及最后一章的同步方法。在书上学习的代码也能用于工作中,让自己上手更快了点。收获还是很大的。以后也要坚持看书,学习!

你可能感兴趣的:(《Unity网络游戏实战》终章: 坦克大战小游戏)