C# Garbageless List – a 0-garbage optimization for my event dispatcher

  • Part 1: basics event system
  • Part 2: event picker drawer for in-editor usage
  • Part 3: debugging with platform dependant compilation
  • Part 4: multiple dispatchers
  • Part 5: optimization [you are here]

This time you get to see the final form!

final form
Final form, for real.

As we’ve anticipated last week, there’s a problem with my implementation of the event dispatcher. It’s garbage. No, not the dispatcher, the problem is garbage. Lots and lots of garbage generated by hidden “optimizations” the compiler makes and by the c# implementation of the delegate.

A C# garbageless list? proof or GTFO!

The data was extracted sandwitching each call between two profiler BeginSample/EndSample calls, all inside a for loop, adding each time a dinamically-generated delegate so that they were actually different functions (with different hashcodes). Now, the main source of garbage was identified and eliminated thanks to this wonderful piece by somasim, it was the use of enums as keys for my double dictionary indexed by channels/events. Each time a reference was made, the kind compiler tought to box the enum with an object that would immediately be discarded, thus generating garbage.

garbageless list profile info
garbageless list profiler info – humbly tagged as “perfection”

Step 1: Garbageless dictionary use

To avoid that boxing requires a huge change in the implementation: indexing by integers. That would be obtained by casting the enums to int when calling the function, which doesn’t even require an explicit change in the calls, thus preserving retrocompatibility of the class. Man, I love enums.
This is what our data structure initializer becomes:

Dictionary<int, Dictionary<int, GarbagelessList>> initializeDicts()

Step 2: actually implementing the C# garbageless list

The second issue was the usage of the delegate += operator to enqueue delegates one to another. And here’s where it gets absolutely painful: the usual collections in c# always generate garbage when used. I tested them a lot and then decided that I can do better. (Or actually: that I can build something more optimized to my needs)

Now, this will need to go quite low level. Here’re the rules to build a garbageless list:

  1. thou shall not instantiate classes
  2. thou shall log an error each time a slight amount of garbage is generated
  3. thou shall not use default c# implemented data structures
  4. thou shall not stop until you beat the c# dictionary
  5. thou shall never ever free a bit of memory

Wait didn’t you say “garbageless”?

Here’s the problem: allocating an array of elements will always generate garbage in c# (100 int means 440B garbage, 200 goes for 880B and so on), and even creating an empity class can’t be done without wasting 16 bytes. So, in reality, we’ll get garbage both on the instantiation and whenever our default initial capacity is exceeded.

Now, the code this time is awfully long and all needed, so I won’t explain every detail or I’d need a new series just for this. But I’ll run you through the core concepts before giving all the code to copy-paste:

  • encode everything into arrays of structs
  • use linked lists and manage their memory manually
  • use indexing to speed up the search

Also, before we start, be aware of the pros and cons: while this list doesn’t generate any garbage on regular use, it does generate garbage when constructor is called and also when the default capacity limit is passed; also, be aware that when you pass a delegate to it the function will be boxed by the compiler if you didn’t construct a delegate object in advance. And last but not least, the price for the zero garbage generation is… not freeing memory. Which means that you should budget your memory accurately when setting the default capacity variables. If you can muster a few hundred bytes of garbage every now and then, just remove the log error calls and set a reasonably small default value, so that you won’t allocate more than what peak consumption requires. If you absolutely want that 0, use a high default value.

First of all, meet the node struct:

    struct node
    {
        public gameEventHandler content;
        public int nextIndex;
        public int lastIndex;
    }

The next 9 hundreds of lines of code will be used to manage this cutie. It will be hosted into a jagged bidimensional Array of nodes, this means we can instantiate the second lot just by initializing the second field of the array, instead of using the Array.Resize call (which creates a bigger copy of the array and transforms the old one in garbage).

    void addBatch()
    {
        if (capacity > 0)
            UnityEngine.Debug.LogError("Had to extend GarbagelessList, set a higher DEFAULTCAPACITY for GarbagelessList or get garbage");
        ArrayData[capacity / DEFAULTCAPACITY] = new node[DEFAULTCAPACITY];

        for (int i = capacity; i < capacity + DEFAULTCAPACITY; i++)
        {
            setNextIndexPos(i, i + 1);
            setLastIndexAtPos(i, i - 1);
        }
        capacity += DEFAULTCAPACITY;

    }

The variablecapacity will be used to store the amount of nodes currently available, when this function is called by the constructor it has value 0, so the log error won’t trigger. To index the array data we use a int on int division that will return the highest integer smaller than the actual float result. After the instantiation is done, it’s time for the initialization, wich is taken care of inside the for. We’re building here a linked list of free nodes, so each node will link to its successor and the first one’s address will be stored inside the freeTail variable.

Now let’s look at how to get the position of a node inside a bidimensional array:

void setNextIndexPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].nextIndex = value;
    }

The first operation is again an int on int division, while the second is the module operator that gives us the remainder of it. So for each integer one and only one node is indexed. I wrapped the access this way for two reasons: first, you can’t just return the node and edit it since it’s a struct, second, I experimented quite a bit before getting to the bidimensional array of structs and didn’t want to rewrite the access everywhere each time. Feel free to substitute the access functions with direct assignments/reads.

    int getFree()
    {
        //if arraydata is full, we need to expand it.
        if (Count >= capacity)
            addBatch();
        int result = freeHead;
        freeHead = getNextIndexAtPos(freeHead);
        return result;
    }

Another important bit of code is here, this is the function I use to get a free node: first it checks if there are any free nodes by comparison between the count and the current capacity of the list, then if needed it adds a new batch, and after that it updates the head of the free nodes list with the successor of the current one.

Access to the data

