|
3 weeks ago | |
---|---|---|
.. | ||
README.md | 3 weeks ago | |
assignment.md | 4 weeks ago |
README.md
Tworzenie gry kosmicznej, część 3: Dodawanie ruchu
Quiz przed wykładem
Gry nie są zbyt ekscytujące, dopóki na ekranie nie pojawią się poruszające się obiekty! W tej grze wykorzystamy dwa rodzaje ruchu:
- Ruch za pomocą klawiatury/myszy: gdy użytkownik używa klawiatury lub myszy, aby poruszać obiektem na ekranie.
- Ruch generowany przez grę: gdy gra automatycznie porusza obiektem w określonych odstępach czasu.
Jak więc poruszać obiektami na ekranie? Wszystko sprowadza się do współrzędnych kartezjańskich: zmieniamy położenie (x, y) obiektu, a następnie odświeżamy ekran.
Typowe kroki, aby osiągnąć ruch na ekranie, to:
- Ustawienie nowej lokalizacji obiektu; jest to konieczne, aby obiekt wydawał się poruszać.
- Wyczyszczenie ekranu, ekran musi być czyszczony pomiędzy kolejnymi rysowaniami. Możemy to zrobić, rysując prostokąt wypełniony kolorem tła.
- Ponowne narysowanie obiektu w nowej lokalizacji. Dzięki temu osiągamy efekt przesunięcia obiektu z jednego miejsca na drugie.
Oto jak to może wyglądać w kodzie:
//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
✅ Czy potrafisz wymyślić, dlaczego rysowanie bohatera wiele razy na sekundę może powodować problemy z wydajnością? Przeczytaj o alternatywach dla tego wzorca.
Obsługa zdarzeń klawiatury
Zdarzenia obsługujemy, przypisując konkretne zdarzenia do kodu. Zdarzenia klawiatury są wywoływane na całym oknie, podczas gdy zdarzenia myszy, takie jak click
, mogą być powiązane z kliknięciem konkretnego elementu. W tym projekcie będziemy korzystać ze zdarzeń klawiatury.
Aby obsłużyć zdarzenie, należy użyć metody addEventListener()
okna i podać jej dwa parametry wejściowe. Pierwszy parametr to nazwa zdarzenia, na przykład keyup
. Drugi parametr to funkcja, która ma zostać wywołana w wyniku wystąpienia zdarzenia.
Oto przykład:
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
Dla zdarzeń klawiatury istnieją dwie właściwości zdarzenia, które można wykorzystać, aby sprawdzić, który klawisz został naciśnięty:
key
, czyli reprezentacja tekstowa naciśniętego klawisza, na przykładArrowUp
.keyCode
, czyli reprezentacja numeryczna, na przykład37
, odpowiadającaArrowLeft
.
✅ Manipulacja zdarzeniami klawiatury jest przydatna nie tylko w tworzeniu gier. Jakie inne zastosowania tej techniki przychodzą Ci do głowy?
Klawisze specjalne: uwaga
Istnieją pewne specjalne klawisze, które wpływają na okno. Oznacza to, że jeśli nasłuchujesz zdarzenia keyup
i używasz tych klawiszy do poruszania bohaterem, może to również powodować przewijanie poziome. Z tego powodu możesz chcieć wyłączyć to wbudowane zachowanie przeglądarki podczas tworzenia gry. Potrzebujesz kodu takiego jak ten:
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
Powyższy kod zapewni, że klawisze strzałek i spacja będą miały domyślne zachowanie wyłączone. Mechanizm wyłączania działa, gdy wywołujemy e.preventDefault()
.
Ruch generowany przez grę
Możemy sprawić, że obiekty będą poruszać się same, używając timerów, takich jak funkcje setTimeout()
lub setInterval()
, które aktualizują lokalizację obiektu w każdym cyklu czasowym. Oto jak to może wyglądać:
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
Pętla gry
Pętla gry to koncepcja, która polega na funkcji wywoływanej w regularnych odstępach czasu. Nazywa się ją pętlą gry, ponieważ wszystko, co powinno być widoczne dla użytkownika, jest rysowane w tej pętli. Pętla gry wykorzystuje wszystkie obiekty gry, które są jej częścią, rysując je, chyba że z jakiegoś powodu nie powinny już być częścią gry. Na przykład, jeśli obiekt to wróg, który został trafiony laserem i eksploduje, nie jest już częścią bieżącej pętli gry (więcej na ten temat dowiesz się w kolejnych lekcjach).
Oto jak typowa pętla gry może wyglądać w kodzie:
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
Powyższa pętla jest wywoływana co 200
milisekund, aby odświeżyć płótno. Możesz wybrać najlepszy interwał, który ma sens dla Twojej gry.
Kontynuacja gry kosmicznej
Weźmiesz istniejący kod i go rozbudujesz. Możesz zacząć od kodu, który ukończyłeś w części I, lub użyć kodu z Part II- starter.
- Ruch bohatera: dodasz kod, aby umożliwić poruszanie bohaterem za pomocą klawiszy strzałek.
- Ruch wrogów: dodasz również kod, aby wrogowie poruszali się z góry na dół w określonym tempie.
Zalecane kroki
Znajdź pliki, które zostały dla Ciebie utworzone w podfolderze your-work
. Powinny zawierać następujące:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Uruchom swój projekt w folderze your_work
, wpisując:
cd your-work
npm start
Powyższe polecenie uruchomi serwer HTTP pod adresem http://localhost:5000
. Otwórz przeglądarkę i wpisz ten adres. Na razie powinien wyświetlać bohatera i wszystkich wrogów; nic się jeszcze nie porusza!
Dodaj kod
-
Dodaj dedykowane obiekty dla
hero
,enemy
igame object
, które powinny mieć właściwościx
iy
. (Pamiętaj o części dotyczącej dziedziczenia lub kompozycji).PODPOWIEDŹ
game object
powinien być tym, który max
iy
oraz zdolność rysowania się na płótnie.wskazówka: zacznij od dodania nowej klasy GameObject z jej konstruktorem zdefiniowanym jak poniżej, a następnie narysuj ją na płótnie:
class GameObject { constructor(x, y) { this.x = x; this.y = y; this.dead = false; this.type = ""; this.width = 0; this.height = 0; this.img = undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } }
Teraz rozszerz ten GameObject, aby utworzyć Hero i Enemy.
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } }
-
Dodaj obsługę zdarzeń klawiatury, aby obsługiwać nawigację klawiszami (poruszanie bohaterem w górę/dół, w lewo/prawo).
PAMIĘTAJ to układ kartezjański, lewy górny róg to
0,0
. Pamiętaj również, aby dodać kod zatrzymujący domyślne zachowanie.wskazówka: utwórz swoją funkcję onKeyDown i przypisz ją do okna:
let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown);
Sprawdź konsolę przeglądarki w tym momencie i obserwuj logowane naciśnięcia klawiszy.
-
Zaimplementuj wzorzec Pub-Sub, aby Twój kod był bardziej przejrzysty w kolejnych częściach.
Aby to zrobić, możesz:
-
Dodać nasłuchiwacza zdarzeń do okna:
window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } });
-
Utworzyć klasę EventEmitter, aby publikować i subskrybować wiadomości:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach((l) => l(message, payload)); } } }
-
Dodać stałe i skonfigurować EventEmitter:
const Messages = { KEY_EVENT_UP: "KEY_EVENT_UP", KEY_EVENT_DOWN: "KEY_EVENT_DOWN", KEY_EVENT_LEFT: "KEY_EVENT_LEFT", KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter();
-
Zainicjalizować grę
function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -=5 ; }) eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); }
-
-
Skonfiguruj pętlę gry
Zrefaktoruj funkcję window.onload, aby zainicjalizować grę i ustawić pętlę gry w odpowiednim interwale. Dodasz również wiązkę laserową:
window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) };
-
Dodaj kod, aby wrogowie poruszali się w określonych odstępach czasu.
Zrefaktoruj funkcję
createEnemies()
, aby tworzyła wrogów i dodawała ich do nowej klasy gameObjects:function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } }
oraz dodaj funkcję
createHero()
, aby wykonać podobny proces dla bohatera.function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); }
Na koniec dodaj funkcję
drawGameObjects()
, aby rozpocząć rysowanie:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
Twoi wrogowie powinni zacząć zbliżać się do statku kosmicznego bohatera!
🚀 Wyzwanie
Jak widzisz, Twój kod może zamienić się w "spaghetti", gdy zaczynasz dodawać funkcje, zmienne i klasy. Jak możesz lepiej zorganizować swój kod, aby był bardziej czytelny? Naszkicuj system organizacji kodu, nawet jeśli nadal znajduje się w jednym pliku.
Quiz po wykładzie
Przegląd i samodzielna nauka
Chociaż piszemy naszą grę bez użycia frameworków, istnieje wiele frameworków opartych na JavaScript do tworzenia gier na płótnie. Poświęć trochę czasu na przeczytanie o nich.
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 języku źródłowym powinien być uznawany za autorytatywne ź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.