Making Javascript TileEngine Scrollable in Canvas - javascript
The basis for the code is from John E. Graham's blog http://johnegraham2.com/blog/2010/09/25/project-javascript-2d-tile-engine-with-html5-canvas-part-4-using-zones-for-further-optimization/
It works perfectly for drawing a screen's worth of tiles, but I cannot for the life of me figure out how to adjust it 1 row/column at a time based on pressing up, down, left, or right keys.
Here is an example with the transparency to help visualize the zones http://simplehotkey.com/Javascript/canvas.html (loading positions of 1,188 tiles but only draws a couple hundred to fill the screen) I had it loading an array with 70,000 entries and it was still quick because it's only drawing whats on the screen, but cannot figure out how to slide everything based on input...
I've come up with a couple ideas and am not sure what's the best way.
One screen worth of tiles is shown here:
tilesArray = [
0,0,0,0,0,0,0,1,2,9,6,0,0,7,0,0,1,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,9,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,9,0,
0,9,9,9,9,9,9,0,7,2,0,0,0,0,0,1,2,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,9,0,7,2,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
];
Where 0 is a wall tile (around perimeter), 9 a floor tile, 7 a door and a couple other random tiles.
That is exactly what is loaded to the screen, but I cannot figure out how to shift everything 1 tile in either direction based on input, up, down, left, right.
The one idea I'm leaning towards now, is just to use that array above as the basis for rendering, and somehow feeding the new values into it based on keyboard input, from another array. Maybe slicing from another, much larger array (holding all the tiles for the entire level) and using that slice to populate the array that's actually rendered???
That's replacing every tile every frame though...
for getting player input I was using:
//Key listener
document.onkeydown = function(e){
e = e?e:window.event;
console.log(e.keyCode + "Down");
switch (e.keyCode){
case 38:
//UP KEY
Game.inputReaction('up');
//Game.moveDir('up');
break;
case 40:
//DOWN KEY
//Game.inputReaction(40);
//Game.moveDir('down');
break;
case 37:
//Left Key
//Game.inputReaction(37);
break;
}
}
The other alternative is to try to adjust the tiles already on the screen and add new tiles but this engine isn't using global variables so I'm not sure how to affect the tile engine programatically based on input....like I can add another method (inputReaction(num)) and trigger some actions from my keyboard input (console.log()) but I can't access the other methods actually drawing the tiles. Or maybe I have to make a copy of the object, change it and return it? but it's pretty complex.
I think it might be easier to adjust the array values that are being fed into the "engine" (array above) rather than changing around how the engine is calculating what's being drawn. Can you confirm this?
Add a camera abstraction that you can move around on the map, then shift the drawing positions according to the camera position. When the camera moves south 10px, all tiles move north 10px, same with east and west. Since you only draw the tiles that are visible, there won't be much of a performance loss.
The renderer looks at the camera to figure out what needs to be drawn and you can expose the camera object to the outside to manipulate it. That way you only need to change the camera position to change what is shown on the screen.
I did this in a proof of concept tiling engine a year ago and I was able to smoothly scroll and scale huge tilemaps.
If you start changing the array itself, your performance will suffer and you won't be able to scroll smoothly since you can only go in steps of one tile and not one pixel.
Related
HTML5 canvas - How to clear and redraw regions with overlapping entities?
Up until now, I've been clearing/redrawing everything on a single canvas at each animation frame, which is naturally very expensive. I've been leaning towards the idea of using multiple canvases, each representing a different layer in the game, and only redrawing regions of the canvas that have changed. This has the potential to drastically reduce the amount of clearing and redrawing in the game (and subsequently improve performance), however I'm foreseeing a problem to this. Splitting the game components into layers is fine, however if there is a layer with a group of entities that move around, there will be times when one mobile entity overlaps an immobile one. As the immobile one hasn't changed, it is not scheduled for a redraw. The part of the immobile entity's image that was cleared by the mobile entity's movement then never gets redrawn (see below images). Black stationary background layer + Foreground layer with moving entities. The white entity is moving, but the yellow one isn't. As the yellow entity isn't moving, it is generally safe to say that the pixels in its region have not changed. However the white entity's movement causes some of those pixels to be cleared. Here are the solutions I have come up with: 1) Like before, clear and redraw the entire canvas, but only for the layer with moving entities. In my game 1 of 4 layers fits into this category. 2) Calculate when objects overlap, and clear/redraw both regions in these situations. 3) Move the stationary entities temporarily to a background layer and process as normal. Comments for each solution: 1) This solution feels like it will not have enough of a performance gain to warrant the redesign. 2) This feels like a more efficient approach, but still requires testing among all entities to see if their borders collide. If the number of entities is great enough, this would have to be further improved by splitting the canvas into regions and processing each region individually. 3) This approach gives me the desirable result of drawing moving entities over stationary ones, but is it as efficient as solution 2? Are there any other potentially better solutions? As requested by Blindman67, here is my draw code: Sprite.prototype.draw = function() { if (this.hasImage()) { if (this.opacity == 1) { this.drawImage() } else { game.ctx.globalAlpha = this.opacity; this.drawWithTransparency(); game.ctx.globalAlpha = 1; } } } Sprite.prototype.drawImage = function() { game.ctx.drawImage( this.image, this.position.x, this.position.y, this.image.width, this.image.height ); } I clear the canvas once at the beginning of every update.
How to calculate the index of the tile underneath the mouse in an isometric world taking into account tile elevation
I have a tile-based isometric world and I can calculate which tile is underneath specific (mouse) coordinates by using the following calculations: function isoTo2D(pt:Point):Point{ var tempPt:Point = new Point(0, 0); tempPt.x = (2 * pt.y + pt.x) / 2; tempPt.y = (2 * pt.y - pt.x) / 2; return(tempPt); } function getTileCoordinates(pt:Point, tileHeight:Number):Point{ var tempPt:Point = new Point(0, 0); tempPt.x = Math.floor(pt.x / tileHeight); tempPt.y = Math.floor(pt.y / tileHeight); return(tempPt); } (Taken from http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511, this is a flash implementation but the maths is the same) However, my problem comes in when I have tiles that have different elevation levels: In these scenarios, due to the height of some tiles which have a higher elevation, the tiles (or portions of tiles) behind are covered up and shouldn't be able to be selected by the mouse, instead selecting the tile which is in front of it. How can I calculate the tile by mouse coordinates taking into account the tiles' elevation? I'm using a javascript and canvas implementation.
There is a technique of capturing object under the mouse on a canvas without needing to recalculate mouse coordinates into your "world" coordinates. This is not perfect, has some drawbacks and restrictions, yet it does it's job in some simple cases. 1) Position another canvas atop of your main canvas and set it's opacity to 0. Make sure your second canvas has the same size and overlaps your main one. 2) Whenever you draw your interactive objects to the main canvas, draw and fill the same objects on the second canvas, but using one unique color per object (from #000000 to #ffffff) 3) Set mouse event handling to the second canvas. 4) Use getPixel on the second canvas at mouse position to get the "id" of the object clicked/hovered over. Main advantage is WYSIWYG principle, so (if everything is done properly) you can be sure, that objects on the main canvas are in the same place as on the second canvas, so you don't need to worry about canvas resizing or object depth (like in your case) calculations to get the right object. Main drawback is need to "double-render" the whole scene, yet it can be optimized by not drawing on the second canvas when it's not necessary, like: in "idling" scene state, when interactive objects are staying on their places and wait for user action. in "locked" scene state, when some stuff is animated or smth. and user is not allowed to interact with objects. Main restriction is a maximum number of interactive objects on the scene (up to #ffffff or 16777215 objects). So... Not reccomended for: Games with big amount of interactive objects on a scene. (bad performance) Fast-paced games, where interactive objects are constantly moved/created/destroyed.(bad performance, issues with re-using id's) Good for: GUI's handling Turn-based games / slow-paced puzzle games.
Your hit test function will need to have access to all your tiles in order to determine which one is hit. It will then perform test hits starting with the tallest elevation. Assuming that you only have discreet (integer) tile heights, the general algorithm would be like this (pseudo code, assuming that tiles is a two-dimensional array of object with an elevation property): function getTile(mousePt, tiles) { var maxElevation = getMaxElevation(tiles); var minElevation = getMinElevation(tiles); var elevation; for (elevation = maxElevation; elevation >= minElevation; elevation--) { var pt = getTileCoordinates(mousePt, elevation); if (tiles[pt.x][pt.y].elevation === elevation) { return pt; } } return null; // not tile hit } This code would need to be adjusted for arbitrary elevations and could be optimized to skip elevation that don't contain any tiles. Note that my pseudocode ignores vertical sides of a tile and clicks on them will select the (lower elevation) tile obscured by the vertical side. If vertical tiles need to be accounted for, then a more generic surface hit detection approach will be needed. You could visit every tile (from closest to farthest away) and test whether the mouse coordinates are in the "roof" or in one of the viewer facing "wall" polygons.
If map is not rotatable and exatly same as picture you posted here, When you are drawing polygons, save each tile's polygon(s) in a polygon array. Then sort the array only once using distance of them(their tile) to you(closest first, farthest last) while keeping them grouped by tile index. When click event happens, get x,y coordinates of mouse, and do point in polygon test starting from first element array until last element. When hit, stop at that element. No matter how high a tile is, will not hide any tile that is closer to you(or even same distance to you). Point in polygon test is already solved: Point in Polygon Algorithm How can I determine whether a 2D Point is within a Polygon? Point in polygon You can even check every pixel of canvas once with this function and save results into an 2d array of points, vect2[x][y] which gives i,j indexes of tiles from x,y coordinates of mouse, then use this as a very fast index finder. Pros: fast and parallelizable using webworkers(if there are millions of tiles) scalable to multiple isometric maps using arrays of arrays of polygons sorted by distance to you. Elevation doesnt decrease performance because of only 3 per tile maximum. Doesn't need any conversion to isometric to 2d. Just the coordinates of corners of polygons on canvas and coordinates of mouse on the same canvas. Cons: You need coordinates of each corner if you haven't already. Clicking a corner will pick closest tile to you while it is on four tiles at the same time.
The answer, oddly, is written up in the Wikipedia page, in the section titled "Mapping Screen to World Coordinates". Rather than try to describe the graphics, just read the section three times. You will need to determine exactly which isomorphic projection you are using, often by measuring the tile size on the screen with a ruler.
Understanding rotation and calculating the top left point in KineticJS
I am working on a page where I can view images. I want to create a rotation tool. I've done that, but, it's not working consistently. When I set up the centre point to rotate by, the image jumps slightly, and it gets worse each time. I was experimenting, and, I have code to add a wedge to the top left corner of my top level group ( so, at 0,0 ). If I rotate the image by 45 degrees and drag it so that half of it is off the left edge of my canvas, then I call getAbsolutePosition on the wedge and on the group, I get these values: layer.getAbsolutePosition() Object {x: 104.66479545850302, y: 279.2748571151325} wedge.getAbsolutePosition() Object {x: 180.2684127179338, y: -73.48773356791764} I think this means my y position is actually the bottom of the image, which is off screen. What I want to do, is calculate the absolute position of the middle of my image, when the mouse moves over it, regardless of it's rotation. I have some code that works out points with rotation, which seems like it works at first, almost, but it just gets more and more broken the more I use the tool. I feel like there's something about how Kinetic is tracking these things and what it's reporting, that I am missing. Any hints would be most appreciated. Tutorials I can read are even better ( yes, I've read everything linked from the KineticJS site and searched the web ). In a nutshell, the question is, if I have an image inside a group, and it's rotated, how do I work out the centre point of the image, taking the rotation in to account, and how do I set the offset so it will rotate from that point, and stay in the same place ? Thanks
As you've discovered about KinetiJS: rotation is easy dragging is easy dragging+rotation is difficult After you drag your image you must reset its rotation point (offsetX/offsetY). KineticJS makes dragging+rotation more difficult than it has to be. Resetting the offset points of your image will cause KineticJS to automatically move your image (Noooo!!). That's what's causing your jumping. The solution to the "jumping" problem: When you reset the image's rotation point (offsetX/OffsetY) you must also reset the image's X/Y position. This code resets both XY and Offsets for an image after dragging: A Demo: http://jsfiddle.net/m1erickson/m9Nw7/ // calc new position and offset var pos=rect.getPosition(); var size=rect.getSize(); var offset=rect.getOffset(); var newX=pos.x-offset.x+size.width/2; var newY=pos.y-offset.y+size.height/2; // reset both position and offset rect.setPosition([newX,newY]); rect.setOffset(size.width/2,size.height/2);
How to implement sketch brush, like in deviantART muro?
deviantART muro has a set of brilliant tools of painting. And I'm very curious how to implement these brushes like Sketch and Paintbrush, arithmetically? Using any normal programming language to explain is okay, though I prefer C++ or JavaScript. I think it's better than read their JS source code.
I'd say it works something like: Track mouse movement On captured mouse movement, draw your desired brush from saved "Old mouse position" to captured "New mouse position", iterating at a pixel's distance at a time If you move the mouse too fast for the script to capture, it will just look like a computed long straight line (which is what it looks like Muro is doing). If you want to get real fancy you can calculate the trajectory from previous mouse positions and draw that instead for a "smoother" line. Since you specified Javascript you'd probably want to draw it in a canvas object. EDIT 1: Sketch specifically seems to save mouse movements and then loop through, say the 20 latest mouse movements for each mouse movement and draw a bezier curve from that point to the current point. So, something like (pseudo code) Object mousemovements = []; on.mousemove(event) { if (mousemovements.length > 20) { mousemovements.removeLast(); } mousemovements.insertAtBeginning([ event.mouseX, event.mouseY ]); for-each (movement in mousemovements) { drawBeziercurveFromTo(movement.mouseX, movement.mouseY, event.mouseX, event.mouseY); } } Jquery/Canvas DEMO based on the above pseudo code EDIT 2: I had a closer look at how "Sketch" worked and it seems that they update the mouse pointer positions, moving the older points closer to the newer points. Something like this: This DEMO works pretty much like the sketch brush
What is the most efficient way to reset the size of a shape after scaling in PaperJS
I am attempting to create a very simple beacon-like animation in Paper JS. The idea is that a circle starts off very small and totally opaque and then gets larger and more transparent until it disappears and the animation restarts. I'm using scaling to make the image larger but resetting it to it's original size is becoming problematic and at the moment I have resorted to cloning a second circle to reset it rather than just working with a single shape, there has to be a simpler way of doing this. I've create a jsFiddle to demonstrate my rough code so far, any help would be appreciated. http://jsfiddle.net/colethecoder/Y3S9n/1
Paperjs does not store the original Path, nor does it remember any operations that have been applied to reach the current state, so it can be difficult to reset to a previous state. The easiest approach is to use the this.scale that your current code is calculating and when you want to reset do this.circle.scale(1/this.scale); Here is a jsfiddle that way. FYI, here is the code path for scaling: Item.scale() Item.transform() Item.apply() Path._apply() Segment._transformCoordinates() So the end result is that _transformCoordinates() is called on each of the four segments in the circle, and it simply moves the point coordinates...nothing is remembered to "undo" the scaling later. Alternatively to remembering the scale yourself, you can use the Path.fitBounds() function to shrink the circles to an arbitrary size...for instance you could save the bounding rectangle right after creating the Circle, and then fitBounds back to that size.
Set item.applyMatrix = false if you don't want to persist transformations alongside item. For example, the following code linearly (i.e. "additively") animates item.scaling: var item = new Path.Rectangle({ point: [75, 75], size: [5, 5], strokeColor: 'black', applyMatrix: false }); function onFrame(event) { item.scaling += 0.1; }
The way i approached this issue was attaching a new object called originalBounds to the paper.js shapes immediately after their instantiation. If i needed to play with its size, coming back its original one became fairly trivial.