MOODUL 06 · TASE 2 — EDASIJÕUDNUD

🎨 Graafika, Animatsioonid & Heli

Sprite sheet'id, piksel-kunst, helidisain, muusika — kõik mis annab mängule elu ja atmosfääri.

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

🖼️ Samm 1 — Sprite Sheet'ide Loomine

Sprite sheet on pilt mis sisaldab mitu "kaadrit" animatsioonist reas.

💡

Tasuta tööriistad: Aseprite (tasuline, kuid parim), Piskel (tasuta, brauseris), Pixilart (tasuta, brauseris). Tasuta varad: kenney.nl, itch.io

sprite.js — Spritesheet laadimine ja animatsioon
class SpriteSheet {
  constructor(image, frameWidth, frameHeight) {
    this.image = image;
    this.frameWidth = frameWidth;
    this.frameHeight = frameHeight;
    this.columns = Math.floor(image.width / frameWidth);
    this.rows = Math.floor(image.height / frameHeight);
    this.totalFrames = this.columns * this.rows;
  }

  drawFrame(ctx, frameIndex, x, y, scale = 1) {
    const col = frameIndex % this.columns;
    const row = Math.floor(frameIndex / this.columns);

    ctx.drawImage(
      this.image,
      col * this.frameWidth,    // Source X
      row * this.frameHeight,   // Source Y
      this.frameWidth,          // Source W
      this.frameHeight,         // Source H
      x, y,                     // Destination X, Y
      this.frameWidth * scale,  // Destination W
      this.frameHeight * scale  // Destination H
    );
  }
}

class Animation {
  constructor(spriteSheet, frames, frameRate = 10, loop = true) {
    this.spriteSheet = spriteSheet;
    this.frames = frames;        // [0, 1, 2, 3] frame indeksid
    this.frameRate = frameRate;
    this.loop = loop;
    this.currentFrame = 0;
    this.timer = 0;
    this.finished = false;
  }

  update(deltaTime) {
    if (this.finished) return;
    this.timer += deltaTime;
    const frameDuration = 1000 / this.frameRate;

    if (this.timer >= frameDuration) {
      this.timer -= frameDuration;
      this.currentFrame++;

      if (this.currentFrame >= this.frames.length) {
        if (this.loop) {
          this.currentFrame = 0;
        } else {
          this.currentFrame = this.frames.length - 1;
          this.finished = true;
        }
      }
    }
  }

  draw(ctx, x, y, scale = 1) {
    const frameIndex = this.frames[this.currentFrame];
    this.spriteSheet.drawFrame(ctx, frameIndex, x, y, scale);
  }

  reset() {
    this.currentFrame = 0;
    this.timer = 0;
    this.finished = false;
  }
}

// Kasutamine:
// const sheet = new SpriteSheet(playerImage, 64, 64);
// const idleAnim = new Animation(sheet, [0, 1, 2, 3], 8);
// const runAnim = new Animation(sheet, [4, 5, 6, 7, 8, 9, 10, 11], 12);

🏃 Samm 2 — Animatsioonide Haldur

animator.js
class Animator {
  constructor() {
    this.animations = new Map();
    this.currentAnim = null;
    this.currentKey = '';
    this.flipX = false;
  }

  add(key, animation) {
    this.animations.set(key, animation);
    if (!this.currentAnim) this.play(key);
  }

  play(key) {
    if (this.currentKey === key) return;
    this.currentKey = key;
    this.currentAnim = this.animations.get(key);
    if (this.currentAnim) this.currentAnim.reset();
  }

  update(deltaTime) {
    if (this.currentAnim) this.currentAnim.update(deltaTime);
  }

  draw(ctx, x, y, scale = 1) {
    if (!this.currentAnim) return;
    ctx.save();
    if (this.flipX) {
      ctx.translate(x + this.currentAnim.spriteSheet.frameWidth * scale, y);
      ctx.scale(-1, 1);
      this.currentAnim.draw(ctx, 0, 0, scale);
    } else {
      this.currentAnim.draw(ctx, x, y, scale);
    }
    ctx.restore();
  }
}

// Kasutamine:
// const animator = new Animator();
// animator.add('idle', idleAnim);
// animator.add('run', runAnim);
// animator.add('jump', jumpAnim);
//
// if (velocity.x !== 0) animator.play('run');
// else animator.play('idle');
// animator.flipX = velocity.x < 0;

🎵 Samm 3 — Web Audio API

Web Audio API annab täieliku kontrolli heli üle — helitugevus, pitch, 3D positsioneerimine, efektid.

audio.js — Helisüsteem
class AudioManager {
  constructor() {
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    this.sounds = new Map();
    this.musicGain = this.ctx.createGain();
    this.sfxGain = this.ctx.createGain();
    this.masterGain = this.ctx.createGain();

    // Helipuu: sounds → sfxGain → masterGain → output
    this.sfxGain.connect(this.masterGain);
    this.musicGain.connect(this.masterGain);
    this.masterGain.connect(this.ctx.destination);

    this.musicGain.gain.value = 0.3;
    this.sfxGain.gain.value = 0.7;
    this.currentMusic = null;
  }

  async load(key, url) {
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();
    const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer);
    this.sounds.set(key, audioBuffer);
  }

  play(key, options = {}) {
    const buffer = this.sounds.get(key);
    if (!buffer) return;

    const source = this.ctx.createBufferSource();
    source.buffer = buffer;

    // Pitch muutmine
    if (options.pitch) {
      source.playbackRate.value = options.pitch;
    }

    // Juhuslik pitch variatsioon (et ei kõlaks monotoonselt)
    if (options.pitchVariation) {
      const variation = 1 + (Math.random() - 0.5) * options.pitchVariation;
      source.playbackRate.value *= variation;
    }

    source.connect(this.sfxGain);
    source.start(0);
    return source;
  }

  playMusic(key, loop = true) {
    if (this.currentMusic) this.currentMusic.stop();

    const buffer = this.sounds.get(key);
    if (!buffer) return;

    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    source.loop = loop;
    source.connect(this.musicGain);
    source.start(0);
    this.currentMusic = source;
  }

  // Sujuv helitugevuse muutmine
  fadeMusic(targetVolume, duration = 1) {
    this.musicGain.gain.linearRampToValueAtTime(
      targetVolume, this.ctx.currentTime + duration
    );
  }

  // Vajalik kasutaja interaktsiooni järel
  resume() {
    if (this.ctx.state === 'suspended') {
      this.ctx.resume();
    }
  }
}

// Kasutamine:
// const audio = new AudioManager();
// await audio.load('shoot', '/assets/audio/shoot.wav');
// await audio.load('bgMusic', '/assets/audio/music.mp3');
// audio.play('shoot', { pitchVariation: 0.2 });
// audio.playMusic('bgMusic');
⚠️

Brauseri piirang: Heli ei saa mängida enne kasutaja interaktsiooni (klikk/klahv). Kutsu audio.resume() esimese interaktsiooni järel!

🎶 Samm 4 — Protseduurilised Heliefektid

sfx.js — Heliefektide genereerimine
class SFXGenerator {
  constructor(audioCtx) {
    this.ctx = audioCtx;
  }

  // Laseriheli
  laser() {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(1000, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.2);

    gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);

    osc.connect(gain).connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.2);
  }

  // Plahvatusheli
  explosion() {
    const bufferSize = this.ctx.sampleRate * 0.5;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);

    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize);
    }

    const source = this.ctx.createBufferSource();
    source.buffer = buffer;

    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(2000, this.ctx.currentTime);
    filter.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.5);

    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.5, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);

    source.connect(filter).connect(gain).connect(this.ctx.destination);
    source.start();
  }

  // Mündi kogumine
  coin() {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(587, this.ctx.currentTime);         // D5
    osc.frequency.setValueAtTime(880, this.ctx.currentTime + 0.08);  // A5

    gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);

    osc.connect(gain).connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.3);
  }
}

// const sfx = new SFXGenerator(audio.ctx);
// sfx.laser();
// sfx.explosion();
// sfx.coin();

🖌️ Samm 5 — Piksel-kunsti Põhitõed

ℹ️

Piksel-kunsti reeglid:

  • Kasuta piiratud värvipalli (8-16 värvi)
  • Tööta väikses suuruses (16×16, 32×32, 64×64)
  • Ära kasuta anti-aliasing'ut — iga piksel on tahtlik
  • Loo esmalt siluett, siis detailid
  • Piirjooned: tumedad (ei pea olema must)
  • Valgus peab tulema ühest suunast
Canvas pixel-perfect renderdamine
// Pixel-perfect Canvas seadistus
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// KRIITILINE — lülita interpolatsioon välja!
ctx.imageSmoothingEnabled = false;
// CSS-is ka:
// canvas { image-rendering: pixelated; image-rendering: crisp-edges; }

// Tööta väikesel Canvasil ja suurenda CSS-iga
canvas.width = 320;   // Mängu loogiline suurus
canvas.height = 240;
canvas.style.width = '960px';  // 3x suurendus
canvas.style.height = '720px';

📝 Harjutused

  • Sprite Sheet animatsioon

    Loo SpriteSheet ja Animation klass. Lae tasuta spritesheet (kenney.nl), loo animatsioonid idle, run ja attack. Juhib klaviatuuriga.

  • Animatsioonide haldur

    Loo Animator klass mis haldab mitu animatsiooni. Lisa flip (peegeldamine) ja automaatne animatsiooni valik (idle/run/jump) vastavalt olekule.

  • Helisüsteemi ehitamine

    Ehita AudioManager Web Audio API-ga. Lae kolm heliefekti + üks taustamuusika. Lisa helitugevuse kontroll, mute nupp ja fade efektid.

  • Protseduurilised heliefektid

    Genereeri Web Audio oscillator'itega: laserilask, plahvatus, mündi kogumine, hüppamine, menu hover. Loo SFXGenerator klass.

  • Piksel-kunsti mängija

    Joonista Piskel'is 32×32 mängija tegelane: idle (4 kaadrit), run (6 kaadrit). Ekspordi PNG spritesheet. Integreeri mängu.

  • Täielik audiovisuaalne demo

    Kombineeri kõik: animeeritud mängija + heliefektid + taustamuusika + partiklid. Loo väike interaktiivne demo kus kõik elemendid töötavad koos.

← Moodul 05 Moodul 06b: TypeScript →