首页 > 其他分享 >鸿蒙万能卡片开发详解-记忆翻牌游戏

鸿蒙万能卡片开发详解-记忆翻牌游戏

时间:2023-08-18 12:35:48浏览次数:46  
标签:卡片 form formId 详解 let 鸿蒙 params logger 翻牌

(目录)

1. 前言

      翻牌游戏万能卡片,随机生成16张共包含8张完全不同的图像,游戏的目标是在有限30秒时间内,将16张卡片中包含相同的图像的卡片两两配对。匹配的规则是连续点击两张卡片,若卡背面的图像相同,则匹配成功,若不同则配对失败。游戏主要考察玩家的记忆力,因为游戏还规定翻开的卡片数量至多有两张,否则一开始被点击而翻开的卡片将再次盖上(若该张卡片没有匹配成功)。此项目是用最新版DevEco Studio 3.1 Release并创建端云一体开发,由于目前此版本不支持直接调用云数据库,不过可以通过云函数调用云数据库,也就是在服务卡片业务逻辑里通过调用云函数来完成游戏数据保存到云数据库,开发工具支持本地函数调用测试,大大方便了开发,此贴重点讲解云函数和云数据库开发,从而进一步学习Serverless知识,翻牌游戏万能卡片效果图如下: 1690979276278.gif

2. 知识点

      为丰富HarmonyOS对云端开发的支持、实现HarmonyOS生态端云联动,DevEco Studio推出了云开发功能,开发者在创建工程时选择云开发模板,即可在DevEco Studio内同时完成HarmonyOS应用/服务的端侧与云侧开发,体验端云一体化协同开发。

相比于传统开发模式,云开发模式具备成本低、效率高、门槛低等优势,具体区别见下表。

区别点 传统开发模式 云开发模式
开发工具 端侧与云侧各需一套开发工具,云侧需自建服务器,工具成本高。 DevEco Studio一套开发工具即可支撑端侧与云侧同时开发,无需搭建服务器,工具成本低。
开发人员 端侧与云侧要求不同的开发语言,技能要求高。需多人投入,且开发人员之间需持续、准确沟通,人力与沟通成本高、效率低。 依托AppGallery Connect(以下简称AGC)Serverless云服务开放的接口,端侧开发人员也能轻松开发云侧代码,大大降低开发门槛。开发人员数量少,降低人力成本,提高沟通效率。
运维 需自行构建运营与运维能力,成本高、负担重。 直接接入AGC Serverless云服务,实现免运维,无运维成本或资源浪费。

2.1. 开发流程

      HarmonyOS应用端云一体化开发流程如下图所示。

img

2.2. 创建端云一体化开发工程

      2.2.1 新建原子化服务工程

      2.2.2 工程初始化配置

      2.2.3 端云一体化开发工程介绍

2.3. 开发云工程

      2.3.1 开发云函数

      2.3.2 开发云数据库

2.4. 部署云工程

      2.4.1 部署云工程

2.5. 小结

      了解这些端云一体化开发知识点后,下面围绕翻牌游戏万能卡片,在云数据库里设计卡片表结构和游戏记录表结构,然后再编写相关云函数,在元服务业务逻辑调用云函数。

3. 云数据库开发讲解

3.1. objecttype创建

      3.1.1 展开CloudProgram -> clouddb -> objecttype 右击objecttype目录,创建 -> Cloud DB Object Type 输入Object Type Name为t_form,点击确认,代码内容如下:

{
  "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目录,创建 -> Cloud DB Object Type 输入Object Type Name为t_record,点击确认,代码内容如下:

{
  "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创建

      3.2.1 展开CloudProgram -> clouddb -> dataentry 右击dataentry目录,创建 -> Cloud DB Data Entry 这里先选择上面创建的Object Type为t_form,再输入Data Entry Name为form_data,点击确认,代码内容如下:

{
  "cloudDBZoneName": "widgetCard",
  "objectTypeName": "t_form",
  "objects": [
    {
      "formId": "x000001",
      "formName": "卡片1",
      "dimension": 2
    }
  ]
}

      3.2.2 展开CloudProgram -> clouddb -> dataentry 右击dataentry目录,创建 -> Cloud DB Data Entry 这里先选择上面创建的Object Type为t_record,再输入Data Entry Name为record_data,点击确认,修改内容如下:

{
  "cloudDBZoneName": "widgetCard",
  "objectTypeName": "t_record",
  "objects": [
    {
      "formId": "x000001",
      "matrixNum": "4x4",
      "bestScore": 2.234
    }
  ]
}

3.3. 小结

      其实dataentry文件可以不创建,这里对两个表都初始化了一条数据,是方便下面的调用使用,云数据库就是定义好表结构、权限配置就可以,数据的添加、修改、删除、查询都可以通过云函数来完成。

4. 云函数开发讲解

4.1. 卡片云函数创建

      4.1.1 展开CloudProgram -> cloudfunctions 右击cloudfunctions目录,创建 -> Cloud Function 输入Cloud Function Name为form,点击确认, 卡片云函数里包含了增删改查操作,所以在form下,创建不同的文件夹来区分,目录结构如下: RU8HXCT94ZCQBR.png

      4.1.2 首先说一下与云数据库交互文件,t_form.js对应的是云数据库实体类,如各属性的get和set方法,之前FA模式下的DevEco Studio端云一体化开发,支持直接调用云数据库,现在Stage模式下的DevEco Studio端云一体化开发,还不支持直接调用云数据库,通过云函数来调用,所以这里的云数据库实体类,可以通过AGC导出,然后复制到t_form文件内,导出步骤图: NZ34Q5TFU5JZLMCEZVJ8.png

如卡片实例体类:

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操作云数据库,这里主要列举构造函数和增加方法内容:

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实例
      let cloudDbInstance;
      try {
        cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
      } catch {
        clouddb.AGConnectCloudDB.initialize(agcClient);
        cloudDbInstance = clouddb.AGConnectCloudDB.getInstance(agcClient);
      }
      // 创建CloudDBZoneConfig配置对象,并设置云侧CloudDB zone名称,打开Cloud DB zone实例
      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 新增卡片函数form-insert,关键代码如下:

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 卡片云函数主入口,关键代码如下:

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. 记录云函数创建

      4.2.1 展开CloudProgram -> cloudfunctions 右击cloudfunctions目录,创建 -> Cloud Function 输入Cloud Function Name为record,点击确认, 成绩云函数里包含了增删改查操作,所以在record下,创建不同的文件夹来区分,目录结构如下: O5R5JCNRNB74J9JA~89N.png

      4.2.2 记录表云数据库操作与卡片操作一样,这里就不在重复了,可以参考一下上面卡片操作方法就可以。

5. 元服务开发

5.1. 1*2卡片开发

​ 5.1.1 创建卡片步骤:

CR4_8PXORBGT0V`5AVR5.png

1AE_XV4~NA8TP1~AI`THT.png 49QEICXVFTJYYU_4ZUKE.png

​ 5.1.2 卡片模板创建好后,修改为翻牌游戏UI, 就是左边显示一张奖牌图片,右边显示最快记录时间,图片效果为: WBNFF~DHUY`LSVZP9I.png

​ UI代码如下:

build() {
    Row() {
      Image($r('app.media.cup'))
        .width(32).height(32).objectFit(ImageFit.Cover)
      Text(`最快成绩:${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卡片开发

​ 5.2.1 创建卡片步骤如上面步骤。

​ 5.2.2 卡片模板创建好后,修改为翻牌游戏UI, 就是顶部显示游戏信息,如:游戏标题,当前用时,倒计时,开始游戏,中部显示16张卡片,图片效果为: IM7EKT91X1DVYTNXUWB.png

​ UI部分代码如下:

build() {
    Column() {
      Row() {
        Text('记忆翻牌游戏')
        // Text(`最快:${this.totalBestScore}'s`)
        //   .fontSize(10)
        Text(`当前:${this.tookTime}'s`)
          .fontSize(10)
        Text(`倒计时:${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. 云函数调用公共类

      DatabaseUtils.ets云函数操作类部分代码如下:

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);
  }

  /**
   * 插入卡片数据。
   *
   * @param{Form}Form表单实体。
   * @param{DataRdb.RdbStore}RDB存储RDB数据库。
   * @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调用公共类

      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;
    // 获取卡片规格: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;

    // 保存卡片信息到数据库
    DatabaseUtils.insertForm(this.context, form);
    // 获取最优成绩
    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 更新卡片成功')
      }
    });

    // 返回初始化卡片数据
    let formData: FormData = new FormData();
    formData.formId = formId;
    formData.bestScore = 0;
    formData.matrixNum = '1x1';
    formData.totalBestScore = 0;
    return formBindingData.createFormBindingData(formData);
  }

6.3. 主界面调用公共类

@Entry
@Component
struct Index {
  @State scoreDataList: Array<FormData> = []

  aboutToAppear() {
    // 请求通知栏权限
    this.requestNotification();
    // 更新卡片信息
    DatabaseUtils.updateForms(getContext(this));
    // 获取成绩历史记录
    this.getScoreListData()
  }
  onPageShow() {
    // 更新卡片信息
    DatabaseUtils.updateForms(getContext(this));
    // 获取成绩历史记录
    this.getScoreListData()
  }
    // 获取成绩历史数据
  getScoreListData() {
    DatabaseUtils.getScoreListData(getContext(this))
      .then((res) => {
        this.scoreDataList = res;
        // 发送通知
        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. 总结

      通过翻牌小游戏元服务使用Serverless云函数、云数据库,学习到不少知识,开始时不懂得怎么使用云函数调用云数据库,一边参考官方商城模板,一边测试,到使用到这个小游戏上, 总结这个项目用到以下知识点:

      1. 使用Notification发布通知。       2. 使用端云一体化开发、开发云函数、开发云数据库。       3. 使用FormExtensionAbility创建、更新、删除元服务卡片。

各位也可以点击元服务官网,了解更多相关信息。 元服务官网链接:https://developer.huawei.com/consumer/cn/harmonyos/fa?ha_source=yuanfuwuGW&ha_sourceld=89000452

本文作者:狼哥Army

想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com/#bkwz​

标签:卡片,form,formId,详解,let,鸿蒙,params,logger,翻牌
From: https://blog.51cto.com/harmonyos/7133369

相关文章

  • JS数据类型详解
    JS的数据类型分为基本数据类型+引用数据类型基本数据类型:number,boolean,string,null,undefined, symbol(独一无二并且不可变的数据类型),bigint引用数据类型: Function,Array,Object区别:基本数据类型由于所占内存大小可控所以放于栈中,引用数据类型所占空间不固定放于堆中,并生......
  • 4G工业路由器的功能与选型!详解工作原理、关键参数、典型品牌
    随着工业互联网的发展,4G工业路由器得到越来越广泛的应用。但是如何根据实际需求选择合适的4G工业路由器,是许多用户关心的问题。为此,本文将深入剖析4G工业路由器的工作原理、重要参数及选型要点,并推荐优质的品牌及产品,以提供选型参考。  一、4G工业路由器的工作原理4G......
  • C++ 多线程详解之异步编程 std::packaged_task
    std::packaged_task将任何可调用对象(比如函数、lambda表达式等等)封装成一个task,可以异步执行。执行结果可以使用std::future获取。比如下面的例子,构造一个std::packaged_task后,get_future()函数返回一个std::future对象,可以获取task异步或者同步执行的结果。#includ......
  • java中volatile关键字详解
    简介volatile是Java语言中的一种轻量级的同步机制,它可以确保共享变量的内存可见性,也就是当一个线程修改了共享变量的值时,其他线程能够立即知道这个修改。跟synchronized一样都是同步机制,但是相比之下,synchronized属于重量级锁,volatile属于轻量级锁。JMM概述JMM就是Java内存模型(Jav......
  • vue3 vue.config.js配置详解
    //vue.config.js文件是用于VueCLI项目的全局配置的module.exports={  //部署应用包时的公共路径  publicPath:"./",  //生产环境构建文件的目录名(默认为dist)  outputDir:"dist",  //放置生成的静态资源的目录(默认为dist/static),可以修改为public。  assetsDir......
  • 【HarmonyOS】鸿蒙应用获取华为帐号手机号码步骤(API7及以下)
    ​【写在前面】本文主要介绍使用API7及以下版本开发HarmonyOS应用时,通过华为帐号SDK和云侧接口获取手机号码的主要开发步骤,注意:开发过程中集成的华为帐号SDK仅支持API7及以下版本的HarmonyOS应用。 【前提准备】1、HarmonyOS应用已申请获取手机号码的权限,申请权限文档请参考......
  • 互斥量概念、用法、死锁演示及解决详解
    互斥量概念、用法、死锁演示及解决详解视频:https://www.bilibili.com/video/BV1Yb411L7ak?p=7&vd_source=4c026d3f6b5fac18846e94bc649fd7d0参考文章:https://blog.csdn.net/qq_38231713/article/details/106091902互斥量(mutex)如果想深入了解可以具体看一下操作系统互斥量的讲......
  • Gson与FastJson详解
    Gson与FastJson详解Java与JSON做什么?将Java中的对象快速的转换为JSON格式的字符串.将JSON格式的字符串,转换为Java的对象.Gson将对象转换为JSON字符串转换JSON字符串的步骤:引入JAR包在需要转换JSON字符串的位置编写如下代码即可:Stringjson=newGson().toJSON(要转换的对象......
  • toggleClass详解
    toggleClass()是一个jQuery方法,用于在元素之间切换一个或多个类。该方法的语法如下:$(selector).toggleClass(class1,class2,...)selector:表示要切换类的元素选择器。class1,class2,...:要切换的一个或多个类名。该方法的作用是,在被选元素上添加指定的类,如果元素已经有......
  • ConcurrentHashMap 源码详解
    ConcurrentHashMap是Java提供的一个并发散列映射实现,它允许多个线程同时读写而不需要同步整个数据结构。它是线程安全的,并且相比于其他线程安全的Map实现(如Collections.synchronizedMap或Hashtable),它提供了更高的并发性能。以下是ConcurrentHashMap的一些核心特性和相应......