Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add types to keyboard controls #1117

Merged
merged 2 commits into from Oct 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 72 additions & 0 deletions .storybook/stories/KeyboardControls.stories.tsx
@@ -0,0 +1,72 @@
import { useFrame } from '@react-three/fiber'
import * as React from 'react'
import { useMemo, useRef } from 'react'
import { MathUtils, Mesh, Vector3 } from 'three'
import { Cone, KeyboardControls, KeyboardControlsEntry, useKeyboardControls } from '../../src'
import { Setup } from '../Setup'

export default {
title: 'Controls/KeyboardControls',
decorators: [
(storyFn) => (
<Setup cameraPosition={new Vector3(0, 10, 0)} lights={true}>
{storyFn()}
</Setup>
),
],
}

enum Controls {
forward = 'forward',
left = 'left',
right = 'right',
back = 'back',
}

export const KeyboardControlsSt = () => {
const map = useMemo<KeyboardControlsEntry[]>(
() => [
{ name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] },
{ name: Controls.back, keys: ['ArrowDown', 's', 'S'] },
{ name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] },
{ name: Controls.right, keys: ['ArrowRight', 'd', 'D'] },
],
[]
)

return (
<KeyboardControls map={map}>
<Player />
</KeyboardControls>
)
}

const _velocity = new Vector3()
const speed = 10

const Player = () => {
const ref = useRef<Mesh>(null)
const [, get] = useKeyboardControls<Controls>()

useFrame((_s, dl) => {
if (!ref.current) return
const state = get()
if (state.left && !state.right) _velocity.x = -1
if (state.right && !state.left) _velocity.x = 1
if (!state.left && !state.right) _velocity.x = 0

if (state.forward && !state.back) _velocity.z = -1
if (state.back && !state.forward) _velocity.z = 1
if (!state.forward && !state.back) _velocity.z = 0

ref.current.position.addScaledVector(_velocity, speed * dl)

ref.current.rotateY(4 * dl * _velocity.x)
})

return (
<Cone ref={ref} args={[1, 3, 4]} rotation={[-90 * MathUtils.DEG2RAD, 0, 0]}>
<meshLambertMaterial color="green" />
</Cone>
)
}
41 changes: 24 additions & 17 deletions README.md
Expand Up @@ -436,11 +436,11 @@ Semi-OrbitControls with spring-physics, polar zoom and snap-back, for presentati
A rudimentary keyboard controller which distributes your defined data-model to the `useKeyboard` hook. It's a rather simple way to get started with keyboard input.

```tsx
type KeyboardControlsState = { [key: string]: boolean }
type KeyboardControlsState<T extends string = string> = { [K in T]: boolean }

type KeyboardControlsEntry = {
type KeyboardControlsEntry<T extends string = string> = {
/** Name of the action */
name: string
name: T
/** The keys that define it, you can use either event.key, or event.code */
keys: string[]
/** If the event receives the keyup event, true by default */
Expand All @@ -461,33 +461,40 @@ type KeyboardControlsProps = {

You start by wrapping your app, or scene, into `<KeyboardControls>`.

```jsx
```tsx
enum Controls {
forward = 'forward',
back = 'back',
left = 'left',
right = 'right',
jump = 'jump',
}
function App() {
const map = useMemo<KeyboardControlsEntry<Controls>>(()=>[
{ name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] },
{ name: Controls.back, keys: ['ArrowDown', 's', 'S'] },
{ name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] },
{ name: Controls.right, keys: ['ArrowRight', 'd', 'D'] },
{ name: Controls.jump, keys: ['Space'] },
], [])
return (
<KeyboardControls
map={[
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
{ name: 'backward', keys: ['ArrowDown', 's', 'S'] },
{ name: 'leftward', keys: ['ArrowLeft', 'a', 'A'] },
{ name: 'rightward', keys: ['ArrowRight', 'd', 'D'] },
{ name: 'jump', keys: ['Space'] },
]}>
<KeyboardControls map={map}>
<App />
</KeyboardControls>
```

You can either respond to input reactively, it uses zustand (with the `subscribeWithSelector` middleware) so all the rules apply:

```jsx
```tsx
function Foo() {
const pressed = useKeyboardControls(state => forward)
const forwardPressed = useKeyboardControls<Controls>(state => state.forward)
```

Or transiently, either by `subscribe`, which is a function which returns a function to unsubscribe, so you can pair it with useEffect for cleanup, or `get`, which fetches fresh state non-reactively.

```jsx
```tsx
function Foo() {
const [sub, get] = useKeyboardControls()
const [sub, get] = useKeyboardControls<Controls>()

useEffect(() => {
return sub(
Expand All @@ -500,7 +507,7 @@ function Foo() {

useFrame(() => {
// Fetch fresh data from store
const pressed = get().backward
const pressed = get().back
})
}
```
Expand Down
27 changes: 18 additions & 9 deletions src/web/KeyboardControls.tsx
Expand Up @@ -2,11 +2,11 @@ import * as React from 'react'
import create, { GetState, StateSelector, Subscribe, UseBoundStore } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

type KeyboardControlsState = { [key: string]: boolean }
type KeyboardControlsState<T extends string = string> = { [K in T]: boolean }

type KeyboardControlsEntry = {
export type KeyboardControlsEntry<T extends string = string> = {
/** Name of the action */
name: string
name: T
/** The keys that define it, you can use either event.key, or event.code */
keys: string[]
/** If the event receives the keyup event, true by default */
Expand All @@ -24,10 +24,10 @@ type KeyboardControlsProps = {
domElement?: HTMLElement
}

type KeyboardControlsApi = [
Subscribe<KeyboardControlsState>,
GetState<KeyboardControlsState>,
UseBoundStore<KeyboardControlsState>
type KeyboardControlsApi<T extends string = string> = [
Subscribe<KeyboardControlsState<T>>,
GetState<KeyboardControlsState<T>>,
UseBoundStore<KeyboardControlsState<T>>
]

const context = /*@__PURE__*/ React.createContext<KeyboardControlsApi>(null!)
Expand Down Expand Up @@ -90,8 +90,17 @@ export function KeyboardControls({ map, children, onChange, domElement }: Keyboa
return <context.Provider value={api} children={children} />
}

export function useKeyboardControls(sel?: StateSelector<KeyboardControlsState, any>) {
const [sub, get, store] = React.useContext(context)
export function useKeyboardControls<T extends string = string, U = any>(): [
Subscribe<KeyboardControlsState<T>>,
GetState<KeyboardControlsState<T>>
]
export function useKeyboardControls<T extends string = string, U = any>(
sel: StateSelector<KeyboardControlsState<T>, U>
): U
export function useKeyboardControls<T extends string = string, U = any>(
sel?: StateSelector<KeyboardControlsState<T>, U>
): U | [Subscribe<KeyboardControlsState<T>>, GetState<KeyboardControlsState<T>>] {
const [sub, get, store] = React.useContext<KeyboardControlsApi<T>>(context)
if (sel) return store(sel)
else return [sub, get]
}