在 RTC 2020 编程挑战赛春季赛中。我们还有一个获奖团队,思路新颖,开发了一款基于双人视频聊天场景的小游戏——“拿头玩”。在视频聊天过程中即可开启游戏。通过人脸识别算法识别转头方向,实现以“接锅”和“甩锅”为主题的玩法。目前实现了Android版本。

我们邀请了这支团队,分享了他们的开发历程:

项目初心

颈椎问题是困扰所有办公族的难题,大多数人工作中很难有机会能起身动一动,回到家里也会因为疲倦而放弃做一些颈椎康复的运动。所以我们想设计一款游戏,让大家在休息的时候可以通过游戏的形式活动颈椎,舒缓疼痛。我们选择了职场中的“甩锅”和“接锅”的场景,来作为游戏中的元素,希望能增加玩家的代入感。此外,我们还添加了截图分享模块,方便游戏进行传播。

主要功能

经过了5天的设计和开发,我们最终完成了《拿头玩》这个作品,下面来分享一下它的主要功能和其中的代码细节。

  • 视频聊天模块的搭建

    视频聊天模块主要是使用声网的音视频sdk,它可以快速的开发出一个基本的视频对话模块,核心代码如下:

//onCreate
val rtcEngine = RtcEngine.create(this, AppConfig.appKey,object : IRtcEngineEventHandler() {override fun onFirstRemoteVideoDecoded(uid: Int,width: Int,height: Int,elapsed: Int) {setupRemoteVideo(uid)}}
//setup
private fun setupRemoteVideo(uid: Int) {val remoteView = RtcEngine.CreateRendererView(baseContext)remoteView.setZOrderMediaOverlay(true)container.addView(remoteView)rtcEngine.setupRemoteVideo(VideoCanvas(remoteView, VideoCanvas.RENDER_MODE_HIDDEN, uid))
}
  • 视频帧数据的获取和处理

    为了进行下一步的人脸识别,我们需要获取到视频帧数据,对帧数据进行预处理。在阅读声网提供的文档和demo后,我们搭建了一个简单的apm-plugin插件,通过这个插件,就可以得到视频聊天过程中的裸数据了。首先我们创建apm-plugin-packet-processing.cpp文件,然后通过CMakeLists.txt配置编译参数:


cmake_minimum_required(VERSION 3.4.1)add_library(apm-plugin-packet-processingSHAREDapm-plugin-packet-processing.cpp)include_directories(../cpp/include) //这里需要导入sdk中的.h文件
...
target_link_libraries(apm-plugin-packet-processing${log-lib})

然后我们定义两个jni方法来注册和反注册裸数据的回调:

JNIEXPORT void JNICALL Java_com_zero_game_utils_frame_VideoFrameHandler_doRegisterProcessing(JNIEnv *env, jobject obj) {if (!rtcEngine) {return;} else {agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine;mediaEngine.queryInterface(rtcEngine, agora::AGORA_IID_MEDIA_ENGINE);s_packetObserver = *new AgoraVideoFrameObserver(jvm, env, env->NewGlobalRef(obj));mediaEngine->registerVideoFrameObserver(&s_packetObserver);}
}JNIEXPORT void JNICALL Java_com_zero_game_utils_frame_VideoFrameHandler_doUnregisterProcessing(JNIEnv *env, jobject obj) {if (!rtcEngine) {return;} else {agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine;mediaEngine.queryInterface(rtcEngine, agora::AGORA_IID_MEDIA_ENGINE);s_packetObserver.release();mediaEngine->registerVideoFrameObserver(nullptr);}
}

agora::media::IVideoFrameObserver这个接口就是声网sdk提供的视频帧回调,只要实现它即可:

class AgoraVideoFrameObserver : public agora::media::IVideoFrameObserver {
public:AgoraVideoFrameObserver() {}AgoraVideoFrameObserver(JavaVM *vm, JNIEnv *env, jobject jobj) {//...}// 获取本地摄像头采集到的视频帧virtual bool onCaptureVideoFrame(VideoFrame &videoFrame) override {//processVideoFrame(videoFrame)return true;}// 获取远端用户发送的视频帧virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame &videoFrame) override {return true;}// 获取本地视频编码前的视频帧virtual bool onPreEncodeVideoFrame(VideoFrame &videoFrame) override {return true;}void release() {//...}
};

由于Android平台中摄像头返回的裸数据是YUV420编码,所以我们还要转换为提供给人脸识别模块的rgba格式才行,最后通过jni方法将数据传递到java层,进行后续的处理:

int width = videoFrame.width;
int height = videoFrame.height;
int index = 0;
char *rgba = new char[width * height * 4];
unsigned char *ybase = static_cast<unsigned char *>(videoFrame.yBuffer);
unsigned char *ubase = static_cast<unsigned char *>(videoFrame.uBuffer);;
unsigned char *vbase = static_cast<unsigned char *>(videoFrame.vBuffer);;
for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {//YYYYYYYYUUVVu_char Y = ybase[x + y * width];u_char U = ubase[y / 2 * width / 2 + (x / 2)];u_char V = vbase[y / 2 * width / 2 + (x / 2)];int r = static_cast<int>(Y + 1.402 * (V - 128));if (r > 255) { r = 255; } if (r < 0) { r = 0; }int g = static_cast<int>(Y - 0.34413 * (U - 128) - 0.71414 * (V - 128));if (g > 255) { g = 255;} if (g < 0) { g = 0; }int b = static_cast<int>(Y + 1.772 * (U - 128));if (b > 255) { b = 255; } if (b < 0) { b = 0; }rgba[index++] = static_cast<char>(r); //Rrgba[index++] = static_cast<char>(g); //Grgba[index++] = static_cast<char>(b); //Brgba[index++] = static_cast<char>(255);}
}jbyte buf[width * height * 4];
int i = 0;
for (i = 0; i < width * height * 4; i++) {buf[i] = rgba[i];
}jbyteArray jarrRV = env->NewByteArray(width * height * 4);
env->SetByteArrayRegion(jarrRV, 0, width * height * 4, buf);
env->CallVoidMethod(jobj, jSendMethodId, jarrRV, width, height, videoFrame.rotation);
env->DeleteLocalRef(jarrRV);
  • 人脸识别和方向检测

    人脸识别主要使用的是MLKit,通过Firebase即可简单配置使用,在上一个环节中,我们把源数据通过jni传到了java层,现在我们需要将它转化成bitmap对象然后传给MLKit中提供的VisionFaceDetector。

val bitmap = Bitmap.createBitmap(color,width,height,Bitmap.Config.ARGB_8888)
//裸数据还需要进行旋转和水平翻转
val matrix = Matrix()
matrix.postRotate(rotation.toFloat())
matrix.postScale(-1.0f, 1.0f)
val rotationBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true)
val image = FirebaseVisionImage.fromBitmap(rotationBitmap)
val detect = FirebaseVision.getInstance().getVisionFaceDetector(highAccuracyOpts)
detect.detectInImage(image).addOnSuccessListener {val leftEye = face.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE)val rightEye = face.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE)val nose = face.getLandmark(FirebaseVisionFaceLandmark.NOSE_BASE)//获取到左眼、右眼和鼻子的位置val leftEyeNose = euclidean(leftEye,nose)//计算鼻子到左眼的距离val rightEyeNode = euclidean(rightEye,nose)//计算鼻子到右眼的距离val ratio = min(leftEyeNose,rightEyeNose) / max(leftEyeNose,rightEyeNose)if (ratio > 0.7 && ratio < 1) {//左右眼离鼻子的比例在0.7-1.0之间我们认为没有转头FaceState.FRONT} else {if (rightHalfFace > leftHalfFace) {//右边眼睛到鼻子距离大于左边的,我们认为转向了左边FaceState.LEFT} else {//反之右边FaceState.RIGHT}}}

实现了转头识别后,配合上UI和动画,我们就可以使游戏中的人偶跟随我们的转头方向运动了。

  • 游戏流程控制

    由于游戏是在两端同时进行的,所以我们需要进行端对端的数据传递,我们采用的是声网提供的消息传输方案。通过实时传递游戏过程中的指令,对双方游戏画面进行控制,传递的指令包括:游戏开始,游戏结束,向左转头,向右转头,没有转头以及实时分数等。

//发送方
streamId = rtcEngine.createDataStream(true, true)
rtcEngine.sendStreamMessage(streamId, "left".toByteArray())//接收方 object : IRtcEngineEventHandler
override fun onStreamMessage(uid: Int, s: Int, data: ByteArray?) {data?.let {val string = String(it)when (string) {"left" -> {//处理游戏}"right"->{//处理游戏}.....}
}

《拿头玩》这个项目是一个起点,基于它的框架,其实可以快速地添加到各种app中,形成一个额外的小游戏模块。将“接锅”“甩锅”的替换成“接优惠券”、“采集素材”等不同元素,可以扩展它的使用场景。通过提供更多有趣的包装,可以有效实现社交裂变引流。

项目 Github地址 :http://dwz.date/btxB

开发者实践:做一个双人视频社交小游戏,“甩头”才能玩相关推荐

  1. 用pygame做一个简单的python小游戏---贪吃蛇

    用pygame做一个简单的python小游戏-贪吃蛇 贪吃蛇游戏博客链接:(方法一样,语言不一样) c++贪吃蛇:https://blog.csdn.net/weixin_46791942/artic ...

  2. 用pygame做一个简单的python小游戏---七彩同心圆

    用pygame做一个简单的python小游戏-七彩同心圆 这个小游戏原是我同学python课的课后作业,并不是很难,就简单实现了一下,顺便加强一下pygame库的学习. 玩法:每次点击鼠标时,会以鼠标 ...

  3. 用pygame做一个简单的python小游戏---生命游戏

    用pygame做一个简单的python小游戏-生命游戏 生命游戏(Game of Life) 生命游戏(Game of Life)是剑桥大学约翰·何顿·康威(John Horton Conway)教授 ...

  4. python七彩同心圆_用pygame做一个简单的python小游戏---七彩同心圆

    用pygame做一个简单的python小游戏---七彩同心圆 用pygame做一个简单的python小游戏-七彩同心圆 这个小游戏原是我同学python课的课后作业,并不是很难,就简单实现了一下,顺便 ...

  5. 做一个简单的java小游戏--单机版五子棋

    做一个简单的java小游戏–单机版五子棋 学了java有一段时间了,今天就来搞一个简单的单机版五子棋游戏. 实现功能:那必须能进行基础的输赢判断.还有重新开始的功能,悔棋的功能,先手设置的功能和退出的 ...

  6. 手把手教你做一个Java贪吃蛇小游戏

    大家好,我是孙不坚1208,这篇博客给大家分享一下:如何做一个贪吃蛇小游戏(Java版)的exe应用程序,希望能给需要帮助的朋友带来方便. 手把手教你做一个Java贪吃蛇小游戏的exe应用程序 一.J ...

  7. 做一个简单的java小游戏--贪吃蛇

    做一个简单的java小游戏–贪吃蛇 贪吃蛇游戏博客链接:(方法一样,语言不一样) c++贪吃蛇:https://blog.csdn.net/weixin_46791942/article/detail ...

  8. python井字棋_用Python做一个井字棋小游戏

    井字棋是一个经典的小游戏,在九宫格上玩家轮流画OXO,当每列或每行或是两个对角成一线时便是获胜. 今天就用Python编写一个井字棋小游戏,与电脑对战. 程序执行画面如下图所示: 程序提供了两种人工智 ...

  9. python井字棋游戏人机对战_用Python做一个井字棋小游戏

    井字棋是一个经典的小游戏,在九宫格上玩家轮流画OXO,当每列或每行或是两个对角成一线时便是获胜. 今天就用Python编写一个井字棋小游戏,与电脑对战. 程序执行画面如下图所示: 程序提供了两种人工智 ...

最新文章

  1. Go实现简单的K-V存储
  2. python条件赋值
  3. 周末,说声php的settergetter(魔术)方法,你们辛苦了
  4. arcengine,深入理解游标Cursors,实现数据的快速查找,插入,删除,更新
  5. Python+OpenCV4:读写输入和输出的简单实践(图片、视频、摄像头)
  6. VC++中使用内存映射文件处理大文件
  7. PHP数组之间的比较,PHP 数组之间的比较方法:
  8. Android 学习笔记【基础扫盲篇】
  9. ruby设计模式之观察者模式2————更加一般化的观察者模式
  10. 【bug】记一个有趣的“bug”
  11. 共享打印机无法连接的解决办法
  12. 算法复杂度分析中的符号(Θ、Ο、ο、Ω、ω)简介
  13. monkeyrunner之环境搭建及实例(三)
  14. Learning Continuous Image Representation with Local Implicit Image Function解读
  15. 【手把手】ElasticSearch的搜索推荐相关
  16. NUST 2009-8
  17. ClassFactory 无法供应请求的类 (异常来自 HRESULT:0x80040111 (CLASS_E_CLASSNOTAVAILABLE))
  18. 基于JAVA城市道路智能停车管理系统计算机毕业设计源码+系统+lw文档+部署
  19. 浙江大学软件学院人工智能保研面经2021
  20. python音标1003python音标_python selenium 爬取百度翻译单词音标

热门文章

  1. 计算机毕业设计django基于Python在线酒店管理系统
  2. 曾拱手让位新加坡 香港能否重新夺回“国际金融中心”之位?
  3. 报告称国产智能手机全球市场份额33.1% 超过韩国
  4. 驱动程序无法使用安全套接字层(SSL)加密建立到SQL Server的安全连接
  5. 模拟手机通信系统(MFC)
  6. 电路设计实例(纽扣型可再充电锂离子电池充电电路)
  7. PeopleSoft开发:创建页面PAGE
  8. 2022-07-13 第五小组 瞒春 学习笔记
  9. 2020校招面试之满帮
  10. VDI负载测试工具使用分享:Login VSI简介