Thursday, June 30, 2005

Coming Back To The Clock

So let's say you've followed my tutorial over there and have a nice clock and you can do groovy animations and various other things all to your hearts contents.

. . . then you decide you'd like to add a bit more functionality . . .

          Yeah sorry about that - it's not the easiest thing to work with.

My current version of the clock includes pausing, deleting and saving & loading (of time events). It also allows a number of clock instances (it's no longer a singleton, not really anyway.) But this proved rather difficult.

In this post I'll suggest things you might like to add and some of the pitfalls you might encounter. As well as a few brief notes on what I did.

Here are a few things you might wish to try your hand at!

Extension Challenges

  • Pause a single time event.


    In the clock class try to write the functions: Pause(TimeEvent t) and Unpause(TimeEvent t). You will have to extend the TimeEvent class, also watch out when sorting the time event list.


  • Delete a single time event


    Try writing the function Delete(TimeEvent t). This is pretty easy. Of course for some object, that-is-creating-and-using-timeEvents, to use this you'd need it to store a local copy of it's TimeEvents. (an orc, let's say, would have to keep a copy of all the timeEvents it send to the clock - like Attack Attack GrowlFiercely, if you decided you didn't want to growl you'd then be able to write Clock.Delete(GrowlFiercely)). I didn't do it this way.


  • Saving and Loading


    Needs some thinking about. I had the TimeEvent class implement ISerializable. The MSDN docs on this are great - I couldn't find too much useful information elsewhere. Try not to think about how to save the delegate until the end.




Notes on how things can get tricky

I avoided much trickiness by not going for the most general solution - rather I kept in mind what I needed and merely handled that.

Delete

How do we know which event we wish to delete without storing a local copy?

Reads one of my comments at the top of the clock class. A Delete() function that deletes everything isn't hard to write. We merely set the kill flag and run the purge events function.

Constraints is the answer. For my purposes I don't need a delete saying "oh delete the third timeEvent I added". What exactly do I need then?

Let's imagine you're fierce warrior is surrounded by 100 orcs. In the TimeEvent queue there are six attacks waiting to happen. Your warrior stabs one in the stomach and it goes down dead on the floor.

Now - we need to get rid of that orcs attack that was due to happen in 3 seconds. Not just it's attack but any time events currently relating to it. We could do with a function saying delete all time events that are parented by object X. In our Orc case we'd want Delete All TimeEvents that the Orc is a parent of.

There's possibly a way to find the parent of a given timeEvent using reflection on the delegate - but I failed utterly to do this. If you know how to do this please drop me a comment or a mail.

I made all time events store who created them. This allows enemies to die without causing problems. After this I created
the function Delete(parent, call). This took killed all TimeEvents of a given type parented by a certain object. If you've just broken the orcs sword he can't still run his stab event - it must be removed. Therefore Delete(orc, StabAttack) is called. (Though this could always be handled in the StabEvent code in the Orc class.)

(I dearly wanted to use reflection for that too but I couldn't get the method's name from the delegate I only ever got Invoke. - I know the actual method name's hiding in there somewhere though.)

Loading and Saving

This is tricky to begin with. Once you create a new clock and wish to load old events they're going to be running on different time lines than before. Maybe this is hard to visualize so I'll try to be clearer.

Clock 1 - Current Time: 5
EventStarted: 4
EventEnds: 8

No problem. We're going to fire in three seconds. But wait! Let's say the user has just saved and quit! Then let's say we just save exactly what we've got . . . well problems abound! The player has just reloaded. His machine time will be totally different!

Clock2 - Current Time: 10000
EventStarted: 4
EventEnds: 8

Well bang the event fires immediately. Maybe now you see the problem. Also remember we may want to save Paused Events and we may also want to save events that are going to repeat once they die - therefore the time they take to execute must be stored.

These problems are a little tricky but (well at least for me - I can't do arthimetic in my head) a small pad of paper and a little bit of time and the problems can be overcome.

The tricky bit is the Delegate. The time event is suppose to call a function. How on earth are you going to save that?

*Possibility 1*
Every object using time events is given a HASH (unique identifying number). This is saved with the object. These hashes are permenant over many loads and saves.

Then we store a copy of the hash in the time event. (also could be useful for delete - use hash to decide who is the parent of the current time event).

All the objects in the game are stored in a hashtable with this hash as their key. (I'm sure you know O(1) access and all that the look up wouldn't cost a lot at all). Let's use the orc example       HashTable(Orc.Hash) -> Orc.

In TimeEvent we store
   a.) The Hash
   b.) The Method name

Then we load an event. The event throws the hash into the hash table and is given the object. Using some nifty reflection with the string of the method name we get the delegate to point to the correct method.

The result is we can load and save entire clocks (provided all objects are loaded first).


And that's one way. But that's seems to be a lot of work. Also it couples the Clock to my code. I don't really want that - I'm after nice and modular. Even though it's small I have thoughts of putting Clock into it's own library that I can use anywhere. I also have trouble doing reflection to get the pointer to method (though with a little messy coding I could have just about managed this).

Also what if the object hadn't been loaded yet - surely a nasty crash, although this probably isn't a big program. Anyway I decided against it. So we go to possibility two (constraining my needs).

*Possibility 2*

Make objects responsible for saving their own children.
They load them, sort out the delegate (using a simple switch statement and some of the code I've implemented with delete) and then add themselves as parent (using the code that was added with delete functionality)


So the second possibility was more to my taste. It seems to work pretty well, too!

Why would you ever want to load and save time events?

You might not! In fact it's entirely possible that you can get away without ever needing to do this! But let's say you wanted to allow saving during combat.

You need to save the current attacks and when they'll fall. Has the magic elf finished concentrating on his battle winning spell - that sort of thing. Of course if it's turned based and not using time events for battle stuff you don't really need to either.

Another example might be a timed mission. If the bombs going to go off a six. You really should remember to save that.

If your world is going to be quite dynamic though - you are going to need to save your time events.

Final Note

So there's one issue I just came upon today that I thought would be worth mentioning.

A problem arises if you want a TimeEvent to create another TimeEvent.

And we all want to do this because it's groovy :D First let's go ove why there's a problem then the solution.

The why. Well the clock trawls through all it's timeEvent at once. It's in the timeEvent queue. If one TimeEvent says "oooh let me add a new TimeEvent" - there's a problem. We're trawling through the queue we can't change it's size mid-trawl. If you try to do this you will get a vague error that will leave you midly baffled.

So the solution follows much the same pattern of how we kill TimeEvents (using a killflag). We add bool ExecutingTimeEvents. This is set to true at the start of the list and set to false at the end. Then in the add a time event function before we add to the TimeEvent-Queue we check if we're executing. If we are we instead an to a second queue called something like pending. Then after we trawled the entire list we take all the TimeEvents from pending and put them into the actual list and then sort it.

And that's the fix now you can do more cool stuff.
Post a Comment