الرسم ثنائي البعد المخصص في جودو

من موسوعة حسوب


مقدمة

لدى جودو عقد لرسم السبرايت والمضلعات والجزيئات وكل الأجزاء المختلفة، هذا كافي لمعظم الحالات. يمكن صنع أي عقدة ثنائية البعد (مبنية على عقدة Control أو  Node2D) ترسم أوامر مخصصة إذا لم تتواجد عقدة لرسم شيء تحتاجه.

إن الرسم المخصص ثنائي البعد مفيد جدًا لعدد من الحالات:

  • رسم أشكال أو تنفيذ منطق لا يمكن عمله في العقد الموجودة، مثل صورة لها أثر ورائها أو مضلع متحرك مخصص.
  • رسومات ليست متوافقة مع العقد، مثل لوحة لعبة التترس. (يستخدم مثال تترس دالة رسم مخصصة لرسم الأشكال).
  • رسم عدد كبير من الكائنات البسيطة. يتفادى الرسم المخصص الزيادة عند استخدام عدد كبير من العقد وربما يقلل استهلاك الذاكرة ويحسن الأداء.
  • صنع واجهة تحكم للمستخدم. هناك العديد من المتحكمات المتوفرة ولكنك ستحتاج إلى تحكم مخصص عندما يكون لديك احتياجات غير شائعة.

الرسم

أضف السكربت لأي عقدة CanvasItem مشتقة مثل Control أو  Node2D ثم أعد تعريف الدالة ‎_draw()‎

بلغة جي دي سكربت:

extends Node2D

func _draw():
	# أوامر الرسم هنا
	pass

بلغة سي شارب:

public override void _Draw()
{
    // أوامر الرسم هنا
}

أوامر الرسم موصوفة في مرجع صنف CanvasItem وهناك العديد من الأوامر المختلفة.

التحديث

يمكن استدعاء دالة ‎_draw()‎‏ مرة واحدة ويتم تخزين أوامر الرسم وتذكرها لذا لا يوجد داعي لاستدعاءات أخرى.

إذا كانت هناك حاجة لإعادة الرسم لأن الحالة أو شيء آخر تغير، استدعي ()CanvasItem.queue_redraw‏‎‎‎‏‏‎‎ في نفس العقدة وسيحصل استدعاء ‎_draw()‎ جديد.

التالي هو مثال أعقد، الذي هو متغير ملمس سيُعاد رسمه إذا تعدل:

بلغة جي دي سكربت:

extends Node2D

export (Texture) var texture setget _set_texture

func _set_texture(value):
	# يتم استدهاء الدالة هذه إذا كان متغير الملمس معدلًا بشكل خارجي
	texture = value  # تغيير الملمس
	queue_redraw()  # إعادة رسم العقدة

func _draw():
	draw_texture(texture, Vector2())

بلغة سي شارب:

using Godot;

public partial class MyNode2D : Node2D
{
    private Texture _texture;
    public Texture Texture
    {
        get
        {
            return _texture;
        }

        set
        {
            _texture = value;
            QueueRedraw();
        }
    }

    public override void _Draw()
    {
        DrawTexture(_texture, new Vector2());
    }
}

في بعض الحالات من المرغوب أن يتم رسم كل إطار، من أجل ذلك استدعي queue redraw()‎ من الدالة ‎_process()‎ كالتالي: بلغة جي دي سكربت:

extends Node2D

func _draw():
	# أوامر الرسم هنا
	pass

func _process(delta):
	queue_redraw()

بلغة سي شارب:

using Godot;

public partial class CustomNode2D : Node2D
{
    public override void _Draw()
    {
        // أوامر الرسم هنا
    }

    public override void _Process(double delta)
    {
        QueueRedraw();
    }
}

الإحداثيات

تستخدم واجهة الرسم البرمجية نظام الإحداثيات الخاص بالعقدة CanvasItem، وليس بالضرورة أن تكون الإحداثيات بواحدة البيكسل. هذا يعني أنها تستخدم فضاء الإحداثيات المُنشأ بعد تطبيق تحويل CanvasItem. إضافة لذلك يمكن تطبيق تحويل مخصص فوقه عن طريق استخدام draw_set_transform أو draw_set_transform_matrix.

يجب أخذ عرض الخط بعين الاعتبار عند استخدام draw_line، يجب تحريك الموقع بمسافة 0.5 عند استخدام عرض رقمه فردي لإبقاء الخط في المنتصف كما يُظهر التالي:


بلغة جي دي سكربت:

func _draw():
	draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
	draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
	draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)

بلغة سي شارب:

