Push Button, Receive Code

Programming tutorials, games, music...free beer.

Working with Joysticks in SFML

Posted by Mark on Monday November 10th, 2014 at 9:01am


In this tutorial, we're going to step through the basics of joystick use in your SFML applications. The idea here, is to get a square on the screen and move it around according to joystick input. Let's begin by putting our necessary boilerplate at the top of the file:

 

#include <iostream>
#include <SFML/Graphics.hpp>

int main(void)
{
	sf::RenderWindow window(sf::VideoMode(800, 600, 32), "Joystick Use", sf::Style::Default);
	sf::Event e;

	sf::RectangleShape square;
	square.setFillColor(sf::Color(255, 0, 0, 255));
	square.setPosition(window.getSize().x / 2, window.getSize().y / 2);
	square.setOutlineColor(sf::Color(0, 0, 0, 255));
	square.setSize(sf::Vector2f(50.f, 50.f));
    ...

So, we've got a window set up, and our square ready to go. Good deal. Next, we need to do get some information about the device we're using. In this example, I chose to use the XBox 360 controller I have lying around. The important thing to note here, is that SFML now provides an easy interface for getting information about the controller, such as the name, the vendor ID, and the product ID:

    ...
    //get information about the joystick
	sf::Joystick::Identification id = sf::Joystick::getIdentification(0);
	std::cout << "\nVendor ID: " << id.vendorId << "\nProduct ID: " << id.productId << std::endl;
	sf::String controller("Joystick Use: " + id.name);
	window.setTitle(controller);//easily tells us what controller is connected
    ...

As you can see, the information required is quite necessary in the event you would like to predict controller setup and get that taken care of in advance. It is pretty annoying for your players to have to configure the controller themselves. Even if you provide an interface to change the button layout, it is still nice to have sensible defaults.

    ...
    //query joystick for settings if it's plugged in...
	if (sf::Joystick::isConnected(0)){
		// check how many buttons joystick number 0 has
		unsigned int buttonCount = sf::Joystick::getButtonCount(0);

		// check if joystick number 0 has a Z axis
		bool hasZ = sf::Joystick::hasAxis(0, sf::Joystick::Z);

		std::cout << "Button count: " << buttonCount << std::endl;
		std::cout << "Has a z-axis: " << hasZ << std::endl;
	}
    ...

It is also nice to know how many buttons the controller has and whether or not there is a z-axis - possibly for panning the camera. What follows, are some additional variables for controlling movement and timekeeping.

    ...
        bool move = false;//indicate whether motion is detected
	int turbo = 1;//indicate whether player is using button for turbo speed ;)

	//for movement
	sf::Vector2f speed = sf::Vector2f(0.f,0.f);

	//time
	sf::Clock tickClock;
	sf::Time timeSinceLastUpdate = sf::Time::Zero;
	sf::Time TimePerFrame = sf::seconds(1.f / 60.f);
	sf::Time duration = sf::Time::Zero;
    ...

It is quite simple to just read the controller for the axis position on the joystick, and multiply by scalar and float values to make adjustments to the player's onscreen motion. Here is the obligatory event loop:

    ...
    bool running = true;
	while (running){
		while (window.pollEvent(e)){
			if (e.type == sf::Event::KeyPressed){
				switch (e.key.code){
				case sf::Keyboard::Escape:
				{
											 window.close();
											 return 0;
				}
					break;
				default:
					break;
				}
			}
			
			if (e.type == sf::Event::JoystickMoved){
				move = true;
				std::cout << "X axis: " << speed.x << std::endl;
				std::cout << "Y axis: " << speed.y << std::endl;
			}
			else{
				move = false;
			}

			if (sf::Joystick::isButtonPressed(0, 0)){//"A" button on the XBox 360 controller
				turbo = 2;
			}

			if (!sf::Joystick::isButtonPressed(0, 0)){
				turbo = 1;
			}

			if(sf::Joystick::isButtonPressed(0,1)){//"B" button on the XBox 360 controller
				window.close();
				return 0;
			}
		}
    ...

In SFML, we can access the controller and it's buttons by index. Looking at the code above, the call to sf::Joystick::isButtonPressed() requires two parameters - the controller index (in this case 0, since I only have one controller plugged in) and the button index. The button index may vary according to controller manufacturer, but with a little guess and check you should be able to figure out the most popular controller button mappings. So, if the analog stick is moved, we set bool moved to true - otherwise it's false. If the user presses, in this case the "A" button (at index 0) the bool turbo is set to true, otherwise it is false. The arbitrary button for exiting the window is the "B" button (at index 1). Finally, we get down to the fun stuff:

    ...
        //check state of joystick
		speed = sf::Vector2f(sf::Joystick::getAxisPosition(0, sf::Joystick::X), sf::Joystick::getAxisPosition(0, sf::Joystick::Y));

		timeSinceLastUpdate += tickClock.restart();
		while (timeSinceLastUpdate > TimePerFrame)
		{
			timeSinceLastUpdate -= TimePerFrame;
			
			//update the position of the square according to input from joystick
			//CHECK DEAD ZONES - OTHERWISE INPUT WILL RESULT IN JITTERY MOVEMENTS WHEN NO INPUT IS PROVIDED
			//INPUT RANGES FROM -100 TO 100
			//A 15% DEAD ZONE SEEMS TO WORK FOR ME - GIVE THAT A SHOT
			if (speed.x > 15.f || speed.x < -15.f || speed.y > 15.f || speed.y < -15.f)
				square.move(turbo*speed.x*TimePerFrame.asSeconds(), turbo*speed.y*TimePerFrame.asSeconds());
		}

		window.clear(sf::Color(255, 0, 255));
		window.draw(square);
		window.display();
	}
	return 0;
}

 

Laid out above, you can see we ascertain the speed vector from the position of the analog stick, which ranges in value from -100 to 100 for both the x and y axes. Perhaps the most useful thing in this block, is that your input will most likely need to be filtered. By this I mean that accepting the raw input from the anolog stick and updating the motion of the square from this will result in jittery motion, and also motion that continues when the controller is at rest. To mitigate this issue, it is a good idea to apply some dead zones to the analog inputs. For instance, in this case, I've just ensured that until the user has pushed on the analog stick about 15% of the way in either direction, the square does not receive any motion updates. Play with this number to get the desired result. With a 15% dead zone, there is still almost no lag in the inputs - it is certainly unnoticeable anyhow.

Well, that about wraps it up. Happy joysticking!