header-banner-image
23/01/2020 13:45

איך לפתח משחק בעצמכם ב-15 דקות!

ב: פיתוח משחקים , מדריכים
נכתב ע''י: רון מרון

בשנים האחרונות עולם הפיתוח של משחקי דפדפנים הולך ומתעצם. מה שהתחיל כתכנות פשוט באובייקט canvas של HTML5 שקיים בכלל הדפדפנים הפך לספריה ענקית של שלל תוספים (הידועים לנו עם הסיומות js) שמאפשרים לנו לפתח משחקים ופעילויות גרפיות בצורה מאוד פשוטה ומהירה.

במדריך זה נתמקד בכלי בשם Phaser (Phaser 3 ליתר דיוק) שמאפשר פיתוח מהיר ופשוט של תוכנות גרפיות ומשחקים באמצעות canvas.

 

דרישות בסיסיות

  • יש צורך בידע מאוד מאוד בסיסי ב-Javascript כדי להבין את קטעי הקוד שיופיעו בכתבה זו.
  • יש להוריד את הספרייה מכאן (או להשתמש בקבצי הספרייה המאוחסנים ב-CDN).

 

מה זה בעצם Phaser?

Phaser היא ספריית HTML5 לפיתוח משחקי דפדפן בצורה קלה ויעילה. הספרייה נוצרה במיוחד כדי לרתום את היתרונות של הדפדפנים המודרניים, הן למחשבים שולחניים/ניידים והן לסמארטפונים.

מבחינת הדפדפן, הדרישה היחידה היא תמיכה בתגית canvas.

 

מה נבנה במדריך זה?

 

 

 

במדריך זה נפתח משחק ברייקאאוט פשוט עם המאפיינים הבאים:

  • שלב אחד עם 45 מלבנים, כדור ומשוט.
  • המטרה של המשחק היא להרוס כמה שיותר מלבנים בלי שהכדור ייפול לתחתית המסך.
  • השליטה במשוט באמצעות העכבר (תזוזה על הציר האופקי בלבד).
  • הניקוד של השחקן יופיע בצד שמאל למעלה כאשר כל צבע של מלבן מזכה את השחקן בניקוד שונה:
    • כחול: 10 נקודות.
    • ירוק: 20 נקודות.
    • סגול: 30 נקודות.
    • צהוב: 40 נקודות.
    • אדום: 50 נקודות.

 

כאן תוכלו לשחק במשחק עצמו:

 

 

 

 

התקנת הסביבה

הסביבה צריכה להיות מורכבת מהקבצים הבאים:

  • קובץ index.html שיכיל את הלוגיקה של המשחק בנוסף ל-html.
  • תיקייה בשם assets שתכיל את כל התמונות של המשחק.

הוסיפו את הקוד הבא ל-index.html:

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Phaser Breakout Tutorial</title>

    <script src="https://cdn.jsdelivr.net/npm/phaser@3.15.1/dist/phaser-arcade-physics.min.js"></script>
</head>

<body>
    <script></script>
</body>

</html>

 

בנוסף יש להוסיף את הסקריפט שמייבא את ספריית פייזר (ניתן גם מה-CDN כמו בתמונה מעלה)

וכעת ניתן להתחיל לעבוד על המשחק.

 

 

פיתוח המשחק

נתחיל בלפתוח תגית <script> בתוך ה-body. בתוך התגית נטפל בקונפיגורציה של המשחק:

 

var config = {
    type: Phaser.AUTO,
    width: 800,
    height: 640,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: false,
            //debug: true
        },
    },
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

 

להלן הסבר קצר על האלמנטים השונים באובייקט config:

  • type: כאן פייזר בעצם מגדיר את המנוע שאיתו הוא יעבוד. יש לערך 3 אפשרויות:
    • Phaser.AUTO
    • Phaser.CANVAS
    • Phaser.WEBGL
    בדרך כלל משתמשים ב-AUTO ואז הספרייה מחליטה לבד האם ומתי להשתמש ב-CANVAS ומתי ב-WEBGL לפי הצורך, על מנת לעשות שימוש מיטבי במשאבי המחשב או המכשיר.
  • width: מגדיר את רוחב החלונית של המשחק.
  • height: מגדיר את אורך החלונית של המשחק.
  • physics: הגדרות פיזיקליות בסיסיות למשחק. למשל, במשחק זה אין צורך באלמנט של גרביטציה.
  • scene: רשימת ה"מצבים" שיש לנו במשחק והפניות לפונקציות שלהן.

שימו לב שניתן לאפשר את השורה debug: true בשלב הפיתוח של המשחק (בעיקר במשחק כמו ברייקאאוט, שכל המהות שלו זה התנגשויות). אופציה זו מאפשרת לנו לראות את תחומי ה-collisions של כל אלמנט במשחק בצורה קלה ונוחה באמצעות מלבנים בצבעים שונים.

 

נגדיר כמה משתנים גלובליים למשחק שלנו:

 

var game = new Phaser.Game(config);
var gameStarted = false;
var score = 0;
var scoreText;
var gameWon = false;

 

  • game: אובייקט המשחק שלנו שמקבל את אובייקט הקונפיגורציה שהגדרנו בסעיף הקודם.
  • gameStarted: משתנה בוליאני אשר מתריע על תחילת משחק.
  • score: הניקוד של השחקן.
  • scoreText: משתנה שיכיל את הטקסט עצמו של הניקוד.
  • gameWon: משתנה בוליאני אשר מתריע על סיום וניצחון של המשחק.

 

כעת נעבור לפונקציות המרכזיות של ספריית פייזר - create, preload ו-update.

נתחיל בפונקציית preload:

 

function preload() {
    this.load.image('ball', './assets/ball.png');
    this.load.image('paddle', './assets/paddle.png');
    this.load.image('brick1', './assets/brick1.png');
    this.load.image('brick2', './assets/brick2.png');
    this.load.image('brick3', './assets/brick3.png');
    this.load.image('brick4', './assets/brick4.png');
    this.load.image('brick5', './assets/brick5.png');
}

 

פונקציית ה-preload שלנו בעצם טוענת את כל הרכיבים (במקרה שלנו מדובר רק בתמונות) הנחוצים כדי שהמשחק יעבוד.

מבחינת ההיררכיה, פונקציה זו היא הראשונה שנקראית ע"י פייזר עוד לפני שהמשחק בכלל התחיל.

ניתן להוריד את התמונות של המשחק בלינק הבא.

 

פונקציית create:

פונקציה זו אחראית לאתחל את כל האובייקטים שיש לנו במשחק (משוט, כדור והמלבנים).

 

function create() {
    this.player = this.physics.add.sprite(0, 610, 'paddle').setScale(0.2);
    this.ball = this.physics.add.sprite(0, 575, 'ball').setScale(0.2);
}

 

נתחיל ביצירת השחקן והכדור. נשתמש בפונקציה add.sprite שתחת האובייקט physics כדי להגדיר את המיקום של אותה תמונה, וגם את שם הרכיב לפי המפתח שהגדרנו בפונקציית preload.

צורת טעינה זו מאפשרת לנו בעצם להשתמש באותה תמונה בכמה מקומות בקוד בלי להגדיר בכל פעם את הנתיב מחדש (רק צריך לזכור את המפתח שהגדרנו בפונקציית preload – לדוגמא: ‘paddle’, ‘ball’).

אופציונאלי – בנוסף, נקטין את התמונות באמצעות setScale(0.2) כי התמונות עצמן די גדולות.

 

לאחר שיצרנו את השחקן ואת הכדור, הגיע הזמן לעבור למלבנים. יש להוסיף את הקוד הבא לפונקציית create:

 

this.blueBricks = createBricksGroup('brick1', 170, this);
this.greenBricks = createBricksGroup('brick2', 140, this);
this.purpleBricks = createBricksGroup('brick3', 110, this);
this.yellowBricks = createBricksGroup('brick4', 80, this);
this.redBricks = createBricksGroup('brick5', 50, this);

 

נחלק את המלבנים לפי צבעים ובעצם לכל צבע תהיה "קבוצה" משלה שתכיל את כל המלבנים המשויכים אליה.

