# 模块结构

> Understand the structure of a Cloud Code module project and how it exposes endpoints.

Cloud Code 模块是简单的 .NET 项目，其中公开了游戏客户端可以调用的终端。模块采用与 .NET 类库类似的范式。例如，一个标准 C# 类可能如下所示：

```csharp
namespace ModulesExample;

public class Example
{
    private static string[] GetCheatCodes()
    {
        return new string[]
        {
            "LEAVEMEALONE",
            "ASPIRINE",
            "BIGBANG"
        };
    }

    public void ChangeHaircut(string haircutName)
    {

    }
}
```

编译后，任何对 `Example` 类的引用都只能使用 `ChangeHaircut()` 方法。

```csharp
using ModulesExample;

class UsageExample
{
    public void GameLoop()
    {
        Example.ChangeHaircut("buzzcut");

        // Compile error "Cannot access private method 'GetCheatCodes()' here"
        var cheatCodes = Example.GetCheatCodes();
    }
}
```

开发者可以通过访问修饰符选择库接口。Cloud Code 模块的工作方式相同，但使用的是 `[CloudCodeFunction("ChangeHaircut")]` 属性。

借助 Cloud Code 模块，您可以创建独立的服务器授权库。因此，您可以在服务器上运行游戏逻辑，并从客户端调用游戏逻辑，从而将游戏逻辑与客户端分离并提高安全性。模块支持代码重用，并提供可用于管理游戏逻辑的接口。

## C# 项目##c-project

Cloud Code 模块项目是类库 C# 项目。此项目也可以是某个解决方案的一部分。最简单的项目结构如下所示：

```text
├─ Main.csproj
    └─ Dependencies
    └─ HelloWorld.cs
```

如果是在一个解决方案中，可能如下所示：

```text
Sample.sln
├─ Main.csproj
    └─ Dependencies
    └─ HelloWorld.cs
```

这个解决方案还可以包含相互引用的多个项目。因此，您可以轻松添加测试项目，并启用代码重用。以下示例显示了一个有效的项目结构：

```text
Sample.sln
├─ Main.csproj
    └─ Dependencies
    └─ HelloWorld.cs
├─ Tests.csproj
    └─ Dependencies
    └─ Tests.cs
├─ Other.csproj
    └─ Dependencies
    └─ Class1.cs
```

但是，只有主项目可以包含 Cloud Code 函数。

请参阅[编写单元测试](./unit-testing.md)，详细了解如何为 Cloud Code 模块编写单元测试。

请参阅 [Cloud Code 限制](../reference/limits)，了解适用的存储限制。

### 主项目##main-project

尽管解决方案可以包含多个 C# 项目，但每个模块只能有一个主项目。主项目包含所有模块入口点（即具有 `CloudCodeFunction` 属性的函数）。

> **Warning:**
>
> **重要**：如果您将任何具有 `CloudCodeFunction` 属性的函数置于主项目外部，则 Cloud Code 服务不会将此类函数识别为入口点。

主项目也是您部署到 Cloud Code 服务的项目。主项目中未引用的项目和依赖项不会部署到 Cloud Code 服务。

您需要让主项目的名称与部署到 Cloud Code 服务的存档文件的名称匹配。

> **Note:**
>
> **注意**：您可以创建一个包含多个 Cloud Code 模块主项目的解决方案。在这种情况下，每个主项目对应一个不同的模块。

### 示例项目##sample-project

最简单的模块项目可以具有以下结构：

```text
Sample.sln
├─ Main.csproj
    └─ Dependencies
    └─ HelloWorld.cs
```

以下示例定义的 Cloud Code 模块包含一个返回 `Hello World!` 字符串的 `Hello` 函数：

```csharp
using Unity.Services.CloudCode.Core;

namespace Sample;

public class HelloWorld
{
    [CloudCodeFunction("Hello")]
    public string PrintHello()
    {
        return "Hello World!";
    }
}
```

## Cloud Code NuGet 包##cloud-code-nuget-packages

作为 .NET 项目，Cloud Code 模块项目也使用 NuGet 来共享打包的代码。

