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

打开应用在通过邀请用户进行设备认证后,用户须根据提示完成相应操作,然后通过分布式流转实现随机传递炸弹给下一位用户的效果。那么这样一款传炸弹应用如何进行开发呢?
完整的项目结构目录如下:
├─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 新建工程
2. 编写游戏页面

图2 游戏界面效果图
效果图如上可以分为两部分:
- 顶部状态提示栏
首先在 @entry 组件入口 build() 中使用 Stack 作为容器,达到图片和文字堆叠的效果;
接着依次写入 Image 包裹的两个 Text 组件;
Stack() {
Image($r(<span class="hljs-string">"app.media.title"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">120</span>)
Column() {
Text(<span class="hljs-keyword">this</span>.duration.toString() + <span class="hljs-string">'ms'</span>).fontColor(Color.White)
Text(<span class="hljs-keyword">this</span>.touchText).fontColor(Color.White)
}
}
- 中间游戏炸弹九宫格区域
使用 Grid 网格容器来编写九宫格区域;
在 GridItem 中 Stack (容器依次添加方块背景图片和炸弹图片;
在 visibility 属性中用 bombIndex 变量值来决定炸弹显示的位置;
通过 onClick 点击事件和 GestureGroup 组合手势加入单击、双击和长按的监听事件;
Stack() {
Image($r(<span class="hljs-string">"app.media.background"</span>)).objectFit(ImageFit.Contain)
Grid() {
ForEach(<span class="hljs-keyword">this</span>.grid, (item) => {
GridItem() {
Stack() {
Image($r(<span class="hljs-string">"app.media.squares"</span>)).objectFit(ImageFit.Contain)
Image($r(<span class="hljs-string">"app.media.bomb"</span>))
.width(<span class="hljs-string">'50%'</span>)
.objectFit(ImageFit.Contain)
.visibility(<span class="hljs-keyword">this</span>.bombIndex == item ? Visibility.Visible : Visibility.Hidden)
<span class="hljs-comment">// 炸弹点击事件</span>
.onClick((event) => {
<span class="hljs-comment">// 单击</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.click)
})
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture({ repeat: <span class="hljs-literal">false</span> })
.onAction((event: GestureEvent) => {
<span class="hljs-comment">// 长按</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.longPress)
}),
TapGesture({ count: <span class="hljs-number">2</span> })
.onAction(() => {
<span class="hljs-comment">// 双击</span>
<span class="hljs-keyword">this</span>.judgeGame(RuleType.doubleClick)
})
)
}
}.forceRebuild(<span class="hljs-literal">false</span>)
}, item => item)
}
.columnsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
.rowsTemplate(<span class="hljs-string">'1fr 1fr 1fr'</span>)
.columnsGap(<span class="hljs-number">10</span>)
.rowsGap(<span class="hljs-number">10</span>)
.width(<span class="hljs-string">'90%'</span>)
.height(<span class="hljs-string">'75%'</span>)
}.width(<span class="hljs-string">'80%'</span>).height(<span class="hljs-string">'70%'</span>)
3. 添加弹窗
- 创建规则游戏弹窗
1)通过 @CustomDialog 装饰器来创建自定义弹窗,使用方式可参考:
2)规则弹窗效果如下,弹窗组成由两个 Text 和两个 Image 竖向排列组成,所以我们可以在 build()下使用 Column 容器来包裹,组件代码如下;

