Skip to content

Commit 75e9299

Browse files
committed
Add game controller support: gamepad and keyboard
This commit adds support for a virtual game controller. The game controller can be a real gamepad or a keyboard. Up to 8 game controllers are supported. Keyboard is used for players 0 and 1. Gamepads are used for all players.
1 parent 090b7a9 commit 75e9299

File tree

6 files changed

+386
-4
lines changed

6 files changed

+386
-4
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ See also [examples](examples) directory and [documentation](https://pkg.go.dev/g
6464
* [x] Cos, Sin, Atan2
6565
* [ ] Min, Max, Mid
6666
* [ ] stretching sprites
67-
* [ ] Add keyboard support
68-
* [ ] Add gamepad/joystick support
69-
* [ ] Add mouse support
70-
* [ ] Add Map API
67+
* [x] Game controller support: gamepad and keyboard
68+
* [ ] Mouse support (dev mode)
69+
* [ ] Full keyboard support (dev mode)
70+
* [ ] Map API
71+
* [ ] Menu screen
7172
* [ ] Development console
7273
* [ ] stopping, resuming the game
7374
* [x] add a programmatic way to stop the game

controller.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// (c) 2022 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package pi
5+
6+
import (
7+
"github.com/hajimehoshi/ebiten/v2"
8+
)
9+
10+
// Button is a virtual button on any game controller. The game controller can be a gamepad or a keyboard.
11+
//
12+
// Button is used by Btn, Btnp, BtnPlayer, BtnpPlayer, BtnBits and BtnpBits.
13+
type Button int
14+
15+
// Keyboard mappings:
16+
//
17+
// player 0: [DPAD] - cursors [O] - Z C N [X] - X V M
18+
// player 1: [DPAD] - SFED [O] - LSHIFT [X] - TAB W Q A
19+
//
20+
// First connected gamepad controller is player 0, second player 1 and so on.
21+
// On XBox controller [O] is A and Y, [X] is B and X.
22+
const (
23+
Left Button = 0
24+
Right Button = 1
25+
Up Button = 2
26+
Down Button = 3
27+
O Button = 4 // O is a first fire button
28+
X Button = 5 // X is a second fire button
29+
)
30+
31+
// Btn returns true if a button is being pressed at this moment by player 0.
32+
func Btn(button Button) bool {
33+
return BtnPlayer(button, 0)
34+
}
35+
36+
// BtnPlayer returns true if a button is being pressed at this moment
37+
// by specific player. The player can be 0..7.
38+
func BtnPlayer(button Button, player int) bool {
39+
return isPressed(buttonDuration(player, button))
40+
}
41+
42+
// Btnp returns true when the button has just been pressed.
43+
// It also returns true after the next 15 frames, and then every 4 frames.
44+
// This simulates keyboard-like repeating.
45+
func Btnp(button Button) bool {
46+
return BtnpPlayer(button, 0)
47+
}
48+
49+
// BtnpPlayer returns true when the button has just been pressed.
50+
// It also returns true after the next 15 frames, and then every 4 frames.
51+
// This simulates keyboard-like repeating. The player can be 0..7.
52+
func BtnpPlayer(button Button, player int) bool {
53+
return isPressedRepeatably(buttonDuration(player, button))
54+
}
55+
56+
// BtnBits returns the state of all buttons for players 0 and 1 as bitset.
57+
//
58+
// The first byte contains the button states for player 0 (bits 0 through 5, bits 6 and 7 are unused).
59+
// The second byte contains the button states for player 1 (bits 8 through 13).
60+
//
61+
// Bit 0 is Left, 1 is Right, bit 5 is the X button.
62+
//
63+
// A bit of 1 means the button is pressed.
64+
func BtnBits() int {
65+
return controllers[0].bits(isPressed) + controllers[1].bits(isPressed)<<8
66+
}
67+
68+
// BtnpBits returns the state of all buttons for players 0 and 1 as bitset.
69+
//
70+
// The first byte contains the button states for player 0 (bits 0 through 5, bits 6 and 7 are unused).
71+
// The second byte contains the button states for player 1 (bits 8 through 13).
72+
//
73+
// Bit 0 is Left, 1 is Right, bit 5 is the X button.
74+
//
75+
// A bit of 1 means the button has just been pressed.
76+
func BtnpBits() int {
77+
return controllers[0].bits(isPressedRepeatably) + controllers[1].bits(isPressedRepeatably)<<8
78+
}
79+
80+
func buttonDuration(player int, button Button) int {
81+
if button < Left || button > X {
82+
return 0
83+
}
84+
85+
if player < 0 || player > 7 {
86+
return 0
87+
}
88+
89+
return controllers[player].buttonDuration[button]
90+
}
91+
92+
func isPressed(duration int) bool {
93+
return duration > 0
94+
}
95+
96+
func isPressedRepeatably(duration int) bool {
97+
const (
98+
pressDuration = 15 // make it configurable
99+
pressInterval = 4 // make it configurable
100+
)
101+
102+
if duration == 1 {
103+
return true
104+
}
105+
106+
return duration >= pressDuration+1 && duration%pressInterval == 0
107+
}
108+
109+
func updateController() {
110+
for player := 0; player < 8; player++ {
111+
controllers[player].update(player)
112+
}
113+
}
114+
115+
var controllers [8]controller
116+
117+
type controller struct {
118+
buttonDuration [6]int // left, right, up, down, o, x
119+
}
120+
121+
func (c *controller) update(player int) {
122+
c.updateDirections(player)
123+
c.updateFireButtons(player)
124+
}
125+
126+
func (c *controller) updateDirections(player int) {
127+
gamepadID := ebiten.GamepadID(player)
128+
129+
axisX := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisLeftStickHorizontal)
130+
axisY := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisLeftStickVertical)
131+
132+
if axisX < -0.5 ||
133+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonLeftLeft) ||
134+
isKeyboardPressed(player, Left) {
135+
c.buttonDuration[Left] += 1
136+
c.buttonDuration[Right] = 0
137+
} else if axisX > 0.5 ||
138+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonLeftRight) ||
139+
isKeyboardPressed(player, Right) {
140+
c.buttonDuration[Right] += 1
141+
c.buttonDuration[Left] = 0
142+
} else {
143+
c.buttonDuration[Right] = 0
144+
c.buttonDuration[Left] = 0
145+
}
146+
147+
if axisY < -0.5 ||
148+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonLeftTop) ||
149+
isKeyboardPressed(player, Up) {
150+
c.buttonDuration[Up] += 1
151+
c.buttonDuration[Down] = 0
152+
} else if axisY > 0.5 ||
153+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonLeftBottom) ||
154+
isKeyboardPressed(player, Down) {
155+
c.buttonDuration[Down] += 1
156+
c.buttonDuration[Up] = 0
157+
} else {
158+
c.buttonDuration[Up] = 0
159+
c.buttonDuration[Down] = 0
160+
}
161+
}
162+
163+
func (c *controller) updateFireButtons(player int) {
164+
gamepadID := ebiten.GamepadID(player)
165+
166+
if ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonRightBottom) ||
167+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonRightTop) ||
168+
isKeyboardPressed(player, O) {
169+
c.buttonDuration[O] += 1
170+
} else {
171+
c.buttonDuration[O] = 0
172+
}
173+
174+
if ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonRightRight) ||
175+
ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonRightLeft) ||
176+
isKeyboardPressed(player, X) {
177+
c.buttonDuration[X] += 1
178+
} else {
179+
c.buttonDuration[X] = 0
180+
}
181+
}
182+
183+
func (c *controller) bits(isSet func(int) bool) int {
184+
var b int
185+
for i := 0; i <= int(X); i++ {
186+
if isSet(c.buttonDuration[i]) {
187+
b += 1 << i
188+
}
189+
}
190+
return b
191+
}
192+
193+
// first array is player, then π key, then slice of Ebitengine keys.
194+
var keyboardMapping = [...][6][]ebiten.Key{
195+
// player0:
196+
{
197+
{ebiten.KeyLeft}, // left
198+
{ebiten.KeyRight}, // right
199+
{ebiten.KeyUp}, // up
200+
{ebiten.KeyDown}, // down
201+
{ebiten.KeyZ, ebiten.KeyC, ebiten.KeyN}, // o
202+
{ebiten.KeyX, ebiten.KeyV, ebiten.KeyM}, // x
203+
},
204+
// player1:
205+
{
206+
{ebiten.KeyS}, // left
207+
{ebiten.KeyF}, // right
208+
{ebiten.KeyE}, // up
209+
{ebiten.KeyD}, // down
210+
{ebiten.KeyShiftLeft}, // o
211+
{ebiten.KeyTab, ebiten.KeyW, ebiten.KeyQ, ebiten.KeyA}, // x
212+
},
213+
}
214+
215+
func isKeyboardPressed(player int, button Button) bool {
216+
if player >= len(keyboardMapping) {
217+
return false
218+
}
219+
220+
keys := keyboardMapping[player][button]
221+
for _, k := range keys {
222+
if ebiten.IsKeyPressed(k) {
223+
return true
224+
}
225+
}
226+
227+
return false
228+
}

controller_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// (c) 2022 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package pi_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/elgopher/pi"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
var allButtons = []pi.Button{pi.Left, pi.Right, pi.Up, pi.Down, pi.O, pi.X}
14+
15+
func TestBtn(t *testing.T) {
16+
testBtn(t, pi.Btn)
17+
}
18+
19+
func testBtn(t *testing.T, btn func(pi.Button) bool) {
20+
t.Run("should return false for invalid button", func(t *testing.T) {
21+
assert.False(t, btn(-1))
22+
assert.False(t, btn(6))
23+
})
24+
25+
t.Run("should not panic", func(t *testing.T) {
26+
for _, button := range allButtons {
27+
btn(button)
28+
}
29+
})
30+
}
31+
32+
func TestBtnPlayer(t *testing.T) {
33+
testBtnPlayer(t, pi.BtnPlayer)
34+
}
35+
36+
func testBtnPlayer(t *testing.T, btnPlayer func(pi.Button, int) bool) {
37+
testBtn(t, func(b pi.Button) bool {
38+
return btnPlayer(b, 0)
39+
})
40+
41+
t.Run("should return false for invalid player", func(t *testing.T) {
42+
assert.False(t, btnPlayer(pi.X, -1))
43+
assert.False(t, btnPlayer(pi.X, 8))
44+
})
45+
46+
t.Run("should not panic", func(t *testing.T) {
47+
for player := 0; player < 8; player++ {
48+
for _, button := range allButtons {
49+
btnPlayer(button, player)
50+
}
51+
}
52+
})
53+
}
54+
55+
func TestBtnp(t *testing.T) {
56+
testBtn(t, pi.Btnp)
57+
}
58+
59+
func TestBtnpPlayer(t *testing.T) {
60+
testBtnPlayer(t, pi.BtnpPlayer)
61+
}
62+
63+
func TestBtnBits(t *testing.T) {
64+
t.Run("should not panic", func(t *testing.T) {
65+
pi.BtnBits()
66+
})
67+
}
68+
69+
func TestBtnpBits(t *testing.T) {
70+
t.Run("should not panic", func(t *testing.T) {
71+
pi.BtnpBits()
72+
})
73+
}

ebitengine.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type ebitengineGame struct {
4242

4343
func (e *ebitengineGame) Update() error {
4444
updateTime()
45+
updateController()
4546

4647
if Update != nil {
4748
Update()

0 commit comments

Comments
 (0)