- 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.