Collision

Collisioniga alustamine

Selles peatükis loome kokkupõrkesüsteemi (collision) kasutades LibGDX Box2D füüsikamootorit. Eesmärk on panna mängija ja kaardi objektid üksteisega füüsikaliselt reageerima.

Box2D aitab hallata:

  • Kokkupõrkeid

  • Liikumist

  • Füüsikat (nt gravitatsioon)

  • Erinevaid objektide tüüpe

Alternatiiv oleks kirjutada collision-süsteem nullist, kuid see on märksa keerulisem ja ajamahukam.

Collision süsteemi eeldused

Eeldame, et projektis on juba olemas:

  • Tiledis loodud kaart

  • Collision object layer

  • Kaart laaditakse LibGDX-is

TmxMapLoader mapLoader = new TmxMapLoader();
TiledMap map = mapLoader.load("maps/level1.tmx");  // Laeb Tiled kaardi failist

Box2D lisamine projekti

Box2D tuleb lisada projekti Gradle dependency kaudu.

core/build.gradle - siia lisame Box2D API, mis on vajalik kogu mängu loogika jaoks.

dependencies {
    api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
}

desktop/build.gradle – siia lisame Box2D native library (natives-desktop), et see töötaks lauaarvutites.

project(":desktop") {
    apply plugin: "java-library"

    dependencies {
        implementation project(":core")
        implementation "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-desktop"
    }
}

Box2D maailma loomine

Box2D kasutamiseks tuleb esmalt initsialiseerida füüsikamootor.

Box2D.init();

Seejärel loome füüsikamaailma (World).

private final World world;
private final Box2DDebugRenderer b2dr;

this.world = new World(new Vector2(0, 0), true);
this.b2dr = new Box2DDebugRenderer();

World klass määratleb Box2D maailma, kus toimub füüsika simuleerimine. World muutujas on Vector2 gravitatsiooni määramiseks ning doSleep, mis säästab arvutusvõimsust staatiliste objektide puhul. Top-down mängu jaoks on gravitatsioon (0, 0). Platformeri jaoks kasuta näiteks new Vector2(0, -9.8f) Kui doSleep on true, siis Box2D ei arvuta füüsikat kehadelt, mis ei liigu. Box2DDebugRenderer renderdab kehasid ja objekte. (See on testimiseks mõeldud.)

Box2D keha tüübid

Box2D-s on kolm peamist kehatüüpi:

StaticBody

  • Ei liigu

  • Ei reageeri jõududele

  • Kasutatakse seinte, põrandate ja platvormide jaoks

DynamicBody

  • Reageerib füüsikale

  • Mõjutavad gravitatsioon, jõud ja kokkupõrked

  • Kasutatakse mängijate, kastide ja muude liikuvate objektide jaoks

KinematicBody

  • Liigub käsitsi määratud kiirusega

  • Ei reageeri füüsikale

  • Kasutatakse näiteks liikuvate platvormide jaoks

Body ja Fixture

Box2D objektid koosnevad kahest osast.

Body

  • Füüsiline keha maailmas

  • Määrab asukoha ja liikumise

Fixture

  • Määrab keha kuju

  • Määrab kokkupõrke omadused (nt friction, restitution, density)

Collision objektide loomine

Tiledis saab collision objektid lisada object layerile. Need objektid saab Box2D kehadeks teisendada. Mängu kehade ja objektide lisamiseks loo uus klass, näiteks Box2DWorldGenerator.

BodyDef bdef = new BodyDef();
PolygonShape shape = new PolygonShape();
FixtureDef fdef = new FixtureDef();
Body body;

for (MapObject object : map.getLayers().get(2).getObjects().getByType(RectangleMapObject.class)) {

    Rectangle rectangle = ((RectangleMapObject) object).getRectangle();

    bdef.type = BodyDef.BodyType.StaticBody;

    bdef.position.set(
        (rectangle.getX() + rectangle.getWidth() / 2) / Demo.PPM,
        (rectangle.getY() + rectangle.getHeight() / 2) / Demo.PPM
    );

    body = world.createBody(bdef);

    shape.setAsBox(
        rectangle.getWidth() / 2 / Demo.PPM,
        rectangle.getHeight() / 2 / Demo.PPM
    );

    fdef.shape = shape;

    body.createFixture(fdef);
}

Oluline: pärast shape kasutamist tuleks see vabastada.

shape.dispose();

BodyDefi kasutatakse Box2D-s keha omaduste määratlemiseks. See sisaldab atribuute nagu keha tüüp (staatiline, kineetiline, dünaamiline), asend, nurk, lineaarne kiirus ja muud keha algsed omadused.

PolygonShape on Box2D klass, mida kasutatakse keha külge kinnitatud objekti kuju määratlemiseks.

FixtureDefi kasutatakse objekti omaduste (nt kuju, tiheduse, hõõrdumise jne) määratlemiseks. See “kinnitatakse” keha külge.

BodyDef ja FixtureDef erinevused

Omadus

BodyDef (Keha määratlus)

FixtureDef (Kokkupõrgete määratlus)

Mida määrab?

Keha üldised füüsikalised omadused

Kokkupõrgete ja füüsika omadused

Olulised parameetrid

type, position, linearVelocity, angle

shape, density, restitution, friction

Kas see lisatakse kehale?

Kasutatakse world.createBody(bdef); kaudu keha loomiseks

Lisatakse kehale body.createFixture(fdef); kaudu

for tsükli abil loome kehasid objektidest, mis on varem Tiled abil kaardile lisatud.

(Hiljem saad lisada teise sarnase tsükli ka teiste kujundite jaoks, näiteks ümmarguste kehade loomiseks.)

bdef.type = BodyDef.BodyType.StaticBody: määrab keha tüübiks StaticBody, mis ei liigu ega reageeri jõududele.

Seejärel määrame keha asukoha ristküliku keskpunkti ja shape saab kuju, mis on pool ristküliku laiusest ja pikkusest. Määrame objekti kujuks shape’i.

Lisaks peame render() meetodisse lisama read:

this.b2dr.setDrawBodies(true);
this.b2dr.render(world, camera.combined);

this.b2dr.setDrawBodies(true);:

  • b2dr.render(world, camera.combined) joonistab Box2D debug vaate ekraanile

  • b2dr.setDrawBodies(true) lubab kehade joonistamise debug vaates

  • Kui väärtus on false, siis kehasid ei joonistata

Mängu jooksutamisel võiksid nüüd ruudukujuliste objektide ümber olla sellised jooned:

1

Mängija keha loomine

Eeldame, et on olemas eraldi mängija klass. Mängija jaoks loome dünaamilise keha.

public Player(World world) {
    this.world = world;
    definePlayer();
}

Mängija klassi võiksid nüüd teha järgnevad muudatused:

private void definePlayer() {

    BodyDef bdef = new BodyDef();
    bdef.position.set(32 / Demo.PPM, 32 / Demo.PPM);
    bdef.type = BodyDef.BodyType.DynamicBody;

    b2body = world.createBody(bdef);

    CircleShape shape = new CircleShape();
    shape.setRadius(10 / Demo.PPM);

    FixtureDef fdef = new FixtureDef();
    fdef.shape = shape;

    b2body.createFixture(fdef);

    shape.dispose();
}

PPM (Pixels Per Meter)

PPM ehk pixels per meter on konstant, mille võiksid defineerida oma mängu põhiklassis või eraldi konstantidele pühendatud klassis. (väärtus võiks olla nt 100).

Kuna Box2D kasutab mõõtühikuna meetreid, tuleb mängu objektid skaleerida väiksemaks. Vastasel juhul muutuvad kehad Box2D jaoks liiga suureks ja simulatsioon võib muutuda ebastabiilseks.

Seetõttu kasutatakse skaleerimiseks konstantset väärtust:

public static final float PPM = 100f;

Näiteks oleks meie b2body raadius ilma PPMita 10 meetrit.

Samuti oleks vaja läbi jagada PPM'ga eelnevalt defineeritud väärtused:

this.viewport = new FitViewport(
Gdx.graphics.getWidth() / Demo.PPM,
Gdx.graphics.getHeight() / Demo.PPM,
camera
);

this.renderer = new OrthogonalTiledMapRenderer(map, 1 / Demo.PPM);

Mängija liigutamine

Box2D keha saab liigutada mitmel viisil.

applyLinearImpulse(impulse, point, wake)

  • Lisab kehale kiire tõuke

  • Sobib hüpeteks või kiireks liikumiseks

impulse – määrab jõu vektori (kiirus ja suund), mida rakendatakse kehasse.

point – punkt, kus jõud rakendatakse (siin on kasutatud keha keskmist punkti playerBody.getWorldCenter()).

wake – määrab, kas keha peaks pärast jõu rakendamist olema ärkvel (kas keha hakkab liikumiseks arvutama järgmise sammu või mitte).

playerBody.applyLinearImpulse(
    new Vector2(0.5f, 0),
    playerBody.getWorldCenter(),
    true
);

setLinearVelocity(vx, vy)

  • Määrab kehale otse kiiruse

  • Sobib täpse kontrolliga liikumiseks (nt top-down mängudes)

vx – keha horisontaalne kiirus (X-telg)

vy – keha vertikaalne kiirus (Y-telg)

playerBody.setLinearVelocity(2f, playerBody.getLinearVelocity().y);

Nüüd peame lisama update() meetodisse:

this.player.b2body.setLinearVelocity(Vector2.Zero);

Vastasel juhul hõljub mängija keha mööda maailma ringi ning ta reageerib kasutaja sisendile vaevumärgatavalt. Seda kasutatakse peamiselt top-down mängudes, kus mängija ei kasuta gravitatsiooni. Platformerites ei tohiks seda kasutada, sest see nullib ka gravitatsiooni mõju.

Füüsikamaailma uuendamine

Box2D maailma tuleb igal kaadril uuendada.

world.step(1/60f, 6, 2);

world.step() uuendab Box2D füüsikasimulatsiooni ühe ajasammu võrra. Tüüpilise mängu jaoks sobivad need väärtused hästi, kuid vajadusel saab neid kohandada.

Parameetrid:

1/60f — simulatsiooni ajasamm

6 — kiiruse iteratsioonid

2 — positsiooni iteratsioonid

Nüüd mängu jooksutades peaksid kaardil nägema ringi, millega saad ringi liikuda. Ring peaks objektidega kokku põrkama.

2

Kui collision ei tööta

Kui kokkupõrked ei tööta, kontrolli järgmisi asju:

  • Kas world.step(...) kutsutakse igal kaadril

  • Kas debug renderer näitab kehasid

  • Kas PPM skaleerimine on järjepidev

  • Kas keha tüüp on õige (StaticBody vs DynamicBody)

  • Kas Tiledis on õige layer

  • Kas fixture loodi õigesti

  • Kas shape.dispose() kutsuti alles pärast fixture loomist