でシンクのデバッグ
デシンクはデシンクロナイゼーションの略です。
デシンクは、2人のプレイヤーが与えられたフレームの入力をシミュレーションした後に異なる状態になるときに発生します。
確認すべきこと:
- システムシミュレーション中に
float
、double
、FP.FromFloat_UNSAFE
、FP.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などのツールで比較して、どのステップでデシンクが発生したかを確認します。
例えば:
PhysicsSystemPre
ステップの後にチェックサムがデシンクしたことがわかります。これは、私たちの物理シグナルのいずれかに問題があるか、Quantum物理シミュレーション自体に問題があることを示しています。