[文章]简单3步,OpenHarmony上跑起ArkUI分布式小游戏

阅读量0
0
5
转自:OpenAtom OpenHarmony

在9月30日更新的 OpenHarmony3.0 LTS 上,标准系统新增支持了方舟开发框架(ArkUI)、分布式组网和 FA 跨设备迁移能力等新特性,因此我们结合了这三种特性使用 ets 开发了一款如下动图所示传***应用。

打开应用在通过邀请用户进行设备认证后,用户须根据提示完成相应操作,然后通过分布式流转实现随机传递***给下一位用户的效果。那么这样一款传***应用如何进行开发呢?

完整的项目结构目录如下:
  1. ├─entry
  2. │  └─src
  3. │      └─main
  4. │          │  config.json // 应用配置
  5. │          │
  6. │          ├─ets
  7. │          │  └─MainAbility
  8. │          │      │  app.ets //ets应用程序主入口
  9. │          │      │
  10. │          │      └─pages
  11. │          │              CommonLog.ets // 日志类
  12. │          │              game.ets // 游戏首页
  13. │          │              RemoteDeviceManager.ets // 设备管理类
  14. │          │
  15. │          └─resources // 静态资源目录
  16. │              ├─base
  17. │              │  ├─element
  18. │              │  │
  19. │              │  ├─graphic
  20. │              │  ├─layout
  21. │              │  ├─media // 存放媒体资源
  22. │              │  │
  23. │              │  └─profile
  24. │              └─rawfile
复制代码

我们可以分为如下 3 步:编写声明式 UI 界面、添加分布式能力和编写游戏逻辑。

一、编写声明式UI界面

1. 新增工程

在 DevEco Studio 中点击 File -> New Project ->Standard Empty Ability->Next,Language 选择 ETS 语言,最后点击 Finish 即创建成功。

图1 新建工程

2. 编写游戏页面

图2 游戏界面效果图

效果图如上可以分为两部分:

  • 顶部状态提示栏
