Overview

Matchstick (MGDF) is a framework designed to make developing, installing, distributing, and updating games on Windows (Windows 7+ supported) easier. The framework uses DirectX 11 for graphics, RawInput/XInput for user input (has full support for the XBox 360 controller), and OpenAL for audio (supports 3d positional audio and audio streaming using Ogg Vorbis). The framework also provides a virtual file system which allows easy access to resources stored in zipped archives (new archive formats can also be plugged in).

MGDF makes developing games in c++ faster and easier as you no longer have to build and rebuild the same boilerplate code for initializing DirectX/Audio etc, loading preferences and setting up a render loop every single time you make a game. This means you can spend more time making your game. It also makes distributing games easier as it includes an auto update mechanism with full versioning support.

MGDF is comprised of two main components (the second of which is not required for running games, but for online updating/distribution)

The core (core.exe)

This is written in c++ and is the part of the framework which actually runs the games. The core works by initializing the graphics/input/audio devices, loading up user preferences and setting up a multithreaded render/simulation loop before loading the user supplied game dll (known as a game module) and passing control on to that module. Once up and running the game module can then access the framework's functionality via a series of c++ interfaces (see the API reference for more details)

These interfaces include the ability to create audio samples and streams which under the hood are powered by the openAL audio system. Also included is the ability to access user interfaces devices including the mouse, keyboard, and any connected xbox 360 controllers. File system access is performed by a virtual file system wrapper which allows simple access to normal files on disk as well as files within compressed archives. Unlike the previous services, Direct3D 11 is directly accessible by the game module so as to negate the need to wrap the entire Direct3D API, though a number of helper interfaces are made available to make changing graphics settings easier.

The GamesManager (GamesManager.exe)

This is written in C# (.NET 4.8) and consists a windows forms application which provides a minimalist launcher that checks for game updates and displays friendly error messages if the core crashes. It is also responsible for sending game statistics if this feature is used. In addition to this, the GamesManager also can be used by an installer to handle the usual tasks of windows program installation such as adding a installed programs entry in the registry, a start menu entry, and a desktop icon (see the command line options for more details)

NOTE This component is optional. If you plan on using your own solution for launching the game and managing updates (Steam, Itch.io, etc.), you can omit this piece of the framework from your game and start your game by running core.exe directly instead of launching it via GamesManager.exe

Building a new game

MGDF takes a convention over configuration approach for the most part and assumes that a number of files will be provided by the game and that these files will exist in a standard folder structure inside the root MGDF install folder. The standard folder layout with the relevant locations highlighted are as follows.

NOTE The MGDF redistributable included in the SDK is already layed out in this format, all you need to add when building a new game is the contents of the /game folder.

/install root

  • core.exe

  • GamesManager.exe

  • GamesManager.FrameworkUpdater.exe

  • ... other framework dlls

  • /resources

  • /dependencies

  • /game

    All games files should be placed inside this folder

    • /bin

      All game executable dlls should be placed inside this folder

      • module.dll [Required]

        This dll is required and provides an entry point for the host to create an instance of the game module.

    • /content

      • Any game data files should go in here. This folder serves as the root of the virtual file system and content outside this folder is not accessable

    • game.json [Required]

      This JSON file contains metadata including the name, version, and preference settings for the game. This file is required and the game will not run unless it is present.

    • gameIcon.png [Optional]

      This 48x48 PNG file is used to display the game logo in the GamesManager UI as well as the installed programs registry entry, and the desktop icon. This file is optional, and if not specified a default MGDF icon will be used instead.

    • gameSystemIcon.ico [Optional]

      By default the PNG image above will be converted automatically into an icon file for display in parts of the system that cannot use PNGs. If you don't wish to use the automatically converted icon file (sometimes the conversion results in messy transparency), and instead want to provide one, you can do so here.

Game.json

As mentioned above, this file is responsible for telling the GamesManager and core everything they need to know about running your game. A basic manifest file is shown below. For full information on all the fields and their meanings, check out the API reference for more information.

{
    "gameUid":"a unique id",
    "gameName":"A human readable name",
    "version":"0.1",
    "interfaceVersion":"1",
    "developerName":"Human readable developer name",
    "homepage":"https://www.example.com",
    "supportEmail":"[email protected]",
}

Module.dll

Module.dll is the actual entry point into your game. This dll requires a number of functions to be exported, and must be able to provide the mgdf host with an instance of the IMGDFModule interface in order for the host to be able to run your game code.

MGDF is multithreaded and runs two main threads, the Sim thread and the Render thread. The Render thread is responsible for all rendering, and the Sim thread is responsible for handling input, audio, game logic. It is important to handle the sharing of data between these threads in a threadsafe manner for a game to work correctly. For the most part this means minimizing the amount of shared state between the Sim and Rendering parts of your game and ensuring that appropriate locking strategies are in place.

One method for managing this is to use a Double Buffered game state, i.e. the Sim thread modifies the game state and on each tick makes a copy of the current game state (or as much of a copy as the renderer needs to do its job) and copies it into a buffer which the Render thread can pick up when it's ready to render the next frame. In this way the Sim and Render threads can operate mostly independantly of each other without having to worry about concurrency issues. By default the Render thread will not run more than once per Sim tick, so you don't have to worry about buffer underrun in this case either (though this behaviour is configurable - if you have a very slow Sim framerate and want to interpolate a single game state over several render frames - check out the host.interpolateFrames preference option in the Game.json API reference)

NOTE The SDK includes a sample visual studio game project in src/samples/EmptyGame. This project contains a game.json file and will create a compatible module.dll with a stub module class.

Implementing the IMGDFModule interface

Below is a stub implmentation of the IMGDFModule interface. All MGDF interfaces are COM objects and inherit from IUnknown, so it is the users responsibility to release them when no longer required. Once a module instance is created, the host communicates events to the module by calling into the various methods provided. You may notice that all methods except Panic are prefixed with ST or RT - RT methods will be called by the host from the render thread, and ST methods will be called by the host from the sim thread. Panic may be called from either thread.

Module.h
#pragma once
#include <MGDF/ComObject.hpp>
#include <MGDF/MGDF.h>

class Module: public MGDF::ComBase<IMGDFModule>
{
public:
	virtual ~Module( void );
	Module();
	BOOL __stdcall STNew( IMGDFSimHost *host ) final;
	BOOL __stdcall STUpdate( IMGDFSimHost *host, double elapsedTime ) final;
	void __stdcall STShutDown( IMGDFSimHost *host ) final;
	BOOL __stdcall STDispose( IMGDFSimHost *host ) final;
	BOOL __stdcall RTBeforeFirstDraw( IMGDFRenderHost *host ) final;
	BOOL __stdcall RTDraw( IMGDFRenderHost *host, double alpha ) final;
	BOOL __stdcall RTBeforeBackBufferChange( IMGDFRenderHost *host ) final;
	BOOL __stdcall RTBackBufferChange( IMGDFRenderHost *host ) final;
	BOOL __stdcall RTBeforeDeviceReset( IMGDFRenderHost *host ) final;
	BOOL __stdcall RTDeviceReset( IMGDFRenderHost *host ) final;
	void __stdcall Panic() final;
};
Module.cpp
#include "Module.hpp"

Module::~Module( void )
{
}
Module::Module()
{
}
BOOL Module::STNew( IMGDFSimHost *host )
{
    // This method is called by the host to initialize the module.
    return true;
}
BOOL Module::STUpdate( IMGDFSimHost *host, double elapsedTime )
{
    // Game logic goes here, the host calls this function once per
    // tick of the simulation thread
    return true;
}
void Module::STShutDown( IMGDFSimHost *host )
{
    // Called by the host to indicate to the module that it should shut down
    // as soon as possible. Note that shutdown will not actually occur until
    // the module then calls host->Shutdown(), which it can do at its discretion
    host->ShutDown();
}
BOOL Module::STDispose( IMGDFSimHost *host  )
{
    // called by the host when the module is to be destroyed
    delete this;
    return true;
}
BOOL Module::RTBeforeFirstDraw( IMGDFRenderHost *host )
{
    // Called by the host before any rendering occurs on the render thread
    // Any first time rendering initialization stuff should go here
    return true;
}
BOOL Module::RTDraw( IMGDFRenderHost *host, double alpha )
{
    // Called by the host once per tick of the render thread
    // Any rendering goes here.
    return true;
}
BOOL Module::RTBeforeBackBufferChange( IMGDFRenderHost *host )
{
    // Called before the host resizes the current backbuffer
    // Anything holding a reference to the backbuffer should release it now
    // otherwise Direct3D will not be able to change the backbuffer
    return true;
}
BOOL Module::RTBackBufferChange( IMGDFRenderHost *host )
{
    // Called after the host has created a newly resized back buffer
    // Any Direct3D resources that may need to be updated as a result of the backbuffer
    // changing should be handled here.
    return true;
}
BOOL Module::RTBeforeDeviceReset( IMGDFRenderHost *host )
{
    // Called by the host before resetting the Direct3D Device
    // Anything holding a reference to ANY device dependent resource should release it now
    // as these references to the removed device are now invalid
    return true;
}
BOOL Module::RTDeviceReset( IMGDFRenderHost *host )
{
    // Called by the host after resetting the Direct3D device
    // Recreate any resources cleared out in RTBeforeDeviceReset
    return true;
}
void Module::Panic()
{
    // Called by the host after any module event fails
}

Implementing main.cpp

Now that we have a module implementation, we need to make it available to the host. Below is a sample main.cpp file for a module dll. When the host starts up, it will first call the GetCompatibleFeatureLevels function. It will then call GetModule, and will invoke events on that module instance for the rest of the hosts lifetime.

#include "Module.hpp"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
	return TRUE;
}

extern "C" __declspec(dllexport) UINT64 GetCompatibleFeatureLevels(D3D_FEATURE_LEVEL *levels,
                                        UINT64 *featureLevelsSize) {
  if (*featureLevelsSize != 1) {
    *featureLevelsSize = 1;
    return 1;
  } else {
    levels[0] = D3D_FEATURE_LEVEL_11_0;
    return 0;
  }
}

extern "C" __declspec(dllexport) HRESULT GetModule(IMGDFModule **module) {
  auto m = MGDF::MakeCom<Module>();
  m.AddRawRef(module);
  return S_OK;
}

Running a game

When distributing a game to users, one simply has to put the game content and module in the prescribed locations inside the framework redistributable (as shown here). Users can then run the game by running GamesManager.exe.

However, having to keep your game inside the redistributable folder can be annoying during development, so there are a few other options available to make running the game in development easier.

Firstly, in development don't bother running GamesManager.exe, run core.exe directly instead. The GamesManager only wraps core.exe and does update checking and some other services that are not needed for testing development builds. Also running core.exe directly allows you to easily attach the Visual Studio debugger when running your game.

Secondly, you can actually run a game that exists anywhere on disk by passing in the following command line parameters when running core.exe

core.exe -gamediroverride "some folder on disk"

Provided that the folder specified has the usual /bin and /content structure that the /game folder in the framework would usually have, things will work as if the folder was in the /game folder in the framework redistributable folder.

Another useful commandline flag is

-userdiroverride

This changes the location that MGDF stores logs and per user data such as saved games and preferences from its usual folder in %appdata%/local/MGDF/1/<GameUid> to a /user folder in the parent folder of the current /game directory. For example, if the -gamediroverride was set to c:\mygame\game, and -userdiroverride was also set, then c:\mygame\user would be used to store all user data.

Distributing a game

Installing

In order to distribute your game to users it is recommended to bundle your game into an installer, rather than just provide a .zip archive containing a framework redistributable combined with your game module and content files.

The main advantages to bundling everything in an installer is that the GamesManager depends on the .NET framework 4.5 runtime, which is not guaranteed to be present on all users systems. Because of this an installer may be necessary to install the framework prior to running the GamesManager

The general procedure for integrating an MGDF game with an installer is as follows

  1. The installer should extract all files in the standard MGDF folder structure to somewhere on disk
  2. The installer should then check for the presence of the .NET framework 4.5, and if not installed should install or prompt the user to install it
  3. The installer should then invoke the following command

    GamesManager.exe -register

    This will cause the GamesManager to

    • Silently install the Visual c++ runtime library that Core depends upon
    • Add the games information to the installed programs list
    • Add a start menu shortcut
    • Add a desktop icon shortcut
    • Perform any integration with the windows game explorer

Example NSIS install script (assuming a game with a gameUid of 'newgame' )

;Includes
!include "MUI2.nsh"
!include "FileFunc.nsh" 
  
;--------------------------------
; The name of the installer
    Name "New game"
    OutFile "Setup.exe"

; The default installation directory
    InstallDir "$PROGRAMFILES\NewGame"
    RequestExecutionLevel admin

;--------------------------------
;Constants
    !define CSIDL_APPDATA '0x1A' ;Application Data path

;--------------------------------
;Pages
    !insertmacro MUI_PAGE_DIRECTORY
    !insertmacro MUI_PAGE_INSTFILES
    !insertmacro MUI_UNPAGE_CONFIRM
    !insertmacro MUI_UNPAGE_INSTFILES

;--------------------------------
;Languages 
    !insertmacro MUI_LANGUAGE "English"
  
;--------------------------------
; check for and install .net 4.8
Section "Install .NET Framework 4.8"
IfSilent Ignoredotnetsetup
    Call CheckAndInstallDotNet
Ignoredotnetsetup:
SectionEnd

;--------------------------------
; Remove lastupdate file so we can check for updates as 
; soon as we next start up (this installer may be out of date)
Section "Remove lastUpdate check"
    Delete $LOCALAPPDATA\MGDF\1\NewGame\.lastupdate
SectionEnd

;--------------------------------
; the core engine components
Section "Install NewGame"
;--- core engine components ---
    SetOutPath $INSTDIR
    File /r "path to the MGDF root folder"
	
