لعبة “سناك” هي من أشهر الألعاب البسيطة اللي ممكن أي حد يتعلم برمجتها بسهولة. فكرتها إنك بتتحكم في ثعبان صغير بيجري على الشاشة، وكل ما ياكل نقطة (الطعام) بيطول جسمه، وبيبقى التحدي إنك متخليش الثعبان يخبط في نفسه أو في حدود الشاشة. اللعبة بسيطة، لكن بتعلمك حاجات مهمة في البرمجة زي التعامل مع الرسوميات، التحكم في الحركة، والتفاعل مع المستخدم.
برمجة لعبة سناك باستخدام رياكت snake game by react
تثبيت React
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 ;
};
export default Food;
Snake.js
// components/Snake.js
import React from 'react';
const Snake = ({ segments }) => {
return (
<>
{segments.map((segment, index) => (
))}
>
);
};
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 (
{isGameOver ? (
Game Over!
) : (
<>
>
)}
);
};
export default GameBoard;
App.js
// App.js
import React from 'react';
import GameBoard from './components/GameBoard';
import './styles.css';
const App = () => {
return (
Snake Game
);
};
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;
}
مجلدات المشروع
تشغيل المشروع
npm start
النتيجة
شرح الكود
استيراد المكتبات والمكونات
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 (
{isGameOver ? (
Game Over!
) : (
<>
>
)}
);
- يتم عرض مكون اللوحة (
game-board
). - إذا كانت اللعبة قد انتهت، تظهر رسالة “Game Over” وزر لإعادة التشغيل.
- إذا كانت اللعبة مستمرة، يتم عرض مكون الثعبان ومكون الطعام.
Post Views: 131