آموزش ویدئویی بازی‌سازی با Unity - جلسه هشت: GAME OVER
1398/02/25 15:06 , میلاد صاحب نظر

آموزش ویدئویی بازی‌سازی با Unity - جلسه هشت: GAME OVER

در این قسمت به یکی از اصلی‌ترین عناصری که هر بازی باید از آن برخوردار باشد خواهیم پرداخت. یعنی GAME OVER بازی. با ما همراه باشید!

مقدمه: آشنایی با موتور بازی‌سازی Unity قسمت سوم: حرکات قسمت ششم: GAMEPLAY قسمت نهم: تغییر مراحل و انیمیشن
قسمت اول: اصول اولیه Unity قسمت چهارم: دوربین دنبال‌گر قسمت هفتم: امتیاز‌دهی (SCORE) و UI قسمت دهم: پایان بازی
قسمت دوم: برنامه‌نویسی در Unity قسمت پنجم: برخورد قسمت هشتم: Game Over  

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

در این بازی مسلما GAME OVER زمانی باید صورت بگیرد که بازیکن با یک مانع برخورد کند یا از لبه زمین بازی به پایین سقوط کند.

در حال حاضر روشی که در آن بررسی می‌کنیم که آیا بازیکن با چیزی برخورد کرده یا نه، دارای یک اسکریپت به نام player colliotion است که تمرکز اصلی آن روی بازیکن است.

اما این اسکریپت محل جالبی برای ایجاد فرآیند GAME OVER نیست. بهترین کار این است که یک مدیر بازی (Game Manager) این امر را کنترل کند. پس باید یک مدیر بازی ایجاد کنیم.

پس در قسمت Hierarchy یک شیء جدید ایجاد کنید. Transform آن را ریست کنید و یک نام جدید (ترجیحا Game Manager) انتخاب کنید. یک component جدید هم به عنوان اسکریپت ایجاد کنید و نام آن را نیز Game Manager بگذارید.

این اسکریپت جدید که برای شیء Game Manager ایجاد کردید قرار است مسئول تغییر وضعیت‌ها در بازی باشد. 

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

در این قسمت هدف اصلی ما کد نویسی است. قصد داریم با نوشتن چند خط کد ساده کاری کنیم که وقتی بازیکن به شیئی برخورد کرد یا از لبه زمین بازی به پایین سقوط کرد، بازی Restart شود.

پس روی اسکریپت Game Manager دبل کلیک کنید تا وارد ویژوال استودیو شوید. حالا مثل همیشه دو تگ using در بالا و دو تابع Start و update را حذف کنید. 

حالا یک تابع جدید به این شکل ایجاد کنید: 

void EndGame ()
{

}

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

پس بین آکولادها این کد را بنویسید: 

Debug.Log("Game Over");

حالا کد را Save کنید و به یونیتی برگردید. بسیار خب، حالا اسکریپت Game Manager دارای تابعی است که می‌تواند پیام Game Over را در کنسول چاپ کند. اما به روشی نیاز داریم که بتوانیم به اسکریپت Game Manager دسترسی یابیم و آن تابع را فراخوانی کنیم. 

برای این منظور روی شیء Player در Hierarchy کلیک کنید و در Component‌های آن اسکریپت Player collition  را بیابید و روی آن دبل کلیک کنید تا وارد کدهایش شوید.

معمولا برای اینکه به اسکریپتی دسترسی پیدا کنید باید یک رفرنس به سمت آن ایجاد کنید. مثلا در کدهای Player Collition مشاهده می‌کنید که یک متغیر به نام Movement ایجاد شده است که برخورد بازیکن را بررسی می‌کند.

می‌توانید برای دسترسی به اسکریپت Game Managerهم همین کار را بکنید و یک متغیر مانند کد زیر ایجاد کنید: 

Public GameManager gamemanager;

حالا کد را Save کنید و به یونیتی بازگردید و شیء Player را به داخل محل خالی که نام آن Game Manager است قرار دهید.

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

چون Game Manager فقط توسط یک شیء رفرنس شده است و وقتی آن شیء حذف شود و دوباره اضافه شود، رفرنس شما از بین خواهد رفت. پس باید یک روش دیگر را به کار بگیریم.

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

