├── Resources ├── logo-square-256px.png ├── logo-square-512px.png ├── logo.svg └── logo-square.svg ├── .gitignore ├── Boing ├── Boing.csproj.DotSettings ├── Boing.csproj ├── IForce.cs ├── Forces │ ├── ViscousForce.cs │ ├── FlowDownwardForce.cs │ ├── OriginAttractionForce.cs │ ├── ColoumbForce.cs │ ├── KeepWithinBoundsForce.cs │ └── Spring.cs ├── FixedTimeStepUpdater.cs ├── LineSegment2f.cs ├── Simulation.cs ├── Vector2f.cs ├── Rectangle2f.cs └── PointMass.cs ├── Directory.Build.targets ├── Boing.Tests ├── Boing.Tests.csproj ├── Rectangle2fTest.cs └── ReadmeSample.cs ├── Directory.Build.props ├── Boing.sln ├── README.md └── LICENSE /Resources/logo-square-256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewnoakes/boing/HEAD/Resources/logo-square-256px.png -------------------------------------------------------------------------------- /Resources/logo-square-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewnoakes/boing/HEAD/Resources/logo-square-512px.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | *.user 4 | *.suo 5 | *.nupkg 6 | packages/ 7 | /.vs/ 8 | project.lock.json 9 | /.idea/ 10 | -------------------------------------------------------------------------------- /Boing/Boing.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Boing.Tests/Boing.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.0 5 | portable 6 | Boing.Tests 7 | Boing.Tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Boing/Boing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A simple library for 2D physics simulations in .NET. 5 | net35;netstandard1.0 6 | true 7 | true 8 | true 9 | Physics simulation;Physics;Physics engine;Physics simulation library;Simulation 10 | https://github.com/drewnoakes/boing 11 | git 12 | https://github.com/drewnoakes/boing.git 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright Drew Noakes, Krzysztof Dul 2015-2020 5 | 1.1.0 6 | Drew Noakes;Krzysztof Dul 7 | 8 8 | enable 9 | Apache-2.0 10 | logo-square-512px.png 11 | portable 12 | true 13 | true 14 | true 15 | snupkg 16 | true 17 | true 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Boing/IForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | // TODO new local force: axis-align force for pairs of point masses 22 | // TODO new local force: hysteresis spring force 23 | 24 | /// 25 | /// A force that potentially applies to every in the . 26 | /// 27 | public interface IForce 28 | { 29 | /// 30 | /// Evaluates the impact of this force on the simulation at the current point in time, 31 | /// and applies forces to point masses as required. 32 | /// 33 | /// 34 | /// Implementations should call on point masses. 35 | /// They can update any number of point masses, or none at all. The force may be identical across 36 | /// all point masses, but is more likely to vary from point to point. 37 | /// 38 | /// The simulation to apply this force to. 39 | void ApplyTo(Simulation simulation); 40 | } 41 | } -------------------------------------------------------------------------------- /Boing/Forces/ViscousForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | /// 22 | /// A force that acts upon point masses as a viscous environment would, exerting a force 23 | /// that inhibits movement and is proportional to point mass velocity. 24 | /// 25 | public sealed class ViscousForce : IForce 26 | { 27 | /// 28 | /// Gets and sets the coefficient of viscosity. 29 | /// 30 | public float Coefficient { get; set; } 31 | 32 | /// 33 | /// Initialises a new instance of . 34 | /// 35 | /// The initial coefficient of viscosity. 36 | public ViscousForce(float coefficient) 37 | { 38 | Coefficient = coefficient; 39 | } 40 | 41 | /// 42 | void IForce.ApplyTo(Simulation simulation) 43 | { 44 | foreach (var pointMass in simulation.PointMasses) 45 | { 46 | if (pointMass.IsPinned) 47 | continue; 48 | 49 | pointMass.ApplyForce(pointMass.Velocity*-Coefficient); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Boing/Forces/FlowDownwardForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | /// 22 | /// A constant downwards force applied to all non-pinned points, akin to gravity. 23 | /// 24 | public sealed class FlowDownwardForce : IForce 25 | { 26 | /// 27 | /// The magnitude of the downward force, in Newtons. 28 | /// 29 | public float Magnitude { get; set; } 30 | 31 | /// 32 | /// Initialises a new instance of . 33 | /// 34 | /// The magnitude of the downward force, in Newtons. The default value is 10. 35 | public FlowDownwardForce(float magnitude = 10.0f) 36 | { 37 | Magnitude = magnitude; 38 | } 39 | 40 | /// 41 | void IForce.ApplyTo(Simulation simulation) 42 | { 43 | var force = new Vector2f(0, Magnitude); 44 | 45 | foreach (var pointMass in simulation.PointMasses) 46 | { 47 | if (pointMass.IsPinned) 48 | continue; 49 | 50 | pointMass.ApplyForce(force); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Boing/Forces/OriginAttractionForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | /// 22 | /// A force that attracts all non-pinned point masses towards the origin of the coordinate system. 23 | /// 24 | public sealed class OriginAttractionForce : IForce 25 | { 26 | /// 27 | /// Gets and sets the magnitude of the force. 28 | /// 29 | /// 30 | /// Positive values cause attraction to the origin, while negative values 31 | /// cause repulsion from it. Larger values cause larger forces, and a zero 32 | /// value effectively disables this force. 33 | /// 34 | public float Stiffness { get; set; } 35 | 36 | /// 37 | /// Initialises a new instance of . 38 | /// 39 | /// 40 | public OriginAttractionForce(float stiffness = 40) 41 | { 42 | Stiffness = stiffness; 43 | } 44 | 45 | /// 46 | void IForce.ApplyTo(Simulation simulation) 47 | { 48 | // ReSharper disable once CompareOfFloatsByEqualityOperator 49 | if (Stiffness == 0) 50 | return; 51 | 52 | var f = -Stiffness; 53 | 54 | foreach (var pointMass in simulation.PointMasses) 55 | { 56 | if (pointMass.IsPinned) 57 | continue; 58 | 59 | pointMass.ApplyForce(f*pointMass.Position); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Boing.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30611.23 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6B981AF-06BE-409F-8D26-6803E9A9886A}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitignore = .gitignore 9 | Directory.Build.props = Directory.Build.props 10 | Directory.Build.targets = Directory.Build.targets 11 | LICENSE = LICENSE 12 | Resources\logo-square-256px.png = Resources\logo-square-256px.png 13 | Resources\logo-square-512px.png = Resources\logo-square-512px.png 14 | Resources\logo-square.svg = Resources\logo-square.svg 15 | Resources\logo.svg = Resources\logo.svg 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Boing", "Boing\Boing.csproj", "{C4FAC4BA-CC6A-4FFB-ABEE-CB67E23745DA}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Boing.Tests", "Boing.Tests\Boing.Tests.csproj", "{CF371345-F529-46F0-AD32-4FFB6759C091}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {C4FAC4BA-CC6A-4FFB-ABEE-CB67E23745DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {C4FAC4BA-CC6A-4FFB-ABEE-CB67E23745DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {C4FAC4BA-CC6A-4FFB-ABEE-CB67E23745DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {C4FAC4BA-CC6A-4FFB-ABEE-CB67E23745DA}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {CF371345-F529-46F0-AD32-4FFB6759C091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {CF371345-F529-46F0-AD32-4FFB6759C091}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {CF371345-F529-46F0-AD32-4FFB6759C091}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CF371345-F529-46F0-AD32-4FFB6759C091}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {1AF0827F-AA78-42F2-9606-C13451BF922F} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /Boing.Tests/Rectangle2fTest.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using Xunit; 20 | 21 | namespace Boing.Tests 22 | { 23 | public sealed class Rectangle2fTest 24 | { 25 | [Fact] 26 | public void Corners() 27 | { 28 | var rect = new Rectangle2f(9, 9, 2, 2); 29 | 30 | Assert.Equal(new Vector2f(9, 9), rect.Min); 31 | Assert.Equal(new Vector2f(11, 11), rect.Max); 32 | 33 | Assert.Equal(new Vector2f(9, 9), rect.TopLeft); 34 | Assert.Equal(new Vector2f(11, 9), rect.TopRight); 35 | Assert.Equal(new Vector2f(9, 11), rect.BottomLeft); 36 | Assert.Equal(new Vector2f(11, 11), rect.BottomRight); 37 | } 38 | 39 | [Fact] 40 | public void Intersect() 41 | { 42 | var rect = new Rectangle2f(9, 9, 2, 2); 43 | 44 | var centre = new Vector2f(10, 10); 45 | var below = new Vector2f(10, 0); 46 | var above = new Vector2f(10, 20); 47 | var left = new Vector2f(0, 10); 48 | var right = new Vector2f(20, 10); 49 | 50 | Vector2f intersectionPoint; 51 | float t; 52 | 53 | Assert.True(rect.TryIntersect(new LineSegment2f(below, centre), out intersectionPoint, out t)); 54 | Assert.Equal(new Vector2f(10, 9), intersectionPoint); 55 | Assert.Equal(0.9, t, precision: 4); 56 | 57 | Assert.True(rect.TryIntersect(new LineSegment2f(above, centre), out intersectionPoint, out t)); 58 | Assert.Equal(new Vector2f(10, 11), intersectionPoint); 59 | Assert.Equal(0.9, t, precision: 4); 60 | 61 | Assert.True(rect.TryIntersect(new LineSegment2f(left, centre), out intersectionPoint, out t)); 62 | Assert.Equal(new Vector2f(9, 10), intersectionPoint); 63 | Assert.Equal(0.9, t, precision: 4); 64 | 65 | Assert.True(rect.TryIntersect(new LineSegment2f(right, centre), out intersectionPoint, out t)); 66 | Assert.Equal(new Vector2f(11, 10), intersectionPoint); 67 | Assert.Equal(0.9, t, precision: 4); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Boing.Tests/ReadmeSample.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Threading; 21 | using System.Threading.Tasks; 22 | 23 | namespace Boing.Tests 24 | { 25 | public static class ReadmeSample 26 | { 27 | public static void Code() 28 | { 29 | // create some point masses 30 | var pointMass1 = new PointMass(mass: 1.0f); 31 | var pointMass2 = new PointMass(mass: 2.0f); 32 | 33 | // create a new simulation 34 | var simulation = new Simulation 35 | { 36 | // add the point masses 37 | pointMass1, 38 | pointMass2, 39 | 40 | // create a spring between these point masses 41 | new Spring(pointMass1, pointMass2, length: 20), 42 | 43 | // point masses are attracted to one another 44 | new ColoumbForce(), 45 | 46 | // point masses move towards the origin 47 | new OriginAttractionForce(stiffness: 10), 48 | 49 | // gravity 50 | new FlowDownwardForce(magnitude: 100) 51 | }; 52 | 53 | void RunAtMaxSpeed() 54 | { 55 | while (true) 56 | { 57 | // update the simulation 58 | simulation.Update(dt: 0.01f); 59 | 60 | // use the resulting positions somehow 61 | Console.WriteLine($"PointMass1 at {pointMass1.Position}, PointMass2 at {pointMass2.Position}"); 62 | } 63 | } 64 | 65 | async Task RunAtFixedRateAsync(CancellationToken token) 66 | { 67 | var updater = new FixedTimeStepUpdater(simulation, timeStepSeconds: 1f/200); 68 | 69 | while (!token.IsCancellationRequested) 70 | { 71 | updater.Update(); 72 | 73 | // TODO render frame 74 | 75 | await Task.Delay(millisecondsDelay: 1/60, cancellationToken: token); 76 | } 77 | } 78 | 79 | // Reference methods to remove IDE warnings 80 | RunAtMaxSpeed(); 81 | RunAtFixedRateAsync(CancellationToken.None).Wait(); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Boing/FixedTimeStepUpdater.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Diagnostics; 21 | 22 | namespace Boing 23 | { 24 | /// 25 | /// Updates a simulation according to a time scale of fixed repeating interval, 26 | /// when compared with a stopwatch (wall time). 27 | /// 28 | /// Each call to may update the simulation zero or more times, 29 | /// 30 | public sealed class FixedTimeStepUpdater 31 | { 32 | private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); 33 | private readonly float _timeStepSeconds; 34 | private readonly long _timeStepTicks; 35 | private readonly Simulation _simulation; 36 | private long _leftOverTicks; 37 | private long _lastTicks; 38 | 39 | /// 40 | /// Initialises a . 41 | /// 42 | /// The simulation to apply to. 43 | /// The number of seconds to 44 | public FixedTimeStepUpdater(Simulation simulation, float timeStepSeconds) 45 | { 46 | _simulation = simulation; 47 | _timeStepSeconds = timeStepSeconds; 48 | _timeStepTicks = TimeSpan.FromSeconds(timeStepSeconds).Ticks; 49 | } 50 | 51 | /// 52 | /// Reset the current time 53 | /// 54 | public void Reset() 55 | { 56 | _leftOverTicks = 0; 57 | _lastTicks = 0; 58 | // Note we can't use Stopwatch.Restart here as we target netstandard1.0 59 | _stopwatch.Reset(); 60 | _stopwatch.Start(); 61 | } 62 | 63 | /// 64 | /// Updates the simulations enough times to bring it up to date. 65 | /// 66 | /// 67 | /// If was called recently enough, it may not actually 68 | /// update the simulation. It may also call it very many times if it has 69 | /// been a long time since it last ran. 70 | /// 71 | public void Update() 72 | { 73 | var ticks = _stopwatch.Elapsed.Ticks; 74 | var delta = ticks - _lastTicks; 75 | _lastTicks = ticks; 76 | 77 | delta += _leftOverTicks; 78 | 79 | var stepsThisUpdate = delta/_timeStepTicks; 80 | 81 | _leftOverTicks = delta%_timeStepTicks; 82 | 83 | for (var i = 0; i < stepsThisUpdate; i++) 84 | _simulation.Update(_timeStepSeconds); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![boing logo](https://cdn.rawgit.com/drewnoakes/boing/master/Resources/logo.svg) 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/xsovru9f2mmib616?svg=true)](https://ci.appveyor.com/project/drewnoakes/boing) 4 | [![Boing NuGet version](https://img.shields.io/nuget/v/Boing.svg)](https://www.nuget.org/packages/Boing/) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | 7 | A simple library for 2D physics simulations in .NET. 8 | 9 | ## Installation 10 | 11 | The easiest way to use this library is via its [NuGet package](https://www.nuget.org/packages/Boing/): 12 | 13 | PM> Install-Package Boing 14 | 15 | Boing supports `net35` (.NET Framework 3.5 and above) and `netstandard1.0` (.NET Standard 1.0 and above) for .NET Core and other platforms. 16 | 17 | ## Usage 18 | 19 | Build a `Simulation` comprising: 20 | 21 | - `PointMass` objects 22 | - Forces such as springs, gravity, Coloumb, viscosity 23 | 24 | Periodically update the simulation with a time step. 25 | 26 | ## Example 27 | 28 | Set up a simulation having some point masses and a few forces: 29 | 30 | ```csharp 31 | // create some point masses 32 | var pointMass1 = new PointMass(mass: 1.0f); 33 | var pointMass2 = new PointMass(mass: 2.0f); 34 | 35 | // create a new simulation 36 | var simulation = new Simulation 37 | { 38 | // add the point masses 39 | pointMass1, 40 | pointMass2, 41 | 42 | // create a spring between these point masses 43 | new Spring(pointMass1, pointMass2, length: 20), 44 | 45 | // point masses are attracted to one another 46 | new ColoumbForce(), 47 | 48 | // point masses move towards the origin 49 | new OriginAttractionForce(stiffness: 10), 50 | 51 | // gravity 52 | new FlowDownwardForce(magnitude: 100) 53 | }; 54 | ``` 55 | 56 | With the simulation configured, we must run it step by step in a loop. You could do this in 57 | several ways, but here are two common approaches. 58 | 59 | #### Max speed 60 | 61 | If you call `Update` as fast as possible, the simulation will most likely run faster than real time. 62 | 63 | ```csharp 64 | void RunAtMaxSpeed() 65 | { 66 | while (true) 67 | { 68 | // update the simulation 69 | simulation.Update(dt: 0.01f); 70 | 71 | // TODO use the resulting positions somehow 72 | Console.WriteLine($"PointMass1 at {pointMass1.Position}, PointMass2 at {pointMass2.Position}"); 73 | } 74 | } 75 | ``` 76 | 77 | #### At a fixed rate 78 | 79 | A more controlled approach is to use `FixedTimeStepUpdater`. This allows you to update the simulation 80 | at one rate, and integrate the simulation's output at another rate. For example, you might run the 81 | simulation at 200Hz, and render the output at 60Hz, as shown here: 82 | 83 | ```csharp 84 | async Task RunAtFixedRateAsync(CancellationToken token) 85 | { 86 | var updater = new FixedTimeStepUpdater(simulation, timeStepSeconds: 1f/200); 87 | 88 | while (!token.IsCancellationRequested) 89 | { 90 | updater.Update(); 91 | 92 | Render(); 93 | 94 | await Task.Delay(millisecondsDelay: 1/60, cancellationToken: token); 95 | } 96 | } 97 | ``` 98 | 99 | For each `Render` call, the simulation will have multiple updates with smaller time deltas. 100 | 101 | This example code is `async`, but only to support a non-blocking delay. You could make it synchronous and use `Thread.Sleep` if you prefer. 102 | -------------------------------------------------------------------------------- /Boing/LineSegment2f.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | /// 22 | /// A line segment in a two dimensional coordinate system. 23 | /// 24 | public readonly struct LineSegment2f 25 | { 26 | /// Gets the starting point of the line segment. 27 | public Vector2f From { get; } 28 | /// Gets the ending point of the line segment. 29 | public Vector2f To { get; } 30 | 31 | /// 32 | /// Initialises a new instance of . 33 | /// 34 | /// The starting point of the line segment 35 | /// The ending point of the line segment 36 | public LineSegment2f(Vector2f from, Vector2f to) 37 | { 38 | From = from; 39 | To = to; 40 | } 41 | 42 | /// Gets a vector representing the difference of the line segment's endpoints. 43 | /// Computed as minus . 44 | public Vector2f Delta => new Vector2f(To.X - From.X, To.Y - From.Y); 45 | 46 | /// 47 | /// Attempt to intersect two line segments. 48 | /// 49 | /// 50 | /// Even if the line segments do not intersect, and will be set. 51 | /// If the lines are parallel, and are set to . 52 | /// 53 | /// The line to attempt intersection of this line with. 54 | /// The point of intersection if within the line segments, or empty.. 55 | /// The distance along this line at which intersection would occur, or NaN if lines are collinear/parallel. 56 | /// The distance along the other line at which intersection would occur, or NaN if lines are collinear/parallel. 57 | /// true if the line segments intersect, otherwise false. 58 | public bool TryIntersect(LineSegment2f other, out Vector2f intersectionPoint, out float t, out float u) 59 | { 60 | static float Fake2DCross(Vector2f a, Vector2f b) => a.X * b.Y - a.Y * b.X; 61 | 62 | var p = From; 63 | var q = other.From; 64 | var r = Delta; 65 | var s = other.Delta; 66 | 67 | // t = (q − p) × s / (r × s) 68 | // u = (q − p) × r / (r × s) 69 | 70 | var denom = Fake2DCross(r, s); 71 | 72 | if (denom == 0) 73 | { 74 | // lines are collinear or parallel 75 | t = float.NaN; 76 | u = float.NaN; 77 | intersectionPoint = default; 78 | return false; 79 | } 80 | 81 | var tNumer = Fake2DCross(q - p, s); 82 | var uNumer = Fake2DCross(q - p, r); 83 | 84 | t = tNumer / denom; 85 | u = uNumer / denom; 86 | 87 | if (t < 0 || t > 1 || u < 0 || u > 1) 88 | { 89 | // line segments do not intersect within their ranges 90 | intersectionPoint = default; 91 | return false; 92 | } 93 | 94 | intersectionPoint = p + r * t; 95 | return true; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /Boing/Forces/ColoumbForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | namespace Boing 20 | { 21 | /// 22 | /// A force between point masses that follows an inverse square law. 23 | /// 24 | /// 25 | /// All pairs of unpinned point masses in the simulation will 26 | /// attract or repel one another with equal and opposite force. 27 | /// 28 | /// The force is proportional to the inverse square of the distance, meaning it 29 | /// is strongest for close particles, and the intensity diminishes quadratically 30 | /// as distance increases. 31 | /// 32 | /// The intensity of this effect can be linearly scaled using . 33 | /// 34 | /// As an optimisation, you may set the property to 35 | /// prevent this force applying over some distance threshold. By default this is 36 | /// 20,000. 37 | /// 38 | /// See https://en.wikipedia.org/wiki/Coulomb%27s_law for more information. 39 | /// 40 | public sealed class ColoumbForce : IForce 41 | { 42 | /// 43 | /// Initialises a new instance of . 44 | /// 45 | /// The to use. The default value is 20,000. Positive values cause repulsion, while negative values cause attraction. 46 | /// The to use. The default value is . 47 | public ColoumbForce(float scale = 20_000, float maxDistance = float.MaxValue) 48 | { 49 | Scale = scale; 50 | MaxDistance = maxDistance; 51 | } 52 | 53 | /// 54 | /// Gets and sets a value that linearly scales the intensity of the force applied between pairs of point masses. 55 | /// 56 | /// 57 | /// This value is equivalent to the force, in Newtons, applied to point masses one unit apart. 58 | /// 59 | /// Positive values cause repulsion, while negative values cause attraction. 60 | /// This value may be safely modified while the simulation is running. 61 | /// 62 | public float Scale { get; set; } 63 | 64 | /// 65 | /// Gets and sets the maximum distance over which the force applies. 66 | /// Point masses separated by distances greater than this threshold will not have a force applied. 67 | /// 68 | /// 69 | /// A value less than or equal to zero effectively disables this force. 70 | /// This value may be safely modified while the simulation is running. 71 | /// 72 | public float MaxDistance { get; set; } 73 | 74 | /// 75 | void IForce.ApplyTo(Simulation simulation) 76 | { 77 | if (MaxDistance <= 0) 78 | return; 79 | 80 | foreach (var pointMass1 in simulation.PointMasses) 81 | { 82 | foreach (var pointMass2 in simulation.PointMasses) 83 | { 84 | if (ReferenceEquals(pointMass1, pointMass2) || pointMass2.IsPinned) 85 | continue; 86 | 87 | var delta = pointMass2.Position - pointMass1.Position; 88 | var distance = delta.Norm(); 89 | 90 | // ReSharper disable once CompareOfFloatsByEqualityOperator 91 | if (distance == 0.0f || distance > MaxDistance) 92 | continue; 93 | 94 | pointMass2.ApplyForce(delta*Scale/(distance*distance)); 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Boing/Forces/KeepWithinBoundsForce.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | 21 | namespace Boing 22 | { 23 | /// 24 | /// A force that works to keep point masses within a given rectangular boundary. 25 | /// 26 | /// 27 | /// No force is exerted on point masses within . 28 | /// As a point moves outside the boundary, the force exerted back towards the boundary increases exponentially 29 | /// (with power determined by ). Forces are capped at 30 | /// to prevent destabilising the simulation. 31 | /// 32 | public sealed class KeepWithinBoundsForce : IForce 33 | { 34 | /// 35 | /// Gets and sets the rectangular bounds to which point masses are constrained. 36 | /// 37 | public Rectangle2f Bounds { get; set; } 38 | 39 | /// 40 | /// Gets and sets the exponential power to use when computing the force to apply 41 | /// to a point mass found outside , based on its distance from 42 | /// the boundary. 43 | /// 44 | public float Magnitude { get; set; } 45 | 46 | /// 47 | /// Gets and sets an upper limit on the force applied to particles, in Newtons, 48 | /// to prevent destabilising the simulation. 49 | /// 50 | public float MaximumForce { get; set; } 51 | 52 | /// 53 | /// Initialises a new instance of . 54 | /// 55 | /// The bounds to which point masses are constrained. 56 | /// 57 | /// The upper limit on the force applied to particles. The default value is 1000 Newtonss. 58 | public KeepWithinBoundsForce(Rectangle2f bounds, float magnitude = 3.0f, float maximumForce = 1000.0f) 59 | { 60 | Bounds = bounds; 61 | Magnitude = magnitude; 62 | MaximumForce = maximumForce; 63 | } 64 | 65 | /// 66 | void IForce.ApplyTo(Simulation simulation) 67 | { 68 | foreach (var pointMass in simulation.PointMasses) 69 | { 70 | if (pointMass.IsPinned) 71 | continue; 72 | 73 | if (pointMass.Position.X > Bounds.Right) 74 | { 75 | var force = (float)Math.Pow(pointMass.Position.X - Bounds.Right, Magnitude); 76 | if (force > MaximumForce) 77 | force = MaximumForce; 78 | pointMass.ApplyForce(new Vector2f(-force, 0)); 79 | } 80 | else if (pointMass.Position.X < Bounds.Left) 81 | { 82 | var force = (float)Math.Pow(Bounds.Left - pointMass.Position.X, Magnitude); 83 | if (force > MaximumForce) 84 | force = MaximumForce; 85 | pointMass.ApplyForce(new Vector2f(force, 0)); 86 | } 87 | 88 | if (pointMass.Position.Y > Bounds.Bottom) 89 | { 90 | var force = (float)Math.Pow(pointMass.Position.Y - Bounds.Bottom, Magnitude); 91 | if (force > MaximumForce) 92 | force = MaximumForce; 93 | pointMass.ApplyForce(new Vector2f(0, -force)); 94 | } 95 | else if (pointMass.Position.Y < Bounds.Top) 96 | { 97 | var force = (float)Math.Pow(Bounds.Top - pointMass.Position.Y, Magnitude); 98 | if (force > MaximumForce) 99 | force = MaximumForce; 100 | pointMass.ApplyForce(new Vector2f(0, force)); 101 | } 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /Boing/Simulation.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Collections; 21 | using System.Collections.Generic; 22 | 23 | namespace Boing 24 | { 25 | /// 26 | /// Top-level object for a Boing physical simulation. 27 | /// Contains instances of and , 28 | /// and can update them one time step at a time. 29 | /// 30 | public sealed class Simulation : IEnumerable 31 | { 32 | private readonly HashSet _pointMasses = new HashSet(); 33 | private readonly HashSet _forces = new HashSet(); 34 | 35 | /// 36 | /// Gets the set of point masses within this simulation. 37 | /// 38 | public IEnumerable PointMasses => _pointMasses; 39 | 40 | /// 41 | /// Gets the set of forces within this simulation. 42 | /// 43 | public IEnumerable Forces => _forces; 44 | 45 | /// 46 | /// Progresses the simulation one time step, updating the and 47 | /// of all point masses within the simulation. 48 | /// 49 | /// Elapsed time since the last update, in seconds. 50 | public void Update(float dt) 51 | { 52 | foreach (var force in _forces) 53 | force.ApplyTo(this); 54 | 55 | foreach (var pointMass in _pointMasses) 56 | pointMass.Update(dt); 57 | } 58 | 59 | /// 60 | /// Aggregates the kinetic energy of all point masses in the simulation, and 61 | /// returns the sum. 62 | /// 63 | /// 64 | /// Kinetic energy is computed based on point mass velocities. 65 | /// It can be useful in determining whether a simulation has come to rest, 66 | /// although a moving average should be applied to prevent incorrectly taking 67 | /// a transiently near-static moment to mean equilibrium is reached. 68 | /// 69 | /// 70 | public float GetTotalKineticEnergy() 71 | { 72 | // Ek = 1/2 m v^2 73 | 74 | float sum = 0; 75 | 76 | // ReSharper disable once LoopCanBeConvertedToQuery 77 | foreach (var pointMass in _pointMasses) 78 | { 79 | var speed = pointMass.Velocity.Norm(); 80 | sum += pointMass.Mass*speed*speed; 81 | } 82 | 83 | return sum / 2; 84 | } 85 | 86 | /// 87 | /// Removes all and objects from the simulation. 88 | /// 89 | public void Clear() 90 | { 91 | _pointMasses.Clear(); 92 | _forces.Clear(); 93 | } 94 | 95 | /// 96 | /// Adds to the simulation. 97 | /// 98 | /// The to add to the simulation. 99 | /// already exists. 100 | public void Add(IForce force) 101 | { 102 | if (!_forces.Add(force)) 103 | throw new ArgumentException("Already exists.", nameof(force)); 104 | } 105 | 106 | /// 107 | /// Removes from the simulation. 108 | /// 109 | /// The to remove from the simulation. 110 | public bool Remove(IForce force) => _forces.Remove(force); 111 | 112 | /// 113 | /// Adds to the simulation. 114 | /// 115 | /// The to add to the simulation. 116 | /// already exists. 117 | public void Add(PointMass pointMass) 118 | { 119 | if (!_pointMasses.Add(pointMass)) 120 | throw new ArgumentException("Already exists.", nameof(pointMass)); 121 | } 122 | 123 | /// 124 | /// Removes from the simulation. 125 | /// 126 | /// The to remove from the simulation. 127 | public bool Remove(PointMass pointMass) => _pointMasses.Remove(pointMass); 128 | 129 | /// 130 | IEnumerator IEnumerable.GetEnumerator() 131 | { 132 | throw new NotSupportedException($"{nameof(Simulation)} only implements {nameof(IEnumerable)} to enable C# object initialisers."); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /Boing/Forces/Spring.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Diagnostics; 21 | 22 | namespace Boing 23 | { 24 | /// 25 | /// A force that models a linear spring between two point masses according to Hooke's law. 26 | /// 27 | /// 28 | /// Unlike many other implementations, a single 29 | /// instance does not apply to all point masses in the simulation. It applies to two, only. 30 | /// 31 | /// The spring is defined by its two point masses, a length and a spring constant. 32 | /// At each time step, the distance between the point masses is computed and compared 33 | /// against the spring's length. 34 | /// 35 | /// If the distance matches the length, no force is applied. 36 | /// When the distance diverges from the spring's length, the spring undergoes compression 37 | /// or elongation and exerts a restorative force against both point masses. 38 | /// 39 | /// Note that if one point mass is pinned, the force applies doubly to the unpinned end. 40 | /// If both point masses are pinned, the spring has no effect. 41 | /// 42 | /// See https://en.wikipedia.org/wiki/Hooke%27s_law for more information. 43 | /// 44 | public sealed class Spring : IForce 45 | { 46 | /// 47 | /// Gets one of the two point masses connected by this spring. 48 | /// 49 | /// 50 | /// The orientation of the spring has no effect. and 51 | /// could be swapped and the outcome would be identical. 52 | /// 53 | public PointMass Source { get; } 54 | 55 | /// 56 | /// Gets one of the two point masses connected by this spring. 57 | /// 58 | /// 59 | /// The orientation of the spring has no effect. and 60 | /// could be swapped and the outcome would be identical. 61 | /// 62 | public PointMass Target { get; } 63 | 64 | /// 65 | /// Gets and sets the resting length of the spring. 66 | /// 67 | public float Length { get; set; } 68 | 69 | /// 70 | /// Gets and sets the spring constant. 71 | /// 72 | public float K { get; set; } 73 | 74 | /// 75 | /// Initialises a new instance of . 76 | /// 77 | /// One of the two point masses. 78 | /// One of the two point masses. 79 | /// The resting length of the spring. The default value is 100 units. 80 | /// The string constant. The default value is 80. 81 | public Spring(PointMass source, PointMass target, float length = 100.0f, float k = 80.0f) 82 | { 83 | Source = source; 84 | Target = target; 85 | Length = length; 86 | K = k; 87 | } 88 | 89 | /// 90 | /// Gets a 2D line segment whose start and end points match those of 91 | /// the and point mass positions. 92 | /// 93 | public LineSegment2f LineSegment => new LineSegment2f(Source.Position, Target.Position); 94 | 95 | /// 96 | /// Gets the 2D axis-aligned bounding box that encloses the and 97 | /// point mass positions. 98 | /// 99 | public Rectangle2f Bounds => new Rectangle2f( 100 | Math.Min(Source.Position.X, Target.Position.X), 101 | Math.Min(Source.Position.Y, Target.Position.Y), 102 | Math.Abs(Source.Position.X - Target.Position.X), 103 | Math.Abs(Source.Position.Y - Target.Position.Y)); 104 | 105 | /// 106 | void IForce.ApplyTo(Simulation simulation) 107 | { 108 | var source = Source; 109 | var target = Target; 110 | 111 | var delta = target.Position - source.Position; 112 | var direction = delta.Normalized(out float deltaNorm); 113 | var displacement = Length - deltaNorm; 114 | 115 | Debug.Assert(!float.IsNaN(displacement), "!float.IsNaN(displacement)"); 116 | 117 | if (!source.IsPinned && !target.IsPinned) 118 | { 119 | source.ApplyForce(direction*(K*displacement*-0.5f)); 120 | target.ApplyForce(direction*(K*displacement*0.5f)); 121 | } 122 | else if (source.IsPinned && !target.IsPinned) 123 | { 124 | target.ApplyForce(direction*(K*displacement)); 125 | } 126 | else if (!source.IsPinned && target.IsPinned) 127 | { 128 | source.ApplyForce(direction*(K*-displacement)); 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /Boing/Vector2f.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | 21 | // ReSharper disable CompareOfFloatsByEqualityOperator 22 | 23 | namespace Boing 24 | { 25 | /// 26 | /// A point in a two dimensional coordinate system. 27 | /// 28 | public readonly struct Vector2f 29 | { 30 | /// 31 | /// Initialises a . 32 | /// 33 | /// The x coordinate value. 34 | /// The y coordinate value. 35 | public Vector2f(float x, float y) 36 | { 37 | X = x; 38 | Y = y; 39 | } 40 | 41 | /// Gets the x coordinate value. 42 | public readonly float X; 43 | /// Gets the y coordinate value. 44 | public readonly float Y; 45 | 46 | /// Gets the norm (length) of the vector. 47 | public float Norm() => (float)Math.Sqrt(X*X + Y*Y); 48 | 49 | /// 50 | /// Returns a normalized version of this vector. 51 | /// 52 | /// 53 | /// The normalized vector has length one (it is a unit vector). 54 | /// 55 | /// The normalized version of this vector. 56 | public Vector2f Normalized() => this/Norm(); 57 | 58 | /// 59 | /// Returns a normalized version of this vector. 60 | /// 61 | /// 62 | /// The normalized vector has length one (it is a unit vector). 63 | /// This overload is useful if you need to know the length of a vector, 64 | /// and need its normalized form, in which case it is both convenient and efficient. 65 | /// 66 | /// The length of the vector before normalization. 67 | /// The normalized version of this vector. 68 | public Vector2f Normalized(out float norm) 69 | { 70 | norm = Norm(); 71 | return this/norm; 72 | } 73 | 74 | /// 75 | /// Gets whether either the or values are . 76 | /// 77 | public bool HasNaN => float.IsNaN(X) || float.IsNaN(Y); 78 | 79 | /// Gets whether either the or values are infinite. 80 | public bool HasInfinity => float.IsInfinity(X) || float.IsInfinity(Y); 81 | 82 | #region Factories 83 | 84 | private static readonly Random _random = new Random(); 85 | 86 | /// 87 | /// Produces random points with and values distributed evenly 88 | /// over a square of side length that is centered at the origin. 89 | /// 90 | /// The width and height of the square over which random points are distributed. 91 | /// The next random point in the specified range. 92 | public static Vector2f Random(float valueRange = 1.0f) 93 | { 94 | return new Vector2f( 95 | valueRange*((float)_random.NextDouble() - 0.5f), 96 | valueRange*((float)_random.NextDouble() - 0.5f)); 97 | } 98 | 99 | /// 100 | /// Gets a vector with both and equalling zero. 101 | /// 102 | public static Vector2f Zero => default; 103 | 104 | #endregion 105 | 106 | #region Equality / Hashing 107 | 108 | /// 109 | /// Indicates whether this instance equals . 110 | /// 111 | /// The vector to compare against. 112 | /// true if the vectors are equal, otherwise false. 113 | public bool Equals(Vector2f other) => X.Equals(other.X) && Y.Equals(other.Y); 114 | 115 | /// 116 | public override bool Equals(object obj) => obj is Vector2f v && Equals(v); 117 | 118 | /// 119 | public override int GetHashCode() => unchecked((X.GetHashCode()*397) ^ Y.GetHashCode()); 120 | 121 | #endregion 122 | 123 | #region Operators 124 | 125 | #pragma warning disable 1591 126 | public static bool operator ==(Vector2f a, Vector2f b) => a.X == b.X && a.Y == b.Y; 127 | public static bool operator !=(Vector2f a, Vector2f b) => a.X != b.X || a.Y != b.Y; 128 | 129 | public static Vector2f operator +(Vector2f a, Vector2f b) => new Vector2f(a.X + b.X, a.Y + b.Y); 130 | public static Vector2f operator -(Vector2f a, Vector2f b) => new Vector2f(a.X - b.X, a.Y - b.Y); 131 | public static Vector2f operator *(Vector2f a, float b) => new Vector2f(a.X*b, a.Y*b); 132 | public static Vector2f operator *(float a, Vector2f b) => new Vector2f(b.X*a, b.Y*a); 133 | public static Vector2f operator /(Vector2f a, float b) => b == 0.0f ? Zero : new Vector2f(a.X/b, a.Y/b); 134 | public static Vector2f operator -(Vector2f v) => new Vector2f(-v.X, -v.Y); 135 | #pragma warning restore 1591 136 | 137 | #endregion 138 | 139 | /// 140 | public override string ToString() => $"X: {X}, Y: {Y}"; 141 | } 142 | } -------------------------------------------------------------------------------- /Boing/Rectangle2f.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | 22 | namespace Boing 23 | { 24 | /// 25 | /// An axis-aligned rectangular area in a two dimensional coordinate system. 26 | /// 27 | public readonly struct Rectangle2f 28 | { 29 | /// 30 | /// The minimum point in the rectangle, which is also the top left corner. 31 | /// 32 | public Vector2f Min { get; } 33 | 34 | /// 35 | /// The maximum point in the rectangle, which is also the bottom right corner. 36 | /// 37 | public Vector2f Max { get; } 38 | 39 | /// 40 | /// Initialises a new instance of from the pair of its minimum and maximum points. 41 | /// 42 | /// The minimum point, which is also the top left corner. 43 | /// The maximum point, which is also the bottom right corner. 44 | /// 45 | public Rectangle2f(Vector2f min, Vector2f max) 46 | { 47 | if (min.X > max.X) 48 | throw new ArgumentException("min.X is greater than max.X"); 49 | if (min.Y > max.Y) 50 | throw new ArgumentException("min.Y is greater than max.Y"); 51 | 52 | Min = min; 53 | Max = max; 54 | } 55 | 56 | /// 57 | /// Initialises a new instance of from its minimum point, and a width and height. 58 | /// 59 | /// The x value of the minimum point. 60 | /// The y value of the minimum point. 61 | /// The width of the rectangle. 62 | /// The height of the rectangle. 63 | public Rectangle2f(float x, float y, float width, float height) 64 | : this(new Vector2f(x, y), new Vector2f(x + width, y + height)) 65 | {} 66 | 67 | /// Gets the x value of the left edge. 68 | public float Left => Min.X; 69 | /// Gets the x value of the right edge. 70 | public float Right => Max.X; 71 | /// Gets the y value of the top edge. 72 | public float Top => Min.Y; 73 | /// Gets the y value of the bottom edge. 74 | public float Bottom => Max.Y; 75 | 76 | /// Gets the width of the rectangle. 77 | public float Width => Right - Left; 78 | /// Gets the height of the rectangle. 79 | public float Height => Bottom - Top; 80 | 81 | /// Gets the position of the top left corner. 82 | public Vector2f TopLeft => Min; 83 | /// Gets the position of the top right corner. 84 | public Vector2f TopRight => new Vector2f(Max.X, Min.Y); 85 | /// Gets the position of the bottom left corner. 86 | public Vector2f BottomLeft => new Vector2f(Min.X, Max.Y); 87 | /// Gets the position of the bottom right corner. 88 | public Vector2f BottomRight => Max; 89 | 90 | /// 91 | /// Enumerates the four edges of the rectangle. 92 | /// 93 | /// 94 | /// Edges are produce in clockwise order: top, right, bottom, left. 95 | /// 96 | public IEnumerable Edges 97 | { 98 | get 99 | { 100 | yield return new LineSegment2f(TopLeft, TopRight); 101 | yield return new LineSegment2f(TopRight, BottomRight); 102 | yield return new LineSegment2f(BottomLeft, BottomRight); 103 | yield return new LineSegment2f(TopLeft, BottomLeft); 104 | } 105 | } 106 | 107 | /// 108 | /// Attempt to intersect a line segment with this rectangle. 109 | /// 110 | /// The line to intersect with this rectangle. 111 | /// The point of intersection. 112 | /// the distance along this line at which intersection would occur, or zero if no intersection occurs. 113 | /// true if an intersection exists, otherwise false. 114 | public bool TryIntersect(LineSegment2f line, out Vector2f intersectionPoint, out float t) 115 | { 116 | var minT = float.MaxValue; 117 | var found = false; 118 | 119 | intersectionPoint = default; 120 | 121 | foreach (var edge in Edges) 122 | { 123 | if (edge.TryIntersect(line, out Vector2f point, out float edgeT, out float lineT)) 124 | { 125 | if (lineT > minT) 126 | continue; 127 | found = true; 128 | minT = lineT; 129 | intersectionPoint = point; 130 | } 131 | } 132 | 133 | if (found) 134 | { 135 | t = minT; 136 | return true; 137 | } 138 | 139 | t = 0; 140 | return false; 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Boing/PointMass.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | 3 | // Copyright 2015-2020 Drew Noakes, Krzysztof Dul 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #endregion 18 | 19 | using System.Diagnostics; 20 | 21 | namespace Boing 22 | { 23 | /// 24 | /// Models an idealised physical body for the purposes of simulation, having mass and infinitesimal volume and dimension. 25 | /// 26 | /// 27 | /// Point masses may be pinned, in which case forces and impulses should not be applied. 28 | /// It is up to implementations of to ensure this. 29 | /// 30 | /// You may store custom data in the property. 31 | /// 32 | /// Whilst all properties of this type are mutable, in normal usage and 33 | /// are modified by implementations during simulation . 34 | /// 35 | /// and may safely be modified at any time, having a direct effect on the simulation. 36 | /// 37 | public sealed class PointMass 38 | { 39 | private Vector2f _force; 40 | 41 | /// 42 | /// Gets and sets the mass of this point mass. 43 | /// 44 | /// 45 | /// This value may be safely modified while the simulation is running. 46 | /// 47 | public float Mass { get; set; } 48 | 49 | /// 50 | /// Gets and sets whether this point mass is pinned. When pinned, it should not be moved. 51 | /// 52 | /// 53 | /// This value may be safely modified while the simulation is running. 54 | /// 55 | public bool IsPinned { get; set; } 56 | 57 | /// 58 | /// Gets and sets an object of miscellaneous data to associate with this point mass. 59 | /// 60 | /// 61 | /// This value is optional. It is provided for convenience, and is unused by the library. 62 | /// 63 | public object? Tag { get; set; } 64 | 65 | /// 66 | /// Gets and sets the position of this point mass. 67 | /// 68 | /// 69 | /// In normal usage this property is modified by implementations 70 | /// during simulation . 71 | /// 72 | public Vector2f Position { get; set; } 73 | 74 | /// 75 | /// Gets and sets the position of this point mass. 76 | /// 77 | /// 78 | /// In normal usage this property is modified by implementations 79 | /// during simulation . 80 | /// 81 | public Vector2f Velocity { get; set; } 82 | 83 | /// 84 | /// Initialises a new instance of . 85 | /// 86 | /// The initial mass. The default value is 1. 87 | /// The initial position. The default value of null causes a random point between +/-0.5 in both X and Y to be set. 88 | public PointMass(float mass = 1.0f, Vector2f? position = null) 89 | { 90 | Mass = mass; 91 | Position = position ?? Vector2f.Random(); 92 | } 93 | 94 | /// 95 | /// Applies to the point mass. 96 | /// 97 | /// 98 | /// Forces are accumulated in each time step, and this method may be called multiple times in a 99 | /// single time step. Only when is called on this point mass will the 100 | /// velocity and position be updated based upon any accumulated force. 101 | /// 102 | /// The force to apply to the point mass. 103 | public void ApplyForce(Vector2f force) 104 | { 105 | Debug.Assert(!float.IsNaN(force.X) && !float.IsNaN(force.Y), "!float.IsNaN(force.X) && !float.IsNaN(force.Y)"); 106 | Debug.Assert(!float.IsInfinity(force.X) && !float.IsInfinity(force.Y), "!float.IsInfinity(force.X) && !float.IsInfinity(force.Y)"); 107 | 108 | // Accumulate force 109 | _force += force; 110 | } 111 | 112 | /// 113 | /// Applies to the point mass. 114 | /// 115 | /// 116 | /// Impulses are applied directly to the point mass's , and as such their impact is 117 | /// unaffected by the simulation's step size. However will only be modified by the next call 118 | /// to . 119 | /// 120 | /// The impulse to apply to the point mass. 121 | public void ApplyImpulse(Vector2f impulse) 122 | { 123 | // Update velocity 124 | Velocity += impulse/Mass; 125 | } 126 | 127 | /// 128 | /// Updates according to any applied forces and the time delta, and then 129 | /// updates . 130 | /// 131 | /// Elapsed time since the last update, in seconds. 132 | public void Update(float dt) 133 | { 134 | // Update velocity 135 | Velocity += _force/Mass*dt; 136 | 137 | // Update position 138 | Position += Velocity*dt; 139 | 140 | Debug.Assert(!float.IsNaN(Position.X) && !float.IsNaN(Position.Y), "!float.IsNaN(Position.X) && !float.IsNaN(Position.Y)"); 141 | Debug.Assert(!float.IsInfinity(Position.X) && !float.IsInfinity(Position.Y), "!float.IsInfinity(Position.X) && !float.IsInfinity(Position.Y)"); 142 | 143 | // Clear force 144 | _force = Vector2f.Zero; 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /Resources/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 70 | 74 | 84 | 94 | 104 | 114 | 124 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /Resources/logo-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 70 | 74 | 84 | 94 | 104 | 114 | 124 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "{}" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015-2020 Drew Noakes, Krzysztof Dul 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | ------------------------------------------------------------------------------ 205 | 206 | Boing depends upon the following 3rd party packages with their own 207 | license terms: 208 | 209 | - xunit, licensed under Apache 2.0 (used during development) 210 | --------------------------------------------------------------------------------