From a9485475c7583a626801e7ae15550d34bd02f8dd Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 23 Jan 2024 18:39:25 +0300 Subject: [PATCH] feat: Initial Physics Code From Previous Repo --- .gitignore | 484 ++++++++++++++++++ Engine.Physics2D/Abstract/ICollider2D.cs | 19 + .../Abstract/ICollisionResolver2D.cs | 6 + Engine.Physics2D/Abstract/IPhysicsEngine2D.cs | 11 + .../Abstract/IPhysicsMaterial2D.cs | 7 + Engine.Physics2D/Abstract/IRigidBody2D.cs | 14 + Engine.Physics2D/Collider2DAABBBehaviour.cs | 67 +++ Engine.Physics2D/CollisionInformation.cs | 9 + Engine.Physics2D/Engine.Physics2D.csproj | 13 + Engine.Physics2D/PhysicsEngine2D.cs | 75 +++ Engine.Physics2D/PhysicsMaterial2D.cs | 5 + Engine.Physics2D/PhysicsMaterial2DDefault.cs | 6 + Engine.Physics2D/PhysicsMath.cs | 118 +++++ Engine.Physics2D/Primitives/AABB.cs | 17 + Engine.Physics2D/Primitives/Circle.cs | 18 + Engine.Physics2D/Primitives/Line.cs | 146 ++++++ Engine.Physics2D/Primitives/LineEquation.cs | 8 + Engine.Physics2D/Primitives/Math.cs | 9 + Engine.Physics2D/Primitives/Shape.cs | 68 +++ Engine.Physics2D/Primitives/Triangle.cs | 54 ++ Engine.Physics2D/RigidBody2D.cs | 24 + Engine.sln | 6 + 22 files changed, 1184 insertions(+) create mode 100644 .gitignore create mode 100644 Engine.Physics2D/Abstract/ICollider2D.cs create mode 100644 Engine.Physics2D/Abstract/ICollisionResolver2D.cs create mode 100644 Engine.Physics2D/Abstract/IPhysicsEngine2D.cs create mode 100644 Engine.Physics2D/Abstract/IPhysicsMaterial2D.cs create mode 100644 Engine.Physics2D/Abstract/IRigidBody2D.cs create mode 100644 Engine.Physics2D/Collider2DAABBBehaviour.cs create mode 100644 Engine.Physics2D/CollisionInformation.cs create mode 100644 Engine.Physics2D/Engine.Physics2D.csproj create mode 100644 Engine.Physics2D/PhysicsEngine2D.cs create mode 100644 Engine.Physics2D/PhysicsMaterial2D.cs create mode 100644 Engine.Physics2D/PhysicsMaterial2DDefault.cs create mode 100644 Engine.Physics2D/PhysicsMath.cs create mode 100644 Engine.Physics2D/Primitives/AABB.cs create mode 100644 Engine.Physics2D/Primitives/Circle.cs create mode 100644 Engine.Physics2D/Primitives/Line.cs create mode 100644 Engine.Physics2D/Primitives/LineEquation.cs create mode 100644 Engine.Physics2D/Primitives/Math.cs create mode 100644 Engine.Physics2D/Primitives/Shape.cs create mode 100644 Engine.Physics2D/Primitives/Triangle.cs create mode 100644 Engine.Physics2D/RigidBody2D.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104b544 --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/Engine.Physics2D/Abstract/ICollider2D.cs b/Engine.Physics2D/Abstract/ICollider2D.cs new file mode 100644 index 0000000..a88a19f --- /dev/null +++ b/Engine.Physics2D/Abstract/ICollider2D.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface ICollider2D : IBehaviour, IAssignableTransform +{ + Action? OnCollisionPreResolve { get; set; } + + IRigidBody2D? RigidBody2D { get; } + + IList Vertices { get; } + + bool CheckCollision(Vector2D point); + void Recalculate(); +} diff --git a/Engine.Physics2D/Abstract/ICollisionResolver2D.cs b/Engine.Physics2D/Abstract/ICollisionResolver2D.cs new file mode 100644 index 0000000..83d54b4 --- /dev/null +++ b/Engine.Physics2D/Abstract/ICollisionResolver2D.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface ICollisionResolver2D +{ + void ResolveCollision(ICollider2D colliderA, ICollider2D colliderB); +} diff --git a/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs b/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs new file mode 100644 index 0000000..aed1d10 --- /dev/null +++ b/Engine.Physics2D/Abstract/IPhysicsEngine2D.cs @@ -0,0 +1,11 @@ +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface IPhysicsEngine2D +{ + int IterationCount { get; set; } + + void AddRigidBody(IRigidBody2D rigidBody); + void RemoveRigidBody(IRigidBody2D rigidBody); + + void Step(float deltaTime); +} diff --git a/Engine.Physics2D/Abstract/IPhysicsMaterial2D.cs b/Engine.Physics2D/Abstract/IPhysicsMaterial2D.cs new file mode 100644 index 0000000..c533cda --- /dev/null +++ b/Engine.Physics2D/Abstract/IPhysicsMaterial2D.cs @@ -0,0 +1,7 @@ +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface IPhysicsMaterial2D +{ + float Friction { get; } + float Restitution { get; } +} diff --git a/Engine.Physics2D/Abstract/IRigidBody2D.cs b/Engine.Physics2D/Abstract/IRigidBody2D.cs new file mode 100644 index 0000000..820caa6 --- /dev/null +++ b/Engine.Physics2D/Abstract/IRigidBody2D.cs @@ -0,0 +1,14 @@ +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface IRigidBody2D : IBehaviour, IAssignableTransform +{ + IPhysicsMaterial2D Material { get; set; } + + Vector2D Velocity { get; set; } + float AngularVelocity { get; set; } + + float Mass { get; set; } +} diff --git a/Engine.Physics2D/Collider2DAABBBehaviour.cs b/Engine.Physics2D/Collider2DAABBBehaviour.cs new file mode 100644 index 0000000..79fc368 --- /dev/null +++ b/Engine.Physics2D/Collider2DAABBBehaviour.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; +using Syntriax.Engine.Physics2D.Abstract; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public class Collider2DAABBBehaviour : BehaviourOverride, ICollider2D +{ + public AABB AABBLocal { get; set; } = null!; + public AABB AABBWorld { get; private set; } = null!; + + private IRigidBody2D? _rigidBody2D = null; + private List _vertices = new List(4); + + public IRigidBody2D? RigidBody2D + { + get + { + if (_rigidBody2D is null) + BehaviourController.TryGetBehaviour(out _rigidBody2D); + + return _rigidBody2D; + } + } + + public Action? OnCollisionPreResolve { get; set; } = null; + + public Action? OnTransformAssigned { get => GameObject.OnTransformAssigned; set => GameObject.OnTransformAssigned = value; } + ITransform IAssignableTransform.Transform => Transform; + public bool Assign(ITransform transform) => GameObject.Assign(transform); + + public IList Vertices => _vertices; + + public bool CheckCollision(Vector2D point) + { + return AABBWorld.Overlaps(point); + } + + public void Recalculate() + { + AABBWorld = new AABB( + AABBLocal.LowerBoundary.Scale(Transform.Scale) + Transform.Position, + AABBLocal.UpperBoundary.Scale(Transform.Scale) + Transform.Position + ); + + Vertices.Clear(); + Vertices.Add(AABBWorld.LowerBoundary); + Vertices.Add(new Vector2D(AABBWorld.LowerBoundary.X, AABBWorld.UpperBoundary.Y)); + Vertices.Add(AABBWorld.UpperBoundary); + Vertices.Add(new Vector2D(AABBWorld.UpperBoundary.X, AABBWorld.LowerBoundary.Y)); + } + public Collider2DAABBBehaviour(Vector2D lowerBoundary, Vector2D upperBoundary) + { + AABBLocal = new AABB(lowerBoundary, upperBoundary); + AABBWorld = new AABB(lowerBoundary, upperBoundary); + } + + public Collider2DAABBBehaviour() + { + AABBLocal = new(Vector2D.Zero, Vector2D.Zero); + AABBWorld = new(Vector2D.Zero, Vector2D.Zero); + } +} diff --git a/Engine.Physics2D/CollisionInformation.cs b/Engine.Physics2D/CollisionInformation.cs new file mode 100644 index 0000000..24d561d --- /dev/null +++ b/Engine.Physics2D/CollisionInformation.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace Syntriax.Engine.Physics2D; + +public record CollisionInformation +( + Vector2 Normal, + Vector2 ContactPosition +); diff --git a/Engine.Physics2D/Engine.Physics2D.csproj b/Engine.Physics2D/Engine.Physics2D.csproj new file mode 100644 index 0000000..adb4974 --- /dev/null +++ b/Engine.Physics2D/Engine.Physics2D.csproj @@ -0,0 +1,13 @@ + + + + + + + + net8.0 + disable + enable + + + diff --git a/Engine.Physics2D/PhysicsEngine2D.cs b/Engine.Physics2D/PhysicsEngine2D.cs new file mode 100644 index 0000000..684bdeb --- /dev/null +++ b/Engine.Physics2D/PhysicsEngine2D.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; +using Syntriax.Engine.Physics2D.Abstract; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public class PhysicsEngine2D : IPhysicsEngine2D +{ + private List rigidBodies = new List(32); + private List colliders = new List(64); + + private int _iterationCount = 1; + + + public int IterationCount { get => _iterationCount; set => _iterationCount = value < 1 ? 1 : value; } + + public void AddRigidBody(IRigidBody2D rigidBody) + { + if (rigidBodies.Contains(rigidBody)) + return; + + rigidBodies.Add(rigidBody); + + foreach (var collider2D in rigidBody.BehaviourController.GetBehaviours()) + colliders.Add(collider2D); + + rigidBody.BehaviourController.OnBehaviourAdded += OnBehaviourAdded; + rigidBody.BehaviourController.OnBehaviourRemoved += OnBehaviourRemoved; + } + + public void RemoveRigidBody(IRigidBody2D rigidBody) + { + rigidBodies.Remove(rigidBody); + } + + public void Step(float deltaTime) + { + float intervalDeltaTime = deltaTime / IterationCount; + + for (int iterationIndex = 0; iterationIndex < IterationCount; iterationIndex++) + { + foreach (var rigidBody in rigidBodies) + StepRigidBody(rigidBody, intervalDeltaTime); + + foreach (var collider in colliders) + collider.Recalculate(); + } + } + + private static void StepRigidBody(IRigidBody2D rigidBody, float intervalDeltaTime) + { + rigidBody.Transform.Position += rigidBody.Velocity * intervalDeltaTime; + rigidBody.Transform.Rotation += rigidBody.AngularVelocity * intervalDeltaTime; + } + + private void OnBehaviourAdded(IBehaviourController controller, IBehaviour behaviour) + { + if (behaviour is not ICollider2D collider2D) + return; + + colliders.Add(collider2D); + } + + private void OnBehaviourRemoved(IBehaviourController controller, IBehaviour behaviour) + { + if (behaviour is not ICollider2D collider2D) + return; + + colliders.Remove(collider2D); + } +} diff --git a/Engine.Physics2D/PhysicsMaterial2D.cs b/Engine.Physics2D/PhysicsMaterial2D.cs new file mode 100644 index 0000000..878691a --- /dev/null +++ b/Engine.Physics2D/PhysicsMaterial2D.cs @@ -0,0 +1,5 @@ +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public record PhysicsMaterial2D(float Friction, float Restitution) : IPhysicsMaterial2D { } diff --git a/Engine.Physics2D/PhysicsMaterial2DDefault.cs b/Engine.Physics2D/PhysicsMaterial2DDefault.cs new file mode 100644 index 0000000..61b4367 --- /dev/null +++ b/Engine.Physics2D/PhysicsMaterial2DDefault.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Physics2D; + +public record PhysicsMaterial2DDefault : PhysicsMaterial2D +{ + public PhysicsMaterial2DDefault() : base(.1f, .1f) { } +} diff --git a/Engine.Physics2D/PhysicsMath.cs b/Engine.Physics2D/PhysicsMath.cs new file mode 100644 index 0000000..7ccd28c --- /dev/null +++ b/Engine.Physics2D/PhysicsMath.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; + +using Microsoft.Xna.Framework; +using Syntriax.Engine.Core; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public static class PhysicsMath +{ + public static Vector2D Scale(this Vector2D original, Vector2D scale) + => new Vector2D(original.X * scale.X, original.Y * scale.Y); + + public static Triangle ToSuperTriangle(this IList vertices) + { + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + foreach (Vector2D point in vertices) + { + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + float dx = maxX - minX; + float dy = maxY - minY; + float deltaMax = MathF.Max(dx, dy); + float midX = (minX + maxX) / 2; + float midY = (minY + maxY) / 2; + + Vector2D p1 = new Vector2D((float)midX - 20f * (float)deltaMax, (float)midY - (float)deltaMax); + Vector2D p2 = new Vector2D((float)midX, (float)midY + 20 * (float)deltaMax); + Vector2D p3 = new Vector2D((float)midX + 20 * (float)deltaMax, (float)midY - (float)deltaMax); + + return new Triangle(p1, p2, p3); + } + + public static IList ToLines(this IList vertices) + { + List lines = new List(vertices.Count - 1); + ToLines(vertices, lines); + return lines; + } + + public static void ToLines(this IList vertices, IList lines) + { + lines.Clear(); + for (int i = 0; i < vertices.Count - 1; i++) + lines.Add(new(vertices[i], vertices[i + 1])); + lines.Add(new(vertices[^1], vertices[0])); + } + + public static bool LaysOn(this Vector2D point, Line line) + => line.Resolve(point.X).ApproximatelyEquals(point); + + + // Given three collinear points p, q, r, the function checks if + // point q lies on line segment 'pr' + public static bool OnSegment(Vector2D p, Vector2D q, Vector2D r) + { + if (q.X <= MathF.Max(p.X, r.X) && q.X >= MathF.Min(p.X, r.X) && + q.Y <= MathF.Max(p.Y, r.Y) && q.Y >= MathF.Min(p.Y, r.Y)) + return true; + + return false; + } + + // To find orientation of ordered triplet (p, q, r). + // The function returns following values + // 0 --> p, q and r are collinear + // 1 --> Clockwise + // 2 --> Counterclockwise + public static int Orientation(Vector2D p, Vector2D q, Vector2D r) + { + // See https://www.geeksforgeeks.org/orientation-3-ordered-points/ + // for details of below formula. + float val = (q.Y - p.Y) * (r.X - q.X) - + (q.X - p.X) * (r.Y - q.Y); + + if (val == 0) return 0; // collinear + + return (val > 0) ? 1 : 2; // clock or counterclock wise + } + + public static float IntersectionParameterT(Vector2D p0, Vector2D p1, Vector2D q0, Vector2D q1) + => ((q0.X - p0.X) * (p1.Y - p0.Y) - (q0.Y - p0.Y) * (p1.X - p0.X)) / + ((q1.Y - q0.Y) * (p1.X - p0.X) - (q1.X - q0.X) * (p1.Y - p0.Y)); + + + public static bool ApproximatelyEquals(this float a, float b) + => ApproximatelyEquals(a, b, float.Epsilon); + public static bool ApproximatelyEquals(this Vector2 a, Vector2 b) + => ApproximatelyEquals(a, b, float.Epsilon); + public static bool ApproximatelyEquals(this Vector2 a, Vector2 b, float epsilon) + => ApproximatelyEquals(a.X, b.X, epsilon) && ApproximatelyEquals(a.Y, b.Y, epsilon); + public static bool ApproximatelyEquals(this Vector2D a, Vector2D b) + => ApproximatelyEquals(a, b, float.Epsilon); + public static bool ApproximatelyEquals(this Vector2D a, Vector2D b, float epsilon) + => ApproximatelyEquals(a.X, b.X, epsilon) && ApproximatelyEquals(a.Y, b.Y, epsilon); + public static bool ApproximatelyEquals(this float a, float b, float epsilon) + { + if (a == b) + return true; + + const float floatNormal = (1 << 23) * float.Epsilon; + float absA = MathF.Abs(a); + float absB = MathF.Abs(b); + float diff = MathF.Abs(a - b); + + if (a == 0.0f || b == 0.0f || diff < floatNormal) + return diff < (epsilon * floatNormal); + + return diff / MathF.Min(absA + absB, float.MaxValue) < epsilon; + } +} diff --git a/Engine.Physics2D/Primitives/AABB.cs b/Engine.Physics2D/Primitives/AABB.cs new file mode 100644 index 0000000..96e1560 --- /dev/null +++ b/Engine.Physics2D/Primitives/AABB.cs @@ -0,0 +1,17 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +public record AABB(Vector2D LowerBoundary, Vector2D UpperBoundary) +{ + public bool Overlaps(Vector2D point) + => point.X >= LowerBoundary.X && point.X <= UpperBoundary.X && + point.Y >= LowerBoundary.Y && point.Y <= UpperBoundary.Y; + + public bool Overlaps(AABB other) + => LowerBoundary.X <= other.UpperBoundary.X && UpperBoundary.X >= other.LowerBoundary.X && + LowerBoundary.Y <= other.UpperBoundary.Y && UpperBoundary.Y >= other.LowerBoundary.Y; + + public bool ApproximatelyEquals(AABB other) + => LowerBoundary.ApproximatelyEquals(other.LowerBoundary) && UpperBoundary.ApproximatelyEquals(other.UpperBoundary); +} diff --git a/Engine.Physics2D/Primitives/Circle.cs b/Engine.Physics2D/Primitives/Circle.cs new file mode 100644 index 0000000..1d30cf2 --- /dev/null +++ b/Engine.Physics2D/Primitives/Circle.cs @@ -0,0 +1,18 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +public record Circle(Vector2D Position, float Radius) +{ + public bool Intersects(Circle other) + { + float distanceSquared = (Position - other.Position).LengthSquared(); + float radiusSumSquared = Radius * Radius + other.Radius * other.Radius; + + return distanceSquared < radiusSumSquared; + } + + public bool Overlaps(Vector2D point) => (Position - point).LengthSquared() <= Radius * Radius; + public bool ApproximatelyEquals(Circle other) + => Position.ApproximatelyEquals(other.Position) && Radius.ApproximatelyEquals(other.Radius); +} diff --git a/Engine.Physics2D/Primitives/Line.cs b/Engine.Physics2D/Primitives/Line.cs new file mode 100644 index 0000000..bd3162c --- /dev/null +++ b/Engine.Physics2D/Primitives/Line.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +public record Line(Vector2D From, Vector2D To) +{ + public Line Reversed => new(To, From); + public Vector2D Direction => Vector2D.Normalize(To - From); + public float Length => (From - To).Length(); + public float LengthSquared => (From - To).LengthSquared(); + + public LineEquation LineEquation + { + get + { + Vector2D slopeVector = To - From; + float slope = slopeVector.Y / slopeVector.X; + + float yOffset = From.Y - (slope * From.X); + + return new LineEquation(slope, yOffset); + } + } + + public bool Intersects(Vector2D point) + => Resolve(point.X).ApproximatelyEquals(point); + + public float GetT(Vector2D point) + { + float fromX = MathF.Abs(From.X); + float toX = MathF.Abs(To.X); + float pointX = MathF.Abs(point.X); + + float min = MathF.Min(fromX, toX); + float max = MathF.Max(fromX, toX) - min; + + pointX -= min; + + float t = pointX / max; + + // FIXME + // I don't even know, apparently whatever I wrote up there doesn't take into account of the direction of the line + // Which... I can see how, but I am also not sure how I can make it take into account. Or actually I'm for some reason + // too unmotivated to find a solution. Future me, find a better way if possible, please. + if (!Lerp(t).ApproximatelyEquals(point)) + return 1f - t; + return t; + } + + public bool Exist(List vertices) + { + for (int i = 0; i < vertices.Count - 1; i++) + { + Vector2D vertexCurrent = vertices[i]; + Vector2D vertexNext = vertices[i]; + if (From == vertexCurrent && To == vertexNext) return true; + if (From == vertexNext && To == vertexCurrent) return true; + } + + Vector2D vertexFirst = vertices[0]; + Vector2D vertexLast = vertices[^1]; + if (From == vertexFirst && To == vertexLast) return true; + if (From == vertexLast && To == vertexFirst) return true; + return false; + } + + public float IntersectionParameterT(Line other) + { + float numerator = (From.X - other.From.X) * (other.From.Y - other.To.Y) - (From.Y - other.From.Y) * (other.From.X - other.To.X); + float denominator = (From.X - To.X) * (other.From.Y - other.To.Y) - (From.Y - To.Y) * (other.From.X - other.To.X); + + // Lines are parallel + if (denominator == 0) + return float.NaN; + + return numerator / denominator; + } + + public Vector2D Lerp(float t) + => new Vector2D( + From.X + (To.X - From.X) * t, + From.Y + (To.Y - From.Y) * t + ); + + public Vector2D Resolve(float x) + => new Vector2D(x, LineEquation.Resolve(x)); + + public Vector2D ClosestPointTo(Vector2D point) + { + // Convert edge points to vectors + var edgeVector = new Vector2D(To.X - From.X, To.Y - From.Y); + var pointVector = new Vector2D(point.X - From.X, point.Y - From.Y); + + // Calculate the projection of pointVector onto edgeVector + float t = (pointVector.X * edgeVector.X + pointVector.Y * edgeVector.Y) / (edgeVector.X * edgeVector.X + edgeVector.Y * edgeVector.Y); + + // Clamp t to the range [0, 1] to ensure the closest point is on the edge + t = MathF.Max(0, MathF.Min(1, t)); + + // Calculate the closest point on the edge + float closestX = From.X + t * edgeVector.X; + float closestY = From.Y + t * edgeVector.Y; + + return new Vector2D((float)closestX, (float)closestY); + } + + public Vector2D IntersectionPoint(Line other) + => Vector2D.Lerp(From, To, IntersectionParameterT(other)); + + public bool Intersects(Line other) + { + int o1 = PhysicsMath.Orientation(From, To, other.From); + int o2 = PhysicsMath.Orientation(From, To, other.To); + int o3 = PhysicsMath.Orientation(other.From, other.To, From); + int o4 = PhysicsMath.Orientation(other.From, other.To, To); + + if (o1 != o2 && o3 != o4) + return true; + + if (o1 == 0 && PhysicsMath.OnSegment(From, other.From, To)) return true; + if (o2 == 0 && PhysicsMath.OnSegment(From, other.To, To)) return true; + if (o3 == 0 && PhysicsMath.OnSegment(other.From, From, other.To)) return true; + if (o4 == 0 && PhysicsMath.OnSegment(other.From, To, other.To)) return true; + + return false; + } + + public bool Intersects(Line other, [NotNullWhen(returnValue: true)] out Vector2D? point) + { + point = null; + + bool result = Intersects(other); + + if (result) + point = IntersectionPoint(other); + + return result; + } + + public bool ApproximatelyEquals(Line other) + => From.ApproximatelyEquals(other.From) && To.ApproximatelyEquals(other.To); +} diff --git a/Engine.Physics2D/Primitives/LineEquation.cs b/Engine.Physics2D/Primitives/LineEquation.cs new file mode 100644 index 0000000..a60977b --- /dev/null +++ b/Engine.Physics2D/Primitives/LineEquation.cs @@ -0,0 +1,8 @@ +namespace Syntriax.Engine.Physics2D.Primitives; + +public record LineEquation(float Slope, float OffsetY) +{ + public float Resolve(float x) => Slope * x + OffsetY; // y = mx + b + public bool ApproximatelyEquals(LineEquation other) + => Slope.ApproximatelyEquals(other.Slope) && OffsetY.ApproximatelyEquals(other.OffsetY); +} diff --git a/Engine.Physics2D/Primitives/Math.cs b/Engine.Physics2D/Primitives/Math.cs new file mode 100644 index 0000000..79875d4 --- /dev/null +++ b/Engine.Physics2D/Primitives/Math.cs @@ -0,0 +1,9 @@ +namespace Syntriax.Engine.Physics2D.Primitives; + +public static class Math +{ + public const float RadianToDegree = 57.29577866666166f; + public const float DegreeToRadian = 0.01745329277777778f; + + public static float Clamp(float value, float min, float max) => (value < min) ? min : (value > max) ? max : value; +} diff --git a/Engine.Physics2D/Primitives/Shape.cs b/Engine.Physics2D/Primitives/Shape.cs new file mode 100644 index 0000000..8ba8dbf --- /dev/null +++ b/Engine.Physics2D/Primitives/Shape.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +public record Shape(IList Vertices) +{ + public Triangle SuperTriangle + { + get + { + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + foreach (Vector2D point in Vertices) + { + minX = MathF.Min(minX, point.X); + minY = MathF.Min(minY, point.Y); + maxX = MathF.Max(maxX, point.X); + maxY = MathF.Max(maxY, point.Y); + } + + float dx = maxX - minX; + float dy = maxY - minY; + float deltaMax = MathF.Max(dx, dy); + float midX = (minX + maxX) / 2; + float midY = (minY + maxY) / 2; + + Vector2D p1 = new Vector2D((float)midX - 20f * (float)deltaMax, (float)midY - (float)deltaMax); + Vector2D p2 = new Vector2D((float)midX, (float)midY + 20 * (float)deltaMax); + Vector2D p3 = new Vector2D((float)midX + 20 * (float)deltaMax, (float)midY - (float)deltaMax); + + return new Triangle(p1, p2, p3); + } + } + + public List Lines + { + get + { + List lines = new List(Vertices.Count - 1); + GetLinesNonAlloc(lines); + return lines; + } + } + + public void GetLinesNonAlloc(IList lines) + { + lines.Clear(); + for (int i = 0; i < Vertices.Count - 1; i++) + lines.Add(new(Vertices[i], Vertices[i + 1])); + lines.Add(new(Vertices[^1], Vertices[0])); + } + + public bool ApproximatelyEquals(Shape other) + { + if (Vertices.Count != other.Vertices.Count) + return false; + + for (int i = 0; i < Vertices.Count; i++) + if (!Vertices[i].ApproximatelyEquals(other.Vertices[i])) + return false; + + return true; + } +} diff --git a/Engine.Physics2D/Primitives/Triangle.cs b/Engine.Physics2D/Primitives/Triangle.cs new file mode 100644 index 0000000..5ffcd6a --- /dev/null +++ b/Engine.Physics2D/Primitives/Triangle.cs @@ -0,0 +1,54 @@ +using System; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +public record Triangle(Vector2D A, Vector2D B, Vector2D C) +{ + public float Area => MathF.Abs(( + A.X * (B.Y - C.Y) + + B.X * (C.Y - A.Y) + + C.X * (A.Y - B.Y) + ) * .5f); + + public Circle CircumCircle + { + get + { + Vector2D midAB = (A + B) / 2; + Vector2D midBC = (B + C) / 2; + + float slopeAB = (B.Y - A.Y) / (B.X - A.X); + float slopeBC = (C.Y - B.Y) / (C.X - B.X); + + Vector2D center; + if (MathF.Abs(slopeAB - slopeBC) > float.Epsilon) + { + float x = (slopeAB * slopeBC * (A.Y - C.Y) + slopeBC * (A.X + B.X) - slopeAB * (B.X + C.X)) / (2 * (slopeBC - slopeAB)); + float y = -(x - (A.X + B.X) / 2) / slopeAB + (A.Y + B.Y) / 2; + center = new Vector2D(x, y); + } + else + center = (midAB + midBC) * .5f; + + return new(center, Vector2D.Distance(center, A)); + } + } + + public bool Overlaps(Vector2D point) + { + float originalTriangleArea = Area; + + float pointTriangleArea1 = new Triangle(point, B, C).Area; + float pointTriangleArea2 = new Triangle(A, point, C).Area; + float pointTriangleArea3 = new Triangle(A, B, point).Area; + + float pointTriangleAreasSum = pointTriangleArea1 + pointTriangleArea2 + pointTriangleArea3; + + return originalTriangleArea.ApproximatelyEquals(pointTriangleAreasSum, float.Epsilon * 3f); + } + + public bool ApproximatelyEquals(Triangle other) + => A.ApproximatelyEquals(other.A) && B.ApproximatelyEquals(other.B) && C.ApproximatelyEquals(other.C); +} diff --git a/Engine.Physics2D/RigidBody2D.cs b/Engine.Physics2D/RigidBody2D.cs new file mode 100644 index 0000000..ed4fb4a --- /dev/null +++ b/Engine.Physics2D/RigidBody2D.cs @@ -0,0 +1,24 @@ +using System; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public class RigidBody2D : BehaviourOverride, IRigidBody2D +{ + public Action? OnTransformAssigned { get => GameObject.OnTransformAssigned; set => GameObject.OnTransformAssigned = value; } + + + public IPhysicsMaterial2D Material { get; set; } = new PhysicsMaterial2DDefault(); + + public Vector2D Velocity { get; set; } = Vector2D.Zero; + public float AngularVelocity { get; set; } = 0f; + public float Mass { get; set; } = 0f; + + ITransform IAssignableTransform.Transform => Transform; + + + public bool Assign(ITransform transform) => GameObject.Assign(transform); +} diff --git a/Engine.sln b/Engine.sln index 35f3e6c..e76e3fd 100644 --- a/Engine.sln +++ b/Engine.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Core", "Engine.Core\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Input", "Engine.Input\Engine.Input.csproj", "{12149E55-1EE8-45B4-A82E-15BA981B0C6A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Physics2D", "Engine.Physics2D\Engine.Physics2D.csproj", "{3B3C3332-07E3-4A00-9898-0A5410BCB08C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {12149E55-1EE8-45B4-A82E-15BA981B0C6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {12149E55-1EE8-45B4-A82E-15BA981B0C6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {12149E55-1EE8-45B4-A82E-15BA981B0C6A}.Release|Any CPU.Build.0 = Release|Any CPU + {3B3C3332-07E3-4A00-9898-0A5410BCB08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B3C3332-07E3-4A00-9898-0A5410BCB08C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B3C3332-07E3-4A00-9898-0A5410BCB08C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B3C3332-07E3-4A00-9898-0A5410BCB08C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal