Over the last day or two, I’ve been focused on prototyping and tuning player movement and controls in High Roller. I’ve got it to a place that feels snappy and fun to me, and I’m ready to move on to adding more obstacles. This post will showcase what I did to make the ball rolling feel fun. To get started, I’m going to share my first YouTube demo video, which highlights the iterative process used to enhance the movement controls.
Walking Before Running
The initial movement implementation was basic, and was really just the default Unity simulation. It featured:
- A 3D sphere representing the ball. This sphere had default values for mass, drag, etc.
- A continuous force (magnitude of 10) that was applied while the user held down movement keys. The force was applied to the top of the ball, and in one of the 8 directions allowed by the control scheme.
The results of this initial implementation can be seen from 0:00 – 0:18 in the above video clip. It had two main issues noticeable right away. First, the ball was very “floaty” and would take a long time to fall to the ground when rolling off an edge. Second, it wasn’t fast or responsive enough. This was most noticeable when trying to climb up ramps, but the sluggishness was always present even on flat terrain.
Defying Gravity with Extra Gravity
The first significant change I wanted to make was fixing the floatiness, and cause the ball to fall more quickly. Forgetting everything I learned in high school physics, I first tried to do this by increasing the mass of the ball. Unlike me, however, you may remember that all objects free fall at the same rate of acceleration regardless of their mass. So, it turns out that adding more mass just made the ball more sluggish, because the force applied by the user was less effective. It was still floaty while falling.
After realizing this, my second idea was to just increase the gravity. In Unity, this can be done via Edit > Project Settings > Physics
. I tried various values here, and was able to get the ball to fall faster. Unfortunately, it made the movement terrible in other ways. Friction made the ball unsteerable, and climbing ramps was virtually impossible with higher gravity.
What I really wanted was to increase the gravity only when the player was falling, but use the default when they are moving on the ground. I accomplished this via custom scripts that have two components:
- Determine if the player is “on the ground” or “in freefall”
- Apply additional downward force if the player is in freefall
I split this into two different components: a FallDetector
and a FreefallGravityBooster
. The reason I split these is that I knew I would need to determine whether the player is falling or not for other game mechanics later.
Recognizing When the Player is Falling
I use collision detection to determine if the player is falling or on the ground. Essentially, each frame I check if the player had a collision with any terrain objects. If they did, I consider them to be on the ground. If there were no terrain collisions, then the player is considered in freefall. This method has a flaw (specifically, falling while grinding against a wall will prevent the fall state from being detected). This flaw can be addressed by checking the direction of the collision, and ignoring collisions where the terrain is not below the sphere. I’m pretty sure I will need to add this enhancement eventually, to fix bugs like this one:
The source code for a (slightly simplified) version of my FallDetector
script is included below.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class FallDetector : MonoBehaviour { // flag indicating if the player is currently falling private bool _falling; // flag indicating if we are "on terrain" in this frame private bool _onTerrain = true; // the tag to use to identify an object we collide with is "terrain" or not [SerializeField] private string _terrainTag = "Terrain"; public bool Falling { get { return _falling; } } void FixedUpdate() { // each update, we set our _falling flag based on whether we contacted terrain last fram or not _falling = !_onTerrain; // clear the 'on terrain' flag, which will be set again by collision detection _onTerrain = false; } private void OnCollisionEnter(Collision other) { HandleCollision(other); } private void OnCollisionStay(Collision other) { HandleCollision(other); } private void HandleCollision(Collision other) { if (other.gameObject.CompareTag(_terrainTag)) { _onTerrain = true; } } }
The FreefallGravityBoost script is much simpler, and uses the AddForce method of Rigidbody to add extra gravitational force. The relevant code is show below, and the full version is in GitHub:
... void FixedUpdate() { if (_fallDetector.Falling && _playerConfig.FreefallGravityMultiplier != 1) { // subtract 1, since the physics engine applies 1x normal gravity for us _body.AddForce(Physics.gravity * (_playerConfig.FreefallGravityMultiplier - 1), ForceMode.Acceleration); } } ...
This uses the FallDetector
‘s “Falling” property defined above, and the gravity
static field of Unity’s Physics class. The boost is a floating-point multiplier of “normal” gravity, and is set via a player configuration ScriptableObject. The benefit of this structure is being able to tune values quickly while prototyping. I describe this process more thoroughly in one of my Slash Racer devlogs.
It turns out, that six times normal gravity while falling “felt right” to me. This is basically just a magic number that I came to by testing a bunch of different values.
Other Enhancements
In addition to tuning gravity, I applied a few other modifications to the default movement physics. This was in the form of additional impulse forces when the player’s change directions. I added the ability to specify additional forces in three situations:
- when the player begins moving from a stationary position
- when the player completely reverses directions on either the north/south or east/west axis
- when the player changes course without completely reversing direction
Portions of the implementation of this are in the PlayerMovement
and PlayerInputController
scripts, which I won’t embed here. The key point is that each of these features is controlled by a global configuration parameter that I tuned during prototyping today. And, if needed, I can change these values (including disabling them) as development continues.
What’s Next?
Now that I feel good with player movement, my goal is to flesh out the level and include more types of obstacles. This will let me verify the controls provide a good balance of fun and challenge in the real courses I design.
For more content like this, check out the complete set of High Roller development logs. Additionally, you might want to browse my set of Unity tutorials or follow me on Twitter.