Improve touch controls (#283)

This commit is contained in:
Alexander Batalov 2023-05-09 18:36:20 +03:00 committed by GitHub
parent efdc2e0199
commit a06097aef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 405 additions and 129 deletions

View File

@ -271,6 +271,8 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
"src/sfall_lists.h"
"src/sfall_opcodes.cc"
"src/sfall_opcodes.h"
"src/touch.cc"
"src/touch.h"
)
if(IOS)

View File

@ -42,7 +42,11 @@ $ sudo apt install libsdl2-2.0-0
### Android
> **NOTE**: Fallout 2 was designed with mouse in mind. There are many controls that require precise cursor positioning, which is not possible with fingers. When playing on Android you'll use fingers to move mouse cursor, not a character, or a map. Double tap to "click" left mouse button in the current cursor position, triple tap to "click" right mouse button. It might feel awkward at first, but it's super handy - you can play with just a thumb. This is not set in stone and might change in the future.
> **NOTE**: Fallout 2 was designed with mouse in mind. There are many controls that require precise cursor positioning, which is not possible with fingers. Current control scheme resembles trackpad usage:
- One finger moves mouse cursor around.
- Tap one finger for left mouse click.
- Tap two fingers for right mouse click (switches mouse cursor mode).
- Move two fingers to scroll current view (map view, worldmap view, inventory scrollers).
> **NOTE**: From Android standpoint release and debug builds are different apps. Both apps require their own copy of game assets and have their own savegames. This is intentional. As a gamer just stick with release version and check for updates.

View File