برای این منظور به قسمتی از کد درون اسکریپت Player Collition که می‌خواهید بازی در آن‌جا تمام شود بروید. در بازی ما، بازی جایی تمام می‌شود که بازیکن با چیزی برخورد کند.

پس به جایی که شرط برخورد بررسی شده است بروید و کد زیر را بنویسید:

FindObjectOfType<GameManager>();

 FindObjectOfType که همان روش آسان جستجوی اسکریپت‌ها در یونیتی است، GameManager در واقع نوعی است که این دستور قرار است آن را بیابد (چون اسم Component همین Game Manager است پس نوع را هم Game Manager نوشتیم).

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

دلیل این اتفاق این نیست که ما یک رفرنس قابل‌قبول به سمت Game Manager نداریم، بلکه به این دلیل است که Game Manager تابعی که در آن ایجاد کرده‌ایم را مخفی می‌کند چون آن تابع فعلا به صورت پیش‌فرض به عنوان Private نشانه‌گذاری شده است.

پس به اسکریپت Game Manager بازگردید و تابعی که ایجاد کردید را به این شکل اصلاح کنید: 

Public Void EndGame()
{ 

}

 کدتان را Save کنید و به اسکریپت Player Collition بروید. حالا می‌توانید به EndGame دسترسی داشته باشید. پس کد جستجوی Game Manager را به این شکل اصلاح کنید: 

FindObjectOfType<GameManager>().EndGame;

این کد یعنی اینکه ما به دنبال تابع EndGame از اسکریپت Game Manager هستیم. 

نکته: برای اینکه این کد بدون هیچ اروری اجرا شود باید حتما نوعی که جستجو می‌کنید (یعنی چیزی که بین <> قرار دارد) حتما در صفحه بازی وجود داشته باشد. پس مراقب باشید که ابتدا حتما Game Manager را ایجاد کرده باشید.

حالا کدتان را Save کنید و به یونیتی بازگردید. وقتی بازی را Play می‌کنید با تعداد زیادی ارور مواجه خواهید شد. ولی نگران نباشید طبیعی است. ما از قبل دو شیء دیگر داریم که به Player رفرنس شده‌اند، یعنی Main Camera و Text. 

یادتان هست که گفتیم وقتی از رفرنس استفاده می‌کنیم اگر شیء از بین برود و دوباره ظاهر شود، آنگاه رفرنس نیز از بین می‌رود؟ خب این کدی که نوشتیم یک بار بدون ارور عمل می‌کند چون همه رفرنس‌ها درست هستند.

اما وقتی یک بار شیء از بین رفت و دوباره ایجاد شد، آنگاه با ارورهایی مواجه خواهید شد که بیان می‌کنند شیءهایی وجود دارند که باید به Player رفرنس شوند. در این بازی ما فقط دو مورد Text و Main Camera را داریم.

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

ما می‌خواهیم همین اتفاق در هنگامی که بازیکن از لبه زمین بازی سقوط می‌کند نیز بیفتد. پس روی شیء Player در Hierarchy کلیک کنید و این بار روی اسکریپت Player Movement دبل کلیک کنید.

اگر یادتان باشد در این اسکریپت حرکات بازیکن و نیروهایی که برای حرکت به چپ و راست به آن وارد می‌شوند را کنرل می‌کردیم. 

پس سقوط بازیکن باید در متد Update که حرکت را کنترل می‌کند اتفاق بیفتد. ما قصد داریم بررسی کنیم ببینیم آیا بازیکن در راستای محور Y (عمودی) از مقدار خاصی پایین‌تر می‌آید یا نه. اگر آمد پس پیام Game Over در کنسول چاپ شود.

پس در انتهای متد Update کد زیر را بنویسید: 

if(rb.position.y < -1f)
{
FindObjectOfType<GameManager>().EndGame;
}

در این کد ۱- مقداری است که به صورت تجربی به دست آوردیم، یعنی شیء Player را به صورت دستی در راستای محور Y جا به جا کردیم تا ببینیم از چه مقدار پایین‌تر برود تا Game Over محسوب شود.

حالا کد را Save کنید و به یونیتی برگردید. وقتی بازی را Play می‌کنید مشاهده خواهید کرد که تعداد خیلی زیادی پیام Game Over در کنسول چاپ می‌شود. 

