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 AcediaObject
s 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 AcediaObject
s' 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 theMutableText
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 withself.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.