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.
Конструктивная критика приветствуется.
Исходники

ссылка на оригинал статьи https://habr.com/ru/post/508800/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *