[关闭]
@wsd1 2017-11-12T11:20:15.000000Z 字数 10008 阅读 1259

ucast前端设计:Android开发交互说明

ucast android 201708

架构角色(role)

uCast全景架构图_201705.png-67.6kB

uControl:是otu和实时引擎的整合;
uLogic:在uControl之上提供应用逻辑的角色;
Edge:是管理具体物联网应用的逻辑角色;
uLogicManager:负责收敛uLogic数据的cache层,微信登录,用户信息;
player\viewer: 玩家或观众

Changelog

20170903

1、场景事件添加 “IDLE” 用于表示 当前机器转变空闲。
2、enter RPC 返回值中 添加 driver字段,用于表示当前游戏正在进行,并且带有司机信息。
3、事件处理流程 补充处理数组代码

20170904

1、补充“操作状态事件推动流程”章节,描述游戏过程。
2、将webview地址添加上了 html后缀。

20170908

1、Enter RPC调用返回值中的driver字段做了调整
2、场景事件 添加了 "ERROR" 事件,用于提示用户出现问题。

名称关键词

app名称:互动抓娃娃
codeName:appClaw
android包名:cc.ucast.claw

主要数据结构

glbAccount 对象,存储用户相关信息

glbRoundInfo 对象,存储游戏信息

主要界面及其内部逻辑说明

闪屏

GET portal/appclaw/cover.json

{img: "http://ucast.oss-cn-beijing.aliyuncs.com/covers/appclaw_splash_1726.png", hold: 2} 两字段分别表示图片地址和持续时间(秒)。

登录

登录过程参见微信开放平台登录文档:
https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&id=open1419317851

基本逻辑

1、APP带上“appid”“scope”和“state”调用SDK请求微信开放平台。

用户点击允许后,微信返回:

ErrCode 错误码 0为同意
code 用户换取access_token的code,仅在ErrCode为0时有效
state 第三方程序发送时用来标识其请求的唯一性的标志,不能超过1K
lang 微信客户端当前语言
country 微信用户当前国家信息

2、APP将code交给ucast后台,后台处理后会返回账户信息。

具体是GET 路径:https://ucast.cc/portal/wxopen_claw?code=xxxx
(20170902:该接口Ready)
可以得到json串,类如:

  1. {
  2. nickname:"",
  3. avatar:"http://xxxx.jpg",
  4. userID: "xccdsdsd",
  5. jwt:"Long string about 300bytes"
  6. }

建议赋值给 glbAccount

列表页

基本逻辑

账户检查和更新

若之前登录过,本地缓存了glbAccount,可以越过微信登录,访问https://ucast.cc/api/v1/me(20170902:该接口Ready),验证令牌,并更新之。
若没有glbAccount,或者验证时失败,就要重新微信登录了。

使用js示例逻辑:

  1. //若没有发现账户信息
  2. if(!glbAccount || !glbAccount.jwt){
  3. //微信登录逻辑
  4. }
  5. //更新账户信息,
  6. $.ajax({
  7. type: 'GET',
  8. url: "https://ucast.cc/api/v1/me",
  9. dataType: 'json',
  10. beforeSend: function(xhr) {
  11. xhr.setRequestHeader('Authorization', "bearer " + glbAccount.jwt);
  12. }
  13. })
  14. .done((dat) => {
  15. console.log(dat);
  16. /* 结果类如:
  17. {
  18. nickname: "ss",
  19. avatar: "http://sss.jpg",
  20. userID: "xxaa",
  21. jwt: "a.b.c" //更新的令牌
  22. }
  23. */
  24. glbAccount.jwt = dat.jwt; //这里表示更新令牌
  25. })
  26. .fail(function(jqXHR, textStatus) {
  27. //若失败,可能是过期,请重新进行微信登录流程
  28. ...
  29. });
Webview 列表界面获取

列表页采用webview,web页面请求需要添加账户令牌,示例代码:

  1. ...
  2. webView.loadUrl("https://ucast.cc/portal/appclaw/index.html#/list?jwt="+glbAccount.jwt);

其内部依赖后台接口:

GET  /api/v1/appclaw/games

