نیمچه برنامه نویس و توسعه دهنده بازی
الگو های طراحی در یونیتی
Observer Pattern in Unity
در دنیای برنامه نویسی Design Pattern های گوناگونی وجود دارند که همگی آنها به خوانایی، زیبا تر شدن و عملکرد بهتر کد ما کمک می کنند.
می خواهم راجب یکی از پرکاربرد ترین Design Pattern هایی که راجب آن بسیار صحبت میکنند با شما اطلاعاتی را به اشتراک بگذارم و با هم به بررسی یک مثال تشریحی و یک مثال عملی بپردازیم.
- مثال تشریحی
فرض کنید که ما یک بازی ساخته اییم که داخل آن کاراکتر ما یک قدرتی به نام Rampage دارد که اگر این قدرت را بدست بیاورد دشمنان کاراکتر ما وحشت زده می شوند و نه تنها به کاراکتر حمله نمی کنند بلکه از او فرار نیز میکنند علاوه بر این ما تغییراتی در Health کاراکتر و UI بازی نیز داریم.( به عنوان مثال UI قرمز رنگ می شود)
حال زمانی که Rampage اتفاق بیوفتد می بایست به تمامی دشمنان و کلاس های Health و UI بازی اطلاع داده بشود که این حالت اتفاق افتاده است و دچار تغییر بشوید یا به اصطلاح دیگر تابعی را در این کلاس ها صدا بزنیم تا تغییرات به آنها اعمال بشوند.
برای ساخت چنین چیزی یکی از راه های پیش روی ما این هست که در تابع Update این کلاس ها در هر فریم از بازی بیایم فعال یا غیر فعال بودن Rampage را چک بکنیم تا زمانی که Rampage فعال شد کلاس های ما مطلع بشوند و شروع به فراخوانی توابع مورد نیاز بکنند.
راه حل اول راه بدی نیست اما این فرآیند چک کردن Rampage که آیا فعال شده است یا خیر در تمامی فریم های بازی ما اتفاق میوفتد در صورتی که ما به چنین چیزی نیاز نداریم و در بیشتر مواقع الکی CPU را درگیر میکند.
در چنین شرایطی بحث Observer Pattern به میان می آید.
در این Design Pattern ما یک آبجکت به نام Subject داریم که آبجکت های گوناگونی به آن وابسته هستند، به آن آبجکت هایی که به Subject ما وابسته هستند Observer (مشاهده گر) می گویند.
نحوه عملکرد Observer Pattern به این گونه هست که تمامی Observer ها منتظر دریافت Notification از Subject هستند تا پس از دریافت آن توابع مورد نیاز را فراخوانی کنند.
در نمودار UML زیر نیز می توانید آن را مشاهده کنید.
با توجه به عملکرد Observer Pattern می توان گفت Rampage بازی ما همان Subject و دشمنان، UI و Health نیز Observer می باشند.
- مثال عملی
حال برای اینکه با این الگوی طراحی بهتر آشنا بشوید به سراغ یک مثال عملی هم میرویم.
می خواهیم راجب یک بازی کوتاه صحبت بکنیم که در آن ما می توانیم امتیازی تحت عنوان تجربه بدست بیاوریم و به واسطه آن سطح خود را داخل این بازی بالا ببریم.
در این برنامه ما سه کلاس به نام های Level، Health و Debugger داریم که به شرح زیر می باشند.
public class Level : MonoBehaviour
{
[SerializeField] int pointsPerLevel = 200;
int experiencePoints;
IEnumerator Start()
{
while (true)
{
yield return new WaitForSeconds(0.5f);
GetExperience(10);
}
}
public void GetExperience(int points)
{
int levl = GetLevel();
experiencePoints += points;
}
public int GetExperience()
{
return experiencePoints;
}
public int GetLevel()
{
return experiencePoints / pointsPerLevel;
}
}
public class Health : MonoBehaviour
{
[SerializeField] float fullHealth = 100f;
[SerializeField] float drainPerSecond = 2f
float currentHealth = 0;
int lastLevel;
void Awake()
{
RestHealth();
StartCoroutine(HealthDrain());
}
public float GetHealth()
{
return currentHealth;
}
void RestHealth()
{
currentHealth = fullHealth;
}
IEnumerator HealthDrain()
{
while (currentHealth > 0)
{
currentHealth -= drainPerSecond;
yield return new WaitForSeconds(1f);
}
}
}
public class Debugger : MonoBehaviour
{
IEnumerator Start()
{
Level level = GetComponent<Level>();
Health health = GetComponent<Health>();
while (true)
{
yield return new WaitForSeconds(1f);
Debug.Log($"Exp: {level.GetExperience()}, Level: {level.GetLevel()}, Health {health.GetHealth()}");
}
}
}
حال می خواهیم که هروقت سطح بازی بالا رفت، میزان Health نیز reset شود.
- راه حل اول: در این راه کد زیر را به تابع GetExperience اضافه میکنیم و تابع ResetHealth را در کلاس Health هم public میکنیم.
int level = GetLevel();
if (GetLevel() > level)
{
GetComponent<Health>().RestHealth();
}
یکی از برگ ترین مشکلات راه اول این هست که ما تمام کامپوننت Health را داریم در کلاس Level دریافت میکنیم در صورتی که ما تنها به یک تابع از کلاس Health نیاز داریم.
- راه حل دوم: در این راه به کلاس Health یک متغیر از جنس int به نام lastLevel و توابع زیر را اضافه میکنیم.
void Start()
{
lastLevel = GetComponent<Level>().GetLevel();
}
void Update()
{
int currentlevel = GetComponent<Level>().GetLevel();
if (currentlevel > lastLevel)
{
lastLevel = currentlevel;
RestHealth();
}
}
از مشکلات بزرگ این راه نیز می توان به تابع آپدیت این کلاس اشاره کرد که حتی در فریم هایی از بازی که ما نیازی به چک کردن level نداریم هم level را چک میکند.
- راه سوم با استفاده از Observer Pattern: ابتدا تمام مواردی که در راه های گذشته به کد خود اضافه کرده اییم را حذف میکنیم سپس در بالای کلاس Level دستور زیر را وارد میکنیم.
Using UnityEngine.Events;
بعد از وارد کردن دستور بالا دو متغیر زیر را نیز در بالای کلاس تعریف میکنیم.
UnityEvent onLevelUp;
public event Action onLevelUpAction;
- سپس کد های زیر را به تابع GetExperience اضافه مکنیم.
if (GetLevel() > levl)
{
onLevelUp.Invoke();
if (onLevelUpAction != null)
{
onLevelUpAction();
}
}
و در نهایت نیز تابع زیر را به کلاس Health اضافه میکنیم.
private void OnEnable()
{
GetComponent<Level>().onLevelUpAction += RestHealth;
}
با استفاده از مراحلی که در راه سوم طی کردیم ما توانستیم از Observer Pattern در بازی خود استفاده بکنیم و همانطور هم که میبینید مشکلات دو راه قبلی را ندارد، تمیز تر و کارایی بالاتری هم دارد.
در مثال عملی بالا کلاس Health همان Observer است که به کلاس Level وابسته می باشد و کلاس Level نیز Subject است که با استفاده از Event ها و زمانی که آن Event احضار یا invoked می شود به Observer اطلاع میدهد که الا باید onLevelUpAction را اجرا بکند، در کلاس Health نیز مشاهده میکنید که به Action مورد نظر به نام onLevelUpAction تابع ResetHealth را اضافه میکند تا زمانی که این Action در کلاس Level صدا زده میشود تابع ResetHealth اجرا بشود .
امیدوارم که مثال های فوق توانسته باشند که این الگوی طراحی را برای شما به خوبی شرح بدهند و بتوانید از این الگو در بازی های خود استفاده بکنید.
سینا کریمی
مطلبی دیگر از این انتشارات
دیزاین پترن Observer در یونیتی
مطلبی دیگر از این انتشارات
موتور بازی ساز چیست؟ با موتورهای بازی ساز برتر جهان آشنا شوید!
مطلبی دیگر از این انتشارات
معماری Scriptable Object بخش اول - ستون های مهندسی