管理中心
我的

此版本文档已归档不再维护,推荐您使用最新版本。

指南游戏服务应用功能开发存档

游戏存档

1. 场景介绍

概念介绍

您可以将玩家的游戏进度存储至华为云空间中,或从华为云空间取得之前的游戏进度继续游戏。因此,只要用户使用相同的华为帐号登录,则游戏存档可以在任意设备取回进行游戏,即使用户之前的设备丢失、损毁或换了新设备也能继续之前的游戏进度。

一个存档主要由2部分组成:

  • 存档文件:开发者可以将游戏数据自行写入需要存档的文件,在获取存档文件后也由开发者自行解析,存档文件大小限制为3MB。

  • 存档元数据:存档元数据是指存档本身的属性,开发者可以向用户展示存档属性,用户可以根据属性信息选择哪个存档继续游戏,比如存档名称、上次修改时间等。存档的主要属性如下表所示。

展开

属性

说明

存档ID

存档在该游戏中的唯一标识,在提交存档后由华为游戏服务器生成。

存档名称

存档保存的文件名,在提交存档后由华为服务器生成。

存档描述

存档用于展示给玩家的描述信息,在提交存档时由开发者自行定义,最多1000个字符。

上次修改时间

服务器生成的时间戳(以毫秒为单位),表示上次更新保存的游戏的时间。

游戏时长

开发者在提交存档时提供的玩家在该存档上游戏总时间(以毫秒为单位)。

游戏进度

开发者在提交存档时提供的玩家在该存档上的游戏进度,由开发者自行定义。比如第几关等,用整数进行识别。

封面图片

开发者在提交存档时提供的存档封面图像,一般来说为游戏存档时的截图,若未提供,则为系统默认图片,最大200KB,支持JPG或PNG格式,长宽比为16:9。

离线同步

  • 没有网络连接时,支持本地缓存的方式读取和保存游戏存档。

  • 读取和写入的均为本地缓存数据,网络重新连接后,自动与华为服务器进行同步。

  • 离线同步仅对有登录华为帐号的用户生效,否则无法离线存储。

  • 若在再次联网前用户进行华为帐号登出,则本地缓存的存档被丢弃。

冲突处理

在游戏存档时可能存在同一个帐号在多个设备中同时存档的情况,如果设备因网络问题存档延时,则可能会发生数据冲突。当更新存档或打开存档时发现两份存档的存档属性对比存在不一致时,华为游戏服务器会返回对应的冲突结果给开发者,并允许开发者选择使用其中某份存档作为最终使用的存档,或者也允许开发者自主混合存档数据作为最终使用的存档。

  • 冲突判断有两个场景,更新存档和打开存档,判断条件有多个,之间为【或】的关系,即只要有一个条件满足,即判断为冲突,同时告知开发者冲突原因。冲突判断条件参见下表。

  • 若有存档对应字段不存在无法进行判断,则认为存在冲突,且认为另一个存档对应字段更大。

展开

操作场景

冲突判断条件

更新存档

存入存档为A,服务器已存在存档为B。

  • B的游戏时长更大

  • B的游戏进度更大

  • B的上次修改时间距现在更近

  • A的描述信息或封面与B不同

打开存档

缓存中已有存档为A,服务器取回存档为B。

  • A的游戏时长更大

  • A的游戏进度更大

  • A的上次修改时间距现在更近

  • A的描述信息或封面与B不同

解决冲突的方式有2种:

  • 开发者在打开存档时指定冲突解决策略,在发生冲突时由华为游戏服务器按冲突解决策略自动解决冲突。

  • 开发者在发生冲突时自行选择冲突解决方案,可以选择其中任何一个作为最终使用的存档,也允许开发者混合编辑数据后,使用编辑后的新存档。

2. 受限说明

  • 游戏存档实际是存储游戏相关文件至当前用户的华为帐号所属的华为云空间中,因此用户需要同意开通华为云空间服务,才能使用游戏存档功能。游戏存档当前仅支持华为云空间服务支持的国家和地区,具体请参见华为云空间服务支持的地区。

  • 调用存档相关接口时,用户的华为帐号必须为已登录状态。

  • 单个用户在华为云空间剩余存储容量充足情况下,最多可以保存100个存档。

  • 用户必须打开华为应用市场(10.3及以上版本)中的游戏服务开关才能使用存档功能。如果用户没有开启游戏服务开关,当游戏调用存档相关接口时HMS SDK会自动拉起华为应用市场的游戏服务开关页面引导用户打开开关,如果用户未打开游戏服务开关,将返回7018错误码,游戏需要主动提示用户到华为应用市场“我的-设置-游戏服务”页面中开启游戏服务开关后才能使用存档功能。

说明:
用户打开游戏服务授权后,建议您在游戏中提供取消游戏服务授权的功能,可调用GamesClient.cancelGameService方法实现。

3. 初始化ArchivesClient实例

1、如果需要使用存档功能,在玩家登录时需要调用HuaweiIdAuthParamsHelper.setScopeList申请DRIVE_APP_DATA的SCOPE。示例代码如下:

收起
自动换行
深色代码主题
复制
  1. List<Scope> scopes = new ArrayList<>();
  2. scopes.add(GameScopes.DRIVE_APP_DATA);
  3. HuaweiIdAuthParams huaweiIdAuthParams = new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM_GAME).setScopeList(scopes).createParams();

2、在使用存档接口的能力前都需要先调用Games.getArchiveClient()获取ArchivesClient实例。示例代码如下:

收起
自动换行
深色代码主题
复制
  1. ArchivesClient client = Games.getArchiveClient(this, mAuthHuaweiId);

接口中传入的mAuthHuaweiId对象在用户登录游戏时获取,如果当前华为帐号登录状态已失效,则需要调用HuaweiIdAuthService.getSignInIntent方法再次登录或调用HuaweiIdAuthService.silentSignIn方法静默授权获取AuthHuaweiId。

4. 新建存档

新建存档是指玩家保存游戏时,游戏在服务器端新建新的存档文件和存档元数据。

1、 玩家在游戏过程中选择存档,此时应用可以先查询华为服务器对存档文件的限制。

收起
自动换行
深色代码主题
复制
  1. Task<Integer> thumbnailSizeTask = Games.getArchiveClient(this, mAuthHuaweiId) .getLimitThumbnailSize();
  2. thumbnailSizeTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
  3.    @Override
  4.    public void onSuccess(Integer integer) {
  5.        int maxThumbnailSize = integer;
  6.    }
  7. }).addOnFailureListener(new OnFailureListener() {
  8.    @Override
  9.    public void onFailure(Exception e) {
  10.        if (e instanceof ApiException) {
  11.            ApiException apiException = (ApiException) e;
  12.            Log.e("archive","statusCode:" + apiException.getStatusCode());
  13.        }
  14.    }
  15. });
  16. Task<Integer> detailSizeTask = Games.getArchiveClient(this, signInHuaweiId) .getLimitDetailsSize();
  17. detailSizeTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
  18.    @Override
  19.    public void onSuccess(Integer integer) {
  20.        int detailSize = integer;
  21.    }
  22. }).addOnFailureListener(new OnFailureListener() {
  23.    @Override
  24.    public void onFailure(Exception e) {
  25.        if (e instanceof ApiException) {
  26.            ApiException apiException = (ApiException) e;
  27.            Log.e("archive","statusCode:" + apiException.getStatusCode());
  28.        }
  29.    }
  30. });

2、已知存档数据的限制后,应用需要使用ArchiveDetails.set方法将玩家当前的游戏存档文件写入ArchiveDetails对象。

3、游戏将存档元数据(如存档描述、游戏进度、封面图片等)写入ArchiveSummaryUpdate对象。

收起
自动换行
深色代码主题
复制
  1. ArchiveSummaryUpdate archiveSummaryUpdate =
  2.        new ArchiveSummaryUpdate.Builder().setActiveTime(playedTime)
  3.                .setCurrentProgress(progress)
  4.                .setDescInfo(description)
  5.                .setThumbnail(bitmap)
  6.                .setThumbnailMimeType(imageType)
  7.                .build();

4、 调用ArchivesClient.addArchive方法提交存档,存档成功后HMS SDK将以ArchiveSummary对象方式向应用返回存档成功后的存档元数据。

说明:
本方式支持无网络时先本地缓存,当网络恢复时再提交服务器,但可能发生用户人为操作导致数据丢失的情况。
收起
自动换行
深色代码主题
复制
  1. ArchiveDetails archiveDetails = new ArchiveDetails.Builder().build();
  2. archiveDetails.set(content);
  3. Task<ArchiveSummary> addArchiveTask = Games.getArchiveClient(this, signInHuaweiId).addArchive(archiveDetails, archiveSummaryUpdate, isSupportCache);
  4. addArchiveTask.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() {
  5.    @Override
  6.    public void onSuccess(ArchiveSummary archiveSummary) {
  7.        if (archiveSummary != null) {
  8.            String fileName = archiveSummary.getFileName();
  9.            String archiveId = archiveSummary.getId();
  10.        }
  11.    }
  12. }).addOnFailureListener(new OnFailureListener() {
  13.    @Override
  14.    public void onFailure(Exception e) {
  15.        if (e instanceof ApiException) {
  16.            ApiException apiException = (ApiException) e;
  17.            Log.e("archive","statusCode:" + apiException.getStatusCode());
  18.        }
  19.    }
  20. });

5. 展示存档列表

玩家可以在存档入口选择浏览存档记录,此时游戏需要展示所有的存档记录供用户选择。存档记录选择界面的展示方法分为2种:

  • 直接展示应用助手的存档页面:游戏需要获取应用助手存档界面的Intent对象,然后通过startActivityForResult方法拉起存档选择页面,开发者不需要自己开发存档展示页面。此方式仅支持EMUI为9.0及以上版本的终端,且必须安装10.3及以上版本的华为应用助手。

注意:
只有游戏正式上架后应用助手才会正常展示存档列表,测试阶段无法正常展示。
  • 自行展示存档列表:获取存档数据,自行开发存档记录页面。

直接展示应用助手的存档列表

1、当玩家进入存档入口时,应用调用ArchivesClient.getShowArchiveListIntent方法获取应用助手存档页面对应的Intent对象。

2、当取得存档列表界面对应的Intent对象后,应用可以调用startActivityForResult方法直接拉起华为应用助手的存档选择页面。

3、如果用户在应用助手存档列表中选择一个存档查看,游戏需要调用ArchivesClient.parseSummary获取选择的存档元数据,Bundle对象使用Intent对象的getParcelableExtra方法获取。

