官术网_书友最值得收藏!

Creating a virtual joystick

We are going to improve the user input and we are going to do it by creating a virtual joystick.

A virtual joystick measures the distance from the touch position to its center and uses this information to set the values on the two axes. It behaves as a traditional analog joystick.

Since it is virtual, we are not constrained to have it at a specific position on the screen, so we can place it anywhere the player touches it.

We cannot, however, take the entire screen for the virtual joystick. There needs to be a fire button too.

We have experienced the frustration of small touch targets, so we are going to make the fire button as big as we can. This means that we are going to use half the screen for the virtual joystick and half the screen for the fire button.

The layout that we are going to use will have two views that fill the screen, each of them covering half of the width. We will name this layout view_vjoystick.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <View android:id="@+id/vjoystick_main"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:layout_weight="1"
  />
  <View android:id="@+id/vjoystick_touch"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:layout_weight="1"
  />
</LinearLayout>

The interesting bit of this layout is the usage of Android:layout_weight to equally pide the screen into two halves. You can modify the weight value to make one view larger than the other if you want a larger space for the virtual joystick or the fire button.

We will create a class to handle this user InputController. We will call it VirtualJoystickInputController and it will, obviously, extend InputController.

To handle the events of this InputController, we are going to use two internal classes. One for each view we want to listen to the events to:

public VirtualJoystickInputController(View view) {
  view.findViewById(R.id.vjoystick_main)
    .setOnTouchListener(new VJoystickTouchListener());
  view.findViewById(R.id.vjoystick_touch)
    .setOnTouchListener(new VFireButtonTouchListener());

  double pixelFactor = view.getHeight() / 400d;
  mMaxDistance = 50*pixelFactor;
}

The mMaxDistance variable defines how far from the touch we consider the user to have reached the maximum. The value is, again, in screen units. You can imagine the maximum distance as the radius of the virtual gamepad. The smaller this distance is, the more sensitive the joystick is.

A small maximum distance will allow quick reactions, while a large one will allow better precision. Feel free to experiment with its size to make it work as you'd like.

The fire button is easier to handle than the virtual joystick. We use the same logic as in the previous example. Set mIsFiring to true when the event is a down action and set it to false when the event is an up action:

private class VFireButtonTouchListener implements View.OnTouchListener {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    int action = event.getActionMasked();
    if (action == MotionEvent.ACTION_DOWN) {
      mIsFiring = true;
    }
    else if (action == MotionEvent.ACTION_UP) {
      mIsFiring = false;
    }
    return true;
  }
}

The listener for the virtual joystick is more interesting. We record the position of the touch when a down action is performed, we also reset the values when the touch goes up. But, as long as it moves, we update the values of mHorizontalFactor and mVerticalFactor based on the distance to the original touch:

private class VJoystickTouchListener implements View.OnTouchListener {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    int action = event.getActionMasked();
    if (action == MotionEvent.ACTION_DOWN) {
      mStartingPositionX = event.getX(0);
      mStartingPositionY = event.getY(0);
    }
    else if (action == MotionEvent.ACTION_UP) {
      mHorizontalFactor = 0;
      mVerticalFactor = 0;
    }
    else if (action == MotionEvent.ACTION_MOVE) {
      // Get the proportion to the max
      mHorizontalFactor = (event.getX(0) - mStartingPositionX) / mMaxDistance;
      if (mHorizontalFactor > 1) {
        mHorizontalFactor = 1;
      }
      else if (mHorizontalFactor < -1) {
        mHorizontalFactor = -1;
      }
      mVerticalFactor = (event.getY(0) - mStartingPositionY) / mMaxDistance;
      if (mVerticalFactor > 1) {
        mVerticalFactor = 1;
      }
      else if (mVerticalFactor < -1) {
        mVerticalFactor = -1;
      }
    }
    return true;
  }
}

Please note that we want to keep mHorizontalFactor and mVerticalFactor between -1 and 1; thus, whenever the distance is larger than mMaxDistance, we do not consider it.

Finally, time to connect this new controller to the GameEngine. It is quite simple. We just have to update the layout for fragment_game.xml, including view_vjoystick.xml instead of view_keypad.xml, and then update the initialization of the GameEngine:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  tools:context="com.plattysoft.yass.counter.GameFragment">

  <TextView
    android:layout_gravity="top|left"
    android:id="@+id/txt_score"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />

  <Button
    android:layout_gravity="top|right"
    android:id="@+id/btn_play_pause"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/pause" />

  <include layout="@layout/view_vjoystick" />

</FrameLayout>

As a reminder, the initialization of the GameEngine is done inside onViewCreated of the GameFragment. We only need to create an instance of the proper InputController:

mGameEngine = new GameEngine(getActivity());
mGameEngine.setInputController(new 
 VirtualJoystickInputController(get View()));
mGameEngine.addGameObject(new Player(getView()));
mGameEngine.startGame();

Time to run the game and try this controller.

General considerations and improvements

This input method is a huge improvement over the basic keypad we did before. The touch area is as big as the screen and the player does not need to look at this area of the screen to tap on small buttons. It will work anywhere.

This system handles diagonal as well as horizontal and vertical movements and also anything in between.

The player does not need to remove his/her finger from the screen to change the directions.

There is a lack of visual feedback, which can be solved by drawing the virtual gamepad as two circles when the player is using it. A big circle will show the range of the virtual joystick, while a smaller one will show the current touch pointer. On the other hand, you may not want to, since the lack of visual clutter makes the screen cleaner.

主站蜘蛛池模板: 濉溪县| 盐源县| 株洲市| 高平市| 垣曲县| 平顺县| 城步| 丰原市| 迭部县| 南京市| 双牌县| 临西县| 鞍山市| 陕西省| 兰坪| 揭阳市| 泰和县| 酒泉市| 合水县| 汝州市| 垦利县| 林芝县| 乳源| 东兴市| 潞城市| 隆尧县| 乌鲁木齐市| 紫阳县| 普兰店市| 大余县| 乳源| 鹿邑县| 垫江县| 孝昌县| 桑日县| 东方市| 汾阳市| 中牟县| 巩留县| 连江县| 丽江市|