This tutorial will demonstrate a multipurpose Radial Menu Class using Actionscript 3. It was primarily designed to be used in Adobe Flash, however several game engines have been implementing Actionscript controlled user interface modules. This tutorial is intended for people at least somewhat familiar with Actionscript 3, and uses Adobe Flash Libraries as well as the project file at the bottom requiring Flash CS6 to open.

What is a radial menu?

Below is a very simple implementation of the class as a flash app.
It displays a mouse over target that will display a circle of buttons, that ‘explode’ into a new mouse target and circle of buttons.
It uses a random size circle and a random number of buttons, to demonstrate how the radial menu class can handle many different situations.

[swf src=”https://kalverda.com/wp-content/uploads/2013/09/RadialMenuExample.swf” width=”400″ height=”550″ params=’wpmode=direct&movie=”file.swf’]Please Install Flash or use a browser/device capable of displaying embedded Flash, To view the embedded demo.
Save Embedded flash (right click and save)

[/swf]

This is a good example of how not to use radial menus, a good radial menu is only visible when needed. A radial menu gives context sensitive options for what element the mouse is currently over. A radial menu is a good way to display choices for the user, associated with an object on the screen, when there are different objects on the screen each with it’s own set of actions.

A good way to use the radial menu class, would be in a card game:

  • You have a radial menu displaying 1 to 6 cards as a semicircle at the bottom of the screen.
  • The mouse over function of a card raises it slightly, letting the user know it’s been selected.
  • Once a card is raised it spawns another radial menu, that contains 3 buttons.
  • A button to play the card face up, a button for face down and a button to discard the card and draw a 2 new ones.

Because each card has it’s own menu and animates to indicate the game knows which card is selected, it is a natural and user friendly way for players to select actions related to a card. Rather than have a chose a card state, select an action state and the need to go back and forth if the player changes their mind.

The scripts

Lets start with the big one, the radial menu class itself. What this class does is both very simple and very complicated.

Simply:
It takes a list of MovieClips that extend RadialMenuItem, places them on the stage and moves them around to form a neat circle.

Complicated:
It needs to calculate angles and distances of where the to place the radial menu items, supporting full circles, half circles, third circles and several other configurations. It performs scaling based on the size of the hitbox area, so the menu items won’t overlap. It also redirects mouse events to ensure mouse over and mouse out events are detected properly by both the buttons and the hitbox that triggers the menu.

Radial Menu Class

<xmp>package com.gag.games
{
	import com.gag.games.RadialMenuItem;
	import flash.events.*;
	import flash.display.DisplayObjectContainer;
	import flash.events.EventDispatcher;
	import fl.transitions.Tween;
	import fl.transitions.easing.*;
	import fl.transitions.TweenEvent;
	import flash.display.MovieClip;
	import flash.display.Stage;
	import flash.utils.Timer;
 
	public class RadialMenu extends EventDispatcher
	{
		public static var COLLAPSING:int = 1;
		public static var STATIC:int = 2;
		public static var INSTANT:int = 3;
		public static var STATIC_START_HIDDEN:int = 4;
 
		public static var FULLCIRCLE:int = 1;
		public static var HALFTOP:int = 2;
		public static var LINE3ELEMENTS:int = 3;
		public static var LINE4ELEMENTS:int = 4;
		public static var LINE5ELEMENTS:int = 5;
		public static var LINE6ELEMENTS:int = 6;
		public static var HALFBOTTOM:int = 7;
		public static var FIRSTQUARTER:int = 8;
		public static var SECONDQUARTER:int = 9;
		public static var THIRDBOTTOM:int = 10;
 
		private var objects:Array;
		private var directcopyfordebugoutput:Array;
 
		private var display:DisplayObjectContainer;
		private var centerX:Number;
		private var centerY:Number;
		private var type:int;
		private var the_stage:DisplayObjectContainer;
 
		private var closetimer:Timer = new Timer(500,1);
		private var ismouseon: Boolean = false;
 
		public var canOpen : Boolean = true;
		public var stayOpen : Boolean = false;
 
		var convertToRadials = Math.PI / 180;
 
		/**
		* RadialMenu is a manager for an object that is to display a circle or semi-circle of 
		* buttons or button like movie clips.
		*
		* Use the following syntax to create a RadialMenu
		* var menu = new RadialMenu(	<array with="" radialmenuitems="">, <object menu="" items="" are="" to="" appear="" over="">, 
		*								<center position="" x="" adjustment="">, <center position="" y="" adjustment="">, 
		*								RadialMenu.FULLCIRCLE, true, RadialMenu.COLLAPSING, 
		*								<parent to="" add="" the="" menu="" items="">, false, 1);
		*
		* <array with="" radialmenuitems="">
		* Usually a list of buttons, but you can implement non-clickable labels if you wanted.
		* See RadialMenuItem for info on what kind of buttons should be used.
		*
		* <object menu="" items="" are="" to="" appear="" over="">
		* The object the buttons appear over is expected to be a hitbox over which the buttons will appear.
		* It is advised to use a hit circle instead of an actual hitbox because that will make it more obvious 
		* where the buttons would appear on the screen.
		* However any container with 4 equal sides works, long as you ensure there are no holes between 
		* the center and the buttons.
		*
		* <center position="" x="" adjustment="">, <center position="" y="" adjustment="">
		* It might be you don't want to add the buttons to the same parent as the hitbox.
		* If you do so you need to adjust the center position for the menu to line up with the center of the hitbox.
		* 
		* The circle type, is to be one of the static circletypes
		* RadialMenu.FULLCIRCLE buttons appear in a circle around the edge of the display object.
		* RadialMenu.HALFTOP buttons appear in a semi-circle around the top of the display.
		* RadialMenu.HALFBOTTOM buttons appear in a semi-circle around the bottom of the display.
		*
		* The topleftAncor boolean declares whether or not the 0,0 point of the hitbox is in the topleft pixel, or in it's center.
		* This is used to determine where the center of the hitbox is relative the the 0,0 point of the parent for the buttons.
		*
		* Then there is the display type, use the static variables in this class to select which should be used.
		* RadialMenu.COLLAPSING causes the buttons to (dis)appear and be tweened to and from their evental positions 
		* on mouse over of the hitbox.
		* RadialMenu.INSTANT causes the buttons to instantly appear or disappear on mouse over or mouse out of the hitbox.
		* RadialMenu.STATIC causes the buttons to always remain visible.
		*
		* The angle boolean allows the menu to rotate the buttons to all be pointing towards the center.
		*
		* And finally the element scale can be used to adjust the effects of automatic scaling on the buttons.
		* If the buttons should be larger to have less empty space around their hitbox set this to a value between 0.1 and 1.
		* If the buttons should be smaller to have extra empty space around their hitbox set this to a value above 1.
		* To turn off autmatic scaling set this to a value to 0.
		*/
		public function RadialMenu(placethese:Array, withHitBox:DisplayObjectContainer, 
								   xonstage : Number, yonstage : Number, 
								   circle_type:int, topleftAncor:Boolean, menu_type:int, 
								   my_stage : DisplayObjectContainer, angle:Boolean, elementscale : Number)
		{
			directcopyfordebugoutput = placethese;
			objects = placethese.slice(); // slice with no parameters returns a copy of the array's contents.
			// Using a copy ensures that we do not end up adding the formatting objects to the original array.
			display = withHitBox;
			type = menu_type;
			the_stage = my_stage;
 
			if (objects.length == 0)
			{
				return;
			}
 
			// Work out the size of the hitbox
			var distancefromcentre:Number;
 
			if (topleftAncor == true)
			{
				centerX = xonstage + display.x + display.width / 2;
				centerY = yonstage + display.y + display.height / 2;
			}
			else
			{
				centerX = xonstage + display.x;
				centerY = yonstage + display.y;
			}
			distancefromcentre = (display.width / 2);
 
			// calculate object orentation and size
			var degreesperstep:Number = calculateAngle(circle_type);
			var smallestscale:Number = calculateSmalestScale( elementscale, distancefromcentre, degreesperstep );
 
			// Now for the actual placement and initialisation of the objects.
			for (var i:int = 0; i < objects.length; i++)
			{
				if(circle_type == LINE3ELEMENTS || circle_type == LINE4ELEMENTS || circle_type == LINE5ELEMENTS || circle_type == LINE6ELEMENTS)
				{
					var relativeElements = circle_type / 2;
					var roomPerElement = distancefromcentre / relativeElements;
					var halfI : int = Math.floor(i / 2.0);
					if(i % 2 == 0)
					{
						objects[i].destinationX = centerX + (roomPerElement * halfI);
					}
					else
					{
						halfI += 1;
						objects[i].destinationX = centerX - (roomPerElement * halfI);
					}
					if(objects.length > circle_type)
					{
						objects[i].destinationX += roomPerElement * 0.5;
					}
					objects[i].destinationY = centerY;
				}
				else
				{
					var degrees:Number = i * degreesperstep;
					var radials:Number = degrees * convertToRadials;
					objects[i].destinationX = centerX + (Math.cos(radials) * distancefromcentre);
					objects[i].destinationY = centerY + (Math.sin(radials) * distancefromcentre);
				}
 
				objects[i].x = centerX;
				objects[i].y = centerY;
				objects[i].scaleX = smallestscale;
				objects[i].scaleY = smallestscale;
				objects[i].visible = false;
				objects[i].mouseEnabled = true;
				objects[i].mouseChildren = true;
				if(angle == true)
				{
					var AmountToRotate : Number = degrees - 90;
					objects[i].rotation = AmountToRotate;
				}
				the_stage.addChild(objects[i]);
				objects[i].addEventListener(MouseEvent.CLICK,passOnMouseEvent);
				objects[i].addEventListener(MouseEvent.MOUSE_OUT,passOnMouseEvent);
				objects[i].addEventListener(MouseEvent.MOUSE_OVER,passOnMouseEvent);
			}
 
			// And then we ensure the hitbox is setup with mouse listeners for the relavant events.
			display.mouseEnabled = true;
			display.mouseChildren = true;
 
			switch (type)
			{
				case RadialMenu.COLLAPSING :
				case RadialMenu.INSTANT :
					display.addEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
					break;
				case RadialMenu.STATIC :
					displayMenu(null);
					break;
			}
		}
 
		public function displayMouseIn(e:Event)
		{
			if(canOpen == false)
			{
				return;
			}
			// trace("Mouse in " + ismouseon);
			if(ismouseon == false)
			{
				ismouseon = true;
				closetimer.removeEventListener("timer", startHideTimer);
				closetimer.removeEventListener("timer", collapseMenu);
				closetimer.stop();
				switch (type)
				{
					case RadialMenu.COLLAPSING :
						// trace("Going to unfold");
						unfoldMenu(e);
						break;
					case RadialMenu.INSTANT :
						// trace("Going to display");
						displayMenu(e);
						break;
				}
			}
			if(display.hasEventListener(MouseEvent.MOUSE_OUT) == false && type != RadialMenu.STATIC)
			{
				display.addEventListener(MouseEvent.MOUSE_OUT, displayMouseOut);
			}
		}
 
		public function displayMouseOut(e:Event)
		{
			if( stayOpen == true )
			{
				return;
			}
			// trace("Mouse out " + ismouseon);
			if(ismouseon == true)
			{
				ismouseon = false;
				switch (type)
				{
					case RadialMenu.COLLAPSING :
						startCollapseTimer(e);
						break;
					case RadialMenu.INSTANT :
						startHideTimer(e);
						break;
				}
			}
			if(display.hasEventListener(MouseEvent.MOUSE_OVER) == false && type != RadialMenu.STATIC)
			{
				display.addEventListener(MouseEvent.MOUSE_OVER, displayMouseIn);
			}
		}
 
		public function displayMenu(e:Event)
		{
			display.removeEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
			if(display.hasEventListener(MouseEvent.MOUSE_OUT) == false && type != RadialMenu.STATIC)
			{
				display.addEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
			}
 
			if(objects.length > 0)
			{
				if (objects[objects.length - 1].tweenXHandler != undefined)
				{
					objects[objects.length - 1].tweenXHandler.removeEventListener(TweenEvent.MOTION_FINISH,hideMenu);
				}
				for each (var object:RadialMenuItem in objects)
				{
					object.visible = true;
					object.mouseEnabled = true;
					object.x = object.destinationX;
					object.y = object.destinationY;
				}
			}
			else
			{
				trace("WARNING: A display menu containing no objects was found [" + objects + "] from [" + directcopyfordebugoutput + "]");
			}
			this.dispatchEvent(new Event("open"));
		}
 
		public function unfoldMenu(e:Event)
		{
			if(objects.length > 0)
			{
				display.removeEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
				if(display.hasEventListener(MouseEvent.MOUSE_OUT) == false)
				{
					display.addEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
				}
 
				if (objects[objects.length - 1].tweenXHandler != undefined)
				{
					objects[objects.length - 1].tweenXHandler.removeEventListener(TweenEvent.MOTION_FINISH,hideMenu);
				}
 
				for each (var object:RadialMenuItem in objects)
				{
					object.visible = true;
					object.mouseEnabled = true;
					object.tweenXHandler = new Tween(object,"x",Strong.easeOut,object.x,object.destinationX,10,false);
					object.tweenYHandler = new Tween(object,"y",Strong.easeOut,object.y,object.destinationY,10,false);
				}
				dispatchEvent(new Event("open"));
			}
			else
			{
				trace("WARNING: A unfold menu containing no objects was found [" + objects + "] from [" + directcopyfordebugoutput + "]");
				display.removeEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
				display.removeEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
			}
		}
 
		public function collapseMenu(e:Event)
		{
			if( stayOpen == true )
			{
				return;
			}
 
			closetimer.removeEventListener("timer", collapseMenu);
			closetimer.stop();
			if(display.hasEventListener(MouseEvent.MOUSE_OVER) == false)
				{
				display.addEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
				}
 
			if(objects.length > 0)
				{
				for each (var object:RadialMenuItem in objects)
					{
					object.tweenXHandler = new Tween(object,"x",Strong.easeOut,object.x,centerX,10,false);
					object.tweenYHandler = new Tween(object,"y",Strong.easeOut,object.y,centerY,10,false);
					}
				objects[objects.length - 1].tweenXHandler.addEventListener(TweenEvent.MOTION_FINISH,hideMenu);
				// trace("collapsing menu " + objects);
				}
			else
				{
				trace("WARNING: A collapse menu containing no objects was found [" + objects + "] from [" + directcopyfordebugoutput + "]");
				display.removeEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
				display.removeEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
				}
		}
 
		public function startCollapseTimer(e : Event)
		{
			if(ismouseon == false)
			{
				display.removeEventListener(MouseEvent.MOUSE_OUT, displayMouseOut);
 
				closetimer = new Timer(500,1);
				if(closetimer.hasEventListener("timer") == false)
				{
					closetimer.addEventListener("timer", collapseMenu);
				}
				closetimer.reset();
				closetimer.start();
			}
		}
 
		public function startHideTimer(e : Event)
		{
			if(ismouseon == false)
			{
				display.removeEventListener(MouseEvent.MOUSE_OUT, displayMouseOut);
 
				closetimer = new Timer(500,1);
				if(closetimer.hasEventListener("timer") == false)
				{
					closetimer.addEventListener("timer", hideMenu);
				}
				closetimer.reset();
				closetimer.start();
			}
		}
 
 
		public function hideMenu(e:Event)
		{
			display.removeEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
			closetimer.removeEventListener("timer", startHideTimer);
			closetimer.stop();
			if(display.hasEventListener(MouseEvent.MOUSE_OVER) == false)
			{
				display.addEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
			}
 
			if (objects[objects.length - 1].tweenXHandler != undefined)
			{
				objects[objects.length - 1].tweenXHandler.removeEventListener(TweenEvent.MOTION_FINISH,hideMenu);
			}
			for each (var object:RadialMenuItem in objects)
			{
				object.visible = false;
			}
			// trace("menu hidden " + objects);
			dispatchEvent(new Event("close"));
		}
 
		/**
		* This method echos any mouse events recieved by the menu items, back to the hitbox.
		* That way even though the menu items are not children of the hitbox, 
		* they will act as though they are for the purpose of mouse over and mouse out.
		*/
		private function passOnMouseEvent(e:MouseEvent)
		{
			// Remove the event listener as the child will report the mouse event back to the parent.
			// Which would an infinite loop of events being reported between the two.
			e.currentTarget.removeEventListener(MouseEvent.CLICK,passOnMouseEvent);
			e.currentTarget.removeEventListener(MouseEvent.MOUSE_OUT,passOnMouseEvent);
			e.currentTarget.removeEventListener(MouseEvent.MOUSE_OVER,passOnMouseEvent);
 
			if(e.type == MouseEvent.MOUSE_OVER)
			{
				ismouseon = true;
			}
			// dispatch the event to the child
			display.dispatchEvent(e);
 
			if(e.type == MouseEvent.MOUSE_OUT)
				{
				// ismouseon = false;
				if(display.hasEventListener(MouseEvent.MOUSE_OVER) == false && type != RadialMenu.STATIC)
					{
					display.addEventListener(MouseEvent.MOUSE_OVER, displayMouseIn);
					}
				}
			else if(e.type == MouseEvent.MOUSE_OVER)
				{
				closetimer.removeEventListener("timer", startHideTimer);
				closetimer.removeEventListener("timer", collapseMenu);
				closetimer.stop();
				if(display.hasEventListener(MouseEvent.MOUSE_OUT) == false)
					{
					display.addEventListener(MouseEvent.MOUSE_OUT, displayMouseOut);
					}
				}
 
			e.currentTarget.addEventListener(MouseEvent.CLICK,passOnMouseEvent);
			e.currentTarget.addEventListener(MouseEvent.MOUSE_OUT,passOnMouseEvent);
			e.currentTarget.addEventListener(MouseEvent.MOUSE_OVER,passOnMouseEvent);
		}
 
		public function getDisplay():DisplayObjectContainer
		{
			return display;
		}
 
		public function setMenuEnabled(to : Boolean)
		{
			for each (var object:RadialMenuItem in objects)
			{
				object.setEnabled(to);
			}
		}
 
		public function getObjects() : Array
		{
			return objects;
		}
 
		/**
		* This function will clean up the menu.
		* Closing and hiding it if necessary, and then removing the menu items from the stage and deleting the references to them.
		* If the references from when they were created have been deleted as well, the trash cleanup should clear them from memory.
		*/
		public function clear()
		{
			canOpen = false;
			stayOpen = false;
 
			display.removeEventListener(MouseEvent.MOUSE_OVER,displayMouseIn);
			display.removeEventListener(MouseEvent.MOUSE_OUT,displayMouseOut);
 
			switch (type)
			{
				case RadialMenu.COLLAPSING :
					collapseMenu(null);
					this.addEventListener("close",	function(e:Event):void 
								{
									while(objects.length > 0)
									{
										var removeme : RadialMenuItem = objects.pop();
 
										if(removeme.parent != null)
										{
											the_stage.removeChild(removeme);
										}
									}
								});
					break;
				case RadialMenu.INSTANT :
				case RadialMenu.STATIC :
				case RadialMenu.STATIC_START_HIDDEN :
					hideMenu(null);
					while(objects.length > 0)
					{
						var removeme : RadialMenuItem = objects.pop();
 
						if(removeme.parent != null)
						{
							the_stage.removeChild(removeme);
						}
					}
					break;
			}
		}
 
		public function calculateAngle( circle_type : int ) : Number
		{
			var degreesperstep:Number;
			switch (circle_type)
			{
				case FULLCIRCLE :
					// we want to make a full circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 360
					// is the amount of degrees we should seprate the items by.
					switch (objects.length)
					{
						case 3 :
							// 3 items in a full circle look better at the angle for 4
							degreesperstep = 360 / 4;
							break;
							//case 5:
							// 5 items looks bad no mater what I try
							// degreesperstep = 360 / 5;
							// break;
						default :
							degreesperstep = 360 / objects.length;
							break;
					}
					break;
 
				case HALFTOP :
					// we want to make half a circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 180
					// gets us a circle that assumes the last item(at 180 degrees) is occupied
					// (as in a full circle 360 is occupied by the object at 0)
					// hence we use <objects.length -1=""> to get it to use all the available space
					switch (objects.length)
						{
						case 1 :
							// if there is only 1 item we want it to appear centered
							// so we set the steps to 3 and place it as the second item
							// which is the one displayed at the bottom
							// so its actually 180 / (3 -1)
							degreesperstep = 180 / 2;
							objects.unshift(new RadialMenuItem());
							break;
						default :
							degreesperstep = 180 / (objects.length - 1);
							break;
						}
					degreesperstep = degreesperstep * -1;// to go the other way we simply use a negative angle
					break;
 
				case LINE3ELEMENTS :
				case LINE4ELEMENTS :
				case LINE5ELEMENTS :
				case LINE6ELEMENTS :
					// line menus are calculated differently, the degrees don't really matter
					degreesperstep = 180 / 2.7;
					break;
				case THIRDBOTTOM :
					// we want to make a third of a circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 120
					// to shift this slice of the circle to face downwards, 
					// we add in blank objects to fill up an extra 60 degrees of the circle. 
					// this gets us a circle that assumes the last item(at 120 degrees) is occupied
					// (as in a full circle the spot at 360 is occupied by the object at 0)
					// hence we use <objects.length -1=""> to get it to use all the available space
					// to make a circle that uses only the bottom
					degreesperstep = 120 / (objects.length - 1);
 
					var numberofblankstoadd : int = 60 / degreesperstep;
					for(var blanksadded : int = 0; blanksadded < numberofblankstoadd; blanksadded ++)
						{
						objects.push(new RadialMenuItem());
						objects.unshift(new RadialMenuItem());
						}
					// The 'break' statement here is intentionally missing. The thirdbottom just adds blanks
					// and the blanks fill up the first and last spots of a halfbottom arrangement
					// causing the 'real' objects to appear in a third of a circle at the bottom.
 
				case HALFBOTTOM :
					// we want to make half a circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 180
					// gets us a circle that assumes the last item(at 180 degrees) is occupied
					// (as in a full circle 360 is occupied by the object at 0)
					// hence we use <objects.length -1=""> to get it to use all the available space
					switch (objects.length)
					{
						case 1 :
							// if there is only 1 item we want it to appear centered
							// so we set the steps to 3 and place it as the second item
							// which is the one displayed at the bottom
							// so its actually 180 / (3 -1)
							degreesperstep = 180 / 2;
							objects.unshift(new RadialMenuItem());
							break;
						case 2 : 
							// if there are 2 items it looks better to add an object at the front an the back of the hand
							objects.push(new RadialMenuItem());
							objects.unshift(new RadialMenuItem());
						default :
							degreesperstep = 180 / (objects.length - 1);
							break;
					}
					break;
				case SECONDQUARTER :
					// we want to make quarter a circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 90
					// gets us a circle that assumes the last item(at 90 degrees) is occupied
					// (as in a full circle the spot at 360 is occupied by the object at 0)
					// hence we use <objects.length -1=""> to get it to use all the available space
					switch (objects.length)
					{
						case 1 :
							// if there is only 1 item we want it to be centered.
							// so it looks better to add an object at the front an the back of the hand
							objects.push(new RadialMenuItem());
							objects.unshift(new RadialMenuItem());
						default :
							degreesperstep = 90 / (objects.length - 1);
							degreesperstep = degreesperstep;
							break;
					}
					break;
				case FIRSTQUARTER :
					// we want to make quarter a circle in <objects.length> steps, 
					// so the number that <objects.length> can be multiplied with to make 90
					// gets us a circle that assumes the last item(at 90 degrees) is occupied
					// (as in a full circle the spot at 360 is occupied by the object at 0)
					// hence we use <objects.length -1=""> to get it to use all the available space
					switch (objects.length)
					{
						case 1 :
							// if there is only 1 item we want it to be centered.
							// so it looks better to add an object at the front an the back of the hand
							objects.push(new RadialMenuItem());
							objects.unshift(new RadialMenuItem());
						default :
							degreesperstep = 90 / (objects.length - 1);
							degreesperstep = degreesperstep * -1; // to go the other way we simply use a negative angle
							break;
					}
					break;
 
			}
 
			return degreesperstep;
		}
 
		public function calculateSmalestScale( elementscale : Number, distancefromcentre : Number, degreesperstep : Number ) : Number
		{
			// Calculate automatic scaling
			var smallestscale:Number;
 
			if(elementscale != 0)
			{
				var firstNonFillerObject : int = -1;
				var secondNonFillerObject : int = -1;
				for ( var atascaleableobject : int = 0;  atascaleableobject < objects.length ; atascaleableobject ++ )
				{
					if(objects[atascaleableobject].width > 1 && objects[atascaleableobject].height > 1)
					{
						if(firstNonFillerObject == -1)
						{
							firstNonFillerObject = atascaleableobject;
						}
						else
						{
							secondNonFillerObject = atascaleableobject;
							// we only need 2 objects to determine scale with, end the loop here.
							break;
						}
					}
				}
 
				if(firstNonFillerObject == -1 || secondNonFillerObject == -1)
				{
					// trace("not enough objects to care about scaling. " + objects); 
					smallestscale = 1;
				}
				else
				{
					// do scaling
					var firstscaleAdjustmentPercentage:Number;
					var secondscaleAdjustmentPercentage:Number;
 
					var firstscalingRadials = 	( 0 * degreesperstep ) * convertToRadials;
					var secondscalingRadials = 	( (secondNonFillerObject - firstNonFillerObject) * degreesperstep ) * convertToRadials;
					var firstObjectX : Number =		Math.cos( firstscalingRadials ) * distancefromcentre;
					var secondObjectX : Number =	Math.cos( secondscalingRadials ) * distancefromcentre ;
					var firstObjectY : Number =		Math.sin( firstscalingRadials ) * distancefromcentre;
					var secondObjectY : Number =	Math.sin( secondscalingRadials ) * distancefromcentre;
 
					var differenceX = ( firstObjectX - secondObjectX );
					var differenceY = ( firstObjectY - secondObjectY );
					var distanceinpixels:Number = Math.abs( Math.sqrt( differenceX * differenceX + differenceY * differenceY ) );
					// angles might be negative, but distance should always be positive.
 
					var averageObjectWidth = (objects[firstNonFillerObject].width + objects[secondNonFillerObject].width) * 0.5;
					var averageObjectHeight = (objects[firstNonFillerObject].width + objects[secondNonFillerObject].width) * 0.5;
					// some buttons might have a width or height larger than their actual clickable area, 
					// due to long text labels for example adjust them by an elementscale to compensate
					averageObjectWidth *= elementscale;
					averageObjectHeight *= elementscale;
 
					firstscaleAdjustmentPercentage = distanceinpixels / averageObjectWidth;
					secondscaleAdjustmentPercentage = distanceinpixels / averageObjectHeight;
 
					smallestscale = Math.min( firstscaleAdjustmentPercentage, secondscaleAdjustmentPercentage );
 
					// as not everything looks good when enlarged cap scale at 100%
					if (smallestscale > 1 || smallestscale <= 0)
					{
						smallestscale = 1;
					}
				}
			}
			else
			{
				smallestscale = 1;
			}
 
			return smallestscale;
		}
	}// end class
}// end package</xmp>

Supporting Classes

Any class intended to be displayed in a radial menu, should extend RadialMenuItem. A radial menu item contains animation values, so it can be tweened to and from the center of the menu.
As class I use most often in a radial menu is my MovieClipButton, which can also be used on it’s own. The MovieClipButton fixes the limitation of the flash SimpleButton class, which does not allow you to attach child movieclips to the button or use the getChild() methods to affect the MovieClips that the SimpleButton is composed of.

<xmp>package com.gag.games
{
	import flash.display.MovieClip;
	import flash.events.*;
	import flash.geom.ColorTransform;
	import flash.text.TextField;
	/**
	* All buttons to be displayed in a radial menu should extend this class.
	* It will look best if all the buttons are roughly the same dimentions.
	* Resizing is done based on the expectation that the first 2 buttons(at position[0] and [1])
	* in the array passed to a new RadialMenu are roughly representative for the size of all other buttons.
	*/
	public class RadialMenuItem extends MovieClip
	{
		public var destinationX:Number;
		public var destinationY:Number;
		public var tweenXHandler:Object;
		public var tweenYHandler:Object;
 
		public function setEnabled(to:Boolean)
		{
			this.mouseEnabled = to;
			this.setBrightness(to == true ? 100:90);
		}
 
		public function setBrightness(b:Number)
		{
			var t = this.transform.colorTransform;
			t.redMultiplier = b / 100;
			t.greenMultiplier = b / 100;
			t.blueMultiplier = b / 100;
			this.transform.colorTransform = t;
			// unfortunately the above only seems to affect the current frame
			// so we have to get all the children in all the other frames and
			// make them darker or brighter too.
			for (var i:int = 0; i > this.numChildren; i++)
			{
				var child = getChildAt(i);
				var childt = child.transform.colorTransform;
				childt.redMultiplier = b / 100;
				childt.greenMultiplier = b / 100;
				childt.blueMultiplier = b / 100;
				getChildAt(i).transform.colorTransform = childt;
			}
			// this.alpha = b/100; // this does not look as good.
		}
	}// end class
}// end package</xmp>
<xmp>package com.gag.games
{
	import flash.display.MovieClip;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import com.gag.games.RadialMenuItem;
	import flash.events.Event;
	import flash.events.EventDispatcher;
 
	/**
	* In AS3 the simple button class does not extend DisplayObjectContainer
	* This causes tons of problems in adding scripted functions dealing with
	* the look or animation of the button.
	*
	* A movieclip exported to actionscript that extends this class functions as
	* a button, dispatching MouseEvent.MOUSE_OVER, MouseEvent.MOUSE_OUT and MouseEvent.CLICK 
	* as a button would.
	*
	* Remeber that the frames for the button need to be named UP, OVER, DOWN
	* The simplebutton type does this automatically, but a movieclip button requires
	* that you name the frames manually.
	* This however means you could use frames other than the first 3 frames, or change the order.
	*/
	public class MovieClipButton extends RadialMenuItem
	{
		private var clickable:Boolean;
 
		public function MovieClipButton()
		{
			this.gotoAndStop("UP");
			this.buttonMode = true;
			this.mouseEnabled = true;
			this.mouseChildren = true;
			this.useHandCursor = true;
			this.addEventListener(MouseEvent.MOUSE_OVER,onMouseOver);
			this.addEventListener(MouseEvent.MOUSE_OUT,onMouseOut);
			this.addEventListener(MouseEvent.MOUSE_DOWN,onMouseDown);
			this.addEventListener(MouseEvent.CLICK,onClick);
			clickable = true;
		}
 
		public function onMouseOver(e:MouseEvent)
		{
			if (clickable == true)
			{
				this.gotoAndStop("OVER");
			}
		}
 
		public function onMouseOut(e:MouseEvent)
		{
			this.gotoAndStop("UP");
		}
 
		public function onMouseDown(e:MouseEvent)
		{
			if (clickable == true)
			{
				this.gotoAndStop("DOWN");
			}
		}
 
		public function onClick(e:MouseEvent)
		{
			if (clickable == true)
			{
				this.gotoAndStop("OVER");
			}
		}
 
		override public function setEnabled(to:Boolean)
		{
			clickable = to;
			this.buttonMode = clickable;
			this.mouseEnabled = clickable;
			this.mouseChildren = clickable;
			this.useHandCursor = clickable;
 
			if (clickable == false)
			{
				setBrightness( 50 );
				this.gotoAndStop("UP");
			}
			else
			{
				setBrightness( 100 );
			}
		}
 
		public function isEnabled():Boolean
		{
			return clickable;
		}
 
   		override public function set visible(b:Boolean):void
			{
				super.visible = b;
				this.dispatchEvent(new Event('VisibleEvent'));
			}
	}// end class
}// end package</xmp>

Using the scripts

All that code might make you think using these scripts to have radial menu’s in your flash project will be a lot of work, but fortunately it isn’t. There are 2 things you have to do before you can create your radial menu.You need to create a Hit-circle and a MovieClip that extends RadialMenuItem.The simplest way is to use the shape tool and draw 2 circles, that don’t overlap. Make one circle larger, that will be the hit-circle.Right click each circle and select, ‘Convert to Symbol’ name them something that will let you tell which is which. Give the hit-circle and instance name you can remember.For the smaller button circle, check the ‘Export for Actionscript’ checkbox. Set the ‘Base Class’ to ‘com.gag.games.MovieClipButton’ and change the ‘Class’ to a name that you can remember.Now you are ready to create your radial menu.Go to the script from which the radial menu is supposed to be created.At the top add:

import com.gag.games.RadialMenu;

Where you want your menu to be spawned, add the following:

<xmp>var buttons = new Array();
var button1 = new ClassNameOfYourButton() ;
button1 .addEventListener(MouseEvent.CLICK,OnClickFunctionOfYourButton1);
buttons.push( button1 );
var button2 = new ClassNameOfYourButton() ;
button2 .addEventListener(MouseEvent.CLICK,OnClickFunctionOfYourButton2);
buttons.push( button2 )
var menu = new RadialMenu( buttons, InstanceNameOfHitCricle, 0, 0, RadialMenu.FULLCIRCLE, false, RadialMenu.COLLAPSING, IntanceNameOfHitCricle.parent, false, 1);</xmp>

And that will create a radial menu with 2 buttons, at the location of the hit-circle. You can change the number of buttons, change the class of the buttons to make them look different, set the hit circle to be visible or not and do whatever you need to make your menu look the way you want.This code is an example on how to instantiate radial menus.

<xmp>package com.zwets.examples
{
	import flash.display.MovieClip;
	import com.gag.games.RadialMenu;
	import flash.events.Event;
	import flash.events.MouseEvent;
 
	public class RadialMenuExample extends MovieClip
	{
		var previousTopLevelMenu = null;
 
		public function RadialMenuExample()
		{
			ExplodeNewRadialMenu( Container.HitArea );
		}
 
		public function ExplodeNewRadialMenu( radialHitArea : MovieClip )
		{
			if( previousTopLevelMenu != null )
			{
				previousTopLevelMenu.setMenuEnabled( false );
			}
 
			var buttons = new Array();
			var randomNumber : int = Math.ceil( 2 + (Math.random() * 5));
			for( var i : int = 0; i < randomNumber; ++i)
			{
				var aButton = new BombButton() ;
				aButton.addEventListener(MouseEvent.CLICK,BombClicked);
				buttons.push( aButton );
			}
			var menu = new RadialMenu( buttons, radialHitArea, 0, 0, RadialMenu.FULLCIRCLE, false, RadialMenu.COLLAPSING, Container, false, 1.15);
			previousTopLevelMenu = menu;
		}
 
		public function BombClicked( e : Event )
		{
			var eventSource = e.currentTarget;
			if( eventSource is BombButton )
			{
				var bombButton = BombButton( eventSource );
				if ( bombButton.isEnabled() == true )
				{
					previousTopLevelMenu.setMenuEnabled( false );
					previousTopLevelMenu.getDisplay().alpha *= 0.25;
					previousTopLevelMenu.clear();
					// re-center everything on screen on the last clicked button.
					Container.x = (stage.stageWidth * 0.5) - bombButton.destinationX;
					Container.y = (stage.stageHeight * 0.5) - bombButton.destinationY;
 
					// spawn a new radial menu
					var newHitArea = new RadialMenuHitArea();
					Container.addChild( newHitArea );
					var randomScale : Number = 0.4 + Math.random();
					newHitArea.x = bombButton.destinationX;
					newHitArea.y = bombButton.destinationY;
					newHitArea.width *= randomScale;
					newHitArea.height *= randomScale;
					ExplodeNewRadialMenu( newHitArea );
				}
			}
		}
	}// end class
}// end package</xmp>

Downloads

You will need Flash CS6 or newer to open the .fla project. If you do not have CS6 you can get a free trail of Adobe’s newest flash software on their website.
If you want to use the scripts in a different project just move the com folder and it’s sub folder to your project folder.
CS6 Project and scripts: AS3 Radial Menu.rar

1 Comment

  1. Willem

    If you have any comments or questions, please feel free to leave a reply. This is only my second tutorial I and can probably use all the advice I can get on improving them.

Leave Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.