Colony tech update 1: Creating a game camera, part 6, following objects

Probably one of the more interesting controllers; controlling the camera by following objects. Or creeping, as we like to call it.

In its most simple form, we store an object that we want to follow on the controller, then every frame, update the camera's position to match it. As an added bonus, to make the camera jump to another object's position, you only need to change the follow object.

While technically we could stop there, to make things more visually interesting, and to make it look as if we're good at our jobs, we want to animate the camera to its final position.

If the object that you're following is static, you could just simply tween the camera there, but then again, if you're following static objects, that's just a waste of good code. As our objects will be moving, we can't really use a tween, as the final position will be changing continuously. Thus we use our friends, lerp() or damp().

Lerping

lerp(), not to be confused with herp() or derp(), stands for linear interpolation; simply going from one value to another. In graph form, it looks a little like this:

An example of lerp, or linear interpolation

In code form, it's equally simple:

public static function lerp( start:Number, end:Number, factor:Number ):Number
{
	return end * factor + ( 1 - factor ) * start;
}

factor here is a Number between 0.0 and 1.0. If it's 0.0, then we stick to the start value, while 1.0 will return the end.

Damping

damp(), on the other hand, is a little more complex, but gives better results visually. It lets us smoothly arrive at our destination point, slowing as we get nearer. It stands for dat's almost my point, and looks a bit like this in graph form:

An example of damp

Code wise, it shapes up like this:

public static function damp( source:Number, dest:Number, dt:Number, factor:Number, precision:Number ):Number
{
	if ( Math.abs( source - dest ) < precision )
		return source;
	return ( ( source * factor ) + ( dest * dt ) ) / ( factor + dt );
}

dt is normally the delta time of your game, but you can modify it to get a better curve (you can play with MainLerpDamp.as to see the effects of different values). factor is similar as in lerp(), though in the opposite direction; 0.0 is the end, while 1.0 is the start (read the graph from right to left to make sense of it). You're not strictly limited to a max of 1.0; again you can play with the values to see the results.

Offsetting

Another trick we can pull with the follow camera, is applying an offset. For this code sample, I've implemented 3 forms of offset:

  • Basic, or static offset
  • An offset modified by the object's rotation
  • An offset modified by the object's position delta (or how much it's moving)

Which one you want to use depends on what your camera is following. If you're doing a top-down shooter where you control a soldier, then you probably want the offset to be based on his rotation - that way you have the most optimum view of the arena in front of him, even when he's moving backwards. If you're doing a side-scroller, or a space game (cough), then you probably want it to be based on your object's change in position; i.e. the faster you move, the more the camera is offset in the direction you're travelling, giving you a better vision of where you're going.

P.S.

The other controllers make use of an "active" Signal so we know when they're active or not. As CameraFollowControls is active as long as it has an object to follow, if you're going to use it with another controller, you can either set the follow object to null, or set active to false when you detect another controller activating.

The code

You can download it below, along with the other classes. Keep scrolling for a live example.

package
{
	import flash.display.DisplayObject;
	import flash.display.Stage;
	import flash.geom.Point;
	
	/**
	 * Controls a camera by following an object
	 * @author Damian Connolly
	 */
	public class CameraFollowControls extends CameraControls 
	{
		
		/*******************************************************************************************/
		
		/**
		 * The ease type to use when following our object
		 */
		public var followEaseType:CameraFollowEaseType = CameraFollowEaseType.NONE;
		
		/**
		 * The offset type to use when applying our follow offset
		 */
		public var followOffsetType:CameraFollowOffsetType = CameraFollowOffsetType.NONE;
		
		/**
		 * The follow factor to add some smoothing between the camera position and the target
		 * position. If we're lerping, then from 0.0 to 1.0 is loose to tight. If damping, then 
		 * 0.0 to 1.0 is tight to loose. Generally this number is between 0.0 and 1.0, but with
		 * damping, you can experiment with other values
		 */
		public var followFactor:Number = 0.5;
		
		/*******************************************************************************************/
		
		private var m_followObj:DisplayObject	= null;	// the object that we're following
		private var m_offset:Point 				= null; // the offset point for where we're following
		private var m_rotatedOffset:Point		= null;	// the rotated offset point, if we're using rotation
		private var m_prevCameraPos:Point		= null;	// the previous camera position
		private var m_prevObjPos:Point			= null;	// the previous follow object position
		
		/*******************************************************************************************/
		
		/**
		 * The object that we're following
		 */
		[Inline] public final function get followObj():DisplayObject { return this.m_followObj; }
		[Inline] public final function set followObj( d:DisplayObject ):void
		{
			this.m_followObj = d;
			if ( this.m_followObj != null )
				this.m_prevObjPos.setTo( this.m_followObj.x, this.m_followObj.y );
		}
		
		/*******************************************************************************************/
		
		/**
		 * Creates a new CameraFollowControls object
		 * @param camera The camera that we're controlling
		 * @param stage The main stage
		 * @param followObj The object that we're following
		 */
		public function CameraFollowControls( camera:Camera, stage:Stage, followObj:DisplayObject = null ) 
		{
			super( camera, stage );
			this.m_offset			= new Point; // anchor point
			this.m_rotatedOffset	= new Point; // if we're using rotation
			this.m_prevCameraPos	= new Point;
			this.m_prevObjPos		= new Point;
			this.followObj 			= followObj; // NOTE: use the setter
		}
		
		/**
		 * Destroys the CameraFollowControls and clears it for garbage collection
		 */
		override public function destroy():void 
		{
			super.destroy();
			this.followEaseType		= null;
			this.followOffsetType	= null;
			this.m_followObj		= null;
			this.m_offset			= null;
			this.m_rotatedOffset	= null;
			this.m_prevCameraPos	= null;
			this.m_prevObjPos		= null;
		}
		
		/**
		 * Sets the offset point, or the offset that we'll apply when following our object
		 * @param ox The offset x position
		 * @param oy The offset y position
		 * @param offsetType The CameraFollowOffsetType to use when applying this. If null, then
		 * CameraFollowOffsetType.NONE is used
		 */
		public function setOffsetPoint( ox:Number, oy:Number, offsetType:CameraFollowOffsetType = null ):void
		{
			this.m_offset.setTo( ox, oy );
			this.followOffsetType = ( offsetType == null ) ? CameraFollowOffsetType.NONE : offsetType;
		}
		
		/**
		 * Updates the CameraFollowControls every frame it's active
		 * @param dt The delta time since the last call to update
		 */
		override public function update( dt:Number ):void 
		{
			// if we don't have a follow object, do nothing
			if ( this.m_followObj == null )
				return;
				
			// get our camera position based on our object and offset
			// NOTE: we're not using global position as the camera will probably move the layer that the object
			// is on, so it'll get screwed up
			var tx:Number = this.m_followObj.x;
			var ty:Number = this.m_followObj.y;
			
			// apply our offset type
			if ( this.followOffsetType == CameraFollowOffsetType.NONE )
			{
				// just add the offset
				tx += this.m_offset.x;
				ty += this.m_offset.y;
			}
			else if ( this.followOffsetType == CameraFollowOffsetType.OBJ_ROTATION )
			{
				// our offset is based on our object's rotation
				MathHelper.rotate( this.m_offset, MathHelper.degreesToRadians( this.m_followObj.rotation ), this.m_rotatedOffset );
				tx += this.m_rotatedOffset.x;
				ty += this.m_rotatedOffset.y;
			}
			else // CameraFollowOffsetType.OBJ_POS_DELTA
			{
				// rotate our offset 
				var radians:Number = MathHelper.radianAngle( this.followObj.x - this.m_prevObjPos.x, this.followObj.y - this.m_prevObjPos.y );
				MathHelper.rotate( this.m_offset, radians, this.m_rotatedOffset );
				
				// multiply our offset by our delta position
				var posDelta:Number = MathHelper.dist( this.m_prevObjPos.x, this.m_prevObjPos.y, this.followObj.x, this.followObj.y );
				tx 					+= this.m_rotatedOffset.x * posDelta;
				ty 					+= this.m_rotatedOffset.y * posDelta;
			}
			
			// interpolate to the position. NOTE: CameraFollowEaseType.NONE is already taken
			// care of 
			if ( this.followEaseType == CameraFollowEaseType.DAMP )
			{
				tx = MathHelper.damp( this.m_camera.cameraX, tx, dt, this.followFactor, 1.0 );
				ty = MathHelper.damp( this.m_camera.cameraY, ty, dt, this.followFactor, 1.0 );
			}
			else if ( this.followEaseType == CameraFollowEaseType.LERP )
			{
				tx = MathHelper.lerp( this.m_camera.cameraX, tx, this.followFactor );
				ty = MathHelper.lerp( this.m_camera.cameraY, ty, this.followFactor );
			}
			
			// move the camera (only if we've moved)
			if ( this.m_prevCameraPos.x != tx || this.m_prevCameraPos.y != ty )
			{
				this.m_camera.moveCameraTo( tx, ty );
				this.m_prevCameraPos.setTo( tx, ty );
			}
			
			// update our previous object position
			this.m_prevObjPos.setTo( this.m_followObj.x, this.m_followObj.y );
		}
		
	}

}

Example

The code in action; click on the stage to give it focus, then click and hold the mouse to move the ship. The GUI on the top left will let you switch between the different modes of following an object, as well as the different offset types.

An example of controlling the camera by following an object

Colony mailing list

Plug for the Colony mailing list:

* indicates required

Share: