← Public packages
@kentcdodds/space-shooter
Vertical scrolling spaceship shooter built with Three.js, playable on phone or desktop
src/input.ts
102 lines · 3.0 KB · TypeScriptimport * as THREE from 'three'
type InputOptions = {
element: HTMLElement
toWorld: (clientX: number, clientY: number) => THREE.Vector2 | null
getShipPosition: () => THREE.Vector2
clampTarget: (target: THREE.Vector2) => THREE.Vector2
}
const TOUCH_SENSITIVITY = 1.4
const movementKeys = new Map<string, [x: number, y: number]>([
['ArrowLeft', [-1, 0]],
['KeyA', [-1, 0]],
['ArrowRight', [1, 0]],
['KeyD', [1, 0]],
['ArrowUp', [0, 1]],
['KeyW', [0, 1]],
['ArrowDown', [0, -1]],
['KeyS', [0, -1]],
])
/**
* Keyboard moves the ship directly. The mouse sets an absolute target to fly
* toward. Touch uses relative dragging so the player's thumb never covers the
* ship.
*/
export class Input {
private readonly pressed = new Set<string>()
private target: THREE.Vector2 | null = null
private drag: { pointerId: number; last: THREE.Vector2 } | null = null
constructor(private readonly options: InputOptions) {
window.addEventListener('keydown', this.handleKeyDown)
window.addEventListener('keyup', this.handleKeyUp)
window.addEventListener('blur', () => this.pressed.clear())
const { element } = options
element.addEventListener('pointerdown', this.handlePointerDown)
element.addEventListener('pointermove', this.handlePointerMove)
element.addEventListener('pointerup', this.handlePointerUp)
element.addEventListener('pointercancel', this.handlePointerUp)
}
keyAxis() {
const axis = new THREE.Vector2()
for (const code of this.pressed) {
const [x, y] = movementKeys.get(code) ?? [0, 0]
axis.x += x
axis.y += y
}
return axis
}
getTarget() {
return this.target
}
reset() {
this.target = null
this.drag = null
}
private handleKeyDown = (event: KeyboardEvent) => {
if (!movementKeys.has(event.code)) return
event.preventDefault()
this.pressed.add(event.code)
this.target = null
}
private handleKeyUp = (event: KeyboardEvent) => {
this.pressed.delete(event.code)
}
private handlePointerDown = (event: PointerEvent) => {
if (event.pointerType === 'mouse') return
const world = this.options.toWorld(event.clientX, event.clientY)
if (!world) return
this.drag = { pointerId: event.pointerId, last: world }
this.target = this.options.getShipPosition().clone()
}
private handlePointerMove = (event: PointerEvent) => {
const world = this.options.toWorld(event.clientX, event.clientY)
if (!world) return
if (event.pointerType === 'mouse') {
this.target = this.options.clampTarget(world)
return
}
if (this.drag?.pointerId !== event.pointerId) return
const delta = world.clone().sub(this.drag.last).multiplyScalar(TOUCH_SENSITIVITY)
this.drag.last = world
const base = this.target ?? this.options.getShipPosition().clone()
this.target = this.options.clampTarget(base.add(delta))
}
private handlePointerUp = (event: PointerEvent) => {
if (this.drag?.pointerId === event.pointerId) this.drag = null
}
}