收起
自动换行
深色代码主题
复制
  1. Task<Intent> task = archivesClient.getshowArchiveListIntent(title, allowAddBtn, allowDeleteBtn, maxArchive);
  2. task.addOnSuccessListener(new OnSuccessListener<Intent>() {
  3.    @Override
  4.    public void onSuccess(Intent intent) {
  5.        if (intent != null) {
  6.            startActivityForResult(intent, 1);
  7.        }
  8.    }
  9. }).addOnFailureListener(new OnFailureListener() {
  10.    @Override
  11.    public void onFailure(Exception e) {
  12.        if (e instanceof ApiException) {
  13.            ApiException apiException = (ApiException) e;
  14.            Log.e("archive", "statusCode:" + apiException.getStatusCode());
  15.        }
  16.    }
  17. });  
  18. …  
  19. @Override  
  20. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  21.    super.onActivityResult(requestCode, resultCode, data);
  22.    if (requestCode == 1) {
  23.        if (data == null) {
  24.            return;
  25.        }
  26.        if (data.hasExtra(ArchiveConstants.ARCHIVE_SELECT)) {
  27.            Bundle bundle = data.getParcelableExtra(ArchiveConstants.ARCHIVE_SELECT);
  28.            Task<ArchiveSummary> task = getArchiveClient().parseSummary(bundle);
  29.            task.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() {
  30.                @Override
  31.                public void onSuccess(ArchiveSummary archiveSummary) {
  32.                    if (archiveSummary != null) {
  33.                        // TODO  
  34.                    }
  35.                }
  36.            }).addOnFailureListener(new OnFailureListener() {
  37.                @Override
  38.                public void onFailure(Exception e) {
  39.                    if (e instanceof ApiException) {
  40.                        ApiException apiException = (ApiException) e;
  41.                        Log.e("archive", "statusCode:" + apiException.getStatusCode());
  42.        }
  43.                }
  44.            });
  45.        } else if (data.hasExtra(ArchiveConstants.ARCHIVE_ADD)) {
  46.            // TODO  call addArchive api to add a new Archive
  47.        }
  48.    }
  49. }

自行展示存档列表

玩家进入存档页面入口时,游戏可以调用ArchivesClient.getArchiveSummaryList方法获取所有存档列表,支持从本地缓存或华为游戏服务器获取。

收起
自动换行
深色代码主题
复制
  1. Task<List<ArchiveSummary>> task = archivesClient.getArchiveSummaryList(isRealTime);  
  2. task.addOnSuccessListener(new OnSuccessListener<List<ArchiveSummary>>() {
  3.    @Override
  4.    public void onSuccess(List<ArchiveSummary> list) {
  5.        if (list != null) {
  6.             Log.i("TAG"," list size" + list.size());
  7. }
  8.    }
  9. }).addOnFailureListener(new OnFailureListener() {
  10.    @Override
  11.    public void onFailure(Exception e) {
  12.        if (e instanceof ApiException) {
  13.            ApiException apiException = (ApiException) e;
  14.            Log.e("archive", "statusCode:" + apiException.getStatusCode());
  15.        }
  16.    }
  17. });

存档的封面图需要先调用ArchiveSummary.hasThumbnail判断存档是否存在封面缩略图,如果存在则需要调用ArchivesClient.getThumbnail获取存档封面的图片数据。

收起
自动换行
深色代码主题
复制
  1. private void loadThumbnail(Archive archive){
  2.    if (archive.getSummary().hasThumbnail()) {
  3.        Task<Bitmap> coverImageTask = getArchiveClient().getThumbnail(archive.getSummary().getId());
  4.        coverImageTask.addOnSuccessListener(new OnSuccessListener<Bitmap>() {
  5.            @Override
  6.            public void onSuccess(Bitmap bitmap) {
  7.                Glide.with(getApplicationContext()).load(bitmap).into(archiveImage);
  8.            }
  9.        }).addOnFailureListener(new OnFailureListener() {
  10.            @Override
  11.            public void onFailure(Exception e) {
  12.                if (e instanceof ApiException) {
  13.                    Log.e("archive","load image failed"+ ((ApiException) e).getStatusCode());
  14.                }
  15.            }
  16.        });
  17.    }
  18. }

6. 更新存档

更新存档是指玩家保存游戏时,如果玩家之前保存过该游戏,游戏服务器选择修改已有的存档文件和存档元数据,以节省云空间的存储空间。

说明:
玩家保存游戏时新增存档还是更新存档,由开发者自行设计确定,华为仅提供能力。


1、玩家保存存档时,游戏调用ArchivesClient.getArchiveSummaryList方法获取当前玩家的存档列表。

2、游戏获取到玩家保存过该游戏,从查询结果中取出存档的ArchiveSummary元数据,解析存档ID。

3、 使用ArchiveDetails.set方法将玩家当前的游戏存档文件写入ArchiveDetails对象,将需要保存的存档元数据写入ArchiveSummaryUpdate对象。

4、调用ArchivesClient.updateArchive方法更新该存档。

5、如果打开结果中的OperationResult.isDifference返回true,表示打开存档时发生数据冲突,开发者需要进行数据冲突处理,详细步骤参见冲突处理。

收起
自动换行
深色代码主题
复制
  1. String description = editTextDescription.getText().toString();
  2. long playedTime = String2Long(editTextPlayedTime.getText().toString());
  3. long progress = String2Long(editTextProgress.getText().toString());
  4. ArchiveSummaryUpdate archiveMetadataChange = builder.build();
  5. ArchiveDetails archiveContents = new ArchiveDetails.Builder().build();
  6. archiveContents.set((progress + description + playedTime).getBytes());
  7. Task<OperationResult> task = getArchivesClient().updateArchive(archiveId, archiveMetadataChange, archiveContents);
  8.    task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
  9.         @Override
  10.         public void onSuccess(OperationResult archiveDataOrConflict) {
  11.             showLog("isDifference:" + ((archiveDataOrConflict == null) ? "" : archiveDataOrConflict.isDifference()));
  12.         }
  13.    }

7. 打开存档

打开存档是指玩家进入游戏后加载之前提交的存档继续进行游戏。

  • 如果打开应用助手展示的存档,由应用助手加载。

  • 如果打开开发者自行展示的存档,建议ArchivesClient.getArchiveSummaryList方法获取的存档,不要打开本地缓存的存档。

1、玩家选择一个存档加载时,应用调用ArchivesClient的loadArchiveDetails方法加载存档文件,有以下几种方式选择。

2、如果结果中的OperationResult.isDifference返回true,表示加载存档时发生数据冲突,开发者需要进行数据冲突处理,详细步骤参见冲突处理。

3、当存档加载成功后,应用可以使用OperationResult.getArchive获取包含存档元数据和存档文件内容的Archive对象,再使用Archive.getDetails方法获取包含存档文件内容的ArchiveDetails对象。

示例代码:

收起
自动换行
深色代码主题
复制
  1. int conflictPolicy = getConflictPolicy();  //指定冲突解决的策略
  2. Task<OperationResult> task;
  3. if (conflictPolicy == -1) {
  4.    task = Games.getArchiveClient(this, authHuaweiId).loadArchiveDetails(archiveId);
  5. } else {
  6.    task = Games.getArchiveClient(this, authHuaweiId).loadArchiveDetails(archiveId, conflictPolicy);
  7. }
  8. task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
  9.    @Override
  10.    public void onSuccess(OperationResult archiveDataOrConflict) {
  11.        Log.i("TAG", "isDifference:" + ((archiveDataOrConflict == null) ? "" : archiveDataOrConflict.isDifference()));
  12.        if (archiveDataOrConflict != null && !archiveDataOrConflict.isDifference()) {
  13.            Archive archive = archiveDataOrConflict.getArchive();
  14.            if (archive != null && archive.getSummary() != null) {
  15.                Log.i("TAG", "ArchiveId:" + archive.getSummary().getId());
  16.            }
  17.        } else {
  18.            //处理冲突
  19.            handleDifference(archiveDataOrConflict);
  20.        }
  21.    }
  22. }).addOnFailureListener(new OnFailureListener() {
  23.    @Override
  24.    public void onFailure(Exception e) {
  25.        if (e instanceof ApiException) {
  26.            ApiException apiException = (ApiException) e;
  27.            Log.e("archive", "statusCode:" + apiException.getStatusCode());
  28.        }
  29.    }
  30. });

4、获取存档文件内容后,应用可以使用ArchiveDetails.get方法读取存档文件中的二进制游戏数据,读取完成后将游戏数据加载给用户。

收起
自动换行
深色代码主题
复制
  1. archive.getArchiveDetails().get();

8. 冲突处理

当更新或打开存档时缓存中的存档和服务器取回的存档发生数据冲突,在返回结果中会返回具体的冲突细节,开发者可以根据游戏的策略自行选择冲突解决方案。

  • 开发者在冲突的两个存档中选择其中任何一个作为最终使用的存档。

  • 开发者自行混合编辑数据后,使用编辑后的新存档作为最终使用的存档。

1、当打开存档时返回的OperationResult.isDifference为true,表示发生数据冲突。应用需要使用OperationResult.getDifference获取冲突细节数据。

2、开发者可以根据冲突数据提示玩家来选择使用哪个存档,或者由开发者自行确定使用哪个存档来解决冲突。

3、开发者确定解决冲突的原则后,生成解决冲突的存档数据。

4、生成解决冲突的存档数据后,应用可以调用ArchivesClient的updateArchive方法解决冲突。

5、冲突解决后根据返回结果中的OperationResult.isDifference方法再次判断是否仍然存在冲突。如果仍然存在冲突,则重复以上步骤再次解决冲突,直到OperationResult.isDifference返回false。

示例代码:

收起
自动换行
深色代码主题
复制
  1. private void handleDifference(OperationResult operationResult) {
  2.    if (operationResult != null) {
  3.        OperationResult.Difference archiveDifference = operationResult.getDifference();
  4.        Archive openedArchive = archiveDifference.getRecentArchive();
  5.        Archive serverArchive = archiveDifference.getServerArchive();
  6.        // Sample use the server archive to solve diffrence
  7.        if (serverArchive == null) {
  8.            return;
  9.        }
  10.        Task<OperationResult> task = Games.getArchiveClient(this, signInHuaweiId).updateArchive(serverArchive);
  11.        task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
  12.            @Override
  13.            public void onSuccess(OperationResult operationResult) {
  14.                Log.i("archive","isDifference:"
  15.                        + ((operationResult == null) ? "" : operationResult.isDifference()));
  16.                if (operationResult != null && !operationResult.isDifference()) {
  17.                    Archive archive = operationResult.getRecentArchive();
  18.                    if (archive != null && archive.getSummary() != null) {
  19.                        Log.i("archive","ArchiveId:" + archive.getSummary().getId());
  20.                    }
  21.                } else {
  22.                    handleDifference(operationResult);
  23.                }
  24.            }
  25.        }).addOnFailureListener(new OnFailureListener() {
  26.            @Override
  27.            public void onFailure(Exception e) {
  28.                ApiException apiException = (ApiException) e;
  29.                Log.e("archive" , "resolve result:" + apiException.getStatusCode());
  30.            }
  31.        });
  32.    }
  33. }

9. 删除存档

  • 如果玩家在应用助手的存档页面删除存档,由应用助手实现删除操作。

  • 如果开发者自行展示存档列表页面,按照以下方法处理删除操作。

当玩家在存档列表页面选择删除某个存档时,应用调用ArchivesClient.removeArchive方法,请求中携带需要删除的存档元数据对象。

收起
自动换行
深色代码主题
复制
  1. Task<String> removeArchiveTask = Games.getArchiveClient(this, signInHuaweiId).removeArchive(archiveSummary);
  2. removeArchiveTask.addOnSuccessListener(new OnSuccessListener<String>() {
  3.    @Override
  4.    public void onSuccess(String s) {
  5.        String deletedArchiveId = s;
  6.    }
  7. }).addOnFailureListener(new OnFailureListener() {
  8.    @Override
  9.    public void onFailure(Exception e) {
  10.        if (e instanceof ApiException) {
  11.            ApiException apiException = (ApiException) e;
  12.            Log.e("archive" ,"statusCode:" + apiException.getStatusCode());
  13.        }
  14.    }
  15. });

如果删除成功将返回该存档ID,删除失败会返回相应错误码,参见游戏通用返回码。

本页面可能包含由第三方许可的内容,请参考具体描述。