Browse Source

World: load levels by parsing a JSON level description.

master
Colin McMillen 4 years ago
parent
commit
5121f6d775
  1. 8
      Shared/Geometry.cs
  2. 5
      Shared/NPC.cs
  3. 14
      Shared/Player.cs
  4. 2
      Shared/SneakGame.cs
  5. 216
      Shared/World.cs

8
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 {

5
Shared/NPC.cs

@ -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) {

14
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;
}

2
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);

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 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 class Tile {
private TextureRef texture;
private Rectangle textureSource;
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; }
}
private List<Tile> ParseLayer(JToken layer) {
var tileList = new List<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));
}
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));
return tileList;
}
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);
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…
Cancel
Save