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

12/10/2021

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

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.