Wednesday, August 17, 2011

Wrapping a wrap

What would be the point of having developer's blog, if there wouldn't be anything on programming. Oh yeah, that's where the things got hot. This is where it works, this is where it fails.

Since launch of the SDK, probably lots of people started struggling with that fancy scripting language - AngelScript. I remember I did. Experienced some successes, failures, helped finding some bugs, worked my way out through this language, started appreciating it, started even liking it (now I see it was indeed quite a good choice). That's why I think we could from time to time talk about scripting in FOnline SDK, talk about the AngelScript, share some common pitfalls, solutions that worked out good, and even the ones that weren't.

Of course basics of angelscripting is required, there won't be any tutorials whatsoever.

Objects

Blah blah blah, everything you can model in your OO language is an object, yeah, we already know that and it sucks (I'm getting a little bit off topic right here). Ok, it does not suck, in fact we're gonna often rely heavily on that fact, and we're gonna put large part of our logic into objects. And we're gonna tie those objects to the in-game objects (critters, items, scenery, maps, locations).

Say we've got a class for objects that are gonna make us some nice abstraction layer over default item functionality:

class SuperFoo

{
Item@ item;
SuperFoo(Item& item)
{
@this.item = item;
}

void Bar()
{
item.Val0++;
}
}

What's going on here? We wrap an item type in some class, and we're adding some useless code to operate on it? Why the hell would we want it? Well, think about the interface you've just gained and you can use throughout your scripts. You may now call: something.Bar() instead of item.Val0++, which shows what you want to do, and not how are you going to do. That's the difference if you want to write good code. Less imperative, less verbosity, less bugs (yes, I know that example suck, and you would want that for far more reasons - think about having some additional data, that's not stored on the wrapped item, but it's one of the class' fields - it's not saved anywhere, but still may be useful).

Wait, did someone say interface?
interface ISuperFoo

{
void Bar();
}

Oh great, now we've got the interface that allows us to call Bar(). And we don't even know what's called in the implementation class (in fact, we don't care, this is what interfaces are for).

So far so good, but FOnline scripting engine works in a way, that you can attach your functionality to the handlers for default events.

Let's start of course, from creating an object. If we want to attach it to the item, we need to do this in the item_init handler.
void item_init(Item& item, bool firstTime)

{
SuperFoo@ foo = SuperFoo(item);
}

Yeah, whoops. We're gonna loose the reference to the newly created foo object as soon as we leave the function. Unacceptable, we need to store it somewhere.

Object Managers

This is also common solution. If you've got many objects in your program, you need to manage them somehow, store the references to them, maintain their lifetime, give the references(via interaface) to those interested.
We're gonna use basic generic array type for that.

array<superfoo@> Foos;

ISuperFoo@ GetFoo(const Item& item)
{
for(uint i = 0; i < Foos.length(); i++)
{
if(Foos[i].item.Id == item.Id)
return Foos[i];
}
return null;
}
void AddFoo(Item item)
{
Foos.insertLast(SuperFoo(item));
}

Oh what a horrible piece of code we've just made. Yes, we've got our array, and function to add an object for an item (which we can use in item_init) and get the interface of such object for existing item. But there is that horrible loop in GetFoo, we need to scrap that. The best would be, if we could perform fetch with direct index to our array. We can do that, if we're able to store the index, at which our Foo is located in the array. Let's store it in Item::Val1 field:
void AddFoo(Item& item)

{
Foos.insertLast(SuperFoo(item));
item.Val1 = Foos.length() - 1; // here we've got index to our array stored
}
ISuperFoo@ GetFoo(const Item& item)
{
if(item.Val1 < Foos.length())
return Foos[item.Val1];
else
return null;
}

and our item_init will now look like:
void item_init(Item& item, bool)

{
AddFoo(item);
}

So, we're now adding our SuperFoo object every time we call item_init, and we can fetch it in any script module (any module that imports this function of course), by obtaining an interface, that's not coupled in any way to the implementation class, nor coupled to the item object. Nifty.

We can use our interface in following manner:
void item_init(Item& item, bool)

{
AddFoo(item);
item.SetEvent(ITEM_EVENT_USE, "_FooUse");
}
bool _FooUse(Item& item, Critter& crit, Critter@ onCritter, Item@ onItem, Scenery@ onScenery)
{
ISuperFoo@ foo = GetFoo(item);
if(valid(foo)) foo.Bar();
}

The _FooUse function is where this solution shines. We call our method via interface(we may even pass some parameters given to us by the engine), and we do not care of what's going on inside.

Now, there is still something wrong with that solution, first who find out what, gets a candy!