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
|
||||
|
Reference in New Issue
Block a user