var Enemy = pc.createScript('enemy');
// === ATTRIBUTES ===
Enemy.attributes.add('viewDistance', { type: 'number', default: 20 });
Enemy.attributes.add('searchDistance', { type: 'number', default: 10 });
Enemy.attributes.add('attackDistance', { type: 'number', default: 3 });
Enemy.attributes.add('stopDistance', { type: 'number', default: 2 });
Enemy.attributes.add('moveSpeed', { type: 'number', default: 2 });
Enemy.attributes.add('rotateSpeed', { type: 'number', default: 3 });
Enemy.attributes.add('maxHealth', { type: 'number', default: 100 });
Enemy.attributes.add('groundOffset', { type: 'number', default: 0.3, title: 'Vertical Offset (prevent sinking)' });
// Facing fix (for models facing wrong direction)
Enemy.attributes.add('facingDirection', {
type: 'number',
enum: [
{ 'Forward (+Z)': 0 },
{ 'Backward (-Z)': 180 },
{ 'Right (+X)': -90 },
{ 'Left (-X)': 90 }
],
default: 0,
title: 'Facing Direction'
});
// === INIT ===
Enemy.prototype.initialize = function () {
this.enemy = this.entity;
this.enemyState = 'Search';
this.targets = this.app.root.findByTag('Target');
this.target = null;
this.currentHealth = this.maxHealth;
this.isMoving = false;
this.isAttacking = false;
this.isAlive = true;
this.timer = 0;
// Lock rigidbody rotation on X/Z but allow Y
if (this.enemy.rigidbody) {
this.enemy.rigidbody.angularFactor = new pc.Vec3(0, 1, 0);
}
// Raise slightly above terrain
const pos = this.enemy.getPosition();
this.enemy.setPosition(pos.x, pos.y + this.groundOffset, pos.z);
// Ensure upright posture (X=90)
const euler = this.enemy.getEulerAngles();
this.enemy.setEulerAngles(90, euler.y, 0);
// Collision detection
if (this.enemy.collision) {
this.enemy.collision.on('collisionstart', this.onCollisionStart, this);
}
};
// === UPDATE ===
Enemy.prototype.update = function (dt) {
if (!this.isAlive) return;
this.findTarget();
this.updateState(dt);
this.updateAnimation();
// Maintain upright rotation
const euler = this.enemy.getEulerAngles();
this.enemy.setEulerAngles(90, euler.y, 0);
};
// === FIND TARGET ===
Enemy.prototype.findTarget = function () {
const myPos = this.enemy.getPosition();
let closest = null;
let closestDist = Infinity;
for (let i = 0; i < this.targets.length; i++) {
const t = this.targets[i];
const dist = myPos.distance(t.getPosition());
if (dist < closestDist && dist < this.viewDistance) {
closest = t;
closestDist = dist;
}
}
if (closest) {
this.target = closest;
this.distanceToTarget = closestDist;
} else {
this.target = null;
}
};
// === STATE MACHINE ===
Enemy.prototype.updateState = function (dt) {
switch (this.enemyState) {
case 'Search': this.search(dt); break;
case 'Chase': this.chase(dt); break;
case 'Attack': this.attack(dt); break;
}
};
// === SEARCH ===
Enemy.prototype.search = function (dt) {
this.timer -= dt;
if (this.timer <= 0) {
this.timer = pc.math.random(2, 4);
this.setRandomDestination();
}
this.moveTowards(this.randomDest, dt, this.moveSpeed);
if (this.target) {
this.enemyState = 'Chase';
}
};
// === CHASE ===
Enemy.prototype.chase = function (dt) {
if (!this.target) {
this.enemyState = 'Search';
return;
}
const dist = this.moveTowards(this.target.getPosition(), dt, this.moveSpeed * 1.5);
if (dist < this.attackDistance) {
this.enemyState = 'Attack';
}
};
// === ATTACK ===
Enemy.prototype.attack = function (dt) {
if (!this.target) {
this.enemyState = 'Search';
return;
}
const dist = this.enemy.getPosition().distance(this.target.getPosition());
if (dist > this.attackDistance + 0.5) {
this.enemyState = 'Chase';
return;
}
this.faceTarget(dt);
if (!this.isAttacking) {
this.isAttacking = true;
const attackAnim = 'Attack' + pc.math.randomInt(1, 3);
this.setAnimation(attackAnim);
setTimeout(() => {
this.isAttacking = false;
}, 1000);
}
};
// === MOVE TOWARD FUNCTION ===
Enemy.prototype.moveTowards = function (targetPos, dt, speed) {
if (!targetPos) return 0;
const pos = this.enemy.getPosition();
const dir = targetPos.clone().sub(pos);
dir.y = 0;
const dist = dir.length();
if (dist < 0.05) return dist;
dir.normalize();
this.faceTarget(dt, dir);
// Move with safe physics teleport
const move = dir.clone().scale(speed * dt);
const newPos = pos.clone().add(move);
if (this.enemy.rigidbody) {
this.enemy.rigidbody.teleport(newPos, this.enemy.getRotation());
} else {
this.enemy.setPosition(newPos);
}
this.isMoving = true;
return dist;
};
// === FACE TARGET ===
Enemy.prototype.faceTarget = function (dt, dir) {
if (!dir && this.target) {
dir = this.target.getPosition().clone().sub(this.enemy.getPosition());
dir.y = 0;
dir.normalize();
}
if (!dir) return;
const targetAngle = Math.atan2(dir.x, dir.z) * pc.math.RAD_TO_DEG + this.facingDirection;
const euler = this.enemy.getEulerAngles();
const smoothY = pc.math.lerpAngle(euler.y, targetAngle, dt * this.rotateSpeed);
this.enemy.setEulerAngles(90, smoothY, 0); // Keep upright X=90
};
// === RANDOM DESTINATION ===
Enemy.prototype.setRandomDestination = function () {
const pos = this.enemy.getPosition();
const range = this.searchDistance || 10;
const x = pc.math.random(pos.x - range, pos.x + range);
const z = pc.math.random(pos.z - range, pos.z + range);
this.randomDest = new pc.Vec3(x, pos.y, z);
};
// === ANIMATION HANDLER ===
Enemy.prototype.setAnimation = function (name) {
if (this.enemy.anim && this.enemy.anim.baseLayer) {
const layer = this.enemy.anim.baseLayer;
if (layer.activeState !== name) layer.transition(name, 0.2);
}
};
Enemy.prototype.updateAnimation = function () {
if (!this.isAlive) return;
if (this.isAttacking) return;
if (this.isMoving) this.setAnimation('Walk');
else this.setAnimation('Idle');
this.isMoving = false;
};
// === DAMAGE / DEATH ===
Enemy.prototype.takeDamage = function (amount) {
if (!this.isAlive) return;
this.currentHealth -= amount;
const hitAnim = 'Hit' + pc.math.randomInt(1, 2);
this.setAnimation(hitAnim);
if (this.currentHealth <= 0) this.die();
};
Enemy.prototype.die = function () {
this.isAlive = false;
const deathAnim = 'Death' + pc.math.randomInt(1, 2);
this.setAnimation(deathAnim);
setTimeout(() => this.enemy.destroy(), 3000);
};
// === COLLISION ===
Enemy.prototype.onCollisionStart = function (result) {
if (result.other.tags.has('bullet')) {
this.takeDamage(25);
result.other.destroy();
}
};
// This is the fix i tried but i still cant get it to follow the player let alone attack the player could you help me out
Since PlayCanvas uses the negative Z axis as forward, you need to rotate the model entity 180 degrees relative to its parent.
With the script i sent you just click whether your editor has a -Z balance and i fixed it but the problem comes in at chasing the player
So the enemy moves correctly until it has a target? If that’s the case, you might need to check your faceTarget function.
May I ask why you changed the default script so much?
Because in the new editor version the script you wrote brought up allot of errors so with every error i had to apply a new fix and i also needed it to work with my game structure
But thanks because it worked as a powerful foundation
That’s strange. I will check this and update the project to the new editor version. Thanks!
No problem at all but i actually would like to ask you a few more questions (Im new to this engine just came in last month on 23 ) so could you please try to explain to me where the error might be in my code because i dont see any errors
If you share the editor link of your project, I can take a look later today.
Ok sure
Alright, I just switched my Basic Enemy AI to the latest editor version, and there are no errors. Everything seems to works fine.
I checked your project and found a few rotation issues in your character setup. After fixing those, my Basic Enemy AI worked as expected, at least from what I’ve seen so far.
-
Make sure the X-axis of the parent entity is set to 0° (instead of 90°).

-
Make sure the X-axis of the model entity is set to -90° and the Y-axis to 180°.

Thanks ill make sure i try that
The player doesn’t have a Target tag here. If you want the enemy to chase the player, you need to add a Target tag to the player entity.
For those who haven’t seen the latest version in action yet, here is a short video showing the enemies chasing the mouse and getting killed by it too. I think it’s pretty cool! ![]()
Wow thanks allot you saved me buddy
Im currently fixing the attack logic (After i added the target tag this popped up (( [Enemy%20script.js?id=261340969&branchId=82dd648b-2650-46c6-af48-c8c3227c1a5b:152]: pc.math.randomInt is not a function TypeError: pc.math.randomInt is not a function at Enemy.attack (https://launch.playcanvas.com/api/assets/files/Scripts/Enemy%20script.js?id=261340969&branchId=82dd648b-2650-46c6-af48-c8c3227c1a5b:152:47) at scriptType.updateState (https://launch.playcanvas.com/api/assets/files/Scripts/Enemy%20script.js?id=261340969&branchId=82dd648b-2650-46c6-af48-c8c3227c1a5b:102:29) at scriptType.update (https://launch.playcanvas.com/api/assets/files/Scripts/Enemy%20script.js?id=261340969&branchId=82dd648b-2650-46c6-af48-c8c3227c1a5b:66:10) at ScriptComponent._scriptMethod (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:109859:28) at ScriptComponent._onUpdate (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:109895:23) at ScriptComponentSystem._callComponentMethod (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:110680:58) at ScriptComponentSystem._onUpdate (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:110696:15) at ComponentSystemRegistry.fire (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:1732:27) at AppBase.update (https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:71961:23) at https://code.playcanvas.com/playcanvas-2.12.4.dbg.js:72920:26) The)) im currently working on it but thanks for the update
When I tried the project you shared, I first got a lot of animation errors that prevented the game from running. They come from animation-controller.js.
But i dont have a script that is called that
Maybe it’s from PlayCanvas itself.
You added the code below to the attack state:
// FIXED RANDOM ANIMATION CHOICE
const attackIndex = Math.floor(pc.math.random(1, 3)); // replaces randomInt
const attackAnim = 'Attack' + attackIndex;
this.setAnimation(attackAnim);
Do all the possible animations exist?
Also, since the attack state runs every frame, I guess you don’t want a new animation triggered every frame?

