[برمجة اندرويد] تحريك الرسومات في الألعاب

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

يوفر الاندرويد عنصر للصور (ImageView) لكن ذلك مخصص لعرض الصور الثابتة أو التي لا تتطلب تغييرات سريعة ولا يمكن استخدامه للصور المتحركة لأنه بطيء. للتعامل مع الصور المتحركة نحتاج إلى الرسم مباشرة على canvas (سطح الرسم) لكن توجد طريقة تبسط العملية وهي باستخدام عنصر SurfaceView والذي يمكنك من الرسم المباشر على canvas وبطريقة سهلة.
عملية التحريك تحتاج أن تعمل في الخلفية لكي لا تعطل عمل التطبيق (لفهم عمليات الخلفية راجع الشرح السابق). لتنظيم العمل سنجعل النص الخاص بالحركة في كلاس (class) خاص سنسمية AnimGuy:

public class AnimGuy extends SurfaceView implements Runnable {
....
}

عملنا الكلاس كامتداد لكلاس SurfaceView وكذلك واجهة لكلاس Runnable وذلك لتشغيله في الخلفية كما سنرى لاحقا. في البداية نقوم بتحميل الصورة وهي عبارة عن ملف واحد مرسوم به إطارات متساوية في المقاس تمثل مراحل الحركة للشخصية (خطوات الركض). لدينا في تلك الصورة 6 إطارات فنحتاج عرضها واحد تلو الآخر إلى الأخير ثم نعود للإطار الأول وهكذا. الدالة run() هي التي نستخدمها لتحريك تلك الإطارات وهي تعمل في الخلفية. لنلقي نظرة على تلك الدالة:

    @Override
    public void run() {
        while (animate) {
            if (surfaceHolder.getSurface().isValid()) {
                Canvas canvas = surfaceHolder.lockCanvas();

                // Clear any existing drawing from previous run
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

                x = frame * imgWidth;
                Rect src = new Rect(x, 0, x + imgWidth, imgHeight);
                Rect dst = new Rect(0, 0, imgWidth, imgHeight);
                canvas.drawBitmap(guyImage, src, dst, null);

                // This is a modulus operation to loop through all the frames and restart over
                frame = ++frame % totalFrames;

                surfaceHolder.unlockCanvasAndPost(canvas);
            }

            try {
                Thread.sleep(120);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

نلاحظ أولا أننا نستخدم حلقة while بمتغير ثنائي animation للتحكم باستمرارية الحلقة، ونتحكم بقيمة هذا المتغير كما سنرى لاحقا لبدء ووقف التحريك. في داخل الحلقة نتأكد أولا من جاهزية السطح للرسم isValid() (في بعض الحالات مثل لحظة بدأ التطبيق لا يكون سطح الرسم جاهزاً بعد ومحاولة الرسم عليه ستؤدي إلى فشل التطبيق)، في حال كان جاهزاً نقوم بقفل السطح lockCanvas() لنظمن أن لا احد غير هذه الدالة يستطيع الرسم عليه. بعد ذلك نقوم بمسح محتوى سطح الرسم لكي لا تبقى به رسومات سابقة. نبدأ الآن بالرسم، أولا نحتاج رقم الإطار المراد رسمة وهو محفوظ في المتغير frame وفي البداية فيمته صفر فتكون الإحداثيات على المحور س تساوي صفر أي من أول الصورة. بعد ذلك نقوم بنسخ مساحة مستطيلة Rect من الصورة الأصلية تبدأ من س إلى س+عرض الإطار، أما الارتفاع (محور ص) فهو بكامل ارتفاع الصورة (يبدأ من صفر إلى ارتفاع الصورة). بعد ذلك نحدد المساحة التي سنرسم عليها dst في المثال هذا وللتبسيط جعلنا مساحة سطح الرسم مساوية لمساحة الإطار الواحد، لذلك فإن منطقة الرسم وحجمها يساوي المساحة الكاملة (يبدأ من الصفر إلى طول وعرض الإطار). نقوم الآن بالرسم بالأمر drawBitmap إذ نمرر له الصورة الكاملة ومعها إحداثياتها ومساحة الإطار المراد نسخة وكذلك إحداثياتها ومساحة المنطقة المراد الرسم عليها. نقوم بعدها بزيادة رقم الإطار ليكون جاهزا لعملية الرسم التالية (لحساب رقم الإطار نستخدم طريقة المعامل الرياضي فعدد الإطارات هو 6 فعندما يبلغ العداد ذلك الرقم يعود من جديد للعد من 0). في الأخير نقوم بفك القفل ورسم الإطار unlockCanvasAndPost. للتحكم بسرعة تحريك الإطارات نستخدم Thread.sleep() والتي تجعل عملية الخلفية تنام (أو تتوقف عن العمل) لمدة زمنية تحسب بأجزاء الثانية، فهذه القيمة تعتمد على عدد الإطارات وكذلك السرعة الحركة المناسبة. لرسم الإطار الذي يليه (رقم 1)  نقوم بنفس الخطوات وإحداثيات الإطار تبدأ من نهاية الإطار السابق.

لاستخدام هذا الكلاس نهيئه أولا من الكلاس الرئيسي بالأمر التالي:

animGuy = new AnimGuy(this);

ويتم تشغيل وايقاف الحركة بالأمرين التاليين:

animGuy.resume();
animGuy.pause();

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

للاستفسار عن هذا الشرح أو التطبيق يمكنك السؤال بالتعليق اسفل الصفحة أو مراسلتي على تويتر

مصدر الصور: (الولد) (الخلفية)

Leave a Reply

كن أوّل من يعلّق

نبّهني عن
avatar
‫wpDiscuz