воскресенье, 29 июля 2012 г.

Я хочу чтобы картинка ожила

После того как проходит эйфория, вызванная успешным приручением Box2d, мы замечаем, что обе точки подвеса нашей "нити" намертво прибиты гвоздями к "земле". Понятно, что как-то пошевелить их в этом состоянии вряд-ли удастся (а нам бы очень этого хотелось). К счастью, нам не придется выдумывать никаких костылей, поскольку разработчики Box2d эту трудность предусмотрели.
Мы воспользуемся не сильно документированным, но активно используемым, для интерактивного управления объектами, в тестовой среде Testbed (исходные текст которого доступны для анализа) MouseJoint-ом. Нам понадобиться два постоянно действующих MouseJoint (никто и нигде не говорил, что MouseJoint может быть только один), для привязки каждого из концов "нити" к некоторой позиции на экране.
Помимо этого, мы несколько усложним конструкцию "нити", добавив к каждому RevoluteJoint (позволяющему соединенным объектам поворачиваться относительно точки соединения) DistanceJoint (позволяющий зафиксировать неизменное расстояние, между соединенными объектами). 
Кроме того, мы принесем жертву богам инкапсуляции и полиморфизма и абстрагируем высокоуровневые методы (такие как отрисовка объекта или обработка им событий) в интерфейс IObject, что позволит нам работать с "нитью" как с единым целым, а не набором звеньев, а также добавлять другие типы обрабатываемых объектов (в конце концов, одна единственная нить на экране, это не очень интересно, будь она хоть трижды реалистична):

package com.WhiteRabbit.YoYo.objects;

import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.Joint;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;

public interface IObject {
    void addBody(Body b);
    void addHandle(Joint j, Body b);
    void draw(Canvas c, Paint p);
    boolean onTouchEvent(MotionEvent event);
}

Измененный класс Task будет выглядеть следующим образом:

package com.WhiteRabbit.YoYo;

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

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.joints.DistanceJointDef;
import org.jbox2d.dynamics.joints.MouseJointDef;
import org.jbox2d.dynamics.joints.RevoluteJointDef;

import com.WhiteRabbit.YoYo.objects.Cord;
import com.WhiteRabbit.YoYo.objects.IObject;

public class Task extends TimerTask  {
   
    private final static int CHAIN_CNT = 30;

    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private YoYoActivity ctx;
    private Timer timer = new Timer();
   
    private World             world;
    private Body              ground;
   
    public Task(YoYoActivity ctx) {
        this.ctx  = ctx;
        configure();
    }
   
    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y - (v.SZ_Y / 5);
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, 10.0f);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
       
        // Создать нить
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(0.5f, 0.125f);
        FixtureDef fd = new FixtureDef();
        fd.shape = shape;
        fd.density = 20.0f;
        fd.friction = 0.2f;
        IObject cord = new Cord(v);
        v.addObj(cord);
        Body prevBody  = ground;
        Body firstBody = null;
        for (int i = 0; i < CHAIN_CNT; i++) {
            bd = new BodyDef();
            bd.type = BodyType.DYNAMIC;
            bd.position.set(X - 14.5f + (1.0f * i), Y);
            bd.linearDamping = 1;
            Body body = world.createBody(bd);
            body.createFixture(fd);
            cord.addBody(body);
            if (firstBody == null) {
                firstBody = body;
            } else {
                RevoluteJointDef jd = new RevoluteJointDef();
                jd.collideConnected = false;
                Vec2 anchor = new Vec2(X - 15.0f + (1.0f * i), Y);
                jd.initialize(prevBody, body, anchor);
                world.createJoint(jd);
                DistanceJointDef dd = new DistanceJointDef();
                dd.initialize(prevBody, body, 
                              prevBody.getWorldCenter(), body.getWorldCenter());
                dd.dampingRatio = 0;
                dd.frequencyHz = 0;
                dd.collideConnected = false;
                world.createJoint(dd);
            }
            prevBody = body;
        }
       
        MouseJointDef mj = new MouseJointDef();
        mj.bodyA  = ground;
        mj.bodyB  = firstBody;
        mj.target.set(new Vec2(X - 14.5f, Y));
        mj.maxForce = 10000.0f * prevBody.getMass();
        mj.dampingRatio = 1;
        mj.frequencyHz = 100;
        cord.addHandle(world.createJoint(mj), firstBody);
        mj.bodyB  = prevBody;
        mj.target.set(new Vec2(X - 15.0f + (1.0f * CHAIN_CNT), Y));
        mj.maxForce = 10000.0f * prevBody.getMass();
        cord.addHandle(world.createJoint(mj), prevBody);

        // Запустить рассчет
        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);
        // Обновить view
        ctx.getView().update();
    }
}
В добавленных Joint-ах, стоит обратить внимание на параметры frequencyHz и dampingRatio, используя которые можно задавать частоту колебаний "пружинного" соединения и коэффициент затухания этих колебаний, соответственно. 
Реализация метода onDraw в MainView упроститься. Также добавиться обработка событий MotionEvent:

package com.WhiteRabbit.YoYo;

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

import com.WhiteRabbit.YoYo.objects.IObject;

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

public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    private YoYoActivity ctx;
   
    public final float SZ_X = 35;    // Физические размеры
    public float SZ_Y;  
   
    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;                  // Экранные размеры
    private int szY;
   
    private List<IObject> objs = new ArrayList<IObject>();
    private Paint paint = new Paint();
   
    public MainView(YoYoActivity ctx) {
        super(ctx);
        this.ctx = ctx;
        this.getHolder().addCallback(this);
    }
   
    public void addObj(IObject obj) {
        objs.add(obj);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, 
                               int width, int height) {}

    @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 (IObject o: objs) {
            o.draw(c, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        for (IObject o: objs) {
            if (o.onTouchEvent(event)) return true;
        }
        return false;
    }
}
Поскольку точек подвеса у нас две, нам понадобиться обрабатывать события Multitouch, уже рассматривавшиеся нами ранее. Нам по прежнему лень возиться с масками action события MotionEvent, поэтому мы будем ассоциировать id каждой точки касания с одной из точек подвеса, а если этот id будет пропадать из списка, разрывать связь. Также, как и в прошлый раз, особой обработки потребует событие отрыва последнего касания, но поскольку, в этот момент, касание будет всего одно, мы будем иметь дело с банальным ACTION_CANCEL, без каких либо дополнительных замаскированных сообщений. Всю логику отрисовки нити (состоящей из соединенных фрагментов), а также обработки событий MotionEvent будет выполнять класс Cord, реализующий интерфейс IObject:

package com.WhiteRabbit.YoYo.objects;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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

import com.WhiteRabbit.YoYo.MainView;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;

public class Cord implements IObject {
   
    private MainView view;
    private List<Body> bodies = new ArrayList<Body>();
    private List<Handle> handles = new ArrayList<Handle>();
   
    public Cord(MainView view) {
        this.view = view;
    }
   
    @Override
    public void addBody(Body b) {
        bodies.add(b);
    }
   
    @Override
    public void addHandle(Joint j, Body b) {
        handles.add(new Handle(view, j, b));
    }
   
    @Override
    public void draw(Canvas c, Paint p) {
        boolean f = true;
        float sx = 0, sy = 0, ex = 0, ey = 0;
        float px = 0, py = 0;
        for (Body b: bodies) {
            Vec2 pos = b.getPosition();
            float x = view.xFromWorld(pos.x);
            float y = view.yFromWorld(pos.y);
            if (!f) {
                c.drawLine(px, py, x, y, p);
            } else {
                sx = x;
                sy = y;
            }
            px = x;
            py = y;
            ex = x;
            ey = y;
            f = false;
        }
        float r = view.getScaledX(Handle.RADIUS);
        c.drawCircle(sx, sy, r, p);
        c.drawCircle(ex, ey, r, p);
    }
   
    public boolean onTouchEvent(MotionEvent event) {
        boolean r = false;
        Set<Handle> listToClear = new HashSet<Handle>();
        for (Handle h: handles) {
            listToClear.add(h);
        }
        int cnt = event.getPointerCount();
        if (!((cnt == 1)&&((event.getAction() == MotionEvent.ACTION_UP)||
                           (event.getAction() == MotionEvent.ACTION_CANCEL)))) {
            for (int i = 0; i < cnt; i++) {
                float x = event.getX(i);
                float y = event.getY(i);
                int   pointerId = event.getPointerId(i);
                boolean isFound = false;
                for (Handle h: handles) {
                    Integer id = h.getPointerId();
                    if (id == null) continue;
                    if (pointerId == id) {
                        h.moveTo(x, y);
                        listToClear.remove(h);
                        isFound = true;
                        r = true;
                    }
                }
                if (isFound) continue;
                for (Handle h: handles) {
                    if (h.isCapturing(x, y)) {
                        h.moveTo(x, y);
                        h.setPointerId(pointerId);
                        listToClear.remove(h);
                        r = true;
                    }
                }
            }
        }
        for (Handle h: listToClear) {
            if (h.getPointerId() != null) {
                h.clearPointId();
            }
        }
        return r;
    }
}
Используемый в нем вспомогательный класс Handle, выглядит следующим образом:

package com.WhiteRabbit.YoYo.objects;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.Joint;
import org.jbox2d.dynamics.joints.MouseJoint;

import com.WhiteRabbit.YoYo.MainView;

public class Handle {
   
    public final static float RADIUS = 10f;
   
    private MainView view;
    private MouseJoint joint;
    private Body body;
   
    private Integer pointerId = null;
   
    public Handle(MainView view, Joint joint, Body body) {
        this.view  = view;
        this.joint = (MouseJoint)joint;
        this.body  = body;
    }
   
    public Integer getPointerId() {
        return pointerId;
    }
   
    public void setPointerId(int id) {
        pointerId = id;
    }
   
    public boolean isCapturing(float x, float y) {
        Vec2 pos = body.getPosition();
        float r = view.getScaledX(RADIUS) * 10;
        float X = view.xFromWorld(pos.x);
        float Y = view.yFromWorld(pos.y);
        float dist = (float)Math.sqrt((double)((X - x) * (X - x) + 

                                               (Y - y) * (Y - y)));
        return (dist <= r);
    }
   
    public void moveTo(float x, float y) {
        joint.setTarget(new Vec2(view.xToWorld(x), view.yToWorld(y)));
    }
   
    public void clearPointId() {
        joint.setTarget(body.getPosition());
        pointerId = null;
    }
}


Запустив измененный проект под отладчиком, убедимся, что точки подвеса можно двигать, после чего, со спокойной совестью экспортируем apk на устройство и убедимся, что Multitouch также работает.

P.S. Те кому лениво воспроизводить все описанные манипуляции руками (лень крайне уважаемое качество программиста) могут забрать готовый проект здесь.

понедельник, 23 июля 2012 г.

Изучаем физику

После появления таких игр как Trine, Angry Birds и Cut the Rope, реализация реалистичной физики уже рассматривается едва-ли не как обязательная составляющая аркадных игр. В то-же время, самостоятельная реализация "физического движка" затруднительна, как в силу необходимости превосходного знания математики, так и вследствие высокой ресурсоемкости такой разработки (как кодирования, так и отладки).
К счастью, имеются готовые реализации физических движков, которые достаточно легко использовать в собственных разработках. Одним из них является Box2d позволяющий реализовать реалистичную физику взаимодействия объектов в 2-мерном пространстве и имеющий невероятное количество портов, для всевозможных языков программирования, включая JavaScript, Macromedia Flash и, разумеется, Java.
Поскольку публикаций по использованию Box2d хватает, в этом цикле статей, я постараюсь описать процесс создания простейшего приложения с реалистичной 2d физикой, стараясь обращать внимание на не очевидные (на мой взгляд) моменты.
Поскольку наше приложение будет заниматься расчетом взаимодействия 2d объектов и отображением их на экране по таймеру, за основу возьмем приложение, использующее SurfaceView (из предыдущей статьи). В Activity поместим следующий, уже знакомый, код:

package com.WhiteRabbit.YoYo;

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 YoYoActivity 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);
    }
}

Реализация View (за исключением метода onDraw, который мы напишем позже), также не отличается от предыдущего проекта:

package com.WhiteRabbit.YoYo;

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

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 YoYoActivity ctx;
   
    public final float SZ_X = 35;    // Физические размеры
    public float SZ_Y;  
   
    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;                  // Экранные размеры
    private int szY;
   
    private Paint paint = new Paint();
   
    public MainView(YoYoActivity ctx) {
        super(ctx);
        this.ctx = ctx;
        this.getHolder().addCallback(this);
    }
   
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    @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 getSizeX(float x) {
        return (x * szX) / SZ_X;
    }
   
    public float getSizeY(float y) {
        return (y * szY) / SZ_Y;
    }
   
    @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();
        }
    }
}

Далее, создаем в проекте папку libs и помещаем в нее jar-файл, содержащий в себе реализацию физического движка:


После этого, можно приступать, к кодированию класса Task, в котором будут определяться моделируемые нами объекты, задачей которого будет периодический расчет изменения взаимного расположения объектов, с учетом их физического взаимодействия. Попробуем смоделировать наиболее сложный объект - веревку, подглядев реализацию в тесте "Bridge", поставляемым вместе с тестовой средой Box2d Testbed:

package com.WhiteRabbit.YoYo;

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

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.joints.RevoluteJointDef;

public class Task extends TimerTask  {
   
    private final static int CHAIN_CNT = 30;

    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private YoYoActivity ctx;
    private Timer timer = new Timer();
   
    private World             world;
    private Body              ground;
   
    public Task(YoYoActivity ctx) {
        this.ctx  = ctx;
        configure();
    }
   
    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y - (v.SZ_Y / 5);
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, 10.0f);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
       
        // Создать нить
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(0.5f, 0.125f);
        FixtureDef fd = new FixtureDef();
        fd.shape = shape;
        fd.density = 20.0f;
        fd.friction = 0.2f;
        RevoluteJointDef jd = new RevoluteJointDef();
        Body prevBody = ground;
        for (int i = 0; i < CHAIN_CNT; i++) {
            bd = new BodyDef();
            bd.type = BodyType.DYNAMIC;
            bd.position.set(X - 14.5f + (1.0f * i), Y);
            Body body = world.createBody(bd);
            body.createFixture(fd);
            v.addBody(body);

            Vec2 anchor = new Vec2(X - 15.0f + (1.0f * i), Y);
            jd.initialize(prevBody, body, anchor);
            world.createJoint(jd);
            prevBody = body;
        }
        Vec2 anchor = new Vec2(X - 15.0f + (1.0f * CHAIN_CNT), Y);
        jd.initialize(prevBody, ground, anchor);
        world.createJoint(jd);

        // Запустить рассчет
        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);
        // Обновить view
        ctx.getView().update();
    }
}
В этом коде мы делаем следующее:
  1. Наследуемся от TimerTask
  2. В констукторе класса, вызываем метод configure, создающий моделируемые нами объекты (на чем мы остановимся чуть позже) и инициализирующий таймер вызовом метода sheduleAtFixedRate 
  3. В периодически вызываемом методе run, выполняем рассчет итерации взаимодействия объектов (timeStep - интервал времени, на которое выполняется рассчет, остальные параметры метода step управляют количесвтом итераций и влияют на точность рассчетов) и перерисовываем view
  4. В методе terminate, завершаем работу таймера (если он существует) и уничтожаем его
Чтобы понять код создания объектов, нам потребуются некоторые базовые сведения о Box2d API, которые можно почерпнуть из официального руководства (также, весьма помогает декомпиляция библиотеки, с использованием, например, Java Decompiler). Что-же нам надо знать, чтобы понять код создания объектов в методе configure?

  1. Объект World отвечает за управление глобальными настройками "мира", действующими на все объекты (такими как гравитация), является фабрикой для моделируемых тел (Body) и соединений (Joint), а также позволяет обрабатывать события, связанные с взаимодействием объектов (коллизии), чем мы пока, правда, пользоваться не будем
  2. Объекты Body моделируют объекты реального мира (положение их центров масс в пространстве, угол наклона, скорости, ускорения и т.п.). Основным свойством Body является type, определяющий является ли объект статическим или динамическим. Разделение объектов на статические и динамические введено с целью оптимизации расчета взаимодействия объектов. Поскольку статические объекты находятся в неизменном положении, Box2d не выполняет расчет взаимодействия статических объектов. Все объекты положение которых должно изменяться в результате расчетов, должны быть помечены как динамические.
  3. Хотя Body обладают массой (и центром масс) сами по себе они друг с другом не взаимодействуют. Box2d вычисляет взаимодействие объектов Fixture, связанных с Body. С одним Body может быть связано несколько Fixture (что может быть удобно, если эти Fixture имеют различную плотность density или коэффициент трения friction). Body выступает в качестве фабрики при создании Fixture. Имеется несколько возможных применения различных Shape при создании Fixture (окружность, полигон), а также средства для упрощенного создания таких примитивов как прямоугольники (PolygonShape.setAsBox)
  4. Между Body могут быть определены разнообразные соединения Joint. В нашем приложении мы будем использовать рычажные соединения RevoluteJoint (полный список доступных соединений можно найти в руководстве Box2d)
 Рассмотрим действия выполняемые нами в методе configure:

  1. Из MainView получаем физические размеры доступной нам области мира и находим координаты центральной точки, от которой все будем позиционировать.
  2. Определяем вектор гравитации (в нашем случае, антигравитации, поскольку он направлен в сторону увеличения координаты Y) и создаем World (второй параметр doSleep, для нашего кода не существенен). Также создаем статический Body (ground), к которому можно привязывать неподвижные динамические объекты.
  3. Заранее создаем FixtureDef и RevoluteJointDef, которые будем многократно использовать в цикле
  4. Создаем 30 прямоугольников, связанных между собой рычажными соединениями. Крайние прямоугольники связаны рычажными соединениями с ground.
  5. Регистрируем созданные Body в MainView, для того, чтобы имелась возможность отображать их на экране
Для того, чтобы насладиться реалистичной физикой, осталось внести необходимые изменения в MainView:

package com.WhiteRabbit.YoYo;

...
public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    ...
    private List<Body> bodies = new ArrayList<Body>();
   
    ...
    public void addBody(Body body) {
        bodies.add(body);
    }

    @Override
    protected void onDraw(Canvas c) {
        c.drawColor(Color.BLACK);
        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
        float sx = 0, sy = 0, ex = 0, ey = 0;
        float px = 0, py = 0;
        boolean f = true;
        for (Body b: bodies) {
            Vec2 pos = b.getPosition();
            float x = getSizeX(pos.x);
            float y = getSizeX(pos.y);
            if (!f) {
                c.drawLine(px, py, x, y, paint);
            } else {
                sx = x;
                sy = y;
            }
            px = x;
            py = y;
            ex = x;
            ey = y;
            f = false;
        }
        float r = (10f * szX) / 320f;
        c.drawCircle(sx, sy, r, paint);
        c.drawCircle(ex, ey, r, paint);
    }
}
Здесь все просто. Мы запоминаем создаваемые Body в списке и отображаем их, соединяя их центры прямой линией. На концах нашей "веревки" рисуем две окружности (они нам понадобятся впоследствии для управления положением "веревки" на экране.
Теперь можно запустить наше приложение на выполнение:




Честно говоря, для меня было сюрпризом, что "веревка" провисла таким образом, ведь и сами созданные прямоугольники и соединения между ними, по идее, нерастяжимы. Но ... факты упрямая вещь :)