From dfb22f10446dbf92be9d40e6e097a8af715386a0 Mon Sep 17 00:00:00 2001 From: photonstorm Date: Wed, 6 Nov 2013 16:46:21 +0000 Subject: [PATCH] Tracked down an evil bug in Group.swap that caused the linked list to get corrupted in an upward (B to A) neighbour swap. --- README.md | 1 + examples/wip/sort.js | 82 ++++ examples/wip/swap.js | 89 ++++ resources/Project Templates/Basic/MainMenu.js | 2 +- .../Project Templates/Basic/Preloader.js | 11 +- resources/Project Templates/Basic/index.html | 3 +- src/core/Game.js | 2 + src/core/Group.js | 437 +++++++++++++----- 8 files changed, 496 insertions(+), 131 deletions(-) create mode 100644 examples/wip/sort.js create mode 100644 examples/wip/swap.js diff --git a/README.md b/README.md index 06f53253..3d420394 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Version 1.1.3 - in build * New: StageScaleMode.forceOrientation allows you to lock your game to one orientation and display a Sprite (i.e. a "please rotate" screen) when incorrect. * New: World.visible boolean added, toggles rendering of the world on/off entirely. * New: Polygon class & drawPolygon method added to Graphics (thanks rjimenezda) +* New: Added Group.iterate, a powerful way to count or return child that match a certain criteria. Refactored Group to use iterate, lots of repeated code cut. * Fixed: Mouse.stop now uses the true useCapture, which means the event listeners stop listening correctly (thanks beeglebug) * Fixed: Input Keyboard example fix (thanks Atrodilla) * Updated: ArcadePhysics.updateMotion applies the dt to the velocity calculations as well as position now (thanks jcs) diff --git a/examples/wip/sort.js b/examples/wip/sort.js new file mode 100644 index 00000000..9b39682c --- /dev/null +++ b/examples/wip/sort.js @@ -0,0 +1,82 @@ +var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create, update: update, render: render }); + +function preload() { + + game.load.image('phaser', 'assets/sprites/phaser-dude.png'); + game.load.spritesheet('veggies', 'assets/sprites/fruitnveg32wh37.png', 32, 32); + +} + +var sprite; +var group; +var oldY = 0; + +function create() { + + game.stage.backgroundColor = '#2d2d2d'; + + // sprite = game.add.sprite(32, 200, 'phaser'); + // sprite.name = 'phaser-dude'; + + group = game.add.group(); + + sprite = group.create(300, 200, 'phaser'); + sprite.name = 'phaser-dude'; + + for (var i = 0; i < 10; i++) + { + var c = group.create(100 + Math.random() * 700, game.world.randomY, 'veggies', game.rnd.integerInRange(0, 36)); + c.name = 'veg' + i; + } + + game.input.onUp.add(sortGroup, this); + game.input.keyboard.addKeyCapture([ Phaser.Keyboard.LEFT, Phaser.Keyboard.RIGHT, Phaser.Keyboard.UP, Phaser.Keyboard.DOWN ]); + +} + +function sortGroup () { + + console.log('%c ', 'background: #efefef'); + group.sort(); + group.dump(false); + +} + +function update() { + + sprite.body.velocity.x = 0; + sprite.body.velocity.y = 0; + + if (game.input.keyboard.isDown(Phaser.Keyboard.LEFT)) + { + sprite.body.velocity.x = -200; + } + else if (game.input.keyboard.isDown(Phaser.Keyboard.RIGHT)) + { + sprite.body.velocity.x = 200; + } + + if (game.input.keyboard.isDown(Phaser.Keyboard.UP)) + { + sprite.body.velocity.y = -200; + } + else if (game.input.keyboard.isDown(Phaser.Keyboard.DOWN)) + { + sprite.body.velocity.y = 200; + } + + if (sprite.y !== oldY) + { + // console.log('sorted'); + // group.sort(); + // oldY = sprite.y; + } + +} + +function render() { + + // game.debug.renderText(group.cursor.name, 32, 32); + // game.debug.renderInputInfo(32, 32); + +} \ No newline at end of file diff --git a/examples/wip/swap.js b/examples/wip/swap.js new file mode 100644 index 00000000..ab0de747 --- /dev/null +++ b/examples/wip/swap.js @@ -0,0 +1,89 @@ +var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create, update: update, render: render }); + +function preload() { + + game.load.image('phaser', 'assets/sprites/phaser-dude.png'); + game.load.spritesheet('veggies', 'assets/sprites/fruitnveg32wh37.png', 32, 32); + +} + +var group; +var start = false; +var swapCount = 0; +var time = 0; +var test = 0; + +function create() { + + game.stage.backgroundColor = '#2d2d2d'; + + group = game.add.group(); + + for (var i = 0; i < 10; i++) + { + var c = group.create(100 + Math.random() * 700, game.world.randomY, 'veggies', game.rnd.integerInRange(0, 36)); + c.name = 'veg' + i; + } + + test = group.length; + + game.input.onUp.add(toggleSwap, this); + +} + +function toggleSwap () { + + if (start) + { + start = false; + } + else + { + start = true; + } + +} + +function update() { + + if (start && game.time.now > time) + { + var a = group.getRandom(); + var b = group.getRandom(); + + if (a.name !== b.name) + { + console.log('************************ NEW ROUND *********************'); + group.dump(true); + console.log('Group Size: ' + group.length); + group.swap(a, b); + swapCount++; + + if (group.length !== test) + { + start = false; + console.log('************************ SHIT *********************'); + group.dump(true); + console.log('************************ SHIT *********************'); + } + + if (group.validate() == false) + { + start = false; + console.log('************************ VALIDATE FAIL *********************'); + group.dump(true); + console.log('************************ VALIDATE FAIL *********************'); + } + + } + + time = game.time.now + 100; + } + +} + +function render() { + + game.debug.renderText('Swap: ' + swapCount, 32, 32); + +} \ No newline at end of file diff --git a/resources/Project Templates/Basic/MainMenu.js b/resources/Project Templates/Basic/MainMenu.js index 968bca66..fe2a1ae1 100644 --- a/resources/Project Templates/Basic/MainMenu.js +++ b/resources/Project Templates/Basic/MainMenu.js @@ -10,7 +10,7 @@ BasicGame.MainMenu.prototype = { create: function () { - // We've already preloaded our assets, so let's kick right into the Main Menu itself + // We've already preloaded our assets, so let's kick right into the Main Menu itself. // Here all we're doing is playing some music and adding a picture and button // Naturally I expect you to do something significantly better :) diff --git a/resources/Project Templates/Basic/Preloader.js b/resources/Project Templates/Basic/Preloader.js index 1b7e2c81..1eefba51 100644 --- a/resources/Project Templates/Basic/Preloader.js +++ b/resources/Project Templates/Basic/Preloader.js @@ -17,12 +17,13 @@ BasicGame.Preloader.prototype = { this.background = this.add.sprite(0, 0, 'preloaderBackground'); this.preloadBar = this.add.sprite(300, 400, 'preloaderBar'); - // This sets the preloadBar sprite as a loader sprite, basically - // what that does is automatically crop the sprite from 0 to full-width + // This sets the preloadBar sprite as a loader sprite. + // What that does is automatically crop the sprite from 0 to full-width // as the files below are loaded in. this.load.setPreloadSprite(this.preloadBar); - // Here we load most of the assets our game needs + // Here we load the rest of the assets our game needs. + // As this is just a Project Template I've not provided these assets, swap them for your own. this.load.image('titlepage', 'images/title.jpg'); this.load.atlas('playButton', 'images/play_button.png', 'images/play_button.json'); this.load.audio('titleMusic', ['audio/main_menu.mp3']); @@ -33,7 +34,7 @@ BasicGame.Preloader.prototype = { create: function () { - // Once the load has finished we disable the crop because we're going to sit in the update loop for a short while + // Once the load has finished we disable the crop because we're going to sit in the update loop for a short while as the music decodes this.preloadBar.cropEnabled = false; }, @@ -51,7 +52,7 @@ BasicGame.Preloader.prototype = { if (this.cache.isSoundDecoded('titleMusic') && this.ready == false) { - this.ready = false; + this.ready = true; this.game.state.start('MainMenu'); } diff --git a/resources/Project Templates/Basic/index.html b/resources/Project Templates/Basic/index.html index 0c85cf8e..76d93862 100644 --- a/resources/Project Templates/Basic/index.html +++ b/resources/Project Templates/Basic/index.html @@ -3,6 +3,7 @@ Phaser Basic Project Template + @@ -17,7 +18,7 @@ window.onload = function() { // Create your Phaser game and inject it into the gameContainer div. - // We did it in a window.onload event, but you can do it anywhere (requireJS load, anonymous function, jQuery dom ready, etc - whatever floats your boat) + // We did it in a window.onload event, but you can do it anywhere (requireJS load, anonymous function, jQuery dom ready, - whatever floats your boat) var game = new Phaser.Game(1024, 768, Phaser.AUTO, 'gameContainer'); // Add the States your game has. diff --git a/src/core/Game.js b/src/core/Game.js index 9b37005b..f6dbc4f7 100644 --- a/src/core/Game.js +++ b/src/core/Game.js @@ -425,6 +425,8 @@ Phaser.Game.prototype = { if (this._paused) { this.renderer.render(this.stage._stage); + this.plugins.render(); + this.state.render(); } else { diff --git a/src/core/Group.js b/src/core/Group.js index 28070872..2b1c7c4d 100644 --- a/src/core/Group.js +++ b/src/core/Group.js @@ -89,8 +89,50 @@ Phaser.Group = function (game, parent, name, useStage) { */ this.cursor = null; + /** + * Helper for sort. + */ + this._sortIndex = ''; + + /** + * Helper for sort. + */ + this._sortOrder = 0; + + this._sortCache = []; + }; +/** +* @constant +* @type {number} +*/ +Phaser.Group.RETURN_NONE = 0; + +/** +* @constant +* @type {number} +*/ +Phaser.Group.RETURN_TOTAL = 1; + +/** +* @constant +* @type {number} +*/ +Phaser.Group.RETURN_CHILD = 2; + +/** +* @constant +* @type {number} +*/ +Phaser.Group.SORT_ASCENDING = -1; + +/** +* @constant +* @type {number} +*/ +Phaser.Group.SORT_DESCENDING = 1; + Phaser.Group.prototype = { /** @@ -303,8 +345,37 @@ Phaser.Group.prototype = { }, + childTest: function (prefix, child) { + + var s = prefix + ' next: '; + + if (child._iNext) + { + s = s + child._iNext.name; + } + else + { + s = s + '-null-'; + } + + s = s + ' ' + prefix + ' prev: '; + + if (child._iPrev) + { + s = s + child._iPrev.name; + } + else + { + s = s + '-null-'; + } + + console.log(s); + + }, + /** * Swaps the position of two children in this Group. + * You cannot swap a child with itself, or swap un-parented children, doing so will return false. * * @method Phaser.Group#swap * @param {*} child1 - The first child to swap. @@ -313,9 +384,11 @@ Phaser.Group.prototype = { */ swap: function (child1, child2) { + console.log('starting swap', child1.name, 'with', child2.name); + if (child1 === child2 || !child1.parent || !child2.parent) { - console.warn('You cannot swap a child with itself or swap un-parented children'); + console.log('cannot swap these') return false; } @@ -325,8 +398,17 @@ Phaser.Group.prototype = { var child2Prev = child2._iPrev; var child2Next = child2._iNext; - var endNode = this._container.last._iNext; + // var endNode = this._container.last._iNext; + var endNode = this._container.last; var currentNode = this.game.stage._stage; + + console.log('start do while. start node: ', currentNode.name); + console.log(typeof endNode); + + if (endNode) + { + console.log('end node: ', endNode.name); + } do { @@ -355,9 +437,15 @@ Phaser.Group.prototype = { } while (currentNode != endNode) + console.log('end do while'); + if (child1._iNext == child2) { // This is a downward (A to B) neighbour swap + console.log('downward A to B'); + this.childTest('1', child1); + this.childTest('2', child2); + child1._iNext = child2Next; child1._iPrev = child2; child2._iNext = child1; @@ -381,13 +469,17 @@ Phaser.Group.prototype = { else if (child2._iNext == child1) { // This is an upward (B to A) neighbour swap + console.log('upward B to A'); + this.childTest('1', child1); + this.childTest('2', child2); + child1._iNext = child2; child1._iPrev = child2Prev; child2._iNext = child1Next; child2._iPrev = child1; if (child2Prev) { child2Prev._iNext = child1; } - if (child1Next) { child2Next._iPrev = child2; } + if (child1Next) { child1Next._iPrev = child2; } if (child1.__renderGroup) { @@ -404,6 +496,11 @@ Phaser.Group.prototype = { else { // Children are far apart + console.log('far apart A to B'); + + this.childTest('1', child1); + this.childTest('2', child2); + child1._iNext = child2Next; child1._iPrev = child2Prev; child2._iNext = child1Next; @@ -754,7 +851,7 @@ Phaser.Group.prototype = { /** * Calls a function on all of the children regardless if they are dead or alive (see callAllExists if you need control over that) - * After the method parameter you can add as many extra parameters as you like, which will all be passed to the child. + * After the method parameter and context you can add as many extra parameters as you like, which will all be passed to the child. * * @method Phaser.Group#callAll * @param {string} method - A string containing the name of the function that will be called. The function must exist on the child. @@ -860,6 +957,24 @@ Phaser.Group.prototype = { }, + /** + * Allows you to call your own function on each alive member of this Group (where child.alive=true). You must pass the callback and context in which it will run. + * You can add as many parameters as you like, which will all be passed to the callback along with the child. + * For example: Group.forEachAlive(causeDamage, this, 500) + * + * @method Phaser.Group#forEachAlive + * @param {function} callback - The function that will be called. Each child of the Group will be passed to it as its first parameter. + * @param {Object} callbackContext - The context in which the function should be called (usually 'this'). + */ + forEachExists: function (callback, callbackContext) { + + var args = Array.prototype.splice.call(arguments, 2); + args.unshift(null); + + this.iterate('exists', true, Phaser.Group.RETURN_TOTAL, callback, callbackContext, args); + + }, + /** * Allows you to call your own function on each alive member of this Group (where child.alive=true). You must pass the callback and context in which it will run. * You can add as many parameters as you like, which will all be passed to the callback along with the child. @@ -874,23 +989,7 @@ Phaser.Group.prototype = { var args = Array.prototype.splice.call(arguments, 2); args.unshift(null); - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (currentNode.alive) - { - args[0] = currentNode; - callback.apply(callbackContext, args); - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - - } + this.iterate('alive', true, Phaser.Group.RETURN_TOTAL, callback, callbackContext, args); }, @@ -908,23 +1007,159 @@ Phaser.Group.prototype = { var args = Array.prototype.splice.call(arguments, 2); args.unshift(null); + this.iterate('alive', false, Phaser.Group.RETURN_TOTAL, callback, callbackContext, args); + + }, + + /** + * Call this function to sort the group according to a particular value and order. + * For example, to sort game objects for Zelda-style overlaps you might call + * myGroup.sort("y",Group.ASCENDING) at the bottom of your + * State.update() override. To sort all existing objects after + * a big explosion or bomb attack, you might call myGroup.sort("exists",Group.DESCENDING). + * + * @param {string} index The string name of the member variable you want to sort on. Default value is "z". + * @param {number} order A Group constant that defines the sort order. Possible values are Group.ASCENDING and Group.DESCENDING. Default value is Group.ASCENDING. + */ + sort: function (index, order) { + + if (typeof index === 'undefined') { index = 'y'; } + if (typeof order === 'undefined') { order = Phaser.Group.SORT_ASCENDING; } + + this._sortIndex = index; + this._sortOrder = order; + this._sortCache = this._container.children.slice(); + + console.log('-vvv--------------------------------------------------------------------------------'); + + for (var i = 0; i < this._sortCache.length; i++) + { + console.log(i + ' = ' + this._sortCache[i].name + ' at y: ' + this._sortCache[i].y); + } + + console.log('---------------------------------------------------------------------------------'); + + this._sortCache.sort(this.sortHandler.bind(this)); + + for (var i = 0; i < this._sortCache.length; i++) + { + console.log(i + ' = ' + this._sortCache[i].name + ' at y: ' + this._sortCache[i].y); + } + + for (var i = 0; i < this._sortCache.length; i++) + { + if (this._container.children[i] !== this._sortCache[i]) + { + console.log('swapped:', this._container.children[i].name,'with',this._sortCache[i].name); + this.swap(this._container.children[i], this._sortCache[i]); + } + } + + // Now put it back again + this._container.children = this._sortCache.slice(); + + this._container.updateTransform(); + + console.log('-^^^--------------------------------------------------------------------------------'); + + }, + + /** + * Helper function for the sort process. + * + * @param {Basic} Obj1 The first object being sorted. + * @param {Basic} Obj2 The second object being sorted. + * + * @return {number} An integer value: -1 (Obj1 before Obj2), 0 (same), or 1 (Obj1 after Obj2). + */ + sortHandler: function (obj1, obj2) { + + if (!obj1 || !obj2) + { + // console.log('null objects in sort', obj1, obj2); + return 0; + } + + // number only test + // return obj1[this._sortIndex] - obj2[this._sortIndex]; + + if (obj1[this._sortIndex] < obj2[this._sortIndex]) + { + // console.log('1 < 2'); + return this._sortOrder; + } + else if (obj1[this._sortIndex] > obj2[this._sortIndex]) + { + // console.log('1 > 2'); + return -this._sortOrder; + } + + return 0; + + }, + + /** + * Iterates over the children of the Group. When a child has a property matching key that equals the given value, it is considered as a match. + * Matched children can be sent to the optional callback, or simply returned or counted. + * You can add as many callback parameters as you like, which will all be passed to the callback along with the child, after the callbackContext parameter. + * + * @method Phaser.Group#iterate + * @param {string} key - The child property to check, i.e. 'exists', 'alive', 'health' + * @param {any} value - If child.key === this value it will be considered a match. Note that a strict comparison is used. + * @param {number} returnType - How to return the data from this method. Either Phaser.Group.RETURN_NONE, Phaser.Group.RETURN_TOTAL or Phaser.Group.RETURN_CHILD. + * @param {function} [callback=null] - Optional function that will be called on each matching child. Each child of the Group will be passed to it as its first parameter. + * @param {Object} [callbackContext] - The context in which the function should be called (usually 'this'). + */ + iterate: function (key, value, returnType, callback, callbackContext, args) { + + if (returnType == Phaser.Group.RETURN_TOTAL && this._container.children.length == 0) + { + return -1; + } + + if (typeof callback === 'undefined') + { + callback = false; + } + + var total = 0; + if (this._container.children.length > 0 && this._container.first._iNext) { var currentNode = this._container.first._iNext; do { - if (currentNode.alive == false) + if (currentNode[key] === value) { - args[0] = currentNode; - callback.apply(callbackContext, args); + total++; + + if (callback) + { + args[0] = currentNode; + callback.apply(callbackContext, args); + } + + if (returnType == Phaser.Group.RETURN_CHILD) + { + return currentNode; + } } currentNode = currentNode._iNext; } while (currentNode != this._container.last._iNext); - } + + if (returnType == Phaser.Group.RETURN_TOTAL) + { + return total; + } + else if (returnType == Phaser.Group.RETURN_CHILD) + { + return null; + } + }, /** @@ -941,23 +1176,7 @@ Phaser.Group.prototype = { state = true; } - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (currentNode.exists === state) - { - return currentNode; - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - } - - return null; + return this.iterate('exists', state, Phaser.Group.RETURN_CHILD); }, @@ -970,23 +1189,7 @@ Phaser.Group.prototype = { */ getFirstAlive: function () { - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (currentNode.alive) - { - return currentNode; - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - } - - return null; + return this.iterate('alive', true, Phaser.Group.RETURN_CHILD); }, @@ -999,23 +1202,7 @@ Phaser.Group.prototype = { */ getFirstDead: function () { - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (!currentNode.alive) - { - return currentNode; - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - } - - return null; + return this.iterate('alive', false, Phaser.Group.RETURN_CHILD); }, @@ -1027,29 +1214,7 @@ Phaser.Group.prototype = { */ countLiving: function () { - var total = 0; - - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (currentNode.alive) - { - total++; - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - } - else - { - total = -1; - } - - return total; + return this.iterate('alive', true, Phaser.Group.RETURN_TOTAL); }, @@ -1061,29 +1226,7 @@ Phaser.Group.prototype = { */ countDead: function () { - var total = 0; - - if (this._container.children.length > 0 && this._container.first._iNext) - { - var currentNode = this._container.first._iNext; - - do - { - if (!currentNode.alive) - { - total++; - } - - currentNode = currentNode._iNext; - } - while (currentNode != this._container.last._iNext); - } - else - { - total = -1; - } - - return total; + return this.iterate('alive', false, Phaser.Group.RETURN_TOTAL); }, @@ -1228,6 +1371,48 @@ Phaser.Group.prototype = { }, + validate: function () { + + var testObject = this.game.stage._stage.last._iNext; + var displayObject = this.game.stage._stage; + var nextObject = null; + var prevObject = null; + var count = 0; + + do + { + if (count > 0) + { + // check next + if (displayObject !== nextObject) + { + console.log('check next fail'); + return false; + } + + // check previous + if (displayObject._iPrev !== prevObject) + { + console.log('check previous fail'); + return false; + } + } + + // Set the next object + nextObject = displayObject._iNext; + prevObject = displayObject; + + displayObject = displayObject._iNext; + + count++; + + } + while(displayObject != testObject) + + return true; + + }, + /** * Dumps out a list of Group children and their index positions to the browser console. Useful for group debugging. * @@ -1252,11 +1437,13 @@ Phaser.Group.prototype = { if (full) { var testObject = this.game.stage._stage.last._iNext; + // var testObject = this.game.stage._stage.last; var displayObject = this.game.stage._stage; } else { var testObject = this._container.last._iNext; + // var testObject = this._container.last; var displayObject = this._container; } @@ -1334,7 +1521,8 @@ Phaser.Group.prototype = { Object.defineProperty(Phaser.Group.prototype, "total", { get: function () { - return this._container.children.length; + return this.iterate('exists', true, Phaser.Group.RETURN_TOTAL); + // return this._container.children.length; } }); @@ -1347,7 +1535,8 @@ Object.defineProperty(Phaser.Group.prototype, "total", { Object.defineProperty(Phaser.Group.prototype, "length", { get: function () { - return this._container.children.length; + return this.iterate('exists', true, Phaser.Group.RETURN_TOTAL); + // return this._container.children.length; } });