17 KiB
Bygg ett rymdspel Del 1: Introduktion
Precis som NASAs kontrollcenter koordinerar flera system under en rymduppskjutning, ska vi bygga ett rymdspel som visar hur olika delar av ett program kan fungera smidigt tillsammans. Genom att skapa något du faktiskt kan spela, kommer du att lära dig viktiga programmeringskoncept som gäller för alla mjukvaruprojekt.
Vi kommer att utforska två grundläggande sätt att organisera kod: arv och komposition. Dessa är inte bara akademiska begrepp – de är samma mönster som driver allt från videospel till banksystem. Vi kommer också att implementera ett kommunikationssystem kallat pub/sub som fungerar som kommunikationsnätverk som används i rymdfarkoster, vilket gör det möjligt för olika komponenter att dela information utan att skapa beroenden.
I slutet av denna serie kommer du att förstå hur man bygger applikationer som kan skalas och utvecklas – oavsett om du utvecklar spel, webbapplikationer eller något annat mjukvarusystem.
Quiz före föreläsningen
Arv och komposition inom spelutveckling
När projekt växer i komplexitet blir kodorganisation avgörande. Det som börjar som ett enkelt skript kan bli svårt att underhålla utan rätt struktur – ungefär som hur Apollo-missionerna krävde noggrann samordning mellan tusentals komponenter.
Vi kommer att utforska två grundläggande sätt att organisera kod: arv och komposition. Varje har sina tydliga fördelar, och att förstå båda hjälper dig att välja rätt metod för olika situationer. Vi kommer att demonstrera dessa koncept genom vårt rymdspel, där hjältar, fiender, power-ups och andra objekt måste interagera effektivt.
✅ En av de mest kända programmeringsböckerna som någonsin skrivits handlar om designmönster.
I vilket spel som helst har du spelobjekt – de interaktiva elementen som fyller din spelvärld. Hjältar, fiender, power-ups och visuella effekter är alla spelobjekt. Varje existerar på specifika skärmkoordinater med hjälp av x och y-värden, liknande att plotta punkter på ett koordinatplan.
Trots sina visuella skillnader delar dessa objekt ofta grundläggande beteenden:
- De existerar någonstans – Varje objekt har x- och y-koordinater så att spelet vet var det ska ritas
- Många kan röra sig – Hjältar springer, fiender jagar, kulor flyger över skärmen
- De har en livslängd – Vissa stannar kvar för alltid, andra (som explosioner) dyker upp kort och försvinner
- De reagerar på saker – När saker kolliderar, samlas power-ups in, hälsomätare uppdateras
✅ Tänk på ett spel som Pac-Man. Kan du identifiera de fyra objekttyperna som nämns ovan i detta spel?
Att uttrycka beteende genom kod
Nu när du förstår de gemensamma beteenden som spelobjekt delar, låt oss utforska hur man implementerar dessa beteenden i JavaScript. Du kan uttrycka objektbeteende genom metoder som är kopplade till antingen klasser eller individuella objekt, och det finns flera tillvägagångssätt att välja mellan.
Den klassbaserade metoden
Klasser och arv ger ett strukturerat sätt att organisera spelobjekt. Precis som det taxonomiska klassificeringssystemet utvecklat av Carl von Linné, börjar du med en basklass som innehåller gemensamma egenskaper, och sedan skapar du specialiserade klasser som ärver dessa grunder samtidigt som de lägger till specifika funktioner.
✅ Arv är ett viktigt koncept att förstå. Läs mer i MDNs artikel om arv.
Så här kan du implementera spelobjekt med hjälp av klasser och arv:
// Step 1: Create the base GameObject class
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
}
Låt oss bryta ner detta steg för steg:
- Vi skapar en grundläggande mall som varje spelobjekt kan använda
- Konstruktoren sparar var objektet är (
x,y) och vilken typ av sak det är - Detta blir grunden som alla dina spelobjekt kommer att bygga på
// Step 2: Add movement capability through inheritance
class Movable extends GameObject {
constructor(x, y, type) {
super(x, y, type); // Call parent constructor
}
// Add the ability to move to a new position
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
I ovanstående har vi:
- Utökat GameObject-klassen för att lägga till rörelsefunktionalitet
- Anropat föräldrakonstruktorn med
super()för att initiera ärvda egenskaper - Lagt till en
moveTo()-metod som uppdaterar objektets position
// Step 3: Create specific game object types
class Hero extends Movable {
constructor(x, y) {
super(x, y, 'Hero'); // Set type automatically
}
}
class Tree extends GameObject {
constructor(x, y) {
super(x, y, 'Tree'); // Trees don't need movement
}
}
// Step 4: Use your game objects
const hero = new Hero(0, 0);
hero.moveTo(5, 5); // Hero can move!
const tree = new Tree(10, 15);
// tree.moveTo() would cause an error - trees can't move
Att förstå dessa koncept:
- Skapar specialiserade objekttyper som ärver lämpliga beteenden
- Demonstrerar hur arv möjliggör selektiv funktionalitet
- Visar att hjältar kan röra sig medan träd förblir stillastående
- Illustrerar hur klasshierarkin förhindrar olämpliga handlingar
✅ Ta några minuter och föreställ dig en Pac-Man-hjälte (Inky, Pinky eller Blinky, till exempel) och hur den skulle skrivas i JavaScript.
Kompositionsmetoden
Komposition följer en modulär designfilosofi, liknande hur ingenjörer designar rymdfarkoster med utbytbara komponenter. Istället för att ärva från en föräldraklass, kombinerar du specifika beteenden för att skapa objekt med exakt den funktionalitet de behöver. Denna metod erbjuder flexibilitet utan stela hierarkiska begränsningar.
// Step 1: Create base behavior objects
const gameObject = {
x: 0,
y: 0,
type: ''
};
const movable = {
moveTo(x, y) {
this.x = x;
this.y = y;
}
};
Här är vad denna kod gör:
- Definierar ett grundläggande
gameObjectmed position och typ-egenskaper - Skapar ett separat
movablebeteendeobjekt med rörelsefunktionalitet - Separerar ansvar genom att hålla positionsdata och rörelselogik oberoende
// Step 2: Compose objects by combining behaviors
const movableObject = { ...gameObject, ...movable };
// Step 3: Create factory functions for different object types
function createHero(x, y) {
return {
...movableObject,
x,
y,
type: 'Hero'
};
}
function createStatic(x, y, type) {
return {
...gameObject,
x,
y,
type
};
}
I ovanstående har vi:
- Kombinerat grundläggande objekt-egenskaper med rörelsebeteende med hjälp av spridningssyntax
- Skapat fabriksfunktioner som returnerar anpassade objekt
- Möjliggjort flexibel objektgenerering utan stela klasshierarkier
- Tillåtit objekt att ha exakt de beteenden de behöver
// Step 4: Create and use your composed objects
const hero = createHero(10, 10);
hero.moveTo(5, 5); // Works perfectly!
const tree = createStatic(0, 0, 'Tree');
// tree.moveTo() is undefined - no movement behavior was composed
Viktiga punkter att komma ihåg:
- Komponerar objekt genom att blanda beteenden istället för att ärva dem
- Ger mer flexibilitet än stela arvshierarkier
- Tillåter objekt att ha exakt de funktioner de behöver
- Använder modern JavaScript-spridningssyntax för ren objektkombination
**Which Pattern Should You Choose?**
> 💡 **Pro Tip**: Both patterns have their place in modern JavaScript development. Classes work well for clearly defined hierarchies, while composition shines when you need maximum flexibility.
>
**Here's when to use each approach:**
- **Choose** inheritance when you have clear "is-a" relationships (a Hero *is-a* Movable object)
- **Select** composition when you need "has-a" relationships (a Hero *has* movement abilities)
- **Consider** your team's preferences and project requirements
- **Remember** that you can mix both approaches in the same application
## Communication Patterns: The Pub/Sub System
As applications grow complex, managing communication between components becomes challenging. The publish-subscribe pattern (pub/sub) solves this problem using principles similar to radio broadcasting – one transmitter can reach multiple receivers without knowing who's listening.
Consider what happens when a hero takes damage: the health bar updates, sound effects play, visual feedback appears. Rather than coupling the hero object directly to these systems, pub/sub allows the hero to broadcast a "damage taken" message. Any system that needs to respond can subscribe to this message type and react accordingly.
✅ **Pub/Sub** stands for 'publish-subscribe'
### Understanding the Pub/Sub Architecture
The pub/sub pattern keeps different parts of your application loosely coupled, meaning they can work together without being directly dependent on each other. This separation makes your code more maintainable, testable, and flexible to changes.
**The key players in pub/sub:**
- **Messages** – Simple text labels like `'PLAYER_SCORED'` that describe what happened (plus any extra info)
- **Publishers** – The objects that shout out "Something happened!" to anyone who's listening
- **Subscribers** – The objects that say "I care about that event" and react when it happens
- **Event System** – The middleman that makes sure messages get to the right listeners
### Building an Event System
Let's create a simple but powerful event system that demonstrates these concepts:
```javascript
// Step 1: Create the EventEmitter class
class EventEmitter {
constructor() {
this.listeners = {}; // Store all event listeners
}
// Register a listener for a specific message type
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
// Send a message to all registered listeners
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach(listener => {
listener(message, payload);
});
}
}
}
Bryter ner vad som händer här:
- Skapar ett centralt händelsehanteringssystem med hjälp av en enkel klass
- Lagrar lyssnare i ett objekt organiserat efter meddelandetyp
- Registrerar nya lyssnare med hjälp av
on()-metoden - Sänder meddelanden till alla intresserade lyssnare med hjälp av
emit() - Stöder valfria datapaket för att skicka relevant information
Sätta ihop allt: Ett praktiskt exempel
Okej, låt oss se detta i praktiken! Vi ska bygga ett enkelt rörelsesystem som visar hur rent och flexibelt pub/sub kan vara:
// Step 1: Define your message types
const Messages = {
HERO_MOVE_LEFT: 'HERO_MOVE_LEFT',
HERO_MOVE_RIGHT: 'HERO_MOVE_RIGHT',
ENEMY_SPOTTED: 'ENEMY_SPOTTED'
};
// Step 2: Create your event system and game objects
const eventEmitter = new EventEmitter();
const hero = createHero(0, 0);
Här är vad denna kod gör:
- Definierar ett konstantobjekt för att förhindra stavfel i meddelandenamn
- Skapar en händelseutgivare-instans för att hantera all kommunikation
- Initierar ett hjälteobjekt vid startpositionen
// Step 3: Set up event listeners (subscribers)
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.moveTo(hero.x - 5, hero.y);
console.log(`Hero moved to position: ${hero.x}, ${hero.y}`);
});
eventEmitter.on(Messages.HERO_MOVE_RIGHT, () => {
hero.moveTo(hero.x + 5, hero.y);
console.log(`Hero moved to position: ${hero.x}, ${hero.y}`);
});
I ovanstående har vi:
- Registrerat händelselyssnare som svarar på rörelsemeddelanden
- Uppdaterat hjälteobjektets position baserat på rörelseriktningen
- Lagt till konsolloggar för att spåra hjälteobjektets positionsförändringar
- Separerat rörelselogiken från inmatningshanteringen
// Step 4: Connect keyboard input to events (publishers)
window.addEventListener('keydown', (event) => {
switch(event.key) {
case 'ArrowLeft':
eventEmitter.emit(Messages.HERO_MOVE_LEFT);
break;
case 'ArrowRight':
eventEmitter.emit(Messages.HERO_MOVE_RIGHT);
break;
}
});
Att förstå dessa koncept:
- Kopplar tangentbordsinmatning till spelhändelser utan hård koppling
- Möjliggör att inmatningssystemet kommunicerar med spelobjekt indirekt
- Tillåter flera system att reagera på samma tangentbordshändelser
- Gör det enkelt att ändra tangentbindningar eller lägga till nya inmatningsmetoder
💡 Proffstips: Det fina med detta mönster är flexibiliteten! Du kan enkelt lägga till ljudeffekter, skärmryckningar eller partikeleffekter genom att helt enkelt lägga till fler händelselyssnare – ingen anledning att ändra den befintliga tangentbords- eller rörelsekoden.
Här är varför du kommer att älska denna metod:
- Att lägga till nya funktioner blir superenkelt – lyssna bara på de händelser du bryr dig om
- Flera saker kan reagera på samma händelse utan att störa varandra
- Testning blir mycket enklare eftersom varje del fungerar oberoende
- När något går fel vet du exakt var du ska leta
Varför Pub/Sub skalar effektivt
Pub/sub-mönstret bibehåller enkelheten när applikationer växer i komplexitet. Oavsett om det handlar om att hantera dussintals fiender, dynamiska UI-uppdateringar eller ljudsystem, hanterar mönstret ökad skala utan arkitektoniska förändringar. Nya funktioner integreras i det befintliga händelsesystemet utan att påverka etablerad funktionalitet.
⚠️ Vanligt misstag: Skapa inte för många specifika meddelandetyper tidigt. Börja med breda kategorier och förfina dem när ditt spels behov blir tydligare.
Bästa praxis att följa:
- Grupperar relaterade meddelanden i logiska kategorier
- Använder beskrivande namn som tydligt anger vad som hände
- Håller meddelandepaket enkla och fokuserade
- Dokumenterar dina meddelandetyper för samarbete i teamet
GitHub Copilot Agent-utmaning 🚀
Använd Agent-läget för att slutföra följande utmaning:
Beskrivning: Skapa ett enkelt system för spelobjekt med både arv och pub/sub-mönstret. Du ska implementera ett grundläggande spel där olika objekt kan kommunicera genom händelser utan att direkt känna till varandra.
Uppgift: Skapa ett JavaScript-spelsystem med följande krav: 1) Skapa en bas GameObject-klass med x-, y-koordinater och en typ-egenskap. 2) Skapa en Hero-klass som utökar GameObject och kan röra sig. 3) Skapa en Enemy-klass som utökar GameObject och kan jaga hjälten. 4) Implementera en EventEmitter-klass för pub/sub-mönstret. 5) Ställ in händelselyssnare så att när hjälten rör sig, får närliggande fiender ett 'HERO_MOVED'-meddelande och uppdaterar sin position för att röra sig mot hjälten. Inkludera konsolloggar för att visa kommunikationen mellan objekten.
Läs mer om agent mode här.
🚀 Utmaning
Fundera på hur pub-sub-mönstret kan förbättra spelarkitekturen. Identifiera vilka komponenter som bör sända händelser och hur systemet ska svara. Designa ett spelkoncept och kartlägg kommunikationsmönstren mellan dess komponenter.
Quiz efter föreläsningen
Granskning & Självstudier
Lär dig mer om Pub/Sub genom att läsa om det.
Uppgift
Ansvarsfriskrivning:
Detta dokument har översatts med hjälp av AI-översättningstjänsten Co-op Translator. Även om vi strävar efter noggrannhet, bör det noteras att automatiserade översättningar kan innehålla fel eller felaktigheter. Det ursprungliga dokumentet på dess ursprungliga språk bör betraktas som den auktoritativa källan. För kritisk information rekommenderas professionell mänsklig översättning. Vi ansvarar inte för eventuella missförstånd eller feltolkningar som uppstår vid användning av denna översättning.