用户点击其中之一项目,会调用native代码,提供数据,示例如下:

  1. //call native method
  2. window.WebViewJavascriptBridge.callHandler(
  3. 'onListItemSelect'
  4. , item.data
  5. , (responseData)=>{console.log(responseData);}
  6. );
  7. /* item.data 数据结构如下:
  8. {
  9. //场景侧(现场)信息
  10. scene:{
  11. sceneDID: 123,
  12. state: "busy"/"ready"/"sleep",
  13. ctrlUrl: "wss://xxx",
  14. uLogicName: "clawSimple"
  15. },
  16. //游戏信息
  17. profile:{
  18. profileName: "claw_basic_profile",
  19. images:["http://img14.360buyimg.com/n1/g16/M00/01/13/rBEbRlNrQy4IAAAAAAMSAcUO_u8AAAUEwPB7PQAAxIZ643.jpg"],
  20. title: "自带乾坤袋哆啦A梦",
  21. fee: 30
  22. }
  23. }
  24. */

观众界面

观众界面下方是游戏详情,其入口地址为:

https://ucast.cc/portal/appclaw/index.html#/detail?sdid={item.data.scene.sceneDID}&jwt={glbAccount.jwt}

进入此界面 主要流程

Created with Raphaël 2.1.2APPAPPuLogicuLogicbus().login;1、subscribe "场景事件"2、subscribe "个人场景事件"3、subscribe "弹幕事件"4、makeRPC "enter"{streams, viewer, driver}播放视频、渲染观众;获取用户账户信息;场景事件"VIEWER"渲染观众;场景个人事件"COINS"更新金币数量,渲染UI;场景事件"IDLE"使能“参与按钮”;场景事件"START"禁止“参与按钮”;弹幕事件渲染弹幕;

1、登录总线:

  1. ds = deepstream(selectedItem.ctrlUrl).login({
  2. username: glbAccount.userID,
  3. password: '',
  4. token: glbAccount.jwt
  5. });

2、关注场景的事件(后文统称“场景事件”):

说明:这是本场景公共信息发布通道,比如,观众数量变化了、当前有人开始(退出)游戏之类的

  1. ds.event.subscribe(`event/uLogic/${selectedItem.scene.uLogicName}/${selectedItem.scene.sceneDID}`,(evts)=>{
  2. //得到的是数组
  3. for(var i = 0; i < evts.length; i++){
  4. let evt = evts[i];
  5. switch(evt.event){
  6. case "VIEWER": //客户端更新观众状态
  7. /*
  8. evt.value类如:
  9. {
  10. count: 12
  11. faces:[
  12. "http://xxx.jpg",
  13. "http://xxx.jpg",
  14. "http://xxx.jpg"
  15. ]
  16. }
  17. */
  18. //更新UI
  19. break;
  20. case "START": //有人开始新的一局
  21. /*
  22. evt.value类如:
  23. {
  24. driverNicks: ["nick1", ...],
  25. driverAvatars: ["avatar1.jpg", ...]
  26. }
  27. */
  28. //此处可以disable 参与按钮,并显示“稍候”。
  29. break;
  30. case "IDLE": //机器空闲
  31. /*
  32. evt.value {} 不关注
  33. */
  34. //此处可以enable 参与按钮,并显示“参加”。
  35. break;
  36. case "ERROR": //检测到故障
  37. /*
  38. evt.value类如:
  39. {
  40. msg: "当前机器可能出现问题,请退出房间,稍候再进来",
  41. }
  42. */
  43. //此处可以toast msg。
  44. break;
  45. }
  46. }
  47. ...
  48. })

3、关注场景发给自己的事件(后文统称“场景个人事件”):

