<?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/page/2/?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>&#x625;&#x646;&#x634;&#x627;&#x621; &#x644;&#x639;&#x628;&#x629; &#x633;&#x641;&#x64A;&#x646;&#x629; &#x641;&#x636;&#x627;&#x621; &#x62B;&#x646;&#x627;&#x626;&#x64A;&#x629; &#x627;&#x644;&#x623;&#x628;&#x639;&#x627;&#x62F; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/6..png.5483a332d271ef84c2fcff2af902282c.png" /></p>
<p>
	سنشرح في هذا المقال والمقالات التي تليه خطوات إعداد وتنفيذ لعبة كلاسيكية في فضاء ثنائي البعد في محرك الألعاب جودو، اللعبة التي سنعمل عليها هي لعبة مركبة فضاء مقاتلة، وفيما يلي لقطة شاشة لما سيكون عليه الحال عند الانتهاء من اللعبة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171617" href="https://academy.hsoub.com/uploads/monthly_2025_05/01_2d_screenshot.png.673815097760b04d2bbf3e867f6bd478.png" rel=""><img alt="01 2d screenshot " class="ipsImage ipsImage_thumbnailed" data-fileid="171617" data-ratio="131.58" data-unique="i19urnver" style="width: 300px; height: auto;" width="456" src="https://academy.hsoub.com/uploads/monthly_2025_05/01_2d_screenshot.thumb.png.a982e9fc716fe8ab9c6746caa2402b30.png"></a>
</p>

<h2 id="-">
	نقطة الانطلاق
</h2>

<p>
	لسهولة الفهم سنبني في كل مقال من هذه السلسلة جزءًا من اللعبة، ونضيف الميزات تدريجيًا ونشرح بالتفصيل ما نفعله في كل مرة. وإن وجدتم  مشكلة في فهم الجانب البرمجي لأي جزء من المشروع ننصحكم بالاطلاع على مصادر مفيدة مثل <a href="https://docs.godotengine.org/en/latest/getting_started/step_by_step/index.html" rel="external nofollow">توثيق جودو الرسمي</a>، وسلسلة مقالات <a href="https://academy.hsoub.com/tags/%D8%AA%D8%B9%D9%84%D9%85%20%D8%AC%D9%88%D8%AF%D9%88/" rel="">تعلم جودو</a> على أكاديمية حسوب.
</p>

<p>
	كما يمكنكم <a href="https://github.com/godotrecipes/classic_shmup" rel="external nofollow">تحميل مشروع اللعبة كاملًا</a> من مستودعه المخصص على جيتهب، أو من هنا مباشرة <a class="ipsAttachLink" data-fileext="zip" data-fileid="171622" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=171622&amp;key=7d4e7b2378cd75463c859a86eb294b91" rel="">classic_shmup.zip</a>وتجربتها لديكم لمزيد من الفهم.
</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>
	اخترنا أن يكون المشروع ثنائي الأبعاد 2D لأن الألعاب ثلاثية الأبعاد 3D أكثر تعقيدًا بالنسبة للمبتدئين ، لكن سترون لاحقًا أن الكثير من الميزات الأساسية التي ستتعلمونها عند بناء اللعبة هي نفسها في حالة الألعاب ثنائية وثلاثية الأبعاد. لهذا ينصح أن تتقنوا أولًا خطوات العمل على لعبة متكاملة ثنائية الأبعاد،  وبعدها سيكون الانتقال إلى الفضاء ثلاثي البعد أسهل.
</p>

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

<h3 id="-assets">
	تحميل أصول اللعبة Assets
</h3>

<p>
	يمكنكم تحميل المحلقات أو الأصول الخاصة باللعبة من صور وأيقونات مختلفة من موقع <a href="https://grafxkid.itch.io/mini-pixel-pack-3" rel="external nofollow">Mini Pixel Pack by Grafxkid</a>، بعد التحميل علينا أن نستخرج الملفات وننسخها إلى المشروع بسحب المجلد وإفلاته ضمن نافذة مدير الملفات File System لتبدأ عمليه <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%D9%83%D8%A7%D8%A6%D9%86%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-r2502/" rel="">استيرادها</a> لداخل اللعبة.
</p>

<p style="text-align: center;">
	<img alt="02_file_system.png" class="ipsImage ipsImage_thumbnailed" data-fileid="171618" data-ratio="113.53" data-unique="oa14h1gdb" width="207" src="https://academy.hsoub.com/uploads/monthly_2025_05/02_file_system.png.7e9c9a6ce371f5a40edee8697a366ee6.png">
</p>

<h3 id="-">
	إعدادات المشروع
</h3>

<p>
	ننتقل الآن إلى القائمة مشروع Project، ثم نختار إعدادات المشروع Project Settings وننقر على زر التبديل إعدادات متقدمة Advanced settings في الزاوية العليا اليسارية، ونضبط ما يلي في قسم النافذة window الموجود ضمن قسم لإظهار Display كما يلي:
</p>

<ul>
	<li>
		عرض منفذ العرض viewport width وطول منفذ العرض viewport height على القيمتين <code>240</code> و <code>320</code>
	</li>
	<li>
		تجاوز عرض النافذة window width override وتجاوز ارتفاع النافذة window height override على القيمتين <code>480</code> و <code>640 </code>على التوالي
	</li>
	<li>
		الخيار تمدد stretch ضمن القسم وضع mode على القيمة <code>canvas_items</code> لجعل عناصر المشهد اللعبة تتمدد وتتناسب مع حجم الشاشة
	</li>
</ul>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171623" href="https://academy.hsoub.com/uploads/monthly_2025_05/697029057_.png.d5ab8a1ae60a5ec496878b2acec9ca40.png" rel=""><img alt="02 إعدادات نافذة المشروع" class="ipsImage ipsImage_thumbnailed" data-fileid="171623" data-ratio="61.71" data-unique="a4g9ckpgu" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2025_05/.thumb.png.f86a9d880cf518728894b147518ae374.png"></a>
</p>

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

<p>
	الآن لننتقل ضمن إعدادات المشروع للقسم معالجة Rendering ثم نختار ملمس Texture ونضبط الخيار تصفية الملمس الافتراضي Default Texture Filter على القيمة <code>Nearest</code>. 
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" href="https://academy.hsoub.com/uploads/monthly_2025_05/22250421_.png.6076416ca33074b1c2a65ab633930dcc.png" data-fileid="171624" data-fileext="png" rel=""><img alt="03 إعدادات معالجة المشروع" class="ipsImage ipsImage_thumbnailed" data-fileid="171624" data-ratio="62.00" data-unique="5ctyk0e04" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2025_05/.thumb.png.b2577776c916b1b5ebf83f088d3701aa.png"></a>
</p>

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

<p style="text-align: center;">
	<img alt="03_pixle_art.png" class="ipsImage ipsImage_thumbnailed" data-fileid="171619" data-ratio="49.50" data-unique="n5yyo0e4t" width="303" src="https://academy.hsoub.com/uploads/monthly_2025_05/03_pixle_art.png.938ba149f219140cf0babc9ae47bf4fb.png">
</p>

<p>
	ننقر الآن على تبويب خريطة الإدخال Input Map في إعدادت المشروع حيث سنضبط هنا عناصر الإدخال التي نستخدمها في اللعبة. نكتب كل كلمة مما يلي <code>right</code> و <code>left</code> و <code>up</code> و <code>down</code> و <code>shoot</code> على حدة في صندوق إضافة إجراء جديد Add New Action ثم ننقر بعدها المفتاح Enter لتتكون لدينا خمسة إجراءات، أربعة منها للتحرك في الاتجاهات المختلفة وواحدة للإطلاق. ننقر بعد ذلك الزر + إلى جانب كل إجراء ثم ننقر أي مفتاح من من لوحة المفاتيح كي نعينه لتنفيذ هذا الإجراء. من المفترض أن نرى نتيجة مشابهة لما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171620" href="https://academy.hsoub.com/uploads/monthly_2025_05/04_input_actions.png.6c4660fe46e96e709652fbfafa735686.png" rel=""><img alt="04_input_actions.png" class="ipsImage ipsImage_thumbnailed" data-fileid="171620" data-ratio="63.78" data-unique="le4cx5yvf" style="width: 700px; height: auto;" width="900" src="https://academy.hsoub.com/uploads/monthly_2025_05/04_input_actions.thumb.png.aa79e2669d85527cd1535d07281db0c7.png"></a>
</p>

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

<h2>
	الخاتمة
</h2>

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

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/index.html" rel="external nofollow">Your first 2D game</a> و <a href="https://kidscancode.org/godot_recipes/4.x/games/first_2d/first_2d_01/index.html" rel="external nofollow">Project setup</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D9%84%D9%85-%D8%A5%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2559/" 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://wiki.hsoub.com/Godot/best_practices/project_organization" rel="external">تنظيم مشروع جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%A8%D8%AF%D8%A1-%D8%A8%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%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%A8%D8%B9%D8%AF-%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-r2280/" rel="">البدء بتطوير لعبةبسيطة ثنائية البعد عبر محرك الألعاب Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2568</guid><pubDate>Mon, 05 May 2025 13:01:01 +0000</pubDate></item><item><title>&#x641;&#x647;&#x645; RayCast2D &#x648;&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645;&#x627;&#x62A;&#x647;&#x627; &#x641;&#x64A; &#x645;&#x62D;&#x631;&#x643; &#x623;&#x644;&#x639;&#x627;&#x628; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_05/1.----Raycast---3d--.png.e4f1fd30a59f9f00913e58dfaf31c295.png" /></p>
<p>
	سنتعرف في مقال اليوم على العقدة RayCast2D في محرك جودو وكيفية استخدامها بكفاءة في تطوير الألعاب ثنائية الأبعاد، من أجل كشف تصادم الأشعة Raycasting الذي يفيدنا في العديد من حالات الاستخدام.
</p>

<h2>
	أهمية العقدة RayCast2D في تطوير الألعاب
</h2>

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

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

<h2 id="raycast2d-1">
	أهم خصائص العقدة <code>RayCast2D</code>
</h2>

<p>
	تتضمن العقدة <code>RayCast2D</code> في جودو مجموعة من الخصائص المهمة التي تساعدنا على ضبط سلوك الشعاع بشكل دقيق. 
</p>

<p>
	دعونا ننشئ مشروع جديد للعبة ثنائية الأبعاد، ونضيف للمشهد عقدة <code>RayCast2D</code> ونتحقق من خصائصها الظاهرة في الفاحص Inspector كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171315" href="https://academy.hsoub.com/uploads/monthly_2025_04/001_kyn_raycast2d_01_4.png.31e16cc822ebb195b0b6483d8166c46c.png" rel=""><img alt="001 kyn raycast2d 01 4" class="ipsImage ipsImage_thumbnailed" data-fileid="171315" data-unique="wfwdzy8a2" src="https://academy.hsoub.com/uploads/monthly_2025_04/001_kyn_raycast2d_01_4.png.31e16cc822ebb195b0b6483d8166c46c.png"> </a>
</p>

<p>
	فيما يلي الخاصيات الرئيسية التي ستحتاج إلى فهمها للتعامل مع هذه العقدة:
</p>

<h3 id="enabled">
	الخاصية <code>Enabled</code>
</h3>

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

<h3 id="excludeparent">
	الخاصية <code>Exclude Parent</code>
</h3>

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

<h3 id="targetposition">
	الخاصية <code>Target Position</code>
</h3>

<p>
	تحدد هذه الخاصية نقطة نهاية الشعاع بالنسبة لموضع العقدة نفسه، أي باستخدام <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%AA%D8%AD%D9%88%D9%8A%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%87%D9%86%D8%AF%D8%B3%D9%8A%D8%A9-transform-%D9%81%D9%8A-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-r2561/" rel="">الإحداثيات المحلية</a>، على سبيل المثال، إذا كانت قيمتها <code>(250,0)</code> فسيمتد الشعاع أفقيًا لليمين لمسافة 250 وحدة وهي نقطة الوجهة للشعاع في الإحداثيات المحلية.
</p>

<p>
	<strong>ملاحظة</strong>: تمثل الإحداثيات المحلية Local Coordinates موقع العقدة بالنسبة لنقطة الأصل الخاصة بالعقدة الأم، بينما تمثل الإحداثيات العامة Global Coordinates الموقع المطلق للعقدة داخل المشهد بأكمله.
</p>

<p>
	لنلاحظ أيضًا القسم بعنوان Collide With ضمن الفاحص، ففي هذا القسم يمكننا تحديد أنواع الكائنات التي يجب أن يتفاعل معها الشعاع، حيث سيكتشف الشعاع افتراضيًا الأجسام الفيزيائية <code>Bodies</code> فقط مثل <code>KinematicBody2D</code> أو <code>RigidBody2D</code>، ولو أردنا منه اكتشاف المناطق أيضًا مثل <code>Area2D</code> فعلينا تفعيل الخيار <code>Area</code>.
</p>

<h2 id="">
	دوال مفيدة للعقدة RayCast2D
</h2>

<p>
	يمكن الاطلاع على القائمة الكاملة لدوال العقدة RayCast2D في <a href="https://docs.godotengine.org/en/stable/classes/class_raycast2d.html" rel="external nofollow">توثيق واجهة برمجة التطبيقات <abbr title="Application Programming Interface | واجهة برمجية"><abbr title="Application Programming Interface | واجهة برمجية">API</abbr></abbr></a>، ولكن سنوضح تاليًا بعض الدوال المفيدة للتعامل مع التصادمات:
</p>

<ul>
	<li>
		<code>is_colliding()‎</code>: دالة منطقية تتيح معرفة فيما إذا كان الشعاع يصطدم بشيء ما
	</li>
	<li>
		<code>get_collision_point()‎</code>: إذا اصطدم الشعاع بشيء ما ستعيد هذه الدالة موضع التصادم في الإحداثيات العامة
	</li>
	<li>
		<code>get_collider()‎</code>: إذا اصطدم الشعاع بشيء ما ستعيد هذه الدالة مرجعًا إلى الكائن المتصادم
	</li>
	<li>
		<code>get_collision_normal()‎</code>: تعيد هذه الدالة الشعاع الناظم Normal على الكائن المتصادم عند نقطة الاصطدام
	</li>
</ul>

<h2 id="-1">
	أمثلة عملية
</h2>

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

<p>
	سنوضح فيما يلي بعض الأمثلة العملية المفيدة.
</p>

<h3 id="-2">
	المثال الأول: إطلاق النار
</h3>

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

<p>
	يمثّل الشكل التالي شخصية اللاعب حيث أضفنا عقدة <code>Raycast2D</code> عند نهاية السلاح، وضبطنا موضع الهدف <code>target_position</code> على القيمة <code>(250,0)</code>لضبط اتجاه الشعاع الذي يُطلق من السلاح.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171316" href="https://academy.hsoub.com/uploads/monthly_2025_04/002_kyn_raycast2d_02.png.e33239f6558ad6a9ad791cdb8470f01d.png" rel=""><img alt="002 kyn raycast2d 02" class="ipsImage ipsImage_thumbnailed" data-fileid="171316" data-unique="562xrj6rb" src="https://academy.hsoub.com/uploads/monthly_2025_04/002_kyn_raycast2d_02.png.e33239f6558ad6a9ad791cdb8470f01d.png"> </a>
</p>

<p>
	إذا أطلق اللاعب قذيفة، فيجب التحقق فيما إذا كان الشعاع يصطدم بشيء ما كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1518_10" 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="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">
        </span><span class="kwd">if</span><span class="pln"> $RayCast2D</span><span class="pun">.</span><span class="pln">is_colliding</span><span class="pun">():</span><span class="pln">
            </span><span class="kwd">print</span><span class="pun">(</span><span class="pln">$RayCast2D</span><span class="pun">.</span><span class="pln">get_collider</span><span class="pun">().</span><span class="pln">name</span><span class="pun">)</span></pre>

<h3 id="-3">
	المثال الثاني: اكتشاف حافة منصة
</h3>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="171317" href="https://academy.hsoub.com/uploads/monthly_2025_04/003_kyn_raycast2d_03.png.c4989d45e08641a52e3e1489ff0b1421.png" rel=""><img alt="003 kyn raycast2d 03" class="ipsImage ipsImage_thumbnailed" data-fileid="171317" data-unique="1h43o7wy3" src="https://academy.hsoub.com/uploads/monthly_2025_04/003_kyn_raycast2d_03.png.c4989d45e08641a52e3e1489ff0b1421.png"> </a>
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1518_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">
    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="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> $RayRight</span><span class="pun">.</span><span class="pln">is_colliding</span><span class="pun">():</span><span class="pln">
        dir </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> $RayLeft</span><span class="pun">.</span><span class="pln">is_colliding</span><span class="pun">():</span><span class="pln">
        dir </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</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
    $AnimatedSprite</span><span class="pun">.</span><span class="pln">flip_h </span><span class="pun">=</span><span class="pln"> velocity</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="pln">
    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"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">UP</span><span class="pun">)</span></pre>

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

<p>
	وسيبدو الأمر أثناء العمل كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="171318" href="https://academy.hsoub.com/uploads/monthly_2025_04/004_kyn_raycast2d_04.gif.1cc04ac25a73a9d8cca68b2fc533188d.gif" rel=""><img alt="004 kyn raycast2d 04" class="ipsImage ipsImage_thumbnailed" data-fileid="171318" data-unique="z2cid1v0d" src="https://academy.hsoub.com/uploads/monthly_2025_04/004_kyn_raycast2d_04.gif.1cc04ac25a73a9d8cca68b2fc533188d.gif"> </a>
</p>

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

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

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

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<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/%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-%D9%84%D9%84%D8%AA%D9%88%D8%AC%D9%87-%D9%86%D8%AD%D9%88-%D9%87%D8%AF%D9%81-%D9%88%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%83-%D9%86%D8%AD%D9%88%D9%87-r2547/" rel="">استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D9%82%D9%84-%D9%81%D9%8A-%D8%B4%D8%AC%D8%B1%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%D9%81%D9%8A-godot-r2512/" 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%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>
</ul>
]]></description><guid isPermaLink="false">2564</guid><pubDate>Thu, 01 May 2025 13:05:00 +0000</pubDate></item><item><title>&#x62A;&#x639;&#x644;&#x645; &#x625;&#x62F;&#x627;&#x631;&#x629; &#x627;&#x644;&#x635;&#x648;&#x62A; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D9%84%D9%85-%D8%A5%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2559/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/5..png.9d6f28d2156cd18b6eec1a041fcb5a17.png" /></p>
<p>
	عند تطوير الألعاب باستخدام جودو، من الشائع أن نربط الأصوات بالأحداث التي تقوم بها الشخصيات، كتشغيل صوت معين عند موت الشخصية وصوت آخر عندما تنفذ هجومًا معينًا. ونستخدم غالبًا العقدة <code>AudioStreamPlayer</code> لتشغيل هذه الأصوات لكن هناك مشكلة شائعة قد تواجهنا، فعند إزالة الشخصية من المشهد بسبب موتها أو لأي سبب آخر  ستزال كل العقد التابعة لها، بما في ذلك عقدة مشغل الصوت. ونتيجة لذلك، سيتوقف الصوت فجأة، حتى لو لم يكن قد اكتمل تشغيله بعد، وهذه التجربة قد تكون مزعجة للاعب، لأنها تجعل اللعبة تبدو غير طبيعية. سنشرح في الفقرات التالية طريقة مناسبة لحل هذه المشكلة.
</p>

<h2 id="-1">
	مشروع مشغل الصوت
</h2>

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

<p>
	لتحقيق ذلك سننشئ مشروع جودو جديد وننشئ ضمنه مشهد جديد ونحفظه باسم <code>audio_demo.tscn</code> أو أي اسم آخر مناسب ونضيف له مجموعة عقد وفق التسلسل الهرمي التالي:
</p>

<pre class="ipsCode" id="ips_uid_1083_8">AudioDemo (MarginContainer)
├── CenterContainer (CenterContainer)
│   └── GridContainer (GridContainer)
├── CanvasLayer (CanvasLayer)
│   └── HBoxContainer (HBoxContainer)
│       ├── Label (Label)
│       ├── Label2 (Label)
│       ├── VSeparator (VSeparator)
│       ├── Label3 (Label)
│       └── Label4 (Label) 
</pre>

<p>
	يتكوّن المشروع من عقدة جذر من نوع <code>MarginContainer</code> سنسميها AudioDemo، وهي المسؤولة عن تنظيم المحتوى. وننشئ بداخلها عقدة ابن من نوع <code>CenterContainer</code> لمحاذاة المحتويات في المنتصف، والتي سنصيف لها عقدة ابن جديدة <code>GridContainer</code> لتوليد الأزرار الخاصة بتشغيل الملفات الصوتية.
</p>

<p>
	كما سنضيف عقدة <code>CanvasLayer</code> لعرض واجهة المستخدم ونضيف لها عقدة ابن من نوع <code>HBoxContainer</code> وضمنها عدة عناصر تسمية  <code>Label</code> لعرض إحصائيات عن الملفات الصوتية التي نشغلها، وعدد الملفات الصوتية الموجودة في قائمة الانتظار وعقدة <code>VSeparator</code> للفصل بين العناصر.
</p>

<p>
	بعدها نضيف مجلد الموارد <code>assets</code> الذي يتضمن مجموعة ملفات صوتية، ويمكنك الحصول عليه من خلال تحميل هذا المشروع من <a href="https://github.com/godotrecipes/audio_manager" rel="external nofollow">مستودع جيتهب</a> أو من هنا مباشرة <a class="ipsAttachLink" data-fileext="zip" data-fileid="170863" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=170863&amp;key=25be9de9dbd256b76f9df26c21b62543" rel="">audio_manager.zip</a>.
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170862" href="https://academy.hsoub.com/uploads/monthly_2025_04/audio_mgr_02.png.1aa6dabf4452e7cde90698c86a356f95.png" rel=""><img alt="audio mgr 02" class="ipsImage ipsImage_thumbnailed" data-fileid="170862" data-ratio="51.33" data-unique="ndfe64az4" style="width: 450px; height: auto;" width="544" src="https://academy.hsoub.com/uploads/monthly_2025_04/audio_mgr_02.png.1aa6dabf4452e7cde90698c86a356f95.png"> </a>
</p>

<h2>
	الكود البرمجي لمدير الصوت
</h2>

<p>
	بعد إضافة العقد المطلوبة وتخصيصها نضيف في البداية سكريبت جديد في محرر جودو ونحفظه باسم <code>audio_manager.gd</code> ونكتب فيه الكود التالي:
</p>

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

var num_players </span><span class="pun">=</span><span class="pln"> </span><span class="lit">8</span><span class="pln">
var bus </span><span class="pun">=</span><span class="pln"> </span><span class="str">"master"</span><span class="pln">

var available </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span><span class="pln"> </span><span class="com"># المشغلات المتاحة</span><span class="pln">
var queue </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span><span class="pln"> </span><span class="com"># رتل الأصوات التي ستُشغل</span><span class="pln">


func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="com"># AudioStreamPlayer ننشئ حلقة من العقد</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> i </span><span class="kwd">in</span><span class="pln"> num_players</span><span class="pun">:</span><span class="pln">
      var player </span><span class="pun">=</span><span class="pln"> </span><span class="typ">AudioStreamPlayer</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
      add_child</span><span class="pun">(</span><span class="pln">player</span><span class="pun">)</span><span class="pln">
      available</span><span class="pun">.</span><span class="pln">append</span><span class="pun">(</span><span class="pln">player</span><span class="pun">)</span><span class="pln">
      player</span><span class="pun">.</span><span class="pln">finished</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">_on_stream_finished</span><span class="pun">.</span><span class="pln">bind</span><span class="pun">(</span><span class="pln">player</span><span class="pun">))</span><span class="pln">
      player</span><span class="pun">.</span><span class="pln">bus </span><span class="pun">=</span><span class="pln"> bus


func _on_stream_finished</span><span class="pun">(</span><span class="pln">stream</span><span class="pun">):</span><span class="pln">
    </span><span class="com"># نجعل المشغل متاحًا مجددًا بعد الانتهاء من تشغيل المقطع الصوتي</span><span class="pln">
    available</span><span class="pun">.</span><span class="pln">append</span><span class="pun">(</span><span class="pln">stream</span><span class="pun">)</span><span class="pln">


func play</span><span class="pun">(</span><span class="pln">sound_path</span><span class="pun">):</span><span class="pln">
    queue</span><span class="pun">.</span><span class="pln">append</span><span class="pun">(</span><span class="pln">sound_path</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">
    </span><span class="com"># نشغل الأصوات في الرتل إن وجد أي لاعب</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> queue</span><span class="pun">.</span><span class="pln">empty</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> available</span><span class="pun">.</span><span class="pln">empty</span><span class="pun">():</span><span class="pln">
      available</span><span class="pun">[</span><span class="lit">0</span><span class="pun">].</span><span class="pln">stream </span><span class="pun">=</span><span class="pln"> load</span><span class="pun">(</span><span class="pln">queue</span><span class="pun">.</span><span class="pln">pop_front</span><span class="pun">())</span><span class="pln">
      available</span><span class="pun">[</span><span class="lit">0</span><span class="pun">].</span><span class="pln">play</span><span class="pun">()</span><span class="pln">
      available</span><span class="pun">.</span><span class="pln">pop_front</span><span class="pun">()</span></pre>

<p>
	أنشأنا في الكود السابق مجموعة من عقد <code>AudioStreamPlayer</code> وعددها هو <code>num_players</code> وأسندنا لها القيمة الافتراضية 8 وخزناها في  قائمة باسم <code>available</code>. كلما أردنا تشغيل صوت نضيفه لرتل أو قائمة انتظار باسم <code>queue</code> مهمته تخزين الأصوات التي نريد تشغيلها لاحقًا، بعد انتهاء تشغيل أي صوت، يعاد المشغل إلى القائمة <code>available </code>ليكون جاهزًا للاستخدام مجددًا.
</p>

<p>
	سنضبط هذا السكريبت حتى يُحمّل تلقائيًا Auto-load  من إعدادات المشروع كي نتمكن من استدعائه من أي مكان بسهولة، للقيام بذلك سنفتح إعدادات المشروع من خلال القائمة Project ثم Project Settings، نذهب بعدها إلى تبويب Global ثم لتبويب التحميل التلقائي AutoLoad كما في الصورة التالية ونختار ملف السكريبت <code>audio_manager.gd</code> من ملفات المشروع، وفي خانة Node Name نمنحه اسمًا سهل التمييز مثل <code>AudioManager</code> ثم نضغط على Add، وبهذا سيضاف السكربت إلى قائمة التحميل التلقائي، وستُفعَّل خانة Enabled تلقائيًا.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170861" href="https://academy.hsoub.com/uploads/monthly_2025_04/01_audio_mgr.png.bec43d977604c4f42e82638296ef666a.png" rel=""><img alt="01 audio mgr" class="ipsImage ipsImage_thumbnailed" data-fileid="170861" data-unique="wpf7lalr3" src="https://academy.hsoub.com/uploads/monthly_2025_04/01_audio_mgr.png.bec43d977604c4f42e82638296ef666a.png"> </a>
</p>

<p>
	بهذا أصبح عندنا نظام صوت مركزي ثابت ومتاح في كل مكان داخل اللعبة، ويمكن أن نستدعيه من أي مكان من مشروعنا نريد فيه تشغيل الصوت بكتابة التالي:
</p>

<pre class="ipsCode">AudioManager.play("res://path/to/sound")
</pre>

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

<h3 id="-2">
	الكود البرمجي لواجهة المشروع الديناميكية
</h3>

<p>
	الخطوة التالية التي سنقوم بها هي توليد الواجهة الديناميكية المكونة من زر لتشغيل كل ملف صوتي، لتحقيق ذلك نلحق سكريبت للعقدة الجذر <code>AudioDemo</code> ونكتب فيه الكود التالي لتشغيل كافة الملفات الصوتية الموجودة في مجلد المشروع:
</p>

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

</span><span class="com"># المجلد الذي يحتوي على الملفات الصوتية</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var sound_dir</span><span class="pun">:</span><span class="pln"> </span><span class="typ">String</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> </span><span class="str">"res://assets"</span><span class="pln">


func _ready</span><span class="pun">():</span><span class="pln">
	</span><span class="com"># نحمل كل الملفات الصوتية الموجودة في المجلد</span><span class="pln">
	var dir </span><span class="pun">=</span><span class="pln"> </span><span class="typ">DirAccess</span><span class="pun">.</span><span class="pln">open</span><span class="pun">(</span><span class="pln">sound_dir</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">
		dir</span><span class="pun">.</span><span class="pln">list_dir_begin</span><span class="pun">()</span><span class="pln">
		var file_name </span><span class="pun">=</span><span class="pln"> dir</span><span class="pun">.</span><span class="pln">get_next</span><span class="pun">()</span><span class="pln">
		</span><span class="kwd">while</span><span class="pln"> file_name </span><span class="pun">!=</span><span class="pln"> </span><span class="str">""</span><span class="pun">:</span><span class="pln">
			</span><span class="kwd">if</span><span class="pln"> file_name</span><span class="pun">.</span><span class="pln">get_extension</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">in</span><span class="pln"> </span><span class="pun">[</span><span class="str">"wav"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"ogg"</span><span class="pun">]:</span><span class="pln">
				add_button</span><span class="pun">(</span><span class="pln">file_name</span><span class="pun">)</span><span class="pln">
			file_name </span><span class="pun">=</span><span class="pln"> dir</span><span class="pun">.</span><span class="pln">get_next</span><span class="pun">()</span><span class="pln">
		dir</span><span class="pun">.</span><span class="pln">list_dir_end</span><span class="pun">()</span><span class="pln">
		

func add_button</span><span class="pun">(</span><span class="pln">file_name</span><span class="pun">):</span><span class="pln">
	</span><span class="com"># إضافة زر لتشغيل الملف الصوتي المخصص له</span><span class="pln">
	var b </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Button</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
	$CenterContainer</span><span class="pun">/</span><span class="typ">GridContainer</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">add_theme_font_override</span><span class="pun">(</span><span class="str">"font"</span><span class="pun">,</span><span class="pln"> load</span><span class="pun">(</span><span class="str">"res://assets/Poppins-Medium.ttf"</span><span class="pun">))</span><span class="pln">
	b</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> file_name
	b</span><span class="pun">.</span><span class="pln">pressed</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">on_audio_button_pressed</span><span class="pun">.</span><span class="pln">bind</span><span class="pun">(</span><span class="pln">b</span><span class="pun">))</span><span class="pln">
	
	
func on_audio_button_pressed</span><span class="pun">(</span><span class="pln">button</span><span class="pun">):</span><span class="pln">
	</span><span class="com"># تشغيل الصوت المرتبط بالزر</span><span class="pln">
	var path </span><span class="pun">=</span><span class="pln"> sound_dir </span><span class="pun">+</span><span class="pln"> </span><span class="str">"/"</span><span class="pln"> </span><span class="pun">+</span><span class="pln"> button</span><span class="pun">.</span><span class="pln">text
	</span><span class="typ">AudioManager</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="pln">path</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">
	</span><span class="com"># تحديث عدد المشغلات المتاحة وعدد الأصوات في قائمة الانتظار</span><span class="pln">
	$CanvasLayer</span><span class="pun">/</span><span class="typ">HBoxContainer</span><span class="pun">/</span><span class="typ">Label2</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> str</span><span class="pun">(</span><span class="typ">AudioManager</span><span class="pun">.</span><span class="pln">available</span><span class="pun">.</span><span class="pln">size</span><span class="pun">())</span><span class="pln">
	$CanvasLayer</span><span class="pun">/</span><span class="typ">HBoxContainer</span><span class="pun">/</span><span class="typ">Label3</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> str</span><span class="pun">(</span><span class="typ">AudioManager</span><span class="pun">.</span><span class="pln">queue</span><span class="pun">.</span><span class="pln">size</span><span class="pun">())</span></pre>

<h2>
	الخاتمة
</h2>

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

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/audio/audio_manager/index.html" rel="external nofollow">Audio manager</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		<span ipsnoautolink="true">المقال السابق:</span> <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%AC%D8%B3%D9%85-%D8%B5%D9%84%D8%A8-rigidbody2d-%D8%A8%D9%88%D8%A7%D9%82%D8%B9%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A7%D9%84%D9%81%D8%B6%D8%A7%D8%A1-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%AC%D9%88%D8%AF%D9%88-r2558/" rel="">تحريك جسم صلب RigidBody2D بواقعية في الفضاء باستخدام جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" rel="">الطريقة الصحيحة للتواصل بين العقد في جودو Godot</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%A5%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D9%86%D9%82%D8%A7%D8%B7-%D9%88%D8%A7%D9%84%D9%84%D8%B9%D8%A8-%D9%85%D8%AC%D8%AF%D8%AF%D9%8B%D8%A7-%D9%88%D8%AA%D8%A3%D8%AB%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-3d-%D8%B6%D9%85%D9%86-%D8%AC%D9%88%D8%AF%D9%88-r2472/" rel="">إضافة النقاط واللعب مجددًا وتأثيرات الصوت للعبة 3D ضمن جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/python/%D8%A5%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D9%85%D8%A4%D8%AB%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D8%B5%D9%88%D8%AA%D9%8A%D8%A9-%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1%D8%A9-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A8%D8%A7%D9%8A%D8%AB%D9%88%D9%86-%D9%88%D9%85%D9%83%D8%AA%D8%A8%D8%A9-pygame-r2439/" rel="">إضافة المؤثرات الصوتية للعبة المطورة باستخدام بايثون ومكتبة Pygame</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2559</guid><pubDate>Wed, 30 Apr 2025 13:01:01 +0000</pubDate></item><item><title>&#x62A;&#x62D;&#x631;&#x64A;&#x643; &#x633;&#x641;&#x64A;&#x646;&#x629; &#x641;&#x636;&#x627;&#x621; &#x628;&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645; RigidBody2D &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/4.RigidBody2D.png.3f666663fb10c35c8b5eda77af9e3c0b.png" /></p>
<p>
	نشرح في هذا المقال طريقة إنشاء جسم يمثل سفنية تتحرك بطريقة واقعية في الفضاء باستخدام عقدة الجسم الصلب <code>RigidBody2D</code>، حيث يوفر محرك الألعاب جودو أنواعًا مختلفة من الأجسام الفيزيائية ومن ضمنها عقدة <code>RigidBody2D</code> التي تناسب جسمًا يتحرك تلقائيًا وفقًا لقوانين الفيزياء. قد يكون استخدام هذه العقدة مربكًا بعض الشيء لأنها غير قابلة للتحريك المباشر بطريقة مشابهة لعقدة الشخصية <code>CharacterBody2D</code> ومن يتحكم بالكامل هو محرك فيزياء جودو الداخلي Godot physics engine، وبالتالي لا يمكننا ببساطة تغيير موقعها مباشرة من أجل تحريكها بل علينا تطبيق قوى فيزيائية عليها لتحقيق الحركة المطلوبة. 
</p>

<p>
	ننصح قبل بدء العمل بإلقاء نظرة على <a href="https://docs.godotengine.org/en/stable/classes/class_rigidbody2d.html" rel="external nofollow">توثيق الواجهة البرمجية RigidBody2D</a> لفهم خصائصها بشكل أعمق.
</p>

<h2 id="">
	بناء الحركة
</h2>

<p>
	سننشئ <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%88%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-scenes-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2357/" rel="">مشهد</a> جديد ثنائي الأبعاد في جودو ونستخدم فيه العقد التالية:
</p>

<pre class="ipsCode" id="ips_uid_6786_10">RigidBody2D (Ship)
├── Sprite2D
└── CollisionShape2D</pre>

<p>
	وإليكم وظيفة كل عقدة منها:
</p>

<ul>
	<li>
		تمثل <code>RigidBody2D(Ship)</code> العقدة الرئيسية للجسم الصلب الذي نريد تحريكه، والذي يتفاعل مع البيئة ويتأثر بقوانين الفيزياء
	</li>
	<li>
		تمثل العقدة <code>Sprite2D</code> الشكل المرئي للجسم الصلب، وهي في حالتنا صورة سفينة الفضاء التي تتحرك تلقائيًا مع الجسم الفيزيائي الصلب
	</li>
	<li>
		تستخدم العقدة <code>CollisionShape2D</code> لتحديد شكل وحجم منطقة التصادم للجسم الصلب، وهي ضرورية كي تتعرف الفيزياء على حدود الجسم وتتفاعل معه بشكل صحيح عند اصطدامه بأجسام أخرى
	</li>
</ul>

<h3 id="-1">
	توجيه الشخصية
</h3>

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

<p>
	كما سنستخدم المدخلات التالية في <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B9%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D8%AF%D8%AE%D9%84-inputs-actions-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2532/" rel="">خريطة الإدخال Input Map</a>:
</p>

<table>
	<thead>
		<tr>
			<th style="text-align:right;">
				المُدخل
			</th>
			<th style="text-align:right;">
				المفتاح
			</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td style="text-align:right;">
				<code>thrust</code>
			</td>
			<td style="text-align:right;">
				<strong>w</strong> أو ↑
			</td>
		</tr>
		<tr>
			<td style="text-align:right;">
				<code>rotate_right</code>
			</td>
			<td style="text-align:right;">
				<strong>d</strong> أو →
			</td>
		</tr>
		<tr>
			<td style="text-align:right;">
				<code>rotate_left</code>
			</td>
			<td style="text-align:right;">
				<strong>a</strong> أو ←
			</td>
		</tr>
	</tbody>
</table>

<p>
	بعدها، سنضيف سكريبت إلى الجسم الصلب ونعرّف فيه بعض المتغيرات:
</p>

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

</span><span class="lit">@export</span><span class="pln"> var engine_power </span><span class="pun">=</span><span class="pln"> </span><span class="lit">800</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var spin_power </span><span class="pun">=</span><span class="pln"> </span><span class="lit">10000</span><span class="pln">

var thrust </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">.</span><span class="pln">ZERO
var rotation_dir </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span></pre>

<p>
	يحدد أول متغيران كيفية التحكم بحركة سفينة الفضاء. إذ يتحكم المتغير <code>engine_power</code> بالتسارع أي سرعة الحركة للأمام، بينما يتحكم المتغير <code>spin_power</code> بسرعة دوران السفينة. ويُضبط كل من <code>thrust</code> و <code>rotation_dir </code>بناءً على مدخلات المستخدم، وهذا ما سنفعله تاليًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6786_14" style=""><span class="pln">func get_input</span><span class="pun">():</span><span class="pln">
    thrust </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">"thrust"</span><span class="pun">):</span><span class="pln">
      thrust </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
    rotation_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">"rotate_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"rotate_right"</span><span class="pun">)</span></pre>

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

<p>
	يمكن للسفينة الفضائية البدء بالطيران عند تطبيق القيم التالية في الدالة <code>(physics_process(delta_.</code>
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6786_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">
    get_input</span><span class="pun">()</span><span class="pln">
    constant_force </span><span class="pun">=</span><span class="pln"> thrust
    constant_torque </span><span class="pun">=</span><span class="pln"> rotation_dir </span><span class="pun">*</span><span class="pln"> spin_power</span></pre>

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

<p>
	لننتقل إلى خصائص العقدة ومنها إلى <code>Linear</code> ثم <code>Damp</code> و <code>Angular </code>ثم <code>Damp</code> ونضبط قيمتي هاتين الخاصيتين على <code>1</code> و <code>2</code> على الترتيب. تحدّ هاتان القيمتان من سرعة الحركة وسرعة الدوران مما يسبب توقف السفينة الفضائية.
</p>

<p>
	<strong>ملاحظة</strong>: يمكن أن نجرّب تغيير قيم هذه الخاصيات ونرى كيف تتفاعل مع <code>engine_power</code> و <code>spin_power</code>.
</p>

<h3 id="-2">
	العودة إلى الشاشة
</h3>

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

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

<p>
	لننتقل الآن إلى <code>screensize</code> في أعلى السكريبت:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9173_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>()integrate_forces_:</code>
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9173_14" style=""><span class="pln">func _integrate_forces</span><span class="pun">(</span><span class="pln">state</span><span class="pun">):</span><span class="pln">
    var xform </span><span class="pun">=</span><span class="pln"> state</span><span class="pun">.</span><span class="pln">transform
    xform</span><span class="pun">.</span><span class="pln">origin</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">xform</span><span class="pun">.</span><span class="pln">origin</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"> screensize</span><span class="pun">.</span><span class="pln">x</span><span class="pun">)</span><span class="pln">
    xform</span><span class="pun">.</span><span class="pln">origin</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">xform</span><span class="pun">.</span><span class="pln">origin</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"> screensize</span><span class="pun">.</span><span class="pln">y</span><span class="pun">)</span><span class="pln">
    state</span><span class="pun">.</span><span class="pln">transform </span><span class="pun">=</span><span class="pln"> xform</span></pre>

<p>
	نلاحظ أن الدالة <code>()integrate_forces_</code>  تستقبل معاملًا باسم <code>state</code>، وهو كائن من النوع <code><a href="https://docs.godotengine.org/en/stable/classes/class_physicsdirectbodystate2d.html" rel="external nofollow">PhysicsDirectBodyState2D</a></code> يمثل الحالة الفيزيائية الحالية للجسم الصلب. يحتوي هذا الكائن على معلومات مثل موضع الجسم <code>position</code> والقوى المؤثرة <code>forces</code> والسرعة <code>velocity</code> في الاتجاهات المختلفة وغيرها من الخصائص الفيزيائية.<br>
	وبالتالي يمكن أن نحصل من خلال <code>state</code> على التحويل الحالي للجسم  current transform أي موقعه واتجاهه، ثم نعدّل إحداثيات الموقع باستخدام الدالة <code>()wrapf </code>لتطبيق التفاف حول الشاشة مما يعني أن الجسم سيتجاوز حافة الشاشة ويظهر من الجهة المقابلة لإعطاء تأثير الحركة المستمرة. وأخيرًا، نُعيد ضبط التحويل الجديد داخل <code>state </code>لضمان استمرار حركة الجسم بشكل طبيعي وواقعي.
</p>

<p>
	ستبدو الحركة كما في الصورة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170833" href="https://academy.hsoub.com/uploads/monthly_2025_04/01_asteroids_wrap.gif.c6fca805e094dd88df95c831ef8c77f6.gif" rel=""><img alt="01 asteroids wrap" class="ipsImage ipsImage_thumbnailed" data-fileid="170833" data-unique="o6ado0usn" src="https://academy.hsoub.com/uploads/monthly_2025_04/01_asteroids_wrap.gif.c6fca805e094dd88df95c831ef8c77f6.gif"> </a>
</p>

<h3 id="-3">
	الانتقال الفوري
</h3>

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

<p>
	نضيف بداية متغيرًا جديدًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9173_16" style=""><span class="pln">var teleport_pos </span><span class="pun">=</span><span class="pln"> null</span></pre>

<p>
	نحضّر تاليًا موقعًا عشوائيًا ضمن الدالة <code>()get_input</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9173_18" 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">"warp"</span><span class="pun">):</span><span class="pln">
      teleport_pos </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">randf_range</span><span class="pun">(</span><span class="lit">0</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"> randf_range</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> screensize</span><span class="pun">.</span><span class="pln">y</span><span class="pun">))</span></pre>

<p>
	وأخيرًا وضمن الدالة <code>()integrate_forces_</code> سنستخدم <code>teleport_position</code> إن كان موجودًا ومن ثم نمسحه:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9173_20" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> teleport_pos</span><span class="pun">:</span><span class="pln">
      physics_state</span><span class="pun">.</span><span class="pln">transform</span><span class="pun">.</span><span class="pln">origin </span><span class="pun">=</span><span class="pln"> teleport_pos
      teleport_pos </span><span class="pun">=</span><span class="pln"> null</span></pre>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170834" href="https://academy.hsoub.com/uploads/monthly_2025_04/02_asteroids_warp.gif.127929d497d3b7932de657a8c547ed21.gif" rel=""><img alt="02 asteroids warp" class="ipsImage ipsImage_thumbnailed" data-fileid="170834" data-unique="8brmvq25l" src="https://academy.hsoub.com/uploads/monthly_2025_04/02_asteroids_warp.gif.127929d497d3b7932de657a8c547ed21.gif"> </a>
</p>

<p>
	وبهذا نكون قد أضفنا ميزة الانتقال الفوري للمركبة الفضائية بشكل آمن ومتوافق مع نظام فيزياء جودو.
</p>

<h2>
	الخاتمة
</h2>

<p>
	بهذا نكون وصلنا لختام هذا المقال الذي استعرضنا فيه كيفية استخدام محرك الفيزياء في جودو لتحريك الأجسام الصلبة والتحكم بها بشكل دقيق لتوفير تجربة حركة تفاعلية واقعية ضمن بيئات ثنائية وثلاثية الأبعاد مع التحكم الكامل في حركة الأجسام. بإمكانك <a href="https://github.com/godotrecipes/asteroids_physics" rel="external nofollow">تحميل المثال كاملًا</a> عبر مستودعه على جيتهب أو من هنا مباشرة <a class="ipsAttachLink" data-fileext="zip" data-fileid="170835" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=170835&amp;key=fcaacc8cffd676f4f9991da1b3f74b30" rel="">asteroids_physics.zip</a>
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/physics/asteroids_physics/index.html" rel="external nofollow">Asteroids-style Physics (using RigidBody2D)</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<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/%D8%A8%D9%86%D8%A7%D8%A1-%D9%88%D8%AA%D9%86%D9%81%D9%8A%D8%B0-%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-%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%A7%D9%84%D8%B9%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D8%AF%D8%AE%D9%84-inputs-actions-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2532/" rel="">العمل مع إجراءات الدخل Inputs Actions في جودو</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%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-3d-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D8%B1-%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2477/" rel="">تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو</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="">التحريك باستخدام SpriteSheet و AnimationTree StateMachine في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2558</guid><pubDate>Wed, 23 Apr 2025 13:06:00 +0000</pubDate></item><item><title>&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645; &#x627;&#x644;&#x62A;&#x62D;&#x648;&#x64A;&#x644;&#x627;&#x62A; &#x627;&#x644;&#x647;&#x646;&#x62F;&#x633;&#x64A;&#x629; Transform &#x641;&#x64A; &#x627;&#xFEF7;&#x644;&#x639;&#x627;&#x628;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%AA%D8%AD%D9%88%D9%8A%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%87%D9%86%D8%AF%D8%B3%D9%8A%D8%A9-transform-%D9%81%D9%8A-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-r2561/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/Transform.png.ced318145af61433ddf30ec9f35a9894.png" /></p>
<p>
	بدأنا في <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%B1%D9%8A%D8%A7%D8%B6%D9%8A%D8%A9-%D8%A3%D8%B3%D8%A7%D8%B3%D9%8A%D8%A9-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-r2556/" rel="">المقال السابق</a> شرح بعض المفاهيم الرياضية الأساسية التي يحتاج مطور الألعاب لمعرفتها، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء النقطي Dot Product، والجداء الشعاعي Cross Product. وسنستكمل في مقال اليوم شرح مفهوم رياضي مهم وهو التحويلات الهندسية Transforms الذي يسمح لنا بتغيير مكان أو شكل الأشياء في الفضاء باستخدام المصفوفات.
</p>

<h2 id="">
	متطلبات العمل
</h2>

<p>
	قبل المتابعة في قراءة هذا المقال، يجب توفر دراية جيدة عن اﻷشعة vectors وكيفية استخدامها في تطوير اﻷلعاب. لهذا ننصح بالعودة إلى سلسلة مقالات <a href="https://academy.hsoub.com/tags/%D8%A7%D9%84%D8%A3%D8%B4%D8%B9%D8%A9/" rel="">الأشعة</a> على أكاديمية حسوب، ومطالعة مقال <a href="https://kidscancode.org/godot_recipes/4.x/math/dot_cross_product/index.html" rel="external nofollow">رياضيات اﻷشعة</a> ضمن توثيق جودو الرسمي.
</p>

<h2 id="-1">
	التحويلات في المستوي ثنائي البعد
</h2>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170927" href="https://academy.hsoub.com/uploads/monthly_2025_04/001_2d_coordinate_plane.png.bcba9eb026e521866a2678ba984f0006.png" rel=""><img alt="001 2d coordinate plane" class="ipsImage ipsImage_thumbnailed" data-fileid="170927" data-unique="i9i52pvxl" style="width: 350px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_04/001_2d_coordinate_plane.png.bcba9eb026e521866a2678ba984f0006.png"> </a>
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170928" href="https://academy.hsoub.com/uploads/monthly_2025_04/002_2d_rocket.png.0bfb8c4cb4319e156ffa3b542c7a5ac2.png" rel=""><img alt="002 2d rocket" class="ipsImage ipsImage_thumbnailed" data-fileid="170928" data-unique="6xl4but71" style="width: 350px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_04/002_2d_rocket.png.0bfb8c4cb4319e156ffa3b542c7a5ac2.png"> </a>
</p>

<p>
	تشير السفينة هنا إلى نفس اتجاه المحور <code>X</code>، فلو أردنا منها التحرك نحو اﻷمام، نضيف مقدار الحركة إلى الإحداثي اﻷفقي <code>X</code> فتتحرك نحو اليمين:
</p>

<pre class="ipsCode">position += Vector2(10, 0)
</pre>

<p>
	لكن ما الذي يحدث عندما تدور السفينة؟
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170929" href="https://academy.hsoub.com/uploads/monthly_2025_04/003_2d_rocket_rotate.png.8654e3fef38671b3e3366813395bfae6.png" rel=""><img alt="003 2d rocket rotate" class="ipsImage ipsImage_thumbnailed" data-fileid="170929" data-unique="ifahr2m7e" style="width: 350px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_04/003_2d_rocket_rotate.png.8654e3fef38671b3e3366813395bfae6.png"> </a>
</p>

<p>
	كيف يمكن اﻵن تحريكها نحو اﻷمام؟ إن كنتم تتذكرون علم المثلثات في المدرسة، فقد تبدأون بالتفكير في الزوايا والنسب المثلثية sin و cos، ثم تنفذون عملية حسابية مثل:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_479_10" style=""><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">10</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> cos</span><span class="pun">(</span><span class="pln">angle</span><span class="pun">),</span><span class="pln"> </span><span class="lit">10</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> sin</span><span class="pun">(</span><span class="pln">angle</span><span class="pun">))</span></pre>

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

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170930" href="https://academy.hsoub.com/uploads/monthly_2025_04/004_2d_rocket_own_coordinate.png.1c83a80c1c70663e8a659eadff29356d.png" rel=""><img alt="004 2d rocket own coordinate" class="ipsImage ipsImage_thumbnailed" data-fileid="170930" data-unique="udvg3pwqe" style="width: 350px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_04/004_2d_rocket_own_coordinate.png.1c83a80c1c70663e8a659eadff29356d.png"> </a>
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3652_7" style=""><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"> </span><span class="lit">10</span></pre>

<p>
	ينص السطر السابق على إضافة الشعاع <code>X</code> للتحويل مضروبًا بالعدد 10. 
</p>

<p>
	لنشرح هذا الأمر بشيء من التفصيل، تضم الخاصية <code>transform</code> اﻹحداثيين <code>x</code> و <code>y</code> الممثلان للإحداثيات المحلية الخاصة بالعقدة، وهما شعاعي واحدة unit vector أي أن طويلة كل منهما تساوي الواحد. كما يطلق على هذان الشعاعان شعاعي توجيه direction vectors، ويدلان على الاتجاه الذي يشير إليه المحور <code>X</code> الخاص بالسفينة. نضرب بعد ذلك شعاعي التوجيه بالعدد 10 لتكبيرهما والانتقال إلى مسافة أبعد.
</p>

<p>
	<strong>ملاحظة</strong>: تتعلق الخاصية <code>transform</code> لعقدة بالعقدة اﻷم لها أي تنسب إحداثياتها الخاصة إلى إحداثيات العقدة اﻷم. فإن أردنا الحصول على اﻹحداثيات العامة بالنسبة إلى الشاشة، نستخدم <code>global_transform</code>.
</p>

<p>
	تضم الخاصية <code>transform</code> إضافة إلى المحاور المحلية، مكونًا يُدعى شعاع الأصل <code>origin</code> ويمثل اﻹنسحاب translation أو تغيير الموضع.
</p>

<p>
	يمثل الشعاع اﻷزرق في الصور التالية شعاع اﻷصل <code>transform.origin</code> ويساوي شعاع الموضع <code>position</code> للكائن:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170931" href="https://academy.hsoub.com/uploads/monthly_2025_04/005_2d_rocket_origin.png.3c094243f3266f478dd14209054387b8.png" rel=""><img alt="005 2d rocket origin" class="ipsImage ipsImage_thumbnailed" data-fileid="170931" data-unique="bsjbaw3u0" style="width: 350px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_04/005_2d_rocket_origin.png.3c094243f3266f478dd14209054387b8.png"> </a>
</p>

<h3 id="-2">
	التحويل بين الفضاء المحلي والعام
</h3>

<p>
	يمكننا تحويل اﻹحداثيات من الفضاء المحلي للعقدة إلى الفضاء العام عن طريق التحويلات. حيث تضم العقد من النوع <code>Noode2D</code> والنوع <code>Spatial</code> في جودو دوال برمجية مساعدة مثل <code>()to_local</code> و <code>()to_global</code> لتحقيق هذا الأمر:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3652_9" style=""><span class="pln">    var global_position </span><span class="pun">=</span><span class="pln"> to_global</span><span class="pun">(</span><span class="pln">local_position</span><span class="pun">)</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3652_11" style=""><span class="pln">extends </span><span class="typ">Sprite</span><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"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pln"> </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">
      </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"> BUTTON_LEFT</span><span class="pun">:</span><span class="pln">
        printt</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"> to_local</span><span class="pun">(</span><span class="pln">event</span><span class="pun">.</span><span class="pln">position</span><span class="pun">))</span></pre>

<p>
	للمزيد حول آلية التحويل من إحداثيات عامة إلى إحداثيات محلية في محرك جودو ننصحكم بقراءة <a href="https://docs.godotengine.org/en/stable/classes/class_transform2d.html" rel="external nofollow">توثيق <code>Transform2D</code></a> للاطلاع على كافة الخاصيات والتوابع المتاحة.
</p>

<h2 id="-3">
	التحويلات في الفضاء ثلاثي البعد
</h2>

<p>
	يطبق مفهوم التحويل في الفضاء ثلاثي البعد 3D بنفس أسلوب تطبيقه في الفضاء ثنائي البعد 2D، بل يغدو تطبيقها أهم لأن العمل مع الزوايا في الفضاء ثلاثي البعد سيقود إلى مشكلات عديدة كما سنوضح بعد قليل.
</p>

<p>
	ترث العقد ثلاثية البعد من العقدة الأساسية <code>Node3D</code> التي تضم معلومات التحويل. ويحتاج التحويل في الفضاء ثلاثي البعد لمعلومات أكثر مقارنة مع الفضاء الثنائي البعد. حيث يبقى شعاع الموضع <code>Position</code> محفوظًا ضمن الخاصية <code>Origin</code>، لكن الدوران موجود ضمن خاصية تدعى <code>basis</code> تضم ثلاثة أشعة واحدة unit vectors تمثل المحاور اﻹحداثية المحلية الثلاث للعقدة <code>X</code> و <code>Y</code> و <code>Z</code>.
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170932" href="https://academy.hsoub.com/uploads/monthly_2025_04/006_3d_intro_gizmo.png.133fa4d40bd3b68acfda587e5470a5d1.png" rel=""><img alt="006 3d intro gizmo" class="ipsImage ipsImage_thumbnailed" data-fileid="170932" data-unique="731630kzd" src="https://academy.hsoub.com/uploads/monthly_2025_04/006_3d_intro_gizmo.png.133fa4d40bd3b68acfda587e5470a5d1.png"> </a>
</p>

<h3 id="-4">
	تفعيل نمط الفضاء المحلي Local Space
</h3>

<p>
	لنتذكر أن الفضاء العام Global Space هو الفضاء الذي يعتمد على محاور المشهد العامة. بمعنى آخر، إذا كنا نحرك أو ندير كائنًا في هذا الفضاء، فإن تحركاته ستكون بناءً على محاور العالم أو المشهد الذي يوجد فيه هذا الكائن، أما الفضاء المحلي Local Space فهو الفضاء الذي يعتمد على محاور الكائن نفسه. أي أن للكائن لديه محاور خاصة به مثل المحور <code>X</code> و <code>Y</code> و <code>Z</code> الخاص به وعندما نحرك أو ندير الكائن في الفضاء المحلي، فإن تحركاته تكون بالنسبة له هو، وليس بالنسبة للمشهد بأكمله.
</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-%D9%85%D8%AD%D8%B1%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2183/" rel="">محرر جودو</a> عرض الاتجاهات المحلية للجسم والتعامل معها بسهولة، وذلك من خلال تفعيل خيار Local Space Mode، مما يسمح بتحريك الجسم أو تدويره وفقًا لمحاوره الخاصة بدلًا من محاور المشهد العامة، وستمثل المحاور الثلاث الملونة في هذا الوضع المحاور اﻷساسية المحلية للجسم.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170933" href="https://academy.hsoub.com/uploads/monthly_2025_04/007_3d_intro_local_space.png.a5655a5db13a081ceeb627a66847dadc.png" rel=""><img alt="007 3d intro local space" class="ipsImage ipsImage_thumbnailed" data-fileid="170933" data-unique="j66xmej3m" src="https://academy.hsoub.com/uploads/monthly_2025_04/007_3d_intro_local_space.png.a5655a5db13a081ceeb627a66847dadc.png"> </a>
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_479_18" style=""><span class="pln">    position </span><span class="pun">+=</span><span class="pln"> </span><span class="pun">-</span><span class="pln">transform</span><span class="pun">.</span><span class="pln">basis</span><span class="pun">.</span><span class="pln">z </span><span class="pun">*</span><span class="pln"> speed </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	<strong>تلميح</strong><span>: </span>يمتلك جودو قيم معرّفة افتراضيًا لبعض الاتجاهات الشائعة، على سبيل المثال يمثل الاختصار <code>Vector3.FORWARD</code> الاتجاه الأمامي في الفضاء ثلاثي الأبعاد:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8581_8" style=""><span class="typ">Vector3</span><span class="pun">.</span><span class="pln">FORWARD </span><span class="pun">==</span><span class="pln"> </span><span class="typ">Vector3</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="pun">-</span><span class="lit">1</span><span class="pun">)</span></pre>

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

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

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

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%B1%D9%8A%D8%A7%D8%B6%D9%8A%D8%A9-%D8%A3%D8%B3%D8%A7%D8%B3%D9%8A%D8%A9-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-r2556/" rel="">مفاهيم رياضية أساسية في تطوير اﻷلعاب</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%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-3d-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D8%B1-%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2477/" rel="">تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2561</guid><pubDate>Mon, 21 Apr 2025 16:00:00 +0000</pubDate></item><item><title>&#x645;&#x641;&#x627;&#x647;&#x64A;&#x645; &#x631;&#x64A;&#x627;&#x636;&#x64A;&#x629; &#x623;&#x633;&#x627;&#x633;&#x64A;&#x629; &#x641;&#x64A; &#x62A;&#x637;&#x648;&#x64A;&#x631; &#x627;&#xFEF7;&#x644;&#x639;&#x627;&#x628;</title><link>https://academy.hsoub.com/programming/game-development/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%B1%D9%8A%D8%A7%D8%B6%D9%8A%D8%A9-%D8%A3%D8%B3%D8%A7%D8%B3%D9%8A%D8%A9-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-r2556/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/Interpolation_.png.dcb1f1f527b7eed3e61a7149bf19f5eb.png" /></p>
<p>
	يحتاج أي مطور ألعاب لمعرفة بعض المفاهيم الرياضية الأساسية ليتحكم في انتقال الشخصيات من حالة لأخرى بانسيابية ويتحكم في اتجاهاتها وتمكينها من معرفة ماذا يوجد أمامها وخلفها، وسنناقش في مقال اليوم مفاهيم تستخدم بكثرة في تطوير اﻷلعاب، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء الداخلي أو النقطي أو السلمي Dot Product، والجداء الخارجي أو الشعاعي Cross Product.<br>
	قد تبدو هذه المصطلحات مقعدة وغامضة لمن يسمعها لأول مرة، لكن لا داعي للقلق فبعد قراءة هذا المقال ومعرفة تطبيقاتها العملية في برمجة اﻷلعاب ستغدو سهلة وبسيطة.
</p>

<h2 id="-1">
	الاستيفاء العددي
</h2>

<p>
	تُعطى الصيغة اﻷساسية للاستيفاء الخطي رياضيًا كالتالي:
</p>

<pre class="ipsCode">func lerp(a, b, t):
    return (1 - t) * a + t * b
</pre>

<p>
	يمثل كل من <code>a</code> و <code>b</code> قيمتين، بينما تمثل <code>t</code> مقدار الاستيفاء بينهما أي النسبة التي تحدد إلى أي درجة ننتقل من a إلى b. وتتراوح قيم <code>t</code> نمطيًا بين <code>0</code> وعندها سيعيد الاستيفاء قيمة <code>a</code>، وبين القيمة <code>1</code> وعندها سيعيد الاستيفاء قيمة <code>b</code>. يعطي تابع الاستيفاء قيمة ما بين <code>a</code> و <code>b</code> كما في المثال التالي:
</p>

<pre class="ipsCode">x = lerp(0, 1, 0.75) # x is 0.75
x = lerp(0, 100, 0.5) # x is 50
x = lerp(10, 75, 0.3) # x is 29.5
x = lerp(30, 2, 0.75) # x is 9
</pre>

<p>
	يُدعى هذا الاستيفاء بالاستيفاء الخطي لأنه يعدّ المسافة بين نقطتي الاستيفاء خطًا مستقيمًا.
</p>

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

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

