AI scripting for NPCs or BOTs

I had to “solve” it using a really really really bad way, but I cannot implement a better one:

// calculate the angle to rotate to
var origin = this.entity.getPosition()
var destination = this.target.getPosition()
var vector = this.target.getPosition().clone().sub(this.entity.getPosition());
var angle = this.entity.forward.angle(vector.normalize())
// set angle
this.direction.set(0, angle, 0)
// rotate to angle
this.entity.rigidbody.enabled = false
this.entity.rotate(this.direction)
this.entity.rigidbody.enabled = true

I use a little library from Useful pc.Quat functions to help me calculate the angles, which worked like a charm (and IMHO should be inserted into the core protos to help newbies like me understand how quaternions/vectors work altogether).

It works as expected, but due to the unexpected behaviours to use rotate on rigidbodies, I want to find the proper way. Next logical thing on the list is applyTorque with the desired vector to look at, but due to that being progressive and not instant, i don’t know how to manage the required time to finish my rotation (even more, what If my target moves during this torque?). Or even applyTorqueInstant, but then, how I stop the torque impulse when I reach the desired angle?

I wish torquing a rigidbody could be as easy as rotate a non rigidbody…

Anyway, if someone needs it or finds it interesting, here is a complete script of an AI that:

  • If no target
    • Looks for the target
    • Waits some random time
    • Rotates some random time
    • Walks some random time
  • Else
    • Calculates angle from NPC to target
    • Rotates to target
    • Runs to target some time
    • If closes to target
      • Attacks the target
    • Else
      • Looks for the target
/* jshint esversion: 6 */
/* jshint asi: true */
/* jshint expr: true */
/* jslint vars: true */

// script
var IA = pc.createScript('IA')

// states of the npc
IA.states = {
    idle: { animation: 'idle.json' },
    walk: { animation: 'walk.json' },
    run: { animation: 'run.json' },
    right: { animation: 'right.json' },
    left: { animation: 'left.json' },
    attack: { animation: 'attack.json' }
}

IA.attributes.add('target', {
    type: 'entity',
})

// reset the variables
IA.prototype.reset = function(dt) {
    // flags
    this.wandering = false
    this.waiting = false
    this.walking = false
    this.rotating = false
    this.attacking = false
    this.chasing = false
    // timers
    this.waitingTime = this.random(2, 4) * 1000
    this.rotatingTime = this.random(2, 4) * 500
    this.walkingTime = this.random(2, 4) * 1000
    this.runningTime = 2000
    this.attackingTime = 1000
    // speeds
    this.rotateSpeed = 70
    this.walkSpeed = 360
    this.runMultiplier = 3
    // blending
    this.blending = 0.2
    // raycasting
    this.result = null
    this.direction = new pc.Vec3()
    this.range = 7
    this.melee = 1.5
    // initial state of the app
    this.state = null
    this.waiting = true
    this.wandering = true
    this.animate('idle')
    this.waitingStartTime = Date.now()
}

// initialize the variables
IA.prototype.initialize = function() {
    // states
    this.reset()
}

// update on each frame
IA.prototype.update = function(dt) {
    if (this.wandering) {
        if (this.target) this.search(dt)
        if (this.waiting) {
            this.wait(dt)
        }
        if (this.rotating) {
            this.rotate(dt)
        }
        if (this.walking) {
            this.walk(dt)
        }
    } else {
        if (this.chasing) {
            this.chase(dt)
        }
        if (this.attacking) {
            this.attack(dt)
        }
    }
}

// idle the npc standing
IA.prototype.wait = function(dt) {
    if (this.state !== 'idle') {
        console.log('waiting...')
        this.animate('idle')
        this.entity.rigidbody.linearVelocity = pc.Vec3.ZERO
        this.entity.rigidbody.angularVelocity = pc.Vec3.ZERO
    }
    if ((Date.now() - this.waitingStartTime) > this.waitingTime) {
        this.waiting = false
        this.waitingTime = this.random(2, 4) * 1000
        this.rotatingStartTime = Date.now()
        this.rotating = true
    }
}

// rotate the npc around
IA.prototype.rotate = function(dt) {
    if (this.state !== 'right' && this.state !== 'left') {
        console.log('rotating...')
        this.animate(Math.random() >= 0.5 ? 'right' : 'left')
    }
    this.entity.rigidbody.linearVelocity = pc.Vec3.ZERO
    this.entity.rigidbody.applyTorque(pc.Vec3.UP.clone().scale(this.rotateSpeed * (this.state === 'right' ? -1 : 1)))
    if ((Date.now() - this.rotatingStartTime) > this.rotatingTime) {
        this.rotating = false
        this.rotatingTime = this.random(2, 4) * 500
        this.walkingStartTime = Date.now()
        this.walking = true
    }   
}

// walk the npc forward
IA.prototype.walk = function(dt) {
    if (this.state !== 'walk') {
        console.log('walking...')
        this.animate('walk')
    }
    this.entity.rigidbody.angularVelocity = pc.Vec3.ZERO
    this.entity.rigidbody.applyForce(this.entity.forward.clone().scale(this.walkSpeed))
    if ((Date.now() - this.walkingStartTime) > this.walkingTime) {
        this.walking = false
        this.walkingTime = this.random(2, 4) * 1000
        this.waitingStartTime = Date.now()
        this.waiting = true
    }
}

// look for the target
IA.prototype.search = function(dt) {
    console.log('searching...')
    // raycast the target
    this.result = this.app.systems.rigidbody.raycastFirst(this.entity.getPosition(), this.target.getPosition())
    if (this.result !== null) {
        if (this.result.entity.name.toLowerCase().includes('player')) {
            if (this.result.entity.getPosition().sub(this.entity.getPosition()).length() <= this.range) {
                console.log('found...!')
                this.wandering = false
                this.chasing = true
            }
        }
    }
}

// chase the target
IA.prototype.chase = function(dt) {
    // calculate the angle to run to
    var origin = this.entity.getPosition()
    var destination = this.target.getPosition()
    var vector = this.target.getPosition().clone().sub(this.entity.getPosition());
    var angle = this.entity.forward.angle(vector.normalize())
    this.direction.set(0, angle, 0)
    if (this.state !== 'run') {
        console.log('chasing...')
        // TODO find a better way
        this.entity.rigidbody.enabled = false
        this.entity.rotate(this.direction)
        this.entity.rigidbody.enabled = true
        this.animate('run')
        this.runningStartTime = Date.now()
    }
    // run
    this.entity.rigidbody.applyForce(this.entity.forward.scale(this.walkSpeed * this.runMultiplier))
    // attack if get close
    if (this.target.getPosition().clone().sub(this.entity.getPosition()).length() <= this.melee) {
        console.log('reaching...')
        this.chasing = false
        this.attacking = true
        this.attackingStartTime = Date.now()
    }
    // reset if runs out of time
    if ((Date.now() - this.runningStartTime) > this.runningTime && !this.attacking) {
        console.log('refacing...')
        this.entity.rigidbody.linearVelocity = pc.Vec3.ZERO
        this.entity.rigidbody.angularVelocity = pc.Vec3.ZERO
        this.animate('idle')
    }
}

// attack the target
IA.prototype.attack = function(dt) {
    var origin = this.entity.getPosition()
    var destination = this.target.getPosition()
    var vector = this.target.getPosition().clone().sub(this.entity.getPosition());
    var angle = this.entity.forward.angle(vector.normalize())
    if (this.state !== 'attack') {
        console.log('attacking...')
        this.entity.rigidbody.linearVelocity = pc.Vec3.ZERO
        this.entity.rigidbody.angularVelocity = pc.Vec3.ZERO
        this.animate('attack')
    }
    if (this.target.getPosition().clone().sub(this.entity.getPosition()).length() > this.melee || Math.abs(angle) >= 20 ) {
        if ((Date.now() - this.attackingStartTime) > this.attackingTime) {
            console.log('refacing...')
            this.wandering = true
            this.attacking = false
        }
    }
}

// animate the npc
IA.prototype.animate = function(state) {
    var states = IA.states
    this.state = state
    this.entity.children[0].animation.play(states[state].animation, this.blending)
}

// get random integer
IA.prototype.random = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min
}

It needs the auxiliar library found in Useful pc.Quat functions

Without disabling the rigidbody you can’t do .setRotation()?

I have done .setRotation() on rigidbodies before… Or is that not what you wanted to achieve?

You could always apply low amounts of torque and then dampen it, like mentioned in yaustar’s original answer… I mean; did you look into doing that?

Well, I was told .rotate() should not be used with dynamic rigidbodies due to glitches in the physics system. I just assumed the same for .setRotation(). I’m gonna try this right away!

Did this work for you?

2 Likes