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 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 {
|
||||
|
@ -52,15 +52,16 @@ namespace SemiColinGames {
|
||||
|
||||
private FSM<NPC> fsm;
|
||||
|
||||
public NPC(Point position) {
|
||||
public NPC(Point position, int facing) {
|
||||
Position = position;
|
||||
Facing = facing;
|
||||
fsm = new FSM<NPC>(new Dictionary<string, IState<NPC>> {
|
||||
{ "idle", new IdleState() },
|
||||
{ "run", new RunState() }
|
||||
}, "run");
|
||||
}
|
||||
|
||||
public int Facing = 1;
|
||||
public int Facing;
|
||||
public Point Position;
|
||||
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
216
Shared/World.cs
216
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<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;
|
||||
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));
|
||||
private List<Tile> ParseLayer(JToken layer) {
|
||||
var tileList = new List<Tile>();
|
||||
|
||||
var tilesList = new List<Tile>();
|
||||
var decorationsList = new List<Tile>();
|
||||
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);
|
||||
int layerWidth = layer.SelectToken("gridCellsX").Value<int>();
|
||||
int layerHeight = layer.SelectToken("gridCellsY").Value<int>();
|
||||
gridWidth = Math.Max(gridWidth, layerWidth);
|
||||
gridHeight = Math.Max(gridHeight, layerHeight);
|
||||
|
||||
int dataIndex = -1;
|
||||
int tileWidth = layer.SelectToken("gridCellWidth").Value<int>();
|
||||
int tileHeight = layer.SelectToken("gridCellHeight").Value<int>();
|
||||
int textureWidth = Textures.Grassland.Get.Width / tileWidth;
|
||||
foreach (int textureIndex in layer.SelectToken("data").Values<int>()) {
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user