Touch

How to read finger touches and Piece placements in your game. The data model is identical across the three SDKs; only the way you receive contacts and the per-engine conventions for coordinates and orientation differ. This guide covers both, side by side.

New to Board’s touch system? Read Touch for how recognition, noise rejection, and unlimited contacts work, and Pieces for what a Glyph is.


The contact model

Every SDK reports touch through the same logical object: a contact. A contact is either a finger or a Piece, and it carries the same information across Unity, Godot, and Web. A contact keeps a stable identity from the moment it appears until it’s lifted or canceled, so you can follow one finger or one Piece across frames.

Field Meaning
contact id Stable identifier for this contact, constant for its whole lifetime
type Whether this is a finger or a Glyph (Piece)
position Where the contact is on the display
orientation Rotation of the Piece (Pieces only; fingers have no orientation)
phase Where the contact is in its lifecycle (see below)
glyph id Which Piece in the Piece Set this is (Pieces only)
touched Whether a hand is currently touching the Piece (Pieces only; always true for fingers)

Phases

A contact moves through a lifecycle. Every SDK reports the same phases:

Phase Meaning
Began The contact appeared this frame
Moved The contact’s position or orientation changed
Stationary The contact is present but unchanged this frame
Ended The contact was lifted normally
Canceled The contact ended abnormally (for example, the pause screen opened)

When the pause screen opens, all active contacts are canceled. Clean up any per-contact state when you receive a Canceled phase.

Fingers vs Pieces

  • Fingers have a position and a phase. They have no meaningful orientation, no Glyph id, and are always considered “touched.”
  • Pieces (Glyphs) additionally carry an orientation, a glyph id that tells you which Piece from the set it is, and a touched flag that tells you whether a hand is currently on the Piece.

Per-SDK conventions

The model above is identical across SDKs. Three representation details differ — each SDK presents data in its host engine’s native convention so it feels native and you don’t have to convert it yourself. When porting code or reading across SDKs, mind these:

Convention Unity Godot Web
Position type Vector2 (screenPosition) x / y in a Dictionary x / y numbers
Origin / Y axis Bottom-left, Y up (Unity screen space) Top-left, Y down Top-left, Y down
Orientation units Radians Degrees Degrees
Field naming camelCase (contactId, glyphId) snake_case (contact_id, glyph_id) camelCase (contactId, glyphId)

Coordinates are in display pixels in every SDK (the panel is 1920×1080). Only the origin, axis direction, and orientation units change.


Reading active contacts

How you receive contacts is the biggest difference between the SDKs. Unity polls them each frame. Godot emits a signal once per inference frame. Web subscribes a callback that fires per frame.

using Board.Input;

void Update()
{
    // Get all active contacts (fingers and Pieces) each frame.
    var contacts = BoardInput.GetActiveContacts();

    foreach (var contact in contacts)
    {
        ProcessContact(contact);
    }
}
import { Board } from "@board.fun/web-sdk";

if (Board.isOnDevice) {
  Board.input.subscribe((contacts) => {
    // Called once per inference frame with the full current contact set.
    for (const c of contacts) {
      processContact(c);
    }
  });
}
func _ready() -> void:
    if not Board.is_on_device:
        return
    # Activate the detector with your Piece Set model, then subscribe.
    Board.input.activate("models/your_piece_set.tflite")
    Board.input.contacts_received.connect(_on_contacts)
    Board.input.subscribe()

func _on_contacts(contacts: Array) -> void:
    # Called once per inference frame with the full current contact set.
    for c in contacts:
        process_contact(c)

Filtering by type

Handle fingers and Pieces separately by filtering. Unity and Web split on the type field; on the Godot channel every contact is reported as a Glyph type, so split on glyph_id there (0 is a finger, 1+ is a Piece).

// Only finger touches
var fingers = BoardInput.GetActiveContacts(BoardContactType.Finger);

// Only Pieces
var pieces = BoardInput.GetActiveContacts(BoardContactType.Glyph);
import { Board, BoardContactType } from "@board.fun/web-sdk";

Board.input.subscribe((contacts) => {
  const fingers = contacts.filter(c => c.type === BoardContactType.Finger);
  const pieces  = contacts.filter(c => c.type === BoardContactType.Glyph);
});
func _on_contacts(contacts: Array) -> void:
    # On the Godot channel the tracker stamps every contact as type_id 1,
    # so filter finger vs Piece on glyph_id (0 = finger, 1+ = Piece).
    for c in contacts:
        if int(c.glyph_id) == 0:
            handle_finger(c)
        else:
            handle_piece(c)

Position

// Vector2 in Unity screen space (origin bottom-left, Y up).
Vector2 position = contact.screenPosition;

// Convert to a world position for 2D games.
Vector3 worldPos = Camera.main.ScreenToWorldPoint(
    new Vector3(position.x, position.y, 10f)
);
// Pixels, origin top-left, Y down (matches canvas / DOM coordinates).
const { x, y } = contact;
element.style.transform = `translate(${x}px, ${y}px)`;
# Pixels, origin top-left, Y down (matches Godot's 2D screen space).
var pos := Vector2(c.x, c.y)
sprite.position = pos

Orientation

Piece rotation only. Fingers have no orientation.

// Radians, counter-clockwise from vertical.
float rotation = contact.orientation;

// Apply to a transform (convert radians to degrees for Euler).
transform.rotation = Quaternion.Euler(0, 0, rotation * Mathf.Rad2Deg);
// Degrees.
const rotation = contact.orientation;
element.style.transform = `rotate(${rotation}deg)`;
# Degrees, screen-CW positive.
sprite.rotation_degrees = c.orientation

Handling phases

A typical contact handler spawns a visual on Began, updates it on Moved, and tears it down on Ended or Canceled.

void ProcessContact(BoardContact contact)
{
    switch (contact.phase)
    {
        case BoardContactPhase.Began:
            SpawnContactVisual(contact);
            break;

        case BoardContactPhase.Moved:
            UpdateContactVisual(contact);
            break;

        case BoardContactPhase.Stationary:
            // Contact unchanged, may still need processing
            break;

        case BoardContactPhase.Ended:
        case BoardContactPhase.Canceled:
            RemoveContactVisual(contact);
            break;
    }
}
import { BoardContactPhase, type BoardContact } from "@board.fun/web-sdk";

function processContact(c: BoardContact) {
  switch (c.phase) {
    case BoardContactPhase.Began:
      spawnContactVisual(c);
      break;
    case BoardContactPhase.Moved:
      updateContactVisual(c);
      break;
    case BoardContactPhase.Stationary:
      // unchanged
      break;
    case BoardContactPhase.Ended:
    case BoardContactPhase.Canceled:
      removeContactVisual(c);
      break;
  }
}
func process_contact(c: Dictionary) -> void:
    match c.phase_id:
        Board.input.PHASE_BEGAN:
            spawn_contact_visual(c)
        Board.input.PHASE_MOVED:
            update_contact_visual(c)
        Board.input.PHASE_STATIONARY:
            pass
        Board.input.PHASE_ENDED, Board.input.PHASE_CANCELED:
            remove_contact_visual(c)

Piece touch state

Pieces report whether a finger is touching them. Use this to highlight Pieces when picked up, run different behavior for held vs resting Pieces, and detect releases.

if (contact.type == BoardContactType.Glyph)
{
    if (contact.isTouched)
    {
        HighlightPiece(contact); // player is holding it
    }
    else
    {
        ShowIdleState(contact);  // resting on the board
    }
}
if (contact.type === BoardContactType.Glyph) {
  if (contact.isTouched) {
    highlightPiece(contact);
  } else {
    showIdleState(contact);
  }
}
if int(c.glyph_id) > 0:  # glyph_id 0 is a finger; 1+ is a Piece
    if c.is_touched:
        highlight_piece(c)
    else:
        show_idle_state(c)

Glyph IDs

For Piece contacts, the glyph id tells you which Piece from the set this is. Each id is a small integer that is stable for that Piece type within your Piece Set (in Godot and Web, id 0 means a finger and Piece ids start at 1). To find out which id corresponds to which physical Piece, place each Piece on Board one at a time and log the id.

if (contact.type == BoardContactType.Glyph)
{
    switch (contact.glyphId)
    {
        case 0: SpawnWarrior(contact); break;
        case 1: SpawnMage(contact);    break;
        case 2: SpawnArcher(contact);  break;
    }
}
// glyphId 0 is a finger; Piece ids start at 1.
if (contact.type === BoardContactType.Glyph) {
  switch (contact.glyphId) {
    case 1: spawnWarrior(contact); break;
    case 2: spawnMage(contact);    break;
    case 3: spawnArcher(contact);  break;
  }
}
# glyph_id 0 is a finger; Piece ids start at 1.
if int(c.glyph_id) > 0:
    match c.glyph_id:
        1: spawn_warrior(c)
        2: spawn_mage(c)
        3: spawn_archer(c)

See Also