From ca5c08db90c49a5c89a8bfb65a5dc977bb712aff Mon Sep 17 00:00:00 2001 From: stallratte <48615771+stallratte@users.noreply.github.com> Date: Fri, 10 May 2024 18:43:10 +0200 Subject: [PATCH] Tile layer memory consumption (#797) * tile layers now need less memory * reducing loading time when using base64 encoded layers --- .../Assets/Tiled/Runtime/Layer.Runtime.cs | 42 +++++++--- .../Assets/Tiled/Runtime/TiledMapLoader.cs | 80 +++++++++++++------ .../Assets/Tiled/Runtime/TiledRendering.cs | 33 ++++---- Nez.Portable/Assets/Tiled/TiledTypes/Layer.cs | 29 +++---- .../Tiled/TiledTypes/TmxLayerTileExt.cs | 4 +- .../ECS/Components/Physics/TiledMapMover.cs | 60 +++++++------- 6 files changed, 148 insertions(+), 100 deletions(-) diff --git a/Nez.Portable/Assets/Tiled/Runtime/Layer.Runtime.cs b/Nez.Portable/Assets/Tiled/Runtime/Layer.Runtime.cs index e26930c71..0bc317e87 100644 --- a/Nez.Portable/Assets/Tiled/Runtime/Layer.Runtime.cs +++ b/Nez.Portable/Assets/Tiled/Runtime/Layer.Runtime.cs @@ -1,17 +1,30 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.Xna.Framework; namespace Nez.Tiled { public partial class TmxLayer : ITmxLayer { - /// - /// gets the TmxLayerTile at the x/y coordinates. Note that these are tile coordinates not world coordinates! - /// - /// The tile. - /// The x coordinate. - /// The y coordinate. - public TmxLayerTile GetTile(int x, int y) => Tiles[x + y * Width]; + + /// + /// gets the TmxLayerTile at the x/y coordinates. Note that these are tile coordinates not world coordinates! + /// + /// The tile. + /// The x coordinate. + /// The y coordinate. + public TmxLayerTile GetTile(int x, int y) + { + Tiles.TryGetValue(Grid[x + y * Width], out var tmxLayerTile); + + return tmxLayerTile; + } + + public TmxLayerTile GetTile(int index) + { + Tiles.TryGetValue(Grid[index], out var tmxLayerTile); + + return tmxLayerTile; + } /// /// gets the TmxLayerTile at the given world position @@ -27,7 +40,7 @@ public TmxLayerTile GetTileAtWorldPosition(Vector2 pos) /// public List GetCollisionRectangles() { - var checkedIndexes = new bool?[Tiles.Length]; + var checkedIndexes = new bool?[Grid.Length]; var rectangles = new List(); var startCol = -1; var index = -1; @@ -132,10 +145,13 @@ public List GetTilesIntersectingBounds(Rectangle bounds) /// call this method or update the TmxLayerTile.tileset manually! /// /// The tile. + /// The x coordinate. + /// The y coordinate. /// Tile. - public TmxLayerTile SetTile(TmxLayerTile tile) + public TmxLayerTile SetTile(int x, int y, TmxLayerTile tile) { - Tiles[tile.X + tile.Y * Width] = tile; + Grid[x + y * Width] = tile.RawGid; + Tiles.Add(tile.RawGid, tile); tile.Tileset = Map.GetTilesetForTileGid(tile.Gid); return tile; @@ -146,6 +162,8 @@ public TmxLayerTile SetTile(TmxLayerTile tile) /// /// The x coordinate. /// The y coordinate. - public void RemoveTile(int x, int y) => Tiles[x + y * Width] = null; + public void RemoveTile(int x, int y) { + Grid[x + y * Width] = 0; + } } } \ No newline at end of file diff --git a/Nez.Portable/Assets/Tiled/Runtime/TiledMapLoader.cs b/Nez.Portable/Assets/Tiled/Runtime/TiledMapLoader.cs index c81200118..6325b3d37 100644 --- a/Nez.Portable/Assets/Tiled/Runtime/TiledMapLoader.cs +++ b/Nez.Portable/Assets/Tiled/Runtime/TiledMapLoader.cs @@ -322,22 +322,30 @@ public static TmxLayer LoadTmxLayer(this TmxLayer layer, TmxMap map, XElement xL var xData = xLayer.Element("data"); var encoding = (string)xData.Attribute("encoding"); - - layer.Tiles = new TmxLayerTile[width * height]; + + layer.Grid = new uint[width * height]; + layer.Tiles = new Dictionary(); + if (encoding == "base64") { var decodedStream = new TmxBase64Data(xData); - var stream = decodedStream.Data; - + var index = 0; - using (var br = new BinaryReader(stream)) - { - for (var j = 0; j < height; j++) - { - for (var i = 0; i < width; i++) - { - var gid = br.ReadUInt32(); - layer.Tiles[index++] = gid != 0 ? new TmxLayerTile(map, gid, i, j) : null; + using (var stream = decodedStream.Data) { + using (var br = new BinaryReader(stream)) { + const int uintSizeInBytes = sizeof(uint); + + var buffer = new byte[uintSizeInBytes * 1024]; + int bytesRead; + + while ((bytesRead = br.Read(buffer, 0, buffer.Length)) > 0) { + var numberOfUIntValuesRead = bytesRead / uintSizeInBytes; + + for (var i = 0; i < numberOfUIntValuesRead; i++) { + var gid = BitConverter.ToUInt32(buffer, i * uintSizeInBytes); + AddTile(layer, map, gid); + layer.Grid[index++] = gid; + } } } } @@ -346,13 +354,19 @@ public static TmxLayer LoadTmxLayer(this TmxLayer layer, TmxMap map, XElement xL { var csvData = xData.Value; int k = 0; - foreach (var s in csvData.Split(',')) + + int startIndex = 0; + for (var i = 0; i < csvData.Length; i++) { - var gid = uint.Parse(s.Trim()); - var x = k % width; - var y = k / width; - - layer.Tiles[k++] = gid != 0 ? new TmxLayerTile(map, gid, x, y) : null; + if (csvData[i] == ',') + { + var gid = ParseString(csvData, startIndex, i - startIndex); + + AddTile(layer, map, gid); + layer.Grid[k++] = gid; + + startIndex = i + 1; + } } } else if (encoding == null) @@ -361,11 +375,9 @@ public static TmxLayer LoadTmxLayer(this TmxLayer layer, TmxMap map, XElement xL foreach (var e in xData.Elements("tile")) { var gid = (uint?)e.Attribute("gid") ?? 0; - - var x = k % width; - var y = k / width; - - layer.Tiles[k++] = gid != 0 ? new TmxLayerTile(map, gid, x, y) : null; + + AddTile(layer, map, gid); + layer.Grid[k++] = gid; } } else throw new Exception("TmxLayer: Unknown encoding."); @@ -375,6 +387,24 @@ public static TmxLayer LoadTmxLayer(this TmxLayer layer, TmxMap map, XElement xL return layer; } + private static void AddTile(TmxLayer layer, TmxMap map, uint gid) { + if (gid != 0 && !layer.Tiles.ContainsKey(gid)) + { + layer.Tiles.Add(gid, new TmxLayerTile(map, gid)); + } + } + + private static uint ParseString(string str, int startIndex, int length) + { + uint result = 0; + for (int i = startIndex; i < startIndex + length; i++) + { + if(char.IsDigit(str[i])) + result = result * 10u + (uint)(str[i] - '0'); + } + return result; + } + public static TmxObjectGroup LoadTmxObjectGroup(this TmxObjectGroup group, TmxMap map, XElement xObjectGroup) { group.Map = map; @@ -445,7 +475,7 @@ public static TmxObject LoadTmxObject(this TmxObject obj, TmxMap map, XElement x if (xGid != null) { - obj.Tile = new TmxLayerTile(map, (uint)xGid, Convert.ToInt32(Math.Round(obj.X)), Convert.ToInt32(Math.Round(obj.Y))); + obj.Tile = new TmxLayerTile(map, (uint)xGid); obj.ObjectType = TmxObjectType.Tile; } else if (xEllipse != null) @@ -499,7 +529,7 @@ public static TmxObject LoadTmxObjectFromTemplate(this TmxObject obj, TmxMap map if (xGid != null) { - obj.Tile = new TmxLayerTile(map, (uint)xGid, Convert.ToInt32(Math.Round(obj.X)), Convert.ToInt32(Math.Round(obj.Y))); + obj.Tile = new TmxLayerTile(map, (uint)xGid); obj.ObjectType = TmxObjectType.Tile; } else if (xEllipse != null) diff --git a/Nez.Portable/Assets/Tiled/Runtime/TiledRendering.cs b/Nez.Portable/Assets/Tiled/Runtime/TiledRendering.cs index d26f627a1..3beb99490 100644 --- a/Nez.Portable/Assets/Tiled/Runtime/TiledRendering.cs +++ b/Nez.Portable/Assets/Tiled/Runtime/TiledRendering.cs @@ -69,13 +69,16 @@ public static void RenderLayer(TmxLayer layer, Batcher batcher, Vector2 position var color = Color.White; color.A = (byte)(layer.Opacity * 255); - for (var i = 0; i < layer.Tiles.Length; i++) + for (var i = 0; i < layer.Grid.Length; i++) { - var tile = layer.Tiles[i]; + var tile = layer.GetTile(i); if (tile == null) continue; - RenderTile(tile, batcher, position, + var x = i % layer.Map.TileWidth; + var y = i / layer.Map.TileWidth; + + RenderTile(tile,x ,y, batcher, position, scale, tileWidth, tileHeight, color, layerDepth, layer.Map.Orientation, layer.Map.Width, layer.Map.Height); @@ -118,7 +121,7 @@ public static void RenderLayer(TmxLayer layer, Batcher batcher, Vector2 position { var tile = layer.GetTile(x, y); if (tile != null) - RenderTile(tile, batcher, position, + RenderTile(tile, x ,y, batcher, position, scale, tileWidth, tileHeight, color, layerDepth, layer.Map.Orientation, layer.Map.Width, layer.Map.Height); @@ -180,7 +183,7 @@ private static (Point, Point, RectangleF) GetLayerCullBounds(TmxLayer layer, Vec } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RenderTile(TmxLayerTile tile, Batcher batcher, Vector2 position, + public static void RenderTile(TmxLayerTile tile, int tileX, int tileY, Batcher batcher, Vector2 position, Vector2 scale, float tileWidth, float tileHeight, Color color, float layerDepth, OrientationType orientation, int mapWidth, int mapHeight) @@ -202,23 +205,23 @@ public static void RenderTile(TmxLayerTile tile, Batcher batcher, Vector2 positi switch (orientation) { case OrientationType.Hexagonal: - bool isEvenRow = tile.Y % 2 == 0; + bool isEvenRow = tileY % 2 == 0; if (isEvenRow) { - tx = tile.X * tileWidth; - ty = tile.Y * tileHeight * 0.75f; + tx = tileX * tileWidth; + ty = tileY * tileHeight * 0.75f; } else { - tx = (tileWidth / 2) + (tile.X * tileWidth); - ty = tile.Y * tileHeight * 0.75f; + tx = (tileWidth / 2) + (tileX * tileWidth); + ty = tileY * tileHeight * 0.75f; } break; case OrientationType.Isometric: - tx = tile.X * tileWidth / 2 - tile.Y * tileWidth / 2 + (mapHeight - 1) * tileWidth / 2; - ty = tile.Y * tileHeight / 2 + tile.X * tileHeight / 2; + tx = tileX * tileWidth / 2 - tileY * tileWidth / 2 + (mapHeight - 1) * tileWidth / 2; + ty = tileY * tileHeight / 2 + tileX * tileHeight / 2; break; case OrientationType.Staggered: throw new NotImplementedException( @@ -228,8 +231,8 @@ public static void RenderTile(TmxLayerTile tile, Batcher batcher, Vector2 positi case OrientationType.Unknown: case OrientationType.Orthogonal: default: - tx = tile.X * tileWidth; - ty = tile.Y * tileHeight; + tx = tileX * tileWidth; + ty = tileY * tileHeight; break; } @@ -322,8 +325,6 @@ public static void RenderObjectGroup(TmxObjectGroup objGroup, Batcher batcher, V batcher.DrawPixel(pos, objGroup.Color, (int)size); goto default; case TmxObjectType.Tile: - var tx = obj.Tile.X * objGroup.Map.TileWidth * scale.X; - var ty = obj.Tile.Y * objGroup.Map.TileHeight * scale.Y; var spriteEffects = SpriteEffects.None; if (obj.Tile.HorizontalFlip) diff --git a/Nez.Portable/Assets/Tiled/TiledTypes/Layer.cs b/Nez.Portable/Assets/Tiled/TiledTypes/Layer.cs index f02651563..fc945ba5f 100644 --- a/Nez.Portable/Assets/Tiled/TiledTypes/Layer.cs +++ b/Nez.Portable/Assets/Tiled/TiledTypes/Layer.cs @@ -27,21 +27,17 @@ public partial class TmxLayer : ITmxLayer /// height in tiles for this layer. Always the same as the map height for fixed-size maps. /// public int Height; - public TmxLayerTile[] Tiles; - + public uint[] Grid; + public Dictionary Tiles; + /// /// returns the TmxLayerTile with gid. This is a slow lookup so cache it! /// /// /// - public TmxLayerTile GetTileWithGid(int gid) - { - for (var i = 0; i < Tiles.Length; i++) - { - if (Tiles[i] != null && Tiles[i].Gid == gid) - return Tiles[i]; - } - return null; + public TmxLayerTile GetTileWithGid(uint gid) { + Tiles.TryGetValue(gid, out var result); + return result; } } @@ -52,10 +48,9 @@ public class TmxLayerTile const uint FLIPPED_DIAGONALLY_FLAG = 0x20000000; public TmxTileset Tileset; + // GID which still contains the flip flags. + public uint RawGid; public int Gid; - public int X; - public int Y; - public Vector2 Position => new Vector2(X, Y); public bool HorizontalFlip; public bool VerticalFlip; public bool DiagonalFlip; @@ -88,12 +83,10 @@ public TmxTilesetTile TilesetTile return Tileset.Tiles[_tilesetTileIndex.Value]; } } - - public TmxLayerTile(TmxMap map, uint id, int x, int y) + + public TmxLayerTile(TmxMap map, uint rawGid) { - X = x; - Y = y; - var rawGid = id; + RawGid = rawGid; // Scan for tile flip bit flags bool flip; diff --git a/Nez.Portable/Assets/Tiled/TiledTypes/TmxLayerTileExt.cs b/Nez.Portable/Assets/Tiled/TiledTypes/TmxLayerTileExt.cs index 72a1bbb27..7b6aef99d 100644 --- a/Nez.Portable/Assets/Tiled/TiledTypes/TmxLayerTileExt.cs +++ b/Nez.Portable/Assets/Tiled/TiledTypes/TmxLayerTileExt.cs @@ -91,10 +91,10 @@ public static Edge GetHighestSlopeEdge(this TmxLayerTile self) /// returns the nearest edge to worldPosition /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Edge GetNearestEdge(this TmxLayerTile self, int worldPosition) + public static Edge GetNearestEdge(this TmxLayerTile self, int x, int worldPosition) { var tileWidth = self.Tileset.Map.TileWidth; - var tileMiddleWorldPosition = self.X * tileWidth + tileWidth / 2; + var tileMiddleWorldPosition = x * tileWidth + tileWidth / 2; return worldPosition < tileMiddleWorldPosition ? Edge.Left : Edge.Right; } } diff --git a/Nez.Portable/ECS/Components/Physics/TiledMapMover.cs b/Nez.Portable/ECS/Components/Physics/TiledMapMover.cs index b119b6c27..4026f224c 100644 --- a/Nez.Portable/ECS/Components/Physics/TiledMapMover.cs +++ b/Nez.Portable/ECS/Components/Physics/TiledMapMover.cs @@ -113,9 +113,9 @@ public override string ToString() public TmxMap TiledMap; /// - /// temporary storage for all the tiles that intersect the bounds being checked + /// temporary storage for all the coordinates of tiles that intersect the bounds being checked /// - List _collidingTiles = new List(); + List _collidingTilesCoordinates = new List(); /// /// temporary storage to avoid having to pass it around @@ -253,24 +253,26 @@ bool TestMapCollision(Rectangle collisionRect, Edge direction, CollisionState co var shouldTestSlopes = side.IsVertical(); PopulateCollidingTiles(collisionRect, direction); - for (var i = 0; i < _collidingTiles.Count; i++) + for (var i = 0; i < _collidingTilesCoordinates.Count; i++) { - if (_collidingTiles[i] == null) + var collidingTile = CollisionLayer.GetTile(_collidingTilesCoordinates[i].X, _collidingTilesCoordinates[i].Y); + + if (collidingTile == null) continue; // disregard horizontal collisions with tiles on the same row as a slope if the last tile we were grounded on was a slope. // the y collision response will push us up on the slope. if (direction.IsHorizontal() && collisionState._lastGroundTile != null && - collisionState._lastGroundTile.IsSlope() && IsSlopeCollisionRow(_collidingTiles[i].Y)) + collisionState._lastGroundTile.IsSlope() && IsSlopeCollisionRow(_collidingTilesCoordinates[i].Y)) continue; - if (TestTileCollision(_collidingTiles[i], side, perpindicularPosition, leadingPosition, + if (TestTileCollision(collidingTile, _collidingTilesCoordinates[i].X, _collidingTilesCoordinates[i].Y,side, perpindicularPosition, leadingPosition, shouldTestSlopes, out collisionResponse)) { // store off our last ground tile if we collided below if (direction == Edge.Bottom) { - collisionState._lastGroundTile = _collidingTiles[i]; + collisionState._lastGroundTile = collidingTile; collisionState.IsGroundedOnOneWayPlatform = collisionState._lastGroundTile.IsOneWayPlatform(); } @@ -282,14 +284,14 @@ bool TestMapCollision(Rectangle collisionRect, Edge direction, CollisionState co { // if grounded on a slope and intersecting a slope or if grounded on a wall and intersecting a tall slope we go sticky. // tall slope here means one where the the slopeTopLeft/Right is 0, i.e. it connects to a wall - var isHighSlopeNearest = _collidingTiles[i].IsSlope() && - _collidingTiles[i].GetNearestEdge(perpindicularPosition) == - _collidingTiles[i].GetHighestSlopeEdge(); - if ((collisionState._lastGroundTile.IsSlope() && _collidingTiles[i].IsSlope()) || + var isHighSlopeNearest = collidingTile.IsSlope() && + collidingTile.GetNearestEdge(perpindicularPosition, _collidingTilesCoordinates[i].X) == + collidingTile.GetHighestSlopeEdge(); + if ((collisionState._lastGroundTile.IsSlope() && collidingTile.IsSlope()) || (!collisionState._lastGroundTile.IsSlope() && isHighSlopeNearest)) { // store off our last ground tile if we collided below - collisionState._lastGroundTile = _collidingTiles[i]; + collisionState._lastGroundTile = collidingTile; return true; } } @@ -305,9 +307,11 @@ bool TestMapCollision(Rectangle collisionRect, Edge direction, CollisionState co /// the row to check bool IsSlopeCollisionRow(int rowY) { - for (var i = 0; i < _collidingTiles.Count; i++) + for (var i = 0; i < _collidingTilesCoordinates.Count; i++) { - if (_collidingTiles[i] != null && _collidingTiles[i].IsSlope() && _collidingTiles[i].Y == rowY) + var collidingTile = CollisionLayer.GetTile(_collidingTilesCoordinates[i].X, _collidingTilesCoordinates[i].Y); + + if (collidingTile != null && collidingTile.IsSlope() && _collidingTilesCoordinates[i].Y == rowY) return true; } @@ -319,12 +323,14 @@ bool IsSlopeCollisionRow(int rowY) /// /// The tile collision. /// Tile. + /// /// x position of the tile. + /// /// y position of the tile.. /// the opposite side of movement, the side the leading edge will collide with /// Perpindicular position. /// Leading position. /// Should test slopes. /// Collision response. - bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosition, int leadingPosition, + bool TestTileCollision(TmxLayerTile tile, int x, int y, Edge edgeToTest, int perpindicularPosition, int leadingPosition, bool shouldTestSlopes, out int collisionResponse) { collisionResponse = leadingPosition; @@ -337,7 +343,7 @@ bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosi return false; // our response should be the top of the platform - collisionResponse = TiledMap.TileToWorldPositionX(tile.Y); + collisionResponse = TiledMap.TileToWorldPositionX(y); return _boxColliderBounds.Bottom <= collisionResponse; } @@ -347,15 +353,15 @@ bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosi // and we were not intesecting the tile before moving. // this prevents clipping through a tile when hitting its edge: -> |\ if (edgeToTest.IsHorizontal() && tile.IsSlope() && - tile.GetNearestEdge(leadingPosition) == tile.GetHighestSlopeEdge()) + tile.GetNearestEdge(leadingPosition, x) == tile.GetHighestSlopeEdge()) { var moveDir = edgeToTest.OppositeEdge(); var leadingPositionPreMovement = _boxColliderBounds.GetSide(moveDir); // we need the tile x position that is on the opposite side of our move direction. Moving right we want the left edge var tileX = moveDir == Edge.Right - ? TiledMap.TileToWorldPositionX(tile.X) - : TiledMap.TileToWorldPositionX(tile.X + 1); + ? TiledMap.TileToWorldPositionX(x) + : TiledMap.TileToWorldPositionX(x + 1); // using the edge before movement, we see if we were colliding before moving. var wasCollidingBeforeMove = moveDir == Edge.Right @@ -372,16 +378,16 @@ bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosi switch (edgeToTest) { case Edge.Top: - collisionResponse = TiledMap.TileToWorldPositionY(tile.Y); + collisionResponse = TiledMap.TileToWorldPositionY(y); break; case Edge.Bottom: - collisionResponse = TiledMap.TileToWorldPositionY(tile.Y + 1); + collisionResponse = TiledMap.TileToWorldPositionY(y + 1); break; case Edge.Left: - collisionResponse = TiledMap.TileToWorldPositionX(tile.X); + collisionResponse = TiledMap.TileToWorldPositionX(x); break; case Edge.Right: - collisionResponse = TiledMap.TileToWorldPositionX(tile.X + 1); + collisionResponse = TiledMap.TileToWorldPositionX(x + 1); break; } @@ -390,8 +396,8 @@ bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosi if (shouldTestSlopes) { - var tileWorldX = TiledMap.TileToWorldPositionX(tile.X); - var tileWorldY = TiledMap.TileToWorldPositionX(tile.Y); + var tileWorldX = TiledMap.TileToWorldPositionX(x); + var tileWorldY = TiledMap.TileToWorldPositionX(y); var slope = tile.GetSlope(); var offset = tile.GetSlopeOffset(); @@ -424,7 +430,7 @@ bool TestTileCollision(TmxLayerTile tile, Edge edgeToTest, int perpindicularPosi /// Direction. void PopulateCollidingTiles(Rectangle bounds, Edge direction) { - _collidingTiles.Clear(); + _collidingTilesCoordinates.Clear(); var isHorizontal = direction.IsHorizontal(); var primaryAxis = isHorizontal ? Axis.X : Axis.Y; var oppositeAxis = primaryAxis == Axis.X ? Axis.Y : Axis.X; @@ -451,7 +457,7 @@ void PopulateCollidingTiles(Rectangle bounds, Edge direction) { var col = isHorizontal ? primary : secondary; var row = !isHorizontal ? primary : secondary; - _collidingTiles.Add(CollisionLayer.GetTile(col, row)); + _collidingTilesCoordinates.Add(new Point(col, row)); #if DEBUG_MOVER if( direction.isHorizontal() )