This document is about: QUANTUM 2
SWITCH TO

Snippets

HTTP請求

自訂Quantum外掛程式

延遲外掛程式回調,比如在建立遊戲(),以從一個受信任的後端來擷取任何特定的房間,並且立即阻擋房間建立。針對以一個資訊物件(在加入(),...)調用房間回調,同樣的情形是可能的。

C#

public override void OnCreateGame(ICreateGameCallInfo info) {
  var request = new HttpRequest() { Url = "http://microsoft.com", Async = false, Callback = OnCreateGameContinue };
  PluginHost.HttpRequest(request, info);

  // Do not call base.OnCreateGame() to prevent the continuation
}

private void OnCreateGameContinue(IHttpResponse response, object userState) {
  // Complete the OnCreateGame() call
  base.OnCreateGame((ICreateGameCallInfo)response.CallInfo);
}

自訂Quantum伺服器

延遲Quantum伺服器回調,以透過從一個受信任的後端擷取的資料來覆寫RuntimePlayer

IPluginHost.HttpRequest(request, info)可透過info = null來使用。

請求必須是**Async = true**。

C#

public override bool OnDeterministicPlayerDataSet(DeterministicPluginClient client, SetPlayerData playerData) {
  // Use client.ClientId as unique client id (UserId)
  var request = new HttpRequest() { Url = "http://microsoft.com", Async = true, Callback = OnDeterministicPlayerDataSetContinue, UserState = playerData.Index };
  ((DeterministicPlugin)PluginHost).PluginHost.HttpRequest(request, null);

  // Return false to not conntinue with SetPlayerData request
  return false;
}

private void OnDeterministicPlayerDataSetContinue(IHttpResponse response, object userState) {
  // Reponse sends player data in json for example: deserialize json into RuntimePlayer
  var runtimePlayer = new RuntimePlayer();
  var playerData = RuntimePlayer.ToByteArray(runtimePlayer);
  var playerIndex = (int)userState;
  var actorIdBytes = BitConverter.GetBytes(GetClientActorId(playerIndex));

  // Continue player data request
  Session?.Input.SetPlayerData(playerIndex, ByteUtils.MergeByteBlocks(playerData, actorIdBytes), false);
}

儲存重播/輸入歷史程式碼片段

輸入歷史是一個DeterministicTickInputSet物件的陣列(刷新數目),其進而針對各個玩家儲存輸入:

C#

public struct DeterministicTickInputSet {
  public int Tick;
  public DeterministicTickInput[] Inputs;
}

OnDeterministicInputConfirmed()回調時,在InputProvider類別中儲存輸入。這是當針對一個玩家的輸入已經被確認。或是建立一個自訂的相似的資料架構。

C#

public override void OnDeterministicSessionConfig(DeterministicPluginClient client, SessionConfig configData)
{
  _config = configData.Config;
}

C#

public override void OnDeterministicStartSession() {
  _inputProvider = new InputProvider(_config);
}

C#

public override void OnDeterministicInputConfirmed(DeterministicPluginClient client, int tick, int playerIndex, DeterministicTickInput input) {
  _inputProvider.InjectInput(input, true);
}

使用ReplayFile資料架構,以透過所需要的組態檔案來建立一個完整的重播。序列化程式是一個QuantumJsonSerializer,其輸出JSON。

C#

private void SaveReplayToFile(int verifiedFrame) {
  var replayFile = new ReplayFile {
    DeterministicConfig = container.DeterministicConfig,
    RuntimeConfig = container.RuntimeConfig,
    InputHistory = _inputProvider.ExportToList(verifiedFrame),
    Length = verifiedFrame
  };


  var filepath = Path.Combine(PluginLocation, "replay.json");
  File.WriteAllBytes(filepath, _serializer.SerializeReplay(replayFile));
}

儲存ReplayFile,而不需要知道最高的已驗證刷新。

C#

private void SaveReplayToFile() {
  // This will not cut out incomplete input in the end, but we should be able to live with it
  var inputSets = _inputProvider.ExportToList(int.MaxValue);

  // Find out what the highest verified tick that has a complete input set (for all players)
  int maxVerifiedTick = 0;
  for (int i = inputSets.Length - 1; i >= 0; i--) {
    if (inputSets[i].IsComplete()) {
      maxVerifiedTick = inputSets[i].Tick;
      break;
    }
  }

  var replayFile = new ReplayFile {
    DeterministicConfig = container.DeterministicConfig,
    RuntimeConfig = container.RuntimeConfig,
    InputHistory = inputSets,
    Length = maxVerifiedTick
  };


  var filepath = Path.Combine(PluginLocation, "replay.json");
  File.WriteAllBytes(filepath, _serializer.SerializeReplay(replayFile));
}

