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.
They can be easily customized to meet the unique requirements of your project.















(8 votes, average: 4.38 out of 5)










This post has 9 comments
Greg
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!
James
This game doesn’t have a good transition between levels.
Ben Reynolds
Cool game, I really like the infinite level concept! Saves a lot of time from having to manually create all the levels. Thanks :)
Emanuele Feronato
@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
robert
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?
Jeremy
How would I go about making the line my own graphic? Say, a rope or chain? Thanks.
Great stuff!
sreekanth
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
GoldenCheese
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.
Ciro
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/