aboutsummaryrefslogtreecommitdiff
path: root/V3/Map
diff options
context:
space:
mode:
Diffstat (limited to 'V3/Map')
-rw-r--r--V3/Map/AbstractLayer.cs259
-rw-r--r--V3/Map/Area.cs119
-rw-r--r--V3/Map/Constants.cs12
-rw-r--r--V3/Map/FloorLayer.cs21
-rw-r--r--V3/Map/FogOfWar.cs107
-rw-r--r--V3/Map/IMapManager.cs88
-rw-r--r--V3/Map/MapManager.cs97
-rw-r--r--V3/Map/ObjectLayer.cs16
-rw-r--r--V3/Map/Pathfinder.cs455
-rw-r--r--V3/Map/PathfindingGrid.cs125
-rw-r--r--V3/Map/SearchNode.cs31
-rw-r--r--V3/Map/TiledParser.cs256
-rw-r--r--V3/Map/Tileset.cs89
13 files changed, 1675 insertions, 0 deletions
diff --git a/V3/Map/AbstractLayer.cs b/V3/Map/AbstractLayer.cs
new file mode 100644
index 0000000..efc626d
--- /dev/null
+++ b/V3/Map/AbstractLayer.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using V3.Camera;
+using V3.Objects;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// A drawable map layer usually created from a Tiled map file.
+ /// </summary>
+ public abstract class AbstractLayer
+ {
+ private const int CellHeight = Constants.CellHeight;
+ private const int CellWidth = Constants.CellWidth;
+
+ private readonly int mTileWidth;
+ private readonly int mTileHeight;
+ private readonly int mMapWidth;
+ private readonly int mMapHeight;
+ private readonly List<IGameObject> mTextureObjects = new List<IGameObject>();
+ private readonly int[,] mTileArray;
+ private readonly SortedList<int, Tileset> mTilesets;
+
+ protected AbstractLayer(int tileWidth,
+ int tileHeight,
+ int mapWidth,
+ int mapHeight,
+ int[,] tileArray,
+ SortedList<int, Tileset> tilesets)
+ {
+ mTileWidth = tileWidth;
+ mTileHeight = tileHeight;
+ mMapWidth = mapWidth;
+ mMapHeight = mapHeight;
+ mTilesets = tilesets;
+ if (tileArray.Length == mapWidth * mapHeight)
+ {
+ mTileArray = tileArray;
+ }
+ else
+ {
+ throw new Exception("Error constructing map layer. Map size does not fit the map description.");
+ }
+ }
+
+ /// <summary>
+ /// Create the map objects according to the given map array.
+ /// </summary>
+ public void CreateObjects()
+ {
+ var firstgridList = mTilesets.Keys;
+ for (int i = 0; i < mMapHeight; i++)
+ {
+ int horizontalOffset = (i % 2 == 0) ? (-mTileWidth / 2) : 0;
+ for (int j = 0; j < mMapWidth; j++)
+ {
+ int tileId = mTileArray[i, j];
+ // Checks which tileset needs to be used for the specific tile ID at position [i, j]
+ for (int k = firstgridList.Count - 1; k >= 0; k--)
+ {
+ if (tileId == 0)
+ {
+ // This does generally nothing. But you can overwrite GenerateNullObject() for other behaviour.
+ TextureObject objectToInsert = GenerateNullObject();
+ if (objectToInsert != null)
+ {
+ mTextureObjects.Add(objectToInsert);
+ }
+ break;
+ }
+ else if (tileId >= firstgridList[k])
+ {
+ Tileset tileset = mTilesets.Values[k];
+ int firstgrid = firstgridList[k];
+ Point position = SelectPosition(j, i, horizontalOffset);
+ Point textureSize = new Point(tileset.TileWidth, tileset.TileHeight);
+ Point destination = SelectDestination(j, i, horizontalOffset, tileset.OffsetX, tileset.TileHeight, tileset.OffsetY);
+ Point source = SelectSource(tileId, firstgrid, tileset.TileWidth, tileset.TileHeight, tileset.Columns);
+ IGameObject objectToInsert;
+ if (tileset.Name == "houses_rear" || tileset.Name == "houses_front")
+ {
+ if (source.Y < textureSize.Y * 2)
+ {
+ int initialDamage = 0;
+ if (source.X / textureSize.X == 1)
+ {
+ initialDamage = 50;
+ }
+ else if (source.X / textureSize.X == 2)
+ {
+ initialDamage = 80;
+ }
+ IBuilding building = new Woodhouse(position.ToVector2(), new Rectangle(destination, textureSize), tileset.Name, source.Y % 384 == 0 ? BuildingFace.SW : BuildingFace.NO);
+ building.TakeDamage(initialDamage);
+ objectToInsert = building;
+ }
+ else
+ {
+ int initialDamage = 0;
+ if (source.X / textureSize.X == 1)
+ {
+ initialDamage = 60;
+ }
+ else if (source.X / textureSize.X == 2)
+ {
+ initialDamage = 100;
+ }
+ IBuilding building = new Forge(position.ToVector2(), new Rectangle(destination, textureSize), tileset.Name, source.Y % 384 == 0 ? BuildingFace.SW : BuildingFace.NO);
+ building.TakeDamage(initialDamage);
+ objectToInsert = building;
+ }
+ }
+ else if (tileset.Name == "castle")
+ {
+ IBuilding building = new Objects.Castle(position.ToVector2(), new Rectangle(destination, textureSize), tileset.Name, BuildingFace.SW);
+ objectToInsert = building;
+ }
+ else
+ {
+ objectToInsert = new TextureObject(position,
+ destination,
+ textureSize,
+ source,
+ tileset.Name);
+ }
+ mTextureObjects.Add(objectToInsert);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ protected virtual TextureObject GenerateNullObject()
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Loads the image files needed for drawing the tilesets.
+ /// </summary>
+ /// <param name="contentManager">Content manager used for loading the ressources.</param>
+ public void LoadContent(ContentManager contentManager)
+ {
+ mTextureObjects.ForEach(o => o.LoadContent(contentManager));
+ }
+
+ /// <summary>
+ /// Draws only the parts of the map which are visible. More efficient than the other Draw-Method.
+ /// Not very robust and maybe does not work correctly most layers.
+ /// This is because of gaps in the list of game objects.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used.</param>
+ /// <param name="camera">Needed to tell which objects of the map are looked upon.</param>
+ public void Draw(SpriteBatch spriteBatch, ICamera camera)
+ {
+ int tilesHorizontal = camera.ScreenSize.X / mTileWidth;
+ int tilesVertical = camera.ScreenSize.Y * 2 / mTileHeight;
+ int horizontalStart = camera.ScreenRectangle.X / mTileWidth;
+ int verticalStart = camera.ScreenRectangle.Y * 2 / mTileHeight;
+ /*
+ for (int i = 0; i < tilesVertical; i++)
+ {
+ for (int j = 0; j < tilesHorizontal; j++)
+ {
+ mTextureObjects[horizontalStart + j].Draw(spriteBatch);
+ }
+ }
+ */
+ for (int j = 0; j < tilesVertical + 2; j++)
+ {
+ for (int i = 0; i < tilesHorizontal + 2; i++)
+ {
+ int index = i + horizontalStart + (j + verticalStart) * mMapWidth;
+ if (index < mTextureObjects.Count)
+ {
+ mTextureObjects[index].Draw(spriteBatch);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Extract a collision grid from the map layer. Used in pathfinding.
+ /// </summary>
+ /// <returns>A two dimensional boolean collision grid.</returns>
+ public bool[,] ExtractCollisions()
+ {
+ int gridHeight = (mMapHeight - 1) * mTileHeight/ CellHeight / 2;
+ int gridWidth = (mMapWidth - 1) * mTileWidth / CellWidth;
+ bool[,] collisionGrid = new bool[gridHeight, gridWidth];
+ var firstgridList = mTilesets.Keys;
+ for (int i = 0; i < mMapHeight; i++)
+ {
+ for (int j = 0; j < mMapWidth; j++)
+ {
+ int tileId = mTileArray[i, j];
+ for (int k = firstgridList.Count - 1; k >= 0; k--)
+ {
+ if (tileId >= firstgridList[k])
+ {
+ Tileset tileset = mTilesets.Values[k];
+ int firstgrid = firstgridList[k];
+ tileId -= firstgrid;
+ bool[,] collisionData;
+ // Is there even collision data for the specific tile ID?
+ if (tileset.TileCollisions.TryGetValue(tileId, out collisionData))
+ {
+ int cellOffset = (i % 2 == 0 ? -mTileWidth / 2 : 0) / CellWidth;
+ int cellsHorizontal = mTileWidth / CellWidth;
+ int cellsVertical = mTileHeight / CellHeight;
+ int iStart = (i - 1) * cellsVertical / 2 + cellsVertical - tileset.CollisionHeight + tileset.OffsetY / CellHeight;
+ int jStart = j * cellsHorizontal + cellOffset + tileset.OffsetX / CellWidth;
+ for (int iData = 0; iData < tileset.CollisionHeight; iData++)
+ {
+ for (int jData = 0; jData < tileset.CollisionWidth; jData++)
+ {
+ // Do we even need to update collisionGrid?
+ if (iStart + iData >= 0 && iStart + iData < gridHeight && jStart + jData >= 0 && jStart + jData < gridWidth &&
+ collisionData[iData, jData] && !collisionGrid[iStart + iData, jStart + jData])
+ {
+ collisionGrid[iStart + iData, jStart + jData] = collisionData[iData, jData];
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ return collisionGrid;
+ }
+
+ public List<IGameObject> ExtractObjects()
+ {
+ return mTextureObjects;
+ }
+
+ private Point SelectDestination(int x, int y, int xOffset, int tileXOffset, int tileHeight, int tileYOffset)
+ {
+ return new Point(x * mTileWidth + xOffset + tileXOffset,
+ (y - 1) * (mTileHeight / 2) - tileHeight + mTileHeight + tileYOffset);
+ }
+
+ private Point SelectSource(int tileId, int firstgrid, int tileWidth, int tileHeight, int tilesPerRow)
+ {
+ return new Point((tileId - firstgrid) % tilesPerRow * tileWidth, (tileId - firstgrid) / tilesPerRow * tileHeight);
+ }
+
+ private Point SelectPosition(int x, int y, int xOffset)
+ {
+ return new Point(x * mTileWidth + xOffset + mTileWidth / 2, y * (mTileHeight / 2));
+ }
+ }
+} \ No newline at end of file
diff --git a/V3/Map/Area.cs b/V3/Map/Area.cs
new file mode 100644
index 0000000..5712c6e
--- /dev/null
+++ b/V3/Map/Area.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using V3.Objects;
+
+namespace V3.Map
+{
+ public enum AreaType
+ {
+ Village, // has no soldiers or knights, only peasants
+ Castle, // many knights patrolling around
+ Graveyard // here you can respawn zombies
+ }
+
+ /// <summary>
+ /// Holding area data of the map. Later used for generating enemies.
+ /// </summary>
+ public sealed class Area
+ {
+ private const int DistanceHorizontal = 64;
+ private const int DistanceVertical = 32;
+ private readonly AreaType mType;
+ private Rectangle mArea;
+ private readonly double mDensity;
+ private readonly double mChance;
+
+ /// <summary>
+ /// Gets the area type.
+ /// </summary>
+ public AreaType Type => mType;
+
+ /// <summary>
+ /// Creates a new area for generating population.
+ /// </summary>
+ /// <param name="type">Which type of area. Determines which population is spawned.</param>
+ /// <param name="data">The size and position of the area.</param>
+ /// <param name="density">The population density. Together with chance.</param>
+ /// <param name="chance">The chance that a creature is actually created.</param>
+ /// <param name="name">Name of the area as shown in the game.</param>
+ // ReSharper disable once UnusedParameter.Local
+ public Area(string type, Rectangle data, double density = 0d, double chance = 0d, string name = "")
+ {
+ switch (type)
+ {
+ case "village":
+ mType = AreaType.Village;
+ break;
+ case "castle":
+ mType = AreaType.Castle;
+ break;
+ case "graveyard":
+ mType = AreaType.Graveyard;
+ break;
+ default:
+ throw new Exception("Error parsing the map. There is no behaviour defined for objects of type " + type + ".");
+ }
+ if (density > 1d || chance > 1d || density < 0d || chance < 0d)
+ {
+ throw new Exception("Error when parsing area data from map. Density and/or chance is not in range 0.0 to 1.0.");
+ }
+ mArea = data;
+ mDensity = density;
+ mChance = chance;
+ }
+
+ /// <summary>
+ /// Creates the initial population for this area.
+ /// </summary>
+ /// <param name="creatureFactory">The factory used for creating creatures.</param>
+ /// <param name="pathfinder">Used for checking collisions when creating population.</param>
+ /// <returns></returns>
+ public List<ICreature> GetPopulation(CreatureFactory creatureFactory, Pathfinder pathfinder)
+ {
+ var population = new List<ICreature>();
+ if (mDensity <= 0) return population; // Catch division by zero.
+ var rndInt = new Random();
+ var rnd = new Random();
+ for (double i = DistanceVertical / mDensity + mArea.Y; i < mArea.Height + mArea.Y; i += DistanceVertical / mDensity )
+ {
+ for (double j = DistanceHorizontal / mDensity + mArea.X; j < mArea.Width + mArea.X; j += DistanceHorizontal / mDensity )
+ {
+ if (mChance < rnd.NextDouble()) continue;
+ var position = new Vector2((float) j, (float) i);
+ if (mType == AreaType.Village)
+ {
+ ICreature peasant;
+ if (rnd.NextDouble() < 0.5d)
+ {
+ peasant = creatureFactory.CreateMalePeasant(position, (MovementDirection)rndInt.Next(8));
+ }
+ else
+ {
+ peasant = creatureFactory.CreateFemalePeasant(position, (MovementDirection)rndInt.Next(8));
+ }
+ if (!pathfinder.AllWalkable(peasant.BoundaryRectangle)) continue;
+ population.Add(peasant);
+ }
+ else if (mType == AreaType.Castle)
+ {
+ ICreature guard = creatureFactory.CreateKingsGuard(position, (MovementDirection)rndInt.Next(8));
+ if (!pathfinder.AllWalkable(guard.BoundaryRectangle)) continue;
+ population.Add(guard);
+ }
+ }
+ }
+ return population;
+ }
+
+ /// <summary>
+ /// Is a given creature standing in the area?
+ /// </summary>
+ /// <param name="creature">Check for this creature.</param>
+ /// <returns></returns>
+ public bool Contains(ICreature creature)
+ {
+ return mArea.Contains(creature.Position.ToPoint());
+ }
+ }
+} \ No newline at end of file
diff --git a/V3/Map/Constants.cs b/V3/Map/Constants.cs
new file mode 100644
index 0000000..2de2e25
--- /dev/null
+++ b/V3/Map/Constants.cs
@@ -0,0 +1,12 @@
+namespace V3.Map
+{
+ /// <summary>
+ /// Constants for describing the size of the cells of the collision grid.
+ /// Important for pathfinding.
+ /// </summary>
+ public static class Constants
+ {
+ public const int CellHeight = 16;
+ public const int CellWidth = 16;
+ }
+} \ No newline at end of file
diff --git a/V3/Map/FloorLayer.cs b/V3/Map/FloorLayer.cs
new file mode 100644
index 0000000..ebe05f0
--- /dev/null
+++ b/V3/Map/FloorLayer.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using V3.Objects;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// The floor of the map consisting of the ground to walk on, grass or water.
+ /// </summary>
+ public sealed class FloorLayer : AbstractLayer
+ {
+ public FloorLayer(int tileWidth, int tileHeight, int mapWidth, int mapHeight, int[,] tileArray, SortedList<int, Tileset> tilesets)
+ : base(tileWidth, tileHeight, mapWidth, mapHeight, tileArray, tilesets)
+ {
+ }
+
+ protected override TextureObject GenerateNullObject()
+ {
+ return new TextureObject();
+ }
+ }
+}
diff --git a/V3/Map/FogOfWar.cs b/V3/Map/FogOfWar.cs
new file mode 100644
index 0000000..a243b60
--- /dev/null
+++ b/V3/Map/FogOfWar.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using V3.Objects;
+
+namespace V3.Map
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class FogOfWar
+ {
+ private const int FogRange = 64;
+ private const int SightRadius = 1000;
+ private Point mMapSize;
+ private Texture2D mFog;
+ private readonly List<Rectangle> mFogRectangle = new List<Rectangle>();
+
+ /// <summary>
+ /// Get the size of the map and create an boolean array
+ /// </summary>
+ /// <param name="size">the size of the map</param>
+ public void LoadGrid(Point size)
+ {
+ mMapSize = size;
+ CreateArray();
+ }
+
+ /// <summary>
+ /// An array so save whether the sprites already walked on this area
+ /// </summary>
+ private void CreateArray()
+ {
+ for (int i = -FogRange; i < mMapSize.Y; i += FogRange)
+ {
+ for (int j = -FogRange * 2; j < mMapSize.X; j += FogRange)
+ {
+ mFogRectangle.Add(new Rectangle(j, i, mFog.Width, mFog.Height));
+ }
+ }
+ }
+
+ /// <summary>
+ /// The position from creatures which can open the fog
+ /// </summary>
+ /// <param name="creature">creatures which are able to open the fog</param>
+ public void Update(ICreature creature)
+ {
+ Ellipse creatureEllipse = new Ellipse(creature.Position, SightRadius, SightRadius);
+ var markedForDeletion = new List<Rectangle>();
+ foreach (var fog in mFogRectangle)
+ {
+ if (!creature.IsDead && creatureEllipse.Contains(fog.Center.ToVector2()))
+ {
+ markedForDeletion.Add(fog);
+ }
+ }
+ foreach (var fogToDelete in markedForDeletion)
+ {
+ mFogRectangle.Remove(fogToDelete);
+ }
+ }
+
+ /// <summary>
+ /// The sprite for the fog
+ /// </summary>
+ /// <param name="content"></param>
+ public void LoadContent(ContentManager content)
+ {
+ mFog = content.Load<Texture2D>("Sprites/cloud");
+ }
+
+ /// <summary>
+ /// Try to draw fog of war efficiently.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used.</param>
+ public void DrawFog(SpriteBatch spriteBatch)
+ {
+ /*
+ var screen = camera.ScreenRectangle;
+ int fogPerRow = (mMapSize.X + FogRange) / FogRange;
+ int fogPerColumn = (mMapSize.Y + FogRange) / FogRange;
+ for (int i = screen.Y / FogRange; i < (screen.Y + screen.Height) / FogRange; i++)
+ {
+ for (int j = screen.X / FogRange; j < (screen.X + screen.Width) / FogRange; j++)
+ {
+ spriteBatch.Draw(mFog, mFogRectangle[i * fogPerRow], Color.Black);
+ }
+ }
+ */
+ foreach (var fog in mFogRectangle)
+ {
+ spriteBatch.Draw(mFog, fog, Color.Black);
+ }
+ }
+
+ public void SetFog(List<Rectangle> fog)
+ {
+ mFogRectangle.Clear();
+ mFogRectangle.AddRange(fog);
+ }
+
+ public List<Rectangle> GetFog()
+ {
+ return mFogRectangle;
+ }
+ }
+}
diff --git a/V3/Map/IMapManager.cs b/V3/Map/IMapManager.cs
new file mode 100644
index 0000000..9333d63
--- /dev/null
+++ b/V3/Map/IMapManager.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using V3.Camera;
+using V3.Objects;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// Manager for loading and drawing game maps. Also holds information about map attributes.
+ /// </summary>
+ [SuppressMessage("ReSharper", "UnusedMember.Global")]
+ public interface IMapManager
+ {
+ /// <summary>
+ /// A list of map areas (rectangle-sized).
+ /// </summary>
+ List<Area> Areas { get; }
+
+ /// <summary>
+ /// Size of the shown map in pixels.
+ /// </summary>
+ Point SizeInPixel { get; }
+ /// <summary>
+ /// Size of the map in tiles. (Some tiles are cut off at the edges.)
+ /// </summary>
+ Point SizeInTiles { get; }
+ /// <summary>
+ /// Size of a single tile in pixels.
+ /// </summary>
+ Point TileSize { get; }
+ /// <summary>
+ /// Number of cells the pathfinding grid consists of.
+ /// </summary>
+ Point PathfindingGridSize { get; }
+ /// <summary>
+ /// Size of a single cell of the pathfinding grid in pixels.
+ /// </summary>
+ Point PathfindingCellSize { get; }
+ /// <summary>
+ /// File name of the loaded map (without suffix).
+ /// </summary>
+ string FileName { get; }
+ /// <summary>
+ /// Efficiently draw the floor layer. Only draw the tiles seen by the camera.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="camera"></param>
+ void DrawFloor(SpriteBatch spriteBatch, ICamera camera);
+ /// <summary>
+ /// Load a map file and create the map layers and pathfinding information.
+ /// </summary>
+ /// <param name="fileName">Name of the map file (without suffix).</param>
+ void Load(string fileName);
+ /// <summary>
+ /// Returns all objects in the objects layer.
+ /// </summary>
+ /// <returns>List of all static game objects imported from the map.</returns>
+ List<IGameObject> GetObjects();
+ /// <summary>
+ /// Returns the pathfinding grid for passing to the pathfinder.
+ /// </summary>
+ /// <returns>A grid used for pathfinding.</returns>
+ PathfindingGrid GetPathfindingGrid();
+ /// <summary>
+ /// Efficiently draw the pathfinding grid. For debugging purposes.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used.</param>
+ /// <param name="camera">Current camera for calculating the shown screen.</param>
+ void DrawPathfindingGrid(SpriteBatch spriteBatch, ICamera camera);
+
+ /// <summary>
+ /// Draws the minimap to specified position.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used.</param>
+ /// <param name="position">Where to draw the minimap and which size.</param>
+ void DrawMinimap(SpriteBatch spriteBatch, Rectangle position);
+
+ /// <summary>
+ /// Automatically creates an initial population from the map data and returns it.
+ /// </summary>
+ /// <param name="creatureFactory">Factory for creating creatues.</param>
+ /// <param name="pathfinder">Pathfinder is used for checking collisions when creating creatures.</param>
+ /// <returns>Initial population in a list.</returns>
+ List<ICreature> GetPopulation(CreatureFactory creatureFactory, Pathfinder pathfinder);
+ }
+} \ No newline at end of file
diff --git a/V3/Map/MapManager.cs b/V3/Map/MapManager.cs
new file mode 100644
index 0000000..f62278c
--- /dev/null
+++ b/V3/Map/MapManager.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using V3.Camera;
+using V3.Objects;
+
+namespace V3.Map
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public class MapManager : IMapManager
+ {
+ private TiledParser mTiledParser;
+ private FloorLayer mFloorLayer;
+ private ObjectLayer mObjectLayer;
+ private List<Area> mAreas;
+ private PathfindingGrid mPathfindingGrid;
+ private readonly ContentManager mContentManager;
+ private readonly GraphicsDeviceManager mGraphicsDeviceManager;
+
+ public List<Area> Areas => mAreas;
+
+ public Point SizeInPixel { get; private set; }
+ public Point SizeInTiles { get; private set; }
+ public Point TileSize { get; private set; }
+ public Point PathfindingGridSize { get; private set; }
+ public Point PathfindingCellSize { get; private set; }
+ public string FileName { get; private set; }
+
+ public MapManager(ContentManager contentManager, GraphicsDeviceManager graphicsDeviceManager)
+ {
+ mContentManager = contentManager;
+ mGraphicsDeviceManager = graphicsDeviceManager;
+ }
+
+ public void DrawFloor(SpriteBatch spriteBatch, ICamera camera)
+ {
+ mFloorLayer.Draw(spriteBatch, camera);
+ }
+
+ public void Load(string fileName)
+ {
+ mTiledParser = new TiledParser();
+ // Parse map data.
+ mTiledParser.Parse(fileName);
+ FileName = fileName;
+ TileSize = new Point(mTiledParser.TileWidth, mTiledParser.TileHeight);
+ SizeInTiles = new Point(mTiledParser.MapWidth, mTiledParser.MapHeight);
+ SizeInPixel = new Point((SizeInTiles.X - 1) * TileSize.X, SizeInTiles.Y / 2 * TileSize.Y - TileSize.Y / 2);
+ // Create floor layer of the map.
+ mFloorLayer = new FloorLayer(mTiledParser.TileWidth, mTiledParser.TileHeight, mTiledParser.MapWidth, mTiledParser.MapHeight, mTiledParser.MapLayers[0], mTiledParser.TileSets);
+ mFloorLayer.CreateObjects();
+ mFloorLayer.LoadContent(mContentManager);
+ // Create object layer of the map.
+ mObjectLayer = new ObjectLayer(mTiledParser.TileWidth, mTiledParser.TileHeight, mTiledParser.MapWidth, mTiledParser.MapHeight, mTiledParser.MapLayers[1], mTiledParser.TileSets);
+ mObjectLayer.CreateObjects();
+ mObjectLayer.LoadContent(mContentManager);
+ // Get areas from the map
+ mAreas = mTiledParser.Areas;
+ // Create pathfinding grid used in the pathfinder.
+ mPathfindingGrid = new PathfindingGrid(mTiledParser.MapWidth, mTiledParser.MapHeight, mTiledParser.TileWidth, mTiledParser.TileHeight);
+ mPathfindingGrid.LoadContent(mContentManager);
+ mPathfindingGrid.CreateCollisions(mFloorLayer.ExtractCollisions());
+ mPathfindingGrid.CreateCollisions(mObjectLayer.ExtractCollisions());
+ PathfindingGridSize = new Point(mPathfindingGrid.mGridWidth, mPathfindingGrid.mGridHeight);
+ PathfindingCellSize = new Point(Constants.CellWidth, Constants.CellHeight);
+ // Create Minimap texture from pathfinding grid.
+ mPathfindingGrid.CreateMinimap(mGraphicsDeviceManager.GraphicsDevice);
+ }
+
+ public List<IGameObject> GetObjects()
+ {
+ return mObjectLayer.ExtractObjects();
+ }
+
+ public List<ICreature> GetPopulation(CreatureFactory creatureFactory, Pathfinder pathfinder)
+ {
+ return mAreas.SelectMany(area => area.GetPopulation(creatureFactory, pathfinder)).ToList();
+ }
+
+ public PathfindingGrid GetPathfindingGrid()
+ {
+ return mPathfindingGrid;
+ }
+
+ public void DrawPathfindingGrid(SpriteBatch spriteBatch, ICamera camera)
+ {
+ mPathfindingGrid.Draw(spriteBatch, camera);
+ }
+
+ public void DrawMinimap(SpriteBatch spriteBatch, Rectangle position)
+ {
+ mPathfindingGrid.DrawSmallGrid(spriteBatch, position);
+ }
+ }
+} \ No newline at end of file
diff --git a/V3/Map/ObjectLayer.cs b/V3/Map/ObjectLayer.cs
new file mode 100644
index 0000000..0e9d13c
--- /dev/null
+++ b/V3/Map/ObjectLayer.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// The map objects which are the same layer as the moving creatutes.
+ /// Buildings, flowers, trees etc.
+ /// </summary>
+ public sealed class ObjectLayer : AbstractLayer
+ {
+ public ObjectLayer(int tileWidth, int tileHeight, int mapWidth, int mapHeight, int[,] tileArray, SortedList<int, Tileset> tilesets)
+ : base(tileWidth, tileHeight, mapWidth, mapHeight, tileArray, tilesets)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/V3/Map/Pathfinder.cs b/V3/Map/Pathfinder.cs
new file mode 100644
index 0000000..3874d0b
--- /dev/null
+++ b/V3/Map/Pathfinder.cs
@@ -0,0 +1,455 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace V3.Map
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public class Pathfinder
+ {
+ private const int CellHeight = Constants.CellHeight;
+ private const int CellWidth = Constants.CellWidth;
+
+ // An array of walkable search nodes
+ private SearchNode[,] mSearchNodes;
+
+ // The width of the map
+ private int mLevelWidth;
+
+ // the height of the map
+ private int mLevelHeight;
+
+ // List for nodes that are available to search
+ private readonly List<SearchNode> mOpenList = new List<SearchNode>();
+
+ // List for nodes that are NOT available to search
+ private readonly List<SearchNode> mClosedList = new List<SearchNode>();
+
+ //Calculates the distance between two (vector)points
+ private float Heuristic(Vector2 position, Vector2 goal)
+ {
+ return (goal - position).Length(); // Manhattan distance
+ }
+
+ public void LoadGrid(PathfindingGrid map)
+ {
+ mLevelWidth = map.mGridWidth;
+ mLevelHeight = map.mGridHeight;
+ InitializeSearchNodes(map);
+ }
+
+ private void InitializeSearchNodes(PathfindingGrid map)
+ {
+ mSearchNodes = new SearchNode[mLevelWidth, mLevelHeight];
+
+ // Creates a searchnode for each tile
+ for (int x = 0; x < mLevelWidth; x++)
+ {
+ for (int y = 0; y < mLevelHeight; y++)
+ {
+ SearchNode node = new SearchNode();
+
+ node.mPosition = new Vector2(x, y);
+
+ // Walk only on walkable tiles
+ node.mWalkable = map.GetIndex(x, y) == 0;
+
+ // Stores nodes that can be walked on
+ if (node.mWalkable)
+ {
+ node.mNeighbors = new SearchNode[4];
+ mSearchNodes[x, y] = node;
+ }
+ }
+ }
+
+ for (int x = 0; x < mLevelWidth; x++)
+ {
+ for (int y = 0; y < mLevelHeight; y++)
+ {
+ SearchNode node = mSearchNodes[x, y];
+
+ // Note only walkable nodes
+ if (node == null || node.mWalkable == false)
+ continue;
+
+
+ // The neighbors for every node
+ Vector2[] neighbors =
+ {
+ new Vector2(x, y - 1), // Node above the current
+ new Vector2(x, y + 1), // Node below the current
+ new Vector2(x - 1, y), // Node to the left
+ new Vector2(x + 1, y) // Node to the right
+ };
+
+ for (int i = 0; i < neighbors.Length; i++)
+ {
+ Vector2 position = neighbors[i];
+
+ // Test whether this neighbor is part of the map
+ if (position.X < 0 || position.X > mLevelWidth - 1 || position.Y < 0 ||
+ position.Y > mLevelHeight - 1)
+ continue;
+
+ SearchNode neighbor = mSearchNodes[(int)position.X, (int)position.Y];
+
+ // Keep a reference to the nodes that can be walked on
+ if (neighbor == null || neighbor.mWalkable == false)
+ continue;
+
+ // A reference to the neighbor
+ node.mNeighbors[i] = neighbor;
+ }
+ }
+ }
+ }
+
+ // Reset the state of the search node
+ private void ResetSearchNodes()
+ {
+ mOpenList.Clear();
+ mClosedList.Clear();
+
+ for (int x = 0; x < mLevelWidth; x++)
+ {
+ for (int y = 0; y < mLevelHeight; y++)
+ {
+ SearchNode node = mSearchNodes[x, y];
+
+ if (node == null)
+ continue;
+
+ node.mInOpenList = false;
+ node.mInClosedList = false;
+ node.mDistanceTraveled = float.MaxValue;
+ node.mDistanceToGoal = float.MaxValue;
+ }
+ }
+ }
+
+ // Returns the node with the smallest distance
+ private SearchNode FindBestNode()
+ {
+ SearchNode currentTile = mOpenList[0];
+
+ float smallestDistanceToGoal = float.MaxValue;
+
+ // Find the closest node to the goal
+ for (int i = 0; i < mOpenList.Count; i++)
+ {
+ if (mOpenList[i].mDistanceToGoal < smallestDistanceToGoal)
+ {
+ currentTile = mOpenList[i];
+ smallestDistanceToGoal = currentTile.mDistanceToGoal;
+ }
+ }
+ return currentTile;
+ }
+
+ // Use parent field to trace a path from search node to start node
+ private List<Vector2> FindFinalPath(SearchNode startNode, SearchNode endNode)
+ {
+ int counter = 0;
+
+ if (startNode == endNode)
+ {
+ return new List<Vector2>();
+ }
+
+ mClosedList.Add(endNode);
+
+ SearchNode parentTile = endNode.mParent;
+
+ // Find the best path
+ while (parentTile != startNode)
+ {
+ mClosedList.Add(parentTile);
+ parentTile = parentTile.mParent;
+ }
+
+ // Path from position to goal (from tile to tile)
+ List<Vector2> betaPath = new List<Vector2>();
+
+ // Final path after RayCasting
+ List<Vector2> finalPath = new List<Vector2>();
+
+ // Reverse the path and transform into the map
+ for (int i = mClosedList.Count - 1; i >= 0; i--)
+ {
+ betaPath.Add(new Vector2(mClosedList[i].mPosition.X * CellWidth + 8, mClosedList[i].mPosition.Y * CellHeight + 8));
+ }
+
+ // Short the path via RayCasting
+ for (int i = 1; i < betaPath.Count;)
+ {
+ if (!RayCast(betaPath[counter], betaPath[i]))
+ {
+ finalPath.Add(betaPath[i - 1]);
+ counter = i - 1;
+ }
+ else
+ {
+ i++;
+ }
+ }
+ finalPath.Add(betaPath[betaPath.Count - 1]);
+ return finalPath;
+ }
+
+ //Test Points
+ private Vector2 CheckStartNode(Vector2 startNode)
+ {
+ var start = startNode;
+
+ var startXPos = startNode;
+ var startXNeg = startNode;
+ var startYPos = startNode;
+ var startYNeg = startNode;
+
+ // When sprite is blocked out of map, he returns to the edge of the map
+ if (startNode.X > mLevelWidth - 2)
+ startNode.X = mLevelWidth - 2;
+ if (startNode.X < 2)
+ startNode.X = 2;
+ if (startNode.Y < 4)
+ startNode.Y = 4;
+ if (startNode.Y > mLevelHeight - 2)
+ startNode.Y = mLevelHeight - 2;
+
+ // When sprite stays on a null-position, he goes to the nearest non null-position around that null-position
+ while (mSearchNodes[(int)start.X, (int)start.Y] == null)
+ {
+ if (startXPos.X < mLevelWidth)
+ startXPos.X++;
+ if (startXNeg.X > 0)
+ startXNeg.X--;
+ if (startYPos.Y < mLevelHeight)
+ startYPos.Y++;
+ if (startYNeg.Y > 0)
+ startYNeg.Y--;
+
+ if (mSearchNodes[(int)startXPos.X, (int)start.Y] != null)
+ {
+ start.X = startXPos.X;
+ return start;
+ }
+ if (mSearchNodes[(int)startXNeg.X, (int)start.Y] != null)
+ {
+ start.X = startXNeg.X;
+ return start;
+ }
+ if (mSearchNodes[(int)start.X, (int)startYPos.Y] != null)
+ {
+ start.Y = startYPos.Y;
+ return start;
+ }
+ if (mSearchNodes[(int)start.X, (int)startYNeg.Y] != null)
+ {
+ start.Y = startYNeg.Y;
+ return start;
+ }
+ }
+ return start;
+ }
+
+ private Vector2 CheckEndNode(Vector2 endNode)
+ {
+ var end = endNode;
+
+ var endXPos = endNode;
+ var endXNeg = endNode;
+ var endYPos = endNode;
+ var endYNeg = endNode;
+
+ // When goal is null-position, the goal will be the nearest non null-position around that null-position
+ while (mSearchNodes[(int) end.X, (int) end.Y] == null)
+ {
+ if(endXPos.X < mLevelWidth - 3)
+ endXPos.X++;
+ if(endXNeg.X > 0)
+ endXNeg.X--;
+ if(endYPos.Y < mLevelHeight - 3)
+ endYPos.Y++;
+ if(endYNeg.Y > 0)
+ endYNeg.Y--;
+
+ if (endXPos.X > mLevelWidth - 3)
+ break;
+ if (endXNeg.X < 0)
+ break;
+ if (endYPos.Y > mLevelHeight - 3)
+ break;
+ if (endYNeg.Y < 0)
+ break;
+
+ if (mSearchNodes[(int)endXPos.X, (int)end.Y] != null)
+ {
+ end.X = endXPos.X;
+ return end;
+ }
+ if (mSearchNodes[(int)endXNeg.X, (int)end.Y] != null)
+ {
+ end.X = endXNeg.X;
+ return end;
+ }
+ if (mSearchNodes[(int)end.X, (int)endYPos.Y] != null)
+ {
+ end.Y = endYPos.Y;
+ return end;
+ }
+ if (mSearchNodes[(int)end.X, (int)endYNeg.Y] != null)
+ {
+ end.Y = endYNeg.Y;
+ return end;
+ }
+ }
+ return end;
+ }
+
+ // Finds the best path
+ public List<Vector2> FindPath(Vector2 startPoint, Vector2 endPoint)
+ {
+ // Start to find path if startpoint and endpoint are different
+ if (startPoint == endPoint)
+ {
+ return new List<Vector2>();
+ }
+
+ // Sprite don't walk out of the map
+ if (endPoint.Y > mLevelHeight - 2 || endPoint.Y < 4 || endPoint.X > mLevelWidth - 2 || endPoint.X < 2)
+ {
+ return new List<Vector2>();
+ }
+
+ // Test nodes for their validity
+ startPoint = CheckStartNode(startPoint);
+ endPoint = CheckEndNode(endPoint);
+
+ /*
+ * Clear the open and closed lists.
+ * reset each's node F and G values
+ */
+ ResetSearchNodes();
+
+ // Store references to the start and end nodes for convenience
+ SearchNode startNode = mSearchNodes[(int)startPoint.X, (int)startPoint.Y];
+ SearchNode endNode = mSearchNodes[(int)endPoint.X, (int)endPoint.Y];
+
+ /*
+ * Set the start node’s G value to 0 and its F value to the
+ * estimated distance between the start node and goal node
+ * (this is where our H function comes in) and add it to the open list
+ */
+ if (startNode != null)
+ {
+ startNode.mInOpenList = true;
+
+ startNode.mDistanceToGoal = Heuristic(startPoint, endPoint);
+ startNode.mDistanceTraveled = 0;
+
+ mOpenList.Add(startNode);
+ }
+
+ /*
+ * While the OpenList is not empty:
+ */
+ while (mOpenList.Count > 0)
+ {
+ // Loop the open list and find the node with the smallest F value
+ SearchNode currentNode = FindBestNode();
+
+ // If the open list ist empty or a node can't be found
+ if (currentNode == null)
+ break;
+
+ // If the active node ist the goal node, we will find and return the path
+ if (currentNode == endNode)
+ return FindFinalPath(startNode, endNode); // Trace our path back to the start
+
+ // Else, for each of the active node's neighbors
+ for (int i = 0; i < currentNode.mNeighbors.Length; i++)
+ {
+ SearchNode neighbor = currentNode.mNeighbors[i];
+
+ // Make sure that the neighbor can be walked on
+ if (neighbor == null || !neighbor.mWalkable)
+ continue;
+
+ // Calculate a new G Value for the neighbors node
+ float distanceTraveled = currentNode.mDistanceTraveled + 1;
+
+ // An estimate of t he distance from this node to the end node
+ float heuristic = Heuristic(neighbor.mPosition, endPoint);
+
+ if (!neighbor.mInOpenList && !neighbor.mInClosedList)
+ {
+ // Set the neighbors node G value to the G value
+ neighbor.mDistanceTraveled = distanceTraveled;
+
+ // Set the neighboring node's F value to the new G value + the estimated
+ // distance between the neighbouring node and goal node
+ neighbor.mDistanceToGoal = distanceTraveled + heuristic;
+
+ // The neighbouring node's mParent property to point at the active node
+ neighbor.mParent = currentNode;
+
+ // Add the neighboring node to the open list
+ neighbor.mInOpenList = true;
+ mOpenList.Add(neighbor);
+ }
+
+ // Else if the neighboring node is in open or closed list
+ else if (neighbor.mInOpenList || neighbor.mInClosedList)
+ {
+ if (neighbor.mDistanceTraveled > distanceTraveled)
+ {
+ neighbor.mDistanceTraveled = distanceTraveled;
+ neighbor.mDistanceToGoal = distanceTraveled + heuristic;
+
+ neighbor.mParent = currentNode;
+ }
+ }
+ }
+
+ // Remove active node from the open list and add to the closed list
+ mOpenList.Remove(currentNode);
+ currentNode.mInOpenList = true;
+
+ }
+
+ // No path could be found
+ return new List<Vector2>();
+ }
+
+ // Check whether an area is completely walkable in given rectangle
+ public bool AllWalkable(Rectangle rectangle)
+ {
+ for (int x = rectangle.X; x <= rectangle.X + rectangle.Width; x++)
+ {
+ for (int y = rectangle.Y; y <= rectangle.Y + rectangle.Height; y++)
+ {
+ if (mSearchNodes[x / 16, y / 16] == null)
+ return false;
+ }
+ }
+ return true;
+ }
+
+ //Raycasting
+ private bool RayCast(Vector2 start, Vector2 goal)
+ {
+ var direction = goal - start;
+ var currentPos = start;
+ direction.Normalize();
+ //direction = direction * 8;
+
+ while (Vector2.Distance(currentPos, goal) > 1f)
+ {
+ if (mSearchNodes[(int)currentPos.X / 16, (int)currentPos.Y / 16] == null)
+ return false;
+ currentPos += direction;
+ }
+ return true;
+ }
+ }
+}
diff --git a/V3/Map/PathfindingGrid.cs b/V3/Map/PathfindingGrid.cs
new file mode 100644
index 0000000..266ea02
--- /dev/null
+++ b/V3/Map/PathfindingGrid.cs
@@ -0,0 +1,125 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using V3.Camera;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// Tells the pathfinder where you can walk.
+ /// </summary>
+ public sealed class PathfindingGrid
+ {
+ private const int CellHeight = Constants.CellHeight;
+ private const int CellWidth = Constants.CellWidth;
+
+ private readonly bool[,] mArray;
+ public readonly int mGridWidth;
+ public readonly int mGridHeight;
+ private Texture2D mTexture;
+ private Texture2D mMinimapTexture;
+
+ public PathfindingGrid(int mapWidth, int mapHeight, int tileWidth, int tileHeight)
+ {
+ mGridHeight = (mapHeight - 1) * tileHeight / CellHeight / 2;
+ mGridWidth = (mapWidth - 1) * tileWidth / CellWidth;
+ mArray = new bool[mGridHeight, mGridWidth];
+ }
+
+ /// <summary>
+ /// Compares the pathfinding grid with the given collision grid and adjusts the former.
+ /// If a cell of the pathfinding grid is false and the cell at the same position of the
+ /// collision grid is true, switch false to true.
+ /// </summary>
+ /// <param name="collisionGrid">A grid of the same size as the pathfinding grid.</param>
+ public void CreateCollisions(bool[,] collisionGrid)
+ {
+ if (collisionGrid.Length == mGridWidth * mGridHeight)
+ {
+ for (int i = 0; i < mGridHeight; i++)
+ {
+ for (int j = 0; j < mGridWidth; j++)
+ {
+ if (!mArray[i, j])
+ {
+ mArray[i, j] = collisionGrid[i, j];
+ }
+ }
+ }
+ }
+ else
+ {
+ throw new Exception("Error creating the collision grid. Object layer data and collision grid data do not fit.");
+ }
+ }
+
+ /// <summary>
+ /// Load content for visual representation of the pathfinding grid.
+ /// </summary>
+ /// <param name="contentManager">Use this content manager.</param>
+ public void LoadContent(ContentManager contentManager)
+ {
+ mTexture = contentManager.Load<Texture2D>("Textures/pathfinder");
+ //mOnePixelTexture = contentManager.Load<Texture2D>("Sprites/WhiteRectangle");
+ }
+
+ /// <summary>
+ /// A visual representation of the pathfinding grid. Drawn efficiently.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used for drawing.</param>
+ /// <param name="camera">For only drawing on the shown part of the map.</param>
+ public void Draw(SpriteBatch spriteBatch, ICamera camera)
+ {
+ Point startPosition = camera.Location.ToPoint() / new Point(CellWidth, CellHeight);
+ Point tilesOnScreen = camera.ScreenSize / new Point(CellWidth, CellHeight) + new Point(1, 1) + startPosition;
+ for (int i = startPosition.Y; i < tilesOnScreen.Y && i < mGridHeight; i++)
+ {
+ for (int j = startPosition.X; j < tilesOnScreen.X && j < mGridWidth; j++)
+ {
+ Rectangle destinationRectangle = new Rectangle(j * CellWidth, i * CellHeight, CellWidth, CellHeight);
+ Rectangle sourceRectangle = new Rectangle(mArray[i, j] ? CellWidth : 0, 0, CellWidth, CellHeight);
+ spriteBatch.Draw(mTexture, destinationRectangle, sourceRectangle, Color.White);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the value at the specified position of the collision array.
+ /// </summary>
+ /// <param name="cellX">Position at the horizontal axis.</param>
+ /// <param name="cellY">Position at the vertical axis.</param>
+ /// <returns>Returns 0 if you can walk at the specified position, 1 otherwise.</returns>
+ public int GetIndex(int cellX, int cellY)
+ {
+ //if (cellX < 0 || cellX > mGridWidth - 1 || cellY < 0 || cellY > mGridHeight - 1)
+ // return 0;
+ return mArray[cellY, cellX] ? 1 : 0;
+ }
+
+ /// <summary>
+ /// Draws a small version of the pathfinding grid to the screen.
+ /// Useful for the minimap.
+ /// </summary>
+ /// <param name="spriteBatch">Sprite batch used.</param>
+ /// <param name="position">Where to draw in pixel coordinates and which size. In pixels.</param>
+ public void DrawSmallGrid(SpriteBatch spriteBatch, Rectangle position)
+ {
+ spriteBatch.Draw(mMinimapTexture, position, Color.White);
+ }
+
+ public void CreateMinimap(GraphicsDevice device)
+ {
+ Color[] colors = new Color[mGridWidth * mGridHeight];
+ for (int i = 0; i < mGridHeight; i++)
+ {
+ for (int j = 0; j < mGridWidth; j++)
+ {
+ colors[i * mGridWidth + j ] = mArray[i, j] ? Color.DarkGray : Color.Green;
+ }
+ }
+ mMinimapTexture = new Texture2D(device, mGridWidth, mGridHeight);
+ mMinimapTexture.SetData(colors);
+ }
+ }
+} \ No newline at end of file
diff --git a/V3/Map/SearchNode.cs b/V3/Map/SearchNode.cs
new file mode 100644
index 0000000..db49f14
--- /dev/null
+++ b/V3/Map/SearchNode.cs
@@ -0,0 +1,31 @@
+using Microsoft.Xna.Framework;
+
+namespace V3.Map
+{
+ class SearchNode
+ {
+ // Location on the map
+ public Vector2 mPosition;
+
+ // If true, the sprite can walk on
+ public bool mWalkable;
+
+ //
+ public SearchNode[] mNeighbors;
+
+ // Previous node
+ public SearchNode mParent;
+
+ // Check whether a node is in the open list
+ public bool mInOpenList;
+
+ // Check whether a node is in the closed list
+ public bool mInClosedList;
+
+ // DIstance from the start node to the goal node (F value)
+ public float mDistanceToGoal;
+
+ // Distance traveled from the spawn point (G value)
+ public float mDistanceTraveled;
+ }
+} \ No newline at end of file
diff --git a/V3/Map/TiledParser.cs b/V3/Map/TiledParser.cs
new file mode 100644
index 0000000..8171ecf
--- /dev/null
+++ b/V3/Map/TiledParser.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+using Microsoft.Xna.Framework;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// Parser for the tmx format of the Tiled Map Editor.
+ /// Reads XML file and returns corresponding data objects.
+ /// </summary>
+ public sealed class TiledParser
+ {
+ private string mFileName;
+ // Map Data:
+ public int MapWidth { get; private set; }
+ public int MapHeight { get; private set; }
+ public int TileWidth { get; private set; }
+ public int TileHeight { get; private set; }
+ public SortedList<int, Tileset> TileSets { get; } = new SortedList<int, Tileset>();
+ public List<int[,]> MapLayers { get; } = new List<int[,]>();
+ public List<Area> Areas { get; } = new List<Area>();
+
+ /// <summary>
+ /// Parse the tmx file and hold data in instance properties.
+ /// </summary>
+ public void Parse(string fileName)
+ {
+ mFileName = fileName;
+ string directory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
+ string fullPath = directory + "/Content/Maps/" + mFileName + ".tmx";
+ int p = (int)Environment.OSVersion.Platform;
+ if ((p == 4) || (p == 6) || (p == 128)) // Running on Unix
+ fullPath = fullPath.Substring(5);
+#if DEBUG
+ Console.WriteLine("Loading Map: " + fullPath);
+#endif
+ XmlReader reader = XmlReader.Create(fullPath);
+ while (reader.Read())
+ {
+ if (reader.IsStartElement())
+ {
+ switch (reader.Name)
+ {
+ case "map":
+ ParseMapData(reader);
+ break;
+ case "tileset":
+ ParseTilesetData(reader);
+ break;
+ case "layer":
+ ParseLayerData(reader);
+ break;
+ case "objectgroup":
+ ParseObjectgroup(reader);
+ break;
+ }
+ }
+ }
+ }
+
+ private void ParseMapData(XmlReader reader)
+ {
+ while (reader.MoveToNextAttribute())
+ {
+ switch (reader.Name)
+ {
+ case "width":
+ MapWidth = reader.ReadContentAsInt();
+ break;
+ case "height":
+ MapHeight = reader.ReadContentAsInt();
+ break;
+ case "tilewidth":
+ TileWidth = reader.ReadContentAsInt();
+ break;
+ case "tileheight":
+ TileHeight = reader.ReadContentAsInt();
+ break;
+ }
+ }
+ reader.MoveToElement();
+ }
+
+ private void ParseTilesetData(XmlReader reader)
+ {
+ if (reader.HasAttributes)
+ {
+ List<string> tilesetAttributes = new List<string>();
+ // Read attributes firstgid, name, tilewidth, tileheight, tilecount, columns.
+ for (int i = 0; i < reader.AttributeCount; i++)
+ {
+ tilesetAttributes.Add(reader[i]);
+ }
+ reader.MoveToElement();
+ // Read attributes for tileoffset x and y if existing.
+ while (reader.Read())
+ {
+ if (reader.Name == "tileoffset")
+ {
+ if (reader.IsStartElement())
+ {
+ for (int i = 0; i < reader.AttributeCount; i++)
+ {
+ tilesetAttributes.Add(reader[i]);
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+ else if (reader.Name == "tile" || reader.Name == "tileset")
+ {
+ break;
+ }
+ }
+ if (tilesetAttributes.Count == 6)
+ {
+ TileSets.Add(int.Parse(tilesetAttributes[0]), new Tileset(tilesetAttributes[1], int.Parse(tilesetAttributes[2]),
+ int.Parse(tilesetAttributes[3]), int.Parse(tilesetAttributes[5])));
+ }
+ else if (tilesetAttributes.Count == 8)
+ {
+ TileSets.Add(int.Parse(tilesetAttributes[0]), new Tileset(tilesetAttributes[1], int.Parse(tilesetAttributes[2]),
+ int.Parse(tilesetAttributes[3]), int.Parse(tilesetAttributes[5]),
+ int.Parse(tilesetAttributes[6]), int.Parse(tilesetAttributes[7])));
+ }
+ else
+ {
+ throw new Exception("Error parsing tileset element in " + mFileName + ".tmx. Does not contain necessary attributes.");
+ }
+ ParseCollisionData(reader, int.Parse(tilesetAttributes[0]));
+ }
+ }
+
+ private void ParseLayerData(XmlReader reader)
+ {
+ while (reader.MoveToNextAttribute())
+ {
+ if (reader.Name == "width")
+ {
+ // TODO: Try catching exceptions and throw specific ones.
+ int width = reader.ReadContentAsInt();
+ reader.MoveToNextAttribute();
+ int height = reader.ReadContentAsInt();
+ reader.MoveToElement();
+ reader.ReadToDescendant("data");
+ MapLayers.Add(new int[height, width]);
+ int currentLayerIndex = MapLayers.Count - 1;
+ // Map data is in CSV format, therefore split at comma.
+ string[] layerData = reader.ReadString().Split(',');
+ for (int i = 0; i < height; i++)
+ {
+ for (int j = 0; j < width; j++)
+ {
+ MapLayers[currentLayerIndex][i, j] = int.Parse(layerData[i * width + j]);
+ }
+ }
+ }
+ }
+ reader.MoveToElement();
+ }
+
+ private void ParseCollisionData(XmlReader reader, int currentTileset)
+ {
+ do
+ {
+ if (!reader.IsStartElement() && reader.Name == "tileset")
+ {
+ // If the end of the tileset note is reached, leave loop.
+ break;
+ }
+ if (reader.IsStartElement() && reader.Name == "tile" && reader.HasAttributes)
+ {
+ string tileId = reader[0];
+ reader.MoveToElement();
+ while (reader.ReadToDescendant("property"))
+ {
+ if (reader.MoveToAttribute("name") && reader.Value == "collision")
+ {
+ reader.MoveToNextAttribute();
+ string collisionData = reader.Value;
+ Tileset tileset = TileSets[currentTileset];
+ if (tileId != null) tileset.AddCollisionData(int.Parse(tileId), collisionData);
+ }
+ }
+ }
+ }
+ while (reader.Read()) ;
+ }
+
+ private void ParseObjectgroup(XmlReader reader)
+ {
+ do
+ {
+ if (!reader.IsStartElement() && reader.Name == "objectgroup")
+ break;
+ if (reader.IsStartElement() && reader.Name == "object")
+ {
+ ParseAreaData(reader);
+ }
+ } while (reader.Read());
+ }
+
+ private void ParseAreaData(XmlReader reader)
+ {
+ string type;
+ string name = "";
+ double density = 0;
+ double chance = 0;
+ Rectangle rectangle;
+ if (reader.AttributeCount == 7)
+ {
+ name = reader[1];
+ type = reader[2];
+ if (!(reader[3] != null && reader[4] != null && reader[5] != null && reader[6] != null))
+ return;
+ rectangle = new Rectangle(int.Parse(reader[3]), int.Parse(reader[4]), int.Parse(reader[5]), int.Parse(reader[6]));
+ }
+ else if (reader.AttributeCount == 6)
+ {
+ type = reader[1];
+ if (!(reader[2] != null && reader[3] != null && reader[4] != null && reader[5] != null))
+ return;
+ rectangle = new Rectangle(int.Parse(reader[2]), int.Parse(reader[3]), int.Parse(reader[4]), int.Parse(reader[5]));
+ }
+ else
+ {
+ throw new Exception("Error parsing the map. One of the objects has not the right number of attributes, specifically: " + reader.AttributeCount);
+ }
+ reader.MoveToElement();
+ while (reader.Read())
+ {
+ if (!reader.IsStartElement() && reader.Name == "properties")
+ break;
+ if (reader.Name == "property" && reader.HasAttributes)
+ {
+ if (reader[2] == null) return;
+ if (reader[0] == "chance")
+ {
+ chance = double.Parse(reader[2], CultureInfo.InvariantCulture);
+ }
+ else if (reader[0] == "density")
+ {
+ density = double.Parse(reader[2], CultureInfo.InvariantCulture);
+ }
+ reader.MoveToElement();
+ }
+ }
+ Area area = new Area(type, rectangle, density, chance, name);
+ Areas.Add(area);
+ }
+ }
+}
diff --git a/V3/Map/Tileset.cs b/V3/Map/Tileset.cs
new file mode 100644
index 0000000..aa12885
--- /dev/null
+++ b/V3/Map/Tileset.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+
+namespace V3.Map
+{
+ /// <summary>
+ /// Class for holding information needed of Tilesets. Needed to draw the map.
+ /// </summary>
+ public sealed class Tileset
+ {
+ private const int CellHeight = Constants.CellHeight;
+ private const int CellWidth = Constants.CellWidth;
+
+ /// <summary>
+ /// Name of the tileset, often the filename.
+ /// </summary>
+ public string Name { get; }
+ /// <summary>
+ /// Tile width of each tile in pixel.
+ /// </summary>
+ public int TileWidth { get; }
+ /// <summary>
+ /// Tile height of each tile in pixel.
+ /// </summary>
+ public int TileHeight { get; }
+
+ /// <summary>
+ /// Columns of tiles of the tileset image.
+ /// </summary>
+ public int Columns { get; private set; }
+ /// <summary>
+ /// When tile is drawn, is there an offset needed on the X axis for correct display.
+ /// </summary>
+ public int OffsetX { get; private set; }
+ /// <summary>
+ ///
+ /// When tile is drawn, is there an offset needed on the Y axis for correct display.
+ /// </summary>
+ public int OffsetY { get; private set; }
+ /// <summary>
+ /// Each tile of the tileset, represented by an integer, can hold collision data consisting of a two dimensional
+ /// array of boolean values. Its size is described by CollisionWidth and CollisionHeight.
+ /// </summary>
+ public Dictionary<int, bool[,]> TileCollisions { get; }
+ public int CollisionWidth => TileWidth / CellWidth;
+ public int CollisionHeight => TileHeight / CellHeight;
+
+ public Tileset(string name, int tileWidth, int tileHeight, int columns, int offsetX = 0, int offsetY = 0)
+ {
+ Name = name;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = columns;
+ OffsetX = offsetX;
+ OffsetY = offsetY;
+ // TODO: Fill dictionary with TiledParser.
+ TileCollisions = new Dictionary<int, bool[,]>();
+ }
+
+ /// <summary>
+ /// Add an entry to the collision dictionary for the specific tile.
+ /// </summary>
+ /// <param name="tileId">The tile ID in the tileset.</param>
+ /// <param name="collisionData">The corresponding collision data as string of '0' and '1'.</param>
+ public void AddCollisionData(int tileId, string collisionData)
+ {
+ int gridWidth = CollisionWidth;
+ int gridHeight = CollisionHeight;
+ bool[,] dataArray = new bool[gridHeight, gridWidth];
+ for (int i = 0; i < gridHeight; i++)
+ {
+ for (int j = 0; j < gridWidth; j++)
+ {
+ try
+ {
+ dataArray[i, j] = collisionData[i * gridWidth + j] == '1';
+ }
+ catch (IndexOutOfRangeException e)
+ {
+ throw new IndexOutOfRangeException("Inconsistencies with the collision data of Tile "
+ + tileId + " in tileset " + Name + ". Check corresponding tmx file or" +
+ "contact the programmer: Thomas.", e);
+ }
+ }
+ }
+ TileCollisions.Add(tileId, dataArray);
+ }
+ }
+} \ No newline at end of file