النموذج الأولي
^title Prototype ^section Design Patterns Revisited
أول مرة سمعت فيها كلمة «نموذج أولي» (prototype) كانت في أنماط التصميم. اليوم يبدو أن الجميع ينطق بها، لكن اتضح أنهم لا يتحدثون عن نمط التصميم نفسه. وسنغطي ذلك هنا، لكنني سأعرض عليك أيضاً أماكن أخرى أكثر إثارة ظهرت فيها هذه الكلمة والمفاهيم الكامنة خلفها. لكن لنبدأ أولاً بالعودة إلى النمط الأصلي.
نمط تصميم النموذج الأولي
لنفترض أننا نصنع لعبة على نمط لعبة Gauntlet. لدينا مخلوقات وشياطين تزحف حول البطل وتتنافس على نصيبها من لحمه.
تدخل هذه الرفقة غير المستساغة الحلبة عبر «منشآت الإنشاء» (spawners)، ولكل نوع من الأعداء منشأة إنشاء خاصة به.
ولأغراض هذا المثال، لنقل لدينا أصناف مختلفة لكل نوع من الوحوش في اللعبة — Ghost و
Demon وSorcerer وما شابه — مثل:
^code monster-classes
تبني منشأة الإنشاء نسخاً من نوع وحش بعينه. ولدعم كل وحش في اللعبة، يمكننا أن نلجأ إلى الحلّ الأعمى بصنف منشأة إنشاء لكل صنف وحش، مما يقود إلى تسلسل أصناف متوازٍ:
ولتنفيذه، سيبدو هكذا:
^code spawner-classes
ما لم تكن تتقضى أجراً على أساس عدد أسطر الشيفرة، فهذي ليست طريقة ممتعة لتجميع هذا الأمر. أصناف كثيرة، وشيفرة تهيئة كثيرة، وتكرار كثير، ونسخ كثير، وتكرار للذات كثير...
يقدّم نمط النموذج الأولي حلاً. والفكرة الأساسية هي أن كائناً يستطيع أن يُنشئ كائنات أخرى شبيهة به.
فإن كان لديك شبح واحد، يمكنك أن تصنع منه أشباحاً أخرى. وإن كان لديك شيطان، يمكنك أن تصنع شياطين أخرى. وأي وحش يمكن معاملته كوحش نموذجي يُستخدم لتوليد نسخ أخرى منه.
ولتنفيذ ذلك، نمنح صنفنا الأساسي Monster دالة clone() مجرَّدة:
^code virtual-clone
يوفّر كل صنف فرعي من الوحوش تنفيذاً يُعيد كائناً مطابقاً له في الصنف والحالة. مثلاً:
^code clone-ghost
وبعد أن تدعم جميع وحوشنا ذلك، لا نعود بحاجة إلى صنف منشأة إنشاء لكل صنف وحش. وبدلاً من ذلك، نعرّف صنفاً واحداً:
^code spawner-clone
وهو يحتفظ في داخله بوحشٍ مخفيّ غرضه الوحيد أن تستخدمه منشأة الإنشاء كقالبٍ تُختَز بمقتضاه وحوشٌ أخرى مثله، على نحو ما تفعل ملكة النحل التي لا تغادر الخلية.
ولإنشاء منشأة إنشاء للأشباح، ننشئ نسخة شبح نموذجية أولية، ثم ننشئ منشأة إنشاء تحمل ذلك النموذج:
^code spawn-ghost-clone
ومن الأجزاء اللطيفة في هذا النمط أنه لا يستنسخ صنف النموذج الأولي فحسب، بل يستنسخ حالته أيضاً.
وهذا يعني أننا يمكننا صنع منشأة إنشاء للأشباح السريعة أو الضعيفة أو البطيئة، بمجرد إنشاء نموذج شبح مناسب.
وأجد في هذا النمط شيئاً أنيقاً ومفاجئاً في آن. لا أستطيع تخيّل أنني كنت سأبتكره بنفسي، لكنني لا أستطيع أيضاً تخيّل عدم معرفتي به الآن بعد أن عرفت.
ما مدى فاعليته؟
حسناً، لسنا بحاجة إلى إنشاء صنف منشأة إنشاء منفصل لكل وحش، وهذا أمر جيد. لكننا نحتاج
إلى تنفيذ clone() في كل صنف وحش. وهذا بقدر ما يكون تقريباً من حجم شيفرة منشآت
الإنشاء.
كما أن هناك بعض الحفر الدلالية القبيحة حين تجلس وتحاول كتابة clone() صحيحة. هل
تُنتج استنساخاً عميقاً أم سطحياً؟ بعبارة أخرى، إن كان الشيطان يحمل منجلاً، فهل ينسخ
استنساخ الشيطان المنجل أيضاً؟
كما أن الأمر، في هذه المشكلة المصطنعة، لا يبدو وكأنه يوفّر علينا كثيراً من الشيفرة، وإلى جانب ذلك فهي مشكلة مصطنعة أصلاً. فنحن مضطرون إلى الأخذ بالافتراض بأن لدينا أصنافاً منفصلة لكل وحش.
وفي الواقع، تعلّمنا معظمنا بالطريقة الصعبة أن التسلسلات الهرمية الكبيرة من الأصناف مُرهقة في الإدارة، ولهذا نستخدم أنماطاً مثل المكوّن (Component) وكائن النوع (Type Object) لنمذجة مختلف أنواع الكيانات دون تثبيت كل منها في صنف خاص به.
دوال الإنشاء
حتى لو كان لدينا فعلاً أصناف مختلفة لكل وحش، فهناك طرق أخرى لتفريخ هذا القطّ الأليف. فبدلاً من صنع أصناف منشآت إنشاء منفصلة لكل وحش، يمكننا صنع دوال إنشاء، مثل هذا:
^code callback
وهذا أقلّ شيفرة تهيئة من صنع صنف كامل لإنشاء وحش من نوع ما. ثمّ إن صنف منشأة الإنشاء الواحد يستطيع أن يخزّن ببساطة مؤشر دالة:
^code spawner-callback
ولإنشاء منشأة إنشاء للأشباح، تفعل:
^code spawn-ghost-callback
القوالب
بحسب الوقت الحالي، فإن معظم مطوّري C++ على معرفة بالقوالب. فصنف منشأة الإنشاء يحتاج إلى إنشاء نسخ من نوع ما، لكننا لا نريد تثبيت صنف وحش بعينه في الشيفرة. الحلّ الطبيعي إذاً هو جعله معامل نوع، وهو ما تتيحه القوالب:
^code templates
ويبدو استخدامه هكذا:
^code use-templates
أنواع من الطبقة الأولى
عالِج الحلّان السابقان الحاجة إلى وجود صنف Spawner مُعامَل بنوع. وفي C++، الأنواع ليست
عادةً من الطبقة الأولى، لذا يتطلب ذلك بعض الحِيَل.
وإن كنت تستخدم لغة ديناميكية الأنواع مثل JavaScript أو Python أو Ruby حيث الأصناف
كائنات عادية يمكنك تمريرها، فستحلّ الأمر على نحو أكثر مباشرة.
وحين تصنع منشأة إنشاء، مرّر ببساطة صنف الوحش الذي يفترض أن تبنيه — أي الكائن الفعلي وقت التنفيذ الذي يمثّل صنف الوحش. بسيطة كقطع الكعكة.
ومع كل هذه الخيارات، لا أستطيع بصدق أن أقول إنني وجدت حالة واحدة شعرت فيها بأن نمط التصميم هذا كان أفضل إجابة. ربما تكون خبرتك مختلفة، لكن دعنا نترك هذا الأمر جانباً ونتحدث عن شيء آخر: النماذج الأولية بوصفها نموذجاً برمجياً.
النموذج البرمجي القائم على النماذج الأولية
كثير من الناس يعتبرون «البرمجة الكائنية» مرادفةً لـ«الأصناف». وتميل تعريفات البرمجة الكائنية إلى أن تبدو كعفائد طوائف دينية متعارضة، لكن هناك مقاربة لا تثير جدلاً كبيراً لها، وهي أن البرمجة الكائنية تتيح لك تعريف «كائنات» تجمع البيانات والشيفرة معاً. ومقارنةً باللغات البنيوية مثل C واللغات الوظيفية مثل Scheme، فإن السمة المميّزة للبرمجة الكائنية هي أنها تربط الحالة والسلوك ربطاً محكماً.
قد تعتقد أن الأصناف هي الطريق الوحيد للقيام بذلك، لكن حفنة من المطوّرين بما فيهم Dave Ungar وRandall Smith تعارضا ذلك. فقد أنشأوا لغة في الثمانينيات اسمها Self. وهي كائنية بقدر ما تكون، لكنها بلا أصناف.
Self
في المعنى الخالص، لغة Self أكثر كائنية من اللغة القائمة على الأصناف. فنحن نفكر في البرمجة الكائنية كزواج بين الحالة والسلوك، لكن اللغات ذات الأصناف تضع في الواقع خط فاصل بينهما.
تخيّل دلالات لغتك المفضّلة القائمة على الأصناف. فللوصول إلى بعض الحالة على كائن، تنظر في ذاكرة النسخة نفسها. فالحالة محتواة في النسخة.
أما لاستدعاء دالة، فتنظر إلى صنف النسخة، ثم تبحث عن الدالة هناك. فالسلوك محتوا في الصنف. وهناك دائماً ذلك المستوى من التوجيه للوصول إلى الدالة، وهذا يعني أن الحقول والدوال مختلفة.
وتلغي Self هذا التمييز. فالبحث عن أي شيء يتم بالنظر إلى الكائن نفسه. فالنسخة يمكن أن تحتوي على الحالة والسلوك معاً. ويمكن أن يكون لديك كائن واحد لديه دالة فريدة عنه تماماً.
ولو كان هذا كل ما تفعله Self، لكان استخدامها صعباً. فالوراثة في اللغات القائمة على الأصناف، رغم عيوبها، تتيح لك آلية مفيدة لإعادة استخدام الشيفرة متعددة الأشكال وتجنّب التكرار. ولتحقيق شيء مشابه بلا أصناف، تمتلك Self التفويض.
وللعثور على حقل أو استدعاء دالة على كائن ما، ننظر أولاً في الكائن نفسه. فإذا كان يحتويه انتهينا. وإن لم يكن كذلك، ننظر إلى الوالد للكائن. وهذا ليس سوى مرجع إلى كائن آخر. وحين نفشل في العثور على خاصية على الكائن الأول، نجرّب والده ثم والد والده وهكذا. بعبارة أخرى، تُفوَّض عمليات البحث الفاشلة إلى والد الكائن.
وتتيح لنا كائنات الآباء إعادة استخدام السلوك (والحالة أيضاً!) عبر عدة كائنات، فقد غطّينا
جزءاً من فائدة الأصناف. والأمر الأساسي الآخر الذي تفعله الأصناف أنه يمنحك طريقة لإنشاء
النسخ. فحين تحتاج إلى شيء جديد، يمكنك أن تكتب مجرّد new Thingamabob() أو ما شابه بحسب
الصيغة التي تفضّلها لغتك. فالصنف مصنع لنسخه.
وبدون أصناف، فكيف نصنع أشياء جديدة؟ والأهم: كيف نصنع مجموعة من الأشياء الجديدة التي يشترك كل منها في أشياء معينة؟ تماماً كما في نمط التصميم، فالطريقة في Self هي الاستنساخ.
في Self، يبدو الأمر كأن كل كائن يدعم نمط تصميم النموذج الأولي تلقائياً. فأي كائن يمكن استنساخه. ولصنع مجموعة من الكائنات المتشابهة، فإنك:
-
تحوّل كائناً واحداً إلى الشكل الذي تريده. يمكنك استنساخ الكائن
Objectالأساسي المدمج في النظام ثم تملأه حقولاً ودوال. -
تستنسخه لتصنع ما تشاء من النسخ... آه... المتشابهة.
هذا يمنحنا أناقة نمط تصميم النموذج الأولي من دون ملل تنفيذ clone() بأنفسنا؛ فهو
مدمج في النظام.
وهو نظام جميل وذكي ومختصر إلى هذا الحد، بحيث إنما عرفتُ عنه بدأتُ أنشئ لغة قائمة على النماذج الأولية لأكتسب خبرة أكبر معه.
كيف سارت الأمور؟
كنت متحمّساً جداً لجرّب لغة قائمة على النماذج الأولية خالصة، لكن حين شغّلت لغاتي، اكتشفتُ حقيقة مزعجة: لم تكن البرمجة فيها ممتعة إلى ذلك الحد.
صحيح، كانت اللغة بسيطة التنفيذ، لكن ذلك لأنها أزاحت التعقيد إلى المستخدم. وما إن بدأت أحاول استخدامها، وجدتني أفتقد البنية التي توفّرها الأصناف. وانتهيت إلى محاولة إعادة تكوينها على مستوى المكتبة لأن اللغة لم تكن تملكها.
ربما يكون هذا لأن خبرتي السابقة في اللغات القائمة على الأصناف، فقد جُهِلت ذهني بذلك النموذج. لكن حدسي يقول إن معظم الناس يحبّون «أنواع الأشياء» المحدّدة بوضوح.
وإلى جانب النجاح الهائل للغات القائمة على الأصناف، انظر إلى كم من الألعاب تملك أصناف شخصيات صريحة وقائمة دقيقة بأنواع مختلفة من الأعداء والغنائم والمهارات، كلٌّ منها موسوم تسميةً واضحة. ولا ترى ألعاباً كثيرة يكون فيها كل وحش ندفة ثلجية فريدة من نوعها، مثل «نوعٌ ما بين التروّل والجوبلين مع قليل من الأفعى ممزوج فيها».
بينما النماذج الأولية نموذج رائع حقاً وأتمنّى لو يعرفه مزيد من الناس، إلا أنني مسرور بأن معظمنا لا يبرمج باستخدامها كل يوم. فـالشيفرة التي رأيتها تتبنّى النماذج الأولية بشكل كامل تبدو لي طريةً وغامضة يصعب عليّ لفهمها.
وماذا عن JavaScript؟
حسناً، إن كانت اللغات القائمة على النماذج الأولية غير ودودة إلى هذا الحد، فكيف أشرح JavaScript؟ إليك لغة تستخدم النماذج الأولية ويستخدمها ملايين الناس كل يوم. فعدد الحاسيبات التي تشغّل JavaScript يفوق أي لغة أخرى على وجه الأرض.
Brendan Eich، مخترع JavaScript، استلهم إلهاماً مباشرةً من Self، وكثير من دلالات JavaScript قائمة على النماذج الأولية. فلكل كائن مجموعة اعتباطية من الخصائص، حقول و«دوال» معاً (وفي الحقيقة دوال مخزّنة كحقول). ويمكن أن يملك الكائن أيضاً كائناً آخر يُسمّى «نموذجه الأولي»، ويفوّض إليه إذا فشل الوصول إلى حقل.
لكن رغم ذلك، أعتقد أن JavaScript عملياً تشترك في أمور أكثر مع اللغات القائمة على الأصناف مما تشتركه مع اللغات القائمة على النماذج الأولية. ومن الدلائل على أن JavaScript اتجهت بعيداً عن Self أن العملية الأساسية في اللغات القائمة على النماذج الأولية، وهي الاستنساخ، لا وجود لها في الأفق.
فلا توجد في JavaScript دالة لاستنساخ كائن. وأقرب ما فيها هو Object.create() التي
تتيح لك إنشاء كائن جديد يفوّض إلى كائن قائم. وحتى هذه لم تُضَف إلا في ECMAScript 5،
أي بعد أربعة عشر عاماً من صدور JavaScript. وبدلاً من الاستنساخ، دعني أمشي بك عبر
الطريقة المعتادة التي تعرّف بها الأنواع وتنشئ الكائنات في JavaScript. فتبدأ بدالة مُنشئة
(constructor):
:::javascript
function Weapon(range, damage) {
this.range = range;
this.damage = damage;
}
وهذا ينشئ كائناً جديداً ويهيّئ حقوله. وتستدعيه هكذا:
:::javascript
var sword = new Weapon(10, 16);
أما كلمة new هنا فتستدعي جسم دالة Weapon() مع ربط this بكائن فارغ جديد. ويضيف
الجسم إليه مجموعة من الحقول، ثم يُعاد الكائن الممتلئ الآن تلقائياً.
وتفعل new شيئاً آخر أيضاً. فعند إنشاء ذلك الكائن الفارغ، تربطه بحيث يفوّض إلى كائن
النموذج الأولي. ويمكنك الوصول إلى ذلك الكائن مباشرةً باستخدام Weapon.prototype.
وبينما تُضاف الحالة في جسم الدالة المُنشئة، فإنك لتعريف السلوك تضيف عادةً دوال إلى كائن النموذج الأولي. شيء كهذا:
:::javascript
Weapon.prototype.attack = function(target) {
if (distanceTo(target) > this.range) {
console.log("Out of range!");
} else {
target.health -= this.damage;
}
}
وهذا يضيف خاصية attack إلى نموذج السلاح وقيمتها دالة. وبما أن كل كائن يُعاد من
new Weapon() يفوّض إلى Weapon.prototype، فصار بإمكانك الآن استدعاء sword.attack()
فتستدعي تلك الدالة. ويبدو الأمر قليلاً كهذا:
لنراجع:
-
الطريقة التي تنشئ بها الكائنات هي عملية «new» تستدعيها باستخدام كائن يمثّل النوع — أي الدالة المُنشئة.
-
تُخزَّن الحالة على النسخة نفسها.
-
يمرّ السلوك عبر مستوى من التوجيه — التفويض إلى النموذج الأولي — ويُخزَّن على كائن منفصل يمثّل مجموعة الدوال المشتركة بين كل كائنات نوع معيّن.
ادعُني مجنوناً، لكن هذا يشبه إلى حدّ كبير وصفي للأصناف في وقت سابق. يمكنك أن تكتب شيفرة بأسلوب النماذج الأولية في JavaScript (من دون استنساخ)، لكن صيغة اللغة وعاداتها تشجّع على المقاربة القائمة على الأصناف.
شخصياً، أعتقد أن هذا أمر جيد. فكما قلت، أجد أن المضي قدماً فيعتمد على النماذج الأولية يجعل الشيفرة أصعب في التعامل، لذا أحبّ أنغلّف JavaScript دلالاتها الجوهرية في شيءٍ أكثر أناقة.
النماذج الأولية لنمذجة البيانات
حسناً، ما زلت أتحدث عن أشياء لا أحبّ استخدام النماذج الأولية فيها، وهذا يجعل هذا الفصل محبطاً حقاً. فأنا أرى هذا الكتاب كوميديا أكثر منه مأساة، لذا لنختم بمجال أعتقد فيه أن النماذج الأولية — أو بتحديد التفويض — يمكن أن تكون مفيدة.
ولو عددتَ كل البايتات في لعبة ما لتعرف أيّها شيفرة وأيّها بيانات، لكنتَ سترى أن نسبة البيانات قد ارتفعت باستمرار منذ فجر البرمجة. فكانت الألعاب الأولى تولّد كل شيء إجرائياً لتتسع على الأقراص المرنة وبطاقات الألعاب القديمة. وفي كثير من ألعاب اليوم، الشيفرة ليست سوى «محرّك» يقود اللعبة، وهي معرَّفة كلياً في البيانات.
لكن هذا رائع، لكن دفع أكوام المحتوى إلى ملفات بيانات لا يحلّ سحراً مشكلات التنظيم في مشروع كبير. بل يجعلها أصعب إن صحّ شيء. فالسبب في استخدامنا لغات البرمجة أنها تملك أدوات لإدارة التعقيد.
وبدلاً من نسخ جزء من الشيفرة ولصقه في عشرة أماكن، ننقله إلى دالة يمكننا استدعاؤها بالاسم. وبدلاً من نسخ دالة في مجموعة من الأصناف، يمكننا وضعها في صنف منفصل ترث منه تلك الأصناف أو تدمجه فيها.
وحين يبلغ حجم بيانات لعبتك حدّاً معيّناً، تبدأ فعلاً في الرغبة في ميزات متشابهة. فنمذجة البيانات موضوع عميق لا أملّ في أن أؤديه حقه هنا، لكنني أريد أن أطرح عليك ميزة واحدة لتنظر فيها في ألعابك: استخدام النماذج الأولية والتفويض لإعادة استخدام البيانات.
لنفترض أننا نعرّف نموذج البيانات للعبة النسخة المسروقة من Gauntlet التي ذكرتها سابقاً. فمصمّمو اللعبة يحتاجون إلى تحديد خصائص الوحوش والغنائم في ملفات من نوع ما.
من المقاربات الشائعة استخدام JSON. فكيانات البيانات هي في الأساس خرائط أو حقائب خصائص أو أي من عشرات المصطلحات الأخرى، لأن لا شيء يحبّه المبرمجون أكثر من اختراع اسم جديد لشيء له اسم بالفعل.
فقد يُعرَّف الجوبلين في اللعبة هكذا:
:::json
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
وهذا بسيط إلى حدٍّ ما، وحتى أكثر المصمّمين نفوراً من النصوص يستطيع التعامل معه. فتقذف بضع فروع شقيقة على شجرة عائلة الجوبلين العظيمة:
:::json
{
"name": "goblin wizard",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"attacks": ["short bow"]
}
والآن، لو كانت هذه شيفرة، لكان ذوقنا الأنيق يطنّ. فهناك تكرار كبير بين هذه الكيانات، والمبرمجون المدرَّبون على تجنّب التكرار يكرهون ذلك. فهو يهدر المساحة ويستغرق وقتاً أطول في التأليف. فعليك أن تقرأ بعناية لتعرف ما إذا كانت البيانات متطابقة أصلاً. فهو صداع صيانة. وإذا قررنا أن نجعل كل الجوبلين في اللعبة أقوى، فعلينا أن نتذكّر تحديث صحّة الثلاثة جميعاً. سيّئ سيّئ سيّئ.
ولو كانت هذه شيفرة، لآنشأنا تجريداً لـ«الجوبلين» وأعدنا استخدامه على أنواع الجوبلين الثلاثة. لكن JSON الساذج لا يعرف شيئاً عن ذلك. فدعنا نجعله أذكى قليلاً.
سنُعلن أن الكائن، إن كان يملك حقلاً "prototype"، فإن ذلك يعرّف
اسم كائن آخر يفضّله هذا الكائن. وأي خصائص لا توجد على الكائن الأول تُبحث على النموذج
الأولي بدلاً من ذلك.
وبهذا يمكننا تبسيط JSON لجماعتنا من الجوبلين:
:::json
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
{
"name": "goblin wizard",
"prototype": "goblin grunt",
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"prototype": "goblin grunt",
"attacks": ["short bow"]
}
ولأن الرامي والساحر لديهما الجوبلين العادي كنموذج أولي، فلا نحتاج إلى تكرار الصحّة والمقاومة والضعف في كل منهما. والمنطق الذي أضفناه إلى نموذج البيانات بسيط جداً — تفويض بسيط منفرد — لكننا تخلّصنا بالفعل من قدر كبير من التكرار.
ومن الأشياء اللافتة للانتباه هنا أننا لم نجهّز نموذجاً أولياً مجرّداً رابعاً باسم «جوبلين أساسي» تفوّض إليه أنواع الجوبلين الثلاثة الملموسة. وبدلاً من ذلك، اخترنا واحداً من الجوبلين كان الأبسط وفوّضنا إليه.
وهذا يبدو طبيعياً في نظام قائم على النماذج الأولية حيث يمكن استخدام أي كائن كنسخة أساس لإنشاء كائنات جديدة ومطوَّرة، وأعتقد أنه يبدو طبيعياً هنا أيضاً. وهو ملائم على وجه الخصوص لبيانات الألعاب التي تضم كيانات خاصة فريدة في عالم اللعبة.
فكّر في الزعماء والغنائم الفريدة. فغالباً ما تكون هذه تحسينات لكائن أشيع في اللعبة، والتفويض عبر النماذج الأولية ملائم لتعريفها. فسيف «قطع الرؤوس» السحري، وهو في الحقيقة مجرد سيف طويل مع بعض المزايا، يمكن التعبير عنه هكذا مباشرةً:
:::json
{
"name": "Sword of Head-Detaching",
"prototype": "longsword",
"damageBonus": "20"
}
ومن ثمّ فإن القليل من القوة الإضافية في نظام نمذجة البيانات في محرّك لعبتك يمكن أن يجعل من الأسهل للمصمّمين إضافة كثير من التنويعات الصغيرة إلى الأسلحة والمخلوقات التي تملأ عالم لعبتك، وهذا الثراء بالضبط هو ما يُبهر اللاعبين.