Windows Asynchronous C++ DLL Extension creation in Eclipse

Bawat

Member
GM Version: GameMaker Studio 2.2.5.481
Target Platform: Windows Only
Download: https://www.bawat.net/downloads/GameMakerStudioResources/DLLExampleUsage.yyz
Links: https://forum.yoyogames.com/index.p...th-c-dll-extension-creation-in-eclipse.73728/
Links: https://docs.yoyogames.com/source/dadiospice/002_reference/001_gml language overview/variables/async_load.html
Links: https://help.yoyogames.com/hc/en-us...es-From-An-Extension-Asynchronously-GMS-v1-3-
Links: https://docs.yoyogames.com/source/dadiospice/001_advanced use/more about async events/steam.html
Links: https://github.com/bawat/AStarMultithreaded/blob/master/GameMakerDLL/DSMap.cpp
Links: https://www.bogotobogo.com/cplusplus/multithreading_pthread.php

Summary:
I was able to increase the number of zombies I had on screen from 25 to 125 by making my AStar pathfinding code asynchronous.
This guide will discuss the issues I had when achieving this goal, and will show you how to achieve this yourself.

Tutorial:
Keep in mind, that when using a DLL with your game, you will only be able to compile your game for the Windows platform.
The AStar DLL I have created will be provided at the end of the article along with the source code on GitHub for reference.
If you haven't read my getting set up guide, I'd recommend looking at it here.
Essentially, you want to
  • Generate 32 bit DLLs using MinGW in Eclipse
  • Prepend __declspec (dllexport) to any function you want GameMaker Studio 2 to be able to see
  • Functions prepended with __declspec (dllexport) will only work with char* or double parameters and return types
  • Functions prepended with __declspec (dllexport) must return a value, be it char* or double
  • Add the following flags to your linker -static-libgcc -static-libstdc++
  • Import the DLL into GameMaker Studio 2 by creating a new Extension and adding YourDLL.dll as one of the files
I've only been using C++ for a few days so all critisicsm on the source code is welcome.
If you don't know C++, I basically just read A Tour of C++ to get this created. It's about 50 pages, half of which can be skim read and the rest Googled.

A Yoyogames article from a few months back brought to light, an update that allows us to create ds_maps inside our DLL code and return them to the Asynchronous -> Social Event in GameMaker Studio 2 through the ds_map named async_load.
There is no need to destroy the async_load ds_map as this is handled for you by GameMaker: Studio.

To create and return a ds_map in C++, Game Maker Studio 2 will expect to find the following procedure inside our DLL.
__declspec (dllexport) void RegisterCallbacks(char *arg1, char *arg2, char *arg3, char *arg4 )
Game Maker Studio 2 will run this automatically on load only if we add the function with that exact name and structure inside GMS2, as shown in the image below.
1587343738368.png

GMS2 can only supply Doubles and Char Pointers to our C++ DLL and vice versa. We should cast them to their appropriate types before using them.
GMS2 will send us a link to the following functions through the 4 different Char*. The Char* arguments are in the order listed.
void CreateAsynEventWithDSMap(int mapID, 70) <-- 70 is the id of the SOCIAL Asynchronous Event
int CreateDsMap(0) <-- You must always pass 0 to this function according to this
bool DsMapAddDouble(int mapID, string key, double value)
bool DsMapAddString(int mapID, string key, string value)

I have created a DSMap.cpp that handles the casting and usage of these functions via a fluent interface.
You're welcome to copy and paste any code from my AStarAsyncDLL project.

Example Usage
C++:
DSMap returnMap{};
returnMap
    .addDouble("pathWaypoints", index)
    .addDouble("requestID", desiredPath.getRequestID());
    .sendToGMS2();
To make our C++ code asynchronous, we will need to allow our code to run after our main method returns a value to GMS.
We can do this by created a new Thread.

Since we can only use MinGW to compile our DLLs with Eclipse, our only multithreading option is PThreads.
Using the MinGW Installation Manager, you will need to install PThreads. For Eclipse, I needed to include the library in my DLL by adding the following arguments to the MinGW C++ linker.
-pthread -static -lpthread -shared
Information about PThreads and how to use them can be found here.

To use pthread_create() we will need to supply a function that takes in any type and returns any type.
Inside that function, we will need to typecast the void* to the structure we stored everything in.

C++:
void* calculateAStar(void* input){
AStarAlgorithmParameters desiredPath = *((struct AStarAlgorithmParameters*)input);

//Do Work

DSMap returnMap{};
returnMap
    .addDouble("pathWaypoints", index)
    .addDouble("requestID", desiredPath.getRequestID());
    .sendToGMS2();
}
It is important to know that when the main method returns, the threadID returned by pthread_create will fall out of scope.
Memory used by the thread will then be used by the computer for new purposes causing unexpected values in your variables, intermittent reliablilty and ultimately crashes.
The threadID MUST NOT fall out of scope for the thread to be allowed to hold it's data and function properly.

I allocate a new pthread on the heap and store it's address inside the AStarAlgorithmParameters.

pthread_t* assignedThread = new pthread_t;
I make sure that all AStarAlgorithmParameters are always held in memory.

inline static std::vector<AStarAlgorithmParameters*> previousInstances;
And that they clean themselves up after 3 seconds have passed.

C++:
AStarAlgorithmParameters(double startX, double startY, double endX, double endY, int gridSize){
    creationTime = now();
    previousInstances.push_back(this);
    clearExpiredInstances();
};

static void clearExpiredInstances(){
    for(AStarAlgorithmParameters* instance : previousInstances){
        if(instance->hasExpired()){
            previousInstances.erase(std::remove(previousInstances.begin(), previousInstances.end(), instance), previousInstances.end());
            pthread_cancel(*instance->assignedThread);
            delete instance->assignedThread;
            delete instance;
        }
    }
}
So how do we run it?
First of all, you will want to create a new Extension inside of GMS2 and add your DLL file as a resource.
If you ever make changes to the DLL, make sure it has the same name as the original DLL, add it as a resource again and you will get a prompt to overwrite the existing file.

Second part - and this is important - is adding the RegisterCallbacks function. Make sure it's prepended with __declspec (dllexport) in your DLL, and then add the function as shown in the first image of this guide. All __declspec (dllexport) functions must return a value.

Thirdly, we will need to add any other functions that your code wants GMS2 to be able to see. For the A* pathfinding on Github, I have a GML function to initialise collision boxes that the pathfinding should avoid.
It is defined as follows
void registerCollisionBox(instanceID, x1, y1, x2, y2);
I also have a function to begin calculating an A* path between two points. The DLL code starts off the calculation and returns a requestID, like a ticket number when you're waiting for food.
It is defined as follows
int requestID asyncAStarDistance(beginX, beginY, endX, endY, gridSize);

Fourthly, someone will need to write code inside Async - Social to catch the value returned from the A* pathfinding when it's completed it's work.
This is the GML code inside Async - Social for each of my zombies
GML:
if (!ds_exists(async_load, ds_type_map)){
    show_debug_message("Async_load doesn't exist?");
    return;
}
if(async_load[? "requestID"] != pathUpdateRequestID) return;

if(movementPath != -1){
    path_delete(movementPath);
}
var size = async_load[? "pathWaypoints"];
movementPath = path_add();
for(var i = size-1; i > 0; i--){
    var xp = async_load[? ("position" + string(i) + "valueX")];
    var yp = async_load[? ("position" + string(i) + "valueY")];
    path_add_point(movementPath, xp, yp, 1);
}

//There is no need to destroy the async_load ds_map as this is handled for you by GameMaker: Studio. https://docs.yoyogames.com/source/dadiospice/001_advanced%20use/more%20about%20async%20events/steam.html
//ds_map_destroy(async_load);

pathUpdateRequestID = -1;
The ds_map we created inside the DLL is given to use by GMS2 in the form of async_load. GameMaker will handle the freeing of this structure for you.
Sometimes when the game first starts up, the async_load won't actually exist. Not sure why this is. So we check existance before continuing.

I use the ds_map accessor syntatic sugar to make sure the request being returned is for our specific zombie.
I then loop through all the elements of the ds_map and store them into a GML path for personal convenience. As ds_maps are hashmaps, this is all done very quickly.

Finally I make sure to reset the request we're waiting on back to -1, as these requestIDs get recycled inside the DLL.

You can test that it is actually running asynchronously but making the program halt for 5 seconds before returning a value. (Just make sure you don't clean up the threads too early)
You should notice that your game didn't freeze for 5 seconds while running the code - congratulations you now know how to make use of the rest .
These DLLs can be quite difficult to debug, so make good use of std::cout! It can print to the GMS2 console even when it's in a different thread.

GameMaker Studio 2's console recieving output from different threads while playing the game.
1587349892835.png

I personally use pthread_exit() when debugging to allow Eclipse to keep the thread alive after the main method has returned.

C++:
int retVal = 20;
pthread_exit(&retVal);//End the main method and return a value, but keep the process loaded until all threads have finished execution.
Remember to free any memory you allocate to the heap as you don't want memory leaks. You can see if your program has a significant memory leak by looking at the memory your game takes in task manager.
If it rapidly increases - you've got a leak. It will hit 3.7gb and then the game will crash and hide in the background processes.
You can know that it's your DLL causing the memory leak by running your game in debug mode. GMS2 will show you how much memory your game is taking through it's UI, and it won't be increasing like task manager shows you.
Again, std::cout is your friend here, and is how I found that I had created an infinate loop.

Capture2.jpg1587355252697.png

https://github.com/bawat/AStarMultithreaded
https://www.bawat.net/downloads/GameMakerStudioResources/AStarPathfinder.dll
https://www.bawat.net/downloads/GameMakerStudioResources/DLLExampleUsage.yyz
 
Last edited:
Great work on this! I don't even know how to spell C+× but I will bookmark and check-out the introduction you linked above. Thanks for sharing!
 

Bawat

Member
Fixed a bug inside AStarPathfinder.dll that occasionally causes an abrupt crash to desktop for the game it's used in.
If you're actively using it in your projects, please redownload the new fixed file.
 

Samuel Venable

Time Killer
Here's an asynchronous dll I wrote that uses the async dialog event on Windows, Mac, and Linux:

Here's a demo project, (dll source code included for all platforms):

In case you were wondering what the ID's were for the all the events, (not just social):
GML:
"ev_web_image_load", 60
"ev_web_sound_load", 61
"ev_web_async", 62
"ev_dialog_async", 63
"ev_web_iap", 66
"ev_web_cloud", 67
"ev_web_networking", 68
"ev_web_steam", 69
"ev_social", 70
"ev_push_notification", 71
"ev_async_save_load", 72
"ev_audio_recording", 73
"ev_audio_playback", 74
"ev_system_event", 75
"ev_broadcast_message", 76
 
Last edited:
Top