Game Programming بخش دوم

دانلود دمو – 24.16 KB

معرفی

هدف این خود آموز این است که به شما نشان دهد چطور یک بازی ساده بدون کمک API های سطح بالا مثل XNA یا DirectX که نیمی از فرایند ها را اتوماتیک برای شما انجام می دهند ، بسازید . تمام چیزی که ما استفاده می کنیم یک فرم ویندوز و توابع GDI+ است برای ترسیمات پایه ای و تعدادی از Event های فرم .

در این بخش ما درمورد double buffering بحث خواهیم کرد تا اثر پرش تصاویر را از بین ببریم ، یک شمارنده FPS ایجاد می کنیم ، ورودی ها را از بازیکن میگیریم و Sprite هایی را روی صفحه ترسیم می کنیم .

مطلب قبلی >>

قدم 2 : ترسیم یک صحنه

کاهش پرش تصویر

اگر ما از دو thread جدا برای اجرای logic و ترسیم صحنه روی صفحه استفاده کرده باشیم ، ممکن است وقتی آیتم های جدید ترسیم میشوند روی صفحه پرش ببینیم . ممکن است شما این پرش را زمانی که تعداد کنترل های زیادی روی فرم برنامه های کاربردیتان وجود دارد هم دیده باشید . راه حل این مشکل Double Buffering است .

ما قبلا یک بافر داشتیم که همان صفحه نمایش بود . این بافر توسط کارت گرافیک برای آپدیت کردن مانیتور استفاده می شود .اگر مستقیماً در این بافر ترسیمات را انجام دهیم تغییرات را به سرعت مشاهده می کنیم که نتیجه اش همان پرش تصویر است . زمانی که تعداد ترسیمات کم باشد مانند این است که در یک لحظه ترسیمات را انجام داده باشیم و شما متوجه هیچ پرشی نمیشوید ، ولی زمانی که تعداد ترسیمات بالا میرود این اثر واضح دیده میشود .

با double buffering ما 2 بافر داریم یکی Back buffer و دیگری screen . ما تمام ترسیمات را در back buffer انجام می دهیم و در یک مرحله تمام back buffer را در screen رسم می کنیم .این کار اثر پرش را حذف می کند. یک نوع از double buffering وجود دارد به نام Page Flipping . در این روش کارت گرافیک  هر دو بافر را در حافظه VRAM نگه میدارد . یکی از آنها روی مانیتور نمایش داده میشود در حالی که ما روی بافر دیگر ترسیمات را انجام می دهیم . سپس جای بافرها با هم عوض میشود درنتیجه کارت گرافیک buffer1 را نمایش میدهد زمانی که ما داریم روی Buffer2 کار میکنیم ، سپس جای آنها عوض میشود و buffer2 نمایش داده می شود و ما در buffer1 ترسیمات را انجام می دهیم .

Page Flipping همیشه تا Vsync صبر میکند (یعنی تا زمانی که به روز رسانی عمودی به پایان برسد صبر میکند و سپس بافر جدید را رسم می کند ). این باعث محدودیت میزان frame rate میشود و بستگی به refresh rate مانیتور شما دارد – در حالی که صبر کردن برای Vsync چیز بدی نیست چون باعث میشود باز هم سرعت بازی  را کم می کند . مزیت بارز صبر کردن برای کامل شدن vertical refresh این است که دیگر shearing نداریم . shearing زمانی اتفاق می افتد که ما بافر را هنگامی که صفحه در حال refresh شدن است کپی کنیم ، در نتیجه نیمه ی بالایی صفحه از بافر قبلی  استفاده می کند و نیمه ی پایینی تصویر از بافر جدید .

خوب بعد از پیاده سازی double buffering کدهای ما به این صورت میشوند :

 Image buffer;
 GetInput();
 PerformLogic();
 DrawGraphics();
 ...
 DrawGraphics()
 {
 Graphics g = Graphics.FromImage(buffer);
 g.Draw... g.Dispose(); 
//We will then draw 'buffer' to the screen 
} 

هنوز کمی در مورد زمانبندی – شمارنده FPS

چطوره یک شمارنده ساده FPS ایجاد کنیم ؟ تمام کاری که ما باید انجام دهیم این است که تعداد دفعاتی که کد های ترسیم در هر ثانیه اجرا میشوند را بشماریم . پس به یک متغییر نیاز داریم که هر بار که یک فریم را رسم میکنیم افزایش یابد . اگر به کدهای مقاله قبل برگردیم در انتهای آن میتوانیم این کدها را اضافه کنیم :

 Timer MainTimer;
 Timer FpsTimer;
 MainTimer.Interaval = 1000/60;
 FpsTimer.Interval = 1000;
 bool runGame = true;
 volatile uint speedCounter = 0;
 uint fpsCounter = 0;
 uint fps = 0;
 Main() 
{
 while(runGame)
 {
  if(speedCounter >0)
 {
 GetInput();
 PerformLogic();
 speedCounter--;
 if(speedCounter == 0)
 DrawGraphics();
 }
 }
 }
 DrawGraphics()
 {
 ...
 fpsCounter++;
 }
 FpsTimer()
 {
 fps = fpsCounter;
 fpsCounter = 0;
 }
 Timer()
 {
 speedCounter++;
 } 

