[Из песочницы] Как создать игру, если ты ни разу не художник

96hpx0qpk4w0_hmjm3csu0lj92o.png


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

И не надо…

Небольшое вступление


Сразу оговорюсь: нашей целью не является зарабатывание денег — на Хабре полно статей на эту тему. Нет, мы будем делать игру мечты.

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

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

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


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

Мы не будем использовать новомодные игровые движки, фреймворки, библиотеки — мы заглянем в самую суть игрового процесса и прочувствуем его изнутри. Откажемся от гибких методологий разработки (задача упрощается необходимостью организовать работу всего одного человека). Мы не будем тратить время и силы на поиски дизайнеров, художников, композиторов и спецов по звуку — мы все сделаем сами, как умеем (но при этом сделаем все по-умному — если вдруг художник у нас появится, нам не составит особых усилий прикрутить модную графику на готовый каркас). В конце концов, мы даже не будем особо изучать инструментарий и выбирать подходящий — сделаем на том, который хорошо знаем и умеем пользовать. Например, на Java, чтоб потом, если нужно, перенести на Андроид (или на кофеварку).

«А!!! Ужас! Кошмар! Как на такую чушь вообще можно время тратить! Проваливай отсюда, я пойду что-то более интересное почитаю!»

Зачем это делать? В смысле, велосипед изобретать? Почему бы не использовать готовый игровой движок? Ответ прост: мы ничего про него не знаем, а игру хотим уже сейчас. Представьте образ мысли среднестатистического программиста: «Хочу делать игру! Там будет мясо, и взрывы, и прокачка, и можно грабить корованы, и сюжет бомбезный, и такого вообще никогда и нигде больше не было! Начну писать прямо сейчас!… А на чем? Посмотрим, что у нас сейчас популярно… Ага, X, Y и Z. Возьмем Z, на нем сейчас все пишут…». И начинает изучать движок. А идею бросает, потому что на нее уже времени не хватает. Fin. Или ладно, не бросает, но толком не изучив движок, принимается за игру. Хорошо, если потом ему хватит совести никому не показывать свою первую «поделку». Обычно нет (зайдите в любой магазин приложений, посмотрите сами) — ну как же, хочется прибылей, нет сил терпеть. Когда-то создание игр было уделом увлеченных творческих людей. Увы, это время безвозвратно прошло — сейчас в игре главное не душа, а бизнес-модель (по крайней мере, разговоров о ней на порядок больше). У нас же цель простая: мы будем делать игры с душой. Потому абстрагируемся от инструмента (подойдет любой) и сосредоточимся на задаче.

Итак, продолжим.
Не буду вдаваться в подробности собственного горького опыта, но скажу, что одна из основных проблем для программиста при разработке игр — это графика. Рисовать программисты обычно не умеют (хотя бывают исключения), а художники обычно не умеют программировать (хотя бывают исключения). А без графики, согласитесь, редкая игра обходится. Что же делать?

Варианты есть:

1. Нарисовать все самому в простом графическом редакторе
5bno-orbfc3-ov3esi3ld_ezdqs.png
Скриншоты игры «Kill Him All», 2003 год


2. Нарисовать все самому в векторе
46dju8rama3wvfx2fzopin-yfiw.png
Скриншоты игры «Raven», 2001 год

jnmmvzujgfsziasffz8m6sitmi0.png
Скриншоты игры «Inferno», 2002 год


3. Попросить брата, который тоже не умеет рисовать (но делает это чуть лучше)
tftxjcy5luf045ehrm5_kdb5fzy.png
Скриншоты игры «Грёбаный», 2004 год


4. Скачать какую-то программу для 3D-моделирования и натаскать оттуда ассетов
k9njo6wryhhmaiagpool44qt5ua.jpeg
Скриншоты игры «Грёбаный 2. Демо», 2006 год


5. В отчаянии рвать волосы на голове
3pfw013bspmxrjyyewsnsgqobma.png
oagxgo1wmfykrll-ji-qwyrsv7e.png
Скриншоты игры «Грёбаный», 2004 год


6. Нарисовать все самому в псевдографике (ASCII)
dbhts5ryeccznntuaiiwjmx32fo.png
Скриншоты игры «Fifa», 2000 год

q0u-vuxozevapmdwswaygicakgg.png
Скриншоты игры «Sumo», 1998 год


Остановимся подробнее на последнем (отчасти потому что он выглядит не так уныло как остальные). Многие неопытные геймеры считают, что игры без крутой современной графики не способны покорить сердца игроков — их даже играми-то назвать язык не поворачивается. Подобным аргументам молчаливо возражают разработчики таких шедевров, как ADOM, NetHack и Dwarf Fortress. Внешний вид не всегда является решающим фактором, использование же ASCII дает некторые интересные примущества:

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


Приведенное выше длинное вступление имело целью помочь начинающим игроделам побороть страхи и предрассудки, перестать волноваться и все ж таки попробовать что-нибудь эдакое сотворить. Готовы? Тогда приступим.

Шаг первый. Идея


Как? У вас все еще нет идеи?

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

А еще можно копировать чужие игры. Ну как, копировать. Конечно, не драть безбожно, рассказывая на каждом углу, какой вы сообразительный, но использовать чужие наработки в своем продукте. Как много после этого в нем останется конерктно от вашей мечты — вопрос второстепенный, ибо частенько у геймеров бывает так: вот все нравится в игре, кроме каких-то двух-трех раздражающих вещей, а вот если бы тут сделать по-другому… Кто знает, возможно, доведение до ума чьей-то хорошей идеи — это и есть ваша мечта.

Но мы пойдем простым путем — предположим, что идея у нас уже есть, и мы над ней долго не думали. В качестве первого нашего грандиозного проекта будем делать клон хорошей игры от Obsidian — Pathfinder Adventures.

«Это что еще за бред! Настолки какие-то?»

Как говорится, pourquoi pas? Предрассудки мы, кажись, уже оставили, а потому смело начинаем отшлифовывать идею. Естественно, клонировать игру один к одну мы не будем, но основные механики позаимствуем. К тому же реализация пошаговой настольной кооперативной игры имеет свои преимущества:

  • она пошаговая — это позволяет не заботиться о таймерах, синхронизации, оптимизации, FPS и прочих муторных вещах;
  • она кооперативная, то есть игрок или игроки соревнуются не друг против друга, а против некоего «окружения», играющего по детерминированным правилам — это избавляет от необходимости программировать ИИ (AI) — одного из самых сложных этапов разработки игр;
  • она осмысленная — настолщики вообще люди прихотливые, во что попало играть не будут: им подавай продуманные механики и интересный геймплей — на одной красивой картинке не выедешь (чем-то знакомым отдает, не так ли?);
  • она с сюжетом — многие киберспортсмены не согласятся, но лично для меня игра должна рассказывать интересную историю — как книга, только с использованием своих особых художественных средств.
  • она занятная, что на любителя — описываемые подходы можно будет применить к любой последующей мечте, сколько бы их у вас ни было.


Для не знакомых с правилами, краткое введение:
Pathfinder Adventures — это цифровая версия настольной карточной игры, созданной на основе настольной ролевой игры (а вернее, целой ролевой системы) Pathfinder. Игроки (в количестве от 1 до 6) выбирают себе персонажа и вместе с ним отправляются в приключение, разбитое на некоторое количество сценариев. Каждый персонаж имеет в своем распоряжении карты разных типов (как то: оружие, броня, заклинания, союзники, предметы итп), при помощи которых в каждом сценарии должен найти и жестоко покарать Негодяя — специальную карту с особыми свойствами.

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

Для победы над картами (и для приобретения новых) персонажи должны пройти проверку одной из своих характеристик (стандартные для РПГ сила, ловкость, мудрость итп), кинув кубик, размер которого определяется значением соответствующей характеристики (от d4 до d12), добавив модификаторы (определяемые правилами и уровнем развития персонажа) и играя для усиления эффекта походящие карты из руки. При победе встреченная карта либо убирается из игры (если это враг), или пополняет руку игрока (если это предмет) и ход переходит к другому игроку. При проигрыше персонажу часто наносится урон, заставляющий его сбрасывать карты из руки. Интересная механика состоит в том, что здоровье персонажа определяется количеством карт в его колоде — как только игроку нужно вытащить из колоды карту, а их нет — его персонаж погибает.

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

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


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

В нашем случае сделаем одно глобальное концептуальное изменение — откажемся от карт. Вернее, не откажемся вовсе, но заменим карты на кубики, по-прежнему разных размеров и разных цветов (технически, не совсем корректно навывать их «кубики», так как кроме правильного шестигранника присутствуют и другие формы, но называть их «кости» мне непривычно и неприятно, а пользоваться американизмом «дайсы» — и вовсе признак дурного тона, потому оставим как есть). Теперь вместо колод у игроков будут мешочки. И у локаций тоже будут лежать мешочки, из которых игроки в процессе исследования будут вытаскивать произвольные кубики. Цвет кубика будет определять его тип и, соответственно, правила прохождения проверки. Личные характеристики персонажа (сила, ловкость итп), как следствие, упразднятся, но зато появятся новые интересные механики (о чем позже).

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

Шаг второй. Дизайн


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

Поначалу ваш дизайн-документ будет выглядеть как-то так
oiqwfpjf6vasnpmy6ucqglbnwy8.jpeg

ffgu7qx80qefetycgrmdraoetv8.jpeg


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

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

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

«Автор, убей себя об стену. Слишком много букв.»

Шаг третий. Моделирование


То есть, все тот же design, только более подробный.
Знаю, многим уже не терпится открыть IDE и начать кодить, но потерпите еще немного. Когда идеи переполняют нашу голову, нам кажется, что стоит лишь прикоснуться к клавиатуре, и руки сами понесутся в заоблачные дали — не успеет кофе вскипеть на плите, как рабочая версия приложения уже будет готова… отправиться в мусор. Чтобы много раз не переписывать одно и то же (а особенно чтобы не убеждаться через три часа разработки, что макет нерабочий и нужно начинать заново), предлагаю для начала хорошенько продумать (и задокументировать) основную структуру приложения.

Поскольку мы, как разработчики, хорошо знакомы с объектно-ориентированным программированием (ООП), будем использовать его принципы в нашем проекте. А для ООП нет ничего более ожидаемого, чем начать разработку с кучи нудных UML-диаграм. (Как, вы не знаете, что такое UML? Я тоже уже почти забыл, но с радостью вспомню — просто чтобы показать, какой я прилежный программист, хе-хе.)

Начнем, пожалуй, с диаграммы «вариантов использования» (use-case). Изобразим на ней способы взаимодействия нашего пользователя (игрока) с будущей системой:

Варианты использования
rtphstumyokd9_gnpgzoewpaux8.png


«Э… это что вообще?»

Шучу-шучу… и, пожалуй, на этом прекращаю шутить — дело-то серьезное (мечта, как-никак). На диаграмме вариантов использования необходимо отобразить возможности, которые система предоставляет пользователю. В подробностях. Но так уж исторически сложилось, что именно данный тип диаграмм получается у меня хуже всего — терпения не хватает, судя по всему. И не надо на меня так смотреть — мы не в ВУЗе диплом защищаем, а получаем удовольствие от рабочего процесса. И для данного процесса не так важны варианты использования. Гораздо важнее грамотно разбить приложение на независимые модули, то есть реализовать игру таким образом, чтобы особенности визуального интерфейса не влияли на игровые механики, и чтобы графическую составляющую при желании можно было легко изменить.

Этот момент можно детализировать на следующей диаграмме компонентов (components):

Компоненты системы
lmbrbrdoyppboylxuoopjr--d5u.png


Здесь мы уже выделили конкретные подсистемы, входящие в состав нашего приложения и, как будет показано дальше, все они будут разрабатываться независимо друг от друга.

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

Если стоите, присядьте
o-f6evthl0hpqu0rhwlejbxyhty.png


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

Колбаски
hsjo_dinyd5genlr77jrgz0ouys.png


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

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

Шаг четвертый. Выбор инструментов


Как уже было условлено, разрабатывать будем кроссплатформенное приложение, работающее как на десктопах под управлением различных операционных систем, так и на мобильных устройствах. В качестве языка программирования выберем Java, а еще лучше Kotlin, так как последний более нов и свеж, и еще не успел искупаться в волнах негодования, с головой захлестнувших его предшественника (заодно подучим, если кто еще не владеет). JVM, как вы знаете, доступен везде и всюду (на трех миллиардах устройств, хе-хе), будем поддерживать и Windows, и UNIX, и даже на удаленном сервере через SSH-подключение можно будет играть (кому это может понадобиться — неизвестно, но возможность такую предоставим). На Андроид тоже перенесем, когда разбогатеем и наймем художника, но об этом позже.

Библиотеки (без них никуда не деться) будем выбирать соответственно нашему требованию кроссплатформенности. В качестве системы сборки будем использовать Maven. Или Gradle. Или все ж таки Maven, начнем с него. Сразу советую настроить систему контроля версий (любую, какая больше нравится), чтобы легче было через много лет с ностальгическими чувствами вспоминать, как было здорово когда-то. IDE тоже выбирайте привычную, любимую и удобную.

Собственно, больше нам ничего и не нужно. Можно приступать к разработке.

Шаг пятый. Создание и настройка проекта


Если вы используете IDE, то создать проект — дело тривиальное. Нужно только выбрать для нашего будущего шедевра какое-то звучное имя (например, Dice), не забыть включить поддержку Maven в настройках, и в файле pom.xml прописать необходимые идентификаторы:

4.0.0
my.company
dice
1.0
jar


Также добавим поддержку Kotlin, по умолчанию отсутствующую:


    org.jetbrains.kotlin
    kotlin-stdlib
    ${kotlin.version}



и некоторые настройки, на которых не станем подробно останавливаться:


    UTF-8
    1.8
    1.8
    1.3.20
    true



Немного информации касательно гибридных проектов
Если в своем проекте вы планируете одновременно использовать и Java, и Kotlin то кроме папки src/main/kotlin у вас также будет присутствовать папка src/main/java. Разработчики языка Kotlin утверждают, что исходные файлы из первой папки (*.kt) должны компилироваться раньше, чем исходные файлы из второй (*.java) и потому настоятельно рекомендуют изменить настройки стандартных целей Maven:

    

        
            org.jetbrains.kotlin
            kotlin-maven-plugin
            ${kotlin.version}
            
                
                    compile
                    process-sources
                    
                        compile
                    
                    
                        
                            ${project.basedir}/src/main/kotlin
                            ${project.basedir}/src/main/java
                        
                    
                
                
                    test-compile
                    
                        test-compile
                    
                    
                        
                            ${project.basedir}/src/test/kotlin
                            ${project.basedir}/src/test/java
                        
                    
                
            
        

        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.5.1
            
                
                
                    default-compile
                    none
                
                
                
                    default-testCompile
                    none
                
                
                    java-compile
                    compile
                    
                        compile
                    
                
                
                    java-test-compile
                    test-compile
                    
                        testCompile
                    
                
            
        

    



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


Создадим сразу три пакета (чего мелочиться-то?):

  • model — для классов, описывающих объекты игрового мира;
  • game — для классов, реализующих игровой процесс;
  • ui — для классов, отвечающих за взаимодействие с пользователем.


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

Не пытайтесь сразу делать идеально: продумывать до мелочей названия пакетов, интерфейсов, классов и методов; досконально прописывать взаимодействие объектов между собой — все это будет меняться, и не один десяток раз. По мере развития проекта многие вещи будут казаться вам некрасивыми, громоздкими, неэффективными и тому подобное — смело меняйте их, благо рефакторинг в современных IDE — весьма дешевая операция.

Создадим также класс c функцией main и мы готовы к великим свершениям. Для запуска можно использовать саму IDE, но как вы в дальнейшем убедитесь, для наших целей этот способ не подходит (стандартная консоль IDE не способна как следет отобразить наши графические изыскания), потому настроим запуск извне, про помощи batch (или shell в системах UNIX) файла. Но перед этим, сделаем кое-какие дополнительные настройки.

После выполнения операции mvn package мы получим на выходе JAR-архив со всеми скомилированными классами. Во-первых, по умолчанию в состав этого архива не входят зависимоти, необходимые для работы проекта (пока что их у нас нет, но в будущем обязательно появятся). Во-вторых, в файле-манифесте архива не прописан путь к главному классу, содержащему метод main, поэтому запустить проект командой java -jar dice-1.0.jar у нас не выйдет. Исправим это, добавив дополнительные настройки в pom.xml:


    

        
            maven-assembly-plugin
            2.6
            
                
                    package
                    
                        single
                    
                
            
            
                
                    jar-with-dependencies
                
                
                    
                        my.company.dice.MainKt
                    
                
            
        

    



Обратите внимание на название главного класса. Для функций Kotlin, содержащихся вне классов (как, например, функции main) при компиляции все равно создаются классы (потому как JVM ничего другого не знает и знать не желает). В качестве имени этого класса используется имя файла с добавкой Kt. То есть, если главный класс вы назвали Main, то скомпилирован он будет в файл MainKt.class. Именно этот последний мы и должны указывать в манифесте jar-файла.

Теперь при сборке проекта мы будем получать на выходе два jar-файла: dice-1.0.jar и dice-1.0-jar-with-dependencies.jar. Нас интересует второй. Напишем для него скрипт запуска.

dice.bat (для Windows)

@ECHO OFF

rem Compiling
call "path_to_maven\mvn.bat" -f "path_to_project\Dice\pom.xml" package
if errorlevel 1 echo Project compilation failed! & pause & goto :EOF

rem Running
java -jar path_to_project\Dice\target\dice-1.0-jar-with-dependencies.jar
pause


dice.sh (для UNIX)

#!/bin/sh

# Compiling
mvn -f "path_to_project/Dice/pom.xml" package
if [[ "$?" -ne 0 ]] ; then
  echo 'Project compilation failed!'; exit $rc
fi

# Running
java -jar path_to_project/Dice/target/dice-1.0-jar-with-dependencies.jar


Обратите внимание, при неудачной компиляции мы вынуждены прервать выполнение скрипта. Иначе будет запущена не последний арфив, а файл, оставшийся от предыдущей успешной сборки (иногда мы и разницу-то не обнаружим). Часто разработчики используют команду mvn clean package для удаления всех скомпилированных ранее файлов, но в этом случае весь процесс компиляции всегда будет начинаться с самого начала (даже если исходный код не менялся), что займет уйму времени. А ждать мы не можем — нам игру нужно делать.

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

Шаг шестой. Основные объекты


Постепенно начнем наполнять пакет model необходимыми для игрового процесса классами.

Диаграмма классов
lvde4t7282puwgyjgl6qrooazka.png


Кубики — наше все, добавим их в первую очередь. Каждый кубик (экземпляр класса Die) характеризуется типом (цветом) и размером. Для типов кубика заведем отдельное перечисление (Die.Type), размер отметим целым числом от 4 до 12. Также реализуем метод roll(), который будет выдавать произвольное, равномерно распределенное число из доступного кубику диапазона (от 1 до значения размера включительно).

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

class Die(val type: Type, val size: Int) : Comparable {

    enum class Type {
        PHYSICAL, //Blue
        SOMATIC, //Green
        MENTAL, //Purple
        VERBAL, //Yellow
        DIVINE, //Cyan
        WOUND, //Gray
        ENEMY, //Red
        VILLAIN, //Orange
        OBSTACLE, //Brown
        ALLY //White
    }

    fun roll() = (1.. size).random()

    override fun toString() = "d$size"

    override fun compareTo(other: Die): Int {
        return compareValuesBy(this, other, Die::type, { -it.size })
    }
}


Чтобы не пылились, кубики хранятся в сумочках (экземплярах класса Bag). О том, что творится внутри сумки, можно лишь догадываться, потому нет смысла использовать упорядоченную коллекцию. Вроде бы. Наборы (sets) хорошо реализуют нужную нам идею, но не подходят по двум причинам. Во-первых, при их использовании придется реализовывать методы equals() и hashCode(), причем непонятно каким образом, так как сравнивать типы и размеры кубиков неверно — в нашем наборе может храниться любое количество идентичных кубиков. Во-вторых, вытягивая кубик из сумки, мы ожидаем получить не просто что-то недетерминированное, но случайное, каждый раз разное. Потому советую все же использовать упорядоченную коллекцию (список) и перемешивать ее каждый раз при добавлении нового элемента (в методе put()) или непосредственно перед выдачей (в методе draw()).

Метод examine() подойдет для случаев, когда уставший от неопределенности игрок в сердцах вытряхнет содержимое сумки на стол (обратите внимание на сортировку), а метод clear() — если вытряхнутые кубики больше в сумку не вернутся.

open class Bag {

    protected val dice = LinkedList()
    val size
        get() = dice.size

    fun put(vararg dice: Die) {
        dice.forEach(this.dice::addLast)
        this.dice.shuffle()
    }

    fun draw(): Die = dice.pollFirst()
    fun clear() = dice.clear()
    fun examine() = dice.sorted().toList()
}


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

class Pile : Bag() {
    fun removeDie(die: Die) = dice.remove(die)
}


Теперь перейдем к нашим главным действующим лицам — героям. То бишь, персонажам, которых отныне будем называть героями (есть весомая причина не называть свой класс именем Character в Java). Герои бывают разных типов (сиречь классов, хотя слово class лучше тоже не использовать), но для нашего рабочего прототипа возьмем лишь два: Brawler (то есть, Fighter с упором на стойкость и силу) и Hunter (он же Ranger/Thief, с упором на ловкость и скрытность). Класс героя определяет его характеристики, умения и начальный набор кубиков, но как будет позже видно, строгой привязки к классам герои иметь не будут, а потому их персональные настройки можно будет с легкостью менять в одном-единственном месте.

Добавим герою необходимые свойства в соответствии с дизайн-документом: имя, любимый тип кубика, лимиты кубиков, навыки изученные и неизученные, руку, сумку и кучу для сброса. Обратите внимание на особенности реализации свойств-коллекций. Во всем цивилизованном мире считается дурным тоном предоставлять наружу доступ (при помощи getter’а) к коллекциям, хранящимся внутри объекта — недобросовестные программисты смогут без ведома класса менять содержимое этих коллекций. Один из способов борьбы с этим — реализовывать отдельные методы для добавления и удаления элементов, получения их количества и доступа по индексу. Можно и getter реализовать, но при этом возвращать не саму коллекцию, а ее неизменяемую копию — для небольшого количества элементов не особо страшно именно так и поступить.

data class Hero(val type: Type) {

    enum class Type {
        BRAWLER
        HUNTER
    }

    var name = ""
    var isAlive = true
    var favoredDieType: Die.Type = Die.Type.ALLY
    val hand = Hand(0)
    val bag: Bag = Bag()
    val discardPile: Pile = Pile()

    private val diceLimits = mutableListOf()
    private val skills = mutableListOf()
    private val dormantSkills = mutableListOf()

    fun addDiceLimit(limit: DiceLimit) = diceLimits.add(limit)
    fun getDiceLimits(): List = Collections.unmodifiableList(diceLimits)
    fun addSkill(skill: Skill) = skills.add(skill)
    fun getSkills(): List = Collections.unmodifiableList(skills)
    fun addDormantSkill(skill: Skill) = d
    
            

© Habrahabr.ru