var time </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
var duration </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2</span><span class="pln"> </span><span class="com"># المدة الزمنية للتأثير</span><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"> time </span><span class="pun">&lt;</span><span class="pln"> duration</span><span class="pun">:</span><span class="pln">
      time </span><span class="pun">+=</span><span class="pln"> delta
      modulate</span><span class="pun">.</span><span class="pln">a </span><span class="pun">=</span><span class="pln"> lerp</span><span class="pun">(</span><span class="lit">1</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">,</span><span class="pln"> time </span><span class="pun">/</span><span class="pln"> duration</span><span class="pun">)</span><span class="pln">
      scale </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"> lerp</span><span class="pun">(</span><span class="lit">1</span><span class="pun">,</span><span class="pln"> </span><span class="lit">5</span><span class="pun">,</span><span class="pln"> time </span><span class="pun">/</span><span class="pln"> duration</span><span class="pun">)</span></pre>

<h2 id="-2">
	الاستيفاء الشعاعي
</h2>

<p>
	من الممكن الاستيفاء أيضًا بين شعاعين وهذا يعني إيجاد شعاع جديد يقع بينهما بناءً على مقدار معين <code>t</code> تمامًا مثل الاستيفاء بين رقمين، لكن هنا نتعامل مع اتجاهات أو مواقع في الفضاء، إذ توفر كلا العقدتين <code>Vector2</code> و <code>Vector3</code> التابع <code>()linear_interpolate</code> لتنفيذ اﻷمر.
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4106_12" style=""><span class="pln">var forward </span><span class="pun">=</span><span class="pln"> </span><span class="pun">-</span><span class="pln">transform</span><span class="pun">.</span><span class="pln">basis</span><span class="pun">.</span><span class="pln">z
var left </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">basis</span><span class="pun">.</span><span class="pln">x
var forward_left </span><span class="pun">=</span><span class="pln"> forward</span><span class="pun">.</span><span class="pln">linear_interpolate</span><span class="pun">(</span><span class="pln">left</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.5</span><span class="pun">)</span></pre>

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

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

var target

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="pln"> </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">
      target </span><span class="pun">=</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">position

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"> 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">linear_interpolate</span><span class="pun">(</span><span class="pln">target</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.1</span><span class="pun">)</span></pre>

<h2 id="-3">
	الجداء الشعاعي الداخلي والخارجي
</h2>

<p>
	يمكن تنفيذ عمليتي جداء على اﻷشعة هما الجداء الداخلي السلمي أو النقطي dot product والذي تكون نتيجته عدد، والجداء الخارجي أو الشعاعي والذي تكون نتيجته شعاعًا.
</p>

<h3 id="-4">
	الجداء الداخلي
</h3>

<p>
	هو عملية حسابية على شعاعين تكون نتيجته عدد حقيقي، وتمثل عادة على أنها مسقط شعاع A على حامل الشعاع اﻵخر B:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170752" href="https://academy.hsoub.com/uploads/monthly_2025_04/01_dot_cross.png.9ff842a8033c6aba159b5497175e89eb.png" rel=""><img alt="01 dot cross" class="ipsImage ipsImage_thumbnailed" data-fileid="170752" data-unique="eok1y6spv" src="https://academy.hsoub.com/uploads/monthly_2025_04/01_dot_cross.png.9ff842a8033c6aba159b5497175e89eb.png"> </a>
</p>

<p>
	تُعطى الصيغة الرياضية للجداء الداخلي بالعلاقة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170753" href="https://academy.hsoub.com/uploads/monthly_2025_04/02_dot_product.png.884309cd47e3df40df087ca523ae1321.png" rel=""><img alt="02 dot product" class="ipsImage ipsImage_thumbnailed" data-fileid="170753" data-unique="lx1fu9due" src="https://academy.hsoub.com/uploads/monthly_2025_04/02_dot_product.png.884309cd47e3df40df087ca523ae1321.png"> </a>
</p>

<p>
	حيث:
</p>

<ul>
	<li>
		<code>θ</code> : هي الزاوية بين الشعاعين
	</li>
	<li>
		<code>||A||</code>: طويلة الشعاع اﻷول
	</li>
	<li>
		<code>||B||</code>:طويلة الشعاع الثاني
	</li>
</ul>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170754" href="https://academy.hsoub.com/uploads/monthly_2025_04/03_dot_product_normalized.png.b12b36bd7d5a43479e72cc02cb21abbf.png" rel=""><img alt="03 dot product  normalized" class="ipsImage ipsImage_thumbnailed" data-fileid="170754" data-unique="w7fwz0q14" src="https://academy.hsoub.com/uploads/monthly_2025_04/03_dot_product_normalized.png.b12b36bd7d5a43479e72cc02cb21abbf.png"> </a>
</p>

<p>
	تشير هذه العلاقة إلى الارتباط المباشر بين الجداء الداخلي والزاوية بين الشعاعين، وطالما أن <code>cos(0)=1</code> و <code>cos(180)=-1</code> ستدل قيمة الجداء السلمي على اتجاه الشعاعين بالنسبة لبعضهما، فهما في الزاوية 0 منطبقان وفي الزاوية 180 في اتجاهين مختلفين:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170755" href="https://academy.hsoub.com/uploads/monthly_2025_04/04_dot_product_angle.png.853a1102e96892efb2b8e702918523bf.png" rel=""><img alt="04 dot product angle" class="ipsImage ipsImage_thumbnailed" data-fileid="170755" data-unique="pi0d2in8b" src="https://academy.hsoub.com/uploads/monthly_2025_04/04_dot_product_angle.png.853a1102e96892efb2b8e702918523bf.png"> </a>
</p>

<p>
	وسنرى في فقرة قادمة كيف نستفيد من هذا الجداء عمليًا.
</p>

<h3 id="-5">
	الجداء الخارجي
</h3>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170756" href="https://academy.hsoub.com/uploads/monthly_2025_04/05_cross_product.gif.2d29079c05e2397952986c43366a71ce.gif" rel=""><img alt="05 cross product" class="ipsImage ipsImage_thumbnailed" data-fileid="170756" data-unique="w5mzy4kcu" src="https://academy.hsoub.com/uploads/monthly_2025_04/05_cross_product.gif.2d29079c05e2397952986c43366a71ce.gif"> </a>
</p>

<p>
	تعطى طويلة الشعاع الناتج عن الجداء الخارجي بالعلاقة:
</p>

<pre class="ipsCode">A x B = ||A||.||B||.sin(θ) //هي الزاوية بين الشعاعين θ
</pre>

<p>
	وإن كانت طويلة كل من الشعاعين هي الواحد ستكون نتيجة الحساب أبسط، إذ تكون طويلة الشعاع الناتج قيمة بين <code>1-</code> و <code>1</code>.
</p>

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

<p>
	من ناحية أخرى، في أطر العمل ثنائية البعد ومن ضمنها جودو، لا يمكن تمثيل الشعاع العمودي داخل نفس المستوى، وبالتالي عند استخدام التابع <code>Vectro2.cross</code> في جودو، فإنه لا يُرجع شعاعًا جديدًا، بل عددًا يمثل طول الشعاع العمودي على الشعاعين في اتجاه الفضاء الثالث أو المحور z وتكون قيمته بين <code>1-</code> و <code>1</code> وتعكس مدى التعامد بين الشعاعين.
</p>

<h3 id="-6">
	تطبيقات عملية
</h3>

<p>
	لنلق نظرة على الصورة المتحركة التالية التي تمثل نتيجة جداء خارجي وداخلي لشعاعين <code>()Vector2.dot</code> و <code>()Vector2.cross</code> وكيف تتغير كل نتيجة مع تغير الزاوية بين الشعاعين:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170757" href="https://academy.hsoub.com/uploads/monthly_2025_04/06_dot_cross.gif.487a6b650d30d1552cfb8bdea03c0722.gif" rel=""><img alt="06 dot cross" class="ipsImage ipsImage_thumbnailed" data-fileid="170757" data-unique="8i3sk25vi" src="https://academy.hsoub.com/uploads/monthly_2025_04/06_dot_cross.gif.487a6b650d30d1552cfb8bdea03c0722.gif"> </a>
</p>

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

<p>
	كما يساعد الجداء الخارجي في معرفة إن كان الكائن إلى اليسار -عندما تكون قيمة الجداء أكبر من الصفر- أو إلى اليمين -عندما تكون قيمة الجداء أصغر من الصفر-.
</p>

<h2>
	الخاتمة
</h2>

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

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/math/interpolation/index.html" rel="external nofollow">Interpolation</a> و <a href="https://kidscancode.org/godot_recipes/4.x/math/dot_cross_product/index.html" rel="external nofollow">Vectors:Using Dot product and Cross product</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<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%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-%D9%84%D9%84%D8%AA%D9%88%D8%AC%D9%87-%D9%86%D8%AD%D9%88-%D9%87%D8%AF%D9%81-%D9%88%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%83-%D9%86%D8%AD%D9%88%D9%87-r2547/" rel="">استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/design/3d/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D8%AC%D8%AF%D8%A7%D8%A1-%D8%A7%D9%84%D9%86%D9%82%D8%B7%D9%8A-dot-product-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D8%A3%D8%B4%D8%B9%D8%A9-%D9%81%D9%8A-%D8%A7%D9%84%D8%AA%D8%B5%D8%A7%D9%85%D9%8A%D9%85-3d-r862/" rel="">تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D</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%85%D8%AD%D8%B1%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2183/" rel="">تعرف على محرر محرك اﻷلعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2556</guid><pubDate>Thu, 17 Apr 2025 16:00:00 +0000</pubDate></item><item><title>&#x627;&#x644;&#x62A;&#x641;&#x627;&#x639;&#x644; &#x628;&#x64A;&#x646; &#x627;&#x644;&#x634;&#x62E;&#x635;&#x64A;&#x627;&#x62A; &#x648;&#x627;&#x644;&#x623;&#x62C;&#x633;&#x627;&#x645; &#x627;&#x644;&#x635;&#x644;&#x628;&#x629; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/3..png.79462c493bfe54346cba17c4a5779684.png" /></p>
<p>
	نشرح في هذا المقال طريقة تفاعل شخصية اللاعب في جودو مع الأجسام الصلبة الموجودة في المشهد. ويمكن أن نطبق الطريقة التي سنشرحها على الفضائين الثنائي والثلاثي البعد على حد سواء.
</p>

<h2 id="-1">
	التفاعل مع الأجسام الصلبة
</h2>

<p>
	إذا جربنا استخدام العقدة <code>CharacterBody2D </code>في <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">محرك الألعاب</a> جودو فسنجد أن العقدة <code>CharacterBody2D</code> التي تتحرك افتراضيًا من خلال تنفيذ أحد التابعين <code>()move_and_slide</code> أو <code>()move_and_collide</code> تصطدم بالأجسام الصلبة الفيزيائية من حولها لكنها لا تتمكن من دفع أي جسم تتصادم معه مثل صندوق أو عدو، فلن تتفاعل عقدة الجسم الصلب مع عقدة اللاعب إطلاقًا وستسلك سلوك العقدة <code>StaticBody2D</code>.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170824" href="https://academy.hsoub.com/uploads/monthly_2025_04/01_char_push_default.gif.2b826465226287485f0b16e6ba9128af.gif" rel=""><img alt="01 char push default" class="ipsImage ipsImage_thumbnailed" data-fileid="170824" data-unique="6z7ys736y" src="https://academy.hsoub.com/uploads/monthly_2025_04/01_char_push_default.gif.2b826465226287485f0b16e6ba9128af.gif"> </a>
</p>

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

<p>
	سنستخدم في هذا المثال شخصية ثنائية البعد يمكن تحميلها مع الموارد الأخرى للعبة من <a href="https://github.com/godotrecipes/character_vs_rigid" rel="external nofollow">هذا المستودع</a>، كما سنستخدم أكثر توابع الحركة شيوعًا لتحريك اللاعب وهو التابع <code><a href="https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html#class-characterbody2d-method-move-and-slide" rel="external nofollow">()move_and_slide</a></code> الذي يستطيع تحريك الكائنات من نوع <code>CharacterBody2D </code>بشكل آمن ومتوافق مع نظام الفيزياء، حيث يتكفّل بإدارة الاصطدامات والانزلاق على الأسطح والتفاعل مع الجاذبية بشكل تلقائي دون الحاجة لكتابة منطق فيزيائي معقد.
</p>

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

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

<h3 id="-2">
	العطالة اللانهائية
</h3>

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

<p>
	لتوضيح الأمر، عرّفنا في مثالنا ثلاث طبقات فيزيائية
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170821" href="https://academy.hsoub.com/uploads/monthly_2025_04/02_2d_physics_layers_01.png.c4112a7868a59e1d657482df8e44b6db.png" rel=""><img alt="02 2d physics layers 01" class="ipsImage ipsImage_thumbnailed" data-fileid="170821" data-unique="4b08vdbaa" src="https://academy.hsoub.com/uploads/monthly_2025_04/02_2d_physics_layers_01.png.c4112a7868a59e1d657482df8e44b6db.png"> </a>
</p>

<p>
	ووضعنا الجسم الصلب ضمن الطبقة رقم 3 وأبقينا على القناع كما هو لتقنيع كل الطبقات:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170822" href="https://academy.hsoub.com/uploads/monthly_2025_04/03_physics_layers_box.png.405a3f4691856f71bd68790699d78bc7.png" rel=""><img alt="03 physics layers box" class="ipsImage ipsImage_thumbnailed" data-fileid="170822" data-unique="fzaafrvhg" src="https://academy.hsoub.com/uploads/monthly_2025_04/03_physics_layers_box.png.405a3f4691856f71bd68790699d78bc7.png"> </a>
</p>

<p>
	وضعنا بعد ذلك اللاعب في الطبقة الثانية وهي الطبقة player وضبطنا القناع ليتجاهل العناصر الأخرى.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170823" href="https://academy.hsoub.com/uploads/monthly_2025_04/04_physics_layers_player.png.a4d5c4081c189de56e975a987895d322.png" rel=""><img alt="04 physics layers player" class="ipsImage ipsImage_thumbnailed" data-fileid="170823" data-unique="qg96sb2ju" src="https://academy.hsoub.com/uploads/monthly_2025_04/04_physics_layers_player.png.a4d5c4081c189de56e975a987895d322.png"> </a>
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170825" href="https://academy.hsoub.com/uploads/monthly_2025_04/05_char_push_inf.gif.c8223e3e06b532a7f2ea6125e30107e1.gif" rel=""><img alt="05 char push inf" class="ipsImage ipsImage_thumbnailed" data-fileid="170825" data-unique="q6nauatfm" src="https://academy.hsoub.com/uploads/monthly_2025_04/05_char_push_inf.gif.c8223e3e06b532a7f2ea6125e30107e1.gif"> </a>
</p>

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

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

<h3 id="-3">
	تطبيق الاندفاعات
</h3>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7823_7" style=""><span class="com"># عطالة اللاعب</span><span class="pln">
var push_force </span><span class="pun">=</span><span class="pln"> </span><span class="lit">80.0</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"># move_and_slide() بعد استدعاء</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> i </span><span class="kwd">in</span><span class="pln"> get_slide_collision_count</span><span class="pun">():</span><span class="pln">
      var c </span><span class="pun">=</span><span class="pln"> get_slide_collision</span><span class="pun">(</span><span class="pln">i</span><span class="pun">)</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> c</span><span class="pun">.</span><span class="pln">get_collider</span><span class="pun">()</span><span class="pln"> </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">RigidBody2D</span><span class="pun">:</span><span class="pln">
        c</span><span class="pun">.</span><span class="pln">get_collider</span><span class="pun">().</span><span class="pln">apply_central_impulse</span><span class="pun">(-</span><span class="pln">c</span><span class="pun">.</span><span class="pln">get_normal</span><span class="pun">()</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> push_force</span><span class="pun">)</span></pre>

<p>
	يتجه ناظم التصادم collision normal خارج الجسم الصلب، لهذا عكسناه ليتجه بعكس اتجاه الشخصية ويُطبّق العامل <code>push_force</code>. وهكذا ستدفع الشخصية الصناديق مجددًا لكنها لن تجبر الصناديق عندما تدفعها نحو الجدار على تسلقه.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170826" href="https://academy.hsoub.com/uploads/monthly_2025_04/06_char_push_impulse.gif.14239bc0548006178a9d585a3072c6f4.gif" rel=""><img alt="06 char push impulse" class="ipsImage ipsImage_thumbnailed" data-fileid="170826" data-unique="5arcuxd42" src="https://academy.hsoub.com/uploads/monthly_2025_04/06_char_push_impulse.gif.14239bc0548006178a9d585a3072c6f4.gif"> </a>
</p>

<p>
	 
</p>

<h2>
	الخاتمة
</h2>

<p>
	تعرفنا في هذا المقال على كيفية تفاعل شخصية اللاعب مع الأجسام الصلبة، واستعرضنا طريقتين أساسيتين لتحقيق دفع الشخصية لهذه الأجسام إما بتجاهل الفيزياء باستخدام العطالة اللانهائية، أو بتطبيق الاندفاعات للحصول على سلوك واقعي. يعتمد اختيار الطريقة الأنسب على طبيعة اللعبة والتجربة التي نرغب في تقديمها للاعب لتمنحه سلوكًا منطقيًا. بإمكانك <a href="https://github.com/godotrecipes/character_vs_rigid" rel="external nofollow">تحميل المشروع</a> كاملًا من مستودعه على جيتهب أو مباشرة من هنا <a class="ipsAttachLink" data-fileext="zip" data-fileid="170829" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=170829&amp;key=ff459f58612dab7cc459c62f7ce931e9" rel="">character_vs_rigid.zip</a>.
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/physics/character_vs_rigid/index.html" rel="external nofollow">Character to rigid Body interaction</a>
</p>

<ul>
	<li>
		<h2>
			اقرأ أيضًا
		</h2>
	</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%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-%D9%84%D9%84%D8%AA%D9%88%D8%AC%D9%87-%D9%86%D8%AD%D9%88-%D9%87%D8%AF%D9%81-%D9%88%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%83-%D9%86%D8%AD%D9%88%D9%87-r2547/" rel="">استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%A5%D8%B4%D8%A7%D8%B1%D8%A7%D8%AA-signals-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2385/" rel="">استخدام الإشارات Signals في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%AE%D8%B1%D8%A7%D8%A6%D8%B7-%D9%85%D8%B5%D8%BA%D8%B1%D8%A9-minimap-%D9%84%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2549/" rel="">إنشاء خرائط مصغرة MiniMap للألعاب في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2557</guid><pubDate>Wed, 16 Apr 2025 13:00:00 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x62E;&#x631;&#x627;&#x626;&#x637; &#x645;&#x635;&#x63A;&#x631;&#x629; MiniMap &#x644;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D8%AE%D8%B1%D8%A7%D8%A6%D8%B7-%D9%85%D8%B5%D8%BA%D8%B1%D8%A9-minimap-%D9%84%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2549/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/_.png.688137662d82fb2a34c4acbe9e6c8563.png" /></p>
<p>
	الخرائط المصغرة هي عبارة عن واجهات رسومية صغيرة تظهر في زاوية شاشة اللعب، تعرض تمثيلًا مصغرًا لخريطة اللعبة الكاملة أو المنطقة المحيطة باللاعب. لتساعد على تحديد موقعنا داخل اللعبة وترينا أماكن الأعداء أو الأشياء المهمة من حولنا، فهي تعمل كنظام رادار لكشف الأعداء والأهداف المخفية وتوفر صورة عامة عن بيئة اللعب ككل. سنبني في هذا المقال خريطة مصغرة تعرض موقع اﻷشياء الواقعة خارج مجال رؤية اللاعب بشكل نقاط أو أيقونات صغيرة وسنحدث مواقع هذه النقاط كلما تحرك اللاعب.
</p>

<h2 id="-1">
	إعداد المشروع
</h2>

<p>
	سننشئ لعبة تخطيطها من اﻷعلى إلى اﻷسفل وسنستخدم ميزة <a href="https://kidscancode.org/godot_recipes/3.x/2d/autotile_intro/index.html" rel="external nofollow">Autotile</a> في محرك الألعاب جودو فهي تُسهّل كثيراً عملية رسم الخرائط باستخدام عناصر رقعة Tiles متداخلة وتمكننا من رسم الجدران أو الأرضيات بحرية، حيث يختار المحرك تلقائيًا الرقعة المناسبة من مجموعة الرقع حتى تتطابق الحواف والزوايا مع الرقع المجاورة، ويمكن <span ipsnoautolink="true">تنزيل الصور المطلوبة</span> لتطبيق المقال من مجلد assets من <a href="https://kidscancode.org/godot_recipes/4.x/files/minimap_assets.zip" rel="external nofollow">هذا الرابط </a>أو من<a class="ipsAttachLink" data-fileext="zip" data-fileid="170396" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=170396&amp;key=490704afe3d2c37d5867659da82aeb24" rel="">minimap_assets.zip</a>
</p>

<p>
	سيبدو المشهد الرئيسي للعبة كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170384" href="https://academy.hsoub.com/uploads/monthly_2025_04/01_minimap_main_scene.png.a678ae858b80264af4812709f3e3cf61.png" rel=""><img alt="01 minimap main scene" class="ipsImage ipsImage_thumbnailed" data-fileid="170384" data-unique="eyn7j7sac" src="https://academy.hsoub.com/uploads/monthly_2025_04/01_minimap_main_scene.png.a678ae858b80264af4812709f3e3cf61.png"> </a>
</p>

<p>
	نستخدم العقدة <code>CanvasLayer</code> لتجميع عناصر واجهة المستخدم بما في ذلك الخريطة المصغرة التي سننشؤها في هذا المقال، ونستخدم العقدة <code>TileMap </code>لرسم الخريطة باستخدام الرقع tiles، بينما نستخدم العقدة <code>Player</code> لتمثيل شخصية اللاعب.
</p>

<h2 id="-2">
	تخطيط واجهة المستخدم
</h2>

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

<p>
	نضيف تاليًا العقدة <code>NinePatchRect</code> وهي مشابهة للعقدة <code>TextureRect</code> لكنها تتعامل مع تغيير اﻷبعاد بطريقة مختلفة، إذ لا تمدد الزوايا أو الحواف مما يحافظ على مظهر الصورة بشكل أفضل عند تغيير الأبعاد. نفلت الصورة <code>panel_woodDetail_blank.png</code> من مجلد assets في لعبتنا في الخاصية <code>Texture</code>، وهي صورة أبعادها 128x128 بكسل، لكن إن غيرنا أبعاد العقدة <code>MarginContainer</code> ستصبح الصورة ممددة وسيئة المظهر.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170393" href="https://academy.hsoub.com/uploads/monthly_2025_04/02_minimap_UI.gif.a7687e8eee856f73006899fb1f080bb2.gif" rel=""><img alt="02 minimap ui" class="ipsImage ipsImage_thumbnailed" data-fileid="170393" data-unique="g94g05yy4" src="https://academy.hsoub.com/uploads/monthly_2025_04/02_minimap_UI.gif.a7687e8eee856f73006899fb1f080bb2.gif"> </a>
</p>

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

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170394" href="https://academy.hsoub.com/uploads/monthly_2025_04/03_minimap_UI_No_Streach.gif.e060cfde10aef308ace4042725e98a0f.gif" rel=""><img alt="03 minimap ui no streach" class="ipsImage ipsImage_thumbnailed" data-fileid="170394" data-unique="306dexhsl" src="https://academy.hsoub.com/uploads/monthly_2025_04/03_minimap_UI_No_Streach.gif.e060cfde10aef308ace4042725e98a0f.gif"> </a>
</p>

<p>
	علينا تاليًا ملء الجزء الداخلي من اﻹطار بنمط يمثل شبكة وذلك باستخدام الصورة <code>pattern_blueprintPaper.png</code>:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170385" href="https://academy.hsoub.com/uploads/monthly_2025_04/04_pattern_blueprintPaper.png.d86ffe333c386c7a19b2d5d74e4b64c2.png" rel=""><img alt="04 pattern blueprintpaper" class="ipsImage ipsImage_thumbnailed" data-fileid="170385" data-unique="0yksihhcj" src="https://academy.hsoub.com/uploads/monthly_2025_04/04_pattern_blueprintPaper.png.d86ffe333c386c7a19b2d5d74e4b64c2.png"> </a>
</p>

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

<p>
	لهذا، نضيف عقدة جديدة <code>MarginContainer</code> كابن للعقدة <code>MiniMap</code> وشقيق للعقدة <code>Frame</code> ثم نضبط خاصيات  <code>Constants</code>  في القسم <code>Theme Overrides </code>على القيمة <code>20</code>. نضيف بعد ذلك عقدة <code>TectureRect</code> كابن للعقدة السابقة ثم نضبط قيمة الخاصية <code>Texure</code> على نفس الصورة السابقة، والخاصية <code>Strech Mode</code> على <code>Tile</code> ونسمي العقدة أخيرًا <code>Grid</code>. لنجرّب تغيير أبعاد العقدة اﻷصلية لرؤية تأثير ما فعلناه حتى اللحظة:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170395" href="https://academy.hsoub.com/uploads/monthly_2025_04/05_minimap_UI_Grid_streach.gif.8b617f1e13c28362f834023eada4a2ae.gif" rel=""><img alt="05 minimap ui grid streach" class="ipsImage ipsImage_thumbnailed" data-fileid="170395" data-unique="6ugn78gi1" src="https://academy.hsoub.com/uploads/monthly_2025_04/05_minimap_UI_Grid_streach.gif.8b617f1e13c28362f834023eada4a2ae.gif"> </a>
</p>

<p>
	لنبق أبعاد الخريطة المصغرة حاليًا على 200x200 بكسل، وبإمكاننا التأكد من هذه اﻷبعاد من الخاصية <code>Size</code> في القسم <code>Layout</code>. ستبدو اﻵن شجرة المشهد كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170386" href="https://academy.hsoub.com/uploads/monthly_2025_04/06_scene_tree.png.ff2047322cff9ba04ffe6650437e2226.png" rel=""><img alt="06 scene tree" class="ipsImage ipsImage_thumbnailed" data-fileid="170386" data-unique="pnwiy011l" src="https://academy.hsoub.com/uploads/monthly_2025_04/06_scene_tree.png.ff2047322cff9ba04ffe6650437e2226.png"> </a>
</p>

<h2 id="-3">
	نقاط علام الخريطة
</h2>

<p>
	سنضيف للخريطة نقاط العلام Marker ترمز  كل منها لأشياء معينة في اللعبة، نضيف أولًا عقدة من النوع <code>Sprite2D</code> كابن للعقدة <code>Grid</code> ونسميها <code>PlayerMarker</code> لتمثل اللاعب ونمنحها الصورة <code>minimapIcon_arrowA.png</code>، وننتبه إلى أن قيمة الخاصية <code>Position</code> في القسم <code>Transform </code>هي <code>(0,0)</code>مما يجعلها في الزاوية العليا اليسارية من العقدة <code>Grid</code>.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170387" href="https://academy.hsoub.com/uploads/monthly_2025_04/07_plaayer_marker_top_left.png.846faac3ab9f6ab6959093cb9ee0194c.png" rel=""><img alt="07 plaayer marker top left" class="ipsImage ipsImage_thumbnailed" data-fileid="170387" data-unique="ldch9nwgc" src="https://academy.hsoub.com/uploads/monthly_2025_04/07_plaayer_marker_top_left.png.846faac3ab9f6ab6959093cb9ee0194c.png"> </a>
</p>

<p>
	إن كانت أبعاد الشبكة <code>(150,150)</code>سيكون مركزها عند <code>(75,75)</code>، لنضبط إذًا موقع <code>PlayerMarker</code> على تلك القيمة، ننوه أن هذه العملية ستكون آلية لاحقًا.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170388" href="https://academy.hsoub.com/uploads/monthly_2025_04/08_player_marker_center.png.a4950121e5d9de18568572bba93fd686.png" rel=""><img alt="08 player marker center" class="ipsImage ipsImage_thumbnailed" data-fileid="170388" data-unique="ncwfp5ywi" src="https://academy.hsoub.com/uploads/monthly_2025_04/08_player_marker_center.png.a4950121e5d9de18568572bba93fd686.png"> </a>
</p>

<p>
	نضيف اﻵن عقدتين جديدتين من نوع <code>Sprite2D</code> ونسميهما <code>AlertMarker</code> و <code>MobMarker</code> ونمنحهما الصورتين <code>minimapIcon_jewelRed.png</code> التي تمثل جوهرة حمراء و <code>minimapIcon_exclamationYellow.png</code> التي تمثل علامة تعجب صفراء كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170389" href="https://academy.hsoub.com/uploads/monthly_2025_04/09_three_markers.png.8833f5ea143f49cba0f414c44eae7c18.png" rel=""><img alt="09 three markers" class="ipsImage ipsImage_thumbnailed" data-fileid="170389" data-unique="nym5k78q8" src="https://academy.hsoub.com/uploads/monthly_2025_04/09_three_markers.png.8833f5ea143f49cba0f414c44eae7c18.png"> </a>
</p>

<p>
	تمثل العقدتان السابقتان نوعين جديدن من نقاط العلام في عالم اللعبة. ننقر على الزر Toggle Visibility المجاور لكل منهما كي لا تظهر العقدة افتراضيًا.
</p>

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

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

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

<p>
	نضيف كل كائن نريده أن يظهر على الخريطة ضمن المجموعة <code>minimap_objects</code> ثم نضبط المتغير <code>minimap_icon</code> في سكريبت كل كائن على القيمة المناسبة من المجموعة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_7" style=""><span class="com"># mob في سكريبت:</span><span class="pln">
var minimap_icon </span><span class="pun">=</span><span class="pln"> </span><span class="str">"mob"</span><span class="pln">

</span><span class="com"># crate في سكريبت:</span><span class="pln">
var minimap_icon </span><span class="pun">=</span><span class="pln"> </span><span class="str">"alert"</span></pre>

<p>
	بإمكاننا اﻵن إضافة سكريبت إلى العقدة <code>MiniMap</code>.
</p>

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

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

</span><span class="lit">@export</span><span class="pln"> var player</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Player</span><span class="pln">
</span><span class="lit">@export</span><span class="pln"> var zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1.5</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var grid </span><span class="pun">=</span><span class="pln"> $MarginContainer</span><span class="pun">/</span><span class="typ">Grid</span><span class="pln">
</span><span class="lit">@onready</span><span class="pln"> var player_marker </span><span class="pun">=</span><span class="pln"> $MarginContainer</span><span class="pun">/</span><span class="typ">Grid</span><span class="pun">/</span><span class="typ">PlayerMarker</span><span class="pln">
</span><span class="lit">@onready</span><span class="pln"> var mob_marker </span><span class="pun">=</span><span class="pln"> $MarginContainer</span><span class="pun">/</span><span class="typ">Grid</span><span class="pun">/</span><span class="typ">MobMarker</span><span class="pln">
</span><span class="lit">@onready</span><span class="pln"> var alert_marker </span><span class="pun">=</span><span class="pln"> $MarginContainer</span><span class="pun">/</span><span class="typ">Grid</span><span class="pun">/</span><span class="typ">AlertMarker</span></pre>

<p>
	نستخدم تاليًا قاموسًا ونسميه <code>minimap-icon</code>  لربط الأنواع بالرموز، حيث نظهر الكائن  <code>mob</code>  الذي يمثل العدو كنقطة حمراء على الخريطة، والكائن<code>  alert</code>  الذي يمثل تحذير كعلامة صفراء:
</p>

<pre class="ipsCode">@onready var icons = {
    "mob": mob_marker,
    "alert": alert_marker
}
</pre>

<p>
	نحتاج أيضُا إلى متغير يخزّن نسبة حجم الخريطة إلى حجم عالم اللعبة. كما نستفيد من قاموس آخر لإسناد نقاط العلام الفعالة إلى كل كائن. وسيكون المفتاح Key في القاموس هو الكائن نفسه أي نسخة عن <code>Mob</code> أو <code>Crate</code> والقيمة value هي نقطة العلام المسندة إليه:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_11" style=""><span class="pln">var grid_scale
var markers </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{}</span></pre>

<p>
	نضبط موقع نقطة علام اللاعب في منتصف الخريطة ضمن الدالة <code>()ready_</code>، ونحسب عامل المقياس.
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_13" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    await get_tree</span><span class="pun">().</span><span class="pln">process_frame
    player_marker</span><span class="pun">.</span><span class="pln">position </span><span class="pun">=</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">size </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
    grid_scale </span><span class="pun">=</span><span class="pln"> grid</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">get_viewport_rect</span><span class="pun">().</span><span class="pln">size </span><span class="pun">*</span><span class="pln"> zoom</span><span class="pun">)</span></pre>

<h3 id="-5">
	العقد الموجودة في الحاويات
</h3>

<p>
	نظرًا للطريقة التي تعامل فيها العقدة <code>Container</code> أبناءها وتغير حجمهم أو موقعهم، لن نحصل على القيمة الصحيحة لأبعاد اﻷبناء وقت تنفيذ الدالة <code>()ready_</code>، لهذا علينا أن ننتظر حتى اﻹطار التالي لنحصل على أبعاد الشبكة.
</p>

<p>
	ننشئ أيضًا نقطة علام لكل لكائن في اللعبة باستخدام المجموعة <code>minimap_objects</code> بمضاعفة العقدة المطابقة لنقطة العلام وربط العلامة بالكائن بالاستفادة من القاموس <code>markers</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_15" style=""><span class="pln">    var map_objects </span><span class="pun">=</span><span class="pln"> get_tree</span><span class="pun">().</span><span class="pln">get_nodes_in_group</span><span class="pun">(</span><span class="str">"minimap_objects"</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> item </span><span class="kwd">in</span><span class="pln"> map_objects</span><span class="pun">:</span><span class="pln">
      var new_marker </span><span class="pun">=</span><span class="pln"> icons</span><span class="pun">[</span><span class="pln">item</span><span class="pun">.</span><span class="pln">minimap_icon</span><span class="pun">].</span><span class="pln">duplicate</span><span class="pun">()</span><span class="pln">
      grid</span><span class="pun">.</span><span class="pln">add_child</span><span class="pun">(</span><span class="pln">new_marker</span><span class="pun">)</span><span class="pln">
      new_marker</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span><span class="pln">
      markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> new_marker</span></pre>

<p>
	بعد أن أنشأنا نقاط العلام وربطناها بالكائنات الموجودة، نستطيع اﻵن تحديث مواقعها ضمن الدالة <code>()process_</code>. وإن لم يُعين أي لاعب <code>player</code> بعد، لا نفعل شيئًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_17" 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">player</span><span class="pun">:</span><span class="pln">
      </span><span class="kwd">return</span></pre>

<p>
	وﻹن كان هناك لاعب، ندور أولًا نقطة علام اللاعب لتطابق جهة حركته. وطالما أن نقطة العلامة <code>PlayerMarker</code> تتجه إلى اﻷعلى وليس بالاتجاه اﻷفقي <code>x</code>، لا بد من إضافة 90 درجة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_19" style=""><span class="pln">player_marker</span><span class="pun">.</span><span class="pln">rotation </span><span class="pun">=</span><span class="pln"> player</span><span class="pun">.</span><span class="pln">rotation </span><span class="pun">+</span><span class="pln"> PI</span><span class="pun">/</span><span class="lit">2</span></pre>

<p>
	نبحث اﻵن عن موقع كل كائن بالنسبة إلى اللاعب ونستخدمه في إيجاد موقع نقطة العلام، لنتذكر إزاحة الموقع بمقدار <code>grid.size/2</code> لأن نقطة المرجع هي الزاوية العليا اليسارية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_21" style=""><span class="kwd">for</span><span class="pln"> item </span><span class="kwd">in</span><span class="pln"> markers</span><span class="pun">:</span><span class="pln">
    var obj_pos </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="pln">item</span><span class="pun">.</span><span class="pln">position </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"> grid_scale </span><span class="pun">+</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">size </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
    markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">].</span><span class="pln">position </span><span class="pun">=</span><span class="pln"> obj_pos</span></pre>

<p>
	تبقى المشكلة إمكانية ظهور بعض نقاط العلام خارج الشبكة كما في الصورة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170390" href="https://academy.hsoub.com/uploads/monthly_2025_04/10_markers_out_of_grid.png.abfb333b397f55ed34be706163ab1200.png" rel=""><img alt="10 markers out of grid" class="ipsImage ipsImage_thumbnailed" data-fileid="170390" data-unique="nmpjz26yy" src="https://academy.hsoub.com/uploads/monthly_2025_04/10_markers_out_of_grid.png.abfb333b397f55ed34be706163ab1200.png"> </a>
</p>

<p>
	وﻹصلاح اﻷمر، نحصر موقع نقطة العلامة بمربع الشبكة باستخدام الدالة <code>clamp</code> بعد حساب المتغير <code>obj_pos</code> وقبل تحديد موقع العلامة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_23" style=""><span class="pln">obj_pos </span><span class="pun">=</span><span class="pln"> obj_pos</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"> grid</span><span class="pun">.</span><span class="pln">size</span><span class="pun">)</span></pre>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170391" href="https://academy.hsoub.com/uploads/monthly_2025_04/11_clmp_to_grid.png.652a61cca8dfeb97afb069b0bc8738d5.png" rel=""><img alt="11 clmp to grid" class="ipsImage ipsImage_thumbnailed" data-fileid="170391" data-unique="qol3cid80" src="https://academy.hsoub.com/uploads/monthly_2025_04/11_clmp_to_grid.png.652a61cca8dfeb97afb069b0bc8738d5.png"> </a>
</p>

<p>
	بإمكاننا أيضًا معالجة العلامات التي تقع خارج الشاشة وخارج مربع الشبكة. باختيار أحد الحلين التاليين وقبل استخدام <code>()clamp</code>. 
</p>

<p>
	الخيار اﻷول هو كالتالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_25" style=""><span class="kwd">if</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">get_rect</span><span class="pun">().</span><span class="pln">has_point</span><span class="pun">(</span><span class="pln">obj_pos </span><span class="pun">+</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">position</span><span class="pun">):</span><span class="pln">
    markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">].</span><span class="pln">show</span><span class="pun">()</span><span class="pln">
</span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
    markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">].</span><span class="pln">hide</span><span class="pun">()</span></pre>

<p>
	الخيار الثاني هو تغيير مظهر العلامات، بأن نجعلها أصغر لتدل على أنها أبعد مسافة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_27" style=""><span class="kwd">if</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">get_rect</span><span class="pun">().</span><span class="pln">has_point</span><span class="pun">(</span><span class="pln">obj_pos </span><span class="pun">+</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">position</span><span class="pun">):</span><span class="pln">
    markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">].</span><span class="pln">scale </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">1</span><span class="pun">,</span><span class="pln"> </span><span class="lit">1</span><span class="pun">)</span><span class="pln">
</span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
    markers</span><span class="pun">[</span><span class="pln">item</span><span class="pun">].</span><span class="pln">scale </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0.75</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.75</span><span class="pun">)</span></pre>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="170392" href="https://academy.hsoub.com/uploads/monthly_2025_04/12_marker_rescale.png.205d3f5b41183fb28a66c52c7bee1894.png" rel=""><img alt="12 marker rescale" class="ipsImage ipsImage_thumbnailed" data-fileid="170392" data-unique="s08bus6fu" src="https://academy.hsoub.com/uploads/monthly_2025_04/12_marker_rescale.png.205d3f5b41183fb28a66c52c7bee1894.png"> </a>
