[Из песочницы] Как создать игру, если ты ни разу не художник
В жизни каждого программиста бывали моменты, когда он мечтал сделать интересную игру. Многие программисты эти мечты реализовывают, и даже успешно, но речь сейчас не о них. Речь о тех, кто любит играть в игры, кто (даже не имея знаний и опыта) и сам пытался их когда-то создавать, вдохновляясь примерами героев-одиночек, добившихся всемирной известности (и огромных прибылей), но в глубине души понимал, что тягаться с гуру игростроя ему не по силам.
И не надо…
Небольшое вступление
Сразу оговорюсь: нашей целью не является зарабатывание денег — на Хабре полно статей на эту тему. Нет, мы будем делать игру мечты.
Люди, не обманывайте себя. Вы делаете не игру мечты, а игру, которая будет хорошо продаваться — это разные вещи. Игрокам (а особенно искушенным) нет дела до вашей мечты и платить за нее они не будут. Хотите прибылей — изучайте тренды, смотрите, что сейчас популярно, делайте что-то уникальное, делайте лучше, необычнее, чем у других, читайте статьи (их много), общайтесь с издателями — в общем, реализовывайте мечты конечных пользователей, не свою.
Если вы еще не сбежали и все же хотите реализовать игру мечты, заранее откажитесь от прибылей. Вообще не продавайте мечту — делитесь ей даром. Дарите людям свою мечту, приобщайте их к ней, и если ваша мечта чего-то стоит, вы получите пусть не деньги, но любовь и признание. Иногда это намного ценнее.
Многие считают, что игры — пустая трата времени и сил, и что серьезные люди не должны вообще на эту тему разговаривать. Но здесь собрались люди несерьезные, поэтому согласимся лишь отчасти — игры действительно отбирают много времени, если в них играть. Однако разработка игр, хоть и занимает во много раз больше времени, способна принести немало пользы. Например, позволяет познакомиться с принципами, подходами и алгоритмами, не встречающимися в разработке неигровых приложений. Или углубить навыки владения инструментами (например, языком программирования), занимаясь чем-то необычным и увлекательным. От себя могу добавить (и многие согласятся), что разработка игр (даже неудачная) — это всегда особый, ни с чем не сравнимый опыт, о котором потом вспоминаешь с трепетом и любовью, чего каждому разработчику хотя бы раз в жизни желаю испытать.
Мы не будем использовать новомодные игровые движки, фреймворки, библиотеки — мы заглянем в самую суть игрового процесса и прочувствуем его изнутри. Откажемся от гибких методологий разработки (задача упрощается необходимостью организовать работу всего одного человека). Мы не будем тратить время и силы на поиски дизайнеров, художников, композиторов и спецов по звуку — мы все сделаем сами, как умеем (но при этом сделаем все по-умному — если вдруг художник у нас появится, нам не составит особых усилий прикрутить модную графику на готовый каркас). В конце концов, мы даже не будем особо изучать инструментарий и выбирать подходящий — сделаем на том, который хорошо знаем и умеем пользовать. Например, на Java, чтоб потом, если нужно, перенести на Андроид (или на кофеварку).
«А!!! Ужас! Кошмар! Как на такую чушь вообще можно время тратить! Проваливай отсюда, я пойду что-то более интересное почитаю!»
Зачем это делать? В смысле, велосипед изобретать? Почему бы не использовать готовый игровой движок? Ответ прост: мы ничего про него не знаем, а игру хотим уже сейчас. Представьте образ мысли среднестатистического программиста: «Хочу делать игру! Там будет мясо, и взрывы, и прокачка, и можно грабить корованы, и сюжет бомбезный, и такого вообще никогда и нигде больше не было! Начну писать прямо сейчас!… А на чем? Посмотрим, что у нас сейчас популярно… Ага, X, Y и Z. Возьмем Z, на нем сейчас все пишут…». И начинает изучать движок. А идею бросает, потому что на нее уже времени не хватает. Fin. Или ладно, не бросает, но толком не изучив движок, принимается за игру. Хорошо, если потом ему хватит совести никому не показывать свою первую «поделку». Обычно нет (зайдите в любой магазин приложений, посмотрите сами) — ну как же, хочется прибылей, нет сил терпеть. Когда-то создание игр было уделом увлеченных творческих людей. Увы, это время безвозвратно прошло — сейчас в игре главное не душа, а бизнес-модель (по крайней мере, разговоров о ней на порядок больше). У нас же цель простая: мы будем делать игры с душой. Потому абстрагируемся от инструмента (подойдет любой) и сосредоточимся на задаче.
Итак, продолжим.
Не буду вдаваться в подробности собственного горького опыта, но скажу, что одна из основных проблем для программиста при разработке игр — это графика. Рисовать программисты обычно не умеют (хотя бывают исключения), а художники обычно не умеют программировать (хотя бывают исключения). А без графики, согласитесь, редкая игра обходится. Что же делать?
Варианты есть:
Скриншоты игры «Kill Him All», 2003 год
Скриншоты игры «Raven», 2001 год
Скриншоты игры «Inferno», 2002 год
Скриншоты игры «Грёбаный», 2004 год
Скриншоты игры «Грёбаный 2. Демо», 2006 год
Скриншоты игры «Грёбаный», 2004 год
Скриншоты игры «Fifa», 2000 год
Скриншоты игры «Sumo», 1998 год
Остановимся подробнее на последнем (отчасти потому что он выглядит не так уныло как остальные). Многие неопытные геймеры считают, что игры без крутой современной графики не способны покорить сердца игроков — их даже играми-то назвать язык не поворачивается. Подобным аргументам молчаливо возражают разработчики таких шедевров, как ADOM, NetHack и Dwarf Fortress. Внешний вид не всегда является решающим фактором, использование же ASCII дает некторые интересные примущества:
- в процессе разработки программист сосредотачивается на геймплее, игровой механике, сюжетной составляющей и прочем, не отвлекаясь на второстепенные вещи;
- разработка графической составляющей не отнимает слишком много времени — рабочий прототип (то есть, версия, поиграв в которую можно понять, а стоит ли вообще продолжать) будет готов намного раньше;
- не нужно осваивать фреймворки и графические движки;
- ваша графика не устареет за те пять лет, которые вы будете разрабатывать игру;
- хардкорщики смогут оценить ваш продукт даже на платформах, не имеющих графической среды;
- если все сделать правильно, то крутую графику можно прикрутить потом, попозже.
Приведенное выше длинное вступление имело целью помочь начинающим игроделам побороть страхи и предрассудки, перестать волноваться и все ж таки попробовать что-нибудь эдакое сотворить. Готовы? Тогда приступим.
Шаг первый. Идея
Как? У вас все еще нет идеи?
Выключайте компьютер, пойдите покушайте, погуляйте, спортом позанимайтесь. Или поспите, на худой конец. Придумать игру это не окна помыть — озарение в процессе не приходит. Обычно идея игры рождается вдруг, неожиданно, когда вы совершенно об этом не думаете. Если такое вдруг произошло, быстрее хватайте карандаш и записывайте, пока идея не улетела. Любой творческий процесс именно так и реализуется.
А еще можно копировать чужие игры. Ну как, копировать. Конечно, не драть безбожно, рассказывая на каждом углу, какой вы сообразительный, но использовать чужие наработки в своем продукте. Как много после этого в нем останется конерктно от вашей мечты — вопрос второстепенный, ибо частенько у геймеров бывает так: вот все нравится в игре, кроме каких-то двух-трех раздражающих вещей, а вот если бы тут сделать по-другому… Кто знает, возможно, доведение до ума чьей-то хорошей идеи — это и есть ваша мечта.
Но мы пойдем простым путем — предположим, что идея у нас уже есть, и мы над ней долго не думали. В качестве первого нашего грандиозного проекта будем делать клон хорошей игры от Obsidian — Pathfinder Adventures.
«Это что еще за бред! Настолки какие-то?»
Как говорится, pourquoi pas? Предрассудки мы, кажись, уже оставили, а потому смело начинаем отшлифовывать идею. Естественно, клонировать игру один к одну мы не будем, но основные механики позаимствуем. К тому же реализация пошаговой настольной кооперативной игры имеет свои преимущества:
- она пошаговая — это позволяет не заботиться о таймерах, синхронизации, оптимизации, FPS и прочих муторных вещах;
- она кооперативная, то есть игрок или игроки соревнуются не друг против друга, а против некоего «окружения», играющего по детерминированным правилам — это избавляет от необходимости программировать ИИ (AI) — одного из самых сложных этапов разработки игр;
- она осмысленная — настолщики вообще люди прихотливые, во что попало играть не будут: им подавай продуманные механики и интересный геймплей — на одной красивой картинке не выедешь (чем-то знакомым отдает, не так ли?);
- она с сюжетом — многие киберспортсмены не согласятся, но лично для меня игра должна рассказывать интересную историю — как книга, только с использованием своих особых художественных средств.
- она занятная, что на любителя — описываемые подходы можно будет применить к любой последующей мечте, сколько бы их у вас ни было.
В каждый сценарии предусмотрен ряд местностей (локаций — их количество зависит от количества игроков), которые игрокам нужно посетить и исследовать. Каждая локация содержит колоду карт, лежащую рубашкой вверх, которую персонажи в свой ход исследуют — то есть открывают верхнюю карту и пытаются по соответствующим правилам ее преодолеть. В этих колодах помимо безобидных карт, пополняющих колоду игрока, имеются также злые враги и препятствия — их необходимо победить, чтобы продвинуться дальше. Карта Негодяя тоже лежит в одной из колод, но игроки не знают в какой именно — ее нужно найти.
Для победы над картами (и для приобретения новых) персонажи должны пройти проверку одной из своих характеристик (стандартные для РПГ сила, ловкость, мудрость итп), кинув кубик, размер которого определяется значением соответствующей характеристики (от d4 до d12), добавив модификаторы (определяемые правилами и уровнем развития персонажа) и играя для усиления эффекта походящие карты из руки. При победе встреченная карта либо убирается из игры (если это враг), или пополняет руку игрока (если это предмет) и ход переходит к другому игроку. При проигрыше персонажу часто наносится урон, заставляющий его сбрасывать карты из руки. Интересная механика состоит в том, что здоровье персонажа определяется количеством карт в его колоде — как только игроку нужно вытащить из колоды карту, а их нет — его персонаж погибает.
Целью же является, пробравшись через карты локаций, найти и победить Негодяя, предварительно перекрыв ему пути к отступлению (подробнее об этом и много другом можно узнать, почитав правила). Сделать же это нужно на время, в чем состоит основная сложность игры. Количество ходов строго ограничено и простым перебором всех имеющихся карт цели не достичь. Потому приходится применять различные ухищрения и умные техники.
По мере выполнения сценариев персонажи будут расти и развиваться, улучшая свои характеристики и приобретая новые полезные навыки. Управление колодой также является очень важным элементом игры, так как исход сценария (особенно на поздних этапах) как правило зависит от правильно подобранных карт (и от кучи везения, но чего вы хотите от игры с кубиками?).
В целом, игра интересная, достойная, заслуживающая внимания и, что важно для нас, достаточно сложная (обратите внимание, я говорю «сложная» не в значении «трудная»), чтобы ее клон было интересно реализовывать.
В нашем случае сделаем одно глобальное концептуальное изменение — откажемся от карт. Вернее, не откажемся вовсе, но заменим карты на кубики, по-прежнему разных размеров и разных цветов (технически, не совсем корректно навывать их «кубики», так как кроме правильного шестигранника присутствуют и другие формы, но называть их «кости» мне непривычно и неприятно, а пользоваться американизмом «дайсы» — и вовсе признак дурного тона, потому оставим как есть). Теперь вместо колод у игроков будут мешочки. И у локаций тоже будут лежать мешочки, из которых игроки в процессе исследования будут вытаскивать произвольные кубики. Цвет кубика будет определять его тип и, соответственно, правила прохождения проверки. Личные характеристики персонажа (сила, ловкость итп), как следствие, упразднятся, но зато появятся новые интересные механики (о чем позже).
Будет ли в это интересно играть? Понятия не имею, и никто этого понять не сможет, пока не будет готов рабочий прототип. Но ведь мы получаем удовольствие не от игры, а от разработки, правда? Потому никаких сомнений в успехе быть не должно.
Шаг второй. Дизайн
Иметь идею — это только треть дела. Теперь важно эту идею развить. То есть не прогуливаться в парке или париться в баньке, а сесть за стол, взять бумагу с ручкой (или открыть любимый текстовый редактор) и вдумчиво написать дизайн-документ, кропотливо прорабатывая каждый аспект игровой механики. Времени на это уйдет прорва, поэтому не рассчитывайте завершить написание в один присест. И даже не надейтесь полностью все продумать с одного раза — по мере реализации вы увидите необходимость сделать кучу правок и изменений (а иногда и глобально что-то переработать), однако какая-то основа должна обязательно присутствовать до начала процесса разработки.
И только справившись с первой волной грандиозных идей вы возьметесь за голову, определитесь со структурой документа и начнете методично наполнять его содержимым (ежесекундно сверяясь с уже написанным, дабы избежать ненужных повторений и особенно противоречий). Постепенно, шаг за шагом получится что-то осмысленное и лаконичное, вроде этого.
При описании дизайна выбирайте тот язык, на котором вам легче излагать свои мысли, особенно если вы работаете в одиночку. Если же когда-либо понадобится привлекать к проекту сторонних разработчиков, убедитесь, что они понимают весь тот творческий бред, который творится у вас в голове.
Для продолжения настоятельно рекомендую прочитать приведенный документ хотя-бы по-диагонали, потому что в дальнейшем я буду ссылаться на представленные там термины и концепции, подробно не задерживаясь на их толковании.
«Автор, убей себя об стену. Слишком много букв.»
Шаг третий. Моделирование
То есть, все тот же design, только более подробный.
Знаю, многим уже не терпится открыть IDE и начать кодить, но потерпите еще немного. Когда идеи переполняют нашу голову, нам кажется, что стоит лишь прикоснуться к клавиатуре, и руки сами понесутся в заоблачные дали — не успеет кофе вскипеть на плите, как рабочая версия приложения уже будет готова… отправиться в мусор. Чтобы много раз не переписывать одно и то же (а особенно чтобы не убеждаться через три часа разработки, что макет нерабочий и нужно начинать заново), предлагаю для начала хорошенько продумать (и задокументировать) основную структуру приложения.
Поскольку мы, как разработчики, хорошо знакомы с объектно-ориентированным программированием (ООП), будем использовать его принципы в нашем проекте. А для ООП нет ничего более ожидаемого, чем начать разработку с кучи нудных UML-диаграм. (Как, вы не знаете, что такое UML? Я тоже уже почти забыл, но с радостью вспомню — просто чтобы показать, какой я прилежный программист, хе-хе.)
Начнем, пожалуй, с диаграммы «вариантов использования» (use-case). Изобразим на ней способы взаимодействия нашего пользователя (игрока) с будущей системой:
«Э… это что вообще?»
Шучу-шучу… и, пожалуй, на этом прекращаю шутить — дело-то серьезное (мечта, как-никак). На диаграмме вариантов использования необходимо отобразить возможности, которые система предоставляет пользователю. В подробностях. Но так уж исторически сложилось, что именно данный тип диаграмм получается у меня хуже всего — терпения не хватает, судя по всему. И не надо на меня так смотреть — мы не в ВУЗе диплом защищаем, а получаем удовольствие от рабочего процесса. И для данного процесса не так важны варианты использования. Гораздо важнее грамотно разбить приложение на независимые модули, то есть реализовать игру таким образом, чтобы особенности визуального интерфейса не влияли на игровые механики, и чтобы графическую составляющую при желании можно было легко изменить.
Этот момент можно детализировать на следующей диаграмме компонентов (components):
Здесь мы уже выделили конкретные подсистемы, входящие в состав нашего приложения и, как будет показано дальше, все они будут разрабатываться независимо друг от друга.
Также, на этом же этапе прикинем, как будет выглядеть основной игровой цикл (вернее, его наиболее интересная часть — та самая, которая реализует прохождение персонажами сценария). Для этого нам подойдет диаграмма деятельности (activity):
Ну и напоследок неплохо бы представить в общем виде последовательность (sequence) взаимодействия конечного пользователя с игровым движком, посредством системы ввода-вывода.
Ночь длинна, до рассвета еще далеко. Посидев как следует за столом, вы спокойно нарисуете остальные два десятка диаграмм — поверьте, в дальнейшем их наличие поможет не сбиться с выбранного пути, повысить свою самооценку, обновить интерьер комнаты, завесив выцветшие обои цветастыми плакатами, а также в простых выражениях донести ваше видение до коллег-разработчиков, которые в скором времени толпами прибегут к дверям вашей новой студии (мы нецелены на успех, помните?).
Любимые всеми диаграммы классов (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
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
необходимыми для игрового процесса классами.
Кубики — наше все, добавим их в первую очередь. Каждый кубик (экземпляр класса 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