I wrote all the code for this project. That means all the gameplay, golf physics, map tooling, networked multiplayer, level state management, etc. There’s a couple of neat problems I had to solve that sit between a few systems with no easy category, so I’ll put all my stories about them here.
Golf Ball Physics / Magnus Forces
Early on, I decided that I should program all of the golf ball movement physics by hand, since I figured that we’d be fiddling with the golf physics a great deal. I’ve been in this situation before with earlier projects, and learned the hard way that when a core gameplay pillar of a game is leaning on an engine-implemented feature, it will result in a lot of very painful back-and-forth with that feature. If I went through the pain of doing movement physics now, that knowledge would be useful for the future when we’re trying to polish everything to a mirror shine.
Most of the implementation went smoothly (thank you, physics class) until I got to the subject of how golf ball topspin functions, and how to model it as a force on the ball. Thankfully, as I quickly found out, there are a lot of college-level physics programs that use the aerodynamics of a golf ball as a physics example when explaining the Magnus Effect, which is the formal physics term for the effects on a rotating object moving through a fluid. If you’ve ever seen this video of a spinning basketball being dropped from a dam, only to suddenly gain horizontal speed and fly off, that’s the Magnus Effect.
The exact way I went about implementing this was to essentially take the ball’s calculated drag force, and apply a portion of it to the ball movement vector’s Y and Z axes (X is forward in this case), based on the topspin and slice / hook spin. After implementing this, it immediately made me realize that our golf ball movement had never looked right. Here’s a diagram from this website showing the tracked trajectory of a golf ball at different underspin speeds:
The vertical loft that a golf ball gets in flight is quite significant, and without it, golf balls don’t even go half as far. The cool thing is, I can demonstrate that this is working identically in my implementation, using some debug draw to plot the live ball’s trajectory on a 2D positional axis:
A normal shot with an in-game spin-drag coefficient of 1. Note the positional graph on the left.
The same shot with a spin-drag coefficient of 0.
For fun, the same shot with a spin-drag coefficient of 5. It stays in the air for about 2 minutes.
Golf Club Tuning
Take a look at this number down here, indicating the range of this club:
Nowhere in this image does it display the “speed” or “power” of the shot. One might assume that, with this club, if you shoot it at max speed, you get the listed range, and if you shoot at 50% speed, you get half the listed range. But that’s not how speed and shot power actually work.
At some point in development, I was spending some time trying to tune all the clubs to have correct real-life ranges, and was tuning power values up and down and taking shots at different power levels, trying to make sure everything matched up correctly. I would change the power level, note the distance on a max power shot, and then play through a course normally trying to see if that number matched up consistently. Oddly, it never did.
Eventually I got frustrated enough with this that I took the ball physics code I had wrote and made an Euler series function out of it, so I could plug in an arbitrary shot angle & power and immediately get the resulting distance. Unfortunately, that function matched up with the max range numbers I had been getting, as if there wasn’t an issue; but in-game, fractional power shots were still unpredictable. What was the problem?
My faulty assumption was that power and range had a linear relationship. If a shot at 200 power went 200y, that meant a shot of 100 power would go 100y, right? Wrong. The hint was the Euler function I had to make. If this was truly a simple, linear relationship, there would be no need to run a series and find the resultant value. The golf ball code had too many variables to be linear; elastic collision thresholds, variable drag, the aforementioned loft, and so on.
But, from a player’s perspective, their input has to be predictable and linear. Bringing up the shot bar again:
If a player hits a shot that says 50% on this bar, they expect it to go 50% of the listed distance, even if that’s not really how shot power works. So, instead, I had to translate the player expectant value to the correct physics value. If a 200 power shot goes 200y, and the player hits a shot at 50%, expecting it to go 100y, I have to find the correct power value that will result in it going that distance.
The way I fixed this was by calculating all the resulting distance values of all power percentages of all clubs and storing that distance. Then, when a player takes a shot, I convert their shot bar percentage to their desired distance value, and lookup the power value in the table that will get me that far. Here’s a graph of the relationship of power percentage to distance for both drivers and irons:
Both clubs have a slow start, and the driver starts to gain less distance with power as the power gets higher. But, critically, the relationship between power and distance is not linear for either club, as expected. The worst case that I regularly encountered before this fix was doing a 25% power driver shot, and only going ~45y, instead of the expected ~130y. Big difference there.
You might have looked at my “calculate everything ahead of time” solution as maybe a bit brute-force-y, but here’s the bright side: we can precalculate all of this ahead of time, for all the clubs defined in the game, and just use the lookup data at runtime. Even that offline calculation time is practically nothing, in the grand scheme of CPU times.