در این مقاله ساخت بازی مار با استفاده از برنامه React را یاد خواهیم گرفت. این یک بازی دو بعدی ساده است که با استفاده از TypeScript ساخته شده است و برای ساخت آن نیازی به استفاده از کتابخانه های اضافی نخواهیم داشت. تصویر چیزی که خواهیم ساخت در زیر آمده است:
بازی Snake یا مار یک بازی سرگرم کننده است که ممکن است آن را در تلفن های همراه قدیمی مانند مدل های نوکیا 3310 بازی کرده باشید.
مفهوم پشت آن ساده است. مار در داخل یک جعبه پرسه میزند، و هنگامی که میوه/شی را می خورد، امتیاز شما افزایش مییابد و مار رشد میکند. اگر مار به مرزهای جعبه بازی برخورد کند یا با خودش برخورد کند، بازی تمام می شود.
این مقاله تمام مهارت ها و مراحل لازم برای ایجاد بازی Snake خود را از ابتدا در اختیار شما قرار می دهد. ابتدا ساختار کد و منطق آن ها را بررسی خواهیم کرد. سپس توضیح خواهم داد که وقتی همه کد ها در کنار هم قرار می گیرند و به نوعی متصل می شوند چگونه کار می کنند.
بدون هیچ مقدمه ای، بیایید شروع کنیم.
پیش از شروع خواندن این مقاله، باید دانش اولیه ای از موضوع های زیر داشته باشید:
نمودارهای کلاس: ما می خواهیم از آنها برای نمایش مثال خود استفاده کنیم. در این جا چند منبع وجود دارد که می توانید برای کسب اطلاعات بیشتر در مورد آنها استفاده کنید:
مار بازی، یک بازی ویدیویی یا آرکید (بازی آرکید یک نوع بازی تصویری کامپیوتری است که توسط ماشین هایی با سکه پول کار می کنند) است که شامل حرکت یک مار در داخل جعبه است. امتیاز شما بر اساس تعداد شی/میوه ای که مار می خورد افزایش می یابد. این کار باعث افزایش سایز مار نیز می شود. اگر با خودش یا به مرز جعبه برخورد کند، بازی تمام می شود.
اطلاعات بیش تر در مورد تاریخچه یا ریشه های بازی را می توانید در این لینک ویکی پدیا بخوانید.
ما قصد داریم از ابزارهای زیر برای ساخت بازی خود استفاده کنیم:
می توانید همه چیز را در مورد موضوعات بالا و نحوه عملکرد داخلی Redux از بخش getting started در Redux doc بیاموزید.
ما از کتابخانه مدیریت state یعنی Redux استفاده می کنیم زیرا به ما کمک می کند تا state سراسری خود را به روشی ساده تر مدیریت کنیم. Redux به ما امکان می دهد از حفاری prop خودداری کنیم. (حفاری prop فرآیند فرستادن prop ها از یک کامپوننت سطح بالاتر به یک کامپوننت سطح پایین است). هم چنین به ما این امکان را میدهد تا از طریق میانافزار یا middleware، کارهای پیچیده همگامسازی را انجام دهیم.
در این جا می توانید درباره میان افزار بیشتر بدانید.
Redux-saga یک میانافزار است که به ما کمک میکند بین action فرستاده شده و ریدوسر store redux تبادل داده داشته باشیم. به ما امکان میدهد تا عوارض جانبی خاصی را بین بین action فرستاده شده و ریدوسر انجام دهیم، مانند واکشی داده، گوش دادن به کارهای خاص یا تنظیم اشتراکها (subscription ها)، ساخت action ها و موارد دیگر.
Redux-saga از ژنراتور ها و توابع ژنراتور استفاده می کند. یک saga معمولی به شکل زیر است:
function* performAction() { yield put({ type: COPY_DATA, payload: "Hello" }); }
performAction یک تابع ژنراتور است. این تابع ژنراتور تابع put را اجرا خواهد کرد. یک شی ایجاد می کند و آن را به saga برمی گرداند و می گوید که چه نوع action باید با چه payload اجرا شود. سپس فراخوانی put یک توصیفگر شی را برمیگرداند که میگوید کدام saga میتواند در آینده آن را بگیرد و یک action خاص را اجرا کند.
توجه: با مراجعه به بخش پیش نیاز می توانید در مورد ژنراتورها و تابع های ژنراتور اطلاعات بیشتری کسب کنید.
اکنون این سوال پیش می آید که چرا از میان افزار redux-saga استفاده می کنیم؟ پاسخ ساده است:
اگر با redux-saga آشنایی ندارید، به شدت توصیه میکنم اسناد را در این جا مرور کنید.
توجه: نمودارهای کانتکس، کانتینر و کلاس ترسیم شده در این پست وبلاگ دقیقا از قراردادهای دقیق این نمودارها پیروی نمی کنند. من آن ها را در اینجا تقریب زدم تا بتوانید مفاهیم اساسی را درک کنید.
پیش از شروع، پیشنهاد میکنم در مورد مدلهای c4، نمودارهای کانتینر و نمودارهای زمینه مطالعه کنید. می توانید منابعی در مورد آن ها را در بخش پیش نیازها پیدا کنید.
تعریف حالت کاملا توضیحی است، و ما در بالا درباره آن چه که بازی مار به آن نیاز دارد گفتگو کردهایم. در زیر نمودار کانتکس برای تعریف حالت ما آمده است:
نمودار زمینه ای (کانتکس) ما بسیار ساده است. بازیکن با رابط کاربری هم کنش دارد. بیایید ژرف تر رابط کاربری UI نگهدارنده (کانتینری) را ببینیم و سیستمهای دیگری را در داخل آن بررسی کنیم:
همان طور که از نمودار بالا می بینید، رابط کاربری Game Board ما به دو لایه تقسیم می شود:
لایه UI از اجزای زیر تشکیل شده است:
هم چنین مسئولیت های زیر را بر عهده دارد:
حالا بیایید در مورد لایه داده صحبت کنیم. لایه داده از اجزای زیر تشکیل شده است:
همه این کامپوننت ها را به طور دقیق بررسی خواهیم کرد و در بخش های بعدی خواهیم دید که چگونه به طور جمعی کار می کنند. ابتدا، بیایید پروژه خود را مقداردهی اولیه کنیم و لایه داده خود را راه اندازی کنیم یعنی Store Redux.
پیش از شروع به درک اجزای بازی خود، اجازه دهید ابتدا برنامه React و لایه داده خود را راه اندازی کنیم.
بازی با React ساخته شده است. من به شدت توصیه می کنم از دستور create-react-app برای نصب تمام موارد لازم برای شروع برنامه React خود استفاده کنید.برای ایجاد یک پروژه CRA(create-react-app) ابتدا باید آن را نصب کنیم. دستور زیر را در ترمینال خود تایپ کنید:
npm install -g create-react-app
توجه: پیش از اجرای این دستور مطمئن شوید که Node.js را در سیستم خود نصب کرده اید. برای نصب این لینک را کلیک کنید.
در مرحله بعد، با ایجاد پروژه خود شروع می کنیم. نام آن را snake می گذاریم. دستور زیر را در ترمینال خود تایپ کنید تا پروژه ایجاد شود:
npx create-react-app snake-game
ممکن است چند دقیقه زمان ببرد تا تکمیل شود. پس از تکمیل این کار، با استفاده از دستور زیر به پروژه جدید ایجاد شده خود بروید:
cd snake-game
پس از ورود به پروژه، دستور زیر را برای راه اندازی پروژه تایپ کنید:
npm run start
این دستور یک برگه جدید در مرورگر شما باز می کند که آرم React در صفحه مانند زیر می چرخد:
اکنون راه اندازی اولیه پروژه ما کامل شده است. بیایید لایه داده خود (Store Redux) را پیکربندی کنیم. لایه داده ما نیاز به نصب بسته های زیر دارد:
ابتدا اجازه دهید با نصب این بسته ها شروع کنیم. پیش از شروع، مطمئن شوید که در پوشه ای که برنامه در آن قرار دارد هستید. دستور زیر را در ترمینال تایپ کنید:
npm install redux react-redux redux-saga
پس از نصب این بسته ها، ابتدا Store Redux خود را پیکربندی می کنیم. برای شروع، اجازه دهید ابتدا یک پوشه به نام store در پوشه src ایجاد می کنیم. این پوشه store شامل تمام فایل های مربوط به Redux خواهد بود. پوشه store خود را به روش زیر سازماندهی می کنیم:
بیایید ببینیم که هر یک از این فایل ها چه کاری انجام می دهند:
export const MOVE_RIGHT = "MOVE_RIGHT"
از این ثابت action برای ایجاد تابعی استفاده می کنیم که یک شی با ویژگی های زیر را برمی گرداند:
این توابع که یک شی را با ویژگی type برمی گرداند، Action Creators یا سازندگان action (کنش) نامیده می شوند. از این توابع برای فرستادن کارها به Store Redux خود استفاده می کنیم.
ویژگی payload نشان می دهد که همراه با این عمل می توانیم داده های اضافی را نیز ارسال کنیم که می تواند برای ذخیره یا به روز رسانی مقدار در داخل حالت جهانی استفاده شود.
توجه: بازگرداندن ویژگی type از سازنده action الزامی است. ویژگی payload اختیاری است. هم چنین نام ویژگی payload می تواند هر چیزی باشد. بیایید نمونه ای از یک سازنده action را ببینیم:
//Without payload export const moveRight = () => ({ type: MOVE_RIGHT }); //With payload export const moveRight = (data: string) => ({ type: MOVE_RIGHT, payload: data });
اکنون که میدانیم اکشنها و سازندگان اکشن چیستند، میتوانیم به پیکربندی reducer، برویم.
reducer ها توابعی هستند که هر بار که یک اکشن فرستاده می شود یک state سراسری جدید را برمی گرداند. آن ها state سراسری فعلی را می گیرند و state جدید را بر اساس اکشن که فرستاده شده/ فراخوانی می شود، برمی گردانند. این state جدید بر اساس state قبلی محاسبه می شود.
در اینجا باید مراقب باشیم که هیچ اثر جانبی یا side effect در داخل این تابع ایجاد نکنیم. ما نباید state سراسری را تغییر دهیم بلکه باید state به روز شده را به عنوان یک شی جدید برگردانیم. بنابراین، تابع reducer باید یک تابع خالص باشد.
به اندازه کافی در مورد reducer ها صحبت کردیم. بیایید نگاهی به reducer های نمونه خود بیندازیم:
const GlobalState = { data: "" }; const gameReducer = (state = GlobalState, action) => { switch (action.type) { case "MOVE_RIGHT": /** * Perform a certain set of operations */ return { ...state, data: action.payload }; default: return state; } }
در این مثال، ما یک تابع reducer ایجاد کرده ایم که به آن gameReducer می گویند. state (پارامتر پیشفرض بهعنوان یک state سراسری) و یک action را میگیرد. هر زمان که action.type داشته باشیم که با یکی از case های switch مطابقت داشته باشد، یک action خاص مانند برگرداندن یک state جدید بر اساس action انجام می دهد.
فایل sagas/index.ts شامل تمام saga هایی است که در برنامه خود استفاده خواهیم کرد. زمانی که اجرای بازی مار را شروع کنیم ژرف تر با توضیح saga ها خواهیم پرداخت.
اکنون درک اولیه ای از Store Redux خود داریم. بیایید ادامه دهیم و stores/index.ts را مانند زیر ایجاد کنیم:
import { createStore, applyMiddleware } from "redux"; import createSagaMiddleware from "redux-saga"; import gameReducer from "./reducers"; import watcherSagas from "./sagas"; const sagaMiddleware = createSagaMiddleware(); const store = createStore(gameReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(watcherSagas); export default store;
ابتدا reducer و saga خود را import خواهیم کرد. در مرحله بعد از تابع createSagaMiddleware() برای ایجاد میان افزار saga استفاده می کنیم.
سپس، آن را با ارسال آن به عنوان آرگومان به تابع applicationMiddleware در داخل createStore که برای ایجاد فروشگاه از آن استفاده می کنید، به فروشگاه خود متصل می کنیم. همچنین gameReducer را به این تابع منتقل می کنیم تا یک کاهش دهنده به فروشگاه ما نگاشت شود.
در نهایت، ما sagaMiddleware خود را با استفاده از این کد اجرا می کنیم:
sagaMiddleware.run(watcherSagas);
مرحله آخر ما این است که این store را در سطح بالای برنامه React با استفاده از کامپوننت Provider رندر شده توسط react-redux به اصطلاح تزریق کنیم. شما می توانید این کار را به صورت زیر انجام دهید:
import { Provider } from "react-redux"; import store from "./store"; const App = () => { return ( <Provider store={store}> // Child components... </Provider> ); }; export default App;
هم چنین باید chakra-UI را به عنوان یک کتابخانه UI برای پروژه خود نصب کنیم. برای نصب chakra-UI دستور زیر را تایپ کنید:
npm install @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^5
هم چنین باید ChakraProvider را که در فایل App.tsx ما قرار می گیرد تنظیم کنیم. فایل App.tsx پس از این تغییر به صورت زیر در خواهد آمد:
import { ChakraProvider, Container, Heading } from "@chakra-ui/react"; import { Provider } from "react-redux"; import store from "./store"; const App = () => { return ( <Provider store={store}> <ChakraProvider> <Container maxW="container.lg" centerContent> <Heading as="h1" size="xl">SNAKE GAME</Heading> //Children components </Container> </ChakraProvider> </Provider> ); }; export default App;
بیایید ابتدا پویایی بازی Snake خود را از دیدگاه UI درک کنیم. پیش از شروع، تا این جا بازی Snake ما به شکل زیر خواهد بود:
لایه UI از 3 لایه تشکیل شده است: محاسبه گر امتیاز، صفحه canvas، و دستورالعمل ها. نمودار زیر این بخش ها را نشان می دهد:
بیایید عمیقتر به هر یک از این بخشها بپردازیم تا بفهمیم بازی Snake ما چگونه کار میکند.
با درک صفحه canvas شروع می کنیم:
صفحه canvas دارای ابعاد height 600، width 1000 است.همه این صفحه به بلوک هایی با اندازه 20x20 تقسیم شده است. یعنی هر چیزی که روی این canvas کشیده می شود دارای ارتفاع 20 و عرض 20 خواهد بود. از تگ canvas برای ترسیم اشکال در کامپوننت استفاده می کنیم.
در پروژه ما، کامپوننت صفحه canvas را در داخل فایل components/CanvasBoard.tsx مینویسیم. اکنون که درک اولیه ما در مورد کامپوننت CanvasBoard روشن شده است، بیایید ساخت این کامپوننت را شروع کنیم. یک کامپوننت ساده ایجاد می کنیم که یک عنصر canvas را به صورت زیر برمی گرداند:
export interface ICanvasBoard { height: number; width: number; } const CanvasBoard = ({ height, width }: ICanvasBoard) => { return ( <canvas style={{ border: "3px solid black", }} height={height} width={width} /> ); };
این کامپوننت را در فایل App.tsx با عرض و ارتفاع 1000 و 600 به عنوان prop مانند زیر فراخوانی کنید:
import { ChakraProvider, Container, Heading } from "@chakra-ui/react"; import { Provider } from "react-redux"; import CanvasBoard from "./components/CanvasBoard"; import ScoreCard from "./components/ScoreCard"; import store from "./store"; const App = () => { return ( <Provider store={store}> <ChakraProvider> <Container maxW="container.lg" centerContent> <Heading as="h1" size="xl">SNAKE GAME</Heading> <CanvasBoard height={600} width={1000} /> //Canvasboard component added </Container> </ChakraProvider> </Provider> ); }; export default App;
این کد یک border ساده با height=600 و width=1000 با border سیاه مانند زیر ایجاد می کند:
حالا بیایید یک مار در مرکز این canvas بکشیم. اما پیش از شروع طراحی، باید context این تگ canvas را بدست آوریم.
context یک تگ canvas تمام اطلاعات مورد نیاز مربوط به این تگ canvas را در اختیار شما قرار می دهد. ابعاد canvas را به شما می دهد و هم چنین به شما کمک می کند تا روی canvas نقاشی بکشید.
برای بدست آوردن context یک تگ canvas باید تابع getCanvas('2d') را فراخوانی کنیم که context 2 بعدی canvas را برمی گرداند. نوع برگشتی این تابع یک interface برای CanvasRenderingContext2D است. برای انجام این کار در JS ساده، کاری مانند زیر انجام می دهیم:
const canvas = document.querySelector('canvas'); const canvasCtx = canvas.getContext('2d');
اما برای انجام این کار در React باید یک ref ایجاد کنیم و آن را به تگ canvas بفرستیم تا بتوانیم بعدا در هوک های مختلف از آن استفاده کنیم. برای انجام این کار در برنامه ما، با استفاده از هوک useRef یک ref ایجاد کنید:
const canvasRef = useRef<HTMLCanvasElement | null>(null);
ref را به تگ canvas خود می فرستیم:
<canvas ref={canvasRef} style={{ border: "3px solid black", }} height={height} width={width} />;
هنگامی که canvasRef به تگ canvas فرستاده داده می شود، می توانیم آن را در داخل یک هوک useEffect استفاده کنیم و متن را در یک متغیر state ذخیره کنیم.
export interface ICanvasBoard { height: number; width: number; } const CanvasBoard = ({ height, width }: ICanvasBoard) => { const canvasRef = (useRef < HTMLCanvasElement) | (null > null); const [context, setContext] = (useState < CanvasRenderingContext2D) | (null > null); useEffect(() => { //Draw on canvas each time setContext(canvasRef.current && canvasRef.current.getContext("2d")); //store in state variable }, [context]); return ( <canvas ref={canvasRef} style={{ border: "3px solid black", }} height={height} width={width} /> ); };
پس از دریافت context، باید هر بار که یک کامپوننت به روز می شود، وظایف زیر را انجام دهیم:
می خواهیم چندین بار canvas را پاک کنیم، بنابراین این را به یک تابع کاربردی تبدیل می کنیم. بنابراین برای آن، اجازه دهید یک پوشه به نام utilities ایجاد کنیم:
mkdir utilities cd utilities touch index.tsx
دستور بالا هم چنین یک فایل index.tsx در داخل پوشه utilities ایجاد می کند. کد زیر را در فایل utilities/index.tsx بیافزایید:
export const clearBoard = (context: CanvasRenderingContext2D | null) => { if (context) { context.clearRect(0, 0, 1000, 600); } };
عملکرد clearBoard بسیار ساده است. اقدامات زیر را انجام می دهد:
ما از این تابع clearBoard در داخل کامپوننت CanvasBoard در useEffect خود برای پاک کردن canvas هر بار که کامپوننت بهروزرسانی میشود استفاده میکنیم. برای تمایز بین useEffect های مختلف، useEffect فوق را useEffect1 نام گذاری می کنیم.
حالا بیایید با ترسیم مار و میوه در یک موقعیت تصادفی شروع کنیم. از آن جایی که قرار است چندین بار اشیا را ترسیم کنیم، یک تابع کاربردی به نام drawObject برای آن ایجاد می کنیم. کد زیر را در فایل utilities/index.tsx اضافه می کنیم:
export interface IObjectBody { x: number; y: number; } export const drawObject = ( context: CanvasRenderingContext2D | null, objectBody: IObjectBody[], fillColor: string, strokeStyle = "#146356" ) => { if (context) { objectBody.forEach((object: IObjectBody) => { context.fillStyle = fillColor; context.strokeStyle = strokeStyle; context?.fillRect(object.x, object.y, 20, 20); context?.strokeRect(object.x, object.y, 20, 20); }); } };
تابع بالا برای کشیدن یک شی بر روی canvas است
تابع drawObject آرگومان های زیر را می پذیرد:
context - یک شی کانتکس دوبعدی canvas برای ترسیم شی روی canvas.
objectBody - آرایه ای از اشیا است که هر شی دارای ویژگی های x و y است، مانند interface به نام IObjectBody
fillColor - رنگی که باید در داخل شی استفاده شود.
strokeStyle - رنگی که باید برای outline شی استفاده شود که دارای مقدار پیشفرض #146356 است.
این تابع بررسی میکند که آیا کانتکس تعریف نشده یا null است. سپس با استفاده از forEach روی objectBody تکرار می شود. برای هر شی عملیات زیر را انجام می دهد:
برای کشیدن مار باید موقعیت مار را حفظ کنیم. برای آن، میتوانیم از ابزار مدیریت state سراسری redux خود استفاده کنیم.ما باید فایل Reducers/index.ts خود را به روز کنیم. از آن جایی که می خواهیم موقعیت مار را ردیابی کنیم، آن را به صورت زیر به state سراسری خود اضافه می کنیم:
interface ISnakeCoord { x: number; y: number; } export interface IGlobalState { snake: ISnakeCoord[] | []; } const globalState: IGlobalState = { //Postion of the entire snake snake: [ { x: 580, y: 300 }, { x: 560, y: 300 }, { x: 540, y: 300 }, { x: 520, y: 300 }, { x: 500, y: 300 }, ], };
در بالا state سراسری به روزآوری می شود.این state را در کامپوننت CanvasBoard خود فراخوانی می کنیم. ما از هوک useSelector از react-redux برای دریافت state مورد نیاز از store استفاده خواهیم کرد. موارد زیر state سراسری مار را به ما نشان می دهد:
const snake1 = useSelector((state: IGlobalState) => state.snake);
بیایید این را در کامپوننت Canvas Board خود قرار دهیم و آن را به تابع drawObject خود بفرستیم و خروجی را ببینیم:
//Importing necessary modules import { useSelector } from "react-redux"; import { clearBoard, drawObject, generateRandomPosition } from "../utils"; export interface ICanvasBoard { height: number; width: number; } const CanvasBoard = ({ height, width }: ICanvasBoard) => { const canvasRef = useRef<HTMLCanvasElement | null>(null); const [context, setContext] = useState<CanvasRenderingContext2D | null>(null); const snake1 = useSelector((state: IGlobalState) => state.snake); const [pos, setPos] = useState<IObjectBody>( generateRandomPosition(width - 20, height - 20) ); useEffect(() => { //Draw on canvas each time setContext(canvasRef.current && canvasRef.current.getContext("2d")); //store in state variable drawObject(context, snake1, "#91C483"); //Draws snake at the required position drawObject(context, [pos], "#676FA3"); //Draws fruit randomly }, [context]) return ( <canvas style={{ border: "3px solid black", }} height={height} width={width} /> ); };
کد بالا برای کشیدن مار و میوه است. بیایید ببینیم وقتی مار کشیده می شود خروجی چگونه خواهد بود:
اکنون که مار خود را کشیده ایم، بیایید یاد بگیریم که چگونه مار را حرکت دهیم.
حرکت مار ساده است. همیشه باید نکات زیر را رعایت کنید:
برای حرکت روان مار، مار باید همیشه به شکل مستطیلی حرکت کند. و برای داشتن آن حرکت باید نکات بالا را رعایت کند.نمودار زیر به طور خلاصه نحوه عملکرد حرکت مار در کل برنامه را نشان می دهد:
توجه: در نمودار بالا، کل حرکت مار با کامپوننت CanvasBoard شروع می شود.
نکته: اگر نمی توانید نمودار بالا را دنبال کنید نگران نباشید. بخش های بعدی را بخوانید تا درک بیشتری به دست آورید.
برای حفظ حرکت مار، state دیگری به نام disallowedDirection را در state سراسری یا عمومی خود معرفی می کنیم. هدف این متغیر پیگیری جهت مخالف حرکت مار است.
به عنوان مثال اگر مار به سمت چپ حرکت می کند، DisallowedDirection به سمت راست تنظیم می شود. بنابراین به طور خلاصه، ما این جهت را دنبال می کنیم تا بتوانیم از حرکت مار در جهت مخالف خود جلوگیری کنیم.
بیایید این متغیر را در state سراسری خود ایجاد کنیم:
interface ISnakeCoord { x: number; y: number; } export interface IGlobalState { snake: ISnakeCoord[] | []; disallowedDirection: string; } const globalState: IGlobalState = { //Postion of the entire snake snake: [ { x: 580, y: 300 }, { x: 560, y: 300 }, { x: 540, y: 300 }, { x: 520, y: 300 }, { x: 500, y: 300 }, ], disallowedDirection: "" };
حالا بیایید چند اکشن و سازنده اکشن بسازیم که به حرکت دادن مار کمک می کند. برای این مورد دو نوع اکشن خواهیم داشت:
در بخش های بعدی به این اکشن ها نگاه دقیق تری خواهیم داشت. یک اکشن دیگر به نام SET_DIS_DIRECTION ایجاد خواهیم کرد تا استیت DisallowedDirection را مقداردهی کنیم.
بیایید چند سازنده اکشن برای حرکت مار ایجاد کنیم:
setDisDirection – این سازنده اکشن برای مقداردهی DisallowedDirection از طریق اکشن SET_DIS_DIRECTION استفاده می شود. در زیر کد این سازنده اکشن آمده است:
export const setDisDirection = (direction: string) => ({ type: SET_DIS_DIRECTION, payload: direction });
makeMove - برای تنظیم یا بهروزرسانی مختصات جدید مار با بهروزرسانی متغیر استیت snake استفاده میشود. در زیر کد این سازنده اکشن آمده است:
export const makeMove = (dx: number, dy: number, move: string) => ({ type: move, payload: [dx, dy] });
پارامترهای dx و dy دلتا هستند. آن ها به Store Redux می گویند که چقدر باید مختصات هر بلوک مار را افزایش یا کاهش دهیم تا مار را در جهت معین حرکت دهیم. از پارامتر move برای تعیین جهت حرکت مار استفاده می شود. به زودی در بخشهای آینده نگاهی به این سازندگان اکشنها خواهیم داشت.
در نهایت، فایل actions/index.ts چیزی شبیه به زیر خواهد بود:
export const MOVE_RIGHT = "MOVE_RIGHT"; export const MOVE_LEFT = "MOVE_LEFT"; export const MOVE_UP = "MOVE_UP"; export const MOVE_DOWN = "MOVE_DOWN"; export const RIGHT = "RIGHT"; export const LEFT = "LEFT"; export const UP = "UP"; export const DOWN = "DOWN"; export const SET_DIS_DIRECTION = "SET_DIS_DIRECTION"; export interface ISnakeCoord { x: number; y: number; } export const makeMove = (dx: number, dy: number, move: string) => ({ type: move, payload: [dx, dy] }); export const setDisDirection = (direction: string) => ({ type: SET_DIS_DIRECTION, payload: direction });
حال، بیایید نگاهی به منطقی که برای حرکت دادن مار بر اساس اکشن های بالا استفاده می کنیم بیندازیم. تمام حرکات مار با اکشن های زیر ردیابی می شود:
همه این اکشن ها برای حرکت مار ضروری هستند. این اکشن ها، هنگامی که ارسال می شوند، همیشه state سراسری مار را بر اساس منطقی که در زیر توضیح می دهیم به روز می کنند. و آنها مختصات جدید مار را در هر حرکت محاسبه می کنند.
برای محاسبه مختصات جدید مار بعد از هر حرکت، از منطق زیر استفاده می کنیم:
اکنون که درک درستی از نحوه حرکت مار داریم، بیایید موارد زیر را در Reducer خود اضافه کنیم:
case RIGHT: case LEFT: case UP: case DOWN: { let newSnake = [...state.snake]; newSnake = [{ //New x and y coordinates x: state.snake[0].x + action.payload[0], y: state.snake[0].y + action.payload[1], }, ...newSnake]; newSnake.pop(); return { ...state, snake: newSnake, }; }
برای هر حرکت مار، مختصات x و y جدید را به روز می کنیم که توسط payload های action.payload[0] و action.payload[1] افزایش می یابد. راهاندازی اکشنها، سازندگان اکشنها و منطق reducer را با موفقیت به پایان رساندیم. ما آماده هستیم و اکنون می توانیم از همه این ها در کامپوننت CanvasBoard خود استفاده کنیم.
ابتدا، اجازه دهید یک هوک useEffect را در کامپوننت CanvasBoard خود اضافه کنیم. ما از این هوک برای پیوست کردن/افزودن یک کنترل کننده رویداد استفاده خواهیم کرد. این کنترل کننده رویداد به فشار کلید رویداد متصل می شود. ما از این رویداد استفاده می کنیم زیرا هر زمان که کلیدهای w a s d را فشار می دهیم باید بتوانیم حرکت مار را کنترل کنیم.
useEffect ما چیزی شبیه به زیر خواهد بود:
useEffect(() => { window.addEventListener("keypress", handleKeyEvents); return () => { window.removeEventListener("keypress", handleKeyEvents); }; }, [disallowedDirection, handleKeyEvents]);
به روش زیر کار می کند:
بیایید نگاهی به نحوه ایجاد تماس handleKeyEvents بیندازیم. در زیر کد مربوط به همین مورد است:
const handleKeyEvents = useCallback( (event: KeyboardEvent) => { if (disallowedDirection) { switch (event.key) { case "w": moveSnake(0, -20, disallowedDirection); break; case "s": moveSnake(0, 20, disallowedDirection); break; case "a": moveSnake(-20, 0, disallowedDirection); break; case "d": event.preventDefault(); moveSnake(20, 0, disallowedDirection); break; } } else { if ( disallowedDirection !== "LEFT" && disallowedDirection !== "UP" && disallowedDirection !== "DOWN" && event.key === "d" ) moveSnake(20, 0, disallowedDirection); //Move RIGHT at start } }, [disallowedDirection, moveSnake] );
این تابع را با یک هوک useCallback نوشته ایم. به این دلیل که ما نسخه ذخیرهسازی شده این تابع را میخواهیم که در هر تغییر state (یعنی در تغییر DisallowedDirection و moveSnake) فراخوانی شود. این تابع با هر کلیدی که روی صفحه کلید فشار داده می شود فراخوانی می شود.
این تابع کار زیر را انجام می دهد:
توجه: در ابتدا مقدار DisallowedDirection یک رشته خالی است. به این ترتیب، می دانیم که اگر مقدار آن خالی باشد، بازی در حالت شروع است.
پس از شروع بازی، DisallowedDirection خالی نخواهد بود و سپس به تمام فشارهای صفحه کلید مانند w s و a گوش می دهد.
در نهایت، در هر فشار کلید، تابعی به نام moveSnake را فراخوانی می کنیم. در بخش بعدی به بررسی دقیق تر آن خواهیم پرداخت.
تابع moveSnake تابعی است که یک اکشن را به سازنده اکشن makeMove را ارسال می کند. این تابع سه آرگومان را می پذیرد:
کد تابع moveSnake به شکل زیر خواهد بود:
const moveSnake = useCallback( (dx = 0, dy = 0, ds: string) => { if (dx > 0 && dy === 0 && ds !== "RIGHT") { dispatch(makeMove(dx, dy, MOVE_RIGHT)); } if (dx < 0 && dy === 0 && ds !== "LEFT") { dispatch(makeMove(dx, dy, MOVE_LEFT)); } if (dx === 0 && dy < 0 && ds !== "UP") { dispatch(makeMove(dx, dy, MOVE_UP)); } if (dx === 0 && dy > 0 && ds !== "DOWN") { dispatch(makeMove(dx, dy, MOVE_DOWN)); } }, [dispatch] );
MoveSnake یک تابع ساده است که شرایط را بررسی می کند:
این مقدار DisallowedDirection در saga های ما مقداردهی شده است که در بخشهای بعدی این مقاله بیش تر در مورد آن صحبت خواهیم کرد. اگر اکنون تابع handleKeyEvents را دوباره بررسی کنیم، بسیار منطقی تر است. بیایید یک مثال را در اینجا مرور کنیم:
به این ترتیب حرکت مار را در جهت خاصی انجام می دهیم. حال بیایید نگاهی به saga هایی که استفاده کردهایم، و نحوه برخورد آن ها با حرکت مار بیاندازیم.
بیایید یک فایل به نام saga/index.ts ایجاد کنیم. این فایل شامل تمام saga های ما خواهد بود. این یک قانون نیست، اما به طور کلی، ما دو saga ایجاد می کنیم.
اولین مورد saga ای است که اکشن های واقعی را به store می فرستد.اجازه دهید این Saga را worker بنامیم. دوم حماسه ناظر است که به دنبال هر اقدامی است که در حال ارسال است - بیایید این Saga را watcher بنامیم.
اکنون باید یک ساگای watcher ایجاد کنیم که مراقب اکشن های زیر باشد: MOVE_RIGHT ،MOVE_LEFT ،MOVE_UP MOVE_DOWN.
function* watcherSaga() { yield takeLatest( [MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN], moveSaga ); }
این ساگا watcher کنش (اکشن) های بالا را مشاهده می کند و تابع moveSaga را که یک ساگا worker است اجرا می کند.
متوجه خواهید شد که ما از یک تابع جدید به نام takeLatest استفاده کرده ایم. اگر هر یک از اکشن های ذکر شده در آرگومان اول ارسال شود، این تابع ساگا watcher را فراخوانی می کند و هر فراخوانی ساگای قبلی را لغو می کند.
takeLatest(pattern, saga, ...args)
برای هر اکشنی که با الگوی مطابقت دارد به store ارسال میشود، saga ای ایجاد میکند و به طور خودکار هر saga قبلی را که قبلا شروع شده بود، در صورتی که هنوز در حال اجرا باشد، لغو می کند.
هر بار که یک اکشن به store فرستاده می شود، اگر این اکشن با الگو مطابقت داشته باشد، takeLatest یک ساگای جدید را در پسزمینه شروع میکند. اگر یک saga قبلا شروع شده باشد (در آخرین اکشنی که پیش از اکشن واقعی ارسال شده است)، و اگر این کار همچنان در حال اجرا باشد، کار لغو خواهد شد.
الگو (pattern): رشته | آرایه | تابع - برای اطلاعات بیشتر به این نشانی مراجعه کنید.
ساگا: تابع - یک تابع ژنراتور
args (آرایه): آرگومان هایی که باید به کار آغازین ارسال شوند. takeLatest اکشن ورودی را به لیست آرگومان اضافه می کند (یعنی اکشن آخرین آرگومان ارائه شده به saga خواهد بود)
حال بیایید یک ساگای worker به نام moveSaga ایجاد کنیم که در واقع اکشن ها را به Store Redux ارسال می کند:
export function* moveSaga(params: { type: string; payload: ISnakeCoord; }): Generator< | PutEffect<{ type: string; payload: ISnakeCoord }> | PutEffect<{ type: string; payload: string }> | CallEffect<true> > { while (true) { //dispatches movement actions yield put({ type: params.type.split("_")[1], payload: params.payload, }); //Dispatches SET_DIS_DIRECTION action switch (params.type.split("_")[1]) { case RIGHT: yield put(setDisDirection(LEFT)); break; case LEFT: yield put(setDisDirection(RIGHT)); break; case UP: yield put(setDisDirection(DOWN)); break; case DOWN: yield put(setDisDirection(UP)); break; } yield delay(100); } }
moveSaga کارهای زیر را انجام می دهد:
yield put({ type: params.type.split("_")[1], payload: params.payload, });
حالا بیایید این حماسه ها را به فایل sagas/index.ts خود اضافه کنیم:
import { CallEffect, delay, put, PutEffect, takeLatest } from "redux-saga/effects"; import { DOWN, ISnakeCoord, LEFT, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT, MOVE_UP, RIGHT, setDisDirection, UP } from "../actions"; export function* moveSaga(params: { type: string; payload: ISnakeCoord; }): Generator< | PutEffect<{ type: string; payload: ISnakeCoord }> | PutEffect<{ type: string; payload: string }> | CallEffect<true> > { while (true) { yield put({ type: params.type.split("_")[1], payload: params.payload, }); switch (params.type.split("_")[1]) { case RIGHT: yield put(setDisDirection(LEFT)); break; case LEFT: yield put(setDisDirection(RIGHT)); break; case UP: yield put(setDisDirection(DOWN)); break; case DOWN: yield put(setDisDirection(UP)); break; } yield delay(100); } } function* watcherSagas() { yield takeLatest( [MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN], moveSaga ); } export default watcherSagas;
اکنون بیایید کامپوننت CanvasBoard را برای هماهنگ شدن با این تغییرها ویرایش کنیم.
کامپوننت CanvasBoard با حرکت مار به روز می شود:
//Importing necessary modules import { useSelector } from "react-redux"; import { drawObject, generateRandomPosition } from "../utils"; export interface ICanvasBoard { height: number; width: number; } const CanvasBoard = ({ height, width }: ICanvasBoard) => { const canvasRef = useRef < HTMLCanvasElement | null > (null); const [context, setContext] = useState < CanvasRenderingContext2D | null > (null); const snake1 = useSelector((state: IGlobalState) => state.snake); const [pos, setPos] = useState < IObjectBody > ( generateRandomPosition(width - 20, height - 20) ); const moveSnake = useCallback( (dx = 0, dy = 0, ds: string) => { if (dx > 0 && dy === 0 && ds !== "RIGHT") { dispatch(makeMove(dx, dy, MOVE_RIGHT)); } if (dx < 0 && dy === 0 && ds !== "LEFT") { dispatch(makeMove(dx, dy, MOVE_LEFT)); } if (dx === 0 && dy < 0 && ds !== "UP") { dispatch(makeMove(dx, dy, MOVE_UP)); } if (dx === 0 && dy > 0 && ds !== "DOWN") { dispatch(makeMove(dx, dy, MOVE_DOWN)); } }, [dispatch] ); const handleKeyEvents = useCallback( (event: KeyboardEvent) => { if (disallowedDirection) { switch (event.key) { case "w": moveSnake(0, -20, disallowedDirection); break; case "s": moveSnake(0, 20, disallowedDirection); break; case "a": moveSnake(-20, 0, disallowedDirection); break; case "d": event.preventDefault(); moveSnake(20, 0, disallowedDirection); break; } } else { if ( disallowedDirection !== "LEFT" && disallowedDirection !== "UP" && disallowedDirection !== "DOWN" && event.key === "d" ) moveSnake(20, 0, disallowedDirection); //Move RIGHT at start } }, [disallowedDirection, moveSnake] ); useEffect(() => { //Draw on canvas each time setContext(canvasRef.current && canvasRef.current.getContext("2d")); //store in state variable clearBoard(context); drawObject(context, snake1, "#91C483"); //Draws snake at the required position }, [context]); useEffect(() => { window.addEventListener("keypress", handleKeyEvents); return () => { window.removeEventListener("keypress", handleKeyEvents); }; }, [disallowedDirection, handleKeyEvents]); return ( <canvas style={{ border: "3px solid black", }} height={height} width={width} /> ); };
هنگامی که این تغییرها را انجام دادید، می توانید حرکت مار را امتحان کنید. پس از اجرا خروجی زیر را خواهید دید:
برای رسم یک میوه در یک موقعیت تصادفی روی صفحه، از تابع generateRandomPosition استفاده می کنیم. بیایید نگاهی به این تابع بیندازیم:
function randomNumber(min: number, max: number) { let random = Math.random() * max; return random - (random % 20); } export const generateRandomPosition = (width: number, height: number) => { return { x: randomNumber(0, width), y: randomNumber(0, height), }; };
این تابعی است که مختصات تصادفی x و y را در مضرب 20 تولید می کند. این مختصات همیشه کمتر از عرض و ارتفاع صفحه خواهند بود. عرض و ارتفاع را به عنوان آرگومان می پذیرد.
وقتی این تابع را داشتیم، میتوانیم از آن برای کشیدن میوه در یک موقعیت تصادفی در داخل صفحه استفاده کنیم.
ابتدا، اجازه دهید یک متغیر حالت pos ایجاد کنیم که در ابتدا از یک موقعیت تصادفی تشکیل شده باشد.
const [pos, setPos] = useState<IObjectBody>(generateRandomPosition(width - 20, height - 20));
سپس، میوه را از طریق تابع drawObject میکشیم. پس از این، هوک useEffect خود را بهروزرسانی میکنیم:
useEffect(() => { //Draw on canvas each time setContext(canvasRef.current && canvasRef.current.getContext("2d")); //store in state variable clearBoard(context); drawObject(context, snake1, "#91C483"); //Draws snake at the required position drawObject(context, [pos], "#676FA3"); //Draws object randomly }, [context]);
پس از انجام تغییرها صفحه به شکل زیر خواهد بود:
امتیاز بازی بر اساس تعداد میوه هایی که مار بدون برخورد با خود یا با مرز جعبه خورده است محاسبه می شود. اگر مار میوه را مصرف کند، اندازه مار افزایش می یابد. اگر با لبه صفحه برخورد کند، بازی تمام می شود.
اکنون که می دانیم معیارهای ما برای محاسبه امتیاز چیست، بیایید نگاهی به نحوه محاسبه پاداش بیندازیم.
پاداش بعد از خوردن میوه توسط مار به صورت زیر محاسبه می شود:
اگر مار میوه را بخورد، باید اندازه مار را افزایش دهیم. این یک کار بسیار ساده است، ما فقط می توانیم مختصات x و y جدید را اضافه کنیم که کمتر از 20 از آخرین عنصر آرایه state مار هستند. به عنوان مثال، اگر مار مختصات زیر را داشته باشد:
{ snake: [ { x: 580, y: 300 }, { x: 560, y: 300 }, { x: 540, y: 300 }, { x: 520, y: 300 }, { x: 500, y: 300 }, ], }
ما باید به سادگی شی زیر را به آرایه snake اضافه کنیم: {x: 480, y: 280}
به این ترتیب اندازه مار را افزایش می دهیم و همچنین قسمت یا بلوک جدید را در انتهای آن اضافه می کنیم. برای این که این کار از طریق Redux و redux-saga پیاده سازی شود، به اکشن و سازنده اکشن زیر نیاز داریم:
export const INCREMENT_SCORE = "INCREMENT_SCORE"; //action export const increaseSnake = () => ({ //action creator type: INCREASE_SNAKE });
هم چنین Reducer را برای تطبیق با این تغییرها به روز خواهیم کرد. case زیر را اضافه می کنیم:
case INCREASE_SNAKE: const snakeLen = state.snake.length; return { ...state, snake: [ ...state.snake, { x: state.snake[snakeLen - 1].x - 20, y: state.snake[snakeLen - 1].y - 20, }, ], };
در کامپوننت CanvasBoard ابتدا یک متغیر حالت به نام isConsumed را معرفی می کنیم. این متغیر بررسی می کند که آیا میوه خورده شده است یا خیر.
const [isConsumed, setIsConsumed] = useState<boolean>(false);
در هوک useEffect خود که در آن مار و میوه خود را درست زیر آن می کشیم، شرط زیر را اضافه می کنیم:
//When the object is consumed if (snake1[0].x === pos?.x && snake1[0].y === pos?.y) { setIsConsumed(true); }
شرط بالا بررسی می کند که آیا سر مار snake[0] با pos یا موقعیت میوه برابر است یا خیر. اگر true باشد، متغیر isConsumed با true مقداردهی می شود.
پس از خوردن میوه، باید اندازه مار را افزایش دهیم. ما می توانیم این کار را به راحتی از طریق یک useEffect دیگر انجام دهیم. بیایید useEffect دیگری ایجاد کنیم و increaseSnake را فراخوانی کنیم:
//useEffect2 useEffect(() => { if (isConsumed) { //Increase snake size when object is consumed successfully dispatch(increaseSnake()); } }, [isConsumed]);
اکنون که اندازه مار را افزایش دادهایم، بیایید نگاهی بیندازیم که چگونه میتوانیم امتیاز را بهروزرسانی کنیم و یک میوه جدید در موقعیت تصادفی دیگری تولید کنیم.
برای تولید یک میوه جدید در یک موقعیت تصادفی دیگر، متغیر pos را به روز می کنیم که useEffect1 را دوباره اجرا می کند و شی را در pos می کشد. ما باید useEffect1 خود را با وابستگی جدید pos به روز کنیم و useEffect2 را به صورت زیر به روز کنیم:
useEffect(() => { //Generate new object if (isConsumed) { const posi = generateRandomPosition(width - 20, height - 20); setPos(posi); setIsConsumed(false); //Increase snake size when object is consumed successfully dispatch(increaseSnake()); } }, [isConsumed, pos, height, width, dispatch]);
آخرین کاری که باید در این سیستم پاداش انجام دهید این است که هر بار که مار میوه را می خورد، امتیاز را به روز کنید. برای انجام این کار مراحل زیر را دنبال می کنیم:
export interface IGlobalState { snake: ISnakeCoord[] | []; disallowedDirection: string; score: number; } const globalState: IGlobalState = { snake: [ { x: 580, y: 300 }, { x: 560, y: 300 }, { x: 540, y: 300 }, { x: 520, y: 300 }, { x: 500, y: 300 }, ], disallowedDirection: "", score: 0, };
export const INCREMENT_SCORE = "INCREMENT_SCORE"; //action //action creator: export const scoreUpdates = (type: string) => ({ type });
case INCREMENT_SCORE: return { ...state, score: state.score + 1, };
useEffect(() => { //Generate new object if (isConsumed) { const posi = generateRandomPosition(width - 20, height - 20); setPos(posi); setIsConsumed(false); //Increase snake size when object is consumed successfully dispatch(increaseSnake()); //Increment the score dispatch(scoreUpdates(INCREMENT_SCORE)); } }, [isConsumed, pos, height, width, dispatch]);
import { Heading } from "@chakra-ui/react"; import { useSelector } from "react-redux"; import { IGlobalState } from "../store/reducers"; const ScoreCard = () => { const score = useSelector((state: IGlobalState) => state.score); return ( <Heading as="h2" size="md" mt={5} mb={5}>Current Score: {score}</Heading> ); } export default ScoreCard;
پس از این، باید کامپوننت ScoreCard را نیز به فایل App.tsx اضافه کنیم تا در صفحه ما نمایش داده شود.
import { ChakraProvider, Container, Heading } from "@chakra-ui/react"; import { Provider } from "react-redux"; import CanvasBoard from "./components/CanvasBoard"; import ScoreCard from "./components/ScoreCard"; import store from "./store"; const App = () => { return ( <Provider store={store}> <ChakraProvider> <Container maxW="container.lg" centerContent> <Heading as="h1" size="xl">SNAKE GAME</Heading> <ScoreCard /> <CanvasBoard height={600} width={1000} /> </Container> </ChakraProvider> </Provider> ); }; export default App;
هنگامی که همه چیز در جای خود قرار گرفت، مار ما یک سیستم پاداش کامل خواهد داشت که اندازه مار را برای به روز رسانی امتیاز افزایش می دهد.
بازیکن بازی می کند و طول مار و امتیاز بازیکن افزایش می یابد.
در این بخش، قصد داریم به نحوه پیاده سازی تشخیص برخورد برای بازی Snake خود نگاهی بیندازیم.
در بازی Snake، اگر برخوردی تشخیص داده شود، بازی تمام می شود یعنی بازی متوقف می شود. دو شرط برای وقوع برخورد وجود دارد:
بیایید نگاهی به شرط اول بیندازیم. فرض کنید سر مار مرزهای صفحه را لمس می کند. در این صورت ما بلافاصله بازی را متوقف خواهیم کرد.
برای این که این اثر در بازی ما گنجانده شود، باید کارهای زیر را انجام دهیم:
export const STOP_GAME = "STOP_GAME"; //action //action creator export const stopGame = () => ({ type: STOP_GAME });
export function* moveSaga(params: { type: string; payload: ISnakeCoord; }): Generator< | PutEffect<{ type: string; payload: ISnakeCoord }> | PutEffect<{ type: string; payload: string }> | CallEffect<true> > { while (params.type !== STOP_GAME) { yield put({ type: params.type.split("_")[1], payload: params.payload, }); switch (params.type.split("_")[1]) { case RIGHT: yield put(setDisDirection(LEFT)); break; case LEFT: yield put(setDisDirection(RIGHT)); break; case UP: yield put(setDisDirection(DOWN)); break; case DOWN: yield put(setDisDirection(UP)); break; } yield delay(100); } } function* watcherSagas() { yield takeLatest( [MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN, STOP_GAME], moveSaga ); }
if ( //Checks if the snake head is out of the boundries of the obox snake1[0].x >= width || snake1[0].x <= 0 || snake1[0].y <= 0 || snake1[0].y >= height ) { setGameEnded(true); dispatch(stopGame()); window.removeEventListener("keypress", handleKeyEvents); }
هم چنین شنونده رویداد handleKeyEvents را حذف می کنیم. این اطمینان حاصل می کند که پس از پایان بازی، بازیکن نمی تواند مار را حرکت دهد.
در نهایت، بیایید نگاهی به چگونگی تشخیص برخورد خود مار بیندازیم. ما قصد داریم از یک تابع ابزار به نام hasSnakeCollided استفاده کنیم. دو پارامتر را می پذیرد: اولی آرایه snake و دومی سر مار است. اگر سر مار به قسمتهایی از خودش برخورد کند، true یا false برمیگردد.تابع hasSnakeCollided به شکل زیر خواهد بود:
export const hasSnakeCollided = ( snake: IObjectBody[], currentHeadPos: IObjectBody ) => { let flag = false; snake.forEach((pos: IObjectBody, index: number) => { if ( pos.x === currentHeadPos.x && pos.y === currentHeadPos.y && index !== 0 ) { flag = true; } }); return flag; };
ممکن است نیاز داشته باشیم که useEffect1 را با بهروزرسانی شرایط تشخیص برخورد مانند زیر بهروزرسانی کنیم:
if ( //Checks if the snake has collided with itself hasSnakeCollided(snake1, snake1[0]) || //Checks if the snake head is out of the boundries of the obox snake1[0].x >= width || snake1[0].x <= 0 || snake1[0].y <= 0 || snake1[0].y >= height ) { setGameEnded(true); dispatch(stopGame()); window.removeEventListener("keypress", handleKeyEvents); }
هنگامی که سیستم تشخیص برخورد را اضافه کنیم، بازی ما مانند زیر خواهد بود:
در حال حاضر به پایان بازی نزدیک شده ایم. کامپوننت نهایی ما Instruction خواهد بود. این شامل دستورالعمل هایی در مورد بازی مانند شرایط اولیه بازی، کلیدهای use و دکمه reset است.
بیایید با ایجاد فایلی به نام components/Instructions.tsx شروع کنیم. کد زیر را در این فایل قرار دهید:
import { Box, Button, Flex, Heading, Kbd } from "@chakra-ui/react"; export interface IInstructionProps { resetBoard: () => void; } const Instruction = ({ resetBoard }: IInstructionProps) => ( <Box mt={3}> <Heading as="h6" size="lg"> How to Play </Heading> <Heading as="h5" size="sm" mt={1}> NOTE: Start the game by pressing <Kbd>d</Kbd> </Heading> <Flex flexDirection="row" mt={3}> <Flex flexDirection={"column"}> <span> <Kbd>w</Kbd> Move Up </span> <span> <Kbd>a</Kbd> Move Left </span> <span> <Kbd>s</Kbd> Move Down </span> <span> <Kbd>d</Kbd> Move Right </span> </Flex> <Flex flexDirection="column"> <Button onClick={() => resetBoard()}>Reset game</Button> </Flex> </Flex> </Box> ); export default Instruction;
کامپوننت Instruction متغیر resetBoard را به عنوان یک prop می پذیرد و تابعی است که به کاربر در زمانی که بازی تمام می شود یا زمانی که می خواهد بازی را reset کند کمک می کند.
پیش از این که وارد تابع resetBoard شویم، باید بهروزرسانیهای زیر را در store Redux و saga خود انجام دهیم:
export const RESET_SCORE = "RESET_SCORE"; //action export const RESET = "RESET"; //action //Action creator: export const resetGame = () => ({ type: RESET });
export function* moveSaga(params: { type: string; payload: ISnakeCoord; }): Generator< | PutEffect<{ type: string; payload: ISnakeCoord }> | PutEffect<{ type: string; payload: string }> | CallEffect<true> > { while (params.type !== RESET && params.type !== STOP_GAME) { yield put({ type: params.type.split("_")[1], payload: params.payload, }); switch (params.type.split("_")[1]) { case RIGHT: yield put(setDisDirection(LEFT)); break; case LEFT: yield put(setDisDirection(RIGHT)); break; case UP: yield put(setDisDirection(DOWN)); break; case DOWN: yield put(setDisDirection(UP)); break; } yield delay(100); } } function* watcherSagas() { yield takeLatest( [MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN, RESET, STOP_GAME], moveSaga ); }
case RESET_SCORE: return { ...state, score: 0 };
هنگامی که sage ها و reducer های ما به روز می شوند، می توانیم نگاهی به عملیاتی که ResetBoard انجام می دهد بیندازیم.
تابع resetBoard عملیات زیر را انجام می دهد:
در زیر نحوه تابع resetBoard نشان داده شده است:
const resetBoard = useCallback(() => { window.removeEventListener("keypress", handleKeyEvents); dispatch(resetGame()); dispatch(scoreUpdates(RESET_SCORE)); clearBoard(context); drawObject(context, snake1, "#91C483"); drawObject( context, [generateRandomPosition(width - 20, height - 20)], "#676FA3" ); //Draws object randomly window.addEventListener("keypress", handleKeyEvents); }, [context, dispatch, handleKeyEvents, height, snake1, width]);
شما باید این تابع را در داخل کامپوننت CanvasBoard قرار دهید و تابع resetBoard را بهعنوان prop به تابع Instruction به صورت زیر بفرستید:
<> <canvas ref={canvasRef} style={{ border: `3px solid ${gameEnded ? "red" : "black"}`, }} width={width} height={height} /> <Instruction resetBoard={resetBoard} /> </>
پس از قرار دادن این، کامپوننت Instruction را مانند زیر تنظیم خواهیم کرد:
اگر تا این مرحله دنبال کرده اید، تبریک می گوییم! شما با موفقیت یک بازی سرگرم کننده Snake را با React ،Redux و redux-sagas ایجاد کرده اید. پس از اتصال همه این موارد، بازی شما به شکل زیر خواهد بود:
کد کامل بازی در این نشانی آمده است.
منبع: وب سایت freecodecamp
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.