Move multiple elements on canvas and also clear the rect - javascript

I have a problem where i have 2 object for example and i did some moving stuff using the keyboard events. Now the problem is that i don't know when to clear the canvas so i can keep multiple instances of them and also to move them individually.
const canvas = document.getElementById('canvas-game');
const context = canvas.getContext('2d');
// Set Canvas To Whole Screen
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
// Player
class Player {
constructor(xPosition = 0, yPosition = 0, height = 25, width = 25) {
this.xPosition = xPosition;
this.yPosition = yPosition;
this.height = height;
this.width = width;
this.moveEvents = this.moveEvents.bind(this);
}
draw() {
// this.clear();
let { xPosition, yPosition, height, width} = this;
context.beginPath();
context.rect(xPosition, yPosition, height, width);
context.closePath();
context.fill();
// Bind Events
this.initEvents();
}
initEvents() {
canvas.addEventListener('keydown', this.moveEvents);
}
clear() {
context.clearRect(0, 0, canvas.height, canvas.width);
}
moveEvents(event) {
let keyPressed = event.keyCode;
if (keyPressed === 38 || keyPressed === 87) {
this.yPosition -= 5;
} else if (keyPressed === 40 || keyPressed === 83) {
this.yPosition += 5;
} else if (keyPressed === 37 || keyPressed === 65) {
this.xPosition -= 5;
} else if (keyPressed === 39 || keyPressed === 68) {
this.xPosition += 5;
}
this.draw();
}
}
// Enemy
class Enemy extends Player {
constructor() {
super();
}
}
function update(...components) {
components.forEach((item) => {
item.draw();
});
}
function init() {
let player = new Player(100, 100);
let player2 = new Player(200, 200);
update(player, player2);
}
init();
It works as it is but it leaves the trail while updating. Many thanks.
Demo Here: jsFiddle

move the clear function out of the Player class (and into the same context as update) since it shouldn't be responsible for clearing the global canvas
call clear in the update function before the draw calls of the players e.g.
function update(...components) {
clear();
components.forEach((item) => {
item.draw();
});
}
replace the draw call from the moveEvents function with the global update function as you want to redraw the whole scene again after each move (as you need to clear the whole canvas)

Related

Prevent player from "flying"

I am currently trying to make an endless runner game, and I've just finished making the jumping mechanics. However, if the player were to hold the up arrow key, or press it before they touched the ground, they are able to mimic the ability to "fly". I am not sure how to prevent them from "flying" if they have not touched the ground yet. If anyone has any ideas, please let me know. My code is below:
let ctx = document.querySelector("canvas").getContext("2d");
// Screen
ctx.canvas.height = 512;
ctx.canvas.width = 512;
// Images
let bg = new Image;
bg.src = "./Background.png";
let fl = new Image;
fl.src = "./Floor.png";
// Player
let y = 256;
let speed = 2.5;
let pl = new Image;
pl.src = "./Idle.png";
pl.onload = function() {
ctx.drawImage(pl, 0, y);
};
// Jumping
let UP = false;
// Ducking
let DOWN = false;
document.onkeydown = function(e) {
if (e.keyCode == 38) UP = true;
if (e.keyCode == 40) DOWN = true;
};
document.onkeyup = function(e) {
if (e.keyCode == 38) UP = false;
if (e.keyCode == 40) DOWN = false;
};
// Frames
function update() {
// Clear
ctx.clearRect(0, 0, 512, 512);
// Background
ctx.drawImage(bg, 0, 0);
// Floor
ctx.drawImage(fl, 0, 384);
ctx.drawImage(fl, 128, 384);
ctx.drawImage(fl, 256, 384);
ctx.drawImage(fl, 384, 384);
// UP
if (UP) {
if (y > 100) {
ctx.drawImage(pl, 0, y -= speed);
} else {
UP = false;
};
} else if (!UP) {
if (y < 256) {
ctx.drawImage(pl, 0, y += speed);
} else {
ctx.drawImage(pl, 0, y);
};
};
// DOWN
if (DOWN) {
pl.src = "./Duck.png";
} else if (!DOWN) {
pl.src = "./Idle.png";
};
};
setInterval(update, 10);

Running multiple requestAnimation loops to fire multiple balls?

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).

How to stop key input from blocking the movement of an object drawn on the canvas?

So i have a simple html5 canvas render loop and I'm handling keydown and keyup.
A rectangle drawn on the screen can move left,right, up and down.
The problem is when you move left and right in succession, the rectangle seems to stop for a very long time, like it's being interrupted and I just want it to have a more smooth transition towards the opposite direction.
Even just changing any direction causes the rectangle to stop.
here's the Jsfiddle: https://jsfiddle.net/NeuroTypicalCure/sq6czebr/39/
let canvas = document.getElementById('c');
let ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
let input = {
key: null,
directions: {
up: 1.5,
down: 0.5,
left: 1,
right: 2
}
}
let player = {
x: 0,
y: 0,
direction: null,
speed: 5
}
// start
draw();
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(input.key === 'w'){
player.direction = input.directions.up;
}
if(input.key === 's'){
player.direction = input.directions.down;
}
if(input.key === 'a'){
player.direction = input.directions.left;
}
if(input.key === 'd'){
player.direction = input.directions.right;
}
// keyup -> speed 0 // else -> speed 5
if(input.key === null){
player.speed = 0;
}else{
player.speed = 5;
}
player.x += Math.cos(player.direction*Math.PI)*player.speed;
player.y += Math.sin(player.direction*Math.PI)*player.speed;
ctx.fillRect(player.x,player.y,50,50);
requestAnimationFrame(draw);
}
function handleKeyDown(e){
e.preventDefault();
input.key = e.key
}
function handleKeyUp(e){
e.preventDefault();
input.key = null;
}
window.addEventListener('keydown',handleKeyDown);
window.addEventListener('keyup',handleKeyUp);
Your problem lies with the fact that you can hold multiple keys at the same time, your logic should reflect that. i.e.:
https://jsfiddle.net/danfoord1/cr84xh2n/19/
let canvas = document.getElementById('c');
let ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
let input = {
keys: [],
directions: {
up: 1.5,
down: 0.5,
left: 1,
right: 2
}
}
let player = {
x: 0,
y: 0,
directions: [],
speed: 5
}
const directions = {
'w': 1.5,
's': 0.5,
'a': 1,
'd': 2
};
// start
draw();
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// keyup -> speed 0 // else -> speed 5
if (input.keys.length === 0) {
player.speed = 0;
} else {
player.speed = 5;
}
player.directions = input.keys.map(k => directions[k]);
player.directions.forEach(d => {
player.x += Math.cos(d * Math.PI) * player.speed;
player.y += Math.sin(d * Math.PI) * player.speed;
});
ctx.fillRect(player.x, player.y, 50, 50);
requestAnimationFrame(draw);
}
function handleKeyDown(e) {
e.preventDefault();
if (input.keys.indexOf(e.key) === -1) {
input.keys.push(e.key);
}
}
function handleKeyUp(e) {
e.preventDefault();
if (input.keys.indexOf(e.key) > -1) {
input.keys.splice(input.keys.indexOf(e.key), 1);
}
}
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
The problem with your code lies in your handling of keyup events:
function handleKeyUp(e){
e.preventDefault();
input.key = null;
}
You're basically reseting input.key whenever a key is released, no matter if it's a different key than the one that initiated the move. So if you press two keys and then release one, it will reset input.key (until your computer sent another keydown event - if you're still holding down the key). This can be fixed with a simple check if the keyup event belongs to the current input.key.
function handleKeyUp(e){
e.preventDefault();
if (e.key === input.key) input.key = null;
}
Here's a code snippet:
let canvas = document.getElementById('c');
let ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
let input = {
key: null,
directions: {
up: 1.5,
down: 0.5,
left: 1,
right: 2
}
}
let player = {
x: 0,
y: 0,
direction: null,
speed: 5
}
// start
draw();
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(input.key === 'w'){
player.direction = input.directions.up;
}
if(input.key === 's'){
player.direction = input.directions.down;
}
if(input.key === 'a'){
player.direction = input.directions.left;
}
if(input.key === 'd'){
player.direction = input.directions.right;
}
// keyup -> speed 0 // else -> speed 5
if(input.key === null){
player.speed = 0;
}else{
player.speed = 5;
}
player.x += Math.cos(player.direction*Math.PI)*player.speed;
player.y += Math.sin(player.direction*Math.PI)*player.speed;
ctx.fillRect(player.x,player.y,50,50);
requestAnimationFrame(draw);
}
function handleKeyDown(e){
e.preventDefault();
input.key = e.key
}
function handleKeyUp(e){
e.preventDefault();
if (e.key === input.key) input.key = null;
}
window.addEventListener('keydown',handleKeyDown);
window.addEventListener('keyup',handleKeyUp);
canvas{
border: 1px solid aqua;
}
<!DOCTYPE html>
<html>
<body>
<canvas id="c"></canvas>
</body>
</html>

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();

I can't get mouse input in my HTML5 game

I'm trying to get mouse input in my game; any help would be appreciated.
I call the event listeners in my init() function, and I have both my mouseMoved() and mouseClicked() functions. But I just haven't been able to get any response.
(I was asked to make a jsFiddle for this project, so here it is. It's not rendering the images, for some reason. But once there's input, there should be text on the top left the shows the mouse coordinates. Also, when you click on the canvas, you should get an alert.)
var canvasBg = document.getElementById('canvasBg');
var ctxBg = canvasBg.getContext('2d');
var canvasEntities = document.getElementById('entities');
var entitiesCtx = canvasEntities.getContext('2d');
var isPlaying = false;
var player;
var enemy;
var mouseX, mouseY;
var playerImg = new Image();
playerImg.src = 'http://placekitten.com/g/50/50';
var enemyImg = new Image();
enemyImg.src = 'http://placehold.it/50x50';
window.onload = init;
var requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
// main functions
function init() {
console.debug('init()');
player = new Entity(250, // xpos
225, // ypos
0, // xd
0, // yd
3, // speed
50, // width
50, // height
playerImg, // imgSrc
true); // player?
enemy = new Entity(500,225,0,0,1,25,25,enemyImg,false);
canvasBg.addEventListener('mousemove', mouseMoved, false);
canvasBg.addEventListener('click', mouseClicked, false);
startLoop();
}
function loop() {
// console.debug('game loop');
if(isPlaying){
update();
draw();
requestAnimFrame(loop);
}
}
function startLoop() {
isPlaying = true;
loop();
}
function stopLoop() {
isPlaying = false;
}
function clearAllCtx() {
ctxBg.clearRect(0, 0, canvasBg.width, canvasBg.height);
Entity.clearCtx();
}
function draw(){
clearAllCtx();
player.draw();
enemy.draw();
}
function update(){
player.update();
}
// end of main functions
// input handling
function mouseMoved(e) {
mouseX = e.layerX - canvasBg.offsetLeft;
mouseY = e.layerY - canvasBg.offsetTop;
document.getElementById('mouseCoors').innerHTML = 'X: ' + mouseX + ' Y: ' + mouseY;
}
function mouseClicked(e) {
alert('You clicked the mouse!');
}
// end of input handling
// Entity functions
function Entity(xpos, ypos, xd, yd, speed, width, height, imagesrc, player) {
this.xpos = xpos;
this.ypos = ypos;
this.xd = xd;
this.yd = yd;
this.speed = speed;
this.width = width;
this.height = height;
this.imagesrc = imagesrc;
this.player = player;
}
Entity.clearCtx = function(){
entitiesCtx.clearRect(0,0,canvasBg.width,canvasBg.height);
};
Entity.prototype.draw = function () {
entitiesCtx.drawImage(this.imagesrc, this.xpos, this.ypos);
};
Entity.prototype.update = function () {
this.xpos += this.xd;
this.ypos -= this.yd;
};
// end of Entity functions
So theres a few things going on, first the fiddle loading was set incorrectly, once I changed it to no wrap in body everything works.
The actual issue you're having is due to the background canvas being under the entities canvas, so it cant get any of the mouse events.
One solution would be to use pointer-events and set it to none like so pointer-events: none on the entities canvas.
Live Demo
#entities {
margin: -500px auto;
pointer-events: none;
}
Another option if you need wider browser support is to have the entities canvas capture the mouse events instead.
Live Demo of Option 2

Categories

Resources