Advertisement

Building Real Time Web Applications With Adobe Cirrus

by

Building real time networked games and applications can be challenging. This tutorial will show you how to connect flash clients using Cirrus, and introduce you to some vital techniques.

Let's take a look at the final result we will be working towards. Click the start button in the SWF above to create a 'sending' version of the application. Open this tutorial again in a second browser window, copy the nearId from the first window into the textbox, and then click Start to create a 'receiving' version of the application.

In the 'receiving' version, you'll see two rotating needles: one red, one blue. The blue needle is rotating of its own accord, at a steady rate of 90°/second. The red needle rotates to match the angle sent out by the 'sending' version.

(If the red needle seems particularly laggy, try moving the browser windows so that you can see both SWFs at once. Flash Player runs EnterFrame events at a much lower rate when the browser window is in the background, so the 'sending' window transmits the new angle much less frequently.)


Step 1: Getting Started

First things first: you need a Cirrus 'developer key', which can be obtained at the Adobe Labs site. This is a text string that is uniquely assigned to you on registration. You will use this in all the programs you write to get access to the service, so it might be best to define it as a constant in one of your AS files, like this:

 
public static const CIRRUS_KEY:String = "<my string here>";

Note that its each developer or development team that needs its own key, not each user of whatever applications you create.


Step 2: Connecting to the Cirrus Service

We begin by creating a network connection using an instance of the (you guessed it) NetConnection class. This is achieved by calling the connect() method with your previously mentioned key, and the URL of a Cirrus 'rendezvous' server. Since at the time of writing Cirrus uses a closed protocol there is only one such server; its address is rtmfp://p2p.rtmfp.net

 
public class Cirrus 
{ 
	public static const CIRRUS_KEY:String = "<my string here>" 
 
	private static var netConnection:NetConnection; 
	 
	public static function Init(key:String):void 
	{ 
		if( netConnection != null ) return; 
			 
		netConnection = new NetConnection(); 
			 
		try { netConnection.connect("rtmfp://p2p.rtmfp.net", key); } 
			 
		catch(e:Error) {} 
	} 
}

Since nothing happens instantly in network communication, the netConnection object will let you know what it's doing by firing events, specifically the NetStatusEvent. The important information is held in the code property of the event's info object.

 
private function OnStatus(e:NetStatusEvent):void 
{ 
	switch(e.info.code) { 
	case "NetConnection.Connect.Success": break; //The connection attempt succeeded. 
	case "NetConnection.Connect.Closed":  break; //The connection was closed successfully. 
	case "NetConnection.Connect.Failed":  break; //The connection attempt failed. 
	} 
}

An unsuccessful connection attempt is usually due to certain ports being blocked by a firewall. If this is the case, you have no choice but to report the failure to the user, as they won't be connecting to anyone until the situation changes. Success, on the other hand, rewards you with your very own nearID. This is a string property of the NetConnection object that represents that particular NetConnection, on that particular Flash Player, on that particular computer. No other NetConnection object in the world will have the same nearID.

The nearID is like your own personal phone number - people who want to talk to you will need to know it. The reverse is also true: you will not be able to connect to anyone else without knowing their nearID. When you supply someone else with your nearID, they will use it as a farID: the farID is the ID of the client that you are trying to connect to. If someone else gives you their nearID, you can use it as a farID to connect to them. Get it?

So all we have to do is connect to a client and ask them for their nearID, and then... oh wait. How do we find out their nearID (to use as our farID) if we're not connected to each other in the first place? The answer, which you'll be suprised to hear, is that it's impossible. You need some kind of third-party service to swap the ids over. Examples would be:

  • Building a server application to act as a 'lobby'
  • Emailing, or instant-messaging, your nearID to someone else
  • Cooking something up using NetGroups, which we might look at in a future tutorial

Step 3: Using Streams

The network connection is purely conceptual and doesn't help us much after the connection has been set up. To actually transfer data from one end of the connection to another we use NetStream objects. If a network connection can be thought of as building a railway between two cities, then a NetStream is a mail train that carrys actual messages down the track.

NetStreams are one-directional. Once created they act as either a Publisher (sending information), or a Subscriber (receiving information). If you want a single client to both send and receive information over a connection, you will therefore need two NetStreams in each client. Once created a NetStream can do fancy things like stream audio and video, but in this tutorial we will stick with simple data.

If, and only if, we recieve a NetStatusEvent from the NetConnection with a code of NetConnection.Connect.Success, we can create a NetStream object for that connection. For a publisher, first construct the stream using a reference to the netConnection object we just created, and the special pre-defined value. Second, call publish() on the stream and give it a name. The name can be anything you like, it's just there for a subscriber to differentiate between multiple streams coming from the same client.

 
var ns:NetStream = new NetStream(netConnection, NetStream.DIRECT_CONNECTIONS); 
 
ns.publish(name, null);

To create a subscriber, you again pass the netConnection object into the constructor, but this time you also pass the farID of the client you want to connect to. Secondly, call play() with the name of the stream that corresponds to the name of the other client's publishing stream. To put it another way, if you publish a stream with the name 'Test', the subscriber will have to use the name 'Test' to connect to it.

 
var ns:NetStream = new NetStream(netConnection, farID); 
 
ns.play(name);

Note how we needed a farID for the subscriber, and not the publisher. We can create as many publishing streams as we like and all they will do is sit there and wait for a connection. Subscribers, on the other hand, need to know exactly which computer in the world they're supposed to be subscribing to.


Step 4: Transferring Data

Once a publishing stream is set up it can be used to send data. The netstream Send method takes two arguments: a 'handler' name, and a variable length set of parameters. You can pass any object you like as one of these parameters, including basic types like String, int and Number. Complex objects are automatically 'serialized' - that is, they have all their properties recorded on the sending side and then re-created on the recieving side. Arrays and ByteArrays copy just fine too.

The handler name corresponds directly to the name of a function that will eventually be called on the receiving side. The variable parameter list corresponds directly to the arguments the receiving function will be called with. So if a call is made such as:

 
var i:int = 42; 
netStream.send("Test", "Is there anybody there?", i);

The receiver must have a method with the same name and a corresponding signature:

 
public function Test(message:String, num:int):void 
{ 
	trace(message + num); 
}

On what object should this receiving method be defined? Any object you like. The NetStream instance has a property called client which can accept any object you assign to it. That's the object on which the Flash Player will look for a method of the corresponding sending name. If there's no method with that name, or if the number of parameters is incorrect, or if any of the argument types cannot be converted to the parameter type, an AsyncErrorEvent will be fired for the sender.


Step 5: Pulling everything together

Let's consolidate the things we've learned so far by putting everything into some kind of framework. Here's what we want to include:

  • Connecting to the Cirrus service
  • Creating publishing and subscribing streams
  • Sending and receiving data
  • Detecting and reporting errors

In order to receive data we need some way of passing an object into the framework that has member functions which can be called in response to the corresponding send calls. Rather than an arbitrary object parameter, I'm going to code a specific interface. I'm also going to put into the interface some callbacks for the various error events that Cirrus can send out - that way I can't just ignore them.

 
package  
{ 
	import flash.events.ErrorEvent; 
	import flash.events.NetStatusEvent; 
	import flash.net.NetStream; 
	 
	public interface ICirrus 
	{ 
		function onPeerConnect(subscriber:NetStream):Boolean; 
		 
		function onStatus(e:NetStatusEvent):void; 
		function onError(e:ErrorEvent):void; 
	} 
	 
	 
}

I want my Cirrus class to be as easy to use as possible, so I want to hide the basic details of streams and connections from the user. Instead, I'll have one class that acts as either a sender or reciever, and which connects the Flash Player to the Cirrus service automatically if another instance hasn't done so already.

 
package  
{ 
	import flash.events.AsyncErrorEvent; 
	import flash.events.ErrorEvent; 
	import flash.events.EventDispatcher; 
	import flash.events.IOErrorEvent; 
	import flash.events.NetStatusEvent; 
	import flash.events.SecurityErrorEvent; 
	import flash.net.NetConnection; 
	import flash.net.NetStream; 
	 
	public class Cirrus 
	{ 
		private static var netConnection:NetConnection; 
		public function get nc():NetConnection { return netConnection; } 
		 
		//Connect to the cirrus service, or if the netConnection object is not null 
		//assume we are already connected 
		public static function Init(key:String):void 
		{ 
			if( netConnection != null ) return; 
			 
			netConnection = new NetConnection(); 
			 
			try { netConnection.connect("rtmfp://p2p.rtmfp.net", key); } 
			 
			catch(e:Error) 
			{ 
				//Can't connect for security reasons, no point retrying. 
			} 
		} 
		 
		public function Cirrus(key:String, iCirrus:ICirrus) 
		{ 
			Init(key); 
			 
			this.iCirrus = iCirrus; 
			 
			netConnection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, OnError); 
			netConnection.addEventListener(IOErrorEvent.IO_ERROR, OnError); 
			netConnection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, OnError) 
			netConnection.addEventListener(NetStatusEvent.NET_STATUS, OnStatus); 
			 
			if( netConnection.connected ) 
			{ 
				netConnection.dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false, {code:"NetConnection.Connect.Success"})); 
			} 
		} 
		 
		private var iCirrus:ICirrus; 
		public  var ns:NetStream = null; 
	} 
}

We'll have one method to turn our Cirrus object into a publisher, and another to turn it into a sender:

 
public function Publish(name:String, wrapSendStream:NetStream = null):void 
{ 
	if( wrapSendStream != null ) ns = wrapSendStream; 
			 
	else 
	{ 
		try { ns = new NetStream(netConnection, NetStream.DIRECT_CONNECTIONS); } 
		catch(e:Error) { return; } 
	} 
			 
	ns.addEventListener(NetStatusEvent.NET_STATUS, OnStatus); 
	ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, OnError); 
	ns.addEventListener(IOErrorEvent.IO_ERROR, OnError); 
			 
	ns.client = iCirrus; 
			 
	ns.publish(name, null); 
} 
		 
public function Play(farId:String, name:String):void 
{ 
	try { ns = new NetStream(netConnection, farId); } 
	catch(e:Error) { return; } 
	 
	ns.addEventListener(NetStatusEvent.NET_STATUS, OnStatus); 
	ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, OnError); 
	ns.addEventListener(IOErrorEvent.IO_ERROR, OnError); 
			 
	ns.client = iCirrus; 
			 
	try { ns.play.apply(ns, [name]); } 
	catch(e:Error) {} 
}

Finally, we need to pass along the events to the interface we created:

 
private function OnError(e:ErrorEvent):void 
{ 
	iCirrus.onError(e); 
} 
		 
private function OnStatus(e:NetStatusEvent):void 
{ 
	iCirrus.onStatus(e); 
}

Step 6: Creating a Test Application

Consider the following scenario involving two Flash applications. The first app has a needle that steadily rotates around in a circle (like a hand on a clock face). On each frame of the app, the hand is rotated a little further, and also the new angle is sent across the internet to the receiving app. The receiving app has a needle, the angle of which is set purely from the latest message received from the sending app. Here's a question: Do both needles (the needle for the sending app and the needle for the receiving app) always point to the same position? If you answered 'yes', I highly recommend you read on.

Let's build it and see. We'll draw a simple needle as a line eminating from the origin (coordinates (0,0)). This way, whenever we set the shape's rotation property the needle will always rotate as if one end is fixed, and also we can easily position the shape by where the centre of rotation should be:

 
private function CreateNeedle(x:Number, y:Number, length:Number, col:uint, alpha:Number):Shape 
{ 
	var shape:Shape = new Shape(); 
	shape.graphics.lineStyle(2, col, alpha); 
	shape.graphics.moveTo(0, 0); 
	shape.graphics.lineTo(0, -length); //draw pointing upwards 
	shape.graphics.lineStyle(); 
	shape.x = x; 
	shape.y = y; 
	return shape; 
}

It's inconvenient to have to set up two computers next to each other so on the receiver we'll actually use two needles. The first (red needle) will act just as in the description above, setting its angle purely from the latest message received; the second (blue needle) will get its initial position from the first rotation message received, but then rotate automatically over time with no further messages, just like the sending needle does. This way, we can see any discrepancy between where the needle should be and where the received rotation messages say it should be, all by starting both apps and then only viewing the receiving app.

 
private var first:Boolean = true; 
 
//Called by the receiving netstream when a message is sent 
public function Data(value:Number):void 
{ 
	shapeNeedleB.rotation = value; 
			 
	if( first ) 
	{ 
		shapeNeedleA.rotation = value; 
		first = false; 
	} 
} 
 
private var dateLast:Date = null; 
 
private function OnEnterFrame(e:Event):void 
{ 
	if( dateLast == null ) dateLast = new Date(); 
	 
	//Work out the amount of time elapsed since the last frame. 
	var dateNow:Date = new Date(); 
			 
	var s:Number = (dateNow.time - dateLast.time) / 1000; 
			 
	dateLast = dateNow; 
	 
	//Needle A is always advanced on each frame. 
	//But if there is a receiving stream attached, 
	//also transmit the value of the rotation. 
	 
	shapeNeedleA.rotation += 360 * (s/4); 
	 
	if( cirrus.ns.peerStreams.length != 0 ) 
		cirrus.ns.send("Data", shapeNeedleA.rotation); 
}

