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
Sprite.cs
1using System.Drawing;
2using System.Numerics;
3using System.Runtime.Serialization;
10using Gondwana.Scenes;
11using Newtonsoft.Json;
12
14
18[JsonObject(IsReference = true)]
19public partial class Sprite : Tile, IMovableOnSceneLayer, ICollisionMovableEntity, IDisposable
20{
24 public event Action<SpriteMovedEventArgs>? SpriteMoved;
28 public event Action<Sprite>? VisualBoundsChanged;
32 public event Action<Sprite>? Disposing;
33
34 [JsonIgnore]
35 internal SceneLayer _sceneLayer;
36
37 private string? _serializedSceneId;
38 private string? _serializedSceneLayerId;
39
40#pragma warning disable IDE0051 // Used by Newtonsoft.Json via reflection for state serialization.
41
45 [JsonProperty("SceneId")]
46 private string? SerializedSceneId
47 {
48 get => !ReferenceEquals(_sceneLayer, SceneLayer.Empty)
49 ? _sceneLayer.Scene?.ID ?? _serializedSceneId
50 : _serializedSceneId;
51 set => _serializedSceneId = value;
52 }
53
57 [JsonProperty("SceneLayerId")]
58 private string? SerializedSceneLayerId
59 {
60 get => !ReferenceEquals(_sceneLayer, SceneLayer.Empty)
61 ? _sceneLayer.ID
62 : _serializedSceneLayerId;
63 set => _serializedSceneLayerId = value;
64 }
65
70 [JsonProperty("SceneLayer")]
71 private SceneLayer? LegacySerializedSceneLayer
72 {
73 set
74 {
75 if (value is null)
76 return;
77
78 _sceneLayer = value;
79 _serializedSceneId = value.Scene?.ID;
80 _serializedSceneLayerId = value.ID;
81 }
82 }
83
84#pragma warning restore IDE0051
85
86 private HorizontalAlignment _horizAlign;
87 private VerticalAlignment _vertAlign;
88 private int _nudgeX;
89 private int _nudgeY;
90 private Size _renderSize;
91 private float _rotation;
92
93 internal bool _pendingDispose;
94
95 [JsonProperty("SceneLayerCoordinates")]
96 private PointF _sceneLayerCoordinates;
97
98 [JsonConstructor]
99 private Sprite()
100 {
101 // New EngineState files bind sprites to the restored scene graph by stable
102 // Scene/SceneLayer IDs after the snapshot's scenes have been materialized.
103 _sceneLayer = SceneLayer.Empty;
104 }
105
111 protected internal Sprite(SceneLayer sceneLayer, Frame frame)
112 : this(
113 sceneLayer,
114 frame,
115 SpriteManager.Instance.DefaultCollisionProfile)
116 {
117 }
118
125 protected internal Sprite(
126 SceneLayer sceneLayer,
127 Frame frame,
128 string collisionProfileName)
129 {
130 _sceneLayer = sceneLayer
131 ?? throw new ArgumentNullException(
132 nameof(sceneLayer),
133 "Sprite must be attached to a SceneLayer.");
134
135 animator = new Animator(this);
136 pauseAnimation = false;
137 _horizAlign = HorizontalAlignment.Center;
138 _vertAlign = VerticalAlignment.Bottom;
139 _nudgeX = 0;
140 _nudgeY = 0;
141
142 Movement = new MovementController(
143 this,
144 MovementState.ForSceneLayer(),
145 SceneLayer);
146
147 AttachCollider(new TileCollider(
148 this,
149 collisionGroup: CollisionMasks.None,
150 collidesWith: CollisionMasks.None));
151
152 SetCollisionProfile(collisionProfileName);
153 CurrentFrame = frame;
154
155 _renderSize = SpriteManager.Instance.SizeNewSpritesToSceneLayer
156 ? new Size(_sceneLayer.TileWidth, _sceneLayer.TileHeight)
157 : CurrentFrame.TileSize;
158
159 zOrder = 1;
160
161 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
162 SpriteManager.Instance.AddSprite(this);
163 }
164
170 internal Sprite(Sprite sprite, SceneLayer sceneLayer)
171 {
172 ArgumentNullException.ThrowIfNull(sprite);
173
174 _sceneLayer = sceneLayer
175 ?? throw new ArgumentNullException(
176 nameof(sceneLayer),
177 "Sprite must be attached to a SceneLayer.");
178
179 animator = new Animator(this);
180
181 frame = sprite.frame;
183 _horizAlign = sprite._horizAlign;
184 _vertAlign = sprite._vertAlign;
185 _nudgeX = sprite._nudgeX;
186 _nudgeY = sprite._nudgeY;
187 _renderSize = sprite._renderSize;
188 _rotation = sprite._rotation;
189 ZOrder = sprite.zOrder;
190 visible = sprite.visible;
191 _sceneLayerCoordinates = sprite.SceneLayerCoordinates;
192
193 Movement = new MovementController(
194 this,
195 MovementState.ForSceneLayer(),
196 SceneLayer);
197
198 AttachCollider(new TileCollider(
199 this,
200 collisionGroup: CollisionMasks.None,
201 collidesWith: CollisionMasks.None));
202
203 if (string.IsNullOrWhiteSpace(CollisionProfileName))
204 SetCollisionProfile(SpriteManager.Instance.DefaultCollisionProfile);
205
206 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
207 SpriteManager.Instance.AddSprite(this);
208 }
209
210 ~Sprite()
211 {
212 Dispose();
213 }
214
215 [OnDeserialized]
216 private void OnDeserialized(StreamingContext context)
217 {
218 animator = new Animator(this);
219 pauseAnimation = false;
220
221 // Legacy files may still supply the old serialized SceneLayer object/reference.
222 // New files carry SceneId + SceneLayerId and are rebound by EngineState only after
223 // the corresponding GSCN scene has been restored.
224 if (!ReferenceEquals(_sceneLayer, SceneLayer.Empty))
225 {
226 InitializeDeserializedRuntimeBindings();
227 SpriteManager.Instance.AddSprite(this);
228 }
229 }
230
234 [JsonIgnore]
235 internal string? SerializedSceneIdForBinding =>
236 !ReferenceEquals(_sceneLayer, SceneLayer.Empty)
237 ? _sceneLayer.Scene?.ID ?? _serializedSceneId
238 : _serializedSceneId;
239
243 [JsonIgnore]
244 internal string? SerializedSceneLayerIdForBinding =>
245 !ReferenceEquals(_sceneLayer, SceneLayer.Empty)
246 ? _sceneLayer.ID
247 : _serializedSceneLayerId;
248
253 internal void RebindSceneLayerAfterDeserialization(SceneLayer sceneLayer)
254 {
255 ArgumentNullException.ThrowIfNull(sceneLayer);
256
257 if (ReferenceEquals(_sceneLayer, sceneLayer) && Movement is not null)
258 return;
259
260 Movement?.Dispose();
262
263 _sceneLayer = sceneLayer;
264 _serializedSceneId = sceneLayer.Scene?.ID;
265 _serializedSceneLayerId = sceneLayer.ID;
266
267 InitializeDeserializedRuntimeBindings();
268 }
269
270 private void InitializeDeserializedRuntimeBindings()
271 {
272 Movement = new MovementController(
273 this,
274 MovementState.ForSceneLayer(),
275 SceneLayer);
276
277 AttachCollider(new TileCollider(
278 this,
279 collisionGroup: CollisionMasks.None,
280 collidesWith: CollisionMasks.None));
281
282 if (string.IsNullOrWhiteSpace(CollisionProfileName))
283 SetCollisionProfile(SpriteManager.Instance.DefaultCollisionProfile);
284 else
285 RefreshCollisionProfile();
286
287 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
288 }
289
294
299 public Vector2 GetPosition() =>
300 new(_sceneLayerCoordinates.X, _sceneLayerCoordinates.Y);
301
306 public void SetPosition(Vector2 pos)
307 {
308 PointF oldCoord = _sceneLayerCoordinates;
309 Rectangle oldDraw = VisualBoundsWorld;
310
311 _sceneLayerCoordinates = new PointF(pos.X, pos.Y);
312
313 Rectangle newDraw = VisualBoundsWorld;
314 Rectangle movementWorldRect = Rectangle.Union(oldDraw, newDraw);
315 movementWorldRect.Inflate(5, 5);
316
317 _sceneLayer.RefreshQueue.AddWorldRect(movementWorldRect);
318 SpriteMoved?.Invoke(
319 new SpriteMovedEventArgs(this, oldCoord, _sceneLayerCoordinates));
320 }
321
330 [JsonProperty]
331 public float Rotation
332 {
333 get => _rotation;
334 set
335 {
336 if (!float.IsFinite(value))
337 throw new ArgumentOutOfRangeException(nameof(value), "Rotation must be a finite number of degrees.");
338
339 float normalized = value % 360f;
340 if (normalized < 0f)
341 normalized += 360f;
342
343 if (_rotation.Equals(normalized))
344 return;
345
346 Rectangle oldBounds = VisualBoundsWorld;
347 _rotation = normalized;
348 InvalidateVisualChange(oldBounds, VisualBoundsWorld);
349 }
350 }
351
355 [JsonIgnore]
356 public Rectangle VisualBoundsWorld =>
357 GetRotatedBounds(DrawLocationWorld, Rotation);
358
364 public RectangleF GetVisualBoundsScreen(View view) =>
365 GetRotatedBounds(GetDrawLocationScreen(view), Rotation);
366
367 internal RectangleF GetVisualBoundsScreen(RectangleF renderRectScreen) =>
368 GetRotatedBounds(renderRectScreen, Rotation);
369
371 public override void Draw(
372 BackbufferBase backbuffer,
373 RectangleF destRectScreen)
374 {
375 if (Rotation == 0f)
376 {
377 base.Draw(backbuffer, destRectScreen);
378 return;
379 }
380
381 float centerX = destRectScreen.Left + destRectScreen.Width * 0.5f;
382 float centerY = destRectScreen.Top + destRectScreen.Height * 0.5f;
383 var canvas = backbuffer.Canvas;
384
385 canvas.Save();
386 try
387 {
388 canvas.RotateDegrees(Rotation, centerX, centerY);
389 base.Draw(backbuffer, destRectScreen);
390 }
391 finally
392 {
393 canvas.Restore();
394 }
395 }
396
403 public void TranslateWorldPx(int dx, int dy)
404 {
405 if (dx == 0 && dy == 0)
406 return;
407
408 var originalRect = CollisionArea;
409 var translatedRect = originalRect;
410 translatedRect.X += dx;
411 translatedRect.Y += dy;
412
413 var originalSceneCoord =
414 GetSceneLayerCoordsFromSpriteWorldRect(originalRect);
415 var translatedSceneCoord =
416 GetSceneLayerCoordsFromSpriteWorldRect(translatedRect);
417
418 var sceneDelta = new Vector2(
419 translatedSceneCoord.X - originalSceneCoord.X,
420 translatedSceneCoord.Y - originalSceneCoord.Y);
421
422 SetPosition(GetPosition() + sceneDelta);
423 }
424
430 public void CancelVelocityComponent(bool cancelX, bool cancelY)
431 {
432 Movement.ZeroVelocityComponent(cancelX, cancelY);
433 }
434
438 [JsonIgnore]
439 public MovementController Movement { get; private set; } = null!;
440
442 [JsonProperty]
443 public override Frame CurrentFrame
444 {
445 get => base.CurrentFrame;
446 set
447 {
448 Rectangle oldBounds = VisualBoundsWorld;
449 base.CurrentFrame = value;
450 InvalidateVisualChange(oldBounds, VisualBoundsWorld);
451 }
452 }
453
455 [JsonProperty]
456 public override bool Visible
457 {
458 get => visible;
459 set
460 {
461 visible = value;
462 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
463 }
464 }
465
469 [JsonProperty]
471 {
472 get => _horizAlign;
473 set
474 {
475 if (_sceneLayer != null)
476 {
477 var oldRect = VisualBoundsWorld;
478 _horizAlign = value;
479 InvalidateVisualChange(oldRect, VisualBoundsWorld);
480 }
481 else
482 {
483 _horizAlign = value;
484 }
485 }
486 }
487
491 [JsonProperty]
493 {
494 get => _vertAlign;
495 set
496 {
497 if (_sceneLayer != null)
498 {
499 var oldRect = VisualBoundsWorld;
500 _vertAlign = value;
501 InvalidateVisualChange(oldRect, VisualBoundsWorld);
502 }
503 else
504 {
505 _vertAlign = value;
506 }
507 }
508 }
509
513 [JsonProperty]
514 public int NudgeX
515 {
516 get => _nudgeX;
517 set
518 {
519 if (_sceneLayer != null)
520 {
521 var oldRect = VisualBoundsWorld;
522 _nudgeX = value;
523 InvalidateVisualChange(oldRect, VisualBoundsWorld);
524 }
525 else
526 {
527 _nudgeX = value;
528 }
529 }
530 }
531
535 [JsonProperty]
536 public int NudgeY
537 {
538 get => _nudgeY;
539 set
540 {
541 if (_sceneLayer != null)
542 {
543 var oldRect = VisualBoundsWorld;
544 _nudgeY = value;
545 InvalidateVisualChange(oldRect, VisualBoundsWorld);
546 }
547 else
548 {
549 _nudgeY = value;
550 }
551 }
552 }
553
557 [JsonProperty]
558 public Size RenderSize
559 {
560 get => _renderSize;
561 set
562 {
563 if (_sceneLayer != null)
564 {
565 var oldRect = VisualBoundsWorld;
566 _renderSize = value;
567 InvalidateVisualChange(oldRect, VisualBoundsWorld);
568 }
569 else
570 {
571 _renderSize = value;
572 }
573 }
574 }
575
577 [JsonIgnore]
578 public override Rectangle DrawLocationWorld
579 {
580 get
581 {
582 Point pxlPt = _sceneLayer.CoordinateSystem
583 .GetAnchorPixelAtSceneLayerCoordinates(
584 _sceneLayer,
586
587 switch (HorizAlign)
588 {
589 case HorizontalAlignment.Center:
590 pxlPt.X += (_sceneLayer.TileWidth - _renderSize.Width) / 2;
591 break;
592 case HorizontalAlignment.Right:
593 pxlPt.X += _sceneLayer.TileWidth - _renderSize.Width;
594 break;
595 }
596
597 switch (VertAlign)
598 {
599 case VerticalAlignment.Middle:
600 pxlPt.Y += (_sceneLayer.TileHeight - _renderSize.Height) / 2;
601 break;
602 case VerticalAlignment.Bottom:
603 pxlPt.Y += _sceneLayer.TileHeight - _renderSize.Height;
604 break;
605 }
606
607 pxlPt.X += NudgeX;
608 pxlPt.Y += NudgeY;
609
610 return new Rectangle(pxlPt, _renderSize);
611 }
612 }
613
615 [JsonIgnore]
616 public override bool IsPositionFixed => false;
617
619 [JsonIgnore]
620 public override PointF SceneLayerCoordinates => _sceneLayerCoordinates;
621
623 [JsonIgnore]
624 public override SceneLayer SceneLayer => _sceneLayer;
625
627 [JsonProperty]
628 public override int ZOrder
629 {
630 get => zOrder;
631 set
632 {
633 zOrder = Math.Max(1, value);
634 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
635 }
636 }
637
638 internal PointF GetSceneLayerCoordsFromSpriteWorldRect(Rectangle worldRectPx)
639 {
640 worldRectPx.X -= NudgeX;
641 worldRectPx.Y -= NudgeY;
642
643 switch (HorizAlign)
644 {
645 case HorizontalAlignment.Center:
646 worldRectPx.X -= (_sceneLayer.TileWidth - worldRectPx.Width) / 2;
647 break;
648 case HorizontalAlignment.Right:
649 worldRectPx.X -= _sceneLayer.TileWidth - worldRectPx.Width;
650 break;
651 }
652
653 switch (VertAlign)
654 {
655 case VerticalAlignment.Middle:
656 worldRectPx.Y -= (_sceneLayer.TileHeight - worldRectPx.Height) / 2;
657 break;
658 case VerticalAlignment.Bottom:
659 worldRectPx.Y -= _sceneLayer.TileHeight - worldRectPx.Height;
660 break;
661 }
662
663 return _sceneLayer.CoordinateSystem.GetSceneLayerCoordinatesAtPixel(
664 _sceneLayer,
665 worldRectPx.Location);
666 }
667
669 public override void Dispose()
670 {
671 _pendingDispose = true;
672 }
673
674 internal void DisposeImmediate()
675 {
676 GC.SuppressFinalize(this);
677 Disposing?.Invoke(this);
678
679 if (_sceneLayer != null)
680 _sceneLayer.RefreshQueue.AddWorldRect(VisualBoundsWorld);
681
682 SpriteMoved = null;
683 VisualBoundsChanged = null;
684 Disposing = null;
685
686 base.Dispose();
687 }
688
689 private void InvalidateVisualChange(Rectangle oldBounds, Rectangle newBounds)
690 {
691 Rectangle dirtyBounds = Rectangle.Union(oldBounds, newBounds);
692 dirtyBounds.Inflate(2, 2);
693 _sceneLayer.RefreshQueue.AddWorldRect(dirtyBounds);
694 VisualBoundsChanged?.Invoke(this);
695 }
696
697 private static Rectangle GetRotatedBounds(Rectangle rect, float degrees)
698 {
699 RectangleF bounds = GetRotatedBounds(
700 new RectangleF(rect.X, rect.Y, rect.Width, rect.Height),
701 degrees);
702
703 return Rectangle.FromLTRB(
704 (int)MathF.Floor(bounds.Left),
705 (int)MathF.Floor(bounds.Top),
706 (int)MathF.Ceiling(bounds.Right),
707 (int)MathF.Ceiling(bounds.Bottom));
708 }
709
710 private static RectangleF GetRotatedBounds(RectangleF rect, float degrees)
711 {
712 if (rect.IsEmpty || degrees % 360f == 0f)
713 return rect;
714
715 float radians = degrees * MathF.PI / 180f;
716 float sin = MathF.Abs(MathF.Sin(radians));
717 float cos = MathF.Abs(MathF.Cos(radians));
718 float width = rect.Width * cos + rect.Height * sin;
719 float height = rect.Width * sin + rect.Height * cos;
720 float centerX = rect.Left + rect.Width * 0.5f;
721 float centerY = rect.Top + rect.Height * 0.5f;
722
723 return new RectangleF(
724 centerX - width * 0.5f,
725 centerY - height * 0.5f,
726 width,
727 height);
728 }
729}
Provides data for sprite movement events.
void TranslateWorldPx(int dx, int dy)
Applies a world-pixel translation without rebuilding the absolute position from an integer rectangle,...
Definition Sprite.cs:403
override void Draw(BackbufferBase backbuffer, RectangleF destRectScreen)
Draws the current content into the backbuffer at the supplied screen-pixel bounds.
Definition Sprite.cs:371
int NudgeX
Gets or sets the nudge x.
Definition Sprite.cs:515
Action< Sprite >? Disposing
Occurs when disposal begins.
Definition Sprite.cs:32
override Rectangle DrawLocationWorld
Definition Sprite.cs:579
VerticalAlignment VertAlign
Gets or sets the vert align.
Definition Sprite.cs:493
MovementController Movement
Gets the controller that manages sprite movement.
Definition Sprite.cs:439
Size RenderSize
Gets or sets the render size.
Definition Sprite.cs:559
Action< SpriteMovedEventArgs >? SpriteMoved
Occurs when the sprite position changes.
Definition Sprite.cs:24
void CancelVelocityComponent(bool cancelX, bool cancelY)
Clears the requested horizontal and vertical velocity components.
Definition Sprite.cs:430
override bool Visible
Gets a value indicating whether the object is visible.
Definition Sprite.cs:457
Action< Sprite >? VisualBoundsChanged
Occurs when the sprite visual bounds change.
Definition Sprite.cs:28
MovementSpace PositionSpace
Gets the coordinate space used by the position API.
Definition Sprite.cs:293
void SetPosition(Vector2 pos)
Sets the sprite position in scene-layer grid coordinates and invalidates its old and new bounds.
Definition Sprite.cs:306
int NudgeY
Gets or sets the nudge y.
Definition Sprite.cs:537
override bool IsPositionFixed
Definition Sprite.cs:616
HorizontalAlignment HorizAlign
Gets or sets the horiz align.
Definition Sprite.cs:471
float Rotation
Gets or sets the clockwise visual rotation, in degrees, around the center of the sprite's render rect...
Definition Sprite.cs:332
override void Dispose()
Releases the resources owned by this instance.
Definition Sprite.cs:669
RectangleF GetVisualBoundsScreen(View view)
Gets the axis-aligned screen-pixel bounds enclosing the rotated sprite.
override PointF SceneLayerCoordinates
Definition Sprite.cs:620
Rectangle VisualBoundsWorld
Gets the axis-aligned world-pixel bounds enclosing the rotated sprite.
Definition Sprite.cs:356
override SceneLayer SceneLayer
Definition Sprite.cs:624
override int ZOrder
Gets the z-order of the element, which determines its visual stacking order relative to other element...
Definition Sprite.cs:629
Vector2 GetPosition()
Gets the sprite position in scene-layer grid coordinates.
override Frame CurrentFrame
Definition Sprite.cs:444
Represents an abstract base class for drawable tiles in the Gondwana engine. Provides core functional...
Definition Tile.cs:19
virtual RectangleF GetDrawLocationScreen(View view)
Transforms the drawable bounds into screen pixels for the specified view.
void AttachCollider(ICollider collider)
Attaches this tile's collider and applies any collision state that was set before the collider became...
Definition Tile.cs:504
virtual Rectangle CollisionArea
Gets the effective collision area in world coordinates.
Definition Tile.cs:235
bool pauseAnimation
The pause animation.
Definition Tile.cs:54
void CopyCollisionSettingsFrom(Tile source)
Copies collision behavior, profile, and effective frame metadata from another tile.
Definition Tile.cs:403
void SetCollisionProfile(string profileName)
Applies a named profile from this tile's parent scene. If the layer has not yet been attached to a sc...
Definition Tile.cs:376
void DetachCollider()
Detaches the current collider from its scene-layer registry without otherwise changing this tile's se...
Definition Tile.cs:490
string? CollisionProfileName
Gets the scene-level collision profile currently associated with this tile.
Definition Tile.cs:331
Central controller responsible for managing all movement modes for a single IMovable object....
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Represents a rendered view of a scene, combining a camera position with a viewport configuration to c...
Definition View.cs:14
Represents a single layer within a Scene, containing a grid of tiles and supporting various coordinat...
Definition SceneLayer.cs:40
string ID
Gets or sets the unique identifier for this layer.
Scene Scene
Gets or sets the parent scene that contains this layer.
static SceneLayer Empty
Gets the singleton empty scene layer instance.
string ID
Gets or sets the unique identifier for this scene.
Definition Scene.cs:196
Represents a collision entity that can be moved or repositioned in response to collision resolution.
Implemented by grid-space IMovable objects that belong to a specific Scenes.SceneLayer.
VerticalAlignment
Specifies the vertical alignment of a sprite or element.
HorizontalAlignment
Specifies the horizontal alignment of an element.
MovementSpace
Specifies the coordinate system used for movement calculations and updates.
Represents the source Tilesheet and its coordinates to render on a destination.
Definition Frame.cs:12