میبینید ! واقعاً آسونه .

اما چی شد . . .

اگر شمارنده FPS را اضافه کرده باشید خواهید دید که FPS واقعی از آنچه که انتظار دارید خیلی کمتر است . بخاطر این است که تایمرهای استاندارد .NET تنها دقتی در حدود یک هجدهم ثانیه دارند . بهمین دلیل ما باید از تایمرهایی قدیمی استفاده کنیم .، که بسیار دقیق تر هستند .این تایمر در کدهای دموی این مقاله قرار داده شده ، پس لازم نیست نگران آن باشید .

اون دکمه ها رو فشار بده – ورودی های کاربر

ما باید ورودیها را از بازیکن بگیریم . برای این کار از رویدادهای KeyDown و KeyUp استفاده می کنیم . به نظر سادست ولی در نظر داشته باشید که ما باید Logic را فقط در loop اصلی و درست در زمان خودش اجرا کنیم . شما نمی توانید logic را زمانی که کلیدی فشرده شد اجرا کنید . به عنوان مثال اگر بازی شما با سرعت 2FPS اجرا شود و بازیکن باید با سرعت 4px در هر فریم حرکت کند ، پس باید در هر ثانیه 8px حرکت کند . اما اگر ما بازیکن را به محض فشرده شدن کلید حرکت دهیم ، بازیکن میتواند با سرعتی که کلید ها زده میشوند حرکت کند . همینطور اگر از یک رویداد استفاده کنیم نمی توانیم بفهمیم که آیا کلید هنوز نگه داشته شده یا نه ؟

پس ما به راهی برای ذخیره کردن کلیدهایی که فشرده شده اند پیدا کنیم تا در حلقه اصلی هرجا که نیاز بود در logic از آنها استفاده کنیم . ما مینوانیم از یک آرایه bool استفاده کنیم ولی راه ساده تر استفاده از Flag هاست . منظورم از flag این است که از یک int یا long برای این کار استفاده کنیم ، و هر بیت از این متغییرها به معنی یک کلید است . در نتیجه در هر int میتوانیم 32 تا کلید ، و در long تا 64 کلید را نگه داریم .اگر از یک int استفاده کنیم ، بیت اول میتواند کلید ‹Left› و بیت دوم ‹Right› و سومین بیت ‹Up› و غیره باشد . این متغییر int  میتواند کلید های Left و Up را به صورت زیر نگه دارد :

 00000000000000000000000000000101 

پس میتوانیم نحوه ی روشن و خاموش بودن بیتها را در متغییرمان ببینیم و متوجه شویم که دو کلید فشرده شده اند . ما از یک Enumeration برای نگه داری کلیدهای بازی ، و از اپراتورهای بیتی برای اضافه و کم کردن کلیدها استفاده می کنیم . پیاده سازی آن به صورت زیر است :

 [Flags]
 public enum GameKeys : int 
{ Null = 0,
 Up = 0x01,
 Down = 0x02,
 Left = 0x04,
 Right = 0x08
 }
...
 GameKeys pressedKeys = GameKeys.Null;
 ...
 KeyDown(KeyEventArgs e)
 {
 switch(e.KeyCode)
 {
 case Keys.Left: pressedKeys |= GameKeys.Left;
 break;
 ... 
}
 }
 KeyUp(KeyEventArgs e) 
{ 
switch(e.KeyCode)
 {
 case Keys.Left: pressedKeys &= ~GameKeys.Left;
 break;
 ...
 }
} 

ما میتوانیم بعداً به این صورت چک کنیم آیا کلیدی فشرده شده :

 if((pressedKeys&GameKeys.Left) == GameKeys.Left) 

این دستور چک خواهد کرد که آیا کلید Left زده شده یا نه . برای اضافه کردن کلید از OR استفاده می کنیم و برای چک کردن از AND استفاده می کنیم .

تنها یک استثنا وجود دارد که logic را در خود رویداد اجرا می کنیم – آن هم بستن فرم است. در دمو شما خواهید دید که من Escape را برای ست کردت m_playing به false بکار بردم تا مطمئن شوم که thread خاتمه می یابد و سپس متد Application.Exite() برای بستن فرم فراخوانی می شود . بسیار مهم است که مطمئن شویم که از thread قبل از بسته شدن فرم خارج میشویم چون در غیر این صورت حتماً یک  قبل از بسته شدن فرم خارج میشویم چون در غیر این صورت حتماً با یک error مواجه خواهیم شد که اصلاً خوشایند نیست .

برای چی همه ی ما اینجاییم – متحرک سازی یک Sprite

حالا میریم سراغ ترسیم و متحرکسازی یک Sprite .

یک Sprite چیست ؟

در بازی های 2D ، یک sprite یک تصویر است که یک سیء را در بازی نمایش می دهد. پس برای ترسیم یک sprite که هیچ حرکتی ندارد تها کافبست به سادگی یک نصویر یا بخشی از یک تصویر را روی صفحه بکشید .

اگر میخواهید بیگانگان در بازی space invaders را در صفحه بکشید باید یک تصویر را از هارد دیسک بارگیری کنید و از Graphics.DrawImage(…) استفاده کنید تا آنرا در بافرمان بکشیم . اگر از این تصویر استفاده کنیم :

باید مختصات x و y بیگانگان را در تصویر بیابیم و همین طور طول و ارتفاع آنرا . من به شما میگویم که اولین بیگانه در (48 , 89) قرار گرفته و سایز آن 16*16 است . برای رسم آن ما از یکی از متدهای اوورلود شده ی DrawImage استفاده می کنیم :

 Bitmap spaceInvaders = new Bitmap("path to image");
 ...
 DrawGraphics()
 {
 Graphics g = Graphics.FromImage(buffer);
 g.DrawImage(spaceInvaders, new Rectangle(50,50,16,16),
 48, 89, 16, 16, GraphicsUnit.Pixel);
 } 

این کدها بیگانه فضایی کوچک ما را در مختصات (50 , 50) در صفحه رسم میکند . اگر میخواهید ابعاد آنرا تغییر دهید تنها کافیست عرض و ارتفاع مستطیل را تغییر دهید .

این خیلی عالیه ولی کار زیادی انجام نمیده

برای متحرکسازی ، به سادگی ما یک تصویر را میکشیم و ور فریم بعد تصویر بعدی را در دنباله تصاویر رسم می کنیم . برای این کار باید بدانیم که الآن در کدام فریم از انیمیشن هستیم ، انیمیشن چند فریم دارد ،و عرض و ارتفاع یک sprite چقدر است . برای متحرک کردن بیگانه فضایی ما تصویر موجود در (48 , 89) را میکشیم و در فریم بعد (48 , 105) را رسم میکنیم و دوباره از اول تکرار می کنیم.

 int numberOfFrames = 2;
 int currentFrame = 0;
 int height = 16;
 Logic() 
{
 currentFrame++;
 if(currentFrame == numberOfFrames)
 currentFrame = 0;
}
 DrawGraphics()
 {
 Graphics g = Graphics.FromImage(buffer);
 int srcY = 89 + (currentFrame*height);
 g.DrawImage(spaceInvaders, new Rectangle(50,50,16,16),
 48, srcY, 16, 16, GraphicsUnit.Pixel);
 } 

کدها در logic() مقدار currentFrame را افزایش میدهد و زمانی که به حداکثر فریمهای در انیمیشن میرسد دوباره به صفر برمیگرداند . مقدار currentFrame * height میزان اختلاف مکانی محور Yها نسبت به اولین فریم است . در فریم اول currentFrame صفر خواهد بود ؛ 0 * 16 صفر میشود و srcY روی 89 می ماند . در فریم دوم ، currentFrame برابر 1 می شود و 1 * 16 میشود 16  پس به 89 اضافه میشود و محل Y جدید را میدهد .

هنور یه چیزی را ندیده گرفته ایم

ممکن است در بعضی بازی ها دیده باشید که وقتی جهتهای مختلف را انتخاب میکنید ، کاراکتر به سمت آن جهت بر می گردد . پیاده سازی آن همانند متحرکسازی یک sprite است . اگر شما تصویر زیر را داشته باشید :

پس برای تغییر جهت کاراکتر ، بجای تغییرمحل-X برای متحرک سازی ، شما باید محل-Y را تغییر بدهید تا در یک ردیف دیگر قرار بگیرد .

آپدیت کردن سورس کد

خوب حالا که انیمیشن را متوجه شدیم زمان خوبی است که سورس هایمان را به روز کنیم تا از تایمر دقیق ، thread جدا برای logic در آن استفاده کنیم . در دمو شما میتوانید ببینید که یک متد به نام GameLoop وجود دارد که در یک thread دیگر فراخوانی میشود و در آن Loop هایی که در مقاله قبل دیدید وجود دارد . ممکن است متوجه Thread.Sleep(1) شده باشید و این به خاطر این است که بازی از 100% وقت پردازنده استفاده نکند و تا زمانی صبر میکند که تایمر یک سیگنال برای اجرای logic بدهد . همینطور دیگر از OnPaint برای کشیدن صحنه استفاده نمیکنیم چون حالا همه چیز را خودمان انجام میدهیم .

Demo

دموی این مقاله شامل ورودی های کاربر میشود که با آن میتوانید بیگانه ی کوچکمان را در صفحه حرکت دهیم ، یک کلاس sprite برای متحرکسازی و رندر کردن بیگانه ، و یک تایمر جدید برای شمارش تعداد فریمها در یک ثانیه .

پایان !

منبع :

Game-Programming-Two

دربارهٔ DeltaCode

Somewhere near the sky Far away from people Far away from noise Somewhere near yourself

Posted on مارس 18, 2012, in بازی and tagged , , , , . Bookmark the permalink. بیان دیدگاه.

بیان دیدگاه