;--- write out the add/remove program's registry keys & install any framework dependencies
    ExecWait '"$INSTDIR\GamesManager.exe" -register'
    SetRegView 64
    WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MGDF1_NewGame" "UninstallString" "$INSTDIR\Uninstall.exe"
	
    ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
    IntFmt $0 "0x%08X" $0
    WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\MGDF1_NewGame" "EstimatedSize" "$0"
	
    WriteUninstaller $INSTDIR\Uninstall.exe	
SectionEnd

;--------------------------------
; uninstall the core engine
Section "Uninstall"
    ;-- remove registry keys, shortcuts etc.
    ExecWait '"$INSTDIR\GamesManager.exe" -deregister'
    ;--- remove uninstaller ---
    Delete $INSTDIR\Uninstall.exe
	
    RMDIR /r $INSTDIR
SectionEnd

Function CheckAndInstallDotNet
;--- https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#net_d
    ClearErrors
    ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release"

    IfErrors NotDetected

    ${If} $0 >= 528040
        DetailPrint "Microsoft .NET Framework 4.8 is installed ($0)"
    ${Else}
    NotDetected:
        DetailPrint "Installing Microsoft .NET Framework 4.8"
        SetDetailsPrint listonly
		SetOutPath $TEMP\Junkship
		File ndp48-web.exe
        ExecWait '"$TEMP\Junkship\ndp48-web.exe" /q /norestart' $0
        ${If} $0 == 3010 
        ${OrIf} $0 == 1641
            DetailPrint "Microsoft .NET Framework 4.8 installer requested reboot"
            SetRebootFlag true
        ${EndIf}
        SetDetailsPrint lastused
        DetailPrint "Microsoft .NET Framework 4.8 installer returned $0"
		Delete $TEMP\Junkship\ndp48-web.exe
    ${EndIf}
FunctionEnd

Updating

The MGDF GamesManager can check for available updates to either the game or framework upon launching the game (this update check will occur at most once per day). To enable this feature, you need to add an updateservice property to your game.json file, the value for this property should be a url pointing to a game update manifest file. This update manifest is a small JSON file which lists the latest version of your game, and the MGDF framework version your games supports. An example update manifest is shown below.

{
  "framework": {
    "version": "0.11.22",
    "url": "https://s3.matchstickframework.org/MGDF_0.11.22_x64.zip",
    "md5": "be4573e2058fa6106093443e6c7857d1"
  },
  "latest": {
    "version": "0.2.4",
    "url": "http://www.example.com/game_0.2.4.zip",
    "md5": "8059891b6663315a72a954c282a963c8"
  },
  "updateOlderVersions": [
    {
      "url": "http://www.example.com/game_0.2.4_update.zip",
      "md5": "eac5db79ad1fa8f61d20283fef37ad33",
      "fromVersion": "0.2.2",
      "toVersion": "0.2.3"
    }
  ]
}

There are three main objects in the update manifest, the first is the framework object which allows you to specify which version of the MGDF framework your game supports, this allows you to upgrade the framework independently to your own game module and content. The url used for this object should point to an MGDF redistributable zip file (such as those included in the SDK)

The other two objects are the latest object which should point to a zipped game package (more on how to create these packages shortly). Finally the updateOlderVersions is an optional object which lists an array of zipped partial update packages that include only the files to upgrade from a particular version (or range of versions) to the version specified in the Latest object.

Building update packages with PackageGen.exe

As mentioned above, in order to provide updates to your game, it has to be packaged into an update package. There are two types of update packages, full, and partial. A full update package is just a zip file containing everything in the /game folder whereas a partial update package contains only files which are different between two full update packages.

In order to make it easier to create these packages, the SDK includes a tool that can generate either full or partial update packages. In the simplest case of creating a full update package, the usage is as shown below (for more information see the API reference)

PackageGen "framework /game folder" -o "game package file"

NOTE The program output includes the version number and MD5 hash of the generated package, which you can use to fill out the field information in the game update manifest.

Statistics tracking

MGDF also has the ability to record and upload statistics from users play sessions. The user is prompted if a game wishes to gather statistics and can allow or deny the statistics gathering.

If permitted, statistics are recorded as key/value pairs. These pairs also include a timestamp which represents the time in seconds since that game session began. All statistics gathered in a game session also include a unique non-identifiable session identifier id so that all the statistics that were posted in a given session can be correlated.

In order to enable statistics tracking you need to add the following properties to your game.json file

  1. statisticsService - This value should point to a url where a compatible statistics web service is hosted (for more information on hosting a statistics service, or implementing your own see the API reference)
  2. statisticsPrivacyPolicy - This value should point to a url containing your privacy policy for usage of the statistics. This will be displayed to the user when they are prompted if they want to accept the statistics tracking or not. If this value is not present, no statistics tracking will be performed.

You can then upload statistics (as key value pairs) using the IMGDFStatisticsManager API. The GamesManager will upload any statistics recorded once a users play session has ended.