Skip to content

Instantly share code, notes, and snippets.

@arturaz
Last active December 20, 2022 15:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arturaz/dbdf3c9fe232b0c315c153e37ed50786 to your computer and use it in GitHub Desktop.
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.
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);
}
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;
}
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;
}
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);
}
}
}
/// <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());
}
}
}
}
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