GameMonkey Script

GameMonkey Script Forums
It is currently Sun Sep 15, 2019 6:07 am

All times are UTC




Post new topic Reply to topic  [ 12 posts ] 
Author Message
PostPosted: Mon Sep 29, 2008 4:39 am 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
So I have my engine very nicely integrated with GameMonkey to the point where they're no inseparable - it's fantastic, I couldn't be happier with a scripting language.

The last major technical requirement I have is I want to be able to store the state of gmMachine and be able to revert to it at any point. What I want to do is each frame copy the state of the game running so I can reverse to a previous state.

The idea is I want to be able make changes to the script that take effect instantly - I have this working. However I want the ability to pause simulation, go back in time and see the new changes. ie: I'm playing the game, I see an enemy react in a way I didn't want them to, I want to pause the game, make the changes, (I have this working up to this point), but now I want to revert the machine to a state 10 seconds before.

Is this possible? Is this easy? What do I need to consider when doing this?

Thanks so much!
-Shawn.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 29, 2008 6:12 pm 
Offline

Joined: Fri Jan 14, 2005 2:28 am
Posts: 439
The only option I think is serializing the machine out every frame, which is likely to be extremely costly in both cpu usage and storage costs.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 29, 2008 8:19 pm 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
That's what I had in mind. I'll just create a big ass memory block - like 512MB or something, and just memcpy all appropriate data per frame, I'm just unsure about what data I actually need.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 29, 2008 8:47 pm 
Offline

Joined: Fri Jan 14, 2005 2:28 am
Posts: 439
The biggest problem is saving and restoring user types. The gmVariable essentially just holds a pointer to them, so you basically need to serialize the actual user types, which could be significantly more than the size of a gmVariable. It would be nice to build in serialization support to the gmMachine, and would probably involve additional callbacks for user types, such as the asstring callbacks for serializing user types. If any of them returned false perhaps the serialization would fail. Basically need to recursively serialize the global table, all gmThreads, all blocks, probably the actual script opcodes each thread is running(stored in gmThread?). Basically probably need to go through all objects similar how the garbage collector goes through all objects(global table, stacks of each gmThread, etc).


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 30, 2008 6:40 am 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
That's actually fairly good news. I'm trying to remove all user types completely now. It makes GM become "too integrated" with my engine - ie it won't really work outside of it, but it'll be very fast and specialized.


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 30, 2008 3:42 pm 
Offline

Joined: Fri Jan 14, 2005 2:28 am
Posts: 439
It depends on how you are using the scripting I suppose, but if you expose your entities to scripting you can't really make something like that a non user type. You can do what I have done and instead of my 'Entity' user type being a pointer to my entity, instead it is a handle that can be passed around by value. That is, I have added a int m_hndl; to the gmVariable union to act as a handle for my entities. In this way 'entity' references are passed around by value, and can be serialized provided I serialize the handle structure. My handles are simply 2 shorts put into an int. The first short is an index into an entity array, and the 2nd short is a serial number, that gets incremented if an entity gets free'd from a particular array index. Using arrays keeps the handle lookup as fast as it can get, while the serial number is the main part behind the handles ability to be matched to an entity. If the serial number at the specified index doesn't match the serial of the handle, the handle is invalid. To serialize this, one would just need to loop through the entity array and serialize the entity at each index(if one exists), along with the entire list of serial numbers for each slot. Other than that though, I have other script user objects that would be far too big to include in the gmVariable as a 'by value' object, so serializing those would be a pain. Depends on your usage. In any case a callback for user objects to serialize to a stream would be a good thing to add to the core of GM.


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 12:32 am 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
I've been experimenting with that idea... I read how Halo 2 did it and implemented something similar.

I couldn't get great performance because I couldn't figure out an optimal solution to storing the "free" spaces... ie, I have an entity pool that's 100 entities large, and I allocate 5, meaning the next free space is 6, then I free entity 3. How did you solve this? Did you move the next free space to 3, then do a linear search for the next free space once it's allocated? did you store a stack of all free slots lower than the max used index?

I'm trying to figure out exactly the most efficient way of doing that.


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 1:10 am 
Offline

