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
TilesheetRegion.cs
1using System.Drawing;
4using SkiaSharp;
5
7
11public sealed class TilesheetRegion : IDisposable
12{
16 public static readonly string DefaultRegionName = "default";
17
18 private TilesheetRegionSlice?[,]? _tileCache;
19 private readonly Dictionary<(int x, int y), CollisionAdjust> _frameCollisionAdjustments = [];
20 private readonly Dictionary<(int x, int y), TileCollisionType> _frameCollisionTypes = [];
22 private TileCollisionType _collisionType = TileCollisionType.None;
23 private bool _disposed;
24
25 private TilesheetRegion() { }
26
27 internal TilesheetRegion(
28 Tilesheet tilesheet,
29 string name,
30 Rectangle area,
31 Size tileSize,
32 Spacing tilePadding,
33 Spacing regionMargin,
34 Spacing overhangPixels,
35 CollisionAdjust collisionAdjust,
36 TileCollisionType collisionType)
37 {
38 Tilesheet = tilesheet ?? throw new ArgumentNullException(nameof(tilesheet));
39 Name = string.IsNullOrWhiteSpace(name) ? DefaultRegionName : name;
40
41 // Assign backing fields directly so construction performs one cache build.
42 _area = area;
43 _tileSize = tileSize;
44 _tilePadding = tilePadding;
45 _regionMargin = regionMargin;
46 Overhang = overhangPixels;
47 _collisionAdjust = collisionAdjust;
48 _collisionType = collisionType;
49
50 BuildTileCache();
51 }
52
53 private Rectangle _area;
54 private Size _tileSize;
55 private Spacing _tilePadding = Spacing.None;
56 private Spacing _regionMargin = Spacing.None;
57
61 public Tilesheet Tilesheet { get; private set; } = null!;
62
66 public string Name { get; private set; } = DefaultRegionName;
67
71 public Rectangle Area
72 {
73 get => _area;
74 set
75 {
76 _area = value;
77 BuildTileCache();
78 }
79 }
80
84 public Size TileSize
85 {
86 get => _tileSize;
87 set
88 {
89 _tileSize = value;
90 BuildTileCache();
91 }
92 }
93
98 {
99 get => _tilePadding;
100 set
101 {
102 _tilePadding = value;
103 BuildTileCache();
104 }
105 }
106
111 {
112 get => _regionMargin;
113 set
114 {
115 _regionMargin = value;
116 BuildTileCache();
117 }
118 }
119
123 public Spacing Overhang { get; set; } = Spacing.None;
124
130 {
131 get => _collisionAdjust;
132 set
133 {
134 ThrowIfDisposed();
135 _collisionAdjust = value;
136 ApplyDefaultCollisionAdjustToCache();
137 }
138 }
139
143 public Rectangle CollisionArea =>
144 _collisionAdjust.ApplyTo(new Rectangle(Point.Empty, _tileSize));
145
151 {
152 get => _collisionType;
153 set
154 {
155 ThrowIfDisposed();
156 _collisionType = value;
157 }
158 }
159
163 public int Columns => _tileCache?.GetLength(0) ?? 0;
167 public int Rows => _tileCache?.GetLength(1) ?? 0;
171 public int TileWidthIncludingPadding => _tilePadding.Left + _tileSize.Width + _tilePadding.Right;
175 public int TileHeightIncludingPadding => _tilePadding.Top + _tileSize.Height + _tilePadding.Bottom;
176
183 public SKImage? GetImage(int x, int y)
184 {
185 ThrowIfDisposed();
186
187 if (_tileCache == null)
188 BuildTileCache();
189
190 if (_tileCache == null ||
191 (uint)x >= (uint)_tileCache.GetLength(0) ||
192 (uint)y >= (uint)_tileCache.GetLength(1))
193 {
194 return null;
195 }
196
197 return _tileCache[x, y]?.Image;
198 }
199
206 public SKBitmap? GetBitmap(int x, int y)
207 {
208 ThrowIfDisposed();
209
210 if (_tileCache == null)
211 BuildTileCache();
212
213 if (_tileCache == null ||
214 (uint)x >= (uint)_tileCache.GetLength(0) ||
215 (uint)y >= (uint)_tileCache.GetLength(1))
216 {
217 return null;
218 }
219
220 return _tileCache[x, y]?.Bitmap;
221 }
222
227 public Dictionary<(int x, int y), SKBitmap> GetAllBitmaps()
228 {
229 ThrowIfDisposed();
230
231 if (_tileCache == null)
232 BuildTileCache();
233
234 var bitmaps = new Dictionary<(int x, int y), SKBitmap>();
235 if (_tileCache == null)
236 return bitmaps;
237
238 for (int y = 0; y < _tileCache.GetLength(1); y++)
239 {
240 for (int x = 0; x < _tileCache.GetLength(0); x++)
241 {
242 if (_tileCache[x, y] is { } slice)
243 bitmaps[(x, y)] = slice.Bitmap;
244 }
245 }
246
247 return bitmaps;
248 }
249
254 public Dictionary<(int x, int y), SKImage> GetAllImages()
255 {
256 ThrowIfDisposed();
257
258 if (_tileCache == null)
259 BuildTileCache();
260
261 var images = new Dictionary<(int x, int y), SKImage>();
262 if (_tileCache == null)
263 return images;
264
265 for (int y = 0; y < _tileCache.GetLength(1); y++)
266 {
267 for (int x = 0; x < _tileCache.GetLength(0); x++)
268 {
269 if (_tileCache[x, y] is { } slice)
270 images[(x, y)] = slice.Image;
271 }
272 }
273
274 return images;
275 }
276
284 {
285 ThrowIfDisposed();
286
287 if (!IsFrameCoordinateValid(x, y))
288 return _collisionAdjust;
289
290 if (_tileCache is not null && _tileCache[x, y] is { } slice)
291 return slice.CollisionAdjust;
292
293 return GetStoredFrameCollisionAdjust(x, y);
294 }
295
309 int x,
310 int y,
311 out CollisionAdjust collisionAdjust)
312 {
313 ThrowIfDisposed();
314
315 if (_tileCache is null)
316 BuildTileCache();
317
318 if (!IsFrameCoordinateValid(x, y))
319 {
320 collisionAdjust = default;
321 return false;
322 }
323
324 return _frameCollisionAdjustments.TryGetValue(
325 (x, y),
326 out collisionAdjust);
327 }
328
335 public void SetFrameCollisionAdjust(int x, int y, CollisionAdjust collisionAdjust)
336 {
337 ThrowIfDisposed();
338
339 if (_tileCache is null)
340 BuildTileCache();
341
342 if (!IsFrameCoordinateValid(x, y))
343 {
344 throw new ArgumentOutOfRangeException(
345 nameof(x),
346 $"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
347 }
348
349 _frameCollisionAdjustments[(x, y)] = collisionAdjust;
350
351 if (_tileCache![x, y] is { } slice)
352 _tileCache[x, y] = slice.WithCollisionAdjust(collisionAdjust);
353 }
354
365 public bool ClearFrameCollisionAdjustOverride(int x, int y)
366 {
367 ThrowIfDisposed();
368
369 if (_tileCache is null)
370 BuildTileCache();
371
372 if (!IsFrameCoordinateValid(x, y))
373 {
374 throw new ArgumentOutOfRangeException(
375 nameof(x),
376 $"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
377 }
378
379 if (!_frameCollisionAdjustments.Remove((x, y)))
380 return false;
381
382 if (_tileCache![x, y] is { } slice)
383 _tileCache[x, y] = slice.WithCollisionAdjust(_collisionAdjust);
384
385 return true;
386 }
387
394 public Rectangle GetFrameCollisionArea(int x, int y) =>
396 .ApplyTo(new Rectangle(Point.Empty, _tileSize));
397
405 {
406 ThrowIfDisposed();
407
408 if (!IsFrameCoordinateValid(x, y))
409 return _collisionType;
410
411 return _frameCollisionTypes.TryGetValue((x, y), out var collisionType)
412 ? collisionType
413 : _collisionType;
414 }
415
424 int x,
425 int y,
426 out TileCollisionType collisionType)
427 {
428 ThrowIfDisposed();
429
430 if (_tileCache is null)
431 BuildTileCache();
432
433 if (!IsFrameCoordinateValid(x, y))
434 {
435 collisionType = default;
436 return false;
437 }
438
439 return _frameCollisionTypes.TryGetValue((x, y), out collisionType);
440 }
441
448 public void SetFrameCollisionType(int x, int y, TileCollisionType collisionType)
449 {
450 ThrowIfDisposed();
451
452 if (_tileCache is null)
453 BuildTileCache();
454
455 if (!IsFrameCoordinateValid(x, y))
456 {
457 throw new ArgumentOutOfRangeException(
458 nameof(x),
459 $"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
460 }
461
462 _frameCollisionTypes[(x, y)] = collisionType;
463 }
464
472 public bool ClearFrameCollisionTypeOverride(int x, int y)
473 {
474 ThrowIfDisposed();
475
476 if (_tileCache is null)
477 BuildTileCache();
478
479 if (!IsFrameCoordinateValid(x, y))
480 {
481 throw new ArgumentOutOfRangeException(
482 nameof(x),
483 $"Frame coordinates ({x}, {y}) are outside region '{Name}'.");
484 }
485
486 return _frameCollisionTypes.Remove((x, y));
487 }
488
492 internal void BuildTileCache()
493 {
494 ThrowIfDisposed();
495 ClearTileCache();
496
497 if (Tilesheet == null ||
498 Tilesheet.SkBitmap == null ||
499 Tilesheet.SkBitmap.IsEmpty ||
500 _tileSize.Width <= 0 ||
501 _tileSize.Height <= 0 ||
502 _area.Width <= 0 ||
503 _area.Height <= 0)
504 {
505 return;
506 }
507
508 if (_tilePadding.Left < 0 || _tilePadding.Top < 0 ||
509 _tilePadding.Right < 0 || _tilePadding.Bottom < 0)
510 {
511 throw new InvalidOperationException("Tilesheet region tile padding cannot be negative.");
512 }
513
514 if (_regionMargin.Left < 0 || _regionMargin.Top < 0 ||
515 _regionMargin.Right < -_tilePadding.Right || _regionMargin.Bottom < -_tilePadding.Bottom)
516 {
517 throw new InvalidOperationException("Leading region margins cannot be negative; trailing margins may only cancel trailing tile padding.");
518 }
519
521 return;
522
523 int xTiles = (_area.Width - _regionMargin.Left - _regionMargin.Right) /
525 int yTiles = (_area.Height - _regionMargin.Top - _regionMargin.Bottom) /
527
528 if (xTiles <= 0 || yTiles <= 0)
529 return;
530
531 PruneFrameCollisionAdjustments(xTiles, yTiles);
532 PruneFrameCollisionTypes(xTiles, yTiles);
533 _tileCache = new TilesheetRegionSlice?[xTiles, yTiles];
534
535 var regionArea = Area;
536 var bitmapBounds = Tilesheet.SkBitmap.Info.Rect;
537
538 for (int y = 0; y < yTiles; y++)
539 {
540 for (int x = 0; x < xTiles; x++)
541 {
542 var srcRect = GetTileBounds(x, y);
543
544 // Prevent this region from bleeding into another region or outside the image.
545 if (!regionArea.Contains(srcRect) ||
546 !bitmapBounds.Contains(srcRect.ToSKRectI()))
547 {
548 continue;
549 }
550
551 var slice = CreateSlice(
552 srcRect,
553 GetStoredFrameCollisionAdjust(x, y));
554
555 if (slice.HasValue)
556 _tileCache[x, y] = slice.Value;
557 }
558 }
559 }
560
561 internal void ClearTileCache()
562 {
563 if (_tileCache == null)
564 return;
565
566 for (int y = 0; y < _tileCache.GetLength(1); y++)
567 {
568 for (int x = 0; x < _tileCache.GetLength(0); x++)
569 {
570 _tileCache[x, y]?.Bitmap.Dispose();
571 _tileCache[x, y]?.Image.Dispose();
572 _tileCache[x, y] = null;
573 }
574 }
575
576 _tileCache = null;
577 }
578
579 internal Rectangle GetFrameSourceBounds(int xTile, int yTile)
580 {
581 ThrowIfDisposed();
582
583 if (_tileCache is null)
584 BuildTileCache();
585
586 return IsFrameCoordinateValid(xTile, yTile)
587 ? GetTileBounds(xTile, yTile)
588 : Rectangle.Empty;
589 }
590
591 private Rectangle GetTileBounds(int xTile, int yTile)
592 {
593 int x = _area.X + _regionMargin.Left + (xTile * TileWidthIncludingPadding);
594 int y = _area.Y + _regionMargin.Top + (yTile * TileHeightIncludingPadding);
595
596 return new Rectangle(
597 x + _tilePadding.Left,
598 y + _tilePadding.Top,
599 _tileSize.Width,
600 _tileSize.Height);
601 }
602
603 private TilesheetRegionSlice? CreateSlice(
604 Rectangle srcRect,
605 CollisionAdjust collisionAdjust)
606 {
607 var srcInfo = Tilesheet.SkBitmap.Info;
608 var sliceInfo = new SKImageInfo(
609 srcRect.Width,
610 srcRect.Height,
611 srcInfo.ColorType,
612 srcInfo.AlphaType);
613
614 var bmp = new SKBitmap(sliceInfo);
615 bmp.Erase(SKColors.Transparent);
616
617 if (!Tilesheet.SkBitmap.ExtractSubset(bmp, srcRect.ToSKRectI()))
618 {
619 bmp.Dispose();
620 return null;
621 }
622
623 var img = SKImage.FromBitmap(bmp);
624 return new TilesheetRegionSlice(bmp, img, collisionAdjust);
625 }
626
627 private CollisionAdjust GetStoredFrameCollisionAdjust(int x, int y) =>
628 _frameCollisionAdjustments.TryGetValue((x, y), out var collisionAdjust)
629 ? collisionAdjust
630 : _collisionAdjust;
631
632 private bool IsFrameCoordinateValid(int x, int y) =>
633 _tileCache is not null &&
634 (uint)x < (uint)_tileCache.GetLength(0) &&
635 (uint)y < (uint)_tileCache.GetLength(1);
636
637 private void ApplyDefaultCollisionAdjustToCache()
638 {
639 if (_tileCache is null)
640 return;
641
642 for (int y = 0; y < _tileCache.GetLength(1); y++)
643 {
644 for (int x = 0; x < _tileCache.GetLength(0); x++)
645 {
646 if (!_frameCollisionAdjustments.ContainsKey((x, y)) &&
647 _tileCache[x, y] is { } slice)
648 {
649 _tileCache[x, y] = slice.WithCollisionAdjust(_collisionAdjust);
650 }
651 }
652 }
653 }
654
655 private void PruneFrameCollisionAdjustments(int columns, int rows)
656 {
657 foreach (var key in _frameCollisionAdjustments.Keys.ToArray())
658 {
659 if ((uint)key.x >= (uint)columns || (uint)key.y >= (uint)rows)
660 _frameCollisionAdjustments.Remove(key);
661 }
662 }
663
664 private void PruneFrameCollisionTypes(int columns, int rows)
665 {
666 foreach (var key in _frameCollisionTypes.Keys.ToArray())
667 {
668 if ((uint)key.x >= (uint)columns || (uint)key.y >= (uint)rows)
669 _frameCollisionTypes.Remove(key);
670 }
671 }
672
673 private void ThrowIfDisposed()
674 {
675 if (_disposed)
676 throw new ObjectDisposedException(nameof(TilesheetRegion));
677 }
678
682 public void Dispose()
683 {
684 if (_disposed)
685 return;
686
687 _disposed = true;
688 ClearTileCache();
689 _frameCollisionAdjustments.Clear();
690 _frameCollisionTypes.Clear();
691 }
692}
Represents a rectangular region within a tilesheet that contains a grid of frames.
Spacing RegionMargin
Gets or sets the spacing inside the region boundary.
Dictionary<(int x, int y), SKBitmap > GetAllBitmaps()
Collects the cached frame bitmaps indexed by their tile coordinates.
Rectangle Area
Gets or sets the region bounds in tilesheet image pixels.
TileCollisionType CollisionType
Gets or sets the collision type inherited by frames that do not have an explicit frame-level override...
SKImage? GetImage(int x, int y)
Gets the cached image for the specified frame.
int Rows
Gets the number of tile rows in the region.
SKBitmap? GetBitmap(int x, int y)
Gets the cached bitmap for the specified frame.
static readonly string DefaultRegionName
The default name assigned to tilesheet regions when no name is specified.
Spacing Overhang
Gets or sets visual overhang. This affects rendering, not slicing.
bool TryGetFrameCollisionAdjustOverride(int x, int y, out CollisionAdjust collisionAdjust)
Attempts to get the explicit collision adjustment assigned to one frame.
Rectangle CollisionArea
Gets the region-default frame-local collision rectangle.
Spacing TilePadding
Gets or sets the spacing around each tile.
void SetFrameCollisionAdjust(int x, int y, CollisionAdjust collisionAdjust)
Sets the collision adjustment for one frame and updates its cache entry.
CollisionAdjust CollisionAdjust
Gets or sets the collision adjustment inherited by frames that do not have an explicit frame-level ov...
void SetFrameCollisionType(int x, int y, TileCollisionType collisionType)
Sets an explicit collision type for one frame.
int Columns
Gets the number of tile columns in the region.
Rectangle GetFrameCollisionArea(int x, int y)
Gets the frame-local collision rectangle for one frame.
CollisionAdjust GetFrameCollisionAdjust(int x, int y)
Gets the effective collision adjustment for one frame.
Size TileSize
Gets or sets the tile dimensions in pixels.
int TileWidthIncludingPadding
Gets the horizontal tile stride including padding.
bool ClearFrameCollisionTypeOverride(int x, int y)
Removes a frame-level collision type so the frame once again inherits CollisionType.
Dictionary<(int x, int y), SKImage > GetAllImages()
Collects the cached frame images indexed by their tile coordinates.
TileCollisionType GetFrameCollisionType(int x, int y)
Gets the effective collision type for one frame.
void Dispose()
Releases the resources owned by this instance.
bool ClearFrameCollisionAdjustOverride(int x, int y)
Removes a frame-level collision adjustment so the frame once again inherits CollisionAdjust.
bool TryGetFrameCollisionTypeOverride(int x, int y, out TileCollisionType collisionType)
Attempts to get the explicit collision type assigned to one frame.
Tilesheet Tilesheet
Gets the tilesheet that owns this region.
int TileHeightIncludingPadding
Gets the vertical tile stride including padding.
Represents a tilesheet image and metadata for rendering tiles.
Definition Tilesheet.cs:14
SKBitmap SkBitmap
Gets the current tilesheet bitmap used for rendering.
Definition Tilesheet.cs:156
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
record struct Spacing([property:JsonProperty("left")] int Left, [property:JsonProperty("top")] int Top, [property:JsonProperty("right")] int Right, [property:JsonProperty("bottom")] int Bottom)
Represents dimensions in pixels when spacing Tilesheets.
Definition Spacing.cs:12
TileCollisionType
Defines the default collision behavior associated with a tilesheet region, frame, or runtime tile.
Per-edge inset amounts applied to visual bounds to produce a collision rectangle.
static readonly CollisionAdjust None
Represents an adjustment with no pixel offsets.
readonly Rectangle ApplyTo(Rectangle rectangle)
Applies this adjustment to the supplied rectangle.