Скрипты и жизненный цикл MonoBehaviour
Как Unity вызывает ваш код — Awake, Start, Update и всё, что между ними.
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 вызывает Update() каждый кадр и Start() ровно один раз перед первым Update. Поля
компонента — это состояние, как useState.
Жизненный цикл — порядок вызовов
Полный жизненный цикл MonoBehaviour сложнее, но вот основные методы, отсортированные по порядку вызова:
Awake()— когда объект инстанцирован в сцене, до загрузки любых других объектов и до подписок. Здесь обычноGetComponent<>.OnEnable()— каждый раз, когда компонент включается (или объект становится активным). Здесь подписываются на события.Start()— один раз перед первымUpdate, но только если компонент активен. Хорошее место для инициализации, которая зависит от других объектов.FixedUpdate()— фиксированный шаг (по умолчанию каждые 0.02 с = 50 Hz). Сюда — физика иRigidbody.AddForce.Update()— каждый кадр. Сюда — ввод и логика, не привязанная к физике.LateUpdate()— каждый кадр, после всехUpdate. Сюда — следящая камера и финальная корректировка положений.OnDisable()— когда компонент выключается. Здесь отписываются от событий.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 — стандартная практика. Это переживает
включение/выключение объекта без утечек и без двойных подписок.
Update vs FixedUpdate vs LateUpdate
Это трёхэтапный цикл, и понимание разницы критично:
| Метод | Частота | Что туда |
|---|---|---|
FixedUpdate | Фиксированно (50 Hz по умолчанию) | Физика: AddForce, MovePosition для Rigidbody |
Update | Каждый кадр | Ввод, AI, нефизическая логика |
LateUpdate | Каждый кадр, после Update | Следящая камера, “догон” за изменениями кадра |
Time.deltaTime в Update — время с последнего кадра (переменное). Time.fixedDeltaTime в
FixedUpdate — фиксированный шаг.
Если у объекта есть 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 сам не дёргает шаги — это делает ваш код.
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 класс.