دلیل این امر آن است که متد Update در هر ثانیه چندین بار فراخوانی می‌شود. اما ما فقط می‌خواهیم یک بار این اتفاق بیفتد. برای حل این مشکل به اسکریپت Game Manager بروید و کدها را به شکل زیر اصلاح کنید:

bool gameHasEnded = false;
Public Void EndGame()
{
if(gameHasEnded == false)
{
gameHasEnded = true;
Debuug.Log("Game Over");
}
}

در این کد یک عبارت boolean به نام gameHasEnded ایجاد کردیم که به صورت پیش‌فرض مقدار اولیه آن False است.

سپس درون تابع EndGame با یک عبارت if بررسی کردیم که اگر gameHasEnded برابر با False باشد، آنگاه مقدار آن را برابر با True قرار بده و چاپ کن Game Over.

حالا وقتی برنامه دوباره متد Update در اسکریپت Player Moveent را دوباره فراخوانی می‌کند این بار مقدار GameHasEnded برابر با true است و شرط تابع EndGame برقرار نیست و دیگر پیام Game Over در کنسول چاپ نمی‌شود. بنابراین فقط یک بار Game Over چاپ خواهد شد. 

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

پس به اسکریپت Game Manager برگردید. Restart شدن بازی یعنی اینکه صحنه بازی باید تغییر کند. برای دسترسی به دستورات مخصوص صحنه بازی باید به تگ‌های اسکریپت تگ زیر را اضافه کنید:

using UnityEngine.SceneManagement;

حالا در زیر تابع EndGame یک تابع جدید با نام Restart ایجاد کرده و آن را در شرط if تابع EndGame فراخوانی می‌کنیم تا هر وقت تابع EndGame فراخوانی شد برنامه وارد تابع Restart‌شود: 

bool gameHasEnded = false;
Public Void EndGame()
{
if(gameHasEnded == false)
{
gameHasEnded = true;
Debuug.Log("Game Over");
Restart;
}
}
Void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

درون تابع Restart کدی را مشاهده می‌کنید که با استفاده از دستور SceneManager نوشته شده است و استفاده از این دستور به کمک تگی که اضافه کردیم میسر شد.

گفتیم که Restart یعنی تغییر وضعیت صحنه بازی، در واقع وقتی بازیکن می‌بازد باید صفحه آغاز مجدد بازی load شود تا بازیکن دوباره تلاش کند.

برای این منظور می‌توانید بین پرانتز‌هایی که بعد از LoadScene قرار دارند فقط نام صفحه‌ای که می‌خواهید Load شود (در بازی ما Level 1) را بنویسید.

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

کدی که بین پرانتزهای پس از LoadScene قرار گرفته است نام صفحه‌ای که بازیکن در آن باخته است را به دست می‌آورد و کد بیرون از پرانتزها آن صفحه را Load می‌کند. 

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

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

برای حل مشکل اول به سربرگ Window در بالای صفحه بروید و Lighting را انتخاب کنید. در سمت راست صفحه یک پنل برای Lighting باز خواهد شد. در پایین این پنل یک چک باکس کوچک را مشاهده می‌کنید که روی auto تنظیم شده است و تیک خورده است.

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

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

بسیار خب مشکل تغییر نورپردازی حل شد. حالا برویم سراغ مشکل بیش از حد سریع Restart شدن. برای اینکه بتوانیم اتفاقات پس از برخورد را ببینیم به کمی تاخیر برای load شدن صفحه نیاز داریم.

برای این منظور به اسکریپت Game Manager بازگردید. برای اینکه بتوانیم میزان تاخیر را در فضای یونیتی کنترل کنیم پس یک متغیر برای تاخیر به شکل زیر ایجاد می‌کنیم:

Public float restartDelay = 1f;

یک متغیر float به نام restartDelay ایجاد کردیم که مقدار پیش‌فرض آن 1‌ ثانیه است. حالا اگر Save کنید و به یونیتی بازگردید مشاهده خواهید کرد که در زیر اسکریپت Game Manager یک محل خالی به نام restartDelay به وجود آمده که می‌توانید مقدار تاخیر را در آن تغییر دهید.

اما باید تاخیر را در فرآیند Restart اعمال کنیم. پس به تابع EndGame به جایی که تابع Restart را فراخوانی کرده‌اید بروید. حالا به جای اینکه تابع Restart را فراخوانی کنید، آن تابع را پاک کنید و کد را به شکل زیر اصلاح کنید:

bool gameHasEnded = false;
Public Void EndGame()
{
if(gameHasEnded == false)
{
gameHasEnded = true;
Debuug.Log("Game Over");
Invoke("Restart", restartDelay);
}
}
Void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

در کد بالا همانطور که گفتیم فراخوانی تابع Restart درون شرط تابع EndGame را پاک کردیم و به جای آن یک خط دستور با کلیدواژه Invoke نوشتیم. این دستور دو پارامتر می‌پذیرد، اولین پارامتر آن نام تابعی است که باید فراخوانی شود و پارامتر دوم میزان تاخیر در فراخوانی آن تابع است.

می‌توانید در پارامتر دوم یک رقم مثل (۲f) بنویسید یعنی ۲ ثانیه تاخیر در فراخوانی داشته باشد. اما ما یک متغیر برای تاخیر ایجاد کردیم تا برای تغییر میزان تغییر دیگر لازم نباشد به سراغ این کد بیاییم و مستقیما از طریق یونیتی تغییر را اعمال کنیم.

پس در کد بالا به اندازه مقدار متغیر restartDelay طول می‌کشد تا تابع Restart‌ فراخوانی شود. حالا کد را Save کتید و به یونیتی بازگردید و باید مشکل سریع Restart شدن نیز برطرف شده باشد. تقریبا کارمان تمام شد اما یک نکته نهایی باقی مانده است که باید بگوییم.

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

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

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

پس روی کلید Add Open Scenes کلیک کنید تا صحنه بازی شما به لیست اضافه شود. مشاهده خواهید کرد که نام صحنه در لیست ظاهر می‌شود که در کنارش یک چک باکس قرار دارد که اگر می‌خواهید به بازی اضافه شود آن را تیک بزنید در غیر این صورت تیک را بردارید.

در سمت راست هم یک عدد مشاهده می‌کنید که build index صحنه شما است. اگر چندین مرحله داشته باشید، معمولا آن‌ها را به ترتیب نام build نمی‌کنید بلکه به ترتیب buil index اجرا می‌کنید. بسیار خب همین بود. حالا پنجره را ببندید.

این هم از این قسمت. امیدواریم کمال لذت را برده باشید. در قسمت بعدی راجع به تغییر مراحل  بازی و اضافه کردن انیمیشن به بازی صحبت خواهیم کرد و بسیار جذاب خواهد بود.

با MUG همراه باشید!

منبع: Brackeys

 مطالب مرتبط

 مقدمه ای بر زبان برنامه نویسی #C و پلتفرم NET Framework.
 آموزش گام به گام #C
 طبقه بندی زبان های برنامه نویسی
۷ زبان توسعه بازی Unity برای آموختن: کدام بهترین است؟
۸ بازی ساخته شده با unity توسط تیم‌های کوچک یا تک‌نفره
برنامه نویسی شیءگرا چیست؟

از آخرین دوره های آموزشی و تخفیف ها مطلع شوید

با تکمیل فرم زیر ، از اخبار و اطلاعات به روز برنامه نویسی و تکنولوژی عقب نمانید

آخرین مطالب

آموزش جامع SQL Server (جلسه ۳۱: توابع رشته‌ای – بخش ۳)
آموزش جامع SQL Server (جلسه ۳۱: توابع رشته‌ای – بخش ۳)

در جلسه قبل بخش ۲ مطالب مبحث توابع رشته‌ای یا string را به صورت کامل توضیح دادیم. و ...

آموزش جامع SQL Server (جلسه ۳۰: توابع رشته‌ای – بخش ۲)
آموزش جامع SQL Server (جلسه ۳۰: توابع رشته‌ای – بخش ۲)

در جلسه قبل، مبحث توابع رشته‌ای یا String را آغاز کردیم. به دلیل ازدیاد این توابع و برای ...

با Visual Studio Code’s Live Share گروهی برنامه‌نویسی کنیم!
با Visual Studio Code’s Live Share گروهی برنامه‌نویسی کنیم!

چه در مراحل اولیه یادگیری یک زبان برنامه‌نویسی باشید یا یک کد نویس با تجربه و ماهر باشید، ...

آخرین دیدگاه ها

دیدگاه خود را درباره این پست بنویسید

فرم ارسال نظرات