آموزش بازی سازی: ‌آشنایی با رندرینگ و بهینه‌سازی آن

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

مقاله‌ی مرتبط

به خاطر همین مسئله در بیشتر موارد اولین تغییرات ما در اجزای بازی به یکباره ما را با سیل مشکلات عجیب و غریبی مواجه می‌کند که نه می‌دانیم مربوط به کدام قسمت از بازی ما هستند و نه می‌دانیم که باید چگونه آن‌ها را برطرف کنیم. به همین جهت یکی از اولین گام‌های آموزش عیب یابی و رفع مشکلا بازی صحبت در مورد سازوکار عملکرد موتور ساخت بازی در فرآیند اجرای اجزای آن است. یکی از مهم‌ترین این موارد نحوه‌ی پردازش اجزای بازی به منظور رندر کردن (Render) نمای نهایی آن است. همه‌ی ما به خوبی می‌دانیم که بازی در نهایت در قالب فریم‌هایی آماده در اختیار گیمر قرار می‌گیرد و هر فریم در نگاه ساده هیچ تفاوتی با یک عکس ندارد. اهمیت این موضوع در این است که ما باید بازی خود را به گونه‌ای آماده کنیم که بتواند حداقل ۳۰ یا در صورت نیاز ۶۰ فریم در ثانیه را تحویل ما بدهد. برای همین در این مقاله می‌خواهیم با بررسی مکانیزم تولید یا همان رندر شدن یک فریم به شایع‌‌ترین مشکلاتی که در این زمینه به‌وجود می‌آیند اشاره کرده و راه‌حل برطرف کردن آن‌ها را هم برای شما توضیح دهیم.

آشنایی اولیه با مبحث رندر کردن

به صورت خلاصه و در اولین گام این پردازنده‌ی مرکزی یا CPU است که مشخص می‌کند چه چیزی و آن هم چگونه باید ترسیم شود. سپس در قدم بعدی و پس از مشخص شدن این موارد، پردازنده‌ی مرکزی دستورالعمل‌های مرتبط را آماده و دسته‌بندی کرده و آن‌ها را برای واحد پردازش گرافیکی یا همان GPU ارسال می‌کند. در قدم بعدی هم GPU یا همان کارت‌گرافیک خودمان بر اساس دستورالعمل‌های رسیده، تمامی موارد مشخص شده را ترسیم کرده و در قالب فریم‌های نهایی برای نمایشگر ارسال می‌کند. همین چند جمله‌ی کوتاه به خوبی جایگاه و اهمیت پردازنده‌ی مرکزی در فرآیند تولید فریم‌های نهایی بازی را نشان داده و مشخص می‌کند که تولید تصویر نهایی صرفا به GPU بستگی ندارد. اما بیایید از نزدیک‌تر به این فرآیند نگاه بکنیم.

یکی از اصطلاحات رایجی که در رابطه با فرآیند رندرینگ مورد استفاده قرار می‌گیرد، Rendering Pipline است. در علوم کامپیوتر معمولا به خطوط منطقی یا اجرایی یک فرآیند چند مرحله‌ای و گام‌به‌گام Pipeline می‌گویند.

pipeline

مثلا منظور از Rendering Pipeline تمامی عملیات‌های گام به گامی است که به ترتیب روی یکسری اطلاعات اولیه به منظور تولید نمای نهایی صورت می‌گیرد. در چنین فرآیند‌هایی به دلیل خطی بودن مراحل، وجود یک مشکل در قسمتی از خط عملیات می‌تواند تمامی عملیات را تحت تاثیر قرار داده و مختل کند. این مسئله شباهت زیادی به یک لوله‌ی انتقال آب دارد و وجود نقصی در قسمتی از این لوله می‌تواند تمامی جریان درون آن را تحت تاثیر قرار بدهد. برای همین می‌توانید این گونه تصور کنید که تمامی یک فرآیند رندر کارآمد، در حفظ همین جریان روان عملیاتی خلاصه می‌شود.

