I am trying to work on the mechanics for a game I want to work on, however I don't have much experience with this area of programming. I have gotten a circle to move smoothly left and right with the A and D keys using requestAnimationFrame(), however I have absolutely no clue what the best approach would be to make the circle "jump" so to speak.
I want the ball to be able to move upwards when W is pressed until a certain height is reached, and then begin to fall down to the ground at a certain rate. I also want to retain the ability for the ball to move left and right while jumping. I'm not exactly sure how to go about this as I have little experience with request animation frame(), I've tried cancelling the current animation frame and calling a new one within a jump function but that doesn't seem to work at all.
Here is my code so far:
var canvas,
ctx,
westX = 300,
westY = 400,
velX = 0,
velY = 0,
speed = 3,
jumpSpeed = 2,
friction = 0.98,
keys = [],
jumping = false;
window.onload = function(){
canvas = document.getElementById("gameCanvas");
ctx = canvas.getContext("2d");
drawLevel(); //intial level draw
}
function drawLevel() {
move();
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = "#c3c3d5";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(westX,westY,8,0,Math.PI*2,true);
ctx.fill();
if (!keys[87] && !keys[32])
requestAnimationFrame(drawLevel);
if (keys[87] || keys[32])
{
jump();
}
}
Character Movement:
//moves character
function move()
{
//requestAnimationFrame(move);
if (keys[65]) { //if a is pressed
if (velX > -speed) {
velX--;
}
}
if (keys[68]) { //if d is pressed
if (velX < speed) {
velX++;
}
}
velX*= friction;
westX += velX;
//checks if character is at the edge
if (westX >= canvas.width-8)
{
westX = canvas.width-8;
} else if (westX <= 8)
{
westX = 8;
}
}
Attempt at jump function:
function jump()
{
requestAnimationFrame(jump)
if (velY > -jumpSpeed) {
velY--;
}
velY*= friction;
westY += velY;
}
document.addEventListener("keydown", function(e) {
keys[e.keyCode] = true;
});
document.addEventListener("keyup", function(e) {
keys[e.keyCode] = false;
});
Also, here is a codepen link to show what I have working so far:
https://codepen.io/shanetorres/pen/OvRdjq?editors=1010
Any help or suggestions would be greatly appreciated!
You're on the right track, but the pieces are not arranged correctly.
Divide the character control in two phases: movement calculation, and animation. Think in kinematics: The character's next position is driven by its actual position vector and its current velocity vector.
In the first phase, movement calculation, you alter only the velocity. Left/right modify the horizontal component of the speed, and the jump and gravity modify the vertical component. You want full air control, so both are independent (otherwise, altering the horizontal velocity manipulation would be conditioned by the vertical position, i.e., not jumping/falling). Ignore inertia for the moment (I think that's a better name than friction IMHO). The horizontal velocity velX is either maxVelX, -maxVelX, or 0, depending on whether right, left, or none/both are pressed. The vertical velocity velY is 0 if y is below or exactly a given value (i.e., the floor); when on the floor, pressing jump will set velY to velJump, and you decrease this value a certain amount every frame (i.e., gravity. Remember that it's an acceleration, i.e., velocity over time). When the character's y is below the floor level, you set velY to 0 and y to the floor level.
Once you have the movement calculated, you apply it to the position (i.e., animate it): x += velX; y += velY; This is the second phase of the character control.
Once you have this running, you can add inertia. Horizontal velocity is increased/decreased every frame until its "target" velocity is reached. I'd remove the vertical inertia/movement dampening unless you want your player to get stuck on the air or fall like a feather. However, it's usually a good idea to limit the falling speed to a hard limit if there's a lot of verticality in your game.
This is, of course, an over-simplistic approach to physics (common in most platforming/arcade games). For realistic or more elaborate approaches, I'd consider a 2D physics engine and using forces instead of just velocities.
Do you specifically want to program the physics yourself or is building the game itself your primary goal? If you're not too interested in programming the physics, I'd recommend that you look into Phaser.io framework for writing the game. It has physics libraries included, there are tons of tutorials and it's just a good tool to for building HTML games.
Related
I'm trying to re-create a game inspired by the minigame in New Super Mario Bros. called "Wanted!". Here's the situation:
I'm trying to have a countdown timer of 10 seconds, using JavaScript. When the timer counts down, I want the timer to refresh every second that passes. But as you'll see in the picture below, the timer writes over itself. I'm trying to do this with pure JavaScript, but HTML will also work if it is a preferred method. Keep in mind that this is a still frame. Here's what I have:
function drawTimer() {
var timeLeft = 10; // Set seconds
let countDown = setInterval(function() {
// Check if we reached 0.
if (timeLeft <= 0) {
alert("Time's up!");
isWinner = false;
document.location.reload(); // reload the page
clearInterval(countDown);
}
else { // Less than 10,
requestAnimationFrame(drawTimer);
ctx.beginPath();
ctx.font = "16px Arial";
ctx.fillStyle = "#FFFFFF";
ctx.strokeStyle = "#000000";
ctx.fillText("Time left: " + timeLeft, 200, 20);
ctx.closePath();
}
timeLeft -= 1;
}, 1000);
}
Here's what it looks like:
Not sure if you understand what canvas do. Its just a "canvas", as it says, and you draw pixels.
For example, if you draw something and you want to move it somewhere, you should draw new pixels on the place where that should be moved and erase previous pixels. Its like you are drawing a circle on a paper, and wanna draw it (move it) to another place (that would be another frame), you should erase current circle and draw a new one on another place.
In your case, for showing timer text you draw pixels, each drawn pixel will stay on canvas unless you erase it at some point. You should find a way to erase it, but easier would be to have an element in html that is above canvas (absolute position) and you will not have a problems with repainting.
I am using next code to force content of canvas to follow finger touch movement on screen (drag and drop).
document.addEventListener('touchmove', function(e){
if(e.targetTouches.length ==1) {
var canavasMovex=e.targetTouches[0].pageX-canvasLeftofset-canvas.width/2;
var canavasMovey=e.targetTouches[0].pageY-canvasTopofset-canvas.height/2;
document.getElementById("a").innerHTML= canavasMovex;
document.getElementById("b").innerHTML= canavasMovey;
var delta = new fabric.Point(-canavasMovex,-canavasMovey);
canvas.absolutePan(delta);
canvas.renderAll();
}
}, false);
Note: "-canavasMovex,-canavasMovey" values have the negative sign (I had to use it because otherwise the canvas was moving opposite to finger moment(I think you can ignore this fact and that it has noting to do with offset, which I am trying to solve asking for help here))
I have displacement of position proportional to the distance of touch point from the centre of canvas. If I start the movement pressing from the centre of screen, then displacement is not so notable. But if I start from top corner (for example) I get centre of canvas there in that touch point. From attached drawing I was trying with taking of consideration of "d" parameter (distance of touching point to the centre of canvas), to find right value for the equation for the canvas.absolutePan() function, but no success. Can you help me with this case,pleas? I was trying to use some solutions from browsing the internet to find the way to move canvas content. But then I had lost some of functions of my app because "new" libraries were not interacting correctly with my existing fabric.min.js library. Image info(external rectangle-web page,internal rectangle-canvas): a-canvas offset,b-distance to centre of canvas,c- touch point,d-distance from touch point to centre of canvas. So on the end I am trying to get to accomplish this task in this way.
Malfunction:
Bigger than distance "d" is on starting of dragging , bigger is then the offset. (actuary if I start dragging from canvas corner, canvas content transfers its centre there)
The solution is to get the finger position, calculate the difference to previous position, add difference to the previous position and then finally do the panning.
var canvasMovexoold = 0;
var canvasMoveyoold = 0;
document.addEventListener('touchstart', function(e){
canvasMovexoold = canvasLeftofset+canvas.width/2-e.targetTouches[0].pageX;
canvasMoveyoold =canvasTopofset+canvas.height/2- e.targetTouches[0].pageY;
}, false);
document.addEventListener('touchmove', function(e){
if(e.targetTouches.length ==1) {
var canvasMovexo += canvasLeftofset+canvas.width/2-e.targetTouches[0].pageX-canvasMovexoold ;
var canvasMoveyo +=canvasTopofset+canvas.height/2- e.targetTouches[0].pageY-canvasMoveyoold ;
var delta = new fabric.Point(canvasMovexo,canvasMoveyo);
canvas.absolutePan(delta);
canvasMovexoold = canvasLeftofset+canvas.width/2-e.targetTouches[0].pageX;
canvasMoveyoold =canvasTopofset+canvas.height/2- e.targetTouches[0].pageY;
}
}, false);
I am currently experimenting with parallax effect that i am planning to implement to my HTML5-canvas game engine.
The effect itself is fairly easy to achieve, but when you add zooming and rotating, things get a little more complicated, at least for me. My goal is to achieve something like this:Youtube video.
As you can see, you can zoom in and out "to the center", and also rotate around it and get the parallax effect.
In my engine i want to have multiple canvases that are going to be my parallax layers, and i am going to translate them.
I came up with something like this:
var parallax = {
target: {
x: Mouse.x,
y: Mouse.y
},
offset: {
x: -ctx.width / 2,
y: -ctx.height / 2
},
factor: {
x: 1,
y: 1
}
}
var angle = 0;
var zoomX = 1;
var zoomY = 1;
var loop = function(){
ctx.canvas.width = ctx.canvas.width; //Clear the canvas.
ctx.translate(parallax.target.x * parallax.factor.x, parallax.target.y * parallax.factor.y);
ctx.rotate(angle);
ctx.scale(zoomX, zoomY);
ctx.translate((-parallax.target.x - parallax.offset.x) * parallax.factor.x, (-parallax.target.y - parallax.offset.y) * parallax.factor.y);
Draw(); //Function that draws all the objects on the screen.
}
This is a very small and simplified part of my script, but i hope that's enough to get what i am doing. The object "parallax" contains the target position, the offset(the distance from the target), and the factor that is determining how fast the canvas is moving away relatively to the target. ctx is the canvas that is moving in the opposite direction of the target.(In this example i am using only one layer.) I am using the mouse as the "target", but i could also use the player, or some other object with x and y property. The target is also the point around which i rotate and scale the canvas.
This method works completely fine as long as the factor is equal to 1. If it is something else, the whole thing suddenly stops working correctly, and when i try to zoom, it zooms to the top-left corner, not the target. I also noticed that if i zoom out too much, the canvas is not moving in the opposite way of the target, but in the same direction.
So my question is: What is the correct way of implementing parallax with zooming and rotating?
P.S. It is important to me that i am using canvases as the layers.
To prepare for the next animation frame, you must undo any previous transforms in the reverse order they were executed:
context.translate(x,y);
context.scale(sx,sy);
context.rotate(r);
// draw stuff
context.rotate(-r);
context.scale(-sx,-sy);
context.translate(-x,-y);
Alternatively, you can use context.save / context.restore to undo the previous transforms.
Adjust your parallax values for the current frame,
Save the un-transformed context state using context.save(),
Do your transforms (translate, scale, rotate, etc),
Draw you objects as if they were in non-transformed space with [0,0] at your translate point,
Restore your context to it's untransformed state using context.restore()/
Either way will correctly give you a default-oriented canvas to use for your next animation frame.
The exact parallax effects you apply are up to your own design, but using these methods will make the canvas return to a normal default state for you to design with.
as you can see in this JSFiddle : http://jsfiddle.net/EychPixels/pABPv/1/ , when you move the character using the arrow keys until the view area moves with it, the character seems to move back 1 tile then forth 1 tile. Is there a way to fix this like animating the view area with the character? For animating the character I used .animate with jQuery, is it possible to do the same thing with the view area? If so can I see an example of this using my code?
If you need any more detail to answer the question just ask.
For your current code, the path of least resistance (i.e. the way that will need to change less code) would be to pass now to the draw function during an animation, if the viewport should animate too. Then you can follow #jimjimmy1995's suggestion (undoing the player.x change and doing an opposite world.x change).
This fiddle offers a crude example (only for the "down" key). Details:
Determine whether or not the viewport will move during the animation:
var move = Math.round(playerY+1) - Math.floor(0.5 * vHeight) > vY;
Pass the now function to draw (only if the viewport will move):
step: function(now) {
playerY = now;
draw(move ? now-start : 0);
}
In the draw adjust the viewport and player positions:
function draw(now) {
if ( !now )
now = 0;
...
theY = (y-now) * 32;
...
context.fillRect((playerX-vX)*32, (playerY-vY-now)*32, 32, 32);
Draw an "extra" tile, to compensate for the blank space (not implemented in the example);
When the animation ends, draw it again, with now == 0 (not implemented in the example).
There are still a few glitches, but should serve as a base for future improvements.
I'm writing a 2D game in html5 using Canvas which requires mouse click and hover events to be detected. There are 3 problems with this: detections must be pixel-perfect, objects are not rectangular (houses, weird-shaped UI buttons...), and it is required to be fast and responsive. (Obviously brute force is not an option)
So what I want to ask is how do I find out which object the mouse is on, and what are the possible optimizations.
P.S: I did some investigation and found a guy who used QuadTree here.
I have a (dated) tutorial that explains the concept of a ghost canvas which is decent for pixel-perfect hit detection. The tutorial is here. Ignore the warning about a newer tutorial, the newer one does not use the ghost canvas concept.
The idea is to draw the image in question to an in-memory canvas and then use getImageData to get the single pixel of the mouse click. Then you see if that single pixel is fully transparent or not.
If its not fully transparent, well, you've got your target.
If it is fully transparent, draw the next object to the in-memory canvas and repeat.
You only have to clear the in-memory canvas at the end.
getImageData is slow but it is your only option if you want pixel-perfect hit detection and aren't pre-computing anything.
Alternatively you could precompute a path or else an array of pixels with an offset. This would be a lot of work but might be faster. For instance if you have a 40x20 image with some transparency you'd compute an array[40][20] that would have true or false corresponding to transparent or not. Then you'd test that against the mouse position, with some offset, if the image is drawn at (25, 55) you'd want to subtract that from the mouse position and then test if the new position is true when you look at array[posx][posy].
That's my answer to your question. My Suggestion? Forget pixel-perfect detection if this is a game.
Seriously.
Instead make paths (not in canvas, in plain javascript code) that represent the objects but are not pixel perfect, for instance a house might be a square with a triangle on the top that is a very close approximation of the image but is used in its stead when it comes to hit testing. It is comparatively extremely fast to compute if a point is inside a path than it is to do pixel-perfect detection. Look up point in polygon winding number rule detection. That's your best bet, honestly.
The common solution in traditional game development is to build a click mask. You can re-render everything onto a separate off-screen canvas in a solid color (the rendering should be very quick). When you want to figure out what was clicked on, you simply sample the color at the x/y co-ordinate on the off-screen canvas. You end up building a color-->obj hash, akin to:
var map = {
'#000000' : obj1
, '#000001' : obj2
, ...
};
You can also optimize the rendering to the secondary canvas to only happen when the user clicks on something. And using various techniques, you can further optimize it to only draw the part of the canvas that the user has clicked on (for example, you can split you canvas into an NxN grid, e.g. a grid of 20x20 pixel squares, and flag all of the objects in that square -- you'd then only need to re-draw a small number of objects)
HTML5 Canvas is just a drawing plane, where you can set different transforms before calling each drawing API function. Objects cannot be created and there is no display list. So you have to build these features yourself or you can use different libraries available for this.
http://www.kineticjs.com/
http://easeljs.com/
A few months before I got interested in this and even wrote a library for this purpose. You can see it here : http://exsprite.com. Ended up facing a lot of performance issues, but because of lack of time I couldn't optimize it. It was really interesting, so waiting for some time to make it perfect.
I believe the comments should suffice. This is how I determine user intention in my 2d isometric scroller, currently located at http://untitled.servegame.com
var lastUp = 0;
function mouseUp(){
mousedown = false; //one of my program globals.
var timeNow = new Date().getTime();
if(mouseX == xmouse && mouseY == ymouse && timeNow > lastUp + 100){//if it was a centralized click. (mouseX = click down point, xmouse = mouse's most recent x) and is at least 1/10th of a second after the previous click.
lastUp = new Date().getTime();
var elem = document.elementFromPoint(mouseX, mouseY); //get the element under the mouse.
var url = extractUrl($(elem).css('background-image')); // function I found here: http://webdevel.blogspot.com/2009/07/jquery-quick-tip-extract-css-background.html
imgW = $("#hiddenCanvas").width(); //EVERY art file is 88px wide. thus my canvas element is set to 88px wide.
imgH = $(elem).css('height').split('p')[0]; //But they vary in height. (currently up to 200);
hiddenCanvas.clearRect(0, 0, imgW, imgH); //so only clear what is necessary.
var img = new Image();
img.src = url;
img.onload = function(){
//draw this elements image to the canvas at 0,0
hiddenCanvas.drawImage(img,0,0);
///This computes where the mouse is clicking the element.
var left = $(elem).css('left').split('p')[0]; //get this element's css absolute left.
var top = $(elem).css('top').split('p')[0];
offX = left - offsetLeft; //left minus the game rendering element's absolute left. gives us the element's position relative of document 0,0
offY = top - offsetTop;
offX = mouseX - offX; //apply the difference of the click point's x and y
offY = mouseY - offY;
var imgPixel = hiddenCanvas.getImageData(offX, offY, 1, 1); //Grab that pixel. Start at it's relative X and it's relative Y and only grab one pixel.
var opacity = imgPixel.data[3]; //get the opacity value of this pixel.
if(opacity == 0){//if that pixel is fully transparent
$(elem).hide();
var temp = document.elementFromPoint(mouseX, mouseY); //set the element right under this one
$(elem).show();
elem = temp;
}
//draw a circle on our hiddenCanvas so when it's not hidden we can see it working!
hiddenCanvas.beginPath();
hiddenCanvas.arc(offX, offY, 10, 0, Math.PI*2, true);
hiddenCanvas.closePath();
hiddenCanvas.fill();
$(elem).css("top", "+=1"); //apply something to the final element.
}
}
}
In conjunction with this:
<canvas id="hiddenCanvas" width="88" height="200"></canvas>
Set the CSS positioning absolute and x = -(width) to hide;