Compare commits

..

No commits in common. "5121f6d775536f136046e33dd923c3762e829788" and "0a37453dbdd81c27752cfaee07c8c297d6019dcb" have entirely different histories.

6 changed files with 149 additions and 102 deletions

View File

@ -2,8 +2,8 @@ using System;
namespace SemiColinGames { namespace SemiColinGames {
class FpsCounter { class FpsCounter {
private readonly int[] frameTimes = new int[60];
private double fps = 0; private double fps = 0;
private int[] frameTimes = new int[60];
private int idx = 0; private int idx = 0;
public int Fps { public int Fps {

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 Tile Tile; public readonly Terrain Terrain;
public AABB(Vector2 position, Vector2 halfSize) : this(position, halfSize, null) { public AABB(Vector2 position, Vector2 halfSize) : this(position, halfSize, Terrain.Empty) {
} }
public AABB(Vector2 position, Vector2 halfSize, Tile tile) { public AABB(Vector2 position, Vector2 halfSize, Terrain terrain) {
Position = position; Position = position;
HalfSize = halfSize; HalfSize = halfSize;
Tile = tile; Terrain = terrain;
} }
public float Top { public float Top {

View File

@ -52,16 +52,15 @@ namespace SemiColinGames {
private FSM<NPC> fsm; private FSM<NPC> fsm;
public NPC(Point position, int facing) { public NPC(Point position) {
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; public int Facing = 1;
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; private Point position = new Point(64, 16 * 12);
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,9 +36,7 @@ namespace SemiColinGames {
private double jumpTime = 0; private double jumpTime = 0;
private float invincibilityTime = 0; private float invincibilityTime = 0;
public Player(Point position, int facing) { public Player() {
this.position = position;
Facing = facing;
Health = MaxHealth; Health = MaxHealth;
} }
@ -46,7 +44,7 @@ namespace SemiColinGames {
public int Health { get; private set; } public int Health { get; private set; }
public int Facing { get; private set; } public int Facing { get; private set; } = 1;
public Point Position { get { return position; } } public Point Position { get { return position; } }
@ -94,7 +92,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.Tile?.IsHarmful ?? false) { if (box.Terrain.IsHarmful) {
Debug.AddRect(box, Color.Red); Debug.AddRect(box, Color.Red);
harmedByCollision = true; harmedByCollision = true;
} }
@ -112,7 +110,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.Tile?.IsHarmful ?? false) { if (box.Terrain.IsHarmful) {
Debug.AddRect(box, Color.Red); Debug.AddRect(box, Color.Red);
harmedByCollision = true; harmedByCollision = true;
} }
@ -130,7 +128,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.Tile?.IsHarmful ?? false) { if (box.Terrain.IsHarmful) {
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(Content.LoadString("levels/demo.json")); world = new World(Levels.ALL_LEVELS[levelIdx % Levels.ALL_LEVELS.Length]);
scene?.Dispose(); scene?.Dispose();
scene = new Scene(GraphicsDevice, camera); scene = new Scene(GraphicsDevice, camera);

View File

@ -1,123 +1,173 @@
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 Tile { public class Terrain {
private TextureRef texture;
private Rectangle textureSource;
public Tile(TextureRef texture, Rectangle textureSource, Rectangle position) { 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;
Position = position; Position = position;
this.texture = texture;
this.textureSource = textureSource;
} }
public Rectangle Position { get; private set; } public Rectangle Position { get; private set; }
public bool IsHarmful = false; public Terrain Terrain { get; private set; }
public void Draw(SpriteBatch spriteBatch) { public void Draw(SpriteBatch spriteBatch) {
spriteBatch.Draw( spriteBatch.Draw(
texture.Get, Position.Location.ToVector2(), textureSource, Color.White); Terrain.Texture.Get, Position.Location.ToVector2(), Terrain.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;
// Kept around for resetting the world's entities after player death or level restart. readonly NPC[] npcs = new NPC[4];
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 gridWidth * TileSize; } get { return tileWidth * TileSize; }
} }
public int Height { public int Height {
get { return gridHeight * TileSize; } get { return tileHeight * TileSize; }
} }
private List<Tile> ParseLayer(JToken layer) { public World(string levelSpecification) {
var tileList = new List<Tile>(); 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));
int layerWidth = layer.SelectToken("gridCellsX").Value<int>(); var tilesList = new List<Tile>();
int layerHeight = layer.SelectToken("gridCellsY").Value<int>(); var decorationsList = new List<Tile>();
gridWidth = Math.Max(gridWidth, layerWidth); string[] worldDesc = levelSpecification.Substring(1).Split('\n');
gridHeight = Math.Max(gridHeight, layerHeight); tileWidth = worldDesc.AsQueryable().Max(a => a.Length);
tileHeight = worldDesc.Length;
int dataIndex = -1; Debug.WriteLine("world size: {0}x{1}", tileWidth, tileHeight);
int tileWidth = layer.SelectToken("gridCellWidth").Value<int>(); for (int i = 0; i < tileWidth; i++) {
int tileHeight = layer.SelectToken("gridCellHeight").Value<int>(); for (int j = 0; j < tileHeight; j++) {
int textureWidth = Textures.Grassland.Get.Width / tileWidth; if (i < worldDesc[j].Length) {
foreach (int textureIndex in layer.SelectToken("data").Values<int>()) { char key = worldDesc[j][i];
dataIndex++; Terrain terrain = Terrain.FromSymbol(key);
if (textureIndex == -1) { if (terrain != null) {
continue; var position = new Rectangle(i * TileSize, j * TileSize, TileSize, TileSize);
} Tile tile = new Tile(terrain, position);
int i = dataIndex % layerWidth; if (tile.Terrain.IsObstacle) {
int j = dataIndex / layerWidth; tilesList.Add(tile);
Rectangle position = new Rectangle( } else {
i * tileWidth, j * tileHeight, tileWidth, tileHeight); decorationsList.Add(tile);
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()); tiles = tilesList.ToArray();
} decorations = decorationsList.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);
// 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
@ -132,7 +182,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]); CollisionTargets[i + 1] = new AABB(center, halfSize, tiles[i].Terrain);
} }
// Add a final synthetic collisionTarget on the right side of the world. // Add a final synthetic collisionTarget on the right side of the world.
@ -151,7 +201,7 @@ namespace SemiColinGames {
} }
void Reset() { void Reset() {
(Player, npcs) = ParseEntities(entitiesLayer); Player = new Player();
} }
public void DrawBackground(SpriteBatch spriteBatch) { public void DrawBackground(SpriteBatch spriteBatch) {