تجاوز العقبات باستخدام واجهة برمجة التطبيقات Gamepad

مقدمة

Let the novices keep their keyboards for adventure games, their precious multi-touchy-feely fingertips for fruit cutting, and their fancy newfangled motion sensors for pretending they can dance like Michael Jackson. (خبر عاجل: لا يمكنهم ذلك). لكنّك مختلف. أنت أفضل. أنت محترف. بالنسبة إليك، تبدأ الألعاب وتنتهي بجهاز تحكّم في يديك.

ولكن مهلاً. ألا يمكنك استخدام جهاز تحكّم في الألعاب في تطبيق الويب؟ ليس بعد الآن. تأتي واجهة برمجة التطبيقات Gamepad API الجديدة كليًا لإنقاذ الموقف، ما يتيح لك استخدام JavaScript لقراءة حالة أي جهاز تحكّم في الألعاب متصل بالكمبيوتر. إنّها ميزة جديدة جدًا، فقد تم طرحها في الإصدار 21 من Chrome الأسبوع الماضي، وهي على وشك أن تصبح متاحة في Firefox (تتوفّر حاليًا في إصدار خاص).

وقد كان ذلك توقيتًا رائعًا، لأنّنا حصلنا على فرصة لاستخدامها مؤخرًا في رسومات شعار Google المبتكرة لسباق الحواجز 2012. ستشرح هذه المقالة بإيجاز كيف أضفنا Gamepad API إلى رسومات الشعار المبتكرة، وما تعلّمناه خلال هذه العملية.

رسومات شعار Google المبتكرة لسباق الحواجز 2012
رسومات شعار Google المبتكرة لسباق الحواجز لعام 2012

اختبار جهاز التحكّم في الألعاب

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

ما هي المتصفحات التي تتيح استخدامها اليوم؟

Browser Support

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 10.1.

Source

ما هي أجهزة التحكّم في الألعاب التي يمكن استخدامها؟

بشكل عام، من المفترض أن يعمل أي جهاز تحكّم حديث في الألعاب يتوافق مع نظامك بشكل أساسي. لقد اختبرنا العديد من أجهزة التحكّم في الألعاب، بدءًا من أجهزة التحكّم التي تتصل عبر USB من علامات تجارية غير معروفة على جهاز كمبيوتر شخصي، مرورًا بأجهزة التحكّم في الألعاب PlayStation 2 المتصلة عبر وحدة حماية بجهاز Mac، وصولاً إلى أجهزة التحكّم التي تتصل عبر البلوتوث والمقترنة بجهاز كمبيوتر محمول يعمل بنظام التشغيل Chrome.

أجهزة التحكّم في الألعاب
أجهزة التحكّم في الألعاب

هذه صورة لبعض وحدات التحكّم التي استخدمناها لاختبار رسومات الشعار المبتكرة. "نعم يا أمي، هذا ما أفعله في العمل". إذا لم تعمل وحدة التحكّم أو إذا تم ربط عناصر التحكّم بشكل غير صحيح، يُرجى الإبلاغ عن خطأ في Chrome أو Firefox . (يُرجى إجراء الاختبار في أحدث إصدار من كل متصفّح للتأكّد من أنّه لم يتم إصلاح المشكلة).

رصد ميزة Gamepad API<

في متصفّح Chrome:

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

يبدو أنّه لا يمكن رصد ذلك في Firefox حتى الآن، فكل شيء يستند إلى الأحداث، ويجب ربط جميع معالِجات الأحداث بالنافذة، ما يمنع عمل أسلوب نموذجي لرصد معالِجات الأحداث.

ولكننا متأكدون من أنّ هذا الوضع مؤقت. توفّر لك أداة Modernizr الرائعة معلومات عن Gamepad API، لذا ننصحك باستخدامها لتلبية جميع احتياجاتك الحالية والمستقبلية في ما يتعلّق برصد الأجهزة:

var gamepadSupportAvailable = Modernizr.gamepads;

التعرّف على أجهزة التحكّم في الألعاب المتصلة

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

وبعد تجاوز هذه العقبة، ستواجه المزيد من العقبات.

إنشاء استطلاعات

يتيح تنفيذ Chrome لواجهة برمجة التطبيقات وظيفة navigator.webkitGetGamepads() التي يمكنك استخدامها للحصول على قائمة بجميع أجهزة التحكّم في الألعاب المتصلة حاليًا بالنظام، بالإضافة إلى حالتها الحالية (الأزرار وعصي التحكّم). سيتم عرض أول جهاز تحكّم في الألعاب متصل كأول إدخال في الصفيف، وهكذا.

(حلّ استدعاء الدالة هذا مؤخرًا محل مصفوفة يمكنك الوصول إليها مباشرةً، وهي navigator.webkitGamepads[]. اعتبارًا من أوائل أغسطس 2012، لا يزال الوصول إلى هذه المصفوفة ضروريًا في Chrome 21، بينما يعمل استدعاء الدالة في Chrome 22 والإصدارات الأحدث. من الآن فصاعدًا، يُنصح باستخدام استدعاء الدالة للاستفادة من واجهة برمجة التطبيقات، وسيتم طرحه تدريجيًا في جميع متصفّحات Chrome المثبّتة).

يتطلب الجزء الذي تم تنفيذه حتى الآن من المواصفات التحقّق باستمرار من حالة أجهزة التحكّم في الألعاب المتصلة (ومقارنتها بالحالة السابقة إذا لزم الأمر)، بدلاً من تنشيط الأحداث عند تغيُّر الحالة. اعتمدنا على requestAnimationFrame() لإعداد الاستطلاع بأكثر الطرق كفاءةً وملاءمةً للبطارية. بالنسبة إلى رسومات الشعار المبتكرة، على الرغم من أنّ لدينا حلقة تكرار requestAnimationFrame() متوافقة مع الصور المتحركة، أنشأنا حلقة ثانية منفصلة تمامًا، لأنّها كانت أسهل في الترميز ولن تؤثر في الأداء بأي شكل من الأشكال.

في ما يلي الرمز الذي تلقّيته من المختبِر:

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

إذا كنت تهتم بجهاز تحكّم واحد فقط، فقد يكون الحصول على بياناته بسيطًا على النحو التالي:

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

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

الفعاليات

يستخدم Firefox طريقة بديلة وأفضل موضّحة في مواصفات Gamepad API. بدلاً من طلب إجراء استطلاع، يعرض حدثَين، هما MozGamepadConnected وMozGamepadDisconnected، يتم تشغيلهما كلما تم توصيل جهاز تحكّم في الألعاب (أو بشكل أكثر دقة، توصيله و "الإعلان عنه" بالضغط على أي من أزراره) أو فصله. يتم تمرير عنصر جهاز التحكّم في الألعاب الذي سيستمر في عكس حالته المستقبلية كمَعلمة .gamepad لعنصر الحدث.

من الرمز المصدر للمختبِر:

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

ملخّص

في النهاية، تبدو دالة الإعداد في أداة الاختبار، والتي تدعم كلا النهجين، على النحو التالي:

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

معلومات عن جهاز التحكّم في الألعاب

سيتم تمثيل كل جهاز تحكّم في الألعاب متصل بالنظام بكائن يشبه ما يلي:

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

المعلومات الأساسية

الحقول القليلة الأولى هي بيانات وصفية بسيطة:

  • ‫id: وصف نصي لجهاز التحكّم في الألعاب
  • ‫index: عدد صحيح مفيد للتمييز بين أجهزة التحكّم المختلفة في الألعاب المتصلة بجهاز كمبيوتر واحد
  • ‫timestamp: الطابع الزمني لآخر تحديث لحالة الزر/المحاور (متوفّر حاليًا في Chrome فقط)

الأزرار وعصي التحكّم

تختلف أجهزة التحكّم في الألعاب اليوم عن تلك التي استخدمها أجدادنا لإنقاذ الأميرة في القلعة الخطأ، فهي تحتوي عادةً على ستة عشر زرًا منفصلاً على الأقل (بعضها منفصل وبعضها تناظري)، بالإضافة إلى عصاَي تحكّم تناظريتَين. ستخبرك واجهة برمجة التطبيقات Gamepad API بجميع الأزرار وعصي التحكّم التناظرية التي يبلغ عنها نظام التشغيل.

بعد الحصول على الحالة الحالية في عنصر جهاز التحكّم في الألعاب، يمكنك الوصول إلى الأزرار من خلال مصفوفات .buttons[] وعصي التحكّم من خلال مصفوفات .axes[]. في ما يلي ملخّص مرئي لما يتوافق معها:

مخطّط بياني لجهاز التحكّم في الألعاب
مخطّط بياني لجهاز التحكّم في الألعاب

تطلب المواصفات من المتصفّح ربط أول ستة عشر زرًا وأربعة محاور بما يلي:

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

ستتم إضافة الأزرار والمحاور الإضافية إلى الأزرار والمحاور أعلاه. يُرجى العِلم أنّ توفُّر ستة عشر زرًا أو أربعة محاور ليس مضمونًا، لذا عليك الاستعداد لعدم تحديد بعضها.

يمكن أن تأخذ الأزرار قيمًا من 0.0 (غير مضغوط) إلى 1.0 (مضغوط بالكامل). تتراوح المحاور من ‎-1.0 (أقصى اليسار أو الأعلى) إلى 0.0 (الوسط) إلى 1.0 (أقصى اليمين أو الأسفل).

هل هي تناظرية أم منفصلة؟

ظاهريًا، يمكن أن يكون كل زر تناظريًا، وهذا أمر شائع إلى حدّ ما في أزرار الكتف، على سبيل المثال. لذلك، من الأفضل ضبط حدّ معيّن بدلاً من مقارنته بشكل مباشر بالقيمة 1.00 (ماذا لو كان الزر التناظري متسخًا قليلاً؟ وقد لا يصل أبدًا إلى 1.00). في رسمة Doodle، نستخدم الطريقة التالية:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

ويمكنك إجراء الشيء نفسه لتحويل عصي التحكّم التناظرية إلى عصي تحكّم رقمية. بالتأكيد، هناك دائمًا لوحة التحكم الرقمية (d-pad)، ولكن قد لا يحتوي جهاز التحكّم في الألعاب على واحدة. إليك الرمز البرمجي الذي يعالج هذه المشكلة:

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

الضغط على الأزرار وتحريك عصا التحكّم

الفعاليات

في بعض الحالات، مثل لعبة محاكاة الطيران، يكون من المنطقي التحقّق باستمرار من مواضع عصا التحكّم أو الضغط على الأزرار والتفاعل معها… ولكن ماذا عن ألعاب مثل لعبة Doodle "سباق الحواجز 2012"؟ قد تتساءل: لماذا أحتاج إلى التحقّق من الأزرار في كل إطار؟ لماذا لا يمكنني الحصول على أحداث مثل تلك التي أحصل عليها عند الضغط على مفاتيح لوحة المفاتيح أو الماوس؟

الخبر السار هو أنّه يمكنك ذلك. أما الخبر السيئ، فهو أنّ ذلك سيحدث في المستقبل. وهي متوفّرة في المواصفات، ولكن لم يتم تنفيذها في أي متصفّح بعد.

إنشاء استطلاعات

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

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

نهج لوحة المفاتيح أولاً في لعبة Doodle لسباق الحواجز لعام 2012

بما أنّ لوحة المفاتيح هي طريقة الإدخال المفضّلة في Doodle اليوم في حال عدم توفّر جهاز تحكّم في الألعاب، قرّرنا أن يحاكي جهاز التحكّم في الألعاب لوحة المفاتيح بشكل وثيق. وهذا يعني ثلاثة قرارات:

  1. يحتاج رسم الشعار المبتكر إلى ثلاثة أزرار فقط، اثنان للجري وواحد للقفز، ولكن من المرجّح أن يحتوي جهاز التحكّم في الألعاب على عدد أكبر من الأزرار. لذلك، قمنا بتعيين جميع الأزرار الستة عشر المعروفة والعصوين المعروفتين على هذه الوظائف المنطقية الثلاث بطريقة اعتقدنا أنها الأكثر منطقية، بحيث يمكن للأشخاص الركض عن طريق: تبديل أزرار A/B، أو تبديل أزرار الكتف، أو الضغط على اليسار/اليمين على لوحة التحكم، أو أرجحة أي من العصوين بعنف يسارًا ويمينًا (بعضها، بالطبع، سيكون أكثر كفاءة من الآخرين). على سبيل المثال:

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. لقد تعاملنا مع كل إدخال تناظري على أنّه إدخال منفصل، وذلك باستخدام دوال الحدّ الأدنى الموضّحة سابقًا.

  3. لقد وصلنا إلى حدّ ربط إدخال جهاز التحكّم في الألعاب برسومات الشعار المبتكرة، بدلاً من دمجه فيها، إذ تعمل حلقة الاستطلاع على تجميع أحداث keydown وkeyup اللازمة (باستخدام رمز keyCode مناسب) وإرسالها مرة أخرى إلى DOM:

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

هذا كل ما في الأمر.

نصائح

  • يُرجى العِلم أنّ جهاز التحكّم في الألعاب لن يظهر في المتصفّح على الإطلاق قبل الضغط على أحد الأزرار.
  • إذا كنت تختبر جهاز التحكّم في الألعاب في متصفّحات مختلفة في الوقت نفسه، يُرجى العِلم أنّ متصفّحًا واحدًا فقط يمكنه رصد وحدة التحكّم. إذا لم تكن تتلقّى أي أحداث، تأكَّد من إغلاق الصفحات الأخرى التي قد تكون تستخدمه. بالإضافة إلى ذلك، من واقع خبرتنا، يمكن للمتصفّح أحيانًا "الاحتفاظ" بجهاز التحكّم في الألعاب حتى إذا أغلقت علامة التبويب أو أغلقت المتصفّح نفسه. في بعض الأحيان، تكون إعادة تشغيل النظام هي الطريقة الوحيدة لحلّ المشاكل.
  • وكما هو الحال دائمًا، استخدِم Chrome Canary وما يعادله في المتصفحات الأخرى للتأكّد من حصولك على أفضل دعم، ثم اتّخِذ الإجراء المناسب إذا لاحظت أنّ الإصدارات القديمة تعمل بشكل مختلف.

المستقبل

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

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

ولكن قبل ذلك، يمكنك تجربة رسومات الشعار المبتكرة للعبة سباق الحواجز لعام 2012 ومعرفة مدى المتعة التي يوفّرها جهاز التحكّم في الألعاب. هل قلت للتو أنك تستطيع تحقيق رقم أفضل من 10.7 ثوانٍ؟ أنا مستعد.

محتوى إضافي للقراءة