برای هر فریمی که رندر می‌شود پردازنده مرکزی کارهای زیر را انجام می‌دهد:

  • چه اشیایی باید رندر شوند؟ همه‌ی اشیا توسط پردازنده بررسی می‌شوند که آیا باید رندر شوند یا خیر. معمولا در موتورهای بازی‌سازی و در بخش مربوط به تنظیمات دوربین بازی، قسمتی برای تعیین این مسئله وجود دارد. مثلا در موتور یونیتی می‌توانیم با استفاده از پارامتری به نام Clipping Planes در دوربین بازی و در موتور آنریل با استفاده از عنصری به نام Cull Distance Volume فاصله‌ی نادیده گرفتن شدن اشیا را مشخص کنیم. البته در مواردی اگر شی توسط شی دیگر هم به کلی پوشانده شده باشد توسط موتور نادیده گرفته می‌شود که مثلا می‌توان آن را هم از طریق گزینه‌ی Occlusion Culling در موتور یونیتی فعال یا غیرفعال کرد.
  • هر شی چگونه باید رندر شود؟ پردازنده تمامی اطلاعات پیرامون اشیایی را که رندر خواهند شد جمع‌آوری کرده و در قالب دستوراتی به نام Draw Calls آماده می‌کند. هر Draw Call شامل اطلاعاتی پیرامون یک مش (Mesh) و چگونگی رندر شدن آن است. مثلا این که برای رندر کردن هر شی از کدام بافت‌ها (Textures) استفاده کند. در شرایط مشخص اشیایی که از تنظیمات مشترکی بهره می‌برند می‌توانند در قالب یک دستور Draw Call برای رندر شدن ارسال شوند. عمل ترکیب کردن اطلاعات چند شی مختلف در یک Draw Call را Batching می‌گویند.
  • آیا اشیای شبیه به هم وجود دارد؟ پردازنده بسته‌ای از اطلاعات آماده شده را که Batch نامیده می‌شود برای هر Draw Call آماده می‌کند. البته این گونه نیست که هر Batch صرفا شامل اطلاعات مرتبط با Draw Call باشد و می‌تواند اطلاعات دیگری هم درون آن قرار بگیرد که به دلیل این که معمولا مشکلی از این جهت برای بازی پیش نمی‌آید ما هم به آن نمی‌پردازیم.

    batching /draw call/setpass call

برای هر Batch که شامل یک Draw Call است پردازنده‌ی مرکزی باید کارهای زیر را انجام بدهد:

  • پردازنده‌ی مرکزی دستوری را به پردازنده‌ی گرافیکی برای تغییر وضعیت رندرینگ ارسال می‌کند که این دستور با نام SetPass Call شناخته می‌شود. هر SetPass Call به پردازنده‌ی گرافیکی تنظیمات لازم برای رندر کردن مش بعدی را تحویل می‌دهد. دقت کنید که هر فریم می‌تواند شامل تعداد زیادی مش باشد و نباید این دو موضوع را با هم اشتباه بگیرید. دقت کنید که هر دستور SetPass Call صرفا موقعی ارسال می‌شود که نیاز به تغییر در تنظیمات رندر برای مش بعدی وجود داشته باشد.
  • پردازنده‌ی مرکزی پس از مرحله‌ی قبل دستور Draw Call را به پردازنده‌ی گرافیکی ارسال می‌کند و او هم با استفاده از دستورات رسیده و تنظیماتی که در مرحله‌ی قبل اعمال شده، مش مورد نظر را ترسیم می‌کند.
  • در شرایط مشخصی گاهی به بیش از یک Pass برای هر Batch برای رندر احتیاج داریم. یک Pass بخشی از یک کد سایه‌زنی (Shader) است و هر Pass جدید نیاز به تغییر در وضعیت رندر دارد. برای همین بسته به تعداد Passها باید به همان تعداد هم دستورات SetPass Call از جانب پردازنده‌ی مرکزی برای پردازنده‌ی گرافیکی فرستاده شود که بعد از ارسال هر SetPass Call باید دوباره دستور Draw Call هم فرستاده شود!

همچنین بد نیست نگاهی به مراحل کاری پردازنده‌ی گرافیکی هم بیاندازیم:

  • پردازنده‌ی گرافیکی وظایفی را که از پردازنده‌ی مرکزی می‌رسد به همان ترتیبی که دریافت کرده اجرا می‌کند.
  • اگر این دستور یک SetPass Call باشد متناسب با آن تنظیمات جدید رندر را اعمال می‌کند.
  • اگر دستور یک Draw Call باشد مش مورد نظر را رندر می‌کند که این فرآیند هم خود دارای چند مرحله جداگانه است که هر کدام از این مراحل به وسیله‌ی بخشی از کد‌های سایه‌زنی تعریف می‌شود. به دلیل پیچیدگی این مبحث ما وارد آن نمی‌شویم.
  • تکرار فرآیند بالا تا هنگامی تمامی وظایف رسیده از جانب پردازنده‌ی مرکزی پردازش شود.

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

انواع مشکلات در رندر کردن

اگر کمی به فرایند‌های گفته شده دقت کرده باشید فهمیده‌اید که برای دست‌یابی به نمای روان و مناسب از بازی هم CPU و هم GPU باید بتوانند کارهای تولید یک فریم را در زمان مناسبی به پایان برسانند. تاخیر در هر کدام از این دو مورد در نهایت منجر به عقب افتادن فرآیند رندر یک فریم و در نهایت نمای بازی ما می‌شود. البته همان گونه که در تست‌های بنچمارک بازی‌ها هم مشاهده کرده‌اید غالبا از دو شاخص میانگین و کمینه‌ی فریم تولیدی برای نشان دادن وضعیت فنی یک بازی استفاده می‌شود و در تمامی بازی‌ها، شاهد کمی افت فریم (کمتر از یک درصد) هستیم. برای همین حد و حدود بهینه‌سازی رسیدن به چنین چیزی است و شما هیچگاه نمی‌توانید به طور صد در صد نرخ فریم بازی خود را قفل کنید.

نرخ فریم / frame rate

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

اما دومین حالت این است که ما بیش از حد ممکن بخواهیم جریانی از اطلاعات را از این خط عملیاتی عبور دهیم. واضح است که حتی بهینه‌ترین خطوط عملیاتی هم دارای محدودیتی در میزان عبور اطلاعات هستند و برای همین اگر چه در حالت قبلی وجود یک تنگه عامل مختل شدن جریان رندرینگ شده بود ولی در این جا بار اضافی و عدم کشش خط عملیاتی، عامل اختلال در جریان رندرینگ است که این وضعیت بیشتر در محصولات تولیدی بازی‌سازان تازه‌کار مشاهده می‌شود.

همچنین ما بسته به محل وجود تنگه‌ی اشاره شده می‌توانیم دسته‌بندی جدیدی را برای مشکلات گرافیکی بازی در نظر بگیریم که این مسئله در دسته‌بندی بهتر راه‌حل‌های می‌تواند به ما کمک کند. در این حالت مشکلات به دو نوع CPU bound و GPU bound تقسیم می‌شوند. در حالت اول گلوگاه ما در پردازنده‌ی مرکزی و گام‌های مرتبط با آن وجود دارد و این در حالی است که در حالت دوم مشکل پیش آمده در گام‌های مرتبط با پردازنده‌ی گرافیکی است.

اگر گلوگاه پردازنده‌ی مرکزی باشد

برای تشخیص محل وقوع مشکل بهتر است مقاله‌ی آشنایی با آنالیز فنی بازی در موتور یونیتی را مطالعه کنید اما به هر جهت اگر از شواهد این گونه برداشت شد که محل وقوع مشکل ما در گام‌های مرتبط با پردازنده‌ی مرکزی است باید به نکات زیر برای حل مسئله توجه کنیم.

تمام کارهای پردازنده‌ی مرکزی را می‌توانیم در سه دسته‌ی کلی زیر تقسیم‌بندی کنیم:

  • مشخص کردن این که چه چیز‌هایی باید ترسیم شوند
  • آماده‌سازی دستورات برای پردازنده‌ی گرافیکی
  • ارسال دستورات به پردازنده‌ی گرافیکی

هر کدام از این دسته‌ها خود شامل وظایف متعددی است که انجام تمامی آن‌ها طریق ترد‌ها (Threads) و توسط پردازنده مرکزی صورت می‌گیرد. باید بدانید که ترد‌ها توانایی انجام همزمان چندین کار را برای پردازنده‌ی مرکزی فراهم می‌کنند. مثلا شاید زیاد شنیده‌اید که یک پردازنده‌ چهار هسته‌ای و چهار تردی یا مثلا چهار هسته‌ای و هشت تردی است. در این نقطه شما به عنوان کسی که می‌خواهد عملیات عیب‌یابی را انجام دهید باید به خوبی با این اصطلاحات آشنا شوید.

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

ترد / thread

در این حالت همانند این می‌ماند که ما دو نفر را مسئول آوردن و بردن کارهای محول شده به یک هسته‌ از پردازنده در اختیار داشته باشیم و برای همین در این حالت ما هیچ گاه شاهد بیکار شدن پردازنده نخواهیم بود و گویی در یک زمان هر پردازنده در حال انجام دو کار است. قبول داریم که این توضیحات آنچنان دقیق و فنی نیستند اما برای پیش‌برد مباحث همین مقدار برای ما کافی است.

اما ترد‌ها از این جنبه برای ما اهمیت دارند که مثلا در فرآیند رندرینگ، موتور یونیتی تمامی کارها را با سه نوع ترد Main Thread یا ترد اصلی، Render Thread یا ترد رندر و Worker Thread یا ترد کارگر انجام می‌دهد. ترد اصلی عمده وظایف مربوط به پردازنده‌ی مرکزی و البته تعدادی از کارهای مربوط به رندر را در بازی ما بر عهده دارد و این در حالی است که ترد رندر صرفا وظیفه‌ی ارسال دستورات را به کارت‌گرافیک یا همان پردازنده‌ی گرافیکی بر عهده دارد. در این میان هم هر یک از ترد‌های کارگر وظیفه‌ی انجام یک کار مشخص را بر عهده دارند که برای پیچیده‌تر نشدن موضوع فعلا به آن‌ها اشاره نمی‌کنیم. اما نکته‌‌ای که در این بین برای ما اهمیت دارد در قدم اول این است که هر چقدر تعداد هسته‌ها و به دنبال آن ترد‌های یک پردازنده‌ی بیشتر باشد، ما ترد‌های کارگر بیشتری را در اختیار داریم و برای همین می‌توانیم کارهای بیشتری را به صورت همزمان انجام دهیم.

اگر چه وجود ترد‌های کارگر بیشتر می‌تواند برای ما مفید باشد اما همین تمرکز تعدادی از کارها روی یک ترد مشخص (ترد اصلی یا ترد رندر) امروزه باعث شده که پردازنده‌های مرکزی با تعداد هسته‌ها و البته ترد‌های بالا به علت فرکانس کاری پایین‌تر نتوانند بهترین عملکرد را در بازی‌ها برای ما داشته باشند. دلیل این مسئله در این است که همانند موتور یونیتی بیشتر موتورهای بازی‌سازی بخش مهمی از کارهای تولید یک فریم را بر عهده‌ی یک ترد مشخص گذاشته‌اند و یک ترد هم نمی‌تواند با بیشتر از یک هسته پردازنده در ارتباط باشد. حال هر چقدر فرکانس کاری آن هسته بیشتر باشد، او می‌تواند تعداد کارهای بیشتری را در یک ثانیه به انجام برساند. پس هر چقدر عملکرد تک هسته‌ی پردازنده‌ی شما بهتر باشد، عملکرد آن در تولید فریم‌های بازی هم بیشتر خواهد بود و این همان دلیل عمده‌ی برتری پردازنده‌های اینتلی در بازار گیمینگ است.

البته در این نقطه بد نیست به این مسئله هم اشاره کنیم که امروزه با گسترش APIهایی از قبیل Vulkan که تمرکز خود را روی بهره‌گیری از حداکثر ظرفیت پردازنده‌های چند هسته‌ای گذاشته‌اند، شاهد این هستیم که تعداد هسته‌های بالاتر عملا عملکرد بهتری به نسبت تعداد هسته‌های پایین‌تر حتی با فرکانس کاری بالاتر دارند. نمونه‌ی بارز این مسئله بازی Wolfenstain 2 است که بر اساس معماری گفته شده تولید شده و با این که بازی از گرافیک بالایی هم برخوردار است اما همواره نرخ فریم‌های میانگین و کمینه‌ی آن به مراتب بیشتر از دیگر بازی‌های بازار است.

اما از این مسائل که بگذریم اهمیت سه دسته ترد‌ اشاره شده در این است که ما می‌توانیم در بخش پروفایل کردن بازی بفهمیم که در انجام وظیفه‌ی کدام یک از این تردها مشکل به‌وجود آمده و این گونه فکری برای حل آن مشکل بکنیم.

unity profiler

 مثلا اگر ما در بخش پروفایلینگ بازی خود مشاهده کردیم که عملیات Culling (عملیاتی برای شناسایی اشیایی که نباید رندر شوند) که روی یک ترد کارگر در حال اجرا است، عامل کندی اجرای بازی ما است، کاهش مدت زمان ارسال دستورات به پردازنده‌ی گرافیکی که روی ترد رندر در حال انجام است، هیچ کمکی به ما نخواهد کرد.

با این که موتور یونیتی در به کار گیری بهینه هسته‌ها و ترد‌های متعدد یک پردازنده آنچنان تعریفی ندارد ولی در نسخه‌های جدید آن قابلیت جدیدی به نام Graphics Jobs به صورت آزمایشی قرار داده شده که شما می‌توانید با فعال کردن آن از بخش Player Setting تعدادی از وظایف مرتبط با رندر ترد اصلی یا حتی ترد رندر را به ترد‌های بیکار کارگر بدهید و این گونه راندمان اجرای بازی خود را افزایش دهید. البته همان گونه که اشاره شد این قابلیت آزمایشی است و ممکن است حتی هیچ تاثیری روی راندمان بازی شما نداشته باشد. برای همین با استفاده از ابزار پروفایلر یونیتی تاثیر آن روی بازی خود را حتما بررسی کنید.

ارسال دستورات به پردازنده‌ی گرافیکی

این مسئله که کاملا مرتبط به ترد رندر است شایع‌ترین مشکل در دسته مشکلات CPU bound است. هزینه‌برترین عملیات در فرآیند ارسال دستورات، SetPass Call نام دارد و برای همین بهترین گزینه برای رفع مشکلمان کاهش تعداد انجام این عملیات است.

برای این که بتوانیم تعداد SetPass Calls  و البته Batches را مشاهده کنیم می‌توانیم در همان بخش پروفایلر یونیتی به Rendering رفته و با کلیک روی نمودار تولید شده این اطلاعات را مشاهده کنیم.

unity profiler

البته صرفا از روی این که چه تعداد SetPass Call یا Batches داریم نمی‌توانیم هیچ گونه نتیجه‌گیری کنیم. چرا که مثلا یک رایانه‌ی قدرتمند توانایی پردازش تعداد بالایی از موارد گفته شده را دارد و این در حالی است که یک رایانه‌ی ضعیف‌تر این گونه نیست. شما مثلا می‌توانید به طور جداگانه و انجام آزمایش روی چند دسته مختلف از سخت‌افزارها، حد و حدود هر کدام را برای خود مشخص کنید.

رابطه‌ی بین SetPass Calls و Batches تحت تاثیر عوامل متعددی است اما در بیشتر حالات اگر ما بتوانیم بدون تغییر وضعیت رندر تعداد بیشتری از اشیا را رندر کنیم می‌توانیم SetPass Callهای کمتری داشته و عملکرد پردازنده‌ی مرکزی را بهبود ببخشیم. البته گاهی کم کردن تعداد Batch تاثیری روی تعداد SetPass Calls ندارد ولی با این حال همواره تاثیر آن روی عملکرد پردازنده موثر خواهد بود. سه روش کلی کم‌کردن تعداد SetPass Calls به قرار زیر است:

  • کاهش تعداد اشیایی که رندر می‌شوند احتمالا می‌تواند هم SetPass Calls و هم Batches را کاهش دهد.
  • کاهش تعداد دفعات رندر یک شی معمولا تعداد SetPass Calls را کاهش می‌دهد
  • ترکیب اطلاعات اشیا در تعداد کم‌تری از Batches که طبیعتا تعداد SetPass Calls را کاهش می‌دهد.

استفاده از هر کدام از این روش‌ها بستگی به بازی شما دارد و ممکن است هر کدام از این موارد بتواند مختصر تاثیری روی بازی شما داشته باشد.

کاهش تعداد اشیایی که رندر می‌شوند

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

Unity / یونیتی

دومین روش استفاده از همان تعیین فاصله‌ی رندر دوربین در صحنه است که در یونیتی با عنوان Clipping Planes و در آنریل با عنوان Cull Distance Volume در دسترس است. البته در این وضعیت معمولا باید با استفاده از تکنیک‌هایی فاصله‌های رندر نشده را از دید مخاطب پنهان کنیم که به عنوان مثال استفاده از افکت Fog در این زمینه می‌تواند مفید باشد که برای استفاده از این افکت در موتور یونیتی باید نکات مربوط به آن را رعایت کنید.

البته چون مبنای این مقاله موتور یونیتی است بد نیست بدانید که می‌‌توانید در یونیتی لایه‌های مختلفی را برای تعیین فاصله‌ی رندر تعریف کنید و این گونه مثلا میان حد فاصله‌ی رندر نشدن اشیای کوچک و بزرگ تفاوت قائل شوید. شما می‌توانید این مبحث را از این لینک (Layer Cull Distances) مطالعه کنید. البته در موتور یونیتی شما با استفاده از عنصر نام برده شده می‌توانید به طور اختصاصی برای بخشی از بازی خود و متناسب با اندازه‌ی عناصری که در آن بخش قرار دارند، فاصله‌ی مورد نیاز برای نادیده گرفته شدن هر شی را مشخص کنید که به نسبت موتور یونیتی از ظرافت به مراتب بیشتری برخودار است.

آخرین راهکار برای کاهش تعداد اشیای رندر شده استفاده از قابلیت Occlusion Culling در موتور یونیتی است که در مورد آن قبلا توضیح داده‌ایم. این قابلیت در یونیتی برای همه صحنه‌ها مفید نیست و حتی ممکن است بار اضافی را بر پردازنده مرکزی تحمیل کند. برای همین طبق اسناد این موتور بهتر است این مقاله را برای استفاده بهتر از این قابلیت مطالعه کنید. البته شما همچنان می‌توانید به صورت دستی هم چنین کاری را برای اشیای درون صحنه‌ی خود انجام دهید که برای انجام این کار باید به دسته بندی و استفاده از تگ‌ها در یونیتی و فعال و غیرفعال کردن قابلیت‌ها از طریق کدنویسی کمی مسلط باشید.

در انتهای این بخش این نکته را به ذهن بسپارید که حتی خود سازندگان موتورهای بازی‌سازی و از جمله یونیتی به این نکته تاکید کرده‌اند که همواره تنظیمات دستی و تجربه‌‌های شخصی می‌توانند بهتر از فرآیندهای خودکار موتورهایشان عمل کنند و دلیل آن ظرافتی است که در حالت دستی می‌توان در بازی اعمال کرد.

کاهش تعداد دفعات رندر شدن هر شی

با این که نورهای پویا، سایه‌ها و بازتاب‌ها جلوه‌های بسیاری باورپذیری را به بازی ما می‌دهند اما استفاده از آن‌ها می‌تواند برای ما بسیار هزینه‌بر باشد. مثلا با هر بار جابجایی مختصر شما درون تصویر سیستم مجبور می‌شود برای محاسبه‌ی تک‌تک این موارد از ابتدا اشیایی بسیاری را دوباره رندر کند که این مهم بسیار هزینه‌بر و سنگین است.

تاثیر این مسئله به طور مستقیم به نوع Rendering Path انتخابی شما برای بازی بستگی دارد. اصطلاح بیان شده نوع انجام محاسبات لازم برای ترسیم یک صحنه را مشخص می‌کند و تفاوت عمده‌ی مدل‌های مختلف آن در نوع محاسبه‌ی نورهای پویا، سایه‌ها و بازتاب‌ها است. مثلا دو نوع معروف در این زمینه Defferred Rendering و Forward Rendering هستند که بر اساس یک قانون کلی استفاده از Deffered Rendering در سیستم‌‌های بالارده و بازی‌هایی که از سه عنصر گفته شده به فراوان استفاده کرده‌اند توصیه می‌شود. اما در مقابل برای سیستم‌های پایین‌رده و بازی‌هایی که از عناصر گفته شده استفاده نمی‌کنند، Forward Rendering گزینه‌ی مناسب‌تری است. توضیح بیشتر در این رابطه خارج از حال این مقاله است ولی در اهمیت این مبحث همین قدر بدانید که مثلا اگر بخواهید از قابلیت Fog در بازی خود استفاده کنید نمی‌توانید از  Defferred Rendering استفاده کنید. پس یا باید این حالت را تغییر داده یا به جای Fog از افکت‌پس‌پردازشی آن درون بازی خود استفاده کنید.

deferred and forward rendering

جدای از این که کدام یک از دو حالت گفته شده را انتخاب می‌کنید، باید فکری به حال سه عنصر گفته شده در بازی خود بکنید که تاثیر بسزایی در عملکرد فنی بازی شما دارند. مواردی که نام برده می‌شوند هر کدام دارای توضیحات مرتبط با خودش است که در مواردی ما صرفا شما را به لینک‌های مربوطه برای مطالعه بیشتر ارجاع می‌دهیم.

  • نورپردازی پویا یا Dynamic Lighting موضوع پیچیده‌ای است و برای آشنایی با آن می‌توانید به این لینک مراجعه کنید و پس از آن از این لینک با تعدادی از تکنیک‌های بهینه‌سازی در این رابطه آشنا شوید. البته اگر در صحنه‌های شما هیچ شی متحرکی وجود نداشته و یا اهمیتی از جهت داشتن سایه ندارد بهتر است به جای استفاده از نورپردازی پویا، از تکنیکی به نام Baking استفاده کنید. در این تکنیک محاسبات مربوط به نور قبلا محاسبه و ذخیر شده و مثلا همانند یک بافت روی صحنه اعمال می‌شود. این گونه دیگر لازم نیست برای تک‌تک عناصر درون صحنه این محاسبه صورت پذیرد. برای آشنایی با این مبحث می‌توانید به ترتیب از این لینک و این لینک استفاده کنید.
  • اگر هدف ما استفاده از سایه‌های داینامیک و پویا درون بازی است می‌توانیم از طریق این لینک و با استفاده از ویژگی‌هایی از قبیل Shadow Distance محدوده‌ی استفاده از این قابلیت را به مقدار مناسبی محدود کرده و از بار هزینه‌های آن کم کنیم. در رابطه با این ویژگی در مقاله‌ی  هم صحبت شده است.
  • بازتاب‌ها به طور مستقیم روی تعداد Batches بازی ما تاثیر دارند و برای همین باید به صورت حداقلی درون بازی مورد استفاده قرار بگیرند و حتی گاهی بر اساس اولویت عملکردی بازی باید استفاده از آن‌ها را کنار بگذارید. به هر جهت شما می‌توانید از این لینک با تعدادی نکته‌ی بهینه‌سازی در این زمینه آشنا بشوید.

ترکیب اشیا در دسته‌های کمتر

ما با استفاده از تکنیک‌هایی می‌توانیم اشیا را در دسته‌های کم‌تری قرار دهیم ولی برای این کار باید دو مورد زیر رعایت شود:

  • به اشتراک‌گذاری نمونه‌ی یکسانی از یک متریال در میان چند شی
  • داشتن تنظیمات متریال یکسان در میان اشیا

دسته‌بندی یا همان Batching اشیا می‌تواند روی عملکرد بازی شما تاثیر مثبتی داشته باشد ولی با تمامی این‌ها باید به این نکته توجه کنید که هزینه‌ی انجام خود عمل دسته‌بندی نباید آنقدری بالا برود که خودش برای بازی ایجاد مشکل کند. برای همین همواره سعی کنید از طریق پروفایل کردن بازی این مسئله را زیر نظر داشته باشید.

Static Batching تکنیکی است که یونیتی با استفاده از آن اشیای نزدیک به یکدیگر و غیرمتحرک را در یک دسته قرار می‌دهد. به عنوان مثال ستون‌های یک ساختمان می‌تواند مثال خوبی برای این مسئله باشد. شما برای هر شی می‌توانید از بخش Inspecter این گزینه را فعال کنید اما برای آشنایی بیشتر با این مسئله می‌توانید از این لینک استفاده کنید. به این مسئله توجه کنید استفاده از این تکنیک میزان مصرف حافظه‌ی رم شما را افزایش می‌دهد. پس این بخش از منابع سخت‌افزاری را در پروفایلر زیر نظر داشته باشید.

Dynamic Batching تکنیک دیگری است که یونیتی با استفاده از آن تمامی اشیا چه ثابت و چه متحرک را دسته‌بندی می‌کند اما مسئله‌ای که وجود دارد محدودیت‌های این روش است که در این لینک در مورد آن‌ها صحبت شده است. به عنوان مثال این تکنیک صرفا بر مش‌های عمل می‌کند که از تعداد راس کم‌تر از ۹۰۰ عدد تشکیل شده باشند که نکته‌ی قابل توجهی است. این تکنیک به راحتی می‌تواند بیشتر از میزان هزینه‌ای که از پردازنده‌ی مرکزی کم می‌کند خود به آن اضافه کند و برای همین باید در استفاده از آن بسیار هشیار باشید.

GPU Instancing به ما اجازه می‌دهد که تعداد زیادی از اشیای یکسان را با استفاده از آن دسته‌بندی کنیم. البته محدودیت‌هایی هم بر استفاده از این تکنیک وجود دارد که از نمونه‌های آن می‌توانیم به پشتیبانی نکردن تمامی سخت‌افزارها از این تکنیک اشاره کنیم. شما می‌توانید از این لینک اطلاعات مربوط به این تکنیک را به دست آورید.

gpu instancing

Texture Atlasing در مواقعی کاربرد دارد که در آن چندین بافت در قالب یک بافت بزرگ‌تر با یکدیگر ترکیب شده‌اند. با این که غالبا این وضعیت در بازی‌های دوبعدی و رابط‌کاربری‌ها(به دلیل ماهیت دوبعدی آن‌ها) به‌وجود می‌آید ولی همچنان این تکنیک را می‌توان در بازی‌های سه‌بعدی هم مورد استفاده قرار دارد. اگر ما از این تکنیک در هنگام آماده کردن آرت‌های خود استفاده کنیم می‌توانیم مطمئن باشیم که اشیایی که به صورت اشتراکی از این بافت‌ها استفاده می‌کنند به راحتی توسط سیستم پردازنده‌ دسته‌بندی خواهند شد. همچنین بد نیست بدانید که موتور یونیتی دارای ابزاری در این زمینه برای بازی‌های دوبعدی است که با نام Sprite Packer شناخته می‌شود.

spriter packer

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

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

منبع: زومیت