This document is about: QUANTUM 1
SWITCH TO

でシンクのデバッグ

デシンクはデシンクロナイゼーションの略です。
デシンクは、2人のプレイヤーが与えられたフレームの入力をシミュレーションした後に異なる状態になるときに発生します。

確認すべきこと:

  • システムシミュレーション中に floatdoubleFP.FromFloat_UNSAFEFP.FromString_UNSAFE を使用しないでください。
  • Frame の外にある状態を使用しないでください。ただし、その状態が1つの Frame を超えない場合は除きます。

これらの要素がコードに使用されていない場合でもデシンクが発生する場合、次のいずれかの可能性があります。

  • 自分のコードにバグがある。
  • Quantumコードでバグに遭遇した。

どちらのケースであるかを見つけるために、シミュレーションの各部分に対してチェックサムを計算することができます。
もしステップNが同期していて、ステップN+1でデシンクが発生した場合(異なるプレイヤー間で Frame チェックサムが異なる、つまり Frame に異なるデータがあったことを意味します)、問題はステップNとN+1の間に存在することが考えられます。

これをデバッグするには、以下のコードを使用してください。

quantum.state: ChecksumStep.qtn

C#

  // This gives a name to each checksum step. Feel free to replace it with your own values.
  enum ChecksumStep {
    None,
    ChecksumCheckSystem,
    PrePhysicsSystem,
    PhysicsSystemPre,
    CharacterInitSystem,
    MovementSystem,
    LootBoxSystemUpdate,
    LootBoxSystemCallbackPre,
    LootBoxSystemCallbackPost,
    ChecksumSystem
  }

quantum.state: checksums.qtn

  import ChecksumStep;

  struct PartialChecksum {
    ChecksumStep step;
    UInt64 checksum;
  }

  global {
    // increase 100 if you have more than 100 potential checksum steps.
    array<PartialChecksum>[100] stepChecksums;
  }

quantum.systems: ChecksumSystem.cs

C#

  using System.Collections.Generic;
  using System.Linq;

  namespace Quantum.Game {
    public class ChecksumSystem : SystemBase {
      public static readonly List<PartialChecksum> checksums = new List<PartialChecksum>();

      public override void Update(Frame f) => f.recordChecksum(ChecksumStep.ChecksumSystem);

      public static string status => string.Join("\n", checksums.Select(e => e.ToString()).ToArray());
    }

    public static unsafe class ChecksumSystemExts {
      public static void recordChecksum(this Frame f, ChecksumStep step) {
        var cs = new PartialChecksum { step = step, checksum = f.CalculateChecksum() };
        if (ChecksumSystem.checksums.Count >= f.Global->stepChecksumsSize) {
          Log.Error("Out of checksums!");
        }
        else {
          *f.Global->stepChecksums(ChecksumSystem.checksums.Count) = cs;
        }

        ChecksumSystem.checksums.Add(cs);
      }
    }
  }

quantum.systems: ChecksumCheckSystem.cs

C#

  namespace Quantum.Game {
    public unsafe class ChecksumCheckSystem : SystemBase {
      public override void Update(Frame f) {
        for (var idx = 0; idx < f.Global->stepChecksumsSize; idx++) {
          *f.Global->stepChecksums(idx) = default;
        }
        ChecksumSystem.checksums.Clear();
        f.recordChecksum(ChecksumStep.ChecksumCheckSystem);
      }
    }
  }

quantum.systems: SystemSetup.cs

C#



  namespace Quantum {
    public static class SystemSetup {
      public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) =>
        new SystemBase[] {
          new Game.ChecksumCheckSystem(),

          // other systems go here

          new Game.ChecksumSystem()
        };
    }
  }

unity: any class extending QuantumCallbacks

フレームの状態をダンプする必要があります。そうすることで、クライアント間で比較できるようになります。ここに私たちのゲームの例コードを示していますが、これは私たちのライブラリに依存するため、あなた自身で書く必要があります。

C#

    string dumpFullFrame(Frame f) =>
      $"Frame[number: {f.Number} is predicted: {f.IsPredicted}, is verified: {f.IsVerified}, dump: {f.DumpFrame()}]";

    // random id for a client to discern between two clients running on same machine
    readonly uint randomId = Rng.now.nextUInt(out _);
    uint desyncNo;
    public override void OnChecksumError(DeterministicTickChecksumError error, Frame[] frames) {
      var checksumsS =
        error.Checksums
          .Select((cs, idx) => $"idx: {idx}, player: {cs.Player}, checksum: {cs.Checksum}")
          .mkString("\n");

      var msg =
        $"Desync at tick {error.Tick}. Checksums:\n{checksumsS}\n\n" +
        $"Local checksums:\n{ChecksumSystem.status}\n\n";
      if (frames.find(f => f.Number == error.Tick).valueOut(out var badFrame)) {
        msg += $"Frame {error.Tick}: {dumpFullFrame(badFrame)}";
      }
      else {
        for (var idx = 0; idx < frames.Length; idx++) {
          var frame = frames[idx];
          msg += $"Frame {idx}: {dumpFullFrame(frame)}\n";
        }
      }
      if (Environment.GetEnvironmentVariable("LOCALAPPDATA").opt().valueOut(out var home)) {
        var path = PathStr.a(home) / $"desync_id{randomId}_no{desyncNo}_frame{error.Tick}.txt";
        File.WriteAllText(path, msg);
        Log.d.error($"Wrote desync log to {path}");
      }
      else {
        Log.d.error(msg);
      }
      desyncNo++;
    }

Regular C# version:

C#


    public void Start()
    {

        if ( randomId == 0U )
        {
            randomId = (uint) UnityEngine.Random.Range( int.MinValue, int.MaxValue );
        }        
    }

    string DumpFullFrame( Frame f ) => $"Frame[number: {f.Number} is predicted: {f.IsPredicted}, is verified: {f.IsVerified}, dump: {f.DumpFrame()}]";

    // random id for a client to discern between two clients running on same machine
    static uint randomId;
    uint desyncNo;

    public override void OnChecksumError( DeterministicTickChecksumError error, Frame[] frames )
    {
        var checksumsS =
    error.Checksums
      .Select((cs, idx) => $"idx: {idx}, player: {cs.Client}, checksum: {cs.Checksum}")
      .mkString("\n");

        var msg =
            $"Desync at tick {error.Tick}. Checksums:\n{checksumsS}\n\n" +
            $"Local checksums:\n{Quantum.Game.ChecksumSystem.status}\n\n";

        bool foundBadFrame = false;
        Frame badFrame = null;

        for ( int idx = 0; idx < frames.Length; idx++ )
        {
            if ( frames[idx].Number == error.Tick )
                badFrame = frames[idx];
        }

        if ( badFrame != null )
        {
            msg += $"Frame {error.Tick}: {DumpFullFrame( badFrame )}";
        }
        else {
            for (var idx = 0; idx<frames.Length; idx++) {
                var frame = frames[idx];
                msg += $"Frame {idx}: {DumpFullFrame( frame)}\n";
            }
        }

        var envPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
        if (envPath == null )
        {
            envPath = ".";
        }

        if ( envPath != null )
        {
            var path = Path.Combine( Path.GetDirectoryName(envPath) , $"desync_id{randomId}_no{desyncNo}_frame{error.Tick}.txt");
            File.WriteAllText(path, msg);
            Log.Error($"Wrote desync log to {path}");
        }
        else {
            Log.Error(msg);
        }
            desyncNo++;
    }

使用法

ステップチェックサムを以下のように記録します: frame.recordChecksum(ChecksumStep.ChecksumCheckSystem);
チェックサムステップが尽きない限り、これを好きなだけ実行できます。
もし尽きる場合は、Quantum定義でその数を増やしてください。

デシンクが発生した際には、同じフレームの2つのクライアントからログファイルを取得し、それらをkdiff3などのツールで比較して、どのステップでデシンクが発生したかを確認します。

例えば:

Quantum Desync Example

Quantum Desync Example

PhysicsSystemPre ステップの後にチェックサムがデシンクしたことがわかります。これは、私たちの物理シグナルのいずれかに問題があるか、Quantum物理シミュレーション自体に問題があることを示しています。

Back to top