月度存档: 1月 2015

unity调整分辨率在android下闪屏

unity版本 4.3.1

为了减少发热量,我们游戏对分辨率进行的缩减,但在缩小的那一祯可以明显看到闪烁。

参考了一些文章,了解了下闪烁的原因:

1、在unity启动时默认是使用上层layout的width/height,游戏基本上都是全屏,所以上来就是全屏显示。

2、在unity中使用Screen.SetResolution,可以调整分辨率,反编译unity android层代码可以看到是调用了surface.setFixedSize,就是这里的原因,在调整期间会闪屏。

既然默认使用的是上层layout的width/height,我们直接修改上层的layout不就OK了?噢,不行,layout是指实际显示的分辨大小,我们的目标是指渲染的界面要小,然后通过缩放进行全屏显示。

所以只能使用surface.setFixedSize方法,那这样只需要在调整分辨前在main acitivity.onCreate方法中设置上就OK了。

查看了unityplayer.surcefaceview,发现是私有的。私有的也难不倒我们,直接使用反射取出私有变量。

需要同时在onCreate和onConfigurationChanged中修改。

修改完后发现触点和unity中的对应不上了,检查一下问题:

1、发现unity中触点是设置的缩小分辨率后对应的,但如果将竖屏和横屏切换一下,即可正确对应上,应该是在oncreate调整分辨率后还需要调整触点。

上述错误。经过很辛苦的查找,终于查找到原来是unity中提供的classes.jar有bug。反编译后,发现:

	protected void triggerResizeCall() {
		Log.i("hack unity", "triggerResizeCall v:" + v + " w:" + w + " v:" + v
				+ " w:" + w);
		this.nativeResize(this.v, this.w, this.v, this.w);
	}

这里第三个参数和第四个参数应该使用屏幕的大小,而不应该是使用分辨率的大小。坑啊。

找到原因就好处理了,hack掉unityplayer,重新提供一个triggerResizeCall,完美解决闪屏问题。

实际上调整分辨率闪屏还有很多更便捷的解决方法,比如unity的splash image设置为全黑,然后在第一个场景加载时,调用分辨率玩家就看不到闪屏了,反正都是黑的,在第一个场景调整完分辨后,再伪造显示一个splash image,哈哈。

附github:

unity classes.jar 反编译后的源码:https://github.com/zhukunqian/unity_java_4_3_1/tree/master

demo 代码:https://github.com/zhukunqian/unity_scale_resolution

参考文章:

1) http://stackoverflow.com/questions/7185644/android-opengl-crazy-aspect-ratio-after-sleep

2)http://android-developers.blogspot.it/2013/09/using-hardware-scaler-for-performance.html

一篇优化android 性能的文章

http://android-developers.blogspot.it/2013/09/using-hardware-scaler-for-performance.html

adb devices中看不到设备

1、adb devices中看不到设备。

以前也遇到过这种问题,解决方案是重装安装android use driver,还需要修改里面的android_winusb.inf文件,很是麻烦。

这里找到一个简单的办法:

修改adb_usb.ini

在任务管理器中结束adb命令,再对下面的修改可能产生影响

在模拟器存放的目录<例如:C:\Documents and Settings\Administrator\.android>下找到或新建一个adb_usb.ini文件,同时增加或写入上面设备的0xVID(VID就是上面的VID_后面跟的数字,例如:0x1782),弄好了再使用重新启动android sdk中的adb就能连接上了

2、adb devices中看到设备是offline状态

-.-!!升级一下adb版本即可。

3、小米手机获取root权限。

安装miui开发版本,adb root进入后使用su获得root权限。

4、小米手机安装驱动

https://www.mi.com/c/service/download/ 下载小米自己的驱动软件

5、三星手机一个解决办法

使用QQ的电脑管家安装手机助手,很奇怪的解决办法。电脑管家识别出手机后,debug看log可以正常用,如果想使用snapdragon抓图,可能还得使用无线连上去才可以。

6、网易MUMU MAC模拟器下出现了OFFILNE,重启了一下ADB_SERVER,解决了。

7、网易MUMU MAC模拟有时需要重启adb_server才生效。

unity 在游戏启动时按home键,再切回游戏黑屏

我们的游戏在启动时,如果切回后台,会几率性出现黑屏及资源加载错误。

我抓了一下android层的异常:

01-20 22:21:48.362: W/IInputConnectionWrapper(6525): showStatusIcon on inactive InputConnection
01-20 22:21:48.440: E/BufferQueue(190): [SurfaceView] connect: BufferQueue has been abandoned!
01-20 22:21:48.440: E/libEGL(6525): EGLNativeWindowType 0x76b53920 already connected to another API
01-20 22:21:48.440: E/libEGL(6525): eglCreateWindowSurface:525 error 3003 (EGL_BAD_ALLOC)
01-20 22:21:48.440: E/Unity(6525): [EGL] Failed to create surface
01-20 22:21:48.440: E/Unity(6525):  
01-20 22:21:48.440: E/Unity(6525): (Filename:  Line: 132)
01-20 22:21:48.440: E/Unity(6525): [EGL] Error:: EGL_BAD_ALLOC: EGL failed to allocate resources for the requested operation.
01-20 22:21:48.440: E/Unity(6525):  
01-20 22:21:48.440: E/Unity(6525): (Filename: ./PlatformDependent/AndroidPlayer/ContextGLES.cpp Line: 132)
01-20 22:21:48.441: E/libEGL(6525): call to OpenGL ES API with no current context (logged once per thread)

在网上搜索了很多文章,没有合适的解决方案。

在测试我们公司另一个项目的游戏时,发现他们没有这个问题,比较我们两个游戏,发现在启动速度上差别很大。

感觉这个问题的出现和启动慢有关系。查看启动部分代码,发现启动的类的field都是直接在类加载时进行的初始化。

感觉这里会阻塞线程,导致的unity初始时间非常长。

写了一个测试用例,分别在Awake(),Start()及类初始化时添加Thread.sleep(15000)的强制延迟。

发现Awake,Start不会阻塞unity的加载,会立即加载出splash image。在类初始化时,消耗大量时间,打出apk包,在手机上运行,也没有阻塞splash image显示。这下可真是奇怪了。

我先把我们游戏中在类加载时初始化的内容全转移至Awake中,再打个包测试下。

经过我们这边和珠海测试团队测试,进游戏时丢贴图的问题没有再次复现。是个好消息。

只剩下最后一个黑屏的问题,这个黑屏并不是BUG,只是客户端代码流程没有处理好。

——————————————————————————————

2015.01.22 更新:

本来以为黑屏是游戏中故意设置的黑屏,把游戏中的黑屏改为绿屏,结果发现黑屏问题依旧。

试着抓了一下异常:

01-22 13:36:59.538: E/XSJData(5438): serverId is null
01-22 13:36:59.675: E/XSJData(5438): serverId is null
01-22 13:38:07.740: E/linker(9195): load_library(linker.cpp:760): library "libmaliinstr.so" not found
01-22 13:38:07.744: E/(9195): appName=com.xsj.moon, acAppName=com.android.cts.openglperf
01-22 13:38:07.744: E/(9195): 0
01-22 13:38:07.744: E/(9195): appName=com.xsj.moon, acAppName=com.android.browser
01-22 13:38:07.744: E/(9195): 0
01-22 13:38:13.985: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/mscorlib.dll.so" not found
01-22 13:38:14.501: E/(9195): appName=com.xsj.moon, acAppName=com.android.cts.openglperf
01-22 13:38:14.501: E/(9195): 0
01-22 13:38:14.501: E/(9195): appName=com.xsj.moon, acAppName=com.android.browser
01-22 13:38:14.501: E/(9195): 0
01-22 13:38:14.714: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/UnityEngine.dll.so" not found
01-22 13:38:14.800: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/Assembly-CSharp-firstpass.dll.so" not found
01-22 13:38:14.902: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/Assembly-CSharp.dll.so" not found
01-22 13:38:14.906: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/Assembly-UnityScript.dll.so" not found
01-22 13:38:14.909: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/HOTween.dll.so" not found
01-22 13:38:14.913: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/JsonFx.Json.dll.so" not found
01-22 13:38:14.916: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/P31RestKit.dll.so" not found
01-22 13:38:14.918: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/Vectrosity.dll.so" not found
01-22 13:38:14.919: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/UMGameAnalyticsLibForAndroid.dll.so" not found
01-22 13:38:14.920: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/UMGameAnalyticsLibForiOS.dll.so" not found
01-22 13:38:17.802: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/System.Core.dll.so" not found
01-22 13:38:17.872: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/System.dll.so" not found
01-22 13:38:18.303: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/System.Xml.dll.so" not found
01-22 13:38:19.756: E/lsy(9195): UnityAwake
01-22 13:38:20.957: E/linker(9195): load_library(linker.cpp:760): library "/data/app/com.xsj.moon-1.apk/assets/bin/Data/Managed/System.dll.so" not found
01-22 13:38:24.040: E/linker(9195): load_library(linker.cpp:760): library "fmod" not found
01-22 13:38:24.048: E/linker(9195): load_library(linker.cpp:760): library "fmodstudio" not found
01-22 13:38:24.062: E/linker(9195): load_library(linker.cpp:760): library "fmodstudio" not found
01-22 13:38:26.182: E/linker(9195): load_library(linker.cpp:760): library "fmodstudio" not found
01-22 13:38:26.252: E/linker(9195): load_library(linker.cpp:760): library "fmodstudio" not found
01-22 13:38:26.255: E/linker(9195): load_library(linker.cpp:760): library "fmod" not found
01-22 13:38:26.738: E/XSJData(9195): serverId is null
01-22 13:38:26.868: E/XSJData(9195): serverId is null
01-22 13:38:28.460: E/WhetstoneSDK-JNI(9195): on Load native Whetstone

看起来还是程序中有BUG,并不是简单的逻辑异常。

unity中包中只存在对应的dll不存在对应的dll.so,这里应该是unity内部的加载机制被破坏掉了。

在goolge上搜索下unity加载dll的流程。

抓了一下正常流程的日志,在正常流程中也有上面的输出。看起来不是上面的原因。

—————————————————————————————————————-

2015.01.29

这几天为了这个花屏,黑屏的问题做了不了调查,现在基本上确认了点信息:

重现流程:

1、启动游戏

2、在splash image加载完成前,点home键返回主界面

3、点游戏图标进入游戏,出现黑屏。如果继续,则有可能出现花屏。

已经确认的信息:

1、我们的游戏中很多资源都放在的resources目录下,这会导致游戏加载很比较慢,这里可能会出现egl初始化未完成时,点home键返回界面,然后再点回来时,又创建了一次egl windows,而android中不允许同时存在两个egl windows,所以就会出现黑屏。

一条可能有用的信息:

01-29 15:23:12.572: W/MALI(3730): __egl_mali_post_to_window_surface:936: __egl_mali_post_to_window_surface: posting to window surface failed; couldn’t acquire output buffer (surface=0x7cb3b008)

还有另一条,从这里开始不停报错:

01-29 15:23:12.573: D/Unity(3730): [EGL] SwapBuffer: EGL_BAD_ALLOC: EGL failed to allocate resources for the requested operation.
01-29 15:23:12.933: E/Unity(3730): [EGL] Failed to create surface

参考:

http://stackoverflow.com/questions/15561780/createwindowsurface-failed-egl-bad-match

发现一件很奇怪的事,在小米手机上,如果安装完后直接启动游戏,则在游戏中按home键返回,再点游戏图标进入游戏,每次都会重新调用onCreate。

如果一开始使用游戏icon进入游戏,则不会有这个问题。

可能有用的一条log:

01-29 23:01:38.793: W/ManagedEGLContext(18120): doTerminate failed: EGL count is 2 but managed count is 1

黑屏的原因找到了,是因为4.3.1f1中unity每次pause后,eglcontext需要重建,但我们游戏中重建需要的时间太长,而且在重建完成前就是黑屏,而且这里还有另一个问题,如果在黑屏期再按pause返回,则会导致游戏中一个10秒的黑屏等待,如果你短时间内不停pause,则每一次puase导致的10秒等待会累加起来。

另参考:http://stackoverflow.com/questions/2112768/prevent-onpause-from-trashing-opengl-context

http://pastebin.com/U4x5JjAr

http://gamedev.stackexchange.com/questions/12629/workaround-to-losing-the-opengl-context-when-android-pauses

http://www.anddev.org/android-2d-3d-graphics-opengl-problems-f55/minimizing-opengl-texture-reloads-on-pause-resume-t52932.html

 

https://code.google.com/p/replicaisland/source/browse/trunk/src/com/replica/replicaisland/GLSurfaceView.java

——————————————————————————————————–

2015.02.03 不知道时间过去多长了,黑屏和花屏问题终于定位到了,项目代码中真是各种天坑。

先列下解决方案吧:

&lt;activity
            android:name="com.haifi.abc.MainActivity"
            android:configChanges="locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
            android:label="@string/app_name"
            android:windowSoftInputMode="stateHidden"
            android:screenOrientation="sensorLandscape"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"&gt;
            &lt;intent-filter&gt;
                &lt;action android:name="android.intent.action.MAIN" /&gt;
                &lt;category android:name="android.intent.category.LAUNCHER" /&gt;
            &lt;/intent-filter&gt;
        &lt;/activity&gt;
        &lt;activity android:label="@string/app_name" android:name="UnityPlayerActivityEx" android:screenOrientation="landscape" android:configChanges="locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"&gt;
            <strong><span style="color: #ff0000;">&lt;meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false" /&gt;</span></strong>
        &lt;/activity&gt;

1、使用unity中提供的默认UnityPlayerActivity,是从UnityPlayerNativeActivity继承而来,

http://docs.unity3d.com/Manual/PluginsForAndroid.html

这里有一段对UnityPlayerNativeActivity的描述,说明了unityplayer.ForwardnativeEventsToDalvik的作用,

我们项目中不知道是谁竟然删除了这些代码,unity默认生成的android工程中带着此代码的。

2、自己写一个类继承自UnityPlayerActivity,然后补充以下代码:

	protected void onCreate(Bundle paramBundle) {
		super.onCreate(paramBundle);
		if (Build.VERSION.SDK_INT >= 19) {
			getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(
					new View.OnSystemUiVisibilityChangeListener() {
						public void onSystemUiVisibilityChange(
								int paramAnonymousInt) {
							View localView = UnityPlayerActivityEx.this
									.getWindow().getDecorView();
							if ((paramAnonymousInt & 0x4) == 0) {
								localView.setSystemUiVisibility(5894);
							}
						}
					});
		}
	}

	public void onWindowFocusChanged(boolean paramBoolean) {
		super.onWindowFocusChanged(paramBoolean);
		if ((Build.VERSION.SDK_INT >= 14) && (paramBoolean)) {
			getWindow().setFlags(1024, 1024);
			getWindow().getDecorView().setSystemUiVisibility(6);
		}
		if ((Build.VERSION.SDK_INT >= 19) && (paramBoolean)) {
			getWindow().getDecorView().setSystemUiVisibility(5894);
		}
	}

OK,什么花屏和黑屏全都消失不见了。真是天坑。

另外这里还有进一步优化的余地:可以进一步阻止surfaceview的创建,这样按home键再返回,不会重建egl context。

 

——————————————————————————————————————–

2015.02.13记。这周又在处理这个问题,在MainActivity中添加代码后发现在第一次安装后直接在安装界面启动,按home键返回,再点游戏图标,就会直接黑屏。

这周怒了,把unity源码各个函数都添加日志,在android那一层把所有可能调用的函数都添加日志,意外发现MainActivity的onCreate被调用了两次???,不可理解,从理论上讲,MainActivity的onCreate永远只会被调用一次。

又添加测试代码,在MainActivity中设置一个private static int m=0;在onCreate中设置m++,然后输出m的值,发现果然,从启动界面启动游戏时,每次从home键返回都会调用onCreate,而且m的值一直在增加,说明这个进程并没有被结束。只是MainActivity被重复的创建。

我一开始想到的方法是既然MainActivity中多次创建,那根据m的值,只在第一次创建时,生成unityPlayer,其它的时间不再重新创建unityPlayer,这样可以避免unityPlayer被多次创建,就也自然避免的黑屏的问题。

但是这个方法一开始很好,但在测试中发现了问题,我们开始游戏时,会播放一个mp4视频,如果这时候按home返回,再返回游戏,又是黑屏,该死,这是因为我们的开发人员把播放视频放在了MainActivity中,而返回时,又因为onCreate的原因,又丢失掉了播放视频的view。

总之,走了NNNNNNN多的弯路。

到这里我又重新审视了自己的解决方法和问题根源,这里根源就是因为onCreate被调用了多次导致的,如果能解决这个问题,其它所有问题都是自然而解。

幸好老外早就遇到问题了,只是可惜我一开始没有走到正确的方向,导致花费了太多的时间。

好吧下面是一系列的奇遇:

1、我在google搜索: “android acitivity recreate from home button”

2、它建议我搜索: “android activity oncreate from home button”,好吧,同意它。

3、看到一个搜索结果:http://stackoverflow.com/questions/16981793/android-home-key-issue-at-first-time-calling-oncreate

4、果然是我需要的问题,里面建议去:http://code.google.com/p/android/issues/detail?id=2373

5、https://code.google.com/p/android/issues/detail?id=2373 好吧,果然是宝地,问题根源及解决方案(三种)都在里面。

这里我采用了,这个大师的解决方案。(当然,另外的解决方案也可以尝试)

<a href="https://github.com/cleverua/android_startup_activity" rel="nofollow">https://github.com/cleverua/android_startup_activity</a>

总结:也是自己太不了解android开发了,不然这个bug一看到就可以解决掉了。自己虽然花费很多精力,但中间解决问题的学习也让自己增长了不长见识。

游戏中重构了网络模块

我们游戏中断线和超时这一块出问题非常多。

这一次我专门抽出了一周时间,了解了下c#下的socket相关函数重写了网络模块。

首要目标是避免另开线程,使用nio。然后是内部处理超时,断线和其它IO异常,重连3次失败后,才提交给游戏自己处理。重连时,自动登陆并续发消息。

这里本来还需要添加一个补发消息的机制(断线后网络传输中导致丢失的消息),但是测试只有续发消息,已经解决掉了原来99%问题。所以补发消息先放一下,先再重构其它地方。

重构后测试的结果非常好,以前各种网络问题都没有复现,断线对玩家来说是不可见的,只要长时间断线(三次重连失败)才会给玩家一个提示框,让玩家手动点击去重连。

//----------------------------------------------
//           NET        : NetManager
//           AUTHOR: zhu kun qian
//----------------------------------------------

using System.IO;

public class NetMsg
{
    // 有必要用id吗?
    // 
    public cmd.CmdType cmdType;
    public uint loop;// 唯一的循环id
    public byte[] data;// protobuf二进制数据
    public byte[] totalData;//完整的消息二进制数据
    public MemoryStream netdata;

    // 这里能避免一次array copy不?
    // TODO:暂不考虑这避免arry copy的消耗,以后有时间时,可以考虑处理下。
}

 

//----------------------------------------------
//           NET        : NetManager
//           AUTHOR: zhu kun qian
//----------------------------------------------

using System.Net;
using System.Net.Sockets;
using System.Runtime.Remoting.Messaging;
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

/*
 * 没有使用任何阻塞函数。
 * 
 * connect是block函数,所以改为使用ConnectAsync, beginConnect也是异步,也可以使用
 * 
 * 连接成功后,socket修改为non-blocking
*/
public class NetManagerSocket
{

    // 如果断开连接,直接new socket

    public string ip;
    public int port;

    private Socket socket;
    private byte[] readBuf = new byte[64 * 1024];// 最高缓存接收64k数据。

    private int readBufIndex = 0;//已经从readBuf中读取的字节数
    private int available = 0;// readbuf已经从socket中接收的字节数

    public SocketError socketError = SocketError.Success;// 这里应该是传递到上层,还是在这里处理?

    // ---------------------------------------
    // -- decoder相关
    private int length = 0;// protobuf消息体中的长度
    private int needRead = 0;// protobuf消息体中剩余需要读取的长度
    // ---------------------------------------

