כל מדריך לפיתוח משחקים מתחיל באותו אופן: מלבנים צבעוניים שגולשים על קנבס אפור. זה בסדר ללמידת תחביר, אבל זה לא מלמד אותך דבר על האופן שבו מנוע משחקים באמת "נושם". רציתי לצאת מעסק המלבנים. רציתי לבנות משהו שמרגיש כמו משחק אמיתי — dungeon crawler בפרספקטיבת top-down עם תנועה, קרב, HUD וסאונד.
התחייבתי ל-Phaser v4 והצבתי לעצמי כלל נוקשה אחד. אפס נכסים (assets) חיצוניים. ללא קבצי תמונה, ללא קטעי אודיו וללא כלי בנייה כמו Webpack או Vite. כל הפרויקט היה חייב להתקיים בתוך קובץ HTML בודד שנכתב ב-JavaScript פשוט. האילוץ הזה לא נועד למינימליזם לשם המינימליזם. הוא נועד להסיר כל תירוץ וכל "קופסה שחורה". כשאתה לא יכול להוריד חבילת ספריטים (sprite pack) כדי לכסות פער בידע שלך, אתה נאלץ ללמוד איך המנוע מנהל טקסטורות, אנימציות, אודיו ומצבים (state) מתחת למכסה המנוע.
ציור העולם מתוך קוד
בפרויקט Phaser רגיל, אתה קורא ל-this.load.image() בתוך פונקציית preload ומפנה את המנוע לקובץ PNG. ללא האפשרות הזו, אתה פונה לאובייקט ה-Graphics. אתה יוצר מופע שלו (instantiate), מצייר צורות פרימיטיביות — מלבנים עבור אריחי רצפה, קווים עבים יותר עבור קירות, אולי עיגול עבור סמל שחקן — ואז קורא ל-generateTexture. המתודה הזו לוכדת את ה-graphics buffer ורושמת אותו ב-texture manager של Phaser תחת מפתח שתבחר.
מרגע זה ואילך, המנוע מתייחס לביטמפ (bitmap) שנוצר בדיוק כמו לקובץ תמונה שנטען. אתה יכול להקצות אותו ל-tilemaps, לחתוך אותו לספריטים, או לצבוע אותו (tint). עבור ה-dungeon crawler, זה אמר שיכולתי לייצר רשת רצפה באופן פרוצדורלי, להטביע מקטעי קירות, ולשנות את פלטת הצבעים מבלי לעזוב מעולם את עורך הקוד שלי. השיעור המעשי הוא שטקסטורה היא בסך הכל מקטע של נתוני ביטמפ היושב בזיכרון. ל-Phaser לא אכפת אם הוא הגיע באמצעות בקשת HTTP או באמצעות קריאה ל-Graphics שנכתבה ידנית.
הגישה הזו גם גורמת לך לחשוב באופן מודע על סדר הציור (draw order) ועל batching. כשכל קיר ואריח רצפה מגיעים מאותה משפחה של טקסטורות שנוצרו, אתה מתחיל לשים לב לאופן שבו Phaser מקבץ קריאות רינדור (render calls). אתה מבחין בהבדל בין שכבת tilemap סטטית לבין spritemap של אובייקטים בודדים, כי אתה מחליט ידנית מה ראוי להתקיים כמופע טקסטורה.
אנימציה ללא spritesheets
ריבועים סטטיים הופכים למשעממים מהר מאוד, אבל מערכת האנימציה של Phaser מצפה ל-spritesheet — בדרך כלל PNG בודד עם פריימים המסודרים בתוך גריד. שחזרתי את הרצועה הזו באמצעות אלמנט HTML canvas מחוץ למסך (offscreen). עבור כל פריים של אנימציה, ניקיתי את הקנבס וציירתי תנוחה חדשה: תנועת חרב פשוטה, מחזור הליכה של שני צעדים, או תנועת המתנה (idle) של אויב. ברגע שהרצועה הושלמה, רשמתי אותה ב-Phaser כ-spritesheet, תוך הגדרת רוחב וגובה הפריים כדי שהמנוע ידע היכן תנוחה אחת מסתיימת והבאה מתחילה.
ביצוע פעולה זו באופן ידני חושף בדיוק מהי אנימציה מתחת לפני השטח: סט של גבולות פריימים על טקסטורה משותפת. רכיב האנימציה של Phaser מבקש פריים התחלה, פריים סיום וקצב פריימים (frame rate). לאחר מכן הוא מקדם מצביע דרך הפרוסות המלבניות הללו בכל "טיק" של שעון המשחק. אתה מפסיק לחשוב על spritesheets כנכסים קסמים שנוצרו על ידי אמנים ומתחיל לראות בהם מתמטיקה של קואורדינטות. הפרספקטיבה הזו היא יקרת ערך מאוחר יותר כשאתה חותך אמנות אמיתית או מנקה באגים (debugging) מדוע אנימציה מציגה פריימים מחוץ לסדר.
סאונד מכלום
קבצי אודיו היו אסורים, אז השתמשתי ב-Web Audio API ישירות. כמה שורות של JavaScript יכולות ליצור oscillator node, להגדיר אותו כגל ריבוע (square wave) או גל סינוס (sine wave), להעביר אותו דרך gain node, ולתזמן פרץ קצר של צליל. כתבתי פונקציות עזר קטנות לאירועים נפוצים: צפצוף בתדר נמוך עבור צעדים, ציוץ עולה עבור איסוף שלל, וטון גל-ריבוע חריף עבור קבלת נזק.
הצלילים המסונתזים הללו הם placeholders מראש, אך הם נותנים למשחק משוב מכני באופן מיידי. אתה יכול להרגיש אם התזמון נכון לפני שאתה מתחייב להקלטה או להשגת אודיו אמיתי. התמורה האמיתית מגיעה בארכיטקטורה. מכיוון שעטפת את טריגר הסאונד בפונקציה פשוטה, החלפת הצפצוף המסונטז ב-sound buffer טעון מאוחר יותר דורשת שינוי של שורה אחת בלבד. שאר המשחק — אירוע ההתנגשות, ההבהוב של ה-UI, הוספת הניקוד — נשאר ללא שינוי. אתה בונה אבטיפוס (prototype) של התחושה תחילה, ואז משדרג את האיכות.
ניהול עולמות מרובים
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
