存档

2017年3月 的存档

[置顶] 房卡麻将分析之"防作弊处理"

2017年3月29日 没有评论

房卡麻将分析之"防作弊处理"

           

          棋牌游戏最重要的一个特点就是人与人对局,因为玩家各自的不可见,就存在着一些作弊的可能性和漏洞。对于手机房卡麻将游戏,大家最讨厌的问题就是作弊。如何防止玩家作弊,保证玩家的公平性,从技术上我们来讨论一些方案。



          首先,我先假设你的代码中服务器并不会犯一些低级错误,比如将所有玩家的手牌信息发给客户端。如果你没有保证好这一点,那么你的游戏将非常容易被辅助外挂进行”明牌化“处理。除去算牌记牌软件的因素,你的服务器只要不存在相关的漏洞就不用担心外挂。

                               [置顶]        房卡麻将分析之"防作弊处理"
          去掉服务器的因素,在客户端现在最经常出现的防作弊方案主要有两个方向:

一。玩家信息检测


第一种方向,主要是对于玩家的状态进行检测,目前也包括三个小的方法:


(一)玩家同IP提示。


          第一种方法它只是能够预防基于同一个局域网的玩家在一起游戏。处理也非常简单,因为服务器在接收手机联接时可以取得客户端IP地址,通过简单的转发,就可以让玩家得知同房间其它玩家的IP地址。如果房间里有相同IP的玩家,做一些提示即可。不过这种方案并不能够有效的预防作弊,基于4G信号来进行游戏的手机可以轻验跳过这样的验证。更多的时候只是一个心理预防。


(二)玩家GPS定位提示。


           第二种方法使用GPS定位来侦测玩家间距离相近或者定位一致。比第一种方案更真实的反映玩家是否在一起的情况。具体开发的时候,可以使用高德或百度的地图SDK,开发者到高德官网上申请账号,注册成为开发者后,可以创建应用,将包的信息提交,取得KEY,按照官网SDK接入游戏即可以方便的获取定位信息了。

[置顶]        房卡麻将分析之"防作弊处理"

           在服务器的数据库上,一般会有玩家上线信息的表,包括头像地址和GPS地址,玩家登录游戏后,通过高德或百度SDK取得当前定位并发送给服务器存储到数据库中,当玩家进入房间后,服务器也将其它玩家的相关信息发过来,在客户端进行对比并提示即可。

                        [置顶]        房卡麻将分析之"防作弊处理"

               在整个游戏过程中,可以根据需要进行定位更新处理。保证牌局对玩家进行实时检测。不过一般只需要游戏每局开局时做一些简单处理即可。


(三)玩家游戏过程视频在线。  


               第三种方法则可以在游戏过程中开启视频聊天,或用微信视频聊天,可以随时看到对方玩家在专注的游戏,也就避免玩家去进行作弊操作。不过这种方式相对比较耗电,技术上也相对较难一点。并不是太推荐。


               总体说来,在第一个方向上第二种方法比较可行,也较易操作。

二。牌局信息对比


第二种方向,主要是对于牌局的过程进行检查,目前也包括两个小的方法:


(一)牌局回放。

                牌局回放的意义在于玩家可以在游戏结束后对整个过程进行回顾,分析四个玩家的牌面进展整个过程,发现一些作弊操作。这个部分的技术方法我在公众号早期文章中进行了详细讲述,这里不再赘述,有兴趣的朋友可以查一下公众号历史文章。


                       [置顶]        房卡麻将分析之"防作弊处理"

(二)吃碰杠的供应记录。

              作弊的手段无非就是给牌,但一般游戏最容易的给牌就是吃和碰,在这方面做一些记录和显示,可以给玩家展现游戏过程中玩家是如何给牌的。比如我们的”大赢家“红中麻将,你可以清楚的看到哪个玩家给哪个玩家的碰牌。这种信息的处理非常重要和细致。

[置顶]        房卡麻将分析之"防作弊处理"

                 好了,基本上来说,并没有什么办法完全避免基于手机的房卡麻将,我们所做的手段,无非是降低作弊难度,提升公平性。如果有人说房卡麻将能百分之百防作弊,我觉得还是不要相信啦。最后我想说的是:游戏开心,远离赌博!

 

                  AR,VR,房卡棋牌技术,关注公众号:红孩儿的游戏开发之路

                                                   [置顶]        房卡麻将分析之"防作弊处理"

分类: 未分类 标签:

OpenGL(六)使用 VAO 打包指令 优化代码结构

2017年3月29日 没有评论

通常说来当创建好vbo的数据结构,还需要设置glVertexAttribPointer等一系列属性,在OpenGL中提供了 VAO (Vertex Array Object)它相当于同时记录了数据在哪里和数据是怎样分布。从实际应用的角度,它最重要的贡献就是简化了代码。

原理

通过接口函数生成一个VertexArray,然后在其中创建和设置VBO的相关参数,在最终绘制时,直接使用。

GLuint myVao;
glGenVertexArrays(1,&myVao);
glBindVertexArray(myVao);
//... some vbo operation
glBindVertexArray(0);

实现

通过C++的匿名函数,可以将vbo操作开放出去

#include <functional>

GLuint CreateVAOWithVBOSettings(std::function<void()> settings)
{
    GLuint vaoTemp;
    glGenVertexArrays(1,&vaoTemp);
    glBindVertexArray(vaoTemp);
    settings();
    glBindVertexArray(0);
    return vaoTemp;
}

调用时,将vbo过程作为参数传入:

struct VertexData
{
    float positon[3];
    float texcoord[2];
    float normal[3];
};

GLuint CreateBufferObject(GLenum bufferType,GLsizeiptr size,GLenum usage,void* data)
{
    GLuint object;
    glGenBuffers(1,&object);
    glBindBuffer(bufferType,object);
    glBufferData(bufferType,size,data,usage);
    glBindBuffer(bufferType,0);
    return object;
}

GLuint vao = CreateVAOWithVBOSettings([&]()->void
{
    GLuint vbo = CreateBufferObject(GL_ARRAY_BUFFER, sizeof(VertexData) * vertexCount, GL_STATIC_DRAW, vertexes);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glEnableVertexAttribArray(posLocation);
    glVertexAttribPointer(posLocation, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), (void*)0);
    glEnableVertexAttribArray(texcoordLocation);
    glVertexAttribPointer(texcoordLocation, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData), (void*)(sizeof(float) * 3));
    glEnableVertexAttribArray(normalLocation);
    glVertexAttribPointer(normalLocation, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), (void*)(sizeof(float) * 5));
    glBindBuffer(GL_ARRAY_BUFFER, 0);
});


//draw
glBindVertexArray(vao);
//...
glBindVertexArray(0);

经过这样的改动,绘制部分变得非常简单。

总结

可以将 VAO 理解为一个存储指向VBO指针的数组,实际上数据还是储存在VBO中。它是将切分读取数据的过程打包在一起,避免了每次都编写繁琐的代码。

OpenGL(六)使用 VAO 打包指令 优化代码结构

关注我的微信公众号,获取更多优质内容

分类: 未分类 标签:

[置顶] 房卡麻将分析之"一键入局"

2017年3月28日 没有评论

  地方棋牌,VR,AR技术,请关注公众号:"红孩儿的游戏开发之路“

                  房卡麻将分析之"一键入局"


         房卡麻将通过微信邀请好友加入房间进行游戏,大大方便了玩家进行麻将游戏。这也凸显了微信对于游戏引流导量的强大平台作用。本次我们就给大家讲一下“一键入局”的技术原理。


          首先,我们先来疏理一下整个过程:

          玩家打开游戏,创建房间,并将房间信息(房间说明和房间号码)生成链接通过微信SDK的分享到好友或朋友圈。其它玩家看到这个链接并点击后,如果已经安装游戏,会提示打开游戏APP并直接加入房间。如果未完装游戏,则会转到游戏下载页。


          这是在微信中分享房间的链接:

              [置顶]        房卡麻将分析之&quot;一键入局&quot;
         [置顶]        房卡麻将分析之&quot;一键入局&quot;
           这里非常重要的一个技术是用到“魔窗”。它是实现通过微信链接打开App并进入房间的关键。

             [置顶]        房卡麻将分析之&quot;一键入局&quot;

           “魔窗”的主页是:http://www.magicwindow.cn,它有非常完整的集成文档,包括Android和IOS,Web等多个平台系统的接入说明,中外还有专业技术妹子的接入视频讲解,非常详尽。

             [置顶]        房卡麻将分析之&quot;一键入局&quot;
              这里主要使用的就是mLink,按照官方的文档一步步接入即可。在这一块,主要是需要生成一个短链接,这个短链接需要带上房间号信息,就像下面这样:

             [置顶]        房卡麻将分析之&quot;一键入局&quot;



             OK,配置好后台的信息并按文档集成好SDK后,在客户端逻辑部分要做的是当"魔窗" 唤醒App时所响应的函数中,将短链接房间号信息取出来。

AppActivity.java中的代码片段:

			MLink.getInstance(context).register("first",new MLinkCallback()
			{
				public void execute(Map<String, String>paramMap, Uri uri,Context context){
					String roomid = uri.getQueryParameter("roomid");
					if (roomid != null)
					{
						Native.WxAutoLoginSetInfo(Integer.parseInt("1"), roomid);
					}
				}
			});

JniCallback.cpp中的代码片段:


		JNIEXPORT void JNICALL Java_org_cocos2dx_cpp_Native_WxAutoLoginSetInfo(JNIEnv* env, jclass method, jint autoLogon, jstring roomId)
		{
			const char* data = env->GetStringUTFChars(roomId, 0);
			MissionWeiXin::m_nAutoLogin = autoLogon;
			MissionWeiXin::m_strRoomID = data;
		}

             有了房间号,设置给游戏中的变量,并在游戏中判断接收到的房间号是否有效,调用加入房间的函数进入房间,即可完成整个“一键入局”的处理了。


void SDHomeScence::update(float delta)
{
    //如果玩家自动进入游戏
	if (MissionWeiXin::m_nAutoLogin == 1 && isVisible())
    {
        int iActJoinNum = atoi(MissionWeiXin::m_strRoomID.c_str());
        int iServerID = iActJoinNum / 10000 - 10;
        SetJoinRoomIdInfo(iActJoinNum );
        
        CGameServerItem* pGameServer = GameManagerBase::InstanceBase().SearchGameServer(iServerID);
        if ( pGameServer )
        {
GameManagerBase::InstanceBase().connectGameServerByServerID(iServerID);
        }
        else
        {
            NoticeMsg::Instance().ShowTopMsgByScript("JoinRoomNumError");
        }
        MissionWeiXin::m_nAutoLogin = 0;
    }
}

        如此,整个过程就算基本完成了。好了,加入了微信”一键入局“,相信我们的房卡麻将游戏可以大大提升下载量和试玩量。


  地方棋牌,VR,AR技术,请关注公众号:"红孩儿的游戏开发之路“

分类: 未分类 标签:

OpenGL(五) 指令错误 检测的封装方法

2017年3月27日 没有评论

OpenGL的指令,返回值均为void,因此没法通过返回值来判断 指令错误 。为了能够第一时间发现问题,需要加入一个封装来监测是否有指令调用失败。

原理

当OpenGL调用出现错误时,会将错误的ID储存到一个GLenum中。这个值可以通过

glGetError();

获取到。

实现

通过一个宏可以封装出GL 指令错误 的调用检查接口。实现如下:

//.h
void CheckGLError(const char* file,int line);
#define  GL_CALL(x) do{x;CheckGLError(__FILE__,__LINE__);}while(0)

然后去实现这个检测函数:

//.cpp
void CheckGLError(const char* file,int line)
{
    GLenum error = glGetError();
    if(error != GL_NO_ERROR)
    {
        switch (error)
        {
        case GL_INVALID_ENUM:
            printf("GL Error: GL_INVALID_ENUM %s : %d /n",file,line);
            break;
        case GL_INVALID_VALUE:
            printf("GL Error: GL_INVALID_VALUE %s : %d /n",file,line);
            break;
        case GL_INVALID_OPERATION:
            printf("GL Error: GL_INVALID_OPERATION %s : %d /n",file,line);
            break;
        case GL_STACK_OVERFLOW:
            printf("GL Error: GL_STACK_OVERFLOW %s : %d /n",file,line);
            break;
        case GL_STACK_UNDERFLOW:
            printf("GL Error: GL_STACK_UNDERFLOW %s : %d /n",file,line);
            break;
        case GL_OUT_OF_MEMORY:
            printf("GL Error: GL_OUT_OF_MEMORY %s : %d /n",file,line);
            break;
        default:
            printf("GL Error: 0x%x %s : %d /n",error,file,line);
            break;
        }
    }
}

调用时可以这样写:

//call
GL_CALL(glEnable(GL_DEPTH_TEST));

如果GL指令出错,会在控制台中打印出行号。

总结

如果每次调用绘制指令都使用这个接口,就能在第一时间捕获 指令错误 。

另外可以设置项目属性来使win32程序运行时显示控制台状态。在项目->属性->配置属性->生成事件->后期生成事件中。在命令行上输入editbin /subsystem:console $(OutDir)$(ProjectName).exe。保存之后每次运行即可打开console

OpenGL(五) 指令错误 检测的封装方法

关注我的微信公众号,获取更多优质内容

分类: 未分类 标签:

Unity3d开发(二十) OnMouse_产生GC 的问题修复

2017年3月21日 没有评论

最近发现一个特别奇怪的现象,在游戏运行时,每帧 OnMove_产生GC ,大小是0.6KB的整数倍。具体Profiler效果如下图所示:

Unity3d开发(二十) OnMouse_产生GC 的问题修复

解决方法

经过比照发现这个问题与摄像机相关。由于是代码创建的摄像机组件,因此并未挂载GUILayer组件。解决这个问题的方法就是使用AddComponet<Camera>()的同时,调用AddComponent<GUILayout>()。在包含Camera的节点上添加这个组件即可解决 OnMove_产生GC 。

原理

由于没有Unity3d的源码只能通过经验推(xia)测(meng)。通过查询文档,发现这个组件主要是为了兼容之前版本的GUI。我觉得这里面的逻辑是,如果有这个组件,就会以引用的形式将这个组件储存起来,进而通过它找到GameObject以及Camera。否则,就需要使用GetComponent来查找。在每一帧调用GetComponent时就产生了GC。

总结

这…算个大坑吧。

Unity3d开发(二十) OnMouse_产生GC 的问题修复

