Remote Shared Objects

[ LiB ]  

In Chapter 6, you learned about local shared objects (LSOs). With them, you could store values on the user 's hard drive for later retrieval. Remote shared objects (RSOs) differ in many ways. The main idea of RSOs is that more than one user gets access to read and write values in an RSO. Also, when one user changes an RSO, all the users connected to the same app are immediately notified. This simple concept of letting multiple users see the same data in real time makes all kinds of applications possibleeverything from text chats to shared whiteboard applications.

Note

Remote Versus Local Shared Objects

Although RSOs have an entirely different purpose than LSOs, they're similar in two ways: Variables are stored in the same data structure (that is, as properties inside the data property) and, when set to be persistent, their values get saved between sessions.


Setting and Accessing Data in Remote Shared Objects

You can access and set values in an RSO like in an LSO: Just create an instance that points to the shared object's name , and then set variables inside the instance's data property. Because RSOs are tied to a NetConnection, however, you need to wait for the connection to succeed. For this reason, I often just invoke a custom function to handle all RSO instantiation. Listing 8.2 includes everything, including the NetConnection portion.

Listing 8.2. Basic Remote Shared Object
 1 function initRSO() { 2 my_rso = SharedObject.getRemote("myRSO", my_nc.uri, true); 3 my_rso.connect(my_nc); 4 } 5 my_nc = new NetConnection(); 6 my_nc.onStatus = function(info) { 7 if ( info .code == "NetConnection.Connect.Success") { 8 //ready to get the remote shared object 9 initRSO(); 10 } 11 }; 12 my_nc.connect("rtmp:/my_app/my_instance1"); 

Notice the initRSO() function is declared first, but isn't triggered until the NetConnection is successful (line 9). The basic code for the RSO is in lines 2 and 3. First, we perform a getRemote() and pass three values: the filename we want to use ("myRS0"), the NetConnection uri property (think: the application instance), and true to make it persistent (that is, saved between sessions). Then, we issue the shared object's connect() method to complete the process.

Because the RSO's connect() method is also asynchronous, you really need to define an onSync callback (such as the my_nc's onStatus callback on line 6) to ensure the RSO has successfully connected before proceeding. You'll see onSync in the next section on synchronizing, but for now, we'll just make sure we wait a second before trying to set or get values from the my_rso object.

Note

Persistent Versus Temporary

When you issue the getRemote() method (effectively creating an RSO instance), you have to include true or false for whether you want the RSO to be persistent or not. It's really pretty simple: If you want the variables contained to be available later, you say true (for persistent). Persistent RSOs actually reside in an FSO file like LSOsbut in the application folder on the server. Any RSO (persistent or not) keeps up-to-date in real time because all connected users are immediately notified any time contained variables change (as discussed in the next section, "Synchronizing Values"). The decision for persistent or not has to do with the nature of the data you're tracking.

Suppose you're building an online meeting app that includes a chat feature. In that case, the persistent option might be appropriate if you want to keep an archiveperhaps to allow someone to see the discussion at a later date. If you're building a two-player game, however, it's probably alright if the RSO that tracks the location of each player's game piece later vanishes when both users disconnect. Naturally, persistent RSOs can start to get cluttered while testing if you keep making up new variable names . Remember that any RSO is tied to the app name and the app instance. FCS maintains as many same-named RSOs as you have app instances. In the case of persistent RSOs, you'll actually see an FSO file show up in a subfolder to your application folder that matches the instance name (specified in the NetConnection connect() method).


After your RSO is connected, you can access and set named properties using the same syntax as for LSOs. That is, they all go inside the data property. Here's an example:


 my_rso.data.someVariable="this value"; trace("value is: " + my_rso.data.someVariable); 


If one connected user sets a named variable and then another user accesses it, both will see the latest value at the time they check. Luckily, however, you don't need each user to repeatedly check the values to see whether they happen to change. The onSync event is triggered every time a contained property changes you just have to write a callback to handle the onSync event.

Synchronizing Values

The idea is simple enough: Any time onSync is called, something about the RSO changed. If your RSO contains just a few properties, when onSync triggers you can just check the value of each property (and, presumably, respond by presenting the user with a visual representation). Although that technique is easy, it's undesirable when your RSO contains lots of propertiesbecause any time one property changes, you're checking each one.

To take something simple, suppose you have three List components that each maintain a real-time list of users who say they're happy, neutral, or unhappy (see Figure 8.4). Your RSO could contain three named properties (for instance, happy, neutral , and unhappy )that each contains an array of usernames. There are two sides to this application: letting users add their names to the appropriate array, and then displaying the arrays' values to everyone connected. Listing 8.3 goes through such an example that, although not ideal, is a decent solution.

Figure 8.4. The mood monitor will let us synchronize many different users' moods .

graphics/08fig04.jpg


Listing 8.3. Real-Time Mood Monitor

Changing the RSO

To give the user a way to set their mood, grab three RadioButton components (named happy_rb, neutral_rb , and unhappy_rb ) and an input text field (named username_txt ). The following code should be added to the code in Listing 8.2:

 1 happy_rb.addEventListener("click", setMyMood); 2 happy_rb.data = "happy"; 3 neutral_rb.addEventListener("click", setMyMood); 4 neutral_rb.data = "neutral"; 5 unhappy_rb.addEventListener("click", setMyMood); 6 unhappy_rb.data = "unhappy"; 7 function setMyMood(me) { 8 var thisMood = me.target.data; 9 if (my_rso.data[thisMood][0] == undefined) { 10 my_rso.data[thisMood] = []; 11 } 12 my_rso.data[thisMood].push(username_txt.text); 13 } 

Aside from some component tricks, there's not much RSO stuff here. The setMyMood() function finds the data in the button just pressed and puts it into the thisMood variable. (Notice the parameter me includes a target property that points back to the component that triggered the function.) Then because I'm about to add the username to one of three properties ( my_rso.data.happy, my_rso.data.neutral , or my_rso.data.unhappy ) but I don't know which, I use the bracket reference with the string thisMood . The if statement in line 9 just ensures the property in question contains an array (and if not puts an empty array into it in line 10). Finally, line 12 is the meat where I push the username_txt's text value into the correct property.

Responding to RSO Changes

The rest of the code just populates the three List components whenever there's a change to the RSO. You can just insert the following code between lines 2 and 3 from Listing 8.2. That is, you're just adding the onSync callback between where my_rso is created and gets connected. (The rest is " boilerplate " NetConnection code.)


 1 my_rso.onSync=function(){ 2 happy_lb.removeAll(); 3 var total=my_rso.data.happy.length; 4 for(var i=0;i<total;i++){ 5 happy_lb.addItem(my_rso.data.happy[i]); 6 } 7 neutral_lb.removeAll(); 8 var total=my_rso.data.neutral.length; 9 for(var i=0;i<total;i++){ 10 neutral_lb.addItem(my_rso.data.neutral[i]); 11 } 12 unhappy_lb.removeAll(); 13 var total=my_rso.data.unhappy.length; 14 for(var i=0;i<total;i++){ 15 unhappy_lb.addItem(my_rso.data.unhappy[i]); 16 } 17 } 


Nothing really special here except perhaps a good opportunity to consolidate the three free-standing loops down to a single nested loop. The main thing you should see, however, is that the approach makes sense: Whenever any property gets changed, all this code runs in order to repopulate the three List components. The inefficient part is that if, say, just one happy person adds his name, all three List components get repopulatedeven though just the happy one needs to update. In this case, it's only inefficient because you're redisplaying the List components. (FCS is smart enough not to send data that hasn't changed.) You'll see how you can track just the changes to an RSO next.

Using onSync Efficiently

The good news is that FCS is super efficient because clients are only notified of the properties that get modified in an RSO. The preceding example used onSync as the indicator that something changed and then just updated everything. When an onSync gets triggered, you can determine exactly what variable or variables changed, who changed them, and what they were changed from. That is, instead of my_rso.onSync=function(){} , you can use my_rso.onSync=function(list){} . The trick is just sorting out the data contained in the parameter that I'm calling list .

Here is a visualization of that parameter:

 

 [ {code:"change" , name:"happy" , oldValue: null}, {code:"change" , name:"neutral" , oldValue: null}, {code:"change" , name:"unhappy" , oldValue: null} ] 


Notice that because more than one variable may have changed, the parameter I'm calling list will always contain an array. In each slot of the array, there's a generic object with three properties: code, name , and oldValue . There are several possible values for code , but "change" is by far the most frequent. A close second is "success" . See, if you're the SWF who changed a property you'll get onSync triggered like normal, but the code (in the first slot of the array) will be "success" , whereas all other connected SWFs will see "change" . It's interesting that you're told the name of the property in questionbut not the new value. The thinking is that you only need to be notified that the variable has updated; it's up to you to go check its valueprovided you want to check it. Notice that in the preceding example oldValue is set to null in each slot. This is likely what you would see when the movie first connects. If another SWF changes a property, however, you'll be told what its old value was before the change.


Note

Other Values for the Code Property

Just to be complete, let me say that in addition to "change" and "success" , other possible values for code are "reject", "delete" , and "clear" . If you try to change a property and another client beats you to the punch, you'll see "reject" . When you delete a property (for instance, delete(my_rso.data.happy); ), you'll see "delete" for code's value. Finally, "clear" primarily applies to RSOs that are not persistent. (That's the third parameter in the getRemote() method.) Basically, when you connect to a nonpersistent RSO, all properties are cleared and you start fresh. (You'll see more about the persistent option in the next section.)


Now that you have an idea of how onSync's parameter is structured, let me show you a modified version of the mood application Listing 8.4but this time only the property that changes causes an update to its respective List component.

Listing 8.4. Revising the onSync

First, consider that the following code extracted from Listing 8.3 (which was repeated for each List component) can be "genericized" by just replacing happy_lb with a dynamic reference to a list.

 happy_lb.removeAll(); var total=my_rso.data.happy.length; for(var i=0;i<total;i++){ happy_lb.addItem(my_rso.data.happy[i]); } 

Here is the revised onSync callback:


 1 my_rso.onSync = function(list) { 2 var totalSOs=list.length; 3 for (var n=0; n<totalSOs; n++) { 4 var thisObj = list[n]; 5 if (thisObj.code == "change"   thisObj.code == "success") { 6 var sName=thisObj.name; 7 _root[sName+"_lb"].removeAll(); 8 var total = my_rso.data[sName].length; 9 for (var i = 0; i<total; i++) { 10 _root[sName+"_lb"].addItem(my_rso.data[sName][i]); 11 } 12 } 13 } 14 }; 


Notice that lines 7 through 11 are based on the code extracted from the old listing. Instead of .happy_lb , however, you see [sName+"_lb"]; and instead of .happy , you see [sName] . These dynamic references rely on the sName variable containing a string name of the property in question. I suppose I should have done something dynamic such as this in the first example, but the big news here is that while you loop through all the objects passed (that is, totalSOs ), you only modify lists that correspond to property names whose code is "change" or "success" .

I'll be the first to admit that it's always easier to just go through all variables and redraw everything needed to bring the visual representation into sync, but it's not efficient. It can get really slow depending what's changing onscreen. In addition, it wastes bandwidth for each SWF to contact the server and check values for variables that haven't changed. Although meticulously parsing through onSync's parameter (looking for code properties and such) will render the best results, later you'll see an alternative messaging approach that is arguably simpler. Messaging involves creating your own named events (not unlike onSync , but homemade). This way you just trigger events like you would functions (but all clients will trigger them, too).

In the mood app, for instance, you could have defined callback events such as my_rso.updateHappy() and my_rso.updateNeutral() . However, because FCS controls when variables change and when clients are notified, you can't rely on a message reaching all clients in the sequence you intend. Because of this, it's not easy to design " client-to-client " event messaging. As you'll see in the "Messaging" section of Chapter 9, however, sending messages through the server as a clearinghouse works quite well because (for one thing) the server can lock an RSO, change a property, unlock it, and then send a message to all the clients.

Just remember, the main thing onSync does is tell you something happened to the RSO. If you want to know exactly what has happened, you need to parse through onSync's parameter.

Architectural Decisions

In Chapter 4, "Working with Complex Data," when you considered different ways to structure data you could usually pick the option that appeared most convenient for you. That is, the difference between 4 rows of 3 or 3 rows of 4 was like the difference between 6 and half a dozenI told you to pick the one that made the most sense to you. In the case of structuring RSOs, however, you can't be so selfish. Your first priority needs to be bandwidth efficiency, because you may have thousands of users connected simultaneously ; put overall performance second, and then the programmer's convenience last.

Generally, your structure should minimize the amount of data that gets updated (and, hence, sent over the Internet). In short, it's best to have lots of small properties in a shared object (as opposed to fewer, larger properties); and it's best not to let any one shared object file get too big. In the mood app, for example, the single RSO had three properties that each had an array of names. That structure is totally valid and pretty convenient. (After all, arrays are good for such cases where you don't know how many items may get added.) However, each array could get pretty large, and the whole array will get sent to each client any time a single name is added to it. So, although it might look totally goofy to a programmer, here is a much better structure (FCS-wise):


 my_rso.data.happy_1 my_rso.data.happy_2// and so on my_rso.data.neutral_1 my_rso.data.neutral_2// and so on my_rso.data.unhappy_1 my_rso.data.unhappy_2// and so on my_rso.data.count_happy my_rso.data.count_neutral my_rso.data.count_unhappy 


The idea is that you'll have as many "happy_x", "neutral_x" , and "unhappy_x" values as you have people of that mood (replacing "x" with an integer). The three count properties are not only to track how many items are in each list, it also helps calculate a unique suffix for each newly created "happy_x" property. This restructuring means you must adjust the code, and it may get more complex. In this case, one could argue that an array full of strings isn't really that much data to be transmitting unnecessarily. In this case, maybe, but just understand why this is a better structure: When someone adds her name to a category, only two small properties are changed in the RSO (her name and the counter for total in that category). Listing 8.5 shows the modified code for both the user input part and the onSync event. (You still need the NetConnection and boilerplate RSO scripts from Listing 8.3). Incidentally, because the RSO's data structure will change, now is a good time to change the app instance name.

Listing 8.5. Using Smaller Properties
 1 function setMyMood(me) { 2 var thisMood = me.target.data; 3 if (my_so.data["count_"+thisMood] == undefined) { 4 my_so.data["count_"+thisMood] = 0; 5 } 6 var nextUp = my_so.data["count_"+thisMood]++; 7 my_so.data[thisMood+"_"+nextUp] = username_txt.text; 8 } 

This input portion of the code changed in a few ways. I do check whether there's a value in count_happy (or whichever mood), although it turns out we could be sloppy because the purpose of the count property is not so much to count but to ensure each new property has a unique suffix. Anyway, in line 7 I increment the appropriate count as well as store it in the nextUp variable, then set the value of a newly created property ( thisMood+"_"+nextUp ) to the user's username. So far, so good.

The only other code that changes is how onSync displays information.


 1 my_so.onSync = function(list) { 2 for (var n = 0; n<list.length; n++) { 3 var thisObj = list[n]; 4 if (thisObj.code == "change"   thisObj.code == "success") { 5 var sName = thisObj.name; 6 var prefix = sName.substr(0, sName.indexOf("_")); 7 if (prefix == "happy"   8 prefix == "neutral"   9 prefix == "unhappy") { 10 _root[prefix+"_lb"].addItem(my_so.data[sName]); 11 } 12 } 13 } 14 }; 


The main difference in this code is that we don't need to perform a removeAll() on the List components. That's because they'll be empty initially, onSync will send all the properties at start up, and then just send the ones that need to be added while the app runs. Notice how the string methods substr() and indexOf() (in line 6) are used to extract just the portion in front of an underscore . Then, line 7 starts the compound if statement that checks whether the property just changed is in fact one of the three moods. If so, that item is added to the list.

It turns out there really wasn't too much extra coding in this revised structure. In this case, you'll probably need to add a ton of usernames to the various lists to see a difference in performance. However, it may very well be worth the effort.

In addition to creating more small properties (instead of a few big ones), you should keep an eye on the size of your FSO file. That is, when you execute a getRemote() , the first parameter you pass refers to the actual filename (minus the extension). It makes sense to group related properties and store each set in a separate RSO. For example:


 my_scores=ShareObject.getRemote("scores", my_nc.uri, true); my_users=SharedObject.getRemote("users", my_nc.uri, true); my_active=SharedObject.getRemote("active", my_nc.uri, false); 


You just have to decide (and then keep track of) which shared object does what. Notice too, this technique means some RSOs could be persistent and others not (such as the last line in the preceding example). Upon arrival to your app, the user needs to wait for the entire RSO to download. Therefore you don't want any single .fso file to grow too large.

Practical Examples

Although the mood population app might have a use somewhere, I've put together a few examples using RSOs that will have more universal appeal .

Example 1: Geographic Locator

Here's a moderately simple example where each user can click a map to share his or her geographic location with all other connected users. Figure 8.5 shows what the result looks like.

Figure 8.5. Remote shared objects are perfect to track and display all users' locations.

graphics/08fig05.jpg


Every time a user clicks, the "you are here" movie clip moves to the _xmouse and _ymouse locations and those values are set in the RSO. Every time the shared object's onSync event is triggered, a new "other person" clip instance is created onstage in the updated location. The interesting part is how to separate the "you are here" indicator from all the other clips. That is, you don't want to see one of the "other person" clips where you arejust where the others are. This is solved two ways: If the onSync's code property is "success" , it means we were the ones who changed a value; therefore we don't need to refresh the screen. In addition, each user is given a unique ID so that when we do display all the other people, we check that the ID doesn't match ours. Listing 8.6 contains some excerpts from the complete code that you can download.

Listing 8.6. Map Code Excerpts
 owner=this;//so we don't use _root owner.onMouseDown = function() { you_mc._x = _xmouse; you_mc._y = _ymouse; so.data.locations[owner.myID].x=you_mc._x so.data.locations[owner.myID].y=you_mc._y; }; 

We'll just have one property in the shared object, an array called locations , which contains objects with three properties each: x, y , and slot . Every time the user clicks, we set the x and y properties of the appropriate slot in the locations array. (The value of owner.myID is assigned when the shared object first loads, as you'll see next.)


 1 init=false; 2 so.onSync = function(list) { 3 if (so.data.locations == undefined) { 4 so.data.locations = []; 5 } 6 if(init==false){ 7 init=true; 8 owner.myID=so.data.locations.length; 9 so.data.locations.push({x:-100, y:-100, slot:owner.myID}); 10 } 11 var totalSOs = list.length; 12 for (var n = 0; n<totalSOs; n++) { 13 var thisObj = data[n]; 14 if (thisObj.code == "change") { 15 refreshDisplay(); 16 } 17 if (thisObj.code == "success") { 18 //don't have to (or want to) do anything to ourselves 19 } 20 } 


In the onSync , we first check that locations is dimensioned as an array (line 3). Then, if we've never been here ( init==false ), we set the owner.myID value to the length of the locations array, and then stuff a generic object onto the end of the array (lines 6 through 9). In line 12, you see the main loop to go through all the properties in the shared object's list even though locations is the only property. Notice that only if the code is "change" (meaning someone else modified the shared object) do we trigger the refreshDisplay() function (as you'll see next).


 function refreshDisplay() { var thisOne; var id; var total = so.data.locations.length; for (var n = 0; n<total; n++) { if(n!=owner.myID){ owner.attachMovie("star", "star_"+n, n+1); thisOne=owner["star_"+n]; thisOne._x=so.data.locations[n].x; thisOne._y=so.data.locations[n].y; } } } 


Basically, this code just goes through every item in the locations array and assuming the slot doesn't match myID , we create a new instance using attachMovie() and position its _x and _y .

You'll find more details in the complete version online, but do notice this example doesn't remove people from the map when they leave. You need a bit of server-side code for that (as discussed in the next chapter).

Example 2: Favorite Movie Vote Counter

The following example can be adapted for all kinds of survey needs. Here, we're just letting people vote for their favorite movies. Everyone can see how many votes each movie has received, and users can even add their favorite movie if it's not already listed. Actually, there are almost no error checks, so users can vote twice or add movies already listed. It's fairly simple and only took about an hour to build. You can see what it looks like in Figure 8.6 or visit www.phillipkerman.com/rias/movie_survey. In addition, the entire code file is available on my site.

Figure 8.6. We can quickly track users' votes for their favorite movies using an RSO.

graphics/08fig06.jpg


I structured the shared object's data by creating a property for each movie "movie_x" (where x went from 1 to 2 to 3 and so on). In each "movie_x" slot, I stored a generic object with three properties: title, votes , and id . Although I'm not using id , I thought it might come in handy later. If, say, I want to sort the movies by rank or name, I need to have a way to identify movies by something other than the order in which they appear in the list. In addition, I have a counter property called total_movies that tracks how many movies there are. Finally, I used a List component (to hold all the movies) and a regular Input Text field to let users add their own titles. Listing 8.7 shows the key excerpts from the code.

Listing 8.7. Movie Code Excerpt
 1 function doAddTitle() { 2 var total = ++so.data.total_movies; 3 so.data["movie_"+ total]=new Object(); 4 so.data["movie_"+ total].title=movieTitle_txt.text; 5 so.data["movie_"+ total].votes=1; 6 so.data["movie_"+ total].id =total; 7 movieTitle_txt.text=""; 8 } 

The doAddTitle() function is triggered only after the user has filled in the movieTitle_txt Input Text field and pressed a button. Line 2 both adds to the current movie count and sets the local variable total to the new value. (Notice ++ will increment total_movies whether you put it in before or after that variable, but putting it before returns the resulting valueso total becomes the new total.) Anyway, lines 3 through 6 put a generic object in a dynamically named property, set the title and votes properties, and assign a value to id . To ensure every movie gets its own id , I just keep adding to the unique property. I'll never lower that number even if I remove movies.


 function doAddVote() { var movieNum = movies_lb.selectedItem.data; so.data["movie_"+movieNum].votes++; } 


When users select a movie they want to vote for (by clicking the movies_lb list), they can trigger the doAddVote() function. All I have to do is increment the votes property of the appropriate "movie_x" (dynamically referenced in line 3).


 1 function refreshDisplay() { 2 movies_lb.removeAll(); 3 var total = so.data.total_movies; 4 var thisOne; 5 for (var i = 0; i<total; i++) { 6 thisOne=so.data["movie_"+Number(i+1)]; 7 movies_lb.addItem("votes: "+thisOne.votes+ 8 " title: "+thisOne.title, thisOne.id); 9 } 10 addVote_pb.enabled=false; 11 } 


The refreshDisplay() function is triggered in the shared object's onSync any time the data changes. Basically, it just clears out the list and then loops through all the movies. Line 8's addItem() method formats how the data is presented in the list. You can see the resulting presentation in Figure 8.6. Just realize that after you have the data you can present it however you want. You could even make a bar graph or something.

Even when you see the complete code for the previous two examples, you'll see they're pretty simple. No doubt you can probably think of ways to improve them. For example, the favorite movie app lets users vote more than once. You could use an LSO to reduce that issue. However, you may want to wait until you learn a little about server-side ActionScript in Chapter 9, because it makes more advanced features easier to implement ( specifically , tracking the user's IP address to prevent multiple votes).

On the surface, RSOs don't seem particularly exciting. Accessing shared variables (and being informed when others change them) doesn't sound like a huge deal, but it really is. Before we move on to audio and video streams (indisputably a sexier topic), let me point out that in addition to multiple users getting access to RSOs, server scripts can too. You'll see in Chapter 9 that just a little bit of serverside coding goes a long way to simplify many tasks . If you want to build a poker game, for example, the server could act as the dealer. Using what you know so far, one user would have to be the dealer and if that user left, the game would halt. Think of server scripts as anonymous robot-users that can make sure everything runs smoothly. Well, maybe it's not that rosy, but I wanted to remind you that RSOs come up again next chapter.

[ LiB ]  


Macromedia Flash MX 2004 for Rich Internet Applications
Macromedia Flash MX 2004 for Rich Internet Applications
ISBN: 0735713669
EAN: 2147483647
Year: 2002
Pages: 120

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net