合作咨询
我们的专家服务团队将竭诚为您提供专业的合作咨询服务
此版本文档已归档不再维护,推荐您使用最新版本。
概念介绍
您可以将玩家的游戏进度存储至华为云空间中,或从华为云空间取得之前的游戏进度继续游戏。因此,只要用户使用相同的华为帐号登录,则游戏存档可以在任意设备取回进行游戏,即使用户之前的设备丢失、损毁或换了新设备也能继续之前的游戏进度。
一个存档主要由2部分组成:
存档文件:开发者可以将游戏数据自行写入需要存档的文件,在获取存档文件后也由开发者自行解析,存档文件大小限制为3MB。
存档元数据:存档元数据是指存档本身的属性,开发者可以向用户展示存档属性,用户可以根据属性信息选择哪个存档继续游戏,比如存档名称、上次修改时间等。存档的主要属性如下表所示。
属性 | 说明 |
存档ID | 存档在该游戏中的唯一标识,在提交存档后由华为游戏服务器生成。 |
存档名称 | 存档保存的文件名,在提交存档后由华为服务器生成。 |
存档描述 | 存档用于展示给玩家的描述信息,在提交存档时由开发者自行定义,最多1000个字符。 |
上次修改时间 | 服务器生成的时间戳(以毫秒为单位),表示上次更新保存的游戏的时间。 |
游戏时长 | 开发者在提交存档时提供的玩家在该存档上游戏总时间(以毫秒为单位)。 |
游戏进度 | 开发者在提交存档时提供的玩家在该存档上的游戏进度,由开发者自行定义。比如第几关等,用整数进行识别。 |
封面图片 | 开发者在提交存档时提供的存档封面图像,一般来说为游戏存档时的截图,若未提供,则为系统默认图片,最大200KB,支持JPG或PNG格式,长宽比为16:9。 |
离线同步
没有网络连接时,支持本地缓存的方式读取和保存游戏存档。
读取和写入的均为本地缓存数据,网络重新连接后,自动与华为服务器进行同步。
离线同步仅对有登录华为帐号的用户生效,否则无法离线存储。
若在再次联网前用户进行华为帐号登出,则本地缓存的存档被丢弃。
冲突处理
在游戏存档时可能存在同一个帐号在多个设备中同时存档的情况,如果设备因网络问题存档延时,则可能会发生数据冲突。当更新存档或打开存档时发现两份存档的存档属性对比存在不一致时,华为游戏服务器会返回对应的冲突结果给开发者,并允许开发者选择使用其中某份存档作为最终使用的存档,或者也允许开发者自主混合存档数据作为最终使用的存档。
冲突判断有两个场景,更新存档和打开存档,判断条件有多个,之间为【或】的关系,即只要有一个条件满足,即判断为冲突,同时告知开发者冲突原因。冲突判断条件参见下表。
若有存档对应字段不存在无法进行判断,则认为存在冲突,且认为另一个存档对应字段更大。
操作场景 | 冲突判断条件 |
更新存档 | 存入存档为A,服务器已存在存档为B。
|
打开存档 | 缓存中已有存档为A,服务器取回存档为B。
|
解决冲突的方式有2种:
开发者在打开存档时指定冲突解决策略,在发生冲突时由华为游戏服务器按冲突解决策略自动解决冲突。
开发者在发生冲突时自行选择冲突解决方案,可以选择其中任何一个作为最终使用的存档,也允许开发者混合编辑数据后,使用编辑后的新存档。
游戏存档实际是存储游戏相关文件至当前用户的华为帐号所属的华为云空间中,因此用户需要同意开通华为云空间服务,才能使用游戏存档功能。游戏存档当前仅支持华为云空间服务支持的国家和地区,具体请参见华为云空间服务支持的地区。
调用存档相关接口时,用户的华为帐号必须为已登录状态。
单个用户在华为云空间剩余存储容量充足情况下,最多可以保存100个存档。
用户必须打开华为应用市场(10.3及以上版本)中的游戏服务开关才能使用存档功能。如果用户没有开启游戏服务开关,当游戏调用存档相关接口时HMS SDK会自动拉起华为应用市场的游戏服务开关页面引导用户打开开关,如果用户未打开游戏服务开关,将返回7018错误码,游戏需要主动提示用户到华为应用市场“我的-设置-游戏服务”页面中开启游戏服务开关后才能使用存档功能。

1、如果需要使用存档功能,在玩家登录时需要调用HuaweiIdAuthParamsHelper.setScopeList申请DRIVE_APP_DATA的SCOPE。示例代码如下:
- List<Scope> scopes = new ArrayList<>();
- scopes.add(GameScopes.DRIVE_APP_DATA);
- HuaweiIdAuthParams huaweiIdAuthParams = new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM_GAME).setScopeList(scopes).createParams();
2、在使用存档接口的能力前都需要先调用Games.getArchiveClient()获取ArchivesClient实例。示例代码如下:
- ArchivesClient client = Games.getArchiveClient(this, mAuthHuaweiId);
接口中传入的mAuthHuaweiId对象在用户登录游戏时获取,如果当前华为帐号登录状态已失效,则需要调用HuaweiIdAuthService.getSignInIntent方法再次登录或调用HuaweiIdAuthService.silentSignIn方法静默授权获取AuthHuaweiId。
新建存档是指玩家保存游戏时,游戏在服务器端新建新的存档文件和存档元数据。

1、 玩家在游戏过程中选择存档,此时应用可以先查询华为服务器对存档文件的限制。
如果需要查询存档封面图片文件的最大字节数,调用ArchivesClient.getLimitThumbnailSize方法。
如果需要查询存档文件的最大字节数,调用ArchivesClient.getLimitDetailsSize方法。
- Task<Integer> thumbnailSizeTask = Games.getArchiveClient(this, mAuthHuaweiId) .getLimitThumbnailSize();
- thumbnailSizeTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
- @Override
- public void onSuccess(Integer integer) {
- int maxThumbnailSize = integer;
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive","statusCode:" + apiException.getStatusCode());
- }
- }
- });
- Task<Integer> detailSizeTask = Games.getArchiveClient(this, signInHuaweiId) .getLimitDetailsSize();
- detailSizeTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
- @Override
- public void onSuccess(Integer integer) {
- int detailSize = integer;
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive","statusCode:" + apiException.getStatusCode());
- }
- }
- });
2、已知存档数据的限制后,应用需要使用ArchiveDetails.set方法将玩家当前的游戏存档文件写入ArchiveDetails对象。
3、游戏将存档元数据(如存档描述、游戏进度、封面图片等)写入ArchiveSummaryUpdate对象。
- ArchiveSummaryUpdate archiveSummaryUpdate =
- new ArchiveSummaryUpdate.Builder().setActiveTime(playedTime)
- .setCurrentProgress(progress)
- .setDescInfo(description)
- .setThumbnail(bitmap)
- .setThumbnailMimeType(imageType)
- .build();
4、 调用ArchivesClient.addArchive方法提交存档,存档成功后HMS SDK将以ArchiveSummary对象方式向应用返回存档成功后的存档元数据。
- ArchiveDetails archiveDetails = new ArchiveDetails.Builder().build();
- archiveDetails.set(content);
- Task<ArchiveSummary> addArchiveTask = Games.getArchiveClient(this, signInHuaweiId).addArchive(archiveDetails, archiveSummaryUpdate, isSupportCache);
- addArchiveTask.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() {
- @Override
- public void onSuccess(ArchiveSummary archiveSummary) {
- if (archiveSummary != null) {
- String fileName = archiveSummary.getFileName();
- String archiveId = archiveSummary.getId();
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive","statusCode:" + apiException.getStatusCode());
- }
- }
- });
玩家可以在存档入口选择浏览存档记录,此时游戏需要展示所有的存档记录供用户选择。存档记录选择界面的展示方法分为2种:
直接展示应用助手的存档页面:游戏需要获取应用助手存档界面的Intent对象,然后通过startActivityForResult方法拉起存档选择页面,开发者不需要自己开发存档展示页面。此方式仅支持EMUI为9.0及以上版本的终端,且必须安装10.3及以上版本的华为应用助手。
自行展示存档列表:获取存档数据,自行开发存档记录页面。

1、当玩家进入存档入口时,应用调用ArchivesClient.getShowArchiveListIntent方法获取应用助手存档页面对应的Intent对象。
2、当取得存档列表界面对应的Intent对象后,应用可以调用startActivityForResult方法直接拉起华为应用助手的存档选择页面。
3、如果用户在应用助手存档列表中选择一个存档查看,游戏需要调用ArchivesClient.parseSummary获取选择的存档元数据,Bundle对象使用Intent对象的getParcelableExtra方法获取。
- Task<Intent> task = archivesClient.getshowArchiveListIntent(title, allowAddBtn, allowDeleteBtn, maxArchive);
- task.addOnSuccessListener(new OnSuccessListener<Intent>() {
- @Override
- public void onSuccess(Intent intent) {
- if (intent != null) {
- startActivityForResult(intent, 1);
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive", "statusCode:" + apiException.getStatusCode());
- }
- }
- });
- …
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == 1) {
- if (data == null) {
- return;
- }
- if (data.hasExtra(ArchiveConstants.ARCHIVE_SELECT)) {
- Bundle bundle = data.getParcelableExtra(ArchiveConstants.ARCHIVE_SELECT);
- Task<ArchiveSummary> task = getArchiveClient().parseSummary(bundle);
- task.addOnSuccessListener(new OnSuccessListener<ArchiveSummary>() {
- @Override
- public void onSuccess(ArchiveSummary archiveSummary) {
- if (archiveSummary != null) {
- // TODO
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive", "statusCode:" + apiException.getStatusCode());
- }
- }
- });
- } else if (data.hasExtra(ArchiveConstants.ARCHIVE_ADD)) {
- // TODO call addArchive api to add a new Archive
- }
- }
- }

玩家进入存档页面入口时,游戏可以调用ArchivesClient.getArchiveSummaryList方法获取所有存档列表,支持从本地缓存或华为游戏服务器获取。
- Task<List<ArchiveSummary>> task = archivesClient.getArchiveSummaryList(isRealTime);
- task.addOnSuccessListener(new OnSuccessListener<List<ArchiveSummary>>() {
- @Override
- public void onSuccess(List<ArchiveSummary> list) {
- if (list != null) {
- Log.i("TAG"," list size" + list.size());
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive", "statusCode:" + apiException.getStatusCode());
- }
- }
- });
存档的封面图需要先调用ArchiveSummary.hasThumbnail判断存档是否存在封面缩略图,如果存在则需要调用ArchivesClient.getThumbnail获取存档封面的图片数据。
- private void loadThumbnail(Archive archive){
- if (archive.getSummary().hasThumbnail()) {
- Task<Bitmap> coverImageTask = getArchiveClient().getThumbnail(archive.getSummary().getId());
- coverImageTask.addOnSuccessListener(new OnSuccessListener<Bitmap>() {
- @Override
- public void onSuccess(Bitmap bitmap) {
- Glide.with(getApplicationContext()).load(bitmap).into(archiveImage);
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- Log.e("archive","load image failed"+ ((ApiException) e).getStatusCode());
- }
- }
- });
- }
- }
更新存档是指玩家保存游戏时,如果玩家之前保存过该游戏,游戏服务器选择修改已有的存档文件和存档元数据,以节省云空间的存储空间。

1、玩家保存存档时,游戏调用ArchivesClient.getArchiveSummaryList方法获取当前玩家的存档列表。
2、游戏获取到玩家保存过该游戏,从查询结果中取出存档的ArchiveSummary元数据,解析存档ID。
3、 使用ArchiveDetails.set方法将玩家当前的游戏存档文件写入ArchiveDetails对象,将需要保存的存档元数据写入ArchiveSummaryUpdate对象。
4、调用ArchivesClient.updateArchive方法更新该存档。
5、如果打开结果中的OperationResult.isDifference返回true,表示打开存档时发生数据冲突,开发者需要进行数据冲突处理,详细步骤参见冲突处理。
- String description = editTextDescription.getText().toString();
- long playedTime = String2Long(editTextPlayedTime.getText().toString());
- long progress = String2Long(editTextProgress.getText().toString());
- ArchiveSummaryUpdate archiveMetadataChange = builder.build();
- ArchiveDetails archiveContents = new ArchiveDetails.Builder().build();
- archiveContents.set((progress + description + playedTime).getBytes());
- Task<OperationResult> task = getArchivesClient().updateArchive(archiveId, archiveMetadataChange, archiveContents);
- task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
- @Override
- public void onSuccess(OperationResult archiveDataOrConflict) {
- showLog("isDifference:" + ((archiveDataOrConflict == null) ? "" : archiveDataOrConflict.isDifference()));
- }
- }
打开存档是指玩家进入游戏后加载之前提交的存档继续进行游戏。
如果打开应用助手展示的存档,由应用助手加载。
如果打开开发者自行展示的存档,建议ArchivesClient.getArchiveSummaryList方法获取的存档,不要打开本地缓存的存档。

1、玩家选择一个存档加载时,应用调用ArchivesClient的loadArchiveDetails方法加载存档文件,有以下几种方式选择。
不处理数据冲突:即在发生数据冲突时华为服务器不处理数据冲突,需要开发者自行处理。此方式使用loadArchiveDetails(ArchiveSummary summary)方法或loadArchiveDetails(String archiveId)方法。
指定冲突处理的策略:可以指定游戏时长、进度、修改时间作为冲突处理策略,如果发生冲突,华为服务器使用指定的策略处理数据冲突。此方式使用loadArchiveDetails(ArchiveSummary summary, int diffStrategy)方法或loadArchiveDetails(String archiveId, int diffStrategy)方法。
2、如果结果中的OperationResult.isDifference返回true,表示加载存档时发生数据冲突,开发者需要进行数据冲突处理,详细步骤参见冲突处理。
3、当存档加载成功后,应用可以使用OperationResult.getArchive获取包含存档元数据和存档文件内容的Archive对象,再使用Archive.getDetails方法获取包含存档文件内容的ArchiveDetails对象。
示例代码:
- int conflictPolicy = getConflictPolicy(); //指定冲突解决的策略
- Task<OperationResult> task;
- if (conflictPolicy == -1) {
- task = Games.getArchiveClient(this, authHuaweiId).loadArchiveDetails(archiveId);
- } else {
- task = Games.getArchiveClient(this, authHuaweiId).loadArchiveDetails(archiveId, conflictPolicy);
- }
- task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
- @Override
- public void onSuccess(OperationResult archiveDataOrConflict) {
- Log.i("TAG", "isDifference:" + ((archiveDataOrConflict == null) ? "" : archiveDataOrConflict.isDifference()));
- if (archiveDataOrConflict != null && !archiveDataOrConflict.isDifference()) {
- Archive archive = archiveDataOrConflict.getArchive();
- if (archive != null && archive.getSummary() != null) {
- Log.i("TAG", "ArchiveId:" + archive.getSummary().getId());
- }
- } else {
- //处理冲突
- handleDifference(archiveDataOrConflict);
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive", "statusCode:" + apiException.getStatusCode());
- }
- }
- });
4、获取存档文件内容后,应用可以使用ArchiveDetails.get方法读取存档文件中的二进制游戏数据,读取完成后将游戏数据加载给用户。
- archive.getArchiveDetails().get();
当更新或打开存档时缓存中的存档和服务器取回的存档发生数据冲突,在返回结果中会返回具体的冲突细节,开发者可以根据游戏的策略自行选择冲突解决方案。
开发者在冲突的两个存档中选择其中任何一个作为最终使用的存档。
开发者自行混合编辑数据后,使用编辑后的新存档作为最终使用的存档。

1、当打开存档时返回的OperationResult.isDifference为true,表示发生数据冲突。应用需要使用OperationResult.getDifference获取冲突细节数据。
2、开发者可以根据冲突数据提示玩家来选择使用哪个存档,或者由开发者自行确定使用哪个存档来解决冲突。
3、开发者确定解决冲突的原则后,生成解决冲突的存档数据。
如果直接本地缓存的存档作为最终存档,可以OperationResult.Difference.getRecentArchive使用方法获取存档数据。
如果选择使用服务器存在的存档作为最终存档,可以使用OperationResult.Difference.getServerArchive方法获取存档数据。
如果选择使用混合编辑后的数据作为最终存档,应用需要自行编辑本地缓存的存档数据和服务器取回的存档数据,并生成最终存档的ArchiveSummaryUpdate和ArchiveDetails对象。
4、生成解决冲突的存档数据后,应用可以调用ArchivesClient的updateArchive方法解决冲突。
如果使用编辑后的ArchiveSummaryUpdate和ArchiveDetails对象解决冲突,可以调用ArchivesClient.updateArchive(String archiveId, ArchiveSummaryUpdate update, ArchiveDetails details)。
如果选择本地缓存存档或服务器取回的存档作为最终存档,可以调用ArchivesClient.updateArchive(Archive archive)方法。
5、冲突解决后根据返回结果中的OperationResult.isDifference方法再次判断是否仍然存在冲突。如果仍然存在冲突,则重复以上步骤再次解决冲突,直到OperationResult.isDifference返回false。
示例代码:
- private void handleDifference(OperationResult operationResult) {
- if (operationResult != null) {
- OperationResult.Difference archiveDifference = operationResult.getDifference();
- Archive openedArchive = archiveDifference.getRecentArchive();
- Archive serverArchive = archiveDifference.getServerArchive();
- // Sample use the server archive to solve diffrence
- if (serverArchive == null) {
- return;
- }
- Task<OperationResult> task = Games.getArchiveClient(this, signInHuaweiId).updateArchive(serverArchive);
- task.addOnSuccessListener(new OnSuccessListener<OperationResult>() {
- @Override
- public void onSuccess(OperationResult operationResult) {
- Log.i("archive","isDifference:"
- + ((operationResult == null) ? "" : operationResult.isDifference()));
- if (operationResult != null && !operationResult.isDifference()) {
- Archive archive = operationResult.getRecentArchive();
- if (archive != null && archive.getSummary() != null) {
- Log.i("archive","ArchiveId:" + archive.getSummary().getId());
- }
- } else {
- handleDifference(operationResult);
- }
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- ApiException apiException = (ApiException) e;
- Log.e("archive" , "resolve result:" + apiException.getStatusCode());
- }
- });
- }
- }
如果玩家在应用助手的存档页面删除存档,由应用助手实现删除操作。
如果开发者自行展示存档列表页面,按照以下方法处理删除操作。

当玩家在存档列表页面选择删除某个存档时,应用调用ArchivesClient.removeArchive方法,请求中携带需要删除的存档元数据对象。
- Task<String> removeArchiveTask = Games.getArchiveClient(this, signInHuaweiId).removeArchive(archiveSummary);
- removeArchiveTask.addOnSuccessListener(new OnSuccessListener<String>() {
- @Override
- public void onSuccess(String s) {
- String deletedArchiveId = s;
- }
- }).addOnFailureListener(new OnFailureListener() {
- @Override
- public void onFailure(Exception e) {
- if (e instanceof ApiException) {
- ApiException apiException = (ApiException) e;
- Log.e("archive" ,"statusCode:" + apiException.getStatusCode());
- }
- }
- });
如果删除成功将返回该存档ID,删除失败会返回相应错误码,参见游戏通用返回码。