ozliginus.ru

Основы C# в Unity: переменные, методы и компоненты

Основы C# в Unity: переменные, методы и компоненты

Когда я только пересел с трейнеров и моддинга на Unity, меня сразу огорошили три кита: переменные, методы и компоненты. Без них игра не оживёт — ни персонаж не дёрнется, ни дверь не откроется. Скрипт в Unity — это не просто код, он становится компонентом, который цепляется на GameObject и диктует ему поведение. Такой подход — основа архитектуры движка: логика пишется скриптами, а скрипт подключается к объекту как компонент. И если вы сейчас в начале пути, то понимание этой тройки сэкономит вам недели мучений.

Что такое компоненты в Unity

Помню, как поначалу путал GameObject и компонент. GameObject — это пустая оболочка, контейнер. А вся магия — в компонентах, которые на него навешиваются: Transform, SpriteRenderer, Rigidbody2D, коллайдеры и, конечно, наши скрипты. Именно они наделяют объект поведением.

Например:

  • у персонажа может быть Transform, SpriteRenderer, Rigidbody2D и скрипт управления;
  • у двери — Transform, Collider и скрипт открытия;
  • у кнопки интерфейса — визуальные компоненты и скрипт обработки нажатия.

Скрипт в Unity обычно наследуется от MonoBehaviour — это ключевой класс, который встраивает наш код в игровой цикл движка. Без него скрипт просто не будет знать, когда ему запускаться. Unity сама вызывает его методы в строго определённые моменты: Start, Update, FixedUpdate и другие. Это надо прочувствовать, потому что порядок вызовов — не магия, а чёткий конвейер.

Почему это важно

Когда я пересел с чистого C# на Unity, первое время ловил себя на мысли: «Где же точка входа?» В консольном приложении есть Main, а тут — ничего подобного. Логика не стартует сама по себе, она живёт только внутри компонента, прикреплённого к объекту на сцене. Это фундаментальный сдвиг: ты не просто пишешь код, ты создаёшь модуль поведения, который существует только в контексте игрового мира. Осознание этого снимает кучу вопросов и избавляет от попыток запустить скрипт как отдельную программу.

Переменные: где хранится состояние объекта

Когда я делал свой первый прототип, то быстро понял: без переменных игра — просто набор статичных моделек. Переменная — это ячейка памяти, в которой лежит всё, что меняется: здоровье, скорость, патроны, состояние двери. В Unity переменные часто выставляют в Inspector — это дико удобно для настройки без перекомпиляции. Прямо как в Cheat Engine, только легально и встроено в движок.

Простой пример:

public int health = 100;
public float speed = 5f;
public string playerName = "Hero";
public bool isAlive = true;

Разбор по смыслу

  • int — целое число, идеально для здоровья, очков, патронов.
  • float — число с дробной частью, часто используется для скорости, таймеров, плавных перемещений.
  • string — текст, имена, сообщения.
  • bool — логическое значение: true или false, флаги состояний.

В Unity такие поля часто делают видимыми в Inspector, если они public или помечены атрибутом [SerializeField]. Это мощный инструмент: геймдизайнер или вы сами можете менять параметры прямо в редакторе, не залезая в код. Я, например, всегда выношу скорость и здоровье в Inspector, чтобы быстро балансировать на лету.

Виды переменных, которые нужны в начале

Локальные переменные

Живут внутри метода и нужны только там. Это рабочие лошадки для временных расчётов.

void CalculateDamage()
{
    int baseDamage = 10;
    int totalDamage = baseDamage * 2;
    // используем totalDamage только здесь
}

Поля класса

Хранят состояние компонента на протяжении всей жизни объекта. Это долговременная память.

public class Player : MonoBehaviour
{
    private int health = 100;
    private float speed = 5f;
}

Ссылки на компоненты

Очень важная вещь в Unity. Это переменная, в которой хранится ссылка на другой компонент или объект. Через них мы связываем объекты без жёсткой привязки в коде. Помню, как в первых трейнерах я искал адреса в памяти, а тут — просто ссылка на компонент, и всё.

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;
    
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }
}

Методы: что делает объект

Методы — это действия. Если переменная отвечает на вопрос «что хранится?», то метод — «что делаем?». Я часто сравниваю методы с кнопками на пульте: нажал — выполнилось. TakeDamage() — отнять здоровье, Jump() — подпрыгнуть. Вызывать их можно откуда угодно: из коллизии, по таймеру, из другого скрипта. Это основа модульности.

Пример:

public void TakeDamage(int damage)
{
    health -= damage;
    if (health <= 0)
    {
        Die();
    }
}

Здесь метод TakeDamage уменьшает здоровье. Его можно вызывать из другого места в коде, когда персонаж получает урон.

Зачем вообще нужны методы

Без методов код превращается в лапшу. Я наступал на эти грабли: сначала пихаешь всё в Update, потом через неделю не можешь понять, где что меняется. Методы позволяют изолировать логику: один метод — одна задача. Не дублируешь код, легко тестировать, легко читать. Это как разложить инструменты по ящикам, а не сваливать в кучу.

  • не дублировать код;
  • разбивать логику на понятные части;
  • делать код читаемым;
  • повторно использовать действия.

Самые важные методы MonoBehaviour

Когда скрипт висит на объекте, Unity сама дёргает определённые методы в строгом порядке. Это как рельсы, по которым едет наш код. Важно не просто запомнить, а прочувствовать, когда что использовать. Я для себя вывел простую мнемонику: Awake — подготовка зависимостей, Start — инициализация, Update — каждый кадр, FixedUpdate — физика, LateUpdate — действия после всех.

Метод Когда вызывается Для чего использовать
Awake() При загрузке экземпляра скрипта Инициализация зависимостей, подготовка данных
Start() Перед первым Update Настройка объекта перед началом работы
Update() Каждый кадр Логика, завязанная на кадр, ввод, простые проверки
FixedUpdate() Через фиксированные интервалы Физика и всё, что связано с Rigidbody
LateUpdate() После Update Действия, которые должны выполниться в конце кадра

Поначалу я путал Awake и Start. Awake вызывается при создании экземпляра, до Start. В Start уже можно безопасно обращаться к другим объектам, потому что все Awake отработали. Это важно для порядка инициализации.

Простое правило выбора

  • Awake — подготовить зависимости.
  • Start — инициализировать, зная что всё на сцене готово.
  • Update — каждый кадр: ввод, анимации, проверки.
  • FixedUpdate — физика: движение через Rigidbody, силы.
  • LateUpdate — после основной логики кадра: камера, сглаживание.

Как выглядят переменные и методы вместе

public class Player : MonoBehaviour
{
    public float speed = 5f;
    private int health = 100;
    
    void Update()
    {
        float move = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
        transform.Translate(move, 0, 0);
    }
    
    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health <= 0)
        {
            Debug.Log("Player died");
        }
    }
}

Что здесь происходит

  • speed — настройка, которую можно крутить в Inspector, не трогая код.
  • health — текущее состояние, меняется только через TakeDamage, что защищает от случайных изменений.
  • Update — сердцебиение объекта, здесь обрабатывается ввод и движение каждый кадр.
  • TakeDamage — публичный интерфейс для нанесения урона, вызывается извне при попадании или столкновении.

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

Как работать с компонентами на практике

Одна из типичных задач — получить доступ к другому компоненту на том же объекте. Для этого используют GetComponent. Это, пожалуй, самый частый метод в моих скриптах. Он ищет компонент нужного типа на том же GameObject и возвращает ссылку.

public class Player : MonoBehaviour
{
    private Rigidbody2D rb;
    
    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }
    
    void FixedUpdate()
    {
        rb.velocity = new Vector2(Input.GetAxis("Horizontal") * speed, rb.velocity.y);
    }
}

Почему это удобно

Это как сохранить указатель в трейнере: один раз нашёл адрес, запомнил, и дальше работаешь быстро. GetComponent даёт тот же комфорт, но в безопасном управляемом окружении. Вместо того чтобы вручную искать объект каждый раз, вы один раз получаете нужный компонент и дальше работаете с ним через переменную.

Что важно помнить

  • Если компонента нет, ссылка будет пустой — всегда проверяйте на null, иначе получите NullReferenceException в самый неподходящий момент.
  • GetComponent лучше не вызывать без необходимости каждый кадр — это дорого. Кэшируйте ссылку в Awake или Start.
  • Если компонент на другом объекте, используйте FindObjectOfType или ссылку через Inspector, но аккуратно — не злоупотребляйте поиском по всей сцене.

Типовые ошибки новичков

1. Всё писать в Update

Когда я начинал, мой Update напоминал свалку: тут и движение, и проверка здоровья, и спавн врагов. Код становился нечитаемым. Вывод: Update — только для того, что должно происходить каждый кадр. Всё остальное — в отдельные методы, вызываемые по событию. Инициализацию держите в Start или Awake, физику — в FixedUpdate.

2. Путать переменную и метод

Переменная хранит данные, метод выполняет действие. Поначалу легко перепутать и назвать метод как переменную или наоборот. Плохо: public int DoDamage; Лучше: public void ApplyDamage(). Имена должны отражать суть: существительное для данных, глагол для действий.

3. Не понимать, где хранить состояние

Если значение должно сохраняться между вызовами — это поле класса. Если нужно только внутри одного действия — локальная переменная. Не делайте всё полями класса, засоряя память и усложняя логику.

4. Бездумно делать всё public

Публичное поле удобно, но когда его слишком много, внешний код начинает менять что угодно и где угодно. Это усложняет отладку. Я стараюсь использовать [SerializeField] private — переменная видна в Inspector, но защищена от внешнего вмешательства.

5. Игнорировать порядок вызова методов

Новички часто ожидают, что Start сработает раньше всего. На практике важно понимать разницу между инициализацией (Awake), подготовкой (Start) и игровым циклом (Update). Неправильный порядок может привести к тому, что вы обратитесь к ещё не инициализированному компоненту.

Мини-практика: собираем простой скрипт

Давайте соберём скрипт открывающейся двери, чтобы закрепить связку переменные-методы-компоненты. Это классика: переменная isOpen хранит состояние, openAngle — угол поворота, метод OpenDoor меняет состояние и крутит объект через встроенный компонент transform. Просто, но показательно.

public class Door : MonoBehaviour
{
    public bool isOpen = false;
    public float openAngle = 90f;
    
    public void OpenDoor()
    {
        if (!isOpen)
        {
            isOpen = true;
            transform.Rotate(0, openAngle, 0);
        }
    }
}

Как это работает

  • isOpen — флаг, чтобы не открывать уже открытую дверь повторно.
  • openAngle — настройка, выставляется в Inspector, позволяет легко менять угол открытия.
  • OpenDoor() — публичный метод, его можно повесить на кнопку UI или вызвать при входе в триггер.
  • transform — встроенный компонент, который есть у каждого GameObject, даёт доступ к позиции, повороту, масштабу. Мы используем transform.Rotate для поворота.

Как понять, что код написан правильно

Я для себя выработал чек-лист, который помогает не скатываться в хаос. Если скрипт проходит по всем пунктам, значит, архитектура не подведёт.

Чек-лист

  • Переменные хранят данные, а не действия (если переменная называется DoSomething, что-то не так).
  • Методы делают одну понятную задачу (не «обработать всё», а «нанести урон»).
  • Start и Awake не перегружены лишним кодом (инициализация только необходимого).
  • Update не содержит всё подряд (только то, что действительно нужно каждый кадр).
  • Компоненты получаются через ссылку или GetComponent, а не копируются вручную (не дублируем данные).
  • Имена переменных и методов понятны по смыслу (health, а не h; TakeDamage, а не TD).

Полезная таблица для запоминания

Чтобы не путаться, держите под рукой эту шпаргалку. Я в своё время распечатал и повесил над столом.

Понятие Что это Пример Где используется
Переменная Хранилище данных health, speed Настройки, состояние, параметры
Метод Действие TakeDamage() Логика, команды, обработка событий
Компонент Блок поведения объекта Rigidbody2D, скрипт Сборка игры из модулей
MonoBehaviour База для игровых скриптов public class Player : MonoBehaviour Связь кода с Unity

Как не запутаться в начале

Мой совет: не пытайтесь сразу сделать RPG с открытым миром. Начните с микро-прототипов. Сделайте пять скриптов: движение персонажа, урон, подбирание предмета, дверь, простой враг с одной атакой. Каждый скрипт — это упражнение на связку переменных, методов и компонентов. После этого придёт понимание, как всё стыкуется, и вы сможете собирать более сложные системы без страха.

Вывод

Пройдя путь от трейнеров до собственных прототипов, я убедился: фундамент Unity — это триада «переменные — методы — компоненты». Переменные хранят состояние, методы — действия, компоненты — привязывают код к объектам. Как только вы начнёте видеть эту структуру в любом скрипте, всё встанет на свои места. Дальше будет проще разбираться с физикой, UI, анимациями и архитектурой. Не пытайтесь запомнить всё и сразу — просто каждый раз спрашивайте себя: где данные, где поведение и к чему оно привязано. Это и есть вход в нормальную разработку на Unity.

FAQ

Что важнее сначала изучить: переменные или методы?

Я бы начал с переменных и базовых типов данных, потому что без них методы бесполезны. Потом методы, и сразу пробовать связывать их в маленьких скриптах. Понимание приходит через практику, а не через зубрёжку.

Почему скрипт в Unity называется компонентом?

Потому что он добавляется к GameObject и работает как часть его поведения, наравне с физикой, коллайдером или рендером. Это не отдельная программа, а модуль, который можно включать/выключать, настраивать в Inspector и комбинировать с другими компонентами.

Зачем нужен MonoBehaviour?

Это базовый класс, который даёт скрипту доступ к игровому циклу Unity и автоматическим методам вроде Start и Update. Без него скрипт — просто класс C#, не привязанный к сцене. MonoBehaviour — это мост между нашим кодом и движком.

Когда использовать Update, а когда FixedUpdate?

Update — для логики кадра и ввода. FixedUpdate — для физики и работы с движением через физическую систему. Если вы двигаете объект через Rigidbody, всегда делайте это в FixedUpdate, иначе получите нестабильное поведение при разной частоте кадров.

Можно ли писать всю логику в одном скрипте?

Технически да, но это плохая практика. Лучше разбивать поведение на небольшие компоненты и отдельные методы. Так код проще поддерживать, тестировать и переиспользовать. Unity поощряет композицию: много маленьких скриптов вместо одного гигантского.