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

Это игра, о которой я говорил. Не стоит пугаться большого количества текста, здесь есть интерактивные демки, первая из которых уже буквально через пару параграфов! [см. оригинал статьи - тут это только гифки]
Это игра, о которой я говорил. Не стоит пугаться большого количества текста, здесь есть интерактивные демки, первая из которых уже буквально через пару параграфов! [см. оригинал статьи - тут это только гифки]

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

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

У меня есть несколько высокоуровневых целей:

  • Игровой движок и пользовательский интерфейс должны быть строго разграничены. Я метался между пиксель арт UI стилем и более олдскульным ASCII, и для меня важно, чтобы движок поддерживал оба способа (Текущая версия Dart [15 июля 2014] полностью основана на ASCII, хотя, что забавно, использует canvas API.) Это значит, что движок не должен зависеть от того, как игра предстает перед игроком. Подобно бизнес приложению, я хочу полное разделение логики и внешнего вида.

  • Все существа должны обрабатываться одинаково. Обычно движки работают с некимactor, смесью Monster и подконтрольного игроку Hero. Я же хотел бы минимизировать разницу между аватаром игрока и простым монстром и сделать так, чтобы тот обрабатывался как любая другая игровая сущность.

Задача игрового цикла

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

В основе движка лежит игровой цикл. Он пробегает по всем существам и говорит им сделать ход. Это выглядит как-то так:

void gameLoop() {
  while (stillPlaying) {
    for (var actor in actors) {
      actor.update();
    }
  }
}

Однако движок отграничен от интерфейса, поэтому он действует независимо от него. Есть основной класс - Game. UI контролирует экземпляр этого класса и говорит ему сделать один "шаг" в игровом процессе, после чего возвращает себе контроль.

Это означает, что Game должен запоминать крайнее действовавшее существо, чтобы иметь возможность продолжить с того места, на котором в прошлый раз остановился. Что-то вроде такого:

class Game {
  final actors = <Actor>[];
  int _currentActor = 0;

  void process() {
    actors[_currentActor].update();
    _currentActor = (_currentActor + 1) % actors.length;
  }
}

Проницательный читатель, вроде вас, наверняка подумает о том, что это звучит как идеальное место для генератора. Действительно, в предыдущих версиях своей игры, написанных на C#, я использовал именно их. Когда в Dart появятся генераторы, возможно я перейду на них, но сейчас придется реализовывать это самостоятельно.

Действия как способ описания игровой логики

Теперь у нас есть зачатки игрового цикла. Когда приходит время, у очередного существа вызывается метод update() и оно что-то делает. Монстр может выбрать направление движения, а последствия этого выбора должны быть нормально обработаны. Он может двинуться на клетку другого существа, что вызовет атаку, а может и в стену или дверь.

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

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

Вместо этого мы применим классический архитектурный прием. Мы отделим решение выполнить действие от его непосредственного выполнения. Другими словами, применим паттерн проектирования "Команда". В Hauberg команды называются действиями.

Игровой цикл собирает с существ действия, а затем говорит им выполнить самих себя, что-то вроде:

void process() {
  var action = actors[_currentActor].getAction();
  action.perform();
  _currentActor = (_currentActor + 1) % actors.length;
}

Здесь есть различные классы, каждый со своим предназначением, представляющие абсолютно все, что может делать существо в этом мире. Среди них WalkAction, OpenDoorAction, EatAction и т.п.

Такая архитектура освобождает класс Actor от кода, описывающего поведение. Что еще лучше, это отделяет все существа друг от друга. Если вы добавляете или меняете возможности существа, вы можете просто добавить самостоятельный небольшой класс - Action. Это воспринимается как система без зависимостей, в которую можно легко добавлять новую логику или изменять старую. (На сегодняшний день [15 июля 2014 г.] в игре 19 различных действий, и я подумываю добавить еще немного.)

Это также, конечно, помогает нам обращаться с монстрами и героями одинаково. Поскольку все классы Action работают с экземплярами Actor, они могут быть использованы как для монстров, так и для героев. (Тут есть немного исключений, так как герои могут то, чего не могут монстры. Сейчас монстры лишены инвентаря, так что все действия, относящиеся к работе с ним, просто неприменимы к самовольным существам.)

Действия с разной скоростью

Мы уже получили простой работающий игровой цикл, однако наша игра как-то слишком пошаговая. Каждый монстр или герой обрабатываются с одинаковой скоростью. Вы делаете шаг, они отвечают тем же - похоже на на древнюю игру с роботами. Вы как не можете быть опереженным, так и опережать. Поглядите сами:

[в оригинале можно поиграть самому]
[в оригинале можно поиграть самому]

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

Разумеется, это "скорость" в рамках пошагового мира, не движение быстрее или медленнее в привычном понимании. Она лишь значит, что "более быстрые" существа ходят чаще других. Если вы в два раза быстрее зеленого слайма, значит на один его ход приходится два ваших.

Эта механика необходима в рогаликах, и уже полно примеров как реализовать ее. Я использую ту же систему, что и Angband, потому что она прекрасна.

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

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

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

Самое интересное в этом то, что, используя накопление энергии за несколько ходов, вы можете делать существ, которые двигаются относительно других с произвольным дробным множителем. Вы могли бы создать существо, двигающееся пять раз за каждые семь ходов другого. (Конечно, вы увидите уже непосредственно в игре, что время от времени первое существо делает два хода подряд. Это просто следует из того, что в среднем отношение должно получаться 7/5)

Хватит слов, пора посмотреть на это в действии:

[в оригинале можно поиграть самому]
[в оригинале можно поиграть самому]

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

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

Одна проблема с героями

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

У нас уже есть два ограничения, делающие эту задачу непростой:

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

  • Поскольку игра запускается в браузере, она не может блокировать ожидание пользовательского ввода. Браузер не работает по таким правилам. Вы должны всегда возвращаться к циклу событий для получения сигналов об очередном вводе.

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

void handleInput(Keyboard keyboard) {
  switch (keyboard.lastPressed) {
    case KeyCode.G:
      game.hero.setNextAction(new PickUpAction())
      break;

    case KeyCode.I:         walk(Direction.NW); break;
    case KeyCode.O:         walk(Direction.N); break;
    case KeyCode.P:         walk(Direction.NE); break;
    case KeyCode.K:         walk(Direction.W); break;
    case KeyCode.L:         walk(Direction.NONE); break;
    case KeyCode.SEMICOLON: walk(Direction.E); break;
    case KeyCode.COMMA:     walk(Direction.SW); break;
    case KeyCode.PERIOD:    walk(Direction.S); break;
    case KeyCode.SLASH:     walk(Direction.SE); break;
  }
}

void walk(Direction dir) {
  game.hero.setNextAction(new WalkAction(dir));
}

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

class Hero extends Actor {
  Action _nextAction;

  void setNextAction(Action action) {
    _nextAction = action;
  }

  Action getAction() {
    var action = _nextAction;
    // Only perform it once. [выполняется только один раз]
    _nextAction = null;
    return action;
  }

  // Other heroic stuff... [прочие героические штуки]
}

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

Это освобождает движок от необходимости обращаться к интерфейсу, и позволяет ему передавать действия во время простаивания. Есть только одна проблема, возникающая, когда движок требует с героя действия, а UI еще не передал его.

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

void process() {
  var action = actors[_currentActor].getAction();

  // Don't advance past the actor if it didn't take a turn.
  // [не пропускает существо, если то не сделало ход]
  if (action == null) return;

  action.perform();
  _currentActor = (_currentActor + 1) % actors.length;
}

Если интерфейс говорит движку идти дальше, но еще не передал действие герою, тот просто ничего не делает и возвращает управление обратно UI. Обратите внимание, что setNextAction() может быть вызван в любое время. Такой способ отлично сочетается с системой скорости, притом UI не нужно ничего контролировать. Он просто передает действие героя в движок и говорит ему идти дальше. Движок же заботится о том, чтобы очередь двигалась только в тогда, когда это возможно.

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

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

Люди имеют свойство ошибаться

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

Например, пускай игрок пытается сделать шаг в стену. Сейчас это создаст действие движения и, когда оно будет обработано, игрок останется на месте, но ход уже будет пропущен. Если герой пытается убежать от мерзкой твари, такая ошибка может стоить ему жизни. Многие игры забили на это, но я не хочу, чтобы подобное наказывалось. Рогалики и без этого безжалостны.

Демки до сих пор работали именно так. Попробуйте вернуться и побегать в стены [см. оригинал]. Видите, как смертельные монстры приближаются к вам, пока вы бьетесь о стены? Это как раз то, что нужно исправить. Когда игрок пытается совершить невозможное действие, мы должны быть уверены, что это не потратит его ход.

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

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

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

oid process() {
  var action = actors[_currentActor].getAction();
  if (action == null) return;

  var success = action.perform();

  // Don't advance if the action failed. [не продвигает очередь в случае неудачи]
  if (!success) return;

  _currentActor = (_currentActor + 1) % actors.length;
}

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

А теперь, попробуйте побиться о стены снова:

[в оригинале можно поиграть самому]
[в оригинале можно поиграть самому]
[ Как было ]

Делай не что сказано, а что нужно

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

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

Когда действие проверяет самого себя, оно может просто сразу провалится, но так же может предложить альтернативное действие. Оно буквально говорит: "не, на самом деле ты имел в виду это".

Учитывая, что у Action метод perform() может возвращать успех, неудачу или другое действие, нам нужно сделать небольшой класс-обертку для этого:

class ActionResult {
  static const success = ActionResult(true);
  static const failure = ActionResult(false);

  /// An alternate [Action] that should be performed instead of
  /// the one that failed.
  final Action alternative;

  /// `true` if the [Action] was successful and energy should
  /// be consumed.
  final bool succeeded;

  const ActionResult(this.succeeded)
  : alternative = null;

  const ActionResult.alternate(this.alternative)
  : succeeded = true;
}

Когда действие выполнится, оно может вернуть ActionResult.success чтобы показать, что все нормально, ActionResult.failure, чтобы показать, что ничего делать не нужно, или ActionResult с .alternate переменной, которая будет ссылаться на новое действие, которое нужно выполнить вместо текущего.

А игровой цикл просто смотрит на возвращаемое значение:

void process() {
  var action = actors[_currentActor].getAction();
  if (action == null) return;

  while (true) {
    var result = action.perform();
    if (!result.succeeded) return;
    if (result.alternate == null) break;
    action = result.alternate;
  }

  _currentActor = (_currentActor + 1) % actors.length;
}

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

  • Когда вы используете предмет, действие "использовать предмет" смотрит, что этот предмет делает (пускает фаербол, телепортирует и т.п.) и возвращает соответствующее действие в качестве альтернативного. Когда вы "используете" носимый предмет, взамен получите действие "надеть предмет".

  • Если существо "двигается" без направления, значит оно отдыхает и возвращает действие "отдохнуть".

  • Если существо двигается в дверь, значит возвращается действие "открыть дверь".

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

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

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

[в оригинале можно поиграть самому]
[в оригинале можно поиграть самому]

Конец... или нет?

Вот черт, я уже скормил вашему вниманию три тысячи слов, но еще осталось столько крутого для обсуждения! Я, должно быть, разделю это на две части.

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

Если вы делаете относительно простой рогалик, этого наверняка должно хватить.

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

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

Если хотите попробовать игру, это можно сделать здесь.

Комментарии (13)


  1. Tarakanator
    20.07.2022 12:08
    +2

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

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

    Это костыль. Он режет потенциально возможные интересные ситуации.
    Например игрок ослеплён и пытается убежать от монстров.
    В обычной реализации он будет собирать лбом стены, в вашей реализации при попытке воткнуться лбом в стену голос свыше будет ему говорить: там стена, иди в другую сторону.


    1. Goerging Автор
      20.07.2022 12:59
      +2

      Автор сам же говорил, что не хочет, чтобы это вызывало попоболь:

      Многие игры забили на это, но я не хочу, чтобы подобное наказывалось. Рогалики и без этого безжалостны.

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

      В общем, я уверен, что при желании можно реализовать что угодно. Цикл и правда получился весьма гибкий.


      1. Tarakanator
        20.07.2022 13:13

        мне в играх очень нравится когда физика игры может сработать неожиданным образом.
        И боюсь такой подход потенциально снижает возможность таких ситуаций.
        Последняя такая фишка, попавшаяся на глаза. Starcraft 2 у противника осталось последнее зданее, улетевшее в мёртвую зону вашего последнего наземного юнита, у вас тоже есть летающие здания, но нет возможности что-то строить (нет рабочих и денег).
        решение: подгоняем своё здание поближе к вражескому, но в пределах огня своего юнита. Стреляем по своему зданию, убивая вражеское сплешевым уроном.

        А если бы разработчик был "умным" и запретил "ошибочную" атаку своих зданий?


        1. Goerging Автор
          20.07.2022 13:35
          +1

          Ну тут уже зависит от разработчика и игры. Starcraft 2 - киберспортивная мега-игра, поэтому логично возложить большую ответственность на игрока за его действия. Где-то это излишне, и описанная автором мне кажется именно этот вариант. Рогалики слишком рандомны по своей природе, и я солидарен с автором, что пускай лучше "голос свыше будет ему говорить", если тыкание в стену не предусмотрено, а всякие дебафы, типа той же слепоты пускай обрабатываются отдельно. Сколько людей, столько и мнений.


    1. Newbilius
      20.07.2022 14:03
      +2

      Это не костыль, а синергия механик)

      Например игрок ослеплён и пытается убежать от монстров.
      Но в игре может и не быть такой механики, как «полное ослепление». Тогда зачем давать игру по случайному клику терять действие?

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


      1. Tarakanator
        20.07.2022 14:17

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

        Обмануть противника, что ты на перезарядке, и вместо выбрасывания обоймы вернуть её обратно.(или пристрелить противника патроном, оставшимся в стволе)
        Или для смены типа патронов.
        Или если ты понимаешь что сейчас сдохнешь и не хочешь, чтобы противнику достались патроны.
        И мой любимый вариант (да я видел и такое). Нажать кнопку выброса магазина НА ЧУЖОМ оружии.

        бесполезно использовать аптечки при полном здоровье

        1)Аптечка может лечить постеменно, а не моментально, в некоторый случаях имеет смысл сожрать такую перед получением урона.
        2)аптечка может лечить от яда и т.д.

        И никаких интересных ситуаций такие «возможности» не создают.

        Так что вы не правы.

        UPD: ещё вспомнил, Разрядить оружие, кинуть его во врага, тот поднимает его автоматически, и пока его перезагяжает(или соображает поменяь обратно) убить его.


        1. Goerging Автор
          20.07.2022 14:26
          +3

          Часто ли вы видите такое? Это из разряда тех игр, где есть ВСЕ. "Рай" с миллионом квестов, бесконечными неповторяющимися заданиями, персонажами и с еще кучей механик, которые перечисляют, как будто, по желаниям ребенка.

          Типа: "О, а пускай там будет гипер-лазер-пушка, стреляющая радиоактивными ягнятами", "О, а еще хочу ядреный хрен, замазывающий экран врага чернилами спрута"

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


          1. Tarakanator
            20.07.2022 14:42

             То, что вы говорите - это игры в сферическом вакууме.

            -смена типа патронов. Сталкер (дробь\жекан)
            -чтоб не достались патроны- стратегия не доставайся же ты никому довольно распространена в том или ином виде.
            -кинуть рязряженное оружие во врага-довольно популярный приём в старых шутерах. Не помню конкретно в какой игре я это практиковал, но вероятно в какой-то части quake.
            -выброс магазина у вражеского оружия: pavlov VR

            Конечно такие события вещь не частая, но именно такие хитрые применения обычных механик дарят незабываемые эмоции.

            любая игра режет механики и лишает интересных ситуаций

            Не совсем так. Многие игры сильно заскриптованы, и не имеют хоть какого-то рассчёта физики мира. Там и ломать нечего. А вот рогалики... там обычно пытаются сделать как раз симуляцию мира.


        1. Newbilius
          20.07.2022 15:08
          +3

          Обмануть противника, что ты на перезарядке, и вместо выбрасывания обоймы вернуть её обратно.(или пристрелить противника патроном, оставшимся в стволе)

          Но для этого нужно, чтобы уже было


          • Оставление патрона в стволе (чего в игре не было)
          • Возможность прервать анимацию перезарядки (чего в игре не было)
          • Реакция AI на процесс перезарядке (чего в игре не было)

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


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

          Чего в той игре не было.


          аптечка может лечить от яда и т.д.

          Чего в той игре не было.


          Разрядить оружие, кинуть его во врага, тот поднимает его автоматически, и пока его перезагяжает(или соображает поменяь обратно) убить его.

          Чего в той игре не было.


          Так что вы не правы.

          Я описываю, как в реальных играх эти вещи ничего интересного не добавляли :) Вы же описываете, как "можно было бы эти косяки сделать фичами". Так да, безусловно, можно. Но само оставление косяка обычно "само по себе" ничего интересного не создаёт.


          Так что, возвращаясь к начальному тезису — невозможность долбиться в стену именно фича, а не "костыль", пока в игре нет других механик, которые бы делали такое долбление осмысленным.


          1. Tarakanator
            20.07.2022 15:41

            Чего в той игре не было.

            1)Я думал мы в принципе про игры, а не про какую-то конкретную.
            2)Сейчас игры допиливают в процессе.
            3)Это может быть элементом реализма, типа переключил оружие, вспоминай сколько патронов осталось, или выкидывай остатки магазина и заряжай гарантированно целый.

            Обычно на перезарядку реагирует естественный интеллект, а не искуственный.

            Так что, возвращаясь к начальному тезису — невозможность долбиться в стену именно фича, а не "костыль", 

            Это костыль, пусть и идеально работающий.

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

            В конкретно этой версии игры разницы может и не быть по написанию функции. Вопрос в том, как функция будет изменяться при изменении игры.


            1. NemoVors
              20.07.2022 16:33
              +3

              Фичей он был бы если бы проверялась не возможность действия, а корректность комманд

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

              К тому же "ход" в рогалике происходит очень быстро. И банальное заклинивание кнопки на секунду (или просто палец дернулся) может означать ходов эдак двадцать получения урона с трех сторон... А загрузиться нельзя. Я полностью поддерживаю эту фичу. А пропуск хода, если он нужен можно и отдельной кнопкой сделать (и кажется автор упоминал "отдых" в тексте).

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

              Старый-то зачем выкидывать? Я во что-то такое играл, так что поддержу вашего оппонента: некоторые возможности лучше ограничить. Особенно, если это шутер, где скорость реакции очень важна. Банальные блокировки от случайных нажатий.

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


              1. Tarakanator
                20.07.2022 17:53

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


              1. chousensha
                21.07.2022 23:27

                В случае с рогаликами, когда проход по длинному коридору (часто часть
                коридора еще и не видна) это либо зажать кнопку и подержать, либо нажать
                ее скажет 55 раз...

                Не обязательно. В том же NetHack есть команды (помимо прочих): "иди в указанном направлении, пока не уткнешься в стену, или найдешь что-нибудь интересное" (например, ответвление коридора), "n раз делай действие x", "иди в указанную точку".