Окно кода на C#, от которого светящиеся линии-связи тянутся к игровым объектам в сцене Unity

Основы C# для Unity: первый скрипт и связь объектов

·  14 мин чтения Для начинающих Unity C# Туториал

Коротко. Скрипт в 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:

C#
using UnityEngine;                    // подключаем библиотеку Unity

public class Rotator : MonoBehaviour  // класс Rotator — это компонент
{
    void Start()                      // один раз перед первым кадром
    {
    }

    void Update()                     // каждый кадр
    {
    }
}

Три строки, которые важно понять с первого дня:

Шаг 1. Создаём скрипт и подключаем редактор кода

Чтобы создать скрипт в Unity, в окне Project нажмите правой кнопкой по папке (лучше заведите папку Scripts) → Create → Scripting → MonoBehaviour Script. В Unity 2022 и старше пункт называется Create → C# Script. Сразу, пока имя выделено, введите Rotator и нажмите Enter.

Контекстное меню окна Project в Unity 6: Create, Scripting, MonoBehaviour Script
Имя вводим сразу — Unity впишет его в название класса.

Внимание: если нажать 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 на этот:

C#
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 в строгом порядке. От того, в какой метод положить код, зависит, будет ли он работать.

Проверить порядок своими глазами можно за минуту — повесьте этот скрипт на объект и посмотрите в Console:

C#
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

Для первых механик хватает небольшого набора конструкций языка. Остальное — классы, интерфейсы, события — подтягивается по ходу, когда в нём появляется реальная нужда.

Этого достаточно, чтобы дочитать статью до конца и написать большинство механик для первого платформера.

Шаг 3. public, private и [SerializeField]: настройки в Inspector

Модификатор доступа решает две вещи: видно ли поле в Inspector и могут ли его менять другие скрипты. Новички часто путают эти две оси, отсюда и вечный вопрос «зачем SerializeField, если есть public».

C#
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: код описывает поведение, а цифры настраиваются мышкой, без перекомпиляции.

Компонент Rotator в Inspector Unity: видны поля Lives и Speed, приватное поле и HideInInspector скрыты
Unity сам переводит speed в «Speed», а isFast — в «Is Fast».

Правило, которое я ставлю в любом проекте как lead-программист: все настройки — [SerializeField] private, а public — только для того, что другим скриптам действительно нужно (и лучше это метод, а не поле). Когда в проекте сотня скриптов и здоровье игрока внезапно стало −50, с приватными полями вы найдёте виновника за минуту: менять его мог только сам скрипт.

Ловушка: значение в Inspector главнее значения в коде. Если вы повесили скрипт со speed = 90f, а потом поменяли в коде на 180f — объект всё равно крутится на 90. Unity сохранил число в сцене. Поменяйте его в Inspector или нажмите на три точки компонента → Reset. И помните: изменения в Inspector во время Play после остановки откатываются.

Скрипты в Unity общаются через ссылки: чтобы вызвать метод чужого скрипта, сначала нужно получить на него ссылку — переменную нужного типа. Способов несколько, и выбор зависит от того, где находится нужный компонент.

Свой компонент: GetComponent в Awake

GetComponent<T>() ищет компонент типа T на том же объекте, где висит скрипт. Вызывайте его один раз в Awake() и сохраняйте результат в поле — а не каждый кадр в Update().

C#
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-методом, который можно вызвать снаружи:

C#
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:

C#
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 просто не даст его бросить в поле: тип защищает от ошибок.

Перетаскивание объекта Player из окна Hierarchy в поле Target типа Health в Inspector Unity
Поле типа Health примет только объект, на котором висит скрипт Health.

Объект, который «прилетел» в игре: TryGetComponent

Когда заранее неизвестно, с кем столкнётся объект — пуля, шипы, монетка, — ссылку получают в момент касания. TryGetComponent пытается достать компонент и сразу говорит, получилось ли:

C#
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(). Поиск по имени ещё и ломается, стоит переименовать объект.

Когда один менеджер нужен буквально всем, часто используют статическую ссылку — паттерн «синглтон» в простейшем виде:

C#
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, и элементы туда можно перетаскивать.

C#
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);
    }
}

Частые ошибки новичков в скриптах

Вывод

Фундамент 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.

Хочешь писать скрипты для своей игры уверенно?

На бесплатном курсе Unity Старт ты напишешь первый скрипт уже на первом уроке, а дальше по программе соберёшь полноценные игры с проверкой заданий.

Начать бесплатно