Last active
December 20, 2022 15:14
-
-
Save arturaz/dbdf3c9fe232b0c315c153e37ed50786 to your computer and use it in GitHub Desktop.
A version of the Photon Quantum DebugDraw commands that last for a duration rather than a single frame only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using FPCSharpUnity.core.dispose; | |
using Quantum.Game.extensions; | |
namespace Quantum; | |
/// <summary> | |
/// <para>Read-only interface for <see cref="CallbackDispatcher"/>.</para> | |
/// | |
/// <para> | |
/// All of the callbacks are listed on | |
/// https://doc.photonengine.com/en-us/quantum/current/manual/quantum-ecs/game-events#callbacks. | |
/// </para> | |
/// | |
/// <para>We have copied them here for convenience.</para> | |
/// | |
/// <list type="table"> | |
/// <listheader> | |
/// <term>Callback</term> | |
/// <description>Description</description> | |
/// </listheader> | |
/// <item> | |
/// <term><see cref="CallbackPollInput"/></term> | |
/// <description>Is called when the simulation queries local input.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackInputConfirmed"/></term> | |
/// <description>Is called when a checksum has been computed.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackGameStarted"/></term> | |
/// <description>Is called when the game has been started.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackGameResynced"/></term> | |
/// <description>Is called when the game has been re-synchronized from a snapshot.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackGameDestroyed"/></term> | |
/// <description>Is called when the game was destroyed.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackUpdateView"/></term> | |
/// <description>Is guaranteed to be called every rendered frame.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackSimulateFinished"/></term> | |
/// <description>Is called when frame simulation has completed.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackEventCanceled"/></term> | |
/// <description> | |
/// Is called when an event raised in a predicted frame was canceled in a verified frame due to a | |
/// roll-back / missed prediction. Synchronised events are only raised on verified frames and thus will | |
/// never be canceled; this is useful to graciously discard non-synchronized events in the view. | |
/// </description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackEventConfirmed"/></term> | |
/// <description>Is called when an event was confirmed by a verified frame.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackChecksumError"/></term> | |
/// <description>Is called on a checksum error.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackChecksumErrorFrameDump"/></term> | |
/// <description>Is called when due to a checksum error a frame is dumped.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CallbackChecksumComputed"/></term> | |
/// <description>Is called when local input was confirmed.</description> | |
/// </item> | |
/// </list> | |
/// </summary> | |
public interface CallbacksSubscribable { | |
/// <inheritdoc cref="DispatcherBaseExts.subscribeCallback{Evt}"/> | |
IDisposable subscribeCallback<Evt>( | |
ITracker tracker, DispatchableHandler<Evt> onEvent | |
) where Evt : CallbackBase; | |
} | |
/// <summary>Wraps <see cref="CallbackDispatcher"/> and presents it as <see cref="CallbacksSubscribable"/>.</summary> | |
public sealed class CallbacksSubscribableWrap : CallbacksSubscribable { | |
readonly CallbackDispatcher dispatcher; | |
public CallbacksSubscribableWrap(CallbackDispatcher dispatcher) => this.dispatcher = dispatcher; | |
public IDisposable subscribeCallback<Evt>( | |
ITracker tracker, DispatchableHandler<Evt> onEvent | |
) where Evt : CallbackBase => dispatcher.subscribeCallback(tracker, onEvent); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Runtime.CompilerServices; | |
using JetBrains.Annotations; | |
using Photon.Deterministic; | |
using FPCSharpUnity.core.log; | |
using GenerationAttributes; | |
using Quantum.Game.utils; | |
using Sirenix.OdinInspector; | |
using UnityEngine; | |
namespace Quantum.Game.data; | |
[Serializable] | |
public partial class DebuggingSettings { | |
[Serializable] | |
public partial class Draw { | |
public UColor color = ColorRGBA.White; | |
public bool enabled; | |
[InfoBox("Duration in seconds for how long the drawn thing should be visible.")] | |
public float duration; | |
[InfoBox("Multiplier of a drawn thing, so you can scale it up or down.")] | |
public FP multiplier = FP._1; | |
[ | |
InfoBox("Width of the drawn lines."), | |
PublicAccessor, SerializeField | |
] FP _lineWidth = FP._1; | |
[ | |
InfoBox("Draws a label at the end of the line. Label writes line length."), | |
PublicAccessor, SerializeField | |
] bool _drawLabel = false; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static implicit operator bool(Draw d) { | |
#if DEBUG | |
return d.enabled; | |
#else | |
return false; | |
#endif | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static implicit operator ColorRGBA(Draw d) => d.color; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static implicit operator float(Draw d) => d.duration; | |
[Conditional("DEBUG")] | |
public void circleDiameter(Frame f, FPVector2 position, FP diameter) { | |
if (enabled) f.draw.Circle(position, diameter * multiplier * FP._0_50, this); | |
} | |
[Conditional("DEBUG")] | |
public void circleRadius(Frame f, FPVector2 position, FP radius) { | |
if (enabled) f.draw.Circle(position, radius * multiplier, this); | |
} | |
[Conditional("DEBUG")] | |
public void arrow(Frame f, FPVector2 position, FPVector2 direction) { | |
if (enabled) f.draw.Arrow(position, direction * multiplier, this); | |
} | |
[Conditional("DEBUG")] | |
public void arrow2(Frame f, FPVector2 position, FPVector2 target) { | |
if (enabled) f.draw.Arrow(position, (target - position) * multiplier, this); | |
} | |
[Conditional("DEBUG")] | |
public void line(Frame f, FPVector2 start, FPVector2 end) { | |
if (enabled) f.draw.Line(start, end, this); | |
} | |
[Conditional("DEBUG")] | |
public void lineBox(Frame f, FPVector2 start, FPVector2 end, FP width) { | |
if (enabled) f.draw.LineBox(start, end, width * multiplier, this); | |
} | |
[Conditional("DEBUG")] | |
public void rectangleWithEdges(Frame f, FPVector2 position, FPVector2 extents, FP rotation) { | |
if (enabled) f.draw.RectangleEdges(position, extents, rotation, this, duration); | |
} | |
[Conditional("DEBUG")] | |
public void rectangleWithEdges(Frame f, FPVector2 position, FP size, FP rotation) => | |
rectangleWithEdges(f, position, new FPVector2(size, size), rotation); | |
[Conditional("DEBUG")] | |
public void shape(Frame f, FPVector2 position, FP rotation, Shape2D shape) { | |
if (!enabled) return; | |
switch (shape.Type) { | |
case Shape2DType.Circle: | |
circleDiameter(f, position, shape.Circle.Radius); | |
break; | |
case Shape2DType.Box: | |
rectangleWithEdges(f, position, shape.Box.Extents, rotation); | |
break; | |
case Shape2DType.None: | |
break; | |
case Shape2DType.Polygon: | |
f.log.mWarn($"Don't know how to debug draw {shape.Type} yet!"); | |
break; | |
} | |
} | |
} | |
[Serializable] | |
public class Log { | |
public bool shouldLog; | |
[Conditional("DEBUG")] | |
public void log(object value) { if (shouldLog) { QLog.i.mInfo(value.ToString()); } } | |
public static implicit operator bool(Log l) => l.shouldLog; | |
} | |
[Serializable] | |
public class Character { | |
[InfoBox( | |
"Draws character bounds rectangle. This includes character legs, " | |
+ "so you can check if leg physics work as intended." | |
)] | |
public Draw bounds = new() { color = ColorRGBA.Blue }; | |
public Draw | |
position = new() { color = ColorRGBA.Yellow }, | |
legBoxCast = new() { color = ColorRGBA.Blue }, | |
legRayCasts = new() { color = ColorRGBA.Blue }, | |
collisionSofteningDetection = new() { color = ColorRGBA.Cyan }, | |
collisionSofteningRaycast = new() { color = ColorRGBA.Yellow }, | |
velocity = new() { color = ColorRGBA.Cyan }, | |
movementForce = new() { color = ColorRGBA.Red }, | |
slidingForce = new() { color = ColorRGBA.Red }, | |
legFloatForce = new() { color = ColorRGBA.Magenta }, | |
collisionWithStatic = new() { color = ColorRGBA.Yellow }, | |
collisionWithAnchorableBody = new() { color = ColorRGBA.Yellow }, | |
gunRotationBase = new() { color = ColorRGBA.Red }, | |
gunRotation = new() { color = ColorRGBA.Red }, | |
gunOffset = new() { color = ColorRGBA.Red }, | |
barrelOffset = new() { color = ColorRGBA.Yellow }, | |
knockback = new() { color = ColorRGBA.Yellow }, | |
wallTouchZone = new() { color = ColorRGBA.Yellow }, | |
wallTouchNormal = new() { color = ColorRGBA.Yellow }, | |
wallJump = new() { color = ColorRGBA.Yellow }, | |
headLine = new() { color = ColorRGBA.Yellow }, | |
abilityRange = new() { color = ColorRGBA.Yellow }, | |
dashTarget = new() { color = ColorRGBA.Yellow }, | |
dashPath = new() { color = ColorRGBA.Yellow }, | |
dashDamagePath = new() { color = ColorRGBA.Yellow }; | |
[InfoBox("Draws when searching where to spawn a character while checking for nearby enemies.")] | |
public Draw spawnEnemyDistanceCheck = new() { color = ColorRGBA.Cyan }; | |
[InfoBox("Draws when searching where to spawn a character while checking for nearby characters.")] | |
public Draw spawnLonerDistanceCheck = new() { color = ColorRGBA.Cyan }; | |
[InfoBox("Draws the shield ability while it is active.")] | |
public Draw shieldAbility = new() { color = ColorRGBA.Cyan }; | |
[PublicAPI] public bool camera; | |
public Log waitStateMachineRanged, waitStateMachineMelee; | |
} | |
[Serializable] | |
public class Audio { | |
[PublicAPI] public Draw audio = new() { color = ColorRGBA.Magenta }; | |
} | |
[Serializable] | |
public class Ladder { | |
public bool cooldownState, snapToCenterState; | |
public Draw snapToCenterLine = new() { color = ColorRGBA.White }; | |
} | |
[Serializable] | |
public class OneWayPlatform { | |
public Draw | |
goingThrough = new() { color = ColorRGBA.Red }, | |
normal = new() { color = ColorRGBA.White }, | |
distanceToCenterPlane = new() { color = ColorRGBA.Red }, | |
ignoreCollision = new() { color = ColorRGBA.Red }; | |
public bool keepDroppingDown; | |
} | |
[Serializable] | |
public class MovingPlatforms { | |
public Draw | |
endPoint = new() { color = ColorRGBA.Magenta }, | |
attachedObjectsBoxcast = new() { color = ColorRGBA.Yellow }, | |
attachedObjectDelta = new() { color = ColorRGBA.Cyan }; | |
} | |
[Serializable] | |
public class MeleeWeapon { | |
public Draw | |
boxcast = new() {color = ColorRGBA.Red}, | |
impulse = new() {color = ColorRGBA.Red}; | |
} | |
[Serializable] | |
public class Projectiles { | |
public Draw | |
linecasts = new() {color = ColorRGBA.Cyan}, | |
contactNormal = new() { color = ColorRGBA.Cyan }, | |
reflectionInput = new() { color = ColorRGBA.Yellow }, | |
reflectionOutput = new() { color = ColorRGBA.Yellow }, | |
headshot = new() { color = ColorRGBA.Red }, | |
homing = new() { color = ColorRGBA.Red }, | |
size = new() { color = ColorRGBA.White }, | |
manageAttractionForceEffectForceRange = new() { color = ColorRGBA.Red }, | |
manageAttractionForceEffectDamageRange = new() { color = ColorRGBA.Red }, | |
chainCheck = new() { color = ColorRGBA.Red }, | |
chainDirection = new() { color = ColorRGBA.Green }, | |
activationRange = new() { color = ColorRGBA.Green }, | |
shieldCollectRange = new() { color = ColorRGBA.Red }, | |
impactForce = new() { color = ColorRGBA.White, multiplier = FP._0_01}; | |
} | |
[Serializable] | |
public class AnchoredBodies { | |
public Draw | |
revoluteJointInitialPosition = new() { color = new ColorRGBA(0, 174, 249) }, | |
revoluteJointVelocity = new() { color = ColorRGBA.Green }, | |
revoluteJointAnchorRotated = new() { color = new ColorRGBA(0, 174, 249) }, | |
legMovementForceVector = new() { color = ColorRGBA.Red }, | |
legCastVector = new() { color = ColorRGBA.Cyan }, | |
legFloatingForceVector = new() { color = ColorRGBA.Blue }; | |
public bool distanceJointInitialPosition, distanceJointLine; | |
} | |
[Serializable] | |
public class VelocityChangeDamage { | |
public bool logNegativeDamage; | |
public FP logForceThreshold, forceDivider = 1000; | |
public Draw | |
contactNormal, | |
bodyVelocity, bodyVelocityAlongNormal, bodyForceAlongNormal, | |
characterVelocity, characterVelocityAlongNormal, characterForceAlongNormal, | |
forceDiffAlongNormal, forceDiffAlongNormalAfterThreshold; | |
} | |
[Serializable] | |
public class Ledge { | |
public Draw | |
boxcast = new() { color = ColorRGBA.Red }, | |
force = new() { color = ColorRGBA.Red }, | |
anchor = new() { color = ColorRGBA.White }; | |
} | |
[Serializable] | |
public class LootBox { | |
public FPVector2 stateExtents = new(FP._0_50, FP._0_50); | |
public bool state; | |
} | |
[Serializable] | |
public class Crouch { | |
public Draw | |
boxcast = new() {color = ColorRGBA.Red}; | |
} | |
[Serializable] | |
public class AOE { | |
[Serializable] | |
public class LineOfSight { | |
[InfoBox("Target's approximated bounding circle that shows its displacement relative to AOEs circular area.")] | |
public Draw targetBoundingCircle = new() { | |
color = new ColorRGBA(0, 255, 255), duration = 3.0f | |
}; | |
[InfoBox("AoE's bounding circle.")] | |
public Draw aoeBoundingCircle = new() { | |
color = new ColorRGBA(255, 0, 255), duration = 3.0f | |
}; | |
[InfoBox("Ray that didn't hit the target.")] | |
public Draw missedRay = new() { | |
color = ColorRGBA.Lightgray, duration = 3.0f | |
}; | |
[InfoBox("Ray that hit the target.")] | |
public Draw hitRay = new() { | |
color = ColorRGBA.Red, duration = 3.0f | |
}; | |
}; | |
public Draw explosion = new() {color = ColorRGBA.Red}; | |
public LineOfSight lineOfSight; | |
} | |
[Serializable] | |
public class CharacterInitSystem { | |
public Log mapReload; | |
} | |
[Serializable] | |
public class DeathZone { | |
public Draw playableArea = new() { color = UColor.rgb(192, 204, 159, 100) }; | |
} | |
[Serializable] | |
public class Deployment { | |
public Draw | |
dropship = new() {color = ColorRGBA.Red}, | |
playerPod = new() {color = ColorRGBA.Blue}, | |
playerPodHorizontalChecker = new() {color = ColorRGBA.Cyan}; | |
} | |
[Serializable] | |
public class Launchpad { | |
public Draw onTopChecker = new() {color = ColorRGBA.Cyan}; | |
} | |
[Serializable] | |
public class Turrets { | |
public bool raycast; | |
public Draw | |
arm = new() {color = ColorRGBA.Red}; | |
public Log waitStateMachine; | |
} | |
[Serializable] | |
public class Avoiders { | |
public Draw | |
raycast = new() {color = ColorRGBA.Cyan}, | |
force = new() {color = ColorRGBA.Yellow}; | |
public FP forceDivisor = FP._1; | |
} | |
[Serializable] | |
public class Mobs { | |
public Draw | |
flyerDashDirection = new() {color = ColorRGBA.Red, duration = 1f}, | |
flyerTarget = new() {color = ColorRGBA.Red}, | |
flyerDirection = new() {color = ColorRGBA.Green}, | |
shooterDetection = new() {color = ColorRGBA.Yellow}, | |
shooterRaycast = new() {color = ColorRGBA.Yellow}, | |
shooterTarget = new() {color = ColorRGBA.Yellow}, | |
shooterBarrel = new() {color = ColorRGBA.Red}; | |
public Log waitStateMachine; | |
} | |
[Serializable] | |
public class Bots { | |
public Draw | |
pathDetachThreshold = new() {color = ColorRGBA.Green}, | |
targetNode = new() {color = ColorRGBA.Cyan}, | |
currentNode = new() {color = ColorRGBA.Red}, | |
movementTarget = new() {color = ColorRGBA.Cyan}, | |
lastShootingTarget = new() {color = ColorRGBA.Red}, | |
lastShootingTargetIfCanShoot = new() {color = ColorRGBA.Red}, | |
shootingTarget = new() {color = ColorRGBA.Red}, | |
preferredLocation = new() {color = ColorRGBA.Yellow}, | |
aimRange = new() {color = ColorRGBA.Yellow}; | |
[InfoBox( | |
"Marks all reachable waypoints. " + | |
"You should not use this when there is more than one bot in the game " + | |
"because information from all bots will overlap and will be useless to look at." | |
)] | |
public Draw reachableWaypoints = new() {color = ColorRGBA.Blue}; | |
public Log movementTargetLog; | |
} | |
public Character character; | |
[PublicAPI] public Audio audio; | |
public Ladder ladder; | |
public OneWayPlatform oneWayPlatform; | |
public MovingPlatforms movingPlatforms; | |
public MeleeWeapon meleeWeapon; | |
public Projectiles projectiles; | |
public AnchoredBodies anchoredBodies; | |
public VelocityChangeDamage velocityChangeDamage; | |
public Ledge ledge; | |
public LootBox lootBox; | |
public Crouch crouch; | |
public AOE aoe; | |
public CharacterInitSystem characterInitSystem; | |
public DeathZone deathZone; | |
public Deployment deployment; | |
public Launchpad launchpad; | |
public Turrets turrets; | |
public Avoiders avoiders; | |
public Mobs mobs; | |
public Bots bots; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Photon.Deterministic; | |
namespace Quantum.Utils; | |
public enum Side : byte { Left, OnLine, Right } | |
public static class FPMath2 { | |
/// <summary>Calculates on which side of a straight line is a given point located.</summary> | |
/// | |
/// https://math.stackexchange.com/a/274728 | |
public static Side onWhichSideOfLineIsPoint(FPVector2 linePoint1, FPVector2 linePoint2, FPVector2 point) { | |
var d = | |
(point.X - linePoint1.X) * (linePoint2.Y - linePoint1.Y) | |
- (point.Y - linePoint1.Y) * (linePoint2.X - linePoint1.X); | |
// If less than 0 then the point lies on one side of the line, and if 0 then it lies on the other side. | |
// If 0 then the point lies exactly line. | |
if (d < FP._0) return Side.Left; | |
if (d > FP._0) return Side.Right; | |
return Side.OnLine; | |
} | |
/// <summary>Checks if two segments intersect.</summary> | |
public static FPVector2? segmentIntersectsSegment(FPVector2 p, FPVector2 p2, FPVector2 q, FPVector2 q2) { | |
if (FPCollision.LineIntersectsLine(p, p2, q, q2, out var intersection, out _)) { | |
return intersection; | |
} | |
return null; | |
} | |
public static unsafe FPVector2 worldToLocalPos(Transform2D* t2d, FPVector2 point) => | |
FPVector2.Rotate(point - t2d->Position, -t2d->Rotation); | |
public static unsafe FPVector2 localToWorldlPos(Transform2D* t2d, FPVector2 point) => | |
FPVector2.Rotate(point, t2d->Rotation) + t2d->Position; | |
public static unsafe FPVector2 worldToLocalDirection(Transform2D* t2d, FPVector2 point) => | |
FPVector2.Rotate(point, -t2d->Rotation); | |
public static FPVector2 closestPointOnSegment(this FPVector2 point, FPVector2 segmentA, FPVector2 segmentB) { | |
var s = segmentB - segmentA; | |
var len = s.SqrMagnitude; | |
if (len == 0) return segmentA; | |
return FPVector2.Lerp(segmentA, segmentB, FPVector2.Dot(point - segmentA, s) / len); | |
} | |
/// <summary> | |
/// Gets point on the rectangle by angle from the center. | |
/// https://stackoverflow.com/questions/39055985/distance-between-center-to-any-point-on-edge-of-rectangle-in-javascript | |
/// </summary> | |
public static FPVector2 getPointOnRectangle(FP angle, FP halfWidth, FP halfHeight) { | |
var c = FPMath.Cos(angle); | |
var s = FPMath.Sin(angle); | |
var point = FPVector2.Zero; | |
if (halfWidth * FPMath.Abs(s) < halfHeight * FPMath.Abs(c)) { | |
point.X = FPMath.Sign(c) * halfWidth; | |
point.Y = FPMath.Tan(angle) * point.X; | |
} else { | |
point.Y = FPMath.Sign(s) * halfHeight; | |
point.X = FPMath.Cos(angle) / FPMath.Sin(angle) * point.Y; | |
} | |
return point; | |
} | |
public static FP sqr(FP fp) => fp * fp; | |
/// <summary>Computes reciprocal (https://www.mathsisfun.com/reciprocal.html) of the given value.</summary> | |
/// <remarks>Properly handles zero values.</remarks> | |
public static FP recipOr0(FP fp) => fp != FP._0 ? 1 / fp : FP._0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using FPCSharpUnity.core.exts; | |
using GenerationAttributes; | |
using Photon.Deterministic; | |
using Quantum.Game.data; | |
using JetBrains.Annotations; | |
using Quantum.Game.extensions; | |
namespace Quantum; | |
public partial class Frame { | |
[LazyProperty] public Draw draw => new Draw(this); | |
/// <summary> | |
/// This must be invoked from <see cref="CopyFromUser"/>. | |
/// </summary> | |
[Conditional("DEBUG")] | |
void copyFromUserDraw(Frame oldFrame) { | |
var oldDraw = oldFrame.draw; | |
// We don't know the frame number when copying frame data, thus we clean up what we can and copy the rest to the | |
// new frame. We'll have to do additional filtering when we draw things. | |
oldDraw.cleanupExpired(); | |
draw.copyFromOldFrame(oldDraw); | |
} | |
/// <summary> | |
/// Extended API to draw debug data to Unity scene that has more capabilities than <see cref="Quantum.Draw"/>. | |
/// </summary> | |
public partial class Draw { | |
[Record] public readonly partial struct DebugLine { | |
public readonly FPVector2 start, end; | |
public readonly ColorRGBA color; | |
public readonly FrameNumber drawUntilFrame; | |
/// See <see cref="DebuggingSettings.Draw._lineWidth"/> | |
public readonly FP lineWidth; | |
/// See <see cref="DebuggingSettings.Draw._drawLabel"/> | |
public readonly bool drawLabel; | |
} | |
[Record] public readonly partial struct DebugCircle { | |
public readonly FPVector2 position; | |
public readonly FP radius; | |
public readonly ColorRGBA color; | |
public readonly FrameNumber drawStartedAtFrame, drawUntilFrame; | |
/// <inheritdoc cref="DebuggingSettings.Draw._lineWidth"/> | |
public readonly FP lineWidth; | |
} | |
readonly Frame frame; | |
[PublicAPI] public readonly List<DebugLine> lines = new(); | |
[PublicAPI] public readonly List<DebugCircle> circles = new(); | |
[PublicAPI] public FrameNumber frameNumber => frame.NumberT; | |
public Draw(Frame frame) => this.frame = frame; | |
/// <summary> | |
/// Removes all drawings which have expired. | |
/// </summary> | |
public void cleanupExpired() { | |
lines.removeWhere(frameNumber, static (line, no) => no > line.drawUntilFrame); | |
circles.removeWhere(frameNumber, static (line, no) => no > line.drawUntilFrame); | |
} | |
/// <summary> | |
/// Copies the lines which should still be visible in this frame from an old frame into the current frame. | |
/// </summary> | |
/// <param name="oldDraw">Instance of <see cref="Draw"/> from the old frame.</param> | |
public void copyFromOldFrame(Draw oldDraw) { | |
lines.Clear(); | |
lines.AddRange(oldDraw.lines); | |
circles.Clear(); | |
circles.AddRange(oldDraw.circles); | |
} | |
[Conditional("DEBUG")] | |
public void Line( | |
FPVector2 start, FPVector2 end, ColorRGBA? color = null, float duration = 0, FP? lineWidth = default, | |
bool drawLabel = false | |
) { | |
var line = new DebugLine( | |
start: start, | |
end: end, | |
color: color.GetValueOrDefault(ColorRGBA.Cyan), | |
drawUntilFrame: new(frame.Number + Math.Max(0, (int) Math.Round(duration * frame.SessionConfig.UpdateFPS))), | |
lineWidth: lineWidth ?? FP._1, | |
drawLabel: drawLabel | |
); | |
lines.Add(line); | |
} | |
[Conditional("DEBUG")] | |
public void Circle( | |
FPVector2 position, FP radius, ColorRGBA? color = null, float duration = 0, FP? lineWidth = default | |
) { | |
var circle = new DebugCircle( | |
position: position, | |
radius: radius, | |
color: color.GetValueOrDefault(ColorRGBA.Cyan), | |
drawStartedAtFrame: frame.NumberT, | |
drawUntilFrame: new(frame.Number + Math.Max(0, (int) Math.Round(duration * frame.SessionConfig.UpdateFPS))), | |
lineWidth: lineWidth ?? FP._1 | |
); | |
circles.Add(circle); | |
} | |
[Conditional("DEBUG")] | |
public void Circle(FPVector2 position, FP radius, DebuggingSettings.Draw settings) => | |
Circle(position, radius, settings.color, settings.duration, lineWidth: settings.lineWidth); | |
[Conditional("DEBUG")] | |
public void Arrow( | |
FPVector2 position, FPVector2 direction, DebuggingSettings.Draw settings, FP? height = null, FP? width = null | |
) => Arrow( | |
position, direction, settings.color, settings.duration, height: height, width: width, | |
lineWidth: settings.lineWidth, drawLabel: settings.drawLabel | |
); | |
[Conditional("DEBUG")] | |
public void Arrow( | |
FPVector2 position, FPVector2 direction, ColorRGBA? color = null, float duration = 0, | |
FP? height = null, FP? width = null, FP? lineWidth = default, bool drawLabel = false | |
) { | |
if (direction == FPVector2.Zero) return; | |
// https://stackoverflow.com/a/10316601/935259 | |
var end = position + direction; | |
var directionNormalized = direction.Normalized; | |
var height_ = height ?? FP._0_10 * FPMath.Sqrt(3); | |
var width_ = width ?? FP._0_10; | |
var perpendicular = new FPVector2(-directionNormalized.Y, directionNormalized.X); | |
// We need to draw the label only on the main line, it is not needed for arrowhead lines. | |
Line(position, end, color, duration, lineWidth: lineWidth, drawLabel: drawLabel); | |
Line(end, end - height_ * directionNormalized + width_ * perpendicular, color, duration, lineWidth: lineWidth); | |
Line(end, end - height_ * directionNormalized - width_ * perpendicular, color, duration, lineWidth: lineWidth); | |
} | |
[Conditional("DEBUG")] | |
public void LineBox(FPVector2 start, FPVector2 end, FP width, DebuggingSettings.Draw settings) => | |
LineBox(start, end, width, settings.color, settings.duration, lineWidth: settings.lineWidth); | |
[Conditional("DEBUG")] | |
public void LineBox( | |
FPVector2 start, FPVector2 end, FP width, ColorRGBA? color = null, float duration = 0, FP? lineWidth = default | |
) { | |
var direction = end - start; | |
var normal = direction.rotate90().Normalized * width; | |
var topLeft = start + normal; | |
var topRight = end + normal; | |
var bottomRight = end - normal; | |
var bottomLeft = start - normal; | |
Line(topLeft, topRight, color, duration, lineWidth: lineWidth); | |
Line(topRight, bottomRight, color, duration, lineWidth: lineWidth); | |
Line(bottomRight, bottomLeft, color, duration, lineWidth: lineWidth); | |
Line(bottomLeft, topLeft, color, duration, lineWidth: lineWidth); | |
} | |
[Conditional("DEBUG")] | |
public void RectangleEdges( | |
FPVector2 position, FP radius, FP rotation = default, ColorRGBA? color = null | |
) => RectangleEdges(position, new FPVector2(radius, radius), rotation, color); | |
[Conditional("DEBUG")] | |
public void RectangleEdges( | |
FPVector2 position, FPVector2 extents, FP rotation, ColorRGBA? color = null, float duration = 0, | |
FP? lineWidth = default | |
) { | |
extents.extentsPoints( | |
rotation, out var bottomLeft, out var bottomRight, out var topLeft, out var topRight | |
); | |
bottomLeft += position; | |
bottomRight += position; | |
topLeft += position; | |
topRight += position; | |
Circle(position, FP._0_05, color, duration); | |
Circle(bottomLeft, FP._0_05, color, duration); | |
Circle(bottomRight, FP._0_05, color, duration); | |
Circle(topLeft, FP._0_05, color, duration); | |
Circle(topRight, FP._0_05, color, duration); | |
Line(bottomLeft, bottomRight, color, duration, lineWidth: lineWidth); | |
Line(bottomRight, topRight, color, duration, lineWidth: lineWidth); | |
Line(topRight, topLeft, color, duration, lineWidth: lineWidth); | |
Line(topLeft, bottomLeft, color, duration, lineWidth: lineWidth); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Initialized debug drawing things from the Quantum simulation. | |
/// </summary> | |
/// <param name="callbacks"></param> | |
/// <param name="tracker"></param> | |
/// <param name="debugDrawRenderingDisabled"> | |
/// If false at the time of <see cref="CallbackUpdateView"/> the rendering is skipped for one frame. | |
/// </param> | |
static void initDebugDraw( | |
CallbacksSubscribable callbacks, ITracker tracker, Val<bool> debugDrawRenderingDisabled | |
) { | |
// Make sure we do not use the standard Quantum drawing functions. | |
void logErrorIfQuantumDrawIsUsed<A>(A data) { | |
log.error($"Quantum drawing should not be used, instead please use Frame.draw! Tried to draw {data}."); | |
} | |
Draw.Init( | |
drawRay: logErrorIfQuantumDrawIsUsed, | |
drawLine: logErrorIfQuantumDrawIsUsed, | |
drawCircle: logErrorIfQuantumDrawIsUsed, | |
drawSphere: logErrorIfQuantumDrawIsUsed, | |
drawRectangle: logErrorIfQuantumDrawIsUsed, | |
drawBox: logErrorIfQuantumDrawIsUsed, | |
clear: () => { /* Do nothing, as Quantum drawing should not be used. */ } | |
); | |
// Do not subscribe to Quantum drawing callbacks as it should not be used. | |
// | |
// // Taken from `QuantumCallbackHandler_DebugDraw`. | |
// callbacks.subscribeCallback(tracker, (CallbackGameStarted _) => DebugDraw.Clear()); | |
// callbacks.subscribeCallback(tracker, (CallbackGameDestroyed _) => DebugDraw.Clear()); | |
// callbacks.subscribeCallback(tracker, (CallbackSimulateFinished _) => DebugDraw.TakeAll()); | |
// callbacks.subscribeCallback(tracker, (CallbackUpdateView _) => DebugDraw.DrawAll()); | |
callbacks.subscribeCallback(tracker, (CallbackUpdateView cb) => { | |
if (debugDrawRenderingDisabled.value) return; | |
var prefs = DebugDrawPrefs.instance; | |
// Draw Quantum dynamic colliders and entities. | |
if (prefs.drawEntities.value) QuantumGameALineGizmos.OnDrawGizmos( | |
getDrawer(), cb.Game, editorSettings: null /* Passing in null makes it use default settings. */ | |
); | |
// Draw our debug drawings. | |
if (prefs.drawSimulationDebugDraw.value) drawOurFrameDebugData(cb.Game.Frames.Predicted); | |
}); | |
// Use the in-game drawer so that the drawings would be visible even with Gizmos disabled. We want to disable | |
// Gizmos because when we turn them on, everything slows down to a crawl. | |
static CommandBuilder getDrawer() => Drawing.Draw.ingame; | |
void drawOurFrameDebugData(Frame f) { | |
var qtnDraw = f.draw; | |
// This is needed to do additional filtering when drawing, for more information see `Frame.copyFromUserDraw()`. | |
var frameNo = f.NumberT; | |
var drawer = getDrawer(); | |
foreach (var line in qtnDraw.lines) { | |
if (frameNo <= line.drawUntilFrame) drawLine(line); | |
} | |
foreach (var circle in qtnDraw.circles) { | |
if (frameNo <= circle.drawUntilFrame) drawCircle(qtnDraw, circle); | |
} | |
void drawLine(in Frame.Draw.DebugLine line) { | |
// Line width must be a positive number, or else we get an exception. | |
using var _ = drawer.WithLineWidth(line.lineWidth.AsFloat.atLeast(1)); | |
drawer.Line(line.start.ToUnityVector3(), line.end.ToUnityVector3(), line.color.ToColor()); | |
if (line.drawLabel) { | |
var diff = line.end - line.start; | |
fixedString.Clear(); | |
// We need to round this number before displaying it, because FP will often have 5 decimal digits even if it | |
// should be a whole number. | |
var roundedMagnitude = (float) Math.Round(diff.Magnitude.AsFloat, 2); | |
fixedString.Append(roundedMagnitude); | |
drawer.Label2D( | |
line.end.ToUnityVector3(), ref fixedString, color: line.color.ToColor(), | |
// Align so that sure that the line does not cross the text. | |
alignment: diff.X >= 0 ? LabelAlignment.MiddleLeft : LabelAlignment.MiddleRight, | |
// 14 is the default size. | |
sizeInPixels: 14 | |
); | |
} | |
} | |
void drawCircle(Frame.Draw draw, in Frame.Draw.DebugCircle circle) { | |
using var _ = drawer.WithLineWidth(circle.lineWidth.AsFloat.atLeast(1)); | |
var position = circle.position.ToUnityVector3(); | |
var radius = circle.radius.AsFloat; | |
if (circle.drawUntilFrame == circle.drawStartedAtFrame) { | |
drawer.CircleXY(position, radius, circle.color.ToColor()); | |
} | |
else { | |
var startTime = circle.drawStartedAtFrame; | |
var endTime = circle.drawUntilFrame; | |
var initialAlpha = circle.color.A; | |
var newAlpha = FloatExts.remap(draw.frameNumber.no, startTime.no, endTime.no, initialAlpha, 0); | |
var color = circle.color; | |
color.A = (byte) Mathf.RoundToInt(newAlpha); | |
drawer.CircleXY(position, radius, color.ToColor()); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Runtime.CompilerServices; | |
using FPCSharpUnity.core.data; | |
using Photon.Deterministic; | |
using Quantum.Utils; | |
using static FPCSharpUnity.core.data.Markers; | |
namespace Quantum.Game.extensions; | |
public static class FPVector2_ { | |
// Copied from Quantum v1, this was removed in v2 for some reason. | |
public static FPVector2 Cross(FP a, FPVector2 b) { | |
FPVector2 fpVector2; | |
fpVector2.X.RawValue = -a.RawValue * b.Y.RawValue >> 16; | |
fpVector2.Y.RawValue = a.RawValue * b.X.RawValue >> 16; | |
return fpVector2; | |
} | |
} | |
public static class FPVector2Exts { | |
/// <summary>As <see cref="FPVector2.Normalized"/> but marked with <see cref="Normalized"/>.</summary> | |
public static Marked<FPVector2, Normalized> normalizedSafe(this FPVector2 v) => new(v.Normalized); | |
public static FPVector2 invertX(this FPVector2 v) => | |
new FPVector2(-v.X, v.Y); | |
public static FP angle(this FPVector2 v) => | |
FPMath.Atan2(v.Y, v.X); | |
public static void extentsPoints( | |
this FPVector2 extents, | |
out FPVector2 bottomLeft, out FPVector2 bottomRight, | |
out FPVector2 topLeft, out FPVector2 topRight | |
) { | |
bottomLeft = new FPVector2(-extents.X, -extents.Y); | |
bottomRight = new FPVector2(extents.X, -extents.Y); | |
topLeft = new FPVector2(-extents.X, extents.Y); | |
topRight = new FPVector2(extents.X, extents.Y); | |
} | |
public static void extentsPoints( | |
this FPVector2 extents, FP rotation, | |
out FPVector2 bottomLeft, out FPVector2 bottomRight, | |
out FPVector2 topLeft, out FPVector2 topRight | |
) { | |
extentsPoints(extents, out bottomLeft, out bottomRight, out topLeft, out topRight); | |
if (rotation != FP._0) { | |
var sin = FPMath.Sin(rotation); | |
var cos = FPMath.Cos(rotation); | |
bottomLeft = FPVector2.Rotate(bottomLeft, sin: sin, cos: cos); | |
bottomRight = FPVector2.Rotate(bottomRight, sin: sin, cos: cos); | |
topLeft = FPVector2.Rotate(topLeft, sin: sin, cos: cos); | |
topRight = FPVector2.Rotate(topRight, sin: sin, cos: cos); | |
} | |
} | |
public static void extentsPoints( | |
this FPVector2 extents, FPVector2 position, FP rotation, | |
out FPVector2 bottomLeft, out FPVector2 bottomRight, | |
out FPVector2 topLeft, out FPVector2 topRight | |
) { | |
extentsPoints(extents, rotation, out bottomLeft, out bottomRight, out topLeft, out topRight); | |
if (position != FPVector2.Zero) { | |
bottomLeft += position; | |
bottomRight += position; | |
topLeft += position; | |
topRight += position; | |
} | |
} | |
public static FPVector2 rotate90(this FPVector2 v) => new FPVector2(-v.Y, v.X); | |
public static FPVector2 rotate180(this FPVector2 v) => new FPVector2(-v.X, -v.Y); | |
public static FPVector2 rotate270(this FPVector2 v) => new FPVector2(v.Y, -v.X); | |
/// <param name="extents"></param> | |
/// <param name="rotation"></param> | |
/// <param name="point"> | |
/// Point is relative to extents. That is (0, 0) is center of extents. | |
/// | |
/// You can relativize it using the power of math! | |
/// <code> | |
/// collider.BoxExtents.extentsContains(c->Transform2D->Position - collider.Position) | |
/// </code> | |
/// </param> | |
public static bool extentsContains(this FPVector2 extents, FP rotation, FPVector2 point) { | |
if (rotation == FP._0) { | |
return | |
-extents.X <= point.X && point.X <= extents.X | |
&& -extents.Y <= point.Y && point.Y <= extents.Y; | |
} | |
else { | |
extents.extentsPoints(rotation, out var bottomLeft, out var bottomRight, out var topLeft, out var topRight); | |
return | |
FPMath2.onWhichSideOfLineIsPoint(bottomLeft, bottomRight, point) != Side.Right | |
&& FPMath2.onWhichSideOfLineIsPoint(bottomRight, topRight, point) != Side.Right | |
&& FPMath2.onWhichSideOfLineIsPoint(topRight, topLeft, point) != Side.Right | |
&& FPMath2.onWhichSideOfLineIsPoint(topLeft, bottomLeft, point) != Side.Right; | |
} | |
} | |
public static FPVector2 addX(this FPVector2 value, FP add) { | |
value.X.RawValue += add.RawValue; | |
return value; | |
} | |
public static FPVector2 addY(this FPVector2 value, FP add) { | |
value.Y.RawValue += add.RawValue; | |
return value; | |
} | |
public static FPVector2 mulX(this FPVector2 value, FP mul) { | |
value.X.RawValue = (value.X.RawValue * mul.RawValue) >> 16; | |
return value; | |
} | |
public static FPVector2 mulY(this FPVector2 value, FP mul) { | |
value.Y.RawValue = (value.Y.RawValue * mul.RawValue) >> 16; | |
return value; | |
} | |
public static FPVector2 flipX(this FPVector2 value, bool flip) { | |
if (flip) value.X.RawValue = -value.X.RawValue; | |
return value; | |
} | |
public static FPVector2 flipY(this FPVector2 value, bool flip) { | |
if (flip) value.Y.RawValue = -value.Y.RawValue; | |
return value; | |
} | |
public static FPVector2 abs(this FPVector2 vector) => new FPVector2(FPMath.Abs(vector.X), FPMath.Abs(vector.Y)); | |
// ReSharper disable once UnusedMember.Global | |
public static void Destructure(this FPVector2 v, out FP x, out FP y) { | |
x = v.X; | |
y = v.Y; | |
} | |
public static bool NormalizeSafe(this FPVector2 v, out FPVector2 normalized, out FP magnitude) { | |
magnitude = new FP {RawValue = (v.X.RawValue * v.X.RawValue >> 16) + (v.Y.RawValue * v.Y.RawValue >> 16)}; | |
if (magnitude.RawValue == 0L) { | |
normalized = new FPVector2(); | |
return true; | |
} | |
if (magnitude.RawValue < 0L) { | |
normalized = default; | |
return false; | |
} | |
magnitude.RawValue = FPMath.SqrtRaw(magnitude.RawValue); | |
normalized.X.RawValue = (v.X.RawValue << 16) / magnitude.RawValue; | |
normalized.Y.RawValue = (v.Y.RawValue << 16) / magnitude.RawValue; | |
return true; | |
} | |
/// <summary> | |
/// Projects vector <see cref="v"/> to a normalized vector <see cref="toVector"/>. | |
/// (https://en.wikipedia.org/wiki/Vector_projection) | |
/// </summary> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 projectTo(this FPVector2 v, Marked<FPVector2, Normalized> toVector) => | |
FPVector2.Dot(toVector, v) * toVector.data; | |
/// <summary> | |
/// Projects vector <see cref="v"/> to a vector <see cref="toVector"/>. | |
/// (https://en.wikipedia.org/wiki/Vector_projection) | |
/// </summary> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 projectTo(this FPVector2 v, FPVector2 toVector) => | |
FPVector2.Dot(toVector, v) * toVector / FPVector2.Dot(toVector, toVector); | |
/// <summary> Squares each vector component. </summary> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 squareComponents(this FPVector2 value) => | |
new FPVector2(value.X * value.X, value.Y * value.Y); | |
/// <summary> Squares each vector component. Keeps the original sign. </summary> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 squareComponentsSigned(this FPVector2 value) => new FPVector2( | |
value.X * value.X * FPMath.Sign(value.X), | |
value.Y * value.Y * FPMath.Sign(value.Y) | |
); | |
/// <summary> Squares vector magnitude. </summary> | |
/// <returns> Same vector with a modified magnitude. </returns> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 squareMagnitude(this FPVector2 value) => | |
FPVector2.Normalize(value) * value.SqrMagnitude; | |
/// <summary> Takes a square root of each vector component. Keeps the original sign. </summary> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 sqrtComponentsSigned(this FPVector2 value) => new FPVector2( | |
value.X >= FP._0 ? FPMath.Sqrt(value.X) : -FPMath.Sqrt(-value.X), | |
value.Y >= FP._0 ? FPMath.Sqrt(value.Y) : -FPMath.Sqrt(-value.Y) | |
); | |
/// <summary> Takes a square root of vector magnitude. </summary> | |
/// <returns> Same vector with a modified magnitude. </returns> | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static FPVector2 sqrtMagnitude(this FPVector2 value) => | |
FPVector2.Normalize(value, out var magnitude) * FPMath.Sqrt(magnitude); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment