项目总结之截屏细节考虑
DionysosLai(906391500@qq.com) 2014/12/22
2014项目总算告一段落,这个产品顺顺利利从开始到最后的上市,圆满成功。借着项目总结,回顾自己做的几个游戏,将一些细节问题归纳。第一篇,就以目前正在做的的新游戏《圣诞节》开篇,讲讲游戏截屏问题。
对于截屏,本身技术,并不是很复杂,一般有两种方法,一种是使用Opengl像素取点方式;另一种是使用RenderTexture纹理方法,详细内容,可以参考,之前写的一篇文章:http://blog.csdn.net/dionysos_lai/article/details/23467209。
两个技术方案考究:
在我游戏中,我使用的是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,
"com.mesrjni.MesrJni", "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,
"com.mesrjni.MesrJni",
"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(), str.toString(),
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://blog.csdn.net/dionysos_lai/article/details/38389765 和 http://blog.csdn.net/dionysos_lai/article/details/40350313 中详细方法的原理与解决方法。
到此文件读写操作细节,基本阐述完毕。
截屏细节:
截屏的效果,大家要参考的话,可以玩一下游戏Toca Boca 游戏《FairyTales》中截屏效果,ps:这个游戏的装扮做的真好。
对于截屏具体技术,上面已经分析过了。但是,由于这次游戏出现一个新问题,就是使用了CCLayerColor类,在游戏关灯时,会罩一个黑色但有透明的图层,表示晚上的感觉。下面两张图片,分别表示白天和夜晚。
由于夜晚使用的是的CCLayerColor,本身带有透明度,因此在截屏时,获取的图片跟看起来不大一样。如果没有做混合处理,实际截取出来的图片,类似如下所示;
如何处理呢?就必须使用混出处理。什么是混出呢?这里可以参考以前写的一篇文章:
http://blog.csdn.net/dionysos_lai/article/details/39030081 这篇文章开头详细介绍了混合原理,以及如何做擦除效果,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上了。
希望上述,对大家有些有帮助。