作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。

知识点

  1. 游戏介绍
  2. 游戏规则
  3. 跨设备文件访问
  4. 分布式数据对象跨设备数据同步

效果图

视频请移步到B站观看https://www.bilibili.com/video/BV1ZrvSeDE8Z/?spm_id_from=333....

具体实现

此实例是基于上一篇 九宫格切图 实例开发,九宫格切图完成了从图库选择图片,点击按钮切割出九张图片,并保存在图库里,拼图游戏切图后,可以不用保存到图库里,这里改为保存到分布式目录下,实现跨设备文件访问。

哈哈

游戏介绍

九宫格拼图游戏,作为一种经典的益智游戏,其游戏规则主要围绕在3×3的方格盘上,通过移动八块拼图(其中一个格子为空),最终将拼图全部归位至正确位置。以下是九宫格拼图游戏规则的详细解释:

游戏目标

  • 将八块拼图在3×3的方格盘上正确排列,使得每行、每列都填满,且没有拼图重叠或遗漏。

游戏准备

  • 准备一个3×3的方格盘,其中八个位置放置拼图,剩下一个位置留空作为移动空间。

游戏技巧

  • 从外围开始:由于外围的拼图更容易移动和归位,因此玩家可以从外围的拼图开始入手,逐步向中心推进。
  • 利用空格:空格是移动拼图的关键所在,玩家需要巧妙地利用空格来创造移动的机会和条件。
  • 观察与预判:在移动拼图之前,玩家需要仔细观察整个方格盘的布局和拼图的位置关系,并预判移动后的结果和可能产生的影响。

游戏规则

  1. 初始布局:游戏开始时,八块拼图在方格盘上随机分布,留有一个空格作为移动区域。
  2. 移动规则

    • 玩家每次只能移动一个拼图,且只能将其移动到与其相邻的空格中(上下左右四个方向)。
    • 拼图不能跳过其他拼图直接移动到更远的空格,也不能斜向移动。
  3. 归位要求

    • 玩家需要通过一系列的移动,将八块拼图逐一归位到正确的位置上,使得整个方格盘呈现出一个完整的图案或数字序列(根据不同的游戏版本而定)。
    • 在归位过程中,玩家需要不断观察并思考最佳的移动策略,以减少移动次数并避免陷入无法解开的局面。

游戏代码讲解

游戏代码逻辑参考官方案例 拼图 更详细内容请查看官方案例,这里通过基于拼图游戏,用上跨设备文件访问知识和分布式对象跨设备数据同步知识。

游戏初始化
gameInit(i: number, pictures: PictureItem[]): PictureItem[] {
    let emptyIndex = this.findEmptyIndex(pictures);
    let isGameStart: boolean = AppStorage.get('isGameStart') as boolean;
    if (isGameStart) {
      switch (emptyIndex) {
        case 0:
          if (i === 1 || i === 3) {
            pictures = this.itemChange(i, pictures);
          }
          break;
        case 2:
          if (i === 1 || i === 5) {
            pictures = this.itemChange(i, pictures);
          }
          break;
        case 6:
          if (i === 3 || i === 7) {
            pictures = this.itemChange(i, pictures);
          }
          break;
        case 8:
          if (i === 5 || i === 7) {
            pictures = this.itemChange(i, pictures);
          }
          break;
        case 3:
          switch (i) {
            case emptyIndex + 1:
            case emptyIndex - 3:
            case emptyIndex + 3:
              pictures = this.itemChange(i, pictures);
          }
          break;
        case 1:
          switch (i) {
            case emptyIndex + 1:
            case emptyIndex - 1:
            case emptyIndex + 3:
              pictures = this.itemChange(i, pictures);
          }
          break;
        case 5:
          switch (i) {
            case emptyIndex + 3:
            case emptyIndex - 3:
            case emptyIndex - 1:
              pictures = this.itemChange(i, pictures);
          }
          break;
        case 7:
          switch (i) {
            case emptyIndex + 1:
            case emptyIndex - 3:
            case emptyIndex - 1:
              pictures = this.itemChange(i, pictures);
          }
          break;
        case 4:
          switch (i) {
            case emptyIndex + 1:
            case emptyIndex - 3:
            case emptyIndex - 1:
            case emptyIndex + 3:
              pictures = this.itemChange(i, pictures);
          }
          break;
      }
    }
    return pictures;
  }
查找空图片下标
  findEmptyIndex(pictures: PictureItem[]): number {
    for (let i = 0; i < pictures.length; i++) {
      if (pictures[i].index === this.EMPTY_PICTURE.index) {
        return i;
      }
    }
    return -1;
  }
更改图片
  itemChange(index: number, pictures: PictureItem[]): PictureItem[] {
    let emptyIndex = this.findEmptyIndex(pictures);
    let temp: PictureItem = pictures[index];
    pictures[index] = this.EMPTY_PICTURE;
    pictures[emptyIndex] = new PictureItem(temp.index, temp.fileName);
    return pictures;
  }
开始游戏
  gameBegin(pictures: PictureItem[]): PictureItem[] {
    console.info(`testTag 随机打乱位置 ${pictures?.length}`)
    AppStorage.set<boolean>('isGameStart', true);
    let len = pictures.length;
    let index: number, temp: PictureItem;
    while (len > 0) {
      index = Math.floor(Math.random() * len);
      temp = pictures[len - 1];
      pictures[len - 1] = pictures[index];
      pictures[index] = temp;
      len--;
    }
    return pictures;
  }

跨设备文件访问

分布式文件系统为应用提供了跨设备文件访问的能力,开发者在多个设备安装同一应用时,通过基础文件接口,可跨设备读写其他设备该应用分布式文件路径(/data/storage/el2/distributedfiles/)下的文件。例如:多设备数据流转的场景,设备组网互联之后,设备A上的应用可访问设备B同应用分布式路径下的文件,当期望应用文件被其他设备访问时,只需将文件移动到分布式文件路径即可。

权限添加

配置文件module.json5里添加读取图片及视频权限和修改图片或视频权限。

      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:distributed_data_sync",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
切割图片

这里切割图片时,和上一篇九宫格切图,有小小不同,就是切割到最后一张时,使用空白代替,方便拼图游戏时,做为移动位置。

// 切换为 3x3 张图片
      for (let i = 0; i < this.splitCount; i++) {
        for (let j = 0; j < this.splitCount; j++) {
          let picItem: PictureItem;
          // 如果是切到最后一张
          if (i === this.splitCount - 1 && j === this.splitCount -1) {
            // 最后一张使用空白图片
            picItem = new PictureItem(this.splitCount * this.splitCount, '');
            imagePixelMap.push(picItem);
          } else {
            let width = imageInfo.size.width / this.splitCount;
            // 设置解码参数DecodingOptions,解码获取pixelMap图片对象
            let decodingOptions: image.DecodingOptions = {
              desiredRegion: {
                size: {
                  height: height, // 切开图片高度
                  width: width  // 切开图片宽度
                },
                x: j * width,   // 切开x起始位置
                y: i * height     // 切开y起始位置
              }
            }
            let img: image.PixelMap = await imageSource.createPixelMap(decodingOptions);


            let context = getContext() as common.UIAbilityContext;
            // 保存到图片到分布式目录
            let fileName = await savePixelMap(context, img);
            console.info(`xx [splitPic]Save Picture ${fileName}`)
            // 保存到内存数组里
            imagePixelMap.push(new PictureItem(i * this.splitCount + j, fileName));
          }
        }
      }
