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);
}
if (!controller.config().calibrated)
if (!controller.config().deadzonesCalibrated)
calibrationQueue.add(controller);
}

View File

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

View File

@ -3,10 +3,13 @@ package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.controller.ControllerConfig;
public class GamepadConfig extends ControllerConfig {
public float leftStickDeadzoneX = 0.2f;
public float leftStickDeadzoneY = 0.2f;
public float rightStickDeadzoneX = 0.2f;
public float rightStickDeadzoneY = 0.2f;
private float leftStickDeadzone = 0.15f;
private float rightStickDeadzone = 0.15f;
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 boolean gyroRequiresButton = true;
@ -16,6 +19,26 @@ public class GamepadConfig extends ControllerConfig {
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
public void setDeadzone(int axis, float deadzone) {
switch (axis) {
@ -23,17 +46,20 @@ public class GamepadConfig extends ControllerConfig {
case 1 -> leftStickDeadzoneY = deadzone;
case 2 -> rightStickDeadzoneX = deadzone;
case 3 -> rightStickDeadzoneY = deadzone;
case 4, 5 -> {} // ignore triggers
default -> {}
}
leftStickDeadzone = Math.max(leftStickDeadzoneX, leftStickDeadzoneY);
rightStickDeadzone = Math.max(rightStickDeadzoneX, rightStickDeadzoneY);
}
@Override
public float getDeadzone(int axis) {
return switch (axis) {
case 0 -> leftStickDeadzoneX;
case 1 -> leftStickDeadzoneY;
case 2 -> rightStickDeadzoneX;
case 3 -> rightStickDeadzoneY;
case 0, 1 -> leftStickDeadzone;
case 2, 3 -> rightStickDeadzone;
case 4, 5 -> 0f; // ignore triggers
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();
GamepadState.AxesState deadzoneAxesState = basicState.axes()
.leftJoystickDeadZone(config().leftStickDeadzoneX, config().leftStickDeadzoneY)
.rightJoystickDeadZone(config().rightStickDeadzoneX, config().rightStickDeadzoneY);
.leftJoystickDeadZone(config().getLeftStickDeadzone())
.rightJoystickDeadZone(config().getRightStickDeadzone());
if (DebugProperties.USE_SNAPBACK) {
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 AxesState leftJoystickDeadZone(float deadZoneX, float deadZoneY) {
public AxesState leftJoystickDeadZone(float deadZone) {
return new AxesState(
ControllerUtils.deadzone(leftStickX, deadZoneX),
ControllerUtils.deadzone(leftStickY, deadZoneY),
ControllerUtils.deadzone(leftStickX, deadZone),
ControllerUtils.deadzone(leftStickY, deadZone),
rightStickX, rightStickY, leftTrigger, rightTrigger
);
}
public AxesState rightJoystickDeadZone(float deadZoneX, float deadZoneY) {
public AxesState rightJoystickDeadZone(float deadZone) {
return new AxesState(
leftStickX, leftStickY,
ControllerUtils.deadzone(rightStickX, deadZoneX),
ControllerUtils.deadzone(rightStickY, deadZoneY),
ControllerUtils.deadzone(rightStickX, deadZone),
ControllerUtils.deadzone(rightStickY, deadZone),
leftTrigger, rightTrigger
);
}

View File

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

View File

@ -1,8 +1,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.utils.Log;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
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.network.chat.Component;
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 {
private static final int CALIBRATION_TIME = 100;
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
protected final Controller<?, ?> controller;
private final Screen parent;
private final Supplier<Screen> parent;
private MultiLineLabel waitLabel, infoLabel, completeLabel;
@ -25,7 +30,13 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
protected boolean calibrating = false, calibrated = false;
protected int calibrationTicks = 0;
private final Map<Integer, double[]> calibrationData = new HashMap<>();
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Screen parent) {
this(controller, () -> parent);
}
public ControllerDeadzoneCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
super(Component.translatable("controlify.calibration.title"));
this.controller = controller;
this.parent = parent;
@ -104,42 +115,45 @@ public class ControllerDeadzoneCalibrationScreen extends Screen {
if (!calibrating)
return;
if (stateChanged())
if (stateChanged()) {
calibrationTicks = 0;
calibrationData.clear();
}
if (calibrationTicks < CALIBRATION_TIME) {
calibrate(calibrationTicks);
if (calibrationTicks < 100) {
calibrationTicks++;
} else {
useCurrentStateAsDeadzone();
applyDeadzones();
calibrating = false;
calibrated = true;
readyButton.active = true;
readyButton.setMessage(Component.translatable("controlify.calibration.done"));
controller.config().calibrated = true;
controller.config().deadzonesCalibrated = true;
Controlify.instance().config().save();
}
}
private void useCurrentStateAsDeadzone() {
var axes = controller.state().axes();
private void calibrate(int tick) {
var axes = controller.state().rawAxes();
for (int i = 0; i < axes.size(); i++) {
var axis = axes.get(i);
var minDeadzone = axis + 0.08f;
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);
var axis = Math.abs(axes.get(i));
calibrationData.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
}
}
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() {
var amt = 0.0001f;
var amt = 0.4f;
return controller.state().axes().stream()
.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
public void onClose() {
minecraft.setScreen(parent);
minecraft.setScreen(parent.get());
}
@Override

View File

@ -251,8 +251,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.\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.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.complete": "Calibration complete! You can now use your controller. Press done to return to the game.",
"controlify.calibration.ready": "Ready",
"controlify.calibration.done": "Done",

View File

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