cykod / Quintus

HTML5 Game Engine
http://html5quintus.com
GNU General Public License v2.0
1.41k stars 400 forks source link

Dynamic collision layer #152

Open murilopolese opened 9 years ago

murilopolese commented 9 years ago

Is there a good way to append chunks of data to the current collision layer on a scene? The main reason is to be able to create dynamic "infinite" games and specially to create the level on the run. I did the following changes on file quintus_2d.js:208`:

load: function(dataAsset) {
  var data;
  if( dataAsset instanceof Array ) {
    data = dataAsset;
  } else {
    var fileParts = dataAsset.split("."),
        fileExt = fileParts[fileParts.length-1].toLowerCase();

    if (fileExt === "json") {
      data = Q._isString(dataAsset) ?  Q.asset(dataAsset) : dataAsset;
    }
    else {
      throw "file type not supported";
    }
  }

So I could keep an array with my level and concat new chunks as the character moves. Here's the demo: http://murilopolese.com.br/sandbox/fabulous-runner/

I'd love to understand what would be the best way to do that or if this is the way to go.

Cheers, Murilo

cykod commented 9 years ago

I'd suggest instead of this plan of attack (expanding the data on 1 collision layer), you instead add additional collisionLayers offset in position - otherwise the 1 collision layer will get extremely large and unwieldly. This way you could in theory remove the old collision layers and free up memory (this would require some custom code to clean up the scene._collisionLayers array when you remove a collision layer.

JaredSartin commented 9 years ago

Necroing... @cykod - how do you suggest offsetting? putting an x position on the tileLayer doesn't allow it to render properly.

R4wizard commented 8 years ago

I'm having a similar problem. I am also trying to stream in chunks of levels. I have also tried setting the x position on the tileLayer, with varying results. I've come to the conclusion that if you change the 'x' of the tileLayer, you move the collision perfectly but then the tiles are rendered at 'x*2'. I can't see a clean way to resolve this as I am unfamilar with the code, but I'll keep digging.

R4wizard commented 8 years ago

After a tiny bit more digging, I think I found the problem: https://github.com/cykod/Quintus/blob/master/lib/quintus_2d.js#L395 The 'p.x' and 'p.y' are added here. If I remove this addition by doing:

        tileLayer.drawBlock = function(ctx, blockX, blockY) {
            var p = tileLayer2.p,
                startX = Math.floor(blockX * p.blockW),
                startY = Math.floor(blockY * p.blockH);

            if(!tileLayer.blocks[blockY] || !tileLayer.blocks[blockY][blockX]) {
                tileLayer.prerenderBlock(blockX,blockY);
            }

            if(tileLayer.blocks[blockY] && tileLayer.blocks[blockY][blockX]) {
                ctx.drawImage(tileLayer.blocks[blockY][blockX],startX,startY);
            }
        };

Then everything renders correctly, and the collision appears to work. I haven't created a push request as I don't know if my changes will affect the code in other ways.

JaredSartin commented 8 years ago

I ended up modifying the collision layer, then calling .setDimensions(); and ._generateCollisionObjects(); on the layer object. My code is messy and from 6 months ago - so I can try to find the key points for you...

R4wizard commented 8 years ago

My current solution seems to function okay, but please do post any information you have. It may all blow up in my face! (and you never know who might find this ticket next and want info).

Thanks!

JaredSartin commented 8 years ago

shitty old code below. What this does is news up a map, with a start room, some empty rooms, and then a goal room. A room is merely and X by Y sized section of the tiles/collision layer. There are helper methods that get called at any point to change "rooms" in the map. See changeRoom - you see it create a tilemap from the given map name with var tileLayer = new Q.TileLayer..., then it finds the room offset on the main tilemap and modifies the data at that point (the while loop following the tile layer creation). We then create a new tile layer from the current tile layer's tiles... THIS is the crappy point. You can't just simply tell the tile layer to update and have it work.

The reason we create a new tile layer had to do with 2 things - the cruft left on the old tile layer, attempting to use old data for collisions with no way to reset it AND the stage collision layer not accepting a reset on the collision layer. What we end up doing is cloning the modified tile layer and re-assigning the cloned layer to the stage as the collision layer.

Yes, brainf*ck. Also, could possibly be done better. I put comments on key points in the code below:

  var mapLayer = new Q.TileLayer({ dataAsset: 'Entrance.json', sheet: 'terrain', tileW: tileSize, tileH: tileSize }); // initial map creation
  var empty = new Q.TileLayer({ dataAsset: 'Empty.json', sheet: 'terrain', tileW: tileSize, tileH: tileSize });
  var goal = new Q.TileLayer({ dataAsset: 'Goal.json', sheet: 'terrain', tileW: tileSize, tileH: tileSize });
  stage.collisionLayer(mapLayer); // collision layer assignment

  // helper method to add tiles to the map layer
  var addTiles = function(tileLayer) {
    var len = mapLayer.p.tiles.length;
    while(len--) {
      mapLayer.p.tiles[len] = mapLayer.p.tiles[len].concat(tileLayer.p.tiles[len]); // Modify current tile/collision layer
    }
  };

  // helper method to change portions of the map
  // called at many points in the game when parts of the map are swapped out.
  var changeRoom = function(roomIdx, mapName) {
    if(roomIdx > totalRooms - 2 || roomIdx < 1) throw "NO! ROOM NUMBER NOT IN RANGE [1," + totalRooms - 2 + "]";
    var len = mapLayer.p.tiles.length;
    var roomOffset = roomWidth * roomIdx;
    var tileLayer = new Q.TileLayer({ dataAsset: mapName + '.json', sheet: 'terrain', tileW: tileSize, tileH: tileSize });
    while(len--) {
      var colCounter = roomWidth;
      while(colCounter--) {
        mapLayer.p.tiles[len][colCounter+roomOffset] = tileLayer.p.tiles[len][colCounter]; // Modify current tile/collision layer
      }
    }
    mapLayer = new Q.TileLayer({ tiles: mapLayer.p.tiles, sheet: 'terrain', tileW: tileSize, tileH: tileSize }); // clone the current layer... see replaceCollisionLayer
    replaceCollisionLayer(mapLayer);
  };

  // We replace the current collision layer with a cloned version of the modified original.
  // The modified original could not be reset properly to tell the stage that is has changed.
  // Also, the way the current layer was referenced, I believe it wouldn't allow a re-assign.
  var replaceCollisionLayer = function(newLayer) {
    stage.collisionLayer(newLayer);
    stage._collisionLayers.shift().destroy(); // This is because we ADDED a new collision layer on the line before. This removes the old layer.
  }

  // All the crap below is setup code - but useful to get bearings on the rest of the code.
  var i = totalRooms - 2;
  while(i--) {
    addTiles(empty);
  }

  var i = totalRooms - 2;
  while(i--) {
    changeRoom(i+1, "Empty");
  }
  addTiles(goal);
  mapLayer.setDimensions();
  mapLayer._generateCollisionObjects();

  stage.centerOn(stage.options.startX, stage.options.startY);
  stage.viewport.scale = stage.options.zoom;
  stage.follow(rogue, {x: stage.options.followX, y: stage.options.followY});