Weapon wheel в Doom 1993

    Приветствую.

    Многие из нас с теплотой относятся к олдскульным видеоиграм, вышедшим на стыке веков. У них превосходная атмосфера, бешеная динамика и множество оригинальных решений, которые не устарели спустя десятилетия. Однако в наши дни видение интерфейса игр несколько изменилось — на смену запутанным уровням пришли линейные коридоры, на смену аптечкам — регенерация, а вместо длинного ряда клавиш 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.

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

    Исходники
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      +3
      Статья могла бы быть интересной, если бы автор потратил на нее чуть больше времени, например указал бы, что за исходники он модифицирует, как, и зачем. Вместо этого на экране обрывок неструктурированной мысли — рассказ про дум, потом внезапно код перехвата собыитй мышки и все.
        0
        Благодарю.
        Если честно, не очень понимаю, как можно перейти от ретроспективы к, непосредственно, модификации, так, что бы переход не бросался в глаза…
        Все исходники, которые используются — это библиотека JNativeHook, остальное — самописное.
        +1
        За это консольное колесо (особенно в динамичных шутерах) геймдизайнеров надо расстреливать из BFG.
          +4

          Колесо — это следствие консольного управления стиками геймпада. И на геймпадах оно удобнее. А вот за идею геймпадов в шутерах я бы стрелял

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

        Самое читаемое