~3 мин чтения

Скрипты и жизненный цикл MonoBehaviour

Как Unity вызывает ваш код — Awake, Start, Update и всё, что между ними.

Иллюстрация: Скрипты и жизненный цикл MonoBehaviour

MonoBehaviour — базовый класс ваших скриптов

Любой ваш C#-скрипт, который вешается на GameObject как компонент, наследуется от MonoBehaviour. Это даёт Unity точки входа — методы с особыми именами, которые движок вызывает сам в нужный момент.

using UnityEngine;

public class Hello : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    private void Start() {
        Debug.Log("Привет из Unity");
    }

    private void Update() {
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

Атрибут [SerializeField] делает приватное поле видимым в Inspector — это идиоматичный способ выставлять параметры. Публичные поля тоже сериализуются по умолчанию, но это считается плохим тоном (нарушает инкапсуляцию).

Веб

React-компонент: useEffect(() => {}, []) — это аналог Start, а useEffect(() => {}) без зависимостей — что-то вроде Update, но в React render-функция вызывается реактивно, а не каждый кадр.

Unity

Unity вызывает Update() каждый кадр и Start() ровно один раз перед первым Update. Поля компонента — это состояние, как useState.

Жизненный цикл — порядок вызовов

Полный жизненный цикл MonoBehaviour сложнее, но вот основные методы, отсортированные по порядку вызова:

  1. Awake() — когда объект инстанцирован в сцене, до загрузки любых других объектов и до подписок. Здесь обычно GetComponent<>.
  2. OnEnable() — каждый раз, когда компонент включается (или объект становится активным). Здесь подписываются на события.
  3. Start() — один раз перед первым Update, но только если компонент активен. Хорошее место для инициализации, которая зависит от других объектов.
  4. FixedUpdate() — фиксированный шаг (по умолчанию каждые 0.02 с = 50 Hz). Сюда — физика и Rigidbody.AddForce.
  5. Update() — каждый кадр. Сюда — ввод и логика, не привязанная к физике.
  6. LateUpdate() — каждый кадр, после всех Update. Сюда — следящая камера и финальная корректировка положений.
  7. OnDisable() — когда компонент выключается. Здесь отписываются от событий.
  8. OnDestroy() — когда GameObject уничтожен (Destroy(obj)) или сцена выгружена.
public class LifecycleExample : MonoBehaviour
{
    private Rigidbody _rb;

    private void Awake()        { _rb = GetComponent<Rigidbody>(); }
    private void OnEnable()     { Health.OnDeath += HandleDeath; }
    private void Start()        { /* инициализация, зависящая от других объектов */ }
    private void FixedUpdate()  { _rb.AddForce(Vector3.up); }
    private void Update()       { /* читаем ввод, логика */ }
    private void LateUpdate()   { /* подвинуть камеру за игроком */ }
    private void OnDisable()    { Health.OnDeath -= HandleDeath; }
    private void OnDestroy()    { /* финальная очистка */ }

    private void HandleDeath() { /* ... */ }
}
Парность OnEnable / OnDisable

Подписки на события в OnEnable и отписки в OnDisable — стандартная практика. Это переживает включение/выключение объекта без утечек и без двойных подписок.

Update vs FixedUpdate vs LateUpdate

Это трёхэтапный цикл, и понимание разницы критично:

МетодЧастотаЧто туда
FixedUpdateФиксированно (50 Hz по умолчанию)Физика: AddForce, MovePosition для Rigidbody
UpdateКаждый кадрВвод, AI, нефизическая логика
LateUpdateКаждый кадр, после UpdateСледящая камера, “догон” за изменениями кадра

Time.deltaTime в Update — время с последнего кадра (переменное). Time.fixedDeltaTime в FixedUpdate — фиксированный шаг.

Никогда не двигайте Rigidbody в Update

Если у объекта есть Rigidbody (не kinematic), напрямую менять transform.position или Rotate() каждый кадр — путь к ошибкам столкновений и дрожанию. Используйте Rigidbody.MovePosition или физические силы в FixedUpdate.

GetComponent: получение других компонентов

private Rigidbody _rb;
private Animator _animator;

private void Awake() {
    _rb = GetComponent<Rigidbody>();
    _animator = GetComponentInChildren<Animator>(); // искать в детях
}

Не вызывайте GetComponent в Update — это поиск по списку компонентов, дорогая операция. Кэшируйте в Awake/Start.

В Unity 6 есть удобная альтернатива — атрибут [RequireComponent(typeof(Rigidbody))] на классе, который гарантирует наличие нужного компонента, и поиск через TryGetComponent для ситуаций, где компонента может не быть.

Связь со сценой: ссылки в Inspector

Самый удобный способ соединить два объекта — поле в Inspector:

public class Enemy : MonoBehaviour
{
    [SerializeField] private Transform target; // перетащите Player в Inspector
    [SerializeField] private float speed = 3f;

    private void Update() {
        var dir = (target.position - transform.position).normalized;
        transform.position += dir * speed * Time.deltaTime;
    }
}

Альтернативы — поиск по сцене (FindAnyObjectByType<Player>(), GameObject.FindWithTag("Player")). Последние медленнее и хрупче, но удобны для прототипа.

Coroutine: “корутины” вместо async/await (часто)

Корутина — IEnumerator, который возвращает управление движку через yield return. Это удобный способ растянуть логику на несколько кадров, не плодя свои таймеры.

private IEnumerator FadeOut(float duration) {
    float t = 0;
    var renderer = GetComponent<Renderer>();
    var color = renderer.material.color;
    while (t < duration) {
        t += Time.deltaTime;
        color.a = Mathf.Lerp(1f, 0f, t / duration);
        renderer.material.color = color;
        yield return null; // подождать следующий кадр
    }
    Destroy(gameObject);
}

// Запуск:
StartCoroutine(FadeOut(2f));

Полезные yield return:

  • null — следующий кадр
  • new WaitForSeconds(1f) — пауза в “игровых секундах” (учитывает Time.timeScale)
  • new WaitForSecondsRealtime(1f) — пауза в реальных секундах (игнорирует timescale)
  • new WaitUntil(() => isReady) — ждать условия
  • new WaitForEndOfFrame() — после рендера кадра (для скриншотов)
Веб

Цепочка setTimeout/requestAnimationFrame с состоянием. Или RxJS Observable, который каждый “тик” отдаёт следующее значение. Только runtime сам не дёргает шаги — это делает ваш код.

Unity

StartCoroutine дёргает ваш IEnumerator каждый кадр (или после нужного yield). Никаких тредов — всё на main thread. Движок берёт на себя планирование шагов.

Async/await: всё ближе к мейнстриму

Unity 6 поддерживает async/await с типом Awaitable:

private async Awaitable LoadAndShow() {
    await Awaitable.WaitForSecondsAsync(1f);
    Debug.Log("Прошла секунда");
    await SceneManager.LoadSceneAsync("Level_02");
}

Awaitable — это zero-alloc альтернатива Task для Unity. Но почти все API Unity всё равно работают только на main thread — отправите вызов transform.position из произвольного потока, получите исключение.

В следующем разделе — ввод: новый Input System и legacy Input класс.