使用声网&环信 SDK 构建元宇宙应用 MetaTown 的最佳实践|2022 RTE 编程挑战赛

参加了【RTE2022 创新编程挑战赛】,整个大赛历时47天,参赛者除了对声网的 SDK 有了很多的体验,还做了很多奇思妙想的结合。在此次大赛中我们团队基于声网&环信 SDK 构建了一个元宇宙应用 MetaTown,获得了环信专项奖,明年努力拿他个一等奖吧。

下面我会把这个参赛作品的实现分享给大家,欢迎一起交流~


关于MetaTown


金钱是被铸造出来的自由——陀思妥耶夫斯基

在三次元的现实世界,你是否为了搞钱而终日奔波?忍受996甚至007的非人待遇?是否正经历着创业人的凛冬?疫情等因素带来的本轮经济下行落实在每个人身上都是真真切切的,对于经历了40年经济暴增的国人来讲更是史无前例的。

在荷包日瘪萎靡不振的日子里,精神慰藉尤为重要,元宇宙正是当下最时髦的,何不创造一个可以躺着赚钱的元宇宙小镇?不为别的,在有虚拟工作的前提下每天我的虚拟角色的金币都会涨,想一想岂不是有一点小欢愉?还能在这个小镇结交一些志同道合沉迷于搞钱的友友们!

自然这些虚拟财富目前还是无法转化为真正的成就感的(变为现实财富),但现在市面上开始有人吹web3.0了!坐等币圈大佬入局,我们有信心打造一款能让一部分人先富起来的元宇宙小镇!


————————正经的分割线————————


MetaTown 是基于声网 RTC 和环信 IM 打造的模拟城市生活的社交类 App。

初来乍到的玩家首次进入 App 先选择不同的职业,在这座城市首先要考虑的是如何赚钱,或当程序员上下班打卡,或自己创业或去银行投资。还要注意身体健康,可能随机生病去医院,挂号咨询不同科室的医生。

这个小镇上的所有场所,均可以随时发起与陌生人私聊,共同兴趣结缘,独自体验在城市闯荡的日子!

项目 GitHub 地址:https://github.com/AgoraIO-Community/RTE-2022-Innovation-Challenge/tree/main/Application-Challenge/%E9%A1%B9%E7%9B%AE243-metatown-metatown


MetaTown 核心技术

MetaTown 使用当下最流行的声网实时音视频以及环信即时通讯 SDK,具体场景如:

  • 医院场景中一对一咨询医生,进行远程实时问诊。
  • 社交场景中与好友实时音视频沟通,聊天。


1、声网音视频

MetaTown 运用了声网的实时音视频功能。

1)集成声网 SDK


1-1)添加声网音视频依赖在 app module 的 build.gradle 文件的 dependencies 代码块中添加如下代码:

implementation 'io.agora.rtc:full-rtc-basic:3.6.2'


然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

ndk {
    abiFilters  "arm64-v8a"
}
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)


1-2)添加必要权限 为了保证 SDK 能正常运行,我们需要在 AndroidManisfest.xml 文件中声明以下权限:

<!--允许程序连接网络-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序录制音频-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--允许程序使用照相设备-->
<uses-permission android:name="android.permission.CAMERA" />
<!--允许程序修改全局音频设置-->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!--允许程序获取网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许对存储空间进行读写-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--允许程序连接到已配对的蓝牙设备-->
<uses-permission android:name="android.permission.BLUETOOTH" />


1-3)APP 在签名打包时防止出现混淆的问题需要在 proguard-rules.pro 文件里添加以下代码:

-keep class io.agora.**{*;}


2)创建并初始化 RtcEngine

)创建并初始化 RtcEngine
private void initializeEngine() {
    try {
        EaseCallKitConfig config =  EaseCallKit.getInstance().getCallKitConfig();
        if(config != null){
            agoraAppId = config.getAgoraAppId();
        }
        mRtcEngine = RtcEngine.create(getBaseContext(), agoraAppId, mRtcEventHandler);

        //因为有小程序 设置为直播模式 角色设置为主播
        mRtcEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING);
        mRtcEngine.setClientRole(CLIENT_ROLE_BROADCASTER);

        EaseCallFloatWindow.getInstance().setRtcEngine(getApplicationContext(), mRtcEngine);
        //设置小窗口悬浮类型
        EaseCallFloatWindow.getInstance().setCallType(EaseCallType.CONFERENCE_CALL);
    } catch (Exception e) {
        EMLog.e(TAG, Log.getStackTraceString(e));
        throw new RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e));
    }
}


