First off, I love this project! I've always wanted to run a good simulation.
Recently, I have tried to port the jbox2d 2.1.2.2 version to the Android device. I updated from the 2.0.1 version once I had finished a tutorial. The 2.1.2.2 jbox2d crashes on android without fail.
Opening the debugger, I found that the error was caused after the world creation,
Code:
world = new World(gravity, doSleep);
was called. The debugger then went to the logger "slf4j" and crashed after its block. I then reverted to the 2.1.2.0 version of jbox2d, but the crash remained.
Googling the problem, I found
this fix. To summarize, the slf4j logger was removed from the 2.1.2 jar file and the 1.6.3 version of slf4j jar was added to the project. The issue was fixed for me after this solution.
So, to whomever runs the jbox2d svn (toucansam?), please investigate this fix and have it committed. Running box2d at the latest revision is essential to using the tutorials and threads found on this forum. Thank you.
Both of the following classes run on Android 2.2 and jbox2d 2.1.2.0 with the solution outlined above.
World creation
Code:
package com.radix.turtle;
import org.jbox2d.callbacks.ContactImpulse;
import org.jbox2d.callbacks.ContactListener;
import org.jbox2d.collision.Manifold;
import org.jbox2d.collision.shapes.CircleShape;
import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.FixtureDef;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.contacts.Contact;
public class PhysicsWorld {
int numBalls = 120;
public float targetFPS = 45;
public float timeStep = (1 / targetFPS);
private int iterations = 2;
public Body[] bodies;
public int count = 0;
private World world;
private BodyDef groundBodyDef;
public void create() {
// Step 1: Create Physics World Boundaries
bodies = new Body[numBalls];
// Step 2: Create Physics World with Gravity
Vec2 gravity = new Vec2( 0, 0.0f);
boolean doSleep = true;
world = new World(gravity, doSleep);
// Set a contact listener.
world.setContactListener( new ContactListener()
{
@Override
public void beginContact(Contact contact) {
// TODO Auto-generated method stub
}
@Override
public void endContact(Contact contact) {
// TODO Auto-generated method stub
}
@Override
public void preSolve(Contact contact, Manifold oldManifold) {
// TODO Auto-generated method stub
}
@Override
public void postSolve(Contact contact, ContactImpulse impulse) {
// TODO Auto-generated method stub
}
} );
// Step 3: Create Ground Box
groundBodyDef = new BodyDef();
groundBodyDef.position.set(new Vec2((float) 0, (float) 0));
groundBodyDef.type = BodyType.STATIC;
Body ground = world.createBody(groundBodyDef);
// Create the fixtures (physical aspects) of the ground body.
FixtureDef groundEdgeFixtureDef = new FixtureDef();
groundEdgeFixtureDef.density = 1.0f;
groundEdgeFixtureDef.friction = 1.0f;
groundEdgeFixtureDef.restitution = 0.4f;
PolygonShape groundEdge = new PolygonShape();
groundEdgeFixtureDef.shape = groundEdge;
groundEdge.setAsBox(2000.0f, 1.0f);
ground.createFixture( groundEdgeFixtureDef );
//left wall
groundBodyDef.position.set(new Vec2((float) -20, (float) 0));
Body ground2 = world.createBody(groundBodyDef);
groundEdge.setAsBox(1.0f, 2000.0f);
ground2.createFixture( groundEdgeFixtureDef );
//right wall
groundBodyDef.position.set(new Vec2((float) 20, (float) 0));
Body ground3 = world.createBody(groundBodyDef);
groundEdge.setAsBox(1.0f, 2000.0f);
ground3.createFixture( groundEdgeFixtureDef );
//top wall
groundBodyDef.position.set(new Vec2((float) 0, (float) 35));
Body ground4 = world.createBody(groundBodyDef);
groundEdge.setAsBox(2000.0f, 1.0f);
ground4.createFixture( groundEdgeFixtureDef );
}
public void addBall() {
// Create Dynamic Body
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.DYNAMIC;
bodyDef.allowSleep=true;
bodyDef.position.set((float) (count/8 + Math.random()*3), (float) (28-count*1.5));
if (bodies == null)
bodies = new Body[numBalls];
bodies[count] = world.createBody(bodyDef);
bodies[count].setLinearVelocity(new Vec2( (float)(Math.random()*3), (float) (Math.random()*3)));
// Create Shape with Properties
CircleShape circle = new CircleShape();
circle.m_radius = (float) (Math.random()*1.5+0.5);
// Assign fixture to Body
FixtureDef ballFixtureDef = new FixtureDef();
ballFixtureDef.density = 1.0f; // Must have a density or else it won't
// be affected by gravity.
ballFixtureDef.restitution = 0.9f; // Define how bouncy the ball is.
ballFixtureDef.friction = 0.2f;
ballFixtureDef.shape = circle;
// Add the fixture to the ball body.
bodies[count].createFixture( ballFixtureDef );
// Increase Counter
count += 1;
}
public void addBallPosVel(float Xcoord, float Ycoord, float Xvel, float Yvel) {
// Create Dynamic Body
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.DYNAMIC;
bodyDef.allowSleep=true;
bodyDef.position.set((float) (Xcoord), Ycoord);
if (bodies == null)
bodies = new Body[numBalls];
bodies[count] = world.createBody(bodyDef);
bodies[count].setLinearVelocity(new Vec2(Xvel, Yvel));
// Create Shape with Properties
CircleShape circle = new CircleShape();
circle.m_radius = (float) (Math.random()*0.9+0.2);
// Assign fixture to Body
FixtureDef ballFixtureDef = new FixtureDef();
ballFixtureDef.density = 1.0f; // Must have a density or else it won't
// be affected by gravity.
ballFixtureDef.restitution = 0.9f; // Define how bouncy the ball is.
ballFixtureDef.friction = 0.2f;
ballFixtureDef.shape = circle;
// Add the fixture to the ball body.
bodies[count].createFixture( ballFixtureDef );
// Increase Counter
count += 1;
}
public void update() {
// Update Physics World
world.step(timeStep, iterations, iterations);
}
}
Drawing to canvas
Code:
package com.radix.turtle;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.GestureDetector;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.Window;
import android.view.WindowManager;
public class PhysicsTest extends Activity implements OnTouchListener{
PhysicsWorld mWorld;
private Handler mHandler;
private Body[] bodies;
int numBallsForRender = 5;
float tStep,bY ,bX, pointerX, pointerY, initialX, initialY;
float screenCenterX, screenCenterY;
float pixelsPerMeter = 30.0f, metersPerPixel = 1.0f/pixelsPerMeter;
float simX, simY;
NauteSurface ourSurfaceView;
float canvasW, canvasH, kH, kW, kHW, midCanvasY, midCanvasX;
Paint debugPaint,circlePaint, numberPaint;
Vec2 position;
boolean firstRun, nextBallIsBullet;
private ScaleGestureDetector mScaleDetector;
GestureDetector gestures;
private float mScaleFactor = 0.4f;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ourSurfaceView = new NauteSurface(this);
ourSurfaceView.setOnTouchListener(this);
mScaleDetector = new ScaleGestureDetector(this, new ScaleListener());
gestures = new GestureDetector(this, new GestureListener());
firstRun=true;
mWorld = new PhysicsWorld();
mWorld.create();
// Add 50 Balls
for (int i=0; i<numBallsForRender; i++) {
mWorld.addBall();
}
// Start Regular Update
mHandler = new Handler();
mHandler.post(update);
//get a fullscreen window!
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
//get a fullscreen window!
setContentView(ourSurfaceView);
}
@Override
protected void onPause() {
super.onPause();
mHandler.removeCallbacks(update);
ourSurfaceView.pause();
//finish();
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
ourSurfaceView.resume();
}
private Runnable update = new Runnable() {
public void run() {
mWorld.update();
tStep = mWorld.timeStep;
mHandler.postDelayed(update, (long) (tStep*1000));
bodies = mWorld.bodies;
numBallsForRender= mWorld.count;
}
};
private void doGameStuff(Canvas canvas) {
canvasW = canvas.getWidth();
canvasH = canvas.getHeight();
midCanvasY = canvasH/2;
midCanvasX = canvasW/2;
kH = canvasH/480; //values for developer device!
kW = canvasW/800; //values for developer device!
kHW = kH*kW;
debugPaint = new Paint();
debugPaint.setColor(Color.WHITE);
debugPaint.setTextAlign(Align.LEFT);
debugPaint.setTextSize(20);
//debugPaint.setTypeface(Typeface.DEFAULT_BOLD);
debugPaint.setAntiAlias(true);
numberPaint = new Paint();
numberPaint.setColor(Color.RED);
numberPaint.setTextAlign(Align.LEFT);
numberPaint.setTextSize(15);
numberPaint.setAntiAlias(true);
circlePaint = new Paint();
circlePaint.setColor(Color.WHITE);
circlePaint.setAntiAlias(true);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
mScaleDetector.onTouchEvent(event);
gestures.onTouchEvent(event);
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
initialX = event.getX();
initialY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
pointerX = event.getX();
pointerY = event.getY();
if ( Math.abs(initialX-pointerX)<2 && Math.abs(initialY-pointerY)<2 ) {
simX = (pointerX - midCanvasX)/mScaleFactor + screenCenterX;//real pixel coord of touch
simY = (pointerY - midCanvasY)/mScaleFactor + screenCenterY;
simX = P2M( simX );
simY = P2M(canvasH-simY);
if (nextBallIsBullet==true){
nextBallIsBullet=false;
mWorld.addBallPosVel(simX, simY, 0, -128.0f );
} else {
mWorld.addBallPosVel(simX, simY, (float)(16.0*(Math.random()-0.5)), (float)(16.0*(Math.random()-0.5)) );
}
} else {
//ENTER THE SCREEN SCROLL DAWG
}
break;
}
return true;
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 7.0f));
return true;
}
}
private class GestureListener implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onDown(MotionEvent e) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// TODO Auto-generated method stub
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// TODO Auto-generated method stub
nextBallIsBullet=true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
// TODO Auto-generated method stub
float k = 1.0f;
screenCenterX+= k*distanceX/mScaleFactor;
screenCenterY+= k*distanceY/mScaleFactor;
return true;
}
@Override
public void onShowPress(MotionEvent e) {
// TODO Auto-generated method stub
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
// TODO Auto-generated method stub
return false;
}
}
private float P2M(float xPixels) {
// TODO Auto-generated method stub
return xPixels*metersPerPixel;
}
private float M2P(float xMetres) {
// TODO Auto-generated method stub
return xMetres*pixelsPerMeter;
}
//SUPER PASTE! BELOW
public class NauteSurface extends SurfaceView implements Runnable{
SurfaceHolder ourHolder;
Thread ourThread = null;
boolean isRunning = false;
Canvas canvas;
Body bd;
String s = "\n", PrintStatus, s1[];
public NauteSurface(Context context) {
super(context);
ourHolder = getHolder();
}
public void pause(){
isRunning = false;
while(true){
try {
ourThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
break;
}
ourThread = null;
finish();
}
public void resume(){
isRunning = true;
ourThread = new Thread(this);
ourThread.start();
}
public void run() {
// TODO Auto-generated method stub
while(isRunning){
if (!ourHolder.getSurface().isValid())
continue;
//if not a valid surface
canvas = ourHolder.lockCanvas();
if (firstRun==true){
doGameStuff(canvas);
firstRun=false;
}
canvas.drawRGB(0, 20, 50);
//setScreenCenter(bX, bY);
//canvas.scale(mScaleFactor, mScaleFactor ,bX, bY);
canvas.save();
canvas.translate(midCanvasX-screenCenterX, midCanvasY-screenCenterY);
canvas.scale(mScaleFactor, mScaleFactor, screenCenterX, screenCenterY);
for(int i=0; i<numBallsForRender; i++){
bd = bodies[i];
bX = M2P(bd.getPosition().x);
bY = 800 - M2P(bd.getPosition().y);
canvas.drawCircle(bX, bY, M2P(bd.m_fixtureList.m_shape.m_radius)*1.06f, circlePaint);
//canvas.drawCircle(bX, bY, 3.0f, circlePaint);
if (i<16)canvas.drawText(i+"", bX, bY, numberPaint);
}
Body bk = bodies[1];
bX = M2P(bk.getPosition().x);
bY = 800 - M2P(bk.getPosition().y);
if (true==true){
PrintStatus = ""
+"step value: "+tStep+" or "+(1/tStep)+s
+"ball pos+ "+bX+" "+bY+s
+"real ball pos+ "+bk.getPosition().x+" "+bk.getPosition().y+s
+"pointer pos+ "+simX+" "+simY+s
+"mass rendering "+numBallsForRender+" balls"+s
+pixelsPerMeter+" pixels per meter"+s
+"nextBallIsBullet :"+nextBallIsBullet+s
+"RADIX 2012"+s
;
s1 = PrintStatus.split(s);
for(int i=0; i<s1.length; i++){
canvas.drawText(s1[i], 300, 225+25*i, debugPaint);
}
}
canvas.restore();
ourHolder.unlockCanvasAndPost(canvas);
}
}
}
}
//Thread.sleep( Math.max(15-(t1-t2), 0) );