<?xml version="1.0"?>
<rss version="2.0"><channel><title>&#x627;&#x644;&#x628;&#x631;&#x645;&#x62C;&#x629;: &#x635;&#x646;&#x627;&#x639;&#x629; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628;</title><link>https://academy.hsoub.com/programming/game-development/?d=2</link><description>&#x627;&#x644;&#x628;&#x631;&#x645;&#x62C;&#x629;: &#x635;&#x646;&#x627;&#x639;&#x629; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628;</description><language>ar</language><item><title>&#x645;&#x631;&#x627;&#x62D;&#x644; &#x62A;&#x635;&#x645;&#x64A;&#x645; &#x627;&#x644;&#x645;&#x633;&#x62A;&#x648;&#x64A;&#x627;&#x62A; &#x641;&#x64A; &#x623;&#x644;&#x639;&#x627;&#x628; &#x627;&#x644;&#x641;&#x64A;&#x62F;&#x64A;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D9%85%D8%B1%D8%A7%D8%AD%D9%84-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D9%88%D9%8A%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-r2603/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2026_03/levels.png.4c6c4cbc9ab7b256da27452c9a854818.png" /></p>
<p>
	تعرفنا في <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AF%D8%AE%D9%84-%D8%A5%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B3%D8%AA%D9%88%D9%8A%D8%A7%D8%AA-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-r2602/" rel="">مقال مدخل إلى تصميم مستويات ألعاب الفيديو</a> على بعض المفاهيم الأساسية في تصميم مستويات الألعاب والمنظور الذي ينبغي أن يدركه مصمم الألعاب لفهم عملية التصميم، وسنشرح في هذا المقال الخطوات التي يمر بها المصمم أثناء تصميم مستوى داخل لعبة فيديو.
</p>

<p>
	تصميم مستويات الألعاب ثلاثية الأبعاد هو قلب تجربة اللعب، حيث تتحول الأفكار والمخططات إلى عوالم يمكن للاعبين التفاعل معها. تتطلب هذه العملية دمجًا متوازنًا بين التخطيط الفني والهندسي والسلوك التفاعلي للاعبين، لتقديم تجربة ممتعة ومتكاملة.
</p>

<p>
	تختلف احتياجات كل مشروع حسب طبيعة اللعبة، فالمشاريع الجماعية تحتاج إلى تركيز أكبر على التخطيط العام وتنسيق الفريق، بينما الألعاب القتالية تركز على موازنة الخرائط والمواجهات، والألعاب الفردية القصصية تحتاج إلى اهتمام أكبر بالحبكة والتفاصيل البصرية والبرمجية. بالتالي، يكتسب المصمم الخبرة تدريجيًا في تحديد المراحل الأكثر أهمية لكل مشروع وتخصيص الجهد بحسب طبيعة اللعبة وأهدافها.
</p>

<p>
	تمر معظم مشاريع تصميم المستويات ثلاثية الأبعاد في ألعاب الفيديو بعدة مراحل:
</p>

<ol>
	<li>
		<strong>ما قبل الإنتاج Pre-production</strong>. هي مرحلة وضع الخطوط العريضة وتصميم تجربة الاستخدام العامة.
	</li>
	<li>
		<strong>المعارك Combat</strong>. هذه مرحلة اختيارية خاصة بالألعاب القتالية، وفيها تُحدد طبيعة التفاعل بين اللاعبين وأنواع الأعداء التي ستكون في اللعبة.
	</li>
	<li>
		<strong>المخطط Layout</strong>. مرحلة رسم مخطط مرئي ثنائي الأبعاد من منظور علوي top view.
	</li>
	<li>
		<strong>النماذج الهيكلية Blockout</strong>. مرحلة بناء مسودة أولية ثلاثية الأبعاد للنماذج الهيكلية (الأجسام والمباني وغيرها) لاختبار قابليتها للعب.
	</li>
	<li>
		<strong>البرمجة النصية أو كتابة السكربتات Scripting</strong>. مرحلة ربط الأحداث بالسلوكيات الخاصة بها، وتشمل المهمات والأبواب -أسلوب فتحها مثلًا-، الأزرار ووظائفها، الذكاء الاصطناعي، إلخ، وقد كان هناك فرق كبير في الأجيال السابقة من محركات الألعاب بين شيفرة محرك اللعبة ومنطق السكربت أو البرمجة النصية في مستوى معين داخل اللعبة.
	</li>
	<li>
		<strong>الإضاءة</strong>. مرحلة توزيع مصادر الإضاءة للعناصر داخل اللعبة لوضوح رؤيتها ولإضفاء عمق واقعي عليها.
	</li>
	<li>
		<strong>فن البيئة Environment Art</strong>. هي مرحلة وضع اللمسة الفنية النهائية من خلال إضافة الأجسام والهياكل والأثاث وغيرها.
	</li>
	<li>
		<strong>الإطلاق Release</strong>. مرحلة توثيق المشروع والتسويق له ونشره.
	</li>
</ol>

<h2 id="-">
	تخصيص المراحل
</h2>

<p>
	لا توجد طريقة ثابتة لتصميم مستوى ما في لعبة، ذلك أن كل مشروع أو مرحلة يحتاج إلى عناصر مختلفة عن غيره، لذا ستجد نفسك كمصمم ألعاب تعدل وتطوّع تلك المراحل وفقًا لاحتياجاتك، ويُنصح أحيانًا بالشروع في هذه المراحل جميعها في نفس الوقت إذا كنت محتارًا في الترتيب المناسب لمشروعك أو حتى المراحل التي تحتاج إليها فيه، وستتعلم مع الوقت ما ينبغي التركيز عليه أو تجاوزه إلى غيره. للتوضيح، لننظر في حالات الألعاب التالية:
</p>

<h3 id="-">
	المشاريع الجماعية
</h3>

<p>
	تحتاج المشاريع الجماعية إلى تركيز أكبر على المرحلة الأولى -ما قبل الإنتاج، والتخطيط- متمثلًا في رسم المخططات العامة وتحديد الأنشطة والتحديات التي سيواجهها اللاعب في المستوى، فيما يعرف باسم Pacing، فغياب الصورة الواضحة لهذه المرحلة سيجعل من الصعب أن يتعاون المصممون أو العاملون على تصميم المستوى معًا.
</p>

<h3 id="-">
	ألعاب القتال أو خرائط الألعاب الجماعية
</h3>

<p>
	تحتاج ألعاب القتال combat games أو خرائط الألعاب الجماعية multiplayer maps إلى التركيز على إنشاء النماذج الهيكلية Blockouts من أجل إخراج متقن للمواجهات encounters التي يواجهها اللاعب، خاصة مع الشخصيات غير القابلة للعب Non playable characters (NPC)، وكذلك التركيز على موازنة الخرائط map balancing للتحكم في الخيارات المتاحة للاعب داخل المستوى، واختبارات مكثفة لتجربة اللعب نفسها playtests لمعرفة إن كان المستوى شيقًا أم مملًا، واضحًا أم محيرًا للاعب، وهكذا.
</p>

<h3 id="-">
	الألعاب الفردية القصصية
</h3>

<p>
	قد تواجه هذه الألعاب مشكلة في ضبط معايير الإخراج لها، فما الذي يحدد مثلًا إن كان الحبكة القصصية قد وصلت إلى مستوى يكفي لإخراج اللعبة بها، فهذا النوع من الألعاب وإن كان يركز بشكل أقل على مرحلة النماذج الهيكلية مثلًا إلا أنه يتطلب الكثير من الوقت والتجارب في مرحلتي البرمجة وفن البيئة.
</p>

<p>
	وستحتاج إذا تغيرت الحبكة داخل اللعبة إلى إعادة العمل من البداية على كل عناصرها، وقد تواجه صعوبة في إبقاء عناصر القصة متسقة مع بعضها في ظل التعديلات والتجارب التي تجريها، لذا تحتاج هذه أيضًا إلى تركيز عليها. سيظهر الفشل في هذه المراحل إذا شعر اللاعب أن قصة اللعبة ليست منطقية أو بها خلل في عناصرها. لننظر الآن في هذه المراحل بتفصيل أكبر.
</p>

<h2 id="-">
	مرحلة ما قبل الإنتاج
</h2>

<p>
	تدور مرحلة ما قبل الإنتاج حول وضع الشكل الأساسي للمشروع وحدوده العامة وأهداف التصميم والقيود التي تحكمها، وغالبًا ما يتجاهل المصمم المبتدئ هذه المرحلة -أي تخطيط المشروع- في حين يقع المصمم الخبير في فخ آخر حيث يستغرق في تخطيط مفرط أكثر من اللازم، وقد تصل مرحلة التخطيط للمشروع في استديوهات تصميم الألعاب الكبيرة إلى شهور أو سنوات حتى.
</p>

<p>
	تكتسب مرحلة التخطيط أهمية بالغة عند العمل في فريق لأنها التي توضع فيها الرؤية العامة للمشروع وتوقعاته، لكن تستطيع تجاوزها إن كنت تعمل وحدك كمصمم مستقل على مشروع صغير نسبيًا بعد أن تصف الخطوط العريضة للمشروع وتوقعاته وأهدافه، ويكون الشكل العام لهذه الخطوط إما وثائق نصية تشرح الرؤية العامة للمشروع، أو لوحات مهام إما حقيقية أو على تطبيقات مثل تريلّو Trello أو جايرا Jira، أو جداول بيانات Spreadsheets.
</p>

<h2 id="-">
	مرحلة المعارك (مرحلة اختيارية)
</h2>

<p>
	ننتقل الآن إلى مرحلة المعارك، سنحتاج إليها إذا كانت اللعبة قتالية أو فيها معارك بين اللاعبين أو بين اللاعبين والشخصيات غير القابلة للعب NPCs، نحتاج هنا أن نعرف الصورة التي ينبغي أن تكون عليها المعركة داخل اللعبة، من حيث مكوناتها واستعداد اللاعبين لها، وكيف نجعل المعارك متجددة وغير مكررة.
</p>

<p>
	تمثل هذه المرحلة الجانب الميكانيكي من اللعبة الذي يبث الحياة فيها، فالقتال أو المعارك ما هي إلا نظام كأي نظام أو آلية أخرى داخل اللعبة، وعليه يجب أن تحدد أهداف تجربة التصميم قبل أن تصمم مستويات اللعبة لهذه الأهداف، واعلم أن المعارك داخل اللعبة لن تكون بالمستوى الكافي من التشويق أو الإثارة والحبكة حتى ينهي المبرمجون والمصممون بناء عناصرها وجعلها واضحة بصريًا وقابلة للعب، أي أن عليك إتمام بناء اللعبة كلها كي تعرف كيف سيكون شعور القتال فيها.
</p>

<blockquote class="ipsQuote" data-gramm="false" data-ipsquote="">
	<div class="ipsQuote_citation">
		اقتباس
	</div>

	<div class="ipsQuote_contents ipsClearfix" data-gramm="false">
		<p>
			نظرًا للجهد الكبير الذي تحتاج إليه هذه المرحلة فإننا ننصح أن تبدأ بتعديل ألعاب قتال جاهزة، كي تكتسب خبرة عملية من خلال التدرب على أنظمة قتالية مجهزة ومختبرة مسبقًا.
		</p>
	</div>
</blockquote>

<p style="text-align: center;">
	<img alt="spreadsheet.png" class="ipsImage ipsImage_thumbnailed" data-fileid="181650" data-ratio="29.07" data-unique="p0st0ohvl" width="829" src="https://academy.hsoub.com/uploads/monthly_2026_03/spreadsheet.png.2c512c870dc73f2ce7fb79fcd4b97edc.png">
</p>

<p style="text-align: center;">
	صورة نشرها <a href="https://x.com/jasondeheras/status/1376005158656638977" rel="external nofollow">المصمم جيسون دي هيراس Jason de Hera</a> على تويتر توضح مصفوفة الحركات لشخصيات فالكيري في لعبة God Of War.
</p>

<h2 id="-layouts">
	مرحلة المخططات Layouts
</h2>

<p>
	يمثل المخطط هنا الهيكل الأساسي للمستوى في اللعبة، ويكون غالبًا رسمًا لمسطح ثنائي الأبعاد للأماكن والعناصر المركزية في المستوى، وقد يكفيك رسم تخطيط بسيط بيدك للمستوى إذا كنت تعمل على مشروع صغير، لكن المشاريع الكبيرة أو الجماعية تحتاج إلى تدقيق أكبر فيها حيث يكون من الصعب على زملائك في العمل قراءة أفكارك وما قصدته برسم يدك.
</p>

<p>
	تكون المخططات في العادة مساقط علوية ثنائية الأبعاد للمستوى، وإن كانت مساقط المنظور أو المخططات الأيزومترية Isometric شائعة كذلك -هذه المخططات ترسم المستوى من منظور أشبه بالرسم ثلاثي الأبعاد-، تذكر أن الهدف هنا ليس أن تصير فنانًا مبدعًا بل أن تستطيع إيصال فكرتك ورسالتك من المخطط الذي ترسمه، فالرسم هنا عبارة عن خطة بصرية تحدد أين يمكن للاعب أن يذهب، وما الذي يستطيع أن يفعله داخل المستوى.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181651" href="https://academy.hsoub.com/uploads/monthly_2026_03/layout.jpeg.65ee14bb2672b93071d0d5cd47249562.jpeg" rel=""><img alt="layout.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181651" data-ratio="75.72" data-unique="ssxa74prz" style="width: 692px; height: auto;" width="792" src="https://academy.hsoub.com/uploads/monthly_2026_03/layout.thumb.jpeg.6bfa01aa46ec44c55d35d7c466d52a2d.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة توضح رسمًا لخريطة Warpath متعددة اللاعبين في لعبة Team Fortress Classic، من رسم المصمم روبن ووكر Robin Walker.
</p>

<h2 id="-">
	مرحلة النماذج الهيكلية
</h2>

<p>
	النموذج الهيكلي هو مسودة أولية قابلة للعب داخل مستوى اللعبة يُبنى باستخدام أشكال هندسية ثلاثية الأبعاد تكون بسيطة وقليلة التفاصيل بغرض تجربة اللعب داخل محرك اللعبة لنعرف إن كان المستوى صغيرًا أم أكبر من اللازم، هل هو محير للاعب أم مشوق ومثير، هل تجربة اللعب متوازنة أم بها مشاكل تقنية، وهكذا.
</p>

<p>
	هذه المرحلة -أي اختبار اللعب- مهمة للغاية في ألعاب القتال أو أي لعبة يعتمد سلوك اللاعب فيها على بنية المستوى -بحيث قد يؤدي تغيير تصميم غرفة مثلًا إلى الحاجة لتغيير سلوك اللاعب في هذه المرحلة-، فإذا اكتشفنا أن أحد النماذج يحدث مشكلة أثناء اللعب -كأن يكون تصميم الغرفة معيقًا للعب في مثالنا هنا- نستطيع تعديله وتغييره وإعادة تجربته بتكلفة قليلة بما أن النموذج نفسه مكون من أشكال بسيطة غير معقدة.
</p>

<p>
	هذا يعني أن ملفات النماذج الهيكلية هي مستويات كاملة أو ملفات مشاهد من اللعبة scene files يمكن لعبها وتجربتها وتحميلها داخل محرك اللعبة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181652" href="https://academy.hsoub.com/uploads/monthly_2026_03/blockout.jpeg.417cfe92f0e9d5d1a8333ee25e1331ec.jpeg" rel=""><img alt="blockout.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181652" data-ratio="56.29" data-unique="pp0nnb8z1" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/blockout.thumb.jpeg.3b705a9ac3729e6616a881260bca83ce.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة لنموذج هيكلي لمستوى في لعبة Uncharted 4.
</p>

<h2 id="-">
	مرحلة البرمجة النصية
</h2>

<p>
	تدور مرحلة البرمجة النصية أو كتابة السكربتات حول دمج السلوكيات والأحداث ومنطق اللعبة معًا في مستوى اللعبة، وننصح هنا بالبدء ببرمجة سلوك الأزرار والعناصر القابلة للجمع collectables قبل النظر في الكائنات الأعقد، فصدق أو لا تصدق، تمثل برمجة الأبواب واحدة من أعقد المشاكل في تطوير الألعاب! وبرمجة المنصات أو الكائنات المتحركة مثل القطارات أشد تعقيدًا.
</p>

<p>
	تعتمد أهداف المهمات الكبيرة والتحديات الصغيرة داخل المستوى quests في الغالب على البرمجة النصية، وبالمثل في حالة المشاهد السينمائية cutscenes وتصميم المواجهات والمعارك والذكاء الاصطناعي، لكن لا تقلق إذا لم تكن متقنًا للبرمجة هنا إذ توفر معظم محركات الألعاب لغات نصية وأدوات لتبسيط العملية.
</p>

<blockquote class="ipsQuote" data-gramm="false" data-ipsquote="">
	<div class="ipsQuote_citation">
		اقتباس
	</div>

	<div class="ipsQuote_contents ipsClearfix" data-gramm="false">
		<p>
			يميل البعض في مجتمعات تصميم المستويات إلى التقليل من قيمة المبرمجين الذين يكتبون هذه السكربتات النصية رغم أن عملهم هو الذي يجعل المرحلة تنبض بالحياة وهو العنصر الحاسم لتحويل الخريطة الصماء إلى تجربة تفاعلية.
		</p>
	</div>
</blockquote>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181653" href="https://academy.hsoub.com/uploads/monthly_2026_03/door.jpeg.0fa9cb74486642ac13f505fee8d4394d.jpeg" rel=""><img alt="door.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181653" data-ratio="56.29" data-unique="s5sisb4ju" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/door.thumb.jpeg.f97cd1dbda09bb766c6570707e3cfa90.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة لنسخة أولية من لعبة Gone Home، لم تعمل الأبواب بالطريقة الصحيحة، من <a href="https://fullbright.company/2012/07/02/code-judo/" rel="external nofollow">مقالة Code Judo</a>.
</p>

<h2 id="-">
	مرحلة الإضاءة
</h2>

<p>
	تضيف الإضاءة ظلالًا وعمقًا إلى النماذج الهيكلية مما يساعد اللاعب على استيعاب الشكل الرئيسي للمستوى، فغياب الإضاءة المناسبة يجعل المستوى يبدو كأنه صورة مسطحة ثنائية الأبعاد وسيصعب تقدير المسافات أو فهم المخطط الكامل لمحيط اللعب، كذلك تلعب الإضاءة والظلال دورًا آخر بكونها طبقة معلومات مهمة وضرورية في ألعاب القتال.
</p>

<p>
	تتعامل صناعة الألعاب عادة مع الإضاءة كعنصر تجميلي يتبع فن البيئة، لكن الحقيقة أن لها وظائف محورية في أسلوب اللعب نفسه، لذا يجب أن تُصمَّم بعد تنسيق دقيق مع مصمم المستوى لفهم كيف ستؤدي دورها في خدمة الهدف من المستوى واللعبة ككل.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181654" href="https://academy.hsoub.com/uploads/monthly_2026_03/lighting.jpeg.b4f94433ef2966f68515074e488e2e8c.jpeg" rel=""><img alt="lighting.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181654" data-ratio="56.29" data-unique="qokqctmua" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/lighting.thumb.jpeg.8c19520afe29e9c2d6d777c0d36b5409.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة لإضاءة غرفة من العصور الوسطى نفذها <a href="https://www.artstation.com/artwork/lBLzJ" rel="external nofollow">هارلي ويلسون Harley Wilson</a> توضح توزيع الإضاءة في المشهد.
</p>

<h2 id="-">
	مرحلة فن البيئة
</h2>

<p>
	ينبغي أن يكون الشكل العام للمستوى وتسلسل المرحلة قد ظهر واقترب من اكتماله بعد المراحل السابقة، وهنا تأتي مرحلة التفاصيل البصرية أو فن البيئة حيث نضيف هذه التفاصيل الفنية فيما يعرف <strong>بعملية Art Pass</strong>، حيث تحتاج أغلب المشاريع إلى تكرار هذه العملية عدة مرات.
</p>

<p>
	يظن الكثير من الناس أن إضافة التفاصيل الفنية عملية ممتعة وسهلة، لذا يندفع المصمم المبتدئ إلى هذه المرحلة دون المرور بمرحلة التخطيط ودون أن يكون لديه مخططات أو نماذج هيكلية واضحة، وهذا يتسبب في ثبات أخطاء التصميم الأولية لأن تغيير المستوى لاحقًا سيكون مكلفًا جدًا إذ سنحتاج إلى إعادة تنفيذ المستوى من جديد بما فيه من تفاصيل فنية، لذا ينبغي أن نتجنب الانسياق وراء الرغبة في البدء بالتفاصيل الفنية قبل إتمام المراحل السابقة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="181655" href="https://academy.hsoub.com/uploads/monthly_2026_03/artpass.gif.7e587f432b19ed4cdd72ebe5dbc45c36.gif" rel=""><img alt="artpass.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="181655" data-ratio="70.71" data-unique="pu6uqurif" style="width: 700px; height: 495px;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/artpass.thumb.gif.fbb726fdfa57869bb56a83579d668a32.gif"></a>
</p>

<p style="text-align: center;">
	صورة متحركة تظهر تطور قاعدة عسكرية بحرية في لعبة Sniper Elite.
</p>

<h2 id="-">
	مرحلة الإطلاق
</h2>

<p>
	عند إتمام العمل على المراحل السابقة نصل إلى موعد إطلاق اللعبة والنشر للجمهور المستهدف، ويمثل إطلاق اللعبة بالنسبة للشركات الكبيرة بداية كابوس جديد إذ عليهم الآن متابعة العمل في تسويق اللعبة والدعاية لها وتحليل آراء المستخدمين وإصلاح الأخطاء البرمجية التي ستظهر، بل وتطوير محتوى إضافي بعد الإطلاق.
</p>

<p>
	لكن في حالة مصمم الألعاب المستقل فعليه توثيق تصميم مستوى اللعبة توثيقًا احترافيًا لضمان أن يفهم المطلع على المشروع ما أنجزته فيه، وتبرز أهمية هذه النصيحة إذا علمنا أن أغلب المبتدئين يفترضون أن أعمالهم "ستتحدث عن نفسها"، لكن الواقع المشاهد يقول أنه إذا لم تحسن تسويق المشروع وإتقان تفاصيل توثيقه فلن يعرف أحد أن هذا المشروع موجود أصلًا.
</p>

<h2 id="-">
	خاتمة
</h2>

<p>
	يشمل تصميم مستويات الألعاب عدة خطوات هي على الترتيب: ما قبل الإنتاج، تصميم المعارك -في حالة الألعاب القتالية-، المخططات العامة، النماذج الهيكلية، البرمجة النصية، الإضاءة، فن البيئة، والإطلاق.
</p>

<p>
	وعرفنا أن المشاريع تختلف عن بعضها في احتياجها لواحدة أو أكثر من هذه المراحل، وأن على المبتدئ في تعلم تصميم مستويات الألعاب أن يبدأ بتجربتها جميعها في نفس الوقت ليكتسب خبرة كافية في تحديد المراحل التي يحتاج إليها لكل مشروع.
</p>

<h2 id="-">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد </a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%88%D8%AD%D8%AF%D8%A9-%D8%AA%D8%AD%D9%83%D9%85-%D9%88%D8%A7%D9%82%D8%B9%D9%8A%D8%A9-%D9%84%D9%84%D8%B3%D9%8A%D8%A7%D8%B1%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2572/" rel="">إنشاء وحدة تحكم واقعية للسيارات في ألعاب الفيديو ثنائية الأبعاد </a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AF%D9%84%D9%8A%D9%84%D9%83-%D8%A7%D9%84%D8%B4%D8%A7%D9%85%D9%84-%D8%A5%D9%84%D9%89-%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2304/" rel="">دليلك الشامل إلى برمجة الألعاب </a>
	</li>
</ul>

<p>
	ترجمة -بتصرف- <a href="https://book.leveldesignbook.com/process/overview" rel="external nofollow">للفصل How to make a level من كتاب The Level Design Book</a>.
</p>
]]></description><guid isPermaLink="false">2603</guid><pubDate>Thu, 12 Mar 2026 12:01:02 +0000</pubDate></item><item><title>&#x645;&#x62F;&#x62E;&#x644; &#x625;&#x644;&#x649; &#x62A;&#x635;&#x645;&#x64A;&#x645; &#x645;&#x633;&#x62A;&#x648;&#x64A;&#x627;&#x62A; &#x623;&#x644;&#x639;&#x627;&#x628; &#x627;&#x644;&#x641;&#x64A;&#x62F;&#x64A;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D9%85%D8%AF%D8%AE%D9%84-%D8%A5%D9%84%D9%89-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B3%D8%AA%D9%88%D9%8A%D8%A7%D8%AA-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-r2602/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2026_03/videogamelevels.png.26d68d710de5732bf461091f97b8eac3.png" /></p>
<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181638" href="https://academy.hsoub.com/uploads/monthly_2026_03/basketballovermap.jpeg.6905ec94074a774f92c77c8960b04b19.jpeg" rel=""><img alt="basketball over map.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181638" data-ratio="54.83" data-unique="edrgttn8v" style="width: 600px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/basketballovermap.thumb.jpeg.55b2d86c7e9cadce700ebc9743ae018f.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة لملعب كرة سلة متراكب فوق مخطط عام لخريطة من لعبة Halo 1 بواسطة <a href="https://twitter.com/neilsonks/status/1658549380691968003" rel="external nofollow">@neilsonks</a>
</p>

<p>
	يشير اصطلاح المستوى Level في ألعاب الفيديو إلى المساحة التي تقع فيها أحداث اللعبة مثل جزيرة فورتنايت fortnite island، وهي مسار من التحديات على <a href="https://www.roblox.com/" rel="external nofollow">منصة روبلوكس Roblox</a>، أو ملعب كرة سلة أو مسار سباق أو ساحة لعب عامة، أو حتى لوحة لعب مونوبولي الشهيرة أو الكلمات المتقاطعة أو كتب التلوين المخصصة للأطفال، فكل هذه تعد مستويات لعب مختلفة الشكل والصعوبة.
</p>

<p>
	تشترك هذه المستويات والمساحات في أنها تضع حدودًا لللاعبين وتفاعلاتهم مع اللعبة ومع بعضهم البعض، وتوفر المستويات المختلفة تنوعًا في تجربة اللعب، فتتشابه ملاعب كرة السلة في أشكالها مثلًا لكن الملعب المكشوف يختلف عن صالة لعب مغطاة في تجربته والمزاج العام له، وهنا يبرز دور مصمم المستوى في اللعبة إذ يصب تركيزه على كيفية تطويع هذه المساحات للتأثير في مشاعر اللاعبين وتوجيه سلوكياتهم.
</p>

<h2 id="-">
	تعريف
</h2>

<p>
	يُعرَّف تصميم المستوى في الألعاب على أنه تخطيط وبناء المساحات المخصصة لألعاب الفيديو وغالبًا ما يكون هذا في ألعاب التصويب من منظور الشخص الأول أو الثالث، رغم أن كثيرًا من مبادئ تصميمها قد تصلح للألعاب ثنائية البعد التي تتحرك يمينًا أو يسارًا مع اللاعب، أو الألعاب الاستراتيجية أو غير القتالية حتى، لكننا ذكرنا ألعاب المنظور الأول والثالث لأن أغلب نظريات تصميم المستويات تتخذ من ألعاب التصويب ثلاثية الأبعاد نموذجًا قياسيًا لها، ويعود ذلك إلى نشأة الدور الوظيفي لمصمم مستويات الألعاب في التسعينات من القرن الماضي والتي هيمنت عليها ألعاب التصويب.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181639" href="https://academy.hsoub.com/uploads/monthly_2026_03/uncharted4.jpeg.97ab02d4ca75fe20f597b38fbd40ec65.jpeg" rel=""><img alt="uncharted 4.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181639" data-ratio="56.33" data-unique="ium630x9o" style="width: 600px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/uncharted4.thumb.jpeg.3fb8497c35bde8f5475af263b2449a44.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة للقطة شاشة لمرحلة الوصول إلى الجزيرة أثناء عملية التطوير بنظام blockout بواسطة إم شاتز للعبة Uncharted 4
</p>

<h2 id="-">
	التصميم الوظيفي للمستويات وفن البيئة
</h2>

<p>
	يركز مصمم المراحل level designer على صياغة سلوك اللاعب، فيكون مسؤولًا في استديوهات التصميم الكبيرة عن كتابة الوثائق التقنية ورسم المخططات الأولية وبناء النماذج الهيكلية Blockouts ومراقبة اختبارات اللعب وكذلك موازنة خيارات اللعب المتاحة في المستوى الواحد فيما يعرف بموازنة الخريطة Map balance، والمواجهات encounters التي يتعرض لها في ساحة مفتوحة مع شخصيات اللعبة الأخرى، خاصة غير القابلة للعب منها Non Playable Characters (NPC).
</p>

<p>
	في المقابل نرى أن فنان البيئة environment artist يركز اهتمامه على الجوانب الرسومية حيث يضيف اللمسات الفنية بتصميم النماذج وإكسائها بالمواد والخامات المناسبة وتنسيق العناصر الديكورية وتوزيع الإضاءة بهدف صقل المظهر البصري للمستوى، ورغم أن هذا يبدو في ظاهره تنسيقًا للديكور العام إلا أن إتقانه يدعم أهداف تصميم تجربة الاستخدام لبيئة اللعب ويساعد اللاعب على فهم المجريات العامة للعبة.
</p>

<p>
	وبناء على ما سبق نستطيع فهم تصميم المستويات من منظورين:
</p>

<ul>
	<li>
		المنظور الرسمي للمجال، وهو تصميم المستويات بمعناه التخصصي المهني.
	</li>
	<li>
		المنظور العام لتصميم المستويات بمعناه الشامل الذي يضم المنظور الرسمي إضافة إلى فن البيئة وكل ما يمكن أن يشمله المستوى.
	</li>
</ul>

<p>
	سنركز في شرحنا على المنظور الرسمي لغرض التعليم، لكن اعلم أن اللاعب يتفاعل مع المنظور العام أكثر.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="181640" href="https://academy.hsoub.com/uploads/monthly_2026_03/Dust514.png.7305c62bd1cacc6d9cbb1b57d044ea40.png" rel=""><img alt="Dust514.png" class="ipsImage ipsImage_thumbnailed" data-fileid="181640" data-ratio="59.17" data-unique="6ei3iqi98" style="width: 600px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/Dust514.thumb.png.09562bf7651dc17f6a3a4b9cd18200a1.png"></a>
</p>

<p style="text-align: center;">
	صورة توضح مخطط يقارن المنظور الوظيفي لتصميم المستوى مع المنظور العام باستخدام صور عمليات من مراحل بناء منشأة أبحاث جالينت من لعبة Dust 514
</p>

<h2 id="-">
	الفرق بين تصميم غرفة وتصميم عالم افتراضي
</h2>

<p>
	يستغرق مصمم المستوى أيامًا أو أسابيع في تصميم غرفة واحدة، لكن هذا التدقيق غير عملي في حالة الألعاب الضخمة مثل العوالم المفتوحة أو الألعاب الجماعية كثيرة اللاعبين Massively Multiplayer Online (MMO) التي تضم مئات أو آلاف الغرف.
</p>

<p>
	وهنا يبرز دور مصمم العالم الافتراضي حيث يهتم بتصميم الحركة العام في المستوى وإرشاد اللاعب wayfinding عبر أحياء كاملة بدلًا من منازل فردية، ومناطق حيوية Bioms بدلًا من أماكن صغيرة، والفئات العامة categories بدلًا من الحالات الخاصة instances، ويتيح هذا المنظور الشمولي مساحة كافية لتفاعل اللاعبين والأنظمة بحرية والتي سيكون العالم الافتراضي فارغًا أو باهتًا بدونها.
</p>

<p>
	وعلى هذا فإذا أردت بناء تجربة موجهة أو تخضع لسيناريو محدد فينبغي أن تصب تركيزك على تفاصيل كل غرفة كأنك مهندس معماري، أما في حالة الألعاب التي تحتوي على كثير من اللاعبين والأنظمة والمساحات الكبيرة فإن تصميم العالم الافتراضي يمنحك لمسة أشبه بالمخطِّط العمراني للمدن الكبيرة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181641" href="https://academy.hsoub.com/uploads/monthly_2026_03/fortnite.jpeg.050a95a028e835ebc9be1e1321dfc585.jpeg" rel=""><img alt="fortnite.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181641" data-ratio="37.50" data-unique="1w8syuqzh" style="width: 600px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/fortnite.thumb.jpeg.d609536f53abaf9e8791207e780d2b1f.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة في مستوى pleasant park في لعبة فورتنايت حيث لا تهم تفاصيل كل غرفة في المنازل الموجودة بقدر الشكل العام للحي كله
</p>

<h2 id="-">
	النظرية مقابل البناء الفعلي
</h2>

<p>
	بما أن هذا المقال مقروء فمن البديهي أننا نؤمن بأهمية القراءة للتعلم، لكن المادة النصية تضعك على أعتاب المجال المهني فقط إذ يتحتم عليك أن تترك المقال أو المادة النصية في مرحلة ما لتبدأ التنفيذ فعليًا.
</p>

<p>
	وقد جرت العادة في مجتمع مصممي خرائط لعبة Quake حين يُكثر المبتدئ من طرح الأسئلة أن يأتيه الرد الحازم "اذهب وصمم خريطة"، وعلى جفاء هذه الرد فإن فيه نية صادقة للتحفيز كأنه ينهاه عن المماطلة في التنظير وأن ينتقل إلى التطبيق العملي الذي سيجيب كثيرًا من أسئلته. إن السبيل الوحيد لأن تصبح مصمم مستويات ألعاب هو بناء المستويات فعلًا، بل الكثير منها.
</p>

<blockquote class="ipsQuote" data-gramm="false" data-ipsquote="">
	<div class="ipsQuote_citation">
		اقتباس
	</div>

	<div class="ipsQuote_contents ipsClearfix" data-gramm="false">
		<p>
			نستخدم أحيانًا مصطلح خريطة map بدلًا من مستوى level، إذ توحي الخريطة بوجود مساحة حرة تدعم أنماطًا مختلفة من اللعب، في حين يشير المستوى إلى مسار أكثر خضوعًا للسيناريوهات البرمجية scripts، لكن لا حرج في استخدام المصطلحين بالتبادل.
		</p>
	</div>
</blockquote>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="jpeg" data-fileid="181642" href="https://academy.hsoub.com/uploads/monthly_2026_03/quake.jpeg.71818e8c7c9fabc782df62da6cd69975.jpeg" rel=""><img alt="quake.jpeg" class="ipsImage ipsImage_thumbnailed" data-fileid="181642" data-ratio="56.33" data-unique="2b1wqgs4v" style="width: 600px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2026_03/quake.thumb.jpeg.742786891148718627d38e995c17fd54.jpeg"></a>
</p>

<p style="text-align: center;">
	صورة للقطة شاشة لمنطقة "مركز البداية" في تعديل Arcane Dimensions للعبة Quake 1، من تصميم سايمون "sock" أوكالاهان Simon "sock" O'Callaghan وآخرين
</p>

<h2 id="-">
	نقاء نظرية التصميم
</h2>

<p>
	تنزلق معظم الشروحات المتعلقة بتصميم المستويات إما إلى التنظير الأكاديمي المعقد أو التبسيط التجاري المخل، لذا نهدف هنا إلى شرح واستخدام نفس اللغة التقنية التي يتداولها المصممون المحترفون في بيئات العمل الحقيقية مع الحفاظ على نظرة نقدية تكفي لتنقية الأفكار من السطحية المخلة.
</p>

<p>
	يفتقر المصمم في الغالب إلى المستوى الاصطلاحي المطلوب لوصف الأشكال والأحجام رغم استفادته الكبيرة من مفهوم التكتيل massing المعماري المتعلق بالإحساس والمنطق العام حول الأشكال الهندسية، لهذا يكثر الاستشهاد في تصميم الألعاب بمصطلحات ومفاهيم خارجية دون إدراك جذورها العميقة، فنشتق مثلًا مفهوم المسارات الحرجة critical paths من طريقة المسار الحرج، وهي أداة هندسية تستخدم لتحديد نطاق العمل وارتباط العناصر ببعضها، ونحن نذكر هذا لبيان أن غياب الدقة في اختيار الكلمات التي تبني التصور النظري فإن جودة التفكير وفعالية التواصل تتضرر حتمًا.
</p>

<p style="text-align: center;">
	<img alt="book.png" class="ipsImage ipsImage_thumbnailed" data-fileid="181643" data-ratio="58.01" data-unique="vdutcclw6" style="width: 662px; height: auto;" width="862" src="https://academy.hsoub.com/uploads/monthly_2026_03/book.png.db9100388909853ab33908cf1a5a079d.png">
</p>

<p style="text-align: center;">
	 صورة توضح الاستراتيجيات المتنوعة للإضاءة النهارية المستخدمة في الهندسة المعمارية، فرانسيس تشينج من كتاب "Form, Space, and Order"
</p>

<h2 id="-">
	النظرة الشمولية
</h2>

<p>
	رغم أنه من الضروري أن يركز مصمم المستوى على سلوك اللاعب إلا أننا نحتاج أحيانًا إلى توسيع المنظور ليشمل الصورة الكاملة، فالمستوى أكبر من مجرد بناء تقني، بل هو مزيج معقد يجمع بين التصميم المكاني spatial design والفن وعلم النفس والبرمجة والثقافة، ولا يوجد فاصل حقيقي من منظور اللاعب بين تصميم المستوى وفن البيئة إذ يشمل تصميم المستوى كل ما يؤثر في عالم اللعبة.
</p>

<p>
	لهذا يفضل تجنب الانسياق خلف القواعد البسيطة التي تقوم على "افعل ولا تفعل" لأن المستويات تتجاوز كونها مجرد مجموعة غرف وصناديق حماية أو مساحات طبيعية تناثرت فيها بعض الصخور والأشجار، فكل مستوى له مرحلة تاريخية وهدف تصميمي، وينبغي عليك كمصمم أن تضع تجربة اللعب كلها في الحسبان أثناء التصميم.
</p>

<h2 id="-">
	خاتمة
</h2>

<p>
	تعرفنا في هذا المقال على بعض المفاهيم الأساسية في تصميم مستويات الألعاب والمنظور الذي ينبغي أن يدركه مصمم الألعاب، وسنشرح في مقالات تالية من السلسلة كيفية إنشاء المستويات ومراحلها، وتصميم المستوى من المنظور الثقافي.
</p>

<p>
	ترجمة بتصرف للفصل <a href="https://book.leveldesignbook.com/introduction" rel="external nofollow">What is level design</a> من كتاب <a href="https://book.leveldesignbook.com/" rel="external nofollow">The Level Design Book</a>.
</p>
]]></description><guid isPermaLink="false">2602</guid><pubDate>Tue, 10 Mar 2026 08:41:36 +0000</pubDate></item><item><title>&#x636;&#x628;&#x637; &#x62D;&#x631;&#x643;&#x627;&#x62A; &#x627;&#x644;&#x634;&#x62E;&#x635;&#x64A;&#x629; &#x641;&#x64A; &#x645;&#x62D;&#x631;&#x643; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%B6%D8%A8%D8%B7-%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2594/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/002..png.e1f49a810610a382041b68a6c8a5b1b9.png" /></p>
<p>
	لنفترض أن لدينا شخصيةً ثلاثية الأبعاد من نوع صلب Rigged تم بناؤها ذاتيًا أو حملناها من مصدر خارجي ونريد إعداد وضبط حركاتها في جودو. سنستكشف في هذا المقال طريقة فعل ذلك، وسنستخدم نفس شخصية المغامر وبقية الأصول التي ذكرناها في <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D9%8A%D8%B1%D8%A7%D8%AF-%D8%A7%D9%84%D8%A3%D8%B5%D9%88%D9%84-assets-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2593/" rel="">المقال السابق</a> من <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">سلسلة دليل جودو</a>.
</p>

<h2 id="">
	إعداد الشخصية
</h2>

<p>
	اخترنا <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">العقدة</a> <code>CharacterBody3D</code> لتمثّل شخصية المغامر، لهذا ينبغي أن يكون المشهد كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172895" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_knight_node.png.91b5bec850ced9e8de43853b9681839f.png" rel=""><img alt="01 knight node" class="ipsImage ipsImage_thumbnailed" data-fileid="172895" data-unique="9joeya557" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_knight_node.png.91b5bec850ced9e8de43853b9681839f.png"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: أغلقنا فرع العقدة <code>Rig</code> كي لا تطول القائمة.
</p>

<p>
	أول ما قد نلاحظه أن يدي المغامر ممتلئتين، فقد زُوّد الفنان المغامر بكل الأسلحة والدروع ووضعها في مكانها واتجاهها الصحيح. بإمكاننا طبعًا التنقل في قائمة العقد وإخفاء ما نريده.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172903" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_3dcharacter_default_pose.png.108d96018f1b4de698df3ab2d2e5dbf2.png" rel=""><img alt="02 3dcharacter default pose" class="ipsImage ipsImage_thumbnailed" data-fileid="172903" data-unique="jnp98c9aq" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_3dcharacter_default_pose.png.108d96018f1b4de698df3ab2d2e5dbf2.png"> </a>
</p>

<h3 id="animationtree">
	استخدام العقدة AnimationTree
</h3>

<p>
	من الصعب التعامل مع كل الحركات المتوفرة للشخصية من خلال الشيفرة. لذا من المهم التفكير مثلًا بعدد العبارات الشرطية <code>if</code> التي نحتاجها كي نحدد الحركة المناسبة التي يجب تنفيذها في التوقيت المطلوب، وذلك اعتمادًا على ما يفعله اللاعب. لن يكون الأمر صعبًا بالطبع عند وجود عدة حركات فقط، لكن سرعان ما ستخرج الأمور عن السيطرة، ولن تكون هناك جدوى فعلية منها.
</p>

<p>
	علينا التفكير أيضًا في الحالة التي يقف فيها اللاعب دون حراك؛ إذ لا بد عندها من تنفيذ حركة الوقوف دون فعل شيء Idle. وعندما يضغط اللاعب مفتاح الحركة إلى الأمام، ستنفذ الشخصية حركة المشي Walking.
</p>

<p>
	ستبدو هذه التغيرات السريعة في الحركة مزعجة، ويُفضّل أن تكون هذه الحركات المتعاقبة متمازجةً ببعضها لتعطي انتقالًا أكثر سلاسة. ولحل مشاكل الحركة المعقدة تلك، يجب استخدام عقدة <code>AnimationTree</code>، وهي عقدة مصممة للتحكم بالعقدة <code>AnimationPlayer</code> وتقدم وظائفًا تتحكم بالانتقال بين الحركات المختلفة وتمزجها معًا.
</p>

<p>
	سنضيف العقدة <code>AnimationTree</code> إلى المشهد ، ثم نضبط الخاصية <code>Tree Root</code> في الفاحص Inspector على القيمة <code>AnimationNodeStateMachine</code>، ثم نلحق العقدة <code>AnimationPlayer</code> بهذه العقدة من خلال الخاصية <code>Anim Player</code>، ونفعّل بعدها الخاصية <code>Active</code>
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172897" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_animation_tree.png.f33b6cfd711ec181be890b42b7c0e94a.png" rel=""><img alt="03 animation tree" class="ipsImage ipsImage_thumbnailed" data-fileid="172897" data-unique="810p5pr9o" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_animation_tree.png.f33b6cfd711ec181be890b42b7c0e94a.png"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: قد نلاحظ أننا لن تتمكن من تشغيل الحركات التي تضمها العقدة <code>AnimationPlayer</code> عندما تكون الخاصية <code>Active</code> للعقدة <code>AnimationTree</code> فعّالة، لذا إن أردنا تعديل أي شيء أو اختبار الحركات، فعلينا إلغاء تفعيل هذه الخاصية مؤقتًا.
</p>

<h3 id="idlewalkrun">
	حلقة الحركات التوقف Idle والمشي Walk والجري Run
</h3>

<p>
	هنالك الكثير من الحركات التي سنجدها في هذه النماذج، لكننا سنركز في مثالنا على حلقة حركات التوقف Idle والمشي Walk والجري Run، مع القفز والهجوم؛ فإن أردنا إضافة حركات أخرى، سنكرر ببساطة ما سنفعله مع الحركة التي نريد.
</p>

<p>
	سنبحث عن الحركات Idle و Running_A و Walking_Backwards و Running_Strafe_Left / Running_Strafe_Right ضمن العقدة <code>AnimationPlayer</code>، وسنتأكد من أن هذه الحركات ستعمل على شكل حلقة.
</p>

<p>
	يمكننا اختبار ذلك من خلال تشغيل الحركة عبرالزر ▶، وفي حال كانت هناك أي حركة غير معدّة لتكرار نفسها، سنعيد استيراد الشخصية بعد ضبط الحركات على وضع التكرار.
</p>

<p>
	سنختار العقدة <code>AnimationTree</code> وسنرى أن هناك لوحة جديدة قد فُتحت أسفل العارض.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172901" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_animtree_empty.png.b0cb4d5c708f13589970151a6b32e480.png" rel=""><img alt="04 animtree empty" class="ipsImage ipsImage_thumbnailed" data-fileid="172901" data-unique="r4ncikefj" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_animtree_empty.thumb.png.7e2063a88604fe1ef7988c19ad56aeb2.png"> </a>
</p>

<p>
	وكمثال عن طريقة العمل، سننقر بالزر الأيمن للفأرة في أي نقطة فارغة من اللوحة، ونضغط على خيار <strong>أضف حركة Add Animation</strong> ثم نختار Idle. نكرر العملية ونضيف أيضًا الحركة 1H_Melee_Attack_Chop.
</p>

<p>
	سنضغط الآن على أيقونة ربط العقد Connect Nodes، ثم نرسم اتصالًا بين الزر <code>start</code> و الحركة <code>Idle</code>، وسرى كيف تُنفّذ الحركة Idle مباشرةً.
</p>

<p>
	نريد الآن الانتقال من التوقف إلى الهجوم ثم العودة إلى حركة التوقف بعد انتهاء حركة الهجوم. ولهذا، سنرسم اتصالين إضافيين من وإلى حركة الهجوم. لن يعمل ذلك كما هو مطلوب، لكنه سينتقل بين الحركتين بسرعة، لأن كلا الاتصالين ضُبطا للانتقال الفوري.
</p>

<p>
	لتغير شروط الانتقال، سننتقل إلى وضع الاختيار، وذلك بالنقر على الأيقونة المخصصة، ثم النقر على أحد الاتصالين، وستظهر في نافذة الفاحص خاصيات الاتصال. نحتاج إلى ضبط الخاصية <code>Advanced&gt;Mode</code> على <code>Enabled</code> في الاتصال من الحركة Idle، ويعني ذلك أن هذا الانتقال سيُنفذ فقط عندما نقرر ذلك وليس آليًا. سنلاحظ بعد ذلك أن لون الأيقونة على خط الاتصال سيتغير.
</p>

<p>
	سنضبط الخاصية <code>Switch</code> للاتصال الثاني إلى الحركة Idle على <code>AtEnd</code>، وتبقى قيمة الخاصية <code>Advanced&gt;Mode</code> على <code>Auto</code>. عند النقر الآن على الزر ▶ في عقدة الهجوم، ستعمل حركة الهجوم، ثم تعود إلى حركة التوقف عندما تنتهي.
</p>

<p>
	يوضح هذا المثال البسيط طريقة إعداد حركات مختلفة وضبط الانتقالات بين هذه الحركات.
</p>

<p>
	سنحتاج لاحقًا بطبيعة الحال إلى أكثر من ذلك، لهذا سنحذف العقد الموجودة في لوحة شجرة التحريك باستخدام أيقونة الحذف، ودعنا نحضّر فضاء المزج blendspace.
</p>

<h3 id="blendspace">
	فضاء المزج blendspace
</h3>

<p>
	ننقر بالزر اليميني للفأرة على أي نقطة فارغة من لوحة شجرة الحركات ثم نختار <code>BlendSpace2D</code>، وننقر بعد ذلك على الأيقونة التي تظهر، مع تغيير اسمها إلى <code>IWR</code>. نضيف بعد ذلك اتصالًا من <code>Start</code> إلى <code>IWR</code> كي يبدأ فضاء المزج بالعمل تلقائيًا.
</p>

<p>
	ننقر بعد ذلك على أيقونة القلم في الفضاء <code>IWR</code> لتظهر لنا النافذة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172899" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_blendspce_edit.png.1f4db7c602fa44b7cd240c988d4e504b.png" rel=""><img alt="05 blendspce edit" class="ipsImage ipsImage_thumbnailed" data-fileid="172899" data-unique="hoppwds1u" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_blendspce_edit.thumb.png.121632ff314da1ec98e875d21d45ee77.png"> </a>
</p>

<p>
	يمثل هذا الفضاء ثنائي البعد شعاع الحركة الأفقية للشخصية، فعندما تقف الشخصية ساكنة، تكون قيمة الشعاع <code>(0,0)</code>. ننقر الآن على زر إنشاء نقاط Create Points، ثم ننقر على منتصف الشبكة لإضافة الحركة <code>idle</code>.
</p>

<p>
	نضيف الحركة Running_A في المنتصف والأعلى، والحركة Walking_Backwards في المنتصف والأسفل الحركة، ثم نضيف في النهايتين الأفقيتين الحركتين Running_Strafe_Left و Running_Strafe_Right.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172902" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_building_blendspace.png.9374d100022cfc44dc6993a22f72fa75.png" rel=""><img alt="06 building blendspace" class="ipsImage ipsImage_thumbnailed" data-fileid="172902" data-unique="y8tbyhkb1" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_building_blendspace.thumb.png.7ac1da5137132b5aa53851f7c06b8912.png"> </a>
</p>

<p>
	يمكننا الآن النقر على أيقونة تحديد موضع الخلط في الفراغ، ثم النقر ضمن الشبكة والسحب لرؤية الانتقال السلس بين الحركات الأربعة والتوقف.
</p>

<p>
	بعد الانتهاء من تجربة عملية المزج، سننقر على الجذر Root لتعود إلى لوحة شجرة الحركات.
</p>

<h3 id="statemachine">
	ضبط آلة الحالة State machine
</h3>

<p>
	يمكن عدّ حلقة الحركات <code>IWR</code> على أنها قلب شجرة الحركات، حيث ستقضي الشخصية معظم الوقت في تنفيذ تلك الحركات، و تتفرع بقية الحركات عنها كما فعلنا قبل قليل مع حركة الهجوم.
</p>

<p>
	تعرض لقطة الشاشة التالية مجموعة من الحركات التي ترتبط بالحلقة <code>IWR</code>، ومن المهم هنا تذكر أن خاصيات الانتقال تُضبط بنفس الأسلوب الذي اتبعناه في المثال السابق.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172896" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_multi_anim.png.9a54a651e81cdf5e130a5815dd860374.png" rel=""><img alt="07 multi anim" class="ipsImage ipsImage_thumbnailed" data-fileid="172896" data-unique="h6lzj20a4" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_multi_anim.thumb.png.be58e1fa6ca0ab5d5e22859c3d83c4d6.png"> </a>
</p>

<p>
	يمكننا أيضًا النقر على الحركة لتغيير اسمها، وذلك لأن بعض الأسماء طويلة؛ أما الحركة التي تختلف عن غيرها فهي حركة القفز، إذ تقسم هذه الحركة إلى ثلاثة أجزاء هي البداية Start والعودة land التي تُنفّذ عندما تبدأ الشخصية بالقفز وعندما تنهيه، بينما يمثل الجزء الثالث التوقف Idle حركة وصل تُنفّذ طالما أن الشخصية لا تزال في الجو، خاصةً إن انتقلت أثناء القفز مسافة طويلة مثلًا.
</p>

<p>
	سنضيف الآن حركات القفز الثلاث كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172900" href="https://academy.hsoub.com/uploads/monthly_2025_05/08_adding_jump_loop.png.ea4888c1c7b82fee238424ab6e178227.png" rel=""><img alt="08 adding jump loop" class="ipsImage ipsImage_thumbnailed" data-fileid="172900" data-unique="9j4lulpcq" src="https://academy.hsoub.com/uploads/monthly_2025_05/08_adding_jump_loop.thumb.png.1fd2dc9a541171efe4a8fffe3605662a.png"> </a>
</p>

<p>
	وبما أننا نريد الانتقال مباشرةً من الحلقة <code>IWR</code> إلى الحركة <code>Jump_Idle</code> عند السقوط عن حافة، فعند النقر على زر القفز ستُنفّذ الحركة <code>Jump_Start</code>. وإضافة إلى ذلك، أبقينا الانتقال من <code>Jump_Start</code> إلى <code>Jump_Idle</code> على القيمة <code>Auto</code>. لكن بدل تغيير الخاصية <code>Advanced&gt;Mode</code> إلى <code>Enabled</code>، أضفنا شرطًا من خلال ضبط قيمة الخاصية <code>Condition</code> على <code>jumping</code>.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172898" href="https://academy.hsoub.com/uploads/monthly_2025_05/09_jumping_condition.png.0e45844f0ffc0de13c45110a17e6c74a.png" rel=""><img alt="09 jumping condition" class="ipsImage ipsImage_thumbnailed" data-fileid="172898" data-unique="9dfymv7tq" src="https://academy.hsoub.com/uploads/monthly_2025_05/09_jumping_condition.png.0e45844f0ffc0de13c45110a17e6c74a.png"> </a>
</p>

<p>
	وبشكل مشابه، نضع شرطًا على تنفيذ الانتقال بين <code>Jump_Idle</code> و <code>Jump_Land</code> هو <code>grounded</code>؛ وسنتمكن من ضبط هذه الشروط لاحقًا من خلال الشيفرة لتفعيل عملية الانتقال.
</p>

<p>
	قد نلاحظ أخيرًا إن أمعنا النظر أن الانتقال بين الحركتين <code>Jump_Land</code> و <code>IWR</code> لا تبدو سلسلة، لأن آخر إطار من الأولى لا يتوافق تمامًا مع أول إطار من الثانية. يمكن أن نتدارك هذا الأمر باختيار الانتقال بينهما وضبط قيمة الخاصية <code>Xfade Time</code> على <code>0.1</code> وسنرى نتيجةً مرضية.
</p>

<h2 id="-1">
	خلاصة
</h2>

<p>
	ضبطنا في هذا المقال حركات شخصية ثلاثية الأبعاد وأصبحت جاهزةً للاستخدام؛ وباستخدام العقدة <code>AnimationTree</code> أصبح اختيار الانتقال بين الحركات أكثر سهولةً في شيفرة الحركة.
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/3d/assets/character_animation/index.html" rel="external nofollow">Character Animation</a>.
</p>

<h2 id="-2">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D9%8A%D8%B1%D8%A7%D8%AF-%D8%A7%D9%84%D8%A3%D8%B5%D9%88%D9%84-assets-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2593/" rel="">استيراد الأصول Assets ثلاثية الأبعاد في محرك الألعاب Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%85%D9%86%D8%B7%D9%82%D8%A9-%D8%A7%D9%84%D9%84%D8%B9%D8%A8-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%AC%D9%88%D8%AF%D9%88-r2444/" rel="">إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2508/" rel="">إنشاء شخصيات ثلاثية الأبعاد في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%8B%D8%A7-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D9%83-%D8%AC%D9%88%D8%AF%D9%88-r2450/" rel="">تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2594</guid><pubDate>Mon, 25 Aug 2025 16:01:02 +0000</pubDate></item><item><title>&#x627;&#x633;&#x62A;&#x64A;&#x631;&#x627;&#x62F; &#x627;&#x644;&#x623;&#x635;&#x648;&#x644; Assets &#x62B;&#x644;&#x627;&#x62B;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F; &#x641;&#x64A; &#x645;&#x62D;&#x631;&#x643; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D9%8A%D8%B1%D8%A7%D8%AF-%D8%A7%D9%84%D8%A3%D8%B5%D9%88%D9%84-assets-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2593/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/001.Assets.png.fdd36f46a890f7f30f0faedfcbb59b2d.png" /></p>
<p>
	نقدم في سلسلة المقالات التالية طرائق لاستيراد أصول الألعاب ثلاثية الألعاب والتعامل معها بما في ذلك النماذج Models والرسوم المتحركة Animations ومواد البناء Materials. سنستخدم في مثالنا الذي نبنيه لتوضيح الفكرة بعض الأصول المتاحة على موقع <a href="https://kaylousberg.itch.io/" rel="external nofollow">Kay Lousberg</a>:
</p>

<ul>
	<li>
		<a href="https://kaylousberg.itch.io/kaykit-adventurers" rel="external nofollow">حزمة شخصيات المغامرين Adventurers</a>
	</li>
	<li>
		<a href="https://kaylousberg.itch.io/kaykit-dungeon-remastered" rel="external nofollow">حزمة Dungeon</a>
	</li>
</ul>

<p>
	عند استخدام حزم الأصول التي ذكرناها ببداية المقال، سنلاحظ أن هناك عدة نسخ من الأصول وبتنسيقات مختلفة مثل OBJ و FBX و GLTF، وأنه توجد ملفات إضافية مثل الأمثلة وملفات نقوش منفصلة في حال أردت تعديل أي شيء. وبالطبع لن نحتاج إلى كل ذلك، فالتنسيق GLTF هو الصيغة المفضلة للاستيراد في جودو. إذًا كل ما علينا فعله هو سحب المجلد <code>gltf</code> أو الملفات <code>gltf.</code> أو <code>glb.</code> وهي الصيغة الثنائية لنفس الملف، إلى مجلد مشروعك.
</p>

<p>
	سنختار المجلد gltf من حزمة Dungeon، والمجلد characters من حزمة Adventurers، ونسحبهما إلى مجلد مشروعنا
</p>

<p>
	<strong>ملاحظة</strong>: هنالك الكثير من الملفات في حزمة Dungeon وقد يستغرق جودو بعض الوقت في قراءتها.
</p>

<h2 id="">
	استيراد شخصية
</h2>

<p>
	سنختار الملف Knight.glb من المجلد characters&gt;gltf ضمن نافذة نظام الملفات FileSystem، ثم ننقر على نافذة استيراد Import في الأعلى إلى جوار نافذة المشهد Scene.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172892" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_import_knight_char.png.5204fca0885ac174ce3bc0a69d91ca5a.png" rel=""><img alt="01 import knight char" class="ipsImage ipsImage_thumbnailed" data-fileid="172892" data-unique="629lotz5g" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_import_knight_char.thumb.png.857dec40d30beacf131cb945f49801ea.png"> </a>
</p>

<p>
	سنجد ضمن النافذة بعض خيارات الاستيراد الأساسية، لكن بإمكاننا تفصيل العملية. سننقر على زر إعدادات متقدمة Advanced، وستظهر لنا نافذة جديدة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172894" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_advanced_import.png.8882112081c7ee77bd1b03994378911f.png" rel=""><img alt="02 advanced import" class="ipsImage ipsImage_thumbnailed" data-fileid="172894" data-unique="czklz5qmn" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_advanced_import.thumb.png.266f204595f6e310ca5f8a7bf339b035.png"> </a>
</p>

<p>
	سنرى في اليمين (أو اليسار في النسخة الإنجليزية) البيانات التي يضمها مشهد GLTF بما في ذلك النقوش textures والحركات Animations؛ وسنلاحظ كيف اُلحقت كل الأسلحة بالشخصية، إضافةً إلى قائمة مطولة بالحركات التي يمكن للشخصية تنفيذها.
</p>

<p>
	تظهر الشخصية في وسط النافذة، إضافةً إلى مجموعة من الخيارات إلى اليسار (اليمين في النسخة الإنجليزية) التي تساعدنا على تعديل ضبط العنصر الذي نختاره من القائمة اليمينية.
</p>

<p>
	وطالما أننا سنكتب شيفرة اللاعب باعتباره <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">عقدةً</a> من النوع <code>CharacterBody3D</code>، فبإمكاننا تحديد نوع العقدة من خلال النقر على مشهد scene، ثم الانتقال إلى القائمة اليمينة والنقر إلى جوار الخاصية <code>Root Type</code> ثم اختيار العقدة <code>CharacterBody3D</code> من النافذة التي ظهرت.
</p>

<h3 id="animations">
	الحركات Animations
</h3>

<p>
	سننتقل إلى الأسفل في نافذة الإعدادات المتقدمة نحو شجرة الحركات Animations، وسنجد الكثير منها. سنحتاج في مثالنا إلى تنفيذ بعض الحركات مرةً واحدةً فقط مثل الهجوم، في حين سننفذ بعضًا آخر في حلقات مثل المشي walk والتوقف دون فعل Idle والجري Running. ولتنفيذ أي حركة مستمرة بشكل حلقة، سنننقر عليها ثم نضبط الخاصية <code>Loop Mode</code> على <code>Linear</code>. سنكرر هذا الأمر على جميع حركات المشي Walking والجري Running والتوقف Idle المختلفة. وعند الانتهاء من ذلك سننقر على زر إعادة الاستيراد Reimport في أسفل النافذة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172890" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_anim_loop.png.c481e4dccfe33e4384810decc6755292.png" rel=""><img alt="03 anim loop" class="ipsImage ipsImage_thumbnailed" data-fileid="172890" data-unique="86ubn8ynf" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_anim_loop.png.c481e4dccfe33e4384810decc6755292.png"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: إن كنا نصمم شخصياتنا بأنفسنا، نستطيع تجاوز عملية التعيين اليدوي لتكرار الحركة، والانتقال مباشرةً إلى تنفيذ الأمر تلقائيًا من خلال إضافة اللاحقة <code>loop-</code> إلى اسم الحركة. وللمزيد من التوضيح يمكن مراجعة <a href="https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_scenes.html#import-hints" rel="external nofollow">توثيق جودو</a>.
</p>

<p>
	سننقر الآن على الملف knight.glb بالزر اليمين للفأرة ونختار الأمر مشهد مورّث جديد New Inherited Scene. سنرى أن هناك عقدة جديدة ظهرت في نافذة المشهد تضم كل النماذج والحركات <code>AnimationPlayer</code> التي نستطيع تجريبها ضمن نافذة التحريك أسفل نافذة العرض.
</p>

<h3 id="-1">
	استيراد عناصر البيئة المحيطة
</h3>

<p>
	يمكنننا استيراد عناصر البيئة المحيطة بنفس الأسلوب. لنستخدم كمثال عملية استيراد حائط من حزمة dungeon. ونظرًا لوجود الكثير من الملفات في الحزمة، سنكتب wall في مرشح البحث، لنجد المشهد المطلوب الآتي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172893" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_filtering_wall.png.c87fd2bf06bcc3259d5fe8d65f62b78a.png" rel=""><img alt="04 filtering wall" class="ipsImage ipsImage_thumbnailed" data-fileid="172893" data-unique="96tfvin6t" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_filtering_wall.png.c87fd2bf06bcc3259d5fe8d65f62b78a.png"> </a>
</p>

<p>
	نريد أن يكون الجدار صلبًا، وسيكون إنشاء جسم من النوع <code>StaticBody3D</code> بأنفسنا مضنيًا مع جميع أشكال التصادم collision shapes التي يجب تعيينها لكل عنصر من الجسم. لكن يستطيع جودو تنفيذ الأمر تلقائيًا عند استيراد العنصر.
</p>

<p>
	سننتقل إلى نافذة الإعدادات المتقدمة للمشهد المستورد، ثم نختار العقدة wall من النوع Mesh أيقونتها شبكة حمراء، ثم نفعّل الخيار فيزياء Physics وننتقل إلى الخاصية <code>Physics&gt;Shape Type</code> ونضبطها على <code>Simple Convex</code>.
</p>

<p>
	يمثل هذا الخيار الشكل المحدب الذي يغلف الجدار الذي سيساعد في اكتشاف تصادم أي جسم مع الجدار.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172891" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_shape_type.png.9f61609b990478fc0729d4c3246d766a.png" rel=""><img alt="05 shape type" class="ipsImage ipsImage_thumbnailed" data-fileid="172891" data-unique="bdysbtrdf" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_shape_type.thumb.png.869c95511dc59b3f58e6b9b17c7fdd12.png"> </a>
</p>

<p>
	سننقر الآن على خيار إعادة الاستيراد Reimport، وهكذا سيُنشئ جودو عقدةً من النوع <code>StaticBody3D</code> عند استخدام هذا العنصر في اللعبة.
</p>

<p>
	<strong>ملاحظة</strong>: كما أشرنا سابقًا، هناك طريقة استيراد لتعيين أشكال التصادم تلقائيًا. لذا سنضيف في مشروع Blender اللاحقة <code>col-</code> مما يساعد البرنامج الذي يستورد الملف في تنفيذ العملية تلقائيًا. ويمكن مراجعة <a href="https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_scenes.html#import-hints" rel="external nofollow">توثيق جودو</a>.
</p>

<h3 id="-2">
	أتمتة الاستيراد
</h3>

<p>
	على الرغم من أن إضافة تلميحات إلى أسماء الملفات المستوردة هي الطريقة المفضلة في أتمتة عملية الاستيراد، خاصةً عندما نصمم بأنفسنا الشخصيات والعناصر. لكنه أمر لا يمكن تنفيذه عند استيراد هذه الملفات من حزم جاهزة كالتي نستخدمها في مثالنا.
</p>

<p>
	بإمكاننا أيضًا كتابة سكريبت استيراد يُنفّذ على كل عقدة مستوردة من نوع معين، فنستطيع مثلًا أتمتة عملية إنشاء جسم التصادم التي شرحناها في الفقرة السابقة، حيث يتنقل السكريبت التالي خلال جميع عقد الكائن الذي استوردناه ويُنشئ جسم تصادم لكل عقدة من النوع <code>MeshInstance3D</code>
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1597_9" style=""><span class="lit">@tool</span><span class="pln">
extends </span><span class="typ">EditorScenePostImport</span><span class="pln">

func _post_import</span><span class="pun">(</span><span class="pln">scene</span><span class="pun">):</span><span class="pln">
    iterate</span><span class="pun">(</span><span class="pln">scene</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">return</span><span class="pln"> scene

func iterate</span><span class="pun">(</span><span class="pln">node</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> node </span><span class="pun">!=</span><span class="pln"> null</span><span class="pun">:</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> node </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">MeshInstance3D</span><span class="pun">:</span><span class="pln">
        node</span><span class="pun">.</span><span class="pln">create_trimesh_collision</span><span class="pun">()</span><span class="pln">
      </span><span class="kwd">for</span><span class="pln"> child </span><span class="kwd">in</span><span class="pln"> node</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
        iterate</span><span class="pun">(</span><span class="pln">child</span><span class="pun">)</span></pre>

<p>
	ننتقل في النافذة استيراد Import إلى الخاصية <code>Import Script</code> ثم نختار السكريبت السابق بعد حفظه في مكان مناسب. وهكذا سيُنشئ جودو أشكال تصادم لكل عقدة من النوع <code>MeshInstance3D</code> عند النقر على إعادة الاستيراد Reimport.
</p>

<h2 id="-3">
	خلاصة
</h2>

<p>
	تعلمنا في هذا المقال طريقة استيراد أصول ثلاثية أبعاد جاهزة إلى <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">محرك الألعاب جود</a>و وضبط بعض الخيارات المهمة لعملية الاستيراد وكتابة سكريبت لأتمتتها.
</p>

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/3d/assets/index.html" rel="external nofollow">Assets</a> و <a href="https://kidscancode.org/godot_recipes/4.x/3d/assets/importing_assets/index.html" rel="external nofollow">Importing Assets</a>
</p>

<h2 id="-4">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%AF%D8%A1-%D9%88%D8%A5%D9%86%D9%87%D8%A7%D8%A1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%AC%D9%88%D8%AF%D9%88-godot-r2592/" rel="">بدء وإنهاء الألعاب في محرك جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%85%D9%86%D8%B7%D9%82%D8%A9-%D8%A7%D9%84%D9%84%D8%B9%D8%A8-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%AC%D9%88%D8%AF%D9%88-r2444/" rel="">إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2508/" rel="">إنشاء شخصيات ثلاثية الأبعاد في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%8B%D8%A7-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D9%83-%D8%AC%D9%88%D8%AF%D9%88-r2450/" rel="">تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2593</guid><pubDate>Mon, 18 Aug 2025 16:04:01 +0000</pubDate></item><item><title>&#x628;&#x62F;&#x621; &#x648;&#x625;&#x646;&#x647;&#x627;&#x621; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; &#x641;&#x64A; &#x645;&#x62D;&#x631;&#x643; &#x62C;&#x648;&#x62F;&#x648; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D8%AF%D8%A1-%D9%88%D8%A5%D9%86%D9%87%D8%A7%D8%A1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%AC%D9%88%D8%AF%D9%88-godot-r2592/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/04..png.9e9675c3ff474556e25bc89f824f39e2.png" /></p>
<p>
	سنتعرف في هذا المقال وهو جزء من <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">سلسلة دليل جودو</a>، على الخطوة الأخيرة في بناء لعبة سفينة الفضاء ثنائية الأبعاد، وهي كيفية إضافة زر لبدء اللعبة وإعلان نهاية اللعبة Game Over وهو ما سنتعلمه في مقال اليوم.
</p>

<h2 id="">
	إطلاق اللعبة
</h2>

<p>
	عندما نشغّل اللعبة حاليًا من داخل <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">محرك الألعاب جودو</a>، سيبدأ اللعب مباشرةً، ولهذا سنضيف زرًا كي لا تبدأ اللعبة إلا بعد النقر عليه. لتحقيق ذلك، سنضع زر البدء في مكان مركزي في واجهة اللعبة ونمكّن اللاعب من النقر عليه لبدء اللعبة.
</p>

<p>
	يمكن تحقيق ذلك باستخدام عقدة <code>CenterContainer</code> و<code>CanvasLayer</code>، وضبط إعدادات التموضع Layout لضمان أن الزر سيكون في مركز الشاشة ويستجيب جيدًا على مختلف أحجام الشاشات.
</p>

<p>
	سنضيف <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">عقدةً</a> من النوع <code>CenterContainer</code> إلى المشهد الرئيسي وكابن للعقدة <code>CanvasLayer</code>، ونضبط الخاصية <code>Layout&gt;Anchor Preset&gt;Layout Mode</code> لتكون قيمتها على كامل المستطيل Full Rect.
</p>

<p>
	سنضيف بعد ذلك عقدة ابن من النوع <code>TextureButton</code> ونسمّيها Start، ثم نضع الصورة <code>START (48 x 8).png</code> في الخاصية <code>Textures&gt;Normal</code> لهذا الزر.
</p>

<p>
	سنضيف مرجعًا إلى الزر في أعلى سكريبت المشهد الرئيسي كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_8" style=""><span class="lit">@onready</span><span class="pln"> var start_button </span><span class="pun">=</span><span class="pln"> $CanvasLayer</span><span class="pun">/</span><span class="typ">CenterContainer</span><span class="pun">/</span><span class="typ">Start</span></pre>

<p>
	والآن سنصل الإشارة <code>pressed</code> الخاصة بالزر إلى العقدة <code>Main</code> ثم نضيف الشيفرة التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_10" style=""><span class="pln">func _on_start_pressed</span><span class="pun">():</span><span class="pln">
    start_button</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span><span class="pln">
    new_game</span><span class="pun">()</span></pre>

<p>
	تعالج الدالة <code>()new_game</code> عملية إطلاق اللعبة، ولهذا سنعدّل مضمون الدالة <code>()ready_</code> كي تعرض زر البدء بدلًا من نشر الأعداء:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_12" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    start_button</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span><span class="pln">
</span><span class="com">#    spawn_enemies()</span></pre>

<p>
	سنضيف الآن الدالة <code>()new_game</code> إلى السكريبت الرئيسي على النحو الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_14" style=""><span class="pln">func new_game</span><span class="pun">():</span><span class="pln">
    score </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
    $CanvasLayer</span><span class="pun">/</span><span class="pln">UI</span><span class="pun">.</span><span class="pln">update_score</span><span class="pun">(</span><span class="pln">score</span><span class="pun">)</span><span class="pln">
    $Player</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span><span class="pln">
    spawn_enemies</span><span class="pun">()</span></pre>

<p>
	يجب أن يظهر زر البداية الآن عند تشغيل اللعبة، وسيؤدي الضغط عليه إلى انطلاقها
</p>

<h2 id="-1">
	إنهاء اللعبة
</h2>

<p>
	من أجل إنهاء اللعبة، ومنحها الشكل النهائي بالعبارة الشهيرة Game Over، سنحتاج إلى إضافة عقدة ابن من النوع <code>TextureRect</code> إلى العقدة <code>CenterContainer</code>، وسنسميها GameOver.
</p>

<p>
	سنستخدم الصورة <code>GAME_OVER (72 x 8).png</code> كخلفية لها، وستتداخل هذه العقدة مع عقدة زر البداية، لكن لن يؤثر ذلك على اللعبة لأن كلًا منهما سيظهر في توقيت مختلف.
</p>

<p>
	سنضيف الآن مرجعًا آخر إلى أعلى السكريبت الرئيسي كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_16" style=""><span class="lit">@onready</span><span class="pln"> var game_over </span><span class="pun">=</span><span class="pln"> $CanvasLayer</span><span class="pun">/</span><span class="typ">CenterContainer</span><span class="pun">/</span><span class="typ">GameOver</span></pre>

<p>
	نضيف بعد ذلك الدالة <code>()game_over.hide</code> إلى الدالة <code>()ready_</code>، ثم نصل إشارة اللاعب <code>died</code> إلى <code>Main</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1977_18" style=""><span class="pln">func _on_player_died</span><span class="pun">():</span><span class="pln">
    get_tree</span><span class="pun">().</span><span class="pln">call_group</span><span class="pun">(</span><span class="str">"enemies"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"queue_free"</span><span class="pun">)</span><span class="pln">
    game_over</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span><span class="pln">
    await get_tree</span><span class="pun">().</span><span class="pln">create_timer</span><span class="pun">(</span><span class="lit">2</span><span class="pun">).</span><span class="pln">timeout
    game_over</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span><span class="pln">
    start_button</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span></pre>

<p>
	وهنا ستعرض الشيفرة السابقة عبارة Game Over لمدة ثانيتين، ثم تعود باللاعب إلى زر البداية كي يتمكن من إعادة اللعبة.
</p>

<h2 id="-2">
	الخلاصة
</h2>

<p>
	بهذا نكون قد أنهينا بناء لعبة بسيطة ثنائية الأبعاد عبر محرك الألعاب جودو، وإن أردت الاطلاع على المزيد من الأفكار والتقنيات في استخدام محرّك الألعاب جودو، وكيفية تسخيرها في تحويل أفكارك إلى ألعاب احترافية ننصحك بالاطلاع على <a href="https://academy.hsoub.com/learn/game-development/" rel="">دورة تطوير الألعاب باستخدام محرك جودو</a> التي تقدمها <a href="https://academy.hsoub.com" rel="external">أكاديمية حسوب</a> التي تنطلق بك من الصفر وحتى الاحتراف من خلال دروس فيديو أعدها وقدمها مطورون خبراء في هذا المجال وباللغة العربية.
</p>

<p>
	وإن كنت تشعر أنك فهمت التقنيات التي ناقشناها في هذه السلسلة، فأنت مستعد لتطوير اللعبة أكثر. لهذا، حاول إضافة ميزة واحدة جديدة إلى اللعبة، ونقترح عليك بعضًا منها:
</p>

<ul>
	<li>
		<strong>إضافة أنواع مختلفة من الأعداء</strong>: ستجد أيقونات مختلفة للأعداء استفد من ذلك
	</li>
	<li>
		<strong>مجموعات من الأعداء</strong>: أضف مجموعة أخرى من الأعداء عندما تنتهي من المجموعة الأولى
	</li>
	<li>
		<strong>كبير الأعداء</strong>: جرب أن تضيف عدوًا أكبر حجماً من الأعداء العاديين
	</li>
	<li>
		<strong>معززات القوة</strong>: جرب عرض جوائز تعزز قوة المركبة الفضائية، وفكر كيف ستجمعها
	</li>
	<li>
		<strong>إعادة شحن الدرع</strong>: أضف معززات لتدعيم الدرع الذي يحمي السفينة الفضائية
	</li>
	<li>
		<strong>تطوير القذائف</strong>: أضف أنواعًا أخرى من القذائف
	</li>
	<li>
		<strong>إضافة مؤثرات صوتية وأصوات</strong>: يمكنك إضافة تأثيرات صوتية إلى اللعبة لجعلها أكثر جاذبية
	</li>
</ul>

<p>
	ترجمة -وبتصرف للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_10/index.html-" rel="external nofollow">Starting and Ending the Game</a> و <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_end/index.html" rel="external nofollow">Wrapping up</a>.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D9%88%D8%A7%D8%AC%D9%87%D8%A9-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%B9%D8%A7%D8%B1%D8%B6-%D8%A7%D9%84%D9%86%D8%AA%D9%8A%D8%AC%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-r2591/" rel="">بناء واجهة المستخدم وعارض النتيجة في لعبة السفينة الفضائية</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D8%B9%D8%AF%D9%88-%D9%84%D9%84%D9%86%D8%B4%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-r2590/" rel="">إنشاء مشهد إطلاق العدو للنشر في لعبة السفينة الفضائية</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2589/" rel="">إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2592</guid><pubDate>Mon, 11 Aug 2025 16:08:01 +0000</pubDate></item><item><title>&#x628;&#x646;&#x627;&#x621; &#x648;&#x627;&#x62C;&#x647;&#x629; &#x627;&#x644;&#x645;&#x633;&#x62A;&#x62E;&#x62F;&#x645; &#x648;&#x639;&#x627;&#x631;&#x636; &#x627;&#x644;&#x646;&#x62A;&#x64A;&#x62C;&#x629; &#x641;&#x64A; &#x644;&#x639;&#x628;&#x629; &#x627;&#x644;&#x633;&#x641;&#x64A;&#x646;&#x629; &#x627;&#x644;&#x641;&#x636;&#x627;&#x626;&#x64A;&#x629;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D9%88%D8%A7%D8%AC%D9%87%D8%A9-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%B9%D8%A7%D8%B1%D8%B6-%D8%A7%D9%84%D9%86%D8%AA%D9%8A%D8%AC%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-r2591/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/03..png.b3d51e733fc3e92c22c377d480b28011.png" /></p>
<p>
	سنشرح في هذا المقال وهو جزء من <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">سلسلة دليل جودو</a>، كيفية بناء واجهة المستخدم وعارض النتيجة في الألعاب ثنائية الأبعاد عبر محرك Godot.
</p>

<p>
	آخر الأقسام الرئيسية التي علينا بناؤها في لعبتنا ثنائية الأبعاد هي واجهة المستخدم User Interface؛ إذ سنحتاج إلى طريقة لعرض نتيجة اللاعب وغيرها من المعلومات. ولتنفيذ الأمر، سنستخدم عقدةً مختلفة من النوع <code>Control</code> التي يزودنا بها محرك الألعاب جودو لبناء واجهات المستخدم.
</p>

<h2 id="">
	مشهد واجهة المستخدم
</h2>

<p>
	سنبدأ المشهد <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">بعقدة</a> من النوع <code>MarginContainer</code> ونسميها <code>UI</code>، حيث تضمن الحاوية <code>MarginContainer</code> ألا يقترب العقد الأبناء كثيرًا من الحافة من خلال إضافة هوامش حولها.
</p>

<p>
	سننقر الآن على العقدة <code>UI</code> وننتقل إلى نافذة الفاحص Inspector، ثم نضبط قيم الهوامش الأربعة على 10 ضمن الخاصية <code>Theme Overrides&gt;Constants</code>.
</p>

<p>
	ننتقل بعدها إلى نافذة العرض الثنائي وننقر على أيقونة تجهيزات المراسي Anchor Preset وأيقونة المرساة Anchor، ثم نختار موقع الحاوية ليكون في أعلى الشاشة وعلى اتساعها بالعرض بالأعلى Top Wide.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172880" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_top_wide.png.b1ffc8f7053ec0a1755e580c090aecb6.png" rel=""><img alt="01 top wide" class="ipsImage ipsImage_thumbnailed" data-fileid="172880" data-unique="4c4tnxxhd" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_top_wide.png.b1ffc8f7053ec0a1755e580c090aecb6.png"> </a>
</p>

<p>
	نضيف تاليًا عقدةً من النوع <code>HBoxContainer</code>، وهي نوع من الحاويات التي تنظم الأبناء أفقيًا. سنضيف ضمن هذه العقدة عقدة ابن من النوع <code>TextureProgressBar</code>، وهي شريط تقدم يمثل وضع درع المركبة الفضائية، وسنسمي هذه العقدة ShieldBar.
</p>

<p>
	لا تضم مجموعة الصور التي نزّلناها في مشروعنا أية صورة مناسبة لشريط التقدم، لذلك سننزلّ الصورتين التاليتين ونحفظهما في مجلد اللعبة. ضع صورة الخلفية الخضراء <a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172881" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_bar_prog.png.6959a736cf737f2d7be6b092d27c8407.png" rel=""><img alt="02 bar prog" class="ipsImage ipsImage_thumbnailed" data-fileid="172881" data-unique="lb4qfmm2p" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_bar_prog.png.6959a736cf737f2d7be6b092d27c8407.png"></a> كقيمة للخاصية <code>Texture&gt;Progress</code>، والصورة البيضاء <a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172877" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_bar_under.png.d8efb899c11487ad7f41aba809257d97.png" rel=""><img alt="03 bar under" class="ipsImage ipsImage_thumbnailed" data-fileid="172877" data-unique="on08gkvmp" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_bar_under.png.d8efb899c11487ad7f41aba809257d97.png"></a> كقيمة للخاصية <code>Texture&gt;Under</code>.<br type="_moz">
	 
</p>

<p>
	سنلاحظ مباشرة أن الشريط صغير جدًا، لذلك سنغير قيمة الخاصية <code>Minimum Custom Size</code> لتصبح <code>(80,60)</code> وسنرى أن المستطيل البرتقالي قد كبر.
</p>

<p>
	وكما هو واضح، لن يكون تمدد الصورة جميلًا، وقد يبدو سيئًا أيضًا، لهذا سنفعّل الخيار <code>Range&gt;Nine Patch Stretch</code> ونضبط بعدها قيم الخاصية <code>Stretch Margin</code> الأربعة التي ستظهر على <code>3</code>.
</p>

<p>
	من المفترض أن نرى الآن شريطًا طويلًا فارغًا. ولمعرفة كيف سيبدو عندما يبدأ بالامتلاء، سنغير قيمة الخاصية <code>Range&gt;Value</code> إلى أي قيمة بين 0 و 100.
</p>

<p style="text-align: center;">
	<img alt="04_level_bar.png" class="ipsImage ipsImage_thumbnailed" data-fileid="172882" data-ratio="39.97" data-unique="0s2lvwhtn" width="593" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_level_bar.png.01d4c21773907fc1a585ab99bc3df46f.png">
</p>

<p>
	سنعرض إلى اليسار نتيجة اللاعب، وقد نستخدم عقدةً من النوع <code>Label</code> وخط مناسب. وبالطبع لن تكون الخطوط التي يقدمها نظام التشغيل مناسبةً للعبة، ولهذا سنستخدم أيقونات الأرقام الموجودة في مجلد الصور، وذلك بعد معالجتها عن طريق الشيفرة لعرضها بالشكل الصحيح.
</p>

<h2 id="-1">
	إنشاء عداد للنتيجة
</h2>

<p>
	سننشئ مشهدًا جديدًا أساسه عقدة من النوع <code>HBoxContainer</code> ونسيمها <code>ScoreCounter</code>. سنضبط هذه العقدة ليكون مركز ارتكاز العقدة بالعرض بالأعلى Top Wide، أي أن العقدة ستمتد أفقيًا وتتموضع عند الجزء العلوي من المشهد، ومحاذاتها نهاية الإنكماش End، أي ستكون محاذاة العقدة على الطرف الأيمن أو نهاية المساحة المتاحة باستخدام الزر المجاورة لأيقونة المرساة.
</p>

<p>
	سنضبط أيضًا الخاصية <code>Theme Overrides&gt;Constants&gt;Separation</code> على القيمة <code>0</code>، وهنا علينا تفعيل الخيار إلى جانب الخاصية.
</p>

<p>
	سنضيف الآن مجموعةً من العقد النصية من النوع <code>TextureRect</code> ضمن الحاوية الأفقية لعرض الأرقام، وسنضيف عقدةً واحدة أولًا ثم نضاعف العدد.
</p>

<p>
	سنسمي العقدة النصية <code>Digit0</code> ثم ننتقل إلى الفاحص، فالخاصية <code>Texture</code>، ونختار إنشاء واحدة جديدة New AtlasTexture. سننقر بعد ذلك على هذا الخيار لتظهر لنا نافذة أسفلها الخاصية <code>Atlas</code>. نسحب الآن الصورة <code>Number_font (8 x 8).png</code> إلى هذه الخاصية ثم نضبط قيم الخاصية <code>Region</code> على <code>(32, 8, 8, <span class="ipsEmoji">😎</span></code>. بعد ذلك ننقر مجددًا على الخاصية <code>Texture</code> لتغلق النافذة، ثم اضبط قيمة الخاصية <code>Stretch Mode</code> على <code>Keep Aspect Centered</code>.
</p>

<p>
	سنعود الآن إلى نافذة المشهد ونختار العقدة <code>Digit0</code>، ثم نضغط على المفتاحين <code>Ctrl+D</code> سبع مرات لإنشاء سبع عقد أخرى مماثلة للأولى من ناحية الشكل والخاصيات. حيث ستبدو نافذة العرض بعد هذه الخطوات كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172878" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_digits_nodes.png.f088e3a0f639d498177989e992d72f48.png" rel=""><img alt="05 digits nodes" class="ipsImage ipsImage_thumbnailed" data-fileid="172878" data-unique="ruup32vsh" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_digits_nodes.thumb.png.b10b79cfdd2f968280b4edb9a129f906.png"> </a>
</p>

<p>
	على الرغم من إنشاء ثمانية عقد <code>TextureRect</code> للأرقام، إلا أن قيمة الخاصية <code>Texture</code> تبقى نفسها، وهذه مشكلة، فعند تغيير الخاصية <code>Region</code> لعرض الأرقام المختلفة ستعرض كل العقد صورة الرقم نفسه.
</p>

<p>
	يعود السبب في ذلك إلى أن كائنات الموارد مثل العقدة <code>Texture</code> ستحمّل في الذاكرة مرةً واحدة ثم تتشاركها العقدة المختلفة. وهذا أمر فعّال جدًا لأنه لا يهدر الذاكرة بتحميل نفس النسخة من المورد عدة مرات. إذًا، علينا تخصيص جزء من المورد لكل عقدة حتى تكون الصورة المعروضة في كل عقدة فريدة، وهنا سننقر في كل عقدة على السهم المجاور للخاصية <code>AtlasTexture</code> ثم نضغط على خيار اجعله فريدًا Make Unique.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172883" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_make_unique.png.a0fa299ce244cacee0f795579dc964e3.png" rel=""><img alt="06 make unique" class="ipsImage ipsImage_thumbnailed" data-fileid="172883" data-unique="rqc6dlrr9" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_make_unique.png.a0fa299ce244cacee0f795579dc964e3.png"> </a>
</p>

<p>
	نضيف الآن سكريبت إلى العقدة <code>ScoreCounter</code>، ونختار فيها المنطقة الصحيحة <code>Region</code> من الصورة لكل رقم نريد عرضه:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_10" style=""><span class="pln">extends </span><span class="typ">HBoxContainer</span><span class="pln">

var digit_coords </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    </span><span class="lit">1</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">2</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">3</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">16</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">4</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">24</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">5</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">32</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">6</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">7</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">8</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">16</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">9</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">24</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln">
    </span><span class="lit">0</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">32</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">)</span><span class="pln">
</span><span class="pun">}</span><span class="pln">

func display_digits</span><span class="pun">(</span><span class="pln">n</span><span class="pun">):</span><span class="pln">
    var s </span><span class="pun">=</span><span class="pln"> </span><span class="str">"%08d"</span><span class="pln"> </span><span class="pun">%</span><span class="pln"> n
    </span><span class="kwd">for</span><span class="pln"> i </span><span class="kwd">in</span><span class="pln"> </span><span class="lit">8</span><span class="pun">:</span><span class="pln">
      get_child</span><span class="pun">(</span><span class="pln">i</span><span class="pun">).</span><span class="pln">texture</span><span class="pun">.</span><span class="pln">region </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">digit_coords</span><span class="pun">[</span><span class="pln">int</span><span class="pun">(</span><span class="pln">s</span><span class="pun">[</span><span class="pln">i</span><span class="pun">])],</span><span class="pln">
          </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">))</span></pre>

<p>
	تبدأ الشيفرة بتشكيل قائمة من الإحداثيات التي تمثل أماكن من الصورة يحدد كل منها مكان وجود رقم، ثم تنسّق الدالة <code>()display_digits</code> العدد المطلوب ليكون من 8 أرقام، بحيث لو كان أمامنا الرقم 285 مثلًا، فسيكتب بالشكل 00000258. بعد ذلك، سنضع الرقم المناسب في كل منزلة من العدد السابق اعتمادًا على مصفوفة الإحداثيات.
</p>

<h2 id="ui">
	إضافة سكريبت واجهة المستخدم UI
</h2>

<p>
	نعود الآن إلى المشهد ui ثم نضيف المشهد <code>ScoreCounter</code> إلى العقدة <code>HBoxContainer</code>. نضيف بعد ذلك السكريبت التالي إلى العقدة UI:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_12" style=""><span class="pln">extends </span><span class="typ">MarginContainer</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var shield_bar </span><span class="pun">=</span><span class="pln"> $HBoxContainer</span><span class="pun">/</span><span class="typ">ShieldBar</span><span class="pln">
</span><span class="lit">@onready</span><span class="pln"> var score_counter </span><span class="pun">=</span><span class="pln"> $HBoxContainer</span><span class="pun">/</span><span class="typ">ScoreCounter</span><span class="pln">

func update_score</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    score_counter</span><span class="pun">.</span><span class="pln">display_digits</span><span class="pun">(</span><span class="pln">value</span><span class="pun">)</span><span class="pln">


func update_shield</span><span class="pun">(</span><span class="pln">max_value</span><span class="pun">,</span><span class="pln"> value</span><span class="pun">):</span><span class="pln">
    shield_bar</span><span class="pun">.</span><span class="pln">max_value </span><span class="pun">=</span><span class="pln"> max_value
    shield_bar</span><span class="pun">.</span><span class="pln">value </span><span class="pun">=</span><span class="pln"> value</span></pre>

<p>
	سنستدعي هاتين الدالتين في المشهد الرئيسي Main في كل مرة نحتاج فيها إلى تغيير النتيجة أو قوة درع السفينة.
</p>

<h2 id="uimain">
	إضافة المشهد UI إلى المشهد الرئيسي Main
</h2>

<p>
	سنضيف الآن عقدةً من النوع <code>CanvasLayer</code> إلى المشهد الرئيسي Main، ثم منسخ إليها المشهد UI كعقدة ابن. ستُنشئ العقدة <code>CanvasLayer</code> طبقة رسم جديدة، ولهذا ستُرسم واجهة المستخدم فوق جميع مكونات اللعبة؛ ولحل هذه المشكلة سنغيّر التابع التالي في السكريبت <code>main.gd</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_14" style=""><span class="pln">func _on_enemy_died</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    score </span><span class="pun">+=</span><span class="pln"> value
    $CanvasLayer</span><span class="pun">/</span><span class="pln">UI</span><span class="pun">.</span><span class="pln">update_score</span><span class="pun">(</span><span class="pln">score</span><span class="pun">)</span></pre>

<h2 id="-2">
	درع اللاعب
</h2>

<p>
	يمكننا إضافة الدرع إلى سكريبت اللاعب، عبر إضافة الأسطر التالية إلى الملف <code>player.gd</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_16" style=""><span class="pln">signal died
signal shield_changed

</span><span class="lit">@export</span><span class="pln"> var max_shield </span><span class="pun">=</span><span class="pln"> </span><span class="lit">10</span><span class="pln">
var shield </span><span class="pun">=</span><span class="pln"> max_shield</span><span class="pun">:</span><span class="pln">
    set </span><span class="pun">=</span><span class="pln"> set_shield</span></pre>

<p>
	سنستدعي الدالة <code>()set_shield</code> من خلال عملية الإسناد <code>= set</code> في كل مرة يُضبط فيها المتغير <code>shield</code> الذي يمثل قيمة درع المركبة كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_18" style=""><span class="pln">func set_shield</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    shield </span><span class="pun">=</span><span class="pln"> min</span><span class="pun">(</span><span class="pln">max_shield</span><span class="pun">,</span><span class="pln"> value</span><span class="pun">)</span><span class="pln">
    shield_changed</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="pln">max_shield</span><span class="pun">,</span><span class="pln"> shield</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> shield </span><span class="pun">&lt;=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
      hide</span><span class="pun">()</span><span class="pln">
      died</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">()</span></pre>

<p>
	نستطيع أيضًا وصل إشارة المركبة <code>area_entered</code> كي نلتقط اصطدام العدو بالمركبة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_20" style=""><span class="pln">func _on_area_entered</span><span class="pun">(</span><span class="pln">area</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> area</span><span class="pun">.</span><span class="pln">is_in_group</span><span class="pun">(</span><span class="str">"enemies"</span><span class="pun">):</span><span class="pln">
      area</span><span class="pun">.</span><span class="pln">explode</span><span class="pun">()</span><span class="pln">
      shield </span><span class="pun">-=</span><span class="pln"> max_shield </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span></pre>

<p>
	لنضف الآن بعض الضرر إلى درع المركبة عندما تُصاب في سكريبت قذائف العدو <code>enemy_bullet.gd</code>، وذلك على النحو الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4068_22" style=""><span class="pln">func _on_area_entered</span><span class="pun">(</span><span class="pln">area</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> area</span><span class="pun">.</span><span class="pln">name </span><span class="pun">==</span><span class="pln"> </span><span class="str">"Player"</span><span class="pun">:</span><span class="pln">
      queue_free</span><span class="pun">()</span><span class="pln">
      area</span><span class="pun">.</span><span class="pln">shield </span><span class="pun">-=</span><span class="pln"> </span><span class="lit">1</span></pre>

<p>
	في الأخير، علينا وصل إشارة اللاعب <code>shield_changed</code> إلى الدالة التي تحدّث شريط الدرع في واجهة المستخدم، ولهذا سننتقل إلى الفاحص بعد اختيار عقدة اللاعب <code>Player</code> في المشهد الرئيسي. سننقر بعد ذلك في نافذة العقدة Node نقرًا مزدوجًا على الإشارة، وذلك لفتح نافذة توصل إشارة إلى دالة Connect a Signal.
</p>

<p>
	سنختار بعد ذلك العقدة <code>UI</code> ثم نكتب <code>update_shield</code> في صندوق الدالة المتلقية Receiver Method.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172879" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_connect_signal.png.447424e2785278d9b70f869a007fe392.png" rel=""><img alt="07 connect signal" class="ipsImage ipsImage_thumbnailed" data-fileid="172879" data-unique="a7lmhcomt" style="width: 300px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_connect_signal.png.447424e2785278d9b70f869a007fe392.png"> </a>
</p>

<p>
	يمكننا الآن تشغيل اللعبة والتأكد من أن طاقة الدرع تنخفض عندما تصيبه قذيفة أو مركبة معادية.
</p>

<h2 id="-3">
	ختامًا
</h2>

<p>
	بهذا نكون قد تعرفنا على كيفية بناء واجهة المستخدم وعارض النتيجة في الألعاب ثنائية الأبعاد عبر <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">محرك الألعاب جودو</a>، وسنتعرف على كيفية تطوير اللعبة أكثر بالمقالات الموالية. ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_09/index.html" rel="external nofollow">UI and Score</a>.
</p>

<h2 id="-4">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D8%B9%D8%AF%D9%88-%D9%84%D9%84%D9%86%D8%B4%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-r2590/" rel="">إنشاء مشهد إطلاق العدو للنشر في لعبة السفينة الفضائية</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2589/" rel="">إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2591</guid><pubDate>Fri, 08 Aug 2025 16:00:01 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x645;&#x634;&#x647;&#x62F; &#x625;&#x637;&#x644;&#x627;&#x642; &#x627;&#x644;&#x639;&#x62F;&#x648; &#x644;&#x644;&#x646;&#x634;&#x631; &#x641;&#x64A; &#x644;&#x639;&#x628;&#x629; &#x627;&#x644;&#x633;&#x641;&#x64A;&#x646;&#x629; &#x627;&#x644;&#x641;&#x636;&#x627;&#x626;&#x64A;&#x629;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D8%B9%D8%AF%D9%88-%D9%84%D9%84%D9%86%D8%B4%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-r2590/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/02..png.1a52028ee0a918ac9990cd9f0a327789.png" /></p>
<p>
	بعد أن بنينا شخصية لاعب قادر على إطلاق النار على العدو في لعبتنا ثنائية الأبعاد ضمن محرك الألعاب جودو، لا بد من إنشاء عدو في اللعبة ونبرمج طريقة حركته في اللعبة وطريقة إضافة آلية إطلاق النار إليه ليهاجم اللاعب، وهذا ما سنتعلمه في مقالنا الذي هو جزء من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a>.
</p>

<h2 id="">
	بناء المشهد
</h2>

<p>
	سنستخدم العقدة <code>Area2D</code> كأساس لبناء المشهد لأننا بحاجة إلى اكتشاف تداخل كائن العدو مع اللاعب أو قذائفه، كما سنحتاج إلى عدة عقد نلخصها كالتالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_8" style=""><span class="typ">Enemy</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Area2D</span><span class="pln">
   </span><span class="typ">Sprite2D</span><span class="pln">
   </span><span class="typ">CollisionShape2D</span><span class="pln">
   </span><span class="typ">AnimationPlayer</span><span class="pln">
  </span><span class="typ">MoveTimer</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Timer</span><span class="pln">
  </span><span class="typ">ShootTimer</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Timer</span></pre>

<p>
	نختار العقدة Enemy ثم ننقر على النافذة <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">عقدة Node</a> إلى جوار الفاحص inspector وننقر بعدها على مجموعات Groups، ثم على أيقونة <code>+</code> لإضافة مجموعة جديدة سنسميها <strong>enemies</strong>.
</p>

<p>
	<strong>ملاحظة</strong>: من المهم هنا تذكر أن السكريبت في مشهد القذائف يبحث عن كائنات من مجموعة الأعداء enemies.
</p>

<p>
	سننتقل بعد ذلك إلى العقدة <code>Sprite2D</code> ثم نضيف الصورة <code>Bon_Bon (16 x 16).png</code> إلى الخاصية <code>Texture</code> ونضبط قيمة الخاصية <code>Animation&gt;Hframes</code> على <code>4</code>.
</p>

<p>
	<strong>ملاحظة</strong>: تمثل الصورة <code>Bon_Bon (16 x 16).png</code> مجموعة من الإطارات الرسومية التي تشكل الحركة فكل إطار يمثل وضعية أو جزءًا من هذه الحركة، وعند ضبط Hframes إلى 4 معناه أننا نخبر جودو بأن الصورة مقسمة أفقيًا إلى 4 إطارات، مما يسمح له بالتعامل مع كل إطار على حدة فإذا كانت الصورة بحجم 16 × 16 بكسل فعند تقسيمها إلى 4 إطارات أفقية سيكون كل إطار بعرض 4 بكسل وارتفاع 16 بكسل ويمكن عرض كل إطار بشكل منفصل لتكوين الحركة.
</p>

<p>
	ننقر الآن على العقدة <code>CollisonShape2D</code> ثم ننتقل إلى الخاصية <code>Shape</code> ونختار شكل التصادم <code>RectangleShape2D</code> ونضبط حجمه ليناسب أبعاد الأيقونة؛ ثم ننتقل إلى أيقونتي المؤقتات ونفعّل الخاصية <code>One Shot</code> لكل مؤقت.
</p>

<p>
	ننقر على العقدة <code>AnimationPlayer</code> وننتقل إلى لوحة التحريك Animation Panel، وننشئ رسمًا متحركًا باسم bounce ونضبطه ليكرر نفسه looping وأن يبدأ العمل تلقائيًا عند تشغيل اللعبة autoplay بالنقر على الأزرار الموافقة.
</p>

<p>
	بعد هذه المرحلة، سنضبط الخيار انطباق أو محاذاة Snap في أسفل اللوحة على القيمة <code>0.05</code>.
</p>

<p>
	سننشئ الآن مسارات تحريك لتتبع الخاصيتين <code>Textrure</code> و <code>Hframes</code> للشخصية وهي بحالتنا العقدة <code>Sprite2D</code>، وذلك بالنقر على أيقونة المفتاح إلى جانب كل منهما. وسنحتاج إلى مفاتيح مرجعية على هذه المسارات لاحقًا عندما نضيف رسمًا متحركًا جديدًا باسم explode يستخدم قيمًا مختلفة لهاتين الخاصيتين.
</p>

<p>
	سنضيف الآن مسارًا تحريكيًا للخاصية <code>Frame</code> بالنقر بدايةً على المفتاح إلى جانب الخاصية، ونبدأ بعدها بإنشاء مفتاح مرجعي يتغير عنده إطار الشخصية وفق الترتيب التالي 2&gt;1&gt;0&gt;3&gt;0، بحيث يفصل بين كل مفتاحين 0.1 ثانية ماعدا آخر إطارين سيكونان متداخلين.
</p>

<p>
	لتنفيذ العملية، سنحرك المقبض الأزرق على الخط الزمني مسافة 0.1 ثانية ونضع قيمة الإطار المطلوب في مربع الخاصية Frame، ثم ننقر على زر المفتاح وهكذا. وسينتج عن تحريك الرسم بهذا الشكل أيقونة تنمو وتعود إلى حجمها الطبيعي في النهاية. وسيبدو شكل لوحة التحريك كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172875" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_bounce_animation.png.9a88f049bd11d45dc0fd12896033953f.png" rel=""><img alt="01 bounce animation" class="ipsImage ipsImage_thumbnailed" data-fileid="172875" data-unique="fk004rwjx" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_bounce_animation.png.9a88f049bd11d45dc0fd12896033953f.png"> </a>
</p>

<p>
	ننقر على زر التشغيل في لوحة التحريك لرؤية نتيجة العمل، وبإمكاننا طبعًا تعديل الحركة حسب رغبتنا.
</p>

<p>
	نضيف الآن رسمًا متحركًا جديدًا باسم explode ونضبط مدته على 0.4 ثانية، كما نستبدل صورة الشخصية بالصورة <code>Explosion (16 x 16).png</code> وننشئ مسارات للخاصيات <code>Frame</code> و <code>Texture</code> و <code>Hframes</code>.
</p>

<p>
	سنعدّل قيمة الخاصية <code>Hframes</code> إلى <code>5</code>، ثم كما فعلنا سابقًا، ونضع مفتاحين مرجعيين في المسار الأول عند اللحظة 0، وتكون قيمة الإطار 0 والثانية عند اللحظة 0.4، كما ستكون قيمة الإطار 4.
</p>

<p>
	عند الانتهاء من كل ذكل، سنشغل الرسم المتحرك لرؤية النتيجة.
</p>

<h2 id="-1">
	سكريبت شخصية العدو
</h2>

<p>
	ينتشر <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B9%D8%AF%D9%88-%D9%88%D8%AD%D9%8A%D9%88%D8%A7%D9%86-%D8%A3%D9%84%D9%8A%D9%81-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-godot-r2584/" rel="">الأعداء</a> أعلى شاشة اللعبة على شكل شبكة، حيث ستقترب شبكة الأعداء من اللاعب بعد فترة زمنية محددة، ثم يعود إلى الأعلى ما لم يُدمر منها، كما سيطلق الأعداء النار على اللاعب دوريًا.
</p>

<p>
	سنضيف سكريبتًا إلى عقدة العدو ونعرّف المتغيرات التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_11" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

var start_pos </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var screensize </span><span class="pun">=</span><span class="pln"> get_viewport_rect</span><span class="pun">().</span><span class="pln">size</span></pre>

<p>
	يتتبع المتغير <code>start_pos</code> موقع انطلاق الأعداء كي يعودوا إلى موقعهم الأصلي بعد الحركة. سنضبط قيمة المتغير عندما ننشر الأعداء في الشبكة، وسنستدعي الدالة <code>()start</code> الخاصة بهم:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_13" style=""><span class="pln">func start</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">):</span><span class="pln">
  speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
  position </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> </span><span class="pun">-</span><span class="pln">pos</span><span class="pun">.</span><span class="pln">y</span><span class="pun">)</span><span class="pln">
  start_pos </span><span class="pun">=</span><span class="pln"> pos
  await get_tree</span><span class="pun">().</span><span class="pln">create_timer</span><span class="pun">(</span><span class="pln">randf_range</span><span class="pun">(</span><span class="lit">0.25</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.55</span><span class="pun">)).</span><span class="pln">timeout
  var tween </span><span class="pun">=</span><span class="pln"> create_tween</span><span class="pun">().</span><span class="pln">set_trans</span><span class="pun">(</span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TRANS_BACK</span><span class="pun">)</span><span class="pln">
  tween</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">self</span><span class="pun">,</span><span class="pln"> </span><span class="str">"position:y"</span><span class="pun">,</span><span class="pln"> start_pos</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> </span><span class="lit">1.4</span><span class="pun">)</span><span class="pln">
  await tween</span><span class="pun">.</span><span class="pln">finished
  $MoveTimer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> randf_range</span><span class="pun">(</span><span class="lit">5</span><span class="pun">,</span><span class="pln"> </span><span class="lit">20</span><span class="pun">)</span><span class="pln">
  $MoveTimer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> randf_range</span><span class="pun">(</span><span class="lit">4</span><span class="pun">,</span><span class="pln"> </span><span class="lit">20</span><span class="pun">)</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span></pre>

<p>
	سنستدعي الدالة السابقة عند نشر الأعداء، ونمرر إليها شعاع موضع يمثل مكان ظهور العدو على الشاشة، وسنلاحظ عندها أننا ننشر العدو فوق الحافة العلوية للشاشة. سنعطي <code>y</code> قيمة سالبة كي نتمكن من تحريك العدو عندما يدخل إلى الشاشة باستخدام مولد إطارات بينية Tween، ونضبط أيضًا قيمتي المؤقتين عشوائيًا كي لا يتحرك العدو ويطلق النار في نفس الوقت، وبعد ذلك سنصل إشارة <code>timeout</code> الخاصة بكل مؤقت.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_15" style=""><span class="pln">func _on_timer_timeout</span><span class="pun">():</span><span class="pln">
  speed </span><span class="pun">=</span><span class="pln"> randf_range</span><span class="pun">(</span><span class="lit">75</span><span class="pun">,</span><span class="pln"> </span><span class="lit">100</span><span class="pun">)</span><span class="pln">

func _on_shoot_timer_timeout</span><span class="pun">():</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> randf_range</span><span class="pun">(</span><span class="lit">4</span><span class="pun">,</span><span class="pln"> </span><span class="lit">20</span><span class="pun">)</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span></pre>

<p>
	يمكن هنا التحرك عند انتهاء المؤقت من عد الفترة الزمنية التي يُضبط عليها، لكننا لم ننشئ قذائف الأعداء بعد، وهذا ما سنتعامل معه لاحقًا.
</p>

<p>
	وبما أننا سنغير سرعة حركة الأعداء من خلال المتغير <code>speed</code>، فمعنى ذلك، أن بإمكاننا التحرك عبر كتابة الشيفرة التالية في دالة المعالجة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_17" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
  position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+=</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta
  </span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">&gt;</span><span class="pln"> screensize</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+</span><span class="pln"> </span><span class="lit">32</span><span class="pun">:</span><span class="pln">
    start</span><span class="pun">(</span><span class="pln">start_pos</span><span class="pun">)</span></pre>

<p>
	إن لم تكن قيمة المتغير <code>speed</code> هي 0، فسنرى العدو يتحرك على الشاشة، وعندما يصل إلى الأسفل يعود من الأعلى مجددًا.
</p>

<p>
	لقد كتبنا الدالة التي تعالج انفجار الأعداء عندما تصطدم بها قذيفة اللاعب الدالة <code>()explode</code> عندما بنينا مشهد القذيفة، لهذا سنضيف نفس الدالة هنا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_19" style=""><span class="pln">func explode</span><span class="pun">():</span><span class="pln">
  speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
  $AnimationPlayer</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="str">"explode"</span><span class="pun">)</span><span class="pln">
  set_deferred</span><span class="pun">(</span><span class="str">"monitoring"</span><span class="pun">,</span><span class="pln"> false</span><span class="pun">)</span><span class="pln">
  died</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="lit">5</span><span class="pun">)</span><span class="pln">
  await $AnimationPlayer</span><span class="pun">.</span><span class="pln">animation_finished
  queue_free</span><span class="pun">()</span></pre>

<p>
	سنوقف في هذه الدالة حركة العدو، ثم نشغّل الرسم المتحرك الخاص بعملية الانفجار ونحذف بعدها العدو في نهاية الدالة. وتضمن الدالة <code>()set_deferred</code> إيقاف الخاصية <code>monitoring</code> في كائن العدو كي لا تصيبه قذيفة أخرى أثناء انفجاره، وعندها سيصيب القذيفة التي تنفجر ويفجرها مجددًا ويستدعي الدالة التي تنفذ عملية التفجير مجددًا، وهكذا.
</p>

<p>
	سنضيف الآن الإشارة <code>died</code> إلى أعلى السكريبت:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_21" style=""><span class="pln">signal died</span></pre>

<p>
	وسنستخدم هذه الإشارة لإبلاغ المشهد الرئيسي أن اللاعب حقق بعض النقاط.
</p>

<h2 id="-2">
	نشر العدو
</h2>

<p>
	سنعود الآن إلى المشهد الرئيسي Main لإضافة بعض الأعداء إلى اللعبة، ولهذا سنضيف سكريبتًا إلى المشهد الرئيسي ونبدأ بتحميل مشهد العدو برمجيًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_23" style=""><span class="pln">extends </span><span class="typ">Node2D</span><span class="pln">

var enemy </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://enemy.tscn"</span><span class="pun">)</span><span class="pln">
var score </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span></pre>

<p>
	لن يبدأ نشر العدو في اللعبة قبل النقر على زر البداية. وطالما أننا لم نرتّب هذا الأمر بعد، فسننشره مباشرة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_25" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
  spawn_enemies</span><span class="pun">()</span><span class="pln">

func spawn_enemies</span><span class="pun">():</span><span class="pln">
  </span><span class="kwd">for</span><span class="pln"> x </span><span class="kwd">in</span><span class="pln"> range</span><span class="pun">(</span><span class="lit">9</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> y </span><span class="kwd">in</span><span class="pln"> range</span><span class="pun">(</span><span class="lit">3</span><span class="pun">):</span><span class="pln">
      var e </span><span class="pun">=</span><span class="pln"> enemy</span><span class="pun">.</span><span class="pln">instantiate</span><span class="pun">()</span><span class="pln">
      var pos </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> </span><span class="pun">(</span><span class="lit">16</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> </span><span class="lit">8</span><span class="pun">)</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> </span><span class="lit">24</span><span class="pun">,</span><span class="pln"> </span><span class="lit">16</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> </span><span class="lit">4</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> y </span><span class="pun">*</span><span class="pln"> </span><span class="lit">16</span><span class="pun">)</span><span class="pln">
      add_child</span><span class="pun">(</span><span class="pln">e</span><span class="pun">)</span><span class="pln">
      e</span><span class="pun">.</span><span class="pln">start</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">)</span><span class="pln">
      e</span><span class="pun">.</span><span class="pln">died</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">_on_enemy_died</span><span class="pun">)</span></pre>

<p>
	تنشر الشيفرة السابقة 27 عدوًا ضمن شبكة في النصف الأعلى من الشاشة، ولا بد من التأكد من وصل الإشارة <code>died</code> لكل عدو، ولهذا علينا إضافة الدالة التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_27" style=""><span class="pln">func _on_enemy_died</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
  score </span><span class="pun">+=</span><span class="pln"> value</span></pre>

<p>
	سنشغل الآن المشهد، وسنرى مجموعةً من الأعداء أعلى الشاشة يسقطون دوريًا نحو الأسفل.
</p>

<h2 id="-3">
	إطلاق النار من المركبات المعادية
</h2>

<p>
	طالما أن مركبات العدو ستتمكن من إطلاق النار، فسنمنحها شيئًا لتطلق النار عليه.
</p>

<h3 id="-4">
	مشهد قذائف العدو
</h3>

<p>
	سننشئ مشهدًا باسم EnemyBullet مشابهًا من ناحية التكوين لمشهد قذائف اللاعب. لن نكرر خطوات الإنشاء هنا طبعًا، لهذا يمكن العودة إلى مشهد قذائف اللاعب <code>bullet</code> في حال مصادفة مشكلة ما؛ إذ سيكون الفرق الوحيد هو الصورة المستخدمة للقذيفة في هذا المشهد، وهي الصورة <code>Enemy_projectile (16 x 16).png</code>، وسيختلف السكريبت هنا قليلًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_29" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">150</span><span class="pln">

func start</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">):</span><span class="pln">
  position </span><span class="pun">=</span><span class="pln"> pos

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
  position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+=</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	نصل الإشارتين <code>screen_exited</code> و <code>area_entered</code> العائدتين للعقدتين <code>VisibleOnScreenNotifier2D</code> و <code>Area2D</code> على التوالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_31" style=""><span class="pln">func _on_visible_on_screen_notifier_2d_screen_exited</span><span class="pun">():</span><span class="pln">
  queue_free</span><span class="pun">()</span><span class="pln">

func _on_area_entered</span><span class="pun">(</span><span class="pln">area</span><span class="pun">):</span><span class="pln">
  </span><span class="kwd">if</span><span class="pln"> area</span><span class="pun">.</span><span class="pln">name </span><span class="pun">==</span><span class="pln"> </span><span class="str">"Player"</span><span class="pun">:</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<p>
	<strong>ملاحظة</strong>: تُطلق الإشارة <code>screen_exited</code> عندما يخرج الكائن من الشاشة، وتُستخدم لحذفه أو إعادة ضبطه وتطلق الإشارة <code>area_entered</code> عندما يدخل جسم إلى منطقة محددة وهي تُستخدم للكشف عن الاصطدامات أو التفاعلات.
</p>

<p>
	وكما نلاحظ، نحن هنا نلتقط اصطدام القذيفة المعادية باللاعب، لكننا لم نحصل على النتيجة المطلوبة حتى اللحظة. سنعود إلى ذلك بعد إضافة طريقة تصف تضرر سفينة اللاعب عند إصابته.
</p>

<h3 id="-5">
	إضافة آلية إطلاق النار إلى العدو
</h3>

<p>
	سنحمّل مشهد قذيفة العدو في أعلى سكريبت العدو:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_33" style=""><span class="pln">var bullet_scene </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://enemy_bullet.tscn"</span><span class="pun">)</span></pre>

<p>
	ونعدّل بعد ذلك دالة إطلاق النار إلى النحو الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8399_35" style=""><span class="pln">func _on_shoot_timer_timeout</span><span class="pun">():</span><span class="pln">
  var b </span><span class="pun">=</span><span class="pln"> bullet_scene</span><span class="pun">.</span><span class="pln">instantiate</span><span class="pun">()</span><span class="pln">
  get_tree</span><span class="pun">().</span><span class="pln">root</span><span class="pun">.</span><span class="pln">add_child</span><span class="pun">(</span><span class="pln">b</span><span class="pun">)</span><span class="pln">
  b</span><span class="pun">.</span><span class="pln">start</span><span class="pun">(</span><span class="pln">position</span><span class="pun">)</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> randf_range</span><span class="pun">(</span><span class="lit">4</span><span class="pun">,</span><span class="pln"> </span><span class="lit">20</span><span class="pun">)</span><span class="pln">
  $ShootTimer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span></pre>

<p>
	سنشغّل الآن المشهد Main، وسنرى بعض قذائف العدو تهطل إلى الأسفل بين الفينة والأخرى.
</p>

<h2 id="-6">
	ختامًا
</h2>

<p>
	بهذا نكون قد أتممنا إعداد مشهد العدو الذي يطلق النار في لعبتنا ثنائية الأبعاد، وسنتعرف على المزيد لتطوير لعبتنا في المقالات القادمة.
</p>

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_07/index.html" rel="external nofollow">Enymies</a> و <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_08/index.html" rel="external nofollow">Enemy Shooting</a>.
</p>

<h2 id="-7">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2589/" rel="">إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%82%D8%B0%D9%8A%D9%81%D8%A9-%D9%88%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%86%D8%A7%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-r2588/" rel="">بناء مشهد القذيفة وإطلاق النار في لعبة سفينة الفضاء</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B9%D8%AF%D9%88-%D9%88%D8%AD%D9%8A%D9%88%D8%A7%D9%86-%D8%A3%D9%84%D9%8A%D9%81-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-godot-r2584/" rel="">برمجة عدو وحيوان أليف في لعبة Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2590</guid><pubDate>Mon, 04 Aug 2025 16:03:02 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x627;&#x644;&#x645;&#x634;&#x647;&#x62F; &#x627;&#x644;&#x631;&#x626;&#x64A;&#x633;&#x64A; &#x644;&#x644;&#x639;&#x628;&#x629; &#x627;&#x644;&#x633;&#x641;&#x64A;&#x646;&#x629; &#x627;&#x644;&#x641;&#x636;&#x627;&#x626;&#x64A;&#x629; &#x639;&#x628;&#x631; &#x645;&#x62D;&#x631;&#x643; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A6%D9%8A%D8%A9-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2589/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/01..png.fb0cf779393216481955d818100af9b0.png" /></p>
<p>
	سنعمل في هذا المقال وهو جزء من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a> على إنشاء المشهد الرئيسي للعبتنا ثنائية الأبعاد، فقبل صناعة الأعداء أو تشغيل السفينة أو أي عمل آخر في اللعبة، سنحتاج إلى توفير مكان تتواجد فيه كل هذه الكائنات معًا، ويُدعى هذا المكان عادة في عالم الألعاب بمشهد المستوى Level أو المشهد الرئيسي Main Scene، وهذا الاسم هو ما سنعتمده في مقالنا.
</p>

<h2 id="">
	إنشاء الخلفية
</h2>

<p>
	سنبدأ المشهد بعقدة من النوع <code>Node2D</code> ونسميها Main ثم نحفظ المشهد، بعدها نضيف عقدة ابن للعقدة Main من النوع <code>Sprite2D</code> ونسيمها Background لتمثل صورة الخلفية.
</p>

<p>
	نجعل بعد ذلك الصورة باسم <code>Space_BG (2 frames) (64 x 64).png</code> نقشًا خاصًا بها، بحيث ستساعد هذه الخلفية في تغطية كامل شاشة اللعبة عند الضبط الصحيح للإعدادات.
</p>

<p>
	تضم الصورة السابقة إطارين أبعاد كل منهما <code>64x64</code> ونريد أن نرصف الصورة على كامل خلفية المشهد، لهذا سنبدأ بضبط الإعدادات التالية:
</p>

<ul>
	<li>
		ننتقل إلى نافذة الفاحص Inspector بعد النقر على العقدة Background، ثم نضبط قيمة الخاصية <code>Offset&gt;Center</code>على القيمة <code>off</code> ليصبح موقع الزاوية العليا اليسارية للصورة هي مبدأ الإحداثيات وليس منتصف الشاشة
	</li>
	<li>
		نضبط قيمة الخاصية <code>Region&gt;Enabled</code> على <code>on</code>، فتظهر مجموعة الخاصيات <code>Rect</code>؛ وهنا نضبط الارتفاع على <code>320</code> والعرض على <code>240</code> كي تتوسع الصورة لتغطي كامل الشاشة
	</li>
	<li>
		نضبط قيمة الخاصية <code>Texture&gt;Repeat</code> على <code>Enabled</code> كي تكرر الصورة نفسها على كامل أبعاد نافذة اللعبة
	</li>
</ul>

<p>
	نضيف الآن مشهد اللاعب Player كمشهد فرعي إلى المشهد Main، وذلك بالنقر على زر إلحاق مشهد فرعي Instantiate Child Scene.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172873" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_instan_player.png.102e1fe1600339a13e0a5ff35ab4d8ca.png" rel=""><img alt="01 instan player" class="ipsImage ipsImage_thumbnailed" data-fileid="172873" data-unique="iv5nojlyd" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_instan_player.png.102e1fe1600339a13e0a5ff35ab4d8ca.png"> </a>
</p>

<h2 id="-1">
	تحريك الخلفية
</h2>

<p>
	يمكن جعل المشهد أكثر ديناميكية عن طريق تحريك الخلفية، وذلك باستخدام الشيفرة عن طريق تغيير قيمة الخاصية <code>region_rect</code> عند كل إطار؛ مع ذلك سنستخدم العقدة <code>AnimationPlayer</code>، ولهذا نضيف واحدة إلى العقدة Main.
</p>

<p>
	تظهر اللوحة Animation تحت نافذة المحرر، وتعرض الكثير من المعلومات التي سنطلع على طريقة تنظيمها:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172874" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_animation.png.72363c208865ef1850fad7b97824013a.png" rel=""><img alt="02 animation" class="ipsImage ipsImage_thumbnailed" data-fileid="172874" data-unique="zcczf1b1s" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_animation.png.72363c208865ef1850fad7b97824013a.png"> </a>
</p>

<p>
	سننقر الآن على زر التحريك Animation، ثم نضغط على الخيار <strong>تحريك جديد New Animation</strong>، ويمكننا تسمية الرسم المتحرك الجديد بالاسم Scroll. سنضبط بعد ذلك طول المقطع Length على <code>2</code> بتغيير قيمة مربع مدة الرسم المتحرك بالثواني ونفعّل خياري تكرار التحريك Looping و تشغيل تلقائي عند التحميل Autoplay بالنقر على زريهما.
</p>

<p>
	يعمل الرسم المتحرك عن طريق إضافة مسارات تمثل الخصائص التي تريد التحكم بها من خلال العقدة <code>AnimationPlayer</code>؛ وضمن الخط الزمني للاعب timeline، سنضيف مفاتيح مرجعية keyframes تعرّف القيم الجديدة للخاصيات في الوقت المحدد.
</p>

<p>
	يمكننا إضافة إطار أو مفتاح مرجعي Keyframe إلى الرسم المتحرك بالنقر على أيقونة المفتاح التي تُعرض إلى جانب كل خاصية من الخواص في نافذة الفاحص، وهنا قبل وضع الإطار المرجعي، يجب التأكد من أن المقبض الأزرق scrubber متواجد على الخط الزمني في نافذة التحريك على القيمة <code>0</code>.
</p>

<p>
	نختار الآن العقدة Background، ثم ننتقل إلى الفاحص وننقر المفتاح إلى جانب الخاصية <code>Region&gt;Rect</code>، وستظهر عندها رسالة تأكيد تخبرنا بأننا سننشئ مقطعًا جديدًا لخاصية وإضافة مفتاح لها، وسنرى عند تأكيد الرسالة مسارًا جديدًا أضيف إلى لوحة التحريك يحتوي على نقطة صغيرة تمثل المفتاح المرجعي الذي أضفناه.
</p>

<p>
	عند هذه المرحلة، سيكفي سحب المقبض إلى الزمن <code>2</code> ثم تغيير قيمة <code>y</code> للخاصية <code>Region&gt;Rect</code> إلى <code>64</code>. وعند النقر على زر تشغيل الرسم Play في لوحة التحريك، سنلاحظ كيف تتحرك الخلفية ببطئ خلف اللاعب.
</p>

<h2 id="-2">
	الخاتمة
</h2>

<p>
	شرحنا في هذا المقال كيفية إعداد وتجهبز المشهد الرئيسي للعبتنا ثنائية الأبعاد وأضفنا الخلفية للمشهد وضبطناها بالطريقة المناسبة. الخطوة التالية التي علينا القيام بها هي إضافة كائنات الأعداء وإعداد طريقة تحركهم ضمن اللعبة، وهذا ما سنفعله في المقال التالي؛ إذ سننشئ مشهد عدو وحيد كما فعلنا مع القذائف في مقال سابق ونكررها في اللعبة عند الحاجة.
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_06/index.html" rel="external nofollow">Main Scene</a>.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%82%D8%B0%D9%8A%D9%81%D8%A9-%D9%88%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%86%D8%A7%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-r2588/" rel="">بناء مشهد القذيفة وإطلاق النار في لعبة سفينة الفضاء</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B9%D8%AF%D9%88-%D9%88%D8%AD%D9%8A%D9%88%D8%A7%D9%86-%D8%A3%D9%84%D9%8A%D9%81-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-godot-r2584/" rel="">برمجة عدو وحيوان أليف في لعبة Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2589</guid><pubDate>Fri, 01 Aug 2025 16:07:01 +0000</pubDate></item><item><title>&#x628;&#x646;&#x627;&#x621; &#x645;&#x634;&#x647;&#x62F; &#x627;&#x644;&#x642;&#x630;&#x64A;&#x641;&#x629; &#x648;&#x625;&#x637;&#x644;&#x627;&#x642; &#x627;&#x644;&#x646;&#x627;&#x631; &#x641;&#x64A; &#x644;&#x639;&#x628;&#x629; &#x633;&#x641;&#x64A;&#x646;&#x629; &#x627;&#x644;&#x641;&#x636;&#x627;&#x621;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%82%D8%B0%D9%8A%D9%81%D8%A9-%D9%88%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%86%D8%A7%D8%B1-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-r2588/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/9..png.874d27e2e918256d875aff2fd81bbb34.png" /></p>
<p>
	انتهينا في <a href="https://academy.hsoub.com/programming/game-development/%D9%83%D8%AA%D8%A7%D8%A8%D8%A9-%D8%B4%D9%8A%D9%81%D8%B1%D8%A9-%D8%A7%D9%84%D8%AA%D8%AD%D9%83%D9%85-%D8%A8%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2587/" rel="">المقال السابق</a> من تحريك سفينة الفضاء، وسنتابع في هذا المقال الذي هو جزء من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a> بناء مشهد القذيفة أو الرصاصة وعملية إطلاق النار من قبل سفينة الفضاء التي تمثل اللاعب الأساسي في لعبتنا ثنائية الأبعاد، وسننفذها بشكل كائن قابل لإعادة الاستخدام.
</p>

<h2 id="">
	الكائنات القابلة لإعادة الاستخدام
</h2>

<p>
	سيطلق اللاعب كمًا كبيرًا من القذائف مع تقدم اللعبة وجميعها أشياء متطابقة تمامًا، وبالتالي سنتحتاج إلى تنفيذ ما يلي:
</p>

<ul>
	<li>
		إظهار القذيفة أمام اللاعب
	</li>
	<li>
		تحريك القذيفة نحو الأمام حتى تغادر الشاشة
	</li>
	<li>
		التقاط حدث تصادم القذيفة مع الأعداء
	</li>
</ul>

<p>
	وطالما أن كل القذائف لها نفس الوظيفة، فسنوفر الكثير من العمل بتصميم نموذج أولي للطلقة واستخدامه كنقطة انطلاق في إنشاء العدد الذي نريده من النسخ المتطابقة، ونظام المشاهد في <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">جودو</a> مثالي لهذا الغرض.
</p>

<h2 id="-1">
	مشهد القذيفة
</h2>

<p>
	سننشئ هنا مشهدًا جديدًا من خلال النقر على خيار المشهد<strong> </strong>Scene ثم على خيار مشهد جديد New Scene، أو بالنقر على الزر <strong>+</strong> في أعلى نافذة العرض.
</p>

<p>
	وكما فعلنا في <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">مشهد اللاعب</a>، لا بد من تحديد العّقد التي نحتاجها حتى تعمل القذيفة كما هو مطلوب. بإمكاننا استخدام العقدة مجددًا طالما أنها قادرة على التقاط حدث الاصطدام بجسم، وهذا يعني بدوره أننا سنحتاج إلى شكل تصادم وشخصية لعرض صورة القذيفة، كما سنحتاج إلى طريقة لاكتشاف خروج القذيفة خارج الشاشة حتى نزيلها تلقائيًا.
</p>

<p>
	وستكون العقد على النحو الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_12" style=""><span class="pun">-</span><span class="pln"> </span><span class="typ">Area2D</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Bullet</span><span class="pln">
  </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Sprite2D</span><span class="pln">
  </span><span class="pun">-</span><span class="pln"> </span><span class="typ">CollisionShape2D</span><span class="pln">
  </span><span class="pun">-</span><span class="pln"> </span><span class="typ">VisibleOnScreenNotifier2D</span></pre>

<p>
	سننقر على العقدة <code>Sprite2D</code> ثم نسحب الصورة <code>Player_charged_beam (16 x 16).png</code> من مدير الملفات إلى الخاصية <code>Texture</code> في نافذة الفاحص Inspector. وكما نلاحظ، توجد أكثر من نسخة للقذيفة، ولهذا سنضبط قيمة الخاصية <code>Animation&gt; Hframe</code> على القيمة <code>2</code> كي نرى نسخةً واحدةً فقط في كل مرة.
</p>

<p>
	سنضبط الآن شكل التصادم <code>CollisionShape2D</code> كما فعلنا في المقال السابق مع شكل التصادم الخاص بعقدة اللاعب.
</p>

<h2 id="-2">
	سكريبت القذيفة
</h2>

<p>
	سنلحق الآن سكريبتًا جديدًا بالعقدة <code>Bullet</code> ونبدأ بتحريك القذيفة كالآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_14" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var speed </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">250</span><span class="pln">

func start</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">):</span><span class="pln">
    position </span><span class="pun">=</span><span class="pln"> pos

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+=</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	من المفترض أن يكون الأمر مألوفًا، فهو مشابه لسكريبت اللاعب، وقد غيرنا فقط <code>position.y</code> لأن الحركة ستكون شاقوليةً نحو الأعلى.
</p>

<p>
	يسمح لنا التابع <code>()start</code> الذي عرّفناه بضبط موقع البداية <code>position</code> للقذيفة، لأن اللاعب سيتحرك باستمرار وينشر القذائف باتجاهات مختلفة.
</p>

<h3 id="signals">
	ربط الإشارات Signals
</h3>

<p>
	نختار العقدة <code>Bullet</code> وننقر على النافذة الفرعية <strong>عقدة Node</strong> إلى جوار الفاحص:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172750" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_signals.png.e242733e81b2762343c84af87d755e55.png" rel=""><img alt="01 signals" class="ipsImage ipsImage_thumbnailed" data-fileid="172750" data-unique="ixfo16x5j" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_signals.png.e242733e81b2762343c84af87d755e55.png"> </a>
</p>

<p>
	تعرض لقطة الشاشة السابقة جميع الإشارات Signals التي يمكن للعقدة المختارة أن تبثها. تخبرنا الإشارات في جودو أن شيئٍا ما قد حدث، وفي حالتنا سنستخدم الإشارة <code>area_entered</code> لتخبرنا متى تلامس فيها هذه القذيفة عقدةً أخرى من النوع <code>Area2D</code>. ولفعل ذلك، سنختار الإشارة <code>area_entered</code>، ثم ننقر على الزر <strong>توصيل Connect</strong> أسفل النافذة، بعدها ننقر على الزر <strong>وصل Connect</strong> في النافذة التي تظهر؛ إذ لسنا بحاجة إلى تغيير أي شيء فيها.
</p>

<p>
	سنلاحظ الآن أننا قد عدنا إلى محرر السكريبت، كما سنلاحظ وجود دالة جديدة في السكريبت <code>bullet.gd</code>. وإلى جانب الدالة، سنجد أيقونة اتصال خضراء لتدلنا على أن الإشارة متصلة بها. وستُستدعى هذه الدالة في كل مرة تلامس فيها هذه العقدة شيئًا ما.
</p>

<p>
	لنضف الآن هذه الشيفرة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_16" style=""><span class="pln">func _on_area_entered</span><span class="pun">(</span><span class="pln">area</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> area</span><span class="pun">.</span><span class="pln">is_in_group</span><span class="pun">(</span><span class="str">"enemies"</span><span class="pun">):</span><span class="pln">
      area</span><span class="pun">.</span><span class="pln">explode</span><span class="pun">()</span><span class="pln">
      queue_free</span><span class="pun">()</span></pre>

<p>
	سنتحقق في هذه الشيفرة مما إذا كانت القذيفة قد أصابت عدوًا، بحيث إن فعلت ذلك، نطلب من العدو أن ينفجر، ومن ثم نمسح القذيفة مباشرةً.
</p>

<p>
	نكرر نفس الخطوات السابقة لوصل الإشارة <code>screen_exited</code> العائدة إلى العقدة <code>VisibleOnScreenNotifier2D</code> ونضيف الشيفرة التالية في الدالة التي توّلدها الإشارة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_18" style=""><span class="pln">func _on_visible_on_screen_notifier_2d_screen_exited</span><span class="pun">():</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<h2 id="-3">
	إطلاق النار
</h2>

<p>
	يقدم المشهد <code>Bullet</code> كائنًا قابلًا لإعادة الاستخدام يمكن نسخه في كل مرة يطلق فيها اللاعب النار. ولضبط عملية إطلاق النار، سنتبع ما يلي.
</p>

<h3 id="-4">
	إضافات إلى مشهد اللاعب
</h3>

<p>
	لنعد إلى سكريبت اللاعب <code>Player</code> ونضيف بعض المتغيرات الجديدة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_20" style=""><span class="lit">@export</span><span class="pln"> var cooldown </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.25</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var bullet_scene </span><span class="pun">:</span><span class="pln"> </span><span class="typ">PackedScene</span><span class="pln">
var can_shoot </span><span class="pun">=</span><span class="pln"> true</span></pre>

<p>
	يساعدنا التوجيه <code>export@</code> هنا في ضبط قيم المتغيرات ضمن نافذة الفاحص، وبالتالي سنكون قادرين على ضبط وقت الانتظار قبل إعادة إطلاق النار Cooldown Time.
</p>

<p>
	نعود الآن إلى مشهد اللاعب، ثم نغيّر قيمة المتغير <code>bullet_scene</code> بالنقر على الخاصية الموافقة في الفاحص، ثم نختار الملف <code>bullet.tscn</code>، أو بسحب المشهد من نافذة نظام الملفات وسحبه إلى الخاصية.
</p>

<p>
	يطلق المبرمجون على المتغير <code>can_shoot</code> تسمية الراية Flag، وهو متغير بولياني منطقي يتحكم بشرط معين. وفي حالتنا سيتحكم بإمكانية إطلاق اللاعب للنار أو لا، وستكون قيمة هذا المتغير في فترة الانتظار <code>false</code>.
</p>

<p>
	سنضيف تاليًا دالة <code>()start</code> مشابهة للتي أضفناها إلى مشهد القذيفة، والتي تساعدنا على ضبط بعض القيم الابتدائية للاعب وإعادة ضبطها عندما تبدأ اللعبة من جديد.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_22" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    start</span><span class="pun">()</span><span class="pln">

func start</span><span class="pun">():</span><span class="pln">
    position </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">screensize</span><span class="pun">.</span><span class="pln">x </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">,</span><span class="pln"> screensize</span><span class="pun">.</span><span class="pln">y </span><span class="pun">-</span><span class="pln"> </span><span class="lit">64</span><span class="pun">)</span><span class="pln">
    $GunCooldown</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> cooldown</span></pre>

<p>
	تضع الشيفرة السابقة اللاعب في أسفل ومنتصف الشاشة وهو مكان جيد للانطلاق، كما نضمن أن زمن الانتظار يأخذ القيمة المناسبة.
</p>

<p>
	تُستدعى الدالة <code>()shoot</code> في أي وقت نضغط فيه على مفتاح الإطلاق الذي يتفعّل عن ضغط الزر Space:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_24" style=""><span class="pln">func shoot</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> can_shoot</span><span class="pun">:</span><span class="pln">
      </span><span class="kwd">return</span><span class="pln">
    can_shoot </span><span class="pun">=</span><span class="pln"> false
    $GunCooldown</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span><span class="pln">
    var b </span><span class="pun">=</span><span class="pln"> bullet_scene</span><span class="pun">.</span><span class="pln">instantiate</span><span class="pun">()</span><span class="pln">
    get_tree</span><span class="pun">().</span><span class="pln">root</span><span class="pun">.</span><span class="pln">add_child</span><span class="pun">(</span><span class="pln">b</span><span class="pun">)</span><span class="pln">
    b</span><span class="pun">.</span><span class="pln">start</span><span class="pun">(</span><span class="pln">position </span><span class="pun">+</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="pun">-</span><span class="lit">8</span><span class="pun">))</span></pre>

<p>
	تتحقق الدالة بدايةً مما إذا ما كان إطلاق النار مسموحًا، فإن لم يكن كذلك، فسيُنهى تنفيذ الدالة مباشرةً باستخدام الأمر <code>return</code>. وإن كان مسموحًا للاعب بإطلاق النار، فسنضبط قيمة الراية على <code>false</code> ونبدأ العد لفترة الانتظار.
</p>

<p>
	سننشئ بعذ ذلك نسخةً عن مشهد القذيفة ونضيفه إلى اللعبة، ونستدعي الدالة <code>()start</code> الخاصة بالقذيفة للتأكد من وضعها في المكان المناسب فوق سفينة اللاعب.
</p>

<p>
	يمكن استدعاء هذه الدالة عندما يضغط اللاعب على مفتاح الإطلاق، لهذا، نضيف ما يلي داخل الدالة <code>()process_</code> بعد السطر <code>()position.clamp</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_26" style=""><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"shoot"</span><span class="pun">):</span><span class="pln">
    shoot</span><span class="pun">()</span></pre>

<p>
	نصل أيضًا الإشارة <code>timeout</code> العائدة للعقدة <code>GunCooldown</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4490_28" style=""><span class="pln">func _on_gun_cooldown_timeout</span><span class="pun">():</span><span class="pln">
    can_shoot </span><span class="pun">=</span><span class="pln"> true</span></pre>

<p>
	عندما تنتهي فترة الانتظار، يُسمح للاعب بإطلاق النار مجددًا؛ وهنا سنشغّل المشهد ونجرب التحرك وإطلاق النار.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172751" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_shooting.gif.323bea72f3f449c949534079037aef11.gif" rel=""><img alt="02 shooting" class="ipsImage ipsImage_thumbnailed" data-fileid="172751" data-unique="h7w6idzri" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_shooting.gif.323bea72f3f449c949534079037aef11.gif"> </a>
</p>

<p>
	وكما هو ظاهر في الصورة أعلاه، فقد أضفنا قذائف جديدة كأبناء لشجرة المشاهد <code>get_tree().root</code> وليس إلى مشهد اللاعب، لأننا لو فعلنا ذلك فستكون القذائف أبناءً للسفينة وتتحرك مع حركتها.
</p>

<h2 id="-5">
	الخاتمة
</h2>

<p>
	وهكذا نكون قد أنجزنا مشهد إطلاق النار كاملة في لعبتنا ثنائية الأبعاد، بدءًا من تصميم القذيفة وربط إشارات التصادم، وصولًا إلى تنفيذ آلية الإطلاق والتحكم بفترة التهدئة بين كل طلقة وأخرى، وتعلمنا كيفية استخدام نظام المشاهد في جودو لننشئ كائنات قابلة لإعادة الاستخدام، وكيفية بناء تفاعل ديناميكي بين اللاعب وبيئة اللعب.
</p>

<p>
	ترجمة-وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_04/index.html" rel="external nofollow">Bullet Scene</a> و <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_05/index.html" rel="external nofollow">Shooting</a>.
</p>

<h2 id="-6">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D9%83%D8%AA%D8%A7%D8%A8%D8%A9-%D8%B4%D9%8A%D9%81%D8%B1%D8%A9-%D8%A7%D9%84%D8%AA%D8%AD%D9%83%D9%85-%D8%A8%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2587/" rel="">كتابة شيفرة التحكم بسفينة الفضاء عبر محرك Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B5%D8%A7%D8%B1%D9%88%D8%AE-%D9%85%D9%88%D8%AC%D9%87-%D9%8A%D8%AA%D8%A8%D8%B9-%D9%87%D8%AF%D9%81-%D9%85%D8%AA%D8%AD%D8%B1%D9%83-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2585/" rel="">برمجة صاروخ موجه يتبع هدف متحرك عبر محرك الألعاب Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2588</guid><pubDate>Wed, 30 Jul 2025 16:03:03 +0000</pubDate></item><item><title>&#x643;&#x62A;&#x627;&#x628;&#x629; &#x634;&#x64A;&#x641;&#x631;&#x629; &#x627;&#x644;&#x62A;&#x62D;&#x643;&#x645; &#x628;&#x633;&#x641;&#x64A;&#x646;&#x629; &#x627;&#x644;&#x641;&#x636;&#x627;&#x621; &#x639;&#x628;&#x631; &#x645;&#x62D;&#x631;&#x643; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D9%83%D8%AA%D8%A7%D8%A8%D8%A9-%D8%B4%D9%8A%D9%81%D8%B1%D8%A9-%D8%A7%D9%84%D8%AA%D8%AD%D9%83%D9%85-%D8%A8%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-godot-r2587/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/8..png.3420d1e323fe263cfdc2a117b32df367.png" /></p>
<p>
	أعددنا في مقال سابق بيئة اللعبة ثم بنينا في المقال الذي يليه مشهد اللاعب، وسنتابع في هذا المقال الذي هو جزء من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a> العمل على لعبتنا ثنائية الأبعاد في محرك الألعاب جودو ونبدأ بكتابة شيفرة سفينة الفضاء التي يتحكم بها اللاعب.
</p>

<h2 id="">
	إضافة سكريبت إلى اللاعب
</h2>

<p>
	يُبنى سلوك الكائنات وآليات اللعب عن طريق كتابة سكريبتات برمجية وإلحاقها بالعقد وبغيرها من الكائنات. وقد رأينا سابقًا كيف يعرض المشهد <code>Player</code> سفينة الفضاء ويُعرّف صندوق التصادم الذي يحيط بها وغيره من الخواص، لكنها لا تستطيع التحرك بعد، ولن يحدث شيء إذا اصطدم بها جسم. لهذا سنكتب شيفرة إضافة هذه الوظائف إلى السفينة.
</p>

<p>
	لفعل ذلك، سنختار العقدة <code>Player</code> ثم ننقر على خيار إلحاق نص برمجي Attach script.
</p>

<p style="text-align: center;">
	<img alt="01_adding_script.png" class="ipsImage ipsImage_thumbnailed" data-fileid="172748" data-ratio="107.17" data-unique="5esdlys81" width="279" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_adding_script.png.2e864bd5f79540b59075fa4a841b6ab7.png">
</p>

<p>
	ليس علينا تغيير أي خيارات في نافذة إلحاق نص برمجي للعقدة، بل علينا النقر فقط على زر Attach Node Script، وسينتقل بنا المحرّك إلى محرر السكريبت.
</p>

<p>
	لنلق نظرةً إلى السطر الأول من السكريبت الذي أُضيف تلقائيًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_8" style=""><span class="pln">extends </span><span class="typ">Area2D</span></pre>

<p>
	يعرّف هذا السطر نوع الكائن الذي يرتبط به السكريبت، وبالتالي سيكون السكريبت قادرًا على الوصول إلى جميع الوظائف التي تقدمها العقدة <code>Area2D</code>، ويجب أن يتطابق السطر دائمًا مع نوع العقدة التي تُلحق السكريبت بها.
</p>

<h3 id="-1">
	الوصول إلى السكريبتات
</h3>

<p>
	لا يفعّل السكريبت الكثير بمفرده، بل يكتفي بتعريف وظائف إضافية في أي كائن ترتبط به فقط؛ إذ لن نحتاج أبدًا إلى الوصول إلى متغيرات في بعض السكريبتات، بل الوصول إلى خاصيات الكائن التي يُعرّفها السكريبت، وتمييز هذا الأمر مهم جدًا.
</p>

<h2 id="-2">
	تحريك السفينة
</h2>

<p>
	سنبدأ الآن بتحريك السفينة حول الشاشة، وسنكتب شيفرة تنفّذ ما يلي:
</p>

<ul>
	<li>
		التقاط مفاتيح الإدخال التي يضغط عليها اللاعب
	</li>
	<li>
		تحريك السفينة في الاتجاه الذي يفرضه مفتاح الدخل
	</li>
</ul>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_10" style=""><span class="lit">@export</span><span class="pln"> var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">150</span><span class="pln">

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var input </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_vector</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"down"</span><span class="pun">)</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> input </span><span class="pun">*</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	يمكّن التوجيه <code>export@</code> المضاف قبل اسم المتغير في الشيفرة أعلاه من تعديل قيمته في نافذة الفاحص Inspector كما هو موضح بالصورة الآتية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172747" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_export_variable.png.9a0fbe9991987fbcaf994123a9555627.png" rel=""><img alt="02 export variable" class="ipsImage ipsImage_thumbnailed" data-fileid="172747" data-unique="rn0a3raov" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_export_variable.png.9a0fbe9991987fbcaf994123a9555627.png"> </a>
</p>

<p>
	أما باقي محتوى الشيفرة أعلاه، فيعني:
</p>

<ul>
	<li>
		تُستدعى الدالة <code>()process_</code> مرةً واحدة من قبل المحرّك عند تنفيذ كل إطار، وتُنفّذ الشيفرة التي تضمها
	</li>
	<li>
		يتحقق التابع <code>()Input.get_vector</code> من حالة المفاتيح المضغوطة من بين المفاتيح الأربعة المخصصة للإدخال ويولّد شعاع مدخلات له نفس اتجاه الحركة
	</li>
	<li>
		نغيّر أخيرًا موقع السفينة <code>position</code> بإضافة شعاع المدخلات وتعديل قيمته إلى السرعة المطلوبة ثم نضربه بالمعامل <code>delta</code>
	</li>
</ul>

<p>
	سنشغّل المشهد الآن بالنقر على زر Run Current Scene ونحاول تحريك السفينة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172746" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_playing_current_scene.png.3c66f3d5dfc9d9827d19745468eeaa2a.png" rel=""><img alt="03  playing current scene" class="ipsImage ipsImage_thumbnailed" data-fileid="172746" data-unique="sfowfrxlk" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_playing_current_scene.png.3c66f3d5dfc9d9827d19745468eeaa2a.png"> </a>
</p>

<h3 id="-3">
	البقاء ضمن الشاشة
</h3>

<p>
	حتى الآن، إذا حاولنا الاستمرار في التحرك باتجاه محدد، فستغادر السفية شاشة اللعبة، ولهذا علينا تحديد قيمة الخاصية <code>position</code> كي تبقى السفينة داخل مربع الشاشة. ولحل هذه المشكلة سنضيف السطر التالي في أعلى السكريبت.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_12" style=""><span class="lit">@onready</span><span class="pln"> var screensize </span><span class="pun">=</span><span class="pln"> get_viewport_rect</span><span class="pun">().</span><span class="pln">size</span></pre>

<p>
	يخبر التوجيه <code>onready@</code> جودو ألا يضبط قيمة المتغيّر <code>screensize</code> حتى تدخل العقدة <code>Player</code> شجرة المشهد. ويعني ذلك حرفيّا ضرورة الانتظار حتى بداية اللعبة، لعدم وجود نافذة نتحرى أبعادها قبل أم تبدأ اللعبة.
</p>

<p>
	ستكون الخطوة التالية هي حصر موقع السفينة ضمن حدود المربع <code>screensize</code> باستخدام التابع <code>()clamp</code> الذي يضمه الشعاع <code>position</code> كونه من النوع <code>Vector2</code>.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_14" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var input </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_vector</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"down"</span><span class="pun">)</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> input </span><span class="pun">*</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta
    position </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">clamp</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO</span><span class="pun">,</span><span class="pln"> screensize</span><span class="pun">)</span></pre>

<p>
	بعدها نشغّل المشهد مجددًا ونحاول أن تحرك السفينة. سنلاحظ كيف تتوقف السفينة عند حواف الشاشة لكن نصفها يغادر الشاشة، وذلك لأن موقع السفينة <code>position</code> هو في مركزها أي مركز العقدة <code>Sprite2D</code>. وطالما أن أبعاد السفينة هي <code>16x16</code>، فبإمكاننا تغيير مقدار الاقتصاص وزيادة ما مقداره 8 بكسل كالتالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_16" style=""><span class="pln">position </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">clamp</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln"> screensize </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">))</span></pre>

<h3 id="-4">
	ربط الرسم المتحرك بالاتجاه
</h3>

<p>
	تتحرك السفية الآن كما هو مطلوب، وبإمكاننا اختيار صور مائلة للسفينة عند التحرك يمينًا أو يسارًا وكذلك صور مائلة للهب كي يعطي الحركة ديناميكية أكبر.
</p>

<p>
	وللتحقق من جهة الحركة، نستطيع التحقق من قيمة <code>x</code> لشعاع الإدخال <code>input</code> فيما لو كان موجبًا فيعطي حركة يمينًا، أو سالبًا فيعطي حركة يسارًا، أو صفر بمعنى عدم وجود أي حركة.
</p>

<p>
	والآن، سنطبق التغييرات على الخاصية <code>frame</code> للعقدة <code>Sprite2D</code>، وذلك باختيار إطار معين عند الحركة يمينًا أو يسارًا لتغيير شكل صورة السفينة؛ وعلى الخاصية <code>animation</code> للعقدة <code>AnimatedSprite2D</code> لتغيير شكل اللهب كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4882_18" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var input </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_vector</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"down"</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> input</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
      $Ship</span><span class="pun">.</span><span class="pln">frame </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
      $Ship</span><span class="pun">/</span><span class="typ">Boosters</span><span class="pun">.</span><span class="pln">animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"right"</span><span class="pln">
    </span><span class="kwd">elif</span><span class="pln"> input</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
      $Ship</span><span class="pun">.</span><span class="pln">frame </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
      $Ship</span><span class="pun">/</span><span class="typ">Boosters</span><span class="pun">.</span><span class="pln">animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"left"</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
      $Ship</span><span class="pun">.</span><span class="pln">frame </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
      $Ship</span><span class="pun">/</span><span class="typ">Boosters</span><span class="pun">.</span><span class="pln">animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"forward"</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> input </span><span class="pun">*</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta
    position </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">clamp</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">),</span><span class="pln"> screensize</span><span class="pun">-</span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">8</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">))</span></pre>

<h2 id="-5">
	ختامًا
</h2>

<p>
	بهذا نكون قد وصلنا إلى نهاية المقال الذي تعرفنا فيه على كيفية كتابة شيفرة التحكم بسفينة الفضاء عبر محرك Godot. يجب التأكد من أن كل شيء يعمل بشكل طبيعي كما هو مطلوب قبل الانتقال إلى الخطوة التالية التي سنشرحها في المقال التالي، والذي سننشئ فيها مشهد الرصاصة ونبرمج عملية إطلاق النار.
</p>

<p>
	ترجمة -وبتصرف- لمقال <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_03/index.html" rel="external nofollow">Coding the Player</a>.
</p>

<h2 id="-6">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/" rel="">تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D9%81%D8%B6%D8%A7%D8%A1-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2568/" rel="">إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D9%81%D8%B6%D8%A7%D8%A1-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-rigidbody2d-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2558/" rel="">تحريك سفينة فضاء باستخدام RigidBody2D في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2587</guid><pubDate>Mon, 28 Jul 2025 16:04:01 +0000</pubDate></item><item><title>&#x62A;&#x635;&#x645;&#x64A;&#x645; &#x645;&#x634;&#x647;&#x62F; &#x627;&#x644;&#x644;&#x627;&#x639;&#x628; &#x641;&#x64A; &#x644;&#x639;&#x628;&#x629; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F; &#x628;&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-godot-r2586/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/07..png.f93c4e8de625ad27ec3d0869a54c4f91.png" /></p>
<p>
	هيئنا في مقال <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D9%81%D8%B6%D8%A7%D8%A1-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2568/" rel="">إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو</a> الإعدادات اللازمة للعبة مركبة الفضاء المقاتلة ونزلنا الأيقونات والصور اللازمة وأصبحنا جاهزين لتصميم مشهد مركبة الفضاء الخاضعة لتحكم اللاعب.
</p>

<h2 id="">
	إعداد مشهد سفينة الفضاء
</h2>

<p>
	من الأقسام الشائعة في مجرى بناء ألعاب جودو هو إنشاء المشاهد. وكما رأينا سابقًا، يمثل المشهد في جودو مجموعة من العقد. وفي معظم ألعاب جودو، يُهيئ كل كائن في اللعبة على شكل مشهد يضم عقدًا تعطيه الوظائف المنوطة به، وقد بعض الشيفرة لتخصيص سلوك هذا الكائن.
</p>

<h3 id="-1">
	اختيار العقد
</h3>

<p>
	علينا في البداية اتخاذ قرار بشأن <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">العقد</a> التي يجب أن نبدأ بها المشهد، حيث تًدعى العقدة الأولى التي تضيفها إلى المشهد بالعقدة الجذرية root node، وينبغي أن تكون هذه العقدة هي العقد الأساسية للمشهد بحيث تعرّف سلوك كائن اللعبة. وسنضيف بعض ذلك عقدًا أبناء إلى هذه العقدة معطيةً الكائن وظائفه.
</p>

<p>
	ما هي العقدة التي ستكونها السفينة إذًا؟ دعونا نحلل المتطلبات، ونرى ما هي العقد التي ستكون مفيدة في تحقيق تلك المتطلبات.
</p>

<p>
	تحتاج سفينة الفضاء إلى:
</p>

<ul>
	<li>
		<strong>الحركة في فضاء ثنائي البعد</strong><span>:</span> تكفينا عقدة <code>Node2D</code> أساسية، وهي عقدة تمتلك خاصيات موقع <code>position</code> و دوران <code>rotation</code> وغيرها من خاصيات الفضاء 2D
	</li>
	<li>
		<strong>عرض صورة</strong>: وتناسب هذه الوظيفة العقدة <code>Sprite2D</code>، وهي أيضًا عقدة من النوع <code>Node2D</code> لهذا يمكن التحكم بها وتحريكها
	</li>
	<li>
		<strong>اكتشاف تصادمها بأجسام أخرى</strong>: سيتحرك الأعداء ويطلقون النار على السفينة، لذا علينا معرفة متى تُصاب السفينة. ستكون العقدة <code>Area2D</code> مثالية، إذ يمكنها اكتشاف التلامس مع كائنات أخرى ولها خاصيات موقع، وليس لها مظهر بحد ذاتها
	</li>
</ul>

<p>
	بالنظر إلى تلك المتطلبات سنجد أن <code>Area2D</code> هي من تقدم الوظيفة الأساسية وتعرض العقدة <code>Sprite2D</code> شكل السفينة وهذا كل ما نحتاجه.
</p>

<h2 id="-2">
	بناء المشهد
</h2>

<p>
	ننقر على زر <code>+</code> أو زر عقدة أخرى Other Node في النافذة الفرعية التي يُطلَق عليها تسمية المشهد Scene، ونشرع بكتابة Area2D لتظهر لنا العقدة في القائمة، فنختارها. وعندما نرى العقدة في نافذة المشهد، سننقر على العقدة ونسمّيها <code>Player</code>، ثم نحفظ المشهد باستخدام الاختصار <code>&lt;Ctrl+S&gt;</code>.
</p>

<h3 id="-3">
	عرض سفينة الفضاء
</h3>

<p>
	سنختار العقدة <code>Player</code> ثم نضيف عقدة <code>Sprite2D</code> ونسمّيها <code>Ship</code> حتى نبقي الأمور أكثر تنظيمًا؛ كما سنسحب بعد ذلك الأيقونة <code>Player_ship (16x16).png</code> من موقعها في نافذة الملفات إلى مربع الخاصية ملمس Texture في نافذة الفاحص Inspector.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172740" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_2d_player_ship.png.c0a390d65a52d082d2f29388d25e658f.png" rel=""><img alt="01 2d player ship" class="ipsImage ipsImage_thumbnailed" data-fileid="172740" data-unique="k4ie8jzlc" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_2d_player_ship.png.c0a390d65a52d082d2f29388d25e658f.png"> </a>
</p>

<p>
	قد نلاحظ مباشرةً وجود ثلاثة مركبات في الصورة، والسبب أن الصورة تضم نسخًا من المركبة التي تتحرك يمينًا ويسارًا، لذا سنستخدم ذلك في خاصيات التحريك Animation من الفاحص.
</p>

<p>
	سننقر على خاصية التحريك Animation ثم على الخيار إطارات افقية Hframes ونضبطه على الرقم 3، وهكذا سنتنتقل بين النسخ الثلاث للصورة عند تغيير قيمة الخاصية إطار Frame. سنترك الآن قيم هذه الخاصية على 1.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172743" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_ship_texture.png.b2d4acf123d87b8a4eb11548c0f5a4dd.png" rel=""><img alt="02 ship texture" class="ipsImage ipsImage_thumbnailed" data-fileid="172743" data-unique="l5p8odnfm" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_ship_texture.png.b2d4acf123d87b8a4eb11548c0f5a4dd.png"> </a>
</p>

<h3 id="-4">
	إضافة شكل التصادم
</h3>

<p>
	سنلاحظ وجود إشارة تحذير صفراء إلى جانب العقدة <code>Area2D</code>، بحيث إذا نقرنا عليها سنجد أنها تنبهنا بأن العقدة لا شكل لها، ولهذا علينا تعريف شكل لها. سنعرّف شكل العقدة بإضافة عقدة أخرى من النوع <code>CollisionShape2D</code> كابن للعقدة <code>Player</code>.
</p>

<p>
	سنجد في نافذة الفاحص عند النقر على العقدة <code>CollisionShape2D</code> الخاصية <code>Shape</code> التي تأخذ قيمة افتراضية فارغة <code>&lt;empty&gt;</code>، وبالنقر على الصندوق المجاور، ستظهر قائمة بكل الأشكال المتاحة، سنختار منها <code>New RectangleShape2D</code>وسنجد مربعًا باللون الأزرق الفاتح قد أحاط بالسفينة. بإمكاننا ضبط حجم الشكل الذي ظهر بسحب الدوائر البرتقالية وتحريكها، أو بالنقر على النقر زر السهم إلى جوار صندوق عقدة الشكل في نافذة الفاحص، ثم اختيار <code>shape</code> ووضع الأبعاد يدويًا.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172742" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_define_shipe_shape.png.9e270c84544944ca1b51f482ccd3f7fc.png" rel=""><img alt="03 define shipe shape" class="ipsImage ipsImage_thumbnailed" data-fileid="172742" data-unique="os8jf1jk5" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_define_shipe_shape.png.9e270c84544944ca1b51f482ccd3f7fc.png"> </a>
</p>

<h3 id="exhaust">
	العادم Exhaust
</h3>

<p>
	ستبدو السفينة أكثر ديناميكيةً وواقعيةً في حال أضفنا بعض الحركة إليها، وسنجد في مجلد الأيقونات بعض الرسوميات التي تمثل العادم أو اللهب الناري الذي يخرج من محركات السفينة ولها الاسم Boosters. سنجد أيضًا ثلاث أيقونات لنسخ أيقونات السفينة الثلاث هي اليمين واليسار والأمام.
</p>

<p>
	ولعرض هذه الأيقونات، سنختار العقدة <code>Ship</code> ونضيف إليها عقدة من النوع <code>AnimatedSprite2D</code> ونسميها <strong>Boosters</strong>. سننتقل بعد ذلك إلى الفاحص ومن ثم إلى شجرة التحريك Animation، وبعدها إلى الخاصية <code>SpriteFrames</code> التي تأخذ افتراضيًا قيمة فارغة <code>&lt;empty&gt;</code>، وننقر عليها لإضافة إطار رسومي جديد <code>New SpriteFrames</code> لننقر بعدها على الخيار <code>SpriteFrames</code> لفتح لوحة التحريك أسفل نافذة المحرر.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172744" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_animation_panel.png.cd500ac77983a5e98abe7db25270e28a.png" rel=""><img alt="04 animation panel" class="ipsImage ipsImage_thumbnailed" data-fileid="172744" data-unique="x8iy5ytw2" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_animation_panel.png.cd500ac77983a5e98abe7db25270e28a.png"> </a>
</p>

<p>
	سننقر الآن نقرًا مزدوجًا على العنصر default ونغير اسمه إلى forward. ولإضافة صور الرسوم المتحركة الآن، علينا النقر على زر إضافة إطارات من الملخص Add frames from sprite sheet الموضح في الصورة التالية بموضع مؤشر الفأرة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172738" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_add_frames.png.b4d72d27fe94db6fceb44a4f22a11f87.png" rel=""><img alt="05 add frames" class="ipsImage ipsImage_thumbnailed" data-fileid="172738" data-unique="d1lw0yfvz" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_add_frames.png.b4d72d27fe94db6fceb44a4f22a11f87.png"> </a>
</p>

<p>
	سنختار الآن الصورة <code>Boosters (16 x 16).png</code> وستظهر لنا نافذة تحديد الإطارات Select frames مباشرةً كي نختار الإطارات التي نريد.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172737" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_select_frame.png.ccd4a8473b6360af09f948513c431a28.png" rel=""><img alt="06 select frame" class="ipsImage ipsImage_thumbnailed" data-fileid="172737" data-unique="lf0bopct5" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_select_frame.thumb.png.579ea2e7656132c7a7088f25de8fa0d6.png"> </a>
</p>

<p>
	هناك إطاران فقط في الصورة، لكن الشبكة غير متناسقة، لذا سنغيّر قيمة الخاصية <code>Size</code> لتتلائم مع أبعاد الصورة <code>16x 16</code>، ثم ننقر على كلا الإطارين لاختيارهما وبعدها على زر إضافة إطارين (Add 2 Frame(s.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172741" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_adding_frames.png.e48e4ad6258f2f8f6d5b4a5e367b0743.png" rel=""><img alt="07 adding frames" class="ipsImage ipsImage_thumbnailed" data-fileid="172741" data-unique="8u3hijhk3" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_adding_frames.thumb.png.f545dba4d4cdc3084fed978d1b207da4.png"> </a>
</p>

<p>
	والآن، بعد أن أضفنا الإطارين، سننقر على زر التشغيل Play لتحريك الرسوم، وبإمكاننا أيضًا تفعيل الخيار Autoplay on Load كي يجري تحريك الصورة تلقائيًا.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172736" href="https://academy.hsoub.com/uploads/monthly_2025_05/08_play_animation.png.305ef07791b3923ac72241cc8a58bc7c.png" rel=""><img alt="08 play animation" class="ipsImage ipsImage_thumbnailed" data-fileid="172736" data-unique="92poz0y5q" style="width: 700px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/08_play_animation.png.305ef07791b3923ac72241cc8a58bc7c.png"> </a>
</p>

<p>
	قد نجد هنا أن سرعة تغير اللهب بطيئة، لهذا يمكننا تعديل السرعة لتصبح <code>5FPS</code>، بعدها سنكرر تنفيذ الخطوات السابقة لإضافة رسم متحرك للهب العادم عندما تتحرك إلى اليمين واليسار ونسميهما <code>left</code> and <code>right</code>.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172739" href="https://academy.hsoub.com/uploads/monthly_2025_05/09_add_new_animation.png.01d44af3993438d36a9306833eda9e88.png" rel=""><img alt="09 add new animation" class="ipsImage ipsImage_thumbnailed" data-fileid="172739" data-unique="3wzi8y2d6" src="https://academy.hsoub.com/uploads/monthly_2025_05/09_add_new_animation.png.01d44af3993438d36a9306833eda9e88.png"> </a>
</p>

<h3 id="-5">
	التحكم بسرعة الإطلاق على العدو
</h3>

<p>
	آخر ما سنفعله لإكمال شخصية اللاعب هو إضافة عقدة <code>Timer</code> للتحكم بسرعة إطلاقه للنار، حيث سنضيف هذه العقدة كابن للعقدة <code>Player</code> ونسميها <code>GunCooldown</code>، وبعدها سنفعّل الخاصية <code>One Shot</code> في نافذة الفاحص كي لا يُفعّل المؤقت تلقائيًا عندما ينتهي من العد. سنفعّل المؤقت برمجيًا في الشيفرة عندما يطلق اللاعب النار، ولن يتمكن من إعادة الإطلاق مجددًا حتى ينتهي المؤقت من العد.
</p>

<h2 id="-6">
	الخاتمة
</h2>

<p>
	إلى هنا نكون قد انتهينا من إعداد مشهد اللاعب في لعبتنا ثنائية الأبعاد في جودو، إذ أضفنا عقدًا تعطي سفينة اللاعب الوظائف التي تحتاجها في اللعبة، وسنضيف في المقال التالي الشيفرة البرمجية اللازمة للتحكم بالسفينة وإطلاق النار، واكتشاف تصادمها مع كائنات أخرى في اللعبة.
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_03/index.html" rel="external nofollow">Designing the Player Scene</a>.
</p>

<h2 id="-7">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%84%D8%B9%D8%A8%D8%A9-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D9%81%D8%B6%D8%A7%D8%A1-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2568/" rel="">إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%B3%D9%81%D9%8A%D9%86%D8%A9-%D9%81%D8%B6%D8%A7%D8%A1-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-rigidbody2d-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2558/" rel="">تحريك سفينة فضاء باستخدام RigidBody2D في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B5%D8%A7%D8%B1%D9%88%D8%AE-%D9%85%D9%88%D8%AC%D9%87-%D9%8A%D8%AA%D8%A8%D8%B9-%D9%87%D8%AF%D9%81-%D9%85%D8%AA%D8%AD%D8%B1%D9%83-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2585/" rel="">برمجة صاروخ موجه يتبع هدف متحرك عبر محرك الألعاب Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2586</guid><pubDate>Mon, 21 Jul 2025 16:06:01 +0000</pubDate></item><item><title>&#x628;&#x631;&#x645;&#x62C;&#x629; &#x635;&#x627;&#x631;&#x648;&#x62E; &#x645;&#x648;&#x62C;&#x647; &#x64A;&#x62A;&#x628;&#x639; &#x647;&#x62F;&#x641; &#x645;&#x62A;&#x62D;&#x631;&#x643; &#x639;&#x628;&#x631; &#x645;&#x62D;&#x631;&#x643; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B5%D8%A7%D8%B1%D9%88%D8%AE-%D9%85%D9%88%D8%AC%D9%87-%D9%8A%D8%AA%D8%A8%D8%B9-%D9%87%D8%AF%D9%81-%D9%85%D8%AA%D8%AD%D8%B1%D9%83-%D8%B9%D8%A8%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-godot-r2585/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/00-05..png.0591d7fd93b7fa317d8195bd1896a8fd.png" /></p>
<p>
	سنوضّح في هذا المقال من سلسلة دليل جودو كيفية برمجة صاروخ موجه ، والذي هو مقذوف يبحث عن هدف متحرك، ، حيث سنستخدم عقدة <code>Area2D</code> لتنفيذ حركة الصاروخ، مع إضافة التأثيرات البصرية مثل الدخان والانفجارات' كما سنوضح كيفية استخدام التسارع والتوجيه الذكي Steering لتحريك الصاروخ نحو الهدف مع التحكم في قوة التوجيه لتحقيق حركة أكثر واقعية؛ كما سنوضح أيضًا كيفية استدعاء الدوال المناسبة للتحكم في وقت حياة الصاروخ، وكيفية تفاعله مع البيئة المحيطة.
</p>

<p>
	ستساعدنا هذه المفاهيم الأساسية على تطوير أنظمة موجهة أخرى في الألعاب التي تتطلب تفاعلًا ديناميكيًا مع الأهداف المتحركة.
</p>

<h2 id="">
	برمجة صاروخ موجه للكشف عن الهدف المتحرك
</h2>

<p>
	لبرمجة صاروخ موجه، والذي هو مقذوف يبحث عن هدف متحرك، سنستخدم عقدة <code>Area2D</code> للمقذوف؛ إذ تُعَد عقد المناطق Areas خيارات جيدة للرصاصات أو الأجسام المتحركة التي تُطلق من الصاروخ لأننا بحاجة إلى كشفها عند ملامستها لشيء ما، ولكن إذا كنا بحاجة أيضًا إلى رصاصة ترجع أو ترتد، فقد تكون العقد من نوع <code>PhysicsBody</code> خيارًا أفضل.
</p>

<p>
	يتشابه إعداد العقدة وسلوك الصاروخ مع الإعداد الذي نستخدمه مع الرصاصات العادية، لذا في حال كنا قد أنشأنا مسبقًا عدة أنواع من الرصاص، فيمكننا استخدام الوراثة لإعداد جميع مقذوفاتنا استنادًا على الإعداد الأساسي نفسه.
</p>

<p>
	فيما يلي العقد التي سنستخدمها:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_8" style=""><span class="typ">Area2D</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Missile</span><span class="pln">
    </span><span class="typ">Sprite2D</span><span class="pln">
    </span><span class="typ">CollisionShape2D</span><span class="pln">
    </span><span class="typ">Timer</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Lifetime</span></pre>

<p>
	يمكن استخدام أي صورة نريدها بالنسبة للخامة Texture كما في المثال التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172692" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_missile.png.ea688a16060f10baf7dad5646602d348.png" rel=""><img alt="01 missile" class="ipsImage ipsImage_thumbnailed" data-fileid="172692" data-unique="xh2kn1cia" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_missile.png.ea688a16060f10baf7dad5646602d348.png"> </a>
</p>

<p>
	يمكن الآن إعداد العقد وضبط خامة الشخصية الرسومية Sprite وشكل التصادم، مع التأكّد من تدوير عقدة <code>Sprite2D</code> بمقدار 90 درجة، بحيث تشير إلى اليمين، مع التأكد من أنها تتطابق مع الاتجاه الأمامي للعقدة الأب.
</p>

<p>
	سنضيف سكربتًا ونتصل بالإشارة <code>body_entered</code> الخاصة بالعقدة <code>Area2D</code> والإشارة <code>timeout</code> الخاصة بالعقدة <code>Timer</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_10" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

export var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">350</span><span class="pln">

var velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
var acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO

func start</span><span class="pun">(</span><span class="pln">_transform</span><span class="pun">):</span><span class="pln">
    global_transform </span><span class="pun">=</span><span class="pln"> _transform
    velocity </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> speed

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    velocity </span><span class="pun">+=</span><span class="pln"> acceleration </span><span class="pun">*</span><span class="pln"> delta
    velocity </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">clamped</span><span class="pun">(</span><span class="pln">speed</span><span class="pun">)</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta

func _on_Missile_body_entered</span><span class="pun">(</span><span class="pln">body</span><span class="pun">):</span><span class="pln">
    queue_free</span><span class="pun">()</span><span class="pln">

func _on_Lifetime_timeout</span><span class="pun">():</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<p>
	سيؤدي هذا إلى إنشاء صاروخ يتحرك في خط مستقيم عند إطلاقه، ويمكن استخدام هذا المقذوف من خلال إنشاء نسخة منه واستدعاء التابع <code>start()‎</code> الخاص به مع التحويل <code>Transform2D</code> المطلوب لضبط موضعه واتجاهه.
</p>

<p>
	سنستخدم التسارع <code>acceleration</code> لتغيير السلوك للبحث عن الهدف، ولكن لا نريد أن يدور الصاروخ بسرعة كبيرة، لذا سنضيف متغيرًا للتحكم في قوة التوجيه Steering، مما يعطي الصاروخ نصفَ قطر دوران يمكن تعديله مع سلوك مختلف.
</p>

<p>
	سنحتاج أيضًا إلى متغير الهدف <code>target</code> حتى يعرف الصاروخ ما الذي يطارده، وسنضعه في التابع <code>start()‎</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_12" style=""><span class="pln">export var steer_force </span><span class="pun">=</span><span class="pln"> </span><span class="lit">50.0</span><span class="pln">

var target </span><span class="pun">=</span><span class="pln"> null

func start</span><span class="pun">(</span><span class="pln">_transform</span><span class="pun">,</span><span class="pln"> _target</span><span class="pun">):</span><span class="pln">
    target </span><span class="pun">=</span><span class="pln"> _target
    </span><span class="pun">…</span></pre>

<p>
	يمكن تغيير اتجاه الصاروخ للتحرك نحو الهدف باستخدام التسارع في ذلك الاتجاه، فالتسارع هو تغير في السرعة؛ حيث يريد الصاروخ الحالي التحرك نحو الهدف مباشرةً، ولكن تشير سرعته الحالية إلى اتجاه مختلف، ويمكننا إيجاد هذا الفرق باستخدام الرياضيات الشعاعية كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172696" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_steering_diagramar.png.c95d23201cd7fc04706d0d52ad70a363.png" rel=""><img alt="02_steering_diagram ar.png" class="ipsImage ipsImage_thumbnailed" data-fileid="172696" data-ratio="38.44" data-unique="niomej1rl" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_steering_diagramar.thumb.png.4961d776bf0178b0fc2ffe8c47aab683.png"></a>
</p>

<p>
	يمثل السهم الأخضر التغير المطلوب في السرعة، أي التسارع <code>acceleration</code>، ولكن إذا انعطفنا مباشرةً، فسيبدو الأمر غير طبيعي، لذا يجب أن يكون طول متجه التوجيه محدودًا، وهذا سبب استخدام المتغير <code>steer_force</code>.
</p>

<p>
	تحسب الدالة التالية هذا التسارع، ويمكننا ملاحظة أنه لن يكون هناك توجيه عند عدم وجود هدف، لذا سيواصل الصاروخ التحرك في خط مستقيم.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_14" style=""><span class="pln">func seek</span><span class="pun">():</span><span class="pln">
    var steer </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">if</span><span class="pln"> target</span><span class="pun">:</span><span class="pln">
       var desired </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">target</span><span class="pun">.</span><span class="pln">position </span><span class="pun">-</span><span class="pln"> position</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed
       steer </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">desired </span><span class="pun">-</span><span class="pln"> velocity</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> steer_force
    </span><span class="kwd">return</span><span class="pln"> steer</span></pre>

<p>
	أخيرًا، يجب تطبيق قوة التوجيه الناتجة في الدالة <code>‎_physics_process()‎</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_16" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    acceleration </span><span class="pun">+=</span><span class="pln"> seek</span><span class="pun">()</span><span class="pln">
    velocity </span><span class="pun">+=</span><span class="pln"> acceleration </span><span class="pun">*</span><span class="pln"> delta
    velocity </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">clamped</span><span class="pun">(</span><span class="pln">speed</span><span class="pun">)</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	فيما يلي مثال عن النتائج مع بعض التأثيرات البصرية الإضافية مثل دخان الجسيمات والانفجارات:
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172693" data-unique="reqq0o5x5">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_homing_missiles.webm.c6152cad2e5be407c637516f797e899e.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172693" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172693&amp;key=0625559f43b424c7c7b92964445c7ae2" rel="">03_homing_missiles.webm</a>
	</source></video>
</p>

<h2>
	 الكود الكامل للصاروخ الموجه مع التأثيرات البصرية
</h2>

<p>
	فيما يلي السكربت الكامل الذي يضيف سلوك الصاروخ الموجه باستخدام <code>Area2D</code> للكشف عن الهدف، ويشمل أيضًا التأثيرات البصرية، مثل الانفجارات ودخان الجسيمات عند الاصطدام أو انتهاء الوقت.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_150_18" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

export var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">350</span><span class="pln">
export var steer_force </span><span class="pun">=</span><span class="pln"> </span><span class="lit">50.0</span><span class="pln">

var velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
var acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
var target </span><span class="pun">=</span><span class="pln"> null

func start</span><span class="pun">(</span><span class="pln">_transform</span><span class="pun">,</span><span class="pln"> _target</span><span class="pun">):</span><span class="pln">
    global_transform </span><span class="pun">=</span><span class="pln"> _transform
    rotation </span><span class="pun">+=</span><span class="pln"> rand_range</span><span class="pun">(-</span><span class="lit">0.09</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.09</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> speed
    target </span><span class="pun">=</span><span class="pln"> _target

func seek</span><span class="pun">():</span><span class="pln">
    var steer </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">if</span><span class="pln"> target</span><span class="pun">:</span><span class="pln">
        var desired </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">target</span><span class="pun">.</span><span class="pln">position </span><span class="pun">-</span><span class="pln"> position</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed
        steer </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">desired </span><span class="pun">-</span><span class="pln"> velocity</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> steer_force
    </span><span class="kwd">return</span><span class="pln"> steer

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    acceleration </span><span class="pun">+=</span><span class="pln"> seek</span><span class="pun">()</span><span class="pln">
    velocity </span><span class="pun">+=</span><span class="pln"> acceleration </span><span class="pun">*</span><span class="pln"> delta
    velocity </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">clamped</span><span class="pun">(</span><span class="pln">speed</span><span class="pun">)</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta

func _on_Missile_body_entered</span><span class="pun">(</span><span class="pln">body</span><span class="pun">):</span><span class="pln">
    explode</span><span class="pun">()</span><span class="pln">

func _on_Lifetime_timeout</span><span class="pun">():</span><span class="pln">
    explode</span><span class="pun">()</span><span class="pln">

func explode</span><span class="pun">():</span><span class="pln">
    $Particles2D</span><span class="pun">.</span><span class="pln">emitting </span><span class="pun">=</span><span class="pln"> false
    set_physics_process</span><span class="pun">(</span><span class="pln">false</span><span class="pun">)</span><span class="pln">
    $AnimationPlayer</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="str">"explode"</span><span class="pun">)</span><span class="pln">
    await $AnimationPlayer</span><span class="pun">.</span><span class="pln">animation_finished
    queue_free</span><span class="pun">()</span></pre>

<h2 id="-2">
	الخاتمة
</h2>

<p>
	بهذا نكون قد وصلنا لنهاية مقالنا الذي تعلمنا كيفية برمجة صاروخ موجه في محرك الألعاب جودو Godot مع تطبيق التسارع والتوجيه الذكي لجعل الصاروخ يتبع هدفًا متحركًا' كما استعرضنا كيفية إضافة تأثيرات بصرية مثل الانفجارات والدخان عند الاصطدام أو انتهاء الوقت.
</p>

<p>
	يمكن تجربة تطبيق هذه الأساليب لتطوير أي ألعاب فيها أعداء يتبعون اللاعب، إذ يتغير سلوك الأعداء بناءً على موقع اللاعب وحركته.
</p>

<p>
	ترجمة -وبتصرّف- للقسم <a href="https://kidscancode.org/godot_recipes/4.x/ai/homing_missile/index.html" rel="external nofollow">Homing missile</a> من توثيقات Kidscancode.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B9%D8%AF%D9%88-%D9%88%D8%AD%D9%8A%D9%88%D8%A7%D9%86-%D8%A3%D9%84%D9%8A%D9%81-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-godot-r2584/" rel="">برمجة عدو وحيوان أليف في لعبة Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%83%D8%A7%D9%85%D9%8A%D8%B1%D8%A7-%D8%AF%D9%8A%D9%86%D8%A7%D9%85%D9%8A%D9%83%D9%8A%D8%A9-%D9%88%D8%A5%D8%B6%D8%A7%D9%81%D8%A9-%D8%AA%D8%B5%D8%A7%D8%AF%D9%85%D8%A7%D8%AA-%D9%85%D8%B9-%D8%AE%D8%B7-2d-%D9%81%D9%8A-godot-r2583/" rel="">إعداد كاميرا ديناميكية وإضافة تصادمات مع خط 2D في Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">فهم RayCast2D واستخداماتها في محرك ألعاب جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2585</guid><pubDate>Fri, 11 Jul 2025 16:00:02 +0000</pubDate></item><item><title>&#x628;&#x631;&#x645;&#x62C;&#x629; &#x639;&#x62F;&#x648; &#x648;&#x62D;&#x64A;&#x648;&#x627;&#x646; &#x623;&#x644;&#x64A;&#x641; &#x641;&#x64A; &#x644;&#x639;&#x628;&#x629; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%B9%D8%AF%D9%88-%D9%88%D8%AD%D9%8A%D9%88%D8%A7%D9%86-%D8%A3%D9%84%D9%8A%D9%81-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-godot-r2584/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/04..png.33d6c46559389fd5f39d08bfb1a42d4b.png" /></p>
<p>
	سنوضّح في هذا المقال من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a> كيفية برمجة عدو لمطاردة اللاعب، وكيفية برمجة كائن في اللعبة مثل حيوان أليف ليتبع شخصية اللاعب.
</p>

<h2 id="">
	كيفية برمجة عدو لمطاردة اللاعب
</h2>

<p>
	تتمثّل الخطوة الأولى لجعل العدو يطارد اللاعب في تحديد الاتجاه الذي يجب أن يتحرك به العدو، حيث يمكن الحصول على متجه يؤشّر من A إلى B من خلال عملية الطرح B - A، ثم نوحّد Normalize النتيجة ونحصل على متجه الاتجاه.
</p>

<p>
	سنحتاج هنا إلى ضبط سرعة العدو في كل إطار للتأشير إلى اتجاه اللاعب كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1411_8" style=""><span class="pln">velocity </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">player</span><span class="pun">.</span><span class="pln">position </span><span class="pun">-</span><span class="pln"> position</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed</span></pre>

<p>
	يحتوي كائن <code>Vector2</code> الخاص <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">بمحرّك الألعاب جودو Godot</a> على دالة مساعدة مُضمَّنة لهذا الغرض وهي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1411_10" style=""><span class="pln">velocity </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">player</span><span class="pun">.</span><span class="pln">position</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed</span></pre>

<p>
	سيؤدي ذلك إلى السماح للعدو بمطاردة اللاعب من أيّ مسافة حتى وإن كانت بعيدة، وهذه مشكلة إلى حد ما ستحتاج إلى حلها من خلال إضافة العقدة <code>Area2D</code> إلى العدو ومطاردة اللاعب عندما يكون ضمن نطاق الكشف Detect Radius فقط.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172685" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_chase_01.png.41349205e479e977d64f658ce4d26e3d.png" rel=""><img alt="01 chase 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172685" data-unique="u3de3siy9" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_chase_01.png.41349205e479e977d64f658ce4d26e3d.png"> </a>
</p>

<p>
	سنربط الآن إشارات <code>body_entered</code> و <code>body_exited</code> من العقدة <code>Area2D</code> حتى يعرف العدو ما إذا كان اللاعب ضمن المجال أم لا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1411_12" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var run_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">25</span><span class="pln">
var player </span><span class="pun">=</span><span class="pln"> null

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">if</span><span class="pln"> player</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">player</span><span class="pun">.</span><span class="pln">position</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> run_speed
    move_and_slide</span><span class="pun">()</span><span class="pln">

func _on_DetectRadius_body_entered</span><span class="pun">(</span><span class="pln">body</span><span class="pun">):</span><span class="pln">
    player </span><span class="pun">=</span><span class="pln"> body

func _on_DetectRadius_body_exited</span><span class="pun">(</span><span class="pln">body</span><span class="pun">):</span><span class="pln">
    player </span><span class="pun">=</span><span class="pln"> null</span></pre>

<p>
	<strong>ملاحظة</strong>: تفترض الشيفرة البرمجية السابقة أن اللاعب هو الجسم الوحيد الذي يدخل أو يخرج، والذي يحدث عادةً من خلال ضبط طبقات أو أقنعة التصادم المناسبة.
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172687" data-unique="5zih71q5z">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_chase_02.webm.401c6368f93032123ba4ea3845356b69.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172687" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172687&amp;key=07a55dcbc96a809a4ffb21ecdc2b1bff" rel="">02_chase_02.webm</a>
	</source></video>
</p>

<p>
	 يمكن توسيع هذا المفهوم ليشمل أنواعًا أخرى من الألعاب، فالفكرة الأساسية هي العثور على متجه الاتجاه من العدو إلى اللاعب، حيث إذا كانت لعبتنا مثلًا هي لعبة ذات عرض جانبي أو لها قيود أخرى في الحركة، فيمكننا استخدام المكوِّن x فقط من المتجه الناتج لتحديد الحركة.
</p>

<h3 id="-1">
	القيود
</h3>

<p>
	يمكن ملاحظة أن هذه الطريقة تعطي حركةً خطيةً مستقيمةً بسيطة، إذ لن يتحرك العدو حول العوائق مثل الجدران، كما لن يتوقف إذا اقترب من اللاعب كثيرًا؛ ويعتمد ما يجب فعله هنا عندما يقترب العدو من اللاعب على طبيعة لعبتنا، إذ يمكن إضافة منطقة ثانية أصغر تتسبب في توقف العدو ومهاجمته، أو يمكن جعل اللاعب يتراجع عند التلامس.
</p>

<p>
	توجد مشكلة أخرى مع الأعداء سريعي الحركة، إذ سيغير الأعداء الذين يستخدمون هذه التقنية اتجاههم مباشرةً مع تحرّك اللاعب، ويمكن الحصول على حركة طبيعية أكثر من خلال استخدام سلوك التوجيه Steering.
</p>

<h2 id="-2">
	برمجة كائن في اللعبة مثل حيوان أليف ليتبع شخصية اللاعب
</h2>

<p>
	لنفترض أن لدينا كيان في لعبتنا مثل حيوان أليف أو تابع لمتابعة شخصية اللاعب كما يلي:
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172684" data-unique="kcdgu0bct">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_pet_follow.webm.12a8e9919943c2754cd998f0f0b121e8.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172684" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172684&amp;key=d8bffbe4e2bb369cbdad8a50bc937f68" rel="">03_pet_follow.webm</a>
	</source></video>
</p>

<p>
	 سنبدأ أولًا بإضافة عقدة <code>Marker2D</code> إلى الشخصية، والتي تمثل المكان الذي يريد الحيوان الأليف المشي فيه بالقرب من الشخصية.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172686" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_pet_follow_01.png.77c2988c79f005ff26366960d8b927b7.png" rel=""><img alt="04 pet follow 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172686" data-unique="op1ar7127" style="width: 500px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_pet_follow_01.png.77c2988c79f005ff26366960d8b927b7.png"> </a>
</p>

<p>
	جعلنا عقدة <code>Marker2D</code> في مثالنا ابنًا للعقدة <code>Sprite2D</code>، لأن الشيفرة البرمجية للشخصية تستخدم الخاصية <code>‎$Sprite2D.scale.x = -1</code> لقلب الاتجاه الأفقي عندما تتحرك الشخصية إلى اليسار، وبالتالي ستنقلب عقدة <code>Marker2D</code> أيضًا لأنها ابن للعقدة <code>Sprite2D</code>.
</p>

<h3 id="-3">
	سكربت الحيوان الأليف
</h3>

<p>
	فيما يلي سكربت الحيوان الأليف:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1411_14" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var parent </span><span class="pun">:</span><span class="pln"> </span><span class="typ">CharacterBody2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">25</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var follow_point </span><span class="pun">=</span><span class="pln"> parent</span><span class="pun">.</span><span class="pln">get_node</span><span class="pun">(</span><span class="str">"Sprite2D/FollowPoint"</span><span class="pun">)</span></pre>

<p>
	يحتوي المتغير <code>parent</code> على مرجع للشخصية التي يجب أن يتبعها الحيوان الأليف، ونحصل بعد ذلك على عقدة <code>FollowPoint</code> منه لنتمكّن من الحصول على موضعه في الدالة <code>‎_physics_process()‎</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1411_16" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var target </span><span class="pun">=</span><span class="pln"> follow_point</span><span class="pun">.</span><span class="pln">global_position
    velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">distance_to</span><span class="pun">(</span><span class="pln">target</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">5</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">target</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed

    </span><span class="kwd">if</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">x </span><span class="pun">!=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        $Sprite2D</span><span class="pun">.</span><span class="pln">scale</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> sign</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">x</span><span class="pun">)</span><span class="pln">

    </span><span class="kwd">if</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        $AnimationPlayer</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="str">"run"</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
        $AnimationPlayer</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="str">"idle"</span><span class="pun">)</span><span class="pln">

    move_and_slide</span><span class="pun">()</span></pre>

<p>
	إذا كان الموضع قريبًا من نقطة الهدف، فسنوقف حركة الحيوان الأليف.
</p>

<h3 id="-4">
	التنقل عبر المعيقات
</h3>

<p>
	يجب الانتباه إلى أننا قد نجد الحيوان الأليف عالقًا بين المعيقات الموجودة بعالمنا الخاص باللعبة، لذا يمكن استخدام التنقل لتكون متابعة الحيوان الأليف للشخصية أقوى.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/ai_behavior_demos" rel="external nofollow">Github</a>.
</p>

<h2 id="-5">
	ختامًا
</h2>

<p>
	بهذا نكون قد تعرفنا على كيفية برمجة شخصية عدو قادرة على مطاردة اللاعب، إلى جانب برمجة حيوان أليف قادر تتبع اللاعب داخل اللعبة، عبر محرك جودو Godot.
</p>

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/ai/chasing/index.html" rel="external nofollow">Chasing the player</a> و <a href="https://kidscancode.org/godot_recipes/4.x/ai/pet_following/index.html" rel="external nofollow">Pet Following</a> من توثيقات Kidscancode.
</p>

<h2 id="-6">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%83%D8%A7%D9%85%D9%8A%D8%B1%D8%A7-%D8%AF%D9%8A%D9%86%D8%A7%D9%85%D9%8A%D9%83%D9%8A%D8%A9-%D9%88%D8%A5%D8%B6%D8%A7%D9%81%D8%A9-%D8%AA%D8%B5%D8%A7%D8%AF%D9%85%D8%A7%D8%AA-%D9%85%D8%B9-%D8%AE%D8%B7-2d-%D9%81%D9%8A-godot-r2583/" rel="">إعداد كاميرا ديناميكية وإضافة تصادمات مع خط 2D في Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D8%B9%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A3%D8%AC%D8%B3%D8%A7%D9%85-%D8%A7%D9%84%D8%B5%D9%84%D8%A8%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2557/" rel="">التفاعل بين الشخصيات والأجسام الصلبة في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">فهم RayCast2D واستخداماتها في محرك ألعاب جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2584</guid><pubDate>Wed, 09 Jul 2025 16:07:01 +0000</pubDate></item><item><title>&#x625;&#x639;&#x62F;&#x627;&#x62F; &#x643;&#x627;&#x645;&#x64A;&#x631;&#x627; &#x62F;&#x64A;&#x646;&#x627;&#x645;&#x64A;&#x643;&#x64A;&#x629; &#x648;&#x625;&#x636;&#x627;&#x641;&#x629; &#x62A;&#x635;&#x627;&#x62F;&#x645;&#x627;&#x62A; &#x645;&#x639; &#x62E;&#x637; 2D &#x641;&#x64A; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%83%D8%A7%D9%85%D9%8A%D8%B1%D8%A7-%D8%AF%D9%8A%D9%86%D8%A7%D9%85%D9%8A%D9%83%D9%8A%D8%A9-%D9%88%D8%A5%D8%B6%D8%A7%D9%81%D8%A9-%D8%AA%D8%B5%D8%A7%D8%AF%D9%85%D8%A7%D8%AA-%D9%85%D8%B9-%D8%AE%D8%B7-2d-%D9%81%D9%8A-godot-r2583/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/00-03..png.5c8abcc81da916171a0e59d662211110.png" /></p>
<p>
	سنوضّح في هذا المقال وهو جزء من سلسلة <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">دليل جودو</a> كيفية إعداد كاميرا ديناميكية تتحرّك وتكبّر وتصغّر المشهد لإبقاء عدة عناصر على الشاشة في الوقت نفسه، وسنتعرّف على كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد.
</p>

<h2 id="">
	طريقة إعداد كاميرا ديناميكية تتبع عدة أهداف في وقت واحد
</h2>

<p>
	سنوضّح فيما يلي كيفية إعداد كاميرا ديناميكية لإبقاء عدة عناصر على الشاشة في الوقت نفسه.
</p>

<p>
	لنفترض أن لدينا لعبة تحتوي على لاعبَين ويجب إبقاء اللاعبين على الشاشة أثناء تحرّكهما، سواءً عند التباعد عن بعضهما البعض أو عند وجودهما قرب بعضهما البعض كما في المثال التالي:
</p>

<p style="text-align: center;">
	<img alt="01_multi_cam_01.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="172683" data-ratio="58.67" data-unique="4wwgmyzhv" width="300" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_multi_cam_01.gif.56843592390eed9feb9171c77cddb650.gif">
</p>

<p>
	قد نكون معتادين على إرفاق الكاميرا باللاعب في لعبة تحتوي على لاعب واحد، بحيث تتبع الكاميرا هذا اللاعب تلقائيًا، ولكننا لا نجد ذلك مطبقًا عند وجود لاعبَين أو أكثر، أو عناصر أخرى في اللعبة نريد إبقاءها على الشاشة طوال الوقت.
</p>

<p>
	لحل المشكلة، يمكننا تطبيق ما يلي على الكاميرا:
</p>

<ol>
	<li>
		إضافة أو إزالة أيّ عددٍ من الأهداف
	</li>
	<li>
		إبقاء موضع الكاميرا متمركزًا عند نقطة المنتصف للأهداف
	</li>
	<li>
		ضبط تكبير أو تصغير الكاميرا لإبقاء جميع الأهداف على الشاشة
	</li>
</ol>

<p>
	سننشئ مشهدًا جديدًا باستخدام عقدة <code>Camera2D</code> ونرفق بها سكربتًا، وسنضيف هذه الكاميرا إلى لعبتنا بعد الانتهاء.
</p>

<p>
	سيبدأ السكربت بالتعليمات التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_9" style=""><span class="pln">extends </span><span class="typ">Camera2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var move_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">30</span><span class="pln"> </span><span class="com"># ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var zoom_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">3.0</span><span class="pln">  </span><span class="com"># ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var min_zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">5.0</span><span class="pln">  </span><span class="com"># لن تقترب الكاميرا أكثر من هذه القيمة</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var max_zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.5</span><span class="pln">  </span><span class="com"># لن تبتعد الكاميرا أكثر من هذه القيمة</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var margin </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">400</span><span class="pun">,</span><span class="pln"> </span><span class="lit">200</span><span class="pun">)</span><span class="pln">  </span><span class="com"># تضمين بعض المساحة العازلة حول الأهداف</span><span class="pln">

var targets </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span><span class="pln">  </span><span class="com"># مصفوفة الأهداف التي يجب تعقّبها</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var screen_size </span><span class="pun">=</span><span class="pln"> get_viewport_rect</span><span class="pun">().</span><span class="pln">size</span></pre>

<p>
	تمكّن هذه الإعدادات من ضبط سلوك الكاميرا، إذ سنستخدم الدالة <code>lerp()‎</code> التي تمثّل الاستيفاء الخطي لجميع تغييرات الكاميرا، ولهذا سيؤدي ضبط سرعات الحركة/التكبير والتصغير على قيم أقل إلى بعض التأخير في تتبّع الكاميرا للتغييرات المفاجئة.
</p>

<p>
	تعتمد قيم التكبير أو التصغير العليا والدنيا أيضًا على حجم الكائنات في لعبتنا ومدى القرب أو البعد الذي نريده، لذا سنضبط هذه القيم لتناسب احتياجاتنا.
</p>

<p>
	تضيف الخاصية <code>margin</code> مساحةً إضافيةً حول الأهداف، بحيث لا تكون على حافة المنطقة القابلة للعرض بالضبط. ستكون لدينا مصفوفة أهداف، ونحصل على حجم نافذة العرض لنتمكّن من حساب المقياس الصحيح.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_11" style=""><span class="pln">func add_target</span><span class="pun">(</span><span class="pln">t</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> t </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        targets</span><span class="pun">.</span><span class="pln">append</span><span class="pun">(</span><span class="pln">t</span><span class="pun">)</span><span class="pln">

func remove_target</span><span class="pun">(</span><span class="pln">t</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> t </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        targets</span><span class="pun">.</span><span class="pln">erase</span><span class="pun">(</span><span class="pln">t</span><span class="pun">)</span></pre>

<p>
	توجد دالتان مساعدتان لإضافة وإزالة الأهداف، ويمكنك استخدامهما أثناء اللعب لتغيير الأهداف التي يجب تعقّبها مثل دخول اللاعب 3 إل اللعبة. وكما نلاحظ، لا نريد تعقّب الهدف نفسه مرتين، لذا سنرفضه إذا كان موجودًا مسبقًا.
</p>

<p>
	تحدث معظم الوظائف في الدالة <code>‎_process()‎</code>، ولكن لنبدأ أولًا بتحريك الكاميرا كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_13" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">!</span><span class="pln">targets</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">return</span><span class="pln">
    </span><span class="com"># إبقاء الكاميرا متمركزة بين الأهداف</span><span class="pln">
    var p </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">for</span><span class="pln"> target </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        p </span><span class="pun">+=</span><span class="pln"> target</span><span class="pun">.</span><span class="pln">position
    p </span><span class="pun">/=</span><span class="pln"> targets</span><span class="pun">.</span><span class="pln">size</span><span class="pun">()</span><span class="pln">
    position </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">position</span><span class="pun">,</span><span class="pln"> p</span><span class="pun">,</span><span class="pln"> move_speed </span><span class="pun">*</span><span class="pln"> delta</span><span class="pun">)</span></pre>

<p>
	سنكرّر ضمن الحلقة التأكيد على مواضع الأهداف ونعثر على المركز المشترك، ونتأكّد باستخدام الدالة <code>lerp()‎</code> من التحرّك بسلاسة.
</p>

<p>
	بعد ذلك، لا بد لنا من التعامل مع التكبير أو التصغير كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_16" style=""><span class="com"># العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف</span><span class="pln">
var r </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">position</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE</span><span class="pun">)</span><span class="pln">
</span><span class="kwd">for</span><span class="pln"> target </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
    r </span><span class="pun">=</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">expand</span><span class="pun">(</span><span class="pln">target</span><span class="pun">.</span><span class="pln">position</span><span class="pun">)</span><span class="pln">
r </span><span class="pun">=</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">grow_individual</span><span class="pun">(</span><span class="pln">margin</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">y</span><span class="pun">)</span><span class="pln">
var z
</span><span class="kwd">if</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&gt;</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">*</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">aspect</span><span class="pun">():</span><span class="pln">
    z </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> clamp</span><span class="pun">(</span><span class="pln">r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">/</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> min_zoom</span><span class="pun">,</span><span class="pln"> max_zoom</span><span class="pun">)</span><span class="pln">
</span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
    z </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> clamp</span><span class="pun">(</span><span class="pln">r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">/</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> min_zoom</span><span class="pun">,</span><span class="pln"> max_zoom</span><span class="pun">)</span><span class="pln">
zoom </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">zoom</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE </span><span class="pun">*</span><span class="pln"> z</span><span class="pun">,</span><span class="pln"> zoom_speed</span><span class="pun">)</span></pre>

<p>
	تأتي الوظيفة الأساسية من الصنف <code>Rect2</code>، إذ نريد العثور على مستطيل يحيط بكل الأهداف، والذي يمكننا الحصول عليه باستخدام التابع <code>expand()‎</code>، ثم نوسّع المستطيل باستخدام الخاصية <code>margin</code>.
</p>

<p>
	يجب علينا هنا الضغط على زر Tab بالمستطيل المرسوم لتفعيل هذا الرسم في المشروع التجريبي:
</p>

<p style="text-align: center;">
	<img alt="02_multi_cam_02.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="172682" data-ratio="58.67" data-unique="3n16ye0pw" width="300" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_multi_cam_02.gif.c97df3ac0b430778699e820545847ee2.gif">
</p>

<p>
	نجد المقياس ونثبّته في النطاق الأقصى/الأدنى الذي حدّدناه اعتمادًا على ما إذا كان المستطيل أوسع أو أطول، إذ يتعلق ذلك بالنسبة إلى أبعاد الشاشة.
</p>

<h3 id="-1">
	السكربت الكامل
</h3>

<p>
	سيكون السكربت الكامل للعملية على النحو الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_18" style=""><span class="pln">extends </span><span class="typ">Camera2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var move_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">30</span><span class="pln"> </span><span class="com"># ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var zoom_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">3.0</span><span class="pln">  </span><span class="com"># ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var min_zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">5.0</span><span class="pln">  </span><span class="com"># لن تقترب الكاميرا أكثر من هذه القيمة</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var max_zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.5</span><span class="pln">  </span><span class="com"># لن تبتعد الكاميرا أكثر من هذه القيمة</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var margin </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">400</span><span class="pun">,</span><span class="pln"> </span><span class="lit">200</span><span class="pun">)</span><span class="pln">  </span><span class="com"># تضمين بعض المساحة العازلة حول الأهداف</span><span class="pln">

var targets </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var screen_size </span><span class="pun">=</span><span class="pln"> get_viewport_rect</span><span class="pun">().</span><span class="pln">size

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">!</span><span class="pln">targets</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">return</span><span class="pln">

    </span><span class="com"># إبقاء الكاميرا متمركزة بين الأهداف</span><span class="pln">
    var p </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">for</span><span class="pln"> target </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        p </span><span class="pun">+=</span><span class="pln"> target</span><span class="pun">.</span><span class="pln">position
    p </span><span class="pun">/=</span><span class="pln"> targets</span><span class="pun">.</span><span class="pln">size</span><span class="pun">()</span><span class="pln">
    position </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">position</span><span class="pun">,</span><span class="pln"> p</span><span class="pun">,</span><span class="pln"> move_speed </span><span class="pun">*</span><span class="pln"> delta</span><span class="pun">)</span><span class="pln">

    </span><span class="com"># العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف</span><span class="pln">
    var r </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">position</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> target </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        r </span><span class="pun">=</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">expand</span><span class="pun">(</span><span class="pln">target</span><span class="pun">.</span><span class="pln">position</span><span class="pun">)</span><span class="pln">
    r </span><span class="pun">=</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">grow_individual</span><span class="pun">(</span><span class="pln">margin</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> margin</span><span class="pun">.</span><span class="pln">y</span><span class="pun">)</span><span class="pln">
    var z
    </span><span class="kwd">if</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&gt;</span><span class="pln"> r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">*</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">aspect</span><span class="pun">():</span><span class="pln">
        z </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> clamp</span><span class="pun">(</span><span class="pln">r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">/</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> max_zoom</span><span class="pun">,</span><span class="pln"> min_zoom</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
    z </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> clamp</span><span class="pun">(</span><span class="pln">r</span><span class="pun">.</span><span class="pln">size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">/</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> max_zoom</span><span class="pun">,</span><span class="pln"> min_zoom</span><span class="pun">)</span><span class="pln">
    zoom </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">zoom</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE </span><span class="pun">*</span><span class="pln"> z</span><span class="pun">,</span><span class="pln"> zoom_speed </span><span class="pun">*</span><span class="pln"> delta</span><span class="pun">)</span><span class="pln">

    </span><span class="com"># لتنقيح الأخطاء</span><span class="pln">
    get_parent</span><span class="pun">().</span><span class="pln">draw_cam_rect</span><span class="pun">(</span><span class="pln">r</span><span class="pun">)</span><span class="pln">

func add_target</span><span class="pun">(</span><span class="pln">t</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> t </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        targets</span><span class="pun">.</span><span class="pln">append</span><span class="pun">(</span><span class="pln">t</span><span class="pun">)</span><span class="pln">

func remove_target</span><span class="pun">(</span><span class="pln">t</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> t </span><span class="kwd">in</span><span class="pln"> targets</span><span class="pun">:</span><span class="pln">
        targets</span><span class="pun">.</span><span class="pln">remove</span><span class="pun">(</span><span class="pln">t</span><span class="pun">)</span></pre>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/multitarget_camera" rel="external nofollow">Github</a>.
</p>

<h2 id="-2">
	إضافة تصادمات مع خط مرسوم ثنائي الأبعاد
</h2>

<p>
	سنوضّح فيما يلي كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد باستخدام عقدة <code>Line2D</code>.
</p>

<h3 id="-3">
	إعداد العقد
</h3>

<p>
	سنحتاج لإضافة العقد التالية إلى مشهدنا، ورسم الخط الذي نريده:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_20" style=""><span class="typ">Line2D</span><span class="pln">
    </span><span class="typ">StaticBody2D</span></pre>

<p>
	لا داعي لإضافة شكل تصادم إلى الجسم حاليًا.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن استخدام العقدة <code>Area2D</code> عوضًا عن ذلك إذا أردنا اكتشاف التداخل مع الخط بدلًا من التصادم.
</p>

<p>
	يجب بعد ذلك إضافة أشكال تصادم إلى الجسم، حيث يوجد لدينا خياران كما سنوضّح فيما يلي.
</p>

<h3 id="segmentshape2d">
	الخيار الأول: استخدام الشكل SegmentShape2D
</h3>

<p>
	يمثّل الشكل <code>SegmentShape2D</code> شكل تصادم لخط ومقطع، حيث نريد إنشاء تصادم مقاطع لكل زوج من النقاط على الخط.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_22" style=""><span class="pln">extends </span><span class="typ">Line2D</span><span class="pln">

func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> i </span><span class="kwd">in</span><span class="pln"> points</span><span class="pun">.</span><span class="pln">size</span><span class="pun">()</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">
        var new_shape </span><span class="pun">=</span><span class="pln"> </span><span class="typ">CollisionShape2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
        $StaticBody2D</span><span class="pun">.</span><span class="pln">add_child</span><span class="pun">(</span><span class="pln">new_shape</span><span class="pun">)</span><span class="pln">
        var segment </span><span class="pun">=</span><span class="pln"> </span><span class="typ">SegmentShape2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
        segment</span><span class="pun">.</span><span class="pln">a </span><span class="pun">=</span><span class="pln"> points</span><span class="pun">[</span><span class="pln">i</span><span class="pun">]</span><span class="pln">
        segment</span><span class="pun">.</span><span class="pln">b </span><span class="pun">=</span><span class="pln"> points</span><span class="pun">[</span><span class="pln">i </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">]</span><span class="pln">
        new_shape</span><span class="pun">.</span><span class="pln">shape </span><span class="pun">=</span><span class="pln"> segment</span></pre>

<h3 id="rectangleshape2d">
	الخيار الثاني: استخدام الشكل RectangleShape2D
</h3>

<p>
	لا يحتوي الشكل <code>SegmentShape2D</code> على أيّ مكوّن للعرض، لذا إذا أردنا أن يكون لتصادم الخطوط ثخانة، فيمكن استخدام تصادم المستطيل بدلًا من ذلك.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8777_24" style=""><span class="pln">extends </span><span class="typ">Line2D</span><span class="pln">

func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> i </span><span class="kwd">in</span><span class="pln"> points</span><span class="pun">.</span><span class="pln">size</span><span class="pun">()</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">
        var new_shape </span><span class="pun">=</span><span class="pln"> </span><span class="typ">CollisionShape2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
        $StaticBody2D</span><span class="pun">.</span><span class="pln">add_child</span><span class="pun">(</span><span class="pln">new_shape</span><span class="pun">)</span><span class="pln">
        var rect </span><span class="pun">=</span><span class="pln"> </span><span class="typ">RectangleShape2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
        new_shape</span><span class="pun">.</span><span class="pln">position </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">points</span><span class="pun">[</span><span class="pln">i</span><span class="pun">]</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> points</span><span class="pun">[</span><span class="pln">i </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">])</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
        new_shape</span><span class="pun">.</span><span class="pln">rotation </span><span class="pun">=</span><span class="pln"> points</span><span class="pun">[</span><span class="pln">i</span><span class="pun">].</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">points</span><span class="pun">[</span><span class="pln">i </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">]).</span><span class="pln">angle</span><span class="pun">()</span><span class="pln">
        var length </span><span class="pun">=</span><span class="pln"> points</span><span class="pun">[</span><span class="pln">i</span><span class="pun">].</span><span class="pln">distance_to</span><span class="pun">(</span><span class="pln">points</span><span class="pun">[</span><span class="pln">i </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">])</span><span class="pln">
        rect</span><span class="pun">.</span><span class="pln">extents </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">length </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">,</span><span class="pln"> width </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">)</span><span class="pln">
        new_shape</span><span class="pun">.</span><span class="pln">shape </span><span class="pun">=</span><span class="pln"> rect</span></pre>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/line2d_collision" rel="external nofollow">Github</a>.
</p>

<h2 id="-4">
	ختامًا
</h2>

<p>
	بهذا نكون قد وصلنا إلى نهاية المقال الذي حددنا فيه طريقة إعداد الكاميرا لتكون ديناميكية داخل اللعبة، كما تعرفنا على كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد.
</p>

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/2d/multi_target_camera/index.html" rel="external nofollow">Multitarget Camera</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/line_collision/index.html" rel="external nofollow">Line2D Collision</a> من توثيقات Kidscancode.
</p>

<h2 id="-5">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%85%D9%82%D8%B0%D9%88%D9%81%D8%A7%D8%AA-%D9%88%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D8%B9%D8%B1%D8%B6-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2573/" rel="">إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AF%D9%84%D9%8A%D9%84%D9%83-%D8%A7%D9%84%D8%B4%D8%A7%D9%85%D9%84-%D8%A5%D9%84%D9%89-%D8%A8%D9%86%D8%A7%D8%A1-%D9%83%D8%A7%D9%85%D9%8A%D8%B1%D8%A7-%D8%AE%D8%A7%D8%B5%D8%A9-%D8%A8%D8%B4%D8%A7%D8%B4%D8%A7%D8%AA-%D8%A7%D9%84%D9%84%D9%85%D8%B3-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%91%D9%83-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2255/" rel="">دليلك الشامل إلى بناء كاميرا خاصة بشاشات اللمس في محرّك اﻷلعاب جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">تعرف على العقد Nodes في محرك ألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2583</guid><pubDate>Mon, 07 Jul 2025 16:07:02 +0000</pubDate></item><item><title>&#x62E;&#x648;&#x627;&#x631;&#x632;&#x645;&#x64A;&#x629; &#x627;&#x644;&#x628;&#x62D;&#x62B; &#x639;&#x646; &#x627;&#x644;&#x645;&#x633;&#x627;&#x631; &#x641;&#x64A; &#x628;&#x64A;&#x626;&#x629; &#x623;&#x644;&#x639;&#x627;&#x628; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F;</title><link>https://academy.hsoub.com/programming/game-development/%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A9-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D8%B9%D9%86-%D8%A7%D9%84%D9%85%D8%B3%D8%A7%D8%B1-%D9%81%D9%8A-%D8%A8%D9%8A%D8%A6%D8%A9-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2577/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/02..png.75fdb52bf94c3c2c1b9b2ea55101ccff.png" /></p>
<p>
	سنوضّح في هذا المقال كيفية إعداد خوارزمية للبحث عن المسار للسماح بالتنقل في بيئة قائمة على الشبكة Grid، حيث يوفّر محرّك الألعاب جودو Godot عددًا من الطرق لتحديد المسار، ولكننا سنستخدم في هذا المقال خوارزمية <code>A*‎</code>، التي لها استخدام واسع في العثور على أقصر مسار بين نقطتين، ويمكن استخدامها في أيّ هيكل بيانات قائمٍ على الرسم البياني Graph، وليس في بيئة شبكية فقط.
</p>

<p>
	يُعَد الصنف <code>AStarGrid2D</code> نسخةً متخصصةً من الصنف <code>AStar2D</code> الأعم في جودو، لذا يُعَد إعداده أسرع وأسهل، نظرًا لأنه متخصص للاستخدام مع الشبكة، إذ لسنا مضطرين لإضافة جميع خلايا الشبكة الفردية واتصالاتها يدويًا.
</p>

<h2 id="">
	إعداد الشبكة
</h2>

<p>
	يُعَد ضبط حجم الخلايا والشبكة أهم خطوة، حيث سنستخدم الحجم ‎(64, 64)‎ في مثالنا، وسنستخدم حجم النافذة لتحديد عدد الخلايا الملائمة للشاشة، ولكن كل شيء سيعمل بالطريقة نفسها بغض النظر عن حجم الخلية.
</p>

<p>
	سنضيف الآن الشيفرة البرمجية التالية إلى <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2490/" rel="">العقدة Node2D</a>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_8" style=""><span class="pln">extends </span><span class="typ">Node2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var cell_size </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2i</span><span class="pun">(</span><span class="lit">64</span><span class="pun">,</span><span class="pln"> </span><span class="lit">64</span><span class="pun">)</span><span class="pln">

var astar_grid </span><span class="pun">=</span><span class="pln"> </span><span class="typ">AStarGrid2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
var grid_size

func _ready</span><span class="pun">():</span><span class="pln">
    initialize_grid</span><span class="pun">()</span><span class="pln">

func initialize_grid</span><span class="pun">():</span><span class="pln">
    grid_size </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2i</span><span class="pun">(</span><span class="pln">get_viewport_rect</span><span class="pun">().</span><span class="pln">size</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> cell_size
    astar_grid</span><span class="pun">.</span><span class="pln">size </span><span class="pun">=</span><span class="pln"> grid_size
    astar_grid</span><span class="pun">.</span><span class="pln">cell_size </span><span class="pun">=</span><span class="pln"> cell_size
    astar_grid</span><span class="pun">.</span><span class="pln">offset </span><span class="pun">=</span><span class="pln"> cell_size </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
    astar_grid</span><span class="pun">.</span><span class="pln">update</span><span class="pun">()</span></pre>

<p>
	نقسم حجمَ الشاشة على حجم الخلية <code>cell_size</code> في هذه الشيفرة البرمجية لحساب حجم الشبكة بالكامل، مما يتيح ضبط الخاصية <code>size</code> الخاصة بالصنف <code>AStarGrid2D</code>.
</p>

<p>
	تُستخدَم خاصية الإزاحة <code>offset</code> عندما نطلب مسارًا بين نقطتين، ويمثّل استخدام <code>cell_size / 2</code> حساب المسار من مركز كل خلية بدلًا من زواياها، ويجب استدعاء الدالة <code>update()‎</code> بعد ضبط أو تغيير خاصيات الصنف <code>AStarGrid2D</code>.
</p>

<h2 id="-1">
	رسم الشبكة
</h2>

<p>
	سنرسم الشبكة على الشاشة في الشيفرة البرمجية للتوضيح، ولكن قد تكون لدينا عقدة <code>TileMap</code> أو أيّ تمثيل مرئي آخر لعالمنا في تطبيق اللعبة. يمكن رسم الشبكة باستخدام الشيفرة البرمجية التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_10" style=""><span class="pln">func _draw</span><span class="pun">():</span><span class="pln">
    draw_grid</span><span class="pun">()</span><span class="pln">

func draw_grid</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> x </span><span class="kwd">in</span><span class="pln"> grid_size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">
        draw_line</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">),</span><span class="pln">
            </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> grid_size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">),</span><span class="pln">
            </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">DARK_GRAY</span><span class="pun">,</span><span class="pln"> </span><span class="lit">2.0</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> y </span><span class="kwd">in</span><span class="pln"> grid_size</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">
        draw_line</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> y </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">),</span><span class="pln">
            </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">grid_size</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> y </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">),</span><span class="pln">
            </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">DARK_GRAY</span><span class="pun">,</span><span class="pln"> </span><span class="lit">2.0</span><span class="pun">)</span></pre>

<p>
	وسنحصل على الشبكة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172344" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_astar_grid_01.png.8bd39ace404ed30c713022c51c49fb80.png" rel=""><img alt="01 astar grid 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172344" data-unique="erhv1lyqk" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_astar_grid_01.png.8bd39ace404ed30c713022c51c49fb80.png"> </a>
</p>

<h2 id="-2">
	رسم المسار
</h2>

<p>
	نحتاج إلى نقطة بداية ونقطة نهاية للعثور على مسار، لذا علينا إضافة المتغيرات التالية في بداية السكربت:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_12" style=""><span class="pln">var start </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2i</span><span class="pun">.</span><span class="pln">ZERO
var end </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2i</span><span class="pun">(</span><span class="lit">5</span><span class="pun">,</span><span class="pln"> </span><span class="lit">5</span><span class="pun">)</span></pre>

<p>
	بعد ذلك نضيف الأسطر التالية في الدالة <code>‎_draw()‎</code> لإظهار هذه النقاط:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_14" style=""><span class="pln">    draw_rect</span><span class="pun">(</span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">start </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">,</span><span class="pln"> cell_size</span><span class="pun">),</span><span class="pln"> </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">GREEN_YELLOW</span><span class="pun">)</span><span class="pln">
    draw_rect</span><span class="pun">(</span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">end </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">,</span><span class="pln"> cell_size</span><span class="pun">),</span><span class="pln"> </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">ORANGE_RED</span><span class="pun">)</span></pre>

<p>
	يمكننا الآن العثور على المسار بين النقطتين باستخدام التابع <code>get_point_path()‎</code>، ولكننا نحتاج أيضًا إلى إظهاره، لذا من المهم استخدام العقدة <code>Line2D</code> وإضافة إلى المشهد، حيث يمكن الحصول على المسار وإضافة النقاط الناتجة إلى العقدة <code>Line2D</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_16" style=""><span class="pln">func update_path</span><span class="pun">():</span><span class="pln">
    $Line2D</span><span class="pun">.</span><span class="pln">points </span><span class="pun">=</span><span class="pln"> </span><span class="typ">PackedVector2Array</span><span class="pun">(</span><span class="pln">astar_grid</span><span class="pun">.</span><span class="pln">get_point_path</span><span class="pun">(</span><span class="pln">start</span><span class="pun">,</span><span class="pln"> end</span><span class="pun">))</span></pre>

<p>
	وسنحصل على النتيجة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172348" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_astar_grid_02.png.093bdecadc245cefa95224a157fd5f82.png" rel=""><img alt="02 astar grid 02" class="ipsImage ipsImage_thumbnailed" data-fileid="172348" data-unique="4z3eayjlv" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_astar_grid_02.png.093bdecadc245cefa95224a157fd5f82.png"> </a>
</p>

<p>
	وكما نلاحظ، لدينا خطًا قطريًا بين النقطتين لأن المسار يستخدم الخطوط القطرية افتراضيًا، ويمكن تعديل ذلك من خلال تغيير الخاصية <code>diagonal_mode</code> باستخدام القيم التالية:
</p>

<ul>
	<li>
		<code>DIAGONAL_MODE_ALWAYS</code>: القيمة الافتراضية، وتستخدم الخطوط القطرية
	</li>
	<li>
		<code>DIAGONAL_MODE_NEVER</code>: تكون الحركة عمودية
	</li>
	<li>
		<code>DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE</code>: تسمح هذه القيمة بالخطوط القطرية، ولكنها تمنع المسار من المرور بين العوائق الموضوعة قطريًا
	</li>
	<li>
		<code>DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE</code>: تسمح بالخطوط القطرية في المناطق المفتوحة فقط، وليس بالقرب من العوائق
	</li>
</ul>

<p>
	قد يؤدي تعديل هذه الخاصية إلى إعطاء نتائج مختلفة جدًا، لذا تأكد من التجربة بناءً على الإعداد الذي تستخدمه، لذا يجب إضافة ما يلي في الدالة <code>initialize_grid()‎</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_18" style=""><span class="pln">astar_grid</span><span class="pun">.</span><span class="pln">diagonal_mode </span><span class="pun">=</span><span class="pln"> </span><span class="typ">AStarGrid2D</span><span class="pun">.</span><span class="pln">DIAGONAL_MODE_NEVER</span></pre>

<p>
	وأصبح لدينا الآن الحركات المتعامدة فقط كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172347" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_astar_grid_03.png.e2a45b40d0c4d0921912ce14736cd400.png" rel=""><img alt="03 astar grid 03" class="ipsImage ipsImage_thumbnailed" data-fileid="172347" data-unique="2lcjh1ger" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_astar_grid_03.png.e2a45b40d0c4d0921912ce14736cd400.png"> </a>
</p>

<h2 id="-3">
	إضافة العوائق
</h2>

<p>
	يمكننا أيضًا إضافة العوائق إلى الشبكة، حيث لن يتضمن المسار خليةً ما من خلال وضع علامة عليها بوصفها صلبة Solid، ويمكن التبديل بين القيمتين صلبة وغير صلبة للخلية باستخدام الدالة <code>set_point_solid()‎</code>.
</p>

<p>
	سنضيف الشيفرة البرمجية التالية لرسم الجدران (إن وُجدت) من خلال العثور على الخلايا الصلبة وتلوينها:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_20" style=""><span class="pln">func fill_walls</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> x </span><span class="kwd">in</span><span class="pln"> grid_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">for</span><span class="pln"> y </span><span class="kwd">in</span><span class="pln"> grid_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">:</span><span class="pln">
            </span><span class="kwd">if</span><span class="pln"> astar_grid</span><span class="pun">.</span><span class="pln">is_point_solid</span><span class="pun">(</span><span class="typ">Vector2i</span><span class="pun">(</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> y</span><span class="pun">)):</span><span class="pln">
                draw_rect</span><span class="pun">(</span><span class="typ">Rect2</span><span class="pun">(</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> y </span><span class="pun">*</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> cell_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">),</span><span class="pln"> </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">DARK_GRAY</span><span class="pun">)</span></pre>

<p>
	سنستدعي الآن هذه الدالة في الدالة <code>‎_draw()‎</code>، ويمكن بعد ذلك استخدام الفأرة للنقر على الخلايا وتبديل حالتها كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_22" style=""><span class="pln">func _input</span><span class="pun">(</span><span class="pln">event</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pun">:</span><span class="pln">
        </span><span class="com"># إضافة أو إزالة حائط</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">button_index </span><span class="pun">==</span><span class="pln"> MOUSE_BUTTON_LEFT </span><span class="kwd">and</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">pressed</span><span class="pun">:</span><span class="pln">
            var pos </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2i</span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">position</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> cell_size
            </span><span class="kwd">if</span><span class="pln"> astar_grid</span><span class="pun">.</span><span class="pln">is_in_boundsv</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">):</span><span class="pln">
                astar_grid</span><span class="pun">.</span><span class="pln">set_point_solid</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> astar_grid</span><span class="pun">.</span><span class="pln">is_point_solid</span><span class="pun">(</span><span class="pln">pos</span><span class="pun">))</span><span class="pln">
            update_path</span><span class="pun">()</span><span class="pln">
            queue_redraw</span><span class="pun">()</span></pre>

<p>
	يمكن ملاحظة أننا نستخدم التابع <code>is_in_boundsv()‎</code> أولًا، مما يمنع حدوث أخطاء إذا نقرنا خارج حدود الشبكة.
</p>

<p>
	يمكننا الآن رؤية تأثير العوائق على المسار كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172346" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_astar_grid_04.png.8180d725bcae29eae1143f42930d8896.png" rel=""><img alt="04 astar grid 04" class="ipsImage ipsImage_thumbnailed" data-fileid="172346" data-unique="da7jste1e" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_astar_grid_04.png.8180d725bcae29eae1143f42930d8896.png"> </a>
</p>

<h2 id="heuristic">
	الاختيار الاستكشافي أو التجريبي Heuristic
</h2>

<p>
	يُعَد الاختيار الاستكشافي الذي نستخدمه عاملًا مهمًا يؤثر على المسار الناتج، حيث يشير مصطلح Heuristic إلى أفضل تخمين، ويمثل ببساطة الاتجاه الذي يجب أن نجرّبه أولًا عند التحرك نحو الهدف في سياق العثور على المسار.
</p>

<p>
	تستخدم المسافة الإقليدية مثلًا نظرية فيثاغورس لتقدير المسار الذي يجب تجربته كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172345" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_astar_grid_03.png.da72d687e5bc119ef9b5a69bd84e57e7.png" rel=""><img alt="05 astar grid 03" class="ipsImage ipsImage_thumbnailed" data-fileid="172345" data-unique="hlr61cbyc" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_astar_grid_03.png.da72d687e5bc119ef9b5a69bd84e57e7.png"> </a>
</p>

<p>
	بينما تأخذ مسافة مانهاتن في حساباتها المسافة في اتجاهات الشمال/الجنوب أو الشرق/الغرب فقط كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172349" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_astar_grid_manhattan.png.49a21c24e6025b033a7d0291a5ef8d66.png" rel=""><img alt="06 astar grid manhattan" class="ipsImage ipsImage_thumbnailed" data-fileid="172349" data-unique="8nq577cle" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_astar_grid_manhattan.png.49a21c24e6025b033a7d0291a5ef8d66.png"> </a>
</p>

<p>
	وتعطي طريقة التجزيء الاستكشافية Octile Heuristic المسار التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172350" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_astar_grid_octile.png.362dac862e65f7d315f64a69b59ac1b1.png" rel=""><img alt="07 astar grid octile" class="ipsImage ipsImage_thumbnailed" data-fileid="172350" data-unique="oghy1bzuc" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_astar_grid_octile.png.362dac862e65f7d315f64a69b59ac1b1.png"> </a>
</p>

<p>
	يمكن استخدام الاختيار الاستكشافي باستخدام الخاصية التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5822_24" style=""><span class="pln">astar_grid</span><span class="pun">.</span><span class="pln">default_estimate_heuristic </span><span class="pun">=</span><span class="pln"> </span><span class="typ">AStarGrid2D</span><span class="pun">.</span><span class="pln">HEURISTIC_OCTILE</span></pre>

<p>
	يعتمد تحديد الخيار الأفضل الذي يعطي المسارات المناسب على طبيعة بيئتنا مثل احتوائها على مساحات مفتوحة واسعة مع القليل من العوائق المنتشرة حولها أم أنها متاهة من الممرات المتعرجة، لذا يجب التأكّد من تجربة مشروعنا أولًا.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/grid_pathfinding" rel="external nofollow">Github</a> لتجربة الإعداد، ويمكن استخدام أزرار الفأرة الأيمن/الأوسط لتحريك مواقع النهاية/البداية بالإضافة إلى وضع الجدران.
</p>

<p>
	ترجمة -وبتصرّف- للقسم <a href="https://kidscancode.org/godot_recipes/4.x/2d/grid_pathfinding/index.html" rel="external nofollow">Pathfinding on a 2D Grid</a> من توثيقات Kidscancode.
</p>

<h2 id="-4">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D9%81%D9%87%D9%88%D9%85-coyote-time-%D9%88%D9%83%D9%8A%D9%81%D9%8A%D8%A9-%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%85%D9%86%D8%B5%D8%A9-%D9%85%D8%AA%D8%AD%D8%B1%D9%83%D8%A9-%D9%81%D9%8A-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%B9%D8%A8%D8%A9-r2576/" rel="">مفهوم Coyote Time وكيفية إعداد منصة متحركة في مشهد اللعبة</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%85%D9%82%D8%B0%D9%88%D9%81%D8%A7%D8%AA-%D9%88%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D8%B9%D8%B1%D8%B6-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2573/" rel="">إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2577</guid><pubDate>Mon, 16 Jun 2025 16:07:01 +0000</pubDate></item><item><title>&#x645;&#x641;&#x647;&#x648;&#x645; Coyote Time &#x648;&#x643;&#x64A;&#x641;&#x64A;&#x629; &#x625;&#x639;&#x62F;&#x627;&#x62F; &#x645;&#x646;&#x635;&#x629; &#x645;&#x62A;&#x62D;&#x631;&#x643;&#x629; &#x641;&#x64A; &#x645;&#x634;&#x647;&#x62F; &#x627;&#x644;&#x644;&#x639;&#x628;&#x629;</title><link>https://academy.hsoub.com/programming/game-development/%D9%85%D9%81%D9%87%D9%88%D9%85-coyote-time-%D9%88%D9%83%D9%8A%D9%81%D9%8A%D8%A9-%D8%A5%D8%B9%D8%AF%D8%A7%D8%AF-%D9%85%D9%86%D8%B5%D8%A9-%D9%85%D8%AA%D8%AD%D8%B1%D9%83%D8%A9-%D9%81%D9%8A-%D9%85%D8%B4%D9%87%D8%AF-%D8%A7%D9%84%D9%84%D8%B9%D8%A8%D8%A9-r2576/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/01.CoyoteTime.png.779db41fccb62b502513e58e6dcf9de1.png" /></p>
<p>
	سنوضح في هذا المقال تقنية Coyote Time ونضيفها إلى شخصية منصة موجودة مسبقًا، وسنتعرّف على كيفية تحريك المنصات في لعبة المنصات ثنائية الأبعاد.
</p>

<h2 id="coyotetime">
	شرح مفهوم Coyote Time
</h2>

<p>
	قد يكون القفز غير موجود في ألعاب المنصات، حيث لا يتمتع اللاعب بقدرٍ جيد من التحكم، وقد يفشل أحيانًا في القفز من حافة المنصات.
</p>

<p>
	يتمثّل الحل لهذه المشكلة في استخدام تقنية Coyote Time التي تمنح اللاعب شعورًا أكبر بالتحكم ومساحة ضئيلة للحركة حول عملية القفز من حواف المنصات، حيث تعمل هذه التقنية بالطريقة التالية: إذا مشى اللاعب بعد حافة منصة، فسنسمح له بالقفز كما لو كان لا يزال على الأرض بعد بضعة إطارات.
</p>

<p>
	<strong>ملاحظة</strong>: أتى اسم هذه التقنية من شخصية الذئب Coyote الكرتونية الشهيرة التي لا تسقط حتى تنظر إلى الأسفل:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172342" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_coyote.png.35a387621e2db838059111aa9116eeba.png" rel=""><img alt="01 coyote" class="ipsImage ipsImage_thumbnailed" data-fileid="172342" data-unique="9jof3u1fw" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_coyote.png.35a387621e2db838059111aa9116eeba.png"> </a>
</p>

<p>
	سنضيف هذه التقنية إلى شخصية منصة موجودة مسبقًا، لذا يمكن الاطلاع على مقال <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a> لمعرفة كيفية إعدادها.
</p>

<p>
	سنضيف عقدة <code>Timer</code> بالاسم <code>CoyoteTimer</code> ونضبطها على لقطة واحدة One Shot للتعامل مع ضبط الوقت، وتوجد بعض المتغيرات الجديدة التي نحتاجها لتعقّب وقت coyote كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_8" style=""><span class="pln">var coyote_frames </span><span class="pun">=</span><span class="pln"> </span><span class="lit">6</span><span class="pln">  </span><span class="com"># عدد الإطارات في الهواء المسموح بها للقفز</span><span class="pln">
var coyote </span><span class="pun">=</span><span class="pln"> false  </span><span class="com"># تتبّع ما إذا كنا في وقت‫ coyote أم لا</span><span class="pln">
var last_floor </span><span class="pun">=</span><span class="pln"> false  </span><span class="com"># حالة الإطار الأخير على الأرض</span></pre>

<p>
	سنستخدم الإطارات لضبط المدة، وبالتالي يمكننا نقل ذلك إلى وقت ضبط طول العقدة <code>Timer</code> في التابع <code>‎_ready()‎</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_11" style=""><span class="pln">$CoyoteTimer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> coyote_frames </span><span class="pun">/</span><span class="pln"> </span><span class="lit">60.0</span></pre>

<p>
	سنخزّن القيمة الحالية للتابع <code>is_on_floor()‎</code> في كل إطار لاستخدامها في الإطار التالي، لذا يجب وضع ما يلي في الدالة <code>‎_physics_process()‎</code> بعد الدالة <code>move_and_slide()‎</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_15" style=""><span class="pln">last_floor </span><span class="pun">=</span><span class="pln"> is_on_floor</span><span class="pun">()</span></pre>

<p>
	يجب التحقق مما إذا كانت الشخصية على الأرض أو في وقت Coyote عندما نكتشف إدخال القفز كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_17" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_just_pressed</span><span class="pun">(</span><span class="str">"jump"</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> </span><span class="pun">(</span><span class="pln">is_on_floor</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">or</span><span class="pln"> coyote</span><span class="pun">):</span><span class="pln">
        velocity</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> jump_speed
        jumping </span><span class="pun">=</span><span class="pln"> true</span></pre>

<p>
	يبدأ وقت Coyote إذا مشى اللاعب بعد حافة المنصة، وبالتالي لم يَعُد على الأرض وكان على الأرض في الإطار السابق. يمكننا التحقق من ذلك وبدء تشغيل المؤقت إذا انتقلنا من الأرض إلى فوقها وفق التالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_20" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">!</span><span class="pln">is_on_floor</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> last_floor </span><span class="kwd">and</span><span class="pln"> </span><span class="pun">!</span><span class="pln">jumping</span><span class="pun">:</span><span class="pln">
        coyote </span><span class="pun">=</span><span class="pln"> true
        $CoyoteTimer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span></pre>

<p>
	تخبرنا العقدة <code>CoyoteTimer</code> متى تنتهي حالة coyote كما يلي:
</p>

<pre class="ipsCode">func _on_coyote_timer_timeout():
    coyote = false
</pre>

<p>
	يمكن تطبيق العملية نفسها على الشخصيات ثلاثية الأبعاد.
</p>

<p>
	<strong>ملاحظة</strong>: نُفِّذت شخصية القسم التالي باستخدام تقنية Coyote Time.
</p>

<h2 id="">
	كيفية إعداد منصة متحركة في مشهد اللعبة
</h2>

<p>
	سنوضّح فيما يلي كيفية تحريك المنصات في لعبة المنصات ثنائية الأبعاد، حيث توجد طرق متعددة للتعامل مع ذلك؛ إذ سنستخدم عقد <code>AnimatableBody2D</code> للمنصة ونحرّكها باستخدام عقدة الانتقال التدريجي <code>Tween</code>، مما يسمح بمجموعة متنوعة من أنماط الحركة مع تقليل الشيفرة البرمجية التي يجب كتابتها.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن أيضًا تطبيق هذه التقنية لتحريك المنصات باستخدام عقدة <code>AnimationPlayer</code> بدلًا من عقدة <code>Tween</code>، حيث سيبقى معظم الإعداد نفسه في كلتا الطريقتين، ولكن ستحرّك خاصية موضع <code>position</code> الجسم بدلًا من شيفرة <code>Tween</code> البرمجية.
</p>

<h3 id="-1">
	الإعداد
</h3>

<p>
	سنبدأ بإعداد لعبة منصات بسيطة باستخدام الطريقة المتبعة في مقال إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد، حيث ستعمل هذه الحركة الأساسية بنجاح مع المنصات، وإذا عدّلناها أو استخدمنا طريقتنا الخاصة، فيجب أن يعمل كل شيء بالطريقة نفسها.
</p>

<h3 id="-2">
	إنشاء المنصة
</h3>

<p>
	يحتوي مشهد المنصة على العقد التالية:
</p>

<ul>
	<li>
		<code>Node2D</code> المنصة المتحركة MovingPlatform: تكون العقدة الأب <code>Node2D</code> موجودةً لتعمل بوصفها نقطة ارتكاز أو نقطة بداية للمنصة، حيث سنحرّك موضع <code>position</code> المنصة بالتناسب مع هذه العقدة الأب
	</li>
	<li>
		<code>AnimatableBody2D</code>: تمثل هذه العقدة المنصة نفسها، وهي العقدة التي ستتحرّك
	</li>
	<li>
		<code>Sprite2D</code>: يمكن استخدام ورقة الشخصية الرسومية Sprite Sheet هنا أو صور فردية أو حتى عقدة <code>TileMap</code>
	</li>
	<li>
		<code>CollisionShape2D</code>: لا يجب أن نجعل مربع الاصطدام كبيرًا جدًا، وإلّا فسيبدو اللاعب وكأنه يحوم فوق حافة المنصة
	</li>
</ul>

<p>
	ستُعِدّ خامة Texture العقدة <code>Sprite2D</code> وشكل الاصطدام بطريقة مناسبة، ونضبط خاصية التزامن مع الفيزياء Sync to Physics على القيمة On في العقدة <code>AnimatableBody2D</code>، مما يضمن تحريك الجسم أثناء خطوة الفيزياء بما أننا نحرّكه في الشيفرة البرمجية، وبالتالي يكون متزامنًا مع اللاعب والأجسام الفيزيائية الأخرى.
</p>

<p>
	سنضيف الآن السكربت التالي إلى العقدة الجذر <code>Node2D</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7566_22" style=""><span class="pln">extends </span><span class="typ">Node2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var offset </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="pun">-</span><span class="lit">320</span><span class="pun">)</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var duration </span><span class="pun">=</span><span class="pln"> </span><span class="lit">5.0</span><span class="pln">

func _ready</span><span class="pun">():</span><span class="pln">
    start_tween</span><span class="pun">()</span><span class="pln">

func start_tween</span><span class="pun">():</span><span class="pln">
    var tween </span><span class="pun">=</span><span class="pln"> get_tree</span><span class="pun">().</span><span class="pln">create_tween</span><span class="pun">().</span><span class="pln">set_process_mode</span><span class="pun">(</span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TWEEN_PROCESS_PHYSICS</span><span class="pun">)</span><span class="pln">
    tween</span><span class="pun">.</span><span class="pln">set_loops</span><span class="pun">().</span><span class="pln">set_parallel</span><span class="pun">(</span><span class="pln">false</span><span class="pun">)</span><span class="pln">
    tween</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">$AnimatableBody2d</span><span class="pun">,</span><span class="pln"> </span><span class="str">"position"</span><span class="pun">,</span><span class="pln"> offset</span><span class="pun">,</span><span class="pln"> duration </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">)</span><span class="pln">
    tween</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">$AnimatableBody2d</span><span class="pun">,</span><span class="pln"> </span><span class="str">"position"</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO</span><span class="pun">,</span><span class="pln"> duration </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">)</span></pre>

<p>
	حتى الآن قد استخدمنا بعض خيارات عقد <code>Tween</code> لجعل كل شيء يعمل بسلاسة مثل:
</p>

<ul>
	<li>
		<code>set_process_mode()‎</code>: يضمن حدوث الحركة أثناء خطوة المعالجة الفيزيائية
	</li>
	<li>
		<code>set_loops()‎</code>: يعمل على تكرار الانتقال التدريجي Tween
	</li>
	<li>
		<code>set_parallel(false)‎</code>: تحدث جميع تغييرات <code>tween_property()‎</code> في الوقت نفسه افتراضيًا، ويؤدي هذا التابع إلى حدوث هذين الأمرين الواحد تلوَ الآخر، وهما التحرك إلى أحد طرفي الإزاحة ثم العودة إلى البداية
	</li>
</ul>

<p>
	يمكن ضبط حركة المنصة باستخدام هاتين الخاصيتين المُصدَّرتين، وهما الإزاحة <code>offset</code> التي يجب أن تضبطها لتحديد المكان الذي يتحرك فيه الانتقال التدريجي Tween بالنسبة لنقطة بدايته، و المدة <code>duration</code> التي يجب أن تضبطها لتحديد المدة التي تستغرقها لإكمال الدورة.
</p>

<p>
	سنضيف الان بعض المنصات في المستوى أو العالم الخاص بنا ونجرّبها كما في المثال التالي:
</p>

<p>
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172343" data-unique="jouumgzra">
		<source type="video/webm" width="700" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_moving_platform4.webm.662f85e537a73e1aeec2a4b9e55e06c1.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172343" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172343&amp;key=44ef97b6886059fa7afccc57c99e66c9" rel="">02_moving_platform4.webm</a>
	</source></video>
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/2d_moving_platforms" rel="external nofollow">Github</a>.
</p>

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/2d/coyote_time/index.html" rel="external nofollow">Coyote Time</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/moving_platforms/index.html" rel="external nofollow">Moving Platforms</a> من توثيقات Kidscancode.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%85%D9%82%D8%B0%D9%88%D9%81%D8%A7%D8%AA-%D9%88%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D8%B9%D8%B1%D8%B6-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2573/" rel="">إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">مدخل إلى محرك الألعاب جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D9%88%D8%A7%D8%AC%D9%87%D8%A9-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2484/" rel="">تعرف على واجهة محرك الألعاب جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2576</guid><pubDate>Fri, 13 Jun 2025 16:08:07 +0000</pubDate></item><item><title>&#x625;&#x637;&#x644;&#x627;&#x642; &#x627;&#x644;&#x645;&#x642;&#x630;&#x648;&#x641;&#x627;&#x62A; &#x648;&#x62A;&#x631;&#x62A;&#x64A;&#x628; &#x639;&#x631;&#x636; &#x627;&#x644;&#x643;&#x627;&#x626;&#x646;&#x627;&#x62A; &#x641;&#x64A; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D8%B7%D9%84%D8%A7%D9%82-%D8%A7%D9%84%D9%85%D9%82%D8%B0%D9%88%D9%81%D8%A7%D8%AA-%D9%88%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D8%B9%D8%B1%D8%B6-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2573/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/5.(2).png.10375859e5e20405d04580dc56477af7.png" /></p>
<p>
	سنوضّح في هذا المقال من <a href="https://academy.hsoub.com/tags/%D8%AF%D9%84%D9%8A%D9%84%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">سلسلة دليل جودو</a> كيفية إطلاق المقذوفات وترتيب عرض الكائنات بناءً على موقعها على محور Y في الألعاب ثنائية الأبعاد في محرك الألعاب الشهير جودو Godot.
</p>

<h2 id="">
	إطلاق المقذوفات
</h2>

<p>
	من أجل إطلاق مقذوفات، سنحتاج بطبيعة الحال إلى القيام بعدة خطوات تسمح لنا بتنفيذ العملية بنجاح. سنوضّح فيما يلي كيفية إطلاق المقذوفات من اللاعب أو الشخصية المتوحشة وغير ذلك في الألعاب ثنائية الأبعاد.
</p>

<h3 id="-1">
	إعداد الرصاصة
</h3>

<p>
	سنضبط أولًا كائن الرصاصة Bullet الذي يمكننا إنشاء نسخة منه، وفيما يلي العقد التي سنستخدمها:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_2739_8" style=""><span class="typ">Area2D</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Bullet</span><span class="pln">
    </span><span class="typ">Sprite2D</span><span class="pln">
    </span><span class="typ">CollisionShape2D</span></pre>

<p>
	يمكن استخدام أي صورة نريدها بالنسبة لخامة العقدة <code>Sprite2D</code> كما هو الحال في المثال التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172059" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_laserRed01.png.b99110d353a4d2788347c30eb2edc197.png" rel=""><img alt="01 laserred01" class="ipsImage ipsImage_thumbnailed" data-fileid="172059" data-unique="vloe2ift5" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_laserRed01.png.b99110d353a4d2788347c30eb2edc197.png"> </a>
</p>

<p>
	يمكن الآن إعداد العقد وضبط الشخصية الرسومية Sprite وشكل التصادم. إذا كانت الخامة الخاصة بنا موجهةً نحو الأعلى كما هو الحال في المثال السابق، فسنتأكد من تدوير العقدة <code>Sprite</code> بمقدار 90 درجة بحيث تشير إلى اليمين. سنتأكد أيضًا من أنها تتطابق مع الاتجاه الأمامي للعقدة الأب.
</p>

<p>
	سنضيف الآن سكربت اتصل من خلاله بالإشارة <code>body_entered</code> الخاصة بالعقدة <code>Area2D</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_2739_10" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">750</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta

func _on_Bullet_body_entered</span><span class="pun">(</span><span class="pln">body</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> body</span><span class="pun">.</span><span class="pln">is_in_group</span><span class="pun">(</span><span class="str">"mobs"</span><span class="pun">):</span><span class="pln">
        body</span><span class="pun">.</span><span class="pln">queue_free</span><span class="pun">()</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<p>
	سنزيل الرصاصة في مثالنا إذا أصابت شيئًا ما، وسنحذف أيّ شيء نشير إليه في مجموعة المتوحشين "mobs"، والذي تصيبه الرصاصة.
</p>

<h3 id="-2">
	إطلاق النار
</h3>

<p>
	يجب إعداد موقع ظهور الرصاصات، لذا من المهم إضافة العقدة <code>Marker2D</code> ووضعها في المكان الذي نريد ظهور الرصاصات فيه، حيث وضعناه عند فوهة البندقية مثلًا، وسميناه بالفوهة "Muzzle":
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172065" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_2d_shoot_01.gif.7b4bc58fba19cbba99c25b724e952945.gif" rel=""><img alt="02 2d shoot 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172065" data-unique="7yjt1vhpv" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_2d_shoot_01.gif.7b4bc58fba19cbba99c25b724e952945.gif"> </a>
</p>

<p>
	نلاحظ بقاء التحويل <code>transform</code> الخاص بالفوهة موجَّهًا مع اتجاه البندقية نفسه عند دوران اللاعب، وسيكون ذلك مناسبًا عند ظهور الرصاصات، حيث يمكن استخدام هذا التحويل للحصول على الموضع والاتجاه المناسبين. سلنضبط الآن التحويل <code>transform</code> الجديد الخاص بالرصاصة ليكون مساويًا لتحويل الفوهة.
</p>

<p>
	<strong>ملاحظة</strong>: ستنجح هذه الطريقة مع أيّ نوع من الشخصيات، وليس فقط مع أسلوب الدوران والتحريك الموضّح في هذا المقال، فما علينا سوى إرفاق العقدة <code>Marker2D</code> حيث نريد ظهور الرصاصات.
</p>

<p>
	سنضيف الآن متغيرًا في سكربت الشخصية للاحتفاظ بمشهد الرصاصة لإنشاء نسخة منه كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_2739_12" style=""><span class="lit">@export</span><span class="pln"> var </span><span class="typ">Bullet</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="typ">PackedScene</span></pre>

<p>
	وتحقق أيضًا من إجراء الإدخال المُعرَّف كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_2739_14" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_just_pressed</span><span class="pun">(</span><span class="str">"shoot"</span><span class="pun">):</span><span class="pln">
        shoot</span><span class="pun">()</span></pre>

<p>
	يمكننا الآن إنشاء نسخة من الرصاصة وإضافتها إلى الشجرة في الدالة <code>shoot()‎</code>، ولكن تُعَد إضافة الرصاصة كابن للاعب من الأخطاء الشائعة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_2739_16" style=""><span class="pln">func shoot</span><span class="pun">():</span><span class="pln">
    var b </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Bullet</span><span class="pun">.</span><span class="pln">instantiate</span><span class="pun">()</span><span class="pln">
    add_child</span><span class="pun">(</span><span class="pln">b</span><span class="pun">)</span><span class="pln">
    b</span><span class="pun">.</span><span class="pln">transform </span><span class="pun">=</span><span class="pln"> $Muzzle</span><span class="pun">.</span><span class="pln">transform</span></pre>

<p>
	تكمن المشكلة في تأثّر الرصاصات عندما يتحرك اللاعب أو يدور لأنها أبناء للاعب.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172062" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_2d_shoot_02.gif.858bb79be6ab33237e3a1fda4ce7a722.gif" rel=""><img alt="03 2d shoot 02" class="ipsImage ipsImage_thumbnailed" data-fileid="172062" data-unique="iiminy33p" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_2d_shoot_02.gif.858bb79be6ab33237e3a1fda4ce7a722.gif"> </a>
</p>

<p>
	يمكن إصلاح هذه المشكلة من خلال التأكد من إضافة الرصاصات إلى المستوى العالمي بدلًا من ذلك، حيث سنستخدم <code>owner</code> في هذه الحالة، والذي يشير إلى العقدة الجذر للمشهد الذي يتواجد فيه اللاعب.
</p>

<p>
	وكما نلاحظ، سنحتاج أيضًا إلى استخدام التحويل العام الخاص بالفوهة، وإلّا لن تكون الرصاصة موجودةً في المكان الذي نتوقعه.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172061" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_2d_shoot_03.gif.0196832a28a6798387d3a8ed957243a6.gif" rel=""><img alt="04 2d shoot 03" class="ipsImage ipsImage_thumbnailed" data-fileid="172061" data-unique="nuxpace6v" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_2d_shoot_03.gif.0196832a28a6798387d3a8ed957243a6.gif"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/2d_shooting" rel="external nofollow">Github</a>.
</p>

<h2 id="y">
	ترتيب عرض الكائنات بناء على موقعها على محور Y
</h2>

<p>
	تستخدم العديد من الألعاب ثنائية الأبعاد منظور 3/4، مما يعطي الانطباع بأن الكاميرا تنظر إلى العالم من زاوية معينة، لذا يجب عرض الكائنات الأبعد خلف الكائنات الأقرب، ويعني ذلك عمليًا أننا نريد ترتيب عرضها بناءً على موقعها على محور Y، مما يجعل ترتيب الرسوم مرتبطًا بإحداثيات الكائن على المحور Y، وكلما كان الكائن أعلى على الشاشة، سيكون أبعد؛ وبالتالي سيكون أخفض وفق ترتيب التصيير Render؛ وفيما يلي مثال لهذه المشكلة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172064" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_ysort_01.png.f6231ae6274b7135601252c0a3182bc4.png" rel=""><img alt="05 ysort 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172064" data-unique="d3g6xbb3e" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_ysort_01.png.f6231ae6274b7135601252c0a3182bc4.png"> </a>
</p>

<p>
	تُرسَم هذه الكائنات وفقًا لترتيب التصيير الافتراضي، والذي هو ترتيب الشجرة، حيث يكون ترتيبها في شجرة المشهد كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172057" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_ysort_06.png.692e3b515ef7246be7c94de71e81a580.png" rel=""><img alt="06 ysort 06" class="ipsImage ipsImage_thumbnailed" data-fileid="172057" data-unique="i0hpriu58" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_ysort_06.png.692e3b515ef7246be7c94de71e81a580.png"> </a>
</p>

<p>
	يحتوي <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">جودو Godot</a> على خيارٍ مُضمَّن لتغيير ترتيب التصيير، حيث يمكننا تفعيل الخاصية Y Sort Enabled مع أيّ عقدة <code>CanvasItem</code> مثل <code>Node2D</code> أو <code>Control</code>، ثم يمكن فرز جميع العقد الأبناء بناءً على موقعها على محور Y. يمكن تفعيل هذه الخاصية مع عقدة <code>TileMap</code> في المثال السابق، ولكن ستبقى المشكلة موجودةً كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172056" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_ysort_01.png.03eec2249ddc7dff1a16bcd089b47144.png" rel=""><img alt="07 ysort 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172056" data-unique="23xo9cfn6" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_ysort_01.png.03eec2249ddc7dff1a16bcd089b47144.png"> </a>
</p>

<p>
	يعتمد ترتيب الرسم على إحداثيات المحور Y لكل كائن، حيث يكون مركز الكائن افتراضيًا كما هو موضح في الشكل التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172060" href="https://academy.hsoub.com/uploads/monthly_2025_05/08_ysort_04.png.816b587c81b6f862f1889ce51730f18d.png" rel=""><img alt="08 ysort 04" class="ipsImage ipsImage_thumbnailed" data-fileid="172060" data-unique="zdi8hp0bz" src="https://academy.hsoub.com/uploads/monthly_2025_05/08_ysort_04.png.816b587c81b6f862f1889ce51730f18d.png"> </a>
</p>

<p>
	نريد الآن إعطاء انطباع بأن الكائنات موجودة على الأرض، لذا يمكن حل هذه المشكلة من خلال إزاحة الشخصية الرسومية Sprite لكل كائن بحيث يحاذي موضعُ <code>position</code> الكائن أسفلَ الشخصية الرسومية كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172063" href="https://academy.hsoub.com/uploads/monthly_2025_05/09_ysort_05.png.7073ca1b8e41218f19b4c7d77a352544.png" rel=""><img alt="09 ysort 05" class="ipsImage ipsImage_thumbnailed" data-fileid="172063" data-unique="2f4ne7k46" src="https://academy.hsoub.com/uploads/monthly_2025_05/09_ysort_05.png.7073ca1b8e41218f19b4c7d77a352544.png"> </a>
</p>

<p>
	وبهذا تكون الأمور قد أصبحت أفضل بكثير كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172058" href="https://academy.hsoub.com/uploads/monthly_2025_05/10_ysort_02.gif.1328d3faf6628d111a1168cae44c5c15.gif" rel=""><img alt="10 ysort 02" class="ipsImage ipsImage_thumbnailed" data-fileid="172058" data-unique="50qabqefa" src="https://academy.hsoub.com/uploads/monthly_2025_05/10_ysort_02.gif.1328d3faf6628d111a1168cae44c5c15.gif"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/using_ysort" rel="external nofollow">Github</a>.
</p>

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/2d/2d_shooting/index.html" rel="external nofollow">Shooting projectiles</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/using_ysort/index.html" rel="external nofollow">Using Y-Sort</a> من توثيقات Kidscancode.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%88%D8%AD%D8%AF%D8%A9-%D8%AA%D8%AD%D9%83%D9%85-%D9%88%D8%A7%D9%82%D8%B9%D9%8A%D8%A9-%D9%84%D9%84%D8%B3%D9%8A%D8%A7%D8%B1%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2572/" rel="">إنشاء وحدة تحكم واقعية للسيارات في ألعاب الفيديو ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A2%D9%84%D9%8A%D8%A9-%D8%A7%D9%84%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D9%81-%D8%AD%D9%88%D9%84-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2571/" rel="">آلية الالتفاف حول الشاشة وتحريك الشخصيات في الألعاب ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2573</guid><pubDate>Thu, 22 May 2025 16:06:01 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x648;&#x62D;&#x62F;&#x629; &#x62A;&#x62D;&#x643;&#x645; &#x648;&#x627;&#x642;&#x639;&#x64A;&#x629; &#x644;&#x644;&#x633;&#x64A;&#x627;&#x631;&#x627;&#x62A; &#x641;&#x64A; &#x623;&#x644;&#x639;&#x627;&#x628; &#x627;&#x644;&#x641;&#x64A;&#x62F;&#x64A;&#x648; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%88%D8%AD%D8%AF%D8%A9-%D8%AA%D8%AD%D9%83%D9%85-%D9%88%D8%A7%D9%82%D8%B9%D9%8A%D8%A9-%D9%84%D9%84%D8%B3%D9%8A%D8%A7%D8%B1%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2572/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/4.(2).png.71bb76c8c8d931d8b51ab0372dbf6ebb.png" /></p>
<p>
	سنوضّح في هذا المقال كيفية إنشاء وحدة تحكم للسيارات في الألعاب ثنائية الأبعاد من الأعلى إلى الأسفل. قد لا يتمكّن المبتدئون من إنشاء شيءٍ يتعامل مع لعبةٍ مشابهة لسيارة حقيقية، لذا سنذكر بعض الأخطاء الشائعة التي قد تظهر في ألعاب السيارات للهواة:
</p>

<ul>
	<li>
		لا تدور السيارة حول مركزها، إذ لا تنزلق Slide عجلات السيارة الخلفية من جانب إلى آخر إلّا في حالة الانجراف Drifting، وسنتحدث عن ذلك لاحقًا
	</li>
	<li>
		لا يمكن للسيارة أن تستدير إلا عندما تتحرك، إذ لا يمكنها الدوران في مكانها
	</li>
	<li>
		السيارة ليست قطارًا، فهي ليست على قضبان سكة حديدية، لذا يجب أن تتضمن الاستدارة بسرعات عالية بعض الانزلاق أو الانجراف
	</li>
</ul>

<p>
	توجد العديد من الأساليب لفيزياء السيارات ثنائية الأبعاد، وتعتمد بصفة أساسية على مدى الواقعية التي نريد أن تكون عليها، لذا نريد الوصول في هذا المقال إلى مستوى معين من الواقعية.
</p>

<p>
	<strong>ملاحظة</strong>: تعتمد الطريقة التي سنوضّحها فيما يلي على الخوارزمية الموجودة في مقال <a href="http://engineeringdotnet.blogspot.com/2010/04/simple-2d-car-physics-in-games.html" rel="external nofollow">فيزياء توجيه السيارات في الألعاب ثنائية الأبعاد البسيطة</a>.
</p>

<p>
	تُقسَم الطريقة المتبعة فيما يلي إلى 5 أجزاء، حيث يضيف كل جزء منها ميزة مختلفة لحركة السيارة، لذا يمكن المزج بينها وفقًا لاحتياجاتنا.
</p>

<h2 id="">
	إعداد المشهد
</h2>

<p>
	فيما يلي إعداد لمشهد السيارة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_9" style=""><span class="typ">CharacterBody2D</span><span class="pln">
    </span><span class="typ">Sprite2D</span><span class="pln">
    </span><span class="typ">CollisionShape2D</span><span class="pln">
    </span><span class="typ">Camera2D</span></pre>

<p>
	سنضيف أي خامة شخصية رسومية Sprite Texture نريدها، حيث سنستخدم في مثالنا <a href="https://kenney.nl/assets/racing-pack" rel="external nofollow">حزمة السباق من موقع Kenney</a>. تُعَد العقدة <code>CapsuleShape2D</code> خيارًا جيدًا للتصادم، بحيث لا تكون للسيارة زوايا حادة قد تتسبب في تعطلها بسبب العوائق. سنستخدم أيضًا أربعة إجراءات إدخال هي: steer_right و steer_left و accelerate و brake، لذا يمكننا ضبطها على أيّ مفاتيح إدخال نفضلها.
</p>

<h2 id="-1">
	الجزء الأول: الحركة
</h2>

<p>
	تتمثل الخطوة الأولى في برمجة الحركة بناءً على الخوارزمية السابقة، لذا سنبدأ ببعض المتغيرات كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_11" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var wheel_base </span><span class="pun">=</span><span class="pln"> </span><span class="lit">70</span><span class="pln">  </span><span class="com"># المسافة من العجلة الأمامية إلى العجلة الخلفية</span><span class="pln">
var steering_angle </span><span class="pun">=</span><span class="pln"> </span><span class="lit">15</span><span class="pln">  </span><span class="com"># مقدار دوران العجلة الأمامية بالدرجات</span><span class="pln">

var steer_direction</span></pre>

<p>
	نضبط المتغير <code>wheelbase</code> على قيمة تتوافق مع شخصيتنا الرسومية، ويمثّل المتغير <code>steer_direction</code> مقدار دوران العجلات.
</p>

<p>
	<strong>ملاحظة</strong>: نستخدم في مثالنا عناصر تحكم لوحة المفاتيح، لذا سيحدث الدوران أو لن يحدث على الإطلاق، ولكن إذا كنا تستخدم عصا تحكم للعب، فيمكننا بدلًا من ذلك تغيير هذه القيمة بناءً على المسافة التي تتحركها العصا.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_13" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    get_input</span><span class="pun">()</span><span class="pln">
    calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">)</span><span class="pln">
    move_and_slide</span><span class="pun">()</span></pre>

<p>
	يجب التحقق من الإدخال وحساب التوجيه Steering في كل إطار، ثم نمرّر السرعة <code>velocity</code> الناتجة إلى الدالة <code>move_and_slide()‎</code>، ونعرّف الدالتين التاليتين كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_16" style=""><span class="pln">func get_input</span><span class="pun">():</span><span class="pln">
    var turn </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"steer_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"steer_right"</span><span class="pun">)</span><span class="pln">
    steer_direction </span><span class="pun">=</span><span class="pln"> turn </span><span class="pun">*</span><span class="pln"> deg_to_rad</span><span class="pun">(</span><span class="pln">steering_angle</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"accelerate"</span><span class="pun">):</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> </span><span class="lit">500</span></pre>

<p>
	سنتحقق من إدخال المستخدم ونضبط السرعة. وكما نلاحظ فقيمة السرعة 500 مؤقتة حتى نتمكّن من اختبار الحركة، وسنعالجها في الجزء التالي.
</p>

<p>
	سننفّذ بعد ذلك الخوارزمية السابقة كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_18" style=""><span class="pln">func calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="com"># 1. العثور على مواضع العجلات</span><span class="pln">
    var rear_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">-</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    var front_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">+</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    </span><span class="com"># 2. تحريك العجلات للأمام</span><span class="pln">
    rear_wheel </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta
    front_wheel </span><span class="pun">+=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">rotated</span><span class="pun">(</span><span class="pln">steer_direction</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> delta
    </span><span class="com"># 3. العثور على متجه الاتجاه الجديد</span><span class="pln">
    var new_heading </span><span class="pun">=</span><span class="pln"> rear_wheel</span><span class="pun">.</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">front_wheel</span><span class="pun">)</span><span class="pln">
    </span><span class="com"># 4. ضبط السرعة والدوران على الاتجاه الجديد</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> new_heading </span><span class="pun">*</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> new_heading</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span></pre>

<p>
	سنشغّل الآن المشروع ويجب أن تتحرك وتدور السيارة، ولكن لا تزال الحركة غير طبيعية؛ إذ ستبدأ السيارة بالحركة وتتوقف مباشرةً، ويمكن إصلاح ذلك من خلال إضافة التسارع إلى العملية الحسابية.
</p>

<h2 id="acceleration">
	الجزء الثاني: التسارع Acceleration
</h2>

<p>
	سنحتاج إلى متغير إعدادٍ آخر ومتغيرًا لتتبّع التسارع الكلي للسيارة كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_20" style=""><span class="pln">var engine_power </span><span class="pun">=</span><span class="pln"> </span><span class="lit">900</span><span class="pln">  </span><span class="com"># قوة التسارع للأمام</span><span class="pln">

var acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO</span></pre>

<p>
	علينا الآن التعديل شيفرة الإدخال البرمجية لتطبيق التسارع بدلًا من تغيير سرعة <code>velocity</code> السيارة مباشرةً كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_22" style=""><span class="pln">func get_input</span><span class="pun">():</span><span class="pln">
    var turn </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"steer_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"steer_right"</span><span class="pun">)</span><span class="pln">
    steer_direction </span><span class="pun">=</span><span class="pln"> turn </span><span class="pun">*</span><span class="pln"> deg_to_rad</span><span class="pun">(</span><span class="pln">steering_angle</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"accelerate"</span><span class="pun">):</span><span class="pln">
        acceleration </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> engine_power</span></pre>

<p>
	يمكن تطبيق التسارع على السرعة بعد الحصول عليه كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_24" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    get_input</span><span class="pun">()</span><span class="pln">
    calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">+=</span><span class="pln"> acceleration </span><span class="pun">*</span><span class="pln"> delta
    move_and_slide</span><span class="pun">()</span></pre>

<p>
	يجب الآن أن تزيد السيارة من سرعتها تدريجيًا عند تشغليها، ولكن ليس لدينا أيّ طريقة لإبطاء السرعة بعد.
</p>

<h2 id="frictiondrag">
	الجزء الثالث: الاحتكاك Friction/السحب Drag
</h2>

<p>
	تتعرض السيارة لقوتين مختلفتين لإبطاء السرعة هما: الاحتكاك والسحب.
</p>

<p>
	الاحتكاك هو القوة التي تطبقها الأرض، وتكون مرتفعةً جدًا عند القيادة على الرمال، ومنخفضةً جدًا عند القيادة على الجليد، ويتناسب الاحتكاك مع السرعة، فكلما زادت السرعة، زادت هذه القوة؛ أما السحب، فهو القوة الناتجة عن مقاومة الرياح، ويعتمد على المقطع العرضي للسيارة، إذ تتعرض الشاحنة الكبيرة أو الشاحنة الصغيرة لسحب أكبر من سيارة السباق، ويتناسب السحب مع مربع السرعة.
</p>

<p>
	يكون الاحتكاك ملحوظًا عند التحرك ببطء، ولكن يغلب السحب عند السرعات العالية. سنضيف كلتا هاتين القوتين إلى العمليات الحسابية الخاصة بنا. ستمنح قيم هاتين القوتين سيارتنا أقصى سرعة، وهي النقطة التي لا تستطيع فيها قوة المحرّك التغلب على قوة السحب.
</p>

<p>
	سنوضح فيما يلي القيم الابتدائية لهاتين الكميتين:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_26" style=""><span class="pln">var friction </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">55</span><span class="pln">
var drag </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">0.06</span></pre>

<p>
	تعني هذه القيم أن قوة السحب تتغلب على قوة الاحتكاك عند السرعة 600 كما نرى في هذا الرسم البياني:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172051" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_car_graph_friction(1)ar.png.01075e7dc7f0549913e58490e9535211.png" rel=""><img alt="01_car_graph_friction(1) ar.png" class="ipsImage ipsImage_thumbnailed" data-fileid="172051" data-ratio="68.73" data-unique="fepbnlyut" style="width: 600px; height: auto;" width="873" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_car_graph_friction(1)ar.thumb.png.3080fef0391de5ca74d8a7874fd32cd7.png"></a>
</p>

<p>
	<strong>ملاحظة</strong>: يمكن <a href="https://www.desmos.com/calculator/e4ayu3xkip" rel="external nofollow">تعديل هذه القيم</a> لمعرفة كيفية تغيرها.
</p>

<p>
	سنستدعي في الدالة <code>‎_physics_process()‎</code> دالةً لحساب الاحتكاك الحالي ونطبقّه على قوة التسارع كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_28" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    get_input</span><span class="pun">()</span><span class="pln">
    apply_friction</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">)</span><span class="pln">
    calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">+=</span><span class="pln"> acceleration </span><span class="pun">*</span><span class="pln"> delta
    velocity </span><span class="pun">=</span><span class="pln"> move_and_slide</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">)</span><span class="pln">

func apply_friction</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> acceleration </span><span class="pun">==</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO </span><span class="kwd">and</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">50</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    var friction_force </span><span class="pun">=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> friction </span><span class="pun">*</span><span class="pln"> delta
    var drag_force </span><span class="pun">=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> drag </span><span class="pun">*</span><span class="pln"> delta
    acceleration </span><span class="pun">+=</span><span class="pln"> drag_force </span><span class="pun">+</span><span class="pln"> friction_force</span></pre>

<p>
	سنحدد أولًا السرعة الدّنيا، مما يضمن عدم استمرار السيارة في التحرك للأمام بسرعات منخفضة للغاية، لأن الاحتكاك لا يؤدي إلى خفض السرعة إلى الصفر أبدًا؛ ونحسب بعد ذلك القوتين ونضيفهما إلى التسارع الكلي، وستؤثران على السيارة في الاتجاه المعاكس لأن قيمتهما سالبة.
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172052" data-unique="sqvccqbi3">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_car_friction.webm.2a88ce75ab77a9e3de46af9e9effcb79.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172052" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172052&amp;key=eabcc36a3e28cde14fee3f8745161d72" rel="">02_car_friction.webm</a>
	</source></video>
</p>

<h2>
	الجزء الرابع: الرجوع للخلف/الفرامل Brake
</h2>

<p>
	نحتاج إلى متغيرين آخرين للإعدادات:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_30" style=""><span class="pln">var braking </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">450</span><span class="pln">
var max_speed_reverse </span><span class="pun">=</span><span class="pln"> </span><span class="lit">250</span></pre>

<p>
	سنضيف الآن الدخل إلى الدالة <code>get_input()‎</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_34" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"brake"</span><span class="pun">):</span><span class="pln">
        acceleration </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> braking</span></pre>

<p>
	يُعَد ذلك جيدًا للتوقف، ولكننا نريد أيضًا أن نتمكن من وضع السيارة في وضع الرجوع للخلف، ولكن ذلك لن ينجح حاليًا، لأن التسارع يُطبَّق في اتجاه التوجّه دائمًا الذي هو إلى الأمام، لذا نحتاج إلى تسارع عكسي عند الرجوع للخلف.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_32" style=""><span class="pln">func calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var rear_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">-</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    var front_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">+</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    rear_wheel </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta
    front_wheel </span><span class="pun">+=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">rotated</span><span class="pun">(</span><span class="pln">steer_angle</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> delta
    var new_heading </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">front_wheel </span><span class="pun">-</span><span class="pln"> rear_wheel</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln">
    var d </span><span class="pun">=</span><span class="pln"> new_heading</span><span class="pun">.</span><span class="pln">dot</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">normalized</span><span class="pun">())</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> d </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> new_heading </span><span class="pun">*</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> d </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="pln">new_heading </span><span class="pun">*</span><span class="pln"> min</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">(),</span><span class="pln"> max_speed_reverse</span><span class="pun">)</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> new_heading</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span></pre>

<p>
	يمكننا معرفة ما إذا كنا نتسارع للأمام أم للخلف باستخدام حاصل الضرب النقطي Dot Product، حيث إذا كان المتجهان متحاذيين، فستكون النتيجة أكبر من 0، وإذا كانت الحركة في الاتجاه المعاكس لاتجاه السيارة، فسيكون حاصل الضرب النقطي أقل من 0 ويجب أن نتحرك للخلف.
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172053" data-unique="8h34usbeu">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_car_reverse.webm.59a5ce142ed1d779f32eae0bc90f9fa6.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172053" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172053&amp;key=0b2abd9400fbb2eb5e1e737b5f25fd54" rel="">03_car_reverse.webm</a>
	</source></video>
</p>

<h2 id="driftslide">
	الجزء الخامس: الانجراف Drift والانزلاق Slide
</h2>

<p>
	يمكن التوقف عند هذه النقطة وسنحظى بتجربة قيادة جيدة، ولكن ستبقى السيارة أشبه بأنها تسير على قضبان سكة حديدية، إذ تكون المنعطفات مثالية حتى عند السرعة القصوى، وكأنّ لإطارات السيارة تحكّم مثالي.
</p>

<p>
	لحل المشكلة، يجب أن تتسبب قوة الانعطاف عند السرعات العالية أو حتى المنخفضة إذا رغبنا في ذلك، في انزلاق الإطارات مما يؤدي إلى حركة انزلاقية، وسنفعل ذلك الآتي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_36" style=""><span class="pln">var slip_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">400</span><span class="pln">  </span><span class="com"># السرعة عند تقليل الشدّ‫ Traction</span><span class="pln">
var traction_fast </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2.5</span><span class="pln"> </span><span class="com"># الشد عالي السرعة</span><span class="pln">
var traction_slow </span><span class="pun">=</span><span class="pln"> </span><span class="lit">10</span><span class="pln">  </span><span class="com"># الشد منخفض السرعة</span></pre>

<p>
	سنطبّق هذه القيم عند حساب التوجيه، إذ تُضبَط السرعة مباشرةً على الاتجاه الجديد حاليًا، ولكننا سنستخدم بدلًا من ذلك الاستيفاء باستخدام الدالة <code>lerp()‎</code> لجعلها تدور جزئيًا نحو الاتجاه الجديد فقط، وستحدّد قيم الشدّ Traction مدى تماسك الإطارات.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1419_38" style=""><span class="pln">func calculate_steering</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var rear_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">-</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    var front_wheel </span><span class="pun">=</span><span class="pln"> position </span><span class="pun">+</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> wheel_base </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2.0</span><span class="pln">
    rear_wheel </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta
    front_wheel </span><span class="pun">+=</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">rotated</span><span class="pun">(</span><span class="pln">steer_angle</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> delta
    var new_heading </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">front_wheel </span><span class="pun">-</span><span class="pln"> rear_wheel</span><span class="pun">).</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln">
    </span><span class="com"># اختر قيمة الشد التي تريد استخدامها، ويجب أن يكون الانزلاق منخفضًا عند السرعات المنخفضة</span><span class="pln">
    var traction </span><span class="pun">=</span><span class="pln"> traction_slow
    </span><span class="kwd">if</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&gt;</span><span class="pln"> slip_speed</span><span class="pun">:</span><span class="pln">
        traction </span><span class="pun">=</span><span class="pln"> traction_fast
    var d </span><span class="pun">=</span><span class="pln"> new_heading</span><span class="pun">.</span><span class="pln">dot</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">normalized</span><span class="pun">())</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> d </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">,</span><span class="pln"> new_heading </span><span class="pun">*</span><span class="pln"> velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">(),</span><span class="pln"> traction </span><span class="pun">*</span><span class="pln"> delta</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> d </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="pln">new_heading </span><span class="pun">*</span><span class="pln"> min</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">length</span><span class="pun">(),</span><span class="pln"> max_speed_reverse</span><span class="pun">)</span><span class="pln">
    rotation </span><span class="pun">=</span><span class="pln"> new_heading</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span></pre>

<p>
	نختار قيمة الشد التي نريد استخدامها ونطبق الدالة <code>lerp()‎</code> على السرعة <code>velocity</code>.
</p>

<p style="text-align: center;">
	<video class="ipsEmbeddedVideo" controls="" data-controller="core.global.core.embeddedvideo" data-fileid="172054" data-unique="ni3tvg9bx">
		<source type="video/webm" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_car_drift.webm.73c33a1d5cdd0f3f9d4b44dfcb0815d8.webm"><a class="ipsAttachLink" data-fileext="webm" data-fileid="172054" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=172054&amp;key=85e2f8e15c7d7c3d77f4d9e0bf498268" rel="">04_car_drift.webm</a>
	</source></video>
</p>

<h2 id="adjustments">
	التعديلات Adjustments
</h2>

<p>
	لدينا في هذه المرحلة عدد كبير من الإعدادات التي تتحكّم في سلوك السيارة، ويمكن أن يؤدي تعديلها إلى تغيير جذري في كيفية قيادة السيارة. سننزّل المشروع الخاص بهذا المقال لتسهيل تجربة قيم مختلفة.
</p>

<p>
	سنرى عند تشغيل اللعبة مجموعةً من أشرطة التمرير التي يمكن استخدامها لتغيير سلوك السيارة أثناء القيادة، ويمكن الضغط على <code>&lt;Tab&gt;</code> لإظهار أو إخفاء لوحة أشرطة التمرير.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172055" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_car_sliders.png.bf498afb3c5c3d9df5372fa688f31fa9.png" rel=""><img alt="05 car sliders" class="ipsImage ipsImage_thumbnailed" data-fileid="172055" data-unique="04w35ctei" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_car_sliders.png.bf498afb3c5c3d9df5372fa688f31fa9.png"> </a>
</p>

<h2 id="-2">
	الخاتمة
</h2>

<p>
	بهذا نكون قد وصلنا لنهاية مقالنا الذي استعرضنا فيه خطوات إنشاء وحدة تحكم لسيارات الألعاب ثنائية الأبعاد بأسلوب واقعي بدءًا من الحركة الأساسية، مرورًا بالتسارع والفرملة، وصولًا إلى الانجراف والانزلاق. يمكن اعتبار هذا المقال بمثابة أساس لتطوير تجربة قيادة أكثر ديناميكية وواقعية في الألعاب.
</p>

<p>
	وللتذكير، يمكن تنزيل شيفرة المشروع البرمجية كاملة من <a href="https://github.com/godotrecipes/2d_car_steering" rel="external nofollow">Github</a>.
</p>

<p>
	ترجمة -وبتصرّف- للقسم <a href="https://kidscancode.org/godot_recipes/4.x/2d/car_steering/index.html" rel="external nofollow">Car steering</a> من توثيقات Kidscancode.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A2%D9%84%D9%8A%D8%A9-%D8%A7%D9%84%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D9%81-%D8%AD%D9%88%D9%84-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2571/" rel="">آلية الالتفاف حول الشاشة وتحريك الشخصيات في الألعاب ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AF%D9%84%D9%8A%D9%84%D9%83-%D8%A7%D9%84%D8%B4%D8%A7%D9%85%D9%84-%D8%A5%D9%84%D9%89-%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2304/" rel="">دليلك الشامل إلى برمجة الألعاب</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%AA%D8%B7%D9%88%D8%B1%D9%87%D8%A7-%D9%88%D8%A3%D9%87%D9%85%D9%8A%D8%AA%D9%87%D8%A7-%D9%88%D8%AE%D8%B7%D9%88%D8%A7%D8%AA-%D8%A8%D8%B1%D9%85%D8%AC%D8%AA%D9%87%D8%A7-r2290/" rel="">ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2572</guid><pubDate>Mon, 19 May 2025 16:02:02 +0000</pubDate></item><item><title>&#x622;&#x644;&#x64A;&#x629; &#x627;&#x644;&#x627;&#x644;&#x62A;&#x641;&#x627;&#x641; &#x62D;&#x648;&#x644; &#x627;&#x644;&#x634;&#x627;&#x634;&#x629; &#x648;&#x62A;&#x62D;&#x631;&#x64A;&#x643; &#x627;&#x644;&#x634;&#x62E;&#x635;&#x64A;&#x627;&#x62A; &#x641;&#x64A; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A2%D9%84%D9%8A%D8%A9-%D8%A7%D9%84%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D9%81-%D8%AD%D9%88%D9%84-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D9%81%D9%8A-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2571/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/3.(2).png.7a52bb05e124a9d1f820063eee9e1294.png" /></p>
<p>
	سنوضّح في هذا المقال كيفية الالتفاف حول الشاشة وبرمجة الحركة من الأعلى إلى الأسفل وتحريك الشخصيات بالاعتماد على الشبكة Grid وفي ثمانية اتجاهات مختلفة في الألعاب ثنائية الأبعاد.
</p>

<h2 id="">
	آلية الالتفاف حول الشاشة
</h2>

<p>
	يُعَد السماح للاعب بالالتفاف حول الشاشة والانتقال الفوري من أحد جانبي الشاشة إلى الجانب الآخر ميزةً شائعة، وخاصة في الألعاب ثنائية الأبعاد القديمة مثل لعبة باك مان Pac-man، إذ يمكن السماح للاعب بالالتفاف حول الشاشة من خلال اتباع الخطوات.
</p>

<p>
	الخطوة الأولى هي بالحصول على حجم الشاشة أو نافذة العرض Viewport كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_8" style=""><span class="lit">@onready</span><span class="pln"> var screen_size </span><span class="pun">=</span><span class="pln"> get_viewport_rect</span><span class="pun">().</span><span class="pln">size</span></pre>

<p>
	حيث تتوفر الدالة <code>get_viewport_rect()‎</code> لأي عقدة مشتقة من <code>CanvasItem</code>.
</p>

<p>
	أما ثاني خطوة، فتتمثل في مقارنة موضع اللاعب كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_10" style=""><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&gt;</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">:</span><span class="pln">
    position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
</span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
    position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">x
</span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">&gt;</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">:</span><span class="pln">
    position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
</span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
    position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">y</span></pre>

<p>
	وكما نلاحظ، تم استخدام موضع <code>position</code> العقدة الذي يكون عادةً مركزًا للشخصية الرسومية Sprite و/أو مركز الجسم.
</p>

<p>
	ثالث خطوة هي عبر تبسيط ما سبق باستخدام الدالة <code>wrapf()‎</code>، إذ يمكن تبسيط الشيفرة البرمجية السابقة باستخدام الدالة <code>wrapf()‎</code> في <a href="https://academy.hsoub.com/programming/game-development/%D9%83%D8%AA%D8%A7%D8%A8%D8%A9-%D8%B3%D9%83%D8%B1%D8%A8%D8%AA%D8%A7%D8%AA-gdscript-%D9%88%D8%A5%D8%B1%D9%81%D8%A7%D9%82%D9%87%D8%A7-%D8%A8%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2495/" rel="">لغة GDScript</a>، والتي تكرّر القيمة بين الحدود المحدّدة.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_12" style=""><span class="pln">position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> wrapf</span><span class="pun">(</span><span class="pln">position</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">x</span><span class="pun">)</span><span class="pln">
position</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> wrapf</span><span class="pun">(</span><span class="pln">position</span><span class="pun">.</span><span class="pln">y</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> screen_size</span><span class="pun">.</span><span class="pln">y</span><span class="pun">)</span></pre>

<h2 id="-1">
	برمجة الحركة من الأعلى إلى الأسفل
</h2>

<p>
	إذا أردنا إنشاء لعبة ثنائية الأبعاد من الأعلى إلى الأسفل، فيجب أن تتحكم في حركة الشخصية، لذا لنفترض تحديد إجراءات الإدخال التالية:
</p>

<table>
	<thead>
		<tr>
			<th>
				اسم الإجراء
			</th>
			<th>
				المفتاح أو مجموعة المفاتيح
			</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>
				<code>"up"</code>
			</td>
			<td>
				المفتاح W أو ↑
			</td>
		</tr>
		<tr>
			<td>
				<code>"down"</code>
			</td>
			<td>
				المفتاح S أو ↓
			</td>
		</tr>
		<tr>
			<td>
				<code>"right"</code>
			</td>
			<td>
				المفتاح D أو →
			</td>
		</tr>
		<tr>
			<td>
				<code>"left"</code>
			</td>
			<td>
				المفتاح A أو ←
			</td>
		</tr>
		<tr>
			<td>
				<code>"click"</code>
			</td>
			<td>
				زر الفأرة 1
			</td>
		</tr>
	</tbody>
</table>

<p>
	سنفترض أيضًا أننا نستخدم عقدة <code>CharacterBody2D</code>. هنا سيكون بإمكاننا أيضًا التحكم في حركة الشخصية باستخدام طرق متعددة اعتمادًا على نوع السلوك الذي تبحث عنه كما سنوضح فيما يلي.
</p>

<h3 id="8">
	الخيار الأول: الحركة في 8 اتجاهات
</h3>

<p>
	يستخدم اللاعب في هذه الحالة مفاتيح الاتجاهات الأربعة للتحرك (بما في ذلك الاتجاهات القطرية) كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_16" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">400</span><span class="pln">  </span><span class="com"># السرعة بالبكسلات في الثانية</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var direction </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_vector</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"down"</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> direction </span><span class="pun">*</span><span class="pln"> speed

    move_and_slide</span><span class="pun">()</span></pre>

<h3 id="-2">
	الخيار الثاني: التدوير والتحريك
</h3>

<p>
	تدوّر الإجراءات لليسار/لليمين "left/right" في هذه الحالة الشخصية وتحرّك الإجراءات للأعلى/للأسفل الشخصية للأمام وللخلف في أيّ اتجاه تواجهه، ويُشار إلى ذلك أحيانًا باسم "الحركة التي تحاكي حركة الكويكبات".
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_18" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">400</span><span class="pln">  </span><span class="com"># سرعة الحركة بالبكسل/ثانية</span><span class="pln">
var rotation_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1.5</span><span class="pln">  </span><span class="com"># سرعة الدوران بالراديان/ثانية</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var move_input </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"down"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">)</span><span class="pln">
    var rotation_direction </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> move_input </span><span class="pun">*</span><span class="pln"> speed
    rotation </span><span class="pun">+=</span><span class="pln"> rotation_direction </span><span class="pun">*</span><span class="pln"> rotation_speed </span><span class="pun">*</span><span class="pln"> delta
    move_and_slide</span><span class="pun">()</span></pre>

<p>
	<strong>ملاحظة</strong>: يَعُد <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">محرّك ألعاب جودو Godot</a> أن الزاوية 0 درجة تؤشّر على طول المحور x، مما يعني أن اتجاه العقدة للأمام (<code>transform.x</code>) يتجه إلى اليمين، لذا يجب التأكد من رسم الشخصية الرسومية لتشير إلى اليمين.
</p>

<h3 id="-3">
	الخيار الثالث: التصويب بالفأرة
</h3>

<p>
	يُعَد هذا الخيار مشابهًا للخيار الثاني، ولكننا نتحكم في دوران الشخصية باستخدام الفأرة هذه المرة، أي أنّ الشخصية تشير دائمًا إلى الفأرة، وتُطبَّق الحركة للأمام/للخلف باستخدام المفاتيح كما في السابق.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_20" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">400</span><span class="pln">  </span><span class="com"># سرعة الحركة بالبكسل/ثانية</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    look_at</span><span class="pun">(</span><span class="pln">get_global_mouse_position</span><span class="pun">())</span><span class="pln">
    var move_input </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"down"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">)</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> move_input </span><span class="pun">*</span><span class="pln"> speed
    move_and_slide</span><span class="pun">()</span></pre>

<h3 id="-4">
	الخيار الرابع: النقر والتحريك
</h3>

<p>
	تنتقل الشخصية في هذا الخيار إلى الموقع الذي نقرنا عليه.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_22" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">400</span><span class="pln">  </span><span class="com"># سرعة الحركة بالبكسل/ثانية</span><span class="pln">
var target </span><span class="pun">=</span><span class="pln"> null

func _input</span><span class="pun">(</span><span class="pln">event</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"click"</span><span class="pun">):</span><span class="pln">
        target </span><span class="pun">=</span><span class="pln"> get_global_mouse_position</span><span class="pun">()</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> target</span><span class="pun">:</span><span class="pln">
        </span><span class="com"># look_at(target)</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">direction_to</span><span class="pun">(</span><span class="pln">target</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed
        </span><span class="kwd">if</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">distance_to</span><span class="pun">(</span><span class="pln">target</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&lt;</span><span class="pln"> </span><span class="lit">10</span><span class="pun">:</span><span class="pln">
            velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
    move_and_slide</span><span class="pun">()</span></pre>

<p>
	وكما نلاحظ، سنتوقف عن الحركة إذا اقتربنا من موضع الهدف، فإن لم نفعل ذلك، فستهتز الشخصية ذهابًا وإيابًا بحيث تتحرك قليلًا بعد الهدف وتعود، ثم تعاود الكرّة وهكذا. يمكننا اختياريًا استخدام الدالة <code>look_at()‎</code> لتكون مواجهةً لاتجاه الحركة.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/topdown_movement" rel="external nofollow">Github</a>.
</p>

<h2 id="grid">
	تحريك الشخصيات بالاعتماد على الشبكة Grid
</h2>

<p>
	سنوضّح فيما يلي كيفية تحريك شخصية ثنائية الأبعاد في نمط شبكي؛ إذ تعني الحركة المعتمدة على الشبكة Grid أو المربع Tile أن موضع الشخصية مقيد، ولا يمكنها الوقوف إلا على مربع معين، كما لا يمكنها أن تكون بين مربعين أبدًا.
</p>

<h3 id="-5">
	إعداد الشخصية
</h3>

<p>
	سنوضح فيما يلي العقد التي سنستخدمها للاعب:
</p>

<ul>
	<li>
		<code>Area2D</code> (اللاعب "Player"): يمثّل استخدام العقدة <code>Area2D</code> أنه يمكننا اكتشاف التداخل لالتقاط الأشياء أو الاصطدام بالأعداء
	</li>
	<li>
		<code>Sprite2D</code>: يمكن استخدام ورقة <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-spritesheet-%D9%88-animationtree-statemachine-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2529/" rel="">الشخصية الرسومية Sprite Sheet</a> هنا، وسنوضّح إعداد الرسوم المتحركة Animations لاحقًا
	</li>
	<li>
		<code>CollisionShape2D</code>: يجب أن لا نجعل مربع الاصطدام كبيرًا جدًا، حيث ستكون التداخلات من عند المركز لأن اللاعب سيقف في منتصف المربع
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">RayCast2D</a>: للتحقق مما إذا كانت الحركة ممكنة في اتجاه محدّد
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-spritesheet-%D9%88-animationtree-statemachine-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2529/" rel="">AnimationPlayer</a>: لتشغيل الرسوم المتحركة الخاصة بمشي الشخصية
	</li>
</ul>

<p>
	سنضيف هنا بعض إجراءات الإدخال إلى خريطة الإدخال Input Map، ويمكننا لأجل ذلك استخدام إجراءات up وdown وleft، و right في هذا المثال.
</p>

<h3 id="-6">
	الحركة الأساسية
</h3>

<p>
	سنبدأ بإعداد الحركة على المربعات الواحد تلو الآخر دون أي رسوم متحركة أو استيفاء Interpolation.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_24" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

var tile_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">64</span><span class="pln">
var inputs </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span><span class="str">"right"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">RIGHT</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"left"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">LEFT</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"up"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">UP</span><span class="pun">,</span><span class="pln">
    </span><span class="str">"down"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">DOWN</span><span class="pun">}</span></pre>

<p>
	يجب ضبط حجم المربعات <code>tile_size</code> ليتطابق مع حجم المربعات الخاصة بنا، ويمكن ضبطه في مشروع أكبر باستخدام المشهد الرئيسي عند إنشاء نسخة من اللاعب، ولكن سنستخدم مربعات بحجم 64x64 في مثالنا. يربط القاموس <code>inputs</code> أسماء إجراءات الإدخال مع متجهات الاتجاه، لذا يجب التأكّد من كتابة الأسماء نفسها هنا وفي خريطة الإدخال مع مراعاة استخدام الحروف الكبيرة.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_26" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    position </span><span class="pun">=</span><span class="pln"> position</span><span class="pun">.</span><span class="pln">snapped</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE </span><span class="pun">*</span><span class="pln"> tile_size</span><span class="pun">)</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ONE </span><span class="pun">*</span><span class="pln"> tile_size</span><span class="pun">/</span><span class="lit">2</span></pre>

<p>
	تتيح الدالة <code>snapped()‎</code> تقريب الموضع إلى أقرب زيادة في المربع، وتضمَن إضافة كمية بمقدار نصف مربع أن يكون اللاعب في مركز المربع.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_28" style=""><span class="pln">func _unhandled_input</span><span class="pun">(</span><span class="pln">event</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> dir </span><span class="kwd">in</span><span class="pln"> inputs</span><span class="pun">.</span><span class="pln">keys</span><span class="pun">():</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">):</span><span class="pln">
            move</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">)</span><span class="pln">

func move</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">):</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> inputs</span><span class="pun">[</span><span class="pln">dir</span><span class="pun">]</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> tile_size</span></pre>

<p>
	تمثل الشيفرة البرمجية السابقة الحركة الفعلية. حيث إذا ظهر حدث إدخال، فسنتحقق من الاتجاهات الأربعة لمعرفة أيّ منها يتطابق مع الحدث، ثم نمرّره إلى الدالة <code>move()‎</code> لتغيير الموضع.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172046" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_grid_example1.gif.9d652f0f590d828bfd9e141207b86a9d.gif" rel=""><img alt="01 grid example1" class="ipsImage ipsImage_thumbnailed" data-fileid="172046" data-unique="j7r2vq2x2" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_grid_example1.gif.9d652f0f590d828bfd9e141207b86a9d.gif"> </a>
</p>

<h3 id="collision">
	الاصطدام Collision
</h3>

<p>
	يمكننا الآن إضافة بعض العوائق، حيث يمكن استخدام عقد <code>StaticBody2D</code> لإضافة بعض العوائق يدويًا، مع تفعيل الالتقاط للتأكد من محاذاتها مع الشبكة، أو استخدام TileMap مع تحديد الاصطدامات كما هو الحال في المثال التالي، وسنستخدم <a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">عقدة RayCast2D</a> لتحديد السماح بالانتقال إلى المربع التالي.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_30" style=""><span class="pln">onready var ray </span><span class="pun">=</span><span class="pln"> $RayCast2D

func move</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">):</span><span class="pln">
    ray</span><span class="pun">.</span><span class="pln">target_position </span><span class="pun">=</span><span class="pln"> inputs</span><span class="pun">[</span><span class="pln">dir</span><span class="pun">]</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> tile_size
    ray</span><span class="pun">.</span><span class="pln">force_raycast_update</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">!</span><span class="pln">ray</span><span class="pun">.</span><span class="pln">is_colliding</span><span class="pun">():</span><span class="pln">
        position </span><span class="pun">+=</span><span class="pln"> inputs</span><span class="pun">[</span><span class="pln">dir</span><span class="pun">]</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> tile_size</span></pre>

<p>
	إذا غيّرنا الخاصية <code>target_position</code> الخاصة بعقدة <code>RayCast2D</code>، فلن يعيد محرّك الفيزياء حساب تصادماته حتى الإطار الفيزيائي التالي. تتيح الدالة <code>force_raycast_update()‎</code> إمكانية تحديث حالة الشعاع مباشرةً، حيث إن لم يكن هناك تصادم، فسنسمح بالتحرك.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172047" href="https://academy.hsoub.com/uploads/monthly_2025_05/02_grid_example2.gif.616ee02897aaab1f9f0a7b3c4c820e1b.gif" rel=""><img alt="02 grid example2" class="ipsImage ipsImage_thumbnailed" data-fileid="172047" data-unique="2hotblh6q" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_grid_example2.gif.616ee02897aaab1f9f0a7b3c4c820e1b.gif"> </a>
</p>

<p>
	<strong>ملاحظة</strong>: تتمثل إحدى الطرق الشائعة الأخرى في استخدام أربع عقد <code>RayCast2D</code> من خلال استخدام عقدة لكل اتجاه.
</p>

<h3 id="-7">
	تنشيط الحركة
</h3>

<p>
	وأخيرًا، يمكننا استيفاء الموضع بين المربعات، مما يعطي إحساسًا سلسًا بالحركة، حيث سنستخدم عقدة <code>Tween</code> لتحريك خاصية الموضع <code>position</code>.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_32" style=""><span class="pln">var animation_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">3</span><span class="pln">
var moving </span><span class="pun">=</span><span class="pln"> false</span></pre>

<p>
	سنضيف الآن مرجعًا إلى عقدة <code>Tween</code> ومتغيرًا لضبط سرعة الحركة كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_34" style=""><span class="pln">func _unhandled_input</span><span class="pun">(</span><span class="pln">event</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> moving</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">return</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> dir </span><span class="kwd">in</span><span class="pln"> inputs</span><span class="pun">.</span><span class="pln">keys</span><span class="pun">():</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">):</span><span class="pln">
            move</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">)</span></pre>

<p>
	سنتجاهل أي إدخال أثناء تشغيل الانتقال التدريجي Tween ونزيل تغيير الموضع <code>position</code> المباشر حتى يتمكّن الانتقال التدريجي من التعامل معه.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_36" style=""><span class="pln">func move</span><span class="pun">(</span><span class="pln">dir</span><span class="pun">):</span><span class="pln">
    ray</span><span class="pun">.</span><span class="pln">target_position </span><span class="pun">=</span><span class="pln"> inputs</span><span class="pun">[</span><span class="pln">dir</span><span class="pun">]</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> tile_size
    ray</span><span class="pun">.</span><span class="pln">force_raycast_update</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">!</span><span class="pln">ray</span><span class="pun">.</span><span class="pln">is_colliding</span><span class="pun">():</span><span class="pln">
        </span><span class="com">#position += inputs[dir] * tile_size</span><span class="pln">
        var tween </span><span class="pun">=</span><span class="pln"> create_tween</span><span class="pun">()</span><span class="pln">
        tween</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">self</span><span class="pun">,</span><span class="pln"> </span><span class="str">"position"</span><span class="pun">,</span><span class="pln">
            position </span><span class="pun">+</span><span class="pln"> inputs</span><span class="pun">[</span><span class="pln">dir</span><span class="pun">]</span><span class="pln"> </span><span class="pun">*</span><span class="pln">    tile_size</span><span class="pun">,</span><span class="pln"> </span><span class="lit">1.0</span><span class="pun">/</span><span class="pln">animation_speed</span><span class="pun">).</span><span class="pln">set_trans</span><span class="pun">(</span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TRANS_SINE</span><span class="pun">)</span><span class="pln">
        moving </span><span class="pun">=</span><span class="pln"> true
        await tween</span><span class="pun">.</span><span class="pln">finished
        moving </span><span class="pun">=</span><span class="pln"> false</span></pre>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172049" href="https://academy.hsoub.com/uploads/monthly_2025_05/03_grid_example3.gif.6f4a04ea39f1f3a2d58a39af84a5ad0c.gif" rel=""><img alt="03 grid example3" class="ipsImage ipsImage_thumbnailed" data-fileid="172049" data-unique="2m551w4hz" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_grid_example3.gif.6f4a04ea39f1f3a2d58a39af84a5ad0c.gif"> </a>
</p>

<p>
	سنجرِّب الآن انتقالات تدريجية مختلفة للحصول على تأثيرات حركية مختلفة.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/2d_grid_movement/" rel="external nofollow">Github</a>.
</p>

<h2 id="8-1">
	تحريك الشخصيات في 8 اتجاهات مختلفة
</h2>

<p>
	سنوضّح فيما يلي كيفية تحريك شخصية ثنائية الأبعاد في 8 اتجاهات مختلفة، حيث سنستخدم في مثالنا <a href="https://remos.itch.io/mini-crusader" rel="external nofollow">شخصية محارب</a> التي تحتوي على رسوم متحركة في 8 اتجاهات لحالات عدم الحركة والجري والهجوم والعديد من الحالات الأخرى.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172044" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_8_direction_01.gif.488099e24e25014093ea834c4e34f534.gif" rel=""><img alt="04 8 direction 01" class="ipsImage ipsImage_thumbnailed" data-fileid="172044" data-unique="nrxtzjuhc" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_8_direction_01.gif.488099e24e25014093ea834c4e34f534.gif"> </a>
</p>

<p>
	تُنظَّم الرسوم المتحركة ضمن مجلدات، مع وجود صورة منفصلة لكل إطار. سنستخدم العقدة <code>AnimatedSprite2D</code> وسنسمّي الرسوم المتحركة بناءً على اتجاهها.
</p>

<p>
	على سبيل المثال، يشير الرسم المتحرك <code>idle0</code> إلى اليمين، ثم ننتقل باتجاه عقارب الساعة حتى الوصول إلى الرسم المتحرك <code>idle7</code>، وتختار الشخصية عند تحرّكها رسومًا متحركة بناءً على اتجاه الحركة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172048" href="https://academy.hsoub.com/uploads/monthly_2025_05/05_8_direction_03w.png.64f12635932622712179ad8abbceb0c5.png" rel=""><img alt="05 8 direction 03w" class="ipsImage ipsImage_thumbnailed" data-fileid="172048" data-unique="9wmhnq7ar" src="https://academy.hsoub.com/uploads/monthly_2025_05/05_8_direction_03w.png.64f12635932622712179ad8abbceb0c5.png"> </a>
</p>

<p>
	سنستخدم الفأرة للتحرك، حيث ستواجه الشخصية الفأرة دائمًا وتتحرك في هذا الاتجاه عندما نضغط على زر الفأرة. يمكن اختيار الرسوم المتحركة التي ستعمل من خلال الحصول على اتجاه الفأرة وربطه مع هذا المجال نفسه من 0 إلى 7؛ إذ تعطي الدالة <code>get_local_mouse_position()‎</code> موضع الفأرة بالنسبة للشخصية، ويمكننا بعد ذلك استخدام الدالة <code>snappedf()‎</code> لضبط زاوية متجه الفأرة إلى أقرب مضاعف للزاوية 45 درجة (أو <code>PI/4</code> راديان) مما يعطي النتيجة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172045" href="https://academy.hsoub.com/uploads/monthly_2025_05/06_8_direction_04w.png.82f0d238da28068b5ca1dd561a917991.png" rel=""><img alt="06 8 direction 04w" class="ipsImage ipsImage_thumbnailed" data-fileid="172045" data-unique="mbnw3epe4" src="https://academy.hsoub.com/uploads/monthly_2025_05/06_8_direction_04w.png.82f0d238da28068b5ca1dd561a917991.png"> </a>
</p>

<p>
	سنقسّم كل قيمة على 45 درجة ( أو <code>PI/4</code> راديان) ونحصل على النتيجة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="172050" href="https://academy.hsoub.com/uploads/monthly_2025_05/07_8_direction_02w.png.1d52d031325e62326bce16887fca5995.png" rel=""><img alt="07 8 direction 02w" class="ipsImage ipsImage_thumbnailed" data-fileid="172050" data-unique="xzrvw5d1m" src="https://academy.hsoub.com/uploads/monthly_2025_05/07_8_direction_02w.png.1d52d031325e62326bce16887fca5995.png"> </a>
</p>

<p>
	في الأخير، يجب ربط المجال الناتج مع المجال ‎0-7 باستخدام الدالة <code>wrapi()‎</code>، وسنحصل على القيم الصحيحة، حيث تعطي إضافة هذه القيمة إلى نهاية اسم الرسوم المتحركة ("idle" و "run" وغيرها) الرسم المتحرك الصحيح كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_38" style=""><span class="pln">func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    current_animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"idle"</span><span class="pln">

    var mouse </span><span class="pun">=</span><span class="pln"> get_local_mouse_position</span><span class="pun">()</span><span class="pln">
    angle </span><span class="pun">=</span><span class="pln"> snappedf</span><span class="pun">(</span><span class="pln">mouse</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">(),</span><span class="pln"> PI</span><span class="pun">/</span><span class="lit">4</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> </span><span class="pun">(</span><span class="pln">PI</span><span class="pun">/</span><span class="lit">4</span><span class="pun">)</span><span class="pln">
    angle </span><span class="pun">=</span><span class="pln"> wrapi</span><span class="pun">(</span><span class="pln">int</span><span class="pun">(</span><span class="pln">angle</span><span class="pun">),</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">)</span><span class="pln">

    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"left_mouse"</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> mouse</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">10</span><span class="pun">:</span><span class="pln">
        current_animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"run"</span><span class="pln">
        velocity </span><span class="pun">=</span><span class="pln"> mouse</span><span class="pun">.</span><span class="pln">normalized</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed
        move_and_slide</span><span class="pun">()</span><span class="pln">
    $AnimatedSprite2D</span><span class="pun">.</span><span class="pln">animation </span><span class="pun">=</span><span class="pln"> current_animation </span><span class="pun">+</span><span class="pln"> str</span><span class="pun">(</span><span class="pln">a</span><span class="pun">)</span></pre>

<p>
	وسنشاهد ما يلي عند اختبار الحركة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="172043" href="https://academy.hsoub.com/uploads/monthly_2025_05/08_8_direction_05.gif.85a484ef5a4369f2b30dfe301076e136.gif" rel=""><img alt="08 8 direction 05" class="ipsImage ipsImage_thumbnailed" data-fileid="172043" data-unique="dta7cylbt" src="https://academy.hsoub.com/uploads/monthly_2025_05/08_8_direction_05.gif.85a484ef5a4369f2b30dfe301076e136.gif"> </a>
</p>

<h3 id="-8">
	الإدخال من لوحة المفاتيح
</h3>

<p>
	إذا كنا نستخدم عناصر التحكم من لوحة المفاتيح بدلًا من الفأرة، فيمكننا الحصول على زاوية الحركة بناءً على المفاتيح التي نضغط عليها، وتسير بقية العملية باستخدام الطريقة نفسها كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4535_40" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    current_animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"idle"</span><span class="pln">
    var input_dir </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_vector</span><span class="pun">(</span><span class="str">"left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"down"</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> input_dir</span><span class="pun">.</span><span class="pln">length</span><span class="pun">()</span><span class="pln"> </span><span class="pun">!=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        angle </span><span class="pun">=</span><span class="pln"> input_dir</span><span class="pun">.</span><span class="pln">angle</span><span class="pun">()</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> </span><span class="pun">(</span><span class="pln">PI</span><span class="pun">/</span><span class="lit">4</span><span class="pun">)</span><span class="pln">
        angle </span><span class="pun">=</span><span class="pln"> wrapi</span><span class="pun">(</span><span class="pln">int</span><span class="pun">(</span><span class="pln">a</span><span class="pun">),</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">8</span><span class="pun">)</span><span class="pln">
        current_animation </span><span class="pun">=</span><span class="pln"> </span><span class="str">"run"</span><span class="pln">
    velocity </span><span class="pun">=</span><span class="pln"> input_dir </span><span class="pun">*</span><span class="pln"> speed
    move_and_slide</span><span class="pun">()</span><span class="pln">
    $AnimatedSprite2D</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="pln">current_animation </span><span class="pun">+</span><span class="pln"> str</span><span class="pun">(</span><span class="pln">angle</span><span class="pun">))</span></pre>

<h2 id="-9">
	الخاتمة
</h2>

<p>
	استعرضنا في هذا المقال أساليب مختلفة لتحريك الشخصيات في الألعاب ثنائية الأبعاد باستخدام محرك ألعاب جودو، بما يشمل الالتفاف حول الشاشة، والحركة من الأعلى إلى الأسفل، والحركة الشبكية، والحركة في ثمانية اتجاهات.
</p>

<p>
	تساعد هذه الأساليب على تحسين تجربة اللعب وتوفير تحكم دقيق ومتناسق مع نوع اللعبة. يُنصح باختيار الطريقة الأفضل حسب تصميم اللعبة وأسلوب اللعب المطلوب. ويمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/8_direction_animation" rel="external nofollow">Github</a> لمزيد من الفهم.
</p>

<p>
	ترجمة -وبتصرّف- للأقسام <a href="https://kidscancode.org/godot_recipes/4.x/2d/screen_wrap/index.html" rel="external nofollow">Screen wrap</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/topdown_movement/index.html" rel="external nofollow">Top-down movement</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/grid_movement/index.html" rel="external nofollow">Grid-based movement</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/8_direction/index.html" rel="external nofollow">8-Directional Movement/Animation</a> من توثيقات Kidscancode.
</p>

<h2 id="-10">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/" rel="">إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">فهم RayCast2D واستخداماتها في محرك ألعاب جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%B3%D8%AD%D8%A8-%D9%88%D8%A5%D9%81%D9%84%D8%A7%D8%AA-%D8%AC%D8%B3%D9%85-%D8%B5%D9%84%D8%A8-rigidbody2d-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2551/" rel="">سحب وإفلات جسم صلب RigidBody2D في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2508/" rel="">إنشاء شخصيات ثلاثية الأبعاد في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D8%B9%D8%A8%D8%A9-%D9%85%D8%AA%D8%A7%D9%87%D8%A9-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D9%83-%D9%8A%D9%88%D9%86%D9%8A%D8%AA%D9%8A-unity-r1700/" rel="">برمجة لعبة متاهة باستخدام محرك يونيتي Unity</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2571</guid><pubDate>Thu, 15 May 2025 16:03:01 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x634;&#x62E;&#x635;&#x64A;&#x629; &#x648;&#x62A;&#x62D;&#x631;&#x64A;&#x643;&#x647;&#x627; &#x641;&#x64A; &#x623;&#x644;&#x639;&#x627;&#x628; &#x627;&#x644;&#x645;&#x646;&#x635;&#x627;&#x62A; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%88%D8%AA%D8%AD%D8%B1%D9%8A%D9%83%D9%87%D8%A7-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D8%A7%D8%AA-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-r2570/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/2.-02.png.0afd599e0ee43f03b1e79d9ee2142375.png" /></p>
<p>
	سنكتشف في هذا المقال متى يدخل أو يخرج كائن من الشاشة، وسنتعرّف على كيفية إنشاء وتحريك شخصية في ألعاب المنصات ثنائية الأبعاد.
</p>

<h2 id="">
	إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد
</h2>

<p>
	سننشئ الأن شخصيةً في ألعاب المنصات ثنائية الأبعاد. يُفاجَأ المطورون المبتدئون في أغلب الأحيان بمدى تعقيد برمجة شخصيات ألعاب المنصات، لذا يوفر <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot/" rel="">محرّك ألعاب جودو Godot</a> بعض الأدوات المُضمَّنة للمساعدة، ولكن يمكن القول بأن هناك عددًا كبيرًا من الحلول التي تضاهي عدد الألعاب الموجودة. لن نتعمق في شرح الميزات مثل ميزات القفزات المزدوجة أو الانحناء أو القفز على الحائط أو الرسوم المتحركة، بل سنناقش أساسيات الحركة في ألعاب المنصات.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن استخدام العقدة <code>RigidBody2D</code> لإنشاء شخصية لألعاب المنصات، ولكننا سنركز على استخدام العقدة <code>CharacterBody2D</code>، إذ تُعَد الأجسام الحركية Kinematic مناسبةً لألعاب المنصات، حيث لن تكون مهتمًا كثيرًا بالفيزياء الواقعية بقدر الاهتمام بالشعور المتجاوب في اللعبة.
</p>

<p>
	سنبدأ بعقدة <code>CharacterBody2D</code>، ونضيف إليها عقدتي <code>Sprite2D</code> و <code>CollisionShape2D</code>، ثم نرفق السكربت التالي بالعقدة الجذر للشخصية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1701_6" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1200</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var jump_speed </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1800</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var gravity </span><span class="pun">=</span><span class="pln"> </span><span class="lit">4000</span><span class="pln">


func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="com"># إضافة الجاذبية في كل إطار</span><span class="pln">
    velocity</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+=</span><span class="pln"> gravity </span><span class="pun">*</span><span class="pln"> delta

    </span><span class="com"># ‫يؤثر الدخل على المحور x فقط</span><span class="pln">
    velocity</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"walk_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"walk_right"</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> speed

    move_and_slide</span><span class="pun">()</span><span class="pln">

    </span><span class="com"># السماح بالقفز عند وجود الشخصية على الأرض فقط</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_just_pressed</span><span class="pun">(</span><span class="str">"jump"</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> is_on_floor</span><span class="pun">():</span><span class="pln">
        velocity</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> jump_speed</span></pre>

<p>
	يمكننا ملاحظة أننا نستخدم إجراءات الإدخال التي عرّفناها في خريطة الإدخال InputMap وهي: <code>"walk_right"</code> و <code>"walk_left"</code> و <code>"jump"</code>، لذا يمكن الاطلاع على <a href="https://kidscancode.org/godot_recipes/4.x/input/input_actions/index.html" rel="external nofollow">إجراءات الإدخال InputActions</a> لمزيد من التفاصيل.
</p>

<p>
	تعتمد القيم المُستخدَمة للسرعة <code>speed</code> والجاذبية <code>gravity</code> وسرعة القفز <code>jump_speed</code> اعتمادًا كبيرًا على حجم الشخصية الرسومية Sprite الخاصة باللاعب، وتكون خامة Texture اللاعب 108x208 بكسل في هذا المثال، وإذا كانت الصورة أصغر حجمًا، فستستخدم قيمًا أصغر.
</p>

<p>
	نريد أيضًا قيمًا عالية للشعور بالسرعة والاستجابة، إذ تؤدي الجاذبية المنخفضة إلى لعبة نشعر فيها بعدم التوازن؛ بينما تمثل القيمة العالية العودة إلى الأرض بسرعة والجاهزية للقفز مرةً أخرى.
</p>

<p>
	سنتحقق من التابع <code>is_on_floor()‎</code> بعد استخدام الدالة <code>move_and_slide()‎</code> التي تضبط قيمة هذا التابع، لذا يجب عدم التحقق منه قبل ذلك، وإلّا سنحصل على القيمة من الإطار السابق.
</p>

<h3 id="frictionacceleration">
	الاحتكاك Friction والتسارع Acceleration
</h3>

<p>
	تُعَد الشيفرة البرمجية السابقة بداية جيدة، إذ يمكننا استخدامها كأساس لمجموعة متنوعة من متحكمات المنصات، ولكنها تواجه مشكلة الحركة اللحظية Instantaneous Movement، ولكن يمكننا الحصول على شعور طبيعي أكثر من خلال تسارع الشخصية مثلًا حتى تصل إلى سرعتها القصوى، ثم تتوقف عند عدم وجود دخل.
</p>

<p>
	إحدى الطرق لإضافة هذا السلوك هي استخدام الاستيفاء الخطي Linear Interpolation أو "lerp"، بحيث ننتقل باستخدام الاستيفاء الخطي من السرعة الحالية إلى السرعة القصوى عند التحرك، وننتقل باستخدام الاستيفاء الخطي من السرعة الحالية إلى الصفر أثناء التوقف، وبالتالي سيوفر تعديل مقدار الاستيفاء الخطي مجموعة متنوعة من أنماط الحركة.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن الاطلاع على مقال <a href="https://kidscancode.org/godot_recipes/4.x/math/interpolation/" rel="external nofollow">الاستيفاء</a> للحصول على مزيد من المعلومات عن الاستيفاء الخطي.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1701_8" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1200</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var jump_speed </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1800</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var gravity </span><span class="pun">=</span><span class="pln"> </span><span class="lit">4000</span><span class="pln">
</span><span class="lit">@export_range</span><span class="pun">(</span><span class="lit">0.0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">1.0</span><span class="pun">)</span><span class="pln"> var friction </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.1</span><span class="pln">
</span><span class="lit">@export_range</span><span class="pun">(</span><span class="lit">0.0</span><span class="pln"> </span><span class="pun">,</span><span class="pln"> </span><span class="lit">1.0</span><span class="pun">)</span><span class="pln"> var acceleration </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.25</span><span class="pln">

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    velocity</span><span class="pun">.</span><span class="pln">y </span><span class="pun">+=</span><span class="pln"> gravity </span><span class="pun">*</span><span class="pln"> delta
    var dir </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_axis</span><span class="pun">(</span><span class="str">"walk_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"walk_right"</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> dir </span><span class="pun">!=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        velocity</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> dir </span><span class="pun">*</span><span class="pln"> speed</span><span class="pun">,</span><span class="pln"> acceleration</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
        velocity</span><span class="pun">.</span><span class="pln">x </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="pln">velocity</span><span class="pun">.</span><span class="pln">x</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.0</span><span class="pun">,</span><span class="pln"> friction</span><span class="pun">)</span><span class="pln">

    move_and_slide</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">is_action_just_pressed</span><span class="pun">(</span><span class="str">"jump"</span><span class="pun">)</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> is_on_floor</span><span class="pun">():</span><span class="pln">
        velocity</span><span class="pun">.</span><span class="pln">y </span><span class="pun">=</span><span class="pln"> jump_speed</span></pre>

<p>
	سنجرب هنا تغيير قيم الاحتكاك <code>friction</code> والتسارع <code>acceleration</code> لمعرفة مدى تأثيرها على شعورنا أثناء اللعب. فقد يحتاج مستوى الجليد إلى قيم منخفضة جدًا مثلًا، مما يجعل الحركة أصعب.
</p>

<p style="text-align: center;">
	<img alt="01_platformer1.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="172041" data-ratio="58.20" data-unique="gizijo2k1" width="512" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_platformer1.gif.77f4779134e9aa500743bd9a498c1d79.gif">
</p>

<p>
	تمنحنا هذه الشيفرة البرمجية نقطة بداية لبناء متحكم منصات خاص بك، لذا يمكن الاطلاع على المقالات اللاحقة لمزيد من ميزات المنصات المتقدمة مثل القفز على الحائط.
</p>

<h2 id="-1">
	تحديد متى يدخل كائن إلى الشاشة أو يغادرها
</h2>

<p>
	يوفر محرّك الألعاب العقدة <code>VisibleOnScreenNotifier2D</code>، حيث إذا أرفقنا هذه العقدة بالكائن، فسنتمكّن من استخدام إشارات <code>screen_entered</code> و <code>screen_exited</code> الخاصة بها.
</p>

<h3 id="1">
	تطبيق عملي 1
</h3>

<p>
	ليكن لدينا مثلًا مقذوف يتحرك في خط مستقيم بعد إطلاقه، وإذا استمرينا في الإطلاق، فسنحصل في النهاية على عدد كبير من الكائنات التي يتعقّبها المحرّك، حتى وإن كانت هذه الكائنات خارج الشاشة، مما قد يتسبب في حدوث تأخير.
</p>

<p>
	توضح الشيفرة البرمجية التالية حركة المقذوف:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1701_10" style=""><span class="pln">extends </span><span class="typ">Area2D</span><span class="pln">

var velocity </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">500</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">)</span><span class="pln">

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    position </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	يمكن حذف المقذوف تلقائيًا عند تحرّكه خارج الشاشة من خلال إضافة العقدة <code>VisibleOnScreenNotifier2D</code> والاتصال بإشارة <code>screen_exited</code> الخاصة بها.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1701_12" style=""><span class="pln">func _on_VisibleOnScreenNotifier2D_screen_exited</span><span class="pun">():</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<h3 id="2">
	تطبيق عملي 2
</h3>

<p>
	ليكن لدينا مثلًا عدو ينفّذ بعض الإجراءات مثل التحرك على طول مسار أو تشغيل رسوم متحركة، ولن يظهر على الشاشة سوى عدد قليل من الأعداء في الوقت نفسه في خريطة كبيرة تحتوي على العديد من الأعداء، إذ يمكننا تعطيل إجراءات العدو أثناء وجوده خارج الشاشة باستخدام العقدة <code>VisibleOnScreenNotifier2D</code> كما هو الحال في جزء الشيفرة البرمجية التالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9196_6" style=""><span class="pln">var active </span><span class="pun">=</span><span class="pln"> false

func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> active</span><span class="pun">:</span><span class="pln">
        play_animation</span><span class="pun">()</span><span class="pln">
        move</span><span class="pun">()</span><span class="pln">

func _on_VisibleOnScreenNotifier2D_screen_entered</span><span class="pun">():</span><span class="pln">
    active </span><span class="pun">=</span><span class="pln"> true

func _on_VisibleOnScreenNotifier2D_screen_exited</span><span class="pun">():</span><span class="pln">
    active </span><span class="pun">=</span><span class="pln"> false</span></pre>

<h2 id="-2">
	الخاتمة
</h2>

<p>
	استعرضنا في هذا المقال كيفية استخدام العقدة <code>VisibleOnScreenNotifier2D</code> لتحديد متى يدخل كائن إلى الشاشة أو يغادرها، مما يساعد في تحسين أداء اللعبة من خلال إدارة الكائنات غير المرئية بكفاءة؛ كما تناولنا أساسيات إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد باستخدام العقدة <code>CharacterBody2D</code>، مع التركيز على مفاهيم مثل الجاذبية، القفز، والتسارع للحصول على حركة سلسة واستجابة طبيعية.
</p>

<p>
	يمكن تنزيل شيفرة المشروع البرمجية من <a href="https://github.com/godotrecipes/2d_platform_basic" rel="external nofollow">Github</a> للاطلاع عليه وفهمه أكثر.
</p>

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/2d/enter_exit_screen/index.html" rel="external nofollow">Entering/Exiting the screen</a> و <a href="https://kidscancode.org/godot_recipes/4.x/2d/platform_character/index.html" rel="external nofollow">Platform character</a> من توثيقات Kidscancode.
</p>

<h2 id="-3">
	اقرأ أيضًا
</h2>

<ul>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%81%D9%87%D9%85-raycast2d-%D9%88%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85%D8%A7%D8%AA%D9%87%D8%A7-%D9%81%D9%8A-%D9%85%D8%AD%D8%B1%D9%83-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2564/" rel="">فهم RayCast2D واستخداماتها في محرك ألعاب جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%B3%D8%AD%D8%A8-%D9%88%D8%A5%D9%81%D9%84%D8%A7%D8%AA-%D8%AC%D8%B3%D9%85-%D8%B5%D9%84%D8%A8-rigidbody2d-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2551/" rel="">سحب وإفلات جسم صلب RigidBody2D في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D8%AB%D9%84%D8%A7%D8%AB%D9%8A%D8%A9-%D8%A7%D9%84%D8%A3%D8%A8%D8%B9%D8%A7%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2508/" rel="">إنشاء شخصيات ثلاثية الأبعاد في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AF%D9%84%D9%8A%D9%84%D9%83-%D8%A7%D9%84%D8%B4%D8%A7%D9%85%D9%84-%D8%A5%D9%84%D9%89-%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2304/" rel="">دليلك الشامل إلى برمجة الألعاب</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D8%AA%D8%B7%D9%88%D8%B1%D9%87%D8%A7-%D9%88%D8%A3%D9%87%D9%85%D9%8A%D8%AA%D9%87%D8%A7-%D9%88%D8%AE%D8%B7%D9%88%D8%A7%D8%AA-%D8%A8%D8%B1%D9%85%D8%AC%D8%AA%D9%87%D8%A7-r2290/" rel="">ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D8%B9%D8%A8%D8%A9-%D9%85%D8%AA%D8%A7%D9%87%D8%A9-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D9%83-%D9%8A%D9%88%D9%86%D9%8A%D8%AA%D9%8A-unity-r1700/" rel="">برمجة لعبة متاهة باستخدام محرك يونيتي Unity</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2570</guid><pubDate>Mon, 12 May 2025 16:00:00 +0000</pubDate></item></channel></rss>
