Colony tech update 1: Creating a game camera, part 5, controlling using the mouse

Now we're getting into the more interesting controllers! In part 5 of this series, I'm going to show you how to control the camera using the mouse; both by dragging and flinging the camera around. As an added bonus, this controller class should work for touch, on mobile.

Support classes

The Input class from part 3 shows its usefulness again, to give us the current mouse position and whether the mouse button is clicked (so we know when we're dragging).

Also, making its debut appearance in this series, is the MathHelper class, but I'll introduce that in the next update, as here we only use a super simple dist() call.

Dragging

The CameraMouseControls class is broken up into two distinct behaviours: dragging and flinging. For dragging the camera, we store the mouse position from the previous frame. The difference in movement between then and now, gives us our move direction. When dragging, we always compenstate for zoom (controlled using the shouldMoveCompenstateForZoom property), as otherwise, the camera won't follow the exact position of the mouse when our zoom isn't 1.0. Basically if you mouse down on an object, and move the camera randomly, when you mouse up, you should still be over that object.

Aside from keeping that in mind, dragging's pretty easy. We ignore moveSpeed etc, as we want to keep a 1:1 ratio with the mouse movement.

Flinging

The second main behaviour is flinging, which is activated if the shouldMoveWithVelocity boolean is true. The first thing we do when we receive a MOUSE_UP event, is check if the current position minus the starting position is greater than the CameraControls.CLICK_DIST constant. This stops the camera from making jerky movements when we click on an object, and also works nicely with the click controller, which we'll see in a future update.

If we don't consider it a click, then we take the most recent movement delta (the difference in movement between this frame and the previous one), to get our move direction. We multiply this by our moveSpeed and delta time, set the camera velocity, and wave our hands as the magic happens.

But what about scaled movement?

The idea behind this is that the larger your move delta, the larger your end camera velocity should be. This is where the concept of the averageDragLength property comes in. This is set to whatever you think your normal drag length is, in one frame, moving at a normal speed. It's a completely arbitrary number, and is essentially based on trial and error. When in doubt, just pick a random number and see what happens ;)

We use the averageDragLength to scale our move direction, to get a nice end velocity. For example, if your averageDragLength is 10.0 and your current move distance is 5.0, then you've moved half of what's considered "normal". Thus, the camera velocity should be half of what it normally is.

Yada yada

Zooming is controlled by the mouse wheel; we use the direction rather than the actual delta value, to have more control over the zoom velocity, and zoom the camera accordingly. On mobile, you should probably use multitouch, as it's hard to scroll a mouse wheel when you don't have one.

As with the other controllers, we make use of a "active" Signal so it plays nicely with the others.

The code

You can download it below, and keep scrolling for a live example.

package
{
	import flash.display.Stage;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import org.osflash.signals.Signal;
	
	/**
	 * Camera controls using the mouse - drag the camera around etc
	 * @author Damian Connolly
	 */
	public class CameraMouseControls extends CameraControls
	{
		
		/*******************************************************************************************/
		
		/**
		 * The signal dispatched when we start moving the camera with the mouse. It should take no
		 * parameters. This is useful if you're using these controls with other ones, such as
		 * CameraFollowControls, where you can disable those while you're dragging
		 */
		public var signalOnStartedMoving:Signal = null;
		
		/**
		 * The average drag length - this will be used when dragging by velocity - the faster we
		 * move, then more our moveSpeed is multiplied by
		 */
		public var averageDragLength:Number = 10.0;
		
		/*******************************************************************************************/
		
		private var m_input:Input					= null;	// the input object that tracks our keys/mouse
		private var m_startMousePos:Point			= null;	// the starting mouse position, when dragging
		private var m_prevMousePos:Point			= null; // the previous mouse position
		private var m_prevMouseWasPressed:Boolean	= false;// was the mouse pressed in the previous frame?
		private var m_hasFiredStartSignal:Boolean	= false;// have we fired our signalOnStartedMoving signal?
		private var m_zoomDir:int					= 0;	// our zoom direction (mouse wheel)
		
		/*******************************************************************************************/
		
		/**
		 * Creates new CameraMouseControls
		 * @param camera The camera that we're controlling
		 * @param stage The main stage
		 * @param input The input object that tracks our keys/mouse
		 */
		public function CameraMouseControls( camera:Camera, stage:Stage, input:Input ) 
		{
			super( camera, stage );
			this.m_input				= input;
			this.signalOnStartedMoving	= new Signal;
			this.m_startMousePos		= new Point;
			this.m_prevMousePos 		= new Point;
			
			// add our zoom listener
			this.m_stage.addEventListener( MouseEvent.MOUSE_WHEEL, this._onMouseWheel );
		}
		
		/**
		 * Destroys the CameraMouseControls and clears them for garbage collection
		 */
		override public function destroy():void 
		{
			super.destroy();
			this.signalOnStartedMoving.removeAll();
			this.signalOnStartedMoving	= null;
			this.m_input				= null;
			this.m_startMousePos		= null;
			this.m_prevMousePos 		= null;
			
			// remove our zoom listener
			this.m_stage.removeEventListener( MouseEvent.MOUSE_WHEEL, this._onMouseWheel );
		}
		
		/**
		 * Updates the CameraMouseControls every frame it's active
		 * @param dt The delta time
		 */
		override public function update( dt:Number ):void 
		{
			// we only update the move if the mouse is pressed
			var isPressed:Boolean	= this.m_input.isMousePressed;
			var currMousePos:Point	= this.m_input.mousePos;
			if ( !isPressed && !this.m_prevMouseWasPressed )
			{
				// keep track of the mouse
				this.m_prevMousePos.copyFrom( currMousePos );
				this.m_hasFiredStartSignal = false;
			}
			else
			{
				// if we weren't pressed in the previous frame, then store our starting position
				if ( isPressed && !this.m_prevMouseWasPressed )
					this.m_startMousePos.copyFrom( currMousePos );
					
				// get our delta movement, and our zoom multiplier if we're compensating for zoom
				var zoomComp:Number	= ( this.shouldMoveCompenstateForZoom ) ? 1.0 / this.m_camera.zoom : 1.0;
				this.m_moveDir.x 	= ( this.m_prevMousePos.x - currMousePos.x );
				this.m_moveDir.y 	= ( this.m_prevMousePos.y - currMousePos.y );
					
				// if we're pressed (or not moving with velocity), then we're currently dragging the camera around,
				// so just move by our moveDir (non-normalised, so it's the delta movement).
				// if we're not pressed, then we've just let go of the mouse, so if the distance is good (i.e. it's
				// not a click), and we're moving with velocity, set the camera off
				var wasClick:Boolean = MathHelper.dist( currMousePos.x, currMousePos.y, this.m_startMousePos.x, this.m_startMousePos.y ) <= CameraControls.CLICK_DIST;
				if ( !wasClick && ( isPressed || !this.shouldMoveWithVelocity ) )
				{
					// NOTE: we always compensate for zoom when dragging, otherwise the camera doesn't move right under our mouse
					if( !this.shouldMoveCompenstateForZoom && isPressed )
						zoomComp = 1.0 / this.m_camera.zoom;
					this.m_camera.moveCameraBy( this.m_moveDir.x * zoomComp, this.m_moveDir.y * zoomComp );
				}
				else if( !wasClick && !isPressed && this.shouldMoveWithVelocity ) // only move with velocity if we've moved far enough (i.e. this is not a click)
				{
					// if we should compensate for zoom, update our move dir
					if ( this.shouldMoveCompenstateForZoom )
					{
						this.m_moveDir.x *= zoomComp;
						this.m_moveDir.y *= zoomComp;
					}
					
					// get the multiplier for our move dir (based on our average drag length) - the faster we move, 
					// the more we multiply our move speed by
					var mult:int = int( ( this.m_moveDir.length / this.averageDragLength ) * zoomComp + 0.5 ); // fast round
					if ( mult == 0 )
						mult = 1;
						
					// normalise our move dir to our multiplier and set our velocity
					this.m_moveDir.normalize( mult );
					this.m_camera.setMoveVelocityBy( this.m_moveDir.x * this.moveSpeed * dt, this.m_moveDir.y * this.moveSpeed * dt );
				}
				
				// update the previous mouse position
				this.m_prevMousePos.copyFrom( currMousePos );
				this.m_prevMouseWasPressed = isPressed;
				
				// dispatch our signal if we haven't already (and it wasn't a click)
				if ( !this.m_hasFiredStartSignal && !wasClick )
				{
					this.signalOnStartedMoving.dispatch();
					this.m_hasFiredStartSignal = true;
				}
			}
			
			// if we're not zooming, just return
			if ( this.m_zoomDir == 0 )
				return;
				
			// set if we're zooming with velocity or just normally (logarithmically so we get a smooth scale)
			if( this.shouldZoomWithVelocity )
				this.m_camera.setZoomVelocityBy( this.m_zoomDir * this.zoomSpeed * dt );
			else
				this.m_camera.zoomCameraLogarithmicallyBy( this.m_zoomDir * this.zoomSpeed * dt );
			
			// clear our zoom dir
			this.m_zoomDir = 0;
		}
		
		/*******************************************************************************************/
		
		// called when we're moving the mouse wheel
		private function _onMouseWheel( e:MouseEvent ):void
		{
			this.m_zoomDir = ( e.delta > 0 ) ? 1 : -1;
		}
		
	}

}

Example

The code in action; click on the stage to give it focus.

An example of controlling the camera using the mouse in action

Colony mailing list

Obligatory-mailing-list-for-Colony plug.

* indicates required

Share: