Full Controller Support

2024/09/17

Categories: Dev Log Tags: godot Pilot Light game dev programming

It was so easy at first

This is the tale of how I struggled to get controllers working in Pilot Light, and a solution to anyone struggling with the same problem that I had. In my very first dev log on YouTube, I made a passing remark about how easy it was to add support for game controllers, saying that it’s unreasonable in current year that a game would release with no controller support whatsoever. I still think that’s true, I think it’s a travesty to boot up a game and have controllers not work at all. I still resent Mojang for not implementing controller support in Minecraft Java Edition years ago.

When I made that comment, however, I only was testing Pilot Light (it didn’t actually have a name, and it was still 2.5d at the time) with an Xbox Series controller. That is my go-to controller for playing games on PC. If a game has any controller suppport, that one is almost guaranteed to work out of the box. Without putting much thought into it, I just bound the the controller in the Godot editor. That was easy, it took less than a minute, and I didn’t have to write any extra code. I put a pin in it and went on to develop the other major parts of the game.

Now that I’m getting close to releasing a demo for Steam Next Fest, I figured I wanted to get support for other controllers. I don’t know which controllers are the most popular. Interestingly I think I’ll only find out once I release the demo and get to look at the the hardware stats. So I took a guess that they’d be Xbox Series, Playstation 4 and 5, Switch Pro, and of course, the Steam Deck. In fact, I am hoping that Pilot Light is Deck-verified by the time it releases.

The first time I hit a wall since I started

I haven’t had much trouble learning Godot since I started. Plenty of indie developers use it, so there are plenty of people on forums and Reddit to consult when I have a problem. Most problems that I’ve had were had by someone else, who then posted about it on a forum or Reddit, and someone gave them a clear answer. Otherwise, Godot’s official documentation is very comprehensive and easy to access. You could either search for certain features and find the documentation for it on the website, or hold Ctrl and click on the name of a class or variable in the editor itself, and get that same documentation without ever leaving the editor. It’s fantastic, and has made learning incredibly straightforward.

That’s all well and good, but someone will always have a problem for the first time, and I was that someone. My problem was simple: the game needs to detect the controller the player is using, then display the correct glyphs and use the correct bindings.

In a lot of other games with controller support, especially AAA games, there’s a neat behavior you can try. If there are button glyphs on screen, you can wiggle your mouse, and they’ll suddenly snap to mouse and keyboard glyphs. When you use a controller, they’ll snap back to the buttons on the controller. When you plug in a different controller, the glyphs will swap to that controller’s style. That’s exactly the behavior I wanted to replicate in Pilot Light.

The button glyphs were actually simpler than I expected. Godot returns a string when you check what controllers are plugged in, so I simply checked that string’s contents for keywords like “Xbox” or “Nintendo”. That worked fine. Getting data from strings can suck sometimes, but that was pretty painless.

func _input(event):
	if event is InputEventKey:
		GUIMaster.input_style = GUIMaster.InputStyle.KEYBOARD
	elif event is InputEventJoypadButton:
		var joypad_name: String = Input.get_joy_name(0)
		if joypad_name.contains("Xbox"):
			GUIMaster.input_style = GUIMaster.InputStyle.XBOX
		elif joypad_name.contains("Nintendo"):
			GUIMaster.input_style = GUIMaster.InputStyle.NINTENDO
		elif joypad_name.contains("Sony"):
			GUIMaster.input_style = GUIMaster.InputStyle.SONY
		else:
			GUIMaster.input_style = GUIMaster.InputStyle.STEAMDECK

The only glyph I needed in the game at the time was the prompt to launch flares for the first time. On Xbox Controllers, that’s the X button, which is on the far left. I wanted that button to also be X on the Switch Pro Controller, so I used the X button glyph, and it worked fine. But the action wasn’t bound to X on the Switch controller, it was bound to Y.

Nobody told me this, but the X button on Xbox controllers is in the same position as the Y button on Nintendo controllers, and you’re not binding the buttons by name, you’re binding them by position. The A and B buttons are also swapped between the two controllers, meaning the boost action was bound to B (where is should have been A), self-destruct was bound to X (where it should have been Y), and even selecting buttons in menus was mapped to B (which you’d expect to mean “back”).

Plan A - Let Steam do the heavy lifting

I naively thought Steam Input would be the solution to my problem, but that just introduced yet another variable that wasn’t easy to test. I got it working for Xbox controllers (of course), but it somehow broke menus and displayed the wrong glyph for the flare prompt when using any other controller. When the engine detects a controller but it doesn’t fit any of the keywords, I have it set to fall back to the Xbox glyphs, so I think the engine was detecting Steam Input as a controller.

I scrapped that idea entirely. Unlike the Godot docs, the docs for the Steam API plugin for Godot 4 sucks. That also goes for most of Valve’s official docs for the backend of Steam. What makes it worse is that all of that is behind a non-disclosure agreement, so there aren’t nearly as many forum or Reddit posts that can help.

Plan B - Cave and download an addon

I tried my best not to use any code or addons that weren’t mine for Pilot Light, but at a certain point after failing to use Steam Input, I caved, and looked into an addon called Input Helper. When I installed it, I just assumed that it would be the answer to my problem (again). I thought I had finally come across someone who had the same problem as me, but I thought wrong.

It turns out that this addon does two things, and that’s detect what controller is being used, which I accomplished on my own, as I said earlier, and remap buttons, which is not at all complicated to do in the engine. Realizing that, I finally had an idea.

Plan C - The correct solution is usually the easiest one

My uncle worked at Motorola, and he told me a story that for certain problems, they would form two teams. Team 1 would methodically find a way to solve the problem, with well-documented repeatable steps that would work 100% of the time, while Team 2 would just start solving the problem as quickly as possible, without worrying about repeating the solution. When the problem only needed to be solved once, Team 2 would usually be able to move on with their lives in an hour. After getting sick of taking the Team 1 approach for over a week, I finally decided to take the Team 2 approach.

Let’s just rebind the buttons.

Rebinding the keys is easy in Godot. One line removes the existing binding, and a second line adds a new binding. It’s as simple as:

InputMap.action_erase_events(action_name)
InputMap.action_add_event(action_name, event)

I think I may have gone too deep into the Team 2 mindset, though. I decided that I should rebind all the problem buttons on every input. So every time you push a button on the keyboard, the game erases the existing controls and re-binds them to whatever device that input came from. It sounds slow, and it sounds like it would cause some sort of lag or stuttering, but it works flawlessly. I even measured the frame time that the function used, and it was so insignificant, it showed up as 0.0ms.

If it’s stupid and it works, it’s not stupid. looking at it now, it’s a beautifly simple solution. As the great, late Terry Davis said, “An idiot admires complexity, a genius admires simplicity.” I always feel like I’m living that quote whenever I solve a difficult problem like this. I will try to keep this quote in mind when I encounter problems like this in the future, for Pilot Light, and for anything else that might come up in my life. I’ll keep the Team 1 mindset that I usually have, but I’ll make it a point to consider what Team 2 would do.

>> Home