mirror of
https://github.com/ApfelTeeSaft/WV-Core.git
synced 2026-08-26 19:43:26 +00:00
Platform Specific Rendering enhanced + Centralized input API
This commit is contained in:
+546
@@ -0,0 +1,546 @@
|
||||
# WillowVox Input API Documentation
|
||||
|
||||
The WillowVox engine provides a Unity-like input system that abstracts platform-specific input into a consistent API. All input is handled through three main classes:
|
||||
|
||||
## Core Classes
|
||||
|
||||
### 1. **InputManager** - Main Input API
|
||||
The primary interface for all input queries. Works across all platforms (PC, mobile, consoles).
|
||||
|
||||
### 2. **GamepadInput** - Gamepad-Specific Features
|
||||
Advanced gamepad features like individual stick values, triggers, and vibration.
|
||||
|
||||
### 3. **TouchInput** - Touch-Specific Features
|
||||
Multi-touch support for mobile (Android, iOS) and Wii U GamePad.
|
||||
|
||||
---
|
||||
|
||||
## InputManager - Core Input API
|
||||
|
||||
### Button Input
|
||||
|
||||
```cpp
|
||||
#include <wv/core.h>
|
||||
|
||||
using namespace WillowVox;
|
||||
|
||||
void Update(const InputState& input)
|
||||
{
|
||||
// Check if jump button is held down
|
||||
if (InputManager::GetButton(InputAction::Jump))
|
||||
{
|
||||
player->Jump();
|
||||
}
|
||||
|
||||
// Check if action button was pressed THIS FRAME
|
||||
if (InputManager::GetButtonDown(InputAction::Action1))
|
||||
{
|
||||
player->Attack();
|
||||
}
|
||||
|
||||
// Check if button was released THIS FRAME
|
||||
if (InputManager::GetButtonUp(InputAction::Action2))
|
||||
{
|
||||
player->StopAiming();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Axis Input (Movement & Look)
|
||||
|
||||
```cpp
|
||||
// Get individual axes by name
|
||||
float horizontal = InputManager::GetAxis("MoveX"); // -1.0 to 1.0
|
||||
float vertical = InputManager::GetAxis("MoveY"); // -1.0 to 1.0
|
||||
float lookX = InputManager::GetAxis("LookX");
|
||||
float lookY = InputManager::GetAxis("LookY");
|
||||
|
||||
// Or get as vectors (recommended)
|
||||
glm::vec2 movement = InputManager::GetMovementVector(); // (x, y)
|
||||
glm::vec2 look = InputManager::GetLookVector(); // (x, y)
|
||||
|
||||
// Apply to player
|
||||
player->Move(movement.x, movement.y);
|
||||
camera->Rotate(look.x, look.y);
|
||||
```
|
||||
|
||||
**Supported Axis Names:**
|
||||
- `"MoveX"` or `"Horizontal"` - Left/Right movement
|
||||
- `"MoveY"` or `"Vertical"` - Forward/Backward movement
|
||||
- `"LookX"` or `"Mouse X"` - Horizontal look/camera
|
||||
- `"LookY"` or `"Mouse Y"` - Vertical look/camera
|
||||
|
||||
### Pointer/Mouse Input
|
||||
|
||||
```cpp
|
||||
// Get pointer position (mouse or touch)
|
||||
glm::vec2 pointerPos = InputManager::GetPointerPosition();
|
||||
|
||||
// Get pointer movement delta (useful for camera)
|
||||
glm::vec2 pointerDelta = InputManager::GetPointerDelta();
|
||||
camera->Rotate(pointerDelta.x * sensitivity, pointerDelta.y * sensitivity);
|
||||
|
||||
// Check if pointer/mouse button is down
|
||||
if (InputManager::GetPointerDown())
|
||||
{
|
||||
// Click or touch is active
|
||||
}
|
||||
|
||||
// Get scroll wheel delta
|
||||
float scroll = InputManager::GetScrollDelta();
|
||||
camera->Zoom(scroll);
|
||||
```
|
||||
|
||||
### Device Detection
|
||||
|
||||
```cpp
|
||||
// Check what device is being used
|
||||
InputDeviceType device = InputManager::GetDeviceType();
|
||||
|
||||
// Check for specific device types
|
||||
if (InputManager::IsGamepad())
|
||||
{
|
||||
// Show gamepad button prompts
|
||||
UI::ShowPrompt("Press A to continue");
|
||||
}
|
||||
else if (InputManager::IsTouchscreen())
|
||||
{
|
||||
// Show touch controls
|
||||
UI::ShowVirtualButtons();
|
||||
}
|
||||
|
||||
// Check if specific device
|
||||
if (InputManager::IsDeviceType(InputDeviceType::XboxController))
|
||||
{
|
||||
// Xbox-specific UI
|
||||
}
|
||||
```
|
||||
|
||||
### Convenience Methods
|
||||
|
||||
```cpp
|
||||
// Check if player is trying to move
|
||||
if (InputManager::IsMoving())
|
||||
{
|
||||
player->PlayWalkAnimation();
|
||||
}
|
||||
|
||||
// Check if player is looking around
|
||||
if (InputManager::IsLooking())
|
||||
{
|
||||
camera->UpdateRotation();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GamepadInput - Gamepad Features
|
||||
|
||||
For more advanced gamepad control, use the `GamepadInput` class:
|
||||
|
||||
### Individual Stick Values
|
||||
|
||||
```cpp
|
||||
#include <wv/input/GamepadInput.h>
|
||||
|
||||
// Get left stick (movement)
|
||||
float leftX = GamepadInput::GetLeftStickX();
|
||||
float leftY = GamepadInput::GetLeftStickY();
|
||||
glm::vec2 leftStick = GamepadInput::GetLeftStick();
|
||||
|
||||
// Get right stick (camera/look)
|
||||
float rightX = GamepadInput::GetRightStickX();
|
||||
float rightY = GamepadInput::GetRightStickY();
|
||||
glm::vec2 rightStick = GamepadInput::GetRightStick();
|
||||
```
|
||||
|
||||
### Trigger Values
|
||||
|
||||
```cpp
|
||||
// Get analog trigger values (0.0 to 1.0)
|
||||
float leftTrigger = GamepadInput::GetTriggerLeft();
|
||||
float rightTrigger = GamepadInput::GetTriggerRight();
|
||||
|
||||
// Use for vehicle acceleration
|
||||
vehicle->Accelerate(rightTrigger);
|
||||
vehicle->Brake(leftTrigger);
|
||||
|
||||
// Check if trigger exceeds threshold
|
||||
if (GamepadInput::GetTriggerRightDown(0.5f))
|
||||
{
|
||||
// Trigger pressed at least 50%
|
||||
}
|
||||
```
|
||||
|
||||
### D-Pad Input
|
||||
|
||||
```cpp
|
||||
// Get D-Pad as vector
|
||||
glm::vec2 dpad = GamepadInput::GetDPad();
|
||||
|
||||
// Or check individual directions
|
||||
if (GamepadInput::GetDPadUp()) { menu->MoveUp(); }
|
||||
if (GamepadInput::GetDPadDown()) { menu->MoveDown(); }
|
||||
if (GamepadInput::GetDPadLeft()) { menu->MoveLeft(); }
|
||||
if (GamepadInput::GetDPadRight()) { menu->MoveRight(); }
|
||||
```
|
||||
|
||||
### Face Buttons (Position-Based)
|
||||
|
||||
```cpp
|
||||
// These map to physical button locations, not labels
|
||||
bool south = GamepadInput::GetButtonSouth(); // A / Cross (bottom)
|
||||
bool east = GamepadInput::GetButtonEast(); // B / Circle (right)
|
||||
bool west = GamepadInput::GetButtonWest(); // X / Square (left)
|
||||
bool north = GamepadInput::GetButtonNorth(); // Y / Triangle (top)
|
||||
|
||||
// Useful for QTE sequences or when you need exact button positions
|
||||
if (GamepadInput::GetButtonSouth())
|
||||
{
|
||||
qte->PressCorrectButton();
|
||||
}
|
||||
```
|
||||
|
||||
### Shoulder Buttons
|
||||
|
||||
```cpp
|
||||
bool lb = GamepadInput::GetShoulderLeft(); // LB / L1
|
||||
bool rb = GamepadInput::GetShoulderRight(); // RB / R1
|
||||
```
|
||||
|
||||
### Connection Status
|
||||
|
||||
```cpp
|
||||
// Check if gamepad is connected
|
||||
if (GamepadInput::IsConnected(0))
|
||||
{
|
||||
// Player 1 gamepad connected
|
||||
}
|
||||
|
||||
// Get gamepad type
|
||||
InputDeviceType type = GamepadInput::GetGamepadType();
|
||||
if (type == InputDeviceType::XboxController)
|
||||
{
|
||||
UI::ShowXboxButtons();
|
||||
}
|
||||
else if (type == InputDeviceType::PSController)
|
||||
{
|
||||
UI::ShowPlayStationButtons();
|
||||
}
|
||||
```
|
||||
|
||||
### Vibration/Rumble
|
||||
|
||||
```cpp
|
||||
// Set vibration (platform-dependent)
|
||||
GamepadInput::SetVibration(0.5f, 0.8f, 1.0f); // (low motor, high motor, duration)
|
||||
|
||||
// Stop vibration
|
||||
GamepadInput::StopVibration();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TouchInput - Mobile & Touch Features
|
||||
|
||||
For touchscreen platforms (Android, iOS, Wii U GamePad):
|
||||
|
||||
### Basic Touch Input
|
||||
|
||||
```cpp
|
||||
#include <wv/input/TouchInput.h>
|
||||
|
||||
// Check if touchscreen is supported
|
||||
if (TouchInput::IsSupported())
|
||||
{
|
||||
// Get number of active touches
|
||||
int touchCount = TouchInput::GetTouchCount();
|
||||
|
||||
// Check if any finger is touching
|
||||
if (TouchInput::IsTouching())
|
||||
{
|
||||
// Handle touch
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Primary Touch
|
||||
|
||||
```cpp
|
||||
// Get the first/primary touch
|
||||
Touch touch = TouchInput::GetPrimaryTouch();
|
||||
|
||||
if (touch.isActive)
|
||||
{
|
||||
glm::vec2 position = touch.position;
|
||||
glm::vec2 delta = touch.deltaPosition;
|
||||
|
||||
// Check touch phase
|
||||
if (touch.phase == Touch::Phase::Began)
|
||||
{
|
||||
// Touch just started
|
||||
}
|
||||
else if (touch.phase == Touch::Phase::Moved)
|
||||
{
|
||||
// Touch is moving
|
||||
camera->Pan(delta.x, delta.y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Touch Support
|
||||
|
||||
```cpp
|
||||
// Get all active touches
|
||||
std::vector<Touch> touches = TouchInput::GetTouches();
|
||||
|
||||
for (const Touch& touch : touches)
|
||||
{
|
||||
// Process each touch point
|
||||
UI::DrawTouchIndicator(touch.position);
|
||||
}
|
||||
|
||||
// Or access by index
|
||||
Touch touch0 = TouchInput::GetTouch(0); // First touch
|
||||
Touch touch1 = TouchInput::GetTouch(1); // Second touch
|
||||
|
||||
// Pinch-to-zoom example
|
||||
if (TouchInput::GetTouchCount() == 2)
|
||||
{
|
||||
Touch t0 = TouchInput::GetTouch(0);
|
||||
Touch t1 = TouchInput::GetTouch(1);
|
||||
|
||||
float distance = glm::distance(t0.position, t1.position);
|
||||
camera->Zoom(distance);
|
||||
}
|
||||
```
|
||||
|
||||
### Touch Phase Detection
|
||||
|
||||
```cpp
|
||||
// Check for specific touch events
|
||||
if (TouchInput::TouchBegan())
|
||||
{
|
||||
// New touch detected this frame
|
||||
}
|
||||
|
||||
if (TouchInput::TouchMoved())
|
||||
{
|
||||
// Active touch moved this frame
|
||||
}
|
||||
|
||||
if (TouchInput::TouchEnded())
|
||||
{
|
||||
// Touch was released this frame
|
||||
}
|
||||
```
|
||||
|
||||
### Platform Features
|
||||
|
||||
```cpp
|
||||
// Check capabilities
|
||||
bool multiTouch = TouchInput::SupportsMultiTouch();
|
||||
int maxTouches = TouchInput::GetMaxTouchCount();
|
||||
|
||||
// Wii U GamePad specific
|
||||
if (TouchInput::IsWiiUGamePad())
|
||||
{
|
||||
// Use GamePad-specific features
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### Desktop (Windows/Linux/macOS)
|
||||
- **Movement**: WASD or arrow keys → `MoveX/MoveY`
|
||||
- **Look**: Mouse movement → `LookX/LookY`
|
||||
- **Actions**: Mouse buttons, Space, etc. → `Action1/2/3, Jump`
|
||||
- **Gamepad**: XInput controllers supported
|
||||
|
||||
### Mobile (Android/iOS)
|
||||
- **Movement**: Virtual joystick (left side) → `MoveX/MoveY`
|
||||
- **Look**: Touch drag (right side) → `LookX/LookY`
|
||||
- **Actions**: Virtual buttons → `Action1/2, Jump`
|
||||
- **Multi-touch**: Supported via `TouchInput`
|
||||
|
||||
### PlayStation (PS3/PS4)
|
||||
- **Movement**: Left analog stick → `MoveX/MoveY`
|
||||
- **Look**: Right analog stick → `LookX/LookY`
|
||||
- **Actions**: DualShock buttons → `Action1/2/3/4`
|
||||
- **Triggers**: L2/R2 → `Crouch/Sprint`
|
||||
|
||||
### Xbox Series X/S
|
||||
- **Movement**: Left analog stick → `MoveX/MoveY`
|
||||
- **Look**: Right analog stick → `LookX/LookY`
|
||||
- **Actions**: ABXY buttons → `Action1/2/3/4`
|
||||
- **Triggers**: LT/RT → `Crouch/Sprint`
|
||||
|
||||
### Nintendo Switch
|
||||
- **Movement**: Left analog stick → `MoveX/MoveY`
|
||||
- **Look**: Right analog stick → `LookX/LookY`
|
||||
- **Actions**: ABXY buttons → `Action1/2/3/4`
|
||||
- **Triggers**: ZL/ZR → `Crouch/Sprint`
|
||||
|
||||
### Nintendo Wii
|
||||
- **Movement**: Nunchuk joystick or D-pad → `MoveX/MoveY`
|
||||
- **Look**: Wiimote IR pointer → `LookX/LookY`
|
||||
- **Actions**: A/B/1/2 buttons → `Action1/2/3/4`
|
||||
|
||||
### Nintendo GameCube
|
||||
- **Movement**: Control stick → `MoveX/MoveY`
|
||||
- **Look**: C-stick → `LookX/LookY`
|
||||
- **Actions**: ABXY buttons → `Action1/2/3/4`
|
||||
- **Triggers**: L/R analog triggers supported
|
||||
|
||||
### Nintendo Wii U
|
||||
- **Movement**: Left analog stick → `MoveX/MoveY`
|
||||
- **Look**: Right analog stick → `LookX/LookY`
|
||||
- **Actions**: ABXY buttons → `Action1/2/3/4`
|
||||
- **Touchscreen**: GamePad touchscreen via `TouchInput`
|
||||
|
||||
---
|
||||
|
||||
## Complete Usage Example
|
||||
|
||||
```cpp
|
||||
#include <wv/core.h>
|
||||
|
||||
using namespace WillowVox;
|
||||
|
||||
class MyGame : public App
|
||||
{
|
||||
public:
|
||||
void Start() override
|
||||
{
|
||||
Logger::Log("Game Started!");
|
||||
}
|
||||
|
||||
void Update(const InputState& input) override
|
||||
{
|
||||
// === Movement ===
|
||||
glm::vec2 movement = InputManager::GetMovementVector();
|
||||
player->Move(movement * speed * App::m_deltaTime);
|
||||
|
||||
// === Camera/Look ===
|
||||
glm::vec2 look = InputManager::GetLookVector();
|
||||
camera->Rotate(look * sensitivity);
|
||||
|
||||
// === Actions ===
|
||||
if (InputManager::GetButtonDown(InputAction::Jump))
|
||||
{
|
||||
player->Jump();
|
||||
}
|
||||
|
||||
if (InputManager::GetButton(InputAction::Action1))
|
||||
{
|
||||
player->PrimaryAction();
|
||||
}
|
||||
|
||||
if (InputManager::GetButtonDown(InputAction::MenuOpen))
|
||||
{
|
||||
menu->Toggle();
|
||||
}
|
||||
|
||||
// === Platform-Specific ===
|
||||
if (InputManager::IsGamepad())
|
||||
{
|
||||
// Advanced gamepad features
|
||||
float rightTrigger = GamepadInput::GetTriggerRight();
|
||||
if (rightTrigger > 0.5f)
|
||||
{
|
||||
player->Sprint();
|
||||
}
|
||||
}
|
||||
else if (InputManager::IsTouchscreen())
|
||||
{
|
||||
// Touch-specific features
|
||||
int touchCount = TouchInput::GetTouchCount();
|
||||
if (touchCount == 2)
|
||||
{
|
||||
// Pinch to zoom
|
||||
Touch t0 = TouchInput::GetTouch(0);
|
||||
Touch t1 = TouchInput::GetTouch(1);
|
||||
float dist = glm::distance(t0.position, t1.position);
|
||||
camera->Zoom(dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Render() override
|
||||
{
|
||||
// Your rendering code
|
||||
}
|
||||
|
||||
private:
|
||||
Player* player;
|
||||
Camera* camera;
|
||||
Menu* menu;
|
||||
float speed = 5.0f;
|
||||
float sensitivity = 2.0f;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## InputAction Reference
|
||||
|
||||
All abstract actions available in `InputAction` enum:
|
||||
|
||||
| Action | Description | Common Mapping |
|
||||
|--------|-------------|----------------|
|
||||
| `MoveForward` | Move forward | W, Up, D-pad Up, Left Stick Up |
|
||||
| `MoveBackward` | Move backward | S, Down, D-pad Down, Left Stick Down |
|
||||
| `MoveLeft` | Move left | A, Left, D-pad Left, Left Stick Left |
|
||||
| `MoveRight` | Move right | D, Right, D-pad Right, Left Stick Right |
|
||||
| `Action1` | Primary action | Mouse Left, A/Cross, Touch Button |
|
||||
| `Action2` | Secondary action | Mouse Right, B/Circle |
|
||||
| `Action3` | Tertiary action | X/Square |
|
||||
| `Action4` | Quaternary action | Y/Triangle |
|
||||
| `Jump` | Jump | Space, A/Cross |
|
||||
| `Crouch` | Crouch | Ctrl, L2/LT, Z |
|
||||
| `Sprint` | Sprint | Shift, R2/RT |
|
||||
| `MenuOpen` | Open menu | Esc, Start, Plus |
|
||||
| `MenuBack` | Back/Cancel | Esc, B/Circle, Minus |
|
||||
| `CycleLeft` | Cycle items left | Q, LB/L1 |
|
||||
| `CycleRight` | Cycle items right | E, RB/R1 |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use InputManager for basic input** - It works across all platforms
|
||||
2. **Use GamepadInput only when you need advanced gamepad features** (triggers, vibration)
|
||||
3. **Use TouchInput only for multi-touch or touch-specific features**
|
||||
4. **Detect device type to show appropriate UI prompts** (keyboard vs gamepad vs touch)
|
||||
5. **Test on all target platforms** - Input behavior may vary slightly
|
||||
6. **Use GetButtonDown for single-press actions** (menus, attacks)
|
||||
7. **Use GetButton for continuous actions** (movement, aiming)
|
||||
8. **Normalize and multiply by deltaTime for frame-independent movement**
|
||||
|
||||
---
|
||||
|
||||
## Migration from Old Input System
|
||||
|
||||
If you're using the old `Input` class (keyboard/mouse only):
|
||||
|
||||
```cpp
|
||||
// OLD (Desktop only)
|
||||
if (Input::GetKey(Key::W))
|
||||
player->Move(0, 1);
|
||||
|
||||
// NEW (All platforms)
|
||||
glm::vec2 movement = InputManager::GetMovementVector();
|
||||
player->Move(movement.x, movement.y);
|
||||
```
|
||||
|
||||
```cpp
|
||||
// OLD (Desktop only)
|
||||
glm::vec2 mouseDelta = Input::GetMouseDelta();
|
||||
|
||||
// NEW (All platforms)
|
||||
glm::vec2 lookDelta = InputManager::GetLookVector();
|
||||
```
|
||||
|
||||
The new API is **backward compatible** - you can still use the old `Input` class for desktop-specific features, but `InputManager` is recommended for cross-platform projects.
|
||||
@@ -12,6 +12,9 @@
|
||||
#include <wv/events/EventDispatcher.h>
|
||||
|
||||
#include <wv/input/Input.h>
|
||||
#include <wv/input/InputManager.h>
|
||||
#include <wv/input/GamepadInput.h>
|
||||
#include <wv/input/TouchInput.h>
|
||||
|
||||
#include <wv/rendering/Camera.h>
|
||||
#include <wv/rendering/Renderer.h>
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
|
||||
#include <wv/platform/InputState.h>
|
||||
#include <wv/wvpch.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* Gamepad-Specific Input API
|
||||
*
|
||||
* Provides access to gamepad-specific features like:
|
||||
* - Individual stick values
|
||||
* - Trigger values
|
||||
* - Vibration/haptics (future)
|
||||
* - Multiple gamepad support (future)
|
||||
*
|
||||
* Usage:
|
||||
* float leftX = GamepadInput::GetLeftStickX();
|
||||
* float rightTrigger = GamepadInput::GetTriggerRight();
|
||||
* bool connected = GamepadInput::IsConnected(0);
|
||||
*/
|
||||
class GamepadInput
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Initialize gamepad input with current state
|
||||
* Called automatically by InputManager
|
||||
*/
|
||||
static void SetInputState(const InputState* state);
|
||||
|
||||
// ===== Stick Input =====
|
||||
|
||||
/**
|
||||
* Get left stick X axis (-1.0 to 1.0)
|
||||
* @return Left stick horizontal value
|
||||
*/
|
||||
static float GetLeftStickX();
|
||||
|
||||
/**
|
||||
* Get left stick Y axis (-1.0 to 1.0)
|
||||
* @return Left stick vertical value
|
||||
*/
|
||||
static float GetLeftStickY();
|
||||
|
||||
/**
|
||||
* Get left stick as a vector
|
||||
* @return glm::vec2 (x, y) of left stick
|
||||
*/
|
||||
static glm::vec2 GetLeftStick();
|
||||
|
||||
/**
|
||||
* Get right stick X axis (-1.0 to 1.0)
|
||||
* @return Right stick horizontal value
|
||||
*/
|
||||
static float GetRightStickX();
|
||||
|
||||
/**
|
||||
* Get right stick Y axis (-1.0 to 1.0)
|
||||
* @return Right stick vertical value
|
||||
*/
|
||||
static float GetRightStickY();
|
||||
|
||||
/**
|
||||
* Get right stick as a vector
|
||||
* @return glm::vec2 (x, y) of right stick
|
||||
*/
|
||||
static glm::vec2 GetRightStick();
|
||||
|
||||
// ===== Trigger Input =====
|
||||
|
||||
/**
|
||||
* Get left trigger value (0.0 to 1.0)
|
||||
* Simulated on platforms without analog triggers
|
||||
* @return Left trigger pressure
|
||||
*/
|
||||
static float GetTriggerLeft();
|
||||
|
||||
/**
|
||||
* Get right trigger value (0.0 to 1.0)
|
||||
* Simulated on platforms without analog triggers
|
||||
* @return Right trigger pressure
|
||||
*/
|
||||
static float GetTriggerRight();
|
||||
|
||||
/**
|
||||
* Check if left trigger is pressed past threshold
|
||||
* @param threshold Threshold value (default 0.5)
|
||||
* @return True if trigger exceeds threshold
|
||||
*/
|
||||
static bool GetTriggerLeftDown(float threshold = 0.5f);
|
||||
|
||||
/**
|
||||
* Check if right trigger is pressed past threshold
|
||||
* @param threshold Threshold value (default 0.5)
|
||||
* @return True if trigger exceeds threshold
|
||||
*/
|
||||
static bool GetTriggerRightDown(float threshold = 0.5f);
|
||||
|
||||
// ===== D-Pad Input =====
|
||||
|
||||
/**
|
||||
* Get D-Pad as a vector
|
||||
* @return glm::vec2 (-1/0/1 for each axis)
|
||||
*/
|
||||
static glm::vec2 GetDPad();
|
||||
|
||||
/**
|
||||
* Check if D-Pad direction is pressed
|
||||
*/
|
||||
static bool GetDPadUp();
|
||||
static bool GetDPadDown();
|
||||
static bool GetDPadLeft();
|
||||
static bool GetDPadRight();
|
||||
|
||||
// ===== Face Buttons =====
|
||||
|
||||
/**
|
||||
* Get face button state (A/B/X/Y or Cross/Circle/Square/Triangle)
|
||||
* These map to the physical location, not semantic meaning
|
||||
*/
|
||||
static bool GetButtonSouth(); // A / Cross
|
||||
static bool GetButtonEast(); // B / Circle
|
||||
static bool GetButtonWest(); // X / Square
|
||||
static bool GetButtonNorth(); // Y / Triangle
|
||||
|
||||
// ===== Shoulder Buttons =====
|
||||
|
||||
static bool GetShoulderLeft(); // LB / L1
|
||||
static bool GetShoulderRight(); // RB / R1
|
||||
|
||||
// ===== Special Buttons =====
|
||||
|
||||
static bool GetButtonStart(); // Start / Options / Plus
|
||||
static bool GetButtonSelect(); // Back / Share / Minus
|
||||
|
||||
// ===== Connection Status =====
|
||||
|
||||
/**
|
||||
* Check if a gamepad is connected
|
||||
* @param index Gamepad index (0-3), default 0
|
||||
* @return True if gamepad is connected
|
||||
*/
|
||||
static bool IsConnected(int index = 0);
|
||||
|
||||
/**
|
||||
* Get the gamepad type
|
||||
* @return InputDeviceType of the connected gamepad
|
||||
*/
|
||||
static InputDeviceType GetGamepadType();
|
||||
|
||||
// ===== Vibration (Platform-Specific) =====
|
||||
|
||||
/**
|
||||
* Set vibration/rumble on the gamepad
|
||||
* Not all platforms support this
|
||||
* @param leftMotor Low frequency motor (0.0-1.0)
|
||||
* @param rightMotor High frequency motor (0.0-1.0)
|
||||
* @param duration Duration in seconds (0 = continuous)
|
||||
*/
|
||||
static void SetVibration(float leftMotor, float rightMotor, float duration = 0.0f);
|
||||
|
||||
/**
|
||||
* Stop all vibration
|
||||
*/
|
||||
static void StopVibration();
|
||||
|
||||
private:
|
||||
static const InputState* s_currentState;
|
||||
static class IPlatform* s_platform;
|
||||
|
||||
// Trigger values (simulated on platforms without analog triggers)
|
||||
static float s_leftTrigger;
|
||||
static float s_rightTrigger;
|
||||
|
||||
friend class InputManager; // Allow InputManager to set platform pointer
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include <wv/platform/InputState.h>
|
||||
#include <wv/wvpch.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* Centralized Input Manager (Unity-like API)
|
||||
*
|
||||
* Provides a static interface for querying input state across all platforms.
|
||||
* Automatically adapts to the current platform's input device.
|
||||
*
|
||||
* Usage:
|
||||
* if (InputManager::GetButton(InputAction::Jump))
|
||||
* if (InputManager::GetButtonDown(InputAction::Action1))
|
||||
* float moveX = InputManager::GetAxis("MoveX");
|
||||
* float lookY = InputManager::GetAxis("LookY");
|
||||
*/
|
||||
class InputManager
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Initialize the input manager with the current input state
|
||||
* Called automatically by the engine each frame
|
||||
*/
|
||||
static void SetInputState(const InputState* state);
|
||||
|
||||
// ===== Button Input =====
|
||||
|
||||
/**
|
||||
* Check if a button is currently held down
|
||||
* @param action The abstract input action to check
|
||||
* @return True if the button is held down
|
||||
*/
|
||||
static bool GetButton(InputAction action);
|
||||
|
||||
/**
|
||||
* Check if a button was pressed this frame
|
||||
* @param action The abstract input action to check
|
||||
* @return True if the button was pressed this frame
|
||||
*/
|
||||
static bool GetButtonDown(InputAction action);
|
||||
|
||||
/**
|
||||
* Check if a button was released this frame
|
||||
* @param action The abstract input action to check
|
||||
* @return True if the button was released this frame
|
||||
*/
|
||||
static bool GetButtonUp(InputAction action);
|
||||
|
||||
// ===== Axis Input =====
|
||||
|
||||
/**
|
||||
* Get the value of a virtual axis (-1.0 to 1.0)
|
||||
* @param axisName Name of the axis ("MoveX", "MoveY", "LookX", "LookY")
|
||||
* @return The axis value normalized to -1.0 to 1.0
|
||||
*/
|
||||
static float GetAxis(const char* axisName);
|
||||
|
||||
/**
|
||||
* Get raw axis value without smoothing
|
||||
* Same as GetAxis() for now, but provided for Unity compatibility
|
||||
*/
|
||||
static float GetAxisRaw(const char* axisName);
|
||||
|
||||
// ===== Movement =====
|
||||
|
||||
/**
|
||||
* Get movement vector (X, Y) from analog stick or keyboard
|
||||
* @return glm::vec2 where x = left/right, y = forward/backward
|
||||
*/
|
||||
static glm::vec2 GetMovementVector();
|
||||
|
||||
/**
|
||||
* Get look vector (X, Y) from analog stick or mouse
|
||||
* @return glm::vec2 where x = horizontal, y = vertical
|
||||
*/
|
||||
static glm::vec2 GetLookVector();
|
||||
|
||||
// ===== Pointer/Touch =====
|
||||
|
||||
/**
|
||||
* Get pointer position (mouse or primary touch)
|
||||
* @return glm::vec2 screen position
|
||||
*/
|
||||
static glm::vec2 GetPointerPosition();
|
||||
|
||||
/**
|
||||
* Get pointer delta (mouse movement or touch drag)
|
||||
* @return glm::vec2 delta since last frame
|
||||
*/
|
||||
static glm::vec2 GetPointerDelta();
|
||||
|
||||
/**
|
||||
* Check if pointer is down (mouse button or touch)
|
||||
* @return True if pointer is currently down
|
||||
*/
|
||||
static bool GetPointerDown();
|
||||
|
||||
// ===== Device Info =====
|
||||
|
||||
/**
|
||||
* Get the current input device type
|
||||
* @return InputDeviceType enum value
|
||||
*/
|
||||
static InputDeviceType GetDeviceType();
|
||||
|
||||
/**
|
||||
* Check if a specific device type is active
|
||||
* @param deviceType Device type to check
|
||||
* @return True if the device type matches
|
||||
*/
|
||||
static bool IsDeviceType(InputDeviceType deviceType);
|
||||
|
||||
/**
|
||||
* Check if the current device is a gamepad
|
||||
* @return True if using any gamepad type
|
||||
*/
|
||||
static bool IsGamepad();
|
||||
|
||||
/**
|
||||
* Check if the current device has a touchscreen
|
||||
* @return True if using touchscreen
|
||||
*/
|
||||
static bool IsTouchscreen();
|
||||
|
||||
// ===== Scroll =====
|
||||
|
||||
/**
|
||||
* Get scroll wheel delta
|
||||
* @return Scroll delta this frame
|
||||
*/
|
||||
static float GetScrollDelta();
|
||||
|
||||
// ===== Convenience Methods =====
|
||||
|
||||
/**
|
||||
* Check if any movement input is active
|
||||
* @return True if player is trying to move
|
||||
*/
|
||||
static bool IsMoving();
|
||||
|
||||
/**
|
||||
* Check if any look input is active
|
||||
* @return True if player is trying to look around
|
||||
*/
|
||||
static bool IsLooking();
|
||||
|
||||
private:
|
||||
static const InputState* s_currentState;
|
||||
|
||||
// Helper to get axis by name
|
||||
static float GetAxisInternal(const char* axisName);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
|
||||
#include <wv/platform/InputState.h>
|
||||
#include <wv/wvpch.h>
|
||||
#include <vector>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* Touch Point Information
|
||||
*
|
||||
* Represents a single touch point on the screen
|
||||
*/
|
||||
struct Touch
|
||||
{
|
||||
int fingerId; // Unique identifier for this touch
|
||||
glm::vec2 position; // Current position (screen coordinates)
|
||||
glm::vec2 deltaPosition; // Movement since last frame
|
||||
float pressure; // Touch pressure (0.0-1.0), if supported
|
||||
bool isActive; // Is this touch currently active
|
||||
|
||||
// Touch phase
|
||||
enum class Phase
|
||||
{
|
||||
Began, // Touch just started
|
||||
Moved, // Touch moved
|
||||
Stationary, // Touch is down but hasn't moved
|
||||
Ended, // Touch just ended
|
||||
Canceled // Touch was canceled
|
||||
};
|
||||
Phase phase;
|
||||
|
||||
Touch() : fingerId(-1), position(0.0f), deltaPosition(0.0f),
|
||||
pressure(1.0f), isActive(false), phase(Phase::Ended) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Touch-Specific Input API
|
||||
*
|
||||
* Provides access to touchscreen features for mobile and Wii U GamePad:
|
||||
* - Multi-touch support
|
||||
* - Touch position and delta
|
||||
* - Touch phase tracking
|
||||
* - Gesture recognition (future)
|
||||
*
|
||||
* Usage:
|
||||
* int touchCount = TouchInput::GetTouchCount();
|
||||
* Touch touch = TouchInput::GetTouch(0);
|
||||
* if (touch.phase == Touch::Phase::Began) { ... }
|
||||
*/
|
||||
class TouchInput
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Initialize touch input with current state
|
||||
* Called automatically by InputManager
|
||||
*/
|
||||
static void SetInputState(const InputState* state);
|
||||
|
||||
// ===== Touch Count =====
|
||||
|
||||
/**
|
||||
* Get the number of active touches
|
||||
* @return Number of fingers currently touching the screen
|
||||
*/
|
||||
static int GetTouchCount();
|
||||
|
||||
/**
|
||||
* Check if any touch is active
|
||||
* @return True if at least one finger is touching
|
||||
*/
|
||||
static bool IsTouching();
|
||||
|
||||
// ===== Touch Access =====
|
||||
|
||||
/**
|
||||
* Get touch information by index
|
||||
* @param index Touch index (0 to GetTouchCount()-1)
|
||||
* @return Touch information, or invalid touch if index out of range
|
||||
*/
|
||||
static Touch GetTouch(int index);
|
||||
|
||||
/**
|
||||
* Get the primary touch (first touch point)
|
||||
* @return Primary touch information
|
||||
*/
|
||||
static Touch GetPrimaryTouch();
|
||||
|
||||
/**
|
||||
* Get all active touches
|
||||
* @return Vector of all active touch points
|
||||
*/
|
||||
static std::vector<Touch> GetTouches();
|
||||
|
||||
// ===== Touch Position =====
|
||||
|
||||
/**
|
||||
* Get primary touch position
|
||||
* @return Screen position of first touch
|
||||
*/
|
||||
static glm::vec2 GetTouchPosition();
|
||||
|
||||
/**
|
||||
* Get primary touch delta
|
||||
* @return Movement delta of first touch
|
||||
*/
|
||||
static glm::vec2 GetTouchDelta();
|
||||
|
||||
// ===== Touch Phase Queries =====
|
||||
|
||||
/**
|
||||
* Check if any touch just began this frame
|
||||
* @return True if new touch detected
|
||||
*/
|
||||
static bool TouchBegan();
|
||||
|
||||
/**
|
||||
* Check if any touch moved this frame
|
||||
* @return True if active touch moved
|
||||
*/
|
||||
static bool TouchMoved();
|
||||
|
||||
/**
|
||||
* Check if any touch ended this frame
|
||||
* @return True if touch was released
|
||||
*/
|
||||
static bool TouchEnded();
|
||||
|
||||
// ===== Platform Features =====
|
||||
|
||||
/**
|
||||
* Check if the current platform supports touch input
|
||||
* @return True if touchscreen is available
|
||||
*/
|
||||
static bool IsSupported();
|
||||
|
||||
/**
|
||||
* Check if the platform supports multi-touch
|
||||
* @return True if multiple simultaneous touches are supported
|
||||
*/
|
||||
static bool SupportsMultiTouch();
|
||||
|
||||
/**
|
||||
* Get maximum number of simultaneous touches supported
|
||||
* @return Max touch points (typically 5-10)
|
||||
*/
|
||||
static int GetMaxTouchCount();
|
||||
|
||||
// ===== Wii U GamePad Specific =====
|
||||
|
||||
/**
|
||||
* Check if touch input is from Wii U GamePad
|
||||
* @return True if device is Wii U GamePad
|
||||
*/
|
||||
static bool IsWiiUGamePad();
|
||||
|
||||
private:
|
||||
static const InputState* s_currentState;
|
||||
static std::vector<Touch> s_touches;
|
||||
static std::vector<Touch> s_prevTouches;
|
||||
static constexpr int MAX_TOUCHES = 10;
|
||||
|
||||
// Update touch list from input state
|
||||
static void UpdateTouches();
|
||||
|
||||
// Helper to find touch in previous frame
|
||||
static Touch* FindPreviousTouch(int fingerId);
|
||||
};
|
||||
}
|
||||
@@ -49,6 +49,11 @@ namespace WillowVox
|
||||
// Special platform features
|
||||
virtual bool HasFeature(const char* featureName) const = 0;
|
||||
|
||||
// Vibration/Rumble (controllers, gamepads)
|
||||
// Not all platforms support this, default implementation is no-op
|
||||
virtual void SetVibration(int playerIndex, float lowFrequency, float highFrequency) {}
|
||||
virtual void StopVibration(int playerIndex) {}
|
||||
|
||||
// Optional: Some platforms may need special per-frame updates
|
||||
virtual void Update(float deltaTime) {}
|
||||
};
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace WillowVox
|
||||
Touchscreen,
|
||||
Gamepad,
|
||||
WiiRemote,
|
||||
GameCubeController,
|
||||
WiiUGamePad,
|
||||
PSController,
|
||||
XboxController,
|
||||
SwitchController
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <wv/Logger.h>
|
||||
#include <wv/platform/IPlatform.h>
|
||||
#include <wv/platform/IGraphicsContext.h>
|
||||
#include <wv/input/InputManager.h>
|
||||
#include <wv/input/GamepadInput.h>
|
||||
#include <wv/input/TouchInput.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace WillowVox
|
||||
@@ -70,6 +73,12 @@ namespace WillowVox
|
||||
// Poll input
|
||||
m_platform->PollInput(m_inputState);
|
||||
|
||||
// Update centralized input managers
|
||||
InputManager::SetInputState(&m_inputState);
|
||||
GamepadInput::SetInputState(&m_inputState);
|
||||
GamepadInput::s_platform = m_platform; // Set platform for vibration
|
||||
TouchInput::SetInputState(&m_inputState);
|
||||
|
||||
// Begin frame
|
||||
m_graphicsContext->BeginFrame();
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include <wv/input/GamepadInput.h>
|
||||
#include <wv/platform/IPlatform.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
const InputState* GamepadInput::s_currentState = nullptr;
|
||||
IPlatform* GamepadInput::s_platform = nullptr;
|
||||
float GamepadInput::s_leftTrigger = 0.0f;
|
||||
float GamepadInput::s_rightTrigger = 0.0f;
|
||||
|
||||
void GamepadInput::SetInputState(const InputState* state)
|
||||
{
|
||||
s_currentState = state;
|
||||
|
||||
// Update simulated trigger values based on button state
|
||||
if (s_currentState)
|
||||
{
|
||||
s_leftTrigger = s_currentState->IsActionHeld(InputAction::Crouch) ? 1.0f : 0.0f;
|
||||
s_rightTrigger = s_currentState->IsActionHeld(InputAction::Sprint) ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float GamepadInput::GetLeftStickX()
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
return s_currentState->moveAxisX;
|
||||
}
|
||||
|
||||
float GamepadInput::GetLeftStickY()
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
return s_currentState->moveAxisY;
|
||||
}
|
||||
|
||||
glm::vec2 GamepadInput::GetLeftStick()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->moveAxisX, s_currentState->moveAxisY);
|
||||
}
|
||||
|
||||
float GamepadInput::GetRightStickX()
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
// Look axis from platforms is typically scaled up (e.g., 100x for sensitivity)
|
||||
// Normalize it back to -1..1 range
|
||||
float value = s_currentState->lookAxisX;
|
||||
return glm::clamp(value / 100.0f, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
float GamepadInput::GetRightStickY()
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
float value = s_currentState->lookAxisY;
|
||||
return glm::clamp(value / 100.0f, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
glm::vec2 GamepadInput::GetRightStick()
|
||||
{
|
||||
return glm::vec2(GetRightStickX(), GetRightStickY());
|
||||
}
|
||||
|
||||
float GamepadInput::GetTriggerLeft()
|
||||
{
|
||||
return s_leftTrigger;
|
||||
}
|
||||
|
||||
float GamepadInput::GetTriggerRight()
|
||||
{
|
||||
return s_rightTrigger;
|
||||
}
|
||||
|
||||
bool GamepadInput::GetTriggerLeftDown(float threshold)
|
||||
{
|
||||
return s_leftTrigger > threshold;
|
||||
}
|
||||
|
||||
bool GamepadInput::GetTriggerRightDown(float threshold)
|
||||
{
|
||||
return s_rightTrigger > threshold;
|
||||
}
|
||||
|
||||
glm::vec2 GamepadInput::GetDPad()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
|
||||
if (s_currentState->IsActionHeld(InputAction::MoveLeft)) x = -1.0f;
|
||||
if (s_currentState->IsActionHeld(InputAction::MoveRight)) x = 1.0f;
|
||||
if (s_currentState->IsActionHeld(InputAction::MoveForward)) y = 1.0f;
|
||||
if (s_currentState->IsActionHeld(InputAction::MoveBackward)) y = -1.0f;
|
||||
|
||||
return glm::vec2(x, y);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetDPadUp()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MoveForward);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetDPadDown()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MoveBackward);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetDPadLeft()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MoveLeft);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetDPadRight()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MoveRight);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonSouth()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::Action1);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonEast()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::Action2);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonWest()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::Action3);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonNorth()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::Action4);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetShoulderLeft()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::CycleLeft);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetShoulderRight()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::CycleRight);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonStart()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MenuOpen);
|
||||
}
|
||||
|
||||
bool GamepadInput::GetButtonSelect()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(InputAction::MenuBack);
|
||||
}
|
||||
|
||||
bool GamepadInput::IsConnected(int index)
|
||||
{
|
||||
if (index != 0) return false; // Only support player 0 for now
|
||||
if (!s_currentState) return false;
|
||||
|
||||
// Consider connected if device type is a gamepad
|
||||
InputDeviceType device = s_currentState->deviceType;
|
||||
return device == InputDeviceType::Gamepad ||
|
||||
device == InputDeviceType::XboxController ||
|
||||
device == InputDeviceType::PSController ||
|
||||
device == InputDeviceType::SwitchController ||
|
||||
device == InputDeviceType::WiiRemote ||
|
||||
device == InputDeviceType::GameCubeController ||
|
||||
device == InputDeviceType::WiiUGamePad;
|
||||
}
|
||||
|
||||
InputDeviceType GamepadInput::GetGamepadType()
|
||||
{
|
||||
if (!s_currentState) return InputDeviceType::Unknown;
|
||||
return s_currentState->deviceType;
|
||||
}
|
||||
|
||||
void GamepadInput::SetVibration(float leftMotor, float rightMotor, float duration)
|
||||
{
|
||||
if (!s_platform)
|
||||
return;
|
||||
|
||||
// Call platform's vibration implementation
|
||||
// Duration is not yet implemented (would need a timer system)
|
||||
s_platform->SetVibration(0, leftMotor, rightMotor);
|
||||
}
|
||||
|
||||
void GamepadInput::StopVibration()
|
||||
{
|
||||
if (!s_platform)
|
||||
return;
|
||||
|
||||
s_platform->StopVibration(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#include <wv/input/InputManager.h>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
const InputState* InputManager::s_currentState = nullptr;
|
||||
|
||||
void InputManager::SetInputState(const InputState* state)
|
||||
{
|
||||
s_currentState = state;
|
||||
}
|
||||
|
||||
bool InputManager::GetButton(InputAction action)
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionHeld(action);
|
||||
}
|
||||
|
||||
bool InputManager::GetButtonDown(InputAction action)
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionPressed(action);
|
||||
}
|
||||
|
||||
bool InputManager::GetButtonUp(InputAction action)
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->IsActionReleased(action);
|
||||
}
|
||||
|
||||
float InputManager::GetAxis(const char* axisName)
|
||||
{
|
||||
return GetAxisInternal(axisName);
|
||||
}
|
||||
|
||||
float InputManager::GetAxisRaw(const char* axisName)
|
||||
{
|
||||
// For now, same as GetAxis - could add smoothing to GetAxis() later
|
||||
return GetAxisInternal(axisName);
|
||||
}
|
||||
|
||||
float InputManager::GetAxisInternal(const char* axisName)
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
|
||||
// Map axis names to input state values
|
||||
if (strcmp(axisName, "MoveX") == 0 || strcmp(axisName, "Horizontal") == 0)
|
||||
return s_currentState->moveAxisX;
|
||||
else if (strcmp(axisName, "MoveY") == 0 || strcmp(axisName, "Vertical") == 0)
|
||||
return s_currentState->moveAxisY;
|
||||
else if (strcmp(axisName, "LookX") == 0 || strcmp(axisName, "Mouse X") == 0)
|
||||
return s_currentState->lookAxisX;
|
||||
else if (strcmp(axisName, "LookY") == 0 || strcmp(axisName, "Mouse Y") == 0)
|
||||
return s_currentState->lookAxisY;
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
glm::vec2 InputManager::GetMovementVector()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->moveAxisX, s_currentState->moveAxisY);
|
||||
}
|
||||
|
||||
glm::vec2 InputManager::GetLookVector()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->lookAxisX, s_currentState->lookAxisY);
|
||||
}
|
||||
|
||||
glm::vec2 InputManager::GetPointerPosition()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->pointerX, s_currentState->pointerY);
|
||||
}
|
||||
|
||||
glm::vec2 InputManager::GetPointerDelta()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY);
|
||||
}
|
||||
|
||||
bool InputManager::GetPointerDown()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->pointerDown;
|
||||
}
|
||||
|
||||
InputDeviceType InputManager::GetDeviceType()
|
||||
{
|
||||
if (!s_currentState) return InputDeviceType::Unknown;
|
||||
return s_currentState->deviceType;
|
||||
}
|
||||
|
||||
bool InputManager::IsDeviceType(InputDeviceType deviceType)
|
||||
{
|
||||
return GetDeviceType() == deviceType;
|
||||
}
|
||||
|
||||
bool InputManager::IsGamepad()
|
||||
{
|
||||
InputDeviceType device = GetDeviceType();
|
||||
return device == InputDeviceType::Gamepad ||
|
||||
device == InputDeviceType::XboxController ||
|
||||
device == InputDeviceType::PSController ||
|
||||
device == InputDeviceType::SwitchController ||
|
||||
device == InputDeviceType::WiiRemote;
|
||||
}
|
||||
|
||||
bool InputManager::IsTouchscreen()
|
||||
{
|
||||
return GetDeviceType() == InputDeviceType::Touchscreen;
|
||||
}
|
||||
|
||||
float InputManager::GetScrollDelta()
|
||||
{
|
||||
if (!s_currentState) return 0.0f;
|
||||
return s_currentState->scrollDelta;
|
||||
}
|
||||
|
||||
bool InputManager::IsMoving()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
float threshold = 0.1f;
|
||||
return std::abs(s_currentState->moveAxisX) > threshold ||
|
||||
std::abs(s_currentState->moveAxisY) > threshold ||
|
||||
GetButton(InputAction::MoveForward) ||
|
||||
GetButton(InputAction::MoveBackward) ||
|
||||
GetButton(InputAction::MoveLeft) ||
|
||||
GetButton(InputAction::MoveRight);
|
||||
}
|
||||
|
||||
bool InputManager::IsLooking()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
float threshold = 0.1f;
|
||||
return std::abs(s_currentState->lookAxisX) > threshold ||
|
||||
std::abs(s_currentState->lookAxisY) > threshold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#include <wv/input/TouchInput.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
const InputState* TouchInput::s_currentState = nullptr;
|
||||
std::vector<Touch> TouchInput::s_touches;
|
||||
std::vector<Touch> TouchInput::s_prevTouches;
|
||||
|
||||
void TouchInput::SetInputState(const InputState* state)
|
||||
{
|
||||
s_currentState = state;
|
||||
|
||||
// Store previous frame's touches
|
||||
s_prevTouches = s_touches;
|
||||
|
||||
UpdateTouches();
|
||||
}
|
||||
|
||||
void TouchInput::UpdateTouches()
|
||||
{
|
||||
s_touches.clear();
|
||||
|
||||
if (!s_currentState) return;
|
||||
if (!IsSupported()) return;
|
||||
|
||||
// Create touch from pointer state
|
||||
if (s_currentState->pointerDown)
|
||||
{
|
||||
Touch touch;
|
||||
touch.fingerId = 0;
|
||||
touch.position = glm::vec2(s_currentState->pointerX, s_currentState->pointerY);
|
||||
touch.deltaPosition = glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY);
|
||||
touch.pressure = 1.0f;
|
||||
touch.isActive = true;
|
||||
|
||||
// Determine phase based on previous frame
|
||||
Touch* prevTouch = FindPreviousTouch(touch.fingerId);
|
||||
|
||||
if (!prevTouch)
|
||||
{
|
||||
// New touch - just began
|
||||
touch.phase = Touch::Phase::Began;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Existing touch - check if moved
|
||||
float deltaX = touch.position.x - prevTouch->position.x;
|
||||
float deltaY = touch.position.y - prevTouch->position.y;
|
||||
float moveDist = std::sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (moveDist > 0.01f)
|
||||
touch.phase = Touch::Phase::Moved;
|
||||
else
|
||||
touch.phase = Touch::Phase::Stationary;
|
||||
}
|
||||
|
||||
s_touches.push_back(touch);
|
||||
}
|
||||
}
|
||||
|
||||
Touch* TouchInput::FindPreviousTouch(int fingerId)
|
||||
{
|
||||
for (auto& touch : s_prevTouches)
|
||||
{
|
||||
if (touch.fingerId == fingerId && touch.isActive)
|
||||
return &touch;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int TouchInput::GetTouchCount()
|
||||
{
|
||||
return static_cast<int>(s_touches.size());
|
||||
}
|
||||
|
||||
bool TouchInput::IsTouching()
|
||||
{
|
||||
return GetTouchCount() > 0;
|
||||
}
|
||||
|
||||
Touch TouchInput::GetTouch(int index)
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(s_touches.size()))
|
||||
return Touch(); // Return invalid touch
|
||||
|
||||
return s_touches[index];
|
||||
}
|
||||
|
||||
Touch TouchInput::GetPrimaryTouch()
|
||||
{
|
||||
return GetTouch(0);
|
||||
}
|
||||
|
||||
std::vector<Touch> TouchInput::GetTouches()
|
||||
{
|
||||
return s_touches;
|
||||
}
|
||||
|
||||
glm::vec2 TouchInput::GetTouchPosition()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->pointerX, s_currentState->pointerY);
|
||||
}
|
||||
|
||||
glm::vec2 TouchInput::GetTouchDelta()
|
||||
{
|
||||
if (!s_currentState) return glm::vec2(0.0f);
|
||||
return glm::vec2(s_currentState->pointerDeltaX, s_currentState->pointerDeltaY);
|
||||
}
|
||||
|
||||
bool TouchInput::TouchBegan()
|
||||
{
|
||||
if (s_touches.empty()) return false;
|
||||
|
||||
for (const auto& touch : s_touches)
|
||||
{
|
||||
if (touch.phase == Touch::Phase::Began)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TouchInput::TouchMoved()
|
||||
{
|
||||
if (s_touches.empty()) return false;
|
||||
|
||||
for (const auto& touch : s_touches)
|
||||
{
|
||||
if (touch.phase == Touch::Phase::Moved)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TouchInput::TouchEnded()
|
||||
{
|
||||
// Check if any touch from previous frame is no longer active
|
||||
for (const auto& prevTouch : s_prevTouches)
|
||||
{
|
||||
if (!prevTouch.isActive)
|
||||
continue;
|
||||
|
||||
// Check if this touch still exists in current frame
|
||||
bool stillActive = false;
|
||||
for (const auto& currentTouch : s_touches)
|
||||
{
|
||||
if (currentTouch.fingerId == prevTouch.fingerId)
|
||||
{
|
||||
stillActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stillActive)
|
||||
return true; // Touch ended this frame
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TouchInput::IsSupported()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
|
||||
return s_currentState->deviceType == InputDeviceType::Touchscreen ||
|
||||
s_currentState->deviceType == InputDeviceType::WiiUGamePad;
|
||||
}
|
||||
|
||||
bool TouchInput::SupportsMultiTouch()
|
||||
{
|
||||
// Most modern touchscreens support multi-touch
|
||||
return IsSupported();
|
||||
}
|
||||
|
||||
int TouchInput::GetMaxTouchCount()
|
||||
{
|
||||
if (!IsSupported()) return 0;
|
||||
return MAX_TOUCHES;
|
||||
}
|
||||
|
||||
bool TouchInput::IsWiiUGamePad()
|
||||
{
|
||||
if (!s_currentState) return false;
|
||||
return s_currentState->deviceType == InputDeviceType::WiiUGamePad;
|
||||
}
|
||||
}
|
||||
@@ -260,11 +260,14 @@ namespace WillowVox
|
||||
{
|
||||
// Platform-specific user data paths
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
static const char* path = "./userdata"; // TODO: Use AppData
|
||||
// Windows: %APPDATA%\WillowVox
|
||||
static const char* path = "%APPDATA%\\WillowVox";
|
||||
#elif defined(PLATFORM_MACOS)
|
||||
static const char* path = "./userdata"; // TODO: Use ~/Library/Application Support
|
||||
// macOS: ~/Library/Application Support/WillowVox
|
||||
static const char* path = "~/Library/Application Support/WillowVox";
|
||||
#else
|
||||
static const char* path = "./userdata"; // TODO: Use ~/.local/share
|
||||
// Linux: ~/.local/share/WillowVox
|
||||
static const char* path = "~/.local/share/WillowVox";
|
||||
#endif
|
||||
return path;
|
||||
}
|
||||
@@ -293,4 +296,25 @@ namespace WillowVox
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void DesktopPlatform::SetVibration(int playerIndex, float lowFrequency, float highFrequency)
|
||||
{
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
// Windows: Use XInput for gamepad vibration
|
||||
// Note: This requires linking against Xinput.lib
|
||||
// For now, GLFW doesn't expose vibration API, so we'd need platform-specific code
|
||||
// This is a placeholder - actual implementation would use XInput directly
|
||||
Logger::EngineLog("Vibration requested (Windows XInput not yet implemented)");
|
||||
#else
|
||||
// Linux/macOS: GLFW doesn't support vibration
|
||||
// Would need to use platform-specific APIs (evdev on Linux, IOKit on macOS)
|
||||
Logger::EngineLog("Vibration not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
void DesktopPlatform::StopVibration(int playerIndex)
|
||||
{
|
||||
// Stop vibration by setting both motors to 0
|
||||
SetVibration(playerIndex, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace WillowVox
|
||||
|
||||
bool HasFeature(const char* featureName) const override;
|
||||
|
||||
void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override;
|
||||
void StopVibration(int playerIndex) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<DesktopGraphicsContext> m_graphicsContext;
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "GameCubeGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_GAMECUBE
|
||||
|
||||
// libogc includes
|
||||
#include <gccore.h>
|
||||
#include <ogc/system.h>
|
||||
#include <ogc/video.h>
|
||||
#include <ogc/gx.h>
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
#define DEFAULT_FIFO_SIZE (256 * 1024)
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
GameCubeGraphicsContext::GameCubeGraphicsContext()
|
||||
{
|
||||
m_startTime = static_cast<float>(SYS_Time()) / TB_TIMER_CLOCK;
|
||||
}
|
||||
|
||||
GameCubeGraphicsContext::~GameCubeGraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool GameCubeGraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
Logger::EngineLog("Initializing GameCube Graphics (GX)");
|
||||
|
||||
// Initialize VIDEO subsystem
|
||||
VIDEO_Init();
|
||||
|
||||
// Get preferred video mode
|
||||
GXRModeObj* rmode = VIDEO_GetPreferredMode(NULL);
|
||||
m_renderMode = rmode;
|
||||
|
||||
m_width = rmode->fbWidth;
|
||||
m_height = rmode->efbHeight;
|
||||
|
||||
Logger::EngineLog("Video mode: %dx%d", m_width, m_height);
|
||||
|
||||
// Allocate framebuffers (double buffering)
|
||||
m_frameBuffer[0] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
|
||||
m_frameBuffer[1] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
|
||||
|
||||
if (!m_frameBuffer[0] || !m_frameBuffer[1])
|
||||
{
|
||||
Logger::EngineError("Failed to allocate framebuffers");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure VIDEO
|
||||
VIDEO_Configure(rmode);
|
||||
VIDEO_SetNextFramebuffer(m_frameBuffer[0]);
|
||||
VIDEO_SetBlack(FALSE);
|
||||
VIDEO_Flush();
|
||||
VIDEO_WaitVSync();
|
||||
if (rmode->viTVMode & VI_NON_INTERLACE)
|
||||
VIDEO_WaitVSync();
|
||||
|
||||
// Allocate GX FIFO buffer
|
||||
m_fifoBuffer = memalign(32, DEFAULT_FIFO_SIZE);
|
||||
if (!m_fifoBuffer)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate GX FIFO buffer");
|
||||
return false;
|
||||
}
|
||||
memset(m_fifoBuffer, 0, DEFAULT_FIFO_SIZE);
|
||||
|
||||
// Initialize GX
|
||||
GX_Init(m_fifoBuffer, DEFAULT_FIFO_SIZE);
|
||||
|
||||
// Set up GX for 3D rendering
|
||||
GX_SetCopyClear((GXColor){26, 26, 26, 255}, 0x00FFFFFF);
|
||||
|
||||
// Set viewport
|
||||
GX_SetViewport(0, 0, rmode->fbWidth, rmode->efbHeight, 0, 1);
|
||||
f32 yscale = GX_GetYScaleFactor(rmode->efbHeight, rmode->xfbHeight);
|
||||
u32 xfbHeight = GX_SetDispCopyYScale(yscale);
|
||||
GX_SetScissor(0, 0, rmode->fbWidth, rmode->efbHeight);
|
||||
GX_SetDispCopySrc(0, 0, rmode->fbWidth, rmode->efbHeight);
|
||||
GX_SetDispCopyDst(rmode->fbWidth, xfbHeight);
|
||||
GX_SetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter);
|
||||
GX_SetFieldMode(rmode->field_rendering, ((rmode->viHeight == 2 * rmode->xfbHeight) ? GX_ENABLE : GX_DISABLE));
|
||||
|
||||
// Set pixel format
|
||||
if (rmode->aa)
|
||||
GX_SetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_LINEAR);
|
||||
else
|
||||
GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR);
|
||||
|
||||
// Enable depth testing
|
||||
GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE);
|
||||
|
||||
// Enable backface culling
|
||||
GX_SetCullMode(GX_CULL_BACK);
|
||||
|
||||
// Enable blending
|
||||
GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR);
|
||||
|
||||
// Set color update
|
||||
GX_SetColorUpdate(GX_TRUE);
|
||||
|
||||
Logger::EngineLog("GameCube Graphics initialized (GX, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
if (m_fifoBuffer)
|
||||
{
|
||||
free(m_fifoBuffer);
|
||||
m_fifoBuffer = nullptr;
|
||||
}
|
||||
|
||||
// Framebuffers are managed by libogc, no need to free
|
||||
|
||||
m_initialized = false;
|
||||
Logger::EngineLog("GameCube Graphics shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::BeginFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::EndFrame()
|
||||
{
|
||||
// Execute all pending GX commands
|
||||
GX_DrawDone();
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::SwapBuffers()
|
||||
{
|
||||
// Copy EFB to XFB
|
||||
GX_CopyDisp(m_frameBuffer[m_currentFB], GX_TRUE);
|
||||
GX_Flush();
|
||||
|
||||
// Swap framebuffers
|
||||
VIDEO_SetNextFramebuffer(m_frameBuffer[m_currentFB]);
|
||||
VIDEO_Flush();
|
||||
|
||||
if (m_vsyncEnabled)
|
||||
VIDEO_WaitVSync();
|
||||
|
||||
m_currentFB ^= 1; // Toggle between 0 and 1
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
GXColor color = {
|
||||
static_cast<u8>(r * 255),
|
||||
static_cast<u8>(g * 255),
|
||||
static_cast<u8>(b * 255),
|
||||
static_cast<u8>(a * 255)
|
||||
};
|
||||
GX_SetCopyClear(color, 0x00FFFFFF);
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::ClearDepth()
|
||||
{
|
||||
// GX clears depth automatically with GX_CopyDisp
|
||||
}
|
||||
|
||||
bool GameCubeGraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
}
|
||||
|
||||
bool GameCubeGraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float GameCubeGraphicsContext::GetTime() const
|
||||
{
|
||||
float currentTime = static_cast<float>(SYS_Time()) / TB_TIMER_CLOCK;
|
||||
return currentTime - m_startTime;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void GameCubeGraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
GX_SetViewport(x, y, width, height, 0, 1);
|
||||
GX_SetScissor(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_GAMECUBE
|
||||
@@ -36,8 +36,16 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 640;
|
||||
int m_height = 480;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// GX-specific framebuffer (using void* to avoid including libogc headers)
|
||||
void* m_fifoBuffer = nullptr; // GX FIFO buffer
|
||||
void* m_frameBuffer[2] = {nullptr, nullptr}; // Double buffering
|
||||
int m_currentFB = 0;
|
||||
void* m_renderMode = nullptr; // GXRModeObj*
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "GameCubePlatform.h"
|
||||
#include "GameCubeGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_GAMECUBE
|
||||
|
||||
// libogc includes
|
||||
#include <gccore.h>
|
||||
#include <ogc/pad.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
GameCubePlatform::GameCubePlatform()
|
||||
{
|
||||
}
|
||||
|
||||
GameCubePlatform::~GameCubePlatform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool GameCubePlatform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing GameCube Platform (libogc)");
|
||||
|
||||
// Initialize PAD (GameCube controller)
|
||||
PAD_Init();
|
||||
|
||||
// Initialize graphics
|
||||
m_graphicsContext = std::make_unique<GameCubeGraphicsContext>();
|
||||
|
||||
Logger::EngineLog("GameCube Platform initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameCubePlatform::Shutdown()
|
||||
{
|
||||
m_graphicsContext.reset();
|
||||
Logger::EngineLog("GameCube Platform shutdown");
|
||||
}
|
||||
|
||||
void GameCubePlatform::ProcessEvents()
|
||||
{
|
||||
// GameCube doesn't have traditional event polling
|
||||
// Input is polled directly via PAD_ScanPads()
|
||||
}
|
||||
|
||||
void GameCubePlatform::PollInput(InputState& outInputState)
|
||||
{
|
||||
UpdateGamepadInput(outInputState);
|
||||
outInputState.deviceType = InputDeviceType::GameCubeController;
|
||||
}
|
||||
|
||||
void GameCubePlatform::UpdateGamepadInput(InputState& outInputState)
|
||||
{
|
||||
// Store previous button state
|
||||
m_prevButtons = m_buttons;
|
||||
|
||||
// Scan for button presses
|
||||
PAD_ScanPads();
|
||||
|
||||
// Read controller 0 state
|
||||
u16 buttonsDown = PAD_ButtonsDown(0);
|
||||
u16 buttonsHeld = PAD_ButtonsHeld(0);
|
||||
m_buttons = buttonsHeld;
|
||||
|
||||
// Read analog sticks
|
||||
s8 mainStickX = PAD_StickX(0);
|
||||
s8 mainStickY = PAD_StickY(0);
|
||||
s8 cStickX = PAD_SubStickX(0);
|
||||
s8 cStickY = PAD_SubStickY(0);
|
||||
|
||||
m_mainStickX = NormalizeAxis(mainStickX);
|
||||
m_mainStickY = NormalizeAxis(mainStickY);
|
||||
m_cStickX = NormalizeAxis(cStickX);
|
||||
m_cStickY = NormalizeAxis(cStickY);
|
||||
|
||||
// Read triggers
|
||||
u8 triggerL = PAD_TriggerL(0);
|
||||
u8 triggerR = PAD_TriggerR(0);
|
||||
|
||||
m_leftTrigger = NormalizeTrigger(triggerL);
|
||||
m_rightTrigger = NormalizeTrigger(triggerR);
|
||||
|
||||
// Map analog sticks
|
||||
outInputState.moveAxisX = m_mainStickX;
|
||||
outInputState.moveAxisY = m_mainStickY;
|
||||
outInputState.lookAxisX = m_cStickX * 100.0f;
|
||||
outInputState.lookAxisY = -m_cStickY * 100.0f; // Invert Y for camera
|
||||
|
||||
// Button mapping (GameCube controller)
|
||||
bool btnA = (m_buttons & PAD_BUTTON_A) != 0;
|
||||
bool btnB = (m_buttons & PAD_BUTTON_B) != 0;
|
||||
bool btnX = (m_buttons & PAD_BUTTON_X) != 0;
|
||||
bool btnY = (m_buttons & PAD_BUTTON_Y) != 0;
|
||||
bool btnZ = (m_buttons & PAD_TRIGGER_Z) != 0;
|
||||
bool btnL = (m_buttons & PAD_TRIGGER_L) != 0;
|
||||
bool btnR = (m_buttons & PAD_TRIGGER_R) != 0;
|
||||
bool btnStart = (m_buttons & PAD_BUTTON_START) != 0;
|
||||
bool btnDpadUp = (m_buttons & PAD_BUTTON_UP) != 0;
|
||||
bool btnDpadDown = (m_buttons & PAD_BUTTON_DOWN) != 0;
|
||||
bool btnDpadLeft = (m_buttons & PAD_BUTTON_LEFT) != 0;
|
||||
bool btnDpadRight = (m_buttons & PAD_BUTTON_RIGHT) != 0;
|
||||
|
||||
// Map to abstract actions
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action1)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action2)] = btnB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action3)] = btnX;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action4)] = btnY;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Jump)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Crouch)] = btnZ;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = btnL;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleRight)] = btnR;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuOpen)] = btnStart;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuBack)] = btnB;
|
||||
|
||||
// Analog triggers for sprint/crouch
|
||||
if (m_leftTrigger > 0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = true;
|
||||
if (m_rightTrigger > 0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Sprint)] = true;
|
||||
|
||||
// D-pad for movement
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = btnDpadUp;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = btnDpadDown;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = btnDpadLeft;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = btnDpadRight;
|
||||
|
||||
// Pressed this frame
|
||||
outInputState.actions[static_cast<int>(InputAction::Action1)] = (buttonsDown & PAD_BUTTON_A) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action2)] = (buttonsDown & PAD_BUTTON_B) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::MenuOpen)] = (buttonsDown & PAD_BUTTON_START) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Jump)] = (buttonsDown & PAD_BUTTON_A) != 0;
|
||||
|
||||
// Analog to digital movement
|
||||
if (std::abs(outInputState.moveAxisY) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisY > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
}
|
||||
if (std::abs(outInputState.moveAxisX) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisX > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float GameCubePlatform::NormalizeAxis(int8_t value) const
|
||||
{
|
||||
// GameCube sticks are -128 to 127
|
||||
float normalized = static_cast<float>(value) / 127.0f;
|
||||
|
||||
// Apply deadzone
|
||||
const float deadzone = 0.15f;
|
||||
if (std::abs(normalized) < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
// Rescale to account for deadzone
|
||||
float sign = (normalized > 0.0f) ? 1.0f : -1.0f;
|
||||
float absValue = std::abs(normalized);
|
||||
return sign * ((absValue - deadzone) / (1.0f - deadzone));
|
||||
}
|
||||
|
||||
float GameCubePlatform::NormalizeTrigger(uint8_t value) const
|
||||
{
|
||||
// GameCube triggers are 0-255
|
||||
float normalized = static_cast<float>(value) / 255.0f;
|
||||
|
||||
// Apply trigger deadzone
|
||||
const float deadzone = 0.1f;
|
||||
if (normalized < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
// Rescale to account for deadzone
|
||||
return (normalized - deadzone) / (1.0f - deadzone);
|
||||
}
|
||||
|
||||
void GameCubePlatform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* GameCubePlatform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* GameCubePlatform::GetUserDataPath() const
|
||||
{
|
||||
return "sd:/WillowVox/userdata";
|
||||
}
|
||||
|
||||
const char* GameCubePlatform::GetAssetsPath() const
|
||||
{
|
||||
return "sd:/WillowVox/assets";
|
||||
}
|
||||
|
||||
const char* GameCubePlatform::GetPlatformName() const
|
||||
{
|
||||
return "Nintendo GameCube";
|
||||
}
|
||||
|
||||
InputDeviceType GameCubePlatform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::GameCubeController;
|
||||
}
|
||||
|
||||
bool GameCubePlatform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "gamepad") == 0) return true;
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_GAMECUBE
|
||||
@@ -44,5 +44,19 @@ namespace WillowVox
|
||||
|
||||
private:
|
||||
std::unique_ptr<GameCubeGraphicsContext> m_graphicsContext;
|
||||
|
||||
// GameCube controller input state
|
||||
uint16_t m_buttons = 0;
|
||||
uint16_t m_prevButtons = 0;
|
||||
float m_mainStickX = 0.0f;
|
||||
float m_mainStickY = 0.0f;
|
||||
float m_cStickX = 0.0f;
|
||||
float m_cStickY = 0.0f;
|
||||
float m_leftTrigger = 0.0f;
|
||||
float m_rightTrigger = 0.0f;
|
||||
|
||||
void UpdateGamepadInput(InputState& outInputState);
|
||||
float NormalizeAxis(int8_t value) const;
|
||||
float NormalizeTrigger(uint8_t value) const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "WiiGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_WII
|
||||
|
||||
// libogc includes
|
||||
#include <gccore.h>
|
||||
#include <ogc/system.h>
|
||||
#include <ogc/video.h>
|
||||
#include <ogc/gx.h>
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
#define DEFAULT_FIFO_SIZE (256 * 1024)
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
WiiGraphicsContext::WiiGraphicsContext()
|
||||
{
|
||||
m_startTime = static_cast<float>(SYS_Time()) / TB_TIMER_CLOCK;
|
||||
}
|
||||
|
||||
WiiGraphicsContext::~WiiGraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool WiiGraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
Logger::EngineLog("Initializing Wii Graphics (GX)");
|
||||
|
||||
// Initialize VIDEO subsystem
|
||||
VIDEO_Init();
|
||||
|
||||
// Get preferred video mode
|
||||
GXRModeObj* rmode = VIDEO_GetPreferredMode(NULL);
|
||||
m_renderMode = rmode;
|
||||
|
||||
m_width = rmode->fbWidth;
|
||||
m_height = rmode->efbHeight;
|
||||
|
||||
Logger::EngineLog("Video mode: %dx%d", m_width, m_height);
|
||||
|
||||
// Allocate framebuffers (double buffering)
|
||||
m_frameBuffer[0] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
|
||||
m_frameBuffer[1] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
|
||||
|
||||
if (!m_frameBuffer[0] || !m_frameBuffer[1])
|
||||
{
|
||||
Logger::EngineError("Failed to allocate framebuffers");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure VIDEO
|
||||
VIDEO_Configure(rmode);
|
||||
VIDEO_SetNextFramebuffer(m_frameBuffer[0]);
|
||||
VIDEO_SetBlack(FALSE);
|
||||
VIDEO_Flush();
|
||||
VIDEO_WaitVSync();
|
||||
if (rmode->viTVMode & VI_NON_INTERLACE)
|
||||
VIDEO_WaitVSync();
|
||||
|
||||
// Allocate GX FIFO buffer
|
||||
m_fifoBuffer = memalign(32, DEFAULT_FIFO_SIZE);
|
||||
if (!m_fifoBuffer)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate GX FIFO buffer");
|
||||
return false;
|
||||
}
|
||||
memset(m_fifoBuffer, 0, DEFAULT_FIFO_SIZE);
|
||||
|
||||
// Initialize GX
|
||||
GX_Init(m_fifoBuffer, DEFAULT_FIFO_SIZE);
|
||||
|
||||
// Set up GX for 3D rendering
|
||||
GX_SetCopyClear((GXColor){26, 26, 26, 255}, 0x00FFFFFF);
|
||||
|
||||
// Set viewport
|
||||
GX_SetViewport(0, 0, rmode->fbWidth, rmode->efbHeight, 0, 1);
|
||||
f32 yscale = GX_GetYScaleFactor(rmode->efbHeight, rmode->xfbHeight);
|
||||
u32 xfbHeight = GX_SetDispCopyYScale(yscale);
|
||||
GX_SetScissor(0, 0, rmode->fbWidth, rmode->efbHeight);
|
||||
GX_SetDispCopySrc(0, 0, rmode->fbWidth, rmode->efbHeight);
|
||||
GX_SetDispCopyDst(rmode->fbWidth, xfbHeight);
|
||||
GX_SetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter);
|
||||
GX_SetFieldMode(rmode->field_rendering, ((rmode->viHeight == 2 * rmode->xfbHeight) ? GX_ENABLE : GX_DISABLE));
|
||||
|
||||
// Set pixel format
|
||||
if (rmode->aa)
|
||||
GX_SetPixelFmt(GX_PF_RGB565_Z16, GX_ZC_LINEAR);
|
||||
else
|
||||
GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR);
|
||||
|
||||
// Enable depth testing
|
||||
GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE);
|
||||
|
||||
// Enable backface culling
|
||||
GX_SetCullMode(GX_CULL_BACK);
|
||||
|
||||
// Enable blending
|
||||
GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR);
|
||||
|
||||
// Set color update
|
||||
GX_SetColorUpdate(GX_TRUE);
|
||||
|
||||
Logger::EngineLog("Wii Graphics initialized (GX, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
if (m_fifoBuffer)
|
||||
{
|
||||
free(m_fifoBuffer);
|
||||
m_fifoBuffer = nullptr;
|
||||
}
|
||||
|
||||
// Framebuffers are managed by libogc, no need to free
|
||||
|
||||
m_initialized = false;
|
||||
Logger::EngineLog("Wii Graphics shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::BeginFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::EndFrame()
|
||||
{
|
||||
// Execute all pending GX commands
|
||||
GX_DrawDone();
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::SwapBuffers()
|
||||
{
|
||||
// Copy EFB to XFB
|
||||
GX_CopyDisp(m_frameBuffer[m_currentFB], GX_TRUE);
|
||||
GX_Flush();
|
||||
|
||||
// Swap framebuffers
|
||||
VIDEO_SetNextFramebuffer(m_frameBuffer[m_currentFB]);
|
||||
VIDEO_Flush();
|
||||
|
||||
if (m_vsyncEnabled)
|
||||
VIDEO_WaitVSync();
|
||||
|
||||
m_currentFB ^= 1; // Toggle between 0 and 1
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
GXColor color = {
|
||||
static_cast<u8>(r * 255),
|
||||
static_cast<u8>(g * 255),
|
||||
static_cast<u8>(b * 255),
|
||||
static_cast<u8>(a * 255)
|
||||
};
|
||||
GX_SetCopyClear(color, 0x00FFFFFF);
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::ClearDepth()
|
||||
{
|
||||
// GX clears depth automatically with GX_CopyDisp
|
||||
}
|
||||
|
||||
bool WiiGraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
}
|
||||
|
||||
bool WiiGraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float WiiGraphicsContext::GetTime() const
|
||||
{
|
||||
float currentTime = static_cast<float>(SYS_Time()) / TB_TIMER_CLOCK;
|
||||
return currentTime - m_startTime;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void WiiGraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
GX_SetViewport(x, y, width, height, 0, 1);
|
||||
GX_SetScissor(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_WII
|
||||
@@ -36,8 +36,16 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 640;
|
||||
int m_height = 480;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// GX-specific framebuffer (using void* to avoid including libogc headers)
|
||||
void* m_fifoBuffer = nullptr; // GX FIFO buffer
|
||||
void* m_frameBuffer[2] = {nullptr, nullptr}; // Double buffering
|
||||
int m_currentFB = 0;
|
||||
void* m_renderMode = nullptr; // GXRModeObj*
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "WiiPlatform.h"
|
||||
#include "WiiGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_WII
|
||||
|
||||
// libogc includes
|
||||
#include <gccore.h>
|
||||
#include <wiiuse/wpad.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
WiiPlatform::WiiPlatform()
|
||||
{
|
||||
}
|
||||
|
||||
WiiPlatform::~WiiPlatform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool WiiPlatform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing Wii Platform (libogc)");
|
||||
|
||||
// Initialize WPAD (Wiimote)
|
||||
WPAD_Init();
|
||||
WPAD_SetDataFormat(WPAD_CHAN_0, WPAD_FMT_BTNS_ACC_IR);
|
||||
WPAD_SetVRes(WPAD_CHAN_0, 640, 480);
|
||||
|
||||
// Initialize graphics
|
||||
m_graphicsContext = std::make_unique<WiiGraphicsContext>();
|
||||
|
||||
Logger::EngineLog("Wii Platform initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiiPlatform::Shutdown()
|
||||
{
|
||||
WPAD_Shutdown();
|
||||
m_graphicsContext.reset();
|
||||
Logger::EngineLog("Wii Platform shutdown");
|
||||
}
|
||||
|
||||
void WiiPlatform::ProcessEvents()
|
||||
{
|
||||
// Wii doesn't have traditional event polling
|
||||
// Input is polled directly via WPAD_ScanPads()
|
||||
}
|
||||
|
||||
void WiiPlatform::PollInput(InputState& outInputState)
|
||||
{
|
||||
UpdateWiimoteInput(outInputState);
|
||||
outInputState.deviceType = InputDeviceType::WiiRemote;
|
||||
}
|
||||
|
||||
void WiiPlatform::UpdateWiimoteInput(InputState& outInputState)
|
||||
{
|
||||
// Store previous button state
|
||||
m_prevButtons = m_buttons;
|
||||
|
||||
// Scan for button presses
|
||||
WPAD_ScanPads();
|
||||
|
||||
// Read Wiimote data
|
||||
u32 pressed = WPAD_ButtonsDown(WPAD_CHAN_0);
|
||||
u32 held = WPAD_ButtonsHeld(WPAD_CHAN_0);
|
||||
m_buttons = held;
|
||||
|
||||
// Get expansion data
|
||||
WPADData* data = WPAD_Data(WPAD_CHAN_0);
|
||||
|
||||
if (data && data->exp.type == WPAD_EXP_NUNCHUK)
|
||||
{
|
||||
// Nunchuk is connected - use for movement
|
||||
joystick_t nunchuk = data->exp.nunchuk.js;
|
||||
m_nunchukX = NormalizeAxis(nunchuk.pos.x, nunchuk.center.x);
|
||||
m_nunchukY = NormalizeAxis(nunchuk.pos.y, nunchuk.center.y);
|
||||
|
||||
// Nunchuk buttons
|
||||
if (data->exp.nunchuk.btns & NUNCHUK_BUTTON_Z)
|
||||
m_buttons |= WPAD_BUTTON_HOME; // Map Z to crouch
|
||||
if (data->exp.nunchuk.btns & NUNCHUK_BUTTON_C)
|
||||
m_buttons |= WPAD_BUTTON_1; // Map C to sprint
|
||||
}
|
||||
else
|
||||
{
|
||||
// No Nunchuk - reset analog values
|
||||
m_nunchukX = 0.0f;
|
||||
m_nunchukY = 0.0f;
|
||||
}
|
||||
|
||||
// Get IR pointer data
|
||||
if (data && data->ir.valid)
|
||||
{
|
||||
m_irValid = true;
|
||||
m_irX = static_cast<float>(data->ir.x) / 640.0f;
|
||||
m_irY = static_cast<float>(data->ir.y) / 480.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_irValid = false;
|
||||
}
|
||||
|
||||
// Map analog sticks
|
||||
outInputState.moveAxisX = m_nunchukX;
|
||||
outInputState.moveAxisY = m_nunchukY;
|
||||
|
||||
// IR pointer for look (if valid)
|
||||
if (m_irValid)
|
||||
{
|
||||
// Center IR to -1..1 range
|
||||
outInputState.lookAxisX = (m_irX - 0.5f) * 200.0f;
|
||||
outInputState.lookAxisY = (m_irY - 0.5f) * 200.0f;
|
||||
}
|
||||
|
||||
// Button mapping (Wiimote)
|
||||
bool btnA = (m_buttons & WPAD_BUTTON_A) != 0;
|
||||
bool btnB = (m_buttons & WPAD_BUTTON_B) != 0;
|
||||
bool btn1 = (m_buttons & WPAD_BUTTON_1) != 0;
|
||||
bool btn2 = (m_buttons & WPAD_BUTTON_2) != 0;
|
||||
bool btnHome = (m_buttons & WPAD_BUTTON_HOME) != 0;
|
||||
bool btnPlus = (m_buttons & WPAD_BUTTON_PLUS) != 0;
|
||||
bool btnMinus = (m_buttons & WPAD_BUTTON_MINUS) != 0;
|
||||
bool btnUp = (m_buttons & WPAD_BUTTON_UP) != 0;
|
||||
bool btnDown = (m_buttons & WPAD_BUTTON_DOWN) != 0;
|
||||
bool btnLeft = (m_buttons & WPAD_BUTTON_LEFT) != 0;
|
||||
bool btnRight = (m_buttons & WPAD_BUTTON_RIGHT) != 0;
|
||||
|
||||
// Map to abstract actions
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action1)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action2)] = btnB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action3)] = btn1;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action4)] = btn2;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Jump)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuOpen)] = btnHome;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuBack)] = btnB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = btnMinus;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleRight)] = btnPlus;
|
||||
|
||||
// D-pad for movement
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = btnUp;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = btnDown;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = btnLeft;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = btnRight;
|
||||
|
||||
// Pressed this frame
|
||||
outInputState.actions[static_cast<int>(InputAction::Action1)] = (pressed & WPAD_BUTTON_A) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action2)] = (pressed & WPAD_BUTTON_B) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::MenuOpen)] = (pressed & WPAD_BUTTON_HOME) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Jump)] = (pressed & WPAD_BUTTON_A) != 0;
|
||||
|
||||
// Analog to digital movement (Nunchuk)
|
||||
if (std::abs(outInputState.moveAxisY) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisY > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
}
|
||||
if (std::abs(outInputState.moveAxisX) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisX > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float WiiPlatform::NormalizeAxis(uint8_t value, uint8_t center) const
|
||||
{
|
||||
// Nunchuk stick is 0-255, center varies by device
|
||||
float normalized = (static_cast<float>(value) - static_cast<float>(center)) / 128.0f;
|
||||
|
||||
// Apply deadzone
|
||||
const float deadzone = 0.15f;
|
||||
if (std::abs(normalized) < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
void WiiPlatform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* WiiPlatform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* WiiPlatform::GetUserDataPath() const
|
||||
{
|
||||
return "sd:/WillowVox/userdata";
|
||||
}
|
||||
|
||||
const char* WiiPlatform::GetAssetsPath() const
|
||||
{
|
||||
return "sd:/WillowVox/assets";
|
||||
}
|
||||
|
||||
const char* WiiPlatform::GetPlatformName() const
|
||||
{
|
||||
return "Nintendo Wii";
|
||||
}
|
||||
|
||||
InputDeviceType WiiPlatform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::WiiRemote;
|
||||
}
|
||||
|
||||
bool WiiPlatform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "gamepad") == 0) return true;
|
||||
if (strcmp(featureName, "motion") == 0) return true;
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_WII
|
||||
@@ -43,6 +43,17 @@ namespace WillowVox
|
||||
|
||||
private:
|
||||
std::unique_ptr<WiiGraphicsContext> m_graphicsContext;
|
||||
// Wii-specific input handling
|
||||
|
||||
// Wiimote + Nunchuk input state
|
||||
uint32_t m_buttons = 0;
|
||||
uint32_t m_prevButtons = 0;
|
||||
float m_nunchukX = 0.0f;
|
||||
float m_nunchukY = 0.0f;
|
||||
float m_irX = 0.0f;
|
||||
float m_irY = 0.0f;
|
||||
bool m_irValid = false;
|
||||
|
||||
void UpdateWiimoteInput(InputState& outInputState);
|
||||
float NormalizeAxis(uint8_t value, uint8_t center = 128) const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "WiiUGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_WIIU
|
||||
|
||||
// wut library includes (Wii U homebrew)
|
||||
#include <coreinit/time.h>
|
||||
#include <coreinit/cache.h>
|
||||
#include <coreinit/memheap.h>
|
||||
#include <coreinit/screen.h>
|
||||
#include <gx2/display.h>
|
||||
#include <gx2/event.h>
|
||||
#include <gx2/state.h>
|
||||
#include <gx2/clear.h>
|
||||
#include <gx2/context.h>
|
||||
#include <gx2/swap.h>
|
||||
#include <gx2/enum.h>
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
WiiUGraphicsContext::WiiUGraphicsContext()
|
||||
{
|
||||
m_startTime = static_cast<float>(OSGetTime()) / static_cast<float>(OSTimerClockSpeed);
|
||||
}
|
||||
|
||||
WiiUGraphicsContext::~WiiUGraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool WiiUGraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
Logger::EngineLog("Initializing Wii U Graphics (GX2)");
|
||||
|
||||
// Get TV scan buffer dimensions
|
||||
m_width = 1280; // Wii U TV default
|
||||
m_height = 720;
|
||||
|
||||
// Initialize GX2
|
||||
GX2Init(NULL);
|
||||
|
||||
// Allocate color buffer
|
||||
GX2ColorBuffer* colorBuffer = (GX2ColorBuffer*)memalign(0x100, sizeof(GX2ColorBuffer));
|
||||
if (!colorBuffer)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate color buffer");
|
||||
return false;
|
||||
}
|
||||
memset(colorBuffer, 0, sizeof(GX2ColorBuffer));
|
||||
m_colorBuffer = colorBuffer;
|
||||
|
||||
// Setup color buffer
|
||||
colorBuffer->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
|
||||
colorBuffer->surface.width = m_width;
|
||||
colorBuffer->surface.height = m_height;
|
||||
colorBuffer->surface.depth = 1;
|
||||
colorBuffer->surface.mipLevels = 1;
|
||||
colorBuffer->surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8;
|
||||
colorBuffer->surface.use = GX2_SURFACE_USE_TEXTURE_COLOR_BUFFER_TV;
|
||||
colorBuffer->viewNumSlices = 1;
|
||||
|
||||
// Calculate surface size
|
||||
GX2CalcSurfaceSizeAndAlignment(&colorBuffer->surface);
|
||||
GX2InitColorBufferRegs(colorBuffer);
|
||||
|
||||
// Allocate color buffer surface
|
||||
colorBuffer->surface.image = memalign(colorBuffer->surface.alignment, colorBuffer->surface.imageSize);
|
||||
if (!colorBuffer->surface.image)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate color buffer surface");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate depth buffer
|
||||
GX2DepthBuffer* depthBuffer = (GX2DepthBuffer*)memalign(0x100, sizeof(GX2DepthBuffer));
|
||||
if (!depthBuffer)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate depth buffer");
|
||||
return false;
|
||||
}
|
||||
memset(depthBuffer, 0, sizeof(GX2DepthBuffer));
|
||||
m_depthBuffer = depthBuffer;
|
||||
|
||||
// Setup depth buffer
|
||||
depthBuffer->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D;
|
||||
depthBuffer->surface.width = m_width;
|
||||
depthBuffer->surface.height = m_height;
|
||||
depthBuffer->surface.depth = 1;
|
||||
depthBuffer->surface.mipLevels = 1;
|
||||
depthBuffer->surface.format = GX2_SURFACE_FORMAT_FLOAT_D24_S8;
|
||||
depthBuffer->surface.use = GX2_SURFACE_USE_DEPTH_BUFFER;
|
||||
depthBuffer->viewNumSlices = 1;
|
||||
|
||||
// Calculate depth surface size
|
||||
GX2CalcSurfaceSizeAndAlignment(&depthBuffer->surface);
|
||||
GX2InitDepthBufferRegs(depthBuffer);
|
||||
|
||||
// Allocate depth buffer surface
|
||||
depthBuffer->surface.image = memalign(depthBuffer->surface.alignment, depthBuffer->surface.imageSize);
|
||||
if (!depthBuffer->surface.image)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate depth buffer surface");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate context state
|
||||
GX2ContextState* contextState = (GX2ContextState*)memalign(GX2_CONTEXT_STATE_ALIGNMENT, sizeof(GX2ContextState));
|
||||
if (!contextState)
|
||||
{
|
||||
Logger::EngineError("Failed to allocate context state");
|
||||
return false;
|
||||
}
|
||||
GX2SetupContextStateEx(contextState, GX2_TRUE);
|
||||
m_contextState = contextState;
|
||||
|
||||
// Set context state
|
||||
GX2SetContextState(contextState);
|
||||
|
||||
// Set color and depth buffers
|
||||
GX2SetColorBuffer(colorBuffer, GX2_RENDER_TARGET_0);
|
||||
GX2SetDepthBuffer(depthBuffer);
|
||||
|
||||
// Set viewport
|
||||
GX2SetViewport(0, 0, m_width, m_height, 0.0f, 1.0f);
|
||||
GX2SetScissor(0, 0, m_width, m_height);
|
||||
|
||||
// Enable depth test
|
||||
GX2SetDepthOnlyControl(GX2_ENABLE, GX2_ENABLE, GX2_COMPARE_FUNC_LEQUAL);
|
||||
|
||||
// Enable backface culling
|
||||
GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, GX2_ENABLE, GX2_DISABLE);
|
||||
|
||||
// Enable alpha blending
|
||||
GX2SetColorControl(GX2_LOGIC_OP_COPY, GX2_ENABLE, GX2_DISABLE, GX2_ENABLE);
|
||||
GX2SetBlendControl(GX2_RENDER_TARGET_0, GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA,
|
||||
GX2_BLEND_COMBINE_MODE_ADD, GX2_ENABLE,
|
||||
GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA,
|
||||
GX2_BLEND_COMBINE_MODE_ADD);
|
||||
|
||||
Logger::EngineLog("Wii U Graphics initialized (GX2, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
if (m_colorBuffer)
|
||||
{
|
||||
GX2ColorBuffer* cb = (GX2ColorBuffer*)m_colorBuffer;
|
||||
if (cb->surface.image)
|
||||
free(cb->surface.image);
|
||||
free(cb);
|
||||
m_colorBuffer = nullptr;
|
||||
}
|
||||
|
||||
if (m_depthBuffer)
|
||||
{
|
||||
GX2DepthBuffer* db = (GX2DepthBuffer*)m_depthBuffer;
|
||||
if (db->surface.image)
|
||||
free(db->surface.image);
|
||||
free(db);
|
||||
m_depthBuffer = nullptr;
|
||||
}
|
||||
|
||||
if (m_contextState)
|
||||
{
|
||||
free(m_contextState);
|
||||
m_contextState = nullptr;
|
||||
}
|
||||
|
||||
GX2Shutdown();
|
||||
m_initialized = false;
|
||||
Logger::EngineLog("Wii U Graphics shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::BeginFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::EndFrame()
|
||||
{
|
||||
// Flush GPU
|
||||
GX2Flush();
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::SwapBuffers()
|
||||
{
|
||||
// Copy color buffer to TV scan buffer
|
||||
GX2CopyColorBufferToScanBuffer((GX2ColorBuffer*)m_colorBuffer, GX2_SCAN_TARGET_TV);
|
||||
|
||||
// Swap scan buffers
|
||||
GX2SwapScanBuffers();
|
||||
|
||||
if (m_vsyncEnabled)
|
||||
{
|
||||
GX2WaitForVsync();
|
||||
}
|
||||
|
||||
// Flush and wait
|
||||
GX2Flush();
|
||||
GX2DrawDone();
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
GX2ClearColor((GX2ColorBuffer*)m_colorBuffer, r, g, b, a);
|
||||
GX2ClearDepthStencilEx((GX2DepthBuffer*)m_depthBuffer, 1.0f, 0, GX2_CLEAR_FLAGS_BOTH);
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::ClearDepth()
|
||||
{
|
||||
GX2ClearDepthStencilEx((GX2DepthBuffer*)m_depthBuffer, 1.0f, 0, GX2_CLEAR_FLAGS_DEPTH);
|
||||
}
|
||||
|
||||
bool WiiUGraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
}
|
||||
|
||||
bool WiiUGraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float WiiUGraphicsContext::GetTime() const
|
||||
{
|
||||
float currentTime = static_cast<float>(OSGetTime()) / static_cast<float>(OSTimerClockSpeed);
|
||||
return currentTime - m_startTime;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void WiiUGraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
GX2SetViewport(x, y, width, height, 0.0f, 1.0f);
|
||||
GX2SetScissor(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_WIIU
|
||||
@@ -36,8 +36,16 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 1920;
|
||||
int m_height = 1080;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// GX2-specific (using void* to avoid including wut headers)
|
||||
void* m_colorBuffer = nullptr; // GX2ColorBuffer*
|
||||
void* m_depthBuffer = nullptr; // GX2DepthBuffer*
|
||||
void* m_contextState = nullptr; // GX2ContextState*
|
||||
int m_currentTV = 0; // TV scan buffer index
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "WiiUPlatform.h"
|
||||
#include "WiiUGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_WIIU
|
||||
|
||||
// wut library includes (Wii U homebrew)
|
||||
#include <vpad/input.h>
|
||||
#include <padscore/kpad.h>
|
||||
#include <coreinit/time.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
WiiUPlatform::WiiUPlatform()
|
||||
{
|
||||
}
|
||||
|
||||
WiiUPlatform::~WiiUPlatform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool WiiUPlatform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing Wii U Platform (wut)");
|
||||
|
||||
// Initialize VPAD (GamePad)
|
||||
VPADInit();
|
||||
|
||||
// Initialize KPAD (Pro Controller / Wiimote)
|
||||
KPADInit();
|
||||
|
||||
// Initialize graphics
|
||||
m_graphicsContext = std::make_unique<WiiUGraphicsContext>();
|
||||
|
||||
Logger::EngineLog("Wii U Platform initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiiUPlatform::Shutdown()
|
||||
{
|
||||
KPADShutdown();
|
||||
VPADShutdown();
|
||||
m_graphicsContext.reset();
|
||||
Logger::EngineLog("Wii U Platform shutdown");
|
||||
}
|
||||
|
||||
void WiiUPlatform::ProcessEvents()
|
||||
{
|
||||
// Wii U doesn't have traditional event polling
|
||||
// Input is polled directly via VPADRead()
|
||||
}
|
||||
|
||||
void WiiUPlatform::PollInput(InputState& outInputState)
|
||||
{
|
||||
UpdateGamepadInput(outInputState);
|
||||
outInputState.deviceType = InputDeviceType::WiiUGamePad;
|
||||
}
|
||||
|
||||
void WiiUPlatform::UpdateGamepadInput(InputState& outInputState)
|
||||
{
|
||||
// Store previous button state
|
||||
m_prevButtons = m_buttons;
|
||||
|
||||
// Read GamePad data (VPAD)
|
||||
VPADStatus vpadStatus;
|
||||
VPADReadError error;
|
||||
VPADRead(VPAD_CHAN_0, &vpadStatus, 1, &error);
|
||||
|
||||
if (error == VPAD_READ_SUCCESS)
|
||||
{
|
||||
// Store button state
|
||||
m_buttons = vpadStatus.hold;
|
||||
|
||||
// Read analog sticks
|
||||
m_lstickX = NormalizeAxis(vpadStatus.leftStick.x, -1.0f, 1.0f);
|
||||
m_lstickY = NormalizeAxis(vpadStatus.leftStick.y, -1.0f, 1.0f);
|
||||
m_rstickX = NormalizeAxis(vpadStatus.rightStick.x, -1.0f, 1.0f);
|
||||
m_rstickY = NormalizeAxis(vpadStatus.rightStick.y, -1.0f, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No input, reset to defaults
|
||||
m_buttons = 0;
|
||||
m_lstickX = 0.0f;
|
||||
m_lstickY = 0.0f;
|
||||
m_rstickX = 0.0f;
|
||||
m_rstickY = 0.0f;
|
||||
}
|
||||
|
||||
// Map analog sticks
|
||||
outInputState.moveAxisX = m_lstickX;
|
||||
outInputState.moveAxisY = m_lstickY;
|
||||
outInputState.lookAxisX = m_rstickX * 100.0f;
|
||||
outInputState.lookAxisY = -m_rstickY * 100.0f; // Invert Y for camera
|
||||
|
||||
// Button mapping (Wii U GamePad - similar to Pro Controller)
|
||||
bool btnA = (m_buttons & VPAD_BUTTON_A) != 0;
|
||||
bool btnB = (m_buttons & VPAD_BUTTON_B) != 0;
|
||||
bool btnX = (m_buttons & VPAD_BUTTON_X) != 0;
|
||||
bool btnY = (m_buttons & VPAD_BUTTON_Y) != 0;
|
||||
bool btnL = (m_buttons & VPAD_BUTTON_L) != 0;
|
||||
bool btnR = (m_buttons & VPAD_BUTTON_R) != 0;
|
||||
bool btnZL = (m_buttons & VPAD_BUTTON_ZL) != 0;
|
||||
bool btnZR = (m_buttons & VPAD_BUTTON_ZR) != 0;
|
||||
bool btnPlus = (m_buttons & VPAD_BUTTON_PLUS) != 0;
|
||||
bool btnMinus = (m_buttons & VPAD_BUTTON_MINUS) != 0;
|
||||
bool btnHome = (m_buttons & VPAD_BUTTON_HOME) != 0;
|
||||
bool btnDpadUp = (m_buttons & VPAD_BUTTON_UP) != 0;
|
||||
bool btnDpadDown = (m_buttons & VPAD_BUTTON_DOWN) != 0;
|
||||
bool btnDpadLeft = (m_buttons & VPAD_BUTTON_LEFT) != 0;
|
||||
bool btnDpadRight = (m_buttons & VPAD_BUTTON_RIGHT) != 0;
|
||||
|
||||
// Map to abstract actions
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action1)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action2)] = btnB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action3)] = btnX;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action4)] = btnY;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Jump)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Crouch)] = btnZL;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Sprint)] = btnZR;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = btnL;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleRight)] = btnR;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuOpen)] = btnPlus || btnHome;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuBack)] = btnB || btnMinus;
|
||||
|
||||
// D-pad for movement
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = btnDpadUp;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = btnDpadDown;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = btnDpadLeft;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = btnDpadRight;
|
||||
|
||||
// Pressed this frame
|
||||
uint32_t pressed = m_buttons & ~m_prevButtons;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action1)] = (pressed & VPAD_BUTTON_A) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action2)] = (pressed & VPAD_BUTTON_B) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::MenuOpen)] = (pressed & VPAD_BUTTON_PLUS) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Jump)] = (pressed & VPAD_BUTTON_A) != 0;
|
||||
|
||||
// Analog to digital movement
|
||||
if (std::abs(outInputState.moveAxisY) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisY > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
}
|
||||
if (std::abs(outInputState.moveAxisX) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisX > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float WiiUPlatform::NormalizeAxis(float value, float min, float max) const
|
||||
{
|
||||
// VPAD sticks are already -1.0 to 1.0
|
||||
// Apply deadzone
|
||||
const float deadzone = 0.15f;
|
||||
if (std::abs(value) < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
// Rescale to account for deadzone
|
||||
float sign = (value > 0.0f) ? 1.0f : -1.0f;
|
||||
float absValue = std::abs(value);
|
||||
return sign * ((absValue - deadzone) / (1.0f - deadzone));
|
||||
}
|
||||
|
||||
void WiiUPlatform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* WiiUPlatform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* WiiUPlatform::GetUserDataPath() const
|
||||
{
|
||||
return "fs:/vol/external01/WillowVox/userdata";
|
||||
}
|
||||
|
||||
const char* WiiUPlatform::GetAssetsPath() const
|
||||
{
|
||||
return "fs:/vol/content/WillowVox/assets";
|
||||
}
|
||||
|
||||
const char* WiiUPlatform::GetPlatformName() const
|
||||
{
|
||||
return "Nintendo Wii U";
|
||||
}
|
||||
|
||||
InputDeviceType WiiUPlatform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::WiiUGamePad;
|
||||
}
|
||||
|
||||
bool WiiUPlatform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "gamepad") == 0) return true;
|
||||
if (strcmp(featureName, "touchscreen") == 0) return true; // GamePad has touchscreen
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_WIIU
|
||||
@@ -36,5 +36,16 @@ namespace WillowVox
|
||||
|
||||
private:
|
||||
std::unique_ptr<WiiUGraphicsContext> m_graphicsContext;
|
||||
|
||||
// Wii U GamePad/Pro Controller input state
|
||||
uint32_t m_buttons = 0;
|
||||
uint32_t m_prevButtons = 0;
|
||||
float m_lstickX = 0.0f;
|
||||
float m_lstickY = 0.0f;
|
||||
float m_rstickX = 0.0f;
|
||||
float m_rstickY = 0.0f;
|
||||
|
||||
void UpdateGamepadInput(InputState& outInputState);
|
||||
float NormalizeAxis(float value, float min, float max) const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "PS4GraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_PS4
|
||||
|
||||
// OpenOrbis includes
|
||||
#include <orbis/Pigletv2VSH.h>
|
||||
#include <orbis/VideoOut.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
PS4GraphicsContext::PS4GraphicsContext()
|
||||
{
|
||||
m_startTime = sceKernelGetProcessTime() / 1000000000.0f;
|
||||
}
|
||||
|
||||
PS4GraphicsContext::~PS4GraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool PS4GraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
|
||||
Logger::EngineLog("Initializing PS4 Graphics (OpenGL via piglet)");
|
||||
|
||||
// Initialize video out
|
||||
int ret = sceVideoOutOpen(ORBIS_USER_SERVICE_USER_ID_SYSTEM, ORBIS_VIDEO_OUT_BUS_TYPE_MAIN, 0, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
Logger::EngineError("Failed to open video out: 0x%08X", ret);
|
||||
return false;
|
||||
}
|
||||
m_videoHandle = ret;
|
||||
|
||||
// Initialize piglet (OpenGL wrapper for PS4)
|
||||
ret = scePigletSetConfigurationVSH(SCE_PIGLET_CONFIGURATION_VSH_API_VERSION,
|
||||
SCE_PIGLET_API_VERSION_GLES20);
|
||||
if (ret != 0)
|
||||
{
|
||||
Logger::EngineError("Failed to set piglet configuration: 0x%08X", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get EGL display
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY)
|
||||
{
|
||||
Logger::EngineError("eglGetDisplay failed");
|
||||
return false;
|
||||
}
|
||||
m_eglDisplay = display;
|
||||
|
||||
// Initialize EGL
|
||||
if (!eglInitialize(display, nullptr, nullptr))
|
||||
{
|
||||
Logger::EngineError("eglInitialize failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Choose EGL config
|
||||
const EGLint attribs[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24,
|
||||
EGL_STENCIL_SIZE, 8,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLConfig config;
|
||||
EGLint numConfigs;
|
||||
if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs))
|
||||
{
|
||||
Logger::EngineError("eglChooseConfig failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create EGL window surface
|
||||
EGLSurface surface = eglCreateWindowSurface(display, config,
|
||||
(EGLNativeWindowType)m_videoHandle, nullptr);
|
||||
if (surface == EGL_NO_SURFACE)
|
||||
{
|
||||
Logger::EngineError("eglCreateWindowSurface failed");
|
||||
return false;
|
||||
}
|
||||
m_eglSurface = surface;
|
||||
|
||||
// Create EGL context
|
||||
const EGLint contextAttribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT)
|
||||
{
|
||||
Logger::EngineError("eglCreateContext failed");
|
||||
return false;
|
||||
}
|
||||
m_eglContext = context;
|
||||
|
||||
// Make context current
|
||||
if (!eglMakeCurrent(display, surface, surface, context))
|
||||
{
|
||||
Logger::EngineError("eglMakeCurrent failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get actual surface size
|
||||
eglQuerySurface(display, surface, EGL_WIDTH, &m_width);
|
||||
eglQuerySurface(display, surface, EGL_HEIGHT, &m_height);
|
||||
|
||||
// Enable OpenGL ES features
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Logger::EngineLog("PS4 Graphics initialized (OpenGL ES 2.0, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
if (m_eglDisplay != EGL_NO_DISPLAY)
|
||||
{
|
||||
eglMakeCurrent((EGLDisplay)m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
|
||||
if (m_eglContext != EGL_NO_CONTEXT)
|
||||
{
|
||||
eglDestroyContext((EGLDisplay)m_eglDisplay, (EGLContext)m_eglContext);
|
||||
m_eglContext = EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
if (m_eglSurface != EGL_NO_SURFACE)
|
||||
{
|
||||
eglDestroySurface((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface);
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
eglTerminate((EGLDisplay)m_eglDisplay);
|
||||
m_eglDisplay = EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
if (m_videoHandle >= 0)
|
||||
{
|
||||
sceVideoOutClose(m_videoHandle);
|
||||
m_videoHandle = -1;
|
||||
}
|
||||
|
||||
m_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::BeginFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::EndFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::SwapBuffers()
|
||||
{
|
||||
if (m_eglDisplay && m_eglSurface)
|
||||
{
|
||||
eglSwapBuffers((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface);
|
||||
}
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
glClearColor(r, g, b, a);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::ClearDepth()
|
||||
{
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
bool PS4GraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
if (m_eglDisplay)
|
||||
{
|
||||
eglSwapInterval((EGLDisplay)m_eglDisplay, enabled ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool PS4GraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float PS4GraphicsContext::GetTime() const
|
||||
{
|
||||
float currentTime = sceKernelGetProcessTime() / 1000000000.0f;
|
||||
return currentTime - m_startTime;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void PS4GraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
glViewport(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_PS4
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* PS4 Graphics Context (GNM)
|
||||
* PS4 Graphics Context (OpenGL ES via piglet)
|
||||
*
|
||||
* PS4 uses GNM (low-level graphics API) via OpenOrbis SDK.
|
||||
* This is a template implementation.
|
||||
* PS4 uses OpenGL ES 2.0 via piglet library (OpenOrbis SDK).
|
||||
* This is easier than GNM and provides good compatibility.
|
||||
*/
|
||||
class PS4GraphicsContext : public IGraphicsContext
|
||||
{
|
||||
@@ -36,8 +36,16 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 1920;
|
||||
int m_height = 1080;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// PS4-specific handles (using void* to avoid including headers)
|
||||
int m_videoHandle = -1;
|
||||
void* m_eglDisplay = nullptr;
|
||||
void* m_eglSurface = nullptr;
|
||||
void* m_eglContext = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "PS4Platform.h"
|
||||
#include "PS4GraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_PS4
|
||||
|
||||
// OpenOrbis includes
|
||||
#include <orbis/Pad.h>
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/libkernel.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
PS4Platform::PS4Platform()
|
||||
{
|
||||
}
|
||||
|
||||
PS4Platform::~PS4Platform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool PS4Platform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing PS4 Platform (OpenOrbis)");
|
||||
|
||||
// Initialize user service
|
||||
int ret = sceUserServiceInitialize(NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
Logger::EngineError("Failed to initialize user service: 0x%08X", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get initial user ID
|
||||
ret = sceUserServiceGetInitialUser(&m_userId);
|
||||
if (ret != 0)
|
||||
{
|
||||
Logger::EngineError("Failed to get initial user: 0x%08X", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize pad library
|
||||
ret = scePadInit();
|
||||
if (ret != 0)
|
||||
{
|
||||
Logger::EngineError("Failed to initialize pad: 0x%08X", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open pad for user
|
||||
m_padHandle = scePadOpen(m_userId, 0, 0, NULL);
|
||||
if (m_padHandle < 0)
|
||||
{
|
||||
Logger::EngineError("Failed to open pad: 0x%08X", m_padHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize graphics
|
||||
m_graphicsContext = std::make_unique<PS4GraphicsContext>();
|
||||
|
||||
Logger::EngineLog("PS4 Platform initialized (User ID: %d, Pad: %d)", m_userId, m_padHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PS4Platform::Shutdown()
|
||||
{
|
||||
if (m_padHandle >= 0)
|
||||
{
|
||||
scePadClose(m_padHandle);
|
||||
m_padHandle = -1;
|
||||
}
|
||||
|
||||
m_graphicsContext.reset();
|
||||
|
||||
Logger::EngineLog("PS4 Platform shutdown");
|
||||
}
|
||||
|
||||
void PS4Platform::ProcessEvents()
|
||||
{
|
||||
// PS4 doesn't have traditional event polling like desktop
|
||||
// Events are handled through system callbacks
|
||||
}
|
||||
|
||||
void PS4Platform::PollInput(InputState& outInputState)
|
||||
{
|
||||
UpdateGamepadInput(outInputState);
|
||||
outInputState.deviceType = InputDeviceType::PSController;
|
||||
}
|
||||
|
||||
void PS4Platform::UpdateGamepadInput(InputState& outInputState)
|
||||
{
|
||||
if (m_padHandle < 0)
|
||||
return;
|
||||
|
||||
// Store previous button state
|
||||
m_prevButtons = m_buttons;
|
||||
|
||||
// Read pad data
|
||||
ScePadData padData;
|
||||
int ret = scePadReadState(m_padHandle, &padData);
|
||||
|
||||
if (ret == 0 && padData.connected)
|
||||
{
|
||||
// Get button states
|
||||
m_buttons = padData.buttons;
|
||||
|
||||
// Get analog stick values
|
||||
m_lstickX = padData.leftStick.x;
|
||||
m_lstickY = padData.leftStick.y;
|
||||
m_rstickX = padData.rightStick.x;
|
||||
m_rstickY = padData.rightStick.y;
|
||||
|
||||
// Trigger values
|
||||
m_l2Value = padData.analogButtons.l2;
|
||||
m_r2Value = padData.analogButtons.r2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Controller disconnected, reset to defaults
|
||||
m_buttons = 0;
|
||||
m_lstickX = 128;
|
||||
m_lstickY = 128;
|
||||
m_rstickX = 128;
|
||||
m_rstickY = 128;
|
||||
m_l2Value = 0;
|
||||
m_r2Value = 0;
|
||||
}
|
||||
|
||||
// Analog sticks
|
||||
outInputState.moveAxisX = NormalizeAxis(m_lstickX);
|
||||
outInputState.moveAxisY = -NormalizeAxis(m_lstickY);
|
||||
outInputState.lookAxisX = NormalizeAxis(m_rstickX) * 100.0f;
|
||||
outInputState.lookAxisY = -NormalizeAxis(m_rstickY) * 100.0f;
|
||||
|
||||
// Button mapping (DualShock 4)
|
||||
bool btnCross = (m_buttons & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
bool btnCircle = (m_buttons & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
bool btnSquare = (m_buttons & SCE_PAD_BUTTON_SQUARE) != 0;
|
||||
bool btnTriangle = (m_buttons & SCE_PAD_BUTTON_TRIANGLE) != 0;
|
||||
bool btnL1 = (m_buttons & SCE_PAD_BUTTON_L1) != 0;
|
||||
bool btnR1 = (m_buttons & SCE_PAD_BUTTON_R1) != 0;
|
||||
bool btnL2 = m_l2Value > 64; // Digital threshold for analog trigger
|
||||
bool btnR2 = m_r2Value > 64;
|
||||
bool btnOptions = (m_buttons & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
bool btnTouchPad = (m_buttons & SCE_PAD_BUTTON_TOUCH_PAD) != 0;
|
||||
|
||||
// Map to abstract actions
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action1)] = btnCross;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action2)] = btnCircle;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action3)] = btnSquare;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action4)] = btnTriangle;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Jump)] = btnCross;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Crouch)] = btnL2;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Sprint)] = btnR2;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = btnL1;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleRight)] = btnR1;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuOpen)] = btnOptions;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuBack)] = btnCircle;
|
||||
|
||||
// Pressed this frame
|
||||
uint32_t pressed = m_buttons & ~m_prevButtons;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action1)] = (pressed & SCE_PAD_BUTTON_CROSS) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action2)] = (pressed & SCE_PAD_BUTTON_CIRCLE) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::MenuOpen)] = (pressed & SCE_PAD_BUTTON_OPTIONS) != 0;
|
||||
|
||||
// Analog to digital movement
|
||||
if (std::abs(outInputState.moveAxisY) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisY > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
}
|
||||
if (std::abs(outInputState.moveAxisX) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisX > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float PS4Platform::NormalizeAxis(uint8_t value) const
|
||||
{
|
||||
// PS4 pad axes are 0-255, center at 128
|
||||
float normalized = (static_cast<float>(value) - 128.0f) / 128.0f;
|
||||
|
||||
// Apply deadzone
|
||||
const float deadzone = 0.15f;
|
||||
if (std::abs(normalized) < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
void PS4Platform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* PS4Platform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* PS4Platform::GetUserDataPath() const
|
||||
{
|
||||
return "/data/WillowVox/userdata";
|
||||
}
|
||||
|
||||
const char* PS4Platform::GetAssetsPath() const
|
||||
{
|
||||
return "/app0/assets";
|
||||
}
|
||||
|
||||
const char* PS4Platform::GetPlatformName() const
|
||||
{
|
||||
return "PlayStation 4";
|
||||
}
|
||||
|
||||
InputDeviceType PS4Platform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::PSController;
|
||||
}
|
||||
|
||||
bool PS4Platform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "gamepad") == 0) return true;
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void PS4Platform::SetVibration(int playerIndex, float lowFrequency, float highFrequency)
|
||||
{
|
||||
if (m_padHandle < 0)
|
||||
return;
|
||||
|
||||
// PS4 uses scePadSetVibration
|
||||
// Values are 0-255
|
||||
ScePadVibrationParam vibParam;
|
||||
vibParam.largeMotor = static_cast<uint8_t>(lowFrequency * 255.0f);
|
||||
vibParam.smallMotor = static_cast<uint8_t>(highFrequency * 255.0f);
|
||||
|
||||
scePadSetVibration(m_padHandle, &vibParam);
|
||||
}
|
||||
|
||||
void PS4Platform::StopVibration(int playerIndex)
|
||||
{
|
||||
SetVibration(playerIndex, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_PS4
|
||||
@@ -10,11 +10,18 @@ namespace WillowVox
|
||||
/**
|
||||
* PlayStation 4 Platform (OpenOrbis Homebrew)
|
||||
*
|
||||
* Controller: DualShock 4 (same mapping as PS3)
|
||||
* Graphics: GNM (low-level) or Gnm wrapper
|
||||
* Input: libSceUserService + libScePad
|
||||
* Controller: DualShock 4
|
||||
* - Cross (X): Action1 / Jump
|
||||
* - Circle: Action2 / MenuBack
|
||||
* - Square: Action3
|
||||
* - Triangle: Action4
|
||||
* - L1/R1: Cycle items
|
||||
* - L2/R2: Crouch/Sprint
|
||||
* - Options: MenuOpen
|
||||
* - TouchPad: Special action
|
||||
*
|
||||
* See PS3Platform for controller mapping (similar button layout)
|
||||
* Graphics: OpenGL via piglet (easier than GNM)
|
||||
* Input: libSceUserService + libScePad (OpenOrbis)
|
||||
*/
|
||||
class PS4Platform : public IPlatform
|
||||
{
|
||||
@@ -34,8 +41,27 @@ namespace WillowVox
|
||||
InputDeviceType GetPrimaryInputDevice() const override;
|
||||
bool HasFeature(const char* featureName) const override;
|
||||
|
||||
void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override;
|
||||
void StopVibration(int playerIndex) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<PS4GraphicsContext> m_graphicsContext;
|
||||
// PS4-specific pad handling (similar to PS3)
|
||||
|
||||
// PS4-specific handles
|
||||
int m_userId = 0;
|
||||
int m_padHandle = -1;
|
||||
|
||||
// Pad state
|
||||
uint32_t m_buttons = 0;
|
||||
uint32_t m_prevButtons = 0;
|
||||
uint8_t m_lstickX = 128;
|
||||
uint8_t m_lstickY = 128;
|
||||
uint8_t m_rstickX = 128;
|
||||
uint8_t m_rstickY = 128;
|
||||
uint8_t m_l2Value = 0;
|
||||
uint8_t m_r2Value = 0;
|
||||
|
||||
void UpdateGamepadInput(InputState& outInputState);
|
||||
float NormalizeAxis(uint8_t value) const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "XboxSeriesGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_XBOX
|
||||
|
||||
// UWP includes
|
||||
#include <Windows.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
// ANGLE includes (OpenGL ES via DirectX)
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <EGL/eglplatform.h>
|
||||
#include <GLES3/gl3.h>
|
||||
|
||||
using namespace Microsoft::WRL;
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
XboxSeriesGraphicsContext::XboxSeriesGraphicsContext()
|
||||
{
|
||||
m_startTime = static_cast<float>(GetTickCount64()) / 1000.0f;
|
||||
}
|
||||
|
||||
XboxSeriesGraphicsContext::~XboxSeriesGraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool XboxSeriesGraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
|
||||
Logger::EngineLog("Initializing Xbox Graphics (OpenGL ES via ANGLE)");
|
||||
|
||||
// Get EGL display (ANGLE will translate to DirectX)
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY)
|
||||
{
|
||||
Logger::EngineError("eglGetDisplay failed");
|
||||
return false;
|
||||
}
|
||||
m_eglDisplay = display;
|
||||
|
||||
// Initialize EGL
|
||||
EGLint major, minor;
|
||||
if (!eglInitialize(display, &major, &minor))
|
||||
{
|
||||
Logger::EngineError("eglInitialize failed");
|
||||
return false;
|
||||
}
|
||||
Logger::EngineLog("EGL Version: %d.%d", major, minor);
|
||||
|
||||
// Choose EGL config for OpenGL ES 3.0
|
||||
const EGLint attribs[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24,
|
||||
EGL_STENCIL_SIZE, 8,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLConfig config;
|
||||
EGLint numConfigs;
|
||||
if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs))
|
||||
{
|
||||
Logger::EngineError("eglChooseConfig failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// In a full UWP implementation, you would get the CoreWindow here
|
||||
// For now, we create a window surface with default parameters
|
||||
EGLint surfaceAttribs[] = {
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLSurface surface = eglCreateWindowSurface(display, config, nullptr, surfaceAttribs);
|
||||
if (surface == EGL_NO_SURFACE)
|
||||
{
|
||||
Logger::EngineError("eglCreateWindowSurface failed: 0x%X", eglGetError());
|
||||
return false;
|
||||
}
|
||||
m_eglSurface = surface;
|
||||
|
||||
// Create OpenGL ES 3.0 context
|
||||
const EGLint contextAttribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 3,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT)
|
||||
{
|
||||
Logger::EngineError("eglCreateContext failed");
|
||||
return false;
|
||||
}
|
||||
m_eglContext = context;
|
||||
|
||||
// Make context current
|
||||
if (!eglMakeCurrent(display, surface, surface, context))
|
||||
{
|
||||
Logger::EngineError("eglMakeCurrent failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Query surface size
|
||||
eglQuerySurface(display, surface, EGL_WIDTH, &m_width);
|
||||
eglQuerySurface(display, surface, EGL_HEIGHT, &m_height);
|
||||
|
||||
// Enable OpenGL ES features
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Logger::EngineLog("Xbox Graphics initialized (OpenGL ES 3.0, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
if (m_eglDisplay != EGL_NO_DISPLAY)
|
||||
{
|
||||
eglMakeCurrent((EGLDisplay)m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
|
||||
if (m_eglContext != EGL_NO_CONTEXT)
|
||||
{
|
||||
eglDestroyContext((EGLDisplay)m_eglDisplay, (EGLContext)m_eglContext);
|
||||
m_eglContext = EGL_NO_CONTEXT;
|
||||
}
|
||||
|
||||
if (m_eglSurface != EGL_NO_SURFACE)
|
||||
{
|
||||
eglDestroySurface((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface);
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
eglTerminate((EGLDisplay)m_eglDisplay);
|
||||
m_eglDisplay = EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
m_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::BeginFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::EndFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::SwapBuffers()
|
||||
{
|
||||
if (m_eglDisplay && m_eglSurface)
|
||||
{
|
||||
eglSwapBuffers((EGLDisplay)m_eglDisplay, (EGLSurface)m_eglSurface);
|
||||
}
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
glClearColor(r, g, b, a);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::ClearDepth()
|
||||
{
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
bool XboxSeriesGraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
if (m_eglDisplay)
|
||||
{
|
||||
eglSwapInterval((EGLDisplay)m_eglDisplay, enabled ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool XboxSeriesGraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float XboxSeriesGraphicsContext::GetTime() const
|
||||
{
|
||||
float currentTime = static_cast<float>(GetTickCount64()) / 1000.0f;
|
||||
return currentTime - m_startTime;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void XboxSeriesGraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
glViewport(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_XBOX
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* Xbox Series X/S Graphics Context (DirectX 12)
|
||||
* Xbox Series X/S Graphics Context (UWP with OpenGL via ANGLE)
|
||||
*
|
||||
* Xbox uses DirectX 12 via the GDK (Game Development Kit).
|
||||
* This is a template implementation.
|
||||
* Uses ANGLE (Almost Native Graphics Layer Evolution) to translate
|
||||
* OpenGL ES calls to DirectX on Xbox via UWP.
|
||||
*/
|
||||
class XboxSeriesGraphicsContext : public IGraphicsContext
|
||||
{
|
||||
@@ -36,8 +36,16 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 1920;
|
||||
int m_height = 1080;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// UWP/ANGLE handles (using void* to avoid including headers)
|
||||
void* m_coreWindow = nullptr; // Windows::UI::Core::CoreWindow^
|
||||
void* m_eglDisplay = nullptr; // EGLDisplay
|
||||
void* m_eglSurface = nullptr; // EGLSurface
|
||||
void* m_eglContext = nullptr; // EGLContext
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "XboxSeriesPlatform.h"
|
||||
#include "XboxSeriesGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_XBOX
|
||||
|
||||
// Windows/Xbox includes
|
||||
#include <Windows.h>
|
||||
#include <Xinput.h>
|
||||
#include <cmath>
|
||||
|
||||
// Link XInput library
|
||||
#pragma comment(lib, "xinput.lib")
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
XboxSeriesPlatform::XboxSeriesPlatform()
|
||||
{
|
||||
}
|
||||
|
||||
XboxSeriesPlatform::~XboxSeriesPlatform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool XboxSeriesPlatform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing Xbox Series Platform (UWP)");
|
||||
|
||||
// Initialize graphics context
|
||||
m_graphicsContext = std::make_unique<XboxSeriesGraphicsContext>();
|
||||
|
||||
Logger::EngineLog("Xbox Series Platform initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::Shutdown()
|
||||
{
|
||||
m_graphicsContext.reset();
|
||||
Logger::EngineLog("Xbox Series Platform shutdown");
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::ProcessEvents()
|
||||
{
|
||||
// UWP event processing would be handled by CoreWindow dispatcher
|
||||
// This is called from the main game loop
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::PollInput(InputState& outInputState)
|
||||
{
|
||||
UpdateGamepadInput(outInputState);
|
||||
outInputState.deviceType = InputDeviceType::XboxController;
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::UpdateGamepadInput(InputState& outInputState)
|
||||
{
|
||||
// Store previous button state
|
||||
m_prevButtons = m_buttons;
|
||||
|
||||
// Read XInput state (controller 0)
|
||||
XINPUT_STATE state;
|
||||
ZeroMemory(&state, sizeof(XINPUT_STATE));
|
||||
|
||||
DWORD result = XInputGetState(0, &state);
|
||||
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
// Controller is connected
|
||||
XINPUT_GAMEPAD& pad = state.Gamepad;
|
||||
|
||||
// Store button state
|
||||
m_buttons = pad.wButtons;
|
||||
|
||||
// Store analog sticks
|
||||
m_lstickX = NormalizeAxis(pad.sThumbLX);
|
||||
m_lstickY = NormalizeAxis(pad.sThumbLY);
|
||||
m_rstickX = NormalizeAxis(pad.sThumbRX);
|
||||
m_rstickY = NormalizeAxis(pad.sThumbRY);
|
||||
|
||||
// Store triggers
|
||||
m_leftTrigger = NormalizeTrigger(pad.bLeftTrigger);
|
||||
m_rightTrigger = NormalizeTrigger(pad.bRightTrigger);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Controller is not connected, reset to defaults
|
||||
m_buttons = 0;
|
||||
m_lstickX = 0.0f;
|
||||
m_lstickY = 0.0f;
|
||||
m_rstickX = 0.0f;
|
||||
m_rstickY = 0.0f;
|
||||
m_leftTrigger = 0.0f;
|
||||
m_rightTrigger = 0.0f;
|
||||
}
|
||||
|
||||
// Map analog sticks
|
||||
outInputState.moveAxisX = m_lstickX;
|
||||
outInputState.moveAxisY = m_lstickY;
|
||||
outInputState.lookAxisX = m_rstickX * 100.0f;
|
||||
outInputState.lookAxisY = -m_rstickY * 100.0f; // Invert Y for camera
|
||||
|
||||
// Button mapping (Xbox controller)
|
||||
bool btnA = (m_buttons & XINPUT_GAMEPAD_A) != 0;
|
||||
bool btnB = (m_buttons & XINPUT_GAMEPAD_B) != 0;
|
||||
bool btnX = (m_buttons & XINPUT_GAMEPAD_X) != 0;
|
||||
bool btnY = (m_buttons & XINPUT_GAMEPAD_Y) != 0;
|
||||
bool btnLB = (m_buttons & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0;
|
||||
bool btnRB = (m_buttons & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
|
||||
bool btnLT = m_leftTrigger > 0.5f; // Digital threshold for analog trigger
|
||||
bool btnRT = m_rightTrigger > 0.5f;
|
||||
bool btnMenu = (m_buttons & XINPUT_GAMEPAD_START) != 0;
|
||||
bool btnView = (m_buttons & XINPUT_GAMEPAD_BACK) != 0;
|
||||
bool btnDpadUp = (m_buttons & XINPUT_GAMEPAD_DPAD_UP) != 0;
|
||||
bool btnDpadDown = (m_buttons & XINPUT_GAMEPAD_DPAD_DOWN) != 0;
|
||||
bool btnDpadLeft = (m_buttons & XINPUT_GAMEPAD_DPAD_LEFT) != 0;
|
||||
bool btnDpadRight = (m_buttons & XINPUT_GAMEPAD_DPAD_RIGHT) != 0;
|
||||
|
||||
// Map to abstract actions
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action1)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action2)] = btnB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action3)] = btnX;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Action4)] = btnY;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Jump)] = btnA;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Crouch)] = btnLT;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::Sprint)] = btnRT;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleLeft)] = btnLB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::CycleRight)] = btnRB;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuOpen)] = btnMenu;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MenuBack)] = btnB || btnView;
|
||||
|
||||
// D-pad for movement
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = btnDpadUp;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = btnDpadDown;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = btnDpadLeft;
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = btnDpadRight;
|
||||
|
||||
// Pressed this frame
|
||||
uint16_t pressed = m_buttons & ~m_prevButtons;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action1)] = (pressed & XINPUT_GAMEPAD_A) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Action2)] = (pressed & XINPUT_GAMEPAD_B) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::MenuOpen)] = (pressed & XINPUT_GAMEPAD_START) != 0;
|
||||
outInputState.actions[static_cast<int>(InputAction::Jump)] = (pressed & XINPUT_GAMEPAD_A) != 0;
|
||||
|
||||
// Analog to digital movement
|
||||
if (std::abs(outInputState.moveAxisY) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisY > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
}
|
||||
if (std::abs(outInputState.moveAxisX) > 0.3f)
|
||||
{
|
||||
if (outInputState.moveAxisX > 0.3f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
else
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
float XboxSeriesPlatform::NormalizeAxis(int16_t value) const
|
||||
{
|
||||
// XInput thumbsticks are -32768 to 32767
|
||||
float normalized = static_cast<float>(value) / 32767.0f;
|
||||
|
||||
// Apply deadzone (XInput default is ~7849 / 32767 = 0.24)
|
||||
const float deadzone = 0.24f;
|
||||
if (std::abs(normalized) < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
// Rescale to account for deadzone
|
||||
float sign = (normalized > 0.0f) ? 1.0f : -1.0f;
|
||||
float absValue = std::abs(normalized);
|
||||
return sign * ((absValue - deadzone) / (1.0f - deadzone));
|
||||
}
|
||||
|
||||
float XboxSeriesPlatform::NormalizeTrigger(uint8_t value) const
|
||||
{
|
||||
// XInput triggers are 0-255
|
||||
float normalized = static_cast<float>(value) / 255.0f;
|
||||
|
||||
// Apply trigger deadzone (XINPUT_GAMEPAD_TRIGGER_THRESHOLD = 30)
|
||||
const float deadzone = 30.0f / 255.0f;
|
||||
if (normalized < deadzone)
|
||||
return 0.0f;
|
||||
|
||||
// Rescale to account for deadzone
|
||||
return (normalized - deadzone) / (1.0f - deadzone);
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* XboxSeriesPlatform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* XboxSeriesPlatform::GetUserDataPath() const
|
||||
{
|
||||
// UWP apps have local application data folder
|
||||
return "LocalState/WillowVox";
|
||||
}
|
||||
|
||||
const char* XboxSeriesPlatform::GetAssetsPath() const
|
||||
{
|
||||
// UWP assets are in the app package
|
||||
return "Assets";
|
||||
}
|
||||
|
||||
const char* XboxSeriesPlatform::GetPlatformName() const
|
||||
{
|
||||
return "Xbox Series X|S";
|
||||
}
|
||||
|
||||
InputDeviceType XboxSeriesPlatform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::XboxController;
|
||||
}
|
||||
|
||||
bool XboxSeriesPlatform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "gamepad") == 0) return true;
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::SetVibration(int playerIndex, float lowFrequency, float highFrequency)
|
||||
{
|
||||
if (playerIndex < 0 || playerIndex >= XUSER_MAX_COUNT)
|
||||
return;
|
||||
|
||||
// XInput vibration values are 0-65535
|
||||
XINPUT_VIBRATION vibration;
|
||||
vibration.wLeftMotorSpeed = static_cast<WORD>(lowFrequency * 65535.0f);
|
||||
vibration.wRightMotorSpeed = static_cast<WORD>(highFrequency * 65535.0f);
|
||||
|
||||
XInputSetState(playerIndex, &vibration);
|
||||
}
|
||||
|
||||
void XboxSeriesPlatform::StopVibration(int playerIndex)
|
||||
{
|
||||
SetVibration(playerIndex, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_XBOX
|
||||
@@ -8,7 +8,7 @@ namespace WillowVox
|
||||
class XboxSeriesGraphicsContext;
|
||||
|
||||
/**
|
||||
* Xbox Series X/S Platform (Dev Mode / GDK)
|
||||
* Xbox Series X/S Platform (UWP with XInput)
|
||||
*
|
||||
* Controller: Xbox Series Controller (standard Xbox layout)
|
||||
* - Left Stick: Movement
|
||||
@@ -22,8 +22,8 @@ namespace WillowVox
|
||||
* - Menu (≡): MenuOpen
|
||||
* - View (::): MenuBack
|
||||
*
|
||||
* Graphics: DirectX 12 (via GDK)
|
||||
* Input: GameInput API or XInput
|
||||
* Graphics: OpenGL ES via ANGLE (translates to DirectX)
|
||||
* Input: XInput API
|
||||
*/
|
||||
class XboxSeriesPlatform : public IPlatform
|
||||
{
|
||||
@@ -43,7 +43,24 @@ namespace WillowVox
|
||||
InputDeviceType GetPrimaryInputDevice() const override;
|
||||
bool HasFeature(const char* featureName) const override;
|
||||
|
||||
void SetVibration(int playerIndex, float lowFrequency, float highFrequency) override;
|
||||
void StopVibration(int playerIndex) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<XboxSeriesGraphicsContext> m_graphicsContext;
|
||||
|
||||
// XInput gamepad state
|
||||
uint16_t m_buttons = 0;
|
||||
uint16_t m_prevButtons = 0;
|
||||
float m_lstickX = 0.0f;
|
||||
float m_lstickY = 0.0f;
|
||||
float m_rstickX = 0.0f;
|
||||
float m_rstickY = 0.0f;
|
||||
float m_leftTrigger = 0.0f;
|
||||
float m_rightTrigger = 0.0f;
|
||||
|
||||
void UpdateGamepadInput(InputState& outInputState);
|
||||
float NormalizeAxis(int16_t value) const;
|
||||
float NormalizeTrigger(uint8_t value) const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
#include "iOSGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_IOS
|
||||
|
||||
// iOS SDK includes
|
||||
#import <OpenGLES/EAGL.h>
|
||||
#import <OpenGLES/ES3/gl.h>
|
||||
#import <OpenGLES/ES3/glext.h>
|
||||
#import <QuartzCore/CAEAGLLayer.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
iOSGraphicsContext::iOSGraphicsContext()
|
||||
{
|
||||
// Get start time
|
||||
mach_timebase_info_data_t timebase;
|
||||
mach_timebase_info(&timebase);
|
||||
uint64_t time = mach_absolute_time();
|
||||
m_startTime = (double)time * (double)timebase.numer / (double)timebase.denom / 1e9;
|
||||
}
|
||||
|
||||
iOSGraphicsContext::~iOSGraphicsContext()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool iOSGraphicsContext::Initialize(int width, int height, const char* title)
|
||||
{
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
|
||||
Logger::EngineLog("Initializing iOS Graphics (OpenGL ES 3.0)");
|
||||
|
||||
// Create EAGLContext with OpenGL ES 3.0
|
||||
EAGLContext* context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
|
||||
if (!context)
|
||||
{
|
||||
Logger::EngineError("Failed to create EAGLContext with ES 3.0, trying ES 2.0");
|
||||
context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
||||
if (!context)
|
||||
{
|
||||
Logger::EngineError("Failed to create EAGLContext");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_eaglContext = (__bridge_retained void*)context;
|
||||
|
||||
// Make context current
|
||||
if (![EAGLContext setCurrentContext:context])
|
||||
{
|
||||
Logger::EngineError("Failed to set current EAGLContext");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the main UIWindow's layer (this is a simplification - in real app, you'd get this from your ViewController)
|
||||
UIWindow* window = [[UIApplication sharedApplication] keyWindow];
|
||||
if (!window)
|
||||
{
|
||||
Logger::EngineError("Failed to get UIWindow");
|
||||
return false;
|
||||
}
|
||||
|
||||
CAEAGLLayer* eaglLayer = (CAEAGLLayer*)window.layer;
|
||||
eaglLayer.opaque = YES;
|
||||
eaglLayer.drawableProperties = @{
|
||||
kEAGLDrawablePropertyRetainedBacking: @NO,
|
||||
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
|
||||
};
|
||||
m_eaglLayer = (__bridge void*)eaglLayer;
|
||||
|
||||
// Create framebuffer
|
||||
glGenFramebuffers(1, &m_framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
|
||||
|
||||
// Create color renderbuffer
|
||||
glGenRenderbuffers(1, &m_colorRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer);
|
||||
|
||||
// Allocate storage for the renderbuffer from the layer
|
||||
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:eaglLayer];
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorRenderbuffer);
|
||||
|
||||
// Get actual renderbuffer size
|
||||
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &m_width);
|
||||
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &m_height);
|
||||
|
||||
// Create depth renderbuffer
|
||||
glGenRenderbuffers(1, &m_depthRenderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_depthRenderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, m_width, m_height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depthRenderbuffer);
|
||||
|
||||
// Check framebuffer status
|
||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
Logger::EngineError("Framebuffer is not complete: 0x%X", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable OpenGL ES features
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Logger::EngineLog("iOS Graphics initialized (OpenGL ES, %dx%d)", m_width, m_height);
|
||||
m_initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::Shutdown()
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
// Delete framebuffer objects
|
||||
if (m_framebuffer)
|
||||
{
|
||||
glDeleteFramebuffers(1, &m_framebuffer);
|
||||
m_framebuffer = 0;
|
||||
}
|
||||
if (m_colorRenderbuffer)
|
||||
{
|
||||
glDeleteRenderbuffers(1, &m_colorRenderbuffer);
|
||||
m_colorRenderbuffer = 0;
|
||||
}
|
||||
if (m_depthRenderbuffer)
|
||||
{
|
||||
glDeleteRenderbuffers(1, &m_depthRenderbuffer);
|
||||
m_depthRenderbuffer = 0;
|
||||
}
|
||||
|
||||
// Release EAGL context
|
||||
if (m_eaglContext)
|
||||
{
|
||||
EAGLContext* context = (__bridge_transfer EAGLContext*)m_eaglContext;
|
||||
if ([EAGLContext currentContext] == context)
|
||||
{
|
||||
[EAGLContext setCurrentContext:nil];
|
||||
}
|
||||
context = nil;
|
||||
m_eaglContext = nullptr;
|
||||
}
|
||||
|
||||
m_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::BeginFrame()
|
||||
{
|
||||
// Bind framebuffer for rendering
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer);
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::EndFrame()
|
||||
{
|
||||
// Nothing specific needed
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::SwapBuffers()
|
||||
{
|
||||
// Present the color renderbuffer
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer);
|
||||
EAGLContext* context = (__bridge EAGLContext*)m_eaglContext;
|
||||
[context presentRenderbuffer:GL_RENDERBUFFER];
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::Clear(float r, float g, float b, float a)
|
||||
{
|
||||
glClearColor(r, g, b, a);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::ClearDepth()
|
||||
{
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
bool iOSGraphicsContext::ShouldClose() const
|
||||
{
|
||||
return m_shouldClose;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::SetShouldClose(bool shouldClose)
|
||||
{
|
||||
m_shouldClose = shouldClose;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::GetFramebufferSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::GetWindowSize(int& width, int& height) const
|
||||
{
|
||||
width = m_width;
|
||||
height = m_height;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::SetVSync(bool enabled)
|
||||
{
|
||||
m_vsyncEnabled = enabled;
|
||||
// VSync on iOS is typically controlled at the CADisplayLink level
|
||||
// This would require integration with the view controller
|
||||
}
|
||||
|
||||
bool iOSGraphicsContext::IsVSyncEnabled() const
|
||||
{
|
||||
return m_vsyncEnabled;
|
||||
}
|
||||
|
||||
float iOSGraphicsContext::GetTime() const
|
||||
{
|
||||
mach_timebase_info_data_t timebase;
|
||||
mach_timebase_info(&timebase);
|
||||
uint64_t time = mach_absolute_time();
|
||||
double currentTime = (double)time * (double)timebase.numer / (double)timebase.denom / 1e9;
|
||||
return static_cast<float>(currentTime - m_startTime);
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::SetBackgroundColor(float r, float g, float b, float a)
|
||||
{
|
||||
m_clearColor[0] = r;
|
||||
m_clearColor[1] = g;
|
||||
m_clearColor[2] = b;
|
||||
m_clearColor[3] = a;
|
||||
}
|
||||
|
||||
void iOSGraphicsContext::SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
glViewport(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_IOS
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace WillowVox
|
||||
{
|
||||
/**
|
||||
* iOS Graphics Context (Metal or OpenGL ES)
|
||||
* iOS Graphics Context (OpenGL ES)
|
||||
*
|
||||
* iOS can use Metal (preferred) or OpenGL ES for compatibility.
|
||||
* This is a template implementation.
|
||||
* Uses OpenGL ES 3.0 with EAGLContext and CAEAGLLayer.
|
||||
* Provides compatibility for cross-platform OpenGL code.
|
||||
*/
|
||||
class iOSGraphicsContext : public IGraphicsContext
|
||||
{
|
||||
@@ -36,8 +36,17 @@ namespace WillowVox
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
bool m_shouldClose = false;
|
||||
bool m_vsyncEnabled = true;
|
||||
int m_width = 0;
|
||||
int m_height = 0;
|
||||
float m_clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f};
|
||||
float m_startTime = 0.0f;
|
||||
|
||||
// iOS-specific OpenGL ES handles (using void* to avoid including Objective-C headers)
|
||||
void* m_eaglContext = nullptr; // EAGLContext*
|
||||
void* m_eaglLayer = nullptr; // CAEAGLLayer*
|
||||
unsigned int m_colorRenderbuffer = 0;
|
||||
unsigned int m_depthRenderbuffer = 0;
|
||||
unsigned int m_framebuffer = 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
#include "iOSPlatform.h"
|
||||
#include "iOSGraphicsContext.h"
|
||||
#include <wv/Logger.h>
|
||||
|
||||
#ifdef PLATFORM_IOS
|
||||
|
||||
// iOS SDK includes
|
||||
#include <UIKit/UIKit.h>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
iOSPlatform::iOSPlatform()
|
||||
{
|
||||
}
|
||||
|
||||
iOSPlatform::~iOSPlatform()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool iOSPlatform::Initialize()
|
||||
{
|
||||
Logger::EngineLog("Initializing iOS Platform");
|
||||
|
||||
// Graphics context will handle UIKit/OpenGL ES setup
|
||||
m_graphicsContext = std::make_unique<iOSGraphicsContext>();
|
||||
|
||||
// Get screen dimensions from UIScreen
|
||||
UIScreen* screen = [UIScreen mainScreen];
|
||||
CGRect bounds = [screen bounds];
|
||||
CGFloat scale = [screen scale];
|
||||
m_screenWidth = static_cast<int>(bounds.size.width * scale);
|
||||
m_screenHeight = static_cast<int>(bounds.size.height * scale);
|
||||
Logger::EngineLog("Screen size: %dx%d (scale: %.1f)", m_screenWidth, m_screenHeight, scale);
|
||||
|
||||
// Setup virtual touch controls (similar to Android)
|
||||
SetupVirtualControls();
|
||||
|
||||
Logger::EngineLog("iOS Platform initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
void iOSPlatform::Shutdown()
|
||||
{
|
||||
m_graphicsContext.reset();
|
||||
Logger::EngineLog("iOS Platform shutdown");
|
||||
}
|
||||
|
||||
void iOSPlatform::ProcessEvents()
|
||||
{
|
||||
// iOS event processing handled by UIKit run loop
|
||||
// This is called from the main game loop
|
||||
}
|
||||
|
||||
void iOSPlatform::PollInput(InputState& outInputState)
|
||||
{
|
||||
ProcessTouchInput(outInputState);
|
||||
|
||||
// Copy button states
|
||||
std::memcpy(m_buttonPrevStates, m_buttonStates, sizeof(m_buttonStates));
|
||||
|
||||
outInputState.deviceType = InputDeviceType::Touchscreen;
|
||||
}
|
||||
|
||||
void iOSPlatform::SetupVirtualControls()
|
||||
{
|
||||
// Virtual joystick (left side, bottom)
|
||||
m_virtualControls.push_back({0.15f, 0.85f, 0.12f, InputAction::Count, true});
|
||||
|
||||
// Action buttons (right side, bottom)
|
||||
m_virtualControls.push_back({0.85f, 0.85f, 0.06f, InputAction::Action1, false});
|
||||
m_virtualControls.push_back({0.75f, 0.80f, 0.06f, InputAction::Action2, false});
|
||||
m_virtualControls.push_back({0.85f, 0.75f, 0.06f, InputAction::Jump, false});
|
||||
|
||||
// Menu button (top left)
|
||||
m_virtualControls.push_back({0.10f, 0.10f, 0.05f, InputAction::MenuOpen, false});
|
||||
}
|
||||
|
||||
void iOSPlatform::ProcessTouchInput(InputState& outInputState)
|
||||
{
|
||||
// Reset per-frame state
|
||||
m_lookDeltaX = 0.0f;
|
||||
m_lookDeltaY = 0.0f;
|
||||
m_joystickX = 0.0f;
|
||||
m_joystickY = 0.0f;
|
||||
|
||||
// Reset button states
|
||||
for (int i = 0; i < static_cast<int>(InputAction::Count); ++i)
|
||||
{
|
||||
m_buttonStates[i] = false;
|
||||
}
|
||||
|
||||
// Process each active touch point
|
||||
// Note: Touch points are populated by UIKit touch event handlers
|
||||
// This would be integrated with UIViewController's touchesBegan/Moved/Ended methods
|
||||
for (int i = 0; i < MAX_TOUCH_POINTS; ++i)
|
||||
{
|
||||
TouchPoint& touch = m_touchPoints[i];
|
||||
if (!touch.active)
|
||||
continue;
|
||||
|
||||
// Check virtual controls
|
||||
bool handled = false;
|
||||
|
||||
// Virtual joystick (left side)
|
||||
if (touch.startX < 0.5f && touch.id == m_joystickTouchId)
|
||||
{
|
||||
// Calculate joystick displacement from start position
|
||||
float dx = touch.x - touch.startX;
|
||||
float dy = touch.y - touch.startY;
|
||||
|
||||
// Normalize joystick input (clamp to radius)
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
float maxDist = 0.12f; // Same as virtual control radius
|
||||
|
||||
if (dist > maxDist)
|
||||
{
|
||||
dx = (dx / dist) * maxDist;
|
||||
dy = (dy / dist) * maxDist;
|
||||
}
|
||||
|
||||
m_joystickX = dx / maxDist;
|
||||
m_joystickY = dy / maxDist;
|
||||
|
||||
handled = true;
|
||||
}
|
||||
else if (touch.startX < 0.5f && m_joystickTouchId == -1)
|
||||
{
|
||||
// New touch on left side - assign to joystick
|
||||
m_joystickTouchId = touch.id;
|
||||
handled = true;
|
||||
}
|
||||
|
||||
// Look control (right side)
|
||||
if (touch.startX >= 0.5f && touch.id == m_lookTouchId)
|
||||
{
|
||||
// Calculate look delta
|
||||
static float prevLookX = touch.startX;
|
||||
static float prevLookY = touch.startY;
|
||||
|
||||
m_lookDeltaX = (touch.x - prevLookX) * m_screenWidth;
|
||||
m_lookDeltaY = (touch.y - prevLookY) * m_screenHeight;
|
||||
|
||||
prevLookX = touch.x;
|
||||
prevLookY = touch.y;
|
||||
|
||||
handled = true;
|
||||
}
|
||||
else if (touch.startX >= 0.5f && m_lookTouchId == -1)
|
||||
{
|
||||
// Check if touching a button first
|
||||
bool isTouchingButton = false;
|
||||
for (const auto& control : m_virtualControls)
|
||||
{
|
||||
if (!control.isJoystick &&
|
||||
IsInsideCircle(touch.x, touch.y, control.x, control.y, control.radius))
|
||||
{
|
||||
m_buttonStates[static_cast<int>(control.action)] = true;
|
||||
isTouchingButton = true;
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not touching a button, use for look control
|
||||
if (!isTouchingButton)
|
||||
{
|
||||
m_lookTouchId = touch.id;
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check button touches
|
||||
if (!handled)
|
||||
{
|
||||
for (const auto& control : m_virtualControls)
|
||||
{
|
||||
if (!control.isJoystick &&
|
||||
IsInsideCircle(touch.x, touch.y, control.x, control.y, control.radius))
|
||||
{
|
||||
m_buttonStates[static_cast<int>(control.action)] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for released touch IDs
|
||||
bool joystickActive = false;
|
||||
bool lookActive = false;
|
||||
for (int i = 0; i < MAX_TOUCH_POINTS; ++i)
|
||||
{
|
||||
if (m_touchPoints[i].active)
|
||||
{
|
||||
if (m_touchPoints[i].id == m_joystickTouchId)
|
||||
joystickActive = true;
|
||||
if (m_touchPoints[i].id == m_lookTouchId)
|
||||
lookActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!joystickActive)
|
||||
m_joystickTouchId = -1;
|
||||
if (!lookActive)
|
||||
m_lookTouchId = -1;
|
||||
|
||||
// Map to InputState
|
||||
outInputState.moveAxisX = m_joystickX;
|
||||
outInputState.moveAxisY = -m_joystickY; // Invert Y
|
||||
|
||||
outInputState.lookAxisX = m_lookDeltaX;
|
||||
outInputState.lookAxisY = m_lookDeltaY;
|
||||
|
||||
outInputState.pointerDeltaX = m_lookDeltaX;
|
||||
outInputState.pointerDeltaY = m_lookDeltaY;
|
||||
|
||||
// Map analog to digital actions
|
||||
if (std::abs(m_joystickX) > 0.5f || std::abs(m_joystickY) > 0.5f)
|
||||
{
|
||||
if (m_joystickY < -0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveForward)] = true;
|
||||
if (m_joystickY > 0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveBackward)] = true;
|
||||
if (m_joystickX < -0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveLeft)] = true;
|
||||
if (m_joystickX > 0.5f)
|
||||
outInputState.actionsHeld[static_cast<int>(InputAction::MoveRight)] = true;
|
||||
}
|
||||
|
||||
// Copy button states
|
||||
for (int i = 0; i < static_cast<int>(InputAction::Count); ++i)
|
||||
{
|
||||
outInputState.actionsHeld[i] = m_buttonStates[i];
|
||||
outInputState.actions[i] = m_buttonStates[i] && !m_buttonPrevStates[i];
|
||||
outInputState.actionsReleased[i] = !m_buttonStates[i] && m_buttonPrevStates[i];
|
||||
}
|
||||
}
|
||||
|
||||
void iOSPlatform::ResetInputFrameState(InputState& inputState)
|
||||
{
|
||||
inputState.ResetFrameStates();
|
||||
}
|
||||
|
||||
IGraphicsContext* iOSPlatform::GetGraphicsContext()
|
||||
{
|
||||
return m_graphicsContext.get();
|
||||
}
|
||||
|
||||
const char* iOSPlatform::GetUserDataPath() const
|
||||
{
|
||||
// iOS uses app-specific directories
|
||||
return "~/Documents/WillowVox";
|
||||
}
|
||||
|
||||
const char* iOSPlatform::GetAssetsPath() const
|
||||
{
|
||||
// iOS bundles assets in the app bundle
|
||||
return ""; // Use NSBundle for assets
|
||||
}
|
||||
|
||||
const char* iOSPlatform::GetPlatformName() const
|
||||
{
|
||||
return "iOS";
|
||||
}
|
||||
|
||||
InputDeviceType iOSPlatform::GetPrimaryInputDevice() const
|
||||
{
|
||||
return InputDeviceType::Touchscreen;
|
||||
}
|
||||
|
||||
bool iOSPlatform::HasFeature(const char* featureName) const
|
||||
{
|
||||
if (strcmp(featureName, "touchscreen") == 0) return true;
|
||||
if (strcmp(featureName, "filesystem") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
TouchPoint* iOSPlatform::GetTouchPoint(int32_t id)
|
||||
{
|
||||
for (int i = 0; i < MAX_TOUCH_POINTS; ++i)
|
||||
{
|
||||
if (m_touchPoints[i].active && m_touchPoints[i].id == id)
|
||||
return &m_touchPoints[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TouchPoint* iOSPlatform::AllocateTouchPoint(int32_t id)
|
||||
{
|
||||
for (int i = 0; i < MAX_TOUCH_POINTS; ++i)
|
||||
{
|
||||
if (!m_touchPoints[i].active)
|
||||
{
|
||||
m_touchPoints[i].active = true;
|
||||
m_touchPoints[i].id = id;
|
||||
return &m_touchPoints[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void iOSPlatform::ReleaseTouchPoint(int32_t id)
|
||||
{
|
||||
for (int i = 0; i < MAX_TOUCH_POINTS; ++i)
|
||||
{
|
||||
if (m_touchPoints[i].active && m_touchPoints[i].id == id)
|
||||
{
|
||||
m_touchPoints[i].active = false;
|
||||
m_touchPoints[i].id = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool iOSPlatform::IsInsideCircle(float x, float y, float cx, float cy, float radius) const
|
||||
{
|
||||
float dx = x - cx;
|
||||
float dy = y - cy;
|
||||
return (dx * dx + dy * dy) <= (radius * radius);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PLATFORM_IOS
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <wv/platform/IPlatform.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace WillowVox
|
||||
{
|
||||
@@ -38,8 +39,51 @@ namespace WillowVox
|
||||
bool HasFeature(const char* featureName) const override;
|
||||
|
||||
private:
|
||||
struct TouchPoint
|
||||
{
|
||||
int32_t id;
|
||||
float x, y;
|
||||
float startX, startY;
|
||||
bool active;
|
||||
};
|
||||
|
||||
std::unique_ptr<iOSGraphicsContext> m_graphicsContext;
|
||||
// Touch input tracking (similar to Android)
|
||||
// Implementation details omitted for brevity
|
||||
|
||||
// Touch tracking
|
||||
static constexpr int MAX_TOUCH_POINTS = 10;
|
||||
TouchPoint m_touchPoints[MAX_TOUCH_POINTS] = {};
|
||||
|
||||
// Virtual control regions (normalized 0-1)
|
||||
struct VirtualControl
|
||||
{
|
||||
float x, y, radius;
|
||||
InputAction action;
|
||||
bool isJoystick;
|
||||
};
|
||||
|
||||
std::vector<VirtualControl> m_virtualControls;
|
||||
|
||||
// Input state
|
||||
float m_joystickX = 0.0f;
|
||||
float m_joystickY = 0.0f;
|
||||
int m_joystickTouchId = -1;
|
||||
|
||||
float m_lookDeltaX = 0.0f;
|
||||
float m_lookDeltaY = 0.0f;
|
||||
int m_lookTouchId = -1;
|
||||
|
||||
bool m_buttonStates[static_cast<int>(InputAction::Count)] = {};
|
||||
bool m_buttonPrevStates[static_cast<int>(InputAction::Count)] = {};
|
||||
|
||||
int m_screenWidth = 1920;
|
||||
int m_screenHeight = 1080;
|
||||
|
||||
// Helper methods
|
||||
void SetupVirtualControls();
|
||||
void ProcessTouchInput(InputState& outInputState);
|
||||
TouchPoint* GetTouchPoint(int32_t id);
|
||||
TouchPoint* AllocateTouchPoint(int32_t id);
|
||||
void ReleaseTouchPoint(int32_t id);
|
||||
bool IsInsideCircle(float x, float y, float cx, float cy, float radius) const;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user