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

https://v.qq.com/x/page/v3313zxq86m.html

刷新
打开应用在通过邀请用户进行设备认证后,用户须根据提示完成相应操作,然后通过分布式流转实现随机传递炸弹给下一位用户的效果。

那么这样一款传炸弹应用如何进行开发呢?

完整的项目结构目录如下:

├─entry
│  └─src
│      └─main
│          │  config.json // 应用配置
│          │
│          ├─ets
│          │  └─MainAbility
│          │      │  app.ets //ets应用程序主入口
│          │      │
│          │      └─pages
│          │              CommonLog.ets // 日志类
│          │              game.ets // 游戏首页
│          │              RemoteDeviceManager.ets // 设备管理类
│          │
│          └─resources // 静态资源目录
│              ├─base
│              │  ├─element
│              │  │
│              │  ├─graphic
│              │  ├─layout
│              │  ├─media // 存放媒体资源
│              │  │
│              │  └─profile
│              └─rawfile

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

一、编写声明式UI界面

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

图1 新建工程

  1. 编写游戏页面

图2 游戏界面效果图

效果图如上可以分为两部分:
顶部状态提示栏

1)首先在 @entry 组件入口 build() 中使用 Stack 作为容器,达到图片和文字堆叠的效果;
 2)接着依次写入 Image 包裹的两个 Text 组件;
 Stack() {
    Image($r("app.media.title")).objectFit(ImageFit.Contain).height(120)
    Column() {
       Text(this.duration.toString() + 'ms').fontColor(Color.White)
       Text(this.touchText).fontColor(Color.White)
    }
 }

中间游戏炸弹九宫格区域

1)使用 Grid 网格容器来编写九宫格区域;
2)在 GridItem 中 Stack (容器依次添加方块背景图片和炸弹图片;
3)在 visibility 属性中用 bombIndex 变量值来决定炸弹显示的位置;
4)通过 onClick 点击事件和 GestureGroup 组合手势加入单击、双击和长按的监听事件;

 Stack() {
   Image($r("app.media.background")).objectFit(ImageFit.Contain)
   Grid() {
     ForEach(this.grid, (item) => {
       GridItem() {
         Stack() {
           Image($r("app.media.squares")).objectFit(ImageFit.Contain)
           Image($r("app.media.bomb"))
             .width('50%')
             .objectFit(ImageFit.Contain)
             .visibility(this.bombIndex == item ? Visibility.Visible : Visibility.Hidden)
             // 炸弹点击事件
             .onClick((event) => {
               // 单击
               this.judgeGame(RuleType.click)
             })
             .gesture(
             GestureGroup(GestureMode.Exclusive,
             LongPressGesture({ repeat: false })
               .onAction((event: GestureEvent) => {
                 // 长按
                 this.judgeGame(RuleType.longPress)
               }),
             TapGesture({ count: 2 })
               .onAction(() => {
                 // 双击
                 this.judgeGame(RuleType.doubleClick)
               })
             )
         }
       }.forceRebuild(false)
     }, item => item)
   }
   .columnsTemplate('1fr 1fr 1fr')
   .rowsTemplate('1fr 1fr 1fr')
   .columnsGap(10)
   .rowsGap(10)
   .width('90%')
   .height('75%')
 }.width('80%').height('70%')
  1. 添加弹窗

创建规则游戏弹窗

1)通过 @CustomDialog 装饰器来创建自定义弹窗,使用方式可参考
自定义弹窗文档:
https://gitee.com/openharmony/docs/blob/master/zh-cn/applicat...

2)规则弹窗效果如下,弹窗组成由两个 Text 和两个 Image 竖向排列组成,所以我们可以在 build()下使用 Column 容器来包裹,组件代码如下;

图3 游戏规则

@CustomDialog
struct RuleDialog {
   controller: CustomDialogController
   confirm: () => void
   invite: () => void
   @Consume deviceList: RemoteDevice[]

   build() {
      Column() {
         Text('游戏规则').fontSize(30).margin(20)
         Text('炸弹会随机出现在9个方块内,需要在规定时间内完成指定操作(点击、双击或长按),即可将炸弹传递给下一个人,小心炸弹可是会越来越快的喔!')
            .fontSize(24).margin({ bottom: 10 })
         Image($r("app.media.btn_start")).objectFit(ImageFit.Contain).height(80).margin(10)
            .onClick(() => {
               console.info(TAG + 'Click start game')
               if (checkTrustedDevice(this.remoteDeviceModel)) {
                  this.controller.close()
                  this.confirm()
               }
            })
         Image($r("app.media.btn_Invite")).objectFit(ImageFit.Contain).height(80).margin(10)
            .onClick(() => {
               this.invite()
            })
      }.width('90%')
      .margin(20)
      .backgroundColor(Color.White)
   }
}

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

@Provide deviceList: RemoteDevice[] = []
private ruleDialog: CustomDialogController = new CustomDialogController({
   builder: RuleDialog({
      invite: () => this.InvitePlayer(),
      confirm: () => this.startGame(),
      deviceList: this.deviceList
   }),
   autoCancel: false
})

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

图4 游戏失败弹窗动画

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

@State toggle: boolean = true
private controller: CustomDialogController
@Consume deviceList: RemoteDevice[]
private confirm: () => void
private interval = null

build() {
   Column() {
      Text('游戏失败').fontSize(30).margin(20)
      Flex({
         direction: this.toggle ? FlexDirection.Column : FlexDirection.ColumnReverse,
         alignItems: ItemAlign.Center
      })
      {
         Image($r("app.media.bomb")).objectFit(ImageFit.Contain).height(80)
      }.height(200)

      Image($r("app.media.btn_restart")).objectFit(ImageFit.Contain).height(120).margin(10)
         .onClick(() => {
               this.controller.close()
               this.confirm()
         })
   }
   .width('80%')
   .margin(50)
   .backgroundColor(Color.White)
}

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

aboutToAppear() {
   this.setBombAnimate()
}

setBombAnimate() {
   let fun = () => {
      this.toggle = !this.toggle;
   }
   this.interval = setInterval(() => {
      animateTo({ duration: 1500, curve: Curve.Sharp }, fun)
   }, 1600)
}

二、添加分布式流转

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

原本分布式流转应用流程如下:

2)调用实例的 startDeviceDiscovery(),开始设备发现未信任设备;
3)设置设备状态监听 on('deviceStateChange',callback),监听设备上下线状态;
4)设置设备状态监听 on('deviceFound',callback),监听设备发现;
5)传入未信任设备参数,调用实例 authenticateDevice 方法,对设备进行 PIN 码认证;
6)若是已信任设备,可通过实例的 getTrustedDeviceListSync() 方法来获取设备信息;
7)将设备信息中的 deviceId 传入featureAbility.startAbility 方法,实现流转;
8)流转接收方可通过featureAbility.getWant() 获取到发送方携带的数据;
9)注销设备发现监听 off('deviceFound');
10)注销设备状态监听 off('deviceStateChange');

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

图5 分布式流转

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

  1. 创建RemoteDeviceManager实例

1)导入 RemoteDeviceManager

import {RemoteDeviceManager} from './RemoteDeviceManager'
(左右滑动,查看更多)
2)声明 @Provide 装饰的设备列表变量 deviceList,和创建 RemoteDeviceManager 实例。
@Provide deviceList: RemoteDevice[] = []
private remoteDm: RemoteDeviceManager = new RemoteDeviceManager(this.deviceList)

  1. 刷新设备列表

在生命周期 aboutToAppear 中,调用刷新设备列表和开始发现设备。

aboutToAppear 定义:函数在创建自定义组件的新实例后,在执行其 build 函数之前执行。

aboutToAppear() {
  this.remoteDm.refreshRemoteDeviceList() // 刷新设备列表
  this.remoteDm.startDeviceDiscovery() // 开始发现设备
}
  1. 设备认证
invitePlayer(remoteDevice:RemoteDevice) {
  if (remoteDevice.status == RemoteDeviceStatus.ONLINE) {
    prompt.showToast({ message: "Already invited!" })
    return
  }
  this.remoteDm.authDevice(remoteDevice).then(() => {
    prompt.showToast({ message: "Invite success! deviceName=" + remoteDevice.deviceName })
  }).catch(() => {
    prompt.showToast({ message: "Invite fail!" })
  })
}
  1. 跨设备流转

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

async startAbilityRandom() {
  let deviceId = this.getRandomDeviceId() // 随机获取设备id
  CommonLog.info('featureAbility.startAbility deviceId=' + deviceId);
  let bundleName = await getBundleName()
  let wantValue = {
    bundleName: bundleName,
    abilityName: 'com.sample.bombgame.MainAbility',
    deviceId: deviceId,
    parameters: {
      ongoing: true,
      transferNumber: this.transferNumber + 1
    }
  };
  featureAbility.startAbility({
    want: wantValue
  }).then((data) => {
    CommonLog.info(' featureAbility.startAbility finished, ' + JSON.stringify(data));
    featureAbility.terminateSelf((error) => {
      CommonLog.info('terminateSelf finished, error=' + error);
    });
  });
}
  1. 注销监听

在声明周期 aboutToDisappear 进行注销监听。

aboutToDisappear 定义:函数在自定义组件析构消耗之前执行。

aboutToDisappear() {
this.remoteDm.stopDeviceDiscovery() // 注销监听
}

三、编写游戏逻辑

  1. 开始游戏
startGame() {
  CommonLog.info('startGame');
  this.randomTouchRule() // 随机游戏点击规则
  this.setRandomBomb() // 随机生成炸弹位置
  this.stopCountDown() // 停止倒计时
  if (this.transferNumber < 10) {
    this.duration = 3000 - this.transferNumber * 100
  } else {
    this.duration = 2000
  }
  const interval: number = 500
  // 开始倒计时
  this.timer = setInterval(() => {
    if (this.duration <= interval) {
      this.duration = 0
      clearInterval(this.timer)
      this.timer = null
      this.gameFail()
    } else {
      this.duration -= interval
    }
  }, interval)
}
  1. 判断输赢

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

/**
 * 判断游戏输赢
 * @param operation 点击类型
 */
judgeGame(operation:RuleType) {
   this.stopCountDown()
   if (operation != this.ruleText) {
      this.gameFail()
   } else {
      prompt.showToast({ message: "finish" })
      this.bombIndex = -1
      this.startAbilityRandom()
   }
}
  1. 游戏失败

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

gameFail() {
  prompt.showToast({
    message: 'Game Fail'
  })
  CommonLog.info('gameFail');
  this.gameFailDialog.open()
}

四、项目下载和导入

项目仓库地址:
https://gitee.com/openharmony-sig/knowledge_demo_temp/tree/ma...

1)git下载

git clone https://gitee.com/openharmony-sig/knowledge_demo_temp.git

2)项目导入

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

五、约束与限制

  1. 设备编译约束

获取OpenHarmony源码(OpenHarmony 版本须 3.0 LTS 以上):
https://www.openharmony.cn/pages/0001000202/#%E5%AE%89%E8%A3%...

安装开发板环境:
https://www.openharmony.cn/pages/0001000400/#hi3516%E5%B7%A5%...
开发板烧录:
https://www.openharmony.cn/pages/0001000401/#%E4%BD%BF%E7%94%...

  1. 应用编译约束

参考应用开发快速入门:
https://www.openharmony.cn/pages/00090000/
集成开发环境(DevEco Studio 3.0.0.601版本以上)下载地址:
https://developer.harmonyos.com/cn/develop/deveco-studio#down...
OpenHarmony SDK 3.0.0.0 以上;
社群二维码HarmonyOS.png


HarmonyOS开发者
72 声望37 粉丝

提供HarmonyOS关键技术解析、版本更新、开发者实践和活动资讯,欢迎各位开发者加入HarmonyOS生态,一起创造无限可能!