AcediaObject

AcediaObject is a base class for all objects in Acedia and it is meant to enable: easy access to Acedia's API through globals, object allocation and release, constructors and finalizers. We'll go over each of these in detail.

Globals and Acedia's API

Globals (clases Global, ServerGlobal and ClientGlobal) is our way of solving the problem of adding an easy access to our new global functions. Examples of global functions are Log(), Caps(), Abs(), VSize() and a multitude of others that you can call from anywhere in UnrealScript. They can be accessed from anywhere because they are all declared as static methods inside Object - a base class for any other class. Problem is, since we cannot add our own methods to the Object, then we also can't add new global functions. The best we can do is declare new static methods in our own classes, but calling them would be cumbersome: class'glb'.static.DoIt().

Idea that we've used to solve this problem for Acedia is to provide every single AcediaObject with an instance of a class that would contain all our global functions. We can then save an instance of this class in a local variable _, which would allow us to simply write _.DoIt().

In actuality we don't just dump all of Acedia's global functions into one variable, but instead group them into a kind of namespaces:

_.text.FromString("I am here!");        // Text API
_.alias.ResolveColor_S("blue");         // Alias API
_.collections.EmptyArrayList();         // Collections API
_.memory.Allocate(class'SimpleSignal'); // Memory API

_ can't be accessed in static methods, since only default values are available inside them. Since writing default._ would also be bulky, AcediaObject provides a static method public static final function Global __() that is always available:

__().text.FromString("I am here!");
__().alias.ResolveColor_S("blue");
__().collections.EmptyArrayList();
__().memory.Allocate(class'SimpleSignal');

Allocation and deallocation

The majority, if not all, of the Acedia's objects you will be using are going to be created by specialized methods like _.text.FromString(), _.collections.EmptyArrayList() or _.time.StartTimer() and can be deallocated with self.FreeSelf() method. However, if you want to allocate instances of your own classes, you'll need the help of MemoryAPI's methods: _.memory.Allocate() and _.memory.Free().

Ultimately, all Acedia's objects must be created with _.memory.Allocate() and destroyed with _.memory.Free(). For example, here is how new Parser is created with _.text.NewParser():

public final function Parser NewParser()
{
    return Parser(_.memory.Allocate(class'Parser'));
}

and self.FreeSelf() is actually defined in AcediaObject as follows (ignore parts about life versions for now, they will be explained in sections below):

public final function FreeSelf(optional int lifeVersion)
{
    if (lifeVersion <= 0 || lifeVersion == GetLifeVersion()) {
        _.memory.Free(self);
    }
}

What is it exactly that Allocate() and Free() methods do?

Reference counting

Lets start with Free(). In earlier versions of Acedia, _.memory.Free() / self.FreeSelf() methods immediately deallocated provided AcediaObject. However that appeared to be a bad design decision, making it harder to understand who was supposed to deallocate what objects and when.

Now calling _.memory.Free() or self.FreeSelf() does not necessarily deallocate AcediaObject, but simply tells it that you no longer store its reference, reducing its internal reference count. AcediaObject is only deallocated once its reference count reaches zero.

Most objects are only used by whatever class allocated them and have reference count of 1 throughout their lifetime. However they become necessary when AcediaObjects are stored somewhere else, most common example being a collection:

local ArrayList myArray;
local Text      myText;

myText = _.text.FromString("hello, world!");
//  ^ here `myText` has reference count of 1
myArray = _.collections.EmptyArrayList().AddItem(myText)'
//  ^ we've just put `myText` into collection,
//  which increased its reference count to 2
myText.FreeSelf();
myText = none;
//  ^ we no longer have a direct reference to that `Text`, but object lives on
//  inside `myArray` collection with reference count of 1
myArray.RemoveIndex(0);
//  ^ now we've also removed it from the collection, once again decreasing its
//  reference count and causing it to get deallocated

Reference count isn't magically tracked by itself. Just as we have to manually reduce it with _.memory.Free() / self.FreeSelf() methods, to increase reference count we have to call self.NewRef() method whenever we want to store AcediaObject instance in some place else. In particular, Acedia's collections are doing it automatically when you add objects inside them.

Who is responsible for releasing references?

For both historical reasons and what seemed like a good idea in practice, two main rules for managing AcediaObjects' references arose:

  • If function returns an object (as a return value or as an out argument) - then whoever called that function is responsible for releasing its reference. If you've called _.text.Empty(), then you must release the MutableText object it has returned. Conversely, if you are implementing function that returns an object, then you lose the reference you've had: you need to either forget a reference to that object after returning it or, if you want to retain your reference, create a new reference with self.NewRef() method first.
  • Functions do not release their arguments. If you pass an object as an argument to a function - you can expect that said object won't be release during function's execution. When implementing your own function - you should not release objects passed as its arguments.

However, these guidelines should be treated as default assumptions and not hard rules. First guideline can be sometimes broken for convenience. For example, EPlayer class has method BorrowConsole() that returns ConsoleWriter for a quick access to player's console. If we had to release returned object, then we'd have to do something like:

local ConsoleWriter writer;

writer = player.BorrowConsole();
writer.UseColorOnce(_.text.Green).WriteLine_S("All is fine!");
writer.FreeSelf();

However this particular method does not return you a new reference and expects you to not release returned ConsoleWriter object, making following code not leak any memory:

player.BorrowConsole().WriteLine_S("All is fine!");

Moreover, another common scenario in which returned object should not be released is when a method returns the caller object itself to allow for method chaining:

player.BorrowConsole().UseColorOnce(_.text.Green).WriteLine_S("All is fine!");

Another example:

local int a, b, c;
local Parser parser;
parser = _.text.Parse_S("78 23 -34");
//  Each call returns `parser`, but...
parser.MInteger(a).Skip().MInteger(b).Skip().MInteger(c);
//  ...`parser` only needs to be released at the very end
parser.FreeSelf();

Such methods always specify that they return objects for method chaining.

Second guideline also has certain exceptions. Obvious one is _.memory.Free() that releases reference one passes to it. Other notable ones are _.text.IntoString() that releases BaseText reference passed to it, returning string instead and Arg() method in Logger class that always releases passed BaseText reference for convenience's sake.

Such exceptions are relatively rare and always documented in the method's description. Methods that break first guideline also usually start with Borrow... prefix and, whenever possible, are made to be error-tolerant if you do release returned reference.

Object pools

To reuse deallocated objects we need to store them somewhere, until they are required again. This idea isn't new and UnrealScript already tried to tackle issue of reusing once created objects: class ObjectPool stores unused objects (mostly resources such as textures) inside dynamic array until they are needed. Unfortunately, using a single ObjectPool for a large volume of objects is impractical from performance perspective, since it stores objects of all classes together and each object allocation from the pool can potentially require going through the whole array:

//  FILE: Engine/ObjectPool.uc
simulated function Object AllocateObject(class ObjectClass)
{
    local Object    Result;
    local int        ObjectIndex;

    for(ObjectIndex = 0;ObjectIndex < Objects.Length;ObjectIndex++)
    {
        if(Objects[ObjectIndex].Class == ObjectClass)
        {
            Result = Objects[ObjectIndex];
            Objects.Remove(ObjectIndex,1);
            break;
        }
    }

    if(Result == None)
        Result = new(Outer) ObjectClass;

    return Result;
}

For that reason Acedia uses a separate object pool (implemented by AcediaObjectPool) for every single class, making object reallocation as trivial as grabbing the last stored object from AcediaObjectPool's internal dynamic array:

//  From `AcediaObjectPool` sources
public final function AcediaObject Fetch()
{
    local AcediaObject result;
    if (storedClass == none)    return none;
    if (objectPool.length <= 0) return none;

    result = objectPool[objectPool.length - 1];
    objectPool.length = objectPool.length - 1;
    return result;
}

New pool is automatically prepared for every class you create, as long as it is inherited from AcediaObject. AcediaActors do not use object pools and are to be simply Destroy()ed.

Constructors and finalizers

Both AcediaObject and AcediaActor support constructors and finalizers. Constructor is a method that's called on object after it was created, preparing it for use. Finalizer is a method that's called when object is deallocated and can be used to clean up any used resources.

NOTE: Technically, right now destructor might be a better terminology for Acedia's finalizers.

A good and simple example is from the ATradingComponent that allocates necessary objects inside its constructor and deallocates them in its finalizer:

protected function Constructor()
{
    onStartSignal   = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onEndSignal     = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
    onTraderSelectSignal = Trading_OnSelect_Signal(
        _.memory.Allocate(class'Trading_OnSelect_Signal'));
}

protected function Finalizer()
{
    _.memory.Free(onStartSignal);
    _.memory.Free(onEndSignal);
    _.memory.Free(onTraderSelectSignal);
    onStartSignal           = none;
    onEndSignal             = none;
    onTraderSelectSignal    = none;
}

Acedia's constructors do not take parameters and because of that some classes also define Initialize() method that is required to be used right after an object was allocated.