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

به طور کلی بهینه سازی یا Optimization به تغییر دادن برنامه در راستای اجرای سریعتر و استفاده بهتر از منابع گفته می شود. بازی سازی از کارهایی است که نیاز دارد برنامه نویس همواره هنگام نوشتن کد این مساله را مد نظر قرار دهد. بهینه سازی به دو دسته macro optimization و Micro optimization تقسیم می شود. دسته اول استفاده از الگوریتم ها و ساختار داده های مناسب برای حل مساله را شامل می شود و دسته دوم به تغییر توابع و عملگر ها و خط های کوچکی از کد و تغییر محاسبات کوچک به شکل بهینه گفته می شود. مثلا استفاده از ساختار آرایه به جای درخت برای استفاده بهتر از cache پردازنده و استفاده از Dictionary در صورت نیاز به یافتن مقادیر مختلف با سرعت بالا به جای آرایه و لیست از نوع macro optimizaiton و انجام تقسیم با استفاده از ضرب و یا استفاده از اعداد صحیح به جای اعداد اعشاری و انجام shift به جای اعمال ریاضی micro optimizaiton هستند.


هدف شما از بهینه سازی در Unity معمولا بالا بردن FPS و قابل اجرا کردن یک الگوریتم و پایین آوردن زمان انتظار کاربر می باشد. همچنین ممکن است برای اجرای بازی روی دستگاه های ضعیفتر نیز به این کار نیاز داشته باشید. اگر بازی را برای اجرا روی سرور بنویسید هم برای مصرف بهتر منابع سرور نیاز به Optimization دارید. برای داشتن یک بازی بهینه باید اولا هنگام نوشتن برنامه با آگاهی از میزان کار انجام شده و مصرف منابع هر عملیات کدی بهینه بنویسید و دوما هنگام مشاهده مشکل با استفاده از ابزار های اندازه گیری مثل profiler ها گلوگاه های سیستم یا همان bottle neck ها را پیدا کنید و آن ها را بهینه کنید. گلوگاه به بخش هایی گفته می شود که تغییر آن ها و سرعت بخشیدن به آن ها تاثیر زیادی بر روی بهینه شدن کل سیستم می گذارد و در حال حاضر منابع زیادی را به شکل غیر بهینه مصرف می کنند. ابتدا مواردی که باید بدانید تا کد بهینه بنویسید را بررسی می کنیم و سپس به روش های اندازه گیری و بهینه سازی می پردازیم.

 

توابع کند در یونیتی
توابعی در Unity وجود دارند که ممکن است در برخی موارد کار را ساده کنند ولی کندتر از توابع دیگر اجرا می شوند و شما باید تا حد ممکن از استفاده از آن ها خود داری نمایید. SendMessage و به خصوص BroadCastMessage و دیگر توابع این خانواده که برای صدا زدن متدی در یک GameObject دیگر و یا Component دیگر مورد استفاده قرار می گیرند بسیار از صدا زدن متد به شکل مستقیم کندتر هستند. این متد ها از reflection برای یافتن متد مورد نظر برای صدا زدن استفاده می کنند و لازم است بین GameObject ها و بچه هایشان در hierarchy و component های هر GameObject عملیات search انجام دهند. سیستم messaging جدید یونیتی بسیار سریعتر از SendMessage عمل می کند ولی تا جای ممکن خودتان متد های مورد نظر را با رفرنس (reference) دادن به GameObject های دیگر و component هایشان صدا بزنید.

GameObject.Find و GameObject.FindObjectWithTag بسیار کند هستند و لازم است کل گراف scene را برای یافتن GameObject مورد نظر جست و جو کنند. البته نسخه ای که دنبال Tag می گردد سریعتر از نسخه ای است که دنبال اسم می گردد.

GetComponent استفاده از این تابع اجتناب ناپذیر است ولی باید بدانید نسخه ای که string اسم component را به عنوان پارامتر دریافت می کند بسیار کندتر از نسخه ی generic و نسخه ای است که type دریافت می کند. همچنین GetComponentInChildren و GetComponentInParent به دلیل انجام جست و جو های بیشتر کندتر از خود Getcomponent هستند و سعی کنید از این توابع درون حلقه ها و کد هایی که زیاد اجرا می شوند (مثل Update و FixedUpdate) استفاده نکنید و مقادیر مورد نیاز را تا حد ممکن کش کنید و در متغیری ذخیره کنید و از مقدار ذخیره شده استفاده کنید.

RayCast و SphereCast و ... به طور کلی cast کردن کار کندی است ولی ray از همه اشکال دیگر بسیار سریعتر است و باید سعی کنید با حد اقل تعداد cast و محدود کردن فاصله و layer های مورد cast آن را بهینه کنید.
Instantiate و Destroy نیز مثل خیلی از موارد بالا باید استفاده شوند ولی هم اجرایشان به دلیل کپی کردن کل داده های یک GameObject کند است و هم باعث ورود و خروج داده های زیادی بین RAM و disk می شوند که کار کندی است. به همین دلیل در صورت داشتن مشکل با این موضوع بهتر است از object pool استفاده کنید که در manual خود یونیتی توضیح داده شده. برای این کار به جای از بین بردن و ساختن GameObject ها شما در ابتدا لیستی از مثلا 100 GameObject را می سازید و همه آنها را با استفاده از GameObject.SetActive غیر فعال می کنید و به جای ساخت GameObject یک آبجکت از لیست pool خود گرفته و فعال می کنید و به جای از بین بردن آبجکت را غیر فعال کرده و به pool برمی گردانید.

به طوری کلی توابعی که با رشته ها ، دیسک و یا شبکه سر و کار دارند و یا باید عملیات جست و جو انجام دهند و یا روی داد های زیادی در حافظه کار کنند کندتر از توابع دیگر هستند.

حلقه ها و تعریف Hot path
به بخش هایی از کد که بارها اجرا می شوند و تغییرات کوچکی در آن ها سرعت برنامه را بسیار کم و زیاد می کند، hot path گفته می شود. مثلا حلقه های تو در تو و یا حلقه های بسیار طولانی (با تعداد گردش بالا) که هر فریم اجرا می شوند و به طور کلی کد های درون Update, LateUpdate و FixedUpdate معمولا در hot path قرار دارند مگر این که کد درون این ساختار ها فقط در شرایط خیلی خاصی اجرا شوند و واقعا همیشه در حال اجرا نباشند. بهینه کردن کد های درون hot path ها مهمترین کار شما هنگام بهینه سازی است.

ساختار داده ها
داده های شما باید با استفاده از ساختمان داده های مناسب برای الگوی استفاده در بازی تعریف شوند. توضیح این مبحث بسیار طولانی بوده و مبحث درس ساختمان داده ها در دانشگاه می باشد. هنگام انتخاب یک ساختمان داده مثل آرایه ، لیست، صف و پشته (array, queue , stack) باید توجه کنید که مرتبه زمانی عملیات هایی که زیاد روی داده انجام می دهید چیست. مثلا اضافه کردن یک المان به یک لیست مرتب sortedList کندتر از اضافه کردن المان به لیست معمولی است ولی در عوض اگر شما به داده های مرتب داشته باشید، لیست مرتب داده ها را همواره به شکل مرتب به شما می دهد.
همچنین برخی ساختمان های داده با مدل حافظه و معماری سیستم بهتر کار می کنند. کلا ساختمان داده های خطی که المان های مربوط به هم به شکل خطی پشت هم قرار گرفته اند باعث استفاده بهتر از کش CPU شده و در بسیاری موارد مناسبتر از انواع دیگر داده هستند. توضیح مفاهیم مربوط به این مطلب نیز بسیار طولانی هستند و به پست های مخصوص به خود نیاز دارد.

رشته ها
به طور کلی استفاده از رشته ها (string) کند می باشد و باید سعی کنید تا جای ممکن به جای رشته از ساختار های دیگر برای چیزهایی مثل id و ... که واقعا رشته نیستند استفاده کنید. رشته ها در .NET از نوع immutable هستند یعنی هر تغییر در رشته در واقع باعث ساخته شدن رشته ای جدید در حافظه می شود که مصرف حافظه را بسیار بالا می برد. توابعی مثل string.split و ... تعداد زیادی رشته روی حافظه می سازند.

garbage collector
در این باره نیز باید پست کاملی نوشته شود ولی به طور کلی حافظه ای که برنامه شما با تعریف متغیر به خود تخصیص می دهد توسط سیستمی در محیط اجرایی .NET به نام garbage collector یا همان GC از برنامه بازپس گرفته می شود. پس از این که یک بخش از حافظه از دسترسی برنامه شما خارج شد و GC می توانست قطعا بگوید این حافظه مورد استفاده نیست، حافظه به سیستم باز می گردد تا برای کارهای دیگر به برنامه خودتان یا برنامه های دیگر داده شود. معمولا باید سعی کنید object هایی که می سازید یا سریع از بین بروند یا مدت بسیار زیادی در حافظه بمانند. کلا بهتر است تا حد ممکن آبجکت جدیدی نسازید. منظور از آبجکت یک کلاس در .NET است و نه GameObject در Unity هر چند ساخت یک GameObject نیز به دلیل ساخت تعداد زیادی آبجکت .NET از همین قانون پیروی می کند.
حافظه ای که برنامه شما می گیرد بر دو نوع است. حافظه heap که GC آن را مدیریت می کند و حافظه stack که بسیار سریعتر است و به سادگی مدیریت می شود و اصلا نیاز به جست و جو روی همه آبجکت ها توسط GC ندارد. داده های پایه .NET و struct ها روی stack تخصیص داده شده و class ها روی heap تخصیص داده می شوند. برای آبجکت های کوچک (از نظر سایز) که زود از بین می روند از struct ها استفاده کنید و همچنین رفرنس آبجکت ها را در متغیر های static قرار ندهید و اگر هم دادید پس از پایان نیاز به آن ها از درون متغیر پاک کنید (متغیر را به null ست کنید) تا GC بتواند حافظه را از برنامه پس بگیرد، زیرا تا وقتی که حتی یک رفرنس به یک بخش از حافظه در برنامه وجود داشته باشد ، حافظه پس گرفته نخواهد شد.

 

مصرف حافظه خود یونیتی
همه GameObject هایی که به شکل مستقیم یا غیر مستقیم از درون scene به آن ها رفرنسی وجود داشته باشد در حافظه لود می شوند. برای جلوگیری از لود شدن آبجکت هایی که نمی خواهید، آن ها را در scene رفرنس ندهید. برای جلوگیری از این مشکل ، اگر مثلا دو نوع از هر asset با اندازه های مختلف برای دستگاه های قدیمی و جدید دارید باید یا آن ها را در resources folder بگذارید و یا از asset bundle ها استفاده کنید.

فیزیک
به طور کلی هر چه تعداد collider ها بیشتر شود و هر چه تعداد rigidbody ها بیشتر شود ، فیزیک بخش بیشتر از وقت Unity را مصرف خواهد کرد. از collider های پیچیده در آبجکت های متحرک کمتر استفاده کنید و از mesh collider برای GameObject های متحرکت تا حد امکان استفاده نکنید.

اندازه گیری ها و profiler
حتی با رعایت موارد بالا و بسیاری موارد دیگر، شما باز هم نیاز خواهید داشت سیستم های نوشته شده را بهینه کنید. برای این کار به جای حدس زدن باید با تستفاده از profiler میزان مصرف حافظه و CPU را با profiler اهندازه بگیرید. برای این کار از منوی Window ادیتور Unity گزینه Profiler را انتخاب کنید. این ابزار به شما میزان مصرف حافظه، CPU و حتی شبکه و GPU را نشان می دهد. محاسبات فیزیک و صدا نیز در این پنجره نمایش داده می شوند و شما می توانید هم در editor و هم بازی های build شده در حال اجرا روی PC یا mobile را profile کنید. برای profile کردن build ها که جواب های دقیقتری هم به شما می دهد (به خصوص در mobile که محیط اجرا کاملا فرق دارد) باید هنگام build کردن تیک development build را بگذارید و ترجیحا autoconnect to profielr را هم انتخاب کنید. سپس در بخش بالای پنجره profiler آن برنامه در حال اجرا را به جای editor برای profile کردن انتخاب نمایید.

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

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

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