right, so I’ve changed the approach a little, given that we already store the indice of our elements.
if you think about it, we’ve had a queue object which could enqueue/dequeue another type of object, and what it gave us in return was a positional index. and that was it.
so what if we kept track of what was actually called and used that to determine the state of all the icons?
so far we’ve had
Enqueue, Dequeue, RemoveAt, and Clear.
that was our primary interface.
Enqueue expects this to be a new object, and it pushes it to a list, so the icon has to appear, right?
Dequeue does the opposite and pops the list, so the icon should disappear.
Clear does this with all icons, so all of them should disappear.
but now this leaves us with RemoveAt, which behaves somewhat differently.
if we had [TRACTOR] [CAR] [DOZER] [HELI] and wanted to remove element at index 2, that would remove [DOZER] and move [HELI] to the left. so [DOZER] should disappear, and [HELI] should just update, ok?
in the meanwhile I’ve implemented Insert as well, so we have an opposite to this to account for.
if we had [TRACTOR] [CAR] [HELI] and wanted to insert [DOZER] at index 2, that would make [HELI] to update and [DOZER] to appear.
ok, so we need a way to track these states but also to get them out of the system, but also notice that BuildQueue might not know all of our icons in advance. if you feed them one by one, in some random order, it’ll slowly learn about them and track their states properly. in the beginning, however, it knows nothing about them and doesn’t need to, so we need to have an undefined icon state as well. let’s just call it Unknown. this is just so that it doesn’t needlessly crash in our faces if we ask it too much.
but now when we’re at it, why not automate the whole thing, so that when we poke something through all this en-queuing and de-queuing (oh boy I’m tired of typing these words), it auto-escalates back to our actual icon controller, responsible for drawing and updating the icons themselves, prompting for a re-evaluation of all the states and adjusting the graphics to accommodate the changes.
so we introduce the event system.
let’s do things one at a time, so you can see what’s going on
here are our states
public enum BuildQueueItemState {
Unknown,
Appear,
Disappear,
Update
}
let’s now introduce the state tracker, that sits along with the list declaration
private Dictionary<T, BuildQueueItemState> _tracker;
simply put, each state will correspond with an object 1 to 1.
we need to create this dictionary in the constructor
public BuildQueue(int maxItems) {
MaxItems = maxItems;
_list = new List<T>(maxItems);
_tracker = new Dictionary<T, BuildQueueItemState>();
}
now we can ask whether BuildQueue knows about the particular queued object. even if it’s not in the queue itself anymore, it should know about it from the past, right? this will work very fast, regardless of how many icons we might end up with potentially.
public bool KnowsAbout(T obj) => _tracker.ContainsKey(obj);
but we’d also like to know if the object is in the queue right now, so let’s take an advantage of this.
public bool Contains(T obj) => KnowsAbout(obj) && _tracker[obj] != BuildQueueItemState.Disappear;
_tracker[obj] will never blow up, because if KnowsAbout(obj) returns true, we know it’s safe to ask about it. however, if we find out that it was set to disappear, that means it must’ve been removed from the queue already. so whether an object is contained right now or not has to be an object that is known to have either appeared or updated.
now we can interrogate the last known state of the icon, without having it blow up if it hears about it for the first time.
public BuildQueueItemState StateOf(T obj) {
if(KnowsAbout(obj)) return _tracker[obj];
return BuildQueueItemState.Unknown;
}
ok, so that’s that. now let’s address the actual methods. first Enqueue
// if full or if object already exists, returns false, otherwise true
public bool Enqueue(T obj) {
if(Count == MaxItems || Contains(obj)) return false; // safety check
_tracker[obj] = BuildQueueItemState.Appear; // we can tell it should appear
_list.Add(obj);
return true;
}
this adds another layer of safety that we didn’t have before. namely, if you repeat the same object twice, it’ll ignore you and return false. previously it would let you do it, but a wrong assumption could have cost us a corrupted state.
Dequeue
// throws an error if empty
public T Dequeue() {
T obj = _list[Count - 1];
_tracker[obj] = BuildQueueItemState.Disappear;
_list.RemoveAt(Count - 1);
return obj;
}
Clear
public void Clear() {
for(int i = 0; i < Count; i++) {
_tracker[_list[i]] = BuildQueueItemState.Disappear; // everything has to go
}
_list.Clear();
}
RemoveAt
// throws an error if empty
public T RemoveAt(int index) {
T obj = _list[index];
_tracker[obj] = BuildQueueItemState.Disappear; // this one should disappear
if(index < Count - 1) { // if index was anything less than the last element
for(int i = index + 1; i < Count; i++) {
_tracker[_list[i]] = BuildQueueItemState.Update; // everything to the right of it should move
}
}
_list.RemoveAt(index);
return obj;
}
a combination of Enqueue and RemoveAt works for Insert
// if full or if object already exists, returns false, otherwise true
public bool Insert(int index, T obj) {
if(Count == MaxItems || Contains(obj)) return false; // same as in Enqueue
_tracker[obj] = BuildQueueItemState.Appear;
if(index < Count - 1) {
for(int i = index + 1; i < Count; i++) {
_tracker[_list[i]] = BuildQueueItemState.Update; // everything to the right of it should move
}
}
_list.Insert(index, obj);
return true;
}
all that has remained is to add an event, but let me glance over some other stuff from the previous version
these two lines of code are useful when working with collections such as this.
public T this[int index] => (index < Count)? _list[index] : null; // I've modified this a bit
public int IndexOf(T obj) => _list.IndexOf(obj);
IndexOf simply exposes the list’s IndexOf method which works by finding an index for a given object.
it’s a slow method, so it’s best used with small lists, but we intend to make this a small thing anyway, right?
it makes sense that you want to see immediately where a specific icon has been stored, for whatever reason.
the other thing is called an indexer and it helps us access this collection as if it were a list or, well, a collection of objects. it’s basically an inverse of IndexOf where
var a = queue[3];
Debug.Log(queue.IndexOf(a)); // 3
and in the end we had this part, that probably looks intimidating
public IEnumerator<T> GetEnumerator() {
for(int i = 0; i < Count; i++) yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
truth being told you may ignore it entirely, but it goes hand in hand with our interface IEnumerable that is used to explain that the point of this object is to be used as a collection
the first method is used to implement the requirement of IEnumerable, whose job is to explain how we’re supposed to iterate over its elements when we use a foreach clause. you don’t have to know much about this, it’s something that could just as easily explode into a topic of its own, other than how to use it
in this case it’s simply
var queue = new BuildQueue<string>(5);
queue.Enqueue("thing1");
queue.Enqueue("thing2");
foreach(var item in queue) {
Debug.Log(item); // will output "thing1" and "thing2"
}
the last method is the bane of many rookies. “what is it? why it looks so complicated?” the truth is that it’s used only to internally satisfy a legacy C# non-generic interface. in other words, it practically does nothing for you, but the thing won’t compile without it, so there’s that.
I’ll add the event in the next post.
and then we can figure out how to complete the picture.