Skip to content

Commit a0214b6

Browse files
Revert code block fixes
1 parent 6ddec86 commit a0214b6

File tree

1 file changed

+37
-36
lines changed
  • articles/tutorials/advanced/2d_shaders/02_hot_reload

1 file changed

+37
-36
lines changed

articles/tutorials/advanced/2d_shaders/02_hot_reload/index.md

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,42 @@ Our snake-like game already has a shader effect and we can use it to validate th
3333
### MSBuild Targets
3434

3535
The existing automatic shader compilation is happening because the `DungeonSlime.csproj` file is referencing the `MonoGame.Content.Builder.Task` Nuget package.
36-
[!code-xml](./snippets/DungeonSlime.csproj?highlight=4)
36+
37+
[!code-xml[](./snippets/DungeonSlime.csproj?highlight=4)]
3738

3839
Nuget packages can add custom build behaviours and the `MonoGame.Content.Builder.Task` package is adding a step to the game's build that runs the MonoGame Content Builder tool. These sorts of build extensions use a conventional `.prop` and `.target` file system. If you are interested, you can learn more about how Nuget packages may extend MSBuild systems on Microsoft's [documentation website](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild?view=vs-2022). For reference, [this](https://github.com/MonoGame/MonoGame/blob/develop/Tools/MonoGame.Content.Builder.Task/MonoGame.Content.Builder.Task.targets#L172) is the `.targets` file for the `MonoGame.Content.Builder.Task`.
3940

4041
This line defines a new MSBuild step, called `IncludeContent`:
4142

42-
[!code-xml](./snippets/snippet-2-01.xml)
43+
[!code-xml[](./snippets/snippet-2-01.xml)]
4344

4445
You can learn more about what all the attributes do in MSBuild. Of particular note, the `BeforeTargets` attribute causes MSBuild to run the `IncludeContent` target before the `BeforeCompile` target is run, which is a standard target in the dotnet sdk.
4546

4647
The `IncludeContent` target can run manually by invoking `dotnet build` by hand. In VSCode, open the embedded terminal to the _DungeonSlime_ project folder, and run the following command:
4748

48-
[!code-sh](./snippets/snippet-2-02.sh)
49+
[!code-sh[](./snippets/snippet-2-02.sh)]
4950

5051
You should see log output indicating that the content for the _DungeonSlime_ game was built.
5152

5253
### Dotnet Watch
5354

5455
There is a tool called [`dotnet watch`](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch) that comes with the standard installation of `dotnet`. Normally, `dotnet watch` is used to watch for changes to `.cs` code files, recompile, and reload those changes into a program without restarting the program. You can try out `dotnet watch`'s normal behaviour by opening VSCode's embedded terminal to the _DungeonSlime_ project, and running the following command. The game should start normally:
5556

56-
[!code-sh](./snippets/snippet-2-03.sh)
57+
[!code-sh[](./snippets/snippet-2-03.sh)]
5758

5859
> [!Tip]
5960
> Use the `ctrl` + `c` key at the same time to quit the `dotnet watch` terminal process.
6061
6162
Then, comment out the `Clear()` function call in the title screen's `Draw()` method. Save the file, and you should see the title screen immediately stop clearing the background on each frame. If you restore the line and save again, the scene will start clearing the background again:
6263

63-
[!code-csharp](./snippets/snippet-2-04.cs?highlight=3)
64+
[!code-csharp[](./snippets/snippet-2-04.cs?highlight=3)]
6465

6566
In our case, we do not want to recompile `.cs` files, but rather `.fx` files. First, `dotnet watch` can be configured to execute any MSBuild target rather than a recompile code. The following command uses the existing target provided by the `MonoGame.Content.Builder.Task`.
6667

6768
> [!Tip]
6869
> All arguments passed after the `--` characters are passed to the `build` command itself, not `dotnet watch`:
6970
70-
[!code-sh](./snippets/snippet-2-05.sh)
71+
[!code-sh[](./snippets/snippet-2-05.sh)]
7172

7273
Now, when you change a _`.fx`_ file, all of the content files are rebuilt into `.xnb` files.
7374

@@ -76,51 +77,51 @@ However, the `.xnb` files are not being copied from the `Content/bin` folder to
7677
The existing `MonoGame.Content.Builder.Task` system knows what the files are, so we can re-use properties defined in the MonoGame package.
7778
Add this `<Target>` block to your `.csproj` file:
7879

79-
[!code-xml](./snippets/snippet-2-06.xml)
80+
[!code-xml[](./snippets/snippet-2-06.xml)]
8081

8182
Now, instead of calling the `IncludeContent` target directly, change your terminal command to invoke the new `BuildAndCopyContent` target:
8283

83-
[!code-sh](./snippets/snippet-2-07.sh)
84+
[!code-sh[](./snippets/snippet-2-07.sh)]
8485

8586
If you delete the `DungeonSlime/bin/Debug/net8.0/Content` folder, make an edit to a `.cs` file and save, you should see the `DungeonSlime/bin/Debug/net8.0/Content` folder be restored.
8687

8788
The next step is to only invoke the target when `.fx` files are edited instead of `.cs` files. These settings can be configured with custom MSBuild item configurations. Open the `DungeonSlime.csproj` file and add this `<ItemGroup>` to specify configuration settings:
8889

89-
[!code-xml](./snippets/snippet-2-08.xml)
90+
[!code-xml[](./snippets/snippet-2-08.xml)]
9091

9192
Now when you re-run the command from earlier, it will only run the `IncludeContent` target when `.fx` files have been changed. All edits to `.cs` files are ignored. Try adding a blank line to the `grayscaleEffect.fx` file, and notice the `dotnet watch` process re-build the content.
9293

9394
However, if you would like to use `dotnet watch` for anything else in your workflow, then the configuration settings are too aggressive, because they will be applied _all_ invocations of `dotnet watch`. The `ItemGroup` can be optionally included when a certain condition is met. We will introduce a new MSBuild property called `OnlyWatchContentFiles`:
9495

95-
[!code-xml](./snippets/snippet-2-09.xml?highlight=1)
96+
[!code-xml[](./snippets/snippet-2-09.xml?highlight=1)]
9697

9798
And now when `dotnet watch` is invoked, it needs to specify the new parameter:
9899

99-
[!code-sh](./snippets/snippet-2-10.sh)
100+
[!code-sh[](./snippets/snippet-2-10.sh)]
100101

101102
The command is getting long and hard to type, and if we want to add more configuration, it will likely get even longer. Instead of invoking `dotnet watch` directly, it can be run as a new `<Target>` MSBuild step. Add this `<Target>` to your `DungeonSlime.csproj` file:
102103

103-
[!code-xml](./snippets/snippet-2-11.xml)
104+
[!code-xml[](./snippets/snippet-2-11.xml)]
104105

105106
And now from the terminal, run the following `dotnet build` command:
106107

107-
[!code-sh](./snippets/snippet-2-12.sh)
108+
[!code-sh[](./snippets/snippet-2-12.sh)]
108109

109110
We now have a way to dynamically recompile shaders on file changes and copy the `.xnb` files into the game folder! There are a few final adjustments to make to the configuration.
110111

111112
### DotNet Watch, but smarter
112113

113114
First, you may notice some odd characters in the log output after putting the `dotnet watch` inside the `WatchContent` target. This is because there are _emoji_ characters in the standard `dotnet watch` log stream, and some terminals do not understand how to display those, especially when streamed between `dotnet build`. To disable the _emoji_ characters, a `DOTNET_WATCH_SUPPRESS_EMOJIS` environment variable needs to be set:
114115

115-
[!code-xml](./snippets/snippet-2-13.xml?highlight=3)
116+
[!code-xml[](./snippets/snippet-2-13.xml?highlight=3)]
116117

117118
Next, the `IncludeContent` target is doing a little too much work for our use case. It is trying to make sure the MonoGame Content Builder tools are installed. For our use case, we can opt out of that check by disabling the existing `AutoRestoreMGCBTool` MSBuild property. It also makes sense to pass `--restore:false` as well so that Nuget packages are not restored on each content file change:
118119

119-
[!code-xml](./snippets/snippet-2-14.xml?highlight=2)
120+
[!code-xml[](./snippets/snippet-2-14.xml?highlight=2)]
120121

121122
To experiment with the system, re-run the following command:
122123

123-
[!code-sh](./snippets/snippet-2-15.sh)
124+
[!code-sh[](./snippets/snippet-2-15.sh)]
124125

125126
And then cause some sort of compiler-error in the `grayscaleEffect.fx` file, such as adding the line, `"tunafish"` to the top of the file. When you save it, you should see the terminal spit out an error containing information about the compilation failure,
126127

@@ -155,37 +156,37 @@ It is important to make a distinction between assets the game _expects_ to be re
155156

156157
Currently, the `grayscaleEffect.fx` is being loaded in the `GameScene` 's `LoadContent()` method like this:
157158

158-
[!code-csharp](./snippets/snippet-2-16.cs)
159+
[!code-csharp[](./snippets/snippet-2-16.cs)]
159160

160161
The `.Load()` function in the existing `ContentManager` is almost sufficient for our needs, but it returns a regular `Effect`, which has no understanding of the dynamic nature of the new content workflow.
161162

162163
1. Create a new _Content_ folder within the _MonoGameLibrary_ project, add a new file named `ContentManagerExtensions.cs`, and add the following code for the foundation of the new system:
163164

164-
[!code-csharp](./snippets/snippet-2-17.cs)
165+
[!code-csharp[](./snippets/snippet-2-17.cs)]
165166

166167
2. Within this Extension class we will add an [extension method](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) for the existing `MonoGame`'s `ContentManager` class and give it capabilities it does not currently have:
167168

168-
[!code-csharp](./snippets/snippet-2-18.cs)
169+
[!code-csharp[](./snippets/snippet-2-18.cs)]
169170

170171
3. This new `Watch` function is an opportunity to enhance how content is loaded. Use this new function to load the `_greyscaleEffect` effect in the `GameScene`:
171172

172-
[!code-csharp](./snippets/snippet-2-19.cs)
173+
[!code-csharp[](./snippets/snippet-2-19.cs)]
173174

174175
### The `WatchedAsset` class
175176

176177
The new system will need to keep track of additional information for each asset that we plan to be _hot-reloadable_. The data will live in a new class, `WatchedAsset<T>`.
177178

178179
1. Add a new file named `WatchedAsset.cs` in the _MonoGameLibrary/Content_ folder:
179180

180-
[!code-csharp](./snippets/snippet-2-20.cs)
181+
[!code-csharp[](./snippets/snippet-2-20.cs)]
181182

182183
2. The new `Watch` method should return a `WatchedAsset<T>` instead of the direct `Effect`:
183184

184-
[!code-csharp](./snippets/snippet-2-21.cs)
185+
[!code-csharp[](./snippets/snippet-2-21.cs)]
185186

186187
3. This will require that the type of `_greyscaleEffect` change to a `WatchedAsset<Effect>` instead of simply an `Effect`:
187188

188-
[!code-csharp](./snippets/snippet-2-22.cs)
189+
[!code-csharp[](./snippets/snippet-2-22.cs)]
189190

190191
> [!IMPORTANT]
191192
> This will cause a few compilation errors where the `_greyscaleEffect` is used throughout the rest of the `GameScene`.
@@ -197,15 +198,15 @@ It is time to extend the `ContentManagerExtensions` extension method that the ga
197198

198199
1. The new `TryRefresh` method will take a `WatchedAsset<T>` and update the inner `Asset` property _if_ the `.xnb` file is newer. The method returns `true` when the asset is reloaded, which will be useful later:
199200

200-
[!code-csharp](./snippets/snippet-2-23.cs)
201+
[!code-csharp[](./snippets/snippet-2-23.cs)]
201202

202203
2. At the top of the `GameScene.Update()` method, add the following line to opt into reloading the `_grayscaleEffect` asset:
203204

204-
[!code-csharp](./snippets/snippet-2-24.cs?highlight=3-4)
205+
[!code-csharp[](./snippets/snippet-2-24.cs?highlight=3-4)]
205206

206207
3. Now, when the `grayscaleEffect.fx` file is modified, the `dotnet watch` system will compile it to an `.xnb` file, copy it to the game's runtime folder, and then in the `Update()` loop, the `TryRefresh()` method will load the new effect and the new shader code will be running live in the game. Try it out by adding this temporary line right before the `return` statement in the `grayscaleEffect.fx` file:
207208

208-
[!code-hlsl](./snippets/snippet-2-25.hlsl?highlight=18-19)
209+
[!code-hlsl[](./snippets/snippet-2-25.hlsl?highlight=18-19)]
209210

210211
This video shows the effect changing.
211212

@@ -223,11 +224,11 @@ There is an edge case bug in the `TryRefresh()` function that checks if the `.xn
223224

224225
1. To start, Add the following function to the `ContentManagerExtensions` class:
225226

226-
[!code-csharp](./snippets/snippet-2-26.cs)
227+
[!code-csharp[](./snippets/snippet-2-26.cs)]
227228

228229
2. Then modify the `TryRefresh` function by returning early if the file is locked:
229230

230-
[!code-csharp](./snippets/snippet-2-27.cs?highlight=13)
231+
[!code-csharp[](./snippets/snippet-2-27.cs?highlight=13)]
231232

232233
### Access the old Asset on reload
233234

@@ -237,41 +238,41 @@ This will be more relevant in the next chapter. To handle this for now:
237238

238239
1. Modify the `TryRefresh` function to contain `out` parameter of the old asset. Change the method signature to the following:
239240

240-
[!code-csharp](./snippets/snippet-2-28.cs)
241+
[!code-csharp[](./snippets/snippet-2-28.cs)]
241242

242243
2. Before updating the `watchedAsset.Asset`, set the `oldAsset` as the previous in-memory asset:
243244

244-
[!code-csharp](./snippets/snippet-2-29.cs?highlight=16)
245+
[!code-csharp[](./snippets/snippet-2-29.cs?highlight=16)]
245246

246247
3. **Do not** forget that the place where the `grayscaleEffect` calls the `TryRefresh()` function will also need to include a no-op out variable:
247248

248-
[!code-csharp](./snippets/snippet-2-30.cs?highlight=4)
249+
[!code-csharp[](./snippets/snippet-2-30.cs?highlight=4)]
249250

250251
### Refresh Convenience Function
251252

252253
Finally, we need to address a subtle usability bug in the existing code. The `TryRefresh` function may `Unload` an asset if a new version is loaded. However, it is not obvious that the `ContentManager` instance doing the `Unload` operation is the same `ContentManager` instance that loaded the original asset in the first place. To solve this:
253254

254255
1. Add a `ContentManager` property to the `WatchedAsset<T>` class so that the asset itself knows which `ContentManager` is responsible for unloading old versions:
255256

256-
[!code-csharp](./snippets/snippet-2-31.cs)
257+
[!code-csharp[](./snippets/snippet-2-31.cs)]
257258

258259
2. Adjust the `WatchAsset` function of the `ContentManagerExtensions` class to fill in this new property:
259260

260-
[!code-csharp](./snippets/snippet-2-32.cs?highlight=9)
261+
[!code-csharp[](./snippets/snippet-2-32.cs?highlight=9)]
261262

262263
3. Then, in the `TryRefresh` function, a small assertion can be added to validate the `ContentManager` is the same:
263264

264-
[!code-csharp](./snippets/snippet-2-33.cs?highlight=6-7)
265+
[!code-csharp[](./snippets/snippet-2-33.cs?highlight=6-7)]
265266

266267
It is annoying to need use the `ContentManager` directly to call `TryRefresh` in the game loop. It would be easier to rely on the new `Owner` property:
267268

268269
4. Add the following method to the `WatchedAsset<T>` class:
269270

270-
[!code-csharp](./snippets/snippet-2-34.cs)
271+
[!code-csharp[](./snippets/snippet-2-34.cs)]
271272

272273
5. Finally, update the `GameScene` to use the new convenience method to refresh the `_grayscaleEffect`:
273274

274-
[!code-csharp](./snippets/snippet-2-35.cs?highlight=4)
275+
[!code-csharp[](./snippets/snippet-2-35.cs?highlight=4)]
275276

276277
## Conclusion
277278

0 commit comments

Comments
 (0)