This week we did some experiments with the AS3 port of the box2d physics engine. I read some complaints online about the awkward API of this engine, but I personally think its a work of art. We are in the process of developing turn based multiplayer games and an engine like this will offer us a very nice strategy for latency masking. (by passing b2Vec data to update the server model and finishing with a final handshake after b2Bodies have gone to sleep). This week we tried to use box2d in our normal MVC routine. These were our goals:
1) All graphics must come from separate .swf files. These can only export linkage ids no MovieClip subclasses should be attached. This will enable a remote designer to build a library without access to code.
2) All Box2d objects will be in models. The main gameloop (where the step function is called) will broadcast per-body notifications. Notifications are linked to Commands (much like pureMVC). The commands know the interface of the view and will update the view. (we call em Updates). View objects can only subscribe to Notifications of other view objects. (We are not using Actor classes or userData to link physics to sprites but dynamic Notifications).
3) Notifications published from the view will also instantiate command objects, these will call modifiers on models (we call em Modifiers). Model objects can only subscribe to Notifications from other model objects.
4) The model and view know nothing about each other. This allows us to test the view with a mock model, and the model with a mock view. We want to be able to replace the box2d objects with Tweening objects just by having them publishing the same Notifications. (and therefore instantiating the same commands). We can also test the model with a simple debugdraw Sprite as normally.
The following sample was loaded from this xml structure:
<game> <!-- Stage size : 640 - 360 --> <graphics> <lib url='assets/assets.swf' id='assets'> <sprite id='face'/> <sprite id='ball'/> <sprite id='box'/> </lib> <lib url='assets/ui.swf' id='ui'> <sprite id='button'/> </lib> </graphics> <level> <!-- GAME BOUNDINGBOX--> <sprite x='-95' y='200' width='200' height='400' linkage='assets.box' density='0' shape='rect' id='wall1' isSensor='0' restitution='0.1' friction='100' /> <sprite x='735' y='200' width='200' height='400' linkage='assets.box' density='0' shape='rect' id='wall2' isSensor='0' restitution='0.1' friction='100' /> <sprite x='340' y='-95' width='680' height='200' linkage='assets.box' density='0' shape='rect' id='wall3' isSensor='0' restitution='0.1' friction='100' /> <sprite x='340' y='455' width='680' height='200' linkage='assets.box' density='0' shape='rect' id='wall4' isSensor='0' restitution='0.1' friction='100' /> <sprite x='200' y='100' r='20' width='40' height='40' linkage='ui.button' id='button' density='10' shape='rect' isSensor='0' restitution='0.8' friction='100' /> <sprite x='205' y='30' radius='15' linkage='assets.face' id='face' density='0.1' shape='circ' /> <sprite x='205' y='30' radius='50' linkage='assets.ball' id='ball' density='0.01' shape='circ' /> <sprite x='205' y='30' width='60' height='60' linkage='assets.box' id='box0' density='0.1' shape='rect' /> <sprite x='205' y='30' width='60' height='60' linkage='assets.box' id='box1' density='0.1' shape='rect' /> <sprite x='205' y='30' width='60' height='30' linkage='assets.box' id='box2' density='0.1' shape='rect' /> <sprite x='205' y='30' width='60' height='60' linkage='assets.box' id='box3' density='0.1' shape='rect' /> <contact add='onFaceHitsFloor' remove='onFaceFromFloor' sprite1='face' sprite2='wall4' /> <contact persist='onButtonOnFloor' sprite1='button' sprite2='wall4' /> </level> </game>
Right now we can only define 2 primitives here, boxes and circles. We can also define contact Notifications. All definitions can also be constructed during run time. (We use XML as parameter objects for factory methods).
Here’s our implementation of the b2ContactListener, we are adding both shape combinations to the Notification look-up table:
package nl.fullscreen.oneonone.model{
import Box2D.Collision.b2ContactPoint;
import Box2D.Dynamics.Contacts.b2ContactResult;
import Box2D.Dynamics.b2ContactListener;
import nl.fullscreen.notifications.NotificationCenter;
public class ContactListener extends b2ContactListener{
private var notificationMap:Object;
public function ContactListener(m:Object){
super();
setNotificationMap(m);
}
public function addContactNotification(name1:String,name2:String,notification:String):void{
notificationMap[name1+"*"+name2] = notification;
notificationMap[name2+"*"+name1] = notification;
}
public function setNotificationMap(nm:Object):void{
notificationMap = nm;
}
public override function Add(point:b2ContactPoint) : void{
var n1:String = point.shape1.GetBody().m_userData.name;
var n2:String = point.shape2.GetBody().m_userData.name;
if (notificationMap[n1+"*"+n2] && notificationMap[n1+"*"+n2].@add != undefined){
NotificationCenter.publish(notificationMap[n1+"*"+n2].@add ,{n1:n1,n2:n2});
}
};
public override function Persist(point:b2ContactPoint) : void{
var n1:String = point.shape1.GetBody().m_userData.name;
var n2:String = point.shape2.GetBody().m_userData.name;
if (notificationMap[n1+"*"+n2] && notificationMap[n1+"*"+n2].@persist != undefined){
NotificationCenter.publish(notificationMap[n1+"*"+n2].@persist ,{n1:n1,n2:n2});
}
};
public override function Remove(point:b2ContactPoint) : void{
var n1:String = point.shape1.GetBody().m_userData.name;
var n2:String = point.shape2.GetBody().m_userData.name;
if (notificationMap[n1+"*"+n2] && notificationMap[n1+"*"+n2].@remove != undefined){
NotificationCenter.publish(notificationMap[n1+"*"+n2].@remove ,{n1:n1,n2:n2});
}
};
public override function Result(point:b2ContactResult) : void{
};
}
}
This is all work in progress but just in case you’re interested here is the source
More on this later.