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:
Kraadinurgaga trajektoor – enim levinud
top-down
-vaatega mängudes. Kuul saab liikuda iga kraadinurgaga alt.Horisontaalne trajektor – enim levinud
platformer
-vaatega mängudes. Kuul saab liikuda vaid paremale või vasakule lineaarselt ning saab rakendada füüsikatBox2D
-ga.

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.

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?
![]() Lineaarne tulistamine¶ |
![]() 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 vajaContactListener
-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?

Maailma reageering kuulile¶
Kuuli loomine:¶
Box2D füüsika Body ja Fixture'iga:¶
Kasutatakse
Body
Sprite
-i jaoks ningFixture
-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:¶
SpriteBatch
-iga joonistamine ningActor
-ite massiivi kuulideks.Actionite
Interpolation
-klass lubab veidi füüsikat jäljendada, et kuulilend täiesti lineaarne poleks.CPU
seisukohast keskmiselt koormav.Animation<TextureRegion>
kasutamine. Väga kasulik, kui palju eriefekte kujutavaid kuule.CPU
seisukohast keskmiselt kuni vähe-koormav.Stage
jaActor
-i kasutamine. Kõige lihtsaim haldamine, kuid võibCPU
ülekoormata.SpriteBatchiga
ainultSprite
-ide joonistamine – kõigeCPU
-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.
Näidismängu laskmise töötlemine¶
Näidismängus lendab kuul neljas suunas: üles, alla, vasakule, paremale. Kogu lasu tee algab kliendist – klahvivajutuste töötlejas. Kui vajutatakse kindlat klahvi, saadab klient serverile sõnumi lasu kohta kindlas asendis.
public void handleShootingInput() {
var shootingMessage = new PlayerShootingMessage();
// detect key presses and send shooting message to the server
if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.LEFT)) {
shootingMessage.setDirection(Direction.LEFT);
} else if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.RIGHT)) {
shootingMessage.setDirection(Direction.RIGHT);
} else if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.UP)) {
shootingMessage.setDirection(Direction.UP);
} else if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.DOWN)) {
shootingMessage.setDirection(Direction.DOWN);
}
// don't send anything if player is not shooting
if (shootingMessage.getDirection() == null) return;
// message is sent to the server
ServerConnection
.getInstance()
.getClient()
.sendUDP(shootingMessage);
}
Võetud siit
Pärast seda teeb server kontrolli: kas kasutaja ei tulista liiga tihti – kui eelmisest lasust pole möödunud määratud viivitus, siis ei juhtu midagi. Kui kõik on korras, luuakse Bullet-klassi objekt, mis salvestab endasse suuna, koordinaadid ja kasutaja ID, kes lasu tegi.
package ee.taltech.examplegame.server.game.object;
import lombok.Getter;
import lombok.Setter;
import message.dto.BulletState;
import message.dto.Direction;
import static constant.Constants.BULLET_SPEED;
@Getter
@Setter
public class Bullet {
private final Direction direction;
private float x;
private float y;
private int shotById;
public Bullet(float x, float y, Direction direction, int shotByPlayerWithId) {
this.x = x;
this.y = y;
this.direction = direction;
this.shotById = shotByPlayerWithId;
}
public void update() {
switch (direction) {
case UP -> y += BULLET_SPEED;
case DOWN -> y -= BULLET_SPEED;
case LEFT -> x -= BULLET_SPEED;
case RIGHT -> x += BULLET_SPEED;
}
}
public BulletState getState() {
BulletState bulletState = new BulletState();
bulletState.setX(x);
bulletState.setY(y);
bulletState.setDirection(direction);
return bulletState;
}
}
Võetud siit
Edasi toimub kuuli ja mängijate kokkupõrke töötlemine. Selleks tuleb esmalt määrata iga osaleva objekti hitBox – objektile või kehale kuuluv ruum. Kui kahe objekti hitBox-id puutuvad kokku, toimub kokkupõrge. Sellisel juhul peame selle kuidagi käsitlema – kas takistama kahel objektil teineteisest läbi minemast (näiteks kui kaks tegelast põrkuvad), või peab midagi juhtuma. Meie näites, kui kuul tabab mängijat – mängija kaotab elu ja kuul kaob.
Allolevas koodis toimubki kahe objekti kokkupõrke kontroll ja reageerimine sellele.
package ee.taltech.examplegame.server.game;
import com.esotericsoftware.minlog.Log;
import ee.taltech.examplegame.server.game.object.Bullet;
import ee.taltech.examplegame.server.game.object.Player;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import static constant.Constants.ARENA_LOWER_BOUND_X;
import static constant.Constants.ARENA_LOWER_BOUND_Y;
import static constant.Constants.ARENA_UPPER_BOUND_X;
import static constant.Constants.ARENA_UPPER_BOUND_Y;
import static constant.Constants.PLAYER_HEIGHT_IN_PIXELS;
import static constant.Constants.PLAYER_WIDTH_IN_PIXELS;
/**
* Handles bullet collisions with players and arena boundaries.
*/
public class BulletCollisionHandler {
/**
* Checks for collisions between bullets and players, removes bullets that hit players or moved out of bounds.
*
* @param bullets The list of bullets in the game.
* @param players The list of players to check for collisions.
* @return A list of remaining bullets after handling collisions.
*/
public List<Bullet> handleCollisions(List<Bullet> bullets, List<Player> players) {
List<Bullet> bulletsToBeRemoved = new ArrayList<>();
for (Player player : players) {
Rectangle hitBox = constructPlayerHitBox(player);
for (Bullet bullet : bullets) {
// register a hit only if the bullet was shot by a different player
if (bullet.getShotById() != player.getId() && hitBox.contains(bullet.getX(), bullet.getY())) {
player.decreaseLives();
bulletsToBeRemoved.add(bullet);
Log.info("Player with id " + player.getId() + " was hit. " + player.getLives() + " lives left.");
}
}
}
bulletsToBeRemoved.addAll(findOutOfBoundsBullets(bullets));
bullets.removeAll(bulletsToBeRemoved); // remove bullets that hit a player or move out of bounds
return bullets;
}
/**
* Finds bullets that are out of the arena bounds.
*
* @param bullets All active bullets.
* @return Bullets that are out of bounds.
*/
private List<Bullet> findOutOfBoundsBullets(List<Bullet> bullets) {
List<Bullet> outOfBoundsBullets = new ArrayList<>();
for (Bullet bullet : bullets) {
if (bullet.getX() < ARENA_LOWER_BOUND_X ||
bullet.getX() > ARENA_UPPER_BOUND_X ||
bullet.getY() < ARENA_LOWER_BOUND_Y ||
bullet.getY() > ARENA_UPPER_BOUND_Y
) {
outOfBoundsBullets.add(bullet);
}
}
return outOfBoundsBullets;
}
/**
* Constructs a rectangular hitbox for a player based on their position.
* A hitbox is essential for detecting collisions between players and bullets.
* Only bullets that visually overlap with the player's sprite register as hits.
*/
private Rectangle constructPlayerHitBox(Player player) {
return
new Rectangle(
(int) (player.getX()), // bottom left corner coordinates
(int) (player.getY()), // bottom left corner coordinates
(int) PLAYER_WIDTH_IN_PIXELS, // rectangle width
(int) PLAYER_HEIGHT_IN_PIXELS // rectangle height
);
}
}
Võetud siit
Ja pärast seda saadab server kõigile mängijatele, sealhulgas ka sellele, kes lasi, sõnumi kuuli oleku kohta – kas tema koordinaadid või sõnumi mängija tabamisest.