1
0
forked from Clones/Controlify

Improve controller calibration algorithm

This commit is contained in:
isXander
2023-06-12 19:41:49 +01:00
parent adc439128f
commit 4df60549c6
9 changed files with 129 additions and 78 deletions

View File

@ -453,7 +453,7 @@ public class Controlify implements ControlifyApi {
this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player); this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player);
} }
if (!controller.config().calibrated) if (!controller.config().deadzonesCalibrated)
calibrationQueue.add(controller); calibrationQueue.add(controller);
} }

View File

@ -3,9 +3,6 @@ package dev.isxander.controlify.controller;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import dev.isxander.controlify.rumble.RumbleSource; import dev.isxander.controlify.rumble.RumbleSource;
import net.minecraft.resources.ResourceLocation;
import java.util.Map;
public abstract class ControllerConfig { public abstract class ControllerConfig {
public float horizontalLookSensitivity = 1f; public float horizontalLookSensitivity = 1f;
@ -31,7 +28,7 @@ public abstract class ControllerConfig {
public boolean allowVibrations = true; public boolean allowVibrations = true;
public JsonObject vibrationStrengths = RumbleSource.getDefaultJson(); public JsonObject vibrationStrengths = RumbleSource.getDefaultJson();
public boolean calibrated = false; public boolean deadzonesCalibrated = false;
public abstract void setDeadzone(int axis, float deadzone); public abstract void setDeadzone(int axis, float deadzone);
public abstract float getDeadzone(int axis); public abstract float getDeadzone(int axis);

View File

@ -3,10 +3,13 @@ package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.ControllerConfig; import dev.isxander.controlify.controller.ControllerConfig;
public class GamepadConfig extends ControllerConfig { public class GamepadConfig extends ControllerConfig {
public float leftStickDeadzoneX = 0.2f; private float leftStickDeadzone = 0.15f;
public float leftStickDeadzoneY = 0.2f; private float rightStickDeadzone = 0.15f;
public float rightStickDeadzoneX = 0.2f;
public float rightStickDeadzoneY = 0.2f; private transient float leftStickDeadzoneX = leftStickDeadzone;
private transient float leftStickDeadzoneY = leftStickDeadzone;
private transient float rightStickDeadzoneX = rightStickDeadzone;
private transient float rightStickDeadzoneY = rightStickDeadzone;
public float gyroLookSensitivity = 0f; public float gyroLookSensitivity = 0f;
public boolean gyroRequiresButton = true; public boolean gyroRequiresButton = true;
@ -16,6 +19,26 @@ public class GamepadConfig extends ControllerConfig {
public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT; public BuiltinGamepadTheme theme = BuiltinGamepadTheme.DEFAULT;
public float getLeftStickDeadzone() {
return leftStickDeadzone;
}
public float getRightStickDeadzone() {
return rightStickDeadzone;
}
public void setLeftStickDeadzone(float deadzone) {
leftStickDeadzoneX = deadzone;
leftStickDeadzoneY = deadzone;
leftStickDeadzone = deadzone;
}
public void setRightStickDeadzone(float deadzone) {
rightStickDeadzoneX = deadzone;
rightStickDeadzoneY = deadzone;
rightStickDeadzone = deadzone;
}
@Override @Override
public void setDeadzone(int axis, float deadzone) { public void setDeadzone(int axis, float deadzone) {
switch (axis) { switch (axis) {
@ -23,17 +46,20 @@ public class GamepadConfig extends ControllerConfig {
case 1 -> leftStickDeadzoneY = deadzone; case 1 -> leftStickDeadzoneY = deadzone;
case 2 -> rightStickDeadzoneX = deadzone; case 2 -> rightStickDeadzoneX = deadzone;
case 3 -> rightStickDeadzoneY = deadzone; case 3 -> rightStickDeadzoneY = deadzone;
case 4, 5 -> {} // ignore triggers
default -> {} default -> {}
} }
leftStickDeadzone = Math.max(leftStickDeadzoneX, leftStickDeadzoneY);
rightStickDeadzone = Math.max(rightStickDeadzoneX, rightStickDeadzoneY);
} }
@Override @Override
public float getDeadzone(int axis) { public float getDeadzone(int axis) {
return switch (axis) { return switch (axis) {
case 0 -> leftStickDeadzoneX; case 0, 1 -> leftStickDeadzone;
case 1 -> leftStickDeadzoneY; case 2, 3 -> rightStickDeadzone;
case 2 -> rightStickDeadzoneX; case 4, 5 -> 0f; // ignore triggers
case 3 -> rightStickDeadzoneY;
default -> throw new IllegalArgumentException("Unknown axis: " + axis); default -> throw new IllegalArgumentException("Unknown axis: " + axis);
}; };
} }

View File

@ -65,8 +65,8 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState(); BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState();
GamepadState.AxesState deadzoneAxesState = basicState.axes() GamepadState.AxesState deadzoneAxesState = basicState.axes()
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY) .leftJoystickDeadZone(config().getLeftStickDeadzone())
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY); .rightJoystickDeadZone(config().getRightStickDeadzone());
if (DebugProperties.USE_SNAPBACK) { if (DebugProperties.USE_SNAPBACK) {
if (antiSnapbackTicksL > 0) { if (antiSnapbackTicksL > 0) {

View File

@ -145,19 +145,19 @@ public final class GamepadState implements ControllerState {
) { ) {
public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0); public static AxesState EMPTY = new AxesState(0, 0, 0, 0, 0, 0);
public AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) { public AxesState leftJoystickDeadZone(float deadZone) {
return new AxesState( return new AxesState(
ControllerUtils.deadzone(leftStickX, deadZoneX), ControllerUtils.deadzone(leftStickX, deadZone),
ControllerUtils.deadzone(leftStickY, deadZoneY), ControllerUtils.deadzone(leftStickY, deadZone),
rightStickX, rightStickY, leftTrigger, rightTrigger rightStickX, rightStickY, leftTrigger, rightTrigger
); );
} }
public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) { public AxesState rightJoystickDeadZone(float deadZone) {
return new AxesState( return new AxesState(
leftStickX, leftStickY, leftStickX, leftStickY,
ControllerUtils.deadzone(rightStickX, deadZoneX), ControllerUtils.deadzone(rightStickX, deadZone),
ControllerUtils.deadzone(rightStickY, deadZoneY), ControllerUtils.deadzone(rightStickY, deadZone),
leftTrigger, rightTrigger leftTrigger, rightTrigger
); );
} }

View File

@ -177,42 +177,51 @@ public class ControllerConfigScreenFactory {
} }
private static OptionGroup makeDeadzoneGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) { private static OptionGroup makeDeadzoneGroup(Controller<?, ?> controller, ControllerConfig def, ControllerConfig config) {
var deadzoneOpts = new ArrayList<Option<Float>>();
var group = OptionGroup.createBuilder() var group = OptionGroup.createBuilder()
.name(Component.translatable("controlify.config.group.deadzones")); .name(Component.translatable("controlify.config.group.deadzones"));
if (controller instanceof GamepadController gamepad) { if (controller instanceof GamepadController gamepad) {
var gpCfg = gamepad.config(); var gpCfg = gamepad.config();
var gpCfgDef = gamepad.defaultConfig(); var gpCfgDef = gamepad.defaultConfig();
group
.option(Option.<Float>createBuilder() Option<Float> left = Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick"))) .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.left_stick")))
.description(OptionDescription.createBuilder() .description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick"))) .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.left_stick")))
.text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.build())
.binding(
Math.max(gpCfgDef.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
() -> Math.max(gpCfg.leftStickDeadzoneX, gpCfgDef.leftStickDeadzoneY),
v -> gpCfg.leftStickDeadzoneX = gpCfg.leftStickDeadzoneY = v
)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build()) .build())
.option(Option.<Float>createBuilder() .binding(
.name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick"))) gpCfgDef.getLeftStickDeadzone(),
.description(OptionDescription.createBuilder() gpCfg::getLeftStickDeadzone,
.text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick"))) gpCfg::setLeftStickDeadzone
.text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED)) )
.build()) .controller(opt -> FloatSliderControllerBuilder.create(opt)
.binding( .range(0f, 1f).step(0.01f)
Math.max(gpCfgDef.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY), .valueFormatter(percentFormatter))
() -> Math.max(gpCfg.rightStickDeadzoneX, gpCfgDef.rightStickDeadzoneY), .build();
v -> gpCfg.rightStickDeadzoneX = gpCfg.rightStickDeadzoneY = v
) Option<Float> right = Option.<Float>createBuilder()
.controller(opt -> FloatSliderControllerBuilder.create(opt) .name(Component.translatable("controlify.gui.axis_deadzone", Component.translatable("controlify.gui.right_stick")))
.range(0f, 1f).step(0.01f) .description(OptionDescription.createBuilder()
.valueFormatter(percentFormatter)) .text(Component.translatable("controlify.gui.axis_deadzone.tooltip", Component.translatable("controlify.gui.right_stick")))
.build()); .text(Component.translatable("controlify.gui.stickdrift_warning").withStyle(ChatFormatting.RED))
.build())
.binding(
gpCfgDef.getRightStickDeadzone(),
gpCfg::getRightStickDeadzone,
gpCfg::setRightStickDeadzone
)
.controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter))
.build();
group.option(left);
group.option(right);
deadzoneOpts.add(left);
deadzoneOpts.add(right);
} else if (controller instanceof SingleJoystickController joystick) { } else if (controller instanceof SingleJoystickController joystick) {
JoystickMapping.Axis[] axes = joystick.mapping().axes(); JoystickMapping.Axis[] axes = joystick.mapping().axes();
Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length) Collection<Integer> deadzoneAxes = IntStream.range(0, axes.length)
@ -231,7 +240,7 @@ public class ControllerConfigScreenFactory {
for (int i : deadzoneAxes) { for (int i : deadzoneAxes) {
var axis = axes[i]; var axis = axes[i];
group.option(Option.<Float>createBuilder() Option<Float> deadzoneOpt = Option.<Float>createBuilder()
.name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name())) .name(Component.translatable("controlify.gui.joystick_axis_deadzone", axis.name()))
.description(OptionDescription.createBuilder() .description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name())) .text(Component.translatable("controlify.gui.joystick_axis_deadzone.tooltip", axis.name()))
@ -241,7 +250,9 @@ public class ControllerConfigScreenFactory {
.controller(opt -> FloatSliderControllerBuilder.create(opt) .controller(opt -> FloatSliderControllerBuilder.create(opt)
.range(0f, 1f).step(0.01f) .range(0f, 1f).step(0.01f)
.valueFormatter(percentFormatter)) .valueFormatter(percentFormatter))
.build()); .build();
group.option(deadzoneOpt);
deadzoneOpts.add(deadzoneOpt);
} }
} }
@ -261,7 +272,10 @@ public class ControllerConfigScreenFactory {
.description(OptionDescription.createBuilder() .description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.auto_calibration.tooltip")) .text(Component.translatable("controlify.gui.auto_calibration.tooltip"))
.build()) .build())
.action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, screen))) .action((screen, button) -> Minecraft.getInstance().setScreen(new ControllerDeadzoneCalibrationScreen(controller, () -> {
deadzoneOpts.forEach(Option::forgetPendingValue);
return screen;
})))
.build()); .build());
return group.build(); return group.build();

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.gui.screen; package dev.isxander.controlify.gui.screen;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.ControllerManager;
import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.utils.Log;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
@ -10,13 +10,18 @@ import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class ControllerDeadzoneCalibrationScreen extends Screen { public class ControllerDeadzoneCalibrationScreen extends Screen {
private static final int CALIBRATION_TIME = 100;
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png"); private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
protected final Controller<?, ?> controller; protected final Controller<?, ?> controller;
private final Screen parent; private final Supplier<Screen> parent;
private MultiLineLabel waitLabel, infoLabel, completeLabel; private MultiLineLabel waitLabel, infoLabel, completeLabel;
@ -25,7 +30,13 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
protected boolean calibrating = false, calibrated = false; protected boolean calibrating = false, calibrated = false;
protected int calibrationTicks = 0; protected int calibrationTicks = 0;
private final Map<Integer, double[]> calibrationData = new HashMap<>();
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Screen parent) { public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Screen parent) {
this(controller, () -> parent);
}
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
super(Component.translatable("controlify.calibration.title")); super(Component.translatable("controlify.calibration.title"));
this.controller = controller; this.controller = controller;
this.parent = parent; this.parent = parent;
@ -104,42 +115,45 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
if (!calibrating) if (!calibrating)
return; return;
if (stateChanged()) if (stateChanged()) {
calibrationTicks = 0; calibrationTicks = 0;
calibrationData.clear();
}
if (calibrationTicks < CALIBRATION_TIME) {
calibrate(calibrationTicks);
if (calibrationTicks < 100) {
calibrationTicks++; calibrationTicks++;
} else { } else {
useCurrentStateAsDeadzone(); applyDeadzones();
calibrating = false; calibrating = false;
calibrated = true; calibrated = true;
readyButton.active = true; readyButton.active = true;
readyButton.setMessage(Component.translatable("controlify.calibration.done")); readyButton.setMessage(Component.translatable("controlify.calibration.done"));
controller.config().calibrated = true; controller.config().deadzonesCalibrated = true;
Controlify.instance().config().save(); Controlify.instance().config().save();
} }
} }
private void useCurrentStateAsDeadzone() { private void calibrate(int tick) {
var axes = controller.state().axes(); var axes = controller.state().rawAxes();
for (int i = 0; i < axes.size(); i++) { for (int i = 0; i < axes.size(); i++) {
var axis = axes.get(i); var axis = Math.abs(axes.get(i));
var minDeadzone = axis + 0.08f; calibrationData.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
var deadzone = (float)Mth.clamp(0.05 * Math.ceil(minDeadzone / 0.05), 0, 0.95);
if (Float.isNaN(deadzone)) {
Log.LOGGER.warn("Deadzone for axis {} is NaN, using default deadzone.", i);
deadzone = controller.defaultConfig().getDeadzone(i);
}
controller.config().setDeadzone(i, deadzone);
} }
} }
private void applyDeadzones() {
calibrationData.forEach((i, data) -> {
var max = Arrays.stream(data).max().orElseThrow();
controller.config().setDeadzone(i, (float) max + 0.05f);
});
}
private boolean stateChanged() { private boolean stateChanged() {
var amt = 0.0001f; var amt = 0.4f;
return controller.state().axes().stream() return controller.state().axes().stream()
.anyMatch(axis -> Math.abs(axis - controller.prevState().axes().get(controller.state().axes().indexOf(axis))) > amt); .anyMatch(axis -> Math.abs(axis - controller.prevState().axes().get(controller.state().axes().indexOf(axis))) > amt);
@ -147,7 +161,7 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
@Override @Override
public void onClose() { public void onClose() {
minecraft.setScreen(parent); minecraft.setScreen(parent.get());
} }
@Override @Override

View File

@ -251,8 +251,8 @@
"controlify.guide.container.quick_move": "Quick Move", "controlify.guide.container.quick_move": "Quick Move",
"controlify.calibration.title": "Controller Calibration for '%s'", "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.\n\nThis will only take a few seconds.", "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. This process 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.complete": "Calibration complete! You can now use your controller. Press done to return to the game.", "controlify.calibration.complete": "Calibration complete! You can now use your controller. Press done to return to the game.",
"controlify.calibration.ready": "Ready", "controlify.calibration.ready": "Ready",
"controlify.calibration.done": "Done", "controlify.calibration.done": "Done",

View File

@ -50,7 +50,7 @@ public class FakeController implements JoystickController<JoystickConfig> {
return false; return false;
} }
}); });
this.config.calibrated = true; this.config.deadzonesCalibrated = true;
} }
@Override @Override