伺服器命令程式碼片段

一個程式碼片段展示了如何攔截客戶端發送的命令,可能會拒絕它們並且從伺服器本身發送命令。

C#

private DeterministicCommandSerializer _cmdSerializer;

public override bool OnDeterministicCommand(DeterministicPluginClient client, Command cmd) {
  if (_cmdSerializer == null) {
    _cmdSerializer = new DeterministicCommandSerializer();
    _cmdSerializer.RegisterFactories(DeterministicCommandSetup.GetCommandFactories(runtimeConfig, null));

    _cmdSerializer.CommandSerializerStreamRead.Reading = true;
    _cmdSerializer.CommandSerializerStreamWrite.Writing = true;
  }

  var stream = _cmdSerializer.CommandSerializerStreamRead;

  stream.SetBuffer(cmd.Data);

  if (_cmdSerializer.ReadNext(stream, out var command)) {
    // handle DeterministicCommand

    // return false if a command should be rejected from the (or any) client
    if (command is TestCommand testCmd) {
      return false;
    }
  }

  return true;
}

public void SendDeterministicCommand(DeterministicCommand cmd) {
  if (_cmdSerializer == null) {
    _cmdSerializer = new DeterministicCommandSerializer();
    _cmdSerializer.RegisterFactories(DeterministicCommandSetup.GetCommandFactories(runtimeConfig, null));

    _cmdSerializer.CommandSerializerStreamRead.Reading = true;
    _cmdSerializer.CommandSerializerStreamWrite.Writing = true;
  }

  var stream = _cmdSerializer.CommandSerializerStreamWrite;

  stream.Reset(stream.Capacity);

  if (_cmdSerializer.PackNext(stream, cmd)) {

    SendDeterministicCommand(new Command {
      Index = 0,
      Data = stream.ToArray(),
    });

    // optional: pool byte arrays and use them instead of allocating with ToArray()
    // Buffer.BlockCopy(stream.Data, stream.Offset, pooledByteArray, 0, stream.BytesRequired);
  }
}

多數決投票

  • 客戶端上傳他們的遊戲結果到伺服器,伺服器等到一個多數決投票發出後,才計算一個結果以發布。
  • 使用一個自訂的c#類別,來代表結果。在這個實例中,伺服器外掛程式只在二進位資料上操作,其就檢查相同結果而言已經足夠。但是當資訊被轉傳到一個自訂後端(未被執行),如果後端期待其他資料格式,則伺服器外掛程式在發送程式碼之前需要取消序列化的程式碼(比如參照到game.dll)。

更改到自訂Quantum外掛程式類別

  • 在關閉房間之前強制進行一個評估
  • 覆寫OnRaiseEvent()以篩選出自訂訊息,總是取消訊息,這樣它不會被轉傳到其他客戶端。

C#

using Photon.Deterministic;
using Photon.Deterministic.Server.Interface;
using Photon.Hive.Plugin;

namespace Quantum {
  public class CustomQuantumPlugin : DeterministicPlugin {
    protected CustomQuantumServer _server;

    public CustomQuantumPlugin(IServer server) : base(server) {
      Assert.Check(server is CustomQuantumServer);
      _server = (CustomQuantumServer)server;
    }

    public override void OnCloseGame(ICloseGameCallInfo info) {
      EvaluateMajorityVote(true);
      _majorityVote?.Dispose();
      _majorityVote = null;
      _server.Dispose();
      base.OnCloseGame(info);
    }

    private MajorityVote _majorityVote = new MajorityVote(2);

    private void EvaluateMajorityVote(bool force) {
      if (_majorityVote != null) {
        if (force || _majorityVote.IsReady || _majorityVote.IsWaitingTimeOver) {
          if (_majorityVote.Evaluate(out var results)) {
            // Send data somewhere
            //results[0].Data
            Log.Warn($"Game result accepted with {results[0].Count}");
            _majorityVote.Dispose();
            _majorityVote = null;
          }
        }
      }
    }

