This document is about: QUANTUM 2
SWITCH TO

고정 소수점

소개

Quantum에서 FP 구조체는 교차 플랫폼 결정론을 보장하기 위해 floatsdoubles 대신에 사용합니다. FPVector2, FPVector3, FPMatrix, FPQuaternion, RNGSession, FPBounds2 등과 같은 일반적인 산술 데이터 구조체 버전을 제공합니다. 물리와 항법을 포함한 Quantum의 모든 시스템은 계산에서 FP 값을 독점적으로 사용합니다.

Quantum에 구현된 고정 소수점 유형은 Q48.16입니다. 정밀도와 성능 사이의 균형이 양호하다는 것이 입증되었습니다.

내부적으로 FP는 결합된 고정 소수점 번호(정수 + 소수 부분)를 나타내기 위해 1개의 long을 사용합니다. long 값은 FP.RawValue를 통해 접근하고 설정할 수 있습니다.

Quantum의 FP Math는 빠른 삼각함수와 제곱근 함수에 대해 주의 깊게 조정된 조회 테이블을 사용합니다( QuantumSDK\quantum_unity\Assets\Photon\Quantum\Resources\LUT 참조).

FP 구문 분석

표현 가능한 FP 분수는 제한적이며 결코 double만큼 정확하지 않습니다. 구문 분석은 근사치이며 가능한 가장 가까운 FP 정밀도로 반올림됩니다. 이는 다음 항목에 반영됩니다.

  • FPfloat 1.1f로 구문 분석한 다음 다시 float로 변환하면 1.09999999f가 발생할 수 있습니다.

  • 다른 구문 분석 방법을 사용하면 동일한 시스템에서 서로 다른 결과를 얻을 수 있습니다.

    C#

    FP.FromFloat(1.1f).RawValue != FP.FromString("1.1").RawValue
    

TLDR

  • 편집시에서만 float를 사용하고, 시뮬레이션 또는 런타임에는 절대 내부에서 사용하지 않습니다.
  • 가능하면 원시 자료에서 변환하는 것이 가장 좋습니다.

FP.FromFloat_UNSAFE()

float에서 변환하는 것은 반올림 오류로 인해 결정적이지 않으며 시뮬레이션 내에서 수행하면 절대로 안 됩니다. 시뮬레이션에서 이러한 변환을 수행하면 100% 디씽크가 발생합니다.

그러나 변환된(FP) 데이터가 처음 생성된 후 모든 사용자와 공유되는 편집 또는 빌드 타임 동안 사용할 수 있습니다. 중요: 이러한 방식으로 다른 시스템에서 생성된 데이터가 호환되지 않을 수 있습니다.

C#

var v = FP.FromFloat_UNSAFE(1.1f);

FP.FromString_UNSAFE()

이렇게 하면 내부적으로 string을 float으로 구문 분석한 다음 FP로 변환됩니다. FromFloat_UNSAFE()의 모든 경고 사항들은 여기에도 적용됩니다.

C#

var v = FP.FromFloat_UNSAFE("1.1");

FP.FromString()

이는 결정론적이므로 어디서나 사용할 수 있지만 가장 성능이 좋은 옵션은 아닙니다. 일반적인 사용 사례는 클라이언트가 서버에서 로드한 다음 Quantum 에셋의 데이터를 업데이트하는 데 사용하는 정보(패치)의 균형을 맞추는 것입니다.

문자열 로케일에 주의하십시오! 소수점에 대한 영문 숫자 형식만 구문 분석하며 점(예: 1000.01f)이 필요합니다.

C#

var v = FP.FromFloat("1.1");

FP.FromRaw()

이것은 내부 표현을 모방하기 때문에 안전하고 빠릅니다.

C#

var v = FP.FromRaw(72089);

이 코드는 편리한 변환을 위해 유니티에서 FP 컨버터 윈도우를 만드는 데 사용할 수 있습니다.

C#

using System;
using UnityEditor;
using Photon.Deterministic;

public class FPConverter : EditorWindow {
  private float _f;
  private FP _fp;

  [MenuItem("Quantum/FP Converter")]
  public static void ShowWindow() {
    GetWindow(typeof(FPConverter), false, "FP Converter");
  }

  public virtual void OnGUI() {
    _f = EditorGUILayout.FloatField("Input", _f);
    try {
      _fp = FP.FromFloat_UNSAFE(_f);
      var f = FPPropertyDrawer.GetRawAsFloat(_fp.RawValue);
      var rect = EditorGUILayout.GetControlRect(true);
      EditorGUI.FloatField(rect, "Output FP", f);
      QuantumEditorGUI.Overlay(rect, "(FP)");
      EditorGUILayout.LongField("Output Raw", _fp.RawValue);
    }
    catch (OverflowException e) {
      EditorGUILayout.LabelField("Out of range");
    }
  }
}

