项目总结之截屏细节考虑
dionysosLai(906391500@qq.com) 2014/12/22
2014项目总算告一段落,这个产品顺顺利利从开始到最后的上市,圆满成功。借着项目总结,回顾自己做的几个游戏,将一些细节问题归纳。第一篇,就以目前正在做的的新游戏《圣诞节》开篇,讲讲游戏截屏问题。
对于截屏,本身技术,并不是很复杂,一般有两种方法,一种是使用Opengl像素取点方式;另一种是使用RenderTexture纹理方法,详细内容,可以参考,之前写的一篇文章:http://www.jb51.cc/article/p-bxxfyjsw-qs.html。
两个技术方案考究:
在我游戏中,我使用的是RenderTexture方法,之所以我要使用的是RenderTexture方法,而不是Opengl中获取像素点glReadPixels的方法,是基于以下几点考虑:
1.二者在效率方面,基本没有差别,可以不考虑;(实际上RenderTexture方法,效率应该更低点);
2.由于在截屏时,我们不可能是将整个当前屏幕所有的元素全部截取下来,必然存在一些元素并不是我们需要的,还有一些元素需要我们临时贴上去,比方说logo之类的。这样的话,使用Opengl方法,必然在截屏之前,必须先去掉一些元素、添加上一些元素,截屏结束,也必须将元素反过来处理一遍。这之间,就存在着一些时间差,做的不好,就是出现闪屏效果。而使用Rendertexture方法,就可以在begin和end之外对元素进行处理,不会有闪屏效果。
3.如果游戏中,我们使用了蒙版技术或者贴上一些半透明图片,难么很抱歉,如果不对图片进行特殊出处理,游戏中图片和截屏出来的图片是不一样的。而使用Rendertexture方法,我们可以很容易先对图片进行混合处理,使截屏图片和游戏画面一致。
截屏细节把握:
正是基于以上三点,我采用的RenderTexture方法。下面,详细介绍截屏细节。
1. 文件读写问题:
文件读写问题,包括图片保存和获取,由于图片读取,基本上不用考虑很多,因此重点是文件写问题。
对于文件写问题,首先考虑的第一个问题就是系统容量检测问题,这点十分必要,如果系统容量不够,而程序强制性写入,导致的第一个问题必然是程序卡机,甚至是系统挂掉(我们平板是第一次试水,在防护方面做得比较差,曾经有音效加载问题,导致系统奔溃)。这可是一个不折不扣的A类bug啊,如果没做这不处理,那么就等着测试妹子找你麻烦吧。
检测系统代码如下:
.h
///@brief 检测sd卡是否可用容量足够 ///@param[in] size---检测容量 注意:这里是已MB来就算 ///@return 0---sd卡不可用 1---sd卡容量不够 2---sd卡可用,容量足够 int checkAvailableSDSize(const unsigned int& size);
.cpp
int HomeScene::checkAvailableSDSize( const unsigned int& size ) { int availableOk = 0; /// 目前,只做android平台检测,其他平台一律默认通过 #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) JniMethodInfo methodInfo; jint ret = 0; if (JniHelper::getStaticMethodInfo(methodInfo,"com.mesrjni.MesrJni","checkAvailableSDSize","(I)I")) { ret = methodInfo.env->CallStaticIntMethod(methodInfo.classID,methodInfo.methodID,size); if (0 == ret) { CCLOG("sd's state is abnormal!"); availableOk = 0; } else if (1 == ret ) { CCLOG("sd's availabel size is not enough!"); availableOk = 1; } else if (2 == ret ) { CCLOG("sd's availabel size is enough!"); availableOk = 2; } else { CCAssert(false,"There is some wrong"); } } #else availableOk = 2; #endif return availableOk; }
.java
/* * 检测sd卡是否可用容量足够 */ public static int checkAvailableSDSize(int size){ /// 先检测SD卡是否可用 String state = Environment.getExternalStorageState(); if(!Environment.MEDIA_MOUNTED.equals(state)){ /// 对sd卡上的存储可以进行读/写操作 Log.d("DEBUG","SD's state is abnormal!"); /* new AlertDialog.Builder(activity) .setTitle("圣诞节" ) .setMessage("小朋友,目前不能保存相册,快叫爸爸妈妈来解决吧!" ) .setPositiveButton("确定",null ) .show(); */ return 0; } /// 获取sd卡用容量 File path = Environment.getExternalStorageDirectory(); //取得sdcard文件路径 StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSize(); long availableBlocks = stat.getAvailableBlocks(); long availableSize = (availableBlocks * blockSize)/1024/1024; Log.d("DEBUG","可用空间:" + availableSize + "Mb"); /// 判断容量是否足够 if(size > availableSize){ Log.d("DEBUG","The sd's availavle size is not enough!"); /* new AlertDialog.Builder(activity) .setTitle("圣诞节" ) .setMessage("小朋友,SD卡空间不足了哦,不能保存相册。快叫爸爸妈妈清理一下吧!" ) .setPositiveButton("确定",null ) .show();*/ return 1; } /// 容量足够 Log.d("DEBUG","The sd's availavle size is enough!"); /* new AlertDialog.Builder(activity) .setTitle("圣诞节" ) .setMessage("小朋友,SD卡空间不足了哦,不能保存相册。快叫爸爸妈妈清理一下吧!" ) .setPositiveButton("确定",null ) .show();*/ return 2; }
这里调用JNI方法,因此这段代码"com.mesrjni.MesrJni",要根据自己游戏进行适配;同时这里注意我注释的一段话,我会调用一个AlertDialog类,来提醒玩家出现问题。这里我将代码注释掉了,因为会出现一些很奇怪问题,其实与我下面JNI调用Toast类一样,后面讲。
保存图片,一般是保存在自己的一个文件夹中,因此我们要首先判断我们是否创建了文件夹,没有,就直接创建一个新的文件夹。这里代码简单,ps,在在创建文件之间,要确保SD可用,这个工作也是必须要做的,可参考上面代码:
File destDir = new File("/sdcard/Christmas/"); if (!destDir.exists()) { destDir.mkdirs(); }
这里我创建了一个Christmas文件夹。
由于保存图片工作,在Android端比较慢,大约会有2s左右时间,这个时间还得看这个平台的硬件水平,在我们公司的平板上,一般是2s左右。因此,点击保存图片时,会出现游戏卡顿问题,那么玩家还以为是游戏没反应,会一直点击保存图片图标(这个现象,基本是玩过这个游戏的玩家,都会如此操作)。因此,很有必要添加一个提示保存图片中的提示标签。
保存提示:
保存提示功能,这里我直接使用了Android自带的Toast类,同时使用Toast类,只能是使用JNI非静态类调用,因此要先同时JNI静态类调用,或者App的object对象。
获取App的object对象:
.cpp
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) JniMethodInfo methodInfo; jobject jobj; bool isHave = JniHelper::getStaticMethodInfo(methodInfo,"getRunActivity","()Ljava/lang/Object;"); if (isHave) { jobj = methodInfo.env->CallStaticObjectMethod(methodInfo.classID,methodInfo.methodID); } #else
.java
/* * 获取this */ public static Object getRunActivity() { System.out.println("----------GetRunActivity"); return activity; }
下面就是调用调用Toast类方法类,给出两端java不同代码,仔细分析问题出现的原因:
.cpp
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) JniMethodInfo methodInfo2; /// 中文转码 std::string strInfo("图片已保存在系统相册中。"); XtcUtils::GBKToUTF8(strInfo); const char* info = strInfo.c_str(); if (JniHelper::getMethodInfo(methodInfo2,"showToast","(Ljava/lang/String;)V")) { methodInfo2.env->CallVoidMethod(jobj,methodInfo2.methodID,methodInfo2.env->NewStringUTF(info)); } #else #endif
.java
代码1: public void showToast(final String str) { Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(),str.toString(),Toast.LENGTH_SHORT).show(); } }); } 代码2: public void showToast(final String str) { Handler handler = new Handler(getApplicationContext().getMainLooper()); handler.post(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(),Toast.LENGTH_SHORT).show(); } }); }
前一段代码,会导致一个Can't create handler inside thread that has not calledLooper.prepare(),至于具体,可参考文章:http://www.cnblogs.com/sonicit/archive/2013/01/13/2858475.html,这里详细讲解了原因。
上一段代码,调用了函数GBKToUTF8,这个函数是用来c++与java转码问题,我在文章:http://www.jb51.cc/article/p-wbwumdwo-qs.html 和 http://www.jb51.cc/article/p-vyrtmmot-qs.html 中详细方法的原理与解决方法。
到此文件读写操作细节,基本阐述完毕。
截屏细节:
截屏的效果,大家要参考的话,可以玩一下游戏Toca Boca 游戏《FairyTales》中截屏效果,ps:这个游戏的装扮做的真好。
对于截屏具体技术,上面已经分析过了。但是,由于这次游戏出现一个新问题,就是使用了CCLayerColor类,在游戏关灯时,会罩一个黑色但有透明的图层,表示晚上的感觉。下面两张图片,分别表示白天和夜晚。
由于夜晚使用的是的CCLayerColor,本身带有透明度,因此在截屏时,获取的图片跟看起来不大一样。如果没有做混合处理,实际截取出来的图片,类似如下所示;
如何处理呢?就必须使用混出处理。什么是混出呢?这里可以参考以前写的一篇文章:
http://www.jb51.cc/article/p-yepuiwgm-qs.html 这篇文章开头详细介绍了混合原理,以及如何做擦除效果,ps:类似刮彩票效果,也可以使用这个原理来做。
对于混合处理,比较关键是设置混合参数。这里提供一个网站:http://www.andersriggelsen.dk/glblendfunc.php, 这个网站可以非常方面的帮助我们调试参数。
根据调试结果,混合参数为:源--GL_ONE,目标--GL_ONE_MINUS_SRC_ALPHA。
下面给出详细代码:
void HomeScene::ShowPicAlbum() { SimpleAudioEngine::sharedEngine()->playEffect(CHR_MF_CAMERA_EXPOSURE); //根据要截取屏幕大小,定义一个渲染纹理 m_renderTextureSplot = CCRenderTexture::create(1280,800); CCScene* pCurScene = CCDirector::sharedDirector()->getRunningScene(); CCPoint ancPos = pCurScene->getAnchorPoint(); visibleNode(); //渲染纹理开始捕捉 int outOpa = m_colorlayerTreeOut->getOpacity(); int inOpa = m_colorlayerTreeIn->getOpacity(); ccBlendFunc blendFunc = { GL_ONE,GL_ONE_MINUS_SRC_ALPHA}; ///< 设置混合模式,这里不设置混合的话,关灯是,遮罩会有一种透明关系。 ccBlendFunc blendFuncB = m_colorlayerTreeIn->getBlendFunc(); m_colorlayerTreeOut->setBlendFunc(blendFunc); m_colorlayerTreeIn->setBlendFunc(blendFunc); m_renderTextureSplot->begin(); //绘制当前场景 pCurScene->visit(); //结束 ……(下面是一些动作代码) }基本上,这次做的游戏截屏细节差不多就这些了,还有一些测试出来的bug,跟这个没什么关联,因此不阐述了。由于代码是从项目里抽出来的,因此不可能完整将代码公式出来,就不是上传代码到自己的Githup上了。
希望上述,对大家有些有帮助。