图3 游戏规则
@CustomDialog
struct RuleDialog {
controller: CustomDialogController
confirm: () => <span class="hljs-keyword">void</span>
invite: () => <span class="hljs-keyword">void</span>
@Consume deviceList: RemoteDevice[] build() {
Column() {
Text(<span class="hljs-string">'游戏规则'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
Text(<span class="hljs-string">'炸弹会随机出现在9个方块内,需要在规定时间内完成指定操作(点击、双击或长按),即可将炸弹传递给下一个人,小心炸弹可是会越来越快的喔!'</span>)
.fontSize(<span class="hljs-number">24</span>).margin({ bottom: <span class="hljs-number">10</span> })
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>)
.onClick(() => {
console.info(TAG + <span class="hljs-string">'Click start game'</span>)
<span class="hljs-keyword">if</span> (checkTrustedDevice(<span class="hljs-keyword">this</span>.remoteDeviceModel)) {
<span class="hljs-keyword">this</span>.controller.close()
<span class="hljs-keyword">this</span>.confirm()
}
})
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>)
.onClick(() => {
<span class="hljs-keyword">this</span>.invite()
})
}.width(<span class="hljs-string">'90%'</span>)
.margin(<span class="hljs-number">20</span>)
.backgroundColor(Color.White)
}
}
3)在 @entry 创建 CustomDialogController 对象并传入弹窗所需参数,后面可通过该对象 open() 和 close() 方法进行打开和关闭弹窗;
@Provide deviceList: RemoteDevice[] = []
private ruleDialog: CustomDialogController = <span class="hljs-keyword">new</span> CustomDialogController({
builder: RuleDialog({
invite: () => <span class="hljs-keyword">this</span>.InvitePlayer(),
confirm: () => <span class="hljs-keyword">this</span>.startGame(),
deviceList: <span class="hljs-keyword">this</span>.deviceList
}),
autoCancel: <span class="hljs-literal">false</span>
})
- 创建游戏失败弹窗,并添加动画效果

图4 游戏失败弹窗动画
1)编写弹窗布局:将游戏失败文本、炸弹图片和再来一局按钮图片放置于 Column 容器中;
2)用变量来控制动画起始和结束的位置:用 Flex 容器包裹炸弹图片,并用 @State 装饰变量 toggle,通过变量来动态修改 [Flex]的direction 属性;
@State toggle: boolean = <span class="hljs-literal">true</span>
private controller: CustomDialogController
@Consume deviceList: RemoteDevice[]
private confirm: () => <span class="hljs-keyword">void</span>
private interval = <span class="hljs-literal">null</span> build() {
Column() {
Text(<span class="hljs-string">'游戏失败'</span>).fontSize(<span class="hljs-number">30</span>).margin(<span class="hljs-number">20</span>)
Flex({
direction: <span class="hljs-keyword">this</span>.toggle ? FlexDirection.Column : FlexDirection.ColumnReverse,
alignItems: ItemAlign.Center
})
{
Image($r(<span class="hljs-string">"app.media.bomb"</span>)).objectFit(ImageFit.Contain).height(<span class="hljs-number">80</span>)
}.height(<span class="hljs-number">200</span>) 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>)
.onClick(() => {
<span class="hljs-keyword">this</span>.controller.close()
<span class="hljs-keyword">this</span>.confirm()
})
}
.width(<span class="hljs-string">'80%'</span>)
.margin(<span class="hljs-number">50</span>)
.backgroundColor(Color.White)
}
3)设置动画效果:使用 animateTo 显式动画接口炸弹位置切换时添加动画,并且设置定时器定时执行动画;
aboutToAppear() {
<span class="hljs-keyword">this</span>.setBombAnimate()
}
setBombAnimate() {
<span class="hljs-keyword">let</span> fun = () => {
<span class="hljs-keyword">this</span>.toggle = !<span class="hljs-keyword">this</span>.toggle;
}
<span class="hljs-keyword">this</span>.interval = setInterval(() => {
animateTo({ duration: <span class="hljs-number">1500</span>, curve: Curve.Sharp }, fun)
}, <span class="hljs-number">1600</span>)
}
二、添加分布式流转
分布式流转需要在同一网络下通过 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
import {RemoteDeviceManager} from <span class="hljs-string">'./RemoteDeviceManager'</span>
2)声明 @Provide 装饰的设备列表变量 deviceList,和创建 RemoteDeviceManager 实例。
@Provide deviceList: RemoteDevice[] = []
private remoteDm: RemoteDeviceManager = <span class="hljs-keyword">new</span> RemoteDeviceManager(<span class="hljs-keyword">this</span>.deviceList)
2. 刷新设备列表
在生命周期 aboutToAppear 中,调用刷新设备列表和开始发现设备。
aboutToAppear 定义:函数在创建自定义组件的新实例后,在执行其 build 函数之前执行。
aboutToAppear() {
<span class="hljs-keyword">this</span>.remoteDm.refreshRemoteDeviceList() <span class="hljs-comment">// 刷新设备列表</span>
<span class="hljs-keyword">this</span>.remoteDm.startDeviceDiscovery() <span class="hljs-comment">// 开始发现设备</span>
}
3. 设备认证
invitePlayer(remoteDevice:RemoteDevice) {
<span class="hljs-keyword">if</span> (remoteDevice.status == RemoteDeviceStatus.ONLINE) {
prompt.showToast({ message: <span class="hljs-string">"Already invited!"</span> })
<span class="hljs-keyword">return</span>
}
<span class="hljs-keyword">this</span>.remoteDm.authDevice(remoteDevice).then(() => {
prompt.showToast({ message: <span class="hljs-string">"Invite success! deviceName="</span> + remoteDevice.deviceName })
}).catch(() => {
prompt.showToast({ message: <span class="hljs-string">"Invite fail!"</span> })
})
}
4. 跨设备流转
从 deviceList 中获取设备列表在线的设备 Id,通过 featureAbility.startAbility 进行流转。
async startAbilityRandom() {
<span class="hljs-keyword">let</span> deviceId = <span class="hljs-keyword">this</span>.getRandomDeviceId() <span class="hljs-comment">// 随机获取设备id</span>
CommonLog.info(<span class="hljs-string">'featureAbility.startAbility deviceId='</span> + deviceId);
<span class="hljs-keyword">let</span> bundleName = await getBundleName()
<span class="hljs-keyword">let</span> wantValue = {
bundleName: bundleName,
abilityName: <span class="hljs-string">'com.sample.bombgame.MainAbility'</span>,
deviceId: deviceId,
parameters: {
ongoing: <span class="hljs-literal">true</span>,
transferNumber: <span class="hljs-keyword">this</span>.transferNumber + <span class="hljs-number">1</span>
}
};
featureAbility.startAbility({
want: wantValue
}).then((data) => {
CommonLog.info(<span class="hljs-string">' featureAbility.startAbility finished, '</span> + <span class="hljs-built_in">JSON</span>.stringify(data));
featureAbility.terminateSelf((error) => {
CommonLog.info(<span class="hljs-string">'terminateSelf finished, error='</span> + error);
});
});
}
5. 注销监听
在声明周期 aboutToDisappear 进行注销监听。
aboutToDisappear 定义:函数在自定义组件析构消耗之前执行。
aboutToDisappear() {
<span class="hljs-keyword">this</span>.remoteDm.stopDeviceDiscovery() <span class="hljs-comment">// 注销监听</span>
}
三、编写游戏逻辑
1. 开始游戏
startGame() {
CommonLog.info(<span class="hljs-string">'startGame'</span>);
<span class="hljs-keyword">this</span>.randomTouchRule() <span class="hljs-comment">// 随机游戏点击规则</span>
<span class="hljs-keyword">this</span>.setRandomBomb() <span class="hljs-comment">// 随机生成炸弹位置</span>
<span class="hljs-keyword">this</span>.stopCountDown() <span class="hljs-comment">// 停止倒计时</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.transferNumber < <span class="hljs-number">10</span>) {
<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>
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">this</span>.duration = <span class="hljs-number">2000</span>
}
<span class="hljs-keyword">const</span> interval: number = <span class="hljs-number">500</span>
<span class="hljs-comment">// 开始倒计时</span>
<span class="hljs-keyword">this</span>.timer = setInterval(() => {
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.duration <= interval) {
<span class="hljs-keyword">this</span>.duration = <span class="hljs-number">0</span>
clearInterval(<span class="hljs-keyword">this</span>.timer)
<span class="hljs-keyword">this</span>.timer = <span class="hljs-literal">null</span>
<span class="hljs-keyword">this</span>.gameFail()
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">this</span>.duration -= interval
}
}, interval)
}
2. 判断输赢
编写判断逻辑,用于不同的点击事件中调用。
/**
* 判断游戏输赢
* @param operation 点击类型
*/
judgeGame(operation:RuleType) {
this.stopCountDown()
if (operation != this.ruleText) {
this.gameFail()
} else {
prompt.showToast({ message: "finish" })
this.bombIndex = -1
this.startAbilityRandom()
}
}
3. 游戏失败
游戏失败,弹出游戏失败弹框。
gameFail() {
prompt.showToast({
message: <span class="hljs-string">'Game Fail'</span>
})
CommonLog.info(<span class="hljs-string">'gameFail'</span>);
<span class="hljs-keyword">this</span>.gameFailDialog.open()
}
四、项目下载和导入
项目仓库地址:
https://gitee.com/openharmony-sig/knowledge_demo_temp/tree/master/FA/Entertainment/BombGame
1)git下载
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. 设备编译约束
- 获取OpenHarmony源码(OpenHarmony 版本须 3.0 LTS 以上):https://www.openharmony.cn/pages/0001000202/#%E5%AE%89%E8%A3%85%E5%BF%85%E8%A6%81%E7%9A%84%E5%BA%93%E5%92%8C%E5%B7%A5%E5%85%B7
- 安装开发板环境:https://www.openharmony.cn/pages/0001000400/#hi3516%E5%B7%A5%E5%85%B7%E8%A6%81%E6%B1%82
- 开发板烧录:https://www.openharmony.cn/pages/0001000401/#%E4%BD%BF%E7%94%A8%E7%BD%91%E5%8F%A3%E7%83%A7%E5%BD%95
2. 应用编译约束
- 参考应用开发快速入门:https://www.openharmony.cn/pages/00090000/
- 集成开发环境(DevEco Studio 3.0.0.601版本以上)下载地址:https://developer.harmonyos.com/cn/develop/deveco-studio#download_beta
- OpenHarmony SDK 3.0.0.0 以上;

简单3步,OpenHarmony上跑起ArkUI分布式小游戏的更多相关文章
- 简单四步開始树莓派上的Docker之旅
大概这篇博文发表之后,应该算是我个人的第一篇翻译作品了,翻译的可能不是非常到位,望各位看官大刀砍过来. 原文链接:http://resin.io/blog/docker-on-raspberry-pi ...
- 【开源】简单4步搞定QQ登录,无需什么代码功底【无语言界限】
说17号发超简单的教程就17号,qq核审通过后就封装了这个,现在放出来~~ 这个是我封装的一个开源项目:https://github.com/dunitian/LoTQQLogin ————————— ...
- 让python在hadoop上跑起来
duang~好久没有更新博客啦,原因很简单,实习啦-好吧,我过来这边上班表示觉得自己简直弱爆了.第一周,配置环境:第二周,将数据可视化,包括学习了excel2013的一些高大上的技能,例如数据透视表和 ...
- JQUERY插件JqueryAjaxFileUplaoder----更简单的异步文件上传
异步上传相信大家都做过类似的功能,JqueryAjaxFileUploader为我们提供了更简单的实现和使用方式.不过既然是JQUERY的插件那么它所依赖的环境大家都懂得.JqueryAjaxFile ...
- 【转】简单十步让你全面理解SQL
简单十步让你全面理解SQL 很多程序员认为SQL是一头难以驯服的野兽.它是为数不多的声明性语言之一,也因为这样,其展示了完全不同于其他的表现形式.命令式语言. 面向对象语言甚至函数式编程语言(虽然有些 ...
- 大麦盒子(domybox)无法进入系统解决方案!【简单几步】
大麦无法进入系统解决方案![简单几步]前提准备:电脑一台盒子控制台软件盒子开机并联网并且盒子和电脑处于同一个路由器下的网络! 前提准备:电脑一台盒子控制台软件盒子开机并联网并且盒子和电脑处于同一个路由 ...
- 在free bsd上跑JMeter 的 plugin "PerfMon Server Agent"
在free bsd上跑JMeter 的 plugin "PerfMon Server Agent" 目的: 在free bsd上跑JMeter 的 plugin "Per ...
- 如何将新项目添加到github仓库中?只需简单几步~即可实现
问题描述:新建了一个项目,如何将其设置为git项目?如何关联到github上的仓库? 只需简单几步,但前提是需要已经安装好了git,并且有github账户 本文使用IntelliJ IDEA 其他编辑 ...
- 简单三步同步你的 VSCode 用户配置
https://www.cnblogs.com/knight-errant/p/10444777.html 设备重装,换设备,VSCode 又要重新配置了?不不不,简单三步,让你的 VSCode 配置 ...
- 我写的界面,在ARM上跑
这个...其实,我对ARM了解并不多,我顶多也就算是知道ARM怎么玩,EMMC干啥,MMU干啥,还有早期的叫法,比如那个NorFlash NandFlash ,然后也就没啥了. 然后写个裸机什么的,那 ...
随机推荐
- java 考试易考识记题目(一)
笔者擅长 C# 语言,4月份要考试,学习 JAVA 是为了考试罢了. 如何在最短时间内学习 JAVA 基础语法和通过考试考核呢~ 学习 JAVA ,要为了应付考试,判断.循环这部分,C.C++.C#. ...
- [linux 爬坑] 几个linux发行版尝试和令人崩溃的ssr安装体验
最近电脑上的manjaro好像出了问题,长时间不用就会死机.也懒得追究原因了,正好决定尝试几个发行版.首先尝试安装银河麒麟 这个发行版实际上就是ubuntu,甚至源什么的都是ubuntu的,也不 ...
- 从源码看webpack3打包流程
在javascript刚刚流行时,前端项目通常比较简单,不需要考虑项目的开发效率.性能和扩展性等. 随着前端项目越来越复杂,需要更正式的软件开发实践,比如单元测试(unit testing).代码检查 ...
- C++ //提高编程 模板(泛型编程 STL) //模板不可以直接使用 它只是一个框架 //模板的通用并不是万能的 //语法 //template<typename T> //函数模板两种方式 //1.自动类型推导 必须推导出一致的数据类型T,才可以使用 //2.显示指定类型 模板必须确定出T的数据类型,才可以使用
1 //C++提高编程 模板(泛型编程 STL) 2 //模板不可以直接使用 它只是一个框架 3 //模板的通用并不是万能的 4 //语法 5 //template<typename T> ...
- Codeforces Round 923 (Div. 3)(A~F)
目录 A B C D E F A #include <bits/stdc++.h> #define int long long #define rep(i,a,b) for(int i = ...
- spirmmvc框架整合手抄版示例,供基础搭建代码对照
注明所有文档和图片完整对照,辟免笔记出错,不能复习 package com.ithm.config; import com.alibaba.druid.pool.DruidDataSource; ...
- IDEA关联Tomcat(详细教程+安装包)
IDEA关联Tomcat 下载Tomcat安装包并解压到全英文目录 第一步:打开IDEA--Settings 第二步:搜索application--进入Application Services--点击 ...
- gap 单词学习 对标 open
为什么gap 和 open 联系记忆呢? gap是从行为动作中来 open 中 op 就是 up,是从 单词字母的角度来 但是 本意 这两个单词都差不多 gap gap : 来自PIE*ghai,打呵 ...
- Linux 服务器Python后台运行服务(ssh断开不退出)
壹: 最近用python搭建一个物联网数据存储的微服务,部署到ubuntu上去,所以,python后台运行是一个必不可少的环节. 贰: 这个只需要是一个命令即可: 命令1(记录所有日志): nohup ...
- 【stras-one】星念漫画下载器
原文链接:[stras-one]星念漫画下载器 - Stars-One的杂货小窝 一款将在线漫画保存到本地的下载工具 应一位蓝奏云批量下载器的用户的要求,实现了这款漫画下载的工具开发, 一直没时间,鸽 ...