Este guia mostra como implementar jogos salvos usando a
API de snapshots fornecida pelos serviços relacionados a jogos do Google Play. As APIs podem ser encontradas nos pacotes
com.google.android.gms.games.snapshot
e com.google.android.gms.games
.
Antes de começar
Caso ainda não tenha feito isso, talvez seja útil revisar o Conceitos de Jogos salvos.
- Ative a compatibilidade com jogos salvos no seu jogo no Google Play Console.
- Faça o download e analise o exemplo de código dos jogos salvos na página de exemplos do Android.
- Conheça melhor as recomendações descritas na Lista de verificação de qualidade.
Como acessar o cliente de snapshots
Para começar a usar a API de snapshots, o jogo precisa ter um objeto
SnapshotsClient
. Para isso, chame o método
Games.getSnapshotsClient()
e transmita a
atividade.
Como especificar o escopo do Drive
A API de snapshots depende da API Google Drive para armazenar os jogos salvos. Para
acessar a API Drive, seu app precisa especificar
Drive.SCOPE_APPFOLDER
ao criar o cliente de Login do Google.
Veja um exemplo de como fazer isso no método
onResume()
para sua atividade de login:
@Override protected void onResume() { super.onResume(); signInSilently(); } private void signInSilently() { GoogleSignInOptions signInOption = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) // Add the APPFOLDER scope for Snapshot support. .requestScopes(Drive.SCOPE_APPFOLDER) .build(); GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption); signInClient.silentSignIn().addOnCompleteListener(this, new OnCompleteListener<GoogleSignInAccount>() { @Override public void onComplete(@NonNull Task<GoogleSignInAccount> task) { if (task.isSuccessful()) { onConnected(task.getResult()); } else { // Player will need to sign-in explicitly using via UI } } }); }
Mostrando jogos salvos
É possível integrar a API de snapshots sempre que o jogo fornecer aos jogadores a opção de salvar ou restaurar o progresso deles. O jogo pode exibir essa opção em pontos designados de salvamento/restauração ou permitir que os jogadores salvem ou restaurem o progresso a qualquer momento.
Depois que os jogadores selecionam a opção de salvar/restaurar o jogo, uma tela pode ser exibida, solicitando a inserção de informações de um novo jogo salvo ou a seleção de um jogo salvo para ser restaurado.
Para simplificar o desenvolvimento, a API de snapshots fornece uma interface do usuário (IU) de seleção padrão de jogos salvos que pode ser usada imediatamente. A IU de seleção de jogos salvos permite que os jogadores criem um novo jogo salvo, visualizem detalhes sobre os já existentes e carreguem os anteriores.
Para iniciar a IU padrão de Jogos salvos.
- Chame
SnapshotsClient.getSelectSnapshotIntent()
para receber umaIntent
para iniciar a IU de seleção padrão de jogos salvos. - Chame
startActivityForResult()
e transmita oIntent
. Se a chamada for bem-sucedida, o jogo vai exibir a IU da seleção de jogos salvos e as opções especificadas.
Confira um exemplo de como iniciar a IU padrão de seleção de jogos salvos:
private static final int RC_SAVED_GAMES = 9009; private void showSavedGamesUI() { SnapshotsClient snapshotsClient = PlayGames.getSnapshotsClient(this); int maxNumberOfSavedGamesToShow = 5; Task<Intent> intentTask = snapshotsClient.getSelectSnapshotIntent( "See My Saves", true, true, maxNumberOfSavedGamesToShow); intentTask.addOnSuccessListener(new OnSuccessListener<Intent>() { @Override public void onSuccess(Intent intent) { startActivityForResult(intent, RC_SAVED_GAMES); } }); }
Se o jogador optar por criar ou carregar um jogo salvo,
a interface envia uma solicitação
para os serviços relacionados a jogos do Google Play. Se a solicitação for bem-sucedida,
os serviços relacionados a jogos do Google Play vão retornar informações para criar ou restaurar o jogo salvo
usando o callback
onActivityResult()
. Seu jogo pode substituir esse callback para verificar se ocorreram erros durante a solicitação.
O snippet de código a seguir mostra um exemplo de implementação de
onActivityResult()
:
private String mCurrentSaveName = "snapshotTemp"; /** * This callback will be triggered after you call startActivityForResult from the * showSavedGamesUI method. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (intent != null) { if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)) { // Load a snapshot. SnapshotMetadata snapshotMetadata = intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA); mCurrentSaveName = snapshotMetadata.getUniqueName(); // Load the game data from the Snapshot // ... } else if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_NEW)) { // Create a new snapshot named with a unique string String unique = new BigInteger(281, new Random()).toString(13); mCurrentSaveName = "snapshotTemp-" + unique; // Create the new snapshot // ... } } }
Gravar jogos salvos
Para armazenar conteúdo em um jogo salvo, faça o seguinte:
- Abra um snapshot de forma assíncrona usando
SnapshotsClient.open()
. Depois, recupere o objetoSnapshot
. do resultado da tarefa chamandoSnapshotsClient.DataOrConflict.getData()
. - Extraia uma instância do
SnapshotContents
usandoSnapshotsClient.SnapshotConflict
. - Chame
SnapshotContents.writeBytes()
para armazenar os dados do jogador em formato de bytes. - Depois que todas as mudanças forem escritas, chame
SnapshotsClient.commitAndClose()
para enviar as alterações aos servidores do Google. Na chamada do método, seu jogo pode oferecer informações adicionais para informar aos serviços relacionados a jogos do Google Play como vai apresentar este jogo salvo aos jogadores. Essas informações são representadas em umaSnapshotMetaDataChange
que o jogo cria usandoSnapshotMetadataChange.Builder
.
O snippet a seguir mostra como o jogo pode confirmar mudanças em um jogo salvo:
private Task<SnapshotMetadata> writeSnapshot(Snapshot snapshot, byte[] data, Bitmap coverImage, String desc) { // Set the data payload for the snapshot snapshot.getSnapshotContents().writeBytes(data); // Create the change operation SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder() .setCoverImage(coverImage) .setDescription(desc) .build(); SnapshotsClient snapshotsClient = PlayGames.getSnapshotsClient(this); // Commit the operation return snapshotsClient.commitAndClose(snapshot, metadataChange); }
Se o dispositivo do jogador não estiver conectado a uma rede quando o app ligar
SnapshotsClient.commitAndClose()
, os serviços relacionados a jogos do Google Play armazenam os dados de jogos salvos localmente em
o dispositivo. Após a reconexão do dispositivo, os serviços relacionados a jogos do Google Play sincronizam o jogo salvo localmente armazenado em cache
nos servidores do Google.
Carregando jogos salvos
Para recuperar jogos salvos do jogador conectado no momento:
- Abra um snapshot de forma assíncrona usando
SnapshotsClient.open()
. Em seguida, extraia o objetoSnapshot
do resultado da tarefa chamandoSnapshotsClient.DataOrConflict.getData()
. Como alternativa, seu jogo também pode recuperar um snapshot específico pela interface de seleção de jogos salvos, conforme descrito em Como exibir Jogos salvos. - Extraia a instância
SnapshotContents
usandoSnapshotsClient.SnapshotConflict
. - Chame
SnapshotContents.readFully()
para ler o conteúdo do snapshot.
O snippet a seguir mostra como carregar um jogo salvo específico:
Task<byte[]> loadSnapshot() { // Display a progress dialog // ... // Get the SnapshotsClient from the signed in account. SnapshotsClient snapshotsClient = PlayGames.getSnapshotsClient(this); // In the case of a conflict, the most recently modified version of this snapshot will be used. int conflictResolutionPolicy = SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED; // Open the saved game using its name. return snapshotsClient.open(mCurrentSaveName, true, conflictResolutionPolicy) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.e(TAG, "Error while opening Snapshot.", e); } }).continueWith(new Continuation<SnapshotsClient.DataOrConflict<Snapshot>, byte[]>() { @Override public byte[] then(@NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task) throws Exception { Snapshot snapshot = task.getResult().getData(); // Opening the snapshot was a success and any conflicts have been resolved. try { // Extract the raw data from the snapshot. return snapshot.getSnapshotContents().readFully(); } catch (IOException e) { Log.e(TAG, "Error while reading Snapshot.", e); } return null; } }).addOnCompleteListener(new OnCompleteListener<byte[]>() { @Override public void onComplete(@NonNull Task<byte[]> task) { // Dismiss progress dialog and reflect the changes in the UI when complete. // ... } }); }
Como lidar com conflitos de jogos salvos
Ao usar a API de snapshots no seu jogo, é possível que vários dispositivos realizem leituras e gravações no mesmo jogo salvo. No caso de um dispositivo perder temporariamente a conexão de rede e depois se reconectar, isso pode causar conflitos de dados em que o jogo salvo no dispositivo local de um jogador esteja dessincronizado com a versão remota armazenada nos servidores do Google.
A API de snapshots fornece um mecanismo de resolução de conflitos que apresenta os dois conjuntos de jogos salvos conflitantes no tempo de leitura, além de permitir que você implemente uma estratégia de resolução adequada para o jogo.
Quando os serviços relacionados a jogos do Google Play detectam um conflito de dados, o método
SnapshotsClient.DataOrConflict.isConflict()
retorna um valor de true
. Nesse caso, a classe
SnapshotsClient.SnapshotConflict
fornece duas versões do jogo salvo:
- Versão do servidor: a versão mais atualizada conhecida pelos serviços relacionados a jogos do Google Play como exata no dispositivo do jogador.
- Versão local: é uma versão modificada detectada em um dos dispositivos do jogador que contém metadados ou conteúdo conflitantes. Talvez ela não seja igual à versão que você tentou salvar.
Seu jogo precisa decidir como resolver o conflito, escolhendo uma das versões fornecidas ou mesclando os dados das duas versões do jogo salvo.
Para detectar e resolver conflitos de jogos salvos:
- Chame
SnapshotsClient.open()
. O resultado da tarefa contém uma classeSnapshotsClient.DataOrConflict
. - Chame o método
SnapshotsClient.DataOrConflict.isConflict()
. Se o resultado for verdadeiro, há um conflito para resolver. - Chame
SnapshotsClient.DataOrConflict.getConflict()
para recuperar umSnaphotsClient.snapshotConflict
. - Chame
SnapshotsClient.SnapshotConflict.getConflictId()
para recuperar o ID do conflito que identifica que identifica o conflito detectado. Seu jogo precisa desse valor para enviar uma solicitação de resolução de conflitos mais tarde. - Chame
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
para acessar a versão local. - Chame
SnapshotsClient.SnapshotConflict.getSnapshot()
para receber a versão do servidor. - Para resolver o conflito do jogo salvo, selecione a versão que você quer salvar no servidor como a
versão final e a transmita para o método
SnapshotsClient.resolveConflict()
.
O snippet a seguir mostra um exemplo de como seu jogo pode lidar com um conflito, selecionando o jogo salvo modificado mais recentemente como a versão final a ser salva:
private static final int MAX_SNAPSHOT_RESOLVE_RETRIES = 10; Task<Snapshot> processSnapshotOpenResult(SnapshotsClient.DataOrConflict<Snapshot> result, final int retryCount) { if (!result.isConflict()) { // There was no conflict, so return the result of the source. TaskCompletionSource<Snapshot> source = new TaskCompletionSource<>(); source.setResult(result.getData()); return source.getTask(); } // There was a conflict. Try resolving it by selecting the newest of the conflicting snapshots. // This is the same as using RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED as a conflict resolution // policy, but we are implementing it as an example of a manual resolution. // One option is to present a UI to the user to choose which snapshot to resolve. SnapshotsClient.SnapshotConflict conflict = result.getConflict(); Snapshot snapshot = conflict.getSnapshot(); Snapshot conflictSnapshot = conflict.getConflictingSnapshot(); // Resolve between conflicts by selecting the newest of the conflicting snapshots. Snapshot resolvedSnapshot = snapshot; if (snapshot.getMetadata().getLastModifiedTimestamp() < conflictSnapshot.getMetadata().getLastModifiedTimestamp()) { resolvedSnapshot = conflictSnapshot; } return PlayGames.getSnapshotsClient(theActivity) .resolveConflict(conflict.getConflictId(), resolvedSnapshot) .continueWithTask( new Continuation< SnapshotsClient.DataOrConflict<Snapshot>, Task<Snapshot>>() { @Override public Task<Snapshot> then( @NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task) throws Exception { // Resolving the conflict may cause another conflict, // so recurse and try another resolution. if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) { return processSnapshotOpenResult(task.getResult(), retryCount + 1); } else { throw new Exception("Could not resolve snapshot conflicts"); } } }); }
Como modificar jogos salvos para resolver conflitos
Se você quiser mesclar dados de vários jogos salvos ou modificar um Snapshot
existente
para salvar no servidor como a versão final resolvida, siga estas etapas:
- Chame
SnapshotsClient.open()
. - Chame
SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent()
para receber um novo objetoSnapshotContents
. - Mescle os dados de
SnapshotsClient.SnapshotConflict.getConflictingSnapshot()
eSnapshotsClient.SnapshotConflict.getSnapshot()
no objetoSnapshotContents
da etapa anterior. - Outra opção é criar uma instância
SnapshotMetadataChange
se houver mudanças nos metadados. campos. - Chame
SnapshotsClient.resolveConflict()
. Na chamada de método, transmitaSnapshotsClient.SnapshotConflict.getConflictId()
como o primeiro argumento e os objetosSnapshotMetadataChange
eSnapshotContents
que você modificou anteriormente como o segundo e o terceiro argumento, respectivamente. - Se a chamada
SnapshotsClient.resolveConflict()
for bem-sucedida, a API vai armazenar o objetoSnapshot
no servidor e tentar abrir o objeto Snapshot no dispositivo local.- Se houver um conflito,
SnapshotsClient.DataOrConflict.isConflict()
vai retornartrue
. Nesse caso, o jogo precisa retornar à etapa 2 e repetir as etapas para modificar o snapshot até que os conflitos sejam resolvidos. - Se não houver conflito,
SnapshotsClient.DataOrConflict.isConflict()
vai retornarfalse
, e o objetoSnapshot
estará aberto para o jogo modificar.
- Se houver um conflito,