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.Core/Abstract/IBehaviour.cs b/Engine.Core/Abstract/IBehaviour.cs index 47c30bc..95d12d8 100644 --- a/Engine.Core/Abstract/IBehaviour.cs +++ b/Engine.Core/Abstract/IBehaviour.cs @@ -16,4 +16,9 @@ public interface IBehaviour : IEntity, IAssignableBehaviourController, IAssignab /// Call priority of the . /// int Priority { get; set; } + + /// + /// If the is active. + /// + bool IsActive { get; } } diff --git a/Engine.Core/Abstract/ICamera.cs b/Engine.Core/Abstract/ICamera.cs deleted file mode 100644 index bcab06c..0000000 --- a/Engine.Core/Abstract/ICamera.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Syntriax.Engine.Core.Abstract; - -public interface ICamera : IAssignableTransform -{ - Action? OnZoomChanged { get; set; } - - float Zoom { get; set; } - - void Update(); -} diff --git a/Engine.Core/Abstract/IGameManager.cs b/Engine.Core/Abstract/IGameManager.cs new file mode 100644 index 0000000..6909fb2 --- /dev/null +++ b/Engine.Core/Abstract/IGameManager.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Syntriax.Engine.Core.Abstract; + +public interface IGameManager : IEntity, IEnumerable +{ + Action? OnGameObjectRegistered { get; set; } + Action? OnGameObjectUnRegistered { get; set; } + + + IReadOnlyList GameObjects { get; } + + + void RegisterGameObject(IGameObject gameObject); + T InstantiateGameObject(params object?[]? args) where T : class, IGameObject; + IGameObject RemoveGameObject(IGameObject gameObject); + + void Update(EngineTime time); + void PreDraw(); +} diff --git a/Engine.Core/Behaviour.cs b/Engine.Core/Behaviour.cs index 943d366..4b9241b 100644 --- a/Engine.Core/Behaviour.cs +++ b/Engine.Core/Behaviour.cs @@ -5,6 +5,7 @@ using Syntriax.Engine.Core.Exceptions; namespace Syntriax.Engine.Core; +[System.Diagnostics.DebuggerDisplay("{GetType().Name, nq}, Priority: {Priority}, Initialized: {Initialized}")] public abstract class Behaviour : IBehaviour { public Action? OnUnassigned { get; set; } = null; @@ -25,6 +26,8 @@ public abstract class Behaviour : IBehaviour public IStateEnable StateEnable => _stateEnable; public IBehaviourController BehaviourController => _behaviourController; + public bool IsActive => StateEnable.Enabled && BehaviourController.GameObject.StateEnable.Enabled; + public bool Initialized { get => _initialized; diff --git a/Engine.Core/BehaviourController.cs b/Engine.Core/BehaviourController.cs index 98d4585..0a96572 100644 --- a/Engine.Core/BehaviourController.cs +++ b/Engine.Core/BehaviourController.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Syntriax.Engine.Core.Abstract; namespace Syntriax.Engine.Core; +[System.Diagnostics.DebuggerDisplay("Behaviour Count: {behaviours.Count}")] public class BehaviourController : IBehaviourController { public Action? OnPreUpdate { get; set; } @@ -55,17 +57,17 @@ public class BehaviourController : IBehaviourController public IList GetBehaviours() { - IList behaviours = new List(); + List? behaviours = null; foreach (var behaviourItem in this.behaviours) { if (behaviourItem is not T behaviour) continue; - behaviours ??= new List(); + behaviours ??= []; behaviours.Add(behaviour); } - return behaviours; + return behaviours ?? Enumerable.Empty().ToList(); } public void RemoveBehaviour(bool removeAll = false) where T : class, IBehaviour diff --git a/Engine.Core/BehaviourOverride.cs b/Engine.Core/BehaviourOverride.cs index 81a8d4c..6e8dcc9 100644 --- a/Engine.Core/BehaviourOverride.cs +++ b/Engine.Core/BehaviourOverride.cs @@ -39,13 +39,13 @@ public abstract class BehaviourOverride : Behaviour OnFinalize(); } - protected virtual void OnPreUpdatePreEnabledCheck() { } + protected virtual void OnPreUpdatePreActiveCheck() { } protected virtual void OnPreUpdate() { } private void PreUpdate(IBehaviourController _) { - OnPreUpdatePreEnabledCheck(); + OnPreUpdatePreActiveCheck(); - if (!StateEnable.Enabled) + if (!IsActive) return; if (isInitializedThisFrame) @@ -61,23 +61,23 @@ public abstract class BehaviourOverride : Behaviour isInitializedThisFrame = false; } - protected virtual void OnUpdatePreEnabledCheck() { } + protected virtual void OnUpdatePreActiveCheck() { } protected virtual void OnUpdate() { } private void Update(IBehaviourController _) { - OnUpdatePreEnabledCheck(); + OnUpdatePreActiveCheck(); - if (!StateEnable.Enabled) + if (!IsActive) return; OnUpdate(); } - protected virtual void OnPreDrawPreEnabledCheck() { } + protected virtual void OnPreDrawPreActiveCheck() { } protected virtual void OnPreDraw() { } private void PreDraw(IBehaviourController _) { - OnPreDrawPreEnabledCheck(); + OnPreDrawPreActiveCheck(); if (!StateEnable.Enabled) return; diff --git a/Engine.Core/Engine.Core.sln b/Engine.Core/Engine.Core.sln deleted file mode 100644 index 5133cda..0000000 --- a/Engine.Core/Engine.Core.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Engine.Core", "Engine.Core.csproj", "{6C1AF4B1-60B0-4225-9A96-F597BC04E9D0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6C1AF4B1-60B0-4225-9A96-F597BC04E9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C1AF4B1-60B0-4225-9A96-F597BC04E9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C1AF4B1-60B0-4225-9A96-F597BC04E9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C1AF4B1-60B0-4225-9A96-F597BC04E9D0}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {CC449885-B99C-4C71-842D-3C19A35E786F} - EndGlobalSection -EndGlobal diff --git a/Engine.Core/EngineTime.cs b/Engine.Core/EngineTime.cs index 107748c..c9d5f87 100644 --- a/Engine.Core/EngineTime.cs +++ b/Engine.Core/EngineTime.cs @@ -2,8 +2,8 @@ using System; namespace Syntriax.Engine.Core; -public record EngineTime -( - TimeSpan Total, - TimeSpan Elapsed -); +public readonly struct EngineTime(TimeSpan Total, TimeSpan Elapsed) +{ + public readonly TimeSpan Total { get; init; } = Total; + public readonly TimeSpan Elapsed { get; init; } = Elapsed; +} diff --git a/Engine.Core/Extensions/Abstract/TransformExtensions.cs b/Engine.Core/Extensions/Abstract/TransformExtensions.cs new file mode 100644 index 0000000..24244a3 --- /dev/null +++ b/Engine.Core/Extensions/Abstract/TransformExtensions.cs @@ -0,0 +1,9 @@ +namespace Syntriax.Engine.Core.Abstract; + +public static class TransformExtensions +{ + public static Vector2D TransformVector2D(this ITransform transform, Vector2D vector) + => vector.Scale(transform.Scale) + .Rotate(transform.Rotation * Math.DegreeToRadian) + .Add(transform.Position); +} diff --git a/Engine.Core/Extensions/FloatExtensions.cs b/Engine.Core/Extensions/FloatExtensions.cs new file mode 100644 index 0000000..019bc39 --- /dev/null +++ b/Engine.Core/Extensions/FloatExtensions.cs @@ -0,0 +1,22 @@ +namespace Syntriax.Engine.Core; + +public static class FloatExtensions +{ + public static bool ApproximatelyEquals(this float a, float b) + => ApproximatelyEquals(a, b, float.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 = Math.Abs(a); + float absB = Math.Abs(b); + float diff = Math.Abs(a - b); + + if (a == 0.0f || b == 0.0f || diff < floatNormal) + return diff < (epsilon * floatNormal); + + return diff / Math.Min(absA + absB, float.MaxValue) < epsilon; + } +} diff --git a/Engine.Core/Extensions/Vector2DExtensions.cs b/Engine.Core/Extensions/Vector2DExtensions.cs index 6207043..29995da 100644 --- a/Engine.Core/Extensions/Vector2DExtensions.cs +++ b/Engine.Core/Extensions/Vector2DExtensions.cs @@ -4,13 +4,23 @@ public static class Vector2DExtensions { public static float Length(this Vector2D vector) => Vector2D.Length(vector); public static float LengthSquared(this Vector2D vector) => Vector2D.LengthSquared(vector); + public static float Distance(this Vector2D from, Vector2D to) => Vector2D.Distance(from, to); + public static Vector2D Invert(this Vector2D vector) => Vector2D.Invert(vector); + public static Vector2D Add(this Vector2D vector, Vector2D vectorToAdd) => Vector2D.Add(vector, vectorToAdd); + public static Vector2D Subtract(this Vector2D vector, Vector2D vectorToSubtract) => Vector2D.Subtract(vector, vectorToSubtract); + public static Vector2D Multiply(this Vector2D vector, float value) => Vector2D.Multiply(vector, value); + public static Vector2D Subdivide(this Vector2D vector, float value) => Vector2D.Subdivide(vector, value); + + public static Vector2D Abs(this Vector2D vector) => Vector2D.Abs(vector); public static Vector2D Reflect(this Vector2D vector, Vector2D normal) => Vector2D.Reflect(vector, normal); public static Vector2D Normalize(this Vector2D vector) => Vector2D.Normalize(vector); public static Vector2D FromTo(this Vector2D from, Vector2D to) => Vector2D.FromTo(from, to); public static Vector2D Scale(this Vector2D vector, Vector2D scale) => Vector2D.Scale(vector, scale); + public static Vector2D Perpendicular(this Vector2D vector) => Vector2D.Perpendicular(vector); + public static Vector2D Rotate(this Vector2D vector, float angleInRadian) => Vector2D.Rotate(vector, angleInRadian); public static Vector2D Min(this Vector2D left, Vector2D right) => Vector2D.Min(left, right); public static Vector2D Max(this Vector2D left, Vector2D right) => Vector2D.Max(left, right); public static Vector2D Clamp(this Vector2D vector, Vector2D min, Vector2D max) => Vector2D.Clamp(vector, min, max); @@ -19,4 +29,7 @@ public static class Vector2DExtensions public static float Cross(this Vector2D left, Vector2D right) => Vector2D.Cross(left, right); public static float AngleBetween(this Vector2D left, Vector2D right) => Vector2D.Angle(left, right); public static float Dot(this Vector2D left, Vector2D right) => Vector2D.Dot(left, right); + + + public static bool ApproximatelyEquals(this Vector2D left, Vector2D right, float epsilon = float.Epsilon) => Vector2D.ApproximatelyEquals(left, right, epsilon); } diff --git a/Engine.Core/GameManager.cs b/Engine.Core/GameManager.cs index 8410bce..b9dd0c4 100644 --- a/Engine.Core/GameManager.cs +++ b/Engine.Core/GameManager.cs @@ -8,9 +8,9 @@ using Syntriax.Engine.Core.Factory; namespace Syntriax.Engine.Core; -public class GameManager : IEntity, IEnumerable +[System.Diagnostics.DebuggerDisplay("GameObject Count: {_gameObjects.Count}")] +public class GameManager : IGameManager { - public Action? OnCameraChanged { get; set; } = null; public Action? OnGameObjectRegistered { get; set; } = null; public Action? OnGameObjectUnRegistered { get; set; } = null; @@ -20,12 +20,11 @@ public class GameManager : IEntity, IEnumerable public Action? OnStateEnableAssigned { get; set; } = null; - private IList _gameObjects = new List(Constants.GAME_OBJECTS_SIZE_INITIAL); + private readonly List _gameObjects = new(Constants.GAME_OBJECTS_SIZE_INITIAL); private IStateEnable _stateEnable = null!; private GameObjectFactory _gameObjectFactory = null!; private bool _initialized = false; - private ICamera _camera = null!; private GameObjectFactory GameObjectFactory { @@ -38,7 +37,8 @@ public class GameManager : IEntity, IEnumerable } public bool Initialized => _initialized; - public IList GameObjects => _gameObjects; + public IReadOnlyList GameObjects => _gameObjects; + public IStateEnable StateEnable { get @@ -54,19 +54,6 @@ public class GameManager : IEntity, IEnumerable } } - public ICamera Camera - { - get => _camera; - set - { - if (_camera == value) - return; - - _camera = value; - OnCameraChanged?.Invoke(this); - } - } - public void RegisterGameObject(IGameObject gameObject) { if (_gameObjects.Contains(gameObject)) @@ -101,6 +88,7 @@ public class GameManager : IEntity, IEnumerable foreach (var gameObject in GameObjects) gameObject.Initialize(); + _initialized = true; OnInitialized?.Invoke(this); return true; } @@ -114,6 +102,7 @@ public class GameManager : IEntity, IEnumerable GameObjects[i].Finalize(); OnFinalized?.Invoke(this); + _initialized = false; return true; } diff --git a/Engine.Core/GameObject.cs b/Engine.Core/GameObject.cs index 7423977..0e50c25 100644 --- a/Engine.Core/GameObject.cs +++ b/Engine.Core/GameObject.cs @@ -5,6 +5,7 @@ using Syntriax.Engine.Core.Exceptions; namespace Syntriax.Engine.Core; +[System.Diagnostics.DebuggerDisplay("Name: {Name}, Initialized: {Initialized}")] public class GameObject : IGameObject { public Action? OnStateEnableAssigned { get; set; } = null; diff --git a/Engine.Core/Math.cs b/Engine.Core/Math.cs new file mode 100644 index 0000000..eda310b --- /dev/null +++ b/Engine.Core/Math.cs @@ -0,0 +1,35 @@ +using System; +using System.Numerics; + +namespace Syntriax.Engine.Core; + +public static class Math +{ + public const float RadianToDegree = 180f / PI; + public const float DegreeToRadian = PI / 180f; + + public const float E = 2.718281828459045f; + public const float PI = 3.1415926535897932f; + public const float Tau = 2f * PI; + + public static T Abs(T x) where T : INumber => x > T.Zero ? x : -x; + public static float Acos(float x) => MathF.Acos(x); + public static float Asin(float x) => MathF.Asin(x); + public static float Atan2(float y, float x) => MathF.Atan2(y, x); + public static float Atanh(float x) => MathF.Atanh(x); + public static T Clamp(this T x, T min, T max) where T : INumber => (x < min) ? min : (x > max) ? max : x; + public static float Ceiling(float x) => MathF.Ceiling(x); + public static float CopySign(float x, float y) => MathF.CopySign(x, y); + public static float Floor(float x) => MathF.Floor(x); + public static float IEEERemainder(float x, float y) => MathF.IEEERemainder(x, y); + public static float Log(float x, float y) => MathF.Log(x, y); + public static T Max(T x, T y) where T : INumber => (x > y) ? x : y; + public static float MaxMagnitude(float x, float y) => MathF.MaxMagnitude(x, y); + public static T Min(T x, T y) where T : INumber => (x < y) ? x : y; + public static float MinMagnitude(float x, float y) => MathF.MinMagnitude(x, y); + public static float Pow(float x, float y) => MathF.Pow(x, y); + public static float Round(float x, int digits, MidpointRounding mode) => MathF.Round(x, digits, mode); + public static T Sqr(T x) where T : INumber => x * x; + public static float Sqrt(float x) => MathF.Sqrt(x); + public static float Truncate(float x) => MathF.Truncate(x); +} diff --git a/Engine.Core/Transform.cs b/Engine.Core/Transform.cs index f4b28dd..cc0c32e 100644 --- a/Engine.Core/Transform.cs +++ b/Engine.Core/Transform.cs @@ -4,6 +4,7 @@ using Syntriax.Engine.Core.Abstract; namespace Syntriax.Engine.Core; +[System.Diagnostics.DebuggerDisplay("Position: {Position.ToString(), nq}, Scale: {Scale.ToString(), nq}, Rotation: {Rotation}")] public class Transform : ITransform { public Action? OnPositionChanged { get; set; } = null; diff --git a/Engine.Core/Vector2D.cs b/Engine.Core/Vector2D.cs index bd15af2..bbf9399 100644 --- a/Engine.Core/Vector2D.cs +++ b/Engine.Core/Vector2D.cs @@ -2,12 +2,15 @@ using System; namespace Syntriax.Engine.Core; -[System.Diagnostics.DebuggerDisplay("{ToString(),nq}, Length: {Magnitude}, LengthSquared: {MagnitudeSquared}, Normalized: {Normalized}")] -public record Vector2D(float X, float Y) +[System.Diagnostics.DebuggerDisplay("{ToString(),nq}, Length: {Magnitude}, LengthSquared: {MagnitudeSquared}, Normalized: {Normalized.ToString(),nq}")] +public readonly struct Vector2D(float X, float Y) { - public float Magnitude => Length(this); - public float MagnitudeSquared => LengthSquared(this); - public Vector2D Normalized => Normalize(this); + public readonly float X { get; init; } = X; + public readonly float Y { get; init; } = Y; + + public readonly float Magnitude => Length(this); + public readonly float MagnitudeSquared => LengthSquared(this); + public readonly Vector2D Normalized => Normalize(this); public readonly static Vector2D Up = new(0f, 1f); public readonly static Vector2D Down = new(0f, -1f); @@ -22,17 +25,28 @@ public record Vector2D(float X, float Y) public static Vector2D operator *(Vector2D vector, float value) => new(vector.X * value, vector.Y * value); public static Vector2D operator *(float value, Vector2D vector) => new(vector.X * value, vector.Y * value); public static Vector2D operator /(Vector2D vector, float value) => new(vector.X / value, vector.Y / value); + public static bool operator ==(Vector2D left, Vector2D right) => left.X == right.X && left.Y == right.Y; + public static bool operator !=(Vector2D left, Vector2D right) => left.X != right.X || left.Y != right.Y; public static float Length(Vector2D vector) => MathF.Sqrt(LengthSquared(vector)); public static float LengthSquared(Vector2D vector) => vector.X * vector.X + vector.Y * vector.Y; public static float Distance(Vector2D from, Vector2D to) => Length(FromTo(from, to)); + public static Vector2D Invert(Vector2D vector) => -vector; + public static Vector2D Add(Vector2D left, Vector2D right) => left + right; + public static Vector2D Subtract(Vector2D left, Vector2D right) => left - right; + public static Vector2D Multiply(Vector2D vector, float value) => vector * value; + public static Vector2D Subdivide(Vector2D vector, float value) => vector / value; + + public static Vector2D Abs(Vector2D vector) => new(Math.Abs(vector.X), Math.Abs(vector.Y)); public static Vector2D Normalize(Vector2D vector) => vector / Length(vector); public static Vector2D Reflect(Vector2D vector, Vector2D normal) => vector - 2f * Dot(vector, normal) * normal; public static Vector2D FromTo(Vector2D from, Vector2D to) => to - from; public static Vector2D Scale(Vector2D vector, Vector2D scale) => new(vector.X * scale.X, vector.Y * scale.Y); + public static Vector2D Perpendicular(Vector2D vector) => new(-vector.Y, vector.X); + public static Vector2D Rotate(Vector2D vector, float angleInRadian) => new(MathF.Cos(angleInRadian) * vector.X - MathF.Sin(angleInRadian) * vector.Y, MathF.Sin(angleInRadian) * vector.X + MathF.Cos(angleInRadian) * vector.Y); public static Vector2D Min(Vector2D left, Vector2D right) => new((left.X < right.X) ? left.X : right.X, (left.Y < right.Y) ? left.Y : right.Y); public static Vector2D Max(Vector2D left, Vector2D right) => new((left.X > right.X) ? left.X : right.X, (left.Y > right.Y) ? left.Y : right.Y); public static Vector2D Clamp(Vector2D vector, Vector2D min, Vector2D max) => new(Math.Clamp(vector.X, min.X, max.X), Math.Clamp(vector.Y, min.Y, max.Y)); @@ -42,5 +56,28 @@ public record Vector2D(float X, float Y) public static float Angle(Vector2D left, Vector2D right) => MathF.Acos(Dot(left, right) / (Length(left) * Length(right))); public static float Dot(Vector2D left, Vector2D right) => left.X * right.X + left.Y * right.Y; + /// + /// Finds the Orientation of 3 s + /// + /// 0 -> Collinear, 1 -> Clockwise, 2 -> Counterclockwise + public static int Orientation(Vector2D left, Vector2D middle, Vector2D right) + { + Vector2D leftToMiddle = left.FromTo(middle); + Vector2D middleToRight = middle.FromTo(right); + + float value = leftToMiddle.Y * middleToRight.X - + leftToMiddle.X * middleToRight.Y; + + if (value > 0) return 1; + if (value < 0) return 2; + return 0; + } + + public static bool ApproximatelyEquals(Vector2D left, Vector2D right, float epsilon = float.Epsilon) + => left.X.ApproximatelyEquals(right.X, epsilon) && left.Y.ApproximatelyEquals(right.Y, epsilon); + public override string ToString() => $"{nameof(Vector2D)}({X}, {Y})"; + + public override bool Equals(object? obj) => obj is Vector2D objVec && X.Equals(objVec.X) && Y.Equals(objVec.Y); + public override int GetHashCode() => HashCode.Combine(X, Y); } diff --git a/Engine.Input/Engine.Input.sln b/Engine.Input/Engine.Input.sln deleted file mode 100644 index c53c614..0000000 --- a/Engine.Input/Engine.Input.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Engine.Input", "Engine.Input.csproj", "{637B86E7-3699-4248-9A9F-C5CB09779B53}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {637B86E7-3699-4248-9A9F-C5CB09779B53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {637B86E7-3699-4248-9A9F-C5CB09779B53}.Debug|Any CPU.Build.0 = Debug|Any CPU - {637B86E7-3699-4248-9A9F-C5CB09779B53}.Release|Any CPU.ActiveCfg = Release|Any CPU - {637B86E7-3699-4248-9A9F-C5CB09779B53}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FB9A6AA6-FAEF-4DE6-BDE0-C9CC4F02B29A} - EndGlobalSection -EndGlobal diff --git a/Engine.Physics2D/Abstract/ICircleCollider2D.cs b/Engine.Physics2D/Abstract/ICircleCollider2D.cs new file mode 100644 index 0000000..2391e53 --- /dev/null +++ b/Engine.Physics2D/Abstract/ICircleCollider2D.cs @@ -0,0 +1,9 @@ +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface ICircleCollider2D : ICollider2D +{ + Circle CircleLocal { get; set; } + Circle CircleWorld { get; } +} diff --git a/Engine.Physics2D/Abstract/ICollider2D.cs b/Engine.Physics2D/Abstract/ICollider2D.cs new file mode 100644 index 0000000..5d2a5ff --- /dev/null +++ b/Engine.Physics2D/Abstract/ICollider2D.cs @@ -0,0 +1,18 @@ +using System; + +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface ICollider2D : IBehaviour, IAssignableTransform +{ + Action? OnCollisionDetected { get; set; } + Action? OnCollisionResolved { get; set; } + + Action? OnTriggered { get; set; } + + IRigidBody2D? RigidBody2D { get; } + bool IsTrigger { get; set; } + + void Recalculate(); +} diff --git a/Engine.Physics2D/Abstract/ICollisionDetector2D.cs b/Engine.Physics2D/Abstract/ICollisionDetector2D.cs new file mode 100644 index 0000000..d76da2a --- /dev/null +++ b/Engine.Physics2D/Abstract/ICollisionDetector2D.cs @@ -0,0 +1,8 @@ +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public interface ICollisionDetector2D +{ + bool TryDetect(T1 left, T2 right, out CollisionDetectionInformation collisionInformation) where T1 : ICollider2D where T2 : ICollider2D; +} diff --git a/Engine.Physics2D/Abstract/ICollisionResolver2D.cs b/Engine.Physics2D/Abstract/ICollisionResolver2D.cs new file mode 100644 index 0000000..05672a0 --- /dev/null +++ b/Engine.Physics2D/Abstract/ICollisionResolver2D.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface ICollisionResolver2D +{ + void Resolve(CollisionDetectionInformation collisionInformation); +} 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..ef8a5c2 --- /dev/null +++ b/Engine.Physics2D/Abstract/IRigidBody2D.cs @@ -0,0 +1,15 @@ +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; } + bool IsStatic { get; set; } +} diff --git a/Engine.Physics2D/Abstract/IShapeCollider2D.cs b/Engine.Physics2D/Abstract/IShapeCollider2D.cs new file mode 100644 index 0000000..2cc5218 --- /dev/null +++ b/Engine.Physics2D/Abstract/IShapeCollider2D.cs @@ -0,0 +1,9 @@ +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D.Abstract; + +public interface IShapeCollider2D : ICollider2D +{ + Shape ShapeLocal { get; set; } + Shape ShapeWorld { get; } +} diff --git a/Engine.Physics2D/Collider2DBehaviourBase.cs b/Engine.Physics2D/Collider2DBehaviourBase.cs new file mode 100644 index 0000000..e69096f --- /dev/null +++ b/Engine.Physics2D/Collider2DBehaviourBase.cs @@ -0,0 +1,73 @@ +using System; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public abstract class Collider2DBehaviourBase : BehaviourOverride, ICollider2D +{ + public Action? OnCollisionDetected { get; set; } = null; + public Action? OnCollisionResolved { get; set; } = null; + public Action? OnTriggered { get; set; } = null; + + + protected bool NeedsRecalculation { get; private set; } = true; + protected IRigidBody2D? _rigidBody2D = null; + + public IRigidBody2D? RigidBody2D => _rigidBody2D; + public bool IsTrigger { get; set; } = false; + + ITransform IAssignableTransform.Transform => Transform; + Action? IAssignableTransform.OnTransformAssigned { get => GameObject.OnTransformAssigned; set => GameObject.OnTransformAssigned = value; } + + bool IAssignableTransform.Assign(ITransform transform) => GameObject.Assign(transform); + + public void Recalculate() + { + if (!NeedsRecalculation) + return; + + CalculateCollider(); + NeedsRecalculation = false; + } + + public abstract void CalculateCollider(); + + protected override void OnInitialize() + { + BehaviourController.TryGetBehaviour(out _rigidBody2D); + + BehaviourController.OnBehaviourAdded += OnBehaviourAddedToController; + BehaviourController.OnBehaviourRemoved += OnBehaviourRemovedFromController; + + Transform.OnPositionChanged += SetNeedsRecalculation; + Transform.OnRotationChanged += SetNeedsRecalculation; + Transform.OnScaleChanged += SetNeedsRecalculation; + } + + private void OnBehaviourAddedToController(IBehaviourController _, IBehaviour behaviour) + { + if (behaviour is IRigidBody2D rigidBody) + _rigidBody2D = rigidBody; + } + + private void OnBehaviourRemovedFromController(IBehaviourController _, IBehaviour behaviour) + { + if (behaviour is IRigidBody2D _) + _rigidBody2D = null; + } + + private void SetNeedsRecalculation(ITransform transform) => NeedsRecalculation = true; + + protected override void OnFinalize() + { + BehaviourController.OnBehaviourAdded -= OnBehaviourAddedToController; + BehaviourController.OnBehaviourRemoved -= OnBehaviourRemovedFromController; + Transform.OnScaleChanged -= SetNeedsRecalculation; + + Transform.OnPositionChanged -= SetNeedsRecalculation; + Transform.OnRotationChanged -= SetNeedsRecalculation; + } +} diff --git a/Engine.Physics2D/Collider2DCircleBehaviour.cs b/Engine.Physics2D/Collider2DCircleBehaviour.cs new file mode 100644 index 0000000..7314c02 --- /dev/null +++ b/Engine.Physics2D/Collider2DCircleBehaviour.cs @@ -0,0 +1,17 @@ +using Syntriax.Engine.Physics2D.Abstract; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public class Collider2DCircleBehaviour : Collider2DBehaviourBase, ICircleCollider2D +{ + public Circle CircleWorld { get; protected set; } = Circle.UnitCircle; + public Circle CircleLocal { get; set; } = Circle.UnitCircle; + + + public override void CalculateCollider() => CircleWorld = Transform.TransformCircle(CircleLocal); + + + public Collider2DCircleBehaviour() { } + public Collider2DCircleBehaviour(Circle circle) => CircleLocal = circle; +} diff --git a/Engine.Physics2D/Collider2DShapeBehaviour.cs b/Engine.Physics2D/Collider2DShapeBehaviour.cs new file mode 100644 index 0000000..a658b14 --- /dev/null +++ b/Engine.Physics2D/Collider2DShapeBehaviour.cs @@ -0,0 +1,18 @@ +using Syntriax.Engine.Physics2D.Abstract; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public class Collider2DShapeBehaviour : Collider2DBehaviourBase, IShapeCollider2D +{ + public Shape ShapeWorld { get => _shapeWorld; protected set => _shapeWorld = value; } + public Shape ShapeLocal { get; set; } = Shape.Box; + + private Shape _shapeWorld = Shape.Box.CreateCopy(); + + public override void CalculateCollider() => Transform.TransformShape(ShapeLocal, ref _shapeWorld); + + + public Collider2DShapeBehaviour() { } + public Collider2DShapeBehaviour(Shape shape) => ShapeLocal = shape; +} diff --git a/Engine.Physics2D/CollisionDetectionInformation.cs b/Engine.Physics2D/CollisionDetectionInformation.cs new file mode 100644 index 0000000..453932c --- /dev/null +++ b/Engine.Physics2D/CollisionDetectionInformation.cs @@ -0,0 +1,19 @@ +using Syntriax.Engine.Core; +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +[System.Diagnostics.DebuggerDisplay("Normal: {Normal.ToString(), nq}, Penetration: {Penetration}")] +public readonly struct CollisionDetectionInformation +( + ICollider2D Left, + ICollider2D Right, + Vector2D Normal, + float Penetration +) +{ + public ICollider2D Left { get; init; } = Left; + public ICollider2D Right { get; init; } = Right; + public Vector2D Normal { get; init; } = Normal; + public float Penetration { get; init; } = Penetration; +} diff --git a/Engine.Physics2D/CollisionDetector2D.cs b/Engine.Physics2D/CollisionDetector2D.cs new file mode 100644 index 0000000..1f9c332 --- /dev/null +++ b/Engine.Physics2D/CollisionDetector2D.cs @@ -0,0 +1,136 @@ +using Syntriax.Engine.Core; +using Syntriax.Engine.Physics2D.Abstract; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Syntriax.Engine.Physics2D; + +public class CollisionDetector2D : ICollisionDetector2D +{ + public bool TryDetect(T1 left, T2 right, out CollisionDetectionInformation collisionInformation) + where T1 : ICollider2D + where T2 : ICollider2D + { + collisionInformation = default; + if (left is IShapeCollider2D shapeColliderLeft) + { + if (right is IShapeCollider2D shapeColliderRight) + return DetectShapeShape(shapeColliderLeft, shapeColliderRight, out collisionInformation); + else if (right is ICircleCollider2D circleColliderRight) + return DetectShapeCircle(shapeColliderLeft, circleColliderRight, out collisionInformation); + } + else if (left is ICircleCollider2D circleColliderLeft) + { + if (right is IShapeCollider2D shapeColliderRight) + return DetectCircleShape(circleColliderLeft, shapeColliderRight, out collisionInformation); + else if (right is ICircleCollider2D circleColliderRight) + return DetectCircleCircle(circleColliderLeft, circleColliderRight, out collisionInformation); + } + + return false; + } + + private static bool DetectCircleShape(ICircleCollider2D circleCollider, IShapeCollider2D shapeCollider, out CollisionDetectionInformation collisionInformation) + { + return DetectShapeCircle(shapeCollider, circleCollider, out collisionInformation); + } + + private static bool DetectShapeShape(IShapeCollider2D left, IShapeCollider2D right, out CollisionDetectionInformation collisionInformation) + { + collisionInformation = default; + return DetectShapeShapeOneWay(left, right, ref collisionInformation) && DetectShapeShapeOneWay(right, left, ref collisionInformation); + } + + private static bool DetectShapeShapeOneWay(IShapeCollider2D left, IShapeCollider2D right, ref CollisionDetectionInformation collisionInformation) + { + var vertices = left.ShapeWorld.Vertices; + int count = vertices.Count; + + for (int indexProjection = 0; indexProjection < count; indexProjection++) + { + Vector2D projectionVector = vertices[indexProjection].FromTo(vertices[(indexProjection + 1) % count]).Perpendicular().Normalized; + + Projection leftProjection = left.ShapeWorld.ToProjection(projectionVector); + Projection rightProjection = right.ShapeWorld.ToProjection(projectionVector); + + if (!leftProjection.Overlaps(rightProjection, out float depth)) + return false; + + if (collisionInformation.Left is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth)) + collisionInformation = new(left, right, projectionVector, depth); + } + + return true; + } + + private static bool DetectShapeCircle(IShapeCollider2D shapeCollider, ICircleCollider2D circleCollider, out CollisionDetectionInformation collisionInformation) + { + collisionInformation = default; + + var vertices = shapeCollider.ShapeWorld.Vertices; + int count = vertices.Count; + + for (int indexProjection = 0; indexProjection < count; indexProjection++) + { + Vector2D projectionVector = vertices[indexProjection].FromTo(vertices[(indexProjection + 1) % count]).Perpendicular().Normalized; + + Projection shapeProjection = shapeCollider.ShapeWorld.ToProjection(projectionVector); + Projection circleProjection = circleCollider.CircleWorld.ToProjection(projectionVector); + + if (!shapeProjection.Overlaps(circleProjection, out float depth)) + return false; + + if (collisionInformation.Left is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth)) + collisionInformation = new(shapeCollider, circleCollider, projectionVector, depth); + } + + { + Vector2D shapeToCircleProjectionVector = shapeCollider.Transform.Position.FromTo(circleCollider.CircleWorld.Center).Normalized; + + Projection shapeProjection = shapeCollider.ShapeWorld.ToProjection(shapeToCircleProjectionVector); + Projection circleProjection = circleCollider.CircleWorld.ToProjection(shapeToCircleProjectionVector); + + if (!shapeProjection.Overlaps(circleProjection, out float depth)) + return false; + + if (collisionInformation.Left is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth)) + collisionInformation = new(shapeCollider, circleCollider, shapeToCircleProjectionVector, depth); + } + + return true; + } + + private static bool DetectCircleCircle(ICircleCollider2D left, ICircleCollider2D right, out CollisionDetectionInformation collisionInformation) + { + collisionInformation = default; + + Vector2D leftToRightCenterProjectionVector = left.CircleWorld.Center.FromTo(right.CircleWorld.Center).Normalized; + + Projection leftProjection = left.CircleWorld.ToProjection(leftToRightCenterProjectionVector); + Projection rightProjection = right.CircleWorld.ToProjection(leftToRightCenterProjectionVector); + + bool collision = leftProjection.Overlaps(rightProjection, out float depth); + + if (collision) + collisionInformation = new(left, right, leftToRightCenterProjectionVector, depth); + + return collision; + } + + // private static bool DetectCircleCircle(ICircleCollider2D left, ICircleCollider2D right, out CollisionDetectionInformation collisionInformation) + // { + // collisionInformation = default; + + // Vector2D leftToRightCenter = left.CircleWorld.Center.FromTo(right.CircleWorld.Center); + // float distanceCircleCenter = leftToRightCenter.Magnitude; + // float radiusSum = left.CircleWorld.Radius + right.CircleWorld.Radius; + + // float circleSurfaceDistance = distanceCircleCenter - radiusSum; + + // bool collision = circleSurfaceDistance <= 0f; + + // if (collision) + // collisionInformation = new(left, right, leftToRightCenter.Normalized, -circleSurfaceDistance); + + // return collision; + // } +} diff --git a/Engine.Physics2D/CollisionResolver2D.cs b/Engine.Physics2D/CollisionResolver2D.cs new file mode 100644 index 0000000..4cb6c04 --- /dev/null +++ b/Engine.Physics2D/CollisionResolver2D.cs @@ -0,0 +1,44 @@ +using Syntriax.Engine.Core; +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public class CollisionResolver2D : ICollisionResolver2D +{ + public void Resolve(CollisionDetectionInformation collisionInformation) + { + Vector2D displacementVector = collisionInformation.Normal * collisionInformation.Penetration; + + ICollider2D left = collisionInformation.Left; + ICollider2D right = collisionInformation.Right; + + bool isLeftStatic = left.RigidBody2D?.IsStatic ?? true; + bool isRightStatic = right.RigidBody2D?.IsStatic ?? true; + + if (isLeftStatic && isRightStatic) + return; + + if (isLeftStatic) + right.Transform.Position += displacementVector; + else if (isRightStatic) + left.Transform.Position -= displacementVector; + else + { + float leftMass = left.RigidBody2D?.Mass ?? float.Epsilon; + float rightMass = right.RigidBody2D?.Mass ?? float.Epsilon; + float sumMass = leftMass + rightMass; + + float leftMomentumPercentage = leftMass / sumMass; + float rightMomentumPercentage = rightMass / sumMass; + + right.Transform.Position += leftMomentumPercentage * displacementVector; + left.Transform.Position -= rightMomentumPercentage * displacementVector; + } + + left.Recalculate(); + right.Recalculate(); + + left.OnCollisionResolved?.Invoke(collisionInformation.Left, collisionInformation); + right.OnCollisionResolved?.Invoke(right, collisionInformation); + } +} 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/Physics2D.cs b/Engine.Physics2D/Physics2D.cs new file mode 100644 index 0000000..a9f722b --- /dev/null +++ b/Engine.Physics2D/Physics2D.cs @@ -0,0 +1,74 @@ +using System; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Physics2D; +using Syntriax.Engine.Physics2D.Primitives; + +namespace Engine.Physics2D; + +public static partial class Physics2D +{ + public static bool Overlaps(this Circle left, Circle right) + { + float distanceSquared = left.Center.FromTo(right.Center).LengthSquared(); + float radiusSumSquared = left.RadiusSquared + right.RadiusSquared; + + return distanceSquared < radiusSumSquared; + } + + public static bool Overlaps(this Circle left, Circle right, out Vector2D normal, out float depth) + { + Vector2D distanceVector = left.Center.FromTo(right.Center); + float distanceSquared = distanceVector.LengthSquared(); + float radiusSumSquared = left.RadiusSquared + right.RadiusSquared; + bool isOverlapping = distanceSquared < radiusSumSquared; + + depth = 0f; + normal = distanceVector.Normalized; + + if (isOverlapping) + depth = MathF.Sqrt(radiusSumSquared - distanceSquared); + + return isOverlapping; + } + + public static bool Overlaps(this Circle circle, Vector2D point) => circle.Center.FromTo(point).LengthSquared() <= circle.RadiusSquared; + public static bool Overlaps(this Circle circle, Vector2D point, out Vector2D normal, out float depth) + { + Vector2D distanceVector = circle.Center.FromTo(point); + float distanceSquared = distanceVector.LengthSquared(); + float radiusSquared = circle.RadiusSquared; + bool isOverlapping = distanceSquared < radiusSquared; + + depth = 0f; + normal = distanceVector.Normalized; + + if (isOverlapping) + depth = MathF.Sqrt(radiusSquared - distanceSquared); + + return isOverlapping; + } + + public static bool Overlaps(this AABB aabb, Vector2D point) + => point.X >= aabb.LowerBoundary.X && point.X <= aabb.UpperBoundary.X && + point.Y >= aabb.LowerBoundary.Y && point.Y <= aabb.UpperBoundary.Y; + + public static bool Overlaps(this AABB left, AABB right) + => left.LowerBoundary.X <= right.UpperBoundary.X && left.UpperBoundary.X >= right.LowerBoundary.X && + left.LowerBoundary.Y <= right.UpperBoundary.Y && left.UpperBoundary.Y >= right.LowerBoundary.Y; + + public static bool Overlaps(Triangle triangle, Vector2D point) + { + float originalTriangleArea = triangle.Area; + + float pointTriangleArea1 = new Triangle(point, triangle.B, triangle.C).Area; + float pointTriangleArea2 = new Triangle(triangle.A, point, triangle.C).Area; + float pointTriangleArea3 = new Triangle(triangle.A, triangle.B, point).Area; + + float pointTriangleAreasSum = pointTriangleArea1 + pointTriangleArea2 + pointTriangleArea3; + + return originalTriangleArea.ApproximatelyEquals(pointTriangleAreasSum, float.Epsilon * 3f); + } + + public static bool LaysOn(this Vector2D point, Line line) => Line.Intersects(line, point); +} diff --git a/Engine.Physics2D/PhysicsEngine2D.cs b/Engine.Physics2D/PhysicsEngine2D.cs new file mode 100644 index 0000000..c61af05 --- /dev/null +++ b/Engine.Physics2D/PhysicsEngine2D.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public class PhysicsEngine2D : IPhysicsEngine2D +{ + private readonly List rigidBodies = new(32); + private readonly List colliders = new(64); + + private int _iterationCount = 1; + private ICollisionDetector2D collisionDetector = new CollisionDetector2D(); + private ICollisionResolver2D collisionResolver = new CollisionResolver2D(); + + 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++) + { + // Can Parallel + for (int i = 0; i < rigidBodies.Count; i++) + StepRigidBody(rigidBodies[i], intervalDeltaTime); + + // Can Parallel + foreach (var collider in colliders) + collider.Recalculate(); + + // Can Parallel + for (int x = 0; x < colliders.Count; x++) + { + ICollider2D? colliderX = colliders[x]; + if (!colliderX.IsActive) + return; + + for (int y = x + 1; y < colliders.Count; y++) + { + ICollider2D? colliderY = colliders[y]; + + if (!colliderY.IsActive) + return; + + if (colliderX.RigidBody2D == colliderY.RigidBody2D) + continue; + + bool bothCollidersAreTriggers = colliderX.IsTrigger && colliderX.IsTrigger == colliderY.IsTrigger; + if (bothCollidersAreTriggers) + continue; + + bool bothCollidersAreStatic = colliderX.RigidBody2D?.IsStatic ?? true && colliderX.RigidBody2D?.IsStatic == colliderY.RigidBody2D?.IsStatic; + if (bothCollidersAreStatic) + continue; + + if (collisionDetector.TryDetect(colliderX, colliderY, out CollisionDetectionInformation information)) + { + if (colliderX.IsTrigger) + { + colliderX.OnTriggered?.Invoke(colliderX, colliderY); + continue; + } + else if (colliderY.IsTrigger) + { + colliderY.OnTriggered?.Invoke(colliderY, colliderY); + continue; + } + + colliderX.OnCollisionDetected?.Invoke(colliderX, information); + colliderY.OnCollisionDetected?.Invoke(colliderY, information); + + collisionResolver?.Resolve(information); + } + } + } + } + } + + private static void StepRigidBody(IRigidBody2D rigidBody, float intervalDeltaTime) + { + if (rigidBody.IsStatic) + return; + + 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..23e462e --- /dev/null +++ b/Engine.Physics2D/PhysicsMaterial2D.cs @@ -0,0 +1,9 @@ +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public readonly struct PhysicsMaterial2D(float Friction, float Restitution) : IPhysicsMaterial2D +{ + public readonly float Friction { get; init; } = Friction; + public readonly float Restitution { get; init; } = Restitution; +} diff --git a/Engine.Physics2D/PhysicsMaterial2DDefault.cs b/Engine.Physics2D/PhysicsMaterial2DDefault.cs new file mode 100644 index 0000000..41d1709 --- /dev/null +++ b/Engine.Physics2D/PhysicsMaterial2DDefault.cs @@ -0,0 +1,10 @@ +using Syntriax.Engine.Physics2D.Abstract; + +namespace Syntriax.Engine.Physics2D; + +public readonly struct PhysicsMaterial2DDefault : IPhysicsMaterial2D +{ + public readonly float Friction => .1f; + + public readonly float Restitution => .1f; +} diff --git a/Engine.Physics2D/Primitives/AABB.cs b/Engine.Physics2D/Primitives/AABB.cs new file mode 100644 index 0000000..e165fd8 --- /dev/null +++ b/Engine.Physics2D/Primitives/AABB.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("LowerBoundary: {LowerBoundary.ToString(), nq}, UpperBoundary: {UpperBoundary.ToString(), nq}")] +public readonly struct AABB(Vector2D LowerBoundary, Vector2D UpperBoundary) +{ + public readonly Vector2D LowerBoundary { get; init; } = LowerBoundary; + public readonly Vector2D UpperBoundary { get; init; } = UpperBoundary; + + public readonly Vector2D Center => (LowerBoundary + UpperBoundary) * .5f; + public readonly Vector2D Size => LowerBoundary.FromTo(UpperBoundary).Abs(); + public readonly Vector2D SizeHalf => Size * .5f; + + public static AABB FromVectors(IEnumerable vectors) + { + int counter = 0; + + Vector2D lowerBoundary = new(float.MaxValue, float.MaxValue); + Vector2D upperBoundary = new(float.MinValue, float.MinValue); + + foreach (Vector2D vector in vectors) + { + lowerBoundary = Vector2D.Min(lowerBoundary, vector); + upperBoundary = Vector2D.Max(upperBoundary, vector); + counter++; + } + + if (counter < 2) + throw new System.ArgumentException($"Parameter {nameof(vectors)} must have at least 2 items."); + + return new(lowerBoundary, upperBoundary); + } + + public static bool ApproximatelyEquals(AABB left, AABB right) + => left.LowerBoundary.ApproximatelyEquals(right.LowerBoundary) && left.UpperBoundary.ApproximatelyEquals(right.UpperBoundary); +} + +public static class AABBExtensions +{ + public static AABB ToAABB(this IEnumerable vectors) => AABB.FromVectors(vectors); + + public static bool ApproximatelyEquals(this AABB left, AABB right) => AABB.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/Primitives/Circle.cs b/Engine.Physics2D/Primitives/Circle.cs new file mode 100644 index 0000000..2be2b56 --- /dev/null +++ b/Engine.Physics2D/Primitives/Circle.cs @@ -0,0 +1,47 @@ +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("Center: {Center.ToString(), nq}, Radius: {Radius}")] +public readonly struct Circle(Vector2D Center, float Radius) +{ + public static readonly Circle UnitCircle = new(Vector2D.Zero, 1f); + + public readonly Vector2D Center { get; init; } = Center; + public readonly float Radius { get; init; } = Radius; + + public readonly float RadiusSquared => Radius * Radius; + public readonly float Diameter => 2f * Radius; + + public static Circle SetCenter(Circle circle, Vector2D center) => new(center, circle.Radius); + public static Circle SetRadius(Circle circle, float radius) => new(circle.Center, radius); + + public static Circle Displace(Circle circle, Vector2D displaceVector) => new(circle.Center + displaceVector, circle.Radius); + + public static Projection Project(Circle circle, Vector2D projectionVector) + { + float projectedCenter = circle.Center.Dot(projectionVector); + return new(projectedCenter - circle.Radius, projectedCenter + circle.Radius); + } + + public static Circle TransformCircle(ITransform transform, Circle circle) + => new(transform.TransformVector2D(circle.Center), circle.Radius * transform.Scale.Magnitude); + + public static bool ApproximatelyEquals(Circle left, Circle right) + => left.Center.ApproximatelyEquals(right.Center) && left.Radius.ApproximatelyEquals(right.Radius); +} + +public static class CircleExtensions +{ + public static Circle SetCenter(this Circle circle, Vector2D center) => Circle.SetCenter(circle, center); + public static Circle SetRadius(this Circle circle, float radius) => Circle.SetRadius(circle, radius); + + public static Circle Displace(this Circle circle, Vector2D displaceVector) => Circle.Displace(circle, displaceVector); + + public static Projection ToProjection(this Circle circle, Vector2D projectionVector) => Circle.Project(circle, projectionVector); + + public static Circle TransformCircle(this ITransform transform, Circle circle) => Circle.TransformCircle(transform, circle); + + public static bool ApproximatelyEquals(this Circle left, Circle right) => Circle.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/Primitives/Line.cs b/Engine.Physics2D/Primitives/Line.cs new file mode 100644 index 0000000..7d0f1ec --- /dev/null +++ b/Engine.Physics2D/Primitives/Line.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("From: {From.ToString(), nq}, To: {To.ToString(), nq}, Direction: {Direction.ToString(), nq}, Length: {Length}")] +public readonly struct Line(Vector2D From, Vector2D To) +{ + public readonly Vector2D From { get; init; } = From; + public readonly Vector2D To { get; init; } = To; + + public readonly Line Reversed => new(To, From); + public readonly Vector2D Direction => From.FromTo(To).Normalize(); + public readonly float Length => From.FromTo(To).Length(); + public readonly float LengthSquared => From.FromTo(To).LengthSquared(); + + public static LineEquation GetLineEquation(Line line) + { + Vector2D slopeVector = line.From.FromTo(line.To); + float slope = slopeVector.Y / slopeVector.X; + + float yOffset = line.From.Y - (slope * line.From.X); + + return new LineEquation(slope, yOffset); + } + + public static bool Intersects(Line line, Vector2D point) + => LineEquation.Resolve(GetLineEquation(line), point.X).ApproximatelyEquals(point.Y); + + public static float GetT(Line line, Vector2D point) + { + float fromX = MathF.Abs(line.From.X); + float toX = MathF.Abs(line.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(line, t).ApproximatelyEquals(point)) + return 1f - t; + return t; + } + + public static bool Exist(Line line, List vertices) + { + for (int i = 0; i < vertices.Count - 1; i++) + { + Vector2D vertexCurrent = vertices[i]; + Vector2D vertexNext = vertices[i]; + if (line.From == vertexCurrent && line.To == vertexNext) return true; + if (line.From == vertexNext && line.To == vertexCurrent) return true; + } + + Vector2D vertexFirst = vertices[0]; + Vector2D vertexLast = vertices[^1]; + if (line.From == vertexFirst && line.To == vertexLast) return true; + if (line.From == vertexLast && line.To == vertexFirst) return true; + return false; + } + + public static float IntersectionParameterT(Line left, Line right) + { + float numerator = (left.From.X - right.From.X) * (right.From.Y - right.To.Y) - (left.From.Y - right.From.Y) * (right.From.X - right.To.X); + float denominator = (left.From.X - left.To.X) * (right.From.Y - right.To.Y) - (left.From.Y - left.To.Y) * (right.From.X - right.To.X); + + // Lines are parallel + if (denominator == 0) + return float.NaN; + + return numerator / denominator; + } + + public static Vector2D Lerp(Line line, float t) + => new Vector2D( + line.From.X + (line.To.X - line.From.X) * t, + line.From.Y + (line.To.Y - line.From.Y) * t + ); + + public static Vector2D ClosestPointTo(Line line, Vector2D point) + { + // Convert edge points to vectors + var edgeVector = new Vector2D(line.To.X - line.From.X, line.To.Y - line.From.Y); + var pointVector = new Vector2D(point.X - line.From.X, point.Y - line.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 = line.From.X + t * edgeVector.X; + float closestY = line.From.Y + t * edgeVector.Y; + + return new Vector2D((float)closestX, (float)closestY); + } + + public static Vector2D IntersectionPoint(Line left, Line right) + => Vector2D.Lerp(left.From, left.To, IntersectionParameterT(left, right)); + + public static bool Intersects(Line left, Line right) + { + int o1 = Vector2D.Orientation(left.From, left.To, right.From); + int o2 = Vector2D.Orientation(left.From, left.To, right.To); + int o3 = Vector2D.Orientation(right.From, right.To, left.From); + int o4 = Vector2D.Orientation(right.From, right.To, left.To); + + if (o1 != o2 && o3 != o4) + return true; + + if (o1 == 0 && OnSegment(left, right.From)) return true; + if (o2 == 0 && OnSegment(left, right.To)) return true; + if (o3 == 0 && OnSegment(right, left.From)) return true; + if (o4 == 0 && OnSegment(right, left.To)) return true; + + return false; + } + + public static bool OnSegment(Line line, Vector2D point) + { + if (point.X <= MathF.Max(line.From.X, line.To.X) && point.X >= MathF.Min(line.From.X, line.To.X) && + point.Y <= MathF.Max(line.From.Y, line.To.Y) && point.Y >= MathF.Min(line.From.Y, line.To.Y)) + return true; + + return false; + } + + public static bool Intersects(Line left, Line right, [NotNullWhen(returnValue: true)] out Vector2D? point) + { + point = null; + + bool result = Intersects(left, right); + + if (result) + point = IntersectionPoint(left, right); + + return result; + } + + public static bool ApproximatelyEquals(Line left, Line right) + => left.From.ApproximatelyEquals(right.From) && left.To.ApproximatelyEquals(right.To); +} + +public static class LineExtensions +{ + public static bool ApproximatelyEquals(this Line left, Line right) => Line.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/Primitives/LineEquation.cs b/Engine.Physics2D/Primitives/LineEquation.cs new file mode 100644 index 0000000..3a3e417 --- /dev/null +++ b/Engine.Physics2D/Primitives/LineEquation.cs @@ -0,0 +1,21 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("y = {Slope}x + {OffsetY}")] +public readonly struct LineEquation(float Slope, float OffsetY) +{ + public readonly float Slope { get; init; } = Slope; + public readonly float OffsetY { get; init; } = OffsetY; + + public static float Resolve(LineEquation lineEquation, float x) => lineEquation.Slope * x + lineEquation.OffsetY; // y = mx + b + + public static bool ApproximatelyEquals(LineEquation left, LineEquation right) + => left.Slope.ApproximatelyEquals(right.Slope) && left.OffsetY.ApproximatelyEquals(right.OffsetY); +} + +public static class LineEquationExtensions +{ + public static float Resolve(this LineEquation lineEquation, float x) => LineEquation.Resolve(lineEquation, x); + public static bool ApproximatelyEquals(this LineEquation left, LineEquation right) => LineEquation.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/Primitives/Projection.cs b/Engine.Physics2D/Primitives/Projection.cs new file mode 100644 index 0000000..2f0af58 --- /dev/null +++ b/Engine.Physics2D/Primitives/Projection.cs @@ -0,0 +1,49 @@ +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("Min: {Min}, Max: {Max}")] +public readonly struct Projection(float Min, float Max) +{ + public readonly float Min { get; init; } = Min; + public readonly float Max { get; init; } = Max; + + public static bool Overlaps(Projection left, Projection right) => Overlaps(left, right, out var _); + public static bool Overlaps(Projection left, Projection right, out float depth) + { + // TODO Try to improve this + bool rightMinInLeft = right.Min > left.Min && right.Min < left.Max; + if (rightMinInLeft) + { + depth = left.Max - right.Min; + return true; + } + + bool rightMaxInLeft = right.Max < left.Max && right.Max > left.Min; + if (rightMaxInLeft) + { + depth = left.Min - right.Max; + return true; + } + + bool leftMinInRight = left.Min > right.Min && left.Min < right.Max; + if (leftMinInRight) + { + depth = right.Max - left.Min; + return true; + } + + bool leftMaxInRight = left.Max < right.Max && left.Max > right.Min; + if (leftMaxInRight) + { + depth = right.Min - left.Max; + return true; + } + + depth = 0f; + return false; + } +} +public static class ProjectionExtensions +{ + public static bool Overlaps(this Projection left, Projection right) => Projection.Overlaps(left, right); + public static bool Overlaps(this Projection left, Projection right, out float depth) => Projection.Overlaps(left, right, out depth); +} diff --git a/Engine.Physics2D/Primitives/Shape.cs b/Engine.Physics2D/Primitives/Shape.cs new file mode 100644 index 0000000..d332ab0 --- /dev/null +++ b/Engine.Physics2D/Primitives/Shape.cs @@ -0,0 +1,155 @@ +using System.Collections; +using System.Collections.Generic; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("Vertices Count: {Vertices.Count}")] +public readonly struct Shape(IList Vertices) : IEnumerable +{ + public static readonly Shape Triangle = CreateNgon(3, Vector2D.Up); + public static readonly Shape Box = CreateNgon(4, Vector2D.One); + public static readonly Shape Pentagon = CreateNgon(5, Vector2D.Up); + public static readonly Shape Hexagon = CreateNgon(6, Vector2D.Right); + + public readonly IList Vertices { get; init; } = Vertices; + + + public Vector2D this[System.Index index] => Vertices[index]; + + public static Shape CreateCopy(Shape shape) => new(new List(shape.Vertices)); + + public static Shape CreateNgon(int vertexCount) => CreateNgon(vertexCount, Vector2D.Up); + public static Shape CreateNgon(int vertexCount, Vector2D positionToRotate) + { + if (vertexCount < 3) + throw new System.ArgumentException($"{nameof(vertexCount)} must have a value of more than 2."); + + List vertices = new(vertexCount); + + float radiansPerVertex = 360f / vertexCount * Math.DegreeToRadian; + + for (int i = 0; i < vertexCount; i++) + vertices.Add(positionToRotate.Rotate(i * radiansPerVertex)); + + return new(vertices); + } + + public static Triangle GetSuperTriangle(Shape shape) + { + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + foreach (Vector2D point in shape.Vertices) + { + minX = Math.Min(minX, point.X); + minY = Math.Min(minY, point.Y); + maxX = Math.Max(maxX, point.X); + maxY = Math.Max(maxY, point.Y); + } + + float dx = maxX - minX; + float dy = maxY - minY; + float deltaMax = Math.Max(dx, dy); + float midX = (minX + maxX) / 2; + float midY = (minY + maxY) / 2; + + Vector2D p1 = new((float)midX - 20f * (float)deltaMax, (float)midY - (float)deltaMax); + Vector2D p2 = new((float)midX, (float)midY + 20 * (float)deltaMax); + Vector2D p3 = new((float)midX + 20 * (float)deltaMax, (float)midY - (float)deltaMax); + + return new Triangle(p1, p2, p3); + } + + public static void GetLines(Shape shape, IList lines) + { + lines.Clear(); + for (int i = 0; i < shape.Vertices.Count - 1; i++) + lines.Add(new(shape.Vertices[i], shape.Vertices[i + 1])); + lines.Add(new(shape.Vertices[^1], shape.Vertices[0])); + } + + public static List GetLines(Shape shape) + { + List lines = new(shape.Vertices.Count - 1); + GetLines(shape, lines); + return lines; + } + + public static void Project(Shape shape, Vector2D projectionVector, IList list) + { + list.Clear(); + + int count = shape.Vertices.Count; + for (int i = 0; i < count; i++) + list.Add(projectionVector.Dot(shape[i])); + } + + public static Projection Project(Shape shape, Vector2D projectionVector) + { + float min = float.MaxValue; + float max = float.MinValue; + + for (int i = 0; i < shape.Vertices.Count; i++) + { + float projectedLength = projectionVector.Dot(shape.Vertices[i]); + min = Math.Min(projectedLength, min); + max = Math.Max(projectedLength, max); + } + + return new(min, max); + } + + public static Shape TransformShape(Shape shape, ITransform transform) + { + List vertices = new(shape.Vertices.Count); + + int count = shape.Vertices.Count; + for (int i = 0; i < count; i++) + vertices.Add(transform.TransformVector2D(shape[i])); + + return new Shape(vertices); + } + + public static void TransformShape(Shape from, ITransform transform, ref Shape to) + { + to.Vertices.Clear(); + + int count = from.Vertices.Count; + for (int i = 0; i < count; i++) + to.Vertices.Add(transform.TransformVector2D(from[i])); + } + + public static bool ApproximatelyEquals(Shape left, Shape right) + { + if (left.Vertices.Count != right.Vertices.Count) + return false; + + for (int i = 0; i < left.Vertices.Count; i++) + if (!left.Vertices[i].ApproximatelyEquals(right.Vertices[i])) + return false; + + return true; + } + + public IEnumerator GetEnumerator() => Vertices.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Vertices.GetEnumerator(); +} + +public static class ShapeExtensions +{ + public static Shape CreateCopy(this Shape shape) => Shape.CreateCopy(shape); + public static Triangle ToSuperTriangle(this Shape shape) => Shape.GetSuperTriangle(shape); + public static void ToLines(this Shape shape, IList lines) => Shape.GetLines(shape, lines); + public static List ToLines(this Shape shape) => Shape.GetLines(shape); + + public static void ToProjection(this Shape shape, Vector2D projectionVector, IList list) => Shape.Project(shape, projectionVector, list); + public static Projection ToProjection(this Shape shape, Vector2D projectionVector) => Shape.Project(shape, projectionVector); + + public static Shape TransformShape(this ITransform transform, Shape shape) => Shape.TransformShape(shape, transform); + public static void TransformShape(this ITransform transform, Shape from, ref Shape to) => Shape.TransformShape(from, transform, ref to); + + public static bool ApproximatelyEquals(this Shape left, Shape right) => Shape.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/Primitives/Triangle.cs b/Engine.Physics2D/Primitives/Triangle.cs new file mode 100644 index 0000000..9695460 --- /dev/null +++ b/Engine.Physics2D/Primitives/Triangle.cs @@ -0,0 +1,49 @@ +using System; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Physics2D.Primitives; + +[System.Diagnostics.DebuggerDisplay("A: {A.ToString(), nq}, B: {B.ToString(), nq}, B: {C.ToString(), nq}")] +public readonly struct Triangle(Vector2D A, Vector2D B, Vector2D C) +{ + public readonly Vector2D A { get; init; } = A; + public readonly Vector2D B { get; init; } = B; + public readonly Vector2D C { get; init; } = C; + + public readonly float Area + => .5f * MathF.Abs( + A.X * (B.Y - C.Y) + + B.X * (C.Y - A.Y) + + C.X * (A.Y - B.Y) + ); + + public static Circle GetCircumCircle(Triangle triangle) + { + Vector2D midAB = (triangle.A + triangle.B) / 2f; + Vector2D midBC = (triangle.B + triangle.C) / 2f; + + float slopeAB = (triangle.B.Y - triangle.A.Y) / (triangle.B.X - triangle.A.X); + float slopeBC = (triangle.C.Y - triangle.B.Y) / (triangle.C.X - triangle.B.X); + + Vector2D center; + if (MathF.Abs(slopeAB - slopeBC) > float.Epsilon) + { + float x = (slopeAB * slopeBC * (triangle.A.Y - triangle.C.Y) + slopeBC * (triangle.A.X + triangle.B.X) - slopeAB * (triangle.B.X + triangle.C.X)) / (2f * (slopeBC - slopeAB)); + float y = -(x - (triangle.A.X + triangle.B.X) / 2f) / slopeAB + (triangle.A.Y + triangle.B.Y) / 2f; + center = new Vector2D(x, y); + } + else + center = (midAB + midBC) * .5f; + + return new(center, Vector2D.Distance(center, triangle.A)); + } + + public static bool ApproximatelyEquals(Triangle left, Triangle right) + => left.A.ApproximatelyEquals(right.A) && left.B.ApproximatelyEquals(right.B) && left.C.ApproximatelyEquals(right.C); +} + +public static class TriangleExtensions +{ + public static bool ApproximatelyEquals(this Triangle left, Triangle right) => Triangle.ApproximatelyEquals(left, right); +} diff --git a/Engine.Physics2D/RigidBody2D.cs b/Engine.Physics2D/RigidBody2D.cs new file mode 100644 index 0000000..3820170 --- /dev/null +++ b/Engine.Physics2D/RigidBody2D.cs @@ -0,0 +1,29 @@ +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; } + + private const float LOWEST_ALLOWED_MASS = 0.00001f; + private float _mass = 1f; + + + public IPhysicsMaterial2D Material { get; set; } = new PhysicsMaterial2DDefault(); + + public Vector2D Velocity { get; set; } = Vector2D.Zero; + public float AngularVelocity { get; set; } = 0f; + public bool IsStatic { get; set; } = false; + + public float Mass { get => _mass; set => _mass = Core.Math.Max(value, LOWEST_ALLOWED_MASS); } + + 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