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
Tilesheet.cs
1using System.Drawing;
5using Microsoft.Extensions.Logging;
6using SkiaSharp;
7
9
13public sealed class Tilesheet : IDisposable
14{
15 private SKImage? _atlasImage;
16
17 [Newtonsoft.Json.JsonIgnore]
18 internal DefinitionProvenance? DefinitionProvenance { get; set; }
19
24 internal SKImage? AtlasImage
25 {
26 get
27 {
28 if (_disposed || SkBitmap is null || SkBitmap.IsEmpty)
29 return null;
30
31 return _atlasImage ??= SKImage.FromBitmap(SkBitmap);
32 }
33 }
34
38 public event Action<Tilesheet>? Disposed;
39
40 private Tilesheet() { }
41
42 internal Tilesheet(string name, SKBitmap bitmap, bool addDefaultRegion = true)
43 {
44 Name = name;
45 SkBitmap = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
46
47 if (addDefaultRegion)
48 AddDefaultRegion();
49 }
50
51 internal Tilesheet(string name, Stream stream, bool addDefaultRegion = true)
52 : this(
53 name,
54 SKBitmap.Decode(stream) ?? throw new ArgumentException("Invalid image stream."),
55 addDefaultRegion)
56 {
57 }
58
59 internal Tilesheet(string name, string file, bool addDefaultRegion = true)
60 : this(
61 name,
62 SKBitmap.Decode(file) ?? throw new ArgumentException($"Invalid image file: {file}"),
63 addDefaultRegion)
64 {
65 ImageFilePath = file;
66 }
67
68 internal Tilesheet(AssetsFile resFile, string entryName, bool addDefaultRegion = true)
69 {
70 ArgumentNullException.ThrowIfNull(resFile);
71
72 if (string.IsNullOrWhiteSpace(entryName))
73 throw new ArgumentException("Asset entry name must be a non-empty string.", nameof(entryName));
74
75 AssetIdentifier = new AssetsFileIdentifier(resFile, AssetTypes.Image, entryName);
76
77 using var assetStream = AssetIdentifier.Data
78 ?? throw new InvalidOperationException(
79 $"Tilesheet asset '{entryName}' could not be loaded from AssetsFile '{resFile.FilePath}'. " +
80 "The asset entry does not exist or returned a null data stream.");
81
82 SkBitmap = SKBitmap.Decode(assetStream)
83 ?? throw new ArgumentException(
84 $"Failed to decode tilesheet bitmap from asset '{entryName}' in AssetsFile '{resFile.FilePath}'. " +
85 "The asset data is corrupt or not a supported image format.");
86
87 Name = entryName;
88
89 if (addDefaultRegion)
90 AddDefaultRegion();
91 }
92
100 internal Tilesheet(Tilesheet baseSheet, string name, string file)
101 {
102 ArgumentNullException.ThrowIfNull(baseSheet);
103
104 Name = name;
105 SkBitmap = SKBitmap.Decode(file)
106 ?? throw new ArgumentException($"Invalid image file: {file}");
107
108 ImageFilePath = file;
109 ValueBag = new(baseSheet.ValueBag);
110
111 foreach (var region in baseSheet.Regions)
112 {
113 var copiedRegion = AddRegion(
114 region.Name,
115 region.Area,
116 region.TileSize,
117 region.TilePadding,
118 region.RegionMargin,
119 region.Overhang,
120 region.CollisionAdjust,
121 region.CollisionType);
122
123 for (int y = 0; y < region.Rows; y++)
124 {
125 for (int x = 0; x < region.Columns; x++)
126 {
127 if (region.TryGetFrameCollisionAdjustOverride(
128 x,
129 y,
130 out var frameCollisionAdjust))
131 {
132 copiedRegion.SetFrameCollisionAdjust(
133 x,
134 y,
135 frameCollisionAdjust);
136 }
137
138 if (region.TryGetFrameCollisionTypeOverride(
139 x,
140 y,
141 out var frameCollisionType))
142 {
143 copiedRegion.SetFrameCollisionType(
144 x,
145 y,
146 frameCollisionType);
147 }
148 }
149 }
150 }
151 }
152
156 public SKBitmap SkBitmap { get; private set; } = null!;
160 public SKBitmap? SkBitmapOriginal { get; private set; }
164 public string Name { get; internal set; } = string.Empty;
168 public List<TilesheetRegion> Regions { get; private set; } = new();
176 public TypedValueBag ValueBag { get; set; } = new();
180 public AssetsFileIdentifier? AssetIdentifier { get; private set; }
184 public string ImageFilePath { get; private set; } = string.Empty;
188 public SKColor? MaskColor { get; private set; }
192 public byte MaskTolerance { get; private set; } = 5;
196 public bool Premultiplied { get; private set; }
197
211 string name,
212 Rectangle area,
213 Size tileSize,
214 Spacing? tilePadding = null,
215 Spacing? regionMargin = null,
216 Spacing? overhangPixels = null,
217 CollisionAdjust? collisionAdjust = null,
218 TileCollisionType collisionType = TileCollisionType.None)
219 {
220 if (string.IsNullOrWhiteSpace(name))
221 throw new ArgumentException("Region name must be a non-empty string.", nameof(name));
222
223 if (GetRegion(name) != null)
224 throw new ArgumentException($"A tilesheet region named '{name}' already exists.", nameof(name));
225
226 var region = new TilesheetRegion(
227 this,
228 name,
229 area,
230 tileSize,
231 tilePadding ?? Spacing.None,
232 regionMargin ?? Spacing.None,
233 overhangPixels ?? Spacing.None,
234 collisionAdjust ?? CollisionAdjust.None,
235 collisionType);
236
237 Regions.Add(region);
238 return region;
239 }
240
246 public TilesheetRegion? GetRegion(string name)
247 {
248 if (string.IsNullOrWhiteSpace(name))
249 return null;
250
251 foreach (var region in Regions)
252 {
253 if (string.Equals(region.Name, name, StringComparison.OrdinalIgnoreCase))
254 return region;
255 }
256
257 return null;
258 }
259
266 public bool RemoveRegion(string name, bool dispose = true)
267 {
268 var region = GetRegion(name);
269 if (region == null)
270 return false;
271
272 Regions.Remove(region);
273
274 if (dispose)
275 region.Dispose();
276
277 return true;
278 }
279
287 public Frame GetFrame(string regionName, int x, int y) =>
288 new(this, regionName, x, y);
289
296 public Frame GetFrame(int x, int y) =>
297 new(this, TilesheetRegion.DefaultRegionName, x, y);
298
304 public void ApplyMask(SKColor? maskColor = null, byte tolerance = 5)
305 {
306 if (SkBitmap == null || SkBitmap.IsEmpty)
307 throw new ArgumentException("Invalid bitmap.");
308
309 var targetColor = maskColor ?? SKColors.White;
310
311 MaskColor = targetColor;
312 MaskTolerance = tolerance;
313 Premultiplied = true;
314
315 InvalidateAtlasImage();
316 ClearTileCache();
317
318 if (SkBitmap.Info.AlphaType == SKAlphaType.Opaque)
319 {
320 var info = new SKImageInfo(
321 SkBitmap.Width,
322 SkBitmap.Height,
323 SkBitmap.Info.ColorType,
324 SKAlphaType.Premul);
325
326 var withAlpha = new SKBitmap(info);
327 using (var canvas = new SKCanvas(withAlpha))
328 canvas.DrawBitmap(SkBitmap, 0, 0);
329
330 SkBitmap.Dispose();
331 SkBitmap = withAlpha;
332 }
333
334 SkBitmapOriginal?.Dispose();
335 SkBitmapOriginal = SkBitmap.Copy();
336
337 SkBitmap.ApplyAlphaMask(targetColor, tolerance);
338 SkBitmap = SkBitmap.PremultiplyAlpha();
339
340 BuildTileCache();
341 }
342
347 {
348 if (SkBitmap == null || SkBitmap.IsEmpty)
349 throw new ArgumentException("Invalid bitmap.");
350
351 Premultiplied = true;
352 InvalidateAtlasImage();
353 ClearTileCache();
354
355 SkBitmapOriginal?.Dispose();
356 SkBitmapOriginal = SkBitmap.Copy();
357 SkBitmap = SkBitmap.PremultiplyAlpha();
358
359 BuildTileCache();
360 }
361
368 public byte[] ToByteArray(
369 SKEncodedImageFormat format = SKEncodedImageFormat.Png,
370 int quality = 100)
371 {
372 if (SkBitmap == null || SkBitmap.IsEmpty)
373 throw new ArgumentException("Invalid bitmap.");
374
375 return SkBitmap.EncodeBitmapToBytes(format, quality);
376 }
377
391 string imageFilePath,
392 SKEncodedImageFormat format = SKEncodedImageFormat.Png,
393 int quality = 100)
394 {
395 if (_disposed)
396 throw new ObjectDisposedException(nameof(Tilesheet));
397
398 if (string.IsNullOrWhiteSpace(imageFilePath))
399 {
400 throw new ArgumentException(
401 "Image file path must be a non-empty string.",
402 nameof(imageFilePath));
403 }
404
405 if (quality is < 0 or > 100)
406 {
407 throw new ArgumentOutOfRangeException(
408 nameof(quality),
409 quality,
410 "Image encoding quality must be between 0 and 100.");
411 }
412
413 if (SkBitmap == null || SkBitmap.IsEmpty)
414 {
415 throw new InvalidOperationException(
416 "Tilesheet does not contain a valid bitmap to persist.");
417 }
418
419 var fullPath = Path.GetFullPath(imageFilePath);
420 var directory = Path.GetDirectoryName(fullPath);
421 if (!string.IsNullOrWhiteSpace(directory))
422 Directory.CreateDirectory(directory);
423
424 var sourceBitmap = SkBitmapOriginal ?? SkBitmap;
425 var imageBytes = sourceBitmap.EncodeBitmapToBytes(format, quality);
426 File.WriteAllBytes(fullPath, imageBytes);
427
428 ImageFilePath = fullPath;
429 AssetIdentifier = null;
430 }
431
439 public SKImage? GetImage(string regionName, int x, int y) =>
440 GetRegion(regionName)?.GetImage(x, y);
441
449 public SKBitmap? GetBitmap(string regionName, int x, int y) =>
450 GetRegion(regionName)?.GetBitmap(x, y);
451
456 public Dictionary<(string regionName, int x, int y), SKBitmap> GetAllBitmaps()
457 {
458 var tiles = new Dictionary<(string regionName, int x, int y), SKBitmap>();
459
460 foreach (var region in Regions)
461 {
462 foreach (var tile in region.GetAllBitmaps())
463 tiles[(region.Name, tile.Key.x, tile.Key.y)] = tile.Value;
464 }
465
466 return tiles;
467 }
468
473 public Dictionary<(string regionName, int x, int y), SKImage> GetAllImages()
474 {
475 var tiles = new Dictionary<(string regionName, int x, int y), SKImage>();
476
477 foreach (var region in Regions)
478 {
479 foreach (var tile in region.GetAllImages())
480 tiles[(region.Name, tile.Key.x, tile.Key.y)] = tile.Value;
481 }
482
483 return tiles;
484 }
485
490 public TilesheetRegion this[string regionName] =>
491 GetRegion(regionName)
492 ?? throw new ArgumentException(
493 $"No tilesheet region named '{regionName}' exists.",
494 nameof(regionName));
495
502 public Frame this[string regionName, int x, int y] =>
503 GetFrame(regionName, x, y);
504
510 public Frame this[int x, int y] => GetFrame(x, y);
511
512 private void BuildTileCache()
513 {
514 foreach (var region in Regions)
515 region.BuildTileCache();
516 }
517
518 private void ClearTileCache()
519 {
520 foreach (var region in Regions)
521 region.ClearTileCache();
522 }
523
524 private void InvalidateAtlasImage()
525 {
526 _atlasImage?.Dispose();
527 _atlasImage = null;
528 }
529
530 private void AddDefaultRegion(
531 Size? tileSize = null,
532 Spacing? tilePadding = null,
533 Spacing? regionMargin = null,
534 Spacing? overhangPixels = null,
535 CollisionAdjust? collisionAdjust = null,
536 TileCollisionType collisionType = TileCollisionType.None)
537 {
538 AddRegion(
539 TilesheetRegion.DefaultRegionName,
540 new Rectangle(0, 0, SkBitmap.Width, SkBitmap.Height),
541 tileSize ?? Size.Empty,
542 tilePadding ?? Spacing.None,
543 regionMargin ?? Spacing.None,
544 overhangPixels ?? Spacing.None,
545 collisionAdjust ?? CollisionAdjust.None,
546 collisionType);
547 }
548
549 private bool _disposed;
550
554 public void Dispose()
555 {
556 Dispose(true);
557 GC.SuppressFinalize(this);
558 }
559
560 private void Dispose(bool disposing)
561 {
562 if (_disposed)
563 return;
564
565 _disposed = true;
566
567 if (!disposing)
568 return;
569
570 foreach (var region in Regions)
571 region.Dispose();
572
573 Regions.Clear();
574
575 InvalidateAtlasImage();
576 SkBitmap?.Dispose();
577 SkBitmapOriginal?.Dispose();
578
579 try
580 {
581 Disposed?.Invoke(this);
582 }
583 catch (Exception ex)
584 {
585 Engine.Logger.LogError(ex, "Error during Tilesheet Disposed event handling.");
586 }
587
588 Disposed = null;
589 }
590}
Represents an identifier for a specific asset within a file, including its type and name.
Represents an asset file used by the engine, providing functionality to load, manage,...
Definition AssetsFile.cs:18
Represents a rectangular region within a tilesheet that contains a grid of frames.
SKImage? GetImage(int x, int y)
Gets the cached image for the specified frame.
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.
Represents a tilesheet image and metadata for rendering tiles.
Definition Tilesheet.cs:14
bool Premultiplied
Gets whether alpha premultiplication has been applied.
Definition Tilesheet.cs:196
TilesheetRegion DefaultRegion
Gets the default tilesheet region.
Definition Tilesheet.cs:172
TypedValueBag ValueBag
Gets or sets the custom typed values associated with this object.
Definition Tilesheet.cs:176
void PersistImageToFile(string imageFilePath, SKEncodedImageFormat format=SKEncodedImageFormat.Png, int quality=100)
Persists this tilesheet's source image to a file and promotes the tilesheet from a runtime-only bitma...
Definition Tilesheet.cs:390
AssetsFileIdentifier? AssetIdentifier
Gets the packed asset from which the image was loaded, if any.
Definition Tilesheet.cs:180
Frame GetFrame(string regionName, int x, int y)
Creates a frame reference at the specified tile coordinates.
SKImage? GetImage(string regionName, int x, int y)
Gets the cached image for the specified frame.
Action< Tilesheet >? Disposed
Occurs after the object releases its resources.
Definition Tilesheet.cs:38
void ApplyMask(SKColor? maskColor=null, byte tolerance=5)
Applies a color-key transparency mask and rebuilds the frame cache.
Definition Tilesheet.cs:304
byte MaskTolerance
Gets the per-channel tolerance used for transparency masking.
Definition Tilesheet.cs:192
void ApplyPremultiplyAlpha()
Premultiplies the bitmap alpha and rebuilds the frame cache.
Definition Tilesheet.cs:346
Dictionary<(string regionName, int x, int y), SKImage > GetAllImages()
Collects the cached frame images indexed by their tile coordinates.
Definition Tilesheet.cs:473
bool RemoveRegion(string name, bool dispose=true)
Removes the named tilesheet region and optionally disposes it.
Definition Tilesheet.cs:266
SKBitmap? GetBitmap(string regionName, int x, int y)
Gets the cached bitmap for the specified frame.
void Dispose()
Releases the resources owned by this instance.
Definition Tilesheet.cs:554
Dictionary<(string regionName, int x, int y), SKBitmap > GetAllBitmaps()
Collects the cached frame bitmaps indexed by their tile coordinates.
Definition Tilesheet.cs:456
List< TilesheetRegion > Regions
Gets the named frame regions in the tilesheet.
Definition Tilesheet.cs:168
SKColor? MaskColor
Gets the color used for transparency masking.
Definition Tilesheet.cs:188
byte[] ToByteArray(SKEncodedImageFormat format=SKEncodedImageFormat.Png, int quality=100)
Encodes the tilesheet bitmap using the requested image format and quality.
Definition Tilesheet.cs:368
string ImageFilePath
Gets the loose source image path, when available.
Definition Tilesheet.cs:184
SKBitmap? SkBitmapOriginal
Gets the source bitmap retained before masking or alpha conversion.
Definition Tilesheet.cs:160
SKBitmap SkBitmap
Gets the current tilesheet bitmap used for rendering.
Definition Tilesheet.cs:156
Frame GetFrame(int x, int y)
Creates a frame reference at the specified tile coordinates.
TilesheetRegion? GetRegion(string name)
Finds a tilesheet region by its case-insensitive name.
Definition Tilesheet.cs:246
TilesheetRegion AddRegion(string name, Rectangle area, Size tileSize, Spacing? tilePadding=null, Spacing? regionMargin=null, Spacing? overhangPixels=null, CollisionAdjust? collisionAdjust=null, TileCollisionType collisionType=TileCollisionType.None)
Adds a region with the supplied default collision adjustment and collision type.
Definition Tilesheet.cs:210
Represents the core game engine singleton responsible for managing the main update loop,...
Definition Engine.cs:36
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Definition Engine.cs:59
A flexible, strongly-typed value container intended for runtime-attached metadata and ephemeral engin...
AssetTypes
Defines the types of assets that can be stored and managed by the engine.
Definition AssetTypes.cs:8
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.
Represents the source Tilesheet and its coordinates to render on a destination.
Definition Frame.cs:12
Per-edge inset amounts applied to visual bounds to produce a collision rectangle.
static readonly CollisionAdjust None
Represents an adjustment with no pixel offsets.