20151004

Handling input

The problem: how to structure the input handling in a game that could run on PC and mobile devices? Each platform has different control schemes. In cases like this, I like to look for the common denominator. In this case, if we use only the left mouse button, the mouse wheel and the mouse itself, we get the same control actions as on the smartphone: tap (click), swipe (drag), pinch (scroll the mousewheel). Since I have experiences only with these two kind of devices, I won't talk about others, but I assume it isn't hard to fit these three actions over them.

Using these input methods, we can define events, like Tap, Drag and Zoom (because fuck naming consistency). However, we can zoom while we are dragging, so we will track the zoom level separately from the other events. We will also need a None event type, for the times nothing happens.

Let's see what kind of event transitions are available: take None, for example: when the user clicks (or touches the screen), it could mean either a start of a Tap, or the start of a drag. How to decide which one? I arbitrarily decided that if the touch (click) lasts no longer than 5 frames, and at the end of it the input device's position is not further than 10 pixels from the original place, I will consider it a tap, otherwise it's a drag. Until it isn't decided, I will call that state Hold.

So None can transform to Hold, Hold can transform to Drag or Tap, and Drag / Tap can transform to None. Maybe introducing Hold isn't a good idea, but I can't see any problems with it yet. Time will tell.

Now, when I worked on other parts of the code, I just threw in some GetMouseButtonDown(0) to check if the mouse was pressed. But now, that I want to use the input, this kind of decentralized input checking is starting to get messy, harder to understand and error-prone. This is why I have created an InputObject, and attached an InputController script to it.

This script has three public variables (scrolling being the fourth, but I will implement that later): current input mode, event position and last position. The current mode can either be None, Hold, Tap or Drag. The event position will always tell where was the cursor when the action happened. In the case of Tap, it is where the action ended, and in the case of drag, where was the cursor dragged to in each frame, and also the last position will tell where the cursor was dragged from. (In all other cases, the last position tells nothing. I know it could be done more elegantly, but this is straightforward enough for now. If I'll make a library out of it, which I seriously doubt, I will do it elegantly.)

Then, in the Update() function, I just use a switch on currentMode to decide what to do next. The cases are bold here.

void Update () {
  leftButtonDown = Input.GetMouseButton(0);
  Vector2 currentPosition = Input.mousePosition;

  switch (currentInputMode) {
  case InputMode.None:
    if (leftButtonDown) {
      currentInputMode = InputMode.Hold;
      lastPosition = currentPosition;
    }
    break;
 
  case InputMode.Hold:
    if (GetFastDistance(currentPosition, lastPosition) < 10 &&
        framesSinceLastClick < 5) {

       if (!leftButtonDown) {
         currentInputMode = InputMode.Tap;
         eventPosition = currentPosition;

       }
    }
    else {
      currentInputMode = InputMode.Drag;
      eventPosition = currentPosition;
    }
    break;
 
  case InputMode.Tap:
    currentInputMode = InputMode.None;
    break;
 
  case InputMode.Drag:
    if (leftButtonDown) {
      framesSinceLastClick++;
      lastPosition = eventPosition;
      eventPosition = currentPosition;
    }
    else {
      framesSinceLastClick = 0;
      currentInputMode = InputMode.None;
    }
    break;
  }
}

Also, since I don't need a precise distance, just something like a pseudo-distance, here is a faster version, that defines a square-shaped area rotated by 45°:

int GetFastDistance(Vector2 p1, Vector2 p2) {
   return Mathf.RoundToInt(
    Mathf.Abs(p1.x - p2.x) + Mathf.Abs(p1.y - p2.y));
}

And this works surprisingly well. Now, the camera controller's update has a code snippet responsible for calculating the amount of displacement for the camera:

 if (inputMode == InputController.InputMode.Drag) {
  Vector3 difference =
    inputController.GetCurrentPosition() -
    inputController.GetPreviousPosition();
  Vector3 pos = myCamera.ScreenToViewportPoint(difference);
  currentMouseDragSpeed =
    new Vector2(-pos.x * mouseDragSpeed, -pos.y * mouseDragSpeed);
}
else {
  currentMouseDragSpeed = currentMouseDragSpeed * 0.8f;
}

(Don't worry, my code is commented, I just don't paste the comments here.)
 

No comments:

Post a Comment