Improve touch controls

This commit is contained in:
Alexander Batalov 2023-05-09 15:42:04 +03:00
parent efdc2e0199
commit 4b9a55c49c
7 changed files with 400 additions and 128 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

@ -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_ */