3)设置视频模式

private void setupVideoConfig() {
    mRtcEngine.enableVideo();
    mRtcEngine.muteLocalVideoStream(true);
    mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(
                VideoEncoderConfiguration.VD_1280x720,
                VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
                VideoEncoderConfiguration.STANDARD_BITRATE,
                VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT));

    //启动谁在说话检测
    int res = mRtcEngine.enableAudioVolumeIndication(500,3,false);
}


4)设置本地视频显示属性4-1)setupLocalVideo( VideoCanvas local ) 方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。

private void setupLocalVideo() {
    if(isFloatWindowShowing()) {
        return;
    }
    localMemberView = createCallMemberView();
    UserInfo info = new UserInfo();
    info.userAccount = EMClient.getInstance().getCurrentUser();
    info.uid = 0;
    localMemberView.setUserInfo(info);
    localMemberView.setVideoOff(true);
    localMemberView.setCameraDirectionFront(isCameraFront);
    callConferenceViewGroup.addView(localMemberView);
    setUserJoinChannelInfo(EMClient.getInstance().getCurrentUser(),0);
    mUidsList.put(0, localMemberView);
    mRtcEngine.setupLocalVideo(new VideoCanvas(localMemberView.getSurfaceView(), VideoCanvas.RENDER_MODE_HIDDEN, 0));
}


4-2)joinChannel(String token,String channelName,String optionalInfo,int optionalUid ) 方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。

private void joinChannel() {
    EaseCallKitConfig callKitConfig = EaseCallKit.getInstance().getCallKitConfig();
    if(listener != null && callKitConfig != null && callKitConfig.isEnableRTCToken()){
        listener.onGenerateToken(EMClient.getInstance().getCurrentUser(),channelName,  EMClient.getInstance().getOptions().getAppKey(), new EaseCallKitTokenCallback(){
            @Override
            public void onSetToken(String token,int uId) {
                EMLog.d(TAG,"onSetToken token:" + token + " uid: " +uId);
                //获取到Token uid加入频道
                mRtcEngine.joinChannel(token, channelName,null,uId);
                //自己信息加入uIdMap
                uIdMap.put(uId,new EaseUserAccount(uId,EMClient.getInstance().getCurrentUser()));
            }

            @Override
            public void onGetTokenError(int error, String errorMsg) {
                EMLog.e(TAG,"onGenerateToken error :" + error + " errorMsg:" + errorMsg);
                //获取Token失败,退出呼叫
                exitChannel();
            }
        });
    }
}

完成以上配置后就可以发起呼叫了,其它一些摄像头控制,声音控制可以参考声网官网的API,这里不再赘述。


2、环信即时通讯

MetaTown 在6大交互场景中运用了环信的即时通讯 IM(Instant Messaging),给 IM 赋予了新的场景活力,支持陌生人私聊,群聊及超大型聊天室。


2-1)会话列表 项目中 IM 会话列表如下图:


  • 会话列表关键代码:
public void show(BaseActivity activity) {
    NiceDialog.init().setLayoutId(R.layout.dialog_message)
            .setConvertListener(new ViewConvertListener() {
                @Override
                protected void convertView(ViewHolder holder, BaseNiceDialog dialog) {
                    RecyclerView rv = holder.getView(R.id.rv);
                    List<EaseConversationInfo> easeConversationInfos = initData();
                    rv.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
                    DialogMsgAdapter dialogMsgAdapter = new DialogMsgAdapter(easeConversationInfos);
                    rv.setAdapter(dialogMsgAdapter);
                    dialogMsgAdapter.setOnItemClickListener(new DialogMsgAdapter.OnItemClickListener() {
                        @Override
                        public void onItemClick(int pos,String name) {
                            SoundUtil.getInstance().playBtnSound();
                            dialog.dismissAllowingStateLoss();
                            EMConversation item = (EMConversation) easeConversationInfos.get(pos).getInfo();
                            ChatDialog.getInstance().show(activity,item.conversationId(), name);
                        }
                    });
                }
            })
            .setAnimStyle(R.style.EndAnimation)
            .setOutCancel(true)
            .setShowEnd(true)
            .show(activity.getSupportFragmentManager());
}


2-2)IM 聊天

项目中 IM 聊天如下图:


  • 发送消息关键代码:
@Override
public void sendMessage(EMMessage message) {
    if(message == null) {
        if(isActive()) {
            runOnUI(() -> mView.sendMessageFail("message is null!"));
        }
        return;
    }
    addMessageAttributes(message);
    if (chatType == EaseConstant.CHATTYPE_GROUP){
        message.setChatType(EMMessage.ChatType.GroupChat);
    }else if(chatType == EaseConstant.CHATTYPE_CHATROOM){
        message.setChatType(EMMessage.ChatType.ChatRoom);
    }
    ...
    EMClient.getInstance().chatManager().sendMessage(message);
    if(isActive()) {
        runOnUI(()-> mView.sendMessageFinish(message));
    }
}


  • 接受消息关键代码:
 public void onMessageReceived(List<EMMessage> messages) {
        super.onMessageReceived(messages);
        LiveDataBus.get().with(Constants.RECEIVE_MSG, LiveEvent.class).postValue(new LiveEvent());
         for (EMMessage message : messages) {
            // in background, do not refresh UI, notify it in notification bar
            if(!MetaTownApp.getInstance().getLifecycleCallbacks().isFront()){
                getNotifier().notify(message);
            }
            //notify new message
            getNotifier().vibrateAndPlayTone(message);
        }
    }

3、场景原画


1)人物行走可分为踏步、水平移动两种动作,分别通过踏步动画和控制人物及背景 scrollview 移动实现。

2)关键点在于人物向左走过半屏继续向左行走,或向右走过半屏继续向右走的情况,以向右走为例,如果人物未超过屏幕中线,则控制人物向右移动;如果超出屏幕中线继续向右移动,则将人物固定在中线位置,背景向左滑动;如果背景向左已滑动至尽头,则保持背景不动,人物继续向右移动;如果人物移动至右边缘,则只控制人物原地踏步,背景和人物均不水平移动。


  • 动画关键代码:
if (isToRight) {
    ivPerson.setRotationY(180f);
}
isToRight = false;

RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();

if (sv != null) {
    if (layoutParams.leftMargin > DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(-1)) {
        layoutParams.leftMargin -= STEP;
        layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
        ivPerson.setLayoutParams(layoutParams);
    } else {
        sv.smoothScrollBy(-STEP, 0);
    }
} else {
    layoutParams.leftMargin -= STEP;
    layoutParams.leftMargin = Math.max(layoutParams.leftMargin, 50);
    ivPerson.setLayoutParams(layoutParams);
}

mAnimationDrawable.start();

if (!isToRight) {
    ivPerson.setRotationY(0f);
}
isToRight = true;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivPerson.getLayoutParams();
if (sv != null) {
    if (layoutParams.leftMargin < DisplayUtil.getHeight(MetaTownApp.getApplication()) || !sv.canScrollHorizontally(1)) {
        layoutParams.leftMargin += STEP;
        layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
        ivPerson.setLayoutParams(layoutParams);
    } else {
        sv.smoothScrollBy(STEP, 0);
    }
} else {
    layoutParams.leftMargin += STEP;
    layoutParams.leftMargin = Math.min(layoutParams.leftMargin, DisplayUtil.getWidth(MetaTownApp.getApplication()) - 100);
    ivPerson.setLayoutParams(layoutParams);
}

mAnimationDrawable.start();


项目开发过程中遇到了一些小问题,不过都通过查阅API以及相关资料顺利解决。


再说说后续计划

1)把现有的几个场景补齐(II期)

2)开发新场景,丰富搞钱路数

3)等一位币圈大佬掉到碗里。


以上是 MetaTown 作品在 RTE2022 编程挑战赛期间的实践分享,更多信息和作品可以访问官方渠道。

(完)



注册登录 后评论
    // 作者
    华丽的尾巴骨 发布于 声网开发者社区
    • 0
    // 本帖子
    // 相关帖子
    Coming soon...
    • 0
    使用声网&环信 SDK 构建元宇宙应用 MetaTown 的最佳实践|2022 RTE 编程挑战赛华丽的尾巴骨 发布于 声网开发者社区