// Copyright 2018 The Ebiten Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package inpututil provides utility functions of input like keyboard or mouse. package inpututil import ( "sort" "sync" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/internal/hooks" ) type inputState struct { keyDurations []int prevKeyDurations []int mouseButtonDurations map[ebiten.MouseButton]int prevMouseButtonDurations map[ebiten.MouseButton]int gamepadIDs map[ebiten.GamepadID]struct{} prevGamepadIDs map[ebiten.GamepadID]struct{} gamepadButtonDurations map[ebiten.GamepadID][]int prevGamepadButtonDurations map[ebiten.GamepadID][]int standardGamepadButtonDurations map[ebiten.GamepadID][]int prevStandardGamepadButtonDurations map[ebiten.GamepadID][]int touchIDs map[ebiten.TouchID]struct{} touchDurations map[ebiten.TouchID]int prevTouchDurations map[ebiten.TouchID]int gamepadIDsBuf []ebiten.GamepadID touchIDsBuf []ebiten.TouchID m sync.RWMutex } var theInputState = &inputState{ keyDurations: make([]int, ebiten.KeyMax+1), prevKeyDurations: make([]int, ebiten.KeyMax+1), mouseButtonDurations: map[ebiten.MouseButton]int{}, prevMouseButtonDurations: map[ebiten.MouseButton]int{}, gamepadIDs: map[ebiten.GamepadID]struct{}{}, prevGamepadIDs: map[ebiten.GamepadID]struct{}{}, gamepadButtonDurations: map[ebiten.GamepadID][]int{}, prevGamepadButtonDurations: map[ebiten.GamepadID][]int{}, standardGamepadButtonDurations: map[ebiten.GamepadID][]int{}, prevStandardGamepadButtonDurations: map[ebiten.GamepadID][]int{}, touchIDs: map[ebiten.TouchID]struct{}{}, touchDurations: map[ebiten.TouchID]int{}, prevTouchDurations: map[ebiten.TouchID]int{}, } func init() { hooks.AppendHookOnBeforeUpdate(func() error { theInputState.update() return nil }) } func (i *inputState) update() { i.m.Lock() defer i.m.Unlock() // Keyboard copy(i.prevKeyDurations[:], i.keyDurations[:]) for k := ebiten.Key(0); k <= ebiten.KeyMax; k++ { if ebiten.IsKeyPressed(k) { i.keyDurations[k]++ } else { i.keyDurations[k] = 0 } } // Mouse for _, b := range []ebiten.MouseButton{ ebiten.MouseButtonLeft, ebiten.MouseButtonRight, ebiten.MouseButtonMiddle, } { i.prevMouseButtonDurations[b] = i.mouseButtonDurations[b] if ebiten.IsMouseButtonPressed(b) { i.mouseButtonDurations[b]++ } else { i.mouseButtonDurations[b] = 0 } } // Gamepads // Copy the gamepad IDs. for id := range i.prevGamepadIDs { delete(i.prevGamepadIDs, id) } for id := range i.gamepadIDs { i.prevGamepadIDs[id] = struct{}{} } // Copy the gamepad button durations. for id := range i.prevGamepadButtonDurations { delete(i.prevGamepadButtonDurations, id) } for id, ds := range i.gamepadButtonDurations { i.prevGamepadButtonDurations[id] = append([]int{}, ds...) } for id := range i.prevStandardGamepadButtonDurations { delete(i.prevStandardGamepadButtonDurations, id) } for id, ds := range i.standardGamepadButtonDurations { i.prevStandardGamepadButtonDurations[id] = append([]int{}, ds...) } for id := range i.gamepadIDs { delete(i.gamepadIDs, id) } i.gamepadIDsBuf = ebiten.AppendGamepadIDs(i.gamepadIDsBuf[:0]) for _, id := range i.gamepadIDsBuf { i.gamepadIDs[id] = struct{}{} if _, ok := i.gamepadButtonDurations[id]; !ok { i.gamepadButtonDurations[id] = make([]int, ebiten.GamepadButtonMax+1) } n := ebiten.GamepadButtonNum(id) for b := ebiten.GamepadButton(0); b < ebiten.GamepadButton(n); b++ { if ebiten.IsGamepadButtonPressed(id, b) { i.gamepadButtonDurations[id][b]++ } else { i.gamepadButtonDurations[id][b] = 0 } } if _, ok := i.standardGamepadButtonDurations[id]; !ok { i.standardGamepadButtonDurations[id] = make([]int, ebiten.StandardGamepadButtonMax+1) } for b := ebiten.StandardGamepadButton(0); b <= ebiten.StandardGamepadButtonMax; b++ { if ebiten.IsStandardGamepadButtonPressed(id, b) { i.standardGamepadButtonDurations[id][b]++ } else { i.standardGamepadButtonDurations[id][b] = 0 } } } for id := range i.gamepadButtonDurations { if _, ok := i.gamepadIDs[id]; !ok { delete(i.gamepadButtonDurations, id) } } for id := range i.standardGamepadButtonDurations { if _, ok := i.gamepadIDs[id]; !ok { delete(i.standardGamepadButtonDurations, id) } } // Touches // Copy the touch durations. for id := range i.prevTouchDurations { delete(i.prevTouchDurations, id) } for id := range i.touchDurations { i.prevTouchDurations[id] = i.touchDurations[id] } for id := range i.touchIDs { delete(i.touchIDs, id) } i.touchIDsBuf = ebiten.AppendTouchIDs(i.touchIDsBuf[:0]) for _, id := range i.touchIDsBuf { i.touchIDs[id] = struct{}{} i.touchDurations[id]++ } for id := range i.touchDurations { if _, ok := i.touchIDs[id]; !ok { delete(i.touchDurations, id) } } } // AppendPressedKeys append currently pressed keyboard keys to keys and returns the extended buffer. // Giving a slice that already has enough capacity works efficiently. // // AppendPressedKeys is concurrent safe. func AppendPressedKeys(keys []ebiten.Key) []ebiten.Key { theInputState.m.RLock() defer theInputState.m.RUnlock() for i, d := range theInputState.keyDurations { if d == 0 { continue } keys = append(keys, ebiten.Key(i)) } return keys } // PressedKeys returns a set of currently pressed keyboard keys. // // Deprecated: as of v2.2. Use AppendPressedKeys instead. func PressedKeys() []ebiten.Key { return AppendPressedKeys(nil) } // IsKeyJustPressed returns a boolean value indicating // whether the given key is pressed just in the current frame. // // IsKeyJustPressed is concurrent safe. func IsKeyJustPressed(key ebiten.Key) bool { return KeyPressDuration(key) == 1 } // IsKeyJustReleased returns a boolean value indicating // whether the given key is released just in the current frame. // // IsKeyJustReleased is concurrent safe. func IsKeyJustReleased(key ebiten.Key) bool { theInputState.m.RLock() r := theInputState.keyDurations[key] == 0 && theInputState.prevKeyDurations[key] > 0 theInputState.m.RUnlock() return r } // KeyPressDuration returns how long the key is pressed in frames. // // KeyPressDuration is concurrent safe. func KeyPressDuration(key ebiten.Key) int { theInputState.m.RLock() s := theInputState.keyDurations[key] theInputState.m.RUnlock() return s } // IsMouseButtonJustPressed returns a boolean value indicating // whether the given mouse button is pressed just in the current frame. // // IsMouseButtonJustPressed is concurrent safe. func IsMouseButtonJustPressed(button ebiten.MouseButton) bool { return MouseButtonPressDuration(button) == 1 } // IsMouseButtonJustReleased returns a boolean value indicating // whether the given mouse button is released just in the current frame. // // IsMouseButtonJustReleased is concurrent safe. func IsMouseButtonJustReleased(button ebiten.MouseButton) bool { theInputState.m.RLock() r := theInputState.mouseButtonDurations[button] == 0 && theInputState.prevMouseButtonDurations[button] > 0 theInputState.m.RUnlock() return r } // MouseButtonPressDuration returns how long the mouse button is pressed in frames. // // MouseButtonPressDuration is concurrent safe. func MouseButtonPressDuration(button ebiten.MouseButton) int { theInputState.m.RLock() s := theInputState.mouseButtonDurations[button] theInputState.m.RUnlock() return s } // AppendJustConnectedGamepadIDs appends gamepad IDs that are connected just in the current frame to gamepadIDs, // and returns the extended buffer. // Giving a slice that already has enough capacity works efficiently. // // AppendJustConnectedGamepadIDs is concurrent safe. func AppendJustConnectedGamepadIDs(gamepadIDs []ebiten.GamepadID) []ebiten.GamepadID { origLen := len(gamepadIDs) theInputState.m.RLock() for id := range theInputState.gamepadIDs { if _, ok := theInputState.prevGamepadIDs[id]; !ok { gamepadIDs = append(gamepadIDs, id) } } theInputState.m.RUnlock() s := gamepadIDs[origLen:] sort.Slice(s, func(a, b int) bool { return s[a] < s[b] }) return gamepadIDs } // JustConnectedGamepadIDs returns gamepad IDs that are connected just in the current frame. // // Deprecated: as of v2.2. Use AppendJustConnectedGamepadIDs instead. func JustConnectedGamepadIDs() []ebiten.GamepadID { return AppendJustConnectedGamepadIDs(nil) } // IsGamepadJustDisconnected returns a boolean value indicating // whether the gamepad of the given id is released just in the current frame. // // IsGamepadJustDisconnected is concurrent safe. func IsGamepadJustDisconnected(id ebiten.GamepadID) bool { theInputState.m.RLock() _, prev := theInputState.prevGamepadIDs[id] _, current := theInputState.gamepadIDs[id] theInputState.m.RUnlock() return prev && !current } // IsGamepadButtonJustPressed returns a boolean value indicating // whether the given gamepad button of the gamepad id is pressed just in the current frame. // // IsGamepadButtonJustPressed is concurrent safe. func IsGamepadButtonJustPressed(id ebiten.GamepadID, button ebiten.GamepadButton) bool { return GamepadButtonPressDuration(id, button) == 1 } // IsGamepadButtonJustReleased returns a boolean value indicating // whether the given gamepad button of the gamepad id is released just in the current frame. // // IsGamepadButtonJustReleased is concurrent safe. func IsGamepadButtonJustReleased(id ebiten.GamepadID, button ebiten.GamepadButton) bool { theInputState.m.RLock() prev := 0 if _, ok := theInputState.prevGamepadButtonDurations[id]; ok { prev = theInputState.prevGamepadButtonDurations[id][button] } current := 0 if _, ok := theInputState.gamepadButtonDurations[id]; ok { current = theInputState.gamepadButtonDurations[id][button] } theInputState.m.RUnlock() return current == 0 && prev > 0 } // GamepadButtonPressDuration returns how long the gamepad button of the gamepad id is pressed in frames. // // GamepadButtonPressDuration is concurrent safe. func GamepadButtonPressDuration(id ebiten.GamepadID, button ebiten.GamepadButton) int { theInputState.m.RLock() s := 0 if _, ok := theInputState.gamepadButtonDurations[id]; ok { s = theInputState.gamepadButtonDurations[id][button] } theInputState.m.RUnlock() return s } // IsStandardGamepadButtonJustPressed returns a boolean value indicating // whether the given standard gamepad button of the gamepad id is pressed just in the current frame. // // IsStandardGamepadButtonJustPressed is concurrent safe. func IsStandardGamepadButtonJustPressed(id ebiten.GamepadID, button ebiten.StandardGamepadButton) bool { return StandardGamepadButtonPressDuration(id, button) == 1 } // IsStandardGamepadButtonJustReleased returns a boolean value indicating // whether the given standard gamepad button of the gamepad id is released just in the current frame. // // IsStandardGamepadButtonJustReleased is concurrent safe. func IsStandardGamepadButtonJustReleased(id ebiten.GamepadID, button ebiten.StandardGamepadButton) bool { theInputState.m.RLock() defer theInputState.m.RUnlock() var prev int if _, ok := theInputState.prevStandardGamepadButtonDurations[id]; ok { prev = theInputState.prevStandardGamepadButtonDurations[id][button] } var current int if _, ok := theInputState.standardGamepadButtonDurations[id]; ok { current = theInputState.standardGamepadButtonDurations[id][button] } return current == 0 && prev > 0 } // StandardGamepadButtonPressDuration returns how long the standard gamepad button of the gamepad id is pressed in frames. // // StandardGamepadButtonPressDuration is concurrent safe. func StandardGamepadButtonPressDuration(id ebiten.GamepadID, button ebiten.StandardGamepadButton) int { theInputState.m.RLock() defer theInputState.m.RUnlock() if _, ok := theInputState.standardGamepadButtonDurations[id]; ok { return theInputState.standardGamepadButtonDurations[id][button] } return 0 } // AppendJustPressedTouchIDs append touch IDs that are created just in the current frame to touchIDs, // and returns the extended buffer. // Giving a slice that already has enough capacity works efficiently. // // AppendJustPressedTouchIDs is concurrent safe. func AppendJustPressedTouchIDs(touchIDs []ebiten.TouchID) []ebiten.TouchID { origLen := len(touchIDs) theInputState.m.RLock() for id, s := range theInputState.touchDurations { if s == 1 { touchIDs = append(touchIDs, id) } } theInputState.m.RUnlock() s := touchIDs[origLen:] sort.Slice(s, func(a, b int) bool { return s[a] < s[b] }) return touchIDs } // JustPressedTouchIDs returns touch IDs that are created just in the current frame. // // Deprecated: as of v2.2. Use AppendJustPressedTouchIDs instead. func JustPressedTouchIDs() []ebiten.TouchID { return AppendJustPressedTouchIDs(nil) } // IsTouchJustReleased returns a boolean value indicating // whether the given touch is released just in the current frame. // // IsTouchJustReleased is concurrent safe. func IsTouchJustReleased(id ebiten.TouchID) bool { theInputState.m.RLock() r := theInputState.touchDurations[id] == 0 && theInputState.prevTouchDurations[id] > 0 theInputState.m.RUnlock() return r } // TouchPressDuration returns how long the touch remains in frames. // // TouchPressDuration is concurrent safe. func TouchPressDuration(id ebiten.TouchID) int { theInputState.m.RLock() s := theInputState.touchDurations[id] theInputState.m.RUnlock() return s }