mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Add item throwing mechanics to VR configuration and input handling
- Introduced `vrCockpitItemThrow` configuration option to enable throwing items in VR. - Implemented `ThrowDetector` and `ThrowSequence` classes to manage item throwing mechanics based on hand movements. - Updated `OpenXRInput` to handle item throwing based on hand swings, integrating with existing VR input systems. - Enhanced settings overlay to allow users to toggle item throwing functionality. - Added tests for item throwing mechanics, ensuring correct behavior for various hand movements and conditions.
This commit is contained in:
1 parent
db30945f4c
commit
ebc97ef9ce
12 files changed
+701
-6
No files matched your search
@@ -63,6 +63,7 @@ native_steering_wheel = true
|
||||
object_culling = false
|
||||
hand_steering = true
|
||||
cockpit_item_hand = "left"
|
||||
cockpit_item_throw = true
|
||||
performance_level = "boost"
|
||||
```
|
||||
|
||||
@@ -314,6 +315,30 @@ layers with their wrap modes, SRT and environment mapping, vertex colours, culli
|
||||
to four TEV stages. Only the lighting is approximated, by a fixed cockpit light in place of the
|
||||
course's light set.
|
||||
|
||||
**Throwing the held item.** With `cockpit_item_throw` on (the default; F10 > VR and the Quest
|
||||
launcher), a quick swing of the item hand forward throws the item ahead of the kart, and a swing back
|
||||
throws it behind. The swing plays what the game reads for an aimed throw: the stick pushed fully
|
||||
forward or back, the item button (the left trigger: Z, or the GameCube's L) pressed and released
|
||||
while it stays pushed, then the stick handed back. Items used on the press and items thrown on the
|
||||
release (a trailed shell or banana) both see the aim. Swinging while holding the item button throws
|
||||
the trailed item, and that held button is then ignored until it is let go, so it does not use the
|
||||
next item of a triple. Only the stick's Y axis moves, so a held wheel keeps steering. A swing counts
|
||||
when the hand covers enough ground along the seat's forward axis within a short window, mostly
|
||||
along that axis, and is still moving that way: with a controller 18 cm forward or 15 cm back within
|
||||
0.12 s. A bare hand gets an easier profile, 15 cm forward or 13 cm back within 0.15 s. The headset's
|
||||
cameras lag and smooth a bare hand, and often lose it for a few frames in the middle of a fast swing.
|
||||
A loss of up to 0.2 s (0.1 s for a controller) is bridged by the positions either side of it. The
|
||||
window counts only tracked time, and across a gap the swing must still average 70 % of the window's
|
||||
speed, so a slow drift the cameras briefly lost never throws. Reaching for the wheel, bringing a
|
||||
hand back to rest, a sideways sweep and the upward flick of a trick never throw either. The hand
|
||||
must not be holding the wheel and must have been free for a quarter of a second, and after a throw
|
||||
the next one waits 0.6 s. It works with controllers and tracked hands, in both controller modes, and
|
||||
a short pulse confirms it. Each throw's line in the run log gives what the swing measured (for
|
||||
example `Held item thrown forward (bare hand, 21 cm in 150 ms, across a tracking gap)`).
|
||||
Mario Kart Wii reads tricks and wheelies from the Wii Remote's accelerometer only
|
||||
(`MotionController::UpdateForNunchuck` never reads the Nunchuk's), so a swing of the left hand cannot
|
||||
trick. With the item in the right hand, a hard swing can also read as a shake of the remote.
|
||||
|
||||
**Bare hands.** On the Quest, with `hand_tracking` on and the controllers put down, the hands drive
|
||||
`khr/simple_controller`: a right pinch is A with the pointer on the hand's aim ray, the left
|
||||
palm-up pinch is + (pause), and in the cockpit, while a hand holds the wheel, that hand holds A and
|
||||
|
||||
@@ -196,6 +196,13 @@ class SettingsPage(
|
||||
write = { c, index -> c.setString("vr", "cockpit_item_hand", ITEM_HANDS[index]) },
|
||||
enabledIf = cockpit,
|
||||
)
|
||||
// runtime_config.h's kVrCockpitItemThrowDefault is on.
|
||||
toggle(
|
||||
R.string.vr_cockpit_item_throw, R.string.vr_cockpit_item_throw_helper,
|
||||
read = { it.bool("vr", "cockpit_item_throw") ?: true },
|
||||
write = { c, value -> c.setBool("vr", "cockpit_item_throw", value) },
|
||||
enabledIf = { c -> cockpit(c) && stringIndex(c, "vr", "cockpit_item_hand", ITEM_HANDS) != 2 },
|
||||
)
|
||||
// Tracked hands can show the item with stick steering as well.
|
||||
toggle(
|
||||
R.string.vr_hand_tracking, R.string.vr_hand_tracking_helper,
|
||||
|
||||
@@ -540,6 +540,8 @@
|
||||
<string name="vr_item_left">Left</string>
|
||||
<string name="vr_item_right">Right</string>
|
||||
<string name="vr_item_off">Off</string>
|
||||
<string name="vr_cockpit_item_throw">Throw the item by swinging</string>
|
||||
<string name="vr_cockpit_item_throw_helper">With that hand off the wheel, swing it quickly forward to throw the item ahead of the kart, or back to throw it behind, as the stick and item button would. Swinging while you hold the item button throws the item you are trailing.</string>
|
||||
<string name="vr_hand_tracking">Tracked hands</string>
|
||||
<string name="vr_hand_tracking_helper">The cockpit hands follow your own. Holding the controllers, the fingers follow their touch sensors; put one down and the cameras track that hand at once. Put the controllers down to drive with bare hands: close a hand on the wheel to hold it, which also holds the gas; pinch with a free hand to use an item; flick your hands up for a trick; pinch with your left palm facing you to pause. In menus, a right pinch is A. Choose Automatic drift. Needs hand tracking on in the headset\'s settings.</string>
|
||||
<string name="vr_lean_back">Lean back angle</string>
|
||||
|
||||
+1
-1
@@ -973,7 +973,7 @@ the app:
|
||||
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update |
|
||||
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
||||
| `debug.wiicompiled.fdm 0` | Launches without fragment density maps at all, whatever `foveation` says, which also drops their flag from every pipeline; `1` asks for them even with `foveation = "off"` |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`). `flick` plays the bare hands' flick, one 150 ms shake of the remote (a trick off a ramp, a wheelie on a bike), with the controllers or none |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`). `flick` plays the bare hands' flick, one 150 ms shake of the remote (a trick off a ramp, a wheelie on a bike), with the controllers or none. `throw_forward` and `throw_backward` play a throw of the held item: the stick pushed that way while the item button is pressed and released (see "Throwing the held item" in `OPENXR.md`) |
|
||||
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s, with per-frame averages of the producer's waits for the frame worker's DONE and SEALED phases and of the worker's seal, permit wait, prepare and encode stretches, and of the draw calls the recorded frame holds and the primitives that merged into them (an overlay that stops draws merging shows up there first). A third line reports the GX thread's command ring (records, waits, busy share). A second line gives the GPU time per frame from timestamp queries on every pass (`mono` native render, `eyeL`/`eyeR` replays, `screen`, `panel`, `efbcopy`, `palette`, `peek`, plus `passes-span` from the first pass begin to the last pass end and `between-passes` for copies and idle gaps). The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%`, clock levels and app GPU time (`App=`) |
|
||||
|
||||
A `Config.toml` written with `adb push` (or `sed -i` in `adb shell`) belongs
|
||||
|
||||
@@ -415,10 +415,10 @@ add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
|
||||
|
||||
# The first-person cockpit (seated eye, wheel and handlebar geometry, level seat,
|
||||
# native wheel vertices) and hand steering (grab, turn, hand-off to the game),
|
||||
# ported from heurazy's mario-kart-wii-VR-port, and tracked hands (grasp, bare-hand
|
||||
# buttons, flick). All header-only.
|
||||
# ported from heurazy's mario-kart-wii-VR-port, tracked hands (grasp, bare-hand
|
||||
# buttons, flick) and throwing the held item. All header-only.
|
||||
foreach(test_name mkw_steering_wheel_tests mkw_vr_cockpit_tests mkw_vr_bullet_bill_tests mkw_vr_hand_steering_tests mkw_vr_camera_toggle_tests
|
||||
mkw_vr_hand_tracking_tests mkw_vr_culling_tests)
|
||||
mkw_vr_hand_tracking_tests mkw_vr_culling_tests mkw_vr_item_throw_tests)
|
||||
string(REGEX REPLACE "^mkw_" "" test_source "${test_name}")
|
||||
add_executable(${test_name} "${CMAKE_CURRENT_LIST_DIR}/tests/${test_source}.cpp")
|
||||
target_include_directories(${test_name} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
|
||||
@@ -79,6 +79,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrObjectCulling;
|
||||
std::optional<bool> vrHandSteering;
|
||||
std::optional<std::string> vrCockpitItemHand;
|
||||
std::optional<bool> vrCockpitItemThrow;
|
||||
std::optional<bool> vrHandTracking;
|
||||
std::optional<float> vrWheelKartDegrees;
|
||||
std::optional<float> vrWheelBikeDegrees;
|
||||
@@ -239,6 +240,8 @@ inline constexpr bool kVrSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrNativeSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrHandSteeringDefault = true;
|
||||
inline constexpr const char* kVrCockpitItemHandDefault = "left";
|
||||
// A quick swing of the item hand forward or back throws the item that way.
|
||||
inline constexpr bool kVrCockpitItemThrowDefault = true;
|
||||
// The game hides karts and objects its own camera cannot see, which a head
|
||||
// turn in VR reveals. object_culling false draws them anyway (see
|
||||
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled. The PC
|
||||
@@ -609,6 +612,8 @@ inline void EnsureConfigFile() {
|
||||
"hand_steering = true\n"
|
||||
"# Show the settled inventory item in one cockpit hand: left, right, or off.\n"
|
||||
"cockpit_item_hand = \"left\"\n"
|
||||
"# Swing that hand forward or back to throw the item that way.\n"
|
||||
"cockpit_item_throw = true\n"
|
||||
"wheel_kart_degrees = 90.0\n"
|
||||
"wheel_bike_degrees = 45.0\n"
|
||||
"wheel_grab_distance = 0.35\n"
|
||||
@@ -885,6 +890,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.vrObjectCulling = FindConfigValue<bool>(document, "vr", "object_culling");
|
||||
config.vrHandSteering = FindConfigValue<bool>(document, "vr", "hand_steering");
|
||||
config.vrCockpitItemHand = FindConfigValue<std::string>(document, "vr", "cockpit_item_hand");
|
||||
config.vrCockpitItemThrow = FindConfigValue<bool>(document, "vr", "cockpit_item_throw");
|
||||
config.vrHandTracking = FindConfigValue<bool>(document, "vr", "hand_tracking");
|
||||
config.vrWheelKartDegrees = readRangedFloat("wheel_kart_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
|
||||
config.vrWheelBikeDegrees = readRangedFloat("wheel_bike_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
|
||||
@@ -1293,6 +1299,11 @@ inline bool SetVrCockpitItemHand(const std::string& value) {
|
||||
return WriteSetting("vr", "cockpit_item_hand", "\"" + value + "\"");
|
||||
}
|
||||
|
||||
inline bool SetVrCockpitItemThrow(bool value) {
|
||||
Mutable().vrCockpitItemThrow = value;
|
||||
return WriteSetting("vr", "cockpit_item_throw", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrHandTracking(bool value) {
|
||||
Mutable().vrHandTracking = value;
|
||||
return WriteSetting("vr", "hand_tracking", value ? "true" : "false");
|
||||
@@ -1789,6 +1800,10 @@ inline std::string VrCockpitItemHand() {
|
||||
return value == "left" || value == "right" || value == "off" ? value : kVrCockpitItemHandDefault;
|
||||
}
|
||||
|
||||
inline bool VrCockpitItemThrow(bool fallback = kVrCockpitItemThrowDefault) {
|
||||
return Get().vrCockpitItemThrow.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrHandTracking(bool fallback = kVrHandTrackingDefault) {
|
||||
return Get().vrHandTracking.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/openxr_driving.h"
|
||||
#include "vr/openxr_hand_tracking.h"
|
||||
#include "vr/openxr_item_throw.h"
|
||||
#include "vr/openxr_runtime.h"
|
||||
#include "vr/openxr_settings_panel.h"
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
@@ -72,6 +73,11 @@ struct OpenXRPointerScreen {
|
||||
// the game (a shoulder on the gamepad; as a Wii Remote the grips are unbound,
|
||||
// so a hand on the wheel cannot hold down a button).
|
||||
//
|
||||
// Throwing the held item ([vr] cockpit_item_throw, openxr_item_throw.h): in the
|
||||
// cockpit, a quick swing forward or back of the hand showing the item pushes the
|
||||
// left stick that way while the left trigger (Z, or the GameCube's L) is pressed
|
||||
// and released, which the game reads as an aimed throw.
|
||||
//
|
||||
// Tracked hands ([vr] hand_tracking, with hand steering): two hand trackers
|
||||
// (XR_EXT_hand_tracking) located every frame give the cockpit the hands' own
|
||||
// joints, from the controllers' touch sensors while they are held and from the
|
||||
@@ -160,6 +166,9 @@ private:
|
||||
void UpdateDriving(XrTime display_time, const driving::SeatFrame& seat,
|
||||
std::array<wii_remote::HandInputs, kHands>& hands, bool withheld);
|
||||
void ResetDriving();
|
||||
// After the wheel and the bare hands: the item hand's swing, and the throw it
|
||||
// plays on the left hand's stick and trigger.
|
||||
void UpdateItemThrow(float dt_seconds, std::array<wii_remote::HandInputs, kHands>& hands, bool withheld);
|
||||
// Tracked hands: the extension's functions at Create, the trackers as the
|
||||
// settings ask for them, and both hands located for `time`, their joints
|
||||
// in `seat` when it is valid.
|
||||
@@ -255,6 +264,10 @@ private:
|
||||
hand_tracking::FlickDetector m_flick;
|
||||
XrTime m_flick_start = 0;
|
||||
bool m_injected_flick_held = false;
|
||||
// Throwing the held item.
|
||||
item_throw::ThrowDetector m_throw;
|
||||
item_throw::ThrowSequence m_throw_sequence;
|
||||
bool m_injected_throw_held = false;
|
||||
uint64_t m_profile_serial = 0;
|
||||
|
||||
bool m_created = false;
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// Throwing the held item ([vr] cockpit_item_throw). In the first-person cockpit,
|
||||
// a quick swing forward or backward of the hand that shows the item plays what
|
||||
// Mario Kart Wii reads as an aimed throw: the stick pushed forward or back while
|
||||
// the item button is pressed and released. It goes through the left hand's
|
||||
// controls, which both controller modes read as the stick and the item button
|
||||
// (the Nunchuk's stick and Z, the GameCube's stick and L), so it works whichever
|
||||
// hand shows the item and with bare hands too. Nothing here depends on OpenXR,
|
||||
// so the rules are tested headlessly (tests/vr_item_throw_tests.cpp).
|
||||
|
||||
#include "vr/openxr_wii_remote.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace mkw::vr::item_throw {
|
||||
|
||||
enum class Direction : uint8_t { None, Forward, Backward };
|
||||
|
||||
inline const char* DirectionLabel(Direction direction) noexcept {
|
||||
switch (direction) {
|
||||
case Direction::Forward: return "forward";
|
||||
case Direction::Backward: return "backward";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool IsFinite(float value) noexcept {
|
||||
// Bit test: the runtime may be built with -ffast-math.
|
||||
uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return (bits & 0x7F800000u) != 0x7F800000u;
|
||||
}
|
||||
|
||||
// The item hand this frame, in the seated frame (+X right, +Y up, -Z forward),
|
||||
// in metres: the grip, or the palm joint when the hand is drawn from joints.
|
||||
struct HandSample {
|
||||
bool tracked = false;
|
||||
bool held = false; // holding the wheel or handlebar
|
||||
bool bare = false; // tracked by the headset's cameras, no controller in it
|
||||
std::array<float, 3> position{};
|
||||
};
|
||||
|
||||
// What counts as a throw for one kind of hand: the travel along the seated
|
||||
// frame's forward axis within the window, how much of the window's displacement
|
||||
// must be along that axis, the speed along it that means the swing goes on, and
|
||||
// how long a loss of tracking is bridged by the positions either side of it.
|
||||
struct Tuning {
|
||||
float window_seconds;
|
||||
float forward_travel; // m
|
||||
float backward_travel; // m: throwing back is harder
|
||||
float axis_share;
|
||||
float moving_speed; // m/s
|
||||
float max_gap_seconds;
|
||||
size_t min_samples;
|
||||
};
|
||||
// A controller is located smoothly every frame: 1.5 m/s forward, 1.25 back.
|
||||
inline constexpr Tuning kControllerTuning{0.12f, 0.18f, 0.15f, 0.7f, 0.5f, 0.1f, 3};
|
||||
// The cameras lag and smooth a bare hand and often lose it for a few frames in
|
||||
// the middle of a fast swing, so its swing reads slower and shorter: a longer
|
||||
// window, less travel (1 m/s forward, 0.87 back), and the gap bridged.
|
||||
inline constexpr Tuning kBareHandTuning{0.15f, 0.15f, 0.13f, 0.65f, 0.3f, 0.2f, 2};
|
||||
|
||||
// A throw is a swing along the seated frame's forward axis: within the last
|
||||
// window the hand has covered at least the direction's travel along that axis,
|
||||
// mostly along it (so a sideways sweep or the upward flick that does a trick
|
||||
// never counts), and it is still moving that way. Reaching for the wheel or
|
||||
// bringing a hand back to rest is too slow to cover the travel. The window is
|
||||
// counted in tracked time, so a bridged gap does not use it up; across a gap
|
||||
// the swing must still average most of the window's speed, so a slow drift the
|
||||
// cameras briefly lost never counts. A hand fires once per swing and then waits
|
||||
// out the cooldown; a hand that just let go of the wheel must be free a moment
|
||||
// first; a tracking jump, faster than any hand, restarts the track.
|
||||
class ThrowDetector {
|
||||
public:
|
||||
static constexpr float kJumpSpeed = 8.0f; // m/s: faster than a hand, a tracking jump
|
||||
static constexpr float kGapSpeedShare = 0.7f;
|
||||
static constexpr float kFreeSeconds = 0.25f;
|
||||
static constexpr float kCooldownSeconds = 0.6f;
|
||||
|
||||
// What the last throw measured, for the log.
|
||||
struct Measure {
|
||||
float travel = 0.0f; // m along the forward axis, + forward
|
||||
float seconds = 0.0f; // the window it covered, gaps included
|
||||
bool bare = false;
|
||||
bool bridged = false; // the window spans a loss of tracking
|
||||
};
|
||||
|
||||
Direction Update(const HandSample& hand, float dt) noexcept {
|
||||
if (!IsFinite(dt) || dt <= 0.0f) {
|
||||
return Direction::None;
|
||||
}
|
||||
dt = std::min(dt, 0.1f);
|
||||
m_cooldown = std::max(m_cooldown - dt, 0.0f);
|
||||
m_free = std::min(m_free + dt, 1.0f);
|
||||
m_time += dt;
|
||||
const Tuning& tuning = hand.bare ? kBareHandTuning : kControllerTuning;
|
||||
const auto& p = hand.position;
|
||||
if (!hand.tracked || !IsFinite(p[0]) || !IsFinite(p[1]) || !IsFinite(p[2])) {
|
||||
if (m_count > 0 && m_time - At(0).time > tuning.max_gap_seconds) {
|
||||
ClearTrack();
|
||||
}
|
||||
return Direction::None;
|
||||
}
|
||||
if (hand.held) {
|
||||
ClearTrack();
|
||||
m_free = 0.0f;
|
||||
return Direction::None;
|
||||
}
|
||||
bool bridged_now = false;
|
||||
if (m_count > 0) {
|
||||
const Sample& last = At(0);
|
||||
const float elapsed = m_time - last.time;
|
||||
if (elapsed > tuning.max_gap_seconds + dt ||
|
||||
Distance(p, last.position) / std::max(elapsed, 1.0e-4f) > kJumpSpeed) {
|
||||
ClearTrack();
|
||||
} else {
|
||||
bridged_now = elapsed > dt + 1.0e-4f;
|
||||
}
|
||||
}
|
||||
m_tracked_time += dt;
|
||||
Push({m_time, m_tracked_time, bridged_now, p});
|
||||
if (m_count < 2) {
|
||||
return Direction::None;
|
||||
}
|
||||
// The swing's speed along the axis now (+ forward), from the last step.
|
||||
const Sample& previous = At(1);
|
||||
const float forward_speed = -(p[2] - previous.position[2]) / std::max(m_time - previous.time, 1.0e-4f);
|
||||
if (m_swing != Direction::None && Along(m_swing, forward_speed) < tuning.moving_speed) {
|
||||
m_swing = Direction::None; // that swing is over
|
||||
}
|
||||
// The oldest sample still inside the window, in tracked time.
|
||||
size_t oldest = 0;
|
||||
bool bridged = false;
|
||||
for (size_t i = 1; i < m_count && m_tracked_time - At(i).tracked <= tuning.window_seconds + 1.0e-4f; ++i) {
|
||||
bridged = bridged || At(i - 1).bridged;
|
||||
oldest = i;
|
||||
}
|
||||
if (oldest + 1 < tuning.min_samples) {
|
||||
return Direction::None;
|
||||
}
|
||||
const Sample& start = At(oldest);
|
||||
const std::array<float, 3> moved{p[0] - start.position[0], p[1] - start.position[1], p[2] - start.position[2]};
|
||||
const float length = std::sqrt(moved[0] * moved[0] + moved[1] * moved[1] + moved[2] * moved[2]);
|
||||
const float travel = -moved[2];
|
||||
const float seconds = m_time - start.time;
|
||||
const Direction direction = travel >= tuning.forward_travel ? Direction::Forward
|
||||
: -travel >= tuning.backward_travel ? Direction::Backward
|
||||
: Direction::None;
|
||||
if (direction == Direction::None) {
|
||||
return Direction::None;
|
||||
}
|
||||
const float needed = direction == Direction::Forward ? tuning.forward_travel : tuning.backward_travel;
|
||||
if (std::fabs(travel) < tuning.axis_share * length || Along(direction, forward_speed) < tuning.moving_speed ||
|
||||
std::fabs(travel) / std::max(seconds, 1.0e-4f) < kGapSpeedShare * needed / tuning.window_seconds) {
|
||||
return Direction::None;
|
||||
}
|
||||
if (m_free < kFreeSeconds || m_cooldown > 0.0f || m_swing == direction) {
|
||||
// Too soon: this whole swing is spent, however long it goes on.
|
||||
m_swing = direction;
|
||||
return Direction::None;
|
||||
}
|
||||
m_cooldown = kCooldownSeconds;
|
||||
m_swing = direction;
|
||||
m_last = {travel, seconds, hand.bare, bridged};
|
||||
return direction;
|
||||
}
|
||||
|
||||
const Measure& Last() const noexcept { return m_last; }
|
||||
|
||||
void Reset() noexcept { *this = ThrowDetector{}; }
|
||||
|
||||
private:
|
||||
struct Sample {
|
||||
float time = 0.0f; // since the detector started, gaps included
|
||||
float tracked = 0.0f; // tracked time only
|
||||
bool bridged = false; // follows a loss of tracking
|
||||
std::array<float, 3> position{};
|
||||
};
|
||||
static constexpr size_t kCapacity = 32; // over either window at any headset's rate
|
||||
|
||||
static float Along(Direction direction, float forward_speed) noexcept {
|
||||
return direction == Direction::Forward ? forward_speed : -forward_speed;
|
||||
}
|
||||
static float Distance(const std::array<float, 3>& a, const std::array<float, 3>& b) noexcept {
|
||||
const float x = a[0] - b[0], y = a[1] - b[1], z = a[2] - b[2];
|
||||
return std::sqrt(x * x + y * y + z * z);
|
||||
}
|
||||
// Newest first: At(0) is the latest sample.
|
||||
const Sample& At(size_t age) const noexcept { return m_samples[(m_next + kCapacity - 1 - age) % kCapacity]; }
|
||||
void Push(const Sample& sample) noexcept {
|
||||
m_samples[m_next] = sample;
|
||||
m_next = (m_next + 1) % kCapacity;
|
||||
m_count = std::min(m_count + 1, kCapacity);
|
||||
}
|
||||
void ClearTrack() noexcept {
|
||||
m_count = 0;
|
||||
m_swing = Direction::None;
|
||||
}
|
||||
|
||||
std::array<Sample, kCapacity> m_samples{};
|
||||
size_t m_next = 0;
|
||||
size_t m_count = 0;
|
||||
float m_time = 0.0f;
|
||||
float m_tracked_time = 0.0f;
|
||||
float m_free = 1.0f; // seconds since the hand last held the wheel, capped
|
||||
float m_cooldown = 0.0f;
|
||||
Direction m_swing = Direction::None; // the swing that last fired, while it lasts
|
||||
Measure m_last{};
|
||||
};
|
||||
|
||||
// The input a throw plays on the left hand's controls. The stick goes first and
|
||||
// stays until the item button has been pressed and released again, so an item
|
||||
// used on the press (a Mushroom) and one thrown on the release (a trailed shell
|
||||
// or banana) both see the aim. A button the player was already holding, to
|
||||
// trail an item, is released by the throw and then ignored until the player
|
||||
// lets go of it, so it does not use the next item of a triple.
|
||||
class ThrowSequence {
|
||||
public:
|
||||
static constexpr float kAimSeconds = 0.05f; // the stick alone, a few game frames
|
||||
static constexpr float kPressSeconds = 0.10f;
|
||||
static constexpr float kReleaseSeconds = 0.10f; // the stick still aimed
|
||||
|
||||
void Start(Direction direction) noexcept {
|
||||
if (direction == Direction::None) {
|
||||
return;
|
||||
}
|
||||
m_direction = direction;
|
||||
m_elapsed = 0.0f;
|
||||
}
|
||||
|
||||
bool Active() const noexcept { return m_direction != Direction::None; }
|
||||
|
||||
void Apply(wii_remote::HandInputs& left, float dt) noexcept {
|
||||
const bool held = left.trigger > wii_remote::kPressThreshold;
|
||||
if (m_ignore_held) {
|
||||
if (held) {
|
||||
left.trigger = 0.0f;
|
||||
} else {
|
||||
m_ignore_held = false;
|
||||
}
|
||||
}
|
||||
if (!Active()) {
|
||||
return;
|
||||
}
|
||||
left.stick_y = m_direction == Direction::Forward ? 1.0f : -1.0f;
|
||||
if (m_elapsed >= kAimSeconds + kPressSeconds) {
|
||||
m_ignore_held = m_ignore_held || held;
|
||||
left.trigger = 0.0f;
|
||||
} else if (m_elapsed >= kAimSeconds) {
|
||||
left.trigger = 1.0f;
|
||||
}
|
||||
m_elapsed += IsFinite(dt) ? std::clamp(dt, 0.0f, 0.1f) : 0.0f;
|
||||
if (m_elapsed >= kAimSeconds + kPressSeconds + kReleaseSeconds) {
|
||||
m_direction = Direction::None;
|
||||
}
|
||||
}
|
||||
|
||||
void Reset() noexcept { *this = ThrowSequence{}; }
|
||||
|
||||
private:
|
||||
Direction m_direction = Direction::None;
|
||||
float m_elapsed = 0.0f;
|
||||
bool m_ignore_held = false;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr::item_throw
|
||||
@@ -171,6 +171,7 @@ bool g_vrNativeSteeringWheel = RuntimeConfigFile::VrNativeSteeringWheel();
|
||||
bool g_vrObjectCulling = RuntimeConfigFile::VrObjectCulling();
|
||||
bool g_vrHandSteering = RuntimeConfigFile::VrHandSteering();
|
||||
constexpr std::array<const char*, 3> kVrCockpitItemHands{"Left", "Right", "Off"};
|
||||
bool g_vrCockpitItemThrow = RuntimeConfigFile::VrCockpitItemThrow();
|
||||
int g_vrCockpitItemHand = RuntimeConfigFile::VrCockpitItemHand() == "right" ? 1 :
|
||||
RuntimeConfigFile::VrCockpitItemHand() == "off" ? 2 : 0;
|
||||
mkw::vr::WheelTuning g_vrWheelTuning = RuntimeConfigFile::VrWheelTuning();
|
||||
@@ -1235,6 +1236,17 @@ void DrawVrSteeringWheelSettings() {
|
||||
ImGui::SetTooltip("Show Player 1's settled inventory item above this palm. "
|
||||
"Triple items show their remaining count. Using or losing the item hides it.");
|
||||
}
|
||||
ImGui::BeginDisabled(g_vrCockpitItemHand == 2);
|
||||
if (ImGui::Checkbox("Throw the item by swinging that hand", &g_vrCockpitItemThrow)) {
|
||||
RuntimeConfigFile::SetVrCockpitItemThrow(g_vrCockpitItemThrow);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("With the hand off the wheel, swing it quickly forward to throw the item ahead "
|
||||
"of the kart, or back to throw it behind: the stick and item button the game "
|
||||
"reads for an aimed throw. Swinging while holding the item button throws the "
|
||||
"trailed item.");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
#if defined(__ANDROID__)
|
||||
ImGui::BeginDisabled(!g_vrHandSteering && g_vrCockpitItemHand == 2);
|
||||
if (ImGui::Checkbox("Tracked hands", &g_vrHandTracking)) {
|
||||
@@ -1759,12 +1771,14 @@ void DrawVrCameraSettings() {
|
||||
g_vrNativeSteeringWheel = RuntimeConfigFile::kVrNativeSteeringWheelDefault;
|
||||
g_vrHandSteering = RuntimeConfigFile::kVrHandSteeringDefault;
|
||||
g_vrCockpitItemHand = 0;
|
||||
g_vrCockpitItemThrow = RuntimeConfigFile::kVrCockpitItemThrowDefault;
|
||||
RuntimeConfigFile::SetVrFirstPersonSeat(RuntimeConfigFile::kVrFirstPersonSeatDefault);
|
||||
RuntimeConfigFile::SetVrCockpitUnitsPerMeter(g_vrCockpitUnitsPerMeter);
|
||||
RuntimeConfigFile::SetVrSteeringWheel(g_vrSteeringWheel);
|
||||
RuntimeConfigFile::SetVrNativeSteeringWheel(g_vrNativeSteeringWheel);
|
||||
RuntimeConfigFile::SetVrHandSteering(g_vrHandSteering);
|
||||
RuntimeConfigFile::SetVrCockpitItemHand(RuntimeConfigFile::kVrCockpitItemHandDefault);
|
||||
RuntimeConfigFile::SetVrCockpitItemThrow(g_vrCockpitItemThrow);
|
||||
#if defined(__ANDROID__)
|
||||
g_vrHandTracking = RuntimeConfigFile::kVrHandTrackingDefault;
|
||||
RuntimeConfigFile::SetVrHandTracking(g_vrHandTracking);
|
||||
|
||||
@@ -215,8 +215,9 @@ VirtualGamepadRelay& Relay() {
|
||||
// directions push the Nunchuk stick). `panel` presses the settings panel's
|
||||
// button (left Y, or both thumbsticks for a gamepad), opening or closing it,
|
||||
// where `a` then selects. `flick` plays the bare hands' flick, one shake of the
|
||||
// remote. The property is unset in normal use, so this costs one property read
|
||||
// every few frames.
|
||||
// remote; `throw_forward` and `throw_backward` play a throw of the held item.
|
||||
// The property is unset in normal use, so this costs one property read every
|
||||
// few frames.
|
||||
constexpr uint32_t kInjectHoldFrames = 12;
|
||||
constexpr uint32_t kInjectPollFrames = 4;
|
||||
|
||||
@@ -857,6 +858,7 @@ void OpenXRInput::Destroy() {
|
||||
m_sources_logged_at = 0;
|
||||
m_squeeze_active = m_hand_driven = m_pinch = {};
|
||||
m_injected_flick_held = false;
|
||||
m_injected_throw_held = false;
|
||||
m_profile_serial = 0;
|
||||
DestroyPoseSpaces();
|
||||
if (m_action_set != XR_NULL_HANDLE) {
|
||||
@@ -1133,6 +1135,10 @@ void OpenXRInput::Sync(XrTime predicted_display_time, const OpenXRPointerScreen&
|
||||
m_injected_flick_held = injected_flick;
|
||||
#endif
|
||||
|
||||
// Throwing the held item: a quick swing of the item hand forward or back
|
||||
// plays an aimed throw on the stick and the item button.
|
||||
UpdateItemThrow(dt_seconds, hands, withheld);
|
||||
|
||||
// What the game sees of the controllers: nothing held while they are withheld.
|
||||
static const std::array<wii_remote::HandInputs, kHands> kIdleHands{};
|
||||
const auto& game_hands = withheld ? kIdleHands : hands;
|
||||
@@ -1319,9 +1325,76 @@ void OpenXRInput::ResetDriving() {
|
||||
}
|
||||
m_flick.Reset();
|
||||
m_flick_start = 0;
|
||||
m_throw.Reset();
|
||||
m_throw_sequence.Reset();
|
||||
OpenXRPublishDriving(m_driving);
|
||||
}
|
||||
|
||||
void OpenXRInput::UpdateItemThrow(float dt_seconds, std::array<wii_remote::HandInputs, kHands>& hands,
|
||||
bool withheld) {
|
||||
const std::string item_hand = RuntimeConfigFile::VrCockpitItemHand();
|
||||
const uint32_t hand = item_hand == "right" ? 1u : 0u;
|
||||
item_throw::Direction direction = item_throw::Direction::None;
|
||||
if (!withheld && m_driving.cockpit_active && item_hand != "off" && RuntimeConfigFile::VrCockpitItemThrow()) {
|
||||
// The hand as the cockpit draws the item on it: the palm joint when the
|
||||
// hand is drawn from joints, else the grip. The detector follows it with
|
||||
// or without an item, so it always knows when the hand left the wheel.
|
||||
const DrivingHand& located = m_driving.hands[hand];
|
||||
const std::array<float, 12>& pose =
|
||||
located.joints_valid ? m_joint_frame.seat_from_joint[hand][hand_tracking::kPalm] : located.seat_from_grip;
|
||||
item_throw::HandSample sample{};
|
||||
sample.tracked = located.tracked;
|
||||
sample.held = located.held;
|
||||
sample.bare = located.bare;
|
||||
sample.position = {pose[3], pose[7], pose[11]};
|
||||
direction = m_throw.Update(sample, dt_seconds);
|
||||
// A swing with nothing in the hand throws nothing.
|
||||
if (direction != item_throw::Direction::None) {
|
||||
const HeldItem item = MkwVRFirstPersonGetHeldItem();
|
||||
if (!item.valid || item.count == 0) {
|
||||
direction = item_throw::Direction::None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m_throw.Reset();
|
||||
}
|
||||
// `debug.wiicompiled.inject <n>:throw_forward` or `throw_backward` plays the
|
||||
// same throw without the swing, to try it unattended.
|
||||
const bool injected_forward = Injected("throw_forward");
|
||||
const bool injected_backward = Injected("throw_backward");
|
||||
bool injected = false;
|
||||
if ((injected_forward || injected_backward) && !m_injected_throw_held && !withheld) {
|
||||
direction = injected_forward ? item_throw::Direction::Forward : item_throw::Direction::Backward;
|
||||
injected = true;
|
||||
}
|
||||
m_injected_throw_held = injected_forward || injected_backward;
|
||||
if (direction != item_throw::Direction::None) {
|
||||
m_throw_sequence.Start(direction);
|
||||
if (RuntimeConfigFile::VrWheelTuning().haptics) {
|
||||
constexpr XrDuration kThrowPulseNs = 40'000'000;
|
||||
ApplyHaptic(hand, 0.5f, kThrowPulseNs);
|
||||
}
|
||||
// What the swing measured, to tune the thresholds from a run log.
|
||||
std::ostringstream message;
|
||||
message << "Held item thrown " << item_throw::DirectionLabel(direction);
|
||||
if (injected) {
|
||||
message << " (injected)";
|
||||
} else {
|
||||
const auto& measure = m_throw.Last();
|
||||
message << " (" << (measure.bare ? "bare hand" : "controller") << ", "
|
||||
<< std::lround(std::fabs(measure.travel) * 100.0f) << " cm in "
|
||||
<< std::lround(measure.seconds * 1000.0f) << " ms"
|
||||
<< (measure.bridged ? ", across a tracking gap" : "") << ')';
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, message.str());
|
||||
}
|
||||
if (withheld) {
|
||||
m_throw_sequence.Reset();
|
||||
return;
|
||||
}
|
||||
m_throw_sequence.Apply(hands[0], dt_seconds);
|
||||
}
|
||||
|
||||
void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& seat,
|
||||
std::array<wii_remote::HandInputs, kHands>& hands, bool withheld) {
|
||||
const FirstPersonAnchor anchor = MkwVRFirstPersonGetAnchor();
|
||||
|
||||
@@ -22,6 +22,10 @@ int main() {
|
||||
Require(std::string_view(RuntimeConfigFile::kVrCockpitItemHandDefault) == "left");
|
||||
Require(!Parse("[vr]\n").vrCockpitItemHand.has_value());
|
||||
Require(!Parse("[vr]\ncockpit_item_hand = 1\n").vrCockpitItemHand.has_value());
|
||||
Require(RuntimeConfigFile::kVrCockpitItemThrowDefault);
|
||||
Require(Parse("[vr]\ncockpit_item_throw = false\n").vrCockpitItemThrow == false);
|
||||
Require(Parse("[vr]\ncockpit_item_throw = true\n").vrCockpitItemThrow == true);
|
||||
Require(!Parse("[vr]\n").vrCockpitItemThrow.has_value());
|
||||
// [vr] foveation: the Quest's foveated rendering level, index-matched to
|
||||
// aurora_set_stereo_foveation.
|
||||
for (std::string_view level : RuntimeConfigFile::kVrFoveationLevels) {
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Throwing the held item: the swing detector and the stick and item button it
|
||||
// plays, tested without a headset (vr/openxr_item_throw.h).
|
||||
|
||||
#include "vr/openxr_item_throw.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace mkw::vr;
|
||||
using namespace mkw::vr::item_throw;
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void Check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
++g_failures;
|
||||
std::cerr << "FAILED: " << what << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
struct Fire {
|
||||
Direction direction;
|
||||
float time;
|
||||
};
|
||||
|
||||
// Plays `duration` seconds of a hand moving with `velocity(t)` (m/s in the seated
|
||||
// frame) from `start`, at `rate` samples a second, and returns every throw.
|
||||
std::vector<Fire> Play(ThrowDetector& detector, const std::function<std::array<float, 3>(float)>& velocity,
|
||||
float duration, float rate = 90.0f, std::array<float, 3> start = {-0.2f, -0.3f, -0.35f},
|
||||
const std::function<bool(float)>& held = nullptr, bool bare = false,
|
||||
const std::function<bool(float)>& tracked = nullptr) {
|
||||
std::vector<Fire> fires;
|
||||
const float dt = 1.0f / rate;
|
||||
std::array<float, 3> position = start;
|
||||
for (float t = 0.0f; t < duration; t += dt) {
|
||||
const auto v = velocity(t);
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
position[axis] += v[axis] * dt;
|
||||
}
|
||||
HandSample sample{};
|
||||
sample.tracked = tracked ? tracked(t) : true;
|
||||
sample.held = held ? held(t) : false;
|
||||
sample.bare = bare;
|
||||
sample.position = position;
|
||||
const Direction direction = detector.Update(sample, dt);
|
||||
if (direction != Direction::None) {
|
||||
fires.push_back({direction, t});
|
||||
}
|
||||
}
|
||||
return fires;
|
||||
}
|
||||
|
||||
// A flick: speeds up at 30 m/s^2 to `peak`, holds it briefly and stops.
|
||||
std::function<std::array<float, 3>(float)> Flick(std::array<float, 3> direction, float peak, float start = 0.1f) {
|
||||
return [=](float t) {
|
||||
const float local = t - start;
|
||||
float speed = 0.0f;
|
||||
if (local >= 0.0f && local < 0.25f) {
|
||||
speed = std::min(30.0f * local, peak);
|
||||
}
|
||||
return std::array<float, 3>{direction[0] * speed, direction[1] * speed, direction[2] * speed};
|
||||
};
|
||||
}
|
||||
|
||||
void TestThrows() {
|
||||
for (float rate : {72.0f, 90.0f, 120.0f}) {
|
||||
ThrowDetector forward;
|
||||
const auto fires = Play(forward, Flick({0.0f, 0.0f, -1.0f}, 3.0f), 0.8f, rate);
|
||||
Check(fires.size() == 1 && fires[0].direction == Direction::Forward, "a forward flick throws forward");
|
||||
Check(!fires.empty() && fires[0].time < 0.35f, "the throw fires during the swing");
|
||||
ThrowDetector backward;
|
||||
const auto back = Play(backward, Flick({0.0f, 0.0f, 1.0f}, 2.5f), 0.8f, rate);
|
||||
Check(back.size() == 1 && back[0].direction == Direction::Backward, "a backward flick throws backward");
|
||||
}
|
||||
// An overhand throw ends forward and down; an underhand toss forward and up.
|
||||
ThrowDetector overhand;
|
||||
Check(Play(overhand, Flick({0.0f, -0.45f, -0.89f}, 3.0f), 0.8f).size() == 1, "an overhand throw counts");
|
||||
ThrowDetector underhand;
|
||||
Check(Play(underhand, Flick({0.0f, 0.45f, -0.89f}, 3.0f), 0.8f).size() == 1, "an underhand toss counts");
|
||||
}
|
||||
|
||||
void TestNotThrows() {
|
||||
ThrowDetector reach;
|
||||
// Reaching for the wheel, and bringing a hand back to rest.
|
||||
Check(Play(reach, [](float t) { return std::array<float, 3>{0.0f, 0.0f, t < 0.4f ? -0.9f : 0.0f}; }, 1.0f).empty(),
|
||||
"reaching for the wheel is no throw");
|
||||
ThrowDetector rest;
|
||||
Check(Play(rest, [](float t) { return std::array<float, 3>{0.0f, 0.0f, t < 0.35f ? 0.9f : 0.0f}; }, 1.0f).empty(),
|
||||
"bringing a hand back is no throw");
|
||||
ThrowDetector sideways;
|
||||
Check(Play(sideways, Flick({1.0f, 0.0f, 0.0f}, 3.0f), 0.8f).empty(), "a sideways sweep is no throw");
|
||||
ThrowDetector upward;
|
||||
Check(Play(upward, Flick({0.0f, 1.0f, 0.0f}, 3.0f), 0.8f).empty(), "an upward flick (a trick) is no throw");
|
||||
ThrowDetector steep;
|
||||
Check(Play(steep, Flick({0.0f, 0.87f, -0.5f}, 3.0f), 0.8f).empty(), "a mostly upward swing is no throw");
|
||||
ThrowDetector still;
|
||||
Check(Play(still, [](float) { return std::array<float, 3>{}; }, 1.0f).empty(), "a still hand is no throw");
|
||||
ThrowDetector untracked;
|
||||
HandSample lost{};
|
||||
lost.position = {0.0f, 0.0f, -5.0f};
|
||||
const bool first = untracked.Update(lost, 0.011f) == Direction::None;
|
||||
lost.position = {0.0f, 0.0f, 5.0f};
|
||||
Check(first && untracked.Update(lost, 0.011f) == Direction::None, "an untracked hand is no throw");
|
||||
}
|
||||
|
||||
void TestWheel() {
|
||||
// A swing while holding the wheel, then right after letting go of it.
|
||||
ThrowDetector on_wheel;
|
||||
Check(Play(on_wheel, Flick({0.0f, 0.0f, -1.0f}, 3.0f), 0.8f, 90.0f, {-0.2f, -0.3f, -0.35f},
|
||||
[](float) { return true; })
|
||||
.empty(),
|
||||
"a hand on the wheel does not throw");
|
||||
ThrowDetector just_free;
|
||||
Check(Play(just_free, Flick({0.0f, 0.0f, -1.0f}, 3.0f, 0.1f), 0.8f, 90.0f, {-0.2f, -0.3f, -0.35f},
|
||||
[](float t) { return t < 0.05f; })
|
||||
.empty(),
|
||||
"a hand that just let go of the wheel does not throw");
|
||||
ThrowDetector free;
|
||||
Check(Play(free, Flick({0.0f, 0.0f, -1.0f}, 3.0f, 0.4f), 1.0f, 90.0f, {-0.2f, -0.3f, -0.35f},
|
||||
[](float t) { return t < 0.05f; })
|
||||
.size() == 1,
|
||||
"a hand free for a moment throws");
|
||||
}
|
||||
|
||||
void TestJumpAndRepeats() {
|
||||
ThrowDetector jump;
|
||||
HandSample sample{};
|
||||
sample.tracked = true;
|
||||
sample.position = {-0.2f, -0.3f, -0.35f};
|
||||
bool fired = false;
|
||||
for (int frame = 0; frame < 60; ++frame) {
|
||||
if (frame == 30) {
|
||||
sample.position[2] -= 0.5f; // the pose snaps half a metre ahead
|
||||
}
|
||||
fired = fired || jump.Update(sample, 1.0f / 90.0f) != Direction::None;
|
||||
}
|
||||
Check(!fired, "a tracking jump is no throw");
|
||||
|
||||
ThrowDetector long_swing;
|
||||
Check(Play(long_swing, [](float t) { return std::array<float, 3>{0.0f, 0.0f, t > 0.1f ? -2.0f : 0.0f}; }, 1.2f)
|
||||
.size() == 1,
|
||||
"one long swing throws once");
|
||||
const auto two = [](float gap) {
|
||||
return [gap](float t) {
|
||||
const bool first = t >= 0.1f && t < 0.3f, second = t >= 0.3f + gap && t < 0.5f + gap;
|
||||
return std::array<float, 3>{0.0f, 0.0f, first || second ? -2.5f : 0.0f};
|
||||
};
|
||||
};
|
||||
ThrowDetector close;
|
||||
Check(Play(close, two(0.2f), 1.5f).size() == 1, "a second swing inside the cooldown does not throw");
|
||||
ThrowDetector apart;
|
||||
Check(Play(apart, two(0.6f), 2.0f).size() == 2, "a second swing after the cooldown throws again");
|
||||
}
|
||||
|
||||
// Bare hands: the cameras lag, smooth and drop a hand in a fast swing.
|
||||
void TestBareHands() {
|
||||
const auto none = [](float) { return false; };
|
||||
// A slower push than a controller needs: 1.4 m/s at its peak.
|
||||
ThrowDetector controller;
|
||||
Check(Play(controller, Flick({0.0f, 0.0f, -1.0f}, 1.4f), 0.8f).empty(), "a controller needs a faster swing");
|
||||
ThrowDetector bare;
|
||||
const auto push = Play(bare, Flick({0.0f, 0.0f, -1.0f}, 1.4f), 0.8f, 60.0f, {-0.2f, -0.3f, -0.35f}, none, true);
|
||||
Check(push.size() == 1 && push[0].direction == Direction::Forward, "a bare hand's slower push throws");
|
||||
Check(bare.Last().bare && bare.Last().travel >= kBareHandTuning.forward_travel, "the throw's measure");
|
||||
ThrowDetector bare_back;
|
||||
const auto back = Play(bare_back, Flick({0.0f, 0.0f, 1.0f}, 1.2f), 0.8f, 60.0f, {-0.2f, -0.3f, -0.35f}, none, true);
|
||||
Check(back.size() == 1 && back[0].direction == Direction::Backward, "a bare hand throws backward");
|
||||
|
||||
// Tracking is lost for the fast part of the swing and comes back as the
|
||||
// hand stops, so only the positions either side of the gap show the throw:
|
||||
// 0.18 s for a bare hand, 0.09 s for a controller.
|
||||
for (bool is_bare : {false, true}) {
|
||||
const float speed = is_bare ? 2.0f : 3.0f, stop = is_bare ? 0.3f : 0.22f, found = is_bare ? 0.3f : 0.21f;
|
||||
ThrowDetector gap;
|
||||
const auto fires = Play(
|
||||
gap, [=](float t) { return std::array<float, 3>{0.0f, 0.0f, t >= 0.1f && t < stop ? -speed : 0.0f}; },
|
||||
0.8f, 72.0f, {-0.2f, -0.3f, -0.35f}, none, is_bare, [=](float t) { return t <= 0.12f || t > found; });
|
||||
Check(fires.size() == 1 && fires[0].direction == Direction::Forward, "a swing across a short gap throws");
|
||||
Check(gap.Last().bridged, "the throw's measure notes the gap");
|
||||
}
|
||||
// A hand the cameras lose for 0.3 s reappears 30 cm ahead: no throw.
|
||||
ThrowDetector long_gap;
|
||||
Check(Play(long_gap, [](float t) { return std::array<float, 3>{0.0f, 0.0f, t > 0.3f && t < 0.6f ? -1.0f : 0.0f}; },
|
||||
1.2f, 72.0f, {-0.2f, -0.3f, -0.35f}, none, true, [](float t) { return t < 0.3f || t > 0.6f; })
|
||||
.empty(),
|
||||
"a hand lost for long is no throw");
|
||||
// A slow drift the cameras lose briefly: 0.6 m/s across a 0.18 s gap.
|
||||
ThrowDetector drift;
|
||||
Check(Play(drift, [](float) { return std::array<float, 3>{0.0f, 0.0f, -0.6f}; }, 1.2f, 72.0f,
|
||||
{-0.2f, -0.3f, 0.0f}, none, true, [](float t) { return t < 0.5f || t > 0.68f; })
|
||||
.empty(),
|
||||
"a slow drift across a gap is no throw");
|
||||
// Reaching for the wheel with a bare hand.
|
||||
ThrowDetector reach;
|
||||
Check(Play(reach, [](float t) { return std::array<float, 3>{0.0f, 0.0f, t < 0.4f ? -0.8f : 0.0f}; }, 1.0f, 60.0f,
|
||||
{-0.2f, -0.3f, -0.35f}, none, true)
|
||||
.empty(),
|
||||
"a bare hand reaching for the wheel is no throw");
|
||||
}
|
||||
|
||||
struct Frame {
|
||||
float stick_y;
|
||||
float trigger;
|
||||
};
|
||||
|
||||
// Runs the sequence at 90 Hz with the player's own trigger from `player(t)`.
|
||||
std::vector<Frame> Sequence(Direction direction, const std::function<float(float)>& player, float duration,
|
||||
ThrowSequence& sequence) {
|
||||
std::vector<Frame> frames;
|
||||
sequence.Start(direction);
|
||||
const float dt = 1.0f / 90.0f;
|
||||
for (float t = 0.0f; t < duration; t += dt) {
|
||||
wii_remote::HandInputs left{};
|
||||
left.stick_x = 0.6f;
|
||||
left.trigger = player(t);
|
||||
sequence.Apply(left, dt);
|
||||
Check(left.stick_x == 0.6f, "the throw leaves the steering alone");
|
||||
frames.push_back({left.stick_y, left.trigger});
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
void TestSequence() {
|
||||
ThrowSequence sequence;
|
||||
const auto frames = Sequence(Direction::Forward, [](float) { return 0.0f; }, 0.4f, sequence);
|
||||
// 90 Hz: aim for frames 0-4 (0.05 s), press 5-13, release 14-22, then done.
|
||||
Check(frames[0].stick_y == 1.0f && frames[0].trigger == 0.0f, "the stick aims before the button");
|
||||
Check(frames[4].trigger == 0.0f && frames[5].trigger == 1.0f, "then the button is pressed");
|
||||
Check(frames[13].trigger == 1.0f && frames[14].trigger == 0.0f, "then released");
|
||||
Check(frames[14].stick_y == 1.0f && frames[21].stick_y == 1.0f, "with the stick still aimed");
|
||||
Check(frames[24].stick_y == 0.0f && !sequence.Active(), "then the stick is the player's again");
|
||||
ThrowSequence back;
|
||||
Check(Sequence(Direction::Backward, [](float) { return 0.0f; }, 0.1f, back)[0].stick_y == -1.0f,
|
||||
"a backward throw pulls the stick back");
|
||||
|
||||
// The player was trailing the item with the button held: the throw lets it
|
||||
// go, and the button stays released until the player lets go of it.
|
||||
ThrowSequence trailed;
|
||||
const auto held = Sequence(Direction::Forward, [](float t) { return t < 0.5f ? 1.0f : (t < 0.6f ? 0.0f : 1.0f); },
|
||||
0.8f, trailed);
|
||||
Check(held[2].trigger == 1.0f && held[10].trigger == 1.0f, "a held button stays pressed until the release");
|
||||
Check(held[14].trigger == 0.0f && held[30].trigger == 0.0f, "the throw releases a held button");
|
||||
Check(held[44].trigger == 0.0f, "a button still held after the throw is ignored");
|
||||
Check(held[60].trigger == 1.0f, "a new press after letting go uses the next item");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestThrows();
|
||||
TestNotThrows();
|
||||
TestWheel();
|
||||
TestJumpAndRepeats();
|
||||
TestBareHands();
|
||||
TestSequence();
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " check(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "mkw_vr_item_throw_tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user