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

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