Skip to content

Commit 566b0ce

Browse files
committed
feat: add joystick support for player controls and camera movement
1 parent b834a0b commit 566b0ce

7 files changed

Lines changed: 208 additions & 20 deletions

File tree

src/common/application.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ int our::Application::run(int run_for_frames) {
265265
setupCallbacks();
266266
keyboard.enable(window);
267267
mouse.enable(window);
268+
joystick.enable();
268269

269270
// Start the ImGui context and set dark style (just my preference :D)
270271
IMGUI_CHECKVERSION();
@@ -323,6 +324,9 @@ int our::Application::run(int run_for_frames) {
323324
// record this event.
324325
keyboard.setEnabled(!io.WantCaptureKeyboard, window);
325326
mouse.setEnabled(!io.WantCaptureMouse, window);
327+
joystick.setEnabled(
328+
!io.WantCaptureKeyboard); // Joystick events are treated as keyboard events, so we check
329+
// io.WantCaptureKeyboard to decide whether to capture them or not.
326330

327331
// Render the ImGui commands we called (this doesn't actually draw to the screen yet.
328332
ImGui::Render();
@@ -383,6 +387,7 @@ int our::Application::run(int run_for_frames) {
383387
// Update the keyboard and mouse data
384388
keyboard.update();
385389
mouse.update();
390+
joystick.update();
386391

387392
// If a scene change was requested, apply it
388393
while (nextState) {
@@ -425,6 +430,7 @@ void our::Application::setupCallbacks() {
425430
// We use GLFW to store a pointer to "this" window instance.
426431
glfwSetWindowUserPointer(window, this);
427432
// The pointer is then retrieved in the callback function.
433+
if (glfwJoystickPresent(GLFW_JOYSTICK_1)) glfwSetJoystickUserPointer(GLFW_JOYSTICK_1, this);
428434

429435
// The second parameter to "glfwSet---Callback" is a function pointer.
430436
// It is replaced by an inline function -lambda expression- as it is not needed to create
@@ -474,4 +480,13 @@ void our::Application::setupCallbacks() {
474480
if (app->currentState) app->currentState->onScrollEvent(x_offset, y_offset);
475481
}
476482
});
483+
484+
// joystick connection callbacks
485+
glfwSetJoystickCallback([](int jid, int event) {
486+
auto* app = static_cast<Application*>(glfwGetJoystickUserPointer(GLFW_JOYSTICK_1));
487+
if (app) {
488+
app->getJoystick().joystickEvent(jid, event);
489+
if (app->currentState) app->currentState->onJoystickEvent(jid, event);
490+
}
491+
});
477492
}

src/common/application.hpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <type_traits>
1515
#include <unordered_map>
1616

17+
#include "input/joystick.hpp"
1718
#include "input/keyboard.hpp"
1819
#include "input/mouse.hpp"
1920
#include "systems/audio-system.hpp"
@@ -49,6 +50,7 @@ namespace our {
4950
virtual void onCursorEnterEvent(int entered) {}
5051
virtual void onMouseButtonEvent(int button, int action, int mods) {}
5152
virtual void onScrollEvent(double x_offset, double y_offset) {}
53+
virtual void onJoystickEvent(int jid, int event) {}
5254

5355
// Returns a pointer
5456
Application* getApp() {
@@ -64,9 +66,9 @@ namespace our {
6466
protected:
6567
GLFWwindow* window = nullptr; // Pointer to the window created by GLFW using "glfwCreateWindow()".
6668

67-
Keyboard keyboard; // Instance of "our" keyboard class that handles keyboard functionalities.
68-
Mouse mouse; // Instance of "our" mouse class that handles mouse functionalities.
69-
69+
Keyboard keyboard; // Instance of "our" keyboard class that handles keyboard functionalities.
70+
Mouse mouse; // Instance of "our" mouse class that handles mouse functionalities.
71+
Joystick joystick; // Instance of "our" joystick class that handles joystick functionalities.
7072
nlohmann::json app_config; // A Json file that contains all application configuration
7173

7274
std::unordered_map<std::string, State*> states; // This will store all the states that the application can run
@@ -143,7 +145,12 @@ namespace our {
143145
[[nodiscard]] const Mouse& getMouse() const {
144146
return mouse;
145147
}
146-
148+
Joystick& getJoystick() {
149+
return joystick;
150+
}
151+
[[nodiscard]] const Joystick& getJoystick() const {
152+
return joystick;
153+
}
147154
[[nodiscard]] const nlohmann::json& getConfig() const {
148155
return app_config;
149156
}

src/common/input/joystick.hpp

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#pragma once
2+
3+
#include <GLFW/glfw3.h>
4+
5+
#include <cstring>
6+
#include <glm/glm.hpp>
7+
#include <iomanip>
8+
#include <iostream>
9+
10+
namespace our {
11+
12+
class Joystick {
13+
private:
14+
bool enabled;
15+
bool currentButtons[GLFW_GAMEPAD_BUTTON_LAST + 1], previousButtons[GLFW_GAMEPAD_BUTTON_LAST + 1];
16+
float currentAxes[GLFW_GAMEPAD_AXIS_LAST + 1], previousAxes[GLFW_GAMEPAD_AXIS_LAST + 1];
17+
int currentJoystickId = -1;
18+
bool currentTriggersPressed[2] = {false, false}, previousTriggersPressed[2] = {false, false};
19+
20+
public:
21+
Joystick() : enabled(false) {
22+
disable();
23+
}
24+
25+
int getFirstGamepad() {
26+
for (int i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_LAST; i++) {
27+
if (glfwJoystickPresent(i) && glfwJoystickIsGamepad(i)) return i;
28+
}
29+
return -1; // none connected
30+
}
31+
32+
void enable() {
33+
enabled = true;
34+
std::memset(currentButtons, 0, sizeof(currentButtons));
35+
std::memset(previousButtons, 0, sizeof(previousButtons));
36+
std::memset(currentAxes, 0, sizeof(currentAxes));
37+
std::memset(previousAxes, 0, sizeof(previousAxes));
38+
currentJoystickId = getFirstGamepad();
39+
GLFWgamepadstate state;
40+
if (glfwJoystickIsGamepad(currentJoystickId) && glfwGetGamepadState(currentJoystickId, &state)) {
41+
for (int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++)
42+
currentButtons[i] = previousButtons[i] = state.buttons[i] == GLFW_PRESS;
43+
for (int i = 0; i <= GLFW_GAMEPAD_AXIS_LAST; i++) currentAxes[i] = previousAxes[i] = state.axes[i];
44+
currentTriggersPressed[0] = previousTriggersPressed[0] =
45+
state.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER] > 0.01f - 1;
46+
currentTriggersPressed[1] = previousTriggersPressed[1] =
47+
state.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER] > 0.01f - 1;
48+
}
49+
}
50+
51+
void disable() {
52+
enabled = false;
53+
currentJoystickId = -1;
54+
std::memset(currentButtons, 0, sizeof(currentButtons));
55+
std::memset(previousButtons, 0, sizeof(previousButtons));
56+
std::memset(currentAxes, 0, sizeof(currentAxes));
57+
std::memset(previousAxes, 0, sizeof(previousAxes));
58+
}
59+
60+
void update() {
61+
if (!enabled) return;
62+
std::memcpy(previousButtons, currentButtons, sizeof(previousButtons));
63+
std::memcpy(previousAxes, currentAxes, sizeof(previousAxes));
64+
std::memcpy(previousTriggersPressed, currentTriggersPressed, sizeof(previousTriggersPressed));
65+
GLFWgamepadstate state;
66+
if (glfwJoystickIsGamepad(currentJoystickId) && glfwGetGamepadState(currentJoystickId, &state)) {
67+
for (int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++) currentButtons[i] = state.buttons[i] == GLFW_PRESS;
68+
for (int i = 0; i <= GLFW_GAMEPAD_AXIS_LAST; i++) currentAxes[i] = state.axes[i];
69+
currentTriggersPressed[0] = state.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER] > 0.01f - 1;
70+
currentTriggersPressed[1] = state.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER] > 0.01f - 1;
71+
} else {
72+
// Joystick disconnected mid-session — clear current but keep previous for justReleased to fire
73+
std::memset(currentButtons, 0, sizeof(currentButtons));
74+
std::memset(currentAxes, 0, sizeof(currentAxes));
75+
std::memset(currentTriggersPressed, 0, sizeof(currentTriggersPressed));
76+
}
77+
}
78+
79+
void joystickEvent(int jid, int event) {
80+
if (event == GLFW_DISCONNECTED && jid == currentJoystickId && enabled) {
81+
enabled = false; // Automatically disable if the joystick is disconnected
82+
currentJoystickId = -1;
83+
std::memset(currentButtons, 0, sizeof(currentButtons));
84+
std::memset(currentAxes, 0, sizeof(currentAxes));
85+
std::memset(currentTriggersPressed, 0, sizeof(currentTriggersPressed));
86+
} else if (event == GLFW_CONNECTED && !enabled) {
87+
enable(); // Automatically enable if the joystick is connected
88+
}
89+
}
90+
91+
[[nodiscard]] bool isPressed(int button) const {
92+
return currentButtons[button];
93+
}
94+
[[nodiscard]] bool justPressed(int button) const {
95+
return currentButtons[button] && !previousButtons[button];
96+
}
97+
[[nodiscard]] bool justReleased(int button) const {
98+
return !currentButtons[button] && previousButtons[button];
99+
}
100+
[[nodiscard]] float getAxis(int axis) const {
101+
return currentAxes[axis];
102+
}
103+
104+
[[nodiscard]] bool isTriggerPressed(int triggerIndex) const {
105+
return currentTriggersPressed[triggerIndex];
106+
}
107+
[[nodiscard]] bool justPressedTrigger(int triggerIndex) const {
108+
return currentTriggersPressed[triggerIndex] && !previousTriggersPressed[triggerIndex];
109+
}
110+
[[nodiscard]] bool justReleasedTrigger(int triggerIndex) const {
111+
return !currentTriggersPressed[triggerIndex] && previousTriggersPressed[triggerIndex];
112+
}
113+
114+
[[nodiscard]] glm::vec2 getLeftStick() const {
115+
return {getAxis(GLFW_GAMEPAD_AXIS_LEFT_X), getAxis(GLFW_GAMEPAD_AXIS_LEFT_Y)};
116+
}
117+
[[nodiscard]] glm::vec2 getRightStick() const {
118+
return {getAxis(GLFW_GAMEPAD_AXIS_RIGHT_X), getAxis(GLFW_GAMEPAD_AXIS_RIGHT_Y)};
119+
}
120+
[[nodiscard]] float getLeftTrigger() const {
121+
return (getAxis(GLFW_GAMEPAD_AXIS_LEFT_TRIGGER) + 1.0f) / 2.0f;
122+
}
123+
[[nodiscard]] float getRightTrigger() const {
124+
return (getAxis(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER) + 1.0f) / 2.0f;
125+
}
126+
127+
[[nodiscard]] bool isEnabled() const {
128+
return enabled;
129+
}
130+
void setEnabled(bool enabled) {
131+
if (this->enabled != enabled) {
132+
if (enabled)
133+
enable();
134+
else
135+
disable();
136+
}
137+
}
138+
};
139+
140+
} // namespace our

src/common/systems/free-camera-controller.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,16 @@ namespace our {
5353
// We get a reference to the entity's rotation
5454
glm::vec3& rotation = entity->localTransform.rotation;
5555

56+
glm::vec2 delta{0.0f};
5657
// While the play state is active, the mouse stays locked
5758
if (mouse_locked) {
58-
glm::vec2 delta = app->getMouse().getMouseDelta();
59+
delta = app->getMouse().getMouseDelta();
60+
rotation.x -= delta.y * controller->rotationSensitivity; // The y-axis controls the pitch
61+
rotation.y -= delta.x * controller->rotationSensitivity; // The x-axis controls the yaw
62+
}
63+
64+
delta = app->getJoystick().getRightStick();
65+
if (glm::length(delta) > 0.1f) { // deadzone
5966
rotation.x -= delta.y * controller->rotationSensitivity; // The y-axis controls the pitch
6067
rotation.y -= delta.x * controller->rotationSensitivity; // The x-axis controls the yaw
6168
}
@@ -71,6 +78,7 @@ namespace our {
7178
if (baseFov <= 0.0f) baseFov = camera->fovY;
7279

7380
aiming = app->getMouse().isPressed(GLFW_MOUSE_BUTTON_RIGHT);
81+
aiming |= app->getJoystick().isTriggerPressed(0); // also check joystick input
7482

7583
currentAimSpeed = controller->aimSpeed;
7684
float targetFov;

src/game/systems/player-movement-system.cpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
namespace gameplay {
88

99
void PlayerMovementSystem::handleSlidingAndCrouching(PlayerMovementComponent* movement, our::Keyboard& keyboard,
10-
const glm::vec3& moveDir, float deltaTime,
11-
float slideStartSpeed) {
10+
our::Joystick& joystick, const glm::vec3& moveDir,
11+
float deltaTime, float slideStartSpeed) {
1212
bool crouchPressed = keyboard.justPressed(GLFW_KEY_LEFT_CONTROL);
13+
crouchPressed |= joystick.justPressed(GLFW_GAMEPAD_BUTTON_B); // also check joystick input for crouching
1314

1415
if (movement->isSliding) {
1516
movement->slideTimer -= deltaTime;
@@ -61,8 +62,10 @@ namespace gameplay {
6162

6263
void PlayerMovementSystem::handleDashing(PlayerMovementComponent* movement, glm::vec3& playerPosition,
6364
our::Keyboard& keyboard, const glm::vec3& frontXZ,
64-
const glm::vec3& moveDir) {
65-
if (keyboard.justPressed(GLFW_KEY_Q) && movement->dashCooldownTimer <= 0) {
65+
const glm::vec3& moveDir, our::Joystick& joystick) {
66+
bool dashPressed = keyboard.justPressed(GLFW_KEY_Q);
67+
dashPressed |= joystick.justPressed(GLFW_GAMEPAD_BUTTON_X);
68+
if (dashPressed && movement->dashCooldownTimer <= 0) {
6669
glm::vec3 dashDirection = glm::normalize(frontXZ);
6770
if (glm::length(moveDir) > 0.001f) {
6871
// Dash along active WASD input direction (not velocity, which may still be decaying)
@@ -102,8 +105,11 @@ namespace gameplay {
102105
}
103106

104107
void PlayerMovementSystem::handleJumpingAndGravity(PlayerMovementComponent* movement, glm::vec3& playerPosition,
105-
our::Keyboard& keyboard, float deltaTime) {
106-
if (keyboard.justPressed(GLFW_KEY_SPACE) && movement->isGrounded) {
108+
our::Keyboard& keyboard, our::Joystick& joystick,
109+
float deltaTime) {
110+
bool jumpPressed = keyboard.justPressed(GLFW_KEY_SPACE);
111+
jumpPressed |= joystick.justPressed(GLFW_GAMEPAD_BUTTON_A); // also check joystick input
112+
if (jumpPressed && movement->isGrounded) {
107113
if (movement->isSliding) {
108114
movement->slideCooldownTimer = movement->slideCooldown;
109115
}
@@ -178,6 +184,7 @@ namespace gameplay {
178184
movement->groundLevel = groundHeight;
179185

180186
our::Keyboard& keyboard = app->getKeyboard();
187+
our::Joystick& joystick = app->getJoystick();
181188
movement->dashTriggeredThisFrame = false;
182189

183190
if (movement->slideCooldownTimer > 0) movement->slideCooldownTimer -= deltaTime;
@@ -196,18 +203,25 @@ namespace gameplay {
196203
if (keyboard.isPressed(GLFW_KEY_A)) moveDir -= rightXZ;
197204
if (glm::length(moveDir) > 0.001f) moveDir = glm::normalize(moveDir);
198205

206+
// handle joystick percent input for movement
207+
glm::vec2 leftStick = joystick.getLeftStick();
208+
if (glm::length(leftStick) > 0.1f) { // deadzone
209+
glm::vec3 joystickMoveDir = frontXZ * leftStick.y - rightXZ * leftStick.x;
210+
moveDir -= joystickMoveDir;
211+
}
212+
199213
movement->isSprinting = keyboard.isPressed(GLFW_KEY_LEFT_SHIFT) && !movement->isSliding;
200214

201215
float sprintSpeed = movement->walkSpeed * movement->sprintMultiplier;
202216
float slideStartSpeed = sprintSpeed * movement->slideSpeedMultiplier;
203217

204-
handleSlidingAndCrouching(movement, keyboard, moveDir, deltaTime, slideStartSpeed);
218+
handleSlidingAndCrouching(movement, keyboard, joystick, moveDir, deltaTime, slideStartSpeed);
205219
handleGroundedMovement(movement, moveDir, deltaTime);
206220

207221
playerPosition += movement->velocity * deltaTime;
208222

209-
handleDashing(movement, playerPosition, keyboard, frontXZ, moveDir);
210-
handleJumpingAndGravity(movement, playerPosition, keyboard, deltaTime);
223+
handleDashing(movement, playerPosition, keyboard, frontXZ, moveDir, joystick);
224+
handleJumpingAndGravity(movement, playerPosition, keyboard, joystick, deltaTime);
211225
handleHeightInterpolation(movement, playerPosition, deltaTime);
212226
}
213227

src/game/systems/player-movement-system.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <components/player-movement.hpp>
55
#include <ecs/world.hpp>
66
#include <glm/glm.hpp>
7+
#include <input/joystick.hpp>
78
#include <input/keyboard.hpp>
89

910
namespace gameplay {
@@ -15,12 +16,13 @@ namespace gameplay {
1516
CollisionSystem* collisionSystem = nullptr; // for raycasting checks
1617

1718
void handleSlidingAndCrouching(PlayerMovementComponent* movement, our::Keyboard& keyboard,
18-
const glm::vec3& moveDir, float deltaTime, float slideStartSpeed);
19+
our::Joystick& joystick, const glm::vec3& moveDir, float deltaTime,
20+
float slideStartSpeed);
1921
void handleGroundedMovement(PlayerMovementComponent* movement, const glm::vec3& moveDir, float deltaTime);
2022
void handleDashing(PlayerMovementComponent* movement, glm::vec3& playerPosition, our::Keyboard& keyboard,
21-
const glm::vec3& frontXZ, const glm::vec3& moveDir);
23+
const glm::vec3& frontXZ, const glm::vec3& moveDir, our::Joystick& joystick);
2224
void handleJumpingAndGravity(PlayerMovementComponent* movement, glm::vec3& playerPosition,
23-
our::Keyboard& keyboard, float deltaTime);
25+
our::Keyboard& keyboard, our::Joystick& joystick, float deltaTime);
2426
void handleHeightInterpolation(PlayerMovementComponent* movement, glm::vec3& playerPosition, float deltaTime);
2527

2628
float getGroundHeight(const glm::vec3& position);

src/game/systems/projectile-system.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,14 @@ namespace gameplay {
134134
if (!playerComp) return false;
135135

136136
WeaponComponent* weapon = playerComp->currentWeapon;
137-
if (!weapon || weapon->currentAmmo <= 0) return false;
137+
if (!weapon) return false;
138138

139139
if (weapon->automatic) {
140-
if (!app->getMouse().isPressed(GLFW_MOUSE_BUTTON_LEFT)) return false;
140+
if (!app->getMouse().isPressed(GLFW_MOUSE_BUTTON_LEFT) && !app->getJoystick().isTriggerPressed(1))
141+
return false;
141142
} else {
142-
if (!app->getMouse().justPressed(GLFW_MOUSE_BUTTON_LEFT)) return false;
143+
if (!app->getMouse().justPressed(GLFW_MOUSE_BUTTON_LEFT) && !app->getJoystick().justPressedTrigger(1))
144+
return false;
143145
}
144146

145147
glm::mat4 playerM = playerEntity->getLocalToWorldMatrix();

0 commit comments

Comments
 (0)