Touch Controls: Move, Rotate, and Drop
With gravity and collision working, this lesson wires up the controls a player actually taps: move left/right, rotate, and drop.
Move and rotate handlers
function useTetris() {
const [board] = useState<Matrix>(createEmptyBoard);
const [piece, setPiece] = useState<ActivePiece>(() => spawnPiece('T'));
function tryMove(deltaRow: number, deltaCol: number) {
setPiece((current) => {
const newRow = current.row + deltaRow;
const newCol = current.col + deltaCol;
if (hasCollision(board, current, newRow, newCol)) return current; // illegal, stay put
return { ...current, row: newRow, col: newCol };
});
}
function tryRotate() {
setPiece((current) => {
const rotatedShape = rotate(current.shape);
const rotated = { ...current, shape: rotatedShape };
if (hasCollision(board, rotated, current.row, current.col)) return current; // would overlap, cancel
return rotated;
});
}
return { board, piece, tryMove, tryRotate };
}
Both functions follow the same shape as hasCollision from the last lesson: compute what the piece would look like, check it, and only apply the change if it's legal. Using the updater form of setPiece (setPiece((current) => ...)) rather than reading piece directly matters here, it guarantees you're always checking against the truly current state, even if multiple moves happen in quick succession before a re-render.
On-screen buttons
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
function Controls({ onLeft, onRight, onRotate, onDrop }: {
onLeft: () => void;
onRight: () => void;
onRotate: () => void;
onDrop: () => void;
}) {
return (
<View style={styles.row}>
<TouchableOpacity style={styles.button} onPress={onLeft}><Text style={styles.label}>◀</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={onRotate}><Text style={styles.label}>⟳</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={onDrop}><Text style={styles.label}>▼</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={onRight}><Text style={styles.label}>▶</Text></TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: 'row', justifyContent: 'space-around', padding: 16 },
button: { backgroundColor: '#1e293b', borderRadius: 12, paddingVertical: 14, paddingHorizontal: 20 },
label: { color: '#fff', fontSize: 20 },
});
TouchableOpacity is React Native's tappable-button-with-a-fade-feedback component, roughly the mobile equivalent of a styled <button> on the web. Each button just calls one of the handler functions from useTetris, onLeft={() => tryMove(0, -1)}, onRight={() => tryMove(0, 1)}, onDrop={() => tryMove(1, 0)}, onRotate={tryRotate}.
TIP
Simple on-screen buttons like this are the easiest way to get Tetris fully playable on a touchscreen. Swipe gestures (via react-native-gesture-handler) feel more natural for a polished version, that's one of this course's final project stretch goals.