Enabling free traveling between levels, in contrast to completing them one-by-one, poses the challenge of storing changes in the level you are leaving and restoring them when returning to it.
Although Crysis Wars doesn't have this feature built-in, the SDK allows the possibility to implement a persistence system doing exactly this.
1.) Technical Implementation
(Ab)Using the Savegame system of Crysis Wars
When thinking of storing data in a game the first thing that may come to mind is the Savegame system. This naive approach did the trick for us:
- When leaving a level, a savegame storing its local data is created
- After returning to it, the saved data is loaded again
Sounds too simple to be true? Correct.
Problems arise as soon as you try to save regularly: After loading, the local data of the levels would stay just the same. We solved this issue by storing the current local data and local data of saved games separately. For it a folder named the same as the regular savegame file is created and the local files are copied into it when saving, while they are moved out of it to the folder containing the current data when loading.
Serialize it!
When taking a closer look at the C++ code of the Crysis (Wars) SDK you may notice that many classes have functions named Serialize(...) or FullSerialize(...). It is these that take care of what a class should store when saving, and read from a savegame file when loading.
They all require a TSerialize object as a parameter that has to be created from an instance of a class implementing the ILoadGame or ISaveGame interface, depending on what you want to do.
In order to write such Serialize functions for custom classes it pays off to check out the ISerialize interface. I won't elaborate further on that since this article is NOT a tutorial.
You may find some ideas in the Code Snippets section though. :)
Triggering everything with the LevelSystem
Now you’ve got an impression of how data can be saved and loaded; but these processes need to be triggered at the right time:
- Local data should be saved just before leaving a level ...
- ... and loaded again just after returning to it, if possible before the player even can move
Our solution was to add an alternative travelling console command, that saves the local data and invokes the "map" command with the target level as parameter (this method works in non-devmode as well, since the console commands are called by code rather than by console).
For restoring the local data at the right time, we simply used the ILevelSystemListener interface and implemented the OnLoadingComplete function accordingly. The loading could also be done with a custom FlowNode - you'd have to include that node in each map you want to remember changes after travelling though.
2.) Code Snippets
Most of the following things weren't documented anywhere (or at least we didn't find any tutorials or similar) so you may find them useful.
Handling Savegame files
With the following code you can create your TSerialize object needed by Serialize functions when saving:
IPlayerProfileManager* pProfileManager =
m_pGame->GetIGameFramework()->GetIPlayerProfileManager();
IPlayerProfile* pProfile =
pProfileManager->GetCurrentProfile(m_pProfileManager->GetCurrentUser());
// Create the savegame file "myfile.temp"
ISaveGame* pSG = pProfile->CreateSaveGame();
pSG->Init(PathUtil::Make("", "myfile", ".temp"));
// You need to create a section to store data in
TSerialize pSer = pSG->AddSection("my_data");
// Use pSer to store data or create more sections...
pSG->Complete(true);
... and for loading it can be done as shown below.
IPlayerProfileManager* pProfileManager =
m_pGame->GetIGameFramework()->GetIPlayerProfileManager();
IPlayerProfile* pProfile =
pProfileManager->GetCurrentProfile(m_pProfileManager->GetCurrentUser());
// Create the loadgame by opening "my_file.temp"
ILoadGame* pLG = pProfile->CreateLoadGame();
pLG->Init(PathUtil::Make("", "my_file", ".temp"))
// The following lines are IMPORTANT!
// Doing this in the wrong way leads to memory errors
std::auto_ptr_ref<TSerialize> pSer(pLG->GetSection("my_data"));
// Now you can load data from the savefile
DUMMY_OBJECT->Serialize(*pSer._Ref);
//...
pLG->Complete();
Storing and loading stuff
Now we have some kind of TSerialize object but how can we handle it? Let's save the player's inventory for example:
// pPlayer should be an IEntity* pointing to the player ;)
if (pPlayer != NULL)
{
// Create a new section in the savefile
TSerialize ser = pSG->AddSection("player_inventory");
pPlayer->GetInventory()->SerializeInventoryForLevelChange(ser);
}
// Don't forget to call pSG->Complete(true) afterwards!
And load it again:
if (pLG->HaveSection("player_inventory"))
{
// get the TSerialize object to load from
std::auto_ptr_ref<TSerialize> pSer(pLG->GetSection("player_inventory"));
// pPlayer should point to the Player entity again ;)
pPlayer->GetInventory()->SerializeInventoryForLevelChange(*pSer._Ref);
}
// Don't forget pLG->Complete() when you're done with loading
Registering a travel command
Creating a custom console command is quite easy. First of all you need to declare the C++ function that will be called by the console command in Game.h:
static void CmdNEWTravel(IConsoleCmdArgs *pArgs);
Then you will need to implement the function and register the new console command in GameCVars.cpp:
m_pConsole->AddCommand("new_travel", CmdNEWTravel, 0, "Travel to another level (same as the map command just with persistence support)");
// Don't forget to unregister the command!
// In CGame::UnregisterConsoleCommands(), add:
m_pConsole->RemoveCommand("new_travel");
// And finally add the command implementation somewhere in that source file
void CGame::CmdNEWTravel(IConsoleCmdArgs *pArgs)
{
string target = pArgs->GetArg(1);
// Place your saving routines HERE
// Trigger the actual travelling using the "map" console command
gEnv->pConsole->ExecuteString(string("map ") + target + " nonblocking");
}
Utilizing the LevelSystemListener interface
If you want to listen to events of the level system, your class (could be a FlowNode for example) needs to implement the ILevelSystemListener interface:
class CPersistenceSystem : public ILevelSystemListener
{
//...
};
// In the actual implementation the loading code goes into the OnLoadingComplete function
void CPersistenceSystem::OnLoadingComplete (ILevel *pLevel)
{
// Load your data here
}
// Also don't forget to implement the remaining functions of the interface as dummies at least
All greek to me but yay!
Damn, that's exactly the system we need for our project. We were not 100% sure that was possible. Your mod is really exceptional.
well your mod looks/sounds pretty promising too i have to say ;)
Great tech news, it might help a few other Crysis mod devs
just so impressive...
This is really cool
That is a brilliant system. Could be very useful. :D
Awesome! Nice engine that you chose for creating and developing such a nice game as this will be, and now, saving system? Just awesome, comepletely good work!
Its nice, but how will I save the dynamical entities position\rotation ? I assume I will need to store each entity, but first find them using iterator ?
Great work dude! Bookmarked for later use! :D