export default class MainMenu extends Phaser.Scene { constructor () { super('MainMenu'); } create () { this.add.image(512, 384, 'background'); const box = this.add.image(512, 384, 'box'); const logo = this.add.image(512, -384, 'logo'); box.setPostPipeline('WipePostFX'); logo.setPostPipeline('ShinePostFX'); const pipeline = box.getPostPipeline('WipePostFX'); pipeline.setTopToBottom(); pipeline.setRevealEffect(); this.tweens.add({ targets: pipeline, progress: 1, duration: 3000 }); this.tweens.add({ targets: logo, y: 384, delay: 1500, duration: 2000, ease: 'sine.out', onComplete: () => { this.input.once('pointerdown', () => { pipeline.setWipeEffect(); pipeline.setTexture('box-inside'); this.tweens.add({ targets: logo, alpha: 0, duration: 1000, ease: 'sine.out' }); this.tweens.add({ targets: pipeline, progress: 1, duration: 2500, onComplete: () => { this.scene.start('Game'); } }); }); } }); } }
export default class Preloader extends Phaser.Scene { constructor () { super('Preloader'); } preload () { this.load.setBaseURL('https://cdn.phaserfiles.com/v385'); this.load.setPath('assets/games/sliding-puzzle/'); this.load.image('background', 'background.png'); this.load.image('logo', 'logo.png'); this.load.image('box', 'box.png'); this.load.image('box-inside', 'box-inside.png'); this.load.image('pic1', 'pic1.png'); this.load.image('pic2', 'pic2.png'); this.load.image('pic3', 'pic3.png'); this.load.setPath('assets/games/sliding-puzzle/audio'); this.load.audio('move', [ 'move.m4a', 'move.wav', 'move.ogg' ]); this.load.audio('win', [ 'win.m4a', 'win.wav', 'win.ogg' ]); } create () { this.scene.start('MainMenu'); } }
/** * @author Richard Davey
* @copyright 2023 Photon Storm Ltd. */ const fragShader = ` #define SHADER_NAME SHINE_FS precision mediump float; uniform sampler2D uMainSampler; uniform vec2 uResolution; uniform float uSpeed; uniform float uTime; uniform float uLineWidth; uniform float uGradient; varying vec2 outTexCoord; void mainImage (out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord.xy / uResolution.xy; vec4 t1 = texture2D(uMainSampler, uv); vec4 col1 = vec4(0.3, 0.0, 0.0, 1.0); vec4 col2 = vec4(0.85, 0.85, 0.85, 1.0); vec2 linepos = uv; linepos.x = linepos.x - mod(uTime * uSpeed, 2.0) + 0.5; float y = linepos.x * uGradient; float s = smoothstep(y - uLineWidth, y, linepos.y) - smoothstep(y, y + uLineWidth, linepos.y); fragColor = (((s * col1) + (s * col2)) * t1) + t1; } void main () { mainImage(gl_FragColor, gl_FragCoord.xy); } `; export default class ShinePostFX extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { constructor (game) { super({ game, name: 'ShinePostFX', fragShader }); this.speed = 0.5; this.lineWidth = 0.58; this.gradient = 3; } onPreRender () { this.setTime('uTime'); this.set1f('uSpeed', this.speed); this.set1f('uLineWidth', this.lineWidth); this.set1f('uGradient', this.gradient); } onDraw (renderTarget) { this.set2f('uResolution', renderTarget.width, renderTarget.height); this.bindAndDraw(renderTarget); } }
const SlidingPuzzle = { ALLOW_CLICK: 0, TWEENING: 1 }; /** * Sliding Puzzle Game Template * ---------------------------- * * This is the classic Sliding Puzzle game. Unlike lots of implementations out there, * we don't use a 'random' starting layout, as otherwise the puzzle will be unsolvable * 50% of the time. Instead we use a puzzle walker function. This allows you to see * the puzzle before-hand, and then it gets all manged up, ready for you to solve. * * You can control the number of iterations, or steps, that the walker goes through. * You can of course provide any image you like to the puzzle, and it'll adapt and resize * without changing much. * * In this example template there are 3 pictures, and as you solve them, the walker * increases in complexity each time, making it harder to solve. * * This web site has some creative tips on solving Sliding Puzzles: * http://www.nordinho.net/vbull/blogs/lunanik/6131-slider-puzzles-solved-once-all.html */ export default class Game extends Phaser.Scene { constructor () { super('Game'); // These are all set in the startPuzzle function this.rows = 0; this.columns = 0; // The width and height of each piece in the puzzle. // Again, this is set automatically in startPuzzle. this.pieceWidth = 0; this.pieceHeight = 0; this.pieces = null; this.spacer = null; // The speed at which the pieces slide, and the tween they use this.slideSpeed = 300; this.slideEase = 'power3'; // The number of iterations the puzzle walker will go through when // scrambling up the puzzle. 10 is a nice and easy puzzle, but // push it higher for much harder ones. this.iterations = 6; // The speed at which the pieces are shuffled at the start. This allows // the player to see the puzzle before trying to solve it. However if // you don't want this, just set the speed to zero and it'll appear // instantly 'scrambled'. this.shuffleSpeed = 200; this.shuffleEase = 'power1'; this.lastMove = null; // The image in the Cache to be used for the puzzle. // Set in the startPuzzle function. this.photo = ''; this.slices = []; this.action = SlidingPuzzle.ALLOW_CLICK; } create () { this.add.image(512, 384, 'background'); this.add.image(512, 384, 'box-inside'); window.solve = () => { this.nextRound(); }; this.startPuzzle('pic1', 3, 3); } /** * This function is responsible for building the puzzle. * It takes an Image key and a width and height of the puzzle (in pieces, not pixels). * Read the comments within this function to find out what happens. */ startPuzzle (key, rows, columns) { this.photo = key; // The size if the puzzle, in pieces (not pixels) this.rows = rows; this.columns = columns; // The size of the source image const texture = this.textures.getFrame(key); const photoWidth = texture.width; const photoHeight = texture.height; // Create our sliding pieces // Each piece will be this size: const pieceWidth = photoWidth / rows; const pieceHeight = photoHeight / columns; this.pieceWidth = pieceWidth; this.pieceHeight = pieceHeight; // A Container to put the pieces in if (this.pieces) { this.pieces.removeAll(true); } else { // The position sets the top-left of the container for the pieces to expand down from this.pieces = this.add.container(194, 66); } // An array to put the texture slices in if (this.slices) { this.slices.forEach(slice => slice.destroy()); this.slices = []; } let i = 0; // Loop through the image and create a new Sprite for each piece of the puzzle. for (let y = 0; y < this.columns; y++) { for (let x = 0; x < this.rows; x++) { // remove old textures const slice = this.textures.addDynamicTexture(`slice${i}`, pieceWidth, pieceHeight); const ox = 0 + (x / this.rows); const oy = 0 + (y / this.columns); slice.stamp(key, null, 0, 0, { originX: ox, originY: oy }); this.slices.push(slice); const piece = this.add.image(x * pieceWidth, y * pieceHeight, `slice${i}`); piece.setOrigin(0, 0); // The current row and column of the piece // Store the row and column the piece _should_ be in, when the puzzle is solved piece.setData({ row: x, column: y, correctRow: x, correctColumn: y }); piece.setInteractive(); piece.on('pointerdown', () => this.checkPiece(piece)); this.pieces.add(piece); i++; } } // The last piece will be our 'spacer' to slide in to this.spacer = this.pieces.getAt(this.pieces.length - 1); this.spacer.alpha = 0; this.lastMove = null; this.shufflePieces(); } /** * This shuffles up our puzzle. * * We can't just 'randomize' the tiles, or 50% of the time we'll get an * unsolvable puzzle. So instead lets walk it, making non-repeating random moves. */ shufflePieces () { // Push all available moves into this array const moves = []; const spacerCol = this.spacer.data.get('column'); const spacerRow = this.spacer.data.get('row'); if (spacerCol > 0 && this.lastMove !== Phaser.DOWN) { moves.push(Phaser.UP); } if (spacerCol < this.columns - 1 && this.lastMove !== Phaser.UP) { moves.push(Phaser.DOWN); } if (spacerRow > 0 && this.lastMove !== Phaser.RIGHT) { moves.push(Phaser.LEFT); } if (spacerRow < this.rows - 1 && this.lastMove !== Phaser.LEFT) { moves.push(Phaser.RIGHT); } // Pick a move at random from the array this.lastMove = Phaser.Utils.Array.GetRandom(moves); // Then move the spacer into the new position switch (this.lastMove) { case Phaser.UP: this.swapPiece(spacerRow, spacerCol - 1); break; case Phaser.DOWN: this.swapPiece(spacerRow, spacerCol + 1); break; case Phaser.LEFT: this.swapPiece(spacerRow - 1, spacerCol); break; case Phaser.RIGHT: this.swapPiece(spacerRow + 1, spacerCol); break; } } /** * Swaps the spacer with the piece in the given row and column. */ swapPiece (row, column) { // row and column is the new destination of the spacer const piece = this.getPiece(row, column); const spacer = this.spacer; const x = spacer.x; const y = spacer.y; // piece.data.set({ // row: spacer.data.get('row'), // column: spacer.data.get('column') // }); piece.data.values.row = spacer.data.values.row; piece.data.values.column = spacer.data.values.column; spacer.data.values.row = row; spacer.data.values.column = column; // spacer.data.set({ // row, // column // }); // this.spacer.data.row = row; // this.spacer.data.column = column; spacer.setPosition(piece.x, piece.y); // If we don't want them to watch the puzzle get shuffled, then just // set the piece to the new position immediately. if (this.shuffleSpeed === 0) { piece.setPosition(x, y); if (this.iterations > 0) { // Any more iterations left? If so, shuffle, otherwise start play this.iterations--; this.shufflePieces(); } else { this.startPlay(); } } else { // Otherwise, tween it into place const tween = this.tweens.add({ targets: piece, x, y, duration: this.shuffleSpeed, ease: this.shuffleEase }); if (this.iterations > 0) { // Any more iterations left? If so, shuffle, otherwise start play this.iterations--; tween.on('complete', this.shufflePieces, this); } else { tween.on('complete', this.startPlay, this); } } } /** * Gets the piece at row and column. */ getPiece (row, column) { for (let i = 0; i < this.pieces.length; i++) { const piece = this.pieces.getAt(i); if (piece.data.get('row') === row && piece.data.get('column') === column) { return piece; } } return null; } /** * Sets the game state to allow the user to click. */ startPlay () { this.action = SlidingPuzzle.ALLOW_CLICK; } /** * Called when the user clicks on any of the puzzle pieces. * It first checks to see if the piece is adjacent to the 'spacer', and if not, bails out. * If it is, the two pieces are swapped by calling `this.slidePiece`. */ checkPiece (piece) { if (this.action !== SlidingPuzzle.ALLOW_CLICK) { return; } // Only allowed if adjacent to the 'spacer' // // Remember: // // Columns = vertical (y) axis // Rows = horizontal (x) axis const spacer = this.spacer; if (piece.data.values.row === spacer.data.values.row) { if (spacer.data.values.column === piece.data.values.column - 1) { // Space above the piece? piece.data.values.column--; spacer.data.values.column++; spacer.y += this.pieceHeight; this.slidePiece(piece, piece.x, piece.y - this.pieceHeight); } else if (spacer.data.values.column === piece.data.values.column + 1) { // Space below the piece? piece.data.values.column++; spacer.data.values.column--; spacer.y -= this.pieceHeight; this.slidePiece(piece, piece.x, piece.y + this.pieceHeight); } } else if (piece.data.values.column === spacer.data.values.column) { if (spacer.data.values.row === piece.data.values.row - 1) { // Space to the left of the piece? piece.data.values.row--; spacer.data.values.row++; spacer.x += this.pieceWidth; this.slidePiece(piece, piece.x - this.pieceWidth, piece.y); } else if (spacer.data.values.row === piece.data.values.row + 1) { // Space to the right of the piece? piece.data.values.row++; spacer.data.values.row--; spacer.x -= this.pieceWidth; this.slidePiece(piece, piece.x + this.pieceWidth, piece.y); } } } /** * Slides the piece into the position previously occupied by the spacer. * Uses a tween (see slideSpeed and slideEase for controls). * When complete, calls tweenOver. */ slidePiece (piece, x, y) { this.action = SlidingPuzzle.TWEENING; this.sound.play('move'); this.tweens.add({ targets: piece, x, y, duration: this.slideSpeed, ease: this.slideEase, onComplete: () => this.tweenOver() }); } /** * Called when a piece finishes sliding into place. * First checks if the puzzle is solved. If not, allows the player to carry on. */ tweenOver () { // Are all the pieces in the right place? let outOfSequence = false; this.pieces.each(piece => { if (piece.data.values.correctRow !== piece.data.values.row || piece.data.values.correctColumn !== piece.data.values.column) { outOfSequence = true; } }); if (outOfSequence) { // Not correct, so let the player carry on. this.action = SlidingPuzzle.ALLOW_CLICK; } else { // If we get this far then the sequence is correct and the puzzle is solved. // Fade the missing piece back in ... // When the tween finishes we'll let them click to start the next round this.sound.play('win'); this.tweens.add({ targets: this.spacer, alpha: 1, duration: this.slideSpeed * 2, ease: 'linear', onComplete: () => { this.input.once('pointerdown', this.nextRound, this); } }); this.pieces.each(piece => { piece.setPostPipeline('ShinePostFX'); }); } } /** * Starts the next round of the game. * * In this template it cycles between the 3 pictures, increasing the iterations and complexity * as it progresses. But you can replace this with whatever you need - perhaps returning to * a main menu to select a new puzzle? */ nextRound () { let size; let iterations; let nextPhoto; if (this.photo === 'pic1') { nextPhoto = 'pic2'; iterations = 20; size = 4; } else if (this.photo === 'pic2') { nextPhoto = 'pic3'; iterations = 30; size = 5; } else { // Back to the start again nextPhoto = 'pic1'; iterations = 10; size = 3; } this.reveal = this.add.image(this.pieces.x, this.pieces.y, nextPhoto).setOrigin(0, 0); this.reveal.setPostPipeline('WipePostFX'); const pipeline = this.reveal.getPostPipeline('WipePostFX'); pipeline.setTopToBottom(); pipeline.setRevealEffect(); this.tweens.add({ targets: pipeline, progress: 1, duration: 2000, onComplete: () => { this.photo = nextPhoto; this.iterations = iterations; this.reveal.destroy(); this.startPuzzle(nextPhoto, size, size); } }); } }
import Game from './Game.js'; import MainMenu from './MainMenu.js'; import Preloader from './Preloader.js'; import ShinePostFX from './ShinePostFX.js'; import WipePostFX from './WipePostFX.js'; const config = { type: Phaser.AUTO, width: 1024, height: 768, backgroundColor: '#002157', parent: 'phaser-example', scene: [ Preloader, MainMenu, Game ], pipeline: { ShinePostFX, WipePostFX } }; let game = new Phaser.Game(config);
/** * @author Richard Davey
* @copyright 2023 Photon Storm Ltd. */ const fragShader = ` #define SHADER_NAME WIPE_FS precision mediump float; uniform sampler2D uMainSampler; uniform sampler2D uMainSampler2; uniform vec2 uResolution; uniform vec4 uInput; uniform float uReveal; void main () { vec2 uv = gl_FragCoord.xy / uResolution.xy; vec4 color0; vec4 color1; if (uReveal == 0.0) { color0 = texture2D(uMainSampler, uv); color1 = texture2D(uMainSampler2, vec2(uv.x, 1.0 - uv.y)); } else { color0 = texture2D(uMainSampler2, vec2(uv.x, 1.0 - uv.y)); color1 = texture2D(uMainSampler, uv); } float distance = uInput.x; float width = uInput.y; float direction = uInput.z; float axis = uv.x; if (uInput.w == 1.0) { axis = uv.y; } float adjust = mix(width, -width, distance); float value = smoothstep(distance - width, distance + width, abs(direction - axis) + adjust); gl_FragColor = mix(color1, color0, value); } `; export default class WipePostFX extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { constructor (game) { super({ game, name: 'WipePostFX', fragShader }); this.progress = 0; this.wipeWidth = 0.1; this.direction = 0; this.axis = 0; this.reveal = 0; this.wipeTexture; } onBoot () { this.setTexture(); } setWipeWidth (width = 0.1) { this.wipeWidth = width; return this; } setLeftToRight () { this.direction = 0; this.axis = 0; return this; } setRightToLeft () { this.direction = 1; this.axis = 0; return this; } setTopToBottom () { this.direction = 1; this.axis = 1; return this; } setBottomToTop () { this.direction = 0; this.axis = 1; return this; } setWipeEffect () { this.reveal = 0; this.progress = 0; return this; } setRevealEffect () { this.wipeTexture = this.game.textures.getFrame('__DEFAULT').glTexture; this.reveal = 1; this.progress = 0; return this; } setTexture (texture = '__DEFAULT') { const phaserTexture = this.game.textures.getFrame(texture); if (phaserTexture) { this.wipeTexture = phaserTexture.glTexture; } else { this.wipeTexture = this.game.textures.getFrame('__DEFAULT').glTexture; } this.set1i('uMainSampler2', 1); return this; } setProgress (value = 0) { this.progress = value; return this; } onPreRender () { this.set4f('uInput', this.progress, this.wipeWidth, this.direction, this.axis); this.set1f('uReveal', this.reveal); } onDraw (renderTarget) { this.set2f('uResolution', renderTarget.width, renderTarget.height); this.bindTexture(this.wipeTexture, 1); this.bindAndDraw(renderTarget); } }
Scan to open on your mobile device
export default class MainMenu extends Phaser.Scene { constructor () { super('MainMenu'); } create () { this.add.image(512, 384, 'background'); const box = this.add.image(512, 384, 'box'); const logo = this.add.image(512, -384, 'logo'); box.setPostPipeline('WipePostFX'); logo.setPostPipeline('ShinePostFX'); const pipeline = box.getPostPipeline('WipePostFX'); pipeline.setTopToBottom(); pipeline.setRevealEffect(); this.tweens.add({ targets: pipeline, progress: 1, duration: 3000 }); this.tweens.add({ targets: logo, y: 384, delay: 1500, duration: 2000, ease: 'sine.out', onComplete: () => { this.input.once('pointerdown', () => { pipeline.setWipeEffect(); pipeline.setTexture('box-inside'); this.tweens.add({ targets: logo, alpha: 0, duration: 1000, ease: 'sine.out' }); this.tweens.add({ targets: pipeline, progress: 1, duration: 2500, onComplete: () => { this.scene.start('Game'); } }); }); } }); } }
export default class Preloader extends Phaser.Scene { constructor () { super('Preloader'); } preload () { this.load.setBaseURL('https://cdn.phaserfiles.com/v385'); this.load.setPath('assets/games/sliding-puzzle/'); this.load.image('background', 'background.png'); this.load.image('logo', 'logo.png'); this.load.image('box', 'box.png'); this.load.image('box-inside', 'box-inside.png'); this.load.image('pic1', 'pic1.png'); this.load.image('pic2', 'pic2.png'); this.load.image('pic3', 'pic3.png'); this.load.setPath('assets/games/sliding-puzzle/audio'); this.load.audio('move', [ 'move.m4a', 'move.wav', 'move.ogg' ]); this.load.audio('win', [ 'win.m4a', 'win.wav', 'win.ogg' ]); } create () { this.scene.start('MainMenu'); } }
/** * @author Richard Davey* @copyright 2023 Photon Storm Ltd. */ const fragShader = ` #define SHADER_NAME SHINE_FS precision mediump float; uniform sampler2D uMainSampler; uniform vec2 uResolution; uniform float uSpeed; uniform float uTime; uniform float uLineWidth; uniform float uGradient; varying vec2 outTexCoord; void mainImage (out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord.xy / uResolution.xy; vec4 t1 = texture2D(uMainSampler, uv); vec4 col1 = vec4(0.3, 0.0, 0.0, 1.0); vec4 col2 = vec4(0.85, 0.85, 0.85, 1.0); vec2 linepos = uv; linepos.x = linepos.x - mod(uTime * uSpeed, 2.0) + 0.5; float y = linepos.x * uGradient; float s = smoothstep(y - uLineWidth, y, linepos.y) - smoothstep(y, y + uLineWidth, linepos.y); fragColor = (((s * col1) + (s * col2)) * t1) + t1; } void main () { mainImage(gl_FragColor, gl_FragCoord.xy); } `; export default class ShinePostFX extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { constructor (game) { super({ game, name: 'ShinePostFX', fragShader }); this.speed = 0.5; this.lineWidth = 0.58; this.gradient = 3; } onPreRender () { this.setTime('uTime'); this.set1f('uSpeed', this.speed); this.set1f('uLineWidth', this.lineWidth); this.set1f('uGradient', this.gradient); } onDraw (renderTarget) { this.set2f('uResolution', renderTarget.width, renderTarget.height); this.bindAndDraw(renderTarget); } }
const SlidingPuzzle = { ALLOW_CLICK: 0, TWEENING: 1 }; /** * Sliding Puzzle Game Template * ---------------------------- * * This is the classic Sliding Puzzle game. Unlike lots of implementations out there, * we don't use a 'random' starting layout, as otherwise the puzzle will be unsolvable * 50% of the time. Instead we use a puzzle walker function. This allows you to see * the puzzle before-hand, and then it gets all manged up, ready for you to solve. * * You can control the number of iterations, or steps, that the walker goes through. * You can of course provide any image you like to the puzzle, and it'll adapt and resize * without changing much. * * In this example template there are 3 pictures, and as you solve them, the walker * increases in complexity each time, making it harder to solve. * * This web site has some creative tips on solving Sliding Puzzles: * http://www.nordinho.net/vbull/blogs/lunanik/6131-slider-puzzles-solved-once-all.html */ export default class Game extends Phaser.Scene { constructor () { super('Game'); // These are all set in the startPuzzle function this.rows = 0; this.columns = 0; // The width and height of each piece in the puzzle. // Again, this is set automatically in startPuzzle. this.pieceWidth = 0; this.pieceHeight = 0; this.pieces = null; this.spacer = null; // The speed at which the pieces slide, and the tween they use this.slideSpeed = 300; this.slideEase = 'power3'; // The number of iterations the puzzle walker will go through when // scrambling up the puzzle. 10 is a nice and easy puzzle, but // push it higher for much harder ones. this.iterations = 6; // The speed at which the pieces are shuffled at the start. This allows // the player to see the puzzle before trying to solve it. However if // you don't want this, just set the speed to zero and it'll appear // instantly 'scrambled'. this.shuffleSpeed = 200; this.shuffleEase = 'power1'; this.lastMove = null; // The image in the Cache to be used for the puzzle. // Set in the startPuzzle function. this.photo = ''; this.slices = []; this.action = SlidingPuzzle.ALLOW_CLICK; } create () { this.add.image(512, 384, 'background'); this.add.image(512, 384, 'box-inside'); window.solve = () => { this.nextRound(); }; this.startPuzzle('pic1', 3, 3); } /** * This function is responsible for building the puzzle. * It takes an Image key and a width and height of the puzzle (in pieces, not pixels). * Read the comments within this function to find out what happens. */ startPuzzle (key, rows, columns) { this.photo = key; // The size if the puzzle, in pieces (not pixels) this.rows = rows; this.columns = columns; // The size of the source image const texture = this.textures.getFrame(key); const photoWidth = texture.width; const photoHeight = texture.height; // Create our sliding pieces // Each piece will be this size: const pieceWidth = photoWidth / rows; const pieceHeight = photoHeight / columns; this.pieceWidth = pieceWidth; this.pieceHeight = pieceHeight; // A Container to put the pieces in if (this.pieces) { this.pieces.removeAll(true); } else { // The position sets the top-left of the container for the pieces to expand down from this.pieces = this.add.container(194, 66); } // An array to put the texture slices in if (this.slices) { this.slices.forEach(slice => slice.destroy()); this.slices = []; } let i = 0; // Loop through the image and create a new Sprite for each piece of the puzzle. for (let y = 0; y < this.columns; y++) { for (let x = 0; x < this.rows; x++) { // remove old textures const slice = this.textures.addDynamicTexture(`slice${i}`, pieceWidth, pieceHeight); const ox = 0 + (x / this.rows); const oy = 0 + (y / this.columns); slice.stamp(key, null, 0, 0, { originX: ox, originY: oy }); this.slices.push(slice); const piece = this.add.image(x * pieceWidth, y * pieceHeight, `slice${i}`); piece.setOrigin(0, 0); // The current row and column of the piece // Store the row and column the piece _should_ be in, when the puzzle is solved piece.setData({ row: x, column: y, correctRow: x, correctColumn: y }); piece.setInteractive(); piece.on('pointerdown', () => this.checkPiece(piece)); this.pieces.add(piece); i++; } } // The last piece will be our 'spacer' to slide in to this.spacer = this.pieces.getAt(this.pieces.length - 1); this.spacer.alpha = 0; this.lastMove = null; this.shufflePieces(); } /** * This shuffles up our puzzle. * * We can't just 'randomize' the tiles, or 50% of the time we'll get an * unsolvable puzzle. So instead lets walk it, making non-repeating random moves. */ shufflePieces () { // Push all available moves into this array const moves = []; const spacerCol = this.spacer.data.get('column'); const spacerRow = this.spacer.data.get('row'); if (spacerCol > 0 && this.lastMove !== Phaser.DOWN) { moves.push(Phaser.UP); } if (spacerCol < this.columns - 1 && this.lastMove !== Phaser.UP) { moves.push(Phaser.DOWN); } if (spacerRow > 0 && this.lastMove !== Phaser.RIGHT) { moves.push(Phaser.LEFT); } if (spacerRow < this.rows - 1 && this.lastMove !== Phaser.LEFT) { moves.push(Phaser.RIGHT); } // Pick a move at random from the array this.lastMove = Phaser.Utils.Array.GetRandom(moves); // Then move the spacer into the new position switch (this.lastMove) { case Phaser.UP: this.swapPiece(spacerRow, spacerCol - 1); break; case Phaser.DOWN: this.swapPiece(spacerRow, spacerCol + 1); break; case Phaser.LEFT: this.swapPiece(spacerRow - 1, spacerCol); break; case Phaser.RIGHT: this.swapPiece(spacerRow + 1, spacerCol); break; } } /** * Swaps the spacer with the piece in the given row and column. */ swapPiece (row, column) { // row and column is the new destination of the spacer const piece = this.getPiece(row, column); const spacer = this.spacer; const x = spacer.x; const y = spacer.y; // piece.data.set({ // row: spacer.data.get('row'), // column: spacer.data.get('column') // }); piece.data.values.row = spacer.data.values.row; piece.data.values.column = spacer.data.values.column; spacer.data.values.row = row; spacer.data.values.column = column; // spacer.data.set({ // row, // column // }); // this.spacer.data.row = row; // this.spacer.data.column = column; spacer.setPosition(piece.x, piece.y); // If we don't want them to watch the puzzle get shuffled, then just // set the piece to the new position immediately. if (this.shuffleSpeed === 0) { piece.setPosition(x, y); if (this.iterations > 0) { // Any more iterations left? If so, shuffle, otherwise start play this.iterations--; this.shufflePieces(); } else { this.startPlay(); } } else { // Otherwise, tween it into place const tween = this.tweens.add({ targets: piece, x, y, duration: this.shuffleSpeed, ease: this.shuffleEase }); if (this.iterations > 0) { // Any more iterations left? If so, shuffle, otherwise start play this.iterations--; tween.on('complete', this.shufflePieces, this); } else { tween.on('complete', this.startPlay, this); } } } /** * Gets the piece at row and column. */ getPiece (row, column) { for (let i = 0; i < this.pieces.length; i++) { const piece = this.pieces.getAt(i); if (piece.data.get('row') === row && piece.data.get('column') === column) { return piece; } } return null; } /** * Sets the game state to allow the user to click. */ startPlay () { this.action = SlidingPuzzle.ALLOW_CLICK; } /** * Called when the user clicks on any of the puzzle pieces. * It first checks to see if the piece is adjacent to the 'spacer', and if not, bails out. * If it is, the two pieces are swapped by calling `this.slidePiece`. */ checkPiece (piece) { if (this.action !== SlidingPuzzle.ALLOW_CLICK) { return; } // Only allowed if adjacent to the 'spacer' // // Remember: // // Columns = vertical (y) axis // Rows = horizontal (x) axis const spacer = this.spacer; if (piece.data.values.row === spacer.data.values.row) { if (spacer.data.values.column === piece.data.values.column - 1) { // Space above the piece? piece.data.values.column--; spacer.data.values.column++; spacer.y += this.pieceHeight; this.slidePiece(piece, piece.x, piece.y - this.pieceHeight); } else if (spacer.data.values.column === piece.data.values.column + 1) { // Space below the piece? piece.data.values.column++; spacer.data.values.column--; spacer.y -= this.pieceHeight; this.slidePiece(piece, piece.x, piece.y + this.pieceHeight); } } else if (piece.data.values.column === spacer.data.values.column) { if (spacer.data.values.row === piece.data.values.row - 1) { // Space to the left of the piece? piece.data.values.row--; spacer.data.values.row++; spacer.x += this.pieceWidth; this.slidePiece(piece, piece.x - this.pieceWidth, piece.y); } else if (spacer.data.values.row === piece.data.values.row + 1) { // Space to the right of the piece? piece.data.values.row++; spacer.data.values.row--; spacer.x -= this.pieceWidth; this.slidePiece(piece, piece.x + this.pieceWidth, piece.y); } } } /** * Slides the piece into the position previously occupied by the spacer. * Uses a tween (see slideSpeed and slideEase for controls). * When complete, calls tweenOver. */ slidePiece (piece, x, y) { this.action = SlidingPuzzle.TWEENING; this.sound.play('move'); this.tweens.add({ targets: piece, x, y, duration: this.slideSpeed, ease: this.slideEase, onComplete: () => this.tweenOver() }); } /** * Called when a piece finishes sliding into place. * First checks if the puzzle is solved. If not, allows the player to carry on. */ tweenOver () { // Are all the pieces in the right place? let outOfSequence = false; this.pieces.each(piece => { if (piece.data.values.correctRow !== piece.data.values.row || piece.data.values.correctColumn !== piece.data.values.column) { outOfSequence = true; } }); if (outOfSequence) { // Not correct, so let the player carry on. this.action = SlidingPuzzle.ALLOW_CLICK; } else { // If we get this far then the sequence is correct and the puzzle is solved. // Fade the missing piece back in ... // When the tween finishes we'll let them click to start the next round this.sound.play('win'); this.tweens.add({ targets: this.spacer, alpha: 1, duration: this.slideSpeed * 2, ease: 'linear', onComplete: () => { this.input.once('pointerdown', this.nextRound, this); } }); this.pieces.each(piece => { piece.setPostPipeline('ShinePostFX'); }); } } /** * Starts the next round of the game. * * In this template it cycles between the 3 pictures, increasing the iterations and complexity * as it progresses. But you can replace this with whatever you need - perhaps returning to * a main menu to select a new puzzle? */ nextRound () { let size; let iterations; let nextPhoto; if (this.photo === 'pic1') { nextPhoto = 'pic2'; iterations = 20; size = 4; } else if (this.photo === 'pic2') { nextPhoto = 'pic3'; iterations = 30; size = 5; } else { // Back to the start again nextPhoto = 'pic1'; iterations = 10; size = 3; } this.reveal = this.add.image(this.pieces.x, this.pieces.y, nextPhoto).setOrigin(0, 0); this.reveal.setPostPipeline('WipePostFX'); const pipeline = this.reveal.getPostPipeline('WipePostFX'); pipeline.setTopToBottom(); pipeline.setRevealEffect(); this.tweens.add({ targets: pipeline, progress: 1, duration: 2000, onComplete: () => { this.photo = nextPhoto; this.iterations = iterations; this.reveal.destroy(); this.startPuzzle(nextPhoto, size, size); } }); } }
import Game from './Game.js'; import MainMenu from './MainMenu.js'; import Preloader from './Preloader.js'; import ShinePostFX from './ShinePostFX.js'; import WipePostFX from './WipePostFX.js'; const config = { type: Phaser.AUTO, width: 1024, height: 768, backgroundColor: '#002157', parent: 'phaser-example', scene: [ Preloader, MainMenu, Game ], pipeline: { ShinePostFX, WipePostFX } }; let game = new Phaser.Game(config);
/** * @author Richard Davey* @copyright 2023 Photon Storm Ltd. */ const fragShader = ` #define SHADER_NAME WIPE_FS precision mediump float; uniform sampler2D uMainSampler; uniform sampler2D uMainSampler2; uniform vec2 uResolution; uniform vec4 uInput; uniform float uReveal; void main () { vec2 uv = gl_FragCoord.xy / uResolution.xy; vec4 color0; vec4 color1; if (uReveal == 0.0) { color0 = texture2D(uMainSampler, uv); color1 = texture2D(uMainSampler2, vec2(uv.x, 1.0 - uv.y)); } else { color0 = texture2D(uMainSampler2, vec2(uv.x, 1.0 - uv.y)); color1 = texture2D(uMainSampler, uv); } float distance = uInput.x; float width = uInput.y; float direction = uInput.z; float axis = uv.x; if (uInput.w == 1.0) { axis = uv.y; } float adjust = mix(width, -width, distance); float value = smoothstep(distance - width, distance + width, abs(direction - axis) + adjust); gl_FragColor = mix(color1, color0, value); } `; export default class WipePostFX extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { constructor (game) { super({ game, name: 'WipePostFX', fragShader }); this.progress = 0; this.wipeWidth = 0.1; this.direction = 0; this.axis = 0; this.reveal = 0; this.wipeTexture; } onBoot () { this.setTexture(); } setWipeWidth (width = 0.1) { this.wipeWidth = width; return this; } setLeftToRight () { this.direction = 0; this.axis = 0; return this; } setRightToLeft () { this.direction = 1; this.axis = 0; return this; } setTopToBottom () { this.direction = 1; this.axis = 1; return this; } setBottomToTop () { this.direction = 0; this.axis = 1; return this; } setWipeEffect () { this.reveal = 0; this.progress = 0; return this; } setRevealEffect () { this.wipeTexture = this.game.textures.getFrame('__DEFAULT').glTexture; this.reveal = 1; this.progress = 0; return this; } setTexture (texture = '__DEFAULT') { const phaserTexture = this.game.textures.getFrame(texture); if (phaserTexture) { this.wipeTexture = phaserTexture.glTexture; } else { this.wipeTexture = this.game.textures.getFrame('__DEFAULT').glTexture; } this.set1i('uMainSampler2', 1); return this; } setProgress (value = 0) { this.progress = value; return this; } onPreRender () { this.set4f('uInput', this.progress, this.wipeWidth, this.direction, this.axis); this.set1f('uReveal', this.reveal); } onDraw (renderTarget) { this.set2f('uResolution', renderTarget.width, renderTarget.height); this.bindTexture(this.wipeTexture, 1); this.bindAndDraw(renderTarget); } }