From 4b9a55c49c8a0d1041aeec2fae793c41147f5bee Mon Sep 17 00:00:00 2001 From: Alexander Batalov Date: Tue, 9 May 2023 15:42:04 +0300 Subject: [PATCH] Improve touch controls --- CMakeLists.txt | 2 + src/dinput.cc | 134 ++------------------- src/game_movie.cc | 6 + src/input.cc | 9 +- src/mouse.cc | 49 ++++++++ src/touch.cc | 290 ++++++++++++++++++++++++++++++++++++++++++++++ src/touch.h | 38 ++++++ 7 files changed, 400 insertions(+), 128 deletions(-) create mode 100644 src/touch.cc create mode 100644 src/touch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fc81b1..dc311d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/dinput.cc b/src/dinput.cc index d84b9f3..4fae607 100644 --- a/src/dinput.cc +++ b/src/dinput.cc @@ -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 diff --git a/src/game_movie.cc b/src/game_movie.cc index b4253a8..c8cb9aa 100644 --- a/src/game_movie.cc +++ b/src/game_movie.cc @@ -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); diff --git a/src/input.cc b/src/input.cc index 95bbc2d..ccfa5a6 100644 --- a/src/input.cc +++ b/src/input.cc @@ -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(); diff --git a/src/mouse.cc b/src/mouse.cc index 6e8a8bb..7815d70 100644 --- a/src/mouse.cc +++ b/src/mouse.cc @@ -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; diff --git a/src/touch.cc b/src/touch.cc new file mode 100644 index 0000000..473fe70 --- /dev/null +++ b/src/touch.cc @@ -0,0 +1,290 @@ +#include "touch.h" + +#include +#include + +#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 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(event->x * screenGetWidth()); + touch->startLocation.y = static_cast(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(event->x * screenGetWidth()); + touch->currentLocation.y = static_cast(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(event->x * screenGetWidth()); + touch->currentLocation.y = static_cast(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 diff --git a/src/touch.h b/src/touch.h new file mode 100644 index 0000000..2a76702 --- /dev/null +++ b/src/touch.h @@ -0,0 +1,38 @@ +#ifndef FALLOUT_TOUCH_H_ +#define FALLOUT_TOUCH_H_ + +#include + +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_ */