We'll have a text field on the app that allows the user to enter a farID to connect to. If the app is started without entering a farID it will set itself up as a publisher. That pretty much covers creating the app you see at the top of the page. If you open two browser windows you can copy the id from one window to the other, and set one app to subscribe to the other. It will actually work for any two computers connected to the Internet - but you'll need some way of copying over the nearID of the subscriber.


Step 7: Putting a Spanner In the Works

If you run both the sender and receiver on the same computer the rotation information for the needle doesn't have far to travel. In fact, the data packets sent out from the sender don't even have to touch the local network at all because they are destined for the same machine. In real-world conditions the data has to to make many hops from computer to computer and with each hop introduced, the likelihood of problems increase.

Latency is one such problem. The further the data physically has to travel, the longer it will take to arrive. For a computer based in London, data will take less time to arrive from New York (a quarter of the way around the globe) than from Sydney (half way around the globe). Network congestion is also a problem. When a device on the Internet is operating at saturation point and is asked to transfer yet another packet, it can do nothing but discard it. Software using the internet must then detect the lost packet and ask the sender for another copy, all of which adds lag into the system. Depending on each end of the connection's location in the world, time of day, and available bandwidth the quality of the connection will vary widely.

So how do you hope to test for all these different scenarios? The only practical answer is not to go out and try and find all these different conditions, but to re-create a given condition as required. This can be achieved using something called a 'WAN emulator'.

A WAN (Wide Area Network) emulator is software that interferes with the network traffic travelling to and from the machine it's running on, in such a way as to attempt to recreate different network conditions. For example, by simply discarding network packets transmitted from a machine, it can emulate the packet loss that might occur at some stage in the real-world transmission of the data. By delaying packets by some amount before they are sent on by the network card, it can simulate various levels of latency.

There are various WAN emulators, for various platforms (Windows, Mac, Linux), all licensed in various ways. For the rest of this article I'm going to use the Softperfect Connection Emulator for Windows for two reasons: it's easy to use, and it has a free trial.

(The author and Tuts+ are in no way affiliated with the product mentioned. Use at your own risk.)

Once your WAN Emulator is installed and running, you can easily test it by downloading some kind of stream (such as Internet radio, or streaming video) and gradually increasing the amount of packet loss. Inevitably the playback will stall once the packet loss reaches some critical value which depends on your bandwidth and the size of the stream.

Oh, and please note the following points:

  • If both the sending and receiving apps are on the same computer the connection will work just fine, but the WAN Emulator will not be able to affect the packets sent between them. This is because (on Windows at least) packets destined for the same computer are not sent to the network device. A sender and receiver on the same local network works fine, however - plus you can copy the nearID to a text file so you don't have to write it down.
  • These days, when a broswer window is minimized, the browser artificially reduces the framerate of the SWF. Keep the browser window visible on screen for consistent results.


SoftPerfect emulator showing packet loss

In the normal state you will see the red and blue needles point to pretty much the same position, perhaps with the red needle occasionally flickering as it falls behind, then suddenly catching up again. Now if you set your WAN emulator to 2% packet loss you will see the effect become much more pronounced: roughly every second or so you will see the same flicker. This is literally what happens when the packet carrying the rotation information is lost: the red needle just sits and waits for the next packet. Imagine how it would look if the app wasn't transferring the needle rotation, but the position of some other player in a multiplayer game - the character would stutter every time it moved to a new position.

In adverse conditions you may expect (and therefore should design for) up to 10% packet loss. Try this with your WAN Emulator and you might catch a glimpse of a second phenomenon. Clearly the stuttering effect is more pronounced - but if you look closely, you'll notice that when the needle falls a long way behind, it doesn't actually snap back to the correct position but has to quickly 'wind' forwards again.

In the game example this is undesirable for two reasons. First, it's going to look odd to see a character not just stuttering but then positively zooming towards its intended position. Second, if all we want to see is a player character at its current position then we don't care about all those intermeditate positions: we only want the most recent position when the packet is lost and then retransmitted. All information except the most recent is a waste of time and bandwidth.



SoftPerfect emulator showing latency

Set your packet loss back to zero and we'll look at latency. It's unlikely that in real-world conditions you'll ever get better then about 30ms latency so set your WAN Emulator for that. When you activate the emulation you'll notice the needle drop back quite some way as each endpoint reconfigures itself to the new network speed. Then, the needle will catch up again until it is consistently some distance behind where it should be. In fact the two needles will look rock solid: just slightly apart from each other as they rotate. By setting different amounts of latency, 30ms, 60ms, 90ms, you can practically control how far apart the needles are.

Image the computer game again with the player character always some distance behind where they should be. Every time you aim at the player and take a shot you will miss, because every time you line up the shot you're looking at where the player used to be, and not where they are now. The worse the latency, the more apparent the problem. Players with poor internet connections could be, for all purposes, invulnerable!


Step 8: Reliability

There aren't many quick fixes in life so it's a pleasure to relate the following one. When we looked at packet loss we saw how the needle would noticably wind forwards as it caught up to its intended rotation after a loss of information. The reason for this is that behind the scenes each packet sent had a serial number associated with it that indicated its order.

In other words, if the sender were to send out 4 packets...

A, B, C, D

And if one, lets say 'B' is lost in transmission so that the receiver gets...

A, C, D

...the receiving stream can pass 'A' immediately to the app, but then has to inform the sender about this missing packet, wait for it to be received again, then pass 're-transmitted copy of B', 'C', 'D'. The advantage of this system is that messages will always be received in the order they were sent, and that any missing information is filled in automatically. The disadvantage is that the loss of a single packet causes relatively large delays in the transmission.