상수 변수

`FP._1_10` 구문을 확장하거나 생성할 수 없습니다.

FP는 구조체이므로 상수로 사용할 수 없습니다. 그러나 "FP" 값을 하드 코딩하여 const 변수에서 사용할 수 있습니다:

  1. 사전 정의된 FP._1 static getters 또는 FP.Raw._1 const 상수와 결합.

C#

FP foo = FP._1 + FP._0_10;
// or
foo.RawValue = FP.Raw._1 + FP.Raw._0_10;

C#

const long MagicNumber = FP.Raw._1 + FP.Raw._0_10;

FP foo = default;
foo.RawValue = MagicNumber;
// or
foo = FP.FromRaw(MagicNumber);
  1. 특정 float를 FP로 한 번 변환하고 원시 값을 상수로 저장합니다.

C#

const long MagicNumber = 72089; // 1.1

var foo = FP.FromRaw(MagicNumber);
// or
foo.RawValue = MagicNumber;
  1. Quantum DSL 내에서 상수를 생성합니다

Unknown

#define FPConst 1.1

그리고 다음과 같이 사용합니다:

C#

var foo = default(FP);
foo += Constants.FPConst;
// or
foo.RawValue += Constants.Raw.FPConst;

다음과 같은 방법으로 상수를 나타내는 코드를 생성합니다:

C#

public static unsafe partial class Constants {
  public const Int32 PLAYER_COUNT = 8;
  /// <summary>1.100006</summary>
  
  public static FP FPConst {
    [MethodImpl(MethodImplOptions.AggressiveInlining)] get {
      FP result;
      result.RawValue = 72090;
      return result;
    }
  }
  public static unsafe partial class Raw {
    /// <summary>1.100006</summary>
    public const Int64 FPConst = 72090;
  }
}
  1. 클래스 내에서 readonly static 변수를 정의합니다.

C#

private readonly static FP MagicNumber = FP._1 +  FP._0_10;

실행 시 값을 임의로 변경하면 디씽크가 발생할 수 있으므로 const 변수와 비교하여 성능 저하가 있으며 readonly로 표시하는 것을 잊지 마십시오.

캐스팅

int, uint, short, ushort, byte, sbyte에서 명시적으로 FP 캐스팅하는 것은 허용되며 안전합니다.

C#

FP v = (FP)1;
FP v = 1;

FP에서 float 또는 double로 명시적인 캐스팅은 가능하지만 시뮬레이션에서는 분명히 사용해서는 안 됩니다.

C#

var v = (double)FP._1;
var v = (float)FP._1;

정수로 캐스팅하고 반대로 캐스팅하는 것은 안전합니다.

C#

FP v = (FP)(int)FP._1;

안전하지 않은 캐스팅은 [obsolete]로 표기되며 InvalidOperationException의 원인이 됩니다.

C#

FP v = 1.1f;  // ERROR
FP v = 1.1d;  // ERROR

인라인

모든 저수준 Quantum 시스템은 수동 인라인 FP 산술을 사용하여 가능한 모든 성능을 추출합니다. 고정점 연산에서는 정수 나눗셈과 곱셈을 사용합니다. 이를 위해 결과 또는 나누기는 계산 전후에 FP-정밀(16)에 의해 비트 시프트 됩니다.

C#

var v = parameter * FP._0_01;

// inlined integer math
FP v = default;
v.RawValue = (parameter.RawValue * FP._0_01.RawValue) >> FPLut.PRECISION;

C#

var v = parameter / FP._0_01;

// inlined integer math
FP v = default;
v.RawValue = (parameter.RawValue << FPLut.PRECISION) / FP._0_01.RawValue;

개요

FP.UseableMax는 자신과 곱할 수 있고 오버플로를 일으키지 않는 가장 큰 FP 수를 나타냅니다(long 범위 초과).

Unknown

FP.UseableMax
    Decimal: 32767.9999847412
    Raw: 2147483647
    Binary: 1111111111111111111111111111111 = 31 bit

Unknown

FP.UseableMin
    Decimal: -32768
    Raw: -2147483648
    Binary: 10000000000000000000000000000000 = 32 bit

정밀도

