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<Text>();
scoreText = skin.transform.Find("InfoPanel/ScoreText").GetComponent<Text>();
createButton = skin.transform.Find("CtrlPanel/CreateButton").GetComponent<Button>();
reflashButton = skin.transform.Find("CtrlPanel/ReflashButton").GetComponent<Button>();
content = skin.transform.Find("ListPanel/Scroll View/Viewport/Content");
roomObj = skin.transform.Find("Room").gameObject;
//不激活房间
roomObj.SetActive(false);
//显示id
idText.text = GameMain.id;
//按钮事件
createButton.onClick.AddListener(OnCreateClick);
reflashButton.onClick.AddListener(OnReflashClick);
//协议监听
NetManager.AddMsgListener("MsgGetAchieve", OnMsgGetAchieve);
NetManager.AddMsgListener("MsgGetRoomList", OnMsgGetRoomList);
NetManager.AddMsgListener("MsgCreateRoom", OnMsgCreateRoom);
NetManager.AddMsgListener("MsgEnterRoom", OnMsgEnterRoom);
//发送查询
MsgGetAchieve msgGetAchieve = new MsgGetAchieve();
NetManager.Send(msgGetAchieve);
MsgGetRoomList msgGetRoomList = new MsgGetRoomList();
NetManager.Send(msgGetRoomList);
}
//关闭
public override void OnClose() {
//协议监听
NetManager.RemoveMsgListener("MsgGetAchieve", OnMsgGetAchieve);
NetManager.RemoveMsgListener("MsgGetRoomList", OnMsgGetRoomList);
NetManager.RemoveMsgListener("MsgCreateRoom", OnMsgCreateRoom);
NetManager.RemoveMsgListener("MsgEnterRoom", OnMsgEnterRoom);
}
//收到成绩查询协议
public void OnMsgGetAchieve (MsgBase msgBase) {
MsgGetAchieve msg = (MsgGetAchieve)msgBase;
scoreText.text = msg.win + "胜 " + msg.lost + "负";
}
//收到房间列表协议
public void OnMsgGetRoomList (MsgBase msgBase) {
MsgGetRoomList msg = (MsgGetRoomList)msgBase;
//清除房间列表
for(int i = content.childCount-1; i >= 0 ; i--){
GameObject o = content.GetChild(i).gameObject;
Destroy(o);
}
//重新生成列表
if(msg.rooms == null){
return;
}
for(int i = 0; i < msg.rooms.Length; i++){
GenerateRoom(msg.rooms[i]);
}
}
//创建一个房间单元
public void GenerateRoom(RoomInfo roomInfo){
//创建物体
GameObject o = Instantiate(roomObj);
o.transform.SetParent(content);
o.SetActive(true);
o.transform.localScale = Vector3.one;
//获取组件
Transform trans = o.transform;
Text idText = trans.Find("IdText").GetComponent<Text>();
Text countText = trans.Find("CountText").GetComponent<Text>();
Text statusText = trans.Find("StatusText").GetComponent<Text>();
Button btn = trans.Find("JoinButton").GetComponent<Button>();
//填充信息
idText.text = roomInfo.id.ToString();
countText.text = roomInfo.count.ToString();
if(roomInfo.status == 0){
statusText.text = "准备中";
}
else{
statusText.text = "战斗中";
}
//按钮事件
btn.name = idText.text;
btn.onClick.AddListener(delegate(){
OnJoinClick(btn.name);
});
}
//点击刷新按钮
public void OnReflashClick(){
MsgGetRoomList msg = new MsgGetRoomList();
NetManager.Send(msg);
}
//点击加入房间按钮
public void OnJoinClick(string idString) {
MsgEnterRoom msg = new MsgEnterRoom();
msg.id = int.Parse(idString);
NetManager.Send(msg);
}
//收到进入房间协议
public void OnMsgEnterRoom (MsgBase msgBase) {
MsgEnterRoom msg = (MsgEnterRoom)msgBase;
//成功进入房间
if(msg.result == 0){
PanelManager.Open<RoomPanel>();
Close();
}
//进入房间失败
else{
PanelManager.Open<TipPanel>("进入房间失败");
}
}
//点击新建房间按钮
public void OnCreateClick() {
MsgCreateRoom msg = new MsgCreateRoom();
NetManager.Send(msg);
}
//收到新建房间协议
public void OnMsgCreateRoom (MsgBase msgBase) {
MsgCreateRoom msg = (MsgCreateRoom)msgBase;
//成功创建房间
if(msg.result == 0){
PanelManager.Open<TipPanel>("创建成功");
PanelManager.Open<RoomPanel>();
Close();
}
//创建房间失败
else{
PanelManager.Open<TipPanel>("创建房间失败");
}
}
}
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<Button>();
closeButton = skin.transform.Find("CtrlPanel/CloseButton").GetComponent<Button>();
content = skin.transform.Find("ListPanel/Scroll View/Viewport/Content");
playerObj = skin.transform.Find("Player").gameObject;
//不激活玩家信息
playerObj.SetActive(false);
//按钮事件
startButton.onClick.AddListener(OnStartClick);
closeButton.onClick.AddListener(OnCloseClick);
//协议监听
NetManager.AddMsgListener("MsgGetRoomInfo", OnMsgGetRoomInfo);
NetManager.AddMsgListener("MsgLeaveRoom", OnMsgLeaveRoom);
NetManager.AddMsgListener("MsgStartBattle", OnMsgStartBattle);
//发送查询
MsgGetRoomInfo msg = new MsgGetRoomInfo();
NetManager.Send(msg);
}
//关闭
public override void OnClose() {
//协议监听
NetManager.RemoveMsgListener("MsgGetRoomInfo", OnMsgGetRoomInfo);
NetManager.RemoveMsgListener("MsgLeaveRoom", OnMsgLeaveRoom);
NetManager.RemoveMsgListener("MsgStartBattle", OnMsgStartBattle);
}
//收到玩家列表协议
public void OnMsgGetRoomInfo (MsgBase msgBase) {
MsgGetRoomInfo msg = (MsgGetRoomInfo)msgBase;
//清除玩家列表
for(int i = content.childCount-1; i >= 0 ; i--){
GameObject o = content.GetChild(i).gameObject;
Destroy(o);
}
//重新生成列表
if(msg.players == null){
return;
}
for(int i = 0; i < msg.players.Length; i++){
GeneratePlayerInfo(msg.players[i]);
}
}
//创建一个玩家信息单元
public void GeneratePlayerInfo(PlayerInfo playerInfo){
//创建物体
GameObject o = Instantiate(playerObj);
o.transform.SetParent(content);
o.SetActive(true);
o.transform.localScale = Vector3.one;
//获取组件
Transform trans = o.transform;
Text idText = trans.Find("IdText").GetComponent<Text>();
Text campText = trans.Find("CampText").GetComponent<Text>();
Text scoreText = trans.Find("ScoreText").GetComponent<Text>();
//填充信息
idText.text = playerInfo.id;
if(playerInfo.camp == 1){
campText.text = "红";
}
else{
campText.text = "蓝";
}
if(playerInfo.isOwner == 1){
campText.text = campText.text + " !";
}
scoreText.text = playerInfo.win + "胜 " + playerInfo.lost + "负";
}
//点击退出按钮
public void OnCloseClick(){
MsgLeaveRoom msg = new MsgLeaveRoom();
NetManager.Send(msg);
}
//收到退出房间协议
public void OnMsgLeaveRoom (MsgBase msgBase) {
MsgLeaveRoom msg = (MsgLeaveRoom)msgBase;
//成功退出房间
if(msg.result == 0){
PanelManager.Open<TipPanel>("退出房间");
PanelManager.Open<RoomListPanel>();
Close();
}
//退出房间失败
else{
PanelManager.Open<TipPanel>("退出房间失败");
}
}
//点击开战按钮
public void OnStartClick(){
MsgStartBattle msg = new MsgStartBattle();
NetManager.Send(msg);
}
//收到开战返回
public void OnMsgStartBattle (MsgBase msgBase) {
MsgLeaveRoom msg = (MsgLeaveRoom)msgBase;
//开战
if(msg.result == 0){
//等待战斗推送的协议
}
//开战失败
else{
PanelManager.Open<TipPanel>("开战失败!两队至少都需要一名玩家,只有队长可以开始战斗!");
}
}
}
服务器主要是实现Room和RoomManager。这两个类在名字上就很清楚它们的功能是什么,比较简单。
Room.cs
using System;
using System.Collections.Generic;
public class Room {
//id
public int id = 0;
//最大玩家数
public int maxPlayer = 6;
//玩家列表
public Dictionary<string, bool> playerIds = new Dictionary<string, bool>();
//房主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<int, Room> rooms = new Dictionary<int, Room>();
//创建房间
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的初学者还是很友好的,非常简单容易上手,代码也简单。最有用的应该是通用客户端网络框架和通用服务器框架那两章。对于网络通讯写的很好,清晰易懂。以及最后一章的同步方法。在书上学习的代码也能用于工作中,让自己上手更快了点。收获还是很大的。以后也要坚持看书,学习!