Найти в Дзене
Сергей Эланд

Основы языка C# для создания игр #3: Перечисления, switch и структуры

У нас в арсенале уже есть основные типы данных, классы, методы, условия, списки и циклы. Из этого можно слепить неплохой функционал для простой игры. Однако, чем сложнее будет становиться игра, тем более изящные решения потребуются, чтобы избежать повторений в коде и сохранить его читаемость. Поэтому нам стоит познакомиться с такими штуками как перечисления, оператор switch и структуры. Перечисления в языке C# Перечисления - это уже более сложный тип данных, представляющий из себя список констант и их имён. Благодаря именам этих констант, мы можем сделать код намного более наглядным и читаемым. Константы, в отличии от переменных, получают свое значение при объявлении и в дальнейшем оно не меняется. В коде если к полю (переменной) добавить const, то переменная станет константой и меняться уже не будет. В перечислении же ничего указывать не надо - там одни константы. Самый простой пример это перечисление дней недели. Перечисление объявляется с помощью слова enum, вне класса - отдельно. В
Оглавление

У нас в арсенале уже есть основные типы данных, классы, методы, условия, списки и циклы. Из этого можно слепить неплохой функционал для простой игры. Однако, чем сложнее будет становиться игра, тем более изящные решения потребуются, чтобы избежать повторений в коде и сохранить его читаемость. Поэтому нам стоит познакомиться с такими штуками как перечисления, оператор switch и структуры.

Перечисления в языке C#

Перечисления - это уже более сложный тип данных, представляющий из себя список констант и их имён. Благодаря именам этих констант, мы можем сделать код намного более наглядным и читаемым.

Константы, в отличии от переменных, получают свое значение при объявлении и в дальнейшем оно не меняется. В коде если к полю (переменной) добавить const, то переменная станет константой и меняться уже не будет.

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

В перечислении же ничего указывать не надо - там одни константы. Самый простой пример это перечисление дней недели.

Перечисление объявляется с помощью слова enum, вне класса - отдельно. Внутри через запятую записываются имена констант, а рядом с ними через знак равенства им присваивается значение. Но значение можно и не указывать. По умолчанию первое значение будет 0, а каждое последующее увеличивается на 1. Если же мы явным образом зададим первое значение равным 1, то остальные по умолчанию также будут увеличиваться на 1 и нумерация получится с 1 до 7.

В перечислениях порой даже не важны значения, главное список имён.
В перечислениях порой даже не важны значения, главное список имён.

Мы можем задать любые целочисленные значения. Это могут быть коды, удобные нам числа или просто нумерация, но главное, что нам не придётся её запоминать - работать в коде мы будем с именами констант перечисления.

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

Константы перечислений можно использовать как и любые целочисленными переменные - сравнивать и производить математические действия, например сложение.

Давайте придумаем какой-то пример для создания игры. Например, создадим перечисление с расами игровых персонажей Race. Информация о каждом игровом персонаже будет храниться в отдельном экземпляре класса Unit.

В одном скрипте можно прописать много классов  и перечислений, а можно разделить их по отдельным файлам.
В одном скрипте можно прописать много классов и перечислений, а можно разделить их по отдельным файлам.

Сделаем метод CreateUnit(), который будет создавать нового юнита и в зависимости от его расы изменять его максимальное количество здоровья. Новый экземпляр класса создаётся с помощью оператора new и скобок () после имени класса.

По умолчанию у нового экземпляра класса Unit значение поля Race будет первым значением из перечисления, т.е. Race.Human
По умолчанию у нового экземпляра класса Unit значение поля Race будет первым значением из перечисления, т.е. Race.Human

В качестве входного параметра мы получаем переменную типа Race и присваиваем её в качестве значения для расы нового юнита. Затем, в зависимости от его расы, присваиваем ему нужное значение здоровья.

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

Новый экземпляр класса Unit после присвоения нужных значений сохраняется в списке юнитов units.
Новый экземпляр класса Unit после присвоения нужных значений сохраняется в списке юнитов units.

Когда в нашей программе появляются большие ветвления условий, как например тут с разновидностями рас, то эффективнее будет использовать условный оператор switch ().

Оператор сопоставления значений switch

Когда нам нужно сравнивать значения, то вместо конструкции if else будет лучше использовать конструкцию switch. Это и выглядит нагляднее (хотя на мой взгляд это спорно), и работает более оптимизированно.

