Несмотря на победу, по моему посту с прошлого раза можно легко понять, что от разработки я получила не так уж много удовольствия. Игра получилась прикольная, но сил и нервов на нее было потрачено много. Я вписалась в новый джем с пониманием, что можно попробовать как-то по-другому. И у меня получилось. Так что в этот раз пост будет более позитивным.
Начать стоит с выбора темы. Я накинула целых две темы в общий список: “игра без графики(в привычном смысле)” и “игра из стоп-кадров, без анимации”. Это технические темы, мне хотелось чтобы джем был ограничительным по механикам, но не по сеттингу. Какое-то время я размышляла об этих темах: “Что, если их выберут? Можно ли вообще сделать игру без графики?”. И придумала столь феерическую штуку, что мне даже немного хотелось чтобы выбрали мою тему. Правда, я бы, скорее всего, не успела все доделать. Поэтому хорошо, что была выбрана тема попроще. Более свободная. 1 vs 100.
Тема хороша тем, что ее можно трактовать как угодно. Она не настолько узкая, как в прошлый раз. Во многом благодаря этому, я придумала концепт уже в первый день. Сначала я пыталась интерпретировать соотношение как шансы и в голову пришла Roulette Knight. Потом я немного отошла от этой концепции и начала думать что же такое будет 100. Мне хотелось сделать какую-нибудь головоломку. Тогда я начала думать про Hexcells и Tametsi и тогда на свет родилась идея про 100 мин на поле 10×10. Звучало настолько абсурдно, что мне понравилось и решение было принято. Оставалось только придумать каким образом сделать эту игру проходимой. Самое большое веселье – слушать в итоге от знакомых о том, что пройти ее невозможно и видеть какие-то теоретические изыскания на этот счет. Это означает, что идея работает!
В то время как первичным вдохновением были стандартные клоны “Сапера”, в дальнейшем свою роль сыграли DungeonUp и AI – The Somnium Files. Первая показывает насколько увлекательной может быть головоломка про числа. Вторая позволяет не сильно задумываться над принципом работы статус-эффектов. В ней это уже обкатано. Изначально идеи были немного не в том направлении. Начнем с того, что у сапера была лопата, у которой тоже было хп. И взорвавшиеся мины можно было еще выкапывать и собирать с них лут. Это немного не вяжется с тем, что minesweeper – это на самом деле судно. Лопате в море делать нечего. Туда же относится сапог на иконке статуса иммобилизации, но его хотя бы символически можно понять.
Помимо лопаты был еще магазин и отдельное повреждение разных частей тела. Все это тоже оказалось выброшено. Я просто сделала самый простой вариант – есть мины с двумя свойствами – сила и эффект. Достаточно все это перемешать и готово!
В субботу утром я уже начала делать игру, но почти весь день был занят другой активностью. Успела только сделать большую часть математики в игре. В качестве движка в этот раз я выбрала MonoGame. Во-первых потому что стратегия с прошлого раза (MMF+Dll) не очень хорошо работает, когда пересчетов так много. Это банально неудобно. Во-вторых потому что для двумерной сетки у меня уже есть база. Я просто перетащила большую часть кода в новую игру и немного адаптировала его. Раз уж мы здесь, давайте я не буду отклоняться от традиций и расскажу про то, как организован код в игре.
Осторожно, кодирование!
Еще с Idle 3 я пытаюсь как-то воплотить в игре MVC паттерн. Получается плохо. Основная проблема в том, что контроллер и вьюшка постоянно обмениваются информацией. Контроллер отвечает за логику. Например, мина – это простой объект с одним методом. Все эти объекты называются сущностями (Entity) и их три штуки: мина, игрок, сетка.
class EMine
{
public Action OnActivated;
public bool Activated { get; private set; } = false;
public readonly int Damage;
public Types.MineProperties Property { get; private set; }
public EMine(int damage, Types.MineProperties property)
{
Damage = damage;
Property = property;
}
public void Activate()
{
Assert.isFalse(Activated);
Activated = true;
Game.Instance.Player.Collect(this);
OnActivated?.Invoke();
}
}
Вся визуальная часть состоит из виджетов, которые отвечают за часть интерфейсной логики (наведение, клики мыши и так далее) и обновление графики. Все активные элементы наследуются от CElement и могут быть зарегистрированы на макете. Эта регистрация присваивает им границы, которые и обрабатываются при наведении мыши уже в другом классе, CLayout. Эта схема от Idle 3 ничем принципиально не отличается. Кроме одного нюанса. Флажок dirty и метод обновления. Это – попытка придумать какую-то альтернативу древовидному обновлению интерфейса, которое было в Idle 3. Я там пыталась экономить на спичках и обновлять элементы пореже. Из-за этого получилась лапша.
А теперь все просто. Элемент меняет свойства, говорит что грязный и обновляется в следующем кадре. Таким образом все обновление картинки оказывается в одном методе Refresh и мне больше не нужно учитывать миллион флажков в OnMouseEnter.
internal abstract class CElement : IDisposable
{
private static uint nextId = 1;
bool dirty = false;
public uint ElementId { private set; get; }
public Rectangle Bounds { protected set; get; }
public bool Enabled { private set; get; }
public Types.MouseButtons Clickable { protected set; get; }
public bool PersistentClick { protected set; get; } //Call OnMouseClick even if the element is not in focus anymore
public virtual void Enable()
{
if (ElementId == 0)
ElementId = nextId++;
Enabled = true;
CLayout.Register(this);
}
public virtual void Disable()
{
Enabled = false;
CLayout.Unregister(this);
}
public virtual void OnMouseEnter() { }
public virtual void OnMouseExit() { }
public virtual void OnMouseClick() { }
public virtual void OnMouseRightClick() { }
public virtual void OnMousePress() { }
public virtual void OnMouseRightPress() { }
public virtual void OnMouseRelease() { }
public virtual void OnMouseRightRelease() { }
public void SetDirty()
{
if (dirty)
return;
dirty = true;
CWindow.OnUpdate += Refresh;
}
public virtual void Refresh(GameTime time)
{
if(dirty)
CWindow.OnUpdate -= Refresh;
dirty = false;
}
public virtual void Dispose()
{
CWindow.OnUpdate -= Refresh;
dirty = false;
Disable();
}
}
Третья категория классов – это контролы. Да, есть сущности, которые как будто бы контроллеры, но контролами называется другое. Контролы – это основные сущности фреймворка. Окно, мышка, клавиатура, загрузчик ресурсов, аудиоплеер. Это все статические классы и к ним можно обратиться из любой части кода.
Вот, в целом так выглядит архитектура. И наверное как-то так я продолжу делать игры в будущем.
Кодирование закончилось!
Первый рабочий прототип был готов уже в воскресенье. И тогда же я ее впервые прошла. Поскольку концептуально игра очень простая, для прототипа тоже нужно сделать совсем немного. Таким образом я ее впервые потестила и… влюбилась. Мне, как большой поклоннице Hexcells и аналогов, очень понравилось то, как в игре работает математика. Можно было подумать, что сумма урона вместо количества мин сильно спрячет за собой все, кроме влияния удачи. Но оказалось, что нет. Все навыки, полученные в сапероподобных играх здесь остаются актуальными. Это, правда, добавляет еще одну проблему. Поскольку я задротка, игра у меня получилась очень сложной. Я прохожу ее на средней сложности при любом раскладе с первой попытки. Но такое ведь получится далеко не у всех! Эта мысль мне пришла в голову очень поздно, уже после того, как я добавила в игру усложненный режим (ведь на нормале слишком легко). Тогда мне пришлось думать как же запилить новый режим, не увеличивая окошко игры. И придумала открывать хард только после первого прохождения. А тем, кто умеет, можно просто шифт зажать и сразу приступить к адовейшему челленджу. Пройти ее на максимальной сложности у меня получилось с трудом и не с первой попытки. На этой сложности везение играет более существенную роль, но и стратегия требуется очень устойчивая, потому что всегда вам везти не может.
Наиболее серьезное внимание я уделяла комфорту решения головоломки. Изначально никаких идей, кроме проставления подсказок с помощью ПКМ и клавиш с цифрами, у меня не было. Но, потестировав игру совсем немного, я поняла, что самым интуитивным способом проставлять подсказки является скролл. Им проще и быстрее все это делать. Я добавила побольше разных способов делать метки для того, чтобы удобно было всем. Кроме того, чтобы стало еще проще и быстрее, я добавила автоматический подсчет меток и остатка при наведении курсора на мину. В первых версиях нужно было считать все циферки в уме. А теперь можно просто вычитать зоны друг из дружки и таким образом вести дедукцию. Очень просто и быстро. Математика все равно требуется, но в намного меньшем объеме.
Вся игра была сделана с нуля где-то за 20-30 часов. Это меньше (раза в два), чем The VAN Journey, но и игра существенно проще. Таков был план. Мне не хотелось выгореть. В итоге план сработал. И это можно считать за успех! Если не ставить слишком амбициозные цели, игра получается хорошая и ее делание остается удовольствием. Стоит отметить, что при написании кода я все же чувствую себя более комфортно, чем при создании игры в Click&Play движках. Просто потому что я знаю, что смогу решить все свои проблемы. Особенно учитывая то, что код MonoGame открыт. Этим я, кстати, воспользовалась. MonoGame кроссплатформенный, но мне нужно было как-то программно свернуть окошко. Для этого пришлось дописать метод Minimize для класса Window. Потому что нативное окошко игры – слишком скучно.
Учитывая, что в этот раз у меня не было отпуска, свободного времени осталось всего два дня. Как раз те самые два дня, на которые этот джем длиннее предыдущего. Этим я воспользовалась, когда писала этот пост. Вообще, учитывая что прототип был сделан за день, возникает вопрос: “Чем же так долго я занималась?”. Об этом я, конечно же, расскажу ниже.
Внимание к деталям
По ходу делания игры мне в голову приходило множество разных идей. Например, когда я делала спрайт для мины – я рисовала стандартную мину с бликом в левом верхнем углу. Нарисовала несколько с разным диаметром и поняла, что блик выглядит почти как глаз. Недолго думая, нарисовала второй глаз и рот. Получилось прикольно! Тогда же пришла идея сделать мины разными. Это было допилено в один из последних дней разработки и смотрится прям офигенно, на мой взгляд. Туда же и лицо “игрока”. Прикольно же было сделать так, чтобы оно отражало запас здоровья? А еще, когда в стандартном сапере кликаешь на мину – он удивленно открывает рот. Хочу тоже так сделать. Но ведь когда здоровья мало, один глаз рисуется крестиком! Пришлось, в общем, рисовать 7 разных лиц, против четырех в оригинале.
Помимо этого пришла в голову идея изменять заголовок игры прямо по ходу игры. Так получается обратный отсчет про то, сколько мин осталось. И даже само слово меняется в зависимости от результата. А еще я специально сделала текущую сложность выделенной на панели. Это нужно для того, чтобы можно было хвалиться друзьям и ничего не писать текстом. Все видно на картинке – и сложность и сколько здоровья осталось, и сколько мин осталось. Все видно. Можно соревноваться даже остатком мин, если игру пройти не получается.
В новинку для меня было использование двух вещей: звуки (которые сделать оказалось намного проще, чем я думала) и тайлмэп. Для создания тайлмэпов я использовала утилиту Tiled. Она прекрасна! Буду теперь всегда ей пользоваться. На карте тайлов не только сама картинка, но и зоны всех областей интерфейса. Это позволяет их переносить довольно быстро и вместе с графикой. В Idle 3 вся графика закодирована и любое изменение приводит к большим сложностям (но там в принципе намного больше нюансов).
Я думала прикрутить еще анимацию взрывов, но поняла что мои ресурсы все. Разработка игры начинает надоедать, а это означает, что пора остановиться. В другой раз.
В этот раз, кажется, попозитивнее получилось. И это правда так. Игра, правда, в этот раз более нишевая и хардкорная. Сомневаюсь, что она победит. Но посмотрим)
P.S. Поиграть во все игры с джема вы можете вот тут. А тут можно посмотреть как в них играет один из участников и даже услышать меня в прямом эфире)
P.P.S. Второе место в этот раз)
Обсуждение в telegram