- 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;
this.type = "bas";
}
get x(){ return this.#x } get y(){ return this.#y } get bild(){ return this.#bild }
// "skyddat" via konvention: används av subklasser
_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 (ex: drawImage)
}
// ===== Statisk =====
class StatisktObjekt extends SpelObjekt {
constructor(opts={}) {
super({ ...opts, bild: opts.bild ?? "statisk" });
this.type = "statisk";
}
// ingen rörelse
}
// ===== 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;
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);
}
}
// ===== Spelare (med tangentbords-kontroller) =====
class Spelare extends RorligtObjekt {
constructor(opts={}) {
super({ ...opts, bild: opts.bild ?? "spelare" });
this.type = "spelare";
this.controller = opts.controller ?? new PilController();
}
uppdatera(dt, world){
this.controller.styr(this, world, dt); // sätter riktning/acc
super.uppdatera(dt, world);
}
}
// Enkel controller: pilar ← → roterar, ↑ gasar
class PilController {
#keys = { ArrowLeft:false, ArrowRight:false, ArrowUp:false };
constructor() {
addEventListener("keydown", e => { if (e.code in this.#keys) this.#keys[e.code]=true; });
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);
}
}
// ===== Fiende (rör sig mot spelaren) =====
class Fiende extends RorligtObjekt {
constructor(opts={}) {
super({ ...opts, bild: opts.bild ?? "fiende" });
this.type = "fiende";
this.jaktAcc = opts.jaktAcc ?? 100;
}
// typ-specifik effekt: sikta mot spelaren
siktaMot(spelare){
const dx = spelare.x - this.x, dy = spelare.y - this.y;
this.setRiktning(Math.atan2(dy, dx));
this.setAcceleration(this.jaktAcc);
}
}
// ===== Exempel: specifika statiska objekt =====
class Stol extends StatisktObjekt {
constructor(opts={}) { super({ ...opts, bild: "stol" }); }
}
// ===== Värld: alla objekt i EN lista =====
const world = { objekt: [], spelare: null };
// Skapa några objekt
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 })); // statiskt exempel
// Central hantering: 1) typ-specifika effekter -> 2) generisk uppdatera
function uppdateraVarld(dt){
for (const o of world.objekt){
if (o.type === "fiende" && world.spelare) o.siktaMot(world.spelare);
// "spelare" styrs redan via controller i sin uppdatera()
// "statisk" gör inget typ-specifikt här
}
for (const o of world.objekt) o.uppdatera(dt, world);
}
// Minimal “tick-loop” för demo (ingen grafik, bara struktur)
let t=0; const step=0.016;
const timer = setInterval(()=>{
t += step; if (t>1.0) clearInterval(timer); // kör ~1s
uppdateraVarld(step);
// console.log(world.spelare.x, world.spelare.y); // valfritt för att se rörelse
}, 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.