点击蓝字关注我哦
乱斗游戏(二)
我们对于之前的机制,做个小小的改动,之前的方法都是写在一个主函数中的,其实以我们的经验来看,却是不是一个明智的做法,肯定需要重构,那么我们怎么重构呢?需要什么方法来重构呢?
我们用到是发射.反射用法文章之前也有解释,可以去翻翻看:链接
我们使用反射,就是希望用协议名来调用函数,我们修改一下服务器端的代码,首先在Program引入反射的命名空间:
using System.Reflection;
那么既然是通过协议名来调用某个函数方法,那么假设我们第一个调用的为”Enter”协议,那么我们要怎么做呢? 我们需要新建一个脚本:MsgHandler,用来专门存放消息处理的函数.
我们打开MsgHandler函数,并写好MsgEnter的函数.
using EchoServer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//通过反射调用
namespace EchoServer
{
class MsgHandler
{
pulic static void MsgEnter(ClientState c, string msgArgs)
{
}
}
}
虽然是个空的函数,但是我们会在Program函数中调用到它.那么回到我们的Program函数中,当读取客户端消息的时候,我们就开始解析客户端的消息,根据客户端的协议类型,去调用相对应的函数:
//处理每个在线客户端的消息
public static bool ReadClientfd(Socket clientfd)
{
ClientState state = clients[clientfd];
//接收字节
int count = 0;
try
{
count = clientfd.Receive(state.readBuff);
}
catch (SocketException e)
{
//下线时调用反射
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisConnect");
object[] ob = { state };
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("异常报告:" + e.Message);
return false;
}
//如果客户端强行下线
if (count <= 0)
{
//下线时调用反射
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisConnect");
object[] ob = { state };
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("socket 已关闭");
return false;
}
//数据处理
string recvStr = Encoding.Default.GetString(state.readBuff, 0, count);
string[] split = recvStr.Split('|');
Console.WriteLine("Receive :" + recvStr);
string msgName = split[0];
string msgArgs = split[1];
string funName = "Msg" + msgName;
MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
object[] o = { state, msgArgs };
mi.Invoke(null, o);
//客户端需要发送的
//string sendStr = recvStr;
//byte[] sendBytes = Encoding.Default.GetBytes(sendStr);
//foreach (ClientState item in clients.Values)
//{
// item.socket.Send(sendBytes);
//}
return true;
}
如你所见,我们将客户端的发送代码注释掉了,所以我们也要单独写一个方法,来广播消息:
//发送
public static void Send(ClientState cs, string sendStr)
{
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
cs.socket.Send(sendBytes);
}
比如客户端来的消息是:Enter|127.0.0.1:12315,那么代码会自动调用MsgEnter的代码逻辑
既然这一个自动调用的步骤有了,玩家打开就会有个Enter的请求发送进来.那么Enter这个函数做什么呢?首先是消息的拆分,前文说过,这条消息包含玩家的位置,旋转,然后是对这个玩家的消息进行广播.那么MsgEnter的消息应当这么写:
public static void MsgEnter(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]);
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 Program. clients.Values)
{
Program.Send(cs ,sendStr );
}
}
那么,仅仅如此就可以了吗?当然没有,因为你目前的做法,只是广播给各个客户端,说有个玩家进来了,然后就没有任何操作了,那么客户端知道 有玩家进来了,但是它不知道该在哪里去生成这个玩家,也不知道玩家的旋转角是多少,所以运行程序来看,与之前的文章的效果还是一样的.所以,我们还要在客户端Enter协议发送之后,再发送一个List协议的请求,这个请求会让服务器会把目前在线的玩家数据统统广播出去,所以,首先在客户端中发送完Enter协议,再发送List协议,用来获取在线玩家列表,这个List的协议如下:
假如目前服务器接收了2位玩家登录.
List|127.0.0.1:4565,3,0,5,0,100,127.0.0.1:4556,4,0,9,0,100,
void Start():
{
NetManager.AddListener("Enter", OnEnter);
NetManager.AddListener("List", OnList);
NetManager.AddListener("Move", OnMove);
NetManager.AddListener("Attack", OnAttack);
NetManager.AddListener("Die", OnDie);
NetManager.AddListener("Leave", OnLeave);
NetManager.Connect("127.0.0.1", 8888);
GameObject go = Instantiate(humanPrefab) as GameObject;
float x = Random.Range(-5, 5);
float z = Random.Range(-5, 5);
go.transform.position = new Vector3(x, 0, z);
myHuman = go.AddComponent<CtrlHuman>();
myHuman.desc = NetManager.Getdesc();
//发送协议
Vector3 pos = myHuman.transform.position;
Vector3 eul = myHuman.transform.eulerAngles;
string sendStr = "Enter|";
sendStr += NetManager.Getdesc() + ",";
sendStr += pos.x + ",";
sendStr += pos.y + ",";
sendStr += pos.z + ",";
sendStr += eul.y + ",";
NetManager.Send(sendStr);
//请求玩家列表
NetManager.Send("List|");
}
当然,只有最后一句代码是新加的,我写上是怕读者看的乱,所以宁愿字数多,也不让读者读着模糊.
那么既然发送了List请求,客户端接受到了服务器发来的消息,也应该有OnList的方法供给调用.那么OnList里面应当有所有已连接的客户端信息.
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]);
int hp = int.Parse(split[i * 6 + 5]);
//如果是自己
if (desc == NetManager.Getdesc())
{
continue;
}
//否则添加角色到场景
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);
}
}
不过不要忘了,增加监听:
NetManager.AddListener("List", OnList);
通过公式计算,服务器有几台客户端在连接着,解析出它们的参数,并实例化在场景中.
服务器中要做的就是遍历所有连接着的客户端,并把它们打包成一条信息,然后广播出去:
public static void MsgList(ClientState c, string msgArgs)
{
string sendStr = "List|";
foreach (ClientState cs in Program .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() + ",";
}
Program.Send(c, sendStr);
}
那么测试一下,打开服务器,并再打开两个客户端,即可发现两个客户端都可以知道有新加的玩家,多添加几个也可以识别到:
那么既然有了上线两个客户端都显示上线玩家.但是关掉某个客户端,却发现其他客户端没有正确的删除下线的玩家,所以我们还需要一个下线的处理,包括玩家异常掉线,与玩家失败被踢出房间.
我们添加一个事件处理的类,用来处理玩家下线的功能,这个类名为:EventHandler,当然,我们使用反射来调用,那么在哪里来调用呢?在客户端关闭,或者是客户端接收消息为空的时候,(其实这个时候已经是客户端异常了,游戏中当然会做掉线重连,但是这里我们默认消息通讯优秀,没有网络波动)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EchoServer;
class EventHandler
{
public static void OnDisConnect(ClientState c)
{
Console.WriteLine("Disconnect");
}
}
我们在接收消息异常和客户端关闭时调用:
//处理每个在线客户端的消息
public static bool ReadClientfd(Socket clientfd)
{
ClientState state = clients[clientfd];
//接收字节
int count = 0;
try
{
count = clientfd.Receive(state.readBuff);
}
catch (SocketException e)
{
//下线时调用反射
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisConnect");
object[] ob = { state };
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("异常报告:" + e.Message);
return false;
}
//如果客户端强行下线
if (count <= 0)
{
//下线时调用反射
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisConnect");
object[] ob = { state };
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("socket 已关闭");
return false;
}
...//省略
}
那么,在客户端异常时,我们会调用这个方法,那么客户端,又如何得知,此时应该处理掉线的用户呢?
我们在调用OnDisconnect的时候,广播一个Leave协议,告诉所有的客户端,某个客户端下线了.
public static void OnDisConnect(ClientState c)
{
Console.WriteLine("Disconnect");
string desc = c.socket.RemoteEndPoint.ToString();
string sendStr = "Leave|" + desc + ",";
foreach (ClientState cs in Program .clients.Values )
{
Program.Send(cs, sendStr);
}
}
那么客户端接收到这条消息,该做何处理呢?依照之前的经验,无阿给是写上OnLeave方法,然后添加监听:
//玩家离开/踢出
void OnLeave(string msgArgs)
{
Debug.Log("OnLeave" + msgArgs);
string[] split = msgArgs.Split(',');
string desc = split[0];
if (!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Destroy(h.gameObject);
otherHumans.Remove(desc);
}
添加监听:
NetManager.AddListener("Leave", OnLeave);
然后我们测试一下,即可发现,我们测试一下,打开服务器,再打开两个 客户端,再关闭其中一个,即可发现一个客户端中也隐藏了该角色.
我们做到这里,依然不够,因为玩家只可以看到自己客户端的人物在移动,另一个客户端并没有在移动.这是因为,另一个客户端根本不知道该玩家移动了,因为没有广播移动的协议.那么这个协议该如何广播呢?就是在客户端按下鼠标左键的时候,将此客户端的鼠标点击位置进行广播.然后另一台客户端解析协议,控制sync玩家走到该地点.
Move协议
首先客户端将位置发送过去,在玩家按下鼠标左键的时候,发送Move协议,包含玩家需要移动到的终点.
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Physics.Raycast(ray,out hit);
if (hit.collider.tag.Equals("Terrain"))
{
MoveTo(hit.point);
//终点消息发送到服务器
string sendStr = "Move|";
sendStr += NetManager.Getdesc() + ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
NetManager.Send(sendStr);
}
}
那么服务接收到这条消息,当然要调用相对应的MsgMove方法,和上面的OnEnter方法类似:
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 ;
forach (ClientState cs in Program .clients.Values )
{
Program.Send(cs, sendStr);
}
}
可能你会有一个疑问,为什么要给C赋值,因为如果再有新的客户端加入进来,服务器广播的就是C的新位置.而不是一开始出生位置.
那么客户端接收到这条消息了该怎么处理呢?首先,仍旧时切割,然后控制sync的玩家走到这个位置.
void OnMove(string msgArgs)
{
Debug.Log("OnMove" + 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]);
if (!otherHumans.ContainsKey(desc))
return;
BaseHuman h = otherHumans[desc];
Vector3 targetPos = new Vector3(x, y, z);
h.MoveTo(targetPos);
}
写完之后,我们测试一下:
也许你会发现,它仍旧会出现,一个客户端出现玩家,另一个客户端并没有出现玩家的现象,这是因为这个客户端没有做消息的正确验证等,所以我们的程序还是有问题的.这个bug后续会解决的.
既然是战斗游戏,当然少不了攻击与清算,那么我们按照顺序先做攻击,攻击具有攻击到了和没有攻击到两种情况,攻击到一定次数,玩家失败退出,调用OnDisconnect .
我们先把攻击动作放到Player上:
攻击动作自己找一个就好了,因为这里时基础中的基础,我就一笔带过.在客户端中把玩家按下鼠标右键改为攻击:当然这是公共的方法,我们要写在Base Human中:
internal bool isAttacking = false;
internal float attackTime = float.MinValue;
public void Attack()
{
isAttacking = true;
attackTime = Time.time;
animator.SetBool("isAttacking", true);
}
public void AttackUpdate()
{
if (!isAttacking) return;
if (Time.time - attackTime < 1.2f) return;
isAttacking = false;
animator.SetBool("isAttacking", false);
}
// Update is called once per frame
public void Update () {
MoveUpdate();
AttackUpdate();
}
在CtrlHuman中控制角色播放此动画:
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);
}
上述代码,已经把攻击的具体信息传给了服务器,其实服务器并不需要做太多工作,仅仅时记录攻击时的转向,然后再广播给所有客户端.
//攻击动作
public static void MsgAttack(ClientState c, string msgArgs)
{
string sendStr = "Attack|" + msgArgs;
foreach (ClientState cs in Program .clients.Values )
{
Program.Send(cs, sendStr);
}
}
那么同样的客户端收到了这条攻击消息,该如何做呢?
还是写一个OnAttack方法,并增加监听.
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);
}
NetManager.AddListener("Attack", OnAttack);
那么你可能看到OnAttack中控制sync角色的播放动画,所以我们要打开SyncHuman脚本,调用攻击父类的攻击代码:
public void SyncAttack(float eulY)
{
transform.eulerAngles = new Vector3(0, eulY, 0);
Attack();
}
那么测试一下,两个客户端同时播放某个玩家在攻击,那么还有一个受伤的的判断,如果玩家仅仅是播放了攻击动作,并没有攻击到另一个玩家,那就不发攻击的协议,如果攻击到了,就发送攻击协议,并广播.
我们这里用简单的射线检测来判断攻击到没有.所以要给玩家添加上碰撞器
并在做攻击动作的时候做一个攻击判定:
if (Input.GetMouseButtonDown(1))
{
...
//攻击判定
Vector3 lineEnd = transform.position + 0.5f * Vector3.up;
Vector3 lineStart = lineEnd + 20 * transform.forward;
if (Physics.Linecast(lineStart, lineEnd, out hit))
{
Debug.DrawLine(lineStart, lineEnd);
GameObject hitObj = hit.collider.gameObject;
if (hitObj == gameObject)
return;
syncHuman h = (syncHuman)hitObj.GetComponent<syncHuman>();
if (h == null)
return;
sendStr = "Hit|";
sendStr += NetManager.Getdesc() + ",";
sendStr += h.desc + ",";
NetManager.Send(sendStr);
Debug.Log(sendStr);
}
}
既然发送了Hit协议,服务器当然要广播出去,
如果血量<0就代表玩家死亡,并发送Die协议,告诉所有的客户端
服务器端的攻击方法:
//攻击hit
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 Program .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 Program .clients .Values )
{
Program.Send(cs, sendStr);
}
}
}
Die协议:
Die协议不需要客户端发送,服务器来判断,其实这个也可以是客户端判断,这个要根据需求来定.客户端接到Die协议,就准备隐藏某个玩家:
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("gameOver");
return;
}
//自己死了
if (!otherHumans.ContainsKey(hitDesc))
{
return;
}
syncHuman h = (syncHuman)otherHumans[hitDesc];
h.gameObject.SetActive(false);
}
这里有句判断,如果是自身,可以退出app.
那么测试一下,发现仍有概率出现问题.因为这套框架并不完整,没有做消息验证,没有做断线重连等等,这些功能我们后面的文章会一一解决.
本案例的客户端与服务器源码下载方法:公众号回复 服务器开发 即可.
…END…
。
技术群:添加小编微信并备注进群
小编微信:mm1552923
公众号:dotNet编程大全