关注我的微信公众号,获取更多优质内容

分类: 未分类 标签:

[置顶] 程序员三十岁之后要考虑什么?

2017年3月20日 没有评论

           程序员三十岁之后要考虑什么?

        周六受邀参加了触控科技CocoaChina开发者社区举办的"移动游戏人才培养和创业机会”的沙龙。其间被问到一个问题:“程序员三十岁之后该怎么办?”。


        “三十岁后该怎么办?”,好吧,虽然我一直都是二十八岁。。。,不过还是考虑一下吧。

              [置顶]        程序员三十岁之后要考虑什么?

        三十岁的程序员,其实正处于学习能力和项目经验的最佳平衡点。按理说是不存在什么问题的,不过人们常说:“三十而立”,进入三十岁,要面临买房讨老婆养家生娃的一系列压力山大阶段,的确是容易给当下高强度开发工作的程序员们一些困扰。我觉得我还是以个人经历给大家聊聊,如果能解解惑或做个参考也好。


       首先,年青时最好是有个中长期规划,不要过一天算一天,这个很重要。我当年上中学的时候,就因为爱上了编程,而立志要考大学计算机系,结果我如愿了。我来北京工作时,给自已订过一个目标,就是要在30岁前努力的成长,每年保持30%以上的薪水增长,30岁之后开始创业。虽然因为当时任引擎总监重任在肩创业晚了两年,但是大体也还是如愿了。还有就是2012年在CSDN开博客,我给自已订了目标,把Cocos2d-x的源码基本上给讲一遍。坚持了快一年,我的博客现在也百万访问量了。


        想想未来,给自已定个目标,容易让你走的更坚定,更有方向。之所以三十岁之后迷茫,是因为等你该进入到人生压力阶段时,你错过了最佳发展阶段时的努力,而导致这个的原因往往是漫无目的活。

        

        其次,要学会处理好事业和家庭的关系,游戏行业呆久了,见惯了大伙努力不要命的状态。有很多朋友为了工作,加班努力到半夜回家,甚至有的一周家人都见不到面,回到家里太晚,家人都睡了,第二天醒来时,老婆上班了,孩子上学了,自已又忙着往公司赶。这种状态我是觉得项目分阶段短期突击一下未尝不可,如果常态化,必然会影响家庭。这时候就需要好好想想,自已到底要什么?

                                 [置顶]        程序员三十岁之后要考虑什么?

         人,不是机器,钱是赚不完的,既然成家,就要学会平衡好对各方的责任和义务,才能走的更长远。


         最后,学会顺应天道。只要你没有停止学习和努力,那么经验和人脉的增长可以补足你在具体事情上精力不足的缺失。年龄本身并不是什么问题,主要还是心态和方法要变。人进入三十五岁关口后,身体会不如年青时那么能拼,要学会迎接长江后浪人材,年青人需要成长,需要拼的过程,而自已,也需要成长,但是化拙为巧,学会看清方向,学会省力,学会通过经验,人脉,市场分析等等引导干劲转化为成果,而不是和年青人拼体力。


        末了,祝各位过了三十的老将永远年青!

                           [置顶]        程序员三十岁之后要考虑什么?

分类: 未分类 标签:

[置顶] 房卡棋牌分析系列之"微信登录"

2017年3月20日 没有评论

”房卡“麻将研发技巧,尽在”红孩儿的游戏开发之路“,欢迎关注公众号!


                       房卡棋牌分析系列之"微信登录"

           房卡棋牌之所以火热,很大程度上取决于当下中国智能机的普及和微信作为基础通讯社交工具的广泛应用。在微信从青年人,到中年人,再到老年人的覆盖率不断扩张的现实中,基于微信登录的具有强大群众基础的棋牌游戏用户量大增。可以这么说,如果没有微信这样一款如此大影响力的通讯社交工具软件,就不会有今天房卡棋牌的火热。今天,我们就来分析一下如何在棋牌游工中加入“微信登录”


           要想应用“微信”的相关功能,首先要做的就是申请微信开发者资质。这需要到微信开放平台进行注册,成为一名微信开放平台的开发者。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

         目前要想成为开发者,除了按流程提交开发资质要求的证照之外,还需要交纳300元一年的费用。在这个过程中,客服会跟你电话沟通验证,并在通过后以挂号信形式将腾讯公司出具的发票寄给你。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;


          在注册好账号后,可以在管理中心里创建一个新的移动应用(一个账号最多可创建不超过十个移动应用),填写好应用的基本信息,平台信息后就可以提交了。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;