首先在 @Entry 组件入口 build() 中使用 Stack 作为容器,达到图片和文字堆叠的效果;
接着依次写入 Image 包裹的两个 Text 组件;
  1. Stack() {
  2.      Image($r(<span class="hljs-string">"app.media.title"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">120</span>)
  3.      Column() {
  4.         Text(<span class="hljs-keyword">this</span>.duration.toString() + <span class="hljs-string">'ms'</span>).fontColor(Color.White)
  5.         Text(<span class="hljs-keyword">this</span>.touchText).fontColor(Color.White)
  6.      }
  7.   }
复制代码

  • 中间游戏***九宫格区域
使用 Grid 网格容器来编写九宫格区域;
在 GridItem 中 Stack (容器依次添加方块背景图片和***图片;
在 visibility 属性中用 bombIndex 变量值来决定***显示的位置;
通过 onClick 点击事件和 GestureGroup 组合手势加入单击、双击和长按的监听事件;
  1. Stack() {
  2.    Image($r(<span class="hljs-string">"app.media.background"</span>)).objectFit(ImageFit.Contain)
  3.    Grid() {
  4.      ForEach(<span class="hljs-keyword">this</span>.grid, (item) => {
  5.        GridItem() {
  6.          Stack() {
  7.            Image($r(<span class="hljs-string">"app.media.squares"</span>)).objectFit(ImageFit.Contain)
  8.            Image($r(<span class="hljs-string">"app.media.bomb"</span>))
  9.              .width(<span class="hljs-string">'50%'</span>)
  10.              .objectFit(ImageFit.Contain)
  11.              .visibility(<span class="hljs-keyword">this</span>.bombIndex == item ? Visibility.Visible : Visibility.Hidden)
  12.              <span class="hljs-comment">// ***点击事件</span>
  13.              .onClick((event) => {
  14.                <span class="hljs-comment">// 单击</span>
  15.                <span class="hljs-keyword">this</span>.judgeGame(RuleType.click)
  16.              })
  17.              .gesture(
  18.              GestureGroup(GestureMode.Exclusive,
  19.              LongPressGesture({ repeat: <span class="hljs-literal">false</span> })
  20.                .onAction((event: GestureEvent) => {
  21.                  <span class="hljs-comment">// 长按</span>
  22.                  <span class="hljs-keyword">this</span>.judgeGame(RuleType.longPress)
  23.                }),
  24. ***Gesture({ count: <span class="hljs-number">2</span> })
  25.                .onAction(() => {
  26.                  <span class="hljs-comment">// 双击</span>
  27.                  <span class="hljs-keyword">this</span>.judgeGame(RuleType.doubleClick)
  28.                })
  29.              )
  30.          }
  31.        }.forceRebuild(<span class="hljs-literal">false</span>)
  32.      }, item => item)
  33.    }
  34.    .columnsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
  35.    .rowsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
  36.    .columnsGap(<span class="hljs-number">10</span>)
  37.    .rowsGap(<span class="hljs-number">10</span>)
  38.    .width(<span class="hljs-string">'90%'</span>)
  39.    .height(<span class="hljs-string">'75%'</span>)
  40. }.width(<span class="hljs-string">'80%'</span>).height(<span class="hljs-string">'70%'</span>)
复制代码

3. 添加弹窗

  • 创建规则游戏弹窗
  1)通过 @CustomDialog 装饰器来创建自定义弹窗,使用方式可参考
  2)规则弹窗效果如下,弹窗组成由两个 Text 和两个 Image 竖向排列组成,所以我们可以在 build()下使用 Column 容器来包裹,组件代码如下;

图3 游戏规则

  1. @CustomDialog
  2.    struct RuleDialog {
  3.       controller: CustomDialogController
  4.       confirm: () => <span class="hljs-keyword">void</span>
  5.       invite: () => <span class="hljs-keyword">void</span>
  6.       [url=home.php?mod=space&uid=3273389]@consume[/url] deviceList: RemoteDevice[]

  7.       build() {
  8.          Column() {
  9.             Text(<span class="hljs-string">'游戏规则'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
  10.             Text(<span class="hljs-string">'***会随机出现在9个方块内,需要在规定时间内完成指定操作(点击、双击或长按),即可将***传递给下一个人,小心***可是会越来越快的喔!'</span>)
  11.                .fontSize(<span class="hljs-number">24</span>).margin({ bottom: <span class="hljs-number">10</span> })
  12.             Image($r(<span class="hljs-string">"app.media.btn_start"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>).margin(<span class="hljs-number">10</span>)
  13.                .onClick(() => {
  14.                   console.info(TAG + <span class="hljs-string">'Click start game'</span>)
  15.                   <span class="hljs-keyword">if</span> (checkTrustedDevice(<span class="hljs-keyword">this</span>.remoteDeviceModel)) {
  16.                      <span class="hljs-keyword">this</span>.controller.close()
  17.                      <span class="hljs-keyword">this</span>.confirm()
  18.                   }
  19.                })
  20.             Image($r(<span class="hljs-string">"app.media.btn_Invite"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>).margin(<span class="hljs-number">10</span>)
  21.                .onClick(() => {
  22.                   <span class="hljs-keyword">this</span>.invite()
  23.                })
  24.          }.width(<span class="hljs-string">'90%'</span>)
  25.          .margin(<span class="hljs-number">20</span>)
  26.          .backgroundColor(Color.White)
  27.       }
  28.    }
复制代码

3)在 @entry 创建 CustomDialogController 对象并传入弹窗所需参数,后面可通过该对象 open() 和 close() 方法进行打开和关闭弹窗;

  1. @Provide deviceList: RemoteDevice[] = []
  2. private ruleDialog: CustomDialogController = <span class="hljs-keyword">new</span> CustomDialogController({
  3.    builder: RuleDialog({
  4.       invite: () => <span class="hljs-keyword">this</span>.InvitePlayer(),
  5.       confirm: () => <span class="hljs-keyword">this</span>.startGame(),
  6.       deviceList: <span class="hljs-keyword">this</span>.deviceList
  7.    }),
  8.    autoCancel: <span class="hljs-literal">false</span>
  9. })
复制代码

  • 创建游戏失败弹窗,并添加动画效果

图4 游戏失败弹窗动画

1)编写弹窗布局:将游戏失败文本、***图片和再来一局按钮图片放置于 Column 容器中;
2)用变量来控制动画起始和结束的位置:用 Flex 容器包裹***图片,并用 @State 装饰变量 toggle,通过变量来动态修改 [Flex]的direction 属性;

  1. @State toggle: boolean = <span class="hljs-literal">true</span>
  2. private controller: CustomDialogController
  3. @Consume deviceList: RemoteDevice[]
  4. private confirm: () => <span class="hljs-keyword">void</span>
  5. private interval = <span class="hljs-literal">null</span>

  6. build() {
  7.    Column() {
  8.       Text(<span class="hljs-string">'游戏失败'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
  9.       Flex({
  10.          direction: <span class="hljs-keyword">this</span>.toggle ? FlexDirection.Column : FlexDirection.ColumnReverse,
  11.          alignItems: ItemAlign.Center
  12.       })
  13.       {
  14.          Image($r(<span class="hljs-string">"app.media.bomb"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>)
  15.       }.height(<span class="hljs-number">200</span>)

  16.       Image($r(<span class="hljs-string">"app.media.btn_restart"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">120</span>).margin(<span class="hljs-number">10</span>)
  17.          .onClick(() => {
  18.                <span class="hljs-keyword">this</span>.controller.close()
  19.                <span class="hljs-keyword">this</span>.confirm()
  20.          })
  21.    }
  22.    .width(<span class="hljs-string">'80%'</span>)
  23.    .margin(<span class="hljs-number">50</span>)
  24.    .backgroundColor(Color.White)
  25. }
复制代码

3)设置动画效果:使用 animateTo 显式动画接口***位置切换时添加动画,并且设置定时器定时执行动画;

  1. aboutToAppear() {
  2.    <span class="hljs-keyword">this</span>.setBombAnimate()
  3. }

  4. setBombAnimate() {
  5.    <span class="hljs-keyword">let</span> fun = () => {
  6.       <span class="hljs-keyword">this</span>.toggle = !<span class="hljs-keyword">this</span>.toggle;
  7.    }
  8.    <span class="hljs-keyword">this</span>.interval = setInterval(() => {
  9.       animateTo({ duration: <span class="hljs-number">1500</span>, curve: Curve.Sharp }, fun)
  10.    }, <span class="hljs-number">1600</span>)
  11. }
复制代码

二、添加分布式流转

分布式流转需要在同一网络下通过  DeviceManager 组件进行设备间发现和认证,获取到可信设备的 deviceId 调用 FeatureAbility.startAbility(parameter),即可把应用程序流转到另一设备。

原本分布式流转应用流程如下:
  • 创建 DeviceManager 实例;
  • 调用实例的 startDeviceDiscovery(),开始设备发现未信任设备;
  • 设置设备状态监听 on('deviceStateChange',callback),监听设备上下线状态;
  • 设置设备状态监听 on('deviceFound',callback),监听设备发现;
  • 传入未信任设备参数,调用实例 authenticateDevice 方法,对设备进行 PIN 码认证;
  • 若是已信任设备,可通过实例的 getTrustedDeviceListSync() 方法来获取设备信息;
  • 将设备信息中的 deviceId 传入featureAbility.startAbility 方法,实现流转;
  • 流转接收方可通过featureAbility.getWant() 获取到发送方携带的数据;
  • 注销设备发现监听 off('deviceFound');
  • 注销设备状态监听 off('deviceStateChange');

项目中将上面设备管理封装至 RemoteDeviceManager,通过 RemoteDeviceManager 的四个方法来动态维护 deviceList 设备信息列表。

图5 分布式流转

项目实现分布式流转只需如下流程:

1. 创建RemoteDeviceManager实例

1)导入 RemoteDeviceManager

  1. import {RemoteDeviceManager} from <span class="hljs-string">'./RemoteDeviceManager'</span>
复制代码

2)声明 @Provide 装饰的设备列表变量 deviceList,和创建 RemoteDeviceManager 实例。

  1. @Provide deviceList: RemoteDevice[] = []
  2. private remoteDm: RemoteDeviceManager = <span class="hljs-keyword">new</span> RemoteDeviceManager(<span class="hljs-keyword">this</span>.deviceList)
复制代码

2. 刷新设备列表

在生命周期 aboutToAppear 中,调用刷新设备列表和开始发现设备。
aboutToAppear 定义:函数在创建自定义组件的新实例后,在执行其 build 函数之前执行。

  1. aboutToAppear() {
  2.   <span class="hljs-keyword">this</span>.remoteDm.refreshRemoteDeviceList() <span class="hljs-comment">// 刷新设备列表</span>
  3.   <span class="hljs-keyword">this</span>.remoteDm.startDeviceDiscovery() <span class="hljs-comment">// 开始发现设备</span>
  4. }
复制代码

3. 设备认证

  1. invitePlayer(remoteDevice:RemoteDevice) {
  2.   <span class="hljs-keyword">if</span> (remoteDevice.status == RemoteDeviceStatus.ONLINE) {
  3.     prompt.showToast({ message: <span class="hljs-string">"Already invited!"</span> })
  4.     <span class="hljs-keyword">return</span>
  5.   }
  6.   <span class="hljs-keyword">this</span>.remoteDm.authDevice(remoteDevice).then(() => {
  7.     prompt.showToast({ message: <span class="hljs-string">"Invite success! deviceName="</span> + remoteDevice.deviceName })
  8.   }).catch(() => {
  9.     prompt.showToast({ message: <span class="hljs-string">"Invite fail!"</span> })
  10.   })
  11. }
复制代码

4. 跨设备流转

从 deviceList 中获取设备列表在线的设备 Id,通过 featureAbility.startAbility 进行流转。

  1. async startAbilityRandom() {
  2.   <span class="hljs-keyword">let</span> deviceId = <span class="hljs-keyword">this</span>.getRandomDeviceId() <span class="hljs-comment">// 随机获取设备id</span>
  3.   CommonLog.info(<span class="hljs-string">'featureAbility.startAbility deviceId='</span> + deviceId);
  4.   <span class="hljs-keyword">let</span> bundleName = await getBundleName()
  5.   <span class="hljs-keyword">let</span> wantValue = {
  6.     bundleName: bundleName,
  7.     abilityName: <span class="hljs-string">'com.sample.bombgame.MainAbility'</span>,
  8.     deviceId: deviceId,
  9.     parameters: {
  10.       ongoing: <span class="hljs-literal">true</span>,
  11.       transferNumber: <span class="hljs-keyword">this</span>.transferNumber + <span class="hljs-number">1</span>
  12.     }
  13.   };
  14.   featureAbility.startAbility({
  15.     want: wantValue
  16.   }).then((data) => {
  17.     CommonLog.info(<span class="hljs-string">' featureAbility.startAbility finished, '</span> + <span class="hljs-built_in">JSON</span>.stringify(data));
  18.     featureAbility.terminateSelf((error) => {
  19.       CommonLog.info(<span class="hljs-string">'terminateSelf finished, error='</span> + error);
  20.     });
  21.   });
  22. }
复制代码

5. 注销监听

在声明周期 aboutToDisappear 进行注销监听。
aboutToDisappear 定义:函数在自定义组件析构消耗之前执行。

  1. aboutToDisappear() {
  2.   <span class="hljs-keyword">this</span>.remoteDm.stopDeviceDiscovery() <span class="hljs-comment">// 注销监听</span>
  3. }
复制代码

三、编写游戏逻辑

1. 开始游戏

  1. startGame() {
  2.   CommonLog.info(<span class="hljs-string">'startGame'</span>);
  3.   <span class="hljs-keyword">this</span>.randomTouchRule() <span class="hljs-comment">// 随机游戏点击规则</span>
  4.   <span class="hljs-keyword">this</span>.setRandomBomb() <span class="hljs-comment">// 随机生成***位置</span>
  5.   <span class="hljs-keyword">this</span>.stopCountDown() <span class="hljs-comment">// 停止倒计时</span>
  6.   <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.transferNumber < <span class="hljs-number">10</span>) {
  7.     <span class="hljs-keyword">this</span>.duration = <span class="hljs-number">3000</span> - <span class="hljs-keyword">this</span>.transferNumber * <span class="hljs-number">100</span>
  8.   } <span class="hljs-keyword">else</span> {
  9.     <span class="hljs-keyword">this</span>.duration = <span class="hljs-number">2000</span>
  10.   }
  11.   <span class="hljs-keyword">const</span> interval: number = <span class="hljs-number">500</span>
  12.   <span class="hljs-comment">// 开始倒计时</span>
  13.   <span class="hljs-keyword">this</span>.timer = setInterval(() => {
  14.     <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.duration <= interval) {
  15.       <span class="hljs-keyword">this</span>.duration = <span class="hljs-number">0</span>
  16.       clearInterval(<span class="hljs-keyword">this</span>.timer)
  17.       <span class="hljs-keyword">this</span>.timer = <span class="hljs-literal">null</span>
  18.       <span class="hljs-keyword">this</span>.gameFail()
  19.     } <span class="hljs-keyword">else</span> {
  20.       <span class="hljs-keyword">this</span>.duration -= interval
  21.     }
  22.   }, interval)
  23. }
复制代码

2. 判断输赢

编写判断逻辑,用于不同的点击事件中调用。

  1. /**
  2. * 判断游戏输赢
  3. * [url=home.php?mod=space&uid=3142012]@param[/url] operation 点击类型
  4. */
  5. judgeGame(operation:RuleType) {
  6.    this.stopCountDown()
  7.    if (operation != this.ruleText) {
  8.       this.gameFail()
  9.    } else {
  10.       prompt.showToast({ message: "finish" })
  11.       this.bombIndex = -1
  12.       this.startAbilityRandom()
  13.    }
  14. }
复制代码

3. 游戏失败

游戏失败,弹出游戏失败弹框。

  1. gameFail() {
  2.   prompt.showToast({
  3.     message: <span class="hljs-string">'Game Fail'</span>
  4.   })
  5.   CommonLog.info(<span class="hljs-string">'gameFail'</span>);
  6.   <span class="hljs-keyword">this</span>.gameFailDialog.open()
  7. }
复制代码

四、项目下载和导入

项目仓库地址:

1)git下载

  1. git clone https:<span class="hljs-comment">//gitee.com/openharmony-sig/knowledge_demo_temp.git</span>
复制代码

2)项目导入

打开 DevEco Studio,点击 File->Open->下载路径/FA/Entertainment/BombGame

五、约束与限制

1. 设备编译约束


2. 应用编译约束




回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友