Кожен туторіал з розробки ігор починається однаково: кольорові прямокутники, що ковзають по сірому полотну. Це підходить для вивчення синтаксису, але це нічого не говорить про те, як насправді «дихає» ігровий рушій. Я хотів вийти за межі малювання прямокутників. Я хотів створити щось, що відчувалося б як справжня гра — dungeon crawler з видом зверху з пересуванням, боєм, HUD та звуком.

Я обрав Phaser v4 і встановив собі одне суворе правило: жодних зовнішніх асетів. Жодних файлів зображень, жодних аудіокліпів і жодних інструментів збірки, таких як Webpack чи Vite. Весь проєкт мав існувати в межах одного HTML-файлу, написаного на чистому JavaScript. Це обмеження було не заради мінімалізму як такого. Воно було спрямоване на те, щоб усунути будь-які виправдання та будь-які «чорні скриньки». Коли ви не можете завантажити пакет спрайтів, щоб прикрити прогалини у своїх знаннях, ви змушені вивчати, як рушій керує текстурами, анімаціями, аудіо та станом «під капотом».

Малювання світу за допомогою коду

У звичайному проєкті Phaser ви викликаєте this.load.image() всередині функції preload і вказуєте рушію шлях до PNG-файлу. Без такої можливості ви звертаєтеся до об'єкта Graphics. Ви створюєте його екземпляр, малюєте примітивні фігури — прямокутники для плиток підлоги, товстіші лінії для стін, можливо, коло для фішки гравця — а потім викликаєте generateTexture. Цей метод захоплює буфер графіки та реєструє його в менеджері текстур Phaser під обраним вами ключем.

З цього моменту рушій сприймає цей згенерований бітмап точно так само, як завантажений файл зображення. Ви можете призначити його тайлмапам, розрізати на спрайти або змінити його колір (tint). Для dungeon crawler це означало, що я міг процедурно генерувати сітку підлоги, розставляти сегменти стін і змінювати кольорову палітру, ніколи не залишаючи редактора коду. Практичний урок полягає в тому, що текстура — це просто шматок бітмап-даних у пам'яті. Phaser байдуже, чи прийшла вона через HTTP-запит, чи через виклик Graphics, написаний вручну.

Такий підхід також змушує вас свідомо думати про порядок малювання та батчинг. Коли кожна стіна та плитка підлоги належать до одного сімейства згенерованих текстур, ви починаєте звертати увагу на те, як Phaser групує виклики рендерингу. Ви помічаєте різницю між статичним шаром тайлмапа та спрайтмапом окремих об'єктів, тому що ви вручну вирішуєте, що саме має існувати як екземпляр текстури.

Анімація без спрайтшитів

Статичні квадрати швидко набридають, але система анімації Phaser очікує спрайтшит — зазвичай це один PNG-файл із кадрами, розташованими в сітці. Я відтворив таку смужку за допомогою елемента offscreen HTML canvas. Для кожного кадру анімації я очищував canvas і малював нову позу: простий замах мечем, двокроковий цикл ходьби або легке погойдування ворога в стані спокою. Коли смужка була готова, я зареєстрував її у Phaser як спрайтшит, визначивши ширину та висоту кадру, щоб рушій знав, де закінчується одна поза і починається наступна.

Виконання цього вручну демонструє, чим насправді є анімація «під капотом»: набором меж кадрів на спільній текстурі. Компонент анімації Phaser запитує початковий кадр, кінцевий кадр і частоту кадрів. Потім він переміщує покажчик крізь ці прямокутні фрагменти на кожному такті ігрового годинника. Ви перестаєте сприймати спрайтшити як магічні асети, створені художниками, і починаєте бачити в них математику координат. Цей підхід є неоціненним пізніше, коли вам доведеться розрізати справжній арт або відлагоджувати, чому анімація відтворює кадри не по порядку.

Звук із нічого

Аудіофайли були під забороною, тому я використовував Web Audio API напряму. Кілька рядків JavaScript можуть створити вузол осцилятора, встановити його на квадратну або синусоїдальну хвилю, пропустити через вузол посилення і запланувати короткий звуковий імпульс. Я написав крихітні допоміжні функції для поширених подій: низькочастотний сигнал для кроків, висхідний звук для

A real game needs more than one screen, so I split the project into separate Phaser scenes: Menu, Game, UI, and Pause. The UI scene runs in parallel with the Game scene, launched simultaneously so the health bar and score counter live in their own sandbox while the dungeon crawls below. They communicate strictly through events. When the player takes damage, the Game scene emits a change. The UI scene listens and updates its text objects. The Game scene does not import the UI, call its methods, or even check if it exists. It simply sends data into the void. That decoupling means you can yank the HUD out for testing, or replace it entirely, without touching the core game loop.

For pausing, I used a stacked Pause scene that sits on top of the Game scene. Crucially, calling scene.pause() on the Game scene actually freezes the physics world and halts timers. The Game scene stops updating, but the Pause scene remains awake to render a menu and wait for an unpause signal. If you have only ever managed pause states with a boolean flag inside one giant update loop, this feels like discovering a light switch. The engine gives you a real pause lifecycle rather than forcing you to pepper your code with if (isPaused) return guards.

Hard Lessons from Real Bugs

Working this close to the metal exposed two habits I needed to change.

First, I tried to use a method in Phaser v4 that looked public but was not part of the documented API. It changed between versions and broke my build. I refactored to use getChildren(), which is a stable, documented public method, and the instability vanished. The lesson is blunt: if a method is not in the official documentation, do not build your game on it. Internal APIs are internal for a reason. Stick to the public surface area and your project will survive engine updates.

Second, I learned never to trust events for everything. Event callbacks work perfectly well for low-stakes interactions like picking up a coin or opening a chest. But for critical state transitions—especially Game Over—I added a redundant check inside the main update loop. Events can misfire if a listener is removed, a scene pauses at an awkward microsecond, or a race condition sneaks in between emission and handling. By checking the player’s health directly in the update loop and forcing the game-over state if it hits zero, I ensured the game could never get stuck in a limbo state if an event failed to fire. The events still handle the secondary effects—screen shake, sound cues, score submission—but the authoritative logic lives where the game clock lives.

Why You Should Try This

If you are learning game development, impose this exact constraint on your next project: no external assets, one HTML file. It sounds restrictive, but it removes every excuse. You do not need to configure a bundler, wrestle with CORS errors on local audio files, or spend an afternoon curating free asset packs. You write code, you refresh the browser, and you see results.

More importantly, you will understand why the engine behaves the way it does. You will know how a texture enters the GPU because you called generateTexture. You will know how animation frames are indexed because you registered the boundaries by hand. You will know how audio reaches the speakers because you wired the oscillator. That knowledge transfers directly to larger projects that do use external assets, because the underlying mechanics never change—the engine