- using System.Collections.Generic;
-
- using UnityEngine;
-
- using UnityEngine.UI;
-
-
-
- public class Answer : MonoBehaviour
-
- {
-
- //读取文档
-
- string[][] ArrayX;//题目数据
-
- string[] lineArray;//读取到题目数据
-
- private int topicMax = 0;//最大题数
-
- private List<bool> isAnserList = new List<bool>();//存放是否答过题的状态
-
-
-
- //加载题目
-
- public GameObject tipsbtn;//提示按钮
-
- public Text tipsText;//提示信息
-
- public List
toggleList;//答题Toggle -
- public Text indexText;//当前第几题
-
- public Text TM_Text;//当前题目
-
- public List
DA_TextList;//选项 -
- private int topicIndex = 0;//第几题
-
-
-
- //按钮功能及提示信息
-
- public Button BtnBack;//上一题
-
- public Button BtnNext;//下一题
-
- public Button BtnTip;//消息提醒
-
- public Text TextAccuracy;//正确率
-
- private int anserint = 0;//已经答过几题
-
- private int isRightNum = 0;//正确题数
-
-
-
- private List<int> randomNum = new List<int>(); //存放随机数
-
-
-
- void Awake()
-
- {
-
- TextCsv();
-
- LoadAnswer();
-
- }
-
- void Start()
-
- {
-
- toggleList[0].onValueChanged.AddListener((isOn) => AnswerRightRrongJudgment(isOn,0));
-
- toggleList[1].onValueChanged.AddListener((isOn) => AnswerRightRrongJudgment(isOn,1));
-
- toggleList[2].onValueChanged.AddListener((isOn) => AnswerRightRrongJudgment(isOn,2));
-
- toggleList[3].onValueChanged.AddListener((isOn) => AnswerRightRrongJudgment(isOn,3));
-
-
-
- BtnTip.onClick.AddListener(() => Select_Answer(0));
-
- BtnBack.onClick.AddListener(() => Select_Answer(1));
-
- BtnNext.onClick.AddListener(() => Select_Answer(2));
-
- }
-
- /*****************读取txt数据******************/
-
- void TextCsv()
-
- {
-
- //读取csv二进制文件
-
- TextAsset binAsset = Resources.Load("YW", typeof(TextAsset)) as TextAsset;
-
- //读取每一行的内容
-
- lineArray = binAsset.text.Split('\r');
-
- //创建二维数组
-
- ArrayX = new string[lineArray.Length][];
-
- //把csv中的数据储存在二维数组中
-
- for (int i = 0; i < lineArray.Length; i++)
-
- {
-
- ArrayX[i] = lineArray[i].Split(':');
-
- }
-
- //设置题目状态
-
- topicMax = lineArray.Length;
-
- for (int x = 0; x < topicMax + 1; x++)
-
- {
-
- isAnserList.Add(false);
-
- }
-
- }
-
- /*****************加载题目******************/
-
- void LoadAnswer()
-
- {
-
- for (int i = 0; i < toggleList.Count; i++)
-
- {
-
- toggleList[i].isOn = false;
-
- }
-
- for (int i = 0; i < toggleList.Count; i++)
-
- {
-
- toggleList[i].interactable = true;
-
- }
-
- GetRandomNum(); //获得随机数
-
- tipsbtn.SetActive(false);
-
- tipsText.text = "";
-
- indexText.text = "第" + (topicIndex + 1) + "题:";//第几题
-
- TM_Text.text = ArrayX[randomNum[topicIndex]][1];//题目
-
- int idx = ArrayX[topicIndex].Length - 3;//有几个选项
-
- for (int x = 0; x < idx; x++)
-
- {
-
- DA_TextList[x].text = ArrayX[randomNum[topicIndex]][x + 2];//选项
-
- }
-
- }
-
- /*****************按钮功能******************/
-
- void Select_Answer(int index)
-
- {
-
- switch (index)
-
- {
-
- case 0://提示
-
- int idx = ArrayX[randomNum[topicIndex]].Length - 1;
-
- int n = int.Parse(ArrayX[randomNum[topicIndex]][idx]);
-
- string nM = "";
-
- switch (n)
-
- {
-
- case 1:
-
- nM = "A";
-
- break;
-
- case 2:
-
- nM = "B";
-
- break;
-
- case 3:
-
- nM = "C";
-
- break;
-
- case 4:
-
- nM = "D";
-
- break;
-
- }
-
- tipsText.text = "
" +"正确答案是:"+ nM + ""; -
- break;
-
- case 1://上一题
-
- if (topicIndex > 0)
-
- {
-
- topicIndex--;
-
- LoadAnswer();
-
- }
-
- else
-
- {
-
- tipsText.text = "
" + "前面已经没有题目了!" + ""; -
- }
-
- break;
-
- case 2://下一题
-
- if (topicIndex < topicMax - 1)
-
- {
-
- topicIndex++;
-
- LoadAnswer();
-
- }
-
- else
-
- {
-
- tipsText.text = "
" + "哎呀!已经是最后一题了。" + ""; -
- }
-
- break;
-
- }
-
- }
-
-
-
- /*****************题目对错判断******************/
-
- void AnswerRightRrongJudgment(bool check,int index)
-
- {
-
- if (check)
-
- {
-
- //判断题目对错
-
- bool isRight;
-
- int idx = ArrayX[randomNum[topicIndex]].Length - 1;
-
- int n = int.Parse(ArrayX[randomNum[topicIndex]][idx]) - 1;
-
- if (n == index)
-
- {
-
- tipsText.text = "
" + "恭喜你,答对了!" + ""; -
- isRight = true;
-
- tipsbtn.SetActive(true);
-
- }
-
- else
-
- {
-
- tipsText.text = "
" + "对不起,答错了!" + ""; -
- isRight = false;
-
- tipsbtn.SetActive(true);
-
- }
-
-
-
- //正确率计算
-
- if (isAnserList[randomNum[topicIndex]])
-
- {
-
- tipsText.text = "
" + "这道题已答过!" + ""; -
- }
-
- else
-
- {
-
- anserint++;
-
- if (isRight)
-
- {
-
- isRightNum++;
-
- }
-
- isAnserList[randomNum[topicIndex]] = true;
-
- TextAccuracy.text = "正确率:" + ((float)isRightNum / anserint * 100).ToString("f2") + "%";
-
- }
-
-
-
- //禁用掉选项
-
- for (int i = 0; i < toggleList.Count; i++)
-
- {
-
- toggleList[i].interactable = false;
-
- }
-
- }
-
- }
-
- /*****************获得随机数******************/
-
- void GetRandomNum()
-
- {
-
- HashSet<int> nums = new HashSet<int>();
-
- System.Random r = new System.Random();
-
- while (nums.Count != topicMax)
-
- {
-
- nums.Add(r.Next(0, topicMax));
-
- }
-
- //Debug.Log(topicMax);
-
- foreach (var item in nums)
-
- {
-
- randomNum.Add(item);
-
- }
-
- }
-
- }
- using System.Net;
-
- using System.Net.Sockets;
-
- using System.IO;
-
- using System.Runtime.Serialization.Formatters.Binary;
-
- using System.Text;
-
- ///
-
- /// 网络工具类
-
- ///
-
- public static class NetworkUtils
-
- {
-
- //序列化:obj -> byte[]
-
- public static byte[] Serialize(object obj)
-
- {
-
- //对象必须被标记为Serializable
-
- if (obj == null || !obj.GetType().IsSerializable)
-
- return null;
-
- BinaryFormatter formatter = new BinaryFormatter();
-
- using (MemoryStream stream = new MemoryStream())
-
- {
-
- formatter.Serialize(stream, obj);
-
- byte[] data = stream.ToArray();
-
- return data;
-
- }
-
- }
-
-
-
- //反序列化:byte[] -> obj
-
- public static T Deserialize<T>(byte[] data) where T : class
-
- {
-
- //T必须是可序列化的类型
-
- if (data == null || !typeof(T).IsSerializable)
-
- return null;
-
- BinaryFormatter formatter = new BinaryFormatter();
-
- using (MemoryStream stream = new MemoryStream(data))
-
- {
-
- object obj = formatter.Deserialize(stream);
-
- return obj as T;
-
- }
-
- }
-
- }
- ///
-
- /// 棋子类型
-
- ///
-
- public enum Chess
-
- {
-
- //棋子类型
-
- None, //空棋
-
- Black,//黑棋
-
- White,//白棋
-
-
-
- //以下用于胜利判断结果和操作结果
-
- Draw, //平局
-
- Null, //表示无结果(用于用户操作失败情况下的返回值)
-
- }
- ///
-
- /// 消息类型
-
- ///
-
- public enum MessageType
-
- {
-
- None, //空类型
-
- HeartBeat, //心跳包验证
-
-
-
- //以下为玩家操作请求类型
-
- Enroll, //注册
-
- CreatRoom, //创建房间
-
- EnterRoom, //进入房间
-
- ExitRoom, //退出房间
-
- StartGame, //开始游戏
-
- PlayChess, //下棋
-
- }
每种玩家操作请求类型都对应一个类(Class),这个类包含客户端向服务器发送的属性,也包含服务器向客户端回应的属性。
以创建房间为例:
[Serializable] //加上C#自带的可序列化特性就可以把该类型序列化了
public class CreatRoom
{
public int RoomId; //房间号码,客户端向服务器发送的属性
public bool Suc; //是否成功,服务器向客户端发送的属性
}
由于协议类型过多,这里只展示一种协议的序列化与反序列化。
byte[] data = NetworkUtils.Serialize(new CreatRoom(){ RoomId = 8848 }); //序列化
CreatRoom room = NetworkUtils.Deserialize
首先我们用VisualStudio创建一个消息协议的C#类库项目.
然后在项目属性里面把输出类型改为类库(DLL)。
对于unity版本低于2017的,应该把目标框架改为.Net Framework 3.5及以下
然后建立一个命名空间:Multiplay,并把上面的协议类型枚举放在命名空间中即可。
以下客户端向服务器之间协议的具体类型:
- [Serializable]
-
- public class Enroll
-
- {
-
- public string Name;//姓名
-
-
-
- public bool Suc; //是否成功
-
- }
-
-
-
- [Serializable]
-
- public class CreatRoom
-
- {
-
- public int RoomId; //房间号码
-
-
-
- public bool Suc; //是否成功
-
- }
-
-
-
- [Serializable]
-
- public class EnterRoom
-
- {
-
- public int RoomId; //房间号码
-
-
-
- public Result result; //结果
-
- public enum Result
-
- {
-
- None,
-
- Player,
-
- Observer,
-
- }
-
- }
-
-
-
- [Serializable]
-
- public class ExitRoom
-
- {
-
- public int RoomId; //房间号码
-
-
-
- public bool Suc; //是否成功
-
- }
-
-
-
- [Serializable]
-
- public class StartGame
-
- {
-
- public int RoomId; //房间号码
-
-
-
- public bool Suc; //是否成功
-
- public bool First; //是否先手
-
- public bool Watch; //是否是观察者
-
- }
-
-
-
- [Serializable]
-
- public class PlayChess
-
- {
-
- public int RoomId; //房间号码
-
- public Chess Chess; //棋子类型
-
- public int X; //棋子坐标
-
- public int Y; //棋子坐标
-
-
-
- public bool Suc; //操作结果
-
- public Chess Challenger; //胜利者
-
- }
获取IP地址
在此介绍网络通信之前,我们编写一个方法并放在之前的NetworkUtils类中,可以快速获得本机IPv4地址(用于表示在网络上的位置),不然就得在cmd中手动输入ipconfig指令查看本机IPv4地址。在之后需要IPv4地址的时候,直接调用该方法即可,代码如下:
///
/// 获取本机IPv4,获取失败则返回null
///
public static string GetLocalIPv4()
{
string hostName = Dns.GetHostName(); //得到主机名
IPHostEntry iPEntry = Dns.GetHostEntry(hostName);
for (int i = 0; i < iPEntry.AddressList.Length; i++)
{
//从IP地址列表中筛选出IPv4类型的IP地址
if (iPEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork)
return iPEntry.AddressList[i].ToString();
}
return null;
}
OK,有了这些工具并知道了基础的概念,那么如何实现网络通信?
我们是基于TCP/IP协议在不同计算机之间进行网络通讯,但是我们总得有一个可以用C#调用的接口:就是服务器跟客户端进行网络操作的API方法。在System.Net.Sockets下,C#为我们贴心地封装了一个网络通讯的API类型:Socket(网络套接字)类型。
服务器网络
我们首先创建一个静态类:Server,其中包含服务器的所有网络操作。
在Server提供一个Start方法用于实例化Socket对象:
//实例化Socket类型 参数1:使用ipv4进行寻址 参数2:使用流进行数据传输 参数3:基于TCP协议
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
创建好socket对象之后需要把socket对象与IP终端对象(包含IP地址与端口号)进行绑定。以此确定服务器在网络空间中的位置与服务器这个程序所用的端口号。
如果把IP地址比作一间房子 ,端口就是出入这间房子的门。
使用端口号,可以找到一台设备上唯一的一个程序。 所以如果需要和某台计算机建立连接的话,只需要知道IP地址或域名即可,但是如果想和该台计算机上的某个程序交换数据的话,还必须知道该程序使用的端口号。
//IP终端
IPEndPoint point = new IPEndPoint(IPAddress.Parse(ip), 8848);
socket.Bind(point); //套接字绑定IP终端
socket.Listen(0); //开始监听来自其他计算机的连接
通过以上操作,服务器的Socket就已经具备监听其他计算机网络连接的功能了,接下来就准备实现服务器最核心的功能:等待客户端连接,接收客户端数据。
服务器概念
服务器采用开房间(Room)的机制(比如LOL中的一局比赛)进行游戏,并且会有一个集合保存所有玩家(Player)的,有一个字典保存所有房间,有一个回调方法队列。
先上一波Server类的数据结构:
using Multiplay; //使用协议
///
///
///
public static class Server
{
public static Dictionary
public static List
private static Socket _serverSocket; //服务器socket
}
稍后会介绍并用到这些属性与类型。
虽然服务器直接利用TCP协议与客户端进行长连接,但是服务器除了会保存玩家状态,数据收发逻辑与HTTP相似。在HTTP中,服务器不会保存客户端的状态,也不会主动向客户端发送信息,只有在客户端向服务器请求数据的时候,服务器才会向客户端发送响应数据。
关于HTTP与TCP/IP可参考:(HTTP与TCP的区别和联系 - CSDN博客)https://blog.csdn.net/u013485792/article/details/52100533
我们首先制作两个关键的数据类型用于保存关键数据。
服务器数据类型
Player类型,包含客户端Socket与玩家的基本信息:
public class Player
{
public Socket Socket; //网络套接字
public string Name; //玩家名字
public bool InRoom; //是否在房间中
public int RoomId; //所处房间号码
public Player(Socket socket)
{
Socket = socket;
Name = "Player Unknown";
InRoom = false;
RoomId = 0;
}
public void EnterRoom(int roomId)
{
InRoom = true;
RoomId = roomId;
}
public void ExitRoom()
{
InRoom = false;
}
}
Room类型,包含房间号码,房间状态,容纳人数,玩家与观察者的集合:
public class Room
{
public enum RoomState
{
Await, //等待
Gaming //对局开始
}
//房间ID
public int RoomId = 0;
//房间棋盘信息
public GamePlay GamePlay;
//房间状态
public RoomState State = RoomState.Await;
//最大玩家数量
public const int MAX_PLAYER_AMOUNT = 2;
//最大观察者数量
public const int MAX_OBSERVER_AMOUNT = 2;
public List
public List
public Room(int roomId) //构造
{
RoomId = roomId;
GamePlay = new GamePlay();
}
}
等待客户端连接
服务器的思路就是:开启服务器监听后,先开启一个线程(Thread)不断接受(Accept)客户端Socket的连接。每当有一个客户端连接上服务器,服务器会获取客户端的Socket并且启动一个线程去接收(Receive)这个客户端发送的信息。
服务器上一旦发生异常且没有处理,等于服务器直接挂掉。所有连接上服务器的客户端全部会失去连接。所以对于服务器来说要非常谨慎地编写关键部分的代码。
我们继续在Server类中编写,以下是等待客户端代码:
//在初始化方法中开启等待玩家线程
Thread thread = new Thread(_Await) { IsBackground = true };
thread.Start();
//等待客户端方法
private static void _Await()
{
Socket client = null;
while (true)
{
try
{
//同步等待,程序会阻塞在这里
client = _serverSocket.Accept();
//获取客户端唯一键
string endPoint = client.RemoteEndPoint.ToString();
//新增玩家
Player player = new Player(client);
Players.Add(player);
Console.WriteLine($"{player.Socket.RemoteEndPoint}连接成功");
//创建特定类型的方法
ParameterizedThreadStart receiveMethod =
new ParameterizedThreadStart(_Receive); //Receive方法在后面实现
Thread listener = new Thread(receiveMethod) { IsBackground = true };
listener.Start(player); //开启一个线程监听该客户端发送的消息
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
- ///
-
- /// 封装数据
-
- ///
-
- private static byte[] _Pack(MessageType type, byte[] data = null)
-
- {
-
- List<byte> list = new List<byte>();
-
- if (data != null)
-
- {
-
- list.AddRange(BitConverter.Getbytes((ushort)(4 + data.Length)));//消息长度2字节
-
- list.AddRange(BitConverter.Getbytes((ushort)type)); //消息类型2字节
-
- list.AddRange(data); //消息内容n字节
-
- }
-
- else
-
- {
-
- list.AddRange((ushort)4); //消息长度2字节
-
- list.AddRange((ushort)type); //消息类型2字节
-
- }
-
- return packer.ToArray();
-
- }
- //回调委托
-
- public delegate void ServerCallBack(Player client, byte[] data);
-
-
-
- //回调类型
-
- public class CallBack
-
- {
-
- public Player Player;
-
-
-
- public byte[] Data;
-
-
-
- public ServerCallBack ServerCallBack;
-
-
-
- public CallBack(Player player, byte[] data, ServerCallBack serverCallBack)
-
- {
-
- Player = player;
-
- Data = data;
-
- ServerCallBack = serverCallBack;
-
- }
-
-
-
- public void Execute()
-
- {
-
- ServerCallBack(Player, Data);
-
- }
-
- }
private static ConcurrentQueue
private static Dictionary
= new Dictionary
///
/// 注册消息回调事件
///
public static void Register(MessageType type, ServerCallBack method)
{
if (!_callBacks.ContainsKey(type))
_callBacks.Add(type, method);
else
Console.WriteLine("注册了相同的回调事件");
}
并且在服务器启动之前就把回调方法注册好,以下方法写在一个新的类型:Network中:
///
/// 启动服务器
///
/// IPv4地址
public Network(string ip)
{
//注册
Server.Register(MessageType.HeartBeat, _HeartBeat);
Server.Register(MessageType.Enroll, _Enroll);
Server.Register(MessageType.CreatRoom, _CreatRoom);
Server.Register(MessageType.EnterRoom, _EnterRoom);
Server.Register(MessageType.ExitRoom, _ExitRoom);
Server.Register(MessageType.StartGame, _StartGame);
Server.Register(MessageType.PlayChess, _PlayChess);
//启动服务器
Server.Start(ip);
}
- //在开启Await线程后,开启回调方法线程
-
- Thread handle = new Thread(_Callback) { IsBackground = true };
-
- handle.Start();
-
-
-
- private static void _Callback()
-
- {
-
- while (true)
-
- {
-
- if (_callBackQueue.Count > 0)
-
- {
-
- //使用TryDequeue保证线程安全
-
- if (_callBackQueue.TryDequeue(out CallBack callBack))
-
- {
-
- //执行回调
-
- callBack.Execute();
-
- }
-
- }
-
- //让出线程
-
- Thread.Sleep(10);
-
- }
-
- }
- private static void _Receive(object obj)
-
- {
-
- Player player = obj as Player;
-
- Socket client = player.Socket;
-
-
-
- //持续接受消息
-
- while (true)
-
- {
-
- //解析数据包过程(服务器与客户端需要严格按照一定的协议制定数据包)
-
- byte[] data = new byte[4];
-
-
-
- int length = 0; //消息长度
-
- MessageType type = MessageType.None; //类型
-
- int receive = 0; //接收信息
-
-
-
- try
-
- {
-
- receive = client.Receive(data); //同步接受消息
-
- }
-
- catch (Exception ex)
-
- {
-
- Console.WriteLine($"{client.RemoteEndPoint}已掉线:{ex.Message}");
-
- player.Offline();
-
- return;
-
- }
-
-
-
- //包头接收不完整
-
- if (receive < data.Length)
-
- {
-
- Console.WriteLine($"{client.RemoteEndPoint}已掉线");
-
- player.Offline();
-
- return;
-
- }
-
-
-
- //解析消息过程
-
- using (MemoryStream stream = new MemoryStream(data))
-
- {
-
- BinaryReader binary = new BinaryReader(stream, Encoding.UTF8);
-
- try
-
- {
-
- length = binary.ReadUInt16();
-
- type = (MessageType)binary.ReadUInt16();
-
- }
-
- catch (Exception)
-
- {
-
- Console.WriteLine($"{client.RemoteEndPoint}已掉线");
-
- player.Offline();
-
- return;
-
- }
-
- }
-
-
-
- //如果有包体
-
- if (length - 4 > 0)
-
- {
-
- data = new byte[length - 4];
-
- receive = client.Receive(data);
-
- if (receive < data.Length)
-
- {
-
- Console.WriteLine($"{client.RemoteEndPoint}已掉线");
-
- player.Offline();
-
- return;
-
- }
-
- }
-
- else
-
- {
-
- data = new byte[0];
-
- receive = 0;
-
- }
-
-
-
- Console.WriteLine($"接受到消息, 房间数:{Rooms.Count}, 玩家数:{Players.Count}");
-
-
-
- //回调机制机制
-
- if (_callBacks.ContainsKey(type))
-
- {
-
- CallBack callBack = new CallBack(player, data, _callBacks[type]);
-
- //放入回调队列
-
- _callBackQueue.Enqueue(callBack);
-
- }
-
- }
-
- }
客户端网络
连接服务器
创建好unity工程后先新建一个NetworkClient脚本直奔主题先。
首先要想该利用那些C#API连接上服务器呢?又如何向服务器收发数据?恩,在这里使用C#的API:TcpClient(连接服务器建立数据流),NetworkStream(在数据流中读写数据)
在NetworkClient脚本中,我们取消继承Monobehaviour而是把这个类型设置为静态类型。
与服务器类似,客户端也有消息回调字典,与一个待发送消息队列,需要发送消息只需把消息添加进这个队列,然后提供有专门的协程(Coroutine)去发送。
以下是部分NetworkClient类的部分属性:
using Multiplay; //使用协议
///
/// 客户端网络状态枚举
///
private enum ClientState
{
None, //未连接
Connected, //连接成功
}
//消息类型与回调字典
private static Dictionary
new Dictionary
//待发送消息队列
private static Queue
//当前状态
private static ClientState _curState;
//向服务器建立TCP连接并获取网络通讯流
private static TcpClient _client;
//在网络通讯流中读写数据
private static NetworkStream _stream;
//目标ip
private static IPAddress _address;
//端口号
private static int _port;
连接服务器之前肯定要先确定服务器的ip地址,端口号服务器跟客户端使用一个自定义的就行(0-65536),最好是8848这种比较靠后的数字,像1024之前的多半被操作系统分配给了其他应用程序。以下是ip地址与端口号初始化方法:
///
/// 初始化网络客户端
///
public static void Init(string address = null, int port = 8848)
{
//连接上后不能重复连接
if (_curState == ClientState.Connected)
return;
//如果为空则默认连接本机ip的服务器
if (address == null)
address = NetworkUtils.GetLocalIPv4();
//类型获取失败则取消连接
if (!IPAddress.TryParse(address, out _address))
return;
}
因为客户端同时担任收发数据的责任,我们得在Unity的不同协程(利用C#迭代器实现的类似Update的机制)中同时收发数据。
Unity协程中不建议用同步方法,大部分同步方法会主线程阻塞或导致死循环
因为NetworkClient类型是静态类型,无法直接使用StartCoroutine,因此我们可以提供一个继承自Monobehaviour的类型去提供这个方法。
以下是这个类型的实现,使用单例模式,提供一个程序退出时调用的委托来关闭网络连接:
public class NetworkCoroutine : MonoBehaviour
{
public Action ApplicationQuitEvent;
private static NetworkCoroutine _instance;
// 场景单例(不随场景改变而销毁)
public static NetworkCoroutine Instance
{
get
{
if (!_instance)
{
GameObject socketClientObj = new GameObject("NetworkCoroutine");
_instance = socketClientObj.AddComponent
DontDestroyOnLoad(socketClientObj);
}
return _instance;
}
}
// 程序退出
private void OnApplicationQuit()
{
if (ApplicationQuitEvent != null)
ApplicationQuitEvent();
}
}
然后我们在NetworkClient创建一个连接服务器的方法(Connect),下面是关键代码:
//以下代码放在一个叫Connect的协程方法中
_client = new TcpClient();
//异步连接服务器
IAsyncResult async = _client.BeginConnect(_address, _port, null, null);
while (!async.IsCompleted)
{
Debug.Log("连接服务器中");
yield return null;
}
//结束异步
_client.EndConnect(async);
//获取网络流
_stream = _client.GetStream();
_curState = ClientState.Connected;
_messages = new Queue
Debug.Log("连接服务器成功");
//设置异步发送消息
NetworkCoroutine.Instance.StartCoroutine(_Send());
//设置异步接收消息
NetworkCoroutine.Instance.StartCoroutine(_Receive());
//设置退出事件
NetworkCoroutine.Instance.ApplicationQuitEvent +=
() => { _client.Close(); _curState = ClientState.None; };
现在我们启动服务器,并在客户端新建一个Network类型脚本,放在场景中,然后可以在Start方法中调用NetworkClient的连接服务器方法尝试连接服务器。
好了。到目前为止,我们已经实现了消息的序列化、服务器房间的搭建以及服务器与客户端的基本通信。
介于字数原因,客户端gameplay部分与收发数据操作放在下一篇介绍。