前言
这一章是教我们做一个大乱斗游戏。但是书中的代码有些前后不一致导致运行错误,如果你也碰到了这样的情况,可以参考我的代码
我们要完成的主要有以下这些事
- 左键操控角色行走
- 右键操控角色攻击
- 受到攻击掉血,队手hp为0时消失,自己hp为0时失败,掉线同理
- 角色类的编写
- 介绍协议、消息队列等
- 客户端以及服务端代码
角色类BaseHuman
unity相关操作基本都省略了
BaseHuman为角色基类
CtrlHuman代表操控角色,在BaseHuman基础上多了处理鼠标操控功能
SyncHuman代表同步角色,处理网络同步,由网络数据驱动,由服务端转发角色的状态信息
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BaseHuman : MonoBehaviour
{
//是否正在移动
protected bool isMoving = false;
//移动目标点
private Vector3 targetPosition;
//移动速度
public float speed = 1.2f;
//动画
private Animator animator;
//描述
public string desc = "";
//是否正在攻击
internal bool isAttacking = false;
internal float attackTime = float.MinValue;
//攻击动作
public void Attack()
{
isAttacking = true;
attackTime = Time.time;
//只要动画不loop,就不会在1.2s内一直攻击
animator.SetBool("isAttacking", true);
}
//攻击update
public void AttackUpdate()
{
if (!isAttacking) return;
if (Time.time - attackTime < 1.2f) return;
isAttacking = false;
animator.SetBool("isAttacking", false);
}
//移动到某处
public void MoveTo(Vector3 pos)
{
targetPosition = pos;
isMoving = true;
animator.SetBool("isMoving", true);
}
//移动update
public void MoveUpdate()
{
if(isMoving == false){
return;
}
Vector3 pos = transform.position;
transform.position = Vector3.MoveTowards(pos, targetPosition, speed * Time.deltaTime);
transform.LookAt(targetPosition);
if(Vector3.Distance(pos,targetPosition) < 0.05f)
{
isMoving = false;
animator.SetBool("isMoving", false);
}
}
protected void Start()
{
animator = GetComponent<Animator>();
}
protected void Update()
{
MoveUpdate();
AttackUpdate();
}
}
我们把移动、攻击等两个子类都需要的写在基类中。
3.3.4CtrlHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman
{
// Start is called before the first frame update
new void Start()
{
base.Start();
}
// Update is called once per frame
new void Update()
{
base.Update();
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if (hit.collider.tag == "Ground")
{
MoveTo(hit.point);
//发送协议
string sendStr = "Move|";
sendStr += NetManager.GetDesc() + ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
NetManager.Send(sendStr);
}
}
//attack
if (Input.GetMouseButtonDown(1))
{
if (isAttacking) return;
if (isMoving) return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
transform.LookAt(hit.point);
Attack();
//发送协议
string sendStr = "Attack|";
sendStr += NetManager.GetDesc() + ",";
sendStr += transform.eulerAngles.y + ",";
NetManager.Send(sendStr);
//攻击判定
Vector3 lineEnd = transform.position + 0.5f * Vector3.up;
Vector3 lineStart = lineEnd + 20 * transform.forward;
if (Physics.Linecast(lineStart, lineEnd, out hit))
{
GameObject hitObj = hit.collider.gameObject;
if (hitObj == gameObject) return;
SyncHuman h = hitObj.GetComponent<SyncHuman>();
if (h == null) return;
sendStr = "Hit|";
sendStr += NetManager.GetDesc() + ",";//我们这里把攻击者和被攻击者的ip都发了
sendStr += h.desc + ",";
NetManager.Send(sendStr);
}
}
}
}
tips:
- CtrlHuman的功能有几个:1.鼠标操控人物。2.操控完后发送move和attack协议给服务器。(具体协议后面介绍)
- new用作修饰符时可以显示隐藏从基类继承的成员。
3.3.5SyncHuman
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SyncHuman : BaseHuman
{
// Start is called before the first frame update
new void Start()
{
base.Start();
}
// Update is called once per frame
new void Update()
{
base.Update();
}
public void SyncAttack(float eulY)
{
transform.eulerAngles = new Vector3(0, eulY, 0);
Attack();
}
}
开放一个同步攻击函数操控自己的转向即可(动画都在基类中转换),大部分操作都由一个Main类来操控,比如设定targetPos,因为都是在Main类里接收并解析服务端转发来的数据
3.4如何使用网络模块
在实际开发中网络模块都是作为底层模块用的,应该和具体的游戏逻辑分开,而不应该把处理逻辑的代码比如给recvStr赋值写到ReceiveCallback里面去,因为它仅需要处理网络数据
所以我们的方法是给网络管理类添加回调,当收到某种消息时自动调用对应的函数,这样就可以将游戏逻辑和底层模块分开。
先介绍委托、协议和消息队列才能更好的写网络管理类
3.4.1委托
NetManager会用委托实现消息分发,可以把委托理解成回调函数的实现方式,比if-else或者switch强!
delegate是C#的一种类型,它能够引用某种类型的方法,相当于C++的函数指针。
- 委托传递的方法必须有相同的参数和返回值类型
- 创建委托对象,将要传递的方法作为参数传入
- 可以通过+=和-=来绑定不同的函数
- 调用委托时,依次调用对应的所有回调函数
3.4.2通信协议
通信协议是通信双方对数据传送控制的一种约定,通信双方必须共同遵守,要知道对方在说什么和让对方听懂我的话。
这里就简单用字符串协议来实现,消息名和消息体用“|”隔开,消息体各个参数用“,”隔开
比如
Move|127.0.0.1:1234,10,0,8,
消息名,ip,端口号,移动坐标
用Split(’|’)和Split(’,’)可将协议中各个参数解析出来。
解析后,通过委托将不同的消息交给不同的方法处理。Move协议就用OnMove方法处理…
3.4.3消息队列
多线程消息处理虽然效率高,但非主线程不能设置Unity3D组件,所以聊天室我们就用了变量recvStr作为主线程和callback线程的桥梁,容易造成混乱。
因为单线程消息可以满足游戏客户端的需要,所以大部分游戏会使用消息队列让主线程取处理异步Socket收到的消息。
C#的异步通信由线程池实现,不同的BeginReceive不一定在同一线程执行**(这里有个坑!Send也要改成异步的,不然如果是send的同步实现形式,消息不是一条一条过去的,而是同时收到的,无法正确解析)**
创建一个消息列表,每次收到消息就在末端添加数据,列表由主线程读取,它作为主线程和异步线程的桥梁。mono的update在主线程执行,所以每次可以从消息列表读取几条信息处理,处理后在列表中删除
用List实现
3.4.4类
Show me code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
using System;
public static class NetManager
{
//套接字
static Socket socket;
//接收缓冲区
static byte[] readBuff = new byte[1024];
//委托类型
public delegate void MsgListener(string str);
//监听列表
//指明各个消息名所对应的处理方法,外部可通过addlistener添加消息名对应的处理函数
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
//消息列表
static List<string> msgList = new List<string>();
//添加监听
public static void AddListener(string msgName,MsgListener listener)
{
//消息对应一个委托,委托即回调。收到此消息直接通过dic触发委托即可
listeners[msgName] = listener;
}
//获取描述
public static string GetDesc()
{
if (socket == null) return "";
if (!socket.Connected) return "";
return socket.LocalEndPoint.ToString();//ip
}
//连接
public static void Connect(string ip,int port)
{
//Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Connect(用同步方式简化代码)
socket.Connect(ip, port);
//BeginReceive
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
}
//Receive回调
private static void ReceiveCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndReceive(ar);
string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
msgList.Add(recvStr);//添加待处理消息,会将对方收到的消息一股脑发过来,要处理
socket.BeginReceive(readBuff, 0, 1024, 0, ReceiveCallback, socket);
}
catch(SocketException ex)
{
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
//发送
public static void Send(string sendStr)
{
if (socket == null) return;
if (!socket.Connected) return;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
//Send回调
private static void SendCallback(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
}
catch(SocketException ex)
{
Debug.Log("Socket Send fail" + ex.ToString());
}
}
//Update,收到消息后让外界调用的,通过消息列表执行对应的回调。
public static void Update()
{
if (msgList.Count <= 0)
return;
string msgStr = msgList[0];
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
//监听回调
if (listeners.ContainsKey(msgName))
{
listeners[msgName](msgArgs);
}
}
}
主要作用:
- Connect
- AddListener,消息监听,其他模块可以通过此方法设置某个消息名对应的处理模块,我们在Main的Start中绑定。
- Send,发送消息给服务端
使用异步Socket接收消息,每次接收到一条消息存入消息队列。再编写一个供外部调用的Update方法,每当调用它就处理消息队列里第一条消息,然后根据协议名将消息分发给对应的回调函数。
注意注意:
Send要用异步来实现!!
3.5进入游戏:Enter协议
客户端进入游戏后发送一条Enter协议个服务端,包含对玩家的描述、位置等,服务端将Enter协议广播出去,其他客户端收到Enter协议后,创建一个同步角色(SyncHuman)
此处把代码全贴了,后序在讲到不同的协议就单独拿出来说
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
public GameObject humanPrefab;
//任务列表
public BaseHuman myHuman;
//不new会怎么样?
public Dictionary<string, BaseHuman> otherHumans = new Dictionary<string, BaseHuman>();
// Start is called before the first frame update
void Start()
{
//net模块
NetManager.AddListener("Enter", OnEnter);
NetManager.AddListener("List", OnList);
NetManager.AddListener("Move", OnMove);
NetManager.AddListener("Leave", OnLeave);
NetManager.AddListener("Attack", OnAttack);
NetManager.AddListener("Die", OnDie);
NetManager.Connect("127.0.0.1", 8888);
//添加一个角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
float x = Random.Range(-5, 5);
float z = Random.Range(-5, 5);
obj.transform.position = new Vector3(x, 0, z);
myHuman = obj.AddComponent<CtrlHuman>();
myHuman.desc = NetManager.GetDesc();
//发送enter协议,调用start的时候会发送enter加入待处理列表
Vector3 pos = myHuman.transform.position;
Vector3 eul = myHuman.transform.eulerAngles;
string sendStr = "Enter|";
sendStr += NetManager.GetDesc() + ",";
sendStr += pos.x + "," + pos.y + "," + pos.z + "," + eul.y+",";
NetManager.Send(sendStr);
//请求玩家列表
//感觉这个被一级加到enter协议里面了
NetManager.Send("List|");
}
private void Update()
{
NetManager.Update();
}
void OnEnter(string msgArgs)
{
Debug.Log("OnEnter " + msgArgs);
//解析参数,netManager那里也要解析,不过是为了绑定回调
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//是自己,netManager只为自己这个客户端服务
if(desc == NetManager.GetDesc())
{
return;
}
//在收到别的客户端进入游戏发送的enter协议时,在本地生成一个同步角色
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
void OnList(string msgArgs)
{
Debug.Log("OnList " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
int count = (split.Length - 1) / 6;//玩家数目
for (int i = 0; i < count; i++)
{
string desc = split[i * 6 + 0];
float x = float.Parse(split[i * 6 + 1]);
float y = float.Parse(split[i * 6 + 2]);
float z = float.Parse(split[i * 6 + 3]);
float eulY = float.Parse(split[i * 6 + 4]);
float hp = int.Parse(split[i * 6 + 5]);
//self
if (desc == NetManager.GetDesc())
{
continue;
}
//添加角色,那enter还需要生成吗
//没有关系,list协议是用于生成在你之前进入游戏的玩家的,在你进入的时候调用一次,之后再有玩家进入就通过enter生成同步对象
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
}
void OnMove(string msg)
{
Debug.Log("OnMove" + msg);
//解析参数
string[] split = msg.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//move
if (!otherHumans.ContainsKey(desc)) return;
BaseHuman h = otherHumans[desc];//这相当于直接修改本体
Vector3 targetPos = new Vector3(x, y, z);
h.MoveTo(targetPos);
}
void OnLeave(string msg)
{
Debug.Log("OnLeave" + msg);
//解析参数
string[] split = msg.Split(',');
string desc = split[0];
//删除
if (!otherHumans.ContainsKey(desc)) return;
BaseHuman h = otherHumans[desc];
Destroy(h.gameObject);
otherHumans.Remove(desc);
}
void OnAttack(string msgArgs)
{
Debug.Log("OnAttack" + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float eulY = float.Parse(split[1]);
//攻击动作
if (!otherHumans.ContainsKey(desc)) return;
SyncHuman h = (SyncHuman)otherHumans[desc];
h.SyncAttack(eulY);//解析协议后,让对应的同步角色做出相应动作
}
void OnDie(string msgArgs)
{
Debug.Log("OnDie" + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0]; //??
string hitDesc = split[0];
//自己死了
if(hitDesc == myHuman.desc)
{
Debug.Log("Game Over");
return;
}
//死了
if (!otherHumans.ContainsKey(hitDesc)) return;
SyncHuman h = (SyncHuman)otherHumans[hitDesc];
h.gameObject.SetActive(false);//应该是直接删掉
}
}
- 在Start中绑定委托
- 创建角色
- 在Update中调用NetManger的Update!让其在有消息的情况下不停解析
- 程序按需给角色添加CtrlHUuman或SyncHUman,不用再prefab上加
Enter协议:
Enter|127.0.0.1:4564,3,0,5,0,
客户端发送和服务端转发的形式一样
3.5.2接收Enter协议
也就是上面OnEnter
客户端收到服务端转发的Enter协议后,解析Enter协议的各个参数,用于生成同步角色并加入到otherHuman列表中(后序可以直接通过ip来操控同步角色)
问题
后进入的客户端看不到前面的玩家
因为后进入的收不到前一个玩家的Enter协议,后序用List协议解决
3.6服务端如何处理消息
客户端用AddListener把网络协议和具体的处理函数对应。服务端就用反射机制把底层网络模块和具体的消息处理函数分开
3.6.1反射机制
using System.Reflection;
using System.Linq;
ReadClientfd(Socket clientfd){
string[] split = recvStr.Split('|');
Console.WriteLine("Receive" + recvStr);
string msgName = split[0];
string msgArgs = split[1];
string funName = "Msg" + msgName;
//MethodInfo类的对象mi包含它所指代的方法的所有信息
//通过这个类可以得到方法的名称、参数、返回值,并且可以调用它
//我们会将所有消息处理方法定义在MsgHandler中,且为static方法。所以可以用下面这条语句获取handler类中名为funName的static func
MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
object[] o = { state, msgArgs };
//调用mi所包含的方法,arg0代表this指针,因为为staic方法所以填null,arg1是参数列表,里面包含消息处理需要的两个参数,客户端state和消息内容msgArgs
mi.Invoke(null, o);
}
我们需要的就是在解析协议名后,自动调用名为“Msg+协议名”的方法
上面的反射代码就可以帮我们实现
MethodInfo类的对象mi包含它所指代的方法的所有信息,通过这个类可以得到方法的名称、参数、返回值,并且可以调用它,我们会将所有消息处理方法定义在MsgHandler中,且为static方法。所以可以用下面这条语句获取handler类中名为funName的static func
调用mi所包含的方法,arg0代表this指针,因为为staic方法所以填null,arg1是参数列表,里面包含消息处理需要的两个参数,客户端state和消息内容msgArgs
3.6.2消息处理
using System;
using System.Collections.Generic;
public class MsgHandler
{
//这是收到服务端收到消息调用的对应函数
//arg0表示消息从哪个客户端发来,arg1代表消息内容
//如果同时发送了list|,把list作为参数一起穿进来了
public static void MsgEnter(ClientState c,string msgArgs)
{
Console.WriteLine("MsgEnter" + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float eulY = float.Parse(split[4]);
//赋值
c.hp = 100;
c.x = x;
c.y = y;
c.z = z;
c.eulY = eulY;
//广播
string sendStr = "Enter|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
public static void MsgList(ClientState c,string msgArgs)
{
string sendStr = "List|";
//组装List协议,将字符串发出去
foreach (ClientState cs in MainClass.clients.Values)
{
sendStr += cs.socket.RemoteEndPoint.ToString() + ",";
sendStr += cs.x.ToString() + ",";
sendStr += cs.y.ToString() + ",";
sendStr += cs.z.ToString() + ",";
sendStr += cs.eulY.ToString() + ",";
sendStr += cs.hp.ToString() + ",";
}
Console.WriteLine("MsgList" + msgArgs);
MainClass.Send(c, sendStr);
}
public static void MsgMove(ClientState c,string msgArgs)
{
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//赋值
c.x = x;
c.y = y;
c.z = z;
//广播
string sendStr = "Move|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
public static void MsgAttack(ClientState c,string msgArgs)
{
//只需转发attack协议即可
string sendStr = "Attack|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
public static void MsgHit(ClientState c,string msgArgs)
{
//解析参数
string[] split = msgArgs.Split(',');
string attDesc = split[0];
string hitDesc = split[1];
//找出被攻击的角色
ClientState hitCS = null;
foreach (ClientState cs in MainClass.clients.Values)
{
if(cs.socket.RemoteEndPoint.ToString() == hitDesc)
{
hitCS = cs;
}
}
if (hitCS == null) return;
//扣血
hitCS.hp -= 25;
//死亡协议只需要服务端发送个客户端即可
if(hitCS.hp <= 0)
{
string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
}
}
一般都是转发给客户端,enter、move这些就需要记录下位置等,hit就要判断是否死亡,死亡就发送die协议。
3.6.3事件处理
using System;
//处理上下线事件,和处理消息的class类似
public class EventHandler
{
//Clientstate类要设为public,不然方法和参数访问性不一致要报错
//角色下线,删除客户端内的同步角色
public static void OnDisconnect(ClientState c)
{
string desc = c.socket.RemoteEndPoint.ToString();
string sendStr = "Leave|" + desc + ",";
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
}
3.7玩家列表:List协议
玩家进入场景后,调用NetManger.Send发送List协议。
此处发送和转发的协议格式不同
- 客户端发送的请求
List|
- 服务端转发的回应
List|127.0.0.1:4564,3,0,5,0,100,127.0.0.1:4578,4,0,9,0,100,
有几个人后面就有几组
在start中发送请求
3.7.1客户端处理
void OnList(string msgArgs)
{
Debug.Log("OnList " + msgArgs);
//解析参数
string[] split = msgArgs.Split(',');
int count = (split.Length - 1) / 6;//玩家数目
for (int i = 0; i < count; i++)
{
string desc = split[i * 6 + 0];
float x = float.Parse(split[i * 6 + 1]);
float y = float.Parse(split[i * 6 + 2]);
float z = float.Parse(split[i * 6 + 3]);
float eulY = float.Parse(split[i * 6 + 4]);
float hp = int.Parse(split[i * 6 + 5]);
//self
if (desc == NetManager.GetDesc())
{
continue;
}
//添加角色,那enter还需要生成吗
//没有关系,list协议是用于生成在你之前进入游戏的玩家的,在你进入的时候调用一次,之后再有玩家进入就通过enter生成同步对象
GameObject obj = (GameObject)Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();
h.desc = desc;
otherHumans.Add(desc, h);
}
}
每次有新的客户端进入,都通过for循环生成前面所有记录下的玩家。
3.7.2服务端处理
组装记录下的所有客户端信息后转发。(这就是我们为什么要在服务端记录人物pos、hp等,可以用于list协议给后面的玩家看)
public static void MsgList(ClientState c,string msgArgs)
{
string sendStr = "List|";
//组装List协议,将字符串发出去
foreach (ClientState cs in MainClass.clients.Values)
{
sendStr += cs.socket.RemoteEndPoint.ToString() + ",";
sendStr += cs.x.ToString() + ",";
sendStr += cs.y.ToString() + ",";
sendStr += cs.z.ToString() + ",";
sendStr += cs.eulY.ToString() + ",";
sendStr += cs.hp.ToString() + ",";
}
Console.WriteLine("MsgList" + msgArgs);
MainClass.Send(c, sendStr);
}
3.8移动同步:Move协议
点击场景后,将targetPos发送给服务端,服务端一边记录位置信息以便广播给其他客户端。其余客户端收到协议后,解析目的地位置信息,用otherHuman和ip控制SyncHuman走到对应位置
3.8.1客户端处理
点击后将targetPos发送给服务端
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray, out hit);
if (hit.collider.tag == "Ground")
{
MoveTo(hit.point);
//发送协议
string sendStr = "Move|";
sendStr += NetManager.GetDesc() + ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
NetManager.Send(sendStr);
}
}
操控对应的同步角色行走
void OnMove(string msg)
{
Debug.Log("OnMove" + msg);
//解析参数
string[] split = msg.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//move
if (!otherHumans.ContainsKey(desc)) return;
BaseHuman h = otherHumans[desc];//这相当于直接修改本体
Vector3 targetPos = new Vector3(x, y, z);
h.MoveTo(targetPos);
}
3.8.2服务端处理
解析参数
记录信息
广播
public static void MsgMove(ClientState c,string msgArgs)
{
//解析参数
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//赋值
c.x = x;
c.y = y;
c.z = z;
//广播
string sendStr = "Move|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
3.8.3完善
由于网络延迟等问题,这种同步方式可能有问题,几个客户端的表现不会完全一致。现在的网络游戏也只能保证误差在可接受范围内。
后续的几个协议都大差不差,就不写了!
最后
在测试中可能会断线、收不到协议等。因为TCP协议是基于数据流的协议,并不保证每次接受的数据都完整,商业级游戏必须解决各种隐患。