понедельник, 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;
    }
}


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


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

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

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