WebRTC Video Chat
作者:追风剑情 发布于:2023-12-13 11:28 分类:Unity3d
WebRTC Video Chat
Unity跨平台视频通话插件WebRtcVideoChat,可实现PC、Android、IOS跨平台的视频通话,可语音、视频以及发送文字。
[百度网盘] WebRTC Video Chat 0.9854.unitypackage 提取码 ijys
[GitHub] WebRTC Signaling Server
注意:在Android平台上创建两个网络对象会报错 Channel is unrecoverably broken and will be disposed! 导致闪退。
注意:4G/5G网络无法建立P2P
视频画面需要逆时针旋转90度再显示,下面是利用Shader旋转uv
fixed4 frag(v2f i) : SV_Target { //逆时针旋转90度 fixed2 uv = fixed2(i.uv.y, i.uv.x); fixed4 col = tex2D(_MainTex, uv); return col; }
WebRTC辅助类
using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using Byn.Awrtc; using Byn.Awrtc.Unity; /// <summary> /// WebRtcVideoChat 辅助类 /// 同一个网络对象上只能创建一个房间,但可以加入多个房间 /// 注意:Android平台上不能同时创建多个网络对象(Network),会报错闪退。 /// </summary> public sealed class WebRTCHelper { #region 变量 //信令服务器地址 (这是官方的测试地址) public static string uSignalingUrl = "ws://signaling.because-why-not.com/chatapp"; //信令服务器地址,基于WebGL的浏览器用这个地址 (这是官方的测试地址) public static string uSecureSignalingUrl = "wss://signaling.because-why-not.com/chatapp"; //true: 强制使用 uSecureSignalingUrl public static bool uForceSecureSignaling = false; //NAT穿透服务器地址, Stun server。(这是官方的测试地址) public static readonly string stunUrl = "stun:stun.because-why-not.com:443"; //NAT穿透服务器地址, Turn server。(这是官方的测试地址) public static readonly string turnUrl = "turn:turn.because-why-not.com:443"; //Turn server user public static readonly string turnUser = "testuser140"; //Turn server password public static readonly string turnPass = "pass140"; //NAT穿透服务器地址 (Turn server 或 Stun server) public static string uIceServer = turnUrl; //备用服务器 public static string uIceServer2 = "stun:stun.l.google.com:19302"; public static string uIceServerUser = turnUser; public static string uIceServerPassword = turnPass; //可以是 native webrtc 或 browser webrtc //BasicNetwork用于收发自定义数据 private static IBasicNetwork mBasicNetwork = null; //谁开房间,谁就作为服务器方 private static bool mIsServer = false; //房间名允许的最长字符个数,不要使用中文 private const int MAX_CODE_LENGTH = 256; //连接ID列表 private static List<ConnectionId> mConnections = new List<ConnectionId>(); //用于音/视频聊天的对象 private static ICall mCall; //true: 每次本地视频设备创建新帧时,FrameUpdateEventArgs都会更新 private static bool mLocalFrameEvents = true; private static ConnectionId mRemoteUserId; private static MediaConfig mMediaConfig; private static MediaConfig mMediaConfigInUse; public static Action<string> OnTextHandler; public static Action<string> OnLogHandler; public static Action<FrameUpdateEventArgs> OnVideoFrameUpdate; //信令协议 public static string SignalingProtocol { get { string protocol = "ws"; if (Application.platform == RuntimePlatform.WebGLPlayer) { protocol = "wss"; } return protocol; } } #endregion #region 初始化 /// <summary> /// 初始WebRTC /// </summary> /// <param name="initBasicNetwork">初始化基本网络对象用于:收发消息</param> /// <param name="initCall">初始化Call对象用于:收发消息、音/视频通话</param> public static void Init(bool initBasicNetwork=true, bool initCall=true) { InitFactory(); if (initBasicNetwork) InitBasicNetwork(); if (initCall) InitCall(); } #endregion #region Factory //初始化 WebRtc Network Factory private static void InitFactory() { Debug.Log("Initializing webrtc factory"); UnityCallFactory.EnsureInit(OnCallFactoryReady, OnCallFactoryFailed); } private static void OnCallFactoryReady() { Debug.Log("WebRtcNetworkFactory created"); UnityCallFactory.Instance.RequestLogLevel(UnityCallFactory.LogLevel.Info); } private static void OnCallFactoryFailed(string error) { string fullErrorMsg = "The " + typeof(UnityCallFactory).Name + " failed to initialize with following error: " + error; Debug.LogError(fullErrorMsg); } #endregion #region 基本网络 //初始化 WebRTC BasicNetwork private static void InitBasicNetwork() { if (mBasicNetwork != null) return; Debug.Log("Initializing webrtc network"); string signalingUrl = uSignalingUrl; if (Application.platform == RuntimePlatform.WebGLPlayer || uForceSecureSignaling) { signalingUrl = uSecureSignalingUrl; } List<IceServer> iceServers = new List<IceServer>(); if (string.IsNullOrEmpty(uIceServer) == false) iceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword)); if (string.IsNullOrEmpty(uIceServer2) == false) iceServers.Add(new IceServer(uIceServer2)); if (string.IsNullOrEmpty(signalingUrl)) { throw new InvalidOperationException("set signaling url is null or empty"); } mBasicNetwork = UnityCallFactory.Instance.CreateBasicNetwork(signalingUrl, iceServers.ToArray()); if (mBasicNetwork != null) { Debug.Log("WebRTCNetwork created"); } else { Debug.Log("Failed to access webrtc "); } } /// <summary> /// 断开某个连接 /// </summary> /// <param name="id">连接ID</param> public static void Disconnect(ConnectionId id) { if (mBasicNetwork == null) return; mBasicNetwork.Disconnect(id); } /// <summary> /// 断开所有连接 /// </summary> public static void DisconnectAll() { if (mBasicNetwork == null) return; mBasicNetwork.Dispose(); } //检查网络事件,需在FixedUpdate()中调用此方法 public static void HandleNetwork() { if (mBasicNetwork == null) return; //从底层读取数据 mBasicNetwork.Update(); //处理自上次更新以来发生的所有新事件 NetworkEvent evt; //检查每个事件 while (mBasicNetwork != null && mBasicNetwork.Dequeue(out evt)) { Debug.LogFormat("Handler Network Event: {0}", evt); switch (evt.Type) { //服务器初始化完成 case NetEventType.ServerInitialized: mIsServer = true; string address = evt.Info; Debug.Log("Server started. Address: " + address); break; //启动服务器失败,可能是信令服务器关闭了 case NetEventType.ServerInitFailed: mIsServer = false; Debug.Log("Server start failed." + evt.Info); Dispose(); break; //服务器关闭 case NetEventType.ServerClosed: mIsServer = false; Debug.LogFormat("Server closed. {0}", evt.Info); break; //用户运行客户端并连接到服务器 //用户运行该服务器,并连接了一个新的客户端 case NetEventType.NewConnection: mConnections.Add(evt.ConnectionId); Debug.Log("New local connection! ID: " + evt.ConnectionId); //如果是服务器,请向所有人发送公告,并使用本地Id作为用户名 if (mIsServer) { //用户运行一个服务器, 向大家宣布新的连接. //使用服务器端连接ID作为标识 string msg = "New user " + evt.ConnectionId + " joined the room."; Debug.Log(msg); SendString(msg); } break; //服务器收到失败连接 //与服务器建立连接失败 case NetEventType.ConnectionFailed: if (mIsServer) { //收到一个失败连接 //原因: 收到了连接请求,但信令传送失败。 //1.防火墙阻止了直接连接,但允许信号启动连接过程(可能是本地防火墙,也可能是远端防火墙) //2.STUN/TURN 服务器未启动 //3.用户在连接完全建立之前就切断了连接 Debug.Log("An incoming connection failed."); } else { //与服务器建立连接失败 Debug.Log("Connection failed. " + evt.Info); Dispose(); } break; //连接中断 case NetEventType.Disconnected: mConnections.Remove(evt.ConnectionId); Debug.Log("Local Connection ID " + evt.ConnectionId + " disconnected"); if (mIsServer) { string userLeftMsg = "User " + evt.ConnectionId + " left the room."; Debug.Log(userLeftMsg); //通过其他客户端,有玩家离线 if (mConnections.Count > 0) { SendString(userLeftMsg); } } else { Dispose(); } break; //收到可靠消息 case NetEventType.ReliableMessageReceived: //收到不可靠消息 case NetEventType.UnreliableMessageReceived: HandleIncommingMessage(ref evt); break; } } //如果更新期间网络未中断,请刷新消息以完成此更新 if (mBasicNetwork != null) mBasicNetwork.Flush(); } //处理收到的消息 private static void HandleIncommingMessage(ref NetworkEvent evt) { MessageDataBuffer buffer = (MessageDataBuffer)evt.MessageData; string msg = Encoding.UTF8.GetString(buffer.Buffer, 0, buffer.ContentLength); OnTextHandler?.Invoke(msg); //如果服务器将消息转发给包括发件人在内的所有其他人 if (mIsServer) { //我们使用服务器端连接id来标识客户端 string idAndMessage = evt.ConnectionId + ":" + msg; Debug.Log(idAndMessage); //SendString(idAndMessage); } else { //客户端收到服务器发来的消息 Debug.Log(msg); } //返回缓冲区,以便网络可以重用它 buffer.Dispose(); } //发送消息 public static void SendString(string msg, bool reliable = true) { if (mBasicNetwork != null) { byte[] msgData = Encoding.UTF8.GetBytes(msg); foreach (ConnectionId id in mConnections) { mBasicNetwork.SendData(id, msgData, 0, msgData.Length, reliable); } } else if (mCall != null) { mCall.Send(msg, reliable); } } /// <summary> /// 创建房间 (仅支持文本聊天) /// </summary> /// <param name="roomName">房间名称(不能超过256个字符)</param> public static void OpenChatRoom(string roomName) { InitBasicNetwork(); roomName = EnsureLength(roomName); //如果该地址正在使用中,则底层系统将返回服务器连接失败 mBasicNetwork.StartServer(roomName); Debug.Log("Open Room " + roomName); } /// <summary> /// 关闭房间 /// 房间关闭后,将不再接受新用户的连接,但不影响现有已建立连接的用户 /// 当房间达到预设的最大人数后,可以调用此方法 /// </summary> public static void CloseChatRoom() { if (mBasicNetwork == null) return; mBasicNetwork.StopServer(); Debug.Log("Close Room"); } /// <summary> /// 加入房间,可以同时加入不同的房间 /// </summary> /// <param name="roomName">房间名称(不能超过256个字符)</param> public static void JoinChatRoom(string roomName) { InitBasicNetwork(); roomName = EnsureLength(roomName); mBasicNetwork.Connect(roomName); Debug.Log("Join Room " + roomName + " ..."); } //确保房间名称长度不超过256 private static string EnsureLength(string roomName) { if (roomName.Length > MAX_CODE_LENGTH) { roomName = roomName.Substring(0, MAX_CODE_LENGTH); } return roomName; } #endregion #region 视频聊天 //初始化Call, 音/视频聊天需要调用这个方法 private static void InitCall() { if (mCall != null) return; NetworkConfig config = CreateNetworkConfig(); mCall = CreateCall(config); if (mCall == null) { Debug.Log("Failed to create the call"); return; } mCall.LocalFrameEvents = mLocalFrameEvents; mCall.CallEvent += OnCallEvent; if (mMediaConfig == null) mMediaConfig = CreateMediaConfig(); mMediaConfigInUse = mMediaConfig.DeepClone(); //如果没有配置摄像头设备名称,则自动设置为前置摄像头 if (mMediaConfigInUse.Video && string.IsNullOrEmpty(mMediaConfigInUse.VideoDeviceName)) { string[] devices = UnityCallFactory.Instance.GetVideoDevices(); if (devices == null || devices.Length == 0) { Debug.Log("no device found or no device information available"); } else { foreach (string s in devices) Debug.Log("device found: " + s + " IsFrontFacing: " + UnityCallFactory.Instance.IsFrontFacing(s)); } mMediaConfigInUse.VideoDeviceName = UnityCallFactory.Instance.GetDefaultVideoDevice(); } Debug.Log("Configure call using MediaConfig: " + mMediaConfigInUse); //设置配置 mCall.Configure(mMediaConfigInUse); } //创建网络配置 private static NetworkConfig CreateNetworkConfig() { NetworkConfig netConfig = new NetworkConfig(); if (string.IsNullOrEmpty(uIceServer) == false) netConfig.IceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword)); if (string.IsNullOrEmpty(uIceServer2) == false) netConfig.IceServers.Add(new IceServer(uIceServer2)); if (Application.platform == RuntimePlatform.WebGLPlayer || uForceSecureSignaling) { netConfig.SignalingUrl = uSecureSignalingUrl; } else { netConfig.SignalingUrl = uSignalingUrl; } if (netConfig.SignalingUrl == "") { throw new InvalidOperationException("set signaling url is empty"); } return netConfig; } //创建Call对象 private static ICall CreateCall(NetworkConfig netConfig) { return UnityCallFactory.Instance.Create(netConfig); } //创建媒体配置 public static MediaConfig CreateMediaConfig() { MediaConfig mediaConfig = new MediaConfig(); //是否消除回音 (native only) bool useEchoCancellation = true; if (useEchoCancellation) { #if (!UNITY_WEBGL && !UNITY_WSA) var nativeConfig = new Byn.Awrtc.Native.NativeMediaConfig(); nativeConfig.AudioOptions.echo_cancellation = true; mediaConfig = nativeConfig; #endif } #if UNITY_WSA && !UNITY_EDITOR var uwpConfig = new Byn.Awrtc.Uwp.UwpMediaConfig(); uwpConfig.Mrc = true; //uwpConfig.ProcessLocalFrames = false; //uwpConfig.DefaultCodec = "H264"; mediaConfig = uwpConfig; Debug.Log("Using uwp specific media config: " + mediaConfig); #endif //开启音频 mediaConfig.Audio = true; //开启视频 mediaConfig.Video = true; mediaConfig.VideoDeviceName = null; //图像格式 mediaConfig.Format = FramePixelFormat.ABGR; //最小分辨率 mediaConfig.MinWidth = 160; mediaConfig.MinHeight = 120; //分辨率越大编码/解码越慢 mediaConfig.MaxWidth = 1920 * 2; mediaConfig.MaxHeight = 1080 * 2; //WebRTC会优先考虑IdealWidth、IdealHeight、IdealFrameRate //理想分辨率、帧率 mediaConfig.IdealWidth = 160; mediaConfig.IdealHeight = 120; mediaConfig.IdealFrameRate = 30; return mediaConfig; } //创建视频聊天房间 public static void OpenCallRoom(string roomName) { InitCall(); mCall.Listen(roomName); } //加入视频聊天房间 public static void JoinCallRoom(string roomName) { InitCall(); //不要在会议模式下使用Call接口 mCall.Call(roomName); } //是否设置成了静音 public static bool IsMute() { if (mCall == null) return true; return mCall.IsMute(); } //设置静音 public static void SetMute(bool val) { if (mCall == null) return; mCall.SetMute(val); } //检查手机扬声器是否已打开。仅适用于移动本地平台 public static bool GetLoudspeakerStatus() { if (mCall != null) { return UnityCallFactory.Instance.GetLoudspeakerStatus(); } return false; } //打开/关闭手机扬声器。仅适用于移动本地平台 public static void SetLoudspeakerStatus(bool state) { if (mCall != null) { UnityCallFactory.Instance.SetLoudspeakerStatus(state); } } //设置是否显示本地视频 //如果不需要显示本地摄像头画面,可以设置成false public static void SetShowLocalVideo(bool showLocalVideo) { mLocalFrameEvents = showLocalVideo; } //视频聊天驱动方法,需要每帧调用 public static void CallUpdate() { if (mCall == null) return; //将底层事件转发到Unity线程中 mCall.Update(); } //处理Call事件 private static void OnCallEvent(object sender, CallEventArgs e) { Debug.Log(e.Type); switch (e.Type) { //呼出电话成功或来电到达 case CallEventType.CallAccepted: { //HasAudioTrack() 判断对方麦克风是否可用 //HasVideoTrack() 判断对方摄像头是否可用 mRemoteUserId = ((CallAcceptedEventArgs)e).ConnectionId; string msg = "New connection with id: " + mRemoteUserId + " audio:" + mCall.HasAudioTrack(mRemoteUserId) + " video:" + mCall.HasVideoTrack(mRemoteUserId); Debug.Log(msg); OnLogHandler?.Invoke(msg); mConnections.Add(mRemoteUserId); } break; //通话结束或其中一个用户挂断电话 case CallEventType.CallEnded: CleanupCall(); break; //来电失败 case CallEventType.ListeningFailed: { ErrorEventArgs args = e as ErrorEventArgs; string msg = "ListeningFailed: " + args.Info; Debug.Log(msg); OnLogHandler?.Invoke(msg); } break; //连接房间失败 case CallEventType.ConnectionFailed: { ErrorEventArgs args = e as ErrorEventArgs; string msg = "ConnectionFailed: " + args.Info; Debug.Log(msg); OnLogHandler?.Invoke(msg); CleanupCall(); } break; //当前系统不支持摄像头或麦克风,或不支持请求的分辨率,或权限不足 case CallEventType.ConfigurationFailed: { ErrorEventArgs args = e as ErrorEventArgs; string msg = "ConfigurationFailed: " + args.Info; Debug.Log(msg); OnLogHandler?.Invoke(msg); CleanupCall(); } break; //收到一个新的视频帧(来自于本地摄像头或网络) case CallEventType.FrameUpdate: if (e is FrameUpdateEventArgs) { var evt = e as FrameUpdateEventArgs; OnVideoFrameUpdate?.Invoke(evt); } break; //收到文本消息 case CallEventType.Message: { MessageEventArgs args = e as MessageEventArgs; Debug.Log(args.Content); OnTextHandler?.Invoke(args.Content); } break; //接到来电 case CallEventType.WaitForIncomingCall: { //聊天应用程序将等待另一个应用程序通过相同的字符串连接 WaitForIncomingCallEventArgs args = e as WaitForIncomingCallEventArgs; string msg = "WaitForIncomingCall: " + args.Address; Debug.Log(msg); OnLogHandler?.Invoke(msg); } break; } } //清理Call public static void CleanupCall() { if (mCall == null) return; mCall.CallEvent -= OnCallEvent; mCall.Dispose(); mCall = null; mRemoteUserId = ConnectionId.INVALID; mConnections.Clear(); GC.Collect(); GC.WaitForPendingFinalizers(); Debug.Log("Call destroyed"); } //请求权限 public static IEnumerator RequestPermissions(bool audio = true, bool video = true) { if (audio) { yield return RequestAudioPermission(); } if (video) { yield return RequestVideoPermission(); } yield return null; } //请求麦克风权限 public static IEnumerator RequestAudioPermission() { #if UNITY_ANDROID && UNITY_2018_3_OR_NEWER if (!HasAudioPermission()) { Debug.Log("Requesting microphone permissions"); UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Microphone); //同一帧中连续调用两次请求会导致请求被忽略 //等待用户按下 "允许" 或 "拒绝" yield return new WaitForSeconds(0.1f); //极少数情况下即使用户选择"允许",也可能返回false Debug.Log("microphone permission: " + HasAudioPermission()); } #endif yield return null; } //请求摄像头权限 public static IEnumerator RequestVideoPermission() { #if UNITY_ANDROID && UNITY_2018_3_OR_NEWER if (!HasVideoPermission()) { Debug.Log("Requesting camera permissions"); UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera); yield return new WaitForSeconds(0.1f); Debug.Log("camera permission: " + HasVideoPermission()); } #endif yield return null; } //是否有使用麦克风的权限 public static bool HasAudioPermission() { #if UNITY_ANDROID && UNITY_2018_3_OR_NEWER if (Application.platform == RuntimePlatform.Android) { return UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Microphone); } #endif return true; } //是否有使用摄像头的权限 public static bool HasVideoPermission() { #if UNITY_ANDROID && UNITY_2018_3_OR_NEWER if (Application.platform == RuntimePlatform.Android) { return UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera); } #endif return true; } #endregion #region 释放资源 //释放 public static void Dispose() { if (mBasicNetwork != null) { mBasicNetwork.Dispose(); mBasicNetwork = null; } mConnections.Clear(); mIsServer = false; CleanupCall(); } #endregion }
视频通话测试
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Byn.Awrtc; using Byn.Awrtc.Unity; /// <summary> /// 视频通话测试 /// </summary> public class CallTest : MonoBehaviour { [SerializeField] private InputField roomNameInput; [SerializeField] private InputField messageInput; [SerializeField] private Text msgText; [SerializeField] private Text logText; [SerializeField] private RawImage localVideoImage; private Texture2D localTexture; [SerializeField] private RawImage remoteVideoImage; private Texture2D remoteTexture; void Start() { //请求权限 StartCoroutine(WebRTCHelper.RequestPermissions()); //初始化 WebRTCHelper.Init(false, true); WebRTCHelper.OnTextHandler = OnTextHandler; WebRTCHelper.OnLogHandler = OnLogHandler; WebRTCHelper.OnVideoFrameUpdate = OnVideoFrameUpdate; } private void OnDestroy() { WebRTCHelper.Dispose(); } void Update() { WebRTCHelper.CallUpdate(); } private void FixedUpdate() { WebRTCHelper.HandleNetwork(); } public void OnClickOpenChatRoom() { WebRTCHelper.OpenChatRoom(roomNameInput.text); } public void OnClickJoinChatRoom() { WebRTCHelper.JoinChatRoom(roomNameInput.text); } public void OnClickOpenCallRoom() { WebRTCHelper.OpenCallRoom(roomNameInput.text); } public void OnClickJoinCallRoom() { WebRTCHelper.JoinCallRoom(roomNameInput.text); } public void OnClickSend() { WebRTCHelper.SendString(messageInput.text); } public void OnClickHungup() { WebRTCHelper.CleanupCall(); } //处理收到的文本消息 private void OnTextHandler(string msg) { msgText.text = msg; } //处理收到的错误消息 private void OnLogHandler(string error) { logText.text = error; } //处理收到的视频帧 private void OnVideoFrameUpdate(FrameUpdateEventArgs e) { if (e.IsRemote) { UpdateRemoteTexture(e.Frame); } else { UpdateLocalTexture(e.Frame); } } //更新本地视频画面 private void UpdateLocalTexture(IFrame frame) { if (frame == null) return; //UnityMediaHelper.UpdateRawImage(localVideoImage, frame); if (localTexture == null) localTexture = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); UnityMediaHelper.UpdateTexture(frame, ref localTexture); if (localVideoImage.texture == null) localVideoImage.texture = localTexture; } //更新远程视频画面 private void UpdateRemoteTexture(IFrame frame) { if (frame == null) return; //UnityMediaHelper.UpdateRawImage(remoteVideoImage, frame); if (remoteTexture == null) remoteTexture = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); UnityMediaHelper.UpdateTexture(frame, ref remoteTexture); if (remoteVideoImage.texture == null) remoteVideoImage.texture = remoteTexture; } }
标签: Unity3d
日历
最新文章
随机文章
热门文章
分类
存档
- 2024年11月(3)
- 2024年10月(5)
- 2024年9月(3)
- 2024年8月(3)
- 2024年7月(11)
- 2024年6月(3)
- 2024年5月(9)
- 2024年4月(10)
- 2024年3月(11)
- 2024年2月(24)
- 2024年1月(12)
- 2023年12月(3)
- 2023年11月(9)
- 2023年10月(7)
- 2023年9月(2)
- 2023年8月(7)
- 2023年7月(9)
- 2023年6月(6)
- 2023年5月(7)
- 2023年4月(11)
- 2023年3月(6)
- 2023年2月(11)
- 2023年1月(8)
- 2022年12月(2)
- 2022年11月(4)
- 2022年10月(10)
- 2022年9月(2)
- 2022年8月(13)
- 2022年7月(7)
- 2022年6月(11)
- 2022年5月(18)
- 2022年4月(29)
- 2022年3月(5)
- 2022年2月(6)
- 2022年1月(8)
- 2021年12月(5)
- 2021年11月(3)
- 2021年10月(4)
- 2021年9月(9)
- 2021年8月(14)
- 2021年7月(8)
- 2021年6月(5)
- 2021年5月(2)
- 2021年4月(3)
- 2021年3月(7)
- 2021年2月(2)
- 2021年1月(8)
- 2020年12月(7)
- 2020年11月(2)
- 2020年10月(6)
- 2020年9月(9)
- 2020年8月(10)
- 2020年7月(9)
- 2020年6月(18)
- 2020年5月(4)
- 2020年4月(25)
- 2020年3月(38)
- 2020年1月(21)
- 2019年12月(13)
- 2019年11月(29)
- 2019年10月(44)
- 2019年9月(17)
- 2019年8月(18)
- 2019年7月(25)
- 2019年6月(25)
- 2019年5月(17)
- 2019年4月(10)
- 2019年3月(36)
- 2019年2月(35)
- 2019年1月(28)
- 2018年12月(30)
- 2018年11月(22)
- 2018年10月(4)
- 2018年9月(7)
- 2018年8月(13)
- 2018年7月(13)
- 2018年6月(6)
- 2018年5月(5)
- 2018年4月(13)
- 2018年3月(5)
- 2018年2月(3)
- 2018年1月(8)
- 2017年12月(35)
- 2017年11月(17)
- 2017年10月(16)
- 2017年9月(17)
- 2017年8月(20)
- 2017年7月(34)
- 2017年6月(17)
- 2017年5月(15)
- 2017年4月(32)
- 2017年3月(8)
- 2017年2月(2)
- 2017年1月(5)
- 2016年12月(14)
- 2016年11月(26)
- 2016年10月(12)
- 2016年9月(25)
- 2016年8月(32)
- 2016年7月(14)
- 2016年6月(21)
- 2016年5月(17)
- 2016年4月(13)
- 2016年3月(8)
- 2016年2月(8)
- 2016年1月(18)
- 2015年12月(13)
- 2015年11月(15)
- 2015年10月(12)
- 2015年9月(18)
- 2015年8月(21)
- 2015年7月(35)
- 2015年6月(13)
- 2015年5月(9)
- 2015年4月(4)
- 2015年3月(5)
- 2015年2月(4)
- 2015年1月(13)
- 2014年12月(7)
- 2014年11月(5)
- 2014年10月(4)
- 2014年9月(8)
- 2014年8月(16)
- 2014年7月(26)
- 2014年6月(22)
- 2014年5月(28)
- 2014年4月(15)
友情链接
- Unity官网
- Unity圣典
- Unity在线手册
- Unity中文手册(圣典)
- Unity官方中文论坛
- Unity游戏蛮牛用户文档
- Unity下载存档
- Unity引擎源码下载
- Unity服务
- Unity Ads
- wiki.unity3d
- Visual Studio Code官网
- SenseAR开发文档
- MSDN
- C# 参考
- C# 编程指南
- .NET Framework类库
- .NET 文档
- .NET 开发
- WPF官方文档
- uLua
- xLua
- SharpZipLib
- Protobuf-net
- Protobuf.js
- OpenSSL
- OPEN CASCADE
- JSON
- MessagePack
- C在线工具
- 游戏蛮牛
- GreenVPN
- 聚合数据
- 热云
- 融云
- 腾讯云
- 腾讯开放平台
- 腾讯游戏服务
- 腾讯游戏开发者平台
- 腾讯课堂
- 微信开放平台
- 腾讯实时音视频
- 腾讯即时通信IM
- 微信公众平台技术文档
- 白鹭引擎官网
- 白鹭引擎开放平台
- 白鹭引擎开发文档
- FairyGUI编辑器
- PureMVC-TypeScript
- 讯飞开放平台
- 亲加通讯云
- Cygwin
- Mono开发者联盟
- Scut游戏服务器引擎
- KBEngine游戏服务器引擎
- Photon游戏服务器引擎
- 码云
- SharpSvn
- 腾讯bugly
- 4399原创平台
- 开源中国
- Firebase
- Firebase-Admob-Unity
- google-services-unity
- Firebase SDK for Unity
- Google-Firebase-SDK
- AppsFlyer SDK
- android-repository
- CQASO
- Facebook开发者平台
- gradle下载
- GradleBuildTool下载
- Android Developers
- Google中国开发者
- AndroidDevTools
- Android社区
- Android开发工具
- Google Play Games Services
- Google商店
- Google APIs for Android
- 金钱豹VPN
- TouchSense SDK
- MakeHuman
- Online RSA Key Converter
- Windows UWP应用
- Visual Studio For Unity
- Open CASCADE Technology
- 慕课网
- 阿里云服务器ECS
- 在线免费文字转语音系统
- AI Studio
- 网云穿
- 百度网盘开放平台
- 迅捷画图
- 菜鸟工具
- [CSDN] 程序员研修院
- 华为人脸识别
- 百度AR导航导览SDK
- 海康威视官网
- 海康开放平台
- 海康SDK下载
- git download
交流QQ群
-
Flash游戏设计: 86184192
Unity游戏设计: 171855449
游戏设计订阅号