Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
SceneLayer.cs
1using System.Collections;
2using System.Drawing;
3using System.Runtime.Serialization;
11using Gondwana.Timers;
12using Newtonsoft.Json;
13
14namespace Gondwana.Scenes;
15
38[JsonObject(IsReference = true)]
39public class SceneLayer : IEnumerable<SceneLayerTile>, IDisposable
40{
41 #region events
42
52 public event Action<SceneLayer>? SceneLayerTileSizeChanged;
53
62 public event Action<SceneLayer>? VisibleChanged;
63
72 public event Action<SceneLayer>? WrappingChanged;
73
82 public event Action<SceneLayer>? ShowGridLinesChanged;
83
92 public event Action<SceneLayer>? ShowCollisionBoxesChanged;
93
102 public event Action<SceneLayer>? ZOrderChanged;
103
112 public event Action<SceneLayer>? ParallaxChanged;
113
122 public event Action<SceneLayer>? OriginPxChanged;
123
132 public event Action<SceneLayer>? Disposing;
133
134 #endregion events
135
136 #region private fields
137
138 private int _tileWidth; // rendered width
139 private int _tileHeight; // rendered height
140 private bool _visible; // is SceneLayer to be rendered; useful with multiple layers
141 private string _defaultTileCollisionProfile = CollisionProfileNames.World;
142 private bool _fixedGridRenderEligibilityDirty;
143 private bool _fixedGridRenderEligible = true;
144 private bool _sceneLayerTileArrayExternallyExposed;
145
146 [JsonIgnore]
147 internal float EffectOpacity { get; set; } = 1f;
148
149 [JsonIgnore]
150 internal float EffectReveal { get; set; } = 1f;
151
152 [JsonIgnore]
153 internal EffectDirection EffectRevealDirection { get; set; } = EffectDirection.FromLeftToRight;
154
155 [JsonIgnore]
156 internal PointF EffectOffsetFactor { get; set; } = PointF.Empty;
157
158 [JsonIgnore]
159 internal PointF EffectOffsetPx { get; set; } = PointF.Empty;
160
161 #endregion private fields
162
163 #region constructors / finalizer
164
175 protected internal SceneLayer(int columnCount,
176 int rowCount,
177 int width = 32,
178 int height = 32,
179 float parallax = 1,
180 CoordinateSystemTypes coordinateSystem = CoordinateSystemTypes.Orthogonal)
181 {
182 var tileArray = new SceneLayerTile[columnCount, rowCount];
183
184 InitValues(tileArray, width, height, parallax, coordinateSystem);
185 }
186
187 [JsonConstructor]
188 private SceneLayer() : this(0, 0) { }
189
190 [OnDeserialized]
191 private void OnDeserialized(StreamingContext context)
192 {
193 // Preserve the deserialized tiles and presentation state. Re-running
194 // InitValues here used to replace every loaded tile with an empty cell.
195 bool fastPathEligible = true;
196 int? fixedZOrder = null;
197
198 for (int x = 0; x < GridColumnCount; x++)
199 for (int y = 0; y < GridRowCount; y++)
200 {
201 var tile = _sceneLayerTileArray[x, y] ??= new SceneLayerTile(this);
202 tile.parentSceneLayer = this;
203 if (tile.Transform != TileTransform.Identity) TransformedTiles.Add(tile);
204 tile.sceneLayerCoordinates = new Point(x, y);
205
206 if (tile.GetType() != typeof(SceneLayerTile) || tile.EnableFog)
207 fastPathEligible = false;
208
209 if (fixedZOrder.HasValue && fixedZOrder.Value != tile.ZOrder)
210 fastPathEligible = false;
211 else
212 fixedZOrder ??= tile.ZOrder;
213 }
214
215 _fixedGridRenderEligible = fastPathEligible;
216 _fixedGridRenderEligibilityDirty = false;
217 _sceneLayerTileArrayExternallyExposed = false;
218 BuildTileColliders();
219 }
220
230 ~SceneLayer()
231 {
232 Dispose();
233 }
234
235 #endregion constructors / finalizer
236
237 #region properties
238
250 [JsonIgnore]
251 public TypedValueBag ValueBag { get; } = new();
252
253 [JsonIgnore]
254 internal ISceneLayerCoordinates CoordinateSystem { get; private set; } = new OrthogonalCoordinates();
255
280 [JsonProperty]
282 {
283 get
284 {
285 return CoordinateSystem switch
286 {
287 OrthogonalCoordinates => CoordinateSystemTypes.Orthogonal,
288 IsometricRhombicCoordinates => CoordinateSystemTypes.IsometricRhombic,
289 IsometricAxialCoordinates => CoordinateSystemTypes.IsometricAxial,
290 HexAxialFlatTopCoordinates => CoordinateSystemTypes.HexAxialFlatTop,
291 HexAxialPointedTop => CoordinateSystemTypes.HexAxialPointedTop,
292 ObliqueRightCoordinates => CoordinateSystemTypes.ObliqueRight,
293 ObliqueLeftCoordinates => CoordinateSystemTypes.ObliqueLeft,
294 _ => throw new InvalidOperationException($"Unknown coordinate system type: {CoordinateSystem.GetType().Name}")
295 };
296 }
297 set
298 {
299 CoordinateSystem = value switch
300 {
301 CoordinateSystemTypes.Orthogonal => new OrthogonalCoordinates(),
302 CoordinateSystemTypes.IsometricRhombic => new IsometricRhombicCoordinates(),
303 CoordinateSystemTypes.IsometricAxial => new IsometricAxialCoordinates(),
304 CoordinateSystemTypes.HexAxialFlatTop => new HexAxialFlatTopCoordinates(),
306 CoordinateSystemTypes.ObliqueRight => new ObliqueRightCoordinates(),
307 CoordinateSystemTypes.ObliqueLeft => new ObliqueLeftCoordinates(),
308 _ => throw new ArgumentOutOfRangeException(nameof(value), $"Unknown coordinate system type: {value}")
309 };
310 }
311 }
312
322 [JsonProperty]
323 public string ID { get; protected internal set; } = Guid.NewGuid().ToString();
324
334 [JsonIgnore]
335 public Scene Scene { get; internal set; }
336
337 [JsonIgnore]
338 internal RefreshQueue RefreshQueue { get; set; }
339
340 private int _zOrder = 0;
341
356 [JsonProperty]
357 public int ZOrder
358 {
359 get => _zOrder;
360 set
361 {
362 _zOrder = value;
363 ZOrderChanged?.Invoke(this);
364 }
365 }
366
367 private float _parallax = 1f; // 1 = default; <1 is slower, >1 is faster
368
389 [JsonProperty]
390 public float Parallax
391 {
392 get { return _parallax; }
393 set
394 {
395 _parallax = value;
396 ParallaxChanged?.Invoke(this);
397 }
398 }
399
415 [JsonProperty]
416 public int TileHeight
417 {
418 get { return _tileHeight; }
419 set
420 {
421 _tileHeight = value;
422 SceneLayerTileSizeChanged?.Invoke(this);
423 }
424 }
425
441 [JsonProperty]
442 public int TileWidth
443 {
444 get { return _tileWidth; }
445 set
446 {
447 _tileWidth = value;
448 SceneLayerTileSizeChanged?.Invoke(this);
449 }
450 }
451
467 [JsonProperty]
468 public bool Visible
469 {
470 get { return _visible; }
471 set
472 {
473 _visible = value;
474 VisibleChanged?.Invoke(this);
475 }
476 }
477
478 [JsonIgnore]
479 internal HashSet<SceneLayerTile> TransformedTiles { get; } = new();
480
481 [JsonProperty("SceneLayerTileArray")]
482 [JsonConverter(typeof(SceneLayerTileArrayConverter))]
483 private SceneLayerTile[,] _sceneLayerTileArray; // array of points; 2 dimensions (X, Y)
484
504 [JsonIgnore]
506 {
507 get
508 {
509 // Element replacement through the returned array bypasses SceneLayer mutation
510 // notifications. Disable the persistent fast path for this layer rather than risk
511 // recording custom/reparented tiles with fixed-grid assumptions.
512 _sceneLayerTileArrayExternallyExposed = true;
513 return _sceneLayerTileArray;
514 }
515 }
516
525 [JsonIgnore]
526 public int GridColumnCount => _sceneLayerTileArray.GetUpperBound(0) + 1;
527
536 [JsonIgnore]
537 public int GridRowCount => _sceneLayerTileArray.GetUpperBound(1) + 1;
538
539 [JsonProperty("WrapHorizontally")]
540 private bool _wrapHoriz = false;
541
556 [JsonIgnore]
558 {
559 get { return _wrapHoriz; }
560 set
561 {
562 _wrapHoriz = value;
563 WrappingChanged?.Invoke(this);
564 }
565 }
566
567 [JsonProperty("WrapVertically")]
568 private bool _wrapVerti = false;
569
584 [JsonIgnore]
585 public bool WrapVertically
586 {
587 get { return _wrapVerti; }
588 set
589 {
590 _wrapVerti = value;
591 WrappingChanged?.Invoke(this);
592 }
593 }
594
595 [JsonProperty("ShowGridLines")]
596 private bool showGridLines = false;
597
611 [JsonIgnore]
612 public bool ShowGridLines
613 {
614 get { return showGridLines; }
615 set
616 {
617 showGridLines = value;
618 ShowGridLinesChanged?.Invoke(this);
619 }
620 }
621
622 [JsonProperty("ShowCollisionBoxes")]
623 private bool showCollisionBoxes = false;
624
639 [JsonIgnore]
641 {
642 get { return showCollisionBoxes; }
643 set
644 {
645 showCollisionBoxes = value;
646 ShowCollisionBoxesChanged?.Invoke(this);
647 }
648 }
649
650 // World-space origin (in pixels) of this layer's (0,0) tile.
651 // Usually (0,0); can be shifted to move the entire layer as a block.
652 [JsonProperty("OriginPx")]
653 private Point _originPx = Point.Empty;
654
670 [JsonIgnore]
671 public Point OriginPx
672 {
673 get => _originPx;
674 set
675 {
676 if (_originPx == value) return;
677 _originPx = value;
678 OriginPxChanged?.Invoke(this);
679 }
680 }
681
691 [JsonIgnore]
692 public ColliderRegistry ColliderRegistry { get; private set; } = new();
693
694 [JsonIgnore]
695 internal CollisionResolver CollisionResolver { get; private set; } = null!;
696
704 [JsonIgnore]
706
711 [JsonProperty]
713 {
714 get => _defaultTileCollisionProfile;
715 set
716 {
717 if (string.IsNullOrWhiteSpace(value))
718 {
719 throw new ArgumentException(
720 "Default tile collision profile cannot be empty.",
721 nameof(value));
722 }
723
724 if (Scene is not null)
725 {
726 var profile = Scene.CollisionProfiles.Get(value);
727 _ = profile.ResolveCollisionGroup(Scene.CollisionGroups);
728 _ = profile.ResolveCollidesWith(Scene.CollisionGroups);
729 }
730
731 _defaultTileCollisionProfile = value;
732
733 if (Scene is not null && _sceneLayerTileArray is not null)
734 ApplyDefaultTileCollisionProfile();
735 }
736 }
737
738 #endregion properties
739
740 #region public methods
741
758 public void SetTileSize(int newWidth, int newHeight)
759 {
760 _tileWidth = newWidth;
761 _tileHeight = newHeight;
762 SceneLayerTileSizeChanged?.Invoke(this);
763 }
764
769 internal void ApplyDefaultTileCollisionProfile()
770 {
771 if (Scene is null || _sceneLayerTileArray is null)
772 return;
773
774 // Resolve once up front so an invalid profile fails before partially
775 // updating the layer.
776 Scene.CollisionProfiles.Get(_defaultTileCollisionProfile);
777
778 foreach (var tile in _sceneLayerTileArray)
779 tile?.SetCollisionProfile(_defaultTileCollisionProfile);
780 }
781
789 public PointF GridToWorldPx(PointF grid) => CoordinateSystem.GetAnchorPixelAtSceneLayerCoordinates(this, grid);
790
798 public PointF WorldPxToGrid(PointF worldPx) => CoordinateSystem.GetSceneLayerCoordinatesAtPixel(this, worldPx);
799
808 public SceneLayerTile? GetAdjacentTile(SceneLayerTile tile, CardinalDirections direction) => CoordinateSystem.GetAdjacentSceneLayerTile(tile, direction);
809
817 public PointF WrapGrid(PointF grid)
818 {
820 return grid;
821
822 _ = GetPeriod();
823
824 if (!float.IsFinite(grid.X) || !float.IsFinite(grid.Y))
825 throw new ArgumentOutOfRangeException(nameof(grid));
826
827 var wrapped = CoordinateSystem.FindEquivalentSceneLayerCoordinates(grid, GridColumnCount - 1, GridRowCount - 1);
828 return new PointF(WrapHorizontally ? wrapped.X : grid.X, WrapVertically ? wrapped.Y : grid.Y);
829 }
830
838 public SceneLayerTile? ResolveWrappedTile(int column, int row)
839 {
841 _ = GetPeriod();
842
843 static int Mod(int value, int period) { int remainder = value % period; return remainder < 0 ? remainder + period : remainder; }
844
845 return this[WrapHorizontally ? Mod(column, GridColumnCount) : column,
846 WrapVertically ? Mod(row, GridRowCount) : row];
847 }
848
858 public IEnumerable<PointF> GetWrappedOffsets(RectangleF contentBounds, RectangleF queryBounds)
859 {
861 return GetPeriod().Offsets(contentBounds, queryBounds);
862
863 return contentBounds.IntersectsWith(queryBounds) ? new[] { PointF.Empty } : Array.Empty<PointF>();
864 }
865
866 internal LayerPeriod GetPeriod() => CoordinateSystem.GetWrapPeriod(this);
867
875 public virtual RectangleF GetLayerBoundsPx()
876 {
877 if (GridColumnCount == 0 || GridRowCount == 0)
878 return RectangleF.Empty;
879
880 int maxCol = GridColumnCount - 1;
881 int maxRow = GridRowCount - 1;
882
883 // Corner tiles
884 var corners = new[]
885 {
886 this[0, 0],
887 this[maxCol, 0],
888 this[0, maxRow],
889 this[maxCol, maxRow]
890 };
891
892 RectangleF result = RectangleF.Empty;
893 bool hasBounds = false;
894
895 foreach (var tile in corners)
896 {
897 var px = CoordinateSystem.GetPixelRangeForTile(tile, includeOverhang: true);
898 var rf = RectangleF.FromLTRB(px.Left, px.Top, px.Right, px.Bottom);
899
900 if (!hasBounds)
901 {
902 result = rf;
903 hasBounds = true;
904 }
905 else
906 {
907 result = RectangleF.Union(result, rf);
908 }
909 }
910
911 // Interior placements can extend beyond the corner cells after rotation.
912 foreach (var tile in TransformedTiles)
913 result = RectangleF.Union(result, tile.DrawLocationWorld);
914
915 return result;
916 }
917
918 #endregion public methods
919
920 #region private methods
921
922 private void InitValues(SceneLayerTile[,] tileArray, int width, int height, float parallax, CoordinateSystemTypes coordinateSystem)
923 {
924 _sceneLayerTileArray = tileArray;
925 _parallax = parallax;
926 _tileWidth = width;
927 _tileHeight = height;
928 _visible = true;
929
930 _originPx = Point.Empty; // layer world origin
931 EffectOpacity = 1f;
932 EffectReveal = 1f;
933 EffectRevealDirection = EffectDirection.FromLeftToRight;
934 EffectOffsetFactor = PointF.Empty;
935 EffectOffsetPx = PointF.Empty;
936
937 CoordinateSystemType = coordinateSystem;
938
940 CollisionResolver = new CollisionResolver(ColliderRegistry);
941
942 // let each SceneLayerTile in array know its position in the array
943 SaveGridCoordinatesToSceneLayerTiles();
944 _fixedGridRenderEligible = true;
945 _fixedGridRenderEligibilityDirty = false;
946 _sceneLayerTileArrayExternallyExposed = false;
947 BuildTileColliders();
948 // Unbound scenes retain dirty regions for a future bitmap host. Once bound,
949 // the scene policy disables queue writes for full-frame GL rendering.
950 RefreshQueue = new RefreshQueue(() => Scene?.UsesDirtyRegionRendering ?? true);
951 }
952
953 private void SaveGridCoordinatesToSceneLayerTiles()
954 {
955 // let each SceneLayerTile in array know its position in the array
956 for (int X = 0; X <= _sceneLayerTileArray.GetUpperBound(0); X++)
957 {
958 for (int Y = 0; Y <= _sceneLayerTileArray.GetUpperBound(1); Y++)
959 {
960 _sceneLayerTileArray[X, Y] = new SceneLayerTile(this);
961 _sceneLayerTileArray[X, Y].sceneLayerCoordinates = new Point(X, Y);
962 }
963 }
964 }
965
966 private void BuildTileColliders()
967 {
968 foreach (var tile in _sceneLayerTileArray)
969 {
970 if (tile is null)
971 continue;
972
973 tile.EnsureCollider();
974 }
975 }
976
977 internal bool IsFixedGridSnapshotFastPathEligible
978 {
979 get
980 {
981 if (_sceneLayerTileArrayExternallyExposed)
982 return false;
983
984 if (_fixedGridRenderEligibilityDirty)
985 RecalculateFixedGridRenderEligibility();
986
987 return _fixedGridRenderEligible;
988 }
989 }
990
991 internal void InvalidateFixedGridRenderEligibility() =>
992 _fixedGridRenderEligibilityDirty = true;
993
994 private void RecalculateFixedGridRenderEligibility()
995 {
996 bool eligible = true;
997 int? fixedZOrder = null;
998
999 foreach (var tile in _sceneLayerTileArray)
1000 {
1001 if (tile is null ||
1002 tile.GetType() != typeof(SceneLayerTile) ||
1003 !ReferenceEquals(tile.SceneLayer, this) ||
1004 tile.EnableFog)
1005 {
1006 eligible = false;
1007 break;
1008 }
1009
1010 if (fixedZOrder.HasValue && fixedZOrder.Value != tile.ZOrder)
1011 {
1012 eligible = false;
1013 break;
1014 }
1015
1016 fixedZOrder ??= tile.ZOrder;
1017 }
1018
1019 _fixedGridRenderEligible = eligible;
1020 _fixedGridRenderEligibilityDirty = false;
1021 }
1022
1023 internal virtual List<IDrawable> GetDrawablesInWorldRect(Rectangle worldRect, bool includeOverhang = true) =>
1024 GetDrawablesInWorldRectCore(
1025 worldRect,
1026 includeOverhang,
1027 collectSortDiagnostics: false,
1028 out _);
1029
1030 internal virtual List<IDrawable> GetDrawablesInWorldRectWithDiagnostics(
1031 Rectangle worldRect,
1032 out long sortTicks,
1033 bool includeOverhang = true) =>
1034 GetDrawablesInWorldRectCore(
1035 worldRect,
1036 includeOverhang,
1037 collectSortDiagnostics: true,
1038 out sortTicks);
1039
1040 private List<IDrawable> GetDrawablesInWorldRectCore(
1041 Rectangle worldRect,
1042 bool includeOverhang,
1043 bool collectSortDiagnostics,
1044 out long sortTicks)
1045 {
1047 return GetWrappedDrawables(worldRect, collectSortDiagnostics, out sortTicks);
1048
1049 // Make selection rect covering so we never miss the edge tile.
1050 // Drawing is still clipped later, so over-selecting is safe.
1051 var queryRect = worldRect;
1052 queryRect.Inflate(TileWidth, TileHeight);
1053 queryRect.Inflate(1, 1);
1054
1055 // 1) Grid tiles. Every projection supplies a conservative render query whose
1056 // traversal follows fixed-tile depth order for that coordinate system.
1057 var renderTiles = CoordinateSystem.GetSceneLayerTilesForRendering(
1058 this,
1059 worldRect,
1060 includeOverhang);
1061 var sceneLayerTiles = renderTiles.Tiles;
1062
1063 var list = new List<IDrawable>(sceneLayerTiles.Count);
1064 bool uniformGridZOrder = true;
1065 int? gridZOrder = null;
1066
1067 for (int i = 0; i < sceneLayerTiles.Count; i++)
1068 {
1069 var tile = sceneLayerTiles[i];
1070
1071 if (!tile.Visible || !HasRenderableTileContent(tile))
1072 continue;
1073
1074 int tileZ = tile.ZOrder;
1075 if (gridZOrder.HasValue && gridZOrder.Value != tileZ)
1076 uniformGridZOrder = false;
1077 else
1078 gridZOrder ??= tileZ;
1079
1080 list.Add(tile);
1081 }
1082
1083 bool hasNonGridDrawables = false;
1084
1085 // 2) Sprites
1086 var sprites = SpriteManager.Instance.GetSpritesInWorldRectRange(
1087 queryRect,
1088 this,
1089 fullEnclosures: false);
1090
1091 for (int i = 0; i < sprites.Count; i++)
1092 {
1093 var sprite = sprites[i];
1094
1095 if (sprite is null)
1096 continue;
1097
1098 if (!sprite.VisualBoundsWorld.IntersectsWith(queryRect))
1099 continue;
1100
1101 list.Add(sprite);
1102 hasNonGridDrawables = true;
1103 }
1104
1105 // 3) DirectDrawing instances
1106 var drawings = DirectDrawingManager.Instance.GetDrawingsForLayer(this);
1107
1108 for (int i = 0; i < drawings.Count; i++)
1109 {
1110 var drawing = drawings[i];
1111
1112 if (!drawing.Visible)
1113 continue;
1114
1115 if (drawing.Mode != DirectDrawingMode.SceneLayer)
1116 continue;
1117
1118 if (!drawing.WorldBounds.IntersectsWith(worldRect))
1119 continue;
1120
1121 list.Add(drawing);
1122 hasNonGridDrawables = true;
1123 }
1124
1125 // Projection-specific render queries are emitted in fixed-tile depth order.
1126 // Per-tile Z differences or geometry-changing transforms still fall back to
1127 // verification/sort so the optimization cannot change rendering semantics.
1128 bool gridOrderGuaranteed =
1129 renderTiles.IsRenderOrdered &&
1130 uniformGridZOrder;
1131
1132 long sortStartTick = collectSortDiagnostics ? HighResTimer.GetCurrentTick() : 0;
1133 if (hasNonGridDrawables)
1134 {
1135 list.Sort(CompareDrawables);
1136 }
1137 else if (!gridOrderGuaranteed && !IsSorted(list, CompareDrawables))
1138 {
1139 list.Sort(CompareDrawables);
1140 }
1141
1142 sortTicks = collectSortDiagnostics
1143 ? Math.Max(0, HighResTimer.GetCurrentTick() - sortStartTick)
1144 : 0;
1145
1146 return list;
1147 }
1148
1149 private List<IDrawable> GetWrappedDrawables(
1150 Rectangle worldRect,
1151 bool collectSortDiagnostics,
1152 out long sortTicks)
1153 {
1154 var period = GetPeriod();
1155 var instances = new List<IDrawable>();
1156 void Add(IDrawable drawable, RectangleF bounds)
1157 {
1158 if (!drawable.Visible) return;
1159 foreach (var offset in period.Offsets(bounds, worldRect))
1160 {
1161 if (instances.Count >= 1_000_000)
1162 throw new InvalidOperationException("Too many visible wrapped instances.");
1163 instances.Add(new WrappedDrawable(drawable, this, offset));
1164 }
1165 }
1166
1167 // Inspect actual artwork bounds, including overhang, rather than assuming
1168 // a tile-sized margin can enclose all content.
1169 foreach (var tile in _sceneLayerTileArray)
1170 if (tile is not null) Add(tile, tile.DrawLocationWorld);
1171 foreach (var sprite in SpriteManager.Instance.AllSprites)
1172 if (ReferenceEquals(sprite.SceneLayer, this)) Add(sprite, sprite.VisualBoundsWorld);
1173 foreach (var drawing in DirectDrawingManager.Instance.GetDrawingsForLayer(this))
1174 if (drawing.Mode == DirectDrawingMode.SceneLayer) Add(drawing, drawing.WorldBounds);
1175
1176 long sortStartTick = collectSortDiagnostics ? HighResTimer.GetCurrentTick() : 0;
1177 instances.Sort(CompareWrappedDrawables);
1178 sortTicks = collectSortDiagnostics
1179 ? Math.Max(0, HighResTimer.GetCurrentTick() - sortStartTick)
1180 : 0;
1181
1182 return instances;
1183 }
1184
1185 private bool HasRenderableTileContent(SceneLayerTile tile)
1186 {
1187 // Derived tiles may provide custom Draw behavior even without a frame.
1188 if (tile.GetType() != typeof(SceneLayerTile))
1189 return true;
1190
1191 if (tile.CurrentFrame.Tilesheet is not null)
1192 return true;
1193
1194 // Empty cells still participate when a diagnostic/post-draw overlay needs
1195 // their geometry.
1196 return tile.EnableFog ||
1197 ShowGridLines ||
1198 (ShowCollisionBoxes && tile.CollisionsEnabled);
1199 }
1200
1201 private static bool IsSorted(
1202 List<IDrawable> drawables,
1203 Comparison<IDrawable> comparison)
1204 {
1205 for (int i = 1; i < drawables.Count; i++)
1206 {
1207 if (comparison(drawables[i - 1], drawables[i]) > 0)
1208 return false;
1209 }
1210
1211 return true;
1212 }
1213
1214 private static int CompareWrappedDrawables(IDrawable a, IDrawable b)
1215 {
1216 var x = (WrappedDrawable)a;
1217 var y = (WrappedDrawable)b;
1218 int z = a.ZOrder.CompareTo(b.ZOrder);
1219 if (z != 0) return z;
1220
1221 if (x.Owner is Tile ta && y.Owner is Tile tb)
1222 {
1223 if (ta.IsPositionFixed != tb.IsPositionFixed)
1224 return ta.IsPositionFixed ? -1 : 1;
1225
1226 float ay =
1227 (ta.IsPositionFixed
1228 ? ta.DrawLocationWorld.Top + ta.Overhang.Top
1229 : ta.DrawLocationWorld.Bottom - ta.Overhang.Bottom - 1) +
1230 x.Offset.Y;
1231 float by =
1232 (tb.IsPositionFixed
1233 ? tb.DrawLocationWorld.Top + tb.Overhang.Top
1234 : tb.DrawLocationWorld.Bottom - tb.Overhang.Bottom - 1) +
1235 y.Offset.Y;
1236 int depth = ay.CompareTo(by);
1237 if (depth != 0) return depth;
1238
1239 int horizontal =
1240 (ta.DrawLocationWorld.Left + x.Offset.X)
1241 .CompareTo(tb.DrawLocationWorld.Left + y.Offset.X);
1242 if (horizontal != 0) return horizontal;
1243 }
1244
1245 int identity = a.Id.CompareTo(b.Id);
1246 return identity != 0
1247 ? identity
1248 : (x.Offset.Y, x.Offset.X).CompareTo((y.Offset.Y, y.Offset.X));
1249 }
1250
1251 private static int CompareDrawables(IDrawable a, IDrawable b)
1252 {
1253 int z = a.ZOrder.CompareTo(b.ZOrder);
1254 if (z != 0)
1255 return z;
1256
1257 if (a is Tile ta && b is Tile tb)
1258 return ta.CompareTo(tb);
1259
1260 return a.Id.CompareTo(b.Id);
1261 }
1262
1263 #endregion private methods
1264
1265 #region indexers
1266
1281 public SceneLayerTile? this[int x, int y] => GetIndexer_NoWrap(x, y);
1282
1295 public SceneLayerTile? this[Point pt] => this[pt.X, pt.Y];
1296
1313 public SceneLayerTile? this[PointF ptF] => this[(int)ptF.X, (int)ptF.Y];
1314
1315 private SceneLayerTile? GetIndexer_NoWrap(int x, int y)
1316 {
1317 if (x > _sceneLayerTileArray.GetUpperBound(0)
1318 || y > _sceneLayerTileArray.GetUpperBound(1)
1319 || x < 0
1320 || y < 0)
1321 return null;
1322 else
1323 return _sceneLayerTileArray[x, y];
1324 }
1325
1326 #endregion indexers
1327
1328 #region IEnumerable Members
1329
1339 public IEnumerator GetEnumerator() => ((IEnumerable<SceneLayerTile>)this).GetEnumerator();
1340
1351 IEnumerator<SceneLayerTile> IEnumerable<SceneLayerTile>.GetEnumerator()
1352 {
1353 for (int x = 0; x <= _sceneLayerTileArray.GetUpperBound(0); x++)
1354 {
1355 for (int y = 0; y <= _sceneLayerTileArray.GetUpperBound(1); y++)
1356 {
1357 yield return _sceneLayerTileArray[x, y];
1358 }
1359 }
1360 }
1361
1362 #endregion IEnumerable Members
1363
1364 #region IDisposable Members
1365
1388 public virtual void Dispose()
1389 {
1390 GC.SuppressFinalize(this);
1391
1392 Disposing?.Invoke(this);
1393
1394 foreach (SceneLayerTile gridPt in this)
1395 gridPt.Dispose();
1396
1397 // cancel all subscriptions to this object
1399 VisibleChanged = null;
1400 WrappingChanged = null;
1401 Disposing = null;
1402 }
1403
1404 #endregion IDisposable Members
1405
1406 #region empty SceneLayer
1407
1431 public static SceneLayer Empty { get; } = new EmptySceneLayer();
1432
1433 private sealed class EmptySceneLayer : SceneLayer
1434 {
1435 internal EmptySceneLayer()
1436 : base(columnCount: 0, rowCount: 0, width: 1, height: 1)
1437 {
1438 Visible = false;
1439 ZOrder = int.MinValue;
1440 Parallax = 1f;
1441 }
1442 }
1443
1444 #endregion empty SceneLayer
1445}
Manages registration and querying of colliders, separating them into static and dynamic collections f...
Manages collision group definitions and provides bitwise mask values for collision filtering....
CollisionProfile Get(string name)
Gets a previously defined profile.
Represents the values stored at a single location on a SceneLayer.
override void Dispose()
Releases the resources owned by this instance.
Represents a single layer within a Scene, containing a grid of tiles and supporting various coordinat...
Definition SceneLayer.cs:40
IEnumerator GetEnumerator()
Returns an enumerator that iterates through the layer's tiles.
SceneLayerTile? GetAdjacentTile(SceneLayerTile tile, CardinalDirections direction)
Returns the neighboring tile in the given direction, or null if the direction would move off the laye...
bool WrapVertically
Gets or sets a value indicating whether this layer repeats along its row axis at the grid boundaries.
CollisionGroupRegistry CollisionGroups
Gets the registry of collision groups used to organize and manage collision detection within the scen...
bool Visible
Gets or sets a value indicating whether this layer should be rendered.
virtual RectangleF GetLayerBoundsPx()
Computes the world-space pixel bounding rectangle for this SceneLayer. This uses the coordinate syste...
PointF WrapGrid(PointF grid)
Resolves a virtual grid coordinate to canonical coordinates on enabled wrapping axes....
SceneLayerTile[,] SceneLayerTileArray
Gets the two-dimensional array of tiles that comprise this layer's grid.
Action< SceneLayer >? ShowGridLinesChanged
Occurs when the grid line display setting changes.
Definition SceneLayer.cs:82
Point OriginPx
Gets or sets the world-space pixel origin of this layer's (0,0) tile position.
void SetTileSize(int newWidth, int newHeight)
Sets both the width and height of tiles in this layer atomically, raising only a single SceneLayerTil...
string ID
Gets or sets the unique identifier for this layer.
Action< SceneLayer >? SceneLayerTileSizeChanged
Occurs when the tile dimensions of this layer change.
Definition SceneLayer.cs:52
IEnumerable< PointF > GetWrappedOffsets(RectangleF contentBounds, RectangleF queryBounds)
Returns world-space translations of a canonical content rectangle that intersect a query rectangle....
CoordinateSystemTypes CoordinateSystemType
Gets or sets the coordinate system type used by this layer for transforming between grid coordinates ...
bool WrapHorizontally
Gets or sets a value indicating whether this layer repeats along its column axis at the grid boundari...
ColliderRegistry ColliderRegistry
Gets the collision world associated with this scene, used for physics and collision detection.
bool ShowGridLines
Gets or sets a value indicating whether grid lines should be rendered over this layer for debugging.
PointF WorldPxToGrid(PointF worldPx)
Converts a world-space pixel position into this layer's grid coordinates. This uses the layer's activ...
int GridRowCount
Gets the number of rows (tiles high) in this layer's grid.
int TileHeight
Gets or sets the height of each tile in this layer, measured in pixels.
TypedValueBag ValueBag
Gets the extensible value bag for storing arbitrary layer-specific metadata.
string DefaultTileCollisionProfile
Gets or sets the scene collision profile applied to fixed tiles on this layer. The default is Collisi...
SceneLayerTile? ResolveWrappedTile(int column, int row)
Resolves a virtual grid cell to its canonical tile. Only enabled axes wrap; an out-of-range disabled ...
PointF GridToWorldPx(PointF grid)
Converts a grid coordinate (col,row) into the projection-defined world-space pixel anchor for that ti...
Action< SceneLayer >? VisibleChanged
Occurs when the visibility state of this layer changes.
Definition SceneLayer.cs:62
int GridColumnCount
Gets the number of columns (tiles wide) in this layer's grid.
Action< SceneLayer >? ParallaxChanged
Occurs when the parallax scrolling factor of this layer changes.
Scene Scene
Gets or sets the parent scene that contains this layer.
float Parallax
Gets or sets the parallax scrolling factor for this layer.
int ZOrder
Gets or sets the z-order (rendering depth) of this layer within its parent scene.
Action< SceneLayer >? WrappingChanged
Occurs when the wrapping configuration of this layer changes.
Definition SceneLayer.cs:72
Action< SceneLayer >? ZOrderChanged
Occurs when the z-order (rendering depth) of this layer changes.
int TileWidth
Gets or sets the width of each tile in this layer, measured in pixels.
bool ShowCollisionBoxes
Gets or sets a value indicating whether collision boxes should be rendered over this layer for debugg...
virtual void Dispose()
Releases all resources used by the SceneLayer and disposes all tiles.
static SceneLayer Empty
Gets the singleton empty scene layer instance.
Action< SceneLayer >? OriginPxChanged
Occurs when the world-space origin point of this layer changes.
Action< SceneLayer >? Disposing
Occurs when this layer is being disposed.
Action< SceneLayer >? ShowCollisionBoxesChanged
Occurs when the collision box display setting changes.
Definition SceneLayer.cs:92
Represents a game scene containing multiple layers of tiles, sprites, and game objects....
Definition Scene.cs:34
CollisionProfileRegistry CollisionProfiles
Gets the named collision-filtering profiles used by layers and sprites in this scene....
Definition Scene.cs:335
A flexible, strongly-typed value container intended for runtime-attached metadata and ephemeral engin...
bool Visible
Gets a value indicating whether the object is visible.
Definition IDrawable.cs:28
Guid Id
Auto-assigned unique identifier for the object.
Definition IDrawable.cs:18
int ZOrder
Gets the z-order of the element, which determines its visual stacking order relative to other element...
Definition IDrawable.cs:34
CardinalDirections
Represents the eight cardinal and intercardinal directions of the compass.
CoordinateSystemTypes
Identifies the layout and math rules used to map tiles to world pixels and world pixels back to grid ...
@ ObliqueRight
Oblique projection using a right-receding, sheared square lattice. Numeric value 5 is retained for co...
@ Orthogonal
Axis-aligned square grid (Cartesian lattice).
@ HexAxialFlatTop
Hexagonal grid using axial coordinates with flat-topped hexes.
@ HexAxialPointedTop
Hexagonal grid using axial coordinates with pointy-topped hexes.
@ IsometricAxial
Isometric projection using an underlying square lattice with diagonal basis vectors.
@ ObliqueLeft
Oblique projection using a left-receding, sheared square lattice.
@ IsometricRhombic
Isometric projection using a rhombic (diamond) lattice.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ SceneLayer
Performs direct drawing within a scene layer using world-space coordinates.
TileTransform
The eight orthogonal orientations of artwork placed in a fixed scene cell.
EffectDirection
Specifies the direction in which a directional display effect travels.