Game components and the service container
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. The component hooks, explicit disposal and drawable unloading are pinned by a lifecycle compatibility suite, while ordering and CompareTo remain source readings; XNA 4.0 comparisons come from a disassembly of its Game assembly.
Components and services are how an XNA game shares work and objects without threading pointers through every constructor. CNA keeps both APIs and their names, but C++ ownership and the lack of runtime reflection change several contracts: when a drawable component loads and unloads, what disposal does to registration, how the collection's events and iterators behave, and how a service is keyed. This page gives the exact rules with the XNA 4.0 comparison and two complete, syntax-checked examples. The user-level introduction is the game loop guide and Tutorial 47; the threading history of the component lists is Case study: Game component lifetime.
The component types
IGameComponent has one pure virtual, Initialize(). IUpdateable and IDrawable each expose an enabled-or-visible getter, an order getter, two changed-event accessors returning System::EventHandler& (the C++ form of an event on an interface) and the Update(GameTime&) or Draw(const GameTime&) method. GameComponent (GameComponent.hpp) implements IGameComponent, IUpdateable, System::IComparable<GameComponent> and System::IDisposable; it knows its owning Game (get only), is Enabled by default and has an UpdateOrder of 0. DrawableGameComponent adds IDrawable: Visible (default true), DrawOrder (default 0), LoadContent() / UnloadContent() and a GraphicsDevice getter that simply forwards to the game — so, unlike XNA's, it works before the component is initialized.
The property setters change the value and then call the protected OnEnabledChanged / OnUpdateOrderChanged / OnVisibleChanged / OnDrawOrderChanged virtual, whose base body raises the public event; setting the current value does nothing. That is XNA 4.0's shape: the protected On…Changed method is what raises the event, so an override that skips its base suppresses it, in CNA as in XNA (HooksDispatchAndCanSuppressAllFourChangeEvents). A component the collection owns is kept alive until the hook returns, even if a handler removes it. Where FNA passes a null event argument, CNA passes EventArgs::Empty, because a reference cannot be null.
Update and draw order
Game keeps its own ordered lists of IUpdateable* and IDrawable*. Each insertion goes before the first entry whose order is strictly greater, so lower values run first and equal values keep insertion order; a change to UpdateOrder or DrawOrder re-sorts that one entry through an event subscription Game installed when it categorized the component. Disabled and invisible entries stay in the lists and are skipped per frame.
GameComponent::CompareTo(other) returns other.UpdateOrder - this.UpdateOrder, the reverse of the usual sign convention. Nothing in Game uses it (the lists compare with <), so the sign never affects the loop, but a caller that sorts components with it gets descending order, and for extreme values (orders near the int limits) the subtraction overflows. The member is not part of XNA 4.0's GameComponent, which implements only IGameComponent, IUpdateable and IDisposable, and it is not marked CNAEXT.
Because the base Game::Draw knows nothing about what the derived Draw drew before or after calling it, a component that must appear on top of the game's own drawing needs both a high DrawOrder (to follow other components) and a derived Draw that calls Game::Draw(gameTime) last.
When a drawable component loads and unloads
DrawableGameComponent.cpp implements a deliberately small contract:
Initialize()callsLoadContent()the first time (aninitialized_flag guards it). A derivedInitialize()that does not callDrawableGameComponent::Initialize()never loads its content.- In the ordinary loop, components are initialized by base
Game::Initialize(), afterDoInitializehas already applied the manager's preferences to the existing device, so this is a sound first load point. A component added later is initialized byGame'sComponentAddedhandler on whichever thread calledAdd— for a loading-screen thread, itsLoadContent()runs there. - The component does not follow the device. A private
OnDeviceCreatedthat would reload content exists but is subscribed to nothing, and there is noDeviceDisposinghook.UnloadContent()runs fromDispose(true)on every explicit disposal, before the base disposal raisesDisposedand even for a component that was never initialized, as XNA's does; the flag is not reset, so a disposed component initialized again does not load again. - Destruction never calls
UnloadContent()(why).
XNA 4.0's DrawableGameComponent.Initialize() (read from a disassembly of its assembly) is stricter and richer: it throws InvalidOperationException when no IGraphicsDeviceService is registered, hooks the service's DeviceCreated, DeviceResetting, DeviceReset and DeviceDisposing, loads content only when a device already exists, reloads on every DeviceCreated and unloads on DeviceDisposing; its GraphicsDevice getter throws before initialization. A CNA component therefore works in a game without a GraphicsDeviceManager, but must be treated as a one-time initial load, not a device-lifecycle subscriber.
Disposal unregisters
GameComponent::Dispose() calls Dispose(true), which sets the component's disposed flag, removes it from Game.Components (raising ComponentRemoved, so Game drops it from its update and draw lists) and then raises its Disposed event, as XNA 4.0's GameComponent.Dispose(true) does. Every explicit call repeats the removal attempt and the event; the destructor's Dispose(false) does neither. So the XNA idiom "dispose the pause menu to make it go away" works. A component the collection owns through the std::shared_ptr overloads stays alive until the whole virtual disposal call has returned, even when the removal drops the last reference. At game shutdown Game::Dispose() disposes a snapshot of the original components, keeping each alive even if an earlier callback removes it, and each component leaves the collection as it is disposed (consequences). DisposalRemovesComponentBeforeRepeatedEvents, LastOwnedReferenceSurvivesTheEntireVirtualDisposalCall and GameDisposesAnOwningSnapshotDespiteCallbackRemoval pin these rules.
GameComponentCollection
GameComponentCollection.cpp stores raw IGameComponent* entries and raises ComponentAdded / ComponentRemoved (EventHandler<GameComponentCollectionEventArgs>) around Add, Insert, Remove, RemoveAt and Clear, with IndexOf, Contains, getCountProperty() and operator[] for queries. The details that surprise:
- Ownership.
Add(IGameComponent*)andInsert(index, IGameComponent*)borrow. TheCNAEXToverloads takingstd::shared_ptr<IGameComponent>make the collection an owner, as XNA's collection holds a strong reference; the reference is taken before the insert (so aComponentAddedhandler that removes the component again finds it) and released only afterComponentRemovedhas been raised. A rejected add (a duplicate, or aComponentAddedhandler that throws after the insert) keeps the reference whenever the collection still lists the component, so a listed component is never left without an owner. - Duplicates and nulls. Adding a pointer that is already present throws
System::ArgumentExceptionwith XNA's text ("Cannot add the same game component to a game component collection multiple times."), the type an XNA game catches. A null pointer can be inserted once — it raises no event andGameskips it — and a second null throws the same duplicate error. - Indices.
operator[],RemoveAtandInsertcheck bounds and throwSystem::ArgumentOutOfRangeExceptionnamingindex, as .NET'sCollection<T>does and unlikestd::vector.IndexOfreturns-1for an absent item;Removereturnsfalsefor one and is[[nodiscard]], so discarding its result draws a compiler warning. - Event timing.
RemoveandRemoveAterase the entry first and then raiseComponentRemoved, so a handler sees a live component that is no longer in the collection.ClearraisesComponentRemovedfor every entry while all of them are still in the collection, then empties it. A destroyed collection raises nothing. - Iteration. Because C#'s
IEnumerable<IGameComponent>has no direct C++ equivalent, the collection addsCNAEXTsize_type,iteratorandconst_iteratoraliases andbegin()/end(), so it works with range-basedforand standard algorithms. The non-const iterators are plain vector iterators over the raw pointers: writing through one replaces an entry without raising either event or updatingGame's lists. Treat them as read-only. - Replacement. The private
SetItemthrowsSystem::NotSupportedException, as XNA and FNA do; it is unreachable through the public surface.
Game subscribes to the two events only when DoInitialize runs; components added before Run() are picked up by that method's categorization loop and initialized by base Game::Initialize(). The component lists behind the collection are protected by a recursive mutex so a loading thread may add components; the collection's own storage is not locked.
A self-contained drawable component
A frame counter is the natural first component. It must count in Draw: under the fixed step Update runs at the target rate even when drawing slows down, and the ElapsedGameTime that Draw receives is the game time covered by all the updates of that tick (worked example), so summing it measures draws per second of game time.
class FpsCounterComponent final : public DrawableGameComponent {
public:
explicit FpsCounterComponent(Game& game) : DrawableGameComponent(game) {}
void Draw(const GameTime& gameTime) override
{
elapsed_ += gameTime.getElapsedGameTimeProperty(); // TimeSpan has operator+=
++frames_;
if (elapsed_.getTotalSecondsProperty() >= 1.0) {
fps_ = static_cast<double>(frames_) / elapsed_.getTotalSecondsProperty();
frames_ = 0;
elapsed_ = TimeSpan::Zero;
}
spriteBatch_->Begin();
spriteBatch_->DrawString(*font_, "FPS: " + std::to_string(static_cast<int>(fps_)),
Vector2(10.0f, 10.0f), Color::Yellow);
spriteBatch_->End();
}
protected:
void LoadContent() override
{
font_.emplace(getGameProperty().getContentProperty().Load<SpriteFont>("fonts/hud"));
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
}
void UnloadContent() override
{
spriteBatch_.reset();
font_.reset();
}
private:
std::optional<SpriteFont> font_; // SpriteFont has no default constructor
std::unique_ptr<SpriteBatch> spriteBatch_;
TimeSpan elapsed_ = TimeSpan::Zero;
int frames_ = 0;
double fps_ = 0.0;
};
class MyGame final : public Game {
public:
MyGame() : graphics_(this), fpsCounter_(*this)
{
fpsCounter_.setDrawOrderProperty(1000); // after every lower DrawOrder
getComponentsProperty().Add(&fpsCounter_); // raw pointer: borrowed, not owned
}
~MyGame() override
{
// Unregister while both objects are alive; Remove() returns [[nodiscard]] bool.
static_cast<void>(getComponentsProperty().Remove(&fpsCounter_));
}
private:
GraphicsDeviceManager graphics_;
FpsCounterComponent fpsCounter_;
};
Three details are deliberate. The component is a plain member, and the derived class's members are destroyed before the Game base and its Components member, so being a member does not make it outlive the collection; the destructor's Remove() runs while both are alive and makes Game drop the component from its lists and unsubscribe its order-change handler. DrawOrder 1000 puts it after other components, and it is drawn on top of the game's own drawing only if the derived Draw calls Game::Draw last. And its UnloadContent() runs only when the game is explicitly disposed (game.Dispose() disposes every IDisposable component); destruction alone releases the members through their own destructors.
GameServiceContainer: type-keyed lookup without reflection
C#'s GameServiceContainer is a dictionary keyed by System.Type. C++ has no runtime reflection to build that from a provider object, so GameServiceContainer.hpp keeps a std::unordered_map<std::type_index, void*> keyed by typeid of the template argument, behind AddService<T>, GetService<T> and RemoveService<T> that delegate to non-template std::type_info overloads.
- The key is the template argument.
AddService<ISaveGameService>(&manager)first converts the pointer toISaveGameService*— adjusting it correctly even when the interface is not the first base — and stores that. A call without the explicit argument,AddService(&manager), deduces the concrete type as the key, andGetService<ISaveGameService>()then returns null. Always spell the interface. - The non-template overload trusts its caller.
AddService(typeid(IFoo), ptr)takes anyvoid*. Passing a concrete-object pointer converts it tovoid*without the base adjustment, so a laterGetService<IFoo>()casts the wrong address wheneverIFoois not at offset zero. The container cannot check assignability (FNA'sIsAssignableFromcheck is omitted, as the source notes). - No overwrite. A null provider throws
System::ArgumentNullExceptionand a second registration for the same typeSystem::ArgumentException, the types XNA throws; there is no last-write-wins.RemoveService<T>()first, then register the replacement. Removing an absent type is a silent no-op;GetServiceof an absent type returns null. - No ownership. The container stores borrowed pointers and never clears them: a service object must outlive its registration or be removed before it is destroyed. Copy construction and copy assignment are deleted ("services are registered by pointer identity") while moves are defaulted — a container can be relocated but never silently duplicated into a second set of pointers; all four are marked
CNAEXT.
GraphicsDeviceManager registers itself under IGraphicsDeviceManager and IGraphicsDeviceService, so GetService<GraphicsDeviceManager>() returns null; a second manager on the same game throws System::ArgumentException. Game caches the looked-up manager and device-service pointers during initialization and never refreshes them, so replacing those registrations after Run() has started has no effect on the loop (runtime module internals).
Registering and consuming a service
struct ISaveGameService {
virtual void Save(const std::string& slot) = 0;
virtual ~ISaveGameService() = default;
};
class SaveGameManager final : public ISaveGameService {
public:
void Save(const std::string& slot) override { lastSlot_ = slot; }
private:
std::string lastSlot_;
};
class ServiceGame final : public Game {
public:
ServiceGame() : graphics_(this)
{
// Name the interface: AddService(&saves_) would deduce SaveGameManager as the key.
getServicesProperty().AddService<ISaveGameService>(&saves_);
}
~ServiceGame() override
{
getServicesProperty().RemoveService<ISaveGameService>(); // the container owns nothing
}
protected:
void Update(GameTime& gameTime) override
{
if (auto* saves = getServicesProperty().GetService<ISaveGameService>())
saves->Save("slot1");
Game::Update(gameTime);
}
private:
GraphicsDeviceManager graphics_;
SaveGameManager saves_;
};
Any component can reach the same service through getGameProperty().getServicesProperty().GetService<ISaveGameService>() without a constructor parameter. The shape follows the container's own tests.
Evidence and limits
Read at this snapshot from the component, collection and container sources and headers linked above and from Game.cpp (CategorizeComponent, SortUpdateable, SortDrawable, OnComponentAdded, OnComponentRemoved). GameServiceContainerTests.cpp has 13 cases, including correct pointer adjustment for a non-primary-base interface, independent lookups of two interfaces on one object and non-ownership; GameComponentCollectionTests.cpp has 34, covering events, bounds, duplicates, range-for iteration and shared ownership, and ComponentLifecycleCompatibilityTests.cpp has 9 that pin the change hooks, repeated explicit disposal, removal before Disposed, drawable unloading and the ownership kept through callbacks. GameComponentTests.cpp and DrawableGameComponentTests.cpp still contain no tests, so CompareTo, the ordering rules and the one-time load are source readings. None of the tests was executed for this page. XNA 4.0 behaviour is quoted from a disassembly of its Microsoft.Xna.Framework.Game assembly. Both examples were syntax-checked as complete translation units with g++ -std=c++23 -fsyntax-only -Wall -Wextra against the headers of the earlier evidence commit, whose declarations used here are unchanged at 3b43bd86 (a single-renderer and an SDL3 platform configuration definition, Sharp Runtime next @ 41b918c9), without warnings; they were not built or run.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Game loop: GameComponent and DrawableGameComponent · Tutorial 47: GameComponent and DrawableGameComponent
- Architecture
- Runtime lifecycle
- Maintainer workflow
- Case study: Game component lifetime · Thread and callback map
- Tests and validation
- Test architecture