I had a request for some pointers on making plugins for AppGameKit with PureBasic so I thought I would post a detailed 'How To' guide
PureBasic is a powerful language that has a long history and a bright future, v6 will see native inline C support and a new C backend and compiler, this will open many doors when combined with AppGameKit, it really is a very easy to use and powerful union.
The Basics:
The first thing we need to do is setup some folders:
navigate to your AppGameKit install dir and find the Plugins folder...
Classic: C:\Program Files (x86)\The Game Creators\AGK2\Tier 1\Compiler\Plugins
Studio: C:\Program Files (x86)\The Game Creators\AppGameKit Studio\Plugins
Create a folder, name it myPlugin (yes this is basic, humour me for now, aye), Inside this folder create a text file, name it 'Commands.txt' and paste the following text
#CommandName,ReturnType,ParameterTypes,Windows,Linux,Mac,Android,iOS,Windows64
MyFunction,0,0,MyFunction,0,0,0,0,0
we will worry about what this content means in a bit, lets get something working first.
Create the PB project
Now, Open PureBasic (x86 - 32 Bit, we will deal with 64 bit later) and create a project, again call it myPlugin and save it where ever you save your PB stuff,
Use the project creation dialog to add a file, call it main.pb and click the 'Create Project' button, this will open the project overview tab.
Now we need to set the compile targets, in the 'Project Targets' list, right click 'Default Target' and click 'Edit Target', this opens the project compiler options.
Set executable format, Shared DLL
Set the input file to the main.pb file that was created with the project and set the output executable to :
Classic: C:\Program Files (x86)\The Game Creators\AGK2\Tier 1\Compiler\Plugins\myPlugin\Windows.dll
Studio: C:\Program Files (x86)\The Game Creators\AppGameKit Studio\Plugins\myPlugin\Windows.dll
you can add both if you want to compile for both systems but set your preferred as default (I set classic) (checkbox at bottom of compile targets list) and set the other 'Studio' Enable in build all targets, now we can compile our plugin for both Classic and Studio at a single button click, handy stuff
Click OK in the dialog, you are now greeted with an empty code page, first lets test our compile targets to make sure we got it right, go to the main menu, click Compiler>>Build All Targets, a dialog will appear and tell you what's going on, when its done you should now have a new DLL in the plugin folder (you will need to launch PureBasic with admin rights to compile to the Program Files folder)
if everything is good, we can add some code, if something went wrong, check each step and try again.
Add some code
Now we will add the a basic function:
When AppGameKit loads the plugin it looks for a function 'ReceiveAGKPtr', this function is called by the runtime and sends the full set of function pointers to the plugin, we will setup this function but not use it yet, this function must exist for the plugin to work so we set this us with a C prototype
PrototypeC _GetAGKFunction(Function.p-ascii)
ProcedureCDLL ReceiveAGKPtr(*GetFunction)
GetAGKFunction._GetAGKFunction=*GetFunction
EndProcedure
we will come back to this later.
Now lets add a basic function, we use the function name we put in the Commands.txt
ProcedureCDLL MyFunction()
MessageRequester("PureBasic", "Hello from a PB plugin", #PB_MessageRequester_Info)
EndProcedure
And once again, Menu>>Compile>>Build all Targets.
Now open AppGameKit, any flavor if you added both compile targets, or just the one you added and create a new project, call it Example - Plugin
Add:
#Import_Plugin myPlugin
or
#Import_Plugin myPlugin as mp
In the first case we call our functions with the syntax myPlugin.MyFunction, in the second case we use an alias 'mp' so the call becomes mp.MyFunction
// show all errors
SetErrorMode(2)
#Import_Plugin myPlugin
// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
myPlugin.MyFunction()
do
Print( ScreenFPS() )
Sync()
loop
or
// Project: Plugin - Example
// Created: 2021-07-24
// show all errors
SetErrorMode(2)
#Import_Plugin myPlugin as mp
// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
mp.MyFunction()
do
Print( ScreenFPS() )
Sync()
loop
Now hit run, you should see a dialog box with a message "Hello from a PB plugin"
Congrats you have made your first Plugin.
Arguments
Ok so now lets send in some arguments, go back to the Commands.txt file
Change : MyFunction,0,0,MyFunction,0,0,0,0,0
To MyFunction,0,
S,MyFunction,0,0,0,0,0
This tells AppGameKit we want to send 1 string argument to the pkugin (S=String, I= Integer, F = Float)
Sidenote:
If we had 2 string and an integer we would stack them like so: MyFunction,0,
SSI,MyFunction,0,0,0,0,0
Now go back to the PureBasic file and change the function to:
ProcedureCDLL MyFunction(message.s)
MessageRequester("PureBasic", message, #PB_MessageRequester_Info)
EndProcedure
and recompile, now to the AppGameKit project and change
mp.MyFunction()
to
mp.MyFunction("Hello from AGK")
and click run ... you should see a dialog box with jibberish, see PureBasic is Unicode, AppGameKit is not so we need to let PB know its Ascii text coming in, we do this with a helper function and a memory peek
Procedure.s S(in.s)
ProcedureReturn PeekS(@in, -1, #PB_Ascii)
EndProcedure
and wrap the text in our function
ProcedureCDLL MyFunction(message.s)
MessageRequester("PureBasic", S(message), #PB_MessageRequester_Info)
EndProcedure
compile, go back to AppGameKit and run,, you should now see the message "Hello from AGK"
As we have a message requester with 2 string arguments, let add them both
Commands.txt
MyFunction,0,SS,MyFunction,0,0,0,0,0
PureBasic:
ProcedureCDLL MyFunction(title.s, message.s)
MessageRequester(S(title), S(message), #PB_MessageRequester_Info)
EndProcedure
AGK:
mp.MyFunction("AppGameKit", "Hello from AGK")
Now see if you can add the integer icon argument by yourself
Return Values
Now lets return a value from the plugin, we tell AppGameKit this function returns a value in the Commands.txt
MyFunction,
I,SS,MyFunction,0,0,0,0,0
for this case we return an integer value
Now in PureBasic, lets change the message requester to a Yes/No dialog and return its value
ProcedureCDLL MyFunction(title.s, message.s)
ProcedureReturn MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
EndProcedure
and in AppGameKit we check the value, but lets add a button so we can check each case
// Project: Plugin - Example
// Created: 2021-07-24
// show all errors
SetErrorMode(2)
#Import_Plugin myPlugin as mp
// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
AddVirtualButton(1, 100, 100, 50)
do
if GetVirtualButtonPressed(1)
yay=mp.MyFunction("AppGameKit", "Would you like to learn more?")
endif
if yay = 6
Print( "You clicked Yes" )
elseif yay = 7
Print( "You clicked No" )
else
Print( "Click the button" )
endif
Print( ScreenFPS() )
Sync()
loop
And there you have it, sending and receiving data to/from a plugin
Returning Strings:
Floats and Integers can be returned directly but we have to handle strings a little different.
For this we head back into PureBasic and the ReceiveAGKPtr function, we need to import some string functions, for this we use the _GetAGKFunction prototype we setup earlier
SideNote, there is a full function wrapper attached to this post, I am showing it this way so you understand what's going on in the code.
PrototypeC _GetAGKFunction(Function.p-ascii)
; store the prototypes in a global
Global agkCreateString : PrototypeC.i AGKCommand0(size.i)
Global agkDeleteString : PrototypeC AGKCommand1(ptr.i)
ProcedureCDLL ReceiveAGKPtr(*GetFunction)
GetAGKFunction._GetAGKFunction=*GetFunction
; ask AGK for pointers to the following functions
agkCreateString.AGKCommand0=GetAGKFunction("CREATESTRING_S_L")
agkDeleteString.AGKCommand1=GetAGKFunction("DELETESTRING_0_S")
EndProcedure
Change Commands.txt from Intger return to String
MyFunction,
S,SS,MyFunction,0,0,0,0,0
In PB, we need to create a string pointer to send to AppGameKit, for this we use a handy helper function to poke the memory
Procedure _agkMakeString(String.s)
Size=StringByteLength(String)
If Size>0
*Ptr=agkCreateString(Size+1)
If *Ptr
PokeS(*Ptr, String, Size, #PB_Ascii)
ProcedureReturn *Ptr
EndIf
EndIf
EndProcedure
And change our function to:
ProcedureCDLL MyFunction(title.s, message.s)
Protected RetString.s=""
Select MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
Case #PB_MessageRequester_Yes
RetString="The plugin reports YES"
Case #PB_MessageRequester_No
RetString="The plugin reports NO"
EndSelect
ProcedureReturn _agkMakeString(RetString)
EndProcedure
And change the AppGameKit code to:
// Project: Plugin - Example
// Created: 2021-07-24
// show all errors
SetErrorMode(2)
#Import_Plugin myPlugin as mp
// set window properties
SetWindowTitle( "Plugin - Example" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate( 30, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
AddVirtualButton(1, 100, 100, 50)
yay$ = "Click the Button"
do
if GetVirtualButtonPressed(1)
yay$=mp.MyFunction("AppGameKit", "Would you like to learn more?")
endif
Print( yay$ )
Print( ScreenFPS() )
Sync()
loop
run the project, now the string is returned from the plugin,
There is one important factor to consider when using agkCreateString, string pointers must be deleted with agkDeleteString but we have the situation where we return the pointer and no longer have access to it, in this case I THINK! AppGameKit handles the deletion of the pointer, I ran some tests with a global pointer and while the pointer refers to valid memory the contents seem to be deleted, there is no documentation to support or disprove this so some input here would help @TGC
Errors:
If you want to prompt an error message from within your plugin we use another helper function
Procedure agkPushError(String.s)
Size=StringByteLength(String)
If Size>0
*Ptr=agkCreateString(Size+1)
If *Ptr
PokeS(*Ptr, String, Size, #PB_Ascii)
agkPluginError(*Ptr)
agkDeleteString(*Ptr)
EndIf
EndIf
EndProcedure
and we muse add the agkPluginError to oue ReceiveAGKPtr function
PrototypeC _GetAGKFunction(Function.p-ascii)
; store the prototypes in a global
Global agkCreateString : PrototypeC.i AGKCommand0(size.i)
Global agkDeleteString : PrototypeC AGKCommand1(ptr.i)
Global agkPluginError : PrototypeC AGKCommand895(szErr.i)
ProcedureCDLL ReceiveAGKPtr(*GetFunction)
GetAGKFunction._GetAGKFunction=*GetFunction
; ask AGK for pointers to the following functions
agkCreateString.AGKCommand0=GetAGKFunction("CREATESTRING_S_L")
agkDeleteString.AGKCommand1=GetAGKFunction("DELETESTRING_0_S")
agkPluginError.AGKCommand895=GetAGKFunction("PLUGINERROR_0_S")
EndProcedure
And add an error to our function
ProcedureCDLL MyFunction(title.s, message.s)
Protected RetString.s=""
If S(title) = "AGK"
agkPushError("You can not use AGK as a title!")
EndIf
Select MessageRequester(S(title), S(message), #PB_MessageRequester_YesNo)
Case #PB_MessageRequester_Yes
RetString="The plugin reports YES"
Case #PB_MessageRequester_No
RetString="The plugin reports NO"
EndSelect
ProcedureReturn _agkMakeString(RetString)
EndProcedure
and change the call in agk to prompt the error
if GetVirtualButtonPressed(1)
yay$=mp.MyFunction("AGK", "Would you like to learn more?")
endif
This simply shows the AppGameKit message box, you could of course just use your own feed back system but this function is here for convince.
And that is the basic setup of PureBasic for plugin production, the full wrap of ReceiveAGKPtr exposing all AppGameKit functions is attached below, the overloaded calls have been commented out and not all functions are tested but should work, you could literally write your entire game in PureBasic and include it and run with a single function call in AGK.
Next, Advanced usage