Using DualShock 4 controllers with Civilization on Mac
Great ergonomics is a thing you grow to appreciate. It's also the main reason why I like playing games on consoles rather than on my computer. Modern games consoles also come with controllers that are out of the box compatible with a vast array of games on Mac OS X and Windows. However, computer strategy games largely ignore these controllers and rely on traditional keyboard and mouse controls instead.
I like playing Civilization V a lot. I'm not any good at the game (or any of them) but I like it. What I don't like, though, is the fact that poor ergonomics stop me from enjoying the game. Using keyboard and mouse forces me to stay in a fixed posture in front of the computer and puts a lot of strain on my upper back. Oh, how I wanted to play Civilization using a modern games controller.
Need ideas? Just browse NPM
During the past few years, I've used Node.js to build various command line tools and quick hacks. It's my go-to environment for hacking something quick together. I just feel so immensely productive when I'm doing stuff on Node, and it's mostly thanks to NPM.
Browsing packages on NPM is one of my favorite pastimes. It was on one of my regular NPM browsing sessions that I ran into two rather interesting modules:
- ds4: “Stream events from DualShock 4 controllers”
- mac-vhid: “Node addon that allows you to control the mouse and keyboard on a Mac through a virtual human input device.”
These are just the things I needed to realize my dream of playing Civilization using a DualShock. As these two modules would do all the heavy-lifting, all that was needed was a piece of glue code to map DS4 data frames to keyboard and mouse events. That's how macds4civ got started.
Reading DualShock events
The ds4 module is actually very simple: it's just a parser for data buffers provided by the node-hid module. It also comes with a simple command line utility, called ds4-dump, that can be used to dump parsed DualShock data frames to console.
The source for the ds4-dump executable can be found in the bin directory of the module. All the things I needed for successfully reading controller events are covered by this code:
- Controller discovery
- Connection type detection
- Data event handling
I just copied the ds4-dump source as a base for macds4civ and worked from there.
Interpreting analog inputs
To make the controls at least somewhat intuitive, I wanted to move the mouse pointer using the left analog stick on the DualShock. Unlike their digital counterparts, analog inputs tend to suffer from noise and calibration issues. This is also true for the analog controls on the DualShock 4 controller and to overcome these issues I needed two things: dead zones and a movable origin.
Analog stick position on an axis is expressed as an integer ranging from 0 to 255. For example, when a centered stick is pushed to the left, the X axis value goes from 127 (center) to 0 (full left). When the stick is pushed to the right, the same value goes from 127 (center) to 255 (full right). Or that's the principle, at least.
In reality, though, the values are somewhat noisy. Having the stick centered (without touching the controller at all) yields values between 120 and 135. To prevent the mouse pointer from slowly wandering around, a dead zone around the origin (about 127 in this case) can be specified. This dead zone should be large enough to cover the full amplitude of the noise but not too large to produce a noticeable step in the analog movement. All values that fall inside the dead zone are interpreted as absolute origin values. It's worth noting that the noise tends to gradually intensify as the controller gets more wear and tear. It seems a dead zone with a radius of around 20 fits the bill rather nicely in my DS4s' case.
Wear and tear can also cause origin drifting. This can, of course, be countered by increasing the dead zone radius, but that essentially decreases the usable analog range. A better way to counter this problem is to recalibrate the range, i.e. shift the origin. Also, specifying the origin makes it very easy to map the stick position to relative changes in mouse position as HID events.
Interpreting button presses
Compared to handling analog inputs, mapping controller button presses to HID events is a lot simpler. There is, however, a conceptual problem: HID events are used to represent changes in state whereas controller data frames represent state. The way to deal with this is to compare current data frame to the previous one and relay differences between the two frames as events.
Putting it all together
At this point I could:
- detect DS4 controllers
- handle analog and digital inputs
- trigger virtual HID events
Now, all I needed was a way to make this thing configurable so that I could easily use the same code with a wide variety of games. For this, I decided to use konfu as it seemed to do just what I needed and nothing more.
So, the main application file is just a hair over 100 lines of JS with about 60 lines of my own code. It took me less than three hours to cobble this thing together and overall I'm very happy with the end result - except it's absolutely useless for me. You see, after I was done with it I tried installing it on the iMac I use to play Civilization and it just kept failing. Apparently, the Node.js installation on that machine is quite miserably broken and I have yet to find a way to fix it.
Oh well, it was a fun project to do. :)