Kuuliloogika

Serveri-kliendi suhtluse peamine reegel on jätta kõik olulised arvutused serverile. Shooter-mängude kontekstis peaks server vastutama kuulilennu koordinaatide arvutamise ja tabamuste määramise eest ning klient hoolitsema ainult visuaalse kujutamise eest.

Kuulilennu trajektoori kujutamisviisid

Kuna tulistamise suund on harilikult sama mängija liikumissuunaga, siis eristatakse rohkemate piirangutega ning vähemate piirangutega kuulilennu trajektoori:

  1. Kraadinurgaga trajektoor – enim levinud top-down-vaatega mängudes. Kuul saab liikuda iga kraadinurgaga alt.

  2. Horisontaalne trajektor – enim levinud platformer-vaatega mängudes. Kuul saab liikuda vaid paremale või vasakule lineaarselt ning saab rakendada füüsikat Box2D-ga.

../_images/physics_shooting.gif

Kraadinurgaga tulistamisee näide hoopis platformer-vaatega mängus

Kuulilennu etapid ja serverilt tulev info

Kuulilend jaguneb kolmeks hästi eristatavaks serveri-kliendi suhtluse faasiks. Kuna server vastab alati viivitusega, pole mõistlik samu arvutusi teha mõlemal pool.

1. Tulistamine (klient saadab serverile)

  • Mängija hetkeasukoht ja liikumissuund.

  • Kas mängija saab ise sihtida (näiteks hiirega) ning on vaja arvutada sihitud-punkti kaugus mängijast või on tulistamise suund vaid liikumissuunaga seotud?

  • Kas vastase koordinaate on vaja? Kui kuulilennu arvutus toimub kliendi poolel (mittesoovitatav), võib vastane vahepeal liikuda, kuid uusi vastase koordinaate teab ainult server.

../_images/physics2_shooting.gif

Mängijale rakendub tagasiviiv jõud ehk knockback tulistades

2. Kuulilend (server saadab kliendile)

  • Server arvutab kuulilennu koordinaadid ja saadab need klientidele joonistamiseks.

  • Kas trajektoor sõltub füüsikast? Kas kuul aeglustub/kiireneb? Kas kiiruse määral on piirang?

  • Kui trajektoor on lineaarne, kas kuvada seda animatsioonina või Sprite-ina?

../_images/visual_shooting.gif

Lineaarne tulistamine

../_images/angry_birds.gif

Füüsikaga tulistamine

3. Tabamus või möödalask (server saadab kliendile)

Kui kuul tabab mängijat:

  • Füüsikaga mängus tuleb luua hitbox fixture-i abil ning tagasilöögiks on vaja ContactListener-i.

  • Füüsikata mängus, kuidas kaotada kuul pärast tabamist?

Kui kuul lendab mööda:

  • Millal kuul eemaldada? Kaamera vaateväljast lahkudes või füüsikaga mängus maastikku tabades?

  • Kas kuul tekitab kahju ka teistele objektidele maailmas? Mis raadiuses?

../_images/rotation_shooting.gif

Maailma reageering kuulile

Kuuli loomine:

Box2D füüsika Body ja Fixture'iga:

  • Kasutatakse Body Sprite-i jaoks ning Fixture-it kujuks, hitbox-i tegemiseks ja pinnaga reageerimiseks.

  • Realistlik erinevate impulsside ja jõudude tõttu, kuid CPU-le koormav.

  • Võimalik ka ilma Fixture-it kasutamata, kui kuulitabamusele järgneb eriloogika.

  • Box2D maailmasse saab panna nii dünaamilisi, staatilisi kui ka kinemaatilisi kehasid ehk panna kuulile reageerima rohkem kui ainult mängijad.

Muud meetodid:

  1. SpriteBatch-iga joonistamine ning Actor-ite massiivi kuulideks. Actionite Interpolation-klass lubab veidi füüsikat jäljendada, et kuulilend täiesti lineaarne poleks. CPU seisukohast keskmiselt koormav.

  2. Animation<TextureRegion> kasutamine. Väga kasulik, kui palju eriefekte kujutavaid kuule. CPU seisukohast keskmiselt kuni vähe-koormav.

  3. Stage ja Actor-i kasutamine. Kõige lihtsaim haldamine, kuid võib CPU ülekoormata.

  4. SpriteBatchiga ainult Sprite-ide joonistamine – kõige CPU-sõbralikum, kuid nõuab palju käsitsi haldamist.

Koodinäited

Body ja Fixture loomine kuulile

BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.DynamicBody;
bodyDef.position.set(startX, startY);
Body body = world.createBody(bodyDef);

CircleShape shape = new CircleShape();  // ümmargune hitbox
shape.setRadius(0.2f); // Box2D  maailm on meetrites mitte pikselites

FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = shape;
fixtureDef.density = 1f;
fixtureDef.friction = 0f;
fixtureDef.restitution = 1f;  // kui bouncy
fixtureDef.filter.categoryBits = BULLET_CATEGORY; // Kui kasutad kategooriaid fixtureDef.filter.maskBits = ENEMY_CATEGORY; // kui tahad näidata, mis objektide vastu saab vaid põrkuda
Fixture fixture = body.createFixture(fixtureDef);
shape.dispose();
Vector2 direction = new Vector2((targetX - startX), (targetY - startY)).nor();  // liikumissuund
body.setLinearVelocity(direction.scl(10f));

SpriteSheeti mitme animatsiooni-kaadriga ühes reas:

Texture bulletTexture = new Texture(Gdx.files.internal("bullets.png"));
TextureRegion[][] frames2D = TextureRegion.split(bulletTexture, oneFrameWidth, oneFrameHeight);
TextureRegion[] frames1D = new TextureRegion[frameRecurrence];

for (int i = 0; i < frameRecurrence; i++) {
    frames1D [i] = frames[0][i];
}
animation = new Animation<>(frameDuration, frames1D);

Tavaliselt on Spritesheet loodud stiilis, et tegelased või objektid on vaikimisi paremale suunatud. Vasakule suunamiseks saab peegeldada frames1D [i].flip(true, false) kaudu. frameDuration on vaja asendada float väärtusega ning frameRecurrence, oneFrameWidth, oneFrameHeight int väärtusega.

Knockbacki loomine:

private void applyBulletHitForce() {
   if (bulletHitForce != 0) {
       body.applyForceToCenter(new Vector2(bulletHitForce, 0), true);
       bulletHitForce *= 0.9f;  // vaikselt jõud kaob
   if (Math.abs(bulletHitForce) < Math.abs(bulletHitForce / 10f)) {
       bulletHitForce = 0;
   }
}

Objektidele füüsika lisamine

// bitimaskide andmine kokkupõrke määramiseks
public static final short LEVEL_BITS = 0x0001;  // põrkub kuulidega, maailma elemdid nagu seinad ja maapind
public static final short FRIENDLY_BITS = 0x0002;  // mängija
public static final short ENEMY_BITS = 0x0004;  // põrkub kuulidega, vaenlane
public static final short NEUTRAL_BITS = 0x0008;
public static final short FOOT_SENSOR = 0x0010;  // kas puutub maad mängija juures
public static final short RIGHT_WALL_SENSOR = 0x0020;
public static final short LEFT_WALL_SENSOR = 0x0040;

public void createPhysics(TiledMap map, String layerName, World world) {

    MapLayer layer = map.getLayers().get(layerName);
    MapObjects objects = layer.getObjects();
    Iterator<MapObject> objectIt = objects.iterator();

    while (objectIt.hasNext()) {
        MapObject object = objectIt.next();
        if (object instanceof TextureMapObject) continue;

        Shape shape = null;
        BodyDef bodyDef = new BodyDef();
        bodyDef.awake = false;
        bodyDef.type = BodyDef.BodyType.StaticBody;

        if (object instanceof RectangleMapObject) {
            shape = getRectangle((RectangleMapObject) object);
        } else if (object instanceof PolygonMapObject) {
            shape = getPolygon((PolygonMapObject) object);
        } else if (object instanceof PolylineMapObject) {
            shape = getPolyline((PolylineMapObject) object);
        } else if (object instanceof CircleMapObject) {
            shape = getCircle((CircleMapObject) object);
        } else {
            Gdx.app.log("Unrecognized shape", "" + object.toString());
            continue;
        }
        FixtureDef fixtureDef = new FixtureDef();
        fixtureDef.shape = shape;
        fixtureDef.filter.categoryBits = 0x0001;
        fixtureDef.filter.maskBits = (short) (PhysicsManager.FRIENDLY_BITS | PhysicsManager.ENEMY_BITS | PhysicsManager.NEUTRAL_BITS | PhysicsManager.FOOT_SENSOR | PhysicsManager.RIGHT_WALL_SENSOR | PhysicsManager.LEFT_WALL_SENSOR);

        Body body = world.createBody(bodyDef);
        body.createFixture(fixtureDef).setUserData("LEVEL");

        fixtureDef.shape = null;
        shape.dispose();
    }
}

Hiljem saame ContactListener-iga Collision-i "LEVEL" kutsudes.

Lingid: