Save and Load for Torque · 1664 days ago

At Nick Zafiris’ prompting I’m going to talk about how I’m handling save and load for UnEarthed Gods and some of the troubles that assailed me along the way.

A while ago, I posted in the private GarageGames forums here talking about ways to handle saving and loading in TGE (Torque Game Engine). For those of you without access worry not, I’ll give the jist of everything there in this devdiary. With much help from Tom Bampton and others I was able to get a working system in place.

Game Design

be like water

Like all games in production, the design is still very liquid. The original plan was to do a traditional “let the player save anytime they want to” system. In the past month this has evolved into a “let the player save at specific savepoints” system. You can return to a given point and save whenever you want to. If you die you will respawn at the last savepoint you used.

This system just feels better in the game, and I really like the idea that in order to accomplish the task of saving you have to interact with and travel in the game world instead of an abstract menu item. I can also use these savepoints as chokepoints in the world to make sure that new game players who don’t understand the idea of saving and loading will at least get a savedgame at regular intervals.

Technical

the answer is right in front of you

ASIDE: From here on down, the discussion will be fairly Torque Game Engine centric.

As a new TGE user, it was unclear how to handle saving and loading, but like most things in this world, the answer was right in front of my face.

At the time I started using it, TGE came with a few default games examples: a multiplayer FPS game, and a multiplayer Racing game. There wasn’t any sort of single player game, and no example of a game that used saving and loading. I had no idea where to start. TGE is a rather intimidating mass of source, and I knew that somewhere in there it probably had exactly what I needed.

It turns out that it does provide a lot of features that make saving and loading simple. Exhibit A: the mission editor. TGE has an in-game mission editor that lets you place things in the world, edit their properties and save out a .mis file (mission file). This mission file is just a set of Torque Script commands and looks something like this:


new SimGroup(MissionGroup) {

new ScriptObject(MissionInfo) { name = "Act1..."; desc0 = "Two ninjas walk into a bar"; }; new MissionArea(MissionArea) { Area = "-1056 -1016 2112 2176"; flightCeiling = "300"; flightCeilingRange = "20"; locked = "true"; }; //other stuff snipped out... };

When these lines are executed the engine creates a SimGroup with the name MissionGroup that contains whatever else is inside it. For the purpose of this discussion, just think of a SimGroup as a collection of other objects, possibly other SimGroups.

Also, take note of the first ScriptObject called MissionInfo. These ScriptObjects are very useful for save and load.

When a mission file is loaded, it is executed like any other script file. When you save a mission in the mission editor, it does a very simple thing that amounts to this:


MissionGroup.save($Server::MissionFile);

When the MissionGroup is saved, all the objects in it are recursively saved out to the .mis file.

Hmm.. handy! So to recap, if you put something into a SimGroup, then call the .save function of that SimGroup object, all the objects inside it will be saved out to a file. Then when you execute that file, it will create the simgroup and all the objects inside it just like loading a mission file.

With this knowledge you can envision a system where you make a simgroup called SaveGameGroup then add things to this group during play, and when it comes time to save the game call:

SaveGameGroup.save("mysaveFile.sav")

Then when it’s time to load the game call:

exec("mysaveFile.sav")

Global Game State

dynamic script variables are your friend

The next problem is how to save all the glorious global game state. Perhaps you are using some Torque Script global variables to keep track of things:


$StatePlayerFoundSword = 1;
$StatePlayerEnteredSecretCave = 1;
$StateKilledTimes = 32;

This is where the ScriptObject mentioned earlier comes into play. Instead of the global variables you can use Torque Script’s dynamic variable property to add game states to a scriptObject willy nilly. Example:


new ScriptObject(GameStates){

FoundSword = 0; }; GameStates.EnteredSecretCave = 1;

GameStates now has two variables listed in it: FoundSword, and EnteredSecretCave. Now, if you add the GameStates object to your SaveGameGroup, when the game is saved out, the current global game states will be saved as well, and your savegame might look something like this:


new SimGroup(SaveGameGroup) {

new ScriptObject(GameStates) { FoundSword = 0; EnteredSecretCave = 1; }; };

Names and Variables Problem

when is 1845 not 1776?

In a few places I was using object references as script member variables. For instance,

  • object number 5 is following the object number 3.
  • object number 6 has home marker object number 2.

This poses a traditional problem for loading in that the id numbers are not necessarily the same each time you load the game. This is the same problem as handling pointers for save and load.

My solution is twofold:

  1. Use names instead of reference id’s
  2. Don’t depend on them

So instead of the object’s ID number, I make sure that any object I need to reference uniquely has a unique name such as ValleyStone1 or Forrest2Marker5. These names are explicit and will always be the same.

The other part of the solution is to simply not use references when you don’t have to. One case of this is with AI code. If the AI is currently fighting dynamic object 5, don’t save that out. Make the AI smart enough that if it is loaded, and sees object 5 in front of it, it will decide that it’s a good object to fight and continue the battle.

Using these two ideas, I’ve been able to make this a non-problem, and get on with making the game.

A Confession

the rumours of my save and load system have been greatly exaggerated

I still have a lot of work to do on saving and loading.

Currently I have a system that works, I have a .sav savegame file that contains all the global game states, and some other game specific info:

  • character name
  • current mission file
  • quickslot binds
  • current respawn/save point
  • played time
  • etc

This info is inside of a SimGroup that is saved as discussed above and looks like so when saved out:


//--- OBJECT WRITE BEGIN ---
new SimGroup(GameStateGroup) {

new ScriptObject(GameInfo) { LastGameTime = "340.899"; RespawnPoint = "playerspawn2"; CharacterLevel = "1"; LastSavedGame = "UEG_thetower/data/saveGames/savegame_19.sav"; missionFile = "UEG_thetower/data/missions/Act1_n.mis"; Character = "Almodovar"; }; new ScriptObject(GameStates) { TutFirstEquip = "1"; AccessToForestVale = "1"; ValleySGaveSword = "1"; TutFirstJump = "1"; TutFirstTalk = "1"; TutOpenedFirstGate = "1"; TutFirstAttack = "1"; }; }; //--- OBJECT WRITE END ---

Then I have a custom formatted file that contains all the info needed for recreating the player’s state. This file is a set of script commands that are simply eval’d in turn. Before the lines are eval’d, the player object is already created, and a global variable is set to reference the player, $SG_PLAYER. This script can use that global variable to manipulate the player object into the proper state like so:


$pref::Player::Name= "Almodovar";
$SG_PLAYER.setTransform(

"791.268 -533.326 164.348 0 0 -1 0.218837" ); $SG_PLAYER.setMaxHitPoints(20); $SG_PLAYER.setHitPoints(2.39293); $SG_PLAYER.setEnergyLevel(60); $SG_PLAYER.maxManaPoints= 20; $SG_PLAYER.manaPoints= 19; $SG_PLAYER.skillNumKnown = 0; $SG_PLAYER.incInventory(sword,1); $SG_PLAYER.use( sword); $SG_PLAYER.incInventory(BoneSolid,2);

I had done this system for the player before I groked the SimGroup.save system, and will be moving all of this info into a SaveGameGroup.

A Confession Continued

I do not currently handle persistent state of the world or AI save and load. Each time the game is loaded the world state is the same, the AI are in the same spot as if the game first started etc. Lots of work to do on this still.

For the AI, my plan is to have a special AIRespawn marker that will be created when you save the game. This special marker will contain all the info needed to recreate the AI, with its previous generated stats health etc.

When a new game is started, the AISpawnMarkers in the MissionGroup will be used to spawn the AI. But when a game is loaded, the SaveGameGroup will be searched for all AIRespawnMarkers and create the AI in the proper place. This will fit in nicely with the AI code I already have working, and should be a piece of cake to do (he said naively).

For persistent state objects, this will take some work. For instance, when you open a gate, and the game is reloaded, the gate should be opened still. When you pick up a sword and the game is reloaded the sword should no longer be in the world, but in your inventory. One idea was to place these objects into a special PersistGroup that would be saved out and loaded, instead of the normal MissionGroup that doesn’t get saved out when you save a game. When you pick up the sword, you simply remove the sword item from the PersistGroup and it will not be created when the game is loaded.

When you save the gate out it will have a dynamic “opened” script variable that is set to true, when the gate is created it will check for that variable and if so, make sure that it is animated into the opened position.

I expect I’ll run into some more interesting problems when I finish the save and load system that I’ll have a chance to talk about in the future.

I hope this has helped one or two of you, and if you have any thoughts on ways to improve it or personal experiences I’d love to hear about it.

-Clint




  1. Thanks Clint, this is great insight for those of us who are still somewhat new to Torque, and to game programming in general. It is educational and encouraging to have this as a starting point to build understanding. Good job!
    wheelbarrow    Aug 20, 12:31 AM    #

  2. Thanks for the kind words my friend.
    Clint    Aug 20, 01:28 AM    #

  3. Good job…
    BurNinG    Aug 22, 07:17 AM    #