数独闯关游戏元服务,支持2*4历史闯关记录卡片、4*4数独闯关游戏卡片,无需进入元服务,可在桌面上开始游戏,同步刷新记录。
选择以下一种方式,打开工程创建向导界面
在Choose Your Ability Template向导页,单击“Atomic Service”切换工程类型,选择云开发模板,单击“Next”进入下一步。
在工程配置界面,配置工程的基本信息。单击“Next”按钮进入关联云开发资源界面。
为工程关联云开发所需的资源,即在DevEco Studio中选择您的华为开发者账号加入的开发者团队,将该团队在AGC的同包名应用关联到当前工程,具体操作如下:
登录成功后,在授权界面单击“允许”按钮为DevEco Studio授权,界面将展示账号昵称。
单击“AppGallery Connect”打开AGC应用创建向导,填写应用信息,单击“确认”按钮创建应用。
完成以上操作后,DevEco Studio即可获取到同包名应用对应的项目信息。
成功创建工程并关联云开发资源后,DevEco Studio会为工程自动执行一些初始化配置,并开通云开发相关服务:认证服务、云函数、云数据库、云托管、API网关、云存储。
端云一体化元服务开发工程目录分为三个子工程:元服务开发工程(Application)、云开发工程(CloudProgram)、端侧公共库(External Libraries)。
元服务开发工程主要用于开发应用端侧的业务代码,元服务开发工程目录结构如下:
- Application - AppScope app.json5 // 应用的全局配置信息 - entry // 应用/服务模块,编译构建生成一个HAP oh_modules // 用于存放三方库依赖信息 - src/main - ets // 用于存放ArkTS源码 - resources // 用于存放应用/服务所用到的资源文件 module.json5 // Stage模型配置文件 build-profile.json5 // 当前模块信息、编译信息配置项 hvigorfile.ts // 模块级编译构建任务脚本 oh-package.json5 // 配置三方包声明的入口及包名 build-profile.json5 // 应用配置信息,包括签名、产品配置等 hvigorfile.ts // 应用级编译构建任务脚本
云开发工程中开发者可以为应用开发云函数和云数据库服务资源,云开发工程目录结构如下:
- CloudProgram - clouddb // 云数据库工程目录 dataentry // 用于存放数据条目文件 objecttype // 用于存放对象类型文件 db-config.json // 模块配置文件 - cloudfunctions // 云函数工程目录 cloudFunctionName // 云函数名称 node_modules // 包含所有三方依赖 cloud-config.json // 云开发工程配置文件 package.json // 定义了TypeScript公共依赖
在云端工程(CloudProgram)中可以创建函数、编写函数业务代码、为函数配置调用触发器。
1.单击“cloudfunctions”目录,选择“New > Cloud Function”创建云函数。
2.输入函数名称,单击“OK”按钮DevEco Studio自动生成函数目录。函数名称仅支持小写英文字母、数字、中划线(-),首字母必须为小写字母,结尾不能为中划线(-)。
3.云函数目录结构。
- sudoku-algorithm node_modules // 自动为该函数引入依赖包 function-config.json // 函数的配置文件,可配置触发器,通过触发器暴露的触发条件来实现函数调用。 package.json // 包含了当前函数的名称、版本等函数元数据。 sudoku-algorithm.ts // 函数入口文件
4.云函数触发器 云函数触发器在function-config.json文件中triggers属性中配置,当前支持HTTP触发器、CLOUDDB触发器、AUTH触发器、CLOUDSTORAGE触发器、CRON触发器五种。
{ "type": "http", "properties": { "enableUrlDecode": true, "authFlag": "true", "authAlgor": "HDA-SYSTEM", "authType": "apigw-client" }}
云函数的代码实现基于不同的语言运行环境可分为Node.js、Java、Python,还有一种比较特别运行环境为Custom Runtime(自定义运行环境)。本工程的语言运行环境为Node.js。
1.云函数的入口方法
module.exports.myHandler = function(event, context, callback, logger)
函数必须通过显示调用callback(object)将事件处理结果返回给AGC,结果可以是任意对象,但必须与JSON.stringify兼容,AGC会将结果转换成JSON字符串,返回给调用方。callback执行完成后,函数即执行结束。
2.为云函数添加返回内容
let myHandler = async function (event, context, callback, logger) { logger.info(event); // do something here callback({ code: 0, desc: "Success.", data: "请求成功!" });};export { myHandler };
3.调试云函数
函数开发过程中,开发者可在本地进行调试,或者将函数部署到AGC云端后,在本地触发调用云端函数。当前本地调试支持Run和Debug两种模式,Debug模式支持使用断点来追踪函数的运行情况。
4.部署云函数完成函数代码开发后,开发者可将函数部署到AGC控制台,支持单个部署和批量部署。
当开发者创建的函数或函数别名中创建一个HTTP类型的触发器后,在应用客户端调用函数时需要传入HTTP触发器的标识,查询方法如下:在函数的触发器页面点击“HTTPTrigger”触发器,查看“触发URL”的后缀,获取触发器标识,格式为“函数名-版本号”。
应用集成云函数SDK后,可以在应用内直接通过SDK API调用AGC中的云函数,云函数SDK与AGC的函数调用基于HTTPS的安全访问。
import agconnect from '@hw-agconnect/api-ohos';import "@hw-agconnect/function-ohos";import { Constants } from '../common/Constants';import { Log } from '../common/Log';import { getAGConnect } from './AgcConfig';export function getSudokuPuzzle(context: any) { return new Promise((resolve, reject) => { getAGConnect(context); // 调用wrap方法设置函数,在方法中传入触发器标识,返回得到可执行云函数对象 let functionCallable = agconnect.function().wrap("sudoku-algorithm-$latest"); // 调用call方法运行云函数,若函数有入参,可以将参数转化为JSON对象或JSON字符串传入,若无参则不传 functionCallable.call().then((ret: any) => { // 可调用getValue方法获取函数的返回值 let result = ret.getValue(); Log.info(Constants.LOG_TAG_NAME, `sudoku-algorithm called, result: ${JSON.stringify(result)}`); resolve(result); }).catch((err: any) => { Log.error(Constants.LOG_TAG_NAME, `sudoku-algorithm failed, cause: ${JSON.stringify(err)}`); }) });}
Button('请求自定义云函数') .fontSize(16) .onClick(() => { getSudokuPuzzle(getContext(this)).then((ret) => { Log.info(Constants.LOG_TAG_NAME, `单击按钮调用云函数返回结果: ${JSON.stringify(ret)}`) }) })
使用回溯算法填充数独谜题,并随机移除一些数字将其作为数独谜题,然后求解指定数独谜题的所有解。
let myHandler = async function (event, context, callback, logger) { // 传递的关卡值作为需要填充的空格数 let body = event.body; let params = JSON.parse(body); let levelNum = params.level; // 创建一个9*9的空白数独谜题 let sudoku = Array.from({ length: 9 }, () => Array(9).fill(0)); // 使用回溯算法填充数独谜题 solve_sudoku(sudoku); // 随机移除一些数字,生成数独谜题 remove_number(sudoku, levelNum); let solutions = answer_sudoku(sudoku); let sudokuPuzzle = { "original": sudoku, "answer": solutions } callback({ code: 0, desc: "Success.", data: JSON.stringify(sudokuPuzzle) });};function solve_sudoku(sudoku){...}function remove_number(sudoku, level){...}function answer_sudoku(sudoku){...}export { myHandler };
当前AGC认证服务为HarmonyOS应用/服务提供的登录认证方式有手机、邮箱和关联账号三种方式。本工程使用“邮箱+验证码”的方式作为应用的登录入口。
// 申请邮箱验证码public requestEmailVerifyCode(email: string) { let verifyCodeSettings = new VerifyCodeSettingBuilder() .setAction(VerifyCodeAction.REGISTER_LOGIN) .setLang('zh_CN') .setSendInterval(60) .build(); this.agc.auth().requestEmailVerifyCode(email, verifyCodeSettings) .then((ret) => { Log.info(TAG, JSON.stringify({ "Verify Code Result: ": ret })); }).catch((error) => { Log.error(TAG, "Error: " + JSON.stringify(error)); });}
// 邮箱账号注册登录public async loginByEmail(email: string, verifyCode: string): Promise<AgUser> { return new Promise((resolve, reject) => { // 如果创建账户的时候没有设置过密码,则只能通过此接口进行登录 let credential = EmailAuthProvider.credentialWithVerifyCode(email, verifyCode); // 登录接口,通过第三方认证来登录AGConnect平台 this.agc.auth().signIn(credential).then(async (ret) => { Log.info(TAG, `User has signed in. User: ${JSON.stringify(ret)}`); let user = ret.getUser(); let userExtra = await ret.getUser().getUserExtra(); let loginRes = new AgUser( user.getUid(), user.getPhotoUrl(), user.getPhone(), user.getDisplayName(), userExtra.getCreateTime(), userExtra.getLastSignInTime()) resolve(loginRes); }).catch((error) => { Log.error(TAG, "Error: ", error); reject(error); }); });}
通过容器组件Flex、Row、Column以及基础组件Text、Image、Button、Navigation、TextInput构建邮箱登录页面。
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Navigation() { Column() { Row({ space: Constants.LENGTH_5_PX }) { TextInput({ placeholder: '请输入邮箱账号..'}) .type(InputType.Email) .layoutWeight(Constants.LENGTH_3_PX) .borderRadius(Constants.BORDER_RADIUS_4_PX) .maxLength(Constants.LENGTH_20_PX) .height(Constants.HEIGHT_40) .enabled(this.timer === 60) .onChange((val) => { this.email = val; }) } .width(Constants.PERCENT_100) .justifyContent(FlexAlign.Center) .margin({ bottom: Constants.LENGTH_20_PX }) Row({ space: Constants.LENGTH_5_PX }) { TextInput({ placeholder: '请输入验证码..', text: this.verificationCode }) .layoutWeight(Constants.LENGTH_3_PX) .borderRadius(Constants.BORDER_RADIUS_4_PX) .maxLength(Constants.LENGTH_6_PX) .height(Constants.HEIGHT_40) .onChange((val) => { this.verificationCode = val; }) Button(this.timer === 60 ? '获取验证码' : this.timer.toString(), { type: ButtonType.Normal }) .backgroundColor('#f9fcfb') .layoutWeight(Constants.LENGTH_2_PX) .borderColor('#169cd5') .borderWidth(Constants.LENGTH_1_PX) .fontColor('#169cd5') .borderRadius(Constants.BORDER_RADIUS_4_PX) .height(Constants.HEIGHT_40) .enabled(this.validateEmailAddress(this.email) && this.timer === 60) .onClick(() => this.onGetCodeButtonClicked()) } .width(Constants.PERCENT_100) .justifyContent(FlexAlign.Center) .margin({ bottom: Constants.LENGTH_20_PX }) Button('登录', { type: ButtonType.Normal }) .width(Constants.PERCENT_100) .borderRadius(Constants.BORDER_RADIUS_4_PX) .backgroundColor('#169cd5') .enabled(this.canAuthorize() && this.verificationCode.length > 5 && this.canLogin) .opacity(this.canLogin ? 1 : 0.5) .height(Constants.HEIGHT_40) .onClick(() => this.onAuthButtonClicked()) } .width(Constants.PERCENT_90).height(Constants.HEIGHT_50) .justifyContent(FlexAlign.Center) .margin({ top: Constants.LENGTH_40_PX }) .padding({ right: Constants.LENGTH_15_PX, left: Constants.LENGTH_15_PX }) .borderRadius(Constants.LENGTH_8_PX) .backgroundColor(0xFFFFFF) } .title(this.NavigationTitle()) .titleMode(NavigationTitleMode.Full) .hideTitleBar(false) .hideToolBar(false)}.width(Constants.PERCENT_100).height(Constants.PERCENT_100).backgroundColor(Constants.VIEW_BG_COLOR)
调用自定义的登录接口实现登录,并使用首选项自定义工具接口将用户信息写入缓存。
onAuthButtonClicked = () => { this.canLogin = false; this.agcAuth.loginByEmail(this.email, this.verificationCode).then((user) => { PreferencesUtil.putPreference(getContext(this), Constants.USER_AUTH_INFO, JSON.stringify(user)); Log.info(Constants.LOG_TAG_NAME, `Logged in successfully. Data: ${JSON.stringify(user)}`); this.canLogin = true; }).catch((err) => { this.canLogin = true; Log.error(Constants.LOG_TAG_NAME, `Logged in failed. Cause: ${JSON.stringify(err)}`); })}
右键单击“entry > src/main/ets > pages”目录选择“New > Pages”新建Setting设置页面。
在页面中使用容器组件Grid实现头像选择(提供可选头像6个)和使用基础组件TextInput实现昵称设置。
头像昵称设置成功后,跳转到游戏主界面,点击“开始”按钮从云函数中获取数独谜题及对应的解,然后通过容器组件Grid和其子组件GridItem构建9*9宫格并使用ForEach渲染宫格的对应组件。
// 获取数独谜题和解getSudoPuzzle = () => { let _this = this; getSudokuPuzzle(getContext(this), this.levelNum).then((ret: string) => { let result: SudokuPuzzle = JSON.parse(ret); _this.puzzles = result.original; _this.answers = result.answer; }).catch((err) => { Log.error(Constants.LOG_TAG_NAME, `cause: ${JSON.stringify(err)}`); })}Grid() { ForEach(this.puzzles, (item: Array<number>, i: number) => { ForEach(item, (temp: number, j: number) => { GridItem() { if (temp === 0) { TextInput() .type(InputType.Number) .maxLength(1) .backgroundColor(0xf47721) .caretColor(Color.White) .onChange((val) => { let answer = this.puzzles; answer = parseInt(val); this.userAnswer = answer; Log.info(Constants.LOG_TAG_NAME, JSON.stringify(this.userAnswer)); if (val == "") { this.userAnswer = <>; } }) } else { Text(temp.toString()) .fontSize(16) } } .borderWidth(1) }, (temp: number) => temp.toString()) }, (item: Array<number>) => item.toString())}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr').width(Constants.PERCENT_96).height(400).backgroundColor(0xFFFFFF)
万能卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。卡片常用于嵌入到其他应用(当前卡片使用方只支持系统应用,如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互功能。
工程在创建初会自动创建2*2服务卡片,位于“entry > src/main/ets > widget > pages”目录。在WidgetCard.ets文件中编写服务卡片呈现内容及样式。
Column() { Image($r("app.media.card_start")) .width(this.FULL_WIDTH_PERCENT) .height(this.FULL_HEIGHT_PERCENT) .objectFit(ImageFit.Cover)}.width(this.FULL_WIDTH_PERCENT).height(this.FULL_HEIGHT_PERCENT).onClick(() => { postCardAction(this, { "action": this.ACTION_TYPE, "abilityName": this.ABILITY_NAME, "params": { "message": this.MESSAGE } });})
ArkTS卡片提供了postCardAction()接口用于卡片内部和提供方应用间交互,当前支持router、message和call三种类型的事件,仅在卡片中可以调用。
接口定义:postCardAction(component: Object, action: Object): void
4*4服务卡片用于在桌面玩游戏,没关通关后需要通过message事件刷新卡片内容生成新的关卡。
创建ArkTS卡片有两种方式:
{ "name": "game", "description": "数独闯关游戏", "src": "./ets/widget/pages/GameCard.ets", "uiSyntax": "arkts", "window": { "designWidth": 720, "autoDesignWidth": true }, "colorMode": "auto", "isDefault": false, "updateEnabled": false, "scheduledUpdateTime": "10:30", "updateDuration": 1, "defaultDimension": "4*4", "supportDimensions": < "4*4" >}
卡片相关的配置主要包括FormExtensionAbility的配置和卡片的配置两部分:
{ "module": { ... "extensionAbilities": < { "name": "EntryFormAbility", "srcEntrance": "./ets/entryformability/EntryFormAbility.ts", "label": "$string:EntryFormAbility_label", "description": "$string:EntryFormAbility_desc", "type": "form", "metadata": < { "name": "ohos.extension.form", "resource": "$profile:form_config" } > } > }}
在卡片页面可以通过postCardAction接口触发message事件拉起FormExtensionAbility,然后由FormExtensionAbility刷新卡片内容。
Image($r('app.media.game_start')) .width(120).height(54) .onClick(() => { postCardAction(this, { 'action': 'message', 'params': { 'functionName': 'getSudoPuzzle' } }) })
onFormEvent(formId, message) { let params = JSON.parse(message); Log.info(Constants.LOG_TAG_NAME, `message ===> ${params.functionName}`); // Called when a specified message event defined by the form provider is triggered. if (params.functionName === "getSudoPuzzle") { let promise = PreferenceUtil.getPreference(this.context, Constants.GAME_LEVEL); promise.then((ret) => { let level = parseInt(ret); let puzzles = <>, answers = <>; getSudokuPuzzle(this.context, level).then((ret: string) => { let result: SudokuPuzzle = JSON.parse(ret); puzzles = result.original; answers = result.answer; let formData = { flag: true, puzzles: puzzles, answers: answers, level: level } let formBD = formBindingData.createFormBindingData(formData); Log.info(Constants.LOG_TAG_NAME, `level. ${JSON.stringify(formBD)}`); formProvider.updateForm(formId, formBD).then((data) => { Log.info(Constants.LOG_TAG_NAME, `FormAbility updateForm success. ${JSON.stringify(data)}`); }).catch((err) => { Log.error(Constants.LOG_TAG_NAME, `FormAbility updateForm failed. ${JSON.stringify(err)}`); }) }).catch((err) => { Log.error(Constants.LOG_TAG_NAME, `cause: ${JSON.stringify(err)}`); }) }) }}
卡片中需要使用@LocalStorageProp装饰器接收。
@LocalStorageProp("puzzles") puzzles: Array<Array<number>> = <>;@LocalStorageProp("answers") answers: Array<Array<Array<number>>> = <>;
ArkTS卡片具备JS卡片的全量能力,并且新增了动效能力和自定义绘制的能力,支持声明式的部分组件、事件、动效、数据管理、状态管理能力。在数独游戏中需要使用输入框录入谜题解,而ArkTS卡片暂时不具备TextInput组件能力,因此使用点击空白区域与数字按钮互换的方式替代TextInput组件能力。
Row() {}.backgroundColor(0xD1D3D5).width(this.FULL_HEIGHT_PERCENT).height(this.FULL_HEIGHT_PERCENT).onClick(() => { this.selectArr = ;})
ForEach(<1, 2, 3, 4, 5, 6, 7, 8, 9>, (item: number) => { Button({ type: ButtonType.Normal }) { Text(item.toString()).fontSize(14).fontWeight(700) } .backgroundColor(Color.Orange) .borderRadius(4) .width(30).height(30) .fontSize(12) .onClick(() => { if (this.selectArr.length !== 0) { let row = this.selectArr<0>; let col = this.selectArr<1>; this.puzzles = item; postCardAction(this, { 'action': 'message', 'params': { 'functionName': 'refresh', 'puzzles': this.puzzles } }) this.userAnswer = this.puzzles; this.selectArr = <>; } })})|
if (params.functionName === 'refresh') { let formData = { puzzles: params.puzzles } let formBD = formBindingData.createFormBindingData(formData); formProvider.updateForm(formId, formBD);}
在应用程序中可以使用setInterval进行计时操作,但当前ArkTS卡片不支持setInterval,因此使用new Date().getTime()开始时间和结束时间差值作为游戏时长替代setInterval方法。
使用第二种方式创建卡片,在"entry > src/main/ets > widget > pages"目录右键单击"New > ArkTS File"创建HistoryCard.ets文件,接着打开"entry > src/main/resources > base > profile"目录下的form_config.json文件,配置名称为history的2*4卡片。
{ "name": "history", "description": "历史闯关记录", "src": "./ets/widget/pages/HistoryCard.ets", "uiSyntax": "arkts", "window": { "designWidth": 720, "autoDesignWidth": true }, "colorMode": "auto", "isDefault": false, "updateEnabled": false, "scheduledUpdateTime": "02:35", "updateDuration": 1, "defaultDimension": "2*4", "supportDimensions": < "2*4" >}
let formStorage = PreferenceUtil.getPreference(this.context, Constants.FORM_CARD_Dimension_2_4);formStorage.then((ret) =>{ let formArr: Array<any> = new Array<any>(); if (ret !== "") { formArr = JSON.parse(ret); formArr.push(formInfoStorage); } else { formArr.push(formInfoStorage); } PreferenceUtil.putPreference(this.context, Constants.FORM_CARD_Dimension_2_4, JSON.stringify(formArr));})let promise = PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS);promise.then((ret) => { if (ret !== "") { let historyArr: Array<History> = JSON.parse(ret); formData = { histories: historyArr } let formBD = formBindingData.createFormBindingData(formData); formProvider.updateForm(formId, formBD); return formBD; }})
PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS).then((ret) => { let historyArr: Array<History> = <>; if (ret !== "") { historyArr = JSON.parse(ret); historyArr.push(history); } else { historyArr.push(history); } PreferenceUtil.putPreference(this.context, Constants.HISTORY_RECORDS, JSON.stringify(historyArr)); let formStorage = PreferenceUtil.getPreference(this.context, Constants.FORM_CARD_Dimension_2_4); formStorage.then((ret) =>{ if (ret !== "") { let formArr: Array<any> = JSON.parse(ret); formArr.forEach((item) => { let promise = PreferenceUtil.getPreference(this.context, Constants.HISTORY_RECORDS); promise.then((ret) => { if (ret !== "") { let historyArr: Array<History> = JSON.parse(ret); let formData = { histories: historyArr } let formBD = formBindingData.createFormBindingData(formData); formProvider.updateForm(item.formId, formBD); } }) }) } })})
大家可以在华为应用市场元服务专区、服务中心入口,体验已经上架的元服务。也可以点击进入元服务官网,了解更多相关信息。