일반적인 FP 정밀도는 수치가 특정 범위(0.01..1000) 내에 있을 때 적절합니다. FP-math 관련 정밀도 문제는 일반적으로 부정확한 결과를 생성하며 (수학 기반) 시스템을 불안정하게 만드는 경향이 있습니다. 매우 일반적인 경우는 매우 높거나 작은 수를 곱하고 예를 들어 나누기를 통해 원래 범위로 돌아가는 경우입니다. 결과 숫자는 정밀도를 잃게 됩니다.

또 다른 예는 ClosestDistanceToTriangle에서 발췌한 이 메소드입니다. 여기서 t0는 두 개의 점 곱을 서로 곱하여 계산되며, 여기서 점 곱도 곱의 결과입니다. 이것은 매우 정확한 결과가 예상될 때 문제입니다. 이 문제를 완화하는 방법은 계산 전에 값을 인위적으로 이동한 다음 결과를 뒤로 옮기는 것입니다. 이것은 입력의 범위를 어느 정도 알고 있을 때 작동합니다.

C#

  var diff = p - v0;
  var edge0 = v1 - v0;
  var edge1 = v2 - v0;
  var a00 = Dot(edge0, edge0);
  var a01 = Dot(edge0, edge1);
  var a11 = Dot(edge1, edge1);
  var b0 = -Dot(diff, edge0);
  var b1 = -Dot(diff, edge1);
  var t0 = a01 * b1 - a11 * b0;
  var t1 = a01 * b0 - a00 * b1;
  // ...
  closestPoint = v0 + t0 * edge0 + t1 * edge1;

FPAnimationCurves

유니티에는 일부 값을 곡선의 형태로 표현하는 데 유용한 도구 세트가 있습니다. 이러한 곡선을 위한 사용자 정의 편집기가 함께 제공되며, 이 편집기는 직렬화된 다음 특정 지점에서 곡선의 값을 평가하기 위해 런타임에 사용할 수 있습니다.

차량 구현 시 조향 정보 표현, AI 에이전트의 의사 결정 시 유틸리티 값(Bot SDK의 유틸리티 이론에서처럼), 일부 공격의 피해에 대한 승수 값 획득 등 곡선을 사용할 수 있는 상황이 많습니다.

Quantum SDK는 이미 FP로 평가되는 FPAnimationCurve이라는 이름의 자체 애니메이션 곡선을 구현하고 있습니다. 이 사용자 지정 유형은 유니티의 데이터 에셋 및 컴포넌트에서 직접 검사될 때 유니티의 기본 애니메이션 커브 편집기를 사용하여 그려지며, 이 편집기의 데이터는 내부적으로 결정론적 유형으로 베이킹 됩니다.

FPAnimationCurve에서 데이터 폴링

Quantum 코드를 사용하여 곡선의 일부 데이터를 폴링 하는 데 필요한 코드는 유니티의 버전과 매우 유사합니다:

C#

// This returns the pre-baked value, interpolated accordingly to the curve's configuration such as it's key points, curve's resolution, tangets modes, etc
FP myValue = myCurve.Evaluate(FP._0_50);

시뮬레이션에서 직접 FPAnimationCurves 생성하기

다음은 시뮬레이션에서 직접 결정론적 애니메이션 커브를 처음부터 생성하는 코드입니다.

C#

// Creating a simple, linear curve with five key points
// Change the parameter as prefered
public static class FPAnimationCurveUtils
{
    public static FPAnimationCurve CreateLinearCurve(FPAnimationCurve.WrapMode preWrapMode, FPAnimationCurve.WrapMode postWrapMode)
    {
        return new FPAnimationCurve
        {
            Samples = new FP[5] { FP._0, FP._0_25, FP._0_50, FP._0_75, FP._1 },
            PostWrapMode = (int)postWrapMode,
            PreWrapMode = (int)preWrapMode,
            StartTime = 0,
            EndTime = 1,
            Resolution = 32
        };
    }
}

C#

// Storing a curve into a local variable
var curve = FPAnimationCurveUtils.CreateLinearCurve(FPAnimationCurve.WrapMode.Clamp, FPAnimationCurve.WrapMode.Clamp);

// It can also be used directly to pre-initialise a curve in an asset
public unsafe partial class CollectibleData
{
    public FPAnimationCurve myCurve = FPAnimationCurveUtils.CreateLinearCurve(FPAnimationCurve.WrapMode.Clamp, FPAnimationCurve.WrapMode.Clamp);

유니티의 AnimationCurve를 FPAnimationCurve로 베이킹하기

일반적인 유니티 AnimationCurveFPAnimationCurve로 변환하는데 필요한 코드는 여기에서 찾을 수 있습니다.

Back to top