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
/// The function used to collect the collision info of a tile, or null to use
public TiledMapCollisions(TiledMap map = null, CollectCollisions collisionFunction = null) {
if (map != null)
this.SetMap(map, collisionFunction);
}
///
/// Sets this collision handler's handled map
///
/// The map
/// The function used to collect the collision info of a tile, or null to use
public void SetMap(TiledMap map, CollectCollisions collisionFunction = null) {
this.map = map;
this.collisionFunction = collisionFunction ?? DefaultCollectCollisions;
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) {
bool DefaultInclusion(TileCollisionInfo tile) {
foreach (var c in tile.Collisions) {
if (c.Intersects(area))
return true;
}
return false;
}
var inclusionFunc = included ?? DefaultInclusion;
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);
}
}
///
/// The default implementation of which is used by if no custom collision collection function is passed
///
/// The list of collisions to add to
/// The tile's collision information
public static void DefaultCollectCollisions(List collisions, TileCollisionInfo tile) {
foreach (var obj in tile.TilesetTile.Objects)
collisions.Add(obj.GetArea(tile.Map, tile.Position, tile.Tile.Flags));
}
///
/// 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);
}
}
}
}