From 3f820e1c019503a64a91c4f08fe13ccc5ade7eac Mon Sep 17 00:00:00 2001 From: isXander Date: Mon, 12 Jun 2023 22:03:01 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9E=95=20Another=20dramatic=20improvement=20?= =?UTF-8?q?to=20gyro=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/isxander/controlify/Controlify.java | 4 +- .../controller/gamepad/GamepadConfig.java | 2 + .../controller/gamepad/GamepadController.java | 8 +- .../controller/gamepad/GamepadState.java | 15 ++- ....java => ControllerCalibrationScreen.java} | 37 ++++++-- .../screen/ControllerConfigScreenFactory.java | 21 ++++- .../controlify/ingame/InGameInputHandler.java | 93 ++++++++++++------- .../assets/controlify/lang/en_us.json | 11 ++- 8 files changed, 138 insertions(+), 53 deletions(-) rename src/main/java/dev/isxander/controlify/gui/screen/{ControllerDeadzoneCalibrationScreen.java => ControllerCalibrationScreen.java} (79%) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 5417e99..8c6a605 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -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); } diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java index 9ee2dfe..a4d949a 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadConfig.java @@ -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; diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java index 48896ba..d0de281 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -22,7 +22,7 @@ public class GamepadController extends AbstractController uniqueDrivers; private int antiSnapbackTicksL, antiSnapbackTicksR; @@ -85,9 +85,9 @@ public class GamepadController extends AbstractController calibrationData = new HashMap<>(); + private final Map 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 parent) { + public ControllerCalibrationScreen(Controller controller, Supplier 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; diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index f24e447..6f98e83 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -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.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.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; diff --git a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java index bc0f8fb..b823a8b 100644 --- a/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java +++ b/src/main/java/dev/isxander/controlify/ingame/InGameInputHandler.java @@ -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,53 +133,68 @@ 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 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 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)); } + } else { + // normal look input + 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); - return; - } - - // 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(); - impulseX *= Math.abs(impulseX); - impulseY *= Math.abs(impulseY); - - if (controller.config().reduceAimingSensitivity && player != null && player.isUsingItem()) { - float aimMultiplier = switch (player.getUseItem().getUseAnimation()) { - case BOW, CROSSBOW, SPEAR -> 0.6f; - case SPYGLASS -> 0.2f; - default -> 1f; - }; - impulseX *= aimMultiplier; - impulseY *= aimMultiplier; + if (controller.config().reduceAimingSensitivity && player != null && player.isUsingItem()) { + float aimMultiplier = switch (player.getUseItem().getUseAnimation()) { + case BOW, CROSSBOW, SPEAR -> 0.6f; + case SPYGLASS -> 0.2f; + default -> 1f; + }; + 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> x, BiFunction, Float> y) implements LookInputModifier { @Override public float modifyX(float x, Controller controller) { diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 7096d41..b116948 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -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",