Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions articles/getting_started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ By the end of this tutorial set, you will have a working project to build for yo

### 4. Advanced Topics

- [Debugging](logging.md)
- [Preparing for Consoles](preparing_for_consoles.md)
- [Using Development Nuget Packages](using_development_nuget_packages.md)
86 changes: 86 additions & 0 deletions articles/getting_started/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Logging
description: During development, debugging a MonoGame project is essentially no different than debugging any other .NET project for the most cases, although graphics related debugging or troubleshooting can require the use of external tools. Logging can be a very useful tool during this process.
---

When a game is under development, the developer usually needs some logging or tracing capabilities in order to troubleshoot or debug the game. In addition to some basic logs output by the MonoGame framework itself, developers will likely need additional logging while they work on their games.

## Enabling Console Window During Debugging

When a MonoGame solution is created via one of the available templates, the project file or files that host the main game window are set up to use `WinExe` as the `OutputType`. This simply means the application has its own window that will display the game contents, with no interaction with the console or shell the underlying operating system provides.<br/>
This means even when the game is launched from a console instance, it will not display any output in said console. A side effect of this is that when the game outputs logs to the console, it will not be visible to the developer outside the IDE integration.

The default platform project (`SolutionName.DesktopVK.csproj`, `SolutionName.WindowsDX12.csproj`, etc.) as it is created by the template would look like this:

[!code-xml[](./snippets/default_game_platform.csproj)]

Locate this line that sets the `OutputType` property in the project file:

[!code-xml[](./snippets/default_output_type.csproj)]

If we replace this line with a couple of conditional lines that set the `OutputType` property based on the build configuration, we can have a console window appear when debugging, while not having one when creating a release build.

[!code-xml[](./snippets/debug_output_type.csproj)]

With this change, the game will start interacting with the OS console:
* In the example above, when the game is run in `Debug` mode, a console window will appear before the actual game window, with all the logging visible to the developer.<br/>
In this more, if the game is launched from an existing console window, no new console window will be instantiated, and the interaction with the game will stay in said console instead.
* When the game is run in `Release` mode, the game window will be the only window that opens up, and no iteraction with the console will take place.

> [!NOTE]
> Leaving the `OutputType` as `WinExe` for a release build is generally a bad idea. This will cause the game to open up a console window in addition to the actual game window, which is generally not a wanted behavior for most games from the perspective of the player. This is why, the default behavior for any `Release` build should be to set it to `WinExe`.

## Adding Additional Logging

The developers can add logging/tracing capabilities to their games using a number of open source libraries that are widely available across the .NET ecosystem, or by building their custom logging implementations.
One easy way of having basic logging/tracing facilities in your game would be to rely on the standard methods in the `System.Diagnostics` namespace that comes with the .NET runtime as part of the base class library. The example below shows how this can be done:

[!code-csharp[](./snippets/debug_logging_simple.cs)]

In the example above, we're using this method to log information: `Debug.WriteLine()`<br/>
We could also use this method to have a similar result: `Trace.TraceInformation()`

But it is important to know the difference between the methods on the `Debug` and `Trace` classes:
* The methods on the `Debug` class will not be compiled into a Release build. This means, logs coming through these methods will not be output in a `Release` build, and all `Debug.Write()`, `Debug.WriteLine()` and similar calls will be stripped from the final executable, which makes them a good way of having logs when working on your game.
* The methods on the `Trace` class will be compiled into ***both** `Debug` and `Release` builds*, and that will allow you to have logs in the games you have shipped.

However, simply calling these methods will not be enough to actually display these log entries in the console window you enable in your MonoGame project through the changes in the project file. By default, the output of these methods will be directed to the output of the IDE you're using for development (e.g. Visual Studio), but they will not be directed to the console window.
In order to have them displayed in a console window, you will need to register a custom `TraceListener` in your game. The default `Program.cs` file for a MonoGame project doesn't include this, but it's very easy to add. This is how a default `Program.cs` file looks like:

[!code-csharp[](./snippets/default_program.cs)]

Using the example below, we will now register a `ConsoleTraceListener` in the `Program.cs` to direct the output of the logging methods to the console window:

[!code-csharp[](./snippets/program_with_consoletracelistener.cs)]

Once this is done, any logs you write with methods like `Debug.WriteLine()`, `Trace.TraceInformation()`, `Trace.TraceError()`, etc. will be visible in the console window, as long as you are running the game in the `Debug` mode.

> [!NOTE]
> The code example above uses top-level statements which is the default for MonoGame project templates. If you are using an older template, you might need to add the code to the `Main` method of your `Program.cs` file instead.

> [!WARNING]
> Having logs in hot-paths like the `Update()` method will generate a significant overhead and will decrease your game's performance, in addition to causing pressure on the garbage collector, which in turn can end up causing stutter.
>
> Thus, make sure to add logging in the relevant methods that only get called when certain things happen in-game.
> And for the same reason, always prefer using `Debug.WriteLine()` over `Trace.TraceInformation()` unless you actually need that particular log in the release builds.
>
> Because even if there is no console window to direct these logs to, `Trace.TraceInformation()` and similar methods will still incur a performance penalty in the release builds.

## Advanced Logging

So far we have only considered a basic logging scenario where the logs will be visible in the console window. This is also why we rely exclusively on `Debug.WriteLine()`, since the console window is not visible in the Release builds.
But there can be scenarios when the developers might need more advanced logging capabilities for their games that are already shipped. For example, we may want to write a log file when the game crashes with an exception, which can be used by the players to report the issue to us.

For this purpose, we can register a `TextWriterTraceListener` or a custom other trace listener implementation that suits our needs. Here's how we can modify the `Program.cs` file to write logs to a file on the disk:

[!code-csharp[](./snippets/program_with_textwritertracelistener.cs)]

In this example, any exception that is not handled in the game itself through a `try/catch` block bubbles up to the top-level `Main` method where it's caught and logged to a file on the disk. In addition to that, any log entry we created via `Trace.TraceError()` in game will also be written to this same log file.<br/>
We can then ask players to send us these log files if they encounter crashes during gameplay.

Going even further, we can even build a custom trace listener that inherits from `System.Diagnostics.TraceListener` to forward error logs to an API, which can be useful for having an overview of the bugs your game is encountering in real-time.

> [!WARNING]
> Setting `Trace.AutoFlush` to `true` will make sure Debug and Trace logs to be flushed to disk before the game crashes, but it will also turn the `Debug.WriteLine()`, `Trace.TraceWarning()` and similar calls into blocking calls, impacting the game's performance. This is good enough for Debug logs, or for crash logs in a Release build. But if we have further trace logging active in-game, this can impact performance.
>
> In that scenario, we should ideally avoid this, and implement our own trace listener to perform async writes to disk or to an API in a background thread.
14 changes: 14 additions & 0 deletions articles/getting_started/snippets/debug_logging_simple.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
protected void ConnectToHost()
{
Debug.WriteLine($"{DateTime.UtcNow:s}::User {_user.Id} is connecting to host...");

var connectionResult = _networkService.ConnectToHost(_user);
if (connectionResult.State == ConnectionState.Success)
{
Debug.WriteLine($"{DateTime.UtcNow:s}::User connected to host {connectionResult.Host}.");
}
else
{
Debug.WriteLine($"{DateTime.UtcNow:s}::User failed to connect to host {connectionResult.Host}.\nConnection state: {connectionResult.State}, Exception: {connectionResult.Exception}");
}
}
2 changes: 2 additions & 0 deletions articles/getting_started/snippets/debug_output_type.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<OutputType Condition="'$(Configuration)' == 'Debug'">Exe</OutputType>
<OutputType Condition="'$(Configuration)' != 'Debug'">WinExe</OutputType>
33 changes: 33 additions & 0 deletions articles/getting_started/snippets/default_game_platform.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<MonoGamePlatform>DesktopVK</MonoGamePlatform>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="Icon.ico" />
<None Remove="Icon.bmp" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico">
<LogicalName>Icon.ico</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Icon.bmp">
<LogicalName>Icon.bmp</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.Native" Version="3.8.5" />
<PackageReference Include="MonoGame.Runtime.Windows.Vulkan" Version="3.8.5" />
<PackageReference Include="MonoGame.Runtime.Mac.Vulkan" Version="3.8.5" />
<PackageReference Include="MonoGame.Runtime.Linux.Vulkan" Version="3.8.5" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<OutputType>WinExe</OutputType>
4 changes: 4 additions & 0 deletions articles/getting_started/snippets/default_program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using FooBar.Game;

using var game = new GameClass();
game.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Diagnostics;
using FooBar.Game;

Trace.Listeners.Add(new ConsoleTraceListener());

using var game = new GameClass();
game.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using FooBar.Game;

namespace FooBar;
public class Program
{
public static void Main(string[] args)
{
Trace.Listeners.Add(new ConsoleTraceListener());
Trace.Listeners.Add(new TextWriterTraceListener($"FooBar_CrashLog_{DateTime.UtcNow:yyyy-MM-dd_HH-mm-ss}.log")
{
Name = "CrashLogger",
Filter = new EventTypeFilter(SourceLevels.Critical | SourceLevels.Error),
});
Trace.AutoFlush = true;

// Catch exceptions on the main thread.
AppDomain.CurrentDomain.UnhandledException += (sender, exArgs) =>
{
var ex = exArgs.ExceptionObject as Exception;
Trace.TraceError($"Unhandled Exception from sender: {sender}\nException: {ex?.Message}\n{ex?.StackTrace}");
};

// Catch exceptions from background tasks/threads.
TaskScheduler.UnobservedTaskException += (sender, exArgs) =>
{
Trace.TraceError($"Unobserved Task Exception from sender: {sender}\nException: {exArgs.Exception.Message}\n{exArgs.Exception.StackTrace}");
};

using var game = new GameClass();
game.Run();
}
}
6 changes: 5 additions & 1 deletion articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ items:
href: getting_to_know/howto/input/index.md
- name: Advanced Topics
items:
- name: Debugging
items:
- name: Logging
href: getting_started/logging.md
- name: Packaging
href: getting_started/packaging_games.md
- name: Preparing for consoles
Expand Down Expand Up @@ -187,7 +191,7 @@ items:
- name: "09: Shadow Effect"
href: tutorials/advanced/2d_shaders/09_shadows_effect/index.md
- name: "10: Next Steps"
href: tutorials/advanced/2d_shaders/10_next_steps/index.md
href: tutorials/advanced/2d_shaders/10_next_steps/index.md
- name: Console Access
href: console_access.md
- name: Help and Support
Expand Down
4 changes: 4 additions & 0 deletions pdf/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ items:
href: ../../articles/getting_to_know/howto/input/index.md
- name: Advanced Topics
items:
- name: Debugging
items:
- name: Logging
href: ../../articles/getting_started/debugging.md
- name: Packaging
href: ../../articles/getting_started/packaging_games.md
- name: Preparing for consoles
Expand Down
12 changes: 11 additions & 1 deletion serve.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
param (
# Accepts an optional port number as the first argument.
[int]$Port
)

# Exit on any error
$ErrorActionPreference = "Stop"

# Run the build script to make sure there's something to serve
.\build.ps1

# Start DocFx serve
dotnet docfx serve .\_site
if ($Port) {
dotnet docfx serve .\_site -p $Port
}
else {
dotnet docfx serve .\_site
}
6 changes: 5 additions & 1 deletion serve.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ set -e
./build.sh

# Start DocFx serve
dotnet docfx serve _site
if [ -n "$1" ]; then
dotnet docfx serve _site -p "$1"
else
dotnet docfx serve _site
fi
Loading