在这个过程中,要注意选择使用微信开放的功能。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

            应用创建成功之后,你将可以拿到AppIDAppSecret这两个重要信息。然后我们开始接入SDK的工作。


            首先,当然是下载微信SDK了,其实微信开发放平台提供了非常详细的下载说明和"接入指南",下载完之后,我们只需要仔细看一下,按着做就好了。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

               "移动应用"下的"微信登录功能"文档说明也非常详细,并有示例演示。"常见问题"中也提供一般性遇到的问题解答。相信对于大部分开发者来说并不是太大的问题。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

                  基本上来说,我们在游戏登录界面要做的就是通过点击"微信登录"按钮时调用如下代码:

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

            在调用这个处理之后,手机会弹出微信要求验证的处理。

                             [置顶]        房卡棋牌分析系列之&quot;微信登录&quot;

           点击“确认登录”,会回调到WXApiDelegate 的onResp函数,得到所要的code并和appid,secret,grant_type组成一个HTTP RUL 字符串 send到https://api.weixin.qq.com/sns/oauth2/access_token? 之后程序就会收到微信账号的昵称,性别,头像图片地址,国家等等基本信息了。下一步,就是跳转到登录成功后的大厅界面,将将这些信息显示在玩家信息栏位了。

[置顶]        房卡棋牌分析系列之&quot;微信登录&quot;


           当然,如果登录过一次后,往往可以省去再次登录,这样就需要你在登录成功后将access_tokenopenid信息存储,并在下次启动游戏时加一个处理直接进行登录。

	std::string access_token = cocos2d::UserDefault::getInstance()->getStringForKey("access_token");
	std::string openid = cocos2d::UserDefault::getInstance()->getStringForKey("openid", "");
	if (access_token != "" && openid != "")
	{
		Req_UserInfo(access_token,openid);
	}
	else
	{
		JniFun::longinWX("","");
	}

       这样,房卡棋牌的"微信登录"基本就做好了。在"微信"已经成为手机最主要的社交软件的今天,基于"微信登录"的好友开房模式游戏必将进一步发展壮大,希望今天的分享对大家有帮助。


分类: 未分类 标签:

OpenGL(四)Shader错误 检测

2017年3月20日 没有评论

由于shader不需要预编译,因此在OpenGL动态加载时会碰到 shader错误 。因此需要区分错误属于C++代码还是shader代码。本文主要探讨如何封装检测 shader错误 的接口。

原理 

在OpenGL中有方法能够获取到 shader错误 信息。使用:

GLint compileResult = GL_TRUE;
glGetShaderiv(shader,GL_COMPILE_STATUS,&compileResult);
glGetShaderInfoLog(shader,1024,&logLen,szLog);

可以将产生的错误储存到字符数组szLog中。 另一方面,使用

GLint linkResult = GL_TRUE;
glGetProgramiv(program,GL_LINK_STATUS,&linkResult);
glGetProgramInfoLog(program,1024,&logLen,szLog);

可以获得链接程序时候的错误信息。

实现

可以编写加载函数如下:

GLuint CompileShader(GLenum shaderType,const char* shaderPath)
{
    GLuint shader = glCreateShader(shaderType);
    if(shader == 0)
    {
        printf("create shader fail: %s/n",shaderPath);
        glDeleteShader(shader);
        return 0;
    }
    const char* shaderCode = LoadFileContent(shaderPath);
    if(shaderCode == nullptr)
    {
        printf("load shader code from %s fail/n",shaderPath);
        glDeleteShader(shader);
        return 0;
    }

    glShaderSource(shader,1,&shaderCode,nullptr);
    glCompileShader(shader);

    GLint compileResult = GL_TRUE;
    glGetShaderiv(shader,GL_COMPILE_STATUS,&compileResult);
    if(compileResult == GL_FALSE){
        char szLog[1024] = {0};
        GLsizei logLen = 0;
        glGetShaderInfoLog(shader,1024,&logLen,szLog);
        printf("Compile Shader fail error log: %s /nshader code:/n%s/n",szLog,shaderCode);
        glDeleteShader(shader);
        shader = 0;
    }
    delete shaderCode;
    return shader;
}

最终加载使用下面函数即可:

GLuint CreateGPUProgram(const char* vsShaderPath,const char* fsShaderPath)
{
    GLuint vsShader = CompileShader(GL_VERTEX_SHADER,vsShaderPath);
    GLuint fsShader = CompileShader(GL_FRAGMENT_SHADER,fsShaderPath);

    //Attach
    GLuint program = glCreateProgram();
    glAttachShader(program,vsShader);
    glAttachShader(program,fsShader);

    //Link
    glLinkProgram(program);

    //Clear
    glDetachShader(program,vsShader);
    glDetachShader(program,fsShader);
    glDeleteShader(vsShader);
    glDeleteShader(fsShader);

    //check error
    GLint linkResult = GL_TRUE;
    glGetProgramiv(program,GL_LINK_STATUS,&linkResult);
    if(linkResult == GL_FALSE){
        char szLog[1024] = {0};
        GLsizei logLen = 0;
        glGetProgramInfoLog(program,1024,&logLen,szLog);
        printf("Link program fail error log: %s /nvs shader code:/n%s/nfs shader code:/n%s/n",szLog,vsShaderPath,fsShaderPath);
        glDeleteShader(program);
        program = 0;
    }

    return program;
}

