Javascript

OOP Exempel

  • SpelObjekt: abstrakt + inkapslat (privata fält för position & bildtyp).
  • StatisktObjekt: egen typ ”statisk”.
  • RörligtObjekt: riktning + acceleration (enkelt, ”lagom fejk”).
  • Spelare: styrs via tangentbord (pilar) med en Controller.
  • Fiende: vänder sig mot spelaren.
  • Alla objekt i en lista; först typ-specifik effekt, sen generisk uppdatera().
// ===== Abstrakt bas =====
class SpelObjekt {
  #x; #y; #bild;

  constructor({ x = 0, y = 0, bild = "okand" } = {}) {
    if (new.target === SpelObjekt) throw new Error("SpelObjekt är abstrakt.");
    this.#x = x; 
    this.#y = y; 
    this.#bild = bild;
  }

  get x() { return this.#x; }  
  get y() { return this.#y; }  
  get bild() { return this.#bild; }

  // Skyddade hjälpmetoder för underklasser
  _setPos(x, y) { this.#x = x; this.#y = y; }
  _moveBy(dx, dy) { this.#x += dx; this.#y += dy; }

  uppdatera(dt, world) {} // Generisk hook
  rita(ctx) {}            // Generisk hook
}

// ===== Statisk =====
class StatisktObjekt extends SpelObjekt {
  constructor(opts = {}) {
    super({ ...opts, bild: opts.bild ?? "statisk" });
  }
}

// ===== Rörlig =====
class RorligtObjekt extends SpelObjekt {
  #riktning; #acc; #hast; #max;

  constructor(opts = {}) {
    super(opts);
    this.#riktning = opts.riktning ?? 0;   // radianer
    this.#acc = opts.acceleration ?? 0;    // skalär
    this.#hast = opts.hastighet ?? 0;
    this.#max = opts.maxHast ?? 200;
  }

  get riktning() { return this.#riktning; }
  setRiktning(v) { this.#riktning = v; }
  setAcceleration(a) { this.#acc = a; }

  uppdatera(dt, world) {
    this.#hast += this.#acc * dt;

    // Enkel friktion/bromsning så att objektet stannar när acc = 0
    if (this.#acc === 0) this.#hast *= 0.95;

    if (this.#hast < 0) this.#hast = 0;
    if (this.#hast > this.#max) this.#hast = this.#max;

    this._moveBy(
      Math.cos(this.#riktning) * this.#hast * dt,
      Math.sin(this.#riktning) * this.#hast * dt
    );
  }
}

// ===== Controller =====
class PilController {
  #keys = { ArrowLeft: false, ArrowRight: false, ArrowUp: false };

  constructor() {
    if (typeof window !== "undefined") {
      window.addEventListener("keydown", e => { if (e.code in this.#keys) this.#keys[e.code] = true; });
      window.addEventListener("keyup",   e => { if (e.code in this.#keys) this.#keys[e.code] = false; });
    }
  }

  styr(obj, world, dt) {
    const TURN = 3.0, ACC = 120;
    let r = obj.riktning;

    if (this.#keys.ArrowLeft)  r -= TURN * dt;
    if (this.#keys.ArrowRight) r += TURN * dt;

    obj.setRiktning(r);
    obj.setAcceleration(this.#keys.ArrowUp ? ACC : 0);
  }
}

// ===== Spelare =====
class Spelare extends RorligtObjekt {
  constructor(opts = {}) {
    super({ ...opts, bild: opts.bild ?? "spelare" });
    this.controller = opts.controller ?? new PilController();
  }

  uppdatera(dt, world) {
    this.controller.styr(this, world, dt);
    super.uppdatera(dt, world);
  }
}

// ===== Fiende =====
class Fiende extends RorligtObjekt {
  constructor(opts = {}) {
    super({ ...opts, bild: opts.bild ?? "fiende" });
    this.jaktAcc = opts.jaktAcc ?? 100;
  }

  siktaMot(target) {
    const dx = target.x - this.x;
    const dy = target.y - this.y;
    this.setRiktning(Math.atan2(dy, dx));
    this.setAcceleration(this.jaktAcc);
  }

  // ✅ Egen uppdatera-metod kapslar in AI-beteendet
  uppdatera(dt, world) {
    if (world.spelare) {
      this.siktaMot(world.spelare);
    }
    super.uppdatera(dt, world);
  }
}

// ===== Specifikt statiskt objekt =====
class Stol extends StatisktObjekt {
  constructor(opts = {}) { 
    super({ ...opts, bild: "stol" }); 
  }
}

// ===== Värld & Loopexempel =====
const world = { 
  objekt: [], 
  spelare: null 
};

world.spelare = new Spelare({ x: 100, y: 100 });
world.objekt.push(world.spelare);
world.objekt.push(new Fiende({ x: 380, y: 220 }));
world.objekt.push(new Fiende({ x: 260, y: 320 }));
world.objekt.push(new Stol({ x: 200, y: 200 }));

// Clean, polymorf uppdatering
function uppdateraVarld(dt) {
  for (const o of world.objekt) {
    o.uppdatera(dt, world);
  }
}

let t = 0; 
const step = 0.016;
const timer = setInterval(() => {
  t += step; 
  if (t > 1.0) clearInterval(timer);
  uppdateraVarld(step);
}, step * 1000);

Varför detta funkar bra pedagogiskt

  • Abstrakt + inkapslat: basen kan inte instansieras; position & bild är privata.
  • Tydliga roller: statiskt vs rörligt; controller separerar input från objektets data.
  • En lista, två pass: först typberoende logik, sen generisk uppdatera() för alla.
  • Enkelt att bygga vidare: lägg till Dörr (statisk), Skott (rörligt), kollisioner etc. utan att ändra kärn-strukturen.