مثال عمل animation في SDL مع شبه مقدمة الى SDL

الناقل : elmasry | الكاتب الأصلى : hasan_aljudy | المصدر : www.arabteam2000-forum.com

بسم الله الرحمن الرحيم
ساقوم بإذن الله باستخدام SDL لعرض مثال بسيط لعمل animation ثنائي الأبعاد باستخدام لغة D. لن اتطرق الى تفاصيل اعداد و تحميل مكتبة SDL او طريقة استخدامها مع D, بل ساطرح الفكرة الرئيسية و اللتي اظن انها يمكن ان تطبق في C++‎ و حتى المكتبات الأخرى غير SDL.
(يعني اذا اردت التطبيق فأنت مجبر ان تقوم بأكثر من مجرد copy-paste :P)
اولا, لا بد من عمل initialization للـ SDL و النافذة, و اعداد الـ program loop او الـ game loop.

import derelict.sdl.sdl;
import derelict.sdl.image;

void init()
{
        //derelict initialization/loading
        //(You don't need this if you're using C++)
        DerelictSDL.load();
        DerelictSDLImage.load();

        //sdl initialization
        SDL_Init( SDL_INIT_EVERYTHING );

        //set some attributes
        //I think it will work even without these
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
        SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

        //create the window
        screen = SDL_SetVideoMode( 640, 480, 32, SDL_SWSURFACE );      
        //set the window title
        SDL_WM_SetCaption( "SDL Experiments!", null );
}

SDL_Surface * screen;

void main()
{
        init();
        SDL_Event event;
       
        mainLoop:
        while( true )
        {
                if( SDL_PollEvent( &event ) )
                {
                        if( event.type == SDL_QUIT )
                        {
                                break mainLoop;
                        }
                }              
        }      
}


في هذه المرحلة لن يحدث شيء سوى ظهور نافذة سوداء.
اول شي فعلناه هو الـ initialization, عن طريق SDL_Init
هناك ايضا initialization خاصة بـ Derelict و هذه الخطوة خاصة فقط بلغة D, فإذا كنت تستعمل C++‎ فلن تحتاج لذلك.
السبب في حاجتنا لها هي طريقة عمل Derelict, و هو امر لن اتطرق اليه هنا.
هناك ايضا عدة استدعائات لـ SDL_GL_SetAttribute و هي فقط لإعداد بعض القيم الخاصة بالفيديو .. و لكني اعتقد انها ليست ضرورية تماما. يمكنكم ان تجربوا عدم استدعائها و رؤية النتائج.
الخطوة الثانية هي انشاء surface للنافذة و استدعاء SDL_SetVideoMode لإنشائها.
دعوني اوضح قليلا .. الـ surface يعني حرفيا سطح .. و لكنه في sdl يعني صورة, او مكان لتخزين الصور.
النافذة اللتي سنفتحها لا بد لها من surface, هذا السطح يعتبر بمثابة الـ frame buffer الخاص بالبرنامج, أو هكذا يمكن التفكير فيها.
SDL_SetVideoMode تقوم بإنشاء النافذة و تعطينا مؤشر للـ surface الخاص بها. يعني اذا اردنا رسم شيء على الشاشة نقوم برسمه على الـ surface الخاص بنافذة البرنامج.
الخطوة الثالثة هي الدخول في حلقة لا نهائية .. يمكن تسميتها main loop او program loop. معالجة الـ events يتم من خلال استدعاء SDL_PollEvent و اللتي تقوم بفحص الـ events, و في حالة وجود event جديد فإنها تقوم بوضعه داخل الـ parameter المرسل لها. عندما يضغط المستخدم على زر اغلاق النافذة, فإن الـ event اللذي سيحدث هو SDL_QUIT.

هذا كل شيء كبداية, اعتقد انها ابسط كثيرا من glut فضلا عن win32 api.
في الخطوة الثانية سنقوم بتحميل صورة و عرضها في منتصف الشاشة .. و سأقوم ايضا بتحديث الشاشة باستمرار داخل الـ main loop.
اولا نقوم بإنشاء مجلد باسم معين مثل images و نضع داخله الصورة اللتي نحملها, هذا افضل من ناحية تنظيمية.
بالنسبة لي, وضعت صورة لـ kaito kid مأخوذة من دعاية لاحد افلام المحقق كونان, بإمكانكم بالطبع اختيار أي صورة.
سنقوم أولا بكتابة الكود بطريقة مسبطة, فقط من اجل توضيح بعض الأشياء. يعني سوف استخدم global variables و ما إلى ذلك.
نقوم بإنشاء surface جديد من اجل تحميل او تخزين محتويات الصورة في الذاكرة, ثم نقوم بتحميل الصورة من ملف bmp. و نضيف شيئ جديد داخل الـ main loop من أجل رسم الصورة.
لنرى كيف حدث ذلك, دعوني اريكم اولا التغييرات اللتي اجريتها على الـ main
void main()
{
        init();
        loadImages();
        SDL_Event event;

        mainLoop:
        while( true )
        {
                drawImages();

                if( SDL_PollEvent( &event ) )
                {
                        if( event.type == SDL_QUIT )
                        {
                                break mainLoop;
                        }
                }      
        }      
}



نلاحظ وجود استدعائين جديدين, الأول هو loadImages و يقوم يتحميل الصورة و وضعها في الـ surface الجديد .. لا تنسوا انني عرفته كـ global variable.
و الأمر الثاني هو drawImages و قد وضعته داخل الـ main loop. و هذا الأمر يقوم برسم الصورة على الـ screen surface, و سبب وضعه داخل الـ loop هو ان تبقى الصورة داخل النافذة حتى لو حركنا النافذة او غيرنا شكلها .. الخ, و ايضا لأننا سنكرر عمل نفس الشي عند عرض الـ animation بإذن الله.
بالنسبة للـ loadImages فقد قمت باستخدام SDL_LoadBitmap و هي عملية موجودة ضمن SDL و تقوم بتحميل الصورة من ملف bmp ووضعه على شكل surface. و لكن هناك مشكلة صغيرة, و هي ان هيئة الصورة قد تكون مختلفة عن هيئة الـ surface الخاص بالشاشة. لذلك, نقوم بعد تحميل الصورة يتغيير هيئتها لكي تكون متطابقة مع هيئة الشاشة, و ذلك عن طريق SDL_DisplayFormat.

SDL_Surface * image;

void loadImages()
{
        SDL_Surface * temp = SDL_LoadBMP("images/kaitokid.bmp");
        image = SDL_DisplayFormat( temp );
        SDL_FreeSurface( temp );
}


لاحظوا انني استخدمت surface مؤقت و قمت بتحريره بعد الانتهاء من استخدامه, لأنني لو لم افعل ذلك فإنني سوف اضيع اجزاء من الذاكرة .. و لو كنت اقوم بتحميل العديد من الصور بنفس الطريقة من دون التخلص من الـ temp surface فسيحصل عندنا memory leak بسرعة.
أما بالنسبة لـ drawImages, فهي تقوم بعرض الصورة على الشاشة .. او بالأحرى على النافذة.
كيف نقوم بذلك؟
سبق و قلت أن النافذة عندها surface خاص بها, و في حالتنا هذه فهذا الـ surface هو المتغير screen. هل تذكرون لماذا؟ لأن هذا الـ surface نحصل عليه من الدالة SDL_SetVideoMode.
إذن, لكي نرسم اي شي على الشاشة, علينا اولا ان نرسم هذا الشي على screen عن طريق SDL_BlitSurface و بعد ذلك نقوم باستدعاء SDL_Flip.

void drawImages()
{
        SDL_Rect offset;
        offset.x = 100;
        offset.y = 100;
        SDL_BlitSurface( image, null, screen, &offset );
        SDL_Flip( screen );
}


الدالة SDL_BlitSurface تقوم بنسخ محتويات surface الى surface آخر, او صورة الى صورة أخرى. البارامتر الأول هو الصورة اللتي تريد النسخ منها, البارامتر الثاني لا ادري ما هو بصراحة .. دعه NULL (في لغة D نكتب null بالحروف الصغيرة), البارامتر الثالث هو الصورة اللتي تريد النسخ اليها, و البارامتر الرابع هو المكان اللذي تريد النسخ اليه .. او مقدار الإزاحة اللتي ستنسخ اليها. اعتقد انه واضح ولا يحتاج شرح, اليس كذلك؟
أما SDL_Flip فإعتقد كما هو واضح .. ان SDL يستخدم double buffering, لذلك لن تظهر الصورة الموجودة في المتغير screen قبل ان نستدعي عليه SDL_Flip.
و الآن, الكود كاملا:
import derelict.sdl.sdl;
import derelict.sdl.image;

void init()
{
        //derelict initialization/loading
        //(You don't need this if you're using C++)
        DerelictSDL.load();
        DerelictSDLImage.load();

        //sdl initialization
        SDL_Init( SDL_INIT_EVERYTHING );

        //set some attributes
        //I think it will work even without these
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
        SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

        //create the window
        screen = SDL_SetVideoMode( 640, 480, 32, SDL_SWSURFACE );      
        //set the window title
        SDL_WM_SetCaption( "SDL Experiments!", null );
}

SDL_Surface * screen;

SDL_Surface * image;

void loadImages()
{
        SDL_Surface * temp = SDL_LoadBMP("images/kaitokid.bmp");
        image = SDL_DisplayFormat( temp );
        SDL_FreeSurface( temp );
}

void drawImages()
{
        SDL_Rect offset;
        offset.x = 100;
        offset.y = 100;
        SDL_BlitSurface( image, null, screen, &offset );
        SDL_Flip( screen );
}

void main()
{
        init();
        loadImages();
        SDL_Event event;

        mainLoop:
        while( true )
        {
                drawImages();

                if( SDL_PollEvent( &event ) )
                {
                        if( event.type == SDL_QUIT )
                        {
                                break mainLoop;
                        }
                }      
        }      
}

النتيجة كما تظهر عندي:

Posted Image


طيب, نحن الآن نعرف كيف نقوم بإنشاء نافذة في SDL و كذلك كيف نقوم بتحميل صورة bmp و عرضها.
نريد الأن تطوير مهاراتنا قليلا و تحميل عدة صورة ثم جعلها تظهر على شكل animation. فكيف نقوم بذلك؟
أولا, نقوم بتحميل عدة صور, و نضعها مثلا في مصفوفة. و نقوم بتغيير الدالة اللتي تعرض الصورة بحيث تقوم بعرض صورة مختلفة في كل مرة, او بالأحرى, نقوم بتغيير الصورة اللتي نعرضها باستمرار لكي نحصل على تأثير و كأن الصورة تتحرك.
من اجل التنظيم, نقوم بوضع هذه الصور في مجلد images اللذي انشأناه في الخطوة السابقة. و نضع عدة صور من اجل ان نقوم بتكوين الـ animation منها. بالنسبة لي فقد استخدمت مجموعة صور ايضا لـ kaitokid و هي في الحقيقة نفس اللقطة .. و لكني تلاعبت بها قليلا لجعلها تبدو متكررة .. على كل حال هذا ليس موضوعنا. المهم هو ان تكون لديك مجموعة من الصور معدة مسبقا, و سنقوم بتحميلها واحدا وراء الآخر, لا يهم كيف, المهم ان نحملها كلها, حتى لو بشكل يدوي manual. بالنسبة لي فقد رقمت الصور من 0000‎.bmp الى 0019‎.bmp و قمت بعمل for loop لتحميلها, و لكن هذا ليس ضروريا حيث يمكن تحميلها من دون for loop, كما انه لا يجب استخدام 20 صورة .. بل يمكن الاكتفاء بخمس او ست صور. المهم ان نحمل الصور الخاصة بالأنيميشن.

SDL_Surface *[20] images;

void loadImages()
{
        for(int i = 0; i < 20; i++ )
        {
                char[] nextFrame;
                char[] num = toString(i);
                if( i < 10 )
                {
                        nextFrame = "000" ~ num ~ ".bmp";
                }
                else
                {
                        nextFrame = "00" ~ num ~ ".bmp";
                }
                SDL_Surface * temp = SDL_LoadBMP("images/" ~ nextFrame);
                images[i] = SDL_DisplayFormat( temp );
                SDL_FreeSurface( temp );
        }
}



لاحظوا اني قمت بتغيير images بحيث اصبح مصفوفة surfaces.
لاحظوا اني استخدمت لغة D, يعني استفدت من بعض خواصها مثل الـ array concatenation و ما إلى ذلك من الأمور الغير موجودة في الـ C++‎. لاحظوا ايضا انني لا احذف الـ strings بعد استخدامها لأن الـ garbage collector يتولي المهمة. و هنا قد يسأل سائل .. لماذا لا يتولى ايضا مسألة الـ temp surface عند تحميل الصورة؟ الجواب هو ان تحرير الـ surface ليس له علاقة بعملية new و delete بل هو شي خاص بـ SDL .. او هكذا يمكن ان نفكر فيه.
في لفة D فإن تعريف متغير SDL_Surface *[20] images يعني ان images هي مصفوفة مؤشرات الى SDL_Surface ووضع 20 داخل الأقواس يعني ان المصفوفة هي static array و حجمها محدد مسبقا بعشرين عنصر. اعتقد ان الأمر في الـ C++‎ أعقد قليلا .. لا احد يسالني .. فانا لم احاول في حياتي انشاء متغيرات معقدة لهذه الدرجة في الـ C++‎ و لا اريد تخيل ذلك. في الـ D الأمر بسيط, فقط قم بقراءة الـ type من اليمين الى اليسار.
SDL_Surface * [20] images
اقراها من اليمين الى اليسار, تصبح:
array of pointers to SDL_Surface
على كل حال, نأتي الى الخطوة التالية, و هي عرض الصور بشكل متتابع لأجل الحصول على تأثير الحركة.
الحركة هي عبارة عن عدة صور تتناوب على الظهور .. بين كل صورة و أخرى هناك فترة زمنية بسيطة جدا.
نحتاج اولا ان نعرف كم من الوقت مضى منذ ان عرضنا الصورة السابقة, و حين نجد ان فترة كافية قد مرت, نقوم بعرض الصورة اللتي بعدها. هذه الصور تمسى أُطُر (جمع إطار) او frame بالانجليزي.
من اجل الحصول على الوقت, سنستخدم SDL_GetTicks و هي تعطينا الوقت المنقضى على عمل البرنامج بالـ millisecond يعني جزء من الف من الثانية.
من اجل عرض الأنيميشن, نقوم اولا بعرض اول صورة و تسجيل الوقت الحالي, ثم في كل مرة نأتي لعرض الصورة, نقوم بتشييك (فحص) الوقت الحالي و نطرحه من وقت عرض الفريم (الاطار) السابق, اذا تعدى الفرق بينهما مثلا 60 ميليثانية, نقوم بعرض الصورة التالية. و الا نبقى على نفس الصورة الحالية.

int currentFrame = 0;
int prevTime = 0;
void drawImages()
{
        int ticks = SDL_GetTicks();
        if( ticks - prevTime > 60 )
        {
                currentFrame = (currentFrame + 1) % 20;
                prevTime = ticks;
        }
       
        SDL_Rect offset;
        offset.x = 100;
        offset.y = 100;
        SDL_BlitSurface( images[currentFrame], null, screen, &offset );
        SDL_Flip( screen );
}


لاحظوا مرة اخرى انني استخدم global variables و ذلك فقط من اجل توضيح هذا المثال البسيط.
هذه هي كل التغييرات المطلوبة, عند القيام بها سنحصل على انيميشن بسيط.
هذا هو الكود كاملا:
import derelict.sdl.sdl;
import derelict.sdl.image;
import std.string;

void init()
{
        //derelict initialization/loading
        //(You don't need this if you're using C++)
        DerelictSDL.load();
        DerelictSDLImage.load();

        //sdl initialization
        SDL_Init( SDL_INIT_EVERYTHING );

        //set some attributes
        //I think it will work even without these
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
        SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
        SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

        //create the window
        screen = SDL_SetVideoMode( 640, 480, 32, SDL_SWSURFACE );      
        //set the window title
        SDL_WM_SetCaption( "SDL Experiments!", null );
}

SDL_Surface * screen;

SDL_Surface *[20] images;

void loadImages()
{
        for(int i = 0; i < 20; i++ )
        {
                char[] nextFrame;
                char[] num = toString(i);
                if( i < 10 )
                {
                        nextFrame = "000" ~ num ~ ".bmp";
                }
                else
                {
                        nextFrame = "00" ~ num ~ ".bmp";
                }
                SDL_Surface * temp = SDL_LoadBMP("images/" ~ nextFrame);
                images[i] = SDL_DisplayFormat( temp );
                SDL_FreeSurface( temp );
        }
}

int currentFrame = 0;
int prevTime = 0;
void drawImages()
{
        int ticks = SDL_GetTicks();
        if( ticks - prevTime > 60 )
        {
                currentFrame = (currentFrame + 1) % 20;
                prevTime = ticks;
        }
       
        SDL_Rect offset;
        offset.x = 100;
        offset.y = 100;
        SDL_BlitSurface( images[currentFrame], null, screen, &offset );
        SDL_Flip( screen );
}

void main()
{
        init();
        loadImages();
        SDL_Event event;

        mainLoop:
        while( true )
        {
                drawImages();

                if( SDL_PollEvent( &event ) )
                {
                        if( event.type == SDL_QUIT )
                        {
                                break mainLoop;
                        }
                }      
        }      
}


في هذه الحالة فإن التحكم بالـ animation صعب بعض الشيء, و كذلك من الصعب عرض عدة animations في وقت واحد, لذلك من الأفضل اعادة تصميم الكود ووضع الانيميشن في كلاس class. و سنقوم بذلك إن شاء الله في المرة القادمة.

و هنا مثال الـ animation مع exe و كذلك الصور المستخدمة. (مرفق)
 
ملف مرفق(ملفات)
 
ملف مرفق  simple_animation.rar (553.69كيلو )