Tutorial 49: Touch Input and Gestures
Platform notes. CNA's touch input is routed through the SDL3 backend. The API surface is implemented, but full hardware validation on Android and iOS touch panels is pending. On desktop Linux/Windows, SDL3 can forward touch-screen events from tablets and some laptop touchscreens. Test on real hardware before shipping.
TouchPanel.GetState()
Returns a TouchCollection containing all currently active touch contacts.
#include "Microsoft/Xna/Framework/Input/Touch/TouchPanel.hpp"
#include "Microsoft/Xna/Framework/Input/Touch/TouchCollection.hpp"
#include "Microsoft/Xna/Framework/Input/Touch/TouchLocation.hpp"
using namespace Microsoft::Xna::Framework::Input::Touch;
void Update(GameTime& gameTime) override {
TouchCollection touches = TouchPanel::GetState();
for (const TouchLocation& touch : touches) {
Vector2 pos = touch.Position; // screen coordinates (pixels)
int id = touch.Id; // unique per contact (finger)
switch (touch.State) {
case TouchLocationState::Pressed:
onTouchDown(id, pos);
break;
case TouchLocationState::Moved:
onTouchMove(id, pos);
break;
case TouchLocationState::Released:
onTouchUp(id, pos);
break;
case TouchLocationState::Invalid:
// Contact lost or prediction artefact — ignore
break;
}
}
}
TouchLocation struct
// TouchLocation fields (read-only)
struct TouchLocation {
int Id; // Unique ID per contact; stable while finger is down
TouchLocationState State; // Pressed / Moved / Released / Invalid
Vector2 Position; // Screen position in pixels
float Pressure; // 0.0 – 1.0 (hardware support varies)
// Try to get the state of this contact from the previous frame
bool TryGetPreviousLocation(TouchLocation& previousLocation) const;
};
TouchLocationState enum
enum class TouchLocationState {
Invalid, // lost or synthetic — do not use
Moved, // finger is held and has moved since last frame
Pressed, // finger just made contact
Released, // finger lifted off the screen
};
Enabling gesture recognition
// Call before the first Update (e.g. in Initialize())
TouchPanel::EnabledGestures =
GestureType::Tap |
GestureType::DoubleTap |
GestureType::FreeDrag |
GestureType::Pinch |
GestureType::PinchComplete;
// In Update(): drain the gesture queue
while (TouchPanel::IsGestureAvailable()) {
GestureSample gesture = TouchPanel::ReadGesture();
processGesture(gesture);
}
GestureType enum (selected values)
| GestureType | Description |
|---|---|
Tap | Quick press and release |
DoubleTap | Two rapid taps |
Hold | Finger held in place |
FreeDrag | Single-finger pan (any direction) |
HorizontalDrag | Horizontal-only pan |
VerticalDrag | Vertical-only pan |
DragComplete | Fired once when drag ends |
Flick | Fast swipe; GestureSample.Delta gives velocity |
Pinch | Two-finger spread/contract |
PinchComplete | Fired once when pinch ends |
GestureSample fields
struct GestureSample {
GestureType GestureType; // which gesture was detected
TimeSpan Timestamp; // when the gesture was recognised
Vector2 Position; // primary contact position (finger 1)
Vector2 Position2; // secondary contact position (finger 2, for Pinch)
Vector2 Delta; // movement delta for this frame (drag, flick velocity)
Vector2 Delta2; // secondary delta (for Pinch)
};
Code example: drag gesture and pinch-to-zoom
class TouchDemo final : public Game {
public:
TouchDemo() : graphics_(this) {}
protected:
void Initialize() override {
Game::Initialize();
// Enable the gestures we need
TouchPanel::EnabledGestures =
GestureType::FreeDrag |
GestureType::DragComplete|
GestureType::Pinch |
GestureType::PinchComplete;
cameraOffset_ = Vector2::Zero;
zoom_ = 1.0f;
}
void Update(GameTime& gameTime) override {
// 1. Raw touch for individual finger tracking
TouchCollection touches = TouchPanel::GetState();
for (const auto& t : touches) {
if (t.State == TouchLocationState::Pressed)
lastTouchCount_ = static_cast<int>(touches.Count());
}
// 2. High-level gestures
while (TouchPanel::IsGestureAvailable()) {
GestureSample gs = TouchPanel::ReadGesture();
switch (gs.GestureType) {
case GestureType::FreeDrag:
// Pan the "camera" offset
cameraOffset_ = cameraOffset_ + gs.Delta;
break;
case GestureType::DragComplete:
// Could apply momentum / deceleration here
break;
case GestureType::Pinch: {
// Compute pinch scale from current vs previous finger distance
Vector2 prevDiff = gs.Position - gs.Position2;
Vector2 currDiff = (gs.Position + gs.Delta)
- (gs.Position2 + gs.Delta2);
float prevDist = prevDiff.Length();
float currDist = currDiff.Length();
if (prevDist > 1.0f) {
float scale = currDist / prevDist;
zoom_ = MathHelper::Clamp(zoom_ * scale, 0.2f, 5.0f);
}
break;
}
case GestureType::PinchComplete:
// Snap zoom to nearest 0.5 step (optional)
zoom_ = std::round(zoom_ * 2.0f) / 2.0f;
break;
default:
break;
}
}
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::CornflowerBlue);
// Apply pan + zoom via a SpriteBatch transform
Matrix transform = Matrix::CreateTranslation(cameraOffset_.X,
cameraOffset_.Y, 0)
* Matrix::CreateScale(zoom_);
spriteBatch_->Begin(SpriteSortMode::Deferred,
nullptr, nullptr, nullptr, nullptr,
nullptr, transform);
// ... draw game world ...
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
Vector2 cameraOffset_;
float zoom_ = 1.0f;
int lastTouchCount_ = 0;
};