עבור כל "קבוצה", נשתמש בפונקציה createBricksGroup עליה נרחיב בהמשך כדי ליצור את קבוצות המלבנים.

 

פונקציית createBricksGroup:

 

function createBricksGroup(name, y, scene) {
    return scene.physics.add.group({
        key: name,
        repeat: 8,
        immovable: true,
        setXY: {
            x: 80,
            y: y,
            stepX: 80
        },
        setScale: { x: 0.2, y: 0.2 }
    });
}

 

פונקציה זו אחראית ליצור לנו קבוצה חדשה של מלבנים כאשר היא מקבלת שלושה משתנים:

  • name: השם של הרכיב שאנחנו רוצים לטעון (לפי מה שהגדרנו בפונקציית preload).
  • y: המיקום האנכי של הקבוצה (זהו הרכיב היחיד שמתשנה בין קבוצה לקבוצה).
  • scene: אובייקט ה-scene של פייזר אליו אנחנו משייכים את הקבוצה.

והיא תחזיר לנו אובייקט מסוג "group" חדש.

 

נשתמש בפונקציה add.group כדי להוסיף קבוצה חדשה של מלבנים, כאשר נגדיר לקבוצה זו את המאפיינים הבאים:

  • key: מילת המפתח של אותו רכיב שהגדרנו בפונקציית preload.
  • repeat: מספר הפעמים שנרצה להעתיק את תמונה זו שוב לתוך הקבוצה בנוסף לתמונה המקורית (כאן אנחנו רוצים 9 מלבנים בשורה ולכן נכתוב 8).
  • immovable: אובייקט זה הוא אובייקט פיזיקאלי ולכן במצב הדיפולטיבי כאשר הכדור יפגע בו יופעל כוח על המלבן והוא יזוז. כאן אנחנו מגדירים שאנחנו לא רוצים שהמלבן יזוז.
  • אובייקט setXY:
    • x: מיקום התמונה על הציר האופקי.
    • y: מיקום התמונה על הציר האנכי.
    • stepX: כאן אנחנו בעצם מגדירים את המרחק האופקי בין כל התמונות המועתקות (שהגדרנו ב-repeat) כדי שהן לא יעלו אחת על השנייה.
  • setScale: כמו שעשינו לשחקן ולכדור, גם כאן נרצה להקטין את התמונות של המלבנים.

 

הצעד הבא הוא לטפל בנושא של ההתנגשויות - כלומר כשהכדור פוגע בשחקן או במלבנים או בגבולות המסך. נוסיף לפונקציית create:

 

this.ball.setCollideWorldBounds(true);
this.ball.setBounce(1, 1);
this.physics.world.checkCollision.down = false;

this.physics.add.collider(this.ball, this.blueBricks, brickCollision, null, this);
this.physics.add.collider(this.ball, this.greenBricks, brickCollision, null, this);
this.physics.add.collider(this.ball, this.purpleBricks, brickCollision, null, this);
this.physics.add.collider(this.ball, this.yellowBricks, brickCollision, null, this);
this.physics.add.collider(this.ball, this.redBricks, brickCollision, null, this);

this.player.setImmovable(true);
this.physics.add.collider(this.ball, this.player, playerCollision, null, this);

 

בשורות הראשונות אנחנו בעצם מגדירים שהכדור לא יוכל לצאת מגבולות העולם שלנו (שזה בעצם האורך והרוחב שהגדרנו למשחק) ובנוסף מורידים את בדיקת ההתנגשות עבור הגבול של תחתית המסך (כדי שהשחקן יוכל להפסיד במקרה והכדור יעבור את הגבול).

הפונקציה setBounce מגדירה שהכדור תמיד ישמור על המהירות בציר האופקי ובציר האנכי וכך בעצם הכדור יקפוץ בחזרה כאשר הוא מתנגש עם גבולות המסך.

לאחר מכן אנחנו מוסיפים קריאות לפונקציה add.collider (עבור כל אחד מקבוצות המלבנים), פונקציה מובנית של פייזר, אשר עושה בדיקת התנגשות אוטומטית בין 2 אובייקטים. הפרמטר הראשון הוא בעצם האובייקט אשר מתנגש, הפרמטר השני הוא האובייקט שמתנגשים בו והפרמטר השלישי הוא פונקציית ה-callback שתופעל כאשר זה קורה.

בסוף, מוסיפים עוד פונקציה אחת שאחראית לבדוק התנגשות בין הכדור למשוט ומגדירים שהמשוט לא יזוז כאשר הכדור יפגע בו (באותה צורה שהגדרנו עבור קבוצות המלבנים עם הפרמטר immovable).

 

כעת נסביר על פונקציות ההתנגשות בין הכדור לשחקן ובין הכדור למלבנים.

 

הפונקציה הבאה תפעל כאשר תתרחש התנגשות בין הכדור למשוט:

 

function playerCollision(ball, player) {
    var velX = Math.abs(ball.body.velocity.x);

    if (ball.x < player.x) {
        ball.setVelocityX(-velX);
    } else {
        ball.setVelocityX(velX);
    }
}

 

היא בעצם עושה בדיקה האם הכדור נמצא בצד השמאלי או הימני של המשוט כאשר הוא פוגע בו – ולפי זה אנחנו יודעים לקבוע לאיזה כיוון הכדור אמור לעוף (נגדי או באותו כיוון) באמצעות הפיכת הסימן של המהירות שלו.

 

הפונקציה הבאה תפעל כאשר תהיה התנגשות בין הכדור לאחד המלבנים:

 

function brickCollision(ball, brick) {
    brick.destroy();
    addScore(brick);

    if (ball.body.velocity.x === 0) {
        if (ball.x < brick.x) {
            ball.body.setVelocityX(-130);
        } else {
            ball.body.setVelocityX(130);
        }
    }

    if (isWon(this.blueBricks, this.greenBricks, this.purpleBricks, this.yellowBricks, this.redBricks)) {
        this.wonText.setVisible(true);
        this.ball.destroy();
        this.player.destroy();
        gameWon = true;
    }
}

 

קורים כאן כמה דברים מעניינים:

  • לפני הכל, אנחנו צריכים להעלים מהמסך את המלבן שהכדור פגע בו.
  • קריאה לפונקציה שמוסיפה ניקוד לשחקן לפי אותו מלבן שהצליח להרוס (כמו שאמרנו בהתחלה – לכל מלבן יהיה ניקוד שונה).
  • הבדיקה המרכזית של פונקציה זו אחראית לבדוק לאיזה כיוון לשלוח את הכדור כאשר הוא הפסיק לנוע בעקבות ההתנגשות עם המלבן (שוב – כמו עם השחקן, בודקים באיזה צד של המלבן הכדור פגע ולפי זה אנחנו יודעים לאיזה כיוון לשלוח אותו).
  • בחלק האחרון, אנחנו בודקים באמצעות פונקציית isWon, האם זהו המלבן האחרון שהשחקן הרס – ואז אפשר להפעיל את לוגיקת הזכייה של המשחק שנפרט עליו בהמשך.
    כאשר זהו המלבן האחרון, נציג טקסט ניצחון לשחקן ונעלים את הכדור ואת המשוט מהמסך.

 

כעת נרחיב על הפונקציות שראינו כאן – פונקציית addScore ו-isWon.

 

זוהי הפונקציה addScore:

 

function addScore(brick) {
    switch (brick.texture.key) {
        case "brick1":
            score += 10;
            break;
        case "brick2":
            score += 20;
            break;
        case "brick3":
            score += 30;
            break;
        case "brick4":
            score += 40;
            break;
        case "brick5":
            score += 50;
            break;
    }
    scoreText.setText('Score: ' + score);
}

 

מטרת הפונקציה – לבדוק באיזה מלבן השחקן פגע ולעדכן את הניקוד בהתאם.

אנחנו בעצם בודקים באמצעות switch case מה המפתח של המלבן שפגענו בו (כדי לדעת את הצבע) ואז לפי זה קובעים ניקוד מתאים לכל צבע של מלבן.

בסוף, מעדכנים את הטקסט על המסך בניקוד העדכני.

 

וזו הפונקציה isWon:

 

function isWon(blueBricks, greenBricks, purpleBricks, yellowBricks, redBricks) {
    return (
        blueBricks.countActive() === 0 &&
        greenBricks.countActive() === 0 &&
        purpleBricks.countActive() === 0 &&
        yellowBricks.countActive() === 0 &&
        redBricks.countActive() === 0
    );
}

 

פונקציה זו עושה בדיקה פשוטה כדי לבדוק האם נשארו מלבנים על המסך באמצעות השוואה של ספירת כל קבוצת מלבנים ל- 0. במידה וכן מחזירה false, ובמידה ולא – true.

 

 

נחזור ונמשיך עם פונקציית ה-create של פייזר. נוסיף אליה את הקוד הבא, בו מגדירים טקסט חדש שישב בדיוק במרכז המסך כדי להתריע לשחקן שכדי לשחק הוא צריך להקליק על העכבר:

 

this.startText = this.add.text(
    this.physics.world.bounds.width / 2,
    this.physics.world.bounds.height / 2,
    'Click to Play',
    {
        fontFamily: 'Arial',
        fontSize: '50px',
        fill: '#fff'
    }
);
this.startText.setOrigin(0.5);

 

כמו טקסט האתחול, נוסיף טקסט לסיום משחק בכשלון. גם הוא ישב במרכז המשחק כאשר בעליית המשחק לא יראו אותו (באמצעות שימוש ב-setVisible):

 

this.gameOverText = this.add.text(
    this.physics.world.bounds.width / 2,
    this.physics.world.bounds.height / 2,
    'Game Over!',
    {
        fontFamily: 'Arial',
        fontSize: '50px',
        fill: '#fff'
    }
);
this.gameOverText.setOrigin(0.5);
this.gameOverText.setVisible(false);

 

ובאותה הדרך, נוסיף גם טקסט לסיום משחק בהצלחה:

 

this.winText = this.add.text(
    this.physics.world.bounds.width / 2,
    this.physics.world.bounds.height / 2,
    'You Win!',
    {
        fontFamily: 'Arial',
        fontSize: '50px',
        fill: '#fff'
    }
);
this.winText.setOrigin(0.5);
this.winText.setVisible(false);

 

ולבסוף, נגדיר את טקסט הניקוד שיופיע בפינה השמאלית למעלה של המשחק:

 

scoreText = this.add.text(
    10,
    10,
    'Score: 0',
    {
        fontFamily: 'Arial',
        fontSize: '18px',
        fill: '#fff'
    }
);

 

 

כעת נעבור לפונקציית ה-update, שמכילה את הלוגיקה של המשחק:

 

function update() {
    if (!gameWon) {
        if (isGameOver(this.physics.world, this.ball)) {
            this.gameOverText.setVisible(true);
            this.ball.destroy()
            this.player.destroy();
        } else {
            this.player.setX(this.input.x);
            if (!gameStarted) {
                this.ball.setX(this.player.x);

                if (this.input.activePointer.isDown) {
                    gameStarted = true;
                    this.ball.setVelocityY(-300);
                    this.startText.setVisible(false);
                }
            }
        }
    }
}

  

אנחנו קודם כל בודקים האם ניצחנו כבר את המשחק באמצעות משתנה בוליאני – אם כן, אין טעם להמשיך עם לוגיקת המשחק.

לאחר מכן בודקים האם השחקן נפסל באמצעות פונקציית isGameOver (שנרחיב עליה בסעיף הבא).

אם כן – נציג לו את טקסט ההפסד שהכנו בפונקציית create.

אם לא – נתחיל במהלך המשחק:

  • כדי שהמשוט תמיד יעקוב אחרי הסמן של השחקן, נקבע שהרכיב האופקי שלו יהיה שווה לרכיב האופקי של הסמן.
  • כדי שהכדור תמיד יהיה צמוד למשוט לפני שהתחלנו לשחק, נקבע שהרכיב האופקי שלו יהיה שווה לרכיב האופקי של המשוט.
  • נבדוק האם השחקן הקליק על העכבר.
    אם כן – נזיז את הכדור באמצעות מהירות אנכית של -300 (כלפי מעלה) ונעלים את טקסט ההתחלה.

 

ולבסוף, להלן פונקציית isGameOver:

 

function isGameOver(world, ball) {
    return ball.body ? ball.body.y > world.bounds.height : true;
}

 

הפונקציה פשוט בודקת האם הכדור עבור את הגבול התחתון של המסך כדי לקבוע אם השחקן הפסיד.

אנחנו גם עושים בדיקה עבור ball.body, במקרה וקראנו לפונקציה ball.destroy() שיחזיר ערך undefined ב-ball.body במקרה הזה.

 

 

איך להמשיך מכאן?

למי שמעוניין, ניתן לשפר את המשחק באופן הבא (נסו זאת בעצמכם!):

 

מתחילים:

  • הצגת הניקוד של השחקן מתחת לטקסט You Win! כאשר המשחק נגמר.
  • הגברת מהירות הכדור בכל פעם שהיא פוגעת במשוט.
  • כשפוגעים במלבן בפעם הראשונה הוא לא נהרס – רק בפעם השנייה -  כאשר התמונה משתנה למלבן "שבור". (אולי אפילו שמספר הפעמים עד שהוא נשבר יהיה תלוי צבעי המלבנים).
  • הוספת תמונת רקע למשחק.
  • הוספת סאונד למשחק (פגיעה של הכדור באלמנטים, מוזיקת רקע, וכו').

 

מתקדמים:

  • הוספת שלבים למשחק עם מלבנים בצבעים שונים ובצורות שונות.
  • הוספת leaderboard של שמות השחקנים עם הניקוד הגבוה ביותר.
  • הוספת בונוסים שנופלים כששוברים מלבנים כמו יריות, מהירות כדור, הגדלת רוחב המשוט.
  • שינוי זווית תנועת הכדור כתלות במיקום בו פגע הכדור במשוט.

 


הקוד המלא של המשחק

 

ניתן להוריד את המשחק כולל כל התיקיות הרלוונטיות מכאן, או להעתיק את הקוד מכאן:

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Phaser Breakout Tutorial</title>

    <script src="https://cdn.jsdelivr.net/npm/phaser@3.15.1/dist/phaser-arcade-physics.min.js"></script>
</head>

<body>
    <script>
        var config = {
            type: Phaser.AUTO,
            width: 800,
            height: 640,
            physics: {
                default: 'arcade',
                arcade: {
                    gravity: false,
                    //debug: true
                },
            },
            scene: {
                preload: preload,
                create: create,
                update: update
            }
        };

        var game = new Phaser.Game(config);
        var gameStarted = false;
        var score = 0;
        var scoreText;
        var gameWon = false;

        function preload() {
            this.load.image('ball', './assets/ball.png');
            this.load.image('paddle', './assets/paddle.png');
            this.load.image('brick1', './assets/brick1.png');
            this.load.image('brick2', './assets/brick2.png');
            this.load.image('brick3', './assets/brick3.png');
            this.load.image('brick4', './assets/brick4.png');
            this.load.image('brick5', './assets/brick5.png');
        }

        function create() {
            this.player = this.physics.add.sprite(0, 610, 'paddle').setScale(0.2);
            this.ball = this.physics.add.sprite(0, 575, 'ball').setScale(0.2);

            this.blueBricks = createBricksGroup('brick1', 170, this);
            this.greenBricks = createBricksGroup('brick2', 140, this);
            this.purpleBricks = createBricksGroup('brick3', 110, this);
            this.yellowBricks = createBricksGroup('brick4', 80, this);
            this.redBricks = createBricksGroup('brick5', 50, this);

            this.ball.setCollideWorldBounds(true);
            this.ball.setBounce(1, 1);
            this.physics.world.checkCollision.down = false;

            this.physics.add.collider(this.ball, this.blueBricks, brickCollision, null, this);
            this.physics.add.collider(this.ball, this.greenBricks, brickCollision, null, this);
            this.physics.add.collider(this.ball, this.purpleBricks, brickCollision, null, this);
            this.physics.add.collider(this.ball, this.yellowBricks, brickCollision, null, this);
            this.physics.add.collider(this.ball, this.redBricks, brickCollision, null, this);

            this.player.setImmovable(true);
            this.physics.add.collider(this.ball, this.player, playerCollision, null, this);

            this.startText = this.add.text(
                this.physics.world.bounds.width / 2,
                this.physics.world.bounds.height / 2,
                'Click to Play',
                {
                    fontFamily: 'Arial',
                    fontSize: '50px',
                    fill: '#fff'
                }
            );
            this.startText.setOrigin(0.5);

            this.gameOverText = this.add.text(
                this.physics.world.bounds.width / 2,
                this.physics.world.bounds.height / 2,
                'Game Over!',
                {
                    fontFamily: 'Arial',
                    fontSize: '50px',
                    fill: '#fff'
                }
            );
            this.gameOverText.setOrigin(0.5);
            this.gameOverText.setVisible(false);

            this.winText = this.add.text(
                this.physics.world.bounds.width / 2,
                this.physics.world.bounds.height / 2,
                'You Win!',
                {
                    fontFamily: 'Arial',
                    fontSize: '50px',
                    fill: '#fff'
                }
            );
            this.winText.setOrigin(0.5);
            this.winText.setVisible(false);

            scoreText = this.add.text(
                10,
                10,
                'Score: 0',
                {
                    fontFamily: 'Arial',
                    fontSize: '18px',
                    fill: '#fff'
                }
            );
        }

        function createBricksGroup(name, y, scene) {
            return scene.physics.add.group({
                key: name,
                repeat: 8,
                immovable: true,
                setXY: {
                    x: 80,
                    y: y,
                    stepX: 80
                },
                setScale: { x: 0.2, y: 0.2 }
            });
        }

        function update() {
            if (!gameWon) {
                if (isGameOver(this.physics.world, this.ball)) {
                    this.gameOverText.setVisible(true);
                    this.ball.destroy()
                    this.player.destroy();
                } else {
                    this.player.setX(this.input.x);
                    if (!gameStarted) {
                        this.ball.setX(this.player.x);

                        if (this.input.activePointer.isDown) {
                            gameStarted = true;
                            this.ball.setVelocityY(-300);
                            this.startText.setVisible(false);
                        }
                    }
                }
            }
        }

        function isGameOver(world, ball) {
            return ball.body ? ball.body.y > world.bounds.height : true;
        }

        function isWon(blueBricks, greenBricks, purpleBricks, yellowBricks, redBricks) {
            return (
                blueBricks.countActive() === 0 &&
                greenBricks.countActive() === 0 &&
                purpleBricks.countActive() === 0 &&
                yellowBricks.countActive() === 0 &&
                redBricks.countActive() === 0
            );
        }

        function brickCollision(ball, brick) {
            brick.destroy();
            addScore(brick);

            if (ball.body.velocity.x === 0) {
                if (ball.x < brick.x) {
                    ball.body.setVelocityX(-130);
                } else {
                    ball.body.setVelocityX(130);
                }
            }

            if (isWon(this.blueBricks, this.greenBricks, this.purpleBricks, this.yellowBricks, this.redBricks)) {
                this.winText.setVisible(true);
                this.ball.destroy();
                this.player.destroy();
                gameWon = true;
            }
        }

        function playerCollision(ball, player) {
            var velX = Math.abs(ball.body.velocity.x);

            if (ball.x < player.x) {
                ball.setVelocityX(-velX);
            } else {
                ball.setVelocityX(velX);
            }
        }

        function addScore(brick) {
            switch (brick.texture.key) {
                case "brick1":
                    score += 10;
                    break;
                case "brick2":
                    score += 20;
                    break;
                case "brick3":
                    score += 30;
                    break;
                case "brick4":
                    score += 40;
                    break;
                case "brick5":
                    score += 50;
                    break;
            }
            scoreText.setText('Score: ' + score);
        }
    </script>
</body>

</html>