World: load levels by parsing a JSON level description.
This commit is contained in:
parent
28ef337691
commit
5121f6d775
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
216
Shared/World.cs
216
Shared/World.cs
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user