שלום אורח התחבר

סקיצה לפיתוח משחק Snake ב React

נושאים:יומי

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

1הקוד החיצוני

מה שיפה במשחק סנייק זה שמאוד קל לראות ולבנות את הלוגיקה שלו: בגירסה הבסיסית של המשחק יש בסך הכל נחש אחד שצריך לתפוס תפוח אחד. בלי ריאקט אני יכול לכתוב את הלוגיקה של המשחק בפחות מ 50 שורות:

  1. הנחש שומר את המיקום והכיוון שלו
  2. התפוח שומר את המיקום שלו
  3. כל X זמן הנחש מתקדם צעד אחד: אם הוא פגע בתפוח הוא גדל, ואם הוא פגע בעצמו מתחילים מהתחלה.

מוזמנים לנסות לכתוב את הקוד בעצמכם. זה מה שיצא לי ושמרתי בקובץ בשם game/snake.js:

import _ from 'lodash';

function restart() {
    snake.size = 4;
    snake.pos = [[0, 0]];
    snake.direction = [1, 0];
    apple.pos = [_.random(-25, 25), _.random(-25, 25)];
}

export const snake = {
    pos: [[0, 0]],
    size: 4,
    direction: [1, 0],
}

export const apple = {
    pos: [_.random(-25, 25), _.random(-25, 25)]
}

function collides(pos1, pos2) {
    return pos1[0] === pos2[0] && pos1[1] === pos2[1];
}

export function tick() {
    if (collides(snake.pos[0], apple.pos)) {
        snake.size += 1;
        apple.pos = [_.random(-25, 25), _.random(-25, 25)];
    }

    const snakeHead = snake.pos[0];
    const nextHead = [snakeHead[0] + snake.direction[0], snakeHead[1] + snake.direction[1]];
    if (nextHead[0] > 25) { nextHead[0] = -25; }
    if (nextHead[0] < -25) { nextHead[0] = 25; }
    if (nextHead[1] > 25) { nextHead[1] = -25; } 
    if (nextHead[1] < -25) { nextHead[1] = 25; }

    if (snake.pos.filter(p => collides(p, nextHead)).length > 0) {
        // snake collides with itself
        return restart();
    }

    snake.pos.unshift(nextHead);
    if (snake.pos.length > snake.size) {
        snake.pos.pop();
    }
}

עיקר הלוגיקה קורה בפונקציה tick - שמקדמת את הנחש בצעד אחד ובודקת התנגשויות.

2חיבור הקוד החיצוני לריאקט

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

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

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

function translate(coord) {
    return `${coord[0] + 25},${coord[1] + 25}`;
}

function getAppleData(apple) {
    return new Set([translate(apple.pos)]);
}

function getSnakeData(snake) {
    return new Set(snake.pos.map(translate));
}

ופה צריך לשים לב שהרבה יותר קל לנו לבנות Set-ים חדשים ולהשתמש בהם בסטייט, מאשר לשמור Reference למידע החיצוני מהסטייט. מבחינת ריאקט כשהוא יקבל Set חדש הוא יפעיל מחדש Render ויבדוק איך השינוי בסטייט משפיע על הקומפוננטה, ובמקרה של משחק הנחש זה בדיוק מה שאנחנו רוצים.

עוד פונקציית עזר קטנה לפני הקומפוננטה תקבל מיקום וסטייט ותגיד מה הצבע של המשבצת במיקום שבחרנו:


function colorOf(row, col, snakeData, appleData) {
    if (snakeData.has(`${row},${col}`)) {
        return "blue";
    }

    if (appleData.has(`${row},${col}`)) {
        return "red";
    }

    return "transparent";
}

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

export default function Snake(props) {
    const [snakeData, setSnakeData] = useState(getSnakeData(snake));
    const [appleData, setAppleData] = useState(getAppleData(apple));
    useEffect(function() {
        const clock = setInterval(function() {
            tick();
            setSnakeData(getSnakeData(snake));
            setAppleData(getAppleData(apple));
        }, 200);

        return function() {
            clearInterval(clock);
        }
    }, [])

    return (
        _.range(50).map(row => (
            <div className="row" key={`row-${row}`}>
                {
                    _.range(50).map(col => (
                        <div
                            className="col"
                            style={{ background: colorOf(row, col, snakeData, appleData)}}
                            key={`col-${row}-${col}`}
                        />
                    ))
                }
            </div>
        ))
    )
}

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

האפקט מפעיל את tick כל 200 מילי שניות ואחרי זה מחשב מחדש את הסטייט כדי שממשק המשתמש יראה את תוצאת החישוב.

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

3שינוי כיוון עם החצים

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

window.addEventListener('keydown', function(ev) {
    if (ev.key === "ArrowDown") {
        snake.direction = [1, 0];
    }

    if (ev.key === "ArrowUp") {
        snake.direction = [-1, 0];
    }

    if (ev.key === "ArrowLeft") {
        snake.direction = [0, -1];
    }

    if (ev.key === "ArrowRight") {
        snake.direction = [0, 1];
    }
});

ואנחנו מוכנים למשחק.

4עכשיו אתם

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

אחרי שתסיימו תוכלו להשתולל - להוסיף עוד תפוחים או פירות אחרים, להוסיף עוד נחש כדי לאפשר משחק בשני שחקנים (או עוד 5 נחשים ל-5 שחקנים) ואפילו לאפשר לאנשים לשחק עם חברים ברשת.

נ.ב. ביום חמישי הקרוב אני מעביר כאן וובינר על בדיקות יישומי ריאקט ושם אראה לכם איך לבדוק את משחק הסנייק הזה, ותוכניות ריאקט באופן כללי. מוזמנים לקפוץ להגיד שלום. הרשמה בקישור: https://www.tocode.co.il/workshops/108.

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

או הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:


נהניתם מהפוסט? מוזמנים לשתף ולהגיב