Tiled¶
Tiled on kasutajasõbralik programm 2D kaartide loomiseks.
See võimaldab:
Hõlpsasti määrata/muuta kaartide suurusi.
Tõmmata alla kõikvõimalikke kujunduselemente.
Kõige peamine: selle abil saab väga lihtsasti hallata
collisioneid
.
Tilediga alustamine¶
1. Mine siia leheküljele ning tõmba endale Tiled alla.
2. Kui oled programmi käima tõmmanud, siis:
Vali
New Map…
ning seejärel saad valida kaardi ja tile’ide suuruseid. Enamik internetist leitavaidtilesete
on küll mõeldud 32x32px suurustele tile’idele, aga nii tile’ide kui kaardi suurust on hiljem võimalik väga lihtsasti muuta.
Järgmiseks peaks avanema tühi ruudustik ja kaardil on üksainus layer
.
3. Kõigepealt peame lisama oma projektile tileseti
, mis näeb välja midagi sellist:

Tilesetid
koosnevad tile’idest ning ongi sisuliselt need, millest me
oma kaardi visuaalse poole ehitame.
Mõned lingid, kust saab tilesete alla laadida:
Samuti on võimalik kujundada ise oma tilesete programmidega nagu Blender, Inkscape või GIMP, aga see on üpriski aeganõudev tegevus ning internetis on väga lai valik erinevaid tasuta saadaval olevaid sete.
4. Kui oled midagi välja valinud ning alla laadinud, siis:
Mine
File -> New -> New Tileset
, ning lisa sinna omatileset
.
Sulle võiks vastu vaadata midagi sellist:

Nagu näed, siis minu tilesetis
on olemas:
Tausta jaoks mõeldud ruudud.
Ruudud, millest saab ehitada erinevaid objekte (lombid, trepid, kastid jms).
Tilesete
võid enda kaardile lisada mitu ning neid saab
vabalt omavahel kombineerida.
Tiled ei erista neid ruute aga omavahel kuidagi ning seetõttu peame
lisama oma kaardile juurde uue layeri
.
Kui ehitaksime tausta valmis ja hakkaksime sama kihi peale objekte laduma, siis joonistataks taustal olevad ruudud lihtsalt üle.
5. Paremal tileset’ide
menüü kohal on koht layerite
haldamiseks. Vali sealt uus tile layer
:

Esimesele
layerile
panemegi taustaks mõeldud tile’id (näiteks muru, vett, mulda jms).
Kui soovid suuremaid alasid katta mingit ühte sorti
tile’iga, siis selleks on väga kasulik bucket fill
tööriist, mis täidab
kõik kaardil veel täitmata alad:

6. Seejärel vajuta äsjaloodud layeri
peale ning saad taustale hakata lisama
objekte.
Objektide üksteise otsa ladumiseks korda seda sammu nii mitu korda kui vaja.
7. Kui oled ära märgistanud kõik soovitud objektid, siis salvesta kaart enda projekti assets kausta.
Samuti lisa sinna kõik tilesetid, mida kaardi loomisel kasutasid.
Kaart peaks sulle salvestuma .tmx failina.
Arvatavasti on Sul juba olemas mingisugune GameScreen
(võiks
implementida Screen interface’i
) klass, mis haldab Sinu mängus
toimuvat.
8. Klassi konstruktorisse on vaja nüüd lisada järgnevad väljad:
private final TiledDemo tiledDemo;
public static OrthographicCamera camera;
private final FitViewport viewport;
private final TmxMapLoader mapLoader;
private final TiledMap map;
private final OrthogonalTiledMapRenderer renderer;
kus
this.camera = new OrthographicCamera();
this.viewport = new FitViewport(800,400, camera);
this.mapLoader = new TmxMapLoader();
this.map = mapLoader.load("demomap.tmx");
this.renderer = new OrthogonalTiledMapRenderer(map);
this.camera.position.set(viewport.getWorldwidth() / 2, viewport.getWorldHeight() / 2, 0);
Kaamera: saab liigutada, pöörata, zoomida, fikseerida mängijale jne. Positsioneerime selle enda kaardi keskele.
On olemas ka PerspectiveCamera()
, kuid erinevalt perperspektiivkaamerast, ei võta ortograafiline kaamera
perspektiivi arvesse, mis tähendab, et objektid kuvatakse nende tegelikus suuruses, olenemata nende kaugusest kaamerast.
ViewPort: aitab skaleerida mängu ekraani suuruse järgi.
ViewPorte
on mitmeid, mina kasutan FitViewport()
.
FitViewport(worldWidth, worldHeight, camera)
tagab, et kuvatav sisu mahub hästi vaatesse, olenemata ekraani suuruse või aspect ratio
muutustest.
MapLoader: laeb kaardi mängu.
MapRenderer: renderdab kaardi.
9. Update
meetodisse lisa camera.update()
ning renderer.setView(camera)
.
Kui tahad kaardil ka ringi liikuda, aga ringi liikumise loogikat Sul
veel pole, siis tee mingi lihtne input handling
meetod, mis reageerib
näiteks ekraanipuudutusele.
Näiteks võid teha midagi sellist, et liikuda kaameraga mööda x ja y telge:
public void handleinput(float delta) {
if (Gdx.input.isTouched()) {
this.camera.position.x += 100 * delta;
this.camera.position.y += 50 * delta;
}
}
Screen interface’i
implementeerimisel tuleb kaasa käputäis erinevaid
@Override
annotatsiooniga meetodeid.
10. Otsi üles render()
meetod ja lisa sinna järgnevad väljad, kui sul neist mõnda veel ei ole:
@Override
public void render(float delta) {
update(delta);
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
this.renderer.render();
this.game.batch.setProjectionMatrix(camera.combined);
this.camera.update();
}
11. Pildil on kasutatud muutujat game
, mille peaksid konstruktorisse lisama kujul:
this.game = game
kus game
on teie mängu põhiklass. Lisa see argumendina GameScreen
klassi konstruktorisse.
Mängu põhiklass peaks extendima LibGDX sisseehitatud klassi Game
.
12. Kirjelda klassis muutuja:
Game game
13. Juurde tekkinud @Override
annotatsiooniga create()
meetodisse lisa rida:
game = this;
Põhiklass võiks välja näha midagi sellist:
public class Demo extends Game {
public SpriteBatch batch;
public static final float PPM = 120;
@Override
public void create () {
this.batch = new SpriteBatch();
setScreen(new GameScreen(this));
}
@Override
public void render () {
super.render();
}
@Override
public void dispose () {
this.batch.dispose();
}
}
Kui proovid oma mängu tööle panna, siis peaks Sulle kuvatama sinu kaart.
Suure tõenäosusega pead muutma viewportis
worldWidth
ja worldHeight
väärtusi
nagu minul juhtus:

Collisionid¶
14. Lisame enda kaardile veel ühe layeri
, seekord aga collisionite
jaoks:

15. Ülevalt menüüst saad valida sobiva kujundi ning selle abil hakata tõmbama jooni ümber kõikide objektide, millega sa tahad, et maailmas olevad mängijad, vaenlased jms kokku põrkaksid.
Vastasel juhul saab nendest lihtsalt üle kõndida nagu tavalisest taustaks mõeldud ruutudest.


Pro tip: Vaikimisi saavad mängijad, vastased, kaamera jms vabalt
kaardilt välja liikuda. Selle vältimiseks on kõige lihtsam viis teha
piisavalt suur kaart ning piirata selle sees mängimiseks mõeldud ala
object layeri
objektidega.
LibGDX-is on vägagi mõistlik kasutada Box2D füüsikaliidest, mille abil saab hallata:
Collisioneid
Mängija liikumist
Gravitatsiooni (nt platformerites) jne.
Selle jaoks tuleb build.gradle
faili vajalik dependency lisada. (Sõber
GPT aitab)
16. Järgmiseks täiendame veel oma GameScreen
klassi konstruktorit:
private final World world;
private final Box2DDebugRenderer b2dr;
this.world = new World(new Vector2(0, 0), true);
this.b2dr = new Box2DDebugRenderer();
World
defineeribki meile Box2D maailma, milles eksisteerib füüsika.World
muutujas onVector 2
gravitatsiooni määramiseks ningdoSleep
selleks, et säästa aega nende objektide pealt, mis on staatilised.
Selle maailma sees on kehad (body
) ja objektid (fixture
). Fixture
kinnitatakse keha külge, et anda talle kuju, võime teiste kehadega kokku
põrgata jne.
Box2DDebugRenderer
renderdab kehasid ja objekte.
17. Mängu kehade ja objektide lisamiseks tee uus klass, näiteks Box2DWorldGenerator
, kuhu anna kaasa
GameScreen
klassi.
Mina teen seda oma GameScreeni
konstruktoris:
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);
}
BodyDefi
kasutatakse Box2D-s keha omaduste määratlemiseks.See sisaldab atribuute nagu keha tüüp (staatiline, kineetiline, dünaamiline), asend, kaal, kiirus jne.
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.
For loopi abil hakkame looma kehasid objektidest, mis me varem Tiled abil enda kaardile lõime.
Nagu näed, siis mina saan enda objektid teisest layerist
: loendamist alustatakse 0st ja
minu kaardis on kokku 3 layerit
, millest objektide layer
on viimane.
Iga teises layeris
oleva RectangleMapObject
jaoks otsib see ristküliku,
mis määrab objekti piirid.
(Hiljem saad lisada teise samasuguse loopi ka teiste kujundite jaoks, näiteks ringikujuliste kehade loomiseks.)
bdef.type = BodyDef.BodyType.StaticBody
: määrab keha tüübiksStaticBody
, mis ei liigu ega reageeri jõududele.
Seejärel määrame keha asukoha ristküliku keskele ja shape
saab enda
kujuks pool ristküliku laiusest ja pikkusest. Määrame objekti kujuks
shape’i
.
Lõpuks loome keha, kasutades enne valmis tehtud fdefi
.
20. Lisaks peame render()
meetodisse lisama read:
this.b2dr.render(world, camera.combined);
this.b2dr.setDrawBodies(true);
this.b2dr.setDrawBodies(true);
:
Renderdab meie objektidele ümber jooned, et saaksime paremini vajadusel
debug-ida
.Kui sulgudesse panna
false
, saame jooned peita.
Mängu jooksutamisel võiksid nüüd ruudukujuliste objektide ümber olla sellised jooned:

Arvatavasti on sul olemas juba mingi klass mängija jaoks.
18. Mängija klassi võiksid nüüd teha järgnevad muudatused:
Uued muutujad
b2body
(klassBody
).World
Konstruktor võiks olla midagi sellist (lisaks olemasolevale loogikale):
public Player(World world) {
this.world = world;
definePlayer();
}
19. Uus meetod definePlayer()
.
Teeme midagi sarnast, mis enne objektide genereerimisega:
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);
FixtureDef fdef = new FixtureDef();
CircleShape shape = new CircleShape();
shape.setRadius(10 / Demo.PPM);
fdef.shape = shape;
b2body.createFixture(fdef);
}
Näed, et igal pool on arvulised väärtused jagatud muutujaga PPM
.
PPM
ehk pixels per meter
on konstant, mille võiksid defineerida oma mängu
põhiklassis (väärtus võiks olla nt 100).
Kuna Box2D engine kasutab meetreid, siis peame skaleerima oma mängu kasutama väiksemaid suurusi.
Näiteks oleks meie b2body
raadius ilma PPMita
10 meetrit.
20. Nüüd navigeeri uuesti GameScreen
klassi ja jaga seal ka läbi PPMiga
järgnevad 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);
this.bdef.position.set((rectangle.getX() + rectangle.getWidth() / 2) /
Demo.PPM, (rectangle.getY() + rectangle.getHeight() / 2) / Demo.PPM);
this.shape.setAsBox(rectangle.getWidth() / 2 / Demo.PPM,
this.rectangle.getHeight() / 2 / Demo.PPM);
Kui sul seda veel pole, siis võiksid luua mängijale liikumisloogika.
Kui sul see juba on, siis võiksid seda muuta nii, et liigutad b2body
,
rakendades sellele applyLinearImpulse
meetodit.
21. Nüüd peame lisama update()
meetodisse:
this.player.b2body.setLinearVelocity(Vector2.Zero);
sest vastasel juhul hõljub mängija keha mööda maailma ringi ning ta reageerib kasutaja sisendile vaevumärgatavalt.
22. Samuti lisame rea:
this.world.step(1/60f, 6, 2);
See määrab kui kiiresti Su mängus igasuguseid kokkupõrkeid jms handlitakse. Selle väärtusi pole otseselt muuta vaja.
23. Viimaseks lisame kaks rida, mis fikseerivad kaamera mängija peale:
this.camera.position.x = player.b2body.getPosition().x;
this.camera.position.y = player.b2body.getPosition().y;
Nüüd jooksuta jälle oma mängu ning:
Peaksid kaardil nägema ringi, millega saad ringi liikuda.
Kaamera peaks olema fikseeritud ringile.
Ring peaks
collide'ima
objektidega, mille enne genereerisime.

Fog of War¶
Fog of War on kontseptsioon, millega kirjeldatakse kaardil varjatud alasid. Seda kasutatakse sageli reaalaja strateegiamängudes, kuid ka käigupõhistes mängudes.

Paljud 4X (eXplore, eXpand, eXploit, eXterminate) mängud kaasaarvatud “Civilization” kasutab “Fog of war” efekti.
"Fog of war" lisab mängule:
Ajendi mängukaarti avastada, et suurendada oma piiratud teadmisi selle kohta.
Realistlikkust: Simuleeritakse lahinguvälja, kus maastiku omadused ja vaenlaste asukohad selguvad alles luure käigus.
Põnevust: Tundmatud piirkonnad avanevad järk-järgult, samal ajal kui vaenlaste positsioon ja väesuurus on vaid ajutised teadmised, mis võivad muutuda, kui mängija nähtavusalast lahkub.

"Dark chess" mängus näeb mängija vaid mänguruute, kuhu on lubatud oma malend viia.
Fog of War top-down tüüpi kaardil¶
Kaardi piiride loomiseks ja hiljem külastatud kohtade tähistamiseks on vaja teada tile
ide mõõtmeid (pikkust ja laiust) ning nende koguarvu. Selle teabe leiame TMX-faili algusest.

Need mõõtmed saab salvestada kas eraldi muutujatena:
int mapWidth
int mapHeight
int tileWidth
int tileHeight
Või loome nende jaoks enumi klass:
public enum MapConfig {
MAP_WIDTH(20),
MAP_HEIGHT(15),
TILE_WIDTH(16),
TILE_HEIGHT(16);
private final int value;
MapConfig(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Nüüd saab näiteks Player
klassis defineerida kaardi piirid. Selleks loome meetodi defineMapBounds
ning kutsume seda meetodit välja Player
klassi konstruktoris. Piiride määramiseks kasutame ChainShape
klassi, mis moodustab suletud kontuuri. Selleks kasutame ChainShape
liidese meetodit createLoop
, mis ühendab tipud suletud ahelaks.
private void defineMapBounds() {
BodyDef bdef = new BodyDef();
bdef.type = BodyDef.BodyType.StaticBody;
ChainShape shape = new ChainShape();
float rightDown = MapConfig.MAP_HEIGHT.getValue() * MapConfig.TILE_HEIGHT.getValue() / MyGame.PPM;
float rightUp = MapConfig.MAP_WIDTH.getValue() * MapConfig.TILE_WIDTH.getValue() / MyGame.PPM;
shape.createLoop(new float[]{
0, 0,
rightUp, 0,
rightUp, rightDown,
0, rightDown
});
FixtureDef fdef = new FixtureDef();
fdef.shape = shape;
world.createBody(bdef).createFixture(fdef);
shape.dispose();
}
Lisame Player
klassi ka meetodid mängija x- ja y-koordinaatide saamiseks, et neid kasutada põhimänguklassis (mis pärib Screen
liidest), “Fog of War”’i efekti tekitamiseks.
public float getX() {
return b2body.getPosition().x;
}
public float getY() {
return b2body.getPosition().y;
}
Lisame põhimänguklassi järgmised muutujad ning ka mapWidth
, mapHeight
, tileWidth
ja tileHeight
muutujaid, kui ei loonud enum klassi.
private static final int VISION_RADIUS = 1; // määratlemiseks, kui mitut ``tile``\ ‘i mängija näeb igas suunas.
private boolean[][] visibilityMap; // näitamaks mis x- ja y-koordinaadi ``tile`` \’idel oleme käinud.
// int mapWidth;
// int mapHeight;
// int tileWidth = 16;
// int tileHeight = 16;
int mapWidth = MapConfig.MAP_WIDTH.getValue();
int mapHeight = MapConfig.MAP_HEIGHT.getValue();
int tileWidth = MapConfig.TILE_WIDTH.getValue();
int tileHeight = MapConfig.TILE_HEIGHT.getValue();
Põhimängukonstruktoris initsialiseerime visibilityMap
muutuja vastavalt kaardi mõõtmetele ning ka kaardimõõtmete muutujad kui me ei võta neid enumi klassist.
public Maze(MyGame game) {
//this.mapWidth = map.getProperties().get("width", Integer.class); // mul 20
//this.mapHeight = map.getProperties().get("height", Integer.class); // mul 15
this.visibilityMap = new boolean[mapWidth][mapHeight];
}
renderFogOfWar
meetodis kasutame ShapeRenderer
klassi, et joonistada mustad ristkülikud tile
‘idele, kus mängija pole veel käinud.
private void renderFogOfWar() {
ShapeRenderer shapeRenderer = new ShapeRenderer();
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
for (int x = 0; x < mapWidth; x++) {
for (int y = 0; y < mapHeight; y++) {
if (!visibilityMap[x][y]) {
shapeRenderer.setColor(0.0f, 0.0f, 0.0f, 0.1f);
shapeRenderer.rect(x * tileWidth / game.getPPM(), y * tileHeight / game.getPPM(), tileWidth / game.getPPM(), tileHeight / game.getPPM());
}
}
}
shapeRenderer.end();
shapeRenderer.dispose();
}
updateFogOfWar
meetodis uuendame tile
‘ide nähtavust mängija hetkese x- ja y-koordinaati järgi nii kaugele, kui on määratud VISION_RADIUS
põhjal.
private void updateFogOfWar(float playerX, float playerY) {
int tileX = (int) (playerX * game.getPPM() / tileWidth);
int tileY = (int) (playerY * game.getPPM() / tileHeight);
for (int x = tileX - VISION_RADIUS; x <= tileX + VISION_RADIUS; x++) {
for (int y = tileY - VISION_RADIUS; y <= tileY + VISION_RADIUS; y++) {
if (x >= 0 && x < visibilityMap.length && y >= 0 && y < visibilityMap[0].length) {
double distance = Math.sqrt((tileX - x) * (tileX - x) + (tileY - y) * (tileY - y));
if (distance <= VISION_RADIUS) {
visibilityMap[x][y] = true;
}
}
}
}
}
render
funktsiooni on nüüd vaja lisada nii updateFogOfWar
kui ka renderFogOfWar
meetodi välja kutsumine. Kuna SpriteBatch
ja ShapeRenderer
ei saa korraga aktiivsed olla (SpriteBatch
joonistab tekstruure ja ShapeRenderer
kujundeid), peame renderFogOfWar
meetodi välja kutsuma peale batch.end
toimingut.
@Override
public void render(float delta) {
updateFogOfWar(player.getX(), player.getY());
// vahepeal on batch.begin() ja batch.end()
renderFogOfWar();
}
