Problem with bullet

I have created a shooting game with a cannon turret, in space 0 gravity, and most of the time the bullet are shoot adequately, but some get stocked at the begining of the canons. I’ve tried to reduce bullet collision box to a minimum, increase cooldown time to 300ms, change bullet shape and collision shape (actually capsule) and it works well for a while then the bullets pile up at the entrance in all cases
Capture d’écran 2025-04-17 à 10.13.02

Hi @magicdidier and welcome!

Do you use a kinematic or dynamic rigidbody for your bullet and how do you move the bullet?

Could it be the bullets are interacting with some gun or player parts?

Do you remove old bullets or are they still in the scene?

I’m also not sure what you mean with 0 gravity.

Hi Albertos, thks for helping, I use dynamic rigidbody, the gun has no collision box, nor rigid body and the bullet spawn ahead of the gun, I checked several positions, even far away from the gun. Has the action takes place in space, I,ve set the Amnos to gravity 0, and the bullet are move by applytorque. I attach the complete script of the player (shipmove.js) and the part supposed to destroy the enemy and the bullet is not working, the bullets are just pushing away the enemy spacecraft. The player is a capsule to which I have attached a cannon turret template. Both have no collision nor rigidbody, they are not intended to be shot at

var ShipMove = pc.createScript('ShipMove');

ShipMove.attributes.add('turnspeed', {type: 'number'});
ShipMove.attributes.add('Bulletspeed', {type: 'number'});
ShipMove.attributes.add('bullet', {type: 'entity'});

// Limites en degrés
ShipMove.prototype.minAnglex = 0;
ShipMove.prototype.maxAnglex = 60;
ShipMove.prototype.minAngley = -60;
ShipMove.prototype.maxAngley = 60;
ShipMove.prototype.BulletMaxDistance = 500;

ShipMove.prototype.initialize = function() 
{
    this.isKeyPressed = false;
};

ShipMove.prototype.update = function(dt) 
{
    var isCurrentlyPressed = this.app.keyboard.isPressed(pc.KEY_LEFT) || 
                            this.app.keyboard.isPressed(pc.KEY_RIGHT) || 
                            this.app.keyboard.isPressed(pc.KEY_UP) || 
                            this.app.keyboard.isPressed(pc.KEY_DOWN);
    
    var euler = this.entity.getLocalEulerAngles();
    var changed = false;

    if (this.entity.sound) 
    {
        if (isCurrentlyPressed && !this.isKeyPressed) 
        {
            this.entity.sound.play('rotate');
        }
        else if (!isCurrentlyPressed && this.isKeyPressed)
        {
            this.entity.sound.stop('rotate');
        }
    }

    this.isKeyPressed = isCurrentlyPressed;

    if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) 
    {
        euler.y -= this.turnspeed * dt;
        changed = true;
    }
    if (this.app.keyboard.isPressed(pc.KEY_LEFT)) 
    {
        euler.y += this.turnspeed * dt;
        changed = true; 
    }
    if (this.app.keyboard.isPressed(pc.KEY_UP)) 
    {
        euler.x -= this.turnspeed * dt;
        changed = true;
    }    
    if (this.app.keyboard.isPressed(pc.KEY_DOWN)) 
    {
        euler.x += this.turnspeed * dt;
        changed = true;   
    }

    euler.x = pc.math.clamp(euler.x, this.minAnglex, this.maxAnglex);
    euler.y = pc.math.clamp(euler.y, this.minAngley, this.maxAngley);

    if (changed) 
    {
        this.entity.setLocalEulerAngles(euler.x, euler.y, euler.z);
    }

    // CORRECTION 3 : Ajout d'un délai de tir (optionnel)
    if (this.app.mouse.isPressed(pc.MOUSEBUTTON_LEFT)) 
    {
        if (!this.fireCooldown || this.fireCooldown <= 0) 
        {
            this.spawnBullets();
            this.fireCooldown = 0.1; 
        }
    }
    
    if (this.fireCooldown) 
    {
        this.fireCooldown -= dt;
    }
    this.checkBulletDestruction(dt);
};

// CORRECTION 4 : Extraction de la logique de création de balles
ShipMove.prototype.spawnBullets = function() 
{
    for (let i = 0; i < 3; i++) 
    {
        const newBullet = this.bullet.clone();
        newBullet.enabled = true;
        
        // Positionnement relatif
        const offsetX = [-0.23, 0.01, 0.25][i];
        newBullet.setPosition(offsetX, 0, -1.4);
        
        // Physique
        if (newBullet.rigidbody) 
        {
            newBullet.rigidbody.applyForce(this.entity.forward.clone().scale(this.Bulletspeed));
        }
        
        this.entity.addChild(newBullet);      
    }
};

// Nouvelle méthode pour vérifier les balles
ShipMove.prototype.checkBulletDestruction = function(dt) 
{
    // Récupération de toutes les entités enfants
    const Bullets = this.entity.children;
    for (let i = Bullets.length - 1; i >= 0; i--) 
    {
        const Bullet = Bullets[i];

        // Vérifie si l'entité est une balle
        if (Bullet.name === 'Bullet') 
        {
            // Vérifie la distance par rapport au point de départ
            if (Bullet.getPosition().length() > this.BulletMaxDistance) 
            {
                Bullet.destroy(); // Détruire la balle si la distance est dépassée
            }
        }
        
        // Vérification de la collision avec "enemi négatif"
        if (Bullet.name === 'Bullet' && Bullet.enabled) 
        {
            const enemy = this.app.root.findByName('enemi negatif');
            if (enemy && enemy.collision && Bullet.collision) 
            {
                if (Bullet.collision.aabb.intersects(enemy.collision.aabb)) 
                {
                    Bullet.destroy(); // Détruire la balle
                    enemy.destroy(); // Détruire l'ennemi
                }
            }
        }
    }
};

here is a link to the project : PlayCanvas 3D HTML5 Game Engine

I see some possible issues in the script.

If the bullet entity has an dynamic rigidbody, you cannot use setPosition(), setEulerAngles() and similar methods, because it only change the entity and not the rigidbody. For example change newBullet.setPosition(offsetX, 0, -1.4); to newBullet.rigidbody.teleport(offsetX, 0, -1.4);.

It seems like you parent the bullet entity to the ship entity. I guess the rigidbody of the bullet entity will not be updated when you change for example the position and rotation of the ship. Maybe you could parent the bullet entity to the root entity instead?

I think you nailed the trouble, but I am now unable to find the right coordinate to fire them at the top of my guns… I, ve been searching for 1 hour, no idea how the iterative system works… In the mean time I have added destruction of the enemi and explosion effect. The collision between the bullet and the space craft works well on the red one, but sometimes is not recognize by the blue one…
https://playcanvas.com/project/1330704/overview/jeu-des-relatifs-212

If I understand you correctly, you can use something like below.

var position = this.app.root.findByName('Gun').getPosition();

You can also create an attribute and apply the correct entity to get a reference to the entity (and position).

Make sure you apply the position after adding the bullet to the scene with reparent() or addChild().

Here are the changes I made :

ShipMove.prototype.spawnBullets = function() 
{
    for (let i = 0; i < 3; i++) 
    {
        const newBullet = this.bullet.clone();
        newBullet.enabled = true;
        const offsetX = [-0.23, 0.01, 0.25][i];
        newBullet.rigidbody.teleport(offsetX, 1, -2)
        newBullet.rigidbody.applyForce(this.entity.forward.clone().scale(this.Bulletspeed));
        this.app.root.addChild(newBullet);
    }

now the bullet is not following the turret correctly
https://playcanvas.com/project/1330704/overview/jeu-des-relatifs-212

You first need to add the new entity to their parent and then set the position using teleport(). Setting the position on something without a parent doesn’t work as expected.

Also if you apply a force only one time (so not in the update function) you maybe want to use applyImpulse() instead.

By the way, it looks like you are setting the local position but you need to set the world position. That means you want to use the position of for example the gun entity, like I mentioned in a post before.

I wanted to limitate the movement of the turret, so I am not using apply torque, but euler angle to rotate the player.

ShipMove.prototype.spawnBullets = function() 
{
    for (let i = 0; i < 3; i++) 
    {
        const newBullet = this.bullet.clone();
        newBullet.enabled = true;
        const offsetX = [-0.23, 0.01, 0.25][i];
        this.entity.addChild(newBullet);
        newBullet.rigidbody.teleport(offsetX, 1, -2)
        newBullet.rigidbody.applyForce(this.entity.forward.clone().scale(this.Bulletspeed));
    }

I made these chages, if i child to the root, the enemy doesn’t explode any longer but with this changes, the bullets are not following the turret properly
I am getting frustrated, I am to newbye to javascritp, and this project looks more and more unfeasable to me, I still have to make those enemy spawn , and even change label for other negative and positive number.
I am a math teacher trying to make a game to learn kids addition and substraction of relative numbers…

I have changed camera to see exactly what’s going on, the bullets are spawning at the same place, but their shooting direction changes as the turret is turning.

Do you use the forward direction of the turret?

I just checked your project and it works as I expected. I think it’s just an optical illusion that makes it seem like the bullets change direction when the turret turns.

I have corrected all the problem using AI, but I think the resulting script is a lot bigger than necessary, what do you think about it :slight_smile:

var ShipMove = pc.createScript('ShipMove');

ShipMove.attributes.add('turnspeed', {type: 'number'});
ShipMove.attributes.add('Bulletspeed', {type: 'number'});
ShipMove.attributes.add('bullet', {type: 'entity'});
ShipMove.attributes.add('boum', {type: 'entity'});
// Nouvelle propriété pour la distance maximale des balles
ShipMove.attributes.add('bulletMaxDistance', {
    type: 'number',
    default: 50,
    title: 'Distance Max des Balles'
});

// Limites en degrés
ShipMove.prototype.minAnglex = 0;
ShipMove.prototype.maxAnglex = 60;
ShipMove.prototype.minAngley = -60;
ShipMove.prototype.maxAngley = 60;

ShipMove.prototype.initialize = function() 
{
    this.isKeyPressed = false;
    
    // Vérification de la présence des ennemis au démarrage
    this.enemies = this.findEnemies();
    console.log("Ennemis trouvés au démarrage:", this.enemies.length);
    
    // Garder une référence aux balles actives
    this.activeBullets = [];
    
    // Map pour suivre les positions d'origine des balles
    this.bulletOrigins = new Map();
};

ShipMove.prototype.findEnemies = function() {
    // Récupérer tous les ennemis par leur nom
    var enemies = [];
    var enemiNegatif = this.app.root.findByName('enemi');
    
    if (enemiNegatif) {
        if (Array.isArray(enemiNegatif)) {
            enemies = enemies.concat(enemiNegatif);
        } else {
            enemies.push(enemiNegatif);
        }
    }
    
    console.log("Ennemis trouvés:", enemies);
    return enemies;
};

ShipMove.prototype.update = function(dt) 
{
    var isCurrentlyPressed = this.app.keyboard.isPressed(pc.KEY_LEFT) || 
                            this.app.keyboard.isPressed(pc.KEY_RIGHT) || 
                            this.app.keyboard.isPressed(pc.KEY_UP) || 
                            this.app.keyboard.isPressed(pc.KEY_DOWN);
    
    var euler = this.entity.getLocalEulerAngles();
    var changed = false;

    if (this.entity.sound) 
    {
        if (isCurrentlyPressed && !this.isKeyPressed) 
        {
            this.entity.sound.play('rotate');
        }
        else if (!isCurrentlyPressed && this.isKeyPressed)
        {
            this.entity.sound.stop('rotate');
        }
    }

    this.isKeyPressed = isCurrentlyPressed;

    if (this.app.keyboard.isPressed(pc.KEY_RIGHT)) 
    {
        euler.y -= this.turnspeed * dt;
        changed = true;
    }
    if (this.app.keyboard.isPressed(pc.KEY_LEFT)) 
    {
        euler.y += this.turnspeed * dt;
        changed = true; 
    }
    if (this.app.keyboard.isPressed(pc.KEY_UP)) 
    {
        euler.x -= this.turnspeed * dt;
        changed = true;
    }    
    if (this.app.keyboard.isPressed(pc.KEY_DOWN)) 
    {
        euler.x += this.turnspeed * dt;
        changed = true;   
    }

    euler.x = pc.math.clamp(euler.x, this.minAnglex, this.maxAnglex);
    euler.y = pc.math.clamp(euler.y, this.minAngley, this.maxAngley);

    if (changed) 
    {
        this.entity.setLocalEulerAngles(euler.x, euler.y, euler.z);
    }

    // Gestion du tir avec délai de recharge
    if (this.app.mouse.isPressed(pc.MOUSEBUTTON_LEFT)) 
    {
        if (!this.fireCooldown || this.fireCooldown <= 0) 
        {
            this.spawnBullets();
            this.fireCooldown = 0.1; 
        }
    }
    
    if (this.fireCooldown) 
    {
        this.fireCooldown -= dt;
    }
    
    // Rechercher à nouveau des ennemis si la liste est vide
    if (this.enemies.length === 0) {
        this.enemies = this.findEnemies();
    }
    
    this.checkBulletDestruction(dt);
};

ShipMove.prototype.spawnExplosion = function(position)
{
    const newExplosion = this.boum.clone();
    newExplosion.enabled = true;
    
    // Positionnement à l'endroit de la collision
    if (position) {
        if (newExplosion.rigidbody) {
            newExplosion.rigidbody.teleport(position.x, position.y, position.z);
        } else {
            newExplosion.setPosition(position);
        }
    }
    
    // Ajouter l'explosion à la scène directement
    this.app.root.addChild(newExplosion);
    
    // Déclencher l'explosion
    if (newExplosion.script && newExplosion.script.explode) {
        console.log("Déclenchement de l'explosion");
        newExplosion.script.explode.explode();
    } else {
        console.error("Script d'explosion non trouvé ou méthode explode manquante");
        // Vérification des scripts disponibles
        console.log("Scripts disponibles:", newExplosion.script);
    }
};

ShipMove.prototype.spawnBullets = function() 
{
    // Positions de départ des balles relatives à la tourelle
    const offsets = [
        new pc.Vec3(-0.25, -0.2, -1.75),
        new pc.Vec3(0.01, -0.2, -1.75),
        new pc.Vec3(0.27, -0.2, -1.75)
    ];
    
    for (let i = 0; i < 3; i++) 
    {
        const newBullet = this.bullet.clone();
        newBullet.enabled = true;
        newBullet.name = 'Bullet';  // Assurez-vous que le nom est correct
        
        // Ajouter au monde plutôt qu'à la tourelle pour éviter le mouvement relatif
        this.app.root.addChild(newBullet);
        
        // Calculer la position mondiale basée sur la position/rotation de la tourelle
        const worldPosition = this.entity.getPosition().clone();
        const worldRotation = this.entity.getRotation().clone();
        
        // Transformer le vecteur d'offset par la rotation de la tourelle
        const transformedOffset = worldRotation.transformVector(offsets[i].clone());
        
        // Calculer la position finale de la balle
        const finalPosition = worldPosition.add(transformedOffset);
        
        // Positionnement et orientation
        if (newBullet.rigidbody) {
            // Téléporter le rigidbody à la position mondiale calculée
            newBullet.rigidbody.teleport(finalPosition.x, finalPosition.y, finalPosition.z);
            
            // Appliquer la rotation de la tourelle à la balle
            newBullet.setRotation(worldRotation);
            
            // Appliquer une force immédiatement avec une valeur plus élevée
            const forceDirection = this.entity.forward.clone().scale(this.Bulletspeed * 3);
            newBullet.rigidbody.applyImpulse(forceDirection);
            
            // Alternative: définir directement une vélocité linéaire
            // Décommenter la ligne suivante et commenter la ligne précédente pour essayer
            // newBullet.rigidbody.linearVelocity = this.entity.forward.clone().scale(this.Bulletspeed);
            
            // Rendre le rigidbody kinematic pour éviter qu'il ne s'arrête
            // Décommenter si vous avez des problèmes de balles qui s'arrêtent
            // newBullet.rigidbody.type = pc.BODYTYPE_KINEMATIC;
        } else {
            // Pour les entités sans rigidbody
            newBullet.setPosition(finalPosition);
            newBullet.setRotation(worldRotation);
        }
        
        // Stocker la position d'origine de la balle pour calculer la distance parcourue
        this.bulletOrigins.set(newBullet.getGuid(), finalPosition.clone());
        
        // Ajouter la balle à notre liste de suivi
        this.activeBullets.push(newBullet);
    }
};

ShipMove.prototype.checkBulletDestruction = function(dt) 
{
    // Nettoyer les balles qui n'existent plus
    this.activeBullets = this.activeBullets.filter(bullet => {
        if (!bullet || !bullet.enabled) {
            this.bulletOrigins.delete(bullet.getGuid());
            return false;
        }
        return true;
    });
    
    // Ajouter des balles supplémentaires qui pourraient être dans la scène
    const sceneBullets = this.app.root.findByName('Bullet');
    if (sceneBullets) {
        if (Array.isArray(sceneBullets)) {
            sceneBullets.forEach(bullet => {
                if (bullet && !this.activeBullets.includes(bullet)) {
                    this.activeBullets.push(bullet);
                    // Si c'est une nouvelle balle sans origine enregistrée, enregistrer sa position actuelle
                    if (!this.bulletOrigins.has(bullet.getGuid())) {
                        this.bulletOrigins.set(bullet.getGuid(), bullet.getPosition().clone());
                    }
                }
            });
        } else if (!this.activeBullets.includes(sceneBullets)) {
            this.activeBullets.push(sceneBullets);
            // Si c'est une nouvelle balle sans origine enregistrée, enregistrer sa position actuelle
            if (!this.bulletOrigins.has(sceneBullets.getGuid())) {
                this.bulletOrigins.set(sceneBullets.getGuid(), sceneBullets.getPosition().clone());
            }
        }
    }
    
    // Parcourir chaque balle active
    for (let i = this.activeBullets.length - 1; i >= 0; i--) 
    {
        const bullet = this.activeBullets[i];
        
        if (!bullet || !bullet.enabled) {
            this.activeBullets.splice(i, 1);
            this.bulletOrigins.delete(bullet.getGuid());
            continue;
        }

        // Vérifier si la balle est immobile ou trop lente
        if (bullet.rigidbody) {
            const velocity = bullet.rigidbody.linearVelocity;
            const speed = velocity.length();
            
            // Si la vitesse est trop faible, donner une nouvelle impulsion ou détruire
            if (speed < 1) {
                console.log("Balle trop lente détectée, destruction");
                bullet.destroy();
                this.activeBullets.splice(i, 1);
                this.bulletOrigins.delete(bullet.getGuid());
                continue;
            }
        }
        
        // Vérifier la distance parcourue
        const bulletPos = bullet.getPosition();
        const originPos = this.bulletOrigins.get(bullet.getGuid());
        
        if (originPos) {
            const distanceTraveled = bulletPos.distance(originPos);
            
            if (distanceTraveled > this.bulletMaxDistance) {
                console.log(`Balle a parcouru ${distanceTraveled} unités, destruction`);
                bullet.destroy();
                this.activeBullets.splice(i, 1);
                this.bulletOrigins.delete(bullet.getGuid());
                continue;
            }
        }
        
        // Vérifier les collisions avec tous les ennemis disponibles
        let collisionDetected = false;
        
        for (let j = this.enemies.length - 1; j >= 0 && !collisionDetected; j--) {
            const enemy = this.enemies[j];
            
            if (!enemy || !enemy.enabled) {
                // Supprimer l'ennemi de la liste s'il n'existe plus
                this.enemies.splice(j, 1);
                continue;
            }
            
            // Méthode 1: Vérification par AABB (si disponible)
            if (bullet.collision && enemy.collision && 
                bullet.collision.aabb && enemy.collision.aabb && 
                bullet.collision.aabb.intersects(enemy.collision.aabb)) 
            {
                this.handleCollision(bullet, enemy);
                collisionDetected = true;
                this.activeBullets.splice(i, 1);
                this.bulletOrigins.delete(bullet.getGuid());
                break;
            }
            
            // Méthode 2: Vérification par distance (solution de secours)
            else {
                const bulletPos = bullet.getPosition();
                const enemyPos = enemy.getPosition();
                const distance = bulletPos.distance(enemyPos);
                
                // Ajuster cette valeur selon la taille de vos objets
                const collisionDistance = 2.0; 
                
                if (distance < collisionDistance) {
                    this.handleCollision(bullet, enemy);
                    collisionDetected = true;
                    this.activeBullets.splice(i, 1);
                    this.bulletOrigins.delete(bullet.getGuid());
                    break;
                }
            }
        }
    }
};

// Nouvelle fonction pour gérer la collision
ShipMove.prototype.handleCollision = function(bullet, enemy) {
    console.log("Collision détectée entre balle et ennemi");
    
    // Récupérer la position de l'ennemi pour l'explosion
    const enemyPosition = enemy.getPosition().clone();
    
    // Créer l'explosion
    this.spawnExplosion(enemyPosition);
    
    // Supprimer la balle
    bullet.destroy();
    
    // Supprimer l'ennemi de la liste avant de le détruire
    const index = this.enemies.indexOf(enemy);
    if (index > -1) {
        this.enemies.splice(index, 1);
    }
    
    // Détruire l'ennemi
    enemy.destroy();
};

I can’t read your comments because they’re not in English (or Dutch), but yes, the script seems a little bit too complex for this project so far. As long as the project runs fine, this is not a problem.

For example, instead of creating the offset by script you can use an empty entity on the correct position for the bullets. Tricks like this can make the script a bit less complex.

1 Like

Got you! In the future 'll translate the comment in english. for the moment, I will clean the script up, and remove all I am able to understand as useless. Thks for your great help.

1 Like