UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

759 lines (679 loc) 27.8 kB
/** * @author Richard Davey <rich@phaser.io> * @copyright 2013-2025 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var AnimationState = require('../../animations/AnimationState'); var Class = require('../../utils/Class'); var GenerateGridVerts = require('../../geom/mesh/GenerateGridVerts'); var IntegerToRGB = require('../../display/color/IntegerToRGB'); var Mesh = require('../mesh/Mesh'); var UUID = require('../../utils/string/UUID'); /** * @classdesc * A Plane Game Object. * * The Plane Game Object is a helper class that takes the Mesh Game Object and extends it, * allowing for fast and easy creation of Planes. A Plane is a one-sided grid of cells, * where you specify the number of cells in each dimension. The Plane can have a texture * that is either repeated (tiled) across each cell, or applied to the full Plane. * * The Plane can then be manipulated in 3D space, with rotation across all 3 axis. * * This allows you to create effects not possible with regular Sprites, such as perspective * distortion. You can also adjust the vertices on a per-vertex basis. Plane data becomes * part of the WebGL batch, just like standard Sprites, so doesn't introduce any additional * shader overhead. Because the Plane just generates vertices into the WebGL batch, like any * other Sprite, you can use all of the common Game Object components on a Plane too, * such as a custom pipeline, mask, blend mode or texture. * * You can use the `uvScroll` and `uvScale` methods to adjust the placement and scaling * of the texture if this Plane is using a single texture, and not a frame from a texture * atlas or sprite sheet. * * The Plane Game Object also has the Animation component, allowing you to play animations * across the Plane just as you would with a Sprite. The animation frame size must be fixed * as the first frame will be the size of the entire animation, for example use a `SpriteSheet`. * * Note that the Plane object is WebGL only and does not have a Canvas counterpart. * * The Plane origin is always 0.5 x 0.5 and cannot be changed. * * @class Plane * @extends Phaser.GameObjects.Mesh * @memberof Phaser.GameObjects * @constructor * @since 3.60.0 * * @param {Phaser.Scene} scene - The Scene to which this Plane belongs. A Plane can only belong to one Scene at a time. * @param {number} [x] - The horizontal position of this Plane in the world. * @param {number} [y] - The vertical position of this Plane in the world. * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture this Plane will use to render with, as stored in the Texture Manager. * @param {string|number} [frame] - An optional frame from the Texture this Plane is rendering with. * @param {number} [width=8] - The width of this Plane, in cells, not pixels. * @param {number} [height=8] - The height of this Plane, in cells, not pixels. * @param {boolean} [tile=false] - Is the texture tiled? I.e. repeated across each cell. */ var Plane = new Class({ Extends: Mesh, initialize: function Plane (scene, x, y, texture, frame, width, height, tile) { if (!texture) { texture = '__DEFAULT'; } Mesh.call(this, scene, x, y, texture, frame); this.type = 'Plane'; /** * The Animation State component of this Sprite. * * This component provides features to apply animations to this Sprite. * It is responsible for playing, loading, queuing animations for later playback, * mixing between animations and setting the current animation frame to this Sprite. * * @name Phaser.GameObjects.Plane#anims * @type {Phaser.Animations.AnimationState} * @since 3.60.0 */ this.anims = new AnimationState(this); /** * The width of this Plane in cells, not pixels. * * This value is read-only. To adjust it, see the `setGridSize` method. * * @name Phaser.GameObjects.Plane#gridWidth * @type {number} * @readonly * @since 3.60.0 */ this.gridWidth; /** * The height of this Plane in cells, not pixels. * * This value is read-only. To adjust it, see the `setGridSize` method. * * @name Phaser.GameObjects.Plane#gridHeight * @type {number} * @readonly * @since 3.60.0 */ this.gridHeight; /** * Is the texture of this Plane tiled across all cells, or not? * * This value is read-only. To adjust it, see the `setGridSize` method. * * @name Phaser.GameObjects.Plane#isTiled * @type {boolean} * @readonly * @since 3.60.0 */ this.isTiled; /** * If this Plane has a checkboard texture, this is a reference to * the WebGLTexture being used. Otherwise, it's null. * * @name Phaser.GameObjects.Plane#_checkerboard * @type {?WebGLTexture} * @private * @since 3.60.0 */ this._checkerboard = null; this.hideCCW = false; this.setGridSize(width, height, tile); this.setSizeToFrame(false); this.setViewHeight(); }, /** * Do not change this value. It has no effect other than to break things. * * @name Phaser.GameObjects.Plane#originX * @type {number} * @readonly * @override * @since 3.70.0 */ originX: { get: function () { return 0.5; } }, /** * Do not change this value. It has no effect other than to break things. * * @name Phaser.GameObjects.Plane#originY * @type {number} * @readonly * @override * @since 3.70.0 */ originY: { get: function () { return 0.5; } }, /** * Modifies the layout of this Plane by adjusting the grid dimensions to the * given width and height. The values are given in cells, not pixels. * * The `tile` parameter allows you to control if the texture is tiled, or * applied across the entire Plane? A tiled texture will repeat with one * iteration per cell. A non-tiled texture will be applied across the whole * Plane. * * Note that if this Plane is using a single texture, not from a texture atlas * or sprite sheet, then you can use the `Plane.uvScale` method to have much * more fine-grained control over the texture tiling. * * @method Phaser.GameObjects.Plane#preDestroy * @since 3.60.0 * * @param {number} [width=8] - The width of this Plane, in cells, not pixels. * @param {number} [height=8] - The height of this Plane, in cells, not pixels. * @param {boolean} [tile=false] - Is the texture tiled? I.e. repeated across each cell. */ setGridSize: function (width, height, tile) { if (width === undefined) { width = 8; } if (height === undefined) { height = 8; } if (tile === undefined) { tile = false; } var flipY = false; if (tile) { flipY = true; } this.gridWidth = width; this.gridHeight = height; this.isTiled = tile; this.clear(); GenerateGridVerts({ mesh: this, widthSegments: width, heightSegments: height, isOrtho: false, tile: tile, flipY: flipY }); return this; }, /** * An internal method that resets the perspective projection for this Plane * when it changes texture or frame, and also resets the cell UV coordinates, * if required. * * @method Phaser.GameObjects.Plane#setSizeToFrame * @since 3.60.0 * @override * * @param {boolean} [resetUV=true] - Reset all of the cell UV coordinates? * * @return {this} This Game Object instance. */ setSizeToFrame: function (resetUV) { if (resetUV === undefined) { resetUV = true; } var frame = this.frame; this.setPerspective(this.width / frame.width, this.height / frame.height); if (this._checkerboard && this._checkerboard !== this.texture) { this.removeCheckerboard(); } if (!resetUV) { return this; } // Reset UV coordinates if frame has changed var gridX = this.gridWidth; var gridY = this.gridHeight; var verts = this.vertices; var frameU0 = frame.u0; var frameU1 = frame.u1; var frameV0 = frame.v0; var frameV1 = frame.v1; var x; var y; var i = 0; if (this.isTiled) { // flipY frameV0 = frame.v1; frameV1 = frame.v0; for (y = 0; y < gridY; y++) { for (x = 0; x < gridX; x++) { verts[i++].setUVs(frameU0, frameV1); verts[i++].setUVs(frameU0, frameV0); verts[i++].setUVs(frameU1, frameV1); verts[i++].setUVs(frameU0, frameV0); verts[i++].setUVs(frameU1, frameV0); verts[i++].setUVs(frameU1, frameV1); } } } else { var gridX1 = gridX + 1; var gridY1 = gridY + 1; var frameU = frameU1 - frameU0; var frameV = frameV1 - frameV0; var uvs = []; for (y = 0; y < gridY1; y++) { for (x = 0; x < gridX1; x++) { var tu = frameU0 + frameU * (x / gridX); var tv = frameV0 + frameV * (y / gridY); uvs.push(tu, tv); } } for (y = 0; y < gridY; y++) { for (x = 0; x < gridX; x++) { var a = (x + gridX1 * y) * 2; var b = (x + gridX1 * (y + 1)) * 2; var c = ((x + 1) + gridX1 * (y + 1)) * 2; var d = ((x + 1) + gridX1 * y) * 2; verts[i++].setUVs(uvs[a], uvs[a + 1]); verts[i++].setUVs(uvs[b], uvs[b + 1]); verts[i++].setUVs(uvs[d], uvs[d + 1]); verts[i++].setUVs(uvs[b], uvs[b + 1]); verts[i++].setUVs(uvs[c], uvs[c + 1]); verts[i++].setUVs(uvs[d], uvs[d + 1]); } } } return this; }, /** * Sets the height of this Plane to match the given value, in pixels. * * This adjusts the `Plane.viewPosition.z` value to achieve this. * * If no `value` parameter is given, it will set the view height to match * that of the current texture frame the Plane is using. * * @method Phaser.GameObjects.Plane#setViewHeight * @since 3.60.0 * * @param {number} [value] - The height, in pixels, to set this Plane view height to. */ setViewHeight: function (value) { if (value === undefined) { value = this.frame.height; } var vFOV = this.fov * (Math.PI / 180); this.viewPosition.z = (this.height / value) / (Math.tan(vFOV / 2)); this.dirtyCache[10] = 1; }, /** * Creates a checkerboard style texture, based on the given colors and alpha * values and applies it to this Plane, replacing any current texture it may * have. * * The colors are used in an alternating pattern, like a chess board. * * Calling this method generates a brand new 16x16 pixel WebGLTexture internally * and applies it to this Plane. While quite fast to do, you should still be * mindful of calling this method either extensively, or in tight parts of * your game. * * @method Phaser.GameObjects.Plane#createCheckerboard * @since 3.60.0 * * @param {number} [color1=0xffffff] - The odd cell color, specified as a hex value. * @param {number} [color2=0x0000ff] - The even cell color, specified as a hex value. * @param {number} [alpha1=255] - The odd cell alpha value, specified as a number between 0 and 255. * @param {number} [alpha2=255] - The even cell alpha value, specified as a number between 0 and 255. * @param {number} [height=128] - The view height of the Plane after creation, in pixels. */ createCheckerboard: function (color1, color2, alpha1, alpha2, height) { if (color1 === undefined) { color1 = 0xffffff; } if (color2 === undefined) { color2 = 0x0000ff; } if (alpha1 === undefined) { alpha1 = 255; } if (alpha2 === undefined) { alpha2 = 255; } if (height === undefined) { height = 128; } // Let's assume 16x16 for our texture size and 8x8 cell size var c1 = IntegerToRGB(color1); var c2 = IntegerToRGB(color2); var colors = []; for (var h = 0; h < 16; h++) { for (var w = 0; w < 16; w++) { if ((h < 8 && w < 8) || (h > 7 && w > 7)) { colors.push(c1.r, c1.g, c1.b, alpha1); } else { colors.push(c2.r, c2.g, c2.b, alpha2); } } } var texture = this.scene.sys.textures.addUint8Array(UUID(), new Uint8Array(colors), 16, 16); this.removeCheckerboard(); this.setTexture(texture); this.setSizeToFrame(); this.setViewHeight(height); return this; }, /** * If this Plane has a Checkerboard Texture, this method will destroy it * and reset the internal flag for it. * * @method Phaser.GameObjects.Plane#removeCheckerboard * @since 3.60.0 */ removeCheckerboard: function () { if (this._checkerboard) { this._checkerboard.destroy(); this._checkerboard = null; } }, /** * Start playing the given animation on this Plane. * * Animations in Phaser can either belong to the global Animation Manager, or specifically to this Plane. * * The benefit of a global animation is that multiple Game Objects can all play the same animation, without * having to duplicate the data. You can just create it once and then play it on any animating Game Object. * * The following code shows how to create a global repeating animation. The animation will be created * from all of the frames within the sprite sheet that was loaded with the key 'muybridge': * * ```javascript * var config = { * key: 'run', * frames: 'muybridge', * frameRate: 15, * repeat: -1 * }; * * // This code should be run from within a Scene: * this.anims.create(config); * ``` * * However, if you wish to create an animation that is unique to this Plane, and this Plane alone, * you can call the `Animation.create` method instead. It accepts the exact same parameters as when * creating a global animation, however the resulting data is kept locally in this Plane. * * With the animation created, either globally or locally, you can now play it on this Plane: * * ```javascript * const plane = this.add.plane(...); * plane.play('run'); * ``` * * Alternatively, if you wish to run it at a different frame rate for example, you can pass a config * object instead: * * ```javascript * const plane = this.add.plane(...); * plane.play({ key: 'run', frameRate: 24 }); * ``` * * When playing an animation on a Plane it will first check to see if it can find a matching key * locally within the Plane. If it can, it will play the local animation. If not, it will then * search the global Animation Manager and look for it there. * * If you need a Plane to be able to play both local and global animations, make sure they don't * have conflicting keys. * * See the documentation for the `PlayAnimationConfig` config object for more details about this. * * Also, see the documentation in the Animation Manager for further details on creating animations. * * @method Phaser.GameObjects.Plane#play * @fires Phaser.Animations.Events#ANIMATION_START * @since 3.60.0 * * @param {(string|Phaser.Animations.Animation|Phaser.Types.Animations.PlayAnimationConfig)} key - The string-based key of the animation to play, or an Animation instance, or a `PlayAnimationConfig` object. * @param {boolean} [ignoreIfPlaying=false] - If an animation is already playing then ignore this call. * * @return {this} This Game Object. */ play: function (key, ignoreIfPlaying) { return this.anims.play(key, ignoreIfPlaying); }, /** * Start playing the given animation on this Plane, in reverse. * * Animations in Phaser can either belong to the global Animation Manager, or specifically to a Game Object. * * The benefit of a global animation is that multiple Game Objects can all play the same animation, without * having to duplicate the data. You can just create it once and then play it on any animating Game Object. * * The following code shows how to create a global repeating animation. The animation will be created * from all of the frames within the sprite sheet that was loaded with the key 'muybridge': * * ```javascript * var config = { * key: 'run', * frames: 'muybridge', * frameRate: 15, * repeat: -1 * }; * * // This code should be run from within a Scene: * this.anims.create(config); * ``` * * However, if you wish to create an animation that is unique to this Game Object, and this Game Object alone, * you can call the `Animation.create` method instead. It accepts the exact same parameters as when * creating a global animation, however the resulting data is kept locally in this Game Object. * * With the animation created, either globally or locally, you can now play it on this Game Object: * * ```javascript * const plane = this.add.plane(...); * plane.playReverse('run'); * ``` * * Alternatively, if you wish to run it at a different frame rate, for example, you can pass a config * object instead: * * ```javascript * const plane = this.add.plane(...); * plane.playReverse({ key: 'run', frameRate: 24 }); * ``` * * When playing an animation on a Game Object it will first check to see if it can find a matching key * locally within the Game Object. If it can, it will play the local animation. If not, it will then * search the global Animation Manager and look for it there. * * If you need a Game Object to be able to play both local and global animations, make sure they don't * have conflicting keys. * * See the documentation for the `PlayAnimationConfig` config object for more details about this. * * Also, see the documentation in the Animation Manager for further details on creating animations. * * @method Phaser.GameObjects.Plane#playReverse * @fires Phaser.Animations.Events#ANIMATION_START * @since 3.60.0 * * @param {(string|Phaser.Animations.Animation|Phaser.Types.Animations.PlayAnimationConfig)} key - The string-based key of the animation to play, or an Animation instance, or a `PlayAnimationConfig` object. * @param {boolean} [ignoreIfPlaying=false] - If an animation is already playing then ignore this call. * * @return {this} This Game Object. */ playReverse: function (key, ignoreIfPlaying) { return this.anims.playReverse(key, ignoreIfPlaying); }, /** * Waits for the specified delay, in milliseconds, then starts playback of the given animation. * * If the animation _also_ has a delay value set in its config, it will be **added** to the delay given here. * * If an animation is already running and a new animation is given to this method, it will wait for * the given delay before starting the new animation. * * If no animation is currently running, the given one begins after the delay. * * When playing an animation on a Game Object it will first check to see if it can find a matching key * locally within the Game Object. If it can, it will play the local animation. If not, it will then * search the global Animation Manager and look for it there. * * @method Phaser.GameObjects.Plane#playAfterDelay * @fires Phaser.Animations.Events#ANIMATION_START * @since 3.60.0 * * @param {(string|Phaser.Animations.Animation|Phaser.Types.Animations.PlayAnimationConfig)} key - The string-based key of the animation to play, or an Animation instance, or a `PlayAnimationConfig` object. * @param {number} delay - The delay, in milliseconds, to wait before starting the animation playing. * * @return {this} This Game Object. */ playAfterDelay: function (key, delay) { return this.anims.playAfterDelay(key, delay); }, /** * Waits for the current animation to complete the `repeatCount` number of repeat cycles, then starts playback * of the given animation. * * You can use this to ensure there are no harsh jumps between two sets of animations, i.e. going from an * idle animation to a walking animation, by making them blend smoothly into each other. * * If no animation is currently running, the given one will start immediately. * * When playing an animation on a Game Object it will first check to see if it can find a matching key * locally within the Game Object. If it can, it will play the local animation. If not, it will then * search the global Animation Manager and look for it there. * * @method Phaser.GameObjects.Plane#playAfterRepeat * @fires Phaser.Animations.Events#ANIMATION_START * @since 3.60.0 * * @param {(string|Phaser.Animations.Animation|Phaser.Types.Animations.PlayAnimationConfig)} key - The string-based key of the animation to play, or an Animation instance, or a `PlayAnimationConfig` object. * @param {number} [repeatCount=1] - How many times should the animation repeat before the next one starts? * * @return {this} This Game Object. */ playAfterRepeat: function (key, repeatCount) { return this.anims.playAfterRepeat(key, repeatCount); }, /** * Immediately stops the current animation from playing and dispatches the `ANIMATION_STOP` events. * * If no animation is playing, no event will be dispatched. * * If there is another animation queued (via the `chain` method) then it will start playing immediately. * * @method Phaser.GameObjects.Plane#stop * @fires Phaser.Animations.Events#ANIMATION_STOP * @since 3.60.0 * * @return {this} This Game Object. */ stop: function () { return this.anims.stop(); }, /** * Stops the current animation from playing after the specified time delay, given in milliseconds. * * It then dispatches the `ANIMATION_STOP` event. * * If no animation is running, no events will be dispatched. * * If there is another animation in the queue (set via the `chain` method) then it will start playing, * when the current one stops. * * @method Phaser.GameObjects.Plane#stopAfterDelay * @fires Phaser.Animations.Events#ANIMATION_STOP * @since 3.60.0 * * @param {number} delay - The number of milliseconds to wait before stopping this animation. * * @return {this} This Game Object. */ stopAfterDelay: function (delay) { return this.anims.stopAfterDelay(delay); }, /** * Stops the current animation from playing after the given number of repeats. * * It then dispatches the `ANIMATION_STOP` event. * * If no animation is running, no events will be dispatched. * * If there is another animation in the queue (set via the `chain` method) then it will start playing, * when the current one stops. * * @method Phaser.GameObjects.Plane#stopAfterRepeat * @fires Phaser.Animations.Events#ANIMATION_STOP * @since 3.60.0 * * @param {number} [repeatCount=1] - How many times should the animation repeat before stopping? * * @return {this} This Game Object. */ stopAfterRepeat: function (repeatCount) { return this.anims.stopAfterRepeat(repeatCount); }, /** * Stops the current animation from playing when it next sets the given frame. * If this frame doesn't exist within the animation it will not stop it from playing. * * It then dispatches the `ANIMATION_STOP` event. * * If no animation is running, no events will be dispatched. * * If there is another animation in the queue (set via the `chain` method) then it will start playing, * when the current one stops. * * @method Phaser.GameObjects.Plane#stopOnFrame * @fires Phaser.Animations.Events#ANIMATION_STOP * @since 3.60.0 * * @param {Phaser.Animations.AnimationFrame} frame - The frame to check before stopping this animation. * * @return {this} This Game Object. */ stopOnFrame: function (frame) { return this.anims.stopOnFrame(frame); }, /** * Runs the preUpdate for this Plane, which will check its Animation State, * if one is playing, and refresh view / model matrices, if updated. * * @method Phaser.GameObjects.Plane#preUpdate * @protected * @since 3.60.0 * * @param {number} time - The current timestamp. * @param {number} delta - The delta time, in ms, elapsed since the last frame. */ preUpdate: function (time, delta) { Mesh.prototype.preUpdate.call(this, time, delta); this.anims.update(time, delta); }, /** * Handles the pre-destroy step for the Plane, which removes the vertices and debug callbacks. * * @method Phaser.GameObjects.Plane#preDestroy * @private * @since 3.60.0 */ preDestroy: function () { this.clear(); this.removeCheckerboard(); this.anims.destroy(); this.anims = undefined; this.debugCallback = null; this.debugGraphic = null; } }); module.exports = Plane;