Neural Storm

Games


Breakout

Breakout

One of the earliest games I remember playing, other than pong, back in the dark ages (pre interwebs, social media, mmorpgs); when FPS were live action games of "cops and robbers" with the neighborhood kids; was Breakout. Well to be honest, I remember many earlier games such as "Prince of Persia" on the Commmodore 64 but that is really taking a ride on the Way Back machine.

I decided to start this journey into game creation with rebuilding the basic mechanics of Breakout using javascript and Three.js. The purpose of this game is to illustrate a simple game loop, scene composition, camera placement, and very basic physics. The physics code could be better but I believe in KISS (Keep it Simple Stupid) and get it working first and then make it better.

To start the game, launch it from the thumbnail, once page is loaded press left mouse button once to release the ball.

Code

(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
	typeof define === 'function' && define.amd ? define(['exports'], factory) :
	(factory((global.BREAKOUT = global.BREAKOUT || {})));
}(this, (function (exports) { 'use strict';

	/*****************************************************************************
	*	Paddle
	*****************************************************************************/
	const PADDLE_WIDTH = 1;
	const PADDLE_HEIGHT = 0.25;
	const PADDLE_COLOR = 'cyan';
	const PADDLE_INITIAL_X = 0;
	const PADDLE_INITIAL_Y = -7;

	function Paddle() {

		this.paddle = new THREE.Object3D();
		this.paddle.position.x = PADDLE_INITIAL_X;
		this.paddle.position.y = PADDLE_INITIAL_Y;
	}

	Paddle.prototype = {

		constructor: Paddle,

		update : function( x ) {

			this.paddle.position.x = x;	
		},

		draw : function( sceneGraph ) {

			var paddleGeometry = new THREE.PlaneGeometry( PADDLE_WIDTH, PADDLE_HEIGHT );
			var paddleMaterial = new THREE.MeshBasicMaterial( { color: PADDLE_COLOR, shading: THREE.SmoothShading, transparent: true, opacity: 1.0 } );
			var paddleMesh = new THREE.Mesh( paddleGeometry, paddleMaterial );
			this.paddle.add( paddleMesh );
			sceneGraph.add( this.paddle );
		},

		getX : function() {

			return this.paddle.position.x;
		},

		getY : function() {

			return this.paddle.position.y;
		},

		getWidth : function() {

			return PADDLE_WIDTH;
		},

		bounce : function( ball ) {

			// TODO: Adjust bounce angle based on far off center the ball strikes the paddle
			
			var paddleBox = this.getBoundingBox();

			if ( paddleBox.intersectsBox( ball.getBoundingBox() ) ) {

				var currentAngle = ball.getAngle();

				if ( currentAngle < ( 1.5 * Math.PI ) ) {

					ball.setAngle( currentAngle - Math.PI / 2 );
				}
				else {

					ball.setAngle( currentAngle + Math.PI / 2 );
				}
			}
		},

		getBoundingBox : function() {

			return new THREE.Box3().setFromObject( this.paddle );
		}
	}

	/*****************************************************************************
	*	Ball
	*****************************************************************************/
	const BALL_RADIUS = 0.25;
	const BALL_SEGMENTS = 50;
	const BALL_INITIAL_VELOCITY = 0.1;
	const BALL_INITIAL_ANGLE = Math.PI / 3;
	const BALL_COLOR = 'red';

	function Ball() {

		this.ball = new THREE.Object3D();
		this.ball.position.x = PADDLE_INITIAL_X;
		this.ball.position.y = PADDLE_INITIAL_Y + ( PADDLE_HEIGHT / 2 ) + BALL_RADIUS;

		this.velocity = BALL_INITIAL_VELOCITY;
		this.angle = BALL_INITIAL_ANGLE;
		this.released = false;
	}

	Ball.prototype = {

		constructor: Ball,

		update : function( deltaTime ) {

			if ( undefined !== deltaTime ) {

				this.ball.position.x += Math.cos( this.angle ) * BALL_INITIAL_VELOCITY * deltaTime;
				this.ball.position.y += Math.sin( this.angle ) * BALL_INITIAL_VELOCITY * deltaTime;
			}
		},

		draw : function( sceneGraph ) {

			var ballGeometry = new THREE.CircleGeometry( BALL_RADIUS, BALL_SEGMENTS );
			var ballMaterial = new THREE.MeshBasicMaterial( { color: BALL_COLOR, shading: THREE.SmoothShading, transparent: true, opacity: 1.0 } );
			var ballMesh = new THREE.Mesh( ballGeometry, ballMaterial );
			this.ball.add( ballMesh );
			sceneGraph.add( this.ball );
		},

		getX : function() {

			return this.ball.position.x;
		},

		setX : function( x ) {

			this.ball.position.x = x;
		},

		getY : function() {

			return this.ball.position.y;
		},

		setAngle : function( angle ) {

			if ( undefined !== angle ) {
				
				if ( angle < 0 ) {
	
					this.angle = angle + ( Math.PI * 2 );
				}
				else if ( angle > ( Math.PI * 2 ) ) {
	
					this.angle = angle - ( Math.PI * 2 );
				}
				else {

					this.angle = angle;
				}
			}
		},

		getAngle : function() {

			return this.angle;	
		},

		release : function() {

			this.released = true;
			this.velocity = BALL_INITIAL_VELOCITY;
		},

		isReleased : function() {

			return this.released;
		},

		getBoundingBox : function() {

			return new THREE.Box3().setFromObject( this.ball );
		},

		reset : function() {

			this.ball.position.x = PADDLE_INITIAL_X;
			this.ball.position.y = PADDLE_INITIAL_Y + ( PADDLE_HEIGHT / 2 ) + BALL_RADIUS;

			this.velocity = BALL_INITIAL_VELOCITY;
			this.angle = BALL_INITIAL_ANGLE;
			this.released = false;			
		}
	}

	/*****************************************************************************
	*	Brick
	*****************************************************************************/
	const BRICK_WIDTH = 1;
	const BRICK_HEIGHT = 0.50;
	const BRICK_DEFAULT_COLOR = 'white';
	const BRICK_DEFAULT_X = 0;
	const BRICK_DEFAULT_Y = 0;

	function Brick( brickColor, x, y ) {

		this.brick = new THREE.Object3D();

		this.brick.position.x = BRICK_DEFAULT_X;
		this.brick.position.y = BRICK_DEFAULT_Y;

		if ( undefined !== x ) {

			this.brick.position.x = x;
		}

		if ( undefined !== y ) {

			this.brick.position.y = y;
		}

		this.brickColor = BRICK_DEFAULT_COLOR;

		if ( undefined !== brickColor ) {

			this.brickColor = brickColor;
		}

	}

	Brick.prototype = {

		constructor: Brick,

		draw : function( sceneGraph ) {

			var brickGeometry = new THREE.PlaneGeometry( BRICK_WIDTH, BRICK_HEIGHT );
			var brickMaterial = new THREE.MeshBasicMaterial( { color: this.brickColor } );
			var brickMesh = new THREE.Mesh( brickGeometry, brickMaterial );
			this.brick.add( brickMesh );
			sceneGraph.add( this.brick );
		},
	}

	/*****************************************************************************
	*	BrickWall
	*****************************************************************************/
	const BRICK_WALL_GAP = 0.1;
	const DEFAULT_NUM_BRICKS_WIDE = 18;
	const DEFAULT_NUM_BRICKS_HIGH = 9;

	function BrickWall( numBricksWide, numBricksHigh ) {

		this.brickWall = new THREE.Object3D();
		this.numBricksWide = DEFAULT_NUM_BRICKS_WIDE;
		this.numBricksHigh = DEFAULT_NUM_BRICKS_HIGH;

		if ( undefined !== numBricksWide ) {

			this.numBricksWide = numBricksWide;
		}

		if ( undefined !== numBricksHigh ) {

			this.numBricksHigh = numBricksHigh;
		}

		this.totalWidth = this.numBricksWide + ( this.numBricksWide * BRICK_WALL_GAP );
		this.totalHeight = this.numBricksHigh + ( this.numBricksHigh * BRICK_WALL_GAP );
	}

	BrickWall.prototype = {

		constructor: BrickWall,

		draw : function( sceneGraph ) {

			var minX = 0;
			var maxX = 0;
			var minY = 0;
			var maxY = 0;

			var posY = this.totalHeight;

			var hue = Math.random() - 0.5;

			var hueStep = ( 1.0 - hue ) / this.numBricksHigh;

			for ( var row = 0; row < this.numBricksHigh; ++row ) {			

				var posX = -( this.totalWidth / 2 ) + BRICK_WIDTH / 2;

				// Change color on row
				var color = new THREE.Color();
				color.setHSL( hue, 0.6, 0.8 );

				for ( var column = 0; column < this.numBricksWide; ++column ) {

					// Create a new brick with the given color and position
					var brick = new Brick( color, posX, posY );
					
					// Draw the brick adding it to the wall
					brick.draw( this.brickWall );
					
					// Set X position for next brick
					posX += BRICK_WIDTH + BRICK_WALL_GAP;
				}

				// Set the Y position for the new row of bricks
				posY -= BRICK_HEIGHT + BRICK_WALL_GAP;

				hue += hueStep;
			}

			sceneGraph.add( this.brickWall );
		},

		bricksBroken : function( ball ) {

			var bricksToRemove = [];

			var ballBox = ball.getBoundingBox();
			
			for ( var idx = 0; idx < this.brickWall.children.length; ++ idx ) {

				var brickBox = new THREE.Box3().setFromObject( this.brickWall.children[ idx ] );

				if ( ballBox.intersectsBox( brickBox ) ) {

					bricksToRemove.push( this.brickWall.children[ idx ] );
				}
			}

			if ( 0 < bricksToRemove.length ) {

				for ( var idx = 0; idx < bricksToRemove.length; ++ idx ) {

					this.brickWall.remove( bricksToRemove[ idx ] );
				}

				var brickBox = new THREE.Box3().setFromObject( bricksToRemove[ 0 ] );

				var angle = ball.getAngle();

				if ( ( 0 < angle ) && ( angle <= Math.PI / 2 ) ) {
						
					if ( ballBox.max.x <= brickBox.min.x ) {

						ball.setAngle( angle + Math.PI / 2 );
					}
					else {
					
						ball.setAngle( Math.PI * 2 - angle );
					}
				}
				else if ( ( Math.PI / 2 < angle ) && ( angle <= Math.PI ) ) {
						
					if ( ballBox.min.x >= brickBox.max.x ) {

						ball.setAngle( angle - Math.PI / 2 );
					}
					else {
					
						ball.setAngle( Math.PI * 2 - angle );
					}
				}
				else if ( ( Math.PI < angle) && ( angle <= 1.5 * Math.PI ) ) {

					if ( ballBox.min.x >= brickBox.max.x ) {

						ball.setAngle( angle + Math.PI / 2 );
					}
					else {
					
						ball.setAngle( Math.PI * 2 - angle );
					}
				}
				else {

					if ( ballBox.max.x <= brickBox.min.x ) {

						ball.setAngle( angle - Math.PI / 2 );
					}
					else {
					
						ball.setAngle( angle - Math.PI / 2 );
					}					
				}
			}
				
			return bricksToRemove.length;
		},

		getWidth : function() {

			this.totalWidth;
		},

		getHeight : function() {

			this.totalHeight;
		}
	}

	/*****************************************************************************
	*	Breakout
	*****************************************************************************/	
	const DEFAULT_NUM_BALLS = 1;

	function Breakout( numBricksWide, numBricksHigh ) {

		this.brickWall = new BrickWall( numBricksWide, numBricksHigh );
		this.paddle = new Paddle();
		this.ball = new Ball();
		this.score = 0;
		this.bounds =  { left : 0, bottom : 0, right : 0, top : 0 };
		this.sceneElements = new THREE.Object3D();
		this.numBalls = DEFAULT_NUM_BALLS;
	}

	Breakout.prototype = {

		constructor: Breakout,

		start : function() {

			this.ball.release();
		},

		end : function() {
		},

		draw : function( scene ) {

			// Add the game components to the scene graph
			this.brickWall.draw( this.sceneElements );
			this.ball.draw( this.sceneElements );
			this.paddle.draw( this.sceneElements );

			scene.add( this.sceneElements );
		},

		calculateBounds : function() {

			// Calculate a bounding box for the scene graph
			var box = new THREE.Box3().setFromObject( this.sceneElements );

			// Center the scene graph
			box.getCenter( this.sceneElements.position );

			// Adjust scene graph
			this.sceneElements.localToWorld( box );
			this.sceneElements.position.multiplyScalar( -1 );

			// Calculate the extent of the scene by finding the furthest points from the origin
			this.bounds.right = Math.max( Math.abs( box.max.x ), Math.abs( box.min.x ) );
			this.bounds.left = -this.bounds.right;
			this.bounds.top = Math.max( Math.abs( box.max.y ), Math.abs( box.min.y ) );
			this.bounds.bottom = -this.bounds.top;
			return this.bounds;
		},

		update : function() {

			if ( ( undefined !== this.ball ) && ( this.ball.isReleased() ) ) {

				this.ball.update( /* this.clock.getDelta() */ 1 );

				// Detect collisions

				// Ball Collision with boundaries?
				if ( this.ball.getX() + BALL_RADIUS > this.bounds.right ) {

					this.ball.setAngle( Math.PI - this.ball.getAngle() );
				}
				else if ( this.ball.getX() - BALL_RADIUS < this.bounds.left ) {
	
					this.ball.setAngle( Math.PI - this.ball.getAngle() );
				}
				else if ( this.ball.getY() + BALL_RADIUS> this.bounds.top - BALL_RADIUS ) {

					this.ball.setAngle( 2 * Math.PI - this.ball.getAngle() );
				}
				else if ( this.ball.getY() <= this.bounds.bottom ) {

					--this.numBalls;

					this.ball.reset();
				}
				else {

					// Ball collision with Bricks?
					this.score += this.brickWall.bricksBroken( this.ball ) * 10;

					// Ball collision with Paddle?
					this.paddle.bounce( this.ball );
				}
			}
		},

		mouseMove : function( event ) {

			var scale = ( -this.bounds.left + this.bounds.right ) / window.innerWidth;

			var x = ( event.clientX - window.innerWidth / 2 ) * scale;
			
			// var y = ( event.clientY - window.innerHeight / 2 ) * scale

			if ( ( this.bounds.left + this.paddle.getWidth() / 2 < x ) && 
			     ( x < this.bounds.right - this.paddle.getWidth() / 2  ) ) {

				this.paddle.update( x );				

				if ( ( undefined !== this.ball ) && ( false === this.ball.isReleased() ) ) {

					this.ball.setX( x );
				}
			}

		},

		mouseDown : function( event ) {

			this.start();
		}
	}

	/*****************************************************************************
	*	Exports
	*****************************************************************************/
	
	exports.Breakout		= Breakout;

})));

Lightning

lightning

Ok, so this is not a game but may be useful in some games...specially those in which you want to create some environmental effects! The lightning flashes are animated and procedurally generated. The generation includes random forking, point of origin, and terminal point. The animation in this particular case deals with the fading effect, a gradual toning down effect using increasing transparency to fade the bolts.

The magic behind this code is an article on Drilian's House of Game Development. The author does an excellent job of illustrating how this procedurally generated lightning is created.

Code

(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
	typeof define === 'function' && define.amd ? define(['exports'], factory) :
	(factory((global.LIGHTNING = global.LIGHTNING || {})));
}(this, (function (exports) { 'use strict';

	/*****************************************************************************
	*	Lightning
	*****************************************************************************/	
	const OPACITY = 1.0;
	const COLOR = 'white';

	function Lightning() {

		this.sceneElements = new THREE.Object3D();

		this.materials = [];

		this.bounds = { right : 0, left : 0, top : 0, bottom : 0 };

		this.parentScene = undefined;
	}

	Lightning.prototype = {

		constructor: Lightning,

		start : function() {

		},

		end : function() {
		},

		drawBolt : function() {

			// Add the game components to the scene graph

			// Pick a point at the top of the scene
			const origin = new THREE.Vector3( Math.random() * 2 - 1, 1, 0 );
	
			var startPoint = origin;
	
			// Create a bolt striking downward
			var x = Math.random() * 2 - 1;
			var y = -1;
			var z = 0;
			var endPoint = new THREE.Vector3( x, y, z );
	
			var segments = [];
	
			segments.push( new THREE.Line3( startPoint, endPoint ) );
	
			var offsetAmount = 0.5;
	
			var numGenerations = 7;
	
			// For each generation (some number of generations) 
			for ( var generation = 0; generation < numGenerations; ++generation ) {
	
				var numSegments = segments.length;
		
				// For each segment that was in segmentList when this generation started 
				for ( var segmentIdx = 0; segmentIdx < numSegments; ++segmentIdx ) {
		
					// Pop the segment to process.
					var segment = segments[ 0 ];
			
					segments.splice( 0 , 1 );
			
					// Get the midpoint of the segment
					var midPoint = segment.getCenter();
			
					midPoint.x += ( 1 - Math.random() * 2 ) * offsetAmount * Math.cos( Math.random() *  15 * ( Math.PI / 180 ) );
					midPoint.y += ( 1 - Math.random() * 2 ) * offsetAmount * Math.sin( Math.random() *  15 * ( Math.PI / 180 ) );
			
					// Create two new segments that span from the start point to the end point, 
					// but with the new (randomly-offset) midpoint.
					segments.push( new THREE.Line3( segment.start, midPoint ) );
					segments.push( new THREE.Line3( midPoint, segment.end ) );
			
					// Fork ?
					if ( Math.random() > 0.7 ) {
			
						var lengthScale = 0.7 * Math.random();
						var sign = ( Math.random() < 0.5 ) ? 1 : -1;
						var splitEnd = new THREE.Vector3( midPoint.x + ( sign * lengthScale * segment.distance() ), midPoint.y - ( lengthScale * segment.distance() ), 0 );
						segments.push( new THREE.Line3( midPoint, splitEnd ) ); 
					} 
				}
			
				// Each subsequent generation offsets at max half as much as the generation before.
				offsetAmount /= 2;
			}


			var bolt = new THREE.Object3D();

			var material = new THREE.LineBasicMaterial( { transparent: true, opacity: OPACITY, color: COLOR, shading: THREE.SmoothShading } );
		
			for ( var segmentIdx = 0; segmentIdx < segments.length; ++segmentIdx ) {
		
				var geometry = new THREE.Geometry();
		
				geometry.vertices.push( segments[ segmentIdx ].start );
		
				geometry.vertices.push( segments[ segmentIdx ].end );
		
				var line = new THREE.Line( geometry, material );
		
				bolt.add( line );
			}

			this.materials.push( material );

			this.sceneElements.add( bolt );

			this.parentScene.add( this.sceneElements );
		},

		draw : function( scene ) {

			this.parentScene = scene;

			this.drawBolt();
		},

		calculateBounds : function() {

			// Calculate a bounding box for the scene graph
			var box = new THREE.Box3().setFromObject( this.sceneElements );

			// Center the scene graph
			box.getCenter( this.sceneElements.position );

			// Adjust scene graph
			this.sceneElements.localToWorld( box );
			this.sceneElements.position.multiplyScalar( -1 );

			// Calculate the extent of the scene by finding the furthest points from the origin
			this.bounds.right = Math.max( Math.abs( box.max.x ), Math.abs( box.min.x ) );
			this.bounds.left = -this.bounds.right;
			this.bounds.top = Math.max( Math.abs( box.max.y ), Math.abs( box.min.y ) );
			this.bounds.bottom = -this.bounds.top;
			return this.bounds;
		},

		update : function() {

			if ( this.sceneElements.children.length > 0 ) {

				// Walk the array removing and disposing each element
				var idx = this.sceneElements.children.length;

				do {
				
					--idx;

					this.materials[ idx ].opacity *= 0.95;
					
					if ( this.materials[ idx ].opacity <= 0.01 ) {

						var bolt = this.sceneElements.children[ idx ];

						this.sceneElements.remove( bolt );
						this.materials.splice( idx, 1 );
												
						for ( var segmentIdx = 0; segmentIdx < bolt.children.length; ++segmentIdx ) {
						
							bolt.children[ segmentIdx ].material.dispose();
							bolt.children[ segmentIdx ].geometry.dispose();
						}
					
						bolt = null;
					}
					
				} while ( idx > 0 );
			}

			if ( Math.random() < 0.025 ) {

				this.drawBolt();
			}
		},
	}

	/*****************************************************************************
	*	Exports
	*****************************************************************************/
	
	exports.Lightning		= Lightning;

})));