I'm trying to make a simple maze navigation game using an HTML5 canvas, where the player character stays in the center of the screen and the maze moves around them.
The maze is represented using a 2D array of tile objects, so my first approach to drawing was something like:
for(var row=frameStart; row<=frameEnd; row++) {
for(var col=frameStart; col<=frameEnd; col++)
maze[row][col].draw();
}
}
...where frameStart and frameEnd are computed each call to the paint method in order to avoid drawing parts of the maze that aren't visible. Anyway, this was a bit too slow for me, so I decided to save the whole maze into an image using
var img = new Image();
img.src = canvas.toDataURL();
So now, I'm drawing the maze just one time, saving it into an image, and from then on just drawing a part of that saved image every frame using ctx.drawImage(img, ...) instead of looping through lots of tile elements and drawing each individually.
However, I found that this method is not noticeably faster, and I'm at a loss for how I can increase the performance any further, and am left wondering why my image idea didn't increase the performance. It currently takes longer than I'd like to render each frame. I currently have the game locked into rendering every single frame so I can easily see how long it takes to do so based on how fast the player character moves.
Improve rendering
Looking at your game I found it ran just fine on my machine.
Use requestAnimationFrame
But looking at your code you have implemented the rendering in a bad way.
You are placing renders back to back with
... cut from bottom of function reactToUserInput
if(needsRedraw) {
// Redraw canvas with interpolation
drawMaze(true, oldLocation, 0);
return; // drawMaze will call this function when it's done
}
}
drawMaze();
setTimeout(reactToUserInput, 0);
in function reactToUserInput
And in drawMaze()
// Either continue interpolation, recall user input function, or stop entirely
if (interpolate) {
if (interpOffset.mag == 0) {
reactToUserInput();
} else {
setTimeout(function () {
drawMaze(true, oldUserLocation, recurseCount + 1);
}, 0);
}
return;
}
This can for some hardware setups cause you avoidable slowdowns.
Use requestAnimationFrame to render the frame every 1/60th second. This will also throttle your game on fast machines as i can not see any time controlled movement. It will also help the rendering on machines with low GPU RAM and GPU power better manage the GPU state as now you are forcing state changes when not needed.
Any rendering done at over 60fps just will not be seen by the user, so avoid needless rendering.
Why is preRendering not helping
Then I ran a profile on your game and the results show that indeed the drawCell function is the bottleneck. The call to ctx.drawImage in drawCell accounts for 32% of your overall processing. (but this value is misleading as you are constantly rendering)
The reason your rendering is not improved by prerendering is that you are asking too much of the GPU. The maze as I saw it is 150 tiles by 150 tiles with each cell being 65 by 65 pixels. That makes the dimension of the whole maze 9750 by 9750 pixels consuming a total of 380.25MB of GPU memory that you are sharing with the page, the OS and any other process that is happening. Only the very top end machines will be happy to handle that amount of RAM, but the rest will be frantically paging it from system RAM causing the slowdown (compounded by the constant rendering).
Rule of thumb about images sizes on the canvas. Never try to use images greater than 4 times the longest resolution of the device. Devices are tuned to the resolution of the display and will happily deal with images that are near that resolution. Go over that and you exceed the capabilities of the hardware.
How to fix and get a good frame rate.
Looking at the game there is no reason why it should not be running at 60fps on all devices.
Use requestAnimationFrame to time the rendering and user input (nobody can toggle a key at over 60hz and your game does not demand instant reaction).
Reduce pixel memory usage. Your tiles are 65 by 65 which means that in the gpu the till image occupies 128 by 128 pixels in memory. Search "rendering powers of two" to find out why.
Change the cell resolution to 64 by 64.
Rather than pre render the whole scene create a offscreen canvas that is the play area size plus 2 cells. So if the number of cells to draw is 32 by 32 then create a canvas that is 34 by 34 cells. On first render draw all the 34 by 34 cells to that canvas. Then draw that canvas to follow the player, when the play get to a point and there are no cells along the edge that you are moving to, copy the canvas onto itself to make room for new cells in the direction of travel, then render the row or column of missing cells.
// playfield is the offscreen canvas with .ctx as it context
playfield.ctx = playfield.getContext("2d");
// to move one cell up in the playfield
playfield.ctx.drawImage(playfield,
0, cellSize, playfield.width, playfield.height-cellSize,
0,0,playfield.width, playfield.height-cellSize
)
// then draw the missing bottom row of cells only
// then just draw the playfield to the onscreen canvas
ctx.drawImage(playfield, mazeLeft, mazeTop)
It will take a bit of a rewrite but will get your game running very smoothly and get you back to concentrating on game play rather than performance.
Related
I am new to Phaser and I am currently having a hard time in generating a tilemap with the aid of the phaser isometric plugin. I am also having trouble with understanding some of the concepts related with the Phaser world, game, and the camera which make the part of generating the tilemap correctly even harder. I have actually noticed that this problem seems to be an obstacle to Phaser newbies, like myself, and having it correctly explained would certainly help to fight this.
Getting to the matter:
I have generated a tilemap using a for loop using single grass sprites. The for loop is working correctly and I also implemented a function where I can specify the number of tiles I want to generate.
{
spawnBasicTiles: function(half_tile_width, half_tile_height, size_x, size_y) {
var tile;
for(var xx = 0; xx < size_x; xx += half_tile_width) {
for(var yy = 0; yy < size_y; yy += half_tile_height) {
tile = game.add.isoSprite(xx, yy, 0, 'tile', 0, tiles_group);
tile.checkWorldBounds = true;
tile.outOfBoundsKill = true;
tile.anchor.set(0.5, 0);
}
}
}
}
So the process of generating the static grass tiles is not a problem. The problem, and one of the reasons I am trying to getting the object pooling to work correctly is when the tiles number is superior to 80 which has a dramatic impact on the game performance. Since I aim for making HUGE, auto-generating maps that are rendered according to the player character position the object pooling is essential. I have created a group for these tiles and added the properties I thought that would be required for having the tiles that are out of bounds to not be rendered(physics, world bounds...). However, after many attempts I concluded that even the tiles that are out of bounds are still being generated. I also used another property rather than add.isoSprite to generate the tiles, which was .create but made no difference. I guess no tiles are being "killed".
Is this process correct? What do I need to do in order to ONLY generate the tiles that appear on camera (I assume the camera is the visible game rectangle) and generate the rest of them when the character moves to another area assuming the camera is already tracking the character movement?
Besides that, I am looking to generate my character in the middle of the world which is the middle of the generated grass tilemap. However, I am having a hard time doing that too. I think the following concepts are the ones I should play with in order to achieve that, despite not being able to:
.anchor.set()
game.world.setBounds(especially this one... it doesn't seem to set where I order to);
Phaser.game ( I set its size to my window.width and window.height, not having much trouble with this...)
Summing up:
Using the following for loop method of generating tiles, how can I make infinite/almost infinite maps that are generated when the camera that follows my character moves to another region? And besides that, what is the proper way of always generating my character in the middle of my generated grass map and also for the camera to start where the character is?
I will appreciate the help a lot and hope this might be valuable for people in similar situations to mine. For any extra information just ask and if you want the function for generating the tiles by a specific number I will gladly post it. Sorry for any language mistakes.
Thank you very much.
EDIT:
I was able to spawn my character always in the middle of the grass by setting its X and Y to (size/width) and (size/height). Size is the measure of X and Y in px.
I'm creating html5 paint application and I'm currently working on blending layers. I'm wondering which of the approaches would be the best (fastest and gimp/photoshop like) to build in such program. My layers (canvases) are stacked.
Changing blend mode by CSS3 property (probably very fast - blending directly on graphics card)
Having hidden canvases (layers) and one canvas to show flattened image to user. So we draws on these hidden canvases and there is some mechanism which taking each of hidden canvas and draw it to user viewable canvas (probably slower but each context.drawImage(...) is optimized and computed on graphics card)
Hidden canvases (layers) are truly virtual. There is no hidden canvases elements at all. Instead there is some structure in memory which imitate canvas. These structure just saving user actions performed on this virtual layer. Then when repainting is required, user canvas is reconstructed by taking each operations from each virtual layers and paint it (true paint). Operations must be correctly ordered is such virtual layer structure (this is probably slow but may be faster than 2nd approach(we don't wasting time to draw anything on layer, just storing options how we will draw on real layer)?)
Blending by WebGL(probably fast)
Compute manually each pixel and show score to user (super slow?)
I'm using second approach but i'm not really sure it's the best (especially for more layers). I was wondering do you know any other approach, maybe you would suggest what would be better to implement blending operations to make it work as in Gimp/Adobe Photoshop
My way to implement this was to have multiple canvases, each of them store it's own blending mode. But it's just virual canvas (not drawable for user). Then there is 33 times per second timer (made by RequestAnimationFrame) which grab all this canvases, flatten them and draw it to viewport. For each layer I perform blend mode change on viewport canvas. It works perfect since drawImage(...) is computed on GPU and it's really fast.
I think code is fairly easy to understand and to apply in own solutions:
Process(lm: dl.LayerManager) {
var canvasViewPort = lm.GetWorkspaceViewPort();
var virtualLayers = lm.GetAllNonTemporary();
canvasViewPort.Save();
canvasViewPort.Clear();
canvasViewPort.SetBlendMode(virtualLayers[0].GetBlendMode());
canvasViewPort.ApplyBlendMode();
canvasViewPort.DrawImage(virtualLayers[0], 0, 0, canvasViewPort.Width(), canvasViewPort.Height(), 0, 0, canvasViewPort.Width(), canvasViewPort.Height());
for (var i = 1; i < virtualLayers.length; ++i) {
var layer = virtualLayers[i];
var shift = other.GetShift();
canvasViewPort.SetBlendMode(virtualLayers[i].GetBlendMode());
canvasViewPort.ApplyBlendMode();
canvasViewPort.DrawImage(layer, 0, 0, layer.Width(), layer.Height(), shift.x, shift.y, layer.Width(), layer.Height());
}
canvasViewPort.Restore();
}
Don't be affraid of this code. It maintains underlaying canvas, pure CanvasRenderingContext2D.
I will perform such optimization: I Won't redraw canvasViewPort until something changes on any virtual canvas.
Next optimization I would perform: Get selected layer, cache all before and all after current layer.
So it will looks like:
CurrentLayer: 3 (which might be affected by some tool)
[1]:L1
[2]:L2
[3]:L3
[4]:L4
[5]:L5
[6]:L6
Draw to temporary with appropiate blend mode [1, 2] to tmp1 and [4, 5, 6] to tmp2.
When something change on third layer, I just need to redraw tmp1 + [3] + tmp2.
So it's just only 2 iteration's. Super fast.
I was wondering if its possible to create an image using various lines and bez-curves that appears and then moves down the canvas and gets larger as it grows. The animation in question requires the room to fill with gas and so the the gas gets greater and greater as it moves around the canvas.
I had thought about just drawing the image and then using a for loop to move the image down until a certain y coordinate, but this doesnt help with the increasing part of the created image
Any help appreciated
There are different ways to make animations happen. What I normally do for my canvas games is that I have a gameloop that loops at a certain FPS that I decide at the start of my project. Each animation is normally controlled by time an speed.
var fps = 60;
var lastUpdateTime = +new Date(); //when did I last update the game?
function gameloop() {
var updateStartTime = +new Date();
update(updateStartTime-lastUpdateTime); //update the game for the according to the elapsed time since last update
lastUpdateTime = updateStartTime;
//Normally I also handle spawning stuff here
//I also remove old object in my gameloop
setTimeout(gameloop,1000/fps)
}
function update(elapsedTime) {
//This function update the locations of game elements such as player position, bullets, enemies, bananas etc. (or even gas clouds!)
//When I change a value, I use the time-parameter passed along with the call so that I get a smooth game even though the browser might lag.
playerX += velocity*timeElapsed; //as an example
}
Here's an example on your problem where I made a function to create a gas bubble object (yes, I do know how wrong it is to use gas bubbles =P). These objects travel downwards (physics?!) and increase in size (OK, this is starting to sound more and more crazy), just have a look:
http://jsfiddle.net/Niddro/ppa4xuw8/
I have a script which takes a picture and creates black and white R, G and B channels. And I would like to count how many channels have something on them (black = no colour, white = colour data).
It works by going through every single pixel and saving data in 3 different canvas elements. Each one showing different channel.
It works great but when I give a Red and Blue picture only, the green channel (obviously) is completely black.
So my question is, how do I check whether the canvas is completely black? I would like to avoid looping through every pixel in every channel to see if it's all black.
cheers
Ok I've tested a few things and the easiest solution I could come up with was creating a 1x1 offscreen canvas, then drawing either your image or your original canvas you want to check for data on it, scaled down to 1 pixel and check it (I also added basic caching).
It's not a precise check, and the result depends on how the pixel data is distributed in the original image, so as GameAlchemist noted, you may loop through all the pixels anyway if the result of the check is 0 if you want accurate results.
function quickCheck(img){
var i = arguments.callee; //caching the canvas&context in the function
if(!i.canvas){i.canvas = document.createElement("canvas")}
if(!i.ctx) {i.ctx = i.canvas.getContext("2d")}
i.canvas.width=i.canvas.height = 1;
i.ctx.drawImage(img,0,0,1,1);
var result = i.ctx.getImageData(0,0,1,1);
return result.data[0]+result.data[1]+result.data[2]+result.data[3];
}
You must iterate the canvas's pixel data--no magic method to avoid that.
The performance cost of iterating through 2 images is trivial (2 images = your front image and back image).
[ Removed un-necessary part of answer after visiting questioner's site ]
Another quick check would be to calculate the sum of the image data. If the sum is equal to the amount of pixels in the canvas times 255, it is probably all black. There is a small chance that some other combination of pixels could cause the sum to be equal to the same number, but the chance is very low (unless you are looping through all the canvas combinations).
I want to check a collision between two Sprites in HTML5 canvas. So for the sake of the discussion, let's assume that both sprites are IMG objects and a collision means that the alpha channel is not 0. Now both of these sprites can have a rotation around the object's center but no other transformation in case this makes this any easier.
Now the obvious solution I came up with would be this:
calculate the transformation matrix for both
figure out a rough estimation of the area where the code should test (like offset of both + calculated extra space for the rotation)
for all the pixels in the intersecting rectangle, transform the coordinate and test the image at the calculated position (rounded to nearest neighbor) for the alpha channel. Then abort on first hit.
The problem I see with that is that a) there are no matrix classes in JavaScript which means I have to do that in JavaScript which could be quite slow, I have to test for collisions every frame which makes this pretty expensive. Furthermore I have to replicate something I already have to do on drawing (or what canvas does for me, setting up the matrices).
I wonder if I'm missing anything here and if there is an easier solution for collision detection.
I'm not a javascript coder but I'd imagine the same optimisation tricks work just as well for Javascript as they do for C++.
Just rotate the corners of the sprite instead of every pixel. Effectively you would be doing something like software texture mapping. You could work out the x,y position of a given pixel using various gradient information. Look up software texture mapping for more info.
If you quadtree decomposed the sprite into "hit" and "non-hit" areas then you could effectively check to see if a given quad tree decomposition is all "non-hit", "all hit" or "possible hit" (ie contains hits and non-hit pixels. The first 2 are trivial to pass through. In the last case you then go down to the next decomposition level and repeat the test. This way you only check the pixels you need too and for large areas of "non-hit" and "hit" you don't have to do such a complex set of checks.
Anyway thats just a couple of thoughts.
I have to replicate something I already have to do on drawing
Well, you could make a new rendering context, plot one rotated white-background mask to it, set the compositing operation to lighter and plot the other rotated mask on top at the given offset.
Now if there's a non-white pixel left, there's a hit. You'd still have to getImageData and sift through the pixels to find that out. You might be able to reduce that workload a bit by scaling the resultant image downwards (relying on anti-aliasing to keep some pixels non-white), but I'm thinking it's probably still going to be quite slow.
I have to test for collisions every frame which makes this pretty expensive.
Yeah, I think realistically you're going to be using precalculated collision tables. If you've got space for it, you could store one hit/no hit bit for every combination of sprite a, sprite b, relative rotation, relative-x-normalised-to-rotation and relative-y-normalised-to-rotation. Depending on how many sprites you have and how many steps of rotation or movement, this could get rather large.
A compromise would be to store the pre-rotated masks of each sprite in a JavaScript array (of Number, giving you 32 bits/pixels of easily &&-able data, or as a character in a Sring, giving you 16 bits) and && each line of intersecting sprite masks together.
Or, give up on pixels and start looking at eg. paths.
Same problem, an alternative solution. First I use getImageData data to find a polygon that surrounds the sprite. Careful here because the implementation works with images with transparent background that have a single solid object. Like a ship. The next step is Ramer Douglas Peucker Algorithm to reduce the number of vertices in the polygon. I finally get a polygon of very few vertices easy and cheap to rotate and check collisions with the other polygons for each sprite.
http://jsfiddle.net/rnrlabs/9dxSg/
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var img = document.getElementById("img");
context.drawImage(img, 0,0);
var dat = context.getImageData(0,0,img.width, img.height);
// see jsfiddle
var startPixel = findStartPixel(dat, 0);
var path = followPath(startPixel, dat, 0);
// 4 is RDP epsilon
map1 = properRDP(path.map, 4, path.startpixel.x, path.startpixel.y);
// draw
context.beginPath();
context.moveTo(path.startpixel.x, path.startpixel.x);
for(var i = 0; i < map.length; i++) {
var p = map[i];
context.lineTo(p.x, p.y);
}
context.strokeStyle = 'red';
context.closePath();
context.stroke();