Create bare godot project
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
68
AGENTS.md
Normal file
68
AGENTS.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
This repository should be organized as a Godot 4 .NET project from the root:
|
||||
|
||||
- `project.godot` project entrypoint
|
||||
- `SideScrollerGame.csproj` and optional solution file
|
||||
- `scenes/` playable scenes such as `scenes/player/Player.tscn`
|
||||
- `scripts/` C# gameplay code paired with scenes
|
||||
- `assets/` sprites, audio, fonts, and tile sets
|
||||
- `tests/SideScrollerGame.Tests/` .NET test project for engine-light logic
|
||||
- `addons/` third-party Godot plugins when needed
|
||||
- `build/` local export output; do not commit generated files unless release workflow requires it
|
||||
|
||||
Keep scene and script paths aligned. Example: `scenes/enemies/Slime.tscn` with `scripts/enemies/Slime.cs`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Run commands from the repository root. On Windows PowerShell, use the repo-local wrapper as `.\godot`.
|
||||
|
||||
- `.\godot --editor --path .` opens the project in the editor
|
||||
- `.\godot --headless --path . --build-solutions` builds generated C# project files through Godot
|
||||
- `dotnet build` compiles the game assemblies and catches regular C# build errors
|
||||
- `dotnet test tests/SideScrollerGame.Tests` runs automated unit tests
|
||||
- `.\godot --headless --path . --export-release "Windows Desktop" ./build/SideScrollerGame.exe` exports a desktop build when presets are configured
|
||||
|
||||
Document any custom wrapper scripts in `README.md` if the team adds them later.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
Use 4 spaces for indentation, nullable reference types, and one primary public type per file. Keep Godot node scripts thin and move reusable gameplay rules into plain C# classes that do not depend on scene state.
|
||||
|
||||
- `PascalCase` for classes, methods, properties, scene files, and C# scripts: `PlayerController`, `PlayerController.cs`
|
||||
- `m_PascalCase` for private fields
|
||||
- `s_PascalCase` for static fields
|
||||
- `camelCase` for locals and parameters
|
||||
- `snake_case` for asset filenames: `player_idle.png`, `forest_theme.ogg`
|
||||
|
||||
Use partial classes only when Godot generation requires them; avoid mixing unrelated responsibilities in one node script.
|
||||
Keep a strict element order inside of types:
|
||||
- Nested types
|
||||
- Constructors
|
||||
- Disposable implementation
|
||||
- Methods
|
||||
- Properties
|
||||
- Static Members
|
||||
- Events
|
||||
- Fields
|
||||
|
||||
## Testing Guidelines
|
||||
Use a .NET test project under `tests/` and mirror gameplay areas, for example `tests/SideScrollerGame.Tests/Player/PlayerControllerTests.cs`. Prefer xUnit-style or NUnit-style tests for movement math, combat rules, save/load logic, and other engine-light systems.
|
||||
|
||||
Add regression tests for every gameplay bug fix when practical. If scene-level automation is added later, keep it separate from fast unit tests.
|
||||
|
||||
## Working rules
|
||||
|
||||
- This is a Windows environment, WSL is not installed (i.e. sed is not available). You're running under PowerShell 7.6.0. Due to platform restrictions, file deletions are not possible. Replacing the entire file content via a context diff is a viable alternative.
|
||||
- PowerShell doesn't support bash-style heredocs. If complex scripts need to be executed, consider using python. Run Python code using python -c with inline commands instead of python - <<'PY'.
|
||||
- Before beginning with the edit phase, always present a plan first. Only begin editing after the user approves the plan.
|
||||
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
|
||||
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
|
||||
- After every iteration, run `jb cleanupcode --build=False $file1;$file2;...` for every file you touched.
|
||||
- After every iteration, if there's a relevant documentation for the current task, update it according to the change.
|
||||
- Update the wording of touched concerns instead of introducing incremental change reports
|
||||
- The documentation should always represent the current state in its entirety and not derail into a historical development log.
|
||||
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
|
||||
- Keep changes small and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
|
||||
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
|
||||
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
|
||||
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
|
||||
1237
FixPoint/FixPoint16.cs
Normal file
1237
FixPoint/FixPoint16.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
FixPoint/FixPoint16.cs.uid
Normal file
1
FixPoint/FixPoint16.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1ymd8m2to6u
|
||||
386
FixPoint/FixPoint16Long.cs
Normal file
386
FixPoint/FixPoint16Long.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
#if DEBUG
|
||||
#define RANGE_CHECK
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MagmaEngine.Math;
|
||||
|
||||
public struct FixPoint16Long : IComparable, IComparable<FixPoint16Long>, IEquatable<FixPoint16Long>
|
||||
{
|
||||
public FixPoint16Long(FixPoint16Long other)
|
||||
{
|
||||
m_Value = other.m_Value;
|
||||
}
|
||||
|
||||
public FixPoint16Long(int value)
|
||||
{
|
||||
m_Value = (long)value << c_Shift;
|
||||
}
|
||||
|
||||
public FixPoint16Long(long value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Long to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
m_Value = value << c_Shift;
|
||||
}
|
||||
|
||||
public FixPoint16Long(double value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Double to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
if (value < 0.0)
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) - 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_Multiplier) + 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
public FixPoint16Long(float value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value < c_IntegerMin || value > c_IntegerMax)
|
||||
{
|
||||
throw new ArithmeticException($"Single to FixPoint argument out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
if (value < 0.0f)
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) - 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Value = (int)((value * c_MultiplierFloat) + 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
public long ToLongFloor()
|
||||
{
|
||||
return m_Value >> c_Shift;
|
||||
}
|
||||
|
||||
public long ToLongCeil()
|
||||
{
|
||||
return (m_Value + c_FractionMask) >> c_Shift;
|
||||
}
|
||||
|
||||
public long ToLongRound()
|
||||
{
|
||||
if (m_Value < 0)
|
||||
{
|
||||
return -((-m_Value + c_Half) >> c_Shift);
|
||||
}
|
||||
|
||||
return (m_Value + c_Half) >> c_Shift;
|
||||
}
|
||||
|
||||
public long ToLong()
|
||||
{
|
||||
if (m_Value < 0)
|
||||
{
|
||||
return -(-m_Value >> c_Shift);
|
||||
}
|
||||
|
||||
return m_Value >> c_Shift;
|
||||
}
|
||||
|
||||
public double ToDouble()
|
||||
{
|
||||
return c_Divisor * m_Value;
|
||||
}
|
||||
|
||||
public float ToFloat()
|
||||
{
|
||||
return c_DivisorFloat * m_Value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ToDouble()}[0x{m_Value:x16}]";
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return m_Value.GetHashCode();
|
||||
}
|
||||
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
if (obj is not FixPoint16Long other)
|
||||
return -1;
|
||||
|
||||
return m_Value.CompareTo(other.m_Value);
|
||||
}
|
||||
|
||||
public int CompareTo(FixPoint16Long other)
|
||||
{
|
||||
return m_Value.CompareTo(other.m_Value);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((FixPoint16Long)obj).m_Value == m_Value;
|
||||
}
|
||||
|
||||
public bool Equals(FixPoint16Long other)
|
||||
{
|
||||
return other.m_Value == m_Value;
|
||||
}
|
||||
|
||||
public bool IsZero()
|
||||
{
|
||||
return m_Value == 0L;
|
||||
}
|
||||
|
||||
public static bool operator ==(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value == b.m_Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value != b.m_Value;
|
||||
}
|
||||
|
||||
public static bool operator <(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value < b.m_Value;
|
||||
}
|
||||
|
||||
public static bool operator >(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value > b.m_Value;
|
||||
}
|
||||
|
||||
public static bool operator <=(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value <= b.m_Value;
|
||||
}
|
||||
|
||||
public static bool operator >=(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return a.m_Value >= b.m_Value;
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator <<(FixPoint16Long a, int shift)
|
||||
{
|
||||
return new() { m_Value = a.m_Value << shift };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator >> (FixPoint16Long a, int shift)
|
||||
{
|
||||
return new() { m_Value = a.m_Value >> shift };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator +(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return new() { m_Value = a.m_Value + b.m_Value };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator -(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
return new() { m_Value = a.m_Value - b.m_Value };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator *(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
Int128 bigA = a.m_Value;
|
||||
Int128 bigB = b.m_Value;
|
||||
var result = ((bigA * bigB) + c_Half) >> c_Shift;
|
||||
#if RANGE_CHECK
|
||||
if (result < long.MinValue || result > long.MaxValue)
|
||||
{
|
||||
throw new ArithmeticException($"Multiplication result out of range: {result}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (long)result };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator /(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (b.m_Value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
Int128 result;
|
||||
|
||||
if (((ulong)a.m_Value & 0x8000000000000000UL) == ((ulong)b.m_Value & 0x8000000000000000UL))
|
||||
{
|
||||
result = (((Int128)a.m_Value << c_Shift) + (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (((Int128)a.m_Value << c_Shift) - (b.m_Value / 2)) / b.m_Value;
|
||||
}
|
||||
#if RANGE_CHECK
|
||||
if (result < long.MinValue || result > long.MaxValue)
|
||||
{
|
||||
throw new ArithmeticException($"Division result out of range: {result}");
|
||||
}
|
||||
#endif
|
||||
return new() { m_Value = (long)result };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator -(FixPoint16Long a)
|
||||
{
|
||||
return new() { m_Value = -a.m_Value };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator *(FixPoint16Long a, int value)
|
||||
{
|
||||
return new() { m_Value = a.m_Value * value };
|
||||
}
|
||||
|
||||
public static FixPoint16Long operator /(FixPoint16Long a, int value)
|
||||
{
|
||||
#if RANGE_CHECK
|
||||
if (value == 0)
|
||||
{
|
||||
throw new ArithmeticException("Divison by zero");
|
||||
}
|
||||
#endif
|
||||
if (((a.m_Value >> 32) & 0x80000000) == (value & 0x80000000))
|
||||
{
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value + (value / 2)) / value) };
|
||||
}
|
||||
|
||||
return new() { m_Value = (long)(((Int128)a.m_Value - (value / 2)) / value) };
|
||||
}
|
||||
|
||||
public static implicit operator FixPoint16Long(int value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static implicit operator FixPoint16Long(long value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static implicit operator FixPoint16Long(FixPoint16 value)
|
||||
{
|
||||
return new() { m_Value = value.m_Value };
|
||||
}
|
||||
|
||||
public static explicit operator FixPoint16Long(double value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static explicit operator FixPoint16Long(float value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static explicit operator int(FixPoint16Long value)
|
||||
{
|
||||
return (int)value.ToLong();
|
||||
}
|
||||
|
||||
public static explicit operator long(FixPoint16Long value)
|
||||
{
|
||||
return value.ToLong();
|
||||
}
|
||||
|
||||
public static explicit operator double(FixPoint16Long value)
|
||||
{
|
||||
return value.ToDouble();
|
||||
}
|
||||
|
||||
public static explicit operator float(FixPoint16Long value)
|
||||
{
|
||||
return value.ToFloat();
|
||||
}
|
||||
|
||||
public static FixPoint16Long Floor(FixPoint16Long value)
|
||||
{
|
||||
return value.ToLongFloor();
|
||||
}
|
||||
|
||||
public static FixPoint16Long Ceil(FixPoint16Long value)
|
||||
{
|
||||
return value.ToLongCeil();
|
||||
}
|
||||
|
||||
public static FixPoint16Long Round(FixPoint16Long value)
|
||||
{
|
||||
return value.ToLongRound();
|
||||
}
|
||||
|
||||
public static int Sign(FixPoint16Long value)
|
||||
{
|
||||
return System.Math.Sign(value.m_Value);
|
||||
}
|
||||
|
||||
public static FixPoint16Long Abs(FixPoint16Long value)
|
||||
{
|
||||
return new() { m_Value = System.Math.Abs(value.m_Value) };
|
||||
}
|
||||
|
||||
public static FixPoint16Long Min(FixPoint16Long value1, FixPoint16Long value2)
|
||||
{
|
||||
return new() { m_Value = System.Math.Min(value1.m_Value, value2.m_Value) };
|
||||
}
|
||||
|
||||
public static FixPoint16Long Max(FixPoint16Long value1, FixPoint16Long value2)
|
||||
{
|
||||
return new() { m_Value = System.Math.Max(value1.m_Value, value2.m_Value) };
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16Long Length(FixPoint16Long a, FixPoint16Long b)
|
||||
{
|
||||
var aSquared = a.m_Value * a.m_Value;
|
||||
var bSquared = b.m_Value * b.m_Value;
|
||||
var value = aSquared + bSquared;
|
||||
#if RANGE_CHECK
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArithmeticException($"Length squared out of range: {value}");
|
||||
}
|
||||
#endif
|
||||
|
||||
return new() { m_Value = IntMath.Sqrt(value) };
|
||||
}
|
||||
|
||||
private const int c_Shift = 16;
|
||||
private const long c_IntegerMin = -140737488355327L;
|
||||
private const long c_IntegerMax = 140737488355327L;
|
||||
private const long c_Half = 32786L;
|
||||
private const long c_FractionMask = 0x000000000000ffffL;
|
||||
private const double c_Multiplier = 65536.0;
|
||||
private const double c_Divisor = 1.0 / 65536.0;
|
||||
private const float c_MultiplierFloat = 65536.0f;
|
||||
private const float c_DivisorFloat = (float)c_Divisor;
|
||||
|
||||
public long m_Value;
|
||||
|
||||
public static readonly FixPoint16Long Zero = new() { m_Value = 0 };
|
||||
public static readonly FixPoint16Long MinValue = new() { m_Value = long.MinValue };
|
||||
public static readonly FixPoint16Long MaxValue = new() { m_Value = long.MaxValue };
|
||||
public static readonly FixPoint16Long Epsilon = new() { m_Value = 1 };
|
||||
public static readonly FixPoint16Long MinusEpsilon = new() { m_Value = -1 };
|
||||
public static readonly FixPoint16Long One = new(1);
|
||||
public static readonly FixPoint16Long MinusOne = new(-1);
|
||||
public static readonly FixPoint16Long Half = new() { m_Value = One.m_Value / 2 };
|
||||
public static readonly FixPoint16Long MinusHalf = -Half;
|
||||
}
|
||||
1
FixPoint/FixPoint16Long.cs.uid
Normal file
1
FixPoint/FixPoint16Long.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://i21oxan7sd8p
|
||||
24670
FixPoint/FixPoint16Tables.cs
Normal file
24670
FixPoint/FixPoint16Tables.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
FixPoint/FixPoint16Tables.cs.uid
Normal file
1
FixPoint/FixPoint16Tables.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c10dgxlu0d17c
|
||||
69
FixPoint/FixPointUtil.cs
Normal file
69
FixPoint/FixPointUtil.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace MagmaEngine.Math;
|
||||
|
||||
public static class FixPointUtil
|
||||
{
|
||||
public static long DistancePointToSegmentSquared(FixPointVector2 point, FixPointVector2 segmentStart, FixPointVector2 segmentEnd)
|
||||
{
|
||||
var segment = segmentEnd - segmentStart;
|
||||
var toStart = point - segmentStart;
|
||||
|
||||
var segmentLengthSquared = FixPointVector2.DotLong(segment, segment);
|
||||
if (segmentLengthSquared == 0L)
|
||||
return FixPointVector2.DotLong(toStart, toStart);
|
||||
|
||||
var projection = FixPointVector2.DotLong(toStart, segment);
|
||||
if (projection <= 0)
|
||||
return FixPointVector2.DotLong(toStart, toStart);
|
||||
|
||||
FixPointVector2 toPointOnSegment;
|
||||
if (projection >= segmentLengthSquared)
|
||||
toPointOnSegment = point - segmentEnd;
|
||||
else
|
||||
toPointOnSegment = toStart - segment * FixPoint16.FromRational(projection, segmentLengthSquared);
|
||||
|
||||
return FixPointVector2.DotLong(toPointOnSegment, toPointOnSegment);
|
||||
}
|
||||
|
||||
public static FixPoint16 DistancePointToSegment(FixPointVector2 point, FixPointVector2 segmentStart, FixPointVector2 segmentEnd)
|
||||
{
|
||||
return FixPoint16.SqrtLong(DistancePointToSegmentSquared(point, segmentStart, segmentEnd));
|
||||
}
|
||||
|
||||
public static bool LineLineIntersection(FixPointVector2 line1Start, FixPointVector2 line1End, FixPointVector2 line2Start, FixPointVector2 line2End, out FixPointVector2 intersectionPoint)
|
||||
{
|
||||
intersectionPoint = default;
|
||||
|
||||
var s1 = line1End - line1Start;
|
||||
var s2 = line2End - line2Start;
|
||||
|
||||
var det = (FixPoint16Long)(-s2.m_X) * s1.m_Y + (FixPoint16Long)(s1.m_X) * s2.m_Y;
|
||||
if (FixPoint16Long.Abs(det) < s_IntersectionEpsilon)
|
||||
return false;
|
||||
|
||||
var t = ((FixPoint16Long)s2.m_X * (line1Start.m_Y - line2Start.m_Y) - (FixPoint16Long)s2.m_Y * (line1Start .m_X - line2Start.m_X)) / det;
|
||||
var px = line1Start.m_X + (t * s1.m_X);
|
||||
var py = line1Start.m_Y + (t * s1.m_Y);
|
||||
|
||||
if (px.m_Value > s_LineIntersectionMax || px.m_Value < s_LineIntersectionMin || py.m_Value > s_LineIntersectionMax || py.m_Value < s_LineIntersectionMin)
|
||||
return false;
|
||||
|
||||
intersectionPoint = new FixPointVector2(FixPoint16.FromValue(px.m_Value), FixPoint16.FromValue(py.m_Value));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static FixPoint16 MultiplyClamped(FixPoint16 a, FixPoint16 b)
|
||||
{
|
||||
var iResult = (((long)a.m_Value * b.m_Value) + FixPoint16.c_Half) >> FixPoint16.c_Shift;
|
||||
|
||||
if (iResult < FixPoint16.c_LongMin)
|
||||
iResult = FixPoint16.c_LongMin;
|
||||
else if (iResult > FixPoint16.c_LongMax)
|
||||
iResult = FixPoint16.c_LongMax;
|
||||
|
||||
return new() { m_Value = (int)iResult };
|
||||
}
|
||||
|
||||
private static readonly FixPoint16 s_IntersectionEpsilon = FixPoint16.Epsilon * 30;
|
||||
private static readonly long s_LineIntersectionMax = FixPoint16.c_LongMax / 4;
|
||||
private static readonly long s_LineIntersectionMin = FixPoint16.c_LongMin / 4;
|
||||
}
|
||||
1
FixPoint/FixPointUtil.cs.uid
Normal file
1
FixPoint/FixPointUtil.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bxrdtopa5ct2u
|
||||
1245
FixPoint/FixPointVector2.cs
Normal file
1245
FixPoint/FixPointVector2.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
FixPoint/FixPointVector2.cs.uid
Normal file
1
FixPoint/FixPointVector2.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cn1byd6pp54cv
|
||||
993
FixPoint/FixPointVector3.cs
Normal file
993
FixPoint/FixPointVector3.cs
Normal file
@@ -0,0 +1,993 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MagmaEngine.Math;
|
||||
|
||||
public struct SFixPointVector3 : IEquatable<SFixPointVector3>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of FixPointVector3
|
||||
/// </summary>
|
||||
/// <param name="x">Initial value for the x-component of the vector.</param>
|
||||
/// <param name="y">Initial value for the y-component of the vector.</param>
|
||||
/// <param name="z">Initial value for the z-component of the vector.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SFixPointVector3(int x, int y, int z)
|
||||
{
|
||||
m_X = x;
|
||||
m_Y = y;
|
||||
m_Z = z;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SFixPointVector3(FixPoint16 x, FixPoint16 y, FixPoint16 z)
|
||||
{
|
||||
m_X = x;
|
||||
m_Y = y;
|
||||
m_Z = z;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SFixPointVector3(FixPoint16 x)
|
||||
{
|
||||
m_X = x;
|
||||
m_Y = x;
|
||||
m_Z = x;
|
||||
}
|
||||
|
||||
public SFixPointVector3(float x, float y, float z)
|
||||
{
|
||||
m_X = new(x);
|
||||
m_Y = new(y);
|
||||
m_Z = new(z);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SFixPointVector3(Vector3 coord)
|
||||
{
|
||||
m_X = new(coord.X);
|
||||
m_Y = new(coord.Y);
|
||||
m_Z = new(coord.Z);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static explicit operator SFixPointVector3(Vector3 coord)
|
||||
{
|
||||
return new(new(coord.X), new(coord.Y), new FixPoint16(coord.Z));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static explicit operator Vector3(SFixPointVector3 coord)
|
||||
{
|
||||
return new(coord.m_X.ToFloat(), coord.m_Y.ToFloat(), coord.m_Z.ToFloat());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public Vector3 ToVector3()
|
||||
{
|
||||
return new(m_X.ToFloat(), m_Y.ToFloat(), m_Z.ToFloat());
|
||||
}
|
||||
|
||||
#region -- base overrides ---------------------------------------------
|
||||
|
||||
[Pure]
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({m_X.ToString()}, {m_Y.ToString()}, {m_Z.ToString()})";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool Equals(SFixPointVector3 other)
|
||||
{
|
||||
return m_X == other.m_X && m_Y == other.m_Y && m_Z == other.m_Z;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SFixPointVector3 fixPointVector3)
|
||||
return Equals(fixPointVector3);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return m_X.GetHashCode() + m_Y.GetHashCode() + m_Z.GetHashCode();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region -- static properties ------------------------------------------
|
||||
|
||||
public static readonly SFixPointVector3 s_Zero = new(0, 0, 0);
|
||||
public static readonly SFixPointVector3 s_One = new(1, 1, 1);
|
||||
public static readonly SFixPointVector3 s_UnitX = new(1, 0, 0);
|
||||
public static readonly SFixPointVector3 s_UnitY = new(0, 1, 0);
|
||||
public static readonly SFixPointVector3 s_UnitZ = new(0, 0, 1);
|
||||
public static readonly SFixPointVector3 s_MaxValue = new(FixPoint16.MaxValue, FixPoint16.MaxValue, FixPoint16.MaxValue);
|
||||
public static readonly SFixPointVector3 s_MinValue = new(FixPoint16.MinValue, FixPoint16.MinValue, FixPoint16.MinValue);
|
||||
|
||||
#endregion
|
||||
|
||||
#region -- public properties -----------------------------------------
|
||||
|
||||
public FixPoint16 this[int i]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0: return m_X;
|
||||
case 1: return m_Y;
|
||||
case 2: return m_Z;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(i));
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0:
|
||||
m_X = value;
|
||||
break;
|
||||
case 1:
|
||||
m_Y = value;
|
||||
break;
|
||||
case 2:
|
||||
m_Z = value;
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new normalized FixPointVector3 from the current vector.
|
||||
/// </summary>
|
||||
public SFixPointVector3 Normalized
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get
|
||||
{
|
||||
var length = Length();
|
||||
|
||||
if (!length.IsZero())
|
||||
{
|
||||
SFixPointVector3 result = new(m_X / length, m_Y / length, m_Z / length);
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Error: can not normalize vector, the vector length is zero.");
|
||||
}
|
||||
}
|
||||
|
||||
public FixPointVector2 XY
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new(m_X, m_Y);
|
||||
}
|
||||
|
||||
public FixPointVector2 XZ
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
get => new(m_X, m_Z);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region -- public methods ---------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the length of the current vector.
|
||||
/// </summary>
|
||||
/// <returns>The Length of the current vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public FixPoint16 Length()
|
||||
{
|
||||
return FixPoint16.Length(m_X, m_Y, m_Z);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public int CompareLength(FixPoint16 length)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((m_X.m_Value * (long)m_X.m_Value) + (m_Y.m_Value * (long)m_Y.m_Value) + (m_Z.m_Value * (long)m_Z.m_Value)).CompareTo(length.m_Value * (long)length.m_Value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public long LengthSquaredLong()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (m_X.m_Value * (long)m_X.m_Value) + (m_Y.m_Value * (long)m_Y.m_Value) + (m_Z.m_Value * (long)m_Z.m_Value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public int CompareLength(SFixPointVector3 other)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((m_X.m_Value * (long)m_X.m_Value) + (m_Y.m_Value * (long)m_Y.m_Value) + (m_Z.m_Value * (long)m_Z.m_Value)).CompareTo(
|
||||
(other.m_X.m_Value * (long)other.m_X.m_Value) + (other.m_Y.m_Value * (long)other.m_Y.m_Value) + (other.m_Z.m_Value * (long)other.m_Z.m_Value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the current vector
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Normalize()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var length = Length();
|
||||
|
||||
if (!length.IsZero())
|
||||
{
|
||||
m_X = m_X / length;
|
||||
m_Y = m_Y / length;
|
||||
m_Z = m_Z / length;
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
throw new("Error: can not normalize vector, the vector length is zero.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool IsZero()
|
||||
{
|
||||
return m_X.IsZero() && m_Y.IsZero() && m_Z.IsZero();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a given scalar value to each component of the current FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="value">The scalar value</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(FixPoint16 value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X + value;
|
||||
m_Y = m_Y + value;
|
||||
m_Z = m_Z + value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a given FixPointVector3 to the current FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="other">The vector to be added.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(SFixPointVector3 other)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X + other.m_X;
|
||||
m_Y = m_Y + other.m_Y;
|
||||
m_Z = m_Z + other.m_Z;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts a scalar value from each component of the current FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="value">The scalar value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Subtract(FixPoint16 value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X - value;
|
||||
m_Y = m_Y - value;
|
||||
m_Z = m_Z - value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts a given FixPointVector3 from the current FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="other">The vector to be subtracted.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Subtract(SFixPointVector3 other)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X - other.m_X;
|
||||
m_Y = m_Y - other.m_Y;
|
||||
m_Z = m_Z - other.m_Z;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies each component of the current FixPointVector3 by a given scalar value.
|
||||
/// </summary>
|
||||
/// <param name="scalar">The scalar value.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Multiply(FixPoint16 scalar)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X * scalar;
|
||||
m_Y = m_Y * scalar;
|
||||
m_Z = m_Z * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies the current FixPointVector3 by another FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="other">The source vector.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Multiply(SFixPointVector3 other)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X * other.m_X;
|
||||
m_Y = m_Y * other.m_Y;
|
||||
m_Z = m_Z * other.m_Z;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides each component of the current FixPointVector3 by a given scalar value.
|
||||
/// </summary>
|
||||
/// <param name="divider">The scalar divider.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Divide(FixPoint16 divider)
|
||||
{
|
||||
#if DEBUG
|
||||
if (divider.IsZero())
|
||||
throw new("Error: divider is zero (FixPointVector3.Divide).");
|
||||
#endif
|
||||
unchecked
|
||||
{
|
||||
m_X = m_X / divider;
|
||||
m_Y = m_Y / divider;
|
||||
m_Z = m_Z / divider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the current FixPointVector3 by another FixPointVector3.
|
||||
/// </summary>
|
||||
/// <param name="other">The vector divider.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Divide(SFixPointVector3 other)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (!other.m_X.IsZero() && !other.m_Y.IsZero() && !other.m_Z.IsZero())
|
||||
{
|
||||
m_X = m_X / other.m_X;
|
||||
m_Y = m_Y / other.m_Y;
|
||||
m_Z = m_Z / other.m_Z;
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
throw new("Error: divider vector contains zero (FixPointVector3.Divide).");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a FixPointVector3.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static SFixPointVector3 Transform(SFixPointVector3 v, SFixPointQuaternionTransform t)
|
||||
{
|
||||
return Transform(v * t.m_Size, t.m_Orientation) + t.m_Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a vector by the given Quaternion rotation value.
|
||||
/// </summary>
|
||||
/// <param name="value">The source vector to be rotated.</param>
|
||||
/// <param name="rotation">The rotation to apply.</param>
|
||||
/// <returns>The transformed vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Transform(SFixPointVector3 value, SFixPointQuaternion rotation)
|
||||
{
|
||||
var x2 = rotation.m_X + rotation.m_X;
|
||||
var y2 = rotation.m_Y + rotation.m_Y;
|
||||
var z2 = rotation.m_Z + rotation.m_Z;
|
||||
|
||||
var wx2 = rotation.m_W * x2;
|
||||
var wy2 = rotation.m_W * y2;
|
||||
var wz2 = rotation.m_W * z2;
|
||||
var xx2 = rotation.m_X * x2;
|
||||
var xy2 = rotation.m_X * y2;
|
||||
var xz2 = rotation.m_X * z2;
|
||||
var yy2 = rotation.m_Y * y2;
|
||||
var yz2 = rotation.m_Y * z2;
|
||||
var zz2 = rotation.m_Z * z2;
|
||||
|
||||
return new((value.m_X * (1 - yy2 - zz2)) + (value.m_Y * (xy2 - wz2)) + (value.m_Z * (xz2 + wy2)), (value.m_X * (xy2 + wz2)) + (value.m_Y * (1 - xx2 - zz2)) + (value.m_Z * (yz2 - wx2)),
|
||||
(value.m_X * (xz2 - wy2)) + (value.m_Y * (yz2 + wx2)) + (value.m_Z * (1 - xx2 - yy2)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region -- public static methods --------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the length of the given vector.
|
||||
/// </summary>
|
||||
/// <returns>The Length of the given vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16 Length(SFixPointVector3 value)
|
||||
{
|
||||
return value.Length();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CompareLength(SFixPointVector3 value, FixPoint16 fLength)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((value.m_X.m_Value * (long)value.m_X.m_Value) + (value.m_Y.m_Value * (long)value.m_Y.m_Value) + (value.m_Z.m_Value * (long)value.m_Z.m_Value)).CompareTo(
|
||||
fLength.m_Value * (long)fLength.m_Value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CompareLength(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((value1.m_X.m_Value * (long)value1.m_X.m_Value) + (value1.m_Y.m_Value * (long)value1.m_Y.m_Value) + (value1.m_Z.m_Value * (long)value1.m_Z.m_Value)).CompareTo(
|
||||
(value2.m_X.m_Value * (long)value2.m_X.m_Value) + (value2.m_Y.m_Value * (long)value2.m_Y.m_Value) + (value2.m_Z.m_Value * (long)value2.m_Z.m_Value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the cross product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="vector1">The first vector.</param>
|
||||
/// <param name="vector2">The second vector.</param>
|
||||
/// <returns>The cross product.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Cross(SFixPointVector3 vector1, SFixPointVector3 vector2)
|
||||
{
|
||||
return new((vector1.m_Y * vector2.m_Z) - (vector1.m_Z * vector2.m_Y), (vector1.m_Z * vector2.m_X) - (vector1.m_X * vector2.m_Z),
|
||||
(vector1.m_X * vector2.m_Y) - (vector1.m_Y * vector2.m_X));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the distance between two vectors. (manhatten/taxi-cab metrix)
|
||||
/// </summary>
|
||||
/// <param name="left">The source vector</param>
|
||||
/// <param name="right">The source vector</param>
|
||||
/// <returns>Distance between the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16 DistanceManhattan(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return FixPoint16.Abs(left.m_X - right.m_X) + FixPoint16.Abs(left.m_Y - right.m_Y) + FixPoint16.Abs(left.m_Z - right.m_Z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the distance between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="left">The source vector</param>
|
||||
/// <param name="right">The source vector</param>
|
||||
/// <returns>Distance between the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16 Distance(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
var dx = left.m_X - right.m_X;
|
||||
var dy = left.m_Y - right.m_Y;
|
||||
var dz = left.m_Z - right.m_Z;
|
||||
return FixPoint16.Length(dx, dy, dz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the dot product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="left">The source vector.</param>
|
||||
/// <param name="right">The source vector.</param>
|
||||
/// <returns>The dot product of the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16 Dot(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return (left.m_X * right.m_X) + (left.m_Y * right.m_Y) + (left.m_Z * right.m_Z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the dot product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="left">The source vector.</param>
|
||||
/// <param name="right">The source vector.</param>
|
||||
/// <returns>The dot product of the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static FixPoint16Long DotLong(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return new() { m_Value = (left.m_X.m_Value * (long)right.m_X.m_Value) + (left.m_Y.m_Value * (long)right.m_Y.m_Value) + (left.m_Z.m_Value * (long)right.m_Z.m_Value) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the sign of the dot product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="left">The source vector.</param>
|
||||
/// <param name="right">The source vector.</param>
|
||||
/// <returns>The sign of the dot product of the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DotSign(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((left.m_X.m_Value * (long)right.m_X.m_Value) + (left.m_Y.m_Value * (long)right.m_Y.m_Value) + (left.m_Z.m_Value * (long)right.m_Z.m_Value)).CompareTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a unit vector from the specified vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The source vector.</param>
|
||||
/// <returns>The created unit vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Normalize(SFixPointVector3 value)
|
||||
{
|
||||
return value.Normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that contains the lowest value from each matching pair of components.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The source vector.</param>
|
||||
/// <returns>The minimized vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Min(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = FixPoint16.Min(value1.m_X, value2.m_X);
|
||||
result.m_Y = FixPoint16.Min(value1.m_Y, value2.m_Y);
|
||||
result.m_Z = FixPoint16.Min(value1.m_Z, value2.m_Z);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that contains the highest value from each matching pair of components.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The source vector.</param>
|
||||
/// <returns>The maximized vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Max(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = FixPoint16.Max(value1.m_X, value2.m_X);
|
||||
result.m_Y = FixPoint16.Max(value1.m_Y, value2.m_Y);
|
||||
result.m_Z = FixPoint16.Max(value1.m_Z, value2.m_Z);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector pointing in the opposite direction.
|
||||
/// </summary>
|
||||
/// <param name="value">The source vector.</param>
|
||||
/// <returns>A new vector pointing in the opposite direction.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Negate(SFixPointVector3 value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = -value.m_X;
|
||||
result.m_Y = -value.m_Y;
|
||||
result.m_Z = -value.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds two vectors
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The source vector.</param>
|
||||
/// <returns>A new vector representing the sum of the source vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Add(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X + value2.m_X;
|
||||
result.m_Y = value1.m_Y + value2.m_Y;
|
||||
result.m_Z = value1.m_Z + value2.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a given scalar value to each component of a given vector.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The scalar value to be added to the vector.</param>
|
||||
/// <returns>A new FixPointVector3 representing the sum of the given vector and scalar.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Add(SFixPointVector3 value1, FixPoint16 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X + value2;
|
||||
result.m_Y = value1.m_Y + value2;
|
||||
result.m_Z = value1.m_Z + value2;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts a vector from another vector.
|
||||
/// </summary>
|
||||
/// <param name="value1">The vector to be subtracted from.</param>
|
||||
/// <param name="value2">The vector to be subtracted.</param>
|
||||
/// <returns>A new vector representing the result of the subtraction.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Subtract(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X - value2.m_X;
|
||||
result.m_Y = value1.m_Y - value2.m_Y;
|
||||
result.m_Z = value1.m_Z - value2.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts a given scalar value from each component of a given vector.
|
||||
/// </summary>
|
||||
/// <param name="value1">The vector to be subtracted from.</param>
|
||||
/// <param name="value2">The scalar value to subtracted.</param>
|
||||
/// <returns>A new FixPointVector3 representing the result of the subtraction.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Subtract(SFixPointVector3 value1, FixPoint16 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X - value2;
|
||||
result.m_Y = value1.m_Y - value2;
|
||||
result.m_Z = value1.m_Z - value2;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies the components of two vectors by each other.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The source vector.</param>
|
||||
/// <returns>A new vector representing the result of the mulitiplication.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Multiply(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X * value2.m_X;
|
||||
result.m_Y = value1.m_Y * value2.m_Y;
|
||||
result.m_Z = value1.m_Z * value2.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a vector by a scalar value.
|
||||
/// </summary>
|
||||
/// <param name="value">The source vector.</param>
|
||||
/// <param name="scalar">The scalar value.</param>
|
||||
/// <returns>A new vector representing the result of the mulitiplication.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Multiply(SFixPointVector3 value, FixPoint16 scalar)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value.m_X * scalar;
|
||||
result.m_Y = value.m_Y * scalar;
|
||||
result.m_Z = value.m_Z * scalar;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the components of a vector by the components of another vector.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The divisor vector.</param>
|
||||
/// <returns>A new vector representing the result of the division.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Divide(SFixPointVector3 value1, SFixPointVector3 value2)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X / value2.m_X;
|
||||
result.m_Y = value1.m_Y / value2.m_Y;
|
||||
result.m_Z = value1.m_Z / value2.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects a vector onto another vector.
|
||||
/// </summary>
|
||||
/// <param name="projected">The projected vector.</param>
|
||||
/// <param name="projectionTarget">The vector the projected vector is being projected on.</param>
|
||||
/// <returns>A new vector representing the result of the division.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Project(SFixPointVector3 projected, SFixPointVector3 projectionTarget)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
var fpDot = Dot(projected, projectionTarget);
|
||||
var fpProjectionTargetLength = projectionTarget.Length();
|
||||
var fpScalar = fpDot / (fpProjectionTargetLength * fpProjectionTargetLength);
|
||||
result = projectionTarget * fpScalar;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides a vector by a scalar value.
|
||||
/// </summary>
|
||||
/// <param name="value">The source vector.</param>
|
||||
/// <param name="divider">The divider</param>
|
||||
/// <returns>A new vector representing the result of the division.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Divide(SFixPointVector3 value, FixPoint16 divider)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value.m_X / divider;
|
||||
result.m_Y = value.m_Y / divider;
|
||||
result.m_Z = value.m_Z / divider;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FixPointVector3 with each component being the result of dividing a scalar value by the corresponding
|
||||
/// component of a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The scalar value to be divided.</param>
|
||||
/// <param name="divider">The divider vector</param>
|
||||
/// <returns>
|
||||
/// A new vector with each component being the result of dividing the scalar value by the corresponding component
|
||||
/// of the vector.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Divide(FixPoint16 value, SFixPointVector3 divider)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value / divider.m_X;
|
||||
result.m_Y = value / divider.m_Y;
|
||||
result.m_Z = value / divider.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source vector.</param>
|
||||
/// <param name="value2">The source vector.</param>
|
||||
/// <param name="amount">
|
||||
/// The value between 0 and 1 indicating the weight of _value2. '0.0' will cause _value1 to be
|
||||
/// returned; '1.0' will cause _value2 to be returned.
|
||||
/// </param>
|
||||
/// <returns>The linear interpolation of the two vectors.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 Lerp(SFixPointVector3 value1, SFixPointVector3 value2, FixPoint16 amount)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X + (amount * (value2.m_X - value1.m_X));
|
||||
result.m_Y = value1.m_Y + (amount * (value2.m_Y - value1.m_Y));
|
||||
result.m_Z = value1.m_Z + (amount * (value2.m_Z - value1.m_Z));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interpolates between two vectors using a cubic equation.
|
||||
/// </summary>
|
||||
/// <param name="value1">The source value.</param>
|
||||
/// <param name="value2">The source value.</param>
|
||||
/// <param name="amount">The weighting value.</param>
|
||||
/// <returns>The interpolated value.</returns>
|
||||
public static SFixPointVector3 SmoothStep(SFixPointVector3 value1, SFixPointVector3 value2, FixPoint16 amount)
|
||||
{
|
||||
var smootstep = FixPoint16.Min(0, FixPoint16.Max(1, amount));
|
||||
smootstep = smootstep * smootstep * (3 - (2 * smootstep));
|
||||
return Lerp(value1, value2, smootstep);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a FixPointVector3 containing the 2D Cartesian coordinates of a point specified in barycentric (areal)
|
||||
/// coordinates relative to a 2D triangle.
|
||||
/// </summary>
|
||||
/// <param name="value1">A FixPointVector3 containing the 2D Cartesian coordinates of vertex 1 of the triangle.</param>
|
||||
/// <param name="value2">A FixPointVector3 containing the 2D Cartesian coordinates of vertex 2 of the triangle.</param>
|
||||
/// <param name="value3">A FixPointVector3 containing the 2D Cartesian coordinates of vertex 3 of the triangle.</param>
|
||||
/// <param name="amount1">
|
||||
/// Barycentric coordinate b2, which expresses the weighting factor toward vertex 2 (specified in
|
||||
/// _value2).
|
||||
/// </param>
|
||||
/// <param name="amount2">
|
||||
/// Barycentric coordinate b3, which expresses the weighting factor toward vertex 3 (specified in
|
||||
/// _value3).
|
||||
/// </param>
|
||||
/// <returns>A new FixPointVector3 containing the 2D Cartesian coordinates of the specified point.</returns>
|
||||
public static SFixPointVector3 Barycentric(SFixPointVector3 value1, SFixPointVector3 value2, SFixPointVector3 value3, FixPoint16 amount1, FixPoint16 amount2)
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = value1.m_X + (amount1 * (value2.m_X - value1.m_X)) + (amount2 * (value3.m_X - value1.m_X));
|
||||
result.m_Y = value1.m_Y + (amount1 * (value2.m_Y - value1.m_Y)) + (amount2 * (value3.m_Y - value1.m_Y));
|
||||
result.m_Z = value1.m_Z + (amount1 * (value2.m_Z - value1.m_Z)) + (amount2 * (value3.m_Z - value1.m_Z));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static SFixPointVector3 Fract(SFixPointVector3 p)
|
||||
{
|
||||
return new(FixPoint16.Fract(p.m_X), FixPoint16.Fract(p.m_Y), FixPoint16.Fract(p.m_Z));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region -- operators --------------------------------------------------
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator -(SFixPointVector3 value)
|
||||
{
|
||||
return Negate(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator +(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = left.m_X + right.m_X;
|
||||
result.m_Y = left.m_Y + right.m_Y;
|
||||
result.m_Z = left.m_Z + right.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator +(SFixPointVector3 left, FixPoint16 right)
|
||||
{
|
||||
return Add(left, right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator -(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
SFixPointVector3 result;
|
||||
result.m_X = left.m_X - right.m_X;
|
||||
result.m_Y = left.m_Y - right.m_Y;
|
||||
result.m_Z = left.m_Z - right.m_Z;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator -(SFixPointVector3 left, FixPoint16 right)
|
||||
{
|
||||
return Subtract(left, right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator *(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return Multiply(left, right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator *(SFixPointVector3 left, FixPoint16 right)
|
||||
{
|
||||
return new() {
|
||||
m_X = left.m_X * right,
|
||||
m_Y = left.m_Y * right,
|
||||
m_Z = left.m_Z * right
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator *(FixPoint16 left, SFixPointVector3 right)
|
||||
{
|
||||
return Multiply(right, left);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator /(SFixPointVector3 left, SFixPointVector3 right)
|
||||
{
|
||||
return Divide(left, right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator /(SFixPointVector3 left, FixPoint16 right)
|
||||
{
|
||||
return new() {
|
||||
m_X = left.m_X / right,
|
||||
m_Y = left.m_Y / right,
|
||||
m_Z = left.m_Z / right
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SFixPointVector3 operator /(FixPoint16 left, SFixPointVector3 right)
|
||||
{
|
||||
return Divide(left, right);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The x-component of the vector.
|
||||
/// </summary>
|
||||
public FixPoint16 m_X;
|
||||
|
||||
/// <summary>
|
||||
/// The y-component of the vector.
|
||||
/// </summary>
|
||||
public FixPoint16 m_Y;
|
||||
|
||||
/// <summary>
|
||||
/// The z-component of the vector.
|
||||
/// </summary>
|
||||
public FixPoint16 m_Z;
|
||||
}
|
||||
1
FixPoint/FixPointVector3.cs.uid
Normal file
1
FixPoint/FixPointVector3.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dci3uksrcv47
|
||||
119
FixPoint/IntRandom.cs
Normal file
119
FixPoint/IntRandom.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
|
||||
namespace MagmaEngine.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a XorShift* PRNG, with 64 bits of internal state.
|
||||
/// See http://en.wikipedia.org/wiki/Xorshift
|
||||
/// </summary>
|
||||
public struct SIntRandom
|
||||
{
|
||||
public SIntRandom(ulong seed)
|
||||
{
|
||||
if (seed == 0)
|
||||
throw new InvalidOperationException("Seed needs to be bigger than zero.");
|
||||
|
||||
m_Seed = seed;
|
||||
}
|
||||
|
||||
public ulong Next()
|
||||
{
|
||||
m_Seed ^= m_Seed >> 12;
|
||||
m_Seed ^= m_Seed << 25;
|
||||
m_Seed ^= m_Seed >> 27;
|
||||
return m_Seed * 2685821657736338717UL; // multiplier taken from wikipedia article on XorShift PRNGs
|
||||
}
|
||||
|
||||
public ulong Next(ulong upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Next() % upperLimit;
|
||||
}
|
||||
|
||||
public FixPoint16 NextFixPoint16()
|
||||
{
|
||||
return new() { m_Value = (int)(Next() & 0xffffUL) };
|
||||
}
|
||||
|
||||
public int RandomizedRound(FixPoint16 value)
|
||||
{
|
||||
int ret = value.ToIntFloor();
|
||||
if (NextFixPoint16() < FixPoint16.Fract(value))
|
||||
ret++;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public uint Next(uint upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (uint)(Next() % upperLimit);
|
||||
}
|
||||
|
||||
public uint Next(uint lowerLimit, uint upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lowerLimit == upperLimit)
|
||||
{
|
||||
return lowerLimit;
|
||||
}
|
||||
|
||||
return lowerLimit + (uint)(Next() % (upperLimit - lowerLimit));
|
||||
}
|
||||
|
||||
public int Next(int upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)(Next() % (uint)(upperLimit & 0x7fffffff));
|
||||
}
|
||||
|
||||
public int Next(int lowerLimit, int upperLimit)
|
||||
{
|
||||
if (upperLimit == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lowerLimit == upperLimit)
|
||||
{
|
||||
return lowerLimit;
|
||||
}
|
||||
|
||||
return lowerLimit + (int)(Next() % (uint)((upperLimit - lowerLimit) & 0x7fffffff));
|
||||
}
|
||||
|
||||
public double NextDouble()
|
||||
{
|
||||
return Next(int.MaxValue) * (1.0 / int.MaxValue);
|
||||
}
|
||||
|
||||
public float NextSingle()
|
||||
{
|
||||
return (float)NextDouble();
|
||||
}
|
||||
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"0x{m_Seed:X}";
|
||||
}
|
||||
|
||||
public readonly ulong Seed => m_Seed;
|
||||
|
||||
private ulong m_Seed;
|
||||
}
|
||||
19
SideScrollerGame.sln
Normal file
19
SideScrollerGame.sln
Normal file
@@ -0,0 +1,19 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SideScrollerGame.Godot", "godot/SideScrollerGame.Godot.csproj", "{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{75DE3F78-FF5C-4E58-8315-8AEF5BF95BBA}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
2
godot.cmd
Normal file
2
godot.cmd
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
"D:\Code\Godot_v4.5.1-stable_mono_win64\Godot_v4.5.1-stable_mono_win64_console.exe" %*
|
||||
4
godot/.editorconfig
Normal file
4
godot/.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
7
godot/SideScrollerGame.Godot.csproj
Normal file
7
godot/SideScrollerGame.Godot.csproj
Normal file
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.5.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net9.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
1
godot/icon.svg
Normal file
1
godot/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
43
godot/icon.svg.import
Normal file
43
godot/icon.svg.import
Normal file
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cvsoq88dy3wvp"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
19
godot/project.godot
Normal file
19
godot/project.godot
Normal file
@@ -0,0 +1,19 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="SideScrollerGame"
|
||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="SideScrollerGame"
|
||||
651
groundwork.md
Normal file
651
groundwork.md
Normal file
@@ -0,0 +1,651 @@
|
||||
# Side Scroller Shooter Groundwork
|
||||
|
||||
## Goal
|
||||
This document defines the technical foundation for a classic 2D side scroller shooter built with Godot 4 and C#. The framework must support the whole genre family, not a single game design: player movement, weapons, enemies, bosses, pickups, scrolling levels, scripted encounters, layered music, overlapping sound effects, and strong debugging tools. Final game-specific rules and content will be built later on top of this groundwork.
|
||||
|
||||
The non-negotiable constraint is that **all authoritative gameplay simulation lives in one pure .NET project with no Godot dependency**. Godot is the host for rendering, input capture, authoring UX, audio playback, and debug tooling. The same simulation assembly is referenced by both xUnit tests and the Godot host so all gameplay logic can be verified without engine or multimedia interference.
|
||||
|
||||
## Hard Constraints
|
||||
1. **Single pure simulation project**. Do not split runtime gameplay and content definitions into separate assemblies unless a real dependency problem appears later.
|
||||
2. **Deterministic fixed-step simulation**. The same content, seed, and input action stream must produce the same state and hashes every run.
|
||||
3. **Serializable input actions**. The simulation consumes serializable action batches so recorded runs can be replayed exactly.
|
||||
4. **Serializable full state**. The entire simulation state must be serializable for save/load, replay checkpoints, and desync verification.
|
||||
5. **Thin Godot host**. Godot must not become gameplay authority for physics, damage, AI, triggers, or progression.
|
||||
6. **xUnit-first verification**. The simulation project is expected to reach 100% gameplay coverage; exclusions are acceptable only for clearly non-logic code such as generated tables or serializer boilerplate.
|
||||
|
||||
## Repository Layout
|
||||
The Godot project should **not** live at repository root. With SDK-style .NET projects, a root-level Godot `.csproj` will glob in files from subfolders, which makes it too easy to accidentally compile tests and simulation sources into the Godot assembly.
|
||||
|
||||
Recommended layout:
|
||||
|
||||
```text
|
||||
/SideScrollerGame.sln
|
||||
/src/SideScrollerGame.Sim/
|
||||
SideScrollerGame.Sim.csproj
|
||||
/FixPoint/ # preferred final home
|
||||
/Definitions/
|
||||
/Runtime/
|
||||
/Systems/
|
||||
/Serialization/
|
||||
/Replay/
|
||||
/Verification/
|
||||
/tests/SideScrollerGame.Sim.Tests/
|
||||
SideScrollerGame.Sim.Tests.csproj
|
||||
/godot/
|
||||
project.godot
|
||||
SideScrollerGame.Godot.csproj
|
||||
/.godot/
|
||||
/scenes/
|
||||
/scripts/host/
|
||||
/assets/
|
||||
/addons/
|
||||
/content/compiled/
|
||||
/tools/
|
||||
```
|
||||
|
||||
Current repository note:
|
||||
- The existing root `FixPoint/` folder is acceptable during bootstrap.
|
||||
- The preferred end state is to move it under `src/SideScrollerGame.Sim/FixPoint/` and adjust namespaces once the simulation project is created.
|
||||
- The Godot project should be moved under `/godot` before the real solution structure grows.
|
||||
|
||||
Dependency rules:
|
||||
|
||||
```text
|
||||
Godot Host -> SideScrollerGame.Sim
|
||||
Tests -> SideScrollerGame.Sim
|
||||
Sim -> no Godot references
|
||||
```
|
||||
|
||||
Within the simulation project, separate concerns by namespaces and folders, not by extra assemblies:
|
||||
- `SideScrollerGame.Sim.Math`
|
||||
- `SideScrollerGame.Sim.Definitions`
|
||||
- `SideScrollerGame.Sim.Runtime`
|
||||
- `SideScrollerGame.Sim.Systems`
|
||||
- `SideScrollerGame.Sim.Serialization`
|
||||
- `SideScrollerGame.Sim.Replay`
|
||||
- `SideScrollerGame.Sim.Verification`
|
||||
|
||||
## Assembly Responsibilities
|
||||
### `SideScrollerGame.Sim`
|
||||
- fixed-step world simulation
|
||||
- immutable gameplay and level definitions
|
||||
- deterministic numeric types and RNG
|
||||
- physics and collision
|
||||
- AI and behavior execution
|
||||
- input action ingestion
|
||||
- save/load serialization
|
||||
- replay support
|
||||
- debug hashes and verification hooks
|
||||
|
||||
### `SideScrollerGame.Godot`
|
||||
- editor bootstrapping
|
||||
- converting physical input into simulation actions
|
||||
- fixed-step runner and interpolation
|
||||
- rendering and presentation node lifecycle
|
||||
- animation mapping
|
||||
- audio playback, music queue, and cross-fading
|
||||
- authoring and validation tools
|
||||
- development-time timeline controls
|
||||
|
||||
## Simulation Boundaries
|
||||
The simulation owns:
|
||||
- world state
|
||||
- movement and collision
|
||||
- weapon firing and ammo
|
||||
- enemy behavior
|
||||
- spawn logic
|
||||
- damage, death, score, drops, checkpoints
|
||||
- scripted triggers
|
||||
- camera gameplay rules
|
||||
- music state requests
|
||||
|
||||
The Godot host owns:
|
||||
- drawing sprites, meshes, parallax, particles, and UI
|
||||
- collecting OS input and converting it to actions
|
||||
- audio stream playback and mixing
|
||||
- interpolation between snapshots
|
||||
- editor tooling and authoring UX
|
||||
- developer overlays and transport controls
|
||||
|
||||
Godot physics, animation trees, collision layers, and timers must never become authoritative for gameplay.
|
||||
|
||||
## Deterministic Simulation Model
|
||||
Use a fixed rate of **60 simulation ticks per second**. This is appropriate for classic side scroller shooters and is easy to reason about. Rendering can run faster or slower; simulation must not.
|
||||
|
||||
Determinism rules:
|
||||
- no floating-point math in simulation logic
|
||||
- no use of `DateTime`, wall clock, threads, async work, GUIDs, or nondeterministic iteration order
|
||||
- stable entity iteration order
|
||||
- immutable definitions after load
|
||||
- deterministic RNG with explicit persisted state
|
||||
- all simulation outputs derived only from definitions, current state, seed, and action stream
|
||||
|
||||
Numerics:
|
||||
- use fixed-point math throughout the simulation
|
||||
- represent position, velocity, and acceleration in deterministic units
|
||||
- keep map sizes and tile sizes as integer values
|
||||
|
||||
The current `FixPoint/` folder should be treated as simulation infrastructure, not Godot host code.
|
||||
|
||||
## Simulation API
|
||||
The simulation should expose a narrow runner API that is safe for tests, replays, and Godot hosting:
|
||||
|
||||
```csharp
|
||||
public sealed class Simulation
|
||||
{
|
||||
public Simulation(GameDefinition gameDefinition, SimulationConfig config, int seed);
|
||||
|
||||
public int CurrentTick { get; }
|
||||
public SimulationState CurrentState { get; }
|
||||
public WorldSnapshot PreviousSnapshot { get; }
|
||||
public WorldSnapshot CurrentSnapshot { get; }
|
||||
|
||||
public TickResult Step(in TickActionBatch actions);
|
||||
|
||||
public byte[] SaveState();
|
||||
public static Simulation LoadState(byte[] data, GameDefinition gameDefinition, SimulationConfig config);
|
||||
}
|
||||
```
|
||||
|
||||
`TickResult` should contain:
|
||||
- gameplay events
|
||||
- presentation events
|
||||
- sound requests
|
||||
- music requests
|
||||
- spawn/despawn notifications
|
||||
- per-tick debug hash
|
||||
- optional verification report
|
||||
|
||||
## Input Model and Replay Format
|
||||
The simulation should receive user intent through serializable actions, not direct polling state.
|
||||
|
||||
Recommended approach:
|
||||
- Godot host converts raw keyboard/gamepad input into `SimulationAction` records
|
||||
- each tick receives a `TickActionBatch`
|
||||
- continuous controls are handled by edge-triggered actions plus persistent input state inside the simulation
|
||||
|
||||
Example action model:
|
||||
|
||||
```csharp
|
||||
public abstract record SimulationAction;
|
||||
|
||||
public sealed record MoveAxisChanged(PlayerId PlayerId, sbyte X, sbyte Y) : SimulationAction;
|
||||
public sealed record AimAxisChanged(PlayerId PlayerId, short X, short Y) : SimulationAction;
|
||||
public sealed record ButtonChanged(PlayerId PlayerId, InputButton Button, bool IsPressed) : SimulationAction;
|
||||
public sealed record WeaponSlotSelected(PlayerId PlayerId, int SlotIndex) : SimulationAction;
|
||||
|
||||
public sealed record TickActionBatch(int Tick, ImmutableArray<SimulationAction> Actions);
|
||||
```
|
||||
|
||||
Why this matters:
|
||||
- the exact action stream can be recorded to disk
|
||||
- bug reports can attach a replay instead of describing a scenario
|
||||
- tests can load recorded runs and verify hashes
|
||||
- stepping to tick `N` is just replay plus optional checkpoint restore
|
||||
|
||||
Replay file contents:
|
||||
- content hash or version id
|
||||
- simulation config
|
||||
- seed
|
||||
- initial level/start condition
|
||||
- per-tick action batches
|
||||
- optional periodic serialized state checkpoints
|
||||
|
||||
## Full State Serialization
|
||||
The entire simulation state must be serializable. This is not just for player save games. It is also required for:
|
||||
- replay checkpoints
|
||||
- fast-forward and jump-to-tick tooling
|
||||
- save/load correctness verification
|
||||
- deterministic debugging
|
||||
|
||||
`SimulationState` should include:
|
||||
- current tick
|
||||
- active level and checkpoint
|
||||
- all runtime entities and subsystem state
|
||||
- timers and cooldowns
|
||||
- RNG state
|
||||
- squad and flock coordination state
|
||||
- scripted trigger state
|
||||
- camera gameplay state
|
||||
- pending spawn queues
|
||||
- music/gameplay progression state
|
||||
- player input hold state
|
||||
|
||||
Serialization guidance:
|
||||
- use an explicit versioned serializer owned by the simulation project
|
||||
- avoid relying on Godot serialization
|
||||
- keep format deterministic and stable
|
||||
- support both compact runtime format and optional debug-friendly dump format if useful
|
||||
|
||||
## Built-In Desync Verification
|
||||
During development, each step should optionally verify save/load correctness and step determinism by round-tripping a clone.
|
||||
|
||||
Recommended `VerificationMode`:
|
||||
- `None`
|
||||
- `RoundTripState`
|
||||
- `RoundTripAndStepClone`
|
||||
|
||||
`RoundTripAndStepClone` algorithm for each tick:
|
||||
1. serialize the live simulation
|
||||
2. deserialize a clone from those bytes
|
||||
3. compare live and clone state hashes or normalized snapshots
|
||||
4. apply the same `TickActionBatch` to both instances
|
||||
5. compare resulting states again
|
||||
6. report any mismatch with tick number, subsystem, and serialized artifacts
|
||||
|
||||
This catches two different problems:
|
||||
- **save/load desync**: serialization fails to reconstruct the same state
|
||||
- **step desync**: cloned and live instances diverge when stepping the same input
|
||||
|
||||
This mode is expensive and should be optional, but it is exactly the right tool for early framework development.
|
||||
|
||||
## Runtime Debug Time Controls
|
||||
Development UX must support stepping and replay inspection from day one.
|
||||
|
||||
Required transport controls:
|
||||
- `Restart`
|
||||
- `Play/Pause`
|
||||
- `Advance One Step`
|
||||
- `Fast Forward To Step X`
|
||||
|
||||
Recommended supporting features:
|
||||
- current tick display
|
||||
- current replay seed
|
||||
- state hash display
|
||||
- verification on/off toggle
|
||||
- playback speed multiplier
|
||||
- jump to checkpoint
|
||||
- scrub to nearest saved checkpoint, then replay to requested tick
|
||||
|
||||
Implementation guidance:
|
||||
- maintain a `RunController` in the Godot host
|
||||
- cache serialized checkpoints every `N` ticks to make fast-forward practical
|
||||
- treat restart as reloading the initial seed, content, and replay
|
||||
- do not let presentation-only pause state alter simulation state
|
||||
|
||||
## World Representation
|
||||
Avoid a generic ECS. A domain-specific, data-oriented model is a better fit for this genre.
|
||||
|
||||
Recommended runtime state groups:
|
||||
- `PlayerState`
|
||||
- `EnemyState`
|
||||
- `ProjectileState`
|
||||
- `PickupState`
|
||||
- `HazardState`
|
||||
- `PlatformState`
|
||||
- `TriggerState`
|
||||
- `CameraState`
|
||||
- `LevelRuntimeState`
|
||||
- `SquadState`
|
||||
|
||||
Cross-cutting concepts:
|
||||
- `EntityId`
|
||||
- faction/team
|
||||
- transform
|
||||
- velocity
|
||||
- health and armor
|
||||
- hurtboxes and hitboxes
|
||||
- timers and modifiers
|
||||
- presentation state keys
|
||||
|
||||
Keep definitions immutable and runtime state mutable.
|
||||
|
||||
## Fixed Tick Pipeline
|
||||
Each tick should run in a fixed order:
|
||||
|
||||
1. apply incoming actions to player input state
|
||||
2. advance timers, cooldowns, modifiers, and scripted sequences
|
||||
3. update AI, squad logic, and flocking decisions
|
||||
4. resolve weapon intents and spawn projectiles/effects
|
||||
5. integrate movement
|
||||
6. resolve map, platform, and environment collision
|
||||
7. resolve projectile, melee, and contact hits
|
||||
8. apply damage, knockback, deaths, drops, score, and checkpoint changes
|
||||
9. process triggers, camera rules, wave progression, and music state transitions
|
||||
10. build events, snapshots, and hashes
|
||||
|
||||
This order must remain explicit and stable. New systems should be inserted deliberately, not opportunistically.
|
||||
|
||||
## Physics and Collision
|
||||
The simulation owns all gameplay physics.
|
||||
|
||||
Recommended model:
|
||||
- 2D kinematic actors
|
||||
- deterministic AABB-based collision for actors
|
||||
- tile-grid collision for level geometry
|
||||
- one-way platforms
|
||||
- ladders and climb volumes
|
||||
- hazard and water zones
|
||||
- deterministic moving platforms
|
||||
- sweep or raycast projectile movement to avoid tunneling
|
||||
|
||||
Movement design targets:
|
||||
- grounded movement
|
||||
- coyote time and jump buffering
|
||||
- crouch and stance-dependent hitboxes
|
||||
- knockback and stun
|
||||
- moving platform carry behavior
|
||||
- optional slopes later if needed
|
||||
|
||||
Do not use Godot rigid bodies as gameplay authority.
|
||||
|
||||
## Frame Interpolation
|
||||
Rendering should interpolate between two authoritative simulation snapshots.
|
||||
|
||||
Godot host loop:
|
||||
1. accumulate real frame time
|
||||
2. run zero or more fixed simulation steps
|
||||
3. compute interpolation alpha
|
||||
4. render between `PreviousSnapshot` and `CurrentSnapshot`
|
||||
|
||||
Interpolate:
|
||||
- position
|
||||
- aim direction
|
||||
- camera anchor
|
||||
- recoil offsets
|
||||
|
||||
Do not interpolate:
|
||||
- deaths
|
||||
- pickups
|
||||
- state machine transitions
|
||||
- damage flashes
|
||||
- spawn and despawn boundaries
|
||||
|
||||
Presentation nodes should be mapped by `EntityId` and recreated only on tick boundaries.
|
||||
|
||||
## Animation Model
|
||||
The simulation should emit semantic animation state, not Godot clip names.
|
||||
|
||||
Example semantic outputs:
|
||||
- locomotion: `Idle`, `Run`, `JumpRise`, `JumpFall`, `Land`, `Crouch`, `Climb`
|
||||
- combat: `Fire`, `Reload`, `Charge`, `Melee`, `Overheat`
|
||||
- damage: `Hurt`, `Invulnerable`, `Dead`
|
||||
- modifiers: facing, aim sector, grounded, wet, frozen, heavy_weapon
|
||||
|
||||
The Godot host maps semantic state to `AnimationTree`, sprite animations, blend spaces, or custom timelines.
|
||||
|
||||
## Audio Architecture
|
||||
The simulation emits logical audio requests; the Godot host performs actual playback.
|
||||
|
||||
### Overlapping Sound Effects
|
||||
Simulation should emit `SoundEvent` data:
|
||||
- cue id
|
||||
- source entity or world position
|
||||
- category/bus
|
||||
- priority
|
||||
- optional pitch/variant seed
|
||||
|
||||
The Godot host should:
|
||||
- pool multiple audio players
|
||||
- allow overlapping playback for repeated weapons and explosions
|
||||
- cap voices per cue and bus
|
||||
- support both positional and UI sounds
|
||||
|
||||
### Cross-Fading Music Queue
|
||||
The simulation should emit logical music requests, not manipulate players directly.
|
||||
|
||||
Recommended requests:
|
||||
- `Queue(trackId)`
|
||||
- `Replace(trackId, fadeOutTicks, fadeInTicks)`
|
||||
- `PushPriority(trackId)`
|
||||
- `PopPriority(trackId)`
|
||||
- `Stop(fadeOutTicks)`
|
||||
|
||||
The Godot host should implement a `MusicDirector` with:
|
||||
- at least two music players for cross-fading
|
||||
- support for intro, loop, and optional outro segments
|
||||
- deterministic transitions triggered from simulation ticks
|
||||
- queue inspection in the debug UI
|
||||
|
||||
## Gameplay Definitions
|
||||
Definitions live inside the simulation project under `Definitions/` and are loaded from compiled engine-agnostic data.
|
||||
|
||||
### HeroDefinition
|
||||
Include:
|
||||
- id, tags, display info
|
||||
- collider and stance shapes
|
||||
- health, armor, lives, invulnerability ticks
|
||||
- movement profile
|
||||
- weapon loadout
|
||||
- pickup interaction rules
|
||||
- animation profile id
|
||||
- sound profile id
|
||||
|
||||
### EnemyDefinition
|
||||
Include:
|
||||
- id, archetype, faction, tags
|
||||
- collider and hurtboxes
|
||||
- health and contact damage
|
||||
- movement profile
|
||||
- behavior definition id
|
||||
- squad role support
|
||||
- weapon set
|
||||
- score reward and drop table
|
||||
- boss phase data when needed
|
||||
|
||||
### WeaponDefinition
|
||||
Include:
|
||||
- slot type
|
||||
- fire mode
|
||||
- cadence and burst settings
|
||||
- ammo and reload rules
|
||||
- muzzle offsets
|
||||
- projectile or hitscan profile
|
||||
- recoil, spread, and status effects
|
||||
- animation and audio ids
|
||||
|
||||
### ProjectileDefinition
|
||||
Include:
|
||||
- speed and lifetime
|
||||
- gravity scale
|
||||
- collider
|
||||
- collision mask
|
||||
- pierce behavior
|
||||
- hit response
|
||||
- damage payload
|
||||
- explosion or follow-up spawn ids
|
||||
|
||||
### PowerUpDefinition
|
||||
Include:
|
||||
- pickup type
|
||||
- effect payload
|
||||
- duration
|
||||
- stacking policy
|
||||
- despawn rules
|
||||
- magnet behavior
|
||||
- audio and presentation ids
|
||||
|
||||
### Modifier Model
|
||||
Use a shared modifier system for buffs and debuffs:
|
||||
- additive and multiplicative stat changes
|
||||
- capability flags
|
||||
- timed effects
|
||||
- stack limits
|
||||
- refresh behavior
|
||||
|
||||
## Behavior Framework
|
||||
Use a data-driven hierarchical state machine model with reusable sensors and actions. Do not rely on Godot scripts for enemy logic.
|
||||
|
||||
Reusable sensors:
|
||||
- target in range
|
||||
- line of sight
|
||||
- ground ahead
|
||||
- wall ahead
|
||||
- recently damaged
|
||||
- timer elapsed
|
||||
- squad signal received
|
||||
|
||||
Reusable actions:
|
||||
- move
|
||||
- stop
|
||||
- jump
|
||||
- fire
|
||||
- dodge
|
||||
- retreat
|
||||
- strafe
|
||||
- hover
|
||||
- call squad action
|
||||
|
||||
### Squadrons and Flocking
|
||||
The behavior framework must also support coordinated enemies, not just isolated actors.
|
||||
|
||||
Required concepts:
|
||||
- `SquadDefinition`
|
||||
- `SquadState`
|
||||
- leader/follower roles
|
||||
- formation anchor and slots
|
||||
- shared target selection
|
||||
- squad orders such as attack, regroup, retreat, flank
|
||||
- separation, cohesion, and alignment style flocking for flyers or swarms
|
||||
|
||||
Use flocking as a tunable subsystem, not a hard-coded enemy type. The simulation should support:
|
||||
- loose flying swarms
|
||||
- tight escort formations
|
||||
- attack waves that break formation temporarily
|
||||
- rejoin logic after disruption
|
||||
|
||||
## Level Data Structure
|
||||
Levels must support both authored geometry and deterministic encounter scripting.
|
||||
|
||||
Recommended `LevelDefinition`:
|
||||
- metadata and dimensions
|
||||
- tile layers and collision/material map
|
||||
- spawn markers
|
||||
- pickups and hazards
|
||||
- moving platforms and paths
|
||||
- checkpoints
|
||||
- trigger zones
|
||||
- encounter and wave definitions
|
||||
- camera zones and locks
|
||||
- music zones and scripted transitions
|
||||
- scripted event graph
|
||||
|
||||
Keep authored Godot scenes as input to a compile/export step. The simulation and tests should run on compiled runtime data, not raw scene trees.
|
||||
|
||||
## Level Editing UX in Godot
|
||||
Godot should be used as the authoring front-end, but authored scenes must compile into engine-agnostic runtime definitions.
|
||||
|
||||
Recommended workflow:
|
||||
1. author level scenes in Godot
|
||||
2. place custom marker nodes for spawns, triggers, checkpoints, patrol paths, squad anchors, and music zones
|
||||
3. validate the authoring scene
|
||||
4. compile to runtime content under `content/compiled/`
|
||||
5. run game and tests against compiled data
|
||||
|
||||
Required authoring UX:
|
||||
- custom inspector editors for definitions and references
|
||||
- gizmos for triggers, camera bounds, paths, and squad formations
|
||||
- validation panel for bad ids, overlapping markers, broken references, and invalid encounter setups
|
||||
- buttons for `Validate`, `Compile`, and `Play From Here`
|
||||
- preview of wave timing, camera rules, and music transitions where practical
|
||||
|
||||
## Content Pipeline
|
||||
There is still a distinction between authoring content and runtime content, but it does not justify a separate assembly.
|
||||
|
||||
Authoring content:
|
||||
- Godot scenes and resources
|
||||
- marker nodes and editor-only metadata
|
||||
|
||||
Compiled runtime content:
|
||||
- engine-agnostic serialized definitions
|
||||
- loaded by the simulation project
|
||||
- used identically by tests and the Godot host
|
||||
|
||||
Compilation should:
|
||||
- resolve references
|
||||
- validate identifiers and links
|
||||
- flatten authored data into deterministic DTOs
|
||||
- assign stable content hashes
|
||||
- produce clear compile errors
|
||||
|
||||
## Save/Load, Replay, and Debugging
|
||||
These three systems should be designed together.
|
||||
|
||||
### Save/Load
|
||||
Save files should contain:
|
||||
- content hash/version
|
||||
- serialized `SimulationState`
|
||||
- optional metadata such as timestamp or user-visible slot info
|
||||
|
||||
### Replay
|
||||
Replay files should contain:
|
||||
- content hash/version
|
||||
- simulation config
|
||||
- seed
|
||||
- initial start state or entry point
|
||||
- action batches per tick
|
||||
- optional periodic state checkpoints
|
||||
|
||||
### Debugging
|
||||
A bug report should ideally include:
|
||||
- replay file
|
||||
- final tick number
|
||||
- expected vs actual state hash
|
||||
- verification failure artifacts if present
|
||||
|
||||
This allows deterministic reproduction without stepping through engine code.
|
||||
|
||||
## Testing Strategy
|
||||
Use **xUnit** for all automated simulation tests.
|
||||
|
||||
Test categories:
|
||||
- unit tests for movement, collision, damage, weapons, modifiers, and trigger logic
|
||||
- scenario tests for encounters and level scripts
|
||||
- replay tests using recorded action streams
|
||||
- serialization roundtrip tests for full simulation state
|
||||
- desync verification tests for clone-step comparison
|
||||
- property or fuzz tests for invariants where useful
|
||||
|
||||
Coverage expectations:
|
||||
- target 100% line and branch coverage for gameplay logic in `SideScrollerGame.Sim`
|
||||
- allow narrow exclusions only for generated lookup tables, source-generated serializers, or similar non-logic infrastructure
|
||||
- do not use exclusions to hide untested gameplay systems
|
||||
|
||||
Recommended tooling:
|
||||
- xUnit
|
||||
- coverlet
|
||||
- deterministic test fixtures and builders
|
||||
|
||||
## Implementation Phases
|
||||
### Phase 1: Project Restructure
|
||||
- move the Godot project under `/godot`
|
||||
- create `src/SideScrollerGame.Sim`
|
||||
- create `tests/SideScrollerGame.Sim.Tests`
|
||||
- temporarily link the existing root `FixPoint/` sources into the simulation project if needed
|
||||
|
||||
### Phase 2: Simulation Foundation
|
||||
- integrate fixed-point numerics and deterministic RNG
|
||||
- define `SimulationState`, snapshots, ids, events, and core step runner
|
||||
- define serialization contracts
|
||||
|
||||
### Phase 3: Replay and Verification
|
||||
- define serializable action model
|
||||
- implement replay recording and playback
|
||||
- implement per-step roundtrip and clone-step verification
|
||||
- add debug hashes
|
||||
|
||||
### Phase 4: Movement and Collision
|
||||
- implement actor movement, platforms, ladders, hazards, and projectile sweeps
|
||||
- build exhaustive xUnit coverage for collision edge cases
|
||||
|
||||
### Phase 5: Combat and Behaviors
|
||||
- add hero, enemy, weapon, projectile, powerup, and modifier definitions
|
||||
- implement behavior graphs, squads, flocking, and boss phase support
|
||||
|
||||
### Phase 6: Levels and Authoring
|
||||
- implement level definitions, triggers, checkpoints, and scripted graph
|
||||
- build Godot authoring nodes, validators, and compile/export flow
|
||||
|
||||
### Phase 7: Presentation Host
|
||||
- add interpolation, presenter registry, animation mapping, audio playback, and music director
|
||||
- add runtime transport controls and replay inspection UI
|
||||
|
||||
## Key Decisions to Hold
|
||||
- keep all gameplay authority in the pure simulation project
|
||||
- keep content definitions in that same project unless a real dependency issue emerges
|
||||
- keep the Godot host in its own subfolder and project
|
||||
- keep replay, save/load, and verification as first-class features
|
||||
- keep fixed-point determinism as the baseline
|
||||
- keep the debug transport controls available throughout development
|
||||
|
||||
If these constraints are maintained, the framework will remain flexible enough for later game design work without collapsing into engine-driven logic or untestable behavior.
|
||||
Reference in New Issue
Block a user