Quick QuickBox2D Tip II – Collision Detection
Once again, I found myself playing around with the excellent QuickBox2D library the other day and whipped up something I needed but thought might help others as well. Actually this tip ain’t all that quick, but it’s not a massive undertaking and could come in quite handy. In any case, we’ll take a look at a way of detecting collisions with QuickBox2D. Now, of course as far as animations go, QB2D (or Box2D, actually) handles all the collisions for you behind the scenes so you don’t really need to think about it. But what if you wanted to create a pool game where the balls click when knocking against each other, or you wanted to remove a game object when it collides with something bad, or – well there are any number reasons you might want to check for collisions when working with a 2D physics engine.
I’m going to assume that if you’re using QuickBox2D, you already have Box2D in your classpath. Now, in Box2D the item responsible for handling custom collision detection is the Box2D.Dynamics.b2ContactListener class which looks like this:
/* * Copyright (c) 2006-2007 Erin Catto http://www.gphysics.com * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ package Box2D.Dynamics{ import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Dynamics.Contacts.*; import Box2D.Dynamics.*; import Box2D.Common.Math.*; import Box2D.Common.*; /// Implement this class to get collision results. You can use these results for /// things like sounds and game logic. You can also get contact results by /// traversing the contact lists after the time step. However, you might miss /// some contacts because continuous physics leads to sub-stepping. /// Additionally you may receive multiple callbacks for the same contact in a /// single time step. /// You should strive to make your callbacks efficient because there may be /// many callbacks per time step. /// @warning The contact separation is the last computed value. /// @warning You cannot create/destroy Box2D entities inside these callbacks. public class b2ContactListener { /// Called when a contact point is added. This includes the geometry /// and the forces. public virtual function Add(point:b2ContactPoint) : void{}; /// Called when a contact point persists. This includes the geometry /// and the forces. public virtual function Persist(point:b2ContactPoint) : void{}; /// Called when a contact point is removed. This includes the last /// computed geometry and forces. public virtual function Remove(point:b2ContactPoint) : void{}; /// Called after a contact point is solved. public virtual function Result(point:b2ContactResult) : void{}; }; }
We’ll be extending that class, so you won’t really need to worry anything about it (you don’t even need to worry about those ‘virtual’ method attributes – we’ll just extend right over top of that and Flash won’t even care one way or the other).
Before we extend the contact listener, though, let’s first create a collision event for QuickBox2D. To do so, create a new package/directory inside the com.actionsnippet package named ‘collisions’. Within there, create another directory named ‘events’. Finally, inside that events directory, we’ll create the event class named QBCollisionEvent shown below.
package com.actionsnippet.collisions.events { import Box2D.Dynamics.b2Body; import flash.events.Event; /** * Collision event for QuickBox2D collision handling * @author Devon O. */ public class QBCollisionEvent extends Event { public static const ADD:String = "add"; public static const REMOVE:String = "remove"; public static const PERSIST:String = "persist"; public static const RESULT:String = "result"; private var _body1:b2Body; private var _body2:b2Body; public function QBCollisionEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) { super(type, bubbles, cancelable); } public override function clone():Event { var evt:QBCollisionEvent = new QBCollisionEvent(type, bubbles, cancelable); evt.body1 = _body1; evt.body2 = _body2; return evt; } public override function toString():String { return formatToString("QBCollisionEvent", "type", "bubbles", "cancelable", "eventPhase"); } public function get body1():b2Body { return _body1; } public function set body1(value:b2Body):void { _body1 = value; } public function get body2():b2Body { return _body2; } public function set body2(value:b2Body):void { _body2 = value; } } }
Notice that this event sports four types (ADD, REMOVE, PERSIST, and RESULT), to mirror the different methods of the b2ContactListener class (read the comments in that class to get an idea of what each is and does). Also, check out that there are two body properties in this event (body1 and body2) which are instances of the b2Body object from Box2D. Obviously, in any given collision, there are at least two objects involved. These two properties just give quick and easy access to those two objects. You can use these to determine which items collided and, if necessary, get details like velocity, mass, position, etc. There’ll be an example down below to see what I mean.
Now that we have our event done, move up one directory to com.actionsnippet.collisions and we’ll create the QBContactListener class which will extend the b2ContactListener and implement IEventDispatcher (in order to dispatch our QBCollisionEvent).
package com.actionsnippet.collisions { import Box2D.Collision.b2ContactPoint; import Box2D.Dynamics.b2ContactListener; import Box2D.Dynamics.Contacts.b2ContactResult; import com.actionsnippet.collisions.events.QBCollisionEvent; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.IEventDispatcher; /** * Contact listener for QuickBox2D - for handling custom collision detection * @author Devon O. */ public class QBContactListener extends b2ContactListener implements IEventDispatcher { private var _dispatcher:EventDispatcher; public function QBContactListener() { _dispatcher = new EventDispatcher(this); } override public function Add(point:b2ContactPoint):void { super.Add(point); var evt:QBCollisionEvent = new QBCollisionEvent(QBCollisionEvent.ADD); evt.body1 = point.shape1.GetBody(); evt.body2 = point.shape2.GetBody(); dispatchEvent(evt); } override public function Remove(point:b2ContactPoint):void { super.Remove(point); var evt:QBCollisionEvent = new QBCollisionEvent(QBCollisionEvent.REMOVE); evt.body1 = point.shape1.GetBody(); evt.body2 = point.shape2.GetBody(); dispatchEvent(evt); } override public function Persist(point:b2ContactPoint):void { super.Persist(point); var evt:QBCollisionEvent = new QBCollisionEvent(QBCollisionEvent.PERSIST); evt.body1 = point.shape1.GetBody(); evt.body2 = point.shape2.GetBody(); dispatchEvent(evt); } override public function Result(point:b2ContactResult):void { super.Result(point); var evt:QBCollisionEvent = new QBCollisionEvent(QBCollisionEvent.RESULT); evt.body1 = point.shape1.GetBody(); evt.body2 = point.shape2.GetBody(); dispatchEvent(evt); } /* INTERFACE flash.events.IEventDispatcher */ public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void{ _dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference); } public function dispatchEvent(event:Event):Boolean { return _dispatcher.dispatchEvent(event); } public function hasEventListener(type:String):Boolean { return _dispatcher.hasEventListener(type); } public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void { _dispatcher.removeEventListener(type, listener, useCapture); } public function willTrigger(type:String):Boolean { return _dispatcher.willTrigger(type); } } }
Not much to say about this class except that, like the b2ContactListener, an instance of this class must be assigned to the m_contactListener property of the main b2World instance. In QuickBox2D terms, the main b2World instance can be reached through the ‘w’ property of the main sim (i.e. QuickBox2D) instance. But let’s put it all together and make a quick example, as it’s starting to sound a lot more complicated than it really is:
package { import com.actionsnippet.collisions.events.QBCollisionEvent; import com.actionsnippet.collisions.QBContactListener; import com.actionsnippet.qbox.QuickObject; import com.actionsnippet.qbox.QuickBox2D; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; import flash.utils.setTimeout; import flash.display.MovieClip; import flash.events.Event; /** * Test of collision detection in QuickBox2D * @author Devon O. */ [SWF(width='640', height='480', backgroundColor='#666666', frameRate='40')] public class Main extends MovieClip { public static const WORLD_SCALE:int = 30; private var _notice:TextField; private var _sim:QuickBox2D; private var _ball:QuickObject; private var _contactListener:QBContactListener; public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); initText(); initWorld(); } // text field to notify of collisions private function initText():void { _notice = new TextField(); _notice.selectable = false; _notice.autoSize = TextFieldAutoSize.LEFT; _notice.mouseEnabled = false; _notice.defaultTextFormat = new TextFormat("_sans", 50, 0x660000); _notice.text = "COLLISION!"; _notice.x = (stage.stageWidth - _notice.width) >> 1; _notice.y = (stage.stageHeight - _notice.height) >> 1; _notice.visible = false; addChild(_notice); } private function showNotice():void { _notice.visible = true; setTimeout(hideNotice, 20); } private function hideNotice():void { _notice.visible = false; } private function initWorld():void { _sim = new QuickBox2D(this); // create our QBContactListener instance, // have it listen for our QBCollisionEvent (ADD type), // and assign it to the QuickBox2D w.m_contactListener property. _contactListener = new QBContactListener(); _contactListener.addEventListener(QBCollisionEvent.ADD, collisionHandler, false, 0, true); _sim.w.m_contactListener = _contactListener; _sim.createStageWalls(); _ball = _sim.addCircle( { radius:1, fillColor:0x006600, restitution:.50 } ); // add some other balls to the mix for (var i:int = 0; i < 5; i++) { _sim.addCircle( { x:Math.random() * stage.stageWidth / WORLD_SCALE, radius:1, fillColor:0x212121, restitution:.50 } ); } _sim.mouseDrag(); _sim.start(); } private function collisionHandler(event:QBCollisionEvent):void { // if the main ball object is involved in the collision if (event.body1 == _ball.body || event.body2 == _ball.body) { showNotice(); } } } }
And compiled, that will give you this (drag and throw the circles around. You'll see whenever the green ball collides with anything (or anything collides with it) the "COLLISION" notice will flash):
[kml_flashembed publishmethod="static" fversion="10.0.0" movie="http://blog.onebyonedesign.com/wp-content/uploads/2010/05/qbtest.swf" width="640" height="480" targetclass="flashmovie"]
[/kml_flashembed]
Of course, instead of flashing the word "collision", you could play a sound or whatever else you wanted to do. And there you go - custom collision handling within QuickBox2D.
Post a comment