My smooth enemy motion script with caveats. Open Source!

So I wanted my enemies to move smoothly, never suddenly changing their velocity or position. I wanted to define some kind of cubic spline or quadratic interpolation on a list of waypoints (ie Vector2 objects), but this proved much harder that I’d like. (Solving arbitrary systems of linear equations in C# must be possible, but I couldn’t do it.)

So instead I defined a MonoBehaviour class called “EnemyCyclicSmoothPathFollow”, which you can attach to an enemy sprite. The enemy ship must be (A) facing “upwards”, so you might need to flip the sprite rendered about y in the Unity inspector, and (B) it needs a Rigidbody2D.

When played, the enemy ships move around the 2D world in a smooth way, and never go beyond a certain point (bounded), and they repeat the same path periodically. (The period depends on the Least Common Multiple of the denominators used in the Sine and Cosine functions.)

There are two weaknesses with my approach:
– (1) you can’t easily control speed. You can rescale it, but the speed will still vary through the ship’s path. (I actually like this, it feels very natural, but the lack of game design control is not good.)
– (2) It’s very hard to define or even compute points that the ship follows. So the whole concept of a “waypoint” sort of goes out of the window with my approach… :confused: Maybe a clever mathematician can help me here.

Here is my script in full. I welcome any feedback or advice! Feel free to use in your own work, I’m not judging and I wouldn’t know anyway :stuck_out_tongue:

using UnityEngine;

[System.Serializable]
public struct RationalSine
{
    public float _lambda;
    public int _numerator;
    public ushort _denominator;  // must be > 0
    public float _mu;

    public float Get(float t)  // t is measured in RADIANS
    {
        return _lambda * Mathf.Sin((_numerator * t) / _denominator + _mu);
    }

    public void Set(float lambda, int numerator, ushort denominator, float mu)
    {
        _lambda = lambda;
        _numerator = numerator;
        _denominator = denominator;
        _mu = mu;
    }

    public RationalSine(float lambda, int numerator, ushort denominator, float mu)
    {
        _lambda = lambda;
        _numerator = numerator;
        _denominator = denominator;
        _mu = mu;
    }
}

[System.Serializable]
public struct RationalCosine
{
    public float _lambda;
    public int _numerator;
    public ushort _denominator;  // must be > 0
    public float _mu;

    public float Get(float t)  // t is measured in RADIANS
    {
        return _lambda * Mathf.Cos((_numerator * t) / _denominator + _mu);
    }

    public void Set(float lambda, int numerator, ushort denominator, float mu)
    {
        _lambda = lambda;
        _numerator = numerator;
        _denominator = denominator;
        _mu = mu;
    }

    public RationalCosine(float lambda, int numerator, ushort denominator, float mu)
    {
        _lambda = lambda;
        _numerator = numerator;
        _denominator = denominator;
        _mu = mu;
    }
}

[RequireComponent(typeof(Rigidbody2D))]
public class EnemyCyclicSmoothPathFollow : MonoBehaviour
{

    public string NoteOnOrientation = "Please ensure that sprites are initially \"facing\" up (ie towards the Y-Axis). This script assumes that that is their forwards-direction.";

    [Tooltip("This is NOT the speed, which in general is not easy with this kind of motion. Must be non-zero (e.g. 1 or -3.7).")]
    public float speedModifier = 1;
    
    public Vector2 translator = Vector2.zero;

    [Tooltip("Choose numerator/denominator coprime (and den > 0).")]
    [SerializeField] RationalSine[] rationalSinesX = null;
    [Tooltip("Ensure that lambda != 0.")]
    [SerializeField] RationalCosine[] rationalCosinesX = null;
    [Tooltip("Ensure that lambda != 0.")]
    [SerializeField] RationalSine[] rationalSinesY = null;
    [Tooltip("Choose numerator/denominator coprime (and den > 0).")]
    [SerializeField] RationalCosine[] rationalCosinesY = null;

    Rigidbody2D rb;


    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }


    void Update()
    {
        float scaled_time = Time.time * speedModifier;
        float trig_sumX = 0;
        float trig_sumY = 0;

        for (int i = 0; i < rationalSinesX.Length; i++)
        {
            trig_sumX   +=  rationalSinesX[i].Get(scaled_time);
        }


        for (int i = 0; i < rationalCosinesX.Length; i++)
        {
            trig_sumX   +=  rationalCosinesX[i].Get(scaled_time);
        }


        for (int i = 0; i < rationalSinesY.Length; i++)
        {
            trig_sumY   +=  rationalSinesY[i].Get(scaled_time);
        }


        for (int i = 0; i < rationalCosinesY.Length; i++)
        {
            trig_sumY   +=  rationalCosinesY[i].Get(scaled_time);
        }

        Vector2 vel = translator + new Vector2(trig_sumX, trig_sumY) - rb.position;

        // make the ship face "up" (which is my "forward" in this 2D world)
        transform.up = vel; // .normalised is not needed :) the method seems to normalize itself automatically! :D

        rb.position += vel;
    }
}
2 Likes

Privacy & Terms