鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲
1.前言
翻牌游戲萬(wàn)能卡片,隨機(jī)生成16張共包含8張完全不同的圖像,游戲的目標(biāo)是在有限30秒時(shí)間內(nèi),將16張卡片中包含相同的圖像的卡片兩兩配對(duì)。匹配的規(guī)則是連續(xù)點(diǎn)擊兩張卡片,若卡背面的圖像相同,則匹配成功,若不同則配對(duì)失敗。游戲主要考察玩家的記憶力,因?yàn)橛螒蜻€規(guī)定翻開的卡片數(shù)量至多有兩張,否則一開始被點(diǎn)擊而翻開的卡片將再次蓋上(若該張卡片沒有匹配成功)。此項(xiàng)目是用最新版DevEco Studio 3.1 Release并創(chuàng)建端云一體開發(fā),由于目前此版本不支持直接調(diào)用云數(shù)據(jù)庫(kù),不過(guò)可以通過(guò)云函數(shù)調(diào)用云數(shù)據(jù)庫(kù),也就是在服務(wù)卡片業(yè)務(wù)邏輯里通過(guò)調(diào)用云函數(shù)來(lái)完成游戲數(shù)據(jù)保存到云數(shù)據(jù)庫(kù),開發(fā)工具支持本地函數(shù)調(diào)用測(cè)試,大大方便了開發(fā),此貼重點(diǎn)講解云函數(shù)和云數(shù)據(jù)庫(kù)開發(fā),從而進(jìn)一步學(xué)習(xí)Serverless知識(shí),翻牌游戲萬(wàn)能卡片效果圖如下:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_元服務(wù)
2.知識(shí)點(diǎn)
為豐富HarmonyOS對(duì)云端開發(fā)的支持、實(shí)現(xiàn)HarmonyOS生態(tài)端云聯(lián)動(dòng),DevEco Studio推出了云開發(fā)功能,開發(fā)者在創(chuàng)建工程時(shí)選擇云開發(fā)模板,即可在DevEco Studio內(nèi)同時(shí)完成HarmonyOS應(yīng)用/服務(wù)的端側(cè)與云側(cè)開發(fā),體驗(yàn)端云一體化協(xié)同開發(fā)。
相比于傳統(tǒng)開發(fā)模式,云開發(fā)模式具備成本低、效率高、門檻低等優(yōu)勢(shì),具體區(qū)別見下表。
2.1. 開發(fā)流程
HarmonyOS應(yīng)用端云一體化開發(fā)流程如下圖所示。
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_游戲卡片_02
2.2. 創(chuàng)建端云一體化開發(fā)工程
2.2.1 新建原子化服務(wù)工程
2.2.2 工程初始化配置
2.2.3 端云一體化開發(fā)工程介紹
2.3. 開發(fā)云工程
2.3.1 開發(fā)云函數(shù)
2.3.2 開發(fā)云數(shù)據(jù)庫(kù)
2.4. 部署云工程
2.4.1 部署云工程
2.5. 小結(jié)
了解這些端云一體化開發(fā)知識(shí)點(diǎn)后,下面圍繞翻牌游戲萬(wàn)能卡片,在云數(shù)據(jù)庫(kù)里設(shè)計(jì)卡片表結(jié)構(gòu)和游戲記錄表結(jié)構(gòu),然后再編寫相關(guān)云函數(shù),在元服務(wù)業(yè)務(wù)邏輯調(diào)用云函數(shù)。
3.云數(shù)據(jù)庫(kù)開發(fā)講解
3.1. objecttype創(chuàng)建
3.1.1 展開CloudProgram -> clouddb -> objecttype 右擊objecttype目錄,創(chuàng)建 -> Cloud DB Object Type 輸入Object Type Name為t_form,點(diǎn)擊確認(rèn),代碼內(nèi)容如下:
{
"fields": [
{
"isNeedEncrypt": false,
"fieldName": "formId",
"notNull": true,
"belongPrimaryKey": true,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "formName",
"notNull": true,
"defaultValue": "",
"belongPrimaryKey": false,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "dimension",
"notNull": true,
"defaultValue": "0",
"belongPrimaryKey": false,
"fieldType": "Integer"
}
],
"indexes": [
{
"indexName": "formId",
"indexList": [{ "fieldName": "formId", "sortType": "ASC" }]
}
],
"objectTypeName": "t_form",
"permissions": [...]
}
3.1.2 展開CloudProgram -> clouddb -> objecttype 右擊objecttype目錄,創(chuàng)建 -> Cloud DB Object Type 輸入Object Type Name為t_record,點(diǎn)擊確認(rèn),代碼內(nèi)容如下:
{
"fields": [
{
"isNeedEncrypt": false,
"fieldName": "formId",
"notNull": true,
"belongPrimaryKey": true,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "matrixNum",
"notNull": true,
"defaultValue": "",
"belongPrimaryKey": false,
"fieldType": "String"
},
{
"isNeedEncrypt": false,
"fieldName": "bestScore",
"notNull": true,
"defaultValue": "0",
"belongPrimaryKey": false,
"fieldType": "Double"
}
],
"indexes": [
{
"indexName": "formId",
"indexList": [{ "fieldName": "formId", "sortType": "ASC" }]
}
],
"objectTypeName": "t_record",
"permissions": [...]
}
3.2. dataentry創(chuàng)建
3.2.1 展開CloudProgram -> clouddb -> dataentry 右擊dataentry目錄,創(chuàng)建 -> Cloud DB Data Entry 這里先選擇上面創(chuàng)建的Object Type為t_form,再輸入Data Entry Name為form_data,點(diǎn)擊確認(rèn),代碼內(nèi)容如下:
{
"cloudDBZoneName": "widgetCard",
"objectTypeName": "t_form",
"objects": [
{
"formId": "x000001",
"formName": "卡片1",
"dimension": 2
}
]
}
3.2.2 展開CloudProgram -> clouddb -> dataentry 右擊dataentry目錄,創(chuàng)建 -> Cloud DB Data Entry 這里先選擇上面創(chuàng)建的Object Type為t_record,再輸入Data Entry Name為record_data,點(diǎn)擊確認(rèn),修改內(nèi)容如下:
{
"cloudDBZoneName": "widgetCard",
"objectTypeName": "t_record",
"objects": [
{
"formId": "x000001",
"matrixNum": "4x4",
"bestScore": 2.234
}
]
}
3.3. 小結(jié)
其實(shí)dataentry文件可以不創(chuàng)建,這里對(duì)兩個(gè)表都初始化了一條數(shù)據(jù),是方便下面的調(diào)用使用,云數(shù)據(jù)庫(kù)就是定義好表結(jié)構(gòu)、權(quán)限配置就可以,數(shù)據(jù)的添加、修改、刪除、查詢都可以通過(guò)云函數(shù)來(lái)完成。
4.云函數(shù)開發(fā)講解
4.1. 卡片云函數(shù)創(chuàng)建
4.1.1 展開CloudProgram -> cloudfunctions 右擊cloudfunctions目錄,創(chuàng)建 -> Cloud Function 輸入Cloud Function Name為form,點(diǎn)擊確認(rèn), 卡片云函數(shù)里包含了增刪改查操作,所以在form下,創(chuàng)建不同的文件夾來(lái)區(qū)分,目錄結(jié)構(gòu)如下:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_Serverless_03
4.1.2 首先說(shuō)一下與云數(shù)據(jù)庫(kù)交互文件,t_form.js對(duì)應(yīng)的是云數(shù)據(jù)庫(kù)實(shí)體類,如各屬性的get和set方法,之前FA模式下的DevEco Studio端云一體化開發(fā),支持直接調(diào)用云數(shù)據(jù)庫(kù),現(xiàn)在Stage模式下的DevEco Studio端云一體化開發(fā),還不支持直接調(diào)用云數(shù)據(jù)庫(kù),通過(guò)云函數(shù)來(lái)調(diào)用,所以這里的云數(shù)據(jù)庫(kù)實(shí)體類,可以通過(guò)AGC導(dǎo)出,然后復(fù)制到t_form文件內(nèi),導(dǎo)出步驟圖:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_游戲卡片_04
如卡片實(shí)例體類:
class t_form {
getFieldTypeMap() {
let fieldTypeMap = new Map();
fieldTypeMap.set('formId', 'String');
fieldTypeMap.set('formName', 'String');
fieldTypeMap.set('dimension', 'Integer');
return fieldTypeMap;
}
getClassName() {
return 't_form';
}
getPrimaryKeyList() {
let primaryKeyList = [];
primaryKeyList.push('formId');
return primaryKeyList;
}
getIndexList() {
let indexList = [];
return indexList;
}
getEncryptedFieldList() {
let encryptedFieldList = [];
return encryptedFieldList;
}
// set and get
setFormId(formId) {this.formId = formId;}
getFormId() {return this.formId;}
setFormName(formName) {this.formName = formName;}
getFormName() {return this.formName;}
setDimension(dimension) {this.dimension = dimension;}
getDimension() {return this.dimension;}
}
module.exports = {t_form}
4.1.3 CloudDBZoneWrapper操作云數(shù)據(jù)庫(kù),這里主要列舉構(gòu)造函數(shù)和增加方法內(nèi)容:
import * as clouddb from '@agconnect/database-server';
import { t_form as FormBean } from './models/t_form';
import * as agconnect from '@agconnect/common-server';
const ZONE_NAME = "widgetCard";
export class CloudDBZoneWrapper {
logger;
cloudDbZone;
constructor(credential, logger) {
this.logger = logger;
try {
// 初始化AGCClient
let agcClient;
try {
agcClient = agconnect.AGCClient.getInstance();
} catch {
agconnect.AGCClient.initialize(credential);
agcClient = agconnect.AGCClient.getInstance();
}
// 初始化AGConnectCloudDB實(shí)例
let cloudDbInstance;
try {
cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
} catch {
clouddb.AGConnectCloudDB.initialize(agcClient);
cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
}
// 創(chuàng)建CloudDBZoneConfig配置對(duì)象,并設(shè)置云側(cè)CloudDB zone名稱,打開Cloud DB zone實(shí)例
const cloudDBZoneConfig = new clouddb.CloudDBZoneConfig(ZONE_NAME);
this.cloudDbZone = cloudDbInstance.openCloudDBZone(cloudDBZoneConfig);
} catch (err) {
logger.error("xx [form-func]CloudDBZoneWrapper init CloudDBZoneWrapper error: " + err);
}
}
async insert(addForm) {
if (!this.cloudDbZone) {
this.logger.error("xx [form-func]CloudDBZoneWrapper->insert CloudDBClient is null, try re-initialize it");
}
try {
let res = await this.cloudDbZone.executeUpsert(addForm);
this.logger.info("xx [form-func]CloudDBZoneWrapper->insert Insert " + res + " records success");
} catch (error) {
this.logger.error("xx [form-func]CloudDBZoneWrapper->insert executeInsert addressRecords failed " + error);
}
}
}
4.1.4 新增卡片函數(shù)form-insert,關(guān)鍵代碼如下:
import { CloudDBZoneWrapper } from '../clouddb/CloudDBZoneWrapper.js';
import * as Utils from '../utils/Utils.js';
export const myHandler = async function (event, context, callback, logger) {
const credential = Utils.getCredential(context, logger);
try {
const cloudDBZoneWrapper = new CloudDBZoneWrapper(credential, logger);
let formObj = cloudDBZoneWrapper.getForm(event);
await cloudDBZoneWrapper.insert(formObj);
callback({
ret: { code: 0, desc: "SUCCESS" },
});
} catch (err) {
logger.error("xx [form-func]insert func error:" + err.message + " stack:" + err.stack);
callback({
ret: { code: -1, desc: "ERROR" },
});
}
};
4.1.5 卡片云函數(shù)主入口,關(guān)鍵代碼如下:
let myHandler = async function (event, context, callback, logger) {
let operation;
let params;
logger.info("xx enter form func with operation " + event.operation);
operation = event.body ? JSON.parse(event.body).operation : event.operation;
params = event.body ? JSON.parse(event.body).params : event.params;
switch (operation) {
case "query":
query.myHandler(params, context, callback, logger);
break;
case "queryById":
queryById.myHandler(params, context, callback, logger);
break;
case "insert":
insert.myHandler(params, context, callback, logger);
break;
case "update":
update.myHandler(params, context, callback, logger);
break;
case "delete":
deleteByObj.myHandler(params, context, callback, logger);
break;
default:
callback({
ret: { code: -1, desc: "no such function" },
});
}
};
module.exports.myHandler = myHandler;
4.2. 記錄云函數(shù)創(chuàng)建
4.2.1 展開CloudProgram -> cloudfunctions 右擊cloudfunctions目錄,創(chuàng)建 -> Cloud Function 輸入Cloud Function Name為record,點(diǎn)擊確認(rèn), 成績(jī)?cè)坪瘮?shù)里包含了增刪改查操作,所以在record下,創(chuàng)建不同的文件夾來(lái)區(qū)分,目錄結(jié)構(gòu)如下:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_Serverless_05
4.2.2 記錄表云數(shù)據(jù)庫(kù)操作與卡片操作一樣,這里就不在重復(fù)了,可以參考一下上面卡片操作方法就可以。
5.元服務(wù)開發(fā)
5.1. 1*2卡片開發(fā)
5.1.1 創(chuàng)建卡片步驟:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_端云一體化開發(fā)_06
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_端云一體化開發(fā)_07
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_Serverless_08
5.1.2 卡片模板創(chuàng)建好后,修改為翻牌游戲UI, 就是左邊顯示一張獎(jiǎng)牌圖片,右邊顯示最快記錄時(shí)間,圖片效果為:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_游戲卡片_09
UI代碼如下:
build() {
Row() {
Image($r('app.media.cup'))
.width(32).height(32).objectFit(ImageFit.Cover)
Text(`最快成績(jī):${this.totalBestScore}'s`)
.fontSize($r('app.float.font_size'))
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.onClick(() => {
postCardAction(this, {
"action": 'router',
"abilityName": 'EntryAbility',
"params": {
"message": 'view history'
}
});
})
}
5.2. 4*4卡片開發(fā)
5.2.1 創(chuàng)建卡片步驟如上面步驟。
5.2.2 卡片模板創(chuàng)建好后,修改為翻牌游戲UI, 就是頂部顯示游戲信息,如:游戲標(biāo)題,當(dāng)前用時(shí),倒計(jì)時(shí),開始游戲,中部顯示16張卡片,圖片效果為:
鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲_Serverless_10
UI部分代碼如下:
build() {
Column() {
Row() {
Text('記憶翻牌游戲')
// Text(`最快:${this.totalBestScore}'s`)
// .fontSize(10)
Text(`當(dāng)前:${this.tookTime}'s`)
.fontSize(10)
Text(`倒計(jì)時(shí):${this.timeCount}'s`)
.fontSize(10)
Text('開始')
.visibility(this.isStart ? Visibility.Visible : Visibility.Hidden)
.onClick(() => {
this.startGame()
})
}
.width(FULL_WIDTH_PERCENT)
.justifyContent(FlexAlign.SpaceBetween)
.height(30)
Stack(){
Flex({wrap: FlexWrap.Wrap, direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceEvenly}) {
ForEach(this.arr, (idx) => {
GameCard({card: this.cards[idx], cardArray: $cards, startTime: this.startTime})
}, (idx) => idx.toString())
}
Text(this.resultMessage)
.width(FULL_WIDTH_PERCENT)
.height(FULL_HEIGHT_PERCENT)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.backgroundColor('rgba(0,0,0,0.5)')
.visibility(this.isStart ? Visibility.Visible : Visibility.None)
}
.width(FULL_WIDTH_PERCENT)
.layoutWeight(1)
}
.width(FULL_WIDTH_PERCENT)
.height(FULL_HEIGHT_PERCENT)
.padding(10)
}
6.代碼講解
6.1. 云函數(shù)調(diào)用公共類
export class DatabaseUtils {
async callWithParams(context, trigger, operation, params) {
await getAGConnect(context);
let body = {
"operation": operation,
"params": params
}
try {
let functionCallable = agconnect.function().wrap(trigger);
let functionResult = await functionCallable.call(body);
return functionResult.getValue();
}
catch (err) {
return {
"ret": {"code": -1, "desc": "ERROR"}
}
}
}
async invoke(context: any, trigger?: string, operation?: string, params?: object) {
console.info(CommonConstants.DATABASE_TAG, 'xx invoke params: '+JSON.stringify(params))
return await this.callWithParams(context, trigger, operation, params);
}
/**
* 插入卡片數(shù)據(jù)。
*
* @param{Form}Form表單實(shí)體。
* @param{DataRdb.RdbStore}RDB存儲(chǔ)RDB數(shù)據(jù)庫(kù)。
* @return返回操作信息。
*/
async insertForm(context: any, form: Form) {
let res = await this.invoke(context, Triggers.FormFunc, RequestType.Insert, form);
console.info(CommonConstants.DATABASE_TAG, 'xx insertForm result: ' + JSON.stringify(res));
}
......
}
6.2. 卡片Ability調(diào)用公共類
EntryFormAbility.ets卡片生命周期代碼如下:
onAddForm(want) {
// 獲取卡片ID:ohos.extra.param.key.form_identity
let formId: string = want.parameters[CommonConstants.FORM_PARAM_IDENTITY_KEY] as string;
// 獲取卡片名稱:ohos.extra.param.key.form_name
let formName: string = want.parameters[CommonConstants.FORM_PARAM_NAME_KEY] as string;
// 獲取卡片規(guī)格:ohos.extra.param.key.form_dimension
let dimensionFlag: number = want.parameters[CommonConstants.FORM_PARAM_DIMENSION_KEY] as number;
// 卡片信息
let form: Form = new Form();
form.formId = formId;
form.formName = formName;
form.dimension = dimensionFlag;
// 保存卡片信息到數(shù)據(jù)庫(kù)
DatabaseUtils.insertForm(this.context, form);
// 獲取最優(yōu)成績(jī)
getScoreById(this.context, dimensionFlag, formId);
// 每五分鐘刷新一次
formProvider.setFormNextRefreshTime(formId, CommonConstants.FORM_NEXT_REFRESH_TIME, (error, data) => {
if (error) {
console.error(CommonConstants.ENTRY_FORM_ABILITY_TAG, 'xx onAddForm 更新卡片失?。? + JSON.stringify(error))
} else {
console.info(CommonConstants.ENTRY_FORM_ABILITY_TAG, 'xx onAddForm 更新卡片成功')
}
});
// 返回初始化卡片數(shù)據(jù)
let formData: FormData = new FormData();
formData.formId = formId;
formData.bestScore = 0;
formData.matrixNum = '1x1';
formData.totalBestScore = 0;
return formBindingData.createFormBindingData(formData);
}
6.3. 主界面調(diào)用公共類
@Entry
@Component
struct Index {
@State scoreDataList: Array<FormData> = []
aboutToAppear() {
// 請(qǐng)求通知欄權(quán)限
this.requestNotification();
// 更新卡片信息
DatabaseUtils.updateForms(getContext(this));
// 獲取成績(jī)歷史記錄
this.getScoreListData()
}
onPageShow() {
// 更新卡片信息
DatabaseUtils.updateForms(getContext(this));
// 獲取成績(jī)歷史記錄
this.getScoreListData()
}
// 獲取成績(jī)歷史數(shù)據(jù)
getScoreListData() {
DatabaseUtils.getScoreListData(getContext(this))
.then((res) => {
this.scoreDataList = res;
// 發(fā)送通知
NotificationUtils.sendNotifications(this.scoreDataList[0].totalBestScore);
}).catch((error) => {
console.error(CommonConstants.MAIN_PAGE_TAG, 'xx aboutToAppear or onPageShow getScoreListData error ' + JSON.stringify(error));
});
}
build() {...}
}
7.總結(jié)
通過(guò)翻牌小游戲元服務(wù)使用Serverless云函數(shù)、云數(shù)據(jù)庫(kù),學(xué)習(xí)到不少知識(shí),開始時(shí)不懂得怎么使用云函數(shù)調(diào)用云數(shù)據(jù)庫(kù),一邊參考官方商城模板,一邊測(cè)試,到使用到這個(gè)小游戲上, 總結(jié)這個(gè)項(xiàng)目用到以下知識(shí)點(diǎn):
- 使用Notification發(fā)布通知。
- 使用端云一體化開發(fā)、開發(fā)云函數(shù)、開發(fā)云數(shù)據(jù)庫(kù)。
- 使用FormExtensionAbility創(chuàng)建、更新、刪除元服務(wù)卡片。
各位也可以點(diǎn)擊元服務(wù)官網(wǎng),了解更多相關(guān)信息。
元服務(wù)官網(wǎng)鏈接: https://developer.huawei.com/consumer/cn/harmonyos/fa?ha_source=yuanfuwuGW&ha_sourceld=89000452
?著作權(quán)歸作者所有:來(lái)自51CTO博客作者狼哥Army的原創(chuàng)作品,請(qǐng)聯(lián)系作者獲取轉(zhuǎn)載授權(quán),否則將追究法律責(zé)任 鴻蒙萬(wàn)能卡片開發(fā)詳解-記憶翻牌游戲 https://blog.51cto.com/u_15008042/6972493