В скобках оператора switch указываем то, с чем будем сравнивать. Для каждого варианта создаём строчку case со значением. После него пишем код, который нужно выполнить в этом случае. А после него break, который разграничивает код для разных вариантов. В конце можно также добавить вариант default, который будет срабатывать если ни одно из описанных нами значений не совпало (по аналогии с else).

Выглядит всё это конечно более громоздким, но под капотом работает иначе, нежели if else и на удивление быстрее, если вариантов будет много.
Выглядит всё это конечно более громоздким, но под капотом работает иначе, нежели if else и на удивление быстрее, если вариантов будет много.

Перечисления enum и оператор switch частенько используют вместе и в случае простых действий все это выглядит нормально. Но, что если нам нужно, при создании юнита, задать ему много разных параметров? Оператор switch разрастётся до огромных масштабов, если после каждого case указывать несколько строчек присвоения значения.

Как одно из решений, можно использовать в этом случае структуру.

Структуры

Структуры struct - это очень интересная штука и может содержать в себе переменные и методы - прямо как класс, но при этом является значимым типом. Это значит, что она копируется, а не передается ссылкой.

Мы можем использовать её для хранения значений множества параметров наших юнитов.

Структура с 4 параметрами для юнитов
Структура с 4 параметрами для юнитов

В структурах можно прописать специальный метод (конструктор), с помощью которого будет удобно создавать новые экземпляры с нужными значениями.

Структура с конструктором для удобного создания экземпляров
Структура с конструктором для удобного создания экземпляров

Используя этот конструктор, мы можем создать структуры с настройками для каждого класса. Например, в методе Start(). Для новых экземпляров структуры нужно объявить поля, чтобы потом иметь возможность к ним обращаться, ну или иными словами, чтобы их где-то хранить.

Не самый изящный способ задания параметров, но об этом ниже
Не самый изящный способ задания параметров, но об этом ниже

Создав экземпляры структуры с настройками для разных классов, мы можем использовать её при создании юнитов и задания им параметров. Для этого в классе юнита объявим поле unitSettings.

В поле unitSettings будет хранится пачка из 4 параметров.
В поле unitSettings будет хранится пачка из 4 параметров.

Метод создания юнитов по объёму не увеличится, а параметров мы зададим в 4 раза больше.

Задание параметров юниту одной строчкой.
Задание параметров юниту одной строчкой.

Теперь мы можем обратится к каждому параметру юнита через структуру, либо разом задать ему одно из значений всей структуры, как мы это сделали выше.

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

При повышении уровня бонус здоровья получат все, но другие бонусы в зависимости от расы.
При повышении уровня бонус здоровья получат все, но другие бонусы в зависимости от расы.

Возможно это не самый удачный пример для большого количества параметров, т.к. если их будет под сотню, то конструктор такой структуры будет слишком громоздким, но для небольших пачек переменных этот способ будет вполне неплохим решением. Например, можно сделать в виде структуры цену, которая состоит из 3 разных видов ресурсов (золото, камень и дерево).

Вот только не очень хорошо, что параметры юнитов мы задаём в коде "магическими числами".

Частая ошибка: Магические числа

"Магические числа" - это термин, означающий явно указанные числовые значения в коде. Непонятно откуда взялись и что вообще значат. Они затрудняют чтение и понимание кода. По-хорошему, все значения должны хранится в переменных с говорящими названиями. Например, humanHealth. И тогда инициализация структуры будет выглядеть более читаемо.

Тут уже магических чисел нет, но приходится держать кучу полей переменных для настроек каждой расы.
Тут уже магических чисел нет, но приходится держать кучу полей переменных для настроек каждой расы.

В некоторых случаях магические числа можно вынести в отдельную переменную, но когда их много, то делают специальные конфиги, чтобы считывать значения оттуда. До этого мы еще дойдём, а пока стоит научиться не вставлять в свой код числа непонятного происхождения, как бы быстро и удобно это не казалось. Потому-что вернувшись к своему коду спустя месяц есть очень высокая вероятность ничего не понять и долго ломать голову над тем, зачем вообще здесь эти цифры. :)

С каждой статьёй темы всё сложнее и их все сложнее объяснять простыми словами. :) Но все эти штуки реально встречаются и их использование нехило повышает качество нашего кода, поэтому стоит разобраться! :)