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
DirectComposite.cs
1using System.Collections.ObjectModel;
2using System.Drawing;
3using System.Numerics;
10
12
29{
30 private static readonly object _parentSyncRoot = new();
31
32 private static readonly Dictionary<IDirectCompositeChild, DirectComposite> _parents =
33 new(ReferenceEqualityComparer.Instance);
34
35 private readonly List<IDirectCompositeChild> _children = [];
36 private readonly Dictionary<IDirectCompositeChild, Vector2> _localOffsetPx =
37 new(ReferenceEqualityComparer.Instance);
38
39 private readonly ReadOnlyCollection<IDirectCompositeChild> _readOnlyChildren;
40
41 private PointF _anchor;
42 private long _lastTick = HighResTimer.GetCurrentTick();
43 private bool _disposed;
44
45 #region events
46
50 public event EventHandler<IDirectDrawable>? Disposing;
51
52 #endregion events
53
54 #region constructors
55
63 public DirectComposite(RenderSurfaceHostBase renderSurfaceHost,
65 PointF anchor = default,
66 string? nickname = null)
67 {
68 RenderSurfaceHost = renderSurfaceHost ?? throw new ArgumentNullException(nameof(renderSurfaceHost));
69
70 Mode = mode;
71 _anchor = anchor;
72 Nickname = nickname ?? Id.ToString();
73
74 _readOnlyChildren = new ReadOnlyCollection<IDirectCompositeChild>(_children);
75
76 Movement = new MovementController(this, MovementState.ForPixel());
77
78 DirectDrawingManager.Instance.AddOrReplace(this);
79 }
80
81 #endregion constructors
82
83 #region public properties
84
88 public Guid Id { get; } = Guid.NewGuid();
89
93 public string? Nickname { get; private set; }
94
99
103 public DirectDrawingMode Mode { get; }
104
112 public SceneLayer? SceneLayer { get; private set; }
113
121 public View? View { get; private set; }
122
129 public ReadOnlyCollection<IDirectCompositeChild> Children => _readOnlyChildren;
130
135
140
144 public bool Visible
145 {
146 get => _children.Any(static child => child.Visible);
147 set
148 {
149 foreach (IDirectCompositeChild child in _children)
150 child.SetIsVisible(value);
151 }
152 }
153
161 public int ZOrder => 0;
162
166 public Rectangle ScreenBounds => GetBounds(static child => child.ScreenBounds);
167
171 public Rectangle WorldBounds => GetBounds(static child => child.WorldBounds);
172
173 #endregion public properties
174
175 #region public methods
176
181 public Vector2 GetPosition() => new(_anchor.X, _anchor.Y);
182
187 public void SetPosition(Vector2 position)
188 {
189 SetPosition(position.X, position.Y);
190 }
191
198 public DirectComposite SetPosition(float x, float y)
199 {
200 ObjectDisposedException.ThrowIf(_disposed, this);
201
202 _anchor = new PointF(x, y);
203 var anchor = new Vector2(x, y);
204
205 foreach (IDirectCompositeChild child in _children)
206 {
207 Vector2 offset = _localOffsetPx.TryGetValue(child, out Vector2 value) ? value : Vector2.Zero;
208 child.SetPosition(anchor + offset);
209 }
210
211 return this;
212 }
213
235 bool keepCurrentOffset = true,
236 Vector2? explicitLocalOffsetPx = null)
237 {
238 ObjectDisposedException.ThrowIf(_disposed, this);
239 ArgumentNullException.ThrowIfNull(child);
240
241 if (_children.Contains(child))
242 return this;
243
244 ValidateChildIdentity(child);
245 ValidateParentRelationship(child);
246 ValidateAndAssignTarget(child);
247
248 Vector2 offset = keepCurrentOffset ? child.GetPosition() - new Vector2(_anchor.X, _anchor.Y) : explicitLocalOffsetPx ?? Vector2.Zero;
249
250 RegisterParent(child);
251
252 try
253 {
254 _children.Add(child);
255 _localOffsetPx[child] = offset;
256 child.Disposing += OnChildDisposing;
257
258 child.SetPosition(new Vector2(_anchor.X, _anchor.Y) + offset);
259
260 OnChildAdded(child);
261 }
262 catch
263 {
264 child.Disposing -= OnChildDisposing;
265 _children.Remove(child);
266 _localOffsetPx.Remove(child);
267 UnregisterParent(child);
268 ResetTargetWhenEmpty();
269 throw;
270 }
271
272 return this;
273 }
274
281 {
282 ObjectDisposedException.ThrowIf(_disposed, this);
283 ArgumentNullException.ThrowIfNull(child);
284
285 if (!_children.Remove(child))
286 return this;
287
288 child.Disposing -= OnChildDisposing;
289 _localOffsetPx.Remove(child);
290 UnregisterParent(child);
291 ResetTargetWhenEmpty();
292
293 OnChildRemoved(child);
294
295 return this;
296 }
297
302 public virtual DirectComposite Clear()
303 {
304 ObjectDisposedException.ThrowIf(_disposed, this);
305
306 IDirectCompositeChild[] children = [.. _children];
307
308 foreach (IDirectCompositeChild child in children)
309 {
310 child.Disposing -= OnChildDisposing;
311 UnregisterParent(child);
312 }
313
314 _children.Clear();
315 _localOffsetPx.Clear();
316 ResetTargetWhenEmpty();
317
318 foreach (IDirectCompositeChild child in children)
319 OnChildRemoved(child);
320
321 return this;
322 }
323
331 Vector2 newLocalOffsetPx)
332 {
333 ObjectDisposedException.ThrowIf(_disposed, this);
334 ArgumentNullException.ThrowIfNull(child);
335
336 if (!_children.Contains(child))
337 return this;
338
339 _localOffsetPx[child] = newLocalOffsetPx;
340
341 child.SetPosition(new Vector2(_anchor.X, _anchor.Y) + newLocalOffsetPx);
342
343 return this;
344 }
345
351 public DirectComposite SetZOrder(int zOrder)
352 {
353 foreach (IDirectCompositeChild child in _children)
354 child.SetZOrder(zOrder);
355
356 return this;
357 }
358
364 public DirectComposite SetOpacity(float opacity)
365 {
366 foreach (IDirectCompositeChild child in _children)
367 child.SetOpacity(opacity);
368
369 return this;
370 }
371
377 public DirectComposite SetIsVisible(bool visible)
378 {
379 Visible = visible;
380 return this;
381 }
382
389 public DirectComposite FadeTo(float targetOpacity, float durationSec)
390 {
391 foreach (IDirectCompositeChild child in _children)
392 child.FadeTo(targetOpacity, durationSec);
393
394 return this;
395 }
396
402 public DirectComposite FadeIn(float durationSec) => FadeTo(1f, durationSec);
403
409 public DirectComposite FadeOut(float durationSec) => FadeTo(0f, durationSec);
410
416 public RectangleF GetDrawLocationScreen(View view)
417 {
418 ArgumentNullException.ThrowIfNull(view);
419
420 if (Mode == DirectDrawingMode.View && !ReferenceEquals(View, view))
421 {
422 return RectangleF.Empty;
423 }
424
425 float minX = float.MaxValue;
426 float minY = float.MaxValue;
427 float maxX = float.MinValue;
428 float maxY = float.MinValue;
429
430 foreach (IDirectCompositeChild child in _children)
431 {
432 if (!child.Visible)
433 continue;
434
435 RectangleF bounds = child.GetDrawLocationScreen(view);
436
437 if (bounds.IsEmpty)
438 continue;
439
440 minX = Math.Min(minX, bounds.Left);
441 minY = Math.Min(minY, bounds.Top);
442 maxX = Math.Max(maxX, bounds.Right);
443 maxY = Math.Max(maxY, bounds.Bottom);
444 }
445
446 if (minX == float.MaxValue)
447 return RectangleF.Empty;
448
449 return RectangleF.FromLTRB(
450 (float)Math.Floor(minX),
451 (float)Math.Floor(minY),
452 (float)Math.Ceiling(maxX),
453 (float)Math.Ceiling(maxY));
454 }
455
461 public void Draw(BackbufferBase backbuffer, RectangleF destRectScreen) { }
462
471 public virtual void Update(long tick)
472 {
473 if (_disposed || tick <= _lastTick)
474 return;
475
476 Movement.AdvanceMovement(
477 HighResTimer.GetDuration(_lastTick, tick));
478
479 _lastTick = tick;
480 }
481
485 public virtual void Dispose()
486 {
487 if (_disposed)
488 return;
489
490 _disposed = true;
491
492 Disposing?.Invoke(this, this);
493
494 IDirectCompositeChild[] children = [.. _children];
495
496 foreach (IDirectCompositeChild child in children)
497 child.Dispose();
498
499 foreach (IDirectCompositeChild child in _children)
500 {
501 child.Disposing -= OnChildDisposing;
502 UnregisterParent(child);
503 }
504
505 _children.Clear();
506 _localOffsetPx.Clear();
507 ResetTargetWhenEmpty();
508 Disposing = null;
509
510 GC.SuppressFinalize(this);
511 }
512
513 #endregion public methods
514
515 #region IDirectCompositeChild explicit interface implementations
516
517 void IDirectCompositeChild.SetIsVisible(bool visible) => SetIsVisible(visible);
518
519 void IDirectCompositeChild.SetZOrder(int zOrder) => SetZOrder(zOrder);
520
521 void IDirectCompositeChild.SetOpacity(float opacity) => SetOpacity(opacity);
522
523 void IDirectCompositeChild.FadeTo(float targetOpacity, float durationSec) => FadeTo(targetOpacity, durationSec);
524
525 #endregion IDirectCompositeChild explicit interface implementations
526
527 #region IDirectCompositeContainer explicit interface implementations
528
529 IReadOnlyCollection<IDirectCompositeChild> IDirectCompositeContainer.Children => Children;
530
531 IDirectCompositeContainer IDirectCompositeContainer.Add(IDirectCompositeChild child, bool keepCurrentOffset, Vector2? explicitLocalOffsetPx)
532 {
533 return Add(child, keepCurrentOffset, explicitLocalOffsetPx);
534 }
535
536 IDirectCompositeContainer IDirectCompositeContainer.Remove(IDirectCompositeChild child)
537 {
538 return Remove(child);
539 }
540
541 IDirectCompositeContainer IDirectCompositeContainer.Clear()
542 {
543 return Clear();
544 }
545
546 #endregion IDirectCompositeContainer explicit interface implementations
547
548 #region protected hooks
549
560 protected virtual void OnChildAdded(IDirectCompositeChild child)
561 {
562 }
563
568 protected virtual void OnChildRemoved(IDirectCompositeChild child)
569 {
570 }
571
572 #endregion protected hooks
573
574 #region private methods
575
576 private Rectangle GetBounds(Func<IDirectCompositeChild, Rectangle> selector)
577 {
578 float minX = float.MaxValue;
579 float minY = float.MaxValue;
580 float maxX = float.MinValue;
581 float maxY = float.MinValue;
582
583 foreach (IDirectCompositeChild child in _children)
584 {
585 if (!child.Visible)
586 continue;
587
588 Rectangle bounds = selector(child);
589
590 if (bounds.IsEmpty)
591 continue;
592
593 minX = Math.Min(minX, bounds.Left);
594 minY = Math.Min(minY, bounds.Top);
595 maxX = Math.Max(maxX, bounds.Right);
596 maxY = Math.Max(maxY, bounds.Bottom);
597 }
598
599 if (minX == float.MaxValue)
600 return Rectangle.Empty;
601
602 return Rectangle.FromLTRB(
603 (int)Math.Floor(minX),
604 (int)Math.Floor(minY),
605 (int)Math.Ceiling(maxX),
606 (int)Math.Ceiling(maxY));
607 }
608
609 private void ValidateChildIdentity(IDirectCompositeChild child)
610 {
611 if (!ReferenceEquals(child.RenderSurfaceHost, RenderSurfaceHost))
612 {
613 throw new ArgumentException("The child must belong to the same RenderSurfaceHost as the composite.", nameof(child));
614 }
615
616 if (child.Mode != Mode)
617 {
618 throw new ArgumentException($"The child drawing mode '{child.Mode}' does not match the composite drawing mode '{Mode}'.", nameof(child));
619 }
620 }
621
622 private void ValidateAndAssignTarget(IDirectCompositeChild child)
623 {
624 if (Mode == DirectDrawingMode.SceneLayer)
625 {
626 SceneLayer childLayer = child.SceneLayer ?? throw new ArgumentException("A scene-layer child must reference a SceneLayer.", nameof(child));
627
628 if (SceneLayer is null)
629 {
630 SceneLayer = childLayer;
631 }
632 else if (!ReferenceEquals(SceneLayer, childLayer))
633 {
634 throw new ArgumentException("All scene-layer children in a composite must reference the same SceneLayer.", nameof(child));
635 }
636
637 return;
638 }
639
640 if (child.View is null)
641 {
642 if (child is DirectComposite compositeChild &&
643 compositeChild.Children.Count == 0)
644 {
645 throw new ArgumentException(
646 "An empty composite cannot be added in view mode because it has no View yet. Add at least one descendant first.",
647 nameof(child));
648 }
649
650 throw new ArgumentException("A view child must reference a View.", nameof(child));
651 }
652
653 View childView = child.View;
654 if (View is null)
655 {
656 View = childView;
657 }
658 else if (!ReferenceEquals(View, childView))
659 {
660 throw new ArgumentException("All view children in a composite must reference the same View.", nameof(child));
661 }
662 }
663
664 private void ValidateParentRelationship(IDirectCompositeChild child)
665 {
666 lock (_parentSyncRoot)
667 {
668 if (_parents.TryGetValue(child, out DirectComposite? existingParent))
669 {
670 throw new InvalidOperationException($"The child already belongs to composite '{existingParent.Nickname}'.");
671 }
672
673 if (child is not DirectComposite childComposite)
674 return;
675
676 DirectComposite? ancestor = this;
677
678 while (ancestor is not null)
679 {
680 if (ReferenceEquals(ancestor, childComposite))
681 {
682 throw new InvalidOperationException("Adding the child would create a recursive composite cycle.");
683 }
684
685 _parents.TryGetValue(ancestor, out ancestor);
686 }
687 }
688 }
689
690 private void RegisterParent(IDirectCompositeChild child)
691 {
692 lock (_parentSyncRoot)
693 _parents.Add(child, this);
694 }
695
696 private void UnregisterParent(IDirectCompositeChild child)
697 {
698 lock (_parentSyncRoot)
699 {
700 if (_parents.TryGetValue(child, out DirectComposite? parent) &&
701 ReferenceEquals(parent, this))
702 {
703 _parents.Remove(child);
704 }
705 }
706 }
707
708 private void ResetTargetWhenEmpty()
709 {
710 if (_children.Count != 0)
711 return;
712
713 SceneLayer = null;
714 View = null;
715 }
716
717 private void OnChildDisposing(object? sender, IDirectDrawable drawing)
718 {
719 if (drawing is not IDirectCompositeChild child)
720 return;
721
722 child.Disposing -= OnChildDisposing;
723
724 if (!_children.Remove(child))
725 return;
726
727 _localOffsetPx.Remove(child);
728 UnregisterParent(child);
729 ResetTargetWhenEmpty();
730
731 OnChildRemoved(child);
732 }
733
734 #endregion private methods
735}
bool Visible
Gets or sets the visibility of all descendants.
DirectComposite SetPosition(float x, float y)
Sets the composite anchor and repositions all children from their stored offsets.
virtual DirectComposite Remove(IDirectCompositeChild child)
Removes a child without disposing it.
string? Nickname
Gets the optional diagnostic nickname of the composite.
virtual void OnChildAdded(IDirectCompositeChild child)
Called after a child has been successfully added.
ReadOnlyCollection< IDirectCompositeChild > Children
Gets the children owned by the composite.
virtual void OnChildRemoved(IDirectCompositeChild child)
Called after a child has been removed from the composite.
DirectComposite FadeIn(float durationSec)
Fades every descendant to full opacity.
RenderSurfaceHostBase RenderSurfaceHost
Gets the render surface host shared by the composite and its children.
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
DirectComposite SetZOrder(int zOrder)
Applies one Z-order to every descendant.
virtual void Update(long tick)
Advances composite movement.
DirectComposite FadeTo(float targetOpacity, float durationSec)
Fades every descendant to the requested opacity.
RectangleF GetDrawLocationScreen(View view)
Gets the union of all visible descendants as projected through a view.
DirectComposite FadeOut(float durationSec)
Fades every descendant to zero opacity.
DirectComposite SetOpacity(float opacity)
Applies one opacity to every descendant.
DirectComposite SetLocalOffset(IDirectCompositeChild child, Vector2 newLocalOffsetPx)
Changes one child's local offset and reapplies its position.
DirectComposite SetIsVisible(bool visible)
Applies one visibility value to every descendant.
Rectangle WorldBounds
Gets the union of all visible descendants' world-space bounds.
EventHandler< IDirectDrawable >? Disposing
Occurs when the composite is being disposed.
Guid Id
Gets the unique identifier of the composite.
Rectangle ScreenBounds
Gets the union of all visible descendants' screen-space bounds.
int ZOrder
Gets the composite Z-order.
virtual DirectComposite Clear()
Removes every child without disposing them.
View? View
Gets the view shared by all children in view mode.
virtual void Dispose()
Disposes the composite and every child it owns.
MovementController Movement
Gets the movement controller used to move the composite anchor.
DirectComposite(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the DirectComposite class.
void Draw(BackbufferBase backbuffer, RectangleF destRectScreen)
Performs no direct rendering because descendants render independently.
Vector2 GetPosition()
Gets the current composite anchor.
MovementSpace PositionSpace
Gets the coordinate space used by the movement controller.
virtual DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.
void SetPosition(Vector2 position)
Sets the composite anchor and repositions all children from their stored offsets.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
Manages the lifecycle, registration, and retrieval of all direct drawing objects in the Gondwana engi...
static DirectDrawingManager Instance
Gets the singleton instance of the direct drawing manager.
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...
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Defines a movable direct drawable that may be owned by a DirectComposite.
void FadeTo(float targetOpacity, float durationSec)
Fades this child to the requested opacity.
void SetIsVisible(bool visible)
Applies a visibility value to this child.
void SetZOrder(int zOrder)
Applies a Z-order value to this child.
void SetOpacity(float opacity)
Applies an opacity value to this child.
Defines a container that can hold and manage a collection of direct composite children.
IDirectCompositeContainer Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a child element to this container.
IReadOnlyCollection< IDirectCompositeChild > Children
Gets the read-only collection of child elements in this container.
IDirectCompositeContainer Remove(IDirectCompositeChild child)
Removes a child element from this container.
IDirectCompositeContainer Clear()
Removes all child elements from this container.
bool Visible
Gets a value indicating whether the object is visible.
Definition IDrawable.cs:28
RectangleF GetDrawLocationScreen(View view)
Computes the object's destination rectangle in SCREEN pixels for rendering on the Backbuffer,...
Vector2 GetPosition()
Get the current position in the mover's PositionSpace.
void SetPosition(Vector2 pos)
Set the position in the mover's PositionSpace.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
MovementSpace
Specifies the coordinate system used for movement calculations and updates.
Represents the current motion state of an IMovable object, including velocity, acceleration,...