# Unity SDK 教程

> Use the Cloud Save Software Development Kit to save and load player data, game data, and files in your Unity project.

此教程演示了如何在 Unity 中使用 Cloud Save SDK 保存和加载数据。

> **Note:**
>
> **注意**：从 Cloud Save SDK 版本 3 开始提供这些功能。

## 在 Unity 中使用 Cloud Save##using-cloud-save-from-unity

您可以在 Unity 中从 C# 脚本调用 Cloud Save SDK。

1. 在 Unity 中创建 C# MonoBehaviour 脚本。
2. 添加代码以将数据保存到 Cloud Save 或从中加载数据（请参阅下面的示例）。
3. 将新制作的脚本附加到游戏对象。
4. 选择 **Play（播放）** 以运行项目，体验 Cloud Save 的运行效果。

要了解有关 Unity 中脚本编写的详细信息，请参阅[创建和使用脚本](https://docs.unity3d.com/Manual/CreatingAndUsingScripts.html)。

### SDK 示例##sdk-sample

SDK 附带的 `CloudSaveSample` 脚本对此教程中的示例进行了扩展。您可以在 Unity 中选择 **Package Manager（包管理器）** > **Cloud Save** > **Samples（示例）** 来查看示例。

## 包含身份验证的示例##example-with-authentication

以下示例演示了如何使用 Authentication 包以匿名身份验证方式登录玩家帐户，初始化 Cloud Save，并提供使用 Cloud Save 保存和加载数据的方法。

```cs
using UnityEngine;
using Unity.Services.Core;
using Unity.Services.Authentication;
using Unity.Services.CloudSave;
using Unity.Services.CloudSave.Models;
using Unity.Services.CloudSave.Models.Data.Player;
using SaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
using System.Collections.Generic;

public class CloudSaveSample : MonoBehaviour
{
    private async void Awake()
    {
        await UnityServices.InitializeAsync();
        await AuthenticationService.Instance.SignInAnonymouslyAsync();
    }

    public async void SaveData()
    {
        var playerData = new Dictionary<string, object>{
          {"firstKeyName", "a text value"},
          {"secondKeyName", 123}
        };
        await CloudSaveService.Instance.Data.Player.SaveAsync(playerData);
        Debug.Log($"Saved data {string.Join(',', playerData)}");
    }

    public async void LoadData()
    {
        var playerData = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string> {
          "firstKeyName", "secondKeyName"
        });

        if (playerData.TryGetValue("firstKeyName", out var firstKey)) {
            Debug.Log($"firstKeyName value: {firstKey.Value.GetAs<string>()}");
        }

        if (playerData.TryGetValue("secondKeyName", out var secondKey)) {
            Debug.Log($"secondKey value: {secondKey.Value.GetAs<int>()}");
        }
    }
}
```

## 玩家数据##player-data

您可以在 Unity Cloud 后台中查看、编辑和删除玩家的数据。

### 保存项##save-an-item

使用 `SaveAsync` 方法保存一个或多个项。

```cs
public async void SaveData()
{
    var data = new Dictionary<string, object>{{"keyName", "value"}};
    await CloudSaveService.Instance.Data.Player.SaveAsync(data);
}
```

如果您希望其他玩家能够读取数据，您可以将该数据保存在公开访问类中。

```cs
public async void SavePublicData()
{
    var data = new Dictionary<string, object>{{"keyName", "value"}};
    await CloudSaveService.Instance.Data.Player.SaveAsync(data, new SaveOptions(new PublicWriteAccessClassOptions()));
}
```

### 获取数据##fetch-data

使用 `LoadAsync` 方法获取作为玩家数据存储的键和值。

```cs
public async void LoadData()
{
    var playerData = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string>{"keyName"});
    if (playerData.TryGetValue("keyName", out var keyName)) {
        Debug.Log($"keyName: {keyName.Value.GetAs<string>()}");
    }
}
```

您可以获取**公开**和**受保护**访问类中的数据的键和值。

```cs
public async void LoadPublicData()
{
  var playerData = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string>{"keyName"}, new LoadOptions(new PublicReadAccessClassOptions()));
  if (playerData.TryGetValue("keyName", out var keyName)) {
      Debug.Log($"keyName: {keyName.Value.GetAs<string>()}");
  }
}
```

```cs
public async void LoadProtectedData()
{
  var playerData = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string>{"keyName"}, new LoadOptions(new ProtectedReadAccessClassOptions()));
  if (playerData.TryGetValue("keyName", out var keyName)) {
      Debug.Log($"keyName: {keyName.Value.GetAs<string>()}");
  }
}
```

您可以通过传递玩家 ID 来读取（但不能写入）另一个玩家的**公开**访问类中的数据。

```cs
public async void LoadPublicDataByPlayerId()
{
  var playerId = "JE1unrWOOzzbIwy3Nl60fRefuiVE";
  var playerData = await CloudSaveService.Instance.Data.Player.LoadAsync(new HashSet<string>{"keyName"}, new LoadOptions(new PublicReadAccessClassOptions(playerId)));
  if (playerData.TryGetValue("keyName", out var keyName)) {
      Debug.Log($"keyName: {keyName.Value.GetAs<string>()}");
  }
}
```

### 删除项##delete-an-item

使用 `DeleteAsync` 方法删除项。

```cs
public async void DeleteData()
{
    await CloudSaveService.Instance.Data.Player.DeleteAsync("key");
}
```

### 获取键列表##get-a-list-of-keys

使用 `ListAllKeysAsync` 方法获取键列表。

还可以获取元数据，例如上次修改键的时间。

```cs
public async void ListKeys()
{
    var keys = await CloudSaveService.Instance.Data.Player.ListAllKeysAsync();
    for (int i = 0; i < keys.Count; i++)
    {
        Debug.Log(keys[i].Key);
    }
}
```

还可以列出**公开**和**受保护**访问类中的数据的键。

```cs
public async void ListKeys()
{
    var keys = await CloudSaveService.Instance.Data.Player.ListAllKeysAsync(
      new ListAllKeysOptions(new PublicReadAccessClassOptions())
    );
    for (int i = 0; i < keys.Count; i++)
    {
        Debug.Log(keys[i].Key);
    }
}
```

```cs
public async void ListKeys()
{
    var keys = await CloudSaveService.Instance.Data.Player.ListAllKeysAsync(
      new ListAllKeysOptions(new ProtectedReadAccessClassOptions())
    );
    for (int i = 0; i < keys.Count; i++)
    {
        Debug.Log(keys[i].Key);
    }
}
```

### 查询玩家数据##query-player-data

查询玩家数据**公开**访问类中任何索引键的值。

> **Note:**
>
> 查询不会返回在创建索引之前保存的数据（或这些数据太大而无法创建索引）。

```cs
public async void QueryPlayerData()
{
  var query = new Query(
    // The first argument to Query is a list of one or more filters, all must evaluate to true for a result to be included
    new List<FieldFilter>
    {
        new FieldFilter("indexedKeyName", "value", FieldFilter.OpOptions.EQ, true),
        new FieldFilter("anotherIndexedKeyName", "otherValue", FieldFilter.OpOptions.NE, true)
    },
    // The second (optional) argument is a list of keys you want to be included with their values in the response
    // This may include keys which are not part of the index, and does not need to include the index keys
    // If you don't specify any, you will still get back a list of IDs for Players that matched the query
    new HashSet<string>
    {
        "indexedKeyName"
    }
  );

  var results = await CloudSaveService.Instance.Data.Player.QueryAsync(query, new QueryOptions());

  Debug.Log($"Number of results from query: {results.Count}");
  results.ForEach(r =>
  {
      Debug.Log($"Player ID: {r.Id}");
      r.Data.ForEach(d => Debug.Log($"Key: {d.Key}, Value: {d.Value.GetAsString()}"));
  });
}
```

## 游戏数据##game-data

您可以使用**自定义项**保存和加载非玩家特定的**游戏数据**。

> **Note:**
>
> 游戏客户端只能读取自定义项。客户端不能创建、修改或删除自定义项。可以使用 Cloud Code、游戏服务器、HTTP API 或 Unity Cloud 后台写入自定义项。

### 获取数据##fetch-data

使用 `LoadAllAsync` 获取自定义项的所有数据。

```cs
public async void LoadCustomItemData()
{
  var customItemId = 'my-custom-item';
  var customItemData = await CloudSaveService.Instance.Data.Custom.LoadAllAsync(customId);
  foreach (var customItem in customItemData)
  {
      Debug.Log($"Key: {customItem.Key}, Value: {customItem.Value.Value}");
  }
}
```

### 查询自定义项##query-custom-items

查询自定义项**默认**访问类中任何索引键的值。

> **Note:**
>
> 查询不会返回在创建索引之前保存的数据（或这些数据太大而无法创建索引）。

```cs
public async void QueryCustomItems()
{
  var query = new Query(
    // The first argument to Query is a list of one or more filters, all must evaluate to true for a result to be included
    new List<FieldFilter>
    {
        new FieldFilter("indexedKeyName", "value", FieldFilter.OpOptions.EQ, true),
        new FieldFilter("anotherIndexedKeyName", "otherValue", FieldFilter.OpOptions.NE, true)
    },
    // The second (optional) argument is a list of keys you want to be included with their values in the response
    // This may include keys which are not part of the index, and does not need to include the index keys
    // If you don't specify any, you will still get back a list of IDs for Players that matched the query
    new HashSet<string>
    {
        "indexedKeyName"
    }
  );

  var results = await CloudSaveService.Instance.Data.Custom.QueryAsync(query);

  Debug.Log($"Number of results from query: {results.Count}");
  results.ForEach(r =>
  {
      Debug.Log($"Custom Item ID: {r.Id}");
      r.Data.ForEach(d => Debug.Log($"Key: {d.Key}, Value: {d.Value.GetAsString()}"));
  });
}
```

## 玩家文件##player-files

[Cloud Save Files](/cloud-save/concepts/files.md) 提供了一种以任何格式存储大型保存文件（最大 1 GB）以及跨设备/平台访问保存的游戏文件的方法。

### 保存玩家文件##save-player-file

读取设备上本地保存的文件，并使用 `SaveAsync` 方法保存到 Cloud Save。

```cs
public async void SavePlayerFile()
{
    byte[] file = System.IO.File.ReadAllBytes("fileName.txt");
    await CloudSaveService.Instance.Files.Player.SaveAsync("fileName", file);
}
```

### 删除玩家文件##delete-player-file

使用 `DeleteAsync` 方法删除玩家文件。

```cs
public async void DeletePlayerFile()
{
    await CloudSaveService.Instance.Files.Player.DeleteAsync("fileName");
}
```

### 列示玩家文件##list-player-files

使用 `ListAllAsync` 方法获取玩家的所有文件名称列表。

```cs
public async void ListPlayerFiles()
{
    List<FileItem> files = await CloudSaveService.Instance.Files.Player.ListAllAsync();
    for (int i = 0; i < files.Count; i++)
    {
        Debug.Log(files[i]);
    }
}
```

### 以字节数组形式获取玩家文件##get-player-file-as-a-byte-array

使用 `LoadBytesAsync` 方法以字节数组形式获取玩家文件。

```cs
public async void GetPlayerFileAsByteArray()
{
    byte[] file = await CloudSaveService.Instance.Files.Player.LoadBytesAsync("fileName");
}
```

### 以流形式获取玩家文件##get-player-file-as-a-stream

使用 `LoadStreamAsync` 方法以流形式获取玩家文件。

```cs
public async void GetPlayerFileAsStream()
{
    Stream file = await CloudSaveService.Instance.Files.Player.LoadStreamAsync("fileName");
}
```

### 获取玩家文件元数据##get-player-file-metadata

使用 `GetMetadataAsync` 方法获取单个玩家文件的元数据（大小、最后修改和创建日期、键、内容类型和当前 WriteLock）。

```cs
public async void GetPlayerFileMetadata()
{
    var metadata = await CloudSaveService.Instance.Files.Player.GetMetadataAsync("fileName");
    Debug.Log(metadata.Key);
    Debug.Log(metadata.Size);
    Debug.Log(metadata.ContentType);
    Debug.Log(metadata.Created);
    Debug.Log(metadata.LastModified);
    Debug.Log(metadata.WriteLock);
}
```

您可以使用 Unity Cloud 后台访问单个玩家的文件。在 Cloud Save 模块的玩家详细信息下可以找到这些文件。

## 错误处理##error-handling

在错误处理逻辑的方法中使用 SDK 返回的错误代码或类型。

避免使用错误消息，因为它们将来可能会发生变化。