@ -2,30 +2,9 @@
namespace fallout {
enum InputType {
INPUT_TYPE_MOUSE,
INPUT_TYPE_TOUCH,
} InputType;
static int gLastInputType = INPUT_TYPE_MOUSE;
static int gTouchMouseLastX = 0;
static int gTouchMouseLastY = 0;
static int gTouchMouseDeltaX = 0;
static int gTouchMouseDeltaY = 0;
static int gTouchFingers = 0;
static unsigned int gTouchGestureLastTouchDownTimestamp = 0;
static unsigned int gTouchGestureLastTouchUpTimestamp = 0;
static int gTouchGestureTaps = 0;
static bool gTouchGestureHandled = false;
static int gMouseWheelDeltaX = 0;
static int gMouseWheelDeltaY = 0;
extern int screenGetWidth();
extern int screenGetHeight();
// 0x4E0400
bool directInputInit()
{
@ -71,49 +50,14 @@ bool mouseDeviceUnacquire()
// 0x4E053C
bool mouseDeviceGetData(MouseData* mouseState)
{
if (gLastInputType == INPUT_TYPE_TOUCH) {
mouseState->x = gTouchMouseDeltaX;
mouseState->y = gTouchMouseDeltaY;
mouseState->buttons[0] = 0;
mouseState->buttons[1] = 0;
mouseState->wheelX = 0;
mouseState->wheelY = 0;
gTouchMouseDeltaX = 0;
gTouchMouseDeltaY = 0;
Uint32 buttons = SDL_GetRelativeMouseState(&(mouseState->x), &(mouseState->y));
mouseState->buttons[0] = (buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
mouseState->buttons[1] = (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
mouseState->wheelX = gMouseWheelDeltaX;
mouseState->wheelY = gMouseWheelDeltaY;
if (gTouchFingers == 0) {
if (SDL_GetTicks() - gTouchGestureLastTouchUpTimestamp > 150) {
if (!gTouchGestureHandled) {
if (gTouchGestureTaps == 2) {
mouseState->buttons[0] = 1;
gTouchGestureHandled = true;
} else if (gTouchGestureTaps == 3) {
mouseState->buttons[1] = 1;
gTouchGestureHandled = true;
}
}
}
} else if (gTouchFingers == 1) {
if (SDL_GetTicks() - gTouchGestureLastTouchDownTimestamp > 150) {
if (gTouchGestureTaps == 1) {
mouseState->buttons[0] = 1;
gTouchGestureHandled = true;
} else if (gTouchGestureTaps == 2) {
mouseState->buttons[1] = 1;
gTouchGestureHandled = true;
}
}
}
} else {
Uint32 buttons = SDL_GetRelativeMouseState(&(mouseState->x), &(mouseState->y));
mouseState->buttons[0] = (buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
mouseState->buttons[1] = (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
mouseState->wheelX = gMouseWheelDeltaX;
mouseState->wheelY = gMouseWheelDeltaY;
gMouseWheelDeltaX = 0;
gMouseWheelDeltaY = 0;
}
gMouseWheelDeltaX = 0;
gMouseWheelDeltaY = 0;
return true;
}
@ -174,70 +118,6 @@ void handleMouseEvent(SDL_Event* event)
gMouseWheelDeltaX += event->wheel.x;
gMouseWheelDeltaY += event->wheel.y;
}
if (gLastInputType != INPUT_TYPE_MOUSE) {
// Reset touch data.
gTouchMouseLastX = 0;
gTouchMouseLastY = 0;
gTouchMouseDeltaX = 0;
gTouchMouseDeltaY = 0;
gTouchFingers = 0;
gTouchGestureLastTouchDownTimestamp = 0;
gTouchGestureLastTouchUpTimestamp = 0;
gTouchGestureTaps = 0;
gTouchGestureHandled = false;
gLastInputType = INPUT_TYPE_MOUSE;
}
}
void handleTouchEvent(SDL_Event* event)
{
int windowWidth = screenGetWidth();
int windowHeight = screenGetHeight();
if (event->tfinger.type == SDL_FINGERDOWN) {
gTouchFingers++;
gTouchMouseLastX = (int)(event->tfinger.x * windowWidth);
gTouchMouseLastY = (int)(event->tfinger.y * windowHeight);
gTouchMouseDeltaX = 0;
gTouchMouseDeltaY = 0;
if (event->tfinger.timestamp - gTouchGestureLastTouchDownTimestamp > 250) {
gTouchGestureTaps = 0;
gTouchGestureHandled = false;
}
gTouchGestureLastTouchDownTimestamp = event->tfinger.timestamp;
} else if (event->tfinger.type == SDL_FINGERMOTION) {
int prevX = gTouchMouseLastX;
int prevY = gTouchMouseLastY;
gTouchMouseLastX = (int)(event->tfinger.x * windowWidth);
gTouchMouseLastY = (int)(event->tfinger.y * windowHeight);
gTouchMouseDeltaX += gTouchMouseLastX - prevX;
gTouchMouseDeltaY += gTouchMouseLastY - prevY;
} else if (event->tfinger.type == SDL_FINGERUP) {
gTouchFingers--;
int prevX = gTouchMouseLastX;
int prevY = gTouchMouseLastY;
gTouchMouseLastX = (int)(event->tfinger.x * windowWidth);
gTouchMouseLastY = (int)(event->tfinger.y * windowHeight);
gTouchMouseDeltaX += gTouchMouseLastX - prevX;
gTouchMouseDeltaY += gTouchMouseLastY - prevY;
gTouchGestureTaps++;
gTouchGestureLastTouchUpTimestamp = event->tfinger.timestamp;
}
if (gLastInputType != INPUT_TYPE_TOUCH) {
// Reset mouse data.
SDL_GetRelativeMouseState(NULL, NULL);
gLastInputType = INPUT_TYPE_TOUCH;
}
}
} // namespace fallout

View File

@ -18,6 +18,7 @@
#include "settings.h"
#include "svga.h"
#include "text_font.h"
#include "touch.h"
#include "window_manager.h"
namespace fallout {
@ -249,6 +250,11 @@ int gameMoviePlay(int movie, int flags)
break;
}
Gesture gesture;
if (touch_get_gesture(&gesture) && gesture.state == kEnded) {
break;
}
int x;
int y;
_mouse_get_raw_state(&x, &y, &buttons);

View File

@ -11,6 +11,7 @@
#include "mouse.h"
#include "svga.h"
#include "text_font.h"
#include "touch.h"
#include "vcr.h"
#include "win32.h"
@ -1084,9 +1085,13 @@ void _GNW95_process_message()
handleMouseEvent(&e);
break;
case SDL_FINGERDOWN:
touch_handle_start(&(e.tfinger));
break;
case SDL_FINGERMOTION:
touch_handle_move(&(e.tfinger));
break;
case SDL_FINGERUP:
handleTouchEvent(&e);
touch_handle_end(&(e.tfinger));
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
@ -1121,6 +1126,8 @@ void _GNW95_process_message()
}
}
touch_process_gesture();
if (gProgramIsActive && !keyboardIsDisabled()) {
// NOTE: Uninline
int tick = getTicks();

View File

@ -6,6 +6,7 @@
#include "kb.h"
#include "memory.h"
#include "svga.h"
#include "touch.h"
#include "vcr.h"
namespace fallout {
@ -381,6 +382,54 @@ void _mouse_info()
return;
}
Gesture gesture;
if (touch_get_gesture(&gesture)) {
static int prevx;
static int prevy;
switch (gesture.type) {
case kTap:
if (gesture.numberOfTouches == 1) {
_mouse_simulate_input(0, 0, MOUSE_STATE_LEFT_BUTTON_DOWN);
} else if (gesture.numberOfTouches == 2) {
_mouse_simulate_input(0, 0, MOUSE_STATE_RIGHT_BUTTON_DOWN);
}
break;
case kLongPress:
case kPan:
if (gesture.state == kBegan) {
prevx = gesture.x;
prevy = gesture.y;
}
if (gesture.type == kLongPress) {
if (gesture.numberOfTouches == 1) {
_mouse_simulate_input(gesture.x - prevx, gesture.y - prevy, MOUSE_STATE_LEFT_BUTTON_DOWN);
} else if (gesture.numberOfTouches == 2) {
_mouse_simulate_input(gesture.x - prevx, gesture.y - prevy, MOUSE_STATE_RIGHT_BUTTON_DOWN);
}
} else if (gesture.type == kPan) {
if (gesture.numberOfTouches == 1) {
_mouse_simulate_input(gesture.x - prevx, gesture.y - prevy, 0);
} else if (gesture.numberOfTouches == 2) {
gMouseWheelX = (prevx - gesture.x) / 2;
gMouseWheelY = (gesture.y - prevy) / 2;
if (gMouseWheelX != 0 || gMouseWheelY != 0) {
gMouseEvent |= MOUSE_EVENT_WHEEL;
_raw_buttons |= MOUSE_EVENT_WHEEL;
}
}
}
prevx = gesture.x;
prevy = gesture.y;
break;
}
return;
}
int x;
int y;
int buttons = 0;

290
src/touch.cc Normal file
View File

@ -0,0 +1,290 @@
#include "touch.h"
#include <algorithm>
#include <stack>
#include "svga.h"
namespace fallout {
#define TOUCH_PHASE_BEGAN 0
#define TOUCH_PHASE_MOVED 1
#define TOUCH_PHASE_ENDED 2
#define MAX_TOUCHES 10
#define TAP_MAXIMUM_DURATION 75
#define PAN_MINIMUM_MOVEMENT 4
#define LONG_PRESS_MINIMUM_DURATION 500
struct TouchLocation {
int x;
int y;
};
struct Touch {
bool used;
SDL_FingerID fingerId;
TouchLocation startLocation;
Uint32 startTimestamp;
TouchLocation currentLocation;
Uint32 currentTimestamp;
int phase;
};
static Touch touches[MAX_TOUCHES];
static Gesture currentGesture;
static std::stack<Gesture> gestureEventsQueue;
static int find_touch(SDL_FingerID fingerId)
{
for (int index = 0; index < MAX_TOUCHES; index++) {
if (touches[index].fingerId == fingerId) {
return index;
}
}
return -1;
}
static int find_unused_touch_index()
{
for (int index = 0; index < MAX_TOUCHES; index++) {
if (!touches[index].used) {
return index;
}
}
return -1;
}
static TouchLocation touch_get_start_location_centroid(int* indexes, int length)
{
TouchLocation centroid;
centroid.x = 0;
centroid.y = 0;
for (int index = 0; index < length; index++) {
centroid.x += touches[indexes[index]].startLocation.x;
centroid.y += touches[indexes[index]].startLocation.y;
}
centroid.x /= length;
centroid.y /= length;
return centroid;
}
static TouchLocation touch_get_current_location_centroid(int* indexes, int length)
{
TouchLocation centroid;
centroid.x = 0;
centroid.y = 0;
for (int index = 0; index < length; index++) {
centroid.x += touches[indexes[index]].currentLocation.x;
centroid.y += touches[indexes[index]].currentLocation.y;
}
centroid.x /= length;
centroid.y /= length;
return centroid;
}
void touch_handle_start(SDL_TouchFingerEvent* event)
{
// On iOS `fingerId` is an address of underlying `UITouch` object. When
// `touchesBegan` is called this object might be reused, but with
// incresed `tapCount` (which is ignored in this implementation).
int index = find_touch(event->fingerId);
if (index == -1) {
index = find_unused_touch_index();
}
if (index != -1) {
Touch* touch = &(touches[index]);
touch->used = true;
touch->fingerId = event->fingerId;
touch->startTimestamp = event->timestamp;
touch->startLocation.x = static_cast<int>(event->x * screenGetWidth());
touch->startLocation.y = static_cast<int>(event->y * screenGetHeight());
touch->currentTimestamp = touch->startTimestamp;
touch->currentLocation = touch->startLocation;
touch->phase = TOUCH_PHASE_BEGAN;
}
}
void touch_handle_move(SDL_TouchFingerEvent* event)
{
int index = find_touch(event->fingerId);
if (index != -1) {
Touch* touch = &(touches[index]);
touch->currentTimestamp = event->timestamp;
touch->currentLocation.x = static_cast<int>(event->x * screenGetWidth());
touch->currentLocation.y = static_cast<int>(event->y * screenGetHeight());
touch->phase = TOUCH_PHASE_MOVED;
}
}
void touch_handle_end(SDL_TouchFingerEvent* event)
{
int index = find_touch(event->fingerId);
if (index != -1) {
Touch* touch = &(touches[index]);
touch->currentTimestamp = event->timestamp;
touch->currentLocation.x = static_cast<int>(event->x * screenGetWidth());
touch->currentLocation.y = static_cast<int>(event->y * screenGetHeight());
touch->phase = TOUCH_PHASE_ENDED;
}
}
void touch_process_gesture()
{
Uint32 sequenceStartTimestamp = -1;
int sequenceStartIndex = -1;
// Find start of sequence (earliest touch).
for (int index = 0; index < MAX_TOUCHES; index++) {
if (touches[index].used) {
if (sequenceStartTimestamp > touches[index].startTimestamp) {
sequenceStartTimestamp = touches[index].startTimestamp;
sequenceStartIndex = index;
}
}
}
if (sequenceStartIndex == -1) {
return;
}
Uint32 sequenceEndTimestamp = -1;
if (touches[sequenceStartIndex].phase == TOUCH_PHASE_ENDED) {
sequenceEndTimestamp = touches[sequenceStartIndex].currentTimestamp;
// Find end timestamp of sequence.
for (int index = 0; index < MAX_TOUCHES; index++) {
if (touches[index].used
&& touches[index].startTimestamp >= sequenceStartTimestamp
&& touches[index].startTimestamp <= sequenceEndTimestamp) {
if (touches[index].phase == TOUCH_PHASE_ENDED) {
if (sequenceEndTimestamp < touches[index].currentTimestamp) {
sequenceEndTimestamp = touches[index].currentTimestamp;
// Start over since we can have fingers missed.
index = -1;
}
} else {
// Sequence is current.
sequenceEndTimestamp = -1;
break;
}
}
}
}
int active[MAX_TOUCHES];
int activeCount = 0;
int ended[MAX_TOUCHES];
int endedCount = 0;
// Split participating fingers into two buckets - active fingers (currently
// on screen) and ended (lifted up).
for (int index = 0; index < MAX_TOUCHES; index++) {
if (touches[index].used
&& touches[index].currentTimestamp >= sequenceStartTimestamp
&& touches[index].currentTimestamp <= sequenceEndTimestamp) {
if (touches[index].phase == TOUCH_PHASE_ENDED) {
ended[endedCount++] = index;
} else {
active[activeCount++] = index;
}
// If this sequence is over, unmark participating finger as used.
if (sequenceEndTimestamp != -1) {
touches[index].used = false;
}
}
}
if (currentGesture.type == kPan || currentGesture.type == kLongPress) {
if (currentGesture.state != kEnded) {
// For continuous gestures we want number of fingers to remain the
// same as it was when gesture was recognized.
if (activeCount == currentGesture.numberOfTouches && endedCount == 0) {
TouchLocation centroid = touch_get_current_location_centroid(active, activeCount);
currentGesture.state = kChanged;
currentGesture.x = centroid.x;
currentGesture.y = centroid.y;
gestureEventsQueue.push(currentGesture);
} else {
currentGesture.state = kEnded;
gestureEventsQueue.push(currentGesture);
}
}
// Reset continuous gesture if when current sequence is over.
if (currentGesture.state == kEnded && sequenceEndTimestamp != -1) {
currentGesture.type = kUnrecognized;
}
} else {
if (activeCount == 0 && endedCount != 0) {
// For taps we need all participating fingers to be both started
// and ended simultaneously (within predefined threshold).
Uint32 startEarliestTimestamp = -1;
Uint32 startLatestTimestamp = 0;
Uint32 endEarliestTimestamp = -1;
Uint32 endLatestTimestamp = 0;
for (int index = 0; index < endedCount; index++) {
startEarliestTimestamp = std::min(startEarliestTimestamp, touches[ended[index]].startTimestamp);
startLatestTimestamp = std::max(startLatestTimestamp, touches[ended[index]].startTimestamp);
endEarliestTimestamp = std::min(endEarliestTimestamp, touches[ended[index]].currentTimestamp);
endLatestTimestamp = std::max(endLatestTimestamp, touches[ended[index]].currentTimestamp);
}
if (startLatestTimestamp - startEarliestTimestamp <= TAP_MAXIMUM_DURATION
&& endLatestTimestamp - endEarliestTimestamp <= TAP_MAXIMUM_DURATION) {
TouchLocation currentCentroid = touch_get_current_location_centroid(ended, endedCount);
currentGesture.type = kTap;
currentGesture.state = kEnded;
currentGesture.numberOfTouches = endedCount;
currentGesture.x = currentCentroid.x;
currentGesture.y = currentCentroid.y;
gestureEventsQueue.push(currentGesture);
// Reset tap gesture immediately.
currentGesture.type = kUnrecognized;
}
} else if (activeCount != 0 && endedCount == 0) {
TouchLocation startCentroid = touch_get_start_location_centroid(active, activeCount);
TouchLocation currentCentroid = touch_get_current_location_centroid(active, activeCount);
// Disambiguate between pan and long press.
if (abs(currentCentroid.x - startCentroid.x) >= PAN_MINIMUM_MOVEMENT
|| abs(currentCentroid.y - startCentroid.y) >= PAN_MINIMUM_MOVEMENT) {
currentGesture.type = kPan;
currentGesture.state = kBegan;
currentGesture.numberOfTouches = activeCount;
currentGesture.x = currentCentroid.x;
currentGesture.y = currentCentroid.y;
gestureEventsQueue.push(currentGesture);
} else if (SDL_GetTicks() - touches[active[0]].startTimestamp >= LONG_PRESS_MINIMUM_DURATION) {
currentGesture.type = kLongPress;
currentGesture.state = kBegan;
currentGesture.numberOfTouches = activeCount;
currentGesture.x = currentCentroid.x;
currentGesture.y = currentCentroid.y;
gestureEventsQueue.push(currentGesture);
}
}
}
}
bool touch_get_gesture(Gesture* gesture)
{
if (gestureEventsQueue.empty()) {
return false;
}
*gesture = gestureEventsQueue.top();
gestureEventsQueue.pop();
return true;
}
} // namespace fallout

38
src/touch.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef FALLOUT_TOUCH_H_
#define FALLOUT_TOUCH_H_
#include <SDL.h>
namespace fallout {
enum GestureType {
kUnrecognized,
kTap,
kLongPress,
kPan,
};
enum GestureState {
kPossible,
kBegan,
kChanged,
kEnded,
};
struct Gesture {
GestureType type;
GestureState state;
int numberOfTouches;
int x;
int y;
};
void touch_handle_start(SDL_TouchFingerEvent* event);
void touch_handle_move(SDL_TouchFingerEvent* event);
void touch_handle_end(SDL_TouchFingerEvent* event);
void touch_process_gesture();
bool touch_get_gesture(Gesture* gesture);
} // namespace fallout
#endif /* FALLOUT_TOUCH_H_ */