forked from Clones/Controlify
➕ Improve controller calibration algorithm
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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()
|
||||
.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
|
||||
)
|
||||
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||
.range(0f, 1f).step(0.01f)
|
||||
.valueFormatter(percentFormatter))
|
||||
|
||||
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())
|
||||
.option(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
|
||||
)
|
||||
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||
.range(0f, 1f).step(0.01f)
|
||||
.valueFormatter(percentFormatter))
|
||||
.build());
|
||||
.binding(
|
||||
gpCfgDef.getLeftStickDeadzone(),
|
||||
gpCfg::getLeftStickDeadzone,
|
||||
gpCfg::setLeftStickDeadzone
|
||||
)
|
||||
.controller(opt -> FloatSliderControllerBuilder.create(opt)
|
||||
.range(0f, 1f).step(0.01f)
|
||||
.valueFormatter(percentFormatter))
|
||||
.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(
|
||||
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) {
|
||||
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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
controller.config().setDeadzone(i, deadzone);
|
||||
var axis = Math.abs(axes.get(i));
|
||||
calibrationData.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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",
|
||||
|
@ -50,7 +50,7 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
this.config.calibrated = true;
|
||||
this.config.deadzonesCalibrated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Reference in New Issue
Block a user