Füüsika

Selles teemas tutvustakse mängude füüsika põhiaspekte, kasutades Java ja LibGDX. Vaatame liikumise mehaanikat ja gravitatsiooni käsitlemise üksikasju.

Kuidas alustada?

1. Kõigepealt peame looma klassi PlayerProcessor ja seadma muutujad meie tegelase hüppekõrguse, jooksukiiruse ja gravitatsiooni jaoks (seda kõike saab kohandada vastavalt oma soovile):

(Ärge kartke, et muutujaid on nii palju, sest enamik neist kasutatakse sujuvaks ja nauditavaks sõiduks, et silmale oleks meeldivam).

public class PlayerProcessor {
    //#region -- VARIABLES --

    // ==== OBJECT VARIABLES ==== //
    private World world;
    private Actor player;

    // ==== MOVEMENT ==== //
    private final float SPEED = 120f;
    private final float JUMP_HEIGHT = 400f;
    private final float ACCELERATION = 12f;
    private final float DECELERATION = 16f;
    private final float VELOCITY_POWER = 0.9f;

    private int moveInput = 1;

    // ==== JUMP MECHANICS ==== //
    private boolean canJump = true;

    // ==== GRAVITY ==== //
    private float gravityForce = Constants.GRAVITY_FORCE;

Mängumaailma (world) objekti kasutatakse PlayerProcessor klassis, et pääseda ligi erinevatele mängumaailma elementidele, nagu mängija (player), staatilised plokid, kuid sellest räägime lähemalt hiljem.

  • SPEED ja JUMP_HEIGHT: Tegelase kiirus ja hüppe kõrgus vastavalt.

  • ACCELERATION, DECELERATION, VELOCITY_POWER: Kiirendus, aeglustus, kiiruse võimsus. Need on parameetrid, mis on vajalikud sujuva ja silmale meeldiva tegelase liikumise arvutamiseks.

  • moveInput: Seda muutujat kasutatakse selleks, et näha, mis suunas tegelast juhatatakse nupuvajutusega (vasakule või paremale).

  • canJump: Boolean parameeter, mida kasutatakse hüpete arvutustes. Koodis on see määratud tõeseks, kui mängija seisab pinnal, ja see muutub vääraks, kui mängija teeb hüppe. See takistab mitu järjestikust hüpet enne kui mängija uuesti maandub.

  • gravityForce: Tähistab mängija objektile rakendatava gravitatsioonijõu suurust.

Funktsioonid horisontaalseks liikumiseks

2. Meil on vaja luua PlayerProccesor konstruktor:

public PlayerProcessor(World world) {
            this.world = world;
            player = this.world.getPlayer();
    }
  • this.world = world;: See võimaldab PlayerProcessor objektil suhelda mängumaailmaga.

  • player = this.world.getPlayer();: Mängija objekti kättesaamine world objektist ja selle salvestamine player muutujasse.

Sel viisil on PlayerProcessor objektil juurdepääs mängija objektile, et töödelda selle tegevusi.

3. Nüüd saame mõjutada mängija liikumist ning teha funktsiooni move(), et saavutada sujuv liikumine:

private void move() {
    // These variables are taken from YouTube tutorial on smooth platformer movements.
    float targetSpeed = moveInput * SPEED; // Direction of movement.
    float speedDifference = targetSpeed - player.getVelocity().x; // Difference between current desired velocity.
    float accelerationRate = (Math.abs(targetSpeed) > 0.01f) ? ACCELERATION : DECCELERATION;
    float movement = (float) Math.pow(Math.abs(speedDifference) * accelerationRate, VELOCITY_POWER) * Math.signum(speedDifference);

    player.getAcceleration().x = movement;
}

Funktsioon move() kasutab võrrandeid mängija liikumise sujuvaks kiirendamiseks ja aeglustamiseks.

  • targetSpeed: Soovitud liikumiskiiruse määramine suuna (moveInput) ja konstantse kiiruse (SPEED) põhjal.

  • speedDifference: Arvutatakse mängija praeguse kiiruse ja soovitud kiiruse vahe.

  • accelerationRate: Valib kiirendus- või aeglustuskiiruse sõltuvalt sellest, kas mängija liigub (kiirus != null) või seisab paigal.

  • movement: Arvutatakse liikumise summa, kasutades matemaatilist võrrandit sujuvate kiiruse muutuste jaoks.

4. Loome funktsiooni horizontalMovement() ning kasutame just loodud move() funktsiooni seal:

private void horizontalMovement() {
    boolean isLeftPressed = Gdx.input.isKeyPressed(Constants.LEFT_KEY);
    boolean isRightPressed = Gdx.input.isKeyPressed(Constants.RIGHT_KEY);
    if (isLeftPressed) {
        moveInput = -1;
        move();
    }
    if (isRightPressed) {
        moveInput = 1;
        move();
    }
    if (((!isLeftPressed && !isRightPressed) || (isLeftPressed && isRightPressed))) {
        player.getVelocity().x = 0f;
    }
}

Funktsioon horizontalMovement() vastutab selles kontekstis kasutaja sisendi käitlemise eest mängija horisontaalseks liikumiseks mängumaailmas.

  • isLeftPressed ja isRightPressed: Kontrollivad, kas klahvivajutus suunab mängija vasakule või paremale.

Kui isLeftPressed ja isRightPressed on mõlemad false või mõlemad on true, siis player.getVelocity().x on 0, mis tähendab, et horisontaalset liikumist ei toimu.

Vastasel juhul määratakse moveInput väärtuseks -1 (vasakule) või 1 (paremale), täpselt nagu x-teljel.

Vertikaalne liikumine

5. Realiseerime funktsiooni jump(), et kontrollida mängija hüppamise loogikat:

private void jump() {
    canJump = false;
    player.getVelocity().y = JUMP_HEIGHT;
}

Funktsioon jump() vastutab mängija hüppesündmuse käitlemise eest.

  • canJump: Väärtuseks on seatud false, see takistab järgnevaid tühikuklahvi vajutusi kuni järgmise maandumiseni.

  • player.getVelocity().y: Sellele on määratud JUMP_HEIGHT. See põhjustab mängija "põrgatamise" ülespoole.

6. Võimaldamaks mängijal hüppamine, teeme funktsiooni verticalMovement():

private void verticalMovement() {
    boolean isSpacePressed = Gdx.input.isKeyJustPressed(Constants.SPACEBAR);

    if (isSpacePressed && canJump) {
        jump();
    }
}

Funktsioon verticalMovement() vastutab selles kontekstis kasutaja sisendi käitlemise eest mängija vertikaalseks liikumiseks mängumaailmas.

  • isSpacePressed: Kontrollib, kas tühikuklahvi on vajutatud. See kontroll tagab, et hüpe toimub ainult siis, kui klahvi esimest korda vajutatakse, mitte kogu ooteaja jooksul.

Ehk, kui isSpacePressed ja canJump on true, siis mängija hüppab.

Gravitatsioon

7. Teeme funktsiooni gravity(), et rakendada mängijale gravitatsiooni vertikaalsel liikumisel:

private void gravity() {
            if (player.getVelocity().y < 0) gravityForce = Constants.GRAVITY_FORCE * Constants.GRAVITY_ACCEL;
    player.getAcceleration().y = -gravityForce;
    }

Funktsioon gravity() vastutab gravitatsioonijõu rakendamise eest mängija objektile. See funktsioon tagab mängija vertikaalse allapoole liikumise, simuleerides gravitatsiooni mõju.

Collision

8. Kokkupõrgete loogika kontrollimise jaoks teeme funktsiooni resolveCollision():

private boolean resolveCollision(Actor in, Static block, float dt) {
    cn = new Vector2();
    cp = new Vector2();
    float contactTime = 0f;
    if (player.DynamicAABB(in, block, cp, cn, contactTime, dt)) {
        cn = player.getContactNormal();
        cp = player.getContactPoint();
        t = player.getContactTime();
        in.getVelocity().x -= cn.x * Math.abs(in.getVelocity().x) * (1 - contactTime);
        in.getVelocity().y -= cn.y * Math.abs(in.getVelocity().y) * (1 - contactTime);
        return true;
    }
    return false;
}

Funktsioon resolveCollision(), teostab kokkupõrgete lahendamise mänguobjekti ja staatilise objekti (näiteks ploki) vahel, kasutades meetodit AABB, et määrata kahe ristkülikukujulise piirkonna ristumiskoht.

Parameetrid:

  • Actor in: Dünaamiline objekt, mis võib põrkuda staatilise plokiga.

  • Static block: Kokkupõrkega seotud staatiline objekt.

  • float dt: Aja delta (astumise kestus).

Selgitused loogikas:

  • cn: Tähendab contact normal. Kontaktnormaal on ühikvektor (vektor pikkusega = 1), mis osutab põrkepunktis staatilise objekti (ploki) pinnaga risti.

  • cp: Tähendab contact point. Kontaktpunkt on täpne asukoht maailmas, kus toimub kokkupõrge dünaamilise objekti (Actor in) ja staatilise objekti (Static block) vahel.

  • contactTime: Kontaktaeg on murdosaline aeg (0,0 kuni 1,0), mis näitab, millal ajaetapis (dt) kokkupõrge toimus.

in.getVelocity() roll:

  • cn.x ja cn.y skaleerivad kiiruse muutumist piki normaalsuunda.

  • Math.abs(in.getVelocity()) tagab, et reguleerimine sõltub ainult kiiruse suurusest.

  • (1 - contactTime) skaleerib kiiruse vähendamise kokkupõrke toimumisaja alusel (varasematel kokkupõrgetel on suurem mõju).

Funktsioonide rakendamine

9. Viimaks teeme funktsiooni update(), mis aitab rakendada eeltehtud funktsioone meie mängus:

public void update(float dt) {
    gravity();
    horizontalMovement();
    verticalMovement();
    changeState();

    player.getAcceleration().scl(dt);
    player.getVelocity().add(player.getAcceleration().x, player.getAcceleration().y);

    for (Static obj : world.collisionBlocks) {
        if (resolveCollision(player, obj, dt)) {
            if (player.getVelocity().y == 0f) canJump = true;
        }
    }

    player.previousDirection = player.direction;
    player.direction = moveInput;
    player.update(dt);
}

Funktsioon update(float dt) ajakohastab mängija asukohta ja tegeleb tema tegevustega mängu igal kaadril. Siin kasutatakse kõiki funktsioone, mida on vaja uuendada kaadrihaaval (nt tegelase liikumise kuvamiseks).

  • 5-6 koodirida: Korrutab mängija kiirenduse eelmisest kaadrist möödunud ajaga (dt - delta time) ja pärast uuendab mängija kiirenduse põhjal tema kiirust.

  • For loopis resolveCollision(): Käsitleb mängija kokkupõrkeid staatiliste objektidega (plokid) mängumaailmas.

  • Kolm viimast koodirida: Uuendab teavet mängija eelmise ja praeguse suuna kohta ning seejärel kutsub update(dt), et uuendada mängija positsiooni ja olekut.

Ja see ongi kõik! Teie tegelane saab liikuda ja hüpata.

movement