Snake by React

برمجة لعبة سناك باستخدام رياكت snake game by react

لعبة “سناك” هي من أشهر الألعاب البسيطة اللي ممكن أي حد يتعلم برمجتها بسهولة. فكرتها إنك بتتحكم في ثعبان صغير بيجري على الشاشة، وكل ما ياكل نقطة (الطعام) بيطول جسمه، وبيبقى التحدي إنك متخليش الثعبان يخبط في نفسه أو في حدود الشاشة. اللعبة بسيطة، لكن بتعلمك حاجات مهمة في البرمجة زي التعامل مع الرسوميات، التحكم في الحركة، والتفاعل مع المستخدم.

برمجة لعبة سناك باستخدام رياكت snake game by react

تثبيت React

بعد التاكد من تثبيت node js على نظامك نقوم بتثبيت رياكت باستخدام الكود التالي

				
					npx create-react-app snake-game
				
			
				
					cd snake-game
				
			

قم بحذف الملفات الغير ضرورية من /src ثم نقوم بإنشاء لعبة Snake باستخدام React بطريقة نظيفة ومنفصلة بحيث تكون كل المكونات في ملفات مستقلة،

ملفات المشروع

Food.js

				
					// components/Food.js
import React from 'react';

const Food = ({ position }) => {
  const style = {
    left: `${position[0]}%`,
    top: `${position[1]}%`,
  };

  return <div className="food" style={style}></div>;
};

export default Food;

				
			

Snake.js

				
					// components/Snake.js
import React from 'react';

const Snake = ({ segments }) => {
  return (
    <>
      {segments.map((segment, index) => (
        <div
          className="snake-segment"
          key={index}
          style={{
            left: `${segment[0]}%`,
            top: `${segment[1]}%`,
          }}
        ></div>
      ))}
    </>
  );
};

export default Snake;

				
			

GameBoard.js

				
					// components/GameBoard.js
import React, { useState, useEffect, useCallback } from 'react';
import Snake from './Snake';
import Food from './Food';

const getRandomPosition = () => [Math.floor(Math.random() * 20) * 5, Math.floor(Math.random() * 20) * 5];
const boardSize = 500; // حجم اللوحة

const GameBoard = () => {
  const [snake, setSnake] = useState([[0, 0], [5, 0]]);
  const [food, setFood] = useState(getRandomPosition());
  const [direction, setDirection] = useState('');
  const [isGameOver, setIsGameOver] = useState(false);
  const [gameStarted, setGameStarted] = useState(false); // حالة تتبع بداية اللعبة

  // التحقق من التصادم
  const checkCollision = useCallback((head) => {
    // التصادم مع الجدران
    if (head[0] >= boardSize || head[0] < 0 || head[1] >= boardSize || head[1] < 0) {
      return true;
    }
    // التصادم مع الجسم
    for (let segment of snake) {
      if (segment[0] === head[0] && segment[1] === head[1]) {
        return true;
      }
    }
    return false;
  }, [snake]);

  const moveSnake = useCallback(() => {
    if (!gameStarted || isGameOver) return; // ابدأ التحريك فقط إذا كانت اللعبة قد بدأت ولم تنتهي

    const newSnake = [...snake];
    const head = newSnake[newSnake.length - 1];

    let newHead;
    switch (direction) {
      case 'UP':
        newHead = [head[0], head[1] - 5];
        break;
      case 'DOWN':
        newHead = [head[0], head[1] + 5];
        break;
      case 'LEFT':
        newHead = [head[0] - 5, head[1]];
        break;
      case 'RIGHT':
        newHead = [head[0] + 5, head[1]];
        break;
      default:
        return;
    }

    newSnake.push(newHead);
    newSnake.shift();

    if (checkCollision(newHead)) {
      setIsGameOver(true);
      return;
    }

    setSnake(newSnake);

    if (newHead[0] === food[0] && newHead[1] === food[1]) {
      setFood(getRandomPosition());
      newSnake.unshift([]); // Add new segment to snake
    }
  }, [snake, direction, food, checkCollision, gameStarted, isGameOver]);

  useEffect(() => {
    if (!gameStarted || isGameOver) return; // ابدأ التحريك فقط إذا كانت اللعبة قد بدأت ولم تنتهي
    const interval = setInterval(moveSnake, 200);
    return () => clearInterval(interval);
  }, [moveSnake, gameStarted, isGameOver]);

  // استخدام لوحة المفاتيح للحركة
  useEffect(() => {
    const handleKeyDown = (event) => {
      if (isGameOver) return;

      if (!gameStarted) setGameStarted(true); // ابدأ اللعبة عند الضغط على مفتاح سهم

      switch (event.key) {
        case 'ArrowUp':
          setDirection('UP');
          break;
        case 'ArrowDown':
          setDirection('DOWN');
          break;
        case 'ArrowLeft':
          setDirection('LEFT');
          break;
        case 'ArrowRight':
          setDirection('RIGHT');
          break;
        default:
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isGameOver, gameStarted]);

  // إعادة تشغيل اللعبة
  const resetGame = () => {
    setSnake([[0, 0], [5, 0]]);
    setFood(getRandomPosition());
    setDirection('');
    setIsGameOver(false);
    setGameStarted(false); // إعادة الحالة لبداية جديدة
  };

  return (
    <div className="game-board">
      {isGameOver ? (
        <div className="game-over">
          <h2>Game Over!</h2>
          <button onClick={resetGame}>Restart</button>
        </div>
      ) : (
        <>
          <Snake segments={snake} />
          <Food position={food} />
        </>
      )}
    </div>
  );
};

export default GameBoard;

				
			

App.js

				
					// App.js
import React from 'react';
import GameBoard from './components/GameBoard';
import './styles.css';

const App = () => {
  return (
    <div className="app">
      <h1>Snake Game</h1>
      <GameBoard />
    </div>
  );
};

export default App;

				
			

styles.css

				
					/* styles.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  
  .app {
    text-align: center;
    margin-top: 50px;
  }
  
  .game-board {
    position: relative;
    width: 500px; /* تكبير حجم اللوحة */
    height: 500px; /* تكبير حجم اللوحة */
    background-color: #fffeac;
    margin: 20px auto;
    border: 2px solid #333;
  }
  
  .snake-segment {
    position: absolute;
    width: 25px;
    height: 25px;
    background-color: green;
  }
  
  .food {
    position: absolute;
    width: 25px;
    height: 25px;
    background-color: red;
    border-radius: 50%;
  }
  
  .buttons {
    margin-top: 20px;
  }
  
  button {
    margin: 5px;
    padding: 10px 20px;
    font-size: 16px;
  }
  
  .game-over {
    color: red;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
  
  .game-over h2 {
    margin-bottom: 20px;
  }
  
				
			

مجلدات المشروع

snake react png

تشغيل المشروع

				
					npm start

				
			

النتيجة

برمجة لعبة سناك باستخدام رياكت snake game by react
result

شرح الكود

استيراد المكتبات والمكونات

				
					import React, { useState, useEffect, useCallback } from 'react';
import Snake from './Snake';
import Food from './Food';

				
			
  • React: المكتبة الأساسية لبناء واجهات المستخدم.
  • useState: هو هوك يستخدم لتخزين الحالة في مكون React.
  • useEffect: هو هوك يستخدم لتنفيذ تأثيرات جانبية (side effects) مثل التلاعب بالـ DOM أو استدعاء APIs.
  • useCallback: هو هوك يستخدم لتحسين الأداء من خلال حفظ دالة (function) حتى لا يتم إعادة إنشاؤها في كل إعادة تصيير للمكون.
  • Snake و Food: مكونان فرعيان يمثلان الثعبان والطعام في اللعبة.

دوال المساعدة

				
					const getRandomPosition = () => [Math.floor(Math.random() * 20) * 5, Math.floor(Math.random() * 20) * 5];
const boardSize = 500; // حجم اللوحة

				
			
  • getRandomPosition: دالة تُعيد موضع عشوائي للطعام داخل اللوحة. تستخدم الأعداد العشوائية لضبط موقع الطعام على الشبكة، حيث كل خانة بحجم 5 بكسل.
  • boardSize: متغير يحدد حجم اللوحة (500 بكسل).

إعداد الحالة

				
					const [snake, setSnake] = useState([[0, 0], [5, 0]]);
const [food, setFood] = useState(getRandomPosition());
const [direction, setDirection] = useState('');
const [isGameOver, setIsGameOver] = useState(false);
const [gameStarted, setGameStarted] = useState(false); // حالة تتبع بداية اللعبة

				
			
  • snake: الحالة التي تحتفظ بمواقع أجزاء الثعبان، تبدأ بقيمتين (الجزء الأول عند [0, 0] والجزء الثاني عند [5, 0]).
  • food: الحالة التي تحتفظ بموقع الطعام، يتم تعيينها عند البداية باستخدام getRandomPosition.
  • direction: حالة لتحديد اتجاه حركة الثعبان (مبدئيًا فارغة).
  • isGameOver: حالة لتحديد ما إذا كانت اللعبة قد انتهت (تبدأ كـ false).
  • gameStarted: حالة لتحديد ما إذا كانت اللعبة قد بدأت أم لا.

دالة التحقق من التصادم

				
					const checkCollision = useCallback((head) => {
    // التصادم مع الجدران
    if (head[0] >= boardSize || head[0] < 0 || head[1] >= boardSize || head[1] < 0) {
        return true;
    }
    // التصادم مع الجسم
    for (let segment of snake) {
        if (segment[0] === head[0] && segment[1] === head[1]) {
            return true;
        }
    }
    return false;
}, [snake]);

				
			
  • checkCollision: دالة تتحقق مما إذا كان رأس الثعبان (الجزء الأخير في مصفوفة snake) قد اصطدم بالجدران أو بأجزاء الثعبان الأخرى.
    • إذا اصطدم برأس الثعبان بالجدار: ترجع true.
    • إذا اصطدم برأس الثعبان بجسمه: ترجع true.
    • إذا لم يحدث أي تصادم: ترجع false.

دالة تحريك الثعبان

				
					const moveSnake = useCallback(() => {
    if (!gameStarted || isGameOver) return; // ابدأ التحريك فقط إذا كانت اللعبة قد بدأت ولم تنتهي

    const newSnake = [...snake];
    const head = newSnake[newSnake.length - 1];
    let newHead;

				
			

moveSnake: دالة تقوم بتحريك الثعبان.

  • تتحقق مما إذا كانت اللعبة قد بدأت وأنها لم تنتهِ، وإذا لم تكن كذلك، تعود بدون فعل أي شيء.
  • تنسخ مصفوفة snake إلى newSnake، وتحدد الرأس الحالي (آخر عنصر في المصفوفة).

تحديد الحركة بناءً على الاتجاه

				
					    switch (direction) {
        case 'UP':
            newHead = [head[0], head[1] - 5];
            break;
        case 'DOWN':
            newHead = [head[0], head[1] + 5];
            break;
        case 'LEFT':
            newHead = [head[0] - 5, head[1]];
            break;
        case 'RIGHT':
            newHead = [head[0] + 5, head[1]];
            break;
        default:
            return;
    }

    newSnake.push(newHead);
    newSnake.shift();

				
			
  • يتحقق من اتجاه الحركة ويحسب الموقع الجديد للرأس بناءً على الاتجاه الحالي.
  • بعد تحديد newHead (الموقع الجديد للرأس)، يتم إضافته إلى newSnake ويتم حذف آخر جزء من الثعبان (لأن الثعبان يتحرك).

التحقق من التصادم وتحديث الحالة

				
					    if (checkCollision(newHead)) {
        setIsGameOver(true);
        return;
    }

    setSnake(newSnake);

    if (newHead[0] === food[0] && newHead[1] === food[1]) {
        setFood(getRandomPosition());
        newSnake.unshift([]); // Add new segment to snake
    }
}, [snake, direction, food, checkCollision, gameStarted, isGameOver]);

				
			
  • يتم التحقق مما إذا كان رأس الثعبان قد اصطدم.
  • إذا حدث تصادم، يتم تعيين isGameOver إلى true.
  • إذا لم يحدث تصادم، يتم تحديث snake بالقيم الجديدة.
  • إذا كان رأس الثعبان يتداخل مع موقع الطعام، يتم تغيير موقع الطعام وتحديث الثعبان ليضيف جزءًا جديدًا.

تأثير لتحريك الثعبان

				
					useEffect(() => {
    if (!gameStarted || isGameOver) return; // ابدأ التحريك فقط إذا كانت اللعبة قد بدأت ولم تنتهي
    const interval = setInterval(moveSnake, 200);
    return () => clearInterval(interval);
}, [moveSnake, gameStarted, isGameOver]);

				
			
  • يقوم بتحديد interval لتحريك الثعبان كل 200 مللي ثانية، فقط إذا كانت اللعبة قد بدأت ولم تنتهِ.
  • عند التوقف، يتم تنظيف المؤقت باستخدام clearInterval.

معالجة ضغطات المفاتيح

				
					useEffect(() => {
    const handleKeyDown = (event) => {
        if (isGameOver) return;

        if (!gameStarted) setGameStarted(true); // ابدأ اللعبة عند الضغط على مفتاح سهم

        switch (event.key) {
            case 'ArrowUp':
                setDirection('UP');
                break;
            case 'ArrowDown':
                setDirection('DOWN');
                break;
            case 'ArrowLeft':
                setDirection('LEFT');
                break;
            case 'ArrowRight':
                setDirection('RIGHT');
                break;
            default:
                break;
        }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
}, [isGameOver, gameStarted]);

				
			
  • يتتبع ضغطات المفاتيح ويغير اتجاه الثعبان بناءً على مفتاح السهم المضغوط.
  • إذا كانت اللعبة لم تبدأ، يتم تعيين gameStarted إلى true عند الضغط على مفتاح سهم لأول مرة.

إعادة تشغيل اللعبة

				
					const resetGame = () => {
    setSnake([[0, 0], [5, 0]]);
    setFood(getRandomPosition());
    setDirection('');
    setIsGameOver(false);
    setGameStarted(false); // إعادة الحالة لبداية جديدة
};

				
			
  • دالة لإعادة تعيين جميع الحالات إلى القيم الأولية، مما يسمح للاعب ببدء لعبة جديدة بعد انتهاء اللعبة.

عرض واجهة اللعبة

				
					return (
    <div className="game-board">
        {isGameOver ? (
            <div className="game-over">
                <h2>Game Over!</h2>
                <button onClick={resetGame}>Restart</button>
            </div>
        ) : (
            <>
                <Snake segments={snake} />
                <Food position={food} />
            </>
        )}
    </div>
);

				
			
  • يتم عرض مكون اللوحة (game-board).
  • إذا كانت اللعبة قد انتهت، تظهر رسالة “Game Over” وزر لإعادة التشغيل.
  • إذا كانت اللعبة مستمرة، يتم عرض مكون الثعبان ومكون الطعام.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top