# 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' }
}

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?

1 Like