UNPKG

phaser-arcade-slopes

Version:

A Phaser CE plugin that brings sloped tile collision handling to Phaser's Arcade Physics engine

470 lines (381 loc) 15.3 kB
/** * @author Chris Andrew <chris@hexus.io> * @copyright 2016-2021 Chris Andrew * @license MIT */ /** * A static class with override methods for Phaser's tilemap collisions and tile * neighbour checks. * * @static * @class Phaser.Plugin.ArcadeSlopes.Override */ Phaser.Plugin.ArcadeSlopes.Overrides = {}; /** * Collide a sprite against a single tile. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#collideSpriteVsTile * @param {integer} i - The tile index. * @param {Phaser.Sprite} sprite - The sprite to check. * @param {Phaser.Tile} tile - The tile to check. * @param {Phaser.TilemapLayer} tilemapLayer - The tilemap layer the tile belongs to. * @param {function} [collideCallback] - An optional collision callback. * @param {function} [processCallback] - An optional overlap processing callback. * @param {object} [callbackContext] - The context in which to run the callbacks. * @param {boolean} [overlapOnly] - Whether to only check for an overlap. * @return {boolean} - Whether a collision occurred. */ Phaser.Plugin.ArcadeSlopes.Overrides.collideSpriteVsTile = function (i, sprite, tile, tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly) { if (!sprite.body || !tile || !tilemapLayer) { return false; } if (tile.hasOwnProperty('slope')) { if (this.game.slopes.collide(i, sprite.body, tile, tilemapLayer, overlapOnly)) { this._total++; if (collideCallback) { collideCallback.call(callbackContext, sprite, tile); } return true; } } else if (this.separateTile(i, sprite.body, tile, tilemapLayer, overlapOnly)) { this._total++; if (collideCallback) { collideCallback.call(callbackContext, sprite, tile); } return true; } return false; }; /** * Collide a sprite against a set of tiles. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#collideSpriteVsTiles * @param {Phaser.Sprite} sprite - The sprite to check. * @param {Phaser.Tile[]} tiles - The tiles to check. * @param {Phaser.TilemapLayer} tilemapLayer - The tilemap layer the tiles belong to. * @param {function} [collideCallback] - An optional collision callback. * @param {function} [processCallback] - An optional overlap processing callback. * @param {object} [callbackContext] - The context in which to run the callbacks. * @param {boolean} [overlapOnly] - Whether to only check for an overlap. * @return {boolean} - Whether a collision occurred. */ Phaser.Plugin.ArcadeSlopes.Overrides.collideSpriteVsTiles = function (sprite, tiles, tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly) { if (!sprite.body || !tiles || !tiles.length || !tilemapLayer) { return false; } var collided = false; for (var i = 0; i < tiles.length; i++) { if (processCallback) { if (processCallback.call(callbackContext, sprite, tiles[i])) { collided = this.collideSpriteVsTile(i, sprite, tiles[i], tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly) || collided; } } else { collided = this.collideSpriteVsTile(i, sprite, tiles[i], tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly) || collided; } } return collided; }; /** * Collide a sprite against a tile map layer. * * This is used to override Phaser.Physics.Arcade.collideSpriteVsTilemapLayer(). * * @override Phaser.Physics.Arcade#collideSpriteVsTilemapLayer * @method Phaser.Plugin.ArcadeSlopes.Overrides#collideSpriteVsTilemapLayer * @param {Phaser.Sprite} sprite - The sprite to check. * @param {Phaser.TilemapLayer} tilemapLayer - The tilemap layer to check. * @param {function} collideCallback - An optional collision callback. * @param {function} processCallback - An optional overlap processing callback. * @param {object} callbackContext - The context in which to run the callbacks. * @param {boolean} overlapOnly - Whether to only check for an overlap. * @return {boolean} - Whether a collision occurred. */ Phaser.Plugin.ArcadeSlopes.Overrides.collideSpriteVsTilemapLayer = function (sprite, tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly) { if (!sprite.body || !tilemapLayer) { return false; } var tiles = tilemapLayer.getTiles( sprite.body.position.x - sprite.body.tilePadding.x - tilemapLayer.getCollisionOffsetX(), sprite.body.position.y - sprite.body.tilePadding.y - tilemapLayer.getCollisionOffsetY(), sprite.body.width + sprite.body.tilePadding.x, sprite.body.height + sprite.body.tilePadding.y, true, false ); if (tiles.length === 0) { return false; } // TODO: Sort by distance from body center to tile center? var collided = this.collideSpriteVsTiles(sprite, tiles, tilemapLayer, collideCallback, processCallback, callbackContext, overlapOnly); return collided; }; /** * Gets the tile to the top left of the coordinates given. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getTileTopLeft * @param {integer} layer - The index of the layer to read the tile from. * @param {integer} x - The X coordinate, in tiles, to get the tile from. * @param {integer} y - The Y coordinate, in tiles, to get the tile from. * @return {Phaser.Tile} - The tile found. */ Phaser.Plugin.ArcadeSlopes.Overrides.getTileTopLeft = function(layer, x, y) { if (x > 0 && y > 0) { return this.layers[layer].data[y - 1][x - 1]; } return null; }; /** * Gets the tile to the top right of the coordinates given. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getTileTopRight * @param {integer} layer - The index of the layer to read the tile from. * @param {integer} x - The X coordinate, in tiles, to get the tile from. * @param {integer} y - The Y coordinate, in tiles, to get the tile from. * @return {Phaser.Tile} - The tile found. */ Phaser.Plugin.ArcadeSlopes.Overrides.getTileTopRight = function(layer, x, y) { if (x < this.layers[layer].width - 1 && y > 0) { return this.layers[layer].data[y - 1][x + 1]; } return null; }; /** * Gets the tile to the bottom left of the coordinates given. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getTileBottomLeft * @param {integer} layer - The index of the layer to read the tile from. * @param {integer} x - The X coordinate, in tiles, to get the tile from. * @param {integer} y - The Y coordinate, in tiles, to get the tile from. * @return {Phaser.Tile} - The tile found. */ Phaser.Plugin.ArcadeSlopes.Overrides.getTileBottomLeft = function(layer, x, y) { if (x > 0 && y < this.layers[layer].height - 1) { return this.layers[layer].data[y + 1][x - 1]; } return null; }; /** * Gets the tile to the bottom right of the coordinates given. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getTileBottomRight * @param {integer} layer - The index of the layer to read the tile from. * @param {integer} x - The X coordinate, in tiles, to get the tile from. * @param {integer} y - The Y coordinate, in tiles, to get the tile from. * @return {Phaser.Tile} - The tile found. */ Phaser.Plugin.ArcadeSlopes.Overrides.getTileBottomRight = function(layer, x, y) { if (x < this.layers[layer].width - 1 && y < this.layers[layer].height - 1) { return this.layers[layer].data[y + 1][x + 1]; } return null; }; /** * Get the X axis collision offset for the tilemap layer. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getCollisionOffsetY * @return {number} */ Phaser.Plugin.ArcadeSlopes.Overrides.getCollisionOffsetX = function () { if (this.getTileOffsetX) { return this.getTileOffsetX(); } return !this.fixedToCamera ? this.position.x : 0; }; /** * Get the Y axis collision offset for the tilemap layer. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#getCollisionOffsetY * @return {number} */ Phaser.Plugin.ArcadeSlopes.Overrides.getCollisionOffsetY = function () { if (this.getTileOffsetY) { return this.getTileOffsetY(); } return !this.fixedToCamera ? this.position.y : 0; }; /** * Renders a tilemap debug overlay on-top of the canvas. * * Called automatically by render when `debug` is true. * * See `debugSettings` for assorted configuration options. * * This override renders extra information regarding Arcade Slopes collisions. * * @method Phaser.Plugin.ArcadeSlopes.Overrides#renderDebug * @private */ Phaser.Plugin.ArcadeSlopes.Overrides.renderDebug = function () { var scrollX = this._mc.scrollX; var scrollY = this._mc.scrollY; var context = this.context; var renderW = this.canvas.width; var renderH = this.canvas.height; var scaleX = this.tileScale ? this.tileScale.x : 1.0 / this.scale.x; var scaleY = this.tileScale ? this.tileScale.y : 1.0 / this.scale.y; var width = this.layer.width; var height = this.layer.height; var tw = this._mc.tileWidth * scaleX; // Tile width var th = this._mc.tileHeight * scaleY; // Tile height var htw = tw / 2; // Half-tile width var hth = th / 2; // Half-tile height var qtw = tw / 4; // Quarter-tile width var qth = th / 4; // Quarter-tile height var cw = this._mc.cw * scaleX; var ch = this._mc.ch * scaleY; var m = this._mc.edgeMidpoint; var left = Math.floor(scrollX / tw); var right = Math.floor((renderW - 1 + scrollX) / tw); var top = Math.floor(scrollY / th); var bottom = Math.floor((renderH - 1 + scrollY) / th); if (!this._wrap) { if (left <= right) { left = Math.max(0, left); right = Math.min(width - 1, right); } if (top <= bottom) { top = Math.max(0, top); bottom = Math.min(height - 1, bottom); } } var baseX = (left * tw) - scrollX; var baseY = (top * th) - scrollY; var normStartX = (left + ((1 << 20) * width)) % width; var normStartY = (top + ((1 << 20) * height)) % height; var tx, ty, x, y, xmax, ymax, polygon, i, j, a, b, norm, gx, gy, line; for (y = normStartY, ymax = bottom - top, ty = baseY; ymax >= 0; y++, ymax--, ty += th) { if (y >= height) { y -= height; } var row = this.layer.data[y]; for (x = normStartX, xmax = right - left, tx = baseX; xmax >= 0; x++, xmax--, tx += tw) { if (x >= width) { x -= width; } var tile = row[x]; if (!tile || tile.index < 0 || !tile.collides) { continue; } if (this.debugSettings.collidingTileOverfill) { context.fillStyle = this.debugSettings.collidingTileOverfill; context.fillRect(tx, ty, cw, ch); } if (this.debugSettings.facingEdgeStroke) { context.beginPath(); context.lineWidth = 1; context.strokeStyle = this.debugSettings.facingEdgeStroke; if (tile.faceTop) { context.moveTo(tx, ty); context.lineTo(tx + cw, ty); } if (tile.faceBottom) { context.moveTo(tx, ty + ch); context.lineTo(tx + cw, ty + ch); } if (tile.faceLeft) { context.moveTo(tx, ty); context.lineTo(tx, ty + ch); } if (tile.faceRight) { context.moveTo(tx + cw, ty); context.lineTo(tx + cw, ty + ch); } context.closePath(); context.stroke(); // Render the tile slope polygons if (tile.slope) { // Fill polygons and stroke their edges if (this.debugSettings.slopeEdgeStroke || this.debugSettings.slopeFill) { context.beginPath(); context.lineWidth = 1; polygon = tile.slope.polygon; // Move to the first vertex context.moveTo(tx + polygon.points[0].x * scaleX, ty + polygon.points[0].y * scaleY); // Draw a path through all vertices for (i = 0; i < polygon.points.length; i++) { j = (i + 1) % polygon.points.length; context.lineTo(tx + polygon.points[j].x * scaleX, ty + polygon.points[j].y * scaleY); } context.closePath(); if (this.debugSettings.slopeEdgeStroke) { context.strokeStyle = this.debugSettings.slopeEdgeStroke; context.stroke(); } if (this.debugSettings.slopeFill) { context.fillStyle = this.debugSettings.slopeFill; context.fill(); } } // Stroke the colliding edges and edge normals if (this.debugSettings.slopeCollidingEdgeStroke) { // Colliding edges context.beginPath(); context.lineWidth = this.debugSettings.slopeCollidingEdgeStrokeWidth || 1; context.strokeStyle = this.debugSettings.slopeCollidingEdgeStroke; polygon = tile.slope.polygon; for (i = 0; i < polygon.points.length; i++) { // Skip the edges with ignored normals if (polygon.normals[i].ignore) { continue; } j = (i + 1) % polygon.points.length; context.moveTo(tx + polygon.points[i].x * scaleX, ty + polygon.points[i].y * scaleY); context.lineTo(tx + polygon.points[j].x * scaleX, ty + polygon.points[j].y * scaleY); } context.closePath(); context.stroke(); // Edge normals for (i = 0; i < polygon.points.length; i++) { context.beginPath(); if (polygon.normals[i].ignore) { context.lineWidth = this.debugSettings.slopeNormalStrokeWidth; context.strokeStyle = this.debugSettings.slopeNormalStroke; } else { context.lineWidth = this.debugSettings.slopeCollidingNormalStrokeWidth; context.strokeStyle = this.debugSettings.slopeCollidingNormalStroke; } j = (i + 1) % polygon.points.length; a = polygon.points[i]; b = polygon.points[j]; norm = polygon.normals[i]; // Midpoint of the edge m.x = (a.x + b.x) / 2; m.y = (a.y + b.y) / 2; // Draw from the midpoint outwards using the normal context.moveTo(tx + m.x * scaleX, ty + m.y * scaleY); context.lineTo(tx + m.x * scaleX + norm.x * qtw, ty + m.y * scaleY + norm.y * qth); context.closePath(); context.stroke(); } // Ignormals if (tile.slope.ignormals) { for (i = 0; i < tile.slope.ignormals.length; i++) { context.beginPath(); context.lineWidth = 1; context.strokeStyle = 'rgba(255, 0, 0, 1)'; gx = tile.slope.ignormals[i].x; gy = tile.slope.ignormals[i].y; context.moveTo(tx + htw, ty + hth); context.lineTo(tx + htw + gx * qtw, ty + hth + gy * qth); context.closePath(); context.stroke(); } } } // Slope line segments if (this.debugSettings.slopeLineStroke && tile.slope.line) { line = tile.slope.line; context.beginPath(); context.lineWidth = this.debugSettings.slopeLineWidth || 2; context.strokeStyle = this.debugSettings.slopeLineStroke; context.moveTo(line.start.x - scrollX, line.start.y - scrollY); context.lineTo(line.end.x - scrollX, line.end.y - scrollY); context.closePath(); context.stroke(); } } } } } };