今天实例是在之前的基础上添加了新的功能,所以不止简单的字符串通信,可以说得上是我们联机游戏中角色的信息同步功能的缩影(只是当前例子只有简单实现而已)
实现效果
先说说咱的实现效果,其实也就是两个客户端联机,在场景中出现两个胶囊体,不同客户端操作的不同胶囊体,实现两胶囊体的位置信息同步而已,所以也就不贴图了。
效果不重要,毕竟才基础,里面的原理才是我们需要了解的~
实现原理
简单描述下原理: 每当一个客户端连接服务器的时候会获得当前客户端接入的顺序的序列号,这里可以是1、2、3... ,我这里默认只连接两个客户端就可以了,客户端一旦接入,便在场景中实例化两个胶囊体对象,且在客户端第一次连接服务器的时候就根据服务器返回的序列号进行设定当前客户端控制的胶囊体对象,这样在对胶囊体控制行为的时候就有了判断,一旦满足两个客户端接入,那么胶囊体分别能被两个客户端进行准确控制,程序一旦运行,客户端则以一定的频率将自身胶囊体位置坐标发送到服务器,服务器接收后转发当前客户端位置信息给其他接入的客户端,而其他客户端则会对信息进行解析及同步胶囊体信息,以此达到两者的信息同步~
服务端搭建
服务端跟之前的差不太多,只有个别地方的改动
Program.cs
using System;
class Program
{
static void Main(string[] args)
{
Server server = new Server("127.0.0.1",8899);
server.Start();
Console.ReadKey();
}
}
Server.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
class Server
{
private Socket serverSocket;
private Socket clientSocket;
private IPAddress ipAddress;
private IPEndPoint ipEndPoint;
private List<Client> clientList = new List<Client>();
public Server(string ip,int port)
{
//创建Socket
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//声明ip地址和端口号
this.ipAddress = IPAddress.Parse(ip);
this.ipEndPoint = new IPEndPoint(ipAddress, port);
}
public void Start()
{
//Socket绑定IP和端口号
serverSocket.Bind(ipEndPoint);
//Socket开始监听
serverSocket.Listen(0);
Console.WriteLine("服务器已经启动");
//客户端接入
serverSocket.BeginAccept(AcceptCallback,null);
}
private void AcceptCallback(IAsyncResult ar)
{
clientSocket = serverSocket.EndAccept(ar);
//获取客户端名称
string clientName = clientSocket.RemoteEndPoint.ToString();
Client client = new Client(clientSocket, clientName);
client.Start();
Console.WriteLine(clientName + "已经登录。。。");
//将连接上的客户端记录在案
clientList.Add(client);
//并且每连上一个客户端便给予当前客户端一个标记
//这里的flag是给先后连上服务器的客户端进行的编号
//输出给客户端是为了让客户端知道顺序,从而进行当前客户端来选择控制对象
client.SendMessage(""+flag);
flag++;
//继续循环监听客户端的连接
serverSocket.BeginAccept(AcceptCallback, null);
}
}
Server有较小的改动就是在服务器接收客户端连接的时候会分配一个代表着客户端接入的序列号给客户端。
Client.cs
using System;
using System.Text;
using System.Net.Sockets;
class Client
{
private Socket clientSocket;
private string clientName;
private byte[] recieveData;
public Client(Socket client,string clientName)
{
this.clientSocket = client;
this.clientName = clientName;
recieveData = new byte[clientSocket.ReceiveBufferSize];
}
public void Start()
{
clientSocket.BeginReceive(recieveData,0, clientSocket.ReceiveBufferSize,SocketFlags.None, RecieveCallback, null);
}
//接收客户端的消息
private void RecieveCallback(IAsyncResult ar)
{
try
{
//接收到的数据长度
int count = clientSocket.EndReceive(ar);
//防止客户端异常退出
if (count == 0)
{
clientSocket.Close();
return;
}
//对接收到的数据进行处理
string msgRec = Encoding.UTF8.GetString(recieveData, 0, count);
HandleResponse(msgRec);
//输出到控制台
Console.WriteLine(msgRec);
//循环接收客户端发送过来的数据
clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RecieveCallback, null);
}
catch (Exception)
{
if (clientSocket != null)
{
clientSocket.Close();
return;
}
}
}
//对客户端返回过来的数据进行处理
private void HandleResponse(string data)
{
//进行数据解析的判断,如果不包含flag标记的数据不执行之后代码
if (data.EndsWith("*")) return;
//只有一个客户端的时候不同步信息
clientList = server.clientList;
if (clientList.Count < 2) return;
//将当前客户端传送过来的同步信息转发给其他客户端进行同步
foreach (Client item in clientList)
{
if (item != this)
{
Console.WriteLine("转发消息 "+data);
item.SendMessage(data);
}
}
}
//发送消息给客户端
private void SendMessage(string data)
{
byte[] msgData = Encoding.UTF8.GetBytes(data);
clientSocket.Send(msgData);
}
}
Client类中的HandleResponse函数对客户端传输过来的位置信息进行解析且转发到其他客户端
客户端创建
ClientManager.cs
using UnityEngine;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System;
using UnityEngine.UI;
public class ClientManager : MonoBehaviour{
[SerializeField]
private GameObject playerPrefab;
[SerializeField]
private GameObject otherPlayerPrefab;
[HideInInspector]
public GameObject currentPlayer;
private Socket clientSocket;
private byte[] recieveData;
private PlayerMove playerMove;
private int recieveNum;
private string recieveStr = "";
private bool isRecieve = false;
void Start()
{
// 首先声明一个Socket
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//申明IP和端口号
IPAddress iPAddress = IPAddress.Parse("10.16.28.122");
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8899);
//连接服务器端
clientSocket.Connect(iPEndPoint);
//接收服务端发送过来的消息
recieveData = new byte[clientSocket.ReceiveBufferSize];
clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RevieveCallback, null);
}
private void RevieveCallback(IAsyncResult ar)
{
int count = clientSocket.EndReceive(ar);
string msgRec = Encoding.UTF8.GetString(recieveData, 0, clientSocket.ReceiveBufferSize);
//输出信息
isRecieve = true;
recieveStr = msgRec;
recieveData = new byte[clientSocket.ReceiveBufferSize];
clientSocket.BeginReceive(recieveData, 0, clientSocket.ReceiveBufferSize, SocketFlags.None, RevieveCallback, null);
}
void Update()
{
//由于同步信息是回调回来的,所以要从主线程调用的话就采取这样的形式
if (isRecieve)
{
isRecieve = false;
RecieveMsg(recieveStr);
}
}
public void RecieveMsg(string data)
{
//当连接服务器第一次的时候会返回当前客户端的flag标记
if(recieveNum < 1)
{
recieveNum++;
playerPrefab = GameObject.Instantiate<GameObject>(playerPrefab);
otherPlayerPrefab = GameObject.Instantiate<GameObject>(otherPlayerPrefab);
//对flag进行判断并进行当前客户端控制对象的选取
if (int.Parse(data) == 1)
{
playerMove = playerPrefab.GetComponent<PlayerMove>();
Destroy(otherPlayerPrefab.GetComponent<PlayerMove>());
playerMove.flag = "1";
currentPlayer = playerPrefab;
}
else
{
playerMove = otherPlayerPrefab.GetComponent<PlayerMove>();
Destroy(playerPrefab.GetComponent<PlayerMove>());
playerMove.flag = "2";
currentPlayer = otherPlayerPrefab;
}
}
else
{
//第一次之后访问服务器则进行数据的接收同步
playerMove.RecieveData(data);
}
}
public void SendMsg(string data)
{
byte[] sendMsg = Encoding.UTF8.GetBytes(data);
clientSocket.Send(sendMsg);
}
}
PlayerMove.cs
using UnityEngine;
public class PlayerMove : MonoBehaviour {
public string flag;
private string tempFlag = "";
private ClientManager clientManager;
private Rigidbody myRigibody;
private Vector3 playerInput;
[SerializeField]
private float movementSpeed = 5.0f;
[SerializeField]
private float turnSpeed = 1000f;
//同步频率
private int syncRate = 20;
private bool isRemotePlayer = false;
private Vector3 pos;
private Vector3 rotation;
private Transform currentPlayerTransform;
void Start () {
myRigibody = GetComponent<Rigidbody>();
clientManager = GameObject.Find("ClientManager").GetComponent<ClientManager>();
currentPlayerTransform = clientManager.currentPlayer.transform;
//持续间隔对数据的同步
InvokeRepeating("SyncPlayerTransform", 3f, 1f / syncRate);
}
void Update () {
//判断当前客户端控制的对象来同步其他客户端的对象信息
if (clientManager.currentPlayer.name.Equals("OtherPlayer(Clone)"))
{
SyncRemoteTransform(GameObject.Find("Player(Clone)").transform);
}
else
{
SyncRemoteTransform(GameObject.Find("OtherPlayer(Clone)").transform);
}
}
//同步自身控制对象的位置信息给服务器
void SyncPlayerTransform()
{
string data = string.Format("{0},{1},{2}*{3}", currentPlayerTransform.localPosition.x,
currentPlayerTransform.localPosition.y, currentPlayerTransform.localPosition.z,flag);
clientManager.SendMsg(data);
}
//同步其他客户端的控制对象位置信息
void SyncRemoteTransform(Transform remotePlayerTransform)
{
remotePlayerTransform.position = pos;
//remotePlayerTransform.eulerAngles = rotation;
}
public void RecieveData(string data)
{
if (!string.IsNullOrEmpty(data)&& !data.Equals("")){
//获取到其他控制器的数据信息并进行解析
string[] strs = data.Split('*');
pos = Parse(strs[0]);
//rotation = Parse(strs[1]);
tempFlag = strs[1];
}
}
void FixedUpdate()
{
//如果当前控制对象不属于此客户端则不控制
if (clientManager.currentPlayer != this.gameObject)
{
return;
}
//设置 坐标的值
playerInput.Set(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical"));
//当用户停止输入 让当前的物体不做任何的操作且待在原地
if (playerInput == Vector3.zero)
return;
//创建一个可以朝向运动的方向
Quaternion newRotation = Quaternion.LookRotation(playerInput);
//如果当前物体并没有朝向运动方向则将当前物体转向运动方向
if (myRigibody.rotation != newRotation)
myRigibody.rotation = Quaternion.RotateTowards(myRigibody.rotation, newRotation, turnSpeed * Time.deltaTime);
//通过我们的输入得到单位的方向向量,再乘以移动速度以及时间得到需要移动的距离Vector3(此时是以原点为基准的)
//再和此前物体的坐标相加可以得到当前物体需要移动到的新坐标
Vector3 newPosition = transform.localPosition + playerInput.normalized * movementSpeed * Time.deltaTime;
//将我们的物体移动到最新的坐标
myRigibody.MovePosition(newPosition);
}
//对字符串数据进行解析
Vector3 Parse(string data)
{
string[] strs = data.Split(',');
float x = float.Parse(strs[0]);
float y = float.Parse(strs[1]);
float z = float.Parse(strs[2]);
return new Vector3(x,y,z);
}
}
在ClientManager中进行Socket的初始化,负责对服务器的连接,数据的发送和数据的接收以及数据的解析功能,而PlayerMove脚本是挂载到场景中的胶囊体上的,主要是控制胶囊体的运动,当然还附加胶囊体数据的同步功能(功能分配有点乱,当前实例以实现功能为主,并不care如何设计类会更加灵活233333)
源码地址
源码工程已上传Github了,在这里
总结
初步了解了下联机游戏中数据同步的概念以及简单的实现流程,加深Socket的基础认识。
就这么多,白了个白~
网友评论