diff --git a/.gitignore b/.gitignore
index 818cae1..dd30b33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.vs
x64/
screenshot.png
+*.vcxproj.user
diff --git a/Examples.sln b/Examples.sln
index 1b4bbd3..36a540b 100644
--- a/Examples.sln
+++ b/Examples.sln
@@ -9,6 +9,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mandelbrot", "Examples\Mand
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Minesweeper", "Examples\Minesweeper\Minesweeper.vcxproj", "{7A5D47D3-25F8-4569-A2DC-910A6C132A93}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ControllerTest", "Examples\ControllerTest\ControllerTest.vcxproj", "{D11903AB-AD22-48A0-BAE2-75ADE9C8C024}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -41,6 +43,14 @@ Global
{7A5D47D3-25F8-4569-A2DC-910A6C132A93}.Release|x64.Build.0 = Release|x64
{7A5D47D3-25F8-4569-A2DC-910A6C132A93}.Release|x86.ActiveCfg = Release|Win32
{7A5D47D3-25F8-4569-A2DC-910A6C132A93}.Release|x86.Build.0 = Release|Win32
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Debug|x64.ActiveCfg = Debug|x64
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Debug|x64.Build.0 = Debug|x64
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Debug|x86.ActiveCfg = Debug|Win32
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Debug|x86.Build.0 = Debug|Win32
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Release|x64.ActiveCfg = Release|x64
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Release|x64.Build.0 = Release|x64
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Release|x86.ActiveCfg = Release|Win32
+ {D11903AB-AD22-48A0-BAE2-75ADE9C8C024}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Examples/ControllerTest/ControllerTest.vcxproj b/Examples/ControllerTest/ControllerTest.vcxproj
new file mode 100644
index 0000000..2b575b8
--- /dev/null
+++ b/Examples/ControllerTest/ControllerTest.vcxproj
@@ -0,0 +1,139 @@
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 17.0
+ Win32Proj
+ {d11903ab-ad22-48a0-bae2-75ade9c8c024}
+ ControllerTest
+ 10.0
+
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Level3
+ true
+ WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+
+
+ Level3
+ true
+ true
+ true
+ WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+
+
+ Level3
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Examples/ControllerTest/ControllerTest.vcxproj.filters b/Examples/ControllerTest/ControllerTest.vcxproj.filters
new file mode 100644
index 0000000..4327830
--- /dev/null
+++ b/Examples/ControllerTest/ControllerTest.vcxproj.filters
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Examples/ControllerTest/main.cpp b/Examples/ControllerTest/main.cpp
new file mode 100644
index 0000000..7493ceb
--- /dev/null
+++ b/Examples/ControllerTest/main.cpp
@@ -0,0 +1,99 @@
+#define THIRTEEN_IMPLEMENTATION
+#include "../../thirteen.h"
+
+#include
+#include
+
+static const unsigned int c_width = 64;
+static const unsigned int c_height = 64;
+static const bool c_fullscreen = false;
+
+static const float c_thumbStickEpsilon = 0.01f;
+
+int main(int argc, char** argv)
+{
+ Thirteen::SetApplicationName("Thirteen Demo - ControllerTest");
+
+ unsigned char* pixels = Thirteen::Init(c_width, c_height, c_fullscreen);
+ if (!pixels)
+ {
+ printf("Could not initialize Thirteen\n");
+ return 1;
+ }
+
+ printf("Interact with the controller to see controller values\n\n");
+
+ do
+ {
+ struct ButtonInfo
+ {
+ Thirteen::ControllerButton button;
+ const char* name;
+ };
+
+ static const ButtonInfo buttons[] =
+ {
+ { Thirteen::ControllerButton::DPadUp, "DPadUp" },
+ { Thirteen::ControllerButton::DPadDown, "DPadDown" },
+ { Thirteen::ControllerButton::DPadLeft, "DPadLeft" },
+ { Thirteen::ControllerButton::DPadRight, "DPadRight" },
+ { Thirteen::ControllerButton::Start, "Start" },
+ { Thirteen::ControllerButton::Back, "Back" },
+ { Thirteen::ControllerButton::LeftThumb, "LeftThumb" },
+ { Thirteen::ControllerButton::RightThumb, "RightThumb" },
+ { Thirteen::ControllerButton::LeftShoulder, "LeftShoulder" },
+ { Thirteen::ControllerButton::RightShoulder, "RightShoulder" },
+ { Thirteen::ControllerButton::A, "A" },
+ { Thirteen::ControllerButton::B, "B" },
+ { Thirteen::ControllerButton::X, "X" },
+ { Thirteen::ControllerButton::Y, "Y" }
+ };
+
+ for (const ButtonInfo& buttonInfo : buttons)
+ {
+ const bool currentState = Thirteen::GetControllerButton(0, buttonInfo.button);
+ const bool previousState = Thirteen::GetControllerButtonLastFrame(0, buttonInfo.button);
+
+ if (currentState != previousState)
+ {
+ if (currentState)
+ printf("%s pressed\n", buttonInfo.name);
+ else
+ printf("%s released\n", buttonInfo.name);
+ }
+ }
+
+ const float leftTrigger = Thirteen::GetControllerTrigger(0, true);
+ const float leftTriggerLastFrame = Thirteen::GetControllerTriggerLastFrame(0, true);
+ if (fabsf(leftTrigger - leftTriggerLastFrame) > c_thumbStickEpsilon)
+ printf("LeftTrigger: %.3f\n", leftTrigger);
+
+ const float rightTrigger = Thirteen::GetControllerTrigger(0, false);
+ const float rightTriggerLastFrame = Thirteen::GetControllerTriggerLastFrame(0, false);
+ if (fabsf(rightTrigger - rightTriggerLastFrame) > c_thumbStickEpsilon)
+ printf("RightTrigger: %.3f\n", rightTrigger);
+
+ float leftThumbX = 0.0f;
+ float leftThumbY = 0.0f;
+ float leftThumbXLastFrame = 0.0f;
+ float leftThumbYLastFrame = 0.0f;
+ Thirteen::GetControllerThumbstick(0, true, leftThumbX, leftThumbY);
+ Thirteen::GetControllerThumbstickLastFrame(0, true, leftThumbXLastFrame, leftThumbYLastFrame);
+ if (fabsf(leftThumbX - leftThumbXLastFrame) > c_thumbStickEpsilon || fabsf(leftThumbY - leftThumbYLastFrame) > c_thumbStickEpsilon)
+ printf("LeftThumb: x=%.3f, y=%.3f\n", leftThumbX, leftThumbY);
+
+ float rightThumbX = 0.0f;
+ float rightThumbY = 0.0f;
+ float rightThumbXLastFrame = 0.0f;
+ float rightThumbYLastFrame = 0.0f;
+ Thirteen::GetControllerThumbstick(0, false, rightThumbX, rightThumbY);
+ Thirteen::GetControllerThumbstickLastFrame(0, false, rightThumbXLastFrame, rightThumbYLastFrame);
+ if (fabsf(rightThumbX - rightThumbXLastFrame) > c_thumbStickEpsilon || fabsf(rightThumbY - rightThumbYLastFrame) > c_thumbStickEpsilon)
+ printf("RightThumb: x=%.3f, y=%.3f\n", rightThumbX, rightThumbY);
+ }
+ while (Thirteen::Render() && !Thirteen::GetKey(VK_ESCAPE));
+
+ Thirteen::Shutdown();
+
+ return 0;
+}
diff --git a/Examples/Mandelbrot/Mandelbrot.vcxproj.user b/Examples/Mandelbrot/Mandelbrot.vcxproj.user
deleted file mode 100644
index 88a5509..0000000
--- a/Examples/Mandelbrot/Mandelbrot.vcxproj.user
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Examples/Minesweeper/Minesweeper.vcxproj.user b/Examples/Minesweeper/Minesweeper.vcxproj.user
deleted file mode 100644
index 88a5509..0000000
--- a/Examples/Minesweeper/Minesweeper.vcxproj.user
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Examples/Simple/Simple.vcxproj.user b/Examples/Simple/Simple.vcxproj.user
deleted file mode 100644
index 88a5509..0000000
--- a/Examples/Simple/Simple.vcxproj.user
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 2041dcd..3e1bbfc 100644
--- a/readme.md
+++ b/readme.md
@@ -226,6 +226,106 @@ Checks if a keyboard key was pressed in the previous frame.
---
+### Controller Input (Windows only)
+
+Controller input currently uses XInput and is available on Windows builds only. On all other platforms, you can call the functions
+but it will always report that no buttons are pressed, the triggers are at 0, and the thumbsticks are at (0,0).
+
+Supported controller indices are `0` to `3`.
+
+Controller button bit flags used with `GetControllerButton()` and `GetControllerButtonLastFrame()`:
+
+- `ControllerButton::DPadUp`
+- `ControllerButton::DPadDown`
+- `ControllerButton::DPadLeft`
+- `ControllerButton::DPadRight`
+- `ControllerButton::Start`
+- `ControllerButton::Back`
+- `ControllerButton::LeftThumb`
+- `ControllerButton::RightThumb`
+- `ControllerButton::LeftShoulder`
+- `ControllerButton::RightShoulder`
+- `ControllerButton::A`
+- `ControllerButton::B`
+- `ControllerButton::X`
+- `ControllerButton::Y`
+
+---
+
+#### `bool GetControllerButton(int controllerIndex, ControllerButton button)`
+Checks if a controller button is currently pressed.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `button` - Button flag from `ControllerButton`
+
+**Returns:** `true` if pressed, `false` otherwise.
+
+---
+
+#### `bool GetControllerButtonLastFrame(int controllerIndex, ControllerButton button)`
+Checks if a controller button was pressed in the previous frame.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `button` - Button flag from `ControllerButton`
+
+**Returns:** `true` if pressed in the previous frame, `false` otherwise.
+
+---
+
+#### `float GetControllerTrigger(int controllerIndex, bool left)`
+Gets the current normalized trigger value.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `left` - `true` for left trigger, `false` for right trigger
+
+**Returns:** Trigger value in the range `[0, 1]`.
+
+---
+
+#### `float GetControllerTriggerLastFrame(int controllerIndex, bool left)`
+Gets the previous-frame normalized trigger value.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `left` - `true` for left trigger, `false` for right trigger
+
+**Returns:** Trigger value in the range `[0, 1]` from the previous frame.
+
+---
+
+#### `void GetControllerThumbstick(int controllerIndex, bool left, float& x, float& y)`
+Gets the current normalized thumbstick vector.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `left` - `true` for left stick, `false` for right stick
+- `x` - Receives the X axis value
+- `y` - Receives the Y axis value
+
+**Notes:**
+- Values are normalized to approximately `[-1, 1]`.
+- For invalid controller indices, both outputs are set to `0`.
+
+---
+
+#### `void GetControllerThumbstickLastFrame(int controllerIndex, bool left, float& x, float& y)`
+Gets the previous-frame normalized thumbstick vector.
+
+**Parameters:**
+- `controllerIndex` - Controller index (`0` to `3`)
+- `left` - `true` for left stick, `false` for right stick
+- `x` - Receives the X axis value
+- `y` - Receives the Y axis value
+
+**Notes:**
+- Values are normalized to approximately `[-1, 1]`.
+- For invalid controller indices, both outputs are set to `0`.
+
+---
+
### Input Helpers
To detect a single key press or button click (not held down), compare current and previous frame states:
diff --git a/thirteen.h b/thirteen.h
index 03a60fd..749253e 100644
--- a/thirteen.h
+++ b/thirteen.h
@@ -38,6 +38,7 @@ Chris Cascioli - GetWindowHandle() and warning cleanup
#include
#include
#include
+ #include
#elif defined(THIRTEEN_PLATFORM_WEB)
#include
#include
@@ -54,6 +55,7 @@ Chris Cascioli - GetWindowHandle() and warning cleanup
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "user32.lib")
+ #pragma comment(lib, "xinput.lib")
#define DX12VALIDATION() (_DEBUG && false)
#endif
@@ -161,12 +163,58 @@ namespace Thirteen
// Returns whether a keyboard key was pressed in the previous frame (use Windows virtual key codes).
[[nodiscard]] bool GetKeyLastFrame(int keyCode);
+ enum ControllerButton : uint16_t
+ {
+ DPadUp = 0x0001,
+ DPadDown = 0x0002,
+ DPadLeft = 0x0004,
+ DPadRight = 0x0008,
+ Start = 0x0010,
+ Back = 0x0020,
+ LeftThumb = 0x0040,
+ RightThumb = 0x0080,
+ LeftShoulder = 0x0100,
+ RightShoulder = 0x0200,
+ A = 0x0400,
+ B = 0x0800,
+ X = 0x1000,
+ Y = 0x2000
+ };
+
+ // Returns whether a controller button is currently pressed.
+ [[nodiscard]] bool GetControllerButton(int controllerIndex, ControllerButton button);
+
+ // Returns whether a controller button was pressed in the previous frame.
+ [[nodiscard]] bool GetControllerButtonLastFrame(int controllerIndex, ControllerButton button);
+
+ // Returns current trigger value in [0, 1].
+ [[nodiscard]] float GetControllerTrigger(int controllerIndex, bool left);
+
+ // Returns previous-frame trigger value in [0, 1].
+ [[nodiscard]] float GetControllerTriggerLastFrame(int controllerIndex, bool left);
+
+ // Returns current thumbstick values in approximately [-1, 1].
+ void GetControllerThumbstick(int controllerIndex, bool left, float& x, float& y);
+
+ // Returns previous-frame thumbstick values in approximately [-1, 1].
+ void GetControllerThumbstickLastFrame(int controllerIndex, bool left, float& x, float& y);
+
}
// ========== Implementation ==========
#ifdef THIRTEEN_IMPLEMENTATION
namespace Thirteen
{
+ struct ControllerState
+ {
+ uint16_t buttons = 0;
+ float leftTrigger = 0.0f;
+ float rightTrigger = 0.0f;
+ float leftThumbX = 0.0f;
+ float leftThumbY = 0.0f;
+ float rightThumbX = 0.0f;
+ float rightThumbY = 0.0f;
+ };
// Internal state
namespace Internal
@@ -202,6 +250,9 @@ namespace Thirteen
bool keys[256] = {};
bool prevKeys[256] = {};
+ ControllerState controllers[4] = {};
+ ControllerState prevControllers[4] = {};
+
// The pixels to write to.
uint8* Pixels = nullptr;
@@ -551,6 +602,59 @@ namespace Thirteen
bool Render(const uint8* pixels, uint32 width, uint32 height, bool vsyncEnabled)
{
+ for (int controllerIndex = 0; controllerIndex < 4; ++controllerIndex)
+ {
+ controllers[controllerIndex] = ControllerState{};
+
+ XINPUT_STATE state;
+ ZeroMemory(&state, sizeof(XINPUT_STATE));
+ DWORD result = XInputGetState(controllerIndex, &state);
+ if (result != ERROR_SUCCESS)
+ continue;
+
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP)
+ controllers[controllerIndex].buttons |= ControllerButton::DPadUp;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN)
+ controllers[controllerIndex].buttons |= ControllerButton::DPadDown;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT)
+ controllers[controllerIndex].buttons |= ControllerButton::DPadLeft;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT)
+ controllers[controllerIndex].buttons |= ControllerButton::DPadRight;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_START)
+ controllers[controllerIndex].buttons |= ControllerButton::Start;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_BACK)
+ controllers[controllerIndex].buttons |= ControllerButton::Back;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB)
+ controllers[controllerIndex].buttons |= ControllerButton::LeftThumb;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB)
+ controllers[controllerIndex].buttons |= ControllerButton::RightThumb;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER)
+ controllers[controllerIndex].buttons |= ControllerButton::LeftShoulder;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER)
+ controllers[controllerIndex].buttons |= ControllerButton::RightShoulder;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_A)
+ controllers[controllerIndex].buttons |= ControllerButton::A;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_B)
+ controllers[controllerIndex].buttons |= ControllerButton::B;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_X)
+ controllers[controllerIndex].buttons |= ControllerButton::X;
+ if (state.Gamepad.wButtons & XINPUT_GAMEPAD_Y)
+ controllers[controllerIndex].buttons |= ControllerButton::Y;
+
+ controllers[controllerIndex].leftTrigger = float(state.Gamepad.bLeftTrigger) / 255.0f;
+ controllers[controllerIndex].rightTrigger = float(state.Gamepad.bRightTrigger) / 255.0f;
+ auto NormalizeThumbAxis = [](short value) -> float
+ {
+ const float normalized = float(value) / 32767.0f;
+ return normalized < -1.0f ? -1.0f : (normalized > 1.0f ? 1.0f : normalized);
+ };
+
+ controllers[controllerIndex].leftThumbX = NormalizeThumbAxis(state.Gamepad.sThumbLX);
+ controllers[controllerIndex].leftThumbY = NormalizeThumbAxis(state.Gamepad.sThumbLY);
+ controllers[controllerIndex].rightThumbX = NormalizeThumbAxis(state.Gamepad.sThumbRX);
+ controllers[controllerIndex].rightThumbY = NormalizeThumbAxis(state.Gamepad.sThumbRY);
+ }
+
WaitForGpu();
frameIndex = swapChain->GetCurrentBackBufferIndex();
@@ -2167,6 +2271,7 @@ namespace Thirteen
prevMouseY = mouseY;
memcpy(prevMouseButtons, mouseButtons, sizeof(mouseButtons));
memcpy(prevKeys, keys, sizeof(keys));
+ memcpy(prevControllers, controllers, sizeof(controllers[0]) * 4);
// Calculate frame time
double currentTime = NowSeconds();
@@ -2330,6 +2435,62 @@ namespace Thirteen
return false;
}
+ bool GetControllerButton(int controllerIndex, ControllerButton button)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ return (Internal::controllers[controllerIndex].buttons & button) != 0;
+ return false;
+ }
+
+ bool GetControllerButtonLastFrame(int controllerIndex, ControllerButton button)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ return (Internal::prevControllers[controllerIndex].buttons & button) != 0;
+ return false;
+ }
+
+ float GetControllerTrigger(int controllerIndex, bool left)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ return left ? Internal::controllers[controllerIndex].leftTrigger : Internal::controllers[controllerIndex].rightTrigger;
+ return 0.0f;
+ }
+
+ float GetControllerTriggerLastFrame(int controllerIndex, bool left)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ return left ? Internal::prevControllers[controllerIndex].leftTrigger : Internal::prevControllers[controllerIndex].rightTrigger;
+ return 0.0f;
+ }
+
+ void GetControllerThumbstick(int controllerIndex, bool left, float& x, float& y)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ {
+ x = left ? Internal::controllers[controllerIndex].leftThumbX : Internal::controllers[controllerIndex].rightThumbX;
+ y = left ? Internal::controllers[controllerIndex].leftThumbY : Internal::controllers[controllerIndex].rightThumbY;
+ }
+ else
+ {
+ x = 0.0f;
+ y = 0.0f;
+ }
+ }
+
+ void GetControllerThumbstickLastFrame(int controllerIndex, bool left, float& x, float& y)
+ {
+ if (controllerIndex >= 0 && controllerIndex < 4)
+ {
+ x = left ? Internal::prevControllers[controllerIndex].leftThumbX : Internal::prevControllers[controllerIndex].rightThumbX;
+ y = left ? Internal::prevControllers[controllerIndex].leftThumbY : Internal::prevControllers[controllerIndex].rightThumbY;
+ }
+ else
+ {
+ x = 0.0f;
+ y = 0.0f;
+ }
+ }
+
void Shutdown()
{
using namespace Internal;