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

狼哥Army
发布于 2023-8-5 07:44
浏览
5收藏

@toc

1. 前言

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

2. 知识点

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

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

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

2.1. 开发流程

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

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

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下,创建不同的文件夹来区分,目录结构如下:
鸿蒙万能卡片开发详解-记忆翻牌游戏-鸿蒙开发者社区

      4.1.2 首先说一下与云数据库交互文件,t_form.js对应的是云数据库实体类,如各属性的get和set方法,之前FA模式下的DevEco Studio端云一体化开发,支持直接调用云数据库,现在Stage模式下的DevEco Studio端云一体化开发,还不支持直接调用云数据库,通过云函数来调用,所以这里的云数据库实体类,可以通过AGC导出,然后复制到t_form文件内,导出步骤图:
鸿蒙万能卡片开发详解-记忆翻牌游戏-鸿蒙开发者社区

如卡片实例体类:

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下,创建不同的文件夹来区分,目录结构如下:
鸿蒙万能卡片开发详解-记忆翻牌游戏-鸿蒙开发者社区

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

5. 元服务开发

5.1. 1*2卡片开发

​ 5.1.1 创建卡片步骤:

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

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

​ 5.1.2 卡片模板创建好后,修改为翻牌游戏UI, 就是左边显示一张奖牌图片,右边显示最快记录时间,图片效果为:
鸿蒙万能卡片开发详解-记忆翻牌游戏-鸿蒙开发者社区

​ 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张卡片,图片效果为:
鸿蒙万能卡片开发详解-记忆翻牌游戏-鸿蒙开发者社区

​ 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

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2023-8-5 07:44:52修改
8
收藏 5
回复
举报
4条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

期待能用端云一体跑大型游戏的一天

回复
2023-8-5 14:48:25
皮皮虾233
皮皮虾233

很详细的教学!

回复
2023-8-7 16:00:57
Tuer白晓明
Tuer白晓明

狼哥发文必须支持一波👍👍👍👍👍

回复
2023-8-8 09:44:49
时空未宇
时空未宇

泰裤辣😉😉

回复
2023-8-11 08:39:16
回复
    相关推荐