Weapon wheel in Doom 1993

Greetings.



Many of us have a fondness for old-school video games that came out at the turn of the century. They have an excellent atmosphere, frantic dynamics and many original solutions that have not become obsolete after decades. However, today the vision of the game interface has changed a bit - linear corridors have replaced the confused levels, regeneration has replaced the first-aid kits, and instead of a long row of 0-9 keys, the mouse wheel and then the virtual wheel have come to select the arsenal. It is about him that we will talk today.



image



Historical summary



Previously, during the emergence of the shooter genre as such, the question of mouse control was not raised - only the keyboard was used to control the protagonist. Moreover, there was no single management format either - WASD became the standard a little later. You can read more about old gaming keyboard layouts here .



Accordingly, in those games where the opportunity to choose equipment was implemented (Doom, Wolfenstein, Quake etc), the only intuitive way at that time was implemented - using the number keys on the keyboard. And for many years this method was the only one.

Then, in the late 90s, it became possible to change weapons with the mouse wheel.



It was not possible to find unambiguous information on this topic, but in CS 1.6 this feature was enabled through the console. However, such precedents may have existed before - in this case, please indicate this in the comments or in the PM. But in the usual form of Weapon Wheel in our time, it came into use only with Crysis and its Suit menu. Although attempts to do something similar began in HL2, the “wheel” went to the masses only in the late 00s, and now it is mainstream ...



However, this is only a historical summary, of interest only as history. Within the framework of this article, there will be no lengthy discussions about the reasons for the popularity of this or that solution. as well as taste about which selector is better. Simply because the following will describe the process of adapting the good old Doom to the choice of weapons with the mouse.



Setting goals



In order to implement WW, you need to somehow intercept the movement of the mouse, track its movement while the selector key is held down, and, when released, emulate a click on the button corresponding to the selected sector.



For this, I used the Java language, in particular, key interception is carried out using the jnativehook library, and pressing is done using awt.Robot. The processing of the received hooks is not difficult, therefore, it is done manually.



Implementation



Previously, classes were developed that specify pairs of coordinates for determining the displacement vector.



In particular, the Shift class allows you to store a two-dimensional vector, as well as determine its length, and the NormalisedShift class, designed to store a normalized vector, among other things, allows you to determine the angle between the intercepted vector and the vector (1,0).



Spoiler header
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);
    };
};




They are of no particular interest, and only lines 73-74, which normalize the vector, require comment. Among other things, the vector is flipped. neg is changing the reference system - the fact is that from the point of view of software and from the point of view of familiar mathematics, vectors are traditionally directed in different ways. That is why the vectors of the Shift class have the origin at the top left, and the NormalizedShift class have the bottom left.



To implement the program, the Wheel class was implemented that implements the NativeMouseMotionListener and NativeKeyListener interfaces. The code is under the spoiler.



Spoiler header
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;
        }
    }




Let's see what happens here.



The KEYCODE variable stores the code of the key used to invoke the selector. Usually this is TAB, but if necessary, it can be changed in the code or - ideally - pulled from the config file.



prev stores the position of the mouse cursor when the selector was called. Current position of the cursor is maintained in current. Accordingly, when the selector key is released, the vectors are subtracted and the shift of the cursor is written to the shift variable during the time the selector key is held down.



Then, on line 140, the vector is normalized, i.e. reduced to form when its length is close to unity. After that, the normalized vector is transferred to the matcher, which establishes a correspondence between the code of the key to be pressed and the angle of rotation of the vector. For readability reasons, the angle is converted to degrees, as well as - it is oriented along a complete unit circle (acos only works with angles up to 180 degrees).



The ButtonMatcher class defines the correspondence between the angle and the selected keycode.



Spoiler header
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);
    }
}




In addition, the variable numberOfButtons determines the number of sectors and their corresponding buttons, baseShift sets the angle of rotation (In particular, it provides symmetry about the vertical axis and turns the wheel 90 degrees so that the melee weapon is on top), and the codes array stores codes keys - in case the buttons are changed, and the codes will not be consecutive. In a more advanced version, it would be possible to pull them from the configuration file, but with the standard keyboard layout - the current version is quite viable.



Conclusion



Within the framework of this article, the possibility of customizing the interface of classic shooters for modern standards was described. Of course, we don't add any first-aid kits or linearity here - there are many mods for this, but often it is in such details that a friendly and convenient interface lies. The author realizes that he probably did not describe the most optimal way to achieve the desired result, and also waits for a picture with a loaf and a trolleybus in the comments, but nevertheless, it was an interesting experience that, perhaps, will encourage some gamer to discover amazing world of Java.



Constructive criticism is welcome.



Source code



All Articles