Показаны сообщения с ярлыком Box2d. Показать все сообщения
Показаны сообщения с ярлыком Box2d. Показать все сообщения

воскресенье, 26 августа 2012 г.

Коллизии

В процессе нашего изучения Box2d мы не затронули очень важную тему - обработку событий. Разумеется, Box2d обрабатывает события столкновения объектов вполне самостоятельно и без нашей помощи, но иногда нам тоже хочется принять в этом участие :) Внесем изменения в наш проект Kepler таким образом, чтобы столкнувшиеся объекты "сливались" в один.
Один из столкнувшихся объектов будем удалять, а у второго будем изменять параметры. Нам необходимо зарегистрировать объект ContactListener, чтобы получать сообщения  о коллизиях и внести в проект изменения, связанные с обработкой этих коллизий. Главное, о чем необходимо помнить, это то, что объекты Box2d НЕ ДОЛЖНЫ изменяться, в процессе расчета очередной итерации. Получив уведомление о коллизии, мы должны сохранить параметры, для того, чтобы выполнить необходимые изменения после расчета итерации Box2d. Измененный модуль Task будет выглядеть следующим образом:

package com.WhiteRabbit.Kepler;

import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

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.World;
import org.jbox2d.dynamics.contacts.Contact;

public class Task extends TimerTask implements ContactListener {

    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private KeplerActivity ctx;
    private Timer timer = new Timer();
   
    private World             world;
    private Body              ground;
   
    public Task(KeplerActivity ctx) {
        this.ctx  = ctx;
        configure();
    }
   
    @Override
    public void beginContact(Contact contact) {}

    @Override
    public void endContact(Contact contact) {}

    @Override
    public void preSolve(Contact contact, Manifold folds) {}
   
    @Override
    public void postSolve(Contact contact, ContactImpulse impulse) {
        Item a = (Item)contact.getFixtureA().getBody().getUserData();
        Item b = (Item)contact.getFixtureB().getBody().getUserData();
        if (a.isMarked() || b.isMarked()) return;
        a.mark(); b.delete();
        Vec2 aSpeed = a.getBody().getLinearVelocity();
        Vec2 bSpeed = b.getBody().getLinearVelocity();
        a.getBody().setLinearVelocity(new Vec2(aSpeed.x + bSpeed.x, 

                                               aSpeed.y + bSpeed.y));
        a.setRadius((float)Math.sqrt((a.getRadius()*a.getRadius()) +  

                                     (b.getRadius()*b.getRadius())));
    }

    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y / 2;
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, 0.0f);
        world = new World(gravity, true);
        world.setContactListener(this);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
       
        // Создать границы
        bd.position.set(0, Y);
        Body edge = world.createBody(bd);
        PolygonShape sd = new PolygonShape();
        sd.setAsBox(1, Y);
        edge.createFixture(sd, 1);
        bd.position.set(2 * X, Y);
        edge = world.createBody(bd);
        edge.createFixture(sd, 1);
        bd.position.set(X, 0);
        edge = world.createBody(bd);
        sd.setAsBox(X, 1);
        edge.createFixture(sd, 1);
        bd.position.set(X, 2 * Y);
        edge = world.createBody(bd);
        edge.createFixture(sd, 1);
       
        // Создать объекты
        CircleShape shape = new CircleShape();
        bd.type = BodyType.DYNAMIC;
        Random r = new Random();
       
        for (int i = 0; i < 15; i++) {
            bd.position.set(r.nextInt((int)X * 2 - 10) + 5, 

                            r.nextInt((int)Y * 2 - 10) + 5);
            bd.linearVelocity.set(r.nextInt(20) - 5, r.nextInt(20) - 10);
            shape.m_radius = r.nextInt(4) + 1;
            Body b = world.createBody(bd);
            b.createFixture(shape, r.nextInt(3) + 1);
            ctx.getView().addBody(b, shape.m_radius);
        }
       
        // Запустить рассчет
        timer.scheduleAtFixedRate(this, 0, SECONDS_INTERVAL / FRAME_RATE);
    }
   
    public void terminate() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    @Override
    public void run() {
        // Выполнить расчет итерации
        float timeStep = 2.0f / FRAME_RATE;
        world.step(timeStep, 10, 10);
        // Удалить столкнувшиеся объекты
        ctx.getView().removeDeleted(world);
        // Рассчитать действующие силы
        ctx.getView().calcForces();
        // Обновить view
        ctx.getView().update();
    }
}


  1. Мы наследуем интерфейс ContactListener и регистрируем его вызовом setContactListener
  2. Для обработки коллизий будем использовать метод postSolve (от других он выгодно отличается наличием параметра impulse, по которому можно оценивать "силу" соударения, чего мы правда не будем делать)
  3. В configure вместо 3 генерируем 15 объектов, используя генератор случайных чисел для формирования начальных параметров
  4. Добавляем границы по краям экрана, чтобы объекты не улетали в открытое пространство
  5. В методе run добавляем фазу удаления объектов помеченных для удаления
В MainView немного изменяем метод addBody (сохраняя в Body ссылку на объект Item) и добавляем метод removeDeleted, осуществляющий удаление объектов:

package com.WhiteRabbit.Kepler;

import java.util.ArrayList;
import java.util.List;

import org.jbox2d.collision.shapes.CircleShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.Fixture;
import org.jbox2d.dynamics.World;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainView extends SurfaceView implements SurfaceHolder.Callback {
    
    ...
    public void addBody(Body b, float r) {
        Item it = new Item(b, r);
        b.setUserData(it);
        items.add(it);
    }
   
    public void removeDeleted(World w) {
        List<Item> newItems = new ArrayList<Item>();
        for (Item it: items) {
            if (it.isDeleted()) {
                w.destroyBody(it.getBody());
            } else {
                if (it.isMarked()) {
                    Body b = it.getBody();
                    CircleShape shape = new CircleShape();
                    shape.m_radius = it.getRadius();
                    Fixture f = b.getFixtureList();
                    if (f !=null) {
                        b.destroyFixture(f);
                    }
                    b.createFixture(shape, 1);
                    it.clear();
                }
                newItems.add(it);
            }
        }
        items = newItems;
    }
    ...   
}

Item приобретает новые члены, но не становится от этого менее тривиальным:

package com.WhiteRabbit.Kepler;

import org.jbox2d.dynamics.Body;

public class Item {
   
    private Body body;
    private float radius;
    private boolean isDeleted = false;
    private boolean isMarked = false;
   
    public Item(Body body, float radius) {
        this.body = body;
        this.radius = radius;
    }
   
    public boolean isDeleted() {
        return isDeleted;
    }
   
    public boolean isMarked() {
        return isDeleted || isMarked;
    }
   
    public void delete() {
        isDeleted = true;
    }
   
    public void mark() {
        isMarked = true;
    }
   
    public void clear() {
        isMarked = false;
    }
   
    public Body getBody() {
        return body;
    }
   
    public float getRadius() {
        return radius;
    }
   
    public void setRadius(float radius) {
        this.radius = radius;
    }
}
Вот и все. Запускаем программу на выполнение и наблюдаем картину "должен остаться только один":



Как обычно, полный исходный код проекта можно взять здесь.

понедельник, 20 августа 2012 г.

Небесная механика

Из моего предыдущего изложения могло создаться впечатление, что сила тяжести может быть задана только одним вектором, действующим одинаково на все объекты мира (World.setGravity). Если бы это было так, сделать что либо похожее на Angry Birds Space с ее микрогравитацией не представлялось бы возможным.
Конечно же это не так. Метод setGravity дает возможность задать направление силы тяжести, действующей на все динамические объекты, наиболее простым способом, но никто не запрещает нам, приложив чуть больше усилий, вычислять все действующие силы самостоятельно.
Сегодня, я предлагаю воспользоваться этой возможностью и смоделировать известную задачу трех тел на плоскости. Исходные тексты Activity не содержат для нас ничего нового (их вполне можно взять из предыдущих проектов):

package com.WhiteRabbit.Kepler;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;

public class KeplerActivity extends Activity {
   
    private MainView view;
    private Task     task = null;
   
    public MainView getView() {
        return view;
    }

    public void createTask() {
        if (task == null) {
            task = new Task(this);
        }
    }
   
    @Override
    protected void onPause() {
        if (task != null) {
            task.terminate();
        }
        super.onPause();
    }
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        LinearLayout.LayoutParams containerParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT, 0.0F);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);

        root.setBackgroundColor(Color.BLACK);
        root.setLayoutParams(containerParams);

        view = new MainView(this);
        root.addView(view);
        setContentView(root);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }
}

В Task мы определим три тела, задав им начальные позиции, скорости и плотность. Также, в методе run, перед тем как отобразить изменения на экране, будем рассчитывать силы взаимного притяжения созданных объектов:

package com.WhiteRabbit.Kepler;

import java.util.Timer;
import java.util.TimerTask;

import org.jbox2d.collision.shapes.CircleShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.World;

public class Task extends TimerTask {

    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private KeplerActivity ctx;
    private Timer timer = new Timer();
   
    private World             world;
    private Body              ground;
   
    public Task(KeplerActivity ctx) {
        this.ctx  = ctx;
        configure();
    }
   
    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y / 2;
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, 0.0f);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
       
        // Создать объекты
        CircleShape shape = new CircleShape();
        bd.type = BodyType.DYNAMIC;
       
        bd.position.set(X, Y);
        bd.linearVelocity.set(1f, 1f);
        shape.m_radius = 5;
        Body b = world.createBody(bd);
        b.createFixture(shape, 1.0f);
        ctx.getView().addBody(b, shape.m_radius);
       
        bd.position.set(X + 10, Y - 40);
        bd.linearVelocity.set(-3.5f, 2f);
        shape.m_radius = 3;
        b = world.createBody(bd);
        b.createFixture(shape, 5.0f);
        ctx.getView().addBody(b, shape.m_radius);

        bd.position.set(X - 20, Y + 30);
        bd.linearVelocity.set(1.7f, 1f);
        shape.m_radius = 4;
        b = world.createBody(bd);
        b.createFixture(shape, 2.0f);
        ctx.getView().addBody(b, shape.m_radius);
       
        // Запустить рассчет
        timer.scheduleAtFixedRate(this, 0, SECONDS_INTERVAL / FRAME_RATE);
    }
   
    public void terminate() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    @Override
    public void run() {
        // Выполнить расчет итерации
        float timeStep = 2.0f / FRAME_RATE;
        world.step(timeStep, 10, 10);
        // Рассчитать действующие силы
        ctx.getView().calcForces();
        // Обновить view
        ctx.getView().update();
    }
}


Реализацию calcForces расположим в MainView. Из курса школьной физики, вспомним, что сила взаимного притяжения объектов прямо пропорциональна произведению масс объектов и обратно пропорциональна квадрату расстояния между ними. Величина силы взаимного притяжения определяется неким коэффициентом, называемым гравитационной постоянной.
Поскольку масса созданных нами объектов пренебрежимо мала, и умножив ее на 10 в минус 11 степени мы вряд-ли сможем пронаблюдать какое-то взаимодействие, перед нами открывается два пути. Мы должны либо многократно увеличить плотность (или размеры) объектов, либо изменить величину гравитационной постоянной. По той причине, что второй путь технически проще (а также по законам военного времени) гравитационная постоянная у нас будет равна 3 (подбирается экспериментальным путем). Дальнейшая реализация очевидна:

package com.WhiteRabbit.Kepler;

import java.util.ArrayList;
import java.util.List;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    private final static float G = 3; // Гравитационная постоянная
   
    private KeplerActivity ctx;
   
    public final float SZ_X = 100;    // Физические размеры
    public float SZ_Y;  
   
    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;                  // Экранные размеры
    private int szY;
   
    private Paint paint = new Paint();
    private List<Item> items = new ArrayList<Item>();
   
    public MainView(KeplerActivity ctx) {
        super(ctx);
        this.ctx = ctx;
        this.getHolder().addCallback(this);
    }
   
    public void addBody(Body b, float r) {
        items.add(new Item(b, r));
    }
   
    public void calcForces() {
        for (Item a: items) {
            Body ab = a.getBody();
            ab.m_force.setZero();
            Vec2 ap = ab.getPosition();
            float fx = 0;
            float fy = 0;
            for (Item b: items) {
                if (a == b) continue;
                Body bb = b.getBody();
                Vec2 bp = bb.getPosition();
                float dx = bp.x - ap.x;
                float dy = bp.y - ap.y;
                float r = (float)Math.sqrt(dx*dx + dy*dy);
                float m = ab.getMass() * bb.getMass();
                float f = (m * G)/(r * r);
                fx += (f * dx)/r;
                fy += (f * dy)/r;
            }
            ab.applyForce(new Vec2(fx, fy), ab.getWorldCenter());
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        update();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {}
   
    public void update() {
        Canvas c = null;
        try {
            c = this.getHolder().lockCanvas();
            if (c != null) {
                onDraw(c);
            }
        } catch (Exception e) {
        } finally {
            if (c != null) {
                this.getHolder().unlockCanvasAndPost(c);
            }
        }
    }
   
    public float xFromWorld(float x) {
        return (x * szX) / SZ_X;
    }
   
    public float yFromWorld(float y) {
        return (y * szY) / SZ_Y;
    }
   
    public float xToWorld(float x) {
        return (x * SZ_X) / szX;
    }
   
    public float yToWorld(float y) {
        return (y * SZ_Y) / szY;
    }
   
    public float getScaledX(float x) {
        return (x * szX) / 320f;
    }
   
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) ||
            (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED)) {
            setMeasuredDimension(maxX, maxY);
        } else {
            szX = MeasureSpec.getSize(widthMeasureSpec);
            szY = MeasureSpec.getSize(heightMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szX > maxX) {
                    szX = maxX;
                }
            }
            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szY > maxY) {
                    szY = maxY;
                }
            }
            SZ_Y = (szY * SZ_X)/szX;
            setMeasuredDimension(szX, szY);
            ctx.createTask();
        }
    }
   
    @Override
    protected void onDraw(Canvas c) {
        c.drawColor(Color.BLACK);
        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
        for (Item it: items) {
            Vec2 pos = it.getBody().getPosition();
            float x = xFromWorld(pos.x);
            float y = yFromWorld(pos.y);
            float r = xFromWorld(it.getRadius());
            c.drawCircle(x, y, r, paint);
        }
    }   
}
Осталось добавить тривиальную реализацию класса Item:

package com.WhiteRabbit.Kepler;

import org.jbox2d.dynamics.Body;

public class Item {
   
    private Body body;
    private float radius;
   
    public Item(Body body, float radius) {
        this.body = body;
        this.radius = radius;
    }
   
    public Body getBody() {
        return body;
    }
   
    public float getRadius() {
        return radius;
    }
}


И запустить программу на выполнение:


Как обычно, исходные тексты проекта доступны здесь.