Skip to content

Conversation

@Doprez
Copy link
Contributor

@Doprez Doprez commented May 28, 2025

PR Details

This PR adds the ability to completely customize the startup of a users game by using dependency injection.

This is a rethinking of my original work to try and make it at least a minimal breaking change so that GameStudio and existing projects can be upgraded gradually. This is not feature complete but I would like to build the new windowing API based on this structure assuming this is approved as a valid change.

I also took some small inspiration from how Avalonia decided on their own startup structure so there are some very small similarities.

Why didn't I use existing AppBuilders/HostBuilders?

I did look into this with the original starting point being based on #1841 but without changing the core Stride libraries it wasn't as useful out of the box. I did still abstract over the IServiceCollection in order to allow us to migrate over to that system in the future or allow users to add dependencies that exist in .NET already. The other issue with the IServiceCollection is that you can not add services at runtime like you can with Strides IServiceRegistry which would have been a major breaking change throughout everything.

What about existing classes inheriting or using Game?

This should be working exactly as it does today apart from how content and GameContext is loaded in the project and this is currently the main thing that could break projects if merged in. Thankfully most functionality was internal already so this shouldn't be an issue for 99% of users.

Why make the GameWindow public?

Honestly just looking for some feedback here but making it public allows users to properly set up the GameContext if they decide to use it with the new startup. This is not concrete as well since assuming this gets approved I would be changing how the GraphicsDevice sees the GameWindow as purely a position and size rather than a whole object.

Why not fully use the IOC PR like #1841

This is a less breaking change and allows the underlying code to be modified without the users noticing. My PR is meant to be the path of least resistance.

Example Startup code

Extensions that are used in the default Game class for a generic Stride games to run:

public static class GameBuildExtensions
{
    public static IGameBuilder UseDefaultGameSystems(this IGameBuilder gameBuilder)
    {
        gameBuilder
            .AddService<ScriptSystem>()
            .AddService<SceneSystem>()
            .AddService<SpriteAnimationSystem>()
            .AddService<DebugTextSystem>()
            .AddService<GameProfilingSystem>()
            .AddService<EffectSystem>()
            .AddService<StreamingManager>()
            .AddService<IAudioEngineProvider, AudioSystem>()
            .AddService<GameFontSystem>()
            .AddService<FontSystem>();

        return gameBuilder;
    }
}

Actual Startup code with custom inputs and logger:

using Doprez.Stride.Input;
using Stride.Core.Diagnostics;
using Stride.Engine.Builder;
using Stride.Games;

var gameBuilder = GameBuilder.Create();

gameBuilder.UseDefaultGameSystems()
    .UseDefaultInput()
    .UseDefaultDb()
    .UseDefaultContentManager()
    .SetGameContext(GameContextFactory.NewGameContextSDL())
    .AddInputSource(new HIDDeviceInputSource()) // custom source from Doprez.Stride.Input
    .AddLogListener(new ConsoleLogListener());

var game = gameBuilder.Build();

game.UseInitialSceneFromSettings()
   .UseInitialGraphicsCompositorFromSettings()
   .UseDefaultEffectCompiler();

game.Run();

Related Issue

Original runtime management change

#2404

This was my first attempt with the focus of only managing the GameWindow but quickly cascaded into a mess of breaking changes and turned into a mess of unmaintainable code. This went from "I want to change the window host" to "dear god everything is hard coded and broken after a single change"


Second attempt at startup changes

#2574

This was my better PR for the startup change but broke WAY too much to justify the cascading changes required to work with GameStudio.


modern IOC change

#1841

I really loved the idea of this one but it breaks so much with the core systems of Stride since they rely on Stride specific alternatives. For example Services and logs which would need to be updated throughout Strides libraries which is out of scope for my time.

In an ideal world I think this one is a good option but without more manpower/hours feels unrealistic for now.


Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@Kryptos-FR
Copy link
Member

Should we create a future branch?

I feel like this too much of a change to fit into a minor release. By having a separate branch we could investigate further changes without setting anything into stone yet.

@VaclavElias
Copy link
Contributor

I like this PR. future branch might work, or anything what would make this changes happen 🙂.

@Doprez
Copy link
Contributor Author

Doprez commented May 28, 2025

I would be be happy with that. I am very hesitant to make some of these APIs public in a release since they were internal due to their unfinished state(specifically the windowing). My main goal of this PR was to get the other core contributors attention to see if there was anything absolutely atrocious that I did 😆

I think one of the benefits here would be for the GameStudio and how it instantiates the scene Game. It should be able to remove a lot of the cascading that exists once I get the windowing changes in.

@VaclavElias
Copy link
Contributor

@Doprez, do you want me to run a copilot review on this and see what it will spit out?

@Doprez
Copy link
Contributor Author

Doprez commented May 29, 2025

@Doprez, do you want me to run a copilot review on this and see what it will spit out?

Sure, doesnt hurt to have some extra hints.

@VaclavElias VaclavElias requested a review from Copilot May 29, 2025 13:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a customizable game startup using dependency injection to allow gradual upgrading of existing projects while exposing a new game windowing API. Key changes include refactoring GameBase and related classes to derive their GamePlatform from the GameContext, updating service registrations throughout, and adding a new GameBuilder for a streamlined startup process.

Reviewed Changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sources/engine/Stride.Games/GameBase.cs Refactored game platform and context handling, updated property access modifiers, and added helper methods for GameContext initialization.
sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs Updated constructor to accept GameContext instead of GameBase.
sources/engine/Stride.Games/Android/GamePlatformAndroid.cs Updated constructor to accept GameContext instead of GameBase.
sources/engine/Stride.Engine/Engine/Game.cs Modified Game constructor and initialization to use provided or default GameContext.
sources/engine/Stride.Engine/Engine/Builder/*.cs Added and updated game builder components to support DI and new service registrations.
sources/editor/* Updated editor game startup code to integrate with the new startup and GameContext management.
sources/core/* Updated service registration methods and file provider services to support context-based initialization.
sources/buildengine/* Adjusted DatabaseFileProvider service to allow value updates.
sources/Directory.Packages.props Added a new package reference for Microsoft.Extensions.Hosting.
Comments suppressed due to low confidence (1)

sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs:120

  • [nitpick] Consider renaming 'dataBase' to a more descriptive and properly cased variable name such as 'databaseProvider' for clarity.
var dataBase = Game.Services.GetService<IDatabaseFileProviderService>();

@VaclavElias VaclavElias requested a review from Copilot May 29, 2025 14:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Introduce a new dependency-injection–driven startup pipeline for Stride games, enabling full customization of game initialization while minimizing breaking changes.

  • Refactored GameBase to use a GameContext, support swapping contexts at runtime, and expose essential members as protected.
  • Added GameBuilder, MinimalGame, and extension methods to configure services, game systems, input sources, and logging via IServiceCollection.
  • Extended ServiceRegistry and core interfaces to allow explicit service registration by type and made database file provider mutable.

Reviewed Changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
sources/engine/Stride.Games/GameBase.cs Refactored startup flow to use GameContext, added SetGameContext/EnsureGameContextIsSet, and made key properties protected.
sources/engine/Stride.Games/Desktop/GamePlatformDesktop.cs Updated constructor to accept GameContext instead of GameBase.
sources/engine/Stride.Games/Android/GamePlatformAndroid.cs Updated constructor to accept GameContext instead of GameBase.
sources/engine/Stride.Engine/Stride.Engine.csproj Added Microsoft.Extensions.Hosting package reference.
sources/engine/Stride.Engine/Engine/Game.cs Modified Game constructor, default context, input source initialization, and removed direct casts for database provider.
sources/engine/Stride.Engine/Engine/Design/GameSettings.cs Removed unused System and Stride.Graphics usings.
sources/engine/Stride.Engine/Engine/Builder/MinimalGame.cs Introduced hosted minimal game class with DI and hosted service implementation.
sources/engine/Stride.Engine/Engine/Builder/IGameBuilder.cs Defined IGameBuilder interface to expose builder state.
sources/engine/Stride.Engine/Engine/Builder/GameBuilderExtensions.cs Provided extension methods to register systems, services, logging, input, DB, and content for a game.
sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs Implemented the GameBuilder class with service wiring, system registration, context setup, and build logic.
sources/editor/Stride.Editor/Preview/PreviewGame.cs Updated constructor to accept GameContext.
sources/editor/Stride.Editor/Preview/GameStudioPreviewService.cs Passed GameContext into PreviewGame.
sources/editor/Stride.Editor/Engine/EmbeddedGame.cs Updated EmbeddedGame constructor to accept GameContext.
sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs Added overload constructor accepting GameContext.
sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EntityHierarchyEditorGame.cs Updated constructor to forward GameContext.
sources/core/Stride.Core/ServiceRegistry.cs Added AddService(object, Type) overload to trigger ServiceAdded event.
sources/core/Stride.Core/IServiceRegistry.cs Declared new AddService(object, Type) interface member.
sources/core/Stride.Core.Serialization/IO/IDatabaseFileProviderService.cs Made FileProvider setter public for mutable DB plumbing.
sources/buildengine/Stride.Core.BuildEngine.Common/MicrothreadLocalDatabases.cs Implemented FileProvider setter on microthread-local provider.
sources/Directory.Packages.props Pin Microsoft.Extensions.Hosting version.
Comments suppressed due to low confidence (4)

sources/engine/Stride.Engine/Engine/Game.cs:447

  • The variables rawInputEnabled and context are not defined in this scope. You likely meant to reference the instance fields Context and a corresponding RawInputEnabled property. Rename and reference the correct fields or pass them into this method.
if (rawInputEnabled && context is GameContextWinforms gameContextWinforms)

sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs:18

  • The collection initializer [] is invalid C#. Use a concrete constructor such as new Dictionary<Type, object>();.
public Dictionary<Type, object> InternalServices { get; internal set; } = [];

sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs:24

  • The list initializer [] is invalid here. Replace with new List<LogListener>(); to correctly instantiate the collection.
public List<LogListener> LogListeners { get; internal set; } = [];

sources/engine/Stride.Engine/Engine/Builder/GameBuilder.cs:26

  • Invalid list initializer []; use new List<IInputSource>(); to create the collection instance.
public List<IInputSource> InputSources { get; internal set; } = [];

@Arc-huangjingtong
Copy link
Contributor

very nice!

Doprez and others added 23 commits December 25, 2025 17:25
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…LocalDatabases.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Doprez
Copy link
Contributor Author

Doprez commented Dec 26, 2025

Finally took some time to rebase and update this PR with the power of Christmas spirit based on your feedback @VaclavElias.

I left out the extra level of extensions from the second suggestion only because it felt like that may be more for the user to add and would create an additional extension for Stride to maintain. The only reason I consider this a concern is due to how GameSettings get loaded with the hard coded string. I dont think there is currently a way to avoid this string without a project settings file that would tell the asset compiler where the assets file for GameSettings is.

*I also updated the example in the PR description.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 28 comments.

Comment on lines +271 to 272
//internal IServiceRegistry Services { get; set; }

Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out line for Services property removal suggests this was part of the refactoring. Either remove this commented line entirely or add a TODO comment explaining why it's commented and when it should be removed. Leaving commented code without explanation reduces code clarity.

Suggested change
//internal IServiceRegistry Services { get; set; }

Copilot uses AI. Check for mistakes.
/// Gets the abstract window.
/// </summary>
/// <value>The window.</value>
[Obsolete("Use GameContext.GameWindow instead.")]
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obsolete attribute message should include information about when this will be removed or what version it's deprecated in, to help users plan their migration path.

Suggested change
[Obsolete("Use GameContext.GameWindow instead.")]
[Obsolete("Use GameContext.GameWindow instead. This property is obsolete and will be removed in a future version of Stride.")]

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +115
foreach (var service in InternalServices)
{
if (service.Key == typeof(IServiceRegistry) || service.Key == typeof(IServiceProvider))
continue;

try
{
if (service.Value == null)
{
var instance = provider.GetService(service.Key);

if(instance == null)
{
//check if the type is inherited from another instance in the services.
foreach (var kvp in InternalServices)
{
if (kvp.Key.IsAssignableFrom(service.Key) && kvp.Value != null)
{
instance = provider.GetService(kvp.Key);
if(instance is not null)
break;
}
}
}

_log.Info($"Registering service {service.Key.Name}.");
Game.Services.AddService(instance, service.Key);
InternalServices[service.Key] = instance;
}
else
{
_log.Info($"Registering service {service.Key.Name}.");
Game.Services.AddService(service.Value, service.Key);
}
}
catch (Exception ex)
{
// TODO: check if service is already registered first.
_log.Error($"Failed to register service {service.Key.Name}.\n\n", ex);
}
}
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GameBuilder.Build method contains complex service resolution logic with nested loops and multiple conditionals (lines 75-115). This could be refactored into smaller, more testable helper methods like ResolveAndRegisterService or FindInheritedService to improve maintainability and clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +96
Services.AddService<IStreamingManager>(this);
Services.AddService<ITexturesStreamingProvider>(this);
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StreamingManager is no longer registering itself as a concrete type in the service registry. This could break existing code that retrieves StreamingManager directly rather than through the IStreamingManager or ITexturesStreamingProvider interfaces. Consider keeping the self-registration for backward compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +22
using System;
using Stride.Core.Mathematics;

namespace Stride.Games.Windowing;
public interface IGameWindow : IStrideSurface
{
public IntPtr WindowHandle { get; }
public Int2 Position { get; set; }

public string Title { get; set; }

public WindowState State { get; set; }
}

public enum WindowState
{
Normal,
Minimized,
Maximized,
FullscreenWindowed,
FullscreenExclusive,
}
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new IStrideSurface and IGameWindow interfaces and WindowState enum lack XML documentation. Since these are new public APIs that users will interact with when customizing game windows, they should include comprehensive documentation explaining their purpose, usage, and how they relate to the new builder pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +38
/// Saves an asset at a specific URL.
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="asset">The asset.</param>
/// <param name="storageType">The custom storage type to use. Use null as default.</param>
/// <exception cref="System.ArgumentNullException">
/// url
/// or
/// asset
/// </exception>
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Save method in IContentManager interface (lines 28-39) lacks implementation details in the interface documentation. While the XML doc describes the parameters, it should also document expected behavior, error conditions, and any threading considerations for saving assets.

Suggested change
/// Saves an asset at a specific URL.
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="asset">The asset.</param>
/// <param name="storageType">The custom storage type to use. Use null as default.</param>
/// <exception cref="System.ArgumentNullException">
/// url
/// or
/// asset
/// </exception>
/// Saves an asset at a specific URL using the content manager's serializer.
/// </summary>
/// <param name="url">
/// The URL where the asset should be stored. Implementations typically interpret this as a virtual
/// or logical path that is resolved to an underlying storage location. If an asset already exists
/// at this URL, it is expected to be overwritten.
/// </param>
/// <param name="asset">
/// The asset instance to save. The object is serialized according to the content manager's
/// configuration and the specified <paramref name="storageType"/> (if any).
/// </param>
/// <param name="storageType">
/// The custom storage type to use for serialization, or <c>null</c> to use the default storage type.
/// Implementations may use this value to select a specific serializer, file format, or backing store.
/// </param>
/// <remarks>
/// <para>
/// This method performs a synchronous save operation and may involve disk or other I/O. Callers
/// should avoid invoking it on performance-critical threads (such as the main game loop) if the
/// underlying implementation performs blocking I/O.
/// </para>
/// <para>
/// Unless otherwise documented by a specific implementation, the <see cref="IContentManager"/>
/// interface does not guarantee thread-safety. If the same <see cref="IContentManager"/> instance
/// is accessed from multiple threads, callers are responsible for providing appropriate
/// synchronization when calling <see cref="Save(string, object, Type?)"/> concurrently with other
/// operations.
/// </para>
/// </remarks>
/// <exception cref="System.ArgumentNullException">
/// Thrown if <paramref name="url"/> or <paramref name="asset"/> is <c>null</c>.
/// </exception>
/// <exception cref="System.Exception">
/// Implementations may throw exceptions derived from <see cref="System.Exception"/> if saving fails,
/// for example due to serialization errors, invalid URLs, insufficient permissions, or I/O errors.
/// </exception>

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
/// Sets the game context.
/// </summary>
/// <param name="context"></param>
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SetGameContext method is added to the IGame interface but lacks XML documentation. This is a public API method that should be documented with details about when it can be called, what happens if context is null, and any exceptions that might be thrown.

Suggested change
/// Sets the game context.
/// </summary>
/// <param name="context"></param>
/// Sets or changes the <see cref="GameContext"/> used by this game instance.
/// </summary>
/// <param name="context">
/// The new game context to use. Implementations may not accept a <see langword="null"/> value
/// and can throw an <see cref="ArgumentNullException"/> in that case.
/// </param>
/// <remarks>
/// This method is typically called during initialization, before starting the main game loop,
/// or when switching to a different platform-specific game context. The exact behavior and the
/// supported call timing are implementation dependent.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="context"/> is <see langword="null"/> and the implementation does not support a null context.
/// </exception>

Copilot uses AI. Check for mistakes.
/// <summary>
/// Resets the <see cref="Sources"/> collection back to it's default values
/// </summary>
[Obsolete("This should be managed manually instead by using the Sources collection")]
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InputManager.ResetSources() method is marked as obsolete, but the obsolete message says "This should be managed manually instead by using the Sources collection" without providing a clear migration example. Consider adding a code example or more detailed guidance in the obsolete message to help users migrate.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +113
// TODO: check if service is already registered first.
_log.Error($"Failed to register service {service.Key.Name}.\n\n", ex);
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment on line 112 indicates incomplete functionality for checking if a service is already registered. This could lead to exceptions being logged when services are legitimately already registered. Consider implementing this check before attempting registration to avoid unnecessary error logging.

Copilot uses AI. Check for mistakes.
/// Gets the main window.
/// </summary>
/// <value>The main window.</value>
[Obsolete("Use GameContext.MainWindow instead. This property will be removed in a future version.")]
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obsolete attribute is missing information about which version this will be removed in. Consider adding a version number or deprecation timeline to help users plan their migration, for example: "Use GameContext.MainWindow instead. This property will be removed in version X.X."

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Feature Requests

Development

Successfully merging this pull request may close these issues.

4 participants