Joined: Fri Jan 14, 2005 2:28 am
Posts: 439
I'm not familiar with how Halo2 did it, got a link to their method? I took my method from Half-life 2's entity handle system. They have a static sized array of entities in their game, say entity *GameEntities[4096]; When an entity is created, it is just created in some free slot, find a free slot with a linear search, no need to optimize that. Entities aren't created often enough to matter, and rarely does the # entities get up in the thousands. Player entities reserve the first 32, 64, etc slots typically. This method goes way back to quake 2, though they didn't use handles back then really, everything basically operated off of the entity index. The slot it gets created in and the serial number of that slot when it gets created is mashed together to make up the entities handle. The server is authoritative over the slot, and tells the client, "this entity was created at index x with serial x". In this way, the handles are networkable as well, which is very nice, and because the handle is made up of an array index, looking up actual objects from the handle is simple and fast, it's just an array index(provided the serial number matches). If the serial doesn't match it's an old and bad handle, and just return null. Also being integer sized, this handle is a good candidate to put into a gmVariable and otherwise expose to script to pass around instead of a user object that is subject to garbage collection. I don't use my handles as extensively in my script bound objects as I should, as most of them are multiple years old at this point. I hope to get some extra time to clean that up at some point.


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 1:19 am 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
Here's how Halo did it:
http://www.game-tech.com/Talks/Butcher.doc

maybe I got a little too fancy. I wanted Entities to be constructable, so I created an entity manager for each type, and templated the initialize methods to accept multiple constructor parameters. I also stored an array + 16 bits: 8 for a salt, and 8 for a counter, so they could be reference counted. I then had the destructor of the handle free the resource, so they were a completely refcounted - this might have been a bad idea.

The data array looked like:
Code:
struct {
   union {
      struct { ubyte counter; ubyte salt; };
      uint16 hash;
   };
   ubyte data[sizeof(T)];
} data[size];


The init methods were like:
Code:
inline Handle<T> getNew() {
   Handle<T> h;
   h.m_index = first_free_slot++;
   h.m_salt = data[h.m_index].salt;
   data[h.m_index].counter = 1;
   new(&(data[h.m_index].data)) T();
   h.dataum = this;
   return h;
}
template <typename A1>
inline Handle<T> getNew(A1 a1) {
   Handle<T> h;
   h.m_index = first_free_slot++;
   h.m_salt = data[h.m_index].salt;
   data[h.m_index].counter = 1;
   new(&(data[h.m_index].data)) T(a1);
   h.dataum = this;
   return h;
}


The free method would lower the counter and change the salt if count == 0.

Did you create a single array for *all* entities?
If so, how did you deal with indexing different sized entities?


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 4:24 am 
Offline

Joined: Fri Jan 14, 2005 2:28 am
Posts: 439
It's not an array of the actual entities, it's an array of Entity *'s, Entity being the base class of your game entities, even if it's just an empty base class simply to use it as a base class pointer. Then your array of pointers could be to anything. In the case of Half-life 2, they have a baseentity class, and the array is just an array of baseentity *, so into that they can make any game entity, since their game uses that as a common base class. That may be less desirable if you are trying to refcount a bunch of unrelated objects. I personally don't bother with ref counting, because I have things set up such that scripts don't own anything(user objects that is), so whether or not there are any references to the objects doesn't really matter, as the scripts don't control their lifetime.


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 5:24 am 
Offline

Joined: Thu Sep 25, 2008 6:28 am
Posts: 24
Ah.. the halo method allowed removing all dynamic memory allocation, speeding the game up, and making a save state as simple as a memcpy or a core dump... making everything great.

There's a method in this month's Game Developer magazine: http://www.gdmag.com/src/sep08.zip that does what you suggest.

Having no dynamic memory allocation is a good thing I think.


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 01, 2008 6:38 am 
Offline

Joined: Mon Dec 15, 2003 1:38 pm
Posts: 708
Shawn wrote:
...I'm trying to figure out exactly the most efficient way of doing that.

My implementation stores a m_firstFree (or m_lastFree) member which points to the first free element in the array. Each free element in the array points to the next free element. When an entity is added, that first free is used to grab a slot, when an entity is removed, the newly freed element becomes the first free and is linked in like a single linked list.

I also store a bit (sign bit) for the 'serial number' part which is actually a reuse counter and that determines if the entity is a local entity or a network replicated one. It actually decrements for locals or incrememnts for shared entities. The reuse counter is updated in a way that makes a reuse of the same handle nearly impossible while keeping the total handle size a 32bit int.


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 12 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 1 guest


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
cron
Powered by phpBB® Forum Software © phpBB Group