Now, to grant a fast access to the data I implemented an enumerator-like series of methods

    bool enumeratorJustReset = false;
    int currentIndex;
    public gameEventHandler Current
    {
        get
        {
            return getContentAtPos(currentIndex);
        }
    }


    public bool MoveNext()
    {
        if (Count == 0)
            return false;
        if (enumeratorJustReset)
        {
            enumeratorJustReset = false;
            return true;
        }
        if (currentIndex == tail)
            return false;
        currentIndex = getNextIndexAtPos(currentIndex);
        return true;
    }

    public void Reset()
    {
        currentIndex = head;
        enumeratorJustReset = true;
    }

Notice that I’ve not implemented the IEnumerator interface. Why? because that would require an object instantiation, and that would violate our 1st commandment. Instead I implemented the usual Reset-MoveNext-Current approach as methods of the class, same result, no garbage.

So, if I just stopped with that stuff (and all the accessory code to actually use it, like the add and remove functions) I’d get a zero-garbage list with horrible time consumption. By nature of a linked list, access is necessarily tape-like: you always need to traverse all the list from the head to the element you need to remove it. In my tests that meant a couple of orders of magnitude slower access when compared to standard Dictionary (especially on removal, which is where it’s required to find a specific delegate to erase).

unacceptable

So how can I solve this? Well, I cheated a bit and went on memory lane back to my computer science major years, and remembered a useful tool: indexing. Of course the index needed to allow for addition and removal and had to resist all temptations of garbage generation, so it would need to be a tree structure encoded on an array. And not just any tree structure, but a balanced one. So I opted to implement a Red-Black Tree (since every open source implementation I found relied on classes and other garbage-prone stuff). From scratch.

Fuck.

A tree of knowledge

Now, the indexing would require to identify the content of the original nodes and then use that to get the locations on the regular data array, but since you may very well add multiple identical listeners to a single event there was also the possibility of duplicates, so each index node would need to be able to refer to multiple locations. Thus an array was needed inside each index node. Meet the index’s own node:

struct indexerCouple
    {
        public int[] indexedArrayData;//-1=free slot
        public int hashValue;
        public int leftChildIndex;//default is 0
        public int rightChildIndex;//default is 0
        public int lastIndex;//default is 0
        public bool isRed;

        public int ContentCount;

        public void resize()
        {
            int oldSize = indexedArrayData.Length;
            Array.Resize<int>(ref indexedArrayData, indexedArrayData.Length * 2);
            for (int i = oldSize; i < indexedArrayData.Length; i++)
            {
                indexedArrayData[i] = -1;
            }
            UnityEngine.Debug.LogError("Had to extend indexedArrayData, set a higher DEFAULTCAPACITY for BST indexer indexedArrayData or get garbage");
        }
    }

The indexing parameter will therefore be the result of a GetHashCode function called on the gameEventHandler to be indexed. Addition and removal in an ordered tree are very simple, you just navigate the tree until you find either a leaf or a null pointer and then get one from the free list to add or put it in said list to remove.

    int findHash(int hashvalue, int currentNode)
    {
        if (currentNode == -1) return -1;
        if (getHashAtPos(currentNode) == hashvalue) return currentNode;
        return findHash(hashvalue, (getHashAtPos(currentNode) > hashvalue) ? getLeftChildIndexAtPos(currentNode) : getRightChildIndexAtPos(currentNode));
    }

As you can see the find function is actually quite simple, but it’s recursive, it calls itself for each node it goes through. If you never heard about this stuff here’s a guide to learn about it.

For RB trees insertion and deletion it’s not as simple as in lists or in plain binary search trees. If you want to get to understand what goes on with that part of the code, there’s no short cut. Here are some resources:

Basically what happens in insertion is that first I check if the node already exists, if so, I just add a new index to his array. Otherwise a new node is added to the tree and the violation of the RB properties is dealt with in a series of possible cases. When deleting, again, first I find if the node exists, then if it can just be substituted with a “lower” one I just copy the data and delete the other, otherwise the node is removed and again the RB property violations are dealt with in a series of cases.

I’ve also left a couple of debugging facilities for any problem that may eventually emerge (this optimized version is tested, but has never been used in production unlike its last version), I’d love to hear from anyone who tries to implement this code.

And with the use of this indexer we may finally access the data in our garbageless list by the means of a binary search, which is quite fast and passes the comparison with c# dictionaries.

And that’s all folks!

I thank you for bearing it with me with this ordeal of a tutorial, I know that a lot is missing but we’re already at almost a couple thousand words and probably only the truly desperate or genuinely curious made it this far. For any question on the unclear aspects of the code please don’t hesitate to contact me either through my Newsletter or my twitter account.

And by the way, I’m currently looking for a job, if you think you may have one for me, take a look at my portfolio!

using System;
using System.Collections.Generic;

public class GarbagelessList
{
    #region enumerator
    bool enumeratorJustReset = false;
    int currentIndex;
    public gameEventHandler Current
    {
        get
        {
            return getContentAtPos(currentIndex);
        }
    }


    public bool MoveNext()
    { 
        if (Count == 0)
            return false;
        if (enumeratorJustReset)
        {
            enumeratorJustReset = false;
            return true;
        }
        if (currentIndex == tail)
            return false;
        currentIndex = getNextIndexAtPos(currentIndex);
        return true;
    }

    public void Reset()
    {
        currentIndex = head;
        enumeratorJustReset = true;
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }
    #endregion


    const int DEFAULTCAPACITY = 1000;
    int capacity;
    public int Count { get; private set; }
    int head;
    int tail;
    int freeHead;
    node[][] ArrayData;
    int newIndexCache;
    struct node
    {
        public gameEventHandler content;
        public int nextIndex;//default is 0
        public int lastIndex;//default is 0
    }
    BST bstIndex;


    #region public functions
    public GarbagelessList()
    {
        ArrayData = new node[DEFAULTCAPACITY][];
        bstIndex = new BST();
        addBatch();
        head = -1;
        tail = -1;
        freeHead = 0;
        Count = 0;
    }

