Collision for my shield in my game doesn’t work sometimes

Hi there to whoever is reading this! I am new here, so please forgive me if I don’t follow the proper customs on this forum. Currently at the moment, I am trying to make a game in vanilla JavaScript. I am currently adding the collision to my shield, but I am having some trouble. Sometimes, the collision doesn’t work, and other times, it does. I don’t have any clue as to why this happens, but I think it could be that I am using both AABB collision and circle collision. Any help is really appreciated! I will put a couple of my files below as to not overwhelm anyone, but if you want any more information, I will happily provide it! Also know that this project is still in the works, which is why I have not really gone back and added enough comments or finished implementing some features. Please take a look, and thanks in advance!

game.js:

import { canvas, ctx, canvWidth, canvHeight } from './gameCanv.js';
import Ship from './ship.js';
import Bullet from './bullet.js';
import nextLvl from './nextLvl.js';
import Shield from './shield.js';
import collision from './collision.js';
import gameOver from './gameOver.js';
import destroy from './destroyAnim.js';

// Creates ship, arrays for bullets, and mouse
let lives = 3;
let level = 1;
let ship = new Ship();
let shield = new Shield(ship.x, ship.y, ship.radius, ship.angle)
const bulletArr = [];
let enemyArr = [];
let squareBullArr = [];
let pentBullArr = [];
let octBullArr = [];
let particleArr = [];

// Keeps track of mouse position
const mouse = {
    x: undefined,
    y: undefined
};

// Listens for any keypresses
const keyPresses = [];
document.body.addEventListener('keydown', e => {
    keyPresses[e.key] = true;
});

// Listens for the release of a button (and start updating shield)
document.body.addEventListener('keyup', e => {
    if (keyPresses[' ']) {
        shield.update(ship.x, ship.y, ship.angle, true);
    }
    keyPresses[e.key] = false;
});

// Updates Mouse Position
document.body.addEventListener('mousemove', e => {
    mouse.x = e.clientX;
    mouse.y = e.clientY;
});

// Fires bullet when left mouse button is clicked
document.body.addEventListener('click', e => {
    bulletArr.push(new Bullet(ship.noseX, ship.noseY, ship.angle, 5, 'orange', 5));
});

// Expires bullet after a certain time
function bulletExpired(arr, expDate) {
    loop1:
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].time >= expDate) {
            arr.splice(i, 1);
            break loop1;
        } 
    }
    return arr;
}

// Renders the game 
function gameLoop() {
    // Clears screen
    ctx.fillStyle = "black"
    ctx.fillRect(0, 0, canvWidth, canvHeight);    

    if (ship.visible === true) {
        // Updates position of ship and draws it
        ship.update(mouse.x, mouse.y, keyPresses);
        ship.draw();

        shield.update(ship.x, ship.y, ship.angle, false);
        shield.draw();
        
        // Generate enemies when there are none left
        if (enemyArr.length === 0) {
            enemyArr = (nextLvl(enemyArr, ship, level)).slice(0);
            level++;
        }
        
        // Updates player's bullet
        if (bulletArr.length !== 0) {
            for (let i = 0; i < bulletArr.length; i++) {
                bulletArr[i].update();
                bulletArr[i].draw();
            }
            bulletExpired(bulletArr, 125);
        }

        // Updates square enemy bullets
        if (squareBullArr.length !== 0) {
            for (let j = 0; j < squareBullArr.length; j++) {
                squareBullArr[j].update();
                squareBullArr[j].draw();
            }
            bulletExpired(squareBullArr, 250);
        }

        // Updates pentagon enemy bullets
        if (pentBullArr.length !== 0) {
            for (let k = 0; k < pentBullArr.length; k++) {
                pentBullArr[k].update();
                pentBullArr[k].draw();
            }
            bulletExpired(pentBullArr, 2000);
        }

        // Updates octagon enemy bullets
        if (octBullArr.length !== 0) {
            for (let l = 0; l < octBullArr.length; l++) {
                octBullArr[l].update();
                octBullArr[l].draw();
            }
            bulletExpired(octBullArr, 800);
        }

        // Updates each enemy position and redraws each one
        loop3:
        for (let p = 0; p < enemyArr.length; p++) {

            // Updates enemy position
            enemyArr[p].update(ship.x, ship.y);
            enemyArr[p].draw();

            // Handles enemy firing
            if (enemyArr[p].name === "square") 
                enemyArr[p].fire(squareBullArr);
            else if (enemyArr[p].name === "pentagon")
                enemyArr[p].fire(pentBullArr);
            else if (enemyArr[p].name === "octagon")
                enemyArr[p].fire(octBullArr);

            // Tests for collision between the enemy and the shield and ship
            if (collision(shield.cx, shield.cy, shield.radius, shield.name, enemyArr[p].x, enemyArr[p].y, enemyArr[p].radius, enemyArr[p].name, shield.angle)) {
                particleArr = destroy(enemyArr[p].x, enemyArr[p].y, 5, particleArr);
                enemyArr.splice(p, 1);
                break loop3;
            } else if (collision(ship.x, ship.y, ship.radius, ship.name, enemyArr[p].x, enemyArr[p].y, enemyArr[p].radius, enemyArr[p].name, shield.angle)) {
                particleArr = destroy(ship.x, ship.y, 50, particleArr);
                lives--;

                if (lives === 0) 
                    ship.visible = false;

                ship.x = canvWidth / 2;
                ship.y = canvHeight / 2;
            } 
        }
    }
    
    // Updates explosion particles if there are any
    if (particleArr.length !== 0) {
        loop2:
        for (let k = 0; k < particleArr.length; k++) {
            particleArr[k].update();

            if (Math.round(particleArr[k].velX) === 0 && Math.round(particleArr[k].velY) === 0) {
                particleArr.splice(k, 1);
                break loop2;
            }

            particleArr[k].draw();
        }
    }

    // Keeps game going as long as live count isn't zero and the explosion particles haven't finished animating
    if (lives !== 0 || particleArr.length !== 0)
        requestAnimationFrame(gameLoop);
}
gameLoop();

collision.js:

export default function collision(oneX, oneY, oneRad, oneName, twoX, twoY, twoRad, twoName, shieldAng) {
     if (oneName === 'shield') { // check if there is a collision with the shield

        // One vertex of shield
        const vertOne = {
            x: oneX + Math.cos(shieldAng + Math.PI/2) * oneRad,
            y: oneY - Math.sin(shieldAng + Math.PI/2) * oneRad
        };

        // Other vertex of shield
        const vertTwo = {
            x: oneX + Math.cos(shieldAng - Math.PI/2) * oneRad,
            y: oneY - Math.sin(shieldAng - Math.PI/2) * oneRad
        }

        const shieldMidY = oneY - Math.sin(shieldAng) * oneRad;

        // Rectangle around shield
        const rectOne =  {
            width: Math.abs(vertTwo.x - vertOne.x),
            height: Math.abs(vertTwo.y - shieldMidY),
            x: vertOne.x < vertTwo.x ? vertOne.x : vertTwo.x,
            y: vertOne.y < shieldMidY ? vertOne.y : shieldMidY 
        };

        // Checks for the type of enemy and finds the top left x and y point
        let rectTwo;
        if (twoName === 'square' || twoName === 'bullet') {
            rectTwo = {
                x: Math.abs(twoX),
                y: Math.abs(twoY),
                width: 2 * twoRad,
                height: 2 * twoRad
            };

        } else {
            rectTwo = {
                x: Math.abs(twoX - twoRad),
                y: Math.abs(twoY - twoRad),
                width: 2 * twoRad,
                height: 2 * twoRad
            };
        }
        
        // Checks for collision using AABB 
        if (rectOne.x < rectTwo.x + rectTwo.width && rectOne.x + rectOne.width > rectTwo.x && rectOne.y < rectTwo.y + rectTwo.height && rectOne.y + rectOne.height > rectTwo.y) 
            return true;

    } else { // Checks for collision of ship
        let distance = Math.sqrt(Math.pow((oneX - twoX), 2) + Math.pow((oneY - twoY), 2));

        // If there is a collision
        if (distance < oneRad + twoRad) 
            return true;
    }

    // If it reaches this, there is no collision
    return false;
}

Hey @yash !

I’m definitely interested in taking a look at this.

Although, it would probably be best if I had access to the entire project. Do you have a GitHub link with instructions on how to start the game? In addition, can you tell me the steps needed to reproduce the problem?

With that information it would be easier to help out.

Michael

3 Likes

Thanks for the reply Michael! I have never really used GitHub before, so I apologize if I made some mistakes getting it set up. I tried to get a repository up, and I think it should work. I am not sure which link is the correct one, so I will just use both.

https://github.com/Josh60169/Shape-Fight.git

As for how the game is started, I am not totally sure what you are asking me, so I will try my best to answer it. I currently am using Visual Studio Code, and have the Live Server extension installed. Then I usually right click the HTML file on the left column and click “Open Live Server”, and then it works. Though you are eons more experienced than I am, so you almost certainly won’t need that last bit. And finally, to move the ship you use the mouse pointer to point in the direction you want to move (works best with a mouse) and then press W to move. You left click to fire, and press space to move the shield. I find that the bug occurs most often in the dead center of the shield, so try to get one of the enemies to run into that part.

I hope this helps you out, and thanks for the reply once again! :grinning:

2 Likes

Excellent! I’ll take a look at it now.

Okay, you have a lot going on here, and the way this is currently setup it’s very hard to test. Is this based on a specific exercise? If so, can you pass the link?

One scenario that seemed to fail consistently was when an enemy hit the center of the shield after I rotated it 90 degrees clockwise. I was able to capture a collision with an octagon in this scenario. Here is the data:

It looks like AABB is failing in this scenario because of how the rectangles are calculated. Since you’re closest to the math, are you sure that your code is correctly calculating the size of the collision rectangles correctly in this scenario?

Here is a scenario in which the shield failed to stop the enemy. Keep in mind this isn’t the same scenario as the data above. Although the two were similar.
image

Another problem could be your precision in your calculation. For example you might be comparing two numbers that are basically the same, but because of your precision JS doesn’t see it that way. For example:

416.1234567 < 416.1234568
/*
Notice that these two numbers are off by 0.0000001
 Returns true, but for your project you might 
want this to return false. Since they are practically equal.  This might open a can of worms that may or may not be affecting your project. It's hard to say. */

Let me know what you think.

By the way I love your work here. It’s very impressive.

One final thought. This problem is maybe symptomatic of a bigger problem in your code. Which is that as you add more features, it can be really hard to test it. This is the down side of “vanilla” projects. You have to make everything from scratch. I’m not sure how much more of the project you have left, but if you find you’re spending too much time debugging then maybe you need to implement some tests into your code.

Let me know what you think.

Michael

1 Like

Update: I now know it is the math that is wrong because the yellow below is the hitbox (I just drew the rectangles)

image

Ah! I had a feeling it was that.

Yeah, so I have been trying to fix it for a while, and I am getting slightly better results, but it still is really far off from the end result that is needed. Do you mind taking a look at this new approach here? I am stumped as to how to exactly fix the remaining problems. I made sure to leave the temporary bounds so you can see what the bounds exactly look like.

The code below is the new collision.js:

import {canvas, ctx} from './gameCanv.js';
export default function collision(oneX, oneY, oneRad, oneName, twoX, twoY, twoRad, twoName, shieldAng) {
     if (oneName === 'shield') { // check if there is a collision with the shield

        // One vertex of shield
        const vertOne = {
            x: oneX + Math.cos(shieldAng + Math.PI/2) * oneRad,
            y: oneY - Math.sin(shieldAng + Math.PI/2) * oneRad  
        };

        // Other vertex of shield
        const vertTwo = {
            x: oneX + Math.cos(shieldAng - Math.PI/2) * oneRad, 
            y: oneY - Math.sin(shieldAng - Math.PI/2) * oneRad 
        }

        const shieldMidY = oneY - Math.sin(shieldAng) * oneRad; 
        const shieldMidX = oneX + Math.cos(shieldAng) * oneRad;

        const rectOne = {
            get x() {
                // If the shield is facing more up and down
                if (Math.round(Math.sin(shieldAng)) === -1 || Math.round(Math.sin(shieldAng)) === 1) 
                    return vertOne.x < vertTwo.x ? vertOne.x : vertTwo.x;
                else // If the shield is facing more left or right
                    return vertOne.x < shieldMidX ? vertOne.x : shieldMidX;   
            },

            get y() {
                // If the shield is facing more up and down
                if (Math.round(Math.sin(shieldAng)) === -1 || Math.round(Math.sin(shieldAng)) === 1) 
                    return vertOne.y < shieldMidY ? vertOne.y : shieldMidY;
                else // If the shield is facing more left or right
                    return vertOne.y < vertTwo.y ? vertOne.y : vertTwo.y;
            },

            get width() {
                // If the shield is facing more up and down
                if (Math.round(Math.sin(shieldAng)) === -1 || Math.round(Math.sin(shieldAng)) === 1) 
                    return Math.abs(vertTwo.x - vertOne.x);
                else // If the shield is facing more left or right
                    return Math.abs(vertTwo.x - shieldMidX);
            },

            get height() {
                // If the shield is facing more up and down
                if (Math.round(Math.sin(shieldAng)) === -1 || Math.round(Math.sin(shieldAng)) === 1) 
                    return Math.abs(vertOne.y - shieldMidY);
                else // If the shield is facing more left or right
                    return Math.abs(vertOne.y - vertTwo.y);
            }
        };

        // Checks for the type of enemy and finds the top left x and y point
        let rectTwo;
        if (twoName === 'square' || twoName === 'bullet') {
            rectTwo = {
                x: twoX,
                y: twoY,
                width: 2 * twoRad,
                height: 2 * twoRad
            };

        } else {
            rectTwo = {
                x: twoX - twoRad,
                y: twoY - twoRad,
                width: 2 * twoRad,
                height: 2 * twoRad
            };
        }

        // This is here to draw out the bounds of the collision, the top left of the collision bounds, the vertices of the shield, and the midpoint of the shield
        ctx.strokeStyle = 'yellow';
        ctx.strokeRect(rectOne.x, rectOne.y, rectOne.width, rectOne.height);
        ctx.strokeRect(rectOne.x - 10, rectOne.y - 10, 20, 20);
        ctx.strokeRect(vertTwo.x - 10, vertTwo.y - 10, 20, 20);
        ctx.strokeRect(vertOne.x - 10, vertOne.y - 10, 20, 20)
        ctx.strokeRect(shieldMidX - 10, shieldMidY - 10, 20, 20);

        // Checks for collision using AABB 
        if (rectOne.x < rectTwo.x + rectTwo.width && rectOne.x + rectOne.width > rectTwo.x && rectOne.y < rectTwo.y + rectTwo.height && rectOne.y + rectOne.height > rectTwo.y) 
            return true;

    } else { // Checks for collision of ship
        let distance = Math.sqrt(Math.pow((oneX - twoX), 2) + Math.pow((oneY - twoY), 2));

        // If there is a collision
        if (distance < oneRad + twoRad) 
            return true;
    }

    // If it reaches this, there is no collision
    return false;
}

Sure I can take a look. Let’s try and keep these changes in the repo though.

I would do the following. Create a GH issue, and then make a branch dedicated to fixing that issue. Because now you are introducing code to your game that you don’t want in your final project, and adding or removing this code can be problematic. Basically a snowball effect.

I’m not sure what IDE you use, but the GH extension on VS Code can really help you do this.

Also, do you mind passing whatever resources you are using to help guide you through this project? This will help me understand that math and what is expected.

Michael

2 Likes

I am not completely sure what you mean, but I installed GitHub Codespaces and made a commit to update the code on GitHub. Let me know if there was a misunderstanding please. Sorry for any issues you faced.

Basically I’m suggesting a more conservative way of modifying your code. Particularly because you are now trying to debug it and you don’t have any tests. This means your changes pose a risk of regressing certain aspects of your code.

When in this situation ,developers typically create an issue on Git Hub, within the repository, and then attach a dedicated branch of their code base to solving that issue. This isolates your code from the code you’re introducing to fix the bug. This will allow you to manually test the application before merging your branch into your main code base.

I’ve gone ahead and made an issue on the repo.

3 Likes

So you’re code seems to handle it better, but it looks like it’s failing at 45 degrees and at 275 degrees

This is looks like it’s more of a trigonometry problem than a coding problem at this point. It’s been a while since I’ve dabbled in that.

I wonder if this khan academy excercise might be helpful.

ship at 45 degress
ship at 275

1 Like

I am sorry, but I am not exactly sure about what you are hinting at (if you even are). Should I be using save states and rotation to rotate the shield to the certain position and then calculate the box?

I think the problem isn’t your code, but possibly your math. I don’t really understand the math you are doing in your code. This is why I’ve asked a few times if you can share whatever resources you are using :slight_smile:

Do you mind sharing the resources you used to decide your strategy?

Michael

2 Likes

I realized I probably did not use any resources, which is why I am confused on what your asking for. I just ended coming up with the idea. My thought process is that if you take the sin of the angle, it will be at maximum the absolute value of 1 because of the definition of a unit circle. However, if it is 0 or closer to 0, it means it is closer to PI or 0 (in radians) because that is is when the sin of a given angle is 0. PI and 0 (in radians once again) is on the left and right sides while PI/2 and 3*PI/2 are 1 and negative 1 (absolute value of 1), which means it would be facing more up and down. So, what I am doing is using that logic to check for the direction, and using the corresponding vertices. However, what I think is going wrong is something to do with the logic afterwards. That is just me though, and I am quite new trigonometry since I am going into sophomore year of high school after this summer break, so I could very well be wrong.

Hmmm, okay.

So you might need to do more research on your approach. I think this is more of a math problem than a coding problem. I say that because you can tell from my pictures that the height of the shield becomes zero at 45 degrees and at 275 degrees.

Looking more at your getter for height. It looks like the height of the hit box is zero at these angles because vertOne.y equals shieldMidy or vertTwo.y (depending on the direction of the shield).

2 Likes

I am very sorry. It has been a while since I replied. I want to let you know I need to use this thing called Separated Axis Theorem, and have been spending a lot of time learning the math for it. Currently though my issue is smaller. I need to find an equation for the top left vertex of the rectangle from vertex one. I have been trying to find out what to do and have this code. However it fails to work properly.

const rectOne = {
    x: vertOne.x + 2 * oneRad * Math.cos(shieldAng),
    y: vertOne.y - oneRad * Math.sin(shieldAng),
    width: 2 * oneRad,
    height: oneRad
};

I am currently typing this on my phone, but I will soon commit the changes on my computer on GitHub so you can see all the changes. Once again, I apologize for the silence.