Collections
This page is up to date for MonoGame.Extended 4.0.3
. If you find outdated information, please open an issue.
What is a collection?
A collection is a data structure with functionality built around it. Think about a collection as a grouping of similar things. All the enemies in a game, the items in the inventory, the textures being drawn, images in an animation, or bullets on the screen.
Collections are more advanced than a simple array or list, and each one can provide unique functionality. There are benefits and disadvantages to each collection type. Don't use a collection without a reason, especially if a simple array or list will suffice. Make sure the collection you are choosing has a purpose that is advantageous to you and your project.
.NET has many default Collection types (List, Dictionary, Queue, Stack, and many more). These are a few additional collections.
General requirements for all collections
All of these require adding the using directive for MonoGame.Extended.Collections
in your file.
using MonoGame.Extended.Collections;
Bag
A Bag
is an un-ordered array of items with fast Add
and Remove
properties.
Bag Functionality and Behavior
- It is much faster than an array when removing items.
- Takes less space than a linked list.
- Bag will resize itself (grow by 1.5 times current size) only when it needs to.
- Used space won't shrink after removing elements.
- You can clear the bag.
- Can't sort the bag.
- Order is not preserved.
- Elements are added to the end of the collection.
Bag Example
The example below creates a new Bag
of size 3 with type int
, adds three integers
to the bag, adds a fourth item, and removes one item. When the fourth item is added this causes a resize of the capacity by 1.5 times the current capacity.
var bag = new Bag<int>(3);
bag.Add(4);
bag.Add(8);
bag.Add(15);
// bag is now [4, 8, 15]
bag.Add(16);
// bag is extended here, capacity is now 4 instead of 3 with items [4, 8, 15, 16]
bag.RemoveAt(1);
// bag is now [4, 16, 15] with a capacity of 4
bag.Remove(4);
// bag is now [15, 16] with a capacity of 4
Notice in the example when we removed the the second element (value 8) with RemoveAt(1)
the last item (value 16) moved to it's place. This is because when an item is removed the bag
will reposition the last element into the removed spot. This is to prevent the code from needing to move every element in the bag. Meaning, do not count on the order of the items in the bag!
This happens again when we use Remove
to look for a specific VALUE in the bag, and remove that item. 15, the last element is swapped into the removed spot.
Deque
Represents a collection of objects in which elements can be added to or removed either from the front or back. See double ended queue.
Deque Functionality and Behavior
The front of the queue is the first item removed when you Pop
an item off (The left of the list), where-as the back is the last item removed with Pop
(The right of the list).
[3, 6, 8, 9, 0, 5, 32]
In the above list of elements, the 3 is the back, and the 32 is the front.
Deque Example
var deque = new Deque<int>();
deque.AddToBack(1);
deque.AddToBack(2);
deque.AddToFront(4);
deque.AddToFront(5);
deque.AddToBack(3);
while (deque.Count > 0)
{
int item = deque.Pop();
Console.WriteLine(item);
}
Result
3
2
1
4
5
KeyedCollection
KeyedCollection
is a wrapper around the Dictionary
class where the key is obtained by a delegate.
This allows you to use a function as the key. While this could be any function, a good example would be using a property of the class in the collection as the key, like an ID field.
Keyed Collection Functionality and Behavior
- The delegate function needs to return a unique value for the key since it will be used as the dictionaries key.
- Simplify adding an Entity to a dictionary by a key value that's inside the Entity.
- Provide helper methods to search for items in the collection
TryGetValue
Lets create an object we can use in the keyed collection.
public class MyEntity
{
public int Id {get; set;}
public string Name {get; set;}
public MyEntity() { }
public MyEntity(int id, string name)
{
this.Id = id;
this.Name = name;
}
}
Now lets use that in the KeyedCollection
.
var keyedCollection = new KeyedCollection<int, MyEntity>(e => e.Id);
keyedCollection.Add(new MyEntity (1, "Player1"));
keyedCollection.Add(new MyEntity {Id = 2, Name = "Player2"});
keyedCollection.TryGetValue(1, out MyEntity entity); // gets Player1
Above we created a new KeyedCollection using the Id
field as the key, and storing the MyEntity
as the value. In the example we added two new instances for MyEntity, and then retrieved an item from the collection using the ID field.
Object Pooling
Pooling of Objects allows reuse of memory for a group of items to avoid Garbage Collection. These are a bit more advanced, and an entire page is dedicated to them.
More information is in the Object Pooling documentation.
ObservableCollection
ObservableCollection<T>
manages an IList<T>
of items firing ItemAdded
, ItemRemoved
, Clearing
, and Cleared
events when the collection is changed.
This allows you to monitor when items are added, removed, being clearing, or cleared. You could then perform an action whenever this happens. If you're familiar with databases, this would be similar to insert/update/delete triggers.
It's for a more event-driven architecture common in GUIs.
ObservableCollection Example with Methods
In this first example, we'll create a blank ObservableCollection<int>
, then add, remove, and clear from it to demonstrate the Event Handlers.
ObservableCollection<int> observableCollection = new ObservableCollection<int>();
Next, we need to create some methods that will handle the events
public void ItemAddedWatcher(object sender, ItemEventArgs<int> e)
{
Debug.Print($"Item Added: {e.Item}");
}
public void ItemRemovedWatcher(object sender, ItemEventArgs<int> e)
{
Debug.Print($"Item Removed: {e.Item}");
}
public void WatchedListClearing(object sender, EventArgs args)
{
Debug.Print("List is clearing....");
}
public void WatchedListCleared(object sender, EventArgs args)
{
Debug.Print("List is now cleared!");
}
Now we need to wire-up (associate) the event handlers to the methods so they are connected.
observableCollection.ItemAdded += ItemAddedWatcher;
observableCollection.ItemRemoved += ItemRemovedWatcher;
observableCollection.Clearing += WatchedListClearing;
observableCollection.Cleared += WatchedListCleared;
Finally we need to manipulate the ObservableCollection so we can watch the events get triggered
observableCollection.Add(1);
observableCollection.Add(2);
observableCollection.Add(77);
observableCollection.Add(3);
observableCollection.Remove(2);
observableCollection.Add(42);
observableCollection.Clear();
The output should be
Item Added: 1
Item Added: 2
Item Added: 77
Item Added: 3
Item Removed: 2
Item Added: 42
List is clearing....
List is now cleared!
ObservableCollection Example with Anonymous functions
In this example we will pass in an already existing collection (A list), and use Anonymous functions
in-line.
Create a list, and then pass it to an ObservableCollection with the same data type.
List<int> sourceList = new List<int> {1, 2, 3, 55, 88, 13, 23, 42 };
ObservableCollection<int> observableCollectionAlt = new ObservableCollection<int>(sourceList);
Wire-up (associate) all the event handlers to the anonymous functions
observableCollectionAlt.ItemAdded += (sender, e) =>
{
Debug.Print($"Item Added: {e.Item}");
};
observableCollectionAlt.ItemRemoved += (sender, e) =>
{
Debug.Print($"Item Removed: {e.Item}");
};
observableCollectionAlt.Clearing += (sender, args) =>
{
Debug.Print("List is clearing....");
};
observableCollectionAlt.Cleared += (sender, args) =>
{
Debug.Print("List is now cleared!");
};
Perform some changes to the ObservableCollection and you'll see debug prints.
observableCollectionAlt.Add(7);
observableCollectionAlt.Add(412);
observableCollectionAlt.Remove(88);
observableCollectionAlt.RemoveAt(3);
observableCollectionAlt.Clear();
This will print out the following:
Item Added: 7
Item Added: 412
Item Removed: 88
Item Removed: 55
List is clearing....
List is now cleared!
Extensions to existing .NET collections
MonoGame.Extended
contains Collections
extensions to the C# collections that are useful for game programming. An extension in .NET is essentially just adding additional functionality to an existing Type Class, without needing to re-compile the source code for .NET 8.0. Read more about them in the link above.
ListExtensions
Adds Shuffle(Random)
to all IList<>
classes. This extension requires that you pass in an instance of the System.Random
class.
// .... setup example
using System;
using System.Collections.Generic;
Random random = new Random();
List<int> nums = new List<int>{1, 2, 3, 4, 5};
// .... end setup example
using MonoGame.Extended.Collections;
// The Extension method for List `Shuffle` called
nums.Shuffle(random);
The list nums
will now be randomly shuffled. We didn't have to recompile .NET either.
DictionaryExtensions
Extends all Dictionary<>
classes with GetValueOrDefault(key, default)
. It allows you to specify a default to return when the key is not found.
// .... setup example
using System.Collections.Generic;
Dictionary<string, int> zipCodeLookupByCity = new Dictionary<string, int>();
// .... end setup example
using MonoGame.Extended.Collections;
// The Extension method for dictionary to return a default if no key found
int zipCode = zipCodeLookupByCity.GetValueOrDefault("typo city name", 90210);
The above code has a made up situation where we want to be able to lookup zipcodes (Postal Codes) by the city name. In this case, someone typed the city name wrong, but we want to always default the zip code to Beverly Hills zip code of 90210.