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