Samuli Natri - Software Developer
menu

.NET Core & SDL2 - Window Creation (C# Game Development Tutorial)

How to create a window with .NET Core using SDL2 C# bindings in macOS.

Create A Console Application

Create a new console application:

mkdir Hello
cd Hello
dotnet new console
dotnet run

This should produce the classic "Hello World!" text.

Install SDL2

In macOS you can use brew to install SDL2:

brew install sdl2

Get SDL2-CS

I will use C# bindings from here: https://github.com/flibitijibibo/SDL2-CS/tree/master/src.

Create an sdl subfolder and put the SDL2.cs file there:

mkdir sdl
cd sdl
wget https://github.com/flibitijibibo/SDL2-CS/raw/master/src/SDL2.cs

SDL2_image, SDL2_mixer and SDL2_tff

There are additional bindings in the same folder that you might want to download. For example if you want to use the SDL_Image image loading library (https://www.libsdl.org/projects/SDL_image/), download the SDL2_image.cs file and install the library: brew install sdl2_image.

AllowUnsafeBlocks

If you run it now with dotnet run, you might get this kind of error:

  SDL2.cs(57, 33): [CS0227] sdl/SDL2.cs(57,33): error CS0227: Unsafe code may only appear if compiling with /unsafe...

Set AllowUnsafeBlocks to true in the project file:

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

Run the app again with dotnet run and you should see "Hello World!".

Draw The Window

First run this code with dotnet run so we can see that the app is building with SDL2:

using SDL2;

namespace Hello
{
    class Program
    {
        static void Main(string[] args)
        {
            SDL.SDL_Init(SDL.SDL_INIT_VIDEO);
            SDL.SDL_Quit();
        }
    }
}

using SDL2; allows us to use the types in the SDL2 namespace so we can do SDL.SDL_Quit(); instead of specifying the namespace like this: SDL2.SDL.SDL_Quit();

SDL.SDL_Init(SDL.SDL_INIT_VIDEO); function initializes the SDL2 library. You must call this before you can use most of the other SDL functions. It takes subsystem initialization flags as parameters.

Some subsystems like file I/O and threading are initialized by default. You have to specifically initialize other subsystems you want to use. In this case we use the video subsystem. This will also initialize the events subsystem so that we can receive user input. You could add more flags like the audio flag SDL.SDL_Init(SDL.SDL_INIT_VIDEO|SDL.SDL_INIT_AUDIO);.

SDL.SDL_Quit(); cleans up all initialized subsystems. This should be called before you exit the program.

Nothing should happen but the build should succeed. Let's add the window creating code:

using System;
using SDL2;

namespace test1
{
    class Program
    {
        static void Main(string[] args)
        {            
            SDL.SDL_Init(SDL.SDL_INIT_VIDEO);
            
            var window = IntPtr.Zero;                        
            window = SDL.SDL_CreateWindow(".NET Core SDL2-CS Tutorial",
                SDL.SDL_WINDOWPOS_CENTERED,
                SDL.SDL_WINDOWPOS_CENTERED,
                1028,
                800,
                SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE
            );
            
            SDL.SDL_Delay(5000);
            SDL.SDL_DestroyWindow(window);            
            SDL.SDL_Quit();
        }
    }
}

IntPtr.Zero represents a pointer or handle that has been initialized to zero.

SDL.SDL_CreateWindow(); creates a window with a title, x / y position, width / height and additional flags. In this example we center the window and make it resizable.

For example if you want to use fullscreen and OpenGL, you would "OR" the flags like this:

...
    800,
    SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN |
    SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL
);
...

SDL.SDL_Delay(5000); waits at least 5000 milliseconds.

If I run the program now in macOS, I can see the window icon for 5 seconds but not the window itself. Let's add a while loop and wait for user input:

...
            //SDL.SDL_Delay(5000);
            
            SDL.SDL_Event e;
            bool quit = false;                        
            while (!quit)
            {
                while (SDL.SDL_PollEvent(out e) != 0)
                {
                    switch (e.type)
                    {
                       case SDL.SDL_EventType.SDL_QUIT:
                           quit = true;
                           break;
                    }
                }
            }
            
            SDL.SDL_DestroyWindow(window);
...

SDL.SDL_Event e; creates an empty SDL_Event structure that we fill with event data.

When user generates input like mouse clicks or key presses, these events will be recorded in an event queue.

while (SDL.SDL_PollEvent(out e) != 0) will go through this queue until it's empty. SDL_PollEvent(out e) removes the latest event and stores it in the e SDL_Event structure. When the queue is empty, SDL_PollEvent will return 0.

out e passes the e structure to the function as a reference, not as value.

The type of the event can be accessed from e.type. If the event type is SDL_QUIT (when window is closed), we set quit = true and break out of the switch. while (!quit) will evaluate to false when quit is true, so we end the while loop and destroy the window with SDL.SDL_DestroyWindow(window);.

Quit With "q"

When you press or release a keyboard button, an SDL_KEYDOWN or SDL_KEYUP event will be added to the queue. Let's close the window when user presses the "q" key:

                    ...
                    switch (e.type)
                    {
                        case SDL.SDL_EventType.SDL_QUIT:
                            quit = true;
                            break;
                        case SDL.SDL_EventType.SDL_KEYDOWN:
                            switch (e.key.keysym.sym)
                            {
                                case SDL.SDL_Keycode.SDLK_q:
                                    quit = true;
                                    break;
                            }
                            break;
                    }
                    ...

e.key.keysym.sym contains the pressed key SDL_Keycode Value. In the switch statement we react to the event if the keycode is SDLK_q and quit the loop.

keysym is a structure that contains key information. sym is the SDL virtual keycode. Find all available keycodes in here https://wiki.libsdl.org/SDL_Keycode.

Run the app with dotnet run and you should be able to use "q" to close the window.

Error Checks

Let's add error checks to the code.

SDL.SDL_Init() returns negative error code on failure or 0 on success:

...
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO) < 0)
{
    Console.WriteLine("Unable to initialize SDL. Error: {0}", SDL.SDL_GetError());
}
...

SDL.SDL_CreateWindow returns NULL on failure or the created window on success:

...
if (window == IntPtr.Zero)
{
    Console.WriteLine("Unable to create a window. SDL. Error: {0}", SDL.SDL_GetError());  
}
...

The Final Code

using System;
using SDL2;

namespace Hello
{
    class Program
    {
        static void Main(string[] args)
        {
            if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO) < 0)
            {
                Console.WriteLine("Unable to initialize SDL. Error: {0}", SDL.SDL_GetError());
            }
            else
            {
                var window = IntPtr.Zero;
                window = SDL.SDL_CreateWindow(".NET Core SDL2-CS Tutorial",
                    SDL.SDL_WINDOWPOS_CENTERED,
                    SDL.SDL_WINDOWPOS_CENTERED,
                    1020,
                    800,
                    SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE
                );

                if (window == IntPtr.Zero)
                {
                    Console.WriteLine("Unable to create a window. SDL. Error: {0}", SDL.SDL_GetError());  
                }
                else
                {
                    //SDL.SDL_Delay(5000);

                    bool quit = false;

                    SDL.SDL_Event e;

                    while (!quit)
                    {
                        while (SDL.SDL_PollEvent(out e) != 0)
                        {
                            switch (e.type)
                            {
                                case SDL.SDL_EventType.SDL_QUIT:
                                    quit = true;
                                    break;

                                case SDL.SDL_EventType.SDL_KEYDOWN:

                                    switch (e.key.keysym.sym)
                                    {
                                        case SDL.SDL_Keycode.SDLK_q:
                                            quit = true;
                                            break;
                                    }

                                    break;
                            }
                        }
                    }
                }

                SDL.SDL_DestroyWindow(window);
                SDL.SDL_Quit();
            }
        }
    }
}

The Final Folder Structure

.
├── Hello.csproj
├── Program.cs
├── bin
├── obj
└── sdl
    └── SDL2.cs