No history yet

هندسة المحركات والمنطق

بنية المحرك: Unity مقابل Unreal

تعتمد كل من Unity و Unreal على نموذج المكونات (Component-based model) كأساس لهندستها، لكن طريقة التطبيق تختلف. في Unity، الوحدة الأساسية هي كائن اللعبة (GameObject)، وهو حاوية فارغة. يمكنك إرفاق مكونات (Components) مختلفة به لمنحه وظائف، مثل Transform للموقع، أو Rigidbody للفيزياء، أو نصوص برمجية مخصصة للسلوك.

أما في Unreal Engine، فالمفهوم مشابه لكنه يُدعى Actor. الـ Actor هو الوحدة الأساسية في المشهد، ويحتوي على مكونات (Actor Components) تُعرّف سلوكه وقدراته. الاختلاف الجوهري هو أن Unreal يشجع على بناء تسلسل هرمي للفئات (class inheritance) بشكل أكبر، حيث يمكنك إنشاء Actor مخصص يرث من فئة أساسية مثل Pawn (للتحكم) أو Character (للشخصيات المتحركة).

يسير Unity الآن نحو مستقبل يعتمد على البيانات بشكل أكبر مع ما يسمى DOTS. إنه تغيير جذري يهدف إلى تحقيق أداء عالٍ للغاية من خلال تنظيم البيانات والمنطق البرمجي بطريقة أكثر كفاءة للمعالجات الحديثة.

إدارة دورة حياة الكائنات

تعد إدارة دورة حياة الكائن (Object Lifecycle) أمراً حيوياً لتجنب الأخطاء وضمان تشغيل اللعبة بسلاسة. يحدد كل محرك ترتيباً صارماً لتنفيذ دوال معينة أثناء عمر الكائن.

في Unity، الترتيب العام هو:

  1. Awake: تُستدعى مرة واحدة عند إنشاء الكائن. مثالية لتهيئة المتغيرات والمراجع الداخلية للمكون نفسه.
  2. OnEnable: تُستدعى عند تفعيل الكائن.
  3. Start: تُستدعى مرة واحدة قبل أول تحديث للإطار (frame). تستخدم غالباً لإعداد الاتصالات بين الكائنات المختلفة، لأنك تضمن أن جميع الكائنات الأخرى قد قامت بتنفيذ Awake بالفعل.
  4. FixedUpdate: تُستدعى على فترات زمنية ثابتة. هذا هو المكان المناسب لوضع منطق الفيزياء.
  5. Update: تُستدعى مرة واحدة كل إطار. تستخدم لمعظم منطق اللعبة غير الفيزيائي، مثل استقبال المدخلات من اللاعب.
  6. LateUpdate: تُستدعى مرة واحدة كل إطار بعد انتهاء جميع دوال Update. مفيدة لمنطق الكاميرا التي تتبع اللاعب لضمان أن اللاعب قد تحرك أولاً.

في Unreal، تكون الدورة مشابهة:

  1. Constructor: عند إنشاء الكائن لأول مرة.
  2. BeginPlay: تُعادل Start في Unity. تُستدعى عند بدء اللعبة أو عند ظهور الـ Actor في المشهد.
  3. Tick: تُعادل Update في Unity. تُستدعى كل إطار لتنفيذ المنطق المستمر.
  4. EndPlay: تُستدعى عند تدمير الـ Actor أو انتهاء اللعبة.

التصميم المعتمد على البيانات

أحد أكبر الأخطاء التي يقع فيها المطورون الجدد هو دمج البيانات مباشرة في الكود (hardcoding). تخيل أن لديك 100 نوع من الأعداء، وكل عدو له نقاط صحة وسرعة وقوة هجوم مختلفة مكتوبة مباشرة في نصه البرمجي. أي تعديل بسيط سيتطلب تغيير الكود وإعادة ترجمته.

الحل هو فصل البيانات عن المنطق. هنا يأتي دور التصميم المعتمد على البيانات. في Unity، الأداة المثالية لهذا هي ScriptableObjects. إنها حاويات بيانات يمكنك إنشاؤها كأصول (assets) منفصلة في مشروعك. يمكن لمصمم اللعبة تعديل هذه البيانات دون لمس سطر واحد من الكود.

// C# Example for a Unity ScriptableObject

using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyStats", menuName = "Game/Enemy Stats")]
public class EnemyStats : ScriptableObject
{
    public string enemyName;
    public float health = 100f;
    public float moveSpeed = 5f;
    public int attackDamage = 10;
    public GameObject enemyPrefab;
}

الآن، يمكن أن يحتوي النص البرمجي للعدو على مرجع لهذا الـ EnemyStats، ويقرأ القيم منه. يمكنك إنشاء ملفات EnemyStats مختلفة لـ "Goblin" و "Orc" و "Dragon"، وكلها تستخدم نفس الكود البرمجي للعدو.

في Unreal Engine، المفهوم المكافئ هو Data Assets. إنها فئة C++ بسيطة ترث من UDataAsset وتستخدم لحفظ البيانات التي يمكن تحريرها في المحرر.

// C++ Example for an Unreal Data Asset

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "EnemyStatsDataAsset.generated.h"

UCLASS(BlueprintType)
class MYGAME_API UEnemyStatsDataAsset : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stats")
    FString EnemyName;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stats")
    float Health = 100.0f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stats")
    float MoveSpeed = 500.0f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stats")
    int32 AttackDamage = 10;
};

أنماط التصميم الشائعة

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

نمط Singleton يضمن هذا النمط وجود نسخة واحدة فقط من فئة معينة، ويوفر نقطة وصول عالمية إليها. إنه مفيد جدًا للمديرين (Managers) مثل GameManager أو AudioManager أو LevelManager. على الرغم من سهولة استخدامه، إلا أن الإفراط فيه يمكن أن يؤدي إلى كود يصعب صيانته واختباره لأنه يخفي الاعتماديات.

استخدم Singleton بحذر. إنه أداة قوية، لكنه يمكن أن يحول مشروعك إلى فوضى إذا تم استخدامه لكل شيء.

نمط Observer يسمح هذا النمط لكائن (الموضوع - Subject) بالحفاظ على قائمة من الكائنات التابعة له (المراقبون - Observers)، وإعلامهم تلقائيًا بأي تغييرات في حالته. مثال كلاسيكي هو واجهة المستخدم (UI) التي تعرض صحة اللاعب. عندما يتلقى اللاعب ضررًا (تغير في الحالة)، يقوم بإعلام عنصر واجهة المستخدم الخاص بالصحة ليقوم بتحديث نفسه. هذا يفصل بين اللاعب وواجهة المستخدم، مما يجعل كل منهما مستقلاً وقابلاً لإعادة الاستخدام.

نمط State يسمح هذا النمط للكائن بتغيير سلوكه عندما تتغير حالته الداخلية. يبدو الأمر كما لو أن الكائن يغير فئته. إنه مثالي لإدارة الذكاء الاصطناعي للأعداء. قد يكون للعدو حالات مثل Patrolling (دورية)، Chasing (مطاردة)، و Attacking (هجوم). بدلاً من استخدام جمل if/else معقدة في دالة Update واحدة، يتم تغليف كل حالة في فئتها الخاصة. عندما يتغير شرط ما (مثل رؤية اللاعب)، يقوم العدو ببساطة بالتبديل من حالة Patrolling إلى حالة Chasing.

Quiz Questions 1/5

في محرك Unity، ما هي الوحدة الأساسية التي تعمل كحاوية لإرفاق المكونات المختلفة بها؟

Quiz Questions 2/5

في أي دالة من دوال دورة حياة كائن Unity يُنصح بوضع منطق الفيزياء لضمان تنفيذه على فترات زمنية ثابتة؟

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