forked from Clones/Controlify
➕ Another dramatic improvement to gyro control
This commit is contained in:
@ -9,7 +9,7 @@ import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||
import dev.isxander.controlify.gui.screen.ControllerCalibrationScreen;
|
||||
import dev.isxander.controlify.gui.screen.SDLOnboardingScreen;
|
||||
import dev.isxander.controlify.reacharound.ReachAroundHandler;
|
||||
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
|
||||
@ -251,7 +251,7 @@ public class Controlify implements ControlifyApi {
|
||||
if (!calibrationQueue.isEmpty() && !(minecraft.screen instanceof SDLOnboardingScreen)) {
|
||||
Screen screen = minecraft.screen;
|
||||
while (!calibrationQueue.isEmpty()) {
|
||||
screen = new ControllerDeadzoneCalibrationScreen(calibrationQueue.poll(), screen);
|
||||
screen = new ControllerCalibrationScreen(calibrationQueue.poll(), screen);
|
||||
}
|
||||
minecraft.setScreen(screen);
|
||||
}
|
||||
|
@ -12,10 +12,12 @@ public class GamepadConfig extends ControllerConfig {
|
||||
private transient float rightStickDeadzoneY = rightStickDeadzone;
|
||||
|
||||
public float gyroLookSensitivity = 0f;
|
||||
public boolean relativeGyroMode = false;
|
||||
public boolean gyroRequiresButton = true;
|
||||
public boolean flickStick = false;
|
||||
public boolean invertGyroX = false;
|
||||
public boolean invertGyroY = false;
|
||||
public GamepadState.GyroState gyroCalibration = GamepadState.GyroState.ORIGIN;
|
||||
|
||||
public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT;
|
||||
|
||||
|
@ -22,7 +22,7 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
private final RumbleManager rumbleManager;
|
||||
private GamepadState.GyroState absoluteGyro = GamepadState.GyroState.ORIGIN;
|
||||
|
||||
private final GamepadDrivers drivers;
|
||||
public final GamepadDrivers drivers;
|
||||
private final Set<Driver> uniqueDrivers;
|
||||
|
||||
private int antiSnapbackTicksL, antiSnapbackTicksR;
|
||||
@ -85,9 +85,9 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
}
|
||||
}
|
||||
|
||||
// todo: make this configurable
|
||||
GamepadState.GyroState gyroState = drivers.gyroDriver().getGyroState().deadzone(0.05f);
|
||||
this.absoluteGyro = this.absoluteGyro.add(gyroState);
|
||||
// TODO: Add some sort of gyro filtering
|
||||
GamepadState.GyroState gyroState = drivers.gyroDriver().getGyroState().subtracted(config().gyroCalibration);
|
||||
this.absoluteGyro = this.absoluteGyro.added(gyroState);
|
||||
|
||||
state = new GamepadState(deadzoneAxesState, basicState.axes(), basicState.buttons(), gyroState, absoluteGyro);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package dev.isxander.controlify.controller.gamepad;
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import dev.isxander.controlify.utils.ControllerUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@ -206,10 +205,22 @@ public final class GamepadState implements ControllerState {
|
||||
public record GyroState(float pitch, float yaw, float roll) {
|
||||
public static GyroState ORIGIN = new GyroState(0, 0, 0);
|
||||
|
||||
public GyroState add(GyroState other) {
|
||||
public GyroState added(GyroState other) {
|
||||
return new GyroState(pitch + other.pitch, yaw + other.yaw, roll + other.roll);
|
||||
}
|
||||
|
||||
public GyroState subtracted(GyroState other) {
|
||||
return new GyroState(pitch - other.pitch, yaw - other.yaw, roll - other.roll);
|
||||
}
|
||||
|
||||
public GyroState multiplied(float scalar) {
|
||||
return new GyroState(pitch * scalar, yaw * scalar, roll * scalar);
|
||||
}
|
||||
|
||||
public GyroState divided(float scalar) {
|
||||
return new GyroState(pitch / scalar, yaw / scalar, roll / scalar);
|
||||
}
|
||||
|
||||
public GyroState deadzone(float deadzone) {
|
||||
return new GyroState(
|
||||
Math.max(pitch - deadzone, 0) + Math.min(pitch + deadzone, 0),
|
||||
|
@ -3,6 +3,8 @@ package dev.isxander.controlify.gui.screen;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
@ -16,7 +18,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
public class ControllerCalibrationScreen extends Screen {
|
||||
private static final int CALIBRATION_TIME = 100;
|
||||
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
|
||||
|
||||
@ -30,13 +32,14 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
protected boolean calibrating = false, calibrated = false;
|
||||
protected int calibrationTicks = 0;
|
||||
|
||||
private final Map<Integer, double[]> calibrationData = new HashMap<>();
|
||||
private final Map<Integer, double[]> deadzoneCalibration = new HashMap<>();
|
||||
private GamepadState.GyroState accumulatedGyroVelocity = GamepadState.GyroState.ORIGIN;
|
||||
|
||||
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Screen parent) {
|
||||
public ControllerCalibrationScreen(Controller<?, ?> controller, Screen parent) {
|
||||
this(controller, () -> parent);
|
||||
}
|
||||
|
||||
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
|
||||
public ControllerCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
|
||||
super(Component.translatable("controlify.calibration.title"));
|
||||
this.controller = controller;
|
||||
this.parent = parent;
|
||||
@ -117,15 +120,19 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
|
||||
if (stateChanged()) {
|
||||
calibrationTicks = 0;
|
||||
calibrationData.clear();
|
||||
deadzoneCalibration.clear();
|
||||
accumulatedGyroVelocity = GamepadState.GyroState.ORIGIN;
|
||||
}
|
||||
|
||||
if (calibrationTicks < CALIBRATION_TIME) {
|
||||
calibrate(calibrationTicks);
|
||||
processDeadzoneData(calibrationTicks);
|
||||
processGyroData();
|
||||
|
||||
calibrationTicks++;
|
||||
} else {
|
||||
applyDeadzones();
|
||||
generateGyroCalibration();
|
||||
|
||||
calibrating = false;
|
||||
calibrated = true;
|
||||
readyButton.active = true;
|
||||
@ -136,22 +143,34 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
private void calibrate(int tick) {
|
||||
private void processDeadzoneData(int tick) {
|
||||
var axes = controller.state().rawAxes();
|
||||
|
||||
for (int i = 0; i < axes.size(); i++) {
|
||||
var axis = Math.abs(axes.get(i));
|
||||
calibrationData.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
|
||||
deadzoneCalibration.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
|
||||
}
|
||||
}
|
||||
|
||||
private void processGyroData() {
|
||||
if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) {
|
||||
accumulatedGyroVelocity = accumulatedGyroVelocity.added(gamepad.drivers.gyroDriver().getGyroState());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDeadzones() {
|
||||
calibrationData.forEach((i, data) -> {
|
||||
deadzoneCalibration.forEach((i, data) -> {
|
||||
var max = Arrays.stream(data).max().orElseThrow();
|
||||
controller.config().setDeadzone(i, (float) max + 0.05f);
|
||||
});
|
||||
}
|
||||
|
||||
private void generateGyroCalibration() {
|
||||
if (controller instanceof GamepadController gamepad && gamepad.hasGyro()) {
|
||||
gamepad.config().gyroCalibration = accumulatedGyroVelocity.divided(CALIBRATION_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stateChanged() {
|
||||
var amt = 0.4f;
|
||||
|
@ -272,7 +272,7 @@ public class ControllerConfigScreenFactory {
|
||||
.description(OptionDescription.createBuilder()
|
||||
.text(Component.translatable("controlify.gui.auto_calibration.tooltip"))
|
||||
.build())
|
||||
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, () -> {
|
||||
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerCalibrationScreen(controller, () -> {
|
||||
deadzoneOpts.forEach(Option::forgetPendingValue);
|
||||
return screen;
|
||||
})))
|
||||
@ -432,6 +432,17 @@ public class ControllerConfigScreenFactory {
|
||||
o.requestSetDefault();
|
||||
}))
|
||||
.build());
|
||||
var relativeModeOpt = Option.<Boolean>createBuilder()
|
||||
.name(Component.translatable("controlify.gui.gyro_behaviour"))
|
||||
.description(val -> OptionDescription.createBuilder()
|
||||
.text(Component.translatable("controlify.gui.gyro_behaviour.tooltip"))
|
||||
.text(val ? Component.translatable("controlify.gui.gyro_behaviour.relative.tooltip") : Component.translatable("controlify.gui.gyro_behaviour.absolute.tooltip"))
|
||||
.build())
|
||||
.binding(gpCfgDef.relativeGyroMode, () -> gpCfg.relativeGyroMode, v -> gpCfg.relativeGyroMode = v)
|
||||
.controller(opt -> BooleanControllerBuilder.create(opt)
|
||||
.valueFormatter(v -> v ? Component.translatable("controlify.gui.gyro_behaviour.relative") : Component.translatable("controlify.gui.gyro_behaviour.absolute")))
|
||||
.build();
|
||||
gyroGroup.option(relativeModeOpt);
|
||||
gyroGroup.option(Util.make(() -> {
|
||||
var opt = Option.<Boolean>createBuilder()
|
||||
.name(Component.translatable("controlify.gui.gyro_invert_x"))
|
||||
@ -461,6 +472,14 @@ public class ControllerConfigScreenFactory {
|
||||
.binding(gpCfgDef.gyroRequiresButton, () -> gpCfg.gyroRequiresButton, v -> gpCfg.gyroRequiresButton = v)
|
||||
.controller(TickBoxControllerBuilder::create)
|
||||
.available(gyroSensitivity.pendingValue() > 0)
|
||||
.listener((o, val) -> {
|
||||
if (val) {
|
||||
relativeModeOpt.setAvailable(gyroSensitivity.pendingValue() > 0);
|
||||
} else {
|
||||
relativeModeOpt.setAvailable(false);
|
||||
relativeModeOpt.requestSet(false);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
gyroOptions.add(opt);
|
||||
return opt;
|
||||
|
@ -6,6 +6,7 @@ import dev.isxander.controlify.api.ingameinput.LookInputModifier;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.api.event.ControlifyEvents;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
||||
import dev.isxander.controlify.utils.Animator;
|
||||
import dev.isxander.controlify.utils.Easings;
|
||||
import dev.isxander.controlify.utils.NavigationHelper;
|
||||
@ -17,8 +18,7 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector2fc;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
@ -30,6 +30,9 @@ public class InGameInputHandler {
|
||||
private double lookInputX, lookInputY;
|
||||
private boolean shouldShowPlayerList;
|
||||
|
||||
private GamepadState.GyroState gyroInput = GamepadState.GyroState.ORIGIN;
|
||||
private boolean wasAiming;
|
||||
|
||||
private final NavigationHelper dropRepeatHelper;
|
||||
|
||||
public InGameInputHandler(Controller<?, ?> controller) {
|
||||
@ -130,31 +133,30 @@ public class InGameInputHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
var isAiming = isAiming(player);
|
||||
|
||||
float impulseY = 0f;
|
||||
float impulseX = 0f;
|
||||
|
||||
// flick stick - turn 90 degrees immediately upon turning
|
||||
// should be paired with gyro controls
|
||||
if (gamepad != null && gamepad.config().flickStick) {
|
||||
var turnAngle = 90 / 0.15f; // Entity#turn multiplies cursor delta by 0.15 to get rotation
|
||||
|
||||
AtomicReference<Float> lastAngle = new AtomicReference<>(0f);
|
||||
Vector2fc flickVec = new Vector2f(
|
||||
controller.bindings().LOOK_RIGHT.justPressed() ? 1 : controller.bindings().LOOK_LEFT.justPressed() ? -1 : 0,
|
||||
controller.bindings().LOOK_DOWN.justPressed() ? 1 : controller.bindings().LOOK_UP.justPressed() ? -1 : 0
|
||||
);
|
||||
float flick = controller.bindings().LOOK_DOWN.justPressed() || controller.bindings().LOOK_RIGHT.justPressed() ? 1 : controller.bindings().LOOK_UP.justPressed() || controller.bindings().LOOK_LEFT.justPressed() ? -1 : 0;
|
||||
|
||||
if (!flickVec.equals(0, 0)) {
|
||||
if (flick != 0f) {
|
||||
AtomicReference<Float> lastAngle = new AtomicReference<>(0f);
|
||||
Animator.INSTANCE.play(new Animator.AnimationInstance(10, Easings::easeOutExpo)
|
||||
.addConsumer(angle -> {
|
||||
player.turn((angle - lastAngle.get()) * flickVec.x(), (angle - lastAngle.get()) * flickVec.y());
|
||||
player.turn((angle - lastAngle.get()) * flick, 0);
|
||||
lastAngle.set(angle);
|
||||
}, 0, turnAngle));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
// normal look input
|
||||
var impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state();
|
||||
var impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
|
||||
impulseY = controller.bindings().LOOK_DOWN.state() - controller.bindings().LOOK_UP.state();
|
||||
impulseX = controller.bindings().LOOK_RIGHT.state() - controller.bindings().LOOK_LEFT.state();
|
||||
impulseX *= Math.abs(impulseX);
|
||||
impulseY *= Math.abs(impulseY);
|
||||
|
||||
@ -167,16 +169,32 @@ public class InGameInputHandler {
|
||||
impulseX *= aimMultiplier;
|
||||
impulseY *= aimMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// gyro input
|
||||
if (gamepad != null
|
||||
&& gamepad.hasGyro()
|
||||
&& (!gamepad.config().gyroRequiresButton || gamepad.bindings().GAMEPAD_GYRO_BUTTON.held())
|
||||
) {
|
||||
var gyroDelta = gamepad.absoluteGyroState().deadzone(0.05f);
|
||||
if (gamepad != null && gamepad.hasGyro()) {
|
||||
boolean useGyro = false;
|
||||
|
||||
impulseY += -gyroDelta.pitch() * gamepad.config().gyroLookSensitivity * (gamepad.config().invertGyroY ? -1 : 1);
|
||||
impulseX += (-gyroDelta.roll() + -gyroDelta.yaw()) * gamepad.config().gyroLookSensitivity * (gamepad.config().invertGyroX ? -1 : 1);
|
||||
if (gamepad.config().gyroRequiresButton) {
|
||||
if (gamepad.bindings().GAMEPAD_GYRO_BUTTON.justPressed() || (isAiming && !wasAiming))
|
||||
gyroInput = GamepadState.GyroState.ORIGIN;
|
||||
|
||||
if (gamepad.bindings().GAMEPAD_GYRO_BUTTON.held() || isAiming) {
|
||||
if (gamepad.config().relativeGyroMode)
|
||||
gyroInput = gyroInput.added(gamepad.state().gyroDelta().multiplied(0.1f));
|
||||
else
|
||||
gyroInput = gamepad.state().gyroDelta();
|
||||
useGyro = true;
|
||||
}
|
||||
} else {
|
||||
gyroInput = gamepad.state().gyroDelta();
|
||||
useGyro = true;
|
||||
}
|
||||
|
||||
if (useGyro) {
|
||||
impulseY += -gyroInput.pitch() * gamepad.config().gyroLookSensitivity * (gamepad.config().invertGyroY ? -1 : 1);
|
||||
impulseX += (-gyroInput.roll() + -gyroInput.yaw()) * gamepad.config().gyroLookSensitivity * (gamepad.config().invertGyroX ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
LookInputModifier lookInputModifier = ControlifyEvents.LOOK_INPUT_MODIFIER.invoker();
|
||||
@ -185,6 +203,8 @@ public class InGameInputHandler {
|
||||
|
||||
lookInputX = impulseX * controller.config().horizontalLookSensitivity * 65f;
|
||||
lookInputY = impulseY * controller.config().verticalLookSensitivity * 65f;
|
||||
|
||||
wasAiming = isAiming;
|
||||
}
|
||||
|
||||
public void processPlayerLook(float deltaTime) {
|
||||
@ -197,6 +217,13 @@ public class InGameInputHandler {
|
||||
return this.shouldShowPlayerList;
|
||||
}
|
||||
|
||||
private boolean isAiming(Player player) {
|
||||
return player.isUsingItem() && switch (player.getUseItem().getUseAnimation()) {
|
||||
case BOW, CROSSBOW, SPEAR, SPYGLASS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public record FunctionalLookInputModifier(BiFunction<Float, Controller<?, ?>, Float> x, BiFunction<Float, Controller<?, ?>, Float> y) implements LookInputModifier {
|
||||
@Override
|
||||
public float modifyX(float x, Controller<?, ?> controller) {
|
||||
|
@ -80,6 +80,12 @@
|
||||
"controlify.gui.group.gyro.no_gyro.tooltip": "This controller does not support Gyro. You must have a DualSense™ controller or other compatible controller to use this feature.",
|
||||
"controlify.gui.gyro_look_sensitivity": "Look Sensitivity",
|
||||
"controlify.gui.gyro_look_sensitivity.tooltip": "How much the camera moves based on gyroscope rotation.\nThe pitch (rotating your controller forward/backward) is used for looking up and down, whilst both the roll (rotating your controller left/right) and yaw (rotating your controller clockwise/anticlockwise) are used for looking left and right.",
|
||||
"controlify.gui.gyro_behaviour": "Gyro Behaviour",
|
||||
"controlify.gui.gyro_behaviour.tooltip": "How the gyroscope input should be interpreted as look input.",
|
||||
"controlify.gui.gyro_behaviour.absolute": "Absolute",
|
||||
"controlify.gui.gyro_behaviour.absolute.tooltip": "Absolute: Equivalent to moving the mouse in a direction.",
|
||||
"controlify.gui.gyro_behaviour.relative": "Relative",
|
||||
"controlify.gui.gyro_behaviour.relative.tooltip": "Relative: Equivalent to moving a thumbstick in a direction.",
|
||||
"controlify.gui.gyro_invert_x": "Invert X",
|
||||
"controlify.gui.gyro_invert_x.tooltip": "Invert the left/right rotation of the gyroscope look direction.",
|
||||
"controlify.gui.gyro_invert_y": "Invert Y",
|
||||
@ -156,6 +162,7 @@
|
||||
"controlify.controller_theme.dualshock4": "PS4",
|
||||
"controlify.controller_theme.steam_deck": "Steam Deck",
|
||||
|
||||
"controlify.binding.controlify.gamepad_gyro_button": "Activate Gyroscope",
|
||||
"controlify.binding.controlify.walk_forward": "Walk Forward",
|
||||
"controlify.binding.controlify.walk_backward": "Walk Backward",
|
||||
"controlify.binding.controlify.strafe_left": "Strafe Left",
|
||||
@ -251,8 +258,8 @@
|
||||
"controlify.guide.container.quick_move": "Quick Move",
|
||||
|
||||
"controlify.calibration.title": "Controller Calibration for '%s'",
|
||||
"controlify.calibration.info": "This process will optimize settings for your controller to prevent stick drift. Stick drift happens in your controller thumbsticks and outputs slightly wrong values when you aren't touching them at all. Deadzones are used to prevent this.\nShaking your controller lightly (without touching thumbsticks) can also aid in getting a more precise calibration.\n\nThis will only take a few seconds.",
|
||||
"controlify.calibration.wait": "Please do not touch your controller thumbsticks until the progress bar is complete. Shaking your controller lightly can improve calibration.\nThis process will only take a few seconds.",
|
||||
"controlify.calibration.info": "This calibration optimizes your controller settings to remove stickdrift and create an accurate reading for the gyroscope (if your controller supports it). Before hitting start, make sure to place your controller down on a flat surface and you are not touching the thumbsticks.\n\nThis will only take a few seconds.",
|
||||
"controlify.calibration.wait": "Please do not touch your controller thumbsticks until the progress bar is complete. Make sure to not touch your controller.\nThis process will only take a few seconds.",
|
||||
"controlify.calibration.complete": "Calibration complete! You can now use your controller. Press done to return to the game.",
|
||||
"controlify.calibration.ready": "Ready",
|
||||
"controlify.calibration.done": "Done",
|
||||
|
Reference in New Issue
Block a user