الدليل النهائي للغة المعالجة الجزء الثاني: بناء لعبة بسيطة
نشرت: 2022-03-11هذا هو الجزء الثاني من الدليل النهائي للغة المعالجة. في الجزء الأول ، قدمت شرحًا تفصيليًا للغة المعالجة الأساسية. الخطوة التالية لتعلم المعالجة هي ببساطة المزيد من البرمجة العملية.
في هذه المقالة ، سأوضح لك كيفية استخدام المعالجة لتنفيذ لعبتك الخاصة ، خطوة بخطوة. سيتم شرح كل خطوة بالتفصيل. بعد ذلك ، سننقل اللعبة إلى الويب.
قبل أن نبدأ البرنامج التعليمي "المعالجة" ، إليك رمز تمرين شعار DVD من الجزء السابق. إذا كان لديك أي أسئلة ، فتأكد من ترك تعليق.
درس المعالجة: لعبة بسيطة
اللعبة التي سنبنيها في هذا البرنامج التعليمي للمعالجة هي نوع من مزيج من Flappy Bird و Pong و Brick Breaker. السبب في أنني اخترت لعبة مثل هذه هو أنها تحتوي على معظم المفاهيم التي يعاني منها المبتدئين عند تعلم تطوير اللعبة. يعتمد هذا على تجربتي منذ أن كنت مساعد تدريس ، لمساعدة المبرمجين الجدد على تعلم كيفية استخدام المعالجة. تتضمن هذه المفاهيم الجاذبية والاصطدامات وحفظ النتائج والتعامل مع الشاشات المختلفة وتفاعلات لوحة المفاتيح / الماوس. يحتوي Flappy Pong على كل منهم.
العب اللعبة الآن!
بدون استخدام مفاهيم البرمجة الموجهة للكائنات (OOP) ، ليس من السهل إنشاء ألعاب معقدة ، مثل ألعاب المنصات ذات المستويات المتعددة واللاعبين والكيانات وما إلى ذلك. بذلت قصارى جهدي للحفاظ على تنظيم وبساطة البرنامج التعليمي للمعالجة.
أنصحك بمتابعة المقال ، والحصول على الكود الكامل ، واللعب به بنفسك ، والبدء في التفكير في لعبتك الخاصة في أسرع وقت ممكن ، والبدء في تنفيذها.
فلنبدأ.
بناء فلابي بونغ
الخطوة التعليمية للمعالجة # 1: تهيئة ومعالجة الشاشات المختلفة
الخطوة الأولى هي بدء مشروعنا. بالنسبة للمبتدئين ، سنكتب الإعداد الخاص بنا ونرسم الكتل كالمعتاد ، لا شيء خيالي أو جديد. بعد ذلك ، سنتعامل مع الشاشات المختلفة (الشاشة الأولية ، شاشة اللعبة ، اللعبة على الشاشة ، إلخ). لذا فإن السؤال الذي يطرح نفسه ، كيف نجعل المعالجة تظهر الصفحة الصحيحة في الوقت الصحيح؟
إنجاز هذه المهمة بسيط إلى حد ما. سيكون لدينا متغير عام يخزن معلومات الشاشة النشطة حاليًا. ثم نرسم محتويات الشاشة الصحيحة اعتمادًا على المتغير. في كتلة الرسم ، سيكون لدينا عبارة if التي تتحقق من المتغير وتعرض محتويات الشاشة وفقًا لذلك. عندما نريد تغيير الشاشة ، سنقوم بتغيير هذا المتغير إلى معرّف الشاشة الذي نريده أن يظهر. مع ذلك ، إليك ما يبدو عليه رمز الهيكل العظمي الخاص بنا:
/********* VARIABLES *********/ // We control which screen is active by settings / updating // gameScreen variable. We display the correct screen according // to the value of this variable. // // 0: Initial Screen // 1: Game Screen // 2: Game-over Screen int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // Display the contents of the current screen if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // codes of initial screen } void gameScreen() { // codes of game screen } void gameOverScreen() { // codes for game over screen } /********* INPUTS *********/ public void mousePressed() { // if we are on the initial screen when clicked, start the game if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // This method sets the necessary variables to start the game void startGame() { gameScreen=1; }
قد يبدو هذا مخيفًا في البداية ، ولكن كل ما فعلناه هو بناء الهيكل الأساسي وفصل الأجزاء المختلفة بكتل التعليقات.
كما ترى ، نحدد طريقة مختلفة لعرض كل شاشة. في كتلة السحب الخاصة بنا ، نتحقق ببساطة من قيمة متغير gameScreen
الخاص بنا ، واستدعاء الطريقة المقابلة.
في الجزء void mousePressed(){...}
، نستمع إلى نقرات الماوس وإذا كانت الشاشة النشطة هي 0 ، الشاشة الأولية ، فإننا نسمي طريقة startGame()
التي تبدأ اللعبة كما تتوقع. يغير السطر الأول من هذه الطريقة متغير gameScreen
إلى 1 ، شاشة اللعبة.
إذا كان هذا مفهوما ، فإن الخطوة التالية هي تنفيذ شاشتنا الأولية. للقيام بذلك ، سنقوم بتحرير طريقة initScreen()
. من هنا تبدأ:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
الآن شاشتنا الأولية لها خلفية سوداء ونص بسيط ، "انقر للبدء" ، يقع في المنتصف ومحاذاة للمركز. ولكن عندما نضغط ، لا يحدث شيء. لم نحدد بعد أي محتوى لشاشة لعبتنا. لا تحتوي طريقة gameScreen()
على أي شيء ، لذا فنحن لا نغطي المحتويات السابقة المأخوذة من الشاشة الأخيرة (النص) من خلال وجود background()
كسطر أول للرسم. لهذا السبب لا يزال النص موجودًا ، على الرغم من عدم استدعاء سطر text()
بعد الآن (تمامًا مثل مثال الكرة المتحركة من الجزء الأخير الذي كان يترك أثراً خلفه) . الخلفية لا تزال سوداء لنفس السبب. لذلك دعونا نمضي قدمًا ونبدأ في تنفيذ شاشة اللعبة.
void gameScreen() { background(255); }
بعد هذا التغيير ، ستلاحظ أن الخلفية تتحول إلى اللون الأبيض ويختفي النص.
الخطوة التعليمية للمعالجة # 2: إنشاء الكرة وتنفيذ الجاذبية
الآن ، سنبدأ العمل على شاشة اللعبة. سننشئ كرتنا أولاً. يجب أن نحدد متغيرات لإحداثياته ولونه وحجمه لأننا قد نرغب في تغيير هذه القيم لاحقًا. على سبيل المثال ، إذا أردنا زيادة حجم الكرة حيث يسجل اللاعب أعلى بحيث تكون اللعبة أكثر صعوبة. سنحتاج إلى تغيير حجمه ، لذلك يجب أن يكون متغيرًا. سنحدد سرعة الكرة أيضًا بعد تطبيق الجاذبية.
أولاً ، دعنا نضيف ما يلي:
... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); }
حددنا الإحداثيات على أنها متغيرات عالمية ، وأنشأنا طريقة لرسم الكرة ، تسمى من طريقة gameScreen . الشيء الوحيد الذي يجب الانتباه إليه هنا هو أننا قمنا بتهيئة الإحداثيات ، لكننا حددناها في setup()
. سبب قيامنا بذلك هو أننا أردنا أن تبدأ الكرة عند ربع من اليسار وخمس من أعلى. لا يوجد سبب معين وراء رغبتنا في ذلك ، لكن هذه نقطة جيدة لبدء الكرة. لذلك احتجنا إلى تحديد width
الرسم height
ديناميكيًا. يتم تحديد حجم الرسم في setup()
، بعد السطر الأول. لم يتم تعيين width
height
قبل تشغيل setup()
، ولهذا السبب لم نتمكن من تحقيق ذلك إذا حددنا المتغيرات في الأعلى.
الجاذبية
الآن تنفيذ الجاذبية هو في الواقع الجزء السهل. لا يوجد سوى القليل من الحيل. هنا هو التنفيذ أولا:
... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // keep ball in the screen void keepInScreen() { // ball hits floor if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // ball hits ceiling if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } }
والنتيجة هي:
امسك خيولك أيها الفيزيائي. أعلم أن هذه ليست طريقة عمل الجاذبية في الحياة الواقعية. بدلاً من ذلك ، هذه عملية رسوم متحركة أكثر من أي شيء آخر. المتغير الذي حددناه على أنه gravity
هو مجرد قيمة عددية - عدد float
بحيث يمكننا استخدام القيم العشرية ، وليس مجرد أعداد صحيحة - نضيفها إلى ballSpeedVert
في كل حلقة. و ballSpeedVert
هي السرعة الرأسية للكرة ، والتي تضاف إلى إحداثي Y للكرة (الكرة ballY
) في كل حلقة. نشاهد إحداثيات الكرة ونتأكد من بقائها في الشاشة. إذا لم نفعل ذلك ، فسوف تسقط الكرة إلى ما لا نهاية. في الوقت الحالي ، تتحرك كرتنا عموديًا فقط. لذلك نشاهد حدود الأرضية والسقف للشاشة. باستخدام keepInScreen()
ballY
نتحقق مما إذا كانت الكرة Y ( + نصف القطر) أقل من height
، وبالمثل ballY
( - نصف القطر) أكبر من 0
. إذا لم تستوف الشروط ، نجعل الكرة ترتد (من الأسفل أو الأعلى) باستخدام makeBounceBottom()
و makeBounceTop()
. لجعل الكرة ترتد ، نقوم ببساطة بتحريك الكرة إلى المكان المحدد حيث كان عليها أن ترتد ونضرب السرعة الرأسية ( ballSpeedVert
) في -1
(الضرب بـ -1 يغير العلامة). عندما تحتوي قيمة السرعة على علامة الطرح ، فإن إضافة إحداثي Y تصبح السرعة ballY + (-ballSpeedVert)
، وهي ballY - ballSpeedVert
. لذا فإن الكرة تغير اتجاهها على الفور بنفس السرعة. بعد ذلك ، عندما نضيف gravity
إلى ballSpeedVert
و ballSpeedVert
لها قيمة سالبة ، تبدأ في الاقتراب من 0
، وتصبح في النهاية 0
، وتبدأ في الزيادة مرة أخرى. هذا يجعل الكرة ترتفع وترتفع أبطأ وتتوقف وتبدأ في السقوط.
هناك مشكلة في عملية الرسوم المتحركة لدينا ، على الرغم من أن الكرة تستمر في الارتداد. إذا كان هذا سيناريو واقعي ، فستواجه الكرة مقاومة الهواء والاحتكاك في كل مرة تلمس فيها سطحًا. هذا هو السلوك الذي نريده لعملية الرسوم المتحركة في لعبتنا ، لذا فإن تنفيذ ذلك أمر سهل. نضيف ما يلي:
... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); }
والآن تنتج عملية الرسوم المتحركة لدينا ما يلي:
كما يوحي الاسم ، فإن friction
هو الاحتكاك السطحي airfriction
هو احتكاك الهواء. لذلك من الواضح أن friction
يجب أن يطبق في كل مرة تلمس فيها الكرة أي سطح. airfriction
ذلك ، يجب أن تطبق قيود الهواء باستمرار. اذا هذا ما فعلناه. تعمل طريقة applyGravity()
على كل حلقة ، لذلك نطرد 0.0001
بالمائة من قيمتها الحالية من ballSpeedVert
في كل حلقة. makeBounceBottom()
و makeBounceTop()
عندما تلمس الكرة أي سطح. بهذه الأساليب ، فعلنا نفس الشيء ، هذه المرة فقط مع friction
.
الخطوة التعليمية للمعالجة # 3: إنشاء المضرب
الآن نحن بحاجة إلى مضرب حتى ترتد الكرة. يجب أن نتحكم في المضرب. دعونا نجعلها قابلة للتحكم بالماوس. ها هو الكود:
... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }
لقد حددنا لون وعرض وارتفاع المضرب كمتغير عالمي ، وقد نرغب في تغييره أثناء اللعب. قمنا بتنفيذ طريقة drawRacket()
التي تقوم بما يوحي به اسمها. قمنا بتعيين وضع المستقيم على المركز ، بحيث يكون rectMode
محاذيًا لمركز المؤشر.
الآن وقد صنعنا المضرب ، علينا أن نجعل الكرة ترتد عليه.
... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // racket moving up if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } }
وها هي النتيجة:
إذن ما تفعله watchRacketBounce()
هو التأكد من اصطدام المضرب والكرة. هناك شيئان يجب التحقق منهما هنا ، وهما ما إذا كانت الكرة والمضرب يصطفان عموديًا وأفقيًا. تتحقق العبارة الأولى إذا كان إحداثي X للجانب الأيمن من الكرة أكبر من إحداثي X للجانب الأيسر من المضرب (والعكس صحيح). إذا كان الأمر كذلك ، فإن العبارة الثانية تتحقق مما إذا كانت المسافة بين الكرة والمضرب أصغر من أو تساوي نصف قطر الكرة (مما يعني أنهما تصطدمان) . لذلك إذا استوفت هذه الشروط ، يتم استدعاء طريقة makeBounceBottom()
وترتد الكرة على mouseY
(عند الماوس Y ، حيث يوجد المضرب).
هل لاحظت متغير overhead
الذي يتم حسابه بواسطة mouseY - pmouseY
؟ يقوم المتغيران pmouseX
و pmouseY
بتخزين إحداثيات الماوس في الإطار السابق. نظرًا لأن الماوس يتحرك بسرعة كبيرة ، فهناك فرصة جيدة لأننا قد لا نكتشف المسافة بين الكرة والمضرب بشكل صحيح بين الإطارات إذا كان الماوس يتحرك نحو الكرة بسرعة كافية. لذلك ، نأخذ الفرق في إحداثيات الماوس بين الإطارات ونأخذ ذلك في الاعتبار أثناء اكتشاف المسافة. كلما كان الماوس يتحرك بشكل أسرع ، كلما زادت المسافة المقبولة.
نستخدم أيضًا overhead
لسبب آخر. نكتشف الطريقة التي يتحرك بها الماوس عن طريق التحقق من علامة overhead
. إذا كان الحمل الزائد سالبًا ، فإن الماوس كان في مكان ما أدناه في الإطار السابق لذلك يتحرك الماوس (المضرب) لأعلى. في هذه الحالة ، نريد إضافة سرعة إضافية للكرة وتحريكها أبعد قليلاً من الارتداد العادي لمحاكاة تأثير ضرب الكرة بالمضرب. إذا كانت overhead
أقل من 0
، نضيفها إلى ballY
و ballSpeedVert
لجعل الكرة تتحرك أعلى وأسرع. لذلك كلما كان المضرب يضرب الكرة بشكل أسرع ، كلما ارتفع إلى أعلى وأسرع.
معالجة الخطوة رقم 4: الحركة الأفقية والتحكم في الكرة
في هذا القسم ، نضيف الحركة الأفقية للكرة. بعد ذلك ، سوف نجعل من الممكن التحكم في الكرة أفقيًا باستخدام مضربنا. ها نحن:
... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }
والنتيجة هي:
الفكرة هنا هي نفسها ما فعلناه للحركة العمودية. أنشأنا متغير سرعة أفقي ، ballSpeedHorizon
. أنشأنا طريقة لتطبيق السرعة الأفقية على ballX
وإزالة احتكاك الهواء. أضفنا عبارتين أخريين إلى طريقة keepInScreen()
التي ستراقب الكرة وهي تضرب الحواف اليمنى واليسرى للشاشة. أخيرًا أنشأنا makeBounceLeft()
و makeBounceRight()
للتعامل مع الارتداد من اليسار واليمين.
الآن بعد أن أضفنا السرعة الأفقية إلى اللعبة ، نريد التحكم في الكرة بالمضرب. كما هو الحال في لعبة أتاري الشهيرة Breakout وفي جميع ألعاب كسر الطوب الأخرى ، يجب أن تتجه الكرة يسارًا أو يمينًا وفقًا للنقطة التي تصطدم بها المضرب. يجب أن تمنح حواف المضرب الكرة سرعة أفقية أكبر بينما لا ينبغي أن يكون للوسط أي تأثير. الكود أولا:

void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } }
النتيجه هي:
أدت إضافة هذا الخط البسيط إلى watchRacketBounce()
المهمة. ما فعلناه هو أننا حددنا مسافة النقطة التي تصطدم بها الكرة من مركز المضرب ballX - mouseX
ثم نجعلها هي السرعة الأفقية. كان الاختلاف الفعلي كبيرًا جدًا ، لذا فقد حاولت عدة محاولات واكتشفت أن عُشر القيمة تبدو طبيعية للغاية.
الخطوة التعليمية للمعالجة # 5: إنشاء الجدران
بدأ رسمنا يبدو وكأنه لعبة مع كل خطوة. في هذه الخطوة ، سنضيف جدرانًا تتحرك نحو اليسار ، تمامًا كما هو الحال في Flappy Bird:
... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // This arraylist stores data of the gaps between the walls. Actuals walls are drawn accordingly. // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // draw actual walls rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } }
ونتج عن ذلك:
على الرغم من أن الشفرة تبدو طويلة ومخيفة ، فإنني أعدك بأنه لا يوجد شيء يصعب فهمه. أول شيء يجب ملاحظته هو ArrayList
. بالنسبة لأولئك منكم الذين لا يعرفون ما هي ArrayList
، فهي مجرد تنفيذ لقائمة تعمل مثل Array ، لكن لها بعض المزايا عليها. يمكن تغيير حجمها ، ولها طرق مفيدة مثل list.add(index)
، list.get(index)
و list.remove(index)
. نحتفظ ببيانات الحائط كمصفوفات عدد صحيح داخل مصفوفات الصفوف. البيانات التي نحتفظ بها في المصفوفات هي للفجوة بين جدارين. تحتوي المصفوفات على القيم التالية:
[gap wall X, gap wall Y, gap wall width, gap wall height]
يتم رسم الجدران الفعلية بناءً على قيم جدار الفجوة. لاحظ أنه يمكن التعامل مع كل هذه الأشياء بشكل أفضل وأنظف باستخدام الفئات ، ولكن نظرًا لأن استخدام البرمجة الموجهة للكائنات (OOP) ليس في نطاق هذا البرنامج التعليمي للمعالجة ، فهذه هي الطريقة التي سنتعامل بها مع ذلك. لدينا طريقتان أساسيتان لإدارة الجدران ، wallAdder()
و wallHandler
.
تضيف طريقة wallAdder()
جدرانًا جديدة في كل wallInterval
بالميلي ثانية إلى المصمم. لدينا متغير lastAddTime
والذي يخزن الوقت الذي تمت فيه إضافة الجدار الأخير (بالمللي ثانية) . إذا كان الملي ثانية الحالي millis()
مطروحًا منه آخر ميلي ثانية مضافة ، lastAddTime
أكبر من فاصل قيمة wallInterval
، فهذا يعني أن الوقت قد حان الآن لإضافة جدار جديد. يتم بعد ذلك إنشاء متغيرات الفجوة العشوائية بناءً على المتغيرات العالمية المحددة في الأعلى. ثم يتم إضافة جدار جديد (مصفوفة عدد صحيح يخزن بيانات جدار الفجوة) إلى قائمة المصفوفات ويتم تعيين lastAddTime
على مللي ثانية millis()
الحالية.
wallHandler()
حلقات من خلال الجدران الحالية الموجودة في arraylist. ولكل عنصر في كل حلقة ، فإنه يستدعي wallRemover(i)
و wallMover(i)
و wallDrawer(i)
بواسطة قيمة مؤشر Arraylist. هذه الأساليب تفعل ما يوحي به اسمها. wallDrawer()
الجدران الفعلية بناءً على بيانات جدار الفجوة. إنه يأخذ مصفوفة بيانات الجدار من المصفوف ، ويستدعي طريقة rect()
لرسم الجدران إلى حيث ينبغي أن تكون بالفعل. تأخذ طريقة wallMover()
العنصر من قائمة المصفوفات ، وتغير موقعها X بناءً على المتغير العام wallSpeed
. أخيرًا ، يزيل wallRemover()
الجدران من Arraylist الخارجة من الشاشة. إذا لم نفعل ذلك ، فإن المعالجة ستعاملهم كما لو كانوا لا يزالون في الشاشة. وكان ذلك سيشكل خسارة كبيرة في الأداء. لذلك عند إزالة جدار من المصفوف ، لا يتم رسمه في الحلقات اللاحقة.
التحدي الأخير المتبقي هو اكتشاف التصادم بين الكرة والجدران.
void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // collides with upper wall } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // collides with lower wall } }
يتم استدعاء طريقة watchWallCollision()
لكل جدار في كل حلقة. نحصل على إحداثيات جدار الفجوة ، ونحسب إحداثيات الجدران الفعلية (أعلى وأسفل) ونتحقق مما إذا كانت إحداثيات الكرة تصطدم بالجدران.
الخطوة التعليمية للمعالجة # 6: الصحة والنتيجة
الآن بعد أن تمكنا من اكتشاف اصطدام الكرة والجدران ، يمكننا تحديد آليات اللعبة. بعد بعض ضبط اللعبة ، تمكنت من جعل اللعبة قابلة للعب إلى حد ما. لكن مع ذلك ، كان الأمر صعبًا للغاية. كان أول ما فكرت به في اللعبة هو جعلها مثل لعبة Flappy Bird ، عندما تلمس الكرة الجدران ، تنتهي اللعبة. لكن بعد ذلك أدركت أنه سيكون من المستحيل اللعب. إذن هذا ما اعتقدته:
يجب أن يكون هناك شريط صحي أعلى الكرة. يجب أن تفقد الكرة صحتها أثناء ملامستها للجدران. مع هذا المنطق ، ليس من المنطقي جعل الكرة ترتد من الجدران. لذلك عندما تكون الصحة 0 ، يجب أن تنتهي اللعبة ويجب أن ننتقل إلى اللعبة على الشاشة. حسنا هيا بنا:
int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // Make it borderless: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } }
وإليك طريقة تشغيل بسيطة:
أنشأنا متغيرًا عالميًا health
للحفاظ على صحة الكرة. ثم أنشأ طريقة drawHealthBar()
التي ترسم مستطيلين فوق الكرة. الأول هو شريط الصحة الأساسي ، والآخر هو الشريط النشط الذي يُظهر الصحة الحالية. عرض الثاني ديناميكي ، ويتم حسابه باستخدام healthBarWidth*(health/maxHealth)
، وهي نسبة صحتنا الحالية فيما يتعلق بعرض شريط الصحة. أخيرًا ، يتم تعيين ألوان التعبئة وفقًا لقيمة الصحة. أخيرًا وليس آخرًا ، الدرجات :
... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // added another value at the end of the array int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); }
كنا بحاجة إلى التسجيل عندما تمر الكرة بالحائط. لكننا نحتاج إلى إضافة درجة واحدة كحد أقصى لكل جدار. بمعنى ، إذا مرت الكرة على الحائط ثم عادت إلى الوراء وتمريرها مرة أخرى ، فلا ينبغي إضافة نتيجة أخرى. لتحقيق ذلك ، أضفنا متغيرًا آخر إلى مصفوفة جدار الفجوة داخل المصفوفة. يخزن المتغير الجديد 0
إذا لم تمر الكرة بعد ذلك الجدار و 1
إذا فعلت ذلك. بعد ذلك ، قمنا بتعديل طريقة watchWallCollision()
. أضفنا شرطًا يطلق طريقة score()
ونضع علامة على الحائط على أنه تم تمريره عندما تمر الكرة بجدار لم يتم تجاوزه من قبل.
نحن الآن قريبون جدًا من النهاية. آخر شيء يجب القيام به هو click to restart
على اللعبة على الشاشة. نحتاج إلى تعيين جميع المتغيرات التي استخدمناها على قيمتها الأولية ، وإعادة تشغيل اللعبة. ها هو:
... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; }
دعونا نضيف المزيد من الألوان.
هاهو! لدينا فلابي بونغ!
يمكن العثور على رمز لعبة المعالجة الكامل هنا.
نقل رمز اللعبة المعالجة إلى الويب باستخدام p5.js
p5.js هي مكتبة JavaScript ببنية مشابهة جدًا لتلك الخاصة بلغة برمجة المعالجة. إنها ليست مكتبة قادرة على تشغيل كود المعالجة الموجود ببساطة ؛ بدلاً من ذلك ، يتطلب p5.js كتابة كود JavaScript فعلي - على غرار منفذ JavaScript للمعالجة المعروف باسم Processing.js. مهمتنا هي تحويل كود المعالجة إلى JavaScript باستخدام واجهة برمجة التطبيقات p5.js. تحتوي المكتبة على مجموعة من الوظائف وبناء جملة مشابه للمعالجة ، وعلينا إجراء تغييرات معينة على الكود الخاص بنا لجعلها تعمل في JavaScript - ولكن نظرًا لأن كل من المعالجة وجافا سكريبت تشتركان في أوجه التشابه مع Java ، فهي أقل قفزة مما تبدو . حتى لو لم تكن مطور JavaScript ، فإن التغييرات تافهة للغاية ويجب أن تكون قادرًا على المتابعة بشكل جيد.
بادئ ذي بدء ، نحتاج إلى إنشاء ملف index.html
بسيط وإضافة p5.min.js
إلى رأس الصفحة. نحتاج أيضًا إلى إنشاء ملف آخر يسمى flappy_pong.js
والذي سيضم كودنا المحول.
<html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html>
يجب أن تكون استراتيجيتنا أثناء تحويل الشفرة هي نسخ جميع الكود الخاص بنا ولصقه في flappy_pong.js
ثم إجراء جميع التغييرات. وهذا ما فعلته. وإليك الخطوات التي اتخذتها لتحديث الكود:
جافا سكريبت هي لغات غير نمطية (لا توجد بيانات نوع مثل
int
وfloat
). لذلك نحن بحاجة إلى تغيير كل تعريفات النوع إلىvar
.لا يوجد
void
في جافا سكريبت. يجب علينا تغيير كل شيءfunction
.نحتاج إلى إزالة بيانات نوع الوسيطات من تواقيع الوظيفة. (على سبيل المثال.
void wallMover(var index) {
tofunction wallMover(index) {
)لا توجد
ArrayList
في JavaScript. لكن يمكننا تحقيق نفس الشيء باستخدام مصفوفات JavaScript. نجري التغييرات التالية:-
ArrayList<int[]> walls = new ArrayList<int[]>();
var walls = [];
-
walls.clear();
علىwalls = [];
-
walls.add(randWall);
علىwalls.push(randWall);
-
walls.remove(index);
إلىwalls.splice(index,1);
-
walls.get(index);
علىwalls[index]
-
walls.size()
إلىwalls.length
-
تغيير تعريف المصفوفة
var randWall = {width, randY, wallWidth, randHeight, 0};
tovar randWall = [width, randY, wallWidth, randHeight, 0];
إزالة جميع الكلمات الرئيسية
public
.انقل جميع إعلانات
color(0)
إلىfunction setup()
لأنcolor()
لن يتم تعريفه قبل استدعاءsetup()
.size(500, 500);
createCanvas(500, 500);
أعد تسمية
function gameScreen(){
إلى شيء آخر مثلfunction gamePlayScreen(){
لأن لدينا بالفعل متغيرًا عامًا باسمgameScreen
. عندما كنا نعمل مع Processing ، كانت إحداهما دالة والأخرى كانت متغيرint
. لكن JavaScript يخلط بين هذه لأنها غير مطبوعة.الشيء نفسه ينطبق على
score()
. لقد أعدت تسميتهaddScore()
.
يمكن العثور على كود JavaScript الكامل الذي يغطي كل شيء في هذا البرنامج التعليمي للمعالجة هنا.
معالجة رمز اللعبة: يمكنك القيام بذلك أيضًا
في هذا البرنامج التعليمي للمعالجة ، حاولت شرح كيفية إنشاء لعبة بسيطة للغاية. ومع ذلك ، ما فعلناه في هذا المقال هو مجرد غيض من فيض. باستخدام لغة برمجة المعالجة ، يمكن تحقيق أي شيء تقريبًا. في رأيي ، إنها أفضل أداة لبرمجة ما تتخيله. كانت نيتي الفعلية من خلال هذا البرنامج التعليمي للمعالجة بدلاً من تدريس المعالجة وبناء لعبة ، لإثبات أن البرمجة ليست بهذه الصعوبة. بناء لعبتك الخاصة ليس مجرد حلم. أردت أن أوضح لك أنه بقليل من الجهد والحماس يمكنك فعل ذلك. آمل حقًا أن تلهم هاتان المقالتان الجميع لمنح البرمجة فرصة.