summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source
diff options
context:
space:
mode:
authorchai <215380520@qq.com>2024-06-03 10:15:45 +0800
committerchai <215380520@qq.com>2024-06-03 10:15:45 +0800
commitacea7b2e728787a0d83bbf83c8c1f042d2c32e7e (patch)
tree0bfec05c1ca2d71be2c337bcd110a0421f19318b /Plugins/MonoGame.Extended/source
parent88febcb02bf127d961c6471d9e846c0e1315f5c3 (diff)
+ plugins project
Diffstat (limited to 'Plugins/MonoGame.Extended/source')
-rw-r--r--Plugins/MonoGame.Extended/source/Directory.Build.props20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs341
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs39
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs310
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs88
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs76
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs79
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config4
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs79
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs108
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs324
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs145
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs227
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs48
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs63
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs90
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs69
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs87
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs120
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs387
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs450
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs203
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs155
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxobin0 -> 4829 bytes
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx72
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxobin0 -> 4833 bytes
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs19
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs114
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs92
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs102
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs70
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs82
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs271
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs104
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs173
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs69
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs331
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs328
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs11
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs125
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs81
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs265
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs147
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs4
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs140
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs67
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs89
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs38
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs57
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs221
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs529
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs134
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs104
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs193
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs39
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs57
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs149
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs77
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs59
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs43
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs152
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs83
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs222
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs49
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs68
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs38
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs125
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs126
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs191
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs75
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs6
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs139
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs78
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs76
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs139
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs11
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs229
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs66
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs70
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs150
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs144
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs120
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs184
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs43
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs133
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs354
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs156
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs217
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs837
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs68
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs157
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs117
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs85
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs83
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs270
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs7
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs224
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs579
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs519
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs101
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs127
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs1038
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs302
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs380
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs379
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs143
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs199
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs71
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs694
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs317
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs332
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs95
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs252
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs311
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs289
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs101
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs88
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs567
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs249
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings2
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs209
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs19
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs78
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs58
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs49
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs29
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs94
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs89
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs179
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs80
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs109
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs107
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs258
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs113
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs456
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs183
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs255
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs96
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs36
423 files changed, 31527 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/Directory.Build.props b/Plugins/MonoGame.Extended/source/Directory.Build.props
new file mode 100644
index 0000000..ae50af1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/Directory.Build.props
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+
+ <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <ArtifactsPath>$(SolutionDirectory).artifacts/source</ArtifactsPath>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <IsPackable>true</IsPackable>
+ <NoWarn>NU1701</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.DesktopGL"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj
new file mode 100644
index 0000000..3745741
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Animations to make MonoGame more awesome.</Description>
+ <PackageTags>monogame animations spritesheet sprite</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
new file mode 100644
index 0000000..51467d9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
@@ -0,0 +1,341 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collisions.Layers;
+using MonoGame.Extended.Collisions.QuadTree;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// Handles basic collision between actors.
+ /// When two actors collide, their OnCollision method is called.
+ /// </summary>
+ public class CollisionComponent : SimpleGameComponent
+ {
+ public const string DEFAULT_LAYER_NAME = "default";
+
+ private Dictionary<string, Layer> _layers = new();
+
+ /// <summary>
+ /// List of collision's layers
+ /// </summary>
+ public IReadOnlyDictionary<string, Layer> Layers => _layers;
+
+ private HashSet<(Layer, Layer)> _layerCollision = new();
+
+ /// <summary>
+ /// Creates component with default layer, which is a collision tree covering the specified area (using <see cref="QuadTree"/>.
+ /// </summary>
+ /// <param name="boundary">Boundary of the collision tree.</param>
+ public CollisionComponent(RectangleF boundary)
+ {
+ SetDefaultLayer(new Layer(new QuadTreeSpace(boundary)));
+ }
+
+ /// <summary>
+ /// Creates component with specifies default layer.
+ /// If layer is null, method creates component without default layer.
+ /// </summary>
+ /// <param name="layer">Default layer</param>
+ public CollisionComponent(Layer layer = null)
+ {
+ if (layer is not null)
+ SetDefaultLayer(layer);
+ }
+
+ /// <summary>
+ /// The main layer has the name from <see cref="DEFAULT_LAYER_NAME"/>.
+ /// The main layer collision with itself and all other layers.
+ /// </summary>
+ /// <param name="layer">Layer to set default</param>
+ public void SetDefaultLayer(Layer layer)
+ {
+ if (_layers.ContainsKey(DEFAULT_LAYER_NAME))
+ Remove(DEFAULT_LAYER_NAME);
+ Add(DEFAULT_LAYER_NAME, layer);
+ foreach (var otherLayer in _layers.Values)
+ AddCollisionBetweenLayer(layer, otherLayer);
+ }
+
+ /// <summary>
+ /// Update the collision tree and process collisions.
+ /// </summary>
+ /// <remarks>
+ /// Boundary shapes are updated if they were changed since the last
+ /// update.
+ /// </remarks>
+ /// <param name="gameTime"></param>
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var layer in _layers.Values)
+ layer.Reset();
+
+ foreach (var (firstLayer, secondLayer) in _layerCollision)
+ foreach (var actor in firstLayer.Space)
+ {
+ var collisions = secondLayer.Space.Query(actor.Bounds.BoundingRectangle);
+ foreach (var other in collisions)
+ if (actor != other && actor.Bounds.Intersects(other.Bounds))
+ {
+ var penetrationVector = CalculatePenetrationVector(actor.Bounds, other.Bounds);
+
+ actor.OnCollision(new CollisionEventArgs
+ {
+ Other = other,
+ PenetrationVector = penetrationVector
+ });
+ other.OnCollision(new CollisionEventArgs
+ {
+ Other = actor,
+ PenetrationVector = -penetrationVector
+ });
+ }
+
+ }
+ }
+
+ /// <summary>
+ /// Inserts the target into the collision tree.
+ /// The target will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="target">Target to insert.</param>
+ public void Insert(ICollisionActor target)
+ {
+ var layerName = target.LayerName ?? DEFAULT_LAYER_NAME;
+ if (!_layers.TryGetValue(layerName, out var layer))
+ {
+ throw new UndefinedLayerException(layerName);
+ }
+
+ layer.Space.Insert(target);
+ }
+
+ /// <summary>
+ /// Removes the target from the collision tree.
+ /// </summary>
+ /// <param name="target">Target to remove.</param>
+ public void Remove(ICollisionActor target)
+ {
+ if (target.LayerName is not null)
+ _layers[target.LayerName].Space.Remove(target);
+ else
+ foreach (var layer in _layers.Values)
+ if (layer.Space.Remove(target))
+ return;
+ }
+
+ #region Layers
+
+ /// <summary>
+ /// Add the new layer. The name of layer must be unique.
+ /// </summary>
+ /// <param name="name">Name of layer</param>
+ /// <param name="layer">The new layer</param>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is null</exception>
+ public void Add(string name, Layer layer)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentNullException(nameof(name));
+
+ if (!_layers.TryAdd(name, layer))
+ throw new DuplicateNameException(name);
+
+ if (name != DEFAULT_LAYER_NAME)
+ AddCollisionBetweenLayer(_layers[DEFAULT_LAYER_NAME], layer);
+ }
+
+ /// <summary>
+ /// Remove the layer and all layer's collisions.
+ /// </summary>
+ /// <param name="name">The name of the layer to delete</param>
+ /// <param name="layer">The layer to delete</param>
+ public void Remove(string name = null, Layer layer = null)
+ {
+ name ??= _layers.First(x => x.Value == layer).Key;
+ _layers.Remove(name, out layer);
+ _layerCollision.RemoveWhere(tuple => tuple.Item1 == layer || tuple.Item2 == layer);
+ }
+
+ public void AddCollisionBetweenLayer(Layer a, Layer b)
+ {
+ _layerCollision.Add((a, b));
+ }
+
+ public void AddCollisionBetweenLayer(string nameA, string nameB)
+ {
+ _layerCollision.Add((_layers[nameA], _layers[nameB]));
+ }
+
+ #endregion
+
+ #region Penetration Vectors
+
+ /// <summary>
+ /// Calculate a's penetration into b
+ /// </summary>
+ /// <param name="a">The penetrating shape.</param>
+ /// <param name="b">The shape being penetrated.</param>
+ /// <returns>The distance vector from the edge of b to a's Position</returns>
+ private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b)
+ {
+ return a switch
+ {
+ CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB),
+ CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB),
+ CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB),
+
+ RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB),
+ RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB),
+ RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB),
+
+ OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB),
+ OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB),
+ OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB),
+
+ _ => throw new ArgumentOutOfRangeException(nameof(a))
+ };
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2)
+ {
+ if (!circ1.Intersects(circ2))
+ {
+ return Vector2.Zero;
+ }
+
+ var displacement = Point2.Displacement(circ1.Center, circ2.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ desiredDisplacement = displacement.NormalizedCopy() * (circ1.Radius + circ2.Radius);
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ1.Radius + circ2.Radius);
+ }
+
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ, RectangleF rect)
+ {
+ var collisionPoint = rect.ClosestPointTo(circ.Center);
+ var cToCollPoint = collisionPoint - circ.Center;
+
+ if (rect.Contains(circ.Center) || cToCollPoint.Equals(Vector2.Zero))
+ {
+ var displacement = Point2.Displacement(circ.Center, rect.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ // Calculate penetration as only in X or Y direction.
+ // Whichever is lower.
+ var dispx = new Vector2(displacement.X, 0);
+ var dispy = new Vector2(0, displacement.Y);
+ dispx.Normalize();
+ dispy.Normalize();
+
+ dispx *= (circ.Radius + rect.Width / 2);
+ dispy *= (circ.Radius + rect.Height / 2);
+
+ if (dispx.LengthSquared() < dispy.LengthSquared())
+ {
+ desiredDisplacement = dispx;
+ displacement.Y = 0;
+ }
+ else
+ {
+ desiredDisplacement = dispy;
+ displacement.X = 0;
+ }
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ.Radius + rect.Height / 2);
+ }
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+ else
+ {
+ var penetration = circ.Radius * cToCollPoint.NormalizedCopy() - cToCollPoint;
+ return penetration;
+ }
+ }
+
+ private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB)
+ {
+ var rotation = Matrix2.CreateRotationZ(orientedRectangleB.Orientation.Rotation);
+ var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center);
+ var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius);
+ var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangleB.Radii);
+
+ var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle);
+ var inverseRotation = Matrix2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation);
+ var transformedPenetration = inverseRotation.Transform(penetrationVector);
+
+ return transformedPenetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect, CircleF circ)
+ {
+ return -PenetrationVector(circ, rect);
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2)
+ {
+ var intersectingRectangle = RectangleF.Intersection(rect1, rect2);
+ Debug.Assert(!intersectingRectangle.IsEmpty,
+ "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector.");
+
+ Vector2 penetration;
+ if (intersectingRectangle.Width < intersectingRectangle.Height)
+ {
+ var d = rect1.Center.X < rect2.Center.X
+ ? intersectingRectangle.Width
+ : -intersectingRectangle.Width;
+ penetration = new Vector2(d, 0);
+ }
+ else
+ {
+ var d = rect1.Center.Y < rect2.Center.Y
+ ? intersectingRectangle.Height
+ : -intersectingRectangle.Height;
+ penetration = new Vector2(0, d);
+ }
+
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB)
+ {
+ return -PenetrationVector(circleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB)
+ {
+ return -PenetrationVector(rectangleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB)
+ .MinimumTranslationVector;
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs
new file mode 100644
index 0000000..ca401df
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs
@@ -0,0 +1,26 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// This class holds data on a collision. It is passed as a parameter to
+ /// OnCollision methods.
+ /// </summary>
+ public class CollisionEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets the object being collided with.
+ /// </summary>
+ public ICollisionActor Other { get; internal set; }
+
+ /// <summary>
+ /// Gets a vector representing the overlap between the two objects.
+ /// </summary>
+ /// <remarks>
+ /// This vector starts at the edge of <see cref="Other"/> and ends at
+ /// the Actor's location.
+ /// </remarks>
+ public Vector2 PenetrationVector { get; internal set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs
new file mode 100644
index 0000000..6a05592
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// An actor that can be collided with.
+ /// </summary>
+ public interface ICollisionActor
+ {
+ /// <summary>
+ /// A name of layer, which will contains this actor.
+ /// If it equals null, an actor will insert into a default layer
+ /// </summary>
+ string LayerName { get => null; }
+
+ /// <summary>
+ /// A bounds of an actor. It is using for collision calculating
+ /// </summary>
+ IShapeF Bounds { get; }
+
+ /// <summary>
+ /// It will called, when collision with an another actor fires
+ /// </summary>
+ /// <param name="collisionInfo">Data about collision</param>
+ void OnCollision(CollisionEventArgs collisionInfo);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs
new file mode 100644
index 0000000..a95f737
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collisions;
+
+/// <summary>
+/// Interface, which split space for optimization of collisions.
+/// </summary>
+public interface ISpaceAlgorithm
+{
+ /// <summary>
+ /// Inserts the actor into the space.
+ /// The actor will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="actor">Actor to insert.</param>
+ void Insert(ICollisionActor actor);
+
+ /// <summary>
+ /// Removes the actor into the space.
+ /// </summary>
+ /// <param name="actor">Actor to remove.</param>
+ bool Remove(ICollisionActor actor);
+
+ /// <summary>
+ /// Removes the actor into the space.
+ /// The actor will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="actor">Actor to remove.</param>
+ IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle);
+
+ /// <summary>
+ /// for foreach
+ /// </summary>
+ /// <returns></returns>
+ List<ICollisionActor>.Enumerator GetEnumerator();
+
+ /// <summary>
+ /// Restructure the space with new positions.
+ /// </summary>
+ void Reset();
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs
new file mode 100644
index 0000000..6e97ac8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace MonoGame.Extended.Collisions.Layers;
+
+/// <summary>
+/// Layer is a group of collision's actors.
+/// </summary>
+public class Layer
+{
+ /// <summary>
+ /// If this property equals true, layer always will reset collision space.
+ /// </summary>
+ public bool IsDynamic { get; set; } = true;
+
+
+ /// <summary>
+ /// The space, which contain actors.
+ /// </summary>
+ public readonly ISpaceAlgorithm Space;
+
+ /// <summary>
+ /// Constructor for layer
+ /// </summary>
+ /// <param name="spaceAlgorithm">A space algorithm for actors</param>
+ /// <exception cref="ArgumentNullException"><paramref name="spaceAlgorithm"/> is null</exception>
+ public Layer(ISpaceAlgorithm spaceAlgorithm)
+ {
+ Space = spaceAlgorithm ?? throw new ArgumentNullException(nameof(spaceAlgorithm));
+ }
+
+ /// <summary>
+ /// Restructure a inner collection, if layer is dynamic, because actors can change own position
+ /// </summary>
+ public virtual void Reset()
+ {
+ if (IsDynamic)
+ Space.Reset();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs
new file mode 100644
index 0000000..a27b5b6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Collisions.Layers;
+
+using System;
+
+/// <summary>
+/// Thrown when the collision system has no layer defined with the specified name
+/// </summary>
+public class UndefinedLayerException : Exception
+{
+ /// <summary>
+ /// Thrown when the collision system has no layer defined with the specified name
+ /// </summary>
+ /// <param name="layerName">The undefined layer name</param>
+ public UndefinedLayerException(string layerName)
+ : base($"Layer with name '{layerName}' is undefined")
+ {
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj
new file mode 100644
index 0000000..bd0729c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Collisions to make MonoGame more awesome.</Description>
+ <PackageTags>monogame collisions</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs
new file mode 100644
index 0000000..a46699a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collisions.QuadTree
+{
+ /// <summary>
+ /// Class for doing collision handling with a quad tree.
+ /// </summary>
+ public class QuadTree
+ {
+ /// <summary>
+ /// The default maximum depth.
+ /// </summary>
+ public const int DefaultMaxDepth = 7;
+
+ /// <summary>
+ /// The default maximum objects per node.
+ /// </summary>
+ public const int DefaultMaxObjectsPerNode = 25;
+
+ /// <summary>
+ /// Contains the children of this node.
+ /// </summary>
+ protected List<QuadTree> Children = new List<QuadTree>();
+
+ /// <summary>
+ /// Contains the data for this node in the quadtree.
+ /// </summary>
+ protected HashSet<QuadtreeData> Contents = new HashSet<QuadtreeData>();
+
+ /// <summary>
+ /// Creates a quad tree with the given bounds.
+ /// </summary>
+ /// <param name="bounds">The bounds of the new quad tree.</param>
+ public QuadTree(RectangleF bounds)
+ {
+ CurrentDepth = 0;
+ NodeBounds = bounds;
+ }
+
+ /// <summary>
+ /// Gets or sets the current depth for this node in the quadtree.
+ /// </summary>
+ protected int CurrentDepth { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum depth of the quadtree.
+ /// </summary>
+ protected int MaxDepth { get; set; } = DefaultMaxDepth;
+ /// <summary>
+ /// Gets or sets the maximum objects per node in this quadtree.
+ /// </summary>
+ protected int MaxObjectsPerNode { get; set; } = DefaultMaxObjectsPerNode;
+
+ /// <summary>
+ /// Gets the bounds of the area contained in this quad tree.
+ /// </summary>
+ public RectangleF NodeBounds { get; protected set; }
+
+ /// <summary>
+ /// Gets whether the current node is a leaf node.
+ /// </summary>
+ public bool IsLeaf => Children.Count == 0;
+
+ /// <summary>
+ /// Counts the number of unique targets in the current Quadtree.
+ /// </summary>
+ /// <returns>Returns the targets of objects found.</returns>
+ public int NumTargets()
+ {
+ List<QuadtreeData> dirtyItems = new List<QuadtreeData>();
+ var objectCount = 0;
+
+ // Do BFS on nodes to count children.
+ var process = new Queue<QuadTree>();
+ process.Enqueue(this);
+ while (process.Count > 0)
+ {
+ var processing = process.Dequeue();
+ if (!processing.IsLeaf)
+ {
+ foreach (var child in processing.Children)
+ {
+ process.Enqueue(child);
+ }
+ }
+ else
+ {
+ foreach (var data in processing.Contents)
+ {
+ if (data.Dirty == false)
+ {
+ objectCount++;
+ data.MarkDirty();
+ dirtyItems.Add(data);
+ }
+ }
+ }
+ }
+ foreach (var quadtreeData in dirtyItems)
+ {
+ quadtreeData.MarkClean();
+ }
+ return objectCount;
+ }
+
+ /// <summary>
+ /// Inserts the data into the tree.
+ /// </summary>
+ /// <param name="data">Data being inserted.</param>
+ public void Insert(QuadtreeData data)
+ {
+ var actorBounds = data.Bounds;
+
+ // Object doesn't fit into this node.
+ if (!NodeBounds.Intersects(actorBounds))
+ {
+ return;
+ }
+
+ if (IsLeaf && Contents.Count >= MaxObjectsPerNode)
+ {
+ Split();
+ }
+
+ if (IsLeaf)
+ {
+ AddToLeaf(data);
+ }
+ else
+ {
+ foreach (var child in Children)
+ {
+ child.Insert(data);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes data from the Quadtree
+ /// </summary>
+ /// <param name="data">The data to be removed.</param>
+ public void Remove(QuadtreeData data)
+ {
+ if (IsLeaf)
+ {
+ data.RemoveParent(this);
+ Contents.Remove(data);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(QuadTree)}");
+ }
+ }
+
+ /// <summary>
+ /// Removes unnecessary leaf nodes and simplifies the quad tree.
+ /// </summary>
+ public void Shake()
+ {
+ if (IsLeaf)
+ {
+ return;
+ }
+
+ List<QuadtreeData> dirtyItems = new List<QuadtreeData>();
+
+ var numObjects = NumTargets();
+ if (numObjects == 0)
+ {
+ Children.Clear();
+ }
+ else if (numObjects < MaxObjectsPerNode)
+ {
+ var process = new Queue<QuadTree>();
+ process.Enqueue(this);
+ while (process.Count > 0)
+ {
+ var processing = process.Dequeue();
+ if (!processing.IsLeaf)
+ {
+ foreach (var subTree in processing.Children)
+ {
+ process.Enqueue(subTree);
+ }
+ }
+ else
+ {
+ foreach (var data in processing.Contents)
+ {
+ if (data.Dirty == false)
+ {
+ AddToLeaf(data);
+ data.MarkDirty();
+ dirtyItems.Add(data);
+ }
+ }
+ }
+ }
+ Children.Clear();
+ }
+
+ foreach (var quadtreeData in dirtyItems)
+ {
+ quadtreeData.MarkClean();
+ }
+ }
+
+ private void AddToLeaf(QuadtreeData data)
+ {
+ data.AddParent(this);
+ Contents.Add(data);
+ }
+
+ /// <summary>
+ /// Splits a quadtree into quadrants.
+ /// </summary>
+ public void Split()
+ {
+ if (CurrentDepth + 1 >= MaxDepth) return;
+
+ var min = NodeBounds.TopLeft;
+ var max = NodeBounds.BottomRight;
+ var center = NodeBounds.Center;
+
+ RectangleF[] childAreas =
+ {
+ RectangleF.CreateFrom(min, center),
+ RectangleF.CreateFrom(new Point2(center.X, min.Y), new Point2(max.X, center.Y)),
+ RectangleF.CreateFrom(center, max),
+ RectangleF.CreateFrom(new Point2(min.X, center.Y), new Point2(center.X, max.Y))
+ };
+
+ for (var i = 0; i < childAreas.Length; ++i)
+ {
+ var node = new QuadTree(childAreas[i]);
+ Children.Add(node);
+ Children[i].CurrentDepth = CurrentDepth + 1;
+ }
+
+ foreach (QuadtreeData contentQuadtree in Contents)
+ {
+ foreach (QuadTree childQuadtree in Children)
+ {
+ childQuadtree.Insert(contentQuadtree);
+ }
+ }
+ Clear();
+ }
+
+ /// <summary>
+ /// Clear current node and all children
+ /// </summary>
+ public void ClearAll()
+ {
+ foreach (QuadTree childQuadtree in Children)
+ childQuadtree.ClearAll();
+ Clear();
+ }
+
+ private void Clear()
+ {
+ foreach (QuadtreeData quadtreeData in Contents)
+ {
+ quadtreeData.RemoveParent(this);
+ }
+ Contents.Clear();
+ }
+
+ /// <summary>
+ /// Queries the quadtree for targets that intersect with the given area.
+ /// </summary>
+ /// <param name="area">The area to query for overlapping targets</param>
+ /// <returns>A unique list of targets intersected by area.</returns>
+ public List<QuadtreeData> Query(ref RectangleF area)
+ {
+ var recursiveResult = new List<QuadtreeData>();
+ QueryWithoutReset(ref area, recursiveResult);
+ foreach (var quadtreeData in recursiveResult)
+ {
+ quadtreeData.MarkClean();
+ }
+ return recursiveResult;
+ }
+
+ private void QueryWithoutReset(ref RectangleF area, List<QuadtreeData> recursiveResult)
+ {
+ if (!NodeBounds.Intersects(area))
+ return;
+
+ if (IsLeaf)
+ {
+ foreach (QuadtreeData quadtreeData in Contents)
+ {
+ if (quadtreeData.Dirty == false && quadtreeData.Bounds.Intersects(area))
+ {
+ recursiveResult.Add(quadtreeData);
+ quadtreeData.MarkDirty();
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0, size = Children.Count; i < size; i++)
+ {
+ Children[i].QueryWithoutReset(ref area, recursiveResult);
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs
new file mode 100644
index 0000000..db6da3e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MonoGame.Extended.Collisions.QuadTree;
+
+/// <summary>
+/// Data structure for the quad tree.
+/// Holds the entity and collision data for it.
+/// </summary>
+public class QuadtreeData
+{
+ private readonly ICollisionActor _target;
+ private readonly HashSet<QuadTree> _parents = new();
+
+ /// <summary>
+ /// Initialize a new instance of QuadTreeData.
+ /// </summary>
+ /// <param name="target"></param>
+ public QuadtreeData(ICollisionActor target)
+ {
+ _target = target;
+ Bounds = _target.Bounds.BoundingRectangle;
+ }
+
+ /// <summary>
+ /// Remove a parent node.
+ /// </summary>
+ /// <param name="parent"></param>
+ public void RemoveParent(QuadTree parent)
+ {
+ _parents.Remove(parent);
+ }
+
+ /// <summary>
+ /// Add a parent node.
+ /// </summary>
+ /// <param name="parent"></param>
+ public void AddParent(QuadTree parent)
+ {
+ _parents.Add(parent);
+ Bounds = _target.Bounds.BoundingRectangle;
+ }
+
+ /// <summary>
+ /// Remove all parent nodes from this node.
+ /// </summary>
+ public void RemoveFromAllParents()
+ {
+ foreach (var parent in _parents.ToList())
+ {
+ parent.Remove(this);
+ }
+
+ _parents.Clear();
+ }
+
+ /// <summary>
+ /// Gets the bounding box for collision detection.
+ /// </summary>
+ public RectangleF Bounds { get; set; }
+
+ /// <summary>
+ /// Gets the collision actor target.
+ /// </summary>
+ public ICollisionActor Target => _target;
+
+ /// <summary>
+ /// Gets or sets whether Target has had its collision handled this
+ /// iteration.
+ /// </summary>
+ public bool Dirty { get; private set; }
+
+ /// <summary>
+ /// Mark node as dirty.
+ /// </summary>
+ public void MarkDirty()
+ {
+ Dirty = true;
+ }
+
+ /// <summary>
+ /// Mark node as clean, i.e. not dirty.
+ /// </summary>
+ public void MarkClean()
+ {
+ Dirty = false;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs
new file mode 100644
index 0000000..3e9625f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MonoGame.Extended.Collisions.QuadTree;
+
+public class QuadTreeSpace: ISpaceAlgorithm
+{
+ private readonly QuadTree _collisionTree;
+ private readonly List<ICollisionActor> _actors = new();
+ private readonly Dictionary<ICollisionActor, QuadtreeData> _targetDataDictionary = new();
+
+ public QuadTreeSpace(RectangleF boundary)
+ {
+ _collisionTree = new QuadTree(boundary);
+ }
+
+ /// <summary>
+ /// Inserts the target into the collision tree.
+ /// The target will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="target">Target to insert.</param>
+ public void Insert(ICollisionActor target)
+ {
+ if (!_targetDataDictionary.ContainsKey(target))
+ {
+ var data = new QuadtreeData(target);
+ _targetDataDictionary.Add(target, data);
+ _collisionTree.Insert(data);
+ _actors.Add(target);
+ }
+ }
+
+ /// <summary>
+ /// Removes the target from the collision tree.
+ /// </summary>
+ /// <param name="target">Target to remove.</param>
+ public bool Remove(ICollisionActor target)
+ {
+ if (_targetDataDictionary.ContainsKey(target))
+ {
+ var data = _targetDataDictionary[target];
+ data.RemoveFromAllParents();
+ _targetDataDictionary.Remove(target);
+ _collisionTree.Shake();
+ _actors.Remove(target);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Restructure a inner collection, if layer is dynamic, because actors can change own position
+ /// </summary>
+ public void Reset()
+ {
+ _collisionTree.ClearAll();
+ foreach (var value in _targetDataDictionary.Values)
+ {
+ _collisionTree.Insert(value);
+ }
+ _collisionTree.Shake();
+ }
+
+ /// <summary>
+ /// foreach support
+ /// </summary>
+ /// <returns></returns>
+ public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator();
+
+ /// <inheritdoc cref="QuadTree.Query"/>
+ public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle)
+ {
+ return _collisionTree.Query(ref boundsBoundingRectangle).Select(x => x.Target);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs
new file mode 100644
index 0000000..2b0920e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Collisions;
+
+public class SpatialHash: ISpaceAlgorithm
+{
+ private readonly Dictionary<int, List<ICollisionActor>> _dictionary = new();
+ private readonly List<ICollisionActor> _actors = new();
+
+ private readonly Size2 _size;
+
+ public SpatialHash(Size2 size)
+ {
+ _size = size;
+ }
+
+ public void Insert(ICollisionActor actor)
+ {
+ InsertToHash(actor);
+ _actors.Add(actor);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void InsertToHash(ICollisionActor actor)
+ {
+ var rect = actor.Bounds.BoundingRectangle;
+ for (var x = rect.Left; x < rect.Right; x+=_size.Width)
+ for (var y = rect.Top; y < rect.Bottom; y+=_size.Height)
+ AddToCell(x, y, actor);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddToCell(float x, float y, ICollisionActor actor)
+ {
+ var index = GetIndex(x, y);
+ if (_dictionary.TryGetValue(index, out var actors))
+ actors.Add(actor);
+ else
+ _dictionary[index] = new() { actor };
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetIndex(float x, float y)
+ {
+ return (int)(x / _size.Width) << 16 + (int)(y / _size.Height);
+ }
+
+ public bool Remove(ICollisionActor actor)
+ {
+ foreach (var actors in _dictionary.Values)
+ actors.Remove(actor);
+ return _actors.Remove(actor);
+ }
+
+ public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle)
+ {
+ var results = new HashSet<ICollisionActor>();
+ var bounds = boundsBoundingRectangle.BoundingRectangle;
+
+ for (var x = boundsBoundingRectangle.Left; x < boundsBoundingRectangle.Right; x+=_size.Width)
+ for (var y = boundsBoundingRectangle.Top; y < boundsBoundingRectangle.Bottom; y+=_size.Height)
+ if (_dictionary.TryGetValue(GetIndex(x, y), out var actors))
+ foreach (var actor in actors)
+ if (bounds.Intersects(actor.Bounds))
+ results.Add(actor);
+ return results;
+ }
+
+ public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator();
+
+ public void Reset()
+ {
+ _dictionary.Clear();
+ foreach (var actor in _actors)
+ InsertToHash(actor);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config
new file mode 100644
index 0000000..28c0144
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MonoGame.Framework.Portable" version="3.6.0.1625" targetFramework="portable45-net45+win8+wpa81" />
+</packages> \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs
new file mode 100644
index 0000000..14638d4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorAnimation
+ {
+ public string Name { get; set; }
+ public int FramesPerSecond { get; set; }
+ public List<string> Frames { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+
+ public AstridAnimatorAnimation(string name, int framesPerSecond)
+ {
+ Name = name;
+ FramesPerSecond = framesPerSecond;
+ Frames = new List<string>();
+ IsLooping = true;
+ IsReversed = false;
+ IsPingPong = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs
new file mode 100644
index 0000000..765f2f2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorFile
+ {
+ public string TextureAtlas { get; set; }
+ public List<AstridAnimatorAnimation> Animations { get; set; }
+
+ public AstridAnimatorFile()
+ {
+ Animations = new List<AstridAnimatorAnimation>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs
new file mode 100644
index 0000000..855a0d3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentImporter(".aa", DefaultProcessor = "AstridAnimatorProcessor",
+ DisplayName = "Astrid Animator Importer - MonoGame.Extended")]
+ public class AstridAnimatorImporter : ContentImporter<ContentImporterResult<AstridAnimatorFile>>
+ {
+ public override ContentImporterResult<AstridAnimatorFile> Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ var data = JsonSerializer.Deserialize<AstridAnimatorFile>(json);
+ return new ContentImporterResult<AstridAnimatorFile>(filename, data);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs
new file mode 100644
index 0000000..e22d65b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs
@@ -0,0 +1,24 @@
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentProcessor(DisplayName = "Astrid Animator Processor - MonoGame.Extended")]
+ public class AstridAnimatorProcessor :
+ ContentProcessor<ContentImporterResult<AstridAnimatorFile>, AstridAnimatorProcessorResult>
+ {
+ public override AstridAnimatorProcessorResult Process(ContentImporterResult<AstridAnimatorFile> input,
+ ContentProcessorContext context)
+ {
+ var data = input.Data;
+ var directory = Path.GetDirectoryName(input.FilePath);
+ var frames = data.Animations
+ .SelectMany(i => i.Frames)
+ .OrderBy(f => f)
+ .Distinct();
+
+ return new AstridAnimatorProcessorResult(directory, data, frames);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs
new file mode 100644
index 0000000..de92ec3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorProcessorResult
+ {
+ public string TextureAtlasAssetName { get; private set; }
+ public string Directory { get; private set; }
+ public AstridAnimatorFile Data { get; private set; }
+ public List<string> Frames { get; private set; }
+
+ public AstridAnimatorProcessorResult(string directory, AstridAnimatorFile data, IEnumerable<string> frames)
+ {
+ Directory = directory;
+ Data = data;
+ Frames = new List<string>(frames);
+ TextureAtlasAssetName = Path.GetFileNameWithoutExtension(data.TextureAtlas);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs
new file mode 100644
index 0000000..6f56f3f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs
@@ -0,0 +1,40 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentTypeWriter]
+ public class AstridAnimatorWriter : ContentTypeWriter<AstridAnimatorProcessorResult>
+ {
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.Animations.SpriteSheets.SpriteSheetAnimationFactoryReader, MonoGame.Extended.Animations";
+ }
+
+ protected override void Write(ContentWriter writer, AstridAnimatorProcessorResult input)
+ {
+ var data = input.Data;
+
+ writer.Write(input.TextureAtlasAssetName);
+ writer.Write(input.Frames.Count);
+
+ foreach (var frame in input.Frames)
+ writer.Write(frame);
+
+ writer.Write(data.Animations.Count);
+
+ foreach (var animation in data.Animations)
+ {
+ writer.Write(animation.Name);
+ writer.Write(animation.FramesPerSecond);
+ writer.Write(animation.IsLooping);
+ writer.Write(animation.IsReversed);
+ writer.Write(animation.IsPingPong);
+ writer.Write(animation.Frames.Count);
+
+ foreach (var frame in animation.Frames)
+ writer.Write(input.Frames.IndexOf(frame));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs
new file mode 100644
index 0000000..c012da4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs
@@ -0,0 +1,41 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontChar
+ {
+ [XmlAttribute("id")]
+ public int Id { get; set; }
+
+ [XmlAttribute("x")]
+ public int X { get; set; }
+
+ [XmlAttribute("y")]
+ public int Y { get; set; }
+
+ [XmlAttribute("width")]
+ public int Width { get; set; }
+
+ [XmlAttribute("height")]
+ public int Height { get; set; }
+
+ [XmlAttribute("xoffset")]
+ public int XOffset { get; set; }
+
+ [XmlAttribute("yoffset")]
+ public int YOffset { get; set; }
+
+ [XmlAttribute("xadvance")]
+ public int XAdvance { get; set; }
+
+ [XmlAttribute("page")]
+ public int Page { get; set; }
+
+ [XmlAttribute("chnl")]
+ public int Channel { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs
new file mode 100644
index 0000000..6247fcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs
@@ -0,0 +1,41 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontCommon
+ {
+ [XmlAttribute("lineHeight")]
+ public int LineHeight { get; set; }
+
+ [XmlAttribute("base")]
+ public int Base { get; set; }
+
+ [XmlAttribute("scaleW")]
+ public int ScaleW { get; set; }
+
+ [XmlAttribute("scaleH")]
+ public int ScaleH { get; set; }
+
+ [XmlAttribute("pages")]
+ public int Pages { get; set; }
+
+ [XmlAttribute("packed")]
+ public int Packed { get; set; }
+
+ [XmlAttribute("alphaChnl")]
+ public int AlphaChannel { get; set; }
+
+ [XmlAttribute("redChnl")]
+ public int RedChannel { get; set; }
+
+ [XmlAttribute("greenChnl")]
+ public int GreenChannel { get; set; }
+
+ [XmlAttribute("blueChnl")]
+ public int BlueChannel { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs
new file mode 100644
index 0000000..db77270
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ [XmlRoot("font")]
+ public class BitmapFontFile
+ {
+ [XmlElement("info")]
+ public BitmapFontInfo Info { get; set; }
+
+ [XmlElement("common")]
+ public BitmapFontCommon Common { get; set; }
+
+ [XmlArray("pages")]
+ [XmlArrayItem("page")]
+ public List<BitmapFontPage> Pages { get; set; }
+
+ [XmlArray("chars")]
+ [XmlArrayItem("char")]
+ public List<BitmapFontChar> Chars { get; set; }
+
+ [XmlArray("kernings")]
+ [XmlArrayItem("kerning")]
+ public List<BitmapFontKerning> Kernings { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs
new file mode 100644
index 0000000..ea4c528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentImporter(".fnt", DefaultProcessor = "BitmapFontProcessor",
+ DisplayName = "BMFont Importer - MonoGame.Extended")]
+ public class BitmapFontImporter : ContentImporter<BitmapFontFile>
+ {
+ public override BitmapFontFile Import(string filename, ContentImporterContext context)
+ {
+ context.Logger.LogMessage("Importing XML file: {0}", filename);
+
+ using (var streamReader = new StreamReader(filename))
+ {
+ var deserializer = new XmlSerializer(typeof(BitmapFontFile));
+ return (BitmapFontFile)deserializer.Deserialize(streamReader);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs
new file mode 100644
index 0000000..1f50cf8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs
@@ -0,0 +1,47 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontInfo
+ {
+ [XmlAttribute("face")]
+ public string Face { get; set; }
+
+ [XmlAttribute("size")]
+ public int Size { get; set; }
+
+ [XmlAttribute("bold")]
+ public int Bold { get; set; }
+
+ [XmlAttribute("italic")]
+ public int Italic { get; set; }
+
+ [XmlAttribute("charset")]
+ public string CharSet { get; set; }
+
+ [XmlAttribute("unicode")]
+ public int Unicode { get; set; }
+
+ [XmlAttribute("stretchH")]
+ public int StretchHeight { get; set; }
+
+ [XmlAttribute("smooth")]
+ public int Smooth { get; set; }
+
+ [XmlAttribute("aa")]
+ public int SuperSampling { get; set; }
+
+ [XmlAttribute("padding")]
+ public string Padding { get; set; }
+
+ [XmlAttribute("spacing")]
+ public string Spacing { get; set; }
+
+ [XmlAttribute("outline")]
+ public int OutLine { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs
new file mode 100644
index 0000000..77caf13
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs
@@ -0,0 +1,20 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontKerning
+ {
+ [XmlAttribute("first")]
+ public int First { get; set; }
+
+ [XmlAttribute("second")]
+ public int Second { get; set; }
+
+ [XmlAttribute("amount")]
+ public int Amount { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs
new file mode 100644
index 0000000..3841ff5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontPage
+ {
+ [XmlAttribute("id")]
+ public int Id { get; set; }
+
+ [XmlAttribute("file")]
+ public string File { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs
new file mode 100644
index 0000000..a859eb5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentProcessor(DisplayName = "BMFont Processor - MonoGame.Extended")]
+ public class BitmapFontProcessor : ContentProcessor<BitmapFontFile, BitmapFontProcessorResult>
+ {
+ public override BitmapFontProcessorResult Process(BitmapFontFile bitmapFontFile, ContentProcessorContext context)
+ {
+ try
+ {
+ context.Logger.LogMessage("Processing BMFont");
+ var result = new BitmapFontProcessorResult(bitmapFontFile);
+
+ foreach (var fontPage in bitmapFontFile.Pages)
+ {
+ var assetName = Path.GetFileNameWithoutExtension(fontPage.File);
+ context.Logger.LogMessage("Expected texture asset: {0}", assetName);
+ result.TextureAssets.Add(assetName);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs
new file mode 100644
index 0000000..5841acc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ public class BitmapFontProcessorResult
+ {
+ public List<string> TextureAssets { get; private set; }
+ public BitmapFontFile FontFile { get; private set; }
+
+ public BitmapFontProcessorResult(BitmapFontFile fontFile)
+ {
+ FontFile = fontFile;
+ TextureAssets = new List<string>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs
new file mode 100644
index 0000000..343c40c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentTypeWriter]
+ public class BitmapFontWriter : ContentTypeWriter<BitmapFontProcessorResult>
+ {
+ protected override void Write(ContentWriter writer, BitmapFontProcessorResult result)
+ {
+ writer.Write(result.TextureAssets.Count);
+
+ foreach (var textureAsset in result.TextureAssets)
+ writer.Write(textureAsset);
+
+ var fontFile = result.FontFile;
+ writer.Write(fontFile.Common.LineHeight);
+ writer.Write(fontFile.Chars.Count);
+
+ foreach (var c in fontFile.Chars)
+ {
+ writer.Write(c.Id);
+ writer.Write(c.Page);
+ writer.Write(c.X);
+ writer.Write(c.Y);
+ writer.Write(c.Width);
+ writer.Write(c.Height);
+ writer.Write(c.XOffset);
+ writer.Write(c.YOffset);
+ writer.Write(c.XAdvance);
+ }
+
+ writer.Write(fontFile.Kernings.Count);
+ foreach(var k in fontFile.Kernings)
+ {
+ writer.Write(k.First);
+ writer.Write(k.Second);
+ writer.Write(k.Amount);
+ }
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.BitmapFonts.BitmapFont, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.BitmapFonts.BitmapFontReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs
new file mode 100644
index 0000000..7f63815
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs
@@ -0,0 +1,15 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline;
+
+public static class ContentImporterContextExtensions
+{
+ public static string AddDependencyWithLogging(this ContentImporterContext context, string filePath, string source)
+ {
+ source = Path.Combine(Path.GetDirectoryName(filePath), source);
+ ContentLogger.Log($"Adding dependency '{source}'");
+ context.AddDependency(source);
+ return source;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs
new file mode 100644
index 0000000..e302ca7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public class ContentImporterResult<T>
+ {
+ public ContentImporterResult(string filePath, T data)
+ {
+ FilePath = filePath;
+ Data = data;
+ }
+
+ public string FilePath { get; }
+ public T Data { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs
new file mode 100644
index 0000000..e69d48e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public interface IExternalReferenceRepository
+ {
+ ExternalReference<TInput> GetExternalReference<TInput>(string source);
+ }
+
+ public class ContentItem<T> : ContentItem, IExternalReferenceRepository
+ {
+ public ContentItem(T data)
+ {
+ Data = data;
+ }
+
+ public T Data { get; }
+
+ private readonly Dictionary<string, ContentItem> _externalReferences = new Dictionary<string, ContentItem>();
+
+ public void BuildExternalReference<TInput>(ContentProcessorContext context, string source, OpaqueDataDictionary parameters = null)
+ {
+ var sourceAsset = new ExternalReference<TInput>(source);
+ var externalReference = context.BuildAsset<TInput, TInput>(sourceAsset, "", parameters, "", "");
+ _externalReferences.Add(source, externalReference);
+ }
+
+ public ExternalReference<TInput> GetExternalReference<TInput>(string source)
+ {
+ if (source is not null && _externalReferences.TryGetValue(source, out var contentItem))
+ return contentItem as ExternalReference<TInput>;
+
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs
new file mode 100644
index 0000000..83848a1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public class ContentLogger
+ {
+ public static ContentBuildLogger Logger { get; set; }
+
+ public static void Log(string message)
+ {
+ Logger?.LogMessage(message);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs
new file mode 100644
index 0000000..b8cf9d2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public static class ContentWriterExtensions
+ {
+ public static void Write(this ContentWriter contentWriter, Color value)
+ {
+ contentWriter.Write(value.R);
+ contentWriter.Write(value.G);
+ contentWriter.Write(value.B);
+ contentWriter.Write(value.A);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Matrix value)
+ {
+ contentWriter.Write(value.M11);
+ contentWriter.Write(value.M12);
+ contentWriter.Write(value.M13);
+ contentWriter.Write(value.M14);
+ contentWriter.Write(value.M21);
+ contentWriter.Write(value.M22);
+ contentWriter.Write(value.M23);
+ contentWriter.Write(value.M24);
+ contentWriter.Write(value.M31);
+ contentWriter.Write(value.M32);
+ contentWriter.Write(value.M33);
+ contentWriter.Write(value.M34);
+ contentWriter.Write(value.M41);
+ contentWriter.Write(value.M42);
+ contentWriter.Write(value.M43);
+ contentWriter.Write(value.M44);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Quaternion value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ contentWriter.Write(value.W);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector2 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector3 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector4 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ contentWriter.Write(value.W);
+ }
+
+ public static void Write(this ContentWriter contentWriter, BoundingSphere value)
+ {
+ contentWriter.Write(value.Center);
+ contentWriter.Write(value.Radius);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Rectangle value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Width);
+ contentWriter.Write(value.Height);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs
new file mode 100644
index 0000000..437d7eb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs
@@ -0,0 +1,15 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentImporter(".json", DefaultProcessor = nameof(JsonContentProcessor), DisplayName = "JSON Importer - MonoGame.Extended")]
+ public class JsonContentImporter : ContentImporter<ContentImporterResult<string>>
+ {
+ public override ContentImporterResult<string> Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ return new ContentImporterResult<string>(filename, json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs
new file mode 100644
index 0000000..6be4ac3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs
@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentProcessor(DisplayName = "JSON Processor - MonoGame.Extended")]
+ public class JsonContentProcessor : ContentProcessor<ContentImporterResult<string>, JsonContentProcessorResult>
+ {
+ [DefaultValue(typeof(Type), "System.Object")]
+ public string ContentType { get; set; }
+
+ public override JsonContentProcessorResult Process(ContentImporterResult<string> input, ContentProcessorContext context)
+ {
+ try
+ {
+ var output = new JsonContentProcessorResult
+ {
+ ContentType = ContentType,
+ Json = input.Data
+ };
+ return output;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs
new file mode 100644
index 0000000..eaef99e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ public class JsonContentProcessorResult
+ {
+ public string ContentType { get; set; }
+ public string Json { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs
new file mode 100644
index 0000000..6efa696
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentTypeWriter]
+ public class JsonContentTypeWriter : ContentTypeWriter<JsonContentProcessorResult>
+ {
+ private string _runtimeType;
+
+ protected override void Write(ContentWriter writer, JsonContentProcessorResult result)
+ {
+ _runtimeType = result.ContentType;
+ writer.Write(result.Json);
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
new file mode 100644
index 0000000..70b81e8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <Content Include="$(ArtifactsPath)/bin/MonoGame.Extended.Content.Pipeline/release/*.dll" Pack="True" PackagePath="tools" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <Description>Content Pipeline importers and processors to make MonoGame more awesome.</Description>
+ <PackageTags>monogame content importer processor reader tiled texturepacker bmfont animations</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Autofac" Version="5.2.0" />
+
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Tiled\MonoGame.Extended.Tiled.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs
new file mode 100644
index 0000000..b85afb5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs
@@ -0,0 +1,14 @@
+using System;
+using System.IO;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public static class PathExtensions
+ {
+ public static string GetApplicationFullPath(params string[] pathParts)
+ {
+ var path = Path.Combine(pathParts);
+ return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs
new file mode 100644
index 0000000..3c4607d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs
@@ -0,0 +1,10 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.Json;
+
+namespace MonoGame.Extended.Content.Pipeline.SpriteFactory
+{
+ [ContentImporter(".sf", DefaultProcessor = nameof(SpriteFactoryContentProcessor), DisplayName = "Sprite Factory Importer - MonoGame.Extended")]
+ public class SpriteFactoryContentImporter : JsonContentImporter
+ {
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs
new file mode 100644
index 0000000..4920f33
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.Json;
+
+namespace MonoGame.Extended.Content.Pipeline.SpriteFactory
+{
+ [ContentProcessor(DisplayName = "Sprite Factory Processor - MonoGame.Extended")]
+ public class SpriteFactoryContentProcessor : JsonContentProcessor
+ {
+ public SpriteFactoryContentProcessor()
+ {
+ ContentType = "MonoGame.Extended MonoGame.Extended.Animations.SpriteFactory.SpriteFactoryFileReader, MonoGame.Extended.Animations";
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs
new file mode 100644
index 0000000..a80e654
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.TextureAtlases;
+
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentImporter(".json", DefaultProcessor = "TexturePackerProcessor", DisplayName = "TexturePacker JSON Importer - MonoGame.Extended")]
+ public class TexturePackerJsonImporter : ContentImporter<TexturePackerFile>
+ {
+ public override TexturePackerFile Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ return JsonSerializer.Deserialize<TexturePackerFile>(json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs
new file mode 100644
index 0000000..1f14ee7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentProcessor(DisplayName = "TexturePacker Processor - MonoGame.Extended")]
+ public class TexturePackerProcessor : ContentProcessor<TexturePackerFile, TexturePackerProcessorResult>
+ {
+ public override TexturePackerProcessorResult Process(TexturePackerFile input, ContentProcessorContext context)
+ {
+ try
+ {
+ var output = new TexturePackerProcessorResult {Data = input};
+ return output;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs
new file mode 100644
index 0000000..a996259
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs
@@ -0,0 +1,9 @@
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ public class TexturePackerProcessorResult
+ {
+ public TexturePackerFile Data { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs
new file mode 100644
index 0000000..3fd15ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentTypeWriter]
+ public class TexturePackerWriter : ContentTypeWriter<TexturePackerProcessorResult>
+ {
+ protected override void Write(ContentWriter writer, TexturePackerProcessorResult result)
+ {
+ var data = result.Data;
+ var metadata = data.Metadata;
+
+ var assetName = Path.GetFileNameWithoutExtension(metadata.Image);
+ Debug.Assert(assetName != null, "assetName != null");
+
+ writer.Write(assetName);
+ writer.Write(data.Regions.Count);
+
+ foreach (var region in data.Regions)
+ {
+ var regionName = Path.ChangeExtension(region.Filename, null);
+ Debug.Assert(regionName != null, "regionName != null");
+
+ writer.Write(regionName);
+ writer.Write(region.Frame.X);
+ writer.Write(region.Frame.Y);
+ writer.Write(region.Frame.Width);
+ writer.Write(region.Frame.Height);
+ }
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.TextureAtlases.TextureAtlas, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.TextureAtlases.TextureAtlasReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
new file mode 100644
index 0000000..28ded17
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class ContentWriterExtensions
+ {
+ // ReSharper disable once SuggestBaseTypeForParameter
+ public static void WriteTiledMapProperties(this ContentWriter writer, IReadOnlyCollection<TiledMapPropertyContent> value)
+ {
+ if (value == null)
+ {
+ writer.Write(0);
+ return;
+ }
+ writer.Write(value.Count);
+ foreach (var property in value)
+ {
+ writer.Write(property.Name);
+ writer.Write(property.Value ?? string.Empty);
+ WriteTiledMapProperties(writer, property.Properties);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
new file mode 100644
index 0000000..b376252
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled;
+
+public class TiledContentItem<T>: ContentItem<T>
+{
+ public TiledContentItem(T data) : base(data)
+ {
+ }
+
+ public void BuildExternalReference<T>(ContentProcessorContext context, TiledMapImageContent image)
+ {
+ var parameters = new OpaqueDataDictionary
+ {
+ { "ColorKeyColor", image.TransparentColor },
+ { "ColorKeyEnabled", true }
+ };
+ BuildExternalReference<Texture2DContent>(context, image.Source, parameters);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
new file mode 100644
index 0000000..b0b7b5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapContentItem : TiledContentItem<TiledMapContent>
+ {
+ public TiledMapContentItem(TiledMapContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
new file mode 100644
index 0000000..70e2bee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tmx", DefaultProcessor = "TiledMapProcessor", DisplayName = "Tiled Map Importer - MonoGame.Extended")]
+ public class TiledMapImporter : ContentImporter<TiledMapContentItem>
+ {
+ public override TiledMapContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var map = DeserializeTiledMapContent(filePath, context);
+
+ if (map.Width > ushort.MaxValue || map.Height > ushort.MaxValue)
+ throw new InvalidContentException($"The map '{filePath} is much too large. The maximum supported width and height for a Tiled map is {ushort.MaxValue}.");
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapContentItem(map);
+
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private static TiledMapContent DeserializeTiledMapContent(string mapFilePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(mapFilePath))
+ {
+ var mapSerializer = new XmlSerializer(typeof(TiledMapContent));
+ var map = (TiledMapContent)mapSerializer.Deserialize(reader);
+
+ map.FilePath = mapFilePath;
+
+ for (var i = 0; i < map.Tilesets.Count; i++)
+ {
+ var tileset = map.Tilesets[i];
+
+ string getTilesetSource(string source)
+ => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(mapFilePath), source));
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ tileset.Source = getTilesetSource(tileset.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Source}");
+ // We depend on the tileset. If the tileset changes, the map also needs to rebuild.
+ context.AddDependency(tileset.Source);
+ }
+ else
+ {
+ tileset.Image.Source = getTilesetSource(tileset.Image.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Image.Source}");
+ context.AddDependency(tileset.Image.Source);
+ }
+ }
+
+ ImportLayers(context, map.Layers, Path.GetDirectoryName(mapFilePath));
+
+ map.Name = mapFilePath;
+ return map;
+ }
+ }
+
+ private static void ImportLayers(ContentImporterContext context, List<TiledMapLayerContent> layers, string path)
+ {
+ for (var i = 0; i < layers.Count; i++)
+ {
+ if (layers[i] is TiledMapImageLayerContent imageLayer)
+ {
+ imageLayer.Image.Source = Path.Combine(path, imageLayer.Image.Source);
+ ContentLogger.Log($"Adding dependency for '{imageLayer.Image.Source}'");
+
+ // Tell the pipeline that we depend on this image and need to rebuild the map if the image changes.
+ // (Maybe the image is a different size)
+ context.AddDependency(imageLayer.Image.Source);
+ }
+ if (layers[i] is TiledMapObjectLayerContent objectLayer)
+ foreach (var obj in objectLayer.Objects)
+ if (!String.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ obj.TemplateSource = Path.Combine(path, obj.TemplateSource);
+ ContentLogger.Log($"Adding dependency for '{obj.TemplateSource}'");
+ // Tell the pipeline that we depend on this template and need to rebuild the map if the template changes.
+ // (Templates are loaded into objects on process, so all objects which depend on the template file
+ // need the change to the template)
+ context.AddDependency(obj.TemplateSource);
+ }
+ if (layers[i] is TiledMapGroupLayerContent groupLayer)
+ // Yay recursion!
+ ImportLayers(context, groupLayer.Layers, path);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
new file mode 100644
index 0000000..eeddcaa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
@@ -0,0 +1,54 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tx", DefaultProcessor = "TiledMapObjectTemplateProcessor", DisplayName = "Tiled Map Object Template Importer - MonoGame.Extended")]
+ public class TiledMapObjectTemplateImporter : ContentImporter<TiledMapObjectTemplateContent>
+ {
+ public override TiledMapObjectTemplateContent Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var template = DeserializeTileMapObjectTemplateContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return template;
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ return null;
+ }
+ }
+
+ private static TiledMapObjectTemplateContent DeserializeTileMapObjectTemplateContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var templateSerializer = new XmlSerializer(typeof(TiledMapObjectTemplateContent));
+ var template = (TiledMapObjectTemplateContent)templateSerializer.Deserialize(reader);
+
+ if (!string.IsNullOrWhiteSpace(template.Tileset?.Source))
+ {
+ template.Tileset.Source = Path.Combine(Path.GetDirectoryName(filePath), template.Tileset.Source);
+ ContentLogger.Log($"Adding dependency '{template.Tileset.Source}'");
+ // We depend on this tileset.
+ context.AddDependency(template.Tileset.Source);
+ }
+
+ return template;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
new file mode 100644
index 0000000..9668a72
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+using MonoGame.Framework.Utilities.Deflate;
+using CompressionMode = System.IO.Compression.CompressionMode;
+using GZipStream = System.IO.Compression.GZipStream;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class TiledMapContentHelper
+ {
+ public static void Process(TiledMapObjectContent obj, ContentProcessorContext context)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ var externalReference = new ExternalReference<TiledMapObjectLayerContent>(obj.TemplateSource);
+ var template = context.BuildAndLoadAsset<TiledMapObjectLayerContent, TiledMapObjectTemplateContent>(externalReference, "");
+
+ // Nothing says a template can't reference another template.
+ // Yay recusion!
+ Process(template.Object, context);
+
+ if (!obj._globalIdentifier.HasValue && template.Object._globalIdentifier.HasValue)
+ obj.GlobalIdentifier = template.Object.GlobalIdentifier;
+
+ if (!obj._height.HasValue && template.Object._height.HasValue)
+ obj.Height = template.Object.Height;
+
+ if (!obj._identifier.HasValue && template.Object._identifier.HasValue)
+ obj.Identifier = template.Object.Identifier;
+
+ if (!obj._rotation.HasValue && template.Object._rotation.HasValue)
+ obj.Rotation = template.Object.Rotation;
+
+ if (!obj._visible.HasValue && template.Object._visible.HasValue)
+ obj.Visible = template.Object.Visible;
+
+ if (!obj._width.HasValue && template.Object._width.HasValue)
+ obj.Width = template.Object.Width;
+
+ if (!obj._x.HasValue && template.Object._x.HasValue)
+ obj.X = template.Object.X;
+
+ if (!obj._y.HasValue && template.Object._y.HasValue)
+ obj.Y = template.Object.Y;
+
+ if (obj.Ellipse == null && template.Object.Ellipse != null)
+ obj.Ellipse = template.Object.Ellipse;
+
+ if (string.IsNullOrWhiteSpace(obj.Name) && !string.IsNullOrWhiteSpace(template.Object.Name))
+ obj.Name = template.Object.Name;
+
+ if (obj.Polygon == null && template.Object.Polygon != null)
+ obj.Polygon = template.Object.Polygon;
+
+ if (obj.Polyline == null && template.Object.Polyline != null)
+ obj.Polyline = template.Object.Polyline;
+
+ foreach (var tProperty in template.Object.Properties)
+ {
+ if (!obj.Properties.Exists(p => p.Name == tProperty.Name))
+ obj.Properties.Add(tProperty);
+ }
+
+ if (string.IsNullOrWhiteSpace(obj.Type) && !string.IsNullOrWhiteSpace(template.Object.Type))
+ obj.Type = template.Object.Type;
+
+ if (string.IsNullOrWhiteSpace(obj.Class) && !string.IsNullOrWhiteSpace(template.Object.Class))
+ obj.Class = template.Object.Class;
+ }
+ }
+ }
+
+
+ [ContentProcessor(DisplayName = "Tiled Map Processor - MonoGame.Extended")]
+ public class TiledMapProcessor : ContentProcessor<TiledMapContentItem, TiledMapContentItem>
+ {
+ public override TiledMapContentItem Process(TiledMapContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ ContentLogger.Logger = context.Logger;
+ var map = contentItem.Data;
+
+ if (map.Orientation == TiledMapOrientationContent.Hexagonal || map.Orientation == TiledMapOrientationContent.Staggered)
+ throw new NotSupportedException($"{map.Orientation} Tiled Maps are currently not implemented!");
+
+ foreach (var tileset in map.Tilesets)
+ {
+ if (string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ // Load the Texture2DContent for the tileset as it will be saved into the map content file.
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+ }
+ else
+ {
+ // Link to the tileset for the content loader to load at runtime.
+ //var externalReference = new ExternalReference<TiledMapTilesetContent>(tileset.Source);
+ //tileset.Content = context.BuildAsset<TiledMapTilesetContent, TiledMapTilesetContent>(externalReference, "");
+ contentItem.BuildExternalReference<TiledMapTilesetContent>(context, tileset.Source);
+ }
+ }
+
+ ProcessLayers(contentItem, map, context, map.Layers);
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw;
+ }
+ }
+
+ private static void ProcessLayers(TiledMapContentItem contentItem, TiledMapContent map, ContentProcessorContext context, List<TiledMapLayerContent> layers)
+ {
+ foreach (var layer in layers)
+ {
+ switch (layer)
+ {
+ case TiledMapImageLayerContent imageLayer:
+ ContentLogger.Log($"Processing image layer '{imageLayer.Name}'");
+ contentItem.BuildExternalReference<Texture2DContent>(context, imageLayer.Image);
+ ContentLogger.Log($"Processed image layer '{imageLayer.Name}'");
+ break;
+
+ case TiledMapTileLayerContent tileLayer when tileLayer.Data.Chunks.Count > 0:
+ throw new NotSupportedException($"{map.FilePath} contains data chunks. These are currently not supported.");
+
+ case TiledMapTileLayerContent tileLayer:
+ var data = tileLayer.Data;
+ var encodingType = data.Encoding ?? "xml";
+ var compressionType = data.Compression ?? "xml";
+
+ ContentLogger.Log($"Processing tile layer '{tileLayer.Name}': Encoding: '{encodingType}', Compression: '{compressionType}'");
+ var tileData = DecodeTileLayerData(encodingType, tileLayer);
+ var tiles = CreateTiles(map.RenderOrder, map.Width, map.Height, tileData);
+ tileLayer.Tiles = tiles;
+ ContentLogger.Log($"Processed tile layer '{tileLayer}': {tiles.Length} tiles");
+ break;
+
+ case TiledMapObjectLayerContent objectLayer:
+ ContentLogger.Log($"Processing object layer '{objectLayer.Name}'");
+
+ foreach (var obj in objectLayer.Objects)
+ TiledMapContentHelper.Process(obj, context);
+
+ ContentLogger.Log($"Processed object layer '{objectLayer.Name}'");
+ break;
+
+ case TiledMapGroupLayerContent groupLayer:
+ ProcessLayers(contentItem, map, context, groupLayer.Layers);
+ break;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeTileLayerData(string encodingType, TiledMapTileLayerContent tileLayer)
+ {
+ List<TiledMapTileContent> tiles;
+
+ switch (encodingType)
+ {
+ case "xml":
+ tiles = tileLayer.Data.Tiles;
+ break;
+ case "csv":
+ tiles = DecodeCommaSeperatedValuesData(tileLayer.Data);
+ break;
+ case "base64":
+ tiles = DecodeBase64Data(tileLayer.Data, tileLayer.Width, tileLayer.Height);
+ break;
+ default:
+ throw new NotSupportedException($"The tile layer encoding '{encodingType}' is not supported.");
+ }
+
+ return tiles;
+ }
+
+ private static TiledMapTile[] CreateTiles(TiledMapTileDrawOrderContent renderOrder, int mapWidth, int mapHeight, List<TiledMapTileContent> tileData)
+ {
+ TiledMapTile[] tiles;
+
+ switch (renderOrder)
+ {
+ case TiledMapTileDrawOrderContent.LeftDown:
+ tiles = CreateTilesInLeftDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.LeftUp:
+ tiles = CreateTilesInLeftUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightDown:
+ tiles = CreateTilesInRightDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightUp:
+ tiles = CreateTilesInRightUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ default:
+ throw new NotSupportedException($"{renderOrder} is not supported.");
+ }
+
+ return tiles.ToArray();
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = 0; x < mapWidth; x++)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeBase64Data(TiledMapTileLayerDataContent data, int width, int height)
+ {
+ var tileList = new List<TiledMapTileContent>();
+ var encodedData = data.Value.Trim();
+ var decodedData = Convert.FromBase64String(encodedData);
+
+ using (var stream = OpenStream(decodedData, data.Compression))
+ {
+ using (var reader = new BinaryReader(stream))
+ {
+ data.Tiles = new List<TiledMapTileContent>();
+
+ for (var y = 0; y < width; y++)
+ {
+ for (var x = 0; x < height; x++)
+ {
+ var gid = reader.ReadUInt32();
+ tileList.Add(new TiledMapTileContent
+ {
+ GlobalIdentifier = gid
+ });
+ }
+ }
+ }
+ }
+
+ return tileList;
+ }
+
+ private static List<TiledMapTileContent> DecodeCommaSeperatedValuesData(TiledMapTileLayerDataContent data)
+ {
+ return data.Value
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(uint.Parse)
+ .Select(x => new TiledMapTileContent { GlobalIdentifier = x })
+ .ToList();
+ }
+
+ private static Stream OpenStream(byte[] decodedData, string compressionMode)
+ {
+ var memoryStream = new MemoryStream(decodedData, false);
+
+ return compressionMode switch
+ {
+ "gzip" => new GZipStream(memoryStream, CompressionMode.Decompress),
+ "zlib" => new ZlibStream(memoryStream, Framework.Utilities.Deflate.CompressionMode.Decompress),
+ _ => memoryStream
+ };
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
new file mode 100644
index 0000000..d4b2221
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapTilesetContentItem : TiledContentItem<TiledMapTilesetContent>
+ {
+ public TiledMapTilesetContentItem(TiledMapTilesetContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
new file mode 100644
index 0000000..c848bde
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
@@ -0,0 +1,60 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tsx", DefaultProcessor = "TiledMapTilesetProcessor", DisplayName = "Tiled Map Tileset Importer - MonoGame.Extended")]
+ public class TiledMapTilesetImporter : ContentImporter<TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var tileset = DeserializeTiledMapTilesetContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapTilesetContentItem(tileset);
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private TiledMapTilesetContent DeserializeTiledMapTilesetContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var tilesetSerializer = new XmlSerializer(typeof(TiledMapTilesetContent));
+ var tileset = (TiledMapTilesetContent)tilesetSerializer.Deserialize(reader);
+
+ if (tileset.Image is not null)
+ tileset.Image.Source = context.AddDependencyWithLogging(filePath, tileset.Image.Source);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ obj.TemplateSource = context.AddDependencyWithLogging(filePath, obj.TemplateSource);
+ }
+ if (tile.Image is not null)
+ tile.Image.Source = context.AddDependencyWithLogging(filePath, tile.Image.Source);
+ }
+
+ return tileset;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
new file mode 100644
index 0000000..5682602
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
@@ -0,0 +1,44 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using System;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentProcessor(DisplayName = "Tiled Map Tileset Processor - MonoGame.Extended")]
+ public class TiledMapTilesetProcessor : ContentProcessor<TiledMapTilesetContentItem, TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Process(TiledMapTilesetContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ var tileset = contentItem.Data;
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Processing tileset '{tileset.Name}'");
+
+ // Build the Texture2D asset and load it as it will be saved as part of this tileset file.
+ if (tileset.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ TiledMapContentHelper.Process(obj, context);
+ }
+ if (tile.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tile.Image);
+ }
+
+ ContentLogger.Log($"Processed tileset '{tileset.Name}'");
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
new file mode 100644
index 0000000..48690ad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
@@ -0,0 +1,145 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapTilesetWriter : ContentTypeWriter<TiledMapTilesetContentItem>
+ {
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTilesetReader, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTileset, MonoGame.Extended.Tiled";
+
+ protected override void Write(ContentWriter writer, TiledMapTilesetContentItem contentItem)
+ {
+ try
+ {
+ WriteTileset(writer, contentItem.Data, contentItem);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ public static void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset, IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tileset.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(tileset.Class ?? tileset.Type ?? string.Empty);
+ writer.Write(tileset.TileWidth);
+ writer.Write(tileset.TileHeight);
+ writer.Write(tileset.TileCount);
+ writer.Write(tileset.Spacing);
+ writer.Write(tileset.Margin);
+ writer.Write(tileset.Columns);
+ writer.Write(tileset.Tiles.Count);
+
+ foreach (var tilesetTile in tileset.Tiles)
+ WriteTilesetTile(writer, tilesetTile, externalReferenceRepository);
+
+ writer.WriteTiledMapProperties(tileset.Properties);
+ }
+
+ private static void WriteTilesetTile(ContentWriter writer, TiledMapTilesetTileContent tilesetTile,
+ IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tilesetTile.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+
+ writer.Write(tilesetTile.LocalIdentifier);
+ writer.Write(tilesetTile.Type);
+ writer.Write(tilesetTile.Frames.Count);
+ writer.Write(tilesetTile.Objects.Count);
+
+ foreach (var @object in tilesetTile.Objects)
+ WriteObject(writer, @object);
+
+ foreach (var frame in tilesetTile.Frames)
+ {
+ writer.Write(frame.TileIdentifier);
+ writer.Write(frame.Duration);
+ }
+
+ writer.WriteTiledMapProperties(tilesetTile.Properties);
+ }
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
new file mode 100644
index 0000000..126debb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapWriter : ContentTypeWriter<TiledMapContentItem>
+ {
+ private TiledMapContentItem _contentItem;
+
+ protected override void Write(ContentWriter writer, TiledMapContentItem contentItem)
+ {
+ _contentItem = contentItem;
+
+ var map = contentItem.Data;
+
+ try
+ {
+ WriteMetaData(writer, map);
+ WriteTilesets(writer, map.Tilesets);
+ WriteLayers(writer, map.Layers);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ private static void WriteMetaData(ContentWriter writer, TiledMapContent map)
+ {
+ writer.Write(map.Class ?? map.Type ?? string.Empty);
+ writer.Write(map.Width);
+ writer.Write(map.Height);
+ writer.Write(map.TileWidth);
+ writer.Write(map.TileHeight);
+ writer.Write(ColorHelper.FromHex(map.BackgroundColor));
+ writer.Write((byte)map.RenderOrder);
+ writer.Write((byte)map.Orientation);
+ writer.WriteTiledMapProperties(map.Properties);
+ }
+
+ private void WriteTilesets(ContentWriter writer, IReadOnlyCollection<TiledMapTilesetContent> tilesets)
+ {
+ writer.Write(tilesets.Count);
+
+ foreach (var tileset in tilesets)
+ WriteTileset(writer, tileset);
+ }
+
+ private void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset)
+ {
+ writer.Write(tileset.FirstGlobalIdentifier);
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ writer.Write(true);
+ writer.WriteExternalReference(_contentItem.GetExternalReference<TiledMapTilesetContent>(tileset.Source));
+ }
+ else
+ {
+ writer.Write(false);
+ TiledMapTilesetWriter.WriteTileset(writer, tileset, _contentItem);
+ }
+ }
+
+ private void WriteLayers(ContentWriter writer, IReadOnlyCollection<TiledMapLayerContent> layers)
+ {
+ writer.Write(layers.Count);
+
+ foreach (var layer in layers)
+ WriteLayer(writer, layer);
+ }
+
+ private void WriteLayer(ContentWriter writer, TiledMapLayerContent layer)
+ {
+ writer.Write((byte)layer.LayerType);
+
+ writer.Write(layer.Name ?? string.Empty);
+ writer.Write(layer.Class ?? layer.Type ?? string.Empty);
+ writer.Write(layer.Visible);
+ writer.Write(layer.Opacity);
+ writer.Write(layer.OffsetX);
+ writer.Write(layer.OffsetY);
+ writer.Write(layer.ParallaxX);
+ writer.Write(layer.ParallaxY);
+
+ writer.WriteTiledMapProperties(layer.Properties);
+
+ switch (layer.LayerType)
+ {
+ case TiledMapLayerType.ImageLayer:
+ WriteImageLayer(writer, (TiledMapImageLayerContent)layer);
+ break;
+ case TiledMapLayerType.TileLayer:
+ WriteTileLayer(writer, (TiledMapTileLayerContent)layer);
+ break;
+ case TiledMapLayerType.ObjectLayer:
+ WriteObjectLayer(writer, (TiledMapObjectLayerContent)layer);
+ break;
+ case TiledMapLayerType.GroupLayer:
+ WriteLayers(writer, ((TiledMapGroupLayerContent)layer).Layers);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(layer.LayerType));
+ }
+ }
+
+ private void WriteImageLayer(ContentWriter writer, TiledMapImageLayerContent imageLayer)
+ {
+ var externalReference = _contentItem.GetExternalReference<Texture2DContent>(imageLayer.Image.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(new Vector2(imageLayer.X, imageLayer.Y));
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WriteTileLayer(ContentWriter writer, TiledMapTileLayerContent tileLayer)
+ {
+ writer.Write(tileLayer.Width);
+ writer.Write(tileLayer.Height);
+
+ writer.Write(tileLayer.Tiles.Length);
+
+ foreach (var tile in tileLayer.Tiles)
+ {
+ writer.Write(tile.GlobalTileIdentifierWithFlags);
+ writer.Write(tile.X);
+ writer.Write(tile.Y);
+ }
+ }
+
+ private static void WriteObjectLayer(ContentWriter writer, TiledMapObjectLayerContent layer)
+ {
+ writer.Write(ColorHelper.FromHex(layer.Color));
+ writer.Write((byte)layer.DrawOrder);
+
+ writer.Write(layer.Objects.Count);
+
+ foreach (var @object in layer.Objects)
+ WriteObject(writer, @object);
+ }
+
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMap, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapReader, MonoGame.Extended.Tiled";
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt
new file mode 100644
index 0000000..54f1cda
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt
@@ -0,0 +1,15 @@
+# MonoGame.Extended.Content.Pipeline
+
+This NuGet package is intended to be used with the MonoGame Content Pipeline tool. You'll need to
+make some changes to your Content Pipline file (usually Content.mgcb) for everything to work
+properly.
+
+Add a reference to the `MonoGame.Extended.Content.Pipeline.dll` now installed in your `packages` folder
+to the [MonoGame Content Pipeline tool](http://www.monogame.net/documentation/?page=Pipeline).
+
+You can do this either with the Reference Editor in the GUI or by manually adding a reference line to
+your `Content.mgcb` file using a text editor.
+
+**Remember**: the versions need to match exactly for everything to work.
+
+For more information, take a look at the [installation guide](http://craftworkgames.github.io/MonoGame.Extended/installation/).
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs
new file mode 100644
index 0000000..e6fb606
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Specialized;
+
+namespace MonoGame.Extended.Entities
+{
+ public class Aspect
+ {
+ internal Aspect()
+ {
+ AllSet = new BitVector32();
+ ExclusionSet = new BitVector32();
+ OneSet = new BitVector32();
+ }
+
+ public BitVector32 AllSet;
+ public BitVector32 ExclusionSet;
+ public BitVector32 OneSet;
+
+ public static AspectBuilder All(params Type[] types)
+ {
+ return new AspectBuilder().All(types);
+ }
+
+ public static AspectBuilder One(params Type[] types)
+ {
+ return new AspectBuilder().One(types);
+ }
+
+ public static AspectBuilder Exclude(params Type[] types)
+ {
+ return new AspectBuilder().Exclude(types);
+ }
+
+ public bool IsInterested(BitVector32 componentBits)
+ {
+ if (AllSet.Data != 0 && (componentBits.Data & AllSet.Data) != AllSet.Data)
+ return false;
+
+ if (ExclusionSet.Data != 0 && (componentBits.Data & ExclusionSet.Data) != 0)
+ return false;
+
+ if (OneSet.Data != 0 && (componentBits.Data & OneSet.Data) == 0)
+ return false;
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs
new file mode 100644
index 0000000..d749795
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Specialized;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public class AspectBuilder
+ {
+ public AspectBuilder()
+ {
+ AllTypes = new Bag<Type>();
+ ExclusionTypes = new Bag<Type>();
+ OneTypes = new Bag<Type>();
+ }
+
+ public Bag<Type> AllTypes { get; }
+ public Bag<Type> ExclusionTypes { get; }
+ public Bag<Type> OneTypes { get; }
+
+ public AspectBuilder All(params Type[] types)
+ {
+ foreach (var type in types)
+ AllTypes.Add(type);
+
+ return this;
+ }
+
+ public AspectBuilder One(params Type[] types)
+ {
+ foreach (var type in types)
+ OneTypes.Add(type);
+
+ return this;
+ }
+
+ public AspectBuilder Exclude(params Type[] types)
+ {
+ foreach (var type in types)
+ ExclusionTypes.Add(type);
+
+ return this;
+ }
+
+ public Aspect Build(ComponentManager componentManager)
+ {
+ var aspect = new Aspect();
+ Associate(componentManager, AllTypes, ref aspect.AllSet);
+ Associate(componentManager, OneTypes, ref aspect.OneSet);
+ Associate(componentManager, ExclusionTypes, ref aspect.ExclusionSet);
+ return aspect;
+ }
+
+ // ReSharper disable once ParameterTypeCanBeEnumerable.Local
+ private static void Associate(ComponentManager componentManager, Bag<Type> types, ref BitVector32 bits)
+ {
+ foreach (var type in types)
+ {
+ var id = componentManager.GetComponentTypeId(type);
+ bits[1 << id] = true;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs
new file mode 100644
index 0000000..2ca0737
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public static class BitArrayExtensions
+ {
+ public static bool IsEmpty(this BitArray bitArray)
+ {
+ for (var i = 0; i < bitArray.Length; i++)
+ {
+ if (bitArray[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool ContainsAll(this BitArray bitArray, BitArray other)
+ {
+ var otherBitsLength = other.Length;
+ var bitsLength = bitArray.Length;
+
+ for (var i = bitsLength; i < otherBitsLength; i++)
+ {
+ if (other[i])
+ return false;
+ }
+
+ var s = Math.Min(bitsLength, otherBitsLength);
+
+ for (var i = 0; s > i; i++)
+ {
+ if ((bitArray[i] & other[i]) != other[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool Intersects(this BitArray bitArray, BitArray other)
+ {
+ var s = Math.Min(bitArray.Length, other.Length);
+
+ for (var i = 0; s > i; i++)
+ {
+ if (bitArray[i] & other[i])
+ return true;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs
new file mode 100644
index 0000000..a59c87c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public interface IComponentMapperService
+ {
+ ComponentMapper<T> GetMapper<T>() where T : class;
+ }
+
+ public class ComponentManager : UpdateSystem, IComponentMapperService
+ {
+ public ComponentManager()
+ {
+ _componentMappers = new Bag<ComponentMapper>();
+ _componentTypes = new Dictionary<Type, int>();
+ }
+
+ private readonly Bag<ComponentMapper> _componentMappers;
+ private readonly Dictionary<Type, int> _componentTypes;
+
+ public Action<int> ComponentsChanged;
+
+ private ComponentMapper<T> CreateMapperForType<T>(int componentTypeId)
+ where T : class
+ {
+ // TODO: We can probably do better than this without a huge performance penalty by creating our own bit vector that grows after the first 32 bits.
+ if (componentTypeId >= 32)
+ throw new InvalidOperationException("Component type limit exceeded. We currently only allow 32 component types for performance reasons.");
+
+ var mapper = new ComponentMapper<T>(componentTypeId, ComponentsChanged);
+ _componentMappers[componentTypeId] = mapper;
+ return mapper;
+ }
+
+ public ComponentMapper GetMapper(int componentTypeId)
+ {
+ return _componentMappers[componentTypeId];
+ }
+
+ public ComponentMapper<T> GetMapper<T>()
+ where T : class
+ {
+ var componentTypeId = GetComponentTypeId(typeof(T));
+
+ if (_componentMappers[componentTypeId] != null)
+ return _componentMappers[componentTypeId] as ComponentMapper<T>;
+
+ return CreateMapperForType<T>(componentTypeId);
+ }
+
+ public int GetComponentTypeId(Type type)
+ {
+ if (_componentTypes.TryGetValue(type, out var id))
+ return id;
+
+ id = _componentTypes.Count;
+ _componentTypes.Add(type, id);
+ return id;
+ }
+
+ public BitVector32 CreateComponentBits(int entityId)
+ {
+ var componentBits = new BitVector32();
+ var mask = BitVector32.CreateMask();
+
+ for (var componentId = 0; componentId < _componentMappers.Count; componentId++)
+ {
+ componentBits[mask] = _componentMappers[componentId]?.Has(entityId) ?? false;
+ mask = BitVector32.CreateMask(mask);
+ }
+
+ return componentBits;
+ }
+
+ public void Destroy(int entityId)
+ {
+ foreach (var componentMapper in _componentMappers)
+ componentMapper?.Delete(entityId);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs
new file mode 100644
index 0000000..35b196c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs
@@ -0,0 +1,69 @@
+using System;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public abstract class ComponentMapper
+ {
+ protected ComponentMapper(int id, Type componentType)
+ {
+ Id = id;
+ ComponentType = componentType;
+ }
+
+ public int Id { get; }
+ public Type ComponentType { get; }
+ public abstract bool Has(int entityId);
+ public abstract void Delete(int entityId);
+ }
+
+ public class ComponentMapper<T> : ComponentMapper
+ where T : class
+ {
+ public event Action<int> OnPut;
+ public event Action<int> OnDelete;
+
+ private readonly Action<int> _onCompositionChanged;
+
+ public ComponentMapper(int id, Action<int> onCompositionChanged)
+ : base(id, typeof(T))
+ {
+ _onCompositionChanged = onCompositionChanged;
+ Components = new Bag<T>();
+ }
+
+ public Bag<T> Components { get; }
+
+ public void Put(int entityId, T component)
+ {
+ Components[entityId] = component;
+ _onCompositionChanged(entityId);
+ OnPut?.Invoke(entityId);
+ }
+
+ public T Get(Entity entity)
+ {
+ return Get(entity.Id);
+ }
+
+ public T Get(int entityId)
+ {
+ return Components[entityId];
+ }
+
+ public override bool Has(int entityId)
+ {
+ if (entityId >= Components.Count)
+ return false;
+
+ return Components[entityId] != null;
+ }
+
+ public override void Delete(int entityId)
+ {
+ Components[entityId] = null;
+ _onCompositionChanged(entityId);
+ OnDelete?.Invoke(entityId);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs
new file mode 100644
index 0000000..ebef996
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace MonoGame.Extended.Entities
+{
+ //public class ComponentType : IEquatable<ComponentType>
+ //{
+ // public ComponentType(Type type, int id)
+ // {
+ // Type = type;
+ // Id = id;
+ // }
+
+ // public Type Type { get; }
+ // public int Id { get; }
+
+ // public bool Equals(ComponentType other)
+ // {
+ // if (ReferenceEquals(null, other)) return false;
+ // if (ReferenceEquals(this, other)) return true;
+ // return Id == other.Id;
+ // }
+
+ // public override bool Equals(object obj)
+ // {
+ // if (ReferenceEquals(null, obj)) return false;
+ // if (ReferenceEquals(this, obj)) return true;
+ // if (obj.GetType() != GetType()) return false;
+ // return Equals((ComponentType) obj);
+ // }
+
+ // public override int GetHashCode()
+ // {
+ // return Id;
+ // }
+
+ // public static bool operator ==(ComponentType left, ComponentType right)
+ // {
+ // return Equals(left, right);
+ // }
+
+ // public static bool operator !=(ComponentType left, ComponentType right)
+ // {
+ // return !Equals(left, right);
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs
new file mode 100644
index 0000000..a56ccff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Specialized;
+
+namespace MonoGame.Extended.Entities
+{
+ public class Entity : IEquatable<Entity>
+ {
+ private readonly EntityManager _entityManager;
+ private readonly ComponentManager _componentManager;
+
+ internal Entity(int id, EntityManager entityManager, ComponentManager componentManager)
+ {
+ Id = id;
+
+ _entityManager = entityManager;
+ _componentManager = componentManager;
+ }
+
+ public int Id { get; }
+
+ public BitVector32 ComponentBits => _entityManager.GetComponentBits(Id);
+
+ public void Attach<T>(T component)
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ mapper.Put(Id, component);
+ }
+
+ public void Detach<T>()
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ mapper.Delete(Id);
+ }
+
+ public T Get<T>()
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ return mapper.Get(Id);
+ }
+
+
+ public bool Has<T>()
+ where T : class
+ {
+ return _componentManager.GetMapper<T>().Has(Id);
+ }
+
+ public void Destroy()
+ {
+ _entityManager.Destroy(Id);
+ }
+
+ public bool Equals(Entity other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Id == other.Id;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((Entity) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ // ReSharper disable once NonReadonlyMemberInGetHashCode
+ return Id;
+ }
+
+ public static bool operator ==(Entity left, Entity right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Entity left, Entity right)
+ {
+ return !Equals(left, right);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs
new file mode 100644
index 0000000..5e1d28a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class EntityManager : UpdateSystem
+ {
+ private const int _defaultBagSize = 128;
+
+ public EntityManager(ComponentManager componentManager)
+ {
+ _componentManager = componentManager;
+ _addedEntities = new Bag<int>(_defaultBagSize);
+ _removedEntities = new Bag<int>(_defaultBagSize);
+ _changedEntities = new Bag<int>(_defaultBagSize);
+ _entityToComponentBits = new Bag<BitVector32>(_defaultBagSize);
+ _componentManager.ComponentsChanged += OnComponentsChanged;
+
+ _entityBag = new Bag<Entity>(_defaultBagSize);
+ _entityPool = new Pool<Entity>(() => new Entity(_nextId++, this, _componentManager), _defaultBagSize);
+ }
+
+ private readonly ComponentManager _componentManager;
+ private int _nextId;
+
+ public int Capacity => _entityBag.Capacity;
+ public IEnumerable<int> Entities => _entityBag.Where(e => e != null).Select(e => e.Id);
+ public int ActiveCount { get; private set; }
+
+ private readonly Bag<Entity> _entityBag;
+ private readonly Pool<Entity> _entityPool;
+ private readonly Bag<int> _addedEntities;
+ private readonly Bag<int> _removedEntities;
+ private readonly Bag<int> _changedEntities;
+ private readonly Bag<BitVector32> _entityToComponentBits;
+
+ public event Action<int> EntityAdded;
+ public event Action<int> EntityRemoved;
+ public event Action<int> EntityChanged;
+
+ public Entity Create()
+ {
+ var entity = _entityPool.Obtain();
+ var id = entity.Id;
+ Debug.Assert(_entityBag[id] == null);
+ _entityBag[id] = entity;
+ _addedEntities.Add(id);
+ _entityToComponentBits[id] = new BitVector32(0);
+ return entity;
+ }
+
+ public void Destroy(int entityId)
+ {
+ if (!_removedEntities.Contains(entityId))
+ _removedEntities.Add(entityId);
+ }
+
+ public void Destroy(Entity entity)
+ {
+ Destroy(entity.Id);
+ }
+
+ public Entity Get(int entityId)
+ {
+ return _entityBag[entityId];
+ }
+
+ public BitVector32 GetComponentBits(int entityId)
+ {
+ return _entityToComponentBits[entityId];
+ }
+
+ private void OnComponentsChanged(int entityId)
+ {
+ _changedEntities.Add(entityId);
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ EntityChanged?.Invoke(entityId);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var entityId in _addedEntities)
+ {
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ ActiveCount++;
+ EntityAdded?.Invoke(entityId);
+ }
+
+ foreach (var entityId in _changedEntities)
+ {
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ EntityChanged?.Invoke(entityId);
+ }
+
+ foreach (var entityId in _removedEntities)
+ {
+ // we must notify subscribers before removing it from the pool
+ // otherwise an entity system could still be using the entity when the same id is obtained.
+ EntityRemoved?.Invoke(entityId);
+
+ var entity = _entityBag[entityId];
+ _entityBag[entityId] = null;
+ _componentManager.Destroy(entityId);
+ _entityToComponentBits[entityId] = default(BitVector32);
+ ActiveCount--;
+ _entityPool.Free(entity);
+ }
+
+ _addedEntities.Clear();
+ _removedEntities.Clear();
+ _changedEntities.Clear();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs
new file mode 100644
index 0000000..6766b0f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs
@@ -0,0 +1,61 @@
+using System;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ internal class EntitySubscription : IDisposable
+ {
+ private readonly Bag<int> _activeEntities;
+ private readonly EntityManager _entityManager;
+ private readonly Aspect _aspect;
+ private bool _rebuildActives;
+
+ internal EntitySubscription(EntityManager entityManager, Aspect aspect)
+ {
+ _entityManager = entityManager;
+ _aspect = aspect;
+ _activeEntities = new Bag<int>(entityManager.Capacity);
+ _rebuildActives = true;
+
+ _entityManager.EntityAdded += OnEntityAdded;
+ _entityManager.EntityRemoved += OnEntityRemoved;
+ _entityManager.EntityChanged += OnEntityChanged;
+ }
+
+ private void OnEntityAdded(int entityId)
+ {
+ if (_aspect.IsInterested(_entityManager.GetComponentBits(entityId)))
+ _activeEntities.Add(entityId);
+ }
+
+ private void OnEntityRemoved(int entityId) => _rebuildActives = true;
+ private void OnEntityChanged(int entityId) => _rebuildActives = true;
+
+ public void Dispose()
+ {
+ _entityManager.EntityAdded -= OnEntityAdded;
+ _entityManager.EntityRemoved -= OnEntityRemoved;
+ }
+
+ public Bag<int> ActiveEntities
+ {
+ get
+ {
+ if (_rebuildActives)
+ RebuildActives();
+
+ return _activeEntities;
+ }
+ }
+
+ private void RebuildActives()
+ {
+ _activeEntities.Clear();
+
+ foreach (var entity in _entityManager.Entities)
+ OnEntityAdded(entity);
+
+ _rebuildActives = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj
new file mode 100644
index 0000000..21ca693
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>An Entity Component System to make MonoGame more awesome.</Description>
+ <PackageTags>monogame ecs entity component system</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs
new file mode 100644
index 0000000..b9e63eb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface IDrawSystem : ISystem
+ {
+ void Draw(GameTime gameTime);
+ }
+
+ public abstract class DrawSystem : IDrawSystem
+ {
+ public virtual void Dispose() { }
+ public virtual void Initialize(World world) { }
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs
new file mode 100644
index 0000000..5a0d6b9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityDrawSystem : EntitySystem, IDrawSystem
+ {
+ protected EntityDrawSystem(AspectBuilder aspect)
+ : base(aspect)
+ {
+ }
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs
new file mode 100644
index 0000000..c4d9339
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityProcessingSystem : EntityUpdateSystem
+ {
+ protected EntityProcessingSystem(AspectBuilder aspectBuilder)
+ : base(aspectBuilder)
+ {
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ Begin();
+
+ foreach (var entityId in ActiveEntities)
+ Process(gameTime, entityId);
+
+ End();
+ }
+
+ public virtual void Begin() { }
+ public abstract void Process(GameTime gameTime, int entityId);
+ public virtual void End() { }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs
new file mode 100644
index 0000000..2c95107
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs
@@ -0,0 +1,51 @@
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntitySystem : ISystem
+ {
+ protected EntitySystem(AspectBuilder aspectBuilder)
+ {
+ _aspectBuilder = aspectBuilder;
+ }
+
+ public void Dispose()
+ {
+ if (_world != null)
+ {
+ _world.EntityManager.EntityAdded -= OnEntityAdded;
+ _world.EntityManager.EntityRemoved -= OnEntityRemoved;
+ }
+ }
+
+ private readonly AspectBuilder _aspectBuilder;
+ private EntitySubscription _subscription;
+
+ private World _world;
+
+ protected virtual void OnEntityChanged(int entityId) { }
+ protected virtual void OnEntityAdded(int entityId) { }
+ protected virtual void OnEntityRemoved(int entityId) { }
+
+ public Bag<int> ActiveEntities => _subscription.ActiveEntities;
+
+ public virtual void Initialize(World world)
+ {
+ _world = world;
+
+ var aspect = _aspectBuilder.Build(_world.ComponentManager);
+ _subscription = new EntitySubscription(_world.EntityManager, aspect);
+ _world.EntityManager.EntityAdded += OnEntityAdded;
+ _world.EntityManager.EntityRemoved += OnEntityRemoved;
+ _world.EntityManager.EntityChanged += OnEntityChanged;
+
+ Initialize(world.ComponentManager);
+ }
+
+ public abstract void Initialize(IComponentMapperService mapperService);
+
+ protected void DestroyEntity(int entityId) => _world.DestroyEntity(entityId);
+ protected Entity CreateEntity() => _world.CreateEntity();
+ protected Entity GetEntity(int entityId) => _world.GetEntity(entityId);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs
new file mode 100644
index 0000000..97a61d5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityUpdateSystem : EntitySystem, IUpdateSystem
+ {
+ protected EntityUpdateSystem(AspectBuilder aspectBuilder)
+ : base(aspectBuilder)
+ {
+ }
+
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs
new file mode 100644
index 0000000..d4511fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface ISystem : IDisposable
+ {
+ void Initialize(World world);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs
new file mode 100644
index 0000000..e85c964
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface IUpdateSystem : ISystem
+ {
+ void Update(GameTime gameTime);
+ }
+
+ public abstract class UpdateSystem : IUpdateSystem
+ {
+ public virtual void Dispose() { }
+ public virtual void Initialize(World world) { }
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs
new file mode 100644
index 0000000..022d4b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs
@@ -0,0 +1,84 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class World : SimpleDrawableGameComponent
+ {
+ private readonly Bag<IUpdateSystem> _updateSystems;
+ private readonly Bag<IDrawSystem> _drawSystems;
+
+ internal World()
+ {
+ _updateSystems = new Bag<IUpdateSystem>();
+ _drawSystems = new Bag<IDrawSystem>();
+
+ RegisterSystem(ComponentManager = new ComponentManager());
+ RegisterSystem(EntityManager = new EntityManager(ComponentManager));
+ }
+
+ public override void Dispose()
+ {
+ foreach (var updateSystem in _updateSystems)
+ updateSystem.Dispose();
+
+ foreach (var drawSystem in _drawSystems)
+ drawSystem.Dispose();
+
+ _updateSystems.Clear();
+ _drawSystems.Clear();
+
+ base.Dispose();
+ }
+
+ internal EntityManager EntityManager { get; }
+ internal ComponentManager ComponentManager { get; }
+
+ public int EntityCount => EntityManager.ActiveCount;
+
+ internal void RegisterSystem(ISystem system)
+ {
+ // ReSharper disable once ConvertIfStatementToSwitchStatement
+ if (system is IUpdateSystem updateSystem)
+ _updateSystems.Add(updateSystem);
+
+ if (system is IDrawSystem drawSystem)
+ _drawSystems.Add(drawSystem);
+
+ system.Initialize(this);
+ }
+
+ public Entity GetEntity(int entityId)
+ {
+ return EntityManager.Get(entityId);
+ }
+
+ public Entity CreateEntity()
+ {
+ return EntityManager.Create();
+ }
+
+ public void DestroyEntity(int entityId)
+ {
+ EntityManager.Destroy(entityId);
+ }
+
+ public void DestroyEntity(Entity entity)
+ {
+ EntityManager.Destroy(entity);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var system in _updateSystems)
+ system.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ foreach (var system in _drawSystems)
+ system.Draw(gameTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs
new file mode 100644
index 0000000..7f03323
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class WorldBuilder
+ {
+ private readonly List<ISystem> _systems = new List<ISystem>();
+
+ public WorldBuilder AddSystem(ISystem system)
+ {
+ _systems.Add(system);
+ return this;
+ }
+
+ public World Build()
+ {
+ var world = new World();
+
+ foreach (var system in _systems)
+ world.RegisterSystem(system);
+
+ return world;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs
new file mode 100644
index 0000000..3cb5704
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs
@@ -0,0 +1,387 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Minimizes draw calls to a <see cref="GraphicsDevice" /> by sorting them and attempting to merge them together
+ /// before submitting them.
+ /// </summary>
+ /// <typeparam name="TDrawCallInfo">The type of the information for a draw call.</typeparam>
+ /// <seealso cref="IDisposable" />
+ public abstract class Batcher<TDrawCallInfo> : IDisposable
+ where TDrawCallInfo : struct, IBatchDrawCallInfo<TDrawCallInfo>, IComparable<TDrawCallInfo>
+ {
+ internal const int DefaultBatchMaximumDrawCallsCount = 2048;
+ private BlendState _blendState;
+ private SamplerState _samplerState;
+ private DepthStencilState _depthStencilState;
+ private RasterizerState _rasterizerState;
+ private readonly Effect _defaultEffect;
+ private Effect _currentEffect;
+ private Matrix? _viewMatrix;
+ private Matrix? _projectionMatrix;
+
+ /// <summary>
+ /// The array of <see cref="TDrawCallInfo" /> structs currently enqueued.
+ /// </summary>
+ protected TDrawCallInfo[] DrawCalls;
+
+ /// <summary>
+ /// The number of <see cref="TDrawCallInfo" /> structs currently enqueued.
+ /// </summary>
+ protected int EnqueuedDrawCallCount;
+
+ /// <summary>
+ /// Gets the <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />.
+ /// </summary>
+ /// <value>
+ /// The <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />.
+ /// </value>
+ public GraphicsDevice GraphicsDevice { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether batching is currently in progress by being within a <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair block of code.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if batching has begun; otherwise, <c>false</c>.
+ /// </value>
+ public bool HasBegun { get; internal set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Batcher{TDrawCallInfo}" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="defaultEffect">The default effect.</param>
+ /// <param name="maximumDrawCallsCount">
+ /// The maximum number of <see cref="TDrawCallInfo" /> structs that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" />
+ /// is required. The default value is <code>2048</code>.
+ /// </param>
+ /// <exception cref="ArgumentNullException">
+ /// <paramref name="graphicsDevice" /> is
+ /// null.
+ /// </exception>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <paramref name="maximumDrawCallsCount" /> is less than or equal
+ /// <code>0</code>.
+ /// </exception>
+ protected Batcher(GraphicsDevice graphicsDevice, Effect defaultEffect,
+ int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount)
+ {
+ if (graphicsDevice == null)
+ throw new ArgumentNullException(nameof(graphicsDevice));
+
+ if (maximumDrawCallsCount <= 0)
+ throw new ArgumentOutOfRangeException(nameof(maximumDrawCallsCount));
+
+ GraphicsDevice = graphicsDevice;
+ _defaultEffect = defaultEffect;
+ DrawCalls = new TDrawCallInfo[maximumDrawCallsCount];
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="diposing">
+ /// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only
+ /// unmanaged resources.
+ /// </param>
+ protected virtual void Dispose(bool diposing)
+ {
+ if (!diposing)
+ return;
+ _defaultEffect.Dispose();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureHasBegun([CallerMemberName] string callerMemberName = null)
+ {
+ if (!HasBegun)
+ throw new InvalidOperationException(
+ $"The {nameof(Begin)} method must be called before the {callerMemberName} method can be called.");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureHasNotBegun([CallerMemberName] string callerMemberName = null)
+ {
+ if (HasBegun)
+ throw new InvalidOperationException(
+ $"The {nameof(End)} method must be called before the {callerMemberName} method can be called.");
+ }
+
+ /// <summary>
+ /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />,
+ /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view
+ /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />,
+ /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are
+ /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />,
+ /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively.
+ /// Passing
+ /// <code>null</code> for any of the previously mentioned parameters result in using their default object.
+ /// </para>
+ /// </remarks>
+ /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param>
+ /// <param name="samplerState">
+ /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="depthStencilState">
+ /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="rasterizerState">
+ /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param>
+ /// <param name="viewMatrix">
+ /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="projectionMatrix">
+ /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for
+ /// drawing, call <see cref="End" />.
+ /// </para>
+ /// </remarks>
+ public virtual void Begin(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, BlendState blendState = null, SamplerState samplerState = null,
+ DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, -1);
+ Begin(ref viewMatrix1, ref projectionMatrix1, blendState, samplerState, depthStencilState, rasterizerState, effect);
+ }
+
+ /// <summary>
+ /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />,
+ /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view
+ /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />,
+ /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are
+ /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />,
+ /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively.
+ /// Passing
+ /// <code>null</code> for any of the previously mentioned parameters result in using their default object.
+ /// </para>
+ /// </remarks>
+ /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param>
+ /// <param name="samplerState">
+ /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="depthStencilState">
+ /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="rasterizerState">
+ /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param>
+ /// <param name="viewMatrix">
+ /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="projectionMatrix">
+ /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for
+ /// drawing, call <see cref="End" />.
+ /// </para>
+ /// </remarks>
+ public virtual void Begin(ref Matrix viewMatrix, ref Matrix projectionMatrix, BlendState blendState = null, SamplerState samplerState = null,
+ DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null)
+ {
+ EnsureHasNotBegun();
+ HasBegun = true;
+
+ // Store the states to be applied on End()
+ // This ensures that two or more batchers will not affect each other
+ _blendState = blendState ?? BlendState.AlphaBlend;
+ _samplerState = samplerState ?? SamplerState.PointClamp;
+ _depthStencilState = depthStencilState ?? DepthStencilState.None;
+ _rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise;
+ _currentEffect = effect ?? _defaultEffect;
+ _projectionMatrix = projectionMatrix;
+ _viewMatrix = viewMatrix;
+ }
+
+ /// <summary>
+ /// Flushes the batched geometry to the <see cref="GraphicsDevice" /> and restores it's state to how it was before
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> was called.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="End" /> cannot be invoked until <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called after all enqueuing of draw calls.
+ /// </para>
+ /// </remarks>
+ public void End()
+ {
+ EnsureHasBegun();
+ Flush();
+ HasBegun = false;
+ }
+
+ /// <summary>
+ /// Sorts then submits the (sorted) enqueued draw calls to the <see cref="GraphicsDevice" /> for
+ /// rendering without ending the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.
+ /// </summary>
+ protected void Flush()
+ {
+ if (EnqueuedDrawCallCount == 0)
+ return;
+ SortDrawCallsAndBindBuffers();
+ ApplyStates();
+ SubmitDrawCalls();
+ RestoreStates();
+ }
+
+ /// <summary>
+ /// Sorts the enqueued draw calls and binds any used <see cref="VertexBuffer" /> or <see cref="IndexBuffer" /> to the <see cref="GraphicsDevice" />.
+ /// </summary>
+ protected abstract void SortDrawCallsAndBindBuffers();
+
+ private void ApplyStates()
+ {
+ var oldBlendState = GraphicsDevice.BlendState;
+ var oldSamplerState = GraphicsDevice.SamplerStates[0];
+ var oldDepthStencilState = GraphicsDevice.DepthStencilState;
+ var oldRasterizerState = GraphicsDevice.RasterizerState;
+
+ GraphicsDevice.BlendState = _blendState;
+
+ GraphicsDevice.SamplerStates[0] = _samplerState;
+ GraphicsDevice.DepthStencilState = _depthStencilState;
+ GraphicsDevice.RasterizerState = _rasterizerState;
+
+ _blendState = oldBlendState;
+ _samplerState = oldSamplerState;
+ _depthStencilState = oldDepthStencilState;
+ _rasterizerState = oldRasterizerState;
+
+ var viewMatrix = _viewMatrix ?? Matrix.Identity;
+ var projectionMatrix = _projectionMatrix ??
+ Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width,
+ GraphicsDevice.Viewport.Height, 0, 0f, -1f);
+
+ var matrixChainEffect = _currentEffect as IMatrixChainEffect;
+ if (matrixChainEffect != null)
+ {
+ matrixChainEffect.World = Matrix.Identity;
+ matrixChainEffect.SetView(ref viewMatrix);
+ matrixChainEffect.SetProjection(ref projectionMatrix);
+ }
+ else
+ {
+ var effectMatrices = _currentEffect as IEffectMatrices;
+ if (effectMatrices == null)
+ return;
+ effectMatrices.World = Matrix.Identity;
+ effectMatrices.View = viewMatrix;
+ effectMatrices.Projection = projectionMatrix;
+ }
+ }
+
+ private void RestoreStates()
+ {
+ GraphicsDevice.BlendState = _blendState;
+ GraphicsDevice.SamplerStates[0] = _samplerState;
+ GraphicsDevice.DepthStencilState = _depthStencilState;
+ GraphicsDevice.RasterizerState = _rasterizerState;
+ }
+
+ /// <summary>
+ /// Enqueues draw call information.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ /// <remarks>
+ /// <para>
+ /// If possible, the <paramref name="drawCall" /> is merged with the last enqueued draw call information instead of
+ /// being
+ /// enqueued.
+ /// </para>
+ /// <para>
+ /// If the enqueue buffer is full, a <see cref="Flush" /> is invoked and then afterwards
+ /// <paramref name="drawCall" /> is enqueued.
+ /// </para>
+ /// </remarks>
+ protected void Enqueue(ref TDrawCallInfo drawCall)
+ {
+ if (EnqueuedDrawCallCount > 0 && drawCall.TryMerge(ref DrawCalls[EnqueuedDrawCallCount - 1]))
+ return;
+ if (EnqueuedDrawCallCount >= DrawCalls.Length)
+ Flush();
+ DrawCalls[EnqueuedDrawCallCount++] = drawCall;
+ }
+
+ /* It might be better to have derived classes just implement the for loop instead of having this virtual method call...
+ * However, if the derived class is only going to override this method once and the code is short, which should both be
+ * true, then maybe we can get away with this virtual method call by having it inlined. So tell the JIT or AOT compiler
+ * we would like it be so. This does NOT guarantee the compiler will respect our wishes.
+ */
+
+ /// <summary>
+ /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="TDrawCallInfo"/>.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected abstract void InvokeDrawCall(ref TDrawCallInfo drawCall);
+
+ private void SubmitDrawCalls()
+ {
+ if (EnqueuedDrawCallCount == 0)
+ return;
+
+ for (var i = 0; i < EnqueuedDrawCallCount; i++)
+ {
+ DrawCalls[i].SetState(_currentEffect);
+
+ foreach (var pass in _currentEffect.CurrentTechnique.Passes)
+ {
+ pass.Apply();
+ InvokeDrawCall(ref DrawCalls[i]);
+ }
+ }
+
+ Array.Clear(DrawCalls, 0, EnqueuedDrawCallCount);
+ EnqueuedDrawCallCount = 0;
+ }
+ }
+}
+ \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs
new file mode 100644
index 0000000..e7ec533
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs
@@ -0,0 +1,450 @@
+using System;
+using System.Text;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Graphics.Effects;
+using MonoGame.Extended.Graphics.Geometry;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// A general purpose <see cref="Batcher{TDrawCallInfo}" /> for two-dimensional geometry that change
+ /// frequently between frames such as sprites and shapes.
+ /// </summary>
+ /// <seealso cref="IDisposable" />
+ /// <remarks>
+ /// <para>For drawing user interfaces, consider using <see cref="UIBatcher(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> instead because it supports scissor rectangles.</para>
+ /// </remarks>
+ public sealed class Batcher2D : Batcher<Batcher2D.DrawCallInfo>
+ {
+
+ internal const int DefaultMaximumVerticesCount = 8192;
+ internal const int DefaultMaximumIndicesCount = 12288;
+
+ private readonly VertexBuffer _vertexBuffer;
+ private readonly IndexBuffer _indexBuffer;
+ private readonly VertexPositionColorTexture[] _vertices;
+ private int _vertexCount;
+ private readonly ushort[] _indices;
+ private int _indexCount;
+ private readonly ushort[] _sortedIndices;
+ private readonly GeometryBuilder2D _geometryBuilder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Batcher2D" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="maximumVerticesCount">
+ /// The maximum number of vertices that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>8192</code>.
+ /// </param>
+ /// <param name="maximumIndicesCount">
+ /// The maximum number of indices that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>12288</code>.
+ /// </param>
+ /// <param name="maximumDrawCallsCount">
+ /// The maximum number of <see cref="DrawCallInfo" /> structs that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>2048</code>.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="graphicsDevice" />.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <paramref name="maximumDrawCallsCount" /> is less than or equal
+ /// <code>0</code>, or <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>, or,
+ /// <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>.
+ /// </exception>
+ public Batcher2D(GraphicsDevice graphicsDevice,
+ ushort maximumVerticesCount = DefaultMaximumVerticesCount,
+ ushort maximumIndicesCount = DefaultMaximumIndicesCount,
+ int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount)
+ : base(
+ graphicsDevice,
+ new DefaultEffect(graphicsDevice)
+ {
+ TextureEnabled = true,
+ VertexColorEnabled = true
+ }, maximumDrawCallsCount)
+
+ {
+ _vertices = new VertexPositionColorTexture[maximumVerticesCount];
+ _vertexBuffer = new DynamicVertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, maximumVerticesCount, BufferUsage.WriteOnly);
+
+ _indices = new ushort[maximumIndicesCount];
+ _sortedIndices = new ushort[maximumIndicesCount];
+ _indexBuffer = new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, maximumIndicesCount, BufferUsage.WriteOnly);
+ _geometryBuilder = new GeometryBuilder2D(4, 6);
+ }
+
+ protected override void SortDrawCallsAndBindBuffers()
+ {
+ // Upload the vertices to the GPU and then select that vertex stream for drawing
+ _vertexBuffer.SetData(_vertices, 0, _vertexCount);
+ GraphicsDevice.SetVertexBuffer(_vertexBuffer);
+
+ Array.Sort(DrawCalls, 0, EnqueuedDrawCallCount);
+ BuildSortedIndices();
+
+ // Upload the indices to the GPU and then select that index stream for drawing
+ _indexBuffer.SetData(_sortedIndices, 0, _indexCount);
+ GraphicsDevice.Indices = _indexBuffer;
+
+ _indexCount = 0;
+ _vertexCount = 0;
+ }
+
+ private void BuildSortedIndices()
+ {
+ var newDrawCallsCount = 0;
+ DrawCalls[0].StartIndex = 0;
+ var currentDrawCall = DrawCalls[0];
+ DrawCalls[newDrawCallsCount++] = DrawCalls[0];
+
+ var drawCallIndexCount = currentDrawCall.PrimitiveCount * 3;
+ Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, 0, drawCallIndexCount);
+ var sortedIndexCount = drawCallIndexCount;
+
+ // iterate through sorted draw calls checking if any can now be merged to reduce expensive draw calls to the graphics API
+ // this might need to be changed for next-gen graphics API (Vulkan, Metal, DirectX 12) where the draw calls are not so expensive
+ for (var i = 1; i < EnqueuedDrawCallCount; i++)
+ {
+ currentDrawCall = DrawCalls[i];
+ drawCallIndexCount = currentDrawCall.PrimitiveCount * 3;
+ Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, sortedIndexCount, drawCallIndexCount);
+ sortedIndexCount += drawCallIndexCount;
+
+ if (currentDrawCall.TryMerge(ref DrawCalls[newDrawCallsCount - 1]))
+ continue;
+
+ currentDrawCall.StartIndex = sortedIndexCount;
+ DrawCalls[newDrawCallsCount++] = currentDrawCall;
+ }
+
+ EnqueuedDrawCallCount = newDrawCallsCount;
+ }
+
+ /// <summary>
+ /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="DrawCallInfo"/>.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ protected override void InvokeDrawCall(ref DrawCallInfo drawCall)
+ {
+ GraphicsDevice.DrawIndexedPrimitives(drawCall.PrimitiveType, 0, drawCall.StartIndex, drawCall.PrimitiveCount);
+ }
+
+ /// <summary>
+ /// Draws a sprite using a specified <see cref="Texture" />, transform <see cref="Matrix2" />, source
+ /// <see cref="Rectangle" />, and an optional
+ /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="texture">The <see cref="Texture" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="sourceRectangle">
+ /// The texture region <see cref="Rectangle" /> of the <paramref name="texture" />. Use
+ /// <code>null</code> to use the entire <see cref="Texture2D" />.
+ /// </param>
+ /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception>
+ public void DrawSprite(Texture2D texture, ref Matrix2 transformMatrix, ref Rectangle sourceRectangle,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref sourceRectangle, color, flags, depth);
+ EnqueueBuiltGeometry(texture, depth);
+ }
+
+ /// <summary>
+ /// Draws a <see cref="Texture" /> using the specified transform <see cref="Matrix2" /> and an optional
+ /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="texture">The <see cref="Texture" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception>
+ public void DrawTexture(Texture2D texture, ref Matrix2 transformMatrix, Color? color = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ var rectangle = default(Rectangle);
+ _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref rectangle, color, flags, depth);
+ EnqueueBuiltGeometry(texture, depth);
+ }
+
+ private void EnqueueBuiltGeometry(Texture2D texture, float depth)
+ {
+ if ((_vertexCount + _geometryBuilder.VertexCount > _vertices.Length) ||
+ (_indexCount + _geometryBuilder.IndexCount > _indices.Length))
+ Flush();
+ var drawCall = new DrawCallInfo(texture, _geometryBuilder.PrimitiveType, _indexCount,
+ _geometryBuilder.PrimitivesCount, depth);
+ Array.Copy(_geometryBuilder.Vertices, 0, _vertices, _vertexCount, _geometryBuilder.VertexCount);
+ _vertexCount += _geometryBuilder.VertexCount;
+ Array.Copy(_geometryBuilder.Indices, 0, _indices, _indexCount, _geometryBuilder.IndexCount);
+ _indexCount += _geometryBuilder.IndexCount;
+ Enqueue(ref drawCall);
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="StringBuilder" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin
+ /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="StringBuilder" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, StringBuilder text, ref Matrix2 transformMatrix,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ EnsureHasBegun();
+
+ if (bitmapFont == null)
+ throw new ArgumentNullException(nameof(bitmapFont));
+
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var lineSpacing = bitmapFont.LineHeight;
+ var offset = new Vector2(0, 0);
+
+ BitmapFontRegion lastGlyph = null;
+ for (var i = 0; i < text.Length;)
+ {
+ int character;
+ if (char.IsLowSurrogate(text[i]))
+ {
+ character = char.ConvertToUtf32(text[i - 1], text[i]);
+ i += 2;
+ }
+ else if (char.IsHighSurrogate(text[i]))
+ {
+ character = char.ConvertToUtf32(text[i], text[i - 1]);
+ i += 2;
+ }
+ else
+ {
+ character = text[i];
+ i += 1;
+ }
+
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (character)
+ {
+ case '\r':
+ continue;
+ case '\n':
+ offset.X = 0;
+ offset.Y += lineSpacing;
+ lastGlyph = null;
+ continue;
+ }
+
+ var fontRegion = bitmapFont.GetCharacterRegion(character);
+ if (fontRegion == null)
+ continue;
+
+ var transform1Matrix = transformMatrix;
+ transform1Matrix.M31 += offset.X + fontRegion.XOffset;
+ transform1Matrix.M32 += offset.Y + fontRegion.YOffset;
+
+ var textureRegion = fontRegion.TextureRegion;
+ var bounds = textureRegion.Bounds;
+ DrawSprite(textureRegion.Texture, ref transform1Matrix, ref bounds, color, flags, depth);
+
+ var advance = fontRegion.XAdvance + bitmapFont.LetterSpacing;
+ if (BitmapFont.UseKernings && lastGlyph != null)
+ {
+ int amount;
+ if (lastGlyph.Kernings.TryGetValue(character, out amount))
+ {
+ advance += amount;
+ }
+ }
+
+ offset.X += i != text.Length - 1
+ ? advance
+ : fontRegion.XOffset + fontRegion.Width;
+
+ lastGlyph = fontRegion;
+ }
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="StringBuilder" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation
+ /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and
+ /// depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="position">The position <see cref="Vector2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="rotation">
+ /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default
+ /// value is <code>0f</code>.
+ /// </param>
+ /// <param name="origin">
+ /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.Zero" />.
+ /// </param>
+ /// <param name="scale">
+ /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.One" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, StringBuilder text, Vector2 position, Color? color = null,
+ float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ Matrix2 transformMatrix;
+ Matrix2.CreateFrom(position, rotation, scale, origin, out transformMatrix);
+ DrawString(bitmapFont, text, ref transformMatrix, color, flags, depth);
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="string" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin
+ /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, string text, ref Matrix2 transformMatrix, Color? color = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ EnsureHasBegun();
+
+ if (bitmapFont == null)
+ throw new ArgumentNullException(nameof(bitmapFont));
+
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var glyphs = bitmapFont.GetGlyphs(text);
+ foreach (var glyph in glyphs)
+ {
+ var transform1Matrix = transformMatrix;
+ transform1Matrix.M31 += glyph.Position.X;
+ transform1Matrix.M32 += glyph.Position.Y;
+
+ var texture = glyph.FontRegion.TextureRegion.Texture;
+ var bounds = texture.Bounds;
+ DrawSprite(texture, ref transform1Matrix, ref bounds, color, flags, depth);
+ }
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="string" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation
+ /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and
+ /// depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="position">The position <see cref="Vector2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="rotation">
+ /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default
+ /// value is <code>0f</code>.
+ /// </param>
+ /// <param name="origin">
+ /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.Zero" />.
+ /// </param>
+ /// <param name="scale">
+ /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.One" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, string text, Vector2 position, Color? color = null,
+ float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ Matrix2 matrix;
+ Matrix2.CreateFrom(position, rotation, scale, origin, out matrix);
+ DrawString(bitmapFont, text, ref matrix, color, flags, depth);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public struct DrawCallInfo : IBatchDrawCallInfo<DrawCallInfo>, IComparable<DrawCallInfo>
+ {
+ internal readonly PrimitiveType PrimitiveType;
+ internal int StartIndex;
+ internal int PrimitiveCount;
+ internal readonly Texture2D Texture;
+ internal readonly uint TextureKey;
+ internal readonly uint DepthKey;
+
+ internal unsafe DrawCallInfo(Texture2D texture, PrimitiveType primitiveType, int startIndex, int primitiveCount, float depth)
+ {
+ PrimitiveType = primitiveType;
+ StartIndex = startIndex;
+ PrimitiveCount = primitiveCount;
+ Texture = texture;
+ TextureKey = (uint)RuntimeHelpers.GetHashCode(texture);
+ DepthKey = *(uint*)&depth;
+ }
+
+ public void SetState(Effect effect)
+ {
+ var textureEffect = effect as ITextureEffect;
+ if (textureEffect != null)
+ textureEffect.Texture = Texture;
+ }
+
+ public bool TryMerge(ref DrawCallInfo drawCall)
+ {
+ if (PrimitiveType != drawCall.PrimitiveType || TextureKey != drawCall.TextureKey ||
+ DepthKey != drawCall.DepthKey)
+ return false;
+ drawCall.PrimitiveCount += PrimitiveCount;
+ return true;
+ }
+
+ [SuppressMessage("ReSharper", "ImpureMethodCallOnReadonlyValueField")]
+ public int CompareTo(DrawCallInfo other)
+ {
+ var result = TextureKey.CompareTo(other.TextureKey);;
+ if (result != 0)
+ return result;
+ result = DepthKey.CompareTo(other.DepthKey);
+ return result != 0 ? result : ((byte)PrimitiveType).CompareTo((byte)other.PrimitiveType);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs
new file mode 100644
index 0000000..df71e47
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs
@@ -0,0 +1,203 @@
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// An <see cref="Effect" /> that allows objects, within a 3D context, to be represented on a 2D monitor.
+ /// </summary>
+ /// <seealso cref="MatrixChainEffect" />
+ /// <seealso cref="ITextureEffect" />
+ public class DefaultEffect : MatrixChainEffect, ITextureEffect
+ {
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether <see cref="Texture" /> has
+ /// changed in the last frame.
+ /// </summary>
+ protected static int DirtyTextureBitMask = BitVector32.CreateMask(UseDefaultProjectionBitMask);
+
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the underlying vertex shader and
+ /// fragment (pixel) shaders have changed to one of the pre-defined shaders in the last frame.
+ /// </summary>
+ protected static int DirtyShaderIndexBitMask = BitVector32.CreateMask(DirtyTextureBitMask);
+
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the material color has changed in
+ /// the last frame.
+ /// </summary>
+ public static int DirtyMaterialColorBitMask = BitVector32.CreateMask(DirtyShaderIndexBitMask);
+
+ private Texture2D _texture;
+ private EffectParameter _textureParameter;
+
+ private float _alpha = 1;
+ private Color _diffuseColor = Color.White;
+ private EffectParameter _diffuseColorParameter;
+
+ private bool _textureEnabled;
+ private bool _vertexColorEnabled;
+
+ /// <summary>
+ /// Gets or sets the material <see cref="Texture2D" />.
+ /// </summary>
+ /// <value>
+ /// The material <see cref="Texture2D" />.
+ /// </value>
+ public Texture2D Texture
+ {
+ get { return _texture; }
+ set
+ {
+ _texture = value;
+ Flags[DirtyTextureBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the material color alpha.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The alpha channel uses the premultiplied (associated) representation. This means that the RGB components of a
+ /// color represent
+ /// the color of the object of pixel, adjusted for its opacity by multiplication of <see cref="Alpha" />.
+ /// </para>
+ /// </remarks>
+ public float Alpha
+ {
+ get { return _alpha; }
+
+ set
+ {
+ _alpha = value;
+ Flags[DirtyMaterialColorBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether texturing is enabled.
+ /// </summary>
+ public bool TextureEnabled
+ {
+ get { return _textureEnabled; }
+
+ set
+ {
+ if (_textureEnabled == value)
+ return;
+ _textureEnabled = value;
+ Flags[DirtyShaderIndexBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether vertex color is enabled.
+ /// </summary>
+ public bool VertexColorEnabled
+ {
+ get { return _vertexColorEnabled; }
+
+ set
+ {
+ if (_vertexColorEnabled == value)
+ return;
+ _vertexColorEnabled = value;
+ Flags[DirtyShaderIndexBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ public DefaultEffect(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice, EffectResource.DefaultEffect.Bytecode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="byteCode">The byte code of the shader program.</param>
+ public DefaultEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="cloneSource">The clone source.</param>
+ public DefaultEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ Flags[DirtyMaterialColorBitMask] = true;
+ _textureParameter = Parameters["Texture"];
+ _diffuseColorParameter = Parameters["DiffuseColor"];
+ }
+
+ /// <summary>
+ /// Computes derived parameter values immediately before applying the effect.
+ /// </summary>
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ if (Flags[DirtyTextureBitMask])
+ {
+ _textureParameter.SetValue(_texture);
+ Flags[DirtyTextureBitMask] = false;
+ }
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyMaterialColorBitMask])
+ {
+ UpdateMaterialColor();
+ Flags[DirtyMaterialColorBitMask] = false;
+ }
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyShaderIndexBitMask])
+ {
+ var shaderIndex = 0;
+
+ if (_textureEnabled)
+ shaderIndex += 1;
+
+ if (_vertexColorEnabled)
+ shaderIndex += 2;
+
+ Flags[DirtyShaderIndexBitMask] = false;
+ CurrentTechnique = Techniques[shaderIndex];
+ }
+ }
+
+ /// <summary>
+ /// Updates the material color parameters associated with this <see cref="DefaultEffect" />.
+ /// </summary>
+ protected virtual void UpdateMaterialColor()
+ {
+ var diffuseColorVector3 = _diffuseColor.ToVector3();
+
+ var diffuseColorVector4 = new Vector4()
+ {
+ X = diffuseColorVector3.X * Alpha,
+ Y = diffuseColorVector3.Y * Alpha,
+ Z = diffuseColorVector3.Z * Alpha,
+ W = Alpha,
+ };
+
+ _diffuseColorParameter.SetValue(diffuseColorVector4);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs
new file mode 100644
index 0000000..43bd535
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// Reperesents the bytecode of an <see cref="Effect" /> that is encapsulated inside a compiled assembly.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Files that are encapsulated inside a compiled assembly are commonly known as Manifiest or embedded resources.
+ /// Since embedded resources are added to the assembly at compiled time, they can not be accidentally deleted or
+ /// misplaced. However, if the file needs to be changed, the assembly will need to be re-compiled with the changed
+ /// file.
+ /// </para>
+ /// <para>
+ /// To add an embedded resource file to an assembly, first add it to the project and then change the Build Action
+ /// in the Properties of the file to <code>Embedded Resource</code>. The next time the project is compiled, the
+ /// compiler will add the file to the assembly as an embedded resource. The compiler adds namespace(s) to the
+ /// embedded resource so it matches with the path of where the file was added to the project.
+ /// </para>
+ /// </remarks>
+ public class EffectResource
+ {
+ private static EffectResource _defaultEffect;
+ private static string _shaderExtension;
+
+ /// <summary>
+ /// Gets the <see cref="Effects.DefaultEffect" /> embedded into the MonoGame.Extended.Graphics library.
+ /// </summary>
+ public static EffectResource DefaultEffect => _defaultEffect ?? (_defaultEffect = new EffectResource($"MonoGame.Extended.Graphics.Effects.Resources.DefaultEffect.{_shaderExtension}.mgfxo"));
+
+ static EffectResource()
+ {
+ DetermineShaderExtension();
+ }
+
+ private static void DetermineShaderExtension()
+ {
+ // use reflection to figure out if Shader.Profile is OpenGL (0) or DirectX (1),
+ // may need to be changed / fixed for future shader profiles
+
+ var assembly = typeof(Game).GetTypeInfo().Assembly;
+ Debug.Assert(assembly != null);
+
+ var shaderType = assembly.GetType("Microsoft.Xna.Framework.Graphics.Shader");
+ Debug.Assert(shaderType != null);
+ var shaderTypeInfo = shaderType.GetTypeInfo();
+ Debug.Assert(shaderTypeInfo != null);
+
+ // https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Shader/Shader.cs#L47
+ var profileProperty = shaderTypeInfo.GetDeclaredProperty("Profile");
+ var value = (int)profileProperty.GetValue(null);
+
+ switch (value)
+ {
+ case 0:
+ // OpenGL
+ _shaderExtension = "ogl";
+ break;
+ case 1:
+ // DirectX
+ _shaderExtension = "dx11";
+ break;
+ default:
+ throw new InvalidOperationException("Unknown shader profile.");
+ }
+ }
+
+ private readonly string _resourceName;
+ private volatile byte[] _bytecode;
+ private readonly Assembly _assembly;
+
+ /// <summary>
+ /// Gets the bytecode of the <see cref="Effect" /> file.
+ /// </summary>
+ /// <value>
+ /// The bytecode of the <see cref="Effect" /> file.
+ /// </value>
+ public byte[] Bytecode
+ {
+ get
+ {
+ if (_bytecode != null)
+ return _bytecode;
+
+ lock (this)
+ {
+ if (_bytecode != null)
+ return _bytecode;
+
+ var stream = _assembly.GetManifestResourceStream(_resourceName);
+ using (var memoryStream = new MemoryStream())
+ {
+ stream.CopyTo(memoryStream);
+ _bytecode = memoryStream.ToArray();
+ }
+ }
+
+ return _bytecode;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EffectResource" /> class.
+ /// </summary>
+ /// <param name="resourceName">The name of the embedded resource. This must include the namespace(s).</param>
+ /// <param name="assembly">The assembly which the embedded resource is apart of.</param>
+ public EffectResource(string resourceName, Assembly assembly = null)
+ {
+ _resourceName = resourceName;
+ _assembly = assembly ?? typeof(EffectResource).GetTypeInfo().Assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs
new file mode 100644
index 0000000..ea2ef0b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// Defines an <see cref="Effect" /> that uses a <see cref="Texture2D" />.
+ /// </summary>
+ public interface ITextureEffect
+ {
+ /// <summary>
+ /// Gets or sets the <see cref="Texture2D" />.
+ /// </summary>
+ /// <value>
+ /// The <see cref="Texture2D" />.
+ /// </value>
+ Texture2D Texture { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs
new file mode 100644
index 0000000..046479f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs
@@ -0,0 +1,155 @@
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// An <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on a 2D
+ /// monitor.
+ /// </summary>
+ /// <seealso cref="Effect" />
+ /// <seealso cref="IEffectMatrices" />
+ public abstract class MatrixChainEffect : Effect, IMatrixChainEffect
+ {
+ /// <summary>
+ /// The bitmask for use with <see cref="Flags"/> indicating wether <see cref="World"/>, <see cref="View"/>, or <see cref="Projection"/> has changed in the last frame.
+ /// </summary>
+ protected static int DirtyWorldViewProjectionBitMask = BitVector32.CreateMask();
+
+ /// <summary>
+ /// The bitmask for use with <see cref="Flags"/> indicating wether to use a default projection matrix or a custom projection matrix.
+ /// </summary>
+ protected static int UseDefaultProjectionBitMask = BitVector32.CreateMask(DirtyWorldViewProjectionBitMask);
+
+ /// <summary>
+ /// The dirty flags associated with this <see cref="MatrixChainEffect"/>.
+ /// </summary>
+ protected BitVector32 Flags;
+
+ private Matrix _projection = Matrix.Identity;
+ private Matrix _view = Matrix.Identity;
+ private Matrix _world = Matrix.Identity;
+ private EffectParameter _matrixParameter;
+
+ /// <summary>
+ /// Gets or sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The model-to-world <see cref="Matrix" />.
+ /// </value>
+ public Matrix World
+ {
+ get { return _world; }
+ set { SetWorld(ref value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The world-to-view <see cref="Matrix" />.
+ /// </value>
+ public Matrix View
+ {
+ get { return _view; }
+ set { SetView(ref value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The view-to-projection <see cref="Matrix" />.
+ /// </value>
+ public Matrix Projection
+ {
+ get { return _projection; }
+ set { SetProjection(ref value); }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="byteCode">The effect code.</param>
+ protected MatrixChainEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class.
+ /// </summary>
+ /// <param name="cloneSource">The clone source.</param>
+ protected MatrixChainEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ Flags[UseDefaultProjectionBitMask] = true;
+
+ _matrixParameter = Parameters["WorldViewProjection"];
+ }
+
+ /// <summary>
+ /// Sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="world">The model-to-world <see cref="Matrix" />.</param>
+ public void SetWorld(ref Matrix world)
+ {
+ _world = world;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ }
+
+ /// <summary>
+ /// Sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="view">The world-to-view <see cref="Matrix" />.</param>
+ public void SetView(ref Matrix view)
+ {
+ _view = view;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ }
+
+ /// <summary>
+ /// Sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param>
+ public void SetProjection(ref Matrix projection)
+ {
+ _projection = projection;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ Flags[UseDefaultProjectionBitMask] = false;
+ }
+
+ /// <summary>
+ /// Computes derived parameter values immediately before applying the effect.
+ /// </summary>
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyWorldViewProjectionBitMask] || Flags[UseDefaultProjectionBitMask])
+ {
+ if (Flags[UseDefaultProjectionBitMask])
+ {
+ var viewport = GraphicsDevice.Viewport;
+ _projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, -1);
+ }
+
+ Matrix worldViewProjection;
+ Matrix.Multiply(ref _world, ref _view, out worldViewProjection);
+ Matrix.Multiply(ref worldViewProjection, ref _projection, out worldViewProjection);
+ _matrixParameter.SetValue(worldViewProjection);
+
+ Flags[DirtyWorldViewProjectionBitMask] = false;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo
new file mode 100644
index 0000000..b83b6ed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo
Binary files differ
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx
new file mode 100644
index 0000000..30a772d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx
@@ -0,0 +1,72 @@
+#include "Macros.fxh"
+#include "Structures.fxh"
+
+DECLARE_TEXTURE(Texture, 0);
+
+BEGIN_CONSTANTS
+
+float4 DiffuseColor = float4(1, 1, 1, 1);
+
+MATRIX_CONSTANTS
+
+float4x4 WorldViewProjection _vs(c0) _cb(c0);
+
+END_CONSTANTS
+
+VertexShaderOutputPosition VertexShaderFunctionPosition(VertexShaderInputPosition input)
+{
+ VertexShaderOutputPosition output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ return output;
+}
+
+float4 PixelShaderFunctionPosition(VertexShaderOutputPosition input) : SV_Target0
+{
+ return DiffuseColor;
+}
+
+VertexShaderOutputPositionTexture VertexShaderFunctionPositionTexture(VertexShaderInputPositionTexture input)
+{
+ VertexShaderOutputPositionTexture output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.TextureCoordinate = input.TextureCoordinate;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionTexture(VertexShaderOutputPositionTexture input) : SV_Target0
+{
+ return SAMPLE_TEXTURE(Texture, input.TextureCoordinate) * DiffuseColor;
+}
+
+VertexShaderOutputPositionColor VertexShaderFunctionPositionColor(VertexShaderInputPositionColor input)
+{
+ VertexShaderOutputPositionColor output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.Color = input.Color;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionColor(VertexShaderOutputPositionColor input) : SV_Target0
+{
+ return input.Color * DiffuseColor;
+}
+
+VertexShaderOutputPositionColorTexture VertexShaderFunctionPositionColorTexture(VertexShaderInputPositionColorTexture input)
+{
+ VertexShaderOutputPositionColorTexture output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.Color = input.Color;
+ output.TextureCoordinate = input.TextureCoordinate;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionColorTexture(VertexShaderOutputPositionColorTexture input) : SV_Target0
+{
+ float4 textureColor = SAMPLE_TEXTURE(Texture, input.TextureCoordinate);
+ return textureColor * input.Color * DiffuseColor;
+}
+
+TECHNIQUE(Position, VertexShaderFunctionPosition, PixelShaderFunctionPosition);
+TECHNIQUE(PositionTexture, VertexShaderFunctionPositionTexture, PixelShaderFunctionPositionTexture);
+TECHNIQUE(PositionColor, VertexShaderFunctionPositionColor, PixelShaderFunctionPositionColor);
+TECHNIQUE(PositionColorTexture, VertexShaderFunctionPositionColorTexture, PixelShaderFunctionPositionColorTexture); \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo
new file mode 100644
index 0000000..4f51d2a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo
Binary files differ
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh
new file mode 100644
index 0000000..c253efe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+// Macros.fxh
+//
+// Microsoft XNA Community Game Platform
+// Copyright (C) Microsoft Corporation. All rights reserved.
+//-----------------------------------------------------------------------------
+
+#if SM4
+
+// Macros for targetting shader model 4.0 (DX11)
+
+#define TECHNIQUE(name, vsname, psname ) \
+ technique name { pass { VertexShader = compile vs_4_0_level_9_1 vsname (); PixelShader = compile ps_4_0_level_9_1 psname(); } }
+
+#define BEGIN_CONSTANTS cbuffer Parameters : register(b0) {
+#define MATRIX_CONSTANTS
+#define END_CONSTANTS };
+
+#define _vs(r)
+#define _ps(r)
+#define _cb(r)
+
+#define DECLARE_TEXTURE(Name, index) \
+ Texture2D<float4> Name : register(t##index); \
+ sampler Name##Sampler : register(s##index)
+
+#define DECLARE_CUBEMAP(Name, index) \
+ TextureCube<float4> Name : register(t##index); \
+ sampler Name##Sampler : register(s##index)
+
+#define SAMPLE_TEXTURE(Name, texCoord) Name.Sample(Name##Sampler, texCoord)
+#define SAMPLE_CUBEMAP(Name, texCoord) Name.Sample(Name##Sampler, texCoord)
+
+
+#else
+
+// Macros for targetting shader model 2.0 (DX9)
+
+#define TECHNIQUE(name, vsname, psname ) \
+ technique name { pass { VertexShader = compile vs_2_0 vsname (); PixelShader = compile ps_2_0 psname(); } }
+
+#define BEGIN_CONSTANTS
+#define MATRIX_CONSTANTS
+#define END_CONSTANTS
+
+#define _vs(r) : register(vs, r)
+#define _ps(r) : register(ps, r)
+#define _cb(r)
+
+#define DECLARE_TEXTURE(Name, index) \
+ sampler2D Name : register(s##index);
+
+#define DECLARE_CUBEMAP(Name, index) \
+ samplerCUBE Name : register(s##index);
+
+#define SAMPLE_TEXTURE(Name, texCoord) tex2D(Name, texCoord)
+#define SAMPLE_CUBEMAP(Name, texCoord) texCUBE(Name, texCoord)
+
+
+#endif \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat
new file mode 100644
index 0000000..4fc7f21
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat
@@ -0,0 +1,15 @@
+setlocal
+
+SET TWOMGFX="mgfxc"
+
+@for /f %%f IN ('dir /b *.fx') do (
+
+ call %TWOMGFX% %%~nf.fx %%~nf.ogl.mgfxo /Profile:OpenGL
+
+ call %TWOMGFX% %%~nf.fx %%~nf.dx11.mgfxo /Profile:DirectX_11
+
+)
+
+endlocal
+
+pause \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh
new file mode 100644
index 0000000..d3af6a0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh
@@ -0,0 +1,51 @@
+// Vertex shader input structures.
+
+struct VertexShaderInputPosition
+{
+ float4 Position : SV_Position;
+};
+
+struct VertexShaderInputPositionColor
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR;
+};
+
+struct VertexShaderInputPositionTexture
+{
+ float4 Position : SV_Position;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+struct VertexShaderInputPositionColorTexture
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+// Vertex shader output structures.
+
+struct VertexShaderOutputPosition
+{
+ float4 Position : SV_Position;
+};
+
+struct VertexShaderOutputPositionColor
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR0;
+};
+
+struct VertexShaderOutputPositionTexture
+{
+ float4 Position : SV_Position;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+struct VertexShaderOutputPositionColorTexture
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR0;
+ float2 TextureCoordinate : TEXCOORD0;
+};
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs
new file mode 100644
index 0000000..6c19887
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MonoGame.Extended.Graphics
+{
+ [Flags]
+ public enum FlipFlags : byte
+ {
+ None = 0,
+ FlipDiagonally = 1 << 0,
+ FlipVertically = 1 << 1,
+ FlipHorizontally = 1 << 2
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs
new file mode 100644
index 0000000..dad4f15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Geometry
+{
+ public abstract class GeometryBuilder<TVertexType, TIndexType>
+ where TVertexType : struct, IVertexType
+ where TIndexType : struct
+ {
+ public PrimitiveType PrimitiveType { get; protected set; }
+ public int VertexCount { get; protected set; }
+ public int IndexCount { get; protected set; }
+ public int PrimitivesCount { get; protected set; }
+
+ public TVertexType[] Vertices { get; }
+ public TIndexType[] Indices { get; }
+
+ protected GeometryBuilder(int maximumVerticesCount, int maximumIndicesCount)
+ {
+ Vertices = new TVertexType[maximumVerticesCount];
+ Indices = new TIndexType[maximumIndicesCount];
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs
new file mode 100644
index 0000000..c113fbe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Geometry
+{
+ public class GeometryBuilder2D : GeometryBuilder<VertexPositionColorTexture, ushort>
+ {
+ public GeometryBuilder2D(int maximumVerticesCount, int maximumIndicesCount)
+ : base(maximumVerticesCount, maximumIndicesCount)
+ {
+ }
+
+ public void BuildSprite(int indexOffset, ref Matrix2 transformMatrix, Texture2D texture,
+ ref Rectangle sourceRectangle,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ if (texture == null)
+ throw new ArgumentNullException(nameof(texture));
+
+ var texelLeft = 0f;
+ var texelTop = 0f;
+ var texelRight = 1f;
+ var texelBottom = 1f;
+
+ if (sourceRectangle.Width > 0)
+ {
+ texelLeft = (float)sourceRectangle.X / texture.Width;
+ texelTop = (float)sourceRectangle.Y / texture.Height;
+ texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+ }
+ else
+ {
+ sourceRectangle.Width = texture.Width;
+ sourceRectangle.Height = texture.Height;
+ }
+
+ var color1 = color ?? Color.White;
+
+ var vertices = Vertices;
+
+ transformMatrix.Transform(0, 0, ref vertices[0].Position);
+ vertices[0].Position.Z = depth;
+ vertices[0].Color = color1;
+ vertices[0].TextureCoordinate.X = texelLeft;
+ vertices[0].TextureCoordinate.Y = texelTop;
+
+ transformMatrix.Transform(sourceRectangle.Width, 0, ref vertices[1].Position);
+ vertices[1].Position.Z = depth;
+ vertices[1].Color = color1;
+ vertices[1].TextureCoordinate.X = texelRight;
+ vertices[1].TextureCoordinate.Y = texelTop;
+
+ transformMatrix.Transform(0, sourceRectangle.Height, ref vertices[2].Position);
+ vertices[2].Position.Z = depth;
+ vertices[2].Color = color1;
+ vertices[2].TextureCoordinate.X = texelLeft;
+ vertices[2].TextureCoordinate.Y = texelBottom;
+
+ transformMatrix.Transform(sourceRectangle.Width, sourceRectangle.Height, ref vertices[3].Position);
+ vertices[3].Position.Z = depth;
+ vertices[3].Color = color1;
+ vertices[3].TextureCoordinate.X = texelRight;
+ vertices[3].TextureCoordinate.Y = texelBottom;
+
+ var flipDiagonally = (flags & FlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & FlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & FlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[1].TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertices[2].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[1].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[2].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X);
+ }
+
+ if (flipVertically)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y);
+ }
+
+ VertexCount = 4;
+ AddQuadrilateralIndices(indexOffset);
+ IndexCount = 6;
+ PrimitivesCount = 2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddQuadrilateralIndices(int indexOffset)
+ {
+ Indices[0] = (ushort)(0 + indexOffset);
+ Indices[1] = (ushort)(1 + indexOffset);
+ Indices[2] = (ushort)(2 + indexOffset);
+ Indices[3] = (ushort)(1 + indexOffset);
+ Indices[4] = (ushort)(3 + indexOffset);
+ Indices[5] = (ushort)(2 + indexOffset);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs
new file mode 100644
index 0000000..67c4755
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs
@@ -0,0 +1,19 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Defines the for a deferred draw call when batching.
+ /// </summary>
+ public interface IBatchDrawCallInfo<TDrawCallInfo> where TDrawCallInfo : IBatchDrawCallInfo<TDrawCallInfo>
+ {
+ /// <summary>
+ /// Applies any state from the <see cref="IBatchDrawCallInfo{TDrawCallInfo}" /> to the
+ /// <see cref="Effect" /> or <see cref="Effect.GraphicsDevice"/>.
+ /// </summary>
+ /// <param name="effect">The effect.</param>
+ void SetState(Effect effect);
+
+ bool TryMerge(ref TDrawCallInfo drawCall);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs
new file mode 100644
index 0000000..da8ccb9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Defines an <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on
+ /// a 2D monitor.
+ /// </summary>
+ public interface IMatrixChainEffect : IEffectMatrices
+ {
+ /// <summary>
+ /// Sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="world">The model-to-world <see cref="Matrix" />.</param>
+ void SetWorld(ref Matrix world);
+
+ /// <summary>
+ /// Sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="view">The world-to-view <see cref="Matrix" />.</param>
+ void SetView(ref Matrix view);
+
+ /// <summary>
+ /// Sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param>
+ void SetProjection(ref Matrix projection);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj
new file mode 100644
index 0000000..fd25850
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj
@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Graphics makes MonoGame more awesome.</Description>
+ <PackageTags>monogame graphics batcher effects</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Remove="Effects\Resources\DefaultEffect.dx11.mgfxo" />
+ <None Remove="Effects\Resources\DefaultEffect.fx" />
+ <None Remove="Effects\Resources\DefaultEffect.ogl.mgfxo" />
+ <None Remove="Effects\Resources\Macros.fxh" />
+ <None Remove="Effects\Resources\Structures.fxh" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.dx11.mgfxo" />
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.fx" />
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.ogl.mgfxo" />
+ <EmbeddedResource Include="Effects\Resources\Macros.fxh" />
+ <EmbeddedResource Include="Effects\Resources\Structures.fxh" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs
new file mode 100644
index 0000000..68a39ef
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ public static class PrimitiveTypeExtensions
+ {
+ internal static int GetPrimitivesCount(this PrimitiveType primitiveType, int verticesCount)
+ {
+ switch (primitiveType)
+ {
+ case PrimitiveType.LineStrip:
+ return verticesCount - 1;
+ case PrimitiveType.LineList:
+ return verticesCount/2;
+ case PrimitiveType.TriangleStrip:
+ return verticesCount - 2;
+ case PrimitiveType.TriangleList:
+ return verticesCount/3;
+ default:
+ throw new ArgumentException("Invalid primitive type.");
+ }
+ }
+
+ internal static int GetVerticesCount(this PrimitiveType primitiveType, int primitivesCount)
+ {
+ switch (primitiveType)
+ {
+ case PrimitiveType.LineStrip:
+ return primitivesCount + 1;
+ case PrimitiveType.LineList:
+ return primitivesCount*2;
+ case PrimitiveType.TriangleStrip:
+ return primitivesCount + 2;
+ case PrimitiveType.TriangleList:
+ return primitivesCount*3;
+ default:
+ throw new ArgumentException("Invalid primitive type.");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs
new file mode 100644
index 0000000..f090603
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ public static class RenderTarget2DExtensions
+ {
+ public static IDisposable BeginDraw(this RenderTarget2D renderTarget, GraphicsDevice graphicsDevice,
+ Color backgroundColor)
+ {
+ return new RenderTargetOperation(renderTarget, graphicsDevice, backgroundColor);
+ }
+
+ private class RenderTargetOperation : IDisposable
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly RenderTargetUsage _previousRenderTargetUsage;
+ private readonly Viewport _viewport;
+
+ public RenderTargetOperation(RenderTarget2D renderTarget, GraphicsDevice graphicsDevice,
+ Color backgroundColor)
+ {
+ _graphicsDevice = graphicsDevice;
+ _viewport = _graphicsDevice.Viewport;
+ _previousRenderTargetUsage = _graphicsDevice.PresentationParameters.RenderTargetUsage;
+
+ _graphicsDevice.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents;
+ _graphicsDevice.SetRenderTarget(renderTarget);
+ _graphicsDevice.Clear(backgroundColor);
+ }
+
+ public void Dispose()
+ {
+ _graphicsDevice.SetRenderTarget(null);
+ _graphicsDevice.PresentationParameters.RenderTargetUsage = _previousRenderTargetUsage;
+ _graphicsDevice.Viewport = _viewport;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
new file mode 100644
index 0000000..1593da3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public class ControlStyle : IDictionary<string, object>
+ {
+ private readonly Dictionary<Guid, Dictionary<string, object>> _previousStates = new Dictionary<Guid, Dictionary<string, object>>();
+
+ public ControlStyle()
+ : this(typeof(Element))
+ {
+ }
+
+ public ControlStyle(Type targetType)
+ : this(targetType.FullName, targetType)
+ {
+ }
+
+ public ControlStyle(string name, Type targetType)
+ {
+ Name = name;
+ TargetType = targetType;
+ _setters = new Dictionary<string, object>();
+ }
+
+ public string Name { get; }
+ public Type TargetType { get; set; }
+
+ private readonly Dictionary<string, object> _setters;
+
+ public void ApplyIf(Control control, bool predicate)
+ {
+ if (predicate)
+ Apply(control);
+ else
+ Revert(control);
+ }
+
+ public void Apply(Control control)
+ {
+ _previousStates[control.Id] = _setters
+ .ToDictionary(i => i.Key, i => TargetType.GetRuntimeProperty(i.Key)?.GetValue(control));
+
+ ChangePropertyValues(control, _setters);
+ }
+
+ public void Revert(Control control)
+ {
+ if (_previousStates.ContainsKey(control.Id) && _previousStates[control.Id] != null)
+ ChangePropertyValues(control, _previousStates[control.Id]);
+
+ _previousStates[control.Id] = null;
+ }
+
+ private static void ChangePropertyValues(Control control, Dictionary<string, object> setters)
+ {
+ var targetType = control.GetType();
+
+ foreach (var propertyName in setters.Keys)
+ {
+ var propertyInfo = targetType.GetRuntimeProperty(propertyName);
+ var value = setters[propertyName];
+
+ if (propertyInfo != null)
+ {
+ if(propertyInfo.CanWrite)
+ propertyInfo.SetValue(control, value);
+
+ // special case when we have a list of items as objects (like on a list box)
+ if (propertyInfo.PropertyType == typeof(List<object>))
+ {
+ var items = (List<object>)value;
+ var addMethod = propertyInfo.PropertyType.GetRuntimeMethod("Add", new[] { typeof(object) });
+
+ foreach (var item in items)
+ addMethod.Invoke(propertyInfo.GetValue(control), new[] {item});
+ }
+ }
+ }
+ }
+
+ public object this[string key]
+ {
+ get { return _setters[key]; }
+ set { _setters[key] = value; }
+ }
+
+ public ICollection<string> Keys => _setters.Keys;
+ public ICollection<object> Values => _setters.Values;
+ public int Count => _setters.Count;
+ public bool IsReadOnly => false;
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _setters.GetEnumerator();
+ public void Add(string key, object value) => _setters.Add(key, value);
+ public void Add(KeyValuePair<string, object> item) => _setters.Add(item.Key, item.Value);
+ public bool Remove(string key) => _setters.Remove(key);
+ public bool Remove(KeyValuePair<string, object> item) => _setters.Remove(item.Key);
+ public void Clear() => _setters.Clear();
+ public bool Contains(KeyValuePair<string, object> item) => _setters.Contains(item);
+ public bool ContainsKey(string key) => _setters.ContainsKey(key);
+ public bool TryGetValue(string key, out object value) => _setters.TryGetValue(key, out value);
+
+ public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
new file mode 100644
index 0000000..1e5c06e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Box : Control
+ {
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(Width, Height);
+ }
+
+ public Color FillColor { get; set; } = Color.White;
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+ renderer.FillRectangle(ContentRectangle, FillColor);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
new file mode 100644
index 0000000..310dc00
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
@@ -0,0 +1,92 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Button : ContentControl
+ {
+ public Button()
+ {
+ }
+
+ public event EventHandler Clicked;
+ public event EventHandler PressedStateChanged;
+
+ private bool _isPressed;
+ public bool IsPressed
+ {
+ get => _isPressed;
+ set
+ {
+ if (_isPressed != value)
+ {
+ _isPressed = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ PressedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _pressedStyle;
+ public ControlStyle PressedStyle
+ {
+ get => _pressedStyle;
+ set
+ {
+ if (_pressedStyle != value)
+ {
+ _pressedStyle = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ }
+ }
+ }
+
+ private bool _isPointerDown;
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ {
+ _isPointerDown = true;
+ IsPressed = true;
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ _isPointerDown = false;
+
+ if (IsPressed)
+ {
+ IsPressed = false;
+
+ if (BoundingRectangle.Contains(args.Position) && IsEnabled)
+ Click();
+ }
+
+ return base.OnPointerUp(context, args);
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && _isPointerDown)
+ IsPressed = true;
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ IsPressed = false;
+
+ return base.OnPointerLeave(context, args);
+ }
+
+ public void Click()
+ {
+ Clicked?.Invoke(this, EventArgs.Empty);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
new file mode 100644
index 0000000..d667a73
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
@@ -0,0 +1,25 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Canvas : LayoutControl
+ {
+ public Canvas()
+ {
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+ PlaceControl(context, control, control.Position.X, control.Position.Y, actualSize.Width, actualSize.Height);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
new file mode 100644
index 0000000..c6c375f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
@@ -0,0 +1,65 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class CheckBox : CompositeControl
+ {
+ public CheckBox()
+ {
+ _contentLabel = new Label();
+ _checkLabel = new Box {Width = 20, Height = 20};
+
+ _toggleButton = new ToggleButton
+ {
+ Margin = 0,
+ Padding = 0,
+ BackgroundColor = Color.Transparent,
+ BorderThickness = 0,
+ HoverStyle = null,
+ CheckedStyle = null,
+ PressedStyle = null,
+ Content = new StackPanel
+ {
+ Margin = 0,
+ Orientation = Orientation.Horizontal,
+ Items =
+ {
+ _checkLabel,
+ _contentLabel
+ }
+ }
+ };
+
+ _toggleButton.CheckedStateChanged += (sender, args) => OnIsCheckedChanged();
+ Template = _toggleButton;
+ OnIsCheckedChanged();
+ }
+
+ private readonly Label _contentLabel;
+ private readonly ToggleButton _toggleButton;
+ private readonly Box _checkLabel;
+
+ protected override Control Template { get; }
+
+ public override object Content
+ {
+ get => _contentLabel.Content;
+ set => _contentLabel.Content = value;
+ }
+
+ public bool IsChecked
+ {
+ get => _toggleButton.IsChecked;
+ set
+ {
+ _toggleButton.IsChecked = value;
+ OnIsCheckedChanged();
+ }
+ }
+
+ private void OnIsCheckedChanged()
+ {
+ _checkLabel.FillColor = IsChecked ? Color.White : Color.Transparent;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
new file mode 100644
index 0000000..f8867d2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
@@ -0,0 +1,102 @@
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ComboBox : SelectorControl
+ {
+ public ComboBox()
+ {
+ }
+
+ public bool IsOpen { get; set; }
+ public TextureRegion2D DropDownRegion { get; set; }
+ public Color DropDownColor { get; set; } = Color.White;
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Enter)
+ IsOpen = false;
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ IsOpen = !IsOpen;
+ return base.OnPointerUp(context, args);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return GetDropDownRectangle(context);
+ }
+
+ public override bool Contains(IGuiContext context, Point point)
+ {
+ return base.Contains(context, point) || IsOpen && GetListAreaRectangle(context).Contains(point);
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ if (itemSize.Height > height)
+ height = itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (IsOpen)
+ {
+ var dropDownRectangle = GetListAreaRectangle(context);
+
+ if (DropDownRegion != null)
+ {
+ renderer.DrawRegion(DropDownRegion, dropDownRectangle, DropDownColor);
+ }
+ else
+ {
+ renderer.FillRectangle(dropDownRectangle, DropDownColor);
+ renderer.DrawRectangle(dropDownRectangle, BorderColor);
+ }
+
+ DrawItemList(context, renderer);
+ }
+
+ var selectedTextInfo = GetItemTextInfo(context, ContentRectangle, SelectedItem);
+
+ if (!string.IsNullOrWhiteSpace(selectedTextInfo.Text))
+ renderer.DrawText(selectedTextInfo.Font, selectedTextInfo.Text, selectedTextInfo.Position + TextOffset, selectedTextInfo.Color, selectedTextInfo.ClippingRectangle);
+ }
+
+ private Rectangle GetDropDownRectangle(IGuiContext context)
+ {
+ var dropDownRectangle = BoundingRectangle;
+
+ dropDownRectangle.Y = dropDownRectangle.Y + dropDownRectangle.Height;
+ dropDownRectangle.Height = (int) Items
+ .Select(item => GetItemSize(context, item))
+ .Select(itemSize => itemSize.Height)
+ .Sum();
+
+ return dropDownRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
new file mode 100644
index 0000000..8217c69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class CompositeControl : Control
+ {
+ protected CompositeControl()
+ {
+ }
+
+ protected bool IsDirty { get; set; } = true;
+
+ public abstract object Content { get; set; }
+
+ protected abstract Control Template { get; }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ var control = Template;
+
+ if (control != null)
+ yield return control;
+ }
+ }
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ IsDirty = true;
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var control = Template;
+
+ if (control != null)
+ return Template.CalculateActualSize(context);
+
+ return Size.Empty;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ var control = Template;
+
+ if (control != null)
+ {
+ if (IsDirty)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ IsDirty = false;
+ }
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var control = Template;
+ control?.Draw(context, renderer, deltaSeconds);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
new file mode 100644
index 0000000..932f8e4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ContentControl : Control
+ {
+ private bool _contentChanged = true;
+
+ private object _content;
+ public object Content
+ {
+ get => _content;
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _contentChanged = true;
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ if (Content is Control control)
+ yield return control;
+ }
+ }
+
+ public bool HasContent => Content == null;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _contentChanged = true;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ if (_content is Control control && _contentChanged)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ _contentChanged = false;
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (Content is Control control)
+ {
+ control.Draw(context, renderer, deltaSeconds);
+ }
+ else
+ {
+ var text = Content?.ToString();
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ if (Content is Control control)
+ return control.CalculateActualSize(context);
+
+ var text = Content?.ToString();
+ var font = Font ?? context.DefaultFont;
+ return (Size)font.MeasureString(text ?? string.Empty);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
new file mode 100644
index 0000000..5440fdf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class Control : Element<Control>, IRectangular
+ {
+ protected Control()
+ {
+ BackgroundColor = Color.White;
+ TextColor = Color.White;
+ IsEnabled = true;
+ IsVisible = true;
+ Origin = Point.Zero;
+ Skin = Skin.Default;
+ }
+
+ private Skin _skin;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Skin Skin
+ {
+ get => _skin;
+ set
+ {
+ if (_skin != value)
+ {
+ _skin = value;
+ _skin?.Apply(this);
+ }
+ }
+ }
+
+ public abstract IEnumerable<Control> Children { get; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Margin { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Padding { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness ClipPadding { get; set; }
+
+ public bool IsLayoutRequired { get; set; }
+
+ public Rectangle ClippingRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + ClipPadding.Left, r.Top + ClipPadding.Top, r.Width - ClipPadding.Width, r.Height - ClipPadding.Height);
+ }
+ }
+
+ public Rectangle ContentRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + Padding.Left, r.Top + Padding.Top, r.Width - Padding.Width, r.Height - Padding.Height);
+ }
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsFocused { get; set; }
+
+ private bool _isHovered;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsHovered
+ {
+ get => _isHovered;
+ private set
+ {
+ if (_isHovered != value)
+ {
+ _isHovered = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ [JsonIgnore]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ public Vector2 Offset { get; set; }
+ public BitmapFont Font { get; set; }
+ public Color TextColor { get; set; }
+ public Vector2 TextOffset { get; set; }
+ public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Stretch;
+ public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Stretch;
+ public HorizontalAlignment HorizontalTextAlignment { get; set; } = HorizontalAlignment.Centre;
+ public VerticalAlignment VerticalTextAlignment { get; set; } = VerticalAlignment.Centre;
+
+ public abstract Size GetContentSize(IGuiContext context);
+
+ public virtual Size CalculateActualSize(IGuiContext context)
+ {
+ var fixedSize = Size;
+ var desiredSize = GetContentSize(context) + Margin.Size + Padding.Size;
+
+ if (desiredSize.Width < MinWidth)
+ desiredSize.Width = MinWidth;
+
+ if (desiredSize.Height < MinHeight)
+ desiredSize.Height = MinHeight;
+
+ if (desiredSize.Width > MaxWidth)
+ desiredSize.Width = MaxWidth;
+
+ if (desiredSize.Height > MaxWidth)
+ desiredSize.Height = MaxHeight;
+
+ var width = fixedSize.Width == 0 ? desiredSize.Width : fixedSize.Width;
+ var height = fixedSize.Height == 0 ? desiredSize.Height : fixedSize.Height;
+ return new Size(width, height);
+ }
+
+ public virtual void InvalidateMeasure() { }
+
+ private bool _isEnabled;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled != value)
+ {
+ _isEnabled = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+ }
+
+ public bool IsVisible { get; set; }
+
+ private ControlStyle _hoverStyle;
+ public ControlStyle HoverStyle
+ {
+ get => _hoverStyle;
+ set
+ {
+ if (_hoverStyle != value)
+ {
+ _hoverStyle = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ private ControlStyle _disabledStyle;
+ public ControlStyle DisabledStyle
+ {
+ get => _disabledStyle;
+ set
+ {
+ _disabledStyle = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+
+ public virtual void OnScrolled(int delta) { }
+
+ public virtual bool OnKeyTyped(IGuiContext context, KeyboardEventArgs args) { return true; }
+ public virtual bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) { return true; }
+
+ public virtual bool OnFocus(IGuiContext context) { return true; }
+ public virtual bool OnUnfocus(IGuiContext context) { return true; }
+
+ public virtual bool OnPointerDown(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerMove(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerUp(IGuiContext context, PointerEventArgs args) { return true; }
+
+ public virtual bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && !IsHovered)
+ IsHovered = true;
+
+ return true;
+ }
+
+ public virtual bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && IsHovered)
+ IsHovered = false;
+
+ return true;
+ }
+
+ public virtual bool Contains(IGuiContext context, Point point)
+ {
+ return BoundingRectangle.Contains(point);
+ }
+
+ public virtual void Update(IGuiContext context, float deltaSeconds) { }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ if (BackgroundRegion != null)
+ renderer.DrawRegion(BackgroundRegion, BoundingRectangle, BackgroundColor);
+ else if (BackgroundColor != Color.Transparent)
+ renderer.FillRectangle(BoundingRectangle, BackgroundColor);
+
+ if (BorderThickness != 0)
+ renderer.DrawRectangle(BoundingRectangle, BorderColor, BorderThickness);
+
+ // handy debug rectangles
+ //renderer.DrawRectangle(ContentRectangle, Color.Magenta);
+ //renderer.DrawRectangle(BoundingRectangle, Color.Lime);
+ }
+
+ public bool HasParent(Control control)
+ {
+ return Parent != null && (Parent == control || Parent.HasParent(control));
+ }
+
+ protected TextInfo GetTextInfo(IGuiContext context, string text, Rectangle targetRectangle, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.GetStringRectangle(text ?? string.Empty, Vector2.Zero).Size;
+ var destinationRectangle = LayoutHelper.AlignRectangle(horizontalAlignment, verticalAlignment, textSize, targetRectangle);
+ var textPosition = destinationRectangle.Location.ToVector2();
+ var textInfo = new TextInfo(text, font, textPosition, textSize, TextColor, targetRectangle);
+ return textInfo;
+ }
+
+ public struct TextInfo
+ {
+ public TextInfo(string text, BitmapFont font, Vector2 position, Size size, Color color, Rectangle? clippingRectangle)
+ {
+ Text = text ?? string.Empty;
+ Font = font;
+ Size = size;
+ Color = color;
+ ClippingRectangle = clippingRectangle;
+ Position = position;
+ }
+
+ public string Text;
+ public BitmapFont Font;
+ public Size Size;
+ public Color Color;
+ public Rectangle? ClippingRectangle;
+ public Vector2 Position;
+ }
+
+ public Dictionary<string, object> AttachedProperties { get; } = new Dictionary<string, object>();
+
+ public object GetAttachedProperty(string name)
+ {
+ return AttachedProperties.TryGetValue(name, out var value) ? value : null;
+ }
+
+ public void SetAttachedProperty(string name, object value)
+ {
+ AttachedProperties[name] = value;
+ }
+
+ public virtual Type GetAttachedPropertyType(string propertyName)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
new file mode 100644
index 0000000..6135b4c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
@@ -0,0 +1,15 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ControlCollection : ElementCollection<Control, Control>
+ {
+ public ControlCollection()
+ : base(null)
+ {
+ }
+
+ public ControlCollection(Control parent)
+ : base(parent)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
new file mode 100644
index 0000000..26f0fca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Dialog : LayoutControl
+ //{
+ // public Dialog()
+ // {
+ // HorizontalAlignment = HorizontalAlignment.Centre;
+ // VerticalAlignment = VerticalAlignment.Centre;
+ // }
+
+ // public Thickness Padding { get; set; }
+ // public Screen Owner { get; private set; }
+
+ // public void Show(Screen owner)
+ // {
+ // Owner = owner;
+ // Owner.Controls.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Owner.Controls.Remove(this);
+ // }
+
+ // protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // var sizes = Items.Select(control => LayoutHelper.GetSizeWithMargins(control, context, availableSize)).ToArray();
+ // var width = sizes.Max(s => s.Width);
+ // var height = sizes.Max(s => s.Height);
+ // return new Size2(width, height) + Padding.Size;
+ // }
+
+ // public override void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Items)
+ // PlaceControl(context, control, Padding.Left, Padding.Top, Width - Padding.Size.Width, Height - Padding.Size.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
new file mode 100644
index 0000000..7dfe1e6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public enum Dock
+ {
+ Left, Right, Top, Bottom
+ }
+
+ public class DockPanel : LayoutControl
+ {
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var size = new Size();
+
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+ var actualSize = control.CalculateActualSize(context);
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ size.Width += actualSize.Width;
+ size.Height += actualSize.Height;
+ }
+ else
+ {
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ case Dock.Right:
+ size.Width += actualSize.Width;
+ break;
+ case Dock.Top:
+ case Dock.Bottom:
+ size.Height += actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ return size;
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+ else
+ {
+ var actualSize = control.CalculateActualSize(context);
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width;
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Right:
+ PlaceControl(context, control, rectangle.Right - actualSize.Width, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Top:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Dock.Bottom:
+ PlaceControl(context, control, rectangle.Left, rectangle.Bottom - actualSize.Height, rectangle.Width, actualSize.Height);
+ rectangle.Height -= actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+ }
+
+ public const string DockProperty = "Dock";
+
+ public override Type GetAttachedPropertyType(string propertyName)
+ {
+ if (string.Equals(DockProperty, propertyName, StringComparison.OrdinalIgnoreCase))
+ return typeof(Dock);
+
+ return base.GetAttachedPropertyType(propertyName);
+ }
+
+ public bool LastChildFill { get; set; } = true;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
new file mode 100644
index 0000000..97b984d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using MonoGame.Extended.Input.InputListeners;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Form : StackPanel
+ //{
+ // public Form()
+ // {
+ // }
+
+ // public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ // {
+ // if (args.Key == Keys.Tab)
+ // {
+ // var controls = FindControls<Control>();
+ // var index = controls.IndexOf(context.FocusedControl);
+ // if (index > -1)
+ // {
+ // index++;
+ // if (index >= controls.Count) index = 0;
+ // context.SetFocus(controls[index]);
+ // return true;
+ // }
+ // }
+
+ // if (args.Key == Keys.Enter)
+ // {
+ // var controls = FindControls<Submit>();
+ // if (controls.Count > 0)
+ // {
+ // var submit = controls.FirstOrDefault();
+ // submit.TriggerClicked();
+ // return true;
+ // }
+ // }
+
+ // return base.OnKeyPressed(context, args);
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
new file mode 100644
index 0000000..f0babaf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class ItemsControl : Control
+ {
+ protected ItemsControl()
+ {
+ Items = new ControlCollection(this);
+ //{
+ // ItemAdded = x => UpdateRootIsLayoutRequired(),
+ // ItemRemoved = x => UpdateRootIsLayoutRequired()
+ //};
+ }
+
+ public override IEnumerable<Control> Children => Items;
+
+ public ControlCollection Items { get; }
+
+ ///// <summary>
+ ///// Recursive Method to find the root element and update the IsLayoutRequired property. So that the screen knows that something in the controls
+ ///// have had a change to their layout. Also, it will reset the size of the element so that it can get a clean build so that the background patches
+ ///// can be rendered with the updates.
+ ///// </summary>
+ //private void UpdateRootIsLayoutRequired()
+ //{
+ // var parent = Parent as ItemsControl;
+
+ // if (parent == null)
+ // IsLayoutRequired = true;
+ // else
+ // parent.UpdateRootIsLayoutRequired();
+
+ // Size = Size2.Empty;
+ //}
+
+ //protected List<T> FindControls<T>()
+ // where T : Control
+ //{
+ // return FindControls<T>(Items);
+ //}
+
+ //protected List<T> FindControls<T>(ControlCollection controls)
+ // where T : Control
+ //{
+ // var results = new List<T>();
+ // foreach (var control in controls)
+ // {
+ // if (control is T)
+ // results.Add(control as T);
+
+ // var itemsControl = control as ItemsControl;
+
+ // if (itemsControl != null && itemsControl.Items.Any())
+ // results = results.Concat(FindControls<T>(itemsControl.Items)).ToList();
+ // }
+ // return results;
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
new file mode 100644
index 0000000..b949148
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Label : ContentControl
+ {
+ public Label()
+ {
+ }
+
+ public Label(string text = null)
+ {
+ Content = text ?? string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
new file mode 100644
index 0000000..3935baa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
@@ -0,0 +1,40 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class LayoutControl : ItemsControl
+ {
+ protected LayoutControl()
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch;
+ VerticalAlignment = VerticalAlignment.Stretch;
+ BackgroundColor = Color.Transparent;
+ }
+
+ private bool _isLayoutValid;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _isLayoutValid = false;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ base.Update(context, deltaSeconds);
+
+ if (!_isLayoutValid)
+ {
+ Layout(context, new Rectangle(Padding.Left, Padding.Top, ContentRectangle.Width, ContentRectangle.Height));
+ _isLayoutValid = true;
+ }
+ }
+
+ protected abstract void Layout(IGuiContext context, Rectangle rectangle);
+
+ protected static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ LayoutHelper.PlaceControl(context, control, x, y, width, height);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
new file mode 100644
index 0000000..d456e07
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ListBox : SelectorControl
+ {
+ public ListBox()
+ {
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ height += itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ ScrollIntoView(context);
+ DrawItemList(context, renderer);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return ContentRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
new file mode 100644
index 0000000..6983d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ProgressBar : Control
+ {
+ public ProgressBar()
+ {
+ }
+
+ private float _progress = 1.0f;
+ public float Progress
+ {
+ get { return _progress; }
+ set
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if(_progress != value)
+ {
+ _progress = value;
+ ProgressChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public TextureRegion2D BarRegion { get; set; }
+ public Color BarColor { get; set; } = Color.White;
+
+ public event EventHandler ProgressChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(5, 5);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var boundingRectangle = ContentRectangle;
+ var clippingRectangle = new Rectangle(boundingRectangle.X, boundingRectangle.Y, (int)(boundingRectangle.Width * Progress), boundingRectangle.Height);
+
+ if (BarRegion != null)
+ renderer.DrawRegion(BarRegion, BoundingRectangle, BarColor, clippingRectangle);
+ else
+ renderer.FillRectangle(BoundingRectangle, BarColor, clippingRectangle);
+ }
+
+ //protected override void DrawBackground(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // base.DrawBackground(context, renderer, deltaSeconds);
+
+
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
new file mode 100644
index 0000000..49b39ba
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class SelectorControl : Control
+ {
+ protected SelectorControl()
+ {
+ }
+
+ private int _selectedIndex = -1;
+ public virtual int SelectedIndex
+ {
+ get { return _selectedIndex; }
+ set
+ {
+ if (_selectedIndex != value)
+ {
+ _selectedIndex = value;
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children => Items.OfType<Control>();
+
+ public virtual List<object> Items { get; } = new List<object>();
+ public virtual Color SelectedTextColor { get; set; } = Color.White;
+ public virtual Color SelectedItemColor { get; set; } = Color.CornflowerBlue;
+ public virtual Thickness ItemPadding { get; set; } = new Thickness(4, 2);
+ public virtual string NameProperty { get; set; }
+
+ public event EventHandler SelectedIndexChanged;
+
+ protected int FirstIndex;
+
+ public object SelectedItem
+ {
+ get { return SelectedIndex >= 0 && SelectedIndex <= Items.Count - 1 ? Items[SelectedIndex] : null; }
+ set { SelectedIndex = Items.IndexOf(value); }
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Down) ScrollDown();
+ if (args.Key == Keys.Up) ScrollUp();
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override void OnScrolled(int delta)
+ {
+ base.OnScrolled(delta);
+
+ if (delta < 0) ScrollDown();
+ if (delta > 0) ScrollUp();
+ }
+
+ private void ScrollDown()
+ {
+ if (SelectedIndex < Items.Count - 1)
+ SelectedIndex++;
+ }
+
+ private void ScrollUp()
+ {
+ if (SelectedIndex > 0)
+ SelectedIndex--;
+ }
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, contentRectangle);
+
+ if (itemRectangle.Contains(args.Position))
+ {
+ SelectedIndex = i;
+ OnItemClicked(context, args);
+ break;
+ }
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ protected virtual void OnItemClicked(IGuiContext context, PointerEventArgs args) { }
+
+ protected TextInfo GetItemTextInfo(IGuiContext context, Rectangle itemRectangle, object item)
+ {
+ var textRectangle = new Rectangle(itemRectangle.X + ItemPadding.Left, itemRectangle.Y + ItemPadding.Top,
+ itemRectangle.Width - ItemPadding.Right, itemRectangle.Height - ItemPadding.Bottom);
+ var itemTextInfo = GetTextInfo(context, GetItemName(item), textRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ return itemTextInfo;
+ }
+
+ private string GetItemName(object item)
+ {
+ if (item != null)
+ {
+ if (NameProperty != null)
+ {
+ return item.GetType()
+ .GetRuntimeProperty(NameProperty)
+ .GetValue(item)
+ ?.ToString() ?? string.Empty;
+ }
+
+ return item.ToString();
+ }
+
+ return string.Empty;
+ }
+
+ protected Rectangle GetItemRectangle(IGuiContext context, int index, Rectangle contentRectangle)
+ {
+ var font = Font ?? context.DefaultFont;
+ var itemHeight = font.LineHeight + ItemPadding.Top + ItemPadding.Bottom;
+ return new Rectangle(contentRectangle.X, contentRectangle.Y + itemHeight * index, contentRectangle.Width, itemHeight);
+ }
+
+ protected void ScrollIntoView(IGuiContext context)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+ var selectedItemRectangle = GetItemRectangle(context, SelectedIndex - FirstIndex, contentRectangle);
+
+ if (selectedItemRectangle.Bottom > ClippingRectangle.Bottom)
+ FirstIndex++;
+
+ if (selectedItemRectangle.Top < ClippingRectangle.Top && FirstIndex > 0)
+ FirstIndex--;
+ }
+
+ protected Size GetItemSize(IGuiContext context, object item)
+ {
+ var text = GetItemName(item);
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.MeasureString(text ?? string.Empty);
+ var itemWidth = textSize.Width + ItemPadding.Width;
+ var itemHeight = textSize.Height + ItemPadding.Height;
+ return new Size(itemWidth, itemHeight);
+ }
+
+ protected abstract Rectangle GetListAreaRectangle(IGuiContext context);
+
+ protected void DrawItemList(IGuiContext context, IGuiRenderer renderer)
+ {
+ var listRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var item = Items[i];
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, listRectangle);
+ var itemTextInfo = GetItemTextInfo(context, itemRectangle, item);
+ var textColor = i == SelectedIndex ? SelectedTextColor : itemTextInfo.Color;
+
+ if (SelectedIndex == i)
+ renderer.FillRectangle(itemRectangle, SelectedItemColor, listRectangle);
+
+ renderer.DrawText(itemTextInfo.Font, itemTextInfo.Text, itemTextInfo.Position + TextOffset, textColor, itemTextInfo.ClippingRectangle);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
new file mode 100644
index 0000000..5d8926d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
@@ -0,0 +1,69 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class StackPanel : LayoutControl
+ {
+ public StackPanel()
+ {
+ }
+
+ public Orientation Orientation { get; set; } = Orientation.Vertical;
+ public int Spacing { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ width += actualSize.Width;
+ height = actualSize.Height > height ? actualSize.Height : height;
+ break;
+ case Orientation.Vertical:
+ width = actualSize.Width > width ? actualSize.Width : width;
+ height += actualSize.Height;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+
+ width += Orientation == Orientation.Horizontal ? (Items.Count - 1) * Spacing : 0;
+ height += Orientation == Orientation.Vertical ? (Items.Count - 1) * Spacing : 0;
+
+ return new Size(width, height);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Vertical:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height + Spacing;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Orientation.Horizontal:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width + Spacing;
+ rectangle.Width -= actualSize.Width;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
new file mode 100644
index 0000000..75b6940
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
@@ -0,0 +1,331 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public sealed class TextBox : Control
+ {
+ public TextBox(string text = null)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ }
+
+ public TextBox()
+ : this(null)
+ {
+ }
+
+ public int SelectionStart { get; set; }
+ public char? PasswordCharacter { get; set; }
+
+ private string _text;
+
+ public string Text
+ {
+ get { return _text; }
+ set
+ {
+ if (_text != value)
+ {
+ _text = value;
+ OnTextChanged();
+ }
+ }
+ }
+
+ private void OnTextChanged()
+ {
+ if (!string.IsNullOrEmpty(Text) && SelectionStart > Text.Length)
+ SelectionStart = Text.Length;
+
+ TextChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ public event EventHandler TextChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size) font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width,
+ stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ //protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ //{
+ // var font = Font ?? context.DefaultFont;
+ // return new Size2(Width + Padding.Left + Padding.Right, (Height <= 0.0f ? font.LineHeight + 2 : Height) + Padding.Top + Padding.Bottom);
+ //}
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ SelectionStart = FindNearestGlyphIndex(context, args.Position);
+ _isCaretVisible = true;
+
+ //_selectionIndexes.Clear();
+ //_selectionIndexes.Push(SelectionStart);
+ //_startSelectionBox = Text.Length > 0;
+
+ return base.OnPointerDown(context, args);
+ }
+
+ //public override bool OnPointerMove(IGuiContext context, PointerEventArgs args)
+ //{
+ // if (_startSelectionBox)
+ // {
+ // var selection = FindNearestGlyphIndex(context, args.Position);
+ // if (selection != _selectionIndexes.Peek())
+ // {
+ // if (_selectionIndexes.Count == 1)
+ // {
+ // _selectionIndexes.Push(selection);
+ // }
+ // else if (_selectionIndexes.Last() < _selectionIndexes.Peek())
+ // {
+ // if (selection > _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // else
+ // {
+ // if (selection < _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // SelectionStart = selection;
+ // }
+ // }
+
+ // return base.OnPointerMove(context, args);
+ //}
+
+ //public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerLeave(context, args);
+ //}
+
+ //public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerUp(context, args);
+ //}
+
+ private int FindNearestGlyphIndex(IGuiContext context, Point position)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textInfo = GetTextInfo(context, Text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ var i = 0;
+
+ foreach (var glyph in font.GetGlyphs(textInfo.Text, textInfo.Position))
+ {
+ var fontRegionWidth = glyph.FontRegion?.Width ?? 0;
+ var glyphMiddle = (int) (glyph.Position.X + fontRegionWidth * 0.5f);
+
+ if (position.X >= glyphMiddle)
+ {
+ i++;
+ continue;
+ }
+
+ return i;
+ }
+
+ return i;
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ case Keys.Enter:
+ return true;
+ case Keys.Back:
+ if (Text.Length > 0)
+ {
+ if (SelectionStart > 0) // && _selectionIndexes.Count <= 1)
+ {
+ SelectionStart--;
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+ }
+
+ break;
+ case Keys.Delete:
+ if (SelectionStart < Text.Length) // && _selectionIndexes.Count <= 1)
+ {
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+ // SelectionStart = 0; // yeah, nah.
+
+ // _selectionIndexes.Clear();
+ //}
+ break;
+ case Keys.Left:
+ if (SelectionStart > 0)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() < SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart--;
+ }
+ }
+
+ break;
+ case Keys.Right:
+ if (SelectionStart < Text.Length)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() > SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart++;
+ }
+ }
+
+ break;
+ case Keys.Home:
+ SelectionStart = 0;
+ //_selectionIndexes.Clear();
+ break;
+ case Keys.End:
+ SelectionStart = Text.Length;
+ //_selectionIndexes.Clear();
+ break;
+ default:
+ if (args.Character != null)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+
+ Text = Text.Insert(SelectionStart, args.Character.ToString());
+ SelectionStart++;
+ }
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ //private bool _startSelectionBox = false;
+ //private Stack<int> _selectionIndexes = new Stack<int>();
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = PasswordCharacter.HasValue ? new string(PasswordCharacter.Value, Text.Length) : Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color,
+ textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo, SelectionStart);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = 0;
+ // var end = 0;
+ // var point = Point2.Zero;
+ // if (_selectionIndexes.Last() > _selectionIndexes.Peek())
+ // {
+ // start = _selectionIndexes.Peek();
+ // end = _selectionIndexes.Last();
+ // point = caretRectangle.Position;
+ // }
+ // else
+ // {
+ // start = _selectionIndexes.Last();
+ // end = _selectionIndexes.Peek();
+ // point = GetCaretRectangle(textInfo, start).Position;
+ // }
+ // var selectionRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(start, end - start), point);
+
+ // renderer.FillRectangle((Rectangle)selectionRectangle, Color.Black * 0.25f);
+ //}
+ }
+ }
+
+
+ //protected override string CreateBoxText(string text, BitmapFont font, float width)
+ //{
+ // return text;
+ //}
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo, int index)
+ {
+ var caretRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(0, index), textInfo.Position);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right
+ ? caretRectangle.Right
+ : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle) caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
new file mode 100644
index 0000000..e20b621
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
@@ -0,0 +1,328 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class TextBox2 : Control
+ {
+ public TextBox2()
+ : this(null)
+ {
+ }
+
+ public TextBox2(string text)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ VerticalTextAlignment = VerticalAlignment.Top;
+ }
+
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ private readonly List<StringBuilder> _lines = new List<StringBuilder>();
+ public string Text
+ {
+ get => string.Concat(_lines.SelectMany(s => $"{s}\n"));
+ set
+ {
+ _lines.Clear();
+
+ var line = new StringBuilder();
+
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+
+ if (c == '\n')
+ {
+ _lines.Add(line);
+ line = new StringBuilder();
+ }
+ else if(c != '\r')
+ {
+ line.Append(c);
+ }
+ }
+
+ _lines.Add(line);
+ }
+ }
+
+ public int CaretIndex => ColumnIndex * LineIndex + ColumnIndex;
+ public int LineIndex { get; set; }
+ public int ColumnIndex { get; set; }
+ public int TabStops { get; set; } = 4;
+
+ public override IEnumerable<Control> Children => Enumerable.Empty<Control>();
+
+ public string GetLineText(int lineIndex) => _lines[lineIndex].ToString();
+ public int GetLineLength(int lineIndex) => _lines[lineIndex].Length;
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size)font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width, stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ Tab();
+ break;
+ case Keys.Back:
+ Backspace();
+ break;
+ case Keys.Delete:
+ Delete();
+ break;
+ case Keys.Left:
+ Left();
+ break;
+ case Keys.Right:
+ Right();
+ break;
+ case Keys.Up:
+ Up();
+ break;
+ case Keys.Down:
+ Down();
+ break;
+ case Keys.Home:
+ Home();
+ break;
+ case Keys.End:
+ End();
+ break;
+ case Keys.Enter:
+ Type('\n');
+ return true;
+ default:
+ if (args.Character.HasValue)
+ Type(args.Character.Value);
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ public void Type(char c)
+ {
+ switch (c)
+ {
+ case '\n':
+ var lineText = GetLineText(LineIndex);
+ var left = lineText.Substring(0, ColumnIndex);
+ var right = lineText.Substring(ColumnIndex);
+ _lines.Insert(LineIndex + 1, new StringBuilder(right));
+ _lines[LineIndex] = new StringBuilder(left);
+ LineIndex++;
+ Home();
+ break;
+ case '\t':
+ Tab();
+ break;
+ default:
+ _lines[LineIndex].Insert(ColumnIndex, c);
+ ColumnIndex++;
+ break;
+ }
+ }
+
+ public void Backspace()
+ {
+ if (ColumnIndex == 0 && LineIndex > 0)
+ {
+ var topLineLength = GetLineLength(LineIndex - 1);
+
+ if (RemoveLineBreak(LineIndex - 1))
+ {
+ LineIndex--;
+ ColumnIndex = topLineLength;
+ }
+ }
+ else if (Left())
+ {
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+ }
+
+ public void Delete()
+ {
+ var lineLength = GetLineLength(LineIndex);
+
+ if (ColumnIndex == lineLength)
+ RemoveLineBreak(LineIndex);
+ else
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+
+ public void RemoveCharacter(int lineIndex, int columnIndex)
+ {
+ _lines[lineIndex].Remove(columnIndex, 1);
+ }
+
+ public bool RemoveLineBreak(int lineIndex)
+ {
+ if (lineIndex < _lines.Count - 1)
+ {
+ var topLine = _lines[lineIndex];
+ var bottomLine = _lines[lineIndex + 1];
+ _lines.RemoveAt(lineIndex + 1);
+ topLine.Append(bottomLine);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Home()
+ {
+ ColumnIndex = 0;
+ return true;
+ }
+
+ public bool End()
+ {
+ ColumnIndex = GetLineLength(LineIndex);
+ return true;
+ }
+
+ public bool Up()
+ {
+ if (LineIndex > 0)
+ {
+ LineIndex--;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Down()
+ {
+ if (LineIndex < _lines.Count - 1)
+ {
+ LineIndex++;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Left()
+ {
+ if (ColumnIndex == 0)
+ {
+ if (LineIndex == 0)
+ return false;
+
+ LineIndex--;
+ ColumnIndex = GetLineLength(LineIndex);
+ }
+ else
+ {
+ ColumnIndex--;
+ }
+
+ return true;
+ }
+
+ public bool Right()
+ {
+ if (ColumnIndex == _lines[LineIndex].Length)
+ {
+ if (LineIndex == _lines.Count - 1)
+ return false;
+
+ LineIndex++;
+ ColumnIndex = 0;
+ }
+ else
+ {
+ ColumnIndex++;
+ }
+
+ return true;
+ }
+
+ public bool Tab()
+ {
+ var spaces = TabStops - ColumnIndex % TabStops;
+
+ for (var s = 0; s < spaces; s++)
+ Type(' ');
+
+ return spaces > 0;
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+ }
+ }
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo)
+ {
+ var font = textInfo.Font;
+ var text = GetLineText(LineIndex);
+ var offset = new Vector2(0, font.LineHeight * LineIndex);
+ var caretRectangle = font.GetStringRectangle(text.Substring(0, ColumnIndex), textInfo.Position + offset);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right ? caretRectangle.Right : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle)caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
new file mode 100644
index 0000000..5858894
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ToggleButton : Button
+ {
+ public ToggleButton()
+ {
+ }
+
+ public event EventHandler CheckedStateChanged;
+
+ private bool _isChecked;
+ public bool IsChecked
+ {
+ get { return _isChecked; }
+ set
+ {
+ if (_isChecked != value)
+ {
+ _isChecked = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ CheckedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _checkedStyle;
+ public ControlStyle CheckedStyle
+ {
+ get { return _checkedStyle; }
+ set
+ {
+ if (_checkedStyle != value)
+ {
+ _checkedStyle = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ }
+ }
+ }
+
+ private ControlStyle _checkedHoverStyle;
+ public ControlStyle CheckedHoverStyle
+ {
+ get { return _checkedHoverStyle; }
+ set
+ {
+ if (_checkedHoverStyle != value)
+ {
+ _checkedHoverStyle = value;
+ CheckedHoverStyle?.ApplyIf(this, _isChecked && IsHovered);
+ }
+ }
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ base.OnPointerUp(context, args);
+
+ if (BoundingRectangle.Contains(args.Position))
+ {
+ HoverStyle?.Revert(this);
+ CheckedHoverStyle?.Revert(this);
+
+ IsChecked = !IsChecked;
+
+ if (IsChecked)
+ CheckedHoverStyle?.Apply(this);
+ else
+ HoverStyle?.Apply(this);
+ }
+
+ return true;
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Apply(this);
+ return true;
+ }
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Revert(this);
+ return true;
+ }
+
+ return base.OnPointerLeave(context, args);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
new file mode 100644
index 0000000..829969d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class UniformGrid : LayoutControl
+ {
+ public UniformGrid()
+ {
+ }
+
+ public int Columns { get; set; }
+ public int Rows { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context))
+ .ToArray();
+ var minCellWidth = sizes.Max(s => s.Width);
+ var minCellHeight = sizes.Max(s => s.Height);
+ return new Size(minCellWidth * columns, minCellHeight * rows);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ var gridInfo = CalculateGridInfo(context, rectangle.Size);
+ var columnIndex = 0;
+ var rowIndex = 0;
+ var cellWidth = HorizontalAlignment == HorizontalAlignment.Stretch ? gridInfo.MaxCellWidth : gridInfo.MinCellWidth;
+ var cellHeight = VerticalAlignment == VerticalAlignment.Stretch ? gridInfo.MaxCellHeight : gridInfo.MinCellHeight;
+
+ foreach (var control in Items)
+ {
+ var x = columnIndex * cellWidth + rectangle.X;
+ var y = rowIndex * cellHeight + rectangle.Y;
+
+ PlaceControl(context, control, x, y, cellWidth, cellHeight);
+ columnIndex++;
+
+ if (columnIndex > gridInfo.Columns - 1)
+ {
+ columnIndex = 0;
+ rowIndex++;
+ }
+ }
+ }
+
+ private struct GridInfo
+ {
+ public float MinCellWidth;
+ public float MinCellHeight;
+ public float MaxCellWidth;
+ public float MaxCellHeight;
+ public float Columns;
+ public float Rows;
+ public Size2 MinCellSize => new Size2(MinCellWidth * Columns, MinCellHeight * Rows);
+ }
+
+ private GridInfo CalculateGridInfo(IGuiContext context, Size2 availableSize)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var maxCellWidth = availableSize.Width / columns;
+ var maxCellHeight = availableSize.Height / rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context)) // LayoutHelper.GetSizeWithMargins(control, context, new Size2(maxCellWidth, maxCellHeight)))
+ .ToArray();
+ var maxControlWidth = sizes.Length == 0 ? 0 : sizes.Max(s => s.Width);
+ var maxControlHeight = sizes.Length == 0 ? 0 : sizes.Max(s => s.Height);
+
+ return new GridInfo
+ {
+ Columns = columns,
+ Rows = rows,
+ MinCellWidth = Math.Min(maxControlWidth, maxCellWidth),
+ MinCellHeight = Math.Min(maxControlHeight, maxCellHeight),
+ MaxCellWidth = maxCellWidth,
+ MaxCellHeight = maxCellHeight
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
new file mode 100644
index 0000000..638baec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
@@ -0,0 +1,11 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Cursor
+ {
+ public TextureRegion2D TextureRegion { get; set; }
+ public Color Color { get; set; } = Color.White;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
new file mode 100644
index 0000000..0c364a9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Binding
+ {
+ public Binding(object viewModel, string viewModelProperty, string viewProperty)
+ {
+ ViewModel = viewModel;
+ ViewModelProperty = viewModelProperty;
+ ViewProperty = viewProperty;
+ }
+
+ public object ViewModel { get; }
+ public string ViewModelProperty { get; }
+ public string ViewProperty { get; }
+ }
+
+ public abstract class Element
+ {
+ public string Name { get; set; }
+ public Point Position { get; set; }
+ public Point Origin { get; set; }
+ public Color BackgroundColor { get; set; }
+ public Color BorderColor { get; set; } = Color.White;
+ public int BorderThickness { get; set; } = 0;
+
+ private TextureRegion2D _backgroundRegion;
+ public TextureRegion2D BackgroundRegion
+ {
+ get => _backgroundRegion;
+ set
+ {
+ _backgroundRegion = value;
+
+ if (_backgroundRegion != null && BackgroundColor == Color.Transparent)
+ BackgroundColor = Color.White;
+ }
+ }
+
+ public List<Binding> Bindings { get; } = new List<Binding>();
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ foreach (var binding in Bindings)
+ {
+ if (binding.ViewProperty == propertyName)
+ {
+ var value = GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewProperty)
+ .GetValue(this);
+
+ binding.ViewModel
+ .GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewModelProperty)
+ .SetValue(binding.ViewModel, value);
+ }
+ }
+ }
+
+ private Size _size;
+ public Size Size
+ {
+ get => _size;
+ set
+ {
+ _size = value;
+ OnSizeChanged();
+ }
+ }
+
+ protected virtual void OnSizeChanged() { }
+
+ public int MinWidth { get; set; }
+ public int MinHeight { get; set; }
+ public int MaxWidth { get; set; } = int.MaxValue;
+ public int MaxHeight { get; set; } = int.MaxValue;
+
+ public int Width
+ {
+ get => Size.Width;
+ set => Size = new Size(value, Size.Height);
+ }
+
+ public int Height
+ {
+ get => Size.Height;
+ set => Size = new Size(Size.Width, value);
+ }
+
+ public Size ActualSize { get; internal set; }
+
+ public abstract void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds);
+ }
+
+ public abstract class Element<TParent> : Element, IRectangular
+ where TParent : IRectangular
+ {
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public TParent Parent { get; internal set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public Rectangle BoundingRectangle
+ {
+ get
+ {
+ var offset = Point.Zero;
+
+ if (Parent != null)
+ offset = Parent.BoundingRectangle.Location;
+
+ return new Rectangle(offset + Position - ActualSize * Origin, ActualSize);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
new file mode 100644
index 0000000..009161d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public abstract class ElementCollection<TChild, TParent> : IList<TChild>
+ where TParent : class, IRectangular
+ where TChild : Element<TParent>
+ {
+ private readonly TParent _parent;
+ private readonly List<TChild> _list = new List<TChild>();
+
+ public Action<TChild> ItemAdded { get; set; }
+ public Action<TChild> ItemRemoved { get; set; }
+
+ protected ElementCollection(TParent parent)
+ {
+ _parent = parent;
+ }
+
+ public IEnumerator<TChild> GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_list).GetEnumerator();
+ }
+
+ public void Add(TChild item)
+ {
+ item.Parent = _parent;
+ _list.Add(item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void Clear()
+ {
+ foreach (var child in _list)
+ {
+ child.Parent = null;
+ ItemRemoved?.Invoke(child);
+ }
+
+ _list.Clear();
+ }
+
+ public bool Contains(TChild item)
+ {
+ return _list.Contains(item);
+ }
+
+ public void CopyTo(TChild[] array, int arrayIndex)
+ {
+ _list.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(TChild item)
+ {
+ item.Parent = null;
+ ItemRemoved?.Invoke(item);
+ return _list.Remove(item);
+ }
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => ((ICollection<Control>)_list).IsReadOnly;
+
+ public int IndexOf(TChild item)
+ {
+ return _list.IndexOf(item);
+ }
+
+ public void Insert(int index, TChild item)
+ {
+ item.Parent = _parent;
+ _list.Insert(index, item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ var child = _list[index];
+ child.Parent = null;
+ _list.RemoveAt(index);
+ ItemRemoved?.Invoke(child);
+ }
+
+ public TChild this[int index]
+ {
+ get { return _list[index]; }
+ set { _list[index] = value; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
new file mode 100644
index 0000000..6d1e8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiRenderer
+ {
+ void Begin();
+ void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null);
+ void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void End();
+ }
+
+ public class GuiSpriteBatchRenderer : IGuiRenderer
+ {
+ private readonly Func<Matrix> _getTransformMatrix;
+ private readonly SpriteBatch _spriteBatch;
+
+ public GuiSpriteBatchRenderer(GraphicsDevice graphicsDevice, Func<Matrix> getTransformMatrix)
+ {
+ _getTransformMatrix = getTransformMatrix;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public SpriteSortMode SortMode { get; set; }
+ public BlendState BlendState { get; set; } = BlendState.AlphaBlend;
+ public SamplerState SamplerState { get; set; } = SamplerState.PointClamp;
+ public DepthStencilState DepthStencilState { get; set; } = DepthStencilState.Default;
+ public RasterizerState RasterizerState { get; set; } = RasterizerState.CullNone;
+ public Effect Effect { get; set; }
+
+ public void Begin()
+ {
+ _spriteBatch.Begin(SortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect, _getTransformMatrix());
+ }
+
+ public void End()
+ {
+ _spriteBatch.End();
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, rectangle, color, clippingRectangle);
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, position, color, clippingRectangle);
+ }
+
+ public void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ _spriteBatch.DrawString(font, text, position, color, clippingRectangle);
+ }
+
+ public void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.DrawRectangle(rectangle, color, thickness);
+ }
+
+ public void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.FillRectangle(rectangle, color);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
new file mode 100644
index 0000000..8b522fb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
@@ -0,0 +1,265 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.ViewportAdapters;
+using System;
+using System.Linq;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiContext
+ {
+ BitmapFont DefaultFont { get; }
+ Vector2 CursorPosition { get; }
+ Control FocusedControl { get; }
+
+ void SetFocus(Control focusedControl);
+ }
+
+ public class GuiSystem : IGuiContext, IRectangular
+ {
+ private readonly ViewportAdapter _viewportAdapter;
+ private readonly IGuiRenderer _renderer;
+ private readonly MouseListener _mouseListener;
+ private readonly TouchListener _touchListener;
+ private readonly KeyboardListener _keyboardListener;
+
+ private Control _preFocusedControl;
+
+ public GuiSystem(ViewportAdapter viewportAdapter, IGuiRenderer renderer)
+ {
+ _viewportAdapter = viewportAdapter;
+ _renderer = renderer;
+
+ _mouseListener = new MouseListener(viewportAdapter);
+ _mouseListener.MouseDown += (s, e) => OnPointerDown(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseUp += (s, e) => OnPointerUp(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseWheelMoved += (s, e) => FocusedControl?.OnScrolled(e.ScrollWheelDelta);
+
+ _touchListener = new TouchListener(viewportAdapter);
+ _touchListener.TouchStarted += (s, e) => OnPointerDown(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchEnded += (s, e) => OnPointerUp(PointerEventArgs.FromTouchArgs(e));
+
+ _keyboardListener = new KeyboardListener();
+ _keyboardListener.KeyTyped += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyTyped(this, args));
+ _keyboardListener.KeyPressed += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyPressed(this, args));
+ }
+
+ public Control FocusedControl { get; private set; }
+ public Control HoveredControl { get; private set; }
+
+ private Screen _activeScreen;
+ public Screen ActiveScreen
+ {
+ get => _activeScreen;
+ set
+ {
+ if (_activeScreen != value)
+ {
+ _activeScreen = value;
+
+ if(_activeScreen != null)
+ InitializeScreen(_activeScreen);
+ }
+ }
+ }
+
+ public Rectangle BoundingRectangle => _viewportAdapter.BoundingRectangle;
+
+ public Vector2 CursorPosition { get; set; }
+
+ public BitmapFont DefaultFont => Skin.Default?.DefaultFont;
+
+ private void InitializeScreen(Screen screen)
+ {
+ screen.Layout(this, BoundingRectangle);
+ }
+
+ public void ClientSizeChanged()
+ {
+ //ActiveScreen?.Content?.InvalidateMeasure();
+ ActiveScreen?.Layout(this, BoundingRectangle);
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ if(ActiveScreen == null)
+ return;
+
+ _touchListener.Update(gameTime);
+ _mouseListener.Update(gameTime);
+ _keyboardListener.Update(gameTime);
+
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ UpdateControl(ActiveScreen.Content, deltaSeconds);
+
+ //if (ActiveScreen.IsLayoutRequired)
+ // ActiveScreen.Layout(this, BoundingRectangle);
+
+ ActiveScreen.Update(gameTime);
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ _renderer.Begin();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ {
+ DrawControl(ActiveScreen.Content, deltaSeconds);
+ //DrawWindows(ActiveScreen.Windows, deltaSeconds);
+ }
+
+ var cursor = Skin.Default?.Cursor;
+
+ if (cursor != null)
+ _renderer.DrawRegion(cursor.TextureRegion, CursorPosition, cursor.Color);
+
+ _renderer.End();
+ }
+
+ //private void DrawWindows(WindowCollection windows, float deltaSeconds)
+ //{
+ // foreach (var window in windows)
+ // {
+ // window.Draw(this, _renderer, deltaSeconds);
+ // DrawChildren(window.Controls, deltaSeconds);
+ // }
+ //}
+
+ public void UpdateControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Update(this, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ UpdateControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void DrawControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Draw(this, _renderer, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ DrawControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void OnPointerDown(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ _preFocusedControl = FindControlAtPoint(args.Position);
+ PropagateDown(HoveredControl, x => x.OnPointerDown(this, args));
+ }
+
+ private void OnPointerUp(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var postFocusedControl = FindControlAtPoint(args.Position);
+
+ if (_preFocusedControl == postFocusedControl)
+ {
+ SetFocus(postFocusedControl);
+ }
+
+ _preFocusedControl = null;
+ PropagateDown(HoveredControl, x => x.OnPointerUp(this, args));
+ }
+
+ private void OnPointerMoved(PointerEventArgs args)
+ {
+ CursorPosition = args.Position.ToVector2();
+
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var hoveredControl = FindControlAtPoint(args.Position);
+
+ if (HoveredControl != hoveredControl)
+ {
+ if (HoveredControl != null && (hoveredControl == null || !hoveredControl.HasParent(HoveredControl)))
+ PropagateDown(HoveredControl, x => x.OnPointerLeave(this, args));
+
+ HoveredControl = hoveredControl;
+ PropagateDown(HoveredControl, x => x.OnPointerEnter(this, args));
+ }
+ else
+ {
+ PropagateDown(HoveredControl, x => x.OnPointerMove(this, args));
+ }
+ }
+
+ public void SetFocus(Control focusedControl)
+ {
+ if (FocusedControl != focusedControl)
+ {
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = false;
+ PropagateDown(FocusedControl, x => x.OnUnfocus(this));
+ }
+
+ FocusedControl = focusedControl;
+
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = true;
+ PropagateDown(FocusedControl, x => x.OnFocus(this));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Method is meant to loop down the parents control to find a suitable event control. If the predicate returns false
+ /// it will continue down the control tree.
+ /// </summary>
+ /// <param name="control">The control we want to check against</param>
+ /// <param name="predicate">A function to check if the propagation should resume, if returns false it will continue down the tree.</param>
+ private static void PropagateDown(Control control, Func<Control, bool> predicate)
+ {
+ while(control != null && predicate(control))
+ {
+ control = control.Parent;
+ }
+ }
+
+ private Control FindControlAtPoint(Point point)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return null;
+
+ return FindControlAtPoint(ActiveScreen.Content, point);
+ }
+
+ private Control FindControlAtPoint(Control control, Point point)
+ {
+ foreach (var controlChild in control.Children.Reverse())
+ {
+ var c = FindControlAtPoint(controlChild, point);
+
+ if (c != null)
+ return c;
+ }
+
+
+ if (control.IsVisible && control.Contains(this, point))
+ return control;
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
new file mode 100644
index 0000000..1268bff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public enum HorizontalAlignment { Left, Right, Centre, Stretch }
+ public enum VerticalAlignment { Top, Bottom, Centre, Stretch }
+
+ public static class LayoutHelper
+ {
+ public static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ var rectangle = new Rectangle((int)x, (int)y, (int)width, (int)height);
+ var desiredSize = control.CalculateActualSize(context);
+ var alignedRectangle = AlignRectangle(control.HorizontalAlignment, control.VerticalAlignment, desiredSize, rectangle);
+
+ control.Position = new Point(control.Margin.Left + alignedRectangle.X, control.Margin.Top + alignedRectangle.Y);
+ control.ActualSize = (Size)alignedRectangle.Size - control.Margin.Size;
+ control.InvalidateMeasure();
+ }
+
+ public static Rectangle AlignRectangle(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ var x = GetHorizontalPosition(horizontalAlignment, size, targetRectangle);
+ var y = GetVerticalPosition(verticalAlignment, size, targetRectangle);
+ var width = horizontalAlignment == HorizontalAlignment.Stretch ? targetRectangle.Width : size.Width;
+ var height = verticalAlignment == VerticalAlignment.Stretch ? targetRectangle.Height : size.Height;
+
+ return new Rectangle(x, y, width, height);
+ }
+
+ public static int GetHorizontalPosition(HorizontalAlignment horizontalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (horizontalAlignment)
+ {
+ case HorizontalAlignment.Stretch:
+ case HorizontalAlignment.Left:
+ return targetRectangle.X;
+ case HorizontalAlignment.Right:
+ return targetRectangle.Right - size.Width;
+ case HorizontalAlignment.Centre:
+ return targetRectangle.X + targetRectangle.Width / 2 - size.Width / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(horizontalAlignment), horizontalAlignment, $"{horizontalAlignment} is not supported");
+ }
+ }
+
+ public static int GetVerticalPosition(VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (verticalAlignment)
+ {
+ case VerticalAlignment.Stretch:
+ case VerticalAlignment.Top:
+ return targetRectangle.Y;
+ case VerticalAlignment.Bottom:
+ return targetRectangle.Bottom - size.Height;
+ case VerticalAlignment.Centre:
+ return targetRectangle.Y + targetRectangle.Height / 2 - size.Height / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(verticalAlignment), verticalAlignment, $"{verticalAlignment} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
new file mode 100644
index 0000000..c2e8776
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Markup
+{
+ public class MarkupParser
+ {
+ public MarkupParser()
+ {
+ }
+
+ private static readonly Dictionary<string, Type> _controlTypes =
+ typeof(Control).Assembly
+ .ExportedTypes
+ .Where(t => t.IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
+
+ private static readonly Dictionary<Type, Func<string, object>> _converters =
+ new Dictionary<Type, Func<string, object>>
+ {
+ {typeof(object), s => s},
+ {typeof(string), s => s},
+ {typeof(bool), s => bool.Parse(s)},
+ {typeof(int), s => int.Parse(s)},
+ {typeof(Color), s => s.StartsWith("#") ? ColorHelper.FromHex(s) : ColorHelper.FromName(s)}
+ };
+
+ private static object ConvertValue(Type propertyType, string input, object dataContext)
+ {
+ var value = ParseBinding(input, dataContext);
+
+ if (_converters.TryGetValue(propertyType, out var converter))
+ return converter(value); //property.SetValue(control, converter(value));
+
+ if (propertyType.IsEnum)
+ return
+ Enum.Parse(propertyType, value,
+ true); // property.SetValue(control, Enum.Parse(propertyType, value, true));
+
+ throw new InvalidOperationException($"Converter not found for {propertyType}");
+ }
+
+ private static object ParseChildNode(XmlNode node, Control parent, object dataContext)
+ {
+ if (node is XmlText)
+ return node.InnerText.Trim();
+
+ if (_controlTypes.TryGetValue(node.Name, out var type))
+ {
+ var typeInfo = type.GetTypeInfo();
+ var control = (Control) Activator.CreateInstance(type);
+
+ // ReSharper disable once AssignNullToNotNullAttribute
+ foreach (var attribute in node.Attributes.Cast<XmlAttribute>())
+ {
+ var property = typeInfo.GetProperty(attribute.Name);
+
+ if (property != null)
+ {
+ var value = ConvertValue(property.PropertyType, attribute.Value, dataContext);
+ property.SetValue(control, value);
+ }
+ else
+ {
+ var parts = attribute.Name.Split('.');
+ var parentType = parts[0];
+ var propertyName = parts[1];
+ var propertyType = parent.GetAttachedPropertyType(propertyName);
+ var propertyValue = ConvertValue(propertyType, attribute.Value, dataContext);
+
+ if (!string.Equals(parent.GetType().Name, parentType, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException(
+ $"Attached properties are only supported on the immediate parent type {parentType}");
+
+ control.SetAttachedProperty(propertyName, propertyValue);
+ }
+ }
+
+
+ if (node.HasChildNodes)
+ {
+ switch (control)
+ {
+ case ContentControl contentControl:
+ if (node.ChildNodes.Count > 1)
+ throw new InvalidOperationException("A content control can only have one child");
+
+ contentControl.Content = ParseChildNode(node.ChildNodes[0], control, dataContext);
+ break;
+ case LayoutControl layoutControl:
+ foreach (var childControl in ParseChildNodes(node.ChildNodes, control, dataContext))
+ layoutControl.Items.Add(childControl as Control);
+ break;
+ }
+ }
+
+ return control;
+ }
+
+ throw new InvalidOperationException($"Unknown control type {node.Name}");
+ }
+
+ private static string ParseBinding(string expression, object dataContext)
+ {
+ if (dataContext != null && expression.StartsWith("{{") && expression.EndsWith("}}"))
+ {
+ var binding = expression.Substring(2, expression.Length - 4);
+ var bindingValue = dataContext
+ .GetType()
+ .GetProperty(binding)
+ ?.GetValue(dataContext);
+
+ return $"{bindingValue}";
+ }
+
+ return expression;
+ }
+
+ private static IEnumerable<object> ParseChildNodes(XmlNodeList nodes, Control parent, object dataContext)
+ {
+ foreach (var node in nodes.Cast<XmlNode>())
+ {
+ if (node.Name == "xml")
+ {
+ // TODO: Validate header
+ }
+ else
+ {
+ yield return ParseChildNode(node, parent, dataContext);
+ }
+ }
+ }
+
+ public Control Parse(string filePath, object dataContext)
+ {
+ var d = new XmlDocument();
+ d.Load(filePath);
+ return ParseChildNodes(d.ChildNodes, null, dataContext)
+ .LastOrDefault() as Control;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
new file mode 100644
index 0000000..d319cb4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A GUI system for MonoGame written from the ground up to make MonoGame more awesome.</Description>
+ <PackageTags>monogame gui</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Input\MonoGame.Extended.Input.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
new file mode 100644
index 0000000..a12599f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
@@ -0,0 +1,4 @@
+namespace MonoGame.Extended.Gui
+{
+ public enum Orientation { Horizontal, Vertical }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
new file mode 100644
index 0000000..18cffac
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui
+{
+ public class PointerEventArgs : EventArgs
+ {
+ private PointerEventArgs()
+ {
+ }
+
+ public Point Position { get; private set; }
+ public MouseButton Button { get; private set; }
+ public int ScrollWheelDelta { get; private set; }
+ public int ScrollWheelValue { get; private set; }
+ public TimeSpan Time { get; private set; }
+
+ public static PointerEventArgs FromMouseArgs(MouseEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = args.Button,
+ ScrollWheelDelta = args.ScrollWheelDelta,
+ ScrollWheelValue = args.ScrollWheelValue,
+ Time = args.Time
+ };
+ }
+
+ public static PointerEventArgs FromTouchArgs(TouchEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = MouseButton.Left,
+ Time = args.Time
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
new file mode 100644
index 0000000..821a1e2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
@@ -0,0 +1,140 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Screen //: Element<GuiSystem>, IDisposable
+ {
+ public Screen()
+ {
+ //Windows = new WindowCollection(this) { ItemAdded = w => _isLayoutRequired = true };
+ }
+
+ public virtual void Dispose()
+ {
+ }
+
+ private Control _content;
+ [JsonPropertyOrder(1)]
+ public Control Content
+ {
+ get { return _content; }
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _isLayoutRequired = true;
+ }
+ }
+ }
+
+ //[JsonIgnore]
+ //public WindowCollection Windows { get; }
+
+ public float Width { get; private set; }
+ public float Height { get; private set; }
+ public Size2 Size => new Size2(Width, Height);
+ public bool IsVisible { get; set; } = true;
+
+ private bool _isLayoutRequired;
+ [JsonIgnore]
+ public bool IsLayoutRequired => _isLayoutRequired || Content.IsLayoutRequired;
+
+ public virtual void Update(GameTime gameTime)
+ {
+
+ }
+
+ public void Show()
+ {
+ IsVisible = true;
+ }
+
+ public void Hide()
+ {
+ IsVisible = false;
+ }
+
+ public T FindControl<T>(string name)
+ where T : Control
+ {
+ return FindControl<T>(Content, name);
+ }
+
+ private static T FindControl<T>(Control rootControl, string name)
+ where T : Control
+ {
+ if (rootControl.Name == name)
+ return rootControl as T;
+
+ foreach (var childControl in rootControl.Children)
+ {
+ var control = FindControl<T>(childControl, name);
+
+ if (control != null)
+ return control;
+ }
+
+ return null;
+ }
+
+ public void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ Width = rectangle.Width;
+ Height = rectangle.Height;
+
+ LayoutHelper.PlaceControl(context, Content, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ //foreach (var window in Windows)
+ // LayoutHelper.PlaceWindow(context, window, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ _isLayoutRequired = false;
+ Content.IsLayoutRequired = false;
+ }
+
+ //public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // renderer.DrawRectangle(BoundingRectangle, Color.Green);
+ //}
+
+ public static Screen FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+
+ public static TScreen FromStream<TScreen>(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ var skinService = new SkinService();
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ options.Converters.Add(new SkinJsonConverter(contentManager, skinService, customControlTypes));
+ options.Converters.Add(new ControlJsonConverter(skinService, customControlTypes));
+ return JsonSerializer.Deserialize<TScreen>(stream, options);
+ }
+
+ public static Screen FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static TScreen FromFile<TScreen>(ContentManager contentManager, string path, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<TScreen>(contentManager, stream, customControlTypes);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
new file mode 100644
index 0000000..0572728
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class ScreenCollection : ElementCollection<Screen, GuiSystem>
+ //{
+ // public ScreenCollection(GuiSystem parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
new file mode 100644
index 0000000..a9c71f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlJsonConverter : JsonConverter<Control>
+ {
+ private readonly IGuiSkinService _guiSkinService;
+ private readonly ControlStyleJsonConverter _styleConverter;
+ private const string _styleProperty = "Style";
+
+ public ControlJsonConverter(IGuiSkinService guiSkinService, params Type[] customControlTypes)
+ {
+ _guiSkinService = guiSkinService;
+ _styleConverter = new ControlStyleJsonConverter(customControlTypes);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Control);
+
+ /// <inheritdoc />
+ public override Control Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var style = _styleConverter.Read(ref reader, typeToConvert, options);
+ var template = GetControlTemplate(style);
+ var skin = _guiSkinService.Skin;
+ var control = skin.Create(style.TargetType, template);
+
+ var itemsControl = control as ItemsControl;
+ if (itemsControl != null)
+ {
+ object childControls;
+
+ if (style.TryGetValue(nameof(ItemsControl.Items), out childControls))
+ {
+ var controlCollection = childControls as ControlCollection;
+
+ if (controlCollection != null)
+ {
+ foreach (var child in controlCollection)
+ itemsControl.Items.Add(child);
+ }
+ }
+ }
+
+ style.Apply(control);
+ return control;
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Control value, JsonSerializerOptions options) { }
+
+
+
+ private static string GetControlTemplate(ControlStyle style)
+ {
+ object template;
+
+ if (style.TryGetValue(_styleProperty, out template))
+ return template as string;
+
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
new file mode 100644
index 0000000..c31549b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlStyleJsonConverter : JsonConverter<ControlStyle>
+ {
+ private readonly Dictionary<string, Type> _controlTypes;
+ private const string _typeProperty = "Type";
+ private const string _nameProperty = "Name";
+
+ public ControlStyleJsonConverter(params Type[] customControlTypes)
+ {
+ _controlTypes = typeof(Control)
+ .GetTypeInfo()
+ .Assembly
+ .ExportedTypes
+ .Concat(customControlTypes)
+ .Where(t => t.GetTypeInfo().IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ControlStyle);
+
+ /// <inheritdoc />
+ public override ControlStyle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var dictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options);
+ var name = dictionary.GetValueOrDefault(_nameProperty) as string;
+ var typeName = dictionary.GetValueOrDefault(_typeProperty) as string;
+
+ if (!_controlTypes.TryGetValue(typeName, out Type controlType))
+ throw new FormatException("invalid control type: " + typeName);
+
+ var targetType = typeName != null ? controlType : typeof(Control);
+ var properties = targetType
+ .GetRuntimeProperties()
+ .ToDictionary(p => p.Name);
+ var style = new ControlStyle(name, targetType);
+
+ foreach (var keyValuePair in dictionary.Where(i => i.Key != _typeProperty))
+ {
+ var propertyName = keyValuePair.Key;
+ var rawValue = keyValuePair.Value;
+
+ PropertyInfo propertyInfo;
+ var value = properties.TryGetValue(propertyName, out propertyInfo)
+ ? DeserializeValueAs(rawValue, propertyInfo.PropertyType)
+ : DeserializeValueAs(rawValue, typeof(object));
+
+ style.Add(propertyName, value);
+ }
+
+ return style;
+ }
+
+ private static object DeserializeValueAs(object value, Type type)
+ {
+ var json = JsonSerializer.Serialize(value, type);
+ return JsonSerializer.Deserialize(json, type);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, ControlStyle value, JsonSerializerOptions options)
+ {
+ var style = (ControlStyle)value;
+ var dictionary = new Dictionary<string, object> { [_typeProperty] = style.TargetType.Name };
+
+ foreach (var keyValuePair in style)
+ dictionary.Add(keyValuePair.Key, keyValuePair.Value);
+
+ JsonSerializer.Serialize(writer, dictionary);
+ }
+
+
+
+
+
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..5ebc880
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public static class GuiJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ContentManager contentManager, params Type[] customControlTypes)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ var textureRegionService = new GuiTextureRegionService();
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new SizeJsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new ThicknessJsonConverter());
+ options.Converters.Add(new ContentManagerJsonConverter<BitmapFont>(contentManager, font => font.Name));
+ options.Converters.Add(new ControlStyleJsonConverter(customControlTypes));
+ options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService));
+ options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new VerticalAlignmentConverter());
+ options.Converters.Add(new HorizontalAlignmentConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
new file mode 100644
index 0000000..9be58e5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
@@ -0,0 +1,15 @@
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiNinePatchRegion2DJsonConverter : NinePatchRegion2DJsonConverter
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiNinePatchRegion2DJsonConverter(IGuiTextureRegionService textureRegionService)
+ : base(textureRegionService)
+ {
+ _textureRegionService = textureRegionService;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
new file mode 100644
index 0000000..a0617cf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiTextureAtlasJsonConverter : ContentManagerJsonConverter<TextureAtlas>
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiTextureAtlasJsonConverter(ContentManager contentManager, IGuiTextureRegionService textureRegionService)
+ : base(contentManager, atlas => atlas.Name)
+ {
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var textureAtlas = base.Read(ref reader, typeToConvert, options);
+ if (textureAtlas is not null)
+ {
+ _textureRegionService.TextureAtlases.Add(textureAtlas);
+ }
+
+ return textureAtlas;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
new file mode 100644
index 0000000..cf9ab9e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public interface IGuiTextureRegionService : ITextureRegionService
+ {
+ IList<TextureAtlas> TextureAtlases { get; }
+ IList<NinePatchRegion2D> NinePatches { get; }
+ }
+
+ public class GuiTextureRegionService : TextureRegionService, IGuiTextureRegionService
+ {
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
new file mode 100644
index 0000000..a696528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class HorizontalAlignmentConverter : JsonConverter<HorizontalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HorizontalAlignment);
+
+ /// <inheritdoc />
+ public override HorizontalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return HorizontalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<HorizontalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(HorizontalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, HorizontalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
new file mode 100644
index 0000000..016ea6d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public interface IGuiSkinService
+{
+ Skin Skin { get; set; }
+}
+
+public class SkinService : IGuiSkinService
+{
+ public Skin Skin { get; set; }
+}
+
+public class SkinJsonConverter : JsonConverter<Skin>
+{
+ private readonly ContentManager _contentManager;
+ private readonly IGuiSkinService _skinService;
+ private readonly Type[] _customControlTypes;
+
+ public SkinJsonConverter(ContentManager contentManager, IGuiSkinService skinService, params Type[] customControlTypes)
+ {
+ _contentManager = contentManager;
+ _skinService = skinService;
+ _customControlTypes = customControlTypes;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Skin);
+
+ /// <inheritdoc />
+ public override Skin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var assetName = reader.GetString();
+
+ // TODO: Load this using the ContentManager instead.
+ using (var stream = TitleContainer.OpenStream(assetName))
+ {
+ var skin = Skin.FromStream(_contentManager, stream, _customControlTypes);
+ _skinService.Skin = skin;
+ return skin;
+ }
+
+ }
+
+ throw new InvalidOperationException($"{nameof(SkinJsonConverter)} can only convert from a string");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Skin value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
new file mode 100644
index 0000000..bc55cda
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class VerticalAlignmentConverter : JsonConverter<VerticalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(VerticalAlignment);
+
+ /// <inheritdoc />
+ public override VerticalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return VerticalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<VerticalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(VerticalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, VerticalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
new file mode 100644
index 0000000..77476be
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MonoGame.Extended.BitmapFonts;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+using MonoGame.Extended.TextureAtlases;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Skin
+ {
+ public Skin()
+ {
+ TextureAtlases = new List<TextureAtlas>();
+ Fonts = new List<BitmapFont>();
+ NinePatches = new List<NinePatchRegion2D>();
+ Styles = new KeyedCollection<string, ControlStyle>(s => s.Name ?? s.TargetType.Name);
+ }
+
+ [JsonPropertyOrder(0)]
+ public string Name { get; set; }
+
+ [JsonPropertyOrder(1)]
+ public IList<TextureAtlas> TextureAtlases { get; set; }
+
+ [JsonPropertyOrder(2)]
+ public IList<BitmapFont> Fonts { get; set; }
+
+ [JsonPropertyOrder(3)]
+ public IList<NinePatchRegion2D> NinePatches { get; set; }
+
+ [JsonPropertyOrder(4)]
+ public BitmapFont DefaultFont => Fonts.FirstOrDefault();
+
+ [JsonPropertyOrder(5)]
+ public Cursor Cursor { get; set; }
+
+ [JsonPropertyOrder(6)]
+ public KeyedCollection<string, ControlStyle> Styles { get; private set; }
+
+ public ControlStyle GetStyle(string name)
+ {
+ if (Styles.TryGetValue(name, out var controlStyle))
+ return controlStyle;
+
+ return null;
+ }
+
+ public ControlStyle GetStyle(Type controlType)
+ {
+ return GetStyle(controlType.FullName);
+ }
+
+ public void Apply(Control control)
+ {
+ // TODO: This allocates memory on each apply because it needs to apply styles in reverse
+ var types = new List<Type>();
+ var controlType = control.GetType();
+
+ while (controlType != null)
+ {
+ types.Add(controlType);
+ controlType = controlType.GetTypeInfo().BaseType;
+ }
+
+ for (var i = types.Count - 1; i >= 0; i--)
+ {
+ var style = GetStyle(types[i]);
+ style?.Apply(control);
+ }
+ }
+
+ public static Skin FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static Skin FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ return JsonSerializer.Deserialize<Skin>(stream, options);
+ }
+
+
+ public T Create<T>(string template, Action<T> onCreate)
+ where T : Control, new()
+ {
+ var control = new T();
+ GetStyle(template).Apply(control);
+ onCreate(control);
+ return control;
+ }
+
+ public Control Create(Type type, string template)
+ {
+ var control = (Control)Activator.CreateInstance(type);
+
+ if (template != null)
+ {
+ var style = GetStyle(template);
+ if (style != null)
+ style.Apply(control);
+ else
+ throw new FormatException($"invalid style {template} for control {type.Name}");
+ }
+
+ return control;
+ }
+
+ public static Skin Default { get; set; }
+
+ public static Skin CreateDefault(BitmapFont font)
+ {
+ Default = new Skin
+ {
+ Fonts = { font },
+ Styles =
+ {
+ new ControlStyle(typeof(Control)) {
+ {nameof(Control.BackgroundColor), new Color(51, 51, 55)},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 1},
+ {nameof(Control.TextColor), new Color(241, 241, 241)},
+ {nameof(Control.Padding), new Thickness(5)},
+ {nameof(Control.DisabledStyle), new ControlStyle(typeof(Control)) {
+ { nameof(Control.TextColor), new Color(78,78,80) }
+ }
+ }
+ },
+ new ControlStyle(typeof(LayoutControl)) {
+ {nameof(Control.BackgroundColor), Color.Transparent},
+ {nameof(Control.BorderColor), Color.Transparent },
+ {nameof(Control.BorderThickness), 0},
+ {nameof(Control.Padding), new Thickness(0)},
+ {nameof(Control.Margin), new Thickness(0)},
+ },
+ new ControlStyle(typeof(ComboBox)) {
+ {nameof(ComboBox.DropDownColor), new Color(71, 71, 75)},
+ {nameof(ComboBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ComboBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(CheckBox))
+ {
+ {nameof(CheckBox.HorizontalTextAlignment), HorizontalAlignment.Left },
+ {nameof(CheckBox.BorderThickness), 0},
+ {nameof(CheckBox.BackgroundColor), Color.Transparent},
+ },
+ new ControlStyle(typeof(ListBox))
+ {
+ {nameof(ListBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ListBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(Label)) {
+ {nameof(Label.BackgroundColor), Color.Transparent},
+ {nameof(Label.TextColor), Color.White},
+ {nameof(Label.BorderColor), Color.Transparent},
+ {nameof(Label.BorderThickness), 0},
+ {nameof(Label.HorizontalTextAlignment), HorizontalAlignment.Left},
+ {nameof(Label.VerticalTextAlignment), VerticalAlignment.Bottom},
+ {nameof(Control.Margin), new Thickness(5,0)},
+ {nameof(Control.Padding), new Thickness(0)},
+ },
+ new ControlStyle(typeof(TextBox)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(TextBox2)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(Button)) {
+ {
+ nameof(Button.HoverStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(62, 62, 64)},
+ {nameof(Button.BorderColor), Color.WhiteSmoke }
+ }
+ },
+ {
+ nameof(Button.PressedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ }
+ },
+ new ControlStyle(typeof(ToggleButton)) {
+ {
+ nameof(ToggleButton.CheckedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ },
+ {
+ nameof(ToggleButton.CheckedHoverStyle), new ControlStyle {
+ {nameof(Button.BorderColor), Color.WhiteSmoke}
+ }
+ }
+ },
+ new ControlStyle(typeof(ProgressBar)) {
+ {nameof(ProgressBar.BarColor), new Color(0, 122, 204) },
+ {nameof(ProgressBar.Height), 32 },
+ {nameof(ProgressBar.Padding), new Thickness(5, 4)},
+ }
+ }
+ };
+ return Default;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
new file mode 100644
index 0000000..12f4dcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ //public class Window : Element<Screen>
+ //{
+ // public Window(Screen parent)
+ // {
+ // Parent = parent;
+ // }
+
+ // public ControlCollection Controls { get; } = new ControlCollection();
+
+ // public void Show()
+ // {
+ // Parent.Windows.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Parent.Windows.Remove(this);
+ // }
+
+ // public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ // {
+ // renderer.FillRectangle(BoundingRectangle, Color.Magenta);
+ // }
+
+ // public Size2 GetDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // return new Size2(Width, Height);
+ // }
+
+ // public void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Controls)
+ // LayoutHelper.PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
new file mode 100644
index 0000000..d22f274
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class WindowCollection : ElementCollection<Window, Screen>
+ //{
+ // public WindowCollection(Screen parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs
new file mode 100644
index 0000000..a153f02
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input
+{
+ /// <summary>Player index enumeration with slots for 8 players</summary>
+ public enum ExtendedPlayerIndex
+ {
+ /// <summary>First player</summary>
+ One = PlayerIndex.One,
+
+ /// <summary>Second player</summary>
+ Two = PlayerIndex.Two,
+
+ /// <summary>Third player</summary>
+ Three = PlayerIndex.Three,
+
+ /// <summary>Fourth player</summary>
+ Four = PlayerIndex.Four,
+
+ /// <summary>Fifth player</summary>
+ Five,
+
+ /// <summary>Sixth player</summary>
+ Six,
+
+ /// <summary>Seventh player</summary>
+ Seven,
+
+ /// <summary>Eigth player</summary>
+ Eight
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs
new file mode 100644
index 0000000..9b113c6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs
@@ -0,0 +1,62 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This class contains all information resulting from events fired by
+ /// <see cref="GamePadListener" />.
+ /// </summary>
+ public class GamePadEventArgs : EventArgs
+ {
+ public GamePadEventArgs(GamePadState previousState, GamePadState currentState,
+ TimeSpan elapsedTime, PlayerIndex playerIndex, Buttons? button = null,
+ float triggerState = 0, Vector2? thumbStickState = null)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = previousState;
+ CurrentState = currentState;
+ ElapsedTime = elapsedTime;
+ if (button != null)
+ Button = button.Value;
+ TriggerState = triggerState;
+ ThumbStickState = thumbStickState ?? Vector2.Zero;
+ }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; private set; }
+
+ /// <summary>
+ /// The state of the controller in the previous update.
+ /// </summary>
+ public GamePadState PreviousState { get; private set; }
+
+ /// <summary>
+ /// The state of the controller in this update.
+ /// </summary>
+ public GamePadState CurrentState { get; private set; }
+
+ /// <summary>
+ /// The button that triggered this event, if appliable.
+ /// </summary>
+ public Buttons Button { get; private set; }
+
+ /// <summary>
+ /// The time elapsed since last event.
+ /// </summary>
+ public TimeSpan ElapsedTime { get; private set; }
+
+ /// <summary>
+ /// If a TriggerMoved event, displays the responsible trigger's position.
+ /// </summary>
+ public float TriggerState { get; private set; }
+
+ /// <summary>
+ /// If a ThumbStickMoved event, displays the responsible stick's position.
+ /// </summary>
+ public Vector2 ThumbStickState { get; private set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs
new file mode 100644
index 0000000..b7ea79b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs
@@ -0,0 +1,529 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This is a listener that exposes several events for easier handling of gamepads.
+ /// </summary>
+ public class GamePadListener : InputListener
+ {
+ private static readonly bool[] _gamePadConnections = new bool[4];
+
+ // These buttons are not to be evaluated normally, but with the debounce filter
+ // in their respective methods.
+ private readonly Buttons[] _excludedButtons =
+ {
+ Buttons.LeftTrigger, Buttons.RightTrigger,
+ Buttons.LeftThumbstickDown, Buttons.LeftThumbstickUp, Buttons.LeftThumbstickRight,
+ Buttons.LeftThumbstickLeft,
+ Buttons.RightThumbstickLeft, Buttons.RightThumbstickRight, Buttons.RightThumbstickUp,
+ Buttons.RightThumbstickDown
+ };
+
+ private GamePadState _currentState;
+ //private int _lastPacketNumber;
+ // Implementation doesn't work, see explanation in CheckAllButtons().
+ private GameTime _gameTime;
+ private Buttons _lastButton;
+ private Buttons _lastLeftStickDirection;
+ private Buttons _lastRightStickDirection;
+ private GamePadState _lastThumbStickState;
+
+ private GamePadState _lastTriggerState;
+
+ private float _leftCurVibrationStrength;
+ private bool _leftStickDown;
+ private bool _leftTriggerDown;
+ private bool _leftVibrating;
+ private GameTime _previousGameTime;
+ private GamePadState _previousState;
+ private int _repeatedButtonTimer;
+ private float _rightCurVibrationStrength;
+ private bool _rightStickDown;
+ private bool _rightTriggerDown;
+ private bool _rightVibrating;
+ private TimeSpan _vibrationDurationLeft;
+ private TimeSpan _vibrationDurationRight;
+ private TimeSpan _vibrationStart;
+
+ private float _vibrationStrengthLeft;
+ private float _vibrationStrengthRight;
+
+ public GamePadListener()
+ : this(new GamePadListenerSettings())
+ {
+ }
+
+ public GamePadListener(GamePadListenerSettings settings)
+ {
+ PlayerIndex = settings.PlayerIndex;
+ VibrationEnabled = settings.VibrationEnabled;
+ VibrationStrengthLeft = settings.VibrationStrengthLeft;
+ VibrationStrengthRight = settings.VibrationStrengthRight;
+ ThumbStickDeltaTreshold = settings.ThumbStickDeltaTreshold;
+ ThumbstickDownTreshold = settings.ThumbstickDownTreshold;
+ TriggerDeltaTreshold = settings.TriggerDeltaTreshold;
+ TriggerDownTreshold = settings.TriggerDownTreshold;
+ RepeatInitialDelay = settings.RepeatInitialDelay;
+ RepeatDelay = settings.RepeatDelay;
+
+ _previousGameTime = new GameTime();
+ _previousState = GamePadState.Default;
+ }
+
+ /// <summary>
+ /// If set to true, the static event <see cref="ControllerConnectionChanged" />
+ /// will fire when any controller changes in connectivity status.
+ /// <para>
+ /// This functionality requires that you have one actively updating
+ /// <see cref="InputListenerManager" />.
+ /// </para>
+ /// </summary>
+ public static bool CheckControllerConnections { get; set; }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; }
+
+ /// <summary>
+ /// When a button is held down, the interval in which
+ /// ButtonRepeated fires. Value in milliseconds.
+ /// </summary>
+ public int RepeatDelay { get; }
+
+ /// <summary>
+ /// The amount of time a button has to be held down
+ /// in order to fire ButtonRepeated the first time.
+ /// Value in milliseconds.
+ /// </summary>
+ public int RepeatInitialDelay { get; }
+
+ /// <summary>
+ /// Whether vibration is enabled for this controller.
+ /// </summary>
+ public bool VibrationEnabled { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthLeft
+ {
+ get { return _vibrationStrengthLeft; }
+ // Clamp the value, just to be sure.
+ set { _vibrationStrengthLeft = MathHelper.Clamp(value, 0, 1); }
+ }
+
+ /// <summary>
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthRight
+ {
+ get { return _vibrationStrengthRight; }
+ // Clamp the value, just to be sure.
+ set { _vibrationStrengthRight = MathHelper.Clamp(value, 0, 1); }
+ }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the trigger's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the trigger's position is not 0f.
+ /// </summary>
+ public float TriggerDeltaTreshold { get; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the thumbstick's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the thumbstick's position is not {x:0, y:0}.
+ /// </summary>
+ public float ThumbStickDeltaTreshold { get; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float TriggerDownTreshold { get; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float ThumbstickDownTreshold { get; }
+
+ /// <summary>
+ /// This event fires whenever a controller connects or disconnects.
+ /// <para>
+ /// In order
+ /// for it to work, the <see cref="CheckControllerConnections" /> property must
+ /// be set to true.
+ /// </para>
+ /// </summary>
+ public static event EventHandler<GamePadEventArgs> ControllerConnectionChanged;
+
+ /// <summary>
+ /// This event fires whenever a button changes from the Up
+ /// to the Down state.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonDown;
+
+ /// <summary>
+ /// This event fires whenever a button changes from the Down
+ /// to the Up state.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonUp;
+
+ /// <summary>
+ /// This event fires repeatedly whenever a button is held sufficiently
+ /// long. Use this for things like menu navigation.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonRepeated;
+
+ /// <summary>
+ /// This event fires whenever a thumbstick changes position.
+ /// <para>
+ /// The parameter governing the sensitivity of this functionality
+ /// is <see cref="GamePadListenerSettings.ThumbStickDeltaTreshold" />.
+ /// </para>
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ThumbStickMoved;
+
+ /// <summary>
+ /// This event fires whenever a trigger changes position.
+ /// <para>
+ /// The parameter governing the sensitivity of this functionality
+ /// is <see cref="GamePadListenerSettings.TriggerDeltaTreshold" />.
+ /// </para>
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> TriggerMoved;
+
+
+ /// <summary>
+ /// Send a vibration command to the controller.
+ /// Returns true if the operation succeeded.
+ /// <para>
+ /// Motor values that are unset preserve
+ /// their current vibration strength and duration.
+ /// </para>
+ /// Note: Vibration currently only works on select platforms,
+ /// like Monogame.Windows.
+ /// </summary>
+ /// <param name="durationMs">Duration of the vibration in milliseconds.</param>
+ /// <param name="leftStrength">
+ /// The strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// </param>
+ /// <param name="rightStrength">
+ /// The strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// </param>
+ /// <returns>Returns true if the operation succeeded.</returns>
+ public bool Vibrate(int durationMs, float leftStrength = float.NegativeInfinity,
+ float rightStrength = float.NegativeInfinity)
+ {
+ if (!VibrationEnabled)
+ return false;
+
+ var lstrength = MathHelper.Clamp(leftStrength, 0, 1);
+ var rstrength = MathHelper.Clamp(rightStrength, 0, 1);
+
+ if (float.IsNegativeInfinity(leftStrength))
+ lstrength = _leftCurVibrationStrength;
+ if (float.IsNegativeInfinity(rightStrength))
+ rstrength = _rightCurVibrationStrength;
+
+ var success = GamePad.SetVibration(PlayerIndex, lstrength*VibrationStrengthLeft,
+ rstrength*VibrationStrengthRight);
+ if (success)
+ {
+ _leftVibrating = true;
+ _rightVibrating = true;
+
+ if (leftStrength > 0)
+ _vibrationDurationLeft = new TimeSpan(0, 0, 0, 0, durationMs);
+ else
+ {
+ if (lstrength > 0)
+ _vibrationDurationLeft -= _gameTime.TotalGameTime - _vibrationStart;
+ else
+ _leftVibrating = false;
+ }
+
+ if (rightStrength > 0)
+ _vibrationDurationRight = new TimeSpan(0, 0, 0, 0, durationMs);
+ else
+ {
+ if (rstrength > 0)
+ _vibrationDurationRight -= _gameTime.TotalGameTime - _vibrationStart;
+ else
+ _rightVibrating = false;
+ }
+
+ _vibrationStart = _gameTime.TotalGameTime;
+
+ _leftCurVibrationStrength = lstrength;
+ _rightCurVibrationStrength = rstrength;
+ }
+ return success;
+ }
+
+ private void CheckAllButtons()
+ {
+ // PacketNumber only and always changes if there is a difference between GamePadStates.
+ // ...At least, that's the theory. It doesn't seem to be implemented. Disabled for now.
+ //if (_lastPacketNumber == _currentState.PacketNumber)
+ // return;
+ foreach (Buttons button in Enum.GetValues(typeof(Buttons)))
+ {
+ if (_excludedButtons.Contains(button))
+ break;
+ if (_currentState.IsButtonDown(button) && _previousState.IsButtonUp(button))
+ RaiseButtonDown(button);
+ if (_currentState.IsButtonUp(button) && _previousState.IsButtonDown(button))
+ RaiseButtonUp(button);
+ }
+
+ // Checks triggers as buttons and floats
+ CheckTriggers(s => s.Triggers.Left, Buttons.LeftTrigger);
+ CheckTriggers(s => s.Triggers.Right, Buttons.RightTrigger);
+
+ // Checks thumbsticks as vector2s
+ CheckThumbSticks(s => s.ThumbSticks.Right, Buttons.RightStick);
+ CheckThumbSticks(s => s.ThumbSticks.Left, Buttons.LeftStick);
+ }
+
+ private void CheckTriggers(Func<GamePadState, float> getButtonState, Buttons button)
+ {
+ var debounce = 0.05f; // Value used to qualify a trigger as coming Up from a Down state
+ var curstate = getButtonState(_currentState);
+ var curdown = curstate > TriggerDownTreshold;
+ var prevdown = button == Buttons.RightTrigger ? _rightTriggerDown : _leftTriggerDown;
+
+ if (!prevdown && curdown)
+ {
+ RaiseButtonDown(button);
+ if (button == Buttons.RightTrigger)
+ _rightTriggerDown = true;
+ else
+ _leftTriggerDown = true;
+ }
+ else
+ {
+ if (prevdown && (curstate < debounce))
+ {
+ RaiseButtonUp(button);
+ if (button == Buttons.RightTrigger)
+ _rightTriggerDown = false;
+ else
+ _leftTriggerDown = false;
+ }
+ }
+
+ var prevstate = getButtonState(_lastTriggerState);
+ if (curstate > TriggerDeltaTreshold)
+ {
+ if (Math.Abs(prevstate - curstate) >= TriggerDeltaTreshold)
+ {
+ TriggerMoved?.Invoke(this, MakeArgs(button, curstate));
+ _lastTriggerState = _currentState;
+ }
+ }
+ else
+ {
+ if (prevstate > TriggerDeltaTreshold)
+ {
+ TriggerMoved?.Invoke(this, MakeArgs(button, curstate));
+ _lastTriggerState = _currentState;
+ }
+ }
+ }
+
+ private void CheckThumbSticks(Func<GamePadState, Vector2> getButtonState, Buttons button)
+ {
+ const float debounce = 0.15f;
+ var curVector = getButtonState(_currentState);
+ var curdown = curVector.Length() > ThumbstickDownTreshold;
+ var right = button == Buttons.RightStick;
+ var prevdown = right ? _rightStickDown : _leftStickDown;
+
+ var prevdir = button == Buttons.RightStick ? _lastRightStickDirection : _lastLeftStickDirection;
+ Buttons curdir;
+ if (curVector.Y > curVector.X)
+ {
+ if (curVector.Y > -curVector.X)
+ curdir = right ? Buttons.RightThumbstickUp : Buttons.LeftThumbstickUp;
+ else
+ curdir = right ? Buttons.RightThumbstickLeft : Buttons.LeftThumbstickLeft;
+ }
+ else
+ {
+ if (curVector.Y < -curVector.X)
+ curdir = right ? Buttons.RightThumbstickDown : Buttons.LeftThumbstickDown;
+ else
+ curdir = right ? Buttons.RightThumbstickRight : Buttons.LeftThumbstickRight;
+ }
+
+ if (!prevdown && curdown)
+ {
+ if (right)
+ _lastRightStickDirection = curdir;
+ else
+ _lastLeftStickDirection = curdir;
+
+ RaiseButtonDown(curdir);
+ if (button == Buttons.RightStick)
+ _rightStickDown = true;
+ else
+ _leftStickDown = true;
+ }
+ else
+ {
+ if (prevdown && (curVector.Length() < debounce))
+ {
+ RaiseButtonUp(prevdir);
+ if (button == Buttons.RightStick)
+ _rightStickDown = false;
+ else
+ _leftStickDown = false;
+ }
+ else
+ {
+ if (prevdown && curdown && (curdir != prevdir))
+ {
+ RaiseButtonUp(prevdir);
+ if (right)
+ _lastRightStickDirection = curdir;
+ else
+ _lastLeftStickDirection = curdir;
+ RaiseButtonDown(curdir);
+ }
+ }
+ }
+
+ var prevVector = getButtonState(_lastThumbStickState);
+ if (curVector.Length() > ThumbStickDeltaTreshold)
+ {
+ if (Vector2.Distance(curVector, prevVector) >= ThumbStickDeltaTreshold)
+ {
+ ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector));
+ _lastThumbStickState = _currentState;
+ }
+ }
+ else
+ {
+ if (prevVector.Length() > ThumbStickDeltaTreshold)
+ {
+ ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector));
+ _lastThumbStickState = _currentState;
+ }
+ }
+ }
+
+ internal static void CheckConnections()
+ {
+ if (!CheckControllerConnections)
+ return;
+
+ foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex)))
+ {
+ if (GamePad.GetState(index).IsConnected ^ _gamePadConnections[(int) index])
+ // We need more XORs in this world
+ {
+ _gamePadConnections[(int) index] = !_gamePadConnections[(int) index];
+ ControllerConnectionChanged?.Invoke(null,
+ new GamePadEventArgs(GamePadState.Default, GamePad.GetState(index), TimeSpan.Zero, index));
+ }
+ }
+ }
+
+ private void CheckVibrate()
+ {
+ if (_leftVibrating && (_vibrationStart + _vibrationDurationLeft < _gameTime.TotalGameTime))
+ Vibrate(0, 0);
+ if (_rightVibrating && (_vibrationStart + _vibrationDurationRight < _gameTime.TotalGameTime))
+ Vibrate(0, rightStrength: 0);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _gameTime = gameTime;
+ _currentState = GamePad.GetState(PlayerIndex);
+ CheckVibrate();
+ if (!_currentState.IsConnected)
+ return;
+ CheckAllButtons();
+ CheckRepeatButton();
+ //_lastPacketNumber = _currentState.PacketNumber;
+ _previousGameTime = gameTime;
+ _previousState = _currentState;
+ }
+
+ private GamePadEventArgs MakeArgs(Buttons? button,
+ float triggerstate = 0, Vector2? thumbStickState = null)
+ {
+ var elapsedTime = _gameTime.TotalGameTime - _previousGameTime.TotalGameTime;
+ return new GamePadEventArgs(_previousState, _currentState,
+ elapsedTime, PlayerIndex, button, triggerstate, thumbStickState);
+ }
+
+ private void RaiseButtonDown(Buttons button)
+ {
+ ButtonDown?.Invoke(this, MakeArgs(button));
+ ButtonRepeated?.Invoke(this, MakeArgs(button));
+ _lastButton = button;
+ _repeatedButtonTimer = 0;
+ }
+
+ private void RaiseButtonUp(Buttons button)
+ {
+ ButtonUp?.Invoke(this, MakeArgs(button));
+ _lastButton = 0;
+ }
+
+ private void CheckRepeatButton()
+ {
+ _repeatedButtonTimer += _gameTime.ElapsedGameTime.Milliseconds;
+
+ if ((_repeatedButtonTimer < RepeatInitialDelay) || (_lastButton == 0))
+ return;
+
+ if (_repeatedButtonTimer < RepeatInitialDelay + RepeatDelay)
+ {
+ ButtonRepeated?.Invoke(this, MakeArgs(_lastButton));
+ _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay;
+ }
+ else
+ {
+ if (_repeatedButtonTimer > RepeatInitialDelay + RepeatDelay*2)
+ {
+ ButtonRepeated?.Invoke(this, MakeArgs(_lastButton));
+ _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs
new file mode 100644
index 0000000..8c36e4c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs
@@ -0,0 +1,134 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />.
+ /// </summary>
+ /// <seealso cref="InputListenerManager" />
+ public class GamePadListenerSettings : InputListenerSettings<GamePadListener>
+ {
+ public GamePadListenerSettings()
+ : this(PlayerIndex.One)
+ {
+ }
+
+ /// <summary>
+ /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />.
+ /// <para>Note: There are a number of extra settings that are settable properties.</para>
+ /// </summary>
+ /// <param name="playerIndex">The index of the controller the listener will be tied to.</param>
+ /// <param name="vibrationEnabled">Whether vibration is enabled on the controller.</param>
+ /// <param name="vibrationStrengthLeft">
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </param>
+ /// <param name="vibrationStrengthRight">
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </param>
+ public GamePadListenerSettings(PlayerIndex playerIndex, bool vibrationEnabled = true,
+ float vibrationStrengthLeft = 1.0f, float vibrationStrengthRight = 1.0f)
+ {
+ PlayerIndex = playerIndex;
+ VibrationEnabled = vibrationEnabled;
+ VibrationStrengthLeft = vibrationStrengthLeft;
+ VibrationStrengthRight = vibrationStrengthRight;
+ TriggerDownTreshold = 0.15f;
+ ThumbstickDownTreshold = 0.5f;
+ RepeatInitialDelay = 500;
+ RepeatDelay = 50;
+ }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; set; }
+
+ /// <summary>
+ /// When a button is held down, the interval in which
+ /// ButtonRepeated fires. Value in milliseconds.
+ /// </summary>
+ public int RepeatDelay { get; set; }
+
+ /// <summary>
+ /// The amount of time a button has to be held down
+ /// in order to fire ButtonRepeated the first time.
+ /// Value in milliseconds.
+ /// </summary>
+ public int RepeatInitialDelay { get; set; }
+
+
+ /// <summary>
+ /// Whether vibration is enabled for this controller.
+ /// </summary>
+ public bool VibrationEnabled { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthLeft { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthRight { get; set; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the trigger's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the trigger's position is not 0f.
+ /// </summary>
+ public float TriggerDeltaTreshold { get; set; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the thumbstick's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the thumbstick's position is not {x:0, y:0}.
+ /// </summary>
+ public float ThumbStickDeltaTreshold { get; set; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float TriggerDownTreshold { get; set; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float ThumbstickDownTreshold { get; private set; }
+
+ public override GamePadListener CreateListener()
+ {
+ return new GamePadListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs
new file mode 100644
index 0000000..46198ec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs
@@ -0,0 +1,13 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public interface IInputService
+ {
+ KeyboardListener GuiKeyboardListener { get; }
+
+ MouseListener GuiMouseListener { get; }
+
+ GamePadListener GuiGamePadListener { get; }
+
+ TouchListener GuiTouchListener { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs
new file mode 100644
index 0000000..6323295
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs
@@ -0,0 +1,13 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public abstract class InputListener
+ {
+ protected InputListener()
+ {
+ }
+
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs
new file mode 100644
index 0000000..302f6b5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class InputListenerComponent : GameComponent, IUpdate
+ {
+ private readonly List<InputListener> _listeners;
+
+ public InputListenerComponent(Game game)
+ : base(game)
+ {
+ _listeners = new List<InputListener>();
+ }
+
+ public InputListenerComponent(Game game, params InputListener[] listeners)
+ : base(game)
+ {
+ _listeners = new List<InputListener>(listeners);
+ }
+
+ public IList<InputListener> Listeners => _listeners;
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ if (Game.IsActive)
+ {
+ foreach (var listener in _listeners)
+ listener.Update(gameTime);
+ }
+
+ GamePadListener.CheckConnections();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs
new file mode 100644
index 0000000..468a30b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public abstract class InputListenerSettings<T>
+ where T : InputListener
+ {
+ public abstract T CreateListener();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs
new file mode 100644
index 0000000..d6d01ab
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs
@@ -0,0 +1,119 @@
+using System;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardEventArgs : EventArgs
+ {
+ public KeyboardEventArgs(Keys key, KeyboardState keyboardState)
+ {
+ Key = key;
+
+ Modifiers = KeyboardModifiers.None;
+
+ if (keyboardState.IsKeyDown(Keys.LeftControl) || keyboardState.IsKeyDown(Keys.RightControl))
+ Modifiers |= KeyboardModifiers.Control;
+
+ if (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift))
+ Modifiers |= KeyboardModifiers.Shift;
+
+ if (keyboardState.IsKeyDown(Keys.LeftAlt) || keyboardState.IsKeyDown(Keys.RightAlt))
+ Modifiers |= KeyboardModifiers.Alt;
+ }
+
+ public Keys Key { get; }
+ public KeyboardModifiers Modifiers { get; }
+
+ public char? Character => ToChar(Key, Modifiers);
+
+ private static char? ToChar(Keys key, KeyboardModifiers modifiers = KeyboardModifiers.None)
+ {
+ var isShiftDown = (modifiers & KeyboardModifiers.Shift) == KeyboardModifiers.Shift;
+
+ if (key == Keys.A) return isShiftDown ? 'A' : 'a';
+ if (key == Keys.B) return isShiftDown ? 'B' : 'b';
+ if (key == Keys.C) return isShiftDown ? 'C' : 'c';
+ if (key == Keys.D) return isShiftDown ? 'D' : 'd';
+ if (key == Keys.E) return isShiftDown ? 'E' : 'e';
+ if (key == Keys.F) return isShiftDown ? 'F' : 'f';
+ if (key == Keys.G) return isShiftDown ? 'G' : 'g';
+ if (key == Keys.H) return isShiftDown ? 'H' : 'h';
+ if (key == Keys.I) return isShiftDown ? 'I' : 'i';
+ if (key == Keys.J) return isShiftDown ? 'J' : 'j';
+ if (key == Keys.K) return isShiftDown ? 'K' : 'k';
+ if (key == Keys.L) return isShiftDown ? 'L' : 'l';
+ if (key == Keys.M) return isShiftDown ? 'M' : 'm';
+ if (key == Keys.N) return isShiftDown ? 'N' : 'n';
+ if (key == Keys.O) return isShiftDown ? 'O' : 'o';
+ if (key == Keys.P) return isShiftDown ? 'P' : 'p';
+ if (key == Keys.Q) return isShiftDown ? 'Q' : 'q';
+ if (key == Keys.R) return isShiftDown ? 'R' : 'r';
+ if (key == Keys.S) return isShiftDown ? 'S' : 's';
+ if (key == Keys.T) return isShiftDown ? 'T' : 't';
+ if (key == Keys.U) return isShiftDown ? 'U' : 'u';
+ if (key == Keys.V) return isShiftDown ? 'V' : 'v';
+ if (key == Keys.W) return isShiftDown ? 'W' : 'w';
+ if (key == Keys.X) return isShiftDown ? 'X' : 'x';
+ if (key == Keys.Y) return isShiftDown ? 'Y' : 'y';
+ if (key == Keys.Z) return isShiftDown ? 'Z' : 'z';
+
+ if (((key == Keys.D0) && !isShiftDown) || (key == Keys.NumPad0)) return '0';
+ if (((key == Keys.D1) && !isShiftDown) || (key == Keys.NumPad1)) return '1';
+ if (((key == Keys.D2) && !isShiftDown) || (key == Keys.NumPad2)) return '2';
+ if (((key == Keys.D3) && !isShiftDown) || (key == Keys.NumPad3)) return '3';
+ if (((key == Keys.D4) && !isShiftDown) || (key == Keys.NumPad4)) return '4';
+ if (((key == Keys.D5) && !isShiftDown) || (key == Keys.NumPad5)) return '5';
+ if (((key == Keys.D6) && !isShiftDown) || (key == Keys.NumPad6)) return '6';
+ if (((key == Keys.D7) && !isShiftDown) || (key == Keys.NumPad7)) return '7';
+ if (((key == Keys.D8) && !isShiftDown) || (key == Keys.NumPad8)) return '8';
+ if (((key == Keys.D9) && !isShiftDown) || (key == Keys.NumPad9)) return '9';
+
+ if ((key == Keys.D0) && isShiftDown) return ')';
+ if ((key == Keys.D1) && isShiftDown) return '!';
+ if ((key == Keys.D2) && isShiftDown) return '@';
+ if ((key == Keys.D3) && isShiftDown) return '#';
+ if ((key == Keys.D4) && isShiftDown) return '$';
+ if ((key == Keys.D5) && isShiftDown) return '%';
+ if ((key == Keys.D6) && isShiftDown) return '^';
+ if ((key == Keys.D7) && isShiftDown) return '&';
+ if ((key == Keys.D8) && isShiftDown) return '*';
+ if ((key == Keys.D9) && isShiftDown) return '(';
+
+ if (key == Keys.Space) return ' ';
+ if (key == Keys.Tab) return '\t';
+ if (key == Keys.Enter) return (char) 13;
+ if (key == Keys.Back) return (char) 8;
+
+ if (key == Keys.Add) return '+';
+ if (key == Keys.Decimal) return '.';
+ if (key == Keys.Divide) return '/';
+ if (key == Keys.Multiply) return '*';
+ if (key == Keys.OemBackslash) return '\\';
+ if ((key == Keys.OemComma) && !isShiftDown) return ',';
+ if ((key == Keys.OemComma) && isShiftDown) return '<';
+ if ((key == Keys.OemOpenBrackets) && !isShiftDown) return '[';
+ if ((key == Keys.OemOpenBrackets) && isShiftDown) return '{';
+ if ((key == Keys.OemCloseBrackets) && !isShiftDown) return ']';
+ if ((key == Keys.OemCloseBrackets) && isShiftDown) return '}';
+ if ((key == Keys.OemPeriod) && !isShiftDown) return '.';
+ if ((key == Keys.OemPeriod) && isShiftDown) return '>';
+ if ((key == Keys.OemPipe) && !isShiftDown) return '\\';
+ if ((key == Keys.OemPipe) && isShiftDown) return '|';
+ if ((key == Keys.OemPlus) && !isShiftDown) return '=';
+ if ((key == Keys.OemPlus) && isShiftDown) return '+';
+ if ((key == Keys.OemMinus) && !isShiftDown) return '-';
+ if ((key == Keys.OemMinus) && isShiftDown) return '_';
+ if ((key == Keys.OemQuestion) && !isShiftDown) return '/';
+ if ((key == Keys.OemQuestion) && isShiftDown) return '?';
+ if ((key == Keys.OemQuotes) && !isShiftDown) return '\'';
+ if ((key == Keys.OemQuotes) && isShiftDown) return '"';
+ if ((key == Keys.OemSemicolon) && !isShiftDown) return ';';
+ if ((key == Keys.OemSemicolon) && isShiftDown) return ':';
+ if ((key == Keys.OemTilde) && !isShiftDown) return '`';
+ if ((key == Keys.OemTilde) && isShiftDown) return '~';
+ if (key == Keys.Subtract) return '-';
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs
new file mode 100644
index 0000000..9fc85a0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardListener : InputListener
+ {
+ private Array _keysValues = Enum.GetValues(typeof(Keys));
+
+ private bool _isInitial;
+ private TimeSpan _lastPressTime;
+
+ private Keys _previousKey;
+ private KeyboardState _previousState;
+
+ public KeyboardListener()
+ : this(new KeyboardListenerSettings())
+ {
+ }
+
+ public KeyboardListener(KeyboardListenerSettings settings)
+ {
+ RepeatPress = settings.RepeatPress;
+ InitialDelay = settings.InitialDelayMilliseconds;
+ RepeatDelay = settings.RepeatDelayMilliseconds;
+ }
+
+ public bool RepeatPress { get; }
+ public int InitialDelay { get; }
+ public int RepeatDelay { get; }
+
+ public event EventHandler<KeyboardEventArgs> KeyTyped;
+ public event EventHandler<KeyboardEventArgs> KeyPressed;
+ public event EventHandler<KeyboardEventArgs> KeyReleased;
+
+ public override void Update(GameTime gameTime)
+ {
+ var currentState = Keyboard.GetState();
+
+ RaisePressedEvents(gameTime, currentState);
+ RaiseReleasedEvents(currentState);
+
+ if (RepeatPress)
+ RaiseRepeatEvents(gameTime, currentState);
+
+ _previousState = currentState;
+ }
+
+ private void RaisePressedEvents(GameTime gameTime, KeyboardState currentState)
+ {
+ if (!currentState.IsKeyDown(Keys.LeftAlt) && !currentState.IsKeyDown(Keys.RightAlt))
+ {
+ var pressedKeys = _keysValues
+ .Cast<Keys>()
+ .Where(key => currentState.IsKeyDown(key) && _previousState.IsKeyUp(key));
+
+ foreach (var key in pressedKeys)
+ {
+ var args = new KeyboardEventArgs(key, currentState);
+
+ KeyPressed?.Invoke(this, args);
+
+ if (args.Character.HasValue)
+ KeyTyped?.Invoke(this, args);
+
+ _previousKey = key;
+ _lastPressTime = gameTime.TotalGameTime;
+ _isInitial = true;
+ }
+ }
+ }
+
+ private void RaiseReleasedEvents(KeyboardState currentState)
+ {
+ var releasedKeys = _keysValues
+ .Cast<Keys>()
+ .Where(key => currentState.IsKeyUp(key) && _previousState.IsKeyDown(key));
+
+ foreach (var key in releasedKeys)
+ KeyReleased?.Invoke(this, new KeyboardEventArgs(key, currentState));
+ }
+
+ private void RaiseRepeatEvents(GameTime gameTime, KeyboardState currentState)
+ {
+ var elapsedTime = (gameTime.TotalGameTime - _lastPressTime).TotalMilliseconds;
+
+ if (currentState.IsKeyDown(_previousKey) &&
+ (_isInitial && elapsedTime > InitialDelay || !_isInitial && elapsedTime > RepeatDelay))
+ {
+ var args = new KeyboardEventArgs(_previousKey, currentState);
+
+ KeyPressed?.Invoke(this, args);
+
+ if (args.Character.HasValue)
+ KeyTyped?.Invoke(this, args);
+
+ _lastPressTime = gameTime.TotalGameTime;
+ _isInitial = false;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs
new file mode 100644
index 0000000..86481ed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardListenerSettings : InputListenerSettings<KeyboardListener>
+ {
+ public KeyboardListenerSettings()
+ {
+ RepeatPress = true;
+ InitialDelayMilliseconds = 800;
+ RepeatDelayMilliseconds = 50;
+ }
+
+ public bool RepeatPress { get; set; }
+ public int InitialDelayMilliseconds { get; set; }
+ public int RepeatDelayMilliseconds { get; set; }
+
+ public override KeyboardListener CreateListener()
+ {
+ return new KeyboardListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs
new file mode 100644
index 0000000..de59905
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ [Flags]
+ public enum KeyboardModifiers
+ {
+ Control = 1,
+ Shift = 2,
+ Alt = 4,
+ None = 0
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs
new file mode 100644
index 0000000..2717325
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs
@@ -0,0 +1,35 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class MouseEventArgs : EventArgs
+ {
+ public MouseEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, MouseState previousState,
+ MouseState currentState,
+ MouseButton button = MouseButton.None)
+ {
+ PreviousState = previousState;
+ CurrentState = currentState;
+ Position = viewportAdapter?.PointToScreen(currentState.X, currentState.Y)
+ ?? new Point(currentState.X, currentState.Y);
+ Button = button;
+ ScrollWheelValue = currentState.ScrollWheelValue;
+ ScrollWheelDelta = currentState.ScrollWheelValue - previousState.ScrollWheelValue;
+ Time = time;
+ }
+
+ public TimeSpan Time { get; }
+
+ public MouseState PreviousState { get; }
+ public MouseState CurrentState { get; }
+ public Point Position { get; }
+ public MouseButton Button { get; }
+ public int ScrollWheelValue { get; }
+ public int ScrollWheelDelta { get; }
+
+ public Vector2 DistanceMoved => CurrentState.Position.ToVector2() - PreviousState.Position.ToVector2();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs
new file mode 100644
index 0000000..e71a67f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs
@@ -0,0 +1,193 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// Handles mouse input.
+ /// </summary>
+ /// <remarks>
+ /// Due to nature of the listener, even when game is not in focus, listener will continue to be updated.
+ /// To avoid that, manual pause of Update() method is required whenever game loses focus.
+ /// To avoid having to do it manually, register listener to <see cref="InputListenerComponent" />
+ /// </remarks>
+ public class MouseListener : InputListener
+ {
+ private MouseState _currentState;
+ private bool _dragging;
+ private GameTime _gameTime;
+ private bool _hasDoubleClicked;
+ private MouseEventArgs _mouseDownArgs;
+ private MouseEventArgs _previousClickArgs;
+ private MouseState _previousState;
+
+ public MouseListener()
+ : this(new MouseListenerSettings())
+ {
+ }
+
+ public MouseListener(ViewportAdapter viewportAdapter)
+ : this(new MouseListenerSettings())
+ {
+ ViewportAdapter = viewportAdapter;
+ }
+
+ public MouseListener(MouseListenerSettings settings)
+ {
+ ViewportAdapter = settings.ViewportAdapter;
+ DoubleClickMilliseconds = settings.DoubleClickMilliseconds;
+ DragThreshold = settings.DragThreshold;
+ }
+
+ public ViewportAdapter ViewportAdapter { get; }
+
+ public int DoubleClickMilliseconds { get; }
+ public int DragThreshold { get; }
+
+ /// <summary>
+ /// Returns true if the mouse has moved between the current and previous frames.
+ /// </summary>
+ /// <value><c>true</c> if the mouse has moved; otherwise, <c>false</c>.</value>
+ public bool HasMouseMoved => (_previousState.X != _currentState.X) || (_previousState.Y != _currentState.Y);
+
+ public event EventHandler<MouseEventArgs> MouseDown;
+ public event EventHandler<MouseEventArgs> MouseUp;
+ public event EventHandler<MouseEventArgs> MouseClicked;
+ public event EventHandler<MouseEventArgs> MouseDoubleClicked;
+ public event EventHandler<MouseEventArgs> MouseMoved;
+ public event EventHandler<MouseEventArgs> MouseWheelMoved;
+ public event EventHandler<MouseEventArgs> MouseDragStart;
+ public event EventHandler<MouseEventArgs> MouseDrag;
+ public event EventHandler<MouseEventArgs> MouseDragEnd;
+
+ private void CheckButtonPressed(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Pressed) &&
+ (getButtonState(_previousState) == ButtonState.Released))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ MouseDown?.Invoke(this, args);
+ _mouseDownArgs = args;
+
+ if (_previousClickArgs != null)
+ {
+ // If the last click was recent
+ var clickMilliseconds = (args.Time - _previousClickArgs.Time).TotalMilliseconds;
+
+ if (clickMilliseconds <= DoubleClickMilliseconds)
+ {
+ MouseDoubleClicked?.Invoke(this, args);
+ _hasDoubleClicked = true;
+ }
+
+ _previousClickArgs = null;
+ }
+ }
+ }
+
+ private void CheckButtonReleased(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Released) &&
+ (getButtonState(_previousState) == ButtonState.Pressed))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ if (_mouseDownArgs.Button == args.Button)
+ {
+ var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position);
+
+ // If the mouse hasn't moved much between mouse down and mouse up
+ if (clickMovement < DragThreshold)
+ {
+ if (!_hasDoubleClicked)
+ MouseClicked?.Invoke(this, args);
+ }
+ else // If the mouse has moved between mouse down and mouse up
+ {
+ MouseDragEnd?.Invoke(this, args);
+ _dragging = false;
+ }
+ }
+
+ MouseUp?.Invoke(this, args);
+
+ _hasDoubleClicked = false;
+ _previousClickArgs = args;
+ }
+ }
+
+ private void CheckMouseDragged(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Pressed) &&
+ (getButtonState(_previousState) == ButtonState.Pressed))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ if (_mouseDownArgs.Button == args.Button)
+ {
+ if (_dragging)
+ MouseDrag?.Invoke(this, args);
+ else
+ {
+ // Only start to drag based on DragThreshold
+ var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position);
+
+ if (clickMovement > DragThreshold)
+ {
+ _dragging = true;
+ MouseDragStart?.Invoke(this, args);
+ }
+ }
+ }
+ }
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _gameTime = gameTime;
+ _currentState = Mouse.GetState();
+
+ CheckButtonPressed(s => s.LeftButton, MouseButton.Left);
+ CheckButtonPressed(s => s.MiddleButton, MouseButton.Middle);
+ CheckButtonPressed(s => s.RightButton, MouseButton.Right);
+ CheckButtonPressed(s => s.XButton1, MouseButton.XButton1);
+ CheckButtonPressed(s => s.XButton2, MouseButton.XButton2);
+
+ CheckButtonReleased(s => s.LeftButton, MouseButton.Left);
+ CheckButtonReleased(s => s.MiddleButton, MouseButton.Middle);
+ CheckButtonReleased(s => s.RightButton, MouseButton.Right);
+ CheckButtonReleased(s => s.XButton1, MouseButton.XButton1);
+ CheckButtonReleased(s => s.XButton2, MouseButton.XButton2);
+
+ // Check for any sort of mouse movement.
+ if (HasMouseMoved)
+ {
+ MouseMoved?.Invoke(this,
+ new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState));
+
+ CheckMouseDragged(s => s.LeftButton, MouseButton.Left);
+ CheckMouseDragged(s => s.MiddleButton, MouseButton.Middle);
+ CheckMouseDragged(s => s.RightButton, MouseButton.Right);
+ CheckMouseDragged(s => s.XButton1, MouseButton.XButton1);
+ CheckMouseDragged(s => s.XButton2, MouseButton.XButton2);
+ }
+
+ // Handle mouse wheel events.
+ if (_previousState.ScrollWheelValue != _currentState.ScrollWheelValue)
+ {
+ MouseWheelMoved?.Invoke(this,
+ new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState));
+ }
+
+ _previousState = _currentState;
+ }
+
+ private static int DistanceBetween(Point a, Point b)
+ {
+ return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs
new file mode 100644
index 0000000..1c0ca3d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs
@@ -0,0 +1,23 @@
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class MouseListenerSettings : InputListenerSettings<MouseListener>
+ {
+ public MouseListenerSettings()
+ {
+ // initial values are windows defaults
+ DoubleClickMilliseconds = 500;
+ DragThreshold = 2;
+ }
+
+ public int DragThreshold { get; set; }
+ public int DoubleClickMilliseconds { get; set; }
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public override MouseListener CreateListener()
+ {
+ return new MouseListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs
new file mode 100644
index 0000000..8172885
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchEventArgs : EventArgs
+ {
+ public TouchEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, TouchLocation location)
+ {
+ ViewportAdapter = viewportAdapter;
+ RawTouchLocation = location;
+ Time = time;
+ Position = viewportAdapter?.PointToScreen((int)location.Position.X, (int)location.Position.Y)
+ ?? location.Position.ToPoint();
+ }
+
+ public ViewportAdapter ViewportAdapter { get; }
+ public TouchLocation RawTouchLocation { get; }
+ public TimeSpan Time { get; }
+ public Point Position { get; }
+
+ public override bool Equals(object other)
+ {
+ var args = other as TouchEventArgs;
+
+ if (args == null)
+ return false;
+
+ return ReferenceEquals(this, args) || RawTouchLocation.Id.Equals(args.RawTouchLocation.Id);
+ }
+
+ public override int GetHashCode()
+ {
+ return RawTouchLocation.Id.GetHashCode();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs
new file mode 100644
index 0000000..2a89cc9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs
@@ -0,0 +1,57 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchListener : InputListener
+ {
+ public TouchListener()
+ : this(new TouchListenerSettings())
+ {
+ }
+
+ public TouchListener(ViewportAdapter viewportAdapter)
+ : this(new TouchListenerSettings())
+ {
+ ViewportAdapter = viewportAdapter;
+ }
+
+ public TouchListener(TouchListenerSettings settings)
+ {
+ ViewportAdapter = settings.ViewportAdapter;
+ }
+
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public event EventHandler<TouchEventArgs> TouchStarted;
+ public event EventHandler<TouchEventArgs> TouchEnded;
+ public event EventHandler<TouchEventArgs> TouchMoved;
+ public event EventHandler<TouchEventArgs> TouchCancelled;
+
+ public override void Update(GameTime gameTime)
+ {
+ var touchCollection = TouchPanel.GetState();
+
+ foreach (var touchLocation in touchCollection)
+ {
+ switch (touchLocation.State)
+ {
+ case TouchLocationState.Pressed:
+ TouchStarted?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Moved:
+ TouchMoved?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Released:
+ TouchEnded?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Invalid:
+ TouchCancelled?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs
new file mode 100644
index 0000000..6d42b42
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs
@@ -0,0 +1,18 @@
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchListenerSettings : InputListenerSettings<TouchListener>
+ {
+ public TouchListenerSettings()
+ {
+ }
+
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public override TouchListener CreateListener()
+ {
+ return new TouchListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs
new file mode 100644
index 0000000..0ed7c76
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public static class KeyboardExtended
+ {
+ // TODO: This global static state was a horrible idea.
+ private static KeyboardState _currentKeyboardState;
+ private static KeyboardState _previousKeyboardState;
+
+ public static KeyboardStateExtended GetState()
+ {
+ return new KeyboardStateExtended(_currentKeyboardState, _previousKeyboardState);
+ }
+
+ public static void Refresh()
+ {
+ _previousKeyboardState = _currentKeyboardState;
+ _currentKeyboardState = Keyboard.GetState();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs
new file mode 100644
index 0000000..ee18677
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public struct KeyboardStateExtended
+ {
+ private KeyboardState _currentKeyboardState;
+ private KeyboardState _previousKeyboardState;
+
+ public KeyboardStateExtended(KeyboardState currentKeyboardState, KeyboardState previousKeyboardState)
+ {
+ _currentKeyboardState = currentKeyboardState;
+ _previousKeyboardState = previousKeyboardState;
+ }
+
+ public bool CapsLock => _currentKeyboardState.CapsLock;
+ public bool NumLock => _currentKeyboardState.NumLock;
+ public bool IsShiftDown() => _currentKeyboardState.IsKeyDown(Keys.LeftShift) || _currentKeyboardState.IsKeyDown(Keys.RightShift);
+ public bool IsControlDown() => _currentKeyboardState.IsKeyDown(Keys.LeftControl) || _currentKeyboardState.IsKeyDown(Keys.RightControl);
+ public bool IsAltDown() => _currentKeyboardState.IsKeyDown(Keys.LeftAlt) || _currentKeyboardState.IsKeyDown(Keys.RightAlt);
+ public bool IsKeyDown(Keys key) => _currentKeyboardState.IsKeyDown(key);
+ public bool IsKeyUp(Keys key) => _currentKeyboardState.IsKeyUp(key);
+ public Keys[] GetPressedKeys() => _currentKeyboardState.GetPressedKeys();
+ public void GetPressedKeys(Keys[] keys) => _currentKeyboardState.GetPressedKeys(keys);
+
+ /// <summary>
+ /// Gets whether the given key was down on the previous state, but is now up.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was released this state-change, otherwise false.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsKeyReleased)}")]
+ public bool WasKeyJustDown(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
+
+ /// <summary>
+ /// Gets whether the given key was up on the previous state, but is now down.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was pressed this state-change, otherwise false.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsKeyPressed)}")]
+ public bool WasKeyJustUp(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
+
+ /// <summary>
+ /// Gets whether the given key was down on the previous state, but is now up.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was released this state-change, otherwise false.</returns>
+ public readonly bool IsKeyReleased(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
+
+ /// <summary>
+ /// Gets whether the given key was up on the previous state, but is now down.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was pressed this state-change, otherwise false.</returns>
+ public readonly bool IsKeyPressed(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
+
+ public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeyCount() > 0;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj
new file mode 100644
index 0000000..4b3bd82
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>An event based input system to MonoGame more awesome.</Description>
+ <PackageTags>monogame input event based listeners</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs
new file mode 100644
index 0000000..e4a00f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace MonoGame.Extended.Input
+{
+ [Flags]
+ public enum MouseButton
+ {
+ None = 0,
+ Left = 1,
+ Middle = 2,
+ Right = 4,
+ XButton1 = 8,
+ XButton2 = 16
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs
new file mode 100644
index 0000000..61d6d18
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public static class MouseExtended
+ {
+ // TODO: This global static state was a horrible idea.
+ private static MouseState _currentMouseState;
+ private static MouseState _previousMouseState;
+
+ public static MouseStateExtended GetState()
+ {
+ return new MouseStateExtended(_currentMouseState, _previousMouseState);
+ }
+
+ public static void Refresh()
+ {
+ _previousMouseState = _currentMouseState;
+ _currentMouseState = Mouse.GetState();
+ }
+
+ public static void SetPosition(int x, int y) => Mouse.SetPosition(x, y);
+ public static void SetPosition(Point point) => Mouse.SetPosition(point.X, point.Y);
+ public static void SetCursor(MouseCursor cursor) => Mouse.SetCursor(cursor);
+
+ public static IntPtr WindowHandle
+ {
+ get => Mouse.WindowHandle;
+ set => Mouse.WindowHandle = value;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs
new file mode 100644
index 0000000..0ec6943
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs
@@ -0,0 +1,149 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public struct MouseStateExtended
+ {
+ private readonly MouseState _currentMouseState;
+ private readonly MouseState _previousMouseState;
+
+ public MouseStateExtended(MouseState currentMouseState, MouseState previousMouseState)
+ {
+ _currentMouseState = currentMouseState;
+ _previousMouseState = previousMouseState;
+ }
+
+ public int X => _currentMouseState.X;
+ public int Y => _currentMouseState.Y;
+ public Point Position => _currentMouseState.Position;
+ public bool PositionChanged => _currentMouseState.Position != _previousMouseState.Position;
+
+ public int DeltaX => _previousMouseState.X - _currentMouseState.X;
+ public int DeltaY => _previousMouseState.Y - _currentMouseState.Y;
+ public Point DeltaPosition => new Point(DeltaX, DeltaY);
+
+ public int ScrollWheelValue => _currentMouseState.ScrollWheelValue;
+ public int DeltaScrollWheelValue => _previousMouseState.ScrollWheelValue - _currentMouseState.ScrollWheelValue;
+
+ public ButtonState LeftButton => _currentMouseState.LeftButton;
+ public ButtonState MiddleButton => _currentMouseState.MiddleButton;
+ public ButtonState RightButton => _currentMouseState.RightButton;
+ public ButtonState XButton1 => _currentMouseState.XButton1;
+ public ButtonState XButton2 => _currentMouseState.XButton2;
+
+ public bool IsButtonDown(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return IsPressed(m => m.LeftButton);
+ case MouseButton.Middle: return IsPressed(m => m.MiddleButton);
+ case MouseButton.Right: return IsPressed(m => m.RightButton);
+ case MouseButton.XButton1: return IsPressed(m => m.XButton1);
+ case MouseButton.XButton2: return IsPressed(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ public bool IsButtonUp(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return IsReleased(m => m.LeftButton);
+ case MouseButton.Middle: return IsReleased(m => m.MiddleButton);
+ case MouseButton.Right: return IsReleased(m => m.RightButton);
+ case MouseButton.XButton1: return IsReleased(m => m.XButton1);
+ case MouseButton.XButton2: return IsReleased(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the just-down state for the mouse on this state-change: true if the mouse button has just been pressed.
+ /// </summary>
+ /// <param name="button"></param>
+ /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
+ /// <returns>The just-down state for the mouse on this state-change.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsButtonPressed)}")]
+ public bool WasButtonJustDown(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return WasJustPressed(m => m.LeftButton);
+ case MouseButton.Middle: return WasJustPressed(m => m.MiddleButton);
+ case MouseButton.Right: return WasJustPressed(m => m.RightButton);
+ case MouseButton.XButton1: return WasJustPressed(m => m.XButton1);
+ case MouseButton.XButton2: return WasJustPressed(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the just-up state for the mouse on this state-change: true if the mouse button has just been released.
+ /// </summary>
+ /// <param name="button"></param>
+ /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
+ /// <returns>The just-up state for the mouse on this state-change.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsButtonReleased)}")]
+ public bool WasButtonJustUp(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return WasJustReleased(m => m.LeftButton);
+ case MouseButton.Middle: return WasJustReleased(m => m.MiddleButton);
+ case MouseButton.Right: return WasJustReleased(m => m.RightButton);
+ case MouseButton.XButton1: return WasJustReleased(m => m.XButton1);
+ case MouseButton.XButton2: return WasJustReleased(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the pressed state of a mouse button, for this state-change.
+ /// </summary>
+ /// <param name="button">The button to check.</param>
+ /// <returns>true if the given mouse button was pressed this state-change, otherwise false</returns>
+ public readonly bool IsButtonPressed(MouseButton button) => button switch
+ {
+ MouseButton.Left => WasJustPressed(m => m.LeftButton),
+ MouseButton.Middle => WasJustPressed(m => m.MiddleButton),
+ MouseButton.Right => WasJustPressed(m => m.RightButton),
+ MouseButton.XButton1 => WasJustPressed(m => m.XButton1),
+ MouseButton.XButton2 => WasJustPressed(m => m.XButton2),
+ _ => false,
+ };
+
+ /// <summary>
+ /// Get the released state of a mouse button, for this state-change.
+ /// </summary>
+ /// <param name="button">The button to check.</param>
+ /// <returns>true if the given mouse button was released this state-change, otherwise false</returns>
+ public readonly bool IsButtonReleased(MouseButton button) => button switch
+ {
+ MouseButton.Left => WasJustReleased(m => m.LeftButton),
+ MouseButton.Middle => WasJustReleased(m => m.MiddleButton),
+ MouseButton.Right => WasJustReleased(m => m.RightButton),
+ MouseButton.XButton1 => WasJustReleased(m => m.XButton1),
+ MouseButton.XButton2 => WasJustReleased(m => m.XButton2),
+ _ => false,
+ };
+
+ private readonly bool IsPressed(Func<MouseState, ButtonState> button)
+ => button(_currentMouseState) == ButtonState.Pressed;
+ private readonly bool IsReleased(Func<MouseState, ButtonState> button)
+ => button(_currentMouseState) == ButtonState.Released;
+ private readonly bool WasJustPressed(Func<MouseState, ButtonState> button)
+ => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed;
+ private readonly bool WasJustReleased(Func<MouseState, ButtonState> button)
+ => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
new file mode 100644
index 0000000..66c99e7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
@@ -0,0 +1,17 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class FastRandomExtensions
+ {
+ public static void NextColor(this FastRandom random, out HslColor color, Range<HslColor> range)
+ {
+ var maxH = range.Max.H >= range.Min.H
+ ? range.Max.H
+ : range.Max.H + 360;
+ color = new HslColor(random.NextSingle(range.Min.H, maxH),
+ random.NextSingle(range.Min.S, range.Max.S),
+ random.NextSingle(range.Min.L, range.Max.L));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
new file mode 100644
index 0000000..a3c1422
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles
+{
+ /// <summary>
+ /// Defines a part of a line that is bounded by two distinct end points.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LineSegment : IEquatable<LineSegment>
+ {
+ internal readonly Vector2 _point1;
+ internal readonly Vector2 _point2;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LineSegment" /> structure.
+ /// </summary>
+ /// <param name="point1"></param>
+ /// <param name="point2"></param>
+ public LineSegment(Vector2 point1, Vector2 point2)
+ {
+ _point1 = point1;
+ _point2 = point2;
+ }
+
+ public static LineSegment FromPoints(Vector2 p1, Vector2 p2) => new LineSegment(p1, p2);
+ public static LineSegment FromOrigin(Vector2 origin, Vector2 vector) => new LineSegment(origin, origin + vector);
+
+ public Vector2 Origin => _point1;
+
+ public Vector2 Direction
+ {
+ get
+ {
+ var coord = _point2 - _point1;
+ return new Vector2(coord.X, coord.Y);
+ }
+ }
+
+ public LineSegment Translate(Vector2 t)
+ {
+ return new LineSegment(_point1 + t, _point2 + t);
+ }
+
+ public Vector2 ToVector()
+ {
+ var t = _point2 - _point1;
+ return new Vector2(t.X, t.Y);
+ }
+
+ public bool Equals(LineSegment other)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return _point1.Equals(other._point1) && _point2.Equals(other._point2);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ return obj is LineSegment & Equals((LineSegment) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return (_point1.GetHashCode()*397) ^ _point2.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return $"({_point1.X}:{_point1.Y} {_point2.X}:{_point2.Y})";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
new file mode 100644
index 0000000..fac85f0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class AgeModifier : Modifier
+ {
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var n = Interpolators.Count;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(particle->Age, particle);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
new file mode 100644
index 0000000..30d2802
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class CircleContainerModifier : Modifier
+ {
+ public float Radius { get; set; }
+ public bool Inside { get; set; } = true;
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var radiusSq = Radius*Radius;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var localPos = particle->Position - particle->TriggerPos;
+
+ var distSq = localPos.LengthSquared();
+ var normal = localPos;
+ normal.Normalize();
+
+ if (Inside)
+ {
+ if (distSq < radiusSq) continue;
+
+ SetReflected(distSq, particle, normal);
+ }
+ else
+ {
+ if (distSq > radiusSq) continue;
+
+ SetReflected(distSq, particle, -normal);
+ }
+ }
+ }
+
+ private unsafe void SetReflected(float distSq, Particle* particle, Vector2 normal)
+ {
+ var dist = (float) Math.Sqrt(distSq);
+ var d = dist - Radius; // how far outside the circle is the particle
+
+ var twoRestDot = 2*RestitutionCoefficient*
+ Vector2.Dot(particle->Velocity, normal);
+ particle->Velocity -= twoRestDot*normal;
+
+ // exact computation requires sqrt or goniometrics
+ particle->Position -= normal*d;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
new file mode 100644
index 0000000..a56e072
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
@@ -0,0 +1,59 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public sealed class RectangleContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var xVel = particle->Velocity.X;
+ var yPos = particle->Position.Y;
+ var yVel = particle->Velocity.Y;
+
+ if ((int) particle->Position.X < left)
+ {
+ xPos = left + (left - xPos);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if (particle->Position.X > right)
+ {
+ xPos = right - (xPos - right);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ }
+
+ if (particle->Position.Y < top)
+ {
+ yPos = top + (top - yPos);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ {
+ yPos = bottom - (yPos - bottom);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ particle->Velocity = new Vector2(xVel, yVel);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
new file mode 100644
index 0000000..4da6efa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class RectangleLoopContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var yPos = particle->Position.Y;
+
+ if ((int) particle->Position.X < left)
+ xPos = particle->Position.X + Width;
+ else
+ {
+ if ((int) particle->Position.X > right)
+ xPos = particle->Position.X - Width;
+ }
+
+ if ((int) particle->Position.Y < top)
+ yPos = particle->Position.Y + Height;
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ yPos = particle->Position.Y - Height;
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
new file mode 100644
index 0000000..f2d6d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class DragModifier : Modifier
+ {
+ public float DragCoefficient { get; set; } = 0.47f;
+ public float Density { get; set; } = .5f;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var drag = -DragCoefficient*Density*particle->Mass*elapsedSeconds;
+
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + particle->Velocity.X*drag,
+ particle->Velocity.Y + particle->Velocity.Y*drag);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
new file mode 100644
index 0000000..f94c689
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
@@ -0,0 +1,13 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ /// <summary>
+ /// Defines a modifier which interpolates the color of a particle over the course of its lifetime.
+ /// </summary>
+ public sealed class ColorInterpolator : Interpolator<HslColor>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = HslColor.Lerp(StartValue, EndValue, amount);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
new file mode 100644
index 0000000..8b58a8e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class HueInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = new HslColor((EndValue - StartValue) * amount + StartValue, particle->Color.S, particle->Color.L);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
new file mode 100644
index 0000000..0ec9866
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
@@ -0,0 +1,26 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public abstract class Interpolator
+ {
+ protected Interpolator()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract unsafe void Update(float amount, Particle* particle);
+ }
+
+ public abstract class Interpolator<T> : Interpolator
+ {
+ /// <summary>
+ /// Gets or sets the intial value when the particles are created.
+ /// </summary>
+ public virtual T StartValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the final value when the particles are retired.
+ /// </summary>
+ public virtual T EndValue { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
new file mode 100644
index 0000000..0a26fcd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class OpacityInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Opacity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
new file mode 100644
index 0000000..44a72de
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class RotationInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Rotation = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
new file mode 100644
index 0000000..aff9244
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class ScaleInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Scale = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
new file mode 100644
index 0000000..bad04b1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class VelocityInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Velocity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
new file mode 100644
index 0000000..e9ed216
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class LinearGravityModifier : Modifier
+ {
+ public Vector2 Direction { get; set; }
+ public float Strength { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var vector = Direction*(Strength*elapsedSeconds);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + vector.X*particle->Mass,
+ particle->Velocity.Y + vector.Y*particle->Mass);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
new file mode 100644
index 0000000..bd62d25
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public abstract class Modifier
+ {
+ protected Modifier()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ public override string ToString()
+ {
+ return $"{Name} [{GetType().Name}]";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
new file mode 100644
index 0000000..9cc44d8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public sealed class OpacityFastFadeModifier : Modifier
+ {
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Opacity = 1.0f - particle->Age;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
new file mode 100644
index 0000000..2b17058
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class RotationModifier : Modifier
+ {
+ public float RotationRate { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var rotationRateDelta = RotationRate*elapsedSeconds;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Rotation += rotationRateDelta;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
new file mode 100644
index 0000000..ae9dc7b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityColorModifier : Modifier
+ {
+ public HslColor StationaryColor { get; set; }
+ public HslColor VelocityColor { get; set; }
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.X*particle->Velocity.X +
+ particle->Velocity.Y*particle->Velocity.Y;
+ var deltaColor = VelocityColor - StationaryColor;
+
+ if (velocity2 >= velocityThreshold2)
+ VelocityColor.CopyTo(out particle->Color);
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+
+ particle->Color = new HslColor(
+ deltaColor.H*t + StationaryColor.H,
+ deltaColor.S*t + StationaryColor.S,
+ deltaColor.L*t + StationaryColor.L);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
new file mode 100644
index 0000000..f089abc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityModifier : Modifier
+ {
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+ var n = Interpolators.Count;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.LengthSquared();
+
+ if (velocity2 >= velocityThreshold2)
+ {
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(1, particle);
+ }
+ }
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(t, particle);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
new file mode 100644
index 0000000..03a9999
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public unsafe class VortexModifier : Modifier
+ {
+ // Note: not the real-life one
+ private const float _gravConst = 100000f;
+
+ public Vector2 Position { get; set; }
+ public float Mass { get; set; }
+ public float MaxSpeed { get; set; }
+
+ public override void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var diff = Position + particle->TriggerPos - particle->Position;
+
+ var distance2 = diff.LengthSquared();
+
+ var speedGain = _gravConst*Mass/distance2*elapsedSeconds;
+ // normalize distances and multiply by speedGain
+ diff.Normalize();
+ particle->Velocity += diff*speedGain;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
new file mode 100644
index 0000000..d82f26e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A high performance particle system to make MonoGame more awesome.</Description>
+ <PackageTags>monogame particle system</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
new file mode 100644
index 0000000..042d8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct Particle
+ {
+ public float Inception;
+ public float Age;
+ public Vector2 Position;
+ public Vector2 TriggerPos;
+ public Vector2 Velocity;
+ public HslColor Color;
+ public float Opacity;
+ public Vector2 Scale;
+ public float Rotation;
+ public float Mass;
+ public float LayerDepth;
+
+ public static readonly int SizeInBytes = Marshal.SizeOf(typeof(Particle));
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
new file mode 100644
index 0000000..2e410fa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleBuffer : IDisposable
+ {
+ private readonly ParticleIterator _iterator;
+ private readonly IntPtr _nativePointer;
+
+ // points to the first memory pos after the buffer
+ protected readonly unsafe Particle* BufferEnd;
+
+ private bool _disposed;
+ // points to the particle after the last active particle.
+ protected unsafe Particle* Tail;
+
+ public unsafe ParticleBuffer(int size)
+ {
+ Size = size;
+ _nativePointer = Marshal.AllocHGlobal(SizeInBytes);
+
+ BufferEnd = (Particle*) (_nativePointer + SizeInBytes);
+ Head = (Particle*) _nativePointer;
+ Tail = (Particle*) _nativePointer;
+
+ _iterator = new ParticleIterator(this);
+
+ GC.AddMemoryPressure(SizeInBytes);
+ }
+
+ public int Size { get; }
+
+ public ParticleIterator Iterator => _iterator.Reset();
+ // pointer to the first particle
+ public unsafe Particle* Head { get; private set; }
+
+ // Number of available particle spots in the buffer
+ public int Available => Size - Count;
+ // current number of particles
+ public int Count { get; private set; }
+ // total size of the buffer (add one extra spot in memory for margin between head and tail so the iterator can see that it's at the end)
+ public int SizeInBytes => Particle.SizeInBytes*(Size + 1);
+ // total size of active particles
+ public int ActiveSizeInBytes => Particle.SizeInBytes*Count;
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Marshal.FreeHGlobal(_nativePointer);
+ _disposed = true;
+ GC.RemoveMemoryPressure(SizeInBytes);
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Release the given number of particles or the most available.
+ /// Returns a started iterator to iterate over the new particles.
+ /// </summary>
+ public unsafe ParticleIterator Release(int releaseQuantity)
+ {
+ var numToRelease = Math.Min(releaseQuantity, Available);
+
+ var prevCount = Count;
+ Count += numToRelease;
+
+ Tail += numToRelease;
+ if (Tail >= BufferEnd) Tail -= Size + 1;
+
+ return Iterator.Reset(prevCount);
+ }
+
+ public unsafe void Reclaim(int number)
+ {
+ Count -= number;
+
+ Head += number;
+ if (Head >= BufferEnd)
+ Head -= Size + 1;
+ }
+
+ //public void CopyTo(IntPtr destination)
+ //{
+ // memcpy(destination, _nativePointer, ActiveSizeInBytes);
+ //}
+
+ //public void CopyToReverse(IntPtr destination)
+ //{
+ // var offset = 0;
+ // for (var i = ActiveSizeInBytes - Particle.SizeInBytes; i >= 0; i -= Particle.SizeInBytes)
+ // {
+ // memcpy(IntPtr.Add(destination, offset), IntPtr.Add(_nativePointer, i), Particle.SizeInBytes);
+ // offset += Particle.SizeInBytes;
+ // }
+ //}
+
+ //[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
+ //public static extern void memcpy(IntPtr dest, IntPtr src, int count);
+
+ ~ParticleBuffer()
+ {
+ Dispose();
+ }
+
+ public class ParticleIterator
+ {
+ private readonly ParticleBuffer _buffer;
+
+ private unsafe Particle* _current;
+
+ public int Total;
+
+ public ParticleIterator(ParticleBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public unsafe bool HasNext => _current != _buffer.Tail;
+
+ public unsafe ParticleIterator Reset()
+ {
+ _current = _buffer.Head;
+ Total = _buffer.Count;
+ return this;
+ }
+
+ internal unsafe ParticleIterator Reset(int offset)
+ {
+ Total = _buffer.Count;
+
+ _current = _buffer.Head + offset;
+ if (_current >= _buffer.BufferEnd)
+ _current -= _buffer.Size + 1;
+
+ return this;
+ }
+
+ public unsafe Particle* Next()
+ {
+ var p = _current;
+ _current++;
+ if (_current == _buffer.BufferEnd)
+ _current = (Particle*) _buffer._nativePointer;
+
+ return p;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
new file mode 100644
index 0000000..ce6c782
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleEffect : Transform2, IDisposable
+ {
+ public ParticleEffect(string name = null)
+ {
+ Name = name;
+ Emitters = new List<ParticleEmitter>();
+ }
+
+ public void Dispose()
+ {
+ foreach (var emitter in Emitters)
+ emitter.Dispose();
+ }
+
+ public string Name { get; set; }
+ public List<ParticleEmitter> Emitters { get; set; }
+ public int ActiveParticles => Emitters.Sum(t => t.ActiveParticles);
+
+ public void FastForward(Vector2 position, float seconds, float triggerPeriod)
+ {
+ var time = 0f;
+ while (time < seconds)
+ {
+ Update(triggerPeriod);
+ Trigger(position);
+ time += triggerPeriod;
+ }
+ }
+
+ public static ParticleEffect FromFile(ITextureRegionService textureRegionService, string path)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(textureRegionService, stream);
+ }
+ }
+
+ public static ParticleEffect FromStream(ITextureRegionService textureRegionService, Stream stream)
+ {
+ var options = ParticleJsonSerializerOptionsProvider.GetOptions(textureRegionService);
+ return JsonSerializer.Deserialize<ParticleEffect>(stream, options);
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Update(elapsedSeconds, Position);
+ }
+
+ public void Trigger()
+ {
+ Trigger(Position);
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(position, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(line, layerDepth);
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
new file mode 100644
index 0000000..1a6273b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public unsafe class ParticleEmitter : IDisposable
+ {
+ private readonly FastRandom _random = new FastRandom(Math.Abs(Guid.NewGuid().GetHashCode()));
+ private float _totalSeconds;
+
+ [JsonConstructor]
+ public ParticleEmitter(string name, TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ {
+ if (profile == null)
+ throw new ArgumentNullException(nameof(profile));
+
+ _lifeSpanSeconds = (float)lifeSpan.TotalSeconds;
+
+ Name = name;
+ TextureRegion = textureRegion;
+ Buffer = new ParticleBuffer(capacity);
+ Offset = Vector2.Zero;
+ Profile = profile;
+ Modifiers = new List<Modifier>();
+ ModifierExecutionStrategy = ParticleModifierExecutionStrategy.Serial;
+ Parameters = new ParticleReleaseParameters();
+ }
+
+ public ParticleEmitter(TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ : this(null, textureRegion, capacity, lifeSpan, profile)
+ {
+ }
+
+ public void Dispose()
+ {
+ Buffer.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ ~ParticleEmitter()
+ {
+ Dispose();
+ }
+
+ public string Name { get; set; }
+ public int ActiveParticles => Buffer.Count;
+ public Vector2 Offset { get; set; }
+ public List<Modifier> Modifiers { get; }
+ public Profile Profile { get; set; }
+ public float LayerDepth { get; set; }
+ public ParticleReleaseParameters Parameters { get; set; }
+ public TextureRegion2D TextureRegion { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ParticleModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
+
+ internal ParticleBuffer Buffer;
+
+ public int Capacity
+ {
+ get { return Buffer.Size; }
+ set
+ {
+ var oldBuffer = Buffer;
+ oldBuffer.Dispose();
+ Buffer = new ParticleBuffer(value);
+ }
+ }
+
+ private float _lifeSpanSeconds;
+ public TimeSpan LifeSpan
+ {
+ get { return TimeSpan.FromSeconds(_lifeSpanSeconds); }
+ set { _lifeSpanSeconds = (float) value.TotalSeconds; }
+ }
+
+ private float _nextAutoTrigger;
+
+ private bool _autoTrigger = true;
+ public bool AutoTrigger
+ {
+ get { return _autoTrigger; }
+ set
+ {
+ _autoTrigger = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private float _autoTriggerFrequency;
+ public float AutoTriggerFrequency
+ {
+ get { return _autoTriggerFrequency; }
+ set
+ {
+ _autoTriggerFrequency = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private void ReclaimExpiredParticles()
+ {
+ var iterator = Buffer.Iterator;
+ var expired = 0;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ if (_totalSeconds - particle->Inception < _lifeSpanSeconds)
+ break;
+
+ expired++;
+ }
+
+ if (expired != 0)
+ Buffer.Reclaim(expired);
+ }
+
+ public bool Update(float elapsedSeconds, Vector2 position = default(Vector2))
+ {
+ _totalSeconds += elapsedSeconds;
+
+ if (_autoTrigger)
+ {
+ _nextAutoTrigger -= elapsedSeconds;
+
+ if (_nextAutoTrigger <= 0)
+ {
+ Trigger(position, this.LayerDepth);
+ _nextAutoTrigger = _autoTriggerFrequency;
+ }
+ }
+
+ if (Buffer.Count == 0)
+ return false;
+
+ ReclaimExpiredParticles();
+
+ var iterator = Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Age = (_totalSeconds - particle->Inception) / _lifeSpanSeconds;
+ particle->Position = particle->Position + particle->Velocity * elapsedSeconds;
+ }
+
+ ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, iterator);
+ return true;
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ Release(position + Offset, numToRelease, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ var lineVector = line.ToVector();
+
+ for (var i = 0; i < numToRelease; i++)
+ {
+ var offset = lineVector * _random.NextSingle();
+ Release(line.Origin + offset, 1, layerDepth);
+ }
+ }
+
+ private void Release(Vector2 position, int numToRelease, float layerDepth)
+ {
+ var iterator = Buffer.Release(numToRelease);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ Vector2 heading;
+ Profile.GetOffsetAndHeading(out particle->Position, out heading);
+
+ particle->Age = 0f;
+ particle->Inception = _totalSeconds;
+ particle->Position += position;
+ particle->TriggerPos = position;
+
+ var speed = _random.NextSingle(Parameters.Speed);
+
+ particle->Velocity = heading * speed;
+
+ _random.NextColor(out particle->Color, Parameters.Color);
+
+ particle->Opacity = _random.NextSingle(Parameters.Opacity);
+
+ if(Parameters.MaintainAspectRatioOnScale)
+ {
+ var scale = _random.NextSingle(Parameters.Scale);
+ particle->Scale = new Vector2(scale, scale);
+ }
+ else
+ {
+ particle->Scale = new Vector2(_random.NextSingle(Parameters.ScaleX), _random.NextSingle(Parameters.ScaleY));
+ }
+
+ particle->Rotation = _random.NextSingle(Parameters.Rotation);
+ particle->Mass = _random.NextSingle(Parameters.Mass);
+ particle->LayerDepth = layerDepth;
+ }
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
new file mode 100644
index 0000000..7566e8d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
@@ -0,0 +1,49 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class ParticleExtensions
+ {
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect)
+ {
+ for (var i = 0; i < effect.Emitters.Count; i++)
+ UnsafeDraw(spriteBatch, effect.Emitters[i]);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ UnsafeDraw(spriteBatch, emitter);
+ }
+
+ private static unsafe void UnsafeDraw(SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ if(emitter.TextureRegion == null)
+ return;
+
+ var textureRegion = emitter.TextureRegion;
+ var origin = new Vector2(textureRegion.Width/2f, textureRegion.Height/2f);
+ var iterator = emitter.Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var color = particle->Color.ToRgb();
+
+ if (spriteBatch.GraphicsDevice.BlendState == BlendState.AlphaBlend)
+ color *= particle->Opacity;
+ else
+ color.A = (byte) (particle->Opacity*255);
+
+ var position = new Vector2(particle->Position.X, particle->Position.Y);
+ var scale = particle->Scale;
+ var particleColor = new Color(color, particle->Opacity);
+ var rotation = particle->Rotation;
+ var layerDepth = particle->LayerDepth;
+
+ spriteBatch.Draw(textureRegion, position, particleColor, rotation, origin, scale, SpriteEffects.None, layerDepth);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
new file mode 100644
index 0000000..20a4d51
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers;
+
+namespace MonoGame.Extended.Particles
+{
+ using TPL = System.Threading.Tasks;
+
+ public abstract class ParticleModifierExecutionStrategy
+ {
+ public static readonly ParticleModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy();
+ public static readonly ParticleModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy();
+
+ internal abstract void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ internal class SerialModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ for (var i = 0; i < modifiers.Count; i++)
+ modifiers[i].Update(elapsedSeconds, iterator.Reset());
+ }
+
+ public override string ToString()
+ {
+ return nameof(Serial);
+ }
+ }
+
+ internal class ParallelModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ TPL.Parallel.ForEach(modifiers, modifier => modifier.Update(elapsedSeconds, iterator.Reset()));
+ }
+
+ public override string ToString()
+ {
+ return nameof(Parallel);
+ }
+ }
+
+ public static ParticleModifierExecutionStrategy Parse(string value)
+ {
+ if (string.Equals(nameof(Parallel), value, StringComparison.OrdinalIgnoreCase))
+ return Parallel;
+
+ if (string.Equals(nameof(Serial), value, StringComparison.OrdinalIgnoreCase))
+ return Serial;
+
+ throw new InvalidOperationException($"Unknown particle modifier execution strategy '{value}'");
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
new file mode 100644
index 0000000..e19fbc2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleReleaseParameters
+ {
+ public ParticleReleaseParameters()
+ {
+ Quantity = 1;
+ Speed = new Range<float>(-1f, 1f);
+ Color = new Range<HslColor>(Microsoft.Xna.Framework.Color.White.ToHsl(), Microsoft.Xna.Framework.Color.White.ToHsl());
+ Opacity = new Range<float>(0f, 1f);
+ Scale = new Range<float>(1f, 1f);
+ Rotation = new Range<float>(-MathHelper.Pi, MathHelper.Pi);
+ Mass = 1f;
+ MaintainAspectRatioOnScale = true;
+ ScaleX = new Range<float>(1f, 1f);
+ ScaleY = new Range<float>(1f, 1f);
+ }
+
+ public Range<int> Quantity { get; set; }
+ public Range<float> Speed { get; set; }
+ public Range<HslColor> Color { get; set; }
+ public Range<float> Opacity { get; set; }
+ public Range<float> Scale { get; set; }
+ public Range<float> Rotation { get; set; }
+ public Range<float> Mass { get; set; }
+ public bool MaintainAspectRatioOnScale { get; set; }
+ public Range<float> ScaleX { get; set; }
+ public Range<float> ScaleY { get; set; }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
new file mode 100644
index 0000000..e363713
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxFillProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f),
+ Random.NextSingle(Height*-0.5f, Height*0.5f));
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
new file mode 100644
index 0000000..7d31984
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
@@ -0,0 +1,31 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ switch (Random.Next(3))
+ {
+ case 0: // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ case 1: // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ break;
+ case 2: // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ default: // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ break;
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
new file mode 100644
index 0000000..9f766dc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxUniformProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var value = Random.Next((int) (2*Width + 2*Height));
+
+ if (value < Width) // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ else
+ {
+ if (value < 2*Width) // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ else
+ {
+ if (value < 2*Width + Height) // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ else // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ }
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
new file mode 100644
index 0000000..ef7f11e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
@@ -0,0 +1,24 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class CircleProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var dist = Random.NextSingle(0f, Radius);
+
+ Random.NextUnitVector(out heading);
+
+ offset = Radiate == CircleRadiation.In
+ ? new Vector2(-heading.X*dist, -heading.Y*dist)
+ : new Vector2(heading.X*dist, heading.Y*dist);
+
+ if (Radiate == CircleRadiation.None)
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
new file mode 100644
index 0000000..bea584e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class LineProfile : Profile
+ {
+ public Vector2 Axis { get; set; }
+ public float Length { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var vect = Axis*Random.NextSingle(Length*-0.5f, Length*0.5f);
+ offset = new Vector2(vect.X, vect.Y);
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
new file mode 100644
index 0000000..04456d3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class PointProfile : Profile
+ {
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = Vector2.Zero;
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
new file mode 100644
index 0000000..bfde371
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
@@ -0,0 +1,68 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public abstract class Profile
+ {
+ public enum CircleRadiation
+ {
+ None,
+ In,
+ Out
+ }
+
+ protected FastRandom Random { get; } = new FastRandom();
+
+ public abstract void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading);
+
+ public object Clone()
+ {
+ return MemberwiseClone();
+ }
+
+ public static Profile Point()
+ {
+ return new PointProfile();
+ }
+
+ public static Profile Line(Vector2 axis, float length)
+ {
+ return new LineProfile {Axis = axis, Length = length};
+ }
+
+ public static Profile Ring(float radius, CircleRadiation radiate)
+ {
+ return new RingProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Box(float width, float height)
+ {
+ return new BoxProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxFill(float width, float height)
+ {
+ return new BoxFillProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxUniform(float width, float height)
+ {
+ return new BoxUniformProfile {Width = width, Height = height};
+ }
+
+ public static Profile Circle(float radius, CircleRadiation radiate)
+ {
+ return new CircleProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Spray(Vector2 direction, float spread)
+ {
+ return new SprayProfile {Direction = direction, Spread = spread};
+ }
+
+ public override string ToString()
+ {
+ return GetType().ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
new file mode 100644
index 0000000..d5ac2f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class RingProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ Random.NextUnitVector(out heading);
+
+ switch (Radiate)
+ {
+ case CircleRadiation.In:
+ offset = new Vector2(-heading.X*Radius, -heading.Y*Radius);
+ break;
+ case CircleRadiation.Out:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ break;
+ case CircleRadiation.None:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ Random.NextUnitVector(out heading);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException($"{Radiate} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
new file mode 100644
index 0000000..6259210
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class SprayProfile : Profile
+ {
+ public Vector2 Direction { get; set; }
+ public float Spread { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var angle = (float) Math.Atan2(Direction.Y, Direction.X);
+
+ angle = Random.NextSingle(angle - Spread/2f, angle + Spread/2f);
+ offset = Vector2.Zero;
+ heading = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
new file mode 100644
index 0000000..21bd7fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class InterpolatorJsonConverter : BaseTypeJsonConverter<Interpolator>
+ {
+ public InterpolatorJsonConverter()
+ : base(GetSupportedTypes(), "Interpolator")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Interpolator)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Interpolator).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
new file mode 100644
index 0000000..8fc894b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+/// <summary>
+/// Converts a <see cref="ParticleModifierExecutionStrategy"/> value to or from JSON.
+/// </summary>
+public class ModifierExecutionStrategyJsonConverter : JsonConverter<ParticleModifierExecutionStrategy>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) =>
+ typeToConvert == typeof(ParticleModifierExecutionStrategy) ||
+ typeToConvert.GetTypeInfo().BaseType == typeof(ParticleModifierExecutionStrategy);
+
+ /// <inheritdoc />
+ public override ParticleModifierExecutionStrategy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = JsonSerializer.Deserialize<string>(ref reader, options);
+ return ParticleModifierExecutionStrategy.Parse(value);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, ParticleModifierExecutionStrategy value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue(value.ToString());
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
new file mode 100644
index 0000000..5985e40
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ModifierJsonConverter : BaseTypeJsonConverter<Modifier>
+ {
+ public ModifierJsonConverter()
+ : base(GetSupportedTypes(), "Modifier")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Modifier)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Modifier).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..489b3ee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public static class ParticleJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ITextureRegionService textureRegionService)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new ProfileJsonConverter());
+ options.Converters.Add(new ModifierJsonConverter());
+ options.Converters.Add(new InterpolatorJsonConverter());
+ options.Converters.Add(new TimeSpanJsonConverter());
+ options.Converters.Add(new RangeJsonConverter<int>());
+ options.Converters.Add(new RangeJsonConverter<float>());
+ options.Converters.Add(new RangeJsonConverter<HslColor>());
+ options.Converters.Add(new HslColorJsonConverter());
+ options.Converters.Add(new ModifierExecutionStrategyJsonConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
new file mode 100644
index 0000000..8d98079
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ProfileJsonConverter : BaseTypeJsonConverter<Profile>
+ {
+ public ProfileJsonConverter()
+ : base(GetSupportedTypes(), nameof(Profile))
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Profile)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => type.IsSubclassOf(typeof(Profile)) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
new file mode 100644
index 0000000..0155b08
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TimeSpan);
+
+ /// <inheritdoc />
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ double seconds = reader.GetDouble();
+ return TimeSpan.FromSeconds(seconds);
+ }
+
+ return TimeSpan.Zero;
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteNumberValue(value.TotalSeconds);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs
new file mode 100644
index 0000000..86f1b6b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs
@@ -0,0 +1,20 @@
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Tiled
+{
+ public static class ContentReaderExtensions
+ {
+ public static void ReadTiledMapProperties(this ContentReader reader, TiledMapProperties properties)
+ {
+ var count = reader.ReadInt32();
+
+ for (var i = 0; i < count; i++)
+ {
+ var key = reader.ReadString();
+ var value = new TiledMapPropertyValue(reader.ReadString());
+ ReadTiledMapProperties(reader, value.Properties);
+ properties[key] = value;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj
new file mode 100644
index 0000000..a52cc65
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Support for Tiled maps to make MonoGame more awesome. See http://www.mapeditor.org</Description>
+ <PackageTags>monogame tiled maps orthographic isometric</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Graphics\MonoGame.Extended.Graphics.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs
new file mode 100644
index 0000000..791819a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public sealed class TiledMapAnimatedLayerModel : TiledMapLayerModel
+ {
+ public TiledMapAnimatedLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices, TiledMapTilesetAnimatedTile[] animatedTilesetTiles, TiledMapTileFlipFlags[] animatedTilesetTileFlipFlags)
+ : base(graphicsDevice, texture, vertices, indices)
+ {
+ Vertices = vertices;
+ AnimatedTilesetTiles = animatedTilesetTiles;
+ _animatedTilesetFlipFlags = animatedTilesetTileFlipFlags;
+ }
+
+ public VertexPositionTexture[] Vertices { get; }
+ public TiledMapTilesetAnimatedTile[] AnimatedTilesetTiles { get; }
+ private readonly TiledMapTileFlipFlags[] _animatedTilesetFlipFlags;
+
+ public ReadOnlySpan<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags => _animatedTilesetFlipFlags;
+
+ protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount)
+ {
+ return new DynamicVertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly);
+ }
+
+ protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount)
+ {
+ return new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs
new file mode 100644
index 0000000..7afcc21
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapAnimatedLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapAnimatedLayerModel>
+ {
+ public TiledMapAnimatedLayerModelBuilder()
+ {
+ AnimatedTilesetTiles = new List<TiledMapTilesetAnimatedTile>();
+ AnimatedTilesetFlipFlags = new List<TiledMapTileFlipFlags>();
+ }
+
+ public List<TiledMapTilesetAnimatedTile> AnimatedTilesetTiles { get; }
+ public List<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags { get; }
+
+ protected override void ClearBuffers()
+ {
+ AnimatedTilesetTiles.Clear();
+ AnimatedTilesetFlipFlags.Clear();
+ }
+
+ protected override TiledMapAnimatedLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ return new TiledMapAnimatedLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray(), AnimatedTilesetTiles.ToArray(), AnimatedTilesetFlipFlags.ToArray());
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs
new file mode 100644
index 0000000..b24e2e9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs
@@ -0,0 +1,37 @@
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Graphics.Effects;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public interface ITiledMapEffect : IEffectMatrices, ITextureEffect
+ {
+ float Alpha { get; set; }
+ }
+
+ public class TiledMapEffect : DefaultEffect, ITiledMapEffect
+ {
+ public TiledMapEffect(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ Initialize();
+ }
+
+ public TiledMapEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ public TiledMapEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ VertexColorEnabled = false;
+ TextureEnabled = true;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs
new file mode 100644
index 0000000..f837e1a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public abstract class TiledMapLayerModel : IDisposable
+ {
+ protected TiledMapLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices)
+ {
+ Texture = texture;
+
+ // ReSharper disable once VirtualMemberCallInConstructor
+ VertexBuffer = CreateVertexBuffer(graphicsDevice, vertices.Length);
+ VertexBuffer.SetData(vertices, 0, vertices.Length);
+
+ // ReSharper disable once VirtualMemberCallInConstructor
+ IndexBuffer = CreateIndexBuffer(graphicsDevice, indices.Length);
+ IndexBuffer.SetData(indices, 0, indices.Length);
+
+ TriangleCount = indices.Length / 3;
+ }
+
+ public void Dispose()
+ {
+ IndexBuffer.Dispose();
+ VertexBuffer.Dispose();
+ }
+
+ public Texture2D Texture { get; }
+ public VertexBuffer VertexBuffer { get; }
+ public IndexBuffer IndexBuffer { get; }
+ public int TriangleCount { get; }
+
+ protected abstract VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount);
+ protected abstract IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount);
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs
new file mode 100644
index 0000000..fbc99d6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public abstract class TiledMapLayerModelBuilder<T>
+ {
+ protected TiledMapLayerModelBuilder()
+ {
+ Indices = new List<ushort>();
+ Vertices = new List<VertexPositionTexture>();
+ }
+
+ public List<ushort> Indices { get; }
+ public List<VertexPositionTexture> Vertices { get; }
+ public bool IsFull => Vertices.Count + TiledMapHelper.VerticesPerTile >= TiledMapHelper.MaximumVerticesPerModel;
+ public bool IsBuildable => Vertices.Any();
+
+ protected abstract void ClearBuffers();
+ protected abstract T CreateModel(GraphicsDevice graphicsDevice, Texture2D texture);
+
+ public T Build(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ var model = CreateModel(graphicsDevice, texture);
+ Vertices.Clear();
+ Indices.Clear();
+ ClearBuffers();
+ return model;
+ }
+
+ public void AddSprite(Texture2D texture, Point2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flipFlags)
+ {
+ Indices.AddRange(CreateTileIndices(Vertices.Count));
+ Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel);
+
+ Vertices.AddRange(CreateVertices(texture, position, sourceRectangle, flipFlags));
+ Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel);
+ }
+
+ private static IEnumerable<VertexPositionTexture> CreateVertices(Texture2D texture, Vector2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None)
+ {
+ var reciprocalWidth = 1f / texture.Width;
+ var reciprocalHeight = 1f / texture.Height;
+ var texelLeft = sourceRectangle.X * reciprocalWidth;
+ var texelTop = sourceRectangle.Y * reciprocalHeight;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) * reciprocalWidth;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) * reciprocalHeight;
+
+ VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight;
+
+ vertexTopLeft.Position = new Vector3(position, 0);
+ vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, 0), 0);
+ vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle.Height), 0);
+ vertexBottomRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, sourceRectangle.Height), 0);
+
+ vertexTopLeft.TextureCoordinate.Y = texelTop;
+ vertexTopLeft.TextureCoordinate.X = texelLeft;
+
+ vertexTopRight.TextureCoordinate.Y = texelTop;
+ vertexTopRight.TextureCoordinate.X = texelRight;
+
+ vertexBottomLeft.TextureCoordinate.Y = texelBottom;
+ vertexBottomLeft.TextureCoordinate.X = texelLeft;
+
+ vertexBottomRight.TextureCoordinate.Y = texelBottom;
+ vertexBottomRight.TextureCoordinate.X = texelRight;
+
+ var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ }
+
+ if (flipVertically)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ }
+
+ yield return vertexTopLeft;
+ yield return vertexTopRight;
+ yield return vertexBottomLeft;
+ yield return vertexBottomRight;
+ }
+
+ private static IEnumerable<ushort> CreateTileIndices(int indexOffset)
+ {
+ yield return (ushort)(0 + indexOffset);
+ yield return (ushort)(1 + indexOffset);
+ yield return (ushort)(2 + indexOffset);
+ yield return (ushort)(1 + indexOffset);
+ yield return (ushort)(3 + indexOffset);
+ yield return (ushort)(2 + indexOffset);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs
new file mode 100644
index 0000000..52e56bc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapModel : IDisposable
+ {
+ private readonly TiledMap _map;
+ private readonly Dictionary<TiledMapTileset, List<TiledMapTilesetAnimatedTile>> _animatedTilesByTileset;
+
+ public TiledMapModel(TiledMap map, Dictionary<TiledMapLayer, TiledMapLayerModel[]> layersOfLayerModels)
+ {
+ _map = map;
+ LayersOfLayerModels = layersOfLayerModels;
+ _animatedTilesByTileset = _map.Tilesets
+ .ToDictionary(i => i, i => i.Tiles.OfType<TiledMapTilesetAnimatedTile>()
+ .ToList());
+ }
+
+ public void Dispose()
+ {
+ foreach (var layerModel in LayersOfLayerModels)
+ foreach (var model in layerModel.Value)
+ model.Dispose();
+ }
+
+ public ReadOnlyCollection<TiledMapTileset> Tilesets => _map.Tilesets;
+ public ReadOnlyCollection<TiledMapLayer> Layers => _map.Layers;
+
+ // each layer has many models
+ public Dictionary<TiledMapLayer, TiledMapLayerModel[]> LayersOfLayerModels { get; }
+
+ public IEnumerable<TiledMapTilesetAnimatedTile> GetAnimatedTiles(int tilesetIndex)
+ {
+ var tileset = _map.Tilesets[tilesetIndex];
+ return _animatedTilesByTileset[tileset];
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs
new file mode 100644
index 0000000..b96925e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapModelBuilder
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+
+ public TiledMapModelBuilder(GraphicsDevice graphicsDevice)
+ {
+ _graphicsDevice = graphicsDevice;
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateLayerModels(TiledMap map, TiledMapLayer layer)
+ {
+ switch(layer)
+ {
+ case TiledMapTileLayer tileLayer:
+ return CreateTileLayerModels(map, tileLayer);
+ case TiledMapImageLayer imageLayer:
+ return CreateImageLayerModels(imageLayer);
+ default:
+ return new List<TiledMapLayerModel>();
+ }
+
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateImageLayerModels(TiledMapImageLayer imageLayer)
+ {
+ var modelBuilder = new TiledMapStaticLayerModelBuilder();
+ modelBuilder.AddSprite(imageLayer.Image, imageLayer.Position, imageLayer.Image.Bounds, TiledMapTileFlipFlags.None);
+ yield return modelBuilder.Build(_graphicsDevice, imageLayer.Image);
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateTileLayerModels(TiledMap map, TiledMapTileLayer tileLayer)
+ {
+ var layerModels = new List<TiledMapLayerModel>();
+ var staticLayerBuilder = new TiledMapStaticLayerModelBuilder();
+ var animatedLayerBuilder = new TiledMapAnimatedLayerModelBuilder();
+
+ foreach (var tileset in map.Tilesets)
+ {
+ var firstGlobalIdentifier = map.GetTilesetFirstGlobalIdentifier(tileset);
+ var lastGlobalIdentifier = tileset.TileCount + firstGlobalIdentifier - 1;
+ var texture = tileset.Texture;
+
+ foreach (var tile in tileLayer.Tiles.Where(t => firstGlobalIdentifier <= t.GlobalIdentifier && t.GlobalIdentifier <= lastGlobalIdentifier))
+ {
+ var tileGid = tile.GlobalIdentifier;
+ var localTileIdentifier = tileGid - firstGlobalIdentifier;
+ var position = GetTilePosition(map, tile);
+ var sourceRectangle = tileset.GetTileRegion(localTileIdentifier);
+ var flipFlags = tile.Flags;
+
+ // animated tiles
+ var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier);
+ if (tilesetTile?.Texture is not null)
+ {
+ position.Y += map.TileHeight - sourceRectangle.Height;
+ texture = tilesetTile.Texture;
+ }
+
+ if (tilesetTile is TiledMapTilesetAnimatedTile animatedTilesetTile)
+ {
+ animatedLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags);
+ animatedTilesetTile.CreateTextureRotations(tileset, flipFlags);
+ animatedLayerBuilder.AnimatedTilesetTiles.Add(animatedTilesetTile);
+ animatedLayerBuilder.AnimatedTilesetFlipFlags.Add(flipFlags);
+
+ if (animatedLayerBuilder.IsFull)
+ layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture));
+ }
+ else
+ {
+ staticLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags);
+
+ if (staticLayerBuilder.IsFull)
+ layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture));
+ }
+ }
+
+ if (staticLayerBuilder.IsBuildable)
+ layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture));
+
+ if (animatedLayerBuilder.IsBuildable)
+ layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture));
+ }
+
+ return layerModels;
+ }
+
+ public TiledMapModel Build(TiledMap map)
+ {
+ var dictionary = new Dictionary<TiledMapLayer, TiledMapLayerModel[]>();
+ foreach (var layer in map.Layers)
+ BuildLayer(map, layer, dictionary);
+
+ return new TiledMapModel(map, dictionary);
+ }
+
+ private void BuildLayer(TiledMap map, TiledMapLayer layer, Dictionary<TiledMapLayer, TiledMapLayerModel[]> dictionary)
+ {
+ if (layer is TiledMapGroupLayer groupLayer)
+ foreach (var subLayer in groupLayer.Layers)
+ BuildLayer(map, subLayer, dictionary);
+ else
+ dictionary.Add(layer, CreateLayerModels(map, layer).ToArray());
+ }
+
+ private static Point2 GetTilePosition(TiledMap map, TiledMapTile mapTile)
+ {
+ switch (map.Orientation)
+ {
+ case TiledMapOrientation.Orthogonal:
+ return TiledMapHelper.GetOrthogonalPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight);
+ case TiledMapOrientation.Isometric:
+ return TiledMapHelper.GetIsometricPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight);
+ default:
+ throw new NotSupportedException($"{map.Orientation} Tiled Maps are not yet implemented.");
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs
new file mode 100644
index 0000000..66babad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapRenderer : IDisposable
+ {
+ private readonly TiledMapModelBuilder _mapModelBuilder;
+ private readonly TiledMapEffect _defaultEffect;
+ private readonly GraphicsDevice _graphicsDevice;
+ private TiledMapModel _mapModel;
+ private Matrix _worldMatrix = Matrix.Identity;
+
+ public TiledMapRenderer(GraphicsDevice graphicsDevice, TiledMap map = null)
+ {
+ if (graphicsDevice == null) throw new ArgumentNullException(nameof(graphicsDevice));
+
+ _graphicsDevice = graphicsDevice;
+ _defaultEffect = new TiledMapEffect(graphicsDevice);
+ _mapModelBuilder = new TiledMapModelBuilder(graphicsDevice);
+
+ if(map != null)
+ LoadMap(map);
+ }
+
+ public void Dispose()
+ {
+ _mapModel?.Dispose();
+ _defaultEffect.Dispose();
+ }
+
+ public void LoadMap(TiledMap map)
+ {
+ _mapModel?.Dispose();
+ _mapModel = map != null ? _mapModelBuilder.Build(map) : null;
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ if(_mapModel == null)
+ return;
+
+ for (var tilesetIndex = 0; tilesetIndex < _mapModel.Tilesets.Count; tilesetIndex++)
+ {
+ foreach (var animatedTilesetTile in _mapModel.GetAnimatedTiles(tilesetIndex))
+ animatedTilesetTile.Update(gameTime);
+ }
+
+ foreach(var layer in _mapModel.LayersOfLayerModels)
+ UpdateAnimatedLayerModels(layer.Value.OfType<TiledMapAnimatedLayerModel>());
+ }
+
+ private static unsafe void UpdateAnimatedLayerModels(IEnumerable<TiledMapAnimatedLayerModel> animatedLayerModels)
+ {
+ foreach (var animatedModel in animatedLayerModels)
+ {
+ // update the texture coordinates for each animated tile
+ fixed (VertexPositionTexture* fixedVerticesPointer = animatedModel.Vertices)
+ {
+ var verticesPointer = fixedVerticesPointer;
+ for (int i = 0; i < animatedModel.AnimatedTilesetTiles.Length; i++)
+ {
+ var currentFrameTextureCoordinates = animatedModel.AnimatedTilesetTiles[i].CurrentAnimationFrame.GetTextureCoordinates(animatedModel.AnimatedTilesetFlipFlags[i]);
+
+ // ReSharper disable ArrangeRedundantParentheses
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[0];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[1];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[2];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[3];
+ // ReSharper restore ArrangeRedundantParentheses
+ }
+ }
+
+ // copy (upload) the updated vertices to the GPU's memory
+ animatedModel.VertexBuffer.SetData(animatedModel.Vertices, 0, animatedModel.Vertices.Length);
+ }
+ }
+
+ public void Draw(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ if (_mapModel == null)
+ return;
+
+ for (var index = 0; index < _mapModel.Layers.Count; index++)
+ Draw(index, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ public void Draw(TiledMapLayer layer, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(layer, ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(int layerIndex, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(layerIndex, ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(int layerIndex, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ var layer = _mapModel.Layers[layerIndex];
+
+ Draw(layer, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ public void Draw(TiledMapLayer layer, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ if (_mapModel == null)
+ return;
+
+ if (!layer.IsVisible)
+ return;
+
+ if (layer is TiledMapObjectLayer)
+ return;
+
+ Draw(layer, Vector2.Zero, Vector2.One, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ private void Draw(TiledMapLayer layer, Vector2 parentOffset, Vector2 parentParallaxFactor, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect, float depth)
+ {
+ var offset = parentOffset + layer.Offset;
+ var parallaxFactor = parentParallaxFactor * layer.ParallaxFactor;
+
+ if (layer is TiledMapGroupLayer groupLayer)
+ {
+ foreach (var subLayer in groupLayer.Layers)
+ Draw(subLayer, offset, parallaxFactor, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+ else
+ {
+ _worldMatrix.Translation = new Vector3(offset, depth);
+
+ var effect1 = effect ?? _defaultEffect;
+ var tiledMapEffect = effect1 as ITiledMapEffect;
+ if (tiledMapEffect == null)
+ return;
+
+ // model-to-world transform
+ tiledMapEffect.World = _worldMatrix;
+ tiledMapEffect.View = parallaxFactor == Vector2.One ? viewMatrix : IncludeParallax(viewMatrix, parallaxFactor);
+ tiledMapEffect.Projection = projectionMatrix;
+
+ foreach (var layerModel in _mapModel.LayersOfLayerModels[layer])
+ {
+ // desired alpha
+ tiledMapEffect.Alpha = layer.Opacity;
+
+ // desired texture
+ tiledMapEffect.Texture = layerModel.Texture;
+
+ // bind the vertex and index buffer
+ _graphicsDevice.SetVertexBuffer(layerModel.VertexBuffer);
+ _graphicsDevice.Indices = layerModel.IndexBuffer;
+
+ // for each pass in our effect
+ foreach (var pass in effect1.CurrentTechnique.Passes)
+ {
+ // apply the pass, effectively choosing which vertex shader and fragment (pixel) shader to use
+ pass.Apply();
+
+ // draw the geometry from the vertex buffer / index buffer
+ _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, layerModel.TriangleCount);
+ }
+ }
+ }
+ }
+
+ private Matrix IncludeParallax(Matrix viewMatrix, Vector2 parallaxFactor)
+ {
+ viewMatrix.Translation *=new Vector3(parallaxFactor, 1f);
+ return viewMatrix;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs
new file mode 100644
index 0000000..34bf683
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public sealed class TiledMapStaticLayerModel : TiledMapLayerModel
+ {
+ public TiledMapStaticLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices)
+ : base(graphicsDevice, texture, vertices, indices)
+ {
+ }
+
+ protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount)
+ {
+ return new VertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly);
+ }
+
+ protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount)
+ {
+ return new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs
new file mode 100644
index 0000000..8c7d49b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapStaticLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapStaticLayerModel>
+ {
+ protected override void ClearBuffers()
+ {
+ }
+
+ protected override TiledMapStaticLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ return new TiledMapStaticLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray());
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs
new file mode 100644
index 0000000..30ecbfd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "map")]
+ public class TiledMapContent
+ {
+ public TiledMapContent()
+ {
+ Properties = new List<TiledMapPropertyContent>();
+ Tilesets = new List<TiledMapTilesetContent>();
+ Layers = new List<TiledMapLayerContent>();
+ }
+
+ [XmlIgnore]
+ public string Name { get; set; }
+
+ [XmlIgnore]
+ public string FilePath { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "version")]
+ public string Version { get; set; }
+
+ [XmlAttribute(AttributeName = "orientation")]
+ public TiledMapOrientationContent Orientation { get; set; }
+
+ [XmlAttribute(AttributeName = "renderorder")]
+ public TiledMapTileDrawOrderContent RenderOrder { get; set; }
+
+ [XmlAttribute(AttributeName = "backgroundcolor")]
+ public string BackgroundColor { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlAttribute(AttributeName = "tilewidth")]
+ public int TileWidth { get; set; }
+
+ [XmlAttribute(AttributeName = "tileheight")]
+ public int TileHeight { get; set; }
+
+ [XmlAttribute(AttributeName = "hexsidelength")]
+ public int HexSideLength { get; set; }
+
+ [XmlAttribute(AttributeName = "staggeraxis")]
+ public TiledMapStaggerAxisContent StaggerAxis { get; set; }
+
+ [XmlAttribute(AttributeName = "staggerindex")]
+ public TiledMapStaggerIndexContent StaggerIndex { get; set; }
+
+ [XmlElement(ElementName = "tileset")]
+ public List<TiledMapTilesetContent> Tilesets { get; set; }
+
+ [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))]
+ [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))]
+ [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))]
+ [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))]
+ public List<TiledMapLayerContent> Layers { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs
new file mode 100644
index 0000000..78f7bdd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs
@@ -0,0 +1,6 @@
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapEllipseContent
+ {
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs
new file mode 100644
index 0000000..a72b276
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapGroupLayerContent : TiledMapLayerContent
+ {
+ protected TiledMapGroupLayerContent()
+ : base(TiledMapLayerType.GroupLayer)
+ {
+ }
+
+ [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))]
+ [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))]
+ [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))]
+ [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))]
+ public List<TiledMapLayerContent> Layers { get; set; }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs
new file mode 100644
index 0000000..714da37
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs
@@ -0,0 +1,44 @@
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapImageContent
+ {
+ //[XmlIgnore]
+ //public Texture2DContent Content { get; set; }
+
+ //[XmlIgnore]
+ //public ExternalReference<Texture2DContent> ContentRef { get; set; }
+
+ [XmlAttribute(AttributeName = "source")]
+ public string Source { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlAttribute(AttributeName = "format")]
+ public string Format { get; set; }
+
+ [XmlAttribute(AttributeName = "trans")]
+ public string RawTransparentColor { get; set; } = string.Empty;
+
+ [XmlIgnore]
+ public Color TransparentColor
+ {
+ get => RawTransparentColor == string.Empty ? Color.Transparent : ColorHelper.FromHex(RawTransparentColor);
+ set => RawTransparentColor = ColorHelper.ToHex(value);
+ }
+
+ [XmlElement(ElementName = "data")]
+ public TiledMapTileLayerDataContent Data { get; set; }
+
+ public override string ToString()
+ {
+ return Source;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs
new file mode 100644
index 0000000..f347d1e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapImageLayerContent : TiledMapLayerContent
+ {
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ public TiledMapImageLayerContent()
+ : base(TiledMapLayerType.ImageLayer)
+ {
+ Opacity = 1.0f;
+ Visible = true;
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}: {Image}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs
new file mode 100644
index 0000000..01d8a9d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlInclude(typeof(TiledMapTileLayerContent))]
+ [XmlInclude(typeof(TiledMapImageLayerContent))]
+ [XmlInclude(typeof(TiledMapObjectLayerContent))]
+ public abstract class TiledMapLayerContent
+ {
+ protected TiledMapLayerContent(TiledMapLayerType layerType)
+ {
+ LayerType = layerType;
+ Opacity = 1.0f;
+ ParallaxX = 1.0f;
+ ParallaxY = 1.0f;
+ Visible = true;
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "opacity")]
+ public float Opacity { get; set; }
+
+ [XmlAttribute(AttributeName = "visible")]
+ public bool Visible { get; set; }
+
+ [XmlAttribute(AttributeName = "offsetx")]
+ public float OffsetX { get; set; }
+
+ [XmlAttribute(AttributeName = "offsety")]
+ public float OffsetY { get; set; }
+
+ [XmlAttribute(AttributeName = "parallaxx")]
+ public float ParallaxX { get; set; }
+
+ [XmlAttribute(AttributeName = "parallaxy")]
+ public float ParallaxY { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlIgnore]
+ public TiledMapLayerType LayerType { get; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs
new file mode 100644
index 0000000..4c2a761
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapLayerModelContent
+ {
+ private readonly List<VertexPositionTexture> _vertices;
+ private readonly List<ushort> _indices;
+
+ public string LayerName { get; }
+ public ReadOnlyCollection<VertexPositionTexture> Vertices { get; }
+ public ReadOnlyCollection<ushort> Indices { get; }
+ public Size2 ImageSize { get; }
+ public string TextureAssetName { get; }
+
+ public TiledMapLayerModelContent(string layerName, TiledMapImageContent image)
+ {
+ LayerName = layerName;
+ _vertices = new List<VertexPositionTexture>();
+ Vertices = new ReadOnlyCollection<VertexPositionTexture>(_vertices);
+ _indices = new List<ushort>();
+ Indices = new ReadOnlyCollection<ushort>(_indices);
+ ImageSize = new Size2(image.Width, image.Height);
+ TextureAssetName = Path.ChangeExtension(image.Source, null);
+ }
+
+ public TiledMapLayerModelContent(string layerName, TiledMapTilesetContent tileset)
+ : this(layerName, tileset.Image)
+ {
+ }
+
+ public void AddTileVertices(Point2 position, Rectangle? sourceRectangle = null, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None)
+ {
+ float texelLeft, texelTop, texelRight, texelBottom;
+ var sourceRectangle1 = sourceRectangle ?? new Rectangle(0, 0, (int)ImageSize.Width, (int)ImageSize.Height);
+
+ if (sourceRectangle.HasValue)
+ {
+ var reciprocalWidth = 1f / ImageSize.Width;
+ var reciprocalHeight = 1f / ImageSize.Height;
+ texelLeft = sourceRectangle1.X * reciprocalWidth;
+ texelTop = sourceRectangle1.Y * reciprocalHeight;
+ texelRight = (sourceRectangle1.X + sourceRectangle1.Width) * reciprocalWidth;
+ texelBottom = (sourceRectangle1.Y + sourceRectangle1.Height) * reciprocalHeight;
+ }
+ else
+ {
+ texelLeft = 0;
+ texelTop = 0;
+ texelBottom = 1;
+ texelRight = 1;
+ }
+
+ VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight;
+
+ vertexTopLeft.Position = new Vector3(position, 0);
+ vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle1.Width, 0), 0);
+ vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle1.Height), 0);
+ vertexBottomRight.Position =
+ new Vector3(position + new Vector2(sourceRectangle1.Width, sourceRectangle1.Height), 0);
+
+ vertexTopLeft.TextureCoordinate.Y = texelTop;
+ vertexTopLeft.TextureCoordinate.X = texelLeft;
+
+ vertexTopRight.TextureCoordinate.Y = texelTop;
+ vertexTopRight.TextureCoordinate.X = texelRight;
+
+ vertexBottomLeft.TextureCoordinate.Y = texelBottom;
+ vertexBottomLeft.TextureCoordinate.X = texelLeft;
+
+ vertexBottomRight.TextureCoordinate.Y = texelBottom;
+ vertexBottomRight.TextureCoordinate.X = texelRight;
+
+ var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ }
+
+ if (flipVertically)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+
+ _vertices.Add(vertexTopLeft);
+ _vertices.Add(vertexTopRight);
+ _vertices.Add(vertexBottomLeft);
+ _vertices.Add(vertexBottomRight);
+
+ Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel);
+ }
+
+ public void AddTileIndices()
+ {
+ var indexOffset = Vertices.Count;
+
+ Debug.Assert(3 + indexOffset <= TiledMapHelper.MaximumVerticesPerModel);
+
+ _indices.Add((ushort)(0 + indexOffset));
+ _indices.Add((ushort)(1 + indexOffset));
+ _indices.Add((ushort)(2 + indexOffset));
+ _indices.Add((ushort)(1 + indexOffset));
+ _indices.Add((ushort)(3 + indexOffset));
+ _indices.Add((ushort)(2 + indexOffset));
+
+ Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs
new file mode 100644
index 0000000..76e2ba2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ // This content class is going to be a lot more complex than the others we use.
+ // Objects can reference a template file which has starting values for the
+ // object. The value in the object file overrides any value specified in the
+ // template. All values have to be able to store a null value so we know if the
+ // XML parser actually found a value for the property and not just a default
+ // value. Default values are used when the object and any templates don't
+ // specify a value.
+ public class TiledMapObjectContent
+ {
+ // TODO: HACK These shouldn't be public
+ public uint? _globalIdentifier;
+ public int? _identifier;
+ public float? _height;
+ public float? _rotation;
+ public bool? _visible;
+ public float? _width;
+ public float? _x;
+ public float? _y;
+
+ [XmlAttribute(DataType = "int", AttributeName = "id")]
+ public int Identifier { get => _identifier ?? 0; set => _identifier = value; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "x")]
+ public float X { get => _x ?? 0; set => _x = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "y")]
+ public float Y { get => _y ?? 0; set => _y = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "width")]
+ public float Width { get => _width ?? 0; set => _width = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "height")]
+ public float Height { get => _height ?? 0; set => _height = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "rotation")]
+ public float Rotation { get => _rotation ?? 0; set => _rotation = value; }
+
+ [XmlAttribute(DataType = "boolean", AttributeName = "visible")]
+ public bool Visible { get => _visible ?? true; set => _visible = value; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlAttribute(DataType = "unsignedInt", AttributeName = "gid")]
+ public uint GlobalIdentifier { get => _globalIdentifier??0; set => _globalIdentifier = value; }
+
+ [XmlElement(ElementName = "ellipse")]
+ public TiledMapEllipseContent Ellipse { get; set; }
+
+ [XmlElement(ElementName = "polygon")]
+ public TiledMapPolygonContent Polygon { get; set; }
+
+ [XmlElement(ElementName = "polyline")]
+ public TiledMapPolylineContent Polyline { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "template")]
+ public string TemplateSource { get; set; }
+
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs
new file mode 100644
index 0000000..f441375
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapObjectDrawOrderContent : byte
+ {
+ [XmlEnum(Name = "topdown")] TopDown,
+ [XmlEnum(Name = "index")] Manual
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs
new file mode 100644
index 0000000..30669b5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapObjectLayerContent : TiledMapLayerContent
+ {
+ public TiledMapObjectLayerContent()
+ : base(TiledMapLayerType.ObjectLayer)
+ {
+ Objects = new List<TiledMapObjectContent>();
+ }
+
+ [XmlAttribute(AttributeName = "color")]
+ public string Color { get; set; }
+
+ [XmlElement(ElementName = "object")]
+ public List<TiledMapObjectContent> Objects { get; set; }
+
+ [XmlAttribute(AttributeName = "draworder")]
+ public TiledMapObjectDrawOrderContent DrawOrder { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs
new file mode 100644
index 0000000..096f528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "template")]
+ public class TiledMapObjectTemplateContent
+ {
+ [XmlElement(ElementName = "tileset")]
+ public TiledMapTilesetContent Tileset { get; set; }
+
+ //[XmlIgnore]
+ //public ExternalReference<TiledMapTilesetContent> TilesetReference { get; set; }
+
+ [XmlElement(ElementName = "object")]
+ public TiledMapObjectContent Object { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs
new file mode 100644
index 0000000..af83824
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs
@@ -0,0 +1,12 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapOrientationContent : byte
+ {
+ [XmlEnum(Name = "orthogonal")] Orthogonal,
+ [XmlEnum(Name = "isometric")] Isometric,
+ [XmlEnum(Name = "staggered")] Staggered,
+ [XmlEnum(Name = "hexagonal")] Hexagonal
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs
new file mode 100644
index 0000000..e4a3d5e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPolygonContent
+ {
+ [XmlAttribute(AttributeName = "points")]
+ public string Points { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs
new file mode 100644
index 0000000..ba15f59
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPolylineContent
+ {
+ [XmlAttribute(AttributeName = "points")]
+ public string Points { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs
new file mode 100644
index 0000000..10cdfc1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPropertyContent
+ {
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ [XmlAttribute(AttributeName = "value")]
+ public string ValueAttribute { get; set; }
+
+ [XmlText]
+ public string ValueBody { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ public string Value => ValueAttribute ?? ValueBody;
+
+ public override string ToString()
+ {
+ return $"{Name}: {Value}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs
new file mode 100644
index 0000000..7a073f5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapStaggerAxisContent : byte
+ {
+ [XmlEnum("x")]X,
+ [XmlEnum("y")]Y
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs
new file mode 100644
index 0000000..834c3a6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapStaggerIndexContent : byte
+ {
+ [XmlEnum("even")]Even,
+ [XmlEnum("odd")]Odd
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs
new file mode 100644
index 0000000..fcd7a69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs
@@ -0,0 +1,9 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public struct TiledMapTileContent
+ {
+ [XmlAttribute(AttributeName = "gid")] public uint GlobalIdentifier;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs
new file mode 100644
index 0000000..4762afd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs
@@ -0,0 +1,12 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapTileDrawOrderContent : byte
+ {
+ [XmlEnum(Name = "right-down")] RightDown,
+ [XmlEnum(Name = "right-up")] RightUp,
+ [XmlEnum(Name = "left-down")] LeftDown,
+ [XmlEnum(Name = "left-up")] LeftUp
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs
new file mode 100644
index 0000000..49f5ccf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs
@@ -0,0 +1,30 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerContent : TiledMapLayerContent
+ {
+ public TiledMapTileLayerContent()
+ : base(TiledMapLayerType.TileLayer)
+ {
+ }
+
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlElement(ElementName = "data")]
+ public TiledMapTileLayerDataContent Data { get; set; }
+
+ [XmlIgnore]
+ public TiledMapTile[] Tiles { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs
new file mode 100644
index 0000000..0ec10d9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerDataChunkContent
+ {
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTileContent> Tiles { get; set; }
+
+ [XmlText]
+ public string Value { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs
new file mode 100644
index 0000000..c19a02e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerDataContent
+ {
+ public TiledMapTileLayerDataContent()
+ {
+ Tiles = new List<TiledMapTileContent>();
+ }
+
+ [XmlAttribute(AttributeName = "encoding")]
+ public string Encoding { get; set; }
+
+ [XmlAttribute(AttributeName = "compression")]
+ public string Compression { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTileContent> Tiles { get; set; }
+
+ [XmlElement(ElementName = "chunk")]
+ public List<TiledMapTileLayerDataChunkContent> Chunks { get; set; }
+
+ [XmlText]
+ public string Value { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Encoding} {Compression}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs
new file mode 100644
index 0000000..7d2af87
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "tileoffset")]
+ public class TiledMapTileOffsetContent
+ {
+ [XmlAttribute(AttributeName = "x")] public int X;
+
+ [XmlAttribute(AttributeName = "y")] public int Y;
+
+ public override string ToString()
+ {
+ return $"{X}, {Y}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs
new file mode 100644
index 0000000..fa04c31
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "tileset")]
+ public class TiledMapTilesetContent
+ {
+ public TiledMapTilesetContent()
+ {
+ TileOffset = new TiledMapTileOffsetContent();
+ Tiles = new List<TiledMapTilesetTileContent>();
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ [XmlAttribute(AttributeName = "firstgid")]
+ public int FirstGlobalIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "source")]
+ public string Source { get; set; }
+
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "tilewidth")]
+ public int TileWidth { get; set; }
+
+ [XmlAttribute(AttributeName = "tileheight")]
+ public int TileHeight { get; set; }
+
+ [XmlAttribute(AttributeName = "spacing")]
+ public int Spacing { get; set; }
+
+ [XmlAttribute(AttributeName = "margin")]
+ public int Margin { get; set; }
+
+ [XmlAttribute(AttributeName = "columns")]
+ public int Columns { get; set; }
+
+ [XmlAttribute(AttributeName = "tilecount")]
+ public int TileCount { get; set; }
+
+ [XmlElement(ElementName = "tileoffset")]
+ public TiledMapTileOffsetContent TileOffset { get; set; }
+
+ [XmlElement(ElementName = "grid")]
+ public TiledMapTilesetGridContent Grid { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTilesetTileContent> Tiles { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ public bool ContainsGlobalIdentifier(int globalIdentifier)
+ {
+ return globalIdentifier >= FirstGlobalIdentifier && globalIdentifier < FirstGlobalIdentifier + TileCount;
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}: {Image}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs
new file mode 100644
index 0000000..a9071b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs
@@ -0,0 +1,16 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetGridContent
+ {
+ [XmlAttribute(AttributeName = "orientation")]
+ public TiledMapOrientationContent Orientation { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs
new file mode 100644
index 0000000..5a17137
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs
@@ -0,0 +1,18 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetTileAnimationFrameContent
+ {
+ [XmlAttribute(AttributeName = "tileid")]
+ public int TileIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "duration")]
+ public int Duration { get; set; }
+
+ public override string ToString()
+ {
+ return $"TileID: {TileIdentifier}, Duration: {Duration}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs
new file mode 100644
index 0000000..2a82197
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetTileContent
+ {
+ public TiledMapTilesetTileContent()
+ {
+ Properties = new List<TiledMapPropertyContent>();
+ Type = string.Empty;
+ }
+
+ [XmlAttribute(AttributeName = "id")]
+ public int LocalIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ [XmlArray("objectgroup")]
+ [XmlArrayItem("object")]
+ public List<TiledMapObjectContent> Objects { get; set; }
+
+ [XmlArray("animation")]
+ [XmlArrayItem("frame")]
+ public List<TiledMapTilesetTileAnimationFrameContent> Frames { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ public override string ToString()
+ {
+ return LocalIdentifier.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs
new file mode 100644
index 0000000..b3486bd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMap
+ {
+ private readonly List<TiledMapImageLayer> _imageLayers = new List<TiledMapImageLayer>();
+ private readonly List<TiledMapLayer> _layers = new List<TiledMapLayer>();
+ private readonly Dictionary<string, TiledMapLayer> _layersByName = new Dictionary<string, TiledMapLayer>();
+ private readonly List<TiledMapObjectLayer> _objectLayers = new List<TiledMapObjectLayer>();
+ private readonly List<TiledMapTileLayer> _tileLayers = new List<TiledMapTileLayer>();
+ private readonly List<TiledMapTileset> _tilesets = new List<TiledMapTileset>();
+ private readonly List<Tuple<TiledMapTileset, int>> _firstGlobalIdentifiers = new List<Tuple<TiledMapTileset, int>>();
+
+ public string Name { get; }
+ public string Type { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public TiledMapTileDrawOrder RenderOrder { get; }
+ public TiledMapOrientation Orientation { get; }
+ public TiledMapProperties Properties { get; }
+ public ReadOnlyCollection<TiledMapTileset> Tilesets { get; }
+ public ReadOnlyCollection<TiledMapLayer> Layers { get; }
+ public ReadOnlyCollection<TiledMapImageLayer> ImageLayers { get; }
+ public ReadOnlyCollection<TiledMapTileLayer> TileLayers { get; }
+ public ReadOnlyCollection<TiledMapObjectLayer> ObjectLayers { get; }
+
+ public Color? BackgroundColor { get; set; }
+ public int WidthInPixels => Width * TileWidth;
+ public int HeightInPixels => Height * TileHeight;
+
+ private TiledMap()
+ {
+ Layers = new ReadOnlyCollection<TiledMapLayer>(_layers);
+ ImageLayers = new ReadOnlyCollection<TiledMapImageLayer>(_imageLayers);
+ TileLayers = new ReadOnlyCollection<TiledMapTileLayer>(_tileLayers);
+ ObjectLayers = new ReadOnlyCollection<TiledMapObjectLayer>(_objectLayers);
+ Tilesets = new ReadOnlyCollection<TiledMapTileset>(_tilesets);
+ Properties = new TiledMapProperties();
+ }
+
+ public TiledMap(string name, string type, int width, int height, int tileWidth, int tileHeight, TiledMapTileDrawOrder renderOrder, TiledMapOrientation orientation, Color? backgroundColor = null)
+ : this()
+ {
+ Name = name;
+ Type = type;
+ Width = width;
+ Height = height;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ RenderOrder = renderOrder;
+ Orientation = orientation;
+ BackgroundColor = backgroundColor;
+ }
+
+ public void AddTileset(TiledMapTileset tileset, int firstGlobalIdentifier)
+ {
+ _tilesets.Add(tileset);
+ _firstGlobalIdentifiers.Add(new Tuple<TiledMapTileset, int>(tileset, firstGlobalIdentifier));
+ }
+
+ public void AddLayer(TiledMapLayer layer)
+ => AddLayer(layer, true);
+
+ private void AddLayer(TiledMapLayer layer, bool root)
+ {
+ if (root) _layers.Add(layer);
+
+ if (_layersByName.ContainsKey(layer.Name))
+ throw new ArgumentException($"The TiledMap '{Name}' contains two or more layers named '{layer.Name}'. Please ensure all layers have unique names.");
+
+ _layersByName.Add(layer.Name, layer);
+
+ switch(layer)
+ {
+ case TiledMapImageLayer imageLayer:
+ _imageLayers.Add(imageLayer);
+ break;
+ case TiledMapTileLayer tileLayer:
+ _tileLayers.Add(tileLayer);
+ break;
+ case TiledMapObjectLayer objectLayer:
+ _objectLayers.Add(objectLayer);
+ break;
+ case TiledMapGroupLayer groupLayer:
+ foreach (var subLayer in groupLayer.Layers)
+ AddLayer(subLayer, false);
+ break;
+ }
+ }
+
+ public TiledMapLayer GetLayer(string layerName)
+ {
+ TiledMapLayer layer;
+ _layersByName.TryGetValue(layerName, out layer);
+ return layer;
+ }
+
+ public T GetLayer<T>(string layerName)
+ where T : TiledMapLayer
+ {
+ return GetLayer(layerName) as T;
+ }
+
+ public TiledMapTileset GetTilesetByTileGlobalIdentifier(int tileIdentifier)
+ {
+ foreach (var tileset in _firstGlobalIdentifiers)
+ {
+ if (tileIdentifier >= tileset.Item2 && tileIdentifier < tileset.Item2 + tileset.Item1.TileCount)
+ return tileset.Item1;
+ }
+
+ return null;
+ }
+
+ public int GetTilesetFirstGlobalIdentifier(TiledMapTileset tileset)
+ {
+ return _firstGlobalIdentifiers.FirstOrDefault(t => t.Item1 == tileset).Item2;
+ }
+
+ private static int CountLayers(TiledMapLayer layer)
+ {
+ var value = 0;
+ if (layer is TiledMapGroupLayer groupLayer)
+ foreach (var subLayer in groupLayer.Layers)
+ value += CountLayers(subLayer);
+ else
+ value = 1;
+
+ return value;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs
new file mode 100644
index 0000000..9140d62
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapEllipseObject : TiledMapObject
+ {
+ public TiledMapEllipseObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Radius = new Vector2(size.Width / 2.0f, size.Height / 2.0f);
+ Center = new Vector2(position.X + Radius.X, position.Y);
+ }
+
+ public Vector2 Center { get; }
+ public Vector2 Radius { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs
new file mode 100644
index 0000000..48d43d1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapGroupLayer : TiledMapLayer
+ {
+ public List<TiledMapLayer> Layers { get; }
+ public TiledMapGroupLayer(string name, string type, List<TiledMapLayer> layers, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Layers = layers;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs
new file mode 100644
index 0000000..43edf67
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs
@@ -0,0 +1,51 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public static class TiledMapHelper
+ {
+ // 4 vertices per tile
+ public const int VerticesPerTile = 4;
+ // 2 triangles per tile (mesh), with each triangle indexing 3 out of 4 vertices, so 6 vertices
+ public const int IndicesPerTile = 6;
+ // by using ushort type for indices we are limited to indexing vertices from 0 to 65535
+ // this limits us on how many vertices can fit inside a single vertex buffer (65536 vertices)
+ public const int MaximumVerticesPerModel = ushort.MaxValue + 1;
+ // and thus, we know how many tiles we can fit inside a vertex or index buffer (16384 tiles)
+ public const int MaximumTilesPerGeometryContent = MaximumVerticesPerModel / VerticesPerTile;
+ // and thus, we also know the maximum number of indices we can fit inside a single index buffer (98304 indices)
+ public const int MaximumIndicesPerModel = MaximumTilesPerGeometryContent * IndicesPerTile;
+ // these optimal maximum numbers of course are not considering texture bindings which would practically lower the actual number of tiles per vertex / index buffer
+ // thus, the reason why it is a good to have ONE giant tileset (at least per layer)
+
+ internal static Rectangle GetTileSourceRectangle(int localTileIdentifier, int tileWidth, int tileHeight, int columns, int margin, int spacing)
+ {
+ var x = margin + localTileIdentifier % columns * (tileWidth + spacing);
+ var y = margin + localTileIdentifier / columns * (tileHeight + spacing);
+ return new Rectangle(x, y, tileWidth, tileHeight);
+ }
+
+ internal static Point2 GetOrthogonalPosition(int tileX, int tileY, int tileWidth, int tileHeight)
+ {
+ var x = tileX * tileWidth;
+ var y = tileY * tileHeight;
+ return new Vector2(x, y);
+ }
+
+ internal static Point2 GetIsometricPosition(int tileX, int tileY, int tileWidth, int tileHeight)
+ {
+ // You can think of an isometric Tiled map as a regular orthogonal map that is rotated -45 degrees
+ // i.e.: the origin (0, 0) is the top tile of the diamond grid;
+ // (mapWidth, 0) is the far right tile of the diamond grid
+ // (0, mapHeight) is the far left tile of the diamond grid
+ // (mapWidth, mapHeight) is the bottom tile of the diamond grid
+
+ var halfTileWidth = tileWidth * 0.5f;
+ var halfTileHeight = tileHeight * 0.5f;
+ // -1 because we want the top the tile-diamond (top-center) to be the origin in tile space
+ var x = (tileX - tileY - 1) * halfTileWidth;
+ var y = (tileX + tileY) * halfTileHeight;
+ return new Vector2(x, y);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs
new file mode 100644
index 0000000..33bb700
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapImageLayer : TiledMapLayer, IMovable
+ {
+ public TiledMapImageLayer(string name, string type, Texture2D image, Vector2? position = null, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Image = image;
+ Position = position ?? Vector2.Zero;
+ }
+
+ public Texture2D Image { get; }
+ public Vector2 Position { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs
new file mode 100644
index 0000000..6226dd1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public abstract class TiledMapLayer
+ {
+ public string Name { get; }
+ public string Type { get; }
+ public bool IsVisible { get; set; }
+ public float Opacity { get; set; }
+ public Vector2 Offset { get; set; }
+ public Vector2 ParallaxFactor { get; set; }
+ public TiledMapProperties Properties { get; }
+
+ protected TiledMapLayer(string name, string type, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ {
+ Name = name;
+ Type = type;
+ Offset = offset ?? Vector2.Zero;
+ ParallaxFactor = parallaxFactor ?? Vector2.One;
+ Opacity = opacity;
+ IsVisible = isVisible;
+ Properties = new TiledMapProperties();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs
new file mode 100644
index 0000000..8af5a2e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapLayerType : byte
+ {
+ ImageLayer = 0,
+ TileLayer = 1,
+ ObjectLayer = 2,
+ GroupLayer = 3
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs
new file mode 100644
index 0000000..1e25084
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs
@@ -0,0 +1,35 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public abstract class TiledMapObject
+ {
+ protected TiledMapObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ {
+ Identifier = identifier;
+ Name = name;
+ IsVisible = isVisible;
+ Rotation = rotation;
+ Position = position;
+ Size = size;
+ Opacity = opacity;
+ Type = type;
+ Properties = new TiledMapProperties();
+ }
+
+ public int Identifier { get; }
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public bool IsVisible { get; set; }
+ public float Opacity { get; set; }
+ public float Rotation { get; set; }
+ public Vector2 Position { get; }
+ public Size2 Size { get; set; }
+ public TiledMapProperties Properties { get; }
+
+ public override string ToString()
+ {
+ return $"{Identifier}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs
new file mode 100644
index 0000000..1668f66
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapObjectDrawOrder : byte
+ {
+ TopDown,
+ Index,
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs
new file mode 100644
index 0000000..b39b8d7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs
@@ -0,0 +1,20 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapObjectLayer : TiledMapLayer
+ {
+ public TiledMapObjectLayer(string name, string type, TiledMapObject[] objects, Color? color = null, TiledMapObjectDrawOrder drawOrder = TiledMapObjectDrawOrder.TopDown,
+ Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Color = color;
+ DrawOrder = drawOrder;
+ Objects = objects;
+ }
+
+ public Color? Color { get; }
+ public TiledMapObjectDrawOrder DrawOrder { get; }
+ public TiledMapObject[] Objects { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs
new file mode 100644
index 0000000..305500a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs
@@ -0,0 +1,11 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapObjectType : byte
+ {
+ Rectangle,
+ Ellipse,
+ Polygon,
+ Polyline,
+ Tile
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs
new file mode 100644
index 0000000..798c1c8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs
@@ -0,0 +1,9 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapOrientation
+ {
+ Orthogonal,
+ Isometric,
+ Staggered
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs
new file mode 100644
index 0000000..2df343f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapPolygonObject : TiledMapObject
+ {
+ public TiledMapPolygonObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Points = points;
+ }
+
+ public Point2[] Points { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs
new file mode 100644
index 0000000..de50dbc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapPolylineObject : TiledMapObject
+ {
+ public TiledMapPolylineObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Points = points;
+ }
+
+ public Point2[] Points { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs
new file mode 100644
index 0000000..651ae5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapProperties : Dictionary<string, TiledMapPropertyValue>
+ {
+ public bool TryGetValue(string key, out string value)
+ {
+ bool result = TryGetValue(key, out TiledMapPropertyValue tmpVal);
+ value = result ? null : tmpVal.Value;
+ return result;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs
new file mode 100644
index 0000000..d7a0893
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs
@@ -0,0 +1,32 @@
+namespace MonoGame.Extended.Tiled;
+
+public class TiledMapPropertyValue
+{
+ public string Value { get; }
+
+ public TiledMapProperties Properties;
+
+ public TiledMapPropertyValue()
+ {
+ Value = string.Empty;
+ Properties = new();
+ }
+
+ public TiledMapPropertyValue(string value)
+ {
+ Value = value;
+ Properties = new();
+ }
+
+ public TiledMapPropertyValue(TiledMapProperties properties)
+ {
+ Value = string.Empty;
+ Properties = properties;
+ }
+
+ public override string ToString() => Value;
+
+ //public static implicit operator TiledMapPropertyValue(string value) => new(value);
+ public static implicit operator string(TiledMapPropertyValue value) => value.Value;
+ public static implicit operator TiledMapProperties(TiledMapPropertyValue value) => value.Properties;
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs
new file mode 100644
index 0000000..bd49655
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapReader : ContentTypeReader<TiledMap>
+ {
+ protected override TiledMap Read(ContentReader reader, TiledMap existingInstance)
+ {
+ if (existingInstance != null)
+ return existingInstance;
+
+ var map = ReadTiledMap(reader);
+ reader.ReadTiledMapProperties(map.Properties);
+ ReadTilesets(reader, map);
+ ReadLayers(reader, map);
+ return map;
+ }
+
+ private static TiledMap ReadTiledMap(ContentReader reader)
+ {
+ var name = reader.AssetName;
+ var type = reader.ReadString();
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var tileWidth = reader.ReadInt32();
+ var tileHeight = reader.ReadInt32();
+ var backgroundColor = reader.ReadColor();
+ var renderOrder = (TiledMapTileDrawOrder)reader.ReadByte();
+ var orientation = (TiledMapOrientation)reader.ReadByte();
+
+ return new TiledMap(name, type, width, height, tileWidth, tileHeight, renderOrder, orientation, backgroundColor);
+ }
+
+ private static void ReadTilesets(ContentReader reader, TiledMap map)
+ {
+ var tilesetCount = reader.ReadInt32();
+
+ for (var i = 0; i < tilesetCount; i++)
+ {
+ var firstGlobalIdentifier = reader.ReadInt32();
+ var tileset = ReadTileset(reader, map);
+ map.AddTileset(tileset, firstGlobalIdentifier);
+ }
+ }
+
+ private static TiledMapTileset ReadTileset(ContentReader reader, TiledMap map)
+ {
+ var external = reader.ReadBoolean();
+ var tileset = external ? reader.ReadExternalReference<TiledMapTileset>() : TiledMapTilesetReader.ReadTileset(reader);
+
+ return tileset;
+ }
+
+ private static void ReadLayers(ContentReader reader, TiledMap map)
+ {
+ foreach (var layer in ReadGroup(reader, map))
+ map.AddLayer(layer);
+ }
+ private static List<TiledMapLayer> ReadGroup(ContentReader reader, TiledMap map)
+ {
+ var layerCount = reader.ReadInt32();
+ var value = new List<TiledMapLayer>(layerCount);
+
+ for (var i = 0; i < layerCount; i++)
+ value.Add(ReadLayer(reader, map));
+
+ return value;
+ }
+
+ private static TiledMapLayer ReadLayer(ContentReader reader, TiledMap map)
+ {
+ var layerType = (TiledMapLayerType)reader.ReadByte();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var isVisible = reader.ReadBoolean();
+ var opacity = reader.ReadSingle();
+ var offsetX = reader.ReadSingle();
+ var offsetY = reader.ReadSingle();
+ var offset = new Vector2(offsetX, offsetY);
+ var parallaxX = reader.ReadSingle();
+ var parallaxY = reader.ReadSingle();
+ var parallaxFactor = new Vector2(parallaxX, parallaxY);
+ var properties = new TiledMapProperties();
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapLayer layer;
+
+ switch (layerType)
+ {
+ case TiledMapLayerType.ImageLayer:
+ layer = ReadImageLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible);
+ break;
+ case TiledMapLayerType.TileLayer:
+ layer = ReadTileLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map);
+ break;
+ case TiledMapLayerType.ObjectLayer:
+ layer = ReadObjectLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map);
+ break;
+ case TiledMapLayerType.GroupLayer:
+ layer = new TiledMapGroupLayer(name, type, ReadGroup(reader, map), offset, parallaxFactor, opacity, isVisible);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ layer.Properties.Add(property.Key, property.Value);
+
+ return layer;
+ }
+
+ private static TiledMapLayer ReadObjectLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map)
+ {
+ var color = reader.ReadColor();
+ var drawOrder = (TiledMapObjectDrawOrder)reader.ReadByte();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, map);
+
+ return new TiledMapObjectLayer(name, type, objects, color, drawOrder, offset, parallaxFactor, opacity, isVisible);
+ }
+
+ private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMap map)
+ {
+ var objectType = (TiledMapObjectType)reader.ReadByte();
+ var identifier = reader.ReadInt32();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var position = new Vector2(reader.ReadSingle(), reader.ReadSingle());
+ var width = reader.ReadSingle();
+ var height = reader.ReadSingle();
+ var size = new Size2(width, height);
+ var rotation = reader.ReadSingle();
+ var isVisible = reader.ReadBoolean();
+ var properties = new TiledMapProperties();
+ const float opacity = 1.0f;
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapObject mapObject;
+
+ switch (objectType)
+ {
+ case TiledMapObjectType.Rectangle:
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Tile:
+ var globalTileIdentifierWithFlags = reader.ReadUInt32();
+ var tile = new TiledMapTile(globalTileIdentifierWithFlags, (ushort)position.X, (ushort)position.Y);
+ var tileset = map.GetTilesetByTileGlobalIdentifier(tile.GlobalIdentifier);
+ var localTileIdentifier = tile.GlobalIdentifier - map.GetTilesetFirstGlobalIdentifier(tileset);
+ var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier);
+ mapObject = new TiledMapTileObject(identifier, name, tileset, tilesetTile, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Ellipse:
+ mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible);
+ break;
+ case TiledMapObjectType.Polygon:
+ mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Polyline:
+ mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ mapObject.Properties.Add(property.Key, property.Value);
+
+ return mapObject;
+ }
+
+ private static Point2[] ReadPoints(ContentReader reader)
+ {
+ var pointCount = reader.ReadInt32();
+ var points = new Point2[pointCount];
+
+ for (var i = 0; i < pointCount; i++)
+ {
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ points[i] = new Point2(x, y);
+ }
+
+ return points;
+ }
+
+ private static TiledMapImageLayer ReadImageLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ var position = new Vector2(x, y);
+ return new TiledMapImageLayer(name, type, texture, position, offset, parallaxFactor, opacity, isVisible);
+ }
+
+ private static TiledMapTileLayer ReadTileLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map)
+ {
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var tileWidth = map.TileWidth;
+ var tileHeight = map.TileHeight;
+
+ var tileCount = reader.ReadInt32();
+ var layer = new TiledMapTileLayer(name, type, width, height, tileWidth, tileHeight, offset, parallaxFactor, opacity, isVisible);
+
+ for (var i = 0; i < tileCount; i++)
+ {
+ var globalTileIdentifierWithFlags = reader.ReadUInt32();
+ var x = reader.ReadUInt16();
+ var y = reader.ReadUInt16();
+
+ layer.Tiles[x + y * width] = new TiledMapTile(globalTileIdentifierWithFlags, x, y);
+ }
+
+ return layer;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs
new file mode 100644
index 0000000..de4c7ec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapRectangleObject : TiledMapObject
+ {
+ public TiledMapRectangleObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs
new file mode 100644
index 0000000..d3147a4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs
@@ -0,0 +1,28 @@
+namespace MonoGame.Extended.Tiled
+{
+ public struct TiledMapTile
+ {
+ public readonly ushort X;
+ public readonly ushort Y;
+ public readonly uint GlobalTileIdentifierWithFlags;
+
+ public int GlobalIdentifier => (int)(GlobalTileIdentifierWithFlags & ~(uint)TiledMapTileFlipFlags.All);
+ public bool IsFlippedHorizontally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ public bool IsFlippedVertically => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipVertically) != 0;
+ public bool IsFlippedDiagonally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ public bool IsBlank => GlobalIdentifier == 0;
+ public TiledMapTileFlipFlags Flags => (TiledMapTileFlipFlags)(GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.All);
+
+ public TiledMapTile(uint globalTileIdentifierWithFlags, ushort x, ushort y)
+ {
+ GlobalTileIdentifierWithFlags = globalTileIdentifierWithFlags;
+ X = x;
+ Y = y;
+ }
+
+ public override string ToString()
+ {
+ return $"GlobalIdentifier: {GlobalIdentifier}, Flags: {Flags}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs
new file mode 100644
index 0000000..a40b596
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapTileDrawOrder : byte
+ {
+ RightDown,
+ RightUp,
+ LeftDown,
+ LeftUp
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs
new file mode 100644
index 0000000..f883bad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MonoGame.Extended.Tiled
+{
+ [Flags]
+ public enum TiledMapTileFlipFlags : uint
+ {
+ None = 0,
+ FlipDiagonally = 0x20000000,
+ FlipVertically = 0x40000000,
+ FlipHorizontally = 0x80000000,
+ All = FlipDiagonally | FlipVertically | FlipHorizontally
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs
new file mode 100644
index 0000000..5d07f06
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTileLayer : TiledMapLayer
+ {
+ public TiledMapTileLayer(string name, string type, int width, int height, int tileWidth, int tileHeight, Vector2? offset = null,
+ Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Width = width;
+ Height = height;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Tiles = new TiledMapTile[Width * Height];
+ }
+
+ public int Width { get; }
+ public int Height { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public TiledMapTile[] Tiles { get; }
+
+ public int GetTileIndex(ushort x, ushort y)
+ {
+ return x + y * Width;
+ }
+
+ public bool TryGetTile(ushort x, ushort y, out TiledMapTile? tile)
+ {
+ if (x >= Width)
+ {
+ tile = null;
+ return false;
+ }
+ var index = GetTileIndex(x, y);
+
+ if (index < 0 || index >= Tiles.Length)
+ {
+ tile = null;
+ return false;
+ }
+
+ tile = Tiles[index];
+ return true;
+ }
+
+ public TiledMapTile GetTile(ushort x, ushort y)
+ {
+ var index = GetTileIndex(x, y);
+ return Tiles[index];
+ }
+
+ public void SetTile(ushort x, ushort y, uint globalIdentifier)
+ {
+ var index = GetTileIndex(x, y);
+ Tiles[index] = new TiledMapTile(globalIdentifier, x, y);
+ }
+
+ public void RemoveTile(ushort x, ushort y)
+ {
+ SetTile(x, y, 0);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs
new file mode 100644
index 0000000..b4e8679
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapTileObject : TiledMapObject
+ {
+ public TiledMapTileObject(int identifier, string name, TiledMapTileset tileset, TiledMapTilesetTile tile,
+ Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Tileset = tileset;
+ Tile = tile;
+ }
+
+ public TiledMapTilesetTile Tile { get; }
+ public TiledMapTileset Tileset { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs
new file mode 100644
index 0000000..ed14e15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Tiled
+{
+ public interface ITileset
+ {
+ int ActualWidth { get; }
+ int Columns { get; }
+ int ActualHeight { get; }
+ int Rows { get; }
+ int TileWidth { get; }
+ int TileHeight { get; }
+ Texture2D Texture { get; }
+ TextureRegion2D GetRegion(int column, int row);
+ }
+
+ public class TiledMapTileset : ITileset
+ {
+ public TiledMapTileset(Texture2D texture, string type, int tileWidth, int tileHeight, int tileCount, int spacing, int margin, int columns)
+ {
+ Texture = texture;
+ Type = type;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ TileCount = tileCount;
+ Spacing = spacing;
+ Margin = margin;
+ Columns = columns;
+ Properties = new TiledMapProperties();
+ Tiles = new List<TiledMapTilesetTile>();
+ }
+
+ public string Name => Texture.Name;
+ public Texture2D Texture { get; }
+
+ public TextureRegion2D GetRegion(int column, int row)
+ {
+ var x = Margin + column * (TileWidth + Spacing);
+ var y = Margin + row * (TileHeight + Spacing);
+ return new TextureRegion2D(Texture, x, y, TileWidth, TileHeight);
+ }
+
+ public string Type { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public int Spacing { get; }
+ public int Margin { get; }
+ public int TileCount { get; }
+ public int Columns { get; }
+ public List<TiledMapTilesetTile> Tiles { get; }
+ public TiledMapProperties Properties { get; }
+
+ public int Rows => (int)Math.Ceiling((double) TileCount / Columns);
+ public int ActualWidth => TileWidth * Columns;
+ public int ActualHeight => TileHeight * Rows;
+
+ public Rectangle GetTileRegion(int localTileIdentifier)
+ {
+ return Texture is not null
+ ? TiledMapHelper.GetTileSourceRectangle(localTileIdentifier, TileWidth, TileHeight, Columns, Margin,
+ Spacing)
+ : Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier).Texture.Bounds;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs
new file mode 100644
index 0000000..1bbb770
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTilesetAnimatedTile : TiledMapTilesetTile
+ {
+ private TimeSpan _timer = TimeSpan.Zero;
+ private int _frameIndex;
+
+ public ReadOnlyCollection<TiledMapTilesetTileAnimationFrame> AnimationFrames { get; }
+ public TiledMapTilesetTileAnimationFrame CurrentAnimationFrame { get; private set; }
+
+ public TiledMapTilesetAnimatedTile(int localTileIdentifier,
+ TiledMapTilesetTileAnimationFrame[] frames, string type = null, TiledMapObject[] objects = null, Texture2D texture = null)
+ : base(localTileIdentifier, type, objects, texture)
+ {
+ if (frames.Length == 0) throw new InvalidOperationException("There must be at least one tileset animation frame");
+
+ AnimationFrames = new ReadOnlyCollection<TiledMapTilesetTileAnimationFrame>(frames);
+ CurrentAnimationFrame = AnimationFrames[0];
+ }
+
+ public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ for (int i = 0; i < AnimationFrames.Count; i++)
+ {
+ AnimationFrames[i].CreateTextureRotations(tileset, flipFlags);
+ }
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ _timer += gameTime.ElapsedGameTime;
+
+ if (_timer <= CurrentAnimationFrame.Duration)
+ return;
+
+ _timer -= CurrentAnimationFrame.Duration;
+ _frameIndex = (_frameIndex + 1) % AnimationFrames.Count;
+ CurrentAnimationFrame = AnimationFrames[_frameIndex];
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs
new file mode 100644
index 0000000..646f835
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs
@@ -0,0 +1,150 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTilesetReader : ContentTypeReader<TiledMapTileset>
+ {
+ protected override TiledMapTileset Read(ContentReader reader, TiledMapTileset existingInstance)
+ {
+ if (existingInstance != null)
+ return existingInstance;
+
+ return ReadTileset(reader);
+ }
+
+ public static TiledMapTileset ReadTileset(ContentReader reader)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var @class = reader.ReadString();
+ var tileWidth = reader.ReadInt32();
+ var tileHeight = reader.ReadInt32();
+ var tileCount = reader.ReadInt32();
+ var spacing = reader.ReadInt32();
+ var margin = reader.ReadInt32();
+ var columns = reader.ReadInt32();
+ var explicitTileCount = reader.ReadInt32();
+
+ var tileset = new TiledMapTileset(texture, @class, tileWidth, tileHeight, tileCount, spacing, margin, columns);
+
+ for (var tileIndex = 0; tileIndex < explicitTileCount; tileIndex++)
+ ReadTile(reader, tileset);
+
+ reader.ReadTiledMapProperties(tileset.Properties);
+ return tileset;
+ }
+
+ private static void ReadTile(ContentReader reader, TiledMapTileset tileset)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+
+ var localTileIdentifier = reader.ReadInt32();
+ var type = reader.ReadString();
+ var animationFramesCount = reader.ReadInt32();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, tileset);
+
+ var tilesetTile = animationFramesCount <= 0
+ ? new TiledMapTilesetTile(localTileIdentifier, type, objects, texture)
+ : new TiledMapTilesetAnimatedTile(localTileIdentifier,
+ ReadTiledMapTilesetAnimationFrames(reader, tileset, animationFramesCount), type, objects, texture);
+
+ reader.ReadTiledMapProperties(tilesetTile.Properties);
+ tileset.Tiles.Add(tilesetTile);
+ }
+
+ private static TiledMapTilesetTileAnimationFrame[] ReadTiledMapTilesetAnimationFrames(ContentReader reader, TiledMapTileset tileset, int animationFramesCount)
+ {
+ var animationFrames = new TiledMapTilesetTileAnimationFrame[animationFramesCount];
+
+ for (var i = 0; i < animationFramesCount; i++)
+ {
+ var localTileIdentifierForFrame = reader.ReadInt32();
+ var frameDurationInMilliseconds = reader.ReadInt32();
+ var tileSetTileFrame = new TiledMapTilesetTileAnimationFrame(tileset, localTileIdentifierForFrame, frameDurationInMilliseconds);
+ animationFrames[i] = tileSetTileFrame;
+ }
+
+ return animationFrames;
+ }
+
+ private static TiledMapTilesetTile ReadTiledMapTilesetTile(ContentReader reader, TiledMapTileset tileset, Func<TiledMapObject[], TiledMapTilesetTile> createTile)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, tileset);
+
+ return createTile(objects);
+ }
+
+ private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMapTileset tileset)
+ {
+ var objectType = (TiledMapObjectType)reader.ReadByte();
+ var identifier = reader.ReadInt32();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var position = new Vector2(reader.ReadSingle(), reader.ReadSingle());
+ var width = reader.ReadSingle();
+ var height = reader.ReadSingle();
+ var size = new Size2(width, height);
+ var rotation = reader.ReadSingle();
+ var isVisible = reader.ReadBoolean();
+ var properties = new TiledMapProperties();
+ const float opacity = 1.0f;
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapObject mapObject;
+
+ switch (objectType)
+ {
+ case TiledMapObjectType.Rectangle:
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Tile:
+ reader.ReadUInt32(); // Tile objects within TiledMapTilesetTiles currently ignore the gid and behave like rectangle objects.
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Ellipse:
+ mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible);
+ break;
+ case TiledMapObjectType.Polygon:
+ mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Polyline:
+ mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ mapObject.Properties.Add(property.Key, property.Value);
+
+ return mapObject;
+ }
+
+ private static Point2[] ReadPoints(ContentReader reader)
+ {
+ var pointCount = reader.ReadInt32();
+ var points = new Point2[pointCount];
+
+ for (var i = 0; i < pointCount; i++)
+ {
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ points[i] = new Point2(x, y);
+ }
+
+ return points;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs
new file mode 100644
index 0000000..e378aa3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ [DebuggerDisplay("{LocalTileIdentifier}: Type: {Type}, Properties: {Properties.Count}, Objects: {Objects.Count}")]
+ public class TiledMapTilesetTile
+ {
+ // For remove libraries
+ public TiledMapTilesetTile(int localTileIdentifier, string type = null,
+ TiledMapObject[] objects = null)
+ {
+ LocalTileIdentifier = localTileIdentifier;
+ Type = type;
+ Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>();
+ Properties = new TiledMapProperties();
+ }
+
+ public TiledMapTilesetTile(int localTileIdentifier, string type = null,
+ TiledMapObject[] objects = null, Texture2D texture = null)
+ {
+ Texture = texture;
+ LocalTileIdentifier = localTileIdentifier;
+ Type = type;
+ Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>();
+ Properties = new TiledMapProperties();
+ }
+
+ public int LocalTileIdentifier { get; }
+ public string Type { get; }
+ public TiledMapProperties Properties { get; }
+ public List<TiledMapObject> Objects { get; }
+ public Texture2D Texture { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs
new file mode 100644
index 0000000..9dbca80
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public struct TiledMapTilesetTileAnimationFrame
+ {
+ public readonly int LocalTileIdentifier;
+ public readonly TimeSpan Duration;
+ public readonly Vector2[] TextureCoordinates;
+ private readonly Dictionary<TiledMapTileFlipFlags, Vector2[]> _flipDictionary = new Dictionary<TiledMapTileFlipFlags, Vector2[]>();
+
+ internal TiledMapTilesetTileAnimationFrame(TiledMapTileset tileset, int localTileIdentifier, int durationInMilliseconds)
+ {
+ LocalTileIdentifier = localTileIdentifier;
+ Duration = new TimeSpan(0, 0, 0, 0, durationInMilliseconds);
+ TextureCoordinates = new Vector2[4];
+ CreateTextureCoordinates(tileset);
+ }
+
+ public Vector2[] GetTextureCoordinates(TiledMapTileFlipFlags flipFlags)
+ {
+ if (!_flipDictionary.TryGetValue(flipFlags, out Vector2[] flippedTextureCoordiantes))
+ {
+ return TextureCoordinates;
+ }
+ else
+ {
+ return flippedTextureCoordiantes;
+ }
+ }
+
+ public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ if (!_flipDictionary.ContainsKey(flipFlags))
+ {
+ if (flipFlags == TiledMapTileFlipFlags.None)
+ {
+ _flipDictionary.Add(flipFlags, TextureCoordinates);
+ }
+ else
+ {
+ _flipDictionary.Add(flipFlags, TransformTextureCoordinates(tileset, flipFlags));
+ }
+ }
+ }
+
+ public Vector2[] TransformTextureCoordinates(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier);
+ var texture = tileset.Texture;
+ var texelLeft = (float)sourceRectangle.X / texture.Width;
+ var texelTop = (float)sourceRectangle.Y / texture.Height;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+
+ var flipDiagonally = (flipFlags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flipFlags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flipFlags & TiledMapTileFlipFlags.FlipVertically) != 0;
+ var transform = new Vector2[4];
+
+ transform[0].X = texelLeft;
+ transform[0].Y = texelTop;
+
+ transform[1].X = texelRight;
+ transform[1].Y = texelTop;
+
+ transform[2].X = texelLeft;
+ transform[2].Y = texelBottom;
+
+ transform[3].X = texelRight;
+ transform[3].Y = texelBottom;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[1].X, ref transform[2].X);
+ FloatHelper.Swap(ref transform[1].Y, ref transform[2].Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[0].Y, ref transform[1].Y);
+ FloatHelper.Swap(ref transform[2].Y, ref transform[3].Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref transform[0].X, ref transform[1].X);
+ FloatHelper.Swap(ref transform[2].X, ref transform[3].X);
+ }
+ }
+
+ if (flipVertically)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[0].X, ref transform[2].X);
+ FloatHelper.Swap(ref transform[1].X, ref transform[3].X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref transform[0].Y, ref transform[2].Y);
+ FloatHelper.Swap(ref transform[1].Y, ref transform[3].Y);
+ }
+ }
+
+ transform[0] = transform[0];
+ transform[1] = transform[1];
+ transform[2] = transform[2];
+ transform[3] = transform[3];
+
+ return transform;
+ }
+
+ private void CreateTextureCoordinates(TiledMapTileset tileset)
+ {
+ var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier);
+ var texture = tileset.Texture;
+ var texelLeft = (float)sourceRectangle.X / texture.Width;
+ var texelTop = (float)sourceRectangle.Y / texture.Height;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+
+ TextureCoordinates[0].X = texelLeft;
+ TextureCoordinates[0].Y = texelTop;
+
+ TextureCoordinates[1].X = texelRight;
+ TextureCoordinates[1].Y = texelTop;
+
+ TextureCoordinates[2].X = texelLeft;
+ TextureCoordinates[2].Y = texelBottom;
+
+ TextureCoordinates[3].X = texelRight;
+ TextureCoordinates[3].Y = texelBottom;
+ }
+
+ public override string ToString()
+ {
+ return $"{LocalTileIdentifier}:{Duration}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs
new file mode 100644
index 0000000..2e98e5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening;
+
+public class ColorTween: Tween<Color>
+{
+ internal ColorTween(object target, float duration, float delay, TweenMember<Color> member, Color endValue) : base(target, duration, delay, member, endValue)
+ {
+ }
+
+ protected override void Interpolate(float n)
+ {
+ Member.Value = Color.Lerp(_startValue, _endValue, n);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs
new file mode 100644
index 0000000..f93a5ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs
@@ -0,0 +1,120 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public static class EasingFunctions
+ {
+ public static float Linear(float value) => value;
+
+ public static float CubicIn(float value) => Power.In(value, 3);
+ public static float CubicOut(float value) => Power.Out(value, 3);
+ public static float CubicInOut(float value) => Power.InOut(value, 3);
+
+ public static float QuadraticIn(float value) => Power.In(value, 2);
+ public static float QuadraticOut(float value) => Power.Out(value, 2);
+ public static float QuadraticInOut(float value) => Power.InOut(value, 2);
+
+ public static float QuarticIn(float value) => Power.In(value, 4);
+ public static float QuarticOut(float value) => Power.Out(value, 4);
+ public static float QuarticInOut(float value) => Power.InOut(value, 4);
+
+ public static float QuinticIn(float value) => Power.In(value, 5);
+ public static float QuinticOut(float value) => Power.Out(value, 5);
+ public static float QuinticInOut(float value) => Power.InOut(value, 5);
+
+ public static float SineIn(float value) => (float) Math.Sin(value*MathHelper.PiOver2 - MathHelper.PiOver2) + 1;
+ public static float SineOut(float value) => (float) Math.Sin(value*MathHelper.PiOver2);
+ public static float SineInOut(float value) => (float) (Math.Sin(value*MathHelper.Pi - MathHelper.PiOver2) + 1)/2;
+
+ public static float ExponentialIn(float value) => (float) Math.Pow(2, 10*(value - 1));
+ public static float ExponentialOut(float value) => Out(value, ExponentialIn);
+ public static float ExponentialInOut(float value) => InOut(value, ExponentialIn);
+
+ public static float CircleIn(float value) => (float) -(Math.Sqrt(1 - value * value) - 1);
+ public static float CircleOut(float value) => (float) Math.Sqrt(1 - (value - 1) * (value - 1));
+ public static float CircleInOut(float value) => (float) (value <= .5 ? (Math.Sqrt(1 - value * value * 4) - 1) / -2 : (Math.Sqrt(1 - (value * 2 - 2) * (value * 2 - 2)) + 1) / 2);
+
+ public static float ElasticIn(float value)
+ {
+ const int oscillations = 1;
+ const float springiness = 3f;
+ var e = (Math.Exp(springiness*value) - 1)/(Math.Exp(springiness) - 1);
+ return (float) (e*Math.Sin((MathHelper.PiOver2 + MathHelper.TwoPi*oscillations)*value));
+ }
+
+ public static float ElasticOut(float value) => Out(value, ElasticIn);
+ public static float ElasticInOut(float value) => InOut(value, ElasticIn);
+
+ public static float BackIn(float value)
+ {
+ const float amplitude = 1f;
+ return (float) (Math.Pow(value, 3) - value*amplitude*Math.Sin(value*MathHelper.Pi));
+ }
+
+ public static float BackOut(float value) => Out(value, BackIn);
+ public static float BackInOut(float value) => InOut(value, BackIn);
+
+ public static float BounceOut(float value) => Out(value, BounceIn);
+ public static float BounceInOut(float value) => InOut(value, BounceIn);
+
+ public static float BounceIn(float value)
+ {
+ const float bounceConst1 = 2.75f;
+ var bounceConst2 = (float) Math.Pow(bounceConst1, 2);
+
+ value = 1 - value; //flip x-axis
+
+ if (value < 1/bounceConst1) // big bounce
+ return 1f - bounceConst2*value*value;
+
+ if (value < 2/bounceConst1)
+ return 1 - (float) (bounceConst2*Math.Pow(value - 1.5f/bounceConst1, 2) + .75);
+
+ if (value < 2.5/bounceConst1)
+ return 1 - (float) (bounceConst2*Math.Pow(value - 2.25f/bounceConst1, 2) + .9375);
+
+ //small bounce
+ return 1f - (float) (bounceConst2*Math.Pow(value - 2.625f/bounceConst1, 2) + .984375);
+ }
+
+
+ private static float Out(float value, Func<float, float> function)
+ {
+ return 1 - function(1 - value);
+ }
+
+ private static float InOut(float value, Func<float, float> function)
+ {
+ if (value < 0.5f)
+ return 0.5f*function(value*2);
+
+ return 1f - 0.5f*function(2 - value*2);
+ }
+
+ private static class Power
+ {
+ public static float In(double value, int power)
+ {
+ return (float) Math.Pow(value, power);
+ }
+
+ public static float Out(double value, int power)
+ {
+ var sign = power%2 == 0 ? -1 : 1;
+ return (float) (sign*(Math.Pow(value - 1, power) + sign));
+ }
+
+ public static float InOut(double s, int power)
+ {
+ s *= 2;
+
+ if (s < 1)
+ return In(s, power)/2;
+
+ var sign = power%2 == 0 ? -1 : 1;
+ return (float) (sign/2.0*(Math.Pow(s - 2, power) + sign*2));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs
new file mode 100644
index 0000000..12a03a8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Linq.Expressions;
+
+namespace MonoGame.Extended.Tweening;
+
+public class LinearOperations<T>
+{
+ static LinearOperations()
+ {
+ var a = Expression.Parameter(typeof(T));
+ var b = Expression.Parameter(typeof(T));
+ var c = Expression.Parameter(typeof(float));
+ Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(a, b), a, b).Compile();
+ Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(a, b), a, b).Compile();
+ Multiply = Expression.Lambda<Func<T, float, T>>(Expression.Multiply(a, c), a, c).Compile();
+ }
+
+ public static Func<T, T, T> Add { get; }
+ public static Func<T, T, T> Subtract { get; }
+ public static Func<T, float, T> Multiply { get; }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs
new file mode 100644
index 0000000..8469a13
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs
@@ -0,0 +1,23 @@
+namespace MonoGame.Extended.Tweening;
+
+public class LinearTween<T>: Tween<T>
+ where T: struct
+{
+ private T _range;
+
+ internal LinearTween(object target, float duration, float delay, TweenMember<T> member, T endValue) : base(target, duration, delay, member, endValue)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+ _range = LinearOperations<T>.Subtract(_endValue, _startValue);
+ }
+
+ protected override void Interpolate(float n)
+ {
+ var value = LinearOperations<T>.Add(_startValue, LinearOperations<T>.Multiply(_range, n));
+ Member.Value = value;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj
new file mode 100644
index 0000000..8395e42
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A tweening system to make MonoGame more awesome.</Description>
+ <PackageTags>monogame animations tweening</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs
new file mode 100644
index 0000000..ad29251
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs
@@ -0,0 +1,184 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public abstract class Tween<T> : Tween
+ where T : struct
+ {
+ internal Tween(object target, float duration, float delay, TweenMember<T> member, T endValue)
+ : base(target, duration, delay)
+ {
+ Member = member;
+ _endValue = endValue;
+ }
+
+ public TweenMember<T> Member { get; }
+ public override string MemberName => Member.Name;
+
+ protected T _startValue;
+ protected T _endValue;
+
+ protected override void Initialize()
+ {
+ _startValue = Member.Value;
+ }
+
+ protected override void Swap()
+ {
+ _endValue = _startValue;
+ Initialize();
+ }
+ }
+
+ public abstract class Tween
+ {
+ internal Tween(object target, float duration, float delay)
+ {
+ Target = target;
+ Duration = duration;
+ Delay = delay;
+ IsAlive = true;
+
+ _remainingDelay = delay;
+ }
+
+ public object Target { get; }
+ public abstract string MemberName { get; }
+ public float Duration { get; }
+ public float Delay { get; }
+ public bool IsPaused { get; set; }
+ public bool IsRepeating => _remainingRepeats != 0;
+ public bool IsRepeatingForever => _remainingRepeats < 0;
+ public bool IsAutoReverse { get; private set; }
+ public bool IsAlive { get; private set; }
+ public bool IsComplete { get; private set; }
+ public float TimeRemaining => Duration - _elapsedDuration;
+ public float Completion => MathHelper.Clamp(_completion, 0, 1);
+
+ private Func<float, float> _easingFunction;
+ private bool _isInitialized;
+ private float _completion;
+ private float _elapsedDuration;
+ private float _remainingDelay;
+ private float _repeatDelay;
+ private int _remainingRepeats;
+ private Action<Tween> _onBegin;
+ private Action<Tween> _onEnd;
+
+ public Tween Easing(Func<float, float> easingFunction) { _easingFunction = easingFunction; return this; }
+ public Tween OnBegin(Action<Tween> action) { _onBegin = action; return this; }
+ public Tween OnEnd(Action<Tween> action) { _onEnd = action; return this; }
+ public Tween Pause() { IsPaused = true; return this; }
+ public Tween Resume() { IsPaused = false; return this; }
+
+ public Tween Repeat(int count, float repeatDelay = 0f)
+ {
+ _remainingRepeats = count;
+ _repeatDelay = repeatDelay;
+ return this;
+ }
+
+ public Tween RepeatForever(float repeatDelay = 0f)
+ {
+ _remainingRepeats = -1;
+ _repeatDelay = repeatDelay;
+ return this;
+ }
+
+ public Tween AutoReverse()
+ {
+ if (_remainingRepeats == 0)
+ _remainingRepeats = 1;
+
+ IsAutoReverse = true;
+ return this;
+ }
+
+ protected abstract void Initialize();
+ protected abstract void Interpolate(float n);
+ protected abstract void Swap();
+
+ public void Cancel()
+ {
+ _remainingRepeats = 0;
+ IsAlive = false;
+ }
+
+ public void CancelAndComplete()
+ {
+ if (IsAlive)
+ {
+ _completion = 1;
+
+ Interpolate(1);
+ IsComplete = true;
+ _onEnd?.Invoke(this);
+ }
+
+ Cancel();
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ if(IsPaused || !IsAlive)
+ return;
+
+ if (_remainingDelay > 0)
+ {
+ _remainingDelay -= elapsedSeconds;
+
+ if (_remainingDelay > 0)
+ return;
+ }
+
+ if (!_isInitialized)
+ {
+ _isInitialized = true;
+ Initialize();
+ _onBegin?.Invoke(this);
+ }
+
+ if (IsComplete)
+ {
+ IsComplete = false;
+ _elapsedDuration = 0;
+ _onBegin?.Invoke(this);
+
+ if (IsAutoReverse)
+ Swap();
+ }
+
+ _elapsedDuration += elapsedSeconds;
+
+ var n = _completion = _elapsedDuration / Duration;
+
+ if (_easingFunction != null)
+ n = _easingFunction(n);
+
+ if (_elapsedDuration >= Duration)
+ {
+ if (_remainingRepeats != 0)
+ {
+ if(_remainingRepeats > 0)
+ _remainingRepeats--;
+
+ _remainingDelay = _repeatDelay;
+ }
+ else if (_remainingRepeats == 0)
+ {
+ IsAlive = false;
+ }
+
+ n = _completion = 1;
+ IsComplete = true;
+ }
+
+ Interpolate(n);
+
+ if (IsComplete)
+ _onEnd?.Invoke(this);
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs
new file mode 100644
index 0000000..da2f75d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace MonoGame.Extended.Tweening
+{
+ public sealed class TweenFieldMember<T> : TweenMember<T>
+ where T : struct
+ {
+ private readonly FieldInfo _fieldInfo;
+
+ public TweenFieldMember(object target, FieldInfo fieldInfo)
+ : base(target, CompileGetMethod(fieldInfo), CompileSetMethod(fieldInfo))
+ {
+ _fieldInfo = fieldInfo;
+ }
+
+ private static Func<object, object> CompileGetMethod(FieldInfo fieldInfo)
+ {
+ var self = Expression.Parameter(typeof(object));
+ var instance = Expression.Convert(self, fieldInfo.DeclaringType);
+ var field = Expression.Field(instance, fieldInfo);
+ var convert = Expression.TypeAs(field, typeof(object));
+
+ return Expression.Lambda<Func<object, object>>(convert, self).Compile();
+ }
+
+ private static Action<object, object> CompileSetMethod(FieldInfo fieldInfo)
+ {
+ Debug.Assert(fieldInfo.DeclaringType != null);
+
+ var self = Expression.Parameter(typeof(object));
+ var value = Expression.Parameter(typeof(object));
+ var fieldExp = Expression.Field(Expression.Convert(self, fieldInfo.DeclaringType), fieldInfo);
+ var assignExp = Expression.Assign(fieldExp, Expression.Convert(value, fieldInfo.FieldType));
+
+ return Expression.Lambda<Action<object, object>>(assignExp, self, value).Compile();
+ }
+
+ public override Type Type => _fieldInfo.FieldType;
+ public override string Name => _fieldInfo.Name;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs
new file mode 100644
index 0000000..adcee59
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq.Expressions;
+
+namespace MonoGame.Extended.Tweening
+{
+ public abstract class TweenMember
+ {
+ protected TweenMember(object target)
+ {
+ Target = target;
+ }
+
+ public object Target { get; }
+ public abstract Type Type { get; }
+ public abstract string Name { get; }
+ }
+
+ public abstract class TweenMember<T> : TweenMember
+ where T : struct
+ {
+ protected TweenMember(object target, Func<object, object> getMethod, Action<object, object> setMethod)
+ : base(target)
+ {
+ _getMethod = getMethod;
+ _setMethod = setMethod;
+ }
+
+ private readonly Func<object, object> _getMethod;
+ private readonly Action<object, object> _setMethod;
+
+ public T Value
+ {
+ get { return (T) _getMethod(Target); }
+ set { _setMethod(Target, value); }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs
new file mode 100644
index 0000000..7b1db71
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace MonoGame.Extended.Tweening
+{
+ public sealed class TweenPropertyMember<T> : TweenMember<T>
+ where T : struct
+ {
+ private readonly PropertyInfo _propertyInfo;
+
+ public TweenPropertyMember(object target, PropertyInfo propertyInfo)
+ : base(target, CompileGetMethod(propertyInfo), CompileSetMethod(propertyInfo))
+ {
+ _propertyInfo = propertyInfo;
+ }
+
+ public override Type Type => _propertyInfo.PropertyType;
+ public override string Name => _propertyInfo.Name;
+
+ private static Func<object, object> CompileGetMethod(PropertyInfo propertyInfo)
+ {
+ var param = Expression.Parameter(typeof(object));
+ var instance = Expression.Convert(param, propertyInfo.DeclaringType);
+ var convert = Expression.TypeAs(Expression.Property(instance, propertyInfo), typeof(object));
+ return Expression.Lambda<Func<object, object>>(convert, param).Compile();
+ }
+
+ private static Action<object, object> CompileSetMethod(PropertyInfo propertyInfo)
+ {
+ Debug.Assert(propertyInfo.DeclaringType != null);
+
+ var param = Expression.Parameter(typeof(object));
+ var argument = Expression.Parameter(typeof(object));
+ var expression = Expression.Convert(param, propertyInfo.DeclaringType);
+ var methodInfo = propertyInfo.SetMethod;
+ var arguments = Expression.Convert(argument, propertyInfo.PropertyType);
+ var setterCall = Expression.Call(expression, methodInfo, arguments);
+ return Expression.Lambda<Action<object, object>>(setterCall, param, argument).Compile();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs
new file mode 100644
index 0000000..61b55f9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public class Tweener : IDisposable
+ {
+ public Tweener()
+ {
+ }
+
+ public void Dispose()
+ {
+ CancelAll();
+ _activeTweens.Clear();
+ _memberCache.Clear();
+ }
+
+ public long AllocationCount { get; private set; }
+
+ private readonly List<Tween> _activeTweens = new List<Tween>();
+
+ public Tween<TMember> TweenTo<TTarget, TMember>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f)
+ where TTarget : class
+ where TMember : struct
+ {
+ switch (toValue)
+ {
+ case Color toValueColor:
+ return (Tween<TMember>)(object)TweenTo<TTarget, Color, ColorTween>(target, expression as Expression<Func<TTarget, Color>>, toValueColor, duration, delay);
+ default:
+ return TweenTo<TTarget, TMember, LinearTween<TMember>>(target, expression, toValue, duration, delay);
+ }
+
+ }
+
+ public Tween<TMember> TweenTo<TTarget, TMember, TTween>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f)
+ where TTarget : class
+ where TMember : struct
+ where TTween : Tween<TMember>
+ {
+ var memberExpression = (MemberExpression)expression.Body;
+ var memberInfo = memberExpression.Member;
+ var member = GetMember<TMember>(target, memberInfo.Name);
+ var activeTween = FindTween(target, member.Name);
+
+ activeTween?.Cancel();
+
+ AllocationCount++;
+ var tween = (TTween)Activator.CreateInstance(typeof(TTween),
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null,
+ new object[]{target, duration, delay, member, toValue}, null);
+ _activeTweens.Add(tween);
+ return tween;
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ for (var i = _activeTweens.Count - 1; i >= 0; i--)
+ {
+ var tween = _activeTweens[i];
+
+ tween.Update(elapsedSeconds);
+
+ if (!tween.IsAlive)
+ _activeTweens.RemoveAt(i);
+ }
+ }
+
+ public Tween FindTween(object target, string memberName)
+ {
+ return _activeTweens.FirstOrDefault(t => t.Target == target && t.MemberName == memberName);
+ }
+
+ public void CancelAll()
+ {
+ foreach (var tween in _activeTweens)
+ tween.Cancel();
+ }
+
+ public void CancelAndCompleteAll()
+ {
+ foreach (var tween in _activeTweens)
+ tween.CancelAndComplete();
+ }
+
+ private struct TweenMemberKey
+ {
+#pragma warning disable 414
+ public object Target;
+ public string MemberName;
+#pragma warning restore 414
+ }
+
+ private readonly Dictionary<TweenMemberKey, TweenMember> _memberCache = new Dictionary<TweenMemberKey, TweenMember>();
+
+ private TweenMember<T> GetMember<T>(object target, string memberName)
+ where T : struct
+ {
+ var key = new TweenMemberKey { Target = target, MemberName = memberName };
+
+ if (_memberCache.TryGetValue(key, out var member))
+ return (TweenMember<T>) member;
+
+ member = CreateMember<T>(target, memberName);
+ _memberCache.Add(key, member);
+ return (TweenMember<T>) member;
+ }
+
+ private TweenMember<T> CreateMember<T>(object target, string memberName)
+ where T : struct
+ {
+ AllocationCount++;
+
+ var type = target.GetType();
+ var property = type.GetTypeInfo().GetProperty(memberName);
+
+ if (property != null)
+ return new TweenPropertyMember<T>(target, property);
+
+ var field = type.GetTypeInfo().GetField(memberName);
+
+ if (field != null)
+ return new TweenFieldMember<T>(target, field);
+
+ throw new InvalidOperationException($"'{memberName}' is not a property or field of the target");
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs
new file mode 100644
index 0000000..28a0e57
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Sprites;
+
+namespace MonoGame.Extended.Animations
+{
+ public class AnimationComponent : GameComponent
+ {
+ public AnimationComponent(Game game)
+ : base(game)
+ {
+ Animations = new List<Animation>();
+ }
+
+ public List<Animation> Animations { get; }
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ for (var i = Animations.Count - 1; i >= 0; i--)
+ {
+ var animation = Animations[i];
+ animation.Update(gameTime);
+ }
+
+ Animations.RemoveAll(a => a.IsDisposed);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs
new file mode 100644
index 0000000..7aaca08
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs
@@ -0,0 +1,354 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFont
+ {
+ private readonly Dictionary<int, BitmapFontRegion> _characterMap = new Dictionary<int, BitmapFontRegion>();
+
+ public BitmapFont(string name, IEnumerable<BitmapFontRegion> regions, int lineHeight)
+ {
+ foreach (var region in regions)
+ _characterMap.Add(region.Character, region);
+
+ Name = name;
+ LineHeight = lineHeight;
+ }
+
+ public string Name { get; }
+ public int LineHeight { get; }
+ public int LetterSpacing { get; set; }
+ public static bool UseKernings { get; set; } = true;
+
+ public BitmapFontRegion GetCharacterRegion(int character)
+ {
+ return _characterMap.TryGetValue(character, out var region) ? region : null;
+ }
+
+ public Size2 MeasureString(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ return Size2.Empty;
+
+ var stringRectangle = GetStringRectangle(text);
+ return new Size2(stringRectangle.Width, stringRectangle.Height);
+ }
+
+ public Size2 MeasureString(StringBuilder text)
+ {
+ if (text == null || text.Length == 0)
+ return Size2.Empty;
+
+ var stringRectangle = GetStringRectangle(text);
+ return new Size2(stringRectangle.Width, stringRectangle.Height);
+ }
+
+ public RectangleF GetStringRectangle(string text)
+ {
+ return GetStringRectangle(text, Point2.Zero);
+ }
+
+ public RectangleF GetStringRectangle(string text, Point2 position)
+ {
+ var glyphs = GetGlyphs(text, position);
+ var rectangle = new RectangleF(position.X, position.Y, 0, LineHeight);
+
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion != null)
+ {
+ var right = glyph.Position.X + glyph.FontRegion.Width;
+
+ if (right > rectangle.Right)
+ rectangle.Width = (int)(right - rectangle.Left);
+ }
+
+ if (glyph.Character == '\n')
+ rectangle.Height += LineHeight;
+ }
+
+ return rectangle;
+ }
+
+ public RectangleF GetStringRectangle(StringBuilder text, Point2? position = null)
+ {
+ var position1 = position ?? new Point2();
+ var glyphs = GetGlyphs(text, position1);
+ var rectangle = new RectangleF(position1.X, position1.Y, 0, LineHeight);
+
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion != null)
+ {
+ var right = glyph.Position.X + glyph.FontRegion.Width;
+
+ if (right > rectangle.Right)
+ rectangle.Width = (int)(right - rectangle.Left);
+ }
+
+ if (glyph.Character == '\n')
+ rectangle.Height += LineHeight;
+ }
+
+ return rectangle;
+ }
+
+ public StringGlyphEnumerable GetGlyphs(string text, Point2? position = null)
+ {
+ return new StringGlyphEnumerable(this, text, position);
+ }
+
+ public StringBuilderGlyphEnumerable GetGlyphs(StringBuilder text, Point2? position)
+ {
+ return new StringBuilderGlyphEnumerable(this, text, position);
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}";
+ }
+
+ public struct StringGlyphEnumerable : IEnumerable<BitmapFontGlyph>
+ {
+ private readonly StringGlyphEnumerator _enumerator;
+
+ public StringGlyphEnumerable(BitmapFont font, string text, Point2? position)
+ {
+ _enumerator = new StringGlyphEnumerator(font, text, position);
+ }
+
+ public StringGlyphEnumerator GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _enumerator;
+ }
+ }
+
+ public struct StringGlyphEnumerator : IEnumerator<BitmapFontGlyph>
+ {
+ private readonly BitmapFont _font;
+ private readonly string _text;
+ private int _index;
+ private readonly Point2 _position;
+ private Vector2 _positionDelta;
+ private BitmapFontGlyph _currentGlyph;
+ private BitmapFontGlyph? _previousGlyph;
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ // casting a struct to object will box it, behaviour we want to avoid...
+ throw new InvalidOperationException();
+ }
+ }
+
+ public BitmapFontGlyph Current => _currentGlyph;
+
+ public StringGlyphEnumerator(BitmapFont font, string text, Point2? position)
+ {
+ _font = font;
+ _text = text;
+ _index = -1;
+ _position = position ?? new Point2();
+ _positionDelta = new Vector2();
+ _currentGlyph = new BitmapFontGlyph();
+ _previousGlyph = null;
+ }
+
+ public bool MoveNext()
+ {
+ if (++_index >= _text.Length)
+ return false;
+
+ var character = GetUnicodeCodePoint(_text, ref _index);
+ _currentGlyph.Character = character;
+ _currentGlyph.FontRegion = _font.GetCharacterRegion(character);
+ _currentGlyph.Position = _position + _positionDelta;
+
+ if (_currentGlyph.FontRegion != null)
+ {
+ _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset;
+ _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset;
+ _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing;
+ }
+
+ if (UseKernings && _previousGlyph?.FontRegion != null)
+ {
+ if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out var amount))
+ {
+ _positionDelta.X += amount;
+ _currentGlyph.Position.X += amount;
+ }
+ }
+
+ _previousGlyph = _currentGlyph;
+
+ if (character != '\n')
+ return true;
+
+ _positionDelta.Y += _font.LineHeight;
+ _positionDelta.X = 0;
+ _previousGlyph = null;
+
+ return true;
+ }
+
+ private static int GetUnicodeCodePoint(string text, ref int index)
+ {
+ return char.IsHighSurrogate(text[index]) && ++index < text.Length
+ ? char.ConvertToUtf32(text[index - 1], text[index])
+ : text[index];
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void Reset()
+ {
+ _positionDelta = new Point2();
+ _index = -1;
+ _previousGlyph = null;
+ }
+ }
+
+ public struct StringBuilderGlyphEnumerable : IEnumerable<BitmapFontGlyph>
+ {
+ private readonly StringBuilderGlyphEnumerator _enumerator;
+
+ public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Point2? position)
+ {
+ _enumerator = new StringBuilderGlyphEnumerator(font, text, position);
+ }
+
+ public StringBuilderGlyphEnumerator GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _enumerator;
+ }
+ }
+
+ public struct StringBuilderGlyphEnumerator : IEnumerator<BitmapFontGlyph>
+ {
+ private readonly BitmapFont _font;
+ private readonly StringBuilder _text;
+ private int _index;
+ private readonly Point2 _position;
+ private Vector2 _positionDelta;
+ private BitmapFontGlyph _currentGlyph;
+ private BitmapFontGlyph? _previousGlyph;
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ // casting a struct to object will box it, behaviour we want to avoid...
+ throw new InvalidOperationException();
+ }
+ }
+
+ public BitmapFontGlyph Current => _currentGlyph;
+
+ public StringBuilderGlyphEnumerator(BitmapFont font, StringBuilder text, Point2? position)
+ {
+ _font = font;
+ _text = text;
+ _index = -1;
+ _position = position ?? new Point2();
+ _positionDelta = new Vector2();
+ _currentGlyph = new BitmapFontGlyph();
+ _previousGlyph = null;
+ }
+
+ public bool MoveNext()
+ {
+ if (++_index >= _text.Length)
+ return false;
+
+ var character = GetUnicodeCodePoint(_text, ref _index);
+ _currentGlyph = new BitmapFontGlyph
+ {
+ Character = character,
+ FontRegion = _font.GetCharacterRegion(character),
+ Position = _position + _positionDelta
+ };
+
+ if (_currentGlyph.FontRegion != null)
+ {
+ _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset;
+ _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset;
+ _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing;
+ }
+
+ if (UseKernings && _previousGlyph.HasValue && _previousGlyph.Value.FontRegion != null)
+ {
+ int amount;
+ if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out amount))
+ {
+ _positionDelta.X += amount;
+ _currentGlyph.Position.X += amount;
+ }
+ }
+
+ _previousGlyph = _currentGlyph;
+
+ if (character != '\n')
+ return true;
+
+ _positionDelta.Y += _font.LineHeight;
+ _positionDelta.X = _position.X;
+ _previousGlyph = null;
+
+ return true;
+ }
+
+ private static int GetUnicodeCodePoint(StringBuilder text, ref int index)
+ {
+ return char.IsHighSurrogate(text[index]) && ++index < text.Length
+ ? char.ConvertToUtf32(text[index - 1], text[index])
+ : text[index];
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void Reset()
+ {
+ _positionDelta = new Point2();
+ _index = -1;
+ _previousGlyph = null;
+ }
+ }
+ }
+
+ public struct BitmapFontGlyph
+ {
+ public int Character;
+ public Vector2 Position;
+ public BitmapFontRegion FontRegion;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs
new file mode 100644
index 0000000..da072c5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public static class BitmapFontExtensions
+ {
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation,
+ /// origin, scale, effects and layer.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="bitmapFont">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param>
+ /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param>
+ /// <param name="scale">Scale factor.</param>
+ /// <param name="effect">Effects to apply.</param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, string text, Vector2 position,
+ Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+ if (effect != SpriteEffects.None)
+ throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}");
+
+ var glyphs = bitmapFont.GetGlyphs(text, position);
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion == null)
+ continue;
+ var characterOrigin = position - glyph.Position + origin;
+ spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle);
+ }
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, StringBuilder text, Vector2 position,
+ Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+ if (effect != SpriteEffects.None)
+ throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}");
+
+ var glyphs = bitmapFont.GetGlyphs(text, position);
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion == null)
+ continue;
+ var characterOrigin = position - glyph.Position + origin;
+ spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle);
+ }
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation,
+ /// origin, scale, effects and layer.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param>
+ /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param>
+ /// <param name="scale">Scale factor.</param>
+ /// <param name="effect">Effects to apply.</param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position,
+ Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle);
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position,
+ Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle);
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, layer,
+ /// and width (in pixels) where to wrap the text at.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="BitmapFont" /> objects are loaded from the Content Manager. See the <see cref="BitmapFont" /> class for
+ /// more information.
+ /// Before any calls to this method you must call <see cref="SpriteBatch.Begin" />. Once all calls
+ /// are complete, call <see cref="SpriteBatch.End" />.
+ /// Use a newline character (\n) to draw more than one line of text.
+ /// </remarks>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: layerDepth, clippingRectangle: clippingRectangle);
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color,
+ /// and width (in pixels) where to wrap the text at. The text is drawn on layer 0f.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: 0, clippingRectangle: clippingRectangle);
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: 0, clippingRectangle: clippingRectangle);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs
new file mode 100644
index 0000000..c6d69fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFontReader : ContentTypeReader<BitmapFont>
+ {
+ protected override BitmapFont Read(ContentReader reader, BitmapFont existingInstance)
+ {
+ var textureAssetCount = reader.ReadInt32();
+ var assets = new List<string>();
+
+ for (var i = 0; i < textureAssetCount; i++)
+ {
+ var assetName = reader.ReadString();
+ assets.Add(assetName);
+ }
+
+ var textures = assets
+ .Select(textureName => reader.ContentManager.Load<Texture2D>(reader.GetRelativeAssetName(textureName)))
+ .ToArray();
+
+ var lineHeight = reader.ReadInt32();
+ var regionCount = reader.ReadInt32();
+ var regions = new BitmapFontRegion[regionCount];
+
+ for (var r = 0; r < regionCount; r++)
+ {
+ var character = reader.ReadInt32();
+ var textureIndex = reader.ReadInt32();
+ var x = reader.ReadInt32();
+ var y = reader.ReadInt32();
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var xOffset = reader.ReadInt32();
+ var yOffset = reader.ReadInt32();
+ var xAdvance = reader.ReadInt32();
+ var textureRegion = new TextureRegion2D(textures[textureIndex], x, y, width, height);
+ regions[r] = new BitmapFontRegion(textureRegion, character, xOffset, yOffset, xAdvance);
+ }
+
+ var characterMap = regions.ToDictionary(r => r.Character);
+ var kerningsCount = reader.ReadInt32();
+
+ for (var k = 0; k < kerningsCount; k++)
+ {
+ var first = reader.ReadInt32();
+ var second = reader.ReadInt32();
+ var amount = reader.ReadInt32();
+
+ // Find region
+ if (!characterMap.TryGetValue(first, out var region))
+ continue;
+
+ region.Kernings[second] = amount;
+ }
+
+ return new BitmapFont(reader.AssetName, regions, lineHeight);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs
new file mode 100644
index 0000000..5254f48
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs
@@ -0,0 +1,33 @@
+using System;
+using MonoGame.Extended.TextureAtlases;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFontRegion
+ {
+ public BitmapFontRegion(TextureRegion2D textureRegion, int character, int xOffset, int yOffset, int xAdvance)
+ {
+ TextureRegion = textureRegion;
+ Character = character;
+ XOffset = xOffset;
+ YOffset = yOffset;
+ XAdvance = xAdvance;
+ Kernings = new Dictionary<int, int>();
+ }
+
+ public int Character { get; }
+ public TextureRegion2D TextureRegion { get; }
+ public int XOffset { get; }
+ public int YOffset { get; }
+ public int XAdvance { get; }
+ public int Width => TextureRegion.Width;
+ public int Height => TextureRegion.Height;
+ public Dictionary<int, int> Kernings { get; }
+
+ public override string ToString()
+ {
+ return $"{Convert.ToChar(Character)} {TextureRegion}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs
new file mode 100644
index 0000000..617cb69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class Camera<T> where T : struct
+ {
+ public abstract T Position { get; set; }
+ public abstract float Rotation { get; set; }
+ public abstract float Zoom { get; set; }
+ public abstract float MinimumZoom { get; set; }
+ public abstract float MaximumZoom { get; set; }
+ public abstract RectangleF BoundingRectangle { get; }
+ public abstract T Origin { get; set; }
+ public abstract T Center { get; }
+
+ public abstract void Move(T direction);
+ public abstract void Rotate(float deltaRadians);
+ public abstract void ZoomIn(float deltaZoom);
+ public abstract void ZoomOut(float deltaZoom);
+ public abstract void LookAt(T position);
+
+ public abstract T WorldToScreen(T worldPosition);
+ public abstract T ScreenToWorld(T screenPosition);
+
+ public abstract Matrix GetViewMatrix();
+ public abstract Matrix GetInverseViewMatrix();
+
+ public abstract BoundingFrustum GetBoundingFrustum();
+ public abstract ContainmentType Contains(Vector2 vector2);
+ public abstract ContainmentType Contains(Rectangle rectangle);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs
new file mode 100644
index 0000000..868b6d7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs
@@ -0,0 +1,217 @@
+// Original code dervied from:
+// https://github.com/thelinuxlich/artemis_CSharp/blob/master/Artemis_XNA_INDEPENDENT/Utils/Bag.cs
+
+// --------------------------------------------------------------------------------------------------------------------
+// <copyright file="Bag.cs" company="GAMADU.COM">
+// Copyright © 2013 GAMADU.COM. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other materials
+// provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY GAMADU.COM 'AS IS' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GAMADU.COM OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// The views and conclusions contained in the software and documentation are those of the
+// authors and should not be interpreted as representing official policies, either expressed
+// or implied, of GAMADU.COM.
+// </copyright>
+// <summary>
+// Class Bag.
+// </summary>
+// --------------------------------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public class Bag<T> : IEnumerable<T>
+ {
+ private T[] _items;
+ private readonly bool _isPrimitive;
+
+ public int Capacity => _items.Length;
+ public bool IsEmpty => Count == 0;
+ public int Count { get; private set; }
+
+ public Bag(int capacity = 16)
+ {
+ _isPrimitive = typeof(T).IsPrimitive;
+ _items = new T[capacity];
+ }
+
+ public T this[int index]
+ {
+ get => index >= _items.Length ? default(T) : _items[index];
+ set
+ {
+ EnsureCapacity(index + 1);
+ if (index >= Count)
+ Count = index + 1;
+ _items[index] = value;
+ }
+ }
+
+ public void Add(T element)
+ {
+ EnsureCapacity(Count + 1);
+ _items[Count] = element;
+ ++Count;
+ }
+
+ public void AddRange(Bag<T> range)
+ {
+ for (int index = 0, j = range.Count; j > index; ++index)
+ Add(range[index]);
+ }
+
+ public void Clear()
+ {
+ if(Count == 0)
+ return;
+
+ // non-primitive types are cleared so the garbage collector can release them
+ if (!_isPrimitive)
+ Array.Clear(_items, 0, Count);
+
+ Count = 0;
+ }
+
+ public bool Contains(T element)
+ {
+ for (var index = Count - 1; index >= 0; --index)
+ {
+ if (element.Equals(_items[index]))
+ return true;
+ }
+
+ return false;
+ }
+
+ public T RemoveAt(int index)
+ {
+ var result = _items[index];
+ --Count;
+ _items[index] = _items[Count];
+ _items[Count] = default(T);
+ return result;
+ }
+
+ public bool Remove(T element)
+ {
+ for (var index = Count - 1; index >= 0; --index)
+ {
+ if (element.Equals(_items[index]))
+ {
+ --Count;
+ _items[index] = _items[Count];
+ _items[Count] = default(T);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool RemoveAll(Bag<T> bag)
+ {
+ var isResult = false;
+
+ for (var index = bag.Count - 1; index >= 0; --index)
+ {
+ if (Remove(bag[index]))
+ isResult = true;
+ }
+
+ return isResult;
+ }
+
+ private void EnsureCapacity(int capacity)
+ {
+ if (capacity < _items.Length)
+ return;
+
+ var newCapacity = Math.Max((int)(_items.Length * 1.5), capacity);
+ var oldElements = _items;
+ _items = new T[newCapacity];
+ Array.Copy(oldElements, 0, _items, 0, oldElements.Length);
+ }
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// <summary>
+ /// Get the <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>.
+ /// </summary>
+ /// <returns></returns>
+ /// <remarks>
+ /// Use this method preferentially over <see cref="IEnumerable.GetEnumerator"/> while enumerating via foreach
+ /// to avoid boxing the enumerator on every iteration, which can be expensive in high-performance environments.
+ /// </remarks>
+ public BagEnumerator GetEnumerator()
+ {
+ return new BagEnumerator(this);
+ }
+
+ /// <summary>
+ /// Enumerates a Bag.
+ /// </summary>
+ public struct BagEnumerator : IEnumerator<T>
+ {
+ private readonly Bag<T> _bag;
+ private volatile int _index;
+
+ /// <summary>
+ /// Creates a new <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>.
+ /// </summary>
+ /// <param name="bag"></param>
+ public BagEnumerator(Bag<T> bag)
+ {
+ _bag = bag;
+ _index = -1;
+ }
+
+ readonly T IEnumerator<T>.Current => _bag[_index];
+
+ readonly object IEnumerator.Current => _bag[_index];
+
+ /// <summary>
+ /// Gets the element in the <see cref="Bag{T}"/> at the current position of the enumerator.
+ /// </summary>
+ public readonly T Current => _bag[_index];
+
+ /// <inheritdoc/>
+ public bool MoveNext()
+ {
+ return ++_index < _bag.Count;
+ }
+
+ /// <inheritdoc/>
+ public readonly void Dispose()
+ {
+ }
+
+ /// <inheritdoc/>
+ public readonly void Reset()
+ {
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs
new file mode 100644
index 0000000..fd59066
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs
@@ -0,0 +1,837 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Collections
+{
+ internal static class Deque
+ {
+ internal static readonly Func<int, int> DefaultResizeFunction = x => x * 2;
+ }
+
+ /// <summary>
+ /// Represents a collection of objects which elements can added to or removed either from the front or back; a
+ /// <a href="https://en.wikipedia.org/wiki/Double-ended_queue">double ended queue</a> (deque).
+ /// </summary>
+ /// <remarks>
+ /// <a href="https://en.wikipedia.org/wiki/Circular_buffer">circular array</a> is used as the internal data
+ /// structure for the <see cref="Deque{T}" />.
+ /// </remarks>
+ /// <typeparam name="T">The type of the elements in the deque.</typeparam>
+ public class Deque<T> : IList<T>
+ {
+ private const int _defaultCapacity = 4;
+ private static readonly T[] _emptyArray = new T[0];
+ private int _frontArrayIndex;
+ private T[] _items;
+ private Func<int, int> _resizeFunction = Deque.DefaultResizeFunction;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the default initial capacity.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can
+ /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal array.
+ /// </para>
+ /// <para>
+ /// If the size of the collection can be estimated, using the <see cref="Deque{T}(int)" /> constructor and
+ /// specifying the initial capacity eliminates the need to perform a number of resizing operations while adding
+ /// elements to the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the
+ /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and
+ /// copies all the
+ /// elements in the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>This constructor is an O(1) operation.</para>
+ /// </remarks>
+ public Deque()
+ {
+ _items = _emptyArray;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that contains elements copied from the specified
+ /// collection and has sufficient capacity to accommodate the number of elements copied.
+ /// </summary>
+ /// <param name="collection">The collection whose elements are copied to the new deque.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="collection" /> is null.</exception>
+ /// <remarks>
+ /// <para>
+ /// The elements are copied onto the <see cref="Deque{T}" /> in the same order they are read by the enumerator of
+ /// <paramref name="collection" />.
+ /// </para>
+ /// <para>This constructor is an O(n) operation, where n is the number of elements in <paramref name="collection" />.</para>
+ /// </remarks>
+ public Deque(IEnumerable<T> collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+
+ var array = collection as T[] ?? collection.ToArray();
+ var count = array.Length;
+
+ if (count == 0)
+ _items = _emptyArray;
+ else
+ {
+ _items = new T[count];
+ array.CopyTo(_items, 0);
+ Count = count;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the specified initial
+ /// capacity.
+ /// </summary>
+ /// <param name="capacity">The number of elements that the new <see cref="Deque{T}" /> can initially store.</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity" /> is less than 0.</exception>
+ /// <remarks>
+ /// <para>
+ /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can
+ /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal array.
+ /// </para>
+ /// <para>
+ /// If the size of the collection can be estimated, specifying the initial capacity eliminates the need to
+ /// perform a number of resizing operations while adding elements to the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the
+ /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and
+ /// copies all the elements in the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>This constructor is an O(n) operation, where n is <paramref name="capacity" />.</para>
+ /// </remarks>
+ public Deque(int capacity)
+ {
+ if (capacity < 0)
+ throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity was less than zero.");
+
+ _items = capacity == 0 ? _emptyArray : new T[capacity];
+ }
+
+ /// <summary>
+ /// Gets or sets the resize function used to calculate and set <see cref="Capacity" /> when a greater capacity is
+ /// required.
+ /// </summary>
+ /// <returns>
+ /// The <see cref="Func{T, TResult}" /> used to calculate and set <see cref="Capacity" /> when a greater capacity
+ /// is required.
+ /// </returns>
+ /// <remarks>
+ /// The default resize function is twice the <see cref="Capacity" />. Setting
+ /// <see cref="ResizeFunction" /> to <c>null</c> will set it back to the default.
+ /// </remarks>
+ public Func<int, int> ResizeFunction
+ {
+ get => _resizeFunction;
+ set => _resizeFunction = value ?? Deque.DefaultResizeFunction;
+ }
+
+ /// <summary>
+ /// Gets or sets the total number of elements the internal data structure can hold without resizing.
+ /// </summary>
+ /// <returns>The number of elements that the <see cref="Deque{T}" /> can contain before resizing is required.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <see cref="Capacity" /> cannot be set to a value less than <see cref="Count" />.
+ /// </exception>
+ /// <remarks>
+ /// Changing <see cref="Capacity" /> reallocates memory and copies all the
+ /// elements in the <see cref="Deque{T}" />.
+ /// </remarks>
+ public int Capacity
+ {
+ get => _items.Length;
+ set
+ {
+ if (value < Count)
+ throw new ArgumentOutOfRangeException(nameof(value), "capacity was less than the current size.");
+
+ if (value == Capacity)
+ return;
+
+ if (value == 0)
+ {
+ _items = _emptyArray;
+ return;
+ }
+
+ var newItems = new T[value];
+ CopyTo(newItems);
+
+ _frontArrayIndex = 0;
+ _items = null;
+ _items = newItems;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the element at the specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns>The element at the specified index.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// Index was out of range. Must be non-negative and less than <see cref="Count" />.
+ /// </exception>
+ /// <remarks>
+ /// <para></para>
+ /// <para>
+ /// Use <c>0</c> for the <paramref name="index" /> to get or set the element at the beginning of the
+ /// <see cref="Deque{T}" />, and use <c><see cref="Count" /> - 1</c> for the <paramref name="index" /> to get the
+ /// element at the end of the <see cref="Deque{T}" />.
+ /// </para>
+ /// </remarks>
+ public T this[int index]
+ {
+ get
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index),
+ "Index was out of range. Must be non-negative and less than the size of the collection.");
+ }
+ return _items[arrayIndex];
+ }
+ set
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index),
+ "Index was out of range. Must be non-negative and less than the size of the collection.");
+ }
+ _items[arrayIndex] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>The number of elements contained in the <see cref="Deque{T}" />.</returns>
+ public int Count { get; private set; }
+
+ bool ICollection<T>.IsReadOnly => false;
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerator{T}" /> that can be used to iterate through the <see cref="Deque{T}" />.</returns>
+ public IEnumerator<T> GetEnumerator()
+ {
+ if (Count == 0)
+ yield break;
+
+ if (Count <= _items.Length - _frontArrayIndex)
+ {
+ for (var i = _frontArrayIndex; i < _frontArrayIndex + Count; i++)
+ yield return _items[i];
+ }
+ else
+ {
+ for (var i = _frontArrayIndex; i < Capacity; i++)
+ yield return _items[i];
+ for (var i = 0; i < (_frontArrayIndex + Count) % Capacity; i++)
+ yield return _items[i];
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ void ICollection<T>.Add(T item)
+ {
+ AddToBack(item);
+ }
+
+ /// <summary>
+ /// Searches for the specified element and returns the zero-based index of the first occurrence within the entire
+ /// <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns>
+ /// The zero-based index of the first occurrence of <paramref name="item" /> within the entire
+ /// <see cref="Deque{T}" />, if found; otherwise, <c>-1</c>.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the
+ /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public int IndexOf(T item)
+ {
+ var comparer = EqualityComparer<T>.Default;
+
+ if (Get(0, out var checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item))
+ return 0;
+
+ var backIndex = Count - 1;
+
+ if (Get(backIndex, out checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item))
+ return backIndex;
+
+ int index;
+
+ if (Count <= _items.Length - _frontArrayIndex)
+ index = Array.IndexOf(_items, item, _frontArrayIndex, Count);
+ else
+ {
+ index = Array.IndexOf(_items, item, _frontArrayIndex, _items.Length - _frontArrayIndex);
+ if (index < 0)
+ index = Array.IndexOf(_items, item, 0, _frontArrayIndex + Count - _items.Length);
+ }
+
+ var circularIndex = (index - _frontArrayIndex + _items.Length) % _items.Length;
+ return circularIndex;
+ }
+
+ void IList<T>.Insert(int index, T item)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific element from the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to remove from the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed; otherwise, false. This method also returns false
+ /// if <paramref name="item" /> is not found in the <see cref="Deque{T}" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the
+ /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public bool Remove(T item)
+ {
+ var index = IndexOf(item);
+ if (index == -1)
+ return false;
+
+ RemoveAt(index);
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the specified index of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to remove.</param>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <para><paramref name="index" /> is less than 0.</para>
+ /// <para>-or-</para>
+ /// <para><paramref name="index" /> is equal to or greater than <see cref="Count" />.</para>
+ /// </exception>
+ public void RemoveAt(int index)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index), index, "Index was less than zero.");
+
+ if (index >= Count)
+ throw new ArgumentOutOfRangeException(nameof(index), index, "Index was equal or greater than TotalCount.");
+
+ if (index == 0)
+ {
+ RemoveFromFront();
+ }
+ else
+ {
+ if (index == Count - 1)
+ {
+ RemoveFromBack();
+ }
+ else
+ {
+ if (index < Count / 2)
+ {
+ var arrayIndex = GetArrayIndex(index);
+ // shift the array from 0 to before the index to remove by 1 to the right
+ // the element to remove is replaced by the copy
+ Array.Copy(_items, 0, _items, 1, arrayIndex);
+ // the first element in the arrya is now either a duplicate or it's default value
+ // to be safe set it to it's default value regardless of circumstance
+ _items[0] = default(T);
+ // if we shifted the front element, adjust the front index
+ if (_frontArrayIndex < arrayIndex)
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ // decrement the count so the back index is calculated correctly
+ Count--;
+ }
+ else
+ {
+ var arrayIndex = GetArrayIndex(index);
+ // shift the array from the center of the array to before the index to remove by 1 to the right
+ // the element to remove is replaced by the copy
+ var arrayCenterIndex = _items.Length / 2;
+ Array.Copy(_items, arrayCenterIndex, _items, arrayCenterIndex + 1, _items.Length - 1 - arrayIndex);
+ // the last element in the array is now either a duplicate or it's default value
+ // to be safe set it to it's default value regardless of circumstance
+ _items[_items.Length - 1] = default(T);
+ // if we shifted the front element, adjust the front index
+ if (_frontArrayIndex < arrayIndex)
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ // decrement the count so the back index is calculated correctly
+ Count--;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes all elements from the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// <see cref="Count" /> is set to <c>0</c>, and references to other objects from elements of the collection are
+ /// also released.
+ /// </para>
+ /// <para>
+ /// <see cref="Capacity" /> remains unchanged. To reset the capacity of the <see cref="Deque{T}" />, call the
+ /// <see cref="TrimExcess" /> method or set the <see cref="Capacity" /> property explictly. Decreasing, or
+ /// increasing, the capacity reallocates memory and copies all the elements in the <see cref="Deque{T}" />.
+ /// Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to the default capacity.
+ /// </para>
+ /// <para>This method is an O(n) operation, where n is <see cref="Count" />.</para>
+ /// </remarks>
+ public void Clear()
+ {
+ // allow the garbage collector to reclaim the references
+
+ if (Count == 0)
+ return;
+
+ if (Count > _items.Length - _frontArrayIndex)
+ {
+ Array.Clear(_items, _frontArrayIndex, _items.Length - _frontArrayIndex);
+ Array.Clear(_items, 0, _frontArrayIndex + Count - _items.Length);
+ }
+ else
+ Array.Clear(_items, _frontArrayIndex, Count);
+ Count = 0;
+ _frontArrayIndex = 0;
+ }
+
+ /// <summary>
+ /// Determines whether an element is in the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns><c>true</c> if <paramref name="item" /> is found in the <see cref="Deque{T}" />; otherwise, false.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method determines equality by using the default equality comparer, as defined by the object's
+ /// implementation
+ /// of the <see cref="IEquatable{T}.Equals(T)" /> method for the type of values in the list.
+ /// </para>
+ /// <para>
+ /// This method performs a linear search; therefore, this method is an O(n) operation, where n is
+ /// <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public bool Contains(T item)
+ {
+ return this.Contains(item, EqualityComparer<T>.Default);
+ }
+
+ /// <summary>
+ /// Copies the entire <see cref="Deque{T}" /> to a compatible one-dimensional array, starting at the specified index of
+ /// the target array.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional <see cref="Array" /> that is the destination of the elements copied from
+ /// <see cref="Deque{T}" />. The <see cref="Array" /> must have zero-based indexing.
+ /// </param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="array" /> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex" /> is less than 0.</exception>
+ /// <exception cref="ArgumentException">
+ /// The number of elements in the source <see cref="Deque{T}" /> is greater than the
+ /// available space from <paramref name="arrayIndex" /> to the end of the destination <paramref name="array" />.
+ /// </exception>
+ /// <remarks>
+ /// This method uses <see cref="Array.Copy(Array, int, Array, int, int)" /> to copy the elements. The elements are
+ /// copied to the <see cref="Array" /> in the same order in which the enumerator iterates
+ /// through the <see cref="Deque{T}" />. This method is an O(n) operation, where n is <see cref="Count" />.
+ /// </remarks>
+ public void CopyTo(T[] array, int arrayIndex = 0)
+ {
+ if (array == null)
+ throw new ArgumentNullException(nameof(array));
+
+ if (array.Rank != 1)
+ throw new ArgumentException("Only single dimensional arrays are supported for the requested action.");
+
+ if (arrayIndex < 0)
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Index was less than the array's lower bound.");
+
+ if (arrayIndex >= array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex),
+ "Index was greater than the array's upper bound.");
+ }
+
+ if (array.Length - arrayIndex < Count)
+ throw new ArgumentException("Destination array was not long enough.");
+
+ if (Count == 0)
+ return;
+
+
+ try
+ {
+ var loopsAround = Count > _items.Length - _frontArrayIndex;
+ if (!loopsAround)
+ Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Count);
+ else
+ {
+ Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Capacity - _frontArrayIndex);
+ Array.Copy(_items, 0, array, arrayIndex + Capacity - _frontArrayIndex,
+ _frontArrayIndex + (Count - Capacity));
+ }
+ }
+ catch (ArrayTypeMismatchException)
+ {
+ throw new ArgumentException(
+ "Target array type is not compatible with the type of items in the collection.");
+ }
+ }
+
+ /// <summary>
+ /// Sets the capacity to the actual number of elements in the <see cref="Deque{T}" />, if that number is less than a
+ /// threshold value.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This method can be used to minimize the <see cref="Deque{T}" />'s memory overhead if no new elements will be
+ /// added. The cost of reallocating and copying the elements of a <see cref="Deque{T}" /> can be considerable.
+ /// However, the <see cref="TrimExcess" /> method does nothing if the <see cref="Count" /> is more than 90% of
+ /// <see cref="Capacity" />. This avoids incurring a large reallocation cost for a relatively small gain.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is more than 90% of <see cref="Capacity" />, this method is an O(1) operation; O(n)
+ /// otherwise, where n is <see cref="Count" />.
+ /// </para>
+ /// <para>
+ /// To reset a <see cref="Deque{T}" /> to its initial state, call the <see cref="Clear" /> method before calling
+ /// the <see cref="TrimExcess" /> method. Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to
+ /// the default capacity.
+ /// </para>
+ /// <para>The capacity can also be set using the <see cref="Capacity" /> property.</para>
+ /// </remarks>
+ public void TrimExcess()
+ {
+ if (Count > (int)(_items.Length * 0.9))
+ return;
+ Capacity = Count;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetArrayIndex(int index)
+ {
+ if ((index < 0) || (index >= Count))
+ return -1;
+ return _items.Length != 0 ? (_frontArrayIndex + index) % _items.Length : 0;
+ }
+
+ private void EnsureCapacity(int minimumCapacity)
+ {
+ if (_items.Length >= minimumCapacity)
+ return;
+ var newCapacity = _defaultCapacity;
+ if (_items.Length > 0)
+ newCapacity = _resizeFunction(_items.Length);
+ newCapacity = Math.Max(newCapacity, minimumCapacity);
+ Capacity = newCapacity;
+ }
+
+ /// <summary>
+ /// Adds an element to the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param>
+ /// <remarks>
+ /// <para>
+ /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the
+ /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n)
+ /// operation, where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public void AddToFront(T item)
+ {
+ EnsureCapacity(Count + 1);
+ _frontArrayIndex = (_frontArrayIndex - 1 + _items.Length) % _items.Length;
+ _items[_frontArrayIndex] = item;
+ Count++;
+ }
+
+ /// <summary>
+ /// Adds an element to the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param>
+ /// <remarks>
+ /// <para>
+ /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the
+ /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n)
+ /// operation, where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public void AddToBack(T item)
+ {
+ EnsureCapacity(Count + 1);
+ var index = (_frontArrayIndex + Count++) % _items.Length;
+ _items[index] = item;
+ }
+
+ /// <summary>
+ /// Returns the element at the specified index of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get.</param>
+ /// <param name="item">
+ /// When this method returns, contains the element at the specified index of the
+ /// <see cref="Deque{T}" />, if <paramref name="index" /> was non-negative and less than <see cref="Count" />;
+ /// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully retrieved at <paramref name="index" /> from the of the
+ /// <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if <paramref name="index" /> was non-negative and less than <see cref="Count" />.
+ /// </returns>
+ public bool Get(int index, out T item)
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ item = default(T);
+ return false;
+ }
+ item = _items[arrayIndex];
+ return true;
+ }
+
+ /// <summary>
+ /// Returns the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element at the beginning of the <see cref="Deque{T}" />, if
+ /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This
+ /// parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ public bool GetFront(out T item)
+ {
+ return Get(0, out item);
+ }
+
+ /// <summary>
+ /// Returns the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element at the end of the <see cref="Deque{T}" />, if
+ /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This
+ /// parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ public bool GetBack(out T item)
+ {
+ return Get(Count - 1, out item);
+ }
+
+ /// <summary>
+ /// Removes the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element removed from the beginning of the
+ /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of
+ /// the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront(out T)" />
+ /// is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromFront(out T item)
+ {
+ if (Count == 0)
+ {
+ item = default(T);
+ return false;
+ }
+
+ var index = _frontArrayIndex % _items.Length;
+ item = _items[index];
+ _items[index] = default(T);
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the element was successfully removed from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront()" /> is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromFront()
+ {
+ if (Count == 0)
+ return false;
+
+ var index = _frontArrayIndex % _items.Length;
+ _items[index] = default(T);
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element removed from the end of the
+ /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of
+ /// the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack(out T)" />
+ /// is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromBack(out T item)
+ {
+ if (Count == 0)
+ {
+ item = default(T);
+ return false;
+ }
+
+ var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length;
+ item = _items[circularBackIndex];
+ _items[circularBackIndex] = default(T);
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the element was successfully removed from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack()" /> is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromBack()
+ {
+ if (Count == 0)
+ return false;
+
+ var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length;
+ _items[circularBackIndex] = default(T);
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes and returns the last item.
+ /// </summary>
+ /// <returns>The item that was removed</returns>
+ public T Pop()
+ {
+ if (RemoveFromBack(out var item))
+ return item;
+
+ throw new InvalidOperationException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs
new file mode 100644
index 0000000..ee581b7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public static class DictionaryExtensions
+ {
+ public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
+ {
+ return GetValueOrDefault(dictionary, key, default(TValue));
+ }
+
+ public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue)
+ {
+ TValue value;
+ return dictionary.TryGetValue(key, out value) ? value : defaultValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs
new file mode 100644
index 0000000..bb4fe16
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ /// <summary>Interface for collections that can be observed</summary>
+ /// <typeparam name="T">Type of items managed in the collection</typeparam>
+ public interface IObservableCollection<T>
+ {
+ /// <summary>Raised when an item has been added to the collection</summary>
+ event EventHandler<ItemEventArgs<T>> ItemAdded;
+
+ /// <summary>Raised when an item is removed from the collection</summary>
+ event EventHandler<ItemEventArgs<T>> ItemRemoved;
+
+ /// <summary>Raised when the collection is about to be cleared</summary>
+ /// <remarks>
+ /// This could be covered by calling ItemRemoved for each item currently
+ /// contained in the collection, but it is often simpler and more efficient
+ /// to process the clearing of the entire collection as a special operation.
+ /// </remarks>
+ event EventHandler Clearing;
+
+ /// <summary>Raised when the collection has been cleared of its items</summary>
+ event EventHandler Cleared;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs
new file mode 100644
index 0000000..cba24b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs
@@ -0,0 +1,12 @@
+namespace MonoGame.Extended.Collections
+{
+ public delegate void ReturnToPoolDelegate(IPoolable poolable);
+
+ public interface IPoolable
+ {
+ IPoolable NextNode { get; set; }
+ IPoolable PreviousNode { get; set; }
+ void Initialize(ReturnToPoolDelegate returnDelegate);
+ void Return();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs
new file mode 100644
index 0000000..0da853b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ /// <summary>
+ /// Arguments class for collections wanting to hand over an item in an event
+ /// </summary>
+ public class ItemEventArgs<T> : EventArgs
+ {
+ /// <summary>Initializes a new event arguments supplier</summary>
+ /// <param name="item">Item to be supplied to the event handler</param>
+ public ItemEventArgs(T item)
+ {
+ Item = item;
+ }
+
+ /// <summary>Obtains the collection item the event arguments are carrying</summary>
+ public T Item { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs
new file mode 100644
index 0000000..e4ebeff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public class KeyedCollection<TKey, TValue> : ICollection<TValue>
+ {
+ private readonly Func<TValue, TKey> _getKey;
+ private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();
+
+ public KeyedCollection(Func<TValue, TKey> getKey)
+ {
+ _getKey = getKey;
+ }
+
+ public TValue this[TKey key] => _dictionary[key];
+ public ICollection<TKey> Keys => _dictionary.Keys;
+ public ICollection<TValue> Values => _dictionary.Values;
+ public int Count => _dictionary.Count;
+ public bool IsReadOnly => false;
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerator<TValue> GetEnumerator()
+ {
+ return _dictionary.Values.GetEnumerator();
+ }
+
+ public void Add(TValue item)
+ {
+ _dictionary.Add(_getKey(item), item);
+ }
+
+ public void Clear()
+ {
+ _dictionary.Clear();
+ }
+
+ public bool Contains(TValue item)
+ {
+ return _dictionary.ContainsKey(_getKey(item));
+ }
+
+ public void CopyTo(TValue[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ public bool Remove(TValue item)
+ {
+ return _dictionary.Remove(_getKey(item));
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return _dictionary.ContainsKey(key);
+ }
+
+ public bool TryGetValue(TKey key, out TValue value)
+ {
+ return _dictionary.TryGetValue(key, out value);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs
new file mode 100644
index 0000000..9452a8e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public static class ListExtensions
+ {
+ public static IList<T> Shuffle<T>(this IList<T> list, Random random)
+ {
+ var n = list.Count;
+ while (n > 1)
+ {
+ n--;
+ var k = random.Next(n + 1);
+ var value = list[k];
+ list[k] = list[n];
+ list[n] = value;
+ }
+ return list;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs
new file mode 100644
index 0000000..066fa96
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace MonoGame.Extended.Collections
+{
+ public enum ObjectPoolIsFullPolicy
+ {
+ ReturnNull,
+ IncreaseSize,
+ KillExisting,
+ }
+
+ public class ObjectPool<T> : IEnumerable<T>
+ where T : class, IPoolable
+ {
+ private readonly ReturnToPoolDelegate _returnToPoolDelegate;
+
+ private readonly Deque<T> _freeItems; // circular buffer for O(1) operations
+ private T _headNode; // linked list for iteration
+ private T _tailNode;
+
+ private readonly Func<T> _instantiationFunction;
+
+ public ObjectPoolIsFullPolicy IsFullPolicy { get; }
+ public int Capacity { get; private set; }
+ public int TotalCount { get; private set; }
+ public int AvailableCount => _freeItems.Count;
+ public int InUseCount => TotalCount - AvailableCount;
+
+ public event Action<T> ItemUsed;
+ public event Action<T> ItemReturned;
+
+ public ObjectPool(Func<T> instantiationFunc, int capacity = 16, ObjectPoolIsFullPolicy isFullPolicy = ObjectPoolIsFullPolicy.ReturnNull)
+ {
+ if (instantiationFunc == null)
+ throw new ArgumentNullException(nameof(instantiationFunc));
+
+ _returnToPoolDelegate = Return;
+
+ _instantiationFunction = instantiationFunc;
+ _freeItems = new Deque<T>(capacity);
+ IsFullPolicy = isFullPolicy;
+
+ Capacity = capacity;
+ }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ var node = _headNode;
+ while (node != null)
+ {
+ yield return node;
+ node = (T)node.NextNode;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public T New()
+ {
+ if (!_freeItems.RemoveFromFront(out var poolable))
+ {
+ if (TotalCount <= Capacity)
+ {
+ poolable = CreateObject();
+ }
+ else
+ {
+ switch (IsFullPolicy)
+ {
+ case ObjectPoolIsFullPolicy.ReturnNull:
+ return null;
+ case ObjectPoolIsFullPolicy.IncreaseSize:
+ Capacity++;
+ poolable = CreateObject();
+ break;
+ case ObjectPoolIsFullPolicy.KillExisting:
+ if (_headNode == null)
+ return null;
+ var newHeadNode = (T)_headNode.NextNode;
+ _headNode.Return();
+ _freeItems.RemoveFromBack(out poolable);
+ _headNode = newHeadNode;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ Use(poolable);
+ return poolable;
+ }
+
+ private T CreateObject()
+ {
+ TotalCount++;
+ var item = _instantiationFunction();
+ if (item == null)
+ throw new NullReferenceException($"The created pooled object of type '{typeof(T).Name}' is null.");
+ item.PreviousNode = _tailNode;
+ item.NextNode = null;
+ if (_headNode == null)
+ _headNode = item;
+ if (_tailNode != null)
+ _tailNode.NextNode = item;
+ _tailNode = item;
+ return item;
+ }
+
+ private void Return(IPoolable item)
+ {
+ Debug.Assert(item != null);
+
+ var poolable1 = (T) item;
+
+ var previousNode = (T)item.PreviousNode;
+ var nextNode = (T)item.NextNode;
+
+ if (previousNode != null)
+ previousNode.NextNode = nextNode;
+ if (nextNode != null)
+ nextNode.PreviousNode = previousNode;
+
+ if (item == _headNode)
+ _headNode = nextNode;
+ if (item == _tailNode)
+ _tailNode = previousNode;
+
+ if (_tailNode != null)
+ _tailNode.NextNode = null;
+
+ _freeItems.AddToBack(poolable1);
+
+ ItemReturned?.Invoke((T)item);
+ }
+
+ private void Use(T item)
+ {
+ item.Initialize(_returnToPoolDelegate);
+ item.NextNode = null;
+ if (item != _tailNode)
+ {
+ item.PreviousNode = _tailNode;
+ _tailNode.NextNode = item;
+ _tailNode = item;
+ }
+
+ ItemUsed?.Invoke(item);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs
new file mode 100644
index 0000000..de827df
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace MonoGame.Extended.Collections
+{
+ public class ObservableCollection<T> : Collection<T>, IObservableCollection<T>
+ {
+ /// <summary>
+ /// Initializes a new instance of the ObservableCollection class that is empty.
+ /// </summary>
+ public ObservableCollection()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the ObservableCollection class as a wrapper
+ /// for the specified list.
+ /// </summary>
+ /// <param name="list">The list that is wrapped by the new collection.</param>
+ /// <exception cref="System.ArgumentNullException">
+ /// List is null.
+ /// </exception>
+ public ObservableCollection(IList<T> list) : base(list)
+ {
+ }
+
+ /// <summary>Raised when an item has been added to the collection</summary>
+ public event EventHandler<ItemEventArgs<T>> ItemAdded;
+
+ /// <summary>Raised when an item is removed from the collection</summary>
+ public event EventHandler<ItemEventArgs<T>> ItemRemoved;
+
+ /// <summary>Raised when the collection is about to be cleared</summary>
+ /// <remarks>
+ /// This could be covered by calling ItemRemoved for each item currently
+ /// contained in the collection, but it is often simpler and more efficient
+ /// to process the clearing of the entire collection as a special operation.
+ /// </remarks>
+ public event EventHandler Clearing;
+
+ /// <summary>Raised when the collection has been cleared</summary>
+ public event EventHandler Cleared;
+
+ /// <summary>Removes all elements from the Collection</summary>
+ protected override void ClearItems()
+ {
+ OnClearing();
+ base.ClearItems();
+ OnCleared();
+ }
+
+ /// <summary>
+ /// Inserts an element into the ObservableCollection at the specified index
+ /// </summary>
+ /// <param name="index">
+ /// The object to insert. The value can be null for reference types.
+ /// </param>
+ /// <param name="item">The zero-based index at which item should be inserted</param>
+ protected override void InsertItem(int index, T item)
+ {
+ base.InsertItem(index, item);
+ OnAdded(item);
+ }
+
+ /// <summary>
+ /// Removes the element at the specified index of the ObservableCollection
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to remove</param>
+ protected override void RemoveItem(int index)
+ {
+ var item = base[index];
+ base.RemoveItem(index);
+ OnRemoved(item);
+ }
+
+ /// <summary>Replaces the element at the specified index</summary>
+ /// <param name="index">
+ /// The new value for the element at the specified index. The value can be null
+ /// for reference types
+ /// </param>
+ /// <param name="item">The zero-based index of the element to replace</param>
+ protected override void SetItem(int index, T item)
+ {
+ var oldItem = base[index];
+ base.SetItem(index, item);
+ OnRemoved(oldItem);
+ OnAdded(item);
+ }
+
+ /// <summary>Fires the 'ItemAdded' event</summary>
+ /// <param name="item">Item that has been added to the collection</param>
+ protected virtual void OnAdded(T item)
+ {
+ ItemAdded?.Invoke(this, new ItemEventArgs<T>(item));
+ }
+
+ /// <summary>Fires the 'ItemRemoved' event</summary>
+ /// <param name="item">Item that has been removed from the collection</param>
+ protected virtual void OnRemoved(T item)
+ {
+ ItemRemoved?.Invoke(this, new ItemEventArgs<T>(item));
+ }
+
+ /// <summary>Fires the 'Clearing' event</summary>
+ protected virtual void OnClearing()
+ {
+ Clearing?.Invoke(this, EventArgs.Empty);
+ }
+
+ /// <summary>Fires the 'Cleared' event</summary>
+ protected virtual void OnCleared()
+ {
+ Cleared?.Invoke(this, EventArgs.Empty);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs
new file mode 100644
index 0000000..7c1546f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ public class Pool<T>
+ where T : class
+ {
+ private readonly Func<T> _createItem;
+ private readonly Action<T> _resetItem;
+ private readonly Deque<T> _freeItems;
+ private readonly int _maximum;
+
+ public Pool(Func<T> createItem, Action<T> resetItem, int capacity = 16, int maximum = int.MaxValue)
+ {
+ _createItem = createItem;
+ _resetItem = resetItem;
+ _maximum = maximum;
+ _freeItems = new Deque<T>(capacity);
+ }
+
+ public Pool(Func<T> createItem, int capacity = 16, int maximum = int.MaxValue)
+ : this(createItem, _ => { }, capacity, maximum)
+ {
+ }
+
+ public int AvailableCount => _freeItems.Count;
+
+ public T Obtain()
+ {
+ if (_freeItems.Count > 0)
+ return _freeItems.Pop();
+
+ return _createItem();
+ }
+
+ public void Free(T item)
+ {
+ if (item == null) throw new ArgumentNullException(nameof(item));
+
+ if (_freeItems.Count < _maximum)
+ _freeItems.AddToBack(item);
+
+ _resetItem(item);
+ }
+
+ public void Clear()
+ {
+ _freeItems.Clear();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs
new file mode 100644
index 0000000..452ad37
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Provides additional methods for working with color
+ /// </summary>
+ public static class ColorExtensions
+ {
+ public static Color FromHex(string value)
+ {
+ var r = int.Parse(value.Substring(1, 2), NumberStyles.HexNumber);
+ var g = int.Parse(value.Substring(3, 2), NumberStyles.HexNumber);
+ var b = int.Parse(value.Substring(5, 2), NumberStyles.HexNumber);
+ var a = value.Length > 7 ? int.Parse(value.Substring(7, 2), NumberStyles.HexNumber) : 255;
+ return new Color(r, g, b, a);
+ }
+
+ public static Color ToRgb(this HslColor c)
+ {
+ var h = c.H;
+ var s = c.S;
+ var l = c.L;
+
+ if (s == 0f)
+ return new Color(l, l, l);
+
+ h = h/360f;
+ var max = l < 0.5f ? l*(1 + s) : l + s - l*s;
+ var min = 2f*l - max;
+
+ return new Color(
+ ComponentFromHue(min, max, h + 1f/3f),
+ ComponentFromHue(min, max, h),
+ ComponentFromHue(min, max, h - 1f/3f));
+ }
+
+ private static float ComponentFromHue(float m1, float m2, float h)
+ {
+ h = (h + 1f)%1f;
+ if (h*6f < 1)
+ return m1 + (m2 - m1)*6f*h;
+ if (h*2 < 1)
+ return m2;
+ if (h*3 < 2)
+ return m1 + (m2 - m1)*(2f/3f - h)*6f;
+ return m1;
+ }
+
+ public static HslColor ToHsl(this Color c)
+ {
+ var r = c.R/255f;
+ var b = c.B/255f;
+ var g = c.G/255f;
+
+ var max = Math.Max(Math.Max(r, g), b);
+ var min = Math.Min(Math.Min(r, g), b);
+ var chroma = max - min;
+ var sum = max + min;
+
+ var l = sum*0.5f;
+
+ if (chroma == 0)
+ return new HslColor(0f, 0f, l);
+
+ float h;
+
+ if (r == max)
+ h = (60*(g - b)/chroma + 360)%360;
+ else
+ {
+ if (g == max)
+ h = 60*(b - r)/chroma + 120f;
+ else
+ h = 60*(r - g)/chroma + 240f;
+ }
+
+ var s = l <= 0.5f ? chroma/sum : chroma/(2f - sum);
+
+ return new HslColor(h, s, l);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs
new file mode 100644
index 0000000..370af3d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class ColorHelper
+ {
+ //http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
+ public static Color FromHsl(float hue, float saturation, float lightness)
+ {
+ var hsl = new Vector4(hue, saturation, lightness, 1);
+ var color = new Vector4(0, 0, 0, hsl.W);
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (hsl.Y == 0.0f)
+ color.X = color.Y = color.Z = hsl.Z;
+ else
+ {
+ var q = hsl.Z < 0.5f ? hsl.Z*(1.0f + hsl.Y) : hsl.Z + hsl.Y - hsl.Z*hsl.Y;
+ var p = 2.0f*hsl.Z - q;
+
+ color.X = HueToRgb(p, q, hsl.X + 1.0f/3.0f);
+ color.Y = HueToRgb(p, q, hsl.X);
+ color.Z = HueToRgb(p, q, hsl.X - 1.0f/3.0f);
+ }
+
+ return new Color(color);
+ }
+
+ private static float HueToRgb(float p, float q, float t)
+ {
+ if (t < 0.0f) t += 1.0f;
+ if (t > 1.0f) t -= 1.0f;
+ if (t < 1.0f/6.0f) return p + (q - p)*6.0f*t;
+ if (t < 1.0f/2.0f) return q;
+ if (t < 2.0f/3.0f) return p + (q - p)*(2.0f/3.0f - t)*6.0f;
+ return p;
+ }
+
+ public static Color FromHex(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return Color.Transparent;
+ var startIndex = 0;
+ if (value.StartsWith("#"))
+ startIndex++;
+ var r = int.Parse(value.Substring(startIndex, 2), NumberStyles.HexNumber);
+ var g = int.Parse(value.Substring(startIndex + 2, 2), NumberStyles.HexNumber);
+ var b = int.Parse(value.Substring(startIndex + 4, 2), NumberStyles.HexNumber);
+ var a = value.Length > 6 + startIndex ? int.Parse(value.Substring(startIndex + 6, 2), NumberStyles.HexNumber) : 255;
+
+ return new Color(r, g, b, a);
+ }
+
+ public static string ToHex(Color color)
+ {
+ var rx = $"{color.R:x2}";
+ var gx = $"{color.G:x2}";
+ var bx = $"{color.B:x2}";
+ var ax = $"{color.A:x2}";
+ return $"#{rx}{gx}{bx}{ax}";
+ }
+
+ private static readonly Dictionary<string, Color> _colorsByName = typeof(Color)
+ .GetRuntimeProperties()
+ .Where(p => p.PropertyType == typeof(Color))
+ .ToDictionary(p => p.Name, p => (Color) p.GetValue(null), StringComparer.OrdinalIgnoreCase);
+
+ public static Color FromName(string name)
+ {
+ Color color;
+
+ if(_colorsByName.TryGetValue(name, out color))
+ return color;
+
+ throw new InvalidOperationException($"{name} is not a valid color");
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs
new file mode 100644
index 0000000..69e7730
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs
@@ -0,0 +1,51 @@
+using System.IO;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Content
+{
+ public interface IContentLoader<out T>
+ {
+ T Load(ContentManager contentManager, string path);
+ }
+
+ public interface IContentLoader
+ {
+ T Load<T>(ContentManager contentManager, string path);
+ }
+
+ public static class ContentManagerExtensions
+ {
+ public const string DirectorySeparatorChar = "/";
+
+ public static Stream OpenStream(this ContentManager contentManager, string path)
+ {
+ return TitleContainer.OpenStream(contentManager.RootDirectory + DirectorySeparatorChar + path);
+ }
+
+ public static GraphicsDevice GetGraphicsDevice(this ContentManager contentManager)
+ {
+ // http://konaju.com/?p=21
+ var serviceProvider = contentManager.ServiceProvider;
+ var graphicsDeviceService = (IGraphicsDeviceService) serviceProvider.GetService(typeof(IGraphicsDeviceService));
+ return graphicsDeviceService.GraphicsDevice;
+ }
+
+ /// <summary>
+ /// Loads the content using a custom content loader.
+ /// </summary>
+ public static T Load<T>(this ContentManager contentManager, string path, IContentLoader contentLoader)
+ {
+ return contentLoader.Load<T>(contentManager, path);
+ }
+
+ /// <summary>
+ /// Loads the content using a custom content loader.
+ /// </summary>
+ public static T Load<T>(this ContentManager contentManager, string path, IContentLoader<T> contentLoader)
+ {
+ return contentLoader.Load(contentManager, path);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs
new file mode 100644
index 0000000..f4c51a4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+using System.Reflection;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Content
+{
+ public static class ContentReaderExtensions
+ {
+ private static readonly FieldInfo _contentReaderGraphicsDeviceFieldInfo = typeof(ContentReader).GetTypeInfo().GetDeclaredField("graphicsDevice");
+
+ public static GraphicsDevice GetGraphicsDevice(this ContentReader contentReader)
+ {
+ return (GraphicsDevice)_contentReaderGraphicsDeviceFieldInfo.GetValue(contentReader);
+ }
+
+ public static string RemoveExtension(string path)
+ {
+ return Path.ChangeExtension(path, null).TrimEnd('.');
+ }
+
+ public static string GetRelativeAssetName(this ContentReader contentReader, string relativeName)
+ {
+ var assetDirectory = Path.GetDirectoryName(contentReader.AssetName);
+ var assetName = RemoveExtension(Path.Combine(assetDirectory, relativeName).Replace('\\', '/'));
+
+ return ShortenRelativePath(assetName);
+ }
+
+ public static string ShortenRelativePath(string relativePath)
+ {
+ var ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal);
+ while (ellipseIndex != -1)
+ {
+ var lastDirectoryIndex = relativePath.LastIndexOf('/', ellipseIndex - 1) + 1;
+ relativePath = relativePath.Remove(lastDirectoryIndex, ellipseIndex + 4 - lastDirectoryIndex);
+ ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal);
+ }
+
+ return relativePath;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs
new file mode 100644
index 0000000..432f1ac
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public class FramesPerSecondCounter : IUpdate
+ {
+ private static readonly TimeSpan _oneSecondTimeSpan = new TimeSpan(0, 0, 1);
+ private int _framesCounter;
+ private TimeSpan _timer = _oneSecondTimeSpan;
+
+ public FramesPerSecondCounter()
+ {
+ }
+
+ public int FramesPerSecond { get; private set; }
+
+ public void Update(GameTime gameTime)
+ {
+ _timer += gameTime.ElapsedGameTime;
+ if (_timer <= _oneSecondTimeSpan)
+ return;
+
+ FramesPerSecond = _framesCounter;
+ _framesCounter = 0;
+ _timer -= _oneSecondTimeSpan;
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ _framesCounter++;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs
new file mode 100644
index 0000000..ef36072
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public class FramesPerSecondCounterComponent : DrawableGameComponent
+ {
+ private readonly FramesPerSecondCounter _fpsCounter;
+
+ public FramesPerSecondCounterComponent(Game game)
+ : base(game)
+ {
+ _fpsCounter = new FramesPerSecondCounter();
+ }
+
+ public int FramesPerSecond => _fpsCounter.FramesPerSecond;
+
+ public override void Update(GameTime gameTime)
+ {
+ _fpsCounter.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _fpsCounter.Draw(gameTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs
new file mode 100644
index 0000000..c470a12
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class GameComponentCollectionExtensions
+ {
+ public static T Add<T>(this GameComponentCollection collection)
+ where T : IGameComponent, new()
+ {
+ var gameComponent = new T();
+ collection.Add(gameComponent);
+ return gameComponent;
+ }
+
+ public static T Add<T>(this GameComponentCollection collection, Func<T> createGameComponent)
+ where T : IGameComponent
+ {
+ var gameComponent = createGameComponent();
+ collection.Add(gameComponent);
+ return gameComponent;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs
new file mode 100644
index 0000000..01c8790
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class GameTimeExtensions
+ {
+ public static float GetElapsedSeconds(this GameTime gameTime)
+ {
+ return (float) gameTime.ElapsedGameTime.TotalSeconds;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs
new file mode 100644
index 0000000..628073d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// An immutable data structure representing a 24bit color composed of separate hue, saturation and lightness channels.
+ /// </summary>
+ //[Serializable]
+ public struct HslColor : IEquatable<HslColor>, IComparable<HslColor>
+ {
+ /// <summary>
+ /// Gets the value of the hue channel in degrees.
+ /// </summary>
+ public readonly float H;
+
+ /// <summary>
+ /// Gets the value of the saturation channel.
+ /// </summary>
+ public readonly float S;
+
+ /// <summary>
+ /// Gets the value of the lightness channel.
+ /// </summary>
+ public readonly float L;
+
+ private static float NormalizeHue(float h)
+ {
+ if (h < 0) return h + 360*((int) (h/360) + 1);
+ return h%360;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HslColor" /> structure.
+ /// </summary>
+ /// <param name="h">The value of the hue channel.</param>
+ /// <param name="s">The value of the saturation channel.</param>
+ /// <param name="l">The value of the lightness channel.</param>
+ public HslColor(float h, float s, float l) : this()
+ {
+ // normalize the hue
+ H = NormalizeHue(h);
+ S = MathHelper.Clamp(s, 0f, 1f);
+ L = MathHelper.Clamp(l, 0f, 1f);
+ }
+
+ /// <summary>
+ /// Copies the individual channels of the color to the specified memory location.
+ /// </summary>
+ /// <param name="destination">The memory location to copy the axis to.</param>
+ public void CopyTo(out HslColor destination)
+ {
+ destination = new HslColor(H, S, L);
+ }
+
+ /// <summary>
+ /// Destructures the color, exposing the individual channels.
+ /// </summary>
+ public void Destructure(out float h, out float s, out float l)
+ {
+ h = H;
+ s = S;
+ l = L;
+ }
+
+ /// <summary>
+ /// Exposes the individual channels of the color to the specified matching function.
+ /// </summary>
+ /// <param name="callback">The function which matches the individual channels of the color.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// Thrown if the value passed to the <paramref name="callback" /> parameter is <c>null</c>.
+ /// </exception>
+ public void Match(Action<float, float, float> callback)
+ {
+ if (callback == null)
+ throw new ArgumentNullException(nameof(callback));
+
+ callback(H, S, L);
+ }
+
+ /// <summary>
+ /// Exposes the individual channels of the color to the specified mapping function and returns the
+ /// result;
+ /// </summary>
+ /// <typeparam name="T">The type being mapped to.</typeparam>
+ /// <param name="map">
+ /// A function which maps the color channels to an instance of <typeparamref name="T" />.
+ /// </param>
+ /// <returns>
+ /// The result of the <paramref name="map" /> function when passed the individual X and Y components.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// Thrown if the value passed to the <paramref name="map" /> parameter is <c>null</c>.
+ /// </exception>
+ public T Map<T>(Func<float, float, float, T> map)
+ {
+ if (map == null)
+ throw new ArgumentNullException(nameof(map));
+
+ return map(H, S, L);
+ }
+
+ public static HslColor operator +(HslColor a, HslColor b)
+ {
+ return new HslColor(a.H + b.H, a.S + b.S, a.L + b.L);
+ }
+
+ public static implicit operator HslColor(string value)
+ {
+ return Parse(value);
+ }
+
+ public int CompareTo(HslColor other)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return H.CompareTo(other.H)*100 + S.CompareTo(other.S)*10 + L.CompareTo(L);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is HslColor)
+ return Equals((HslColor) obj);
+
+ return base.Equals(obj);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="HslColor" /> is equal to this instance.
+ /// </summary>
+ /// <param name="value">The <see cref="HslColor" /> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="HslColor" /> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(HslColor value)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return H.Equals(value.H) && S.Equals(value.S) && L.Equals(value.L);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ return H.GetHashCode() ^
+ S.GetHashCode() ^
+ L.GetHashCode();
+ }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String" /> that represents this instance.
+ /// </returns>
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "H:{0:N1}° S:{1:N1} L:{2:N1}",
+ H, 100*S, 100*L);
+ }
+
+ public static HslColor Parse(string s)
+ {
+ var hsl = s.Split(',');
+ var hue = float.Parse(hsl[0].TrimEnd('°'), CultureInfo.InvariantCulture.NumberFormat);
+ var sat = float.Parse(hsl[1], CultureInfo.InvariantCulture.NumberFormat);
+ var lig = float.Parse(hsl[2], CultureInfo.InvariantCulture.NumberFormat);
+
+ return new HslColor(hue, sat, lig);
+ }
+
+ /// <summary>
+ /// Implements the operator ==.
+ /// </summary>
+ /// <param name="x">The lvalue.</param>
+ /// <param name="y">The rvalue.</param>
+ /// <returns>
+ /// <c>true</c> if the lvalue <see cref="HslColor" /> is equal to the rvalue; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(HslColor x, HslColor y)
+ {
+ return x.Equals(y);
+ }
+
+ /// <summary>
+ /// Implements the operator !=.
+ /// </summary>
+ /// <param name="x">The lvalue.</param>
+ /// <param name="y">The rvalue.</param>
+ /// <returns>
+ /// <c>true</c> if the lvalue <see cref="HslColor" /> is not equal to the rvalue; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(HslColor x, HslColor y)
+ {
+ return !x.Equals(y);
+ }
+
+ public static HslColor operator -(HslColor a, HslColor b)
+ {
+ return new HslColor(a.H - b.H, a.S - b.S, a.L - b.L);
+ }
+
+ public static HslColor Lerp(HslColor c1, HslColor c2, float t)
+ {
+ // loop around if c2.H < c1.H
+ var h2 = c2.H >= c1.H ? c2.H : c2.H + 360;
+ return new HslColor(
+ c1.H + t*(h2 - c1.H),
+ c1.S + t*(c2.S - c1.S),
+ c1.L + t*(c2.L - c1.L));
+ }
+
+ public static HslColor FromRgb(Color color)
+ {
+ // derived from http://www.geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm
+ var r = color.R / 255f;
+ var g = color.G / 255f;
+ var b = color.B / 255f;
+ var h = 0f; // default to black
+ var s = 0f;
+ var l = 0f;
+ var v = Math.Max(r, g);
+ v = Math.Max(v, b);
+
+ var m = Math.Min(r, g);
+ m = Math.Min(m, b);
+ l = (m + v) / 2.0f;
+
+ if (l <= 0.0)
+ return new HslColor(h, s, l);
+
+ var vm = v - m;
+ s = vm;
+
+ if (s > 0.0)
+ s /= l <= 0.5f ? v + m : 2.0f - v - m;
+ else
+ return new HslColor(h, s, l);
+
+ var r2 = (v - r) / vm;
+ var g2 = (v - g) / vm;
+ var b2 = (v - b) / vm;
+
+ if (Math.Abs(r - v) < float.Epsilon)
+ h = Math.Abs(g - m) < float.Epsilon ? 5.0f + b2 : 1.0f - g2;
+ else if (Math.Abs(g - v) < float.Epsilon)
+ h = Math.Abs(b - m) < float.Epsilon ? 1.0f + r2 : 3.0f - b2;
+ else
+ h = Math.Abs(r - m) < float.Epsilon ? 3.0f + g2 : 5.0f - r2;
+
+ h *= 60;
+ h = NormalizeHue(h);
+
+ return new HslColor(h, s, l);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs
new file mode 100644
index 0000000..ad3c35c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IColorable
+ {
+ Color Color { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs
new file mode 100644
index 0000000..678e510
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Defines a generalized method that a value type or class implements to create a type-specific method for
+ /// determining equality of instances by reference.
+ /// </summary>
+ /// <typeparam name="T">The type of values or objects to compare.</typeparam>
+ public interface IEquatableByRef<T>
+ {
+ /// <summary>
+ /// Indicates whether the current value or object is equal to another value or object of the same type by
+ /// reference.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the current value or object is equal to the <paramref name="other" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ /// <param name="other">A value or object to compare with this value or object.</param>
+ bool Equals(ref T other);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs
new file mode 100644
index 0000000..a851088
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IMovable
+ {
+ Vector2 Position { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs
new file mode 100644
index 0000000..a8b1eab
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IRectangular
+ {
+ Rectangle BoundingRectangle { get; }
+ }
+
+ public interface IRectangularF
+ {
+ RectangleF BoundingRectangle { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs
new file mode 100644
index 0000000..7a7d622
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs
@@ -0,0 +1,7 @@
+namespace MonoGame.Extended
+{
+ public interface IRotatable
+ {
+ float Rotation { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs
new file mode 100644
index 0000000..70a527b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IScalable
+ {
+ Vector2 Scale { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs
new file mode 100644
index 0000000..5b55058
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs
@@ -0,0 +1,8 @@
+
+namespace MonoGame.Extended
+{
+ public interface ISizable
+ {
+ Size2 Size { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs
new file mode 100644
index 0000000..6240e04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IUpdate
+ {
+ void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs
new file mode 100644
index 0000000..19f4855
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs
@@ -0,0 +1,224 @@
+/*
+* -----------------------------------------------------------------------------
+* Original code from SlimMath project. http://code.google.com/p/slimmath/
+* Greetings to SlimDX Group. Original code published with the following license:
+* -----------------------------------------------------------------------------
+*
+* Copyright (c) 2007-2010 SlimDX Group
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*/
+
+using System;
+using System.Diagnostics;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public enum AngleType : byte
+ {
+ Radian = 0,
+ Degree,
+ Revolution, //or Turn / cycle
+ Gradian // or Gon
+ }
+
+ [DataContract]
+ [DebuggerDisplay("{ToString(),nq}")]
+ public struct Angle : IComparable<Angle>, IEquatable<Angle>
+ {
+ private const float _tau = (float) (Math.PI*2.0);
+ private const float _tauInv = (float) (0.5/Math.PI);
+ private const float _degreeRadian = (float) (Math.PI/180.0);
+ private const float _radianDegree = (float) (180.0/Math.PI);
+ private const float _gradianRadian = (float) (Math.PI/200.0);
+ private const float _radianGradian = (float) (200.0/Math.PI);
+
+ [DataMember]
+ public float Radians { get; set; }
+
+ public float Degrees
+ {
+ get => Radians*_radianDegree;
+ set => Radians = value*_degreeRadian;
+ }
+
+ public float Gradians
+ {
+ get => Radians*_radianGradian;
+ set => Radians = value*_gradianRadian;
+ }
+
+ public float Revolutions
+ {
+ get => Radians*_tauInv;
+ set => Radians = value*_tau;
+ }
+
+ public Angle(float value, AngleType angleType = AngleType.Radian)
+ {
+ switch (angleType)
+ {
+ default:
+ Radians = 0f;
+ break;
+ case AngleType.Radian:
+ Radians = value;
+ break;
+ case AngleType.Degree:
+ Radians = value*_degreeRadian;
+ break;
+ case AngleType.Revolution:
+ Radians = value*_tau;
+ break;
+ case AngleType.Gradian:
+ Radians = value*_gradianRadian;
+ break;
+ }
+ }
+
+ public float GetValue(AngleType angleType)
+ {
+ switch (angleType)
+ {
+ default:
+ return 0f;
+ case AngleType.Radian:
+ return Radians;
+ case AngleType.Degree:
+ return Degrees;
+ case AngleType.Revolution:
+ return Revolutions;
+ case AngleType.Gradian:
+ return Gradians;
+ }
+ }
+
+ public void Wrap()
+ {
+ var angle = Radians%_tau;
+ if (angle <= Math.PI) angle += _tau;
+ if (angle > Math.PI) angle -= _tau;
+ Radians = angle;
+ }
+
+ public void WrapPositive()
+ {
+ Radians %= _tau;
+ if (Radians < 0d) Radians += _tau;
+ Radians = Radians;
+ }
+
+ public static Angle FromVector(Vector2 vector)
+ {
+ return new Angle((float) Math.Atan2(-vector.Y, vector.X));
+ }
+
+ public Vector2 ToUnitVector() => ToVector(1);
+
+ public Vector2 ToVector(float length)
+ {
+ return new Vector2(length*(float) Math.Cos(Radians), -length*(float) Math.Sin(Radians));
+ }
+
+ public static bool IsBetween(Angle value, Angle min, Angle end)
+ {
+ return end < min
+ ? (value >= min) || (value <= end)
+ : (value >= min) && (value <= end);
+ }
+
+ public int CompareTo(Angle other)
+ {
+ WrapPositive();
+ other.WrapPositive();
+ return Radians.CompareTo(other.Radians);
+ }
+
+ public bool Equals(Angle other)
+ {
+ WrapPositive();
+ other.WrapPositive();
+ return Radians.Equals(other.Radians);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ return obj is Angle a && Equals(a);
+ }
+
+ public override int GetHashCode()
+ {
+ // ReSharper disable once NonReadonlyMemberInGetHashCode
+ return Radians.GetHashCode();
+ }
+
+ public static implicit operator float(Angle angle)
+ {
+ return angle.Radians;
+ }
+
+ public static explicit operator Angle(float angle)
+ {
+ return new Angle(angle);
+ }
+
+ public static Angle operator -(Angle angle)
+ {
+ return new Angle(-angle.Radians);
+ }
+
+ public static bool operator ==(Angle a, Angle b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(Angle a, Angle b)
+ {
+ return !a.Equals(b);
+ }
+
+ public static Angle operator -(Angle left, Angle right)
+ {
+ return new Angle(left.Radians - right.Radians);
+ }
+
+ public static Angle operator *(Angle left, float right)
+ {
+ return new Angle(left.Radians*right);
+ }
+
+ public static Angle operator *(float left, Angle right)
+ {
+ return new Angle(right.Radians*left);
+ }
+
+ public static Angle operator +(Angle left, Angle right)
+ {
+ return new Angle(left.Radians + right.Radians);
+ }
+
+ public override string ToString()
+ {
+ return $"{Radians} Radians";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs
new file mode 100644
index 0000000..324ebb2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs
@@ -0,0 +1,579 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77
+
+ /// <summary>
+ /// An axis-aligned, four sided, two dimensional box defined by a centre <see cref="Point2" /> and a radii
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="BoundingRectangle" /> is categorized by having its faces oriented in such a way that its
+ /// face normals are at all times parallel with the axes of the given coordinate system.
+ /// </para>
+ /// <para>
+ /// The <see cref="BoundingRectangle" /> of a rotated <see cref="BoundingRectangle" /> will be equivalent or larger
+ /// in size
+ /// than the original depending on the angle of rotation.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DebuggerDisplay("{" + nameof(DebugDisplayString) + ",nq}")]
+ public struct BoundingRectangle : IEquatable<BoundingRectangle>,
+ IEquatableByRef<BoundingRectangle>
+ {
+ /// <summary>
+ /// The <see cref="BoundingRectangle" /> with <see cref="Center" /> <see cref="Point2.Zero"/> and
+ /// <see cref="HalfExtents" /> set to <see cref="Vector2.Zero"/>.
+ /// </summary>
+ public static readonly BoundingRectangle Empty = new BoundingRectangle();
+
+ /// <summary>
+ /// The centre position of this <see cref="BoundingRectangle" />.
+ /// </summary>
+ public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ public Vector2 HalfExtents;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radii <see cref="Size2" />.
+ /// </summary>
+ /// <param name="center">The centre <see cref="Point2" />.</param>
+ /// <param name="halfExtents">The radii <see cref="Vector2" />.</param>
+ public BoundingRectangle(Point2 center, Size2 halfExtents)
+ {
+ Center = center;
+ HalfExtents = halfExtents;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out BoundingRectangle result)
+ {
+ result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f);
+ result.HalfExtents = new Vector2((maximum.X - minimum.X) * 0.5f, (maximum.Y - minimum.Y) * 0.5f);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>The resulting <see cref="BoundingRectangle" />.</returns>
+ public static BoundingRectangle CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ BoundingRectangle result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out BoundingRectangle result)
+ {
+ Point2 minimum;
+ Point2 maximum;
+ PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum);
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>The resulting <see cref="BoundingRectangle" />.</returns>
+ public static BoundingRectangle CreateFrom(IReadOnlyList<Point2> points)
+ {
+ BoundingRectangle result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ /// <returns>
+ /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the
+ /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static void Transform(ref BoundingRectangle boundingRectangle,
+ ref Matrix2 transformMatrix, out BoundingRectangle result)
+ {
+ PrimitivesHelper.TransformRectangle(ref boundingRectangle.Center, ref boundingRectangle.HalfExtents, ref transformMatrix);
+ result.Center = boundingRectangle.Center;
+ result.HalfExtents = boundingRectangle.HalfExtents;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <returns>
+ /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the
+ /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static BoundingRectangle Transform(BoundingRectangle boundingRectangle,
+ ref Matrix2 transformMatrix)
+ {
+ BoundingRectangle result;
+ Transform(ref boundingRectangle, ref transformMatrix, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <param name="result">The resulting bounding rectangle that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.</param>
+ public static void Union(ref BoundingRectangle first, ref BoundingRectangle second, out BoundingRectangle result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 6.5; Bounding Volume Hierarchies - Merging Bounding Volumes. pg 267
+
+ var firstMinimum = first.Center - first.HalfExtents;
+ var firstMaximum = first.Center + first.HalfExtents;
+ var secondMinimum = second.Center - second.HalfExtents;
+ var secondMaximum = second.Center + second.HalfExtents;
+
+ var minimum = Point2.Minimum(firstMinimum, secondMinimum);
+ var maximum = Point2.Maximum(firstMaximum, secondMaximum);
+
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.
+ /// </returns>
+ public static BoundingRectangle Union(BoundingRectangle first, BoundingRectangle second)
+ {
+ BoundingRectangle result;
+ Union(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains both the specified
+ /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="boundingRectangle" /> and
+ /// this
+ /// <see cref="BoundingRectangle" />.
+ /// </returns>
+ public BoundingRectangle Union(BoundingRectangle boundingRectangle)
+ {
+ return Union(this, boundingRectangle);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <param name="result">The resulting bounding rectangle that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param>
+ public static void Intersection(ref BoundingRectangle first,
+ ref BoundingRectangle second, out BoundingRectangle result)
+ {
+ var firstMinimum = first.Center - first.HalfExtents;
+ var firstMaximum = first.Center + first.HalfExtents;
+ var secondMinimum = second.Center - second.HalfExtents;
+ var secondMaximum = second.Center + second.HalfExtents;
+
+ var minimum = Point2.Maximum(firstMinimum, secondMinimum);
+ var maximum = Point2.Minimum(firstMaximum, secondMaximum);
+
+ if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y))
+ result = new BoundingRectangle();
+ else
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public static BoundingRectangle Intersection(BoundingRectangle first,
+ BoundingRectangle second)
+ {
+ BoundingRectangle result;
+ Intersection(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the specified
+ /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="boundingRectangle" /> and
+ /// this <see cref="BoundingRectangle"/>, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public BoundingRectangle Intersection(BoundingRectangle boundingRectangle)
+ {
+ BoundingRectangle result;
+ Intersection(ref this, ref boundingRectangle, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref BoundingRectangle first, ref BoundingRectangle second)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 80
+
+ var distance = first.Center - second.Center;
+ var radii = first.HalfExtents + second.HalfExtents;
+ return Math.Abs(distance.X) <= radii.X && Math.Abs(distance.Y) <= radii.Y;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(BoundingRectangle first, BoundingRectangle second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref BoundingRectangle boundingRectangle)
+ {
+ return Intersects(ref this, ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle)
+ {
+ return Intersects(ref this, ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Updates this <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ public void UpdateFromPoints(IReadOnlyList<Point2> points)
+ {
+ var boundingRectangle = CreateFrom(points);
+ Center = boundingRectangle.Center;
+ HalfExtents = boundingRectangle.HalfExtents;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref BoundingRectangle boundingRectangle, ref Point2 point)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 78
+
+ var distance = boundingRectangle.Center - point;
+ var radii = boundingRectangle.HalfExtents;
+
+ return (Math.Abs(distance.X) <= radii.X) && (Math.Abs(distance.Y) <= radii.Y);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(BoundingRectangle boundingRectangle, Point2 point)
+ {
+ return Contains(ref boundingRectangle, ref point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="BoundingRectangle" /> contains the specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(this, point);
+ }
+
+ /// <summary>
+ /// Computes the squared distance from this <see cref="BoundingRectangle"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="BoundingRectangle"/> to the <paramref name="point"/>.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ return PrimitivesHelper.SquaredDistanceToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point);
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ Point2 result;
+ PrimitivesHelper.ClosestPointToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(BoundingRectangle first, BoundingRectangle second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(BoundingRectangle first, BoundingRectangle second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(BoundingRectangle boundingRectangle)
+ {
+ return Equals(ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />;
+ /// otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref BoundingRectangle boundingRectangle)
+ {
+ return (boundingRectangle.Center == Center) && (boundingRectangle.HalfExtents == HalfExtents);
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="BoundingRectangle" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is BoundingRectangle)
+ return Equals((BoundingRectangle)obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="BoundingRectangle" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="BoundingRectangle" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Center.GetHashCode() * 397) ^ HalfExtents.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="BoundingRectangle" />.
+ /// </returns>
+ public static implicit operator BoundingRectangle(Rectangle rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+ return new BoundingRectangle(centre, radii);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static implicit operator Rectangle(BoundingRectangle boundingRectangle)
+ {
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ return new Rectangle((int)minimum.X, (int)minimum.Y, (int)boundingRectangle.HalfExtents.X * 2,
+ (int)boundingRectangle.HalfExtents.Y * 2);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="RectangleF" /> to a <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="BoundingRectangle" />.
+ /// </returns>
+ public static implicit operator BoundingRectangle(RectangleF rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+ return new BoundingRectangle(centre, radii);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static implicit operator RectangleF(BoundingRectangle boundingRectangle)
+ {
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ return new RectangleF(minimum.X, minimum.Y, boundingRectangle.HalfExtents.X * 2,
+ boundingRectangle.HalfExtents.Y * 2);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="BoundingRectangle" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radii: {HalfExtents}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs
new file mode 100644
index 0000000..70f9307
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs
@@ -0,0 +1,519 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88
+
+ /// <summary>
+ /// A two dimensional circle defined by a centre <see cref="Point2" /> and a radius <see cref="float" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="CircleF" /> is categorized by the set of all points in a plane that are at equal distance from
+ /// the
+ /// centre.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DataContract]
+ public struct CircleF : IEquatable<CircleF>, IEquatableByRef<CircleF>, IShapeF
+ {
+ /// <summary>
+ /// The centre position of this <see cref="CircleF" />.
+ /// </summary>
+ [DataMember] public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point to any point on the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ [DataMember] public float Radius;
+
+ /// <summary>
+ /// Gets or sets the position of the circle.
+ /// </summary>
+ public Point2 Position
+ {
+ get => Center;
+ set => Center = value;
+ }
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Center.X - Radius;
+ var minY = Center.Y - Radius;
+ return new RectangleF(minX, minY, Diameter, Diameter);
+ }
+ }
+
+ /// <summary>
+ /// Gets the distance from a point to the opposite point, both on the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ public float Diameter => 2 * Radius;
+
+ /// <summary>
+ /// Gets the distance around the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ public float Circumference => 2 * MathHelper.Pi * Radius;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CircleF" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radius <see cref="float" />.
+ /// </summary>
+ /// <param name="center">The centre point.</param>
+ /// <param name="radius">The radius.</param>
+ public CircleF(Point2 center, float radius)
+ {
+ Center = center;
+ Radius = radius;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting circle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out CircleF result)
+ {
+ result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f);
+ var distanceVector = maximum - minimum;
+ result.Radius = distanceVector.X > distanceVector.Y ? distanceVector.X * 0.5f : distanceVector.Y * 0.5f;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>An <see cref="CircleF" />.</returns>
+ public static CircleF CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ CircleF result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting circle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out CircleF result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 89-90
+
+ if (points == null || points.Count == 0)
+ {
+ result = default(CircleF);
+ return;
+ }
+
+ var minimum = new Point2(float.MaxValue, float.MaxValue);
+ var maximum = new Point2(float.MinValue, float.MinValue);
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ for (var index = points.Count - 1; index >= 0; --index)
+ {
+ var point = points[index];
+ minimum = Point2.Minimum(minimum, point);
+ maximum = Point2.Maximum(maximum, point);
+ }
+
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>An <see cref="CircleF" />.</returns>
+ public static CircleF CreateFrom(IReadOnlyList<Point2> points)
+ {
+ CircleF result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="CircleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref CircleF first, ref CircleF second)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88
+
+ // Calculate squared distance between centers
+ var distanceVector = first.Center - second.Center;
+ var distanceSquared = distanceVector.Dot(distanceVector);
+ var radiusSum = first.Radius + second.Radius;
+ return distanceSquared <= radiusSum * radiusSum;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="CircleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(CircleF first, CircleF second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref CircleF circle)
+ {
+ return Intersects(ref this, ref circle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(CircleF circle)
+ {
+ return Intersects(ref this, ref circle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c>
+ /// .
+ /// </returns>
+ public static bool Intersects(ref CircleF circle, ref BoundingRectangle rectangle)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.25; Basic Primitives Test - Testing Sphere Against AABB. pg 165-166
+
+ // Compute squared distance between sphere center and AABB boundary
+ var distanceSquared = rectangle.SquaredDistanceTo(circle.Center);
+ // Circle and AABB intersect if the (squared) distance between the AABB's boundary and the circle is less than the (squared) circle's radius
+ return distanceSquared <= circle.Radius * circle.Radius;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c>
+ /// .
+ /// </returns>
+ public static bool Intersects(CircleF circle, BoundingRectangle rectangle)
+ {
+ return Intersects(ref circle, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref BoundingRectangle rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref CircleF circle, Point2 point)
+ {
+ var dx = circle.Center.X - point.X;
+ var dy = circle.Center.Y - point.Y;
+ var d2 = dx * dx + dy * dy;
+ var r2 = circle.Radius * circle.Radius;
+ return d2 <= r2;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(CircleF circle, Point2 point)
+ {
+ return Contains(ref circle, point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="CircleF" /> contains the specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(ref this, point);
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="CircleF" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="CircleF" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ var distanceVector = point - Center;
+ var lengthSquared = distanceVector.Dot(distanceVector);
+ if (lengthSquared <= Radius * Radius)
+ return point;
+ distanceVector.Normalize();
+ return Center + Radius * distanceVector;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="Point2" /> on the boundary of of this <see cref="CircleF" /> using the specified angle.
+ /// </summary>
+ /// <param name="angle">The angle in radians.</param>
+ /// <returns>The <see cref="Point2" /> on the boundary of this <see cref="CircleF" /> using <paramref name="angle" />.</returns>
+ public Point2 BoundaryPointAt(float angle)
+ {
+ var direction = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ return Center + Radius * direction;
+ }
+
+ [Obsolete("Circle.GetPointAlongEdge() may be removed in the future. Use BoundaryPointAt() instead.")]
+ public Point2 GetPointAlongEdge(float angle)
+ {
+ return Center + new Vector2(Radius * (float) Math.Cos(angle), Radius * (float) Math.Sin(angle));
+ }
+
+ /// <summary>
+ /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(CircleF first, CircleF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two
+ /// <see cref="CircleF" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(CircleF first, CircleF second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(CircleF circle)
+ {
+ return Equals(ref circle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />;
+ /// otherwise,<c>false</c>.
+ /// </returns>
+ public bool Equals(ref CircleF circle)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return circle.Center == Center && circle.Radius == Radius;
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="CircleF" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is CircleF && Equals((CircleF) obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="CircleF" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="CircleF" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Center.GetHashCode() * 397) ^ Radius.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static explicit operator Rectangle(CircleF circle)
+ {
+ var diameter = (int) circle.Diameter;
+ return new Rectangle((int) (circle.Center.X - circle.Radius), (int) (circle.Center.Y - circle.Radius),
+ diameter, diameter);
+ }
+
+ /// <summary>
+ /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public Rectangle ToRectangle()
+ {
+ return (Rectangle)this;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="CircleF" />.
+ /// </returns>
+ public static explicit operator CircleF(Rectangle rectangle)
+ {
+ var halfWidth = rectangle.Width / 2;
+ var halfHeight = rectangle.Height / 2;
+ return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight),
+ halfWidth > halfHeight ? halfWidth : halfHeight);
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public static explicit operator RectangleF(CircleF circle)
+ {
+ var diameter = circle.Diameter;
+ return new RectangleF(circle.Center.X - circle.Radius, circle.Center.Y - circle.Radius, diameter, diameter);
+ }
+
+ /// <summary>
+ /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public RectangleF ToRectangleF()
+ {
+ return (RectangleF)this;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="RectangleF" /> to a <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="CircleF" />.
+ /// </returns>
+ public static explicit operator CircleF(RectangleF rectangle)
+ {
+ var halfWidth = rectangle.Width * 0.5f;
+ var halfHeight = rectangle.Height * 0.5f;
+ return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight),
+ halfWidth > halfHeight ? halfWidth : halfHeight);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="CircleF" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="CircleF" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radius: {Radius}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs
new file mode 100644
index 0000000..684a770
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ [DataContract]
+ public struct EllipseF : IEquatable<EllipseF>, IEquatableByRef<EllipseF>, IShapeF
+ {
+ [DataMember] public Vector2 Center { get; set; }
+ [DataMember] public float RadiusX { get; set; }
+ [DataMember] public float RadiusY { get; set; }
+
+ public Point2 Position
+ {
+ get => Center;
+ set => Center = value;
+ }
+
+ public EllipseF(Vector2 center, float radiusX, float radiusY)
+ {
+ Center = center;
+ RadiusX = radiusX;
+ RadiusY = radiusY;
+ }
+
+ public float Left => Center.X - RadiusX;
+ public float Top => Center.Y - RadiusY;
+ public float Right => Center.X + RadiusX;
+ public float Bottom => Center.Y + RadiusY;
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(float x, float y)
+ {
+ float xCalc = (float) (Math.Pow(x - Center.X, 2) / Math.Pow(RadiusX, 2));
+ float yCalc = (float) (Math.Pow(y - Center.Y, 2) / Math.Pow(RadiusY, 2));
+
+ return xCalc + yCalc <= 1;
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+
+ public bool Equals(EllipseF ellispse)
+ {
+ return Equals(ref ellispse);
+ }
+
+ public bool Equals(ref EllipseF ellispse)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return ellispse.Center == Center
+ && ellispse.RadiusX == RadiusX
+ && ellispse.RadiusY == RadiusY;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is EllipseF && Equals((EllipseF)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Center.GetHashCode();
+ hashCode = (hashCode * 397) ^ RadiusX.GetHashCode();
+ hashCode = (hashCode * 397) ^ RadiusY.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"Centre: {Center}, RadiusX: {RadiusX}, RadiusY: {RadiusY}";
+ }
+
+ public static bool operator ==(EllipseF first, EllipseF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ public static bool operator !=(EllipseF first, EllipseF second)
+ {
+ return !(first == second);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs
new file mode 100644
index 0000000..472518b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs
@@ -0,0 +1,127 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A random number generator that uses a fast algorithm to generate random values.
+ /// The speed comes at the price of true 'randomness' though, there are noticeable
+ /// patterns & it compares quite unfavourably to other algorithms in that respect.
+ /// It's a good choice in situations where speed is more desirable than a
+ /// good random distribution, and a poor choice when random distribution is important.
+ /// </summary>
+ public class FastRandom
+ {
+ private int _state;
+
+ public FastRandom()
+ : this(1)
+ {
+ }
+
+ public FastRandom(int seed)
+ {
+ if (seed < 1)
+ throw new ArgumentOutOfRangeException(nameof(seed), "seed must be greater than zero");
+
+ _state = seed;
+ }
+
+ /// <summary>
+ /// Gets the next random integer value.
+ /// </summary>
+ /// <returns>A random positive integer.</returns>
+ public int Next()
+ {
+ _state = 214013*_state + 2531011;
+ return (_state >> 16) & 0x7FFF;
+ }
+
+ /// <summary>
+ /// Gets the next random integer value which is greater than zero and less than or equal to
+ /// the specified maxmimum value.
+ /// </summary>
+ /// <param name="max">The maximum random integer value to return.</param>
+ /// <returns>A random integer value between zero and the specified maximum value.</returns>
+ public int Next(int max)
+ {
+ return (int) (max*NextSingle() + 0.5f);
+ }
+
+ /// <summary>
+ /// Gets the next random integer between the specified minimum and maximum values.
+ /// </summary>
+ /// <param name="min">The inclusive minimum value.</param>
+ /// <param name="max">The inclusive maximum value.</param>
+ public int Next(int min, int max)
+ {
+ return (int) ((max - min)*NextSingle() + 0.5f) + min;
+ }
+
+ /// <summary>
+ /// Gets the next random integer between the specified range of values.
+ /// </summary>
+ /// <param name="range">A range representing the inclusive minimum and maximum values.</param>
+ /// <returns>A random integer between the specified minumum and maximum values.</returns>
+ public int Next(Range<int> range)
+ {
+ return Next(range.Min, range.Max);
+ }
+
+ /// <summary>
+ /// Gets the next random single value.
+ /// </summary>
+ /// <returns>A random single value between 0 and 1.</returns>
+ public float NextSingle()
+ {
+ return Next()/(float) short.MaxValue;
+ }
+
+ /// <summary>
+ /// Gets the next random single value which is greater than zero and less than or equal to
+ /// the specified maxmimum value.
+ /// </summary>
+ /// <param name="max">The maximum random single value to return.</param>
+ /// <returns>A random single value between zero and the specified maximum value.</returns>
+ public float NextSingle(float max)
+ {
+ return max*NextSingle();
+ }
+
+ /// <summary>
+ /// Gets the next random single value between the specified minimum and maximum values.
+ /// </summary>
+ /// <param name="min">The inclusive minimum value.</param>
+ /// <param name="max">The inclusive maximum value.</param>
+ /// <returns>A random single value between the specified minimum and maximum values.</returns>
+ public float NextSingle(float min, float max)
+ {
+ return (max - min)*NextSingle() + min;
+ }
+
+ /// <summary>
+ /// Gets the next random single value between the specified range of values.
+ /// </summary>
+ /// <param name="range">A range representing the inclusive minimum and maximum values.</param>
+ /// <returns>A random single value between the specified minimum and maximum values.</returns>
+ public float NextSingle(Range<float> range)
+ {
+ return NextSingle(range.Min, range.Max);
+ }
+
+ /// <summary>
+ /// Gets the next random angle value.
+ /// </summary>
+ /// <returns>A random angle value.</returns>
+ public float NextAngle()
+ {
+ return NextSingle(-MathHelper.Pi, MathHelper.Pi);
+ }
+
+ public void NextUnitVector(out Vector2 vector)
+ {
+ var angle = NextAngle();
+ vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs
new file mode 100644
index 0000000..a3792e6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended
+{
+ public static class FloatHelper
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Swap(ref float value1, ref float value2)
+ {
+ var temp = value1;
+ value1 = value2;
+ value2 = temp;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs
new file mode 100644
index 0000000..cb56710
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs
@@ -0,0 +1,1038 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+namespace MonoGame.Extended
+{
+ // https://en.wikipedia.org/wiki/Matrix_(mathematics)
+ // "Immersive Linear Algebra"; Jacob Ström, Kalle Åström & Tomas Akenine-Möller; 2015-2016. Chapter 6: The Matrix. http://immersivemath.com/ila/ch06_matrices/ch06.html
+ // "Real-Time Collision Detection"; Christer Ericson; 2005. Chapter 3.1: A Math and Geometry Primer - Matrices. pg 23-34
+
+ // Original code was from Matrix2D.cs in the Nez Library: https://github.com/prime31/Nez/
+
+ /// <summary>
+ /// Defines a 3x3 matrix using floating point numbers which can store two dimensional translation, scale and rotation
+ /// information in a right-handed coordinate system.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Matrices use a row vector layout in the XNA / MonoGame Framework but, in general, matrices can be either have
+ /// a row vector or column vector layout. Row vector matrices view vectors as a row from left to right, while
+ /// column vector matrices view vectors as a column from top to bottom. For example, the <see cref="Translation" />
+ /// corresponds to the fields <see cref="M31" /> and <see cref="M32" />.
+ /// </para>
+ /// <para>
+ /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the
+ /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a
+ /// value of <code>1.0f</code>.
+ /// </para>
+ /// </remarks>
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Matrix2 : IEquatable<Matrix2>, IEquatableByRef<Matrix2>
+ {
+ public float M11; // x scale, also used for rotation
+ public float M12; // used for rotation
+
+ public float M21; // used for rotation
+ public float M22; // y scale, also used for rotation
+
+ public float M31; // x translation
+ public float M32; // y translation
+
+ /// <summary>
+ /// Gets the identity matrix.
+ /// </summary>
+ /// <value>
+ /// The identity matrix.
+ /// </value>
+ public static Matrix2 Identity { get; } = new Matrix2(1f, 0f, 0f, 1f, 0f, 0f);
+
+ /// <summary>
+ /// Gets the translation.
+ /// </summary>
+ /// <value>
+ /// The translation.
+ /// </value>
+ /// <remarks>The <see cref="Translation" /> is equal to the vector <code>(M31, M32)</code>.</remarks>
+ public Vector2 Translation => new Vector2(M31, M32);
+
+ /// <summary>
+ /// Gets the rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The rotation angle in radians.
+ /// </value>
+ /// <remarks>
+ /// The <see cref="Rotation" /> is equal to <code>Atan2(M21, M11)</code>.
+ /// </remarks>
+ public float Rotation => (float)Math.Atan2(M21, M11);
+
+ /// <summary>
+ /// Gets the scale.
+ /// </summary>
+ /// <value>
+ /// The scale.
+ /// </value>
+ /// <remarks>
+ /// The <see cref="Scale" /> is equal to the vector
+ /// <code>(Sqrt(M11 * M11 + M21 * M21), Sqrt(M12 * M12 + M22 * M22))</code>.
+ /// </remarks>
+ public Vector2 Scale
+ {
+ get
+ {
+ var scaleX = (float)Math.Sqrt(M11 * M11 + M21 * M21);
+ var scaleY = (float)Math.Sqrt(M12 * M12 + M22 * M22);
+ return new Vector2(scaleX, scaleY);
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct.
+ /// </summary>
+ /// <param name="m11">The value to initialize <see cref="M11" /> to.</param>
+ /// <param name="m12">The value to initialize <see cref="M12" /> to.</param>
+ /// <param name="m21">The value to initialize <see cref="M21" /> to.</param>
+ /// <param name="m22">The value to initialize <see cref="M22" /> to.</param>
+ /// <param name="m31">The value to initialize <see cref="M31" /> to.</param>
+ /// <param name="m32">The value to initialize <see cref="M32" /> to.</param>
+ /// <remarks>
+ /// <para>
+ /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the
+ /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a
+ /// value of <code>1.0f</code>.
+ /// </para>
+ /// </remarks>
+ public Matrix2(float m11, float m12, float m21, float m22, float m31, float m32)
+ {
+ M11 = m11;
+ M12 = m12;
+
+ M21 = m21;
+ M22 = m22;
+
+ M31 = m31;
+ M32 = m32;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>The resulting <see cref="Vector2" />.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector2 Transform(Vector2 vector)
+ {
+ Vector2 result;
+ Transform(vector, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <param name="result">The resulting <see cref="Vector2" />.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(Vector2 vector, out Vector2 result)
+ {
+ result.X = vector.X * M11 + vector.Y * M21 + M31;
+ result.Y = vector.X * M12 + vector.Y * M22 + M32;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="x">The x value of the vector.</param>
+ /// <param name="y">The y value of the vector.</param>
+ /// <param name="result">The resulting <see cref="Vector2" />.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(float x, float y, out Vector2 result)
+ {
+ result.X = x * M11 + y * M21 + M31;
+ result.Y = x * M12 + y * M22 + M32;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(float x, float y, ref Vector3 result)
+ {
+ result.X = x * M11 + y * M21 + M31;
+ result.Y = x * M12 + y * M22 + M32;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="origin">The point which to rotate and scale around.</param>
+ /// <param name="transformMatrix">The resulting <see cref="Matrix2" /></param>
+ public static void CreateFrom(Vector2 position, float rotation, Vector2? scale, Vector2? origin,
+ out Matrix2 transformMatrix)
+ {
+ transformMatrix = Identity;
+
+ if (origin.HasValue)
+ {
+ transformMatrix.M31 = -origin.Value.X;
+ transformMatrix.M32 = -origin.Value.Y;
+ }
+
+ if (scale.HasValue)
+ {
+ var scaleMatrix = CreateScale(scale.Value);
+ Multiply(ref transformMatrix, ref scaleMatrix, out transformMatrix);
+ }
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0f)
+ {
+ var rotationMatrix = CreateRotationZ(-rotation);
+ Multiply(ref transformMatrix, ref rotationMatrix, out transformMatrix);
+ }
+
+ var translationMatrix = CreateTranslation(position);
+ Multiply(ref transformMatrix, ref translationMatrix, out transformMatrix);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="origin">The point which to rotate and scale around.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateFrom(Vector2 position, float rotation, Vector2? scale = null, Vector2? origin = null)
+ {
+ var transformMatrix = Identity;
+
+ if (origin.HasValue)
+ {
+ transformMatrix.M31 = -origin.Value.X;
+ transformMatrix.M32 = -origin.Value.Y;
+ }
+
+ if (scale.HasValue)
+ {
+ var scaleMatrix = CreateScale(scale.Value);
+ transformMatrix = Multiply(transformMatrix, scaleMatrix);
+ }
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0f)
+ {
+ var rotationMatrix = CreateRotationZ(-rotation);
+ transformMatrix = Multiply(transformMatrix, rotationMatrix);
+ }
+
+ var translationMatrix = CreateTranslation(position);
+ transformMatrix = Multiply(transformMatrix, translationMatrix);
+
+ return transformMatrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices
+ /// around the z-axis.
+ /// </summary>
+ /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateRotationZ(float radians)
+ {
+ Matrix2 result;
+ CreateRotationZ(radians, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices around the z-axis.
+ /// </summary>
+ /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateRotationZ(float radians, out Matrix2 result)
+ {
+ var val1 = (float)Math.Cos(radians);
+ var val2 = (float)Math.Sin(radians);
+
+ result = new Matrix2
+ {
+ M11 = val1,
+ M12 = val2,
+ M21 = -val2,
+ M22 = val1,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(float scale)
+ {
+ Matrix2 result;
+ CreateScale(scale, scale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(float scale, out Matrix2 result)
+ {
+ CreateScale(scale, scale, out result);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="xScale">The amount to scale by on the x-axis.</param>
+ /// <param name="yScale">The amount to scale by on the y-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(float xScale, float yScale)
+ {
+ Matrix2 result;
+ CreateScale(xScale, yScale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="xScale">The amount to scale by on the x-axis.</param>
+ /// <param name="yScale">The amount to scale by on the y-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(float xScale, float yScale, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = xScale,
+ M12 = 0,
+ M21 = 0,
+ M22 = yScale,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amounts to scale by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(Vector2 scale)
+ {
+ Matrix2 result;
+ CreateScale(ref scale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amounts to scale by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(ref Vector2 scale, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = scale.X,
+ M12 = 0,
+ M21 = 0,
+ M22 = scale.Y,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="xPosition">The amount to translate by on the x-axis.</param>
+ /// <param name="yPosition">The amount to translate by on the y-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateTranslation(float xPosition, float yPosition)
+ {
+ Matrix2 result;
+ CreateTranslation(xPosition, yPosition, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="xPosition">The amount to translate by on the x-axis.</param>
+ /// <param name="yPosition">The amount to translate by on the y-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateTranslation(float xPosition, float yPosition, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = 1,
+ M12 = 0,
+ M21 = 0,
+ M22 = 1,
+ M31 = xPosition,
+ M32 = yPosition
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateTranslation(Vector2 position)
+ {
+ Matrix2 result;
+ CreateTranslation(ref position, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateTranslation(ref Vector2 position, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = 1,
+ M12 = 0,
+ M21 = 0,
+ M22 = 1,
+ M31 = position.X,
+ M32 = position.Y
+ };
+ }
+
+ /// <summary>
+ /// Calculates the determinant of the <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>The determinant of the <see cref="Matrix2" />.</returns>
+ public float Determinant()
+ {
+ return M11 * M22 - M12 * M21;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Add(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 += matrix2.M11;
+ matrix1.M12 += matrix2.M12;
+
+ matrix1.M21 += matrix2.M21;
+ matrix1.M22 += matrix2.M22;
+
+ matrix1.M31 += matrix2.M31;
+ matrix1.M32 += matrix2.M32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> summation of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Add(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 + matrix2.M11,
+ M12 = matrix1.M12 + matrix2.M12,
+ M21 = matrix1.M21 + matrix2.M21,
+ M22 = matrix1.M22 + matrix2.M22,
+ M31 = matrix1.M31 + matrix2.M31,
+ M32 = matrix1.M32 + matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator +(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 + matrix2.M11;
+ matrix1.M12 = matrix1.M12 + matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 + matrix2.M21;
+ matrix1.M22 = matrix1.M22 + matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 + matrix2.M31;
+ matrix1.M32 = matrix1.M32 + matrix2.M32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Subtract(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 - matrix2.M11;
+ matrix1.M12 = matrix1.M12 - matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 - matrix2.M21;
+ matrix1.M22 = matrix1.M22 - matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 - matrix2.M31;
+ matrix1.M32 = matrix1.M32 - matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> substraction of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Subtract(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 - matrix2.M11,
+ M12 = matrix1.M12 - matrix2.M12,
+ M21 = matrix1.M21 - matrix2.M21,
+ M22 = matrix1.M22 - matrix2.M22,
+ M31 = matrix1.M31 - matrix2.M31,
+ M32 = matrix1.M32 - matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator -(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 - matrix2.M11;
+ matrix1.M12 = matrix1.M12 - matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 - matrix2.M21;
+ matrix1.M22 = matrix1.M22 - matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 - matrix2.M31;
+ matrix1.M32 = matrix1.M32 - matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Multiply(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ matrix1.M11 = m11;
+ matrix1.M12 = m12;
+
+ matrix1.M21 = m21;
+ matrix1.M22 = m22;
+
+ matrix1.M31 = m31;
+ matrix1.M32 = m32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> multiplication of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Multiply(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ result = new Matrix2
+ {
+ M11 = m11,
+ M12 = m12,
+ M21 = m21,
+ M22 = m22,
+ M31 = m31,
+ M32 = m32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Multiply(Matrix2 matrix, float scalar)
+ {
+ matrix.M11 *= scalar;
+ matrix.M12 *= scalar;
+
+ matrix.M21 *= scalar;
+ matrix.M22 *= scalar;
+
+ matrix.M31 *= scalar;
+ matrix.M32 *= scalar;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Calculates the multiplication of a <see cref="Matrix2" /> by a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to multiple the <see cref="Matrix2" /> by.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Multiply(ref Matrix2 matrix, float scalar, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix.M11 * scalar,
+ M12 = matrix.M12 * scalar,
+ M21 = matrix.M21 * scalar,
+ M22 = matrix.M22 * scalar,
+ M31 = matrix.M31 * scalar,
+ M32 = matrix.M32 * scalar
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator *(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ matrix1.M11 = m11;
+ matrix1.M12 = m12;
+
+ matrix1.M21 = m21;
+ matrix1.M22 = m22;
+
+ matrix1.M31 = m31;
+ matrix1.M32 = m32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator *(Matrix2 matrix, float scalar)
+ {
+ matrix.M11 = matrix.M11 * scalar;
+ matrix.M12 = matrix.M12 * scalar;
+
+ matrix.M21 = matrix.M21 * scalar;
+ matrix.M22 = matrix.M22 * scalar;
+
+ matrix.M31 = matrix.M31 * scalar;
+ matrix.M32 = matrix.M32 * scalar;
+
+ return matrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Divide(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 / matrix2.M11;
+ matrix1.M12 = matrix1.M12 / matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 / matrix2.M21;
+ matrix1.M22 = matrix1.M22 / matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 / matrix2.M31;
+ matrix1.M32 = matrix1.M32 / matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> division of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Divide(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 / matrix2.M11,
+ M12 = matrix1.M12 / matrix2.M12,
+ M21 = matrix1.M21 / matrix2.M21,
+ M22 = matrix1.M22 / matrix2.M22,
+ M31 = matrix1.M31 / matrix2.M31,
+ M32 = matrix1.M32 / matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Divide(Matrix2 matrix, float scalar)
+ {
+ var num = 1f / scalar;
+ matrix.M11 = matrix.M11 * num;
+ matrix.M12 = matrix.M12 * num;
+
+ matrix.M21 = matrix.M21 * num;
+ matrix.M22 = matrix.M22 * num;
+
+ matrix.M31 = matrix.M31 * num;
+ matrix.M32 = matrix.M32 * num;
+
+ return matrix;
+ }
+
+ /// <summary>
+ /// Calculates the division of a <see cref="Matrix2" /> by a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Divide(ref Matrix2 matrix, float scalar, out Matrix2 result)
+ {
+ var num = 1f / scalar;
+ result = new Matrix2
+ {
+ M11 = matrix.M11 * num,
+ M12 = matrix.M12 * num,
+ M21 = matrix.M21 * num,
+ M22 = matrix.M22 * num,
+ M31 = matrix.M31 * num,
+ M32 = matrix.M32 * num
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator /(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 / matrix2.M11;
+ matrix1.M12 = matrix1.M12 / matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 / matrix2.M21;
+ matrix1.M22 = matrix1.M22 / matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 / matrix2.M31;
+ matrix1.M32 = matrix1.M32 / matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator /(Matrix2 matrix, float scalar)
+ {
+ var num = 1f / scalar;
+ matrix.M11 = matrix.M11 * num;
+ matrix.M12 = matrix.M12 * num;
+
+ matrix.M21 = matrix.M21 * num;
+ matrix.M22 = matrix.M22 * num;
+
+ matrix.M31 = matrix.M31 * num;
+ matrix.M32 = matrix.M32 * num;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Invert(Matrix2 matrix)
+ {
+ var det = 1 / matrix.Determinant();
+
+ var m11 = matrix.M22 * det;
+ var m12 = -matrix.M12 * det;
+
+ var m21 = -matrix.M21 * det;
+ var m22 = matrix.M11 * det;
+
+ var m31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det;
+ var m32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det;
+
+ return new Matrix2(m11, m12, m21, m22, m31, m32);
+ }
+
+ /// <summary>
+ /// Calculates the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Invert(ref Matrix2 matrix, out Matrix2 result)
+ {
+ var det = 1 / matrix.Determinant();
+
+ result = new Matrix2
+ {
+ M11 = matrix.M22 * det,
+ M12 = -matrix.M12 * det,
+ M21 = -matrix.M21 * det,
+ M22 = matrix.M11 * det,
+ M31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det,
+ M32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator -(Matrix2 matrix)
+ {
+ matrix.M11 = -matrix.M11;
+ matrix.M12 = -matrix.M12;
+
+ matrix.M21 = -matrix.M21;
+ matrix.M22 = -matrix.M22;
+
+ matrix.M31 = -matrix.M31;
+ matrix.M32 = -matrix.M32;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Compares a <see cref="Matrix2" /> for equality with another <see cref="Matrix2" /> without any tolerance.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns><c>true</c> if the <see cref="Matrix2" />s are equal; <c>false</c> otherwise.</returns>
+ public static bool operator ==(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ return (matrix1.M11 == matrix2.M11) && (matrix1.M12 == matrix2.M12) && (matrix1.M21 == matrix2.M21) &&
+ (matrix1.M22 == matrix2.M22) && (matrix1.M31 == matrix2.M31) && (matrix1.M32 == matrix2.M32);
+ }
+
+ /// <summary>
+ /// Compares a <see cref="Matrix2" /> for inequality with another <see cref="Matrix2" /> without any tolerance.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns><c>true</c> if the <see cref="Matrix2" />s are not equal; <c>false</c> otherwise.</returns>
+ public static bool operator !=(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ return (matrix1.M11 != matrix2.M11) || (matrix1.M12 != matrix2.M12) || (matrix1.M21 != matrix2.M21) ||
+ (matrix1.M22 != matrix2.M22) || (matrix1.M31 != matrix2.M31) || (matrix1.M32 != matrix2.M32);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified
+ /// <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public bool Equals(ref Matrix2 matrix)
+ {
+ return (M11 == matrix.M11) && (M12 == matrix.M12) && (M21 == matrix.M21) && (M22 == matrix.M22) &&
+ (M31 == matrix.M31) && (M32 == matrix.M32);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified
+ /// <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public bool Equals(Matrix2 matrix)
+ {
+ return Equals(ref matrix);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified object;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is Matrix2 && Equals((Matrix2)obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this <see cref="Matrix2" />, suitable for use in hashing algorithms and data structures like a
+ /// hash table.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ // ReSharper disable NonReadonlyMemberInGetHashCode
+ return M11.GetHashCode() + M12.GetHashCode() + M21.GetHashCode() + M22.GetHashCode() + M31.GetHashCode() +
+ M32.GetHashCode();
+ // ReSharper restore NonReadonlyMemberInGetHashCode
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from <see cref="Matrix2" /> to <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>
+ /// The resulting <see cref="Matrix" />.
+ /// </returns>
+ public static implicit operator Matrix(Matrix2 matrix)
+ {
+ return new Matrix(matrix.M11, matrix.M12, 0, 0, matrix.M21, matrix.M22, 0, 0, 0, 0, 1, 0, matrix.M31,
+ matrix.M32, 0, 1);
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="depth">The depth value.</param>
+ /// <param name="result">The resulting <see cref="Matrix" />.</param>
+ public static void ToMatrix(ref Matrix2 matrix, float depth, out Matrix result)
+ {
+ result.M11 = matrix.M11;
+ result.M12 = matrix.M12;
+ result.M13 = 0;
+ result.M14 = 0;
+
+ result.M21 = matrix.M21;
+ result.M22 = matrix.M22;
+ result.M23 = 0;
+ result.M24 = 0;
+
+ result.M31 = 0;
+ result.M32 = 0;
+ result.M33 = 1;
+ result.M34 = 0;
+
+ result.M41 = matrix.M31;
+ result.M42 = matrix.M32;
+ result.M43 = depth;
+ result.M44 = 1;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="depth">The depth value.</param>
+ /// <returns>The resulting <see cref="Matrix" />.</returns>
+ public Matrix ToMatrix(float depth = 0)
+ {
+ Matrix result;
+ ToMatrix(ref this, depth, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the debug display string.
+ /// </summary>
+ /// <value>
+ /// The debug display string.
+ /// </value>
+ internal string DebugDisplayString => this == Identity
+ ? "Identity"
+ : $"T:({Translation.X:0.##},{Translation.Y:0.##}), R:{MathHelper.ToDegrees(Rotation):0.##}°, S:({Scale.X:0.##},{Scale.Y:0.##})"
+ ;
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Matrix2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{{M11:{M11} M12:{M12}}} {{M21:{M21} M22:{M22}}} {{M31:{M31} M32:{M32}}}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs
new file mode 100644
index 0000000..f69da97
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class MatrixExtensions
+ {
+ public static bool Decompose(this Matrix matrix, out Vector2 position, out float rotation, out Vector2 scale)
+ {
+ Vector3 position3, scale3;
+ Quaternion rotationQuaternion;
+
+ if (matrix.Decompose(out scale3, out rotationQuaternion, out position3))
+ {
+ var direction = Vector2.Transform(Vector2.UnitX, rotationQuaternion);
+ rotation = (float) Math.Atan2(direction.Y, direction.X);
+ position = new Vector2(position3.X, position3.Y);
+ scale = new Vector2(scale3.X, scale3.Y);
+ return true;
+ }
+
+ position = Vector2.Zero;
+ rotation = 0;
+ scale = Vector2.One;
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs
new file mode 100644
index 0000000..1439e9f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+using SharpDX.Direct3D;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Bounding Volumes - Oriented Bounding Boxes (OBBs), pg 101.
+
+ /// <summary>
+ /// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle
+ /// <see cref="BoundingRectangle" /> but with an arbitrary orientation <see cref="Vector2" />.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ [DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")]
+ public struct OrientedRectangle : IEquatable<OrientedRectangle>, IShapeF
+ {
+ /// <summary>
+ /// The centre position of this <see cref="OrientedRectangle" />.
+ /// </summary>
+ public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this
+ /// <see cref="OrientedRectangle" />.
+ /// </summary>
+ ///
+ public Vector2 Radii;
+
+ /// <summary>
+ /// The rotation matrix <see cref="Matrix2" /> of the bounding rectangle <see cref="OrientedRectangle" />.
+ /// </summary>
+ public Matrix2 Orientation;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radii <see cref="Size2" />.
+ /// </summary>
+ /// <param name="center">The centre <see cref="Point2" />.</param>
+ /// <param name="radii">The radii <see cref="Vector2" />.</param>
+ /// <param name="orientation">The orientation <see cref="Matrix2" />.</param>
+ public OrientedRectangle(Point2 center, Size2 radii, Matrix2 orientation)
+ {
+ Center = center;
+ Radii = radii;
+ Orientation = orientation;
+ }
+
+ /// <summary>
+ /// Gets a list of points defining the corner points of the oriented rectangle.
+ /// </summary>
+ public IReadOnlyList<Vector2> Points
+ {
+ get
+ {
+ var topLeft = -Radii;
+ var bottomLeft = -new Vector2(Radii.X, -Radii.Y);
+ var topRight = (Vector2)new Point2(Radii.X, -Radii.Y);
+ var bottomRight = Radii;
+
+ return new List<Vector2>
+ {
+ Vector2.Transform(topRight, Orientation) + Center,
+ Vector2.Transform(topLeft, Orientation) + Center,
+ Vector2.Transform(bottomLeft, Orientation) + Center,
+ Vector2.Transform(bottomRight, Orientation) + Center
+ };
+ }
+ }
+
+ public Point2 Position
+ {
+ get => Vector2.Transform(-Radii, Orientation) + Center;
+ set => throw new NotImplementedException();
+ }
+
+ public RectangleF BoundingRectangle => (RectangleF)this;
+
+ /// <summary>
+ /// Computes the <see cref="OrientedRectangle"/> from the specified <paramref name="rectangle"/>
+ /// transformed by <paramref name="transformMatrix"/>.
+ /// </summary>
+ /// <param name="rectangle">The <see cref="OrientedRectangle"/> to transform.</param>
+ /// <param name="transformMatrix">The <see cref="Matrix2"/> transformation.</param>
+ /// <returns>A new <see cref="OrientedRectangle"/>.</returns>
+ public static OrientedRectangle Transform(OrientedRectangle rectangle, ref Matrix2 transformMatrix)
+ {
+ Transform(ref rectangle, ref transformMatrix, out var result);
+ return result;
+ }
+
+ private static void Transform(ref OrientedRectangle rectangle, ref Matrix2 transformMatrix, out OrientedRectangle result)
+ {
+ PrimitivesHelper.TransformOrientedRectangle(
+ ref rectangle.Center,
+ ref rectangle.Orientation,
+ ref transformMatrix);
+ result = new OrientedRectangle();
+ result.Center = rectangle.Center;
+ result.Radii = rectangle.Radii;
+ result.Orientation = rectangle.Orientation;
+ }
+
+ /// <summary>
+ /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
+ /// the values of the <see cref="Center"/>, <see cref="Radii"/> and <see cref="Orientation"/> are
+ /// equal.
+ /// </summary>
+ /// <param name="left">The left <see cref="OrientedRectangle" />.</param>
+ /// <param name="right">The right <see cref="OrientedRectangle" />.</param>
+ /// <returns><c>true</c> if left and right argument are equal; otherwise, <c>false</c>.</returns>
+ public static bool operator ==(OrientedRectangle left, OrientedRectangle right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
+ /// the values of the <see cref="Center"/>, <see cref="Radii"/> or <see cref="Orientation"/> are
+ /// unequal.
+ /// </summary>
+ /// <param name="left">The left <see cref="OrientedRectangle" />.</param>
+ /// <param name="right">The right <see cref="OrientedRectangle" />.</param>
+ /// <returns><c>true</c> if left and right argument are unequal; otherwise, <c>false</c>.</returns>
+ public static bool operator !=(OrientedRectangle left, OrientedRectangle right)
+ {
+ return !left.Equals(right);
+ }
+
+ /// <summary>
+ /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
+ /// </summary>
+ /// <param name="other">The other <see cref="OrientedRectangle"/>.</param>
+ /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
+ /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
+ public bool Equals(OrientedRectangle other)
+ {
+ return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation);
+ }
+
+ /// <summary>
+ /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
+ /// </summary>
+ /// <param name="obj">The <see cref="OrientedRectangle"/> to compare to.</param>
+ /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
+ /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return obj is OrientedRectangle other && Equals(other);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this object instance.
+ /// </summary>
+ /// <returns></returns>
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Center, Radii, Orientation);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="RectangleF" /> to <see cref="OrientedRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle to convert.</param>
+ /// <returns>The resulting <see cref="OrientedRectangle" />.</returns>
+ public static explicit operator OrientedRectangle(RectangleF rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+
+ return new OrientedRectangle(centre, radii, Matrix2.Identity);
+ }
+
+ public static explicit operator RectangleF(OrientedRectangle orientedRectangle)
+ {
+ var topLeft = -orientedRectangle.Radii;
+ var rectangle = new RectangleF(topLeft, orientedRectangle.Radii * 2);
+ var orientation = orientedRectangle.Orientation * Matrix2.CreateTranslation(orientedRectangle.Center);
+ return RectangleF.Transform(rectangle, ref orientation);
+ }
+
+ /// <summary>
+ /// See:
+ /// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml
+ /// https://dyn4j.org/2010/01/sat
+ /// </summary>
+ /// <param name="rectangle"></param>
+ /// <param name="other"></param>
+ /// <returns></returns>
+ /// <exception cref="NotImplementedException"></exception>
+ public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(
+ OrientedRectangle rectangle, OrientedRectangle other)
+ {
+ var corners = rectangle.Points;
+ var otherCorners = other.Points;
+
+ var allAxis = new[]
+ {
+ corners[1] - corners[0],
+ corners[3] - corners[0],
+ otherCorners[1] - otherCorners[0],
+ otherCorners[3] - otherCorners[0],
+ };
+ var normalizedAxis = new[]
+ {
+ allAxis[0],
+ allAxis[1],
+ allAxis[2],
+ allAxis[3]
+ };
+ var overlap = 0f;
+ var minimumTranslationVector = Vector2.Zero;
+
+ // Make the length of each axis 1/edge length, so we know any
+ // dot product must be less than 1 to fall within the edge.
+ for (var a = 0; a < normalizedAxis.Length; a++)
+ {
+ normalizedAxis[a] /= normalizedAxis[a].LengthSquared();
+ }
+
+ for (var a = 0; a < normalizedAxis.Length; a++)
+ {
+ var axisProjectedOnto = normalizedAxis[a];
+ var originalAxis = allAxis[a];
+
+ var p1 = Project(corners, axisProjectedOnto);
+ var p2 = Project(otherCorners, axisProjectedOnto);
+
+ if (!IsOverlapping(p1, p2))
+ {
+ // There was no intersection along this dimension;
+ // the boxes cannot possibly overlap.
+ return (false, Vector2.Zero);
+ }
+
+ var o = GetOverlap(p1, p2);
+ if (o < overlap || overlap == 0f)
+ {
+ overlap = o;
+ minimumTranslationVector = originalAxis * overlap;
+ if (p1.Min > p2.Min)
+ {
+ minimumTranslationVector = -minimumTranslationVector;
+ }
+ }
+ }
+
+ // There was no dimension along which there is no intersection.
+ // Therefore, the boxes overlap.
+ return (true, minimumTranslationVector);
+
+ (float Min, float Max) Project(IReadOnlyList<Vector2> vertices, Vector2 axis)
+ {
+ var t = vertices[0].Dot(axis);
+
+ // Find the extent of box 2 on axis a
+ var min = t;
+ var max = t;
+
+ for (var c = 1; c < 4; c++)
+ {
+ t = vertices[c].Dot(axis);
+
+ if (t < min)
+ {
+ min = t;
+ }
+ else if (t > max)
+ {
+ max = t;
+ }
+ }
+
+ return (min, max);
+ }
+
+ bool IsOverlapping((float Min, float Max) p1, (float Min, float Max) p2)
+ {
+ return p1.Min <= p2.Max && p1.Max >= p2.Min;
+ }
+
+ float GetOverlap((float Min, float Max) p1, (float Min, float Max) p2)
+ {
+ return Math.Min(p1.Max, p2.Max) - Math.Max(p1.Min, p2.Min);
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs
new file mode 100644
index 0000000..5f8de92
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.2; A Math and Geometry Primer - Coordinate Systems and Points. pg 35
+
+ /// <summary>
+ /// A two-dimensional point defined by a 2-tuple of real numbers, (x, y).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A point is a position in two-dimensional space, the location of which is described in terms of a
+ /// two-dimensional coordinate system, given by a reference point, called the origin, and two coordinate axes.
+ /// </para>
+ /// <para>
+ /// A common two-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the
+ /// coordinate axes, conventionally denoted the X axis and Y axis, are perpindicular to each other. For the
+ /// three-dimensional rectangular coordinate system, the third axis is called the Z axis.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Point2}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Point2 : IEquatable<Point2>, IEquatableByRef<Point2>
+ {
+ /// <summary>
+ /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Point2 Zero = new Point2();
+
+ /// <summary>
+ /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> set to not a number.
+ /// </summary>
+ public static readonly Point2 NaN = new Point2(float.NaN, float.NaN);
+
+ /// <summary>
+ /// The x-coordinate of this <see cref="Point2" />.
+ /// </summary>
+ public float X;
+
+ /// <summary>
+ /// The y-coordinate of this <see cref="Point2" />.
+ /// </summary>
+ public float Y;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Point2" /> structure from the specified coordinates.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ public Point2(float x, float y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point2" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> and <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> and <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Point2 first, Point2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Point2 point)
+ {
+ return Equals(ref point);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Point2 point)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (point.X == X) && (point.Y == Y);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Point2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Point2)
+ return Equals((Point2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point2" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> or <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> or <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Point2 first, Point2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 operator +(Point2 point, Vector2 vector)
+ {
+ return Add(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 Add(Point2 point, Vector2 vector)
+ {
+ Point2 p;
+ p.X = point.X + vector.X;
+ p.Y = point.Y + vector.Y;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the subtraction of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 operator -(Point2 point, Vector2 vector)
+ {
+ return Subtract(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 Subtract(Point2 point, Vector2 vector)
+ {
+ Point2 p;
+ p.X = point.X - vector.X;
+ p.Y = point.Y - vector.Y;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </returns>
+ public static Vector2 operator -(Point2 point1, Point2 point2)
+ {
+ return Displacement(point1, point2);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </returns>
+ public static Vector2 Displacement(Point2 point2, Point2 point1)
+ {
+ Vector2 vector;
+ vector.X = point2.X - point1.X;
+ vector.Y = point2.Y - point1.Y;
+ return vector;
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 operator +(Point2 point, Size2 size)
+ {
+ return Add(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 Add(Point2 point, Size2 size)
+ {
+ return new Point2(point.X + size.Width, point.Y + size.Height);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 operator -(Point2 point, Size2 size)
+ {
+ return Subtract(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' /> .
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 Subtract(Point2 point, Size2 size)
+ {
+ return new Point2(point.X - size.Width, point.Y - size.Height);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Point2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (X.GetHashCode()*397) ^ Y.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// </returns>
+ public static Point2 Minimum(Point2 first, Point2 second)
+ {
+ return new Point2(first.X < second.X ? first.X : second.X,
+ first.Y < second.Y ? first.Y : second.Y);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// </returns>
+ public static Point2 Maximum(Point2 first, Point2 second)
+ {
+ return new Point2(first.X > second.X ? first.X : second.X,
+ first.Y > second.Y ? first.Y : second.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a position <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector2" />.
+ /// </returns>
+ public static implicit operator Vector2(Point2 point)
+ {
+ return new Vector2(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector2" /> to a position <see cref="Point2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Vector2 vector)
+ {
+ return new Point2(vector.X, vector.Y);
+ }
+
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Point point)
+ {
+ return new Point2(point.X, point.Y);
+ }
+
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Point2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Point2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"({X}, {Y})";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs
new file mode 100644
index 0000000..cb7b249
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A three-dimensional point defined by a 3-tuple of real numbers, (x, y, z).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A point is a position in three-dimensional space, the location of which is described in terms of a
+ /// three-dimensional coordinate system, given by a reference point, called the origin, and three coordinate axes.
+ /// </para>
+ /// <para>
+ /// A common three-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the
+ /// coordinate axes, conventionally denoted the X axis, Y axis and Z axis, are perpindicular to each other.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Point3}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Point3 : IEquatable<Point3>, IEquatableByRef<Point3>
+ {
+ /// <summary>
+ /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Point3 Zero = new Point3();
+
+ /// <summary>
+ /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> set to not a number.
+ /// </summary>
+ public static readonly Point3 NaN = new Point3(float.NaN, float.NaN, float.NaN);
+
+ /// <summary>
+ /// The x-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float X;
+
+ /// <summary>
+ /// The y-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float Y;
+
+ /// <summary>
+ /// The z-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float Z;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Point3" /> structure from the specified coordinates.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ /// <param name="z">The z-coordinate.</param>
+ public Point3(float x, float y, float z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point3" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> <see cref="Y" /> and <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> and <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Point3 first, Point3 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Point3 point)
+ {
+ return Equals(ref point);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Point3 point)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (point.X == X) && (point.Y == Y) && (point.Z == Z);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Point3" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Point3)
+ return Equals((Point3) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point3" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> <see cref="Y" /> or <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> or <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Point3 first, Point3 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 operator +(Point3 point, Vector3 vector)
+ {
+ return Add(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 Add(Point3 point, Vector3 vector)
+ {
+ Point3 p;
+ p.X = point.X + vector.X;
+ p.Y = point.Y + vector.Y;
+ p.Z = point.Z + vector.Z;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the subtraction of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 operator -(Point3 point, Vector3 vector)
+ {
+ return Subtract(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 Subtract(Point3 point, Vector3 vector)
+ {
+ Point3 p;
+ p.X = point.X - vector.X;
+ p.Y = point.Y - vector.Y;
+ p.Z = point.Z - vector.Z;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </returns>
+ public static Vector3 operator -(Point3 point1, Point3 point2)
+ {
+ return Displacement(point1, point2);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </returns>
+ public static Vector3 Displacement(Point3 point2, Point3 point1)
+ {
+ Vector3 vector;
+ vector.X = point2.X - point1.X;
+ vector.Y = point2.Y - point1.Y;
+ vector.Z = point2.Z - point1.Z;
+ return vector;
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 operator +(Point3 point, Size3 size)
+ {
+ return Add(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 Add(Point3 point, Size3 size)
+ {
+ return new Point3(point.X + size.Width, point.Y + size.Height, point.Z + size.Depth);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 operator -(Point3 point, Size3 size)
+ {
+ return Subtract(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' /> .
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 Subtract(Point3 point, Size3 size)
+ {
+ return new Point3(point.X - size.Width, point.Y - size.Height, point.Z - size.Depth);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Point3" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point3" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + X.GetHashCode();
+ hash = hash * 23 + Y.GetHashCode();
+ hash = hash * 23 + Z.GetHashCode();
+ return hash;
+ }
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// </returns>
+ public static Point3 Minimum(Point3 first, Point3 second)
+ {
+ return new Point3(first.X < second.X ? first.X : second.X,
+ first.Y < second.Y ? first.Y : second.Y,
+ first.Z < second.Z ? first.Z : second.Z);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// </returns>
+ public static Point3 Maximum(Point3 first, Point3 second)
+ {
+ return new Point3(first.X > second.X ? first.X : second.X,
+ first.Y > second.Y ? first.Y : second.Y,
+ first.Z > second.Z ? first.Z : second.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a position <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector3" />.
+ /// </returns>
+ public static implicit operator Vector3(Point3 point)
+ {
+ return new Vector3(point.X, point.Y, point.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector3" /> to a position <see cref="Point3" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Point3" />.
+ /// </returns>
+ public static implicit operator Point3(Vector3 vector)
+ {
+ return new Point3(vector.X, vector.Y, vector.Z);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Point3" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Point3" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"({X}, {Y}, {Z})";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs
new file mode 100644
index 0000000..ed08270
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ internal class PrimitivesHelper
+ {
+ // Used by Ray2 and Segment2
+ internal static bool IntersectsSlab(float positionCoordinate, float directionCoordinate, float slabMinimum,
+ float slabMaximum, ref float rayMinimumDistance, ref float rayMaximumDistance)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ if (Math.Abs(directionCoordinate) < float.Epsilon)
+ return (positionCoordinate >= slabMinimum) && (positionCoordinate <= slabMaximum);
+
+ // Compute intersection values of ray with near and far plane of slab
+ var rayNearDistance = (slabMinimum - positionCoordinate)/directionCoordinate;
+ var rayFarDistance = (slabMaximum - positionCoordinate)/directionCoordinate;
+
+ if (rayNearDistance > rayFarDistance)
+ {
+ // Swap near and far distance
+ var temp = rayFarDistance;
+ rayNearDistance = rayFarDistance;
+ rayFarDistance = temp;
+ }
+
+ // Compute the intersection of slab intersection intervals
+ rayMinimumDistance = rayNearDistance > rayMinimumDistance ? rayNearDistance : rayMinimumDistance;
+ rayMaximumDistance = rayFarDistance < rayMaximumDistance ? rayFarDistance : rayMaximumDistance;
+
+ // Exit with no collision as soon as slab intersection becomes empty
+ return rayMinimumDistance <= rayMaximumDistance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void CreateRectangleFromPoints(IReadOnlyList<Point2> points, out Point2 minimum, out Point2 maximum)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 82-84
+
+ if (points == null || points.Count == 0)
+ {
+ minimum = Point2.Zero;
+ maximum = Point2.Zero;
+ return;
+ }
+
+ minimum = maximum = points[0];
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ for (var index = points.Count - 1; index > 0; --index)
+ {
+ var point = points[index];
+ minimum = Point2.Minimum(minimum, point);
+ maximum = Point2.Maximum(maximum, point);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void TransformRectangle(ref Point2 center, ref Vector2 halfExtents, ref Matrix2 transformMatrix)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 86-87
+
+ center = transformMatrix.Transform(center);
+ var xRadius = halfExtents.X;
+ var yRadius = halfExtents.Y;
+ halfExtents.X = xRadius * Math.Abs(transformMatrix.M11) + yRadius * Math.Abs(transformMatrix.M12);
+ halfExtents.Y = xRadius * Math.Abs(transformMatrix.M21) + yRadius * Math.Abs(transformMatrix.M22);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void TransformOrientedRectangle(
+ ref Point2 center,
+ ref Matrix2 orientation,
+ ref Matrix2 transformMatrix)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Oriented Bounding Boxes (OBBs), pg 101-105.
+
+ center = transformMatrix.Transform(center);
+ orientation *= transformMatrix;
+ // Reset the translation since orientation is only about rotation
+ orientation.M31 = 0;
+ orientation.M32 = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static float SquaredDistanceToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.3.1; Basic Primitive Tests - Closest-point Computations - Distance of Point to AABB. pg 130-131
+ var squaredDistance = 0.0f;
+
+ // for each axis add up the excess distance outside the box
+
+ // x-axis
+ if (point.X < minimum.X)
+ {
+ var distance = minimum.X - point.X;
+ squaredDistance += distance * distance;
+ }
+ else if (point.X > maximum.X)
+ {
+ var distance = maximum.X - point.X;
+ squaredDistance += distance * distance;
+ }
+
+ // y-axis
+ if (point.Y < minimum.Y)
+ {
+ var distance = minimum.Y - point.Y;
+ squaredDistance += distance * distance;
+ }
+ else if (point.Y > maximum.Y)
+ {
+ var distance = maximum.Y - point.Y;
+ squaredDistance += distance * distance;
+ }
+ return squaredDistance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ClosestPointToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point, out Point2 result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest-point Computations. pg 130-131
+
+ result = point;
+
+ // For each coordinate axis, if the point coordinate value is outside box, clamp it to the box, else keep it as is
+ if (result.X < minimum.X)
+ result.X = minimum.X;
+ else if (result.X > maximum.X)
+ result.X = maximum.X;
+
+ if (result.Y < minimum.Y)
+ result.Y = minimum.Y;
+ else if (result.Y > maximum.Y)
+ result.Y = maximum.Y;
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs
new file mode 100644
index 0000000..14abb5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class RandomExtensions
+ {
+ public static int Next(this Random random, Range<int> range)
+ {
+ return random.Next(range.Min, range.Max);
+ }
+
+ public static float NextSingle(this Random random, float min, float max)
+ {
+ return (max - min)*NextSingle(random) + min;
+ }
+
+ public static float NextSingle(this Random random, float max)
+ {
+ return max*NextSingle(random);
+ }
+
+ public static float NextSingle(this Random random)
+ {
+ return (float) random.NextDouble();
+ }
+
+ public static float NextSingle(this Random random, Range<float> range)
+ {
+ return NextSingle(random, range.Min, range.Max);
+ }
+
+ public static float NextAngle(this Random random)
+ {
+ return NextSingle(random, -MathHelper.Pi, MathHelper.Pi);
+ }
+
+ public static void NextUnitVector(this Random random, out Vector2 vector)
+ {
+ var angle = NextAngle(random);
+ vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs
new file mode 100644
index 0000000..5402233
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs
@@ -0,0 +1,84 @@
+using System;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Represents a closed interval defined by a minimum and a maximum value of a give type.
+ /// </summary>
+ public struct Range<T> : IEquatable<Range<T>> where T : IComparable<T>
+ {
+ public Range(T min, T max)
+ {
+ if (min.CompareTo(max) > 0 || max.CompareTo(min) < 0)
+ throw new ArgumentException("Min has to be smaller than or equal to max.");
+
+ Min = min;
+ Max = max;
+ }
+
+ public Range(T value)
+ : this(value, value)
+ {
+ }
+
+ /// <summary>
+ /// Gets the minium value of the <see cref="Range{T}" />.
+ /// </summary>
+ public T Min { get; }
+
+ /// <summary>
+ /// Gets the maximum value of the <see cref="Range{T}" />.
+ /// </summary>
+ public T Max { get; }
+
+
+ /// <summary>
+ /// Returns wheter or not this <see cref="Range{T}" /> is degenerate.
+ /// (Min and Max are the same)
+ /// </summary>
+ public bool IsDegenerate => Min.Equals(Max);
+
+ /// <summary>
+ /// Returns wheter or not this <see cref="Range{T}" /> is proper.
+ /// (Min and Max are not the same)
+ /// </summary>
+ public bool IsProper => !Min.Equals(Max);
+
+ public bool Equals(Range<T> value) => Min.Equals(value.Min) && Max.Equals(value.Max);
+
+ public override bool Equals(object obj) => obj is Range<T> && Equals((Range<T>) obj);
+
+ public override int GetHashCode() => Min.GetHashCode() ^ Max.GetHashCode();
+
+ public static bool operator ==(Range<T> value1, Range<T> value2) => value1.Equals(value2);
+
+ public static bool operator !=(Range<T> value1, Range<T> value2) => !value1.Equals(value2);
+
+ public static implicit operator Range<T>(T value) => new Range<T>(value, value);
+
+ public override string ToString() => $"Range<{typeof(T).Name}> [{Min} {Max}]";
+
+ /// <summary>
+ /// Returns wheter or not the value falls in this <see cref="Range{T}" />.
+ /// </summary>
+ public bool IsInBetween(T value, bool minValueExclusive = false, bool maxValueExclusive = false)
+ {
+ if (minValueExclusive)
+ {
+ if (value.CompareTo(Min) <= 0)
+ return false;
+ }
+
+ if (value.CompareTo(Min) < 0)
+ return false;
+
+ if (maxValueExclusive)
+ {
+ if (value.CompareTo(Max) >= 0)
+ return false;
+ }
+
+ return value.CompareTo(Max) <= 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs
new file mode 100644
index 0000000..c8ed7e5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54
+ /// <summary>
+ /// A two dimensional ray defined by a starting <see cref="Point2" /> and a direction <see cref="Vector2" />.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Ray2}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Ray2 : IEquatable<Ray2>, IEquatableByRef<Ray2>
+ {
+ /// <summary>
+ /// The starting <see cref="Point2" /> of this <see cref="Ray2" />.
+ /// </summary>
+ public Point2 Position;
+
+ /// <summary>
+ /// The direction <see cref="Vector2" /> of this <see cref="Ray2" />.
+ /// </summary>
+ public Vector2 Direction;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Ray2" /> structure from the specified position and direction.
+ /// </summary>
+ /// <param name="position">The starting point.</param>
+ /// <param name="direction">The direction vector.</param>
+ public Ray2(Point2 position, Vector2 direction)
+ {
+ Position = position;
+ Direction = direction;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Ray2" /> intersects with a specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="rayNearDistance">
+ /// When this method returns, contains the distance along the ray to the first intersection
+ /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise,
+ /// <see cref="float.NaN" />.
+ /// This parameter is passed uninitialized.
+ /// </param>
+ /// <param name="rayFarDistance">
+ /// When this method returns, contains the distance along the ray to the second intersection
+ /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise,
+ /// <see cref="float.NaN" />.
+ /// This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> intersects with <paramref name="boundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle, out float rayNearDistance, out float rayFarDistance)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ var maximum = boundingRectangle.Center + boundingRectangle.HalfExtents;
+
+ // Set to the smallest possible value so the algorithm can find the first hit along the ray
+ var minimumDistanceAlongRay = float.MinValue;
+ // Set to the maximum possible value so the algorithm can find the last hit along the ray
+ var maximumDistanceAlongRay = float.MaxValue;
+
+ // For all relevant slabs which in this case is two.
+
+ // The first, horizontal, slab.
+ if (!PrimitivesHelper.IntersectsSlab(Position.X, Direction.X, minimum.X, maximum.X,
+ ref minimumDistanceAlongRay,
+ ref maximumDistanceAlongRay))
+ {
+ rayNearDistance = rayFarDistance = float.NaN;
+ return false;
+ }
+
+ // The second, vertical, slab.
+ if (!PrimitivesHelper.IntersectsSlab(Position.Y, Direction.Y, minimum.Y, maximum.Y,
+ ref minimumDistanceAlongRay,
+ ref maximumDistanceAlongRay))
+ {
+ rayNearDistance = rayFarDistance = float.NaN;
+ return false;
+ }
+
+ // Ray intersects the 2 slabs.
+ rayNearDistance = minimumDistanceAlongRay < 0 ? 0 : minimumDistanceAlongRay;
+ rayFarDistance = maximumDistanceAlongRay;
+ return true;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the
+ /// <see cref="Position" />
+ /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first ray.</param>
+ /// <param name="second">The second ray.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" />
+ /// fields of the two <see cref="Ray2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Ray2 first, Ray2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />.
+ /// </summary>
+ /// <param name="ray">The ray.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Ray2 ray)
+ {
+ return Equals(ref ray);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />.
+ /// </summary>
+ /// <param name="ray">The ray.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Ray2 ray)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (ray.Position == Position) && (ray.Direction == Direction);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Ray2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Ray2)
+ return Equals((Ray2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the
+ /// <see cref='Position' />
+ /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first ray.</param>
+ /// <param name="second">The second ray.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" />
+ /// fields of the two <see cref="Ray2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Ray2 first, Ray2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Ray2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Ray2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Position.GetHashCode()*397) ^ Direction.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Ray2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Ray2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Position: {Position}, Direction: {Direction}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs
new file mode 100644
index 0000000..a6b917a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs
@@ -0,0 +1,71 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class RectangleExtensions
+ {
+ /// <summary>
+ /// Gets the corners of the rectangle in a clockwise direction starting at the top left.
+ /// </summary>
+ public static Point[] GetCorners(this Rectangle rectangle)
+ {
+ var corners = new Point[4];
+ corners[0] = new Point(rectangle.Left, rectangle.Top);
+ corners[1] = new Point(rectangle.Right, rectangle.Top);
+ corners[2] = new Point(rectangle.Right, rectangle.Bottom);
+ corners[3] = new Point(rectangle.Left, rectangle.Bottom);
+ return corners;
+ }
+
+ /// <summary>
+ /// Gets the corners of the rectangle in a clockwise direction starting at the top left.
+ /// </summary>
+ public static Vector2[] GetCorners(this RectangleF rectangle)
+ {
+ var corners = new Vector2[4];
+ corners[0] = new Vector2(rectangle.Left, rectangle.Top);
+ corners[1] = new Vector2(rectangle.Right, rectangle.Top);
+ corners[2] = new Vector2(rectangle.Right, rectangle.Bottom);
+ corners[3] = new Vector2(rectangle.Left, rectangle.Bottom);
+ return corners;
+ }
+
+ public static Rectangle ToRectangle(this RectangleF rectangle)
+ {
+ return new Rectangle((int) rectangle.X, (int) rectangle.Y, (int) rectangle.Width, (int) rectangle.Height);
+ }
+
+ public static RectangleF ToRectangleF(this Rectangle rectangle)
+ {
+ return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+
+ public static Rectangle Clip(this Rectangle rectangle, Rectangle clippingRectangle)
+ {
+ var clip = clippingRectangle;
+ rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X;
+ rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y;
+ rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width;
+ rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height;
+
+ if (rectangle.Width <= 0 || rectangle.Height <= 0)
+ return Rectangle.Empty;
+
+ return rectangle;
+ }
+
+ public static RectangleF Clip(this RectangleF rectangle, RectangleF clippingRectangle)
+ {
+ var clip = clippingRectangle;
+ rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X;
+ rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y;
+ rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width;
+ rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height;
+
+ if(rectangle.Width <= 0 || rectangle.Height <= 0)
+ return RectangleF.Empty;
+
+ return rectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs
new file mode 100644
index 0000000..1a1d685
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs
@@ -0,0 +1,694 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77
+
+ /// <summary>
+ /// An axis-aligned, four sided, two dimensional box defined by a top-left position (<see cref="X" /> and
+ /// <see cref="Y" />) and a size (<see cref="Width" /> and <see cref="Height" />).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="RectangleF" /> is categorized by having its faces oriented in such a way that its
+ /// face normals are at all times parallel with the axes of the given coordinate system.
+ /// </para>
+ /// <para>
+ /// The bounding <see cref="RectangleF" /> of a rotated <see cref="RectangleF" /> will be equivalent or larger
+ /// in size than the original depending on the angle of rotation.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DataContract]
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct RectangleF : IEquatable<RectangleF>, IEquatableByRef<RectangleF>, IShapeF
+ {
+ /// <summary>
+ /// The <see cref="RectangleF" /> with <see cref="X" />, <see cref="Y" />, <see cref="Width" /> and
+ /// <see cref="Height" /> all set to <code>0.0f</code>.
+ /// </summary>
+ public static readonly RectangleF Empty = new RectangleF();
+
+ /// <summary>
+ /// The x-coordinate of the top-left corner position of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float X;
+
+ /// <summary>
+ /// The y-coordinate of the top-left corner position of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Y;
+
+ /// <summary>
+ /// The width of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Width;
+
+ /// <summary>
+ /// The height of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Height;
+
+ /// <summary>
+ /// Gets the x-coordinate of the left edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Left => X;
+
+ /// <summary>
+ /// Gets the x-coordinate of the right edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Right => X + Width;
+
+ /// <summary>
+ /// Gets the y-coordinate of the top edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Top => Y;
+
+ /// <summary>
+ /// Gets the y-coordinate of the bottom edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Bottom => Y + Height;
+
+ /// <summary>
+ /// Gets a value indicating whether this <see cref="RectangleF" /> has a <see cref="X" />, <see cref="Y" />,
+ /// <see cref="Width" />,
+ /// <see cref="Height" /> all equal to <code>0.0f</code>.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is empty; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsEmpty => Width.Equals(0) && Height.Equals(0) && X.Equals(0) && Y.Equals(0);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the the top-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 Position
+ {
+ get { return new Point2(X, Y); }
+ set
+ {
+ X = value.X;
+ Y = value.Y;
+ }
+ }
+
+ public RectangleF BoundingRectangle => this;
+
+ /// <summary>
+ /// Gets the <see cref="Size2" /> representing the extents of this <see cref="RectangleF" />.
+ /// </summary>
+ public Size2 Size
+ {
+ get { return new Size2(Width, Height); }
+ set
+ {
+ Width = value.Width;
+ Height = value.Height;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the center of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 Center => new Point2(X + Width * 0.5f, Y + Height * 0.5f);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the top-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 TopLeft => new Point2(X, Y);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the top-right of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 TopRight => new Point2(X + Width, Y);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the bottom-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 BottomLeft => new Point2(X, Y + Height);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the bottom-right of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 BottomRight => new Point2(X + Width, Y + Height);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left xy-coordinate
+ /// <see cref="float" />s, width <see cref="float" /> and height <see cref="float" />.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public RectangleF(float x, float y, float width, float height)
+ {
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left
+ /// <see cref="Point2" /> and the extents <see cref="Size2" />.
+ /// </summary>
+ /// <param name="position">The top-left point.</param>
+ /// <param name="size">The extents.</param>
+ public RectangleF(Point2 position, Size2 size)
+ {
+ X = position.X;
+ Y = position.Y;
+ Width = size.Width;
+ Height = size.Height;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting rectangle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out RectangleF result)
+ {
+ result.X = minimum.X;
+ result.Y = minimum.Y;
+ result.Width = maximum.X - minimum.X;
+ result.Height = maximum.Y - minimum.Y;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>The resulting <see cref="RectangleF" />.</returns>
+ public static RectangleF CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ RectangleF result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting rectangle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out RectangleF result)
+ {
+ Point2 minimum;
+ Point2 maximum;
+ PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum);
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>The resulting <see cref="RectangleF" />.</returns>
+ public static RectangleF CreateFrom(IReadOnlyList<Point2> points)
+ {
+ RectangleF result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from the specified <see cref="RectangleF" /> transformed by
+ /// the specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle to be transformed.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <param name="result">The resulting transformed rectangle.</param>
+ /// <returns>
+ /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the
+ /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static void Transform(ref RectangleF rectangle,
+ ref Matrix2 transformMatrix, out RectangleF result)
+ {
+ var center = rectangle.Center;
+ var halfExtents = (Vector2)rectangle.Size * 0.5f;
+
+ PrimitivesHelper.TransformRectangle(ref center, ref halfExtents, ref transformMatrix);
+
+ result.X = center.X - halfExtents.X;
+ result.Y = center.Y - halfExtents.Y;
+ result.Width = halfExtents.X * 2;
+ result.Height = halfExtents.Y * 2;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from the specified <see cref="Extended.BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <returns>
+ /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the
+ /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static RectangleF Transform(RectangleF rectangle, ref Matrix2 transformMatrix)
+ {
+ RectangleF result;
+ Transform(ref rectangle, ref transformMatrix, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <param name="result">The resulting rectangle that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.</param>
+ public static void Union(ref RectangleF first, ref RectangleF second, out RectangleF result)
+ {
+ result.X = Math.Min(first.X, second.X);
+ result.Y = Math.Min(first.Y, second.Y);
+ result.Width = Math.Max(first.Right, second.Right) - result.X;
+ result.Height = Math.Max(first.Bottom, second.Bottom) - result.Y;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// An <see cref="RectangleF" /> that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.
+ /// </returns>
+ public static RectangleF Union(RectangleF first, RectangleF second)
+ {
+ RectangleF result;
+ Union(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains both the specified <see cref="RectangleF" /> and this <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// An <see cref="RectangleF" /> that contains both the <paramref name="rectangle" /> and
+ /// this <see cref="RectangleF" />.
+ /// </returns>
+ public RectangleF Union(RectangleF rectangle)
+ {
+ RectangleF result;
+ Union(ref this, ref rectangle, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <param name="result">The resulting rectangle that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param>
+ public static void Intersection(ref RectangleF first,
+ ref RectangleF second, out RectangleF result)
+ {
+ var firstMinimum = first.TopLeft;
+ var firstMaximum = first.BottomRight;
+ var secondMinimum = second.TopLeft;
+ var secondMaximum = second.BottomRight;
+
+ var minimum = Point2.Maximum(firstMinimum, secondMinimum);
+ var maximum = Point2.Minimum(firstMaximum, secondMaximum);
+
+ if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y))
+ result = new RectangleF();
+ else
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// A <see cref="RectangleF" /> that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public static RectangleF Intersection(RectangleF first,
+ RectangleF second)
+ {
+ RectangleF result;
+ Intersection(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the specified
+ /// <see cref="RectangleF" /> and this <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// A <see cref="RectangleF" /> that is in common between both the <paramref name="rectangle" /> and
+ /// this <see cref="RectangleF"/>, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public RectangleF Intersection(RectangleF rectangle)
+ {
+ RectangleF result;
+ Intersection(ref this, ref rectangle, out result);
+ return result;
+ }
+
+ [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")]
+ public static RectangleF Intersect(RectangleF value1, RectangleF value2)
+ {
+ RectangleF rectangle;
+ Intersection(ref value1, ref value2, out rectangle);
+ return rectangle;
+ }
+
+ [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")]
+ public static void Intersect(ref RectangleF value1, ref RectangleF value2, out RectangleF result)
+ {
+ Intersection(ref value1, ref value2, out result);
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="RectangleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref RectangleF first, ref RectangleF second)
+ {
+ return first.X < second.X + second.Width && first.X + first.Width > second.X &&
+ first.Y < second.Y + second.Height && first.Y + first.Height > second.Y;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="RectangleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(RectangleF first, RectangleF second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> intersects with this
+ /// <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this
+ /// <see cref="RectangleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(RectangleF rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref RectangleF rectangle, ref Point2 point)
+ {
+ return rectangle.X <= point.X && point.X < rectangle.X + rectangle.Width && rectangle.Y <= point.Y && point.Y < rectangle.Y + rectangle.Height;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(RectangleF rectangle, Point2 point)
+ {
+ return Contains(ref rectangle, ref point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the this <see cref="RectangleF"/> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(ref this, ref point);
+ }
+
+ /// <summary>
+ /// Updates this <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ public void UpdateFromPoints(IReadOnlyList<Point2> points)
+ {
+ var rectangle = CreateFrom(points);
+ X = rectangle.X;
+ Y = rectangle.Y;
+ Width = rectangle.Width;
+ Height = rectangle.Height;
+ }
+
+ /// <summary>
+ /// Computes the squared distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ return PrimitivesHelper.SquaredDistanceToPointFromRectangle(TopLeft, BottomRight, point);
+ }
+
+ /// <summary>
+ /// Computes the distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns>
+ public float DistanceTo(Point2 point)
+ {
+ return (float)Math.Sqrt(SquaredDistanceTo(point));
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="RectangleF" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="RectangleF" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ Point2 result;
+ PrimitivesHelper.ClosestPointToPointFromRectangle(TopLeft, BottomRight, point, out result);
+ return result;
+ }
+
+ //TODO: Document this.
+ public void Inflate(float horizontalAmount, float verticalAmount)
+ {
+ X -= horizontalAmount;
+ Y -= verticalAmount;
+ Width += horizontalAmount * 2;
+ Height += verticalAmount * 2;
+ }
+
+ //TODO: Document this.
+ public void Offset(float offsetX, float offsetY)
+ {
+ X += offsetX;
+ Y += offsetY;
+ }
+
+ //TODO: Document this.
+ public void Offset(Vector2 amount)
+ {
+ X += amount.X;
+ Y += amount.Y;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(RectangleF first, RectangleF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(RectangleF first, RectangleF second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(RectangleF rectangle)
+ {
+ return Equals(ref rectangle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(ref RectangleF rectangle)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return X == rectangle.X && Y == rectangle.Y && Width == rectangle.Width && Height == rectangle.Height;
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="RectangleF" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is RectangleF && Equals((RectangleF)obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="RectangleF" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="RectangleF" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = X.GetHashCode();
+ hashCode = (hashCode * 397) ^ Y.GetHashCode();
+ hashCode = (hashCode * 397) ^ Width.GetHashCode();
+ hashCode = (hashCode * 397) ^ Height.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public static implicit operator RectangleF(Rectangle rectangle)
+ {
+ return new RectangleF
+ {
+ X = rectangle.X,
+ Y = rectangle.Y,
+ Width = rectangle.Width,
+ Height = rectangle.Height
+ };
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>A loss of precision may occur due to the truncation from <see cref="float" /> to <see cref="int" />.</para>
+ /// </remarks>
+ public static explicit operator Rectangle(RectangleF rectangle)
+ {
+ return new Rectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="RectangleF" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="RectangleF" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{{X: {X}, Y: {Y}, Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => string.Concat(X, " ", Y, " ", Width, " ", Height);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs
new file mode 100644
index 0000000..d8452ae
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs
@@ -0,0 +1,317 @@
+using System;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54
+ /// <summary>
+ /// A two dimensional line segment defined by two <see cref="Point2" /> structures, a starting point and an ending
+ /// point.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Segment2}" />
+ public struct Segment2 : IEquatable<Segment2>, IEquatableByRef<Segment2>
+ {
+ /// <summary>
+ /// The starting <see cref="Point2" /> of this <see cref="Segment2" />.
+ /// </summary>
+ public Point2 Start;
+
+ /// <summary>
+ /// The ending <see cref="Point2" /> of this <see cref="Segment2" />.
+ /// </summary>
+ public Point2 End;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Segment2" /> structure from the specified starting and ending
+ /// <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="start">The starting point.</param>
+ /// <param name="end">The ending point.</param>
+ public Segment2(Point2 start, Point2 end)
+ {
+ Start = start;
+ End = end;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Segment2" /> structure.
+ /// </summary>
+ /// <param name="x1">The starting x-coordinate.</param>
+ /// <param name="y1">The starting y-coordinate.</param>
+ /// <param name="x2">The ending x-coordinate.</param>
+ /// <param name="y2">The ending y-coordinate.</param>
+ public Segment2(float x1, float y1, float x2, float y2)
+ : this(new Point2(x1, y1), new Point2(x2, y2))
+ {
+ }
+
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest Point on Line Segment to Point. pg 127-130
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="Segment2" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ // Computes the parameterized position: d(t) = Start + t * (End – Start)
+
+ var startToEnd = End - Start;
+ var startToPoint = point - Start;
+ // Project arbitrary point onto the line segment, deferring the division
+ var t = startToEnd.Dot(startToPoint);
+ // If outside segment, clamp t (and therefore d) to the closest endpoint
+ if (t <= 0)
+ return Start;
+
+ // Always nonnegative since denom = (||vector||)^2
+ var denominator = startToEnd.Dot(startToEnd);
+ if (t >= denominator)
+ return End;
+
+ // The point projects inside the [Start, End] interval, must do deferred division now
+ t /= denominator;
+ startToEnd *= t;
+ return new Point2(Start.X + startToEnd.X, Start.Y + startToEnd.Y);
+ }
+
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2.1; Basic Primitive Tests - Distance of Point to Segment. pg 127-130
+ /// <summary>
+ /// Computes the squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ var startToEnd = End - Start;
+ var startToPoint = point - Start;
+ var endToPoint = point - End;
+ // Handle cases where the point projects outside the line segment
+ var dot = startToPoint.Dot(startToEnd);
+ var startToPointDistanceSquared = startToPoint.Dot(startToPoint);
+ if (dot <= 0.0f)
+ return startToPointDistanceSquared;
+ var startToEndDistanceSquared = startToEnd.Dot(startToEnd);
+ if (dot >= startToEndDistanceSquared)
+ endToPoint.Dot(endToPoint);
+ // Handle the case where the point projects onto the line segment
+ return startToPointDistanceSquared - dot*dot/startToEndDistanceSquared;
+ }
+
+ /// <summary>
+ /// Computes the distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns>
+ public float DistanceTo(Point2 point)
+ {
+ return (float) Math.Sqrt(SquaredDistanceTo(point));
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding box.</param>
+ /// <param name="intersectionPoint">
+ /// When this method returns, contains the <see cref="Point2" /> of intersection, if an
+ /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="rectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(RectangleF rectangle, out Point2 intersectionPoint)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimumPoint = rectangle.TopLeft;
+ var maximumPoint = rectangle.BottomRight;
+ var minimumDistance = float.MinValue;
+ var maximumDistance = float.MaxValue;
+
+ var direction = End - Start;
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ // Segment intersects the 2 slabs.
+
+ if (minimumDistance <= 0)
+ intersectionPoint = Start;
+ else
+ {
+ intersectionPoint = minimumDistance * direction;
+ intersectionPoint.X += Start.X;
+ intersectionPoint.Y += Start.Y;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding box.</param>
+ /// <param name="intersectionPoint">
+ /// When this method returns, contains the <see cref="Point2" /> of intersection, if an
+ /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="boundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle, out Point2 intersectionPoint)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimumPoint = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ var maximumPoint = boundingRectangle.Center + boundingRectangle.HalfExtents;
+ var minimumDistance = float.MinValue;
+ var maximumDistance = float.MaxValue;
+
+ var direction = End - Start;
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ // Segment intersects the 2 slabs.
+
+ if (minimumDistance <= 0)
+ intersectionPoint = Start;
+ else
+ {
+ intersectionPoint = minimumDistance*direction;
+ intersectionPoint.X += Start.X;
+ intersectionPoint.Y += Start.Y;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Segment2" /> structures. The result specifies
+ /// whether the values of the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref='Segment2' />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first segment.</param>
+ /// <param name="second">The second segment.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Segment2 first, Segment2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />.
+ /// </summary>
+ /// <param name="segment">The segment.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(Segment2 segment)
+ {
+ return Equals(ref segment);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />.
+ /// </summary>
+ /// <param name="segment">The segment.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Segment2 segment)
+ {
+ return (Start == segment.Start) && (End == segment.End);
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Segment2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Segment2)
+ return Equals((Segment2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Segment2" /> structures. The result specifies
+ /// whether the values of the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Segment2 first, Segment2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Segment2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Segment2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Start.GetHashCode()*397) ^ End.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Segment2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Segment2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{Start} -> {End}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs
new file mode 100644
index 0000000..f1f6f10
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Shapes;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Sprite batch extensions for drawing primitive shapes
+ /// </summary>
+ public static class ShapeExtensions
+ {
+ private static Texture2D _whitePixelTexture;
+
+ private static Texture2D GetTexture(SpriteBatch spriteBatch)
+ {
+ if (_whitePixelTexture == null)
+ {
+ _whitePixelTexture = new Texture2D(spriteBatch.GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
+ _whitePixelTexture.SetData(new[] { Color.White });
+ spriteBatch.Disposing += (sender, args) =>
+ {
+ _whitePixelTexture?.Dispose();
+ _whitePixelTexture = null;
+ };
+ }
+
+ return _whitePixelTexture;
+ }
+
+ /// <summary>
+ /// Draws a closed polygon from a <see cref="Polygon" /> shape
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// ///
+ /// <param name="position">Where to position the polygon</param>
+ /// <param name="polygon">The polygon to draw</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 position, Polygon polygon, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, position, polygon.Vertices, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a closed polygon from an array of points
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// ///
+ /// <param name="offset">Where to offset the points</param>
+ /// <param name="points">The points to connect with lines</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 offset, IReadOnlyList<Vector2> points, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ if (points.Count == 0)
+ return;
+
+ if (points.Count == 1)
+ {
+ DrawPoint(spriteBatch, points[0], color, (int)thickness);
+ return;
+ }
+
+ var texture = GetTexture(spriteBatch);
+
+ for (var i = 0; i < points.Count - 1; i++)
+ DrawPolygonEdge(spriteBatch, texture, points[i] + offset, points[i + 1] + offset, color, thickness, layerDepth);
+
+ DrawPolygonEdge(spriteBatch, texture, points[points.Count - 1] + offset, points[0] + offset, color, thickness, layerDepth);
+ }
+
+ private static void DrawPolygonEdge(SpriteBatch spriteBatch, Texture2D texture, Vector2 point1, Vector2 point2, Color color, float thickness, float layerDepth)
+ {
+ var length = Vector2.Distance(point1, point2);
+ var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X);
+ var scale = new Vector2(length, thickness);
+ spriteBatch.Draw(texture, point1, null, color, angle, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="rectangle">The rectangle to draw</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float layerDepth = 0)
+ {
+ FillRectangle(spriteBatch, rectangle.Position, rectangle.Size, color, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="location">Where to draw</param>
+ /// <param name="size">The size of the rectangle</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float layerDepth = 0)
+ {
+ spriteBatch.Draw(GetTexture(spriteBatch), location, null, color, 0, Vector2.Zero, size, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x">The X coord of the left side</param>
+ /// <param name="y">The Y coord of the upper side</param>
+ /// <param name="width">Width</param>
+ /// <param name="height">Height</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float layerDepth = 0)
+ {
+ FillRectangle(spriteBatch, new Vector2(x, y), new Size2(width, height), color, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a rectangle with the thickness provided
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="rectangle">The rectangle to draw</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ var texture = GetTexture(spriteBatch);
+ var topLeft = new Vector2(rectangle.X, rectangle.Y);
+ var topRight = new Vector2(rectangle.Right - thickness, rectangle.Y);
+ var bottomLeft = new Vector2(rectangle.X, rectangle.Bottom - thickness);
+ var horizontalScale = new Vector2(rectangle.Width, thickness);
+ var verticalScale = new Vector2(thickness, rectangle.Height);
+
+ spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, topRight, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, bottomLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a rectangle with the thickness provided
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="location">Where to draw</param>
+ /// <param name="size">The size of the rectangle</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawRectangle(spriteBatch, new RectangleF(location.X, location.Y, size.Width, size.Height), color, thickness, layerDepth);
+ }
+
+
+ /// <summary>
+ /// Draws a rectangle outline.
+ /// </summary>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawRectangle(spriteBatch, new RectangleF(x, y, width, height), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x1">The X coord of the first point</param>
+ /// <param name="y1">The Y coord of the first point</param>
+ /// <param name="x2">The X coord of the second point</param>
+ /// <param name="y2">The Y coord of the second point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, float x1, float y1, float x2, float y2, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawLine(spriteBatch, new Vector2(x1, y1), new Vector2(x2, y2), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="point1">The first point</param>
+ /// <param name="point2">The second point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ // calculate the distance between the two vectors
+ var distance = Vector2.Distance(point1, point2);
+
+ // calculate the angle between the two vectors
+ var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X);
+
+ DrawLine(spriteBatch, point1, distance, angle, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="point">The starting point</param>
+ /// <param name="length">The length of the line</param>
+ /// <param name="angle">The angle of this line from the starting point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point, float length, float angle, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ var origin = new Vector2(0f, 0.5f);
+ var scale = new Vector2(length, thickness);
+ spriteBatch.Draw(GetTexture(spriteBatch), point, null, color, angle, origin, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a point at the specified x, y position. The center of the point will be at the position.
+ /// </summary>
+ public static void DrawPoint(this SpriteBatch spriteBatch, float x, float y, Color color, float size = 1f, float layerDepth = 0)
+ {
+ DrawPoint(spriteBatch, new Vector2(x, y), color, size, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a point at the specified position. The center of the point will be at the position.
+ /// </summary>
+ public static void DrawPoint(this SpriteBatch spriteBatch, Vector2 position, Color color, float size = 1f, float layerDepth = 0)
+ {
+ var scale = Vector2.One * size;
+ var offset = new Vector2(0.5f) - new Vector2(size * 0.5f);
+ spriteBatch.Draw(GetTexture(spriteBatch), position + offset, null, color, 0f, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle from a <see cref="CircleF" /> shape
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="circle">The circle shape to draw</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the lines used</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, CircleF circle, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawCircle(spriteBatch, circle.Center, circle.Radius, sides, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="center">The center of the circle</param>
+ /// <param name="radius">The radius of the circle</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the lines used</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, Vector2 center, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, center, CreateCircle(radius, sides), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x">The center X of the circle</param>
+ /// <param name="y">The center Y of the circle</param>
+ /// <param name="radius">The radius of the circle</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, float x, float y, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, new Vector2(x, y), CreateCircle(radius, sides), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw an ellipse.
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="center">Center of the ellipse</param>
+ /// <param name="radius">Radius of the ellipse</param>
+ /// <param name="sides">The number of sides to generate.</param>
+ /// <param name="color">The color of the ellipse.</param>
+ /// <param name="thickness">The thickness of the line around the ellipse.</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawEllipse(this SpriteBatch spriteBatch, Vector2 center, Vector2 radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, center, CreateEllipse(radius.X, radius.Y, sides), color, thickness, layerDepth);
+ }
+
+ private static Vector2[] CreateCircle(double radius, int sides)
+ {
+ const double max = 2.0 * Math.PI;
+ var points = new Vector2[sides];
+ var step = max / sides;
+ var theta = 0.0;
+
+ for (var i = 0; i < sides; i++)
+ {
+ points[i] = new Vector2((float)(radius * Math.Cos(theta)), (float)(radius * Math.Sin(theta)));
+ theta += step;
+ }
+
+ return points;
+ }
+
+ private static Vector2[] CreateEllipse(float rx, float ry, int sides)
+ {
+ var vertices = new Vector2[sides];
+
+ var t = 0.0;
+ var dt = 2.0 * Math.PI / sides;
+ for (var i = 0; i < sides; i++, t += dt)
+ {
+ var x = (float)(rx * Math.Cos(t));
+ var y = (float)(ry * Math.Sin(t));
+ vertices[i] = new Vector2(x, y);
+ }
+ return vertices;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs
new file mode 100644
index 0000000..7478f75
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs
@@ -0,0 +1,95 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Base class for shapes.
+ /// </summary>
+ /// <remakarks>
+ /// Created to allow checking intersection between shapes of different types.
+ /// </remakarks>
+ public interface IShapeF
+ {
+ /// <summary>
+ /// Gets or sets the position of the shape.
+ /// </summary>
+ Point2 Position { get; set; }
+
+ /// <summary>
+ /// Gets escribed rectangle, which lying outside the shape
+ /// </summary>
+ RectangleF BoundingRectangle { get; }
+ }
+
+ /// <summary>
+ /// Class that implements methods for shared <see cref="IShapeF" /> methods.
+ /// </summary>
+ public static class Shape
+ {
+ /// <summary>
+ /// Check if two shapes intersect.
+ /// </summary>
+ /// <param name="a">The first shape.</param>
+ /// <param name="b">The second shape.</param>
+ /// <returns>True if the two shapes intersect.</returns>
+ public static bool Intersects(this IShapeF a, IShapeF b)
+ {
+ return a switch
+ {
+ CircleF circleA when b is CircleF circleB => circleA.Intersects(circleB),
+ CircleF circleA when b is RectangleF rectangleB => circleA.Intersects(rectangleB),
+ CircleF circleA when b is OrientedRectangle orientedRectangleB => Intersects(circleA, orientedRectangleB),
+
+ RectangleF rectangleA when b is CircleF circleB => Intersects(circleB, rectangleA),
+ RectangleF rectangleA when b is RectangleF rectangleB => rectangleA.Intersects(rectangleB),
+ RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => Intersects(rectangleA, orientedRectangleB).Intersects,
+
+ OrientedRectangle orientedRectangleA when b is CircleF circleB => Intersects(circleB, orientedRectangleA),
+ OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => Intersects(rectangleB, orientedRectangleA).Intersects,
+ OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB
+ => OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB).Intersects,
+
+ _ => throw new ArgumentOutOfRangeException(nameof(a))
+ };
+ }
+
+ /// <summary>
+ /// Checks if a circle and rectangle intersect.
+ /// </summary>
+ /// <param name="circle">Circle to check intersection with rectangle.</param>
+ /// <param name="rectangle">Rectangle to check intersection with circle.</param>
+ /// <returns>True if the circle and rectangle intersect.</returns>
+ public static bool Intersects(CircleF circle, RectangleF rectangle)
+ {
+ var closestPoint = rectangle.ClosestPointTo(circle.Center);
+ return circle.Contains(closestPoint);
+ }
+
+ /// <summary>
+ /// Checks whether a <see cref="CircleF"/> and <see cref="OrientedRectangle"/> intersects.
+ /// </summary>
+ /// <param name="circle"><see cref="CircleF"/>to use in intersection test.</param>
+ /// <param name="orientedRectangle"><see cref="OrientedRectangle"/>to use in intersection test.</param>
+ /// <returns>True if the circle and oriented bounded rectangle intersects, otherwise false.</returns>
+ public static bool Intersects(CircleF circle, OrientedRectangle orientedRectangle)
+ {
+ var rotation = Matrix2.CreateRotationZ(orientedRectangle.Orientation.Rotation);
+ var circleCenterInRectangleSpace = rotation.Transform(circle.Center - orientedRectangle.Center);
+ var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circle.Radius);
+ var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangle.Radii);
+ return circleInRectangleSpace.Intersects(boundingRectangle);
+ }
+
+ /// <summary>
+ /// Checks if a <see cref="RectangleF"/> and <see cref="OrientedRectangle"/> intersects.
+ /// </summary>
+ /// <param name="rectangleF"></param>
+ /// <param name="orientedRectangle"></param>
+ /// <returns>True if objects are intersecting, otherwise false.</returns>
+ public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(RectangleF rectangleF, OrientedRectangle orientedRectangle)
+ {
+ return OrientedRectangle.Intersects(orientedRectangle, (OrientedRectangle)rectangleF);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs
new file mode 100644
index 0000000..be2122e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs
@@ -0,0 +1,252 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A two dimensional size defined by two real numbers, a width and a height.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional
+ /// coordinate system, given by a reference point and two coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size}" />
+ public struct Size : IEquatable<Size>, IEquatableByRef<Size>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size Empty = new Size();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size" />.
+ /// </summary>
+ public int Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size" />.
+ /// </summary>
+ public int Height;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size" /> is empty.
+ /// </summary>
+ public bool IsEmpty => Width == 0 && Height == 0;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public Size(int width, int height)
+ {
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size first, Size second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size size)
+ {
+ return Width == size.Width && Height == size.Height;
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size)
+ return Equals((Size) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size first, Size second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if
+ /// they
+ /// were <see cref="Vector2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if they
+ /// were <see cref="Vector2" /> structures.
+ /// </returns>
+ public static Size operator +(Size first, Size second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size Add(Size first, Size second)
+ {
+ Size size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size operator -(Size first, Size second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size operator /(Size size, int value)
+ {
+ return new Size(size.Width / value, size.Height / value);
+ }
+
+ public static Size operator *(Size size, int value)
+ {
+ return new Size(size.Width * value, size.Height * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size Subtract(Size first, Size second)
+ {
+ Size size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ // ReSharper disable NonReadonlyMemberInGetHashCode
+ return (Width.GetHashCode()*397) ^ Height.GetHashCode();
+ // ReSharper restore NonReadonlyMemberInGetHashCode
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size" />.
+ /// </returns>
+ public static implicit operator Size(Point point)
+ {
+ return new Size(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point" />.
+ /// </returns>
+ public static implicit operator Point(Size size)
+ {
+ return new Point(size.Width, size.Height);
+ }
+
+ public static explicit operator Size(Size2 size)
+ {
+ return new Size((int) size.Width, (int) size.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs
new file mode 100644
index 0000000..19b22a2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs
@@ -0,0 +1,311 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A two dimensional size defined by two real numbers, a width and a height.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional
+ /// coordinate system, given by a reference point and two coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size2}" />
+ public struct Size2 : IEquatable<Size2>, IEquatableByRef<Size2>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size2" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size2 Empty = new Size2();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size2" />.
+ /// </summary>
+ public float Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size2" />.
+ /// </summary>
+ public float Height;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size2" /> is empty.
+ /// </summary>
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ public bool IsEmpty => (Width == 0) && (Height == 0);
+
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size2" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public Size2(float width, float height)
+ {
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size2" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point2" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point2" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size2 first, Size2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size2 size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size2 size)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (Width == size.Width) && (Height == size.Height);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size2)
+ return Equals((Size2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size2" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size2" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size2" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size2 first, Size2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if
+ /// they
+ /// were <see cref="Vector2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if they
+ /// were <see cref="Vector2" /> structures.
+ /// </returns>
+ public static Size2 operator +(Size2 first, Size2 second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 Add(Size2 first, Size2 second)
+ {
+ Size2 size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 operator -(Size2 first, Size2 second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size2 operator /(Size2 size, float value)
+ {
+ return new Size2(size.Width / value, size.Height / value);
+ }
+
+ public static Size2 operator *(Size2 size, float value)
+ {
+ return new Size2(size.Width * value, size.Height * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 Subtract(Size2 first, Size2 second)
+ {
+ Size2 size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Width.GetHashCode()*397) ^ Height.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Point2 point)
+ {
+ return new Size2(point.X, point.Y);
+ }
+
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Point point)
+ {
+ return new Size2(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Size2 size)
+ {
+ return new Point2(size.Width, size.Height);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Size2" /> to a <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector2" />.
+ /// </returns>
+ public static implicit operator Vector2(Size2 size)
+ {
+ return new Vector2(size.Width, size.Height);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Vector2 vector)
+ {
+ return new Size2(vector.X, vector.Y);
+ }
+
+ ///// <summary>
+ ///// Performs an implicit conversion from a <see cref="Size" /> to a <see cref="Size2" />.
+ ///// </summary>
+ ///// <param name="size">The size.</param>
+ ///// <returns>
+ ///// The resulting <see cref="Size2" />.
+ ///// </returns>
+ //public static implicit operator Size2(Size size)
+ //{
+ // return new Size2(size.Width, size.Height);
+ //}
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Size2" /> to a <see cref="Point" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static explicit operator Point(Size2 size)
+ {
+ return new Point((int)size.Width, (int)size.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs
new file mode 100644
index 0000000..9872aee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs
@@ -0,0 +1,289 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A three dimensional size defined by two real numbers, a width a height and a depth.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of three-dimensional space, the area of which is described in terms of a three-dimensional
+ /// coordinate system, given by a reference point and three coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size3}" />
+ public struct Size3 : IEquatable<Size3>, IEquatableByRef<Size3>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size3" /> with <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size3 Empty = new Size3();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size3" />.
+ /// </summary>
+ public float Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size3" />.
+ /// </summary>
+ public float Height;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size3" />.
+ /// </summary>
+ public float Depth;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size3" /> is empty.
+ /// </summary>
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ public bool IsEmpty => (Width == 0) && (Height == 0) && (Depth == 0);
+
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size3" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ /// <param name="depth">The depth.</param>
+ public Size3(float width, float height, float depth)
+ {
+ Width = width;
+ Height = height;
+ Depth = depth;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size3" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" />
+ /// fields of the two <see cref="Point3" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" />
+ /// fields of the two <see cref="Point3" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size3 first, Size3 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size3 size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size3 size)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (Width == size.Width) && (Height == size.Height) && (Depth == size.Depth);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size3" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size3)
+ return Equals((Size3) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size3" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" />
+ /// fields of the two <see cref="Size3" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" />
+ /// fields of the two <see cref="Size3" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size3 first, Size3 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if
+ /// they were <see cref="Vector3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if they
+ /// were <see cref="Vector3" /> structures.
+ /// </returns>
+ public static Size3 operator +(Size3 first, Size3 second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 Add(Size3 first, Size3 second)
+ {
+ Size3 size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ size.Depth = first.Depth + second.Depth;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 operator -(Size3 first, Size3 second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size3 operator /(Size3 size, float value)
+ {
+ return new Size3(size.Width / value, size.Height / value, size.Depth / value);
+ }
+
+ public static Size3 operator *(Size3 size, float value)
+ {
+ return new Size3(size.Width * value, size.Height * value, size.Depth * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 Subtract(Size3 first, Size3 second)
+ {
+ Size3 size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ size.Depth = first.Depth - second.Depth;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size3" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point3" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + Width.GetHashCode();
+ hash = hash * 23 + Height.GetHashCode();
+ hash = hash * 23 + Depth.GetHashCode();
+ return hash;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size3" />.
+ /// </returns>
+ public static implicit operator Size3(Point3 point)
+ {
+ return new Size3(point.X, point.Y, point.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point3" />.
+ /// </returns>
+ public static implicit operator Point3(Size3 size)
+ {
+ return new Point3(size.Width, size.Height, size.Depth);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Size3" /> to a <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector3" />.
+ /// </returns>
+ public static implicit operator Vector3(Size3 size)
+ {
+ return new Vector3(size.Width, size.Height, size.Depth);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Size3" />.
+ /// </returns>
+ public static implicit operator Size3(Vector3 vector)
+ {
+ return new Size3(vector.X, vector.Y, vector.Z);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size3" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size3" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}, Depth: {Depth}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs
new file mode 100644
index 0000000..dfecbc9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Linq;
+
+namespace MonoGame.Extended
+{
+ public struct Thickness : IEquatable<Thickness>
+ {
+ public Thickness(int all)
+ : this(all, all, all, all)
+ {
+ }
+
+ public Thickness(int leftRight, int topBottom)
+ : this(leftRight, topBottom, leftRight, topBottom)
+ {
+ }
+
+ public Thickness(int left, int top, int right, int bottom)
+ : this()
+ {
+ Left = left;
+ Top = top;
+ Right = right;
+ Bottom = bottom;
+ }
+
+ public int Left { get; set; }
+ public int Top { get; set; }
+ public int Right { get; set; }
+ public int Bottom { get; set; }
+ public int Width => Left + Right;
+ public int Height => Top + Bottom;
+ public Size Size => new Size(Width, Height);
+
+ public static implicit operator Thickness(int value)
+ {
+ return new Thickness(value);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Thickness)
+ {
+ var other = (Thickness)obj;
+ return Equals(other);
+ }
+
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Left;
+ hashCode = (hashCode * 397) ^ Top;
+ hashCode = (hashCode * 397) ^ Right;
+ hashCode = (hashCode * 397) ^ Bottom;
+ return hashCode;
+ }
+ }
+
+ public bool Equals(Thickness other)
+ {
+ return Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom;
+ }
+
+ public static Thickness FromValues(int[] values)
+ {
+ switch (values.Length)
+ {
+ case 1:
+ return new Thickness(values[0]);
+ case 2:
+ return new Thickness(values[0], values[1]);
+ case 4:
+ return new Thickness(values[0], values[1], values[2], values[3]);
+ default:
+ throw new FormatException("Invalid thickness");
+ }
+ }
+
+ public static Thickness Parse(string value)
+ {
+ var values = value
+ .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(int.Parse)
+ .ToArray();
+
+ return FromValues(values);
+ }
+
+ public override string ToString()
+ {
+ if (Left == Right && Top == Bottom)
+ return Left == Top ? $"{Left}" : $"{Left} {Top}";
+
+ return $"{Left}, {Right}, {Top}, {Bottom}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs
new file mode 100644
index 0000000..551e510
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// Implements a List structure as a cyclical list where indices are wrapped.
+ /// </summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// <typeparam name="T">The Type to hold in the list.</typeparam>
+ class CyclicalList<T> : List<T>
+ {
+ public new T this[int index]
+ {
+ get
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ return base[index];
+ }
+ set
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ base[index] = value;
+ }
+ }
+
+ public CyclicalList() { }
+
+ public CyclicalList(IEnumerable<T> collection)
+ : base(collection)
+ {
+ }
+
+ public new void RemoveAt(int index)
+ {
+ Remove(this[index]);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs
new file mode 100644
index 0000000..d84ee82
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// Implements a LinkedList that is both indexable as well as cyclical. Thus
+ /// indexing into the list with an out-of-bounds index will automatically cycle
+ /// around the list to find a valid node.
+ /// </summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ class IndexableCyclicalLinkedList<T> : LinkedList<T>
+ {
+ /// <summary>
+ /// Gets the LinkedListNode at a particular index.
+ /// </summary>
+ /// <param name="index">The index of the node to retrieve.</param>
+ /// <returns>The LinkedListNode found at the index given.</returns>
+ public LinkedListNode<T> this[int index]
+ {
+ get
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ //find the proper node
+ LinkedListNode<T> node = First;
+ for (int i = 0; i < index; i++)
+ node = node.Next;
+
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// Removes the node at a given index.
+ /// </summary>
+ /// <param name="index">The index of the node to remove.</param>
+ public void RemoveAt(int index)
+ {
+ Remove(this[index]);
+ }
+
+ /// <summary>
+ /// Finds the index of a given item.
+ /// </summary>
+ /// <param name="item">The item to find.</param>
+ /// <returns>The index of the item if found; -1 if the item is not found.</returns>
+ public int IndexOf(T item)
+ {
+ for (int i = 0; i < Count; i++)
+ if (this[i].Value.Equals(item))
+ return i;
+
+ return -1;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs
new file mode 100644
index 0000000..bbe0c04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs
@@ -0,0 +1,61 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct LineSegment
+ {
+ public Vertex A;
+ public Vertex B;
+
+ public LineSegment(Vertex a, Vertex b)
+ {
+ A = a;
+ B = b;
+ }
+
+ public float? IntersectsWithRay(Vector2 origin, Vector2 direction)
+ {
+ float largestDistance = MathHelper.Max(A.Position.X - origin.X, B.Position.X - origin.X) * 2f;
+ LineSegment raySegment = new LineSegment(new Vertex(origin, 0), new Vertex(origin + (direction * largestDistance), 0));
+
+ Vector2? intersection = FindIntersection(this, raySegment);
+ float? value = null;
+
+ if (intersection != null)
+ value = Vector2.Distance(origin, intersection.Value);
+
+ return value;
+ }
+
+ public static Vector2? FindIntersection(LineSegment a, LineSegment b)
+ {
+ float x1 = a.A.Position.X;
+ float y1 = a.A.Position.Y;
+ float x2 = a.B.Position.X;
+ float y2 = a.B.Position.Y;
+ float x3 = b.A.Position.X;
+ float y3 = b.A.Position.Y;
+ float x4 = b.B.Position.X;
+ float y4 = b.B.Position.Y;
+
+ float denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+
+ float uaNum = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
+ float ubNum = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
+
+ float ua = uaNum / denom;
+ float ub = ubNum / denom;
+
+ if (MathHelper.Clamp(ua, 0f, 1f) != ua || MathHelper.Clamp(ub, 0f, 1f) != ub)
+ return null;
+
+ return a.A.Position + (a.B.Position - a.A.Position) * ua;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs
new file mode 100644
index 0000000..8c1d35c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// A basic triangle structure that holds the three vertices that make up a given triangle.
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct Triangle
+ {
+ public readonly Vertex A;
+ public readonly Vertex B;
+ public readonly Vertex C;
+
+ public Triangle(Vertex a, Vertex b, Vertex c)
+ {
+ A = a;
+ B = b;
+ C = c;
+ }
+
+ public bool ContainsPoint(Vertex point)
+ {
+ //return true if the point to test is one of the vertices
+ if (point.Equals(A) || point.Equals(B) || point.Equals(C))
+ return true;
+
+ bool oddNodes = false;
+
+ if (checkPointToSegment(C, A, point))
+ oddNodes = !oddNodes;
+ if (checkPointToSegment(A, B, point))
+ oddNodes = !oddNodes;
+ if (checkPointToSegment(B, C, point))
+ oddNodes = !oddNodes;
+
+ return oddNodes;
+ }
+
+ public static bool ContainsPoint(Vertex a, Vertex b, Vertex c, Vertex point)
+ {
+ return new Triangle(a, b, c).ContainsPoint(point);
+ }
+
+ static bool checkPointToSegment(Vertex sA, Vertex sB, Vertex point)
+ {
+ if ((sA.Position.Y < point.Position.Y && sB.Position.Y >= point.Position.Y) ||
+ (sB.Position.Y < point.Position.Y && sA.Position.Y >= point.Position.Y))
+ {
+ float x =
+ sA.Position.X +
+ (point.Position.Y - sA.Position.Y) /
+ (sB.Position.Y - sA.Position.Y) *
+ (sB.Position.X - sA.Position.X);
+
+ if (x < point.Position.X)
+ return true;
+ }
+
+ return false;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj.GetType() != typeof(Triangle))
+ return false;
+ return Equals((Triangle)obj);
+ }
+
+ public bool Equals(Triangle obj)
+ {
+ return obj.A.Equals(A) && obj.B.Equals(B) && obj.C.Equals(C);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int result = A.GetHashCode();
+ result = (result * 397) ^ B.GetHashCode();
+ result = (result * 397) ^ C.GetHashCode();
+ return result;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs
new file mode 100644
index 0000000..5e03089
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs
@@ -0,0 +1,567 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ ///
+ /// A static class exposing methods for triangulating 2D polygons. This is the sole public
+ /// class in the entire library; all other classes/structures are intended as internal-only
+ /// objects used only to assist in triangulation.
+ ///
+ /// This class makes use of the DEBUG conditional and produces quite verbose output when built
+ /// in Debug mode. This is quite useful for debugging purposes, but can slow the process down
+ /// quite a bit. For optimal performance, build the library in Release mode.
+ ///
+ /// The triangulation is also not optimized for garbage sensitive processing. The point of the
+ /// library is a robust, yet simple, system for triangulating 2D shapes. It is intended to be
+ /// used as part of your content pipeline or at load-time. It is not something you want to be
+ /// using each and every frame unless you really don't care about garbage.
+ /// </summary>
+ public static class Triangulator
+ {
+ #region Fields
+
+ static readonly IndexableCyclicalLinkedList<Vertex> polygonVertices = new IndexableCyclicalLinkedList<Vertex>();
+ static readonly IndexableCyclicalLinkedList<Vertex> earVertices = new IndexableCyclicalLinkedList<Vertex>();
+ static readonly CyclicalList<Vertex> convexVertices = new CyclicalList<Vertex>();
+ static readonly CyclicalList<Vertex> reflexVertices = new CyclicalList<Vertex>();
+
+ #endregion
+
+ #region Public Methods
+
+ #region Triangulate
+
+ /// <summary>
+ /// Triangulates a 2D polygon produced the indexes required to render the points as a triangle list.
+ /// </summary>
+ /// <param name="inputVertices">The polygon vertices in counter-clockwise winding order.</param>
+ /// <param name="desiredWindingOrder">The desired output winding order.</param>
+ /// <param name="outputVertices">The resulting vertices that include any reversals of winding order and holes.</param>
+ /// <param name="indices">The resulting indices for rendering the shape as a triangle list.</param>
+ public static void Triangulate(
+ Vector2[] inputVertices,
+ WindingOrder desiredWindingOrder,
+ out Vector2[] outputVertices,
+ out int[] indices)
+ {
+ Log("\nBeginning triangulation...");
+
+ List<Triangle> triangles = new List<Triangle>();
+
+ //make sure we have our vertices wound properly
+ if (DetermineWindingOrder(inputVertices) == WindingOrder.Clockwise)
+ outputVertices = ReverseWindingOrder(inputVertices);
+ else
+ outputVertices = (Vector2[])inputVertices.Clone();
+
+ //clear all of the lists
+ polygonVertices.Clear();
+ earVertices.Clear();
+ convexVertices.Clear();
+ reflexVertices.Clear();
+
+ //generate the cyclical list of vertices in the polygon
+ for (int i = 0; i < outputVertices.Length; i++)
+ polygonVertices.AddLast(new Vertex(outputVertices[i], i));
+
+ //categorize all of the vertices as convex, reflex, and ear
+ FindConvexAndReflexVertices();
+ FindEarVertices();
+
+ //clip all the ear vertices
+ while (polygonVertices.Count > 3 && earVertices.Count > 0)
+ ClipNextEar(triangles);
+
+ //if there are still three points, use that for the last triangle
+ if (polygonVertices.Count == 3)
+ triangles.Add(new Triangle(
+ polygonVertices[0].Value,
+ polygonVertices[1].Value,
+ polygonVertices[2].Value));
+
+ //add all of the triangle indices to the output array
+ indices = new int[triangles.Count * 3];
+
+ //move the if statement out of the loop to prevent all the
+ //redundant comparisons
+ if (desiredWindingOrder == WindingOrder.CounterClockwise)
+ {
+ for (int i = 0; i < triangles.Count; i++)
+ {
+ indices[(i * 3)] = triangles[i].A.Index;
+ indices[(i * 3) + 1] = triangles[i].B.Index;
+ indices[(i * 3) + 2] = triangles[i].C.Index;
+ }
+ }
+ else
+ {
+ for (int i = 0; i < triangles.Count; i++)
+ {
+ indices[(i * 3)] = triangles[i].C.Index;
+ indices[(i * 3) + 1] = triangles[i].B.Index;
+ indices[(i * 3) + 2] = triangles[i].A.Index;
+ }
+ }
+ }
+
+ #endregion
+
+ #region CutHoleInShape
+
+ /// <summary>
+ /// Cuts a hole into a shape.
+ /// </summary>
+ /// <param name="shapeVerts">An array of vertices for the primary shape.</param>
+ /// <param name="holeVerts">An array of vertices for the hole to be cut. It is assumed that these vertices lie completely within the shape verts.</param>
+ /// <returns>The new array of vertices that can be passed to Triangulate to properly triangulate the shape with the hole.</returns>
+ public static Vector2[] CutHoleInShape(Vector2[] shapeVerts, Vector2[] holeVerts)
+ {
+ Log("\nCutting hole into shape...");
+
+ //make sure the shape vertices are wound counter clockwise and the hole vertices clockwise
+ shapeVerts = EnsureWindingOrder(shapeVerts, WindingOrder.CounterClockwise);
+ holeVerts = EnsureWindingOrder(holeVerts, WindingOrder.Clockwise);
+
+ //clear all of the lists
+ polygonVertices.Clear();
+ earVertices.Clear();
+ convexVertices.Clear();
+ reflexVertices.Clear();
+
+ //generate the cyclical list of vertices in the polygon
+ for (int i = 0; i < shapeVerts.Length; i++)
+ polygonVertices.AddLast(new Vertex(shapeVerts[i], i));
+
+ CyclicalList<Vertex> holePolygon = new CyclicalList<Vertex>();
+ for (int i = 0; i < holeVerts.Length; i++)
+ holePolygon.Add(new Vertex(holeVerts[i], i + polygonVertices.Count));
+
+#if DEBUG
+ StringBuilder vString = new StringBuilder();
+ foreach (Vertex v in polygonVertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Shape Vertices: {0}", vString);
+
+ vString = new StringBuilder();
+ foreach (Vertex v in holePolygon)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Hole Vertices: {0}", vString);
+#endif
+
+ FindConvexAndReflexVertices();
+ FindEarVertices();
+
+ //find the hole vertex with the largest X value
+ Vertex rightMostHoleVertex = holePolygon[0];
+ foreach (Vertex v in holePolygon)
+ if (v.Position.X > rightMostHoleVertex.Position.X)
+ rightMostHoleVertex = v;
+
+ //construct a list of all line segments where at least one vertex
+ //is to the right of the rightmost hole vertex with one vertex
+ //above the hole vertex and one below
+ List<LineSegment> segmentsToTest = new List<LineSegment>();
+ for (int i = 0; i < polygonVertices.Count; i++)
+ {
+ Vertex a = polygonVertices[i].Value;
+ Vertex b = polygonVertices[i + 1].Value;
+
+ if ((a.Position.X > rightMostHoleVertex.Position.X || b.Position.X > rightMostHoleVertex.Position.X) &&
+ ((a.Position.Y >= rightMostHoleVertex.Position.Y && b.Position.Y <= rightMostHoleVertex.Position.Y) ||
+ (a.Position.Y <= rightMostHoleVertex.Position.Y && b.Position.Y >= rightMostHoleVertex.Position.Y)))
+ segmentsToTest.Add(new LineSegment(a, b));
+ }
+
+ //now we try to find the closest intersection point heading to the right from
+ //our hole vertex.
+ float? closestPoint = null;
+ LineSegment closestSegment = new LineSegment();
+ foreach (LineSegment segment in segmentsToTest)
+ {
+ float? intersection = segment.IntersectsWithRay(rightMostHoleVertex.Position, Vector2.UnitX);
+ if (intersection != null)
+ {
+ if (closestPoint == null || closestPoint.Value > intersection.Value)
+ {
+ closestPoint = intersection;
+ closestSegment = segment;
+ }
+ }
+ }
+
+ //if closestPoint is null, there were no collisions (likely from improper input data),
+ //but we'll just return without doing anything else
+ if (closestPoint == null)
+ return shapeVerts;
+
+ //otherwise we can find our mutually visible vertex to split the polygon
+ Vector2 I = rightMostHoleVertex.Position + Vector2.UnitX * closestPoint.Value;
+ Vertex P = (closestSegment.A.Position.X > closestSegment.B.Position.X)
+ ? closestSegment.A
+ : closestSegment.B;
+
+ //construct triangle MIP
+ Triangle mip = new Triangle(rightMostHoleVertex, new Vertex(I, 1), P);
+
+ //see if any of the reflex vertices lie inside of the MIP triangle
+ List<Vertex> interiorReflexVertices = new List<Vertex>();
+ foreach (Vertex v in reflexVertices)
+ if (mip.ContainsPoint(v))
+ interiorReflexVertices.Add(v);
+
+ //if there are any interior reflex vertices, find the one that, when connected
+ //to our rightMostHoleVertex, forms the line closest to Vector2.UnitX
+ if (interiorReflexVertices.Count > 0)
+ {
+ float closestDot = -1f;
+ foreach (Vertex v in interiorReflexVertices)
+ {
+ //compute the dot product of the vector against the UnitX
+ Vector2 d = Vector2.Normalize(v.Position - rightMostHoleVertex.Position);
+ float dot = Vector2.Dot(Vector2.UnitX, d);
+
+ //if this line is the closest we've found
+ if (dot > closestDot)
+ {
+ //save the value and save the vertex as P
+ closestDot = dot;
+ P = v;
+ }
+ }
+ }
+
+ //now we just form our output array by injecting the hole vertices into place
+ //we know we have to inject the hole into the main array after point P going from
+ //rightMostHoleVertex around and then back to P.
+ int mIndex = holePolygon.IndexOf(rightMostHoleVertex);
+ int injectPoint = polygonVertices.IndexOf(P);
+
+ Log("Inserting hole at injection point {0} starting at hole vertex {1}.",
+ P,
+ rightMostHoleVertex);
+ for (int i = mIndex; i <= mIndex + holePolygon.Count; i++)
+ {
+ Log("Inserting vertex {0} after vertex {1}.", holePolygon[i], polygonVertices[injectPoint].Value);
+ polygonVertices.AddAfter(polygonVertices[injectPoint++], holePolygon[i]);
+ }
+ polygonVertices.AddAfter(polygonVertices[injectPoint], P);
+
+#if DEBUG
+ vString = new StringBuilder();
+ foreach (Vertex v in polygonVertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("New Shape Vertices: {0}\n", vString);
+#endif
+
+ //finally we write out the new polygon vertices and return them out
+ Vector2[] newShapeVerts = new Vector2[polygonVertices.Count];
+ for (int i = 0; i < polygonVertices.Count; i++)
+ newShapeVerts[i] = polygonVertices[i].Value.Position;
+
+ return newShapeVerts;
+ }
+
+ #endregion
+
+ #region EnsureWindingOrder
+
+ /// <summary>
+ /// Ensures that a set of vertices are wound in a particular order, reversing them if necessary.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <param name="windingOrder">The desired winding order.</param>
+ /// <returns>A new set of vertices if the winding order didn't match; otherwise the original set.</returns>
+ public static Vector2[] EnsureWindingOrder(Vector2[] vertices, WindingOrder windingOrder)
+ {
+ Log("\nEnsuring winding order of {0}...", windingOrder);
+ if (DetermineWindingOrder(vertices) != windingOrder)
+ {
+ Log("Reversing vertices...");
+ return ReverseWindingOrder(vertices);
+ }
+
+ Log("No reversal needed.");
+ return vertices;
+ }
+
+ #endregion
+
+ #region ReverseWindingOrder
+
+ /// <summary>
+ /// Reverses the winding order for a set of vertices.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <returns>The new vertices for the polygon with the opposite winding order.</returns>
+ public static Vector2[] ReverseWindingOrder(Vector2[] vertices)
+ {
+ Log("\nReversing winding order...");
+ Vector2[] newVerts = new Vector2[vertices.Length];
+
+#if DEBUG
+ StringBuilder vString = new StringBuilder();
+ foreach (Vector2 v in vertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Original Vertices: {0}", vString);
+#endif
+
+ newVerts[0] = vertices[0];
+ for (int i = 1; i < newVerts.Length; i++)
+ newVerts[i] = vertices[vertices.Length - i];
+
+#if DEBUG
+ vString = new StringBuilder();
+ foreach (Vector2 v in newVerts)
+ vString.Append(string.Format("{0}, ", v));
+ Log("New Vertices After Reversal: {0}\n", vString);
+#endif
+
+ return newVerts;
+ }
+
+ #endregion
+
+ #region DetermineWindingOrder
+
+ /// <summary>
+ /// Determines the winding order of a polygon given a set of vertices.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <returns>The calculated winding order of the polygon.</returns>
+ public static WindingOrder DetermineWindingOrder(Vector2[] vertices)
+ {
+ int clockWiseCount = 0;
+ int counterClockWiseCount = 0;
+ Vector2 p1 = vertices[0];
+
+ for (int i = 1; i < vertices.Length; i++)
+ {
+ Vector2 p2 = vertices[i];
+ Vector2 p3 = vertices[(i + 1) % vertices.Length];
+
+ Vector2 e1 = p1 - p2;
+ Vector2 e2 = p3 - p2;
+
+ if (e1.X * e2.Y - e1.Y * e2.X >= 0)
+ clockWiseCount++;
+ else
+ counterClockWiseCount++;
+
+ p1 = p2;
+ }
+
+ return (clockWiseCount > counterClockWiseCount)
+ ? WindingOrder.Clockwise
+ : WindingOrder.CounterClockwise;
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Private Methods
+
+ #region ClipNextEar
+
+ private static void ClipNextEar(ICollection<Triangle> triangles)
+ {
+ //find the triangle
+ Vertex ear = earVertices[0].Value;
+ Vertex prev = polygonVertices[polygonVertices.IndexOf(ear) - 1].Value;
+ Vertex next = polygonVertices[polygonVertices.IndexOf(ear) + 1].Value;
+ triangles.Add(new Triangle(ear, next, prev));
+
+ //remove the ear from the shape
+ earVertices.RemoveAt(0);
+ polygonVertices.RemoveAt(polygonVertices.IndexOf(ear));
+ Log("\nRemoved Ear: {0}", ear);
+
+ //validate the neighboring vertices
+ ValidateAdjacentVertex(prev);
+ ValidateAdjacentVertex(next);
+
+ //write out the states of each of the lists
+#if DEBUG
+ StringBuilder rString = new StringBuilder();
+ foreach (Vertex v in reflexVertices)
+ rString.Append(string.Format("{0}, ", v.Index));
+ Log("Reflex Vertices: {0}", rString);
+
+ StringBuilder cString = new StringBuilder();
+ foreach (Vertex v in convexVertices)
+ cString.Append(string.Format("{0}, ", v.Index));
+ Log("Convex Vertices: {0}", cString);
+
+ StringBuilder eString = new StringBuilder();
+ foreach (Vertex v in earVertices)
+ eString.Append(string.Format("{0}, ", v.Index));
+ Log("Ear Vertices: {0}", eString);
+#endif
+ }
+
+ #endregion
+
+ #region ValidateAdjacentVertex
+
+ private static void ValidateAdjacentVertex(Vertex vertex)
+ {
+ Log("Validating: {0}...", vertex);
+
+ if (reflexVertices.Contains(vertex))
+ {
+ if (IsConvex(vertex))
+ {
+ reflexVertices.Remove(vertex);
+ convexVertices.Add(vertex);
+ Log("Vertex: {0} now convex", vertex);
+ }
+ else
+ {
+ Log("Vertex: {0} still reflex", vertex);
+ }
+ }
+
+ if (convexVertices.Contains(vertex))
+ {
+ bool wasEar = earVertices.Contains(vertex);
+ bool isEar = IsEar(vertex);
+
+ if (wasEar && !isEar)
+ {
+ earVertices.Remove(vertex);
+ Log("Vertex: {0} no longer ear", vertex);
+ }
+ else if (!wasEar && isEar)
+ {
+ earVertices.AddFirst(vertex);
+ Log("Vertex: {0} now ear", vertex);
+ }
+ else
+ {
+ Log("Vertex: {0} still ear", vertex);
+ }
+ }
+ }
+
+ #endregion
+
+ #region FindConvexAndReflexVertices
+
+ private static void FindConvexAndReflexVertices()
+ {
+ for (int i = 0; i < polygonVertices.Count; i++)
+ {
+ Vertex v = polygonVertices[i].Value;
+
+ if (IsConvex(v))
+ {
+ convexVertices.Add(v);
+ Log("Convex: {0}", v);
+ }
+ else
+ {
+ reflexVertices.Add(v);
+ Log("Reflex: {0}", v);
+ }
+ }
+ }
+
+ #endregion
+
+ #region FindEarVertices
+
+ private static void FindEarVertices()
+ {
+ for (int i = 0; i < convexVertices.Count; i++)
+ {
+ Vertex c = convexVertices[i];
+
+ if (IsEar(c))
+ {
+ earVertices.AddLast(c);
+ Log("Ear: {0}", c);
+ }
+ }
+ }
+
+ #endregion
+
+ #region IsEar
+
+ private static bool IsEar(Vertex c)
+ {
+ Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
+ Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
+
+ Log("Testing vertex {0} as ear with triangle {1}, {0}, {2}...", c, p, n);
+
+ foreach (Vertex t in reflexVertices)
+ {
+ if (t.Equals(p) || t.Equals(c) || t.Equals(n))
+ continue;
+
+ if (Triangle.ContainsPoint(p, c, n, t))
+ {
+ Log("\tTriangle contains vertex {0}...", t);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region IsConvex
+
+ private static bool IsConvex(Vertex c)
+ {
+ Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
+ Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
+
+ Vector2 d1 = Vector2.Normalize(c.Position - p.Position);
+ Vector2 d2 = Vector2.Normalize(n.Position - c.Position);
+ Vector2 n2 = new Vector2(-d2.Y, d2.X);
+
+ return (Vector2.Dot(d1, n2) <= 0f);
+ }
+
+ #endregion
+
+ #region IsReflex
+
+ private static bool IsReflex(Vertex c)
+ {
+ return !IsConvex(c);
+ }
+
+ #endregion
+
+ #region Log
+
+ [Conditional("DEBUG")]
+ private static void Log(string format, params object[] parameters)
+ {
+ //System.Console.WriteLine(format, parameters);
+ }
+
+ #endregion
+
+ #endregion
+ }
+
+ /// <summary>
+ /// Specifies a desired winding order for the shape vertices.
+ /// </summary>
+ public enum WindingOrder
+ {
+ Clockwise,
+ CounterClockwise
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs
new file mode 100644
index 0000000..2630b07
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs
@@ -0,0 +1,47 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct Vertex
+ {
+ public readonly Vector2 Position;
+ public readonly int Index;
+
+ public Vertex(Vector2 position, int index)
+ {
+ Position = position;
+ Index = index;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj.GetType() != typeof(Vertex))
+ return false;
+ return Equals((Vertex)obj);
+ }
+
+ public bool Equals(Vertex obj)
+ {
+ return obj.Position.Equals(Position) && obj.Index == Index;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Position.GetHashCode() * 397) ^ Index;
+ }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("{0} ({1})", Position, Index);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs
new file mode 100644
index 0000000..b50a366
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class Vector2Extensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 SetX(this Vector2 vector2, float x) => new Vector2(x, vector2.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 SetY(this Vector2 vector2, float y) => new Vector2(vector2.X, y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Translate(this Vector2 vector2, float x, float y) => new Vector2(vector2.X + x, vector2.Y + y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size2 ToSize(this Vector2 value) => new Size2(value.X, value.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size2 ToAbsoluteSize(this Vector2 value)
+ {
+ var x = Math.Abs(value.X);
+ var y = Math.Abs(value.Y);
+ return new Size2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value, int digits, MidpointRounding mode)
+ {
+ var x = (float)Math.Round(value.X, digits, mode);
+ var y = (float)Math.Round(value.Y, digits, mode);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value, int digits)
+ {
+ var x = (float)Math.Round(value.X, digits);
+ var y = (float)Math.Round(value.Y, digits);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value)
+ {
+ var x = (float)Math.Round(value.X);
+ var y = (float)Math.Round(value.Y);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EqualsWithTolerence(this Vector2 value, Vector2 otherValue, float tolerance = 0.00001f)
+ {
+ return Math.Abs(value.X - otherValue.X) <= tolerance && (Math.Abs(value.Y - otherValue.Y) <= tolerance);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Rotate(this Vector2 value, float radians)
+ {
+ var cos = (float) Math.Cos(radians);
+ var sin = (float) Math.Sin(radians);
+ return new Vector2(value.X*cos - value.Y*sin, value.X*sin + value.Y*cos);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 NormalizedCopy(this Vector2 value)
+ {
+ var newVector2 = new Vector2(value.X, value.Y);
+ newVector2.Normalize();
+ return newVector2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 PerpendicularClockwise(this Vector2 value) => new Vector2(value.Y, -value.X);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 PerpendicularCounterClockwise(this Vector2 value) => new Vector2(-value.Y, value.X);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Truncate(this Vector2 value, float maxLength)
+ {
+ if (value.LengthSquared() > maxLength*maxLength)
+ return value.NormalizedCopy()*maxLength;
+
+ return value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNaN(this Vector2 value) => float.IsNaN(value.X) || float.IsNaN(value.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ToAngle(this Vector2 value) => (float) Math.Atan2(value.X, -value.Y);
+
+ /// <summary>
+ /// Calculates the dot product of two vectors. If the two vectors are unit vectors, the dot product returns a floating
+ /// point value between -1 and 1 that can be used to determine some properties of the angle between two vectors. For
+ /// example, it can show whether the vectors are orthogonal, parallel, or have an acute or obtuse angle between them.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The dot product of the two vectors.</returns>
+ /// <remarks>
+ /// <para>The dot product is also known as the inner product.</para>
+ /// <para>
+ /// For any two vectors, the dot product is defined as: <c>(vector1.X * vector2.X) + (vector1.Y * vector2.Y).</c>
+ /// The result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>Length(vector1) * Length(vector2) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle between the
+ /// two vectors.
+ /// </para>
+ /// <para>
+ /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, the length of each
+ /// vector will be equal to 1. So, when <paramref name="vector1" /> and <paramref name="vector2" /> are unit
+ /// vectors, the dot product is simply equal to the cosine of the angle between the two vectors. For example, both
+ /// <c>cos</c> values in the following calcuations would be equal in value:
+ /// <c>vector1.Normalize(); vector2.Normalize(); var cos = vector1.Dot(vector2)</c>,
+ /// <c>var cos = System.Math.Cos(theta)</c>, where <c>theta</c> is angle in radians betwen the two vectors.
+ /// </para>
+ /// <para>
+ /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, without knowing the value of
+ /// <c>theta</c> or using a potentially processor-intensive trigonometric function, the value of the dot product
+ /// can tell us the
+ /// following things:
+ /// <list type="bullet">
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) &gt; 0</c>, the angle between the two vectors
+ /// is less than 90 degrees.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) &lt; 0</c>, the angle between the two vectors
+ /// is more than 90 degrees.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == 0</c>, the angle between the two vectors
+ /// is 90 degrees; that is, the vectors are othogonal.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == 1</c>, the angle between the two vectors
+ /// is 0 degrees; that is, the vectors point in the same direction and are parallel.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == -1</c>, the angle between the two vectors
+ /// is 180 degrees; that is, the vectors point in opposite directions and are parallel.
+ /// </description>
+ /// </item>
+ /// </list>
+ /// </para>
+ /// <note type="caution">
+ /// Because of floating point error, two orthogonal vectors may not return a dot product that is exactly zero. It
+ /// might be zero plus some amount of floating point error. In your code, you will want to determine what amount of
+ /// error is acceptable in your calculation, and take that into account when you do your comparisons.
+ /// </note>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float Dot(this Vector2 vector1, Vector2 vector2)
+ {
+ return vector1.X*vector2.X + vector1.Y*vector2.Y;
+ }
+
+ /// <summary>
+ /// Calculates the scalar projection of one vector onto another. The scalar projection returns the length of the
+ /// orthogonal projection of the first vector onto a straight line parallel to the second vector, with a negative value
+ /// if the projection has an opposite direction with respect to the second vector.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The scalar projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns>
+ /// <remarks>
+ /// <para>
+ /// The scalar projection is also known as the scalar resolute of the first vector in the direction of the second
+ /// vector.
+ /// </para>
+ /// <para>
+ /// For any two vectors, the scalar projection is defined as: <c>vector1.Dot(vector2) / Length(vector2)</c>. The
+ /// result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>Length(vector1) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle in radians between
+ /// <paramref name="vector1" /> and <paramref name="vector2" />.
+ /// </para>
+ /// <para>
+ /// The value of the scalar projection can tell us the following things:
+ /// <list type="bullet">
+ /// <item>
+ /// <description>
+ /// If <c>vector1.ScalarProjectOnto(vector2) &gt;= 0</c>, the angle between <paramref name="vector1" />
+ /// and <paramref name="vector2" /> is between 0 degrees (exclusive) and 90 degrees (inclusive).
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.ScalarProjectOnto(vector2) &lt; 0</c>, the angle between <paramref name="vector1" />
+ /// and <paramref name="vector2" /> is between 90 degrees (exclusive) and 180 degrees (inclusive).
+ /// </description>
+ /// </item>
+ /// </list>
+ /// </para>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ScalarProjectOnto(this Vector2 vector1, Vector2 vector2)
+ {
+ var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y;
+ var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y;
+ return dotNumerator/(float) Math.Sqrt(lengthSquaredDenominator);
+ }
+
+ /// <summary>
+ /// Calculates the vector projection of one vector onto another. The vector projection returns the orthogonal
+ /// projection of the first vector onto a straight line parallel to the second vector.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The vector projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns>
+ /// <remarks>
+ /// <para>
+ /// The vector projection is also known as the vector component or vector resolute of the first vector in the
+ /// direction of the second vector.
+ /// </para>
+ /// <para>
+ /// For any two vectors, the vector projection is defined as:
+ /// <c>( vector1.Dot(vector2) / Length(vector2)^2 ) * vector2</c>.
+ /// The
+ /// result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>( Length(vector1) * System.Math.Cos(theta) ) * vector2 / Length(vector2)</c>, where <c>theta</c> is the
+ /// angle in radians between <paramref name="vector1" /> and <paramref name="vector2" />.
+ /// </para>
+ /// <para>
+ /// This function is easier to compute than <see cref="ScalarProjectOnto" /> since it does not use a square root.
+ /// When the vector projection and the scalar projection is required, consider using this function; the scalar
+ /// projection can be obtained by taking the length of the projection vector.
+ /// </para>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 ProjectOnto(this Vector2 vector1, Vector2 vector2)
+ {
+ var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y;
+ var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y;
+ return dotNumerator/lengthSquaredDenominator*vector2;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj
new file mode 100644
index 0000000..e99bbe5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>It makes MonoGame more awesome.</Description>
+ <PackageTags>monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings
new file mode 100644
index 0000000..66c073e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=math/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs
new file mode 100644
index 0000000..ef98c99
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs
@@ -0,0 +1,209 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended
+{
+ public sealed class OrthographicCamera : Camera<Vector2>, IMovable, IRotatable
+ {
+ private readonly ViewportAdapter _viewportAdapter;
+ private float _maximumZoom = float.MaxValue;
+ private float _minimumZoom;
+ private float _zoom;
+
+ public OrthographicCamera(GraphicsDevice graphicsDevice)
+ : this(new DefaultViewportAdapter(graphicsDevice))
+ {
+ }
+
+ public OrthographicCamera(ViewportAdapter viewportAdapter)
+ {
+ _viewportAdapter = viewportAdapter;
+
+ Rotation = 0;
+ Zoom = 1;
+ Origin = new Vector2(viewportAdapter.VirtualWidth/2f, viewportAdapter.VirtualHeight/2f);
+ Position = Vector2.Zero;
+ }
+
+ public override Vector2 Position { get; set; }
+ public override float Rotation { get; set; }
+ public override Vector2 Origin { get; set; }
+ public override Vector2 Center => Position + Origin;
+
+ public override float Zoom
+ {
+ get => _zoom;
+ set
+ {
+ if ((value < MinimumZoom) || (value > MaximumZoom))
+ throw new ArgumentException("Zoom must be between MinimumZoom and MaximumZoom");
+
+ _zoom = value;
+ }
+ }
+
+ public override float MinimumZoom
+ {
+ get => _minimumZoom;
+ set
+ {
+ if (value < 0)
+ throw new ArgumentException("MinimumZoom must be greater than zero");
+
+ if (Zoom < value)
+ Zoom = MinimumZoom;
+
+ _minimumZoom = value;
+ }
+ }
+
+ public override float MaximumZoom
+ {
+ get => _maximumZoom;
+ set
+ {
+ if (value < 0)
+ throw new ArgumentException("MaximumZoom must be greater than zero");
+
+ if (Zoom > value)
+ Zoom = value;
+
+ _maximumZoom = value;
+ }
+ }
+
+ public override RectangleF BoundingRectangle
+ {
+ get
+ {
+ var frustum = GetBoundingFrustum();
+ var corners = frustum.GetCorners();
+ var topLeft = corners[0];
+ var bottomRight = corners[2];
+ var width = bottomRight.X - topLeft.X;
+ var height = bottomRight.Y - topLeft.Y;
+ return new RectangleF(topLeft.X, topLeft.Y, width, height);
+ }
+ }
+
+ public override void Move(Vector2 direction)
+ {
+ Position += Vector2.Transform(direction, Matrix.CreateRotationZ(-Rotation));
+ }
+
+ public override void Rotate(float deltaRadians)
+ {
+ Rotation += deltaRadians;
+ }
+
+ public override void ZoomIn(float deltaZoom)
+ {
+ ClampZoom(Zoom + deltaZoom);
+ }
+
+ public override void ZoomOut(float deltaZoom)
+ {
+ ClampZoom(Zoom - deltaZoom);
+ }
+
+ private void ClampZoom(float value)
+ {
+ if (value < MinimumZoom)
+ Zoom = MinimumZoom;
+ else
+ Zoom = value > MaximumZoom ? MaximumZoom : value;
+ }
+
+ public override void LookAt(Vector2 position)
+ {
+ Position = position - new Vector2(_viewportAdapter.VirtualWidth/2f, _viewportAdapter.VirtualHeight/2f);
+ }
+
+ public Vector2 WorldToScreen(float x, float y)
+ {
+ return WorldToScreen(new Vector2(x, y));
+ }
+
+ public override Vector2 WorldToScreen(Vector2 worldPosition)
+ {
+ var viewport = _viewportAdapter.Viewport;
+ return Vector2.Transform(worldPosition + new Vector2(viewport.X, viewport.Y), GetViewMatrix());
+ }
+
+ public Vector2 ScreenToWorld(float x, float y)
+ {
+ return ScreenToWorld(new Vector2(x, y));
+ }
+
+ public override Vector2 ScreenToWorld(Vector2 screenPosition)
+ {
+ var viewport = _viewportAdapter.Viewport;
+ return Vector2.Transform(screenPosition - new Vector2(viewport.X, viewport.Y),
+ Matrix.Invert(GetViewMatrix()));
+ }
+
+ public Matrix GetViewMatrix(Vector2 parallaxFactor)
+ {
+ return GetVirtualViewMatrix(parallaxFactor)*_viewportAdapter.GetScaleMatrix();
+ }
+
+ private Matrix GetVirtualViewMatrix(Vector2 parallaxFactor)
+ {
+ return
+ Matrix.CreateTranslation(new Vector3(-Position*parallaxFactor, 0.0f))*
+ Matrix.CreateTranslation(new Vector3(-Origin, 0.0f))*
+ Matrix.CreateRotationZ(Rotation)*
+ Matrix.CreateScale(Zoom, Zoom, 1)*
+ Matrix.CreateTranslation(new Vector3(Origin, 0.0f));
+ }
+
+ private Matrix GetVirtualViewMatrix()
+ {
+ return GetVirtualViewMatrix(Vector2.One);
+ }
+
+ public override Matrix GetViewMatrix()
+ {
+ return GetViewMatrix(Vector2.One);
+ }
+
+ public override Matrix GetInverseViewMatrix()
+ {
+ return Matrix.Invert(GetViewMatrix());
+ }
+
+ private Matrix GetProjectionMatrix(Matrix viewMatrix)
+ {
+ var projection = Matrix.CreateOrthographicOffCenter(0, _viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight, 0, -1, 0);
+ Matrix.Multiply(ref viewMatrix, ref projection, out projection);
+ return projection;
+ }
+
+ public override BoundingFrustum GetBoundingFrustum()
+ {
+ var viewMatrix = GetVirtualViewMatrix();
+ var projectionMatrix = GetProjectionMatrix(viewMatrix);
+ return new BoundingFrustum(projectionMatrix);
+ }
+
+ public ContainmentType Contains(Point point)
+ {
+ return Contains(point.ToVector2());
+ }
+
+ public override ContainmentType Contains(Vector2 vector2)
+ {
+ return GetBoundingFrustum().Contains(new Vector3(vector2.X, vector2.Y, 0));
+ }
+
+ public override ContainmentType Contains(Rectangle rectangle)
+ {
+ var max = new Vector3(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height, 0.5f);
+ var min = new Vector3(rectangle.X, rectangle.Y, 0.5f);
+ var boundingBox = new BoundingBox(min, max);
+ return GetBoundingFrustum().Contains(boundingBox);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs
new file mode 100644
index 0000000..7752c02
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs
@@ -0,0 +1,19 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens
+{
+ public abstract class GameScreen : Screen
+ {
+ protected GameScreen(Game game)
+ {
+ Game = game;
+ }
+
+ public Game Game { get; }
+ public ContentManager Content => Game.Content;
+ public GraphicsDevice GraphicsDevice => Game.GraphicsDevice;
+ public GameServiceContainer Services => Game.Services;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs
new file mode 100644
index 0000000..59137f4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Screens
+{
+ public abstract class Screen : IDisposable
+ {
+ public ScreenManager ScreenManager { get; internal set; }
+
+ public virtual void Dispose() { }
+ public virtual void Initialize() { }
+ public virtual void LoadContent() { }
+ public virtual void UnloadContent() { }
+ public abstract void Update(GameTime gameTime);
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs
new file mode 100644
index 0000000..ad441c6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs
@@ -0,0 +1,78 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Screens.Transitions;
+
+namespace MonoGame.Extended.Screens
+{
+ public class ScreenManager : SimpleDrawableGameComponent
+ {
+ public ScreenManager()
+ {
+ }
+
+ private Screen _activeScreen;
+ //private bool _isInitialized;
+ //private bool _isLoaded;
+ private Transition _activeTransition;
+
+ public void LoadScreen(Screen screen, Transition transition)
+ {
+ if(_activeTransition != null)
+ return;
+
+ _activeTransition = transition;
+ _activeTransition.StateChanged += (sender, args) => LoadScreen(screen);
+ _activeTransition.Completed += (sender, args) =>
+ {
+ _activeTransition.Dispose();
+ _activeTransition = null;
+ };
+ }
+
+ public void LoadScreen(Screen screen)
+ {
+ _activeScreen?.UnloadContent();
+ _activeScreen?.Dispose();
+
+ screen.ScreenManager = this;
+
+ screen.Initialize();
+
+ screen.LoadContent();
+
+ _activeScreen = screen;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _activeScreen?.Initialize();
+ //_isInitialized = true;
+ }
+
+ protected override void LoadContent()
+ {
+ base.LoadContent();
+ _activeScreen?.LoadContent();
+ //_isLoaded = true;
+ }
+
+ protected override void UnloadContent()
+ {
+ base.UnloadContent();
+ _activeScreen?.UnloadContent();
+ //_isLoaded = false;
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _activeScreen?.Update(gameTime);
+ _activeTransition?.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _activeScreen?.Draw(gameTime);
+ _activeTransition?.Draw(gameTime);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs
new file mode 100644
index 0000000..32a8cca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public class ExpandTransition : Transition
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly SpriteBatch _spriteBatch;
+
+ public ExpandTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f)
+ : base(duration)
+ {
+ Color = color;
+
+ _graphicsDevice = graphicsDevice;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public override void Dispose()
+ {
+ _spriteBatch.Dispose();
+ }
+
+ public Color Color { get; }
+
+ public override void Draw(GameTime gameTime)
+ {
+ var halfWidth = _graphicsDevice.Viewport.Width / 2f;
+ var halfHeight = _graphicsDevice.Viewport.Height / 2f;
+ var x = halfWidth * (1.0f - Value);
+ var y = halfHeight * (1.0f - Value);
+ var width = _graphicsDevice.Viewport.Width * Value;
+ var height = _graphicsDevice.Viewport.Height * Value;
+ var rectangle = new RectangleF(x, y, width, height);
+
+ _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ _spriteBatch.FillRectangle(rectangle, Color);
+ _spriteBatch.End();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs
new file mode 100644
index 0000000..49fab4a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public class FadeTransition : Transition
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly SpriteBatch _spriteBatch;
+
+ public FadeTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f)
+ : base(duration)
+ {
+ Color = color;
+
+ _graphicsDevice = graphicsDevice;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public override void Dispose()
+ {
+ _spriteBatch.Dispose();
+ }
+
+ public Color Color { get; }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ _spriteBatch.FillRectangle(0, 0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, Color * Value);
+ _spriteBatch.End();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs
new file mode 100644
index 0000000..8336a04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs
@@ -0,0 +1,58 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public enum TransitionState { In, Out }
+
+ public abstract class Transition : IDisposable
+ {
+ private readonly float _halfDuration;
+ private float _currentSeconds;
+
+ protected Transition(float duration)
+ {
+ Duration = duration;
+ _halfDuration = Duration / 2f;
+ }
+
+ public abstract void Dispose();
+
+ public TransitionState State { get; private set; } = TransitionState.Out;
+ public float Duration { get; }
+ public float Value => MathHelper.Clamp(_currentSeconds / _halfDuration, 0f, 1f);
+
+ public event EventHandler StateChanged;
+ public event EventHandler Completed;
+
+ public void Update(GameTime gameTime)
+ {
+ var elapsedSeconds = gameTime.GetElapsedSeconds();
+
+ switch (State)
+ {
+ case TransitionState.Out:
+ _currentSeconds += elapsedSeconds;
+
+ if (_currentSeconds >= _halfDuration)
+ {
+ State = TransitionState.In;
+ StateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ break;
+ case TransitionState.In:
+ _currentSeconds -= elapsedSeconds;
+
+ if (_currentSeconds <= 0.0f)
+ {
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs
new file mode 100644
index 0000000..cdbec1e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization
+{
+ public abstract class BaseTypeJsonConverter<T> : JsonConverter<T>
+ {
+ private readonly string _suffix;
+ private readonly Dictionary<string, Type> _namesToTypes;
+ private readonly Dictionary<Type, string> _typesToNames;
+ private readonly JsonSerializerOptions _serializerOptions;
+ private readonly JsonNamingPolicy _namingPolicy = JsonNamingPolicy.CamelCase;
+
+ protected BaseTypeJsonConverter(IEnumerable<TypeInfo> supportedTypes, string suffix)
+ {
+ _suffix = suffix;
+ _namesToTypes = supportedTypes
+ .ToDictionary(t => TrimSuffix(t.Name, suffix), t => t.AsType(), StringComparer.OrdinalIgnoreCase);
+ _typesToNames = _namesToTypes.ToDictionary(i => i.Value, i => i.Key);
+
+ _serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = _namingPolicy,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+ _serializerOptions.Converters.Add(this);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) =>
+ _namesToTypes.ContainsValue(typeToConvert) || typeof(T) == typeToConvert;
+
+ /// <inheritdoc />
+ /// <exception cref="InvalidOperationException" />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
+ {
+ var jObject = doc.RootElement;
+ var key = jObject.GetProperty("type").GetString();
+
+ if (_namesToTypes.TryGetValue(key, out Type type))
+ {
+ var value = JsonSerializer.Deserialize(jObject.GetRawText(), type, options);
+ return (T)value;
+ }
+
+ throw new InvalidOperationException($"Unknown {_suffix} type '{key}'");
+ }
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ var type = value.GetType();
+ var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ writer.WriteStartObject();
+ writer.WriteString("type", _typesToNames[type]);
+
+ foreach (var property in properties)
+ {
+ var propertyName = _namingPolicy.ConvertName(property.Name);
+ writer.WritePropertyName(propertyName);
+ JsonSerializer.Serialize(writer, property.GetValue(value), property.PropertyType, options);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ private static string TrimSuffix(string input, string suffix)
+ {
+ if (input.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ return input.Substring(0, input.Length - suffix.Length);
+
+ return input;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs
new file mode 100644
index 0000000..41ceabe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Color"/> value to or from JSON.
+/// </summary>
+public class ColorJsonConverter : JsonConverter<Color>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Color);
+
+ /// <inheritdoc />
+ public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ return value[0] == '#' ? ColorHelper.FromHex(value) : ColorHelper.FromName(value);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ var hexValue = ColorHelper.ToHex(value);
+ writer.WriteStringValue(hexValue);
+ }
+}
+
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs
new file mode 100644
index 0000000..6b57bfe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Loads content from a JSON file into the <see cref="ContentManager"/> using the asset name
+/// </summary>
+/// <typeparam name="T">The type of content to load</typeparam>
+public class ContentManagerJsonConverter<T> : JsonConverter<T>
+{
+ private readonly ContentManager _contentManager;
+ private readonly Func<T, string> _getAssetName;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContentManagerJsonConverter{T}"/> class.
+ /// </summary>
+ /// <param name="contentManager">The <see cref="ContentManager"/> used to load content.</param>
+ /// <param name="getAssetName">A function that returns the asset name for a given instance of <typeparamref name="T"/>.</param>
+ public ContentManagerJsonConverter(ContentManager contentManager, Func<T, string> getAssetName)
+ {
+ _contentManager = contentManager;
+ _getAssetName = getAssetName;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T);
+
+ /// <inheritdoc />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var assetName = reader.GetString();
+ return _contentManager.Load<T>(assetName);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ var asset = (T)value;
+ var assetName = _getAssetName(asset);
+ writer.WriteStringValue(assetName);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs
new file mode 100644
index 0000000..a19c589
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+public class FloatStringConverter : JsonConverter<float>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(float) || typeToConvert == typeof(string);
+
+ /// <inheritdoc />
+ public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ if (float.TryParse(reader.GetString(), out float value))
+ return value;
+ }
+ else if (reader.TokenType == JsonTokenType.Number)
+ {
+ return reader.GetSingle();
+ }
+
+ throw new JsonException($"Unable to convert value of type {reader.TokenType} to {typeof(float)}");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteNumberValue(value);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs
new file mode 100644
index 0000000..6d63a15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="HslColor"/> value to or from JSON.
+/// </summary>
+public class HslColorJsonConverter : JsonConverter<HslColor>
+{
+ private readonly ColorJsonConverter _colorConverter = new ColorJsonConverter();
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HslColor);
+
+ /// <inheritdoc />
+ public override HslColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var color = _colorConverter.Read(ref reader, typeToConvert, options);
+ return HslColor.FromRgb(color);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, HslColor value, JsonSerializerOptions options)
+ {
+ var color = ((HslColor)value).ToRgb();
+ _colorConverter.Write(writer, color, options);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs
new file mode 100644
index 0000000..69a1212
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs
@@ -0,0 +1,18 @@
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Content;
+
+
+namespace MonoGame.Extended.Serialization
+{
+ public class JsonContentLoader : IContentLoader
+ {
+ public T Load<T>(ContentManager contentManager, string path)
+ {
+
+ using var stream = contentManager.OpenStream(path);
+ var monoGameSerializerOptions = MonoGameJsonSerializerOptionsProvider.GetOptions(contentManager, path);
+ return JsonSerializer.Deserialize<T>(stream, monoGameSerializerOptions);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs
new file mode 100644
index 0000000..1d5609e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs
@@ -0,0 +1,16 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+
+
+namespace MonoGame.Extended.Serialization
+{
+ public class JsonContentTypeReader<T> : ContentTypeReader<T>
+ {
+ protected override T Read(ContentReader reader, T existingInstance)
+ {
+ var json = reader.ReadString();
+ return JsonSerializer.Deserialize<T>(json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..32a099f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs
@@ -0,0 +1,29 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+public static class MonoGameJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ContentManager contentManager, string contentPath)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ options.Converters.Add(new RangeJsonConverter<int>());
+ options.Converters.Add(new RangeJsonConverter<float>());
+ options.Converters.Add(new RangeJsonConverter<HslColor>());
+ options.Converters.Add(new ThicknessJsonConverter());
+ options.Converters.Add(new RectangleFJsonConverter());
+ options.Converters.Add(new TextureAtlasJsonConverter(contentManager, contentPath));
+ options.Converters.Add(new Size2JsonConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs
new file mode 100644
index 0000000..44bbbf7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="NinePatchRegion2D"/> value to or from JSON.
+/// </summary>
+public class NinePatchRegion2DJsonConverter : JsonConverter<NinePatchRegion2D>
+{
+ private readonly ITextureRegionService _textureRegionService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NinePatchRegion2DJsonConverter"/> class.
+ /// </summary>
+ /// <param name="textureRegionService">The texture region service used to retrieve texture regions.</param>
+ public NinePatchRegion2DJsonConverter(ITextureRegionService textureRegionService)
+ {
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(NinePatchRegion2D);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="NinePatchRegion2D"/> value
+ /// </exception>
+ public override NinePatchRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException($"Expected {nameof(JsonTokenType.StartObject)} token");
+ }
+
+ string padding = string.Empty;
+ string regionName = string.Empty;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (reader.TokenType == JsonTokenType.PropertyName)
+ {
+ var propertyName = reader.GetString();
+ reader.Read();
+
+ if (propertyName.Equals("Padding", StringComparison.Ordinal))
+ {
+ padding = reader.GetString();
+ }
+ else if (propertyName.Equals("TextureRegion", StringComparison.Ordinal))
+ {
+ regionName = reader.GetString();
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(padding) || string.IsNullOrEmpty(regionName))
+ {
+ throw new JsonException($"Missing required properties \"Padding\" and \"TextureRegion\"");
+ }
+
+ var thickness = Thickness.Parse(padding);
+ var region = _textureRegionService.GetTextureRegion(regionName);
+
+ return new NinePatchRegion2D(region, thickness.Left, thickness.Top, thickness.Right, thickness.Bottom);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, NinePatchRegion2D value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ writer.WriteStartObject();
+ writer.WriteString("TextureRegion", value.Name);
+ writer.WriteString("Padding", value.Padding.ToString());
+ writer.WriteEndObject();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs
new file mode 100644
index 0000000..ef5b5cb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Range{T}"/> value to or from JSON.
+/// </summary>
+public class RangeJsonConverter<T> : JsonConverter<Range<T>> where T : IComparable<T>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Range<T>);
+
+ /// <inheritdoc />
+ public override Range<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ Span<T> values = reader.ReadAsMultiDimensional<T>();
+
+ if (values.Length == 2)
+ {
+ if (values[0].CompareTo(values[1]) < 0)
+ {
+ return new Range<T>(values[0], values[1]);
+ }
+
+ return new Range<T>(values[1], values[0]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Range<T>(values[0], values[0]);
+ }
+
+ throw new InvalidOperationException("Invalid range");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Range<T> value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStartArray();
+ JsonSerializer.Serialize(writer, value.Min, options);
+ JsonSerializer.Serialize(writer, value.Max, options);
+ writer.WriteEndArray();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs
new file mode 100644
index 0000000..080400a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="RectangleF"/> value to or from JSON.
+/// </summary>
+public class RectangleFJsonConverter : JsonConverter<RectangleF>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(RectangleF);
+
+ /// <inheritdoc />
+ public override RectangleF Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+ return new RectangleF(values[0], values[1], values[2], values[3]);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, RectangleF value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Left} {value.Top} {value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs
new file mode 100644
index 0000000..9eeb329
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Size2"/> value to or from JSON.
+/// </summary>
+public class Size2JsonConverter : JsonConverter<Size2>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size2);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Size2"/> value
+ /// </exception>
+ public override Size2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+
+ if (values.Length == 2)
+ {
+ return new Size2(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Size2(values[0], values[0]);
+ }
+
+ throw new JsonException("Invalid Size2 property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Size2 value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs
new file mode 100644
index 0000000..3b454a1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Size"/> value to or from JSON.
+/// </summary>
+public class SizeJsonConverter : JsonConverter<Size>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Size"/> value
+ /// </exception>
+ public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<int>();
+
+ if (values.Length == 2)
+ {
+ return new Size(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Size(values[0], values[0]);
+ }
+
+ throw new JsonException("Invalid Size property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Size value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs
new file mode 100644
index 0000000..273aaed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.Serialization
+{
+ public class TextContentLoader : IContentLoader<string>
+ {
+ public string Load(ContentManager contentManager, string path)
+ {
+ using (var stream = contentManager.OpenStream(path))
+ using (var reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs
new file mode 100644
index 0000000..dbcb66c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="TextureRegion2D"/> value to or from JSON.
+/// </summary>
+public class TextureRegion2DJsonConverter : JsonConverter<TextureRegion2D>
+{
+ private readonly ITextureRegionService _textureRegionService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TextureRegion2DJsonConverter"/> class.
+ /// </summary>
+ /// <param name="textureRegionService">The texture region service to use for retrieving texture regions.</param>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown if <paramref name="textureRegionService"/> is <see langword="null"/>.
+ /// </exception>
+ public TextureRegion2DJsonConverter(ITextureRegionService textureRegionService)
+ {
+ ArgumentNullException.ThrowIfNull(textureRegionService);
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureRegion2D);
+
+ /// <inheritdoc />
+ public override TextureRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var regionName = reader.GetString();
+ return string.IsNullOrEmpty(regionName) ? null : _textureRegionService.GetTextureRegion(regionName);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ ///
+ /// -or-
+ ///
+ /// Thrown if <paramref name="value"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, TextureRegion2D value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ ArgumentNullException.ThrowIfNull(value);
+ writer.WriteStringValue(value.Name);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs
new file mode 100644
index 0000000..3914730
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization
+{
+ public interface ITextureRegionService
+ {
+ TextureRegion2D GetTextureRegion(string name);
+ }
+
+ public class TextureRegionService : ITextureRegionService
+ {
+ public TextureRegionService()
+ {
+ TextureAtlases = new List<TextureAtlas>();
+ NinePatches = new List<NinePatchRegion2D>();
+ }
+
+ public IList<TextureAtlas> TextureAtlases { get; }
+ public IList<NinePatchRegion2D> NinePatches { get; }
+
+ public TextureRegion2D GetTextureRegion(string name)
+ {
+ var ninePatch = NinePatches.FirstOrDefault(p => p.Name == name);
+
+ if (ninePatch != null)
+ return ninePatch;
+
+ return TextureAtlases
+ .Select(textureAtlas => textureAtlas.GetRegion(name))
+ .FirstOrDefault(region => region != null);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs
new file mode 100644
index 0000000..1a60573
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Thickness"/> value to or from JSON.
+/// </summary>
+public class ThicknessJsonConverter : JsonConverter<Thickness>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Thickness);
+
+ /// <inheritdoc />
+ public override Thickness Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<int>();
+ return Thickness.FromValues(values);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Thickness value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Left} {value.Top} {value.Right} {value.Bottom}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs
new file mode 100644
index 0000000..73ab532
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Provides extension methods for working with <see cref="Utf8JsonReader"/>.
+/// </summary>
+public static class Utf8JsonReaderExtensions
+{
+ private static readonly Dictionary<Type, Func<string, object>> s_stringParsers = new Dictionary<Type, Func<string, object>>
+ {
+ {typeof(int), s => int.Parse(s, CultureInfo.InvariantCulture.NumberFormat)},
+ {typeof(float), s => float.Parse(s, CultureInfo.InvariantCulture.NumberFormat)},
+ {typeof(HslColor), s => ColorExtensions.FromHex(s).ToHsl() }
+ };
+
+ /// <summary>
+ /// Reads a multi-dimensional JSON array and converts it to an array of the specified type.
+ /// </summary>
+ /// <typeparam name="T">The type of the array elements.</typeparam>
+ /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
+ /// <returns>An array of the specified type.</returns>
+ /// <exception cref="NotSupportedException">Thrown when the token type is not supported.</exception>
+ public static T[] ReadAsMultiDimensional<T>(this ref Utf8JsonReader reader)
+ {
+ var tokenType = reader.TokenType;
+
+ switch (tokenType)
+ {
+ case JsonTokenType.StartArray:
+ return reader.ReadAsJArray<T>();
+
+ case JsonTokenType.String:
+ return reader.ReadAsDelimitedString<T>();
+
+ case JsonTokenType.Number:
+ return reader.ReadAsSingleValue<T>();
+
+ default:
+ throw new NotSupportedException($"{tokenType} is not currently supported in the multi-dimensional parser");
+ }
+ }
+
+ private static T[] ReadAsSingleValue<T>(this ref Utf8JsonReader reader)
+ {
+ var token = JsonDocument.ParseValue(ref reader).RootElement;
+ var value = JsonSerializer.Deserialize<T>(token.GetRawText());
+ return new T[] { value };
+ }
+
+ private static T[] ReadAsJArray<T>(this ref Utf8JsonReader reader)
+ {
+ var items = new List<T>();
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ break;
+ }
+
+ items.Add(JsonSerializer.Deserialize<T>(ref reader));
+ }
+
+ return items.ToArray();
+ }
+
+ private static T[] ReadAsDelimitedString<T>(this ref Utf8JsonReader reader)
+ {
+ var value = reader.GetString();
+ if (string.IsNullOrEmpty(value))
+ {
+ return Array.Empty<T>();
+ }
+
+ Span<string> values = value.Split(' ');
+ var result = new T[values.Length];
+ var parser = s_stringParsers[typeof(T)];
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ result[i] = (T)parser(values[i]);
+ }
+
+ return result;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs
new file mode 100644
index 0000000..e10c72b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Vector2"/> value to or from JSON.
+/// </summary>
+public class Vector2JsonConverter : JsonConverter<Vector2>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Vector2);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Vector2"/> value
+ /// </exception>
+ public override Vector2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+
+ if (values.Length == 2)
+ {
+ return new Vector2(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Vector2(values[0]);
+ }
+
+ throw new JsonException("Invalid Size2 property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Vector2 value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.X} {value.Y}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs
new file mode 100644
index 0000000..841ce2b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Shapes
+{
+ public class Polygon : IEquatable<Polygon>
+ {
+ public Polygon(IEnumerable<Vector2> vertices)
+ {
+ _localVertices = vertices.ToArray();
+ _transformedVertices = _localVertices;
+ _offset = Vector2.Zero;
+ _rotation = 0;
+ _scale = Vector2.One;
+ _isDirty = false;
+ }
+
+ private readonly Vector2[] _localVertices;
+ private Vector2[] _transformedVertices;
+ private Vector2 _offset;
+ private float _rotation;
+ private Vector2 _scale;
+ private bool _isDirty;
+
+ public Vector2[] Vertices
+ {
+ get
+ {
+ if (_isDirty)
+ {
+ _transformedVertices = GetTransformedVertices();
+ _isDirty = false;
+ }
+
+ return _transformedVertices;
+ }
+ }
+
+ public float Left
+ {
+ get { return Vertices.Min(v => v.X); }
+ }
+
+ public float Right
+ {
+ get { return Vertices.Max(v => v.X); }
+ }
+
+ public float Top
+ {
+ get { return Vertices.Min(v => v.Y); }
+ }
+
+ public float Bottom
+ {
+ get { return Vertices.Max(v => v.Y); }
+ }
+
+ public void Offset(Vector2 amount)
+ {
+ _offset += amount;
+ _isDirty = true;
+ }
+
+ public void Rotate(float amount)
+ {
+ _rotation += amount;
+ _isDirty = true;
+ }
+
+ public void Scale(Vector2 amount)
+ {
+ _scale += amount;
+ _isDirty = true;
+ }
+
+ private Vector2[] GetTransformedVertices()
+ {
+ var newVertices = new Vector2[_localVertices.Length];
+ var isScaled = _scale != Vector2.One;
+
+ for (var i = 0; i < _localVertices.Length; i++)
+ {
+ var p = _localVertices[i];
+
+ if (isScaled)
+ p *= _scale;
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (_rotation != 0)
+ {
+ var cos = (float) Math.Cos(_rotation);
+ var sin = (float) Math.Sin(_rotation);
+ p = new Vector2(cos*p.X - sin*p.Y, sin*p.X + cos*p.Y);
+ }
+
+ newVertices[i] = p + _offset;
+ }
+
+ return newVertices;
+ }
+
+ public Polygon TransformedCopy(Vector2 offset, float rotation, Vector2 scale)
+ {
+ var polygon = new Polygon(_localVertices);
+ polygon.Offset(offset);
+ polygon.Rotate(rotation);
+ polygon.Scale(scale - Vector2.One);
+ return new Polygon(polygon.Vertices);
+ }
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+
+ public bool Contains(float x, float y)
+ {
+ var intersects = 0;
+ var vertices = Vertices;
+
+ for (var i = 0; i < vertices.Length; i++)
+ {
+ var x1 = vertices[i].X;
+ var y1 = vertices[i].Y;
+ var x2 = vertices[(i + 1)%vertices.Length].X;
+ var y2 = vertices[(i + 1)%vertices.Length].Y;
+
+ if ((((y1 <= y) && (y < y2)) || ((y2 <= y) && (y < y1))) && (x < (x2 - x1)/(y2 - y1)*(y - y1) + x1))
+ intersects++;
+ }
+
+ return (intersects & 1) == 1;
+ }
+
+ public static bool operator ==(Polygon a, Polygon b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(Polygon a, Polygon b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ return obj is Polygon && Equals((Polygon) obj);
+ }
+
+ public bool Equals(Polygon other)
+ {
+ return Vertices.SequenceEqual(other.Vertices);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return Vertices.Aggregate(27, (current, v) => current + 13*current + v.GetHashCode());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs
new file mode 100644
index 0000000..97b34ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Shapes
+{
+ public class Polyline
+ {
+ public Polyline(IEnumerable<Vector2> points)
+ {
+ Points = points;
+ }
+
+ public IEnumerable<Vector2> Points { get; private set; }
+ public float Left => Points.Min(p => p.X);
+ public float Top => Points.Min(p => p.Y);
+ public float Right => Points.Max(p => p.X);
+ public float Bottom => Points.Max(p => p.Y);
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(float x, float y)
+ {
+ return false;
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs
new file mode 100644
index 0000000..5487c64
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class SimpleDrawableGameComponent : SimpleGameComponent, IDrawable
+ {
+ protected SimpleDrawableGameComponent()
+ {
+ }
+
+ private bool _isVisible = true;
+ public bool Visible
+ {
+ get => _isVisible;
+ set
+ {
+ if (_isVisible == value)
+ return;
+
+ _isVisible = value;
+ VisibleChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ bool IDrawable.Visible => _isVisible;
+
+ private int _drawOrder;
+ public int DrawOrder
+ {
+ get => _drawOrder;
+ set
+ {
+ if (_drawOrder == value)
+ return;
+
+ _drawOrder = value;
+ DrawOrderChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public event EventHandler<EventArgs> DrawOrderChanged;
+ public event EventHandler<EventArgs> VisibleChanged;
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs
new file mode 100644
index 0000000..57cccfc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs
@@ -0,0 +1,80 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class SimpleGameComponent : IGameComponent, IUpdateable, IDisposable, IComparable<GameComponent>, IComparable<SimpleGameComponent>
+ {
+ private bool _isInitialized;
+
+ protected SimpleGameComponent()
+ {
+ }
+
+ public virtual void Dispose()
+ {
+ if (_isInitialized)
+ {
+ UnloadContent();
+ _isInitialized = false;
+ }
+ }
+
+ private bool _isEnabled = true;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled == value)
+ return;
+
+ _isEnabled = value;
+ EnabledChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ if (!_isInitialized)
+ {
+ LoadContent();
+ _isInitialized = true;
+ }
+ }
+
+ protected virtual void LoadContent() { }
+ protected virtual void UnloadContent() { }
+
+ bool IUpdateable.Enabled => _isEnabled;
+
+ private int _updateOrder;
+ public int UpdateOrder
+ {
+ get => _updateOrder;
+ set
+ {
+ if (_updateOrder == value)
+ return;
+
+ _updateOrder = value;
+ UpdateOrderChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public event EventHandler<EventArgs> EnabledChanged;
+ public event EventHandler<EventArgs> UpdateOrderChanged;
+
+ public abstract void Update(GameTime gameTime);
+
+ public int CompareTo(GameComponent other)
+ {
+ return other.UpdateOrder - UpdateOrder;
+ }
+
+ public int CompareTo(SimpleGameComponent other)
+ {
+ return other.UpdateOrder - UpdateOrder;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs
new file mode 100644
index 0000000..a6bf393
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class AnimatedSprite : Sprite
+ {
+ private readonly SpriteSheet _spriteSheet;
+ private SpriteSheetAnimation _currentAnimation;
+
+ public AnimatedSprite(SpriteSheet spriteSheet, string playAnimation = null)
+ : base(spriteSheet.TextureAtlas[0])
+ {
+ _spriteSheet = spriteSheet;
+
+ if (playAnimation != null)
+ Play(playAnimation);
+ }
+
+ public SpriteSheetAnimation Play(string name, Action onCompleted = null)
+ {
+ if (_currentAnimation == null || _currentAnimation.IsComplete || _currentAnimation.Name != name)
+ {
+ var cycle = _spriteSheet.Cycles[name];
+ var keyFrames = cycle.Frames.Select(f => _spriteSheet.TextureAtlas[f.Index]).ToArray();
+ _currentAnimation = new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong);
+
+ if(_currentAnimation != null)
+ _currentAnimation.OnCompleted = onCompleted;
+ }
+
+ return _currentAnimation;
+ }
+
+ public void Update(float deltaTime)
+ {
+ if (_currentAnimation != null && !_currentAnimation.IsComplete)
+ {
+ _currentAnimation.Update(deltaTime);
+ TextureRegion = _currentAnimation.CurrentFrame;
+ }
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ Update(gameTime.GetElapsedSeconds());
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs
new file mode 100644
index 0000000..cd6e092
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs
@@ -0,0 +1,86 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Sprites
+{
+ public abstract class Animation : IUpdate, IDisposable
+ {
+ private readonly bool _disposeOnComplete;
+ private readonly Action _onCompleteAction;
+ private bool _isComplete;
+
+ protected Animation(Action onCompleteAction, bool disposeOnComplete)
+ {
+ _onCompleteAction = onCompleteAction;
+ _disposeOnComplete = disposeOnComplete;
+ IsPaused = false;
+ }
+
+ public bool IsComplete
+ {
+ get { return _isComplete; }
+ protected set
+ {
+ if (_isComplete != value)
+ {
+ _isComplete = value;
+
+ if (_isComplete)
+ {
+ _onCompleteAction?.Invoke();
+
+ if (_disposeOnComplete)
+ Dispose();
+ }
+ }
+ }
+ }
+
+ public bool IsDisposed { get; private set; }
+ public bool IsPlaying => !IsPaused && !IsComplete;
+ public bool IsPaused { get; private set; }
+ public float CurrentTime { get; protected set; }
+
+ public virtual void Dispose()
+ {
+ IsDisposed = true;
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ Update(gameTime.GetElapsedSeconds());
+ }
+
+ public void Play()
+ {
+ IsPaused = false;
+ }
+
+ public void Pause()
+ {
+ IsPaused = true;
+ }
+
+ public void Stop()
+ {
+ Pause();
+ Rewind();
+ }
+
+ public void Rewind()
+ {
+ CurrentTime = 0;
+ }
+
+ protected abstract bool OnUpdate(float deltaTime);
+
+ public void Update(float deltaTime)
+ {
+ if (!IsPlaying)
+ return;
+
+ CurrentTime += deltaTime;
+ IsComplete = OnUpdate(deltaTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs
new file mode 100644
index 0000000..ab31417
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public interface ISpriteBatchDrawable
+ {
+ bool IsVisible { get; }
+ TextureRegion2D TextureRegion { get; }
+ Vector2 Position { get; }
+ float Rotation { get; }
+ Vector2 Scale { get; }
+ Color Color { get; }
+ Vector2 Origin { get; }
+ SpriteEffects Effect { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs
new file mode 100644
index 0000000..2174c8a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class Sprite : IColorable
+ {
+ private TextureRegion2D _textureRegion;
+
+ public Sprite(TextureRegion2D textureRegion)
+ {
+ if (textureRegion == null) throw new ArgumentNullException(nameof(textureRegion));
+
+ _textureRegion = textureRegion;
+
+ Alpha = 1.0f;
+ Color = Color.White;
+ IsVisible = true;
+ Effect = SpriteEffects.None;
+ OriginNormalized = new Vector2(0.5f, 0.5f);
+ Depth = 0.0f;
+ }
+
+ public Sprite(Texture2D texture)
+ : this(new TextureRegion2D(texture))
+ {
+ }
+
+ public float Alpha { get; set; }
+ public float Depth { get; set; }
+ public object Tag { get; set; }
+
+ public Vector2 OriginNormalized
+ {
+ get => new Vector2(Origin.X/TextureRegion.Width, Origin.Y/TextureRegion.Height);
+ set => Origin = new Vector2(value.X*TextureRegion.Width, value.Y*TextureRegion.Height);
+ }
+
+ public Color Color { get; set; }
+
+ public RectangleF GetBoundingRectangle(Transform2 transform)
+ {
+ return GetBoundingRectangle(transform.Position, transform.Rotation, transform.Scale);
+ }
+
+ public RectangleF GetBoundingRectangle(Vector2 position, float rotation, Vector2 scale)
+ {
+ var corners = GetCorners(position, rotation, scale);
+ var min = new Vector2(corners.Min(i => i.X), corners.Min(i => i.Y));
+ var max = new Vector2(corners.Max(i => i.X), corners.Max(i => i.Y));
+ return new RectangleF(min.X, min.Y, max.X - min.X, max.Y - min.Y);
+ }
+
+ public bool IsVisible { get; set; }
+ public Vector2 Origin { get; set; }
+ public SpriteEffects Effect { get; set; }
+
+ public TextureRegion2D TextureRegion
+ {
+ get => _textureRegion;
+ set
+ {
+ if (value == null)
+ throw new InvalidOperationException("TextureRegion cannot be null");
+
+ // preserve the origin if the texture size changes
+ var originNormalized = OriginNormalized;
+ _textureRegion = value;
+ OriginNormalized = originNormalized;
+ }
+ }
+
+ public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale)
+ {
+ var min = -Origin;
+ var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height);
+ var offset = position;
+
+ if (scale != Vector2.One)
+ {
+ min *= scale;
+ max = max * scale;
+ }
+
+ var corners = new Vector2[4];
+ corners[0] = min;
+ corners[1] = new Vector2(max.X, min.Y);
+ corners[2] = max;
+ corners[3] = new Vector2(min.X, max.Y);
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0)
+ {
+ var matrix = Matrix.CreateRotationZ(rotation);
+
+ for (var i = 0; i < 4; i++)
+ corners[i] = Vector2.Transform(corners[i], matrix);
+ }
+
+ for (var i = 0; i < 4; i++)
+ corners[i] += offset;
+
+ return corners;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs
new file mode 100644
index 0000000..983065b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Sprites
+{
+ public static class SpriteExtensions
+ {
+ public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale)
+ {
+ Draw(spriteBatch, sprite, position, rotation, scale);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform)
+ {
+ Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0)
+ {
+ Draw(spriteBatch, sprite, position, rotation, Vector2.One);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale)
+ {
+ if (sprite == null) throw new ArgumentNullException(nameof(sprite));
+
+ if (sprite.IsVisible)
+ {
+ var texture = sprite.TextureRegion.Texture;
+ var sourceRectangle = sprite.TextureRegion.Bounds;
+ spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color*sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs
new file mode 100644
index 0000000..6a8acb1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheet
+ {
+ public SpriteSheet()
+ {
+ Cycles = new Dictionary<string, SpriteSheetAnimationCycle>();
+ }
+
+ public TextureAtlas TextureAtlas { get; set; }
+ public Dictionary<string, SpriteSheetAnimationCycle> Cycles { get; set; }
+
+ public SpriteSheetAnimation CreateAnimation(string name)
+ {
+ var cycle = Cycles[name];
+ var keyFrames = cycle.Frames
+ .Select(f => TextureAtlas[f.Index])
+ .ToArray();
+
+ return new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs
new file mode 100644
index 0000000..3b60e5e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimation : Animation
+ {
+ public const float DefaultFrameDuration = 0.2f;
+
+ public SpriteSheetAnimation(string name, TextureAtlas textureAtlas, float frameDuration = DefaultFrameDuration,
+ bool isLooping = true, bool isReversed = false, bool isPingPong = false)
+ : this(name, textureAtlas.Regions.ToArray(), frameDuration, isLooping, isReversed, isPingPong)
+ {
+ }
+
+ public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, float frameDuration = DefaultFrameDuration,
+ bool isLooping = true, bool isReversed = false, bool isPingPong = false)
+ : base(null, false)
+ {
+ Name = name;
+ KeyFrames = keyFrames;
+ FrameDuration = frameDuration;
+ IsLooping = isLooping;
+ IsReversed = isReversed;
+ IsPingPong = isPingPong;
+ CurrentFrameIndex = IsReversed ? KeyFrames.Length - 1 : 0;
+ }
+
+ public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, SpriteSheetAnimationData data)
+ : this(name, keyFrames, data.FrameDuration, data.IsLooping, data.IsReversed, data.IsPingPong)
+ {
+ }
+
+ public string Name { get; }
+ public TextureRegion2D[] KeyFrames { get; }
+ public float FrameDuration { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+ public new bool IsComplete => CurrentTime >= AnimationDuration;
+
+ public float AnimationDuration => IsPingPong
+ ? (KeyFrames.Length*2 - (IsLooping ? 2 : 1))*FrameDuration
+ : KeyFrames.Length*FrameDuration;
+
+ public TextureRegion2D CurrentFrame => KeyFrames[CurrentFrameIndex];
+ public int CurrentFrameIndex { get; private set; }
+
+ public float FramesPerSecond
+ {
+ get => 1.0f/FrameDuration;
+ set => FrameDuration = value/1.0f;
+ }
+
+ public Action OnCompleted { get; set; }
+
+ protected override bool OnUpdate(float deltaTime)
+ {
+ if (IsComplete)
+ {
+ if (IsLooping)
+ CurrentTime %= AnimationDuration;
+ else
+ OnCompleted?.Invoke();
+ }
+
+ if (KeyFrames.Length == 1)
+ {
+ CurrentFrameIndex = 0;
+ return IsComplete;
+ }
+
+ var frameIndex = (int) (CurrentTime/FrameDuration);
+ var length = KeyFrames.Length;
+
+ if (IsPingPong)
+ {
+ if (IsComplete)
+ frameIndex = 0;
+ else
+ {
+ frameIndex = frameIndex % (length * 2 - 2);
+
+ if (frameIndex >= length)
+ frameIndex = length - 2 - (frameIndex - length);
+ }
+ }
+
+ if (IsLooping)
+ {
+ if (IsReversed)
+ {
+ frameIndex = frameIndex%length;
+ frameIndex = length - frameIndex - 1;
+ }
+ else
+ frameIndex = frameIndex%length;
+ }
+ else
+ frameIndex = IsReversed ? Math.Max(length - frameIndex - 1, 0) : Math.Min(length - 1, frameIndex);
+
+ CurrentFrameIndex = frameIndex;
+ return IsComplete;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs
new file mode 100644
index 0000000..d9e8674
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimationCycle
+ {
+ public SpriteSheetAnimationCycle()
+ {
+ Frames = new List<SpriteSheetAnimationFrame>();
+ }
+
+ public float FrameDuration { get; set; } = 0.2f;
+ public List<SpriteSheetAnimationFrame> Frames { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs
new file mode 100644
index 0000000..0bdf89d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimationData
+ {
+ public SpriteSheetAnimationData(int[] frameIndicies, float frameDuration = 0.2f, bool isLooping = true,
+ bool isReversed = false, bool isPingPong = false)
+ {
+ FrameIndicies = frameIndicies;
+ FrameDuration = frameDuration;
+ IsLooping = isLooping;
+ IsReversed = isReversed;
+ IsPingPong = isPingPong;
+ }
+
+ public int[] FrameIndicies { get; }
+ public float FrameDuration { get; }
+ public bool IsLooping { get; }
+ public bool IsReversed { get; }
+ public bool IsPingPong { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs
new file mode 100644
index 0000000..2de8e39
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+
+namespace MonoGame.Extended.Sprites
+{
+ [JsonConverter(typeof(SpriteSheetAnimationFrameJsonConverter))]
+ [DebuggerDisplay("{Index} {Duration}")]
+ public class SpriteSheetAnimationFrame
+ {
+ public SpriteSheetAnimationFrame(int index, float duration = 0.2f)
+ {
+ Index = index;
+ Duration = duration;
+ }
+
+ public int Index { get; set; }
+ public float Duration { get; set; }
+ }
+
+ public class SpriteSheetAnimationFrameJsonConverter : JsonConverter<SpriteSheetAnimationFrame>
+ {
+ /// <inheritdoc />
+ public override SpriteSheetAnimationFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Number:
+ var index = reader.GetInt32();
+ return new SpriteSheetAnimationFrame(index);
+
+ case JsonTokenType.StartObject:
+ var frame = JsonSerializer.Deserialize<SpriteSheetAnimationFrame>(ref reader, options);
+ return frame;
+
+ case JsonTokenType.Null:
+ return null;
+
+ default:
+ throw new JsonException();
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, SpriteSheetAnimationFrame value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ JsonSerializer.Serialize(writer, value, options);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs
new file mode 100644
index 0000000..49323e0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs
@@ -0,0 +1,84 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class NinePatchRegion2D : TextureRegion2D
+ {
+ public Rectangle[] SourcePatches { get; } = new Rectangle[9];
+ public Thickness Padding { get; }
+ public int LeftPadding => Padding.Left;
+ public int TopPadding => Padding.Top;
+ public int RightPadding => Padding.Right;
+ public int BottomPadding => Padding.Bottom;
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, Thickness padding)
+ : base(textureRegion.Name, textureRegion.Texture, textureRegion.X, textureRegion.Y, textureRegion.Width, textureRegion.Height)
+ {
+ Padding = padding;
+ CachePatches(textureRegion.Bounds, SourcePatches);
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int padding)
+ : this(textureRegion, padding, padding, padding, padding)
+ {
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int leftRightPadding, int topBottomPadding)
+ : this(textureRegion, leftRightPadding, topBottomPadding, leftRightPadding, topBottomPadding)
+ {
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int leftPadding, int topPadding, int rightPadding, int bottomPadding)
+ : this(textureRegion, new Thickness(leftPadding, topPadding, rightPadding, bottomPadding))
+ {
+ }
+
+ public NinePatchRegion2D(Texture2D texture, Thickness thickness)
+ : this(new TextureRegion2D(texture), thickness)
+ {
+ }
+
+ public const int TopLeft = 0;
+ public const int TopMiddle = 1;
+ public const int TopRight = 2;
+ public const int MiddleLeft = 3;
+ public const int Middle = 4;
+ public const int MiddleRight = 5;
+ public const int BottomLeft = 6;
+ public const int BottomMiddle = 7;
+ public const int BottomRight = 8;
+
+ private readonly Rectangle[] _destinationPatches = new Rectangle[9];
+
+ public Rectangle[] CreatePatches(Rectangle rectangle)
+ {
+ CachePatches(rectangle, _destinationPatches);
+ return _destinationPatches;
+ }
+
+ private void CachePatches(Rectangle sourceRectangle, Rectangle[] patchCache)
+ {
+ var x = sourceRectangle.X;
+ var y = sourceRectangle.Y;
+ var w = sourceRectangle.Width;
+ var h = sourceRectangle.Height;
+ var middleWidth = w - LeftPadding - RightPadding;
+ var middleHeight = h - TopPadding - BottomPadding;
+ var bottomY = y + h - BottomPadding;
+ var rightX = x + w - RightPadding;
+ var leftX = x + LeftPadding;
+ var topY = y + TopPadding;
+
+ patchCache[TopLeft] = new Rectangle(x, y, LeftPadding, TopPadding);
+ patchCache[TopMiddle] = new Rectangle(leftX, y, middleWidth, TopPadding);
+ patchCache[TopRight] = new Rectangle(rightX, y, RightPadding, TopPadding);
+ patchCache[MiddleLeft] = new Rectangle(x, topY, LeftPadding, middleHeight);
+ patchCache[Middle] = new Rectangle(leftX, topY, middleWidth, middleHeight);
+ patchCache[MiddleRight] = new Rectangle(rightX, topY, RightPadding, middleHeight);
+ patchCache[BottomLeft] = new Rectangle(x, bottomY, LeftPadding, BottomPadding);
+ patchCache[BottomMiddle] = new Rectangle(leftX, bottomY, middleWidth, BottomPadding);
+ patchCache[BottomRight] = new Rectangle(rightX, bottomY, RightPadding, BottomPadding);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs
new file mode 100644
index 0000000..d9fd41d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ /// <summary>
+ /// Defines a texture atlas which stores a source image and contains regions specifying its sub-images.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Texture atlas (also called a tile map, tile engine, or sprite sheet) is a large image containing a collection,
+ /// or "atlas", of sub-images, each of which is a texture map for some part of a 2D or 3D model.
+ /// The sub-textures can be rendered by modifying the texture coordinates of the object's uvmap on the atlas,
+ /// essentially telling it which part of the image its texture is in.
+ /// In an application where many small textures are used frequently, it is often more efficient to store the
+ /// textures in a texture atlas which is treated as a single unit by the graphics hardware.
+ /// This saves memory and because there are less rendering state changes by binding once, it can be faster to bind
+ /// one large texture once than to bind many smaller textures as they are drawn.
+ /// Careful alignment may be needed to avoid bleeding between sub textures when used with mipmapping, and artefacts
+ /// between tiles for texture compression.
+ /// </para>
+ /// </remarks>
+ public class TextureAtlas : IEnumerable<TextureRegion2D>
+ {
+ /// <summary>
+ /// Initializes a new texture atlas with an empty list of regions.
+ /// </summary>
+ /// <param name="name">The asset name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="Texture2D " /> image used to draw on screen.</param>
+ public TextureAtlas(string name, Texture2D texture)
+ {
+ Name = name;
+ Texture = texture;
+
+ _regionsByName = new Dictionary<string, TextureRegion2D>();
+ _regionsByIndex = new List<TextureRegion2D>();
+ }
+
+ /// <inheritdoc />
+ /// <summary>
+ /// Initializes a new texture atlas and populates it with regions.
+ /// </summary>
+ /// <param name="name">The asset name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="!:Texture2D " /> image used to draw on screen.</param>
+ /// <param name="regions">A collection of regions to populate the atlas with.</param>
+ public TextureAtlas(string name, Texture2D texture, Dictionary<string, Rectangle> regions)
+ : this(name, texture)
+ {
+ foreach (var region in regions)
+ CreateRegion(region.Key, region.Value.X, region.Value.Y, region.Value.Width, region.Value.Height);
+ }
+
+ private readonly Dictionary<string, TextureRegion2D> _regionsByName;
+ private readonly List<TextureRegion2D> _regionsByIndex;
+
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets a source <see cref="Texture2D" /> image.
+ /// </summary>
+ public Texture2D Texture { get; }
+
+ /// <summary>
+ /// Gets a list of regions in the <see cref="TextureAtlas" />.
+ /// </summary>
+ public IEnumerable<TextureRegion2D> Regions => _regionsByIndex;
+
+ /// <summary>
+ /// Gets the number of regions in the <see cref="TextureAtlas" />.
+ /// </summary>
+ public int RegionCount => _regionsByIndex.Count;
+
+ public TextureRegion2D this[string name] => GetRegion(name);
+ public TextureRegion2D this[int index] => GetRegion(index);
+
+ /// <summary>
+ /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerator" /> of regions.</returns>
+ public IEnumerator<TextureRegion2D> GetEnumerator()
+ {
+ return _regionsByIndex.GetEnumerator();
+ }
+
+ /// <summary>
+ /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerator" /> of regions</returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <summary>
+ /// Determines whether the texture atlas contains a region
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <returns></returns>
+ public bool ContainsRegion(string name)
+ {
+ return _regionsByName.ContainsKey(name);
+ }
+
+ /// <summary>
+ /// Internal method for adding region
+ /// </summary>
+ /// <param name="region">Texture region.</param>
+ private void AddRegion(TextureRegion2D region)
+ {
+ _regionsByIndex.Add(region);
+ _regionsByName.Add(region.Name, region);
+ }
+
+ /// <summary>
+ /// Creates a new texture region and adds it to the list of the <see cref="TextureAtlas" />' regions.
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <param name="x">X coordinate of the region's top left corner.</param>
+ /// <param name="y">Y coordinate of the region's top left corner.</param>
+ /// <param name="width">Width of the texture region.</param>
+ /// <param name="height">Height of the texture region.</param>
+ /// <returns>Created texture region.</returns>
+ public TextureRegion2D CreateRegion(string name, int x, int y, int width, int height)
+ {
+ if (_regionsByName.ContainsKey(name))
+ throw new InvalidOperationException($"Region {name} already exists in the texture atlas");
+
+ var region = new TextureRegion2D(name, Texture, x, y, width, height);
+ AddRegion(region);
+ return region;
+ }
+
+ /// <summary>
+ /// Creates a new nine patch texture region and adds it to the list of the <see cref="TextureAtlas" />' regions.
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <param name="x">X coordinate of the region's top left corner.</param>
+ /// <param name="y">Y coordinate of the region's top left corner.</param>
+ /// <param name="width">Width of the texture region.</param>
+ /// <param name="height">Height of the texture region.</param>
+ /// <param name="thickness">Thickness of the nine patch region.</param>
+ /// <returns>Created texture region.</returns>
+ public NinePatchRegion2D CreateNinePatchRegion(string name, int x, int y, int width, int height, Thickness thickness)
+ {
+ if (_regionsByName.ContainsKey(name))
+ throw new InvalidOperationException($"Region {name} already exists in the texture atlas");
+
+ var textureRegion = new TextureRegion2D(name, Texture, x, y, width, height);
+ var ninePatchRegion = new NinePatchRegion2D(textureRegion, thickness);
+ AddRegion(ninePatchRegion);
+ return ninePatchRegion;
+ }
+
+ /// <summary>
+ /// Removes a texture region from the <see cref="TextureAtlas" />
+ /// </summary>
+ /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to remove</param>
+ public void RemoveRegion(int index)
+ {
+ var region = _regionsByIndex[index];
+ _regionsByIndex.RemoveAt(index);
+
+ if(region.Name != null)
+ _regionsByName.Remove(region.Name);
+ }
+
+ /// <summary>
+ /// Removes a texture region from the <see cref="TextureAtlas" />
+ /// </summary>
+ /// <param name="name">Name of the <see cref="TextureRegion2D" /> to remove</param>
+ public void RemoveRegion(string name)
+ {
+ if (_regionsByName.TryGetValue(name, out var region))
+ {
+ _regionsByName.Remove(name);
+ _regionsByIndex.Remove(region);
+ }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list.
+ /// </summary>
+ /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to get.</param>
+ /// <returns>The <see cref="TextureRegion2D" />.</returns>
+ public TextureRegion2D GetRegion(int index)
+ {
+ if (index < 0 || index >= _regionsByIndex.Count)
+ throw new IndexOutOfRangeException();
+
+ return _regionsByIndex[index];
+ }
+
+ /// <summary>
+ /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list.
+ /// </summary>
+ /// <param name="name">Name of the <see cref="TextureRegion2D" /> to get.</param>
+ /// <returns>The <see cref="TextureRegion2D" />.</returns>
+ public TextureRegion2D GetRegion(string name)
+ {
+ return GetRegion<TextureRegion2D>(name);
+ }
+
+ /// <summary>
+ /// Gets a texture region from the <see cref="TextureAtlas" /> of a specified type.
+ /// This is can be useful if the atlas contains <see cref="NinePatchRegion2D"/>'s.
+ /// </summary>
+ /// <typeparam name="T">Type of the region to get</typeparam>
+ /// <param name="name">Name of the region to get</param>
+ /// <returns>The texture region</returns>
+ public T GetRegion<T>(string name) where T : TextureRegion2D
+ {
+ if (_regionsByName.TryGetValue(name, out var region))
+ return (T)region;
+
+ throw new KeyNotFoundException(name);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="TextureAtlas" /> and populates it with a grid of <see cref="TextureRegion2D" />.
+ /// </summary>
+ /// <param name="name">The name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="Texture2D" /> image used to draw on screen</param>
+ /// <param name="regionWidth">Width of the <see cref="TextureRegion2D" />.</param>
+ /// <param name="regionHeight">Height of the <see cref="TextureRegion2D" />.</param>
+ /// <param name="maxRegionCount">The number of <see cref="TextureRegion2D" /> to create.</param>
+ /// <param name="margin">Minimum distance of the regions from the border of the source <see cref="Texture2D" /> image.</param>
+ /// <param name="spacing">Horizontal and vertical space between regions.</param>
+ /// <returns>A created and populated <see cref="TextureAtlas" />.</returns>
+ public static TextureAtlas Create(string name, Texture2D texture, int regionWidth, int regionHeight,
+ int maxRegionCount = int.MaxValue, int margin = 0, int spacing = 0)
+ {
+ var textureAtlas = new TextureAtlas(name, texture);
+ var count = 0;
+ var width = texture.Width - margin;
+ var height = texture.Height - margin;
+ var xIncrement = regionWidth + spacing;
+ var yIncrement = regionHeight + spacing;
+
+ for (var y = margin; y < height; y += yIncrement)
+ {
+ for (var x = margin; x < width; x += xIncrement)
+ {
+ var regionName = $"{texture.Name ?? "region"}{count}";
+ textureAtlas.CreateRegion(regionName, x, y, regionWidth, regionHeight);
+ count++;
+
+ if (count >= maxRegionCount)
+ return textureAtlas;
+ }
+ }
+
+ return textureAtlas;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs
new file mode 100644
index 0000000..0ec6b86
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs
@@ -0,0 +1,113 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public static class TextureAtlasExtensions
+ {
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color,
+ float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ var sourceRectangle = textureRegion.Bounds;
+
+ if (clippingRectangle.HasValue)
+ {
+ var x = (int)(position.X - origin.X);
+ var y = (int)(position.Y - origin.Y);
+ var width = (int)(textureRegion.Width * scale.X);
+ var height = (int)(textureRegion.Height * scale.Y);
+ var destinationRectangle = new Rectangle(x, y, width, height);
+
+ sourceRectangle = ClipSourceRectangle(textureRegion.Bounds, destinationRectangle, clippingRectangle.Value);
+ position.X += sourceRectangle.X - textureRegion.Bounds.X;
+ position.Y += sourceRectangle.Y - textureRegion.Bounds.Y;
+
+ if(sourceRectangle.Width <= 0 || sourceRectangle.Height <= 0)
+ return;
+ }
+
+ spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ var ninePatchRegion = textureRegion as NinePatchRegion2D;
+
+ if (ninePatchRegion != null)
+ Draw(spriteBatch, ninePatchRegion, destinationRectangle, color, clippingRectangle);
+ else
+ Draw(spriteBatch, textureRegion.Texture, textureRegion.Bounds, destinationRectangle, color, clippingRectangle);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, NinePatchRegion2D ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ var destinationPatches = ninePatchRegion.CreatePatches(destinationRectangle);
+ var sourcePatches = ninePatchRegion.SourcePatches;
+
+ for (var i = 0; i < sourcePatches.Length; i++)
+ {
+ var sourcePatch = sourcePatches[i];
+ var destinationPatch = destinationPatches[i];
+
+ if (clippingRectangle.HasValue)
+ {
+ sourcePatch = ClipSourceRectangle(sourcePatch, destinationPatch, clippingRectangle.Value);
+ destinationPatch = ClipDestinationRectangle(destinationPatch, clippingRectangle.Value);
+ Draw(spriteBatch, ninePatchRegion.Texture, sourcePatch, destinationPatch, color, clippingRectangle);
+ }
+ else
+ {
+ if (destinationPatch.Width > 0 && destinationPatch.Height > 0)
+ spriteBatch.Draw(ninePatchRegion.Texture, sourceRectangle: sourcePatch, destinationRectangle: destinationPatch, color: color);
+ }
+ }
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle)
+ {
+ if (clippingRectangle.HasValue)
+ {
+ sourceRectangle = ClipSourceRectangle(sourceRectangle, destinationRectangle, clippingRectangle.Value);
+ destinationRectangle = ClipDestinationRectangle(destinationRectangle, clippingRectangle.Value);
+ }
+
+ if (destinationRectangle.Width > 0 && destinationRectangle.Height > 0)
+ spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color);
+ }
+
+ private static Rectangle ClipSourceRectangle(Rectangle sourceRectangle, Rectangle destinationRectangle, Rectangle clippingRectangle)
+ {
+ var left = (float)(clippingRectangle.Left - destinationRectangle.Left);
+ var right = (float)(destinationRectangle.Right - clippingRectangle.Right);
+ var top = (float)(clippingRectangle.Top - destinationRectangle.Top);
+ var bottom = (float)(destinationRectangle.Bottom - clippingRectangle.Bottom);
+ var x = left > 0 ? left : 0;
+ var y = top > 0 ? top : 0;
+ var w = (right > 0 ? right : 0) + x;
+ var h = (bottom > 0 ? bottom : 0) + y;
+
+ var scaleX = (float)destinationRectangle.Width / sourceRectangle.Width;
+ var scaleY = (float)destinationRectangle.Height / sourceRectangle.Height;
+ x /= scaleX;
+ y /= scaleY;
+ w /= scaleX;
+ h /= scaleY;
+
+ return new Rectangle((int)(sourceRectangle.X + x), (int)(sourceRectangle.Y + y), (int)(sourceRectangle.Width - w), (int)(sourceRectangle.Height - h));
+ }
+
+ private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle)
+ {
+ var left = clippingRectangle.Left < destinationRectangle.Left ? destinationRectangle.Left : clippingRectangle.Left;
+ var top = clippingRectangle.Top < destinationRectangle.Top ? destinationRectangle.Top : clippingRectangle.Top;
+ var bottom = clippingRectangle.Bottom < destinationRectangle.Bottom ? clippingRectangle.Bottom : destinationRectangle.Bottom;
+ var right = clippingRectangle.Right < destinationRectangle.Right ? clippingRectangle.Right : destinationRectangle.Right;
+ return new Rectangle(left, top, right - left, bottom - top);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs
new file mode 100644
index 0000000..20a7ae6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs
@@ -0,0 +1,41 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.Serialization;
+using ContentReaderExtensions = MonoGame.Extended.Content.ContentReaderExtensions;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasJsonContentTypeReader : JsonContentTypeReader<TextureAtlas>
+ {
+ private static TexturePackerFile Load(ContentReader reader)
+ {
+ var json = reader.ReadString();
+ return JsonSerializer.Deserialize<TexturePackerFile>(json);
+ }
+
+ protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance)
+ {
+ var texturePackerFile = Load(reader);
+ var assetName = reader.GetRelativeAssetName(texturePackerFile.Metadata.Image);
+ var texture = reader.ContentManager.Load<Texture2D>(assetName);
+ var atlas = new TextureAtlas(assetName, texture);
+
+ var regionCount = texturePackerFile.Regions.Count;
+
+ for (var i = 0; i < regionCount; i++)
+ {
+ atlas.CreateRegion(
+ ContentReaderExtensions.RemoveExtension(texturePackerFile.Regions[i].Filename),
+ texturePackerFile.Regions[i].Frame.X,
+ texturePackerFile.Regions[i].Frame.Y,
+ texturePackerFile.Regions[i].Frame.Width,
+ texturePackerFile.Regions[i].Frame.Height);
+ }
+
+ return atlas;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs
new file mode 100644
index 0000000..2d88709
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs
@@ -0,0 +1,84 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasJsonConverter : JsonConverter<TextureAtlas>
+ {
+ private readonly ContentManager _contentManager;
+ private readonly string _path;
+
+ public TextureAtlasJsonConverter(ContentManager contentManager, string path)
+ {
+ _contentManager = contentManager;
+ _path = path;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureAtlas);
+
+ public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if(reader.TokenType == JsonTokenType.String)
+ {
+ // TODO: (Aristurtle 05/20/2024) What is this for? It's just an if block that throws an exception. Need
+ // to investigate.
+ var textureAtlasAssetName = reader.GetString();
+ var contentPath = GetContentPath(textureAtlasAssetName);
+ var texturePackerFile = _contentManager.Load<TexturePackerFile>(contentPath, new JsonContentLoader());
+ var texture = _contentManager.Load<Texture2D>(texturePackerFile.Metadata.Image);
+ //return TextureAtlas.Create(texturePackerFile.Metadata.Image, texture );
+ throw new NotImplementedException();
+ }
+ else
+ {
+ var metadata = JsonSerializer.Deserialize<InlineTextureAtlas>(ref reader, options);
+
+ // TODO: When we get to .NET Standard 2.1 it would be more robust to use
+ // [Path.GetRelativePath](https://docs.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=netstandard-2.1)
+ var textureName = Path.GetFileNameWithoutExtension(metadata.Texture);
+ var textureDirectory = Path.GetDirectoryName(metadata.Texture);
+ var directory = Path.GetDirectoryName(_path);
+ var relativePath = Path.Combine(_contentManager.RootDirectory, directory, textureDirectory, textureName);
+ var resolvedAssetName = Path.GetFullPath(relativePath);
+ Texture2D texture;
+ try
+ {
+ texture = _contentManager.Load<Texture2D>(resolvedAssetName);
+ }
+ catch (Exception ex)
+ {
+ if (textureDirectory == null || textureDirectory == "")
+ texture = _contentManager.Load<Texture2D>(textureName);
+ else
+ texture = _contentManager.Load<Texture2D>(textureDirectory + "/" + textureName);
+ }
+ return TextureAtlas.Create(resolvedAssetName, texture, metadata.RegionWidth, metadata.RegionHeight);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, TextureAtlas value, JsonSerializerOptions options) { }
+
+
+ // ReSharper disable once ClassNeverInstantiated.Local
+ private class InlineTextureAtlas
+ {
+ public string Texture { get; set; }
+ public int RegionWidth { get; set; }
+ public int RegionHeight { get; set; }
+ }
+
+ private string GetContentPath(string relativePath)
+ {
+ var directory = Path.GetDirectoryName(_path);
+ return Path.Combine(directory, relativePath);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs
new file mode 100644
index 0000000..bfc7934
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasReader : ContentTypeReader<TextureAtlas>
+ {
+ protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance)
+ {
+ var assetName = reader.GetRelativeAssetName(reader.ReadString());
+ var texture = reader.ContentManager.Load<Texture2D>(assetName);
+ var atlas = new TextureAtlas(assetName, texture);
+
+ var regionCount = reader.ReadInt32();
+
+ for (var i = 0; i < regionCount; i++)
+ {
+ atlas.CreateRegion(
+ reader.ReadString(),
+ reader.ReadInt32(),
+ reader.ReadInt32(),
+ reader.ReadInt32(),
+ reader.ReadInt32());
+ }
+
+ return atlas;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs
new file mode 100644
index 0000000..b2cc243
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerFile
+ {
+ [JsonPropertyName("frames")]
+ public List<TexturePackerRegion> Regions { get; set; }
+
+ [JsonPropertyName("meta")]
+ public TexturePackerMeta Metadata { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs
new file mode 100644
index 0000000..5cdf21d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs
@@ -0,0 +1,36 @@
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerMeta
+ {
+ [JsonPropertyName("app")]
+ public string App { get; set; }
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ [JsonPropertyName("format")]
+ public string Format { get; set; }
+
+ [JsonPropertyName("size")]
+ public TexturePackerSize Size { get; set; }
+
+ [JsonPropertyName("scale")]
+ [JsonConverter(typeof(FloatStringConverter))]
+ public float Scale { get; set; }
+
+ [JsonPropertyName("smartupdate")]
+ public string SmartUpdate { get; set; }
+
+ public override string ToString()
+ {
+ return Image;
+ }
+ }
+}
+
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs
new file mode 100644
index 0000000..c4b5acd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerPoint
+ {
+ [JsonPropertyName("x")]
+ public double X { get; set; }
+
+ [JsonPropertyName("y")]
+ public double Y { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} {1}", X, Y);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs
new file mode 100644
index 0000000..21f3d3b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerRectangle
+ {
+ [JsonPropertyName("x")]
+ public int X { get; set; }
+
+ [JsonPropertyName("y")]
+ public int Y { get; set; }
+
+ [JsonPropertyName("w")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("h")]
+ public int Height { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} {1} {2} {3}", X, Y, Width, Height);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs
new file mode 100644
index 0000000..8664aa6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerRegion
+ {
+ [JsonPropertyName("filename")]
+ public string Filename { get; set; }
+
+ [JsonPropertyName("frame")]
+ public TexturePackerRectangle Frame { get; set; }
+
+ [JsonPropertyName("rotated")]
+ public bool IsRotated { get; set; }
+
+ [JsonPropertyName("trimmed")]
+ public bool IsTrimmed { get; set; }
+
+ [JsonPropertyName("spriteSourceSize")]
+ public TexturePackerRectangle SourceRectangle { get; set; }
+
+ [JsonPropertyName("sourceSize")]
+ public TexturePackerSize SourceSize { get; set; }
+
+ [JsonPropertyName("pivot")]
+ public TexturePackerPoint PivotPoint { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Filename} {Frame}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs
new file mode 100644
index 0000000..05351d0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerSize
+ {
+ [JsonPropertyName("w")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("h")]
+ public int Height { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Width} {Height}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs
new file mode 100644
index 0000000..05da555
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs
@@ -0,0 +1,54 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureRegion2D
+ {
+ public TextureRegion2D(Texture2D texture, int x, int y, int width, int height)
+ : this(null, texture, x, y, width, height)
+ {
+ }
+
+ public TextureRegion2D(Texture2D texture, Rectangle region)
+ : this(null, texture, region.X, region.Y, region.Width, region.Height)
+ {
+ }
+
+ public TextureRegion2D(string name, Texture2D texture, Rectangle region)
+ : this(name, texture, region.X, region.Y, region.Width, region.Height)
+ {
+ }
+
+ public TextureRegion2D(Texture2D texture)
+ : this(texture.Name, texture, 0, 0, texture.Width, texture.Height)
+ {
+ }
+
+ public TextureRegion2D(string name, Texture2D texture, int x, int y, int width, int height)
+ {
+ Name = name;
+ Texture = texture;
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+
+ public string Name { get; }
+ public Texture2D Texture { get; protected set; }
+ public int X { get; }
+ public int Y { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public Size2 Size => new Size2(Width, Height);
+ public object Tag { get; set; }
+ public Rectangle Bounds => new Rectangle(X, Y, Width, Height);
+
+ public override string ToString()
+ {
+ return $"{Name ?? string.Empty} {Bounds}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs
new file mode 100644
index 0000000..c31930b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public class ContinuousClock : GameTimer
+ {
+ public ContinuousClock(double intervalSeconds)
+ : base(intervalSeconds)
+ {
+ }
+
+ public ContinuousClock(TimeSpan interval)
+ : base(interval)
+ {
+ }
+
+ public TimeSpan NextTickTime { get; protected set; }
+
+ public event EventHandler Tick;
+
+ protected override void OnStopped()
+ {
+ NextTickTime = CurrentTime + Interval;
+ }
+
+ protected override void OnUpdate(GameTime gameTime)
+ {
+ if (CurrentTime >= NextTickTime)
+ {
+ NextTickTime = CurrentTime + Interval;
+ Tick?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs
new file mode 100644
index 0000000..fea7659
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public class CountdownTimer : GameTimer
+ {
+ public CountdownTimer(double intervalSeconds)
+ : base(intervalSeconds)
+ {
+ }
+
+ public CountdownTimer(TimeSpan interval)
+ : base(interval)
+ {
+ }
+
+ public TimeSpan TimeRemaining { get; private set; }
+
+ public event EventHandler TimeRemainingChanged;
+ public event EventHandler Completed;
+
+ protected override void OnStopped()
+ {
+ CurrentTime = TimeSpan.Zero;
+ }
+
+ protected override void OnUpdate(GameTime gameTime)
+ {
+ TimeRemaining = Interval - CurrentTime;
+ TimeRemainingChanged?.Invoke(this, EventArgs.Empty);
+
+ if (CurrentTime >= Interval)
+ {
+ State = TimerState.Completed;
+ CurrentTime = Interval;
+ TimeRemaining = TimeSpan.Zero;
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs
new file mode 100644
index 0000000..6a3acfe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public abstract class GameTimer : IUpdate
+ {
+ protected GameTimer(double intervalSeconds)
+ : this(TimeSpan.FromSeconds(intervalSeconds))
+ {
+ }
+
+ protected GameTimer(TimeSpan interval)
+ {
+ Interval = interval;
+ Restart();
+ }
+
+ public TimeSpan Interval { get; set; }
+ public TimeSpan CurrentTime { get; protected set; }
+ public TimerState State { get; protected set; }
+
+ public void Update(GameTime gameTime)
+ {
+ if (State != TimerState.Started)
+ return;
+
+ CurrentTime += gameTime.ElapsedGameTime;
+ OnUpdate(gameTime);
+ }
+
+ public event EventHandler Started;
+ public event EventHandler Stopped;
+ public event EventHandler Paused;
+
+ public void Start()
+ {
+ State = TimerState.Started;
+ Started?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Stop()
+ {
+ State = TimerState.Stopped;
+ CurrentTime = TimeSpan.Zero;
+ OnStopped();
+ Stopped?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Restart()
+ {
+ Stop();
+ Start();
+ }
+
+ public void Pause()
+ {
+ State = TimerState.Paused;
+ Paused?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected abstract void OnStopped();
+ protected abstract void OnUpdate(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs
new file mode 100644
index 0000000..1ab74ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Timers
+{
+ public enum TimerState
+ {
+ Started,
+ Stopped,
+ Paused,
+ Completed
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs
new file mode 100644
index 0000000..b0a0be4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs
@@ -0,0 +1,456 @@
+using System;
+using System.ComponentModel;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Code derived from top answer: http://gamedev.stackexchange.com/questions/113977/should-i-store-local-forward-right-up-vector-or-calculate-when-necessary
+
+ [Flags]
+ internal enum TransformFlags : byte
+ {
+ WorldMatrixIsDirty = 1 << 0,
+ LocalMatrixIsDirty = 1 << 1,
+ All = WorldMatrixIsDirty | LocalMatrixIsDirty
+ }
+
+ /// <summary>
+ /// Represents the base class for the position, rotation, and scale of a game object in two-dimensions or
+ /// three-dimensions.
+ /// </summary>
+ /// <typeparam name="TMatrix">The type of the matrix.</typeparam>
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// <para>
+ /// This class shouldn't be used directly. Instead use either of the derived classes; <see cref="Transform2" /> or
+ /// Transform3D.
+ /// </para>
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public abstract class BaseTransform<TMatrix>
+ where TMatrix : struct
+ {
+ private TransformFlags _flags = TransformFlags.All; // dirty flags, set all dirty flags when created
+ private TMatrix _localMatrix; // model space to local space
+ private BaseTransform<TMatrix> _parent; // parent
+ private TMatrix _worldMatrix; // local space to world space
+
+ // internal contructor because people should not be using this class directly; they should use Transform2D or Transform3D
+ internal BaseTransform()
+ {
+ }
+
+ /// <summary>
+ /// Gets the model-to-local space <see cref="Matrix2" />.
+ /// </summary>
+ /// <value>
+ /// The model-to-local space <see cref="Matrix2" />.
+ /// </value>
+ public TMatrix LocalMatrix
+ {
+ get
+ {
+ RecalculateLocalMatrixIfNecessary(); // attempt to update local matrix upon request if it is dirty
+ return _localMatrix;
+ }
+ }
+
+ /// <summary>
+ /// Gets the local-to-world space <see cref="Matrix2" />.
+ /// </summary>
+ /// <value>
+ /// The local-to-world space <see cref="Matrix2" />.
+ /// </value>
+ public TMatrix WorldMatrix
+ {
+ get
+ {
+ RecalculateWorldMatrixIfNecessary(); // attempt to update world matrix upon request if it is dirty
+ return _worldMatrix;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the parent instance.
+ /// </summary>
+ /// <value>
+ /// The parent instance.
+ /// </value>
+ /// <remarks>
+ /// <para>
+ /// Setting <see cref="Parent" /> to a non-null instance enables this instance to
+ /// inherit the position, rotation, and scale of the parent instance. Setting <see cref="Parent" /> to
+ /// <code>null</code> disables the inheritance altogether for this instance.
+ /// </para>
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public BaseTransform<TMatrix> Parent
+ {
+ get { return _parent; }
+ set
+ {
+ if (_parent == value)
+ return;
+
+ var oldParentTransform = Parent;
+ _parent = value;
+ OnParentChanged(oldParentTransform, value);
+ }
+ }
+
+ public event Action TransformBecameDirty; // observer pattern for when the world (or local) matrix became dirty
+ public event Action TranformUpdated; // observer pattern for after the world (or local) matrix was re-calculated
+
+ /// <summary>
+ /// Gets the model-to-local space <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The model-to-local space <see cref="Matrix2" />.</param>
+ public void GetLocalMatrix(out TMatrix matrix)
+ {
+ RecalculateLocalMatrixIfNecessary();
+ matrix = _localMatrix;
+ }
+
+ /// <summary>
+ /// Gets the local-to-world space <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The local-to-world space <see cref="Matrix2" />.</param>
+ public void GetWorldMatrix(out TMatrix matrix)
+ {
+ RecalculateWorldMatrixIfNecessary();
+ matrix = _worldMatrix;
+ }
+
+ protected internal void LocalMatrixBecameDirty()
+ {
+ _flags |= TransformFlags.LocalMatrixIsDirty;
+ }
+
+ protected internal void WorldMatrixBecameDirty()
+ {
+ _flags |= TransformFlags.WorldMatrixIsDirty;
+ TransformBecameDirty?.Invoke();
+ }
+
+ private void OnParentChanged(BaseTransform<TMatrix> oldParent, BaseTransform<TMatrix> newParent)
+ {
+ var parent = oldParent;
+ while (parent != null)
+ {
+ parent.TransformBecameDirty -= ParentOnTransformBecameDirty;
+ parent = parent.Parent;
+ }
+
+ parent = newParent;
+ while (parent != null)
+ {
+ parent.TransformBecameDirty += ParentOnTransformBecameDirty;
+ parent = parent.Parent;
+ }
+ }
+
+ private void ParentOnTransformBecameDirty()
+ {
+ _flags |= TransformFlags.All;
+ }
+
+ private void RecalculateWorldMatrixIfNecessary()
+ {
+ if ((_flags & TransformFlags.WorldMatrixIsDirty) == 0)
+ return;
+
+ RecalculateLocalMatrixIfNecessary();
+ RecalculateWorldMatrix(ref _localMatrix, out _worldMatrix);
+
+ _flags &= ~TransformFlags.WorldMatrixIsDirty;
+ TranformUpdated?.Invoke();
+ }
+
+ protected internal abstract void RecalculateWorldMatrix(ref TMatrix localMatrix, out TMatrix matrix);
+
+ private void RecalculateLocalMatrixIfNecessary()
+ {
+ if ((_flags & TransformFlags.LocalMatrixIsDirty) == 0)
+ return;
+
+ RecalculateLocalMatrix(out _localMatrix);
+
+ _flags &= ~TransformFlags.LocalMatrixIsDirty;
+ WorldMatrixBecameDirty();
+ }
+
+ protected internal abstract void RecalculateLocalMatrix(out TMatrix matrix);
+ }
+
+ /// <summary>
+ /// Represents the position, rotation, and scale of a two-dimensional game object.
+ /// </summary>
+ /// <seealso cref="BaseTransform{Matrix2D}" />
+ /// <seealso cref="IMovable" />
+ /// <seealso cref="IRotatable" />
+ /// <seealso cref="IScalable" />
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// </remarks>
+ public class Transform2 : BaseTransform<Matrix2>, IMovable, IRotatable, IScalable
+ {
+ private Vector2 _position;
+ private float _rotation;
+ private Vector2 _scale = Vector2.One;
+
+ public Transform2()
+ : this(Vector2.Zero, 0, Vector2.One)
+ {
+ }
+
+ public Transform2(float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1)
+ : this(new Vector2(x, y), rotation, new Vector2(scaleX, scaleY))
+ {
+ }
+
+ public Transform2(Vector2? position = null, float rotation = 0, Vector2? scale = null)
+ {
+ Position = position ?? Vector2.Zero;
+ Rotation = rotation;
+ Scale = scale ?? Vector2.One;
+ }
+
+ /// <summary>
+ /// Gets the world position.
+ /// </summary>
+ /// <value>
+ /// The world position.
+ /// </value>
+ public Vector2 WorldPosition => WorldMatrix.Translation;
+
+ /// <summary>
+ /// Gets the world scale.
+ /// </summary>
+ /// <value>
+ /// The world scale.
+ /// </value>
+ public Vector2 WorldScale => WorldMatrix.Scale;
+
+ /// <summary>
+ /// Gets the world rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The world rotation angle in radians.
+ /// </value>
+ public float WorldRotation => WorldMatrix.Rotation;
+
+ /// <summary>
+ /// Gets or sets the local position.
+ /// </summary>
+ /// <value>
+ /// The local position.
+ /// </value>
+ public Vector2 Position
+ {
+ get { return _position; }
+ set
+ {
+ _position = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The local rotation angle in radians.
+ /// </value>
+ public float Rotation
+ {
+ get { return _rotation; }
+ set
+ {
+ _rotation = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local scale.
+ /// </summary>
+ /// <value>
+ /// The local scale.
+ /// </value>
+ public Vector2 Scale
+ {
+ get { return _scale; }
+ set
+ {
+ _scale = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ protected internal override void RecalculateWorldMatrix(ref Matrix2 localMatrix, out Matrix2 matrix)
+ {
+ if (Parent != null)
+ {
+ Parent.GetWorldMatrix(out matrix);
+ Matrix2.Multiply(ref localMatrix, ref matrix, out matrix);
+ }
+ else
+ {
+ matrix = localMatrix;
+ }
+ }
+
+ protected internal override void RecalculateLocalMatrix(out Matrix2 matrix)
+ {
+ matrix = Matrix2.CreateScale(_scale) *
+ Matrix2.CreateRotationZ(_rotation) *
+ Matrix2.CreateTranslation(_position);
+ }
+
+ public override string ToString()
+ {
+ return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}";
+ }
+ }
+
+
+ /// <summary>
+ /// Represents the position, rotation, and scale of a three-dimensional game object.
+ /// </summary>
+ /// <seealso cref="BaseTransform{Matrix}" />
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// </remarks>
+ public class Transform3 : BaseTransform<Matrix> {
+ private Vector3 _position;
+ private Quaternion _rotation;
+ private Vector3 _scale = Vector3.One;
+
+ public Transform3(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) {
+ Position = position ?? Vector3.Zero;
+ Rotation = rotation ?? Quaternion.Identity;
+ Scale = scale ?? Vector3.One;
+ }
+
+ /// <summary>
+ /// Gets the world position.
+ /// </summary>
+ /// <value>
+ /// The world position.
+ /// </value>
+ public Vector3 WorldPosition => WorldMatrix.Translation;
+
+ /// <summary>
+ /// Gets the world scale.
+ /// </summary>
+ /// <value>
+ /// The world scale.
+ /// </value>
+ public Vector3 WorldScale {
+ get {
+ Vector3 scale = Vector3.Zero;
+ Quaternion rotation = Quaternion.Identity;
+ Vector3 translation = Vector3.Zero;
+ WorldMatrix.Decompose(out scale, out rotation, out translation);
+ return scale;
+ }
+ }
+
+
+ /// <summary>
+ /// Gets the world rotation quaternion in radians.
+ /// </summary>
+ /// <value>
+ /// The world rotation quaternion in radians.
+ /// </value>
+ public Quaternion WorldRotation {
+ get {
+ Vector3 scale = Vector3.Zero;
+ Quaternion rotation = Quaternion.Identity;
+ Vector3 translation = Vector3.Zero;
+ WorldMatrix.Decompose(out scale, out rotation, out translation);
+ return rotation;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local position.
+ /// </summary>
+ /// <value>
+ /// The local position.
+ /// </value>
+ public Vector3 Position {
+ get { return _position; }
+ set {
+ _position = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local rotation quaternion in radians.
+ /// </summary>
+ /// <value>
+ /// The local rotation quaternion in radians.
+ /// </value>
+ public Quaternion Rotation {
+ get { return _rotation; }
+ set {
+ _rotation = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local scale.
+ /// </summary>
+ /// <value>
+ /// The local scale.
+ /// </value>
+ public Vector3 Scale {
+ get { return _scale; }
+ set {
+ _scale = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ protected internal override void RecalculateWorldMatrix(ref Matrix localMatrix, out Matrix matrix) {
+ if (Parent != null) {
+ Parent.GetWorldMatrix(out matrix);
+ Matrix.Multiply(ref localMatrix, ref matrix, out matrix);
+ }
+ else {
+ matrix = localMatrix;
+ }
+ }
+
+ protected internal override void RecalculateLocalMatrix(out Matrix matrix) {
+ matrix = Matrix.CreateScale(_scale) *
+ Matrix.CreateFromQuaternion(_rotation) *
+ Matrix.CreateTranslation(_position);
+ }
+
+ public override string ToString() {
+ return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs
new file mode 100644
index 0000000..621a627
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs
@@ -0,0 +1,183 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.VectorDraw
+{
+ //Taken from Velcro Physics
+ //Used with permission: https://github.com/craftworkgames/MonoGame.Extended/issues/574
+
+ public class PrimitiveBatch : IDisposable
+ {
+ private const int DefaultBufferSize = 500;
+
+ // a basic effect, which contains the shaders that we will use to draw our
+ // primitives.
+ private readonly BasicEffect _basicEffect;
+
+ // the device that we will issue draw calls to.
+ private readonly GraphicsDevice _device;
+
+ private readonly VertexPositionColor[] _lineVertices;
+ private readonly VertexPositionColor[] _triangleVertices;
+
+ // hasBegun is flipped to true once Begin is called, and is used to make
+ // sure users don't call End before Begin is called.
+ private bool _hasBegun;
+
+ private bool _isDisposed;
+ private int _lineVertsCount;
+ private int _triangleVertsCount;
+
+ public PrimitiveBatch(GraphicsDevice graphicsDevice, int bufferSize = DefaultBufferSize)
+ {
+ if (graphicsDevice == null)
+ throw new ArgumentNullException(nameof(graphicsDevice));
+
+ _device = graphicsDevice;
+
+ _triangleVertices = new VertexPositionColor[bufferSize - bufferSize % 3];
+ _lineVertices = new VertexPositionColor[bufferSize - bufferSize % 2];
+
+ // set up a new basic effect, and enable vertex colors.
+ _basicEffect = new BasicEffect(graphicsDevice);
+ _basicEffect.VertexColorEnabled = true;
+ }
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ public void SetProjection(ref Matrix projection)
+ {
+ _basicEffect.Projection = projection;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing && !_isDisposed)
+ {
+ if (_basicEffect != null)
+ _basicEffect.Dispose();
+
+ _isDisposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Begin is called to tell the PrimitiveBatch what kind of primitives will be
+ /// drawn, and to prepare the graphics card to render those primitives.
+ /// </summary>
+ /// <param name="projection">The projection.</param>
+ /// <param name="view">The view.</param>
+ public void Begin(ref Matrix projection, ref Matrix view)
+ {
+ if (_hasBegun)
+ throw new InvalidOperationException("End must be called before Begin can be called again.");
+
+ //tell our basic effect to begin.
+ _basicEffect.Projection = projection;
+ _basicEffect.View = view;
+ _basicEffect.CurrentTechnique.Passes[0].Apply();
+
+ // flip the error checking boolean. It's now ok to call AddVertex, Flush,
+ // and End.
+ _hasBegun = true;
+ }
+
+ public bool IsReady()
+ {
+ return _hasBegun;
+ }
+
+ public void AddVertex(Vector2 vertex, Color color, PrimitiveType primitiveType)
+ {
+ if (!_hasBegun)
+ throw new InvalidOperationException("Begin must be called before AddVertex can be called.");
+
+ if (primitiveType == PrimitiveType.LineStrip || primitiveType == PrimitiveType.TriangleStrip)
+ throw new NotSupportedException("The specified primitiveType is not supported by PrimitiveBatch.");
+
+ if (primitiveType == PrimitiveType.TriangleList)
+ {
+ if (_triangleVertsCount >= _triangleVertices.Length)
+ FlushTriangles();
+
+ _triangleVertices[_triangleVertsCount].Position = new Vector3(vertex, -0.1f);
+ _triangleVertices[_triangleVertsCount].Color = color;
+ _triangleVertsCount++;
+ }
+
+ if (primitiveType == PrimitiveType.LineList)
+ {
+ if (_lineVertsCount >= _lineVertices.Length)
+ FlushLines();
+
+ _lineVertices[_lineVertsCount].Position = new Vector3(vertex, 0f);
+ _lineVertices[_lineVertsCount].Color = color;
+ _lineVertsCount++;
+ }
+ }
+
+ /// <summary>
+ /// End is called once all the primitives have been drawn using AddVertex.
+ /// it will call Flush to actually submit the draw call to the graphics card, and
+ /// then tell the basic effect to end.
+ /// </summary>
+ public void End()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before End can be called.");
+ }
+
+ // Draw whatever the user wanted us to draw
+ FlushTriangles();
+ FlushLines();
+
+ _hasBegun = false;
+ }
+
+ private void FlushTriangles()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before Flush can be called.");
+ }
+ if (_triangleVertsCount >= 3)
+ {
+ int primitiveCount = _triangleVertsCount / 3;
+
+ // submit the draw call to the graphics card
+ _device.SamplerStates[0] = SamplerState.AnisotropicClamp;
+ _device.DrawUserPrimitives(PrimitiveType.TriangleList, _triangleVertices, 0, primitiveCount);
+ _triangleVertsCount -= primitiveCount * 3;
+ }
+ }
+
+ private void FlushLines()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before Flush can be called.");
+ }
+ if (_lineVertsCount >= 2)
+ {
+ int primitiveCount = _lineVertsCount / 2;
+
+ // submit the draw call to the graphics card
+ _device.SamplerStates[0] = SamplerState.AnisotropicClamp;
+ _device.DrawUserPrimitives(PrimitiveType.LineList, _lineVertices, 0, primitiveCount);
+ _lineVertsCount -= primitiveCount * 2;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs
new file mode 100644
index 0000000..fcf5791
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs
@@ -0,0 +1,255 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Triangulation;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.VectorDraw
+{
+ public class PrimitiveDrawing
+ {
+ //Drawing
+ private PrimitiveBatch _primitiveBatch;
+
+ private SpriteBatch _batch;
+ private SpriteFont _font;
+ private GraphicsDevice _device;
+ private readonly Vector2[] _tempVertices = new Vector2[1000]; //TODO: something else...
+
+ //private Matrix _localProjection;
+ //private Matrix _localView;
+
+ //TODO: do we need to split this based on platform?
+#if XBOX || WINDOWS_PHONE
+ public const int CircleSegments = 16;
+#else
+ public const int CircleSegments = 32;
+#endif
+
+ public PrimitiveDrawing(PrimitiveBatch primitiveBatch)
+ {
+ _primitiveBatch = primitiveBatch;
+ }
+
+ public void DrawPoint(Vector2 center, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ //Add two points or the PrimitiveBatch acts up
+ _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
+ }
+
+ public void DrawRectangle(Vector2 location, float width, float height, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Vector2[] rectVerts = new Vector2[4]
+ {
+ new Vector2(0, 0),
+ new Vector2(width, 0),
+ new Vector2(width, height),
+ new Vector2(0, height)
+ };
+
+ //Location is offset here
+ DrawPolygon(location, rectVerts, color);
+ }
+
+ public void DrawSolidRectangle(Vector2 location, float width, float height, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Vector2[] rectVerts = new Vector2[4]
+ {
+ new Vector2(0, 0),
+ new Vector2(width, 0),
+ new Vector2(width, height),
+ new Vector2(0, height)
+ };
+
+ DrawSolidPolygon(location, rectVerts, color);
+ }
+
+ public void DrawCircle(Vector2 center, float radius, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ for (int i = 0; i < CircleSegments; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
+
+ theta += increment;
+ }
+ }
+
+ public void DrawSolidCircle(Vector2 center, float radius, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ Color colorFill = color * 0.5f;
+
+ Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ theta += increment;
+
+ for (int i = 1; i < CircleSegments - 1; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v0, colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v1, colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v2, colorFill, PrimitiveType.TriangleList);
+
+ theta += increment;
+ }
+
+ DrawCircle(center, radius, color);
+ }
+ public void DrawSolidCircle(Vector2 center, float radius, Color color, Color fillcolor)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ theta += increment;
+
+ for (int i = 1; i < CircleSegments - 1; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v0, fillcolor, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v1, fillcolor, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v2, fillcolor, PrimitiveType.TriangleList);
+
+ theta += increment;
+ }
+
+ DrawCircle(center, radius, color);
+ }
+
+ public void DrawSegment(Vector2 start, Vector2 end, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ _primitiveBatch.AddVertex(start, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(end, color, PrimitiveType.LineList);
+ }
+
+ public void DrawPolygon(Vector2 position, Vector2[] vertices, Color color, bool closed = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ int count = vertices.Length;
+
+ for (int i = 0; i < count - 1; i++)
+ {
+ //translate the vertices according to the position passed
+ _primitiveBatch.AddVertex(new Vector2(vertices[i].X + position.X, vertices[i].Y + position.Y), color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(new Vector2(vertices[i + 1].X + position.X, vertices[i + 1].Y + position.Y), color, PrimitiveType.LineList);
+ }
+ if (closed)
+ {
+ //TODO: verify closed is working as expected
+ _primitiveBatch.AddVertex(new Vector2(vertices[count - 1].X + position.X, vertices[count - 1].Y + position.Y), color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(new Vector2(vertices[0].X + position.X, vertices[0].Y + position.Y), color, PrimitiveType.LineList);
+ }
+ }
+
+ public void DrawSolidPolygon(Vector2 position, Vector2[] vertices, Color color, bool outline = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ int count = vertices.Length;
+
+ if (count == 2)
+ {
+ DrawPolygon(position, vertices, color);
+ return;
+ }
+
+ Color colorFill = color * (outline ? 0.5f : 1.0f);
+
+ Vector2[] outVertices;
+ int[] outIndices;
+ Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
+
+ for(int i = 0; i < outIndices.Length - 2; i += 3)
+ {
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + position.X, outVertices[outIndices[i]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + position.X, outVertices[outIndices[i + 1]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + position.X, outVertices[outIndices[i + 2]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ }
+
+ if (outline)
+ DrawPolygon(position, vertices, color);
+ }
+
+ public void DrawEllipse(Vector2 center, Vector2 radius, int sides, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ DrawPolygon(center, CreateEllipse(radius.X, radius.Y, sides), color);
+ }
+
+ public void DrawSolidEllipse(Vector2 center, Vector2 radius, int sides, Color color, bool outline = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Color colorFill = color * (outline ? 0.5f : 1.0f);
+
+ Vector2[] vertices = CreateEllipse(radius.X, radius.Y, sides);
+
+ Vector2[] outVertices;
+ int[] outIndices;
+ Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
+
+ for (int i = 0; i < outIndices.Length - 2; i += 3)
+ {
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + center.X, outVertices[outIndices[i]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + center.X, outVertices[outIndices[i + 1]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + center.X, outVertices[outIndices[i + 2]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ }
+ }
+
+ private static Vector2[] CreateEllipse(float rx, float ry, int sides)
+ {
+ var vertices = new Vector2[sides];
+
+ var t = 0.0;
+ var dt = 2.0 * Math.PI / sides;
+ for (var i = 0; i < sides; i++, t += dt)
+ {
+ var x = (float)(rx * Math.Cos(t));
+ var y = (float)(ry * Math.Sin(t));
+ vertices[i] = new Vector2(x, y);
+ }
+ return vertices;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs
new file mode 100644
index 0000000..ea80a5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs
@@ -0,0 +1,96 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+// ReSharper disable once CheckNamespace
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public enum BoxingMode
+ {
+ None,
+ Letterbox,
+ Pillarbox
+ }
+
+ public class BoxingViewportAdapter : ScalingViewportAdapter
+ {
+ private readonly GameWindow _window;
+ private readonly GraphicsDevice _graphicsDevice;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoxingViewportAdapter" />.
+ /// </summary>
+ public BoxingViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight, int horizontalBleed = 0, int verticalBleed = 0)
+ : base(graphicsDevice, virtualWidth, virtualHeight)
+ {
+ _window = window;
+ _graphicsDevice = graphicsDevice;
+ window.ClientSizeChanged += OnClientSizeChanged;
+ HorizontalBleed = horizontalBleed;
+ VerticalBleed = verticalBleed;
+ }
+
+ public override void Dispose()
+ {
+ _window.ClientSizeChanged -= OnClientSizeChanged;
+ base.Dispose();
+ }
+
+ /// <summary>
+ /// Size of horizontal bleed areas (from left and right edges) which can be safely cut off
+ /// </summary>
+ public int HorizontalBleed { get; }
+
+ /// <summary>
+ /// Size of vertical bleed areas (from top and bottom edges) which can be safely cut off
+ /// </summary>
+ public int VerticalBleed { get; }
+
+ public BoxingMode BoxingMode { get; private set; }
+
+ private void OnClientSizeChanged(object sender, EventArgs eventArgs)
+ {
+ var clientBounds = _window.ClientBounds;
+
+ var worldScaleX = (float)clientBounds.Width / VirtualWidth;
+ var worldScaleY = (float)clientBounds.Height / VirtualHeight;
+
+ var safeScaleX = (float)clientBounds.Width / (VirtualWidth - HorizontalBleed);
+ var safeScaleY = (float)clientBounds.Height / (VirtualHeight - VerticalBleed);
+
+ var worldScale = MathHelper.Max(worldScaleX, worldScaleY);
+ var safeScale = MathHelper.Min(safeScaleX, safeScaleY);
+ var scale = MathHelper.Min(worldScale, safeScale);
+
+ var width = (int)(scale * VirtualWidth + 0.5f);
+ var height = (int)(scale * VirtualHeight + 0.5f);
+
+ if (height >= clientBounds.Height && width < clientBounds.Width)
+ BoxingMode = BoxingMode.Pillarbox;
+ else
+ {
+ if (width >= clientBounds.Height && height <= clientBounds.Height)
+ BoxingMode = BoxingMode.Letterbox;
+ else
+ BoxingMode = BoxingMode.None;
+ }
+
+ var x = clientBounds.Width / 2 - width / 2;
+ var y = clientBounds.Height / 2 - height / 2;
+ GraphicsDevice.Viewport = new Viewport(x, y, width, height);
+ }
+
+ public override void Reset()
+ {
+ base.Reset();
+ OnClientSizeChanged(this, EventArgs.Empty);
+ }
+
+ public override Point PointToScreen(int x, int y)
+ {
+ var viewport = GraphicsDevice.Viewport;
+ return base.PointToScreen(x - viewport.X, y - viewport.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs
new file mode 100644
index 0000000..9026ce4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs
@@ -0,0 +1,28 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+// ReSharper disable once CheckNamespace
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class DefaultViewportAdapter : ViewportAdapter
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+
+ public DefaultViewportAdapter(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ _graphicsDevice = graphicsDevice;
+ }
+
+ public override int VirtualWidth => _graphicsDevice.Viewport.Width;
+ public override int VirtualHeight => _graphicsDevice.Viewport.Height;
+ public override int ViewportWidth => _graphicsDevice.Viewport.Width;
+ public override int ViewportHeight => _graphicsDevice.Viewport.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ return Matrix.Identity;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs
new file mode 100644
index 0000000..674a62e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class ScalingViewportAdapter : ViewportAdapter
+ {
+ public ScalingViewportAdapter(GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight)
+ : base(graphicsDevice)
+ {
+ VirtualWidth = virtualWidth;
+ VirtualHeight = virtualHeight;
+ }
+
+ public override int VirtualWidth { get; }
+ public override int VirtualHeight { get; }
+ public override int ViewportWidth => GraphicsDevice.Viewport.Width;
+ public override int ViewportHeight => GraphicsDevice.Viewport.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ var scaleX = (float) ViewportWidth/VirtualWidth;
+ var scaleY = (float) ViewportHeight/VirtualHeight;
+ return Matrix.CreateScale(scaleX, scaleY, 1.0f);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs
new file mode 100644
index 0000000..eb0491e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs
@@ -0,0 +1,46 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public abstract class ViewportAdapter : IDisposable
+ {
+ protected ViewportAdapter(GraphicsDevice graphicsDevice)
+ {
+ GraphicsDevice = graphicsDevice;
+ }
+
+ public virtual void Dispose()
+ {
+ }
+
+ public GraphicsDevice GraphicsDevice { get; }
+ public Viewport Viewport => GraphicsDevice.Viewport;
+
+ public abstract int VirtualWidth { get; }
+ public abstract int VirtualHeight { get; }
+ public abstract int ViewportWidth { get; }
+ public abstract int ViewportHeight { get; }
+
+ public Rectangle BoundingRectangle => new Rectangle(0, 0, VirtualWidth, VirtualHeight);
+ public Point Center => BoundingRectangle.Center;
+ public abstract Matrix GetScaleMatrix();
+
+ public Point PointToScreen(Point point)
+ {
+ return PointToScreen(point.X, point.Y);
+ }
+
+ public virtual Point PointToScreen(int x, int y)
+ {
+ var scaleMatrix = GetScaleMatrix();
+ var invertedMatrix = Matrix.Invert(scaleMatrix);
+ return Vector2.Transform(new Vector2(x, y), invertedMatrix).ToPoint();
+ }
+
+ public virtual void Reset()
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs
new file mode 100644
index 0000000..b995887
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class WindowViewportAdapter : ViewportAdapter
+ {
+ protected readonly GameWindow Window;
+
+ public WindowViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ Window = window;
+ window.ClientSizeChanged += OnClientSizeChanged;
+ }
+
+ public override int ViewportWidth => Window.ClientBounds.Width;
+ public override int ViewportHeight => Window.ClientBounds.Height;
+ public override int VirtualWidth => Window.ClientBounds.Width;
+ public override int VirtualHeight => Window.ClientBounds.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ return Matrix.Identity;
+ }
+
+ private void OnClientSizeChanged(object sender, EventArgs eventArgs)
+ {
+ var x = Window.ClientBounds.Width;
+ var y = Window.ClientBounds.Height;
+
+ GraphicsDevice.Viewport = new Viewport(0, 0, x, y);
+ }
+ }
+} \ No newline at end of file