# Създаване на космическа игра, част 3: Добавяне на движение Помислете за любимите си игри – това, което ги прави завладяващи, не са само красивите графики, а начинът, по който всичко се движи и реагира на вашите действия. В момента вашата космическа игра е като красива картина, но сега ще добавим движение, което ще я оживи. Когато инженерите на НАСА програмираха компютъра за управление на мисиите Аполо, те се сблъскаха с подобно предизвикателство: как да накарат космическия кораб да реагира на командите на пилота, като същевременно автоматично поддържа корекции на курса? Принципите, които ще научим днес, отразяват същите концепции – управление на движението, контролирано от играча, заедно с автоматични системни поведения. В този урок ще научите как да накарате космическите кораби да се движат по екрана, да реагират на командите на играча и да създават плавни модели на движение. Ще разделим всичко на управляеми концепции, които естествено се надграждат една върху друга. До края ще можете да управлявате героичния си кораб по екрана, докато вражеските кораби патрулират над него. По-важното е, че ще разберете основните принципи, които задвижват системите за движение в игрите. ## Тест преди лекцията [Тест преди лекцията](https://ff-quizzes.netlify.app/web/quiz/33) ## Разбиране на движението в игрите Игрите оживяват, когато нещата започнат да се движат, и основно има два начина, по които това се случва: - **Движение, контролирано от играча**: Когато натиснете клавиш или кликнете с мишката, нещо се движи. Това е директната връзка между вас и света на играта. - **Автоматично движение**: Когато самата игра решава да движи нещата – например онези вражески кораби, които трябва да патрулират по екрана, независимо дали правите нещо или не. Да накарате обекти да се движат на компютърен екран е по-лесно, отколкото си мислите. Спомняте ли си координатите x и y от часовете по математика? Точно с тях ще работим тук. Когато Галилей наблюдавал луните на Юпитер през 1610 г., той всъщност правел същото – проследявал позиции във времето, за да разбере моделите на движение. Движението на екрана е като създаване на анимация с листчета – трябва да следвате тези три прости стъпки: 1. **Актуализирайте позицията** – Променете къде трябва да бъде вашият обект (например преместете го с 5 пиксела надясно) 2. **Изтрийте стария кадър** – Изчистете екрана, за да не виждате призрачни следи навсякъде 3. **Нарисувайте новия кадър** – Поставете обекта си на новото му място Правете това достатъчно бързо и бум! Ще получите плавно движение, което се усеща естествено за играчите. Ето как може да изглежда това в код: ```javascript // 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); ``` **Какво прави този код:** - **Актуализира** x-координатата на героя с 5 пиксела, за да го премести хоризонтално - **Изчиства** цялата област на платното, за да премахне предишния кадър - **Запълва** платното с черен фон - **Преначертава** изображението на героя на новата му позиция ✅ Можете ли да измислите причина, поради която преначертаването на героя много кадри в секунда може да доведе до разходи за производителност? Прочетете за [алтернативи на този модел](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas). ## Обработка на събития от клавиатурата Тук свързваме входа на играча с действията в играта. Когато някой натисне интервала, за да изстреля лазер, или докосне стрелка, за да избегне астероид, вашата игра трябва да открие и да реагира на този вход. Събитията от клавиатурата се случват на ниво прозорец, което означава, че целият прозорец на браузъра слуша тези натискания на клавиши. Кликванията с мишката, от друга страна, могат да бъдат свързани със специфични елементи (като кликване върху бутон). За нашата космическа игра ще се фокусираме върху контроли с клавиатура, тъй като те дават на играчите класическо аркадно усещане. Това ми напомня как телеграфните оператори през 1800-те години трябваше да превеждат входа на морзовия код в смислени съобщения – правим нещо подобно, превеждаме натисканията на клавиши в команди за игра. За да обработите събитие, трябва да използвате метода `addEventListener()` на прозореца и да му предоставите два входни параметъра. Първият параметър е името на събитието, например `keyup`. Вторият параметър е функцията, която трябва да бъде извикана в резултат на възникването на събитието. Ето пример: ```javascript window.addEventListener('keyup', (evt) => { // evt.key = string representation of the key if (evt.key === 'ArrowUp') { // do something } }); ``` **Разбивка на случващото се тук:** - **Слуша** събития от клавиатурата в целия прозорец - **Улавя** обекта на събитието, който съдържа информация за това кой клавиш е натиснат - **Проверява** дали натиснатият клавиш съответства на конкретен клавиш (в случая стрелката нагоре) - **Изпълнява** код, когато условието е изпълнено За събития от клавиатурата има два свойства на събитието, които можете да използвате, за да видите кой клавиш е натиснат: - `key` - това е текстово представяне на натиснатия клавиш, например `'ArrowUp'` - `keyCode` - това е числово представяне, например `37`, което съответства на `ArrowLeft` ✅ Манипулацията на събития от клавиатурата е полезна извън разработката на игри. За какви други приложения можете да се сетите за тази техника? ### Специални клавиши: внимание! Някои клавиши имат вградени поведения на браузъра, които могат да попречат на вашата игра. Стрелките превъртат страницата, а интервалът я премества надолу – поведения, които не искате, когато някой се опитва да управлява своя космически кораб. Можем да предотвратим тези стандартни поведения и да оставим нашата игра да обработва входа вместо това. Това е подобно на начина, по който ранните компютърни програмисти трябваше да отменят системни прекъсвания, за да създадат персонализирани поведения – просто го правим на ниво браузър. Ето как: ```javascript const 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); ``` **Разбиране на този код за предотвратяване:** - **Проверява** за конкретни кодове на клавиши, които могат да причинят нежелано поведение на браузъра - **Предотвратява** стандартното действие на браузъра за стрелките и интервала - **Позволява** на други клавиши да функционират нормално - **Използва** `e.preventDefault()`, за да спре вграденото поведение на браузъра ## Движение, предизвикано от играта Сега нека поговорим за обекти, които се движат без вход от играча. Помислете за вражески кораби, които се движат по екрана, куршуми, летящи в прави линии, или облаци, които се носят на заден план. Това автономно движение прави света на играта да изглежда жив, дори когато никой не докосва контролите. Използваме вградените таймери на JavaScript, за да актуализираме позициите на редовни интервали. Тази концепция е подобна на начина, по който работят махаловите часовници – редовен механизъм, който задейства последователни, времеви действия. Ето колко просто може да бъде: ```javascript const id = setInterval(() => { // Move the enemy on the y axis enemy.y += 10; }, 100); ``` **Какво прави този код за движение:** - **Създава** таймер, който се изпълнява на всеки 100 милисекунди - **Актуализира** y-координатата на врага с 10 пиксела всеки път - **Съхранява** ID на интервала, за да можем да го спрем по-късно, ако е необходимо - **Премества** врага надолу по екрана автоматично ## Игровият цикъл Ето концепцията, която обединява всичко – игровият цикъл. Ако вашата игра беше филм, игровият цикъл щеше да бъде проекторът, показващ кадър след кадър толкова бързо, че всичко изглежда плавно. Всяка игра има един от тези цикли, който работи зад кулисите. Това е функция, която актуализира всички игрови обекти, преначертава екрана и повтаря този процес непрекъснато. Това следи вашия герой, всички врагове, всички летящи лазери – цялото състояние на играта. Тази концепция ми напомня как ранните аниматори като Уолт Дисни трябваше да преначертават героите кадър по кадър, за да създадат илюзията за движение. Ние правим същото, само че с код вместо с моливи. Ето как обикновено изглежда игровият цикъл, изразен в код: ```javascript const 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(); } gameLoop(); }, 200); ``` **Разбиране на структурата на игровия цикъл:** - **Изчиства** цялото платно, за да премахне предишния кадър - **Запълва** фона с плътен цвят - **Рисува** всички игрови обекти на текущите им позиции - **Повтаря** този процес на всеки 200 милисекунди, за да създаде плавна анимация - **Управлява** честотата на кадрите чрез контролиране на времето на интервала ## Продължаване на космическата игра Сега ще добавим движение към статичната сцена, която създадохте преди. Ще я трансформираме от екранна снимка в интерактивно изживяване. Ще работим стъпка по стъпка, за да гарантираме, че всяка част се надгражда върху предишната. Вземете кода от мястото, където спряхме в предишния урок (или започнете с кода в папката [Part II- starter](../../../../6-space-game/3-moving-elements-around/your-work), ако имате нужда от ново начало). **Ето какво ще изградим днес:** - **Контроли на героя**: Стрелките ще управляват вашия космически кораб по екрана - **Движение на враговете**: Тези извънземни кораби ще започнат своето настъпление Нека започнем с имплементацията на тези функции. ## Препоръчителни стъпки Намерете файловете, които са създадени за вас в подпапката `your-work`. Тя трябва да съдържа следното: ```bash -| assets -| enemyShip.png -| player.png -| index.html -| app.js -| package.json ``` Започнете проекта си в папката `your-work`, като въведете: ```bash cd your-work npm start ``` **Какво прави тази команда:** - **Навигира** до директорията на вашия проект - **Стартира** HTTP сървър на адрес `http://localhost:5000` - **Сервира** файловете на вашата игра, за да можете да ги тествате в браузър Горната команда ще стартира HTTP сървър на адрес `http://localhost:5000`. Отворете браузър и въведете този адрес, в момента трябва да се визуализира героят и всички врагове; нищо все още не се движи - но скоро ще! ### Добавяне на код 1. **Добавете специални обекти** за `hero`, `enemy` и `game object`, те трябва да имат свойства `x` и `y`. (Спомнете си частта за [Наследяване или композиция](../README.md)). *ПОДСКАЗКА* `game object` трябва да бъде този с `x` и `y` и способността да се рисува върху платно. > **Съвет**: Започнете, като добавите нов клас `GameObject` с неговия конструктор, описан по-долу, и след това го нарисувайте върху платното: ```javascript 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); } } ``` **Разбиране на този базов клас:** - **Определя** общи свойства, които всички игрови обекти споделят (позиция, размер, изображение) - **Включва** флаг `dead`, за да следи дали обектът трябва да бъде премахнат - **Осигурява** метод `draw()`, който изобразява обекта върху платното - **Задава** стандартни стойности за всички свойства, които дъщерните класове могат да заменят Сега разширете този `GameObject`, за да създадете `Hero` и `Enemy`: ```javascript class Hero extends GameObject { constructor(x, y) { super(x, y); this.width = 98; this.height = 75; this.type = "Hero"; this.speed = 5; } } ``` ```javascript class Enemy extends GameObject { constructor(x, y) { super(x, y); this.width = 98; this.height = 50; this.type = "Enemy"; const id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y); clearInterval(id); } }, 300); } } ``` **Основни концепции в тези класове:** - **Наследява** от `GameObject`, използвайки ключовата дума `extends` - **Извиква** конструктора на родителя с `super(x, y)` - **Задава** специфични размери и свойства за всеки тип обект - **Имплементира** автоматично движение за враговете, използвайки `setInterval()` 2. **Добавете обработчици на събития от клавиатурата**, за да управлявате движението на героя (нагоре/надолу, наляво/надясно) *ЗАПОМНЕТЕ* това е картезианска система, горният ляв ъгъл е `0,0`. Също така не забравяйте да добавите код за спиране на *стандартното поведение* > **Съвет**: Създайте вашата функция `onKeyDown` и я свържете към прозореца: ```javascript const onKeyDown = function (e) { console.log(e.keyCode); // Add the code from the lesson above to stop default behavior 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); ``` **Какво прави този обработчик на събития:** - **Слуша** събития за натискане на клавиши в целия прозорец - **Записва** кода на клавиша, за да ви помогне да разберете кои клавиши се натискат - **Предотвратява** стандартното поведение на браузъра за стрелките и интервала - **Позволява** други клавиши да функционират нормално Проверете конзолата на браузъра си на този етап и наблюдавайте как се записват натисканията на клавиши. 3. **Имплементирайте** [Модела Pub-Sub](../README.md), това ще поддържа кода ви чист, докато следвате останалите части. Моделът Publish-Subscribe помага да организирате кода си, като разделя откриването на събития от тяхната обработка. Това прави кода ви по-модулен и лесен за поддръжка. За да направите тази последна част, можете: 1. **Добавете слушател на събития** към прозореца: ```javascript 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); } }); ``` **Какво прави тази система за събития:** - **Открива** вход от клавиатурата и го преобразува в персонализирани събития за игра - **Разделя** откриването на вход от логиката на играта - **Прави** лесно промяната на контролите по-късно, без да се засяга кода на играта - **Позволява** множество системи да реагират на един и същ вход 2. **Създайте клас EventEmitter**, за да публикувате и абонирате съобщения: ```javascript class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } 3. **Добавете константи** и настройте EventEmitter: ```javascript 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(); ``` **Разбиране на настройката:** - **Определя** константи за съобщения, за да избегне грешки и да улесни рефакторинга - **Декларира** променливи за изображения, контекст на плат - **Създава** мрежа от врагове, използвайки вложени цикли - **Присвоява** изображението на врага на всеки обект враг - **Добавя** всеки враг към глобалния масив с игрови обекти и добавете функция `createHero()`, която да изпълнява подобен процес за героя. ```javascript function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); } ``` **Какво прави създаването на героя:** - **Позиционира** героя в долния център на екрана - **Присвоява** изображението на героя на обекта герой - **Добавя** героя към масива с игрови обекти за рендиране и накрая добавете функция `drawGameObjects()`, за да започнете рисуването: ```javascript function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); } ``` **Разбиране на функцията за рисуване:** - **Итерира** през всички игрови обекти в масива - **Извиква** метода `draw()` за всеки обект - **Предава** контекста на платното, за да могат обектите да се рендират сами Вашите врагове трябва да започнат да настъпват към космическия кораб на героя! } } ``` and add a `createHero()` function to do a similar process for the hero. ```javascript function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); } ``` и накрая добавете функция `drawGameObjects()`, за да започнете рисуването: ```javascript function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); } ``` Вашите врагове трябва да започнат да настъпват към космическия кораб на героя! --- ## Предизвикателство на GitHub Copilot Agent 🚀 Ето едно предизвикателство, което ще подобри полировката на вашата игра: добавяне на граници и плавни контроли. В момента вашият герой може да излети извън екрана, а движението може да изглежда накъсано. **Вашата мисия:** Направете космическия кораб да се усеща по-реалистично, като внедрите граници на екрана и плавно движение. Това е подобно на начина, по който системите за управление на полети на НАСА предотвратяват превишаването на безопасните оперативни параметри на космическите кораби. **Какво да изградите:** Създайте система, която да задържа космическия кораб на героя на екрана и направете контролите да се усещат плавно. Когато играчите задържат клавиш със стрелка, корабът трябва да се движи плавно, вместо да се премества на отделни стъпки. Помислете за добавяне на визуална обратна връзка, когато корабът достигне границите на екрана – например лек ефект, който да показва края на игровата зона. Научете повече за [режим агент](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) тук. ## 🚀 Предизвикателство Организацията на кода става все по-важна, когато проектите растат. Може би сте забелязали, че вашият файл става претрупан с функции, променливи и класове, смесени заедно. Това ми напомня за начина, по който инженерите, организиращи кода за мисията Аполо, трябваше да създадат ясни, поддържани системи, върху които множество екипи да могат да работят едновременно. **Вашата мисия:** Мислете като софтуерен архитект. Как бихте организирали кода си така, че след шест месеца вие (или ваш колега) да можете да разберете какво се случва? Дори всичко да остане в един файл засега, можете да създадете по-добра организация: - **Групиране на свързани функции** заедно с ясни заглавия на коментари - **Разделяне на отговорностите** - отделете игровата логика от рендирането - **Използване на последователни наименования** за променливи и функции - **Създаване на модули** или пространства от имена за организиране на различни аспекти на вашата игра - **Добавяне на документация**, която обяснява целта на всяка основна секция **Въпроси за размисъл:** - Кои части от вашия код са най-трудни за разбиране, когато се върнете към тях? - Как бихте могли да организирате кода си, за да улесните някой друг да допринесе? - Какво би се случило, ако искате да добавите нови функции като бонуси или различни типове врагове? ## Тест след лекцията [Тест след лекцията](https://ff-quizzes.netlify.app/web/quiz/34) ## Преглед и самостоятелно обучение Създаваме всичко от нулата, което е страхотно за учене, но ето една малка тайна – има невероятни JavaScript рамки, които могат да се справят с много от тежката работа вместо вас. След като се почувствате уверени с основите, които разгледахме, си струва [да проучите наличните опции](https://github.com/collections/javascript-game-engines). Мислете за рамките като за добре оборудвана кутия с инструменти, вместо да правите всеки инструмент на ръка. Те могат да решат много от тези предизвикателства за организация на кода, за които говорихме, плюс да предложат функции, които биха отнели седмици за изграждане сами. **Неща, които си струва да се проучат:** - Как игровите двигатели организират кода – ще бъдете изумени от умните модели, които използват - Трикове за производителност, които правят игрите с платно да работят гладко - Съвременни функции на JavaScript, които могат да направят кода ви по-чист и по-лесен за поддръжка - Различни подходи за управление на игрови обекти и техните взаимоотношения ## Задача [Коментирайте вашия код](assignment.md) --- **Отказ от отговорност**: Този документ е преведен с помощта на AI услуга за превод [Co-op Translator](https://github.com/Azure/co-op-translator). Въпреки че се стремим към точност, моля, имайте предвид, че автоматизираните преводи може да съдържат грешки или неточности. Оригиналният документ на неговия роден език трябва да се счита за авторитетен източник. За критична информация се препоръчва професионален човешки превод. Ние не носим отговорност за каквито и да е недоразумения или погрешни интерпретации, произтичащи от използването на този превод.