目录
!!代码与书中相比有少许更改,解决了一些可能出现的问题
一、服务器
1、主程序:Program.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Reflection;
//客户状态类,用来存储客户数据
public class ClientState
{
public Socket socket;
public byte[] readBuff = new byte[1024];
public int hp = -100;
public float x = 0;
public float y = 0;
public float z = 0;
public float eulY = 0;
}
public class MainClass
{
//监听Socket
static Socket listenfd;
//用字典来存储客户端Socket及状态信息
public static Dictionary<Socket, ClientState> clients =
new Dictionary<Socket, ClientState>();
//程序入口函数
public static void Main(string[] args)
{
//创建Socket
listenfd = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//连接Bind,给listenfd套接字绑定IP和端口
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(ipEp);
//开启监听,等待客户端连接。参数可以指定队列中最多可容纳等待接受的连接数,0表示不限制
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
//checkRead,后面用来筛选可读的socket
List<Socket> checkRead = new List<Socket>();
//主循环
while (true)
{
//填充checkRead列表
checkRead.Clear();
checkRead.Add(listenfd);
foreach (ClientState s in clients.Values)
{
checkRead.Add(s.socket);
}
//Select可以确定一个或多个Socket对象的状态
//四个参数分别为:检查是否有可读的Socket列表,检查是否有可写的Socket列表,检查是否有出错的Socke列表,等待回应的时间
Socket.Select(checkRead, null, null, 1000);//(时间单位为微妙,-1表示一直等待,0表示非阻塞)
//筛选之后checkRead列表只会剩下可读的socket,不可读的就会被舍弃
//检查遍历可读对象
foreach (Socket s in checkRead)
{
if (s == listenfd)
{
//如果服务器端的Socket可读,说明可能有客户端来连接了
ReadListenfd(s);
}
else
{
//客户端的socket可读,说明客户端发消息了,来接收处理客户端消息
ReadClientfd(s);
}
}
}
}
//用来给客户端发送消息的函数
public static void Send(ClientState cs, string sendStr)
{
if (cs == null || cs.socket == null) return;
if (!cs.socket.Connected) return;
//GetString方法可以将byte型数组转换成字符串。System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
cs.socket.Send(sendBytes);
}
//读取Listenfd
public static void ReadListenfd(Socket listenfd)
{
Console.WriteLine("Accept");
//Accept返回一个新客户端的Socket对象
Socket clientfd = listenfd.Accept();
ClientState state = new ClientState();
state.socket = clientfd;
//添加到clients字典中
clients.Add(clientfd, state);
}
//读取Clientfd,读取客户端的消息
public static bool ReadClientfd(Socket clientfd)
{
//通过字典获得当前客户端的状态
ClientState state = clients[clientfd];
//接收
int count = 0;
try
{
//接收消息存储在state.readBuff里面,返回字节数
count = clientfd.Receive(state.readBuff);
}
catch (SocketException ex)
{
//关闭客户端
//MethodInfo就是通过反射指定类获取到的 属性并提供对方法函数数据的访问。
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
object[] ob = { state };
//通过mei调用所获得的函数,
//第一个参数null代表this指针,由于消息处理方法都是静态方法,
//第二个参数ob代表的是参数列表
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Receive SocketException" + ex.ToString());
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 Close");
return false;
}
//消息处理
string recvStr =
System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
//有时候客户端连续发送两条消息给服务器端的时候,可能会导致两条消息合并发送过来,后面了解到这叫TCP粘包
//这是由于底层的网络传输机制,多个小数据包可能会被合并成一个大数据包,这就导致了两个消息在一起发送的现象。
//所以我在每条消息前面加了一个*号,然后根据*来先进行一次分割,即使粘包了,也可以根据*号来进行拆分。
string[] allRecv = recvStr.Split('*');
for(int i=0;i<allRecv.Length;i++)
{
if (allRecv[i] == "") continue;
Console.WriteLine("Recv:" + allRecv[i]);//打印接收到的消息
string[] split = allRecv[i].Split('|');//使用‘|’进行二次分割
string msgName = split[0];//得到的第一个字符串就是协议的名字,比如Enter表示进入游戏,Move表示玩家移动等
string msgArgs = split[1];//第二个字符串就是执行协议所需要的相关的参数
string funName = "Msg" + msgName;
//所有的消息处理函数都在 MsgHandler 里面,且都是静态的,通过反射获得函数信息
MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
//这个例子所有的消息处理函数都需要这样两个参数ClientState c, string msgArgs
object[] o = { state, msgArgs };
mi.Invoke(null, o);
}
return true;
}
}
2、消息处理类:MsgHandler.cs
using System;
class MsgHandler
{
//Enter协议,表示有客户端进入游戏
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 MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
//List协议,用来获得所有当前正在游玩的玩家的 位置和朝向 信息。玩家刚进入游戏时,要通过这个协议来得到其他玩家信息
public static void MsgList(ClientState c, string msgArgs)
{
string sendStr = "*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(sendStr);
MainClass.Send(c, sendStr);
}
//Move协议,每个玩家的每次移动都要向服务器端发送move协议,然后服务器再转发给所有其他客户端,这样能显示其他玩家的移动
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);
}
}
//Attack协议,每个玩家的每次攻击都要向服务器端发送Attack协议,然后服务器再转发给所有其他客户端。
public static void MsgAttack(ClientState c, string msgArgs)
{
//广播
string sendStr = "Attack|" + msgArgs;
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
//Hit协议,玩家如果攻击到了其他玩家,就要向服务器发送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 MainClass.clients.Values)
{
if (cs.socket.RemoteEndPoint.ToString() == hitDesc)
hitCS = cs;
}
if (hitCS == null)
return;
//扣血
hitCS.hp -= 25;
//死亡
if (hitCS.hp <= 0)
{
Die协议,某个玩家hp小于0了,要告诉所有其他玩家某个玩家死亡了。
string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
}
}
3、事件处理类:EventHandler.cs
using System;
public class EventHandler
{
//某个客户端退出时调用,告诉其他客户端,有个客户端退出了
public static void OnDisconnect(ClientState c)
{
string desc = c.socket.RemoteEndPoint.ToString();
//Leave协议,客户端收到leave协议之后,就会把对应的玩家从场景中卸载
string sendStr = "*Leave|" + desc + ",";
Console.WriteLine("OnDisconnect:" + sendStr);
foreach (ClientState cs in MainClass.clients.Values)
{
MainClass.Send(cs, sendStr);
}
}
}
二、客户端
1、player父类:BaseHuman.cs
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;
// Use this for initialization
protected void Start()
{
animator = GetComponent<Animator>();//获得动画组件
}
// Update is called once per frame
internal void Update()
{
MoveUpdate();
AttackUpdate();
}
//移动到某处
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);
}
}
//攻击动作
public void Attack()
{
isAttacking = true;
attackTime = Time.time;//记录当前攻击的时间
animator.SetBool("isAttacking", true);
}
//攻击Update
public void AttackUpdate()
{
if (!isAttacking) return;
if (Time.time - attackTime < 1.2f) return;//冷却时间,1.2s内只能攻击一次
isAttacking = false;
animator.SetBool("isAttacking", false);
}
}
2、本客户端玩家类:CtrlHuman.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman
{
// Use this for initialization
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);
//提前把可移动的地面的tag设置为Terrain
if (hit.collider.tag == "Terrain")
{
MoveTo(hit.point);
//发送Move协议给服务器端,以此来更新其他客户端的位置信息
string sendStr = "*Move|";
sendStr += NetManager.GetDesc() + ",";
sendStr += hit.point.x + ",";
sendStr += hit.point.y + ",";
sendStr += hit.point.z + ",";
NetManager.Send(sendStr);
}
}
//右键攻击
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();
//攻击时给服务器发送attack协议,然后其他客户端才会显示攻击的动画等。
string sendStr = "*Attack|";
sendStr += NetManager.GetDesc() + ",";
sendStr += transform.eulerAngles.y + ",";
NetManager.Send(sendStr);
//攻击判定,发射一条线段判断是否触碰到其他玩家(SyncHuman),表示攻击到了此玩家
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;
//如果攻击到玩家之后,给服务器发送hit协议,
sendStr = "*Hit|";
sendStr += NetManager.GetDesc() + ",";
sendStr += h.desc + ",";
NetManager.Send(sendStr);
}
}
}
}
3、其他客户端玩家类:SyncHuman.cs
using UnityEngine;
public class SyncHuman : BaseHuman
{
// Use this for initialization
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();
}
}
4、通信管理类:NetManager.cs
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using System;
public static class NetManager
{
//定义套接字
static Socket socket;
//接收缓冲区
static byte[] readBuff = new byte[1024];
//委托类型
public delegate void MsgListener(string str);
//监听列表
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)
{
listeners[msgName] = listener;
}
//获取描述,获得客户端ip地址和端口号
public static string GetDesc()
{
if (socket == null) return "";
if (!socket.Connected) return "";
return socket.LocalEndPoint.ToString();
}
//连接
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);
string[] allrecv = recvStr.Split('*');//因为每条独立的消息都是以*为开头
foreach (string a in allrecv)
{
if (a == "") continue;
msgList.Add(a);//把消息放入消息列表
Debug.Log("接收消息:" + 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.Send(sendBytes);
}
//Update,处理消息列表的消息
public static void Update()
{
if (msgList.Count <= 0)
return;
string msgStr = msgList[0];
Debug.Log("处理消息:"+msgStr);
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];//第一项为协议名称
string msgArgs = split[1];
//监听回调;
if (listeners.ContainsKey(msgName))
{
//调用对应协议的委托函数
listeners[msgName](msgArgs);
}
}
}
5、通信使用类:Main.cs
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
//人物模型预设
public GameObject humanPrefab;
//人物列表
public BaseHuman myHuman;
public Dictionary<string, BaseHuman> otherHumans;
void Start()
{
otherHumans = new();
//网络模块,添加协议以及对应委托函数
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 = 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>();//添加CtrlHuman组件
myHuman.desc = NetManager.GetDesc();
//发送Enter协议,告诉其他玩家,有新玩家进入游戏
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|");
}
private void Update()
{
NetManager.Update();
}
void OnEnter(string msgArgs)
{
Debug.Log("OnEnter:" + 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]);
//是自己
if (desc == NetManager.GetDesc())
return;
//添加一个角色(其他玩家)
GameObject obj = Instantiate(humanPrefab);
obj.transform.position = new Vector3(x, y, z);
obj.transform.eulerAngles = new Vector3(0, eulY, 0);
BaseHuman h = obj.AddComponent<SyncHuman>();//其他玩家添加SyncHuman组件即可
h.desc = desc;
otherHumans.Add(desc, h);
}
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);
}
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);
}
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 = 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 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);
}
}