总结

通过glGetShaderInfoLogglGetProgramInfoLog两个函数,我们可以获取到程序运行过程中产生的 shader错误 。另一方面,也可以通过这两个接口为其他功能编写函数。

OpenGL(四)Shader错误 检测

关注我的微信公众号,获取更多优质内容

分类: 未分类 标签:

[置顶] 房卡麻将分析系列之"千里传音"

2017年3月16日 没有评论

 ”房卡“麻将研发技巧,尽在”红孩儿的游戏开发之路“,欢迎关注公众号!



               房卡麻将分析系列之"千里传音"

                   在房卡棋牌游戏中,因为要频繁的看牌,出牌。为了实时沟通打字聊天往往比较麻烦,通过语音交流,催牌可以很好的帮助玩家及时的表达情绪,增强游戏的气氛。

                                       

[置顶]        房卡麻将分析系列之&quot;千里传音&quot;


           那么这是怎么做到的呢?

           

           首先这个过程分为三步: 

           

           一。录制声音并压缩成数据包:这个过程一般是当玩家点击按钮,开始录音,松开按钮,停止录音并生成WAV文件,之后通过编码转换压缩为

在这里要根据安卓和苹果两个平台来做区分。


        void startSoundRecord()
	{
		std::string kFileName = utility::toString(time(NULL),".wav");
		s_kRecordFileName = cocos2d::FileUtils::getInstance()->getWritablePath()+kFileName;
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
		JniMethodInfo minfo;  
		bool isHave = JniHelper::getStaticMethodInfo(minfo,JAVA_CLASSNAME, "startSoundRecord", "(Ljava/lang/String;)V");
		if (isHave)  
		{  
			jstring jurl = minfo.env->NewStringUTF(kFileName.c_str());
			minfo.env->CallStaticVoidMethod(minfo.classID, minfo.methodID,jurl); 
			cocos2d::log("JniFun call startSoundRecord over!");

			minfo.env->DeleteLocalRef(minfo.classID);  
		}  
		else
		{
			cocos2d::log("JniFun call startSoundRecord error!");
		}
#endif
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS
		IosHelper::beginRecord(s_kRecordFileName.c_str());
#endif
	}


	const char* stopSoundRecord()
	{
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
		std::string str;
		JniMethodInfo minfo;  
		bool isHave = JniHelper::getStaticMethodInfo(minfo,JAVA_CLASSNAME, "stopSoundRecord", "()Ljava/lang/String;");
		if (isHave)  
		{  
			jstring jFileName = (jstring)minfo.env->CallStaticObjectMethod(minfo.classID, minfo.methodID); 
			const char *newStr = minfo.env->GetStringUTFChars(jFileName, 0);
			str = newStr;
			cocos2d::log("JniFun call stopSoundRecord over :");
			cocos2d::log("%s",str.c_str());
			minfo.env->ReleaseStringUTFChars(jFileName, newStr);
			minfo.env->DeleteLocalRef(minfo.classID); 
		}  
		else
		{
			cocos2d::log("JniFun call stopSoundRecord error!");
		}
		return str.c_str();
#endif
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS
		IosHelper::endRecord();
		return s_kRecordFileName.c_str();
#endif
		return "";
	}



在Native.java中实现录音和结束:


     

    //开始录音
         public static void startSoundRecord( String SoundFileName)
	 {
		 String SoundFilePath= Environment.getExternalStorageDirectory().getAbsolutePath();  

		if (filePath != null)
		{
			File file = new File(filePath);
			if (file!= null && file.exists())
			{
				file.delete();
			}
		 }
		filePath = SoundFilePath+"/"+SoundFileName;
		recorder = new MediaRecorder();
                //从麦克风中录音
		recorder.setAudioSource(MediaRecorder.AudioSource.MIC);  
                //设置编码格式为AMR
		recorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);  
		recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);  
		recorder.setOutputFile(SoundFilePath+"/"+SoundFileName);  
		try {  
			recorder.prepare();//
			recorder.start();//
		} catch (IllegalStateException e) {  
			e.printStackTrace();  
		} catch (IOException e) {  
			e.printStackTrace();  
		}  
	 }
	 
        //结束录音
	 public static String stopSoundRecord()
	 {
		recorder.stop();// 
                recorder.release(); // 
                recorder = null;  
		return filePath;
	 }


另外,要在AndroidMainfest.xml中注意开启录音权限:


 

<uses-permission android:name="android.permission.RECORD_AUDIO" />  


IOS版本处理:需要在mm文件中完成相应函数


 AVAudioRecorder *recorder = NULL;
void IosHelper::beginRecord(const char *_fileName)
{
      if (recorder == nil)
      {
        //设置文件名和录音路径
        NSString *recordFilePath = [NSString stringWithCString:_fileName encoding:NSUTF8StringEncoding];
        
        NSDictionary *recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys:
                                       [NSNumber numberWithFloat: 8000.0],AVSampleRateKey, //采样率
                                       [NSNumber numberWithInt: kAudioFormatLinearPCM],AVFormatIDKey,
                                       [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,//采样位数 默认 16
                                       [NSNumber numberWithInt: 1], AVNumberOfChannelsKey,//通道的数目
                                       nil];
        //初始化录音
        NSError *error = nil;
        recorder = [[ AVAudioRecorder alloc] initWithURL:[NSURL URLWithString:recordFilePath] settings:recordSetting error:&error];
      }
      recorder.meteringEnabled = YES;
      [recorder prepareToRecord];
      //开始录音
      UInt32 sessionCategory = kAudioSessionCategory_PlayAndRecord;
      AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(sessionCategory), &sessionCategory);
    
      // 扬声器播放
      UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
      AudioSessionSetProperty (kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
      [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil];
      [[AVAudioSession sharedInstance] setActive:YES error:nil];
      [recorder record];
}

const char * IosHelper::endRecord()
{
        if (recorder == nil)
        return "";
        if (recorder.isRecording)
        [recorder stop];
	return "";
}

                                           [置顶]        房卡麻将分析系列之&quot;千里传音&quot;
            


          二。发送声音数据到服务器:在结束录制声音并生成文件后,将文件发送出去。


	std::string kFileName = JniFun::stopSoundRecord();
	sendTalkFile(m_pLocal->GetChairID(),kFileName);


        这里就是将文件以数据包形式发送出去,不做详细表述。

        

        三。接收数据并解压,播放: 在接收到消息后,将数据写入文件并播放即可。

          

bool GameBase::RevTalk_File(CMD_GR_C_TableTalk* pNetInfo)
{
	if (pNetInfo->strTalkSize == 0)
	{
		return true;
	}
	static int iIdex = 0;
	iIdex ++;
	std::string kFile = utility::toString(cocos2d::CCFileUtils::sharedFileUtils()->getWritablePath(),"TableTalk",iIdex,".arm");
	FILE *fp = fopen(kFile.c_str(), "wb");

	fseek(fp,0,SEEK_END);
	fseek(fp,0,SEEK_SET);
	fwrite(&pNetInfo->strTalkData,sizeof(unsigned char), pNetInfo->strTalkSize,fp);
	fclose(fp);
	int iAddTime = pNetInfo->strTalkSize/1200+2.0f;
	if (iAddTime > 10)
	{
		iAddTime = 10;
	}
	std::string kDestFile = kFile;
	utility::StringReplace(kDestFile,"arm","wav");
        //这里需要做一个解压转换,将ARM转换成WAV
	ArmFun::ArmToWav(kFile.c_str(),kDestFile.c_str());
        //为了防止游戏音乐干扰,先静音游戏音乐
	SoundFun::Instance().PaseBackMusic();
	SoundFun::Instance().ResumeBackMusic(iAddTime);
	SoundFun::Instance().PaseEffectMusic();
	SoundFun::Instance().ResumeEffectMusic(iAddTime);
        //播放接收到的声音文件
	SoundFun::Instance().playEffectDirect(kDestFile);
        //指定玩家显示播放语音的动画图标
	GamePlayer* pPlayer = getBasePlayerByChairID(pNetInfo->cbChairID);
	if (pPlayer)
	{
		pPlayer->showTalkState(pNetInfo);
	}
        
	return true;
}

          

        最终,房卡棋牌中的语音聊天就完整的实现出来了,当然,这种方式并不完美,如果能开启P2P的实时语音对话就更好了。另外,这套代码中会不断的产生声音文件,这是个问题,小伙伴们可以在发送完声音和播放完声音后删除生成的声音文件,以免造成空间增长的BUG~


 ”房卡“麻将研发技巧,尽在”红孩儿的游戏开发之路“,欢迎关注公众号!

                             [置顶]        房卡麻将分析系列之&quot;千里传音&quot;

分类: 未分类 标签:

OpenGL(三) 加载贴图

2017年3月15日 没有评论

有了模型还需要贴图。 加载贴图 的流程大体分为两部分,首先是图片的解码,其次是使用UV坐标与模型对应。本文主要从底层原理和第三方库两个方面来介绍 加载贴图 。

解码

下面分别介绍硬编码实现和SOIL库两种方式。

硬编码实现

因为加载不同的类型图片偏移值不一样,加载图片之前要确定图片类型。另一方面,对于DXT这种压缩图片,也需要在压缩图的基础上进行采样,而不是将其还原回未压缩的图元。

static unsigned char* DecodeBMPData(unsigned char* imageData,int&width,int& heigh)
{
    //decode bmp
    int pixelDataOffset =*((int*)(imageData+10));
    width = *((int*)(imageData +18));
    heigh = *((int*)(imageData +22));
    unsigned char* pixelData = (imageData+pixelDataOffset);
    for(int i = 0;i<width*heigh*3;i+=3)
    {
        //bgr->rgb
        unsigned char temp = pixelData[i+2];
        pixelData[i+2] = pixelData[i];
        pixelData[i] = temp;
    }
    return pixelData;
}

