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