舒尔特方格游戏-关系型数据库版本,请移步到上一篇帖子查看,本篇帖子内容还是舒尔特方格游戏,只是换了后宫_云数据库。
此项目是用最新版 DevEco Studio 3.1 Release 并创建端云一体开发,由于目前此版本不支持直接调用云数据库,不过可以通过云函数调用云数据库。
也就是在服务卡片业务逻辑里通过调用云函数来完成游戏数据保存到云数据库,开发工具支持本地函数调用测试,大大方便了开发。
此贴重点讲解云函数和云数据库开发,本地和远端调用,从而进一步学习 Serverless 知识。
舒尔特方格游戏效果图如下:
知识点
为丰富 HarmonyOS 对云端开发的支持、实现 HarmonyOS 生态端云联动,DevEco Studio 推出了云开发功能,开发者在创建工程时选择云开发模板。
即可在 DevEco Studio 内同时完成 HarmonyOS 应用/服务的端侧与云侧开发,体验端云一体化协同开发。
相比于传统开发模式,云开发模式具备成本低、效率高、门槛低等优势,具体区别见下表。
①开发流程
HarmonyOS 应用端云一体化开发流程如下图所示:
②创建端云一体化开发工程
新建原子化服务工程:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-createproject-0000001443369760-V3#section15198822192610
工程初始化配置:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-createproject-0000001443369760-V3#section1938317533494
端云一体化开发工程介绍:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-createproject-0000001443369760-V3#section20250910164411
③开发云工程
开发云函数:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-cloudfunctions-0000001493089797-V3
开发云数据库:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-clouddb-0000001443049860-V3
④部署云工程
部署云工程:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/agc-harmonyos-clouddev-deploy-0000001493209765-V3
小结:了解这些端云一体化开发知识点后,下面围绕舒尔特方格游戏,在云数据库里设计卡片表结构和成绩表结构,然后再编写相关云函数,并在本地测试通过后,再测试远程,最后在元服务业务逻辑调用云函数。
云数据库开发讲解
①objecttype 创建
展开 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": [...]}
展开 CloudProgram→clouddb→objecttype 右击 objecttype 目录,创建→Cloud DB Object Type 输入 Object Type Name 为 t_score,点击确认。
修改内容如下:
{ "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_score", "permissions": [...]}
②dataentry 创建
展开 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 } ]}
展开 CloudProgram→clouddb→dataentry 右击 dataentry 目录,创建→Cloud DB Data Entry 这里先选择上面创建的 Object Type为t_score,再输入 Data Entry Name 为 score_data,点击确认。
修改内容如下:
{ "cloudDBZoneName": "widgetCard", "objectTypeName": "t_score", "objects": [ { "formId": "x000001", "matrixNum": "3x3", "bestScore": 2.234 } ]}
小结:其实 dataentry 文件可以不创建,这里对两个表都初始化了一条数据,是方便下面的调用使用,云数据库就是定义好表结构、权限配置就可以,数据的添加、修改、删除、查询都可以通过云函数来完成。
云函数开发讲解
①卡片云函数创建
展开 CloudProgram→cloudfunctions 右击 cloudfunctions 目录,创建→Cloud Function 输入 Cloud Function Name 为 form-func,点击确认。
卡片云函数里包含了增删改查操作,所以在 form-func 下,创建不同的文件夹来区分,目录结构如下:
首先说一下与云数据库交互文件,t_form.js 对应的是云数据库实体类,如各属性的 get 和 set 方法,之前 FA 模式下的 DevEco Studio 端云一体化开发,支持直接调用云数据库。
现在 Stage 模式下的 DevEco Studio 端云一体化开发,还不支持直接调用云数据库,通过云函数来调用。
所以这里的云数据库实体类,除了属性的 get 和 set 方法外,还要手工添加一些方法。
如卡片实例体类:
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}
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); } }}
新增卡片函数 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" }, }); }};
卡片云函数主入口,关键代码如下:
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;
②成绩云函数创建
展开 CloudProgram→cloudfunctions 右击 cloudfunctions 目录,创建→Cloud Function 输入 Cloud Function Name 为 score-func,点击确认,。
成绩云函数里包含了增删改查操作,所以在 score-func 下,创建不同的文件夹来区分。
目录结构如下:
成绩表云数据库操作与卡片操作一样,这里就不在重复了,可以参考一下上面卡片操作方法就可以。
云函数本地与远程调试
①Run 模式启动调试
右击“cloudfunctions”目录,选择“Run Cloud Functions”。
查看“Run”面板。如果出现“Cloud Functions loaded successfully”,表示所有函数已成功加载到本地运行的 HTTP Server 中,并生成对应的 POST URL。
在菜单栏选择“Tools > CloudDev > Cloud Functions Requestor”,使用 Cloud Functions Requestor 触发函数调用。
在弹出的“Cloud Functions Requestor”面板,填写触发事件参数。
点击“Save”,可保存当前触发事件。
②Debug 模式启动调试
右击“cloudfunctions”目录,选择“Run Cloud Functions”。
查看 Console 面板。如果出现“Cloud Functions loaded successfully”,表示函数成功加载到本地运行的 HTTP Server 中,并生成对应的 POST URL。
如需设置断点调试,在函数代码中选定要设置断点的有效代码行,在行号后单击鼠标左键设置断点,设置断点后,调试能够在断点处中断,并高亮显示该行。
在菜单栏选择“Tools > CloudDev > Cloud Functions Requestor”,使用 Cloud Functions Requestor 触发函数调用。
在弹出的“Cloud Functions Requestor”面板,填写触发事件参数。
点击“Save”,可保存当前触发事件。
③自定义 Run/Debug 配置
在菜单栏选择“Run > Edit Configurations”。
在“Run/Debug Configurations”窗口,点击+,选择“Cloud Functions”,新增一个 Run/Debug 配置。
自定义 Run/Debug 配置,完成后点击“OK”。
· Name:Run/Debug配置的名称,如“functions-custom1”。· Server Port:HTTP服务端监听端口。默认为“18090”,自定义端口号建议大于1024。勾选“Auto increment”表示如当前端口被占用则端口号自动加“1”。· Environment variables:函数运行的环境变量,为key-value形式。点击“Edit environment variables”按钮,在“Environment Variables”弹窗中点击“+”添加一个环境变量,然后点击“OK”。添加成功后,您便可以将变量配置信息传入到函数执行环境中,用于函数运行时读取。
选择刚刚自定义的 Run/Debug 配置,分别点击 Run 或 Debug。后续调试步骤与默认配置下的调试步骤一致,请分别参见 Run 模式启动调试或 Debug 模式启动调试。
④测试
实现云函数调用云数据库,需要您先部署云工程,云端才会有相关数据及环境变量。
同时,云函数为访问云数据库使用了“PROJECT_CREDENTIAL”环境变量,部署函数到 AGC 云端时,云端会自动配置好“PROJECT_CREDENTIAL”以运行环境变量。
但在本地调试函数时,需要您手动将“PROJECT_CREDENTIAL”环境变量添加到 Run/Debug 配置中。
否则,函数调试代码执行会因获取不到“PROJECT_CREDENTIAL”环境变量而中断。
从 AGC 获取的“PROJECT_CREDENTIAL”环境变量添加到调试配置中。您也可以添加您需要的其他环境变量。
添加完环境变量后,启动函数,再点击 Trigger,就可以看到成功返回数据了。
代码讲解
①云函数调用公共类
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(); // return functionResult.getValue().result; } 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)); } ......}
②卡片 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); }
③主界面调用公共类
@Entry@Componentstruct 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() {...}}
总结
通过把之前小游戏元服务-关系型数据库修改为使用 Serverless 云函数、云数据库,学习到不少知识,开始时不懂得怎么使用云函数调用云数据库,一边参考官方商城模板,一边测试,到使用到这个小游戏上,。
总结这个项目用到以下知识点:
使用 notification 发布通知。
使用端云一体化开发、开发云函数、开发云数据库。
使用 FormExtensionAbility 创建、更新、删除元服务卡片。
备注:资源文件是我在学习云函数调用云数据库写的一个简单实例,有云函数调用云数据库需求的小伙伴可以下载下来参考一下。
用户评论
终于在大平台上玩到舒尔特方格了!
有10位网友表示赞同!
HarmonyOS的开发太棒了,能把老游戏移植过来真不错。
有13位网友表示赞同!
掌机大屏幕下玩舒尔特方格体验更好啊。
有9位网友表示赞同!
云端游戏好稳定,不用担心手机性能问题。
有9位网友表示赞同!
希望以后也能上线其他经典的益智类游戏!
有16位网友表示赞同!
这个游戏界面简洁清晰,操作方便。
有16位网友表示赞同!
舒尔特方格永远的神,太烧脑了!
有10位网友表示赞同!
这个云端开发模式真是太给力了!
有15位网友表示赞同!
手机的游戏体验比电脑的舒服好多!
有17位网友表示赞同!
喜欢这种简单易懂却又让人上瘾的游戏。
有6位网友表示赞同!
这款游戏还能联机挑战好友,真不错!
有9位网友表示赞同!
舒尔特方格一直是我的心头好!有了HarmonyOS就更方便了。
有12位网友表示赞同!
玩儿着玩儿着就入了迷,好久没这么专注过一个游戏了。
有16位网友表示赞同!
这个游戏非常适合碎片时间休闲玩。
有5位网友表示赞同!
对舒尔特方格的了解可以用“入门玩家”来概括,这款游戏让我更加深入学习这个领域。
有19位网友表示赞同!
希望以后可以加入更多游戏模式和挑战。
有16位网友表示赞同!
这个游戏完全符合我的口味!
有5位网友表示赞同!
HarmonyOS真厉害,把那么多开发者资源聚集在一起!
有10位网友表示赞同!
期待下期更新可以完善游戏的功能!
有19位网友表示赞同!
这个平台越来越好用了!值得推荐给所有喜欢舒尔特方格的朋友。
有9位网友表示赞同!