This is a post about the algorithm I created to handle touchscreen input during the development of my mobile game Space Ninja. Space Ninja is available for purchase on the Google Play Store here.
Space Ninja
Space Ninja is an arcade-style mobile game in which you control a small spaceship with your finger to slice evil aliens in half which gives you points. The controls are very intuitive: the spaceship follows your finger when it is touching the screen. However, several problems appeared as I developed the game. In this article, I will describe them and explain how I solved them one by one.
The Basic Algorithm
Space Ninja is made in the Godot Game Engine. When a touch input happens in Godot, the engine calls _input(event) (although, I actually used _unhandled_input, but that’s a separate issue) which is a function the user writes to explain what should happen for any given input event. There are 3 relevant types of touch events:
- Press
- This is when you first place your finger on the touch screen
- Implemented in Godot as an InputEventScreenTouch with pressed==true
- Drag
- This is when you drag your finger across the screen
- Implemented in Godot an InputEventScreenDrag
- Release
- This is when you take your finger off the screen
- Implemented in Godot as an InputEventScreenTouch with pressed==false
Each of these events has a position (drag events have 2, a start and end position). So, the most basic algorithm would be to tell the ship to move towards the position (or end position) of any incoming event.
The Edge of the Screen
One problem happens when the player touches the very edge of the screen. Most phones have features at the edge of the screen such as virtual buttons or a swipe down menu. When the player is interacting with these things, they probably are not trying to move the ship to their finger.
The simple fix I used is to ignore inputs within a certain distance of the edge of the screen, so the ship simply will not follow your finger if you touch the very top part of the screen. This has the added benefit of preventing the player from moving outside of the play area, so they cannot do things like kill enemies at their spawn points.
Simultaneous Inputs
The biggest problem with the basic algorithm is that it doesn’t consider simultaneous inputs. When two fingers are on the screen at once, they can both cause touch events, so the ship will alternate trying to move between them which looks a bit erratic. It can also be used to kill some enemies much faster than intended.
I decided that my goal was for the ship to follow one finger up until it released and only then would it follow a new finger. I thought that Godot and/or Android might have a built-in solution for this problem, but if they do I couldn’t find it, so I was forced to build it myself.
A Flawed Solution
One way I considered solving this problem was to look at inputs on a frame-by-frame basis. Each frame, I would collect all the new touch inputs that happened. Then, since the finger that was previously controlling the ship should still be close by only one frame later, only the event closest to the ship would be activated and the rest ignored.
This is a great idea, but has some difficulties that turned me away from it. First of all, Godot doesn’t pass all inputs in a given frame, it passes them the moment each input happens. In order to implement the frame idea, I would need to keep a list of all events that have occurred since last frame and then activate the closest one. This waiting would add some amount of delay and also there is no guarantee that two touch events from the same finger will occur in the same frame. That is to say, if two fingers are on the screen at once, the same problem may still present itself occasionally.
The Final Solution
Instead, I took a different approach. My solution compares the distance of a new event, newEvent, to the previously accepted event prevEvent to a constant acceptedDistance. If newEvent is closer to prevEvent than acceptedDistance, then newEvent is accepted. Otherwise newEvent is ignored. Obviously, if there is no previous event (prevEvent==null), the distance is not considered at all and newEvent is accepted without a moment’s notice.
When accepting an event, the behavior is slightly different depending on the type of event.
- Press
- Ship is directed to event position
- prevEvent = newEvent
- Drag
- ship is directed to event end position
- prevEvent = newEvent
- Release
- prevEvent = null
The release event sets prevEvent to null because if the player releases their finger, the ship should now move towards any new touch event on the screen regardless of distance to the release event. So there is no reason to save release events.
Fail-Safe
The algorithm above ignores inputs that are too far from the previous event, but this can be dangerous. Consider the possibility that someone’s phone sends a drag event but never a release event when they let go of the screen. Then, the ship could become stuck in place and almost nothing the player does would move the ship at all. This would make the game literally unplayable and ruin their run.
To remedy this, I put a timer on how long prevEvent is stored. When prevEvent is set, a timer for 0.3 seconds begins. If it runs out, prevEvent is reset to null so that all inputs affect the ship again regardless of distance. This way, at most, someone could become unable to move for 0.3 seconds tops. And, the smooth handling of input is retained as well even with the timer, so no harm comes from adding this feature.