29 June

Weapon wheel в Doom 1993

JavaGames and game consoles
Приветствую.

Многие из нас с теплотой относятся к олдскульным видеоиграм, вышедшим на стыке веков. У них превосходная атмосфера, бешеная динамика и множество оригинальных решений, которые не устарели спустя десятилетия. Однако в наши дни видение интерфейса игр несколько изменилось — на смену запутанным уровням пришли линейные коридоры, на смену аптечкам — регенерация, а вместо длинного ряда клавиш 0-9 для выбора арсенала пришли сначала колесико мыши, а затем — виртуальное колесо. Именно о нем сегодня и пойдет речь.

image

Историческая сводка


Раньше, во время появления жанра шутеров как таковых, вопрос об управлении мышкой не стоял — для управления протагонистом использовалась только клавиатура. Причем единого формата управления тоже не было — WASD стал стандартом чуть позднее. Более подробно о старых игровых раскладках клавиатуры можно почитать вот тут.

Соответственно, в тех играх, где была реализована возможность выбора снаряжения (Doom, Wolfenstein, Quake etc) был реализован единственным интуитивным на тот момент способом — с помощью цифровых клавиш на клавиатуре. И на многие годы этот способ был единственным.
Потом, в конце 90х годов, появилась возможность смены вооружения колесиком мышки.

Однозначной информации на эту тему найти не удалось, однако в CS 1.6 такая возможность включалась через консоль. Впрочем, возможно такие прецеденты были и ранее — в таком случае, просьба указать на это в комментариях или в ЛС. А вот в привычном в наше время виде Weapon Wheel вошло в использование лишь с Crysis'ом и его Suit menu, Хотя попытки сделать нечто похожее были начиная с HL2, в массы «колесо» пошло лишь в конце 00х годов, а сейчас — является мейнстримом.

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

Постановка задач


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

Для этого мной был использован язык Java, в частности, перехват клавиш осуществляется за счет библиотеки jnativehook, а нажатие — за счет awt.Robot. Обработка полученных хуков не представляет сложностей, поэтому производится вручную.

Реализация


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

В частности, класс Shift позволяет хранить двумерный вектор, а также — определять его длину, а класс NormalisedShift, разработанный для хранения нормализованного вектора, помимо прочего, позволяет определить угол между перехваченным вектором и вектором (1,0).

Заголовок спойлера
class Shift{
    int xShift;
    int yShift;

    public int getxShift() {
        return xShift;
    }

    public int getyShift() {
        return yShift;
    }

    public void setxShift(int xShift) {
        this.xShift = xShift;
    }

    public void setyShift(int yShift) {
        this.yShift = yShift;
    }
    double getLenght(){
        return Math.sqrt(xShift*xShift+yShift*yShift);
    }

}
class NormalisedShift{
  double normalizedXShift;
  double normalizedYShift;
  double angle;
  NormalisedShift (Shift shift){
      if (shift.getLenght()>0)
      {
          normalizedXShift = -shift.getxShift()/shift.getLenght();
        normalizedYShift = -shift.getyShift()/shift.getLenght();
      }
      else
      {
          normalizedXShift = 0;
          normalizedYShift = 0;
      }
  }
  void calcAngle(){
      angle = Math.acos(normalizedXShift);
  }

  double getAngle(){
      calcAngle();
      return (normalizedYShift<0?angle*360/2/Math.PI:360-angle*360/2/Math.PI);
    };
};


Особого интереса они не представляют, и комментарий требуют только строки 73-74, нормализующие вектор. Помимо всего прочего, вектор переворачивается. у нег меняется система отсчета — дело в том, что с точки зрения программного обеспечения и с точки зрения привычной математики вектора традиционно направляют по разному. Именно поэтому вектора класса Shift имеют начало координат слева сверху, а класса NormalizedShift — слева снизу.

Для реализации работы программы был реализован класс Wheel, реализующий интерфейсы NativeMouseMotionListener и NativeKeyListener. Код — под спойлером.

Заголовок спойлера
public class Wheel  implements NativeMouseMotionListener, NativeKeyListener {

    final int KEYCODE = 15;
    Shift prev = new Shift();
    Shift current = new Shift();
    ButtomMatcher mathcer = new ButtomMatcher();


    boolean wasPressed = false;

    @Override
    public void nativeMouseMoved(NativeMouseEvent nativeMouseEvent) {
        current.setxShift(nativeMouseEvent.getX());
        current.setyShift(nativeMouseEvent.getY());

    }
    @Override
    public void nativeMouseDragged(NativeMouseEvent nativeMouseEvent) {

    }
    @Override
    public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {

    }

    @Override
    public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) {
        if (nativeKeyEvent.getKeyCode()==KEYCODE){
            if (!wasPressed)
            {
                prev.setxShift(current.getxShift());
                prev.setyShift(current.getyShift());
            }
            wasPressed = true;

        }
    }

    @Override
    public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {
        if (nativeKeyEvent.getKeyCode() == KEYCODE){
            Shift shift = new Shift();
            shift.setxShift(prev.getxShift() - current.getxShift());
            shift.setyShift(prev.getyShift() - current.getyShift());
            NormalisedShift normalisedShift = new NormalisedShift(shift);
            mathcer.pressKey(mathcer.getCodeByAngle(normalisedShift.getAngle()));
            wasPressed = false;
        }
    }


Разберемся, что тут происходит.

В переменной KEYCODE хранится код клавиши, служащей для вызова селектора. Обычно это TAB, но при необходимости, его можно изменить в коде или — в идеале — подтянуть из файла конфига.

prev хранит положение курсора мыши, которое было на момент вызова селектора. В сurrent поддерживается актуальное положение курсора в настоящий момент времени. Соответственно, при отпускании клавиши селектора происходит вычитание векторов и в переменную shift записывается смещение курсора за время удержания клавиши селектора.

Затем, в строке 140, вектор нормализуется, т.е. приводится к виду, когда его длина близка к единице. После чего, нормализованный вектор передается в матчер, который устанавливает соответствие между кодом клавиши, которую нужно нажать и углом проворота вектора. Из соображений читаемости, угол переводится в градусы, а так же — ориентируется по полному единичному кругу (acos работает только с углами до 180 градусов).

В классе ButtonMatcher определяется соответствие между углом и выбранным кодом клавиши.

Заголовок спойлера
class ButtomMatcher{

    Robot robot;
    final int numberOfButtons = 6;
    int buttonSection = 360/numberOfButtons;
    int baseShift = 90-buttonSection/2;
    ArrayList<Integer> codes = new ArrayList<>();
    void matchButtons(){
        for (int i =49; i<55; i++)
            codes.add(i);

    }
    int getCodeByAngle(double angle){
        angle= (angle+360-baseShift)%360;
        int section = (int) angle/buttonSection;
        System.out.println(codes.get(section));
        return codes.get(section);
    }
    ButtomMatcher() {
        matchButtons();
        try
        {
            robot = new Robot();
        }
        catch (AWTException e) {
            e.printStackTrace();
        }
    }
    void pressKey(int keyPress)
    {

        robot.keyPress(keyPress);
        robot.keyRelease(keyPress);
    }
}


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

Заключение


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

Конструктивная критика приветствуется.

Исходники
Tags:javadoommoddingигры
Hubs: Java Games and game consoles
+3
2.3k 6
Comments 4