    public void Add(gameEventHandler toAdd)
    {
        //only on the first add we use an ad hoc setup
        if (head == -1)
        {
            head = freeHead;
            tail = freeHead;
            freeHead = getNextIndexAtPos(freeHead);
            setContentAtPos(head, toAdd);
            bstIndex.Add(head, toAdd.GetHashCode());
        }
        else
        {
            newIndexCache = getFree();
            freeHead = getNextIndexAtPos(freeHead);
            setNextIndexPos(tail, newIndexCache);
            setLastIndexAtPos(newIndexCache, tail);
            tail = newIndexCache;
            setContentAtPos(newIndexCache, toAdd);
            bstIndex.Add(newIndexCache, toAdd.GetHashCode());
        }
        ++Count;
    }

    public void Remove(gameEventHandler toRemove)
    {
        int i = bstIndex.getIndexList(toRemove.GetHashCode())[0];

        if (getContentAtPos(i) == toRemove)
        {
            if (head != i)
                setNextIndexPos(getLastIndexAtPos(i), getNextIndexAtPos(i));
            else
            { head = getNextIndexAtPos(i); }
            if (tail != i)
                setLastIndexAtPos(getNextIndexAtPos(i), getLastIndexAtPos(i));
            else
            { tail = getLastIndexAtPos(i); }

            setLastIndexAtPos(freeHead, i);
            setNextIndexPos(i, freeHead);
            freeHead = i;

            --Count;
        }
    }
    /*
    public gameEventHandler this[int key]
    {
        get
        {
            return getContentAtPos(key);
        }
        set
        {
            setContentAtPos(key, value);
        }
    }
    */
    #endregion
    #region arrayWrap
    gameEventHandler getContentAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].content;
    }
    void setContentAtPos(int key, gameEventHandler value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].content = value;
    }
    int getNextIndexAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].nextIndex;
    }
    void setNextIndexPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].nextIndex = value;
    }
    int getLastIndexAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].lastIndex;
    }
    void setLastIndexAtPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].lastIndex = value;
    }
    #endregion

    #region memory management
    int getFree()
    {
        //if arraydata is full, we need to expand it.
        if (Count >= capacity)
            addBatch();
        int result = freeHead;
        freeHead = getNextIndexAtPos(freeHead);
        return result;
    }
    void addBatch()
    {
        if (capacity > 0)
            UnityEngine.Debug.LogError("Had to extend GarbagelessList, set a higher DEFAULTCAPACITY for GarbagelessList or get garbage");
        ArrayData[capacity / DEFAULTCAPACITY] = new node[DEFAULTCAPACITY];

        for (int i = capacity; i < capacity + DEFAULTCAPACITY; i++)
        {
            setNextIndexPos(i, i + 1);
            setLastIndexAtPos(i, i - 1);
        }
        capacity += DEFAULTCAPACITY;

    }
    #endregion

}


public class BST
{
    indexerCouple[][] ArrayData;
    const int DEFAULTCAPACITY = 1000;
    public BST()
    {

        ArrayData = new indexerCouple[DEFAULTCAPACITY][];
        indexlistHead = -1;
        poolHead = -1;
        capacity = 0;
        count = 0;
        addBatch();
    }

    #region indexercoupledef

    struct indexerCouple
    {
        public int[] indexedArrayData;//-1=free slot
        public int hashValue;
        public int leftChildIndex;//default is 0
        public int rightChildIndex;//default is 0
        public int lastIndex;//default is 0
        public bool isRed;

        public int ContentCount;

        public void resize()
        {
            int oldSize = indexedArrayData.Length;
            Array.Resize<int>(ref indexedArrayData, indexedArrayData.Length * 2);
            for (int i = oldSize; i < indexedArrayData.Length; i++)
            {
                indexedArrayData[i] = -1;
            }
            UnityEngine.Debug.LogError("Had to extend indexedArrayData, set a higher DEFAULTCAPACITY for BST indexer indexedArrayData or get garbage");
        }
    }

    public bool blackOrNull(int toCeck) { return (toCeck == -1) ? true : (!getIsRedAtPos(toCeck)); }

    public int Sibling(int index)
    {
        int lastIndex = getLastIndexAtPos(index);
        if (lastIndex == -1)
            return -1;
        if (index == getLeftChildIndexAtPos(lastIndex))
            return getRightChildIndexAtPos(lastIndex);
        else
            return getLeftChildIndexAtPos(lastIndex);
    }
    public bool isInternal(int index)
    {
        int lastIndex = getLastIndexAtPos(index);
        if (lastIndex == -1)
            return false;
        if (getLastIndexAtPos(lastIndex) == -1)
            return false;
        if (getRightChildIndexAtPos(lastIndex) == index && getLeftChildIndexAtPos(getLastIndexAtPos(lastIndex)) == lastIndex)
            return true;

        if (getLeftChildIndexAtPos(lastIndex) == index && getRightChildIndexAtPos(getLastIndexAtPos(lastIndex)) == lastIndex)
            return true;
        return false;
    }

    public int InternalChild(int index)
    {
        int lastIndex = getLastIndexAtPos(index);
        if (lastIndex == -1) return -1;
        if (getRightChildIndexAtPos(lastIndex) == index)
            return getLeftChildIndexAtPos(index);
        else
            return getRightChildIndexAtPos(index);

    }

    public int ExternalChild(int index)
    {
        int lastIndex = getLastIndexAtPos(index);
        if (lastIndex == -1) return -1;
        if (getRightChildIndexAtPos(lastIndex) == index)
            return getRightChildIndexAtPos(index);
        else
            return getLeftChildIndexAtPos(index);
    }


    public bool indexerContains(int index, int value)
    {
        for (int i = 0; i < getIndexedArrayDataAtPos(index).Length; i++)
        {
            if (getIndexedArrayDataAtPos(index, i) == value)
                return true;
        }
        return false;
    }
    public void indexerAdd(int index, int value)
    {
        for (int i = 0; i < getIndexedArrayDataAtPos(index).Length; i++)
        {
            if (getIndexedArrayDataAtPos(index, i) == -1)
            {
                setIndexedArrayDataIndexPos(index, i, value);
                incrementIndexedDataCountAtPos(index);
                return;
            }
        }
        resizeIndexedDataCountAtPos(index);
        indexerAdd(index, value);
    }
    public void indexerRemove(int index, int value)
    {
        for (int i = 0; i < getIndexedArrayDataAtPos(index).Length; i++)
        {
            if (getIndexedArrayDataAtPos(index, i) == value)
            {
                setIndexedArrayDataIndexPos(index, i, -1);
                decrementIndexedDataCountAtPos(index);
                return;
            }
        }
    }
    #endregion

    int indexlistHead;
    int poolHead;
    int capacity;
    int count;
    #region query
    public int[] getIndexList(int hashvalue)
    {
        int result = findHash(hashvalue, indexlistHead);
        if (result == -1)
            throw new KeyNotFoundException();
        else
            return getIndexedArrayDataAtPos(result);
    }
    int findHash(int hashvalue, int currentNode)
    {
        if (currentNode == -1) return -1;
        if (getHashAtPos(currentNode) == hashvalue) return currentNode;
        return findHash(hashvalue, (getHashAtPos(currentNode) > hashvalue) ? getLeftChildIndexAtPos(currentNode) : getRightChildIndexAtPos(currentNode));
    }
    int findHashParent(int hashvalue, int currentNode, int parentNode)
    {
        if (currentNode == -1) return parentNode;
        return findHashParent(hashvalue, (getHashAtPos(currentNode) > hashvalue) ? getLeftChildIndexAtPos(currentNode) : getRightChildIndexAtPos(currentNode), currentNode);
    }

    #endregion

    #region pool Management
    void addBatch()
    {
        if (capacity > 0)
            UnityEngine.Debug.LogError("Had to extend BST, set a higher DEFAULTCAPACITY for BST or get garbage");

        ArrayData[capacity / DEFAULTCAPACITY] = new indexerCouple[DEFAULTCAPACITY];

        poolHead = capacity;
        for (int i = capacity; i < capacity + DEFAULTCAPACITY; i++)
        {
            setLastIndexAtPos(i, i + 1);
            setHashIndexPos(i, -1);
            setLeftChildIndexAtPos(i, -1);
            setRightChildIndexAtPos(i, -1);
            int[] toBeSet = new int[DEFAULTCAPACITY];
            for (int j = 0; j < DEFAULTCAPACITY; j++)
            {
                toBeSet[j] = -1;
            }
            setIndexedArrayDataAtPos(i, toBeSet);
        }
        capacity += DEFAULTCAPACITY;

    }

    int makeNew()
    {
        if (poolHead < capacity)
        {
            int result = poolHead;
            poolHead = getLastIndexAtPos(poolHead);
            setLastIndexAtPos(result, -1);
            resetIndexedDataCountAtPos(result);
            return result;
        }
        else
        {
            addBatch();
            return makeNew();
        }
    }
    void dispose(int toDispose)
    {
        setLastIndexAtPos(toDispose, poolHead);
        poolHead = toDispose;
    }
    #endregion
    #region rotation
    void rotateLeft(int parent)
    {
        //x= parent, y=rightchild
        int rightChildIndex = getRightChildIndexAtPos(parent);
        //move rightchild to his new parent
        if (parent != indexlistHead)
        {
            if (getLeftChildIndexAtPos(getLastIndexAtPos(parent)) == parent)
                setLeftChildIndexAtPos(getLastIndexAtPos(parent), rightChildIndex);
            else
                setRightChildIndexAtPos(getLastIndexAtPos(parent), rightChildIndex);
        }
        else
        {
            indexlistHead = rightChildIndex;
        }
        setLastIndexAtPos(rightChildIndex, getLastIndexAtPos(parent));

        int B = getLeftChildIndexAtPos(rightChildIndex);

        //move parent as the new rightchild's leftchild
        setLeftChildIndexAtPos(rightChildIndex, parent);
        setLastIndexAtPos(parent, rightChildIndex);

        //move B to his new parent
        setRightChildIndexAtPos(parent, B);
        if (B != -1)
            setLastIndexAtPos(B, parent);

    }

