Install Circle Chain on your iPhone for free and get the source code!! 645 downloads to go - last updated: April 21, 2012

AS3 version of Nodes engine: full playable game with infinite solvable levels

Today I want to point you on a very interesting prototype I made more than four years ago… Creation of the engine behind “Nodes” game with Flash.

It’s a basic prototype of Nodes, a puzzle game which got some success years ago.

Why am I publishing this prototype today? First, because I improved and now it’s a “complete” game, which means you can play for real, trying to beat infinite random-generated solvable levels.

Second, and probably most important reason, is the gameplay is perfect for a mobile porting, which I am currently making.

Anyway, this is the game:

Drag the red nodes to highlight all blue targets with the laser. Once a level is completed, you will face another random generated level.

And this is the short source code I used to make it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private const DRAGGABLES:Number=4;
		private const TARGETS:Number=5;
		private var draggableNode:DraggableNode;
		private var target:Target;
		private var laserCanvas:Sprite=new Sprite();
		private var draggableVector:Vector.<DraggableNode>=new Vector.<DraggableNode>();
		private var targetVector:Vector.<Target>=new Vector.<Target>();
		private var isDragging:Boolean=false;
		public function Main() {
			addChild(laserCanvas);
			placeDraggables(false);
			placeTargets(false);
			shuffleDraggables();
			drawLaser();
			stage.addEventListener(MouseEvent.MOUSE_MOVE,moving);
		}
		private function placeDraggables(justMove:Boolean):void {
			for (var i:Number=0; i<DRAGGABLES; i++) {
				if (! justMove) {
					draggableNode=new DraggableNode();
					addChild(draggableNode);
					draggableVector.push(draggableNode);
					draggableNode.addEventListener(MouseEvent.MOUSE_DOWN,drag);
					draggableNode.addEventListener(MouseEvent.MOUSE_UP,dontDrag);
				}
				do {
					var wellPlaced:Boolean=true;
					draggableVector[i].x=Math.floor(Math.random()*600)+20;
					draggableVector[i].y=Math.floor(Math.random()*440)+20;
					for (var j:Number=i-1; j>=0; j--) {
						if (getDistance(draggableVector[j],draggableVector[i])<150) {
							wellPlaced=false;
						}
					}
				} while (!wellPlaced);
			}
		}
		private function placeTargets(justMove:Boolean):void {
			for (var i:Number=0; i<TARGETS; i++) {
				if (! justMove) {
					target=new Target();
					addChildAt(target,0);
					targetVector.push(target);
					target.alpha=0.5;
				}
				do {
					var wellPlaced:Boolean=true;
					var segment=Math.floor(Math.random()*DRAGGABLES);
					var p1:Sprite=draggableVector[segment];
					var p2:Sprite=draggableVector[(segment+1)%DRAGGABLES];
					var angle:Number=Math.atan2((p2.y-p1.y),(p2.x-p1.x))
					var targetDistance:Number=Math.random()*getDistance(p1,p2);
					targetVector[i].x=p1.x+targetDistance*Math.cos(angle);
					targetVector[i].y=p1.y+targetDistance*Math.sin(angle);
					for (var j:Number=i-1; j>=0; j--) {
						if (getDistance(targetVector[j],targetVector[i])<50) {
							wellPlaced=false;
						}
					}
					for (j=0; j<DRAGGABLES; j++) {
						if (getDistance(draggableVector[j],targetVector[i])<50) {
							wellPlaced=false;
						}
					}
				} while (!wellPlaced);
			}
		}
		private function shuffleDraggables():void {
			for (var i:Number=0; i<DRAGGABLES; i++) {
				draggableVector[i].x=Math.floor(Math.random()*600)+20;
				draggableVector[i].y=Math.floor(Math.random()*440)+20;
			}
		}
		private function drag(e:MouseEvent):void {
			e.target.startDrag(true);
			isDragging=true;
		}
		private function dontDrag(e:MouseEvent):void {
			e.target.stopDrag();
			isDragging=false;
		}
		private function moving(e:MouseEvent):void {
			var connected:Number=0;
			if (isDragging) {
				drawLaser();
				for (var i:Number=0; i<TARGETS; i++) {
					if (laserCanvas.hitTestPoint(targetVector[i].x,targetVector[i].y,true)) {
						targetVector[i].alpha=1;
						connected++;
					}
					else {
						targetVector[i].alpha=0.5;
					}
				}
				if (connected==TARGETS) {
					placeDraggables(true);
					placeTargets(true);
					shuffleDraggables();
					drawLaser();
				}
			}
		}
		private function drawLaser():void {
			laserCanvas.graphics.clear();
			laserCanvas.graphics.lineStyle(5,0xff0000);
			laserCanvas.graphics.moveTo(draggableVector[0].x,draggableVector[0].y);
			for (var i:Number=1; i<DRAGGABLES; i++) {
				laserCanvas.graphics.lineTo(draggableVector[i].x,draggableVector[i].y);
			}
			laserCanvas.graphics.lineTo(draggableVector[0].x,draggableVector[0].y);
		}
		private function getDistance(s1:Sprite,s2:Sprite):Number {
			var dX:Number=s2.x-s1.x;
			var dY:Number=s2.y-s1.y;
			return Math.sqrt(dX*dX+dY*dY);
		}
	}
}

Let’s see the most interesting lines:

5
6
7
8
9
10
11
12
private const DRAGGABLES:Number=4;
private const TARGETS:Number=5;
private var draggableNode:DraggableNode;
private var target:Target;
private var laserCanvas:Sprite=new Sprite();
private var draggableVector:Vector.<DraggableNode>=new Vector.<DraggableNode>();
private var targetVector:Vector.<Target>=new Vector.<Target>();
private var isDragging:Boolean=false;

DRAGGABLES and TARGETS are respectively the amounts of red draggable nodes and blue targets.

draggableNode and target are variables to instantiate respectively DraggableNode and Target sprites, in the library

laserCanvas is the sprite we will use to draw the laser

draggableVector and targetVector are vectors we will fill respectively with draggable nodes and targets

isDragging is a Boolean variable to help us know whether the player is dragging a node.

14
15
16
17
18
19
addChild(laserCanvas);
placeDraggables(false);
placeTargets(false);
shuffleDraggables();
drawLaser();
stage.addEventListener(MouseEvent.MOUSE_MOVE,moving);

This is the game itself, which works this way: first, draggable nodes are randomly placed on the stage, then target are randomly placed over lasers, generating a solvable levels, then draggable nodes are shuffled and finally the laser is shown.

A listener is added to trigger mouse movements.

And this is how we place draggable nodes:

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private function placeDraggables(justMove:Boolean):void {
	for (var i:Number=0; i<DRAGGABLES; i++) {
		if (! justMove) {
			draggableNode=new DraggableNode();
			addChild(draggableNode);
			draggableVector.push(draggableNode);
			draggableNode.addEventListener(MouseEvent.MOUSE_DOWN,drag);
			draggableNode.addEventListener(MouseEvent.MOUSE_UP,dontDrag);
		}
		do {
			var wellPlaced:Boolean=true;
			draggableVector[i].x=Math.floor(Math.random()*600)+20;
			draggableVector[i].y=Math.floor(Math.random()*440)+20;
			for (var j:Number=i-1; j>=0; j--) {
				if (getDistance(draggableVector[j],draggableVector[i])<150) {
					wellPlaced=false;
				}
			}
		} while (!wellPlaced);
	}
}

The Boolean argument is used to let us know whether we just have to adjust nodes or we also have to physically add them to stage and fill draggableVector vector. When the game is run, we have to physically add the nodes, but when the player completes a level we just have to arrange nodes in different positions.

Look at the listeners attached to each node, and also look how we are ensuring there are at least 150 pixels of empty space around nodes.

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private function placeTargets(justMove:Boolean):void {
	for (var i:Number=0; i<TARGETS; i++) {
		if (! justMove) {
			target=new Target();
			addChildAt(target,0);
			targetVector.push(target);
			target.alpha=0.5;
		}
		do {
			var wellPlaced:Boolean=true;
			var segment=Math.floor(Math.random()*DRAGGABLES);
			var p1:Sprite=draggableVector[segment];
			var p2:Sprite=draggableVector[(segment+1)%DRAGGABLES];
			var angle:Number=Math.atan2((p2.y-p1.y),(p2.x-p1.x))
			var targetDistance:Number=Math.random()*getDistance(p1,p2);
			targetVector[i].x=p1.x+targetDistance*Math.cos(angle);
			targetVector[i].y=p1.y+targetDistance*Math.sin(angle);
			for (var j:Number=i-1; j>=0; j--) {
				if (getDistance(targetVector[j],targetVector[i])<50) {
					wellPlaced=false;
				}
			}
			for (j=0; j<DRAGGABLES; j++) {
				if (getDistance(draggableVector[j],targetVector[i])<50) {
					wellPlaced=false;
				}
			}
		} while (!wellPlaced);
	}
}

Targets are placed/moved in the same way, just keeping at least 50 pixels of empty space before we find another target or a draggable node.

The interesting part starts at line 52 where I choose a random laser segment where to place the target.

Lines 53-54: Given the segment, I determine the nodes which delimit such segment

Line 55: Finding the angle of the segment

Line 56: Finding segment length

Lines 57-58: Once I know the start and end points of the segment, as well as its length and angle, it’s easy to place the target in a random point of the segment using trigonometry.

72
73
74
75
76
77
private function shuffleDraggables():void {
	for (var i:Number=0; i<DRAGGABLES; i++) {
		draggableVector[i].x=Math.floor(Math.random()*600)+20;
		draggableVector[i].y=Math.floor(Math.random()*440)+20;
	}
}

This is how draggable nodes are shuffled… just placing them in a random position.

107
108
109
110
111
112
113
114
115
private function drawLaser():void {
	laserCanvas.graphics.clear();
	laserCanvas.graphics.lineStyle(5,0xff0000);
	laserCanvas.graphics.moveTo(draggableVector[0].x,draggableVector[0].y);
	for (var i:Number=1; i<DRAGGABLES; i++) {
		laserCanvas.graphics.lineTo(draggableVector[i].x,draggableVector[i].y);
	}
	laserCanvas.graphics.lineTo(draggableVector[0].x,draggableVector[0].y);
}

And this is how the laser is placed on the stage, it’s just a line connecting all nodes in sequence, also connecting the latest node with the first node.

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
private function moving(e:MouseEvent):void {
	var connected:Number=0;
	if (isDragging) {
		drawLaser();
		for (var i:Number=0; i<TARGETS; i++) {
			if (laserCanvas.hitTestPoint(targetVector[i].x,targetVector[i].y,true)) {
				targetVector[i].alpha=1;
				connected++;
			}
			else {
				targetVector[i].alpha=0.5;
			}
		}
		if (connected==TARGETS) {
			placeDraggables(true);
			placeTargets(true);
			shuffleDraggables();
			drawLaser();
		}
	}
}

The last interesting function is the one which handles nodes movement. In this case, I just perform an hit test between nodes registration point and the laser line, so the strongest the line, the easier the game. Once all targets are touched by laser simultaneously, a new level starts.

And that’s all, download the source code, hope you enjoyed this prototype and you will enjoy the mobile porting too.

Rate this post: 1 Star2 Stars3 Stars4 Stars5 Stars (8 votes, average: 4.38 out of 5)
Loading ... Loading ...
Flash Templates provided by Template Monster are pre-made web design products developed using Flash technology.
They can be easily customized to meet the unique requirements of your project.
Be my fan on Facebook and follow me on Twitter! Exclusive content for my Facebook fans and Twitter followers

This post has 9 comments

  1. Greg

    on November 8, 2011 at 9:14 pm

    Hey Emanuele,
    I’m kind of confused as to why you handle your hit testing with hitTestPoint when the original nodes game (presumably) uses a hit test object method. It makes your game much harder, but also gives no real indication as to where the laser should land to activate the node. This is manageable with my mouse, but I think I would get pretty pissed playing this on my phone on the train!

  2. James

    on November 9, 2011 at 1:53 am

    This game doesn’t have a good transition between levels.

  3. Ben Reynolds

    on November 9, 2011 at 2:21 am

    Cool game, I really like the infinite level concept! Saves a lot of time from having to manually create all the levels. Thanks :)

  4. Emanuele Feronato

    on November 9, 2011 at 12:04 pm

    @greg: it should land in the center of the target. Anyway, the error-proof way to determine activation would be a point-segment distance. I’ll publish something about it later

  5. robert

    on November 12, 2011 at 7:32 pm

    hey Emanuele, i was wondering how i would be able to make the draggable nodes rotate so that the smallest arc distance for the lines is in front of them…
    i.e. i changed the graphics to people and i want them to rotate to make it seem like the lines are coming from the front of their body

    also how would i go about changing the “laser” to a movie clip i created?

  6. Jeremy

    on November 21, 2011 at 8:45 pm

    How would I go about making the line my own graphic? Say, a rope or chain? Thanks.
    Great stuff!

  7. sreekanth

    on December 2, 2011 at 1:45 pm

    I wanted the dragable nodes look professional like in the real game and there should be a background in the game.And the laser should be changed into a chain.
    Can you please help me.
    Also can after that i publish it in my website.
    I will mention your name also

  8. GoldenCheese

    on December 13, 2011 at 11:28 am

    When i change the DRAGGABLES:Number=2 and TARGETS:Number=2, the game would error when linked all targets. How can i fix this error.

  9. Ciro

    on February 11, 2012 at 7:27 am

    Hi emanuele.

    After seeing how easily you created this in AS3, I decided to go about making a Facebook version of this game called ‘Nodes Social’. I used your logic as a rough guide, and built it from scratch. I’ve credited this site, and the maker of the original game in the about section.

    The cool thing about this game is that after each level it tells you what friends you beat.

    Let me know what you think. https://apps.facebook.com/nodessocial/