We have improved the async/await support in WaveEngine 2.4.0 by replacing our Dispatcher and TaskScheduler services with a custom implementation of .Net TaskScheduler and creating some useful helpers.
Porting project
The biggest change is the substitution of Dispatcher and TaskScheduler services. The new way to execute code in the foreground or background threads is by using the new helper methods from WaveForegroundTask and WaveBackgroundTask static classes.
To port current projects using the new classes, we need to change the following:
- If you are using WaveServices.Dispatcher, you must replace it to use WaveForegroundTask.
1 2 3 4 |
//From WaveServices.Dispatcher.RunOnWaveThread(() => ...) //to WaveForegroundTask.Run(() => ...) |
- If you are using WaveServices.TaskScheduler, you must replace it to use WaveBackgroundTask.
1 2 3 4 |
//From WaveServices.TaskScheduler.CreateTask(() => ...) //to WaveBackgroundTask.Run(() => ...) |
Both new ways to execute code return a .Net Task, which integrates easily with async .Net code.
Foreground and Background threads
Before we start to review the new included features, it is good to know these details:
- When a task is executed in the Foreground thread, like with the Dispatcher service, it is executed in the next update cycle.
- With the Background thread, the tasks are executed sequentially in a specific thread that can access the graphic resources. This allows us, for example, to load textures. The old TaskScheduler executes the code in a .Net thread and without taking concurrency issues into account when accessing the graphic context. In comparison, this new service ensures that each task is executed in the same thread and sequentially.
ConfigureWaveAwait
One useful helper introduced in the new WaveEngine version is the ConfigureWaveAwait extension method.
It allows us to configure how the await continuation is handled. Just as the .Net ConfigureAwait extension method allows us to configure if we want that the continuation of the await is executed on the captured context, the method ConfigureWaveAwait allows us to configure where the await continuation is executed: in the Foreground or the Background thread.
A simple way to test this new method is to load an image from a URL into a sprite:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public async Task LoadImageFromUrlInSprite(string imageUrl, Sprite spriteComponent) { var texture = await this.LoadTextureAsync(imageUrl).ConfigureWaveAwait(WaveTaskContinueOn.Foreground); // This code is executed in the foreground thread. spriteComponent.Texture = texture; } private async Task<Texture2D> LoadTextureAsync(string imageUrl) { // The method LoadImageStreamAsync load the image stream from the imageUrl asynchronously using (var imageStreamResult = await ImagesHelper.LoadImageStreamAsync(imageUrl).ConfigureWaveAwait(WaveTaskContinueOn.Background)) { // This code is executed in the background thread. var texture = Texture2D.FromFile(this.RenderManager.GraphicsDevice, imageStreamResult.Stream); return texture; } } |
The WaveTaskContinueOn enum also includes other options. The Default option acts as if the ConfigureWaveAwait has not been used, and the ThreadPool option acts as if the ConfigureAwait(false) from .Net has been used.
Async/await and GameActions
With the inclusion of GameActions, WaveEngine has acquired an excellent way to define a flow of actions that is integrated perfectly with the common scenarios in a game (like animations, play sounds, wait for a condition, etc.).
In this release, we have included the AsTask extension method that simplifies the way that .Net Tasks and GameAction interact with each other.
A simple sample using all of these new features:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
private async void AsyncSceneLoad(object sender, GestureEventArgs e) { var sceneTask = WaveBackgroundTask.Run(() => { // We load a heavy scene in the background thread var heavyScene = new HeavyLoadScene(); heavyScene.Initialize(WaveServices.GraphicsDevice); return heavyScene; }); // Starts a loading animation using GameActions that ends when the sceneTask is completed await this.CreateLoopGameActionUntil(() => this.LoadingAnimation(this.sceneLoadButton), () => sceneTask.IsCompleted).AsTask(); // When the sceneTask is completed, we await it to get the scene instance, // and configure the await to navigate to the scene in the foreground thread. var scene = await sceneTask.ConfigureWaveAwait(WaveTaskContinueOn.Foreground); ScreenContext screenContext = new ScreenContext(scene); WaveServices.ScreenContextManager.To(screenContext); } public IGameAction LoadingAnimation(Entity entity) { var transform = entity.FindComponent<Transform2D>(); return new FloatAnimationGameAction(entity, 0f, 2 * MathHelper.Pi, TimeSpan.FromMilliseconds(800), EaseFunction.None, val => { transform.LocalRotation = val; }); } |
I hope you find it useful when you want to use async code in your game. It is the first step and we will work to include new async APIs and features in next releases.