图片存储到分布式目录
export async function savePixelMap(context: Context, pm: PixelMap): Promise<string> {
  if (pm === null) {
    return '';
  }
  const imagePackerApi: image.ImagePacker = image.createImagePacker();
  const packOpts: image.PackingOption = { format: 'image/jpeg', quality: 30 };
  try {
    const data: ArrayBuffer = await imagePackerApi.packing(pm, packOpts);
    return await saveFile(context, data);
  } catch (err) {
    return '';
  }
}
async function saveFile(context: Context, data: ArrayBuffer): Promise<string> {
  let fileName: string = new Date().getTime() + ".jpg";

  let dduri: string = context.distributedFilesDir + '/' + fileName;
  let ddfile = fileIo.openSync(dduri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);

  fileIo.writeSync(ddfile.fd, data);
  fileIo.closeSync(ddfile);
  // 只返回文件名,到时分布式对象,只存储文件名,使用时就和分布式目录路径拼接
  return fileName;
}

分布式数据对象跨设备数据同步

分布式数据对象是一个JS对象型的封装。每一个分布式数据对象实例会创建一个内存数据库中的数据表,每个应用程序创建的内存数据库相互隔离,对分布式数据对象的“读取”或“赋值”会自动映射到对应数据库的get/put操作。

分布式数据对象的生命周期包括以下状态:

  • 未初始化:未实例化,或已被销毁。
  • 本地数据对象:已创建对应的数据表,但是还无法进行数据同步。
  • 分布式数据对象:已创建对应的数据表,设备在线且组网内设置同样sessionId的对象数>=2,可以跨设备同步数据。若设备掉线或将sessionId置为空,分布式数据对象退化为本地数据对象。
页面使用@StorageLink存储拼图里图片数据
  1. 页面部分变量声明
  // 使用@StorageLink声明,与EntryAbility里使用分布式对象有关联
  @StorageLink('numArray') numArray: Array<PictureItem> = [];
  @StorageLink('isContinuation') isContinuation: string = 'false';
    // 标识目前是否在游戏
  @StorageLink('isGameStart') isGameStart: boolean = false;
  // 游戏时间,初始化为5分钟
  @StorageLink('gameTime') @Watch('onTimeOver') gameTime: number = 300;
  // 选择图库图片的下标
  @StorageLink('index') @Watch('onImageChange') index: number = 0;
  1. 页面拼图游戏关键代码
      Grid() {
        ForEach(this.numArray, (item: PictureItem, index:number) => {
          GridItem() {
            // 此处通过文件名,到分布式目录下获取图片
            Image(`${fileUri.getUriFromPath(this.context.distributedFilesDir + '/' + item.fileName)}`)
              .width('99%')
              .objectFit(ImageFit.Fill)
              .height(90)
          }
          .id(`image${index}`)
          .backgroundColor(item.fileName === '' ? '#f5f5f5' : '#ffdead')
          .onClick(() => {
            if (this.isRefresh) {
              return;
            }
            if (this.isGameStart) {
              this.isRefresh = true;
              this.numArray = this.game.gameInit(index, this.numArray);
              this.gameOver();
              this.isRefresh = false;
            }
          })
        }, (item: PictureItem) => JSON.stringify(item))
      }
      .id('IMAGE_GRID')
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(2)
      .rowsGap(2)
      .backgroundColor('#ccc')
      .width('100%')
      .height('100%')
EntryAbility关键代码
  1. 相关权限检查
  async checkPermissions(): Promise<void> {
    const accessManager = abilityAccessCtrl.createAtManager();
    try {
      const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
      const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags);
      const grantStatus = await accessManager.checkAccessToken(bundleInfo.appInfo.accessTokenId, permissions[0]);
      if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
        accessManager.requestPermissionsFromUser(this.context, permissions);
      }
    } catch (err) {
      console.error('EntryAbility', 'checkPermissions', `Catch err: ${err}`);
      return;
    }
  }
  1. 自由流转前,数据存储
  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
    wantParam.isGameStart = AppStorage.get('isGameStart') as string;
    wantParam.gameTime = AppStorage.get('gameTime') as string;
    wantParam.isContinuation = AppStorage.get('isContinuation') as string;
    wantParam.index = AppStorage.get('index') as string;
    try {
      let sessionId: string = distributedDataObject.genSessionId();

      if (this.localObject) {
        this.localObject.setSessionId(sessionId);
        let numArrayStr: string = JSON.stringify(AppStorage.get<Array<PictureItem>>('numArray'));
        hilog.info(0x0000, 'testTag', '%{public}s', '**onContinue numArrayStr: ' + numArrayStr);
        this.localObject['numArray'] = AppStorage.get<Array<PictureItem>>('numArray'); //numArrayStr;
        this.targetDeviceId = wantParam.targetDevice as string;
        this.localObject.save(wantParam.targetDevice as string).then(() => {
          hilog.info(0x0000, 'testTag', '%{public}s', 'onContinue localObject save success');
        }).catch((err: BusinessError) => {
          hilog.error(0x0000, 'testTag', '%{public}s', `Failed to save. Code:${err.code},message:${err.message}`);
        });
      }
      wantParam.distributedSessionId = sessionId;
    } catch (error) {
      hilog.error(0x0000, 'testTag', '%{public}s distributedDataObject failed', `code ${(error as BusinessError).code}`);
    }
    return AbilityConstant.OnContinueResult.AGREE;
  }
  1. 自由流转后,数据恢复