const unsigned long  FORMATE_DXT1 = 0x31545844l; //DXT1-> 1 T X D

static unsigned char* DecodeDXT1Data(unsigned char* imageData,int&width,int& height,int& pixelSize)
{
    height = *(unsigned long*)(imageData+sizeof(unsigned long)*3);
    width = *(unsigned long*)(imageData+sizeof(unsigned long)*4);
    pixelSize = *(unsigned long*)(imageData+sizeof(unsigned long)*5);
    unsigned long compressFormate;
    compressFormate = *(unsigned long*)(imageData+sizeof(unsigned long)*21);

    switch (compressFormate)
    {
    case FORMATE_DXT1:
        printf("DXT1/n");
        break;
    default:
        break;
    }
    unsigned char* pixelData = new unsigned char[pixelSize];
    memcpy(pixelData,(imageData+sizeof(unsigned long)*32),pixelSize);

    return pixelData;
}

GLuint CreateTextureFromFile(const char* imagePath)
{
    unsigned char* imageData =(unsigned char*) LoadFileContent(imagePath);

    int width = 0;
    int heigh = 0;
    //decode bmp
    unsigned char* pixelData =nullptr;
    int pixelDataSize = 0;
    GLenum srcForamte = GL_RGB;
    if ((*(unsigned short*)imageData) == 0x4D42)
    {
        pixelData = DecodeBMPData(imageData,width,heigh);
    } 
    else if (memcmp(imageData,"DDS ",4)==0)
    {
        pixelData = DecodeDXT1Data(imageData,width,heigh,pixelDataSize);
        srcForamte = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
    }
    
    if (pixelData == nullptr)
    {
        printf("cannot decode %s /n",imagePath);
        delete imageData;
        return 0;
    }

    GLuint texture;
    glGenTextures(1,&texture);
    glBindTexture(GL_TEXTURE_2D,texture);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    if(srcForamte == GL_RGB){
        glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,heigh,0,GL_RGB,GL_UNSIGNED_BYTE,pixelData);
    }
    else if (srcForamte == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT)
    {
        glCompressedTexImage2D(GL_TEXTURE_2D,0,srcForamte,width,heigh,0,pixelDataSize,pixelData);
    }
    glBindBuffer(GL_TEXTURE_2D,0);
    delete imageData;
    return texture;
}

可以注意到,通过glGenTextures生成buffer,通过glTexParameteri设置图片的显示参数,采样参数等。最后通过glTexImage2D生成图元。

第三方库实现

加载贴图 还可以使用SOIL库,SOIL是简易OpenGL图像库(Simple OpenGL Image Library)的缩写,它支持大多数流行的图像格式,使用起来也很简单,你可以从它的主页下载。如果使用SOIL库加载,代码封装如下:

GLuint CreateTextureFromFile(const char* imagePath)
{
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

    int width, height;
    unsigned char* image = SOIL_load_image(imagePath, &width, &height, 0, SOIL_LOAD_RGB);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glGenerateMipmap(GL_TEXTURE_2D);
    SOIL_free_image_data(image);
    glBindTexture(GL_TEXTURE_2D, 0); 

    return texture;
}

使用贴图

通过glActiveTexture可以开启图元位置。

TextureLocation = glGetUniformLocation(s_program,"U_MainTexture");

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,mainTexture);
glUniform1i(TextureLocation,0);

在OpenGL中图元可以直接被指定,而无需glUniform为其赋值,它可以从0到16,分别传入显卡中,默认0对应第一张图元的位置。当然也可以通过使用glUniform1i,给纹理采样器分配一个位置值,这样可以实现在一个片段着色器中设置多个 加载贴图 。

默认情况下GL_TEXTURE0是被激活的。因此在单图元的情况下,可以只写如下代码进行渲染。

glBindTexture(GL_TEXTURE_2D,mainTexture);

shader绘制

在编写shader时,需要在vs中加入texcoord,并传递给fs。fs方面要加入sampler2D来接收纹理。

//vs
attribute vec3 pos;
attribute vec2 texcoord;
attribute vec3 normal;

uniform mat4 M;
uniform mat4 V;
uniform mat4 P;

varying vec2 V_Texcoord;

void main()
{
    V_Texcoord = texcoord;
    gl_Position=P*V*M*vec4(pos,1.0);
}

//fs
uniform sampler2D U_MainTexture;
varying vec2 V_Texcoord;

void main()
{
   gl_FragColor= texture2D(U_MainTexture,V_Texcoord);
}

总结

通过以上代码,可以 加载贴图 并将其绘制出来。建议将其封装成接口或类,因为这部分代码很底层,通常不会更改。

OpenGL(三) 加载贴图

关注我的微信公众号,获取更多优质内容

分类: 未分类 标签: