From 5121f6d775536f136046e33dd923c3762e829788 Mon Sep 17 00:00:00 2001 From: Colin McMillen Date: Sun, 8 Mar 2020 17:23:51 -0400 Subject: [PATCH] World: load levels by parsing a JSON level description. --- Shared/Geometry.cs | 8 +- Shared/NPC.cs | 5 +- Shared/Player.cs | 14 +-- Shared/SneakGame.cs | 2 +- Shared/World.cs | 222 +++++++++++++++++--------------------------- 5 files changed, 102 insertions(+), 149 deletions(-) diff --git a/Shared/Geometry.cs b/Shared/Geometry.cs index fbc5e43..da6d162 100644 --- a/Shared/Geometry.cs +++ b/Shared/Geometry.cs @@ -83,15 +83,15 @@ namespace SemiColinGames { public readonly struct AABB { public readonly Vector2 Position; // centroid public readonly Vector2 HalfSize; - public readonly Terrain Terrain; + public readonly Tile Tile; - public AABB(Vector2 position, Vector2 halfSize) : this(position, halfSize, Terrain.Empty) { + public AABB(Vector2 position, Vector2 halfSize) : this(position, halfSize, null) { } - public AABB(Vector2 position, Vector2 halfSize, Terrain terrain) { + public AABB(Vector2 position, Vector2 halfSize, Tile tile) { Position = position; HalfSize = halfSize; - Terrain = terrain; + Tile = tile; } public float Top { diff --git a/Shared/NPC.cs b/Shared/NPC.cs index dcd3de3..15a43c1 100644 --- a/Shared/NPC.cs +++ b/Shared/NPC.cs @@ -52,15 +52,16 @@ namespace SemiColinGames { private FSM fsm; - public NPC(Point position) { + public NPC(Point position, int facing) { Position = position; + Facing = facing; fsm = new FSM(new Dictionary> { { "idle", new IdleState() }, { "run", new RunState() } }, "run"); } - public int Facing = 1; + public int Facing; public Point Position; public void Update(float modelTime, World world) { diff --git a/Shared/Player.cs b/Shared/Player.cs index d5b1818..e0f2157 100644 --- a/Shared/Player.cs +++ b/Shared/Player.cs @@ -22,7 +22,7 @@ namespace SemiColinGames { // Position is tracked at the Player's center. The Player's bounding box is a rectangle // centered at that point and extending out by halfSize.X and halfSize.Y. - private Point position = new Point(64, 16 * 12); + private Point position; private Vector2 halfSize = new Vector2(11, 24); private Vector2 eyeOffsetStanding = new Vector2(7, -14); private Vector2 eyeOffsetWalking = new Vector2(15, -7); @@ -36,7 +36,9 @@ namespace SemiColinGames { private double jumpTime = 0; private float invincibilityTime = 0; - public Player() { + public Player(Point position, int facing) { + this.position = position; + Facing = facing; Health = MaxHealth; } @@ -44,7 +46,7 @@ namespace SemiColinGames { public int Health { get; private set; } - public int Facing { get; private set; } = 1; + public int Facing { get; private set; } public Point Position { get { return position; } } @@ -92,7 +94,7 @@ namespace SemiColinGames { if (box.Intersect(player) != null) { Debug.AddRect(box, Color.Cyan); reject = true; - if (box.Terrain.IsHarmful) { + if (box.Tile?.IsHarmful ?? false) { Debug.AddRect(box, Color.Red); harmedByCollision = true; } @@ -110,7 +112,7 @@ namespace SemiColinGames { if (box.Intersect(player) != null) { Debug.AddRect(box, Color.Cyan); reject = true; - if (box.Terrain.IsHarmful) { + if (box.Tile?.IsHarmful ?? false) { Debug.AddRect(box, Color.Red); harmedByCollision = true; } @@ -128,7 +130,7 @@ namespace SemiColinGames { if (groundIntersect.Intersect(box) != null) { Debug.AddRect(box, Color.Cyan); standingOnGround = true; - if (box.Terrain.IsHarmful) { + if (box.Tile?.IsHarmful ?? false) { Debug.AddRect(box, Color.Red); harmedByCollision = true; } diff --git a/Shared/SneakGame.cs b/Shared/SneakGame.cs index 90d942e..55520db 100644 --- a/Shared/SneakGame.cs +++ b/Shared/SneakGame.cs @@ -70,7 +70,7 @@ namespace SemiColinGames { private void LoadLevel() { camera = new Camera(); levelIdx++; - world = new World(Levels.ALL_LEVELS[levelIdx % Levels.ALL_LEVELS.Length]); + world = new World(Content.LoadString("levels/demo.json")); scene?.Dispose(); scene = new Scene(GraphicsDevice, camera); diff --git a/Shared/World.cs b/Shared/World.cs index 0808da3..159188a 100644 --- a/Shared/World.cs +++ b/Shared/World.cs @@ -1,173 +1,123 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; namespace SemiColinGames { - public class Terrain { + public class Tile { + private TextureRef texture; + private Rectangle textureSource; - public static Terrain FromSymbol(char symbol) { - if (mapping.ContainsKey(symbol)) { - return mapping[symbol]; - } else { - return null; - } - } - - private readonly static Dictionary mapping = new Dictionary(); - - public static Terrain Empty = - new Terrain('\0', Textures.Grassland, 0, 0); - public static Terrain Spike = - new Terrain('^', Textures.Grassland, 11, 8, isObstacle: true, isHarmful: true); - public static Terrain Grass = - new Terrain('=', Textures.Grassland, 3, 0, isObstacle: true); - public static Terrain GrassL = - new Terrain('<', Textures.Grassland, 2, 0, isObstacle: true); - public static Terrain GrassR = - new Terrain('>', Textures.Grassland, 4, 0, isObstacle: true); - public static Terrain Rock = - new Terrain('.', Textures.Grassland, 3, 1, isObstacle: true); - public static Terrain RockL = - new Terrain('[', Textures.Grassland, 1, 2, isObstacle: true); - public static Terrain RockR = - new Terrain(']', Textures.Grassland, 5, 2, isObstacle: true); - public static Terrain Block = - new Terrain('X', Textures.Grassland, 23, 0, isObstacle: true); - public static Terrain Wood = - new Terrain('_', Textures.Grassland, 10, 3, isObstacle: true); - public static Terrain WoodL = - new Terrain('(', Textures.Grassland, 9, 3, isObstacle: true); - public static Terrain WoodR = - new Terrain(')', Textures.Grassland, 12, 3, isObstacle: true); - public static Terrain WoodVert = - new Terrain('|', Textures.Grassland, 9, 5); - public static Terrain WoodVertL = - new Terrain('/', Textures.Grassland, 9, 4); - public static Terrain WoodVertR = - new Terrain('\\', Textures.Grassland, 12, 4); - public static Terrain WoodBottom = - new Terrain('v', Textures.Grassland, 10, 5); - public static Terrain WaterL = - new Terrain('~', Textures.Grassland, 9, 2); - public static Terrain WaterR = - new Terrain('`', Textures.Grassland, 10, 2); - public static Terrain FenceL = - new Terrain('d', Textures.Grassland, 5, 4); - public static Terrain Fence = - new Terrain('f', Textures.Grassland, 6, 4); - public static Terrain FencePost = - new Terrain('x', Textures.Grassland, 7, 4); - public static Terrain FenceR = - new Terrain('b', Textures.Grassland, 8, 4); - public static Terrain VineTop = - new Terrain('{', Textures.Grassland, 20, 0); - public static Terrain VineMid = - new Terrain(';', Textures.Grassland, 20, 1); - public static Terrain VineBottom = - new Terrain('}', Textures.Grassland, 20, 2); - public static Terrain GrassTall = - new Terrain('q', Textures.Grassland, 13, 0); - public static Terrain GrassShort = - new Terrain('w', Textures.Grassland, 14, 0); - public static Terrain Shoots = - new Terrain('e', Textures.Grassland, 15, 0); - public static Terrain Bush = - new Terrain('r', Textures.Grassland, 13, 2); - public static Terrain Mushroom = - new Terrain('t', Textures.Grassland, 17, 2); - - public bool IsObstacle { get; private set; } - public bool IsHarmful { get; private set; } - public TextureRef Texture { get; private set; } - public Rectangle TextureSource { get; private set; } - - private Terrain( - char symbol, TextureRef texture, int x, int y, - bool isObstacle = false, bool isHarmful = false) { - if (mapping.ContainsKey(symbol)) { - throw new ArgumentException("already have a terrain with symbol " + symbol); - } - mapping[symbol] = this; - IsObstacle = isObstacle; - IsHarmful = isHarmful; - Texture = texture; - int size = World.TileSize; - TextureSource = new Rectangle(x * size, y * size, size, size); - } - } - - class Tile { - public Tile(Terrain terrain, Rectangle position) { - Terrain = terrain; + public Tile(TextureRef texture, Rectangle textureSource, Rectangle position) { Position = position; + this.texture = texture; + this.textureSource = textureSource; } public Rectangle Position { get; private set; } - public Terrain Terrain { get; private set; } + public bool IsHarmful = false; public void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw( - Terrain.Texture.Get, Position.Location.ToVector2(), Terrain.TextureSource, Color.White); + texture.Get, Position.Location.ToVector2(), textureSource, Color.White); } } public class World { + // Size of World in terms of tile grid. + private int gridWidth; + private int gridHeight; + + // TODO: remove this. public const int TileSize = 16; readonly Tile[] tiles; readonly Tile[] decorations; - readonly NPC[] npcs = new NPC[4]; - - // Size of World in terms of tile grid. - private readonly int tileWidth; - private readonly int tileHeight; + // Kept around for resetting the world's entities after player death or level restart. + readonly JToken entitiesLayer; + NPC[] npcs; public Player Player { get; private set; } // Size of World in pixels. public int Width { - get { return tileWidth * TileSize; } + get { return gridWidth * TileSize; } } public int Height { - get { return tileHeight * TileSize; } + get { return gridHeight * TileSize; } } - public World(string levelSpecification) { - Player = new Player(); - npcs[0] = new NPC(new Point(16 * 8, 16 * 6)); - npcs[1] = new NPC(new Point(16 * 28, 16 * 6)); - npcs[2] = new NPC(new Point(16 * 36, 16 * 6)); - npcs[3] = new NPC(new Point(16 * 36, 16 * 12)); - - var tilesList = new List(); - var decorationsList = new List(); - string[] worldDesc = levelSpecification.Substring(1).Split('\n'); - tileWidth = worldDesc.AsQueryable().Max(a => a.Length); - tileHeight = worldDesc.Length; - Debug.WriteLine("world size: {0}x{1}", tileWidth, tileHeight); - for (int i = 0; i < tileWidth; i++) { - for (int j = 0; j < tileHeight; j++) { - if (i < worldDesc[j].Length) { - char key = worldDesc[j][i]; - Terrain terrain = Terrain.FromSymbol(key); - if (terrain != null) { - var position = new Rectangle(i * TileSize, j * TileSize, TileSize, TileSize); - Tile tile = new Tile(terrain, position); - if (tile.Terrain.IsObstacle) { - tilesList.Add(tile); - } else { - decorationsList.Add(tile); - } - } - } + private List ParseLayer(JToken layer) { + var tileList = new List(); + + int layerWidth = layer.SelectToken("gridCellsX").Value(); + int layerHeight = layer.SelectToken("gridCellsY").Value(); + gridWidth = Math.Max(gridWidth, layerWidth); + gridHeight = Math.Max(gridHeight, layerHeight); + + int dataIndex = -1; + int tileWidth = layer.SelectToken("gridCellWidth").Value(); + int tileHeight = layer.SelectToken("gridCellHeight").Value(); + int textureWidth = Textures.Grassland.Get.Width / tileWidth; + foreach (int textureIndex in layer.SelectToken("data").Values()) { + dataIndex++; + if (textureIndex == -1) { + continue; + } + int i = dataIndex % layerWidth; + int j = dataIndex / layerWidth; + Rectangle position = new Rectangle( + i * tileWidth, j * tileHeight, tileWidth, tileHeight); + int x = textureIndex % textureWidth; + int y = textureIndex / textureWidth; + Rectangle textureSource = new Rectangle( + x * tileWidth, y * tileHeight, tileWidth, tileHeight); + tileList.Add(new Tile(Textures.Grassland, textureSource, position)); + } + + return tileList; + } + + private (Player, NPC[]) ParseEntities(JToken layer) { + Player player = null; + List npcs = new List(); + foreach (JToken entity in layer.SelectToken("entities").Children()) { + string name = entity.SelectToken("name").Value(); + int x = entity.SelectToken("x").Value(); + int y = entity.SelectToken("y").Value(); + int facing = entity.SelectToken("flippedX").Value() ? -1 : 1; + if (name == "player") { + player = new Player(new Point(x, y), facing); + } else if (name == "executioner") { + npcs.Add(new NPC(new Point(x, y), facing)); + } + } + return (player, npcs.ToArray()); + } + + public World(string json) { + JObject root = JObject.Parse(json); + + foreach (JToken layer in root.SelectToken("layers").Children()) { + string layerName = layer.SelectToken("name").Value(); + if (layerName == "entities") { + entitiesLayer = layer; + (Player, npcs) = ParseEntities(layer); + continue; + } + List tileList = ParseLayer(layer); + // TODO: add background layer + if (layerName == "obstacles") { + tiles = tileList.ToArray(); + } else if (layerName == "decorations") { + decorations = tileList.ToArray(); } } - tiles = tilesList.ToArray(); - decorations = decorationsList.ToArray(); + Debug.WriteLine("world size: {0}x{1}", gridWidth, gridHeight); // Because we added tiles from left to right, the CollisionTargets are sorted by x-position. // We maintain this invariant so that it's possible to efficiently find CollisionTargets that @@ -182,7 +132,7 @@ namespace SemiColinGames { for (int i = 0; i < tiles.Length; i++) { Vector2 center = new Vector2( tiles[i].Position.Left + halfSize.X, tiles[i].Position.Top + halfSize.Y); - CollisionTargets[i + 1] = new AABB(center, halfSize, tiles[i].Terrain); + CollisionTargets[i + 1] = new AABB(center, halfSize, tiles[i]); } // Add a final synthetic collisionTarget on the right side of the world. @@ -201,7 +151,7 @@ namespace SemiColinGames { } void Reset() { - Player = new Player(); + (Player, npcs) = ParseEntities(entitiesLayer); } public void DrawBackground(SpriteBatch spriteBatch) {