Anton Tarasenko
3 years ago
4 changed files with 679 additions and 19 deletions
@ -0,0 +1,61 @@ |
|||||||
|
# Acedia's API |
||||||
|
|
||||||
|
Acedia's API is our way of solving the problem of adding 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 object, |
||||||
|
including actors. |
||||||
|
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 define |
||||||
|
a separate class that would contain all our global functions. |
||||||
|
If we save an instance of this class in some local variable |
||||||
|
`glb`, then we can simply write `glb.DoIt()` instead of |
||||||
|
`class'glb'.static.DoIt()`. |
||||||
|
|
||||||
|
In actuality we don't just dump all of Acedia's global functions into |
||||||
|
one object, but group them into different APIs that can be accessed |
||||||
|
through `_` variable, defined in every `AcediaObject` and `AcediaActor`: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
_.text.FromString("I am here!"); // Text API |
||||||
|
_.alias.ResolveColor("blue"); // Alias API |
||||||
|
_.collections.EmptyDynamicArray(); // Collections API |
||||||
|
_.memory.Allocate(class'SimpleSignal'); // Memory API |
||||||
|
``` |
||||||
|
|
||||||
|
`_` can't be accessed in static methods, since only default values are |
||||||
|
available in them. |
||||||
|
Since writing `default._` would also be bulky, `AcediaObject` and `AcediaActor` |
||||||
|
provide a static method `public static final function Global __()` |
||||||
|
that is always available: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
__().text.FromString("I am here!"); |
||||||
|
__().alias.ResolveColor("blue"); |
||||||
|
__().collections.EmptyDynamicArray(); |
||||||
|
__().memory.Allocate(class'SimpleSignal'); |
||||||
|
``` |
||||||
|
|
||||||
|
Any class you make that derives from either `AcediaObject` or `AcediaActor` |
||||||
|
will have `_` and `__()` defined. |
||||||
|
If you need to create a class that does not derive from Acedia's classes, |
||||||
|
but you want to make Acedia's API be available inside it, |
||||||
|
then you simply need to redefine them: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
var Global _; |
||||||
|
|
||||||
|
public static final function Global __() |
||||||
|
{ |
||||||
|
return class'Global'.static.GetInstance(); |
||||||
|
} |
||||||
|
|
||||||
|
// ... |
||||||
|
// Set `_`'s value somewhere before using your class: |
||||||
|
_ = class'Global'.static.GetInstance(); |
||||||
|
``` |
@ -0,0 +1,588 @@ |
|||||||
|
# `AcediaObject` and `AcediaActor` |
||||||
|
|
||||||
|
Acedia defines its own base classes for both actor (`AcediaActor`) |
||||||
|
and non-actor objects (`AcediaObject`), better integrated into |
||||||
|
Acedia's infrastructure. |
||||||
|
Here we will go over everything you need to understand them.`Object` and `Actor`. |
||||||
|
|
||||||
|
## Who is responsible for objects? |
||||||
|
|
||||||
|
If you've already read [safety rules](./safety.md) (and you should have), |
||||||
|
then you already know about the importance of deallocation. |
||||||
|
But which objects exactly are you supposed to deallocate? |
||||||
|
Understanding what objects you are responsible for is likely the most important |
||||||
|
concept to get when working with Acedia. |
||||||
|
There are two main guidelines: |
||||||
|
|
||||||
|
* **If function returns an object (as a return value or as an `out` argument) - |
||||||
|
then this object must be deallocated by |
||||||
|
whoever called that function.** |
||||||
|
If you've called `_.text.Empty()`, then you must deallocate |
||||||
|
the `MutableText`object it returned. |
||||||
|
Conversely, if you are implementing function that returns an object, |
||||||
|
then you must not deallocate it yourself. |
||||||
|
In fact, you are expected not to use that object at all, |
||||||
|
since now you can't know when it will be deallocated. |
||||||
|
* **Functions do not deallocate their arguments.** |
||||||
|
If you pass an object as an argument to a function - you can expect |
||||||
|
that it won't be deallocated during that call. |
||||||
|
It might get *modified*, but not *deallocated*. |
||||||
|
And, again, when implementing your own function - you should not deallocate |
||||||
|
its arguments either. |
||||||
|
|
||||||
|
However, these guidelines should be treated as *default assumptions* and |
||||||
|
not *hard rules*. |
||||||
|
|
||||||
|
### Exceptions |
||||||
|
|
||||||
|
First guideline, for example, can be broken if returned object is supposed to |
||||||
|
be shared: `_.players.GetPlayers()` returns array with references to |
||||||
|
*player objects* (`array<APLayer>`) that aren't supposed to ever be deallocated. |
||||||
|
Similarly, Acedia's collections operate by different rules: |
||||||
|
they might still consider themselves responsible for objects returned with |
||||||
|
`GetItem()`. |
||||||
|
|
||||||
|
Second guideline can also be broken by some of the methods for the sake of |
||||||
|
convenience. |
||||||
|
If you need to turn a `Text` object `textToConvert` into a `string`, |
||||||
|
then you can either do: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
if (textToConvert != none) |
||||||
|
{ |
||||||
|
result = textToConvert.ToPlainString(); |
||||||
|
textToConvert.FreeSelf(); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
or simply call `_.text.ToString()` that automatically deallocates its argument: |
||||||
|
`result = _.text.ToString(textToConvert)`. |
||||||
|
|
||||||
|
> Any such exceptions are documented (or at least should be), so simply read |
||||||
|
> the docs for functions you're using. |
||||||
|
> If they don't mention anything about how their arguments or return values |
||||||
|
> should be treated - assume above stated guidelines. |
||||||
|
|
||||||
|
## `MemoryAPI` |
||||||
|
|
||||||
|
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.EmptyDynamicArray()` or `_.time.StartTimer()` |
||||||
|
and can be deallocated with `self.FreeSelf()` method. |
||||||
|
However, that won't be enough if you want to create and allocate your own |
||||||
|
classes, for that you'll need the help of `MemoryAPI`. |
||||||
|
|
||||||
|
They are less powerful than `new` keyword and `Spawn()` function, but perform |
||||||
|
certain background work, necessary for Acedia to function and |
||||||
|
**you should always use them for creating Acedia's objects**. |
||||||
|
Ultimately, all Acedia's objects and actors are created with |
||||||
|
`_.memory.Allocate()` and "destroyed" with `_.memory.Free()`. |
||||||
|
For example, here is how new `Parser` is created with `_.text.NewParser()`: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
public final function Parser NewParser() |
||||||
|
{ |
||||||
|
return Parser(_.memory.Allocate(class'Parser')); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
and `self.FreeSelf()` is actually defined in `AcediaObject` |
||||||
|
and `AcediaActor` as follows (ignore parts about life versions for now, |
||||||
|
they will be explained in sections below): |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
public final function FreeSelf(optional int lifeVersion) |
||||||
|
{ |
||||||
|
if (lifeVersion <= 0 || lifeVersion == GetLifeVersion()) { |
||||||
|
_.memory.Free(self); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
These two functions are the most important ones in `MemoryAPI`, |
||||||
|
but it contains several more useful ones: |
||||||
|
|
||||||
|
| Function | Description | |
||||||
|
| -------- | ----------- | |
||||||
|
| `Allocate(class<Object>, optional bool)` | Creates a new `Object` / `Actor` of a given class. `bool` argument allows to forbid reallocation, forcing creation of a new object. |
||||||
|
| `LoadClass(Text)` | Creates a class instance from its textual representation. | |
||||||
|
| `AllocateByReference(Text, optional bool)` | Same as `Allocate()`, but takes textual representation of the class as an argument. | |
||||||
|
| `Free(Object)` | Deallocates provided object. | |
||||||
|
| `FreeMany(array<Object>)` | Deallocates every object inside given array. | |
||||||
|
| `CollectGarbage(optional bool)` | Forces garbage collection. By default also includes all deallocated (but not destroyed) objects. `bool` argument allows to skip collecting them. |
||||||
|
|
||||||
|
> **NOTE:** `MemoryAPI` can also be used for creating objects that do not |
||||||
|
> derive from either `AcediaObject` or `AcediaActor`, but there is no point in |
||||||
|
> using them over `new` or `Spawn()`: |
||||||
|
> Acedia will not reallocate non-Acedia objects. |
||||||
|
|
||||||
|
## Constructors and finalizers |
||||||
|
|
||||||
|
Both `AcediaObject` and `AcediaActor` support |
||||||
|
[constructors]( |
||||||
|
https://en.wikipedia.org/wiki/Constructor_(object-oriented_programming)) |
||||||
|
and |
||||||
|
[finalizers](https://en.wikipedia.org/wiki/Finalizer). |
||||||
|
*Constructor* is a method that's called on object after it's created, |
||||||
|
preparing it for use. |
||||||
|
In Acedia *Finalizer* is a method that's called when object is deallocated |
||||||
|
(or actor is destroyed) and can be used to clean up any used resources. |
||||||
|
|
||||||
|
> Technically, right now *destructor* might be a better terminology for Acedia's |
||||||
|
> finalizers, but, if development is not halted, current name would eventually |
||||||
|
> become a better fit. |
||||||
|
|
||||||
|
A good and simple example is from the `ATradingComponent` that |
||||||
|
allocates necessary objects inside its constructor and deallocates them in |
||||||
|
its finalizer: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
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; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
To use constructors and finalizers in your own classes you simply need to |
||||||
|
overload `Constructor()` and `Finalizer()` methods (they are defined in both |
||||||
|
`AcediaObject` and `AcediaActor`), just like in the example above. |
||||||
|
|
||||||
|
> 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. |
||||||
|
|
||||||
|
## Object equality and object hash |
||||||
|
|
||||||
|
Comparing object variable with `==` operator simply checks if they refer to |
||||||
|
the exact same object. |
||||||
|
But sometimes we want a comparison that compares the content of two objects |
||||||
|
instead: like checking that two different `Text`s store the exact same data. |
||||||
|
Acedia provides an alternative way to compare two objects - `IsEqual()` method. |
||||||
|
Its default implementation corresponds to that of `==` operator: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
public function bool IsEqual(Object other) |
||||||
|
{ |
||||||
|
return (self == other); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
but can be redefined, as long as it obeys following rules: |
||||||
|
|
||||||
|
* `a.IsEqual(a) == true`; |
||||||
|
* `a.IsEqual(b)` if and only if `b.IsEqual(a)`; |
||||||
|
* Result of `a.IsEqual(b)` does not change unless one of the objects gets |
||||||
|
deallocated. |
||||||
|
|
||||||
|
Because of last rule two `MutableText`s cannot be compared base on their content |
||||||
|
since their contents can change without deallocation. |
||||||
|
|
||||||
|
Reimplementing `IsEqual()` method also requires you to reimplement how object's |
||||||
|
[hash value](https://en.wikipedia.org/wiki/Hash_function) is calculated. |
||||||
|
*Hash value* is a an `int` associated with an object. |
||||||
|
Several different objects can have the same hash value and equal objects *must* |
||||||
|
have the same hash value. |
||||||
|
|
||||||
|
By default, Acedia's objects simply use randomly generated value as their hash. |
||||||
|
This can be changed by reimplementing `CalculateHashCode()` method. |
||||||
|
Every object will only call it once to cache it for `GetHashCode()`: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
public final function int GetHashCode() |
||||||
|
{ |
||||||
|
if (_hashCodeWasCached) { |
||||||
|
return _cachedHashCode; |
||||||
|
} |
||||||
|
_hashCodeWasCached = true; |
||||||
|
_cachedHashCode = CalculateHashCode(); |
||||||
|
return _cachedHashCode; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
As an example, here is `Text`'s definition that calculates hash based on |
||||||
|
the contents: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
protected function int CalculateHashCode() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local int hash; |
||||||
|
hash = 5381; |
||||||
|
for (i = 0; i < codePoints.length; i += 1) |
||||||
|
{ |
||||||
|
// hash * 33 + codePoints[i] |
||||||
|
hash = ((hash << 5) + hash) + codePoints[i]; |
||||||
|
} |
||||||
|
return hash; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Boxing |
||||||
|
|
||||||
|
Last important topic to go over is |
||||||
|
[boxing]( |
||||||
|
https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)#Boxing), |
||||||
|
a process of turning primitive types such as `bool`, `byte`, `int` or `float` |
||||||
|
into objects. |
||||||
|
The concept is very simple - we create a *box* object, which is just an object |
||||||
|
that stores a single primitive value and could be implemented kind of like that: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MyBox extends Object; |
||||||
|
var float value; |
||||||
|
``` |
||||||
|
|
||||||
|
Except Acedia's boxes are *immutable* - their value cannot change once |
||||||
|
the box was created. |
||||||
|
|
||||||
|
Boxes were introduced because they allowed creation of general collections: |
||||||
|
Acedia's collections can only store `AcediaObject`, but thanks to boxing |
||||||
|
any value can be turned into `AcediaObject` and stored in the collection. |
||||||
|
For native primitive types they can be created with either `BoxAPI` or manually: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
local IntBox box1; |
||||||
|
local FloatBox box2; |
||||||
|
// Created with `BoxAPI` |
||||||
|
box1 = _.box.int(7); |
||||||
|
// Allocated and initialized manually |
||||||
|
box2 = FloatBox(_.memory.Allocate(class'FloatBox')); |
||||||
|
box2.Initialize(-2.48); // Must be done immediately after allocation! |
||||||
|
// Works the same |
||||||
|
Log("Int value:" @ box1.Get()); // Int value: 7 |
||||||
|
Log("Float value:" @ box2.Get()); // Float value: -2.48 |
||||||
|
``` |
||||||
|
|
||||||
|
Immutable boxes also have a counterpart - mutable *references* that also |
||||||
|
provide `Set()` method: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
local IntRef ref1; |
||||||
|
local FloatRef ref2; |
||||||
|
// Created with `BoxAPI` |
||||||
|
ref1 = _.ref.int(7); |
||||||
|
// Allocated and initialized manually |
||||||
|
ref2 = FloatRef(_.memory.Allocate(class'FloatRef')); |
||||||
|
ref2.Initialize(-2.48); // Must be done immediately after allocation! |
||||||
|
// Change values |
||||||
|
ref1.Set(-89); |
||||||
|
ref2.Set(0.56); |
||||||
|
Log("Int value:" @ ref1.Get()); // Int value: -89 |
||||||
|
Log("Float value:" @ ref2.Get()); // Float value: 0.56 |
||||||
|
``` |
||||||
|
|
||||||
|
The most important difference between boxes and references concerns how their |
||||||
|
`IsEqual()` and `GetHash()` are implemented: |
||||||
|
|
||||||
|
* Since boxes redefine `IsEqual()` and `GetHash()` to depend on the stored value. |
||||||
|
Since value inside the box cannot change, then there is no problem to base |
||||||
|
equality and hash on it. |
||||||
|
* References do not redefine `IsEqual()` / `GetHash()` and behave like any |
||||||
|
other object - their hash is random and they are only equal to themselves. |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
local ByteBox box1, box2; |
||||||
|
local ByteRef ref1, ref2; |
||||||
|
box1 = _.box.byte(56); |
||||||
|
box2 = _.box.byte(56); |
||||||
|
ref1 = _.ref.byte(247); |
||||||
|
ref2 = _.ref.byte(247); |
||||||
|
// Boxes equality: true |
||||||
|
Log("Boxes equality:" @ (box1.IsEqual(box2))); |
||||||
|
// Boxes hash equality: true |
||||||
|
Log("Boxes hash equality:" @ (box1.GetHash() == box2.GetHash())); |
||||||
|
// Refs equality: false |
||||||
|
Log("Refs equality:" @ (ref1.IsEqual(ref2))); |
||||||
|
// Refs hash equality: false |
||||||
|
// (that's the most likely result, but it can actually be `true` by pure chance) |
||||||
|
Log("Refs hash equality:" @ (ref1.GetHash() == ref2.GetHash())); |
||||||
|
``` |
||||||
|
|
||||||
|
> **NOTE:** For `string`s the role of boxes and references is performed by |
||||||
|
> `Text` and `MutableText` classes that are discussed separately. |
||||||
|
|
||||||
|
### Actor references with `NativeActorRef` |
||||||
|
|
||||||
|
As was explained in [safety rules](./safety.md), storing references to actors |
||||||
|
inside objects is a bad idea. |
||||||
|
Actor boxes and references provide us with a safe way to do that: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MyObject extends AcediaObject; |
||||||
|
|
||||||
|
var NativeActorRef pawnReference; |
||||||
|
// ... |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
_.memory.Free(pawnReference); // This does not destroy stored pawn! |
||||||
|
pawnReference = none; |
||||||
|
} |
||||||
|
|
||||||
|
function Pawn GetMyPawn() |
||||||
|
{ |
||||||
|
if (pawnReference == none) { |
||||||
|
return none; |
||||||
|
} |
||||||
|
return Pawn(pawnReference.Get()); |
||||||
|
} |
||||||
|
|
||||||
|
function SetMyPawn(Pawn newPawn) |
||||||
|
{ |
||||||
|
if (pawnReference == none) |
||||||
|
{ |
||||||
|
// `UnrealAPI` deals with storing non-Acedia actors such as `Pawn`. |
||||||
|
// For `AcediaActor`s you can also use `_.ref.Actor()`. |
||||||
|
pawnReference = _.unreal.ActorRef(newPawn); |
||||||
|
} |
||||||
|
else { |
||||||
|
pawnReference.Set(newPawn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function DoWork() |
||||||
|
{ |
||||||
|
local Pawn myPawn; |
||||||
|
myPawn = GetMyPawn(); |
||||||
|
if (myPawn == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// <Some code that might `Destroy()` our pawn> |
||||||
|
// ^ After destroying a pawn, |
||||||
|
// `myPawn` local variable might go "bad" and cause crashes, |
||||||
|
// so it's a good idea to update it from safe `pawnReference`: |
||||||
|
myPawn = GetMyPawn(); |
||||||
|
myPawn.health += 10; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Actor boxes do not exist, since we cannot guarantee that value stored inside |
||||||
|
them will never change - destroying stored actor will always reset it to `none`. |
||||||
|
|
||||||
|
### Array boxes and references |
||||||
|
|
||||||
|
If necessary, box and reference classes can be manually created for any type |
||||||
|
of value, including `array<...>`s and `struct`s. |
||||||
|
Acedia provides such classes for arrays of primitive types out of the box. |
||||||
|
They can be useful for passing huge arrays between objects and functions |
||||||
|
by reference, without copying their entire data every time. |
||||||
|
They also provide quite a few several convenience methods. |
||||||
|
Here is a list for `FloatArrayRef` as an example: |
||||||
|
|
||||||
|
| Method | Description | |
||||||
|
| ------ | ----------- | |
||||||
|
| `Get()` | Returns the whole stored array as `array<float>`. | |
||||||
|
| `Set(array<float>)` | Sets the whole array value. | |
||||||
|
| `GetItem(int, optional float)` | Returns item at specified index. If index is invalid, returns passed default value. | |
||||||
|
| `SetItem(int, float)` |Changes array's value at specified index. | |
||||||
|
| `GetLength()` | Returns length of the array. `ref.GetLength()` is faster than `ref.Get().length`, since latter will make a copy of the whole array first | |
||||||
|
| `SetLength(int)` | Resizes stored array, doing nothing on negative input. | |
||||||
|
| `Empty()` | Empties stored array. | |
||||||
|
| `Add(int)` | Increases length of the array by adding specified amount of new elements at the end. | |
||||||
|
| `Insert(int index, int count)` | Inserts `count` empty elements into the array at specified position. The indices of the following elements are increased by `count` in order to make room for the new elements. | |
||||||
|
| `Remove(int index, int count)` | Removes number elements from the array, starting at `index`. All elements before position and from `index + count` on are not changed, but the element indices change, - they shift to close the gap, created by removed elements. | |
||||||
|
| `RemoveIndex(int)` | Removes value at a given index, shifting all the elements that come after one place backwards. | |
||||||
|
| `AddItem(float)` | Adds given `float` at the end of the array, expanding it by 1 element. | |
||||||
|
| `InsertItem(int, float)` | Inserts given item at index of the array, shifting all the elements starting from `index` one position to the right. | |
||||||
|
| `AddArray(array<float>)` / `AddArrayRef(FloatArrayRef)` | Adds given array of items at the end of the array, expanding it by inserted amount. | |
||||||
|
| `InsertArray(array<float>)` / `InsertArrayRef(FloatArrayRef)` | Inserts items array at specified index of the array, shifting all the elements starting from `index` by inserted amount to the right. | |
||||||
|
| `RemoveItem(float, bool)` | Returns all occurrences of `item` in the caller `float` (optionally only first one). | |
||||||
|
| `Find(float)` | Finds first occurrence of specified item in caller `FloatArrayRef` and returns its index. | |
||||||
|
| `Replace(float search, float replacement)` | Replaces any occurrence of `search` with `replacement`. | |
||||||
|
| `Sort(optional bool descending)` | Sorts array in either ascending or descending order. | |
||||||
|
|
||||||
|
## [Advanced] Static constructors and finalizers |
||||||
|
|
||||||
|
Acedia also supports a notion of static constructors and finalizers. |
||||||
|
|
||||||
|
Static constructor is called for each class only once: |
||||||
|
|
||||||
|
* Whenever first object of such class is created, |
||||||
|
before its constructor is called; |
||||||
|
* If you want static initialization to be done earlier, |
||||||
|
it is allowed to call static constructor manually: |
||||||
|
`class'...'.static.StaticConstructor()`. |
||||||
|
|
||||||
|
> **NOTE:** Static constructor being called for your class does not guarantee it |
||||||
|
> being called for its parent class. They are considered independently. |
||||||
|
|
||||||
|
Right now relying on static constructors in not advised, but if you are sure |
||||||
|
you need them, you can define them like this: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
public static function StaticConstructor() |
||||||
|
{ |
||||||
|
// This condition is necessary, DO NOT remove it, leave it AS IS |
||||||
|
if (StaticConstructorGuard()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// Place your logic here |
||||||
|
// ... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Static finalizers, however, are more important. |
||||||
|
They are called during Acedia's shutdown for any class that had its |
||||||
|
static constructor invoked (including for any Acedia class that was allocated). |
||||||
|
It can be used to "clean up" after yourself. |
||||||
|
To have a clean level change it is important that you undo as many changes to |
||||||
|
game's objects as you reasonably can. |
||||||
|
It is especially important to reset default values, unless their change is |
||||||
|
deliberate. |
||||||
|
Here is an example used in the base `AcediaObject` class at some point: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
protected static function StaticFinalizer() |
||||||
|
{ |
||||||
|
// Not cleaning object references in `default` values will interfere |
||||||
|
// with garbage collection |
||||||
|
default._textCache = none; |
||||||
|
default._objectPool = none; |
||||||
|
// Not cleaning this value will prevent static constructors |
||||||
|
// (and a whole bunch of other code) from being called after the map change |
||||||
|
default._staticConstructorWasCalled = false; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## [Advanced] Technical details |
||||||
|
|
||||||
|
### How allocation and deallocation works |
||||||
|
|
||||||
|
UnrealScript lacks any practical way to destroy objects on demand: |
||||||
|
the best one can do is remove any references to the object and wait for |
||||||
|
garbage collection. |
||||||
|
But garbage collection itself is too slow and causes noticeable lag spikes |
||||||
|
for players, making it suitable only for cleaning objects when switching levels. |
||||||
|
To alleviate this problem, there exists a standard class `ObjectPool` |
||||||
|
that stores unused objects 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: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
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; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Acedia uses a separate object pool (implemented by `AcediaObjectPool`) |
||||||
|
for every single class, making object allocation as trivial as grabbing |
||||||
|
the last stored object from `AcediaObjectPool`'s internal dynamic array: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// 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 prepared for every class you create, as long as it is inherited |
||||||
|
from `AcediaObject`. |
||||||
|
`AcediaActor`s do not use object pools and are simply `Destroy()`ed. |
||||||
|
|
||||||
|
### Detecting deallocated objects |
||||||
|
|
||||||
|
Deallocated objects are not destroyed, but simply stored inside a special pool |
||||||
|
to be later reused. |
||||||
|
Problems can arise if some function deallocates your object without telling you. |
||||||
|
If you suspect this might be the case or just want to make extra sure |
||||||
|
your object is intact, then there are ways to confirm it. |
||||||
|
|
||||||
|
First relevant method is defined in any class derived from |
||||||
|
`AcediaObject` or `AcediaActor`: `IsAllocated()` that returns |
||||||
|
`true` for objects that are currently allocated and `false` otherwise. |
||||||
|
However, this method is not enough, since your object might be *reallocated*: |
||||||
|
first deallocated and then allocated again by some other code. |
||||||
|
Then `IsAllocate()` will return `true` even though your reference is |
||||||
|
no longer valid. |
||||||
|
|
||||||
|
This issue can be solved with *life version* - `int` value that changes |
||||||
|
each time object is reallocated: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
local int lifeVersion; |
||||||
|
local Text originalObject, newObject; |
||||||
|
// Get object and remember its life version |
||||||
|
originalObject = _.text.FromString("My string"); |
||||||
|
lifeVersion = originalObject.GetLifeVersion(); |
||||||
|
// Allocated objects always have positive life version |
||||||
|
// and it won't change until they get deallocated |
||||||
|
Log(originalObject.IsAllocated()); // true |
||||||
|
Log(originalObject.GetLifeVersion() > 0); // true |
||||||
|
Log(originalObject.GetLifeVersion() == lifeVersion); // true |
||||||
|
// But after deallocation, life version will change and become negative |
||||||
|
originalObject.FreeSelf(); |
||||||
|
Log(originalObject.IsAllocated()); // false |
||||||
|
Log(originalObject.GetLifeVersion() > 0); // false |
||||||
|
Log(originalObject.GetLifeVersion() == lifeVersion); // false |
||||||
|
// This will reallocate object we've just deallocated |
||||||
|
// and it will have different (positive) life version |
||||||
|
newObject = _.text.FromString("New string!"); |
||||||
|
Log(originalObject == newObject); // true |
||||||
|
Log(originalObject.IsAllocated()); // true |
||||||
|
Log(originalObject.GetLifeVersion() > 0); // true |
||||||
|
Log(originalObject.GetLifeVersion() == lifeVersion); // false |
||||||
|
``` |
||||||
|
|
||||||
|
Summarizing, to detect whether your object was reallocated - |
||||||
|
remember its life version value right after allocation |
||||||
|
and then compare it to the `GetLifeVersion()`'s result. |
||||||
|
Value returned by `GetLifeVersion()` changes after each reallocation |
||||||
|
and won't repeat for the same object. |
||||||
|
The only guarantee about life versions of deallocated objects is that they will |
||||||
|
be negative. |
||||||
|
|
||||||
|
### Customizing object pools for your classes |
||||||
|
|
||||||
|
Object pool usage can be disabled completely for your class by setting |
||||||
|
`usesObjectPool = false` in `defaultproperties` block. |
||||||
|
Without object pools `_.memory.Allocate()` will create a new instance of |
||||||
|
your class every single time. |
||||||
|
|
||||||
|
You can also set a limit to how many objects will be stored in |
||||||
|
an object pool with `defaultMaxPoolSize` variable. |
||||||
|
Negative number (default for `AcediaObject`) means that object pool can |
||||||
|
grow without a limit. |
||||||
|
`0` effectively disables object pool, similar to setting |
||||||
|
`usesObjectPool = false`. |
||||||
|
However, this can be overwritten by server's settings |
||||||
|
(see `AcediaSystem.ini: AcediaObjectPool`). |
Loading…
Reference in new issue