Articles Archive
Articles Search
Director Wiki
 

Saving game states with objects

September 7, 1999
by CC Chamberlin

You've just finished your cool new shockwave game when it hits you: the player needs to save her progress. Unfortunately, your game is pretty complex, and you need to save more than just a few high scores and names; you need to save objects, and you can't just write out objects using setPref() like you can with numbers and strings. Should you scrap the whole project? Is it too nightmarish a task to go on?

Actually, it's pretty simple, if you know how to prepare for it, and have a few powerful functions under your belt. (In fact, these routines can be used anytime you need to save and restore the state of your application.) Let's take a look at some game state saving strategies!

Before we begin

This article assumes:

If you don't know how to save and restore information, check out Mike Weiland's article Saving a User's State from a Shockwave Movie. Code for reading and writing files is omitted from the examples. For linear lists, property lists, and object scripts, see the Director Manual. Also, there is a an article by Zac on debugging object-oriented programming which may come in useful and covers some similar topics.

Finally, save your fingers! All the code is already available in an example movie which you can download in Mac or PC format.

The problem

We want to save the state of our game to a text file, and then read it back in to pick up the game later. Actually writing our information out to disk and reading it back in is almost trivial; the hard part is preparing the data that needs to go out to disk.

Saving a player's name and high score is easy. But what happens when we are using objects in our game? You can't just use setPref() to save an object, because we would somehow have to translate it to a string. Instead, we'll devise a way to represent an object as a list which we can output as a string using the string() function.

If we want to be able to restore an object from a game, we need to store two things for later use. First, we need to know which parent script to create the new object from, and second, we need to know what to set the properties to. If we can find a way to cajole this information out of any given object, we can write a generic object-saving handler.

Let's take these two items in order.

Finding the Script Name

There is no easy way to get the script name of an object; there is no built-in Lingo function that you can pass an object to and get the scriptname back. However, there is a sneaky way to get the scriptname. The key comes from this observation in the message window:

g = new(script "test")
put g
-- <offspring "test" 2 af7f8e8>
put string(g)
-- "<offspring "test" 3 af7f8e8>"

Notice that the string() function applied to an object returns a string which contains the name of the script used to create the object. Thus, we can extract the scriptname using a simple function:

  -- scriptName
  -- Returns the name of the script used to create an object.
  -- Returns an empty string if not passed an object.
  on scriptName obj
  
    oldDelimiter = the itemDelimiter
    the itemDelimiter = QUOTE
    scriptName = item 2 of string(obj)
    the itemDelimiter = oldDelimiter
    return scriptName
    
  end scriptName

(This routine could be shortened to two lines, but since the itemDelimiter is set across your entire environment, it is safer to set it back to whatever it was when we entered the handler. If you're careful to always set the itemDelimiter before referring to items, you can just set the itemDelimiter and return the scriptname immediately.)

This routine, if passed an object, returns a string containing the scriptname used to create the object. It's pretty bulletproof, returning an empty string if passed a non-object. (It will choke on objects which were created from scripts containing quotes in the script name, but if that is a concern for you, that is simple enough to fix, and is left as an exercise for you.)

Restoring Properties

The next task is to get a list of the properties and their values so we can restore their values when we restore the object. This is where a certain Lingo feature comes in really handy: properties of objects can be read as if they were property lists.

For example, say our "test" script has one property named myprop, and it defaults to "ABC". Then you could type the following in the message window:

g = new(script "test")
put g.count
-- 1
put getpropat(g,1)
-- #myprop
put getAProp(g,#myprop)
-- "ABC"

This gives us all we need to write a generic handler for translating a simple object into a list that can be stored away.

Objects with ancestors are somewhat problematic, however. It is possible to save ancestral properties, too, but that is beyond the scope of this article (but see the Further Explorations section at the end of this article for some notes!).

Objects as Lists

Here's how we can convert an object to a list. First, we find out how many properties the object has using the count function. Then, we step through them, one by one, building a new property list with the same properties of the object. If we store the scriptName (obtained from the handler above) as one of the properties, then we can use that later to determine the parent script when we restore our game. Here's how it is done:

  -- objectToList()
  -- Takes an object and returns a property list
  -- The property list includes the scriptName.
  
  on objectToList what
  
    z = [:]
    propTotal = what.count
    
    repeat with looper = 1 to propTotal
      propName = getPropAt(what,looper)
      addProp(z,propName,getAProp(what,propName))
    end repeat
    
    addProp(z,#scriptName,scriptName(what))
    return z
    
  end objectToList

This routine generates a property list that has as its properties the properties of the object that is passed to it, and one special property called #scriptName that has our object's script name. We now have all the information we need to restore the object. First, we look up the #scriptName property for the name of the script to create an instance of. Then, we simply step through and set the object's properties. Here's how:

  -- listToObject()
  -- Takes a property list with a #scriptName property
  -- and returns an object
  
  on listToObject what
  
    sName = getAProp(what,#scriptName)
    theObj = new(script sName)
    listCount = what.count
    
    repeat with looper = 1 to listCount
      propName = getPropAt(what,looper)
      if (propName <> #scriptName) then theObj[propName] = ¬
        getAt(what,looper)
    end repeat
    
    return theObj
  end listToObject

Game saving strategies

So now you have a routine for storing a single object! However, many games will have more information than just the properties of a single ancestor-less object! Let's construct a way of saving our entire game state easily.

Suppose your game consists of a player going around collecting weapons and treasures. You might have a player object, objects representing the treasures the player has collected, and a few "loose" global variables that need storing, such as whether they have registered your game.

Rather than writing a routine to save off each of these variables and objects in turn, we can store them all in a single property list called "gameState," and then just save and restore that single property list. It might look like this:

put gameState
-- [#player:<offspring "playerobject" 2 2aae55c>,¬
  #key:#red,#registered:false]

To save the game, all we need to do is convert all the objects in the gameState to property lists, and we can save it using standard text read and write. We do this by stepping through all the elements of the property list, and, if something is an object, we use our objectToList() routine, developed above, to convert it to a sub-list. We can then save our list (as a string) to a text file.

  --  saveGame()
  --  Saves the contents of the gameState variable
  
  on saveGame
  
    global gameState
    
    saveProps = [:]
    savePropCount = gameState.count
    
    repeat with looper = 1 to savePropCount
      thisPropertyName = getPropAt(gameState,looper)
      thisPropertyValue = getAProp(gameState,thisPropertyName)
      if (objectp(thisPropertyValue)=true) then
        objstring = objectToList(thisPropertyValue)
        setAProp(saveProps,thisPropertyName,objstring)
      else
        setAProp(saveProps,thisPropertyName,thisPropertyValue)
      end if
    end repeat
    
    return saveProps
    
  end savegame

To restore the game, we read in the text, get its value() to convert it back into a property list, and then step through all the properties, restoring any objects using the listToObject() routine.

You may be wondering: how do we differentiate between things that should be property lists, and things that used to be objects before they were converted? This is where our #scriptName property comes in handy: any property list with #scriptName as a property is an object! If a property list has a #scriptName property, then we convert it to an object. Here's how:

  --  restoreGame()
  --  Restores the contents of the 
  -- gameState variable from the text
  
  on restoreGame saveText
  
    global gameState
    
    gameState = value(saveText)
    gamePropCount = gameState.count
    
    repeat with looper = 1 to gamePropCount
      thisProperty = gameState[looper]
      -- We leave the property alone unless it is an object
      if (ilk(thisProperty) = #PropList) then
        if (voidp(thisProperty[#scriptname])=false) then
          gameState[looper] = listToObject(thisProperty)
        end if
      end if
    end repeat
    
  end restoreGame

And we're done! All we need to do now is write out and read in simple text files using saveGame() and restoreGame(). For example, if you were using the preferences functions:

You could also use FileIO, or even post these strings to a server. This would also be the place to encrypt the string to prevent people from hacking your saved games, if you care.

Wrapping up

Want to try it out? Below is an example shockwave "game" that uses these exact routines. Click the "What's Going On?" button for an explanation.

The source code for this shockwave game is available for download in Mac or PC format.

That's about it for the scope of this article (but see below for further explorations). The important part is the concept that objects can be generically converted to lists and back. With that simple-seeming feature, you can write generic routines for saving and restoring any complicated game state you create.

Now you can allow your players to leave your game and come back at any time, picking up right where they left off. And these concepts really apply to any sort of application you are writing in Lingo; anytime you need to save and restore data, you can use these routines.

Enjoy!

Further Explorations

The routines provided above do have some limitations, and overcoming them are left as exercises for you, although I will probably tackle them in a later article. In the mean time, try improving the situation:

Problem: What if some of our gameState properties point at cast member references? Sprites? Do we need to change any code?

Solution: Experiment in the message window to see if member and sprite references need any special handling.

Problem: What if we had a gameState property called #Inventory which contains a list of objects? The saveGame() and restoreGame() functions only catch objects at the "top" level of the gameState list, so the inventory would get corrupted when saved and restored.

Solution: Rewrite saveGame() and restoreGame() to recursively go down through any lists to convert any objects further down.

Problem: What if an object has an ancestor, or a property that points to another object?

Solution: An ancestor will (probably *) show up as a property, so rewrite objectToList() and listToObject() to recursively go down through the properties and convert any objects it finds, and traversing any lists it finds.

* Hint: There appears to be some strange behavior regarding whether or not the ancestor shows up as a property, and how the ancestor's properties are accessed when the object is treated as a property list. Try different syntax in the message window to investigate.

Problem: This one is great! Note that 'the globals' is just like any other object - you can read and write to its properties (i.e. global variables) as if the globals were a property list! Therefore, you could restore everything by restoring the globals variable to its original state! This requires only a slight modification.

Solution: Rewrite saveGame() and restoreGame() to restore the globals instead of the gameState.

Problem: What happens when you have two objects having properties that point at each other? If you use the saveGame() and restoreGame() routines we've used up to this point, you'll end up with four objects instead of two. (Why?) This is probably the most complex game saving situation.

Solution: Do a pre-processing step. Before saving the objects, recurse through everything and assign each object a unique identifier. Then, save the object data with the identifiers, using the identifiers for placement in the list hierarchy. Then, before restoring, restore all of the objects, and then step through the hierarchy, inserting the objects based on the identifiers.

"C.C." Chamberlin returned to creating educational outreach multimedia for New Mexico State University after spending three years in Virginia creating Math and Science shockwave modules for ExploreLearning.com (while his wife worked on her doctorate at UVA). He has done work for the USDA, the FBI, USWEST, NSF, the Kellogg Foundation, and the Smithsonian, among others. During his non-programming hours, he enjoys writing, 3D graphics, eating hot chile, working on haunted house props for next Halloween, and spending time with his new baby boy.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.