【从零开始游戏开发】Unity 前后端网络通信该如何搭建?注释解答 | 全面总结 |建议收藏
创始人
2024-04-04 11:39:20
0

你知道的越多,你不知道的越多 🇨🇳🇨🇳🇨🇳
点赞再看,养成习惯,别忘了一键三连哦 👍👍👍
文章持续更新中 📝📝📝



通信该如何搭建?

服务器端:

1. 入口类(Program):

static void Main(string[] args)
{//1. 构造网络服务类: NetServer net = new NetServer();//2. 调用初始化方法: net.Init();//3. 调用开始方法:net.Start();//4. 死循环处理指令的输入Run();//5. 当循环跳出关闭服务器 调用停止服务方法net.Stop();
}public static void Run()
{bool run = true;while (run){//1. 处理输入的指令 输入 Exit 指令会关闭服务器}
}

2. 网络服务器类(Net Server):

public void Init()
{//1. 绑定一个IP与端口号:IPEndPoint ipPoint = new IPEndPoint(IP, Port);//2. 构造一个监听类:ServerListener = new TcpSocketListener(ipPoint);//3. 给监听类绑定一个Socket连接事件:ServerListener.SocketConnected += OnSocketConnected;
}public void Start()
{//1. 调用监听类开始方法:Log.Warning("Starting Listener...");ServerListener.Start();//2. 调用消息分发器类开始函数MessageDistributer>.Instance.Start(8);Log.Warning("NetService Started");
}public void Stop()
{//1. 调用监听类停止方法ServerListener.Stop();
}//监听类监听到了对象连接,最后执行此方法
private void OnSocketConnected(object sender, Socket e)
{//1. 获取连接的IP端地址 IPEndPoint clientIP = (IPEndPoint)e.RemoteEndPoint;//2. 构造一个 连接模型SocketAsyncEventArgs args = new SocketAsyncEventArgs();//3. 构造一个 NetSessionNetSession session = new NetSession();//4. 构造一个 连接类 NetConnectionNetConnection connection = new NetConnection()
}//传递给连接类的接收数据事件
void DataReceived(NetConnection sender, DataEventArgs e)
{//打印一条信息Log.WarningFormat("Client[{0}] DataReceived Len:{1}", e.RemoteEndPoint, e.Length);//1. 处理接收到的数据lock (sender.packageHandler){// 调用消息处理类的 接收数据方法sender.packageHandler.ReceiveData(e.Data, 0, e.Data.Length);}
}//传递给连接类的断开连接事件
void Disconnected(NetConnection sender, SocketAsyncEventArgs e)
{//打印一条信息Log.WarningFormat("Client[{0}] Disconnected", e.RemoteEndPoint);
}

3. 监听类(TCP Socket Listener)

private Socket m_ListenSocket;		//监听Socket
private SocketAsyncEventArgs args;  //连接的一种模型//构造函数
public TcpSocketListener(IPEndPoint endPoint)
{//1. 实例化连接模型args = new SocketAsyncEventArgs();//2. 加上监听事件,到监听到有对象连接会执行args.Completed += OnSocketAccepted; 
}public void Start()
{lock (this)	//加 Lock 是以为 此类会有多个客户端同时发起连接 {//1. 初始化Socket 绑定终端 然后 开始监听:m_ListenSocket.Listen(0);//2. 监听到有连接执行:BeginAccept(args);}        
}public void Stop()
{lock (this){if (listenerSocket == null)retutn;//1. 关闭监听套接字listenerSocket.Close();listenerSocket = null;}
}private void BeginAccept(SocketAsyncEventArgs args)
{//1. 制空 否则会报错 args.AcceptSocket = null;			//2. 得到监听到的对象m_ListenSocket.AcceptAsync(args); 
}//监听事件
private void OnSocketAccepted(object sender, SocketAsyncEventArgs e)
{//1. 获取或设置异步套接字操作的结果。SocketError error = e.SocketError; //容错处理if (e.SocketError == SocketError.OperationAborted) retutn;//2. 连接成功if (e.SocketError == SocketError.Success){//1. 获取到连接的套接字:Socket handler = e.AcceptSocket;//2. 执行连接事件:OnSocketConnected(handler);}lock (this){BeginAccept(e);	//达到循环监听}
}//连接事件
private void OnSocketConnected(Socket client)
{//1. 执行连接事件: SocketConnected?.Invoke(this, client);
}//定义连接事件 网络服务器类初始化时 将此事件进行了赋值
public event EventHandler SocketConnected;//析构函数 结束时
~TcpSocketListener()
{Dispose(false);
}//此类继承了 IDisposable 实现接口
public void Dispose()
{Dispose(true);//请求公共语言运行时不要调用指定对象的终结器。手动调用了Dispose释放资源,那么析构函数就是不必要的了,这里阻止GC调用析构函数GC.SuppressFinalize(this);
}//处理结束函数 bool 参数,防止 析构函数清理资源一次 之后 Dispose 又一次清理
private void Dispose(bool disposing)
{if (!this.disposed){if (disposing){Stop();if (args != null)args.Dispose();}disposed = true;}       
}

4. 连接类(Net Connection <泛型类 T > )

/* 1. 对此类的介绍
每当监听类监听到有对象连接,执行了对应的事件,会构造一个当前类。
当前为泛型类 泛型 T 为 NetSession 类,此类缓存了连接对象的数据。
可以说是,每个连接的客户端都 对应了一个当前类,之后当前类会为客户端所服务。
*/
public class NetConnection
{// 定义 接收数据的回调 (委托)public delegate void DataReceivedCallback(NetConnection sender, DataEventArgs e);// 定义 断开连接的回调 (委托)public delegate void DisconnectedCallback(NetConnection sender, SocketAsyncEventArgs e);internal class State{public DataReceivedCallback dataReceived;			//接收数据委托变量public DisconnectedCallback disconnectedCallback;	//断开连接委托变量public Socket socket;}// 异步套接字操作private SocketAsyncEventArgs eventArgs;// 消息处理类对象public PackageHandler> packageHandler;// 存储玩家数据类的变量private T session;public T Session { get { return session; } }//构造函数public NetConnection(Socket socket, SocketAsyncEventArgs args, DataReceivedCallback dataReceived,DisconnectedCallback disconnectedCallback, T session){lock (this)	// 因为类对 SocketAsyncEventArgs 此类进行操作时候 必须要加Lock 否则会出现报错{//构造一个 消息处理器this.packageHandler = new PackageHandler>(this);State state = new State(){//1. 持有对构造方传递过来的值socket = socket,dataReceived = dataReceived,disconnectedCallback = disconnectedCallback}//1. 初始化eventArgs = new SocketAsyncEventArgs();//2. 对 eventArgs 进行赋值eventArgs.AcceptSocket = socket;			//连接的SocketeventArgs.Completed += ReceivedCompleted;	//添加接收数据事件eventArgs.UserToken = state;				//将 对象 缓存eventArgs.SetBuffer(new byte[64 * 1024],0, 64 * 1024);	//设置 接收字节缓冲区this.session = session;		//将类缓存//执行消息接收BeginReceive(eventArgs);	}}//接收消息方法private void BeginReceive(SocketAsyncEventArgs args){lock (this){//取得socketSocket socket = (args.UserToken as State).socket;//判断socket的连接状态if (socket.Connected){//开启异步请求,接收来自连接的数据args.AcceptSocket.ReceiveAsync(args);//当接收到了数据就会执行,此类构造时,赋值的接收数据事件}}}//接收数据事件    private void ReceivedCompleted(Object sender, SocketAsyncEventArgs args){// 判断接收到的字节数if (args.BytesTransferred == 0){CloseConnection(args); //Graceful disconnectreturn;}// 判断造成结果,是否成功if (args.SocketError != SocketError.Success){CloseConnection(args); //NOT graceful disconnectreturn;}//1. 取得 对象  【这里又是对 UserToken 的应用】State state = args.UserToken as State;//2. 构建一个接收到的字节数长度的字节数组Byte[] data = new Byte[args.BytesTransferred];//3. 将 args 对象的字节数组内的数据 复制到 dataArray.Copy(args.Buffer, args.Offset, data, 0, data.Length);//4. 执行接收到数据的事件OnDataReceived(data, args.RemoteEndPoint as IPEndPoint, state.dataReceived);//5. 达到循环接收BeginReceive(args);}//调用接收数据事件的方法  事件是网络服务器类 构造此类是 传递来的private void OnDataReceived(Byte[] data, IPEndPoint remoteEndPoint, DataReceivedCallback callback){callback(this, new DataEventArgs() { RemoteEndPoint = remoteEndPoint, Data = data, Offset =0, Length = data.Length  });}//断开连接的方法private void CloseConnection(SocketAsyncEventArgs args){//1. 从对象中获取到 Socket的对象	【这里就体现出 UserToken 这个属性的作用,当然不止这些】State state = args.UserToken as State;Socket socket = state.socket;try{socket.Shutdown(SocketShutdown.Both);	//禁止掉 Socket 的接收和发送}catch { } // throws if client process has already closed//2. 关闭socketsocket.Close();socket = null;//3. 去掉接收数据的事件,没有这步会出现报错args.Completed -= ReceivedCompleted; //MUST Remember This!//4. 执行断开连接的事件OnDisconnected(args, state.disconnectedCallback);}//断开连接的事件,第二个参数 在网络服务器类构造此类时传递的事件方法private void OnDisconnected(SocketAsyncEventArgs args, DisconnectedCallback callback){callback(this, args);}
}

5. 字节数组类(Data Event ARGs)

public class DataEventArgs : EventArgs
{public IPEndPoint RemoteEndPoint { get; set; }public Byte[] Data { get; set; }public Int32 Offset { get; set; }public Int32 Length { get; set; }
}

6. 消息包处理类(Package Handler)

public class PackageHandler
{// 字节数据缓冲区private MemoryStream stream = new MemoryStream(64 * 1024);//字节读取索引private int readOffset = 0;//接收 数据方法,调用者:网络服务器类public void ReceiveData(byte[] data,int offset,int count){//判断缓存区加上一定长度的数据会不会超过缓存的容量if(stream.Position + count > stream.Capacity){throw new Exception("PackageHandler write buffer overflow");}//1. 将数据写入到 缓冲区stream.Write(data, offset, count);//2. 解析消息包方法ParsePackage();}//数据包解析方法bool ParsePackage(){/*这里为什么要加 4,这个判断很关键因为第一次接收 读取索引为0,加4可以判断,数据最基本的长度,如果这个都没有,那么这个包就没必要解析*/if (readOffset + 4 < stream.Position){//1. 获取包内容的大小int packageSize = BitConverter.ToInt32(stream.GetBuffer(), readOffset);//2. 这里的 读取索引readOffset,感觉加没加都一样,根据整个方法代码,这个变量在这的作用为0//但是这步判断很重要,包大小 加 4 如果小于等于成立,说明消息包是完整的,可解if (packageSize + readOffset + 4 <= stream.Position){//3. 调用通过字节提取消息类Message.NetMessage message = UnpackMessage(stream.GetBuffer(), this.readOffset + 4, packageSize);if (message == null){throw new Exception("PackageHandler ParsePackage faild,invalid package");//容错处理}//4. 得到了 类,调用消息分发器传递该类,进行处理MessageDistributer.Instance.ReceiveMessage(this.sender, message);//5. 将读取索引加上 整条完整数据长度this.readOffset += (packageSize + 4);//6. 返回这个类,是为了再次判断是否,有粘包,分包处理return ParsePackage();}}//当消息接收处理过一次,读取索引必定大于0if (this.readOffset > 0){//1. 这里是获取可能存在没有其他不完整的数据long size = stream.Position - this.readOffset;//2. 如果这个小于,说明缓冲区还有数据if (this.readOffset < stream.Position){Array.Copy(stream.GetBuffer(), this.readOffset, stream.GetBuffer(), 0, stream.Position - this.readOffset);}//3. Reset Streamthis.readOffset = 0;stream.Position = size;stream.SetLength(size);}return true;//结束}//提取消息类public static Message.NetMessage UnpackMessage(byte[] packet,int offset,int length){Message.NetMessage message = null;using (MemoryStream ms = new MemoryStream(packet, offset, length)){message = ProtoBuf.Serializer.Deserialize(ms);}return message;}// -------- 客户端专用构造方式 --------public class PackageHandler : PackageHandler{public PackageHandler(object sender) : base(sender){}}
}
 

7. 消息分发器类(MessageDistributer)单例类

public class MessageDistributer : Singleton>
{// 队列的消息类对象class MessageArgs{public T sender;public Message.NetMessage message;}// 消息类队列private Queue messageQueue = new Queue();// 自动重置事件,终止状态private AutoResetEvent threadEvent = new AutoResetEvent(true);// 线程执行状态private bool Running = false;// 开启的线程数量public int ThreadCount = 0;// 激活状态的线程数public int ActiveThreadCount = 0;// 处理消息包处理类传递的类  sender 是消息包处理类被构造时候,存放是 NetConnection 对象,public void ReceiveMessage(T sender ,Message.NetMessage message){// 将消息添加到消息队列	MessageArgsthis.messageQueue.Enqueue(new MessageArgs() { sender = sender, message = message });// 将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。threadEvent.Set();}// 多线程模式消息处理开始函数,参数为 工作线程的数量public void Start(int ThreadNum){this.ThreadCount = ThreadNum;// 进行限制if (this.ThreadCount < 1) this.ThreadCount = 1;if (this.ThreadCount > 1000) this.ThreadCount = 1000;// 激活线程执行状态Running = true;for (int i = 0; i < this.ThreadCount; i++){// 调用 QueueUserWorkItem 方法以将方法排队以便在线程池线程上执行。 为此,可将方法传递给 WaitCallback 委托。 委托具有签名ThreadPool.QueueUserWorkItem(new WaitCallback(MessageDistribute));}while (ActiveThreadCount < this.ThreadCount){// 工作状态的线程 小于 总线程,进入休眠	具体作用不得而知 =-=  可能只是为了减轻CPU压力哦~Thread.Sleep(100);}}// 消息处理线程private void MessageDistribute(Object stateInfo){Log.Warning("MessageDistribute thread start");try{// 如果不使用 Increment 和,则在 Decrement 执行前两个步骤后,线程可以被抢占ActiveThreadCount = Interlocked.Increment(ref ActiveThreadCount);while (Running){if (this.messageQueue.Count  == 0){// 消息队列没有消息时 阻止当前线程,直到当前 WaitHandle 收到信号。threadEvent.WaitOne();continue;}// 取出队列消息MessageArgs package = this.messageQueue.Dequeue();if (package.message.Request != null){// 请求消息不为空,执行请求事件消息发送	用于客户端~MessageDispatch.Instance.Dispatch(package.sender, package.message.Request);}if (package.message.Response != null){// 响应消息不为空,执行请求事件消息发送	用于服务端~MessageDispatch.Instance.Dispatch(package.sender, package.message.Response);}}catch{}finally	//必会执行的代码块 0.0  Increment Decrement,就和有生就有死一个道理{ActiveThreadCount = Interlocked.Decrement(ref ActiveThreadCount);Log.Warning("MessageDistribute thread end");}}}// 停止多线程模式消息处理器public void Stop(){Running = false;this.messageQueue.Clear();while (ActiveThreadCount > 0){threadEvent.Set();}Thread.Sleep(100);}// 定义的委托public delegate void MessageHandler(T sender, Tm message);// 委托字典private Dictionary messageHandlers = new Dictionary();// 就一个false 变量public bool ThrowException = false;// 消息派发类调用此方法,此方法也是每条通信数据最后的一处理,会执行谁订阅的方法public void RaiseEvent(T sender,Tm msg){string key = msg.GetType().Name;if (messageHandlers.ContainsKey(key)){// 在委托字典通过key 取得对应的委托MessageHandler handler = (MessageHandler)messageHandlers[key];if (handler != null){try{handler(sender, msg);}catch (System.Exception ex){Log.ErrorFormat("Message handler Fail");// 也不知道这里要干嘛 =-=, 感觉没啥大作用if (ThrowException)throw ex;}}else{Log.Warning("No handler subscribed for {0}" + msg.ToString());}}}// 订阅事件public void Subscribe(MessageHandler messageHandler){string type = typeof(Tm).Name;// 如果有相同的,则把之前的制空,重新赋值if (!messageHandlers.ContainsKey(type)){messageHandlers[type] = null;}messageHandlers[type] = (MessageHandler)messageHandlers[type] + messageHandler;}// 有订阅 当然有注销public void Unsubscribe(MessageHandler messageHandler){string type = typeof(Tm).Name;if (!messageHandlers.ContainsKey(type)){messageHandlers[type] = null;}messageHandlers[type] = (MessageHandler)messageHandlers[type] - messageHandler;}//------- 下面区域是只有客户端才会用到的方法-------public void Clear(){// 清空接收消息存储的容器this.messageQueue.Clear();}
}

8. 消息派发器类(MessageDispatch)单例类

// 在消息分发器类中,有订阅事件,此类只充当一个中介
public class MessageDispatch : Singleton>
{// 响应派发	RaiseEvent 消息分发器类中处理public void Dispatch(T sender, Message.NetMessageResponse message){if (message.userRegister != null) { MessageDistributer.Instance.RaiseEvent(sender, message.userRegister); }}// 请求派发public void Dispatch(T sender, SkillBridge.Message.NetMessageRequest message){if (message.userRegister != null) { MessageDistributer.Instance.RaiseEvent(sender,message.userRegister); }}
}

至此服务器端会用到的注释完毕

客户端:

网络类(NetClient)Mono单例类

class NetClient : MonoSingleton
{private IPEndPoint address;	 // 终端private Socket clientSocket;public bool running { get; set; }	// 网络服务器运行状态private bool connecting = false;	// 是否正在连接服务器void Awake(){running = true;}// 初始化函数 率先绑定 终端public void Init(string serverIP, int port){this.address = new IPEndPoint(IPAddress.Parse(serverIP), port);}// 连接服务器函数public void Connect(int times = DEF_TRY_CONNECT_TIMES){if (this.connecting){return;	//当前在连接中,直接跳出}if (this.clientSocket != null){//socket这东西,先关掉,在使用,每次使用前,先关掉,为什么呢? 如果你不做断线重连就可以忽略this.clientSocket.Close();	}if (this.address == default(IPEndPoint)){throw new Exception("Please Init first.");}this.connecting = true; // 正在连接//真正执行连接this.DoConnect();}// 开始连接void DoConnect(){Debug.Log("NetClient.DoConnect on " + this.address.ToString());try{if (this.clientSocket != null){this.clientSocket.Close();	//解释过}this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);this.clientSocket.Blocking = true;	//开启阻塞Debug.Log(string.Format("Connect[{0}] to server {1}", this.retryTimes, this.address) + "\n");//得到一个异步连接的结果~IAsyncResult result = this.clientSocket.BeginConnect(this.address, null, null);bool success = result.AsyncWaitHandle.WaitOne(1000);//1000 是连接等待结果的时间if (success){// success 为真,说明连接这档事成了, EndConnect 接收结果后,clientSocket 就可以发送接收消息了this.clientSocket.EndConnect(result);}}catch(SocketException ex){// 连接被拒绝 为什么被拒绝,有点数的都知道被拉黑名单了 ( •̀ ω •́ )✧if(ex.SocketErrorCode == SocketError.ConnectionRefused){this.CloseConnection();}Debug.LogErrorFormat("DoConnect SocketException Fail");}catch (Exception e){Debug.Log("DoConnect Exception:" + e.ToString() + "\n");}// 判断是否连接成功if (this.clientSocket.Connected){this.clientSocket.Blocking = false;	//关掉阻塞//this.RaiseConnected(0, "Success"); 这条注释,是因为会执行一个事件,重连事件,属于功能性}else{// 进行重连的事情}this.connecting = false; //连接完毕}// 关闭连接 没有参数,因为参数只是打印出关闭的原因public void CloseConnection(){this.connecting = false;if (this.clientSocket != null){this.clientSocket.Close();}//清空缓冲区MessageDistributer.Instance.Clear();	//因为这个类是公用的,断线就需要清理,此类最下面有客户端专用的方法区域this.sendQueue.Clear();	//清空发送消息队列this.receiveBuffer.Position = 0;	//接收消息的缓存区清0this.sendBuffer.Position = sendOffset = 0; //发送的缓冲区 和发送索引 清0}// 需要发送的消息队列private Queue sendQueue = new Queue();// 发送数据的缓冲区private MemoryStream sendBuffer = new MemoryStream();// 接收数据的缓冲区private MemoryStream receiveBuffer = new MemoryStream(64 * 1024);// 这个类的构造,看此类最下方public PackageHandler packageHandler = new PackageHandler(null);//发送消息public void SendMessage(NetMessage message){if (!running){return;}if (!this.Connected){// 这里呢,就是你没了连接服务器 就调用此方法,这里会忽略掉你的请求,先连接服务器this.receiveBuffer.Position = 0;this.sendBuffer.Position = sendOffset = 0;this.Connect();Debug.Log("Connect Server before Send Message!");return;}// 将消息添加到队列sendQueue.Enqueue(message);}public void Update(){if (!running){return; // 没有连接 跳出~}if (this.KeepConnect()){if (this.ProcessRecv()){// 保存是连接状态if (this.Connected){this.ProcessSend();	//发送过程MessageDistributer.Instance.Distribute();	//ProceeMessage(); 方法只有这一条代码 直接写了,处理派发~}}}}// 发送过程bool ProcessSend(){bool ret = false;try{if (this.clientSocket.Blocking){Debug.Log("this.clientSocket.Blocking = true\n");}bool error = this.clientSocket.Poll(0, SelectMode.SelectError);if (error){Debug.Log("ProcessSend Poll SelectError\n");this.CloseConnection();return false;}// 检测是否可写,这个只要socket正常,就一定是真ret = this.clientSocket.Poll(0, SelectMode.SelectWrite);if (ret){// 第一次肯定不大于因为都是0,执行完后,这里就大于了if (this.sendBuffer.Position > this.sendOffset){// 得到大小int bufsize = (int)(this.sendBuffer.Position - this.sendOffset);// 发送int n = this.clientSocket.Send(this.sendBuffer.GetBuffer(), this.sendOffset, bufsize, SocketFlags.None);if (n <= 0){this.CloseConnection();return false;}this.sendOffset += n;if (this.sendOffset >= this.sendBuffer.Position){//清空一定要做 this.sendOffset += n; if (this.sendOffset >= this.sendBuffer.Position) 其实可以不要this.sendOffset = 0;this.sendBuffer.Position = 0;this.sendQueue.Dequeue();}}else{if (this.sendQueue.Count > 0){// 上面说 可以不要 ,是因为 这里,每次只求一条NetMessage message = this.sendQueue.Peek();byte[] package = PackageHandler.PackMessage(message);// 写入数据this.sendBuffer.Write(package, 0, package.Length);}}}}}// 接收消息的过程方法bool ProcessRecv(){bool ret = false;try{if (this.clientSocket.Blocking)	//如果是阻塞的 就一直卡折,所以抛出个异常 提醒一下{Debug.Log("this.clientSocket.Blocking = true\n");}bool error = this.clientSocket.Poll(0, SelectMode.SelectError);if (error){// 检测到错误 所以关闭Debug.Log("ProcessRecv Poll SelectError\n");this.CloseConnection();return false;}ret = this.clientSocket.Poll(0, SelectMode.SelectRead);if (ret){// 检测到有可读的消息int n = this.clientSocket.Receive(this.receiveBuffer.GetBuffer(), 0, this.receiveBuffer.Capacity, SocketFlags.None);if (n <= 0){//接收到的长度为0.所以关闭,有消息过来大小为零的只有 关闭请求/响应 =-=this.CloseConnection();return false;}// 跟服务器端一样,接收数据的处理是一致的this.packageHandler.ReceiveData(this.receiveBuffer.GetBuffer(), 0, n);}}catch (Exception e){// 接收过程异常~Debug.Log("ProcessReceive exception:" + e.ToString() + "\n");this.CloseConnection();return false;}return true;}// 状态判断把,比较Update 一直在执行bool KeepConnect(){if (this.connecting)return false;if (this.address == null)return false;if (this.Connected)return true;//if (this.retryTimes < this.retryTimesTotal)	this.Connect(); //这个还是断线重连的处理return false;}
}

🎁🌻🌼🌸 粉丝福利来喽 🎁🌻🌼🌸

  1. 免费领取海量资源 🎁
    简历自测评分表、Unity职级技能表、面试题库、入行学习路径等
  2. 《Unity游戏开发五天集训营 》50个名额 🎁
    我给大家争取到了 50个《游戏开发五天集训营 》名额,原价198,前50个免费
    扫码加入,暗号小听歌
    即可参加ARPG狼人战斗系统、饥荒生存类游戏开发、回合制RPG口袋妖怪游戏等游戏开发训练营
  3. 额外抽奖机会🎁
    参加游戏训练营、还有机会获得大厂老师在线面试指导、或者有机会获得价值1998元的《Unity极速入门与实战》课程
🔻🔻🔻🔻 扫下方二维码,获取游戏开发福利,暗号小听歌 🔻🔻🔻🔻

相关内容

热门资讯

李光洁32天走7500公里吃8... 李光洁32天走7500公里吃8城,《拿一座城市下酒》这部纪录片怎么样?我觉得这部纪录片非常好,观看的...
每我世如你果只没界喜就欢的生一... 每我世如你果只没界喜就欢的生一爱有你过在想会。把这22个字组成一句话。我想过,如果在每一生只喜欢你,...
调查校园里的植物和动物说说有哪... 调查校园里的植物和动物说说有哪些动植物?可以分成几类?兰花,梅花可人丌··植物:乔木(杨树、柳树、银...
血脂高的原因? 血脂高的原因?血脂高的原因高血脂的诱因包括原发性和继发性两种:原发性高血脂症的病因:1、遗传因素。2...
为什么孩子总是重复看同一集动画... 为什么孩子总是重复看同一集动画片?是在传递这3个信号 小孩子爱看动画片是非常普遍的,动画片带给他们动...
终极三国里 49集刘备为什么这... 终极三国里 49集刘备为什么这么做?有没有官方回答?下集自己看吧 现在不会有官方回答的哟第一:可能真...
西式糕点制作大全的内容简介 西式糕点制作大全的内容简介《西式糕点制作大全》主要介绍了制作甜点的基本知识,例如各种制作工具,制作点...
逻辑思维又是什么?就是推理吗,... 逻辑思维又是什么?就是推理吗,怎么培养!?逻辑思维是一种严格分析思维。不一定是推理。推理是逻辑思维的...
囊萤夜读有一句俗语就是出自这个... 囊萤夜读有一句俗语就是出自这个故事你知道是什么吗?囊萤映雪 ( náng yíng yìng xuě...
什么是不伦恋情? 什么是不伦恋情?什么是不伦恋情.,?男跟女年龄相差很大?还是?男的比女的小?还是老夫少妻?是近亲谈恋...
有书名带晨星的嘛? 有书名带晨星的嘛?有书名带晨星的嘛?晨星传这本书。漫画书晨星物语
如果有些事情说不出口怎么办? 如果有些事情说不出口怎么办?烦恼皆是因为自己过分的执着 即使你在这样子下去 更不就不会有好的结果 为...
有哪些类似于《非自然死亡》题材... 有哪些类似于《非自然死亡》题材的日剧推荐?非自然死亡的题材电影确实不多,电视剧的话也不好找啊。不喜欢...
素书全集的内容简介 素书全集的内容简介 本书采用了《素书》的权威原著,参照《四库全书》并加上了宋代宰相张商英的注和清代王...
【世纪花园】小区对口的学校有重... 【世纪花园】小区对口的学校有重点小学和初中吗?世纪花园东区里有未来强者幼儿园,小区南边有个华兴小区,...
东南大学现有的专业中有哪些是属... 东南大学现有的专业中有哪些是属于老东南的1928年学校改名为国立中央大学,设理、工、医、农、文、法、...
一个男人一有钱就请朋友吃饭,没... 一个男人一有钱就请朋友吃饭,没钱就又说,买东西还赊账,商店里的老板都找上门来了?像陵念前这种男人的话...
坟上栽什么草好? 坟上栽什么草好?坟地种什么草好耐旱坟上栽野蕨草、扎根不深、浅根植物、可以固土、南方雨水多、不会造成坟...
关于国富潜力基金 关于国富潜力基金我9月24日上午买的国富基金,申购价格是9月24日开盘的价格吗?还是9月28日开盘的...
我是一个高中生。想学武术。在学... 我是一个高中生。想学武术。在学校没什么时间。是练散打还是跆拳道好。我是一个高中生。想学武术。在学校没...