public override void _Draw()
{
    DrawLine(new Vector2(1.5f, 1.0f), new Vector2(1.5f, 4.0f), Colors.Green, 1.0f);
    DrawLine(new Vector2(4.0f, 1.0f), new Vector2(4.0f, 4.0f), Colors.Green, 2.0f);
    DrawLine(new Vector2(7.5f, 1.0f), new Vector2(7.5f, 4.0f), Colors.Green, 3.0f);
}

الأمر ذاته ينطبق على التابع draw_rect مع filled=false.


بلغة جي دي سكربت:

func _draw():
	draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
	draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
	draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
	draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)

بلغة سي شارب:

public override void _Draw()
{
    DrawRect(new Rect2(1.0f, 1.0f, 3.0f, 3.0f), Colors.Green);
    DrawRect(new Rect2(5.5f, 1.5f, 2.0f, 2.0f), Colors.Green, false, 1.0f);
    DrawRect(new Rect2(9.0f, 1.0f, 5.0f, 5.0f), Colors.Green);
    DrawRect(new Rect2(16.0f, 2.0f, 3.0f, 3.0f), Colors.Green, false, 2.0f);
}

مثال: رسم أقواس دائرية

سنستخدم خاصية الرسم المخصصة لمحرك جودو لرسم شيء لا تقدمه دوال جودو. تقدم جودو دالة darw_circle()‎ التي ترسم دائرة كاملة. ولكن ماذا عن رسم جزء من دائرة؟ يجب عليك كتابة شيفرة لدالة للقيام بذلك ورسمها بنفسك.

دالة القوس

يعرّف القوس بمعاملات الدائرة المكونة له، أي الموقع المركزي ونصف القطر. ويعرّف القوس بزاوية البداية والنهاية. يجب تقديم هذه الوسائط الأربعة لدالة الرسم. سنقدم أيضًا قيمة اللون لرسم القوس بألوان مختلفة إذا رغبنا.

يحتاج رسم شكل على الشاشة أن يتم تفكيكه إلى عدد من النقط المرتبطة واحدة تلو الأخرى، كلما كان الشكل فيه نقاط أكثر كان شكله أسلس ولكن يكون تأثيرها أكبر للمعالجة. بالعادة إذا كان الشكل أكبر (أو أقرب إلى الكاميرا في الأبعاد الثلاث) ستحتاج لرسم نقاط أكثر لكيلا يكون شكله خشن، على العكس عندما يكون الشكل صغير (أو أبعد إلى الكاميرا في الأبعاد الثلاث) يمكن تقليل عدد النقاط للمحافظة على قدرة المعالجة. هذا ما يسمى مستوى التفاصيل Level of Detail (LOD)‎. في مثالنا نحتاج فقط لاستخدام عدد محدد من النقاط مهما يكون نصف القطر.

بلغة جي دي سكربت:

func draw_circle_arc(center, radius, angle_from, angle_to, color):
	var nb_points = 32
	var points_arc = PackedVector2Array()

	for i in range(nb_points + 1):
		var angle_point = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
		points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)

	for index_point in range(nb_points):
		draw_line(points_arc[index_point], points_arc[index_point + 1], color)

بلغة سي شارب:

public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
    int nbPoints = 32;
    var pointsArc = new Vector2[nbPoints + 1];

    for (int i = 0; i <= nbPoints; i++)
    {
        float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
        pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
    }

    for (int i = 0; i < nbPoints - 1; i++)
    {
        DrawLine(pointsArc[i], pointsArc[i + 1], color);
    }
}

أتذكر عدد النقط في شكلنا التي سيتم تفكيكها؟ حددنا هذا الرقم في المتغير nb_points إلى القيمة 32. ثم أنشأنا PackedVector2Array فارغة وهي مصفوفة بسيطة من النوع Vector2.

الخطوة التالية هي حساب عدد المواقع الفعلية لهذه النقاط الـ 32 التي تكون القوس. يتم عمل ذلك في حلقة for الأولى، نكرر عدد النقاط التي نريد حساب المواقع بالإضافة لواحد لضم أخر نقطة. يجب تحديد زاوية كل نقطة بين زوايا البداية والنهاية.

سبب تقليل الزاوية إلى 90 درجة هو أنه سيحسب الموقع ثنائي البعد من كل زاوية باستخدام المثلثات ( أي sin وcosine و.).‎ ولكن يستخدم التابعين cos()‎ وsin()‎ واحدة الراديان وليس الدرجات. إن زاوية 0 (0 راديان) تبدأ على نفس مكان الساعة 3 في الساعة على الرغم من أننا نبدأ العد من الساعة 12. لذا ننقص كل زاوية بمقدار 90 درجة بهذا الترتيب لبدء العد من الساعة 12.

يعطى المكان الحقيقي لنقطة على الدائرة في الزاوية angle (بالراديان) باستخدام Vector2(cos(angle)، sin(angle)). لأن sin()‎ وcos()‎ تعيدان قيم بين 1 و -1 يكون الموقع موجودًا على دائرة نصف قطرها 1. للحصول على هذا الموقع على دائرة الدعم التي لديها نصف قطر radius يجب علينا ضرب الموقع في radius. أخيرًا نحتاج لوضع دائرة الدعم في المركز center عن طريق إضافة القيمة إلى Vector2 الخاصة بنا، ونضع النقطة في PackedVector2Array التي عرفناه مسبقًا.

نحتاج الآن إلى رسم النقط وكما تتخيل لن نرسم النقاط البالغ عددها 32 ببساطة؛ إذ أننا سنحتاج إلى رسم كل شيء بينها. يمكن حساب كل نقطة باستخدام الطريقة السابقة ورسمها واحدة تلو الأخرى. ولكنها معقدة جدًا وغير سريعة (ما عدا في حال احتياجها بشكل واضح)، لذا سنرسم خطوط بين كل زوج نقاط. لن يكون طول كل خط واضحًا بين النقط ما عدا عندما يكون نصف قطر دائرة الدعم كبير، وإذا كان كذلك سنزيد عدد النقاط.

رسم القوس على الشاشة

لدينا الآن دالة ترسم على الشاشة حان وقت استدعائها على الدالة ‎_‎darw()‎

بلغة جي دي سكربت:

func _draw():
	var center = Vector2(200, 200)
	var radius = 80
	var angle_from = 75
	var angle_to = 195
	var color = Color(1.0, 0.0, 0.0)
	draw_circle_arc(center, radius, angle_from, angle_to, color)

بلغة سي شارب:

public override void _Draw()
{
    var center = new Vector2(200, 200);
    float radius = 80;
    float angleFrom = 75;
    float angleTo = 195;
    var color = new Color(1, 0, 0);
    DrawCircleArc(center, radius, angleFrom, angleTo, color);
}

النتيجة:

دالة قوس مضلع

يمكن القيام بأكثر من ذلك بعدم الاكتفاء فقط بدالة ترسم القسم البسيط من القرص المعرف بالقوس ولكن أيضًا شكله. الطريقة هي نفسها كما قبل ولكن نرسم مضلع بدلًا من الخطوط.

بلغة جي دي سكربت:

func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
	var nb_points = 32
	var points_arc = PackedVector2Array()
	points_arc.push_back(center)
	var colors = PackedColorArray([color])

	for i in range(nb_points + 1):
		var angle_point = deg_to_rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
		points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
	draw_polygon(points_arc, colors)

بلغة سي شارب

public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
{
    int nbPoints = 32;
    var pointsArc = new Vector2[nbPoints + 2];
    pointsArc[0] = center;
    var colors = new Color[] { color };

    for (int i = 0; i <= nbPoints; i++)
    {
        float anglePoint = Mathf.DegToRad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
        pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
    }

    DrawPolygon(pointsArc, colors);
}

رسم ديناميكي مخصص

نستطيع الآن رسم أشياء مخصصة على الشاشة، ولكن تكون ثابتة لنجعل هذا الشكل يدور حول مركزه. الطريقة هي بتغيير قيم angle_from وangle_to مع الزمن. نزيد في مثالنا القيم بـ 50. يجب على هذه الزيادة أن تكون ثابتة أو ستتغير القيمة تبعا لها.

يجب أولًا جعل القيم angle_from وangle_to عامة في أعلى السكربت. لاحظ أنه يمكننا أيضًا تخزينها في عقد أخرى والوصول إليها باستخدام get_node()

بلغة جي دي سكربت:

extends Node2D

var rotation_angle = 50
var angle_from = 75
var angle_to = 195

بلغة سي شارب

using Godot;

public partial class MyNode2D : Node2D
{
    private float _rotationAngle = 50;
    private float _angleFrom = 75;
    private float _angleTo = 195;
}

نغير هذه القيم في دالة ‎_process(delta)

يمكننا زيادة قيم angle_from وangle_to هنا ولكن يجب ألا ننسى استخدام wrap()‎ مع القيم الناتجة بين 0 و 360! أي إذا كانت الزاوية 361 ستكون بالحقيقة 1. إذا لم نغلف القيم فسيعمل السكربت بشكل عادي حتى يصل لأعلى قيمة يمكن أن تتحملها جودو (وهي 2^31 - 1) عندما نصل لهذه القيمة يمكن أن يتوقف جودو عن العمل أو يعطي سلوك غير متوقع.

لا يجب أن ننسى استدعاء الدالة queue_redraw()‎ التي تستدعي ‎_draw()‎ تلقائيًا. تستطيع أن تتحكم بهذه الطريقة متى تريد تحديث الإطار.

بلغة جي دي سكربت:

func _process(delta):
	angle_from += rotation_angle
	angle_to += rotation_angle

	# نستخدم الدوران مع الزوايا عندما تكون أكبر من 360
	if angle_from > 360 and angle_to > 360:
		angle_from = wrapf(angle_from, 0, 360)
		angle_to = wrapf(angle_to, 0, 360)
	queue_redraw()

بلغة سي شارب:

public override void _Process(double delta)
{
    _angleFrom += _rotationAngle;
    _angleTo += _rotationAngle;

    // نستخدم الالتفاف مع الزوايتين فقط عندما تكونان أكبر من 360
    if (_angleFrom > 360 && _angleTo > 360)
    {
        _angleFrom = Mathf.Wrap(_angleFrom, 0, 360);
        _angleTo = Mathf.Wrap(_angleTo, 0, 360);
    }
    QueueRedraw();
}

لا ننسى أيضًا تعديل الدالة ‎_draw()‎ للاستفادة من هذه المتغيرات بلغة جي دي سكربت:

func _draw():
   var center = Vector2(200, 200)
   var radius = 80
   var color = Color(1.0, 0.0, 0.0)

   draw_circle_arc( center, radius, angle_from, angle_to, color )

بلغة سي شارب:

public override void _Draw()
{
    var center = new Vector2(200, 200);
    float radius = 80;
    var color = new Color(1, 0, 0);

    DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
}

لننفذ البرنامج! إنه يعمل ولكن يدور القرص بسرعة كبيرة! ما المشكلة؟

السبب هو أن كرت الشاشة يظهر الإطارات بأسرع ما يمكنه. سنحتاج لتسوية الرسم بهذه السرعة يجب علينا لتحقيق ذلك استخدام المعامل delta للدالة ‎_process()‎. يمثل delta الوقت ما بين آخر إطارين مصيرين. ويكون مقدارًا صغيرًا بالعادة (حوالي 0.0003 ثانية ولكن ذلك يعتمد على العتاد الخاص بك) لذا استخدام delta للتحكم بالرسم يضمن أن البرنامج يعمل بنفس السرعة على كل أنواع العتاد.

سنحتاج في حالتنا إلى ضرب المتغير rotation_angle بقيمة delta في الدالة ‎_process()‎ . بهذه الطريقة ستزداد الزاويتان بقيمة صغيرة جدًا تعتمد على سرعة التصيير.

بلغة جي دي سكربت:

func _process(delta):
	angle_from += rotation_angle * delta
	angle_to += rotation_angle * delta

	# نستخدم الالتفاف عندما تكون كل من الزاويتين أكبر من 360
	if angle_from > 360 and angle_to > 360:
		angle_from = wrapf(angle_from, 0, 360)
		angle_to = wrapf(angle_to, 0, 360)
	queue_redraw()

بلغة سي شارب:

public override void _Process(double delta)
{
    _angleFrom += _rotationAngle * (float)delta;
    _angleTo += _rotationAngle * (float)delta;

    // نستخدم الالتفاف فقط عندما تكون الزاويتان أكبر من 360
    if (_angleFrom > 360 && _angleTo > 360)
    {
        _angleFrom = Wrap(_angleFrom, 0, 360);
        _angleTo = Wrap(_angleTo, 0, 360);
    }
    QueueRedraw();
}

لننفذ مرة أخرى! هذه المرة يكون الدوران جيدًا.

الرسم منعم التشويش

تقدم جودو معاملات للتابع في draw_line لتفعيل تنعيم التشويش antialiasing، ولكن لا تقدم كل توابع الرسم المخصص معامل antialiased

يمكن تفعيل تنعيم التشويش متعدد العينات ثنائي البعد بالنسبة لتوابع الرسم التي لا تقدم معامل antialiased الذي يؤثر على التصيير في كل الإطار المعروض. يقدم ذلك تنعيم تشويش عالي النوعية ولكن على حساب الأداء وعلى عناصر محددة فقط. راجع تنعيم التشويش ثنائي البعد لمعلومات أكثر.

الأدوات

يكون رسم العقد الخاصة بك مرغوبًا عند تنفيذها في المحرر. يمكن استخدام ذلك لمشاهدة أو رسم بعض الميزات أو السلوك. راجع تنفيذ الشيفرة في المحرر لمعلومات أكثر عن هذه العملية.

مصادر