• בלוג
  • בואו נכתוב משחק סיימון ב React כדי ללמוד לעבוד עם שעונים

בואו נכתוב משחק סיימון ב React כדי ללמוד לעבוד עם שעונים

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

1. מהו משחק סיימון

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

2. הצגת הממשק

בגלל שאנחנו ב Web נוכל לייצג כל כפתור בתור div וסך הכל נציג 4 מהם על המסך. דיב שיש לו את הקלאס active יוצג בצבע שלו, ואלה בלי הקלאס active יוצגו שקופים. כשמשתמש ילחץ על div נוכל להוסיף את הקלאס active לכמה מאיות שניה כדי להראות אפקט של לחיצה, וכשמציגים את רצף האורות פשוט נוסיף ונוריד את הקלאס active ל div-ים לפי הסדר.

קוד ה CSS יספיק בשביל הממשק:

.colored-box {
  width: 100px;
  height: 100px;
  vertical-align: top;
  border: 1px solid;
  display: inline-block;
  line-height: 100px;
  text-align: center;
  font-size: 1.5rem;
}

.red {
  background: red;
}
.yellow {
  background: yellow;
}
.blue {
  background: blue;
}
.green {
  background: green;
}

.colored-box:not(.active) {
  background: transparent;
}

ואיתו הקוד הבא עבור ה React:

import React, { useState } from "react";

import "./styles.css";

const colors = ["red", "blue", "green", "yellow"];

function randomSequence() {
  return [1, 2, 1, 1];
}

function Game(props) {
  const [currentSequence, setCurrentSequence] = useState(randomSequence());
  const [index, setIndex] = useState(0);

  function cx(squareIndex) {
    return `colored-box ${colors[squareIndex - 1]} ${
      index === squareIndex ? "active" : ""
    } `;
  }

  return (
    <div className="game">
      <div className={cx(1)}>1</div>
      <div className={cx(2)}>2</div>
      <div className={cx(3)}>3</div>
      <div className={cx(4)}>4</div>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Game />
    </div>
  );
}

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

אפשר לראות שבשביל להתחיל את המשחק אני לא באמת צריך רצף אקראי ולכן הפונקציה randomSequence מחזירה מערך קבוע. אחרי שהכל יעבוד אוכל להחליף אותה לפונקציה שמייצרת מערך אקראי.

אפשר לראות את הקוד לייב בקודסנדבוקס בקישור הבא: https://codesandbox.io/s/delicate-darkness-crrq1?file=/src/App.js

3. מימוש הצגת רצף צבעים

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

  1. אני מגדיר משתנה סטייט בשם index שמייצג את האינדקס ברצף שעכשיו צריך להציג.

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

כדי שהמימוש יראה טוב נרצה להוסיף עוד כמה טריקים:

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

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

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

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

  useEffect(() => {
    if (index >= secretSequence.length) return;

    const timer = setTimeout(
      () => {
        setIndex((i) => i + 1);
      },
      index % 2 === 0 ? 600 : 200
    );

    return () => {
      clearTimeout(timer);
    };
  }, [index, secretSequence]);

  function cx(squareIndex) {
    return `colored-box ${colors[squareIndex - 1]} ${
      secretSequence[index] === squareIndex ? "active" : ""
    } `;
  }

ואת קוד המשחק המלא עם האפקט שמציג את רצף הצבעים אפשר לראות בקודפן הבא: https://codesandbox.io/s/elated-minsky-3uuko?file=/src/App.js:362-799

4. קליטת רצף צבעים מהמשתמש

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

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

כל פעם שמשתמש לוחץ על אחד ה div-ים אני מוסיף את הערך המתאים של ה div ל currentSequence.

ובשביל שמשתמשים ידעו שהלחיצה נקלטה הוספתי מנגנון flash קצר של הכפתורים: משתנה State בשם flash מייצג את הערך של הכפתור שעכשיו צריך להציג אור לזמן קצר; כל פעם שמעדכנים אותו מתחיל useEffect שסופר 200 מילי שניות עד לכיבוי אוטומטי ועדכון הערך ל-0.

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

import React, { useState, useEffect } from "react";
import _ from "lodash";

import "./styles.css";

const colors = ["red", "blue", "green", "yellow"];
const sequenceLength = 4;

function randomSequence(len) {
  const seq = new Array(len)
    .fill(0)
    .map((x) => Math.floor(Math.random() * colors.length) + 1);
  const zeros = new Array(len).fill(0);

  return _.flatMap(_.zip(seq, zeros));
}

function Game(props) {
  const [secretSequence, setSecretSequence] = useState(
    randomSequence(sequenceLength)
  );
  const [index, setIndex] = useState(0);
  const [currentSequence, setCurrentSequence] = useState([]);
  const [flash, setFlash] = useState(0);

  useEffect(() => {
    if (flash === 0) return;

    const timer = setTimeout(() => {
      setFlash(0);
    }, 200);

    return () => {
      clearTimeout(timer);
    };
  }, [flash]);

  useEffect(() => {
    if (
      _.isEqual(currentSequence.slice(-1 * sequenceLength * 2), secretSequence)
    ) {
      alert("Bravo!");
      setCurrentSequence([]);
      setSecretSequence(randomSequence(sequenceLength));
      setIndex(0);
    }
  }, [currentSequence, secretSequence]);

  useEffect(() => {
    if (index >= secretSequence.length) return;

    const timer = setTimeout(
      () => {
        setIndex((i) => i + 1);
      },
      index % 2 === 0 ? 600 : 200
    );

    return () => {
      clearTimeout(timer);
    };
  }, [index, secretSequence]);

  function cx(squareIndex) {
    return `
    colored-box
    ${colors[squareIndex - 1]}
    ${secretSequence[index] === squareIndex ? "active" : ""}
    ${flash === squareIndex ? "active" : ""}
    `;
  }

  function clicked(n) {
    setCurrentSequence((s) => [...s, n, 0]);
    setFlash(n);
  }

  return (
    <div>
      <div className="game">
        <div className={cx(1)} onClick={() => clicked(1)}>
          1
        </div>
        <div className={cx(2)} onClick={() => clicked(2)}>
          2
        </div>
        <div className={cx(3)} onClick={() => clicked(3)}>
          3
        </div>
        <div className={cx(4)} onClick={() => clicked(4)}>
          4
        </div>
      </div>
      <div>
        <button onClick={() => setIndex(0)}>Play Again</button>
      </div>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Game />
    </div>
  );
}

ואתם מוזמנים לשחק בו או לעדכן את הקוד בקישור: https://codesandbox.io/s/epic-bell-pcf9z?file=/src/App.js.