diff options
Diffstat (limited to 'V3/Map')
-rw-r--r-- | V3/Map/AbstractLayer.cs | 259 | ||||
-rw-r--r-- | V3/Map/Area.cs | 119 | ||||
-rw-r--r-- | V3/Map/Constants.cs | 12 | ||||
-rw-r--r-- | V3/Map/FloorLayer.cs | 21 | ||||
-rw-r--r-- | V3/Map/FogOfWar.cs | 107 | ||||
-rw-r--r-- | V3/Map/IMapManager.cs | 88 | ||||
-rw-r--r-- | V3/Map/MapManager.cs | 97 | ||||
-rw-r--r-- | V3/Map/ObjectLayer.cs | 16 | ||||
-rw-r--r-- | V3/Map/Pathfinder.cs | 455 | ||||
-rw-r--r-- | V3/Map/PathfindingGrid.cs | 125 | ||||
-rw-r--r-- | V3/Map/SearchNode.cs | 31 | ||||
-rw-r--r-- | V3/Map/TiledParser.cs | 256 | ||||
-rw-r--r-- | V3/Map/Tileset.cs | 89 |
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 |