Saving and Restoring: Difference between revisions
No edit summary |
|||
Line 266: | Line 266: | ||
As mentioned above, the '''idEntityPtr''' structure doesn't store an actual idEntity* raw pointer, but uses an integer to do so (which is looked up in the entity index in '''gameLocal.entities'''). This integer is resolved as soon as the client calls '''getEntity()'''. It is important that the gameLocal.entities array is initialised by the time the getEntity() method is called, which '''can't be guaranteed when the restore routine is still running'''. The order the entities are restored from the savegame is arbitrary and you therefore can't rely on entity X to be restored and available before entity Y. If you really need to access other entities to fully restore your class, you must work around that by using the Event system. Just post an event with 0 milliseconds delay time which restores the information. By the time the event is fired, all the entities are restored and it's safe to dereference the idEntityPtr. Examples can be found by searching for '''PostEventMS''' in the project: | As mentioned above, the '''idEntityPtr''' structure doesn't store an actual idEntity* raw pointer, but uses an integer to do so (which is looked up in the entity index in '''gameLocal.entities'''). This integer is resolved as soon as the client calls '''getEntity()'''. It is important that the gameLocal.entities array is initialised by the time the getEntity() method is called, which '''can't be guaranteed when the restore routine is still running'''. The order the entities are restored from the savegame is arbitrary and you therefore can't rely on entity X to be restored and available before entity Y. If you really need to access other entities to fully restore your class, you must work around that by using the Event system. Just post an event with 0 milliseconds delay time which restores the information. By the time the event is fired, all the entities are restored and it's safe to dereference the idEntityPtr. Examples can be found by searching for '''PostEventMS''' in the project: | ||
PostEventMS(&EV_SetOwnerFromSpawnArgs, 0); // Call the event with 0 msecs delay | PostEventMS(&EV_SetOwnerFromSpawnArgs, 0); // Call the event with 0 msecs delay | ||
{{tutorial-sdk}} | |||
{{coding-sdk}} |
Revision as of 07:36, 11 October 2007
Originally written by Ishtvan on http://forums.thedarkmod.com/topic/1586
WARNING: This is a first draft, complete with spelling and grammatical errors. Also, I just got this functionality working myself. So there may well be some unknown issues with this code that have yet to pop up.
Saving and Restoring Non-Spawned Classes
NOTE: This tutorial only applies to non-spawned objects. You only have to do all of this if you are NOT deriving your class from idEntity, i.e., you do not want your class to be spawned in the map. If you're deriving from idEntity, your object will automatically be saved and restored (although you still might have to save specialized data by writing Save and Restore methods)
Let's suppose you create a new class, and have a global object for it that you want to save and restore the state of, but you do NOT want to actually spawn this object as an entity in the map (this would require the mappers to place a lot of weird invisible entities on every map, just to store specialized data).
If you take a look at src/game/game_local.cpp, you will see 2 relevant methods: idGameLocal::SaveGame and idGameLocal::InitFromSave . Within these, a bunch of calls are made to the savegame and restore game to save and restore objects. So how do you put your object in there? I will try to explain that in the following tutorial.
Step 1. Add Your Class to the Id Class Heirarchy
If your new class is not already derived from an Id class, you must add it to the Id class heirarchy by inheriting from idClass and call the proper macros to let the Id class heirarchy know about your class.
NOTE: Be aware that adding your class to the Id class heirarchy may cause some methods to be called automatically in certain situations, if they have a certain name (such as Init, Shutdown). Therefore if you don't want your method called automatically, you should name it something else. See /src/game/gamesys/class.h for a complete list of methods on idClass that may be called automatically.
Step 1A
Put the macro CLASS_PROTOTYPE( <your class name> ) within your class declaration in the header file.
Example:
In myclass.h
class myClass : public idClass { CLASS_PROTOTYPE( myClass ); public: // [...]
Step 1B
Put the macro: CLASS_DECLARATION in your implementation file. In this case we will NOT add any scripting events to the class, because, from the start, we do not intend to ever spawn this class into an entity, so it does not make sense to define scripts on it, as scripts are primarily run from entities.
The syntax is CLASS_DECLARATION( <base class>, <your derived class> )
Example:
In myclass.cpp:
CLASS_DECLARATION( idClass, myClass ) END_CLASS
Step 2. Write Your Save Method
Write the method Save( idSaveFile *save ) const for your class. This will be called automatically by Id during the save process. Save and Restore methods typically utilize idSaveFile methods to read and write relevant data. Look at src/game/gamesys/savegame.h for an explanation of idSaveFile methods.
Example:
void myClass::Save( idSaveGame *saveGame ) const { // write an integer member variable to the save file saveGame->WriteInt( m_intVar ); return; }
Step 3. Write Your Restore Method
A restore method reads relevant data from the savefile.
Key Point: (Thanks to Sparhawk for figuring this out) Since your object is not going to be spawned, it will not be copied to the entities list after being restored. When restored, your object is not automatically copied ANYWHERE, and only exists in the scope of idGameLocal::InitFromSave. When InitFromSave is finished loading everything, and the game exits from this method (which it does before starting the map), your object will go out of scope and WILL BE DESTROYED, unless you copy it somewhere else yourself.
In order to avoid destruction when idGameLocal::InitFromSave exits, your restore method must copy the instance of your class somewhere, in addition to loading the data from the savefile. It could copy it to a global variable, static member variable in the class, whatever.
Example:
Assuming that elsewhere you have declared a global, and that you also have a working copy constructor for your class, etc.:
Somewhere else with global scope:
myClass g_globalMyClass;
In myClass.cpp
void myClass::Restore( idRestoreGame *SaveGame ) { // read in your data, in this case an integer member var SaveGame->ReadInt( m_intVar ); // copy this instance somewhere else // In this case we assume that a global of type myClass has been declared // elsewhere. It could also be a static member variable of type myClass. g_globalMyClass = *this; return; }
Step 4. Add your object to the SaveGame method in idGameLocal
idSaveGame::AddObject adds any object that's been derived from idClass to a savefile, and registers the object within the savefile so that it can be restored easily later.
We want to add our object in when everything else is saved, using idSaveGame::AddObject. As far as I can tell, you can put it in anywhere in this method, as long as it's before the call to savegame.WriteObjectList(), since your object must be added before the object list is written to the savefile.
To be safe, I put it in right before savegame.WriteObjectList().
Example:
Assuming you have declared myClass myObject elsewhere, and are using it to store data.
In idGameLocal::SaveGame
// [...] // Add your object savegame.AddObject( &myObject ); // write out complete object list savegame.WriteObjectList(); // [...]
The save method for the instance of your class in g_globalMyClass will now be called automatically now when the game is saved, and the savefile will know about your object when it comes time to restore.
NOTE: AddObject takes a reference to your object. If you want to use forward-declaration of your myClass object in game_local.cpp, just call AddObject with the reference to your object.
Step 5. You're Done!
The good news is, in game_local::InitFromSave, your object will now be restored automatically, when idRestoreGame::RestoreObjects is called, which goes thru all the objects that were added to the savefile and calls the Restore method on them.
Remember to have your restore method copy your object somewhere else, since your object is not a spawnable entity and it will otherwise go out of scope and be destroyed when idGameLocal::InitFromSave exits.
Addendum: Saving Entities
Automatic Stuff
Just to emphasize again, the above tutorial is for saving something that's not necessarily an entity. If you derive from idEntity, your object will automatically be saved in idGameLocal::SaveGame. In InitFromSave, it will be automatically restored, and (I think) copied to the entities[] array.
(although I thought gameLocal.entities[] was an array of entity pointers, not actual entities, so it may be copied somewhere else... in any cases, it's copied to the same array where all the spawned entities are stored)
All you have to do is put your entity on the map, and all of this happens automatically.
Must write Save/Restore methods in your class if you want to Save/Restore data that's not in the derived class
However, if you do not specify a Save method, it will call the one on the base class/superclass of your entity. So if you have a new data member in your class that was not in the base class, and you want it saved and restored, you MUST write your own myClass::Save and myClass::Restore methods, as described above (but don't worry about the copying over for the Restore method).
I think that idSaveGame::SaveObject (the method that's called to save your object) will keep calling Save all the way up the inheritence chain, so I don't believe you need to inherit the Save method of the base class in your new class' Save method. I'm not positive about this though.
If you want an example of this, the code would be almost the same as the Save and Restore methods in the first post, assuming your new data is some member variable m_intVar. In this case, since we derived from idEntity, you only need to call saveGame->ReadInt in myClass::Restore. You do NOT need to copy your object somewhere as was done above.
Note: Spawnargs are already saved by default when an entity is saved, so if the only new data stored on your new entity is stored in spawnargs, you don't need to write Save/Restore methods.
Addendum 2
Sweet, TinMan over at D3World found another way to restore the object by adding Savefile->ReadObject in the "Init from Savegame" section of game_local.cpp. ReadObject takes a pointer, so I thought it would just turn into a bad pointer when InitFromSave exited, but apparently it doesn't. So you can use ReadObject to copy your object, instead of copying it in yourObject::Restore.
Of course this means you have to maintain your objects saving/restoring in both idGameLocal::SaveGame and idGameLocal::InitFromSave .. with my method you only need to add the object in SaveGame and then it's automatically restored in InitFromSave. Anyway, here is his method copied from D3World:
Follow the tutorial for the most part. Set up your class as a global pointer to an instance of your class, create new instance in idGameLocal::Init. Don't copy out to global in your class ::Restore Clear your object in idGameLocal::MapShutdown
In idGameLocal::SaveGame near the end
// [...] // TinMan: Save object index to this spot in file savegame.WriteObject( myObject ); // write out pending events idEvent::Save( &savegame ); savegame.Close(); }
In idGameLocal::InitFromSaveGame near the end
// [...] // TinMan: Overwrite your global pointer with the restored instance. savegame.ReadObject( reinterpret_cast<idClass *&>( overGame ) ); // Read out pending events idEvent::Restore( &savegame ); savegame.RestoreObjects(); // [...]
Important Note: For any idSaveGame::Write function you must have a corresponding idSaveGame::Read function in the same order since the savefile is being operated on in a stream.
Further Notes:
- idSaveGame::Close
- Calls ::Save on all objects in it's list (you know the one you added to via AddObject).
- idRestoreGame::RestoreObjects
- Yep you guessed it, calls ::Restore on all objects.
Further Notes potentially making your Life easier
written by greebo
When traversing the SDK to make the Saving/Loading routines working again with our Mod code, I noticed a few things. I better write them here to prevent other people from running into the same situation.
Data Types that can be saved/restored
There is a limited number of data types that can be serialised and written into the savefile. It's always best to look into the idSaveGame declaration to see which types can be written to the savefile. Amongst them are the atomic types like int, unsigned int, string, bool as well as some often used structures like idVec3, trace_t, joint_t and so on. Note that raw pointers and C++ references are NOT amonst them (you can't expect the addresses to be the same when the game is restored from file again). If you're using custom structures, it's probably best to define Save/Restore methods on them which in turn call the idSaveGame::writeInt(),writeBool(),writeString(),.. methods.
Call Order
Another important thing to not is the call order in the Save/Restore methods. As an example, suppose you want to save an int, a string and a bool to your savegame:
void myClass::Save(idSaveGame* savefile) { savefile->WriteInt(myInt); savefile->WriteString(myStr); savefile->WriteBool(myBool); }
The important thing is to exactly match the call order of your Restore function:
void myClass::Restore(idRestoreGame* savefile) { savefile->ReadInt(myInt); savefile->ReadString(myStr); savefile->ReadBool(myBool); }
The savefile can be considered as a stream of data. All the classes are saved/restored in the exact same order, otherwise the data gets restored to the wrong class members.
Entity Pointers
There is a templated container call idEntityPtr: this object wraps around an idEntity* pointer and can be saved/restored just fine. It provides an assignment operator taking idEntity* pointers, which looks up the entity index in the gameLocal.entities[] array. Only this index is saved into the structure (and the savefile). In case you need to access the entity, you call the idEntityPtr::getEntity() method, see the example below.
class idAI { // This wraps around a idEntity* pointer idEntityPtr<idEntity> myEnemy; public: void setEnemy(idEntity* newEnemy) { // Use the assignment operator to store the <newEnemy> pointer myEnemy = newEnemy; // Only the entity index is stored into the idEntityPtr, it is resolved // using the gameLocal.entities[] array on demand. } idEntity* getEnemy() { // Now call getEntity() to convert the entity index into a valid pointer return myEnemy.getEntity(); } };
The container is a template so you can use this container to store pointers to subclasses of idEntity too:
idEntityPtr<idAI> myAI;
As mentioned above, the idEntityPtr provides a Save/Restore method, so it can be conveniently used. Behind the scenes, only the entity index gets written to the savefile, as you might expect.
Saving Raw Pointers
First rule is: whereever possible, avoid to use raw pointers (except for the example Ishtvan wrote about, using the WriteObject()/ReadObject() method). Pointers can't be saved/restored just like that. Supposed you have some kind of graph which is interconnected by raw pointers and you're in the unlucky situation to save this structure, you can follow some of the general guidelines of graph serialisation: http://www.parashift.com/c++-faq-lite/serialization.html
Saving References
As C++ references can't be changed after class construction, so this is an absolute no-no for use in the SDK. Unless you start to use some tricks with reference encapsulation classes, you're lost here. Change your reference to a compatible structure like idEntityPtr or change your class.
Saving idList
I thought it would be handy to have a prototype save/restore routine for idList in this article, so here it goes. Suppose we have a list of strings like this:
idList<idStr> myStringList;
The saving routine is pretty straightforward:
// Important: save the number of items into the list before the actual list members savefile->WriteInt(myStringList.Num()); // Now write all the list members to the file for (int i = 0; i < myStringList.Num(); i++) { savefile->WriteString(myStringList[i]); }
The restore method reads the integer and resizes the list accordingly. Then the members are restored one by one:
// Read the number, we need a local object to pass a reference int num; savefile->ReadInt(num); // Resize the list myStringList.SetNum(num); for (int i = 0; i < num; i++) { savefile->ReadString(myStringList[i]); }
The important thing is to resize the list before starting the for loop, the myStringList[i] array access must not crash.
Dereferencing idEntityPtrs during Restore
As mentioned above, the idEntityPtr structure doesn't store an actual idEntity* raw pointer, but uses an integer to do so (which is looked up in the entity index in gameLocal.entities). This integer is resolved as soon as the client calls getEntity(). It is important that the gameLocal.entities array is initialised by the time the getEntity() method is called, which can't be guaranteed when the restore routine is still running. The order the entities are restored from the savegame is arbitrary and you therefore can't rely on entity X to be restored and available before entity Y. If you really need to access other entities to fully restore your class, you must work around that by using the Event system. Just post an event with 0 milliseconds delay time which restores the information. By the time the event is fired, all the entities are restored and it's safe to dereference the idEntityPtr. Examples can be found by searching for PostEventMS in the project:
PostEventMS(&EV_SetOwnerFromSpawnArgs, 0); // Call the event with 0 msecs delay