World: load levels by parsing a JSON level description.

This commit is contained in:
Colin McMillen 2020-03-08 17:23:51 -04:00
parent 28ef337691
commit 5121f6d775
5 changed files with 101 additions and 148 deletions

View File

@ -83,15 +83,15 @@ namespace SemiColinGames {
public readonly struct AABB { public readonly struct AABB {
public readonly Vector2 Position; // centroid public readonly Vector2 Position; // centroid
public readonly Vector2 HalfSize; 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; Position = position;
HalfSize = halfSize; HalfSize = halfSize;
Terrain = terrain; Tile = tile;
} }
public float Top { public float Top {

View File

@ -52,15 +52,16 @@ namespace SemiColinGames {
private FSM<NPC> fsm; private FSM<NPC> fsm;
public NPC(Point position) { public NPC(Point position, int facing) {
Position = position; Position = position;
Facing = facing;
fsm = new FSM<NPC>(new Dictionary<string, IState<NPC>> { fsm = new FSM<NPC>(new Dictionary<string, IState<NPC>> {
{ "idle", new IdleState() }, { "idle", new IdleState() },
{ "run", new RunState() } { "run", new RunState() }
}, "run"); }, "run");
} }
public int Facing = 1; public int Facing;
public Point Position; public Point Position;
public void Update(float modelTime, World world) { public void Update(float modelTime, World world) {

View File

@ -22,7 +22,7 @@ namespace SemiColinGames {
// Position is tracked at the Player's center. The Player's bounding box is a rectangle // 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. // 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 halfSize = new Vector2(11, 24);
private Vector2 eyeOffsetStanding = new Vector2(7, -14); private Vector2 eyeOffsetStanding = new Vector2(7, -14);
private Vector2 eyeOffsetWalking = new Vector2(15, -7); private Vector2 eyeOffsetWalking = new Vector2(15, -7);
@ -36,7 +36,9 @@ namespace SemiColinGames {
private double jumpTime = 0; private double jumpTime = 0;
private float invincibilityTime = 0; private float invincibilityTime = 0;
public Player() { public Player(Point position, int facing) {
this.position = position;
Facing = facing;
Health = MaxHealth; Health = MaxHealth;
} }
@ -44,7 +46,7 @@ namespace SemiColinGames {
public int Health { get; private set; } 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; } } public Point Position { get { return position; } }
@ -92,7 +94,7 @@ namespace SemiColinGames {
if (box.Intersect(player) != null) { if (box.Intersect(player) != null) {
Debug.AddRect(box, Color.Cyan); Debug.AddRect(box, Color.Cyan);
reject = true; reject = true;
if (box.Terrain.IsHarmful) { if (box.Tile?.IsHarmful ?? false) {
Debug.AddRect(box, Color.Red); Debug.AddRect(box, Color.Red);
harmedByCollision = true; harmedByCollision = true;
} }
@ -110,7 +112,7 @@ namespace SemiColinGames {
if (box.Intersect(player) != null) { if (box.Intersect(player) != null) {
Debug.AddRect(box, Color.Cyan); Debug.AddRect(box, Color.Cyan);
reject = true; reject = true;
if (box.Terrain.IsHarmful) { if (box.Tile?.IsHarmful ?? false) {
Debug.AddRect(box, Color.Red); Debug.AddRect(box, Color.Red);
harmedByCollision = true; harmedByCollision = true;
} }
@ -128,7 +130,7 @@ namespace SemiColinGames {
if (groundIntersect.Intersect(box) != null) { if (groundIntersect.Intersect(box) != null) {
Debug.AddRect(box, Color.Cyan); Debug.AddRect(box, Color.Cyan);
standingOnGround = true; standingOnGround = true;
if (box.Terrain.IsHarmful) { if (box.Tile?.IsHarmful ?? false) {
Debug.AddRect(box, Color.Red); Debug.AddRect(box, Color.Red);
harmedByCollision = true; harmedByCollision = true;
} }

View File

@ -70,7 +70,7 @@ namespace SemiColinGames {
private void LoadLevel() { private void LoadLevel() {
camera = new Camera(); camera = new Camera();
levelIdx++; levelIdx++;
world = new World(Levels.ALL_LEVELS[levelIdx % Levels.ALL_LEVELS.Length]); world = new World(Content.LoadString("levels/demo.json"));
scene?.Dispose(); scene?.Dispose();
scene = new Scene(GraphicsDevice, camera); scene = new Scene(GraphicsDevice, camera);

View File

@ -1,173 +1,123 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace SemiColinGames { namespace SemiColinGames {
public class Terrain { public class Tile {
private TextureRef texture;
private Rectangle textureSource;
public static Terrain FromSymbol(char symbol) { public Tile(TextureRef texture, Rectangle textureSource, Rectangle position) {
if (mapping.ContainsKey(symbol)) {
return mapping[symbol];
} else {
return null;
}
}
private readonly static Dictionary<char, Terrain> mapping = new Dictionary<char, Terrain>();
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;
Position = position; Position = position;
this.texture = texture;
this.textureSource = textureSource;
} }
public Rectangle Position { get; private set; } public Rectangle Position { get; private set; }
public Terrain Terrain { get; private set; } public bool IsHarmful = false;
public void Draw(SpriteBatch spriteBatch) { public void Draw(SpriteBatch spriteBatch) {
spriteBatch.Draw( spriteBatch.Draw(
Terrain.Texture.Get, Position.Location.ToVector2(), Terrain.TextureSource, Color.White); texture.Get, Position.Location.ToVector2(), textureSource, Color.White);
} }
} }
public class World { 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; public const int TileSize = 16;
readonly Tile[] tiles; readonly Tile[] tiles;
readonly Tile[] decorations; readonly Tile[] decorations;
readonly NPC[] npcs = new NPC[4]; // Kept around for resetting the world's entities after player death or level restart.
readonly JToken entitiesLayer;
// Size of World in terms of tile grid.
private readonly int tileWidth;
private readonly int tileHeight;
NPC[] npcs;
public Player Player { get; private set; } public Player Player { get; private set; }
// Size of World in pixels. // Size of World in pixels.
public int Width { public int Width {
get { return tileWidth * TileSize; } get { return gridWidth * TileSize; }
} }
public int Height { public int Height {
get { return tileHeight * TileSize; } get { return gridHeight * TileSize; }
} }
public World(string levelSpecification) { private List<Tile> ParseLayer(JToken layer) {
Player = new Player(); var tileList = new List<Tile>();
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<Tile>(); int layerWidth = layer.SelectToken("gridCellsX").Value<int>();
var decorationsList = new List<Tile>(); int layerHeight = layer.SelectToken("gridCellsY").Value<int>();
string[] worldDesc = levelSpecification.Substring(1).Split('\n'); gridWidth = Math.Max(gridWidth, layerWidth);
tileWidth = worldDesc.AsQueryable().Max(a => a.Length); gridHeight = Math.Max(gridHeight, layerHeight);
tileHeight = worldDesc.Length;
Debug.WriteLine("world size: {0}x{1}", tileWidth, tileHeight); int dataIndex = -1;
for (int i = 0; i < tileWidth; i++) { int tileWidth = layer.SelectToken("gridCellWidth").Value<int>();
for (int j = 0; j < tileHeight; j++) { int tileHeight = layer.SelectToken("gridCellHeight").Value<int>();
if (i < worldDesc[j].Length) { int textureWidth = Textures.Grassland.Get.Width / tileWidth;
char key = worldDesc[j][i]; foreach (int textureIndex in layer.SelectToken("data").Values<int>()) {
Terrain terrain = Terrain.FromSymbol(key); dataIndex++;
if (terrain != null) { if (textureIndex == -1) {
var position = new Rectangle(i * TileSize, j * TileSize, TileSize, TileSize); continue;
Tile tile = new Tile(terrain, position); }
if (tile.Terrain.IsObstacle) { int i = dataIndex % layerWidth;
tilesList.Add(tile); int j = dataIndex / layerWidth;
} else { Rectangle position = new Rectangle(
decorationsList.Add(tile); 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<NPC> npcs = new List<NPC>();
foreach (JToken entity in layer.SelectToken("entities").Children()) {
string name = entity.SelectToken("name").Value<string>();
int x = entity.SelectToken("x").Value<int>();
int y = entity.SelectToken("y").Value<int>();
int facing = entity.SelectToken("flippedX").Value<bool>() ? -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<string>();
if (layerName == "entities") {
entitiesLayer = layer;
(Player, npcs) = ParseEntities(layer);
continue;
}
List<Tile> tileList = ParseLayer(layer);
// TODO: add background layer
if (layerName == "obstacles") {
tiles = tileList.ToArray();
} else if (layerName == "decorations") {
decorations = tileList.ToArray();
} }
} }
} Debug.WriteLine("world size: {0}x{1}", gridWidth, gridHeight);
tiles = tilesList.ToArray();
decorations = decorationsList.ToArray();
// Because we added tiles from left to right, the CollisionTargets are sorted by x-position. // 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 // 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++) { for (int i = 0; i < tiles.Length; i++) {
Vector2 center = new Vector2( Vector2 center = new Vector2(
tiles[i].Position.Left + halfSize.X, tiles[i].Position.Top + halfSize.Y); 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. // Add a final synthetic collisionTarget on the right side of the world.
@ -201,7 +151,7 @@ namespace SemiColinGames {
} }
void Reset() { void Reset() {
Player = new Player(); (Player, npcs) = ParseEntities(entitiesLayer);
} }
public void DrawBackground(SpriteBatch spriteBatch) { public void DrawBackground(SpriteBatch spriteBatch) {