├── .gitattributes ├── .gitignore ├── .vs └── config │ └── applicationhost.config ├── Images ├── cycle.gif ├── firework.gif ├── metaball.gif ├── newton_scradle.gif ├── poly_rod.gif └── spring.gif ├── Physics2D.sln ├── Physics2D ├── Collision │ ├── Basic │ │ ├── ParticleLink.cs │ │ ├── ParticleRod.cs │ │ └── ParticleRope.cs │ ├── ContactType.cs │ ├── ParticleCollisionDetector.cs │ ├── ParticleContact.cs │ ├── ParticleContactGenerator.cs │ ├── ParticleContactResolver.cs │ └── Shapes │ │ ├── Circle.cs │ │ ├── Edge.cs │ │ ├── Point.cs │ │ ├── Shape.cs │ │ └── ShapeType.cs ├── Common │ ├── Events │ │ └── ContactEvent.cs │ ├── Exceptions │ │ └── InvalidArgumentException.cs │ ├── MathHelper.cs │ └── Vector2D.cs ├── ConvertUnits.cs ├── Core │ ├── ContactRegistry.cs │ ├── ForceRegistry.cs │ └── World.cs ├── Factories │ ├── ContactFactory.cs │ ├── ParticleFactory.cs │ └── ZoneFactory.cs ├── Force │ ├── ParticleConstantForce.cs │ ├── ParticleDrag.cs │ ├── ParticleElastic.cs │ ├── ParticleForceGenerator.cs │ ├── ParticleGravity.cs │ └── Zones │ │ ├── GlobalZone.cs │ │ ├── RectangleZone.cs │ │ └── Zone.cs ├── Object │ ├── CombinedParticle.cs │ ├── CustomObject.cs │ ├── Particle.cs │ ├── PhysicsObject.cs │ └── Tools │ │ ├── Handle.cs │ │ └── IPin.cs ├── Physics2D.csproj ├── Settings.cs └── stylecop.json ├── README.md ├── UnitTest ├── Collision │ ├── Basic │ │ ├── ParticleRodTest.cs │ │ └── ParticleRopeTest.cs │ ├── ParticleCollisionDetectorTest.cs │ ├── ParticleContactResolverTest.cs │ ├── ParticleContactTest.cs │ └── Shapes │ │ ├── CircleTest.cs │ │ ├── EdgeTest.cs │ │ ├── PointTest.cs │ │ └── ShapeTest.cs ├── Common │ ├── MathHelperTest.cs │ └── Vector2DTest.cs ├── ConvertUnitsTest.cs ├── Core │ ├── ContactRegistryTest.cs │ ├── ForceRegistryTest.cs │ └── WorldTest.cs ├── Factories │ ├── ContactFactoryTest.cs │ ├── ParticleFactoryTest.cs │ └── ZoneFactoryTest.cs ├── Force │ ├── ParticleConstantForceTest.cs │ ├── ParticleDragTest.cs │ ├── ParticleElasticTest.cs │ ├── ParticleGravityTest.cs │ └── Zones │ │ ├── GlobalZoneTest.cs │ │ └── RectangleZoneTest.cs ├── Object │ ├── CombinedParticleTest.cs │ ├── ParticleTest.cs │ └── Tools │ │ └── HandleTest.cs ├── Properties │ └── AssemblyInfo.cs ├── UnitTest.csproj └── packages.config └── WPFDemo ├── App.config ├── App.xaml ├── App.xaml.cs ├── CircleDemo ├── Circle.xaml ├── Circle.xaml.cs └── CircleDemo.cs ├── ContactDemo ├── Ball.cs ├── Contact.xaml ├── Contact.xaml.cs └── ContactDemo.cs ├── DrawingCanvas.cs ├── ElasticDemo ├── DestructibleElastic.cs ├── Elastic.xaml ├── Elastic.xaml.cs ├── ElasticDemo.cs └── ElasticatedNet.cs ├── FireworksDemo ├── Fireworks.xaml ├── Fireworks.xaml.cs └── FireworksDemo.cs ├── FluidDemo ├── Fluid.xaml ├── Fluid.xaml.cs ├── FluidDemo.cs └── Water.cs ├── Graphic ├── IDrawable.cs ├── PhysicsGraphic.cs └── TimeTracker.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── RobDemo ├── Rob.xaml ├── Rob.xaml.cs └── RobDemo.cs ├── WPFDemo.csproj ├── packages.config └── stylecop.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | 241 | # SQL Server files 242 | *.mdf 243 | *.ldf 244 | *.ndf 245 | 246 | # Business Intelligence projects 247 | *.rdl.data 248 | *.bim.layout 249 | *.bim_*.settings 250 | 251 | # Microsoft Fakes 252 | FakesAssemblies/ 253 | 254 | # GhostDoc plugin setting file 255 | *.GhostDoc.xml 256 | 257 | # Node.js Tools for Visual Studio 258 | .ntvs_analysis.dat 259 | node_modules/ 260 | 261 | # TypeScript v1 declaration files 262 | typings/ 263 | 264 | # Visual Studio 6 build log 265 | *.plg 266 | 267 | # Visual Studio 6 workspace options file 268 | *.opt 269 | 270 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 271 | *.vbw 272 | 273 | # Visual Studio LightSwitch build output 274 | **/*.HTMLClient/GeneratedArtifacts 275 | **/*.DesktopClient/GeneratedArtifacts 276 | **/*.DesktopClient/ModelManifest.xml 277 | **/*.Server/GeneratedArtifacts 278 | **/*.Server/ModelManifest.xml 279 | _Pvt_Extensions 280 | 281 | # Paket dependency manager 282 | .paket/paket.exe 283 | paket-files/ 284 | 285 | # FAKE - F# Make 286 | .fake/ 287 | 288 | # JetBrains Rider 289 | .idea/ 290 | *.sln.iml 291 | 292 | # CodeRush 293 | .cr/ 294 | 295 | # Python Tools for Visual Studio (PTVS) 296 | __pycache__/ 297 | *.pyc 298 | 299 | # Cake - Uncomment if you are using it 300 | # tools/** 301 | # !tools/packages.config 302 | 303 | # Tabs Studio 304 | *.tss 305 | 306 | # Telerik's JustMock configuration file 307 | *.jmconfig 308 | 309 | # BizTalk build output 310 | *.btp.cs 311 | *.btm.cs 312 | *.odx.cs 313 | *.xsd.cs 314 | 315 | # OpenCover UI analysis results 316 | OpenCover/ 317 | 318 | # Azure Stream Analytics local run output 319 | ASALocalRun/ 320 | 321 | # MSBuild Binary and Structured Log 322 | *.binlog -------------------------------------------------------------------------------- /Images/cycle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/cycle.gif -------------------------------------------------------------------------------- /Images/firework.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/firework.gif -------------------------------------------------------------------------------- /Images/metaball.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/metaball.gif -------------------------------------------------------------------------------- /Images/newton_scradle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/newton_scradle.gif -------------------------------------------------------------------------------- /Images/poly_rod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/poly_rod.gif -------------------------------------------------------------------------------- /Images/spring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blueve/Physics2D/42a133a0eab403a7e9797f6629b81916f267c812/Images/spring.gif -------------------------------------------------------------------------------- /Physics2D.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "UnitTest\UnitTest.csproj", "{EAA4C780-0963-4479-9D11-E162127C0014}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WPFDemo", "WPFDemo\WPFDemo.csproj", "{7BEAE24F-5041-441D-99D9-D82657A8BBE7}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Physics2D", "Physics2D\Physics2D.csproj", "{E102F925-90FE-41F2-856E-3FAB4BB2AD58}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|ARM = Debug|ARM 16 | Debug|x64 = Debug|x64 17 | Debug|x86 = Debug|x86 18 | Release|Any CPU = Release|Any CPU 19 | Release|ARM = Release|ARM 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|ARM.ActiveCfg = Debug|Any CPU 27 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|ARM.Build.0 = Debug|Any CPU 28 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|x64.Build.0 = Debug|Any CPU 30 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {EAA4C780-0963-4479-9D11-E162127C0014}.Debug|x86.Build.0 = Debug|Any CPU 32 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|ARM.ActiveCfg = Release|Any CPU 35 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|ARM.Build.0 = Release|Any CPU 36 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|x64.ActiveCfg = Release|Any CPU 37 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|x64.Build.0 = Release|Any CPU 38 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|x86.ActiveCfg = Release|Any CPU 39 | {EAA4C780-0963-4479-9D11-E162127C0014}.Release|x86.Build.0 = Release|Any CPU 40 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|ARM.ActiveCfg = Debug|Any CPU 43 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|ARM.Build.0 = Debug|Any CPU 44 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|x64.Build.0 = Debug|Any CPU 46 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Debug|x86.Build.0 = Debug|Any CPU 48 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|ARM.ActiveCfg = Release|Any CPU 51 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|ARM.Build.0 = Release|Any CPU 52 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|x64.ActiveCfg = Release|Any CPU 53 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|x64.Build.0 = Release|Any CPU 54 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|x86.ActiveCfg = Release|Any CPU 55 | {7BEAE24F-5041-441D-99D9-D82657A8BBE7}.Release|x86.Build.0 = Release|Any CPU 56 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|ARM.ActiveCfg = Debug|Any CPU 59 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|ARM.Build.0 = Debug|Any CPU 60 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|x64.ActiveCfg = Debug|Any CPU 61 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|x64.Build.0 = Debug|Any CPU 62 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|x86.ActiveCfg = Debug|Any CPU 63 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Debug|x86.Build.0 = Debug|Any CPU 64 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|ARM.ActiveCfg = Release|Any CPU 67 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|ARM.Build.0 = Release|Any CPU 68 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|x64.ActiveCfg = Release|Any CPU 69 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|x64.Build.0 = Release|Any CPU 70 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|x86.ActiveCfg = Release|Any CPU 71 | {E102F925-90FE-41F2-856E-3FAB4BB2AD58}.Release|x86.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {EF498AD8-8378-4329-B8DE-97A6D1141002} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /Physics2D/Collision/Basic/ParticleLink.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Basic 2 | { 3 | using Physics2D.Object; 4 | 5 | public abstract class ParticleLink : ParticleContactGenerator 6 | { 7 | /// 8 | /// 质体A 9 | /// 10 | public Particle ParticleA; 11 | 12 | /// 13 | /// 质体B 14 | /// 15 | public Particle ParticleB; 16 | 17 | /// 18 | /// 返回当前连接的长度 19 | /// 20 | /// 21 | protected double CurrentLength() 22 | { 23 | return (this.ParticleA.Position - this.ParticleB.Position).Length(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Physics2D/Collision/Basic/ParticleRod.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Basic 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Object; 5 | 6 | public class ParticleRod : ParticleLink 7 | { 8 | /// 9 | /// 长度 10 | /// 11 | public double Length { get; } 12 | 13 | public ParticleRod(Particle pA, Particle pB) 14 | { 15 | this.Length = (pB.Position - pA.Position).Length(); 16 | this.ParticleA = pA; 17 | this.ParticleB = pB; 18 | } 19 | 20 | public override IEnumerator GetEnumerator() 21 | { 22 | double length = this.CurrentLength(); 23 | double penetration = length - this.Length; 24 | 25 | if (penetration == 0) 26 | { 27 | yield break; 28 | } 29 | 30 | var normal = (this.ParticleB.Position - this.ParticleA.Position).Normalize(); 31 | 32 | if (length <= this.Length) 33 | { 34 | normal *= -1; 35 | penetration *= -1; 36 | } 37 | 38 | yield return new ParticleContact(this.ParticleA, this.ParticleB, 0, penetration, normal); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Physics2D/Collision/Basic/ParticleRope.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Basic 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Object; 5 | 6 | public class ParticleRope : ParticleLink 7 | { 8 | /// 9 | /// 最大长度 10 | /// 11 | public double MaxLength { get; } 12 | 13 | /// 14 | /// 弹性系数 15 | /// 16 | public double Restitution { get; } 17 | 18 | public ParticleRope(double maxLength, double restitution, Particle pA, Particle pB) 19 | { 20 | this.MaxLength = maxLength; 21 | this.Restitution = restitution; 22 | this.ParticleA = pA; 23 | this.ParticleB = pB; 24 | } 25 | 26 | public override IEnumerator GetEnumerator() 27 | { 28 | double length = this.CurrentLength(); 29 | 30 | // 未超过绳索长度 31 | if (length < this.MaxLength) 32 | { 33 | yield break; 34 | } 35 | 36 | var normal = (this.ParticleB.Position - this.ParticleA.Position).Normalize(); 37 | 38 | yield return new ParticleContact(this.ParticleA, this.ParticleB, this.Restitution, length - this.MaxLength, normal); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Physics2D/Collision/ContactType.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision 2 | { 3 | public enum ContactType 4 | { 5 | CircleAndCircle, 6 | CircleAndEdge, 7 | CircleAndBox, 8 | EdgeAndBox, 9 | BoxAndBox, 10 | NotSupport 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Physics2D/Collision/ParticleCollisionDetector.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision 2 | { 3 | using System; 4 | using Physics2D.Collision.Shapes; 5 | using Physics2D.Common; 6 | 7 | public class ParticleCollisionDetector 8 | { 9 | /// 10 | /// 检测圆形与圆形间的碰撞 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static ParticleContact CircleAndCircle(Circle A, Circle B) 17 | { 18 | var d = (A.Body.Position - B.Body.Position).Length(); 19 | 20 | // 碰撞检测 21 | var l = A.R + B.R; 22 | 23 | if (d >= l) 24 | { 25 | return null; 26 | } 27 | 28 | // 产生一组碰撞 29 | return new ParticleContact( 30 | A.Body, B.Body, 31 | (A.Body.Restitution + B.Body.Restitution) / 2, 32 | l - d, 33 | (A.Body.Position - B.Body.Position).Normalize()); 34 | } 35 | 36 | /// 37 | /// 检测圆形与边缘的碰撞 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | public static ParticleContact CircleAndEdge(Circle circle, Edge edge) 44 | { 45 | var BA = edge.PointB - edge.PointA; 46 | var BALengthSquared = BA.LengthSquared(); 47 | 48 | var intersectionPoint = MathHelper.LineIntersection( 49 | circle.Body.PrePosition, 50 | circle.Body.Position, 51 | edge.PointA, 52 | edge.PointB); 53 | 54 | // 检测物体的运动路径是否发生穿越 55 | if (intersectionPoint != null) 56 | { 57 | // 发生穿越则认为发生碰撞 将质体位置退至相交点 58 | circle.Body.Position = (Vector2D)intersectionPoint; 59 | } 60 | 61 | // 若未发生穿越则计算 62 | double n1 = (edge.PointA - circle.Body.Position) * (edge.PointA - edge.PointB); 63 | double n2 = (edge.PointB - circle.Body.Position) * (edge.PointA - edge.PointB); 64 | 65 | if (n1 * n2 <= 0) 66 | { 67 | // 圆心的投影在线上 68 | var normal = BA * (circle.Body.Position - edge.PointA) * BA / BALengthSquared; 69 | normal = (circle.Body.Position - edge.PointA) - normal; 70 | 71 | // 线到圆心的距离 72 | var d = normal.Length(); 73 | if (circle.R > d) 74 | { 75 | // 针对圆心正好处于线上的情况的作处理 76 | if (Math.Abs(d) < Settings.Percision) 77 | { 78 | normal = BA * (circle.Body.PrePosition - edge.PointA) * BA / BALengthSquared; 79 | normal = (circle.Body.PrePosition - edge.PointA) - normal; 80 | } 81 | 82 | return new ParticleContact( 83 | circle.Body, null, 84 | circle.Body.Restitution, 85 | circle.R - d, 86 | normal.Normalize()); 87 | } 88 | } 89 | else 90 | { 91 | // 圆心的投影在线外 92 | var dAO = (circle.Body.Position - edge.PointA).LengthSquared(); 93 | var dBO = (circle.Body.Position - edge.PointB).LengthSquared(); 94 | if (circle.R * circle.R > Math.Min(dAO, dBO)) 95 | { 96 | var normal = circle.Body.Position - (dAO < dBO ? edge.PointA : edge.PointB); 97 | return new ParticleContact( 98 | circle.Body, null, 99 | circle.Body.Restitution, 100 | circle.R - Math.Sqrt(dAO < dBO ? dAO : dBO), 101 | normal.Normalize()); 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Physics2D/Collision/ParticleContact.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision 2 | { 3 | using System; 4 | using Physics2D.Common; 5 | using Physics2D.Object; 6 | 7 | public class ParticleContact 8 | { 9 | /// 10 | /// 接触物体A 11 | /// 12 | public PhysicsObject PA; 13 | 14 | /// 15 | /// 接触物体B 16 | /// 17 | public PhysicsObject PB; 18 | 19 | /// 20 | /// 碰撞恢复系数 21 | /// 22 | public double Restitution; 23 | 24 | /// 25 | /// 相交深度 26 | /// 27 | public double Penetration; 28 | 29 | /// 30 | /// 碰撞法线 31 | /// 32 | public Vector2D ContactNormal; 33 | 34 | /// 35 | /// 物体A的移动 36 | /// 37 | public Vector2D MovementA; 38 | 39 | /// 40 | /// 物体B的移动 41 | /// 42 | public Vector2D MovementB; 43 | 44 | public ParticleContact( 45 | PhysicsObject a, PhysicsObject b, 46 | double restitution, 47 | double penetration, 48 | Vector2D contactNormal) 49 | { 50 | this.PA = a; 51 | this.PB = b; 52 | this.Restitution = restitution; 53 | this.Penetration = penetration; 54 | this.ContactNormal = contactNormal; 55 | this.MovementA = this.MovementB = Vector2D.Zero; 56 | } 57 | 58 | /// 59 | /// 解决碰撞问题 60 | /// 解决速度及相交 61 | /// 62 | /// 持续时间 63 | public void Resolve(double duration) 64 | { 65 | this.ResolveInterpenetration(duration); 66 | this.ResolveVelocity(duration); 67 | } 68 | 69 | /// 70 | /// 计算分离速度 71 | /// 72 | /// 分离速度 73 | public double CalculateSeparatingVelocity() 74 | { 75 | return (this.PA.Velocity - (this.PB?.Velocity ?? Vector2D.Zero)) * this.ContactNormal; 76 | } 77 | 78 | /// 79 | /// 解决碰撞后速度 80 | /// 81 | /// 82 | private void ResolveVelocity(double duration) 83 | { 84 | double separatingVelocity = this.CalculateSeparatingVelocity(); 85 | 86 | if (separatingVelocity > 0) 87 | { 88 | // 两个物体正在分离 89 | return; 90 | } 91 | 92 | double newSeparatingVelocity = -separatingVelocity * this.Restitution; 93 | 94 | // 检查仅由加速度产生的速度 95 | double accCausedSeparatingVelocity = (this.PA.Acceleration - (this.PB?.Acceleration ?? Vector2D.Zero)) * this.ContactNormal * duration; 96 | if (accCausedSeparatingVelocity < 0) 97 | { 98 | // 补偿由加速度产生的速度 99 | newSeparatingVelocity += this.Restitution * accCausedSeparatingVelocity; 100 | 101 | // 避免过度补偿 102 | if (newSeparatingVelocity < 0) 103 | { 104 | newSeparatingVelocity = 0; 105 | } 106 | } 107 | 108 | double deltaVelocity = newSeparatingVelocity - separatingVelocity; 109 | double totalInverseMass = this.PA.InverseMass + (this.PB?.InverseMass ?? 0); 110 | 111 | // 两个物体全为固定或匀速物体则不处理 112 | if (totalInverseMass <= 0) 113 | { 114 | return; 115 | } 116 | 117 | // 计算冲量 118 | double impulse = deltaVelocity / totalInverseMass; 119 | 120 | // 施加冲量 121 | var impulsePerIMass = this.ContactNormal * impulse; 122 | this.PA.Velocity += impulsePerIMass * this.PA.InverseMass; 123 | 124 | if (this.PB != null) 125 | this.PB.Velocity -= impulsePerIMass * this.PB.InverseMass; 126 | else 127 | { 128 | // 静态碰撞的处理 129 | 130 | // 计算PA在碰撞法线上的速度分量 131 | //var vP = PA.Velocity * ContactNormal; 132 | //// 计算PA加速度在未来产生的速度在碰撞法线上的分量 133 | //var vF = PA.Acceleration * duration * ContactNormal; 134 | //if (Math.Abs(vP + vF) < Math.Abs(vF)) 135 | //{ 136 | // PA.Velocity -= vP * ContactNormal; 137 | //} 138 | } 139 | } 140 | 141 | /// 142 | /// 解决碰撞后相交 143 | /// 144 | /// 145 | private void ResolveInterpenetration(double duration) 146 | { 147 | // 对象未相交 148 | if (this.Penetration <= 0) 149 | { 150 | return; 151 | } 152 | 153 | double vA = Math.Abs(this.PA.Velocity * this.ContactNormal); 154 | double vB = Math.Abs(this.PB?.Velocity * this.ContactNormal ?? .0); 155 | var totalVec = vA + vB; 156 | 157 | // 不处理两个均为固定或常速运动的物体 158 | double totalInverseMass = this.PA.InverseMass + (this.PB?.InverseMass ?? 0); 159 | if (totalInverseMass <= 0) 160 | { 161 | return; 162 | } 163 | 164 | // 两质体速度和不为0时根据速度将物体分离 165 | if (Math.Abs(totalVec) > Settings.Percision) 166 | { 167 | var totalInverseVec = 1 / totalVec; 168 | this.MovementA = this.ContactNormal * this.Penetration * totalInverseVec * vA; 169 | this.MovementB = -this.ContactNormal * this.Penetration * totalInverseVec * vB; 170 | } 171 | 172 | // 两质体速度和为0时根据质量将物体分离 173 | else 174 | { 175 | var movePerIMass = this.ContactNormal * (this.Penetration / totalInverseMass); 176 | 177 | this.MovementA = this.PA.InverseMass * movePerIMass; 178 | this.MovementB = -this.PB?.InverseMass * movePerIMass ?? Vector2D.Zero; 179 | } 180 | 181 | this.PA.Position += this.MovementA; 182 | if (this.PB != null) 183 | { 184 | this.PB.Position += this.MovementB; 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Physics2D/Collision/ParticleContactGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision 2 | { 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// 碰撞生成器 8 | /// 9 | public abstract class ParticleContactGenerator : IEnumerable 10 | { 11 | public abstract IEnumerator GetEnumerator(); 12 | 13 | IEnumerator IEnumerable.GetEnumerator() 14 | { 15 | return this.GetEnumerator(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Physics2D/Collision/ParticleContactResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: InternalsVisibleTo("UnitTest")] 5 | 6 | namespace Physics2D.Collision 7 | { 8 | internal class ParticleContactResolver 9 | { 10 | public ParticleContactResolver(int iterations) 11 | { 12 | this.Iterations = iterations; 13 | } 14 | 15 | public void ResolveContacts(List contactList, double duration) 16 | { 17 | if (contactList.Count == 0) 18 | { 19 | return; 20 | } 21 | 22 | int iterationsUsed = 0; 23 | while (iterationsUsed++ < this.Iterations) 24 | { 25 | // 找到权值最小(碰撞程度最为严重)的一组碰撞 优先处理 26 | double max = 0; 27 | int maxI = contactList.Count; 28 | for (int i = 0; i < contactList.Count; i++) 29 | { 30 | // 计算权值 = 分离速度 * 时间 - 相交深度 31 | double weight = contactList[i].CalculateSeparatingVelocity() * duration - contactList[i].Penetration; 32 | if (weight < max) 33 | { 34 | max = weight; 35 | maxI = i; 36 | } 37 | } 38 | 39 | // 解决碰撞 40 | if (maxI == contactList.Count) 41 | { 42 | break; 43 | } 44 | 45 | contactList[maxI].Resolve(duration); 46 | 47 | // 更新相交长度 48 | var maxItem = contactList[maxI]; 49 | var movementA = contactList[maxI].MovementA; 50 | var movementB = contactList[maxI].MovementB; 51 | foreach (var item in contactList) 52 | { 53 | if (item.PA == maxItem.PA) 54 | { 55 | item.Penetration -= movementA * item.ContactNormal; 56 | } 57 | else if (item.PA == maxItem.PB) 58 | { 59 | item.Penetration -= movementB * item.ContactNormal; 60 | } 61 | 62 | if (item.PB != null) 63 | { 64 | if (item.PB == maxItem.PA) 65 | { 66 | item.Penetration += movementA * item.ContactNormal; 67 | } 68 | else if (item.PB == maxItem.PB) 69 | { 70 | item.Penetration += movementB * item.ContactNormal; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | public int Iterations { get; set; } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Physics2D/Collision/Shapes/Circle.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Shapes 2 | { 3 | public sealed class Circle : Shape 4 | { 5 | public double R; 6 | 7 | public Circle(double r, int id = 0) 8 | { 9 | this.R = r; 10 | this.Id = id; 11 | } 12 | 13 | public override ShapeType Type 14 | { 15 | get { return ShapeType.Circle; } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Physics2D/Collision/Shapes/Edge.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Shapes 2 | { 3 | using Physics2D.Common; 4 | 5 | public sealed class Edge : Shape 6 | { 7 | public Vector2D PointA; 8 | public Vector2D PointB; 9 | 10 | public Edge(Vector2D pA, Vector2D pB) 11 | { 12 | this.PointA = pA; 13 | this.PointB = pB; 14 | } 15 | 16 | public Edge(double x1, double y1, double x2, double y2) 17 | { 18 | this.PointA = new Vector2D(x1, y1); 19 | this.PointB = new Vector2D(x2, y2); 20 | } 21 | 22 | public override ShapeType Type 23 | { 24 | get { return ShapeType.Edge; } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Physics2D/Collision/Shapes/Point.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Shapes 2 | { 3 | /// 4 | /// 点形状适用于无形体的物体,通常将其作为物体的默认形状 5 | /// 点状物体不参与碰撞检测 6 | /// 7 | public sealed class Point : Shape 8 | { 9 | public override ShapeType Type 10 | { 11 | get { return ShapeType.Point; } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Physics2D/Collision/Shapes/Shape.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Shapes 2 | { 3 | using Physics2D.Object; 4 | 5 | public abstract class Shape 6 | { 7 | /// 8 | /// 绑定的物体 9 | /// 10 | public PhysicsObject Body; 11 | 12 | /// 13 | /// 标识符 14 | /// 标识符相同且不为0的形状不执行碰撞检测 15 | /// 16 | public int Id = 0; 17 | 18 | /// 19 | /// 标识符基数 20 | /// 21 | private static int idBase = 1; 22 | 23 | /// 24 | /// 产生一个新的Id 25 | /// 26 | /// 27 | public static int NewId() => idBase++; 28 | 29 | /// 30 | /// 返回当前形状的类型 31 | /// 32 | public abstract ShapeType Type 33 | { 34 | get; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Physics2D/Collision/Shapes/ShapeType.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Collision.Shapes 2 | { 3 | public enum ShapeType 4 | { 5 | Point = -1, 6 | Circle = 0, 7 | Edge = 1, 8 | Box = 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Physics2D/Common/Events/ContactEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Common.Events 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Physics2D.Collision; 6 | 7 | public delegate void ContactHandle(object sender, ContactEventArgs e); 8 | 9 | public sealed class ContactEventArgs : EventArgs 10 | { 11 | public IReadOnlyList ContactList { get; } 12 | 13 | public ContactEventArgs(IReadOnlyList contact) 14 | { 15 | this.ContactList = contact; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Physics2D/Common/Exceptions/InvalidArgumentException.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class InvalidArgumentException : ArgumentException 6 | { 7 | public InvalidArgumentException(string message, string paramName) 8 | : base(message, paramName) 9 | { } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Physics2D/Common/MathHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Common 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public static class MathHelper 7 | { 8 | /// 9 | /// 计算一个点到一个线段的向量 10 | /// 返回点到线的距离最短的向量 11 | /// 12 | /// 点 13 | /// 线段端点A 14 | /// 线段端点B 15 | /// 16 | public static Vector2D PointToLineVector(Vector2D point, Vector2D linePA, Vector2D linePB) 17 | { 18 | var lineBA = linePB - linePA; 19 | var normal = lineBA * (point - linePA) * lineBA / lineBA.LengthSquared() - (point - linePA); 20 | 21 | return normal; 22 | } 23 | 24 | /// 25 | /// 计算两个线段的交点 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | public static Vector2D? LineIntersection(Vector2D lineA1, Vector2D lineA2, Vector2D lineB1, Vector2D lineB2) 33 | { 34 | var area1 = SignedTriangleArea(lineA1, lineA2, lineB2); 35 | var area2 = SignedTriangleArea(lineA1, lineA2, lineB1); 36 | 37 | if (area1 * area2 < 0) 38 | { 39 | var area3 = SignedTriangleArea(lineB1, lineB2, lineA1); 40 | var area4 = area3 + area2 - area1; 41 | if (area3 * area4 < 0) 42 | { 43 | var intersectionScale = area3 / (area3 - area4); 44 | var intersectionPoint = (lineA2 - lineA1) * intersectionScale + lineA1; 45 | 46 | // 返回交点 47 | return intersectionPoint; 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /// 55 | /// 计算三角形的有向面积 56 | /// 57 | /// 顶点A 58 | /// 顶点B 59 | /// 顶点C 60 | /// 61 | public static double SignedTriangleArea(Vector2D a, Vector2D b, Vector2D c) 62 | { 63 | return (a.X - c.X) * (b.Y - c.Y) - (a.Y - c.Y) * (b.X - c.X); 64 | } 65 | 66 | /// 67 | /// 测试一个点是否在一个多边形的内部 68 | /// 69 | /// 多边形的顺时针点集,最后一个点应当与第一个点为同一点 70 | /// 71 | /// 72 | public static bool IsInside(IReadOnlyList vertexs, Vector2D point) 73 | { 74 | int count = 0; 75 | int num = vertexs.Count() - 1; 76 | 77 | if (num < 3) 78 | { 79 | // 当点集不能围成多边形时该函数永假 80 | return false; 81 | } 82 | 83 | for (int i = 0; i < num; i++) 84 | { 85 | var slope = (vertexs[i + 1].Y - vertexs[i].Y) / (vertexs[i + 1].X - vertexs[i].X); 86 | bool cond1 = (vertexs[i].X <= point.X) && (point.X < vertexs[i + 1].X); 87 | bool cond2 = (vertexs[i + 1].X <= point.X) && (point.X < vertexs[i].X); 88 | bool above = point.Y < slope * (point.X - vertexs[i].X) + vertexs[i].Y; 89 | if ((cond1 || cond2) && above) 90 | { 91 | count++; 92 | } 93 | } 94 | 95 | return count % 2 != 0; 96 | } 97 | 98 | /// 99 | /// 计算点到直线的距离的平方 100 | /// 101 | /// 102 | /// 103 | /// 104 | /// 105 | public static double PointToLineDistenceSquared(Vector2D point, Vector2D linePA, Vector2D linePB) 106 | { 107 | return PointToLineVector(point, linePA, linePB).LengthSquared(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Physics2D/Common/Vector2D.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Common 2 | { 3 | using System; 4 | using static System.Math; 5 | 6 | public struct Vector2D : IEquatable 7 | { 8 | public double X; 9 | public double Y; 10 | 11 | public static Vector2D Zero { get; } = new Vector2D(); 12 | 13 | public static Vector2D One { get; } = new Vector2D(1, 1); 14 | 15 | public static Vector2D UnitX { get; } = new Vector2D(1, 0); 16 | 17 | public static Vector2D UnitY { get; } = new Vector2D(0, 1); 18 | 19 | public Vector2D(double x, double y) 20 | { 21 | this.X = x; 22 | this.Y = y; 23 | } 24 | 25 | public Vector2D(Vector2D vec) 26 | { 27 | this.X = vec.X; 28 | this.Y = vec.Y; 29 | } 30 | 31 | public static double DistanceSquared(Vector2D value1, Vector2D value2) 32 | { 33 | return (value1.X - value2.X) * (value1.X - value2.X) + (value1.Y - value2.Y) * (value1.Y - value2.Y); 34 | } 35 | 36 | public static double Distance(Vector2D value1, Vector2D value2) => Sqrt(DistanceSquared(value1, value2)); 37 | 38 | public double LengthSquared() => DistanceSquared(this, Zero); 39 | 40 | public double Length() => Distance(this, Zero); 41 | 42 | public static Vector2D Normalize(Vector2D value) 43 | { 44 | double distance = Distance(value, Zero); 45 | 46 | // 零向量标准化仍为零向量 47 | if (distance == 0) 48 | { 49 | return Zero; 50 | } 51 | 52 | var factor = 1 / distance; 53 | 54 | return new Vector2D(value.X * factor, value.Y * factor); 55 | } 56 | 57 | public Vector2D Normalize() => Normalize(this); 58 | 59 | public void Set(double x, double y) 60 | { 61 | this.X = x; 62 | this.Y = y; 63 | } 64 | 65 | public static Vector2D operator +(Vector2D left, Vector2D right) => new Vector2D(left.X + right.X, left.Y + right.Y); 66 | 67 | public static Vector2D operator -(Vector2D left, Vector2D right) => new Vector2D(left.X - right.X, left.Y - right.Y); 68 | 69 | public static Vector2D operator -(Vector2D right) => new Vector2D(.0 - right.X, .0 - right.Y); 70 | 71 | public static double operator *(Vector2D left, Vector2D right) => left.X * right.X + left.Y * right.Y; 72 | 73 | public static Vector2D operator *(Vector2D left, double factor) => new Vector2D(left.X * factor, left.Y * factor); 74 | 75 | public static Vector2D operator *(double factor, Vector2D right) => right * factor; 76 | 77 | public static Vector2D operator /(Vector2D left, double divisor) => new Vector2D(left.X / divisor, left.Y / divisor); 78 | 79 | public static bool operator ==(Vector2D left, Vector2D right) => Abs(left.X - right.X) < Settings.Percision && Abs(left.Y - right.Y) < Settings.Percision; 80 | 81 | public static bool operator !=(Vector2D left, Vector2D right) => !(left == right); 82 | 83 | public override bool Equals(object obj) 84 | { 85 | if (ReferenceEquals(null, obj)) 86 | { 87 | return false; 88 | } 89 | 90 | return obj is Vector2D && this.Equals((Vector2D)obj); 91 | } 92 | 93 | public bool Equals(Vector2D other) => Abs(this.X - other.X) < Settings.Percision && Abs(this.Y - other.Y) < Settings.Percision; 94 | 95 | public override string ToString() => $"({this.X:f2}, {this.Y:f2})"; 96 | 97 | public override int GetHashCode() => base.GetHashCode(); 98 | } 99 | } -------------------------------------------------------------------------------- /Physics2D/ConvertUnits.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D 2 | { 3 | using Physics2D.Common; 4 | 5 | /// 6 | /// The units convert class. 7 | /// This is a static helper class for convert units between real world unit and graphic units. 8 | /// 9 | public static class ConvertUnits 10 | { 11 | /// 12 | /// The radio of display units to physical units. 13 | /// 14 | private static double displayUnitsToSimUnitsRatio = 50; 15 | 16 | /// 17 | /// The radio of physical units to display units. 18 | /// 19 | private static double simUnitsToDisplayUnitsRatio = 1 / displayUnitsToSimUnitsRatio; 20 | 21 | /// 22 | /// Set the ratio. 23 | /// 24 | /// The ratio of display units to physical units. 25 | public static void SetDisplayUnitToSimUnitRatio(double displayUnitsPerSimUnit) 26 | { 27 | displayUnitsToSimUnitsRatio = displayUnitsPerSimUnit; 28 | simUnitsToDisplayUnitsRatio = 1 / displayUnitsPerSimUnit; 29 | } 30 | 31 | /// 32 | /// Convert to display size. 33 | /// 34 | /// The double type size by physical units. 35 | /// The size by display units. 36 | public static int ToDisplayUnits(this double simUnits) 37 | { 38 | return (int)(simUnits * displayUnitsToSimUnitsRatio); 39 | } 40 | 41 | /// 42 | /// Convert to display size. 43 | /// 44 | /// The int type size by physical units. 45 | /// The size by display units. 46 | public static int ToDisplayUnits(this int simUnits) 47 | { 48 | return (int)(simUnits * displayUnitsToSimUnitsRatio); 49 | } 50 | 51 | /// 52 | /// Convert to display size. 53 | /// 54 | /// The type size by physical units. 55 | /// The size by display units. 56 | public static Vector2D ToDisplayUnits(this Vector2D simUnits) 57 | { 58 | return simUnits * displayUnitsToSimUnitsRatio; 59 | } 60 | 61 | /// 62 | /// Convert to physical size. 63 | /// 64 | /// The double type size by display units. 65 | /// The size by physical units. 66 | public static double ToSimUnits(this double displayUnits) 67 | { 68 | return displayUnits / displayUnitsToSimUnitsRatio; 69 | } 70 | 71 | /// 72 | /// Convert to physical size. 73 | /// 74 | /// The int type size by display units. 75 | /// The size by physical units. 76 | public static double ToSimUnits(this int displayUnits) 77 | { 78 | return displayUnits / displayUnitsToSimUnitsRatio; 79 | } 80 | 81 | /// 82 | /// Convert to physical size. 83 | /// 84 | /// The type size by display units. 85 | /// The size by physical units. 86 | public static Vector2D ToSimUnits(this Vector2D displayUnits) 87 | { 88 | return displayUnits / displayUnitsToSimUnitsRatio; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Physics2D/Core/ForceRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Core 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Force; 5 | using Physics2D.Object; 6 | 7 | /// 8 | /// 粒子的作用力发生器管理模块 9 | /// 10 | public sealed class ForceRegistry 11 | { 12 | /// 13 | /// 作用力发生器集合 14 | /// 15 | private readonly HashSet generators = new HashSet(); 16 | 17 | /// 18 | /// 添加一个新项目 19 | /// 20 | /// 作用力发生器 21 | public void Add(ParticleForceGenerator forceGenerator) => this.generators.Add(forceGenerator); 22 | 23 | /// 24 | /// 删除一个作用力发生器 25 | /// 26 | /// 作用力发生器 27 | public void Remove(ParticleForceGenerator forceGenerator) => this.generators.Remove(forceGenerator); 28 | 29 | /// 30 | /// 删除一个项目 31 | /// 依据粒子进行删除,只要包含该粒子,即执行删除操作 32 | /// 33 | /// 粒子 34 | public void Remove(Particle particle) 35 | { 36 | foreach (var particleForceGenerator in this.generators) 37 | { 38 | particleForceGenerator.Remove(particle); 39 | } 40 | } 41 | 42 | /// 43 | /// 执行所有的作用力发生器 44 | /// 45 | /// 46 | public void Update(double duration) 47 | { 48 | foreach (var particleForceGenerator in this.generators) 49 | { 50 | particleForceGenerator.Apply(duration); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Physics2D/Core/World.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Core 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Force.Zones; 9 | using Physics2D.Object; 10 | using Physics2D.Object.Tools; 11 | 12 | public sealed class World 13 | { 14 | /// 15 | /// 物体集合 16 | /// 17 | private readonly HashSet objects; 18 | 19 | /// 20 | /// 边缘集合 21 | /// 22 | private readonly HashSet edges; 23 | 24 | /// 25 | /// Pin集合 26 | /// 27 | private readonly Dictionary pins = new Dictionary(); 28 | 29 | /// 30 | /// 作用力区域集合 31 | /// 32 | public readonly HashSet Zones = new HashSet(); 33 | 34 | /// 35 | /// 质体作用力管理器 36 | /// 37 | public readonly ForceRegistry ForceGenerators = new ForceRegistry(); 38 | 39 | /// 40 | /// 质体碰撞管理器 41 | /// 42 | public readonly ContactRegistry ContactGenerators; 43 | 44 | public World() 45 | { 46 | this.objects = new HashSet(); 47 | this.edges = new HashSet(); 48 | this.ContactGenerators = new ContactRegistry(this.objects, this.edges); 49 | } 50 | 51 | /// 52 | /// 向物理世界中添加一个物体 53 | /// 54 | /// 55 | public void AddObject(PhysicsObject obj) 56 | { 57 | this.objects.Add(obj); 58 | } 59 | 60 | /// 61 | /// 从物理世界中移除一个物体 62 | /// 63 | /// 64 | public void RemoveObject(PhysicsObject obj) 65 | { 66 | this.objects.Remove(obj); 67 | 68 | // 仅在物体为质体时执行注销操作 69 | var particle = obj as Particle; 70 | if (particle != null) 71 | { 72 | this.ForceGenerators.Remove(particle); 73 | } 74 | } 75 | 76 | /// 77 | /// 向物理世界添加一个定制的物体 78 | /// 79 | /// 80 | public void AddObject(CustomObject obj) 81 | { 82 | this.AddObject((PhysicsObject)obj); 83 | obj.OnInit(this); 84 | } 85 | 86 | /// 87 | /// 从物理世界移除一个定制的物体 88 | /// 89 | /// 90 | public void RemoveObject(CustomObject obj) 91 | { 92 | this.RemoveObject((PhysicsObject)obj); 93 | obj.OnRemove(this); 94 | } 95 | 96 | /// 97 | /// 向物理世界添加一条边 98 | /// 99 | /// 100 | public void AddEdge(Edge edge) 101 | { 102 | this.edges.Add(edge); 103 | } 104 | 105 | /// 106 | /// 从物理世界移除一条边 107 | /// 108 | /// 109 | public void RemoveEdge(Edge edge) 110 | { 111 | this.edges.Remove(edge); 112 | } 113 | 114 | /// 115 | /// 将制定物体Pin在物理世界 116 | /// 117 | /// 118 | /// 119 | /// 120 | public Handle Pin(IPin obj, Vector2D position) 121 | { 122 | if (!this.pins.ContainsKey(obj)) 123 | { 124 | this.pins[obj] = obj.Pin(this, position); 125 | } 126 | else 127 | { 128 | throw new InvalidOperationException("Can't pin target object which was alreadly pinned."); 129 | } 130 | 131 | return this.pins[obj]; 132 | } 133 | 134 | /// 135 | /// 解除指定物体在物理世界的Pin 136 | /// 137 | /// 138 | public void UnPin(IPin obj) 139 | { 140 | if (this.pins.ContainsKey(obj)) 141 | { 142 | obj.Unpin(this); 143 | this.pins[obj].Release(); 144 | this.pins.Remove(obj); 145 | } 146 | else 147 | { 148 | throw new InvalidOperationException("Can't unpin target object which was not pinned."); 149 | } 150 | } 151 | 152 | /// 153 | /// 按时间间隔更新整个物理世界 154 | /// 155 | /// 时间间隔 156 | public void Update(double duration) 157 | { 158 | // 为粒子施加作用力 159 | this.ForceGenerators.Update(duration); 160 | 161 | // 更新物理对象 162 | Parallel.ForEach(this.objects, item => 163 | { 164 | // 为物理对象施加区域作用力 165 | foreach (var z in this.Zones) 166 | { 167 | z.TryApplyTo(item, duration); 168 | } 169 | 170 | // 对物理对象进行积分 171 | item.Update(duration); 172 | }); 173 | 174 | // 质体碰撞检测 175 | this.ContactGenerators.ResolveContacts(duration); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /Physics2D/Factories/ContactFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Factories 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Collision; 5 | using Physics2D.Collision.Basic; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Common.Exceptions; 9 | using Physics2D.Core; 10 | using Physics2D.Object; 11 | 12 | public static class ContactFactory 13 | { 14 | /// 15 | /// 在物理世界创建一条边缘 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static Edge CreateEdge(this World world, Vector2D pointA, Vector2D pointB) 24 | { 25 | var edge = new Edge(pointA, pointB); 26 | world.AddEdge(edge); 27 | return edge; 28 | } 29 | 30 | /// 31 | /// 在物理世界创建一条边缘 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | public static Edge CreateEdge(this World world, double x1, double y1, double x2, double y2) 40 | { 41 | var edge = new Edge(x1, y1, x2, y2); 42 | world.AddEdge(edge); 43 | return edge; 44 | } 45 | 46 | /// 47 | /// 在物理世界创建一个由边缘围成的封闭多边形 48 | /// 49 | /// 50 | /// 多边形点集(逆时针) 51 | /// 52 | public static IEnumerable CreatePolygonEdge(this World world, params Vector2D[] points) 53 | { 54 | List result = new List(); 55 | 56 | if (points.Length < 3) 57 | { 58 | throw new InvalidArgumentException( 59 | $"Can't create a polygon by given points. points.Length = {points.Length}", nameof(points)); 60 | } 61 | 62 | for (int i = 1; i < points.Length; i++) 63 | { 64 | result.Add(world.CreateEdge(points[i - 1], points[i])); 65 | } 66 | 67 | result.Add(world.CreateEdge(points[points.Length - 1], points[0])); 68 | return result; 69 | } 70 | 71 | /// 72 | /// 在物理世界创建一条质体绳索 73 | /// 该绳索由两个质体(质点)组成,两个质体的距离被限定在指定的数值内 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// 79 | /// 80 | /// 81 | public static ParticleRope CreateRope(this World world, double maxLength, double restitution, Particle a, Particle b) 82 | { 83 | var rope = new ParticleRope(maxLength, restitution, a, b); 84 | return world.CreateContact(rope); 85 | } 86 | 87 | /// 88 | /// 在物理世界创建一根质体硬棒 89 | /// 该硬棒由两个质体(质点)组成,两个质体的距离不会发生变化 90 | /// 91 | /// 92 | /// 93 | /// 94 | /// 95 | public static ParticleRod CreateRod(this World world, Particle a, Particle b) 96 | { 97 | var rod = new ParticleRod(a, b); 98 | return world.CreateContact(rod); 99 | } 100 | 101 | /// 102 | /// 在物理世界中创建一组关联 103 | /// 104 | /// 105 | /// 106 | /// 107 | /// 108 | public static T CreateContact(this World world, T contact) 109 | where T : ParticleContactGenerator 110 | { 111 | world.ContactGenerators.Add(contact); 112 | return contact; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Physics2D/Factories/ParticleFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Factories 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Core; 5 | using Physics2D.Object; 6 | 7 | /// 8 | /// 粒子工厂 9 | /// 10 | public static class ParticleFactory 11 | { 12 | /// 13 | /// 创建一个粒子 14 | /// 15 | /// 物理世界 16 | /// 初位置 17 | /// 初速度 18 | /// 质量 19 | /// 20 | public static Particle CreateParticle(this World world, Vector2D p, Vector2D v, double m, double restitution = 1) 21 | { 22 | var particle = new Particle 23 | { 24 | Position = p, 25 | Mass = m, 26 | Velocity = v, 27 | PrePosition = p, 28 | Restitution = restitution 29 | }; 30 | world.AddObject(particle); 31 | return particle; 32 | } 33 | 34 | /// 35 | /// 创建固定不动的粒子 36 | /// 37 | /// 物理世界 38 | /// 初位置 39 | /// 40 | public static Particle CreateFixedParticle(this World world, Vector2D p) 41 | { 42 | var particle = new Particle 43 | { 44 | Position = p, 45 | InverseMass = 0f, 46 | PrePosition = p 47 | }; 48 | world.AddObject(particle); 49 | return particle; 50 | } 51 | 52 | /// 53 | /// 创建永远保持匀速运动的粒子 54 | /// 55 | /// 物理世界 56 | /// 初位置 57 | /// 初速度 58 | /// 59 | public static Particle CreateUnstoppableParticle(this World world, Vector2D p, Vector2D v) 60 | { 61 | var particle = new Particle 62 | { 63 | Position = p, 64 | InverseMass = 0f, 65 | Velocity = v, 66 | PrePosition = p 67 | }; 68 | world.AddObject(particle); 69 | return particle; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Physics2D/Factories/ZoneFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Factories 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Core; 5 | using Physics2D.Force; 6 | using Physics2D.Force.Zones; 7 | 8 | /// 9 | /// 区域作用力工厂 10 | /// 11 | public static class ZoneFactory 12 | { 13 | /// 14 | /// 在物理世界创建一个全局作用力区域 15 | /// 16 | /// 物理世界 17 | /// 作用力发生器 18 | /// 19 | public static GlobalZone CreateGlobalZone( 20 | this World world, 21 | ParticleForceGenerator particleForceGenerator) 22 | { 23 | var zone = new GlobalZone(); 24 | return world.CreateZone(zone, particleForceGenerator); 25 | } 26 | 27 | /// 28 | /// 在物理世界创建一个矩形区域作用力 29 | /// 30 | /// 物理世界 31 | /// 作用力发生器 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static RectangleZone CreateRectangleZone( 38 | this World world, 39 | ParticleForceGenerator particleForceGenerator, 40 | double x1, 41 | double y1, 42 | double x2, 43 | double y2) 44 | { 45 | var zone = new RectangleZone(x1, y1, x2, y2); 46 | return world.CreateZone(zone, particleForceGenerator); 47 | } 48 | 49 | /// 50 | /// 为物理世界创建重力 51 | /// 重力的方向向下 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static GlobalZone CreateGravity(this World world, double g) 57 | { 58 | return world.CreateGlobalZone(new ParticleGravity(new Vector2D(0, g))); 59 | } 60 | 61 | /// 62 | /// 在物理世界中创建一个区域 63 | /// 64 | /// 65 | /// 66 | /// 67 | /// 68 | /// 69 | public static T CreateZone( 70 | this World world, 71 | T zone, 72 | ParticleForceGenerator particleForceGenerator) 73 | where T : Zone 74 | { 75 | zone.Add(particleForceGenerator); 76 | world.Zones.Add(zone); 77 | return zone; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Physics2D/Force/ParticleConstantForce.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Object; 5 | 6 | public class ParticleConstantForce : ParticleForceGenerator 7 | { 8 | private readonly Vector2D force; 9 | 10 | public ParticleConstantForce(Vector2D force) 11 | { 12 | this.force = force; 13 | } 14 | 15 | public override void ApplyTo(Particle particle, double duration) 16 | { 17 | particle.AddForce(this.force); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Physics2D/Force/ParticleDrag.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Object; 5 | 6 | public class ParticleDrag : ParticleForceGenerator 7 | { 8 | private readonly double k1; 9 | private readonly double k2; 10 | 11 | public ParticleDrag(double k1, double k2) 12 | { 13 | this.k1 = k1; 14 | this.k2 = k2; 15 | } 16 | 17 | public override void ApplyTo(Particle particle, double duration) 18 | { 19 | if (particle.Velocity == Vector2D.Zero) 20 | { 21 | return; 22 | } 23 | 24 | double c = particle.Velocity.Length(); 25 | c = this.k1 * c + this.k2 * c * c; 26 | 27 | particle.AddForce(particle.Velocity.Normalize() * -c); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Physics2D/Force/ParticleElastic.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Object; 5 | 6 | public class ParticleElastic : ParticleForceGenerator 7 | { 8 | private readonly List linked = new List(); 9 | 10 | private readonly double k; 11 | private readonly double length; 12 | 13 | public ParticleElastic(double k, double length) 14 | { 15 | this.k = k; 16 | this.length = length; 17 | } 18 | 19 | public void LinkWith(Particle item) 20 | { 21 | this.linked.Add(item); 22 | } 23 | 24 | public override void ApplyTo(Particle particle, double duration) 25 | { 26 | foreach (var item in this.linked) 27 | { 28 | var d = particle.Position - item.Position; 29 | 30 | double force = (this.length - d.Length()) * this.k; 31 | d.Normalize(); 32 | particle.AddForce(d * force); 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Physics2D/Force/ParticleForceGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Object; 5 | 6 | public abstract class ParticleForceGenerator 7 | { 8 | /// 9 | /// 受管理的质体集合 10 | /// 11 | protected readonly HashSet Objects = new HashSet(); 12 | 13 | /// 14 | /// 添加新的受力对象 15 | /// 16 | /// 受力对象 17 | public void Add(Particle particle) => this.Objects.Add(particle); 18 | 19 | /// 20 | /// 移除受力对象 21 | /// 22 | /// 受力对象 23 | public void Remove(Particle particle) => this.Objects.Remove(particle); 24 | 25 | /// 26 | /// 为指定质体施加作用力 27 | /// 28 | /// 受力对象 29 | /// 30 | public abstract void ApplyTo(Particle particle, double duration); 31 | 32 | /// 33 | /// 为所管理的所有对象施加作用力 34 | /// 35 | /// 36 | public void Apply(double duration) 37 | { 38 | foreach (var particle in this.Objects) 39 | { 40 | this.ApplyTo(particle, duration); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Physics2D/Force/ParticleGravity.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Object; 5 | 6 | public class ParticleGravity : ParticleForceGenerator 7 | { 8 | private readonly Vector2D gravity; 9 | 10 | public ParticleGravity(Vector2D gravity) 11 | { 12 | this.gravity = gravity; 13 | } 14 | 15 | public override void ApplyTo(Particle particle, double duration) 16 | { 17 | // 质量无限大的物体不受重力影响 18 | if (particle.InverseMass == 0) 19 | { 20 | return; 21 | } 22 | 23 | // 施加重力 24 | particle.AddForce(this.gravity * particle.Mass); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Physics2D/Force/Zones/GlobalZone.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force.Zones 2 | { 3 | /// 4 | /// 能对所有物体施加作用力的区域 5 | /// 6 | public class GlobalZone : Zone 7 | { 8 | protected override bool IsIn(Object.PhysicsObject obj) => true; 9 | } 10 | } -------------------------------------------------------------------------------- /Physics2D/Force/Zones/RectangleZone.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force.Zones 2 | { 3 | using Physics2D.Object; 4 | 5 | /// 6 | /// 在矩形内施加作用力的区域 7 | /// 8 | public class RectangleZone : Zone 9 | { 10 | public double X1 { get; } 11 | public double Y1 { get; } 12 | public double X2 { get; } 13 | public double Y2 { get; } 14 | 15 | public RectangleZone(double x1, double y1, double x2, double y2) 16 | { 17 | this.X1 = x1; 18 | this.Y1 = y1; 19 | this.X2 = x2; 20 | this.Y2 = y2; 21 | } 22 | 23 | protected override bool IsIn(PhysicsObject obj) 24 | { 25 | return obj.Position.X > this.X1 && obj.Position.X < this.X2 && 26 | obj.Position.Y > this.Y1 && obj.Position.Y < this.Y2; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Physics2D/Force/Zones/Zone.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Force.Zones 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Object; 5 | 6 | /// 7 | /// 作用力区域 8 | /// 使在区域内的粒子均受到指定的作用力 9 | /// 10 | public abstract class Zone 11 | { 12 | /// 13 | /// 区域内粒子作用力发生器 14 | /// 15 | private readonly HashSet particleForceGenerators = new HashSet(); 16 | 17 | /// 18 | /// 添加一个作用力发生器 19 | /// 20 | /// 21 | public void Add(ParticleForceGenerator particleForceGenerator) 22 | => this.particleForceGenerators.Add(particleForceGenerator); 23 | 24 | /// 25 | /// 移除一个作用力发生器 26 | /// 27 | /// 28 | public void Remove(ParticleForceGenerator particleForceGenerator) 29 | => this.particleForceGenerators.Remove(particleForceGenerator); 30 | 31 | /// 32 | /// 判断给定物体是否存在于当前区域 33 | /// 34 | /// 物体 35 | /// 36 | protected abstract bool IsIn(PhysicsObject obj); 37 | 38 | /// 39 | /// 尝试为给定物体施加作用力 40 | /// 41 | /// 给定物体 42 | /// 施加作用力的时间 43 | public void TryApplyTo(PhysicsObject obj, double duration) 44 | { 45 | if (!this.IsIn(obj)) 46 | { 47 | return; 48 | } 49 | 50 | if (obj is Particle) 51 | { 52 | foreach (var item in this.particleForceGenerators) 53 | { 54 | item.ApplyTo((Particle)obj, duration); 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Physics2D/Object/CombinedParticle.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Collision.Basic; 5 | using Physics2D.Common; 6 | using Physics2D.Common.Exceptions; 7 | using Physics2D.Core; 8 | using Physics2D.Factories; 9 | using Physics2D.Object.Tools; 10 | 11 | /// 12 | /// 由多个质点组合成的物体 13 | /// 质点由刚性连杆连接,可获得类似刚体的自由度 14 | /// 15 | public class CombinedParticle : CustomObject, IPin 16 | { 17 | /// 18 | /// 质体顶点表(顺时针) 19 | /// 20 | private readonly List vertexs = new List(); 21 | 22 | /// 23 | /// 刚性连杆表(顺时针) 24 | /// 25 | private readonly List rods = new List(); 26 | 27 | /// 28 | /// 锚点连杆表 29 | /// 30 | private List pinRods = new List(); 31 | 32 | /// 33 | /// 是否封闭(首尾相连) 34 | /// 35 | private readonly bool isClose; 36 | 37 | /// 38 | /// 质体顶点表(顺时针) 39 | /// 40 | public IReadOnlyList Vertexs => this.vertexs; 41 | 42 | /// 43 | /// 刚性连杆表(顺时针) 44 | /// 45 | public IReadOnlyList Rods => this.rods; 46 | 47 | /// 48 | /// 是否封闭(首尾相连) 49 | /// 50 | public bool IsClose => this.isClose; 51 | 52 | public IReadOnlyList PinRods => this.pinRods; 53 | 54 | /// 55 | /// 创建一个联合质体 56 | /// 57 | /// 58 | /// 59 | /// 60 | /// 61 | /// 62 | public CombinedParticle(List vertexs, double mass = 1, double restitution = 1, bool isClose = true) 63 | { 64 | var num = vertexs.Count; 65 | if (num < 2) 66 | { 67 | throw new InvalidArgumentException( 68 | $"Can't create a combined particle by given vertexs. vertexs.Count = {vertexs.Count}", nameof(vertexs)); 69 | } 70 | else if (isClose && num < 3) 71 | { 72 | throw new InvalidArgumentException( 73 | $"Can't create a closed combined particle by given vertexs. vertexs.Count = {vertexs.Count}", nameof(vertexs)); 74 | } 75 | 76 | foreach (var vertex in vertexs) 77 | { 78 | this.vertexs.Add(new Particle 79 | { 80 | Position = vertex, 81 | Mass = mass / num, 82 | Restitution = restitution 83 | }); 84 | } 85 | 86 | // 在可形成多边形的时候允许链接为封闭图形 87 | this.isClose = isClose; 88 | if (isClose && num > 2) 89 | { 90 | this.vertexs.Add(this.vertexs[0]); 91 | } 92 | 93 | // 创建刚性连杆 94 | for (int i = 1; i < this.vertexs.Count; i++) 95 | { 96 | this.rods.Add(new ParticleRod(this.vertexs[i - 1], this.vertexs[i])); 97 | } 98 | } 99 | 100 | /// 101 | /// 装载到物理世界 102 | /// 103 | /// 104 | public override void OnInit(World world) 105 | { 106 | foreach (var vertex in this.vertexs) 107 | { 108 | world.AddObject(vertex); 109 | } 110 | 111 | foreach (var rod in this.rods) 112 | { 113 | world.ContactGenerators.Add(rod); 114 | } 115 | } 116 | 117 | /// 118 | /// 从物理世界中移除 119 | /// 120 | /// 121 | public override void OnRemove(World world) 122 | { 123 | foreach (var vertex in this.vertexs) 124 | { 125 | world.RemoveObject(vertex); 126 | } 127 | } 128 | 129 | /// 130 | /// 更新物体 131 | /// 132 | /// 133 | public override void Update(double duration) 134 | { 135 | // 组织质体的位移被视为所有质体的偏移量 136 | // 该偏移量在每次施加偏移过后置0 137 | var N = this.isClose ? this.vertexs.Count - 1 : this.vertexs.Count; 138 | for (int i = 0; i < N; i++) 139 | { 140 | this.vertexs[i].Position += this.Position; 141 | } 142 | 143 | this.Position = Vector2D.Zero; 144 | } 145 | 146 | Handle IPin.Pin(World world, Vector2D position) 147 | { 148 | return this.Pin(world, position); 149 | } 150 | 151 | void IPin.Unpin(World world) 152 | { 153 | this.UnPin(world); 154 | } 155 | 156 | protected Handle Pin(World world, Vector2D position) 157 | { 158 | var pin = new Particle 159 | { 160 | Position = position, 161 | InverseMass = 0 162 | }; 163 | 164 | // 对于封闭图形,任意连接三个点即可固定住形状 165 | // 对于不封闭图形,需要每个点都连接才可固定 166 | var N = this.isClose ? 3 : this.vertexs.Count; 167 | for (int i = 0; i < N; i++) 168 | { 169 | this.pinRods.Add(world.CreateRod(this.vertexs[i], pin)); 170 | } 171 | 172 | var handle = new Handle(position); 173 | handle.PropertyChanged += (obj, e) => 174 | { 175 | var p = ((Handle)obj).Position; 176 | var d = p - pin.Position; 177 | this.Position = d; 178 | pin.Position = p; 179 | }; 180 | 181 | return handle; 182 | } 183 | 184 | protected void UnPin(World world) 185 | { 186 | // 移除连接 187 | foreach (var rod in this.pinRods) 188 | { 189 | world.ContactGenerators.Remove(rod); 190 | } 191 | 192 | this.pinRods.Clear(); 193 | 194 | // 恢复速度 195 | foreach (var vertex in this.vertexs) 196 | { 197 | vertex.Velocity = this.Velocity; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Physics2D/Object/CustomObject.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object 2 | { 3 | using Physics2D.Core; 4 | 5 | /// 6 | /// 自定义的物体需要实现该抽象类 7 | /// 8 | public abstract class CustomObject : PhysicsObject 9 | { 10 | /// 11 | /// 实现自定义的物理世界装载 12 | /// 13 | /// 14 | public abstract void OnInit(World world); 15 | 16 | /// 17 | /// 实现自定义的物理世界移除 18 | /// 19 | /// 20 | public abstract void OnRemove(World world); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Physics2D/Object/Particle.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object 2 | { 3 | using Physics2D.Common; 4 | 5 | public class Particle : PhysicsObject 6 | { 7 | /// 8 | /// 更新质体 9 | /// 10 | /// 11 | public override void Update(double duration) 12 | { 13 | this.PrePosition = this.Position; 14 | 15 | // 对位置速度以及加速度进行更新 16 | this.Acceleration = this.forceAccum * this.inverseMass; 17 | 18 | this.Position += this.Velocity * duration; 19 | this.Velocity += this.Acceleration * duration; 20 | 21 | // 清除作用力 22 | this.forceAccum = Vector2D.Zero; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Physics2D/Object/PhysicsObject.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object 2 | { 3 | using System; 4 | using Physics2D.Collision.Shapes; 5 | using Physics2D.Common; 6 | 7 | public abstract class PhysicsObject 8 | { 9 | /// 10 | /// 位置 11 | /// 12 | public Vector2D Position; 13 | 14 | /// 15 | /// 速度 16 | /// 17 | public Vector2D Velocity; 18 | 19 | /// 20 | /// 加速度 21 | /// 22 | public Vector2D Acceleration; 23 | 24 | /// 25 | /// 上一帧的位置 26 | /// 27 | public Vector2D PrePosition; 28 | 29 | /// 30 | /// 碰撞回弹系数 31 | /// 32 | public double Restitution = 1; 33 | 34 | /// 35 | /// 质量 36 | /// 37 | protected double mass; 38 | 39 | /// 40 | /// 质量的倒数 41 | /// 42 | protected double inverseMass; 43 | 44 | /// 45 | /// 物体绑定的形状 46 | /// 47 | protected Shape shape = new Point(); 48 | 49 | /// 50 | /// 物体所受的力的合力 51 | /// 52 | protected Vector2D forceAccum; 53 | 54 | /// 55 | /// 为物体施加力 56 | /// 57 | /// 58 | public void AddForce(Vector2D force) 59 | { 60 | this.forceAccum += force; 61 | } 62 | 63 | /// 64 | /// 质量 65 | /// 66 | public double Mass 67 | { 68 | set 69 | { 70 | if (value != 0) 71 | { 72 | this.mass = value; 73 | this.inverseMass = 1.0 / value; 74 | } 75 | else 76 | throw new ArgumentOutOfRangeException("Particle's mass cannot be zero."); 77 | } 78 | get { return this.mass; } 79 | } 80 | 81 | /// 82 | /// 质量的倒数 83 | /// 84 | public double InverseMass 85 | { 86 | set 87 | { 88 | this.mass = value == 0 ? double.MaxValue : 1.0 / value; 89 | this.inverseMass = value; 90 | } 91 | get { return this.inverseMass; } 92 | } 93 | 94 | public Shape Shape { get { return this.shape; } } 95 | 96 | /// 97 | /// 为物体绑定一个形状 98 | /// 99 | /// 100 | public void BindShape(Shape shape) 101 | { 102 | shape.Body = this; 103 | this.shape = shape; 104 | } 105 | 106 | /// 107 | /// 更新物体 108 | /// 109 | /// 110 | public abstract void Update(double duration); 111 | } 112 | } -------------------------------------------------------------------------------- /Physics2D/Object/Tools/Handle.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object.Tools 2 | { 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | using Physics2D.Common; 6 | 7 | public class Handle : INotifyPropertyChanged 8 | { 9 | /// 10 | /// The position. 11 | /// 12 | private Vector2D position; 13 | 14 | /// 15 | /// The position. 16 | /// 17 | public Vector2D Position 18 | { 19 | get { return this.position; } 20 | set { this.SetProperty(ref this.position, value); } 21 | } 22 | 23 | public Handle(Vector2D position) 24 | { 25 | this.position = position; 26 | } 27 | 28 | /// 29 | /// Set property's value. 30 | /// Trigger property changed event when value changed. 31 | /// 32 | /// The type. 33 | /// Current value. 34 | /// Aim value. 35 | /// The property's name. 36 | /// True if property value updated. 37 | protected bool SetProperty(ref T storge, T value, [CallerMemberName]string propertyName = null) 38 | { 39 | if (object.Equals(storge, value)) 40 | { 41 | return false; 42 | } 43 | 44 | storge = value; 45 | this.OnPropertyChanged(propertyName); 46 | return true; 47 | } 48 | 49 | /// 50 | /// The event of property changed. 51 | /// 52 | /// The property's name. 53 | protected void OnPropertyChanged([CallerMemberName]string propertyName = null) 54 | { 55 | this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 56 | } 57 | 58 | /// 59 | /// Release all events. 60 | /// 61 | public void Release() 62 | { 63 | var delegates = this.PropertyChanged.GetInvocationList(); 64 | foreach (var d in delegates) 65 | { 66 | this.PropertyChanged -= d as PropertyChangedEventHandler; 67 | } 68 | } 69 | 70 | /// 71 | /// The property changed event. 72 | /// Registed delegation will fired on property changed. 73 | /// 74 | public event PropertyChangedEventHandler PropertyChanged; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Physics2D/Object/Tools/IPin.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D.Object.Tools 2 | { 3 | using Physics2D.Common; 4 | using Physics2D.Core; 5 | 6 | public interface IPin 7 | { 8 | /// 9 | /// Pin self to world. 10 | /// 11 | /// The . 12 | /// The 13 | /// The pined point. 14 | Handle Pin(World world, Vector2D position); 15 | 16 | /// 17 | /// Unpin self from world. 18 | /// 19 | /// The . 20 | void Unpin(World world); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Physics2D/Physics2D.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Physics2D/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace Physics2D 2 | { 3 | public class Settings 4 | { 5 | /// 6 | /// Gets or sets the percision of double type. 7 | /// 8 | public static double Percision { get; set; } = 1e-8; 9 | 10 | /// 11 | /// Gets or sets the max contact number. 12 | /// 13 | public static int MaxContacts { get; set; } = 500; 14 | 15 | /// 16 | /// Gets or sets the iteration nuumber of contact resolver. 17 | /// 18 | public static int ContactIteration { get; set; } = 1; 19 | } 20 | } -------------------------------------------------------------------------------- /Physics2D/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "PlaceholderCompany" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Physics2D 2 | ========= 3 | 4 | 一个使用C#编写的2D质体物理引擎,项目包含了 5 | 6 | - 质体物理引擎 7 | - 单元测试 8 | - 基于WPF绘制的简单Demo合集 9 | 10 | 该项目是一个实验项目(Toy Project),用于实践我所学习到的关于框架设计、语言特性等知识,因此该项目不具备工业项目的稳定性及性能(说到性能,其实用C#做这样的项目似乎就不算太合适),但是制作一些基于WPF的小型游戏应该还是可以做到的。为了简化系统自身,并且基于项目的实现目的,该引擎并暂时没有考虑实现刚体物理模型,但是各个模块的设计为实现刚体物理模型提供了可能性和便利性,也就是说你可以很容易的将刚体物理模型加入到本引擎当中。 11 | 12 | 该项目力求追随工程项目的最佳实践,希望能够从设计、重构、文档、测试方面都能够较为完备。目前大部分代码都包含详实(但不啰嗦)的注释,大部分方法和属性都标注了文档注释,可以使用Doxygen等第三方软件生成可读的API文档。物理引擎类库部分的所有代码都被被单元测试覆盖,但遗憾的是,单元测试的代码覆盖率并不能证明程序执行的正确性,所以测试用例还需在后续的开发过程中不断地补充完善。 13 | 14 | ## 功能(包含计划中的) 15 | 16 | - [x] 质体(粒子) 17 | - [x] 可以定制的质体作用力发生器 18 | - [x] 可以定制的区域作用力 19 | - [x] 质体碰撞处理 20 | - [x] 质体连杆、绳索 21 | - [ ] 摩擦力 22 | - [ ] 液体模拟 23 | - [ ] 沙盒模拟器 24 | 25 | ## 开始使用 26 | 27 | 基本使用 28 | ```csharp 29 | // 创建一个物理世界 30 | var world = new World(); 31 | 32 | // 在物理世界里创建一个质体(粒子) 33 | // 位置 (0, 0) 初速度 (1, 0) 质量 1 kg 碰撞恢复系数0.9 34 | world.CreateParticle(Vector2D.Zero, new Vector2D(1, 0), 1, 0.9); 35 | 36 | // 创建一个全局有效的重力场 37 | world.CreateGravity(9.8); 38 | 39 | // 执行 1/60 s 40 | world.Update(1/60.0); 41 | ``` 42 | 43 | 添加碰撞 44 | ```csharp 45 | // 创建一条长5m深4m的底边 46 | var edge = world.CreateEdge(0, 4, 5, 4); 47 | 48 | // 将质点视为可与底边接触的球(圆),半径为0.2m 49 | particle.BindSharp(new Circle(0.2)); 50 | 51 | // 执行 1/60 s 52 | world.Update(1/60.0); 53 | ``` 54 | 55 | ## 分支说明 56 | 57 | 目前该项目的Demo图形依赖于WriteableBitmapEx(通过nuget引入到项目),限于GDI+的性能问题,也希望能够尝试使用SharpDX或Win2D这样的由DirectX驱动的图形库,为此我创建了两个分支: 58 | 59 | - SharpDXonWPF 60 | 在WPFDemo项目中增加了基于SharpDX中Direct2D的Image扩展,使用Direct2D作为渲染驱动。该尝试仍处于试验状态。 61 | 62 | - UWPDemo 63 | 由于Win2D仅支持UWP等沙盒内应用,所以增加了UWPDemo项目,由于将Physics2D项目更改为了PCL类型,所以Physics2D可以同时支持WPF以及UWP应用。还未正式启动该项尝试。 64 | 65 | 如果你喜欢相关的技术,欢迎为相关的分支贡献代码。 66 | 67 | ## 演示动画 68 | 69 | 由于图片比较大,加载完毕可能需要一定时间 70 | 71 | - 烟花 72 | 73 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/firework.gif) 74 | 75 | - 圆周运动 76 | 77 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/cycle.gif) 78 | 79 | - 弹性材料 80 | 81 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/spring.gif) 82 | 83 | - 变形球 84 | 85 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/metaball.gif) 86 | 87 | - 牛顿摆 88 | 89 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/newton_scradle.gif) 90 | 91 | - 多边形连杆(模拟刚体) 92 | 93 | ![](https://github.com/Blueve/Physics2D/blob/master/Images/poly_rod.gif) 94 | -------------------------------------------------------------------------------- /UnitTest/Collision/Basic/ParticleRodTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Basic 2 | { 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Physics2D.Collision; 7 | using Physics2D.Collision.Basic; 8 | using Physics2D.Common; 9 | using Physics2D.Object; 10 | 11 | [TestClass] 12 | public class ParticleRodTest 13 | { 14 | [TestMethod] 15 | public void TestConstructor() 16 | { 17 | var pA = new Particle { Mass = 1 }; 18 | var pB = new Particle { Mass = 1, Position = new Vector2D(5, 0) }; 19 | var rod = new ParticleRod(pA, pB); 20 | 21 | Assert.AreEqual(pA, rod.ParticleA); 22 | Assert.AreEqual(pB, rod.ParticleB); 23 | } 24 | 25 | [TestMethod] 26 | public void TestGetEnumerator() 27 | { 28 | var pA = new Particle { Mass = 1 }; 29 | var pB = new Particle { Mass = 1, Position = new Vector2D(5, 0) }; 30 | var rod = new ParticleRod(pA, pB); 31 | 32 | var contacts = new List(); 33 | contacts.AddRange(rod); 34 | Assert.AreEqual(0, contacts.Count, "长度未变化时不产生任何碰撞"); 35 | 36 | pB.Position = new Vector2D(6, 0); 37 | foreach (var contact in rod) 38 | { 39 | Assert.AreEqual(1, contact.Penetration); 40 | Assert.AreEqual(new Vector2D(1, 0), contact.ContactNormal); 41 | } 42 | 43 | pB.Position = new Vector2D(4, 0); 44 | foreach (var contact in rod) 45 | { 46 | Assert.AreEqual(1, contact.Penetration); 47 | Assert.AreEqual(new Vector2D(-1, 0), contact.ContactNormal); 48 | } 49 | 50 | pB.Position = new Vector2D(4, 0); 51 | IEnumerable iEnum = rod; 52 | foreach (ParticleContact contact in iEnum) 53 | { 54 | Assert.AreEqual(1, contact.Penetration); 55 | Assert.AreEqual(new Vector2D(-1, 0), contact.ContactNormal); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /UnitTest/Collision/Basic/ParticleRopeTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Basic 2 | { 3 | using System.Collections.Generic; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Collision; 6 | using Physics2D.Collision.Basic; 7 | using Physics2D.Common; 8 | using Physics2D.Object; 9 | 10 | [TestClass] 11 | public class ParticleRopeTest 12 | { 13 | [TestMethod] 14 | public void TestConstructor() 15 | { 16 | var pA = new Particle { Mass = 1 }; 17 | var pB = new Particle { Mass = 1 }; 18 | var rope = new ParticleRope(10, 0.5, pA, pB); 19 | 20 | Assert.AreEqual(10, rope.MaxLength); 21 | Assert.AreEqual(pA, rope.ParticleA); 22 | Assert.AreEqual(pB, rope.ParticleB); 23 | Assert.AreEqual(0.5, rope.Restitution); 24 | } 25 | 26 | [TestMethod] 27 | public void TestGetEnumerator() 28 | { 29 | var pA = new Particle { Mass = 1 }; 30 | var pB = new Particle { Mass = 1 }; 31 | var rope = new ParticleRope(10, 0.5, pA, pB); 32 | 33 | var contacts = new List(); 34 | contacts.AddRange(rope); 35 | Assert.AreEqual(0, contacts.Count, "长度未变化时不产生任何碰撞"); 36 | 37 | pB.Position = new Vector2D(20, 0); 38 | foreach (var contact in rope) 39 | { 40 | Assert.AreEqual(10, contact.Penetration); 41 | Assert.AreEqual(new Vector2D(1, 0), contact.ContactNormal); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UnitTest/Collision/ParticleCollisionDetectorTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision 2 | { 3 | using System; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Collision; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Object; 9 | 10 | [TestClass] 11 | public class ParticleCollisionDetectorTest 12 | { 13 | [TestMethod] 14 | public void TestCircleAndCircleCollided() 15 | { 16 | Particle pA = new Particle { Position = new Vector2D(0, 0), Restitution = 1 }; 17 | Particle pB = new Particle { Position = new Vector2D(0, 3), Restitution = 0.5 }; 18 | 19 | pA.BindShape(new Circle(2)); 20 | pB.BindShape(new Circle(2)); 21 | 22 | TestDetectorsResult( 23 | new ParticleContact(pA, pB, 0.75, 1, new Vector2D(0, -1)), 24 | ParticleCollisionDetector.CircleAndCircle(pA.Shape as Circle, pB.Shape as Circle)); 25 | } 26 | 27 | [TestMethod] 28 | public void TestCircleAndCircleNotCollided() 29 | { 30 | Particle pA = new Particle { Position = new Vector2D(0, 0) }; 31 | Particle pB = new Particle { Position = new Vector2D(0, 3) }; 32 | 33 | pA.BindShape(new Circle(1)); 34 | pB.BindShape(new Circle(1)); 35 | 36 | Assert.IsNull(ParticleCollisionDetector.CircleAndCircle(pA.Shape as Circle, pB.Shape as Circle)); 37 | } 38 | 39 | [TestMethod] 40 | public void TestCircleAndEdgeCollided() 41 | { 42 | Particle pA = new Particle { Position = new Vector2D(0, 2), Restitution = 1 }; 43 | Edge edge = new Edge(0, 0, 5, 0); 44 | pA.BindShape(new Circle(5)); 45 | 46 | TestDetectorsResult( 47 | new ParticleContact(pA, null, 1, 3, new Vector2D(0, 1)), 48 | ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿上"); 49 | 50 | pA.Position = new Vector2D(-1, 2); 51 | TestDetectorsResult( 52 | new ParticleContact(pA, null, 1, 5 - Math.Sqrt(5), new Vector2D(-1, 2).Normalize()), 53 | ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿左延长线上"); 54 | 55 | pA.Position = new Vector2D(6, 2); 56 | TestDetectorsResult( 57 | new ParticleContact(pA, null, 1, 5 - Math.Sqrt(5), new Vector2D(1, 2).Normalize()), 58 | ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿右延长线上"); 59 | 60 | pA.Position = new Vector2D(-1, 0); 61 | TestDetectorsResult( 62 | new ParticleContact(pA, null, 1, 4, new Vector2D(-1, 0)), 63 | ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心在边沿的延长线上"); 64 | 65 | pA.PrePosition = new Vector2D(2.5, 10); 66 | pA.Position = new Vector2D(2.5, -10); 67 | TestDetectorsResult( 68 | new ParticleContact(pA, null, 1, 5, new Vector2D(0, 1)), 69 | ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "发生了穿越"); 70 | } 71 | 72 | [TestMethod] 73 | public void TestCircleAndEdgeNotCollided() 74 | { 75 | Particle pA = new Particle { Position = new Vector2D(-1, 6), Restitution = 1 }; 76 | Edge edge = new Edge(0, 0, 5, 0); 77 | pA.BindShape(new Circle(5)); 78 | 79 | Assert.IsNull(ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿左延长线上"); 80 | 81 | pA.Position = new Vector2D(6, 6); 82 | Assert.IsNull(ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿右延长线上"); 83 | 84 | pA.Position = new Vector2D(2.5, 6); 85 | Assert.IsNull(ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心投影在边沿上"); 86 | 87 | pA.Position = new Vector2D(-6, 0); 88 | Assert.IsNull(ParticleCollisionDetector.CircleAndEdge(pA.Shape as Circle, edge), "圆心在边沿的延长线上"); 89 | } 90 | 91 | private static void TestDetectorsResult( 92 | ParticleContact expectContact, 93 | ParticleContact contact, 94 | string message = "") 95 | { 96 | Assert.IsNotNull(contact, $"{message} 产生了碰撞"); 97 | Assert.AreEqual(expectContact.PA, contact.PA, $"{message} 物体A"); 98 | Assert.AreEqual(expectContact.PB, contact.PB, $"{message} 物体B"); 99 | Assert.AreEqual(expectContact.ContactNormal, contact.ContactNormal, $"{message} 碰撞法线"); 100 | Assert.AreEqual(expectContact.Restitution, contact.Restitution, $"{message} 回弹系数"); 101 | Assert.AreEqual(expectContact.Penetration, contact.Penetration, $"{message} 相交深度"); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /UnitTest/Collision/ParticleContactResolverTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision 2 | { 3 | using System.Collections.Generic; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Collision; 6 | using Physics2D.Common; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class ParticleContactResolverTest 11 | { 12 | [TestMethod] 13 | public void TestConstructor() 14 | { 15 | var resolver = new ParticleContactResolver(100); 16 | 17 | Assert.AreEqual(100, resolver.Iterations); 18 | } 19 | 20 | [TestMethod] 21 | public void TestGetterAndSetterOfIterations() 22 | { 23 | var resolver = new ParticleContactResolver(100); 24 | resolver.Iterations = 1000; 25 | 26 | Assert.AreEqual(1000, resolver.Iterations); 27 | } 28 | 29 | [TestMethod] 30 | public void TestResolveContacts() 31 | { 32 | var resolver = new ParticleContactResolver(100); 33 | 34 | var p = new List 35 | { 36 | new Particle 37 | { 38 | Position = new Vector2D(0, 0), 39 | Velocity = new Vector2D(1, 0), 40 | Mass = 1 41 | }, 42 | new Particle 43 | { 44 | Position = new Vector2D(2, 0), 45 | Velocity = new Vector2D(0, 0), 46 | Mass = 1 47 | } 48 | }; 49 | var contact = new ParticleContact(p[0], p[1], 1, 1, new Vector2D(-1, 0)); 50 | 51 | var contactList = new List(); 52 | resolver.ResolveContacts(contactList, 1 / 60.0); 53 | 54 | contactList.Add(contact); 55 | resolver.ResolveContacts(contactList, 1 / 60.0); 56 | Assert.AreEqual(new Vector2D(-1, 0), p[0].Position, "物体0依据速度分量分离"); 57 | Assert.AreEqual(new Vector2D(0, 0), p[0].Velocity, "物体0碰撞后速度相反"); 58 | Assert.AreEqual(new Vector2D(1, 0), p[1].Velocity, "物体1碰撞后速度相反"); 59 | 60 | p = new List 61 | { 62 | new Particle 63 | { 64 | Position = new Vector2D(0, 0), 65 | Velocity = new Vector2D(1, 0), 66 | Mass = 1 67 | }, 68 | new Particle 69 | { 70 | Position = new Vector2D(2, 0), 71 | Velocity = new Vector2D(0, 0), 72 | Mass = 1 73 | } 74 | }; 75 | contactList = new List 76 | { 77 | new ParticleContact(p[1], p[0], 1, 1, new Vector2D(1, 0)) 78 | }; 79 | resolver.ResolveContacts(contactList, 1 / 60.0); 80 | Assert.AreEqual(new Vector2D(-1, 0), p[0].Position, "函数满足对称性"); 81 | Assert.AreEqual(new Vector2D(0, 0), p[0].Velocity, "函数满足对称性"); 82 | Assert.AreEqual(new Vector2D(1, 0), p[1].Velocity, "函数满足对称性"); 83 | 84 | } 85 | 86 | [TestMethod] 87 | public void TestResovleMultiContacts() 88 | { 89 | var resolver = new ParticleContactResolver(100); 90 | var p = new List 91 | { 92 | new Particle 93 | { 94 | Position = new Vector2D(0, 0), 95 | Velocity = new Vector2D(0, 0), 96 | Mass = 1 97 | }, 98 | new Particle 99 | { 100 | Position = new Vector2D(2, 0), 101 | Velocity = new Vector2D(0, 0), 102 | Mass = 1 103 | }, 104 | new Particle 105 | { 106 | Position = new Vector2D(4, 0), 107 | Velocity = new Vector2D(0, 0), 108 | Mass = 1 109 | } 110 | }; 111 | var contactList = new List 112 | { 113 | new ParticleContact(p[0], p[1], 1, 1, new Vector2D(-1, 0)), 114 | new ParticleContact(p[2], p[1], 1, 1, new Vector2D(1, 0)) 115 | }; 116 | resolver.ResolveContacts(contactList, 1 / 60.0); 117 | Assert.AreEqual(new Vector2D(-1, 0), p[0].Position, "物体0向左分离"); 118 | Assert.AreEqual(new Vector2D(2, 0), p[1].Position, "物体1位置不变"); 119 | Assert.AreEqual(new Vector2D(5, 0), p[2].Position, "物体2向右分离"); 120 | } 121 | 122 | [TestMethod] 123 | public void TestResolveContinuousContacts() 124 | { 125 | var resolver = new ParticleContactResolver(100); 126 | var p = new List 127 | { 128 | new Particle 129 | { 130 | Position = new Vector2D(0, 0), 131 | Velocity = new Vector2D(1, 0), 132 | Mass = 1 133 | }, 134 | new Particle 135 | { 136 | Position = new Vector2D(2, 0), 137 | Velocity = new Vector2D(0, 0), 138 | Mass = 1 139 | }, 140 | new Particle 141 | { 142 | Position = new Vector2D(4, 0), 143 | Velocity = new Vector2D(0, 0), 144 | Mass = 1 145 | } 146 | }; 147 | var contactList = new List 148 | { 149 | new ParticleContact(p[0], p[1], 1, 1, new Vector2D(-1, 0)), 150 | new ParticleContact(p[1], p[2], 1, 0, new Vector2D(-1, 0)) 151 | }; 152 | resolver.ResolveContacts(contactList, 1 / 60.0); 153 | Assert.AreEqual(new Vector2D(-1, 0), p[0].Position, "物体0向左分离"); 154 | Assert.AreEqual(new Vector2D(2, 0), p[1].Position, "物体1位置不变"); 155 | Assert.AreEqual(new Vector2D(4, 0), p[2].Position, "物体2位置不变"); 156 | 157 | Assert.AreEqual(new Vector2D(0, 0), p[0].Velocity, "物体0碰撞后无速度"); 158 | Assert.AreEqual(new Vector2D(0, 0), p[1].Velocity, "物体1碰撞后无速度"); 159 | Assert.AreEqual(new Vector2D(1, 0), p[2].Velocity, "物体2获得物体0的速度"); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /UnitTest/Collision/Shapes/CircleTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Shapes 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Collision.Shapes; 5 | 6 | [TestClass] 7 | public class CircleTest 8 | { 9 | [TestMethod] 10 | public void TestConstructor() 11 | { 12 | var circle = new Circle(10); 13 | Assert.AreEqual(10, circle.R); 14 | Assert.AreEqual(0, circle.Id); 15 | 16 | circle = new Circle(10, 1); 17 | Assert.AreEqual(1, circle.Id); 18 | } 19 | 20 | [TestMethod] 21 | public void TestType() 22 | { 23 | var circle = new Circle(10); 24 | Assert.AreEqual(ShapeType.Circle, circle.Type); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UnitTest/Collision/Shapes/EdgeTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Shapes 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Collision.Shapes; 5 | using Physics2D.Common; 6 | 7 | [TestClass] 8 | public class EdgeTest 9 | { 10 | [TestMethod] 11 | public void TestConstructor() 12 | { 13 | var edge = new Edge(new Vector2D(0, 0), new Vector2D(1, 1)); 14 | Assert.AreEqual(new Vector2D(0, 0), edge.PointA); 15 | Assert.AreEqual(new Vector2D(1, 1), edge.PointB); 16 | 17 | edge = new Edge(0, 0, 1, 1); 18 | Assert.AreEqual(new Vector2D(0, 0), edge.PointA); 19 | Assert.AreEqual(new Vector2D(1, 1), edge.PointB); 20 | Assert.AreEqual(0, edge.Id); 21 | 22 | } 23 | 24 | [TestMethod] 25 | public void TestType() 26 | { 27 | var edge = new Edge(new Vector2D(0, 0), new Vector2D(1, 1)); 28 | 29 | Assert.AreEqual(ShapeType.Edge, edge.Type); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /UnitTest/Collision/Shapes/PointTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Shapes 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Collision.Shapes; 5 | 6 | [TestClass] 7 | public class PointTest 8 | { 9 | [TestMethod] 10 | public void TestType() 11 | { 12 | var p = new Point(); 13 | 14 | Assert.AreEqual(ShapeType.Point, p.Type); 15 | Assert.AreEqual(0, p.Id); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UnitTest/Collision/Shapes/ShapeTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Collision.Shapes 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Collision.Shapes; 5 | 6 | [TestClass] 7 | public class ShapeTest 8 | { 9 | [TestMethod] 10 | public void TestNewId() 11 | { 12 | Assert.AreEqual(1, Shape.NewId()); 13 | Assert.AreEqual(2, Shape.NewId()); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UnitTest/Common/MathHelperTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Common 2 | { 3 | using System.Collections.Generic; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Common; 6 | 7 | [TestClass] 8 | public class MathHelperTest 9 | { 10 | [TestMethod] 11 | public void TestPointToLineVector() 12 | { 13 | Vector2D linePA = new Vector2D(0, 0); 14 | Vector2D linePB = new Vector2D(5, 0); 15 | Vector2D point = new Vector2D(2.5, 5); 16 | 17 | Assert.AreEqual(new Vector2D(0, -5), MathHelper.PointToLineVector(point, linePA, linePB)); 18 | } 19 | 20 | [TestMethod] 21 | public void TestLineIntersection() 22 | { 23 | // 有交点的情况 24 | Vector2D pA1 = new Vector2D(0, 3); 25 | Vector2D pA2 = new Vector2D(6, 3); 26 | 27 | Vector2D pB1 = new Vector2D(3, 0); 28 | Vector2D pB2 = new Vector2D(3, 6); 29 | 30 | var actual = MathHelper.LineIntersection(pA1, pA2, pB1, pB2); 31 | Assert.AreEqual(new Vector2D(3, 3), actual); 32 | 33 | // 无交点的情况 34 | Vector2D pC1 = new Vector2D(1, 4); 35 | Vector2D pC2 = new Vector2D(7, 4); 36 | Assert.IsNull(MathHelper.LineIntersection(pA1, pA2, pC1, pC2)); 37 | 38 | Vector2D pD1 = new Vector2D(12, 6); 39 | Vector2D pD2 = new Vector2D(12, 0); 40 | Assert.IsNull(MathHelper.LineIntersection(pA2, pA1, pD1, pD2)); 41 | } 42 | 43 | [TestMethod] 44 | public void TestIsInside() 45 | { 46 | // 测试正常情况 47 | var vertexs = new List 48 | { 49 | new Vector2D(0, 0), 50 | new Vector2D(100, 0), 51 | new Vector2D(0, 100) 52 | }; 53 | vertexs.Add(vertexs[0]); 54 | 55 | Assert.IsTrue(MathHelper.IsInside(vertexs, new Vector2D(25, 25))); 56 | Assert.IsFalse(MathHelper.IsInside(vertexs, new Vector2D(100, 100))); 57 | } 58 | 59 | [TestMethod] 60 | public void TestIsInsideFail() 61 | { 62 | // 测试不能围成多边形的情况 63 | var vertexs = new List 64 | { 65 | new Vector2D(0, 0), 66 | new Vector2D(100, 0), 67 | new Vector2D(0, 100) 68 | }; 69 | 70 | Assert.IsFalse(MathHelper.IsInside(vertexs, new Vector2D(25, 25))); 71 | } 72 | 73 | [TestMethod] 74 | public void TestPointToLineDistanceSquared() 75 | { 76 | Assert.AreEqual(25, MathHelper.PointToLineDistenceSquared( 77 | new Vector2D(0, 5), 78 | new Vector2D(-5, 0), new Vector2D(5, 0))); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /UnitTest/Common/Vector2DTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Common 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | 6 | [TestClass] 7 | public class Vector2DTest 8 | { 9 | [TestMethod] 10 | public void TestConstructor() 11 | { 12 | Vector2D vector = new Vector2D(); 13 | vector.X = vector.Y = 0; 14 | 15 | Assert.AreEqual(vector, new Vector2D(0, 0)); 16 | Assert.AreEqual(vector, new Vector2D(vector)); 17 | } 18 | 19 | [TestMethod] 20 | public void TestEquals() 21 | { 22 | Vector2D vector = new Vector2D(1, 2); 23 | 24 | Assert.IsTrue(vector.Equals(new Vector2D(1, 2))); 25 | Assert.IsFalse(vector.Equals(new Vector2D(2, 1))); 26 | Assert.IsFalse(vector.Equals("(1, 2)")); 27 | Assert.IsFalse(vector.Equals(null)); 28 | } 29 | 30 | [TestMethod] 31 | public void TestSet() 32 | { 33 | Vector2D vector = new Vector2D(0, 0); 34 | vector.Set(5, 2); 35 | 36 | Assert.AreEqual(new Vector2D(5, 2), vector); 37 | } 38 | 39 | [TestMethod] 40 | public void TestSpecificProperty() 41 | { 42 | Assert.AreEqual(new Vector2D(1, 1), Vector2D.One); 43 | Assert.AreEqual(new Vector2D(1, 0), Vector2D.UnitX); 44 | Assert.AreEqual(new Vector2D(0, 1), Vector2D.UnitY); 45 | Assert.AreEqual(new Vector2D(0, 0), Vector2D.Zero); 46 | } 47 | 48 | [TestMethod] 49 | public void TestToString() 50 | { 51 | Vector2D vector = new Vector2D(1, 2); 52 | 53 | Assert.AreEqual("(1.23, 4.56)", new Vector2D(1.23, 4.56).ToString()); 54 | } 55 | 56 | [TestMethod] 57 | public void TestGetHashCode() 58 | { 59 | Vector2D vector = new Vector2D(1, 2); 60 | 61 | Assert.IsNotNull(vector.GetHashCode()); 62 | } 63 | 64 | [TestMethod] 65 | public void TestAddition() 66 | { 67 | Vector2D left = new Vector2D(3, 4); 68 | Vector2D right = new Vector2D(2, 2); 69 | 70 | Assert.AreEqual(new Vector2D(5, 6), left + right); 71 | } 72 | 73 | [TestMethod] 74 | public void TestSubtraction() 75 | { 76 | Vector2D left = new Vector2D(3, 4); 77 | Vector2D right = new Vector2D(2, 2); 78 | 79 | Assert.AreEqual(new Vector2D(1, 2), left - right); 80 | Assert.AreEqual(new Vector2D(-3, -4), -left); 81 | } 82 | 83 | [TestMethod] 84 | public void TestMultiply() 85 | { 86 | Vector2D left = new Vector2D(3, 4); 87 | Vector2D right = new Vector2D(2, 2); 88 | 89 | Assert.AreEqual(3 * 2 + 4 * 2, left * right); 90 | Assert.AreEqual(new Vector2D(6, 8), left * 2); 91 | Assert.AreEqual(new Vector2D(-10, -10), -5 * right); 92 | } 93 | 94 | [TestMethod] 95 | public void TestDivision() 96 | { 97 | Vector2D vector = new Vector2D(3, 4); 98 | 99 | Assert.AreEqual(new Vector2D(1.5, 2), vector / 2); 100 | } 101 | 102 | [TestMethod] 103 | public void TestUnaryNegation() 104 | { 105 | Vector2D vector = new Vector2D(3, 4); 106 | 107 | Assert.AreEqual(new Vector2D(-3, -4), -vector); 108 | } 109 | 110 | [TestMethod] 111 | public void TestEquality() 112 | { 113 | Vector2D left = new Vector2D(3, 4); 114 | Vector2D right = new Vector2D(2, 2); 115 | 116 | Assert.IsFalse(left == right); 117 | Assert.IsTrue(left == new Vector2D(3, 4)); 118 | } 119 | 120 | [TestMethod] 121 | public void TestInequality() 122 | { 123 | Vector2D left = new Vector2D(3, 4); 124 | Vector2D right = new Vector2D(2, 2); 125 | 126 | Assert.IsTrue(left != right); 127 | Assert.IsFalse(left != new Vector2D(3, 4)); 128 | } 129 | 130 | [TestMethod] 131 | public void TestNormalize() 132 | { 133 | Vector2D vect1 = new Vector2D(3, 0); 134 | Vector2D vect2 = new Vector2D(0, 4); 135 | 136 | Assert.AreEqual(Vector2D.UnitX, Vector2D.Normalize(vect1)); 137 | Assert.AreEqual(Vector2D.UnitY, vect2.Normalize()); 138 | Assert.AreEqual(Vector2D.Zero, Vector2D.Zero.Normalize()); 139 | } 140 | 141 | [TestMethod] 142 | public void TestLength() 143 | { 144 | Vector2D vect1 = new Vector2D(3, 0); 145 | Vector2D vect2 = new Vector2D(0, 4); 146 | 147 | Assert.AreEqual(3f, vect1.Length()); 148 | Assert.AreEqual(16f, vect2.LengthSquared()); 149 | } 150 | 151 | [TestMethod] 152 | public void TestDistance() 153 | { 154 | Vector2D vect1 = new Vector2D(3, 0); 155 | Vector2D vect2 = new Vector2D(0, 4); 156 | 157 | Assert.AreEqual(25.0, Vector2D.DistanceSquared(vect1, vect2)); 158 | Assert.AreEqual(5.0, Vector2D.Distance(vect1, vect2)); 159 | Assert.AreEqual(0, Vector2D.Distance(vect1, vect1)); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /UnitTest/ConvertUnitsTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D; 5 | using Physics2D.Common; 6 | 7 | [TestClass] 8 | public class ConvertUnitsTest 9 | { 10 | [TestMethod] 11 | public void TestToSimUnits() 12 | { 13 | Assert.AreEqual(2, 100.ToSimUnits()); 14 | Assert.AreEqual(2, 100.0.ToSimUnits()); 15 | Assert.AreEqual(new Vector2D(2, 1), new Vector2D(100, 50).ToSimUnits()); 16 | } 17 | 18 | [TestMethod] 19 | public void TestToDisplayUnits() 20 | { 21 | Assert.AreEqual(100, 2.ToDisplayUnits()); 22 | Assert.AreEqual(100, 2.0.ToDisplayUnits()); 23 | Assert.AreEqual(new Vector2D(100, 50), new Vector2D(2, 1).ToDisplayUnits()); 24 | } 25 | 26 | [TestMethod] 27 | public void TestSetDisplayUnitToSimUnitRatio() 28 | { 29 | ConvertUnits.SetDisplayUnitToSimUnitRatio(50); 30 | 31 | this.TestToSimUnits(); 32 | this.TestToDisplayUnits(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /UnitTest/Core/ForceRegistryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Core 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Core; 6 | using Physics2D.Force; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class ForceRegistryTest 11 | { 12 | [TestMethod] 13 | public void TestUpdate() 14 | { 15 | var forceRegistry = new ForceRegistry(); 16 | var force = new ParticleConstantForce(new Vector2D(5, 0)); 17 | var p = new Particle { Mass = 1 }; 18 | force.Add(p); 19 | 20 | this.TestAddForceGenerator(forceRegistry, force); 21 | forceRegistry.Update(1 / 60.0); 22 | p.Update(1 / 60.0); 23 | Assert.AreEqual(new Vector2D(5, 0), p.Acceleration, "物体被赋予正确的加速度"); 24 | 25 | p.Acceleration = Vector2D.Zero; 26 | this.TestRemoveForceGenerator(forceRegistry, force); 27 | forceRegistry.Update(1 / 60.0); 28 | p.Update(1 / 60.0); 29 | Assert.AreEqual(new Vector2D(0, 0), p.Acceleration, "删去作用力发生器,物体不再受该作用力发生器所产生的力"); 30 | 31 | force.Add(p); 32 | this.TestAddForceGenerator(forceRegistry, force); 33 | this.TestRemoveParticle(forceRegistry, p); 34 | forceRegistry.Update(1 / 60.0); 35 | p.Update(1 / 60.0); 36 | Assert.AreEqual(new Vector2D(0, 0), p.Acceleration, "删去物体,物体不再受该作用力发生器所产生的力"); 37 | } 38 | 39 | private void TestAddForceGenerator( 40 | ForceRegistry forceRegistry, 41 | ParticleForceGenerator force) 42 | { 43 | forceRegistry.Add(force); 44 | } 45 | 46 | private void TestRemoveForceGenerator( 47 | ForceRegistry forceRegistry, 48 | ParticleForceGenerator force) 49 | { 50 | forceRegistry.Remove(force); 51 | } 52 | 53 | private void TestRemoveParticle( 54 | ForceRegistry forceRegistry, 55 | Particle p) 56 | { 57 | forceRegistry.Remove(p); 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /UnitTest/Core/WorldTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Core 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Core; 9 | using Physics2D.Force; 10 | using Physics2D.Force.Zones; 11 | using Physics2D.Object; 12 | using Physics2D.Object.Tools; 13 | 14 | [TestClass] 15 | public class WorldTest 16 | { 17 | [TestMethod] 18 | public void TestUpdate() 19 | { 20 | var world = new World(); 21 | 22 | // 两种物体 23 | var objA = new CombinedParticle( 24 | new List { 25 | new Vector2D(0, 0), 26 | new Vector2D(5, 0), 27 | new Vector2D(0, 5) 28 | }); 29 | var objB = new Particle { Mass = 1, Position = Vector2D.Zero }; 30 | 31 | // 一个全局作用力 32 | var zone = new GlobalZone(); 33 | zone.Add(new ParticleGravity(new Vector2D(0, 9.8))); 34 | world.Zones.Add(zone); 35 | 36 | this.TestAddCustomObject(world, objA); 37 | var handle = this.TestPin(world, objA); 38 | this.TestUnPin(world, objA, handle); 39 | this.TestRemoveCustomObject(world, objA); 40 | 41 | // 一个作用力 42 | var force = new ParticleConstantForce(new Vector2D(5, 0)); 43 | force.Add(objB); 44 | world.ForceGenerators.Add(force); 45 | 46 | this.TestAddObject(world, objB); 47 | this.TestRemoveObject(world, objB); 48 | } 49 | 50 | [TestMethod] 51 | public void TestAddAndRemoveEdge() 52 | { 53 | var world = new World(); 54 | var obj = new Particle { Mass = 1, Position = new Vector2D(0, 5), Velocity = new Vector2D(0, 5) }; 55 | obj.BindShape(new Circle(2)); 56 | var edgeA = new Edge(-5, 0, 5, 0); 57 | var edgeB = new Edge(-5, 10, 5, 10); 58 | 59 | world.AddObject(obj); 60 | world.AddEdge(edgeA); 61 | world.AddEdge(edgeB); 62 | 63 | world.Update(1); 64 | Assert.AreEqual(new Vector2D(0, -5), obj.Velocity, "质体会被反弹"); 65 | 66 | world.Update(1); 67 | world.Update(1); 68 | Assert.AreEqual(new Vector2D(0, 5), obj.Velocity, "质体会被再次反弹"); 69 | 70 | world.RemoveEdge(edgeB); 71 | world.Update(1); 72 | world.Update(1); 73 | Assert.AreEqual(new Vector2D(0, 5), obj.Velocity, "移除边缘后质体不会被反弹"); 74 | } 75 | 76 | private void TestAddCustomObject(World world, CombinedParticle obj) 77 | { 78 | world.AddObject(obj); 79 | world.Update(1); 80 | 81 | var vertexs = obj.Vertexs; 82 | for (var i = 0; i < vertexs.Count; i++) 83 | { 84 | Assert.AreEqual(new Vector2D(0, 9.8), vertexs[i].Acceleration, $"作用力应被正确地施加到了物体{i}上"); 85 | } 86 | } 87 | 88 | private Handle TestPin(World world, CombinedParticle obj) 89 | { 90 | obj.Position = Vector2D.Zero; 91 | var handle = world.Pin(obj, Vector2D.Zero); 92 | 93 | try 94 | { 95 | world.Pin(obj, Vector2D.UnitX); 96 | } 97 | catch (Exception e) 98 | { 99 | Assert.IsInstanceOfType(e, typeof(InvalidOperationException), "不允许对同一物体Pin多次"); 100 | } 101 | 102 | handle.Position = new Vector2D(100, 100); 103 | Assert.AreEqual(new Vector2D(100, 100), obj.Position, "Handle能对其生效"); 104 | 105 | return handle; 106 | } 107 | 108 | private void TestUnPin(World world, CombinedParticle obj, Handle handle) 109 | { 110 | obj.Position = Vector2D.Zero; 111 | world.UnPin(obj); 112 | handle.Position = new Vector2D(100, 100); 113 | Assert.AreEqual(new Vector2D(0, 0), obj.Position, "Handle不能对其生效"); 114 | 115 | try 116 | { 117 | world.UnPin(obj); 118 | } 119 | catch (Exception e) 120 | { 121 | Assert.IsInstanceOfType(e, typeof(InvalidOperationException), "不允许对未Pin的物体执行UnPin"); 122 | } 123 | } 124 | 125 | private void TestRemoveCustomObject(World world, CombinedParticle obj) 126 | { 127 | var vertexs = obj.Vertexs; 128 | foreach (var vertex in vertexs) 129 | { 130 | vertex.Acceleration = Vector2D.Zero; 131 | } 132 | 133 | world.RemoveObject(obj); 134 | world.Update(1); 135 | 136 | for (var i = 0; i < vertexs.Count; i++) 137 | { 138 | Assert.AreEqual(new Vector2D(0, 0), vertexs[i].Acceleration, $"物体{i}上"); 139 | } 140 | } 141 | 142 | private void TestAddObject(World world, PhysicsObject obj) 143 | { 144 | world.AddObject(obj); 145 | world.Update(1); 146 | 147 | Assert.AreEqual(new Vector2D(5, 9.8), obj.Acceleration, "作用力应被正确地施加到了物体上"); 148 | } 149 | 150 | private void TestRemoveObject(World world, PhysicsObject obj) 151 | { 152 | obj.Acceleration = Vector2D.Zero; 153 | world.RemoveObject(obj); 154 | world.Update(1); 155 | 156 | Assert.AreEqual(new Vector2D(0, 0), obj.Acceleration, "作用力不再作用于物体上"); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /UnitTest/Factories/ContactFactoryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Factories 2 | { 3 | using System; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Collision.Basic; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Common.Exceptions; 9 | using Physics2D.Core; 10 | using Physics2D.Factories; 11 | using Physics2D.Object; 12 | 13 | [TestClass] 14 | public class ContactFactoryTest 15 | { 16 | [TestMethod] 17 | public void TestCreateEdge() 18 | { 19 | var world = new World(); 20 | 21 | var edge = world.CreateEdge(new Vector2D(0, 0), new Vector2D(0, 5)); 22 | this.TestEdgeProperty(edge, new Vector2D(0, 0), new Vector2D(0, 5)); 23 | 24 | edge = world.CreateEdge(0, 0, 0, 5); 25 | this.TestEdgeProperty(edge, new Vector2D(0, 0), new Vector2D(0, 5)); 26 | } 27 | 28 | private void TestEdgeProperty(Edge edge, Vector2D pointA, Vector2D pointB) 29 | { 30 | Assert.AreEqual(pointA, edge.PointA); 31 | Assert.AreEqual(pointB, edge.PointB); 32 | Assert.AreEqual(0, edge.Id); 33 | Assert.IsNull(edge.Body); 34 | } 35 | 36 | [TestMethod] 37 | public void TestCreatePolygonEdge() 38 | { 39 | var world = new World(); 40 | 41 | var poly = world.CreatePolygonEdge(new Vector2D(0, 0), new Vector2D(0, 5), new Vector2D(5, 0)); 42 | 43 | var it = poly.GetEnumerator(); 44 | it.MoveNext(); 45 | this.TestEdgeProperty(it.Current, new Vector2D(0, 0), new Vector2D(0, 5)); 46 | 47 | it.MoveNext(); 48 | this.TestEdgeProperty(it.Current, new Vector2D(0, 5), new Vector2D(5, 0)); 49 | 50 | it.MoveNext(); 51 | this.TestEdgeProperty(it.Current, new Vector2D(5, 0), new Vector2D(0, 0)); 52 | 53 | try 54 | { 55 | poly = world.CreatePolygonEdge(new Vector2D(0, 0), new Vector2D(0, 5)); 56 | } 57 | catch (Exception e) 58 | { 59 | Assert.IsInstanceOfType(e, typeof(InvalidArgumentException), "点集不能构成多边形的时候抛出异常"); 60 | } 61 | } 62 | 63 | [TestMethod] 64 | [ExpectedException(typeof(InvalidArgumentException))] 65 | public void TestCreatePolygonEdgeFail() 66 | { 67 | var world = new World(); 68 | 69 | var poly = world.CreatePolygonEdge(new Vector2D(0, 0), new Vector2D(0, 5)); 70 | } 71 | 72 | [TestMethod] 73 | public void TestCreateRod() 74 | { 75 | var world = new World(); 76 | 77 | var rod = world.CreateRod( 78 | new Particle { Position = new Vector2D(0, 0) }, 79 | new Particle { Position = new Vector2D(0, 5) }); 80 | 81 | this.TestLinkProperty(rod, new Vector2D(0, 0), new Vector2D(0, 5)); 82 | Assert.AreEqual(5, rod.Length); 83 | 84 | } 85 | 86 | private void TestLinkProperty(ParticleLink link, Vector2D pointA, Vector2D pointB) 87 | { 88 | Assert.AreEqual(pointA, link.ParticleA.Position); 89 | Assert.AreEqual(pointB, link.ParticleB.Position); 90 | } 91 | 92 | [TestMethod] 93 | public void TestCreateRope() 94 | { 95 | var world = new World(); 96 | 97 | var rope = world.CreateRope( 98 | 10, 99 | 1, 100 | new Particle { Position = new Vector2D(0, 0) }, 101 | new Particle { Position = new Vector2D(0, 5) }); 102 | 103 | this.TestLinkProperty(rope, new Vector2D(0, 0), new Vector2D(0, 5)); 104 | Assert.AreEqual(10, rope.MaxLength); 105 | Assert.AreEqual(1, rope.Restitution); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /UnitTest/Factories/ParticleFactoryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Factories 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Core; 6 | using Physics2D.Factories; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class ParticleFactoryTest 11 | { 12 | [TestMethod] 13 | public void TestCreateParticle() 14 | { 15 | var world = new World(); 16 | 17 | var p = world.CreateParticle(new Vector2D(0, 1), new Vector2D(1, 0), 1, 1); 18 | this.TestParticleProperty(p, new Vector2D(0, 1), new Vector2D(1, 0), 1, 1); 19 | } 20 | 21 | [TestMethod] 22 | public void TestCreateFixedParticle() 23 | { 24 | var world = new World(); 25 | 26 | var p = world.CreateFixedParticle(new Vector2D(0, 1)); 27 | this.TestParticleProperty(p, new Vector2D(0, 1), Vector2D.Zero, double.MaxValue, 1); 28 | } 29 | 30 | [TestMethod] 31 | public void TestCreateUnstoppableParticle() 32 | { 33 | var world = new World(); 34 | 35 | var p = world.CreateUnstoppableParticle(new Vector2D(0, 1), new Vector2D(1, 0)); 36 | this.TestParticleProperty(p, new Vector2D(0, 1), new Vector2D(1, 0), double.MaxValue, 1); 37 | } 38 | 39 | private void TestParticleProperty( 40 | Particle particle, 41 | Vector2D p, 42 | Vector2D v, 43 | double m, 44 | double restitution) 45 | { 46 | Assert.AreEqual(p, particle.Position, "位置"); 47 | Assert.AreEqual(v, particle.Velocity, "速度"); 48 | Assert.AreEqual(m, particle.Mass, "质量"); 49 | Assert.AreEqual(restitution, particle.Restitution, "回弹系数"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /UnitTest/Factories/ZoneFactoryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Factories 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Core; 6 | using Physics2D.Factories; 7 | using Physics2D.Force; 8 | 9 | [TestClass] 10 | public class ZoneFactoryTest 11 | { 12 | [TestMethod] 13 | public void TestCreateGlobalZone() 14 | { 15 | var world = new World(); 16 | var zone = world.CreateGlobalZone(new ParticleConstantForce(new Vector2D(0, 5))); 17 | Assert.IsNotNull(zone); 18 | } 19 | 20 | [TestMethod] 21 | public void TestCreateRectangleZone() 22 | { 23 | var world = new World(); 24 | var zone = world.CreateRectangleZone( 25 | new ParticleConstantForce(new Vector2D(0, 5)), 26 | 1, 2, 3, 4); 27 | Assert.IsNotNull(zone); 28 | Assert.AreEqual(zone.X1, 1); 29 | Assert.AreEqual(zone.Y1, 2); 30 | Assert.AreEqual(zone.X2, 3); 31 | Assert.AreEqual(zone.Y2, 4); 32 | } 33 | 34 | [TestMethod] 35 | public void TestCreateGravity() 36 | { 37 | var world = new World(); 38 | var zone = world.CreateGravity(9.8); 39 | Assert.IsNotNull(zone); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UnitTest/Force/ParticleConstantForceTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Object; 7 | 8 | [TestClass] 9 | public class ParticleConstantForceTest 10 | { 11 | [TestMethod] 12 | public void TestConstructor() 13 | { 14 | var force = new ParticleConstantForce(new Vector2D(5, 0)); 15 | Assert.IsNotNull(force); 16 | } 17 | 18 | [TestMethod] 19 | public void TestApplyTo() 20 | { 21 | var force = new ParticleConstantForce(new Vector2D(5, 0)); 22 | var particle = new Particle 23 | { 24 | Mass = 1 25 | }; 26 | force.Add(particle); 27 | force.Apply(1); 28 | 29 | particle.Update(1); 30 | 31 | Assert.AreEqual(new Vector2D(5, 0), particle.Acceleration); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /UnitTest/Force/ParticleDragTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Object; 7 | 8 | [TestClass] 9 | public class ParticleDragTest 10 | { 11 | [TestMethod] 12 | public void TestConstructor() 13 | { 14 | var force = new ParticleDrag(0.1, 0.1); 15 | Assert.IsNotNull(force); 16 | } 17 | 18 | [TestMethod] 19 | public void TestApplyTo() 20 | { 21 | var force = new ParticleDrag(0.1, 0.1); 22 | var particle = new Particle 23 | { 24 | Mass = 1 25 | }; 26 | force.Add(particle); 27 | force.Apply(1); 28 | 29 | particle.Update(1); 30 | Assert.AreEqual(new Vector2D(0, 0), particle.Acceleration, "速度为0的物体不受阻力影响"); 31 | 32 | particle.Velocity = new Vector2D(1, 0); 33 | 34 | force.Apply(1); 35 | particle.Update(1); 36 | Assert.AreEqual(new Vector2D(-0.2, 0), particle.Acceleration, "速度不为0的物体按照公式计算阻力大小"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /UnitTest/Force/ParticleElasticTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Object; 7 | 8 | [TestClass] 9 | public class ParticleElasticTest 10 | { 11 | [TestMethod] 12 | public void TestConstructor() 13 | { 14 | var force = new ParticleElastic(1, 2); 15 | Assert.IsNotNull(force); 16 | } 17 | 18 | [TestMethod] 19 | public void TestApplyTo() 20 | { 21 | var force = new ParticleElastic(1, 2); 22 | var pA = new Particle 23 | { 24 | Position = new Vector2D(0, 0), 25 | Mass = 1 26 | }; 27 | var pB = new Particle 28 | { 29 | Position = new Vector2D(1, 0), 30 | Mass = 1 31 | }; 32 | force.Add(pA); 33 | force.LinkWith(pB); 34 | 35 | force.Apply(1); 36 | 37 | pA.Update(1); 38 | Assert.AreEqual(new Vector2D(-1, 0), pA.Acceleration); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /UnitTest/Force/ParticleGravityTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Object; 7 | 8 | [TestClass] 9 | public class ParticleGravityTest 10 | { 11 | [TestMethod] 12 | public void TestConstructor() 13 | { 14 | var force = new ParticleGravity(new Vector2D(0, 9.8)); 15 | Assert.IsNotNull(force); 16 | } 17 | 18 | [TestMethod] 19 | public void TestApplyTo() 20 | { 21 | var force = new ParticleGravity(new Vector2D(0, 9.8)); 22 | var particle = new Particle 23 | { 24 | Mass = 2 25 | }; 26 | force.Add(particle); 27 | force.Apply(1); 28 | particle.Update(1); 29 | Assert.AreEqual(new Vector2D(0, 9.8), particle.Acceleration, "根据公式计算物体所应受到的重力"); 30 | 31 | particle.InverseMass = 0; 32 | force.Apply(1); 33 | particle.Update(1); 34 | Assert.AreEqual(new Vector2D(0, 0), particle.Acceleration, "质量无限大的物体设定为不受重力影响"); 35 | 36 | force.Remove(particle); 37 | force.Apply(1); 38 | particle.Update(1); 39 | Assert.AreEqual(new Vector2D(0, 0), particle.Acceleration, "被从作用力发生器中移除的物体不受重力影响"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UnitTest/Force/Zones/GlobalZoneTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force.Zones 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Force.Zones; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class GlobalZoneTest 11 | { 12 | [TestMethod] 13 | public void TestTryApplyTo() 14 | { 15 | var p = new Particle 16 | { 17 | Mass = 1 18 | }; 19 | var zone = new GlobalZone(); 20 | zone.Add(new ParticleConstantForce(new Vector2D(5, 0))); 21 | 22 | zone.TryApplyTo(p, 1 / 60.0); 23 | p.Update(1 / 60.0); 24 | 25 | Assert.AreEqual(new Vector2D(5, 0), p.Acceleration); 26 | } 27 | 28 | [TestMethod] 29 | public void TestRemove() 30 | { 31 | var p = new Particle 32 | { 33 | Mass = 1 34 | }; 35 | var zone = new GlobalZone(); 36 | var force = new ParticleConstantForce(new Vector2D(5, 0)); 37 | zone.Add(force); 38 | zone.Remove(force); 39 | zone.TryApplyTo(p, 1 / 60.0); 40 | p.Update(1 / 60.0); 41 | 42 | Assert.AreEqual(new Vector2D(0, 0), p.Acceleration); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UnitTest/Force/Zones/RectangleZoneTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Force.Zones 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Force; 6 | using Physics2D.Force.Zones; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class RectangleZoneTest 11 | { 12 | [TestMethod] 13 | public void TestTryApplyTo() 14 | { 15 | var pIn = new Particle 16 | { 17 | Position = new Vector2D(2, 3), 18 | Mass = 1 19 | }; 20 | var pOut = new Particle 21 | { 22 | Position = new Vector2D(6, 6), 23 | Mass = 1 24 | }; 25 | var zone = new RectangleZone(0, 0, 5, 5); 26 | zone.Add(new ParticleConstantForce(new Vector2D(5, 0))); 27 | 28 | zone.TryApplyTo(pIn, 1 / 60.0); 29 | zone.TryApplyTo(pOut, 1 / 60.0); 30 | pIn.Update(1 / 60.0); 31 | pOut.Update(1 / 60.0); 32 | 33 | Assert.AreEqual(new Vector2D(5, 0), pIn.Acceleration); 34 | Assert.AreEqual(new Vector2D(0, 0), pOut.Acceleration); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /UnitTest/Object/ParticleTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Object 2 | { 3 | using System; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Physics2D.Collision.Shapes; 6 | using Physics2D.Common; 7 | using Physics2D.Object; 8 | 9 | [TestClass] 10 | public class ParticleTest 11 | { 12 | [TestMethod] 13 | public void TestConstructor() 14 | { 15 | Particle obj = new Particle 16 | { 17 | Mass = 1, 18 | Position = new Vector2D(0, 50) 19 | }; 20 | 21 | Assert.IsNotNull(obj); 22 | } 23 | 24 | [TestMethod] 25 | public void TestSetMass() 26 | { 27 | Particle obj = new Particle 28 | { 29 | Position = new Vector2D(0, 50) 30 | }; 31 | 32 | // Normal 33 | obj.Mass = 2; 34 | Assert.AreEqual(2, obj.Mass); 35 | Assert.AreEqual(0.5, obj.InverseMass); 36 | 37 | obj.InverseMass = 2; 38 | Assert.AreEqual(0.5, obj.Mass); 39 | Assert.AreEqual(2, obj.InverseMass); 40 | 41 | // Zero 42 | try 43 | { 44 | obj.Mass = 0; 45 | } 46 | catch (ArgumentOutOfRangeException) { } 47 | catch (Exception) 48 | { 49 | Assert.Fail(); 50 | } 51 | 52 | obj.InverseMass = 0; 53 | Assert.AreEqual(0, obj.InverseMass); 54 | Assert.AreEqual(double.MaxValue, obj.Mass); 55 | } 56 | 57 | [TestMethod] 58 | public void TestUpdate() 59 | { 60 | Particle obj = new Particle 61 | { 62 | Mass = 1, 63 | Position = new Vector2D(0, 0) 64 | }; 65 | obj.AddForce(new Vector2D(1, 0)); 66 | 67 | obj.Update(1); 68 | Assert.AreEqual(new Vector2D(0, 0), obj.Position); 69 | Assert.AreEqual(new Vector2D(1, 0), obj.Velocity); 70 | Assert.AreEqual(new Vector2D(1, 0), obj.Acceleration); 71 | 72 | obj.Update(1); 73 | obj.Update(1); 74 | Assert.AreEqual(new Vector2D(2, 0), obj.Position); 75 | Assert.AreEqual(new Vector2D(1, 0), obj.Velocity); 76 | Assert.AreEqual(new Vector2D(0, 0), obj.Acceleration); 77 | } 78 | 79 | [TestMethod] 80 | public void TestBindShape() 81 | { 82 | Particle obj = new Particle 83 | { 84 | Position = new Vector2D(0, 50) 85 | }; 86 | obj.BindShape(new Circle(10)); 87 | 88 | Assert.IsNotNull(obj.Shape as Circle); 89 | Assert.IsTrue(obj.Shape.Body == obj); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /UnitTest/Object/Tools/HandleTest.cs: -------------------------------------------------------------------------------- 1 | namespace UnitTest.Object.Tools 2 | { 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Physics2D.Common; 5 | using Physics2D.Object.Tools; 6 | 7 | [TestClass] 8 | public class HandleTest 9 | { 10 | [TestMethod] 11 | public void TestConstructor() 12 | { 13 | Handle handle = new Handle(new Vector2D(1, 0)); 14 | 15 | Assert.IsNotNull(handle); 16 | Assert.AreEqual(new Vector2D(1, 0), handle.Position); 17 | } 18 | 19 | [TestMethod] 20 | public void TestPropertyChangedEvent() 21 | { 22 | Handle handle = new Handle(new Vector2D(0, 0)); 23 | 24 | Vector2D position = Vector2D.Zero; 25 | handle.PropertyChanged += (sender, e) => 26 | { 27 | position = (sender as Handle).Position; 28 | }; 29 | 30 | handle.Position = new Vector2D(1, 0); 31 | Assert.AreEqual(new Vector2D(1, 0), position); 32 | 33 | handle.Position = new Vector2D(1, 0); 34 | Assert.AreEqual(new Vector2D(1, 0), position); 35 | } 36 | 37 | [TestMethod] 38 | public void TestRelease() 39 | { 40 | Handle handle = new Handle(new Vector2D(0, 0)); 41 | 42 | Vector2D position = Vector2D.Zero; 43 | handle.PropertyChanged += (sender, e) => 44 | { 45 | position = (sender as Handle).Position; 46 | }; 47 | handle.Release(); 48 | 49 | handle.Position = new Vector2D(1, 0); 50 | Assert.AreEqual(Vector2D.Zero, position); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /UnitTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("UnitTest")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("UnitTest")] 9 | [assembly: AssemblyCopyright("Copyright © 2014")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | [assembly: ComVisible(false)] 13 | [assembly: Guid("58d989d3-e99c-47b9-865d-e46238d0e1bb")] 14 | [assembly: AssemblyVersion("1.0.0.0")] 15 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /UnitTest/UnitTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {EAA4C780-0963-4479-9D11-E162127C0014} 7 | Library 8 | Properties 9 | UnitTest 10 | UnitTest 11 | v4.6.1 12 | 512 13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 10.0 15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 17 | False 18 | UnitTest 19 | 20 | 21 | 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | 30 | 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | False 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {e102f925-90fe-41f2-856e-3fab4bb2ad58} 88 | Physics2D 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | False 103 | 104 | 105 | False 106 | 107 | 108 | False 109 | 110 | 111 | False 112 | 113 | 114 | 115 | 116 | 117 | 118 | 125 | -------------------------------------------------------------------------------- /UnitTest/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /WPFDemo/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /WPFDemo/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | -------------------------------------------------------------------------------- /WPFDemo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo 2 | { 3 | using System.Windows; 4 | 5 | /// 6 | /// App.xaml 的交互逻辑 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /WPFDemo/CircleDemo/Circle.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WPFDemo/CircleDemo/Circle.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.CircleDemo 2 | { 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | /// 8 | /// Circle.xaml 的交互逻辑 9 | /// 10 | public partial class Circle : Window 11 | { 12 | private readonly CircleDemo circleDemo; 13 | 14 | public Circle() 15 | { 16 | this.InitializeComponent(); 17 | this.circleDemo = new CircleDemo(this.ImageSurface); 18 | 19 | this.ImageSurface.Source = this.circleDemo.Bitmap; 20 | CompositionTarget.Rendering += this.circleDemo.Update; 21 | } 22 | 23 | private void ImageSurface_MouseDown(object sender, MouseButtonEventArgs e) 24 | { 25 | this.circleDemo.Fire(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /WPFDemo/CircleDemo/CircleDemo.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.CircleDemo 2 | { 3 | using System.Collections.Generic; 4 | using System.Windows.Controls; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Imaging; 7 | using Physics2D; 8 | using Physics2D.Common; 9 | using Physics2D.Factories; 10 | using Physics2D.Force; 11 | using Physics2D.Object; 12 | using WPFDemo.Graphic; 13 | 14 | public class CircleDemo : PhysicsGraphic, IDrawable 15 | { 16 | /// 17 | /// 中心点 18 | /// 19 | private readonly Particle centerObj; 20 | 21 | /// 22 | /// 物体列表 23 | /// 24 | private readonly List objList = new List(); 25 | 26 | public CircleDemo(Image image) 27 | : base(image) 28 | { 29 | // 初始化中心点 30 | this.centerObj = this.PhysicsWorld.CreateFixedParticle( 31 | new Vector2D( 32 | 250.ToSimUnits(), 33 | 200.ToSimUnits())); 34 | 35 | // 注册绘制对象 36 | this.DrawQueue.Add(this); 37 | } 38 | 39 | protected override void UpdatePhysics(double duration) 40 | { 41 | foreach (var item in this.objList) 42 | { 43 | var v = this.centerObj.Position - item.Position; 44 | item.AddForce(v.Normalize() * 30); 45 | } 46 | 47 | this.PhysicsWorld.Update(duration); 48 | } 49 | 50 | public void Fire() 51 | { 52 | if (!this.Start) 53 | { 54 | // 全局增加一个小阻尼 55 | this.PhysicsWorld.CreateGlobalZone(new ParticleDrag(0.01, 0.02)); 56 | this.Start = true; 57 | } 58 | 59 | var item = this.PhysicsWorld.CreateParticle( 60 | new Vector2D(200.ToSimUnits(), 200.ToSimUnits()), 61 | new Vector2D(0, 5), 62 | 1); 63 | this.objList.Add(item); 64 | } 65 | 66 | public void Draw(WriteableBitmap bitmap) 67 | { 68 | bitmap.FillEllipseCentered( 69 | this.centerObj.Position.X.ToDisplayUnits(), 70 | this.centerObj.Position.Y.ToDisplayUnits(), 6, 6, Colors.Red); 71 | for (int i = this.objList.Count - 1; i >= 0; i--) 72 | { 73 | int x = this.objList[i].Position.X.ToDisplayUnits(); 74 | int y = this.objList[i].Position.Y.ToDisplayUnits(); 75 | 76 | bitmap.FillEllipseCentered(x, y, 4, 4, Colors.Black); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /WPFDemo/ContactDemo/Ball.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ContactDemo 2 | { 3 | using System.Windows.Media; 4 | using System.Windows.Media.Imaging; 5 | using Physics2D; 6 | using Physics2D.Object; 7 | using WPFDemo.Graphic; 8 | 9 | internal class Ball : IDrawable 10 | { 11 | public Particle FixedParticle; 12 | public Particle Particle; 13 | public int R; 14 | 15 | public void Draw(WriteableBitmap bitmap) 16 | { 17 | bitmap.DrawLineAa( 18 | this.FixedParticle.Position.X.ToDisplayUnits(), 19 | this.FixedParticle.Position.Y.ToDisplayUnits(), 20 | this.Particle.Position.X.ToDisplayUnits(), 21 | this.Particle.Position.Y.ToDisplayUnits(), 22 | Colors.DarkGray); 23 | bitmap.FillEllipseCentered( 24 | this.Particle.Position.X.ToDisplayUnits(), 25 | this.Particle.Position.Y.ToDisplayUnits(), this.R, this.R, Colors.DarkRed); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WPFDemo/ContactDemo/Contact.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /WPFDemo/ContactDemo/Contact.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ContactDemo 2 | { 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | /// 8 | /// Contact.xaml 的交互逻辑 9 | /// 10 | public partial class Contact : Window 11 | { 12 | private readonly ContactDemo contactDemo; 13 | 14 | public Contact() 15 | { 16 | this.InitializeComponent(); 17 | 18 | this.contactDemo = new ContactDemo(this.ImageSurface); 19 | this.ImageSurface.Source = this.contactDemo.Bitmap; 20 | CompositionTarget.Rendering += this.contactDemo.Update; 21 | } 22 | 23 | private void ImageSurface_MouseDown(object sender, MouseButtonEventArgs e) 24 | { 25 | this.contactDemo.Fire(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WPFDemo/ContactDemo/ContactDemo.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ContactDemo 2 | { 3 | using System.Collections.Generic; 4 | using System.Windows.Controls; 5 | using Physics2D; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Factories; 9 | using Physics2D.Force; 10 | using WPFDemo.Graphic; 11 | 12 | internal class ContactDemo : PhysicsGraphic 13 | { 14 | /// 15 | /// 钢珠列表 16 | /// 17 | private readonly List ballList = new List(); 18 | 19 | public ContactDemo(Image image) 20 | : base(image) 21 | { 22 | Settings.ContactIteration = 1; 23 | 24 | const int num = 5; 25 | 26 | for (int i = 0; i < num; i++) 27 | { 28 | var fB = this.PhysicsWorld.CreateFixedParticle(new Vector2D(160 + 40 * i, 0).ToSimUnits()); 29 | var pB = this.PhysicsWorld.CreateParticle(new Vector2D(160 + 40 * i, 200).ToSimUnits(), new Vector2D(0, 0), 2); 30 | 31 | var ball = new Ball 32 | { 33 | FixedParticle = fB, 34 | Particle = pB, 35 | R = 20 36 | }; 37 | 38 | // 为质体绑定形状 39 | ball.Particle.BindShape(new Circle(ball.R.ToSimUnits())); 40 | 41 | this.PhysicsWorld.CreateRope(200.ToSimUnits(), 0, fB, pB); 42 | this.DrawQueue.Add(ball); 43 | this.ballList.Add(ball); 44 | } 45 | 46 | // 增加重力和空气阻力 47 | this.PhysicsWorld.CreateGlobalZone(new ParticleGravity(new Vector2D(0, 40))); 48 | this.PhysicsWorld.CreateParticle(Vector2D.Zero, new Vector2D(1, 0), 1); 49 | this.Slot = 1 / 120.0; 50 | 51 | this.Start = true; 52 | } 53 | 54 | protected override void UpdatePhysics(double duration) 55 | { 56 | this.PhysicsWorld.Update(duration); 57 | } 58 | 59 | public void Fire() 60 | { 61 | this.ballList[0].Particle.Velocity = new Vector2D(-10, 0); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /WPFDemo/DrawingCanvas.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo 2 | { 3 | using System.Collections.Generic; 4 | using System.Windows.Controls; 5 | using System.Windows.Media; 6 | 7 | public class DrawingCanvas : Panel 8 | { 9 | private readonly List visuals = new List(); 10 | 11 | protected override Visual GetVisualChild(int index) 12 | { 13 | return this.visuals[index]; 14 | } 15 | 16 | protected override int VisualChildrenCount => this.visuals.Count; 17 | 18 | public void AddVisual(Visual visual) 19 | { 20 | this.visuals.Add(visual); 21 | 22 | this.AddVisualChild(visual); 23 | this.AddLogicalChild(visual); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /WPFDemo/ElasticDemo/DestructibleElastic.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ElasticDemo 2 | { 3 | using System.Collections.Generic; 4 | using Physics2D.Force; 5 | using Physics2D.Object; 6 | 7 | internal sealed class DestructibleElastic : ParticleForceGenerator 8 | { 9 | /// 10 | /// 所有链接 11 | /// 12 | private readonly List linked = new List(); 13 | 14 | /// 15 | /// 弹性常量 16 | /// 17 | private readonly double k; 18 | 19 | /// 20 | /// 静息长度 21 | /// 22 | private readonly double length; 23 | 24 | /// 25 | /// 延展系数 26 | /// 当链接的长度与静息长度的比值超过该数值的时发生断裂 27 | /// 28 | private readonly double lengthFactor; 29 | 30 | public DestructibleElastic(double k, double length, double lengthFactor = 4) 31 | { 32 | this.k = k; 33 | this.length = length; 34 | this.lengthFactor = lengthFactor; 35 | } 36 | 37 | public void Joint(Particle item) 38 | { 39 | this.linked.Add(new LinkedItem 40 | { 41 | Particle = item, 42 | IsValid = true 43 | }); 44 | } 45 | 46 | public override void ApplyTo(Particle particle, double duration) 47 | { 48 | for (int i = this.linked.Count - 1; i >= 0; i--) 49 | { 50 | if (this.linked[i].IsValid) 51 | { 52 | var d = particle.Position - this.linked[i].Particle.Position; 53 | if (d.Length() / this.length > this.lengthFactor) 54 | { 55 | this.linked[i].IsValid = false; 56 | continue; 57 | } 58 | 59 | double force = (this.length - d.Length()) * this.k; 60 | particle.AddForce(d.Normalize() * force); 61 | } 62 | } 63 | } 64 | 65 | private class LinkedItem 66 | { 67 | public Particle Particle; 68 | public bool IsValid; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /WPFDemo/ElasticDemo/Elastic.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WPFDemo/ElasticDemo/Elastic.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ElasticDemo 2 | { 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | /// 8 | /// ElasticDemo.xaml 的交互逻辑 9 | /// 10 | public partial class Elastic : Window 11 | { 12 | private ElasticDemo elasticDemo; 13 | 14 | public Elastic() 15 | { 16 | this.InitializeComponent(); 17 | 18 | this.elasticDemo = new ElasticDemo(this.ImageSurface); 19 | 20 | this.ImageSurface.Source = this.elasticDemo.Bitmap; 21 | CompositionTarget.Rendering += this.elasticDemo.Update; 22 | } 23 | 24 | private void ImageSurface_MouseDown(object sender, MouseButtonEventArgs e) 25 | { 26 | this.elasticDemo.Fire(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /WPFDemo/ElasticDemo/ElasticDemo.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ElasticDemo 2 | { 3 | using System.Windows.Controls; 4 | using Physics2D; 5 | using Physics2D.Common; 6 | using Physics2D.Factories; 7 | using Physics2D.Force; 8 | using WPFDemo.Graphic; 9 | 10 | public class ElasticDemo : PhysicsGraphic 11 | { 12 | /// 13 | /// 网格宽 14 | /// 15 | private const int Width = 20; 16 | 17 | /// 18 | /// 网格高 19 | /// 20 | private const int Height = 10; 21 | 22 | /// 23 | /// 延展系数 24 | /// 25 | private const double Factor = 4; 26 | 27 | /// 28 | /// 网格尺寸 29 | /// 30 | private static readonly double GridSize = 20.ToSimUnits(); 31 | 32 | /// 33 | /// 弹性网 34 | /// 35 | private ElasticatedNet elasticatedNet; 36 | 37 | public ElasticDemo(Image image) 38 | : base(image) 39 | { 40 | // 设置全局的阻力用以增加稳定性 41 | this.PhysicsWorld.CreateGlobalZone(new ParticleDrag(0.8f, 0.6f)); 42 | } 43 | 44 | /// 45 | /// 更新物理世界 46 | /// 47 | /// 48 | protected override void UpdatePhysics(double duration) 49 | { 50 | this.PhysicsWorld.Update(duration); 51 | } 52 | 53 | /// 54 | /// 响应鼠标的操作 55 | /// 56 | public void Fire() 57 | { 58 | if (this.Start) 59 | { 60 | this.PhysicsWorld.RemoveObject(this.elasticatedNet); 61 | this.DrawQueue.Remove(this.elasticatedNet); 62 | } 63 | 64 | // 创建弹性网并加入到物理世界和绘制队列 65 | this.elasticatedNet = new ElasticatedNet( 66 | new Vector2D(40, 120).ToSimUnits(), 67 | Width, Height, GridSize, Factor); 68 | this.DrawQueue.Add(this.elasticatedNet); 69 | this.PhysicsWorld.AddObject(this.elasticatedNet); 70 | 71 | // 拉扯弹性网 72 | var net = this.elasticatedNet.Net; 73 | for (int i = 0; i < Width; i++) 74 | { 75 | net[i, Height - 1].Velocity = new Vector2D(0, 0.1) * 4 * (i >= Width / 2 ? -1 : 1); 76 | net[i, Height - 1].InverseMass = 0; 77 | } 78 | 79 | this.Start = true; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /WPFDemo/ElasticDemo/ElasticatedNet.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.ElasticDemo 2 | { 3 | using System.Windows.Media; 4 | using System.Windows.Media.Imaging; 5 | using Physics2D; 6 | using Physics2D.Common; 7 | using Physics2D.Core; 8 | using Physics2D.Factories; 9 | using Physics2D.Object; 10 | using WPFDemo.Graphic; 11 | 12 | internal sealed class ElasticatedNet : CustomObject, IDrawable 13 | { 14 | private readonly Particle[,] net; 15 | private readonly int width; 16 | private readonly int height; 17 | private readonly Vector2D startPosition; 18 | private readonly double gridSize; 19 | private readonly double factor; 20 | 21 | /// 22 | /// 弹簧网网格 23 | /// 24 | public Particle[,] Net 25 | { 26 | get { return this.net; } 27 | } 28 | 29 | public ElasticatedNet(Vector2D startPosition, int width, int height, double gridSize, double factor) 30 | { 31 | this.width = width; 32 | this.height = height; 33 | this.startPosition = startPosition; 34 | this.gridSize = gridSize; 35 | this.factor = factor; 36 | this.net = new Particle[width, height]; 37 | } 38 | 39 | /// 40 | /// 实现图形接口的绘图逻辑 41 | /// 42 | /// 43 | public void Draw(WriteableBitmap bitmap) 44 | { 45 | bitmap.Clear(); 46 | for (int i = 0; i < this.width; i++) 47 | { 48 | for (int j = 0; j < this.height; j++) 49 | { 50 | int x = this.net[i, j].Position.X.ToDisplayUnits(); 51 | int y = this.net[i, j].Position.Y.ToDisplayUnits(); 52 | 53 | // 绘制弹性连线 54 | double dLeft; 55 | double dDown; 56 | if (i > 0 && (dLeft = (this.net[i, j].Position - this.net[i - 1, j].Position).Length()) / this.gridSize < this.factor) 57 | { 58 | byte colorRow = dLeft > this.gridSize ? (byte)((int)(255 - (dLeft - this.gridSize) / ((this.factor - 1) * this.gridSize) * 200)) : (byte)255; 59 | bitmap.DrawLineAa( 60 | x, y, 61 | this.net[i - 1, j].Position.X.ToDisplayUnits(), this.net[i - 1, j].Position.Y.ToDisplayUnits(), 62 | Color.FromArgb(colorRow, 0, 0, 0)); 63 | } 64 | 65 | if (j > 0 && (dDown = (this.net[i, j].Position - this.net[i, j - 1].Position).Length()) / this.gridSize < this.factor) 66 | { 67 | byte colorCol = dDown > this.gridSize ? (byte)((int)(255 - (dDown - this.gridSize) / ((this.factor - 1) * this.gridSize) * 200)) : (byte)255; 68 | bitmap.DrawLineAa( 69 | x, y, 70 | this.net[i, j - 1].Position.X.ToDisplayUnits(), this.net[i, j - 1].Position.Y.ToDisplayUnits(), 71 | Color.FromArgb(colorCol, 0, 0, 0)); 72 | } 73 | 74 | // 绘制点 75 | bitmap.FillEllipseCentered(x, y, 2, 2, Colors.Black); 76 | } 77 | } 78 | } 79 | 80 | public override void OnInit(World world) 81 | { 82 | // 根据参数创建弹性网 83 | for (int i = 0; i < this.width; i++) 84 | { 85 | for (int j = 0; j < this.height; j++) 86 | { 87 | this.net[i, j] = world.CreateParticle( 88 | new Vector2D( 89 | this.startPosition.X + i * this.gridSize, 90 | this.startPosition.Y + j * this.gridSize), 91 | Vector2D.Zero, 92 | 1); 93 | } 94 | } 95 | 96 | // 设置弹性网 97 | for (int i = 0; i < this.width; i++) 98 | { 99 | for (int j = 0; j < this.height; j++) 100 | { 101 | var spring = new DestructibleElastic(12, this.gridSize, this.factor); 102 | if (i > 0) 103 | { 104 | spring.Joint(this.net[i - 1, j]); 105 | } 106 | 107 | if (i < this.width - 1) 108 | { 109 | spring.Joint(this.net[i + 1, j]); 110 | } 111 | 112 | if (j > 0) 113 | { 114 | spring.Joint(this.net[i, j - 1]); 115 | } 116 | 117 | if (j < this.height - 1) 118 | { 119 | spring.Joint(this.net[i, j + 1]); 120 | } 121 | 122 | spring.Add(this.net[i, j]); 123 | world.ForceGenerators.Add(spring); 124 | } 125 | } 126 | } 127 | 128 | public override void OnRemove(World world) 129 | { 130 | foreach (var item in this.net) 131 | { 132 | world.RemoveObject(item); 133 | } 134 | } 135 | 136 | public override void Update(double duration) 137 | { 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /WPFDemo/FireworksDemo/Fireworks.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /WPFDemo/FireworksDemo/Fireworks.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.FireworksDemo 2 | { 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | using Physics2D; 7 | 8 | /// 9 | /// Fireworks.xaml 的交互逻辑 10 | /// 11 | public partial class Fireworks : Window 12 | { 13 | private FireworksDemo fireworksDemo; 14 | 15 | public Fireworks() 16 | { 17 | this.InitializeComponent(); 18 | this.fireworksDemo = new FireworksDemo(this.ImageSurface); 19 | 20 | this.ImageSurface.Source = this.fireworksDemo.Bitmap; 21 | CompositionTarget.Rendering += this.fireworksDemo.Update; 22 | } 23 | 24 | private void ImageSurface_MouseDown(object sender, MouseButtonEventArgs e) 25 | { 26 | this.fireworksDemo.Fire(e.GetPosition(this.ImageSurface).X.ToSimUnits(), e.GetPosition(this.ImageSurface).Y.ToSimUnits()); 27 | } 28 | 29 | private void Checked(object sender, RoutedEventArgs e) 30 | { 31 | if (this.Drag.IsChecked == true) 32 | { 33 | this.fireworksDemo.Type = FireworksDemo.PhysicsType.Water; 34 | } 35 | else if (this.Wind.IsChecked == true) 36 | { 37 | this.fireworksDemo.Type = FireworksDemo.PhysicsType.Wind; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /WPFDemo/FireworksDemo/FireworksDemo.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.FireworksDemo 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Windows.Controls; 6 | using System.Windows.Media; 7 | using System.Windows.Media.Imaging; 8 | using Physics2D; 9 | using Physics2D.Collision.Shapes; 10 | using Physics2D.Common; 11 | using Physics2D.Factories; 12 | using Physics2D.Force; 13 | using Physics2D.Force.Zones; 14 | using Physics2D.Object; 15 | using WPFDemo.Graphic; 16 | 17 | public class FireworksDemo : PhysicsGraphic, IDrawable 18 | { 19 | /// 20 | /// 选项状态枚举 21 | /// 22 | public enum PhysicsType 23 | { 24 | None, 25 | Water, 26 | Wind 27 | } 28 | 29 | /// 30 | /// 当前选项 31 | /// 32 | private PhysicsType type = PhysicsType.None; 33 | 34 | /// 35 | /// 当前选项 36 | /// 37 | public PhysicsType Type 38 | { 39 | get 40 | { 41 | return this.type; 42 | } 43 | 44 | set 45 | { 46 | this.type = value; 47 | 48 | // 设置力场 49 | if (this.type == PhysicsType.Water) 50 | { 51 | this.PhysicsWorld.Zones.Add(this.dragZone); 52 | this.PhysicsWorld.Zones.Remove(this.windZone); 53 | } 54 | else if (this.type == PhysicsType.Wind) 55 | { 56 | this.PhysicsWorld.Zones.Add(this.windZone); 57 | this.PhysicsWorld.Zones.Remove(this.dragZone); 58 | } 59 | } 60 | } 61 | 62 | /// 63 | /// 阻力 64 | /// 65 | private readonly ParticleDrag drag = new ParticleDrag(2, 1); 66 | 67 | /// 68 | /// 风 69 | /// 70 | private readonly ParticleConstantForce wind = new ParticleConstantForce(new Vector2D(20, -5)); 71 | 72 | /// 73 | /// 阻力区 74 | /// 75 | private readonly Zone dragZone; 76 | 77 | /// 78 | /// 有风区 79 | /// 80 | private readonly Zone windZone; 81 | 82 | /// 83 | /// 横板 84 | /// 85 | private Edge edge; 86 | 87 | /// 88 | /// 物体列表 89 | /// 90 | private readonly List objList = new List(); 91 | 92 | /// 93 | /// 粒子形状Id 94 | /// 所有粒子都使用相同的Id,从而使粒子之间可以交叠 95 | /// 96 | private readonly int shapeId = Shape.NewId(); 97 | 98 | /// 99 | /// 粒子半径 100 | /// 101 | private const int BallSize = 4; 102 | private const int WorldHeight = 400; 103 | private const int WorldWidth = 500; 104 | 105 | public FireworksDemo(Image image) 106 | : base(image) 107 | { 108 | this.DrawQueue.Add(this); 109 | 110 | this.dragZone = new RectangleZone( 111 | 0.ToSimUnits(), 112 | (WorldHeight * 2 / 3.0).ToSimUnits(), 113 | 500.ToSimUnits(), 114 | 400.ToSimUnits()); 115 | this.dragZone.Add(this.drag); 116 | 117 | this.windZone = new RectangleZone( 118 | 0.ToSimUnits(), 119 | (WorldHeight * 1 / 3.0).ToSimUnits(), 120 | 500.ToSimUnits(), 121 | (WorldHeight * 2 / 3.0).ToSimUnits()); 122 | this.windZone.Add(this.wind); 123 | } 124 | 125 | protected override void UpdatePhysics(double duration) 126 | { 127 | this.PhysicsWorld.Update(duration); 128 | } 129 | 130 | public void Draw(WriteableBitmap bitmap) 131 | { 132 | if (this.type == PhysicsType.Water) 133 | { 134 | bitmap.FillRectangle(0, WorldHeight * 2 / 3, WorldWidth, WorldHeight, Colors.SkyBlue); 135 | } 136 | else if (this.type == PhysicsType.Wind) 137 | { 138 | bitmap.FillRectangle(0, WorldHeight * 1 / 3, WorldWidth, WorldHeight * 2 / 3, Colors.LightGray); 139 | } 140 | 141 | bitmap.DrawLineAa( 142 | this.edge.PointA.X.ToDisplayUnits(), 143 | this.edge.PointA.Y.ToDisplayUnits(), 144 | this.edge.PointB.X.ToDisplayUnits(), 145 | this.edge.PointB.Y.ToDisplayUnits(), Colors.Black); 146 | 147 | for (int i = this.objList.Count - 1; i >= 0; i--) 148 | { 149 | int x = this.objList[i].Position.X.ToDisplayUnits(); 150 | int y = this.objList[i].Position.Y.ToDisplayUnits(); 151 | 152 | if (y > WorldHeight || x > WorldWidth || x < 0 || y < 0) 153 | { 154 | this.PhysicsWorld.RemoveObject(this.objList[i]); 155 | this.objList.Remove(this.objList[i]); 156 | } 157 | else 158 | { 159 | if (this.type == PhysicsType.Water) 160 | { 161 | bitmap.FillEllipseCentered(x, y, BallSize, BallSize, y > WorldHeight * 2 / 3 ? Colors.DarkBlue : Colors.Black); 162 | } 163 | else 164 | { 165 | bitmap.FillEllipseCentered(x, y, BallSize, BallSize, Colors.Black); 166 | } 167 | } 168 | } 169 | } 170 | 171 | public void Fire(double x, double y) 172 | { 173 | if (!this.Start) 174 | { 175 | this.Start = true; 176 | 177 | // 增加重力 178 | this.PhysicsWorld.CreateGravity(9.8); 179 | 180 | // 添加边缘 181 | this.edge = this.PhysicsWorld.CreateEdge( 182 | 100.ToSimUnits(), 183 | 350.ToSimUnits(), 184 | 400.ToSimUnits(), 185 | 200.ToSimUnits()); 186 | this.Slot = 1 / 60.0; 187 | } 188 | 189 | var rnd = new Random(); 190 | 191 | for (int i = 0; i < 10; i++) 192 | { 193 | var paritcle = this.PhysicsWorld.CreateParticle( 194 | new Vector2D(x, y), 195 | new Vector2D(rnd.NextDouble() * 6 - 3, rnd.NextDouble() * 6 - 3), 196 | 1f, 0.1); 197 | paritcle.BindShape(new Circle(BallSize.ToSimUnits(), this.shapeId)); 198 | this.objList.Add(paritcle); 199 | } 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /WPFDemo/FluidDemo/Fluid.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WPFDemo/FluidDemo/Fluid.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.FluidDemo 2 | { 3 | using System.Windows.Input; 4 | using System.Windows.Media; 5 | 6 | /// 7 | /// Fluid.xaml 的交互逻辑 8 | /// 9 | public partial class Fluid 10 | { 11 | private readonly FluidDemo fluidDemo; 12 | 13 | public Fluid() 14 | { 15 | this.InitializeComponent(); 16 | this.fluidDemo = new FluidDemo(this.ImageSurface); 17 | 18 | this.ImageSurface.Source = this.fluidDemo.Bitmap; 19 | CompositionTarget.Rendering += this.fluidDemo.Update; 20 | } 21 | 22 | private void imageSurface_MouseDown(object sender, MouseButtonEventArgs e) 23 | { 24 | this.fluidDemo.Fire(); 25 | } 26 | 27 | private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) 28 | { 29 | CompositionTarget.Rendering -= this.fluidDemo.Update; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /WPFDemo/FluidDemo/FluidDemo.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.FluidDemo 2 | { 3 | using System; 4 | using System.Windows.Controls; 5 | using Physics2D; 6 | using Physics2D.Collision.Shapes; 7 | using Physics2D.Common; 8 | using Physics2D.Factories; 9 | using Physics2D.Force; 10 | using WPFDemo.Graphic; 11 | 12 | public class FluidDemo : PhysicsGraphic 13 | { 14 | private readonly Water water; 15 | 16 | private readonly Vector2D center = new Vector2D(ConvertUnits.ToSimUnits(250f), ConvertUnits.ToSimUnits(200f)); 17 | 18 | public FluidDemo(Image image) 19 | : base(image) 20 | { 21 | // 创建液体容器 22 | this.water = new Water((int)image.Width, (int)image.Height); 23 | 24 | // 添加到绘制队列 25 | this.DrawQueue.Add(this.water); 26 | } 27 | 28 | protected override void UpdatePhysics(double duration) 29 | { 30 | if (!this.flag) 31 | foreach (var item in this.water.ObjList) 32 | { 33 | Vector2D v = this.center - item.Position; 34 | double d = v.Length(); 35 | item.AddForce(v.Normalize() * 5 * d); 36 | } 37 | 38 | this.PhysicsWorld.Update(duration); 39 | } 40 | 41 | public void Fire() 42 | { 43 | Random rnd = new Random(); 44 | if (!this.Start) 45 | { 46 | this.Start = true; 47 | 48 | // 设置全局的阻力 49 | this.PhysicsWorld.CreateGlobalZone(new ParticleDrag(0.5, 0.5)); 50 | 51 | // 初始化水 52 | for (int i = 0; i < 20; i++) 53 | { 54 | var item = this.PhysicsWorld.CreateParticle( 55 | new Vector2D( 56 | rnd.Next((int)this.Bitmap.Width).ToSimUnits(), 57 | rnd.Next((int)this.Bitmap.Height).ToSimUnits()), 58 | Vector2D.Zero, 59 | 1); 60 | item.BindShape(new Circle(2.ToSimUnits())); 61 | this.water.ObjList.Add(item); 62 | } 63 | } 64 | 65 | // 抖动 66 | this.water.ObjList.ForEach(obj => obj.Velocity.Set(rnd.Next(5) - 2.5, rnd.Next(5) - 2.5)); 67 | } 68 | 69 | private bool flag = false; 70 | 71 | } 72 | } -------------------------------------------------------------------------------- /WPFDemo/FluidDemo/Water.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.FluidDemo 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using System.Windows.Media.Imaging; 7 | using Physics2D; 8 | using Physics2D.Object; 9 | using WPFDemo.Graphic; 10 | 11 | internal class Water : IDrawable 12 | { 13 | public readonly List ObjList = new List(); 14 | 15 | private const int Threshold = 900; 16 | private const int GridR = 60; 17 | 18 | private const int R = 150; 19 | 20 | private readonly int[] metaTable; 21 | private readonly int[,] cacheTable; 22 | private readonly object[,] cacheLocks; 23 | private int[,] cache; 24 | 25 | public Water(int maxWidth, int maxHeight) 26 | { 27 | // 计算势能函数缓存 28 | this.metaTable = new int[GridR]; 29 | this.metaTable[0] = Threshold; 30 | for (int i = 1; i < GridR; i++) 31 | { 32 | this.metaTable[i] = R * R / (i * i); 33 | } 34 | 35 | // 计算势能缓存 36 | this.cacheTable = new int[2 * GridR, 2 * GridR]; 37 | for (int i = 0; i < 2 * GridR; i++) 38 | { 39 | for (int j = 0; j < 2 * GridR; j++) 40 | { 41 | int d = (int)Math.Sqrt((i - GridR) * (i - GridR) + (j - GridR) * (j - GridR) + 0.5); 42 | this.cacheTable[i, j] = d < GridR ? this.metaTable[d] : 0; 43 | } 44 | } 45 | 46 | // 初始化锁 47 | int w = maxWidth; 48 | int h = maxHeight; 49 | this.cacheLocks = new object[w, h]; 50 | for (int i = 0; i < w; i++) 51 | { 52 | for (int j = 0; j < h; j++) 53 | { 54 | this.cacheLocks[i, j] = new object(); 55 | } 56 | } 57 | } 58 | 59 | public unsafe void Draw(WriteableBitmap bitmap) 60 | { 61 | // 绘制Metaball 62 | using (var wc = bitmap.GetBitmapContext()) 63 | { 64 | int w = wc.Width; 65 | int h = wc.Height; 66 | var pixels = wc.Pixels; 67 | this.cache = this.cache ?? new int[w, h]; 68 | Array.Clear(this.cache, 0, this.cache.Length); 69 | 70 | // 叠加每个球的势能 71 | Parallel.ForEach(this.ObjList, obj => 72 | { 73 | int x = obj.Position.X.ToDisplayUnits(); 74 | int y = obj.Position.Y.ToDisplayUnits(); 75 | 76 | for (int i = x - GridR, I = 0; i < x + GridR; i++, I++) 77 | { 78 | for (int j = y - GridR, J = 0; j < y + GridR; j++, J++) 79 | { 80 | if (i < 0 || i >= w || j < 0 || j >= h) continue; 81 | else 82 | { 83 | lock (this.cacheLocks[i, j]) 84 | { 85 | this.cache[i, j] += this.cacheTable[I, J]; 86 | } 87 | } 88 | } 89 | } 90 | }); 91 | 92 | // 渲染画布 93 | Parallel.For(0, h, y => 94 | { 95 | for (int x = 0; x < w; x++) 96 | { 97 | if (this.cache[x, y] >= Threshold) 98 | { 99 | pixels[y * w + x] = (255 << 24) | (16 << 68) | (146 << 8) | 216; 100 | } 101 | } 102 | }); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /WPFDemo/Graphic/IDrawable.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.Graphic 2 | { 3 | using System.Windows.Media.Imaging; 4 | 5 | public interface IDrawable 6 | { 7 | void Draw(WriteableBitmap bitmap); 8 | } 9 | } -------------------------------------------------------------------------------- /WPFDemo/Graphic/PhysicsGraphic.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.Graphic 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Windows.Controls; 6 | using System.Windows.Media.Imaging; 7 | using Physics2D.Core; 8 | 9 | public abstract class PhysicsGraphic 10 | { 11 | /// 12 | /// 物理世界 13 | /// 14 | protected World PhysicsWorld = new World(); 15 | 16 | /// 17 | /// 帧率控制计时器 18 | /// 19 | protected TimeTracker TimeTracker = new TimeTracker(); 20 | 21 | protected double TimeSpan = 0; 22 | 23 | /// 24 | /// 是否渲染标记 25 | /// 26 | public bool Start = false; 27 | 28 | /// 29 | /// 绘制层 30 | /// 31 | public readonly WriteableBitmap Bitmap; 32 | 33 | /// 34 | /// 绘制队列 35 | /// 36 | protected readonly List DrawQueue = new List(); 37 | 38 | /// 39 | /// 物理演算时间槽大小 40 | /// 41 | protected double Slot = 1 / 60.0; 42 | 43 | protected PhysicsGraphic(Image image) 44 | { 45 | this.Bitmap = BitmapFactory.New((int)image.Width, (int)image.Height); 46 | } 47 | 48 | public void Update(object sender, EventArgs e) 49 | { 50 | var interval = this.TimeTracker.Update(); 51 | if (!this.Start) 52 | { 53 | return; 54 | } 55 | 56 | // 更新物理世界 57 | this.TimeSpan += interval; 58 | for (; this.TimeSpan >= this.Slot; this.TimeSpan -= this.Slot) 59 | { 60 | this.UpdatePhysics(this.Slot); 61 | } 62 | 63 | // 更新图形 64 | this.Bitmap.Clear(); 65 | this.DrawQueue.ForEach(item => item.Draw(this.Bitmap)); 66 | } 67 | 68 | /// 69 | /// 更新物理世界 70 | /// 71 | /// 持续时间 72 | protected abstract void UpdatePhysics(double duration); 73 | } 74 | } -------------------------------------------------------------------------------- /WPFDemo/Graphic/TimeTracker.cs: -------------------------------------------------------------------------------- 1 | namespace WPFDemo.Graphic 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | 6 | public class TimeTracker 7 | { 8 | public double TimerInterval { get; set; } = -1; 9 | 10 | public DateTime ElapsedTime { get; private set; } 11 | 12 | public double DeltaSeconds { get; private set; } 13 | 14 | public event EventHandler TimerFired; 15 | 16 | public TimeTracker() 17 | { 18 | this.ElapsedTime = DateTime.Now; 19 | } 20 | 21 | public double Update() 22 | { 23 | DateTime currentTime = DateTime.Now; 24 | TimeSpan diffTime = currentTime - this.ElapsedTime; 25 | 26 | this.DeltaSeconds = diffTime.TotalSeconds; 27 | if (this.TimerInterval > 0) 28 | { 29 | if (currentTime != this.ElapsedTime) 30 | { 31 | Debug.Assert(this.TimerFired != null, "TimerFired != null"); 32 | this.TimerFired(this, null); 33 | } 34 | } 35 | 36 | this.ElapsedTime = currentTime; 37 | 38 | return this.DeltaSeconds; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /WPFDemo/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 |