Skip to content

Instantly share code, notes, and snippets.

@mbutler
Created February 26, 2012 23:21
Show Gist options
  • Save mbutler/1919621 to your computer and use it in GitHub Desktop.
Save mbutler/1919621 to your computer and use it in GitHub Desktop.
Scrollable hex map demo
/* A scrollable hexagonal map with dynamic loading and unloading of tiles, implemented using JQuery and CraftyJS.
*
* by Lynna Merrill, http://www.lynnamerrill.com
*
* Credit for some ideas and/or code snippets goes to the isometric map implementation at http://craftyjs.com/demos/isometric
and the game tutorial at https://github.com/starmelt/craftyjstut/wiki/A-first-game.
*
* All graphics used for this demo are copyrighted and cannot be distributed or reproduced in any way without permission. You may download the graphics if you want to test this demo on your own computer, but please use your own images for any posts or projects of yours.
*
* 6 December 2011
*
*/
window.onload = (function(){
Crafty.init(600, 400);
var drawWay = "DOM";
//loading separate images in this demo; use a real spritemap for production
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/1.png", {
terrain1: [0,0, 1, 1.1] //mountain
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/2.png", {
terrain2: [0,0, 1, 1.1] //hill
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/3.png", {
terrain3: [0,0, 1, 1.1] //grass
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/4.png", {
terrain4: [0,0, 1, 1.1] //water
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/forest-1.png", {
forest1: [0,0, 1, 1.1] //mountain forest
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/forest-2.png", {
forest2: [0,0, 1, 1.1] //hill forest
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/forest-3.png", {
forest3: [0,0, 1, 1.1] //plains forest
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/woodcuttersShack96.png", {
shack: [0,0]
});
Crafty.sprite(96, "http://lynnamerrill.com/wp-content/uploads/2011/12/stoneQuarry96.png", {
quarry: [0,0]
});
var tileWidth=94; //value is smaller than real image width because of drawing gaps; tweak for your own images
var tileHeight=104; //same as above
var rowLimit = 9;
var colLimit = 9;
//start map drawing with a tile of which only a piece is visible in the upper left corner
var i = -2*tileWidth - tileWidth/2;
var j = -2*tileHeight - 3/4*tileHeight;
for (var rowCount=1; rowCount<=rowLimit; rowCount++){
if (rowCount%2==0) {//row number is even
i = -2*tileWidth; //tile doesn't protrude to the left of visible space
}
else if (rowCount!=1){ //row number is odd but is not the first row
i = -2*tileWidth - tileWidth/2; //tile protrudes to the left
}
for (var colCount=1; colCount<=colLimit; colCount++){
drawTile (i, j);
i=i+tileWidth;
}
j = j + 3/4*tileHeight;
//get first row down
}
//draw a single tile, with terrain, rivers, etc.
function drawTile (tileX, tileY){
//Generate random terrain information for the hexagonal to be drawn
terrainType = Crafty.randRange(1,4);
theTile = Crafty.e("2D,"+drawWay+", Mouse, terrain"+terrainType).attr({
x: tileX,
y: tileY,
h:tileHeight+2,
w:tileWidth+2 //makes it 96px, the real tile width
})
.bind("Click", function(e) {
//sample event handling; upon click draw a quarry on the respective terrain tile (you need to use right-click for the terrain tiles with forests or shacks)
this.attach(Crafty.e("2D,"+drawWay+", Mouse, "+"quarry").attr({
x: tileX + 10,
y: tileY + 30,
z: 2//,
}))}
);
if (colCount==1){ //this is a first-in-a-row tile
theTile.addComponent("leftTile");
}
if (colCount==colLimit){
theTile.addComponent("rightTile");
}
if (rowCount==1){
theTile.addComponent("topTile")
}
if (rowCount==rowLimit){
theTile.addComponent("bottomTile");
}
forest = Crafty.randRange(0,1); //randomly determine if there is a forest on the tile or not
if (forest==1){
drawTileElement("forest"+terrainType, 1, 0, 0);
}
shack = Crafty.randRange(0,1);
if (shack==1 && terrainType!=4){ //don't draw shacks on water tiles
drawTileElement("shack", 2, 0, 0);
}
function drawTileElement (sprite, zIndex, xAdjust, yAdjust){
theTile.attach(Crafty.e("2D,"+drawWay+", Mouse, "+sprite).attr({
x: i+xAdjust,
y: j+yAdjust,
z: zIndex//,
}).bind("Click", function(e) {
//sample event handling: upon right click initiate the "Click" event handler of parent tile (the terrain tile, in this case)
if(e.button == 2) {
this._parent.trigger("Click", e);
}
}));
}
}
//Start code for dynamic tile loading upon pan
var dxCom = 0;
var dyCom = 0;
var topPNum = 1; //used for determining whether to shift new rows left or right
var bottomPNum = 1; //used for determining whether to shift new rows up or down
Crafty.addEvent(this, Crafty.stage.elem, "mousedown", function(e) {
if(e.button > 1) return;
var base = {
x: e.clientX,
y: e.clientY
};
function scroll(e) {
//NOTE: Currently, panning is implemented so that the viewport is not shifted the exact number of pixels the user moves the mouse, but is shifted with 8px with every move.
var dx = base.x - e.clientX;
var shiftX = 0;
if (dx>0){
shiftX = 8;
}
else if (dx<0){
shiftX = -8;
}
dxCom += shiftX;
var dy = base.y - e.clientY;
var shiftY = 0;
if (dy>0){
shiftY = 8;
}
else if (dy<0){
shiftY = -8;
}
dyCom += shiftY;
base = {
x: e.clientX,
y: e.clientY
};
//used for automatic tile loading during drag and drop
var theXFormula;
var theYFormula;
panMap();
if (dxCom>=tileWidth){ //whole map has moved to the left, load respective number of tiles to the right, unload them from the left
theXFormula = function (theX, theK){
return theX+theK*tileWidth;
};
theYFormula = function (theY, theK){
return theY;
}
//just one tile loaded and unloaded at a time
panningCare ("rightTile", "leftTile", 1, theXFormula, theYFormula);
dxCom = 0;
}
else if (dxCom<=(-1*tileWidth)){ //map moves to the right (view moves to the left); load to the left, unload from the right
theXFormula = function (theX, theK){
return theX-theK*tileWidth;
};
theYFormula = function (theY, theK){
return theY;
};
panningCare ("leftTile", "rightTile", 1, theXFormula, theYFormula);
dxCom = 0;
}
if (dyCom>=3/4*tileHeight){ //whole map moves up, load tiles down, unload up
// NOTE: the calculations below assume always an odd number of rows; make sure # is always odd
theXFormula = function (theX, theK){
if (bottomPNum%2==0){ //even row to draw below current first, shift to left
return theX-1/2*tileWidth;
}
else { //odd row to draw below current first (counting down, starting at 1), shift to right
return theX+1/2*tileWidth;
}
}
theYFormula = function(theY, theK){
return theY+theK*(3/4*tileHeight);
}
panningCare ("bottomTile", "topTile", 1, theXFormula, theYFormula);
dyCom = 0;
bottomPNum++;
topPNum--;
}
else if (dyCom<=(-3/4*tileHeight)){ //whole map moves down, load tiles up, unload down
theXFormula = function (theX, theK){
if (topPNum%2==0){ //even row to draw above current first, shift to left
return theX-1/2*tileWidth;
}
else { //odd row to draw above current first (counting up, starting at 1), shift to right
return theX+1/2*tileWidth;
}
}
theYFormula = function(theY, theK){
return theY-theK*(3/4*tileHeight);
}
panningCare ("topTile", "bottomTile", 1, theXFormula, theYFormula);
dyCom = 0;
topPNum++; //loading
bottomPNum--; //unloading
}
//end of code for dynamic loading of tiles upon map panning
function panMap(){
//the panning functionality itself
Crafty.viewport.x -=shiftX;
Crafty.viewport.y -= shiftY;
}
};
Crafty.addEvent(this, Crafty.stage.elem, "mousemove", scroll);
Crafty.addEvent(this, Crafty.stage.elem, "mouseup", function() {
Crafty.removeEvent(this, Crafty.stage.elem, "mousemove", scroll);
});
});
//help functions for dynamic loading of tiles upon pan
function panningCare(tileLabel, tileUnloadLabel, numToDraw, theXFormula, theYFormula){
Crafty(tileLabel).each(function(){
for (var k=1; k<=numToDraw; k++){
neighbor = drawNeighbor(theXFormula(this._x, k), theYFormula(this._y, k));
if (k==numToDraw){
neighbor.addComponent(tileLabel);
}
if (tileLabel=="leftTile" || tileLabel=="rightTile"){
if (this.__c["topTile"]){
neighbor.addComponent("topTile");
}
//"else if" assumes map is not just one row
else if(this.__c["bottomTile"]){
neighbor.addComponent("bottomTile");
}
}
//"else if" assumes map is not just one column
if (tileLabel=="topTile" || tileLabel=="bottomTile"){
if (this.has("leftTile")){
neighbor.addComponent("leftTile");
}
else if (this.has("rightTile")){
neighbor.addComponent("rightTile");
}
}
this.removeComponent(tileLabel);
}
});
Crafty(tileUnloadLabel).each(function(){
theUnloadTile = this;
for (var l=1; l<=numToDraw; l++){
theX = theXFormula(theUnloadTile._x, l);
theY = theYFormula(theUnloadTile._y, l);
neighborsOfUnloaded=Crafty.map.search({
_x:theX,
_y:theY,
_w:tileWidth,
_h:tileHeight
});
theUnloadChildren = theUnloadTile._children; //the shacks, quarries, and forests; we need to destroy them together with their terrain when the respective terrain tiles are unloaded
for (var m=0; m<theUnloadChildren.length; m++){
theUnloadChildren[m].destroy();
}
theUnloadTile.destroy();
for (var a=0; a<neighborsOfUnloaded.length;a++){
if (neighborsOfUnloaded[a]._x==theX && neighborsOfUnloaded[a]._y==theY && (neighborsOfUnloaded[a].has("terrain1")|| neighborsOfUnloaded[a].has("terrain1")||neighborsOfUnloaded[a].has("terrain2") ||neighborsOfUnloaded[a].has("terrain3") || neighborsOfUnloaded[a].has("terrain4"))){ //this is the exact neighboring terrain tile; needed because Crafty.map.search will get everything that intersects the rectangle even for a pixel, not just everything with its origin in the rectangle
theUnloadTile = neighborsOfUnloaded[a];
}
}
if (l==numToDraw){
theUnloadTile.addComponent(tileUnloadLabel); //won't be destroyed, will have the label for top, bottom, left, or right tile instead
}
}
}
);
} //end of panning care
function drawNeighbor(nbX, nbY){ //the drawing function called when dynamically loading tiles, it will only draw mountain tines. Replace with your own drawing functionality for your projects, to draw whatever is needed.
return Crafty.e("2D,"+drawWay+", Mouse, terrain1").attr({
x:nbX,
y:nbY,
h:tileHeight,
w:tileWidth
});
}
//end of help functions for dynamic loading of tiles upon pan
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment