CrocoDocs
CrocoDocs

Introduction

What is CrocoDocs?Season Breakdown

Getting Started

Programming in FTCJavaBlocksAndroid Studio

Control Systems

IntroductionJoystick MappingPID ControlMotion ProfilingKalman FilterLow-Pass Filter

Autonomous

IntroductionTime vs Encoder-Based MovementOdometryMotion PlanningPure PursuitSensor Fusion

Codebase Etiquette and Good Practices

IntroductionNaming ConventionsCode OrganizationComments and DocumentationTeam Collaboration

Libraries

LibrariesNextFTCPedro PathingFTC DashboardMercurialPanelsSloth

Sensors and Vision

Vision OverviewVision Basics

Joystick Mapping

Connect your gamepad to robot movements

What is Joystick Mapping?

Joystick mapping is the process of taking inputs from your gamepad (joysticks, buttons, triggers) and translating them into robot actions. It's how you make your robot respond when a driver moves a stick or presses a button.

Think of it like configuring controls in a video game — you decide what each button does!

Understanding Gamepad Inputs

FTC uses standard game controllers (like Xbox or PlayStation controllers). Each controller has several input types:

Xbox Controller Layout

Analog Inputs (Joysticks & Triggers)

  • Values range from -1.0 to 1.0 (or 0.0 to 1.0 for triggers)
  • Smooth, gradual control
  • Perfect for driving and precise movements

Digital Inputs (Buttons & D-pad)

  • Values are true or false
  • Instant on/off actions
  • Great for toggles and single actions

Basic Drive Control

The most common joystick mapping is for your drive base. Here are the main styles:

Tank Drive

Each side of the robot is controlled by a separate joystick — just like driving a tank! Most FTC robots use 4 or 6 wheels for better traction and stability.

Tank Drive Layout

@TeleOp(name="Tank Drive")
public class TankDrive extends LinearOpMode {
    private DcMotor frontLeft;
    private DcMotor frontRight;
    private DcMotor backLeft;
    private DcMotor backRight;
    
    @Override
    public void runOpMode() {
        // Initialize all 4 motors
        frontLeft = hardwareMap.get(DcMotor.class, "front_left");
        frontRight = hardwareMap.get(DcMotor.class, "front_right");
        backLeft = hardwareMap.get(DcMotor.class, "back_left");
        backRight = hardwareMap.get(DcMotor.class, "back_right");
        
        // Reverse right side motors so they drive in the same direction
        frontRight.setDirection(DcMotor.Direction.REVERSE);
        backRight.setDirection(DcMotor.Direction.REVERSE);
        
        waitForStart();
        
        while (opModeIsActive()) {
            // Left stick controls left side
            double leftPower = -gamepad1.left_stick_y;
            
            // Right stick controls right side
            double rightPower = -gamepad1.right_stick_y;
            
            // Apply same power to motors on each side
            frontLeft.setPower(leftPower);
            backLeft.setPower(leftPower);
            frontRight.setPower(rightPower);
            backRight.setPower(rightPower);
            
            telemetry.addData("Left", leftPower);
            telemetry.addData("Right", rightPower);
            telemetry.update();
        }
    }
}
@TeleOp(name = "Tank Drive")
class TankDrive : LinearOpMode() {
    private lateinit var frontLeft: DcMotor
    private lateinit var frontRight: DcMotor
    private lateinit var backLeft: DcMotor
    private lateinit var backRight: DcMotor
    
    override fun runOpMode() {
        // Initialize all 4 motors
        frontLeft = hardwareMap.get(DcMotor::class.java, "front_left")
        frontRight = hardwareMap.get(DcMotor::class.java, "front_right")
        backLeft = hardwareMap.get(DcMotor::class.java, "back_left")
        backRight = hardwareMap.get(DcMotor::class.java, "back_right")
        
        // Reverse right side motors so they drive in the same direction
        frontRight.direction = DcMotor.Direction.REVERSE
        backRight.direction = DcMotor.Direction.REVERSE
        
        waitForStart()
        
        while (opModeIsActive()) {
            // Left stick controls left side
            val leftPower = -gamepad1.left_stick_y.toDouble()
            
            // Right stick controls right side
            val rightPower = -gamepad1.right_stick_y.toDouble()
            
            // Apply same power to motors on each side
            frontLeft.power = leftPower
            backLeft.power = leftPower
            frontRight.power = rightPower
            backRight.power = rightPower
            
            telemetry.addData("Left", leftPower)
            telemetry.addData("Right", rightPower)
            telemetry.update()
        }
    }
}

Why the negative sign? Joysticks return negative values when pushed up, but we usually want forward to be positive power. The negative sign flips this to feel more natural.

X-Drive (Holonomic)

X-Drive is a holonomic drivetrain that uses 4 motors mounted at 45-degree angles. This allows your robot to move in any direction and rotate independently — all at the same time! It's one of the simplest holonomic designs to understand and program.

What makes it holonomic? Your robot can strafe (move sideways) without turning. Traditional drives must turn to face the direction they want to go, but holonomic drives move freely in any direction.

X-Drive Motor Layout

Motor Layout: Motors are positioned at the 4 corners, rotated 45° so they point toward the corners of the robot. Typically using omni wheels or mecanum wheels.

@TeleOp(name="X-Drive")
public class XDrive extends LinearOpMode {
    private DcMotor frontLeft;
    private DcMotor frontRight;
    private DcMotor backLeft;
    private DcMotor backRight;
    
    @Override
    public void runOpMode() {
        // Initialize motors
        frontLeft = hardwareMap.get(DcMotor.class, "front_left");
        frontRight = hardwareMap.get(DcMotor.class, "front_right");
        backLeft = hardwareMap.get(DcMotor.class, "back_left");
        backRight = hardwareMap.get(DcMotor.class, "back_right");
        
        // Reverse right side motors
        frontRight.setDirection(DcMotor.Direction.REVERSE);
        backRight.setDirection(DcMotor.Direction.REVERSE);
        
        waitForStart();
        
        while (opModeIsActive()) {
            // Get joystick inputs
            double drive = -gamepad1.left_stick_y;   // Forward/backward
            double strafe = gamepad1.left_stick_x;   // Left/right
            double turn = gamepad1.right_stick_x;    // Rotation
            
            // Calculate power for each motor
            // The math works because of the 45° motor angles
            double frontLeftPower = drive + strafe + turn;
            double frontRightPower = drive - strafe - turn;
            double backLeftPower = drive - strafe + turn;
            double backRightPower = drive + strafe - turn;
            
            // Find the maximum power value
            double maxPower = Math.max(Math.abs(frontLeftPower),
                              Math.max(Math.abs(frontRightPower),
                              Math.max(Math.abs(backLeftPower),
                                       Math.abs(backRightPower))));
            
            // Normalize powers if any exceeds 1.0
            if (maxPower > 1.0) {
                frontLeftPower /= maxPower;
                frontRightPower /= maxPower;
                backLeftPower /= maxPower;
                backRightPower /= maxPower;
            }
            
            // Apply power to motors
            frontLeft.setPower(frontLeftPower);
            frontRight.setPower(frontRightPower);
            backLeft.setPower(backLeftPower);
            backRight.setPower(backRightPower);
            
            // Telemetry
            telemetry.addData("Drive", "%.2f", drive);
            telemetry.addData("Strafe", "%.2f", strafe);
            telemetry.addData("Turn", "%.2f", turn);
            telemetry.update();
        }
    }
}
@TeleOp(name = "X-Drive")
class XDrive : LinearOpMode() {
    private lateinit var frontLeft: DcMotor
    private lateinit var frontRight: DcMotor
    private lateinit var backLeft: DcMotor
    private lateinit var backRight: DcMotor
    
    override fun runOpMode() {
        // Initialize motors
        frontLeft = hardwareMap.get(DcMotor::class.java, "front_left")
        frontRight = hardwareMap.get(DcMotor::class.java, "front_right")
        backLeft = hardwareMap.get(DcMotor::class.java, "back_left")
        backRight = hardwareMap.get(DcMotor::class.java, "back_right")
        
        // Reverse right side motors
        frontRight.direction = DcMotor.Direction.REVERSE
        backRight.direction = DcMotor.Direction.REVERSE
        
        waitForStart()
        
        while (opModeIsActive()) {
            // Get joystick inputs
            val drive = -gamepad1.left_stick_y.toDouble()   // Forward/backward
            val strafe = gamepad1.left_stick_x.toDouble()   // Left/right
            val turn = gamepad1.right_stick_x.toDouble()    // Rotation
            
            // Calculate power for each motor
            // The math works because of the 45° motor angles
            var frontLeftPower = drive + strafe + turn
            var frontRightPower = drive - strafe - turn
            var backLeftPower = drive - strafe + turn
            var backRightPower = drive + strafe - turn
            
            // Find the maximum power value
            val maxPower = maxOf(
                Math.abs(frontLeftPower),
                Math.abs(frontRightPower),
                Math.abs(backLeftPower),
                Math.abs(backRightPower)
            )
            
            // Normalize powers if any exceeds 1.0
            if (maxPower > 1.0) {
                frontLeftPower /= maxPower
                frontRightPower /= maxPower
                backLeftPower /= maxPower
                backRightPower /= maxPower
            }
            
            // Apply power to motors
            frontLeft.power = frontLeftPower
            frontRight.power = frontRightPower
            backLeft.power = backLeftPower
            backRight.power = backRightPower
            
            // Telemetry
            telemetry.addData("Drive", "%.2f".format(drive))
            telemetry.addData("Strafe", "%.2f".format(strafe))
            telemetry.addData("Turn", "%.2f".format(turn))
            telemetry.update()
        }
    }
}

Power Normalization: When combining drive, strafe, and turn, the total power can exceed 1.0. We normalize by dividing all powers by the maximum to keep values in the valid range while maintaining the correct ratios.

Understanding the X-Drive Math

The formulas might look confusing, but here's what's happening:

  • Forward/Backward: All motors spin the same direction (drive)
  • Strafe Left/Right: Front-left and back-right spin opposite to front-right and back-left (strafe)
  • Rotate: Left motors spin opposite to right motors (turn)

Each motor's power is a combination of these three movements. The 45° angle of the motors naturally creates the strafing motion!

Other Holonomic Options

Mecanum Drive:

  • Uses special mecanum wheels with angled rollers
  • Similar control to X-Drive
  • More complex math with vector decomposition
  • Popular but requires precise wheel alignment

H-Drive:

  • Three or four regular omni wheels plus a center strafe wheel
  • Simpler mechanically than X-Drive
  • The center wheel handles strafing
  • Good for beginners but less efficient

Why X-Drive is good for learning:

  • No complex vector math needed
  • Intuitive motor power calculations
  • Works with standard omni wheels
  • Easy to understand how each movement affects the motors

Want to learn more about drivetrain designs? Check out Game Manual 0's comprehensive guide to drivetrains for in-depth comparisons, build tips, and advanced options.

Button Mapping for Mechanisms

Buttons are perfect for controlling other parts of your robot like arms, claws, and intakes.

Simple Button Control

// In your main loop
while (opModeIsActive()) {
    // Claw control - press A to close, B to open
    if (gamepad1.a) {
        clawServo.setPosition(0.0); // Closed
    } else if (gamepad1.b) {
        clawServo.setPosition(1.0); // Open
    }
    
    // Intake control - hold X to run intake
    if (gamepad1.x) {
        intakeMotor.setPower(1.0);
    } else {
        intakeMotor.setPower(0.0);
    }
    
    // Arm control with left trigger and right trigger
    double armPower = gamepad1.right_trigger - gamepad1.left_trigger;
    armMotor.setPower(armPower);
}
// In your main loop
while (opModeIsActive()) {
    // Claw control - press A to close, B to open
    if (gamepad1.a) {
        clawServo.position = 0.0 // Closed
    } else if (gamepad1.b) {
        clawServo.position = 1.0 // Open
    }
    
    // Intake control - hold X to run intake
    if (gamepad1.x) {
        intakeMotor.power = 1.0
    } else {
        intakeMotor.power = 0.0
    }
    
    // Arm control with left trigger and right trigger
    val armPower = gamepad1.right_trigger.toDouble() - gamepad1.left_trigger.toDouble()
    armMotor.power = armPower
}

Toggle Button (Advanced)

Sometimes you want a button to switch between two states (like an on/off switch). This requires tracking the previous button state:

private boolean intakeOn = false;
private boolean previousY = false;

// In your main loop
while (opModeIsActive()) {
    // Detect button press (not hold)
    boolean currentY = gamepad1.y;
    if (currentY && !previousY) {
        // Button was just pressed - toggle
        intakeOn = !intakeOn;
    }
    previousY = currentY;
    
    // Apply the state
    if (intakeOn) {
        intakeMotor.setPower(1.0);
    } else {
        intakeMotor.setPower(0.0);
    }
    
    telemetry.addData("Intake", intakeOn ? "ON" : "OFF");
    telemetry.update();
}
private var intakeOn = false
private var previousY = false

// In your main loop
while (opModeIsActive()) {
    // Detect button press (not hold)
    val currentY = gamepad1.y
    if (currentY && !previousY) {
        // Button was just pressed - toggle
        intakeOn = !intakeOn
    }
    previousY = currentY
    
    // Apply the state
    if (intakeOn) {
        intakeMotor.power = 1.0
    } else {
        intakeMotor.power = 0.0
    }
    
    telemetry.addData("Intake", if (intakeOn) "ON" else "OFF")
    telemetry.update()
}

Improving Control Feel

Deadzone

Joysticks often don't return exactly 0.0 when centered. Add a deadzone to ignore tiny movements:

public double applyDeadzone(double value, double deadzone) {
    if (Math.abs(value) < deadzone) {
        return 0.0;
    }
    return value;
}

// Use it like this:
double drive = applyDeadzone(-gamepad1.left_stick_y, 0.1);
double turn = applyDeadzone(gamepad1.right_stick_x, 0.1);
fun applyDeadzone(value: Double, deadzone: Double): Double {
    if (Math.abs(value) < deadzone) {
        return 0.0
    }
    return value
}

// Use it like this:
val drive = applyDeadzone(-gamepad1.left_stick_y.toDouble(), 0.1)
val turn = applyDeadzone(gamepad1.right_stick_x.toDouble(), 0.1)

Speed Limiting

Make precise control easier by reducing maximum speed:

// Slow mode when holding left bumper
double speedMultiplier = gamepad1.left_bumper ? 0.4 : 1.0;

double leftPower = drive + turn;
double rightPower = drive - turn;

leftMotor.setPower(leftPower * speedMultiplier);
rightMotor.setPower(rightPower * speedMultiplier);
// Slow mode when holding left bumper
val speedMultiplier = if (gamepad1.left_bumper) 0.4 else 1.0

val leftPower = drive + turn
val rightPower = drive - turn

leftMotor.power = leftPower * speedMultiplier
rightMotor.power = rightPower * speedMultiplier

Common Mapping Patterns

Here's a typical control scheme many teams use:

Two Controller Setup

Gamepad 1 (Driver):

  • Left stick Y → Forward/backward
  • Right stick X → Turning
  • Left bumper → Slow mode
  • Right bumper → Turbo mode

Gamepad 2 (Operator):

  • Left stick Y → Arm up/down
  • Right stick Y → Lift up/down
  • A button → Close claw
  • B button → Open claw
  • X button → Intake in
  • Y button → Intake out
  • D-pad → Preset positions

Tips for Good Joystick Mapping

  1. Keep it intuitive — Forward on the stick should feel like forward on the robot
  2. Use both controllers — Driver focuses on moving, operator handles mechanisms
  3. Test with your drivers — What makes sense to you might not work for them
  4. Add telemetry — Show what each input is doing so you can debug easily
  5. Document your controls — Write down what each button does for your drivers!

Next Steps

Once you have basic joystick mapping working, you can move on to more advanced topics like:

  • Adding ramping to prevent jerky movements
  • Implementing field-centric control for mecanum drives
  • Creating preset positions with PID control

Introduction

Master the fundamentals of robot control

PID Control

Self-correcting robot control made simple

On this page

What is Joystick Mapping?Understanding Gamepad InputsAnalog Inputs (Joysticks & Triggers)Digital Inputs (Buttons & D-pad)Basic Drive ControlTank DriveX-Drive (Holonomic)Understanding the X-Drive MathOther Holonomic OptionsButton Mapping for MechanismsSimple Button ControlToggle Button (Advanced)Improving Control FeelDeadzoneSpeed LimitingCommon Mapping PatternsTips for Good Joystick MappingNext Steps