1
\$\begingroup\$

I've been working on a custom enemy "AI", the way it works is it generates a point inside a colliders bounds, then moves the enemy towards the point. Once the enemy reaches it. The point is generated in a random different place. Kinda giving the effect that it's flying around randomly.

The only posts and information I could find was removing wobbling or stuttering. I thought maybe I could use some Perlin Noise and then add it as like an offset. But I couldn't figure out a way to do it well. So does anyone know how I can add an offset using Perlin noise? The solution cant just add a random offset otherwise it just jitters around, it has to be a smooth wobble. That's the part that I'm finding difficult.

Here is my code (with some unnecessary parts cutout):

[Header("Enemy Settings")]
public EnemyType enemyType;
public float speed;
public float maxDistance;

[Header("Flight Settings")]
public BoxCollider2D flightBounds;
public Transform flightBoundsPosition;
public bool stationaryWhenIdle;

[Header("Attack Settings")]
public EnemyAttackType attackType;
public EnemyAgression enemyAgression;
public float playerDetectionRadius;
public LayerMask playerLayer;

public float playerFollowSpeed;
public float chargeForce;
public float chargeDuration;
public float chargeCooldown;

// internals
private Rigidbody2D rb;
private Bounds boxBounds;
bool atPoint = false;
Vector2 point;
int dir;

void Update() {
    if (enemyType == EnemyType.Walking) {
        FlipEnemy();
        MoveEnemy(EnemyType.Walking);
        AlignEnemy();
    }
    if (enemyType == EnemyType.Flying) {
        FlipEnemy();
        MoveEnemy(EnemyType.Flying);
        AlignEnemy();
    }
}

private void MoveEnemy(EnemyType type) {
    if (type == EnemyType.Flying) {
        // flight
        if (!stationaryWhenIdle) {
            point = GeneratePoint(false);
            float step = speed * Time.deltaTime;
            transform.position = Vector2.MoveTowards(transform.position, point, step);
        }

        // attack
        if (enemyAgression == EnemyAgression.Passive) return;
        Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(transform.position, playerDetectionRadius, playerLayer);
        foreach (Collider2D col in collider2Ds) {
            if (attackType == EnemyAttackType.Follow) {
                float step = playerFollowSpeed * Time.deltaTime;
                transform.position = Vector2.MoveTowards(transform.position, col.transform.position, step);
                break;
            }
        }
    }
    else {
        rb.velocity = new Vector2(speed * dir, rb.velocity.y);
    }
}

private Vector2 GeneratePoint(bool isStart) {
    if (Vector2.Distance(transform.position, point) < 0.2f) atPoint = true;
    else atPoint = false;
    if (atPoint || isStart) {
        Vector2 position = new Vector2(
            Random.Range(boxBounds.min.x, boxBounds.max.x),
            Random.Range(boxBounds.min.y, boxBounds.max.y)
        );
        return position;
    }
    else return point;
}
\$\endgroup\$
1
  • \$\begingroup\$ Perlin noise worked for me, but if you know better ways that look better, I would appreciate any answers. \$\endgroup\$
    – Pow
    Jan 18 at 13:08

3 Answers 3

2
\$\begingroup\$

When I want to make an object "wobble" in a way that looks natural and fluent, then my go-to solution is to use sine waves. Using different frequencies for different axis, and probably layering a couple different curves with different amplitudes and frequencies might not actually be random but it looks pretty chaotic to the player.

Here is an example script that uses two separate sine waves with separate frequency and amplitude, one for the x axis and one for the y axis.

It does that by setting the localPosition. The Idea is that you parent the visual object with this script to an invisible parent. The parent has the position where the object is supposed to be, while the Wobble-script makes the visible object wobble around that position.

using UnityEngine;

public class Wobble : MonoBehaviour {

    public float xFrequency = 4.1f;
    public float xAmplitude = 1.0f;
    public float yFrequency = 4.3f;
    public float yAmplitude = 1.0f;

    void Update()
    {
        transform.localPosition = new Vector3(
            Mathf.Sin(Time.time * xFrequency) * xAmplitude,
            Mathf.Sin(Time.time * yFrequency) * yAmplitude,
            0f
        );
    }
}

Result:

Wobbling Sphere

If you want the wobble to look more random, then you can layer some more sine waves with different frequencies and amplitudes. Here is a script that allows you to define as many waves for the x and y axis as you want:

using System;
using UnityEngine;

public class Wobble : MonoBehaviour {

    [Serializable]
    public struct SineWave {
        public float frequency;
        public float amplitude;
        public float Resolve() {
            return Mathf.Sin(Time.time * frequency) * amplitude;
        }
    }

    public SineWave[] xWaves;
    public SineWave[] yWaves;

    void Update()
    {
        float x = 0f;
        foreach (var xWave in xWaves) {
            x += xWave.Resolve();
        }
        float y = 0f;
        foreach (var yWave in yWaves) {
            y += yWave.Resolve();
        }

        transform.localPosition = new Vector3(x, y, 0f);
    }
}

Here is a result with 3 different waves per axis:

Wobble Sphere 2

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Interesting, it looks pretty good, but its not super random, that's why I've been using noise. \$\endgroup\$
    – Pow
    Jan 18 at 20:22
  • 1
    \$\begingroup\$ @Pow If you want it to be more random, add a couple more sine waves with different frequencies and amplitudes. With 2 or 3, most people won't be able to see a pattern anymore. \$\endgroup\$
    – Philipp
    Jan 18 at 22:03
  • 1
    \$\begingroup\$ Hmm yeah, definitely looks more random, ill give it a go. \$\endgroup\$
    – Pow
    Jan 18 at 22:44
2
\$\begingroup\$

Performant Random Noise

Perlin Noise can be a rather slow calculation. If you need to apply to many objects it would be prohibitively slow.

You can use a pseudo random number (PRN) generator to sample a random sequence seeding the sequence at regular interval/locations to smooth and repeat the result.

PRNs are fast and repeatable.

This can give you deterministic yet random looking behaviours. The PRN below appears very similar to perlin noise (pink noise) but does not have the fixed frequency spread of perlin (Example PRN is white noise ignoring the sample frequence)

Example of PRN generator

Very simple random number generator. For humans it looks very random, for machines not so much.

const double MAX = 2576436549074795; 
const double INV_MAX = 1.0 / MAX;
double RandUnit(double seed) { // return unit value 0.0 <= RandUnit(seed) < 1.0
    seed = Math.floor(seed );
    return (((8765432352450986.0 * seed) + 8507698654323524.0) % MAX) * INV_MAX; 
}

Example of noise function using above PRN

// One dimension time based noise
// time in seconds, 
// freq in changes per second
// returns unit double 0.0 <= Noise() < 1.0
double Noise(double time, double freq) { 
    const double randLow = RandUnit(Math.floor(time * freq));
    const double randHi  = RandUnit(Math.floor(time * freq) + 1.0);       
    return (randHi - randLow) * ((time * freq) % 1) + randLow;
}

The above noise function would replace the perlin noise function.

The noise and random functions return unit values that are easily scaled and used to lerp values you wish to add noise to.

Example

Below is a runnable example written in JavaScript. It is a example proof of concept for you. Not too hard to convert to C#.

It has two core functions that you will need.

  • RandFloat(seed) which returns a random unit value (0 to 1) based on the seed value.

    NOTE that the example is using doubles 64bit forced to integer. Scale the magic numbers to fit floats if using 32bit values

  • Noise(time, freq, useEase) which uses RandFloat to return a unit noise value based on time and desired frequency.

    NOTE The bool useEase if true will add a in out ease on the unit value (Big dots in example have smoothed (eased) noise)

The example create a set of moving objects each with a position and velocity.

Each object is also given a noise frequency (in seconds) and a noise amplitude (scale in pixels)

Noise is added perpendicular to velocity.

The objects move via a global delta time, and the noise position is based on time as with your example code. Each object is shadowed by a lighter circle showing the position it would be without the noise applied.

requestAnimationFrame(MainLoop);
const OBJECT_DESITY = 0.001;   // Objects per canvas pixels squared
const NOISE_SCALE_MIN = 10;    // In pixels
const NOISE_SCALE_MAX = 23;    // In pixels
const NOISE_FREQ_MIN = 1;      // per second
const NOISE_FREQ_MAX = 4;      // per second
const MIN_SPEED = 5;           // Object speed in pixels per second
const MAX_SPEED = 10;          // Object speed in pixels per second
const objects = [];
const INV_MS_PER_SEC = 1/1000;
var time;
const ctx = canvas.getContext("2d");

const eCurve   = (v, p = 2, vp) =>  v < 0 ? 0 : v > 1 ? 1 : (vp = v ** p) / (vp + (1 - v) ** p);
const RanRan = (min, max) => Math.random() * (max - min) + min;
const V2 = (x = 0, y = 0) => ({x, y});
const InvLength = (v2) => 1 / (v2.x * v2.x + v2.y * v2.y) ** 0.5;
const Normalize = (v2, res = V2()) => {
  const invLen = InvLength(v2);
  res.x = v2.x * invLen;
  res.y = v2.y * invLen;
  return res;
}


const RandFloat = (() => {
    var seed = 1;
    const MAX = 2576436549074795; 
    const INV_MAX = 1 / MAX;
    return  (s = seed) => { 
        seed = Math.floor(s);
        return (seed = ((8765432352450986 * seed) + 8507698654323524) % MAX) * INV_MAX; 
    }

})();
function Noise(time, freq, useEase) {
    const t = time * freq;
    const tLow = Math.floor(t);
    const tHi = tLow + 1;
    const tFrac = t % 1;
    const randLow = RandFloat(tLow);
    const randHi  = RandFloat(tHi);       
    return (randHi - randLow) * (useEase ? eCurve(tFrac) : tFrac) + randLow;
}


const CreateObj = () => {
    const dir = RanRan(0, Math.PI * 2);
    const speed = RanRan(MIN_SPEED, MAX_SPEED);
    const id = objects.length;
    const radius = id % 2 ? 4 : 3; // smooth random larger objects
    
    return {
        pos: V2(RanRan(0, ctx.canvas.width), RanRan(0, ctx.canvas.height)),
        nPos: V2(),
        vel: V2(Math.cos(dir) * speed, Math.sin(dir) * speed),
        noiseFreq: RanRan(NOISE_FREQ_MIN, NOISE_FREQ_MAX),
        noiseScale: RanRan(NOISE_SCALE_MIN, NOISE_SCALE_MAX),
        Update(deltaTime) {
            this.pos.x += this.vel.x * deltaTime;
            this.pos.y += this.vel.y * deltaTime;
            const perp = Normalize(this.vel);
            const noise = Noise(time * INV_MS_PER_SEC, this.noiseFreq, id % 2) - 0.5;
            this.nPos.x = this.pos.x + -perp.y * noise * this.noiseScale;
            this.nPos.y = this.pos.y +  perp.x * noise * this.noiseScale;
        },
        Draw() {

            const x = ((this.pos.x + 4) % (canvas.width + 8)) - 4;
            const y = ((this.pos.y + 4) % (canvas.height + 8)) - 4;
            ctx.moveTo(x + radius, y);
            ctx.arc(x, y, radius, 0, Math.PI * 2);
        },
        DrawRnd() {
            const x = ((this.nPos.x + 4) % (canvas.width + 8)) - 4;
            const y = ((this.nPos.y + 4) % (canvas.height + 8)) - 4;
            ctx.moveTo(x + radius, y);
            ctx.arc(x, y, radius, 0, Math.PI * 2);
        },        
    };
}
function Resize() {
    canvas.width = innerWidth;
    canvas.height = innerHeight;
    objects.length = 0;
    var i = Math.floor(Math.min(5000, OBJECT_DESITY * canvas.width * canvas.height));
    info.innerText = "Object count: " + i;
    while (i-- > 0) { objects.push(CreateObj()); }
}


function MainLoop(timeNow) {
    if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
        Resize();
    }
    if (time == undefined) {
        time = timeNow;
    }
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    const deltaTime = (timeNow - time) * INV_MS_PER_SEC;
    for (const obj of objects) { obj.Update(deltaTime); }
    
    ctx.fillStyle = "#0002";
    ctx.beginPath();
    for (const obj of objects) { obj.Draw(); }
    ctx.fill();    
    
    ctx.fillStyle = "#0009";
    ctx.beginPath();
    for (const obj of objects) { obj.DrawRnd(); }
    ctx.fill();
    time = timeNow;
    requestAnimationFrame(MainLoop);


}
canvas {
    position: absolute;
    top: 0px;
    left: 0px;
}
div {
    position: absolute;
    top: 20px;
    left: 20px;
    color: red;
    background: white;
    font-family: arial black;
}
<canvas id="canvas" width="1" height="1"></canvas>
<div id="info"></div>

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the answer, currently the noise isn't really having a huge impact, but I will definitely take a deeper look into it if there are any performance related issues. \$\endgroup\$
    – Pow
    Jan 18 at 17:02
0
\$\begingroup\$

I figured it out, You can just generate some Perlin noise using the time like this:

float noise = Mathf.PerlinNoise(Time.time, Time.time);

Then apply it to the moving objects y axis:

transform.position += new Vector3(0, noise, 0)

The only downside is there's no way to change the noise scale, it works fine anyway though.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ The only downside is there's no way to change the noise scale If I understand you correctly, you can also multiply noise by a number to modify its scale. \$\endgroup\$ Jan 18 at 10:01
  • 1
    \$\begingroup\$ amplitude * Mathf.PerlinNoise(frequency * Time.time, 0.5f) will let you scale both the amplitude of the noise (how far it wanders away from zero) and the frequency of the noise (how quickly it wiggles back and forth, from slow gentle sways to high-frequency jitters). Scanning along y=0.5 helps reduce some regularity, because Perlin noise always crosses zero at every integer lattice point. \$\endgroup\$
    – DMGregory
    Jan 18 at 11:31
  • \$\begingroup\$ Thanks a lot, I already multiplied the amplitude within the movement transform.position part (I didnt add it to the code in this post) but the frequency and scanning should help. \$\endgroup\$
    – Pow
    Jan 18 at 13:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .