- Mastering Android Game Development
- Raul Portales
- 787字
- 2021-07-16 13:59:09
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.
- Unreal Engine Physics Essentials
- Reporting with Visual Studio and Crystal Reports
- 工程軟件開發技術基礎
- Pandas Cookbook
- Python入門很簡單
- 微服務設計原理與架構
- React.js Essentials
- Unity 5.x By Example
- 深入理解Elasticsearch(原書第3版)
- Kotlin從基礎到實戰
- Android移動開發案例教程:基于Android Studio開發環境
- Zabbix Performance Tuning
- Nagios Core Administration Cookbook(Second Edition)
- 零基礎學Python編程(少兒趣味版)
- 零基礎學C語言(升級版)