MOODUL 05 · TASE 2 — EDASIJÕUDNUD

🚀 Phaser.js Mänguraamistik

Professionaalne 2D mängumootor brauseris. Sprite'id, füüsika, animatsioonid, stseenid, tilemap'id — kõik mida vajad täisväärtusliku mängu loomiseks!

⏱️ ~6-8 tundi 📝 8 harjutust 🟡 Edasijõudnud

📋 Eeldused

ℹ️

Peaksid olema läbinud Moodul 03 ja mõistma mängutsüklit. Phaser teeb raskema töö sinu eest!

⚡ Samm 1 — Projekti Seadistamine

Terminal — Phaser projekti algus
# Loo uus projekt
mkdir phaser-game && cd phaser-game
npm init -y

# Paigalda Phaser 3
npm install phaser

# Paigalda Vite arendusserveriks
npm install -D vite

# package.json skriptid:
# "scripts": {
#   "dev": "vite",
#   "build": "vite build",
#   "preview": "vite preview"
# }
index.html
<!DOCTYPE html>
<html lang="et">
<head>
  <meta charset="UTF-8">
  <title>Minu Phaser Mäng</title>
  <style>
    body { margin: 0; background: #0a0a0a; display: flex; justify-content: center; align-items: center; height: 100vh; }
  </style>
</head>
<body>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

🎮 Samm 2 — Phaser Konfiguratsioon ja Stseenid

Phaser mäng koosneb stseenidest (Scenes). Iga stseen on eraldi "ekraan" — menüü, mängu tase, game over jne.

src/main.js — Mängu algus
import Phaser from 'phaser';
import { BootScene } from './scenes/BootScene.js';
import { MenuScene } from './scenes/MenuScene.js';
import { GameScene } from './scenes/GameScene.js';
import { GameOverScene } from './scenes/GameOverScene.js';

const config = {
  type: Phaser.AUTO,  // WebGL automaatselt, fallback Canvas
  width: 800,
  height: 600,
  parent: 'game-container',
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 0 },
      debug: false  // true = näita hitbox'e
    }
  },
  scene: [BootScene, MenuScene, GameScene, GameOverScene]
};

const game = new Phaser.Game(config);

📦 Samm 3 — Boot ja Preload

src/scenes/BootScene.js
export class BootScene extends Phaser.Scene {
  constructor() {
    super('BootScene');
  }

  preload() {
    // Lae kõik varad (assets)
    this.load.image('player', 'assets/player.png');
    this.load.image('enemy', 'assets/enemy.png');
    this.load.image('bullet', 'assets/bullet.png');
    this.load.image('background', 'assets/bg.png');

    // Spritesheet animatsioonideks
    this.load.spritesheet('playerAnim', 'assets/player-sheet.png', {
      frameWidth: 64,
      frameHeight: 64
    });

    // Heli
    this.load.audio('shoot', 'assets/audio/shoot.wav');
    this.load.audio('explosion', 'assets/audio/explosion.wav');
    this.load.audio('bgMusic', 'assets/audio/music.mp3');

    // Laadimisriba
    const bar = this.add.graphics();
    this.load.on('progress', (value) => {
      bar.clear();
      bar.fillStyle(0x22c55e, 1);
      bar.fillRect(200, 290, 400 * value, 20);
    });
  }

  create() {
    // Loo animatsioonid
    this.anims.create({
      key: 'playerIdle',
      frames: this.anims.generateFrameNumbers('playerAnim', { start: 0, end: 3 }),
      frameRate: 8,
      repeat: -1  // -1 = lõpmatu kordus
    });

    this.anims.create({
      key: 'playerRun',
      frames: this.anims.generateFrameNumbers('playerAnim', { start: 4, end: 11 }),
      frameRate: 12,
      repeat: -1
    });

    this.scene.start('MenuScene');
  }
}

🏃 Samm 4 — Mängustseen ja Mängija

src/scenes/GameScene.js
export class GameScene extends Phaser.Scene {
  constructor() {
    super('GameScene');
    this.score = 0;
  }

  create() {
    // Taust
    this.add.image(400, 300, 'background');

    // Mängija füüsikakehaga
    this.player = this.physics.add.sprite(400, 500, 'player');
    this.player.setCollideWorldBounds(true);

    // Vaenlaste grupp (object pool!)
    this.enemies = this.physics.add.group({
      maxSize: 30,
      classType: Phaser.Physics.Arcade.Sprite
    });

    // Kuulide grupp
    this.bullets = this.physics.add.group({
      maxSize: 20,
      classType: Phaser.Physics.Arcade.Sprite
    });

    // Sisend
    this.cursors = this.input.keyboard.createCursorKeys();
    this.fireKey = this.input.keyboard.addKey('SPACE');

    // Vaenlaste spawn timer
    this.spawnTimer = this.time.addEvent({
      delay: 1500,
      callback: this.spawnEnemy,
      callbackScope: this,
      loop: true
    });

    // Kokkupõrke kontroll
    this.physics.add.overlap(
      this.bullets, this.enemies, this.hitEnemy, null, this
    );
    this.physics.add.overlap(
      this.player, this.enemies, this.gameOver, null, this
    );

    // Skoori tekst
    this.scoreText = this.add.text(16, 16, 'Skoor: 0', {
      fontSize: '24px',
      fontFamily: 'Inter',
      color: '#ffffff',
      stroke: '#000000',
      strokeThickness: 4
    });
  }

  update(time, delta) {
    // Mängija liikumine
    const speed = 300;

    if (this.cursors.left.isDown) {
      this.player.setVelocityX(-speed);
      this.player.anims.play('playerRun', true);
      this.player.setFlipX(true);
    } else if (this.cursors.right.isDown) {
      this.player.setVelocityX(speed);
      this.player.anims.play('playerRun', true);
      this.player.setFlipX(false);
    } else {
      this.player.setVelocityX(0);
      this.player.anims.play('playerIdle', true);
    }

    if (this.cursors.up.isDown) {
      this.player.setVelocityY(-speed);
    } else if (this.cursors.down.isDown) {
      this.player.setVelocityY(speed);
    } else {
      this.player.setVelocityY(0);
    }

    // Tulistamine
    if (Phaser.Input.Keyboard.JustDown(this.fireKey)) {
      this.shoot();
    }
  }

  shoot() {
    const bullet = this.bullets.get(this.player.x, this.player.y - 20, 'bullet');
    if (bullet) {
      bullet.setActive(true).setVisible(true);
      bullet.setVelocityY(-400);
      this.sound.play('shoot', { volume: 0.3 });

      // Hävita kuul kui läheb ekraanilt välja
      this.time.delayedCall(2000, () => {
        bullet.setActive(false).setVisible(false);
        bullet.body.stop();
      });
    }
  }

  spawnEnemy() {
    const x = Phaser.Math.Between(50, 750);
    const enemy = this.enemies.get(x, -30, 'enemy');
    if (enemy) {
      enemy.setActive(true).setVisible(true);
      enemy.setVelocityY(Phaser.Math.Between(100, 250));
    }
  }

  hitEnemy(bullet, enemy) {
    bullet.setActive(false).setVisible(false);
    enemy.setActive(false).setVisible(false);
    bullet.body.stop();
    enemy.body.stop();

    this.score += 10;
    this.scoreText.setText('Skoor: ' + this.score);
    this.sound.play('explosion', { volume: 0.5 });

    // Partikliefekt (sisseehitatud!)
    const particles = this.add.particles(enemy.x, enemy.y, 'bullet', {
      speed: { min: 50, max: 200 },
      scale: { start: 0.5, end: 0 },
      lifespan: 500,
      quantity: 15,
      emitting: false
    });
    particles.explode();
  }

  gameOver() {
    this.scene.start('GameOverScene', { score: this.score });
  }
}

🗺️ Samm 5 — Tilemap'id (Tiled)

Tilemap võimaldab visuaalselt disainida mängu tasemeid Tiled editoriga. See on 2D mängude "level editor" standard.

ℹ️

Tiled töövoog: Laadi alla Tiled Map Editor (tasuta). Loo uus kaart → määra tile suurus (nt 32x32) → impordi tileset pilt → joonista kihid → ekspordi JSON.

Tiled — Kaardi loomine samm-sammult
# 1. Ava Tiled → File → New → New Map
#    Orientation: Orthogonal
#    Tile size: 32 x 32 px
#    Map size: 40 x 30 tiles (1280 x 960 px)

# 2. Tileset: Map → New Tileset
#    Name: "tileset"
#    Source: tileset.png (nt kenney.nl tiles)
#    Tile width/height: 32 x 32

# 3. Loo kihid (Layers panel):
#    - "Ground"  → muru, liiv, vesi (aluskiht)
#    - "Walls"   → seinad, kivid, platvormid
#    - "Decor"   → puud, lilled, lambid (ülekiht)
#    - "Objects" → Object Layer (spawn points, kogutavad jne)

# 4. Walls kihi omadused:
#    Vali tile → Properties → Lisa: collides = true (boolean)
#    Nii saab Phaser teada millised tile'd blokeerivad liikumist

# 5. Object Layer:
#    Insert Point → nimeta "PlayerSpawn" (x, y positsioon)
#    Insert Point → nimeta "CoinSpawn" (mitu tükki)
#    Insert Rectangle → nimeta "KillZone" (ohtlik ala)

# 6. Ekspordi: File → Export As → level1.json
#    Kopeeri JSON + tileset.png → assets/maps/ kausta
BootScene.js — Tilemap varade laadimine
// preload() meetodis:

// Tilemap JSON (Tiled eksport)
this.load.tilemapTiledJSON('level1', 'assets/maps/level1.json');
this.load.tilemapTiledJSON('level2', 'assets/maps/level2.json');

// Tileset pildid (peab kattuma Tiled tileset nimega!)
this.load.image('terrain-tiles', 'assets/maps/terrain.png');
this.load.image('decor-tiles', 'assets/maps/decorations.png');
GameScene.js — Tilemap loomine ja kihtide seadistamine
create() {
  // ---- TILEMAP ----
  const map = this.make.tilemap({ key: 'level1' });

  // Ühenda tileset pilt nimega Tiled-is
  // addTilesetImage("nimi Tiled-is", "Phaser cache key")
  const terrain = map.addTilesetImage('terrain', 'terrain-tiles');
  const decor = map.addTilesetImage('decorations', 'decor-tiles');

  // ---- KIHID (alumisest ülemiseni) ----
  const groundLayer = map.createLayer('Ground', terrain, 0, 0);
  const wallsLayer = map.createLayer('Walls', terrain, 0, 0);
  const decorLayer = map.createLayer('Decor', decor, 0, 0);

  // Dekoratsioonid mängija EES (sügavus)
  decorLayer.setDepth(10);

  // ---- KOKKUPÕRKED ----

  // Variant A: Property järgi (Tiled-is märgitud collides=true)
  wallsLayer.setCollisionByProperty({ collides: true });

  // Variant B: Tile indeksite järgi
  // wallsLayer.setCollision([1, 2, 5, 6, 12, 13]);

  // Variant C: Kõik mitte-tühjad tile'd
  // wallsLayer.setCollisionByExclusion([-1]);

  // ---- MÄNGIJA SPAWN ----
  // Loe spawn point Object Layer'ist
  const spawnPoint = map.findObject('Objects', obj => obj.name === 'PlayerSpawn');
  this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');
  this.player.setCollideWorldBounds(true);

  // Füüsika kokkupõrge tile kihtidega
  this.physics.add.collider(this.player, wallsLayer);

  // ---- KOGUTAVAD OBJEKTID (Object Layer'ist) ----
  this.coins = this.physics.add.group({ allowGravity: false });

  const coinObjects = map.filterObjects('Objects', obj => obj.name === 'CoinSpawn');
  coinObjects.forEach(coinObj => {
    const coin = this.coins.create(coinObj.x, coinObj.y, 'coin');
    coin.setOrigin(0.5, 1); // Tiled kasutab bottom-left origin
  });

  this.physics.add.overlap(this.player, this.coins, this.collectCoin, null, this);

  // ---- OHTLIKUD ALAD (Rectangles Object Layer'ist) ----
  const killZones = map.filterObjects('Objects', obj => obj.name === 'KillZone');
  killZones.forEach(zone => {
    const zoneSprite = this.add.zone(zone.x + zone.width / 2, zone.y + zone.height / 2,
      zone.width, zone.height);
    this.physics.world.enable(zoneSprite);
    zoneSprite.body.setAllowGravity(false);
    this.physics.add.overlap(this.player, zoneSprite, this.playerDie, null, this);
  });

  // ---- KAAMERA ----
  // Maailma piirid = kaardi suurus
  this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);

  // Kaamera jälgib mängijat sujuvalt
  this.cameras.main.startFollow(this.player, true, 0.08, 0.08);
  this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);

  // Dead zone: kaamera ei liigu kui mängija on ekraani keskel
  this.cameras.main.setDeadzone(200, 150);
}

collectCoin(player, coin) {
  coin.disableBody(true, true); // Eemalda füüsika + peida
  this.score += 10;
  this.scoreText.setText('Mündid: ' + this.score);
  this.sound.play('coinCollect');
}
Parallax taust tilemap'iga
create() {
  // Parallax taustakihid (liiguvad aeglasemalt kui kaamera)
  // scrollFactor < 1 = liigub aeglasemalt (kaugel)
  // scrollFactor = 1 = liigub kaameraga kaasa (normaalne)

  // Taevas (ei liigu üldse)
  this.add.image(400, 200, 'sky').setScrollFactor(0);

  // Kauged mäed (liiguvad väga aeglaselt)
  this.add.tileSprite(400, 400, 800, 200, 'mountains')
    .setScrollFactor(0.1);

  // Lähemad puud
  this.add.tileSprite(400, 450, 800, 200, 'trees')
    .setScrollFactor(0.4);

  // ... siis tilemap kihid (scrollFactor = 1, vaikimisi)
}
💡 Vihje

