// Demo for generating space stations or lunar base radial layouts // Author: Amit J Patel // License: MIT (see LICENSE file) // Url: http://www-cs-students.stanford.edu/~amitp/game-programming/game-ideas/radial-base/ package { import mx.core.UIComponent; import flash.display.*; import flash.geom.*; import flash.filters.*; // Population slider, compute number of levels automatically // Note all nodes need their own radial segment // Irregular spacing of nodes, levels [SWF(width="400", height="400")] public class spacestation extends UIComponent /* Or Sprite */ { [Bindable] public var numLevels:int = 6; [Bindable] public var radialSpacing:Number = 25.0; [Bindable] public var arcDensity:Number = 3.0; [Bindable] public var nodeDensity:Number = 0.2; [Bindable] public var nodeDensityShrink:Number = 0.5; [Bindable] public var nodeRadius:Number = 9.0; [Bindable] public var nodeRadiusShrink:Number = 0.3; [Bindable] public var edgeDiameter:Number = 6.0; [Bindable] public var edgeDiameterShrink:Number = 0.2; [Bindable] public var arcWidth:Number = 0.01; public var nodeColor:int = 0x3399cc; public var edgeColor:int = 0x99aabb; public var center:Point = new Point(200, 200); public var content:Sprite = new Sprite(); public var nodes:Sprite = new Sprite(); public var edges:Sprite = new Sprite(); function spacestation() { addChild(content); content.addChild(edges); content.addChild(nodes); content.filters = [new DropShadowFilter(0.5)]; nodes.filters = [ new BevelFilter(3.0, 45, 0xffffff, 0.5, 0x000000, 0.5, 4.0, 2.0, 2), new BevelFilter(5.0, 55, 0xffffff, 0.3, 0x000000, 0.3, 4.0, 2.0, 2), new BevelFilter(7.0, 35, 0xffffff, 0.3, 0x000000, 0.3, 4.0, 2.0, 2), new GlowFilter(0x77ffff, 0.5, 9.0, 9.0, 4)]; edges.filters = [new BevelFilter()]; nodes.x = center.x; nodes.y = center.y; edges.x = center.x; edges.y = center.y; AngularRange.runTests(); reseed(); redraw(); } // We want to repeat the same random number stream; since there's // no easy way to set the seed in Flash, I simply save a bunch of // random numbers in an array and reuse them. private var randomNumberIndex:int = 0; private var randomNumbers:Array = []; private function random():Number { var x:Number = randomNumbers[randomNumberIndex]; randomNumberIndex = (randomNumberIndex+1) % randomNumbers.length; return x; } // Generate a new set of random numbers public function reseed():void { randomNumberIndex = 0; randomNumbers = []; for (var i:int = 0; i < 10000; i++) { randomNumbers.push(Math.random()); } } // Compute the locations of the nodes public function generateLayout():Array { randomNumberIndex = 0; // nodesAtLevel is an array of array of node descriptions where // each description contains the angle and the arc extension // from that node. var nodesAtLevel:Array = []; // We build up the structure layer by layer, with each layer // modifying the previous one to make sure the arc extensions // are wide enough to cover the angles of the nodes in the new // layer. for (var level:int = 0; level < numLevels; level++) { nodesAtLevel.push([]); var radius:Number = level * radialSpacing; var numNodes:int = 1 + Math.ceil(level * arcDensity); var startAngle:Number = Math.PI * 2 * random(); for (var i:int = 0; i < numNodes; i++) { var angle:Number = AngularRange.normalize( startAngle + i * (Math.PI * 2) / numNodes); var descriptor:Object = { angle: angle, // NOTE: there may be a bug when arc extensions are too // long (greater than a full circle); the constructor // should probably take left+span instead of left+right. arc: new AngularRange(angle - arcWidth*random()*Math.PI*2, angle + arcWidth*random()*Math.PI*2) }; var probabilityOfNode:Number = nodeDensity * (1.0 - nodeDensityShrink * level/numLevels) if (i == 0 || random() < probabilityOfNode) { nodesAtLevel[level].push(descriptor); // Now we need to scan the previous level to make sure // that some arc is large enough to include the current // angle. Find the closest arc and extend it. var bestRange:AngularRange = null; var bestDistance:Number = 10000; if (level > 0) { for (var k:int = 0; k < nodesAtLevel[level-1].length; k++) { descriptor = nodesAtLevel[level-1][k]; var d:Number = descriptor.arc.distanceTo(angle); if (d < bestDistance) { bestDistance = d; bestRange = descriptor.arc; } } bestRange.extend(angle); } } } } return nodesAtLevel; } // Redraw everything public function redraw():void { var nodesAtLevel:Array = generateLayout(); drawBackground(); nodes.graphics.clear(); edges.graphics.clear(); for (var level:int = 0; level < nodesAtLevel.length; level++) { for (var i:int = 0; i < nodesAtLevel[level].length; i++) { var radius:Number = level * radialSpacing; var angle:Number = nodesAtLevel[level][i].angle; var angularRange:AngularRange = nodesAtLevel[level][i].arc; // Draw the node itself as a circle; it might be // interesting to try different shapes here, including // some with animation nodes.graphics.beginFill(nodeColor); nodes.graphics.drawCircle( radius*Math.cos(angle), radius*Math.sin(angle), nodeRadius * (1.0 - nodeRadiusShrink*level/numLevels)); nodes.graphics.endFill(); // Draw the edges connected to this node edges.graphics.lineStyle( edgeDiameter * (1.0 - edgeDiameterShrink*level/numLevels), edgeColor); if (level > 0) { // Draw the connector from this node to the previous ring edges.graphics.moveTo(radius*Math.cos(angle), radius*Math.sin(angle)); edges.graphics.lineTo((level-1)*radialSpacing*Math.cos(angle), (level-1)*radialSpacing*Math.sin(angle)); } // Draw the portion of the ring connected to this node var steps:int = Math.ceil(angularRange.span/0.1); edges.graphics.moveTo(radius*Math.cos(angularRange.left), radius*Math.sin(angularRange.left)); for (var a:int = 1; a <= steps; a++) { var p:Point = new Point( radius*Math.cos(angularRange.left + angularRange.span*a/steps), radius*Math.sin(angularRange.left + angularRange.span*a/steps)); edges.graphics.lineTo(p.x, p.y); } edges.graphics.lineStyle(); } } } // Draw a "space" themed background with some Perlin noise public function drawBackground():void { graphics.clear(); var bitmapData:BitmapData = new BitmapData(400, 400); bitmapData.perlinNoise(400, 400, 6, Math.round(10000*random()), true, false, 7, true); bitmapData.colorTransform( bitmapData.rect, new ColorTransform(0.5, 0.5, 0.5, 1.0, 48, 32, 64, 0)); graphics.beginBitmapFill(bitmapData); graphics.drawRect(0, 0, 400, 400); graphics.endFill(); } } }