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