</p>

<h3 id="-6">
	إزالة الكائنات
</h3>

<p>
	ستزدحم اللعبة وتتوقف إن قُتِل أي كائن <code>Mob</code> أو التُقط كائن <code>Crate</code> لأن نقاط العلام حينها لن تكون صحيحة. لهذا نحتاج إلى طريقة نتأكد من خلالها من إزالة نقاط العلام في حال إزالة الكائنات، وفيما يلي طريقة سريعة سنتبعها في هذا المقال:
</p>

<p>
	نضيف <code>signal removed</code> إلى أي كائن وضعناه ضمن المجموعة <code>minimap_object</code> ثم نبث هذه الرسالة عندما يتدمر الكائن أو يُلتقط مع مرجع إلى الكائن نفسه كي تتعرف عليه الخريطة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_29" style=""><span class="pln">removed</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="pln">self</span><span class="pun">)</span></pre>

<p>
	نصل هذه اﻹشارات إلى الخريطة في الدالة <code>()ready_</code> للسكريبت الرئيسي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_31" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> object </span><span class="kwd">in</span><span class="pln"> get_tree</span><span class="pun">().</span><span class="pln">get_nodes_in_group</span><span class="pun">(</span><span class="str">"minimap_objects"</span><span class="pun">):</span><span class="pln">
      object</span><span class="pun">.</span><span class="pln">removed</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">minimap</span><span class="pun">.</span><span class="pln">_on_object_removed</span><span class="pun">)</span></pre>

<p>
	نضيف اﻵن دالة استقبال اﻹشارات إلى سكريبت الخريطة المصغرة لتحرير نقطة العلام وإزالة المرجع:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_33" style=""><span class="pln">func _on_object_removed</span><span class="pun">(</span><span class="pln">object</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> object </span><span class="kwd">in</span><span class="pln"> markers</span><span class="pun">:</span><span class="pln">
      markers</span><span class="pun">[</span><span class="pln">object</span><span class="pun">].</span><span class="pln">queue_free</span><span class="pun">()</span><span class="pln">
      markers</span><span class="pun">.</span><span class="pln">erase</span><span class="pun">(</span><span class="pln">object</span><span class="pun">)</span></pre>

<h3 id="-7">
	تكبير وتصغير الخريطة المصغرة
</h3>

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

<p>
	نضف بداية دالة تهيئة setter إلى الخاصية <code>zoom</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_35" style=""><span class="lit">@export</span><span class="pln"> var zoom </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1.5</span><span class="pun">:</span><span class="pln">
    set </span><span class="pun">=</span><span class="pln"> set_zoom

func set_zoom</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    zoom </span><span class="pun">=</span><span class="pln"> clamp</span><span class="pun">(</span><span class="pln">value</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.5</span><span class="pun">,</span><span class="pln"> </span><span class="lit">5</span><span class="pun">)</span><span class="pln">
    grid_scale </span><span class="pun">=</span><span class="pln"> grid</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">get_viewport_rect</span><span class="pun">().</span><span class="pln">size </span><span class="pun">*</span><span class="pln"> zoom</span><span class="pun">)</span></pre>

<p>
	نصل في نافذة الفاحص اﻹشارة <code>_gui_input</code> بالعقدة <code>MiniMap</code> حتى نتمكن من معالجة أفعال تدوير عجلة الفأرة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8979_37" style=""><span class="pln">func _on_gui_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="pln"> </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">
      </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_WHEEL_UP</span><span class="pun">:</span><span class="pln">
        zoom </span><span class="pun">+=</span><span class="pln"> </span><span class="lit">0.1</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_WHEEL_DOWN</span><span class="pun">:</span><span class="pln">
        zoom </span><span class="pun">-=</span><span class="pln"> </span><span class="lit">0.1</span></pre>

<p>
	فيما يلي نتيجة الشيفرة:
</p>

<p style="text-align: center;">
	<img alt="minimap_10.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="170397" data-ratio="100.00" data-unique="h1rvhjslr" width="98" src="https://academy.hsoub.com/uploads/monthly_2025_04/minimap_10.gif.6719c2265529b38d13522ffdb852ec7f.gif">
</p>

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

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

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

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/ui/minimap/index.html" rel="external nofollow">MinMap/Radar</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق:  <a href="https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D9%84%D8%A7%D8%AE%D8%AA%D9%8A%D8%A7%D8%B1-%D9%85%D8%B3%D8%AA%D9%88%D9%89-%D8%A7%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2545/" rel="">إنشاء قائمة لاختيار مستوى اللعبة في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D8%B4%D8%B1%D9%8A%D8%B7-%D8%B5%D8%AD%D8%A9-%D9%88%D9%86%D8%B5%D9%88%D8%B5-%D8%B7%D8%A7%D9%81%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2541/" rel="">بناء شريط صحة ونصوص طافية في ألعاب جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%B9%D8%B1%D8%B6-%D8%B9%D8%AF%D8%A7%D8%AF-%D8%AA%D9%86%D8%A7%D8%B2%D9%84%D9%8A-countdown-%D9%88%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AF%D8%A7%D8%A6%D8%B1%D9%8A%D8%A9-radial-menu-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2534/" rel="">عرض عداد تنازلي وقائمة دائرية في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%81%D8%A3%D8%B1%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2533/" rel="">التعامل مع إجراءات دخل الفأرة في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2549</guid><pubDate>Mon, 14 Apr 2025 14:08:00 +0000</pubDate></item><item><title>&#x633;&#x62D;&#x628; &#x648;&#x625;&#x641;&#x644;&#x627;&#x62A; &#x62C;&#x633;&#x645; &#x635;&#x644;&#x628; RigidBody2D &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/2.RigidBody2D.png.ec85cdadddcc51da2f58733a73ebee74.png" /></p>
<p>
	نشرح في هذا المقال طريقة اختيار أجسام صلبة وسحبها وإفلاتها من مكان لآخر باستخدام الفأرة في محرك الألعاب جودو، فقد يكون العمل مع الأجسام الصلبة مربكًا في جودو نظرًا لتحكم محرّك الفيزياء بهذه الحركة، وأي تدخل من قبلنا في الأمر سيقود غالبًا إلى نتائج غير متوقعة. إن مفتاح الحل في هذه الحالة هو استخدام الخاصية <code>mode</code> للجسم، ويُطبق هذا الأمر في الفضاء ثنائي البعد وثلاثي البعد.
</p>

<p style="text-align: center;">
	<img alt="001 سحب وإفلات جسم صلب" class="ipsImage ipsImage_thumbnailed" data-fileid="170504" data-ratio="58.75" data-unique="7xai9gyx3" style="width: 400px; height: auto;" width="774" src="https://academy.hsoub.com/uploads/monthly_2025_04/001_rbody_drag_drop.gif.89cd536b10b6de2608340541fc51d4e9.gif">
</p>

<h2 id="">
	إعداد الجسم الصلب
</h2>

<p>
	لننشئ مشروع لعبة جديدة ثنائية الأبعاد في جودو ونعمل مع كائن يمثل الجسم الصلب بإضافة العقدتين <code>Sprite2D</code> و <code>CollisionShape2D</code>. بإمكاننا أيضًا إضافة العقدة <code>PhysicsMaterial</code> إن أردنا ضبط خاصيتي الارتداد <code>Bounce</code> والاحتكاك <code>Friction</code>. 
</p>

<p>
	كما سنستخدم الخاصية <code>freeze</code> لإبعاد الجسم عن سيطرة محرّك الفيزياء عندما نسحبه. وطالما أننا نريد من الجسم الصلب أن يكون قابلًا لحركة، لا بد من ضبط قيمة الخاصية <code>Freeze </code>في القسم<code> Mode </code>على القيمة <code>kinematic</code> بدلًا من القيمة الافتراضية <code>Static</code>.
</p>

<p>
	نضع الجسم ضمن مجموعة تدعى pickable، ونستخدمها لضم نسخ متعددة من الكائنات التي يمكن التقاطها في المشهد الرئيسي. 
</p>

<p>
	نضيف سكريبت برمجي للجسم الصلب ثم نصل الإشارة <code>_input_event</code> الخاصة به كما يلي.
</p>

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

signal clicked

var held </span><span class="pun">=</span><span class="pln"> false

func _on_input_event</span><span class="pun">(</span><span class="pln">viewport</span><span class="pun">,</span><span class="pln"> event</span><span class="pun">,</span><span class="pln"> shape_idx</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="pln"> </span><span class="kwd">and</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="pun">:</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">pressed</span><span class="pun">:</span><span class="pln">
            </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"clicked"</span><span class="pun">)</span><span class="pln">
            clicked</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="pln">self</span><span class="pun">)</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3241_8" style=""><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"> held</span><span class="pun">:</span><span class="pln">
        global_transform</span><span class="pun">.</span><span class="pln">origin </span><span class="pun">=</span><span class="pln"> get_global_mouse_position</span><span class="pun">()</span></pre>

<p>
	فيما يلي الدالتان اللتان سنستدعيهما عند التقاط الجسم وإفلاته. ولنتذكر أن تغيير قيمة الخاصية <code>freeze</code> إلى <code>true</code> سيزيل الجسم من عمليات محرّك الفيزياء. 
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3241_10" style=""><span class="pln">func pickup</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> held</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">return</span><span class="pln">
    freeze </span><span class="pun">=</span><span class="pln"> true
    held </span><span class="pun">=</span><span class="pln"> true

func drop</span><span class="pun">(</span><span class="pln">impulse</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">
    </span><span class="kwd">if</span><span class="pln"> held</span><span class="pun">:</span><span class="pln">
        freeze </span><span class="pun">=</span><span class="pln"> false
        apply_central_impulse</span><span class="pun">(</span><span class="pln">impulse</span><span class="pun">)</span><span class="pln">
        held </span><span class="pun">=</span><span class="pln"> false</span></pre>

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

<div class="banner-container ipsBox ipsPadding">
	<div class="inner-banner-container">
		<p class="banner-heading">
			دورة تطوير الألعاب
		</p>

		<p class="banner-subtitle">
			ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة<br>
			ومليئة بالتحديات
		</p>

		<div>
			<a class="ipsButton ipsButton_large ipsButton_primary ipsButton_important" href="https://academy.hsoub.com/learn/game-development/" rel="">اشترك الآن</a>
		</div>
	</div>

	<div class="banner-img">
		<a href="https://academy.hsoub.com/learn/game-development/" rel=""><img alt="دورة تطوير الألعاب" src="https://academy.hsoub.com/learn/assets/images/courses/game-development.png"></a>
	</div>
</div>

<h2 id="-1">
	المشهد الرئيسي
</h2>

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

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

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

func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> node </span><span class="kwd">in</span><span class="pln"> get_tree</span><span class="pun">().</span><span class="pln">get_nodes_in_group</span><span class="pun">(</span><span class="str">"pickable"</span><span class="pun">):</span><span class="pln">
        node</span><span class="pun">.</span><span class="pln">clicked</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">_on_pickable_clicked</span><span class="pun">)</span></pre>

<p>
	نعرّف بعد ذلك الدالة التي نربط بها الإشارات، وتضبط هذه الدالة قيمة <code>held_object</code> حتى نعرف بوجود جسم يُسحب حاليًا، ونستدعي التابع <code>()pickup</code> الخاص بالجسم.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3241_14" style=""><span class="pln">func _on_pickable_clicked</span><span class="pun">(</span><span class="pln">object</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">held_object</span><span class="pun">:</span><span class="pln">
        object</span><span class="pun">.</span><span class="pln">pickup</span><span class="pun">()</span><span class="pln">
        held_object </span><span class="pun">=</span><span class="pln"> object</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3241_16" 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"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pln"> </span><span class="kwd">and</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="pun">:</span><span class="pln">
        </span><span class="kwd">if</span><span class="pln"> held_object </span><span class="kwd">and</span><span class="pln"> </span><span class="pun">!</span><span class="pln">event</span><span class="pun">.</span><span class="pln">pressed</span><span class="pun">:</span><span class="pln">
            held_object</span><span class="pun">.</span><span class="pln">drop</span><span class="pun">(</span><span class="typ">Input</span><span class="pun">.</span><span class="pln">get_last_mouse_velocity</span><span class="pun">())</span><span class="pln">
            held_object </span><span class="pun">=</span><span class="pln"> null</span></pre>

<p>
	لنلاحظ كيف استخدمنا هنا التابع <code>()get_last_mouse_velocity</code> لتمرير الدفع إلى الكائن. علينا أن ننتبه إلى ذلك جيدّا. مع ذلك، سنجد أننا نطلق الجسم الصلب بسرعة كبيرة وخاصة إذا كانت قيمة الخاصية <code>mass</code> للجسم قليلة. لهذا، من الأجدى أن نحدد القيمة العظمى للكتلة على قيمة مناسبة باستخدام التابع <code>()clamp</code>. يمكن تجربة عدة قيم حتى نحدد ما يناسب لعبتنا.
</p>

<h2>
	الخاتمة
</h2>

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

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

<p>
	يمكن <span ipsnoautolink="true">تحميل المثال كاملً</span>ا من <a href="https://github.com/godotrecipes/rigidbody_drag_drop" rel="external nofollow">هذا الرابط</a> لفهمه بصورة جيدة وتجربة التعديل عليه، أو تحميله مباشرة من هنا <a class="ipsAttachLink" data-fileext="zip" data-fileid="170505" href="https://academy.hsoub.com/applications/core/interface/file/attachment.php?id=170505&amp;key=6fc8c9c8d566a76d0dd8e3af73f30abd" rel="">rigidbody_drag_drop-master.zip</a>
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/physics/rigidbody_drag_drop/index.html" rel="external nofollow">RigidBody2D Drag and Drop</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%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-%D9%84%D9%84%D8%AA%D9%88%D8%AC%D9%87-%D9%86%D8%AD%D9%88-%D9%87%D8%AF%D9%81-%D9%88%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%83-%D9%86%D8%AD%D9%88%D9%87-r2547/" rel="">استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه</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%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%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%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-r2281/" 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-%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>
	<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%88%D8%AD%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D9%88%D8%B4%D8%AE%D8%B5%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%AE%D8%B5%D9%88%D9%85-%D9%81%D9%8A-unity3d-r134/" rel="">إنشاء الوحدات البنائية وشخصيات الخصوم في Unity3D</a>
	</li>
</ul>

<p>
	 
</p>
]]></description><guid isPermaLink="false">2551</guid><pubDate>Wed, 09 Apr 2025 15:04:01 +0000</pubDate></item><item><title>&#x625;&#x646;&#x634;&#x627;&#x621; &#x642;&#x627;&#x626;&#x645;&#x629; &#x644;&#x627;&#x62E;&#x62A;&#x64A;&#x627;&#x631; &#x645;&#x633;&#x62A;&#x648;&#x649; &#x627;&#x644;&#x644;&#x639;&#x628;&#x629; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A5%D9%86%D8%B4%D8%A7%D8%A1-%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D9%84%D8%A7%D8%AE%D8%AA%D9%8A%D8%A7%D8%B1-%D9%85%D8%B3%D8%AA%D9%88%D9%89-%D8%A7%D9%84%D9%84%D8%B9%D8%A8%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2545/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/_.png.c8e3bce9e2a3ba1ba78cd0b709014d83.png" /></p>
<p>
	نشرح في هذا المقال طريقة بناء قائمة تتضمن مجموعة خيارات تظهر في بداية اللعبة وتساعد اللاعب على تحديد مستوى اللعبة. سيكون تصميم القائمة على شكل شبكة قابلة للتمرير وضمنها صناديق تمثل كل مستوى، بحيث يمكن التنقل بينها واختيار المرحلة التي يرغب اللاعب في اللعب فيها كما توضح الصورة التالية.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169965" href="https://academy.hsoub.com/uploads/monthly_2025_03/01_level_select.gif.55916fae834883e6985360853320c1e5.gif" rel=""><img alt="01 level select" class="ipsImage ipsImage_thumbnailed" data-fileid="169965" data-ratio="58.50" data-unique="tadmrj1fr" style="width: 400px; height: auto;" width="400" src="https://academy.hsoub.com/uploads/monthly_2025_03/01_level_select.gif.55916fae834883e6985360853320c1e5.gif"></a>
</p>

<h2 id="-1">
	مراحل بناء القائمة
</h2>

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

<h3 id="-2">
	بناء صندوق المستوى LevelBox
</h3>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169966" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_level_box_style.gif.49290422a8b1f7a9d5672804726b35e3.gif" rel=""><img alt="02 level box style" class="ipsImage ipsImage_thumbnailed" data-fileid="169966" data-ratio="99.00" data-unique="dl0xdimr4" width="100" src="https://academy.hsoub.com/uploads/monthly_2025_03/02_level_box_style.gif.49290422a8b1f7a9d5672804726b35e3.gif"></a>
</p>

<p>
	ستكون هيكلية العقدة اللازمة لبناء هذا الصندوق على النحو التالي:
</p>

<pre class="ipsCode" id="ips_uid_376_10">LevelBox: PanelContainer
     Label
     MarginContainer
       TextureRect</pre>

<p>
	استخدمنا العناصر التالية لتشكيل الصندوق:
</p>

<ul>
	<li>
		حاوية PanelContainer لتنظيم وعرض العناصر داخلها
	</li>
	<li>
		عقدة تسمية نصية Label لعرض رقم المستوى
	</li>
	<li>
		حاوية MarginContainer لإضافة هوامش حول المحتوى 
	</li>
	<li>
		عقدة <code>LevelBox</code> من نوع  TextureRect لعرض صورة قفل عندما يكون المستوى مغلق، ورقم عندما يكون مفتوح<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169966" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_level_box_style.gif.49290422a8b1f7a9d5672804726b35e3.gif" rel=""> </a>
	</li>
</ul>

<p style="text-align: center;">
	 
</p>

<p>
	نتأكد من ضبط الخاصية <code>Layout</code>  في القسم <code>Custom Minimum Size </code>للعقدة <code>LevelBox</code>  على القيمة <code>(110,110)</code> ويمكن اختيار أي حجم آخر مناسب لتخطيط القائمة.
</p>

<p>
	نضيف سكريبت إلى العقدة التي تمثل صندوق المستوى من أجل وصل اﻹشارة <code><a href="https://docs.godotengine.org/en/stable/classes/class_control.html#class-control-private-method-gui-input" rel="external nofollow">gui_input</a></code> التي تُطلق عندما تتلقى العقدة حدث إدخال مثل النقر بالفأرة أو الضغط على لوحة المفاتيح. حيث يمكن أن يكون المستوى مغلقًا أو مفتوحًا، وعند النقر على الصندوق سنرسل إرسال إشارة لاختيار المستوى.
</p>

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

signal level_selected  </span><span class="com"># إشارة تُطلق عند تحديد المستوى</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var locked </span><span class="pun">=</span><span class="pln"> true</span><span class="pun">:</span><span class="pln">  </span><span class="com"># يحدد إذا كان المستوى مغلقًا</span><span class="pln">
    set </span><span class="pun">=</span><span class="pln"> set_locked

</span><span class="lit">@export</span><span class="pln"> var level_num </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">  </span><span class="com"># رقم المستوى</span><span class="pln">
    set </span><span class="pun">=</span><span class="pln"> set_level

</span><span class="lit">@onready</span><span class="pln"> var lock </span><span class="pun">=</span><span class="pln"> $MarginContainer</span><span class="pun">/</span><span class="typ">Lock</span><span class="pln">  </span><span class="com"># صورة القفل</span><span class="pln">
</span><span class="lit">@onready</span><span class="pln"> var label </span><span class="pun">=</span><span class="pln"> $Label  </span><span class="com">#  رقم المستوى</span><span class="pln">

</span><span class="com"># دالة لضبط حالة القفل وإظهارأو إخفاء العناصر</span><span class="pln">
func set_locked</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    locked </span><span class="pun">=</span><span class="pln"> value
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> is_inside_tree</span><span class="pun">():</span><span class="pln">  </span><span class="com"># ننتظر حتى يكتمل تحميل العنصر داخل المشهد</span><span class="pln">
      await ready
    lock</span><span class="pun">.</span><span class="pln">visible </span><span class="pun">=</span><span class="pln"> value  </span><span class="com"># نعرض صورة القفل إذا كان المستوى مغلقًا</span><span class="pln">
    label</span><span class="pun">.</span><span class="pln">visible </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> value  </span><span class="com"># نظهر النص إذا كان المستوى غير مغلق</span><span class="pln">

</span><span class="com"># دالة لضبط رقم المستوى وتحديث النص المعروض</span><span class="pln">
func set_level</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    level_num </span><span class="pun">=</span><span class="pln"> value
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> is_inside_tree</span><span class="pun">():</span><span class="pln">  </span><span class="com"># ننتظر تحميل العنصر في المشهد</span><span class="pln">
      await ready
    label</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> str</span><span class="pun">(</span><span class="pln">level_num</span><span class="pun">)</span><span class="pln">  </span><span class="com"># تحديث النص برقم المستوى</span><span class="pln">

</span><span class="com"># دالة لمعالجة مدخلات المستخدم </span><span class="pln">
func _on_gui_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"> locked</span><span class="pun">:</span><span class="pln">  </span><span class="com"># إذا كان المستوى مغلقًا، نتجاهل النقر</span><span class="pln">
      </span><span class="kwd">return</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="pln"> </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">  </span><span class="com"># التحقق من النقر بزر الفأرة</span><span class="pln">
      level_selected</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="pln">level_num</span><span class="pun">)</span><span class="pln">  </span><span class="com"># إطلاق الإشارة مع رقم المستوى</span><span class="pln">
      </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"Clicked level "</span><span class="pun">,</span><span class="pln"> level_num</span><span class="pun">)</span><span class="pln">  </span><span class="com"># طباعة رسالة لاختبار النقر</span></pre>

<p>
	نستخدم في شيفرتنا التوجيه <code>tool@</code> حتى نتمكن من تغيير قيم الخاصيات عبر نافذة الفاحص inspector ونرى التأثير مباشرة دون الحاجة لتشغيل المشهد. لنجرب اﻵن النقر على الخاصية <code>Locked</code> ونتحقق من رؤية صورة القفل تظهر وتختفي. وطالما أن اللعبة لا تضم مستويات فعلية لتحميلها، ستساعدنا الدالة <code>()print</code> على اختبار التقاط حدث النقر.
</p>

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

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169962" href="https://academy.hsoub.com/uploads/monthly_2025_03/03_level_select_grid.png.52f07cc190621e0ab014bcfce66c8eb4.png" rel=""><img alt="03 level select grid" class="ipsImage ipsImage_thumbnailed" data-fileid="169962" data-unique="hxd0rf5a8" src="https://academy.hsoub.com/uploads/monthly_2025_03/03_level_select_grid.png.52f07cc190621e0ab014bcfce66c8eb4.png"> </a>
</p>

<p>
	ننتقل للقسم <code>Theme Overrides </code>ثم <code>Seperation </code>ونضبط قيمتي الخاصيتين <code>V Seperation</code> و  <code>H Seperation </code>على <code>10</code>، ونحفظ المشهد باسم <code>LevelGrid</code>. سنستخدم في القائمة عدة نسخ عن عقدة الشبكة لعرض العدد المطلوب من المستويات.
</p>

<h3 id="-4">
	شاشة القائمة
</h3>

<p>
	بإمكاننا اﻵن تجميع القائمة النهائية. توضح الصورة التالية التخطيط اﻷولي الذي سننفذه:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169963" href="https://academy.hsoub.com/uploads/monthly_2025_03/04_level_select_screen_menu.png.1242d0d12cfb52347552133e7933519f.png" rel=""><img alt="04 level select screen menu" class="ipsImage ipsImage_thumbnailed" data-fileid="169963" data-unique="7qsp21r2o" src="https://academy.hsoub.com/uploads/monthly_2025_03/04_level_select_screen_menu.png.1242d0d12cfb52347552133e7933519f.png"> </a>
</p>

<p>
	ننشئ المشهد انطلاقًا من العقد التالية:
</p>

<pre class="ipsCode">LevelMenu: MarginContainer
     VBoxContainer
      Title: Label
       HBoxContainer
        BackButton: TextureButton
        ClipControl: Control
        NextButton: TextureButton
</pre>

<p>
	نضبط خواص العقد كالتالي:
</p>

<ul>
	<li>
		العقدة <code>LevelMenu</code>: نضبط <code>Theme Overrides</code> ثم <code>Constants</code> ثم <code>Margins </code>على القيمة <code>20</code>
	</li>
	<li>
		العقدة <code>VBoxContainer</code>:نضبط <code>Theme Overrides</code> ثم <code>Constants</code> ثم <code>Margins</code> على القيمة <code>50</code>
	</li>
	<li>
		العقدة <code>Title</code>: ننسقها بالطريقة التي نريدها
	</li>
	<li>
		نضبط العقدتين <code>BackButton</code> و <code>NextButton</code> كالتالي:
		<ul>
			<li>
				<code>Ignore Texture Size: On</code> لضبط حجم الزر وفقًا لحجمه داخل واجهة المستخدم
			</li>
			<li>
				<code>Stretch Mode: Keep Centered</code> للحفاظ على محاذاة المحتوى داخل الأزرار
			</li>
			<li>
				<code>Layout/Container: On </code> ليعمل الزر كحاوية
			</li>
			<li>
				<code>Sizing/Horizontal/Expand: On </code>لتوسيع الأزرار أفقيًا داخل المساحة المتاحة
			</li>
		</ul>
	</li>
	<li>
		نضبط العقدة <code>ClipControl</code> كما يلي:
		<ul>
			<li>
				<code>Layout/Clip Contents:On </code>لاقتصاص أي محتوى يتجاوز حجم الإطار
			</li>
			<li>
				<code>Layout&gt;Custom Minimum Size:(710, 350) </code>لتحديد الحجم الأدنى للعقدة  
			</li>
		</ul>
	</li>
</ul>

<p>
	بعد ذلك نضع الشبكة ضمن العقدة <code>ClipControl</code>، بما أننا  تمكين الخاصية <code>Clip Content</code> سيقتص محتوى العقدة إن كانت أكبر من عنصر التحكم. وسنتمكن اﻵن من بناء شبكة من صناديق المستويات قابلة للتمرير، لهذا نضيف عقدة من النوع <code>HBoxContainer</code> تُدعى <code>GridBox</code> إلى <code>ClipControl</code> إضافة إلى ثلاث نسخ من العقدة <code>LevrlGrid</code> أو أكثر إن أردنا. ونتأكد من ضبط الخاصية <code>Theme Overrides</code> ثم <code>Constants</code> ثم <code>Separation</code> على القيمة <code>0</code>.
</p>

<p>
	ينبغي أن يبدو تخطيط المشهد اﻵن كما في الشكل التالي تقريبًا، مع العلم أننا عطلنا الخاصية <code>Clip content</code> لنعرض ما يحدث بشكل أوضح:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169964" href="https://academy.hsoub.com/uploads/monthly_2025_03/05_level_select_complete.png.2a91d95a67a0b4ffe8811bd5b388a339.png" rel=""><img alt="05 level select complete" class="ipsImage ipsImage_thumbnailed" data-fileid="169964" data-unique="on09prxau" src="https://academy.hsoub.com/uploads/monthly_2025_03/05_level_select_complete.png.2a91d95a67a0b4ffe8811bd5b388a339.png"> </a>
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_376_14" style=""><span class="lit">110</span><span class="pln"> </span><span class="pun">(</span><span class="pln">width of each </span><span class="typ">LevelBox</span><span class="pun">)</span><span class="pln">
    </span><span class="pun">*</span><span class="pln"> </span><span class="lit">6</span><span class="pln"> </span><span class="pun">(</span><span class="pln">grid columns</span><span class="pun">)</span><span class="pln">
    </span><span class="pun">+</span><span class="pln"> </span><span class="lit">10</span><span class="pln"> </span><span class="pun">(</span><span class="pln">grid spacing</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> </span><span class="lit">5</span><span class="pln">
    </span><span class="pun">==</span><span class="pln"> </span><span class="lit">710</span></pre>

<p>
	قد يتبادر للذهن سؤال عن عدم استخدام العقدة <code>ScrollCointainer</code>. هنا، بالتأكيد يمكن ذلك، لكننا لا نريد التنقل بين الشبكات باستمرار، ولا نريد أيضًا رؤية شريط تمرير.
</p>

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

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

var num_grids </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
var current_grid </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
var grid_width </span><span class="pun">=</span><span class="pln"> </span><span class="lit">710</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var gridbox </span><span class="pun">=</span><span class="pln"> $VBoxContainer</span><span class="pun">/</span><span class="typ">HBoxContainer</span><span class="pun">/</span><span class="typ">ClipControl</span><span class="pun">/</span><span class="typ">GridBox</span><span class="pln">

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

    </span><span class="com"># ترقيم جميع صناديق المستويات وإلغاء قفلها   </span><span class="pln">
    </span><span class="com"># يمكن استبداله بما يتناسب مع نظام المستويات في اللعبة</span><span class="pln">
    </span><span class="com"># يمكن أيضًا ربط إشارات "level_selected" هنا </span><span class="pln">
    num_grids </span><span class="pun">=</span><span class="pln"> gridbox</span><span class="pun">.</span><span class="pln">get_child_count</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> grid </span><span class="kwd">in</span><span class="pln"> gridbox</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
      </span><span class="kwd">for</span><span class="pln"> box </span><span class="kwd">in</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
        var num </span><span class="pun">=</span><span class="pln"> box</span><span class="pun">.</span><span class="pln">get_position_in_parent</span><span class="pun">()</span><span class="pln"> </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"> </span><span class="lit">18</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> grid</span><span class="pun">.</span><span class="pln">get_position_in_parent</span><span class="pun">()</span><span class="pln">
        box</span><span class="pun">.</span><span class="pln">level_num </span><span class="pun">=</span><span class="pln"> num
        box</span><span class="pun">.</span><span class="pln">locked </span><span class="pun">=</span><span class="pln"> false

func _on_BackButton_pressed</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> current_grid </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">1</span><span class="pun">:</span><span class="pln">
      current_grid </span><span class="pun">-=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
      gridbox</span><span class="pun">.</span><span class="pln">rect_position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">+=</span><span class="pln"> grid_width

func _on_NextButton_pressed</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> current_grid </span><span class="pun">&lt;</span><span class="pln"> num_grids</span><span class="pun">:</span><span class="pln">
      current_grid </span><span class="pun">+=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
      gridbox</span><span class="pun">.</span><span class="pln">rect_position</span><span class="pun">.</span><span class="pln">x </span><span class="pun">-=</span><span class="pln"> grid_width</span></pre>

<p>
	ننقر على زر التالي Next والسابق Back عند تشغيل المشهد ونتأكد من تمرير العناصر كما هو متوقع. من المفترض أن يطبع النقر على صندوق المستوى شيئًا في شاشة الطرفية.
</p>

<h2>
	الخاتمة
</h2>

<p>
	بهذا نكون قد انتهينا من مقالنا الذي يشرح طريقة بناء شبكة قابلة للتمرير يمينًا ويسارًا تضم صناديق لتحديد مرحلة أو مستوى اللعبة، ويمكن تحميل <a href="https://github.com/godotrecipes/ui_level_select" rel="external nofollow">المثال بالكامل</a> لرؤية كل شيء يعمل كما يجب بما في ذلك اﻹجراءات وبعض عمليات توليد اﻷطر البينية tweens لتجميل عملية التمرير.
</p>

<p>
	ترجمة -وبتصرف- للمقال: <a href="https://kidscancode.org/godot_recipes/4.x/ui/level_select/index.html" rel="external nofollow">Level Select Menu</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D8%B4%D8%B1%D9%8A%D8%B7-%D8%B5%D8%AD%D8%A9-%D9%88%D9%86%D8%B5%D9%88%D8%B5-%D8%B7%D8%A7%D9%81%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2541/" rel="">بناء شريط صحة ونصوص طافية في ألعاب جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D9%85%D8%A7%D8%B9-%D9%84%D9%85%D8%AF%D8%AE%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2384/" rel="">الاستماع لمدخلات اللاعب في جودو Godot</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/%D9%84%D8%BA%D8%A7%D8%AA-%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8/" rel="">تعرف على أشهر لغات برمجة الألعاب</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">أشهر محركات الألعاب Game Engines</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2545</guid><pubDate>Mon, 07 Apr 2025 15:07:00 +0000</pubDate></item><item><title>&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645; RigidBody2D &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648; &#x644;&#x644;&#x62A;&#x648;&#x62C;&#x647; &#x646;&#x62D;&#x648; &#x647;&#x62F;&#x641; &#x648;&#x627;&#x644;&#x62A;&#x62D;&#x631;&#x643; &#x646;&#x62D;&#x648;&#x647;</title><link>https://academy.hsoub.com/programming/game-development/%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-%D9%84%D9%84%D8%AA%D9%88%D8%AC%D9%87-%D9%86%D8%AD%D9%88-%D9%87%D8%AF%D9%81-%D9%88%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%83-%D9%86%D8%AD%D9%88%D9%87-r2547/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_04/1.RigidBody2D-2.png.d2f351e400fc3c3016ac5306f3305629.png" /></p>
<p>
	نشرح في هذا المقال طريقة تدوير جسم صلب بسلاسة في محرك الألعاب جودو من خلال استخدام العقدة <code>RigidBody2D،</code> ونوضح طريقة توجيه هذا الجسم نحو هدف معين ليطبق عليه، أو يتحرك نحوه.
</p>

<h2 id="">
	الإطباق على هدف
</h2>

<p>
	قد يكون استخدام العقدة <code>RigidBody2D</code> مربكًا لأن من يتحكم بها هو محرك الفيزياء الموجود ضمن <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">محرك الألعاب</a> جودو Godot physics engine. فإن أردنا تحريك الجسم ضمن اللعبة التي نطورها بجودو، فعلينا تطبيق قوى معينة عليه بدلًا من تحريكه مباشرة. وننصح قبل بدء العمل على تطبيق خطوات هذا المقال بإلقاء نظرة على <a href="https://docs.godotengine.org/en/stable/classes/class_rigidbody2d.html" rel="external nofollow" target="_blank">توثيق الواجهة البرمجية RigidBody2D</a>.
</p>

<p>
	لا بد من تطبيق عزم دوراني torque حتى نتمكن من تدوير الجسم، وبمجرد أن يبدأ الجسم بالدوران، علينا تقليل عزم الدوران تدريجيًا مع اقتراب hg[sl من الوجهة النهائية بحيث لا يفرط الجسم في الدوران ويتوقف في الاتجاه الصحيح بدقة. تُعدُّ هذه حالة مثالية لتطبيق الجداء النقطي المعروف أيضًا باسم الداخلي dot product فهو مناسب جدًا لتحديد الاتجاه المطلوب للوصول للوجهة النهائية.
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="170130" href="https://academy.hsoub.com/uploads/monthly_2025_04/dot_cross_01.gif.08d9fd66f867df2de57dc9b22491cfc4.gif" rel=""><img alt="dot cross 01" class="ipsImage ipsImage_thumbnailed" data-fileid="170130" data-ratio="54.46" data-unique="ln5x93af6" width="426" src="https://academy.hsoub.com/uploads/monthly_2025_04/dot_cross_01.gif.08d9fd66f867df2de57dc9b22491cfc4.gif"></a>
</p>

<h3>
	ناتج الجداء النقطي موجب
</h3>

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

<h3>
	ناتج الجداء النقطي صفر
</h3>

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

<h3>
	ناتج الجداء النقطي سالب
</h3>

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

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

<p>
	لنلقِ نظرة على الكود التالي المكتوب <a href="https://academy.hsoub.com/programming/game-development/%D9%84%D8%BA%D8%A7%D8%AA-%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8/" rel="">بلغة GDScript</a> لتدوير جسم ثنائي الأبعاد 2D باتجاه هدف معين بتطبيق عزم دوراني torque:
</p>

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

var angular_force </span><span class="pun">=</span><span class="pln"> </span><span class="lit">50000</span><span class="pln">
var target </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">RIGHT

func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var dir </span><span class="pun">=</span><span class="pln"> transform</span><span class="pun">.</span><span class="pln">y</span><span class="pun">.</span><span class="pln">dot</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">
    constant_torque </span><span class="pun">=</span><span class="pln"> dir </span><span class="pun">*</span><span class="pln"> angular_force</span></pre>

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

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

<h3 id="-1">
	تفادي مشكلات تدوير الجسم الصلب RigidBody2D
</h3>

<p>
	يمكننا تفادي تعقيدات تحريك الجسم الصلب بالامتناع عن تدوير الجسم الصلب نفسه <code>RigidBody2D</code>، ونعمل بدلًا من ذلك على تعديل الخاصية <code>rotation</code> لشخصية الابن child sprite كي يتوجه نحو الهدف. وبالإمكان حينها استخدام التابعين <code>()lerp</code> و <code>()Tween</code> لجعل الحركة الدورانية سلسلة قدر المستطاع. ويعد هذا الحل مناسبًا في كثير من الحالات، حيث يمكن أن يكون للجسم الأساسي اتجاهه الخاص بينما تتجه الشخصية أو العنصر المرتبط به في اتجاه مختلف نحو الهدف. إذ يمكن للجسم الأساسي أن يتحرك في اتجاه معين، بينما يمكن للشخصية الملحقة به مثل الرأس أو السلاح أن تدور بشكل مستقل دون الحاجة لأن يكون توجيهها مطابقًا تمامًا لتوجيه الجسم الأساسي.
</p>

<h2 id="-2">
	تحرك جسم نحو الهدف
</h2>

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

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

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

<p>
	لنلقِ نظرة على الكود التالي الذي يجعل الجسم الصلب <code>RigidBody2D</code>  يتحرك نحو هدف معين بطريقة سلسة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5586_14" style=""><span class="com"># تحريك الجسم بسلاسة نحو الهدف</span><span class="pln">
extends </span><span class="typ">RigidBody2D</span><span class="pln">

var linear_force </span><span class="pun">=</span><span class="pln"> </span><span class="lit">5</span><span class="pln">
var target </span><span class="pun">=</span><span class="pln"> position


func _physics_process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    var dist </span><span class="pun">=</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">
    constant_force </span><span class="pun">=</span><span class="pln"> dir </span><span class="pun">*</span><span class="pln"> linear_force </span><span class="pun">*</span><span class="pln"> dist</span></pre>

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

<h3 id="lineardump">
	أهمية الخاصية <code>linear_damp</code>
</h3>

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

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

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

<h2>
	الخاتمة
</h2>

<p>
	تعلمنا في مقال اليوم كيفية تحريك وتدوير جسم صلب <code>RigidBody2D</code>  في جودو بشكل سلس نحو هدف معين باستخدام القوى والعزم الدوراني، كما شرحنا كيفية التحكم في التباطؤ باستخدام الخاصية <code>linear_damp</code> لضمان حركة طبيعية للجسم.
</p>

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/physics/smooth_rigid_rotate/index.html" rel="external nofollow" target="_blank">RigidBody2D: Look at Target</a> و <a href="https://kidscancode.org/godot_recipes/4.x/physics/smooth_rigid_move/index.html" rel="external nofollow" target="_blank">RigidBody2D: Move to Target</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<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>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%A5%D8%B4%D8%A7%D8%B1%D8%A7%D8%AA-signals-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2385/" rel="">استخدام الإشارات Signals في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">تعرف على أشهر محركات الألعاب Game Engines</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%81%D8%A3%D8%B1%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2533/" rel="">التعامل مع إجراءات دخل الفأرة في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2547</guid><pubDate>Wed, 02 Apr 2025 15:00:00 +0000</pubDate></item><item><title>&#x628;&#x646;&#x627;&#x621; &#x634;&#x631;&#x64A;&#x637; &#x635;&#x62D;&#x629; &#x648;&#x646;&#x635;&#x648;&#x635; &#x637;&#x627;&#x641;&#x64A;&#x629; &#x641;&#x64A; &#x623;&#x644;&#x639;&#x627;&#x628; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A8%D9%86%D8%A7%D8%A1-%D8%B4%D8%B1%D9%8A%D8%B7-%D8%B5%D8%AD%D8%A9-%D9%88%D9%86%D8%B5%D9%88%D8%B5-%D8%B7%D8%A7%D9%81%D9%8A%D8%A9-%D9%81%D9%8A-%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-r2541/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/_.png.878b4a6d4f988f7c4797bd2543a47297.png" /></p>
<p>
	نتعرف في هذا المقال على طريقة بناء شريط صحة Health Bar يضم قلوبًا أو غيرها من اﻷيقونات، كما سنتعرف على طريقة عرض نسبة تضرر الشخصية في لعبة على شكل نص يطفو فوق الشخصية.
</p>

<h2 id="-1">
	ثلاث طرق لبناء شريط صحة يضم قلوبًا
</h2>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169744" href="https://academy.hsoub.com/uploads/monthly_2025_03/01_heart_bar.png.1408876bf7c173b70fdba246546b3163.png" rel=""><img alt="01 heart bar" class="ipsImage ipsImage_thumbnailed" data-fileid="169744" data-ratio="84.40" data-unique="rlteqot27" style="width: 250px; height: auto;" width="250" src="https://academy.hsoub.com/uploads/monthly_2025_03/01_heart_bar.png.1408876bf7c173b70fdba246546b3163.png"> </a>
</p>

<p>
	تعرض الصورة السابقة ثلاث حالات ممكنة لعرض شريط الصحة:
</p>

<ul>
	<li>
		الطريقة البسيطة: تعرض القلوب ممتلئة بالكامل
	</li>
	<li>
		الطريقة الفارغة: تعرض قلوب فارغة وأخرى ممتلئة
	</li>
	<li>
		الطريقة الجزئية: تعرض القلوب نصف ممتلئة
	</li>
</ul>

<h3 id="-2">
	إعداد شريط اﻷيقونات
</h3>

<p>
	نستخدم في هذا المثال صور قلوب أبعادها 53x45 حصلنا عليها من موقع <a href="https://kenney.nl/assets/platformer-art-deluxe" rel="external nofollow">Kenney.nl: Platformer Art Deluxe</a>. ومن المفترض أن يكون وضع الشريط ضمن شاشة عرض معلومات المستخدم HUD أو واجهة المستخدم UI سهلًا، لذا من المنطقي أن نبني هذا الشريط ضمن مشهد مستقل.
</p>

<p>
	سنبدأ بعقدة من النوع <code>Node2D</code> كي نُبقى اﻷمور على نفس السوية، ونضبط قيمة الخاصية <code>Sepration</code> ضمن <code>Constants</code> في القسم  <code>Theme Overrides</code> من الفاحص على القيمة <code>5</code>.
</p>

<p>
	نضيف بعد ذلك عقدة ابن من النوع <code>TextureRect</code> ثم نسحب أيقونة القلب إلى الخاصية <code>Texture</code> ونضبط قيمة <code>Strech Mode</code> على <code>Keep</code>. نعيد تسمية العقدة لتكون <code>1</code> ثم باستخدام مفتاحي Ctrl+D ننسخ هذه العقدة بعدد القلوب التي نريد عرضها في الشريط 5 مثلًا. ستبدو لوحة العقد في محرك جودو كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169745" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_heart_bar_nodes.png.a445bf1c5bb64098faf69d19e06c45c5.png" rel=""><img alt="02 heart bar nodes" class="ipsImage ipsImage_thumbnailed" data-fileid="169745" data-unique="q3siqvw1b" src="https://academy.hsoub.com/uploads/monthly_2025_03/02_heart_bar_nodes.png.a445bf1c5bb64098faf69d19e06c45c5.png"> </a>
</p>

<h3 id="-3">
	إضافة السكريبت
</h3>

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

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

enum modes </span><span class="pun">{</span><span class="pln">SIMPLE</span><span class="pun">,</span><span class="pln"> EMPTY</span><span class="pun">,</span><span class="pln"> PARTIAL</span><span class="pun">}</span><span class="pln">

var heart_full </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://assets/hud_heartFull.png"</span><span class="pun">)</span><span class="pln">
var heart_empty </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://assets/hud_heartEmpty.png"</span><span class="pun">)</span><span class="pln">
var heart_half </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://assets/hud_heartHalf.png"</span><span class="pun">)</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var mode </span><span class="pun">:</span><span class="pln"> modes

func update_health</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    match mode</span><span class="pun">:</span><span class="pln">
      MODES</span><span class="pun">.</span><span class="pln">simple</span><span class="pun">:</span><span class="pln">
        update_simple</span><span class="pun">(</span><span class="pln">value</span><span class="pun">)</span><span class="pln">
      MODES</span><span class="pun">.</span><span class="pln">empty</span><span class="pun">:</span><span class="pln">
        update_empty</span><span class="pun">(</span><span class="pln">value</span><span class="pun">)</span><span class="pln">
      MODES</span><span class="pun">.</span><span class="pln">partial</span><span class="pun">:</span><span class="pln">
        update_partial</span><span class="pun">(</span><span class="pln">value</span><span class="pun">)</span></pre>

<p>
	يؤدي استدعاء الدالة <code>()update_health</code> العائدة إلى الشريط عرض القيمة الممرة إليه وفقًا للنمط المختار.
</p>

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

<p>
	نتنقل في الدالة <code>()update_simple</code> بين أشرطة الأيقونات ونضبط ظهور كل عقدة <code>TextureRect</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_9" style=""><span class="pln">func update_simple</span><span class="pun">(</span><span class="pln">value</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"> get_child_count</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">visible </span><span class="pun">=</span><span class="pln"> value </span><span class="pun">&gt;</span><span class="pln"> i</span></pre>

<p>
	واﻷمر مشابه في الدالة <code>()update_empty</code> ما عدا أننا نغير اﻷيقونة إلى اﻷيقونة الفارغة بدلًا من إخفائها:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_11" style=""><span class="pln">func update_empty</span><span class="pun">(</span><span class="pln">value</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"> get_child_count</span><span class="pun">():</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> value </span><span class="pun">&gt;</span><span class="pln"> i</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"> heart_full
      </span><span class="kwd">else</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"> heart_empty</span></pre>

<p>
	أما في الحالة اﻷخيرة، فلدينا أيقونة ثالثة وضعف القيم الممكنة فمن خلال إنقاص القيمة بمقدار 1 مثلًا يعطي نصف قلب وإنقاص 1 مرة أخرى تعطي قلبًا فارغًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_13" style=""><span class="pln">func update_partial</span><span class="pun">(</span><span class="pln">value</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"> get_child_count</span><span class="pun">():</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> value </span><span class="pun">&gt;</span><span class="pln"> i </span><span class="pun">*</span><span class="pln"> </span><span class="lit">2</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">
        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"> heart_full
      </span><span class="kwd">elif</span><span class="pln"> value </span><span class="pun">&gt;</span><span class="pln"> i </span><span class="pun">*</span><span class="pln"> </span><span class="lit">2</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"> heart_half
      </span><span class="kwd">else</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"> heart_empty</span></pre>

<p>
	توضح الصورة أدناه مثالًا عن عمل كل شريط:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169746" href="https://academy.hsoub.com/uploads/monthly_2025_03/03_heart_bar_final.gif.1622b8f3ec60e5564eea4c9279d70e33.gif" rel=""><img alt="03 heart bar final" class="ipsImage ipsImage_thumbnailed" data-fileid="169746" data-ratio="52.80" data-unique="795n555v5" style="width: 250px; height: auto;" width="250" src="https://academy.hsoub.com/uploads/monthly_2025_03/03_heart_bar_final.gif.1622b8f3ec60e5564eea4c9279d70e33.gif"> </a>
</p>

<h2 id="-4">
	إنشاء نصوص طافية فوق الشخصية
</h2>

<p>
	هناك طرق عدة لتحقيق النصوص الطافية floating text، منها استخدام خط كتابة نقطية bitmap font وبناء صورة لكل عدد انطلاقًا من اﻷرقام المكونة له، ومن ثم استخدام العقدة <code>Sprite2D</code> لعرض وتحريك النص الناتج.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169747" href="https://academy.hsoub.com/uploads/monthly_2025_03/04_fct_demo.gif.5ae6ab0684a10024a7233d98334480b5.gif" rel=""><img alt="04 fct demo" class="ipsImage ipsImage_thumbnailed" data-fileid="169747" data-ratio="66.00" data-unique="uvxfbgntf" style="width: 300px; height: auto;" width="300" src="https://academy.hsoub.com/uploads/monthly_2025_03/04_fct_demo.gif.5ae6ab0684a10024a7233d98334480b5.gif"> </a>
</p>

<p>
	لكن ما سنفعله في مقالنا هو استخدام العقدة <code>Label</code> واسمها FCT وبهذا سنمتلك مرونة في تغيير الخط إضافة إلى سهولة عرض اﻷعداد كنصوص أو عرض نصوص أخرى مثل "أخفق miss".
</p>

<p>
	نضيف المورد الذي نريده في الخاصية <code>Label Settings</code> ونختار خطًا مناسبًا وقياسًا مناسبًا له، وقد استخدمنا في المثال الخط Xolonium.ttf والقياس <code>28</code> مع إطار خارجي أسود بعرض 4 بكسل.
</p>

<p>
	نضيف اﻵن السكريبت التالي إلى العقدة <code>Label</code>:
</p>

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

func show_value</span><span class="pun">(</span><span class="pln">value</span><span class="pun">,</span><span class="pln"> travel</span><span class="pun">,</span><span class="pln"> duration</span><span class="pun">,</span><span class="pln"> spread</span><span class="pun">,</span><span class="pln"> crit</span><span class="pun">=</span><span class="pln">false</span><span class="pun">):</span></pre>

<p>
	نستدعي عند توليد النصوص الطافية الدالة <code>()show_value</code> التي تضبط قيم المعاملات التالية:
</p>

<ul>
	<li>
		<code>value</code> وهو العدد أو النص الذي نريد توليده
	</li>
	<li>
		<code>travel</code> وهو عقدة شعاع <code>Vector2</code> التي تمثل اتجاه حركة النص أو العدد
	</li>
	<li>
		<code>duration</code> تحدد كم سيبقى النص على قيد الحياة
	</li>
	<li>
		<code>spread</code> يحدد أن الحركة ستكون عشوائية عبر هذا القوس
	</li>
	<li>
		<code>crit</code> يشير لأن الضرر كبير في حال كانت قيمته <code>true</code>
	</li>
</ul>

<p>
	وهذا ما تفعله الدالة <code>()show_value</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_19" style=""><span class="pln">    text </span><span class="pun">=</span><span class="pln"> value
    var movement </span><span class="pun">=</span><span class="pln"> travel</span><span class="pun">.</span><span class="pln">rotated</span><span class="pun">(</span><span class="pln">rand_range</span><span class="pun">(-</span><span class="pln">spread</span><span class="pun">/</span><span class="lit">2</span><span class="pun">,</span><span class="pln"> spread</span><span class="pun">/</span><span class="lit">2</span><span class="pun">))</span><span class="pln">
    rect_pivot_offset </span><span class="pun">=</span><span class="pln"> rect_size </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_21" style=""><span class="pln">    $Tween</span><span class="pun">.</span><span class="pln">interpolate_property</span><span class="pun">(</span><span class="pln">self</span><span class="pun">,</span><span class="pln"> </span><span class="str">"rect_position"</span><span class="pun">,</span><span class="pln">
        rect_position</span><span class="pun">,</span><span class="pln"> rect_position </span><span class="pun">+</span><span class="pln"> movement</span><span class="pun">,</span><span class="pln">
        duration</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TRANS_LINEAR</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">EASE_IN_OUT</span><span class="pun">)</span><span class="pln">
    $Tween</span><span class="pun">.</span><span class="pln">interpolate_property</span><span class="pun">(</span><span class="pln">self</span><span class="pun">,</span><span class="pln"> </span><span class="str">"modulate:a"</span><span class="pun">,</span><span class="pln">
        </span><span class="lit">1.0</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.0</span><span class="pun">,</span><span class="pln"> duration</span><span class="pun">,</span><span class="pln">
        </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TRANS_LINEAR</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">EASE_IN_OUT</span><span class="pun">)</span></pre>

<p>
	نجري بعد ذلك استيفاء interpolation على قيمتي الخاصية بين لحظتين هما <code>rect_position</code> لتحريك العدد الطافي و <code>modulate.a</code> ﻹخفاء هذا النص بشكل تدريجي وسلس:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_23" style=""><span class="pln">    </span><span class="kwd">if</span><span class="pln"> crit</span><span class="pun">:</span><span class="pln">
      modulate </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Color</span><span class="pun">(</span><span class="lit">1</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">0</span><span class="pun">)</span><span class="pln">
      $Tween</span><span class="pun">.</span><span class="pln">interpolate_property</span><span class="pun">(</span><span class="pln">self</span><span class="pun">,</span><span class="pln"> </span><span class="str">"rect_scale"</span><span class="pun">,</span><span class="pln">
        rect_scale</span><span class="pun">*</span><span class="lit">2</span><span class="pun">,</span><span class="pln"> rect_scale</span><span class="pun">,</span><span class="pln">
        </span><span class="lit">0.4</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">TRANS_BACK</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">EASE_IN</span><span class="pun">)</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_25" style=""><span class="pln">    $Tween</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">yield</span><span class="pun">(</span><span class="pln">$Tween</span><span class="pun">,</span><span class="pln"> </span><span class="str">"tween_all_completed"</span><span class="pun">)</span><span class="pln">
    queue_free</span><span class="pun">()</span></pre>

<p>
	نبدأ بعد ذلك عملية بناء اﻹطارات البينية من خلال التعليمة <code>Tween</code> وننتظر حتى تنتهي ثم نزيل العنوان <code>Label</code>.
</p>

<h3 id="-5">
	إدراة النصوص الطافية FCTManager
</h3>

<p>
	ننشئ اﻵن عقدة صغيرة تدير توليد النصوص وتحديد مكانها، وستُلحق بكيانات اللعبة التي نريد أن نضيف إليها تأثير النصوص الطافية. نسمي هذه العقدة من النوع <code>Node2D</code> بالاسم <code>FCTManager</code> ونضيف إليها السكريبت التالي:
</p>

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

var FCT </span><span class="pun">=</span><span class="pln"> preload</span><span class="pun">(</span><span class="str">"res://FCT.tscn"</span><span class="pun">)</span><span class="pln">

export var travel </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">80</span><span class="pun">)</span><span class="pln">
export var duration </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
export var spread </span><span class="pun">=</span><span class="pln"> PI</span><span class="pun">/</span><span class="lit">2</span><span class="pln">

func show_value</span><span class="pun">(</span><span class="pln">value</span><span class="pun">,</span><span class="pln"> crit</span><span class="pun">=</span><span class="pln">false</span><span class="pun">):</span><span class="pln">
    var fct </span><span class="pun">=</span><span class="pln"> FCT</span><span class="pun">.</span><span class="pln">instance</span><span class="pun">()</span><span class="pln">
    add_child</span><span class="pun">(</span><span class="pln">fct</span><span class="pun">)</span><span class="pln">
    fct</span><span class="pun">.</span><span class="pln">show_value</span><span class="pun">(</span><span class="pln">str</span><span class="pun">(</span><span class="pln">value</span><span class="pun">),</span><span class="pln"> travel</span><span class="pun">,</span><span class="pln"> duration</span><span class="pun">,</span><span class="pln"> spread</span><span class="pun">,</span><span class="pln"> crit</span><span class="pun">)</span></pre>

<p>
	يمكن تعديل ما نريده من خصائص العقدة من خلال نافذة الفاحص Inspector، لكن الدالة <code>()show_value</code> أيضًا تولد النص الطافي وتضبط خصائصه. وبإمكاننا إلحاق نسخة من هذه العقدة بأي وحدة من وحدات اللعبة نريدها أن تمتلك تأثير النصوص الطافية، ثم نضيف كود مشابه لما يلي ضمن التابع <code>()take_damage</code> للوحدة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_8985_29" style=""><span class="pln">$FCTManager</span><span class="pun">.</span><span class="pln">show_value</span><span class="pun">(</span><span class="pln">dmg</span><span class="pun">,</span><span class="pln"> crit</span><span class="pun">)</span></pre>

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

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

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

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/ui/heart_containers_3/index.html" rel="external nofollow">HeartContainers: 3 ways</a> و <a href="https://kidscancode.org/godot_recipes/4.x/ui/floating_text/index.html" rel="external nofollow">Floating Combat Text</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%B9%D8%B1%D8%B6-%D8%B9%D8%AF%D8%A7%D8%AF-%D8%AA%D9%86%D8%A7%D8%B2%D9%84%D9%8A-countdown-%D9%88%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AF%D8%A7%D8%A6%D8%B1%D9%8A%D8%A9-radial-menu-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2534/" rel="">عرض عداد تنازلي Countdown وقائمة دائرية Radial Menu في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%A5%D8%B4%D8%A7%D8%B1%D8%A7%D8%AA-signals-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2385/" rel="">استخدام الإشارات Signals في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D9%85%D8%A7%D8%B9-%D9%84%D9%85%D8%AF%D8%AE%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%84%D8%A7%D8%B9%D8%A8-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2384/" 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">2541</guid><pubDate>Wed, 26 Mar 2025 12:01:01 +0000</pubDate></item><item><title>&#x639;&#x631;&#x636; &#x639;&#x62F;&#x627;&#x62F; &#x62A;&#x646;&#x627;&#x632;&#x644;&#x64A; Countdown &#x648;&#x642;&#x627;&#x626;&#x645;&#x629; &#x62F;&#x627;&#x626;&#x631;&#x64A;&#x629; Radial Menu &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%B9%D8%B1%D8%B6-%D8%B9%D8%AF%D8%A7%D8%AF-%D8%AA%D9%86%D8%A7%D8%B2%D9%84%D9%8A-countdown-%D9%88%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AF%D8%A7%D8%A6%D8%B1%D9%8A%D8%A9-radial-menu-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2534/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/_.png.81374ac6814d97d4513d627cc551849d.png" /></p>
<p>
	سنتعرف في هذا المقال على طريقة بناء أزرار عد تنازلي countdown ضمن ألعاب جودو لتساعدنا في تحقيق ميزة الانتظار في اللعبة، مثلًا يمكن أن لا تتفعّل ميزة أو قدرة معينة للاعب ما إلا بعد مضي فترة زمنية معينة، كما سنشرح طريقة بناء قائمة دائرية الشكل Radial menu تعرض للاعب عدة خيارات موزعة على شكل حلقة لتسهيل الوصول لكل خيار وإضافة طابع فريد لواجهة اللعبة.
</p>

<h2 id="">
	بناء أزرار العد التنازلي
</h2>

<p>
	قد نرغب في إضافة عدة أزرار تمنح اللاعب مهارات أو قدرات خاصة ability buttons مع توفير ميزة الانتظار لفترة معينة قبل تمكين اللاعب من النقر على كل زر منها واكتساب القدرة المطلوبة، يمكننا تحقيق ذلك من خلال ميزة العد التنازلي countdown، وفي حال احتجنا لأيقونات ورسومات مناسبة لاستخدامها مع هذه الأزرار فستجد كمًا كبيرًا من التصاميم المناسبة في موقع <a href="https://game-icons.net/" rel="external nofollow">Game-icons.net</a> وسنستخدم بعضها في مقالنا.
</p>

<p style="text-align: center;">
	<img alt="001 زر القدرة الخاصة" class="ipsImage ipsImage_thumbnailed" data-fileid="169661" data-ratio="96.88" data-unique="qvvlvbq9s" style="width: 64px; height: auto;" width="64" src="https://academy.hsoub.com/uploads/monthly_2025_03/cooldown_01.gif.679739ce15782459d7ba92b1bad89484.gif">
</p>

<h2 id="-1">
	إعداد مشهد اللعبة
</h2>

<p>
	يضم المشهد الذي سنعمل عليه العقد nodes التالية:
</p>

<p style="text-align: center;">
	<img alt="عقد المشهد" class="ipsImage ipsImage_thumbnailed" data-fileid="169662" data-ratio="41.00" data-unique="ohwblncjw" style="width: 300px; height: auto;" width="508" src="https://academy.hsoub.com/uploads/monthly_2025_03/002.PNG.57da9fe653f1138dc037949f06c23721.PNG">
</p>

<p>
	لنوضح بإيجاز دور كل عقدة منها:
</p>

<table>
	<thead>
		<tr>
			<th>
				<strong>العقدة</strong>
			</th>
			<th>
				<strong>النوع</strong>
			</th>
			<th>
				<strong>الوظيفة</strong>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>
				AbilityButton
			</td>
			<td>
				<code>TextureButton</code>
			</td>
			<td>
				زر ينشط قدرة خاصة للاعب عند الضغط عليه
			</td>
		</tr>
		<tr>
			<td>
				Sweep
			</td>
			<td>
				<code>TextureProgress</code>
			</td>
			<td>
				شريط تقدم يظهر عد تنازلي بعد الضغط على الزر
			</td>
		</tr>
		<tr>
			<td>
				Timer
			</td>
			<td>
				<code>Timer</code>
			</td>
			<td>
				مؤقت يتحكم في فترة التهدئة Cooldown قبل التمكن من إعادة استخدام القدرة الخاصة
			</td>
		</tr>
		<tr>
			<td>
				Counter
			</td>
			<td>
				<code>MarginContainer</code>
			</td>
			<td>
				حاوية خاصة تتيح إضافة هوامش Margins بين عناصرها
			</td>
		</tr>
		<tr>
			<td>
				Value
			</td>
			<td>
				<code>Label</code>
			</td>
			<td>
				مكون تسمية توضيحية يعرض التوقيت
			</td>
		</tr>
	</tbody>
</table>

<p>
	سنحدد الأيقونة الخاصة بكل زر من خلال الخاصية <code>Textures</code>  ثم <code>Normal </code>لزر القدرة  <code>AbilityButton</code> حيث يمكننا من هنا تحديد الأيقونة  الافتراضية للزر عندما لا يكون مضغوطًا، ثم نختار القيمة <code>Full Rect</code> في العقدة <code>Sweep</code> من القائمة <code>Presets</code> لتحديد تأثيرات التعبئة أو المسح التدريجي ليكون على كامل الزر. بعد ذلك، نضبط الخاصية <code>FillMode</code> للعقدة <code>Counter</code> بالقيمة <code>clockwise</code>.
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169350" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_cooldown_color.png.56918179d2c00a1941e7e224f9e1a330.png" rel=""><img alt="02 cooldown color" class="ipsImage ipsImage_thumbnailed" data-fileid="169350" data-unique="0tdxr99kj" src="https://academy.hsoub.com/uploads/monthly_2025_03/02_cooldown_color.png.56918179d2c00a1941e7e224f9e1a330.png"> </a>
</p>

<p>
	نضبط عقدة المؤقت <code>Timer</code> على القيمة <code>One Shot</code> لجعلها تعمل مرة واحدة فقط، وفيما يخص العقدة <code>Counter</code> وهي حاوية تحتوي النص وتحاذيه، ينبغي ضبط تخطيطها على <code>Bottom Wide</code> وضبط الخاصيتين <code>Margin Right</code> و <code>Margin Left</code> للمسافات الجانبية على القيمة <code>5</code> وذلك ضمن القسم <code>Theme Overrides </code>ثم <code>Constants.</code>
</p>

<p>
	بالنسبة للعقدة <code>value</code> سنضبط خاصية المحاذاة الأفقية <code>Horizontal Alignment</code> على القيمة <code>Right</code>، وخاصية اقتصاص النص <code>Clip Text</code> على القيمة <code>on</code> لتجنب تجاوز النص لحدود الحاوية. ونختار الخط المناسب من القسم <code>Theme Overrides</code> ثم <code>Font</code> ونضع قيمة <code>0.0</code> في الحقل النصي. وطالما أن اﻷيقونة التي نستخدمها سوداء، فمن الجيد ضبط قيمة خاصية حجم الحدود <code>Theme Outline Size</code> من القسم  <code>Overrides </code>ثم  Constants  بالقيمة <code>1</code> لجعل الأيقونة أكثر وضوحًا.
</p>

<h3 id="-2">
	إضافة كود برمجي لزر العد التنازلي
</h3>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_13" style=""><span class="pln">extends </span><span class="typ">TextureButton</span><span class="pln">
class_name </span><span class="typ">AbilityButton</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var time_label </span><span class="pun">=</span><span class="pln"> $Counter</span><span class="pun">/</span><span class="typ">Value</span><span class="pln">

</span><span class="lit">@export</span><span class="pln"> var cooldown </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1.0</span><span class="pln">


func _ready</span><span class="pun">():</span><span class="pln">
    time_label</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span><span class="pln">
    $Sweep</span><span class="pun">.</span><span class="pln">value </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
    $Sweep</span><span class="pun">.</span><span class="pln">texture_progress </span><span class="pun">=</span><span class="pln"> texture_normal
    $Timer</span><span class="pun">.</span><span class="pln">wait_time </span><span class="pun">=</span><span class="pln"> cooldown
    set_process</span><span class="pun">(</span><span class="pln">false</span><span class="pun">)</span></pre>

<p>
	يبدأ السكريبت بتصدير المتغير <code>cooldown</code> الذي يحدد طول فترة الانتظار قبل تفعيل الزر، ومن ثم نضبط المؤقت <code>Timer</code> داخل التابع <code>()ready_</code> لاستخدام هذه القيمة.
</p>

<p>
	سنحتاج بعد ذلك لخامة texture لنسندها إلى <code>TextureProgress</code>، سنستخدم نفس خامة الزر، ويمكن استخدام أي خامة أخرى نفضلها.
</p>

<p>
	أخيرًا، لنتأكد من أن العمليات الخاصة بالمتغير <code>Sweep</code> قد انتهت بشكل صحيح، سنتأكد إن كانت قيمة <code>Sweep</code> هي <code>0</code> ونضبط قيمة معالجة العقدة <code>processing </code>على <code>false</code>. وبما أننا ننفذ التحريك ضمن التابع <code>()process_</code> لذا لا نحتاج لتنفيذ هذا التابع إن لم نكن في فترة التهدئة CoolDown.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_17" style=""><span class="pln">func _process</span><span class="pun">(</span><span class="pln">delta</span><span class="pun">):</span><span class="pln">
    time_label</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> </span><span class="str">"%3.1f"</span><span class="pln"> </span><span class="pun">%</span><span class="pln"> $Timer</span><span class="pun">.</span><span class="pln">time_left
    $Sweep</span><span class="pun">.</span><span class="pln">value </span><span class="pun">=</span><span class="pln"> int</span><span class="pun">((</span><span class="pln">$Timer</span><span class="pun">.</span><span class="pln">time_left </span><span class="pun">/</span><span class="pln"> cooldown</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> </span><span class="lit">100</span><span class="pun">)</span></pre>

<p>
	نلاحظ في الكود السابق أننا استخدمنا الخاصية <code>time_left</code> للمؤقت <code>Timer</code> لضبط الخاصية <code>text</code> للعقدة <code>labe</code> والخاصية <code>value</code> للعقدة <code>Sweep</code>.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_22" style=""><span class="pln">func _on_AbilityButton_pressed</span><span class="pun">():</span><span class="pln">
    disabled </span><span class="pun">=</span><span class="pln"> true
    set_process</span><span class="pun">(</span><span class="pln">true</span><span class="pun">)</span><span class="pln">
    $Timer</span><span class="pun">.</span><span class="pln">start</span><span class="pun">()</span><span class="pln">
    time_label</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span></pre>

<p>
	عندما يُنقر الزر سيبدأ كل شيء:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_24" style=""><span class="pln">func _on_Timer_timeout</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"ability ready"</span><span class="pun">)</span><span class="pln">
    $Sweep</span><span class="pun">.</span><span class="pln">value </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pln">
    disabled </span><span class="pun">=</span><span class="pln"> false
    time_label</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span><span class="pln">
    set_process</span><span class="pun">(</span><span class="pln">false</span><span class="pun">)</span></pre>

<p>
	كما يعود كل شيء إلى وضعه عندما ينتهي المؤقت من العد.
</p>

<p>
	بإمكاننا وضع عدة أزرار ضمن عقدة حاوية من النوع <code>HBoxContainer</code> وسنحصل على شريط أفقي من أزرار القدرة كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169351" href="https://academy.hsoub.com/uploads/monthly_2025_03/03_cooldown_ready.gif.5310a97a84b14fa104e66c11cec9e6bd.gif" rel=""><img alt="03 cooldown ready" class="ipsImage ipsImage_thumbnailed" data-fileid="169351" data-unique="5n8g9qqhe" src="https://academy.hsoub.com/uploads/monthly_2025_03/03_cooldown_ready.gif.5310a97a84b14fa104e66c11cec9e6bd.gif"> </a>
</p>

<h2 id="-3">
	بناء قائمة دائرية منبثقة
</h2>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169352" href="https://academy.hsoub.com/uploads/monthly_2025_03/04_ui_radial_menu.png.3828cf7397f3ba95301ac5449f5d27ba.png" rel=""><img alt="04 ui radial menu" class="ipsImage ipsImage_thumbnailed" data-fileid="169352" data-unique="4zzy3veol" style="width: 300px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/04_ui_radial_menu.png.3828cf7397f3ba95301ac5449f5d27ba.png"> </a>
</p>

<p>
	نحتاج لاستخدام عقدة <code>TextureButton</code> من النوع <code>RadialMenuButton</code> لتكون عقدة جذر وهي تمثل الزر الرئيسي الذي سننقره لفتح أو إغلاق القائمة الدائرية، وعقدة <code>Buttons</code> من النوع <code>control</code> كحاوية تتضمن كافة الأزرار التي نريد عرضها في القائمة الدائرية، ونتأكد من ضبط قيمة الخاصية <code>Mouse</code> ثم <code>Filter</code> على القيمة <code>Ignore</code> كي لا تعترض أفعال النقر على الفأرة. كما سنستخدم تسعة أزرار لعرض القدرات الخاصة من نوع العداد التنازلي <code>Cooldown</code>.
</p>

<p>
	الخطوة التالية هي إضافة السكريبت التالي للعقدة الجذر:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_28" style=""><span class="pln">extends </span><span class="typ">TextureButton</span><span class="pln">
class_name </span><span class="typ">RadialMenuButton</span><span class="pln">

export var radius </span><span class="pun">=</span><span class="pln"> </span><span class="lit">120</span><span class="pln">
export var speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.25</span><span class="pln">

var num
var active </span><span class="pun">=</span><span class="pln"> false</span></pre>

<p>
	يمثل المتغير <code>radius</code> حجم القائمة وهو قطر الدائرة التي سنوزع عليها اﻷزرار، بينما يُستخدم المتغير <code>speed</code> في تحديد سرعة تحريك أزرار القائمة فالقيم اﻷصغر هي اﻷسرع. ويحدد المتغير <code>num</code> عدد اﻷزرار في القائمة، بينما يمثل المتغير <code>active</code> راية flag تدل على إغلاق أو فتح القائمة.
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_30" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    $Buttons</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span><span class="pln">
    num </span><span class="pun">=</span><span class="pln"> $Buttons</span><span class="pun">.</span><span class="pln">get_child_count</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> b </span><span class="kwd">in</span><span class="pln"> $Buttons</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
      b</span><span class="pun">.</span><span class="pln">position </span><span class="pun">=</span><span class="pln"> position</span></pre>

<p>
	نبدأ بإعداد منطق القائمة في التابع <code>()ready_</code> وذلك بإخفاء جميع أزرار القائمة افتراضيًا وضبط المسافة بينها وبين الزر الرئيسي للقائمة. ثم نربط اﻹشارة <code>pressed</code> للزر الرئيسي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_32" style=""><span class="pln">func _on_pressed</span><span class="pun">():</span><span class="pln">
    disabled </span><span class="pun">=</span><span class="pln"> true
    </span><span class="kwd">if</span><span class="pln"> active</span><span class="pun">:</span><span class="pln">
      hide_menu</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
      show_menu</span><span class="pun">()</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_34" style=""><span class="pln">func _on_tween_finished</span><span class="pun">():</span><span class="pln">
    disabled </span><span class="pun">=</span><span class="pln"> false
    </span><span class="kwd">if</span><span class="pln"> </span><span class="kwd">not</span><span class="pln"> active</span><span class="pun">:</span><span class="pln">
      $Buttons</span><span class="pun">.</span><span class="pln">hide</span><span class="pun">()</span></pre>

<p>
	عندما ينتهي تحريك اﻹطارات البينية، ننقل حالة الزر إلى تمكين مجددًا. 
</p>

<p>
	لنلقِ نظرة على الدالة <code>()show_menu</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_37" style=""><span class="pln">func show_menu</span><span class="pun">():</span><span class="pln">
    $Buttons</span><span class="pun">.</span><span class="pln">show</span><span class="pun">()</span><span class="pln">
    var spacing </span><span class="pun">=</span><span class="pln"> TAU </span><span class="pun">/</span><span class="pln"> num
    active </span><span class="pun">=</span><span class="pln"> true
    var tw </span><span class="pun">=</span><span class="pln"> create_tween</span><span class="pun">().</span><span class="pln">set_parallel</span><span class="pun">()</span><span class="pln">
    tw</span><span class="pun">.</span><span class="pln">finished</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">_on_tween_finished</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> b </span><span class="kwd">in</span><span class="pln"> $Buttons</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
      </span><span class="com">#لوضع الزر اﻷول في اﻷعلى PI/2 اطرح </span><span class="pln">
      var a </span><span class="pun">=</span><span class="pln"> spacing </span><span class="pun">*</span><span class="pln"> b</span><span class="pun">.</span><span class="pln">get_position_in_parent</span><span class="pun">()</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> PI </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pln">
      var dest </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="pln">radius</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">).</span><span class="pln">rotated</span><span class="pun">(</span><span class="pln">a</span><span class="pun">)</span><span class="pln">
      tw</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">b</span><span class="pun">,</span><span class="pln"> </span><span class="str">"position"</span><span class="pun">,</span><span class="pln"> dest</span><span class="pun">,</span><span class="pln"> speed</span><span class="pun">).</span><span class="kwd">from</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">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">set_ease</span><span class="pun">(</span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">EASE_OUT</span><span class="pun">)</span><span class="pln">
      tw</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">b</span><span class="pun">,</span><span class="pln"> </span><span class="str">"scale"</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"> speed</span><span class="pun">).</span><span class="kwd">from</span><span class="pun">(</span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0.5</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.5</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_LINEAR</span><span class="pun">)</span></pre>

<p>
	نحسب في هذه الدالة المسافة <code>spacing</code> أو بالأصح الزاوية التي نريدها بين كل عنصرين على القائمة، ومن ثم نتنقل بين اﻷزرار ونحدد وجهة كل زر <code>dest</code> وفقًا للزاوية المحسوبة وقيمة نصف القطر <code>radius</code>. ونولد لكل زر خاصيتين هما <code>position</code> و <code>scale</code> لإعطاء اﻷثر المرغوب عند توليد إطارات التحريك tween أثناء تحرك الزر. وتنفذ الدالة <code>()hide_menu</code> العكس تمامًا:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_3827_39" style=""><span class="pln">func hide_menu</span><span class="pun">():</span><span class="pln">
    active </span><span class="pun">=</span><span class="pln"> false
    var tw </span><span class="pun">=</span><span class="pln"> create_tween</span><span class="pun">().</span><span class="pln">set_parallel</span><span class="pun">()</span><span class="pln">
    tw</span><span class="pun">.</span><span class="pln">finished</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">_on_tween_finished</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">for</span><span class="pln"> b </span><span class="kwd">in</span><span class="pln"> $Buttons</span><span class="pun">.</span><span class="pln">get_children</span><span class="pun">():</span><span class="pln">
      tw</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">b</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"> 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_BACK</span><span class="pun">).</span><span class="pln">set_ease</span><span class="pun">(</span><span class="typ">Tween</span><span class="pun">.</span><span class="pln">EASE_IN</span><span class="pun">)</span><span class="pln">
      tw</span><span class="pun">.</span><span class="pln">tween_property</span><span class="pun">(</span><span class="pln">b</span><span class="pun">,</span><span class="pln"> </span><span class="str">"scale"</span><span class="pun">,</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">0.5</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0.5</span><span class="pun">),</span><span class="pln"> 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_LINEAR</span><span class="pun">)</span></pre>

<p>
	ستبدو القائمة كالتالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169353" href="https://academy.hsoub.com/uploads/monthly_2025_03/05_ui_radial_menu_ready.gif.51a8add9d548bdbd6579cf4a3f414d4f.gif" rel=""><img alt="05 ui radial menu ready" class="ipsImage ipsImage_thumbnailed" data-fileid="169353" data-unique="z6x2gisw6" style="width: 250px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/05_ui_radial_menu_ready.gif.51a8add9d548bdbd6579cf4a3f414d4f.gif"> </a>
</p>

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

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

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/ui/cooldown_button/index.html" rel="external nofollow">CoolDown Button</a> و <a href="https://kidscancode.org/godot_recipes/4.x/ui/radial_menu/index.html" rel="external nofollow">Radial Popup Menu</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%81%D8%A3%D8%B1%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2533/" rel="">التعامل مع إجراءات دخل الفأرة في جودو</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>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">تعرف على أشهر محركات الألعاب Game Engines</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" rel="">الطريقة الصحيحة للتواصل بين العقد في جودو</a>
	</li>
</ul>

<p>
	 
</p>
]]></description><guid isPermaLink="false">2534</guid><pubDate>Thu, 20 Mar 2025 12:00:00 +0000</pubDate></item><item><title>&#x627;&#x644;&#x62A;&#x639;&#x627;&#x645;&#x644; &#x645;&#x639; &#x625;&#x62C;&#x631;&#x627;&#x621;&#x627;&#x62A; &#x62F;&#x62E;&#x644; &#x627;&#x644;&#x641;&#x623;&#x631;&#x629; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%AF%D8%AE%D9%84-%D8%A7%D9%84%D9%81%D8%A3%D8%B1%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2533/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/764259279_.png.af124a73bb3c46f8363d1b15ce8749dd.png" /></p>
<p>
	نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي <code>InputEventMouse</code> الذي يتضمن الخاصيتين <code>position</code> و <code>global_position</code> ويرث من هذا الصنف كل من الصنفين <code>InputEventMouseButton</code> و <code>InputEventMouseMotion</code>.
</p>

<p>
	<strong>ملاحظة</strong>: بإمكاننا تعيين أحداث النقر على أزرار الفأرة من خلال الصنف <code>InputMap</code> وهو صنف متفرد <a href="https://wiki.hsoub.com/Design_Patterns/singleton" rel="external">singleton</a> وبالتالي سنتمكن من استخدامها مع الدالة <code>()is_action_pressed</code>.
</p>

<h2 id="inputeventmousebutton">
	استخدام الصنف <code>InputEventMouseButton</code>
</h2>

<p>
	يضم الصنف <code>GlobalScope.ButtonList@</code> قائمة بكل ثوابت اﻷزرار الممكنة <code>*_BUTTON</code> التي قد نحددها في الخاصية <code>button_index</code>. ولنتذكر أن عجلة التمرير في الفأرة scrollwheel تُعد زرًا -أو زرين إن أردنا توخي الدقة- لأن الحدثين <code>BUTTON_WHEEL_UP</code> و <code>BUTTON_WHEEL_DOWN</code> منفصلان.
</p>

<p>
	<strong>تلميح</strong>: يولد النقر على عجلة تمرير الفأرة الحدث <code>pressed</code> فقط، ولا يوجد حدث لتحرير الزر كما في اﻷزرار الأخرى.
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_7" 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"> 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="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"> BUTTON_LEFT</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">pressed</span><span class="pun">:</span><span class="pln">
          </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"Left button was clicked at "</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="kwd">else</span><span class="pun">:</span><span class="pln">
          </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"Left button was released"</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">button_index </span><span class="pun">==</span><span class="pln"> BUTTON_WHEEL_DOWN</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"Wheel down"</span><span class="pun">)</span></pre>

<h2 id="inputeventmousemotion">
	استخدام الصنف <code>InputEventMouseMotion</code>
</h2>

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

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_11" style=""><span class="com"># حساسية الفأرة التي تتحكم في سرعة التدوير عند تحريك الفأرة</span><span class="pln">
var mouse_sensitivity </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0.002</span><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"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseMotion</span><span class="pun">:</span><span class="pln">
      rotate_y</span><span class="pun">(-</span><span class="pln">event</span><span class="pun">.</span><span class="pln">relative</span><span class="pun">.</span><span class="pln">x </span><span class="pun">*</span><span class="pln"> mouse_sensitivity</span><span class="pun">)</span></pre>

<h2 id="-1">
	الاحتفاظ بمؤشر الفأرة ضمن نافذة اللعبة
</h2>

<p>
	بإمكاننا إخفاء مؤشر الفأرة ومنعها من مغادرة نافذة اللعبة، وهذا سلوك شائع في اﻷلعاب ثلاثية الأبعاد وحتى بعض اﻷلعاب ثنائية البعد. وللفأرة أربعة أنماط يمكنك اختيار أي منها باستخدام <code>Input.mouse_mode</code>:
</p>

<ul>
	<li>
		MOUSE<em>_MODE</em>_VISIBLE: المؤشر مرئي ويمكن تحريكه بحرية داخل وخارج نافذة اللعبة
	</li>
	<li>
		MOUSE_<em>MODE_</em>HIDDEN:المؤشر مخفي ويمكن له مغادرة نافذة اللعبة
	</li>
	<li>
		MOUSE_<em>MODE</em>_CAPTURED:المؤشر مخفي ولا يمكن له مغادرة نافذة اللعبة
	</li>
	<li>
		MOUSE_<em>MODE</em>_CONFINED:مؤشر الفأرة مرئي ولا يمكن له مغادرة نافذة اللعبة
	</li>
</ul>

<p>
	الخيار الثالث <code>Captured</code> هو الخيار اﻷكثر شيوعًا، ويمكننا أيضًا ضبط حالة مؤشر الفأرة أثناء التنفيذ:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_13" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">mouse_mode </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">MOUSE_MODE_CAPTURED</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_15" 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="pun">.</span><span class="pln">is_action_pressed</span><span class="pun">(</span><span class="str">"ui_cancel"</span><span class="pun">):</span><span class="pln">
      </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">mouse_mode </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">MOUSE_MODE_VISIBLE</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_17" style=""><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">mouse_mode </span><span class="pun">==</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">MOUSE_MODE_CAPTURED</span><span class="pun">:</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_19" style=""><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">
      </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">mouse_mode </span><span class="pun">==</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">MOUSE_MODE_VISIBLE</span><span class="pun">:</span><span class="pln">
        </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">mouse_mode </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Input</span><span class="pun">.</span><span class="pln">MOUSE_MODE_CAPTURED</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_21" style=""><span class="pln">get_tree</span><span class="pun">().</span><span class="pln">set_input_as_handled</span><span class="pun">()</span></pre>

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

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169344" href="https://academy.hsoub.com/uploads/monthly_2025_03/01_multi_unit_select.gif.5cd27cf3c409f0d8d628303f21812972.gif" rel=""><img alt="01 multi unit select" class="ipsImage ipsImage_thumbnailed" data-fileid="169344" data-ratio="56.33" data-unique="9a0hikzc7" style="width: 300px; height: auto;" width="300" src="https://academy.hsoub.com/uploads/monthly_2025_03/01_multi_unit_select.gif.5cd27cf3c409f0d8d628303f21812972.gif"> </a>
</p>

<h3 id="-3">
	إعداد عناصر اللعب
</h3>

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

<h3 id="-4">
	إعداد عالم اللعبة
</h3>

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

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

var dragging </span><span class="pun">=</span><span class="pln"> false </span><span class="com"># هل نسحب الفأرة حاليًا؟</span><span class="pln">
var selected </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span><span class="pln"> </span><span class="com"># مصفوفة العناصر المسحوبة</span><span class="pln">
var drag_start </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="com"># موقع بداية السحب</span><span class="pln">
var select_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"> </span><span class="com"># شكل التصادم لصندوق السحب</span></pre>

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

<h3 id="-5">
	رسم الصندوق
</h3>

<p>
	نستخدم زر الفأرة اﻷيسر في هذه الطريقة، إذ تبدأ عملية النقر برسم مربع السحب وتنهي عملية تحرير زر الفأرة الرسم، وبهذا يُرسم الصندوق أثناء سحب الفأرة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_26" 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"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pln"> </span><span class="kwd">and</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="pun">:</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">pressed</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"> selected</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">0</span><span class="pun">:</span><span class="pln">
          dragging </span><span class="pun">=</span><span class="pln"> true
          drag_start </span><span class="pun">=</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">position
      </span><span class="com"># إن حُرر زر الفأرة ونحن في حالة سحب نوقف السحب</span><span class="pln">
      </span><span class="kwd">elif</span><span class="pln"> dragging</span><span class="pun">:</span><span class="pln">
        dragging </span><span class="pun">=</span><span class="pln"> false
        queue_redraw</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">InputEventMouseMotion</span><span class="pln"> </span><span class="kwd">and</span><span class="pln"> dragging</span><span class="pun">:</span><span class="pln">
      queue_redraw</span><span class="pun">()</span><span class="pln">

func _draw</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> dragging</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">drag_start</span><span class="pun">,</span><span class="pln"> get_global_mouse_position</span><span class="pun">()</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> drag_start</span><span class="pun">),</span><span class="pln">
          </span><span class="typ">Color</span><span class="pun">.</span><span class="pln">YELLOW</span><span class="pun">,</span><span class="pln"> false</span><span class="pun">,</span><span class="pln"> </span><span class="lit">2.0</span><span class="pun">)</span></pre>

<h3 id="-6">
	اختيار العناصر
</h3>

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

<p>
	نستخدم التابع <code>()PhysicsDirectSpaceState2D.intersect_shape</code> ﻹيجاد العناصر، ويتطلب التابع رسم شكل مستطيل في حالتنا وإجراء تحويل transform للموقع:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_30" style=""><span class="kwd">elif</span><span class="pln"> dragging</span><span class="pun">:</span><span class="pln">
    dragging </span><span class="pun">=</span><span class="pln"> false
    queue_redraw</span><span class="pun">()</span><span class="pln">
    var drag_end </span><span class="pun">=</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">position
    select_rect</span><span class="pun">.</span><span class="pln">extents </span><span class="pun">=</span><span class="pln"> abs</span><span class="pun">(</span><span class="pln">drag_end </span><span class="pun">-</span><span class="pln"> drag_start</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span></pre>

<p>
	نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، إذ نستخدمه في تحديد خاصية الامتداد <code>extents</code> للكائن <code>RectangleShape2D</code> (تقاس <code>extents</code> من مركز المستطيل فهي تمثل نصف الارتفاع أو الاتساع)
</p>

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

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_34" style=""><span class="pln">    </span><span class="com"># مساحة عالم اللعبة</span><span class="pln">
    var space </span><span class="pun">=</span><span class="pln"> get_world_2d</span><span class="pun">().</span><span class="pln">direct_space_state
    </span><span class="com"># استعلام بحث عن التصادم</span><span class="pln">
    var query </span><span class="pun">=</span><span class="pln"> </span><span class="typ">PhysicsShapeQueryParameters2D</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
    </span><span class="com"># تحديد الشكل الذي سنبحث عنه</span><span class="pln">
    query</span><span class="pun">.</span><span class="pln">shape </span><span class="pun">=</span><span class="pln"> select_rect
    </span><span class="com"># 2 تحديد العناصر في طبقة التصادم</span><span class="pln">
    query</span><span class="pun">.</span><span class="pln">collision_mask </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2</span><span class="pln"> 
    </span><span class="com"># ضبط موقع الشكل</span><span class="pln">
    query</span><span class="pun">.</span><span class="pln">transform </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Transform2D</span><span class="pun">(</span><span class="lit">0</span><span class="pun">,</span><span class="pln"> </span><span class="pun">(</span><span class="pln">drag_end </span><span class="pun">+</span><span class="pln"> drag_start</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> </span><span class="lit">2</span><span class="pun">)</span><span class="pln">
    </span><span class="com"># البحث عن التصادمات</span><span class="pln">
    selected </span><span class="pun">=</span><span class="pln"> space</span><span class="pun">.</span><span class="pln">intersect_shape</span><span class="pun">(</span><span class="pln">query</span><span class="pun">)</span></pre>

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

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_36" style=""><span class="pun">[{</span><span class="pln"> </span><span class="str">"rid"</span><span class="pun">:</span><span class="pln"> RID</span><span class="pun">(</span><span class="lit">4093103833089</span><span class="pun">),</span><span class="pln"> </span><span class="str">"collider_id"</span><span class="pun">:</span><span class="pln"> </span><span class="lit">32145147326</span><span class="pun">,</span><span class="pln"> </span><span class="str">"collider"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Unit2</span><span class="pun">:&lt;</span><span class="typ">CharacterBody2D</span><span class="com">#32145147326&gt;, "shape": 0 },</span><span class="pln">
</span><span class="pun">{</span><span class="pln"> </span><span class="str">"rid"</span><span class="pun">:</span><span class="pln"> RID</span><span class="pun">(</span><span class="lit">4123168604162</span><span class="pun">),</span><span class="pln"> </span><span class="str">"collider_id"</span><span class="pun">:</span><span class="pln"> </span><span class="lit">32229033411</span><span class="pun">,</span><span class="pln"> </span><span class="str">"collider"</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Unit3</span><span class="pun">:&lt;</span><span class="typ">CharacterBody2D</span><span class="com">#32229033411&gt;, "shape": 0 }]</span></pre>

<p>
	يدل كل متصادم <code>collider</code> في هذه المصفوفة إلى عنصر، لهذا يمكن استخدامه لتمييز العناصر التي اختيرت وتفعيل طار ملون يحيط بهذه العناصر:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_38" style=""><span class="pln">    </span><span class="kwd">for</span><span class="pln"> item </span><span class="kwd">in</span><span class="pln"> selected</span><span class="pun">:</span><span class="pln">
      item</span><span class="pun">.</span><span class="pln">collider</span><span class="pun">.</span><span class="pln">selected </span><span class="pun">=</span><span class="pln"> true</span></pre>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="169345" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_multi_unit_shader.gif.3b7c5741686eb93a5f570be0c4e646a9.gif" rel=""><img alt="02 multi unit shader" class="ipsImage ipsImage_thumbnailed" data-fileid="169345" data-unique="0t2py20g2" src="https://academy.hsoub.com/uploads/monthly_2025_03/02_multi_unit_shader.gif.3b7c5741686eb93a5f570be0c4e646a9.gif"> </a>
</p>

<h3 id="-7">
	إرسال اﻷوامر إلى العناصر
</h3>

<p>
	بإمكاننا اﻵن إرسال أمر التحرك للعناصر لتتجه نحو مكان ما على الشاشة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_5847_40" 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"> event </span><span class="kwd">is</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pln"> </span><span class="kwd">and</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="pun">:</span><span class="pln">
      </span><span class="kwd">if</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">pressed</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"> selected</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">0</span><span class="pun">:</span><span class="pln">
          dragging </span><span class="pun">=</span><span class="pln"> true
          drag_start </span><span class="pun">=</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">position
        </span><span class="com"># وإلا ستخبر النقرة جميع العناصر المختارة بالتحرك</span><span class="pln">
        </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
          </span><span class="kwd">for</span><span class="pln"> item </span><span class="kwd">in</span><span class="pln"> selected</span><span class="pun">:</span><span class="pln">
            item</span><span class="pun">.</span><span class="pln">collider</span><span class="pun">.</span><span class="pln">target </span><span class="pun">=</span><span class="pln"> event</span><span class="pun">.</span><span class="pln">position
            item</span><span class="pun">.</span><span class="pln">collider</span><span class="pun">.</span><span class="pln">selected </span><span class="pun">=</span><span class="pln"> false
          selected </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[]</span></pre>

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

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

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

<p>
	ترجمة -وبتصرف- للمقالات: <a href="https://kidscancode.org/godot_recipes/4.x/input/mouse_input/index.html" rel="external nofollow">Mouse Input</a> و <a href="https://kidscancode.org/godot_recipes/4.x/input/mouse_capture/index.html" rel="external nofollow">Capturing the mouse</a> و <a href="https://kidscancode.org/godot_recipes/4.x/input/multi_unit_select/index.html" rel="external nofollow">Mouse:Drag-Select multiple units</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B9%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D8%AF%D8%AE%D9%84-inputs-actions-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2532/" rel="">العمل مع إجراءات الدخل Inputs Actions في جودو</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AD%D9%81%D8%B8-%D9%88%D8%A7%D8%B3%D8%AA%D8%B1%D8%AC%D8%A7%D8%B9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%AD%D9%84%D9%8A%D8%A9-%D8%A8%D9%8A%D9%86-%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D9%84%D8%B9%D8%A8-r2522/" rel="">حفظ واسترجاع البيانات المحلية بين جلسات اللعب</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>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D8%A7%D9%84%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-3d-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D8%B1-%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2477/" rel="">تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2533</guid><pubDate>Tue, 18 Mar 2025 13:00:00 +0000</pubDate></item><item><title>&#x627;&#x644;&#x639;&#x645;&#x644; &#x645;&#x639; &#x625;&#x62C;&#x631;&#x627;&#x621;&#x627;&#x62A; &#x627;&#x644;&#x62F;&#x62E;&#x644; Inputs Actions &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B9%D9%85%D9%84-%D9%85%D8%B9-%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D8%AF%D8%AE%D9%84-inputs-actions-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2532/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/inputsactions_.png.4d84476026c4cd82d95d4108690f1d03.png" /></p>
<p>
	نشرح في هذا المقال كيفية استخدام إجراءات الدخل Input Actions في محرك الألعاب جودو، والتي توفر لنا طريقة فعالة للتحكم في الشخصيات والعناصر داخل اللعبة. فبدلاً من تحديد كل مفتاح كتابيًا في الشيفرة البرمجية، يمكننا استخدام إجراءات الدخل لتهيئة المدخلات وتخصيص مفاتيح التحكم باللاعب بطريقة منظمة وسهلة التعديل.
</p>

<p>
	لنفترض أن لدينا شخصية تتحرك في لعبتنا من اﻷعلى إلى اﻷسفل وأردنا كتابة شيفرة باستخدام العقدة <code>InputActionKey</code> التي تتيح لنا بالتحكم بالشخصية عبر مفاتيح اﻷسهم في لوحة المفاتيح، لكن سنجد أن الكثير من اللاعبين يفضلون استخادم المفاتيح W و A و S و D للتحكم بالشخصية، قد نحاول العودة إلى اللعبة وإضافة مفاتيح إضافية لكن النتيجة ستكون شيفرة مضاعفة وزائدة.
</p>

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

<h2 id="">
	إنشاء المدخلات
</h2>

<p>
	بإمكاننا تعريف إجراءات دخل وتعيينها من داخل محرر جودو من خلال الانتقال إلى المشروع <strong>Project </strong>ثم إعدادات المشروع <strong>Project Settings</strong> ثم الانتقال لتبويب خريطة اﻹدخال <strong>Input Map</strong>.
</p>

<p>
	سنجد عند النقر على هذا التبويب بعض إجراءات الدخل المهيأة افتراضيًا تُعرف جميعها بالاسم *_ui كي نعلم بوجود إجراءات إدخال افتراضية.
</p>

<p>
	<strong>ملاحظة</strong>: نحتاج لتفعيل مفتاح التبديل أظهر الإجراءات المدمجة <strong>Show default Actions</strong> من أجل عرض إجراءات الدخل.
</p>

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

<p>
	سننشئ إجراءً جديدًا يُدعى shoot بكتابة اسم اﻹجراء ضمن الحقل <strong>إضافة إجراء جديد</strong> <strong>Add New Action</strong> ثم ننقر على زر أضف <strong>Add </strong>أو الضغط على مفتاح Enter، وسنرى أن الإجراء أضيف إلى القائمة الموجودة.
</p>

<p>
	سنتمكن اﻵن من تعيين مدخلات لهذا اﻹجراء بالنقر على الزر <strong>+</strong> إلى اليمين. قد تكون المدخلات من لوحة المفاتيح أو أزرار الفأرة أو عصا التحكم. وقد اخترنا في حالتنا لوحة المفاتيح Key ثم نقرنا ضمن حقل الاستماع إلى المدخلات <strong>listen to inputs</strong> ونقرنا بعدها على مفتاح المسافة Spacebar لتعيينه كمفتاح لحدث الدخل، ثم نقرنا على زر حسنًا <strong>OK </strong>للموافقة على الإضافة كما في الصورة التالية:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169341" href="https://academy.hsoub.com/uploads/monthly_2025_03/01_input_actions.png.88bcb8cebbb945d1a1e961b3c5c35de1.png" rel=""><img alt="01 input actions" class="ipsImage ipsImage_thumbnailed" data-fileid="169341" data-ratio="60.75" data-unique="z26oqgw6m" style="width: 400px; height: auto;" width="400" src="https://academy.hsoub.com/uploads/monthly_2025_03/01_input_actions.png.88bcb8cebbb945d1a1e961b3c5c35de1.png"> </a>
</p>

<p>
	بنفس الطريقة سندخل إجراء آخر ونختار أزرار الفأرة Mouse Button ونتأكد من وجودنا ضمن حقل الاستماع إلى المدخلات <strong>listen to inputs</strong> ثم ننقر زر الفأرة اﻷيسر لتعيينه.
</p>

<h2 id="-1">
	استخدام إجراءات الدخل
</h2>

<p>
	بإمكاننا التحقق من اﻹجراء باستدعاء الصنف <code>Input</code> في كل إطار:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7703_9" 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="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">
      </span><span class="com">#ستُنقذ هذه الشيفرة في كل إطار طالما أن زر اﻹدخال مضغوط</span></pre>

<p>
	ولهذا اﻷمر أهميته للإجراءات المستمرة التي نريد التحقق منها باستمرار مثل حركة اللاعب. لكن إن أردنا التقاط اﻹجراء لحظة وقوعه، نستطيع استخدام دالة رد النداء <code>()input_</code> أو <code>()unhandled_input_</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7703_11" 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"> event</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">
      </span><span class="com"># ستعمل الشيفرة في اﻹطار بمجرد الضغط على عنصر اﻹدخال</span></pre>

<p>
	بإمكاننا استخدام دوال متعددة للتحقق من حالة اﻹدخال:
</p>

<ul>
	<li>
		<code><strong>is_action_pressed</strong></code>: تعيد القيمة <code>true</code> إن كانت حالة اﻹجراء حاليًا <code>pressed</code>
	</li>
	<li>
		<code><strong>is_action_released</strong></code>: تعيد القيمة <code>true</code> إن لم تكن حالة اﻹجراء حاليًا <code>pressed</code>.
	</li>
	<li>
		<strong><code>is_action_just_pressed</code></strong> و <strong><code>is_action_just_released</code></strong>: تعيدان <code>true</code> فقط ضمن إطار واحد عند وقوع الحدث. وهي مفيدة في اﻹجراءات غير المستمرة التي لابد فيها من تحرير الزر ثم ضغطه لتكرار اﻹجراء
	</li>
</ul>

<h2 id="-2">
	إضافة إجراءات دخل إلى الشيفرة مباشرة
</h2>

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

<p>
	فيما يلي مثال يضيف إجراءً جديدًا باسم attack عند الضغط على مفتاح المسافة Spacebar:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7703_13" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">add_action</span><span class="pun">(</span><span class="str">"attack"</span><span class="pun">)</span><span class="pln">
    var ev </span><span class="pun">=</span><span class="pln"> </span><span class="typ">InputEventKey</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
    ev</span><span class="pun">.</span><span class="pln">keycode </span><span class="pun">=</span><span class="pln"> KEY_SPACE
    </span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">action_add_event</span><span class="pun">(</span><span class="str">"attack"</span><span class="pun">,</span><span class="pln"> ev</span><span class="pun">)</span></pre>

<p>
	وإن أردنا أيضًا إضافة النقر على الزر اﻷيسر للفأرة إلى اﻹجراءات:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7703_15" style=""><span class="pln">ev </span><span class="pun">=</span><span class="pln"> </span><span class="typ">InputEventMouseButton</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
ev</span><span class="pun">.</span><span class="pln">button_index </span><span class="pun">=</span><span class="pln"> MOUSE_BUTTON_LEFT
</span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">action_add_event</span><span class="pun">(</span><span class="str">"attack"</span><span class="pun">,</span><span class="pln"> ev</span><span class="pun">)</span></pre>

<p>
	<strong>ملاحظة</strong>: سيرمي التابع <code>()InputMap.add_action</code> خطأ إن كان اﻹجراء موجودًا مسبقًا، لهذا علينا التحقق من وجوده من خلال التابع <code>()InputMap.has_action</code> قبل محاولة إضافته.
</p>

<h3 id="-3">
	مثال تطبيقي
</h3>

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

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_7703_17" style=""><span class="pln">var controls </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span><span class="str">"walk_right"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln">KEY_RIGHT</span><span class="pun">,</span><span class="pln"> KEY_D</span><span class="pun">],</span><span class="pln">
          </span><span class="str">"walk_left"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln">KEY_LEFT</span><span class="pun">,</span><span class="pln"> KEY_A</span><span class="pun">],</span><span class="pln">
          </span><span class="str">"jump"</span><span class="pun">:</span><span class="pln"> </span><span class="pun">[</span><span class="pln">KEY_UP</span><span class="pun">,</span><span class="pln"> KEY_W</span><span class="pun">,</span><span class="pln"> KEY_SPACE</span><span class="pun">]}</span><span class="pln">

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

func add_inputs</span><span class="pun">():</span><span class="pln">
    var ev
    </span><span class="kwd">for</span><span class="pln"> action </span><span class="kwd">in</span><span class="pln"> controls</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"> </span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">has_action</span><span class="pun">(</span><span class="pln">action</span><span class="pun">):</span><span class="pln">
        </span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">add_action</span><span class="pun">(</span><span class="pln">action</span><span class="pun">)</span><span class="pln">
      </span><span class="kwd">for</span><span class="pln"> key </span><span class="kwd">in</span><span class="pln"> controls</span><span class="pun">[</span><span class="pln">action</span><span class="pun">]:</span><span class="pln">
        ev </span><span class="pun">=</span><span class="pln"> </span><span class="typ">InputEventKey</span><span class="pun">.</span><span class="pln">new</span><span class="pun">()</span><span class="pln">
        ev</span><span class="pun">.</span><span class="pln">keycode </span><span class="pun">=</span><span class="pln"> key
        </span><span class="typ">InputMap</span><span class="pun">.</span><span class="pln">action_add_event</span><span class="pun">(</span><span class="pln">action</span><span class="pun">,</span><span class="pln"> ev</span><span class="pun">)</span></pre>

<h2>
	الخاتمة
</h2>

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

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/input/input_actions/index.html" rel="external nofollow">Input Actions</a> و <a href="https://kidscancode.org/godot_recipes/4.x/input/custom_actions/index.html" rel="external nofollow">Adding Input Action in Code</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D9%86%D8%B8%D9%8A%D9%85-%D8%A7%D9%84%D8%B1%D8%B3%D9%88%D9%85-%D8%A7%D9%84%D9%85%D8%AA%D8%AD%D8%B1%D9%83%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-spritesheet-%D9%88-animationtreestate-machine-r2529/" rel="">التحريك باستخدام SpriteSheet و AnimationTree StateMachine في جودو</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%85%D9%81%D9%87%D9%88%D9%85-delta-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2519/" rel="">تعرف على مفهوم Delta في تطوير الألعاب</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>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">تعرف على أشهر محركات الألعاب Game Engines</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2532</guid><pubDate>Thu, 13 Mar 2025 12:02:00 +0000</pubDate></item><item><title>&#x627;&#x644;&#x62A;&#x62D;&#x631;&#x64A;&#x643; &#x628;&#x627;&#x633;&#x62A;&#x62E;&#x62F;&#x627;&#x645; SpriteSheet &#x648; AnimationTree StateMachine &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648;</title><link>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/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/SpriteSheetAnimationTreeStatemachine.png.aec4cff775eed77bd803a4926e4dc704.png" /></p>
<p>
	نشرح في هذا المقال كيفية استخدام جدول SpriteSheet لتنظيم حركة الشخصية في الألعاب الثنائية الأبعاد ضمن محرك الألعاب جودو، كما نوضح دور المتحكم AnimationTreeState Machine في تنظيم حركة الشخصية والتحكم في عمليات التنقل بين حالات الحركة المتختلفة.
</p>

<h2 id="">
	الحركات داخل جدول الشخصيات SpriteSheet
</h2>

<p>
	تُعد جداول الشخصيات Spritesheets من الطرق الشائعة لتوزيع الحركات للرسوم المتحركة ثنائية البعد، إذ توضع جميع إطارات الشخصية ضمن صورة واحدة. سنستخدم في مقالنا شخصية المغامر، ويمكن الحصول عليها وعلى غيرها من الشخصيات من متجر <a href="https://elthen.itch.io/" rel="external nofollow">Elthen's Pixel Art Shop</a> الذي يوفر ملحقات مفيدة يمكن استخدامها في تطوير الألعاب.
</p>

<p style="text-align: center;">
	<img alt="01_adventurer_sprite_sheet_v1.1.png" class="ipsImage ipsImage_thumbnailed" data-fileid="169136" data-ratio="61.54" data-unique="c2pw9178n" width="416" src="https://academy.hsoub.com/uploads/monthly_2025_03/01_adventurer_sprite_sheet_v1.1.png.3add136544efd47e804b9929090cd0fd.png">
</p>

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

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

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

<p>
	لنضف العقد التالية إلى المشهد:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1624_7" style=""><span class="typ">CharacterBody2D</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Player</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></pre>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169137" href="https://academy.hsoub.com/uploads/monthly_2025_03/02_sprite_animation_frames.png.fa5fa73e3cd5a7930c0871bd9223c525.png" rel=""><img alt="02 sprite animation frames" class="ipsImage ipsImage_thumbnailed" data-fileid="169137" data-unique="3zojdv9yb" src="https://academy.hsoub.com/uploads/monthly_2025_03/02_sprite_animation_frames.png.fa5fa73e3cd5a7930c0871bd9223c525.png"> </a>
</p>

<p>
	لنجرّب تغيير الخاصية <code>Frame</code> لمراقبة تغيّر الصورة، فهي الخاصية التي سنعمل على تحريكها.
</p>

<h3 id="-2">
	تحريك الشخصية
</h3>

<p>
	سنختار العقدة <code>AnimationPlayer</code> ثم ننقر الزر تحريك Animation يتبعه الزر جديد New ونسمي الحركة الجديدة idle. نضبط بعد ذلك مدة الحركة على <code>2</code> ثانية وننقر الزر Loop كي تتكرر الحركة باستمرار.
</p>

<p>
	نجعل شريط التقدم Scrubber عند الزمن <code>0</code> ثم نختار العقدة <code>Sprite2D</code>. نضبط الخاصية <code>Animation&gt;Frame</code> على <code>0</code> ثم ننقر على أيقونة المفتاح إلى جوار القيمة.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169138" href="https://academy.hsoub.com/uploads/monthly_2025_03/03_sprite_animation_Frames.png.83504aaeef41831817c690fe8a3bf52a.png" rel=""><img alt="03 sprite animation frames" class="ipsImage ipsImage_thumbnailed" data-fileid="169138" data-unique="6xr6u8u5k" src="https://academy.hsoub.com/uploads/monthly_2025_03/03_sprite_animation_Frames.png.83504aaeef41831817c690fe8a3bf52a.png"> </a>
</p>

<p>
	إن حاولنا الآن تشغيل الحركة فلن نرى شيئًا، لأن الإطار الأخير رقم 12 يبدو مشابهًا للإطار الأول رقم 0. مع ذلك لم نتمكن من رؤية الإطارات بينهما. لإصلاح الأمر نغير الخاصية <code>Update Mode</code> للمسار من القيمة الافتراضية <code>Discrete</code> إلى <code>Continuous</code> وسنجد هذا الزر في نهاية المسار من الجانب الأيمن.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169139" href="https://academy.hsoub.com/uploads/monthly_2025_03/04_sprite_animation_track.png.35ac99e86bb0fc5f0e996d849891d2e8.png" rel=""><img alt="04 sprite animation track" class="ipsImage ipsImage_thumbnailed" data-fileid="169139" data-unique="wdok9qacc" style="width: 500px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/04_sprite_animation_track.png.35ac99e86bb0fc5f0e996d849891d2e8.png"> </a>
</p>

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

<p style="text-align: center;">
	<img alt="11_animation_tree_control.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="169144" data-ratio="85.57" data-unique="gf6eqjk1g" width="194" src="https://academy.hsoub.com/uploads/monthly_2025_03/11_animation_tree_control.gif.af408074b8c4f47cc1b4f2805e0039ef.gif">
</p>

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

<h2 id="animationtreestatemachine">
	استخدام المتحكم <code>AnimationTreeStateMachine</code>
</h2>

<p>
	لنتخيل أن لدينا كم كبير من الحركات، وأصبح من الصعب علينا التحكم عملية التنقل فيما بينها، وامتلأ السكريبت بعبارات <code>if</code> وكلما أردنا تصحيح شيء أخفق ما تبقى. لحل الأمر نستخدم العقدة <code>AnimationTree</code> لإنشاء مُتحكّم يسمح لنا بترتيب الحركات المختلفة للشخصية وإدارة عملية التنقل فيما بينها.
</p>

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

<ul>
	<li>
		سكون idle 
	</li>
	<li>
		ركض run 
	</li>
	<li>
		هجوم attack1 
	</li>
	<li>
		هجوم attack2 
	</li>
	<li>
		إصابة hurt 
	</li>
	<li>
		موت die
	</li>
</ul>

<h3 id="animationtree">
	استخدام شجرة الرسوميات <code>AnimationTree</code>
</h3>

<p>
	نضيف العقدة <code>AnimationTree</code> إلى المشهد ثم نختار New AnimationNodeStateMachine من الخاصية <code>TreeRoot</code>.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169140" href="https://academy.hsoub.com/uploads/monthly_2025_03/06_animation_tree.png.a8cf6abb9ae7df80283bb5d62a62b4a8.png" rel=""><img alt="06 animation tree" class="ipsImage ipsImage_thumbnailed" data-fileid="169140" data-unique="m7td0yp7y" style="width: 250px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/06_animation_tree.png.a8cf6abb9ae7df80283bb5d62a62b4a8.png"> </a>
</p>

<p>
	تتحكم العقدة <code>AnimationTree</code> بالرسوميات التي تنشأ ضمن العقدة <code>AnimationPlayer</code>، ولكي نسمح لها بالوصول إلى الرسوميات الموجودة، ننقر على الخاصية <code>Assign</code> ضمن الخاصية <code>Anim Player</code> ثم نختار عقدة الحركة.
</p>

<p>
	يمكننا الآن إعداد متحكم التنقل ضمن نافذة <code>AnimationTree</code>:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169141" href="https://academy.hsoub.com/uploads/monthly_2025_03/07_anim_tree_panel.png.48c505880ae65dbb4ab4688f28a6f0c5.png" rel=""><img alt="07 anim tree panel" class="ipsImage ipsImage_thumbnailed" data-fileid="169141" data-unique="okwja2ed6" style="width: 500px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/07_anim_tree_panel.thumb.png.b69f3f6b099519931e3784078116a5c2.png"> </a>
</p>

<p>
	ننتبه إلى التحذير الظاهر، ونضبط الخاصية <code>Active</code> في نافذة الفاحص على القيمة <code>On</code> ثم ننقر بعد ذلك بالزر اليميني للفأرة ونختار Add Animation. نختار بعد ذلك الحركة idle وسنرى صندوقًا صغيرًا يمثل هذه الحركة. نكرر نفس العملية لإضافة مثل هذه الصناديق إلى بقية الحركات.
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169142" href="https://academy.hsoub.com/uploads/monthly_2025_03/08_animation_tree_connection.png.848c1ba232a0990b18d32d6c95e47d6c.png" rel=""><img alt="08 animation tree connection" class="ipsImage ipsImage_thumbnailed" data-fileid="169142" data-unique="mp9zrjddt" style="width: 200px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/08_animation_tree_connection.png.848c1ba232a0990b18d32d6c95e47d6c.png"> </a>
</p>

<p>
	عندما تختار حركة، ستتبع الشجرة المسار الذي يصل العقدة الحالية إلى الوجهة. لكن في طريقة إعداد المثال السابق، لن نرى الهجوم الأول attack1 إن شغلنا الهجوم الثاني attack2. يعود السبب في ذلك إلى أن نمط التبديل switch mode للاتصال نوعه مباشر immediate. لهذا، ننقر على زر Move/select ثم ننقر على الاتصال بين attack1 و attack2 ثم نغير من نافذة الفاحص الخاصية <code>Switch Mode</code> إلى <code>At End</code> ونكرر ذلك على الاتصال بين attack2 و idle.
</p>

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

<p style="text-align: center;">
	<img alt="09_animation_tree_loop.gif" class="ipsImage ipsImage_thumbnailed" data-fileid="169145" data-ratio="206.49" data-unique="zudzlko2r" width="154" src="https://academy.hsoub.com/uploads/monthly_2025_03/09_animation_tree_loop.gif.033d2fe808b074446dfb6819a58f3a57.gif">
</p>

<p>
	وهكذا ستُتنفذ الحركات على التتابع بمجرد تفعيلها.
</p>

<h3 id="-4">
	استدعاء الحالات في الشيفرة
</h3>

<p>
	فيما يلي شجرة الحركات بأكملها:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="169143" href="https://academy.hsoub.com/uploads/monthly_2025_03/10_animation_tree_full.png.ac50305fa31760f96a0b92b0e6611336.png" rel=""><img alt="10 animation tree full" class="ipsImage ipsImage_thumbnailed" data-fileid="169143" data-unique="zkt7n80np" style="width: 500px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_03/10_animation_tree_full.png.ac50305fa31760f96a0b92b0e6611336.png"> </a>
</p>

<p>
	لنهيئ الآن الشخصية كي تستخدم هذه الحركات:
</p>

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

var state_machine
var run_speed </span><span class="pun">=</span><span class="pln"> </span><span class="lit">80.0</span><span class="pln">
var attacks </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[</span><span class="str">"attack1"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"attack2"</span><span class="pun">]</span><span class="pln">

</span><span class="lit">@onready</span><span class="pln"> var state_machine </span><span class="pun">=</span><span class="pln"> $AnimationTree</span><span class="pun">[</span><span class="str">"parameters/playback"</span><span class="pun">]</span></pre>

<p>
	تضم الخاصية <code>state_machine</code> مرجعًا إلى المتحكم بالحالة وهو <code>AnimationNodeStateMachinePlayback</code>، ولاستدعاء حركة محددة، نستخدم التابع <code>travel</code> الذي سيتّبع الاتصالات إلى الرسم المتحرك المحدد:
</p>

<pre class="ipsCode prettyprint lang-sql prettyprinted" id="ips_uid_1624_11" style=""><span class="pln">func hurt</span><span class="pun">():</span><span class="pln">
    state_machine</span><span class="pun">.</span><span class="pln">travel</span><span class="pun">(</span><span class="str">"hurt"</span><span class="pun">)</span><span class="pln">

func </span><span class="kwd">die</span><span class="pun">():</span><span class="pln">
    state_machine</span><span class="pun">.</span><span class="pln">travel</span><span class="pun">(</span><span class="str">"die"</span><span class="pun">)</span><span class="pln">
    set_physics_process</span><span class="pun">(</span><span class="kwd">false</span><span class="pun">)</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1624_13" style=""><span class="pln">func get_input</span><span class="pun">():</span><span class="pln">
    var current </span><span class="pun">=</span><span class="pln"> state_machine</span><span class="pun">.</span><span class="pln">get_current_node</span><span class="pun">()</span><span class="pln">
    velocity </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">"move_left"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"move_right"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"move_up"</span><span class="pun">,</span><span class="pln"> </span><span class="str">"move_down"</span><span class="pun">)</span><span class="pln"> </span><span class="pun">*</span><span class="pln"> run_speed
    </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">"attack"</span><span class="pun">):</span><span class="pln">
      state_machine</span><span class="pun">.</span><span class="pln">travel</span><span class="pun">(</span><span class="pln">attacks</span><span class="pun">.</span><span class="pln">pick_random</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">
    </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="com"># اختر رسمًا متحركًا</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">
      state_machine</span><span class="pun">.</span><span class="pln">travel</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">
      state_machine</span><span class="pun">.</span><span class="pln">travel</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>
	نلاحظ استخدام <code>return</code> بعد الانتقال إلى حركة الهجوم كي نتمكن من الانتقال إلى حالات الحركة أو السكون لاحقًا في الدالة.
</p>

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

<p>
	تعرفنا في هذا المقال على طريقة استخدام SpriteSheet في جودو لتوليد حركات مختلفة للشخصية، كما تعرفنا على استخدام AnimationTree Animation Tree State Machine في إدارة التنقل بين الرسوميات المختلفة للشخصية. وبإمكانك من الاطلاع على <a href="https://github.com/godotrecipes/ai_behavior_demos" rel="external nofollow">المشروع بصيغته المكتملة</a> لتنفيذه وفهمه بصورة أفضل.
</p>

<p>
	ترجمة -وبتصرف- للمقالين: <a href="https://kidscancode.org/godot_recipes/4.x/animation/spritesheet_animation/index.html" rel="external nofollow">SpriteSheet ANimation</a> و <a href="https://kidscancode.org/godot_recipes/4.x/animation/using_animation_sm/index.html" rel="external nofollow">Using AnimationTreeStateMachine</a>
</p>

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" 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%88%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%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%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-r2281/" rel="">إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو</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%D8%B4%D8%AE%D8%B5%D9%8A%D8%A9-%D9%81%D9%8A-%D9%84%D8%B9%D8%A8%D8%A9-3d-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D9%85%D8%AD%D8%B1%D8%B1-%D8%A7%D9%84%D8%AA%D8%AD%D8%B1%D9%8A%D9%83-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-r2477/" rel="">تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو</a>
	</li>
	<li>
		<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>
	</li>
</ul>
]]></description><guid isPermaLink="false">2529</guid><pubDate>Tue, 11 Mar 2025 12:00:00 +0000</pubDate></item><item><title>&#x62D;&#x641;&#x638; &#x648;&#x627;&#x633;&#x62A;&#x631;&#x62C;&#x627;&#x639; &#x627;&#x644;&#x628;&#x64A;&#x627;&#x646;&#x627;&#x62A; &#x627;&#x644;&#x645;&#x62D;&#x644;&#x64A;&#x629; &#x628;&#x64A;&#x646; &#x62C;&#x644;&#x633;&#x627;&#x62A; &#x627;&#x644;&#x644;&#x639;&#x628;</title><link>https://academy.hsoub.com/programming/game-development/%D8%AD%D9%81%D8%B8-%D9%88%D8%A7%D8%B3%D8%AA%D8%B1%D8%AC%D8%A7%D8%B9-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%AD%D9%84%D9%8A%D8%A9-%D8%A8%D9%8A%D9%86-%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D9%84%D8%B9%D8%A8-r2522/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_03/2072562464_.png.7ca1ee201b82e3dddfdce5c4371b4c57.png" /></p>
<p>
	سنتعلم في مقال اليوم كيفية حفظ البيانات المحلية بين جلسات اللعب وتحميل هذه البيانات عند الحاجة لها، هذا الموضوع مهم بشكل خاص عندما نريد الاحتفاظ بتقدم <span ipsnoautolink="true">اللاعب</span> أو إعدادات اللعبة عبر عدة جلسات لعب، حتى بعد إغلاق اللعبة وإعادة فتحها.
</p>

<h2>
	كيف نحفظ البيانات المحلية في جودو
</h2>

<p>
	يعتمد نظام إدخال وإخراج الملفات الخاص <a href="https://academy.hsoub.com/programming/game-development/%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-game-engines/" rel="">بمحرك الألعاب</a> جودو Godot على كائن يسمى <code>FileAccess</code> ويمكنا فتحه من خلال استدعاء التابع <code>open()‎</code> كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6189_6" style=""><span class="pln">var file </span><span class="pun">=</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">open</span><span class="pun">(</span><span class="str">"user://myfile.name"</span><span class="pun">,</span><span class="pln"> </span><span class="typ">File</span><span class="pun">.</span><span class="pln">READ</span><span class="pun">)</span></pre>

<p>
	<strong>ملاحظة</strong>: يجب تخزين بيانات المستخدم فقط في المسار <code>user://‎</code>، ويمكننا استخدام المسار <code>res://‎</code> عند التشغيل من المحرّر، ولكن يصبح هذا المسار للقراءة فقط عند تصدير مشروعنا.
</p>

<p>
	الوسيط الثاني الموجود بعد مسار الملف هو راية الوضع Mode Flag ويمكن أن يكون لها أحد الخيارات التالية:
</p>

<ul>
	<li>
		<strong>FileAccess.READ</strong>: مفتوح للقراءة
	</li>
	<li>
		<strong>FileAccess.WRITE</strong>: مفتوح للكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه Truncate إذا كان موجودًا مسبقًا
	</li>
	<li>
		<strong>FileAccess.READ_WRITE</strong>: مفتوح للقراءة والكتابة، ولا يقتطع الملف
	</li>
	<li>
		<strong>FileAccess.WRITE_READ</strong>: مفتوح للقراءة أو الكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه إذا كان موجودًا مسبقًا
	</li>
</ul>

<h2 id="-1">
	تخزين البيانات
</h2>

<p>
	يمكننا حفظ البيانات باستخدام نوع البيانات المحدّد مثل <code>store_float()‎</code> و <code>store_string()‎</code> وغير ذلك، أو باستخدام الدالة <code>store_var()‎</code> المعمَّمة، والتي ستستخدم التسلسل Serialization المُدمَج في جودو لتشفير بياناتك بما في ذلك البيانات المعقدة مثل الكائنات التي سنتحدث عنها لاحقًا.
</p>

<p>
	لنبدأ بمثال بسيط لحفظ أعلى نتيجة للاعب، حيث يمكننا كتابة دالة يمكن استدعاؤها كلما احتجنا إلى حفظ النتيجة:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6189_8" style=""><span class="pln">var save_path </span><span class="pun">=</span><span class="pln"> </span><span class="str">"user://score.save"</span><span class="pln">

func save_score</span><span class="pun">():</span><span class="pln">
    var file </span><span class="pun">=</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">open</span><span class="pun">(</span><span class="pln">save_path</span><span class="pun">,</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">WRITE</span><span class="pun">)</span><span class="pln">
    file</span><span class="pun">.</span><span class="pln">store_var</span><span class="pun">(</span><span class="pln">highscore</span><span class="pun">)</span></pre>

<p>
	نحفظ النتيجة، ولكن يجب تحميلها عند بدء اللعبة كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6189_10" style=""><span class="pln">func load_score</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">file_exists</span><span class="pun">(</span><span class="pln">save_path</span><span class="pun">):</span><span class="pln">
        </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"file found"</span><span class="pun">)</span><span class="pln">
        var file </span><span class="pun">=</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">open</span><span class="pun">(</span><span class="pln">save_path</span><span class="pun">,</span><span class="pln"> </span><span class="typ">FileAccess</span><span class="pun">.</span><span class="pln">READ</span><span class="pun">)</span><span class="pln">
        highscore </span><span class="pun">=</span><span class="pln"> file</span><span class="pun">.</span><span class="pln">get_var</span><span class="pun">()</span><span class="pln">
    </span><span class="kwd">else</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"file not found"</span><span class="pun">)</span><span class="pln">
        highscore </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span></pre>

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

<p>
	كما يمكن استخدام الدالتين <code>store_var()‎</code> و <code>get_var()‎</code> عدة مرات حسب حاجتك مع أيّ عدد من القيم.
</p>

<h2 id="-2">
	حفظ الموارد
</h2>

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

<p>
	يمكن حفظ الموارد وتحميلها باستخدام صنفي جودو <code>ResourceSaver</code> و <code>ResourceLoader</code>.
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6189_12" style=""><span class="pln">extends </span><span class="typ">Resource</span><span class="pln">
class_name </span><span class="typ">PlayerData</span><span class="pln">

var level </span><span class="pun">=</span><span class="pln"> </span><span class="lit">1</span><span class="pln">
var experience </span><span class="pun">=</span><span class="pln"> </span><span class="lit">100</span><span class="pln">

var strength </span><span class="pun">=</span><span class="pln"> </span><span class="lit">5</span><span class="pln">
var intelligence </span><span class="pun">=</span><span class="pln"> </span><span class="lit">3</span><span class="pln">
var charisma </span><span class="pun">=</span><span class="pln"> </span><span class="lit">2</span></pre>

<p>
	يمكننا بعد ذلك الحفظ والتحميل كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_6189_14" style=""><span class="pln">func load_character_data</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">if</span><span class="pln"> </span><span class="typ">ResourceLoader</span><span class="pun">.</span><span class="pln">exists</span><span class="pun">(</span><span class="pln">save_path</span><span class="pun">):</span><span class="pln">
        </span><span class="kwd">return</span><span class="pln"> load</span><span class="pun">(</span><span class="pln">save_path</span><span class="pun">)</span><span class="pln">
    </span><span class="kwd">return</span><span class="pln"> null

func save_character_data</span><span class="pun">(</span><span class="pln">data</span><span class="pun">):</span><span class="pln">
    </span><span class="typ">ResourceSaver</span><span class="pun">.</span><span class="pln">save</span><span class="pun">(</span><span class="pln">data</span><span class="pun">,</span><span class="pln"> save_path</span><span class="pun">)</span></pre>

<p>
	قد تحتوي الموارد على موارد فرعية، لذا يمكن أيضًا تضمين موارد مخزن اللاعب وغير ذلك.
</p>

<h2 id="json">
	هل يمكن تخزين البيانات في ملف JSON
</h2>

<p>
	قد يخطر في البال سؤال عن إمكانية استخدام صيغة JSON لحفظ البيانات، ولكن يُوصَى بعدم استخدام JSON مع ملفات الحفظ الخاصة بنا. يدعم جودو <a href="https://docs.godotengine.org/en/latest/classes/class_json.html" rel="external nofollow">صيغة JSON</a>، ولكن لا يُعَد حفظ بيانات اللعبة هدف استخدام JSON التي هي صيغة لتبادل البيانات، والغرض منها هو السماح للأنظمة التي تستخدم صيغ بيانات ولغات مختلفة بتبادل البيانات، لذا ستسبّب صيغة JSON قيودًا سلبية عندما يتعلق الأمر بحفظ بيانات اللعبة. 
</p>

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

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

<h2>
	الخاتمة
</h2>

<p>
	تعلمنا في مقال اليوم أساسيات حفظ واسترجاع البيانات المحلية بين جلسات اللعب، وتجدر الإشارة لأن هذا المقال لا يمثل سوى جزء بسيط ممّا يمكنك إنجازه باستخدام <code>FileAccess</code>، لذا ننصح بالاطلاع على <a href="https://docs.godotengine.org/en/stable/classes/class_fileaccess.html" rel="external nofollow">توثيق FileAccess</a> للحصول على القائمة الكاملة لتوابعه.
</p>

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

<h2>
	اقرأ أيضًا
</h2>

<ul>
	<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%85%D9%81%D9%87%D9%88%D9%85-delta-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2519/" rel="">تعرف على مفهوم Delta في تطوير الألعاب</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" rel="">الطريقة الصحيحة للتواصل بين العقد في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D9%85%D8%AA%D8%A7%D8%AD%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2371/" rel="">لغات البرمجة المتاحة في جودو Godot</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>
</ul>
]]></description><guid isPermaLink="false">2522</guid><pubDate>Tue, 04 Mar 2025 12:00:01 +0000</pubDate></item><item><title>&#x62A;&#x639;&#x631;&#x641; &#x639;&#x644;&#x649; &#x645;&#x641;&#x647;&#x648;&#x645; Delta &#x641;&#x64A; &#x62A;&#x637;&#x648;&#x64A;&#x631; &#x627;&#x644;&#x623;&#x644;&#x639;&#x627;&#x628;</title><link>https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D9%85%D9%81%D9%87%D9%88%D9%85-delta-%D9%81%D9%8A-%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8-r2519/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_02/delta.png.a0d3b5dd369f6c06de8e2e52be7c29a4.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> مفهوم Delta في مجال صناعة الألعاب، ونوضح كيفية استخدامه.
</p>

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

<p>
	ليكن لدينا عقدة <code>Sprite</code> تتحرك عبر الشاشة. إذا كان عرض الشاشة 600 بكسل ونريد أن نعبر الشخصية الرسومية Sprite الشاشة خلال 5 ثوانٍ، فيمكننا استخدام العملية الحسابية التالية لإيجاد السرعة اللازمة لذلك:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9297_9" style=""><span class="lit">600</span><span class="pln"> pixels </span><span class="pun">/</span><span class="pln"> </span><span class="lit">5</span><span class="pln"> seconds </span><span class="pun">=</span><span class="pln"> </span><span class="lit">120</span><span class="pln"> pixels</span><span class="pun">/</span><span class="pln">second</span></pre>

<p>
	سنحرّك الشخصية الرسومية في كل إطار باستخدام الدالة <code>‎_process()‎</code>، بحيث إذا شُغِّلت اللعبة بمعدل 60 إطارًا في الثانية، فيمكننا إيجاد الحركة لكل إطار باستخدام العملية الحسابية التالية:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9297_11" style=""><span class="lit">120</span><span class="pln"> pixels</span><span class="pun">/</span><span class="pln">second </span><span class="pun">*</span><span class="pln"> </span><span class="lit">1</span><span class="pun">/</span><span class="lit">60</span><span class="pln"> second</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"> pixels</span><span class="pun">/</span><span class="pln">frame</span></pre>

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

<p>
	تكون الشيفرة البرمجية الضرورية كما يلي:
</p>

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

</span><span class="com"># الحركة المطلوبة بالبكسلات لكل إطار</span><span class="pln">
var movement </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">2</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">
    $Sprite</span><span class="pun">.</span><span class="pln">position </span><span class="pun">+=</span><span class="pln"> movement</span></pre>

<p>
	نشغّل الشيفرة البرمجية السابقة وسنجد أن الصورة تعبر الشاشة خلال 5 ثوانٍ.
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="168583" href="https://academy.hsoub.com/uploads/monthly_2025_02/01_delta_01.gif.d934a8642dd69a4e2171dce8b149c4f3.gif" rel=""><img alt="عبور صورة اللعبة خلال 5 ثوانٍ في محرك الألعاب جودو" class="ipsImage ipsImage_thumbnailed" data-fileid="168583" data-unique="hdqz3eu20" style="width: 300px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/01_delta_01.gif.d934a8642dd69a4e2171dce8b149c4f3.gif"> </a>
</p>

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

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

<p style="text-align: center;">
	<img alt="عبور صورة اللعبة خلال 10 ثوانٍ في محرك الألعاب جودو" class="ipsImage ipsImage_thumbnailed" data-fileid="168586" data-ratio="50.67" data-unique="t8j3vhed0" style="width: 300px; height: auto;" width="300" src="https://academy.hsoub.com/uploads/monthly_2025_02/02_delta_02.gif.d1dae0c6f86bb3d65eebebe01b0b4682.gif">
</p>

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

<h2 id="">
	إصلاح مشكلة معدل الإطارات
</h2>

<p>
	تتضمن الدالة <code>‎_process()‎</code> تلقائيًا عند استخدامها معاملًا بالاسم <code>delta</code> يمرّره المحرّك كما في الدالة <code>‎_physics_process()‎</code> التي تُستخدَم في الشيفرة البرمجية المتعلقة بالفيزياء.
</p>

<p>
	المعامل <code>delta</code> هو قيمة عشرية تمثل الوقت المستغرق منذ الإطار السابق، والذي سيكون 1/60 أو 0.0167 ثانية تقريبًا. يمكننا التوقف عن القلق بشأن مقدار تحريك كل إطار من خلال استخدام المعامل <code>delta</code>، إذ سنحتاج للاهتمام فقط بالسرعة المطلوبة بالبكسلات في الثانية، والتي هي 120 من العمليات الحسابية السابقة.
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9297_15" style=""><span class="com"># ‫60 إطار في الثانية</span><span class="pln">
</span><span class="lit">120</span><span class="pln"> pixels</span><span class="pun">/</span><span class="pln">second </span><span class="pun">*</span><span class="pln"> </span><span class="lit">1</span><span class="pun">/</span><span class="lit">60</span><span class="pln"> second</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"> pixels</span><span class="pun">/</span><span class="pln">frame

</span><span class="com"># ‫30 إطار في الثانية</span><span class="pln">
</span><span class="lit">120</span><span class="pln"> pixels</span><span class="pun">/</span><span class="pln">second </span><span class="pun">*</span><span class="pln"> </span><span class="lit">1</span><span class="pun">/</span><span class="lit">30</span><span class="pln"> second</span><span class="pun">/</span><span class="pln">frame </span><span class="pun">=</span><span class="pln"> </span><span class="lit">4</span><span class="pln"> pixels</span><span class="pun">/</span><span class="pln">frame</span></pre>

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

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

</span><span class="com"># الحركة المطلوبة بالبكسلات لكل إطار</span><span class="pln">
var movement </span><span class="pun">=</span><span class="pln"> </span><span class="typ">Vector2</span><span class="pun">(</span><span class="lit">120</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">
    $Sprite</span><span class="pun">.</span><span class="pln">position </span><span class="pun">+=</span><span class="pln"> movement </span><span class="pun">*</span><span class="pln"> delta</span></pre>

<p>
	يكون زمن الانتقال متناسقًا عند التشغيل بمعدل 30 إطارًا في الثانية كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="168584" href="https://academy.hsoub.com/uploads/monthly_2025_02/03_delta_03.gif.4498147fe0e82354ec62f994be16731b.gif" rel=""><img alt="زمن انتقال اللعبة متناسق عند التشغيل بمعدل 30 إطارًا في الثانية" class="ipsImage ipsImage_thumbnailed" data-fileid="168584" data-unique="ypen4grx3" style="width: 300px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/03_delta_03.gif.4498147fe0e82354ec62f994be16731b.gif"> </a>
</p>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="gif" data-fileid="168585" href="https://academy.hsoub.com/uploads/monthly_2025_02/04_delta_04.gif.5716b0e198665d5f91cbc1d8a60b1105.gif" rel=""><img alt="تعثر حركة اللعبة في محرك جودو في مدة محددة" class="ipsImage ipsImage_thumbnailed" data-fileid="168585" data-unique="l32dj24gi" style="width: 300px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/04_delta_04.gif.5716b0e198665d5f91cbc1d8a60b1105.gif"> </a>
</p>

<h2 id="-1">
	استخدام معامل دلتا مع معادِلات الحركة
</h2>

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

<p>
	<strong>ملاحظة</strong>: يُعَد التعامل بالبكسلات والثواني أسهل بكثير لأنه يتعلق بكيفية قياس هذه الكميات في العالم الحقيقي، فمثلًا الجاذبية Gravity هي 100 بكسل/ثانية/ثانية، لذا ستتحرك الكرة بسرعة 200 بكسل/ثانية بعد سقوطها لمدة ثانيتين، وإذا استخدمنا وحدة الإطارات، فيجب استخدام التسارع Acceleration بوحدة البكسل/إطار/إطار، ولكننا سنجد أن هذه الوحدة غير مألوفة.
</p>

<p>
	إذا طبّقنا الجاذبية مثلًا، فإنها تمثّل التسارع، بحيث ستزيد السرعة بمقدارٍ معين في كل إطار، وستغير السرعة موضع العقدة كما هو الحال في المثال السابق؛ وهنا سنضبط قيم <code>delta</code> و <code>target_fps</code> في الشيفرة البرمجية التالية لمعرفة النتائج:
</p>

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

</span><span class="com"># التسارع بالبكسل/ثانية/ثانية</span><span class="pln">
var gravity </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">120</span><span class="pun">)</span><span class="pln">
</span><span class="com"># التسارع بالبكسل/إطار/إطار</span><span class="pln">
var gravity_frame </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">033</span><span class="pun">)</span><span class="pln">

</span><span class="com"># السرعة بالبكسل/ثانية أو بالبكسل/إطار</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 use_delta </span><span class="pun">=</span><span class="pln"> false
var target_fps </span><span class="pun">=</span><span class="pln"> </span><span class="lit">60</span><span class="pln">

func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="typ">Engine</span><span class="pun">.</span><span class="pln">target_fps </span><span class="pun">=</span><span class="pln"> target_fps

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"> use_delta</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">+=</span><span class="pln"> gravity </span><span class="pun">*</span><span class="pln"> delta
        $Sprite</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><span class="kwd">else</span><span class="pun">:</span><span class="pln">
        velocity </span><span class="pun">+=</span><span class="pln"> gravity_frame
        $Sprite</span><span class="pun">.</span><span class="pln">position </span><span class="pun">+=</span><span class="pln"> velocity</span></pre>

<p>
	وكما هو ظاهر، فقد ضربنا القيمة المحدثة في الخطوة الزمنية لكل إطار لتحديث السرعة <code>velocity</code> والموضع <code>position</code>، إذ يجب ضرب أي كمية مُحدَّثة في كل إطار بقيمة <code>delta</code> لضمان تغيرها بحيث تكون مستقلة عن معدل الإطارات.
</p>

<h3 id="kinematic">
	استخدام الدوال الحركية Kinematic
</h3>

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

<p>
	إذا استخدمنا جسمًا حركيًا Kinematic ثنائي الأبعاد أو ثلاثي الأبعاد، فسنحتاج لاستخدام أحد توابع الحركة الخاصة به بدلًا من ذلك، خاصةً في حالة استخدام التابع <code>move_and_slide()‎</code>؛ إذ قد يحدث بعض الارتباك لأنه يستخدم متجه السرعة وليس الموضع، وهذا يعني أننا لن نضرب السرعة بقيمة <code>delta</code> لإيجاد المسافة، إذ تنجز الدالة ذلك نيابةً عنا، ولكن يجب تطبيقها على أيّ عمليات حسابية أخرى مثل التسارع كما في المثال التالي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9297_21" style=""><span class="com"># الشيفرة البرمجية لحركة الشخصية الرسومية‫:</span><span class="pln">
velocity </span><span class="pun">+=</span><span class="pln"> gravity </span><span class="pun">*</span><span class="pln"> delta
position </span><span class="pun">+=</span><span class="pln"> velocity </span><span class="pun">*</span><span class="pln"> delta

</span><span class="com"># الشيفرة البرمجية لحركة الجسم الحركي‫:</span><span class="pln">
velocity </span><span class="pun">+=</span><span class="pln"> gravity </span><span class="pun">*</span><span class="pln"> delta
move_and_slide</span><span class="pun">()</span></pre>

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

<p>
	<strong>ملاحظة</strong>: يجب أيضًا تطبيق قيمة <code>delta</code> على أي كميات أخرى مثل الجاذبية والاحتكاك وغير ذلك عند استخدام التابع <code>move_and_slide()‎</code>.
</p>

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

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

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

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

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" rel="">الطريقة الصحيحة للتواصل بين العقد في جودو </a><a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/" 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/%D9%85%D8%B7%D9%88%D8%B1-%D8%A7%D9%84%D8%A3%D9%84%D8%B9%D8%A7%D8%A8/" rel="">مطور الألعاب: من هو وما هي مهامه</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2519</guid><pubDate>Thu, 27 Feb 2025 16:01:03 +0000</pubDate></item><item><title>&#x627;&#x644;&#x637;&#x631;&#x64A;&#x642;&#x629; &#x627;&#x644;&#x635;&#x62D;&#x64A;&#x62D;&#x629; &#x644;&#x644;&#x62A;&#x648;&#x627;&#x635;&#x644; &#x628;&#x64A;&#x646; &#x627;&#x644;&#x639;&#x642;&#x62F; &#x641;&#x64A; &#x62C;&#x648;&#x62F;&#x648; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B7%D8%B1%D9%8A%D9%82%D8%A9-%D8%A7%D9%84%D8%B5%D8%AD%D9%8A%D8%AD%D8%A9-%D9%84%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84-%D8%A8%D9%8A%D9%86-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2518/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_02/Godot.png.0506c3df739859f14da4bd92dd0f11fb.png" /></p>
<p>
	بعد أن تعرفنا في <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D9%82%D9%84-%D9%81%D9%8A-%D8%B4%D8%AC%D8%B1%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%D9%81%D9%8A-godot-r2512/" 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> على كيفية ترتيب معالجة العقد والتنقل في شجرة المشاهد في محرك الألعاب جودو Godot، سنتعرف في هذا المقال على الطريقة السليمة للتواصل بين العقد Nodes ونوضح المشاكل التي قد تحدث عند القيام بممارسات غير متوافقة مع الطريقة الصحيحة المنصوح بها.
</p>

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

	<div class="ipsQuote_contents ipsClearfix" data-gramm="false">
		<p>
			<strong>ملاحظة</strong>: اُستلهم <a href="https://kidscancode.org/godot_recipes/4.x/img/node_access_theduriel.png" rel="external nofollow">المخطط الأصلي</a> لهذا المقال من ‎@TheDuriel على <a href="https://discord.com/invite/zH7NUgz" rel="external nofollow">Godot Discord</a>.
		</p>
	</div>
</blockquote>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_10" style=""><span class="pln">get_node</span><span class="pun">(</span><span class="str">"../../SomeNode/SomeOtherNode"</span><span class="pun">)</span><span class="pln">
get_parent</span><span class="pun">().</span><span class="pln">get_parent</span><span class="pun">().</span><span class="pln">get_node</span><span class="pun">(</span><span class="str">"SomeNode"</span><span class="pun">)</span><span class="pln">
get_tree</span><span class="pun">().</span><span class="pln">get_root</span><span class="pun">().</span><span class="pln">get_node</span><span class="pun">(</span><span class="str">"SomeNode/SomeOtherNode"</span><span class="pun">)</span></pre>

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

<p>
	يجب أن تدير العقد أبناءها وليس العكس؛ حيث إذا استخدمنا الدالة <code>get_parent()‎</code> أو <code>get_node("..")‎</code>، فيُحتمَل أننا نتجه إلى مشكلة ما، إذ تكون مثل هذه المسارات للعقد سهلة الكسر. سنذكر ففيما يلي المشاكل الرئيسية الثلاث في هذا الترتيب:
</p>

<ol>
	<li>
		لا يمكن اختبار مشهد بطريقة مستقلة؛ فإذا شغّلنا المشهد بمفرده أو في مشهد اختبار لا يحتوي على إعداد العقدة نفسه، فستسبّب الدالة <code>get_node()‎</code> عطلًا
	</li>
	<li>
		لا يمكننا تغيير الأشياء بسهولة، لأننا إذا قرّرنا إعادة ترتيب أو تصميم الشجرة، فلن تكون المسارات صالحةً مجددًا
	</li>
	<li>
		يبدأ ترتيب الجاهزية من الأبناء أولًا حتى الوصول إلى الأب أخيرًا، وهذا يعني فشل محاولة الوصول إلى خاصية الأب في التابع <code>‎_ready()‎</code> الخاصة بالعقدة، لأن الأب غير جاهزٍ بعد
	</li>
</ol>

<p>
	<strong>ملاحظة</strong>: يُنصح بالاطلاع على <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D9%82%D9%84-%D9%81%D9%8A-%D8%B4%D8%AC%D8%B1%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%D9%81%D9%8A-godot-r2512/" rel="">المقال السابق</a> للحصول على شرح لكيفية دخول العقد إلى الشجرة وكيف تصبح جاهزة.
</p>

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

<p>
	تشير هذه القاعدة إلى أنه في حال استدعَت العقدة ابنًا، بمعنى تنقلها إلى أسفل الشجرة، فسيكون استدعاء الدالة <code>get_node()‎</code> مناسبًا؛ وإذا كانت العقدة بحاجة إلى التواصل مع أعلى الشجرة، فيجب استخدام إشارة لذلك. يؤدي استخدام هذه القاعدة عند تصميم إعداد المشهد الخاص بنا إلى الحصول على مشروع قابل للصيانة ومنظم جيدًا، وسنتجنب بذلك استخدام مسارات العقد المعقدة التي تؤدي إلى مشاكل.
</p>

<p>
	لنلقِ الآن نظرةً على كلٍّ من هذه الاستراتيجيات مع بعض الأمثلة.
</p>

<h2 id="get_node">
	استخدام الدالة get_node()‎
</h2>

<p>
	يعبُر استدعاء الدالة <code>get_node()‎</code> شجرة المشاهد باستخدام مسار معين للعثور على العقدة المسماة.
</p>

<h3 id="get_node-1">
	مثال عن استخدام get_node()‎
</h3>

<p>
	ليكن لدينا الضبط التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="168576" href="https://academy.hsoub.com/uploads/monthly_2025_02/01_node_access_01.png.47d01ffa8aacdaf0edb4c71b889ebcf0.png" rel=""><img alt="مثال عن استخدام get_node() في محرك جودو Godot" class="ipsImage ipsImage_thumbnailed" data-fileid="168576" data-unique="xnxw4kbvg" style="width: 233px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/01_node_access_01.png.47d01ffa8aacdaf0edb4c71b889ebcf0.png"> </a>
</p>

<p>
	يجب أن يُعلِم السكربت الموجود في العقدة <code>Player</code> العقدةَ <code>AnimatedSprite2D</code> بالرسوم المتحركة التي تُشغَّل بناءً على حركة اللاعب، بحيث تعمل الدالة <code>get_node()‎</code> في هذه الحالة بنجاح كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_12" style=""><span class="pln">extends </span><span class="typ">CharacterBody2D</span><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"> speed </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        get_node</span><span class="pun">(</span><span class="str">"AnimatedSprite2D"</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">
        get_node</span><span class="pun">(</span><span class="str">"AnimatedSprite2D"</span><span class="pun">).</span><span class="pln">play</span><span class="pun">(</span><span class="str">"idle"</span><span class="pun">)</span></pre>

<p>
	<strong>ملاحظة</strong>: يمكننا استخدام المحرف <code>$</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> كاختصار لاستدعاء <code>get_node()‎</code>، لذا بإمكاننا كتابة <code>‎$AnimatedSprite2D</code> مباشرةً.
</p>

<h3 id="">
	طريقة أفضل للاستدعاء
</h3>

<p>
	تتمثل سلبية الطريقة السابقة في تحديد مسار العقدة، حيث إذا تغير هذا المسار لاحقًا، فيجب تعديل الشيفرة البرمجية أيضًا؛ ولهذا يمكننا استخدام الميزة <code>‎@export</code> لتحديد عقدة مباشرةً كما يلي:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_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 animation </span><span class="pun">:</span><span class="pln"> </span><span class="typ">AnimatedSprite2D</span><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"> speed </span><span class="pun">&gt;</span><span class="pln"> </span><span class="lit">0</span><span class="pun">:</span><span class="pln">
        animation</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">
        animation</span><span class="pun">.</span><span class="pln">play</span><span class="pun">(</span><span class="str">"idle"</span><span class="pun">)</span></pre>

<p>
	يمكننا باستخدام هذه الطريقة إسناد قيمةٍ إلى المتغير مباشرةً في الفاحص Inspector من خلال اختيار العقدة.
</p>

<h2 id="signals">
	استخدام الإشارات Signals
</h2>

<p>
	يجب استخدام الإشارات لاستدعاء الدوال في العقد الموجودة في مستوًى أعلى من الشجرة أو الموجودة على المستوى نفسه (أي العقد الأشقاء Siblings).
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_16" style=""><span class="pln">signal_name</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">target_node</span><span class="pun">.</span><span class="pln">target_function</span><span class="pun">)</span></pre>

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

<h3 id="-1">
	مثال عن استخدام الإشارات
</h3>

<p>
	يُعَد تحديث واجهة المستخدم UI الخاصة بك حالة استخدام شائعة جدًا للإشارات، لأننا نريد مثلًا تحديث عرض <code>Label</code> أو <code>ProgressBar</code> كلما اختلف المتغير <code>health</code> الخاص باللاعب، ولكن تكون عقد واجهة المستخدم الخاصة بك منفصلة تمامًا عن اللاعب إذ لا يعرف اللاعب شيئًا عن مكان هذه العقد وكيفية العثور عليها.
</p>

<p>
	ليكن لدينا إعداد المثال التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="168577" href="https://academy.hsoub.com/uploads/monthly_2025_02/02_node_access_05.png.f4c2b2fe3135511092273d8f4b4caaa9.png" rel=""><img alt="مثال عن استخدام الإشارات في جودو" class="ipsImage ipsImage_thumbnailed" data-fileid="168577" data-unique="5899amc7c" style="width: 225px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/02_node_access_05.png.f4c2b2fe3135511092273d8f4b4caaa9.png"> </a>
</p>

<p>
	يمكننا ملاحظة أن واجهة المستخدم هي نسخة من مشهد، لأننا نعرض العقد المضمَّنة فقط، وهو المكان الذي نرى فيه أشياء مثل <code>get_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health)‎</code> التي نريد تجنبها، لذا يصدر اللاعب بدلًا من ذلك إشارة <code>health_changed</code> كلما أضاف أو فقد جزءًا من صحته، ويجب إرسال هذه الإشارة إلى الدالة <code>update_health()‎</code> الخاصة بواجهة المستخدم UI، والتي تتولّى ضبط قيمة <code>Label</code>.
</p>

<p>
	سنستخدم الشيفرة البرمجية التالية في سكربت <code>Player</code> كلما تغيرت صحة اللاعب:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_1100_10" style=""><span class="pln">health_changed</span><span class="pun">.</span><span class="pln">emit</span><span class="pun">(</span><span class="pln">health</span><span class="pun">)</span></pre>

<p>
	لدينا ما يلي في سكربت واجهة المستخدم <code>UI</code>:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_20" style=""><span class="pln">onready var label </span><span class="pun">=</span><span class="pln"> $VBoxContainer</span><span class="pun">/</span><span class="typ">HBoxContainer</span><span class="pun">/</span><span class="typ">Label</span><span class="pln">

func update_health</span><span class="pun">(</span><span class="pln">value</span><span class="pun">):</span><span class="pln">
    label</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> str</span><span class="pun">(</span><span class="pln">value</span><span class="pun">)</span></pre>

<p>
	كل ما نحتاجه الآن هو توصيل الإشارة بالدالة، والمكان المثالي ذلك موجود في سكربت <code>World</code> الذي يمثّل الأب المشترك للعقدتين ويعرف مكانهما:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_22" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    $Player</span><span class="pun">.</span><span class="pln">health_changed</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">$UI</span><span class="pun">.</span><span class="pln">update_health</span><span class="pun">)</span></pre>

<h2 id="-2">
	استخدام المجموعات
</h2>

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

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

<p>
	تتعقّب شجرة المشهد SceneTree الوسوم ويكون لديها دوال مثل الدالة <code>get_nodes_in_group()‎</code> للمساعدة في العثور على جميع العقد التي يكون لها وسم معين.
</p>

<h3 id="-3">
	مثال عن استخدام المجموعات
</h3>

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

<p>
	سنحتاج أولًا إلى إضافة جميع الأعداء إلى المجموعة enemies في المحرّر باستخدام تبويب العقدة Node:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="168579" href="https://academy.hsoub.com/uploads/monthly_2025_02/03_node_access_03.png.5209e5e3745c5a1e10ff6bf75ec04963.png" rel=""><img alt="مثال عن استخدام المجموعات في محرك الألعاب جودو" class="ipsImage ipsImage_thumbnailed" data-fileid="168579" data-unique="tnzkzkw2t" style="width: 212px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/03_node_access_03.png.5209e5e3745c5a1e10ff6bf75ec04963.png"> </a>
</p>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_24" style=""><span class="pln">func _ready</span><span class="pun">():</span><span class="pln">
    add_to_group</span><span class="pun">(</span><span class="str">"enemies"</span><span class="pun">)</span></pre>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_4679_26" style=""><span class="pln">func activate_smart_bomb</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">"explode"</span><span class="pun">)</span></pre>

<h2 id="owner">
	استخدام خاصية المالك owner
</h2>

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

<h3 id="owner-1">
	مثال عن استخدام الخاصية owner
</h3>

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

<p>
	ليكن لدينا مثلًا الإعداد التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="168578" href="https://academy.hsoub.com/uploads/monthly_2025_02/04_node_access_02.png.1c944e8c9d2e5328f6fa86f048e7d9f0.png" rel=""><img alt="مثال عن استخدام الخاصية owner في محرك الألعاب جودو" class="ipsImage ipsImage_thumbnailed" data-fileid="168578" data-unique="618p1rklr" style="width: 231px; height: auto;" src="https://academy.hsoub.com/uploads/monthly_2025_02/04_node_access_02.png.1c944e8c9d2e5328f6fa86f048e7d9f0.png"> </a>
</p>

<p>
	يحتوي السكربت الموجود على العقدة الجذر <code>CenterContainer</code> على الدالة التالية التي نريد استدعاءها عند الضغط على أيّ زر:
</p>

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

func _on_button_pressed</span><span class="pun">(</span><span class="pln">button_name</span><span class="pun">):</span><span class="pln">
    </span><span class="kwd">print</span><span class="pun">(</span><span class="pln">button_name</span><span class="pun">,</span><span class="pln"> </span><span class="str">" was pressed"</span><span class="pun">)</span></pre>

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

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

func _ready</span><span class="pun">():</span><span class="pln">
    pressed</span><span class="pun">.</span><span class="pln">connect</span><span class="pun">(</span><span class="pln">owner</span><span class="pun">.</span><span class="pln">_on_button_pressed</span><span class="pun">.</span><span class="pln">bind</span><span class="pun">(</span><span class="pln">name</span><span class="pun">))</span></pre>

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

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

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

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

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

<ul>
	<li>
		المقال السابق: <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D9%82%D9%84-%D9%81%D9%8A-%D8%B4%D8%AC%D8%B1%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%D9%81%D9%8A-godot-r2512/" 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>
	<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="">تعرف على واجهة محرك الألعاب جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2518</guid><pubDate>Tue, 25 Feb 2025 16:00:00 +0000</pubDate></item><item><title>&#x62A;&#x631;&#x62A;&#x64A;&#x628; &#x645;&#x639;&#x627;&#x644;&#x62C;&#x629; &#x627;&#x644;&#x639;&#x642;&#x62F; &#x648;&#x627;&#x644;&#x62A;&#x646;&#x642;&#x644; &#x641;&#x64A; &#x634;&#x62C;&#x631;&#x629; &#x627;&#x644;&#x645;&#x634;&#x627;&#x647;&#x62F; &#x641;&#x64A; Godot</title><link>https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B1%D8%AA%D9%8A%D8%A8-%D9%85%D8%B9%D8%A7%D9%84%D8%AC%D8%A9-%D8%A7%D9%84%D8%B9%D9%82%D8%AF-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D9%82%D9%84-%D9%81%D9%8A-%D8%B4%D8%AC%D8%B1%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-%D9%81%D9%8A-godot-r2512/</link><description><![CDATA[
<p><img src="https://academy.hsoub.com/uploads/monthly_2025_02/Godot.png.26a9ff12e17cf8125cf5b569cdda2494.png" /></p>
<p>
	سنشرح في هذا المقال آلية معالجة العقد ضمن شجرة المشاهد والترتيب الذي يتبعه محرك الألعاب جودو Godot للتعامل معها، كما سنوضح ما هي مسارات العقد وكيفية التنقل بينها، سيساعدنا ذلك على فهم طريقة تنظيم لعبتنا، والتحكم بها بفعالية أكبر.
</p>

<h2 id="">
	ترتيب معالجة العقد في شجرة المشاهد
</h2>

<p>
	يتضمن محرك ألعاب جودو مفهوم يسمى <a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D9%84%D8%B9%D9%82%D8%AF-nodes-%D9%88%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D9%87%D8%AF-scenes-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2357/" rel="">شجرة المشاهد Scene Tree</a> تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="167659" href="https://academy.hsoub.com/uploads/monthly_2025_02/01_tree_order_01.png.d76c3d7d23d2070db9d9ec22061d0621.png" rel=""><img alt="01 tree order 01" class="ipsImage ipsImage_thumbnailed" data-fileid="167659" data-unique="xfnjousuu" src="https://academy.hsoub.com/uploads/monthly_2025_02/01_tree_order_01.png.d76c3d7d23d2070db9d9ec22061d0621.png"> </a>
</p>

<p>
	سنرفق هذا الكود بكل عقدة:
</p>

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

func _init</span><span class="pun">():</span><span class="pln">
    </span><span class="com"># ملاحظة: العقدة ليس لها اسم بعد هنا</span><span class="pln">
    </span><span class="kwd">print</span><span class="pun">(</span><span class="str">"TestRoot init"</span><span class="pun">)</span><span class="pln">

func _enter_tree</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">print</span><span class="pun">(</span><span class="pln">name </span><span class="pun">+</span><span class="pln"> </span><span class="str">" enter tree"</span><span class="pun">)</span><span class="pln">

func _ready</span><span class="pun">():</span><span class="pln">
    </span><span class="kwd">print</span><span class="pun">(</span><span class="pln">name </span><span class="pun">+</span><span class="pln"> </span><span class="str">" ready"</span><span class="pun">)</span><span class="pln">

</span><span class="com"># ‫يضمن ما يلي أننا نطبع مرة واحدة فقط في process()‎</span><span class="pln">
var test </span><span class="pun">=</span><span class="pln"> true
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"> test</span><span class="pun">:</span><span class="pln">
        </span><span class="kwd">print</span><span class="pun">(</span><span class="pln">name </span><span class="pun">+</span><span class="pln"> </span><span class="str">" process"</span><span class="pun">)</span><span class="pln">
    test </span><span class="pun">=</span><span class="pln"> false</span></pre>

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

<ul>
	<li>
		تستدعى الدالة <code>‎_init()‎</code> عند إنشاء العقدة أو الكائن لأول مرة، وتكون العقدة حينها موجودة في ذاكرة الحاسوب ولم تُضف لشجرة المشاهد
	</li>
	<li>
		تستدعى الدالة <code>‎_enter_tree()‎</code> عند إضافة العقدة للشجرة لأول مرة، ويمكن أن يحدث ذلك عند إنشاء نسخة من العقدة أو عند إنشاء عقدة ابن من عقدة ما باستخدام التابع <code>add_child()‎</code> مثلًا
	</li>
	<li>
		تستدعى الدالة <code>‎_ready()‎</code> عند اكتمال إضافة العقدة وأبنائها بنجاح لشجرة المشاهد وجاهزيتها للعمل
	</li>
	<li>
		تستدعى الدالة <code>‎_process()‎</code> بشكل دوري في كل إطار -60 مرة في الثانية عادةً- وذلك لكل عقدة في الشجرة ويمكن استخدامها للتعامل مع التحديثات المتكررة
	</li>
</ul>

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

<pre class="ipsCode">TestRoot init
TestRoot enter tree
TestRoot ready
TestRoot process
</pre>

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

<pre class="ipsCode">TestRoot init
TestChild1 init
TestChild3 init
TestChild2 init

TestRoot enter tree
TestChild1 enter tree
TestChild3 enter tree
TestChild2 enter tree

TestChild3 ready
TestChild1 ready
TestChild2 ready
TestRoot ready

TestRoot process
TestChild1 process
TestChild3 process
TestChild2 process
</pre>

<p>
	طبعت جميع هذه العقد رسائلها بترتيب الشجرة من الأعلى إلى الأسفل باستثناء الشيفرة البرمجية الخاصة بالتابع <code>‎_ready()‎</code> والذي يُستدعَى عندما تكون العقدة جاهزة، أي عندما تدخل العقدة وأبناؤها شجرة المشاهد، فإذا كان للعقدة أبناء، فستُشغَّل استدعاءات التابع <code>‎_ready</code> الخاصة بالأبناء أولًا، وستتلقى العقدة الأب إشعار الجاهزية بعد ذلك كما يوضح <a href="https://docs.godotengine.org/en/3.2/classes/class_node.html#class-node-method-ready" rel="external nofollow">توثيق جودو الرسمي</a>.
</p>

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

<p>
	علينا تذكّر ذلك عند محاولة الوصول لعقد أخرى في التابع <code>‎_ready()‎</code>، فإذا كنا بحاجة للانتقال لأعلى الشجرة إلى عقدة أب أو عقدة جَد، فيجب تشغيل هذه الشيفرة البرمجية في العقدة الأب وليس في العقدة الابن.
</p>

<h2 id="-1">
	فهم مسارات العقد والتنقل في شجرة المشاهد
</h2>

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

<pre class="ipsCode">Invalid get index ‘position’ (on base: ’null instance’).
</pre>

<p>
	يُعَد الجزء الأخير من رسالة الخطأ null instance مصدر هذه المشكلة، وهو يسبب إرباكًا <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>

<h3 id="-2">
	مسارات العقد
</h3>

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

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="167660" href="https://academy.hsoub.com/uploads/monthly_2025_02/02_node_paths_01.png.1668f002bdda877dbdf9f00ac20926e0.png" rel=""><img alt="02 node paths 01" class="ipsImage ipsImage_thumbnailed" data-fileid="167660" data-unique="73pdmas5m" src="https://academy.hsoub.com/uploads/monthly_2025_02/02_node_paths_01.png.1668f002bdda877dbdf9f00ac20926e0.png"> </a>
</p>

<p>
	يوجد كود هذا المشهد في العقدة <code>Player</code>. إذا كان السكربت بحاجة إلى استدعاء الدالة <code>play()‎</code> مع العقدة <code>AnimatedSprite</code>، فسيحتاج إلى مرجع إلى تلك العقدة:
</p>

<pre class="ipsCode">get_node("AnimatedSprite").play()
</pre>

<p>
	إن وسيط الدالة <code>get_node()‎</code> هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ <code>null instance</code> وخطأ عدم العثور على العقدة <code>Node not found</code> أيضًا.
</p>

<p>
	يُعَد الحصول على مرجع عقدة باستخدام الدالة <code>get_node()‎</code> حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة <code>$</code> للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة <code>AnimatedSprite‎</code> وتشغيل الدالة <code>()play</code> عليها مباشرة نكتب:
</p>

<pre class="ipsCode">$AnimatedSprite.play()
</pre>

<p>
	<strong>ملاحظة</strong>: تعيد الدالة <code>get_node()‎</code> مرجعًا Reference إلى العقدة المطلوبة.
</p>

<p>
	لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي:
</p>

<p style="text-align: center;">
	<a class="ipsAttachLink ipsAttachLink_image" data-fileext="png" data-fileid="167661" href="https://academy.hsoub.com/uploads/monthly_2025_02/03_node_paths_02.png.16a30f7729c8c84334a6afd913fa823f.png" rel=""><img alt="03 node paths 02" class="ipsImage ipsImage_thumbnailed" data-fileid="167661" data-unique="4svum3a5m" src="https://academy.hsoub.com/uploads/monthly_2025_02/03_node_paths_02.png.16a30f7729c8c84334a6afd913fa823f.png"> </a>
</p>

<p>
	إذا احتاج الكود المرفق بالعقدة <code>Main</code> إلى الوصول إلى العقدة <code>ScoreLabel</code>، فيمكنه ذلك باستخدام هذا المسار:
</p>

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9915_10" style=""><span class="pln">get_node</span><span class="pun">(</span><span class="str">"HUD/ScoreLabel"</span><span class="pun">).</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> </span><span class="str">"0"</span><span class="pln">
</span><span class="com"># ‫أو باستخدام الاختصار:</span><span class="pln">
$HUD</span><span class="pun">/</span><span class="typ">ScoreLabel</span><span class="pun">.</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> </span><span class="str">"0"</span></pre>

<p>
	<strong>ملاحظة</strong>: سيكمل <a href="https://academy.hsoub.com/programming/game-development/%D8%AA%D8%B9%D8%B1%D9%81-%D8%B9%D9%84%D9%89-%D9%85%D8%AD%D8%B1%D8%B1-%D9%85%D8%AD%D8%B1%D9%83-%D8%A7%EF%BB%B7%D9%84%D8%B9%D8%A7%D8%A8-%D8%AC%D9%88%D8%AF%D9%88-godot-r2183/" rel="">محرر جودو</a> المسارات تلقائيًا نيابةً عنا عند استخدام صيغة <code>$</code>، ويمكننا أيضًا النقر بزر الفأرة الأيمن على عقدة ما في تبويب المشهد Scene واختيار نسخ مسار العقدة Copy Node Path.
</p>

<p>
	إذا كانت العقدة التي نريد الوصول إليها موجودة في مكان أعلى من الشجرة، فيمكننا استخدام الدالة <code>get_parent()‎</code> أو <code>".."</code> للإشارة إلى العقدة الأب، حيث يمكننا الحصول على العقدة <code>Player</code> من العقدة <code>ScoreLabel</code> في شجرة المثال السابق كما يلي:
</p>

<pre class="ipsCode">get_node("../../Player")
</pre>

<p>
	يمثّل المسار ‎<code>"../../Player"</code>‎ الحصول على العقدة التي تقع في مستوى واحد أعلى <code>HUD</code> ثم العقدة التي تقع في مستوى أعلى وهي <code>Main</code> ثم الوصول إلى العقدة الابن لها وهي <code>Player</code>.
</p>

<p>
	<strong>ملاحظة</strong>: تعمل مسارات العقد مثل مسارات المجلدات في نظام التشغيل، حيث تشير الشرطة المائلة <code>/</code> إلى علاقة أب-ابن، وتعني <code>..</code> مستوى واحد أعلى.
</p>

<h3 id="relativeabsolute">
	المسارات النسبية Relative والمسارات المطلقة Absolute
</h3>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9915_12" style=""><span class="pln">get_node</span><span class="pun">(</span><span class="str">"/root/Main/Player"</span><span class="pun">)</span></pre>

<p>
	لا يعيد المسار <code>‎/root</code> والذي يمكن الوصول إليها أيضًا باستخدام <code>get_tree().root</code> العقدة الجذر لمشهدنا الحالي، ولكنه يعيد العقدة الجذر لنافذة العرض <a href="https://docs.godotengine.org/en/stable/classes/class_viewport.html" rel="external nofollow">Viewport</a> التي توجد دائمًا في شجرة المشهد SceneTree افتراضيًا.
</p>

<h3 id="-3">
	مشكلة في التعامل مع مسارات العقد في جودو
</h3>

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

<pre class="ipsCode prettyprint lang-py prettyprinted" id="ips_uid_9915_15" style=""><span class="pln">func take_damage</span><span class="pun">(</span><span class="pln">amount</span><span class="pun">):</span><span class="pln">
    health </span><span class="pun">-=</span><span class="pln"> amount
    get_node</span><span class="pun">(</span><span class="str">"../Main/UI/HealthBar"</span><span class="pun">).</span><span class="pln">text </span><span class="pun">=</span><span class="pln"> str</span><span class="pun">(</span><span class="pln">health</span><span class="pun">)</span></pre>

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

<ol>
	<li>
		لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة <code>get_node()‎</code> في حدوث عطل
	</li>
	<li>
		لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره
	</li>
</ol>

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

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

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

<p>
	ترجمة -وبتصرّف- للقسمين <a href="https://kidscancode.org/godot_recipes/4.x/basics/tree_ready_order/index.html" rel="external nofollow">Understanding tree order</a> و <a href="https://kidscancode.org/godot_recipes/4.x/basics/getting_nodes/index.html" rel="external nofollow">Understanding node paths</a> من توثيقات Kidscancode.
</p>

<p>
	اقرأ أيضًا
</p>

<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%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/%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>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-%D8%A7%D9%84%D8%A5%D8%B4%D8%A7%D8%B1%D8%A7%D8%AA-signals-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2385/" rel="">استخدام الإشارات Signals في جودو Godot</a>
	</li>
	<li>
		<a href="https://academy.hsoub.com/programming/game-development/%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D9%85%D8%AA%D8%A7%D8%AD%D8%A9-%D9%81%D9%8A-%D8%AC%D9%88%D8%AF%D9%88-godot-r2371/" rel="">لغات البرمجة المتاحة في جودو Godot</a>
	</li>
</ul>
]]></description><guid isPermaLink="false">2512</guid><pubDate>Tue, 18 Feb 2025 15:04:01 +0000</pubDate></item></channel></rss>
