Game Development Community

Save game / saving and loading single player game thoughts.

by Clint S. Brewer · in Torque Game Engine · 02/07/2005 (9:49 pm) · 39 replies

I'm making a single player game with some rpg elements.
I'm about to add the save game part right now and would appreciate some advice.

I've seen all the resources (I think), they look ok and were helpful, but definitely don't handle saving and loading the entire game. I'm hoping I can do this will a little less work.

I was thinking about:

save:
just saving out the mission / missioncleanup / relevant groups to a file.

load:
just pretend you are loading a new mission and load that file you saved out.


problems:
1. make sure that everything is in those groups that I plan to save out.

2. I store handle references some places, so I'll have dynamic variables set up like myStatic = "1897" this won't work since I assume it'll be very possible that handle number will change?

solutions:
For 1, it's not really a problem, have to do this for things anyway

for 2. I was thinking perhaps if I used names instead of handles and made sure things had unique names then it would be ok...

seems to me like someone else must have run through this and come up with a nice solution before. Of course I can just roll my own and loop through the objects saving things as I see fit, but maybe I can get this mission saving type of save game to work.

any advice?

thanks,
clint
Page«First 1 2 Next»
#21
02/11/2005 (3:17 pm)
@Ben: right, but one of the sneaky parts (at least for me) was the fact that you get no warning if you add an object to a new simGroup, but it was already in one. That took me a good 2 days to troubleshoot, so I thought I would share :)
#22
02/11/2005 (3:35 pm)
Stephen,

Not sure if I mentioned it above, but in my original code for this I deleted the PersistGroup at the same time as the MissionCleanup group. In my code at least they are functionally identical with the exception that the persist one is saved out to disk. An even quicker way to do this would be simply: MissionCleanup.add(PersistGroup); now that I think about it ;-)

Id forgotten to mention the "only in one group" thing. If it became a concern it would be trivial to add a warning message if the group is forcably changed, but its definately something to be aware of.

Clint,

As Mission* is a SimGroup then simply adding to PersistGroup is enough to move it. I dont remember if I modified the mission editor or not, I probably didnt. I was just messing around so I didnt really attempt to solve the problem of adding objects and instead just did it all through test scripts. Adding a "persistant" field to objects in the MissionEditor may work, but there is probably a cleaner way. In the bit you quoted I was referring to the MissionInfo object that defines the name and description of the mission; when I save out or load the group I just use MissionInfo.persistFile as the filename rather then hardcoding it. It just lets you have per-mission perstance (or indeed use the same data for multiple missions) without having to hardcode or auto generate filenames. It was handy for testing, but I'd probably do it by generating a filename based on the mission filename if I were doing it for a real game.

I guess this is a rather long winded way of saying I'm not really sure on the best way to integrate it with the mission editor. Perhaps have the PersistGroup accessible in the mission editor as a group in the mission so that you could just move or add things into it the same way you would any other group. Now that I think about it, I tried this quickly before. I think the problem was that the PersistGroup had to be in the MissionGroup to show up and be usable from the mission editor, which played merry hell when saving the mission. I didnt put too much thought into it though so there might be a better way. I'd be interested to see if anyone can come up with a workable solution. Maybe a "Make Persistant" menu option that moves the currently selected objects out of the mission and into PersistGroup (would just be a PersistGroup.add();) would be enough.

T.
#23
02/11/2005 (4:21 pm)
I only make a foogroup in the editor.
Saving it with Tom's way.
When i restart the mission i only exec my saved foo.cs file.
And the simgroup shows in the editor and loads on all clients.
All objects i have in this file are placed on the map.
I dont need the to add a new simgroup(foogroup).
The foogroup deletes and loads between missions.
Anything more i need to think of ?

Edit.
Stupid me the foogroup is added from the foo.cs file :)
#24
02/11/2005 (5:28 pm)
@Tom
Quote:I think the problem was that the PersistGroup had to be in the MissionGroup to show up and be usable from the mission editor, which played merry hell when saving the mission..

right because it would then save out everything else you happened to put into the persist group when you saved the mission.
hmm.
#25
02/12/2005 (3:14 am)
Clint,

Quote:
right because it would then save out everything else you happened to put into the persist group when you saved the mission.

Exactly. You could kludge it pretty easily by removing the PersistGroup before saving and re-adding it afterwards, but this feels nasty when the rest of the solution is so clean and simple. I think the way it ends up being done is going to depend on what turns out to be the most usable in the editor. At the moment I think that using groups fits in best with the way the current editor stuff works and adding a menu option to move things around would be counter intuitive. Maybe the kludge is worth it for over all usability :)

Perhaps we should pool all this together into a resource once the issues are worked out. Ill see if I can come up with a draft and will post it here so you guys can edit/add your stuff.

Billy,

That sounds about right, I think. I'm not sure if you'll run into any problems, but seems like a good start.

T.
#26
02/14/2005 (12:21 pm)
Quote:
You could kludge it pretty easily by removing the PersistGroup before saving and re-adding it afterwards

we could make a distinction between loading a mission for editing and loading a game for gameplay.
when loading the mission to work on it, the internal persistgroup (part of missiongroup) is loaded normally

when the mission is loaded normally the internal persist group is loaded
when the game is saved during play the persist group is saved out to a separate file.
when the game is loaded for play, the internal persist group is destroyed and the saved persist group is loaded and added to the mission group.


Another thing to think about, at least for my game is that I will have multiple maps that will need to be saved. For instance you might be traveling in Area1, do some things that change the state, then move to Area2 where you save the game. Area1's state will need to be saved when leaving Area1 to Area2 and that state will have to be associated with the savegame.
I think this'll be a common requirement for anyone making a multiple map single player game.
#27
02/14/2005 (12:33 pm)
Ye Clint im doing most things what you explained above.
It works well so far i can see even in multiplayer.
#28
02/21/2005 (6:58 am)
Finally found time to get back to this.

Clint, same concerns here for the mission loading.
What I'm thinking is that I shouldn't have to modify anything to distinguish if I'm in level editing mode or playing the game. Right now I'm going through the mission files in common/server. The mission loading proccess was developed with mission cycling in mind for multiplayer type games. So I think for developing a single player game, we need to start from those files and modify them in order to suit a SP game's needs.

So far for MissionGroup which stores all objects created in the World Editor I have found that:
MissionGroup is created in each .mis file (duh)
MissionGroup is saved in common/editor/EditorGui.cs file when clicking on File -> Save Mission
MissionGroup is deleted in common/server/missionLoad.cs file when ending a mission or in common/server/server.cs before creating a new server

MissionCleanup which stores objects that are added to it by script:
MissionCleanup is created in common/server/missionLoad.cs file when loading a mission or when reseting a mission
MissionCleanup is saved no where
MissionCleanup is deleted in common/server/missionLoad.cs file when ending a mission or when reseting a mission. Also deleted in common/server/server.cs before creating a new server
#29
02/21/2005 (6:59 am)
For travelling from Area to Area like you mentioned, the flow needs to be worked out and I'm thinking this as I'm writing it so I may have errors. Mission loading is spread across many files and it's difficult to figure out the flow.

Let's see:
New Game
Select New Game when you want to play the game for the first time. The first level (e.g. Area1.mis) gets loaded.
Also New Game should be selected when you want to edit/modify the mission, but in this case you need to explicitly specify which mission you want to modify in the startMissionGui.gui file in SM_StartMission() function. After you're done editing, save like normal in the Mission Editor (File -> Save Mission). While you're modifying, make sure you don't "play" the game cause that will screw up the game state when you save the mission in the editor. In other words, if you move the character and pick up an item, don't save the mission using the Editor unless that item is put back in the same place.


Save Game
When you're playing the game and want to save it, ofcourse you wouldn't do it by saving the mission in the Mission Editor. An in-game save GUI must be created that will perform the following (simplisticly speaking):

1. Ask for the save game name (e.g. SaveGame1)
2. Create the SaveGame1 subfolder
3. Save the $currentMission name in SaveGame1/currentMission.cs (e.g. $currentMission = "Area1";)
4. Save MissionGroup in SaveGame1/Area1.cs
5. Save MissionCleanup in SaveGame1/gameState.cs


Load Game

Load Game should only be selected when you want to play the game. There's no reason to load a game if you want to edit it.
So when Loading:

1. Select the saved game name you want to load (e.g. SaveGame1)
2. Exec SaveGame1/currentMission.cs to find out in which mission to load. In this case, $currentMission = "Area1";
3. Exec SaveGame1/Area1.cs
4. Exec SaveGame1/gameState.cs
5. Set the $LoadingGame variable to true
6. In server/scripts/game.cs createPlayer(), have something like this:
If ($LoadingGame)
%player.setTransform(Data from saved file)
else
[commands that are applicable only when starting a New Game]


Travelling from Area to Area
1. When player reaches the trigger that sends him to Area2, automatically perform steps 4 and 5 as when saving a game.
2. The trigger should contain the name of the mission he's supposed to enter (Area2) so save it in $currentMission.
3. End the mission and delete MissionGroup and MissionCleanup
4. Exec SaveGame1/Area2.cs
5. Exec SaveGame1/gameState.cs

Also, a lot of cleaning needs to be done in the mission files in common/server for single player mission loading. Before I start on this, I'd like to get some feedback. What do you guys think?

Nick
#30
02/21/2005 (7:54 am)
Sounds good. Im not sure on the mission editing, though. There is a lot of potential for accidentally screwing up the mission without noticing. I cant really think of any more ways to handle it at the moment. I guess that its something that's just going to have to be experimented with.

T.
#31
01/24/2007 (1:39 am)
I'm sorry for bumping a two year old thread, but is this done completely in script? And if so, has anyone worked it out in full? Or is there a Save/Load system out there that does work?

Since this is the Engine forum, I just assumed there was C++ required, but I'm not a programmer so T'script is better for me:)

Any help is greatly appreciated.

Thanks!

Tony
#32
01/24/2007 (10:51 am)
Hey Infinitum3D, for me it has been done entirely in script. It's pretty worked out for my game so far at least. and all the info is above here. The key thing for me was how easy it was to save out simgroups with scriptobjects in them, and then just make use of that.

and also using markers for my ai and other dynamic content in the mission editor instead of placing the actual object.

Adding conditional loads to the marker.

And when the game starts I go through look at all the markers, see if it should be created or not and then create it...or something like that been a while since I looked closely at it.
#33
01/24/2007 (10:52 am)
I think that Nick and I both had some blog entries about it as well if you look through our history..or search our blogs.
#34
01/24/2007 (10:57 am)
Might as well share some :)

for example, heres my basic save funcs
////////////////////

function GetValidSaveGameFileName()
{
	//returns a string of a new valid filename to save to

   //will be of the format savegame_000.gst

   %saveFileBase = "savegame_";
   %saveFileExt = ".gst";
   %maxSaveFiles = 99;

   %filespec = $SaveGameDirectory @ "/*" @ %saveFileExt;
   %curVal = 0;
   %succ = 0;

   while( (%curVal < %maxSaveFiles ) && (%succ == 0))
   {

	   if(%curVal < 10)
	   		%attemptName = %saveFileBase @ "0" @ %curVal @ %saveFileExt;
	   else
	   		%attemptName = %saveFileBase @ %curVal @ %saveFileExt;

	   %attemptPath = $SaveGameDirectory @ "/" @ %attemptName;

	   //if open for read fails then the file doesn't exist
	   // and it is a good one we can use.

		%file = new FileObject();

		%file.openForRead(%attemptPath);
		if(%file.isEOF())
		{
			//good
			%succ = 1;
		}
		%file.close();
		%file.delete();

        %curVal ++;
   }

   echo("choosing save game file: " @ %attemptPath);

   if(%curVal >= %maxSaveFiles)
   {
	   error("max savegame files exceeded");
	   return "toomanysaves.gst";
   }

   return %attemptPath;
}

function SaveANewGame()
{
	//this will choose a new savegame file and save to that

	%filename = GetValidSaveGameFileName();
	SaveTheCurrentGame(%filename);

}

//Save the current games state
//  if savegameFileName is specified use that
//  otherwise use the last savedgamefilename
function SaveTheCurrentGame(%filename)
{
	echo("saving game-------------------");

    %player = ClientGroup.getObject(0).player;
    if(!isObject(%player))
    {
		error("SG: client doesn't have a player object, can't save now");
		return 0;
	}
	if(ClientGroup.getCount() > 1)
	{
		error("SG: more than one client cannot save single player game");
		return 0;
	}

	if(%filename $="")
	{
		%filename = GameInfo.LastSavedGame;
		if(%filename $="")
		{
			error("error: SaveTheCurrentGame GameInfo doesn't have valid LastSavedGame");
			return 0;
		}
	}
	else
	{
		echo("savegame to: "@ %filename);
	}

	$pref::Player::ContinueGame = %filename; //store for continue


    %screenshotName = getSubStr(%filename, 0 , strlen(%filename)-4  ) @ ".jpg";
    %gameStateFileName = getSubStr(%filename, 0 , strlen(%filename)-4  ) @ ".gst";
    %aiSaveFileName = getSubStr(%filename, 0 , strlen(%filename)-4  ) @ ".ais";
	%invSaveFileName = getSubStr(%filename, 0 , strlen(%filename)-4  ) @ ".inv";

	screenShot( %screenshotName , "JPEG");

	GameInfo.MissionFile = $Server::MissionFile;
	GameInfo.LastSavedGame = %filename;
	GameInfo.LastGameTime = getTotalPlayedTime();
	GameInfo.Character = $pref::Player::Name ;
	GameInfo.CharacterLevel = GameInfo.CharacterLevel; //set directly when leveling up
	GameInfo.QuickSlot1 = $InventoryBind[1];
	GameInfo.QuickSlot2 = $InventoryBind[2];
	GameInfo.QuickSlot3 = $InventoryBind[3];
	GameInfo.QuickSlot4 = $InventoryBind[4];
	GameInfo.QuickSlot5 = $InventoryBind[5];
	GameInfo.QuickSlot6 = $InventoryBind[6];
	GameInfo.QuickSlot7 = $InventoryBind[7];
	GameInfo.QuickSlot8 = $InventoryBind[8];
	GameInfo.QuickSlot9 = $InventoryBind[9];
	GameInfo.QuickSlot0 = $InventoryBind[0];


	FillPlayerExecLinesIntoScriptObject(GameInfo, %player );


	GameStateGroup.save(%gameStateFileName);
    AISystem::SaveGame(%aiSaveFileName);
	UEGInventorySystem::SaveGame(%invSaveFileName);



	echo("done saving game----------------");

	return true;

}
#35
01/24/2007 (10:59 am)
Here's how the gamestate group is created for a new game, you can see that it's just a simgroup using some scriptobjects to store info. As game states change I update the GameStates script object dynamically, then when the game is saved, all those states are saved out.

function ReCreateInitialGameStateGroup()
{
	if(isObject(GameStateGroup))
		    GameStateGroup.delete();//out with the old

	new SimGroup(GameStateGroup) {
		new ScriptObject(GameInfo){
				missionFile  	= "UEG_thetower/data/missions/Act1_n.mis";
				RespawnPoint    = "playerspawn1";
				LastGameTime 	= "0";
				Character		= "A Name";
				CharacterLevel	= "1";
				QuickSlot1		= "";
				QuickSlot2		= "";
				QuickSlot3		= "";
				QuickSlot4		= "";
				QuickSlot5		= "";
				QuickSlot6		= "";
				QuickSlot7		= "";
				QuickSlot8		= "";
				QuickSlot9		= "";
				QuickSlot0		= "";
		};
		new ScriptObject(GameStates){
		};
	};

}
#36
01/24/2007 (11:02 am)
For the game states, here are the funcs that I use to set and test
function getGameState(%state)
{
	//Note that this can return any type of state,
	// it could be a boolean, or a string, or whatever!

	%test = "$GSGet = GameStates."@%state@";";
	eval(%test);
	return $GSGet;
}

function setGameState(%state, %val)
{
	%text = "GameStates."@%state@" = " @%val@";";
	eval(%text);
}

here's the load saved game func
function LoadSavedGame(%saveGameFile)
{


    %player = ClientGroup.getObject(0).player;
    if(!isObject(%player))
    {
		error("SG: client doesn't have a player object, can't load now");
		return 0;
	}
	if(ClientGroup.getCount() > 1)
	{
		error("SG: more than one client cannot load single player game");
		return 0;
	}


	$pref::Player::ContinueGame = %saveGameFile; //store for continue


	%gameStateFileName = getSubStr(%saveGameFile, 0 , strlen(%saveGameFile)-4  ) @ ".gst";
	%aiSaveFileName = getSubStr(%saveGameFile, 0 , strlen(%saveGameFile)-4  ) @ ".ais";
	%invSaveFileName = getSubStr(%saveGameFile, 0 , strlen(%saveGameFile)-4  ) @ ".inv";

	if(isObject(GameStateGroup))
	    GameStateGroup.delete();//out with the old

	compile(%gameStateFileName);//make sure to re-compile it
	exec(%gameStateFileName);//in with the new

	$pref::Player::Name = GameInfo.Character;


	UEGInventorySystem::LoadSavedFile(%invSaveFileName);
    //gamestate must be loaded before the ai's are loaded

    AISystem::LoadSavedFile(%aiSaveFileName);

	$Server::MissionFile = GameInfo.MissionFile;
	$LastGameTime = GameInfo.LastGameTime;

	EvalPlayerExecLinesFromScriptObject(GameInfo, %player);

}
#37
01/24/2007 (11:05 am)
With the caveats of the above being, I'm making a single player game, and the game only exists in a single mission right now.

The next extension to this I've been avoiding but will probably have to make is supporting multiple missions, using a global save file, along with mission specific save files. To divide the world up for performance reasons.
#38
01/24/2007 (1:05 pm)
And for completeness, here's where it all starts off when you load a saved game
//-----------------------------------------------------------------------------
function GameConnection::spawnPlayerForSavedGame(%this, %savegamefile)
{
   echo("*** New Game Started");
   // Combination create player and drop him somewhere
   %spawnPoint = pickSpawnPoint(); //just to load a saved game
   %this.createPlayer(%spawnPoint);

   LoadSavedGame(%savegamefile);


   //let any object that needs to do special stuff once the game is loaded do it now
   EventPostSaveGameLoaded(MissionGroup, $TypeMasks::ShapeBaseObjectType);
   //pass through missioncleanup so any players/ai players get the event too
   EventPostSaveGameLoaded(MissionCleanup, $TypeMasks::ShapeBaseObjectType);



   //now get the new spawnpoint based on the loaded data
   //and place the player there.
   %spawnPoint = pickSpawnPoint();
   %this.player.setTransform(%spawnPoint);
   %this.camera.setTransform(%this.player.getEyeTransform());

   applyDexterityBasedModifiers(%this.player);


   //check the loaded game
   if(!isObject(%this.player.inventory))
   	   error("ERR: GameConnection::spawnPlayerForSavedGame  bad inventory object after load game");




}

and here's the eventpostsavegameloaded func that I've found useful for setting up some data based on gamestates and the like once the game is loaded

function EventPostSaveGameLoaded(%theSimGroup, %typeMask)
{

	//WARNING this is a recursive function, you call it the first time
	// on your root group....for instance MissionGroup, then it will
	// call itself again on any sub SimGroups it finds

	%count = %theSimGroup.getCount();
	for(%i = 0; %i < %count ; %i++)
	{
		%object = %theSimGroup.getObject(%i);

		if( %object.getType() & %typeMask)
		{
			%object.EventSavedGameLoaded();
		}
		else if(%object.getclassname() $= "SimGroup")
		{
			//recurse here
			EventPostSaveGameLoaded(%object,  %typeMask);
		}
	}

}
#39
01/24/2007 (1:14 pm)
And slightly interesting ai group saving to show how thats done
note that this is done with my custom ai system...the details are not important...but this shows just another instance of making a simgroup, filling it with stuff, then saving that simgroup to a file. And finally loading it just by compiling and executing the file.

easy as pie


function AISystem::SaveGame(%saveFile)
{
	echo("AISystem saving ai......");

	if(isObject(AISaveGroup))
		AISaveGroup.delete();

	new SimGroup(AISaveGroup);
	MissionCleanUp.add(AISaveGroup);

	for(%i = 0 ; %i < AIPlayerGroup.getCount(); %i++)
	{
		%tmpAI = AIPlayerGroup.getObject(%i);
		%tmpAI.getDataBlock().SaveEntity(%tmpAI, AISaveGroup);
	}

    echo("AISystem saved "@ AISaveGroup.getCount() @" ai markers to file "@ %saveFile);
	AISaveGroup.save(%saveFile);

}

function AISystem::LoadSavedFile(%aiSaveFileName)
{
	echo("AISystem loading saved ai markers......");
	if(isObject(AISaveGroup))
		AISaveGroup.delete(); //out with the old!

	compile(%aiSaveFileName);//make sure to re-compile it
	exec(%aiSaveFileName);//in with the new

	MissionCleanUp.add(AISaveGroup);

	echo("AISystem loaded AISaveGroup with " @ AISaveGroup.getCount()@" markers......");

}
Page«First 1 2 Next»