-
Notifications
You must be signed in to change notification settings - Fork 8
/
system.go
270 lines (249 loc) · 8.35 KB
/
system.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
package input
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
// System is the main component of the input library.
//
// You usually need only one input system object.
//
// Store System object (by value) inside your game context/state object like this:
//
// struct GameState {
// InputSystem input.System
// }
//
// When ebitengine game is executed, call gameState.InputSystem.Init() once.
//
// On every ebitengine Update() call, use gameState.InputSystem.Update().
//
// The system is usually not used directly after the input handlers are created.
// Use input handlers to handle the user input.
type System struct {
gamepadIDs []ebiten.GamepadID
gamepadInfo []gamepadInfo
// This is a scratch slice for ebiten.AppendPressedKeys operation.
keySlice []ebiten.Key
gamepadKeySlice []ebiten.GamepadButton
pendingEvents []simulatedEvent
prevSimulatedEvents []simulatedEvent
simulatedEvents []simulatedEvent
hasSimulatedActions bool
touchEnabled bool
touchHasTap bool
touchHasLongTap bool
touchJustHadDrag bool
touchHasDrag bool
touchDragging bool
touchIDs []ebiten.TouchID // This is a scratch slice, we don't support multi-touches yet
touchActiveID ebiten.TouchID
touchTapPos Vec
touchDragPos Vec
touchStartPos Vec
touchTime float64
mouseEnabled bool
mouseHasDrag bool // For "drag" event
mouseDragging bool // For "drag" event
mouseJustHadDrag bool // For "drag" event
mouseJustReleasedDrag bool // For "drag" event
mousePressed bool // For "drag" event
mouseStartPos Vec // For "drag" event
mouseDragPos Vec // For "drag" event
cursorPos Vec
wheel Vec
}
// SystemConfig configures the input system.
// This configuration can't be changed once created.
type SystemConfig struct {
// DevicesEnabled selects the input devices that should be handled.
// For the most cases, AnyDevice value is a good option.
DevicesEnabled DeviceKind
}
func (sys *System) Init(config SystemConfig) {
sys.keySlice = make([]ebiten.Key, 0, 4)
sys.gamepadKeySlice = make([]ebiten.GamepadButton, 0, 2)
sys.touchEnabled = config.DevicesEnabled&TouchDevice != 0
sys.mouseEnabled = config.DevicesEnabled&MouseDevice != 0
sys.gamepadIDs = make([]ebiten.GamepadID, 0, 8)
sys.gamepadInfo = make([]gamepadInfo, 8)
if sys.touchEnabled {
sys.touchIDs = make([]ebiten.TouchID, 0, 8)
sys.touchActiveID = -1
}
}
// UpdateWithDelta is like Update(), but it allows you to specify the time delta.
func (sys *System) UpdateWithDelta(delta float64) {
// Rotate the events slices.
// Pending events become simulated in this frame.
// Re-use the other slice capacity to push new events.
// prev simulated <- simulated
// pending <- prev simulated
// simulated <- pending
sys.prevSimulatedEvents, sys.pendingEvents, sys.simulatedEvents =
sys.simulatedEvents, sys.prevSimulatedEvents, sys.pendingEvents
sys.pendingEvents = sys.pendingEvents[:0]
sys.hasSimulatedActions = false
for i := range sys.simulatedEvents {
if sys.simulatedEvents[i].keyKind == keySimulated {
sys.hasSimulatedActions = true
break
}
}
sys.gamepadIDs = ebiten.AppendGamepadIDs(sys.gamepadIDs[:0])
if len(sys.gamepadIDs) != 0 {
for i, id := range sys.gamepadIDs {
info := &sys.gamepadInfo[i]
info.axisCount = ebiten.GamepadAxisCount(id)
modelName := ebiten.GamepadName(id)
if info.modelName != modelName {
info.modelName = modelName
switch {
case ebiten.IsStandardGamepadLayoutAvailable(id):
info.model = gamepadStandard
case isFirefox():
info.model = guessFirefoxGamepadModel(int(id))
default:
info.model = guessGamepadModel(modelName)
}
}
sys.updateGamepadInfo(id, info)
}
}
if sys.touchEnabled {
sys.touchHasTap = false
sys.touchHasLongTap = false
sys.touchHasDrag = false
sys.touchJustHadDrag = false
// Track the touch gesture release.
// If it was a tap, set a flag.
if sys.touchActiveID != -1 && inpututil.IsTouchJustReleased(sys.touchActiveID) {
if !sys.touchDragging {
if sys.touchTime >= 0.5 {
sys.touchHasLongTap = true
} else {
sys.touchHasTap = true
}
sys.touchTapPos = sys.touchStartPos
}
sys.touchActiveID = -1
sys.touchDragging = false
}
// Check if this gesture entered a drag mode.
// Drag mode gestures will not trigger a tap when released.
// Drag events emit a pos delta relative to a start pos every frame.
if sys.touchActiveID != -1 {
x, y := ebiten.TouchPosition(sys.touchActiveID)
currentPos := Vec{X: float64(x), Y: float64(y)}
if sys.touchDragging {
sys.touchHasDrag = true
sys.touchDragPos = currentPos
} else {
sys.touchTime += delta
if vecDistance(sys.touchStartPos, currentPos) > 5 {
sys.touchDragging = true
sys.touchJustHadDrag = true
sys.touchHasDrag = true
sys.touchDragPos = currentPos
}
}
}
// Check if a new touch gesture is started.
if sys.touchActiveID == -1 {
sys.touchIDs = inpututil.AppendJustPressedTouchIDs(sys.touchIDs[:0])
for _, id := range sys.touchIDs {
x, y := ebiten.TouchPosition(id)
sys.touchStartPos = Vec{X: float64(x), Y: float64(y)}
sys.touchActiveID = id
sys.touchTime = 0
break
}
}
}
if sys.mouseEnabled {
x, y := ebiten.CursorPosition()
sys.cursorPos = Vec{X: float64(x), Y: float64(y)}
// We copy a lot from the touch-style drag gesture.
// This is not mandatory as getting a cursor pos is much easier on PC.
// But I do value the consistency and easier cross-platform coding,
// so let's try to make them behave as close to each other as feasible.
sys.mouseHasDrag = false
sys.mouseJustHadDrag = false
sys.mouseJustReleasedDrag = false
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
if sys.mouseDragging {
sys.mouseJustReleasedDrag = true
}
sys.mouseDragging = false
sys.mousePressed = false
}
if sys.mousePressed {
if sys.mouseDragging {
sys.mouseHasDrag = true
sys.mouseDragPos = sys.cursorPos
} else {
// Mouse pointer is more precise than a finger gesture,
// therefore we can have a lower threshold here.
if vecDistance(sys.mouseStartPos, sys.cursorPos) > 1 {
sys.mouseDragging = true
sys.mouseJustHadDrag = true
sys.mouseHasDrag = true
sys.mouseDragPos = sys.cursorPos
}
}
}
if !sys.mousePressed && inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
sys.mouseStartPos = sys.cursorPos
sys.mousePressed = true
}
}
if sys.mouseEnabled || sys.touchEnabled {
x, y := ebiten.Wheel()
sys.wheel = Vec{X: x, Y: y}
}
}
// Update reads the input state and updates the information
// available to all input handlers.
// Generally, you call this method from your ebiten.Game.Update() method.
//
// Since ebitengine uses a fixed timestep architecture,
// a time delta of 1.0/60.0 is implied.
// If you need a control over that, use UpdateWithDelta() instead.
//
// The time delta mostly needed for things like press gesture
// detection: we need to calculate when a tap becomes a [long] press.
func (sys *System) Update() {
sys.UpdateWithDelta(1.0 / 60.0)
}
func (sys *System) updateGamepadInfo(id ebiten.GamepadID, info *gamepadInfo) {
switch info.model {
case gamepadStandard:
copy(info.prevAxisValues[:], info.axisValues[:])
for axis := ebiten.StandardGamepadAxisLeftStickHorizontal; axis <= ebiten.StandardGamepadAxisMax; axis++ {
v := ebiten.StandardGamepadAxisValue(id, axis)
info.axisValues[int(axis)] = v
}
case gamepadFirefoxXinput:
copy(info.prevAxisValues[:], info.axisValues[:])
for axis := 0; axis < info.axisCount; axis++ {
v := ebiten.GamepadAxisValue(id, axis)
info.axisValues[axis] = v
}
}
}
// NewHandler creates a handler associated with player/device ID.
// IDs should start with 0 with a step of 1.
// So, NewHandler(0, ...) then NewHandler(1, ...).
//
// If you want to configure the handler further, use Handler fields/methods
// to do that. For example, see Handler.GamepadDeadzone.
func (sys *System) NewHandler(playerID uint8, keymap Keymap) *Handler {
return &Handler{
id: playerID,
keymap: keymap,
sys: sys,
// My gamepads may have false positive activations with a
// value lower than 0.03; we're using 0.055 here just to be safe.
// Various sources indicate that a value of ~0.05 is optimal for a default.
GamepadDeadzone: 0.055,
}
}