Keyword: attribute

From Adventure Game Studio | Wiki
Jump to navigation Jump to search

For some time people have wondered about this attribute keyword that shows up in the autocomplete list. It isn't documented anywhere, but that comes with good reason. It is not supported for use.

Now I've never been one to let something so silly as that dictate whether or not I use something. For those of you who like to "play it safe" I suggest you just turn back now, while you still can. As for the rest of you, let's take a look at what this keyword does. Due to the complexity of this article, I recommend that only advanced users with a firm grasp on scripting concepts try to wrap your minds around this. All I'm saying is I'm not responsible for any sprained cerebrums.

OO Programming, Encapsulation, and Everything

For those who aren't familiar with the OO programming concept of encapsulation, Wikipedia has an article on it. We'll just scrape the surface of what it really is. You should have some notion of how to use a struct in AGS.

The basic idea of encapsulation (for the intents and purposes of this article at the least) is that you are going to protect your data structure (struct) by using methods (functions) to get and/or set the properties instead of making them directly readable/writable. So for example, the example struct given in the manual might look more like this:

 struct Weapon {
   protected int damage;
   import int GetDamage();
   import void SetDamage(int);
   protected int price;
   import int GetPrice();
   import void SetPrice(int);
   protected String name;
   import String GetName();
   import void SetName(String);
 };
 
 int Weapon::GetDamage() {
   return this.damage;
 }
 
 void Weapon::SetDamage(int damage) {
   this.damage = damage;
 }
 
 // ...etc...

Why would you want to do this you ask? Let's say for example that you want to make sure that the damage property of your Weapons is always a positive (or 0) value. If the user could directly set sword.damage = -15; then you would have no way to prevent the property being changed. To further extrapolate this problem if the user of this code is defining dynamic instances of this struct you wouldn't even be able to authenticate the data using repeatedly_execute (short of forcing them to call an Update method every loop).

By encapsulating the property you can make sure that the user is supplying a valid value before storing it into the instance:

 void Weapon::SetDamage(int damage) {
   if (damage < 0) damage = 0;
   this.damage = damage;
 }

Now if the user supplies an invalid value for the damage it will be replaced with 0. This makes sure that the damage does not get set to a negative value.

writeprotected

The use of getter functions in AGS is somewhat redundant due to the existence of the writeprotected keyword. This keyword allows us to define a property that the user can read but they cannot assign a value to.

NOTE: There is a readonly keyword, but if you use that then even you can't change the value! writeprotected means that only methods of this struct (like our setter method) can change the value.

Using the writeprotected keyword our struct definition might look something more like this:

 struct Weapon {
   writeprotected int Damage;
   import bool SetDamage(int damage);
   writeprotected int Price;
   import bool SetPrice(int price);
   writeprotected String Name;
   import bool SetName(String name);
 };

So all-in-all we now have a pretty decent method of protecting our data while still making it accessible to the user. However, it's not as easy for the user to set the data now. Consider for example if you wanted to decrease the price of a weapon. Originally we could have done:

 sword.price -= 50; // apply coupon

But now in order to make sure that the value remains positive we would have to do something more like:

 sword.SetPrice(sword.Price - 50);

We now have to reference the property twice (once via the setter method and then again via the property itself) to use this functionality. The use of the attribute keyword can help alleviate this problem.

attribute

The keyword attribute is actually comparable to the C# idea of properties, though the actual implementation is of course different. An attribute gives us the ability to encapsulate our properties so we can protect our data without losing the ease of access that just using properties grants.

NOTE: The information hereafter is NOT officially supported by AGS and is subject to change at any time. If you use this method be advised that it may not work with future versions of AGS. The latest AGS version as of this writing is AGS 3.2 RC 3 which has been used for testing purposes. This method may work for versions as early as 2.7, but again since it's unsupported there are no guarantees!

Defining an attribute

An attribute is declared more like a method than a property. You must also supply two functions for each attribute, a getter and a setter, named get_XXX and set_XXX respectively where XXX is the name of the attribute.

 struct Weapon {
   import attribute int Damage;
   import int get_Damage();
   import void set_Damage(int damage);
   import attribute int Price;
   import int get_Price();
   import void set_Price(int price);
   import attribute String Name;
   import String get_Name();
   import void set_Name(String name);
 };

That's our basic structure for defining the attributes. However, we now no longer have anywhere to store the data! Attributes are virtual properties. You can't actually store data in them. Instead we need to add one additional item, a property, for each attribute:

 struct Weapon {
   protected int damage; // this is our actual property to store the damage
   import attribute int Damage; // this of course is our attribute
   import int get_Damage();
   import void set_Damage(int damage);
   protected int price;
   import attribute int Price;
   import int get_Price();
   import void set_Price(int price);
   protected String name;
   import attribute String Name;
   import String get_Name();
   import void set_Name(String name);
 };
 
 int Weapon::get_Damage() {
   return this.damage; // return the data stored in the actual property
 }
 
 void Weapon::set_Damage(int damage) {
   if (damage < 0) damage = 0; // make sure the data is safe for this property
   this.damage = damage;
 }
 
 // ...etc...

Okay, so now we just have to call these get and set functions? I think I missed something here...

Don't get ahead of me! The advantage of using an attribute, as I said, is it lets us use the getter and setter functions implicitly, letting us treat the attribute as if it were a property:

 Display("Weapon name: %s", sword.Name); // calls sword.get_Name() automatically
 sword.Damage = 5; // calls sword.set_Damage(5) automatically
 sword.Price -= 50; // calls sword.set_Price(sword.get_Price() - 50) automatically

Well that's easy! ...but what about these get and set methods...I don't want the user to know about them.

AGS has a way of hiding values from the autocomplete so that you can implement these functions without making them show up in the autocomplete list for your struct:

 struct Weapon {
   protected int damage;
   import attribute int Damage;
   import int get_Damage(); // $AUTOCOMPLETEIGNORE$
   import void set_Damage(int damage); // $AUTOCOMPLETEIGNORE$
   protected int price;
   import attribute int Price;
   import int get_Price(); // $AUTOCOMPLETEIGNORE$
   import void set_Price(int price); // $AUTOCOMPLETEIGNORE$
   protected String name;
   import attribute String Name;
   import String get_Name(); // $AUTOCOMPLETEIGNORE$
   import void set_Name(String name); // $AUTOCOMPLETEIGNORE$
 };

We don't need to hide the properties because the protected keyword will already make sure they don't show up outside of our member functions of the struct. Later we will see an even better method of masking these methods. Next we're going to take a look at another kind of attribute.

Array attributes

Another advantage of attributes is that it grants us the ability to implement a dynamically sized array of attributes within a struct. In fact, an attribute array can only be declared without a static size. Array attributes use geti and seti methods so that an index can be specified:

 struct Array {
   import attribute int Data[];
   import int geti_Data(int index); // $AUTOCOMPLETEIGNORE$
   import void seti_Data(int index, int value); // $AUTOCOMPLETEIGNORE$
 };

The problem here is that we can't currently implement a dynamically sized array of properties (as a member of a struct). This brings us back to the point where we don't have anywhere to store our data. If you are only providing a specific set of instances to the end user then you could create a (set of) dynamic array(s) in your script (outside of the struct definition; not in the header) with the warning that creating new instances of the struct will not work.

Or if you're using a static sized array but still want to provide the encapsulation that an attribute would provide you could declare the property with the static size needed:

 #define MAX_PEOPLE_COUNT 20 // max 20 people
 
 struct People {
   protected String names[MAX_PEOPLE_COUNT];
   import attribute String Names[];
   import String geti_Names(int index); // $AUTOCOMPLETEIGNORE$
   import void seti_Names(int index, String name); // $AUTOCOMPLETEIGNORE$
   readonly import attribute int Count;
   import int get_Count(); // $AUTOCOMPLETEIGNORE$
 };
 
 String People::geti_Names(int index) {
   if ((index < 0) || (index >= MAX_PEOPLE_COUNT)) return null; // invalid index specified
   return this.names[index];
 }
 
 void People::seti_Names(int index, String name) {
   if ((index < 0) || (index >= MAX_PEOPLE_COUNT)) return;
   if (String.IsNullOrEmpty(name)) name = "John Doe";
   this.names[index] = name;
 }
 
 int People::get_Count() {
   return PEOPLE_COUNT;
 }

Now there's a lot going on here, so we'll break it down and I'll try to explain. PEOPLE_COUNT is a simple #define to tell us the maximum number of people, and define the size of the actual array of properties. Next we define the array (of properties), the attribute array (to encapsulate the real array), and the getter and setter methods for the array. This should all be straightforward enough at this point.

The only difference between an array attribute and a normal attribute is the fact that the getter and setter methods have the i in their name and take an int parameter (as the first parameter) to define the given index. This will be whatever index the user specified, so how you handle invalid values is up to you. Here we're being pretty lenient, but if this was a built-in array then it would be considered a fatal error (crashing the game).

Next we have a Count attribute. This attribute works a little bit differently because we aren't using a property for the data, we're using the #define PEOPLE_COUNT. Using an attribute here means that not only do you not have to initialize Count for any instances you may provide to the user, but if the user declares a new instance, Count will still be set to the same value.

But what about that readonly? Didn't you say earlier that we shouldn't be using that and should use writeprotected instead?

There are some interesting implications behind the usage of readonly or writeprotected in this context. We will actually cover that later, but note that since we are never setting a value they provide the same function here.

A VERY important point to make here is that you can NOT use an attribute in the same script where the getter and setter methods are defined. If you try, you'll get an error like this:

 ---------------------------
 Adventure Game Studio
 ---------------------------
 An internal error has occurred. Please note down the following information.
 If the problem persists, post the details on the AGS Technical Forum.
 (ACI version 3.20.1101)
 
 Error: is_script_import: NULL pointer passed
 
 ---------------------------
 OK   
 ---------------------------

Not particularly friendly as it doesn't actually tell you what's going on, leaving you to ask, "WTF?!?" Just note again that this is all unsupported technology so if you did go post in the Technical Forum you're likely to get asked what you think you're doing...In case you do see that error try looking for cases where you've used an attribute in the same script where its accessor functions are defined. If you need access to that value, use the property (or whatever else is storing the data) or call the get or set function directly.

Now that we've seen how to define both regular and array attributes, let's take a look at a better way of handling the accessor methods.

Extender methods are better accessors

Oh shoot I gave it away. That's right kids, extender methods lend a hand again. So far we've been using the autocomplete trick of $AUTOCOMPLETEIGNORE$ to mask our accessor functions from the user. This works, but it still means that the functions are globally accessible and we have to reference them in the script header.

NOTE: Extender methods are of course only available to AGS 3.0 and higher.

Extender methods save us the trouble of having to put those pesky references in the header as well as having to mask them. So using extenders, let's take a look at our old stand-by:

 // Script.ash (header file)
 struct Weapon {
   protected int damage;
   import attribute int Damage;
   protected int price;
   import attribute int Price;
   protected String name;
   import attribute String Name;
 };
 
 // Script.asc (main script file)
 int get_Damage(this Weapon*) {
   return this.damage;
 }
 
 void set_Damage(this Weapon*, int damage) {
   if (damage < 0) damage = 0;
   this.damage = damage;
 }
 
 // ...etc...

Remember how I said that if you use an attribute in the same script as its accessors are defined you'd get that nasty error message? If you're using extender methods then the error would look more like this instead:

 ---------------------------
 Adventure Game Studio
 ---------------------------
 Script link failed: Runtime error: unresolved import 'Weapon::get_Damage'
 
 ---------------------------
 OK   
 ---------------------------

This is the same error you'd get if the accessor wasn't defined at all so it might be confusing, but again...unsupported technology. Just know that if you get this error message and you're sure the function is defined it means you're using the attribute where you shouldn't be.

readonly vs. writeprotected attributes

Okay, I said we'd come back to this, so here we are. We've already seen an implementation of a readonly attribute, so let's see what a writeprotected attribute looks like:

 struct Some {
   protected int thing;
   writeprotected import attribute int Thing;
 };
 
 int get_Thing(this Some*) {
   return this.thing;
 }
 
 void set_Thing(this Some*, int thing) {
   this.thing = thing;
 }

Okay that looks normal...but wait a second! It's writeprotected...why are you defining a set method at all?

That's the difference between readonly and writeprotected attributes. writeprotected means that methods of this struct can assign a value whereas readonly means that a value can never be explicitly assigned. Since a writeprotected attribute can have a value assigned at some point, we need a setter method. Otherwise we'd get an error that it's missing.

If I can't use the attribute in the same script as the accessor methods though, where am I supposed to assign this attribute a value that it would need to be writeprotected for?

You can't use the attribute in the same script. However, it is possible for functions declared as part of the struct to be defined in a different script or for extender methods to be defined to extend the struct. Really if you're assigning a value then you could probably just assign it directly into the property, but this may not always be the case. If your struct is extensible you may want to consider using writeprotected instead of readonly when defining attributes. Otherwise readonly saves you having to define the setter.

Static attributes

One feature that has been largely requested is the ability to define static properties within a struct. Using an attribute will actually allow you to simulate this same functionality:

 struct Some {
   import static attribute int Thing;
 };
 
 int Some_Thing; // since the attribute is static adding a property to the struct doesn't make sense
 
 int get_Thing(this Some*) { // even though it's static we can still use extenders to define the accessors
   return Some_Thing;
 }
 
 void set_Thing(this Some*, int thing) {
   Some_Thing = thing;
 }
 
 // Meanwhile, in some other script...
 
 Some.Thing = 42;

If we really wanted to, we could even throw it all together in some sort of horrible act of proof-of-conceptuatlity:

 struct Some {
   readonly import static attribute int Things[];
   readonly import static attribute int ThingCount;
 };
 
 int Things[MAX_THINGS];
 
 int geti_Things(int index) {
   if ((index < 0) || (index > MAX_THINGS)) return 0;
   return Things[index];
 }
 
 int get_ThingCount() {
   return MAX_THINGS;
 }

Not that horrible actually, I'm just suffering an acute lack-of-original-imaginative-thought-for-clever-examples attack at the moment.

Closing and some sort of disclaimer

So that's what attributes are and how they're used in AGS. Again NONE of this attribute stuff is supported. If you choose to use it, be advised you are doing so at your own risk. I assume no responsibility for anything that may come of reading this article. Unless of course the something in question measures above a 9.5 on the awesomometer in which case I accept cash and PayPal. Until next time. -Monkey 05 06 02:24, 21 January 2010 (UTC)