Skip to content

Commit

Permalink
feat: add types to keyboard controls (#1117)
Browse files Browse the repository at this point in the history
* feat: add types to keyboard controls

* docs: keyboard controls update
  • Loading branch information
RodrigoHamuy committed Oct 29, 2022
1 parent 9033468 commit d60c941
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 26 deletions.
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]
}

1 comment on commit d60c941

@vercel
Copy link

@vercel vercel bot commented on d60c941 Oct 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.