Colony tech update 1: Creating a game camera, part 4, controlling using the screen edges

Next in our line of camera controllers is controlling using the screen edges. You'll see this sort of behaviour in a lot of PC games, especially 4X games (Medieval, Civilisation, etc).

The input

We'll use the Input class from part 3 to handle what we need from the mouse, which for this, is only the current mouse position on the stage.

The CameraMouseEdgeControls class

For this controller, we need to define a border where our mouse will be considered active. Once the mouse has entered this border zone, we're going to move the camera in that direction, scaled depending on how far we're inside it.

A visual of the mouse edge border zone

In the above example, we scale the camera movement, so that when the mouse is just over the dotted line, we're barely moving, and when we're right up against the screen edge, we're moving at full speed. This sort of linear velocity gives a nice user experience, as sudden changes in the camera are quickly irritating when you're trying to click on something.

Like the CameraKeyControls, we normalise the move vector so that we don't move faster in the diagonals. We also make use of an "active" Signal, for when you're sporting more than one controller.

For this controller, there's no zooming. If you come up with an idea on how to implement it with this, I'm all ears :)

Caveat

Because of the nature of this controller, there are a few things you need to keep in mind:

  • Selecting objects at the edge of the screen
  • GUI/HUD elements

For the first, when the user goes to select an object that's close to the edge of the screen, the camera will move, which might cause them to misclick. Even if they don't, the camera moving can be irritating if they're trying to keep a certain region of the game on-screen. To get around this, you can:

  • Use minimal borders, so that the user really needs to be up against the screen edges before moving (NOTE: this can have problems on mobile, with fat fingers)
  • When we enter the border zone, pause for a bit before moving, so we can see if the player's going to do anything in that region
  • Track the mouse position, and if it's over an element that we can select (unit, building, etc), then cancel the camera movement - though this can have its own problems if you have a lot of selectable objects

For the second, GUI elements are commonly arranged around the edges of the screen, because GUI in the middle of the screen doesn't make for the most amazing of games. If you can select something on your GUI (buttons, gauges, whatever), then when you move to select this element, you'll probably trigger the camera movement, as you'll most likely be inside the bounds. This is usually Not a Good Thing. To fix it:

  • Like above, use minimal borders to limit the problem
  • Track the mouse, and once you're over the GUI, kill any camera movement
  • Have "blocks" of the screen that are registered for the GUI, so any illicit mouse movement in this area is ignored by the controllers.

The code

Again, make sure and add it to your update loop. You can also download it below. Keep scrolling for an example

package
{
	import flash.display.Stage;
	import flash.geom.Point;
	import org.osflash.signals.Signal;
	
	/**
	 * Controls a camera by moving it when the mouse is near the edges
	 * @author Damian Connolly
	 */
	public class CameraMouseEdgeControls 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;
		
		/**
		 * How big the move border is
		 */
		public var border:Number = 20.0;
		
		/*******************************************************************************************/
		
		private var m_hasFiredStartSignal:Boolean 	= false;// have we fired our signalOnStartedMoving signal?
		private var m_input:Input					= null; // the input object that tracks our keys/mouse
		
		/*******************************************************************************************/
		
		/**
		 * Creates a new controller for a camera
		 * @param camera The camera that we're going to control
		 * @param stage The main stage
		 * @param input The input object that tracks our keys/mouse
		 */
		public function CameraMouseEdgeControls( camera:Camera, stage:Stage, input:Input ) 
		{
			super( camera, stage );
			this.m_input				= input;
			this.signalOnStartedMoving 	= new Signal;
		}
		
		/**
		 * Destroys the CameraMouseEdgeControls and clears it for garbage collection
		 */
		override public function destroy():void 
		{
			super.destroy();
			this.signalOnStartedMoving.removeAll();
			this.signalOnStartedMoving 	= null;
			this.m_input				= null;
		}
		
		/**
		 * Called every frame the CameraMouseEdgeControls are active
		 * @param dt The delta time since the last update
		 */
		override public function update( dt:Number ):void
		{
			// get our delta depending on the mouse position
			var mousePos:Point = this.m_input.mousePos;
			
			// horizontal
			if ( mousePos.x <= this.m_camera.x + this.border ) // use equal in case border is 0
				this.m_moveDir.x = ( this.border <= 0.0 || mousePos.x <= this.m_camera.x ) ? -1.0 : -( this.border - ( mousePos.x - this.m_camera.x  ) ) / this.border;
			else if ( mousePos.x >= this.m_camera.x + this.m_camera.width - this.border )
				this.m_moveDir.x = ( this.border <= 0.0 || mousePos.x >= this.m_camera.x + this.m_camera.width ) ? 1.0 : ( this.border - ( this.m_camera.x + this.m_camera.width - mousePos.x ) ) / this.border;
			else
				this.m_moveDir.x = 0.0;
				
			// vertical
			if ( mousePos.y <= this.m_camera.x + this.border )
				this.m_moveDir.y = ( this.border <= 0.0 || mousePos.y <= this.m_camera.y ) ? -1.0 : -( this.border - ( mousePos.y - this.m_camera.y ) ) / this.border;
			else if ( mousePos.y >= this.m_camera.x + this.m_camera.height - this.border )
				this.m_moveDir.y = ( this.border <= 0.0 || mousePos.y >= this.m_camera.y + this.m_camera.height ) ? 1.0 : ( this.border - ( this.m_camera.y + this.m_camera.height - mousePos.y ) ) / this.border;
			else
				this.m_moveDir.y = 0.0;
				
			// if there's no change, do nothing
			if ( this.m_moveDir.x != 0.0 || this.m_moveDir.y != 0.0 )
			{
				// normalise our dir if we're moving diagonally, otherwise we'll move quicker
				if ( this.m_moveDir.x != 0.0 && this.m_moveDir.y != 0.0 )
					this.m_moveDir.normalize( 1.0 );
					
				// check if we should compensate for zoom
				var zoomComp:Number	= ( this.shouldMoveCompenstateForZoom ) ? 1.0 / this.m_camera.zoom : 1.0;
				if ( zoomComp != 1.0 )
				{
					this.m_moveDir.x *= zoomComp;
					this.m_moveDir.y *= zoomComp;
				}
				
				// either move directly, or with velocity
				if( this.shouldMoveWithVelocity )
					this.m_camera.setMoveVelocityBy( this.m_moveDir.x * this.moveSpeed * dt, this.m_moveDir.y * this.moveSpeed * dt );
				else
					this.m_camera.moveCameraBy( this.m_moveDir.x * this.moveSpeed * dt, this.m_moveDir.y * this.moveSpeed * dt );
				
				// dispatch our signal if we haven't already
				if ( !this.m_hasFiredStartSignal )
				{
					this.signalOnStartedMoving.dispatch();
					this.m_hasFiredStartSignal = true;
				}
			}
			else
				this.m_hasFiredStartSignal = false;
		}
		
	}

}

Example

Here it is in action! Click on the stage to give it focus, and any time after that to reset the camera to the center.

An example of controlling the camera using the screen edges in action

Colony mailing list

Mailing list mailing list mailing list.

* indicates required

Share: