轉自: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 以上;

掃碼添加開發者小助手微信
獲取更多HarmonyOS開發資源和開發者活動資訊
