Running multiple requestAnimation loops to fire multiple balls? - javascript

I'm trying to get the ball below to keep appearing and firing up accross the y axis at a set interval and always from where the x position of paddle(mouse) is, i need there to be a delay between each ball firing. I'm trying to make space invaders but with the ball constantly firing at a set interval.
Do I need to create multiple requestAnimationFrame loops for each ball? Can someone help with a very basic example of how this should be done please or link a good article? I am stuck at creating an array for each ball and not sure how to architect the loop to achieve this effect. All the examples I can find are too complex
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* {
padding: 0;
margin: 0;
}
canvas {
background: #eee;
display: block;
margin: 0 auto;
width: 30%;
}
</style>
</head>
<body>
<canvas id="myCanvas" height="400"></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
//start the requestAnimationFrame loop
var myRequestAnimation;
var myRequestAnimationBall;
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
drawLoop();
setInterval(drawBallLoop, 400);
var x = canvas.width / 2;
var y = canvas.height - 30;
var defaultSpeedX = 0;
var defaultSpeedY = 4;
var dx = defaultSpeedX;
var dy = -defaultSpeedY;
var ballRadius = 10;
var paddleX = (canvas.width - paddleWidth) / 2;
var paddleHeight = 10;
var paddleWidth = 70;
//control stuff
var rightPressed = false;
var leftPressed = false;
var brickRowCount = 1;
var brickColumnCount = 1;
var brickWidth = 40;
var brickHeight = 20;
var brickPadding = 10;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;
var score = 0;
var lives = 3;
//paddle
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
//bricks
function drawBricks() {
for (var c = 0; c < brickColumnCount; c++) {
for (var r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status == 1) {
var brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
var brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
//collision detection
function collisionDetection() {
for (var c = 0; c < brickColumnCount; c++) {
for (var r = 0; r < brickRowCount; r++) {
var b = bricks[c][r];
if (b.status == 1) {
if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
dy = -dy;
b.status = 0;
score++;
console.log(score);
if (score == brickRowCount * brickColumnCount) {
console.log("YOU WIN, CONGRATS!");
window.cancelAnimationFrame(myRequestAnimation);
}
}
}
}
}
}
//default bricks
var bricks = [];
for (var c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (var r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
//lives
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
}
// ball1
var ball1 = {
x,
y,
directionX: 0,
directionY: -5
}
// ball1
var ball2 = {
x,
y,
directionX: 0,
directionY: -2
}
// put each ball in a balls[] array
var balls = [ball1, ball2];
function drawBall() {
// clearCanvas();
for (var i = 0; i < balls.length; i++) {
var ball = balls[i]
ctx.beginPath();
ctx.arc(ball.x, ball.y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
///////DRAW BALL LOOP////////
function drawBallLoop() {
myRequestAnimationBall = requestAnimationFrame(drawBallLoop);
// clear frame
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawBall();
//move balls
for (var i = 0; i < balls.length; i++) {
balls[i].y += balls[i].directionY;
}
}
//Clear Canvas
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
///////DRAW MAIN LOOP////////
function drawLoop() {
myRequestAnimation = requestAnimationFrame(drawLoop);
// clear frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw ball
drawPaddle();
drawBricks();
collisionDetection();
drawLives();
//bounce off walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (rightPressed) {
paddleX += 7;
if (paddleX + paddleWidth > canvas.width) {
paddleX = canvas.width - paddleWidth;
}
}
else if (leftPressed) {
paddleX -= 7;
if (paddleX < 0) {
paddleX = 0;
}
}
}
//keyboard left/right logic
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
function keyDownHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = true;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key == "Right" || e.key == "ArrowRight") {
rightPressed = false;
}
else if (e.key == "Left" || e.key == "ArrowLeft") {
leftPressed = false;
}
}
//relative mouse pos
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect(), // abs. size of element
scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X
scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y
return {
x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have
y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element
}
}
//mouse movemment
document.addEventListener("mousemove", mouseMoveHandler, false);
function mouseMoveHandler(e) {
var mouseX = getMousePos(canvas, e).x;
//e.clientX = the horizontal mouse position in the viewport
//canvas.offsetLeft = the distance between the left edge of the canvas and left edge of the viewport
var relativeX = mouseX;
// console.log('mouse= ',relativeX, canvas.offsetLeft)
// console.log('paddle= ', paddleX);
// console.log(getMousePos(canvas, e).x);
if (relativeX - (paddleWidth / 2) > 0 && relativeX < canvas.width - (paddleWidth / 2)) {
paddleX = relativeX - (paddleWidth / 2);
}
}
</script>
</body>
</html>

Basic principles
Here's one way you could do it:
you need a Game object that will handle the update logic, store all current entities, deal with the game loop... IMO, this is where you should keep track of when was the last Ball fired and whether to fire a new one.
In this demo, this object also handles current time, delta time, and requesting animation frames, but some might argue that this logic could be externalised, and just call some sort of Game.update(deltaTime) on each frame.
you need different objects for all entities in your game. I created an Entity class because I want to make sure all game entities have the minimum required to function (ie. update, draw, x, y...).
There is a Ball class that extends Entity and is responsible for knowing it's own parameters (speed, size, ...), how to update and draw itself, ...
There is a Paddle class that I left bare for you to complete.
The bottom line is it's all a matter of separation of concerns. Who should know what about whom? And then pass variables around.
As for your other question:
Do I need to create multiple requestAnimationFrame loops for each ball?
It's definitely possible, but I'd argue that having a centralized place that handles lastUpdate, deltaTime, lastBallCreated makes things much simpler. And in practice, devs tends to try and have a single animation frame loop for this.
class Entity {
constructor(x, y) {
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.speed = 100 // px per second
this.size = 10 // radius in px
}
update(deltaTime) {
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.size, 0, 2 * Math.PI)
context.fill()
}
isDead() {
return this.y < 0 - this.size
}
}
class Paddle extends Entity {
constructor() {
super(0, 0)
}
update() { /**/ }
draw() { /**/ }
isDead() { return false }
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 1000 // ms between each ball
this.lastBallCreated = 0 // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
const paddle = new Paddle()
this.entities.push(paddle)
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
this.entities.forEach(entity => entity.update(deltaTime))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
const ball = new Ball(100, 300) // this is quick and dirty, you should put some more thought into `x` and `y` here
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
Managing player inputs
Now let's say you want to add keyboard inputs to your game. In that case, I'd actually create a separate class, because depending on how many "buttons" you want to support, it can get very complicated very quick.
So first, let's draw a basic paddle so we can see what's happening:
class Paddle extends Entity {
constructor() {
// we just add a default initial x,y and height,width
super(150, 20)
this.width = 50
this.height = 10
}
update() { /**/ }
/** #param {CanvasRenderingContext2D} context */
draw(context) {
// we just draw a simple rectangle centered on x,y
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
And now we add a basic InputsManager class that you can make as complicated as you want. Just for two keys, handling keydown and keyup and the fact that two keys can be pressed at once it already a few lines of code so it's good to keep things separate so as to not clutter our Game object.
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
Now, we can update our Game class to make use of this new InputsManager
class Game {
// ...
start() {
// ...
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
} // we now pass more data to the update method so that entities that need to can also read from our InputsManager
this.entities.forEach(entity => entity.update(frameData))
}
// ...
}
And after updating the code for the update methods of our entities to actually use the new InputsManager, here's the result:
class Entity {
constructor(x, y) {
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({deltaTime}) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fill()
}
isDead() {
return this.y < 0 - this.radius
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.speed = 200
this.width = 50
this.height = 10
}
update({deltaTime, inputs}) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
// we can now read from this.player to the the position of where to fire a Ball
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead()) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Once you click "Run code snippet", you have to click on the iframe to focus it so it can listen for keyboard inputs (arrow left, arrow right).
As a bonus, since we are now able to draw and move the paddle, I added the ability to create a Ball at the same x coordinate as the paddle. You can read the comments I left in the code snippet above for a quick explanation as to how this works.
How to add functionality
Now I want to give you a more general outlook at how to approach future problems you might have when expending on this example. I'll take the example of wanting to test for collision between two game objects. You should ask yourself where to place the logic?
where is a place that all game objects can share logic? (creating the information)
where will you need to know about collisions? (accessing the information)
In this example, all game objects are sub classes of Entity so to me this makes sense to put the code there:
class Entity {
constructor(x, y) {
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if(a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if(b.collision === 'none') {
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if(a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius
}
if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
}
Now there are many kinds of 2D collisions so the code is a bit verbose, but the main point is: this is a design decision that I'm making here. I can be generalist and future proof this but then it looks like the above... And I have to add a .collision property to all of my game objects so that they know whether they should be treated as a 'circle' or a 'rect' in the above algorithm.
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
}
// ...
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
}
// ...
}
Or I can be minimalist and just add what I need, in which case it might make more sense to actually put the code in the Paddle entity:
class Paddle extends Entity {
testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x + ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x - ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
Either way, I now have access to the collision information from the cleanup function the Game loop (where I chose to place the logic of removing dead entities).
With my first generalist solution, I would use it like this:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
With the second minimalist approach, I'd still have to pass the player around for the test:
class Game {
cleanup() {
this.entities.forEach(entity => {
// I'm passing this.player so all entities can test for collision with the player
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
}
class Ball extends Entity {
isDead(player) {
// this is the "out of bounds" test we already had
const outOfBounds = this.y < 0 - this.radius
// this is the new "collision with player paddle"
const collidesWithPlayer = player.testBallCollision(this)
return outOfBounds || collidesWithPlayer
}
}
Final result
I hope you've learned something. In the mean time, here's the final result of this very long answer post:
class Entity {
constructor(x, y) {
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if(a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if(b.collision === 'none') {
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if(a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2) < a.radius + b.radius
}
if(a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height / 2
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height / 2
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 2
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({deltaTime}) {
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fill()
}
isDead(player) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithPlayer = Entity.testCollision(player, this)
return outOfBounds || collidesWithPlayer
}
}
class Paddle extends Entity {
constructor() {
super(150, 50)
this.collision = 'rect'
this.speed = 200
this.width = 50
this.height = 10
}
update({deltaTime, inputs}) {
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height)
context.fill()
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if(this.direction === -1)
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if(this.direction === 1)
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 500 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
start() {
this.lastUpdate = performance.now()
this.player = new Paddle()
this.entities.push(this.player)
this.inputsManager = new InputsManager()
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// update every entity
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if(this.lastBallCreated + this.newBallInterval < newTime) {
const ball = new Ball(this.player.x, 300)
this.entities.push(ball)
this.lastBallCreated = newTime
}
// remember current time for next update
this.lastUpdate = newTime
}
draw() {
this.entities.forEach(entity => entity.draw(this.context))
}
cleanup() {
// to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if(entity.isDead(this.player)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
loop() {
requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
this.update()
this.draw()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
<canvas height="300" width="300"></canvas>
<script src="script.js"></script>
Once you click "Run code snippet", you have to click on the iframe to focus it so it can listen for keyboard inputs (arrow left, arrow right).

Related

How to move object to target naturally and smoothly?

Can somebody fix it script to make it works properly?
What I expects:
Run script
Click at the canvas to set target (circle)
Object (triangle) starts to rotate and move towards to target (circle)
Change target at any time
How it works:
Sometimes object rotates correctly, sometimes isn't
Looks like one half sphere works well, another isn't
Thanks!
// prepare 2d context
const c = window.document.body.appendChild(window.document.createElement('canvas'))
.getContext('2d');
c.canvas.addEventListener('click', e => tgt = { x: e.offsetX, y: e.offsetY });
rate = 75 // updates delay
w = c.canvas.width;
h = c.canvas.height;
pi2 = Math.PI * 2;
// object that moves towards the target
obj = {
x: 20,
y: 20,
a: 0, // angle
};
// target
tgt = undefined;
// main loop
setInterval(() => {
c.fillStyle = 'black';
c.fillRect(0, 0, w, h);
// update object state
if (tgt) {
// draw target
c.beginPath();
c.arc(tgt.x, tgt.y, 2, 0, pi2);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
// update object position
// vector from obj to tgt
dx = tgt.x - obj.x;
dy = tgt.y - obj.y;
// normalize
l = Math.sqrt(dx*dx + dy*dy);
dnx = (dx / l);// * 0.2;
dny = (dy / l);// * 0.2;
// update object position
obj.x += dnx;
obj.y += dny;
// angle between +x and tgt
a = Math.atan2(0 * dx - 1 * dy, 1 * dx + 0 * dy);
// update object angle
obj.a += -a * 0.04;
}
// draw object
c.translate(obj.x, obj.y);
c.rotate(obj.a);
c.beginPath();
c.moveTo(5, 0);
c.lineTo(-5, 4);
c.lineTo(-5, -4);
//c.lineTo(3, 0);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
c.rotate(-obj.a);
c.translate(-obj.x, -obj.y);
}, rate);
This turned out to be a bit more challenging than I first thought and I ended up just re-writing the code.
The challenges:
Ensure the ship only rotated to the exact point of target. This required me to compare the two angle from the ship current position to where we want it to go.
Ensure the target did not rotate past the target and the ship did not translate past the target. This required some buffer space for each because when animating having this.x === this.x when an object is moving is very rare to happen so we need some room for the logic to work.
Ensure the ship turned in the shortest direction to the target.
I have added notes in the code to better explain. Hopefully you can implement this into yours or use it as is. Oh and you can change the movement speed and rotation speed as you see fit.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let mouse = { x: 20, y: 20 };
let canvasBounds = canvas.getBoundingClientRect();
let target;
canvas.addEventListener("mousedown", (e) => {
mouse.x = e.x - canvasBounds.x;
mouse.y = e.y - canvasBounds.y;
target = new Target();
});
class Ship {
constructor() {
this.x = 20;
this.y = 20;
this.ptA = { x: 15, y: 0 };
this.ptB = { x: -15, y: 10 };
this.ptC = { x: -15, y: -10 };
this.color = "red";
this.angle1 = 0;
this.angle2 = 0;
this.dir = 1;
}
draw() {
ctx.save();
//use translate to move the ship
ctx.translate(this.x, this.y);
//angle1 is the angle from the ship to the target point
//angle2 is the ships current rotation angle. Once they equal each other then the rotation stops. When you click somewhere else they are no longer equal and the ship will rotate again.
if (!this.direction(this.angle1, this.angle2)) {
//see direction() method for more info on this
if (this.dir == 1) {
this.angle2 += 0.05; //change rotation speed here
} else if (this.dir == 0) {
this.angle2 -= 0.05; //change rotation speed here
}
} else {
this.angle2 = this.angle1;
}
ctx.rotate(this.angle2);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.moveTo(this.ptA.x, this.ptA.y);
ctx.lineTo(this.ptB.x, this.ptB.y);
ctx.lineTo(this.ptC.x, this.ptC.y);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
driveToTarget() {
//get angle to mouse click
this.angle1 = Math.atan2(mouse.y - this.y, mouse.x - this.x);
//normalize vector
let vecX = mouse.x - this.x;
let vecY = mouse.y - this.y;
let dist = Math.hypot(vecX, vecY);
vecX /= dist;
vecY /= dist;
//Prevent continuous x and y increment by checking if either vec == 0
if (vecX != 0 || vecY != 0) {
//then also give the ship a little buffer incase it passes the given point it doesn't turn back around. This allows time for it to stop if you increase the speed.
if (
this.x >= mouse.x + 3 ||
this.x <= mouse.x - 3 ||
this.y >= mouse.y + 3 ||
this.y <= mouse.y - 3
) {
this.x += vecX; //multiple VecX by n to increase speed (vecX*2)
this.y += vecY; //multiple VecY by n to increase speed (vecY*2)
}
}
}
direction(ang1, ang2) {
//converts rads to degrees and ensures we get numbers from 0-359
let a1 = ang1 * (180 / Math.PI);
if (a1 < 0) {
a1 += 360;
}
let a2 = ang2 * (180 / Math.PI);
if (a2 < 0) {
a2 += 360;
}
//checks whether the target is on the right or left side of the ship.
//We use then to ensure it turns in the shortest direction
if ((360 + a1 - a2) % 360 > 180) {
this.dir = 0;
} else {
this.dir = 1;
}
//Because of animation timeframes there is a chance the ship could turn past the target if rotating too fast. This gives the ship a 1 degree buffer to either side of the target to determine if it is pointed in the right direction.
//We then correct it to the exact degrees in the draw() method above once the if statment defaults to 'else'
if (
Math.trunc(a2) <= Math.trunc(a1) + 1 &&
Math.trunc(a2) >= Math.trunc(a1) - 1
) {
return true;
}
return false;
}
}
let ship = new Ship();
class Target {
constructor() {
this.x = mouse.x;
this.y = mouse.y;
this.r = 3;
this.color = "red";
}
draw() {
ctx.strokeStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.stroke();
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
ship.draw();
ship.driveToTarget();
if (target) {
target.draw();
}
requestAnimationFrame(animate);
}
animate();
<canvas id="canvas"></canvas>

Game loop failing when speed of ball is faster

I'm building a space invaders game and the code below works, the ball is fired and if it hits the enemy the isDead() function is triggered which also switches dead to true for that ball and this is also passed to the enemy class so it causes the enemy to get destroyed. however when I increase the ball frequency the enemy isDead function fails to run, I'm really not sure why when the ball interval is higher this whole system breaks.
when this.newBallInterval = 700 the enemy square dies
when this.newBallInterval = 600 it doesn't
why? and how to fix?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
background-color: aquamarine;
}
</style>
</head>
<body>
<canvas height="300" width="300"></canvas>
</body>
<script>
class Entity {
constructor(x, y) {
this.dead = false;
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if (a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if (b.collision === 'none') { d
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if (a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
}
if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
static testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.dead = false;
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({ deltaTime }) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fillStyle = "#1ee511";
context.fill()
}
isDead(enemy) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithEnemy = Entity.testCollision(enemy, this)
if (outOfBounds) {
return true
}
if (collidesWithEnemy){
//console.log('dead')
this.dead = true;
return true
}
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speed = 0;
this.y = y;
}
update() {
this.x += this.speed;
if (this.x > canvas.width - this.width) {
this.speed -= 5;
}
if (this.x === 0) {
this.speed += 5;
}
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x, this.y, this.width, this.height);
context.fillStyle = "#9995DD";
context.fill();
context.closePath();
}
isDead(enemy, ball) {
//// collision detection
// const collidesWithEnemy = Entity.testCollision(enemy, ball)
// if (collidesWithEnemy){
// console.log('enemy dead')
// game.hitEnemy();
// return true
// }
if (ball.dead){
console.log('enemy dead')
game.hitEnemy();
return true
}
}
}
class Paddle extends Entity {
constructor(x, width) {
super(150, 300)
this.collision = 'rect'
this.speed = 200
this.height = 10
this.width = 50
}
update({ deltaTime, inputs }) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
context.fillStyle = "#0095DD";
context.fill();
context.closePath();
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if (this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if (this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 700 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('end game')
}
hitEnemy() {
const endGame = 1;
game.loop(endGame)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(100, 20)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
//start watching inputs
this.inputsManager = new InputsManager()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// we now pass more data to the update method so that entities that need to can also read from our InputsManager
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
// update every entity
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if (this.lastBallCreated + this.newBallInterval < newTime) {
// this is quick and dirty, you should put some more thought into `x` and `y` here
this.ball = new Ball(this.player.x, 280)
this.entities.push(this.ball)
this.lastBallCreated = newTime
}0
//draw entities
this.entities.forEach(entity => entity.draw(this.context))
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy, this.ball)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if(endGame){
cancelAnimationFrame(this.myLoop);
this.endGame();
return;
}
this.update()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>
The problem is that you are overwriting this.ball, so your enemy only checks against the newly spawned one, which is not dead yet.
You could simply store all the balls in their own Array and check all of them in your enemy.isDead() method:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: rgb(214, 238, 149);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
background-color: aquamarine;
}
</style>
</head>
<body>
<canvas height="300" width="300"></canvas>
</body>
<script>
class Entity {
constructor(x, y) {
this.dead = false;
this.collision = 'none'
this.x = x
this.y = y
}
update() { console.warn(`${this.constructor.name} needs an update() function`) }
draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
static testCollision(a, b) {
if (a.collision === 'none') {
console.warn(`${a.constructor.name} needs a collision type`)
return undefined
}
if (b.collision === 'none') { d
console.warn(`${b.constructor.name} needs a collision type`)
return undefined
}
if (a.collision === 'circle' && b.collision === 'circle') {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) < a.radius + b.radius
}
if (a.collision === 'circle' && b.collision === 'rect' || a.collision === 'rect' && b.collision === 'circle') {
let circle = a.collision === 'circle' ? a : b
let rect = a.collision === 'rect' ? a : b
// this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
const topOfBallIsAboveBottomOfRect = circle.y - circle.radius <= rect.y + rect.height
const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius >= rect.y - rect.height
const ballIsRightOfRectLeftSide = circle.x + circle.radius >= rect.x - rect.width / 4
const ballIsLeftOfRectRightSide = circle.x - circle.radius <= rect.x + rect.width
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
return undefined
}
static testBallCollision(ball) {
const topOfBallIsAboveBottomOfRect = ball.y - ball.radius <= this.y + this.height / 2
const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius >= this.y - this.height / 2
const ballIsRightOfRectLeftSide = ball.x - ball.radius >= this.x - this.width / 2
const ballIsLeftOfRectRightSide = ball.x + ball.radius <= this.x + this.width / 2
return topOfBallIsAboveBottomOfRect && bottomOfBallIsBelowTopOfRect && ballIsRightOfRectLeftSide && ballIsLeftOfRectRightSide
}
}
class Ball extends Entity {
constructor(x, y) {
super(x, y)
this.dead = false;
this.collision = 'circle'
this.speed = 300 // px per second
this.radius = 10 // radius in px
}
update({ deltaTime }) {
// Ball still only needs deltaTime to calculate its update
this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
context.fillStyle = "#1ee511";
context.fill()
}
isDead(enemy) {
const outOfBounds = this.y < 0 - this.radius
const collidesWithEnemy = Entity.testCollision(enemy, this)
if (outOfBounds) {
return true
}
if (collidesWithEnemy){
//console.log('dead')
this.dead = true;
return true
}
}
}
class Enemy extends Entity {
constructor(x, y) {
super(x, y)
this.collision = 'rect'
this.height = 50;
this.width = 50;
this.speed = 0;
this.y = y;
}
update() {
this.x += this.speed;
if (this.x > canvas.width - this.width) {
this.speed -= 5;
}
if (this.x === 0) {
this.speed += 5;
}
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x, this.y, this.width, this.height);
context.fillStyle = "#9995DD";
context.fill();
context.closePath();
}
isDead(enemy, balls) {
//// collision detection
// const collidesWithEnemy = Entity.testCollision(enemy, ball)
// if (collidesWithEnemy){
// console.log('enemy dead')
// game.hitEnemy();
// return true
// }
if (balls.some(ball => ball.dead)){
console.log('enemy dead')
game.hitEnemy();
return true
}
}
}
class Paddle extends Entity {
constructor(x, width) {
super(150, 300)
this.collision = 'rect'
this.speed = 200
this.height = 10
this.width = 50
}
update({ deltaTime, inputs }) {
// Paddle needs to read both deltaTime and inputs
this.x += this.speed * deltaTime / 1000 * inputs.direction
}
/** #param {CanvasRenderingContext2D} context */
draw(context) {
context.beginPath();
context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
context.fillStyle = "#0095DD";
context.fill();
context.closePath();
}
isDead() { return false }
}
class InputsManager {
constructor() {
this.direction = 0 // this is the value we actually need in out Game object
window.addEventListener('keydown', this.onKeydown.bind(this))
window.addEventListener('keyup', this.onKeyup.bind(this))
}
onKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.direction = -1
break
case 'ArrowRight':
this.direction = 1
break
}
}
onKeyup(event) {
switch (event.key) {
case 'ArrowLeft':
if (this.direction === -1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
case 'ArrowRight':
this.direction = 1
if (this.direction === 1) // make sure the direction was set by this key before resetting it
this.direction = 0
break
}
}
}
class Game {
/** #param {HTMLCanvasElement} canvas */
constructor(canvas) {
this.balls = [];
this.entities = [] // contains all game entities (Balls, Paddles, ...)
this.context = canvas.getContext('2d')
this.newBallInterval = 300 // ms between each ball
this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
}
endGame() {
//clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
console.log('end game')
}
hitEnemy() {
const endGame = 1;
game.loop(endGame)
}
start() {
this.lastUpdate = performance.now()
this.enemy = new Enemy(100, 20)
this.entities.push(this.enemy)
// we store the new Paddle in this.player so we can read from it later
this.player = new Paddle()
// but we still add it to the entities list so it gets updated like every other Entity
this.entities.push(this.player)
//start watching inputs
this.inputsManager = new InputsManager()
//start game loop
this.loop()
}
update() {
// calculate time elapsed
const newTime = performance.now()
const deltaTime = newTime - this.lastUpdate
// we now pass more data to the update method so that entities that need to can also read from our InputsManager
const frameData = {
deltaTime,
inputs: this.inputsManager,
}
// update every entity
this.entities.forEach(entity => entity.update(frameData))
// other update logic (here, create new entities)
if (this.lastBallCreated + this.newBallInterval < newTime) {
// this is quick and dirty, you should put some more thought into `x` and `y` here
const newBall = new Ball(this.player.x, 280);
this.balls.push( newBall );
this.entities.push( newBall )
this.lastBallCreated = newTime
}0
//draw entities
this.entities.forEach(entity => entity.draw(this.context))
// remember current time for next update
this.lastUpdate = newTime
}
cleanup() {
//to prevent memory leak, don't forget to cleanup dead entities
this.entities.forEach(entity => {
if (entity.isDead(this.enemy, this.balls)) {
const index = this.entities.indexOf(entity)
this.entities.splice(index, 1)
}
})
}
//main game loop
loop(endGame) {
this.myLoop = requestAnimationFrame(() => {
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
if(endGame){
cancelAnimationFrame(this.myLoop);
this.endGame();
return;
}
this.update()
this.cleanup()
this.loop()
})
}
}
const canvas = document.querySelector('canvas')
const game = new Game(canvas)
game.start()
</script>
</html>
But honestly the whole logic seems odd here and I'm afraid you find out you'll need to rewrite a lot of it later on (e.g when you'll want to have more than a single enemy), but doing this for you would be too much for an SO answer.

Why isnt collision detection working? (p5 js frame work)

I created a collision detection between Snake and BasicEnemy. I created a for loop to make five different enemies but the collision detection doesn't get called on any of the enemies that were created from the for loop. The collision only works with the one BasicEnemy object. Why isn't collision function being called for all of the enemies inside the array? Thank you.
Sketch.js
var snake;
var food;
var basicEnemy;
var scl = 20;
var enemies = [];
function setup() {
createCanvas(600, 500);
snake = new Snake();
basicEnemy = new BasicEnemy();
//** CREATE FIVE ENEMIES **
for (var i = 0; i < 5; i++) {
enemies[i] = new BasicEnemy();
}
}
// **FUNCTION WHEN SNAKE HITS ENEMY**
function collision() {
console.log("hit!");
}
function draw() {
background(51);
//Draw snake
snake.update();
snake.show();
//Draw basicEnemy
basicEnemy.update();
basicEnemy.show();
//** LOOP THROUGH ENEMIES AND UPDATE AND SHOW **
for (var i = 0; i < enemies.length; i++) {
enemies[i].show();
enemies[i].update();
if (enemies[i].hits(snake)) {
collision();
}
}
}
function keyPressed() {
if (keyCode === UP_ARROW){
snake.dir(0, -1);
} else if (keyCode === DOWN_ARROW) {
snake.dir(0, 1);
} else if (keyCode === LEFT_ARROW) {
snake.dir(-1 , 0);
} else if (keyCode === RIGHT_ARROW) {
snake.dir(1 , 0);
}
}
BasicEnemy.js
function BasicEnemy() {
this.x = random(700);
this.y = random(700);
this.velX = 15;
this.velY = 15;
}
//** FUNCTION TO CHECK IF ENEMY AND SNAKE ARE IN THE SAME LOCATION **
this.hits = function (pos) {
var = d = dist(this.x, this.y, pos.x, pos.y);
if(d < 1) {
return true;
} else {
return false;
}
}
this.show = function () {
fill(255, 0, 100);
rect(this.x, this.y, scl, scl);
}
Snake.js
function Snake() {
this.x = 0;
this.y = 0;
this.xspeed = 1;
this.yspeed = 0;
this.update = function() {
this.x = this.x + this.xspeed * scl;
this.y = this.y + this.yspeed * scl;
this.x = constrain(this.x, 0, width - scl);
this.y = constrain(this.y, 0, height - scl);
}
this.show = function() {
fill(255);
rect(this.x, this.y, scl, scl);
}
this.dir = function (x , y) {
this.xspeed = x;
this.yspeed = y;
}
}
Because you're essentially checking for the distance between the top left corners of the snake and the enemy, this'll only return true, if they completely overlap.
Use an AABB collision detection instead:
return this.x + scl >= pos.x && this.x <= pos.x + scl && this.y + scl >= pos.y && this.y <= pos.y + scl;
This returns true, if the first rectangle contains the second rectangle.
MDN says:
One of the simpler forms of collision detection is between two rectangles that are axis aligned — meaning no rotation. The algorithm works by ensuring there is no gap between any of the 4 sides of the rectangles. Any gap means a collision does not exist.

Canvas collision

I am a new in javascript and trying to find out how to make a collision with ball and plank which will stop the game and alert player with something like "You lost". But I only want red balls to hit the plank and blue to pass on without touching. Here is code that I am working on. (I dont mind if you could help to do collision only with both balls)
var spawnRate = 100;
var spawnRateOfDescent = 2;
var lastSpawn = -10;
var objects = [];
var startTime = Date.now();
function spawnRandomObject() {
var t;
if (Math.random() < 0.50) {
t = "red";
} else {
t = "blue";
}
var object = {
type: t,
x: Math.random() * (canvas.width - 30) + 15,
y: 0
}
objects.push(object);
}
function animate() {
var time = Date.now();
if (time > (lastSpawn + spawnRate)) {
lastSpawn = time;
spawnRandomObject();
}
for (var i = 0; i < objects.length; i++) {
var object = objects[i];
object.y += spawnRateOfDescent;
ctx.beginPath();
ctx.arc(object.x, object.y, 8, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = object.type;
ctx.fill();
}
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var paddleHeight = 10;
var paddleWidth = 60;
var paddleY = 480
var paddleX = (canvas.width-paddleWidth)/2;
var rightPressed = false;
var leftPressed = false;
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
function keyDownHandler(e) {
if(e.keyCode == 39) {
rightPressed = true;
}
else if(e.keyCode == 37) {
leftPressed = true;
}
}
function keyUpHandler(e) {
if(e.keyCode == 39) {
rightPressed = false;
}
else if(e.keyCode == 37) {
leftPressed = false;
}
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, paddleY, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPaddle();
animate();
if(rightPressed && paddleX < canvas.width-paddleWidth) {
paddleX += 3;
}
else if(leftPressed && paddleX > 0) {
paddleX -= 3;
}
}
setInterval(draw, 10);
Thanks!
If you have an object like this:
let ball = { type: 'red', x: 10, y: 10, width: 10, height: 10 };
You might want to consider adding a method to this to check if it overlaps any other rectangle:
ball.overlapsBall = function( otherBall ){
return !(
otherBall.x + otherBall.width < this.x
&& otherBall.y + otherBall.height < this.y
&& otherBall.y > this.y + this.height
&& otherBall.x > this.x + this.height
);
}
You do this by checking if it does not overlap, which is only true if one box is entirely outside of the other (have a read through the if statement and try to visualise it, its actually rather simple)
In your draw function you could now add a loop to see if any overlap occurs:
var overlap = objects.filter(function( ball ) { return paddle.overlapsBall( ball ) });
You could even place an if statement to check it's type! (The filter will take you entire array of balls and check the overlaps, and remove anything from the array that does not return true. Now you can use overlaps.forEach(function( ball ){ /* ... */}); to do something with all the balls that overlapped your paddle.)
One last thing, if you are planning on doing this with many objects you might want to consider using a simple class like this for every paddle or ball you make:
class Object2D {
constructor(x = 0, y = 0;, width = 1, height = 1){
this.x = x;
this.y = x;
this.width = width;
this.height = height;
}
overlaps( otherObject ){
!( otherObject.x + otherObject.width < this.x && otherObject.y + otherObject.height < this.y && otherObject.y > this.y + this.height && otherObject.x > this.x + this.height );
}
}
This allows you to this simple expression to create a new object that automatically has a method to check for overlaps with similar objects:
var paddle = new Object2D(0,0,20,10);
var ball = new Object2D(5,5,10,10);
paddle.overlaps( ball ); // true!
On top of that, you are ensured that any Object2D contains the values you will need for your calculations. You can check if this object is if the right type using paddle instanceof Object2D (which is true).
Note Please note, as #Janje so continuously points out in the comments below, that we are doing a rectangle overlap here and it might create some 'false positives' for all the pieces of rectangle that aren't the circle. This is good enough for most cases, but you can find the math for other overlaps and collisions easily ith a quick google search.
Update: Simple Implementation
See below for a very simple example of how overlaps work in action:
var paddle = { x: 50, y: 50, width: 60, height: 20 };
var box = { x: 5, y: 20, width: 20, height: 20 };
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild( canvas );
canvas.width = 300;
canvas.height = 300;
function overlaps( a, b ){
return !!( a.x + a.width > b.x && a.x < b.x + b.width
&& a.y + a.height > b.y && a.y < b.y + b.height );
}
function animate(){
ctx.clearRect( 0, 0, canvas.width, canvas.height );
ctx.fillStyle = overlaps( paddle, box ) ? "red" : "black";
ctx.fillRect( paddle.x, paddle.y, paddle.width, paddle.height );
ctx.fillRect( box.x, box.y, box.width, box.height );
window.requestAnimationFrame( animate );
}
canvas.addEventListener('mousemove', function(event){
paddle.x = event.clientX - paddle.width / 2;
paddle.y = event.clientY - paddle.height / 2;
})
animate();

JS Function only runs if key is pressed

so I've put some simple collision detection code on a canvas: if my obstacle car sprites touch my user car sprite, the obstacle car stops. For some reason, when the cars are close, the collision is only detected if I am pressing the keys that my user car uses to move (up, down, left, and right arrow keys). How can I get this function to work all the time, regardless of if I am pressing down the keys to move?
collision detection code:
//Collide test
function firstObstaclecollideTest () {
if (Math.abs(x1 - (usercar.width / 2) - x) < usercar.width && Math.abs(y1 - (usercar.height / 2) - y) < usercar.height) {
mod1 = 0;
speed1 = 0;
}
requestAnimationFrame(firstObstaclecollideTest);
}
requestAnimationFrame(firstObstaclecollideTest);
function secondObstaclecollideTest () {
if (Math.abs(x2 - (usercar.width / 2) - x) < usercar.width && Math.abs(y2 - (usercar.height / 2) - y) < usercar.height) {
mod2 = 0;
speed2 = 0;
}
requestAnimationFrame(secondObstaclecollideTest);
}
requestAnimationFrame(secondObstaclecollideTest);
Full Code: http://jsbin.com/dofihiwize/1/edit?output
Your code is a bit messy i fear :
- You are triggering 4 animation loops : have only one loop to avoid headaches.
- You are duplicating quite some code : go for a Car class to clean things up.
- There are several confusion of concern : for instance, the function drawing the car is clearing the canvas, and also drawing the time elapsed. The function names are also misleading (gameStart is a game loop, ... ).
updated fiddle is here :
http://jsbin.com/bafulazose/1/edit?js,output
//Setting the canvas and context
var canvas = document.getElementById('background');
var context = canvas.getContext('2d');
//================
// CAR Class
//================
//Uploading obstacle car
var carImage = new Image();
carImage.src = "http://www.i2clipart.com/cliparts/f/e/3/a/128135fe3a51f073730a8d561282d05b4f35ab.png";
function Car(x, y, speed, mod, angle) {
this.x = x; // x center of car
this.y = y; // y center of car
this.speed = speed;
this.mod = mod;
this.angle = angle;
this.move = function () {
this.x += (this.speed * this.mod) * Math.cos(Math.PI / 180 * this.angle);
this.y += (this.speed * this.mod) * Math.sin(Math.PI / 180 * this.angle);
if (this.y > context.canvas.height + 150) {
this.y = -carImage.height;
this.x = Math.floor(Math.random() * canvas.width);
}
};
this.draw = function () {
context.save();
context.translate(this.x, this.y);
context.rotate(this.angle* Math.PI / 180);
context.drawImage(carImage, -(carImage.width / 2), -(carImage.height / 2));
context.strokeRect(-(carImage.width / 2), -(carImage.height / 2), carImage.width , carImage.height);
context.restore();
};
this.testCollision = function(other) {
var dx = Math.abs(this.x - other.x );
var dy = Math.abs(this.y - other.y );
if ( dx < carImage.width && dy < carImage.height) {
this.mod = 0;
this.speed = 0;
}
};
}
//================
//ENTER: USER CAR
//================
var userCar = new Car(450, 550, 10, -1, -90);
setupKeys(userCar);
//=====================
//ENTER: OBSTACLE CAR 1
//=====================
var obstacleCar1 ;
//======================
//ENTER: OBSTACLE CAR 2
//======================
var obstacleCar2 ;
function setupGame () {
obstacleCar1 = new Car(200, 5, 5, 1, 90);
obstacleCar2 = new Car(340, 5, 5, 1, 90);
gameOver = false;
startTime = Date.now();
score = 0;
}
//=========================
//Properties for score keep
//=========================
var score;
var startTime;
var gameOver;
var spaceBarPressed = false;
//=========================
// Launch the game
//=========================
setupGame () ;
var gameLoopInterval = setInterval(gameLoop, 30);
//===========================
//Draw Final and Elasped Time
//===========================
function drawElapsedTime() {
context.save();
context.fillStyle = "black";
context.font = "30px Verdana";
context.fillText(parseInt((Date.now() - startTime) / 1000) + " secs", canvas.width - 120, 40);
context.restore();
}
function drawFinalScore() {
context.save();
context.fillStyle = "black";
context.font = "30px Verdana";
context.fillText("Game Over: " + score + " secs", 100, 100);
context.font = "12px Verdana";
context.fillText("Press space to restart", 190, 150);
context.restore();
}
//========================
// Game loop
//========================
function gameLoop() {
context.clearRect(0, 0, canvas.width, canvas.height);
if (gameOver) {
drawFinalScore();
if (spaceBarPressed) {
setupGame ();
}
return;
}
obstacleCar1.move();
obstacleCar2.move();
obstacleCar1.testCollision(userCar);
obstacleCar2.testCollision(userCar);
if (obstacleCar1.speed===0 && obstacleCar2.speed===0) {
score = parseInt((Date.now() - startTime) / 1000);
gameOver = true;
spaceBarPressed = false;
}
obstacleCar1.draw();
obstacleCar2.draw();
userCar.draw();
drawElapsedTime();
}
//========================
// Keys handling
//========================
function setupKeys(target) {
var cancelledKeys = [32, 37, 38, 39, 40];
function keyUpHandler(event) {
if (event.keyCode == 38 || event.keyCode == 40) {
mod = 0;
}
}
function keyDownHandler(event) {
var keyCode = event.keyCode;
if (keyCode == 37) {
target.x -= target.speed;
}
if (keyCode == 39) {
target.x += target.speed;
}
if (keyCode == 32) {
spaceBarPressed = true;
}
// space and arrow keys
if (cancelledKeys.indexOf(keyCode) > -1) {
event.preventDefault();
}
}
//Event listeners for keys
window.addEventListener("keydown", keyDownHandler, false);
window.addEventListener("keyup", keyUpHandler, false);
}
Edit :
Morning coffee improvements (:-)) :
- moves are smooth ( requestAnimationFrame + position += speed * time elapsed)
- keys are handled properly
- cars have a clean spawn function
- cars are now in a 'scene graph' (an array) so we can test intersection
- road !! (with roadPos, roadSpeed)
http://jsbin.com/zujecerehe/1/edit?js,output

Categories

Resources