Найти тему
ElandGames

Основы языка C# для создания игр #2: Классы, ссылки, списки и циклы

Оглавление

Изучив основные типы данных, методы и условия мы уже можем сделать игру, основанную на простых алгоритмах. Но придётся весь код писать в одной программе, выражаясь по "сиШарповски" в одном классе. Эта программа (класс) будет разрастаться семимильными шагами и очень скоро в этой "простыне" станет сложно что-либо найти, легко потерять и проще забить. :)

Классы в C#

Вместо того, чтобы пытаться запихнуть всё в одну программу (класс), будет гораздо удобнее разделить логику и код по разным классам (программам), в зависимости от того, к какому объекту они относятся.

Например, класс Game вполне может заниматься основными переменными игры, типа очки и номер уровня, а также методами "Начать новую игру" и "Пауза", но вот код для управления персонажем логично было бы вынести в отдельный класс под названием Player или PlayerController.

Классы могут работать параллельно и независимо, а могут и учитывать данные друг друга. Например, если в классе Game переменная isPause станет true, то Player не должен иметь возможность управлять персонажем, пока пауза не будет выключена.

Для того, чтобы один класс мог что-то узнать из другого класса нам понадобится ссылка на него!

Значимые и ссылочные типы данных

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

  • Значимые: int, float, bool и другие
  • Ссылочные: class, string, object и другие
Вот это поворот! Оказывается такой простой тип как string является классом!
Вот это поворот! Оказывается такой простой тип как string является классом!

Отличаются они в основном тем, как эти данные копируются и сравниваются. Например, уже знакомый нам тип string является ссылочным и при сравнении двух строковых переменных, будут сравниваться не их значения, а ссылки (на один ли объект они указывают). А вот int является значимым типом и при сравнении двух переменных типа int будут сравниваться их значения.

При обычном сравнении переменных типа string сравниваются ссылки, а чтобы сравнить значения двух переменных типа string, есть специальный метод Equals() и условие будет выглядеть вот так: if (text.Equals(text2))
При обычном сравнении переменных типа string сравниваются ссылки, а чтобы сравнить значения двух переменных типа string, есть специальный метод Equals() и условие будет выглядеть вот так: if (text.Equals(text2))

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

По сути string (строка) является набором элементов типа char (символ).
По сути string (строка) является набором элементов типа char (символ).

Ссылки между классами

Если мы хотим, чтобы один класс имел возможность работать с полями и методами другого класса, то мы делаем в первом классе ссылку на второй. При работе с Unity, мы просто создаём публичное поле с названием нужного класса и указываем название класса в качестве типа переменной. Размещаем скрипты с этими классами на объектах сцены в Unity и у нас два варианта эту ссылку получить:

1. В коде получить ссылку на объект с помощью метода FindObjectOfType<Game>()

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

2. В редакторе Unity в окне Inspector можно просто перетащить объект с нужным скриптом (содержащим этот класс) в поле скрипта висящего на другом объекте.

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

Теперь мы можем из класса Lesson обратится к классу Game и получить доступ к его полям и методам, если конечно они не против и имеют идентификатор доступа public.

Идентификаторы доступа к полям и методам

Когда мы объявляем поля и методы в нашем классе, то перед типом данных мы можем написать идентификатор доступа public, что даст возможность другим классам получить доступ к ним. По умолчанию, если мы ничего не напишем, то идентификатор доступа будет private. Можем и вручную private написать для наглядности. Это сделает поля и методы недоступными извне. В таком случае только сам класс, в котором они объявлены, жадно и ревностно будет ими пользоваться.

Зачем такие сложности и зачем запрещать доступ? Все ради безопасности! Чтобы посторонний класс, например написанный другим человеком, ничего в нашей игре не поломал. Вдруг этот посторонний класс сделает так, что аптечка будет добавлять не 100 здоровья, а минус 999 и главный герой будет все время умирать, пытаясь вылечиться. Ну, а мы будем искать проблему, которую возможно даже и не мы создали.

Видов идентификаторов доступа существует больше, но на первых порах public и private нам вполне хватит.

Вот так с помощью ссылки на класс Game, мы можем обращаться к его полям и методам
Вот так с помощью ссылки на класс Game, мы можем обращаться к его полям и методам

Работа с большим количеством данных

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

Это получается придётся для каждого параметра каждого объекта, которых например 100, завести переменную и в коде к каждой из них обращаться. Наш класс, по объёму повторяющегося кода, стремительно начнёт догонять произведение "Война и мир". Да и трудоемкость написания такого когда будет
просто колоссальная! А хороший программист - это ленивый программист и главный босс ленивых программистов придумал
массивы данных и списки.

Списки и массивы для работы с данными

Данные одного типа мы можем хранить в одном массиве и обращаться к каждому элементу этого массива по номеру. Например, в нашей игре много уровней и мы хотим куда-то сохранить данные о том, какие уровни пройдены, а какие нет. Вместо того, чтобы создавать 100 переменных типа bool, мы можем создать один массив данных типа bool со 100 элементами.

Давайте создадим массив на 100 элементов, в котором будет храниться информация какие уровни доступны для выбора, а какие нет. Массив объявляется просто указанием типа данных и добавление квадратных скобок [].

Но массив мало просто объявить. Он изначально пустой и не содержит ни одного элемента. Нужно его инициализировать, задав ему значение в виде нового экземпляра массива данных типа bool, указав в скобках количество элементов. По умолчанию все элементы примут значение false, 0 или null в зависимости от типа. В нашем случае будет false.

Если массив не инициализировать, то VisualStudio выдаст ошибку при попытке задать значение его элементу и скажет, что мы не можем обратится к массиву прежде, чем его инициализируем
Если массив не инициализировать, то VisualStudio выдаст ошибку при попытке задать значение его элементу и скажет, что мы не можем обратится к массиву прежде, чем его инициализируем

Теперь мы можем, указывая в скобках номер элемента, задавать ему значение true или false. Главное помнить, что нумерация в массивах идёт от нуля, а значит, нам доступны элементы с 0 до 99, т.к. мы задали общее количество 100.

Предположим, что метод OpenLevel() открывает нам первый уровень и вызывает метод UpdateLevels(), который делает иконки уровней активными.

Открывая один уровень, мы могли бы сразу его сделать активным и мудрить бы не пришлось. Но что если мы загрузили сохраненную игру и у нас весь массив openedLevels заполнился сохраненными значениями, которые нужно все обработать (у тех уровней, где значение true, сделать иконки активными). Тут нам на помощь придут циклы!

Циклы для работы с массивами

Обработать весь массив нам поможет цикл for, который может перебрать все 100 элементов, сделать для каждого проверку и выполнить действия в случае значения true.

У массива есть параметр Length (длина массива) равный количеству его элементов, который мы будем использовать в качестве аргумента условия цикла for. Цикл будет перебирать числа типа int от 0 и до Length, прибавляя каждый раз по 1 к своей внутренней переменной i. Для каждого значения i мы сделаем проверку в теле цикла - имеет ли элемент массива под этим номером значение true. Если условие истинно, то выполним включение иконки соответствующего уровня.

Перебираем каждый элемент массива и если он имеет значение true, то выполняем для него нужное действие
Перебираем каждый элемент массива и если он имеет значение true, то выполняем для него нужное действие

Списки и их отличие от массивов

По сути, список является более удобной версией массива данных и его разновидностью. Список объявляется точно также, как переменная внутри нашей программы, с помощью указания типа списка List<> и указания в треугольных скобках типа данных, которые в нём будут храниться.

Изначально список пуст и не содержит ни одного элемента. Но мы можем при объявлении, также как и массив, его инициализировать, задав нужное количество элементов. В списках вместо переменной Length используется переменная Count равная количеству элементов в списке.

Списки и массивы во многом ведут себя одинаково, но массивы работают быстрее, а списки гибче
Списки и массивы во многом ведут себя одинаково, но массивы работают быстрее, а списки гибче

Как видно, это практически тоже самое. Но список в отличии от массива может менять свою длину при добавлении и удалении элементов. Например, мы можем в списке хранить количество предметов в сумке у игрока. Их может быть 20, а может и не быть вовсе. Мы сразу узнаем об этом по значению Count.

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

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

Добавлять в список элементы можно методом Add, а убирать методом Remove. Причем убирать можем элемент под нужным номером RemoveAt(10), либо элемент с нужным значением Remove(false) - убрать все элементы равные false.
Добавлять в список элементы можно методом Add, а убирать методом Remove. Причем убирать можем элемент под нужным номером RemoveAt(10), либо элемент с нужным значением Remove(false) - убрать все элементы равные false.

Есть ещё очень удобная разновидность цикла for под название foreach, которая перебирает по очереди все элементы коллекции (коллекции - это общее название для массивов и списков). Она удобна, если нам не важен номер элемента в списке для действий над ним.

Метод ShowAllPlayers() просто выведет имена всех игроков, которые хранятся в списке playersName. А вот метод ShowTop10Players() выведет, только первые 10 имён игроков с порядковым номером, если конечно они вообще есть в списке.

Цикл foreach заметно короче и нагляднее, но применим не везде
Цикл foreach заметно короче и нагляднее, но применим не везде

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

Спасибо, что дочитали до конца! :) Если статья была для вас полезна и вы не отказались бы почитать продолжение, то подписывайтесь на канал, а чтобы смотивировать меня выпустить его как можно скорее можно поставить лайк или даже оставить комментарий! :)

Вот вам еще первая часть по C#:

И подборка статей по разработке игры на Unity:

Учимся делать игры и программировать на C# | Сергей Эланд | Дзен