角色类
基类Base Human是基础的角色类,它处理"操控角色"和"同步角色"的一些共有功能;CtrlHuman类代表"操控角色",它在BaseHuman类的基础上处理鼠标操控功能;SyncHuman类是"同步角色"类,它也继承自BaseHuman,并处理网络同步(如果有必要)。
BaseHuman
cs
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 = "";
//移动到某处
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);
}
}
// Use this for initialization
protected void Start () {
animator = GetComponent<Animator>();
}
// Update is called once per frame
protected void Update () {
MoveUpdate();
}
}
CtrlHuman
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CtrlHuman : BaseHuman
{
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 == "Terrain") {
MoveTo(hit.point);
}
}
}
}
如何使用网络模块
在实际的网络游戏开发中,网络模块往往是作为一个底层模块用的,它应该和具体的游戏逻辑分开,而不应该把处理逻辑的代码写到 ReceiveCallback 里面去。因为ReceiveCallback应当只处理网络数据,不应该去处理游戏功能
一个可行的做法是,给网络管理类添加回调方法,当收到某种消息时就自动调用某个函数,这样便能够将游戏逻辑和底层模块分开。制作网络管理类前,需要先了解委托、协议和消息队列这三个概念。
通信协议
通信协议是通信双方对数据传送控制的一种约定,通信双方必须共同遵守,方能"知道对方在说什么"和"让对方听懂我的话"。
使用一种最简单的字符串协议来实现。协议格式如下所示,消息名和消息体用"|"隔开,消息体中各个参数用", "隔开。
消息名|参数1, 参数2, 参数3, ...
Move|127.0.0.1:1234, 10, 0, 8,
处理数据:
cs
string str = "Move|127.0.0.1:1234, 10, 0,8, ";
string[] args = str.Split('|');
string msgName = args[0]; //协议名:Move
string msgBody = args[1]; //协议体:127.0.0.1:1234, 10, 0,8,
string[] bodyArgs = msgBody.Split(', ');
string desc = bodyArgs [0]; //玩家描述:127.0.0.1:1234
float x = float.Parse(bodyArgs [1]); //x坐标:10
float y = float.Parse(bodyArgs [2]); //y坐标:0
float z = float.Parse(bodyArgs [3]); //z坐标:8
消息队列
多线程消息处理虽然效率较高,但非主线程不能设置Unity3D组件 ,而且容易造成各种莫名其妙的混乱。由于单线程消息处理足以满足游戏客户端的需要,因此大部分游戏会使用消息队列让主线程去处理异步Socket接收到的消息。
C#的异步通信由线程池实现,不同的BeginReceive不一定在同一线程中执行。创建一个消息列表,每当收到消息便在列表末端添加数据,这个列表由主线程读取,它可以作为主线程和异步接收线程之间的桥梁。由于MonoBehaviour的Update方法在主线程中执行,可让Update方法每次从消息列表中读取几条信息并处理,处理后便在消息列表中删除它们
NetManager类
网络模块中最核心的地方是一个称为NetManager的静态类,这个类对外提供了三个最主要的接口。
- Connect方法,调用后发起连接;
- AddListener方法,消息监听。其他模块可以通过AddListener设置某个消息名对应的处理方法,当网络模块接收到这类消息时,就会回调处理方法;
- Send方法,发送消息给服务端。
无论内部实现有多么复杂,网络模块对外的接口只有图片展示的这几个:
对内部而言,NetManager使用了异步Socket接收消息,每次接收到一条消息后,NetManager会把消息存入消息队列中。NetManager有一个供外部调用的Update方法,每当调用它时就会处理消息队列里的第一条消息,然后根据协议名将消息分发给对应的回调函数
cs
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);
//监听列表
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;
}
//获取描述
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);
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.Send(sendBytes);
}
//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);
}
}
}
漏洞
上述代码没有处理粘包分包、线程冲突等问题