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

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.

主站蜘蛛池模板: 乌兰浩特市| 平安县| 蕲春县| 同心县| 延吉市| 临漳县| 桓仁| 上虞市| 台中市| 金坛市| 张掖市| 兴化市| 庆城县| 南昌市| 山东| 吉安市| 稷山县| 高唐县| 楚雄市| 宁国市| 资中县| 讷河市| 云林县| 南康市| 雷波县| 革吉县| 红河县| 龙陵县| 类乌齐县| 阿瓦提县| 翁牛特旗| 稷山县| 嘉黎县| 鲁山县| 长葛市| 怀远县| 枣强县| 灌云县| 柳河县| 房产| 衡水市|