Tilemap'id toetavad ka animeeritud tile'e — Tiled-is: Tileset → Animation Editor. Sobib ideaalselt vee, laava ja tule tile'idele!

🎬 Samm 6 — Tweens ja Efektid

Tweens on Phaser'i animatsioonimootor mis interpoleerib väärtusi ajas. Ideaalne UI animatsioonide, mündi hõljumise, surmaefektide ja "juice" lisamise jaoks.

tweens.js — Põhilised tweenid
// ============================================
// 1. HÕLJUV MÜNT (üles-alla, lõpmatu)
// ============================================
this.tweens.add({
  targets: coin,
  y: coin.y - 10,       // Liigu 10px üles
  duration: 800,         // 0.8 sekundit
  ease: 'Sine.easeInOut', // Sujuv kiirendus/aeglustus
  yoyo: true,            // Tagasi algpositsioonile
  repeat: -1             // Lõpmatu kordus
});

// ============================================
// 2. MÜNDI KOGUMINE (scale up + fade out)
// ============================================
collectCoin(player, coin) {
  coin.disableBody(true, false); // Keela füüsika, ära peida veel

  this.tweens.add({
    targets: coin,
    y: coin.y - 40,     // Lenda üles
    alpha: 0,            // Fade out
    scale: 1.5,          // Suurenda
    duration: 300,
    ease: 'Power2',
    onComplete: () => coin.destroy()
  });

  // "+10" tekst mis lendab üles
  const scorePopup = this.add.text(coin.x, coin.y, '+10', {
    fontSize: '20px', fontFamily: 'Inter', color: '#ffd700',
    stroke: '#000', strokeThickness: 3
  }).setOrigin(0.5);

  this.tweens.add({
    targets: scorePopup,
    y: scorePopup.y - 50,
    alpha: 0,
    duration: 600,
    ease: 'Power2',
    onComplete: () => scorePopup.destroy()
  });
}

// ============================================
// 3. VAENLASE SURM (keerlev + fade)
// ============================================
killEnemy(enemy) {
  enemy.disableBody(true, false);
  enemy.setTint(0xff0000); // Punane flash

  this.tweens.add({
    targets: enemy,
    alpha: 0,
    scale: 2,
    angle: 360,
    duration: 400,
    ease: 'Power3',
    onComplete: () => enemy.destroy()
  });
}
Kaameraefektid ja ekraaniraputus
// ============================================
// 4. KAAMERAEFEKTID
// ============================================

// Ekraani raputus (tabamus, plahvatus)
this.cameras.main.shake(200, 0.005);

// Punane flash (damage)
this.cameras.main.flash(100, 255, 0, 0, true);

// Fade out → fade in (stseeni vahetus)
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
  this.scene.start('NextLevel');
});

// Aeglane zoom efekt (boss ilmumine)
this.tweens.add({
  targets: this.cameras.main,
  zoom: 1.3,
  duration: 1000,
  ease: 'Sine.easeInOut',
  yoyo: true
});

// Slow motion efekt!
sloMo() {
  this.time.timeScale = 0.3;  // 30% kiirusega
  this.tweens.add({
    targets: this.time,
    timeScale: 1,             // Tagasi normaalkiirusele
    duration: 1000,
    ease: 'Power2'
  });
}
Partikliefektid
// ============================================
// 5. PARTIKLIEFEKTID (Phaser 3.60+)
// ============================================

// Plahvatus (ühekordne)
createExplosion(x, y) {
  const particles = this.add.particles(x, y, 'spark', {
    speed: { min: 80, max: 300 },
    angle: { min: 0, max: 360 },
    scale: { start: 0.6, end: 0 },
    alpha: { start: 1, end: 0 },
    lifespan: 600,
    quantity: 20,
    tint: [0xff4444, 0xff8800, 0xffff00], // Punane→oranž→kollane
    emitting: false
  });
  particles.explode();

  // Hävita emitter mälu puhastamiseks
  this.time.delayedCall(1000, () => particles.destroy());
}

// Jälg mängija taga (pidevalt emiteerib)
createPlayerTrail() {
  this.playerTrail = this.add.particles(0, 0, 'dot', {
    follow: this.player,           // Jälgi mängijat
    followOffset: { y: 16 },      // Jalgade juurest
    scale: { start: 0.3, end: 0 },
    alpha: { start: 0.5, end: 0 },
    lifespan: 400,
    frequency: 50,                  // Iga 50ms uus partikkel
    tint: 0xaaaaff,
    blendMode: 'ADD'               // Helendav efekt
  });
}

// Vihm
createRain() {
  this.add.particles(400, -10, 'raindrop', {
    x: { min: -400, max: 400 },
    speedY: { min: 300, max: 500 },
    speedX: { min: -20, max: -50 },
    scale: { min: 0.1, max: 0.3 },
    alpha: { min: 0.3, max: 0.6 },
    lifespan: 2000,
    frequency: 20,
    quantity: 2
  });
}
UI tweenid ja Ease funktsioonid
// ============================================
// 6. UI ANIMATSIOONID
// ============================================

// Menüü nupp ilmumine (alt üles + fade in)
showMenu(buttons) {
  buttons.forEach((btn, i) => {
    btn.setAlpha(0);
    btn.y += 30;

    this.tweens.add({
      targets: btn,
      alpha: 1,
      y: btn.y - 30,
      duration: 400,
      delay: i * 100,  // Staggered — iga nupp hilineb 100ms
      ease: 'Back.easeOut'  // Natuke üle vibutab
    });
  });
}

// "GAME OVER" tekst mis suureneb
showGameOver() {
  const text = this.add.text(400, 300, 'GAME OVER', {
    fontSize: '64px', fontFamily: 'Inter', color: '#ff4444',
    stroke: '#000', strokeThickness: 6
  }).setOrigin(0.5).setScale(0);

  this.tweens.add({
    targets: text,
    scale: 1,
    duration: 600,
    ease: 'Elastic.easeOut'  // Vedru-efekt!
  });
}

// ============================================
// EASE FUNKTSIOONIDE SPIKKER:
// ============================================
// 'Linear'          — ühtlane kiirus
// 'Power2'          — kiirenemine (Quad)
// 'Power3'          — veelgi kiirem kiirenemine (Cubic)
// 'Sine.easeInOut'  — sujuv algus ja lõpp
// 'Back.easeOut'    — läheb natuke üle ja tuleb tagasi
// 'Bounce.easeOut'  — põrkav efekt
// 'Elastic.easeOut' — vedru efekt
// 'Expo.easeOut'    — järsk algus, aeglane lõpp
//
// Proovi visuaalselt: https://easings.net/
💡 "Game Juice" kontrollnimekiri
  • ✅ Ekraani shake tabamuse/plahvatuse korral
  • ✅ Partiklid surma/kogumise juures
  • ✅ Score popup mis lendab üles
  • ✅ Tint flash (punane = damage, valge = kogutud)
  • ✅ Slow-motion kriitilise tabamuse korral
  • ✅ Hõljuvad/pulseerivad kogutavad esemed
  • ✅ Sujuvad UI üleminekud (fade, slide, scale)

📝 Harjutused

  • Seadista Phaser + Vite projekt

    Loo uus projekt npm init + Phaser 3 + Vite. Loo BootScene, MenuScene ja GameScene. Kontrolli et mäng käivitub brauseris.

  • Sprite liikumine ja animatsioon

    Lae spritesheet (nt kenney.nl), loo animatsioonid (idle, run, jump) ja juhi mängijat klaviatuuriga. Lisa setCollideWorldBounds.

  • Füüsikaobjektid ja kokkupõrked

    Loo grupp kukkuvaid objekte, lisa gravitatsioon. Mängija peab neid vältima. Lisa skoor ja elude süsteem. Kasuta physics.add.overlap().

  • Tulistamismäng (Shooter)

    Loo top-down shooter: mängija tulistab kuule, vaenlased spawni'vad lainetes. Lisa object pooling (group.maxSize), partikliefektid ja heliefektid.

  • Platformer Tilemap'iga

    Loo platformer: disaini tase Tiled editoris, ekspordi JSON. Lae Phaser'isse, lisa kokkupõrked seintega. Mängija saab joosta ja hüpata.

  • Stseenide haldus

    Loo täielik mängutsükkel: Boot → Menu → Game → GameOver → Menu. Lisa andmete edastamine stseenide vahel (skoor). Lisa pause stseen.

  • Kaamera süsteem

    Loo suur maailm ja kaamera mis jälgib mängijat. Lisa "dead zone", parallax tausta kihid ja zoom efektid.

  • Tweens ja visuaalsed efektid

    Lisa mängule poleeritud efektid: mündi hõljumine tweeniga, surma animatsioon (fade + scale), ekraani shake tabamuse korral, flash efektid.

← Moodul 04 Moodul 06: Graafika & Heli →