Smooth Waypoint Follower Script 📄

Features:

  • Super Smooth
  • Set Triggered Speed Changes At Waypoints
  • Invoke Method Calls At Waypoints

TLDW: Feel free to skip to last minute or so if you want to see the full capabilities of this script.

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityStandardAssets.Utility;

#if UNITY_EDITOR
using UnityEditor;
#endif

/**
 * Created by Zaneris.
 * Follows the route of a WaypointCircuit at a constant speed.
 * Enables Waypoint based speed settings.
 * Enables invoke of method calls triggered by waypoints.
 **/
public class BetterWaypointFollower : MonoBehaviour {
    public UnityEngine.Object circuitObject;
    public WaypointCircuit circuit;
    public float routeSpeed = 50f;
    public float lookAheadDistance = 100f;

    #region Varying Speed Variables
    public bool varyingSpeed = false;
    public float initialSpeed;
    public float rateOfChange = 1f;
    public float[] waypointSpeedFactors;
    #endregion

    #region Invoke Method Variables
    public bool invokeMethods = false;
    public bool[] invokeWaypointEnabled;
    public string[] invokeNames;
    public float[] invokeDelay;
    public UnityEngine.Object[] invokeObject;
    #endregion

    private float[] distances;
    private float progressDistance;
    private float currentSpeed;
    private int lastWaypoint;

    // Pull Waypoint data from circuit to set up our varying speed variables.
    public void InitializeSpeeds() {
        if(waypointSpeedFactors == null || waypointSpeedFactors.Length != circuit.Waypoints.Length) {
            waypointSpeedFactors = new float[circuit.Waypoints.Length];
            for(int i = 0; i < waypointSpeedFactors.Length; i++)
                waypointSpeedFactors[i] = 1f;
            initialSpeed = routeSpeed;
        }
    }

    // Pulling for invoke method variables.
    public void InitializeInvoke() {
        if(invokeObject == null || invokeObject.Length != circuit.Waypoints.Length) {
            invokeWaypointEnabled = new bool[circuit.Waypoints.Length];
            invokeNames = new string[circuit.Waypoints.Length];
            invokeDelay = new float[circuit.Waypoints.Length];
            invokeObject = new UnityEngine.Object[circuit.Waypoints.Length];
            for(int i = 0; i < invokeNames.Length; i++)
                invokeWaypointEnabled[i] = false;
            initialSpeed = routeSpeed;
        }
    }

    private void Start() {
        // Set up our distances, so we know which waypoint we're at.
        distances = new float[circuit.Waypoints.Length + 1];
        for(int i = 1; i < circuit.Waypoints.Length; i++)
            distances[i] = (circuit.Waypoints[i].position - circuit.Waypoints[i - 1].position).magnitude + distances[i - 1];
        distances[circuit.Waypoints.Length] = (circuit.Waypoints[circuit.Waypoints.Length - 1].position - circuit.Waypoints[0].position).magnitude + distances[circuit.Waypoints.Length - 1];

        ResetWaypointCircuit();
    }

    // Reset everything to the starting point.
    public void ResetWaypointCircuit() {
        progressDistance = 0;
        transform.position = circuit.Waypoints[0].position;
        transform.LookAt(circuit.GetRoutePoint(lookAheadDistance + 6.576f).position);
        if(varyingSpeed) {
            currentSpeed = initialSpeed;
        } else {
            currentSpeed = routeSpeed;
        }
    }

    private void Update() {
        #region Determining our position relative to waypoints.
        if(progressDistance > distances[distances.Length - 1])
            progressDistance -= distances[distances.Length - 1];
        for(int i = 1; i < distances.Length; i++)
            if(progressDistance < distances[i]) {
                // Check if we've passed a new waypoint and whether or not it has a method call attached.
                if(i - 1 != lastWaypoint && invokeMethods && invokeWaypointEnabled[i - 1])
                    ((MonoBehaviour)invokeObject[i - 1]).Invoke(invokeNames[i - 1], invokeDelay[i - 1]);
                lastWaypoint = i - 1;
                break;
            }
        #endregion
        if(varyingSpeed) { // Adjust speed based on Editor provided data.
            float waypointSpeed = Mathf.Pow(routeSpeed, waypointSpeedFactors[lastWaypoint]) - .99f;
            float acceleration = Mathf.Pow(routeSpeed, rateOfChange);
            if(Mathf.Abs(currentSpeed - waypointSpeed) > acceleration / 50f)
                currentSpeed += (waypointSpeed < currentSpeed ? -1f : 1f) * acceleration * Time.deltaTime;
            else
                currentSpeed = waypointSpeed;
        } else {
            currentSpeed = routeSpeed;
        }

        Vector3 nextPosition, nextDelta;
        do { // Making sure our target point is far enough ahead on the route.
            progressDistance += Time.deltaTime * currentSpeed * .8f;
            nextPosition = circuit.GetRoutePoint(progressDistance).position;
            nextDelta = nextPosition - transform.position;
        } while(nextDelta.magnitude < 10f);

        nextDelta.Normalize(); // Set our direction vector to exactly 1 in magnitude.
        nextDelta *= currentSpeed; // Scale it back up to exactly our specified speed.
        transform.position += nextDelta * Time.deltaTime;
        transform.LookAt(circuit.GetRoutePoint(progressDistance + lookAheadDistance).position);
    }

    private void OnDrawGizmos() {
        if(Application.isPlaying) {
            Gizmos.color = Color.green;
            Gizmos.DrawLine(transform.position, transform.position);
            Gizmos.DrawWireSphere(circuit.GetRoutePosition(progressDistance), 1);
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(transform.position, transform.position + transform.forward);
        }
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(BetterWaypointFollower))]
public class BetterWaypointEditor : Editor {
    List<String> monoBehaviours;
    int totalMethods;

    public override void OnInspectorGUI() {
        BetterWaypointFollower script = (BetterWaypointFollower)target;
        script.circuitObject = EditorGUILayout.ObjectField("Waypoint Circuit", script.circuitObject, typeof(WaypointCircuit), true);
        if(script.circuitObject != null) {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("-- Settings --", EditorStyles.boldLabel);
            script.circuit = (WaypointCircuit)script.circuitObject;
            script.routeSpeed = EditorGUILayout.FloatField("Speed In Units/Sec", script.routeSpeed);
            script.lookAheadDistance = EditorGUILayout.FloatField("Look Ahead Distance", script.lookAheadDistance);
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("-- Varying Speed --", EditorStyles.boldLabel);
            if(GUILayout.Toggle(script.varyingSpeed, " Enable Varying Circuit Speeds?")) {
                script.varyingSpeed = true;
                script.InitializeSpeeds();
            } else {
                script.varyingSpeed = false;
            }
            if(script.varyingSpeed) {
                EditorGUILayout.Space();
                script.initialSpeed = EditorGUILayout.FloatField("Initial Speed In Units/Sec", script.initialSpeed);
                EditorGUILayout.Space();
                for(int i = 0; i < script.waypointSpeedFactors.Length; i++)
                    script.waypointSpeedFactors[i] = EditorGUILayout.Slider("Waypoint " + i.ToString("D3") + " Factor", script.waypointSpeedFactors[i], 0f, 2f);
                EditorGUILayout.Space();
                script.rateOfChange = EditorGUILayout.Slider("Rate Of Change Factor", script.rateOfChange, 0f, 2f);
                EditorGUILayout.HelpBox("Waypoint speed factors take effect as soon as you pass that point, "
                    + "and how quickly the change happens is determined by the 'Rate Of Change'."
                    + "\nThe speed factor is exponential. For example, 30 to the power of 2 is 900.", MessageType.Info);
            }
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("-- Invoke Methods --", EditorStyles.boldLabel);
            if(GUILayout.Toggle(script.invokeMethods, " Enable Invoke Method Calls At Waypoints?")) {
                script.invokeMethods = true;
                script.InitializeInvoke();
            } else {
                script.invokeMethods = false;
            }
            if(script.invokeMethods) {
                EditorGUILayout.Space();
                for(int i = 0; i < script.invokeWaypointEnabled.Length; i++) {
                    script.invokeWaypointEnabled[i] = EditorGUILayout.Toggle("Invoke At Waypoint " + i.ToString("D3") + "?", script.invokeWaypointEnabled[i]);
                    if(script.invokeWaypointEnabled[i]) {
                        script.invokeObject[i] = EditorGUILayout.ObjectField("Game Object Target", script.invokeObject[i], typeof(MonoBehaviour), true);
                        if(script.invokeObject[i] != null) {
                            Type type = script.invokeObject[i].GetType();
                            MethodInfo[] methods = type.GetMethods();
                            if(monoBehaviours == null)
                                FillList();
                            string[] names = new string[methods.Length - totalMethods + 1];
                            if(names.Length == 0) {
                                EditorGUILayout.HelpBox("You have no public methods in your game object."
                                        + "\nAdd 'public' in front of the method you wish to invoke.", MessageType.Error);
                            } else {
                                int l = 0;
                                for(int j = 0; j < methods.Length; j++)
                                    if(!monoBehaviours.Contains(methods[j].Name)) {
                                        names[l++] = methods[j].Name;
                                    }
                                int index = 0;
                                for(int j = 0; j < names.Length; j++)
                                    if(names[j] == script.invokeNames[i]) {
                                        index = j;
                                        break;
                                    }
                                index = EditorGUILayout.Popup("Method To Call", index, names);
                                script.invokeNames[i] = names[index];
                                float delay = EditorGUILayout.FloatField("Delay In Seconds", script.invokeDelay[i]);
                                script.invokeDelay[i] = delay < 0f ? 0f : delay;
                            }
                        }
                    }
                }
            }
        }
    }

    private void FillList() {
        totalMethods = 0;
        monoBehaviours = new List<string>();
        MethodInfo[] methods = typeof(MonoBehaviour).GetMethods();
        foreach(MethodInfo mi in methods) {
            monoBehaviours.Add(mi.Name);
            totalMethods++;
        }
    }
}
#endif

BetterWaypointFollower.zip (2.8 KB)

Just attach the script to your camera the same as the original WaypointProgressTracker script.

Cheers! @ben @Daniel_Altman @VitasAn @Severn2j

61 Likes

Added a ton of new features!

Change your speed at certain waypoints.
Invoke method calls when crossing certain waypoints!

Watch the YouTube video up top.

4 Likes

thanks!

1 Like

Looks nice! I wonder how hard it would be to include a method to begin the loop at a specific waypoint… (I’m thinking along the lines of giving 3 enemies the same waypoint follower and starting them each at different points on the loop)

1 Like

Hey Brian, it wouldn’t be too difficult to do, I might modify the script a bit to give the ability to change things like that from outside of it, like, being able to link it to your own script and set starting positions.

Otherwise you’re free to give it a shot to try and make some changes in the mean time :slight_smile:

1 Like

Great work. Have you thought about submitting it to the asset store?

2 Likes

@Zaneris thank you so much for the better follower script this has fixed a ton of camera problems i had and also fly trough problems thank you so much after replacing this my ship is much smoother , your terrain and flytrough and game setup looks amazing btw

2 Likes

Thank you! This worked great!!

1 Like

This is fantastic, the lurching issues I was having before really bothered me, glad you took the time to do this, thanks!

2 Likes

<3 Thank you!

1 Like

this is amazing ! thanks for your great work

1 Like

If anyone is concerned about licensing, etc… Consider it public domain for those taking the unity course. :+1:

5 Likes

Many thanks for creating and sharing this and for making it available to those on the course. You rock!

3 Likes

Thank You very much for this. It smoothed out the initial rushed flying of the ship and the rest of the route became quite smooth.
Ship navigate

2 Likes

Great work, thanks for sharing!

2 Likes

nice m8 now do this for the unreal course :grinning:

1 Like

Thanks a lot for sharing! A lot better than the Unity one.

1 Like

Thanks for this, I was having problems with the supplied version where my ship would race around the circuit at some crazy speed before continuing on at normal speed. This one is much better :slight_smile:

1 Like

@Zaneris thank you! This is awesome :+1:

1 Like

Thanks for doing this!

1 Like