async restoringData(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 检查相关权限
    this.checkPermissions();
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      AppStorage.setOrCreate<string>('isContinuation', 'true');
      AppStorage.setOrCreate<boolean>('isGameStart', want.parameters?.isGameStart as boolean);
      AppStorage.setOrCreate<number>('gameTime', want.parameters?.gameTime as number);
      AppStorage.setOrCreate<number>('index', want.parameters?.index as number);
      let sessionId : string = want.parameters?.distributedSessionId as string;
      if (!this.localObject) {
        let imageInfo: ImageInfo = new ImageInfo(undefined);
        this.localObject = distributedDataObject.create(this.context, imageInfo);
        this.localObject.on('change', this.changeCall);
      }
      if (sessionId && this.localObject) {
        await this.localObject.setSessionId(sessionId);
        let numArrayStr: string = JSON.stringify(this.localObject['numArray']);
        hilog.info(0x0000, 'testTag', '%{public}s', '**restoringData numArrayStr: ' + numArrayStr);
        AppStorage.setOrCreate<Array<PictureItem>>('numArray', this.localObject['numArray']);
      }
      this.context.restoreWindowStage(new LocalStorage());
    }
  }
  1. 在onCreate生命周期时, 调用数据恢复
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    await this.restoringData(want, launchParam);
  }
  1. 在onNewWant生命周期时, 调用数据恢复
  async onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onNewWant');
    await this.restoringData(want, launchParam);
  }
  1. 在onWindowStageCreate生命周期,创建分布式对象
  onWindowStageCreate(windowStage: window.WindowStage): void {
    ......
    if (!this.localObject) {
      let imageInfo: ImageInfo = new ImageInfo(undefined);
      this.localObject = distributedDataObject.create(this.context, imageInfo);
    }
  }
  1. 在onDestroy生命周期,保存对象,保存对象数据成功后,应用退出时对象数据不会释放,应用退出后,在保存的设备上恢复数据。
  async onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
    if (this.localObject && this.targetDeviceId) {
      await this.localObject.save(this.targetDeviceId).then(() => {
        hilog.info(0x0000, 'testTag', 'onDestroy localObject save success');
      }).catch((err: BusinessError) => {
        hilog.error(0x0000, 'testTag', `Failed to save. Code:${err.code},message:${err.message}`);
      });
    }
  }

总结

通过此案例,可以回顾 九宫格切图 案例,同时学习到跨设备文件访问知识点和分布式对象跨设备数据同步知识点,通过简单的案例,运用上各方面的知识点,为之后的大项目做好准备,大家动手起来吧!!!

相关权限

读取图片及视频权限:ohos.permission.READ_IMAGEVIDEO

修改图片或视频权限:ohos.permission.WRITE_IMAGEVIDEO

允许不同设备间的数据交换权限:ohos.permission.DISTRIBUTED_DATASYNC

约束与限制

1.本示例仅支持标准系统上运行,支持设备:华为手机。

2.HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。

3.DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。

4.HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。


狼哥
1 声望0 粉丝