要了解有关 NuGet 的更多信息，请参阅 [Microsoft 文档](https://learn.microsoft.com/en-us/nuget/what-is-nuget)。

Cloud Code 提供了两个可以包含在模块中的包：

* `com.Unity.Services.CloudCode.Core`：此包中包含 Cloud Code 的核心功能，包括 `CloudCodeFunction` 属性。所有 Cloud Code 模块都需要此包。
* `com.Unity.Services.CloudCode.Apis`：这是一个可选包，其中包含可用于与 Cloud Code 服务交互的 Unity Gaming Services（Unity 游戏服务）API。您可以使用此包调用其他 Cloud Code SDK，例如 Cloud Save。

如需了解更多信息，请参阅有关如何使用 [Jetbrains Rider](https://www.jetbrains.com/help/rider/Using_NuGet.html) 或 [Visual Studio](https://learn.microsoft.com/en-us/nuget/install-nuget-client-tools) 在项目中安装和使用 NuGet 包的文档。

> **Note:**
>
> **注意**：还可以安装要在模块项目中使用的任何外部 NuGet 包。如需了解更多信息，请参阅[与外部服务集成](./external-services-integration.md)。

## 代码结构##code-structure

为了避免代码结构问题，请遵循以下准则。

### CloudCodeFunction 属性##cloudcodefunction-attribute

要将方法标记为 Cloud Code 函数并使其成为可行的 Cloud Code 模块终端，请使用 `CloudCodeFunction` 属性。您只能在模块的主类中使用 Cloud Code 函数属性。

为了调用该函数，需要调用属性的名称。

### 方法签名##method-signature

使用以下结构作为模块函数的方法签名。

```csharp
[CloudCodeFunction("FunctionName")]
public async Task<ReturnType> FunctionName(IGameApiClient gameApiClient, IExecutionContext ctx, string parameter1, string parameter2)
{
    ...
}
```

> **Warning:**
>
> **重要**：`async void` 模式会导致错误。始终返回 `Task` 或 `Task<T>`。请参阅官方 [Microsoft 文档](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming)以了解有关异步编程的最佳做法。

> **Warning:**
>
> **重要**：Cloud Code 不支持 Cloud Code 函数的静态方法。

### 对模块使用单例模式##use-singleton-pattern-for-modules

您可以使用单例模式来与 Cloud Code C# 包和外部服务调用进行集成。单例模式可提高 Cloud Code 中的性能和可扩展性，并确保您不会为同一对象创建多个实例。

您可以使用[依赖项注入](./initialize-modules/dependency-injection.md)将 `IGameApiClient` 和 `IPushClient` 接口注入到模块中。

> **Warning:**
>
> **重要**：不要依赖模块函数调用之间的共享状态，因为当有多个工作线程时，不能保证 API 调用会落在同一个工作线程上。

> **Warning:**
>
> **重要**：Cloud Code 工作线程的内存限制为 256 MB。超过此限制的工作线程最终会崩溃。

## 传递参数##passing-in-parameters

您可以将参数传递给模块函数。

### 原始类型和复杂类型##primitive-and-complex-types

包括任何原始类型（例如 `int`、`string`、`bool` 和 `float`）以及复杂类型（例如 `List<T>`、`Dictionary<T>` 和 `Dictionary<string, T>`）。

例如，可以将 `string` 和 `List<string>` 传递给模块函数：

```json
{
  "params": {
    "parameter1": "a",
    "parameter2": ["b", "c"]
  }
}
```

定义具有相同参数的模块函数：

```csharp
[CloudCodeFunction("SimpleParams")]
public async Task SimpleParams(IExecutionContext ctx, string parameter1, List<string> parameter2)
{
  return parameter1 + parameter2[0] + parameter2[1];
}
```

### 自定义类型##custom-types

您还可以传递在模块项目中定义的任何自定义类型。

例如，可以将 JSON 对象传递给模块对象：

```json
{
  "params": {
    "player": {
      "name": "player",
      "health": 50,
      "stamina": 20,
      "inventory": {
        "count": 5,
        "weight": 20
      }
    }
  }
}
```

在您的模块项目中定义一个 `Player` 和 `Inventory` 类和一个函数：

```csharp
public class Player
{
    public string Name { get; set; }
    public int Health { get; set; }
    public int Stamina { get; set; }

    public Inventory Inventory { get; set; }
}

public class Inventory
{
    public int Count { get; set; }
    public int Weight { get; set; }
}

[CloudCodeFunction("CustomParams")]
public async Task<string> CustomParams(IExecutionContext ctx, Player player)
{
    return $"{player.Name} has {player.Health} health and {player.Stamina} stamina and {player.Inventory.Count} items in their inventory with a total weight of {player.Inventory.Weight}";
}
```

## 可用接口##available-interfaces

Cloud Code 模块可以使用 `IGameApiClient`、`IAdminApiClient`、`IPushClient` 和 `IExecutionContext` 接口来扩展模块的功能。

`ICloudCodeSetup` 接口可用于管理模块的依赖项。请在[依赖项注入](./initialize-modules/dependency-injection.md)文档中了解更多相关信息。

### IGameApiClient 接口##igameapiclient-interface

`IGameApiClient` 接口提供了一种访问 Cloud Code C# Client SDK 的方法。您可以使用[依赖项注入](./initialize-modules/dependency-injection.md)将 `IGameApiClient` 注入到模块中。

1. 定义一个实现 `ICloudCodeSetup` 接口的类，并添加 `GameApiClient` 为单例：
   ```csharp
    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(GameApiClient.Create());
        }
    }
   ```
2. 将 `IGameApiClient` 接口作为函数参数传入模块函数。

### IAdminApiClient 接口##iadminapiclient-interface

`IAdminApiClient` 接口提供了一种访问 Cloud Code C# Admin SDK 的方法。您可以使用[依赖项注入](./initialize-modules/dependency-injection.md)将 `IAdminApiClient` 注入到模块中。

1. 定义一个实现 `ICloudCodeSetup` 接口的类，并添加 `AdminApiClient` 为单例：
   ```csharp
    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(AdminApiClient.Create());
        }
    }
   ```
2. 将 `IAdminApiClient` 接口作为函数参数传入模块函数。

### IPushClient 接口##ipushclient-interface

`IPushClient` 接口提供了一种向玩家发送推送通知的方法。您可以使用[依赖项注入](./initialize-modules/dependency-injection.md)将 `IPushClient` 注入到模块中。

1. 定义一个实现 `ICloudCodeSetup` 接口的类，并添加 `PushClient` 为单例：
   ```csharp
    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(PushClient.Create());
        }
    }
   ```
2. 将 `PushClient` 对象作为函数参数传入模块函数。

要详细了解如何从 Cloud Code 发送推送消息，请参阅[推送消息](./push-messages.md)。

### IExecution 上下文##iexecution-context

要在模块中调用其他 UGS 服务，可以使用 `IExecutionContext` 接口获取有关当前调用的额外信息。您可以使用以下值访问[服务 SDK](./unity-services-integration.md)，并查看哪个玩家发出了请求。

* `ProjectId`：调用方通过身份验证的 Project ID。
* `PlayerId`：执行脚本的玩家 ID。**注意**：当服务调用脚本时，玩家 ID 不可用。
* `EnvironmentId`：模块的环境 ID。
* `EnvironmentName`：模块的环境名称。
* `AccessToken`：玩家用于向 Cloud Code 进行身份验证的 JSON Web Token (JWT) 凭据。
* `UserId`：服务帐户的用户 ID（注意：当服务调用脚本时，用户 ID 不可用）。
* `Issuer`：签发者或服务帐户令牌（注意：当玩家调用脚本时，签发者不可用）。例如，如果 Multiplay 发出调用，则签发者为 `multiplay`。
* `ServiceToken`：Cloud Code 服务帐户 JWT 凭据。
* `AnalyticsUserId`：玩家的 Analytics 用户 ID（注意：当服务调用脚本时，Analytics 用户 ID 不可用）。
* `UnityInstallationId`：玩家的 Unity 设备安装 ID（注意：当服务调用脚本时，Unity 安装 ID 不可用）。

您可以使用执行上下文获取玩家 ID 并向不同的 Unity Gaming Services（Unity 游戏服务）进行身份验证。您可以通过类的构造函数以及具有 `CloudCodeFunction` 属性的任何方法获取执行上下文。

例如，您可以使用执行上下文来调用 Cloud Save 获取玩家密钥：

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Shared;

namespace Sample;

public class HelloWorld
{
    private static ILogger<HelloWorld> _logger;
    public CloudSaveSdkSample(ILogger<HelloWorld> logger)
    {
        _logger = logger;
    }

    [CloudCodeFunction("GetData")]
    public async Task<object> GetData(IExecutionContext context, IGameApiClient gameApiClient, string key)
    {
        try
        {
            var result = await gameApiClient.CloudSaveData.GetItemsAsync(context, context.AccessToken,
                context.ProjectId, context.PlayerId, new List<string> { key });
            return result.Data.Results.First().Value;
        }
        catch (ApiException ex)
        {
            _logger.LogError("Failed to get data from Cloud Save. Error: {Error}", ex.Message);
            throw new Exception($"Failed to get data for playerId {ctx.PlayerId}. Error: {e.Message}");
        }
    }

    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(GameApiClient.Create());
        }
    }


}
```

#### 令牌身份验证##token-authentication

`AccessToken` 和 `ServiceToken` 是 [JWT](https://jwt.io/)，也是 `IExecutionContext` 接口的属性。

您可以使用这些令牌对 Cloud Code 的其他 Unity Gaming Services（Unity 游戏服务）调用进行身份验证。

| 令牌类型           | 来源                                                            | 数据访问         | 用途                                                                              |
| -------------- | ------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- |
| `accessToken`  | 由 [Authentication 服务](/authentication/use-anon-sign-in.md)生成。 | 仅限经过身份验证的玩家。 | `AccessToken` 是用于对 Cloud Code 调用进行身份验证的 JWT。您可以将此令牌传递给其他 UGS 服务来访问经过身份验证的玩家的数据。 |
| `serviceToken` | 由 Cloud Code 生成。                                              | 跨玩家数据访问。     | `ServiceToken` 是用于调用其他 UGS 服务并处理跨玩家数据的令牌。                                       |

您配置的任何[访问控制](./access-control.md)规则都会影响 `AccessToken`。

以下示例显示了如何使用 `AccessToken` 通过 Cloud Save 访问经过身份验证的玩家的数据：

```csharp
using Microsoft.Extensions.DependencyInjection;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Shared;
using Unity.Services.CloudSave.Model;

namespace TokenSample;

public class TokenSample
{
    [CloudCodeFunction("GetKeysForAuthenticatedPlayer")]
    public  Task<ApiResponse<GetKeysResponse>> GetPlayerKeys(IExecutionContext ctx, IGameApiClient gameApiClient)
    {
        try
        {
            // Using ctx.AccessToken to access the data for the player calling the function
            return gameApiClient.CloudSaveData.GetKeysAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.PlayerId);
        }
        catch (ApiException e)
        {
            throw new Exception($"Failed to get keys for playerId {ctx.PlayerId}. Error: {e.Message}");

        }
    }

    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(GameApiClient.Create());
        }
    }
}
```

要访问所有玩家的数据，您可以使用 `ServiceToken` 并传入 `playerId` 的参数：

```csharp
using Microsoft.Extensions.DependencyInjection;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Shared;
using Unity.Services.CloudSave.Model;

namespace TokenSample;

public class TokenSample
{
    [CloudCodeFunction("GetKeysForAnyPlayer")]
    public  Task<ApiResponse<GetKeysResponse>> GetPlayerKeys(IExecutionContext ctx, IGameApiClient gameApiClient, string playerId)
    {
        try
        {
            // Using ctx.ServiceToken to access the data for any player
            return gameApiClient.CloudSaveData.GetKeysAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId);
        } catch (ApiException e)
        {
            throw new Exception($"Failed to get keys for playerId {ctx.PlayerId}. Error: {e.Message}");
        }
    }

    public class ModuleConfig : ICloudCodeSetup
    {
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(GameApiClient.Create());
        }
    }
}
```

如需了解这两个令牌之间差异的深入说明，请参阅[服务和访问令牌](./token-support.md)文档。

## Cloud Code 模块元数据##cloud-code-module-metadata

要获取 Cloud Code 模块及其元数据，您可以向 Cloud Code 服务发送 `GET` 请求。响应是具有以下结构的 JSON 对象：

```json
{
  "name": "string",
  "language": "CS",
  "tags": {
    "property1": "string",
    "property2": "string"
  },
  "signedDownloadURL": "string",
  "dateCreated": "2022-04-05T09:12:13Z",
  "dateModified": "2022-04-05T09:12:13Z"
}
```

### 标签##tags

标签是可以附加到 Cloud Code 模块的键/值对。您可以在标签中记录所需的任何信息，例如模块的版本、作者或提交哈希（如果使用版本控制）。

模块最多可以存储 10 个标签，每个标签键和标签值最多可以使用 128 个字符。

> **Note:**
>
> **注意**：仅当您使用 [Cloud Code API](./run-modules/rest-api.md) 进行创作时才支持自定义标签。使用 [Unity Gaming Services（Unity 游戏服务）CLI](./write-modules/cli.md) 和 [Unity 编辑器](./write-modules/unity-editor.md)进行创作时，Cloud Code 会忽略标签。

### 签名下载 URL##signed-download-url

签名下载 URL (Signed Download URL) 是一种可用于下载模块存档文件的临时 URL。