    public override void OnRaiseEvent(IRaiseEventCallInfo info) {
      if (info.Request.EvCode == 41) {
        // Cancel the message right away, it should not be send to anyone else
        info.Cancel();

        var client = _server.GetClientForActor(info.ActorNr);
        if (client == null) {
          // Dismiss the message when the client has already left
          return;
        }

        if (info.Request.Data == null) {
          // Client send no data, disconnect
          _server.DisconnectClient(client, "Operation Failed");
          return;
        }

        if (_majorityVote == null) {
          // Vote is over
          return;
        }

        _majorityVote.AddVote(client.ClientId, (byte[])info.Request.Data);
        EvaluateMajorityVote(false);

        // Don't process message any further
        return;
      }

      base.OnRaiseEvent(info);
    }
  }
}

多數決投票類別

最小需求投票及等待時間設定的時機點,取決於實際的遊戲:有多少玩家?遊戲是否有團隊?等等。

可能有些事情比StructuralComparisons.StructuralEqualityComparer更快。雖然Linq提供好的集合工具,但不在伺服器程式碼上使用它。

C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace Quantum {
  /// <summary>
  /// Trying to not use Linq for performance.
  /// Only compares binary data, serialzed by the client.
  ///   The final result should be send to backend to deserialize or deserialize on custom plugin with correct references.
  /// </summary>
  public class MajorityVote : IDisposable {
    private bool _startWaitTimeSet;
    private DateTime _startWaitTime;
    private double _minWaitTimeSec;
    private double _maxWaitTime;
    private int _minVotesRequired;
    private List<ClientResult> _clientResults;
    private HashSet<string> _clientResultsHashset;
    private MD5CryptoServiceProvider _hashProvider;

    /// <summary>
    /// There is at least one result and at least MinVotesRequired.
    /// The MinWaitingTime has passed.
    /// </summary>
    public bool IsReady {
      get {
        return
          _clientResults.Count >= _minVotesRequired &&
          (DateTime.Now - _startWaitTime).TotalSeconds >= _minWaitTimeSec;
      }
    }

    /// <summary>
    /// The MaxWaitingTime has passed a result be tried to evaluate now.
    /// </summary>
    public bool IsWaitingTimeOver {
      get {
        return
          _maxWaitTime > 0 &&
          (DateTime.Now - _startWaitTime).TotalSeconds >= _maxWaitTime;
      }
    }

    /// <summary>
    /// Number of client votes, indentified by their ClientId.
    /// </summary>
    public int Count => _clientResults.Count;

    /// <summary>
    /// Create a voting machine.
    /// </summary>
    /// <param name="minVotesRequired">The minimal votes required to make IsReady return true</param>
    /// <param name="minWaitTimeSec">The min wait tim in seconds to make IsReady return true</param>
    /// <param name="maxWaitTime">The max wait time in seconds to make IsWaitingTimeOver return true, 0 = undefinately</param>
    public MajorityVote(int minVotesRequired, double minWaitTimeSec = 0, double maxWaitTime = 0) {
      _minVotesRequired = Math.Max(minVotesRequired, 1);
      _minWaitTimeSec = minWaitTimeSec;
      _maxWaitTime = maxWaitTime;
      _clientResults = new List<ClientResult>();
      _clientResultsHashset = new HashSet<string>();
      _hashProvider = new MD5CryptoServiceProvider();
    }

    /// <summary>
    /// Disposes the hash provider object and clears internal lists.
    /// </summary>
    public void Dispose() {
      _clientResults?.Clear();
      _clientResults = null;

      _clientResultsHashset?.Clear();
      _clientResultsHashset = null;

      _hashProvider?.Dispose();
      _hashProvider = null;
    }

    /// <summary>
    /// Add a vote for a ClientId. If a client already passed the vote subsequent times are ignored.
    /// </summary>
    /// <param name="clientId">The clients id</param>
    /// <param name="data">The result as byte[] array</param>
    public void AddVote(string clientId, byte[] data) {
      if (_clientResultsHashset.Contains(clientId) == false) {
        var hash = _hashProvider.ComputeHash(data);
        var result = new ClientResult { ClientId = clientId, Result = data, Hash = hash };
        _clientResults.Add(result);
        _clientResultsHashset.Add(clientId);
      }

      if (_startWaitTimeSet == false && Count >= 2) {
        // Only start the waitime when two votes have been cast. Two votes because one would be too easy to missuse.
        _startWaitTime = DateTime.Now;
        _startWaitTimeSet = true;
      }
    }

    /// <summary>
    /// Run majority vote for the votes that have been cast.
    /// </summary>
    /// <param name="finalResults">Summary of the results, sorted by count</param>
    /// <returns>True if there has been consensus.</returns>
    public bool Evaluate(out List<FinalResult> finalResults) {
      var map = new Dictionary<byte[], FinalResult>(ByteArrayComparer.Default);

      var majority = 0;
      if (_clientResults.Count % 2 == 0) {
        majority = _clientResults.Count / 2 + 1;
      }
      else {
        majority = (int)Math.Ceiling(_clientResults.Count / (double)2);
      }

      // Compare each result hash with eath other
      for (int i = 0; i < _clientResults.Count; i++) {
        var vote = default(FinalResult);
        if (map.TryGetValue(_clientResults[i].Hash, out vote) == false) {
          vote = new FinalResult { ClientIds = new List<string>(), Result = _clientResults[i].Result };
          map.Add(_clientResults[i].Hash, vote);
        }

        vote.ClientIds.Add(_clientResults[i].ClientId);
        vote.Count++;
      }

      // Sort
      finalResults = new List<FinalResult>();
      foreach (var v in map) {
        finalResults.Add(v.Value);
      }
      finalResults.Sort(FinalResult.CompareByCount);

      if (finalResults.Count > 0 && finalResults[0].Count >= majority) {
        return true;
      }

      return false;
    }

    public class FinalResult {
      public byte[] Result;
      public int Count;
      public List<string> ClientIds;

      public static int CompareByCount(FinalResult a, FinalResult b) {
        return a.Count.CompareTo(b.Count);
      }
    }

    private class ClientResult {
      public string ClientId;
      public byte[] Result;
      public byte[] Hash;
    }

    private class ByteArrayComparer : IEqualityComparer<byte[]> {
      private static ByteArrayComparer _default;

      public static ByteArrayComparer Default {
        get {
          if (_default == null) {
            _default = new ByteArrayComparer();
          }

          return _default;
        }
      }

      public bool Equals(byte[] a, byte[] b) {
        return StructuralComparisons.StructuralEqualityComparer.Equals(a, b);
      }

      public int GetHashCode(byte[] obj) {
        return StructuralComparisons.StructuralEqualityComparer.GetHashCode(obj);
      }
    }
  }
}

Unity測試程式碼

  • 使用OpRaiseEvent()發送到外掛程式
  • 使用ByteArraySlice以發送二進位資料,以減少進一步的配置
  • 結果應該從一個已驗證幀來取得,並且它應該在各個(未經篡改的)客戶端上是相同的

C#

using ExitGames.Client.Photon;
using Photon.Realtime;
using Quantum.Demo;
using UnityEngine;

public class SendGameResult : MonoBehaviour {
  private readonly ByteArraySlice _sendSlice = new ByteArraySlice();

  private class GameResult {
    public struct Place {
      public string PlayerId;
      public int Points;
    }
    public Place[] Ranking;

    public void Serialize(Photon.Deterministic.BitStream stream) {
      foreach (var r in Ranking) {
        stream.WriteString(r.PlayerId);
        stream.WriteInt(r.Points);
      }
    }
  }

  private void Update() {
    // Use ByteSliceArray for optimization (non-alloc)
    // Gather results from a verified frame only (otherwise prediciton can differ)
    if (Input.GetKeyDown(KeyCode.Space)) {
      var gameResult = new GameResult { Ranking = new GameResult.Place[] {
        new GameResult.Place { PlayerId = "a", Points = 1 },
        new GameResult.Place { PlayerId = "b", Points = 2 } }
      };

      var stream = new Photon.Deterministic.BitStream(new byte[100 * 1024]);
      gameResult.Serialize(stream);

      _sendSlice.Buffer = stream.Data;
      _sendSlice.Count = stream.BytesRequired;
      _sendSlice.Offset = 0;

      UIMain.Client.OpRaiseEvent(41, _sendSlice, RaiseEventOptions.Default, SendOptions.SendReliable);
    }
  }
}
Back to top