    void rotateRight(int parent)
    {
        //x= parent, y=leftchild
        int leftChildIndex = getLeftChildIndexAtPos(parent);

        //move leftChild to his new parent
        if (parent != indexlistHead)
        {
            if (getLeftChildIndexAtPos(getLastIndexAtPos(parent)) == parent)
                setLeftChildIndexAtPos(getLastIndexAtPos(parent), leftChildIndex);
            else
                setRightChildIndexAtPos(getLastIndexAtPos(parent), leftChildIndex);
        }
        else
        {
            indexlistHead = leftChildIndex;
        }
        setLastIndexAtPos(leftChildIndex, getLastIndexAtPos(parent));

        int B = getRightChildIndexAtPos(leftChildIndex);

        //move parent as the new leftchild's rightchild
        setRightChildIndexAtPos(leftChildIndex, parent);
        setLastIndexAtPos(parent, leftChildIndex);

        //move B to his new parent
        setLeftChildIndexAtPos(parent, B);
        if (B != -1)
            setLastIndexAtPos(B, parent);

    }
    /// <summary>
    /// revert child-parent relationship through rotation
    /// </summary>
    /// <param name="parent"></param>
    /// <param name="child"></param>
    void rotationSwap(int parent, int child)
    {
        if (getLeftChildIndexAtPos(parent) == child)
            rotateRight(parent);
        else rotateLeft(parent);
    }
    #endregion
    #region insertion
    public void Add(int indexedArrayElement, int hashvalue)
    {
        if (indexlistHead == -1)
        {
            indexlistHead = makeNew();
            setHashIndexPos(indexlistHead, hashvalue);

            indexerAdd(indexlistHead, indexedArrayElement);
            setIsRedAtPos(indexlistHead, false);

        }
        else
        {
            int holder = findHash(hashvalue, indexlistHead);
            if (holder != -1)
            {
                if (!indexerContains(holder, indexedArrayElement))
                {
                    indexerAdd(holder, indexedArrayElement);
                }
            }
            else
            {
                holder = makeNew();
                setHashIndexPos(holder, hashvalue);
                indexerAdd(holder, indexedArrayElement);
                redBlackInsertion(holder);
            }
        }
        ++count;
    }
    /// <summary>
    /// ensure that this is called only when there is no duplicate for toAdd hashvalue in the tree and tree is not empity
    /// </summary>
    /// <param name="toAdd"></param>
    void redBlackInsertion(int toAdd)
    {

        setIsRedAtPos(toAdd, true);

        int parent = findHashParent(getHashAtPos(toAdd), indexlistHead, -1);
        setLastIndexAtPos(toAdd, parent);
        if (getHashAtPos(parent) > getHashAtPos(toAdd))
            setLeftChildIndexAtPos(parent, toAdd);
        else
            setRightChildIndexAtPos(parent, toAdd);
        if (getIsRedAtPos(parent))
            fixRBPropertyViolations(toAdd);
    }
    void fixRBPropertyViolations(int current)
    {
        if (getIsRedAtPos(current))
        {
            if (getLastIndexAtPos(current) == -1)
                setIsRedAtPos(current, false);//if this is root, just recolor it and double reds are solved
            else if (getIsRedAtPos(getLastIndexAtPos(current)))//if parent of current is black there is no double red violation
            {
                //if parent of current is red, then it has at least a grandparent
                int parent = getLastIndexAtPos(current);
                int uncle = Sibling(parent);
                int grandparent = getLastIndexAtPos(parent);

                if (uncle != -1)
                    if (getIsRedAtPos(uncle) && (!getIsRedAtPos(grandparent)))
                    {//case 1, just switch color and recur
                        setIsRedAtPos(parent, false);
                        setIsRedAtPos(uncle, false);
                        setIsRedAtPos(grandparent, true);
                        fixRBPropertyViolations(grandparent);
                        return;
                    }
                if (isInternal(current))
                {
                    //case 2
                    rotationSwap(parent, current);
                    fixRBPropertyViolations(parent);
                    return;
                }
                //case 3
                setIsRedAtPos(grandparent, true);
                setIsRedAtPos(parent, false);
                rotationSwap(grandparent, parent);
            }

        }

    }
    #endregion
    #region deletion
    public void Remove(int indexedArrayElement, int hashvalue)
    {
        if (indexlistHead == -1)
            throw new KeyNotFoundException();
        int holder = findHash(hashvalue, indexlistHead);
        if (holder == -1)
            throw new KeyNotFoundException();
        else
        {
            if (!indexerContains(holder, indexedArrayElement))
                throw new KeyNotFoundException();
            else
            {
                indexerRemove(holder, indexedArrayElement);
                if (getIndexedDataCountAtPos(holder) <= 0)
                {
                    Remove(holder);
                    --count;
                }

            }
        }
    }
    int getInOrderNextInSubTree(int node)
    {
        if (getRightChildIndexAtPos(node) == -1)
            return -1;

        int result = getRightChildIndexAtPos(node);
        while (getLeftChildIndexAtPos(result) != -1)
            result = getLeftChildIndexAtPos(result);
        return result;
    }
    int getInOrderPreviousInSubTree(int node)
    {
        if (getLeftChildIndexAtPos(node) == -1)
            return -1;

        int result = getLeftChildIndexAtPos(node);
        while (getRightChildIndexAtPos(result) != -1)
            result = getRightChildIndexAtPos(result);
        return result;
    }
    void Remove(int toRemove)
    {
        //if has 2 child, switch places with next.
        if (getRightChildIndexAtPos(toRemove) != -1 && getLeftChildIndexAtPos(toRemove) != -1)
        {
            int toSwitchWith = getInOrderNextInSubTree(toRemove);
            setHashIndexPos(toRemove, getHashAtPos(toSwitchWith));
            setIndexedArrayDataAtPos(toRemove, getIndexedArrayDataAtPos(toSwitchWith));
            Remove(toSwitchWith);
            return;
        }
        else
        {

            //if is red leaf, remove
            if (getRightChildIndexAtPos(toRemove) == -1 && getLeftChildIndexAtPos(toRemove) == -1)
            {
                int parent = getLastIndexAtPos(toRemove);
                if (parent != -1)//if isn't root
                {
                    int sibling = Sibling(toRemove);
                    if (getLeftChildIndexAtPos(parent) == toRemove)//if it's a left child
                    {
                        setLeftChildIndexAtPos(parent, -1);
                    }
                    else//if it's a right child
                    {
                        setRightChildIndexAtPos(parent, -1);
                    }

                    if (!getIsRedAtPos(toRemove))
                        solveDoubleBlack(parent, toRemove, sibling);
                }
                else
                {//if is root, and has no children, delete tree
                    indexlistHead = -1;
                }
                dispose(toRemove);

                return;
            }
            //if has one child
            if (getLeftChildIndexAtPos(toRemove) != -1)
                deleteWithChild(getLeftChildIndexAtPos(toRemove));
            else
                deleteWithChild(getRightChildIndexAtPos(toRemove));
        }

    }

    /// <summary>
    /// substitute the toDestroy node with the source node inside the tree, preserving all the content and label data of source, with flags for each tie
    /// </summary>
    /// <param name="source"></param>
    /// <param name="toDestroy"></param>
    void substitute(int source, int toDestroy, bool preserveLast = false, bool preserveLeft = false, bool preserveRight = false)
    {
        if (preserveLast)
        {
            if (getLastIndexAtPos(toDestroy) != -1)
            {
                if (getLeftChildIndexAtPos(getLastIndexAtPos(toDestroy)) == toDestroy)
                    setLeftChildIndexAtPos(getLastIndexAtPos(toDestroy), source);
                else
                    setRightChildIndexAtPos(getLastIndexAtPos(toDestroy), source);
            }
            setLastIndexAtPos(source, getLastIndexAtPos(toDestroy));
        }
        if (preserveRight)
        {
            if (getRightChildIndexAtPos(toDestroy) != -1)
            {
                setLastIndexAtPos(getRightChildIndexAtPos(toDestroy), source);
            }
            setRightChildIndexAtPos(source, getRightChildIndexAtPos(toDestroy));
        }
        if (preserveLeft)
        {
            if (getLeftChildIndexAtPos(toDestroy) != -1)
            {
                setLastIndexAtPos(getLeftChildIndexAtPos(toDestroy), source);
            }
            setLeftChildIndexAtPos(source, getLeftChildIndexAtPos(toDestroy));
        }
        dispose(toDestroy);

    }

