Data Cast Members
October 12, 1999
by Irv Kalb
Did you ever have a program that needed a lot of data, but you weren't quite sure where to put it? You may have numbers or strings that you need to use for constants or initial values, but wherever you put them, it just doesn't seem right?
There are two obvious solutions; 1) put the data in code and use many "set" statements, or 2) put the data in "field" members, and pull it out and parse it at run time. This article discusses an alternative technique that I have been using for a while. The basic idea is to put data into property list, in a handler, in a named script cast member. Then you can get data returned to you as a property list, by calling that handler of the specific cast member.
The details
Take any pieces of data that you want access to and put them into a property list so that each element of data can be individually addressed:
[#data1: value1, #data2: value2, etc]
Wrap that property list into a handler that just returns the property list.
on mGetList me return ([#data1:value1, #data2:value2, etc]) end mGetList
Then put this handler into a "parent" script, and give it a recognizable name.
For example:
-- Member "ShoeData" on mGetList me return ([#type: "shoe", #color: "Brown", #mfg: ¬ "Floorsheim", #sizes:[7, 8, 9, 10, 11, 12]) end mGetList
This type of parent script, which I refer to as a "data cast member", is rather odd looking since it has no properties and there is no "new" handler. The trick here is that even though this is a parent script, you don't create an object from it. So it doesn't have, or need, a "new" handler. Instead, to get the data from the script, you call the handler(s) in the script, using the name of the script. For example, to get the data from the example above, you would make a call like this:
set lData = mGetList(script "MyDataCastMember")
This line calls the mGetList handler of the "MyDataCastMember" script. Since this handler just returns a property list, the variable "lData" gets set to the value of the property list in the mGetList handler. Then you can pull out the individual items from that list as follows:
set size = getProp(lData, #size) set color = getProp(lData, #color) set mfg = getProp(lData, #mfg) set lSizes = getProp(lData, #sizes)
Or if you are using Director 7 you can access these properties using dot syntax
size = lData.size color = lData.color mfg = lData.mfg lSizes = lData.sizes
Note that there is nothing special about the handler name "mGetList", any name will work just as well. I have just used this name in all of my coding and have gotten used to it as a convention. In fact, such a data cast member could have many different routines for getting different types of data:
on mGetStrings me return [<any strings you want to have>] end mGetStrings me on mGetConstants me return [<any constants you want to have>] end mGetConstants me on mGetInitialValues me return [<any initial values you want to have>] end mGetInitialValues
The names of all cast members are available anywhere in a movie. So by using the name of a script cast member, you are, in effect, using a "global" which doesn't need to be declared.
The best thing about the use of data cast members is that they are very fast. I have done a number of timings where I compared this style of data cast members against putting the same type of data in a field member. The result is that using this type of data cast members is about five times as fast as getting the same information from a field. This speed comes from the fact that data cast members are really just code returning a property list. (A speed comparison routine is available in the downloadable code at the end of the article but here are some samples).
-- Welcome to Director -- SpeedTest() -- "getList took 122" -- "field took 593" SpeedTest() -- "getList took 121" -- "field took 591" SpeedTest() -- "getList took 121" -- "field took 594"
There are some other additional advantages:
- The intent of a handler can become more clear when it is not cluttered up by the specific values it is operating on. This is often referred to as "separation of code from data". Further, once you have the code written correctly, you can then modify the data as much as you wish without ever looking at the code again.
- If you are using object oriented programming and you create many different objects from one parent script, this approach gives you an easy way for objects to have different initialization data. When you create an object, you can pass in the name of a data cast member that describes the data to be used to initialize the properties in the object. (This is a big reason why I like it.)
- If you have large amounts of data and code this approach helps you get around the 32K limit for one script.
Three uses
Here are three different ways that data cast members can be used to store and retrieve information. First, I'll show how you can use data cast members to just store pure data. Then, I'll show how you can use data cast members to effectively implement a database. And finally, I'll demonstrate how you can use data cast members to supply information when you create objects.
In order to make the demonstration clear, in these examples the amount of data that I am storing and retrieving is relatively small. In practice, the more data that you want to store and retrieve, the more this approach makes sense.
Representing the levels of a game
Imagine we are implementing a game. (This is not a real game, I'm just making one up for the purposes of showing how this style of code comes in handy.) Let's say that we want to code a board game that is played on a grid. On this grid, there is one player and some amount of four different kinds of "Robots" (R1, R2, R3, and R4) which the player must capture. The object of the game is for the player to move around the board and capture all the robots without being captured. When the player win the game, they move to the next of 40 levels. Each level gets more difficult by having more of each type of robot. There are also bonus items to be picked up at each level.
Since each level of the game will contain some number of R1, R2, R3, and R4 robots, we need a convenient way of encoding the data for each of the 40 levels of the game. To represent this, we can create a data cast member for each level.
First, I'll introduce a simple naming convention. I name all data cast members "D_" followed by some name. This makes them easy to find and it makes it easy to compute their names.
For this example, I'll call the data cast members: D_Level1, D_Level2, ... D_Level40. Each data cast member stores the number of the different types of robots and bonuses. For example, level one might have 1 R1 robot, 1 R2 robot, 0 R3 robots, 0 R4 robots, and 2 bonuses. The data cast member for D_Level1 could then be written as:
on mGetList me return [#nR1:1, #nR2:1, #nR3:0, #nR4:0, #nBonuses:2] end mGetList me
The data cast member for D_Level2 might be:
on mGetList me return [#nR1:2, #nR2:1, #nR3:1, #nR4:0, #nBonuses:2] end mGetList me
Whereas the data cast member for the D_Level40 might be:
on mGetList me return [#nR1:4, #nR2:4, #nR3:6, #nR4:6 , ¬ #nBonuses: 20]] end mGetList me
When we start a new level, we would extract out the information for that level as follows:
on StartLevel newLevel set dataCastMemberName = "D_Level" & string(newLevel) set lData = mGetList(script dataCastMemberName) set nR1s = getProp(#nR1) set nR2s = getProp(#nR2) set nR3s = getProp(#nR3) set nR4s = getProp(#nR4) set nBonuses = getProp(#nBonuses) -- play the game end StartLevel
Once this small piece of code is written, we can then alter the data and try out the different combinations of robots and bonuses by only altering the "D_Level" data cast members.
Keeping track of all players' progress
As another requirement in the game, let's say that we need to keep track of the progress of all the players who have played the game. At the beginning of the game, we would want to present a list of all known players and provide a type in field. If the player had played before, he or she could type in their name or select their name from the list. If they had not played before, they would have to type in their name.
So, what we need are ways to 1) keep track of the data for all players, 2) add a new player, and 3) update information about a player. This sounds like a job for a database. And, in essence it is. But I'd rather not use an external database program, and I would prefer not to store each player's information in a separate external file.
Instead, we can use a Director castlib as a database of data cast members. We can create a dedicated castlib which I'll "Players". Each member of the "Players" castlib is used to represent one player. As a player enters their name for the first time, we create a data cast member and name it using the same naming convention. For example, if I sign in as "Irv", we would create a data cast member named "D_Irv". The data in each data cast member might consist of a level number and a score, and any other information we wish to keep track of. So, a data cast member to represent my status might look like this:
on mGetList me return [#level:1, #score: 0] end mGetList
And, after playing through the first four levels, the contents of data cast member "D_Irv" might look something like this:
on mGetList me return [#level: 5, #score: 3000] end mGetList
So, here are the steps to implement this. At the beginning of the game, we need to scan through our "Players" castlib to find all existing players, so we can present this list:
on ShowExistingPlayers set nItems = the number of members of castlib "Players" set strPlayers = "" repeat with thisMemberNum = 1 to nItems if the type of member thisMemberNum of castlib ¬ "Players" = #script then set thisName = the name of member ¬ thisMemberNum of castlib "Players" -- eliminate the "D_" delete char 1 to 2 of thisName set strPlayers = strPlayers & thisName & RETURN end if end repeat if the length of strPlayers > 0 then -- get rid of the last RETURN delete the last char of strPlayers end if set the text of field "PlayerList" = strPlayers end ShowExistingPlayers
Next, we need to have a utility routine that, given the name of an existing player, returns the current information about that player. We do this by computing the name of the associated data cast member and then making a call to its mGetList method.
on GetPlayerData playerName set thisMemberName = "D_" & playerName set lData = mGetList(thisMemberName) -- For testing, show the data in the -- message window put "The data for player " & playerName & ¬ " is " & lData return lData end GetPlayerData
If player clicked on a name in the list of players, then we need to get the current information about that player. Here we just call the GetPlayerData routine passing the appropriate player name.
on ChoosePlayer lineNum set playerName = line lineNum of field "PlayerList" set lData = GetPlayerData(playerName) return lData end ChoosePlayer
We also need to allow the user to type in a name. In this case, we need to check if we have seen this name before. We do this by trying to find the related data cast member. If we find a match, we pull out the data for this player. Otherwise this is a new player and we must create a data cast member for this player.
on TypedInPlayerName set thePlayerName = the text of field ¬ "PlayerTypeIn" set fullName = "D_" & thePlayerName set nm = the number of member fullName ¬ of castlib "Players" if nm > 0 then set lData = GetPlayerData(thePlayerName) else set lData = CreateNewPlayer(thePlayerName) end if return lData end TypedInPlayerName
For a new player we must create a data cast member. After we create the script, we give it the appropriate name and set its "scriptType" to #parent. We assign the default beginning values for level and score, but call a different routine to actually store the data.
on CreateNewPlayer playerName set nmPlayer = new("script", castlib "Players") set the name of member nmPlayer = "D_" & playerName set the scriptType of member nmPlayer = #parent set lData = [#level:1, #score:0] SaveData(playerName, lData, nmPlayer) return lData end CreateNewPlayer
Finally, we must have a routine to write or re-write the data for a given player. This routine sets the "scriptText" of the data cast member so that the data can be retrieved later by just calling the mGetList method.
on SaveData playerName, lData, nmPlayer if voidp(nmPlayer) then -- if not passed in, then find it set nmPlayer = the number of member ("D_" & ¬ playerName) of castlib "Players" end if set theNewScript = "on mGetList me" & RETURN & ¬ " return " & string(lData) & RETURN & "end mGetList" set the scriptText of member nmPlayer = theNewScript end SaveData
One thing to note here. If you are using this approach to save user data, the castlib must reside on a modifiable disk, typically the user's hard disk. If the castlib were to reside on a CD-ROM, you would not be able to rewrite any user's data.
Posing test questions
To give the game some educational value, suppose we require that the player must answer a test question before moving on to the next level. Let's assume that there are nine questions at each level and that we should randomly select one question for the user to answer. All the question and answer data could be stored in data cast members. A single data cast member could consist of a question, a list of answers, and a right answer number. Here's how a single question and its answers might look:
on mGetList me return [#question: ["Robots usually use this ¬ as power source"], #lAnswers: [["Pizza"], ¬ ["Choclate Chip Cookies"], ["Jolt Cola"], ¬ ["Electricity"]], #rightAnswer: 4] end
At the beginning of the game, we could create a "Question" object whose job is to handle all the tasks of posing the question and answers, checking for the correct answer, and even tracking how many right and wrong answers the user has selected. Here is a parent script that will do this:
property pRightAnswer property pnAnsweredRight property pnAnsweredWrong on new me set pnAnsweredRight = 0 set pnAnsweredWrong = 0 return me end new on mAskQuestion me, currentLevel -- Assume that there are 9 questions per level -- named D_ -- and we want to pick a random one set dataCastMemberName = "D_" & currentLevel & random(9) set lData = mGetList(script dataCastMemberName) set questionText = getProp(lData, #question) set lAnswers = getProp(lData, #lAnswers) set pRightAnswer = getProp(lData, pRightAnswer) set the text of field "Question" = questionText set the text of field "Answer1" = getAt(lAnswers, 1) set the text of field "Answer2" = getAt(lAnswers, 2) set the text of field "Answer3" = getAt(lAnswers, 3) set the text of field "Answer4" = getAt(lAnswers, 4) end mAskQuestion -- When the user clicks on an answer, we call this method on mAnswerQuestion me, whichAnswerClicked if whichAnswerClicked = pRightAnswer then set pnAnsweredRight = pnAnsweredRight + 1 return TRUE else set pnAnsweredWrong = pnAnsweredWrong + 1 return FALSE end if end mAnswerQuestion
The important point here is that once the "Question" parent script has been written and debugged, the data can be modified without ever looking at the "Question" code. Using this approach, you can enlist the aid of a "content expert" who knows a great deal about a given subject matter, but who may know little or nothing about programming. In fact, on some projects I have worked on, I have written a "parser" which reads in content from a simple text file and creates data cast members. This way, the content expert can just edit a simple text document. When the content is ready, I just run the parser and create data cast members that are ready to be read in by the program.
Conclusion
Data cast members provide a way to store and very quickly retrieve data. They can allow you to separate out your code from your data so that your code will be much clearer. Data cast members can also be used to provide a very powerful database capability built purely in Lingo.
A sample movie is available for downlaod in Mac or PC format.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.