Коротко. Скрипт в Unity — это класс на C#, унаследованный от MonoBehaviour, который вешается на объект как компонент. Создаётся он в окне Project: Create → Scripting → MonoBehaviour Script. Unity сам вызывает в нём методы: Awake() — при создании объекта, Start() — перед первым кадром, Update() — каждый кадр. Настройки выносят в Inspector через [SerializeField] private, свои компоненты получают через GetComponent<T>(), а к другому скрипту обращаются через поле-ссылку и вызывают его public-методы.
Ниже — четыре шага от пустого файла до двух скриптов, которые общаются друг с другом, плюс массивы и List, минимум синтаксиса C# и разбор ошибок, на которых спотыкается каждый новичок.
Без C# в Unity не оживёт ни одна механика: движение, прыжок, урон, очки, меню — всё это скрипты. Хорошая новость в том, что для старта не нужно «учить программирование» месяцами. Достаточно понять пять вещей: как устроен скрипт, когда Unity вызывает его методы, как показать переменную в Inspector, как достать компонент и как один скрипт зовёт другой. Ровно это и разберём — на коде, который можно сразу скопировать в проект.
Когда я проверяю домашние задания учеников, девять ошибок из десяти — не про синтаксис, а про связь объектов: пустое поле в Inspector, GetComponent в Update(), всё подряд public. Поэтому половина статьи — именно про то, как скрипты находят друг друга. Если Unity ещё не установлен, начните с гайда как установить Unity и создать первый проект и возвращайтесь.
Что такое скрипт в Unity и как он устроен
Скрипт в Unity — это компонент, такой же, как Transform или Rigidbody2D, только поведение у него описываете вы. Сам по себе файл ничего не делает: он начинает работать, когда висит на объекте в сцене. Один скрипт можно повесить на много объектов — у каждого будет своя копия со своими значениями.
Вот как выглядит минимальный скрипт, который создаёт Unity 6:
using UnityEngine; // подключаем библиотеку Unity
public class Rotator : MonoBehaviour // класс Rotator — это компонент
{
void Start() // один раз перед первым кадром
{
}
void Update() // каждый кадр
{
}
}
Три строки, которые важно понять с первого дня:
using UnityEngine;— подключает всё «юнитёвое»:Transform,Vector3,Debug,GameObject. Без неё компилятор не знает этих слов.public class Rotator— имя класса. Оно обязано совпадать с именем файлаRotator.cs, вплоть до регистра. Иначе скрипт не повесится на объект.: MonoBehaviour— наследование. Именно оно превращает обычный класс C# в компонент: даёт доступ кtransform,gameObjectи к «магическим» методамStart,Updateи другим, которые Unity вызывает сам.
Шаг 1. Создаём скрипт и подключаем редактор кода
Чтобы создать скрипт в Unity, в окне Project нажмите правой кнопкой по папке (лучше заведите папку Scripts) → Create → Scripting → MonoBehaviour Script. В Unity 2022 и старше пункт называется Create → C# Script. Сразу, пока имя выделено, введите Rotator и нажмите Enter.
Внимание: если нажать Enter с именем по умолчанию, а потом переименовать файл, класс внутри останется старым. Unity выдаст ошибку Can't add script component… the script class cannot be found. Лечится просто: открыть файл и вручную поправить имя после public class.
Двойной клик по скрипту открывает его в редакторе кода. Подойдёт любой из трёх: Visual Studio Community (ставится вместе с Unity через Hub), VS Code с расширением Unity или JetBrains Rider (бесплатен для некоммерческих проектов). Главное — чтобы редактор был связан с Unity: Edit → Preferences → External Tools → External Script Editor. Тогда заработают подсказки и автодополнение — для новичка это половина успеха.
Шаг 2. Пишем первый код и вешаем скрипт на объект
Первый скрипт будет вращать объект. Создайте в сцене любой объект (например, GameObject → 2D Object → Sprites → Square) и замените код в Rotator.cs на этот:
using UnityEngine;
public class Rotator : MonoBehaviour
{
private void Start()
{
Debug.Log("Привет! Я " + gameObject.name);
}
private void Update()
{
// 90 градусов в секунду вокруг оси Z
transform.Rotate(0f, 0f, 90f * Time.deltaTime);
}
}
Сохраните файл (Ctrl+S), вернитесь в Unity и дождитесь, пока он перекомпилирует код (крутящийся значок в правом нижнем углу). Теперь перетащите Rotator из окна Project прямо на объект в Hierarchy — или выделите объект и нажмите Add Component → Rotator. Жмите Play: квадрат вращается, а в окне Console появилось приветствие.
Обратите внимание на Time.deltaTime — это время, прошедшее с прошлого кадра. Умножая на него, мы получаем «90 градусов в секунду», а не «90 градусов за кадр». Без него на мощном компьютере (200 FPS) объект крутился бы втрое быстрее, чем на слабом (60 FPS).
Совет: Debug.Log() — ваш главный инструмент отладки на старте. Не понимаете, вызывается ли метод и что лежит в переменной, — выведите это в Console: Debug.Log("score = " + score);
Awake, Start и Update: когда Unity вызывает методы
Методы Awake, Start и Update — это события жизненного цикла: вы их не вызываете, их вызывает сам Unity в строгом порядке. От того, в какой метод положить код, зависит, будет ли он работать.
Awake()— первым, сразу при создании объекта, один раз. Даже если сам компонент выключен. Здесь получаем ссылки на свои компоненты:rb = GetComponent<Rigidbody2D>().OnEnable()— каждый раз, когда компонент или объект включается. Удобно для подписки на события.Start()— один раз перед первым кадром, когда всеAwake()на сцене уже отработали. Поэтому здесь безопасно обращаться к другим объектам — они уже инициализированы.Update()— каждый кадр. Ввод с клавиатуры, таймеры, всё, что проверяется постоянно.FixedUpdate()— с фиксированным шагом физики (по умолчанию 50 раз в секунду). Здесь меняют скоростьRigidbody— подробно это разобрано в статье про движение и прыжок в Unity 2D.LateUpdate()— каждый кадр, но после всехUpdate(). Классика — камера, которая следует за уже сдвинувшимся игроком.
Проверить порядок своими глазами можно за минуту — повесьте этот скрипт на объект и посмотрите в Console:
using UnityEngine;
public class LifecycleDemo : MonoBehaviour
{
private int frames;
private void Awake() { Debug.Log("1. Awake — объект создан"); }
private void OnEnable() { Debug.Log("2. OnEnable — компонент включён"); }
private void Start() { Debug.Log("3. Start — перед первым кадром"); }
private void Update()
{
frames++;
if (frames <= 3)
{
Debug.Log("4. Update — кадр " + frames);
}
}
}
Простое правило: в Awake — «настроить себя», в Start — «познакомиться с другими», в Update — «жить».
Минимум C#, который нужен для Unity
Для первых механик хватает небольшого набора конструкций языка. Остальное — классы, интерфейсы, события — подтягивается по ходу, когда в нём появляется реальная нужда.
- Переменные и типы:
int lives = 3;— целое,float speed = 5f;— дробное (букваfобязательна!),bool isDead = false;— да/нет,string playerName = "Bob";— текст. Плюс юнитёвые типы:Vector2,Vector3,GameObject,Transform. - Условия:
if (lives <= 0) { ... } else { ... }. Сравнение — двумя знаками==, один=— это присваивание. - Методы: именованный кусок кода.
void— ничего не возвращает,int GetScore()— возвращает число черезreturn. - Циклы:
for— повторить N раз,foreach— пройти по всем элементам массива или списка.
Этого достаточно, чтобы дочитать статью до конца и написать большинство механик для первого платформера.
Шаг 3. public, private и [SerializeField]: настройки в Inspector
Модификатор доступа решает две вещи: видно ли поле в Inspector и могут ли его менять другие скрипты. Новички часто путают эти две оси, отсюда и вечный вопрос «зачем SerializeField, если есть public».
using UnityEngine;
public class Rotator : MonoBehaviour
{
public int lives = 3; // в Inspector: ДА · другие скрипты: ДА
[SerializeField] private float speed = 90f; // в Inspector: ДА · другие скрипты: НЕТ
private int turns; // в Inspector: НЕТ · другие скрипты: НЕТ
[HideInInspector] public bool isFast; // в Inspector: НЕТ · другие скрипты: ДА
private void Update()
{
transform.Rotate(0f, 0f, speed * Time.deltaTime);
}
}
Теперь скорость вращения можно менять прямо в Inspector — даже во время Play, подбирая значение «на глаз». Это и есть главная суперсила Unity: код описывает поведение, а цифры настраиваются мышкой, без перекомпиляции.
speed в «Speed», а isFast — в «Is Fast».Правило, которое я ставлю в любом проекте как lead-программист: все настройки — [SerializeField] private, а public — только для того, что другим скриптам действительно нужно (и лучше это метод, а не поле). Когда в проекте сотня скриптов и здоровье игрока внезапно стало −50, с приватными полями вы найдёте виновника за минуту: менять его мог только сам скрипт.
Ловушка: значение в Inspector главнее значения в коде. Если вы повесили скрипт со speed = 90f, а потом поменяли в коде на 180f — объект всё равно крутится на 90. Unity сохранил число в сцене. Поменяйте его в Inspector или нажмите на три точки компонента → Reset. И помните: изменения в Inspector во время Play после остановки откатываются.
Шаг 4. GetComponent и обращение к другому скрипту
Скрипты в Unity общаются через ссылки: чтобы вызвать метод чужого скрипта, сначала нужно получить на него ссылку — переменную нужного типа. Способов несколько, и выбор зависит от того, где находится нужный компонент.
Свой компонент: GetComponent в Awake
GetComponent<T>() ищет компонент типа T на том же объекте, где висит скрипт. Вызывайте его один раз в Awake() и сохраняйте результат в поле — а не каждый кадр в Update().
using UnityEngine;
[RequireComponent(typeof(SpriteRenderer))] // Unity сам добавит компонент, если его нет
public class Blinker : MonoBehaviour
{
[SerializeField] private float interval = 0.5f;
private SpriteRenderer sprite;
private float timer;
private void Awake()
{
sprite = GetComponent<SpriteRenderer>(); // один раз — и храним
}
private void Update()
{
timer += Time.deltaTime;
if (timer >= interval)
{
timer = 0f;
sprite.enabled = !sprite.enabled; // мигаем
}
}
}
Атрибут [RequireComponent] страхует от NullReferenceException: скрипт просто не повесится на объект без нужного компонента. Для компонентов на дочерних объектах есть GetComponentInChildren<T>(), на родителе — GetComponentInParent<T>().
Скрипт на другом объекте: ссылка через Inspector
Самый надёжный и быстрый способ обратиться к другому скрипту — объявить поле с типом этого скрипта и перетащить объект в поле мышкой. Сделаем пару «здоровье + ловушка». Сначала скрипт здоровья с public-методом, который можно вызвать снаружи:
using UnityEngine;
public class Health : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
private int current;
private void Awake()
{
current = maxHealth;
}
public void TakeDamage(int amount) // public — другие скрипты могут вызвать
{
current -= amount;
Debug.Log(gameObject.name + ": осталось " + current + " HP");
if (current <= 0)
{
Destroy(gameObject);
}
}
public int GetHealth() // метод, который возвращает значение
{
return current;
}
}
Повесьте Health на игрока. Теперь скрипт, которому нужно ударить игрока, хранит ссылку на его Health:
using UnityEngine;
public class PoisonCloud : MonoBehaviour
{
[SerializeField] private Health target; // перетащите сюда игрока
[SerializeField] private int damagePerTick = 5;
[SerializeField] private float tickInterval = 1f;
private float timer;
private void Update()
{
if (target == null) return; // игрок уже уничтожен
timer += Time.deltaTime;
if (timer >= tickInterval)
{
timer = 0f;
target.TakeDamage(damagePerTick); // вызываем метод другого скрипта
}
}
}
В Inspector у PoisonCloud появится поле Target с подписью None (Health). Перетащите в него игрока из Hierarchy — Unity сам найдёт на нём компонент Health. Если у объекта такого компонента нет, Unity просто не даст его бросить в поле: тип защищает от ошибок.
Health примет только объект, на котором висит скрипт Health.Объект, который «прилетел» в игре: TryGetComponent
Когда заранее неизвестно, с кем столкнётся объект — пуля, шипы, монетка, — ссылку получают в момент касания. TryGetComponent пытается достать компонент и сразу говорит, получилось ли:
using UnityEngine;
public class Spikes : MonoBehaviour
{
[SerializeField] private int damage = 25;
// У шипов Collider2D с галочкой Is Trigger,
// у игрока — Collider2D и Rigidbody2D
private void OnTriggerEnter2D(Collider2D other)
{
if (other.TryGetComponent(out Health health))
{
health.TakeDamage(damage);
}
}
}
Красота в том, что шипам всё равно, кто на них наступил: игрок, враг или ящик. Есть Health — получит урон, нет — ничего не случится. Именно так в реальных проектах устроены урон, подбор предметов и взаимодействие с объектами.
Поиск по сцене и менеджеры
Для единственных объектов вроде GameManager есть поиск по типу — FindFirstObjectByType<GameManager>() (старый FindObjectOfType в Unity 6 помечен устаревшим), и поиск по имени — GameObject.Find("Player"). Оба медленные: вызывайте их максимум один раз в Start(), а не в Update(). Поиск по имени ещё и ломается, стоит переименовать объект.
Когда один менеджер нужен буквально всем, часто используют статическую ссылку — паттерн «синглтон» в простейшем виде:
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
private int score;
private void Awake()
{
Instance = this;
}
public void AddScore(int amount)
{
score += amount;
Debug.Log("Очки: " + score);
}
}
// В любом другом скрипте, например в монетке:
// GameManager.Instance.AddScore(1);
Какой способ выбрать: свой компонент — GetComponent в Awake; известный заранее объект — поле [SerializeField] и перетаскивание; объект из столкновения — TryGetComponent; один глобальный менеджер — Instance. GameObject.Find — в крайнем случае.
Массивы и List в Unity
Массив и List хранят много значений одного типа в одной переменной. Разница в том, что размер массива фиксирован, а список растёт и сжимается во время игры. Оба типа, помеченные [SerializeField], отображаются в Inspector, и элементы туда можно перетаскивать.
using System.Collections.Generic; // нужно для List
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private Transform[] spawnPoints; // массив: набор точек не меняется
private List<GameObject> enemies = new List<GameObject>(); // список: враги появляются и исчезают
private void Start()
{
foreach (Transform point in spawnPoints)
{
GameObject enemy = Instantiate(enemyPrefab, point.position, Quaternion.identity);
enemies.Add(enemy);
}
Debug.Log("Точек: " + spawnPoints.Length + ", врагов: " + enemies.Count);
}
public void KillFirst()
{
if (enemies.Count == 0) return;
Destroy(enemies[0]);
enemies.RemoveAt(0);
}
}
- Длина массива —
Length, количество в списке —Count. - Индексы начинаются с нуля: первый элемент —
[0], последний —[Length - 1]. Выход за границы даётIndexOutOfRangeException(для списка —ArgumentOutOfRangeException). - Не удаляйте элементы из списка внутри
foreachпо нему же — получитеInvalidOperationException. Используйте обычныйforс конца к началу.
Частые ошибки новичков в скриптах
UnassignedReferenceException: The variable target of PoisonCloud has not been assigned— поле-ссылку забыли заполнить в Inspector. Unity прямо пишет, какое поле и в каком скрипте. Перетащите объект.NullReferenceException— ссылка пустая:GetComponentничего не нашёл (компонента нет на объекте), или объект уже уничтожен. Двойной клик по ошибке в Console откроет нужную строку.- Скрипт не вешается на объект — имя класса не совпадает с именем файла или в Console есть красные ошибки компиляции. Пока в проекте хоть одна ошибка, Unity не применяет новые изменения ни в одном скрипте.
Update()не вызывается — опечатка в имени метода.update()с маленькой буквы для Unity — обычный метод, и вызывать его никто не будет. То же сStart,Awake,OnTriggerEnter2D.- Изменения в коде «не работают» — файл не сохранён, или значение поля перекрыто тем, что сохранено в Inspector (см. ловушку выше).
- Игра тормозит —
GetComponent,FindилиDebug.Logвызываются вUpdate()каждый кадр. Кэшируйте ссылки вAwake/Start.
Вывод
Фундамент C# для Unity — это класс-наследник MonoBehaviour с именем как у файла, понимание порядка Awake → Start → Update, настройки через [SerializeField] private и ссылки между скриптами: GetComponent для своих компонентов, поле в Inspector для известных объектов и TryGetComponent для тех, что встретились в игре. На этих пяти идеях держится 90% кода в учебных и коммерческих проектах.
Следующий логичный шаг — применить всё это к управлению персонажем в статье как сделать движение и прыжок в Unity 2D, а пример посложнее со SceneManager ждёт в гайде про переход между уровнями. Если хочется разбирать код не в одиночку, а с проверкой заданий и обратной связью, начните с бесплатного курса Unity Старт — там первый скрипт пишется на первом же уроке, а дальше программа ведёт до готовой игры.
Частые вопросы
Как создать скрипт в Unity?
В окне Project нажмите правой кнопкой → Create → Scripting → MonoBehaviour Script (в Unity 2022 и старше — Create → C# Script) и сразу задайте имя, например PlayerMove. Имя файла должно совпадать с именем класса внутри. Затем перетащите скрипт на объект в сцене или добавьте его через Add Component.
Чем отличаются Awake, Start и Update в Unity?
Awake вызывается первым, сразу при создании объекта — в нём получают ссылки на свои компоненты. Start вызывается один раз перед первым кадром, когда все Awake уже отработали, — в нём обращаются к другим объектам. Update вызывается каждый кадр — в нём живёт логика, которая должна происходить постоянно.
Зачем нужен [SerializeField], если есть public?
[SerializeField] показывает приватное поле в Inspector, но не открывает его другим скриптам. public делает и то и другое — и любой скрипт может случайно изменить значение. Правило хорошего тона: настройки — private с [SerializeField], public — только для того, что другим скриптам действительно нужно.
Как обратиться к другому скрипту в Unity?
Самый надёжный способ — объявить поле с типом нужного скрипта, например [SerializeField] private Health target, и перетащить объект в это поле в Inspector. Если объект попадается в игре (столкновение, триггер), берите скрипт через other.TryGetComponent(out Health health). После этого вызывайте его public-методы: target.TakeDamage(10).
Почему Unity не даёт добавить скрипт на объект?
Чаще всего имя файла не совпадает с именем класса или в проекте есть ошибки компиляции — красные строки в Console. Исправьте все ошибки и переименуйте класс точно как файл, с учётом регистра. Также класс должен наследоваться от MonoBehaviour.
Что использовать в Unity — массив или List?
Массив подходит для фиксированного набора, который задаётся в Inspector и не меняется: точки спавна, уровни. List нужен, когда элементы добавляются и удаляются во время игры: враги на сцене, предметы в инвентаре. Длина массива — Length, количество в List — Count.


