I am attempting to recreate a mobile app called ballz within javascript. This is my first project so I apologize if it hurts your head to read through it.
The ball is shot using the mouse to determine its velocity.
The ball should continually check for collision with a static block.
Depending on if the ball hits a corner or side of the block, the velocity is calculated using the code I provided.
The issue is that the collision response is all over the place. Sometimes the sideCollisionReponse works flawlessly, sometimes the cornerCollisionResponse works flawlessly. However, most of the time, the ball clips through blocks, reflects sporadically, or a combination of the two.
document.addEventListener('DOMContentLoaded', function() {
const canvas = document.querySelector('canvas');
const c = canvas.getContext("2d");
canvas.width = 360;
canvas.height = 600;
let isDragging = false;
let aimX = 0;
let aimY = 0;
function drawPoints(points) {
c.font = '24px Arial';
c.fillStyle = 'white';
c.textAlign = 'center';
c.fillText('Points: ' + points, canvas.width / 2, 30);
}
class Sprite {
constructor(position) {
this.position = position;
this.velocity = {
x: 0,
y: 0
};
this.isShooting = false;
this.isStuck = false;
this.points = 0;
this.radius = 10;
}
// May be able to optimize by defining the ball size into a variable for size specific calls
draw() {
c.beginPath();
c.arc(this.position.x, this.position.y, 10, 0, 2 * Math.PI);
c.fillStyle = "orange";
c.fill();
}
checkCollision() {
this.wallsCollisionResponse();
for (let i = 0; i < blocks.length; i++) {
let block = blocks[i];
const topLeftDist = Math.sqrt((this.position.x - block.x) ** 2 + (this.position.y - block.y) ** 2);
const topRightDist = Math.sqrt((this.position.x - block.x - block.width) ** 2 + (this.position.y - block.y) ** 2);
const bottomLeftDist = Math.sqrt((this.position.x - block.x) ** 2 + (this.position.y - block.y - block.height) ** 2);
const bottomRightDist = Math.sqrt((this.position.x - block.x - block.width) ** 2 + (this.position.y - block.y - block.height) ** 2);
const isCornerCollision = (topLeftDist <= this.radius || topRightDist <= this.radius || bottomLeftDist <= this.radius || bottomRightDist <= this.radius)
if (!isCornerCollision) {
this.sidesCollisionResponse(block);
} else if (isCornerCollision) {
this.cornerCollisionResponse(block);
}
}
};
wallsCollisionResponse() {
let x = this.position.x
let y = this.position.y
// Check collision with walls
if (x - this.radius <= 0 || x + this.radius >= canvas.width) {
this.velocity.x = -this.velocity.x; // Reverse horizontal velocity
this.position.x = Math.max(this.radius, Math.min(canvas.width - this.radius, x)); // Limit position within bounds
}
if (y - this.radius <= 40 || y + this.radius >= canvas.height) {
this.velocity.y = -this.velocity.y; // Reverse vertical velocity
this.position.y = Math.max(this.radius + 40, Math.min(canvas.height - this.radius, y)); // Limit position within bounds
}
// Check if ball hits bottom
if (y + this.radius >= canvas.height) {
this.isStuck = true;
this.velocity = {
x: 0,
y: 0
};
}
}
cornerCollisionResponse(block) {
// get line from ball center to corner
const v1x = this.position.x - block.x
const v1y = this.position.y - block.y;
// normalize the line and rotated 90deg to get tanget
const len = (v1x ** 2 + v1y ** 2) ** 0.5;
const tx = -v1y / len;
const ty = v1x / len;
const dot = (this.velocity.x * tx + this.velocity.y * ty) * 2;
this.velocity.x = -this.velocity.x + tx * dot;
this.velocity.y = -this.velocity.y + ty * dot;
block.decreaseValue();
}
sidesCollisionResponse(block) {
let x = this.position.x
let y = this.position.y
let maxSpeed = 20;
if (
block.x + block.width >= x - this.radius &&
block.x <= x + this.radius &&
block.y + block.height >= y - this.radius &&
block.y <= y + this.radius
) {
// Calculate collision normal (assuming block is axis-aligned)
let normalX = 0;
let normalY = 0;
if (x < block.x) {
normalX = -1; // Collision from left
} else if (x > block.x + block.width) {
normalX = 1; // Collision from right
}
if (y < block.y) {
normalY = -1; // Collision from top
} else if (y > block.y + block.height) {
normalY = 1; // Collision from bottom
}
console.log('Initial velocity1:', this.velocity);
// Reflect velocity using dot product with normal vector
let dotProduct = this.velocity.x * normalX + this.velocity.y * normalY;
this.velocity.x -= 2 * dotProduct * normalX;
this.velocity.y -= 2 * dotProduct * normalY;
console.log('Initial velocity2:', this.velocity);
// Limit velocity to maxSpeed
const currentSpeed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
if (currentSpeed > maxSpeed) {
const ratio = maxSpeed / currentSpeed;
this.velocity.x *= ratio;
this.velocity.y *= ratio;
}
this.limitVelocity();
// Adjust position to prevent clipping
const overlapX = Math.abs(x - block.x - block.width);
const overlapY = Math.abs(y - block.y - block.height);
const separationX = overlapX * normalX;
const separationY = overlapY * normalY;
//this.position.x += separationX;
//this.position.y += separationY;
block.decreaseValue();
}
}
limitVelocity() {
// Limit velocity to maxSpeed
const maxValue = 20;
if (this.velocity.x > maxValue) {
this.velocity.x = maxValue;
} else if (this.velocity.x < -maxValue) {
this.velocity.x = -maxValue
}
if (this.velocity.y > maxValue) {
this.velocity.y = maxValue;
} else if (this.velocity.y < -maxValue) {
this.velocity.y = -maxValue
}
}
update() {
if (this.isShooting && !this.isStuck) {
this.move();
this.checkCollision();
}
}
move() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Simulate gravity
// this.velocity.y += 0.2;
}
shoot(mouseX, mouseY) {
// Calculate direction vector from ball to mouse
const dx = mouseX - this.position.x;
const dy = mouseY - this.position.y;
const magnitude = Math.sqrt(dx * dx + dy * dy);
const maxSpeed = 20; // Maximum speed limit
if (magnitude !== 0) {
const normalizedDx = dx / magnitude;
const normalizedDy = dy / magnitude;
const speed = Math.min(magnitude, maxSpeed); // Limit speed to maximum speed
this.velocity.x = normalizedDx * speed;
this.velocity.y = normalizedDy * speed;
}
this.isShooting = true;
}
reset(position) {
this.position = position; // Maintains last position on reset
this.velocity = {
x: 0,
y: 0
};
this.isShooting = false; // this may be unneccessary
this.isStuck = false;
}
getPoints() {
this.points++;
return this.points;
}
}
const player = new Sprite({
x: 180,
y: 590
});
class Block {
constructor(x, y, width, height, value) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.value = value + 1;
}
draw() {
c.fillStyle = 'green';
c.fillRect(this.x, this.y, this.width, this.height);
// Text
c.fillStyle = 'white';
c.font = '12px Arial'; // Adjust the font size and style as needed
c.fillText(this.value.toString(), this.x + this.width / 2, this.y + this.height / 1.75);
}
decreaseValue() {
if (this.value > 0) {
this.value - 1;
if (this.value === 0) {
// Remove block if value reaches 0
blocks.splice(blocks.indexOf(this), 1);
}
}
}
shiftDown() {
this.y += this.height + 6
}
}
const blocks = [];
function createBlocks() {
const blockWidth = 40;
const blockHeight = 40;
const numBlocks = 1;
//Math.floor(canvas.width / blockWidth);
for (let i = 0; i < numBlocks; i++) {
const blockValue = player.points;
blocks.push(new Block(120, 80, blockWidth, blockHeight, blockValue));
}
}
createBlocks();
function animate() {
requestAnimationFrame(animate); // clears canvas before next frame
c.clearRect(0, 0, canvas.width, canvas.height);
c.fillStyle = '#4D4D4D'; // Top bar color
c.fillRect(0, 0, canvas.width, 40);
if (isDragging && !player.isShooting) {
const dx = aimX - player.position.x;
const dy = aimY - player.position.y;
const magnitude = Math.sqrt(dx * dx + dy * dy);
const maxSegments = 20; // Maximum number of segments
const totalTime = 1; // Total time for preview (adjust as needed)
for (let i = 0; i < maxSegments; i++) {
const progress = i / maxSegments; // Progress along the trajectory
const time = progress * totalTime; // Time at this segment
const previewX = player.position.x + dx * time;
const previewY = player.position.y + dy * time; // Vertical motion with gravity + 0.5 * 9.81 * time * time
c.beginPath();
c.arc(previewX, previewY, player.radius, 0, 2 * Math.PI);
const alpha = 1 - i * (0.5 / maxSegments); // Decrease transparency for each segment
c.fillStyle = `rgba(255, 165, 0, ${alpha})`; // Transparent orange
c.fill();
}
}
player.draw();
player.update();
drawPoints(player.points);
blocks.forEach((block, index) => {
block.draw();
});
if (player.isStuck) {
player.reset({
x: player.position.x,
y: player.position.y
})
player.getPoints();
blocks.forEach((block, index) => {
block.shiftDown();
})
createBlocks();
}
}
animate();
canvas.addEventListener("mousedown", (event) => {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
if (!player.isShooting && mouseX >= 0 && mouseX <= canvas.width && mouseY >= 0 && mouseY <= canvas.height - 20) {
isDragging = true;
aimX = mouseX;
aimY = mouseY;
}
})
canvas.addEventListener("mouseup", (event) => {
if (isDragging) {
isDragging = false;
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
if (mouseX >= 0 && mouseX <= canvas.width && mouseY >= 0 && mouseY <= canvas.height - 20) {
player.shoot(aimX, aimY); // Pass aim position to shoot method
}
}
})
canvas.addEventListener("mousemove", (event) => {
if (isDragging) {
const rect = canvas.getBoundingClientRect();
aimX = event.clientX - rect.left;
aimY = event.clientY - rect.top;
}
})
});