AS3: Creating a game like circle chain

The Chain Reaction Game

This game was inspired by Emanuele Feronato’s Circle Chain game and as he posted the AS2 code I took it and converted is to a basic AS3 chain reaction game.This tutorial also covers the mochi encryption issues and the possibility to load different APIs despite those.

Here is the DocumentClass called ScreenGame, it includes MochiAds and the Kongregate API that is only loaded when the game is played on Kongregate. This class is somewhat bloated because I just made it as tutorial and didn’t want to put to mujch effort in the game’s stucture (title screen, help screen, etc.) but then included too much features (nay, not too much but two tutorials at once because you can’t test the MochiAds thing without a game…)

package {
	import flash.display.Loader;
	import flash.display.LoaderInfo;
	import flash.display.Stage;
	import flash.display.MovieClip;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.events.Event;
	import flash.text.TextField;
	import flash.ui.Mouse;
	import flash.ui.ContextMenu;
	import flash.ui.ContextMenuItem;
	import flash.events.ContextMenuEvent;
	import flash.net.navigateToURL;
	import flash.net.URLRequest;
	import flash.media.SoundChannel;
	import mochi.as3.*

	public class ScreenGame extends MovieClip {

		public var bullet:MovieClip;
		public var bulletArray:Array;
		public var startX:Number;
		public var startY:Number;
		public var bulletDir:Number;
		public var gameTimer:Timer;

		public var monomer:MovieClip;
		public var monomerArray:Array;
		public var maxMonomer:int;

		public var levelText:MovieClip;
		public var levelDesc:Array;

		public var textOn:Boolean;
		public var gameStarted:Boolean;

		public var levelNumber:int;

		public var blueArray:Array;
		public var pinkArray:Array;
		public var greyArray:Array;
		public var blackArray:Array;
		public var killArray:Array;
		public var monomerKilled:int;

		public var monomerHolder:MovieClip;
		public var bulletHolder:MovieClip;

		public var lostAnyway:Boolean;

		public var counter:int;

		public var cMenu:ContextMenu;
		public var cMenuHomePage:ContextMenuItem;
		public var cMenuCopyRight:ContextMenuItem;

		public var soundFail:SoundFail;
		public var sfxSoundChannel:SoundChannel;

		public var kongregate:*;

All the includes that are needed. The level design is done via the blue, pink, grey, black and kill arrays.

		public function ScreenGame () {

			MochiBot.track(this, "xxxxxxxx");
			var _mochiads_game_id:String = "xxxxxxxxxxxxxxxx";

			this.addEventListener(Event.ADDED_TO_STAGE, initGame, false, 0, true);

		}

The EventListener is added to make the game work with the MochiAds Encryption and Version Control wrapper. Setting up the game directly in here would lead to a failure.

		public function initGame(event:Event) {

			//Need that to get the real url:
			var domainCheck = LoaderInfo(root.loaderInfo.loader.loaderInfo).url;

			//That would be used without Mochi's Encryption:
			//var domainCheck = LoaderInfo(root.loaderInfo).url;

			if ( domainCheck.indexOf("natan") >= 0 ) {
				domainName.text = "playing at blog.natan.info";
			}

			else if ( domainCheck.indexOf("kongregate") >= 0 ) {

				// Pull the API path from the FlashVars
				var paramObj:Object = LoaderInfo(root.loaderInfo.loader.loaderInfo).parameters;
				// The API path. The debug version ("shadow" API) will load if testing locally.
				var api_url:String = paramObj.api_path || "http://www.kongregate.com/flash/API_AS3_Local.swf";
				// Debug
				trace ( "API path: " + api_url );
				// Load the API
				var request:URLRequest = new URLRequest ( api_url );
				var loader:Loader = new Loader();
				loader.contentLoaderInfo.addEventListener ( Event.COMPLETE, loadComplete );
				loader.load ( request );
				this.addChild ( loader );
				//Kongregate API reference
				//done in public vars
				//var kongregate:*
				// Called when API swf finishes loading
				function loadComplete ( event:Event ):void {
				    // Save Kongregate API reference
				    kongregate = event.target.content;
				    // Connect
				kongregate.services.connect();
				    // Debug our services
					trace ( "\n" + kongregate.services );
					trace ( "\n" + kongregate.user );
			    	trace ( "\n" + kongregate.scores );
			    	trace ( "\n" + kongregate.stats );
				}
				domainName.text = " playing at Kongregate";
			}

			else {
				domainName.text = "blog.natan.info";
			}

That is the domain check to be used with the MochiAds wrapper. I tested it with the Kongregate API and it seems to work.

			cMenu = new ContextMenu();
			cMenu.hideBuiltInItems();
			cMenuHomePage = new ContextMenuItem("LÞ the keg'o'grog blog");
			cMenuCopyRight = new ContextMenuItem("© - 2009 _ 1.0");
			cMenuCopyRight.enabled = false;
			cMenuCopyRight.separatorBefore = true;
			cMenuHomePage.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, goToHomePage, false, 0, true);
			cMenu.customItems.push(cMenuHomePage, cMenuCopyRight);
			this.contextMenu = cMenu;

			soundFail = new SoundFail ();

Custom context menu and the ‘plopp’ sound.

			levelNumber = 0;
			bulletArray = new Array();
			monomerArray = new Array();
			//level number           1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20
			blueArray = new Array  ( 3,  0,  0,  0,  3,  3,  2, 25,  5,  0, 10,  2,  1, 10,  0,  5,  0, 25,  2, 20);
			pinkArray = new Array  ( 0,  0,  0,  2,  1,  2,  3,  0,  1,  5,  0,  1,  1,  0,  1,  0,  6,  0,  1, Math.round(Math.random()));
			greyArray = new Array  ( 0,  3,  0,  0,  0,  1,  2,  0,  4,  1, 10,  1,  1, 10,  0,  5,  0, 20,  2, 20);
			blackArray = new Array ( 0,  0,  3,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1, 10,  5,  5,  2,  5,  2, 20);
			killArray = new Array  ( 1,  1,  1,  0,  1,  2,  2, 24,  5,  1, 18,  3,  3, 30,  1, 13,  1, 50,  5,  0);
			levelDesc = new Array () ;
			levelDesc [0] = "LEVEL 1\n\nStart the first explosion with a click and try to start a chain reaction.";
			levelDesc [1] = "LEVEL 2\n\nThe number above shows how much goobies have to explode.";
			levelDesc [2] = "LEVEL 3\n\nClick anywhere to start.";
			levelDesc [3] = "LEVEL 4\n\nThe stars are not to be hit. Period.";
			levelDesc [4] = "LEVEL 5\n\nThe first combination.";
			levelDesc [5] = "LEVEL 6\n\nTry this!";
			levelDesc [6] = "LEVEL 7\n\nThis might be hard but the next will be fun.";
			levelDesc [7] = "LEVEL 8\n\nWow! Have fun!";
			levelDesc [8] = "LEVEL 9\n\nThis is possible.";
			levelDesc [9] = "LEVEL 10\n\nOne is the loneliest number. The grey Goobie knows...";
			levelDesc [10] = "LEVEL 11\n\nAll for one or one for all?";
			levelDesc [11] = "LEVEL 12\n\nEasy?";
			levelDesc [12] = "LEVEL 13\n\nAll together now!";
			levelDesc [13] = "LEVEL 14\n\nNow you really need all.";
			levelDesc [14] = "LEVEL 15\n\nYeah, one is all you need.";
			levelDesc [15] = "LEVEL 16\n\nPhew! This will be easy.";
			levelDesc [16] = "LEVEL 17\n\nTwo can be as bad as one...";
			levelDesc [17] = "LEVEL 18\n\nAll! This time for real.\nMass destruction!";
			levelDesc [18] = "LEVEL 19\n\nLast level. Then back to work.";
			levelDesc [19] = "DONE!\n\nCongratulations! Check out the Tutorial and other games at\n\nblog.natan.info"

			setupGame();

		}

This is the level design. The arrays blue, pink, grey and black hold the number of monomers, the killArray holds the number of enemies that must explode in the level. There is a random chance that after finishing level 19 there is no star in the mass of goobies.

		public function setupGame () {

			Mouse.show();
			this.addEventListener(Event.ENTER_FRAME, enterFrameHandler, false, 0, true);
			gameStarted = false;
			textOn = true;
			monomerKilled = 0;
			lostAnyway = false;
			monomerHolder = new MovieClip();
			bulletHolder = new MovieClip();
			this.addChild(monomerHolder);
			this.addChild(bulletHolder);
			this.addChild(monomerKillText);

			monomerHolder.mouseChildren = false;
			bulletHolder.mouseChildren = false;

			for (counter = 0; counter <= blueArray[levelNumber] -1 ; counter++ ) {
				monomer = new Monomer(this, "blue");
				monomer.x = Math.random()*640;
				monomer.y = Math.random()*480;
				monomer.mouseEnabled = false;
				monomerArray.push(monomer);
				monomerHolder.addChild(monomer);
			}

			for ( counter = 0; counter <= pinkArray[levelNumber] -1 ; counter++ ) {
				monomer = new Monomer(this, "pink");
				monomer.x = Math.random()*640;
				monomer.y = Math.random()*480;
				monomer.mouseEnabled = false;
				monomerArray.push(monomer);
				monomerHolder.addChild(monomer);
			}

			for ( counter = 0; counter <= greyArray[levelNumber] -1 ; counter++ ) {
				monomer = new Monomer(this, "grey");
				monomer.x = Math.random()*640;
				monomer.y = Math.random()*480;
				monomer.mouseEnabled = false;
				monomerArray.push(monomer);
				monomerHolder.addChild(monomer);
			}

			for ( counter = 0; counter <= blackArray[levelNumber] -1 ; counter++ ) {
				monomer = new Monomer(this, "black");
				monomer.x = Math.random()*640;
				monomer.y = Math.random()*480;
				monomer.mouseEnabled = false;
				monomerArray.push(monomer);
				monomerHolder.addChild(monomer);
			}

			levelText = new LevelText();
			levelText.textArea.text = levelDesc[levelNumber];
			this.addChild(levelText);
			levelText.addEventListener(MouseEvent.CLICK, removeText, false, 0, true);
		}

Every array is checked and the given number of monomers is placed to the holderClip (to completely remove everything and level end) and an array for hittesting purposes.

		public function removeText(event:MouseEvent) {
			levelText.removeEventListener(MouseEvent.CLICK, removeText);
			levelText.visible = false;
			stage.addEventListener(MouseEvent.CLICK, releaseMouseBullet, false, 0, true);
		}

The level description overlay is removed.


		public function releaseMouseBullet (event:MouseEvent) {
			if (textOn == true ) {
				textOn = false;
			}
			else {
				Mouse.hide();
				addVerticalBullet(mouseX, mouseY);
				stage.removeEventListener(MouseEvent.CLICK, releaseMouseBullet);
				gameStarted = true;
			}
		}

As it is a one-click-per-level game the listener is removed after the click.

		public function addVerticalBullet (startX, startY) {
			for (counter = 0; counter <= 3; counter++) {
				bulletDir = Math.PI * 0.5 * counter;
				bullet = new Bullet(this, bulletDir);
				bullet.x = startX;
				bullet.y = startY;
				bulletArray.push(bullet);
				bulletHolder.addChild(bullet);
			}
		}

		public function addDiagonalBullet (startX, startY) {
			for (counter = 0; counter <= 3; counter++ ) {
				bulletDir = Math.PI * 0.5 * (counter + 0.5);
				bullet = new Bullet(this, bulletDir);
				bullet.x = startX;
				bullet.y = startY;
				bulletArray.push(bullet);
				bulletHolder.addChild(bullet);
			}
		}

All you need is math: PI*0.5 means 90°.


		public function enterFrameHandler (event:Event) {
			for each ( bullet in bulletArray ) {
				bullet.moveBullet();
			}
			for each ( monomer in monomerArray ) {
				monomer.moveMonomer ();
			}
			bulletArrayLength.text = bulletArray.length.toString();
			monomerKillText.text = monomerKilled.toString()+" / "+killArray[levelNumber].toString();
			monomerArrayLength.text = monomerArray.length.toString();
			if(gameStarted == true ) {
				if(bulletArray.length == 0 || lostAnyway || monomerArray.length == 0) {
					if ( monomerKilled >= killArray[levelNumber] && !lostAnyway ) {
						if ( levelNumber < 19 ) {
							levelNumber++;
						}
					}
					this.removeChild(monomerHolder);
					this.removeChild(bulletHolder);
					monomerArray.splice(0, monomerArray.length);
					bulletArray.splice(0, bulletArray.length);
					setupGame();
				}
			}
		}

The game itself: at every frame (framerate set to 30) every bullet and monomer is moved. With mouse click you set the gameStarted to true. That means there are four bullets on stage, once all bullets are removed the game should be over OR if a star is hit lostAnyway is set true and the level is immediately over OR if all monomers are removed (and no star) the level is passed. As long as there are levels the number is increased. One could direct the player to the score table with an 'else if' at this point. After all, the stage is purged and the next level is set up.

		public function removeBullet (which) {
			if(bulletHolder.contains(which)){
				bulletHolder.removeChild (which);
				//which.alpha = 0.5;
				bulletArray.splice(bulletArray.indexOf(which), 1);
			}
		}

		public function removeMonomer (which) {
			if(monomerHolder.contains(which)){
				sfxSoundChannel = soundFail.play();
				monomerHolder.removeChild (which);
				//which.alpha = 0.5;
				monomerArray.splice(monomerArray.indexOf(which), 1);
				monomerKilled++;
			}
		}
		public function goToHomePage(event:ContextMenuEvent){
			var homeRequest = new URLRequest("http://blog.natan.info");
			navigateToURL(homeRequest, "_blank");
		}
	}
}

The Monomer Class includes PixelPerfectCollisionTesting by Troy Gilbert that is explained a bit at Michael's Blog.

package {
	import flash.display.MovieClip;

	public class Monomer extends MovieClip {

		public var clipRef:MovieClip;
		public var bulletDir:Number;
		public var xSpeed:Number;
		public var ySpeed:Number;
		public var sType:String;
		public var monomerDir:Number;

		public function Monomer (clipRef, sType:String = "blue") {
			this.clipRef = clipRef;
			this.sType = sType;
			this.gotoAndStop(this.sType);
			monomerDir = Math.PI * Math.random() * 2;
			this.rotation = this.monomerDir*180/Math.PI;
			this.xSpeed = Math.cos(monomerDir)*1.5;
			this.ySpeed = Math.sin(monomerDir)*1.5;
			//this.scaleX = this.scaleY = 0.75;
		}

		public function moveMonomer () {

			this.x += this.xSpeed;
			this.y += this.ySpeed;

			for each ( var bullet in clipRef.bulletArray ) {

				//if ( this.hitTestPoint(bullet.x, bullet.y, true )) {
				if (PPCD.isColliding(this, bullet, clipRef, true)){
					if ( this.sType == "blue" ) {
						if(clipRef.bulletArray.length < 150){
						clipRef.addVerticalBullet(this.x, this.y);
						}
					}
					if ( this.sType == "pink" ) {
						clipRef.lostAnyway = true;
					}
					if ( this.sType == "grey" ) {
						if(clipRef.bulletArray.length < 150){
						clipRef.addDiagonalBullet(this.x, this.y);
						}
					}
					if ( this.sType == "black" ) {
						if(clipRef.bulletArray.length < 150){
						clipRef.addVerticalBullet(this.x, this.y);
						clipRef.addDiagonalBullet(this.x, this.y);
						}
					}
					clipRef.removeMonomer(this);
				}
			}

			if ( this.x < -20 ) { this.x = 660; }
			if ( this.x > 660 ) { this.x = -20; }
			if ( this.y < -20 ) { this.y = 500; }
			if ( this.y > 500 ) { this.y = -20; }
		}
	}
}

sType is used to set up the color of the monomer. The Monomer clip has four frames labelled 'blue', 'pink' etc. If everything should be animated then I would recommend using an extra 'monomerGrapic' clip and add this to the monomer (which then holds no graphic itself'.

And the Bullet Class:

package {
	import flash.display.MovieClip;

	public class Bullet extends MovieClip {

		public var clipRef:MovieClip;
		public var bulletDir:Number;
		public var xSpeed:Number;
		public var ySpeed:Number;

		public function Bullet (clipRef, bulletDir) {
			this.clipRef = clipRef;
			this.bulletDir = bulletDir;
			this.xSpeed = Math.cos(bulletDir)*5;
			this.ySpeed = Math.sin(bulletDir)*5;
		}

		public function moveBullet () {
			this.x += this.xSpeed;
			this.y += this.ySpeed;

			if ( this.x < 0 ) { clipRef.removeBullet(this); }
			if ( this.x > 640 ) { clipRef.removeBullet(this); }
			if ( this.y < 0 ) { clipRef.removeBullet(this); }
			if ( this.y > 480 ) { clipRef.removeBullet(this); }
		}
	}
}

Myke them bounce or add gravity, lots of possibilities.

And that is the final result:

There are unlimited possibilities: As the score you could count the number of tries, the time needed, the number of exploded goobies, etc.

Well, you can come up with questions, mateys.

This entry was posted in game development, mochiads and tagged , , , , , , . Bookmark the permalink.