گیمولوژی | رسانه تحلیلی بازی سازی | چگونه یک بازی چندنفره ساده در یونیتی بسازیم
(مطلب پیش رو ترجمه ای از مقاله سایت gamedevacademy.org همراه با تغییراتی از سوی مترجم است)
در این آموزش ما می خواهیم یک دموی ساده بسازیم تا یاد بگیریم چگونه از امکانات چندنفره انجین Unity استفاده کنیم. بازی ما یک Scene ساده خواهد داشت، جایی که ما یک شوتر فضایی چندنفره را پیاده سازی خواهیم کرد. در دموی ما، چند بازیکن قادر خواهند بود تا به یک بازی واحد وارد شوند و به دشمنانی که تصادفی ظاهر می شوند تیر بزنند.
برای ادامه دادن این آموزش، انتظار می رود که با موارد زیر آشنا باشید:
· برنامه نویسی C#
· استفاده از ادیتور یونیتی و مواردی چون وارد کردن Asset ها، ساخت Prefab و اضافه کردن کامپوننت
ساخت پروژه و وارد کردن Asset
پیش از آغاز به خواندن آموزش، نیاز دارید که یک پروژه جدید در یونیتی بسازید و تمام اسپرایت های موجود در سورس کد را Import کنید. برای انجام دادن این کار، یک پوشه به نام Sprites بسازید و تمام اسپرایت ها را در آن کپی کنید. بخش Inspector یونیتی به طور خودکار آن ها را وارد پروژه تان خواهد کرد.
با این حال، تعدادی از این اسپرایت ها در Spritesheet قرار دارند، مثل موارد مربوط به دشمنان که نیاز است از هم جدا شوند. برای این کار، نیاز است تا Sprite Mode را روی Multiple قرار دهید و اسپرایت ادیتور را باز کنید.
در اسپرایت ادیتور (که در تصویر پایین نشان داده شده)، باید منوی Slice را بازی کنید و روی دکمه Slice کلیک کنید. بخش Slice Typer هم روی Automatic قرار دهید. در نهایت هم دکمه Apply را کلیک کرده و پنجره را ببندید.
فایل های سورس کد
می توانید فایل های سورس کد را از طریق این لینک دانلود کنید.
طرح پیش زمینه
اولین چیزی که قرار است انجام دهیم، ساخت یک طرح پیش زمینه است تا تصویر پیش زمینه را نمایش دهیم.
می توان این کار را با ساخت یک تصویر(Image) جدید در Hierarchy انجام داد که این کار به طور خودکار یک Canvas خواهد ساخت (یادتان نرود اسم طرح را به BackgroundCanvas تغییر دهید).
در BackgroundCanvas نیاز است که حالت Render Mode به Screen Space Camera تنظیم شود (یادتان باشد که دوربین اصلی خود را به آن متصل(attach) کنید.). پس از آن، حالت UI Scale Mode را روی Scale With Screen Size تنظیم کنید. با این کار طرح در پیش زمینه قرار خواهد گرفته و جلوی آبجکت های دیگر ظاهر نخواهد شد.
در بخش BackgroundImage تنها نیاز است که مسیر تصویر پیش زمینه را کنید.
حالا بازی را اجرا کنید. باید تصویر پیش زمینه را در بازی ببینید.
مدیریت شبکه
برای داشتن یک بازی چندنفره، ما به کامپوننت های GameObject و NetworkManager و NetworkManagerHUD نیاز داریم. پس به شروع به ساخت آن می کنیم.
این آبجکت مسئول مدیریت اتصال کلاینت های مختلف در یک بازی است و آبجکت های بازی را میان تمام کلاینت ها سینک می کند. بخش Network Manager HUD یک HUD ساده را برای اتصال به بازی به بازیکنان نشان می دهد.
برای مثال، اگر شما همین حالا بازی را اجرا کنید، باید تصویر زیر را ببینید:
در این آموزش قرار است که ما از گزینه های LAN Host و LAN Client استفاده کنیم. بازی های چندنفره یونیتی به این طریق کار می کنند: ابتدا یک بازیکن بازی را به عنوان Host شروع می کند (با LAN Host). یک هاست همزمان هم به عنوان کلاینت و هم به عنوان سرور کار می کند. سپس، دیگر بازیکنان می توانند به عنوان کلاینت به این هاست متصل شوند ( با LAN Client). کلاینت به سرور متصل می شود، اما هیچ کد مختص سرور را اجرا نمی کند. بنابراین، برای تست بازی مان قرار است دو نمونه داشته باشیم؛ یکی به عنوان هاست و دیگری به عنوان کلاینت.
با این حال، شما نمی توانید دو نمونه بازی را در ادیتور یونیتی باز کنید. برای انجام این کار، نیاز است که بازی را Build کنید و نمونه اول از طریق فایل exe اجرا کنید. حالا نمونه دوم را می توانید از طریق ادیتور یونیتی اجرا کنید (در حالت Play Mode)
برای این که بازی خود را Build کنید، نیاز است که یک Game Scene به بیلد اضافه کنید. پس به مسیر File> Build Setting رفته و Game Scene را به بیلد اضافه کنید. سپس می توانید فایل اجرایی را از مسیر File> Build & Run ساخته و اجرا کنید. این کار یک پنجره جدید در کنار بازی باز خواهد کرد. پس از انجام این کار، می توانید وارد بخش Play Mode در ادیتور یونیتی شوید تا نمونه دوم بازی را اجرا کنید. این کاری است که هر بار برای تست یک بازی چندنفره باید انجام دهیم.
حرکت سفینه فضایی
حالا که ما NetworkManager را داریم، می توانیم شروع به ساخت آبجکت هایی کنیم که با آن مدیریت می شوند. اولین چیزی که قرار است بسازیم سفینه بازیکن است.
در حال حاضر، سفینه تنها به صورت افقی در صفحه حرکت خواهد کرد و مختصات اش توسط NetworkManager به روز می شود. در مراحل بعد قابلیت شلیک گلوله و آسیب دیدن را هم به آن اضافه خواهیم کرد.
بنابراین اول از همه، یک گیم آبجکت جدید به نام Ship بسازید و آن را به یک Prefab تبدیل کنید. تصویر زیر اجزای Prefab سفینه را نشان می دهد که چیزی است که الان توضیح خواهیم داد.
برای این که یک گیم آبجکت توسط NetworkManager مدیریت شود، نیاز داریم تا کامپوننت NetworkIdentity را به آن اضافه کنیم. به علاوه، چون سفینه توسط بازیکن کنترل می شود، باید تیک گزینه Local Player Authority را نیز قرار دهیم.
کامپوننت NetworkTransform مسئول به روزرسانی مختصات سفینه در سرور و تمام کلاینت هاست. در صورت استفاده نکردن از آن، اگر سفینه را در صفحه حرکت دهیم، مکان اش در صفحه های دیگر بازیکنان تغییر نخواهد کرد. کامپوننت های NetworkIdentity و NetworkTransform دو کامپوننت ضروری برای امکانات بخش چندنفره هستند.
حالا، برای به دست گرفتن حرکت و برخوردها، نیاز داریم تا RigidBody2D و BoxCollider2D را اضافه کنیم. این را هم اضافه کنم که BoxCollider2D یک تریگر (با مقدار پیش فرض True) خواهد بود، چون ما نمی خواهیم که برخوردها فیزیک سفینه را تحت تاثیر قرار دهد
در نهایت، اسکریپت MoveShip را اضافه خواهیم کرد که یک پارامتر سرعت خواهد داشت. اسکریپت های دیگر بعدتر اضافه خواهد شد و در حال حاضر همین یک مورد را داریم.
اسکریپت MoveShip بسیار ساده است. در متد FixedUpdate مختصات حرکت را از محور افقی می گیریم و سرعت سفینه را بر اساس آن تنظیم می کنیم. با این وجود، دو مورد بسیار مهم و مربوط به شبکه وجود دارد که باید توضیح داده شوند.
اول، به طور معمول تمام اسکریپت ها در یک بازی ساخته شده با یونیتی از MonoBehaviour برای استفاده از API آن کمک می گیرند. برای این که از API شبکه استفاده کنیم، اسکریپت باید به جای MonoBehaviour از NetworkBehaviour بهره بگیرد. برای انجام این کار نیاز دارید که Namespace) شبکه را (با استفاده از UnityEngine.Networking) قرار دهید.
همچنین در یک بازی چندنفره در یونیتی، یک کد یکسان در تمام سمت ها (چه سرور و چه کلاینت) اجرا می شود. برای این که هر بازیکن تنها سفینه خودش را کنترل کند و به سفینه های دیگر دسترسی نداشته باشد، نیاز داریم که یک موقعیت شرطی (If Condition) در ابتدای متد FixedUpdate قرار دهیم که چک کند آیا بازیکن یک بازیکن محلی است یا خیر (اگر کنجکاو هستید که بازی چگونه بدون این شرط کار می کند، آن را پاک کنید. خواهید دید که موقع حرکت یک سفینه در صفحه، همه آن ها با هم حرکت می کنند).
قبل از بازی کردن، ما هنوز نیاز داریم به NetworkManager بگوییم که Prefab سفینه همان Prefab بازیکن است. ما این کار را با Prefab سفینه در قسمت Player Prefab در کامپوننت NetworkManager انجام می دهیم. با این کار، هر بار که بازیکن یک نمونه جدید از بازی را آغاز می کند، یک سفینه توسط NetworkManager به آن اختصاص داده می شود.
حالا شما می توانید بازی را امتحان کنید. حرکت سفینه باید بین دو طرف (کلاینت) هماهنگ باشد.
مختصات ظاهر شدن
تا حالا تمام سفینه ها در وسط صفحه ظاهر می شدند، با این حال این می تواند جالب باشد که چند نقطه ظهور از پیش تعریف شده داشته باشید, کاری که انجامش با API بخش چندنفره یونیتی واقعا آسان است.
ابتدا احتیاج داریم که یک گیم آبجکت جدید بسازیم تا موقعیت ظاهر شدن مان باشد و آن را در نقطه ظاهر شدن دلخواه قرار دهد. سپس ما کامپوننت NetworkStartPosition را به آن اضافه می کنیم. ما قرار است تا دو نقطه ظاهر شدن خلق کنیم؛ یکی در مختصات (4,4) و دیگری در مختصات (4,4).
حالا ما احتیاج داریم که تعریف کنیم چگونه NetworkManager آن موقعیت ها را استفاده کند. ما این کار را با تنظیم بخش Player Spawn Method انجام می دهیم. دو حالت برای این بخش وجود دارد؛ حالت Random و حالت Round Robin که در حالت اول، محل ظهور سفینه به طور تصادفی از میان یکی از مختصات از پیش تعیین شده می شود و در حالت دوم، به ترتیب بین مختصات می چرخد تا وقتی که همه آن ها استفاده شود و پس از آن دوباره از ابتدای لیست شروع می کند. در این جا ما به سراغ حالت Round Robin می رویم.
حالا می توانید دوباره بازی را اجرا کنید و ببینید که آیا سفینه ها در مختصات درست ظاهر می شوند یا خیر.
شلیک گلوله
مورد بعدی که می خواهیم به بازی مان اضافه کنیم، قابلیت شلیک گلوله توسط سفینه هاست. لازم به ذکر است که گلوله ها باید بین تمام نمونه ها هماهنگ باشد و به طور همزمان برای هر دو بازیکن اجرا شود.
اول از همه نیاز داریم تا Prefab گلوله را بسازیم؛ پس یک گیم آبجکت با نام Bullet ایجاد کرده و آن را به Prefab تبدیل می کنیم. برای مدیریت این مورد در شبکه، همچون سفینه به دو کامپوننت NetworkIdentity و NetworkTransform نیاز داریم. با این حال وقتی یک گلوله ایجاد می شود، نیازی نیست که مختصاتش در شبکه منتشر شود، چون مختصات با موتور فیزیکی به روز می شود. بنابراین میزان قسمت Network Send Rate را روی صفر می گذاریم تا از بارگذاری اضافی روی شبکه جلوگیری شود.
گلوله ها همچنین دارای سرعت هستند و بعدتر باید به دشمنان نیز برخورد کنند. بنابراین کامپوننت های RigidBody2D و CircleCollider2D را به Prefab اضافه می کنیم. لازم است دوباره یادآوری کنیم که CircleCollider2D یک تریگر (متغیر بین دو حالت فعال و غیر فعال) است.
حالا که ما Prefab گلوله را داریم، می توانیم یک اسکریپت برای شلیک گلوله ها به سفینه اضافه کنیم. این اسکریپت پارامترهایی برای سرعت گلوله و Prefab گلوله خواهد داشت.
اسکریپت ShootBullets نیز از NetworkBehaviour بهره می برد و در پایین نشان داده شده است. در متد به روزرسانی، بررسی می شود که آیا بازیکن دکمه Space (به عنوان دکمه شلیک) را فشار داده یا خیر و در صورت فشار دادن، متدی را فرا می خواند تا گلوله شلیک کند. این متد یک گلوله جدید ایجاد کرده، سرعت آن را تنظیم و پس از یک ثانیه (مدتی که طول می کشد تا از صفحه خارج شود) آن را نابود می کند.
در این جا لازم است تا چند نکته مربوط به شبکه در اسکریپت توضیح داده شود. ابتدا این که یک تگ [Command] بالای متد CmdShoot وجود دارد. این تگ و عبارت Cmd در ابتدای نام متد آن را به یک متد خاص تبدیل می کند که «فرمان» (Command) نام دارد. در یونیتی، فرمان متدی است که در سرور اجرا می شود، اگر چه در کلاینت فرا خوانده می شود. در این مورد، وقتی بازیکن تیری شلیک می کند، به جای فرا خواندن متد در کلاینت، بازی یک درخواست به سرور می فرستد و سرور فرمان را اجرا خواهد کرد.
مورد بعد این که در متد CmdShoot یک فراخوانی به NetworkServer.Spawn وجود دارد. متد Spawn یا همان ظاهرسازی مسئول ایجاد گلوله در تمامی نمونه ها و برای تمام کلاینت هاست. بنابراین کاری که CmdShoot انجام می دهد این است که یک گلوله در سرور ایجاد و سپس سرور آن را بین تمامی کلاینت ها تکثیر می کند. نکته مهم این که این امر تنها به این خاطر ممکن است که CmdShoot یک فرمان است و یک متد معمول نیست. اسکریپت مورد نظر در ادامه آمده است:
در نهایت نیاز داریم به Network Manager بگوییم که می تواند گلوله ها را ظاهر کند. ما این کار را با اضافه کردن Prefab گلوله به فهرست بخش Registered Spawnable Prefab انجام می دهیم.
حالا می توانید دوباره بازی را امتحان کرده و این بار به شلیک گلوله بپردازید. گلوله ها باید بین تمامی نمونه ها و کلاینت ها هماهنگ و همزمان باشند.
ظاهر کردن دشمنان
قدم بعد در بازی ما اضافه کردن دشمنان است.
ابتدا ما به یک Prefab برای دشمنان نیاز داریم. بنابراین یک گیم آبجکت جدید با نام Enemy ساخته و آن را به Prefab تبدیل می کنیم. همچون سفینه، دشمنان هم دارای RigidBody2D و BoxCollider2D هستند تا حرکات و برخوردها کنترل شوند. همچنین به NetworkIdentity و NetworkTransform نیاز است که توسط NetworkManager کنترل می شوند. در مراحل جلوتر یک اسکریپت نیز اضافه خواهیم کرد، اما تا به این جا همین کارها باید انجام شود.
حالا یک گیم آبجکت جدید با نام EnemySpawner می سازیم که یک NetworkIdentity نیز خواهد داشت. اما اینجا ما بخش Server Only را در کامپوننت می کنیم. با این کار، ظاهرکننده تنها در سرور موجود خواهد بود، چون نمی خواهیم که دشمنان در هر کلاینت ساخته شوند. این بخش یک اسکریپت نیز خواهد داشت که دشمنان را در یک فاصله معمول ظاهر می کند (پارامترها شامل Prefab دشمن، فاصله ظاهر شدن و سرعت دشمن است).
اسکریپت SpawnEnemies در پایین نشان داده شده است. نکته مهم این که در این جا از یک متد جدید در یونیتی استفاده می کنیم که OnStartServer نام دارد. این متد بسیار شبیه OnStart است، با این تفاوت که تنها در سرور فراخوانی می شود. وقتی این اتفاق می افتد، ما InvokeRepeating را فراخوانی می کنیم تا متد SpawnEnemy را هر یک ثانیه یک بار فراخوانی کند (بر طبق SpawnInterval).
متد SpawnEnemy یک دشمن جدید را در یک مختصات تصادفی ایجاد خواهد کرد و از NetworkServer.Spawn استفاده می کند تا آن را میان نمونه ها و کلاینت ها تکثیر کند. در نهایت، دشمن بعد از ده ثانیه نابود خواهد شد.
قبل از امتحان بازی، نیاز داریم تا Prefab دشمن را به فهرست بخش Registered Spawnable Prefabs اضافه کنیم.
حالا می توانید بازی را با وجود دشمنان امتحان کنید. لازم به ذکر است که بازی هنوز هیچ کنترل برخوردی ندارد، بنابراین نمی توانید به دشمنان شلیک کنید و این کار قدم بعدی مان خواهد بود.
آسیب رساندن
آخرین چیزی که قرار است به بازی مان اضافه کنیم، قابلیت صدمه زدن به دشمنان و متاسفانه، مردن و باختن به آن هاست. برای ساده نگه داشتن این آموزش، از یک اسکریپت یکسان برای سفینه و دشمنان استفاده می کنیم.
اسکریپتی که قرار است استفاده شود ReceiveDamage نام دارد که در پایین تر نشان داده شده است. این اسکریپت دارای پارامترهای maxHealth و enemyTag و destroyOnDeath است؛ پارامتر اول میزان سلامتی اولیه آبجکت را تعیین می کند، دومی برای تعیین چیزی که باعث آسیب می شود به کار می رود. مثلا مقدار enemyTag برای سفینه، دشمنان (Enemy) و برای دشمنان، گلوله (Bullet) است. با این کار، تنها گلوله به دشمن و تنها دشمن به سفینه شما آسیب می زند. پارامتر آخر هم تعیین می کند که آیا آبجکت پس از مردن نابود می شود یا دوباره ظاهر می گردد.
حالا به بررسی متدها می پردازیم. در متد آغازین، اسکریپت مقدار currentHealth را بالاترین میزان ممکن قرار می دهد و مختصات مکان اولیه را نگه می دارد (این مختصات برای وقتی که سفینه دوباره ظاهر می شود به کار می آید). همچنین اطلاع می دهد که تگ [SyncVar] در بالای تعریف currentHealth قرار گرفته است. این به معنی آن است که مقدار این ویژگی باید بین تمام نمونه ها یکسان و هماهنگ باشد. بنابراین اگر سفینه یک بازیکن آسیب ببیند، این آسیب در سرور و کلاینت های دیگر نیز دیده می شود.
متد OnTriggerEnter2D مسئول کنترل و مدیریت برخوردهاست (چون برخوردکننده ها به صورت تریگر تنظیم شده اند). ابتدا، چک می کنیم که تگ برخوردکننده همان باشد که در enemyTag دنبالش هستیم تا برخوردهای در برابر آبجکت مورد نظر را کنترل کند (دشمنان در برابر سفینه و گلوله در برابر دشمنان). پس از آن متد TakeDamage را فرا می خوانیم و آبجکت دیگر که آسیب رسانده را نابود می کنیم.
متد TakeDamage در نوبت خود تنها در سرور فراخوانی می شود، چون که currentHealth از نوع SyncVar است. بنابراین تنها در سرور به روز می شود و بین کلاینت ها آن را هماهنگ می کنیم. به غیر از این، متد TakeDamage ساده است؛ مقدار currentHealth را کاهش می دهد و چک می کند که کمتر مساوی صفر هست یا خیر. اگر این شرط برقرار شود آن را نابود می کند و مقدار destroyOnDeath را True می کند و در این صورت، مقدار currentHealth ریست و آبجکت دوباره در صفحه ظاهر می شود. برای تمرین، ما دشمنان را به گونه ای قرار خواهیم داد که در هنگام مرگ نابود شده و سفینه را به گونه ای خواهیم گذاشت که پس از مرگ دوباره ظاهر می شوند.
آخرین متد مربوط به دوباره ظاهر شدن (Respawn) است. در این جا از یکی دیگر از امکانات مخصوص بخش چندنفره به اسم ClientRpc استفاده می کنیم (به تگ [ClientRpc] در بالای تعریف متد توجه کنید). این تگ در واقع عملکردی مخالف عملکرد تگ Command یا فرمان دارد؛ فرمان ها از کلاینت به سرور فرستاده می شود، اما ClientRpc در کلاینت اجرا می شود. حتی اگر متد از سوی سرور فراخوانی شده باشد. بنابراین وقتی نیاز است تا یک آبجکت دوباره ظاهر شود، سرور یک درخواست به کلاینت می فرستد تا متد RpcRespawn را اجرا کند (اسم متد باید با Rpc شروع شود) که به سادگی مختصات را به حالت اولیه بر می گرداند. این متد باید در کلاینت اجرا شود، چرا که ما می خواهیم آن را برای سفینه ها فراخوانی کنیم و سفینه ها تنها توسط بازیکن ها کنترل می شود (ما مقدار بخش Local Player Authority را در کامپوننت NetworkIdentity روی True قرار داده ایم).
در نهایت، نیاز است تا این اسکریپت را به Prefab های سفینه و دشمن اضافه کنیم. حواس تان باشد که باید تگ دشمن را برای سفینه Enemy و برای دشمن Bullet قرار دهید (احتمالا نیاز خواهید داشت تا تگ های Prefab ها را نیز تعریف کنید). همچنین در Prefab دشمن باید ویژگی Destroy on Death را فعال قرار دهیم.
حالا می توانید بازی را امتحان کرده و به دشمنان شلیک کنید. بگذارید دشمنان به سفینه شما برخورد کنند و مطمئن شوید که آیا سفینه ها بعد از نابودی به درستی ظاهر خواهند شد یا خیر.
هر گونه انتقاد یا پیشنهاد در رابطه با این مطلب را در بخش کامنت ها در میان بگذارید.