feat: Stride Gamebuilder as an alternative for .NET IOC startup
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)
{
var services = gameBuilder.Game.Services;
var scriptSystem = new ScriptSystem(services);
var sceneSystem = new SceneSystem(services);
var audioSystem = new AudioSystem(services);
var gameFontSystem = new GameFontSystem(services);
var spriteAnimationSystem = new SpriteAnimationSystem(services);
var debugTextSystem = new DebugTextSystem(services);
var gameProfilingSystem = new GameProfilingSystem(services);
var inputSystem = new InputSystem(services);
var effectSystem = new EffectSystem(services);
// registers itself as a service
var streamingManager = new StreamingManager(services);
gameBuilder
.AddGameSystem(scriptSystem)
.AddGameSystem(sceneSystem)
.AddGameSystem(audioSystem)
.AddGameSystem(gameFontSystem)
.AddGameSystem(spriteAnimationSystem)
.AddGameSystem(debugTextSystem)
.AddGameSystem(gameProfilingSystem)
.AddGameSystem(inputSystem)
.AddGameSystem(effectSystem)
.AddGameSystem(streamingManager);
// add services
gameBuilder
.AddService(scriptSystem)
.AddService(sceneSystem)
.AddService(spriteAnimationSystem)
.AddService(debugTextSystem)
.AddService(gameProfilingSystem)
.AddService(inputSystem)
.AddService(effectSystem)
.AddService(inputSystem.Manager)
.AddService(audioSystem)
.AddService<IAudioEngineProvider>(audioSystem)
.AddService(gameFontSystem)
.AddService(gameFontSystem.FontSystem)
.AddService<IFontFactory>(gameFontSystem.FontSystem);
return gameBuilder;
}
public static IGameBuilder UseDefaultGameSystemsDI(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; // for SharpHookInputSource and HIDDeviceInputSource
using GameBuilderTest;
using Stride.Core.Diagnostics;
using Stride.Core.IO;
using Stride.Core.Serialization.Contents;
using Stride.Engine;
using Stride.Engine.Builder;
using Stride.Engine.Design;
using Stride.Games;
using Stride.Rendering;
var gameBuilder = GameBuilder.Create();
gameBuilder.UseDefaultGameSystemsDI()
.UseStrideInput()
.UseStrideFontSystem()
.UseDefaultDb()
.UseDefaultContentManager()
.SetGameContext(GameContextFactory.NewGameContextSDL()) // This allows users to still use the default context behaviour
.AddStrideInputSource(new SharpHookInputSource()) // These are custom inputs I made
.AddStrideInputSource(new HIDDeviceInputSource()) // These are custom inputs I made
.AddLogListener(new ConsoleLogListener()); // Add loggers easily through here using Strides logging system not .NETs
var game = gameBuilder.Build();
var content = game.Services.GetService<IContentManager>();
var settings = content.Load<GameSettings>("GameSettings");
var sceneSystem = game.Services.GetService<SceneSystem>();
sceneSystem.InitialSceneUrl = settings.DefaultSceneUrl;
sceneSystem.InitialGraphicsCompositorUrl = settings.DefaultGraphicsCompositorUrl;
var fileProvider = game.Services.GetService<IDatabaseFileProviderService>().FileProvider;
game.Services.GetService<EffectSystem>()
.CreateDefaultEffectCompiler(fileProvider);
game.Run();
Related Issue
Original runtime management change
https://github.com/stride3d/stride/pull/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
https://github.com/stride3d/stride/pull/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
https://github.com/stride3d/stride/pull/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)
- [x] 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.
- [x] I have built and run the editor to try this change out.
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.
I like this PR. future branch might work, or anything what would make this changes happen 🙂.
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.
@Doprez, do you want me to run a copilot review on this and see what it will spit out?
@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.
very nice!