1
0
forked from Clones/Controlify

Another dramatic improvement to gyro control

This commit is contained in:
isXander
2023-06-12 22:03:01 +01:00
parent 4df60549c6
commit 3f820e1c01
8 changed files with 138 additions and 53 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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),

View File

@ -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;

View File

@ -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;

View File

@ -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<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));
}
} 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, Controller<?, ?>, Float> x, BiFunction<Float, Controller<?, ?>, Float> y) implements LookInputModifier {
@Override
public float modifyX(float x, Controller<?, ?> controller) {