绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
简单3步,OpenHarmony上跑起ArkUI分布式小游戏
2021-12-14 11:18:54

转自:OpenAtom OpenHarmony

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

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

├─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 游戏界面效果图

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


  • 顶部状态提示栏

1. 首先在 @entry 组件入口 build() 中使用 Stack 作为容器,达到图片和文字堆叠的效果;


2. 接着依次写入 Image 包裹的两个 Text 组件;

Stack() {
Image($r(class="hljs-string">"app.media.title")).objectFit(ImageFit.Contain).height(class="hljs-number">120)
Column() {
Text(class="hljs-keyword">this.duration.toString() + class="hljs-string">'ms').fontColor(Color.White)
Text(class="hljs-keyword">this.touchText).fontColor(Color.White)
}
}

  • 中间游戏dan 九宫格区域

1. 使用 Grid 网格容器来编写九宫格区域;


2. 在 GridItem 中 Stack (容器依次添加方块背景图片和

dan  图片;


3. 在 visibility 属性中用 bombIndex 变量值来决定

dan  显示的位置;


4. 通过 onClick 点击事件和 GestureGroup 组合手势加入单击、双击和长按的监听事件;

Stack() {
Image($r(<span class="hljs-string">"app.media.background"span>)).objectFit(ImageFit.Contain)
Grid() {
ForEach(<span class="hljs-keyword">thisspan>.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">thisspan>.bombIndex == item ? Visibility.Visible : Visibility.Hidden)
<span class="hljs-comment">// 炸dan点击事件span>
.onClick((event) => {
<span class="hljs-comment">// 单击span>
<span class="hljs-keyword">thisspan>.judgeGame(RuleType.click)
})
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture({ repeat: <span class="hljs-literal">falsespan> })
.onAction((event: GestureEvent) => {
<span class="hljs-comment">// 长按span>
<span class="hljs-keyword">thisspan>.judgeGame(RuleType.longPress)
}),
TapGesture({ count: <span class="hljs-number">2span> })
.onAction(() => {
<span class="hljs-comment">// 双击span>
<span class="hljs-keyword">thisspan>.judgeGame(RuleType.doubleClick)
})
)
}
}.forceRebuild(<span class="hljs-literal">falsespan>)
}, 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">10span>)
.rowsGap(<span class="hljs-number">10span>)
.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: () => class="hljs-keyword">void
invite: () => class="hljs-keyword">void
@Consume deviceList: RemoteDevice[]

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

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

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

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

图4 游戏失败弹窗动画

1)编写弹窗布局:将游戏失败文本、

dan  图片和再来一局按钮图片放置于 Column 容器中;


2)用变量来控制动画起始和结束的位置:用 Flex 容器包裹

dan  图片,并用 @State 装饰变量 toggle,通过变量来动态修改 [Flex]的direction 属性;

@State toggle: boolean = class="hljs-literal">true
private controller: CustomDialogController
@Consume deviceList: RemoteDevice[]
private confirm: () => class="hljs-keyword">void
private interval = class="hljs-literal">null

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

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

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

aboutToAppear() {
class="hljs-keyword">this.setBombAnimate()
}

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

二、添加分布式流转

分布式流转需要在同一网络下通过 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 class="hljs-string">'./RemoteDeviceManager'</span>

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

@Provide deviceList: RemoteDevice[] = []
private remoteDm: RemoteDeviceManager = class="hljs-keyword">new RemoteDeviceManager(class="hljs-keyword">this.deviceList)

2. 刷新设备列表

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


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

aboutToAppear() {
class="hljs-keyword">this</span>.remoteDm.refreshRemoteDeviceList() // 刷新设备列表span>
<span class="hljs-keyword">thisspan>.remoteDm.startDeviceDiscovery() class="hljs-comment">// 开始发现设备
}

3. 设备认证

invitePlayer(remoteDevice:RemoteDevice) {
class="hljs-keyword">if (remoteDevice.status == RemoteDeviceStatus.ONLINE) {
prompt.showToast({ message: class="hljs-string">"Already invited!" })
class="hljs-keyword">return
}
class="hljs-keyword">this.remoteDm.authDevice(remoteDevice).then(() => {
prompt.showToast({ message: class="hljs-string">"Invite success! deviceName=" + remoteDevice.deviceName })
}).catch(() => {
prompt.showToast({ message: class="hljs-string">"Invite fail!" })
})
}

4. 跨设备流转

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

async startAbilityRandom() {
<span class="hljs-keyword">letspan> deviceId = <span class="hljs-keyword">thisspan>.getRandomDeviceId() <span class="hljs-comment">// 随机获取设备idspan>
CommonLog.info(<span class="hljs-string">'featureAbility.startAbility deviceId='span> + deviceId);
<span class="hljs-keyword">letspan> bundleName = await getBundleName()
<span class="hljs-keyword">letspan> wantValue = {
bundleName: bundleName,
abilityName: <span class="hljs-string">'com.sample.bombgame.MainAbility'span>,
deviceId: deviceId,
parameters: {
ongoing: <span class="hljs-literal">truespan>,
transferNumber: <span class="hljs-keyword">thisspan>.transferNumber + <span class="hljs-number">1span>
}
};
featureAbility.startAbility({
want: wantValue
}).then((data) => {
CommonLog.info(<span class="hljs-string">' featureAbility.startAbility finished, 'span> + <span class="hljs-built_in">JSONspan>.stringify(data));
featureAbility.terminateSelf((error) => {
CommonLog.info(<span class="hljs-string">'terminateSelf finished, error='span> + error);
});
});
}

5. 注销监听

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

aboutToDisappear() {
class="hljs-keyword">this.remoteDm.stopDeviceDiscovery() class="hljs-comment">// 注销监听
}

三、编写游戏逻辑

1. 开始游戏

startGame() {
CommonLog.info("hljs-string">'startGame');
"hljs-keyword">this.randomTouchRule() "hljs-comment">// 随机游戏点击规则
"hljs-keyword">this.setRandomBomb() "hljs-comment">// 随机生成炸dan位置
"hljs-keyword">this.stopCountDown() "hljs-comment">// 停止倒计时
"hljs-keyword">if ("hljs-keyword">this.transferNumber < "hljs-number">10) {
"hljs-keyword">this.duration = "hljs-number">3000 - "hljs-keyword">this.transferNumber * "hljs-number">100
} "hljs-keyword">else {
"hljs-keyword">this.duration = "hljs-number">2000
}
"hljs-keyword">const interval: number = "hljs-number">500
"hljs-comment">// 开始倒计时
"hljs-keyword">this.timer = setInterval(() => {
"hljs-keyword">if ("hljs-keyword">this.duration <= interval) {
"hljs-keyword">this.duration = "hljs-number">0
clearInterval("hljs-keyword">this.timer)
"hljs-keyword">this.timer = "hljs-literal">null
"hljs-keyword">this.gameFail()
} "hljs-keyword">else {
"hljs-keyword">this.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: class="hljs-string">'Game Fail'
})
CommonLog.info(class="hljs-string">'gameFail');
class="hljs-keyword">this.gameFailDialog.open()
}

四、项目下载和导入

项目仓库地址:


https://gitee.com/openharmony-sig/knowledge_demo_temp/tree/master/FA/Entertainment/BombGame

1)git下载

git clone https:class="hljs-comment">//gitee.com/openharmony-sig/knowledge_demo_temp.git

2)项目导入
打开 DevEco Studio,点击 File->Open->下载路径/FA/Entertainment/BombGame

五、约束与限制

1. 设备编译约束

2. 应用编译约束

 

扫码添加开发者小助手微信
获取更多HarmonyOS开发资源和开发者活动资讯

分享好友

分享这个小栈给你的朋友们,一起进步吧。

HarmonyOS开发者社区
创建时间:2021-11-12 10:38:00
提供HarmonyOS关键技术解析、版本更新、开发者实践和活动资讯,欢迎各位开发者加入HarmonyOS生态,一起创造无限可能!
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • HarmonyOS开发者社区
    栈主
戳我,来吐槽~