2019-09-18 15:54:20 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using Microsoft.Xna.Framework ;
2021-03-07 22:03:29 +01:00
using MLEM.Extended.Extensions ;
2019-09-18 15:54:20 +02:00
using MLEM.Extensions ;
2020-03-21 00:49:43 +01:00
using MLEM.Misc ;
2019-09-18 15:54:20 +02:00
using MonoGame.Extended.Tiled ;
2020-03-21 00:49:43 +01:00
using RectangleF = MonoGame . Extended . RectangleF ;
2019-09-18 15:54:20 +02:00
namespace MLEM.Extended.Tiled {
2020-05-22 20:32:38 +02:00
/// <summary>
/// A collision handler for a MonoGame.Extended tiled tile map.
/// The idea behind this collision handler is that, on the map's tileset, each tile is assigned a certain rectangular area. That area is converted into a collision map that is dealt with in tile units, where each tile's covered area is 1x1 units big.
/// </summary>
2019-09-18 15:54:20 +02:00
public class TiledMapCollisions {
private TiledMap map ;
private TileCollisionInfo [ , , ] collisionInfos ;
2020-01-01 12:41:48 +01:00
private CollectCollisions collisionFunction ;
2019-09-18 15:54:20 +02:00
2020-05-22 20:32:38 +02:00
/// <summary>
/// Creates a new tiled map collision handler for the given map
/// </summary>
/// <param name="map">The map</param>
2019-09-18 15:54:20 +02:00
public TiledMapCollisions ( TiledMap map = null ) {
if ( map ! = null )
this . SetMap ( map ) ;
}
2020-05-22 20:32:38 +02:00
/// <summary>
/// Sets this collision handler's handled map
/// </summary>
/// <param name="map">The map</param>
/// <param name="collisionFunction">The function used to collect the collision info of a tile, or null for the default handling</param>
2020-01-01 12:41:48 +01:00
public void SetMap ( TiledMap map , CollectCollisions collisionFunction = null ) {
2019-09-18 15:54:20 +02:00
this . map = map ;
2020-01-01 12:41:48 +01:00
this . collisionFunction = collisionFunction ? ? ( ( collisions , tile ) = > {
foreach ( var obj in tile . TilesetTile . Objects ) {
var area = obj . GetArea ( tile . Map ) ;
if ( tile . Tile . IsFlippedHorizontally )
area . X = 1 - area . X - area . Width ;
if ( tile . Tile . IsFlippedVertically )
area . Y = 1 - area . Y - area . Height ;
area . Offset ( tile . Position ) ;
collisions . Add ( area ) ;
}
} ) ;
2019-09-18 15:54:20 +02:00
this . collisionInfos = new TileCollisionInfo [ map . Layers . Count , map . Width , map . Height ] ;
for ( var i = 0 ; i < map . TileLayers . Count ; i + + ) {
for ( var x = 0 ; x < map . Width ; x + + ) {
for ( var y = 0 ; y < map . Height ; y + + ) {
2019-09-19 11:11:47 +02:00
this . UpdateCollisionInfo ( i , x , y ) ;
2019-09-18 15:54:20 +02:00
}
}
}
}
2020-05-22 20:32:38 +02:00
/// <summary>
/// Updates the collision info for the tile at the given position.
/// </summary>
/// <param name="layerIndex">The index of the tile's layer in <see cref="TiledMap.TileLayers"/></param>
/// <param name="x">The tile's x coordinate</param>
/// <param name="y">The tile's y coordinate</param>
2019-09-19 11:11:47 +02:00
public void UpdateCollisionInfo ( int layerIndex , int x , int y ) {
var layer = this . map . TileLayers [ layerIndex ] ;
2019-09-24 12:26:38 +02:00
var tile = layer . GetTile ( x , y ) ;
2019-12-07 02:08:21 +01:00
if ( tile . IsBlank ) {
this . collisionInfos [ layerIndex , x , y ] = null ;
2019-09-19 11:11:47 +02:00
return ;
2019-12-07 02:08:21 +01:00
}
2019-09-19 11:11:47 +02:00
var tilesetTile = tile . GetTilesetTile ( this . map ) ;
2020-01-01 12:41:48 +01:00
this . collisionInfos [ layerIndex , x , y ] = new TileCollisionInfo ( this . map , new Vector2 ( x , y ) , tile , layer , tilesetTile , this . collisionFunction ) ;
2019-09-19 11:11:47 +02:00
}
2020-05-22 20:32:38 +02:00
/// <summary>
/// Returns an enumerable of tile collision infos that intersect the given area.
/// Optionally, a predicate can be supplied that narrows the search.
/// </summary>
/// <param name="area">The area to check for collisions in</param>
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <returns>An enumerable of collision infos for that area</returns>
2019-09-18 15:54:20 +02:00
public IEnumerable < TileCollisionInfo > GetCollidingTiles ( RectangleF area , Func < TileCollisionInfo , bool > included = null ) {
var inclusionFunc = included ? ? ( tile = > tile . Collisions . Any ( c = > c . Intersects ( area ) ) ) ;
2019-09-25 21:06:56 +02:00
var minX = Math . Max ( 0 , area . Left . Floor ( ) ) ;
2021-03-07 20:45:00 +01:00
var maxX = Math . Min ( this . map . Width - 1 , area . Right . Floor ( ) ) ;
2019-09-25 21:06:56 +02:00
var minY = Math . Max ( 0 , area . Top . Floor ( ) ) ;
2021-03-07 20:45:00 +01:00
var maxY = Math . Min ( this . map . Height - 1 , area . Bottom . Floor ( ) ) ;
2019-09-18 15:54:20 +02:00
for ( var i = 0 ; i < this . map . TileLayers . Count ; i + + ) {
2021-03-07 20:59:10 +01:00
for ( var y = maxY ; y > = minY ; y - - ) {
for ( var x = minX ; x < = maxX ; x + + ) {
2019-09-18 15:54:20 +02:00
var tile = this . collisionInfos [ i , x , y ] ;
if ( tile = = null )
continue ;
if ( inclusionFunc ( tile ) )
yield return tile ;
}
}
}
}
2020-05-22 20:32:38 +02:00
/// <summary>
/// Returns whether there are any colliding tiles in the given area.
/// Optionally, a predicate can be supplied that narrows the search.
/// </summary>
/// <param name="area">The area to check for collisions in</param>
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <returns>True if there are any colliders in the area, false otherwise</returns>
2019-09-18 15:54:20 +02:00
public bool IsColliding ( RectangleF area , Func < TileCollisionInfo , bool > included = null ) {
return this . GetCollidingTiles ( area , included ) . Any ( ) ;
}
2020-05-22 20:32:38 +02:00
/// <summary>
2021-03-07 22:13:24 +01:00
/// Returns an enumerable of all of the <see cref="TileCollisionInfo.Collisions"/> of the colliding tiles in the given area.
/// This method is a convenience method based on <see cref="GetCollidingTiles"/>.
/// </summary>
/// <param name="area">The area to check for collisions in</param>
/// <param name="included">A function that determines if a certain info should be included or not</param>
/// <returns>An enumerable of collision rectangles for that area</returns>
public IEnumerable < RectangleF > GetCollidingAreas ( RectangleF area , Func < TileCollisionInfo , bool > included = null ) {
foreach ( var tile in this . GetCollidingTiles ( area , included ) ) {
foreach ( var col in tile . Collisions )
yield return col ;
}
}
/// <summary>
/// Returns an enumerable of normals and penetration amounts for each <see cref="TileCollisionInfo"/> that intersects with the given <see cref="RectangleF"/> area.
2021-03-07 22:03:29 +01:00
/// The normals and penetration amounts are based on <see cref="MLEM.Extensions.NumberExtensions.Penetrate"/>.
2021-03-08 15:12:13 +01:00
/// Note that all x penetrations are returned before all y penetrations, which improves collision detection in sidescrolling games with gravity. Note that this behavior can be inverted using <paramref name="prioritizeX"/>.
2021-03-07 22:03:29 +01:00
/// </summary>
/// <param name="getArea">The area to penetrate</param>
2021-03-07 22:13:24 +01:00
/// <param name="included">A function that determines if a certain info should be included or not</param>
2021-03-08 15:12:13 +01:00
/// <param name="prioritizeX">Whether all x penetrations should be prioritized (returned first). If this is false, all y penetrations are prioritized instead.</param>
2021-03-07 22:03:29 +01:00
/// <returns>A set of normals and penetration amounts</returns>
2021-03-08 15:12:13 +01:00
public IEnumerable < ( Vector2 , float ) > GetPenetrations ( Func < RectangleF > getArea , Func < TileCollisionInfo , bool > included = null , bool prioritizeX = true ) {
2021-03-07 22:13:24 +01:00
foreach ( var col in this . GetCollidingAreas ( getArea ( ) , included ) ) {
2021-03-08 15:12:13 +01:00
if ( getArea ( ) . Penetrate ( col , out var normal , out var penetration ) & & normal . X ! = 0 = = prioritizeX )
2021-03-07 22:03:29 +01:00
yield return ( normal , penetration ) ;
}
2021-03-07 22:13:24 +01:00
foreach ( var col in this . GetCollidingAreas ( getArea ( ) , included ) ) {
2021-03-08 15:12:13 +01:00
if ( getArea ( ) . Penetrate ( col , out var normal , out var penetration ) & & normal . X = = 0 = = prioritizeX )
2021-03-07 22:03:29 +01:00
yield return ( normal , penetration ) ;
}
}
/// <summary>
2020-05-22 20:32:38 +02:00
/// A delegate method used to override the default collision checking behavior.
/// </summary>
/// <param name="collisions">The list of collisions to add to</param>
/// <param name="tile">The tile's collision information</param>
2020-01-01 12:41:48 +01:00
public delegate void CollectCollisions ( List < RectangleF > collisions , TileCollisionInfo tile ) ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// A tile collision info stores information about a tile at a given location on a given layer, including its objects and their bounds.
/// </summary>
2020-03-21 00:49:43 +01:00
public class TileCollisionInfo : GenericDataHolder {
2019-09-18 15:54:20 +02:00
2020-05-22 20:32:38 +02:00
/// <summary>
/// The map the tile is on
/// </summary>
2020-01-01 12:41:48 +01:00
public readonly TiledMap Map ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// The position of the tile, in tile units
/// </summary>
2020-01-01 12:41:48 +01:00
public readonly Vector2 Position ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// The tiled map tile
/// </summary>
2019-09-18 15:54:20 +02:00
public readonly TiledMapTile Tile ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// The layer that this tile is on
/// </summary>
2019-12-30 19:19:40 +01:00
public readonly TiledMapTileLayer Layer ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// The tileset tile for this tile
/// </summary>
2019-09-18 15:54:20 +02:00
public readonly TiledMapTilesetTile TilesetTile ;
2020-05-22 20:32:38 +02:00
/// <summary>
/// The list of colliders for this tile
/// </summary>
2020-01-01 12:41:48 +01:00
public readonly List < RectangleF > Collisions ;
2019-09-18 15:54:20 +02:00
2020-05-22 20:32:38 +02:00
internal TileCollisionInfo ( TiledMap map , Vector2 position , TiledMapTile tile , TiledMapTileLayer layer , TiledMapTilesetTile tilesetTile , CollectCollisions collisionFunction ) {
2019-09-18 15:54:20 +02:00
this . TilesetTile = tilesetTile ;
2019-12-30 19:19:40 +01:00
this . Layer = layer ;
2019-09-18 15:54:20 +02:00
this . Tile = tile ;
2020-01-01 12:41:48 +01:00
this . Map = map ;
this . Position = position ;
2019-09-18 15:54:20 +02:00
2020-01-01 12:41:48 +01:00
this . Collisions = new List < RectangleF > ( ) ;
collisionFunction ( this . Collisions , this ) ;
2019-09-18 15:54:20 +02:00
}
}
}
}