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()
.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();

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