מצבים כמחלקות
חלק מהיחידה ארכיטקטורת משחקים ב-Unity במסלול ה-Unity של Coddy. שיעור 28 מתוך 50.
מתג על enum ביחידה 3 משאיר את העבודה של כל מצב בתוך Update אחד. הפתרון הזה מפסיק להיות מדרגי כשמצבים מכילים נתונים משלהם: טיימר שמשמש רק מצב אחד הופך לשדה של MonoBehaviour, לצד השדות של כל שאר המצבים, וכל case חייב לזכור לאפס את מה שהוא משתמש בו. תבנית המצבים מ-C# מעניקה לכל מצב מחלקה משלו, וכל מחלקה מממשת ממשק אחד. משואת הדרך הזאת מהבהבת או נשארת דולקת, ומקש Space מחליף בין שני המצבים:
public interface IState
{
void Enter();
void Tick();
void Exit();
}
public class BlinkState : IState
{
private Renderer lamp;
private float timer;
public BlinkState(Renderer lamp)
{
this.lamp = lamp;
}
public void Enter() { timer = 0f; }
public void Tick()
{
timer += Time.deltaTime;
if (timer >= 0.5f)
{
timer = 0f;
lamp.enabled = !lamp.enabled;
}
}
public void Exit() { lamp.enabled = true; }
}
public class SteadyState : IState
{
public void Enter() { }
public void Tick() { }
public void Exit() { }
}
public class Beacon : MonoBehaviour
{
public Renderer lamp;
private IState blink;
private IState steady;
private IState current;
void Start()
{
blink = new BlinkState(lamp);
steady = new SteadyState();
SetState(steady);
}
void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame) SetState(current == steady ? blink : steady);
current.Tick();
}
void SetState(IState next)
{
if (current != null) current.Exit();
current = next;
current.Enter();
}
}המצבים הם מחלקות C# רגילות, לא MonoBehaviours. יוצרים אותם באמצעות new ומעבירים להם את מה שהם צריכים דרך הבנאי — במקרה הזה, ה-Renderer של המנורה — כי הם אינם רכיבים ואינם יכולים לקרוא ל-GetComponent או להציג שדות ב-Inspector. גם Unity לא קוראת למתודות שלהם: Beacon הוא ה-MonoBehaviour, ולכן Update שלו קוראת ל-current.Tick() בכל פריים.
ה-timer שייך רק ל-BlinkState, והמתודה Enter שלו מאפסת אותו ל-0 בכל פעם שהמשואה מתחילה להבהב. Exit מנקה את המצב כשהוא מסתיים: מקש Space עשוי להילחץ כשהמנורה כבויה, ו-Exit מדליקה אותה מחדש, כך ש-SteadyState לעולם לא מתחיל כשהמנורה כבויה. SetState מפעילה את Exit של המצב הישן, ואז את Enter של המצב החדש. מצב שלישי, כמו פעימה איטית, הוא מחלקה נוספת, וה-Update של Beacon לא משתנה.
אתגר
בינוניצריח זקיף שומר על החצר. החצים מזיזים את Player (מוכן). תנו לצריח שני מצבים, המוגדרים כמחלקות:
- ב־
IState.cs, הכריזו עלpublic interface IStateעםvoid Enter(),void Tick()ו־void Exit(). - ב־
TurretStates.cs, כתבו שתי מחלקות רגילות שמממשות אותו.ScanStateמקבל את ראש הצריח בבנאי שלו. הוא מסובב את הראש מצד לצד: זווית ה־y המקומית של הראש נעה במהירות של 90 מעלות בשנייה בין-45ל־45, מתחילה מ־0 ופונה כלפי מעלה בכל פעם שהסריקה מתחילה. פעולת Enter שלו גם צובעת את הראש ב־Color.green. AlertStateמקבל את הראש ואת השחקן. פעולת Enter שלו צובעת את הראש ב־Color.redורושמת ביומןIntruder!; פעולת Tick שלו מסובבת את הראש כך שיפנה אל השחקן סביב ציר y בלבד, ללא הטיה; פעולת Exit שלו רושמת ביומןAll clear.- ב־
Turret.cs, צרו את שני המצבים ב־Start והתחילו בסריקה. בכל פריים, עברו למצב התראה כשהשחקן קרוב יותר לצריח מ־range(4), וחזרו לסריקה כשהוא אינו קרוב יותר, ואז הפעילו את Tick של המצב הנוכחי. בעת שינוי מצב, פעולת Exit של המצב הקודם מופעלת, ולאחריה פעולת Enter של המצב החדש.
לחצו על הפעלה, התקרבו לצריח והתרחקו ממנו, ואז לחצו על בדיקה.
נסו בעצמכם
using UnityEngine;
// ScanState ו-AlertState
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה ארכיטקטורת משחקים ב-Unity
1רכיבים, לא היררכיות
רכיבים קטניםסקריפט בסיס לאויברכיבים מופשטיםרכיבים שעובדים יחדסיכום: צריח מודולרי