Предисловие
Вообще, InScale — это моя первая игра не только на движке Unity3D, но и в принципе абсолютно первая игра, которую я решился сделать и куда-то выложить(Google Play). Из этого следуют некоторые не очевидные, а может и даже глупые поступки с моей стороны, о которых вы прочтёте под катом. Игру я сделал ещё в мае, так что на момент написания статьи она полностью готова, и сейчас я вспоминаю, какого оно было, в честь релиза.
8 мая. 15:00
На улице холодно. Идёт снег. А голова кипит от переизбытка мыслительных процессов. Немного отойдя от происходящего, а именно от осознания зимы длиною более чем в полгода в европейской части России(~конец октября-начало мая), разум достучался до меня, напомнив об идее игры для мобильных телефонов. Мало того, что у меня есть идея, так у меня ещё и Unity3D вместе со всем необходимым для разработки установлен на компьютере. Теперь горит не только голова, но и руки жаждущие всё реализовать как можно быстрее.
15:10
Суть в том, что на экране появляются две одинаковые геометрические фигуры: чёрная и белая. Внизу написано слово больше/меньше. В зависимости от этого, удержанием пальца на экране нужно белую фигуру сделать больше/меньше чёрной. При этом белая фигура появляется в случайном пространственном положении. Поэтому не ясно сколько времени нажимать надо. Если не понятно, что тут написано, вот скриншот.
Итак, зайдя в Unity, я думал, как назвать игра. Что делает игрок? Масштабирует объект до нужного масштаба. Scalable? Нет, странновато как-то. Тогда in scale? А чтоб выглядело лучше напишу как название класса: InScale. Окей с названием определился, но как я буду отрисовывать фигуры?
Код
15:15
Я сначала думал заготовить фигуры в виде спрайтов, но понял, что много времени потрачу на создание шаблонов в фотошопе. Тогда, ко мне пришла в голову мысль, что фигуры могут быть плоскими 3D моделями, т.е. набором полигонов, т.е. мешем. Набросав, небольшой код по генерации треугольника, подсмотренный в документации, я осознал, что, во-первых, мне нужно генерировать меш, который отображается с двух сторон, во-вторых генерировать несколько мешей, в-третьих, иметь набор фигур, где одна из них, выбранная случайным образом, будет отрисовываться. После недолгих размышлений на тему решение проблем было готово.
Собственно, один GameObject может иметь один меш, соотвественно, на одном пустом игровом объекте(scalablemesh) должны висеть два child'а, каждый для отрисовки плоского меша с двух сторон. Но объект-образец(samplemesh) на протяжении всего игрового процесса остаётся неподвижным, поэтому ему достаточно одного потомка. Это объясняет иерархию. И да, меш отрисовывается через метод. Пусть фигуры имеют разные размер и положение в пространстве, но сами по себе они одинаковые, следовательно рисовать их надо одним методом сразу. Реализация.
//на примере треугольника
private static Mesh[][] Trianglemesh(params GameObject[] objs)
{
Mesh[][] mesh_jagged = new Mesh[objs.Length][];
for (int i = 0; i < mesh_jagged.Length; i++)
{
mesh_jagged[i] = new Mesh[objs[i].transform.childCount];
for (int j = 0; j < objs[i].transform.childCount; j++)
{
mesh_jagged[i][j] = new Mesh();
GameObject oneofthecrowd = objs[i].transform.GetChild(j).gameObject;
MeshFilter body = oneofthecrowd.GetComponent<MeshFilter>();
Vector3[] vertices = new Vector3[]
{
new Vector3(-65, 0, z),//0
new Vector3(0, -112.58f, z),//1
new Vector3(65, 0, z),//2
};
Vector2[] UVs = new Vector2[]
{
new Vector2(0, 0),
new Vector2(0, 1),
new Vector2(1, 1)
};
int[] triangles = new int[]
{
0,2,1,
};
MeshInitialize(j, vertices, UVs, ref triangles, ref mesh_jagged[i], ref body);
}
}
return mesh_jagged;
}
//метод инициализации меша
private static void MeshInitialize(int index, Vector3[] verts, Vector2[] uvs, ref int[] tris, ref Mesh[] ms, ref MeshFilter mf)
{
if ((index + 1) % 2 == 0)
Reverse(ref tris);
ms[index].vertices = verts;
ms[index].triangles = tris;
ms[index].uv = uvs;
ms[index].RecalculateNormals();
mf.mesh = ms[index];
}
//хитрёж с вершинами полигонов для отрисовки с обратной стороны
private static void Reverse(ref int[] array)
{
for (int j = array.Length; j > (array.Length / 2); j--)
{
var temp = array[j - 1];
array[j - 1] = array[array.Length - j];
array[array.Length - j] = temp;
}
}
Как видно метод возвращает зубчатый массив или массив массивов. Почему? Потому что массивы-элементы имеют разную длину, что удобно в контексте иерархии игровых объектов-фигур и вышеописанных требований к методу отрисовки меша.
16:15
Значит, путём элементарной копипасты и антагонично сложных математико-геометрических вычислений, за часок другой, мною были набросаны 9 шаблонов для отрисовки. Но третья проблема никуда не исчезла. Как мы помним у нас набор фигур, то есть они должны размещаться в массиве, и случайный элемент будет вызывать отрисовку. То есть методы используются почти, как поля. Что-то такое в C# припоминается. Ах да, это же делегаты. Тогда я создал делегат MeshDelegate и массив того же типа с уже объявленными объектами, нацеленными на методы отрисовки.
Строчка с объявлением делегата:
delegate Mesh[][] MeshDelegate(params GameObject[] crowd);
Массив объектов делегата:
static MeshDelegate[] figures = new MeshDelegate[]
{
new MeshDelegate(Trianglemesh),
new MeshDelegate(Planemesh),
new MeshDelegate(Pentamesh),
new MeshDelegate(Hexamesh),
new MeshDelegate(Swanmesh),
new MeshDelegate(Pacmanmesh),
new MeshDelegate(Weightmesh),
new MeshDelegate(Fishmesh),
new MeshDelegate(Ufomesh)
};
И, барабанная дробь, код, отвечающий за отрисовку полигонов:
figures[Random.Range(0, figures.Length)].Invoke(samplemesh, scalablemesh);
Вообще можно было не создавать свой делегат, а использовать обобщённый, но тогда исключается использование модификатора params. В остальном всё тоже самое.
Также можно было создать один объект делегата MeshDelegate, подписать на него все методы, и вызывать один из них таким образом.
//пусть md - описанный объект делегата MeshDelegate
var methods = md.GetInvocationList();
MeshDelegate temp = (MeshDelegate)methods[Random.Range(0, methods.Length)];
temp.Invoke(samplemesh, scalablemesh);
Но этот вариант показался мне более костыльным. Предлагайте в комментах свои варианты решения задачи вызова случайного метода из имеющегося набора.
16:20
Господи, предпраздничный день, мне 16 лет, все мои друзья по впискам на хаты разошлись, а я тут какой-то фигнёй страдаю. Ах да, мне дизайн с саундтреком делать надо, а не ныть.
Музыка
16:25
Для таких несложных проектов как у меня существует куча ресурсов с бесплатной музыкой без авторских прав(ссылки в спойлере), но мне лень бродить по ним в поисках "того, не знаю чего". Поэтому я написал своему другу, начинающему диджею, попросив его сделать простенький закольцованный трек для фона. Пока он его делал, я решил заняться "изготовлением" пользовательского интерфейса или же попросту засел за дизайн.
http://freesound.org/
http://99sounds.org/
http://www.noiseforfun.com/
http://incompetech.com/
https://opengameart.org/
http://raisedbeaches.com/octave/
https://musopen.org/ru/
https://www.playonloop.com/
http://www.bensound.com/
https://www.soundeffectsplus.com/
http://dig.ccmixter.org/
http://www.soundgator.com/
http://www.pacdv.com/
http://www.freesfx.co.uk/
http://soundtrack.imphenzia.com/
Дизайн
Чтобы понять, что представляет из себя мой дизайн, и вообще его концепция, вот вам абстрактный мемес.
Солипсизм — это философское представление о том, что ничего нет, кроме источника самого представления (наблюдателя, сущности). Точнее, солипсист не уверен, есть ли что-то, кроме его ощущений. А если и есть, то непонятно, что оно такое.
В общем, я решил вообще не париться по поводу дизайна и сделать его настолько минималистичным, что его как бы и нет. Но для моего восприятия он же есть.
16:30
Конечно же, разработку дизайна в моём понимании надо начинать с фона. Учитывая, что фигуры черного и белого цвета, я решил не использовать эти цвета на фоне. И тут я вспомнил панель регистрации инстаграма, а точнее как там цвета прикольно и плавно перетекают друг в друга, и сделал что-то похожее, используя более менее сочные цвета.
16:40
Остальная часть интерфейса стала состоять лишь из небольшого количества текста и кнопок, на рисовании текстур для которых я застрял. А застрял почему? В первую очередь, передо мной стоял вопрос "а что рисовать", а потом "как рисовать". Естественно, ответ на первый вопрос пришёл дольше. Так вот, кнопки в моей игре есть белые кружки с вырезанными иконками, чтобы цвет внутри тоже менялся. Ноль текста(ну только на кнопке play, и то он вырезан), но небольшие затруднениягеморой с вырезанием.
17:40
Друг прислал трек, я его встроил, все в порядке. Дело шло к иконке.
17:50
Тогда вроде была модной длинная тень, вот я и сделал просто надпись с тенью в рамочке. Получилось так. Впрочем ничего нового.
18:00
Собравши .apk файл, я обнаружил критический недостаток 25 зелёных для регистрации аккаунта разработчика, поэтому отложил игру до тех пор, пока не что-нибудь отчего доллары появятся.
Итог
Волшебным образом оказавшиеся у меня 25 бачинских довели дело до конца. Это моя первая игра, поэтому монетизацией и особо сильным продвижением я заниматься не стал. Из сложного хочется выделить создание визуальной части проекта, так как фотошоп откровенно не такое уж и моё. Получилось в итоге что-то вроде этого.
А пока за сим всё. Откланиваюсь и иду готовиться к ЕГЭ и искать подработку.
Комментарии (5)
GeorgWarden
03.01.2018 16:53Буду честен, я очень сильно сгорел по поводу этой статьи. Почему парень, который едва освоил школьный курс программирования и Юнити пилит низкокачественный контент и заливает ещё про это статью на Хабр, а потом ещё получает за это рейтинг? Не подумайте, что я завидую. Я негодую! В чем смысл этой статьи? Она абсолютно никакая, это не туториал, не какая-то редкая годнота, вроде как повысить ФПС без ущерба картинки. Парень просто наговнокодил и сделал себе на этом пиар. Хабр вообще ещё можно считать хорошим ресурсом?
KpoKec
03.01.2018 16:56Достаточно было двухсторонний материал вместо двух мешей склееных.
Есть делегат System.Action, у него от 0 до 16 параметров есть перегрузки.Stefanio Автор
03.01.2018 17:01Про материалы, честь слово не знал, возьму на заметку, спасибо за предложенный вариант.
Про то, почему я не использую обобщённые делегаты, написал, но видимо недостаточно ясно. Главное, что при использовании System.Action я не могу использовать модификатор params, а он мне нужен для обработки массива типа GameObjeсt внутри метода построения фигур, потому что я проектировал его так, чтобы максимально не зависеть от того, сколько игровых объектов придётся обработать таким образом. Это просто задел на будущее.
arthur_khazbs
Годная игра
https://play.google.com/store/apps/details?id=com.PseeHeesStudio.InScale
robit
Задумка отличная, но реализация храмает. Можно просто зажать чтобы фигура сильно уменьшилась/увеличилась, и всё.