Photon Fuisonをunityで採用しつつ、Playfab Multiplayer上（Playfabの中でマルチプレイ用のサーバーサービス）を実現するための導入をイメージしやすく（自分が）するために、擬人化（おっさん化）して、文章にしたためてみました。

Playfab、Photonfusionを全く知らないおっさん４９歳がだいたい１ヶ月ぐらいで、遠回りしまくってみた内容をまとめてみました。

:::note info
すごいわかる人には、下記の二つを見ればOK
https://doc.photonengine.com/ja-jp/fusion/current/technical-samples/dedicated-server/fusion-dedicated-server
https://learn.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/mps-unity
:::

## 主な役割を担うおっさん達
:man:Server
:man:Agent
:man:Heartbeat
:man:Build
:man:Client
５つのおっさんが必要

## ローカルでの開発に必要なおっさん達
:man:Docker Desktop
:man:LocalMpsAgent

### おっさん達の詳細
<dl>
  <dt>Server</dt>
  <dd>Playfab Multiplayer
Playfab内で、複数人数用のサーバーのサービスを用意してくれるおっさん</dd>
  <dt>Agent</dt>
<dd>「Playfab Multiplayerおっさん」を呼び出すためのおっさん</dd>
<dt>Heartbeat</dt>
<dd>「Serverのおっさん」と、「Agentのおっさん」との間で、生きてますかぁ？生きてますよぉの生存確認を伝えるおっさん</dd>
<dt>Build</dt>
<dd>画面は持たずClientのリクエストに応じて返事をするおっさん
Unity で書いたServerの役割を持つ部分
FuisonのRunnerのインスタンを持ち回し、StartSimulationで、環境を作る</dd>
<dt>Client</dt>
<dd>エンドユーザーの端末に入りユーザーからのボタンや十字キーからの命令を一旦受け取ったり、画面を生成するおっさん</dd>

</dl>


下記が実際のコードUnityで適当にobjectを作りスクリプトを当てれば大丈夫
```
namespace Fusion.Sample.DedicatedServer
{
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using PlayFab;
    using Fusion.Photon.Realtime;
    using Fusion.Sockets;
    using System.Collections;
    using System.Threading.Tasks;
    using System.Net;
    
    public class ServerManager : MonoBehaviour
    {
        [SerializeField] private NetworkRunner _runnerPrefab;
        public bool Debugging = true;
        
        public void Awake()
        {
            Debug.Log("ServerManager Awake");
            if (CommandLineUtils.IsHeadlessMode() == false) {
                SceneManager.LoadScene((int)SceneDefs.CLIENTMENU, LoadSceneMode.Single);
                return;
            }
        }
        void Start () {
            Debug.Log("ServerManager Start");
            PlayFabMultiplayerAgentAPI.Start();
            PlayFabMultiplayerAgentAPI.IsDebugging = Debugging;
            PlayFabMultiplayerAgentAPI.OnServerActiveCallback += OnServerActive;
            StartCoroutine(ReadyForPlayers());
        }
        
        IEnumerator ReadyForPlayers()
        {
            Debug.Log("ServerManager ReadyForPlayers");
            yield return new WaitForSeconds(.5f);
            PlayFabMultiplayerAgentAPI.ReadyForPlayers();
        }
    
        private void OnServerActive()
        {
            Debug.Log("ServerManager OnServerActive");
            
            //For Fusion
            StartFusion();
        }
        async void StartFusion() {
          Debug.Log("ServerManager StartFusion");
          Application.targetFrameRate = 30;

          var config = DedicatedServerConfig.Resolve();
          Debug.Log(config);
          
          var runner = Instantiate(_runnerPrefab);
          var result = await StartSimulation(
            runner,
            config.SessionName,
            config.SessionProperties,
            config.Port,
            config.Lobby,
            config.Region,
            config.PublicIP,
            config.PublicPort
          );
          
          if (result.Ok) {
            Log.Debug($"Runner Start DONE");
          } else {
            Log.Debug($"Error while starting Server: {result.ShutdownReason}");
            Application.Quit(1);
          }
        }

        private Task<StartGameResult> StartSimulation(
          NetworkRunner runner,
          string SessionName,
          Dictionary<string, SessionProperty> customProps,
          ushort port,
          string customLobby,
          string customRegion,
          string customPublicIP = null,
          ushort customPublicPort = 0
        ) {

          Debug.Log("StartFusion StartSimulation");
          
          // Build Custom Photon Config
          var photonSettings = PhotonAppSettings.Instance.AppSettings.GetCopy();

          if (string.IsNullOrEmpty(customRegion) == false) {
            photonSettings.FixedRegion = customRegion.ToLower();
          }

          // Build Custom External Addr
          NetAddress? externalAddr = null;

          if (string.IsNullOrEmpty(customPublicIP) == false && customPublicPort > 0) {
            if (IPAddress.TryParse(customPublicIP, out var _)) {
              externalAddr = NetAddress.CreateFromIpPort(customPublicIP, customPublicPort);
            } else {
              Log.Warn("Unable to parse 'Custom Public IP'");
            }
          }
          
          return runner.StartGame(new StartGameArgs() {
            SessionName = SessionName,
            GameMode = GameMode.Server,
            SceneManager = runner.gameObject.AddComponent<NetworkSceneManagerDefault>(),
            Scene = (int)SceneDefs.CLIENTGAME,
            SessionProperties = customProps,
            Address = NetAddress.Any(port),
            CustomPublicAddress = externalAddr,
            CustomLobbyName = customLobby,
            CustomPhotonAppSettings = photonSettings,
          });
        }
    }
}
```

Buildした後は、linuxの場合、Dockerを使ってイメージ化が必要
docker build -t [yourplayfabuserIDforRegistory]/[yourgamename]:1.0
docker push [yourplayfabuserIDforRegistory]/[yourgamename]:1.0

Pushすると、PlayfabMultiplayerの管理画面でOSをLinuxにした場合に選択できる。


#### Dockerfile
```
# base image
FROM ubuntu:bionic

# installRUN apt-get --yes update && \
    apt-get --yes clean && \
    apt-get --yes install sudo

# add and setup fusion user
RUN useradd -m fusion && \
    echo "fusion:fusion" | chpasswd && \
    adduser fusion sudo

# copy server binary files
COPY --chown=fusion:fusion bin /home/fusion/bin

# set permissions on entrypoint
RUN chmod +x /home/fusion/bin/entrypoint.sh

# execute container as this user
USER fusion

# setup start scriptENTRYPOINT ["/home/fusion/bin/entrypoint.sh"]

# Fusion Server Port
EXPOSE 27015/udp
```

一旦疲れたのでここで終了。
他の方がめっちゃ丁寧に文章を書いてくれてるんだなと、改めてすごいと思う。
いつもありがとうございます。