Building a Golden Tee Fore Cabinet: Part 3 - Fix Trackball

Check out Part 1 and Part 2 to get some background on this project, including how I went about building an actual arcade cabinet, as well as specifics on how I built a custom PC to emulate the game in MAME.


Background

Trackball woes.

If you followed Parts 1 and 2, I have effectively built a custom Golden Tee Fore arcade cabinet, I have everything installed and previously tested working, and I go to rip my driver off the first tee and the club sputters, stops randomly, then dinks the ball a fraction of the distance it should go. What's going on here and why didn't I see this in my testing?

Up until this point in time, the only testing I had done with the actual trackball was physically holding the trackball assembly with one hand, and flicking the ball forward with my other hand. With that limited amount of force, the game worked fine in all my testing. It was only when I was able to actually mount the trackball, and give it a good proper swing that I ran into this issue.


What's the problem?

To understand what's happening here, you need to understand how these analog trackballs work. When you move the trackball around, you're turning two wheels, one for the X-axis, and one for Y-axis. These "wheels" are slotted, typically with 24 openings. Rather than detecting the speed of rotation at the axel that turns the wheel, an optical sensor counts how quickly the pulses come in from the spinning wheel.

Here's a rough depiction of what's happening here. Each rotation that crosses a slot triggers a "pulse" and the arrival of those pulses get converted into an acceleration of sorts that a computer interprets as movement.

The problem is that if you spin the trackball too quickly, this wheel rotates so fast that the sensor doesn't actually detect any movement, or it only detects a fraction of the actual pulses it should, which translate into way less movement to the cursor (golf club) then intended:

A MAME Dev confirmed this behavior:

The phenomenon referenced is the Wagon-wheel effect.


Proposed Solution

So I went through a few different iterations for this and landed on something relatively straight forward: I can listen for the acceleration of the trackball, and when I think it's about to bottom out, I artificially inject a large movement that the game interprets as a very strong swing.

This works because it's proactive rather than reactive. In that, if I am listening for a dropout, then it's already too late to respond since the computer would have already received a counter-intuitive reading, such as a "0" rather than a really high number signifying movement. And once the game reads a "0", then your swing is already ruined. To solve for this, we have to approximate when we think the trackball is about to bottom out, and inject movement.

Prerequisites for this solution

Short answer: Linux

Longer answer: I think the difficult part of this challenge is being able to read, in a near-real time capacity, the inputs from your trackball. Not only that, but artificially injecting movement as if it were your trackball. Turns out all Linux systems have a generic input event interface that (to quote Wikipedia):

generalizes raw input events from device drivers and makes them available through character devices in the /dev/input/ directory

Luckily I was able to find python-evdev which provides a Python-bindings to this interface, which makes it materially easier to work with.


Code and Walkthrough:

As a disclaimer, I won't claim this is a perfect solution, but it has been working for me for a few months now. I've played around with some other approaches that did not work as intended.

Anyway, here's basically what's happening in this script:

  • Find your trackball device (line 32)
  • Every time you move the trackball, an event is generated, and I'm specifically listening for relative movements that are on the Y-axis (vertical)
  • Each time an event that meets that criteria is detected, I capture the relative movement value, as well as the timestamp.
  • I then use the previous event to compute an arbitrary "acceleration" of sorts.
  • If that "acceleration" is greater than a certain threshold (suggesting that you smacked the trackball but it probably isn't going to register), then I send my own input of a very large "relative value" change, 10 times at 120hz.
  • The loop continues indefinitely.

If you run the program and keep the print line in, you'll get something like this when you start moving the cursor.

  • The first line defines the device being used. In this case, HID 1241:1111 is my Suzo Happ trackball
  • Next, each line has four values: The calculated acceleration, the current relative movement, the last relative movement, and the difference in milliseconds between the last two events.

Notice how in many cases, the calculated acceleration is 0? This is because the relative movement of the trackball hasn't changed. If it was moving -1 at one interval and -1 at the next interval, then it's moving the same speed, it's just still moving.

If you look on the far right at the really long numbers - that is the difference in timestamps. If you divide by 1, you'll get the approximate "refresh rate" of the trackball. In my case, seeing a difference of 0.008 seconds / 1 = ~125hz refresh rate. This tells me that even when you're barely moving the trackball an "event" of that movement is being sent up to a hundred times a second!

Now look at some of the bigger numbers on the far left side, -15k and 15k. These numbers themselves are very arbitrary, but it's their magnitude that's meaningful. As the trackball starts to accelerate very slightly from -1 to -2 I'm capturing that as some form of acceleration because that change happened over 0.008 seconds.

In my case, the numbers of negative (perhaps again because of how I mounted the trackball?), but a negative number is an acceleration, and positive number is a deceleration suggesting the trackball is slowing down.

Here's what happens when I smack the trackball. You can roughly follow the progress of me "detecting" the large change in acceleration within a fraction of a second, and injecting movement to counteract some of the "stalling" or "jittery" behavior we know causes your swing to fail.

I highlighted the -125k "acceleration" value here as I set a threshold of -100k as when I believe a "large" swing was detected/ is in progress. In fact, you can see the very next line has an acceleration in the billions (again the number here is irrelevant, but the magnitude is important) suggesting that I successfully predicted a large movement. And although I assume my script captured it, notice that it took another fraction of a second (after another ~10 events) for my artificially injected event to be registered.

In this case, each print statement includes two events, so seeing an event say -35  -35 is a pretty clear sign to me that two of my artificial events are being seen back-t0-back.

Does it feel stiff?

I know what you're thinking... if I take any movement that even looks like a strong push of the trackball and convert it into a huge forward momentum, won't that make all swings in the game identical?

Not at all actually. In fact, I think simply because of the impreciseness of this script running and how quickly it can actually inject the movement, the swing is often subject to the imperfect nature of how it was accelerated as the artificial movement are seen right alongside actual ones.

Secondly, your swing in Golden Tee is also subject to how your X-axis acceleration is, and this script does not impact that in any way. So if you think you're going straight, but crush the trackball a few degrees off center, then that's what gets reflected in the game.

The only caveat and area for improvement I'm exploring is when you do some of the extreme ball-spin moves, such as an "A1" or "C3" where you're effectively swinging the club at some 45 degree angle in both your backswing and forward swing. In my testing, the impact of these swings is slightly dampened, though I suspect this is more because of how I figured my MAME sensitivity to X and Y separately rather than anything this script is doing.

The end result, a Golden Tee game that is really playable!


Run from startup

As I mentioned in Part 2, you'll want this script running anytime you're playing Golden Tee. In my case, since that's the only thing the machine does, I have an autostart script that triggers this to run at the same time I auto-launch Golden Tee. Same as before, just a simple shell script:

#!/bin/bash 
cd Desktop/goldentee/
qterminal -e python3 fix_trackball.py

Find whatever "autostart" or equivalent your operating system uses and link to your script, again, in my case this is LXQt via Lubuntu:


Some other things I considered

If you follow the above, you're probably thinking there a better way to do this, though my trial and error suggested to me that this is partially a game of optimization. If you spend too much "time" calculating the movements, then you risk not injecting them fast enough for it to matter. Since we can't "block out" the actual events coming from the trackball, which will go haywire at some point due to the above described effect, our best bet is to just inject so many large movements that the game "sees" this movement and reacts with a big swing before anything else happens.

One of the other avenues I considered was making the "value" I inject relative to how fast I felt you were accelerating. Something like:

if accel < -100000:
#this rough math tries to create a linear relationship between the calculated acceleration and relative trackball movements
# scale is approximately -100k = +35 relative Y; -200k = +45 relative movement
# the clamp method here ensures you don't send a value > 45, which is the largest human-achievable acceleration value I was able to get myself
	approxRelMovement = clamp(round(accel / -4400))
	pushValue(approxRelMovement)

The issue I received here is that I basically limited the output value to something between -35 and -45. Both of which are very large swings. However I was having to do so much math to convert some arbitrarily huge acceleration back to a relative movement value that I think I "missed" the window to actually make an impact. When I tested this, despite seeing evidence of sending a string of values between -35 and -45, they all were too "late", and the game often saw some of the rogue -1 / -35 values which caused the swing to jitter further.

There's another train of thought related to what classifies as "acceleration". I arrived at the -100k figure mainly through trial and error. Actually my first approach was to just look for a big relative movement (say -20+) and use that as the proxy for injecting movement. The problem here is that you may not ever actually get an event for a "big" swing. If you hit the trackball so fast that it accelerates so rapidly, it may jump from it's first reading of some minor acceleration (say <10), to immediately too fast to actually register. In which case, the next event detected could very well be a small number as the wheel bottoms out again and only picks up a minor movement, rather than the really-fast reality of the spinning wheel.


One Last Plug

I completed this project back in late 2021. It was a multi-month effort to scope out, design, mill, assemble, paint, build, configure, and setup this custom unit in my basement. I've played a dozen or so rounds, and it's always a conversation piece with friends and family over. I'm still scoping out my next project, but this one was a ton of fun. It was a pain-staking process of trial and error, but I'm very pleased how it turned out. If you want to support me and this effort, I'd encourage you to check out my design plans that are specific to the creation of the actual showpiece unit. My blog of Part 2 and 3 related to the hardware/ software and trackball fixes is my opportunity to give back to the community with the hope that there may be further development in optimization for the franchise.

Thanks for coming on this adventure!