Author Topic: Tutorial : How to make custom, managed objects using the AGS plugin API (v1.1)  (Read 1104 times)  Share 

Calin Leafshade

  • AGS Project Admins
  • Long live King Cat!
    • I can help with AGS tutoring
    •  
    • I can help with voice acting
    •  
  • Calin Leafshade worked on a game that was nominated for an AGS Award!Calin Leafshade worked on a game that won an AGS Award!
I made a tutorial on how to make custom structs in C++!

I would appreciate it if some C++ type person could go through it and maybe tell me if I messed up anywhere with my definitions and explanations.

GO!

What you will need:

- A c++ compiler and some basic knowledge on how to use it. I suggest using Visual C++ Express from Microsoft since it is nice and comprehensive but also free.
- The template here: http://www.thethoughtradar.com/AGS/strippedplugintemplate.zip
- No... that's pretty much it.

What *is* a managed object

Without getting too technical about memory usage, a managed object (struct) is basically something that you can declare a pointer to. Built-in examples include DynamicSprite, Character, DrawingSurface and so on.

Why would I want one?

One advantage is to allow your code to be neater and more object oriented. Consider the following example:

[code]
struct Particle{
   int posX;
   int posY;
   int posZ;
   int colorRed;
   int colorBlue;
   int colorGreen;
   int colorAlpha;
   int spriteNumber;
};
[/code]

Do you see a problem here? Wouldn't it be nicer if we could do this instead:

[code]
struct Particle{
   Point3D *position;
   Color *color;
   int spriteNumber;
};
[/code]

Then, when accessing the members of the struct it looks like this:

[code]
//'part' is an instance of the Particle struct
part.color.red = 10;
part.position.x = 32;
[/code]

Another advantage is that it allows you to pass whole structs from functions like this:

[code]
Color *GetColorFromAGSColor(int agsColor){

Color *toReturn = Color.CreateColor(agsColor);
return *toReturn;

}
[/code]

Ofcourse this function is pretty stupid, you get the idea..

Without the color struct you would need 3 different functions for the red, green and blue components (or a selector of some kind) which is inefficient and messy.

Advantage number 3 is that it allows you to nest structs within one another like the Particle example above.

Ok Fine, I am still reading, show me how

Make a project!

First you'll need to get the plugin API header and example file that is linked above. This is a custom version of the demo given on the AGS site which removes all the superfluous stuff in there for demo purposes. Make a new  empty project in VC++ as a Win32 DLL and name it something beginning with the letters AGS. AGS won't load your plugin unless it starts with AGS.

Import the header and the source file (the header is the .h and the source file is the .cpp) into the project.

In MSVC++ this will compile out of the box with no editing. Try and compile it. If it compiles and produces a .dll file in the 'Debug' folder of your project folder then you are good to start coding.

Defining the struct!

Near the top of the source file you will find a section that looks like this:

[code]
const char *ourScriptHeader =
   "//script header for plugin\r\n"
   "//This is my script header\r\n";
[/code]

This is the header that we will give to AGS. The syntax used is just AGS script so you can put anything in here that you could put in AGS script.
the '\r\n' parts just tell the compiler that its a new line. Look up 'Escape Characters' for more information.

Ok so lets define the simple struct that we are going to be making:

[code]
const char *ourScriptHeader =
   "managed struct Point2D {\r\n"
   "      import attribute int X;\r\n"
   "      import attribute int Y;\r\n"
   "      import void Add(Point2D *toAdd);\r\n"
   "      import void Subtract(Point2D *toSub);\r\n"
   "       import static Point2D* Create(int X, int Y);\r\n"
   "};\r\n";
[/code]

Let's go line by line.

The first line of the struct definition tells AGS that its a *managed* struct called Point2D
The second and third lines define 'attributes' X and Y. Attributes are variables with accessors functions so they are got and set by functions in the script and not directly. (I'll show you how later)
The fourth and fifth lines define new functions for us to use. Notice that they take a single variable which is another Point2D pointer.
The sixth line defines a static function called create which allows us to create a new instance of our struct. AGS doesnt allow dynamic creation of managed objects from within the script so we have to use a function in C++ to create one for us and then return he whole struct to AGSScript.
And the last line just closes the struct, duh.

STRUCT DEFINED! LEVEL UP!

Define our C++ class to handle this

What we have done above just defines the struct in AGS. This is useless to C++. C++ spits on this information and asks for a proper definition. What an asshole.

SO lets do that before it gets violent and starts chucking chairs around.

Right click the project in the Solution Explorer in the top right of your screen and goto Add -> Class. Your screen may look different to mine since I'm using VS Studio not Express.



Now click Add (C++ Class should already be selected)

Enter 'Point2D' (no quotes) into class name and press OK or Next or To Infinity And Beyond! or whatever it asks you to press.. yes I closed the window.

You will be presented with a new source file and a new header file (Point2D.cpp and Point2D.h respectively). Open the header file if it's not already open.

It will currently look like this (or similar):

[code]
#pragma once
class Point2D
{
public:
   Point2D(void);
   ~Point2D(void);
};
[/code]

exciting huh.

The header file is just like a struct definition. It defines what the class *is* and what it *does* but not *how* it does it.

Let's define stuff.

[code]
#pragma once
class Point2D
{
public:
   Point2D(int x, int y);
   void Add(Point2D *obj);
   void Subtract(Point2D *obj);
   ~Point2D(void);
   int X;
   int Y;
};
[/code]

Ok line by line again:

Line 1: #pragma defines are used internally by the compiler for stuff. This one just tells the compiler to only include this header once.. you can ignore it.
2: This just defines the class name.
3: no, shut up.
4: This is really turning into a c++ primer but basically this means that all the declarations below this line are public and accessable from outside the class.
5: Now we're getting somewhere. This defines the 'constructor' of the class. This is the method which is called every time you make a new instance of this class. Notice there is no return type.
6 & 7: These are methods which will be contained in the class.
8: This is the DEconstructor and it's called everytime an instance of your class dies. YES DIES, YOU MONSTER.
9 & 10: This are variables in your class.
11: Closes the goddamn class.

(Dear C++ developers, Yes I know I could override the + and - operators but that is beyond the scope of this article and also I would have to look it up)

Right so we've defined the class. We should *implement* it now.

Open the Point2D.cpp source file which should look like this (similar):

[code]
#include "Point2D.h"

Point2D::Point3D(void)
{
}

Point2D::~Point3D(void)
{
}
[/code]

#include "Point2D.h" just tells the compiled to include the header file in this class.

The 'Point2D::' parts just mean that what follows are part of the Point2D class.

So let's implement stuff.

[code]
#include "Point2D.h"

Point2D::Point2D(int x, int y)
{
   X = x;
   Y = y;
}

void Point2D::Add(Point2D *obj)
{
   X += obj->X;
   Y += obj->Y;
}

void Point2D::Subtract(Point2D *obj)
{
   X -= obj->X;
   Y -= obj->Y;
}

Point2D::~Point2D(void)
{
}
[/code]

Most of this is self-explanatory but lets look at important things.

Firstly I have changed the constructor to match the definition in the header.

X = x; might look silly but notice that the cases are different.
The capital X refers to the X we defined in the header while the lower case x is the one passed by the constructor.
Basically it means (Make the object's X equal the one i was given).

The other two methods just add (or subtract) the passed object's X & Y values to the classes X & Y values.

The last method is just an empty deconstructor.. we dont have any memory that needs to be released in this class so nothing needs to be done here (look it up in a C++ book)

Including the Point2D files in the main source file

Go back to the main source file and add the following line just after #include "agsplugin.h"

[code]
#include "Point2D.h"
[/code]

This simply includes your Point2D class into the main source file.

Define accessor interfaces

Ok this part is a little tricky so pay attention.

AGS needs two interfaces to interact with your object. Specifically a 'ObjectReader' and a 'ManagedObject' interface.

ManagedObject Interface

Insert this code into your main source file just below the script header:

[code]
class ManagedPoint2DInterface : public IAGSScriptManagedObject {
public:
   ManagedPoint2DInterface() {};

   virtual int Dispose(const char *address, bool force) {
      delete address; // free the memory and return 1 to tell AGS we did.
      return 1;
   }
   virtual const char *GetType() {
      return typeName;
   }
   virtual int Serialize(const char *address, char *buffer, int bufsize) {
      
         memcpy(buffer, address, sizeof (Point2D));
         return sizeof(Point2D);
   }

   static const char* typeName;
};

const char* ManagedPoint2DInterface::typeName = "Point2D";
ManagedPoint2DInterface gManagedPoint2DInterface;

[/code]

This is a new class that derives from IAGSScriptManagedObject. The specifics of the mechanics of this are beyond the scope of this article but lets look at what happens.

The 'Dispose' method is run by AGS whenever an instance of your class has been mercilessly slaughtered once it's gone out of scope.
It passes 2 arguments, *address and force.
*address is simply a pointer to the object that it being destroyed. If you want to do anything to that object then you can cast the pointer to the class and work with it.
force is a bool which tells you whether or not this is your last opportunity to release the memory (on a game restore or something) If it is 'true' then you must delete the pointer or you will have a memory leak.
If you do delete the pointer then you must return 1 if not then return 0.

In the case of our very simple struct we just delete the pointer and return 1.

*GetType returns a pointer to a string with the name of the struct that this interface deals with. In our case it just returns a pointer to a static string "Point2D" which is assigned just after the class.

The Seralize method is where we save our object when the game is saved. Since our object is just a simple type we can just memcpy our object into the buffer provided and return how many bytes we wrote.

The last line defines a global reference to the interface that we can use to actually process our objects.

For most simple types you can copy the code above and just swap out all the names so don't worry if you dont understand the code too much.

ObjectReader Interface

The Reader interface is used to read an object back from the save file when a game is restored. It looks like this:

[code]
class ManagedPoint2DReader : public IAGSManagedObjectReader {
public:
   ManagedPoint2DReader() {}

   virtual void Unserialize(int key, const char *serializedData, int dataSize) {
      Point2D *newPoint = new Point2D(0,0);
      memcpy(newPoint, serializedData, sizeof(Point2D));
      engine->RegisterUnserializedObject(key, newPoint, &gManagedPoint2DInterface);
   }
};

ManagedPoint2DReader gManagedPoint2DReader;
[/code]

This interface is much simpler and just defines a single method which Unserializes your objects from a save file.

The first line of the method creates a pointer to a new Point2D and the next line assigns the serializedData memory to it which effectively loads your Point2D from the save file. AGS does all the hard work for you.
The second line registers the Object with the script engine which allows the scripts to access it again. The 'key' argument ensures that the memory is assigned to the correct pointer in the script.
The last line makes a global reference to the interface that AGS can use to deserialise our custom struct.

Putting it all together

Ok thats all the hard stuff out the way, now we just need to stich all the elements together!

Registering the object with AGS

In the main source file you should find a function called AGS_EngineStartup(IAGSEngine *lpEngine). This function is run, surprisingly enough, when the engine starts up. Add the following line to the end of the function:

[code]
engine->AddManagedObjectReader(ManagedPoint2DInterface::typeName, &gManagedPoint2DReader);
[/code]

This does pretty much what it says. It let's AGS know that there is a new managed object in town and it better make accomodations for it or else shit might go down.

Making Accessor Functions

Just above the AGS_EngineStartup function add the following:

[code]
void Point2D_set_X(Point2D* obj, int val) {
   obj->X = val;
}

int Point2D_get_X(Point2D* obj){
   return obj->X;
}

void Point2D_set_Y(Point2D* obj, int val) {
   obj->Y = val;
}

int Point2D_get_Y(Point2D* obj){
   return obj->Y;
}

void AddPoint(Point2D *obj, Point2D* obj2){
   obj->Add(obj2);
}

void SubtractPoint(Point2D *obj, Point2D* obj2){
   obj->Subtract(obj2);
}

Point2D* CreatePoint2D(int X, int Y) {
   Point2D *newPoint = new Point2D(X, Y);
   engine->RegisterManagedObject(newPoint, &gManagedPoint2DInterface); // the second parameter is the ManagedObject interface that is responsible for disposing of the object
   return newPoint;
}
[/code]

I hope that the first four of those functions are pretty straight forward. They just get or set a value in our Point2D object, a pointer to which is always passed at the first parameter to the function (i named it obj).
The 5th and 6th functions just call the Add and Subtract methods of our class.
The 7th function is our static Create function from the struct. It makes a new Point2D (passing the x and y variables it was given), registers that new object with AGS and tells it how to dispose of that object if it needs to and then returns the constructed point to your ags script.

All good? Excellent.

And FINALLY registering those accessor functions with AGS

The final step is to link our accessor functions with the functions defined in the script which is done in the AGS_EngineStartup function.

Just at the end of that function add the following:

[code]
   engine->RegisterScriptFunction("Point2D::Create^2", CreatePoint2D);
   engine->RegisterScriptFunction("Point2D::Add^1", AddPoint);
   engine->RegisterScriptFunction("Point2D::Subtract^1", SubtractPoint);
   engine->RegisterScriptFunction("Point2D::set_X", Point2D_set_X);
   engine->RegisterScriptFunction("Point2D::get_X", Point2D_get_X);
   engine->RegisterScriptFunction("Point2D::set_Y", Point2D_set_Y);
   engine->RegisterScriptFunction("Point2D::get_Y", Point2D_get_Y);
[/code]

The RegisterScriptFunction method takes two values. The first is the name of the AGS function we are registering which we need to look at in more detail.

Firstly notice the 'Point2D::' part. This tells AGS that the following function is part of the Point2D struct.
The number following the ^ symbol tells AGS how many parameters the function takes *not including* the object itself. For instance the 'Add' function takes the object that the function is acting on plus 1 more (the object to add to it in this case) so we append the function name with ^1.

"What about those get_ and set_ functions? They aren't in our struct. WHAT IS GOING ON!?"
Remember we talked about 'attributes' earlier? This is where they come into play. When you define an attribute AGS will use two magic functions to deal with that attribute. They are called get_varName and set_varName. If i try to retrieve a varible's value like this:

[code]
Point2D* p = Point2D.Create(1,1);
Display("%d", p.X); // <--- here
[/code]

AGS will run the Point2D::get_X method and return whatever value it returns. Likewise if I try and *set* a value it will run the set_X method and pass the value I give to it.
So those funny functions are used internally by AGS to access our values.

And thats it!

Compile and add your plugin to your game and you will have a brand new custom object to play with. Lucky you!

A final word on 'linking'

When you compile your plugin under the default settings it will 'dyamically' link to the libraries used by the MSVC++ compiler.. this isnt ideal since it will mean that your users will need that library installed on their computer. So let's fix that.

Firstly change your compiler to 'Release' (if you're using MSVC++, If you aren't i cant help you). You can do this here:


Yours might look different. Tell me and I'll update the tutorial.

Now right click your project in the Solution Explorer and click Properties.



Change the highlighted setting to "Multi-threaded (/MT)" This will include the basic libraries in with your plugin and save alot of people some trouble.

Recompile and your plugin will now be in the 'Release' folder of your project folder. Changing to Release also heavily optimises your plugins code and doesn't include all the debugging symbols and stuff. So hooray for that.

Have fun*!

*Fun involves C++ programming. Might not actually be fun.

Change History

1.0 - initial

1.1 - Fixed Unserialize method.
« Last Edit: 27 May 2011, 16:37 by Calin Leafshade »

Leon: You need the sword first before you can get the monkey.

Dualnames

  • AGS Baker
  • Dualnames worked on a game that was nominated for an AGS Award!
AMAZING!! That's really helpful, way to go Cal!  :D

Monsieur OUXX

  • Mittens Serf
  • Mittens Half Initiate
    • I can help with proof reading
    •  
    • I can help with translating
    •  
    • I can help with voice acting
    •  
Reviving this topic because I'm trying to follow the tutorial and also because it deserves it :)

I'm trying to follow the steps in Visual C++ Express 2010.
I have issues with the "out of the box" compiling.

Here is what I do :
- File-->New-->Project
- VisualC++ --> Win32 Project
- I give it a name starting with "AGS". Actually I call it "agsplugin" to match the name of the .cpp file given by Calin (by the way, does it have to be "AGS" upper case?)
- "Welcome to the Win32 application wizard" --> Next --> Application type = "DLL"
- I tick "Empty project" because I assume that the .cpp file provided by Calin is meant to be the main source file.
- In the newly created project tree : Add-->Existing Item.  Then I add the files provided by Calin for download at the beginning of this thread (the .h in the "headers" folder, and the .cpp in the "source" folder).

Then when I compile, it has issues with the STRINGIFY macro.
It says :
Code: Adventure Game Studio
  1. 1>c:\...\agsplugin\agsplugin\agsplugin.cpp(133): error C2146: syntax error : missing ')' before identifier 'STRINGIFY'
  2. 1>c:\...\agsplugin\agsplugin\agsplugin.cpp(133): error C2059: syntax error : ')'
  3.  

The faulty line is :
Code: Adventure Game Studio
  1. engine->AbortGame("Plugin needs engine version " STRINGIFY(MIN_ENGINE_VERSION) " or newer.");
  2.  
And indeed, I wonder how a macro would manage to concatenate C strings in that manner, but I'm going to trust Calin, as I've seen some crazy witchcraft achieved with macros.

So, what's the catch? To I need to include to some obscure new Microsoft header?
« Last Edit: 17 Jun 2012, 19:25 by Monsieur OUXX »

Crimson Wizard

  • AGS Project Admins
  • not et suppotreD
    • I can help with translating
    •  
Quote from: Calin Leafshade
*Fun involves C++ programming. Might not actually be fun.
Hahaha, depends on how you look at that :D

From what I see in the AGS code right now, there's only one place where this macro is defined: AGSBlend plugin (by Steven Poulton :D).

I believe you should add these declarations:
Code: Text
  1. #define STRINGIFY(s) STRINGIFY_X(s)
  2. #define STRINGIFY_X(s) #s
  3.  

EDIT: or use C sprintf function to format string

Code: Text
  1. char longstr[100];
  2. sprintf(longstr, "Plugin needs engine version %d or newer.", MIN_ENGINE_VERSION);
  3. engine->AbortGame(longstr);
  4.  
WARNING: unsafe code, do not use that at home (hypothetically may cause overflow).
« Last Edit: 17 Jun 2012, 17:27 by Crimson Wizard »

Calin Leafshade

  • AGS Project Admins
  • Long live King Cat!
    • I can help with AGS tutoring
    •  
    • I can help with voice acting
    •  
  • Calin Leafshade worked on a game that was nominated for an AGS Award!Calin Leafshade worked on a game that won an AGS Award!
Sorry, I must've stripped out the macro but forgot it was still used there. Either solution given by CW is fine tho.

Leon: You need the sword first before you can get the monkey.

Monsieur OUXX

  • Mittens Serf
  • Mittens Half Initiate
    • I can help with proof reading
    •  
    • I can help with translating
    •  
    • I can help with voice acting
    •  
Since we're there, there's a teeny weeny copy-paste mistake in Calin's original post: You wrote "Point2D::Point3D" instead of "Point2D::Point2D" (and again with the destructor).

Please don't forget my question: Is it important if "AGS" at the beginning of the plugin's name is upper case or lower case?
« Last Edit: 17 Jun 2012, 19:24 by Monsieur OUXX »

Calin Leafshade

  • AGS Project Admins
  • Long live King Cat!
    • I can help with AGS tutoring
    •  
    • I can help with voice acting
    •  
  • Calin Leafshade worked on a game that was nominated for an AGS Award!Calin Leafshade worked on a game that won an AGS Award!
Please don't forget my question: Is ti important if "AGS" at the beginning of the plugin's name is upper case or lower case?

I don't think it matters, certainly not in windows. However, the convention is that they should be lower case.

Leon: You need the sword first before you can get the monkey.

Monsieur OUXX

  • Mittens Serf
  • Mittens Half Initiate
    • I can help with proof reading
    •  
    • I can help with translating
    •  
    • I can help with voice acting
    •  
I also forgot to mention that "AGS_EngineDebugHook" is supposed to return an int, and in the present version there is no "return" statement.
I've added "return 0;" as a workaround, but I'm not sure what it's supposed to return. I'd rather return the correct value as the .cpp file I'm producing is meant to be distributed in my next plugin (I need it clean).

Calin Leafshade

  • AGS Project Admins
  • Long live King Cat!
    • I can help with AGS tutoring
    •  
    • I can help with voice acting
    •  
  • Calin Leafshade worked on a game that was nominated for an AGS Award!Calin Leafshade worked on a game that won an AGS Award!
That is for handling debug hooks (surprisingly). You should return 1 if the plugin has handled debugging the script line and 0 if it has not.

Its only run if you have requested the hook anyway.

Leon: You need the sword first before you can get the monkey.