diff options
author | chai <chaifix@163.com> | 2019-08-14 22:50:43 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2019-08-14 22:50:43 +0800 |
commit | 15740faf9fe9fe4be08965098bbf2947e096aeeb (patch) | |
tree | a730ec236656cc8cab5b13f088adfaed6bb218fb /Runtime/Input/TouchPhaseEmulation.cpp |
Diffstat (limited to 'Runtime/Input/TouchPhaseEmulation.cpp')
-rw-r--r-- | Runtime/Input/TouchPhaseEmulation.cpp | 719 |
1 files changed, 719 insertions, 0 deletions
diff --git a/Runtime/Input/TouchPhaseEmulation.cpp b/Runtime/Input/TouchPhaseEmulation.cpp new file mode 100644 index 0000000..be1ab28 --- /dev/null +++ b/Runtime/Input/TouchPhaseEmulation.cpp @@ -0,0 +1,719 @@ +#include "UnityPrefix.h" + +/* + * TouchPhaseEmulation is layer between an 'event-based' touch OS (Android, Metro etc) and our phase-based script API (similar to iOS). + * + * The core logic of this layer is in DispatchTouchEvent, which handles mapping and collapsing of events to a frame-discrete phase. + */ + +#define DEBUG_TOUCH_EMU (DEBUGMODE && 0) + +#include "TouchPhaseEmulation.h" + +#if UNITY_BLACKBERRY + static const int touchTimeout = 400; +#else + static const int touchTimeout = 150; +#endif + +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + +class TouchImpl : public Touch +{ + enum { kEmptyTouchId = ~0UL }; + +public: + TouchImpl () + { + clear (); + } + + long long timestamp; // in milliseconds + UInt32 pointerId; // matches OS pointerId + size_t frameToReport; // frame # for this event to be reported. Should + // only be =gFrameCount or =gFrameCount+1 + size_t frameBegan; // frame # when BEGIN event was received + UInt32 endPhaseInQueue; // acts both as a bool to indicate that this event + // has already received an END/CANCEL from OS and + // will be reported to scripts next frame, and as + // a container to hold the actual value: was it + // END or CANCEL? + + void setDeltaTime (long long newTimestamp) + { + if (timestamp == 0) + return; + + deltaTime = (newTimestamp - timestamp) / 1000.0f; + } + + void setDeltaPos (Vector2f const& newPos) + { + if (CompareApproximately (pos, Vector2f::zero)) + return; + + deltaPos = newPos - pos; + } + + bool isMultitap (long long newTimestamp, Vector2f const& newPos, float screenDPI) + { + static const float tapZoneRadiusCM = 0.4f; // 4mm + static const float cmToInch = 0.393701f; + static const float multitapRadiusPixels = screenDPI * tapZoneRadiusCM * cmToInch; + static const float multitapRadiusSqr = multitapRadiusPixels * multitapRadiusPixels; + + return newTimestamp - timestamp < touchTimeout + && SqrMagnitude (pos - newPos) < multitapRadiusSqr; + } + + void setTapCount (long long newTimestamp, Vector2f const &newPos, float screenDPI) + { + if (isMultitap (newTimestamp, newPos, screenDPI)) + ++tapCount; + else + tapCount = 1; + } + + void init(size_t _pointerId, Vector2f _pos, TouchPhaseEmulation::TouchPhase _phase, + long long _timestamp, size_t currFrame) + { + pointerId = _pointerId; + pos = _pos; + rawPos = _pos; + phase = _phase; + frameBegan = currFrame; + timestamp = _timestamp; + frameToReport = currFrame; + } + + void clear () + { + id = kEmptyTouchId; + phase = TouchPhaseEmulation::kTouchCanceled; + endPhaseInQueue = 0; + deltaPos = Vector2f (0.0f, 0.0f); + deltaTime = 0.0f; + frameToReport = 0; + frameBegan = 0; + tapCount = 0; + rawPos = pos = Vector2f (0.0f, 0.0f); + timestamp = 0; + pointerId = kEmptyTouchId; + } + + bool isOld (size_t frame) const + { + return frameToReport < frame; + } + + bool isEmpty () const + { + return id == kEmptyTouchId; + } + + bool isFinished () const + { + return !isEmpty () && IsEnd (phase); + } + + bool willBeFinishedNextFrame () const + { + return !isEmpty () && IsEnd (endPhaseInQueue); + } + + bool isNow (size_t frame) const + { + return frameToReport == frame; + } + + static bool IsBegin (size_t phase) + { + return phase == TouchPhaseEmulation::kTouchBegan; + } + + static bool IsTransitional (size_t phase) + { + return phase == TouchPhaseEmulation::kTouchMoved || phase == TouchPhaseEmulation::kTouchStationary; + } + + static bool IsEnd (size_t phase) + { + return phase == TouchPhaseEmulation::kTouchEnded || phase == TouchPhaseEmulation::kTouchCanceled; + } + + +#if DEBUG_TOUCH_EMU + void dump (TouchImpl* gAllTouches) + { + size_t index = this - gAllTouches; + char const* phaseNames[] = { + "<Began>", + "<Moved>", + "<Stationary>", + "<Ended>", + "<Canceled>", + }; + + char const *fmt = + "T[%02d]={fid=%d, pid=%d, phase=%s, p=(%3.1f,%3.1f), dp=(%3.1f,%3.1f),\n" + " tm=%lld, dtm=%f, endPhaseQ=%d, fbeg=%d, frep=%d, tcnt=%d}\n"; + + printf_console (fmt, index, id, pointerId, phaseNames[phase], pos.x, pos.y, + deltaPos.x, deltaPos.y, timestamp, deltaTime, + endPhaseInQueue, frameBegan, frameToReport, tapCount); + } +#endif +}; + +// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + +TouchPhaseEmulation::TouchPhaseEmulation(float screenDPI, bool singleTouchDevice) +: m_AllocatedFingerIDs(0) +, m_FrameCount(0) +, m_ScreenDPI(screenDPI) +, m_IsMultiTouchEnabled(!singleTouchDevice) +, m_IsSingleTouchDevice(singleTouchDevice) +{ + m_TouchSlots = new TouchImpl[kMaxTouchCount]; + InitTouches(); +} + +TouchPhaseEmulation::~TouchPhaseEmulation() +{ + delete [] m_TouchSlots; +} + +void TouchPhaseEmulation::InitTouches () +{ + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + m_TouchSlots[i].clear(); + } + m_AllocatedFingerIDs = 0; + + m_FrameCount = 1; +} + +void TouchPhaseEmulation::PreprocessTouches () +{ + DiscardRedundantTouches(); +} + +void TouchPhaseEmulation::PostprocessTouches () +{ +#if DEBUG_TOUCH_EMU + DumpAll (); +#endif + ++m_FrameCount; + UpdateActiveTouches(); +} + + +bool TouchPhaseEmulation::IsExistingTouch( int pointerId ) +{ + TouchImpl* matchingSlots[kMaxTouchCount]; + const size_t slotsFound = FindByPointerId(matchingSlots, pointerId); + + for (size_t i = 0; i < slotsFound; ++i) + if (matchingSlots[i]) + return true; + + return false; +} + +void TouchPhaseEmulation::AddTouchEvent (int pointerId, float x, float y, TouchPhase newPhase, long long timestamp) +{ + Vector2f pos = Vector2f (x, y); + +#if UNITY_WINRT + // [Metro] With one finger touching; that touch pointerId is usually > 0 + if (!m_IsMultiTouchEnabled && GetTouchCount() > 0 && !IsExistingTouch(pointerId)) + return; +#else + if (!m_IsMultiTouchEnabled && pointerId > 0) + return; +#endif + + DispatchTouchEvent (pointerId, pos, static_cast<TouchPhase> (newPhase), timestamp, m_FrameCount); +} + +void TouchPhaseEmulation::DispatchTouchEvent (size_t pointerId, Vector2f pos, TouchPhase newPhase, long long timestamp, size_t currFrame) +{ + // Brief terminology legend: + // action The OS level indication of what happened in a particular touch event; DOWN, MOVE, UP etc. + // phase Logical state of a touch, emulated to the script side; Began/Moved/Ended. + // Phase is considered constant per frame, and all intra-frame actions are collapsed to a single phase. + // If two actions can't be collapsed (like UP/DOWN), then the DOWN action will be delayed one frame. + // pointerId The ID given to a touch event by the OS. These IDs can be reused if touches end/start within the same frame. + // fingerId The ID we give a touch when presented to the script side. Also known as 'id' in the Touch struct. + // touch event OS level touch information + // touch slot Script level touch information. + + // Step 1: Determine if already tracking this 'pointerId' + // We do this by looping through all touch slots while looking for an active touch with matching 'pointerId'. + // If a slot with phase == Ended has been inactive for 150ms => set it to Inactive + // Step 2: Determine if an old touch slot should be updated with the new event information, or if a new touch slot should be allocated. + // If no touch slot was found in Step 1 => allocate a new touch slot. Skip to step 4. + // If only a singular touch slot was found in Step 1 => use that slot. Skip to step 4. + // If multiple slots were found in Step 1 => determine which slot to use, or if another slot needs to be allocated. + // Step 3: Determine which active slot to use, or allocate a new. + // If phase == Began : for each slot with phase == Ended, consider event to be a multi-tap by comparing position and timestamp. + // If phase != Began : find slot with phase != Ended (should only be one!) + // If no slot matches : allocate a new slot with fingerId = highestFingerIdUsed + 1. + // Step 4: Initialize/update the touch slot based on current and old touch phase. + // If new phase == Began and old phase == Ended => increase tap count. + // If new phase == Began => try to compact finger id + // If new phase == Ended and old phase == Began on the same frame => delay new phase (Ended) until next frame. + // If new phase == Moved and old phase == Stationary => check distance against threshold for Moved/Stationary. + // + // After every frame, loop through all active touch slots: + // If a delayed phase (Ended) was enabled => update the phase. + // If a touch != Ended wasn't updated this frame => set phase = Stationary + // If a touch began and ended the this frame, and it resulted in another, currently active, touch having an increased tap count + // => clear those extra touches, and compact the finger id + + // NB: for now we do use passed position as both raw position and position + // on ios, where we actually implement raw position, different code is used + + FreeExpiredTouches(m_FrameCount, timestamp); + + TouchImpl* matchingSlots[kMaxTouchCount]; + const size_t slotsFound = FindByPointerId(matchingSlots, pointerId); + + TouchImpl* touch = NULL; + int inheritedTapCount = 0; + +#if UNITY_WINRT + // [Metro] Calculate tapCount manually as pointerIds aren't reused. + if (TouchImpl::IsBegin(newPhase)) + inheritedTapCount += CalculateTapCount(timestamp, pos); +#endif + + for (size_t i = 0; i < slotsFound; ++i) + { + TouchImpl* slot = matchingSlots[i]; + + bool touchFinished = slot->isFinished() || slot->willBeFinishedNextFrame(); + if (TouchImpl::IsBegin(newPhase)) + { + if (touchFinished) + { + if (slot->isOld(m_FrameCount)) + { + touch = slot; + } + + if (slot->isMultitap(timestamp, pos, m_ScreenDPI)) + { + inheritedTapCount = slot->tapCount; + } + } + } + else + { + if (!touchFinished) + { + if (touch) + { + if (DEBUGMODE) printf_console("Stale/stuck touch released."); + ExpireOld(*touch); + } + touch = slot; + } + } + } + + if (!touch) + { + if (!TouchImpl::IsBegin(newPhase)) + { + if (DEBUGMODE) printf_console("Dropping touch event part of canceled gesture."); + return; + } + if (!(touch = AllocateNew())) + return; + } + + if (TouchImpl::IsBegin (newPhase)) + { + touch->tapCount = inheritedTapCount; + #if DEBUG_TOUCH_EMU + printf_console("Slot before initialized:"); + touch->dump(m_TouchSlots); + #endif + + touch->init( pointerId, pos, newPhase, timestamp, currFrame ); + touch->setTapCount( timestamp, pos, m_ScreenDPI ); + touch->id = CompactFingerID(touch->id); + + #if DEBUG_TOUCH_EMU + printf_console("Slot initialized:"); + touch->dump(m_TouchSlots); + #endif + return; + } + else if (TouchImpl::IsEnd (newPhase)) + { + // if touch began this frame, we will delay end phase for one frame + if (touch->frameBegan == currFrame) + touch->endPhaseInQueue = newPhase; + else + touch->phase = newPhase; + + // Android only sends CANCELED on the first pointer in a gesture - kill all other active touches when this happens. + if (newPhase == kTouchCanceled) + { + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl* slot = &m_TouchSlots[i]; + if (slot->isEmpty() || slot->isFinished() || slot->willBeFinishedNextFrame()) + continue; + slot->endPhaseInQueue = newPhase; + #if DEBUG_TOUCH_EMU + m_TouchSlots[i].dump(m_TouchSlots); + #endif + } + } + } + else if (newPhase == kTouchMoved + && touch->phase == kTouchStationary) + { + // old event is STATIONARY, the new one is MOVE. Android does not + // report STATIONARY Events, so if MOVE's deltaPos is not big enough, + // let's keep it STATIONARY. Promote to MOVE otherwise. + static const float deltaPosTolerance = 0.5f; + if (Magnitude (touch->pos - pos) >= deltaPosTolerance) + touch->phase = newPhase; + } + + touch->setDeltaPos (pos); + touch->pos = pos; + + touch->setDeltaTime (timestamp); + touch->timestamp = timestamp; + touch->frameToReport = currFrame; + +#if DEBUG_TOUCH_EMU + printf_console("Slot updated:"); + touch->dump(m_TouchSlots); +#endif + +} + +size_t TouchPhaseEmulation::FindByPointerId(TouchImpl* matchingSlots[kMaxTouchCount], size_t pointerId) +{ +#if DEBUG_TOUCH_EMU + printf_console("%s", __FUNCTION__); +#endif + size_t slotsFound = 0; + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + if (m_TouchSlots[i].pointerId != pointerId) + continue; +#if DEBUG_TOUCH_EMU + m_TouchSlots[i].dump(m_TouchSlots); +#endif + matchingSlots[slotsFound++] = &m_TouchSlots[i]; + } + return slotsFound; +} + +TouchImpl* TouchPhaseEmulation::AllocateNew() +{ +#if DEBUG_TOUCH_EMU + printf_console("%s", __FUNCTION__); +#endif + // allocate virtual fingerId + int fingerId = 0; + const int maxFingerId = sizeof(m_AllocatedFingerIDs) * 8; + for (; fingerId < maxFingerId; ++fingerId) + { + UInt32 bitField = (1 << fingerId); + if (m_AllocatedFingerIDs & bitField) + continue; + m_AllocatedFingerIDs |= bitField; + break; + } + if (fingerId >= maxFingerId) + { + Assert (!"Out of virtual finger IDs!"); + return NULL; + } + + // find empty slot for touch + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl& t = m_TouchSlots[i]; + + if (!t.isEmpty()) + continue; + + t.id = fingerId; + t.deltaPos = Vector2f(0, 0); + t.deltaTime = 0.0f; + t.endPhaseInQueue = 0; + + return &t; + } + + Assert (!"Out of free touches!"); + return NULL; +} + +void TouchPhaseEmulation::ExpireOld(TouchImpl& touch) +{ +#if DEBUG_TOUCH_EMU + printf_console("%s", __FUNCTION__); +#endif + + if (touch.isEmpty()) + { + ErrorString("Trying to expire empty touch slot!"); + return; + } + + // deallocate virtual fingerId + UInt32 bitField = (1 << touch.id); + Assert((m_AllocatedFingerIDs & bitField) && "Touch with stale finger ID killed!"); + m_AllocatedFingerIDs &= ~bitField; + + Assert(!touch.endPhaseInQueue && "Delayed touch killed prematurely!"); + touch.clear(); +} + +int TouchPhaseEmulation::CompactFingerID(int id) +{ + int fingerId = 0; + const int maxFingerId = sizeof(m_AllocatedFingerIDs) * 8; + for (; fingerId < maxFingerId; ++fingerId) + { + UInt32 bitField = (1 << fingerId); + if (m_AllocatedFingerIDs & bitField) + continue; + + if (id < fingerId) + return id; + + m_AllocatedFingerIDs |= bitField; + bitField = (1 << id); + Assert((m_AllocatedFingerIDs & bitField) && "Touch with stale finger ID killed!"); + m_AllocatedFingerIDs &= ~bitField; + id = fingerId; + break; + } + return id; +} + +void TouchPhaseEmulation::FreeExpiredTouches (size_t eventFrame, long long timestamp) +{ + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl& touch = m_TouchSlots[i]; + + if (touch.isEmpty()) + continue; + + long long age = timestamp - touch.timestamp; + if (touch.isOld(eventFrame) && touch.isFinished() && age > touchTimeout) + { + ExpireOld(touch); + } + } +} + +void TouchPhaseEmulation::DiscardRedundantTouches() +{ + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl* t0 = &m_TouchSlots[i]; + TouchImpl* t1 = 0; + + if (t0->isEmpty()) + continue; + + // find Down/Up touches recorded within a single frame + bool downUpTouch = + t0->frameBegan == m_FrameCount && + t0->frameToReport == m_FrameCount && + t0->willBeFinishedNextFrame() && + !t0->isFinished(); + + if (!downUpTouch) + continue; + + #if DEBUG_TOUCH_EMU + printf_console("Found new touch set to expire next frame"); + t0->dump(m_TouchSlots); + #endif + + bool redundant = false; + + // compare the multitap info + for (size_t j = 0; j < kMaxTouchCount; ++j) + { + t1 = &m_TouchSlots[j]; + + if (t1->isEmpty() || i == j) + continue; + + bool multitapTouch = + t1->frameBegan == m_FrameCount && + t1->frameToReport == m_FrameCount && + t1->pointerId == t0->pointerId && + t1->tapCount > t0->tapCount && + t0->isMultitap(t1->timestamp, t1->pos, m_ScreenDPI) && + !t1->isFinished(); + + if (!multitapTouch) + continue; + + #if DEBUG_TOUCH_EMU + printf_console("Found new touch, with tapCount that matches the one found earlier"); + t1->dump(m_TouchSlots); + #endif + // found a match + redundant = true; + break; + } + + if (redundant) + { + t0->endPhaseInQueue = 0; // this touch is officially gone anyway + ExpireOld(*t0); + t1->id = CompactFingerID(t1->id); + #if DEBUG_TOUCH_EMU + printf_console("New touch, with compacted finger id"); + t1->dump(m_TouchSlots); + #endif + } + else + { + t0->id = CompactFingerID(t0->id); + #if DEBUG_TOUCH_EMU + printf_console("Refresh touch, with compacted finger id"); + t0->dump(m_TouchSlots); + #endif + } + } +} + +void TouchPhaseEmulation::UpdateActiveTouches() +{ + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl& touch = m_TouchSlots[i]; + + if (touch.isEmpty()) + continue; + + // Skip Expired Touches + if (touch.isFinished()) + { + continue; + } + + // End Delayed Touches + if (touch.willBeFinishedNextFrame ()) + { + touch.deltaPos = Vector2f (0, 0); + touch.phase = touch.endPhaseInQueue; + touch.endPhaseInQueue = 0; + touch.frameToReport = m_FrameCount; + continue; + } + + // Default Stationary Touches + touch.phase = kTouchStationary; + touch.deltaPos = Vector2f (0, 0); + touch.frameToReport = m_FrameCount; + } + +} + +size_t TouchPhaseEmulation::GetTouchCount () +{ + size_t count = 0; + + // TODO: on first call to GetTouchCount() per frame, call PackTouchIds() to + // compact virtual touch IDs to stand out less + + for (size_t i = 0; i < kMaxTouchCount; ++i) + if ( m_TouchSlots[i].isNow(m_FrameCount) && + !m_TouchSlots[i].isEmpty() ) + ++count; + + return count; +} + +size_t TouchPhaseEmulation::GetActiveTouchCount () +{ + size_t count = 0; + + for (size_t i = 0; i < kMaxTouchCount; ++i) + if (!m_TouchSlots[i].isEmpty() && !m_TouchSlots[i].isFinished()) + ++count; + + return count; +} + +// @param index Zero-based index of events that are to be reported this +// frame. Since not all of the events in the container need to +// be reported this frame, it's used to skip already reported +// ones. +bool TouchPhaseEmulation::GetTouch (size_t index, Touch& touch) +{ + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + if ( m_TouchSlots[i].isNow(m_FrameCount) && + !m_TouchSlots[i].isEmpty() && + index-- == 0 ) + { + touch = m_TouchSlots[i]; + return true; + } + } + + return false; +} + +bool TouchPhaseEmulation::IsMultiTouchEnabled () +{ + if (m_IsSingleTouchDevice) + return false; + + return m_IsMultiTouchEnabled; +} + +void TouchPhaseEmulation::SetMultiTouchEnabled (bool enabled) +{ + if (m_IsSingleTouchDevice) + return; + + m_IsMultiTouchEnabled = enabled; +} + +int TouchPhaseEmulation::CalculateTapCount( long long timestamp, Vector2f const &pos ) const +{ + int result = 0; + for (size_t i = 0; i < kMaxTouchCount; ++i) + { + TouchImpl& touch = m_TouchSlots[i]; + + if (touch.isEmpty()) + continue; + + if (touch.isMultitap(timestamp, pos, m_ScreenDPI)) + result += touch.tapCount; + } + + return result; +} + +#if DEBUG_TOUCH_EMU +void TouchPhaseEmulation::DumpAll (bool verbose) +{ + for (int i = 0; i < kMaxTouchCount; ++i) + if (!m_TouchSlots[i].isEmpty() && !m_TouchSlots[i].isOld(m_FrameCount) || verbose) + m_TouchSlots[i].dump( m_TouchSlots ); +} +#endif |