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

Птичка на проводе

После того как мы научились управлять положением нашей "веревочки", пришла пора заставить по ней что-то двигаться. Первое, что мы замечаем, это то, что наш интерфейс IObject спроектирован не очень удачно. С точки зрения логики приложения, классу Task совершенно незачем знать про такие детали реализации, как addBody и addHandle. Кроме того, низкоуровневому коду создания объектов Box2d не место в классе Task, поскольку это детали реализации наследников IObject. Уберем из IObject лишние методы:

package com.WhiteRabbit.YoYo.objects;

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

public interface IObject {
    void draw(Canvas c, Paint p);
    boolean onTouchEvent(MotionEvent event);
}
Также, для нашего удобства, создадим абстрактный класс, предоставляющий пустую реализацию метода onTouch:

package com.WhiteRabbit.YoYo.objects;

import android.view.MotionEvent;

public abstract class AbstractObject implements IObject {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }
}
Перенесем код создания объектов Box2d в класс Cord:

package com.WhiteRabbit.YoYo.objects;

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

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.MainView;

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

public class Cord extends AbstractObject {
   
    private final static int CHAIN_CNT = 30;
   
    private MainView view;
    private List<Body> bodies = new ArrayList<Body>();
    private List<Handle> handles = new ArrayList<Handle>();
   
    public Cord(MainView v, float X, float Y) {
        this.view = v;
        World world = v.getWorld();
        Body ground = v.getGround();
   
        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;
        fd.filter.groupIndex = -1;
        Body prevBody  = ground;
        Body firstBody = null;
        for (int i = 0; i < CHAIN_CNT; i++) {
            BodyDef 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.setBullet(true);
            body.createFixture(fd);
            bodies.add(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;
        handles.add(new Handle(v, 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();
        handles.add(new Handle(v, world.createJoint(mj), prevBody));
        v.addObj(this);
    }
   
    @Override
    public void draw(Canvas c, Paint p) {
        p.setColor(Color.WHITE);
        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;
    }
}
В класс MainView добавляем совершенно очевидную реализацию методов, отвечающих за централизованное хранение и получение объектов world и ground:

package com.WhiteRabbit.YoYo;

...
public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    ...   
    private World world;
    private Body ground;
   
    ...
    public void setWorld(World world, Body ground) {
        this.world = world;
        this.ground = ground;
    }
   
    public World getWorld() {
        return world;
    }
   
    public Body getGround() {
        return ground;
    }
    ...
}
Теперь добавим новый динамический объект, который можно будет катать по нашей "веревочке":

package com.WhiteRabbit.YoYo.objects;

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.FixtureDef;

import com.WhiteRabbit.YoYo.MainView;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class YoYo extends AbstractObject {
   
    private static final float INT_RADIUS = 0.5f;
    private static final float EXT_RADIUS = 3f;

    private MainView view;
    private Body body = null;
   
    public YoYo(MainView view, float x, float y) {
        this.view = view;
        BodyDef bd = new BodyDef();
        bd.type = BodyType.DYNAMIC;
        bd.position.set(x, y);
        body = view.getWorld().createBody(bd);
        body.setBullet(true);
        CircleShape cs = new CircleShape();
        FixtureDef fd = new FixtureDef();
        fd.shape = cs;
        fd.density = 1f;
        fd.friction = 0.5f;
        cs.m_radius = INT_RADIUS;
        body.createFixture(fd);
        cs.m_radius = EXT_RADIUS;
        fd.density = 10.0f;
        fd.filter.groupIndex = -1;
        body.createFixture(fd);
        view.addObj(this);
    }
   
    @Override
    public void draw(Canvas c, Paint p) {
        p.setColor(Color.GREEN);
        p.setAlpha(0x7F);
        Vec2 pos = body.getPosition();
        float x = view.xFromWorld(pos.x);
        float y = view.yFromWorld(pos.y);
        c.drawCircle(x, y, view.yFromWorld(EXT_RADIUS), p);
        p.setAlpha(0xFF);
        c.drawCircle(x, y, view.yFromWorld(INT_RADIUS), p);
    }
}


Наш объект состоит из двух вложенных окружностей, большая из которых не взаимодействует с прямоугольниками, составляющими "веревочку", благодаря установке свойства filter.groupIndex у соответствующей fixture (объекты имеющие одинаковый отрицательный groupIndex никогда не взаимодействуют). Благодаря тому, что мы перенесли большую часть кода в конструкторы объектов, реализация класса Task упростилась:

package com.WhiteRabbit.YoYo;

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

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

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

public class Task extends TimerTask {
   
    private final static int G_FORCE   = 10;
    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, G_FORCE);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
        v.setWorld(world, ground);
       
        // Создать нить
        new Cord(v, X, Y);
       
        // Создать волчок
        new YoYo(v, X - 14.5f + (1.0f * CHAIN_CNT / 5), Y - 10f);

        // Запустить рассчет
        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);
        // Изменить направление силы гравитации
        Vec2 gravity = ctx.getView().getGVect(G_FORCE);
        if (gravity != null) {
            world.setGravity(gravity);
        }
        // Обновить view
        ctx.getView().update();
    }
}
Готово :) Запускаем приложение под отладчиком и видим ... как волчок проваливается сквозь нить так, как если бы ее не существовало вовсе :( Дело в том, что и волчок и прямоугольники, составляющие собой нить представляют собой динамические объекты (в противном случае, они попросту оставались бы неподвижными). Рассчет коллизий движущихся объектов - дело крайне ресурсоемкое, и, как показывает практика, в Box2d, не работающее (во всяком случае, при взаимодействии мелких объектов). Вообще говоря, на этот случай, существует параметр setBullet, управляющий этой оптимизацией, но, как мы видим, его установка не приводит к какому-либо видимому эффекту.
Единственный способ, которым мне удалось решить эту проблему, является увеличение размеров взаимодействующих объектов. Увеличим ширину прямоугольников, составляющих нить:

package com.WhiteRabbit.YoYo.objects;

...
public class Cord extends AbstractObject {
 
    ...   
    public Cord(MainView v, float X, float Y) {
        this.view = v;
        World world = v.getWorld();
        Body ground = v.getGround();
   
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(0.5f, 3 /*0.125f*/);
                     . . .
    }
    ...   
}
и снова запустим приложение:


Что-же, получилось явно не совсем то, что мы хотели, но зато мы узнали неприятную особенность Box2d, которую в дальнейшем будем иметь в виду.

Комментариев нет:

Отправить комментарий