بخش هشتم آموزشی یونتی متوسطه (آموزش سیو بازی)
آخرین به روزرسانی در 21/10/2022
در بخش هشتم از آموزش یونتی متوسطه قرار داریم که در این بخش قصد آموزش سیو بازی را داریم.
قابلیت سیو و لود بازی از آن دسته قابلیت هایی است که جزوه پایه های اصلی یک بازی محسوب می شود.
تقریبا در حال حاضر شما در هیچ بازی ای نخواهید دید که قابلیت سیو کردن در آن وجود نداشته باشد.
پس تا پایان با ما همراه باشید تا با یک آموزش پروژه محور و جامع برای آموزش سیو بازی در خدمت شما باشیم.
پیش نیاز این آموزش تسلط به مبحث اسکریپت نویسی در سی شارپ می باشد.
پیش نیاز دوم این دوره ی آموزشی تسلط و درک مفاهیم اولیه ی زبان برنامه نویسی C# می باشد.
مفاهیم مهم ذخیره سازی بازی
چهار مفهوم کلیدی برای آموزش سیو بازی در یونیتی وجود دارد ، که عبارتند از :
PlayerPrefs : این یک سیستم کش ویژه برای پیگیری تنظیمات ساده برای بازیکن در بین مراحل بازی است.
بسیاری از برنامه نویسان جدید این اشتباه را مرتکب می شوند که فکر می کنند می توانند از این سیستم به عنوان یک سیستم ذخیره بازی نیز استفاده کنند، اما انجام این کار قطعا تجربه ی بدی است.
PlayerPrefs باید تنها برای ردیابی چیزهای ساده مانند گرافیک ، تنظیمات صدا ، اطلاعات ورود به سیستم یا سایر دادههای اساسی مرتبط با کاربر استفاده شود.
Serialization : این جادوی است که یکی از هسته های یونیتی می باشد.
Serialization تبدیل یک آبجکت به جریانی از بایت است.
ممکن است مبهم به نظر برسد اما نگاهی گذرا به این تصویر زیر بیندازید :
آبجکت چیست؟
در این مورد یک ابجکت ، هر اسکریپت یا فایلی در Unity است.
در واقع، هر زمان که یک اسکریپت MonoBehaviour ایجاد میکنید، Unity از Serialization و deserialization برای تبدیل آن فایل به کد C++ و سپس بازگشت به کد C# که در inspector window میبینید، استفاده میکند.
اگر تا به حال [SerializeField] را اضافه کرده اید تا چیزی در inspector ظاهر شود ، اکنون ایده ای از آنچه در حال وقوع است دارید.
توجه : اگر شما یک توسعه دهنده جاوا یا وب هستید ، ممکن است با مفهومی به نام marshalling آشنا باشید.
Serialization و marshalling تقریباً مترادف هستند، اما اگر میپرسید چه تفاوتی وجود دارد، می توان گفت Serialization در مورد تبدیل یک آبجکت از یک شکل به شکل دیگر (مثلاً یک آبجکت به بایت) است ، در حالی که marshalling به معنای دریافت پارامترها از یک مکان به مکان دیگر است.
Deserialization : این دقیقا مفهومی برعکس Serialization دارد ، یعنی تبدیل یک جریان از بایت ها به یک آبجکت.
JSON : این مخفف عبارت JavaScript Object Notation است ، که فرمتی مناسب برای ارسال و دریافت دادههایی است که زبانی agnostic هستند.
برای مثال، ممکن است یک وب سرور در جاوا یا PHP داشته باشید.
شما نمی توانید فقط یک آبجکت C# را روی آن بفرستید، بلکه می توانید یک نمایش JSON از آن آبجکت بفرستید و به سرور اجازه دهید یک نسخه محلی شده از آن را ایجاد کند.
در بخش آخر درباره این قالب بیشتر خواهید آموخت ، اما در حال حاضر فقط بدانید که این روشی است برای قالببندی دادهها برای خوانایی آنها در چند پلتفرم (مانند XML) ، استفاده می شود.
هنگامی که با تبدیل به JSON سروکار داریم، اصطلاحات به ترتیب عبارتند از JSON serialization و JSON deserialization.
Player Prefs
خب برای شروع کار آموزش سیو بازی این پروژه را دانلود کنید.
پروژه را باز کنید، سپس صحنه با نام Game را باز کنید و روی play کلیک کنید.
برای شروع یک بازی، روی دکمه بازی جدید کلیک کنید.
برای انجام بازی، شما به سادگی ماوس خود را حرکت دهید و اسلحه حرکت شما را دنبال می کند.
با کلیک بر روی دکمه سمت چپ ماوس یک گلوله شلیک کنید به اهداف (که در فواصل زمانی مختلف بالا و پایین می چرخند) ضربه بزنید تا امتیاز بگیرید.
اینکار را امتحان کنید و ببینید در 30 ثانیه چقدر می توانید نمره بالایی کسب کنید.
برای نمایش منو در هر زمان، کلید escape را فشار دهید.
همانطور که این بازی می تواند سرگرم کننده باشد ، اما بدون استفاده از موسیقی چندان جالب به نظر نمی رسد.
ممکن است متوجه شده باشید که یک music toggle وجود دارد ، اما خاموش بود.
برای شروع یک بازی جدید، روی پخش کلیک کنید، اما این بار روی دکمه موسیقی کلیک کنید تا روی «On» تنظیم شود و وقتی بازی خود را شروع میکنید، موسیقی میشنوید.
مطمئن شوید که بلندگوهای شما روشن هستند.
تغییر تنظیمات موسیقی ساده بود، اما دوباره روی دکمه play کلیک کنید و متوجه یک مشکل خواهید شد :
در حالی که قبلاً تنظیمات موسیقی را تغییر دادید ، هیچ چیزی این تغییر را ذخیره نکرد. این همان چیزی است که PlayerPrefs در آن برتری دارد.
یک اسکریپت جدید به نام PlayerSettings در پوشه Scripts ایجاد کنید.
از آنجایی که از برخی از عناصر UI استفاده می کنید، خط زیر را در بالای فایل با namespaces دیگر اضافه کنید:
using UnityEngine.UI;
سپس ، متغیرهای زیر را اضافه کنید:
[SerializeField]
private Toggle toggle;
[SerializeField]
private AudioSource myAudio;
اینها آبجکت های Toggle و AudioSource را ردیابی می کنند.
حال تابع زیر را اضافه کنید:
public void Awake ()
{
// 1
if (!PlayerPrefs.HasKey("music"))
{
PlayerPrefs.SetInt("music", 1);
toggle.isOn = true;
myAudio.enabled = true;
PlayerPrefs.Save ();
}
// 2
else
{
if (PlayerPrefs.GetInt ("music") == 0)
{
myAudio.enabled = false;
toggle.isOn = false;
}
else
{
myAudio.enabled = true;
toggle.isOn = true;
}
}
}
هنگام تنظیم، این کار:
1- بررسی کنید که آیا PlayerPrefs یک cached setting برای کلید “music” دارد یا خیر.
اگر مقداری وجود نداشته باشد، یک جفت key-value برای کلید music با مقدار 1 ایجاد میکند.
همچنین کلید را روی on تنظیم میکند و AudioSource را فعال میکند.
این اولین باری است که پلیر بازی را پلی می کند.
از مقدار 1 استفاده می شود زیرا نمی توانید یک Boolean را ذخیره کنید (اما می توانید از 0 به عنوان false و 1 به عنوان true استفاده کنید).
2- این کلید “music” ذخیره شده در PlayerPrefs را بررسی می کند.
اگر مقدار روی 1 تنظیم شود ، PlayerPrefs را روشن کرده است ، بنابراین موسیقی را فعال می کند و کلید را روی on تنظیم می کند.
در غیر این صورت، موسیقی را off می کند و toggle را غیرفعال می کند.
حالا تغییرات را در اسکریپت خود ذخیره کنید و به Unity برگردید.
اسکریپت PlayerSettings را به Game GameObject اضافه کنید.
سپس UI GameObject را باز کنید، سپس منو GameObject را باز کنید تا فرزندان خود را نشان دهد.
حال Music GameObject را به قسمت Toggle اسکریپت PlayerSettings بکشید.
سپس Game GameObject را انتخاب کنید و AudioSource را به قسمت MyAudio بکشید.
هنگام اجرای بازی ، موسیقی به گونهای تنظیم میشود که کار کند (زیرا کدی در عملکرد Awake وجود دارد) ، اما اگر player تنظیمات را در حین بازی تغییر داد ، همچنان باید کد را اضافه کنید.
اسکریپت PlayerSettings را باز کنید و تابع زیر را اضافه کنید:
public void ToggleMusic()
{
if (toggle.isOn)
{
PlayerPrefs.SetInt ("music", 1);
myAudio.enabled = true;
}
else
{
PlayerPrefs.SetInt ("music", 0);
myAudio.enabled = false;
}
PlayerPrefs.Save ();
}
این تقریباً مشابه کدی است که قبلاً نوشتید، اما با یک تفاوت مهم.
این کد وضعیت music toggle را بررسی می کند و سپس تنظیمات ذخیره شده را متناسب با آن به روز می کند.
برای اینکه این متد فراخوانی شود و در نتیجه بتواند کار خود را انجام دهد، باید متد callback را روی Toggle GameObject تنظیم کنید.
Music GameObject را انتخاب کنید و Game GameObject را روی فیلد ابجکت در بخش OnValueChanged بکشید:
منوی dropdown را که در حال حاضر No Function میگوید انتخاب کنید و ()PlayerSettings -> ToggleMusic را انتخاب کنید.
هنگامی که دکمه toggle در منو فشار داده می شود ، تابع ToggleMusic را فراخوانی می کند.
اکنون مواردی را برای ذخیره ی تنظیمات موسیقی تنظیم کردهاید.
روی Play کلیک کنید و با on یا off کردن دکمه موزیک و سپس پایان دادن به play session و شروع یک play session جدید، آن را امتحان کنید.
تنظیمات موسیقی اکنون به درستی ذخیره شده است.
ذخیره ی بازی
در بخش قبلی آموزش سیو بازی استفاده از PlayerPrefs بسیار ساده بود، اینطور نیست؟ با استفاده از آن، میتوانید تنظیمات دیگری مانند تنظیمات گرافیکی پلیر،
یا اطلاعات ورود به سیستم (شاید توکنهای فیسبوک یا توییتر) و هر تنظیمات پیکربندی دیگری که برای پیگیری آن برای پلیر منطقی است را به راحتی در آن ذخیره کنید.
با این حال و تمامی این تفاسیر ، PlayerPrefs برای پیگیری ذخیرههای بازی طراحی نشده است.
برای ذخیره سازی ، ما می خواهیم از serialization استفاده کنیم.
اولین قدم برای ایجاد یک فایل ذخیره بازی، ایجاد کلاس فایل Save است.
یک اسکریپت به نام Save ایجاد کنید و MonoBehaviour را حذف کنید.
متدهای پیش فرض ()Start و ()Update را نیز حذف کنید.
بعد، متغیرهای زیر را اضافه کنید:
public List livingTargetPositions = new List();
public List livingTargetsTypes = new List();
public int hits = 0;
public int shots = 0;
برای ذخیره بازی باید ردیابی کنید که ربات های موجود کجا هستند و چه نوع هستند.
این دو لیست این کار را انجام می دهند.
برای تعداد ضربات و عکسها، فقط میخواهید آنها را بهعنوان ints ذخیره کنید.
یک بیت کد بسیار مهم دیگر وجود دارد که باید اضافه کنید.
بالای اعلان کلاس، خط زیر را اضافه کنید:
[System.Serializable]
این به عنوان یک attribute شناخته می شود و یک metadata برای کد شما است.
این به یونیتی میگوید که این کلاس را میتوان serialized کرد، به این معنی که میتوانید آن را به جریانی از بایت تبدیل کنید و در یک فایل روی دیسک ذخیره کنید.
توجه : attribute ها طیف وسیعی از کاربردها را دارند و به شما امکان می دهند داده ها را به یک کلاس، متد یا متغیر متصل کنید (این داده به عنوان metadata شناخته می شود).
شما حتی می توانید attribute های خود را برای استفاده در کد خود تعریف کنید.
serialized از attribute های [SerializeField] و [System.Serializable] استفاده می کند تا بداند هنگام attribute شی چه چیزی بنویسد.
کاربردهای دیگر attribute ها شامل تنظیمات برای تست های unit و dependency injection است که بسیار فراتر از محدوده این آموزش هستند اما صرفا در حد اطلاعات عمومی برای این مرحله بدانید.
کل اسکریپت Save باید به شکل زیر باشد :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Save
{
public List livingTargetPositions = new List();
public List livingTargetsTypes = new List();
public int hits = 0;
public int shots = 0;
}
سپس اسکریپت Game را باز کنید و متد زیر را اضافه کنید:
private Save CreateSaveGameObject()
{
Save save = new Save();
int i = 0;
foreach (GameObject targetGameObject in targets)
{
Target target = targetGameObject.GetComponent();
if (target.activeRobot != null)
{
save.livingTargetPositions.Add(target.position);
save.livingTargetsTypes.Add((int)target.activeRobot.GetComponent().type);
i++;
}
}
save.hits = hits;
save.shots = shots;
return save;
}
این کد نمونه ای از کلاس Save را که قبلا ساخته اید ایجاد می کند و سپس مقادیر روبات های موجود را تنظیم می کند.
همچنین ضربات بازیکنان را ذخیره می کند.
دکمه Save به روش SaveGame در اسکریپت Game متصل شده است، اما هنوز کدی در SaveGame وجود ندارد.
تابع SaveGame را با کد زیر جایگزین کنید :
public void SaveGame()
{
// 1
Save save = CreateSaveGameObject();
// 2
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath + "/gamesave.save");
bf.Serialize(file, save);
file.Close();
// 3
hits = 0;
shots = 0;
shotsText.text = "Shots: " + shots;
hitsText.text = "Hits: " + hits;
ClearRobots();
ClearBullets();
Debug.Log("Game Saved");
}
در نظر گرفتن آن نظر به نظر:
1- یک نمونه Save ایجاد کنید که تمام داده های session جاری در آن ذخیره شده است.
2- یک BinaryFormatter و یک FileStream با عبور دادن مسیری برای Save instance ایجاد کنید تا در آن ذخیره شود.
داده ها را serializes می کند (به بایت) و روی دیسک می نویسد و FileStream را می بندد.
اکنون فایلی به نام gamesave.save در رایانه شما وجود خواهد داشت.
از .save فقط به عنوان مثال استفاده شد و می توانید از هر پسوندی برای نام ذخیره فایل استفاده کنید.
3- این فقط بازی را ریست می کند تا پس از ذخیره پلیر، همه چیز در حالت پیش فرض قرار گیرد.
برای ذخیره بازی، در هر زمان در حین بازی Escape را فشار دهید و روی دکمه Save کلیک کنید.
باید متوجه شوید که همه چیز ریست می شود و console output یادداشتی را نشان می دهد که بازی ذخیره شده است.
LoadGame در اسکریپت Game به دکمه Load متصل است.
اسکریپت بازی را باز کنید و تابع LoadGame را پیدا کنید.
آن را با موارد زیر استفاده کنید :
public void LoadGame()
{
// 1
if (File.Exists(Application.persistentDataPath + "/gamesave.save"))
{
ClearBullets();
ClearRobots();
RefreshRobots();
// 2
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/gamesave.save", FileMode.Open);
Save save = (Save)bf.Deserialize(file);
file.Close();
// 3
for (int i = 0; i < save.livingTargetPositions.Count; i++)
{
int position = save.livingTargetPositions[i];
Target target = targets[position].GetComponent();
target.ActivateRobot((RobotTypes)save.livingTargetsTypes[i]);
target.GetComponent().ResetDeathTimer();
}
// 4
shotsText.text = "Shots: " + save.shots;
hitsText.text = "Hits: " + save.hits;
shots = save.shots;
hits = save.hits;
Debug.Log("Game Loaded");
Unpause();
}
else
{
Debug.Log("No game saved!");
}
}
1- بررسی می کند که آیا فایل ذخیره وجود دارد.
اگر این کار را انجام دهد، روبات ها و امتیاز را پاک می کند.
در غیر این صورت به کنسول وارد می شود که هیچ بازی ذخیره شده ای وجود ندارد.
2- مشابه کاری که هنگام ذخیره بازی انجام دادید، دوباره یک BinaryFormatter ایجاد می کنید، فقط این بار به جای نوشتن، جریانی از بایت ها را برای خواندن در اختیار آن قرار می دهید.
بنابراین شما به سادگی آن را به مسیر فایل ذخیره منتقل می کنید.
آبجکت Save را ایجاد می کند و FileStream را می بندد.
3- حتی اگر اطلاعات ذخیره را دارید، هنوز باید آن را به حالت بازی تبدیل کنید.
این کد از طریق پوزیشن های ذخیره شده ربات (برای living robots) حلقه می زند و یک ربات در آن موقعیت اضافه می کند.
همچنین آن را به نوع مناسب تنظیم می کند.
برای سادگی، تایمرها تنظیم مجدد می شوند ، اما در صورت تمایل می توانید آن را حذف کنید.
این کار از ناپدید شدن روباتها به سرعت جلوگیری میکند و به بازیکن چند ثانیه فرصت میدهد تا در جهان جهتگیری کند.
همچنین، برای سادگی، انیمیشن حرکت روبات به سمت بالا به پایان رسیده است ، به همین دلیل است که رباتهایی که تا حدی به سمت بالا حرکت میکنند، زمانی که شما ذخیره میکنید، در هنگام بارگیری بازی به صورت کاملاً بالا نشان داده میشوند.
4- این UI را بهروزرسانی میکند تا ضربات و شوتهای مناسب را تنظیم کند، و متغیرهای لوکال را طوری تنظیم میکند که وقتی بازیکن شلیک میکند یا به هدفی میخورد، همچنان روی مقدار قبلی حساب میشود .
اگر این مرحله را انجام ندادید، دفعه بعد که بازیکن هدفی را شلیک یا ضربه زد، مقادیر نمایش داده شده روی 1 تنظیم می شود.
روی Play کلیک کنید، کمی بازی کنید و سپس ذخیره کنید.
روی دکمه Load کلیک کنید و خواهید دید که دشمنان را همانطور که قبلاً در هنگام ذخیره بازی تنظیم شده بودند بارگذاری می کند.
همچنین امتیاز شما و تیرهایی را که شلیک کرده اید به درستی تنظیم می کند.
ذخیره داده ها با JSON
در آموزش سیو بازی ما یک ترفند دیگر وجود دارد که می توانید هنگام ذخیره داده ها از آن استفاده کنید و آن JSON است.
می توانید یک نمایش لوکال JSON از ذخیره بازی خود ایجاد کنید، آن را به یک سرور ارسال کنید، سپس آن JSON (به عنوان یک رشته) را به دستگاه دیگری دریافت کنید و آن را از یک رشته به JSON تبدیل کنید.
این آموزش ارسال/دریافت از وب را پوشش نمی دهد، اما دانستن نحوه استفاده از JSON بسیار مفید است ، و فوق العاده ساده است.
فرمت JSON ممکن است کمی متفاوت از آن چیزی باشد که ممکن است از کد C# استفاده کنید، اما بسیار ساده است.
در اینجا یک مثال ساده JSON وجود دارد:
{
"message":"hi",
"age":22
"items":
[
"Broadsword",
"Bow"
]
}
براکت های بیرونی نشان دهنده موجودیت والد است که JSON است.
اگر با ساختار داده Dictionary آشنا هستید، JSON نیز مشابه است.
یک فایل JSON دارای key و value است.
بنابراین مثال بالا دارای 3 جفت key –value است.
با JSON، کلیدها همیشه رشتهای هستند، اما مقادیر میتوانند آبجکت ها (به عنوان مثال، آبجکت های children JSON)، آرایهها، اعداد یا رشتهها باشند.
مقدار تنظیم شده برای کلید “message” “hi”، مقدار کلید “age” عدد 22 و مقدار کلید “items” آرایه ای با دو رشته است.
خود آبجکت JSON با یک نوع String نشان داده می شود.
با ارسال این داده به عنوان یک رشته ، هر زبانی می تواند به راحتی آبجکتJSON را از رشته به عنوان آرگومان سازنده دوباره ایجاد کند.
بسیار راحت و بسیار ساده.
هر زبان روش خاص خود را برای ایجاد یک آبجکت از این قالب دارد.
از زمان Unity 5.3، یک متد بومی برای ایجاد یک آبجکت JSON از یک رشته JSON وجود دارد.
شما یک نمایش JSON از امتیاز بالای پلیر ایجاد می کنید و سپس آن را در کنسول چاپ می کنید.
اما شما این منطق را با ارسال JSON به سرور گسترش می دهید.
اسکریپت بازی دارای متدی به نام SaveAsJSON است که به دکمه Save As JSON متصل است.
کد زیر را جایگزین SaveAsJSON کنید:
public void SaveAsJSON()
{
Save save = CreateSaveGameObject();
string json = JsonUtility.ToJson(save);
Debug.Log("Saving as JSON: " + json);
}
این نمونه Save را مانند قبل ایجاد می کند.
سپس با استفاده از متد ToJSON در کلاس JsonUtility یک رشته JSON ایجاد می کند.
سپس خروجی را در کنسول چاپ می کند.
یک بازی را شروع کنید ، به چند هدف ضربه بزنید، سپس Escape را فشار دهید تا منو ظاهر شود.
روی دکمه Save As JSON کلیک کنید و رشته JSON را که ایجاد کرده اید خواهید دید:
اگر می خواهید آن JSON را به یک نمونه Save تبدیل کنید، به سادگی از این موارد استفاده کنید:
Save save = JsonUtility.FromJson(json);
اگر بخواهید یک فایل ذخیره شده را از وب دانلود کنید و سپس آن را در بازی خود بارگذاری کنید، این همان کاری است که انجام می دهید.
اما راه اندازی وب سرور یک فرآیند کامل دیگر است.
مهرشاد شادان مهر
مدرس سئو ، طراح سایت ، انیماتور
قهرمان زندگی شما در چند سال آینده ی شما می باشد