In the computer game example discussed (where we are updating the player character's position in real time), despite not actually wanting to lose information, it's better to just wait for the next packet to come along than to take the time to tell the sender and wait for re-transmission. By the time packet 'B' arrives it will already have been superseded by packets 'C', and 'D', and the data it contains will be stale.

As of Flash Player 10.1, a property was added to the NetStream class to control just this kind of behaviour. It is used like this:

 
public function SetRealtime(ns:NetStream):void 
{ 
	ns.dataReliable = false; 
	ns.bufferTime   = 0; 
}

Specifically it's the dataReliable property that was added, but for technical reasons it should always be used in conjunction with setting the bufferTime property to zero. If you alter the code to set the sending and receiving streams in this way and run another test on packet loss, you will notice the winding effect disappears.


Step 9: Interpolation

That's a start, but it still leaves a very jittery needle. The problem is that the position of the receiving needle is entirely at the mercy of the messages received. At even 10% packet loss the vast majority of information is still being received, yet because graphically the app depends so much on a smooth and regular flow of messages, any slight discrepancy shows up immediately.

We know how the rotation should look; why not just 'fill in' the missing information to wallpaper over the cracks? We'll start with a class like the following that has two methods, one for updating with the most current rotation, one for reading off the current rotation:

 
public class Msg 
{ 
	public function Write(value:Number, date:Date):void 
	{ 
	} 
		 
	public function Read():Number 
	{	 
	} 
}

Now the process has been 'decoupled'. Every frame we can call the Read() method and update the shape's rotation. As and when new messages come in we can call the Write() method to update the class with the latest information. We'll also adjust the app so that it receives not just the rotation but the time the rotation was sent.

The process of filling in missing values from known ones is called interpolation. Interpolation is a large subject that takes many forms, so we will deal with a subset called Linear Interpolation, or 'Lerping'. Programatically it looks like this:

 
public function Lerp(a:Number, b:Number, x:Number):Number 
{ 
	return a + ((b - a) * x); 
}

A and B are any two values; X is usually a value between zero and one. If X is zero, the method returns A. If X is one, the method returns B. For fractional values between zero and one, the method returns values part way between A and B - so an X value of 0.25 returns a value 25% of the way from A to B.

In other words, if at 1:00pm O've driven 5 miles, and at 2:00pm i've driven 60 miles, then at 1:30pm I've driven Lerp(5, 60, 0.5) miles. As it happens I may have sped up, slowed down, and waited in traffic at various parts of the journey, but the interpolation function can't account for that as it only has two values to work from. Therefore the result is a linear approximation and not an exact answer.

 
//Hold 2 recent values to interpolate from.  
private var valueA:Number = NaN; 
private var valueB:Number = NaN; 
 
//And the instances in time that the values refer to.		 
private var secA:Number = NaN; 
private var secB:Number = NaN; 
		 
public function Write(value:Number, date:Date):void 
{			 
	var secC:Number = date.time / 1000.0; 
	 
	//If the new value is reasonably distant from the last 
	//then set a as b, and b as the new value. 
	if( isNaN(secB) || secC -secB > 0.1) 
	{ 
		valueA = valueB; 
		secA   = secB; 
			 
		valueB = value; 
		secB = secC; 
	} 
} 
		 
public function Read():Number 
{ 
	if( isNaN(valueA) ) return valueB; 
			 
	var secC:Number = new Date().time / 1000.0; 
	 
	var x:Number  = (secC-secA) / (secB-secA); 
			 
	return Lerp(valueA, valueB, x);		 
}

Step 10: So Near and Yet So Far

If you implement the code above you'll notice that it almost works correctly but seems to have some sort of glitch - every time the needle does one rotation it appears to then suddenly snap back in the opposite direction. Did we miss something? The documentation for the rotation property of the DisplayObject class reveals the following:

Indicates the rotation of the DisplayObject instance, in degrees, from its original orientation. Values from 0 to 180 represent clockwise rotation; values from 0 to -180 represent counterclockwise rotation. Values outside this range are added to or subtracted from 360 to obtain a value within the range.

That was naive - we assumed a single number line from which we could pick any two points and interpolate. Instead we're dealing not with a line but with a circle of values. If we go past +180, we wrap around again to -180. That's why the needle was behaving strangely. We still need to interpolate, but we need a form of interpolation that can wrap correctly around a circle.

Imagine looking at two separate images of somebody riding a bike. In the first image the pedals are positioned towards the top of the bike; in the second image the pedals are positioned towards the front of the bike. From just these two images and with no additional knowledge it's not possible to work out whether the rider is pedalling forwards or backwards. The pedals could have advanced a quarter of a circle forwards, or three-quarters of a circle backwards. As it happens, in the app we've built, the needles are always 'pedalling' forwards, but we'd like to code for the general case.

The standard way to resolve this is to assume that the shortest distance around the circle is the correct direction and also hope that updates come in fast enough so that there is less than half a circle's difference between each update. You may have had the experience playing a multiplayer driving game where another player's car has momentarily rotated in a seemingly impossible way - that's the reason why.

 
var min:Number = -180; 
var max:Number = +180; 
 
//We can 'add' or 'subtract' our way around the circle 
//giving two different measures of distance 
var difAdd:Number = (b > a)? b-a : (max-a) + (b-min); 
var difSub:Number = (b < a)? a-b : (a-min) + (max-b);

If 'difAdd' is smaller than 'difSub', we will start at 'a', and add to it a linear interpolation of the amount X. If 'difSub' is the lesser distance, we will start at 'a' and subtract from it a linear interpolation of the amount X. Potentially that might give a value which is out of the 'min' and 'max' range, so we will use some modular arithmetic to get a value which is back in range again. The full set of calculations looks like this:

 
//A function that gives a similar result to the % 
//mod operator, but for float value. 
public function Mod(val:Number, div:Number):Number 
{					 
	return (val - Math.floor(val / div) * div); 
} 
 
//Ensures that values out of the min/max range 
//wrap correctly back in range 
public function Circle(val:Number, min:Number, max:Number):Number 
{ 
	return Mod(val - min, (max-min) ) + min; 
} 
 
//Performs a circular interpolation of A and B by the factor X, 
//wrapping at extremes min/max 
public function CLerp(a:Number, b:Number, x:Number, min:Number, max:Number):Number 
{ 
	var difAdd:Number = (b > a)? b-a : (max-a) + (b-min); 
	var difSub:Number = (b < a)? a-b : (a-min) + (max-b); 
			 
	return (difAdd < difSub)? Circle( a + (difAdd*x), min, max) 
							: Circle( a - (difSub*x), min, max); 
}

If you add this to the code and re-test, you should find the receiver's needle actually looks pretty smooth under a variety of network conditions. The source code attached to this tutorial has several constants which can be changed to re-compile with various combinations of the features we have discussed.

Conclusion

We began by looking at how to create a Cirrus connection and then set up NetStreams between clients. This was wrapped up into a resusable class that we could test with and expand on. We created an application and examined its performance under different networking conditions using a utility, then looked at techniques to improve the experience for the application user. Finally we discovered that we have to apply these techniques with care and with an understanding of what underlying data the app is representing.

I hope this has given you a basic grounding in building real time applications and that you now feel you are equipped to face the issues involved. Good luck!

Advertisement
Related Posts