July 24, 2017
July 24, 2017
Press Releases
July 24, 2017
Games Press

If you enjoy reading this site, you might also want to check out these UBM Tech sites:

Lovers in a Dangerous Spacetime DevLog #7: Learning How to Walk
by Adam Winkels on 02/20/14 03:52:00 pm

The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

We've had ground-based enemies, which we call Walkers, in Lovers since way back in the days of the GDC 2013 build. Until recently these enemies have been tethered to spherical (well, circular) planets, so programming their movement was simply a matter of ensuring that their distance from the center of the planet was constant and their velocity was tangential to the vector from the enemy's position to the planet's center. However, as we continued to add new scenarios for players to experience we needed Walkers to be able to traverse more exotic terrain. Being the lazy developers that we are, our first attempt to implement a more robust walking algorithm was the simplest and most naive that we could come up with. Luckily for us, it worked out pretty well.

Our naivety lead us to use raycasts to check for a collider underneath a Walker. Shooting one ray from the center may work for some situations, but the Walker would look squirrely as it moves across significant changes in terrain. Instead we shoot two rays, one from each side of the Walker:

bool doubleRaycastDown(TerrainMovementRayProperties movementRay, float rayLength, out RaycastHit leftHitInfo, out RaycastHit rightHitInfo)
{
Vector3 transformUp = transform.up;
Vector3 transformRight = transform.right;
Ray leftRay = new Ray(transform.position + movementRay.originOffsetY * transformUp + movementRay.distanceFromCenter * transformRight, -transformUp);
Ray rightRay = new Ray(transform.position + movementRay.originOffsetY * transformUp - movementRay.distanceFromCenter * transformRight, -transformUp);

return Physics.Raycast(leftRay, out leftHitInfo, rayLength, DefaultTerrainLayerMask) && Physics.Raycast(rightRay, out rightHitInfo, rayLength, DefaultTerrainLayerMask);
}

If both rays hit a collider with the proper layer, we use take the average of the `position` and `normal` vectors of the resulting `RaycastHit` objects and use them to position and orient the Walker, respectively. Easy as that!

void positionOnTerrain(RaycastHit leftHitInfo, RaycastHit rightHitInfo, float maxRotationDegrees, float positionOffsetY)
{
Vector3 averageNormal = (leftHitInfo.normal + rightHitInfo.normal) / 2;
Vector3 averagePoint = (leftHitInfo.point + rightHitInfo.point) / 2;

Quaternion targetRotation = Quaternion.FromToRotation(Vector3.up, averageNormal);
Quaternion finalRotation = Quaternion.RotateTowards(transform.rotation, targetRotation, maxRotationDegrees);
transform.rotation = Quaternion.Euler(0, 0, finalRotation.eulerAngles.z);

transform.position = averagePoint + transform.up * positionOffsetY;
}

Well, almost as easy as that. Allowing the Walker to rotate an arbitrary amount each frame leads to very jittery-looking motion as it traverses the bumps and crevices. To combat this, we limit the amount it is allowed to rotate each frame using `Quaternion.RotateTowards`. This tweak goes a long way to allowing for more lifelike movement.

The Walker also had a tendency to get stuck in or clip through certain types of concave geometry. To overcome this, we added a very short horizontal ray in the direction of motion that extends to about the edge of the character's body. If this ray collides with any piece of terrain, it will override the downward ray on that side of the Walker.

if(rigidbody.velocity.sqrMagnitude > 0)
{
//if moving left
if(MathUtilities.VectorSimilarity(rigidbody.velocity, selfTransform.right) > 0)
{
RaycastHit overrideLeftHitInfo;
Ray overrideLeftRay = new Ray(transform.position + horizontalRayProperties.originOffsetY * selfTransform.up, selfTransform.right);
{
leftHitInfo = overrideLeftHitInfo;
}
}
//if moving right
else
{
RaycastHit overrideRightHitInfo;
Ray overrideRightRay = new Ray(transform.position + horizontalRayProperties.originOffsetY * selfTransform.up, -selfTransform.right);
{
rightHitInfo = overrideRightHitInfo;
}
}
}

Below is the system in motion. The yellow and red lines represent the rays being shot from the Walker, the green and magenta lines are the normals generated from those rays' collisions with terrain and the cyan line is the average of those normals, originating at the average terrain intersection point of the Walker rays. (The horizontal override ray is not shown.)

The asteroid the enemy is walking on is a compound collider consisting of sphere and capsule colliders, but this system works equally well on an arbitrary mesh collider.

Original post

// Adam Winkels (@winkels) is a co-founder of Asteroid Base.

### Related Jobs

Phosphor Games Studio — Chciago, Illinois, United States
[07.23.17]

All Positions
Phosphor Games Studio — Chciago, Illinois, United States
[07.23.17]

Programmer
Infinity Ward / Activision — Woodland Hills, California, United States
[07.22.17]

Senior AI Engineer - Infinity Ward
Disruptor Beam — FRAMINGHAM, Massachusetts, United States
[07.21.17]

Sr. QA Engineer