    void deleteWithChild(int child)
    {
        if (getIsRedAtPos(child))
        {
            setIsRedAtPos(child, false);
            substitute(child, getLastIndexAtPos(child), preserveLast: true);
            return;
        }
        else
        {
            substitute(child, getLastIndexAtPos(child), preserveLast: true);
            int parent = getLastIndexAtPos(child);
            if (!getIsRedAtPos(parent))
            {
                //case when there's double black
                if (parent != -1)
                    solveDoubleBlack(parent, child, Sibling(child));
            }
        }
    }

    void solveDoubleBlack(int doubleBlackParent, int child, int sibling)
    {
        //if is root, stop it
        if (doubleBlackParent == -1)
            return;
        //if is chain with red parent, just make it black
        if (sibling == -1 && getIsRedAtPos(doubleBlackParent))
        { setIsRedAtPos(doubleBlackParent, false); return; }
        //if is chain with black parent, just recur on him
        if (sibling == -1 && !getIsRedAtPos(doubleBlackParent))
        { solveDoubleBlack(getLastIndexAtPos(doubleBlackParent), doubleBlackParent, Sibling(doubleBlackParent)); return; }

        //otherwise, both parent and sibling exist, deal with doubleblack
        recognizeDoubleBlackCase(doubleBlackParent, child, sibling);

    }

    void recognizeDoubleBlackCase(int Parent, int doubleBlack, int sibling)
    {
        //PrintPretty(Parent, " parent", true, false);
        //PrintPretty(doubleBlack, " doubleblack ", true, false);
        //PrintPretty(sibling, " sibling", true, false);
        if ((!getIsRedAtPos(Parent)) && (!blackOrNull(sibling)))
            solveCaseOne(Parent, doubleBlack, sibling);
        else if ((getIsRedAtPos(Parent)) && blackOrNull(sibling) && (blackOrNull(InternalChild(sibling)) && blackOrNull(ExternalChild(sibling))))
            solveCaseTwo(Parent, doubleBlack, sibling);
        else if ((!getIsRedAtPos(Parent)) && blackOrNull(sibling) && (blackOrNull(InternalChild(sibling)) && blackOrNull(ExternalChild(sibling))))
            solveCaseThree(Parent, doubleBlack, sibling);
        else if (blackOrNull(sibling) && ((!blackOrNull(InternalChild(sibling))) && blackOrNull(ExternalChild(sibling))))
            solveCaseFour(Parent, doubleBlack, sibling);
        else if (blackOrNull(sibling) && (!blackOrNull(ExternalChild(sibling))))
            solveCaseFive(Parent, doubleBlack, sibling);
        else
            throw new InvalidOperationException(" unrecognized case with p red:" + getIsRedAtPos(Parent) + " sibling blackornull:" + blackOrNull(sibling) + " internal blackornull:" + blackOrNull(InternalChild(sibling)) + " external blackornull:" + blackOrNull(ExternalChild(sibling)));
    }
    void solveCaseOne(int Parent, int doubleBlack, int sibling)
    {
        int futureSibling = InternalChild(sibling);
        bool temp = getIsRedAtPos(Parent);
        setIsRedAtPos(Parent, getIsRedAtPos(sibling));
        setIsRedAtPos(sibling, temp);
        rotationSwap(Parent, sibling);
        solveDoubleBlack(Parent, doubleBlack, futureSibling);
    }
    void solveCaseTwo(int Parent, int doubleBlack, int sibling)
    {
        setIsRedAtPos(sibling, true);
        setIsRedAtPos(Parent, false);
    }
    void solveCaseThree(int Parent, int doubleBlack, int sibling)
    {
        setIsRedAtPos(sibling, true);
        solveDoubleBlack(getLastIndexAtPos(Parent), Parent, Sibling(Parent));
    }
    void solveCaseFour(int Parent, int doubleBlack, int sibling)
    {
        int futureSibling = InternalChild(sibling);
        setIsRedAtPos(InternalChild(sibling), false);
        setIsRedAtPos(sibling, true);
        rotationSwap(sibling, InternalChild(sibling));
        printRBTree();
        isRBTree();
        solveCaseFive(Parent, doubleBlack, futureSibling);
    }
    void solveCaseFive(int Parent, int doubleBlack, int sibling)
    {
        setIsRedAtPos(sibling, getIsRedAtPos(Parent));
        setIsRedAtPos(Parent, false);
        setIsRedAtPos(ExternalChild(sibling), false);

        rotationSwap(Parent, sibling);

    }

    #endregion
    #region debug
    public bool isRBTree() { int pathcountvar = 0; return isRBTree(indexlistHead, out pathcountvar); }
    bool isRBTree(int subTreeHead, out int pathCount)
    {

        if (subTreeHead == -1)
        {
            pathCount = 0;
            return true;
        }
        int lpathCount;
        bool left = isRBTree(getLeftChildIndexAtPos(subTreeHead), out lpathCount);
        int rpathCount;
        bool right = isRBTree(getRightChildIndexAtPos(subTreeHead), out rpathCount);
        pathCount = lpathCount;

        if (getLastIndexAtPos(subTreeHead) == -1)
        {
            pathCount = 0;
            if (getIsRedAtPos(subTreeHead))
            {
                UnityEngine.Debug.LogError("Red root detected! on hash " + getHashAtPos(subTreeHead));
                return false;
            }
        }
        else
        {
            if (!getIsRedAtPos(subTreeHead))
            { ++pathCount; }
            else
                if (getIsRedAtPos(getLastIndexAtPos(subTreeHead)))
            {
                UnityEngine.Debug.LogError("Double red detected! on hash " + getHashAtPos(subTreeHead));
                return false;
            }
        }
        if (lpathCount != rpathCount)
            UnityEngine.Debug.LogError("black child count not matching on hash " + getHashAtPos(subTreeHead));

        return (lpathCount == rpathCount && left && right);

    }
    public void printRBTree()
    {
        if (indexlistHead == -1)
            UnityEngine.Debug.Log(" empity ---");
        else
        {
            UnityEngine.Debug.Log(" BST: ");
            PrintPretty(indexlistHead, "", true, true);
        }

    }
    public void PrintPretty(int index, string indent, bool last, bool recursive)
    {
        string result = indent;
        if (last)
        {
            result += "\\-";
            indent += "  ";
        }
        else
        {
            result += "|-";
            indent += "| ";
        }
        if (getLastIndexAtPos(index) != -1)
        {
            result += (getLeftChildIndexAtPos(getLastIndexAtPos(index)) == index) ? "L->" : "R->";
        }
        int arraycount = getIndexedArrayDataAtPos(index).Length;
        result += getHashAtPos(index) + (getIsRedAtPos(index) ? " _R_" : " _B_") + " content size " + arraycount;
        for (int i = 0; i < arraycount; i++)
        {
            if (getIndexedArrayDataAtPos(index, i) != -1)
                result += " [" + i + "]=" + getIndexedArrayDataAtPos(index, i) + "|";
        }
        UnityEngine.Debug.Log(result);
        if (recursive)
        {
            if (getLeftChildIndexAtPos(index) != -1)
                PrintPretty(getLeftChildIndexAtPos(index), indent, getRightChildIndexAtPos(index) == -1, true);
            if (getRightChildIndexAtPos(index) != -1)
                PrintPretty(getRightChildIndexAtPos(index), indent, true, true);
        }

    }
    #endregion

    #region arrayWrap
    int getIndexedDataCountAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].ContentCount;
    }
    void resizeIndexedDataCountAtPos(int key)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].resize();
    }
    void resetIndexedDataCountAtPos(int key)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].ContentCount = 0;
    }
    void incrementIndexedDataCountAtPos(int key)
    {
        ++ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].ContentCount;
    }
    void decrementIndexedDataCountAtPos(int key)
    {
        --ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].ContentCount;
    }
    int[] getIndexedArrayDataAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].indexedArrayData;
    }
    void setIndexedArrayDataAtPos(int key, int[] indexedArrayData)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].indexedArrayData = indexedArrayData;
    }
    int getIndexedArrayDataAtPos(int key, int subPos)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].indexedArrayData[subPos];
    }
    void setIndexedArrayDataIndexPos(int key, int subPos, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].indexedArrayData[subPos] = value;
    }
    int getHashAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].hashValue;
    }
    void setHashIndexPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].hashValue = value;
    }
    int getRightChildIndexAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].rightChildIndex;
    }
    void setRightChildIndexAtPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].rightChildIndex = value;
    }
    int getLeftChildIndexAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].leftChildIndex;
    }
    void setLeftChildIndexAtPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].leftChildIndex = value;
    }

    int getLastIndexAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].lastIndex;
    }
    void setLastIndexAtPos(int key, int value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].lastIndex = value;
    }

    bool getIsRedAtPos(int key)
    {
        return ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].isRed;
    }
    void setIsRedAtPos(int key, bool value)
    {
        ArrayData[key / DEFAULTCAPACITY][key % DEFAULTCAPACITY].isRed = value;
    }
    #endregion
}

 

using System;
using System.Collections.Generic;
using UnityEngine;
/*
This eventHandlerManager supports registering/unregistering listeners for specific events and it's usable 
even in Awake functions since its actual initialization happens in Reset time through the InitializeDictionary static function
This class is usable both as a static reference and as a specific instance.
eventHandlerManager has also a debug feature that can be used to check on wether events 
are happening and where (if any) crashes are happening in a series of callbacks for the same event.
To debug the static instance through the inspector an instance of the script needs to be added in the scene but it's not needed for usage.
It's the script's responsibility to remove listeners when the script is destroyed, otherwise there will be an error in the callback chain that will
block the execution of the successive callbacks.
*/
public class eventHandlerManager : MonoBehaviour
{
    //platform-conditional compilation helps to avoid the debug overhead in the final build.
#if UNITY_EDITOR
    //each flag controls a different debug message
    public bool debug;//print debug when any event is broadcasted, unless debugSpecificEvent is true, in which case only that one event is debugged
    public bool debugAdds; //print debug when any listener is added
    public bool debugRemovals;//print debug when any listener is removed
    public bool debugSpecificEvent;//print debug when a specific event is broadcasted, after each callback, 
    public EventPicker picker; //component that gives the UI element to select the event to debug with dropdowns
    //invoked after a debugAll check
    bool shouldDebugEvent(int ev)
    {
        return (!debugSpecificEvent) || (ev == picker.Selected);
    }
    public static eventHandlerManager globalDebugController;
    public bool debugGlobally;//if true activates the debugging for the global eventHandlerManager
    /*
     in reaction to setting the debugGlobally variable, set the instance as 
    globalDebugController so that its debug fields are used for the static
    functions to control debugging, it also ensures that only one debugGlobally 
    variable can be set as true in all the instances of the eventHandlerManager
    */
    void OnValidate()
    {
        if (debugSpecificEvent)
            debug = true;
        if (debugGlobally)
        {
            eventHandlerManager temp = globalDebugController;
            globalDebugController = this;
            if (temp != null)
                if (temp.GetInstanceID() != this.GetInstanceID())
                    temp.debugGlobally = false;
        }
        else
        {
            if (globalDebugController != null)
                if (globalDebugController.GetInstanceID() == this.GetInstanceID())
                    globalDebugController = null;
        }
    }
#endif
    //these dictionaries host the callback delegates, they are initialized by the initializeDicts functions, so that they are already initialized when the first Awake is called.
    static Dictionary<int, Dictionary<int, GarbagelessList>> globalListenerFunctions = initializeDicts();
    Dictionary<int, Dictionary<int, GarbagelessList>> ListenerFunctions = initializeDicts();
    #region broadcast
    public static void globalBroadcast(MonoBehaviour source, eventChannels evType, int ev, object e)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeBroadcast(false, false, source, evType, ev, e, globalListenerFunctions);
        else
#endif
            executeBroadcast(
#if UNITY_EDITOR
                globalDebugController.debug, globalDebugController.shouldDebugEvent(ev),
#endif
                 source, evType, ev, e, globalListenerFunctions);
    }
    public void Broadcast(MonoBehaviour source, eventChannels evType, int ev, object e)
    {
        executeBroadcast(
#if UNITY_EDITOR
                debug, shouldDebugEvent(ev),
#endif
                source, evType, ev, e, ListenerFunctions);
    }
    static void executeBroadcast(
#if UNITY_EDITOR
        bool debug, bool specific,
#endif
        MonoBehaviour source, eventChannels evType, int ev, object e, Dictionary<int, Dictionary<int, GarbagelessList>> target)
    {
#if UNITY_EDITOR
        //if the flags are true prints a list of what is going to be invoked before execution

        if (debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast" + evType + " - " + ev + " calling " + target[(int)evType][ev].Count + " functions:");
            int i = 0;
            target[(int)evType][ev].Reset();
            while (target[(int)evType][ev].MoveNext())
            {
                Debug.Log(evType + " - " + ev + " [" + i + "](" + target[(int)evType][ev].Current.Method.DeclaringType.ToString() + ">" + target[(int)evType][ev].Current.ToString() + ")");
                ++i;
            }
            Debug.Log("EventHandleManager Broadcast - START");
        }

#endif
        //invoke event delegates
        target[(int)evType][ev].Reset();
        while (target[(int)evType][ev].MoveNext())
        {
            target[(int)evType][ev].Current(e);
        }
#if UNITY_EDITOR
        if (debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast - OVER");
        }
#endif
    }
    #endregion
    #region AddListener
    public static void globalAddListener(eventChannels evType, int ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeAddListener(false, false, false, evType, ev, eventListener, globalListenerFunctions);
        else
#endif
            executeAddListener(
#if UNITY_EDITOR
                globalDebugController.debug, globalDebugController.shouldDebugEvent(ev), globalDebugController.debugAdds,
#endif
                 evType, ev, eventListener, globalListenerFunctions);
    }
    public void AddListener(eventChannels evType, int ev, gameEventHandler eventListener)
    {
        executeAddListener(
#if UNITY_EDITOR
                debug, shouldDebugEvent(ev), debugAdds,
#endif
         evType, ev, eventListener, ListenerFunctions);
    }
    static void executeAddListener(
#if UNITY_EDITOR
                bool debug, bool specific, bool debugAdds,
#endif
         eventChannels evType, int ev, gameEventHandler eventListener, Dictionary<int, Dictionary<int, GarbagelessList>> target)
    {
        target[(int)evType][ev].Add(eventListener);
#if UNITY_EDITOR
        //if this event's execution should be logged, add a debugging delegate before the method
        if (debug && specific)
        {
            target[(int)evType][ev].Add(new gameEventHandler(delegate (object e)
             {
                 Debug.Log("EventHandleManager execution" + evType + " - " + ev +
                     " finished " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
             }));
        }
        if (debugAdds)
            Debug.Log("EventHandleManager Added event " + evType + " - " + ev +
                        " by " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
#endif
    }
    #endregion
    #region RemoveListener
    public static void globalRemoveListener(eventChannels evType, int ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeRemoveListener(false, evType, ev, eventListener, globalListenerFunctions);
        else
#endif
            executeRemoveListener(
#if UNITY_EDITOR
                globalDebugController.debugRemovals,
#endif
                 evType, ev, eventListener, globalListenerFunctions);
    }
    public void RemoveListener(eventChannels evType, int ev, gameEventHandler eventListener)
    {
        executeRemoveListener(
#if UNITY_EDITOR
                debugRemovals,
#endif
                evType, ev, eventListener, ListenerFunctions);
    }
    static void executeRemoveListener(
#if UNITY_EDITOR
                bool debugRemovals,
#endif
        eventChannels evType, int ev, gameEventHandler eventListener, Dictionary<int, Dictionary<int, GarbagelessList>> target)
    {
#if UNITY_EDITOR
        if (debugRemovals)
            Debug.Log("EventHandleManager Removed event " + evType + " - " + ev +
                        " by " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
#endif
        target[(int)evType][ev].Remove(eventListener);
    }
    #endregion

    public void OnDestroy()
    {
        ListenerFunctions = initializeDicts();
    }

    /*
    this method initializes the ListenerFunctions dictionary by doubly indexing first by eventChannel and then by specific event. The initialization consists in an empity gameEventHandler to which new listeners will eventually be added with the AddListener functions
    */
    static Dictionary<int, Dictionary<int, GarbagelessList>> initializeDicts()
    {
        //gets the information on the structure of channels from ChannelEnums
        Dictionary<eventChannels, Array> enumChannelEventList = ChannelEnums.getChannelEnumList();
        Dictionary<int, Dictionary<int, GarbagelessList>> result = new Dictionary<int, Dictionary<int, GarbagelessList>>();
        foreach (var val in (eventChannels[])Enum.GetValues(typeof(eventChannels)))
        {
            result.Add((int)val, new Dictionary<int, GarbagelessList>());
            foreach (var ev in enumChannelEventList[val])
            {
                //adds an empity gameEventHandler for each event
                result[(int)val].Add((int)ev, new GarbagelessList());
            }
        }
        return result;
    }
}
//delegate signature for the callback functions
public delegate void gameEventHandler(object e);

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •