簡單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 裝飾器來創建自定義彈窗,使用方式可參考:

自定義彈窗文檔:https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-methods-custom-dialog-box.md

  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. 設備編譯約束

 


 2. 應用編譯約束

 

 

掃碼添加開發者小助手微信

獲取更多HarmonyOS開發資源和開發者活動資訊​​​​


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM