using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using MLEM.Extended.Extensions; using MLEM.Extensions; using MLEM.Misc; using MonoGame.Extended.Tiled; using RectangleF = MonoGame.Extended.RectangleF; namespace MLEM.Extended.Tiled { /// /// 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. /// public class TiledMapCollisions { private TiledMap map; private TileCollisionInfo[,,] collisionInfos; private CollectCollisions collisionFunction; /// /// Creates a new tiled map collision handler for the given map /// /// The map public TiledMapCollisions(TiledMap map = null) { if (map != null) this.SetMap(map); } /// /// Sets this collision handler's handled map /// /// The map /// The function used to collect the collision info of a tile, or null for the default handling public void SetMap(TiledMap map, CollectCollisions collisionFunction = null) { this.map = map; 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); } }); 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++) { this.UpdateCollisionInfo(i, x, y); } } } } /// /// Updates the collision info for the tile at the given position. /// /// The index of the tile's layer in /// The tile's x coordinate /// The tile's y coordinate public void UpdateCollisionInfo(int layerIndex, int x, int y) { var layer = this.map.TileLayers[layerIndex]; var tile = layer.GetTile(x, y); if (tile.IsBlank) { this.collisionInfos[layerIndex, x, y] = null; return; } var tilesetTile = tile.GetTilesetTile(this.map); this.collisionInfos[layerIndex, x, y] = new TileCollisionInfo(this.map, new Vector2(x, y), tile, layer, tilesetTile, this.collisionFunction); } /// /// Returns an enumerable of tile collision infos that intersect the given area. /// Optionally, a predicate can be supplied that narrows the search. /// /// The area to check for collisions in /// A function that determines if a certain info should be included or not /// An enumerable of collision infos for that area public IEnumerable GetCollidingTiles(RectangleF area, Func included = null) { var inclusionFunc = included ?? (tile => tile.Collisions.Any(c => c.Intersects(area))); var minX = Math.Max(0, area.Left.Floor()); var maxX = Math.Min(this.map.Width - 1, area.Right.Floor()); var minY = Math.Max(0, area.Top.Floor()); var maxY = Math.Min(this.map.Height - 1, area.Bottom.Floor()); for (var i = 0; i < this.map.TileLayers.Count; i++) { for (var y = maxY; y >= minY; y--) { for (var x = minX; x <= maxX; x++) { var tile = this.collisionInfos[i, x, y]; if (tile == null) continue; if (inclusionFunc(tile)) yield return tile; } } } } /// /// Returns whether there are any colliding tiles in the given area. /// Optionally, a predicate can be supplied that narrows the search. /// /// The area to check for collisions in /// A function that determines if a certain info should be included or not /// True if there are any colliders in the area, false otherwise public bool IsColliding(RectangleF area, Func included = null) { return this.GetCollidingTiles(area, included).Any(); } /// /// Returns an enumerable of all of the of the colliding tiles in the given area. /// This method is a convenience method based on . /// /// The area to check for collisions in /// A function that determines if a certain info should be included or not /// An enumerable of collision rectangles for that area public IEnumerable GetCollidingAreas(RectangleF area, Func included = null) { foreach (var tile in this.GetCollidingTiles(area, included)) { foreach (var col in tile.Collisions) yield return col; } } /// /// Returns an enumerable of normals and penetration amounts for each that intersects with the given area. /// The normals and penetration amounts are based on . /// 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 . /// /// The area to penetrate /// A function that determines if a certain info should be included or not /// Whether all x penetrations should be prioritized (returned first). If this is false, all y penetrations are prioritized instead. /// A set of normals and penetration amounts public IEnumerable<(Vector2, float)> GetPenetrations(Func getArea, Func included = null, bool prioritizeX = true) { foreach (var col in this.GetCollidingAreas(getArea(), included)) { if (getArea().Penetrate(col, out var normal, out var penetration) && normal.X != 0 == prioritizeX) yield return (normal, penetration); } foreach (var col in this.GetCollidingAreas(getArea(), included)) { if (getArea().Penetrate(col, out var normal, out var penetration) && normal.X == 0 == prioritizeX) yield return (normal, penetration); } } /// /// A delegate method used to override the default collision checking behavior. /// /// The list of collisions to add to /// The tile's collision information public delegate void CollectCollisions(List collisions, TileCollisionInfo tile); /// /// A tile collision info stores information about a tile at a given location on a given layer, including its objects and their bounds. /// public class TileCollisionInfo : GenericDataHolder { /// /// The map the tile is on /// public readonly TiledMap Map; /// /// The position of the tile, in tile units /// public readonly Vector2 Position; /// /// The tiled map tile /// public readonly TiledMapTile Tile; /// /// The layer that this tile is on /// public readonly TiledMapTileLayer Layer; /// /// The tileset tile for this tile /// public readonly TiledMapTilesetTile TilesetTile; /// /// The list of colliders for this tile /// public readonly List Collisions; internal TileCollisionInfo(TiledMap map, Vector2 position, TiledMapTile tile, TiledMapTileLayer layer, TiledMapTilesetTile tilesetTile, CollectCollisions collisionFunction) { this.TilesetTile = tilesetTile; this.Layer = layer; this.Tile = tile; this.Map = map; this.Position = position; this.Collisions = new List(); collisionFunction(this.Collisions, this); } } } }