SQYD.studio

Independent game development that follows the fun.

Refactoring Gobland 5 – On Finding The Ground

Hello anyone! I ‘m back and blogging. It was a big week for our new goblin controller- we’ve started filling up the animator and re-implemented jumping and air control with much simplified calculations. Lots of setting to tweak and some behavior logic to iron out but I’m optimistic. It’s on the right track.

So I figure it would be a good time to present: drumrollTHE RAYCAST APPARATUSthunderous applause.

using Unity.Collections;  
using Unity.Jobs;  
using UnityEditor;  
using UnityEngine;

public class GoblinRaycastApparatus : MonoBehaviour  
{  
    [Tooltip("Settings that determine the behaviour of the raycast apparatus.")] 
    [SerializeField] private GoblinRaycastSettings raycastSettings;  

    NativeArray<RaycastCommand> _commands;
    NativeArray<RaycastHit> _results;  

    JobHandle _raycastHandle;  

    private GoblinState _goblinState;  

    public void InitializeApparatus(GoblinState state) {
        _goblinState = state;  
    }  

    private void Update()  {
        ScheduleRaycasts();  
    }  
    private void LateUpdate()  {      
        CompleteRaycasts();  
    }

With the caveat that I (mostly) have no idea what I’m doing*, let’s walk through this together so we can all learn how goblins find the ground. In this top bit of code we have a reference to the GoblinRacastSettings (see this post for more information about how I use Scriptable Objectsto handle settings). Notice the tooltip- I always put a tooltip on any field that is exposed to the Unity inspector. It’ll help me figure out what it does later, because I will forget. Next we create some NativeArrays to hold our RaycastCommands and their results (as RaycastHits).

This is my first toe in the water using Unity’s job system. Which I’ll admit I’m still getting my head around. The JobHandle is used to identify the raycasting job and await its completion before passing the results back to the GoblinState (check this post for more info). InitializeApparatus() is just used by the top level Goblin to make sure this component has a reference to the GoblinState.

The idea is to push the actual raycasting off the main CPU thread- we do that each Update()callback with ScheduleRaycasts() and we get the results from those side threads in LateUpdate() using CompleteRaycasts(). Are you ready for MORE CODE! no? Too bad. Here it comes:

private void ScheduleRaycasts()  
{  
    _commands = new NativeArray<RaycastCommand>(8, Allocator.TempJob);  
    _results = new NativeArray<RaycastHit>(8, Allocator.TempJob);  

    //Create the raycast to ground.  
    _commands[0] = new RaycastCommand(transform.position, Vector3.down, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    //Create the raycast to look ahead.  
    Vector3 lookAheadDirection = Vector3.RotateTowards(-transform.up,
    _goblinState.GlobalMomentum, raycastSettings.LookAheadRaycastRotation, 0);  

    _commands[1] = new RaycastCommand(transform.position, lookAheadDirection,
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    //Create the raycast to ground below hips.  
    _commands[2] = new RaycastCommand(transform.position, -transform.up, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    // Create the raycast upwards.  
    _commands[3] = new RaycastCommand(transform.position, transform.up, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    // Create the raycast to the left.  
    _commands[4] = new RaycastCommand(transform.position, -transform.right, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    // Create the raycast to the right.  
    _commands[5] = new RaycastCommand(transform.position, transform.right, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  
    // Create the raycast to the forward.  
    _commands[6] = new RaycastCommand(transform.position, transform.forward, 
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  
    // Create the raycast to the backward.  
    _commands[7] = new RaycastCommand(transform.position, -transform.forward,
    new QueryParameters(raycastSettings.GroundLayers), Mathf.Infinity);  

    _raycastHandle = RaycastCommand.ScheduleBatch(_commands, _results, 8); 
}

Each time update is called (once per frame) we are scheduling eight raycasts. The Apparatus component is on a game object which is is a child of the Goblin’s hips. If you want, anywhere you see transform.position here you can just think “goblin’s crotch” I won’t judge.

We start by setting up new NativeArrays (which maybe someday I’ll understand- but for now I just know the Jobs system needs ’em) for our results and commands. Then we get into scheduling our RaycastCommands. The first three commands (i.e 0, 1, 2) are what I’ve been calling “Grounding Raycasts” here we are trying to find a point somewhere below the goblin to anchor the ragdoll to.

  • _commands[0] goes straight down from the hips along down the global y-axis (i.e. Vector3.down).
  • _commands[1] uses a point directly below the goblin’s hips but rotated in the direction of the goblin is currently moving- that’s the lookAheadDirection. This value is used to check for slopes in the goblin’s path and pre-rotate the character controller for that real all-terrain goblin action.
  • _commands[2] gets a point directly below the apparatus (below the crotch). These three raycasts are used to figure out if the goblin has ground below it.

Whichever of these result has the shortest RaycastHit.distance will be considered the “Grounded Point”. We place the character controller at the grounded point each frame- when the goblin’s ragdoll is flying through the air the character controller stays on the ground below it ready to “catch” it when it lands.

_commands[2 – 7] are directional commands (yes, 2 gets used twice). These are cast from the crotch in six directions (up, down, forward, backward, left, right) in order to find the nearest ground point. This is used to determine if the goblin is close enough to wall jump and what animation to use when they do, among a few other animator parameter things. After that we package all the commands into the _raycastHandle and schedule the job to unpack during LateUpdate().

MOAR CODE NOW!

private void CompleteRaycasts()  
{  
    _raycastHandle.Complete();  

    RaycastHit downData;  
    RaycastHit belowHipsData;  
    RaycastHit momentumData;  
    RaycastHit[] directionData = new RaycastHit[6];  

    //Check for ground Down Raycast at position 0- Down.  
    downData = _results[0];  
    //Check for ground in momentum direction Raycast at position 1- Look Ahead.  
    momentumData = _results[1];  
    //Check for ground below hips Raycast at position 2- Below.  
    belowHipsData = _results[2];  
    directionData[0] = _results[2];  
    //Check for ground Up Raycast at position 3 - Above.  
    directionData[1] = _results[3];  
    //Check for ground Left Raycast at position 4 - Left.  
    directionData[2] = _results[4];  
    //Check for ground Right Raycast at position 5 - Right.  
    directionData[3] = _results[5];  
    //Check for ground Forward Raycast at position 6 - Forward.  
    directionData[4] = _results[6];  
    //Check for ground Backward Raycast at position 7 - Backward.  
    directionData[5] = _results[7];  
    //Set the goblin state with the raycast data.  
    _goblinState.SetGroundingInformation(downData, belowHipsData, momentumData,
    directionData);  

    _results.Dispose();  
    _commands.Dispose();  
}

Here we unpack the raycasts in LateUpdate(). First we call Complete() on the raycast job handle. This makes sure that all the raycast jobs are complete when we need them. Yes this takes the any incomplete RaycastCommands and runs them on the main thread but from what I can see in the profiler usually the job is usually already complete by the time LateUpdate() is called. Then we throw all the RaycastHits into some arrays and pass them off to the GoblinState to process. Logic for checking what raycast produces the “Grounded Point” and which produces the “Near Ground Point” is handled in the GoblinState. That’s all for the raycast apparatus. Almost. I got one more function to share.

public void OnDrawGizmosSelected()  
{  
//We only want to draw gizmos if we have a goblin state.  
    if(_goblinState == null) {  
        return;  
    }   

//Draw the Raycast to the ground below hips.  
    if (_goblinState.HasGroundBelowHips)  
    {        Gizmos.color = Color.green;  
        Gizmos.DrawLine(transform.position, _goblinState.GroundBelowHipsPoint);  
        Gizmos.DrawSphere(_goblinState.GroundBelowHipsPoint, 0.1f);  
        Handles.Label(_goblinState.GroundBelowHipsPoint, $"Below Hips");  
    }  
//Draw the Raycast to the ground down (Vector3.down).  
    if (_goblinState.HasGroundDown){
        Gizmos.color = Color.yellow;  
        Gizmos.DrawLine(transform.position, _goblinState.GroundDownPoint);  
        Gizmos.DrawSphere(_goblinState.GroundDownPoint, 0.1f);  
        Handles.Label(_goblinState.GroundDownPoint, $"Down";  
    }
//Draw the Raycast in the lookahead direction  
    if(_goblinState.HasGroundInMomentumDirection){        
        Gizmos.color = Color.blue;  
        Gizmos.DrawLine(transform.position,
        _goblinState.GroundInMomentumDirectionPoint);  
        Gizmos.DrawSphere(_goblinState.GroundInMomentumDirectionPoint, 0.1f);  
        Handles.Label(_goblinState.GroundInMomentumDirectionPoint, $"Momentum");  
    }

//Draw the raycast to the Nearest ground point          
if (_goblinState.HasClosestGround_Directional){
        Gizmos.color = Color.red;  
        Gizmos.DrawLine(transform.position, 
        _goblinState.ClosestGroundPoint_Directional);  
        Gizmos.DrawSphere(_goblinState.ClosestGroundPoint_Directional, 0.1f);  
        Handles.Label(_goblinState.ClosestGroundPoint_Directional, $"Closest");  
    }
}

I’m not going to walk you through this block, but I wanted to include it. Why? I wish I made better use of OnDrawGizmos() and/or OnDrawGizmosSelected() when I was starting to learn Unity. These Monobehaviour calls can illuminate so much about what’s going on under the hood of your game. If you are new to Unity, don’t be like me, learn your gizmos early and use them often. They will help you get to grips with manipulating vectors, quaternions, whatever. Absolutely essential debugging tools.

And that’s full time for me for this week. Taking the time to write this out helped me notice some optimizations I could still make (do the raycasts need to be infinite? is it actually necessary to repackage the RaycastHits from the native array?), but I’ve learned my lesson about premature optimizationand my apparatus is working so I’m going to leave it as is for now.

Thank you for reading, if you did. If you have any comments/questions/criticisms about my code please let me know, but be kind- my ego is fragile and I will cry(joke**). I hope maybe this helps someone new to Unity get their head around RaycastCommands, and I very hope it’s not terrible advice that will lead the naive knowledge seeker down a dark path (it probably isn’t).

Until next week.


* It’s okay because a buddy of mine who’s been a professional software developer for a decade tells me he has no idea what he’s doing either.
** Sort of a joke. Amount/likelyhood of cry depends on my overall level of excitement/agitation, quality of last night’s sleet, and how long it’s been since I remembered I have to eat food regularly in order to survive.

Leave a comment