2D Collision for 3D: Rectangles

the simple idea

When developing a game in 3D, collision may seem at first a seven headed beast. At least, definitely, a three headed beast. Even though the math is almost always the same.

For instance, Circle collision in 3D can be easily done, just by adding Z to the Pythagorean theorem, so from the 2D:

Math.pow(c2.x - c1.x, 2) + Math.pow(c2.y - c1.y, 2) <= Math.pow(c1.r + c2.r, 2)

Where c1 and c2 are the center points for two circles and c1.r and c2.r are the radii of the two circles.

In 3D you add Z:

Math.pow(c2.x - c1.x, 2) + Math.pow(c2.y - c1.y, 2) + Math.pow(c2.z - c1.z, 2) <= Math.pow(c1.r + c2.r, 2)

Where c1 and c2 are the Vector3D objects marking the center of two spheres.

It's when a 3D Physics engine is needed that problems really start. But often enough you won't need any of that. If you've seen my Isometric tutorials or if you have developed anything in Isometric rendering, you have already faced the main peculiarities of writing for 3D rendering. Namely, Y axis means altitude in 3D, and the 2D Y axis can be treated as the Z axis.

So in this first tutorial I will show you what this means with a very simple rectangle collision. I will replicate the following 2D collision detection, but in 3D:

Here is the code for the 2D collision. It is very simple, so just take some time to see how collision is detected in the main loop:

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	
	[SWF(width="800", height="600", backgroundColor="0xFFFFFF", frameRate="30")]
	public class Away3DYRectangle extends Sprite
	{
		private var box1:Sprite;
		private var box2:Sprite;
		private var box3:Sprite;
		private var yVel:int = 1;
		
		public function Away3DYRectangle() {
		
			x = 350;
			y = 250;
			
			box1 = new Sprite ();
			box1.graphics.beginFill(0xCCCCCC);
			box1.graphics.drawRect(-25,-50, 50, 100);
			box1.graphics.endFill();
			box1.y = -100;
			addChild(box1);
			
			box2 = new Sprite ();
			box2.graphics.beginFill(0xCCCCCC);
			box2.graphics.drawRect(-50,-25, 100, 50);
			box2.graphics.endFill();
			box2.y = -200;
			addChild(box2);
			
			
			box3 = new Sprite ();
			box3.graphics.beginFill(0xCCCCCC);
			box3.graphics.drawRect(-50,-25, 100, 50);
			box3.graphics.endFill();
			box3.y = 200;
			addChild(box3);
			
			addEventListener(Event.ENTER_FRAME, onLoop, false, 0, true);
			
		}
		
		private function onLoop (event:Event):void {
			var nextY:Number = box1.y + 10*yVel;
			
			if (box1.y >= box2.y + box2.height/2 + box1.height/2 && nextY <= box2.y + box2.height/2  + box1.height/2 ) {
				nextY = box2.y + box2.height/2  + box1.height/2 ;
				yVel *= -1;
			}
			
			if (box1.y <= box3.y - box3.height/2 - box1.height/2 && nextY >= box3.y - box3.height/2  - box1.height/2 ) {
				nextY = box3.y - box3.height/2  - box1.height/2 ;
				yVel *= -1;
			}
			
			box1.y = nextY;
		}
		
	}
}

Now, when changing this type of collision you can do it in two ways. One way is by changing the 2D Y value to 3D Z. Altitude is not important for this "game" so the 3D Y axis can be ignored:

Here is the code for it:

package  {
	
	
	import away3d.cameras.*;
	import away3d.containers.*;
	import away3d.materials.*;
	import away3d.primitives.*;
	
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.*;
	
	[SWF(width="800", height="600", backgroundColor="0xFFFFFF", frameRate="30")]
	
	public class Away3DCollision extends Sprite {
		
		private var box1:Rectangle;
		private var box2:Rectangle;
		private var box3:Rectangle;
		
		private var _view:View3D;
		private var _cube1:Cube;
		private var _cube2:Cube;
		private var _cube3:Cube;
		
		private var _container:ObjectContainer3D;
		private var zVel:int = 1;
		private var temp:Sprite;
		
		
		
		function Away3DCollision (){
			
			//create 2D elements
			box1 = new Rectangle (0, 100, 50, 100);
			box1.offset(-25, -50);
			box2 = new Rectangle (0, 200, 100, 50);
			box2.offset(-50, -25);
			box3 = new Rectangle (0, -200, 100, 50);
			box3.offset(-50, -25);
			
			
			_container = new ObjectContainer3D();
			
			var scene:Scene3D = new Scene3D();
			
			var camera:Camera3D = new Camera3D({y:300, z:-300, zoom:15, focus:30});
			camera.lookAt(new Vector3D(0,0,0));
	
			_view = new View3D({scene:scene, camera:camera});
			_view.x = 400;
			_view.y = 300;
			addChild(_view);			
			
			var material:WireColorMaterial = new WireColorMaterial(0x990000,{wireColor:0x484848});
			
			_cube1 = new Cube({material:material, width:box1.width,height:box1.width,depth:box1.height});
			_cube1.z = box1.y;
			_view.scene.addChild(_cube1);
			
			
			_cube2 = new Cube({material:material, width:box2.width,height:box2.height,depth:box2.height});
			_cube2.z = box2.y;
			_view.scene.addChild(_cube2);
			
			
			_cube3 = new Cube({material:material, width:box3.width,height:box3.height,depth:box3.height});
			_cube3.z = box3.y;
			_view.scene.addChild(_cube3);
			
			addEventListener(Event.ENTER_FRAME, onLoop, false, 0, true);
		}
		
		
		private function onLoop (event:Event):void {
			
			var nextZ:Number = _cube1.z + 10*zVel;
			
			if (_cube1.z <= _cube2.z - _cube2.depth/2 - _cube1.depth/2 && nextZ >= _cube2.z - _cube2.depth/2 - _cube1.depth/2  ) {
				nextZ = _cube2.z - _cube2.depth/2 - _cube1.depth/2;
				zVel *= -1;
			}
			
			if (_cube1.z >= _cube3.z + _cube3.depth/2 + _cube1.depth/2 && nextZ <= _cube3.z + _cube3.depth/2 + _cube1.depth/2  ) {
				nextZ = _cube3.z + _cube3.depth/2 + _cube1.depth/2;
				zVel *= -1;
			}
			_cube1.z = nextZ;
			
			_view.camera.x = stage.mouseX - 400;
			_view.camera.lookAt(new Vector3D(0,0,0));
			
			
			_view.render();
			
		}
		
	}
	
}

Or, if you picture the 2D version of this "game" as a painting on a wall, you can simply flip that painting on its 3D X axis and you have a 3D game which uses the same collision code from the 2D logic. The example shown on the top of this page uses this method.

Here is the code for this version:

package
{
	import away3d.cameras.*;
	import away3d.containers.*;
	import away3d.lights.*;
	import away3d.materials.*;
	import away3d.primitives.*;
	
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.Vector3D;
	
	
	[SWF(width="800", height="600", backgroundColor="0x222222", frameRate="30")]
	public class Away3DYCollision extends Sprite
	{
		private var yVel:int = 1;
		
		private var _view:View3D;
		private var _cube1:Cube;
		private var _cube2:Cube;
		private var _cube3:Cube;
		
		private var _container:ObjectContainer3D;
		
		public function Away3DYCollision() {
			
			_container = new ObjectContainer3D();
			var scene:Scene3D = new Scene3D();
			var camera:Camera3D = new Camera3D({zoom:15, focus:30,  y:300, z:-300});
			camera.lookAt(new Vector3D(0,0,0));
			
			_view = new View3D({scene:scene, camera:camera});
			_view.x = 400;
			_view.y = 300;
			addChild(_view);
			
			var light : PointLight3D = new PointLight3D(); 
			light.position = new Vector3D(500,0, -500);
			light.color = 0xFF9900; 
			light.diffuse = 0.7;
			light.brightness = 4;
			_view.scene.addLight(light);
			
			var material:ShadingColorMaterial = new ShadingColorMaterial(); 
			material.ambient = 0xFF9900; 
			material.diffuse = 0xFF9900; 
			material.specular = 0xFF9900;
			
			_cube1 = new Cube({material:material, width:50,height:100,depth:50});
			_cube1.y = -100;
			_container.addChild(_cube1);
			
			_cube2 = new Cube({material:material, width:100,height:50,depth:50});
			_cube2.y = -200;
			_container.addChild(_cube2);
			
			_cube3 = new Cube({material:material, width:100,height:50,depth:50});
			_cube3.y = 200;
			_container.addChild(_cube3);
			
			_view.scene.addChild(_container);
			
			//rotate the container so it looks 3D
			_container.rotationX = 90;
			
			addEventListener(Event.ENTER_FRAME, onLoop, false, 0, true);
			
		}
		
		private function onLoop (event:Event):void {
			
			var nextY:Number = _cube1.y + 10*yVel;
			
			if (_cube1.y >= _cube2.y + _cube2.height/2 + _cube1.height/2 && nextY <= _cube2.y + _cube2.height/2  + _cube1.height/2 ) {
				nextY = _cube2.y + _cube2.height/2  + _cube1.height/2 ;
				yVel *= -1;
			}
			
			if (_cube1.y <= _cube3.y - _cube3.height/2 - _cube1.height/2 && nextY >= _cube3.y - _cube3.height/2  - _cube1.height/2 ) {
				nextY = _cube3.y - _cube3.height/2  - _cube1.height/2 ;
				yVel *= -1;
			}
			
			_cube1.y = nextY;
			
			_view.camera.x = (stage.mouseX - 400)*0.3;
			_view.camera.lookAt(new Vector3D(0,0,0));
			
			_view.render();
		}
		
	}
}

This might not be very impressive but you'd be amazed at how many 3D racing car games you could build using this logic.