# Photon Fusion 1

### 在 Arbitrium 上的 Photon Fusion

本指南将帮助您为使用以下内容的 Unity 项目在 Edgegap 上创建无头服务器 [Photon Fusion](https://doc.photonengine.com/en-us/fusion/current/getting-started/fusion-intro) 作为其联网解决方案。

我们将使用示例项目 [来自 Photon Fusion 的 Tanknarok](https://doc.photonengine.com/en-us/fusion/current/samples/fusion-tanknarok) 作为示例。

因为当 Edgegap 知道所有玩家的位置信息时（以便选择最佳位置的服务器）效果最佳，Tanknarok 游戏将首先以客户端托管模式启动，然后在所有玩家加入后切换到 Edgegap 服务器。这是因为 Tanknarok 在比赛开始前有一个可玩的大厅，玩家可以随意加入和离开。所以逻辑流程如下：

1. 玩家使用相同的房间通过连接 `AutoHostOrClient` 模式。该模式会自动选择房间中的第一个玩家作为主机。
2. 所有玩家在加入时将他们的 IP 地址发送给主机。
3. 一旦玩家准备就绪，主机会使用这些 IP 地址启动一个 Edgegap 服务器版本的游戏，并使用随机化的房间代码（使用 guid）。
4. 当主机看到 Edgegap 实例已完成部署时，会将新的房间代码告知其他玩家。
5. 包括主机在内的所有玩家断开当前房间连接，并以客户端模式重新连接到由 Edgegap 服务器托管的新房间。
6. 当 Edgegap 服务器看到所有预期玩家都已加入时，它开始比赛。
7. 当 Edgegap 服务器看到所有人都已离开比赛时，它会自行关闭。

### 账户设置

要运行此应用，您需要同时拥有 Photon 帐户和 Edgegap 帐户。首先，您需要在 Photon 仪表板上创建一个 Fusion 应用并在 `PhotonAppSettings` 脚本化对象文件中设置应用 ID。然后，您需要在仪表板上添加一个应用并创建一个 API 令牌。您将使用这些在 `EdgegapConfig` 脚本化对象文件中设置应用名称、版本和 API 令牌。

{% hint style="info" %}
在创建 Edgegap 应用时，该程序需要 512 CPU 单位和 512 MB 内存。
{% endhint %}

还有两个插件可以帮助开发：用于更好 JSON 序列化的 Newtonsoft Json 和 [ParrelSync](https://github.com/VeriorPies/ParrelSync) 用于更方便地在多个 Unity 编辑器中测试。

### 第 1 步：服务器模式设置

您可以在获取修改后的 Tanknarok 项目 [GitHub](https://github.com/edgegap)。您还需要将 [Photon Fusion SDK](https://doc.photonengine.com/en-us/fusion/current/getting-started/sdk-download) 导入到项目中。

Photon Fusion 的 Tanknarok 项目需要稍作修改以用作服务器。这主要涉及自动启动比赛并设置模式。

1. 已修改的 `GameLauncher.cs` 以在启动时使用启动 `GameMode.Server`.
2. 已修改的 `FusionLauncher.cs` 来调用 `_spawnWorldCallback` 一旦游戏运行而不是依赖于 `InstantiatePlayer` （因为服务器不会实例化玩家）。
3. 如果是服务器则跳过大厅场景，在所有玩家加入后直接进入比赛。

这已在我们可用的修改过的 Tanknarok 项目中完成， [GitHub](https://github.com/edgegap).

#### Dockerfile

Docker 文件非常基础。我们只是使用一个 ubuntu 基础镜像，复制已编译的 Linux 服务器构建，设置文件权限，然后运行它。

### 第 2 步：第一个客户端

初始客户端承担主机和管理 Edgegap 服务器的角色。它通过 `EdgegapManager.cs` 文件来完成，该文件又使用 `EdgegapAPIInterface.cs` 文件，该文件封装了与 Edgegap 交互的大部分功能。

在 `GameManager.cs`, `OnAllPlayersReady` 主机的函数将部署一个 Edgegap 服务器而不是直接开始比赛。它根据所有玩家的 IP 地址来完成此操作。Unity 协程允许这些操作异步发生。Deploy 方法接受一个回调函数，该函数会接收服务器将使用的新房间名称。一个 Photon RPC 函数会发送以通知所有客户端切换。

```cs
    if (EdgegapManager.EdgegapPreServerMode)
    {
        // 如果是主机，启动新的 edgegap 服务器
        // 一旦服务器准备就绪，连接到它

        FindObjectOfType<GameLauncher>().ShowMatchStartingUI();

        if (Object.HasStateAuthority)
        {
            string[] ips = PlayerManager.allPlayers.Select(p => p.ipAddress).ToArray();
            StartCoroutine(EdgegapManager.Instance.Deploy(ips, RpcOnEdgegapServerReady));
        }
    }
    else
    {
        LoadLevel(_levelManager.GetRandomLevelIndex(), -1);
    }
```

切换发生在名为的空场景中 `TransferringScene`.

在 EdgegapManager 中，会创建一个随机化的房间代码，并将其连同玩家数量作为环境变量通过部署 API 传递给新创建的实例。这与 `status` API 调用结合，每秒开始检查实例何时完成部署。您也可以使用 [webhook](https://docs.edgegap.com/zh/docs/deployment/arbitrium-deploy-webhook) 选项时，动态注入环境变量。

```cs
    /// <summary>
    /// 调用 Edgegap API 来部署服务器
    /// </summary>
    /// <param name="clientIpList">客户端 IP 地址列表</param>
    /// <param name="OnDeployed">带有会话名称作为参数的回调方法</param>
    public IEnumerator Deploy(string[] clientIpList, Action<string> OnDeployed = null)
    {
        var roomCode = Guid.NewGuid().ToString();
        yield return EdgegapAPIInterface.RequestDeployment(clientIpList,
             new Dictionary<string, string> {
                { "room_code", roomCode },
                { "player_count", clientIpList.Length.ToString() } });

        yield return EdgegapAPIInterface.WaitForDeployment();

        OnDeployed(roomCode);
    }
```

### 第 3 步：加入玩家

除了在被告知时切换到新的 Edgegap 服务器外，加入的玩家还需要将他们的 IP 地址告知主机。由于 Photon 不提供此信息，因此在 `GameManager` 中的一个 RPC 调用名为 `ShareIpAddress` 从 `Player.cs` 类的 `Spawned` 方法中被调用。

然后主机会根据 RPC 消息的来源将这些 IP 地址存储在所有玩家的玩家类中：

```cs
    [Rpc(sources:RpcSources.All, RpcTargets.StateAuthority)]
    public void RpcShareIpAddress(string ipAddress, RpcInfo info = default)
    {
        if (info.Source == PlayerRef.None)
        {
            // 本地玩家的来源将为 None
            Player.local.ipAddress = ipAddress;
            return;
        }

        foreach (var player in PlayerManager.allPlayers)
        {
            if (player.playerID == info.Source.PlayerId)
            {
                player.ipAddress = ipAddress;
                break;
            }
        }
    }
```

### 第 4 步：清理

回到服务器中，已修改的 `GameManager.cs` 在主 Update 函数中跟踪比赛中的玩家数量。它既检查目标玩家数量何时已加入以便开始比赛，也确定何时应关闭自身。如果游戏已运行超过一分钟且没有玩家，则调用 Edgegap API 来停止其自身的部署。

```cs
    if (EdgegapManager.IsServer())
    {
        if (GameManager.playState == GameManager.PlayState.LOBBY)
        {
            // 如果我们在大厅并且所有玩家都已加入，则开始比赛
            if (PlayerManager.allPlayers.Count >= EdgegapManager.ExpectedPlayerCount)
            {
                OnAllPlayersReady();
            }
        }

        if(Time.time > 60 && PlayerManager.allPlayers.Count == 0
            && GameManager.playState != GameManager.PlayState.TRANSITION)
        {
            // 如果已经超过一分钟且没有人，停止服务器
            Debug.LogWarning("Shutting Down");
            StartCoroutine(EdgegapAPIInterface.StopDeploymentFromServer());
            GameManager.playState = PlayState.TRANSITION;
        }
    }
```

现在您有一个可按需部署的 Photon Fusion 项目！
