9.4 KiB
Stwórz grę kosmiczną, część 1: Wprowadzenie
Quiz przed wykładem
Dziedziczenie i kompozycja w tworzeniu gier
W poprzednich lekcjach nie było potrzeby martwić się o architekturę projektową aplikacji, ponieważ projekty miały niewielki zakres. Jednak gdy Twoje aplikacje rosną pod względem rozmiaru i złożoności, decyzje architektoniczne stają się coraz ważniejsze. Istnieją dwa główne podejścia do tworzenia większych aplikacji w JavaScript: kompozycja lub dziedziczenie. Oba mają swoje zalety i wady, ale wyjaśnijmy je w kontekście gry.
✅ Jedna z najsłynniejszych książek o programowaniu dotyczy wzorców projektowych.
W grze masz obiekty gry
, czyli obiekty, które istnieją na ekranie. Oznacza to, że mają one lokalizację w kartezjańskim układzie współrzędnych, charakteryzowaną przez współrzędne x
i y
. Tworząc grę, zauważysz, że wszystkie obiekty gry mają standardowe właściwości, wspólne dla każdej gry, które tworzysz, a mianowicie elementy, które są:
- oparte na lokalizacji Większość, jeśli nie wszystkie, elementy gry są oparte na lokalizacji. Oznacza to, że mają one lokalizację,
x
iy
. - ruchome Są to obiekty, które mogą przemieszczać się do nowej lokalizacji. Zazwyczaj jest to bohater, potwór lub NPC (postać niezależna), ale nie na przykład obiekt statyczny, taki jak drzewo.
- samodestrukcyjne Te obiekty istnieją tylko przez określony czas, zanim zostaną oznaczone do usunięcia. Zazwyczaj jest to reprezentowane przez wartość logiczną
dead
lubdestroyed
, która sygnalizuje silnikowi gry, że ten obiekt nie powinien być już renderowany. - z czasem odnowienia 'Czas odnowienia' to typowa właściwość krótkotrwałych obiektów. Typowym przykładem jest kawałek tekstu lub efekt graficzny, taki jak eksplozja, który powinien być widoczny tylko przez kilka milisekund.
✅ Pomyśl o grze, takiej jak Pac-Man. Czy potrafisz zidentyfikować cztery wymienione powyżej typy obiektów w tej grze?
Wyrażanie zachowań
Wszystkie opisane powyżej cechy to zachowania, które mogą mieć obiekty gry. Jak je zakodować? Możemy wyrazić te zachowania jako metody powiązane z klasami lub obiektami.
Klasy
Pomysł polega na użyciu klas
w połączeniu z dziedziczeniem
, aby dodać określone zachowanie do klasy.
✅ Dziedziczenie to ważna koncepcja do zrozumienia. Dowiedz się więcej z artykułu MDN o dziedziczeniu.
Wyrażone w kodzie, obiekt gry może wyglądać tak:
//set up the class GameObject
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
}
//this class will extend the GameObject's inherent class properties
class Movable extends GameObject {
constructor(x,y, type) {
super(x,y, type)
}
//this movable object can be moved on the screen
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//this is a specific class that extends the Movable class, so it can take advantage of all the properties that it inherits
class Hero extends Movable {
constructor(x,y) {
super(x,y, 'Hero')
}
}
//this class, on the other hand, only inherits the GameObject properties
class Tree extends GameObject {
constructor(x,y) {
super(x,y, 'Tree')
}
}
//a hero can move...
const hero = new Hero();
hero.moveTo(5,5);
//but a tree cannot
const tree = new Tree();
✅ Poświęć kilka minut, aby wyobrazić sobie bohatera Pac-Mana (na przykład Inky, Pinky lub Blinky) i jak można go napisać w JavaScript.
Kompozycja
Innym sposobem radzenia sobie z dziedziczeniem obiektów jest użycie kompozycji. Wtedy obiekty wyrażają swoje zachowanie w ten sposób:
//create a constant gameObject
const gameObject = {
x: 0,
y: 0,
type: ''
};
//...and a constant movable
const movable = {
moveTo(x, y) {
this.x = x;
this.y = y;
}
}
//then the constant movableObject is composed of the gameObject and movable constants
const movableObject = {...gameObject, ...movable};
//then create a function to create a new Hero who inherits the movableObject properties
function createHero(x, y) {
return {
...movableObject,
x,
y,
type: 'Hero'
}
}
//...and a static object that inherits only the gameObject properties
function createStatic(x, y, type) {
return {
...gameObject
x,
y,
type
}
}
//create the hero and move it
const hero = createHero(10,10);
hero.moveTo(5,5);
//and create a static tree which only stands around
const tree = createStatic(0,0, 'Tree');
Który wzorzec powinienem wybrać?
To zależy od Ciebie, który wzorzec wybierzesz. JavaScript obsługuje oba te paradygmaty.
--
Inny wzorzec, powszechny w tworzeniu gier, rozwiązuje problem zarządzania doświadczeniem użytkownika i wydajnością gry.
Wzorzec pub/sub
✅ Pub/Sub oznacza 'publish-subscribe' (publikuj-subskrybuj)
Ten wzorzec odnosi się do idei, że różne części Twojej aplikacji nie powinny wiedzieć o sobie nawzajem. Dlaczego? Dzięki temu łatwiej jest zrozumieć, co się dzieje w ogóle, jeśli różne części są oddzielone. Ułatwia to również nagłą zmianę zachowania, jeśli zajdzie taka potrzeba. Jak to osiągnąć? Robimy to, wprowadzając kilka pojęć:
- wiadomość: Wiadomość to zazwyczaj ciąg tekstowy z opcjonalnym ładunkiem (danymi, które wyjaśniają, czego dotyczy wiadomość). Typowa wiadomość w grze może być
KEY_PRESSED_ENTER
. - wydawca: Ten element publikuje wiadomość i wysyła ją do wszystkich subskrybentów.
- subskrybent: Ten element nasłuchuje określonych wiadomości i wykonuje jakieś zadanie w wyniku ich otrzymania, na przykład wystrzelenie lasera.
Implementacja jest dość mała, ale to bardzo potężny wzorzec. Oto jak można go zaimplementować:
//set up an EventEmitter class that contains listeners
class EventEmitter {
constructor() {
this.listeners = {};
}
//when a message is received, let the listener to handle its payload
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
//when a message is sent, send it to a listener with some payload
emit(message, payload = null) {
if (this.listeners[message]) {
this.listeners[message].forEach(l => l(message, payload))
}
}
}
Aby użyć powyższego kodu, możemy stworzyć bardzo małą implementację:
//set up a message structure
const Messages = {
HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//invoke the eventEmitter you set up above
const eventEmitter = new EventEmitter();
//set up a hero
const hero = createHero(0,0);
//let the eventEmitter know to watch for messages pertaining to the hero moving left, and act on it
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
//set up the window to listen for the keyup event, specifically if the left arrow is hit, emit a message to move the hero left
window.addEventListener('keyup', (evt) => {
if (evt.key === 'ArrowLeft') {
eventEmitter.emit(Messages.HERO_MOVE_LEFT)
}
});
Powyżej łączymy zdarzenie klawiatury, ArrowLeft
, i wysyłamy wiadomość HERO_MOVE_LEFT
. Nasłuchujemy tej wiadomości i w wyniku tego przesuwamy hero
. Siła tego wzorca polega na tym, że nasłuchiwacz zdarzeń i bohater nie wiedzą o sobie nawzajem. Możesz przypisać ArrowLeft
do klawisza A
. Dodatkowo możliwe byłoby zrobienie czegoś zupełnie innego na ArrowLeft
, dokonując kilku edycji funkcji on
w eventEmitter
:
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
hero.move(5,0);
});
Gdy Twoja gra staje się bardziej skomplikowana, ten wzorzec pozostaje tak samo prosty, a Twój kod pozostaje czysty. Naprawdę warto przyjąć ten wzorzec.
🚀 Wyzwanie
Pomyśl, jak wzorzec pub-sub może ulepszyć grę. Które części powinny emitować zdarzenia, a jak gra powinna na nie reagować? Teraz masz szansę być kreatywnym, wymyślając nową grę i sposób, w jaki jej elementy mogą się zachowywać.
Quiz po wykładzie
Przegląd i samodzielna nauka
Dowiedz się więcej o Pub/Sub, czytając o tym.
Zadanie
Zastrzeżenie:
Ten dokument został przetłumaczony za pomocą usługi tłumaczenia AI Co-op Translator. Chociaż dokładamy wszelkich starań, aby tłumaczenie było precyzyjne, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub nieścisłości. Oryginalny dokument w jego rodzimym języku powinien być uznawany za wiarygodne źródło. W przypadku informacji o kluczowym znaczeniu zaleca się skorzystanie z profesjonalnego tłumaczenia przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.