    public void connect()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 5000);// 设置5秒发送超时
        socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false);
        socket.NoDelay = true;
        // socket.Blocking = false;
        socket.Blocking = true;//设置为非阻塞

        socketError = SocketError.Success;

        Debug.Log("dontLinger:" + socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger));
        try
        {
            // 这里需要修改为异步connect
            // http://msdn.microsoft.com/en-us/library/d7ew360f%28v=vs.110%29.aspx
            // Connect method will block, unless you specifically set the Blocking property to false prior to calling Connect.
            // If you are using a connection-oriented protocol like TCP and you do disable blocking, Connect will throw a SocketException because it needs time to make the connection
            // 结论:很奇怪,并没有按文档上描述的执行,设置为blocking,并没有抛出异常。

            // socket.Connect(ip, port);

            // The asynchronous BeginConnect operation must be completed by calling the EndConnect method.
            // IAsyncResult result= socket.BeginConnect(ip, port, new AsyncCallback(connectCallback), socket);

            // Debug.Log("asyncResult is completed:"+result.IsCompleted);

            // 异步处理必须设置为blocking,why?
            //*
            SocketAsyncEventArgs eventArgs = new SocketAsyncEventArgs();
            eventArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            eventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(connectCallback2);
            eventArgs.UserToken = socket;

            socket.ConnectAsync(eventArgs);

            // */
            // 这里需要添加超时处理吗?
        }
        catch (Exception ex)
        {
            Debug.LogError("socket connection exception:" + ex);
            socketError = SocketError.ConnectionRefused;
        }
    }

    private void connectCallback2(object src, SocketAsyncEventArgs result)
    {
        Debug.Log("connectCallback2, isCompleted:" + result + " " + result.SocketError);
        if (result.SocketError != SocketError.Success)
        {
            socketError = result.SocketError;
        }
        else
        {
            Socket socket = (Socket)src;
            socket.Blocking = false;
        }
    }

    void connectCallback(IAsyncResult result)
    {
        Debug.Log("connectCallback22, isCompleted:" + result.IsCompleted);
        // connected = true;
        try
        {
            Socket socket = (Socket)result.AsyncState;
            socket.EndConnect(result);

            Debug.Log("socket is connected:" + socket.Connected);
            socket.Send(new byte[] { });
        }
        catch (Exception e)
        {
            Debug.LogError("error:" + e);
            socketError = SocketError.ConnectionRefused;
        }
    }

    public void disconnect()
    {
        if (socket != null)
        {
            socket.Close();
            socket = null;
        }
    }

    //

    public NetMsg read()
    {
        // 这里同步读也可以,但只读数据接收到足够一个消息的情况
        // 使用availed预判进行同步读,不会阻塞主线程
        // 解析消息放在这里。
        if (socket != null && socket.Available > 0)
        {
            if (available == 0)
            {
                if (socket.Available < 12)
                {
                    // 一个数据包,最小为12字节
                    return null;
                }
                // 开始从socket中读入一条数据
                // 如果足够,就直接解析为消息返回
                // 如果不够,就将数据放在cache中

                socketRead(2);
                if (socketError != SocketError.Success)
                {
                    return null;
                }

                length = readUshort();

                if (length == 0)
                {
                    socketRead(4);
                    if (socketError != SocketError.Success)
                    {
                        return null;
                    }
                    length = readInt();
                }
                int socketAvailable = socket.Available;
                needRead = length;
                if (socketAvailable < needRead)
                {
                    // 不足时,socket有多少缓存数据就读多少
                    socketRead(socketAvailable);
                    if (socketError != SocketError.Success)
                    {
                        return null;
                    }

                    needRead = needRead - socketAvailable;
                    return null;
                }
                else
                {
                    // 数据足够,解析数据
                    socketRead(needRead);
                    if (socketError != SocketError.Success)
                    {
                        return null;
                    }
                    return readMsg();
                }
            }
            else
            {
                // 继续从socket中接收数据
                int socketAvailable = socket.Available;
                if (socketAvailable < needRead)
                {
                    // 数据依旧不足
                    socketRead(socketAvailable);
                    if (socketError != SocketError.Success)
                    {
                        return null;
                    }

                    needRead = needRead - socketAvailable;
                    return null;
                }
                else
                {
                    // 数据足够,解析数据
                    socketRead(needRead);
                    if (socketError != SocketError.Success)
                    {
                        return null;
                    }

                    return readMsg();
                }
            }
        }
        return null;
    }

    private void readReset()
    {
        // 读入一个完整消息后,重置数据
        available = 0;
        readBufIndex = 0;
        length = 0;
        needRead = 0;
    }
    private NetMsg readMsg()
    {
        NetMsg netMsg = new NetMsg();

        UInt32 loop = readUInt();
        short cmdId = readShort();

        byte[] protoData = null;
        if (cmdId > 0)
        {
            protoData = new byte[length - 10];
            Array.Copy(readBuf, readBufIndex, protoData, 0, length - 10);
        }
        else
        {
            // 有压缩
            MemoryStream inms = new MemoryStream(readBuf, readBufIndex, length - 10);
            MemoryStream outms = new MemoryStream();
            SevenZipTool.Unzip(inms, outms);
            protoData = outms.ToArray();
            cmdId = (short)-cmdId;
        }
        

        netMsg.loop = loop;
        netMsg.cmdType = (cmd.CmdType)cmdId;
        netMsg.data = protoData;
        netMsg.netdata=new MemoryStream(protoData);
        readReset();
        return netMsg;
    }

    private short readShort()
    {
        short ret = BitConverter.ToInt16(readBuf, readBufIndex);
        readBufIndex += 2;
        return Endian.SwapInt16(ret);
    }
    private ushort readUshort()
    {
        ushort ret = BitConverter.ToUInt16(readBuf, readBufIndex);
        readBufIndex += 2;
        return Endian.SwapUInt16(ret);
    }

    private int readInt()
    {
        int ret = BitConverter.ToInt32(readBuf, readBufIndex);
        readBufIndex += 4;
        return Endian.SwapInt32(ret);
    }

    private uint readUInt()
    {
        uint ret = BitConverter.ToUInt32(readBuf, readBufIndex);
        readBufIndex += 4;
        return Endian.SwapUInt32(ret);

    }
    private void socketRead(int readLen)
    {    //从socket中读入数据入在readBuf中
        socket.Receive(readBuf, available, readLen, SocketFlags.None, out socketError);
        if (socketError != SocketError.Success)
        {
            Debug.LogError("socket Read error:" + socketError);
        }
        available += readLen;
    }

    public int send(NetMsg netMsg, int offset)
    {
        if (netMsg.totalData == null)
        {
            encode(netMsg);
        }

        int sendNum = socket.Send(netMsg.totalData, offset, netMsg.totalData.Length - offset, SocketFlags.None, out socketError);
        if (socketError != SocketError.Success)
        {
            Debug.LogError("socket send error:" + socketError);
            return 0;
        }

        if (sendNum + offset == netMsg.totalData.Length)
        {
            return -1;//标志,全部发送完成
        }
        return sendNum;
    }


    private void encode(NetMsg netMsg)
    {
        MemoryStream outstream = new MemoryStream();
        byte[] _t = null;

        int totalLength = netMsg.data.Length + 10;
        if (totalLength > 65535)
        {
            _t = BitConverter.GetBytes(Endian.SwapInt16((short)0));
            outstream.Write(_t, 0, _t.Length);
            _t = BitConverter.GetBytes(Endian.SwapInt32(totalLength));
            outstream.Write(_t, 0, _t.Length);
        }
        else
        {
            _t = BitConverter.GetBytes(Endian.SwapInt16((short)totalLength));
            outstream.Write(_t, 0, _t.Length);
        }

        _t = BitConverter.GetBytes(Endian.SwapUInt32(netMsg.loop));
        outstream.Write(_t, 0, _t.Length);
        _t = BitConverter.GetBytes(Endian.SwapInt16((short)netMsg.cmdType));
        outstream.Write(_t, 0, _t.Length);
        outstream.Write(netMsg.data, 0, netMsg.data.Length);
        _t = BitConverter.GetBytes(Endian.SwapInt32((int)0));
        outstream.Write(_t, 0, _t.Length);

        _t = outstream.ToArray();
        netMsg.totalData = _t;
    }

    public bool isConnected()
    {
        return socket != null && (socket.Connected);
    }
}
//----------------------------------------------
//           NET        : NetManager
//           AUTHOR: zhu kun qian
//----------------------------------------------

using System;
using System.Linq;
using System.Net.Sockets;

using UnityEngine;
using System.Collections.Generic;
using System.IO;


public class NetManager
{
    public uint loop;// 永不重复,一直加1

    public string ip = "";
    public int port = 0;

    private NetManagerSocket socket = null;

    private int maxQuerySize = 100;//接收和发送最多cache 100条消息
    private int maxReconnectTimes = 3;//重连最多重试3次

    // 发送相关
    public Queue<NetMsg> msgQuery = new Queue<NetMsg>(); // 发送消息队列
    public NetMsg reconnectTokenLogin = null;//重连发送的登陆消息
    public bool loginSuccess = false;

    private int sendBytes = 0;
    private float sendBytesTimeout = 0f; // 发送消息超时

    // 接收的消息队列
    private Queue<NetMsg> receviedMsgQueue = new Queue<NetMsg>();// 从服务器接收到的消息

    private int reconnectTryTimes = 0; // 重连次数
    private float connectTimeout = 0f; // 连接超时
    public EventDelegate.Callback reconnectErrorCallback; // 如果出现内部无法处理的得连(1、连试3次无法重连成功 2、累积的消息数量过量,需要重启游戏客户端)
    

    // 如果断线后,是使用原socket重连,还是使用新的socket?新new出来
    public static NetManager Instance = null;
    public NetManager()
    {
        Instance = this;
    }

    public bool isConnected()
    {
        return socket != null && socket.isConnected();
    }
    public void connect()
    {
        if (socket != null)
        {
            Debug.LogError("socket is not closed,try close");
            socket.disconnect();
        }

        Debug.Log("start connect ip:" + ip + " port:" + port);

        socket = new NetManagerSocket();
        socket.ip = ip;
        socket.port = port;
        socket.connect();

        sendBytes = 0;
        sendBytesTimeout = 0f;

        connectTimeout = 0f;
    }

    public void disconnect()
    {
        if (socket == null)
        {
            Debug.LogError("socket is null");
            return;
        }
        socket.disconnect();
        socket = null;
    }

    public void onUpdate()
    {
        // 每祯执行,没有阻塞
        if (socket != null)
        {
            // 改为在socket中处理呢
            if (socket.socketError != SocketError.Success)
            {
                // 如果遇到sokcet错误,不需要等等超时,立即重连
                Debug.LogError("socket error:" + socket.socketError);
                tryReconnect();
                return;
            }

            if (socket.isConnected())
            {
                NetMsg msg = socket.read();
                if (msg != null)
                {
                    receviedMsgQueue.Enqueue(msg);
                }
                NetMsg netMsg = null;
                if (reconnectTokenLogin != null)
                {
                    netMsg = reconnectTokenLogin;
                }
                else
                {
                    if (loginSuccess)
                    {
                        lock (msgQuery)
                        {
                            if (msgQuery.Count > 0)
                            {
                                netMsg = msgQuery.First();
                            }
                        }
                    }
                }
                if (netMsg != null)
                {
                    socketSend(netMsg);
                }
            }
            else
            {
                if (connectTimeout == 0f)
                {
                    connectTimeout = Time.realtimeSinceStartup;
                }
                else if (Time.realtimeSinceStartup - connectTimeout > 5)
                {
                    // 连接5秒超时,处理重连
                    tryReconnect();
                }
            }
        }
    }

    private void tryReconnect()
    {
        if (reconnectTryTimes >= 3)
        {
            // 跳出错误处理回调,交给外部处理
            if (reconnectErrorCallback != null)
            {
                reconnectErrorCallback();
            }
            disconnect();
            return;
        }
        Debug.LogError("socket connect timeout, try reconnect:" + reconnectTryTimes + " " + socket.socketError);
        reconnectTryTimes++;
        disconnect();
        connect();
        // 重试需要,重新发送登陆消息505
        LoginState.Instance.loginWithTokenSub(true);

    }

    public NetMsg readMsg()
    {
        if (receviedMsgQueue.Count == 0)
        {
            return null;
        }
        return receviedMsgQueue.Dequeue();
    }

    // true:超时
    private void socketSend(NetMsg netMsg)
    {
        // 发送数据
        bool newMsg = false;// 新发送的消息
        if (sendBytes == 0)
        {
            newMsg = true;

        }
        int num = socket.send(netMsg, sendBytes);
        if (num > 0)
        {
            sendBytes += num;
        }
        if (num == -1)
        {
            // 全部发送完成
            if (cmd.CmdType.tokenLogin == netMsg.cmdType)
            {
                reconnectTokenLogin = null;
            }
            else
            {
                lock (msgQuery)
                {
                    msgQuery.Dequeue();
                }
            }
            sendBytes = 0;
            sendBytesTimeout = 0f;
        }
        else
        {
            // 未发送完成,处理超时逻辑
            if (newMsg)
            {
                sendBytesTimeout = Time.realtimeSinceStartup;
            }
            else
            {
                // 检查时间是否超时
                if (Time.realtimeSinceStartup - sendBytesTimeout > 5)
                {
                    // 超过5秒
                    Debug.LogError("socket timeout.try reconnect");
                    // 重连重发
                    if (socket.socketError != SocketError.Success)
                    {
                        Debug.LogError("socket error:" + socket.socketError);
                    }
                    socket.socketError = SocketError.TimedOut;
                }
            }
        }
    }

    public void SendCmd<T>(cmd.CmdType cmdType, T protoObj)
    {
        send(cmdType,protoObj);
    }
    public void send<T>(cmd.CmdType cmdType, T protoObj)
    {
        NetMsg netMsg = new NetMsg();
        netMsg.loop = ++loop;
        netMsg.cmdType = cmdType;

        MemoryStream outms = new MemoryStream();
        ProtoBuf.Serializer.Serialize(outms, protoObj);
        netMsg.data = outms.ToArray();

        // todo:因为放在onupdate中,感觉这个lock也是可以避免掉的。暂时先加上,以后测试后再考虑去掉。
        // 只要能确认不会多线程操作,就可以去掉这个lock
        if (cmdType == cmd.CmdType.tokenLogin)
        {
            reconnectTokenLogin = netMsg;
            loginSuccess = false;
            return;
        }
        lock (msgQuery)
        {
            msgQuery.Enqueue(netMsg);
            // 在onupdate中发送,这样只差3ms,是可以接受的
        }

        if (msgQuery.Count > maxQuerySize)
        {
            Debug.LogError("msgQuery more than max size:" + msgQuery.Count);
        }
    }
}








2015.01.19记:我重构时,未添加断线补发未送达的消息,我们客户端写的逻辑不严谨,在登陆游戏按钮上连续多次点击,会导致卡住登陆。我这里只是使用禁止多次点击来避免这个问题,如果使用有断线补发未送达的消息也可以不会出现这个问题。总之,不要依赖客户端代码很强壮,最好还是添加上断线补发消息。


Warning: Use of undefined constant XML - assumed 'XML' (this will throw an Error in a future version of PHP) in /opt/wordpress/wp-content/plugins/wp-syntaxhighlighter/wp-syntaxhighlighter.php on line 1048