说明,本通道主要是用来为用户参与游戏服务,用户可以得到类如:COINS、PLAY、CATCH、RESULT 这些事件,其作用见下面相关专题。

  1. ds.event.subscribe("event/uLogic/${selectedItem.scene.uLogicName}/${selectedItem.scene.sceneDID}/${glbAccount.userID}",(evts)=>{
  2. //得到的是数组
  3. for(var i = 0; i < evts.length; i++){
  4. let evt = evts[i];
  5. switch(evt.event){
  6. case "COINS": //客户端更新本地金币数量
  7. glbAccount.coins = evt.value;
  8. //更新UI
  9. break;
  10. case "PLAY": //客户端得到确认消息,机器已经ready
  11. glbRoundInfo.startTs = utc_now();
  12. glbRoundInfo.endTs = glbRoundInfo.startTs + evt.value.gameTime;
  13. //安排下抓定时器,倒计时
  14. //播放"ready,go"的背景声
  15. break;
  16. case "CATCH": //机器侧传回下抓事件
  17. //倒计时停止
  18. //操控界面禁止
  19. break;
  20. case "SUCCESS": //机器传回结果
  21. //弹出成功对话框
  22. //evt.value.reserveTime 指定保留期时间
  23. break;
  24. case "FAIL": //机器传回结果
  25. //弹出失败对话框
  26. //evt.value.reserveTime 指定保留期时间
  27. break;
  28. }
  29. }
  30. ...
  31. })
  32. /*
  33. 事件数据类如:
  34. [ { event:"evtName",
  35. value:{...} //根据具体情况确定
  36. },
  37. {},
  38. ...
  39. ]
  40. */

4、关注场景弹幕事件(统称“弹幕事件”):

弹幕事件就是用来得到当前场景弹幕的,很简单。

  1. ds.event.subscribe(`event/uLogic/danmaku/${selectedItem.scene.sceneDID}`)
  2. //弹幕事件数据也是数组,类如:
  3. [{ bbb:"吾从未看过如此....",
  4. from:"xxx"
  5. },
  6. { bbb:"啦啦啦😝....",
  7. from:"xxx"
  8. },...
  9. ]

5、调用RPC "enter"

  1. ds.rpc.make(
  2. `rpc/uLogic/${selectedItem.scene.uLogicName}/enter`, {
  3. sceneDID: selectedItem.scene.sceneDID,
  4. userID: glbAccount.userID,
  5. nickname: glbAccount.nickname,
  6. avatar: glbAccount.avatar
  7. }, (err, res) => {
  8. if(!err){
  9. /* 得到res类如:
  10. res: {
  11. //该字段用于播放流,若streams只有1路,可以不用显示切换视角按钮
  12. streams:["rtmp://xxx","rtmp://xxx"],
  13. //该字段初始化渲染观众栏
  14. viewer: {
  15. count: 12
  16. faces:[
  17. "http://xxx.jpg",
  18. "http://xxx.jpg",
  19. "http://xxx.jpg"
  20. ]
  21. },
  22. //该字段初始化渲染当前司机,若无,参加按钮激活
  23. driver: {
  24. drivers: ["name1"],
  25. driverNicks: ["nick1"],
  26. driverAvatars: ["avatar1.jpg"]
  27. }
  28. }
  29. */
  30. }
  31. });

退出此界面 主要流程

1、调用RPC "exit"

  1. ds.rpc.make(
  2. `rpc/uLogic/${selectedItem.scene.uLogicName}/exit`, {
  3. sceneDID: selectedItem.scene.sceneDID,
  4. userID: glbAccount.userID
  5. };

2、取消订阅

  1. //取消订阅弹幕事件
  2. ds.event.unsubscribe(`event/uLogic/danmaku/${selectedItem.scene.sceneDID}`)
  3. //取消订阅本人场景事件
  4. ds.event.subscribe("event/uLogic/${selectedItem.scene.uLogicName}/${selectedItem.scene.sceneDID}/${glbAccount.userID}")
  5. //取消订阅场景事件
  6. ds.event.subscribe(`event/uLogic/${selectedItem.scene.uLogicName}/${selectedItem.scene.sceneDID}`)

界面元素 操作与数据来源

视频流

调用RPC enter,得到数据:

  1. result.streams 类如:
  2. ["rtmp://xxx", "rtmp://xxx"]
观众栏

初始数据来源:调用RPC enter,得到数据;

  1. result.viewer 类如:
  2. {
  3. count: 100
  4. faces:[
  5. "http://xxx.jpg",
  6. "http://xxx.jpg",
  7. "http://xxx.jpg"
  8. ]
  9. }

场景"VIEWER"事件随后也会提供更新该元素的数据。
类如:

  1. [{ event: "VIEWER",
  2. value: {
  3. count: 100
  4. faces:[
  5. "http://xxx.jpg",
  6. "http://xxx.jpg",
  7. "http://xxx.jpg"
  8. ]
  9. }
  10. },
  11. ...
  12. ]
弹幕

接收:
数据来自于之前订阅弹幕路径:
初版,位置随机放置。

发送:

  1. let rpc_path = `rpc/uLogic/danmaku/fire`;
  2. let rpc_data = {
  3. userID: "xxx",
  4. sceneDID: xxx,
  5. nickname: "xxx",
  6. bb:"哈哈哈😆"
  7. }
  8. ds.rpc.make(rpc_path, rpc_data, (err, ret)=>{
  9. if(!err)
  10. console.log("发送成功");
  11. })
金币数量

流程:
1、用户进入观众界面后,会调用 enter RPC
2、后台会根据用户信息去中心服务器获取用户最新金币数据,之后会通过场景个人事件"COINS"发送给APP。
3、APP仅通过该方式获得金币数量,在获取之前为零。
4、获取之后,放在全局对象 glbAccount.coins,并渲染UI

参加按钮

参加按钮 有两种状态,
使能状态,其显示“参与”
禁止状态,其显示“稍候”
流程:
1、用户进入观众界面后,会调用 enter RPC,获得结果内有 driver字段则表示当前忙,建议设置按钮为 禁止状态。若无,则反之。

2、之后,场景事件会有 "START" 和 "IDLE"事件,这两个事件分别禁止和使能该按钮。

3、在使能状态下,如果用户金币不足,则提示请充值,再次点击该按钮进入“我的”界面。若金币富余,则进入操作状态流程。

游戏详情

播放器界面下方是游戏详情,界面使用Webview,加载地址为:

  1. ...
  2. webView.loadUrl("https://ucast.cc/portal/appclaw/detail.html?jwt="+glbAccount.jwt);

其内部依赖后台接口:

GET  /api/v1/appclaw/games/:sceneDID

进入操作状态主要流程

用户点击 "参与"按钮,实施如下伪代码:

  1. //在本地数据比较金币和费用
  2. if(glbAccount.coin > selectedItem.game.fee){
  3. //调用参与RPC
  4. result = ds.rpc.make(
  5. `rpc/uLogic/${selectedItem.scene.uLogicName}/participate`, {
  6. sceneDID: selectedItem.scene.sceneDID,
  7. userID: glbAccount.userID
  8. },
  9. //结果处理回调(暂时比较简单,只有成功和失败区别,细节再细化)
  10. (err, res) => {
  11. if (err) {
  12. //参与失败
  13. return;
  14. }
  15. //切换到控制方向盘和抓取按钮
  16. //切换到驾驶状态
  17. });
  18. }
Created with Raphaël 2.1.2APPAPPuLogicuLogicmakeRPC "participate"看看此货金币是否足够OK

participate方法返回错误格式及其定义

类如:error: {code:xx, message:"xxx"}

participate关键返回错误解释:
error: {code:xx, message:"xxx"}
ReturnCode.PARAM_INVALID 参数不合法
ReturnCode.PARAM_ERROR 错误的参数
ReturnCode.NOT_IN_ROOM 不在房间 所以没法参与
ReturnCode.BUSY 司机在忙,无法参与
ReturnCode.NOT_READY 机器准备或者后台条件不足
ReturnCode.LESS_MONEY 没钱玩毛

错误码定义见:

  1. module.exports = {
  2. OK: 0,
  3. PARAM_INVALID: 1, //参数格式错误,不合调用需求
  4. PARAM_ERROR: 2, //错误参数,无法执行
  5. NOT_IN_ROOM: 3, //当前用户不在房间内(有些接口需要保证用户在房间内)
  6. DRIVER_BUSY: 4, //司机开车中
  7. NOT_READY: 5, //哪里条件不允许
  8. LESS_MONEY: 6, //没钱
  9. };

操作状态事件推动流程

调用participate成功进入操作状态之后,后续过程都会通过 “场景个人事件”推动完成。如下,大致示例:
uLogic为后台游戏逻辑。

Created with Raphaël 2.1.2APPAPPuLogicuLogicmakeRPC "participate"OK场景事件"START"场景个人事件"PLAY"渲染摇杆界面;播放"ready go";通过evt.value.gameTime,开始倒计时;场景个人事件"COINS"更新本地金币;场景个人事件"CATCH"取消本地倒计时;操控界面禁止输入;场景个人事件"SUCCESS"操控界面还原为观众界面;盛大庆祝界面,evt.value.reserveTime保留期场景个人事件"FAIL"操控界面还原为观众界面;低落界面,evt.value.reserveTime保留期场景事件"IDLE" 释放控制权其他用户也能点击参加了

额外说明:
1、APP本地倒计时自然结束处理
若,APP本地倒计时自然结束后,禁止控制界面,等候CATCH事件即可。

2、保留期
后端发出(SUCCESS或FAIL)个人事件时,会携带一个reserveTime,单位为秒。
司机得到游戏结果后,建议结果界面保留reserveTime后自动dismiss。
保留期之后,"再次参与"按钮就没有参与优势了。下面详解

保留期原理说明

APP侧基本约定:
作为观众,收到 START事件,会禁止参与按钮。收到IDLE事件,使能参与按钮。

后端发布游戏结果流程:
1、后端判断当前游戏结果
2、发出个人事件SUCCESS或FAIL给司机
3、等候reserveTime指定时间(保留期)
4、发布场景事件"IDLE"。

可见,对于观众来说,保留期之后才能继续参与游戏。
其实,后台在得知结果之后(保留期之前),就已经准备好被participate。
司机得到结果后(保留期内),会弹出提示图,其中有"再来一局"的按钮,此按钮是司机优先参与的机会(即使背景参与按钮还是禁止状态)。
司机参与之后,后台不再发出IDLE事件。

这就是保留期的原理,它是为司机准备的一个优先参与的时间窗。
建议APP 处理"再来一局"按钮逻辑时,忽略IDLE事件的影响,直接实施Participate流程。

Created with Raphaël 2.1.2APPAPPuLogicuLogicmakeRPC "participate"OK... 过程不论 ...场景个人事件"FAIL"evt.value.reserveTime保留期司机点击"再来一局"makeRPC "participate"OK场景事件"START"场景个人事件"PLAY"注意,场景事件"IDLE"没有了这就是保留期司机优先参与特征

操作界面

操作界面仅仅替换操控按键和抓取按钮。

!
!
!
!
!
!

我的界面

Webview 界面获取

我的页采用webview,web页面请求需要添加账户令牌,示例代码:

  1. ...
  2. webView.loadUrl("https://ucast.cc/portal/appclaw/index.html#/my/home?jwt="+glbAccount.jwt);

支付流程

此处输入图片的描述
相关文档:
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1#

app调用后台预下单

预下单过程:
app侧告诉后台想付多少钱,后台从微信侧取得一个“预支付识别标识”,连带着此次支付账单编号,发回给app;

细节:
1、用户在"我的"->"获取金币" 界面选取支付项目,选中10元(1000分)点击充值;
2、webview内JS调用后台https://ucast.cc/api/v1/appclaw/wxpay?fee=1000 接口
3、ucast后台从微信接口获取预支付识别标识prepayid后,连带签名信息,一同返回给前端,数据结构类如:

  1. /* 类如
  2. {
  3. "wxpay_param": {
  4. "appId": "wx2d8...9ce587", //应用APPID
  5. "partnerId": "149...92", //微信支付商户号
  6. "prepayId": "wx20171...70924263783",// <--预支付识别标识
  7. "packageValue": "Sign=WXPay",
  8. "nonceStr": "xWLXur4...1VFDyd8bKk3aHS",
  9. "timeStamp": "151...488",
  10. "sign": "6BA44262...545A56AD1AA4511"
  11. },
  12. "bill_id": "5a082a0...284f6160", // <-- 商户内订单编号
  13. "fee": 1
  14. }
  15. */

4、通过jsBridge 将该信息交给native代码,后者调用SDK,唤起微信支付界面

  1. //这里是关键代码,将选取的游戏信息提交给native
  2. window.WebViewJavascriptBridge.callHandler(
  3. 'onWechatPayPreOrder',
  4. res.body, // <-- 这个就是 上面描述的数据结构
  5. (responseData) => {
  6. console.log(responseData);
  7. }
  8. );

android调起过程详见:
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5

支付过程在微信中完成,且后台会得到微信支付后台的通知,并加以记录。

支付确认

支付确认的过程需要native支持的内容如下:

可见,额外多出一个bill_id参数。

如此,webpage可以获知最近有支付行为,并作出相应表现。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注