├── .babelrc ├── .eslintignore ├── docs ├── .babelrc ├── dist │ ├── assets │ │ ├── cube.png │ │ ├── tile.png │ │ └── main.css │ ├── projection.bundle.js │ ├── interaction.bundle.js │ └── collision.bundle.js ├── pages │ ├── collision_example.html │ ├── interaction_example.html │ └── projection_example.html ├── index.html ├── .eslintrc.js ├── package.json ├── webpack.config.js └── src │ ├── IsoInteractionExample.js │ ├── IsoProjectionExample.js │ └── IsoCollisionExample.js ├── webpack.config.js ├── .gitignore ├── .eslintrc.js ├── package.json ├── LICENSE.md ├── README.md └── src ├── Projector.js ├── IsoPlugin.js ├── Point3.js ├── IsoSprite.js ├── Octree.js ├── physics ├── IsoPhysics.js ├── Body.js └── World.js └── Cube.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/dist/** 3 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/dist/assets/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebashwa/phaser3-plugin-isometric/HEAD/docs/dist/assets/cube.png -------------------------------------------------------------------------------- /docs/dist/assets/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebashwa/phaser3-plugin-isometric/HEAD/docs/dist/assets/tile.png -------------------------------------------------------------------------------- /docs/dist/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | display: flex; 5 | font-family: "Lucida Console", Monaco, monospace; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | a, a:active { 12 | color: white; 13 | text-decoration: none; 14 | margin-bottom: 1em; 15 | } 16 | -------------------------------------------------------------------------------- /docs/pages/collision_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Phaser 3 Iso Collision Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/pages/interaction_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Phaser 3 Iso Interaction Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/pages/projection_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Phaser 3 Iso Projection Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: [ 4 | './src/IsoPlugin.js' 5 | ], 6 | module: { 7 | rules: [ 8 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 9 | ] 10 | }, 11 | output: { 12 | path: __dirname + '/dist', 13 | publicPath: '/', 14 | filename: 'phaser-plugin-isometric.js', 15 | library: 'phaser-plugin-isometric', 16 | libraryTarget: 'commonjs2' 17 | } 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Phaser 3 Isometric Plugin Examples 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Phaser 3 Plugin Isometric

12 | Projection Example 13 | Collision Example 14 | Interaction Example 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | node_modules/ 40 | docs/node_modules/ 41 | 42 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser3-plugin-isometric-docs", 3 | "version": "1.0.0", 4 | "description": "The docs page for phaser3-plugin-isometric", 5 | "main": "index.js", 6 | "repository": "https://sebashwa.github.io/phaser3-plugin-isometric", 7 | "author": "sebashwa", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "babel-core": "^6.26.0", 11 | "babel-loader": "^7.1.4", 12 | "babel-preset-env": "^1.6.1", 13 | "eslint": "^4.19.1", 14 | "raw-loader": "^0.5.1", 15 | "webpack": "^4.5.0", 16 | "webpack-cli": "^2.0.14", 17 | "webpack-dev-server": "^3.1.1" 18 | }, 19 | "dependencies": { 20 | "phaser": "^3.x", 21 | "phaser3-plugin-isometric": "^0.0.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "globals": { 13 | "Phaser": false 14 | }, 15 | "rules": { 16 | "indent": [ 17 | "error", 18 | 2 19 | ], 20 | "linebreak-style": [ 21 | "error", 22 | "unix" 23 | ], 24 | "quotes": [ 25 | "error", 26 | "single" 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser3-plugin-isometric", 3 | "description": "Feature-packed axonometric plugin for Phaser 3 which stays true to the Phaser API.", 4 | "contributors": { 5 | "email": "sebashwa@mailbox.org" 6 | }, 7 | "keywords": [ 8 | "phaser", 9 | "phaser3", 10 | "phaser 3", 11 | "isometric", 12 | "plugin", 13 | "axonometric" 14 | ], 15 | "license": "MIT", 16 | "version": "0.0.7", 17 | "devDependencies": { 18 | "babel-core": "^6.26.0", 19 | "babel-loader": "^7.1.4", 20 | "babel-preset-env": "^1.6.1", 21 | "eslint": "^4.19.1", 22 | "webpack": "^4.4.1", 23 | "webpack-cli": "^3.1.2" 24 | }, 25 | "peerDependencies": { 26 | "phaser": "3.x" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/sebashwa/phaser-plugin-isometric.git" 31 | }, 32 | "main": "dist/phaser-plugin-isometric.js" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lewis Lane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | projection: './src/IsoProjectionExample.js', 7 | collision: './src/IsoCollisionExample.js', 8 | interaction: './src/IsoInteractionExample.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 13 | { test: [/\.vert$/, /\.frag$/], use: 'raw-loader' } 14 | ] 15 | }, 16 | output: { 17 | path: __dirname + '/dist', 18 | publicPath: '/phaser3-plugin-isometric/dist', 19 | filename: '[name].bundle.js' 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | WEBGL_RENDERER: true, 24 | CANVAS_RENDERER: true 25 | }) 26 | ], 27 | devServer: { 28 | contentBase: __dirname, 29 | publicPath: '/dist' 30 | }, 31 | optimization: { 32 | splitChunks: { 33 | cacheGroups: { 34 | commons: { 35 | name: 'commons', 36 | chunks: 'initial', 37 | minChunks: 2, 38 | minSize: 0 39 | } 40 | } 41 | }, 42 | occurrenceOrder: true 43 | } 44 | }; 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a **WIP** fork of [lewster32/phaser-plugin-isometric](https://github.com/lewster32/phaser-plugin-isometric) to make it work with Phaser 3. 2 | 3 | ## Description 4 | Phaser Isometric is a comprehensive axonometric plugin for Phaser which provides an API for handling axonometric projection of assets in 3D space to the screen. 5 | The goal has been to mimic as closely as possible the existing APIs provided by Phaser for standard orthogonal 2D projection, but add a third dimension. 6 | Also included is an Arcade-based 3D AABB physics engine, which again is closely equivalent in functionality and its API. 7 | 8 | ## Features 9 | 10 | * Familiar Phaser API - if you've grasped the basics of Phaser, you can use this! 11 | * 3D geometry helpers in the form of Point3 and Cube 12 | * Adjustable axonometric projection angle to allow for classic 2:1 pixel dimetric, true 120° isometric or any angle you like via ```scene.isometric.projectionAngle``` 13 | * Simple x+y (with z fudging) 14 | * Arcade Physics derived 3D physics engine (**Working, but needs refactoring!**) 15 | * Helpful debug utilities (**Not working yet!**) 16 | * Familiar factory methods added to GameObjectFactory and GameObjectCreator so you can do ```scene.add.isoSprite``` 17 | 18 | ## Examples 19 | Check out the [docs](https://github.com/sebashwa/phaser3-plugin-isometric/tree/master/docs) folder and the [github-page](https://sebashwa.github.io/phaser3-plugin-isometric/) 20 | -------------------------------------------------------------------------------- /docs/src/IsoInteractionExample.js: -------------------------------------------------------------------------------- 1 | import Phaser, { Game, Scene } from 'phaser'; 2 | import IsoPlugin from 'phaser3-plugin-isometric'; 3 | 4 | class IsoInteractionExample extends Scene { 5 | constructor() { 6 | const sceneConfig = { 7 | key: 'IsoInteractionExample', 8 | mapAdd: { isoPlugin: 'iso' } 9 | }; 10 | 11 | super(sceneConfig); 12 | } 13 | 14 | preload() { 15 | this.load.image('tile', '../dist/assets/tile.png'); 16 | this.load.scenePlugin({ 17 | key: 'IsoPlugin', 18 | url: IsoPlugin, 19 | sceneKey: 'iso' 20 | }); 21 | } 22 | 23 | create() { 24 | this.isoGroup = this.add.group(); 25 | 26 | this.iso.projector.origin.setTo(0.5, 0.3); 27 | 28 | // Add some tiles to our scene 29 | this.spawnTiles(); 30 | } 31 | 32 | spawnTiles() { 33 | var tile; 34 | 35 | for (var xx = 0; xx < 256; xx += 38) { 36 | for (var yy = 0; yy < 256; yy += 38) { 37 | tile = this.add.isoSprite(xx, yy, 0, 'tile', this.isoGroup); 38 | tile.setInteractive(); 39 | 40 | tile.on('pointerover', function() { 41 | this.setTint(0x86bfda); 42 | this.isoZ += 5; 43 | }); 44 | 45 | tile.on('pointerout', function() { 46 | this.clearTint(); 47 | this.isoZ -= 5; 48 | }); 49 | } 50 | } 51 | } 52 | } 53 | 54 | let config = { 55 | type: Phaser.AUTO, 56 | width: 800, 57 | height: 600, 58 | pixelArt: true, 59 | scene: IsoInteractionExample 60 | }; 61 | 62 | new Game(config); 63 | -------------------------------------------------------------------------------- /docs/src/IsoProjectionExample.js: -------------------------------------------------------------------------------- 1 | import Phaser, { Scene, Game } from 'phaser'; 2 | import IsoPlugin from 'phaser3-plugin-isometric'; 3 | 4 | class IsoProjectionExample extends Scene { 5 | constructor() { 6 | super({ 7 | key: 'IsoProjectionExample', 8 | mapAdd: { isoPlugin: 'iso' } 9 | }); 10 | } 11 | 12 | preload() { 13 | this.load.image('cube', '../dist/assets/cube.png'); 14 | this.load.scenePlugin({ 15 | key: 'IsoPlugin', 16 | url: IsoPlugin, 17 | sceneKey: 'iso' 18 | }); 19 | } 20 | 21 | create() { 22 | // Set the origin of the isometric projection to the mid top of the screen 23 | this.iso.projector.origin.setTo(0.5, 0.1); 24 | 25 | // Even though the children are added back to front, it is sorted the right way 26 | // because depth value is set on the IsoSprites and Phaser 3 sorts after that by default. 27 | for (var xx = 256; xx > 0; xx -= 48) { 28 | for (var yy = 256; yy > 0; yy -= 48) { 29 | // Create a cube using the new isoSprite factory method at the specified position. 30 | const isoCube = this.add.isoSprite(xx, yy, 0, 'cube'); 31 | 32 | // Add a tween so we can see the depth sorting works on updates 33 | this.tweens.add({ 34 | targets: isoCube, 35 | isoX: 256 - xx + 32, 36 | duration: 2000, 37 | ease: 'Quad.easeInOut', 38 | delay: 0, 39 | yoyo: true, 40 | repeat: Infinity 41 | }); 42 | } 43 | } 44 | } 45 | 46 | update() { 47 | // We can interact with the cubes 48 | // Pick a pseudo-random cube and let it sink on click / touch 49 | if (this.input.activePointer.justDown) { 50 | const cube = this.children.list[Math.trunc(Math.random() * 35)]; 51 | cube.isoZ -= 48; 52 | } 53 | } 54 | } 55 | 56 | let config = { 57 | type: Phaser.AUTO, 58 | width: 600, 59 | height: 600, 60 | pixelArt: true, 61 | scene: IsoProjectionExample 62 | }; 63 | 64 | new Game(config); 65 | -------------------------------------------------------------------------------- /docs/src/IsoCollisionExample.js: -------------------------------------------------------------------------------- 1 | import Phaser, { Game, Scene } from 'phaser'; 2 | import IsoPlugin, { IsoPhysics } from 'phaser3-plugin-isometric'; 3 | 4 | class IsoCollisionExample extends Scene { 5 | constructor() { 6 | const sceneConfig = { 7 | key: 'IsoCollisionExample', 8 | mapAdd: { isoPlugin: 'iso', isoPhysics: 'isoPhysics' } 9 | }; 10 | 11 | super(sceneConfig); 12 | } 13 | 14 | preload() { 15 | this.load.image('cube', '../dist/assets/cube.png'); 16 | this.load.scenePlugin({ 17 | key: 'IsoPlugin', 18 | url: IsoPlugin, 19 | sceneKey: 'iso' 20 | }); 21 | 22 | this.load.scenePlugin({ 23 | key: 'IsoPhysics', 24 | url: IsoPhysics, 25 | sceneKey: 'isoPhysics' 26 | }); 27 | } 28 | 29 | create() { 30 | this.isoGroup = this.add.group(); 31 | 32 | // Apply some gravity on our cubes 33 | this.isoPhysics.world.gravity.setTo(0, 0, -500); 34 | 35 | this.isoPhysics.projector.origin.setTo(0.5, 0.3); 36 | 37 | // Add some first cubes to our scene 38 | this.spawnCubes(); 39 | } 40 | 41 | update() { 42 | // Collide cubes against each other 43 | this.isoPhysics.world.collide(this.isoGroup); 44 | 45 | // Moooore cuuuubes 46 | if (this.input.activePointer.justDown) { 47 | this.spawnCubes(); 48 | } 49 | } 50 | 51 | spawnCubes() { 52 | let cube; 53 | for (let xx = 256; xx > 0; xx -= 64) { 54 | for (let yy = 256; yy > 0; yy -= 64) { 55 | // Add a cube which is way above the ground 56 | cube = this.add.isoSprite(xx, yy, 600, 'cube', this.isoGroup); 57 | 58 | // Enable the physics body on this cube 59 | this.isoPhysics.world.enable(cube); 60 | 61 | // Collide with the world bounds so it doesn't go falling forever or fly off the screen! 62 | cube.body.collideWorldBounds = true; 63 | 64 | // Add a full bounce on the x and y axes, and a bit on the z axis. 65 | cube.body.bounce.set(1, 1, 0.2); 66 | 67 | // Send the cubes off in random x and y directions! Wheee! 68 | const randomX = Math.trunc((Math.random() * 100 - 50)); 69 | const randomY = Math.trunc((Math.random() * 100 - 50)); 70 | cube.body.velocity.setTo(randomX, randomY, 0); 71 | } 72 | } 73 | } 74 | } 75 | 76 | let config = { 77 | type: Phaser.AUTO, 78 | width: 800, 79 | height: 600, 80 | pixelArt: true, 81 | scene: IsoCollisionExample 82 | }; 83 | 84 | new Game(config); 85 | -------------------------------------------------------------------------------- /docs/dist/projection.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var r,u,a=t[0],c=t[1],l=t[2],f=0,p=[];f0;e-=48)for(var t=256;t>0;t-=48){var n=this.add.isoSprite(e,t,0,"cube");this.tweens.add({targets:n,isoX:256-e+32,duration:2e3,ease:"Quad.easeInOut",delay:0,yoyo:!0,repeat:1/0})}}},{key:"update",value:function(){this.input.activePointer.justDown&&(this.children.list[Math.trunc(35*Math.random())].isoZ-=48)}}]),t}(),l={type:i.default.AUTO,width:600,height:600,pixelArt:!0,scene:c};new o.Game(l)}}); -------------------------------------------------------------------------------- /docs/dist/interaction.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var r,u,a=t[0],s=t[1],l=t[2],f=0,p=[];f0;t-=64)for(var o=256;o>0;o-=64){e=this.add.isoSprite(t,o,600,"cube",this.isoGroup),this.isoPhysics.world.enable(e),e.body.collideWorldBounds=!0,e.body.bounce.set(1,1,.2);var n=Math.trunc(100*Math.random()-50),r=Math.trunc(100*Math.random()-50);e.body.velocity.setTo(n,r,0)}}}]),t}(),l={type:i.default.AUTO,width:800,height:600,pixelArt:!0,scene:c};new r.Game(l)}}); -------------------------------------------------------------------------------- /src/Projector.js: -------------------------------------------------------------------------------- 1 | import Point3 from './Point3'; 2 | const Point = Phaser.Geom.Point; 3 | 4 | // Projection angles 5 | export const CLASSIC = Math.atan(0.5); 6 | export const ISOMETRIC = Math.PI / 6; 7 | export const MILITARY = Math.PI / 4; 8 | 9 | /** 10 | * @class Projector 11 | * 12 | * @classdesc 13 | * Creates a new Isometric Projector object, which has helpers for projecting x, y and z coordinates into axonometric x and y equivalents. 14 | */ 15 | class Projector { 16 | /** 17 | * @constructor 18 | * @param {Phaser.Game} game - The current game object. 19 | * @param {number} projectionAngle - The angle of the axonometric projection in radians. Defaults to approx. 0.4636476 (Math.atan(0.5) which is suitable for 2:1 pixel art dimetric) 20 | * @return {Cube} This Cube object. 21 | */ 22 | constructor(scene, projectionAngle) { 23 | /** 24 | * @property {Phaser.Scene} scne - The current scene object. 25 | */ 26 | this.scene = scene; 27 | 28 | /** 29 | * @property {array} _transform - The pre-calculated axonometric transformation values. 30 | * @private 31 | */ 32 | this._transform = null; 33 | 34 | /** 35 | * @property {number} _projectionAngle - The cached angle of projection in radians. 36 | * @private 37 | */ 38 | this._projectionAngle = 0; 39 | 40 | /** 41 | * @property {number} projectionAngle - The angle of projection in radians. 42 | * @default 43 | */ 44 | this.projectionAngle = projectionAngle || CLASSIC; 45 | 46 | /** 47 | * @property {Phaser.Geom.Point} origin - The x and y offset multipliers as a ratio of the game world size. 48 | * @default 49 | */ 50 | this.origin = new Point(0.5, 0.5); 51 | } 52 | 53 | /** 54 | * @name Projector#projectionAngle 55 | * @property {number} projectionAngle - The angle of axonometric projection. 56 | */ 57 | set projectionAngle(angle) { 58 | if (angle === this._projectionAngle) { return; } 59 | 60 | this._projectionAngle = angle; 61 | this._transform = [Math.cos(this._projectionAngle), Math.sin(this._projectionAngle)]; 62 | } 63 | 64 | get projectionAngle() { 65 | return this._projectionAngle; 66 | } 67 | 68 | /** 69 | * Use axonometric projection to transform a 3D Point3 coordinate to a 2D Point coordinate. If given the coordinates will be set into the object, otherwise a brand new Point object will be created and returned. 70 | * @method Projector#project 71 | * @param {Point3} point3 - The Point3 to project from. 72 | * @param {Phaser.Geom.Point} out - The Point to project to. 73 | * @return {Phaser.Geom.Point} The transformed Point. 74 | */ 75 | project(point3, out = new Point()) { 76 | out.x = (point3.x - point3.y) * this._transform[0]; 77 | out.y = ((point3.x + point3.y) * this._transform[1]) - point3.z; 78 | 79 | const { width, height } = this.scene.sys.game.config; 80 | out.x += width * this.origin.x; 81 | out.y += height * this.origin.y; 82 | 83 | return out; 84 | } 85 | 86 | /** 87 | * Use axonometric projection to transform a 3D Point3 coordinate to a 2D Point coordinate, ignoring the z-axis. If given the coordinates will be set into the object, otherwise a brand new Point object will be created and returned. 88 | * @method Projector#projectXY 89 | * @param {Point3} point3 - The Point3 to project from. 90 | * @param {Phaser.Geom.Point} out - The Point to project to. 91 | * @return {Phaser.Geom.Point} The transformed Point. 92 | */ 93 | projectXY(point3, out = new Point()) { 94 | out.x = (point3.x - point3.y) * this._transform[0]; 95 | out.y = (point3.x + point3.y) * this._transform[1]; 96 | 97 | out.x += this.game.world.width * this.origin.x; 98 | out.y += this.game.world.height * this.origin.y; 99 | 100 | return out; 101 | } 102 | 103 | /** 104 | * Use reverse axonometric projection to transform a 2D Point coordinate to a 3D Point3 coordinate. If given the coordinates will be set into the object, otherwise a brand new Point3 object will be created and returned. 105 | * @method Projector#unproject 106 | * @param {Phaser.Geom.Point} point - The Point to project from. 107 | * @param {Point3} out - The Point3 to project to. 108 | * @param {number} [z] - Specified z-plane to project to. 109 | * @return {Point3} The transformed Point3. 110 | */ 111 | unproject(point, out = new Point3(), z = 0) { 112 | const x = point.x - this.game.world.x - (this.game.world.width * this.origin.x); 113 | const y = point.y - this.game.world.y - (this.game.world.height * this.origin.y) + z; 114 | 115 | out.x = x / (2 * this._transform[0]) + y / (2 * this._transform[1]); 116 | out.y = -(x / (2 * this._transform[0])) + y / (2 * this._transform[1]); 117 | out.z = z; 118 | 119 | return out; 120 | } 121 | } 122 | 123 | export default Projector; 124 | -------------------------------------------------------------------------------- /src/IsoPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | 4 | * Copyright (c) 2015 Lewis Lane 5 | 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | /** 26 | * @author Lewis Lane 27 | * @copyright 2015 Lewis Lane (Rotates.org) 28 | * @license {@link http://opensource.org/licenses/MIT|MIT License} 29 | */ 30 | 31 | import Projector from './Projector'; 32 | import IsoSprite from './IsoSprite'; 33 | export { default as IsoPhysics } from './physics/IsoPhysics'; 34 | 35 | // Type consts 36 | 37 | /** 38 | * @class IsometricPlugin 39 | * 40 | * @classdesc 41 | * Isometric is a comprehensive axonometric plugin for Phaser which provides an API for handling axonometric projection of assets in 3D space to the screen. 42 | * The goal has been to mimic as closely as possible the existing APIs provided by Phaser for standard orthogonal 2D projection, but add a third dimension. 43 | * Also included is an Arcade-based 3D AABB physics engine, which again is closely equivalent in functionality and its API. 44 | */ 45 | export default class IsoPlugin { 46 | /** 47 | * @constructor 48 | * @param {Phaser.Scene} scene The current scene instance 49 | */ 50 | constructor(scene) { 51 | this.scene = scene; 52 | this.systems = scene.sys; 53 | 54 | if (!scene.sys.settings.isBooted) { 55 | scene.sys.events.once('boot', this.boot, this); 56 | } 57 | 58 | this.projector = new Projector(scene, scene.isometricType); 59 | 60 | /** 61 | * Create a new IsoSprite with specific position and sprite sheet key. 62 | * 63 | * @method Phaser.GameObjectFactory#isoSprite 64 | * @param {number} x - X position of the new IsoSprite. 65 | * @param {number} y - Y position of the new IsoSprite. 66 | * @param {number} y - Z position of the new IsoSprite. 67 | * @param {string|Phaser.RenderTexture|PIXI.Texture} key - This is the image or texture used by the Sprite during rendering. It can be a string which is a reference to the Cache entry, or an instance of a RenderTexture or PIXI.Texture. 68 | * @param {string|number} [frame] - If the sprite uses an image from a texture atlas or sprite sheet you can pass the frame here. Either a number for a frame ID or a string for a frame name. 69 | * @returns {IsoSprite} the newly created IsoSprite object. 70 | */ 71 | 72 | Phaser.GameObjects.GameObjectCreator.register('isoSprite', function (x, y, z, key, frame) { 73 | return new IsoSprite(this.scene, x, y, z, key, frame); 74 | }); 75 | 76 | /** 77 | * Create a new IsoSprite with specific position and sprite sheet key. 78 | * 79 | * @method Phaser.GameObjectFactory#isoSprite 80 | * @param {number} x - X position of the new IsoSprite. 81 | * @param {number} y - Y position of the new IsoSprite. 82 | * @param {number} y - Z position of the new IsoSprite. 83 | * @param {string|Phaser.RenderTexture|PIXI.Texture} key - This is the image or texture used by the Sprite during rendering. It can be a string which is a reference to the Cache entry, or an instance of a RenderTexture or PIXI.Texture. 84 | * @param {string|number} [frame] - If the sprite uses an image from a texture atlas or sprite sheet you can pass the frame here. Either a number for a frame ID or a string for a frame name. 85 | * @param {Phaser.Group} [group] - Optional Group to add the object to. If not specified it will be added to the World group. 86 | * @returns {IsoSprite} the newly created IsoSprite object. 87 | */ 88 | Phaser.GameObjects.GameObjectFactory.register('isoSprite', function (x, y, z, key, group, frame = 0) { 89 | const sprite = new IsoSprite(this.scene, x, y, z, key, frame); 90 | 91 | if (typeof group === 'undefined') { 92 | this.displayList.add(sprite); 93 | this.updateList.add(sprite); 94 | } else { 95 | group.add(sprite, true); 96 | } 97 | 98 | return sprite; 99 | }); 100 | } 101 | 102 | boot() { 103 | } 104 | 105 | static register(PluginManager) { 106 | PluginManager.register('IsoPlugin', IsoPlugin, 'isoPlugin'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Point3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Point3 3 | * 4 | * @classdesc 5 | * The Point3 object represents a location in a three-dimensional coordinate system, 6 | * where x and y represent the horizontal axes and z represents the vertical axis. 7 | * The following code creates a point at (0,0,0): 8 | * `var myPoint = new Point3();` 9 | * 10 | * Creates a new Point3 object. If you pass no parameters a Point3 is created set to (0, 0, 0). 11 | */ 12 | class Point3 { 13 | /** 14 | * @constructor 15 | * @param {number} [x=0] - The horizontal X position of this Point. 16 | * @param {number} [y=0] - The horizontal Y position of this Point. 17 | * @param {number} [z=0] - The vertical position of this Point. 18 | */ 19 | constructor(x = 0, y = 0, z = 0) { 20 | /** 21 | * @property {number} x - The x value of the point. 22 | */ 23 | this.x = x; 24 | 25 | /** 26 | * @property {number} y - The y value of the point. 27 | */ 28 | this.y = y; 29 | 30 | /** 31 | * @property {number} z - The z value of the point. 32 | */ 33 | this.z = z; 34 | } 35 | 36 | /** 37 | * Copies the x, y and z properties from any given object to this Point3. 38 | * 39 | * @method Point3#copyFrom 40 | * @param {any} source - The object to copy from. 41 | * @return {Point3} This Point3 object. 42 | */ 43 | copyFrom(source) { 44 | return this.setTo(source.x, source.y, source.z); 45 | } 46 | 47 | /** 48 | * Copies the x, y and z properties from this Point3 to any given object. 49 | * 50 | * @method Point3#copyTo 51 | * @param {any} dest - The object to copy to. 52 | * @return {Object} The dest object. 53 | */ 54 | copyTo(dest) { 55 | dest.x = this.x; 56 | dest.y = this.y; 57 | dest.z = this.z; 58 | 59 | return dest; 60 | } 61 | 62 | /** 63 | * Determines whether the given object's x/y/z values are equal to this Point3 object. 64 | * 65 | * @method Point3#equals 66 | * @param {Point3|any} a - The object to compare with this Point3. 67 | * @return {boolean} A value of true if the x and y points are equal, otherwise false. 68 | */ 69 | equals(a) { 70 | return (a.x === this.x && a.y === this.y && a.z === this.z); 71 | } 72 | 73 | /** 74 | * Sets the x, y and z values of this Point3 object to the given values. 75 | * If you omit the y and z value then the x value will be applied to all three, for example: 76 | * `Point3.set(2)` is the same as `Point3.set(2, 2, 2)` 77 | * If however you set both x and y, but no z, the z value will be set to 0. 78 | * 79 | * @method Point3#set 80 | * @param {number} x - The x value of this point. 81 | * @param {number} [y] - The y value of this point. If not given the x value will be used in its place. 82 | * @param {number} [z] - The z value of this point. If not given and the y value is also not given, the x value will be used in its place. 83 | * @return {Point3} This Point3 object. Useful for chaining method calls. 84 | */ 85 | set(x, y, z) { 86 | this.x = x || 0; 87 | this.y = y || ((y !== 0) ? this.x : 0); 88 | this.z = z || ((typeof y === 'undefined') ? this.x : 0); 89 | } 90 | 91 | /** 92 | * Sets the x, y and z values of this Point3 object to the given values. 93 | * If you omit the y and z value then the x value will be applied to all three, for example: 94 | * `Point3.setTo(2)` is the same as `Point3.setTo(2, 2, 2)` 95 | * If however you set both x and y, but no z, the z value will be set to 0. 96 | * 97 | * @method Point3#setTo 98 | * @param {number} x - The x value of this point. 99 | * @param {number} [y] - The y value of this point. If not given the x value will be used in its place. 100 | * @param {number} [z] - The z value of this point. If not given and the y value is also not given, the x value will be used in its place. 101 | * @return {Point3} This Point3 object. Useful for chaining method calls. 102 | */ 103 | setTo(x, y, z) { 104 | return this.set(x, y, z); 105 | } 106 | 107 | /** 108 | * Adds the given x, y and z values to this Point3. 109 | * 110 | * @method Point3#add 111 | * @param {number} x - The value to add to Point3.x. 112 | * @param {number} y - The value to add to Point3.y. 113 | * @param {number} z - The value to add to Point3.z. 114 | * @return {Point3} This Point3 object. Useful for chaining method calls. 115 | */ 116 | add(x, y, z) { 117 | this.x += x || 0; 118 | this.y += y || 0; 119 | this.z += z || 0; 120 | 121 | return this; 122 | } 123 | 124 | /** 125 | * Subtracts the given x, y and z values from this Point3. 126 | * 127 | * @method Point3#subtract 128 | * @param {number} x - The value to subtract from Point3.x. 129 | * @param {number} y - The value to subtract from Point3.y. 130 | * @param {number} z - The value to subtract from Point3.z. 131 | * @return {Point3} This Point3 object. Useful for chaining method calls. 132 | */ 133 | subtract(x, y, z) { 134 | this.x -= x || 0; 135 | this.y -= y || 0; 136 | this.z -= z || 0; 137 | 138 | return this; 139 | } 140 | 141 | /** 142 | * Multiplies Point3.x, Point3.y and Point3.z by the given x and y values. Sometimes known as `Scale`. 143 | * 144 | * @method Point3#multiply 145 | * @param {number} x - The value to multiply Point3.x by. 146 | * @param {number} y - The value to multiply Point3.y by. 147 | * @param {number} z - The value to multiply Point3.z by. 148 | * @return {Point3} This Point3 object. Useful for chaining method calls. 149 | */ 150 | multiply(x, y, z) { 151 | this.x *= x || 1; 152 | this.y *= y || 1; 153 | this.z *= z || 1; 154 | 155 | return this; 156 | } 157 | 158 | /** 159 | * Divides Point3.x, Point3.y and Point3.z by the given x, y and z values. 160 | * 161 | * @method Point3#divide 162 | * @param {number} x - The value to divide Point3.x by. 163 | * @param {number} y - The value to divide Point3.y by. 164 | * @param {number} z - The value to divide Point3.z by. 165 | * @return {Point3} This Point3 object. Useful for chaining method calls. 166 | */ 167 | divide(x, y, z) { 168 | this.x /= x || 1; 169 | this.y /= y || 1; 170 | this.z /= z || 1; 171 | 172 | return this; 173 | } 174 | 175 | /** 176 | * Adds the coordinates of two points together to create a new point. 177 | * 178 | * @method Point3.add 179 | * @param {Point3} a - The first Point3 object. 180 | * @param {Point3} b - The second Point3 object. 181 | * @param {Point3} [out] - Optional Point3 to store the value in, if not supplied a new Point3 object will be created. 182 | * @return {Point3} The new Point3 object. 183 | */ 184 | static add(a, b, out = new Point3()) { 185 | out.x = a.x + b.x; 186 | out.y = a.y + b.y; 187 | out.z = a.z + b.z; 188 | 189 | return out; 190 | } 191 | 192 | /** 193 | * Subtracts the coordinates of two points to create a new point. 194 | * 195 | * @method Point3.subtract 196 | * @param {Point3} a - The first Point3 object. 197 | * @param {Point3} b - The second Point3 object. 198 | * @param {Point3} [out] - Optional Point3 to store the value in, if not supplied a new Point3 object will be created. 199 | * @return {Point3} The new Point3 object. 200 | */ 201 | static subtract(a, b, out = new Point3()) { 202 | out.x = a.x - b.x; 203 | out.y = a.y - b.y; 204 | out.z = a.z - b.z; 205 | 206 | return out; 207 | } 208 | 209 | /** 210 | * Multiplies the coordinates of two points to create a new point. 211 | * 212 | * @method Point3.multiply 213 | * @param {Point3} a - The first Point3 object. 214 | * @param {Point3} b - The second Point3 object. 215 | * @param {Point3} [out] - Optional Point3 to store the value in, if not supplied a new Point3 object will be created. 216 | * @return {Point3} The new Point3 object. 217 | */ 218 | static multiply(a, b, out = new Point3()) { 219 | out.x = a.x * b.x; 220 | out.y = a.y * b.y; 221 | out.z = a.z * b.z; 222 | 223 | return out; 224 | } 225 | 226 | /** 227 | * Divides the coordinates of two points to create a new point. 228 | * 229 | * @method Point3.divide 230 | * @param {Point3} a - The first Point3 object. 231 | * @param {Point3} b - The second Point3 object. 232 | * @param {Point3} [out] - Optional Point3 to store the value in, if not supplied a new Point3 object3 will be created. 233 | * @return {Point3} The new Point3 object. 234 | */ 235 | static divide (a, b, out = new Point3()) { 236 | out.x = a.x / b.x; 237 | out.y = a.y / b.y; 238 | out.z = a.z / b.z; 239 | 240 | return out; 241 | } 242 | 243 | /** 244 | * Determines whether the two given Point3 objects are equal. They are considered equal if they have the same x, y and z values. 245 | * 246 | * @method Point3.equals 247 | * @param {Point3} a - The first Point3 object. 248 | * @param {Point3} b - The second Point3 object. 249 | * @return {boolean} A value of true if the Points3 are equal, otherwise false. 250 | */ 251 | static equals(a, b) { 252 | return (a.x === b.x && a.y === b.y && a.z === b.z); 253 | } 254 | } 255 | 256 | export default Point3; 257 | -------------------------------------------------------------------------------- /src/IsoSprite.js: -------------------------------------------------------------------------------- 1 | import Point3 from './Point3'; 2 | import Cube from './Cube'; 3 | 4 | export const ISOSPRITE = 'IsoSprite'; 5 | const Sprite = Phaser.GameObjects.Sprite; 6 | 7 | /** 8 | * @class IsoSprite 9 | * 10 | * @classdesc 11 | * Create a new `IsoSprite` object. IsoSprites are extended versions of standard Sprites that are suitable for axonometric positioning. 12 | * 13 | * IsoSprites are simply Sprites that have three new position properties (isoX, isoY and isoZ) and ask the instance of Projector what their position should be in a 2D scene whenever these properties are changed. 14 | * The IsoSprites retain their 2D position property to prevent any problems and allow you to interact with them as you would a normal Sprite. The upside of this simplicity is that things should behave predictably for those already used to Phaser. 15 | */ 16 | export default class IsoSprite extends Sprite { 17 | /** 18 | * @constructor 19 | * @extends Phaser.GameObjects.Sprite 20 | * @param {Phaser.Scene} scene - A reference to the current scene. 21 | * @param {number} x - The x coordinate (in 3D space) to position the IsoSprite at. 22 | * @param {number} y - The y coordinate (in 3D space) to position the IsoSprite at. 23 | * @param {number} z - The z coordinate (in 3D space) to position the IsoSprite at. 24 | * @param {string|Phaser.RenderTexture|Phaser.BitmapData} key - This is the image or texture used by the IsoSprite during rendering. It can be a string which is a reference to the Cache entry, or an instance of a RenderTexture or PIXI.Texture. 25 | * @param {string|number} frame - If this IsoSprite is using part of a sprite sheet or texture atlas you can specify the exact frame to use by giving a string or numeric index. 26 | */ 27 | constructor(scene, x, y, z, texture, frame) { 28 | super(scene, x, y, texture, frame); 29 | 30 | /** 31 | * @property {number} type - The const type of this object. 32 | * @readonly 33 | */ 34 | this.type = ISOSPRITE; 35 | 36 | /** 37 | * @property {Point3} _isoPosition - Internal 3D position. 38 | * @private 39 | */ 40 | this._isoPosition = new Point3(x, y, z); 41 | 42 | /** 43 | * @property {number} snap - Snap this IsoSprite's position to the specified value; handy for keeping pixel art snapped to whole pixels. 44 | * @default 45 | */ 46 | this.snap = 0; 47 | 48 | /** 49 | * @property {boolean} _isoPositionChanged - Internal invalidation control for positioning. 50 | * @readonly 51 | * @private 52 | */ 53 | this._isoPositionChanged = true; 54 | 55 | /** 56 | * @property {boolean} _isoBoundsChanged - Internal invalidation control for isometric bounds. 57 | * @readonly 58 | * @private 59 | */ 60 | this._isoBoundsChanged = true; 61 | 62 | this._project(); 63 | 64 | /** 65 | * @property {Cube} _isoBounds - Internal derived 3D bounds. 66 | * @private 67 | */ 68 | this._isoBounds = this.resetIsoBounds(); 69 | } 70 | 71 | /** 72 | * The axonometric position of the IsoSprite on the x axis. Increasing the x coordinate will move the object down and to the right on the screen. 73 | * 74 | * @name IsoSprite#isoX 75 | * @property {number} isoX - The axonometric position of the IsoSprite on the x axis. 76 | */ 77 | get isoX() { 78 | return this._isoPosition.x; 79 | } 80 | 81 | set isoX(value) { 82 | this._isoPosition.x = value; 83 | this._isoPositionChanged = this._isoBoundsChanged = true; 84 | if (this.body){ 85 | this.body._reset = true; 86 | } 87 | } 88 | 89 | /** 90 | * The axonometric position of the IsoSprite on the y axis. Increasing the y coordinate will move the object down and to the left on the screen. 91 | * 92 | * @name IsoSprite#isoY 93 | * @property {number} isoY - The axonometric position of the IsoSprite on the y axis. 94 | */ 95 | get isoY() { 96 | return this._isoPosition.y; 97 | } 98 | 99 | set isoY(value) { 100 | this._isoPosition.y = value; 101 | this._isoPositionChanged = this._isoBoundsChanged = true; 102 | 103 | if (this.body){ 104 | this.body._reset = true; 105 | } 106 | } 107 | 108 | /** 109 | * The axonometric position of the IsoSprite on the z axis. Increasing the z coordinate will move the object directly upwards on the screen. 110 | * 111 | * @name Phaser.Plugin.Isometric.IsoSprite#isoZ 112 | * @property {number} isoZ - The axonometric position of the IsoSprite on the z axis. 113 | */ 114 | get isoZ() { 115 | return this._isoPosition.z; 116 | } 117 | 118 | set isoZ(value) { 119 | this._isoPosition.z = value; 120 | this._isoPositionChanged = this._isoBoundsChanged = true; 121 | if (this.body){ 122 | this.body._reset = true; 123 | } 124 | } 125 | 126 | /** 127 | * A Point3 object representing the axonometric position of the IsoSprite. 128 | * 129 | * @name Phaser.Plugin.Isometric.IsoSprite#isoPosition 130 | * @property {Point3} isoPosition - The axonometric position of the IsoSprite. 131 | * @readonly 132 | */ 133 | get isoPosition() { 134 | return this._isoPosition; 135 | } 136 | 137 | /** 138 | * A Cube object representing the derived boundsof the IsoSprite. 139 | * 140 | * @name Phaser.Plugin.Isometric.IsoSprite#isoBounds 141 | * @property {Point3} isoBounds - The derived 3D bounds of the IsoSprite. 142 | * @readonly 143 | */ 144 | get isoBounds() { 145 | if (this._isoBoundsChanged || !this._isoBounds) { 146 | this.resetIsoBounds(); 147 | this._isoBoundsChanged = false; 148 | } 149 | 150 | return this._isoBounds; 151 | } 152 | 153 | /** 154 | * Internal function that performs the axonometric projection from 3D to 2D space. 155 | * @method Phaser.Plugin.Isometric.IsoSprite#_project 156 | * @memberof Phaser.Plugin.Isometric.IsoSprite 157 | * @private 158 | */ 159 | _project() { 160 | if (this._isoPositionChanged) { 161 | const pluginKey = this.scene.sys.settings.map.isoPlugin; 162 | const sceneProjector = this.scene[pluginKey].projector; 163 | const { x, y } = sceneProjector.project(this._isoPosition); 164 | 165 | this.x = x; 166 | this.y = y; 167 | this.depth = (this._isoPosition.x + this._isoPosition.y) + (this._isoPosition.z * 1.25); 168 | 169 | if (this.snap > 0) { 170 | this.x = Phaser.Math.snapTo(this.x, this.snap); 171 | this.y = Phaser.Math.snapTo(this.y, this.snap); 172 | } 173 | 174 | this._isoPositionChanged = this._isoBoundsChanged = true; 175 | } 176 | } 177 | 178 | /** 179 | * Internal function called by the World update cycle. 180 | * 181 | * @method IsoSprite#preUpdate 182 | * @memberof IsoSprite 183 | */ 184 | preUpdate(time, delta) { 185 | Sprite.prototype.preUpdate.call(this, time, delta); 186 | 187 | this._project(); 188 | } 189 | 190 | resetIsoBounds() { 191 | if (typeof this._isoBounds === 'undefined') { 192 | this._isoBounds = new Cube(); 193 | } 194 | 195 | var asx = Math.abs(this.scaleX); 196 | var asy = Math.abs(this.scaleY); 197 | 198 | this._isoBounds.widthX = Math.round(Math.abs(this.width) * 0.5) * asx; 199 | this._isoBounds.widthY = Math.round(Math.abs(this.width) * 0.5) * asx; 200 | this._isoBounds.height = Math.round(Math.abs(this.height) - (Math.abs(this.width) * 0.5)) * asy; 201 | 202 | this._isoBounds.x = this.isoX + (this._isoBounds.widthX * -this.originX) + this._isoBounds.widthX * 0.5; 203 | this._isoBounds.y = this.isoY + (this._isoBounds.widthY * this.originX) - this._isoBounds.widthY * 0.5; 204 | this._isoBounds.z = this.isoZ - (Math.abs(this.height) * (1 - this.originY)) + (Math.abs(this.width * 0.5)); 205 | 206 | return this._isoBounds; 207 | } 208 | } 209 | 210 | 211 | 212 | 213 | // Phaser.Utils.Debug.prototype.isoSprite = function (sprite, color, filled) { 214 | // 215 | // if (!sprite.isoBounds) { 216 | // return; 217 | // } 218 | // 219 | // if (typeof filled === 'undefined') { 220 | // filled = true; 221 | // } 222 | // 223 | // color = color || 'rgba(0,255,0,0.4)'; 224 | // 225 | // 226 | // var points = [], 227 | // corners = sprite.isoBounds.getCorners(); 228 | // 229 | // var posX = -sprite.game.camera.x; 230 | // var posY = -sprite.game.camera.y; 231 | // 232 | // this.start(); 233 | // 234 | // if (filled) { 235 | // points = [corners[1], corners[3], corners[2], corners[6], corners[4], corners[5], corners[1]]; 236 | // 237 | // points = points.map(function (p) { 238 | // var newPos = sprite.game.iso.project(p); 239 | // newPos.x += posX; 240 | // newPos.y += posY; 241 | // return newPos; 242 | // }); 243 | // this.context.beginPath(); 244 | // this.context.fillStyle = color; 245 | // this.context.moveTo(points[0].x, points[0].y); 246 | // 247 | // for (var i = 1; i < points.length; i++) { 248 | // this.context.lineTo(points[i].x, points[i].y); 249 | // } 250 | // this.context.fill(); 251 | // } else { 252 | // points = corners.slice(0, corners.length); 253 | // points = points.map(function (p) { 254 | // var newPos = sprite.game.iso.project(p); 255 | // newPos.x += posX; 256 | // newPos.y += posY; 257 | // return newPos; 258 | // }); 259 | // 260 | // this.context.moveTo(points[0].x, points[0].y); 261 | // this.context.beginPath(); 262 | // this.context.strokeStyle = color; 263 | // 264 | // this.context.lineTo(points[1].x, points[1].y); 265 | // this.context.lineTo(points[3].x, points[3].y); 266 | // this.context.lineTo(points[2].x, points[2].y); 267 | // this.context.lineTo(points[6].x, points[6].y); 268 | // this.context.lineTo(points[4].x, points[4].y); 269 | // this.context.lineTo(points[5].x, points[5].y); 270 | // this.context.lineTo(points[1].x, points[1].y); 271 | // this.context.lineTo(points[0].x, points[0].y); 272 | // this.context.lineTo(points[4].x, points[4].y); 273 | // this.context.moveTo(points[0].x, points[0].y); 274 | // this.context.lineTo(points[2].x, points[2].y); 275 | // this.context.moveTo(points[3].x, points[3].y); 276 | // this.context.lineTo(points[7].x, points[7].y); 277 | // this.context.lineTo(points[6].x, points[6].y); 278 | // this.context.moveTo(points[7].x, points[7].y); 279 | // this.context.lineTo(points[5].x, points[5].y); 280 | // this.context.stroke(); 281 | // this.context.closePath(); 282 | // } 283 | // 284 | // this.stop(); 285 | // 286 | // }; 287 | -------------------------------------------------------------------------------- /src/Octree.js: -------------------------------------------------------------------------------- 1 | import Cube from './Cube'; 2 | 3 | /** 4 | * @class Octree 5 | * 6 | * @classdesc A Octree implementation based on Phaser.QuadTree. 7 | * Original version at https://github.com/timohausmann/quadtree-js/ 8 | */ 9 | class Octree { 10 | /** 11 | * @constructor 12 | * @param {number} x - The bottom-back coordinate of the octree. 13 | * @param {number} y - The bottom-back coordinate of the octree. 14 | * @param {number} z - The bottom-back coordinate of the octree. 15 | * @param {number} widthX - The width X (breadth) of the octree. 16 | * @param {number} widthY - The width Y (depth) of the octree. 17 | * @param {number} height - The height (Z) of the octree. 18 | * @param {number} [maxObjects=10] - The maximum number of objects per node. 19 | * @param {number} [maxLevels=4] - The maximum number of levels to iterate to. 20 | * @param {number} [level=0] - Which level is this? 21 | */ 22 | constructor(x, y, z, widthX, widthY, height, maxObjects, maxLevels, level) { 23 | /** 24 | * @property {number} maxObjects - The maximum number of objects per node. 25 | * @default 26 | */ 27 | this.maxObjects = 10; 28 | 29 | /** 30 | * @property {number} maxLevels - The maximum number of levels to break down to. 31 | * @default 32 | */ 33 | this.maxLevels = 4; 34 | 35 | /** 36 | * @property {number} level - The current level. 37 | */ 38 | this.level = 0; 39 | 40 | /** 41 | * @property {object} bounds - Object that contains the octree bounds. 42 | */ 43 | this.bounds = {}; 44 | 45 | /** 46 | * @property {array} objects - Array of octree children. 47 | */ 48 | this.objects = []; 49 | 50 | /** 51 | * @property {array} nodes - Array of associated child nodes. 52 | */ 53 | this.nodes = []; 54 | 55 | /** 56 | * @property {array} _empty - Internal empty array. 57 | * @private 58 | */ 59 | this._empty = []; 60 | 61 | this.reset(x, y, z, widthX, widthY, height, maxObjects, maxLevels, level); 62 | } 63 | 64 | /** 65 | * Resets the QuadTree. 66 | * 67 | * @method Octree#reset 68 | * @param {number} x - The bottom-back coordinate of the octree. 69 | * @param {number} y - The bottom-back coordinate of the octree. 70 | * @param {number} z - The bottom-back coordinate of the octree. 71 | * @param {number} widthX - The width X (breadth) of the octree. 72 | * @param {number} widthY - The width Y (depth) of the octree. 73 | * @param {number} height - The height (Z) of the octree. 74 | * @param {number} [maxObjects=10] - The maximum number of objects per node. 75 | * @param {number} [maxLevels=4] - The maximum number of levels to iterate to. 76 | * @param {number} [level=0] - Which level is this? 77 | */ 78 | reset(x, y, z, widthX, widthY, height, maxObjects, maxLevels, level) { 79 | this.maxObjects = maxObjects || 10; 80 | this.maxLevels = maxLevels || 4; 81 | this.level = level || 0; 82 | 83 | this.bounds = { 84 | x: Math.round(x), 85 | y: Math.round(y), 86 | z: Math.round(z), 87 | widthX: widthX, 88 | widthY: widthY, 89 | height: height, 90 | subWidthX: Math.floor(widthX * 0.5), 91 | subWidthY: Math.floor(widthY * 0.5), 92 | subHeight: Math.floor(height * 0.5), 93 | frontX: Math.round(x) + Math.floor(widthX * 0.5), 94 | frontY: Math.round(y) + Math.floor(widthY * 0.5), 95 | top: Math.round(z) + Math.floor(height * 0.5) 96 | }; 97 | 98 | this.objects.length = 0; 99 | this.nodes.length = 0; 100 | } 101 | 102 | /** 103 | * Populates this octree with the children of the given Group. In order to be added the child must exist and have a body property. 104 | * 105 | * @method Octree#populate 106 | * @param {Phaser.Group} group - The Group to add to the octree. 107 | */ 108 | populate(group) { 109 | const len = group.children.size; 110 | const children = group.children.entries; 111 | 112 | for(let i = 0; i < len; i++) { 113 | this.populateHandler(children[i]); 114 | } 115 | } 116 | 117 | /** 118 | * Handler for the populate method. 119 | * 120 | * @method Octree#populateHandler 121 | * @param {IsoSprite|object} sprite - The Sprite to check. 122 | */ 123 | populateHandler(sprite) { 124 | if (sprite.body) { 125 | this.insert(sprite.body); 126 | } 127 | } 128 | 129 | /** 130 | * Split the node into 8 subnodes 131 | * 132 | * @method Octree#split 133 | */ 134 | split() { 135 | // bottom four octants 136 | // -x-y-z 137 | this.nodes[0] = new Octree(this.bounds.x, this.bounds.y, this.bounds.z, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 138 | // +x-y-z 139 | this.nodes[1] = new Octree(this.bounds.frontX, this.bounds.y, this.bounds.z, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 140 | // -x+y-z 141 | this.nodes[2] = new Octree(this.bounds.x, this.bounds.frontY, this.bounds.z, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 142 | // +x+y-z 143 | this.nodes[3] = new Octree(this.bounds.frontX, this.bounds.frontY, this.bounds.z, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 144 | 145 | // top four octants 146 | // -x-y+z 147 | this.nodes[4] = new Octree(this.bounds.x, this.bounds.y, this.bounds.top, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 148 | // +x-y+z 149 | this.nodes[5] = new Octree(this.bounds.frontX, this.bounds.y, this.bounds.top, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 150 | // -x+y+z 151 | this.nodes[6] = new Octree(this.bounds.x, this.bounds.frontY, this.bounds.top, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 152 | // +x+y+z 153 | this.nodes[7] = new Octree(this.bounds.frontX, this.bounds.frontY, this.bounds.top, this.bounds.subWidthX, this.bounds.subWidthY, this.bounds.subHeight, this.maxLevels, (this.level + 1)); 154 | } 155 | 156 | /** 157 | * Insert the object into the node. If the node exceeds the capacity, it will split and add all objects to their corresponding subnodes. 158 | * 159 | * @method Octree#insert 160 | * @param {Body|Cube|object} body - The Body object to insert into the octree. Can be any object so long as it exposes x, y, z, frontX, frontY and top properties. 161 | */ 162 | insert(body) { 163 | var i = 0; 164 | var index; 165 | 166 | // if we have subnodes ... 167 | if (this.nodes[0] != null) { 168 | index = this.getIndex(body); 169 | 170 | if (index != -1) { 171 | this.nodes[index].insert(body); 172 | return; 173 | } 174 | } 175 | 176 | this.objects.push(body); 177 | 178 | if (this.objects.length > this.maxObjects && this.level < this.maxLevels) { 179 | // Split if we don't already have subnodes 180 | if (this.nodes[0] == null) { 181 | this.split(); 182 | } 183 | 184 | // Add objects to subnodes 185 | while (i < this.objects.length) { 186 | index = this.getIndex(this.objects[i]); 187 | 188 | if (index != -1) { 189 | // this is expensive - see what we can do about it 190 | this.nodes[index].insert(this.objects.splice(i, 1)[0]); 191 | } else { 192 | i++; 193 | } 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * Determine which node the object belongs to. 200 | * 201 | * @method Octree#getIndex 202 | * @param {Cube|object} cube - The bounds in which to check. 203 | * @return {number} index - Index of the subnode (0-7), or -1 if cube cannot completely fit within a subnode and is part of the parent node. 204 | */ 205 | getIndex(cube) { 206 | // default is that cube doesn't fit, i.e. it straddles the internal octants 207 | var index = -1; 208 | 209 | if (cube.x < this.bounds.frontX && cube.frontX < this.bounds.frontX) { 210 | if (cube.y < this.bounds.frontY && cube.frontY < this.bounds.frontY) { 211 | if (cube.z < this.bounds.top && cube.top < this.bounds.top) { 212 | // cube fits into -x-y-z octant 213 | index = 0; 214 | } else if (cube.z > this.bounds.top) { 215 | // cube fits into -x-y+z octant 216 | index = 4; 217 | } 218 | } else if (cube.y > this.bounds.frontY) { 219 | if (cube.z < this.bounds.top && cube.top < this.bounds.top) { 220 | // cube fits into -x+y-z octant 221 | index = 2; 222 | } else if (cube.z > this.bounds.top) { 223 | // cube fits into -x+y+z octant 224 | index = 6; 225 | } 226 | } 227 | } else if (cube.x > this.bounds.frontX) { 228 | if (cube.y < this.bounds.frontY && cube.frontY < this.bounds.frontY) { 229 | if (cube.z < this.bounds.top && cube.top < this.bounds.top) { 230 | // cube fits into +x-y-z octant 231 | index = 1; 232 | } else if (cube.z > this.bounds.top) { 233 | // cube fits into +x-y+z octant 234 | index = 5; 235 | } 236 | } else if (cube.y > this.bounds.frontY) { 237 | if (cube.z < this.bounds.top && cube.top < this.bounds.top) { 238 | // cube fits into +x+y-z octant 239 | index = 3; 240 | } else if (cube.z > this.bounds.top) { 241 | // cube fits into +x+y+z octant 242 | index = 7; 243 | } 244 | } 245 | } 246 | 247 | return index; 248 | } 249 | 250 | /** 251 | * Return all objects that could collide with the given IsoSprite or Cube. 252 | * 253 | * @method Octree#retrieve 254 | * @param {IsoSprite|Cube} source - The source object to check the Octree against. Either a IsoSprite or Cube. 255 | * @return {array} - Array with all detected objects. 256 | */ 257 | retrieve(source) { 258 | var returnObjects, index; 259 | 260 | if (source instanceof Cube) { 261 | returnObjects = this.objects; 262 | 263 | index = this.getIndex(source); 264 | } else { 265 | if (!source.body) { 266 | return this._empty; 267 | } 268 | 269 | returnObjects = this.objects; 270 | 271 | index = this.getIndex(source.body); 272 | } 273 | 274 | if (this.nodes[0]) { 275 | // If cube fits into a subnode .. 276 | if (index !== -1) { 277 | returnObjects = returnObjects.concat(this.nodes[index].retrieve(source)); 278 | } else { 279 | // If cube does not fit into a subnode, check it against all subnodes (unrolled for speed) 280 | returnObjects = returnObjects.concat(this.nodes[0].retrieve(source)); 281 | returnObjects = returnObjects.concat(this.nodes[1].retrieve(source)); 282 | returnObjects = returnObjects.concat(this.nodes[2].retrieve(source)); 283 | returnObjects = returnObjects.concat(this.nodes[3].retrieve(source)); 284 | returnObjects = returnObjects.concat(this.nodes[4].retrieve(source)); 285 | returnObjects = returnObjects.concat(this.nodes[5].retrieve(source)); 286 | returnObjects = returnObjects.concat(this.nodes[6].retrieve(source)); 287 | returnObjects = returnObjects.concat(this.nodes[7].retrieve(source)); 288 | } 289 | } 290 | 291 | return returnObjects; 292 | } 293 | 294 | /** 295 | * Clear the octree. 296 | * @method Octree#clear 297 | */ 298 | clear() { 299 | this.objects.length = 0; 300 | 301 | var i = this.nodes.length; 302 | 303 | while (i--) { 304 | this.nodes[i].clear(); 305 | this.nodes.splice(i, 1); 306 | } 307 | 308 | this.nodes.length = 0; 309 | } 310 | } 311 | 312 | /** 313 | * Visually renders an Octree to the display. 314 | * 315 | * @method Phaser.Utils.Debug#octree 316 | * @param {Octree} octree - The octree to render. 317 | * @param {string} color - The color of the lines in the quadtree. 318 | */ 319 | // Phaser.Utils.Debug.prototype.octree = function (octree, color) { 320 | // color = color || 'rgba(255,0,0,0.3)'; 321 | // 322 | // this.start(); 323 | // 324 | // var bounds = octree.bounds, 325 | // i, points; 326 | // 327 | // if (octree.nodes.length === 0) { 328 | // 329 | // this.context.strokeStyle = color; 330 | // 331 | // var cube = new Cube(bounds.x, bounds.y, bounds.z, bounds.widthX, bounds.widthY, bounds.height); 332 | // var corners = cube.getCorners(); 333 | // 334 | // var posX = -this.game.camera.x; 335 | // var posY = -this.game.camera.y; 336 | // 337 | // points = corners.slice(0, corners.length); 338 | // 339 | // points = points.map(function (p) { 340 | // var newPos = this.game.iso.project(p); 341 | // newPos.x += posX; 342 | // newPos.y += posY; 343 | // return newPos; 344 | // }); 345 | // 346 | // this.context.moveTo(points[0].x, points[0].y); 347 | // this.context.beginPath(); 348 | // this.context.strokeStyle = color; 349 | // 350 | // this.context.lineTo(points[1].x, points[1].y); 351 | // this.context.lineTo(points[3].x, points[3].y); 352 | // this.context.lineTo(points[2].x, points[2].y); 353 | // this.context.lineTo(points[6].x, points[6].y); 354 | // this.context.lineTo(points[4].x, points[4].y); 355 | // this.context.lineTo(points[5].x, points[5].y); 356 | // this.context.lineTo(points[1].x, points[1].y); 357 | // this.context.lineTo(points[0].x, points[0].y); 358 | // this.context.lineTo(points[4].x, points[4].y); 359 | // this.context.moveTo(points[0].x, points[0].y); 360 | // this.context.lineTo(points[2].x, points[2].y); 361 | // this.context.moveTo(points[3].x, points[3].y); 362 | // this.context.lineTo(points[7].x, points[7].y); 363 | // this.context.lineTo(points[6].x, points[6].y); 364 | // this.context.moveTo(points[7].x, points[7].y); 365 | // this.context.lineTo(points[5].x, points[5].y); 366 | // this.context.stroke(); 367 | // this.context.closePath(); 368 | // 369 | // for (i = 0; i < octree.objects.length; i++) { 370 | // this.body(octree.objects[i].sprite, 'rgb(0,255,0)', false); 371 | // } 372 | // } else { 373 | // for (i = 0; i < octree.nodes.length; i++) { 374 | // this.octree(octree.nodes[i]); 375 | // } 376 | // } 377 | // 378 | // this.stop(); 379 | // }; 380 | 381 | export default Octree; 382 | -------------------------------------------------------------------------------- /src/physics/IsoPhysics.js: -------------------------------------------------------------------------------- 1 | import Point3 from '../Point3'; 2 | import World from './World'; 3 | 4 | /** 5 | * IsoPhysics Physics constructor. 6 | * 7 | * @class IsoPhysics 8 | * @classdesc IsoPhysics Physics 9 | */ 10 | export default class IsoPhysics { 11 | /** 12 | * @constructor 13 | * @param {Phaser.Scene} Reference to the current scene instance. 14 | */ 15 | constructor(scene) { 16 | /** 17 | * @property {Phaser.Scene} scene - Local reference to scene. 18 | */ 19 | this.scene = scene; 20 | 21 | /** 22 | * @property {Projector} projector - Local reference to the current projector. 23 | */ 24 | const pluginKey = this.scene.sys.settings.map.isoPlugin; 25 | this.projector = this.scene[pluginKey].projector; 26 | 27 | /** 28 | * @property {World} world - The physics World that gets created on start. 29 | */ 30 | this.world = new World(this.scene); 31 | } 32 | 33 | /** 34 | * Find the distance between two display objects (like Sprites). 35 | * 36 | * @method Isometric.IsoPhysics#distanceBetween 37 | * @param {any} source - The Display Object to test from. 38 | * @param {any} target - The Display Object to test to. 39 | * @return {number} The distance between the source and target objects. 40 | */ 41 | distanceBetween(source, target) { 42 | this._dx = source.x - target.x; 43 | this._dy = source.y - target.y; 44 | this._dz = source.z - target.z; 45 | 46 | return Math.sqrt(this._dx * this._dx + this._dy * this._dy + this._dz * this._dz); 47 | } 48 | 49 | /** 50 | * Find the distance between a display object (like a Sprite) and the given x/y coordinates only (ignore z). 51 | * The calculation is made from the display objects x/y coordinate. This may be the top-left if its anchor hasn't been changed. 52 | * If you need to calculate from the center of a display object instead use the method distanceBetweenCenters() 53 | * 54 | * @method IsoPhysics#distanceToXY 55 | * @param {any} displayObject - The Display Object to test from. 56 | * @param {number} x - The x coordinate to test to. 57 | * @param {number} y - The y coordinate to test to. 58 | * @return {number} The distance between the object and the x/y coordinates. 59 | */ 60 | distanceToXY(displayObject, x, y) { 61 | this._dx = displayObject.x - x; 62 | this._dy = displayObject.y - y; 63 | 64 | return Math.sqrt(this._dx * this._dx + this._dy * this._dy); 65 | } 66 | 67 | /** 68 | * Find the distance between a display object (like a Sprite) and the given x/y/z coordinates. 69 | * The calculation is made from the display objects x/y/z coordinate. This may be the top-left if its anchor hasn't been changed. 70 | * If you need to calculate from the center of a display object instead use the method distanceBetweenCenters() 71 | * 72 | * @method IsoPhysics#distanceToXYZ 73 | * @param {any} displayObjectBody - The Display Object to test from. 74 | * @param {number} x - The x coordinate to test to. 75 | * @param {number} y - The y coordinate to test to. 76 | * @param {number} z - The y coordinate to test to 77 | * @return {number} The distance between the object and the x/y coordinates. 78 | */ 79 | distanceToXYZ(displayObjectBody, x, y, z) { 80 | this._dx = displayObjectBody.x - x; 81 | this._dy = displayObjectBody.y - y; 82 | this._dz = displayObjectBody.z - z; 83 | 84 | return Math.sqrt(this._dx * this._dx + this._dy * this._dy + this._dz * this._dz); 85 | } 86 | 87 | /** 88 | * Find the distance between a display object (like a Sprite) and a Pointer. If no Pointer is given the Input.activePointer is used. 89 | * The calculation is made from the display objects x/y coordinate. This may be the top-left if its anchor hasn't been changed. 90 | * If you need to calculate from the center of a display object instead use the method distanceBetweenCenters() 91 | * The distance to the Pointer is returned in isometric distance. 92 | * 93 | * @method IsoPhysics#distanceToPointer 94 | * @param {any} displayObjectBody - The Display Object to test from. 95 | * @param {Phaser.Pointer} [pointer] - The Phaser.Pointer to test to. If none is given then Input.activePointer is used. 96 | * @return {number} The distance between the object and the Pointer. 97 | */ 98 | distanceToPointer(displayObjectBody, pointer) { 99 | pointer = pointer || this.scene.input.activePointer; 100 | var isoPointer = this.projector.unproject(pointer.position, undefined, displayObjectBody.z); 101 | isoPointer.z = displayObjectBody.z; 102 | var a = this.anglesToXYZ(displayObjectBody, isoPointer.x, isoPointer.y, isoPointer.z); 103 | 104 | return a.r; 105 | } 106 | 107 | /** 108 | * Find the angles in radians between a display object (like a IsoSprite) and the given x/y/z coordinate. 109 | * 110 | * @method Phaser.Physics.Isometric.Isometric.IsoPhysics#anglesToXYZ 111 | * @param {any} displayObjectBody - The Display Object to test from. 112 | * @param {number} x - The x coordinate to get the angle to. 113 | * @param {number} y - The y coordinate to get the angle to. 114 | * @param {number} z - The z coordinate to get the angle to. 115 | * @return {number} The angle in radians between displayObjectBody.x/y to Pointer.x/y 116 | */ 117 | anglesToXYZ(displayObjectBody, x, y, z) { 118 | // Spherical polar coordinates 119 | var r = this.distanceToXYZ(displayObjectBody, x, y, z); 120 | var theta = Math.atan2(y - displayObjectBody.y, x - displayObjectBody.x); 121 | var phi = Math.acos((z - displayObjectBody.z)/ r); 122 | 123 | return {r:r,theta:theta,phi:phi}; 124 | } 125 | 126 | /** 127 | * Find the angle in radians between a display object (like a Sprite) and a Pointer, taking their x/y and center into account. 128 | * This is not the visual angle but the angle in the isometric co-ordinate system. 129 | * 130 | * @method Phaser.Physics.Isometric.IsoPhysics#angleToPointer 131 | * @param {any} displayObjectBody - The Display Object to test from. 132 | * @param {Phaser.Pointer} [pointer] - The Phaser.Pointer to test to. If none is given then Input.activePointer is used. 133 | * @return {number} The (isometric) angle in radians between displayObjectBody.x/y to Pointer.x/y. 134 | */ 135 | angleToPointer(displayObjectBody, pointer) { 136 | pointer = pointer || this.scene.input.activePointer; 137 | var isoPointer = this.projector.unproject(pointer.position, undefined, displayObjectBody.z); 138 | isoPointer.z = displayObjectBody.z; 139 | var a = this.anglesToXYZ(displayObjectBody, isoPointer.x, isoPointer.y, isoPointer.z); 140 | 141 | return a.theta; 142 | } 143 | 144 | /** 145 | * Given the angle (in degrees) and speed calculate the velocity and return it as a Point object, or set it to the given point object. 146 | * One way to use this is: velocityFromAngle(angle, 200, sprite.velocity) which will set the values directly to the sprites velocity and not create a new Point object. 147 | * 148 | * @method Phaser.Physics.IsoPhysics#velocityFromAngle 149 | * @param {number} theta - The angle in radians for x,y in the isometric co-ordinate system 150 | * @param {number} [phi=Math.PI/2] - The angle in radians for z in the isometric co-ordinate system 151 | * @param {number} [speed=60] - The speed it will move, in pixels per second sq. 152 | * @param {Phaser.Point|object} [point] - The Point object in which the x and y properties will be set to the calculated velocity. 153 | * @return {Point3} - A Point where point.x contains the velocity x value and so on for y and z. 154 | */ 155 | velocityFromAngles(theta, phi, speed) { 156 | if (phi === undefined) { phi = Math.sin(Math.PI/2); } 157 | if (speed === undefined) { speed = 60; } 158 | 159 | return new Point3( 160 | Math.cos(theta) * Math.sin(phi) * speed, 161 | Math.sin(theta) * Math.sin(phi) * speed, 162 | Math.cos(phi) * speed 163 | ); 164 | } 165 | 166 | /** 167 | * Sets the acceleration.x/y property on the display object so it will move towards the x/y coordinates at the given speed (in pixels per second sq.) 168 | * You must give a maximum speed value, beyond which the display object won't go any faster. 169 | * Note: The display object does not continuously track the target. If the target changes location during transit the display object will not modify its course. 170 | * Note: The display object doesn't stop moving once it reaches the destination coordinates. 171 | * 172 | * @method Phaser.Physics.Isometric.IsoPhysics#accelerateToXYZ 173 | * @param {any} displayObject - The display object to move. 174 | * @param {number} x - The x coordinate to accelerate towards. 175 | * @param {number} y - The y coordinate to accelerate towards. 176 | * @param {number} z - The z coordinate to accelerate towards. 177 | * @param {number} [speed=60] - The speed it will accelerate in pixels per second. 178 | * @param {number} [xSpeedMax=500] - The maximum x velocity the display object can reach. 179 | * @param {number} [ySpeedMax=500] - The maximum y velocity the display object can reach. 180 | * @param {number} [zSpeedMax=500] - The maximum z velocity the display object can reach. 181 | * @return {number} The angle (in radians). 182 | */ 183 | accelerateToXYZ(displayObject, x, y, z, speed, xSpeedMax, ySpeedMax, zSpeedMax) { 184 | if (speed === undefined) { speed = 60; } 185 | if (xSpeedMax === undefined) { xSpeedMax = 500; } 186 | if (ySpeedMax === undefined) { ySpeedMax = 500; } 187 | if (zSpeedMax === undefined) { zSpeedMax = 500; } 188 | 189 | var a = this.anglesToXYZ(displayObject.body, x, y,z); 190 | var v = this.velocityFromAngles(a.theta,a.phi,speed); 191 | 192 | displayObject.body.acceleration.setTo(v.x,v.y,v.z); 193 | displayObject.body.maxVelocity.setTo(xSpeedMax, ySpeedMax, zSpeedMax); 194 | 195 | return a.theta; 196 | } 197 | 198 | /** 199 | * Move the given display object towards the x/y coordinates at a steady velocity. 200 | * If you specify a maxTime then it will adjust the speed (over-writing what you set) so it arrives at the destination in that number of seconds. 201 | * Timings are approximate due to the way browser timers work. Allow for a variance of +- 50ms. 202 | * Note: The display object does not continuously track the target. If the target changes location during transit the display object will not modify its course. 203 | * Note: The display object doesn't stop moving once it reaches the destination coordinates. 204 | * Note: Doesn't take into account acceleration, maxVelocity or drag (if you've set drag or acceleration too high this object may not move at all) 205 | * 206 | * @method Phaser.Physics.Isometric.IsoPhysics#moveToXYZ 207 | * @param {any} displayObject - The display object to move, must have an isoArcade body. 208 | * @param {number} x - The x coordinate to move towards. 209 | * @param {number} y - The y coordinate to move towards. 210 | * @param {number} z - The z coordinate to move towards. 211 | * @param {number} [speed=60] - The speed it will move, in pixels per second (default is 60 pixels/sec) 212 | * @param {number} [maxTime=0] - Time given in milliseconds (1000 = 1 sec). If set the speed is adjusted so the object will arrive at destination in the given number of ms. 213 | * @return {number} The angle (in radians). 214 | */ 215 | moveToXYZ(displayObject, x, y, z, speed, maxTime) { 216 | if (typeof speed === 'undefined') { 217 | speed = 60; 218 | } 219 | if (typeof maxTime === 'undefined') { 220 | maxTime = 0; 221 | } 222 | 223 | if (maxTime > 0) { 224 | // We know how many pixels we need to move, but how fast? 225 | speed = this.distanceToXYZ(displayObject.body, x, y ,z) / (maxTime / 1000); 226 | } 227 | var a = this.anglesToXYZ(displayObject.body, x, y,z); 228 | var v = this.velocityFromAngles(a.theta,a.phi,speed); 229 | 230 | displayObject.body.velocity.copyFrom(v); 231 | 232 | return a.theta; 233 | } 234 | 235 | /** 236 | * Move the given display object towards the destination object at a steady velocity. 237 | * If you specify a maxTime then it will adjust the speed (overwriting what you set) so it arrives at the destination in that number of seconds. 238 | * Timings are approximate due to the way browser timers work. Allow for a variance of +- 50ms. 239 | * Note: The display object does not continuously track the target. If the target changes location during transit the display object will not modify its course. 240 | * Note: The display object doesn't stop moving once it reaches the destination coordinates. 241 | * Note: Doesn't take into account acceleration, maxVelocity or drag (if you've set drag or acceleration too high this object may not move at all) 242 | * 243 | * @method Phaser.Physics.Isometric.IsoPhysics#moveToObject 244 | * @param {any} displayObject - The display object to move. 245 | * @param {any} destination - The display object to move towards. Can be any object but must have visible x/y/z properties. 246 | * @param {number} [speed=60] - The speed it will move, in pixels per second (default is 60 pixels/sec) 247 | * @param {number} [maxTime=0] - Time given in milliseconds (1000 = 1 sec). If set the speed is adjusted so the object will arrive at destination in the given number of ms. 248 | * @return {number} The angle (in radians). 249 | */ 250 | moveToObject(displayObject, destination, speed, maxTime) { 251 | return this.moveToXYZ(displayObject, destination.x, destination.y, destination.z, speed, maxTime); 252 | } 253 | 254 | /** 255 | * Move the given display object towards the pointer at a steady x & y velocity. If no pointer is given it will use Phaser.Input.activePointer. 256 | * If you specify a maxTime then it will adjust the speed (over-writing what you set) so it arrives at the destination in that number of seconds. 257 | * Timings are approximate due to the way browser timers work. Allow for a variance of +- 50ms. 258 | * Note: The display object does not continuously track the target. If the target changes location during transit the display object will not modify its course. 259 | * Note: The display object doesn't stop moving once it reaches the destination coordinates. 260 | * 261 | * @method Phaser.Physics.Isometric.IsoPhysics#moveToPointer 262 | * @param {any} displayObject - The display object to move. 263 | * @param {number} [speed=60] - The speed it will move, in pixels per second (default is 60 pixels/sec) 264 | * @param {Phaser.Pointer} [pointer] - The pointer to move towards. Defaults to Phaser.Input.activePointer. 265 | * @param {number} [maxTime=0] - Time given in milliseconds (1000 = 1 sec). If set the speed is adjusted so the object will arrive at destination in the given number of ms. 266 | * @return {number} The angle (in radians). 267 | */ 268 | moveToPointer(displayObject, speed, pointer, maxTime) { 269 | pointer = pointer || this.game.input.activePointer; 270 | var isoPointer = this.game.iso.unproject(pointer.position,undefined,displayObject.body.z); 271 | isoPointer.z = displayObject.body.z; 272 | 273 | if (typeof speed === 'undefined') { 274 | speed = 60; 275 | } 276 | if (typeof maxTime === 'undefined') { 277 | maxTime = 0; 278 | } 279 | 280 | if (maxTime > 0) { 281 | // We know how many pixels we need to move, but how fast? 282 | speed = this.distanceToXYZ(displayObject.body, isoPointer.x, isoPointer.y ,isoPointer.z) / (maxTime / 1000); 283 | } 284 | var a = this.anglesToXYZ(displayObject.body, isoPointer.x, isoPointer.y,isoPointer.z); 285 | var v = this.velocityFromAngles(a.theta,a.phi,speed); 286 | 287 | displayObject.body.velocity.x=v.x; 288 | displayObject.body.velocity.y=v.y; 289 | 290 | return a.theta; 291 | } 292 | 293 | boot() { 294 | const eventEmitter = this.scene.sys.events; 295 | 296 | eventEmitter.on('update', this.world.update, this.world); 297 | eventEmitter.on('postupdate', this.world.postUpdate, this.world); 298 | } 299 | 300 | static register(PluginManager) { 301 | PluginManager.register('IsoPhysics', IsoPhysics, 'isoPhysics'); 302 | } 303 | } 304 | 305 | // 306 | // Phaser.Physics.prototype.parseConfig = (function (_super) { 307 | // 308 | // return function () { 309 | // if (this.config.hasOwnProperty('isoArcade') && this.config['isoArcade'] === true && hasOwnProperty('IsoPhysics')) { 310 | // this.isoArcade = new Phaser.Plugin.Isometric(this.game, this.config); 311 | // } 312 | // return _super.call(this); 313 | // }; 314 | // 315 | // })(Phaser.Physics.prototype.parseConfig); 316 | // 317 | // Phaser.Physics.prototype.startSystem = (function (_super) { 318 | // 319 | // return function (system) { 320 | // if (system === ISOARCADE && this.isoArcade === null) { 321 | // this.isoArcade = new IsoPhysics(this.game); 322 | // this.setBoundsToWorld(); 323 | // } 324 | // return _super.call(this, system); 325 | // }; 326 | // 327 | // })(Phaser.Physics.prototype.startSystem); 328 | // 329 | // Phaser.Physics.prototype.enable = (function (_super) { 330 | // 331 | // return function (sprite, system) { 332 | // if (system === ISOARCADE && this.isoArcade) { 333 | // this.isoArcade.enable(sprite); 334 | // } 335 | // return _super.call(this, sprite, system); 336 | // }; 337 | // 338 | // })(Phaser.Physics.prototype.enable); 339 | // 340 | // Phaser.Physics.prototype.setBoundsToWorld = (function (_super) { 341 | // 342 | // return function () { 343 | // if (this.isoArcade) { 344 | // this.isoArcade.setBoundsToWorld(); 345 | // } 346 | // return _super.call(this); 347 | // }; 348 | // 349 | // })(Phaser.Physics.prototype.setBoundsToWorld); 350 | // 351 | // Phaser.Physics.prototype.destroy = (function (_super) { 352 | // 353 | // return function () { 354 | // this.isoArcade = null; 355 | // 356 | // return _super.call(this); 357 | // }; 358 | // 359 | // })(Phaser.Physics.prototype.destroy); 360 | -------------------------------------------------------------------------------- /src/Cube.js: -------------------------------------------------------------------------------- 1 | import Point3 from './Point3'; 2 | 3 | /** 4 | * @class Cube 5 | * 6 | * @classdesc 7 | * Creates a new Cube object with the bottom-back corner specified by the x, y and z parameters, with the specified breadth (widthX), depth (widthY) and height parameters. If you call this function without parameters, a Cube with x, y, z, breadth, depth and height properties set to 0 is created. 8 | */ 9 | class Cube { 10 | /** 11 | * @constructor 12 | * @param {number} x - The x coordinate of the bottom-back corner of the Cube. 13 | * @param {number} y - The y coordinate of the bottom-back corner of the Cube. 14 | * @param {number} z - The z coordinate of the bottom-back corner of the Cube. 15 | * @param {number} widthX - The X axis width (breadth) of the Cube. Should always be either zero or a positive value. 16 | * @param {number} widthY - The Y axis width (depth) of the Cube. Should always be either zero or a positive value. 17 | * @param {number} height - The Z axis height of the Cube. Should always be either zero or a positive value. 18 | * @return {Cube} This Cube object. 19 | */ 20 | constructor(x = 0, y = 0, z = 0, widthX = 0, widthY = 0, height = 0) { 21 | /** 22 | * @property {number} x - The x coordinate of the bottom-back corner of the Cube. 23 | */ 24 | this.x = x; 25 | 26 | /** 27 | * @property {number} y - The y coordinate of the bottom-back corner of the Cube. 28 | */ 29 | this.y = y; 30 | 31 | /** 32 | * @property {number} z - The z coordinate of the bottom-back corner of the Cube. 33 | */ 34 | this.z = z; 35 | 36 | /** 37 | * @property {number} widthX - The X axis width (breadth) of the Cube. This value should never be set to a negative. 38 | */ 39 | this.widthX = widthX; 40 | 41 | /** 42 | * @property {number} widthY - The Y axis width (depth) of the Cube. This value should never be set to a negative. 43 | */ 44 | this.widthY = widthY; 45 | 46 | /** 47 | * @property {number} height - The Z axis height of the Cube. This value should never be set to a negative. 48 | */ 49 | this.height = height; 50 | 51 | /** 52 | * @property {Array.} _corners - The 8 corners of the Cube. 53 | * @private 54 | */ 55 | this._corners = [ 56 | new Point3(this.x, this.y, this.z), 57 | new Point3(this.x, this.y, this.z + this.height), 58 | new Point3(this.x, this.y + this.widthY, this.z), 59 | new Point3(this.x, this.y + this.widthY, this.z + this.height), 60 | new Point3(this.x + this.widthX, this.y, this.z), 61 | new Point3(this.x + this.widthX, this.y, this.z + this.height), 62 | new Point3(this.x + this.widthX, this.y + this.widthY, this.z), 63 | new Point3(this.x + this.widthX, this.y + this.widthY, this.z + this.height) 64 | ]; 65 | } 66 | 67 | /** 68 | * Sets the members of Cube to the specified values. 69 | * @method Cube#setTo 70 | * @param {number} x - The x coordinate of the bottom-back corner of the Cube. 71 | * @param {number} y - The y coordinate of the bottom-back corner of the Cube. 72 | * @param {number} z - The z coordinate of the bottom-back corner of the Cube. 73 | * @param {number} widthX - The X axis width (breadth) of the Cube. This value should never be set to a negative. 74 | * @param {number} widthY - The Y axis width (depth) of the Cube. This value should never be set to a negative. 75 | * @param {number} height - The Z axis height of the Cube. This value should never be set to a negative. 76 | * @return {Cube} This Cube object 77 | */ 78 | setTo(x, y, z, widthX, widthY, height) { 79 | this.x = x; 80 | this.y = y; 81 | this.z = z; 82 | this.widthX = widthX; 83 | this.widthY = widthY; 84 | this.height = height; 85 | 86 | return this; 87 | } 88 | 89 | /** 90 | * Copies the x, y, z, widthX, widthY and height properties from any given object to this Cube. 91 | * @method Cube#copyFrom 92 | * @param {any} source - The object to copy from. 93 | * @return {Cube} This Cube object. 94 | */ 95 | copyFrom(source) { 96 | this.setTo(source.x, source.y, source.z, source.widthX, source.widthY, source.height); 97 | } 98 | 99 | /** 100 | * Copies the x, y, z, widthX, widthY and height properties from this Cube to any given object. 101 | * @method Cube#copyTo 102 | * @param {any} dest - The object to copy to. 103 | * @return {Cube} This Cube object. 104 | */ 105 | copyTo(dest) { 106 | dest.x = this.x; 107 | dest.y = this.y; 108 | dest.z = this.z; 109 | dest.widthX = this.widthX; 110 | dest.widthY = this.widthY; 111 | dest.height = this.height; 112 | 113 | return dest; 114 | } 115 | 116 | /** 117 | * The size of the Cube object, expressed as a Point3 object with the values of the widthX, widthY and height properties. 118 | * @method Cube#size 119 | * @param {Point3} [output] - Optional Point3 object. If given the values will be set into the object, otherwise a brand new Point3 object will be created and returned. 120 | * @return {Point3} The size of the Cube object. 121 | */ 122 | size(output) { 123 | return Cube.size(this, output); 124 | } 125 | 126 | /** 127 | * Determines whether the specified coordinates are contained within the region defined by this Cube object. 128 | * @method Cube#contains 129 | * @param {number} x - The x coordinate of the point to test. 130 | * @param {number} y - The y coordinate of the point to test. 131 | * @param {number} y - The z coordinate of the point to test. 132 | * @return {boolean} A value of true if the Cube object contains the specified point; otherwise false. 133 | */ 134 | contains(x, y, z) { 135 | return Cube.contains(this, x, y, z); 136 | } 137 | 138 | /** 139 | * Determines whether the specified X and Y coordinates are contained within the region defined by this Cube object. 140 | * @method Cube#containsXY 141 | * @param {number} x - The x coordinate of the point to test. 142 | * @param {number} y - The y coordinate of the point to test. 143 | * @return {boolean} A value of true if this Cube object contains the specified point; otherwise false. 144 | */ 145 | containsXY(x, y) { 146 | return Cube.containsXY(this, x, y); 147 | } 148 | 149 | /** 150 | * Returns a new Cube object with the same values for the x, y, z, widthX, widthY and height properties as the original Cube object. 151 | * @method Cube#clone 152 | * @param {Cube} [output] - Optional Cube object. If given the values will be set into the object, otherwise a brand new Cube object will be created and returned. 153 | * @return {Cube} 154 | */ 155 | clone(output) { 156 | return Cube.clone(this, output); 157 | } 158 | 159 | /** 160 | * Determines whether the two Cubes intersect with each other. 161 | * This method checks the x, y, z, widthX, widthY, and height properties of the Cubes. 162 | * @method Cube#intersects 163 | * @param {Cube} b - The second Cube object. 164 | * @return {boolean} A value of true if the specified object intersects with this Cube object; otherwise false. 165 | */ 166 | intersects(b) { 167 | return Cube.intersects(this, b); 168 | } 169 | 170 | /** 171 | * Updates and returns an Array of eight Point3 objects containing the corners of this Cube. 172 | * @method Cube#getCorners 173 | * @return {Array.} The corners of this Cube expressed as an Array of eight Point3 objects. 174 | */ 175 | getCorners() { 176 | this._corners[0].setTo(this.x, this.y, this.z); 177 | this._corners[1].setTo(this.x, this.y, this.z + this.height); 178 | this._corners[2].setTo(this.x, this.y + this.widthY, this.z); 179 | this._corners[3].setTo(this.x, this.y + this.widthY, this.z + this.height); 180 | this._corners[4].setTo(this.x + this.widthX, this.y, this.z); 181 | this._corners[5].setTo(this.x + this.widthX, this.y, this.z + this.height); 182 | this._corners[6].setTo(this.x + this.widthX, this.y + this.widthY, this.z); 183 | this._corners[7].setTo(this.x + this.widthX, this.y + this.widthY, this.z + this.height); 184 | 185 | return this._corners; 186 | } 187 | 188 | /** 189 | * Returns a string representation of this object. 190 | * @method Cube#toString 191 | * @return {string} A string representation of the instance. 192 | */ 193 | toString() { 194 | return `[{Cube (x=${this.x} y=${this.y} z=${this.z} widthX=${this.widthX} widthY=${this.widthY} height=${this.height} empty=${this.empty})}]`; 195 | } 196 | 197 | /** 198 | * @name Cube#halfWidthX 199 | * @property {number} halfWidthX - Half of the widthX of the Cube. 200 | * @readonly 201 | */ 202 | get halfWidthX() { 203 | return Math.round(this.widthX * 0.5); 204 | } 205 | 206 | /** 207 | * @name Cube#halfWidthY 208 | * @property {number} halfWidthY - Half of the widthY of the Cube. 209 | * @readonly 210 | */ 211 | get halfWidthY() { 212 | return Math.round(this.widthY * 0.5); 213 | } 214 | 215 | /** 216 | * @name Cube#halfHeight 217 | * @property {number} halfHeight - Half of the height of the Cube. 218 | * @readonly 219 | */ 220 | get halfHeight() { 221 | return Math.round(this.height * 0.5); 222 | } 223 | 224 | /** 225 | * The z coordinate of the bottom of the Cube. Changing the bottom property of a Cube object has no effect on the x, y, widthX and widthY properties. 226 | * However it does affect the height property, whereas changing the z value does not affect the height property. 227 | * @name Cube#bottom 228 | * @property {number} bottom - The z coordinate of the bottom of the Cube. 229 | */ 230 | get bottom() { 231 | return this.z; 232 | } 233 | 234 | set bottom(value) { 235 | if (value >= this.top) { 236 | this.height = 0; 237 | } else { 238 | this.height = (this.top - value); 239 | } 240 | this.z = value; 241 | } 242 | 243 | /** 244 | * The sum of the z and height properties. Changing the top property of a Cube object has no effect on the x, y, z, widthX and widthY properties, but does change the height property. 245 | * @name Cube#top 246 | * @property {number} top - The sum of the z and height properties. 247 | */ 248 | get top() { 249 | return this.z + this.height; 250 | } 251 | 252 | set top(value) { 253 | if (value <= this.z) { 254 | this.height = 0; 255 | } else { 256 | this.height = (value - this.z); 257 | } 258 | } 259 | 260 | /** 261 | * The x coordinate of the back of the Cube. Changing the backX property of a Cube object has no effect on the y, z, widthY and height properties. However it does affect the widthX property, whereas changing the x value does not affect the width property. 262 | * @name Cube#backX 263 | * @property {number} backX - The x coordinate of the left of the Cube. 264 | */ 265 | get backX() { 266 | return this.x; 267 | } 268 | 269 | set backX(value) { 270 | if (value >= this.frontX) { 271 | this.widthX = 0; 272 | } else { 273 | this.widthX = (this.frontX - value); 274 | } 275 | this.x = value; 276 | } 277 | 278 | /** 279 | * The y coordinate of the back of the Cube. Changing the backY property of a Cube object has no effect on the x, z, widthX and height properties. However it does affect the widthY property, whereas changing the y value does not affect the width property. 280 | * @name Cube#backY 281 | * @property {number} backY - The x coordinate of the left of the Cube. 282 | */ 283 | get backY() { 284 | return this.y; 285 | } 286 | 287 | set backY(value) { 288 | if (value >= this.frontY) { 289 | this.widthY = 0; 290 | } else { 291 | this.widthY = (this.frontY - value); 292 | } 293 | this.y = value; 294 | } 295 | 296 | /** 297 | * The sum of the x and widthX properties. Changing the frontX property of a Cube object has no effect on the x, y, z, widthY and height properties, however it does affect the widthX property. 298 | * @name Cube#frontX 299 | * @property {number} frontX - The sum of the x and widthX properties. 300 | */ 301 | get frontX() { 302 | return this.x + this.widthX; 303 | } 304 | 305 | set frontX(value) { 306 | if (value <= this.x) { 307 | this.widthX = 0; 308 | } else { 309 | this.widthX = (value - this.x); 310 | } 311 | } 312 | 313 | /** 314 | * The sum of the y and widthY properties. Changing the frontY property of a Cube object has no effect on the x, y, z, widthX and height properties, however it does affect the widthY property. 315 | * @name Cube#frontY 316 | * @property {number} frontY - The sum of the y and widthY properties. 317 | */ 318 | get frontY() { 319 | return this.y + this.widthY; 320 | } 321 | 322 | set frontY(value) { 323 | if (value <= this.y) { 324 | this.widthY = 0; 325 | } else { 326 | this.widthY = (value - this.y); 327 | } 328 | } 329 | 330 | /** 331 | * The volume of the Cube derived from widthX * widthY * height. 332 | * @name Cube#volume 333 | * @property {number} volume - The volume of the Cube derived from widthX * widthY * height. 334 | * @readonly 335 | */ 336 | get volume() { 337 | return this.widthX * this.widthY * this.height; 338 | } 339 | 340 | /** 341 | * The x coordinate of the center of the Cube. 342 | * @name Cube#centerX 343 | * @property {number} centerX - The x coordinate of the center of the Cube. 344 | */ 345 | get centerX() { 346 | return this.x + this.halfWidthX; 347 | } 348 | 349 | set centerX(value) { 350 | this.x = value - this.halfWidthX; 351 | } 352 | 353 | /** 354 | * The y coordinate of the center of the Cube. 355 | * @name Cube#centerY 356 | * @property {number} centerY - The y coordinate of the center of the Cube. 357 | */ 358 | get centerY() { 359 | return this.y + this.halfWidthY; 360 | } 361 | 362 | set centerY(value) { 363 | this.y = value - this.halfWidthY; 364 | } 365 | 366 | /** 367 | * The z coordinate of the center of the Cube. 368 | * @name Cube#centerZ 369 | * @property {number} centerZ - The z coordinate of the center of the Cube. 370 | */ 371 | get centerZ() { 372 | return this.z + this.halfHeight; 373 | } 374 | 375 | set centerZ(value) { 376 | this.z = value - this.halfHeight; 377 | } 378 | 379 | /** 380 | * A random value between the frontX and backX values (inclusive) of the Cube. 381 | * 382 | * @name Cube#randomX 383 | * @property {number} randomX - A random value between the frontX and backX values (inclusive) of the Cube. 384 | */ 385 | get randomX() { 386 | return this.x + (Math.random() * this.widthX); 387 | } 388 | 389 | /** 390 | * A random value between the frontY and backY values (inclusive) of the Cube. 391 | * 392 | * @name Cube#randomY 393 | * @property {number} randomY - A random value between the frontY and backY values (inclusive) of the Cube. 394 | */ 395 | get randomY() { 396 | return this.y + (Math.random() * this.widthY); 397 | } 398 | 399 | /** 400 | * A random value between the bottom and top values (inclusive) of the Cube. 401 | * 402 | * @name Cube#randomZ 403 | * @property {number} randomZ - A random value between the bottom and top values (inclusive) of the Cube. 404 | */ 405 | get randomZ() { 406 | return this.z + (Math.random() * this.height); 407 | } 408 | 409 | /** 410 | * Determines whether or not this Cube object is empty. A Cube object is empty if its widthX, widthY or height is less than or equal to 0. 411 | * If set to true then all of the Cube properties are set to 0. 412 | * @name Cube#empty 413 | * @property {boolean} empty - Gets or sets the Cube's empty state. 414 | */ 415 | get empty() { 416 | return (!this.widthX || !this.widthY || !this.height); 417 | } 418 | 419 | set empty(value) { 420 | if (value === true) { 421 | this.setTo(0, 0, 0, 0, 0, 0); 422 | } 423 | } 424 | 425 | /** 426 | * The size of the Cube object, expressed as a Point3 object with the values of the widthX, widthY and height properties. 427 | * @method Cube.size 428 | * @param {Cube} a - The Cube object. 429 | * @param {Point3} [output] - Optional Point3 object. If given the values will be set into the object, otherwise a brand new Point3 object will be created and returned. 430 | * @return {Point3} The size of the Cube object 431 | */ 432 | static size(a, output) { 433 | if (typeof output === 'undefined' || output === null) { 434 | output = new Point3(a.widthX, a.widthY, a.height); 435 | } else { 436 | output.setTo(a.widthX, a.widthY, a.height); 437 | } 438 | 439 | return output; 440 | } 441 | 442 | /** 443 | * Returns a new Cube object with the same values for the x, y, z, widthX, widthY, and height properties as the original Cube object. 444 | * @method Cube.clone 445 | * @param {Cube} a - The Cube object. 446 | * @param {Cube} [output] - Optional Cube object. If given the values will be set into the object, otherwise a brand new Cube object will be created and returned. 447 | * @return {Cube} 448 | */ 449 | static clone(a, output) { 450 | if (typeof output === 'undefined' || output === null) { 451 | output = new Cube(a.x, a.y, a.z, a.widthX, a.widthY, a.height); 452 | } else { 453 | output.setTo(a.x, a.y, a.z, a.widthX, a.widthY, a.height); 454 | } 455 | 456 | return output; 457 | } 458 | 459 | /** 460 | * Determines whether the specified coordinates are contained within the region defined by this Cube object. 461 | * @method Cube.contains 462 | * @param {Cube} a - The Cube object. 463 | * @param {number} x - The x coordinate of the point to test. 464 | * @param {number} y - The y coordinate of the point to test. 465 | * @param {number} z - The z coordinate of the point to test. 466 | * @return {boolean} A value of true if the Cube object contains the specified point; otherwise false. 467 | */ 468 | static contains(a, x, y, z) { 469 | if (a.widthX <= 0 || a.widthY <= 0 || a.height <= 0) { 470 | return false; 471 | } 472 | 473 | return (x >= a.x && x <= a.frontX && y >= a.y && y <= a.frontY && z >= a.z && z <= a.top); 474 | } 475 | 476 | /** 477 | * Determines whether the specified X and Y coordinates are contained within the region defined by this Cube object. 478 | * @method Cube.containsXY 479 | * @param {Cube} a - The Cube object. 480 | * @param {number} x - The x coordinate of the point to test. 481 | * @param {number} y - The y coordinate of the point to test. 482 | * @return {boolean} A value of true if the Cube object contains the specified point; otherwise false. 483 | */ 484 | static containsXY(a, x, y) { 485 | if (a.widthX <= 0 || a.widthY <= 0) { 486 | return false; 487 | } 488 | 489 | return (x >= a.x && x <= a.frontX && y >= a.y && y <= a.frontY); 490 | } 491 | 492 | /** 493 | * Determines whether the specified point is contained within the cubic region defined by this Cube object. This method is similar to the Cube.contains() method, except that it takes a Point3 object as a parameter. 494 | * @method Cube.containsPoint3 495 | * @param {Cube} a - The Cube object. 496 | * @param {Point3} point3 - The Point3 object being checked. Can be Point3 or any object with .x, .y and .z values. 497 | * @return {boolean} A value of true if the Cube object contains the specified point; otherwise false. 498 | */ 499 | static containsPoint3(a, point3) { 500 | return Cube.contains(a, point3.x, point3.y, point3.z); 501 | } 502 | 503 | /** 504 | * Determines whether the first Cube object is fully contained within the second Cube object. 505 | * A Cube object is said to contain another if the second Cube object falls entirely within the boundaries of the first. 506 | * @method Cube.containsCube 507 | * @param {Cube} a - The first Cube object. 508 | * @param {Cube} b - The second Cube object. 509 | * @return {boolean} A value of true if the Cube object contains the specified point; otherwise false. 510 | */ 511 | static containsCube(a, b) { 512 | // If the given cube has a larger volume than this one then it can never contain it 513 | if (a.volume > b.volume) { return false; } 514 | 515 | return (a.x >= b.x && a.y >= b.y && a.z >= b.z && a.frontX <= b.frontX && a.frontY <= b.frontY && a.top <= b.top); 516 | } 517 | 518 | /** 519 | * Determines whether the two Cubes intersect with each other. 520 | * This method checks the x, y, z, widthX, widthY, and height properties of the Cubes. 521 | * @method Cube.intersects 522 | * @param {Cube} a - The first Cube object. 523 | * @param {Cube} b - The second Cube object. 524 | * @return {boolean} A value of true if the specified object intersects with this Cube object; otherwise false. 525 | */ 526 | static intersects(a, b) { 527 | if (a.widthX <= 0 || a.widthY <= 0 || a.height <= 0 || b.widthX <= 0 || b.widthY <= 0 || b.height <= 0) { 528 | return false; 529 | } 530 | return !(a.frontX < b.x || a.frontY < b.y || a.x > b.frontX || a.y > b.frontY || a.z > b.top || a.top < b.z); 531 | } 532 | } 533 | 534 | export default Cube; 535 | -------------------------------------------------------------------------------- /src/physics/Body.js: -------------------------------------------------------------------------------- 1 | import Point3 from '../Point3'; 2 | import Cube from '../Cube'; 3 | 4 | const UP = 0; 5 | const DOWN = 1; 6 | const FORWARDX = 2; 7 | const FORWARDY = 3; 8 | const BACKWARDX = 4; 9 | const BACKWARDY = 5; 10 | export const TYPE = 'IsoPhysics'; 11 | 12 | /** 13 | * @class Isometric.Body 14 | * 15 | * @classdesc 16 | * The Physics Body is linked to a single IsoSprite. All physics operations should be performed against the body rather than 17 | * the IsoSprite itself. For example you can set the velocity, acceleration, bounce values etc all on the Body. 18 | */ 19 | export default class Body { 20 | /* 21 | * @constructor 22 | * @param {IsoSprite} sprite - The IsoSprite object this physics body belongs to. 23 | */ 24 | constructor(sprite) { 25 | /** 26 | * @property {IsoSprite} sprite - Reference to the parent IsoSprite. 27 | */ 28 | this.sprite = sprite; 29 | 30 | /** 31 | * @property {Phaser.Scene} scene - Local reference to scene. 32 | */ 33 | this.scene = sprite.scene; 34 | 35 | /** 36 | * @property {number} type - The type of physics system this body belongs to. 37 | */ 38 | this.type = TYPE; 39 | 40 | /** 41 | * @property {boolean} enable - A disabled body won't be checked for any form of collision or overlap or have its pre/post updates run. 42 | * @default 43 | */ 44 | this.enable = true; 45 | 46 | /** 47 | * @property {Phaser.Point} offset - The offset of the Physics Body from the IsoSprite x/y/z position. 48 | */ 49 | this.offset = new Point3(); 50 | 51 | /** 52 | * @property {Point3} position - The position of the physics body. 53 | * @readonly 54 | */ 55 | this.position = new Point3(sprite.isoX, sprite.isoY, sprite.isoZ); 56 | 57 | /** 58 | * @property {Phaser.Point} prev - The previous position of the physics body. 59 | * @readonly 60 | */ 61 | this.prev = new Point3(this.position.x, this.position.y, this.position.z); 62 | 63 | /** 64 | * @property {boolean} allowRotation - Allow this Body to be rotated? (via angularVelocity, etc) 65 | * @default 66 | */ 67 | this.allowRotation = true; 68 | 69 | /** 70 | * @property {number} rotation - The amount the Body is rotated. 71 | */ 72 | this.rotation = sprite.rotation; 73 | 74 | /** 75 | * @property {number} preRotation - The previous rotation of the physics body. 76 | * @readonly 77 | */ 78 | this.preRotation = sprite.rotation; 79 | 80 | /** 81 | * @property {number} sourceWidthX - The un-scaled original size. 82 | * @readonly 83 | */ 84 | this.sourceWidthX = sprite.width / sprite.scaleX; 85 | 86 | /** 87 | * @property {number} sourceWidthY - The un-scaled original size. 88 | * @readonly 89 | */ 90 | this.sourceWidthY = sprite.width / sprite.scaleX; 91 | 92 | /** 93 | * @property {number} sourceHeight - The un-scaled original size. 94 | * @readonly 95 | */ 96 | this.sourceHeight = sprite.height / sprite.scaleY; 97 | 98 | /** 99 | * @property {number} widthX - The calculated X width (breadth) of the physics body. 100 | */ 101 | this.widthX = Math.ceil(sprite.width * 0.5); 102 | 103 | /** 104 | * @property {number} widthY - The calculated Y width (depth) of the physics body. 105 | */ 106 | this.widthY = Math.ceil(sprite.width * 0.5); 107 | 108 | /** 109 | * @property {number} height - The calculated height of the physics body. 110 | */ 111 | this.height = sprite.height - Math.ceil(sprite.width * 0.5); 112 | 113 | /** 114 | * @property {number} halfWidthX - The calculated X width / 2 of the physics body. 115 | */ 116 | this.halfWidthX = Math.abs(this.widthX * 0.5); 117 | 118 | /** 119 | * @property {number} halfWidthX - The calculated X width / 2 of the physics body. 120 | */ 121 | this.halfWidthY = Math.abs(this.widthY * 0.5); 122 | 123 | /** 124 | * @property {number} halfHeight - The calculated height / 2 of the physics body. 125 | */ 126 | this.halfHeight = Math.abs(this.height * 0.5); 127 | 128 | /** 129 | * @property {Point3} center - The center coordinate of the physics body. 130 | */ 131 | this.center = new Point3(sprite.isoX + this.halfWidthX, sprite.isoY + this.halfWidthY, sprite.isoZ + this.halfHeight); 132 | 133 | /** 134 | * @property {Point3} velocity - The velocity in pixels per second sq. of the Body. 135 | */ 136 | this.velocity = new Point3(); 137 | 138 | /** 139 | * @property {Point3} newVelocity - New velocity. 140 | * @readonly 141 | */ 142 | this.newVelocity = new Point3(); 143 | 144 | /** 145 | * @property {Point3} deltaMax - The Sprite position is updated based on the delta x/y values. You can set a cap on those (both +-) using deltaMax. 146 | */ 147 | this.deltaMax = new Point3(); 148 | 149 | /** 150 | * @property {Point3} acceleration - The velocity in pixels per second sq. of the Body. 151 | */ 152 | this.acceleration = new Point3(); 153 | 154 | /** 155 | * @property {Point3} drag - The drag applied to the motion of the Body. 156 | */ 157 | this.drag = new Point3(); 158 | 159 | /** 160 | * @property {boolean} allowGravity - Allow this Body to be influenced by gravity? Either world or local. 161 | * @default 162 | */ 163 | this.allowGravity = true; 164 | 165 | /** 166 | * @property {Point3} gravity - A local gravity applied to this Body. If non-zero this over rides any world gravity, unless Body.allowGravity is set to false. 167 | */ 168 | this.gravity = new Point3(); 169 | 170 | /** 171 | * @property {Point3} bounce - The elasticitiy of the Body when colliding. bounce.x/y/z = 1 means full rebound, bounce.x/y/z = 0.5 means 50% rebound velocity. 172 | */ 173 | this.bounce = new Point3(); 174 | 175 | /** 176 | * @property {Point3} maxVelocity - The maximum velocity in pixels per second sq. that the Body can reach. 177 | * @default 178 | */ 179 | this.maxVelocity = new Point3(10000, 10000, 10000); 180 | 181 | /** 182 | * @property {number} angularVelocity - The angular velocity in pixels per second sq. of the Body. 183 | * @default 184 | */ 185 | this.angularVelocity = 0; 186 | 187 | /** 188 | * @property {number} angularAcceleration - The angular acceleration in pixels per second sq. of the Body. 189 | * @default 190 | */ 191 | this.angularAcceleration = 0; 192 | 193 | /** 194 | * @property {number} angularDrag - The angular drag applied to the rotation of the Body. 195 | * @default 196 | */ 197 | this.angularDrag = 0; 198 | 199 | /** 200 | * @property {number} maxAngular - The maximum angular velocity in pixels per second sq. that the Body can reach. 201 | * @default 202 | */ 203 | this.maxAngular = 1000; 204 | 205 | /** 206 | * @property {number} mass - The mass of the Body. 207 | * @default 208 | */ 209 | this.mass = 1; 210 | 211 | /** 212 | * @property {number} angle - The angle of the Body in radians as calculated by its velocity, rather than its visual angle. 213 | * @readonly 214 | */ 215 | this.angle = 0; 216 | 217 | /** 218 | * @property {number} speed - The speed of the Body as calculated by its velocity. 219 | * @readonly 220 | */ 221 | this.speed = 0; 222 | 223 | /** 224 | * @property {number} facing - A const reference to the direction the Body is traveling or facing. 225 | * @default 226 | */ 227 | this.facing = Phaser.NONE; 228 | 229 | /** 230 | * @property {boolean} immovable - An immovable Body will not receive any impacts from other bodies. 231 | * @default 232 | */ 233 | this.immovable = false; 234 | 235 | /** 236 | * If you have a Body that is being moved around the world via a tween or a Group motion, but its local x/y position never 237 | * actually changes, then you should set Body.moves = false. Otherwise it will most likely fly off the screen. 238 | * If you want the physics system to move the body around, then set moves to true. 239 | * @property {boolean} moves - Set to true to allow the Physics system to move this Body, other false to move it manually. 240 | * @default 241 | */ 242 | this.moves = true; 243 | 244 | /** 245 | * This flag allows you to disable the custom x separation that takes place by Physics.IsoArcade.separate. 246 | * Used in combination with your own collision processHandler you can create whatever type of collision response you need. 247 | * @property {boolean} customSeparateX - Use a custom separation system or the built-in one? 248 | * @default 249 | */ 250 | this.customSeparateX = false; 251 | 252 | /** 253 | * This flag allows you to disable the custom y separation that takes place by Physics.IsoArcade.separate. 254 | * Used in combination with your own collision processHandler you can create whatever type of collision response you need. 255 | * @property {boolean} customSeparateY - Use a custom separation system or the built-in one? 256 | * @default 257 | */ 258 | this.customSeparateY = false; 259 | 260 | /** 261 | * This flag allows you to disable the custom z separation that takes place by Physics.IsoArcade.separate. 262 | * Used in combination with your own collision processHandler you can create whatever type of collision response you need. 263 | * @property {boolean} customSeparateZ - Use a custom separation system or the built-in one? 264 | * @default 265 | */ 266 | this.customSeparateZ = false; 267 | 268 | /** 269 | * When this body collides with another, the amount of overlap is stored here. 270 | * @property {number} overlapX - The amount of horizontal overlap during the collision. 271 | */ 272 | this.overlapX = 0; 273 | 274 | /** 275 | * When this body collides with another, the amount of overlap is stored here. 276 | * @property {number} overlapY - The amount of vertical overlap during the collision. 277 | */ 278 | this.overlapY = 0; 279 | 280 | /** 281 | * When this body collides with another, the amount of overlap is stored here. 282 | * @property {number} overlapY - The amount of vertical overlap during the collision. 283 | */ 284 | this.overlapZ = 0; 285 | 286 | /** 287 | * If a body is overlapping with another body, but neither of them are moving (maybe they spawned on-top of each other?) this is set to true. 288 | * @property {boolean} embedded - Body embed value. 289 | */ 290 | this.embedded = false; 291 | 292 | /** 293 | * A Body can be set to collide against the World bounds automatically and rebound back into the World if this is set to true. Otherwise it will leave the World. 294 | * @property {boolean} collideWorldBounds - Should the Body collide with the World bounds? 295 | */ 296 | this.collideWorldBounds = false; 297 | 298 | /** 299 | * Set the checkCollision properties to control which directions collision is processed for this Body. 300 | * For example checkCollision.up = false means it won't collide when the collision happened while moving up. 301 | * @property {object} checkCollision - An object containing allowed collision. 302 | */ 303 | this.checkCollision = { 304 | none: false, 305 | any: true, 306 | up: true, 307 | down: true, 308 | frontX: true, 309 | frontY: true, 310 | backX: true, 311 | backY: true 312 | }; 313 | 314 | /** 315 | * This object is populated with boolean values when the Body collides with another. 316 | * touching.up = true means the collision happened to the top of this Body for example. 317 | * @property {object} touching - An object containing touching results. 318 | */ 319 | this.touching = { 320 | none: true, 321 | up: false, 322 | down: false, 323 | frontX: false, 324 | frontY: false, 325 | backX: false, 326 | backY: false 327 | }; 328 | 329 | /** 330 | * This object is populated with previous touching values from the bodies previous collision. 331 | * @property {object} wasTouching - An object containing previous touching results. 332 | */ 333 | this.wasTouching = { 334 | none: true, 335 | up: false, 336 | down: false, 337 | frontX: false, 338 | frontY: false, 339 | backX: false, 340 | backY: false 341 | }; 342 | 343 | /** 344 | * This object is populated with boolean values when the Body collides with the World bounds or a Tile. 345 | * For example if blocked.up is true then the Body cannot move up. 346 | * @property {object} blocked - An object containing on which faces this Body is blocked from moving, if any. 347 | */ 348 | this.blocked = { 349 | up: false, 350 | down: false, 351 | frontX: false, 352 | frontY: false, 353 | backX: false, 354 | backY: false 355 | }; 356 | 357 | /** 358 | * @property {number} phase - Is this Body in a update (1) or postUpdate (2) state? 359 | */ 360 | this.phase = 0; 361 | 362 | /** 363 | * @property {boolean} skipTree - If true and you collide this IsoSprite against a Group, it will disable the collision check from using an Octree. 364 | */ 365 | this.skipTree = false; 366 | 367 | /** 368 | * @property {boolean} _reset - Internal cache var. 369 | * @private 370 | */ 371 | this._reset = true; 372 | 373 | /** 374 | * @property {number} _sx - Internal cache var. 375 | * @private 376 | */ 377 | this._sx = sprite.scaleX; 378 | 379 | /** 380 | * @property {number} _sy - Internal cache var. 381 | * @private 382 | */ 383 | this._sy = sprite.scaleY; 384 | 385 | /** 386 | * @property {number} _dx - Internal cache var. 387 | * @private 388 | */ 389 | this._dx = 0; 390 | 391 | /** 392 | * @property {number} _dy - Internal cache var. 393 | * @private 394 | */ 395 | this._dy = 0; 396 | 397 | /** 398 | * @property {number} _dz - Internal cache var. 399 | * @private 400 | */ 401 | this._dz = 0; 402 | 403 | /** 404 | * @property {Array.} _corners - The 8 corners of the bounding cube. 405 | * @private 406 | */ 407 | this._corners = [new Point3(this.x, this.y, this.z), 408 | new Point3(this.x, this.y, this.z + this.height), 409 | new Point3(this.x, this.y + this.widthY, this.z), 410 | new Point3(this.x, this.y + this.widthY, this.z + this.height), 411 | new Point3(this.x + this.widthX, this.y, this.z), 412 | new Point3(this.x + this.widthX, this.y, this.z + this.height), 413 | new Point3(this.x + this.widthX, this.y + this.widthY, this.z), 414 | new Point3(this.x + this.widthX, this.y + this.widthY, this.z + this.height) 415 | ]; 416 | } 417 | 418 | /** 419 | * Internal method. 420 | * 421 | * @method Body#updateBounds 422 | * @protected 423 | */ 424 | updateBounds() { 425 | var asx = Math.abs(this.sprite.scaleX); 426 | var asy = Math.abs(this.sprite.scaleY); 427 | 428 | if (asx !== this._sx || asy !== this._sy) { 429 | this.widthX = Math.ceil(this.sprite.width * 0.5); 430 | this.widthY = Math.ceil(this.sprite.width * 0.5); 431 | this.height = Math.ceil(this.sprite.height - (this.sprite.width * 0.5)); 432 | this.halfWidthX = Math.floor(this.widthX * 0.5); 433 | this.halfWidthY = Math.floor(this.widthY * 0.5); 434 | this.halfHeight = Math.floor(this.height * 0.5); 435 | this._sx = asx; 436 | this._sy = asy; 437 | this.center.setTo(this.position.x + this.halfWidthX, this.position.y + this.halfWidthY, this.position.z + this.halfHeight); 438 | 439 | this._reset = true; 440 | } 441 | } 442 | 443 | /** 444 | * Internal method. 445 | * 446 | * @method Body#update 447 | * @protected 448 | */ 449 | update(time, delta) { 450 | if (!this.enable) { return; } 451 | 452 | this.phase = 1; 453 | 454 | // Store and reset collision flags 455 | this.wasTouching.none = this.touching.none; 456 | this.wasTouching.up = this.touching.up; 457 | this.wasTouching.down = this.touching.down; 458 | this.wasTouching.backX = this.touching.backX; 459 | this.wasTouching.backY = this.touching.backY; 460 | this.wasTouching.frontX = this.touching.frontX; 461 | this.wasTouching.frontY = this.touching.frontY; 462 | 463 | this.touching.none = true; 464 | this.touching.up = false; 465 | this.touching.down = false; 466 | this.touching.backX = false; 467 | this.touching.backY = false; 468 | this.touching.frontX = false; 469 | this.touching.frontY = false; 470 | 471 | this.blocked.up = false; 472 | this.blocked.down = false; 473 | this.blocked.frontY = false; 474 | this.blocked.frontX = false; 475 | this.blocked.backY = false; 476 | this.blocked.backX = false; 477 | 478 | this.embedded = false; 479 | 480 | this.updateBounds(); 481 | 482 | // Working out how to incorporate anchors into this was... fun. 483 | this.position.x = this.sprite.isoX + ((this.widthX * -this.sprite.originX) + this.widthX * 0.5) + this.offset.x; 484 | this.position.y = this.sprite.isoY + ((this.widthY * this.sprite.originX) - this.widthY * 0.5) + this.offset.y; 485 | this.position.z = this.sprite.isoZ - (Math.abs(this.sprite.height) * (1 - this.sprite.originY)) + (Math.abs(this.sprite.width * 0.5)) + this.offset.z; 486 | 487 | 488 | this.rotation = this.sprite.angle; 489 | 490 | this.preRotation = this.rotation; 491 | 492 | if (this._reset || this.sprite.fresh === true) { 493 | this.prev.x = this.position.x; 494 | this.prev.y = this.position.y; 495 | this.prev.z = this.position.z; 496 | } 497 | 498 | if (this.moves) { 499 | const pluginKey = this.scene.sys.settings.map.isoPhysics; 500 | const { world } = this.scene[pluginKey]; 501 | delta /= 1000; 502 | world.updateMotion(this, delta); 503 | 504 | this.newVelocity.set(this.velocity.x * delta, this.velocity.y * delta, this.velocity.z * delta); 505 | 506 | this.position.x += this.newVelocity.x; 507 | this.position.y += this.newVelocity.y; 508 | this.position.z += this.newVelocity.z; 509 | 510 | if (this.position.x !== this.prev.x || this.position.y !== this.prev.y || this.position.z !== this.prev.z) { 511 | this.speed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y + this.velocity.z * this.velocity.z); 512 | this.angle = Math.atan2(this.velocity.y, this.velocity.x); 513 | } 514 | 515 | // Now the State update will throw collision checks at the Body 516 | // And finally we'll integrate the new position back to the Sprite in postUpdate 517 | 518 | if (this.collideWorldBounds) { 519 | this.checkWorldBounds(); 520 | } 521 | 522 | if (this.sprite.outOfBoundsKill && !world.bounds.intersects(this.sprite.isoBounds)){ 523 | this.sprite.kill(); 524 | } 525 | } 526 | 527 | this._dx = this.deltaX(); 528 | this._dy = this.deltaY(); 529 | this._dz = this.deltaZ(); 530 | 531 | this._reset = false; 532 | } 533 | 534 | /** 535 | * Internal method. 536 | * 537 | * @method Body#postUpdate 538 | * @protected 539 | */ 540 | postUpdate() { 541 | // Only allow postUpdate to be called once per frame 542 | if (!this.enable || this.phase === 2) { return; } 543 | 544 | this.phase = 2; 545 | 546 | // stops sprites flying off if isoPosition is changed during update 547 | if (this._reset) { 548 | this.prev.x = this.position.x; 549 | this.prev.y = this.position.y; 550 | this.prev.z = this.position.z; 551 | } 552 | 553 | if (this.deltaAbsX() >= this.deltaAbsY() && this.deltaAbsX() >= this.deltaAbsZ()){ 554 | if (this.deltaX() < 0) { 555 | this.facing = BACKWARDX; 556 | } else if (this.deltaX() > 0) { 557 | this.facing = FORWARDX; 558 | } 559 | } else if (this.deltaAbsY() >= this.deltaAbsX() && this.deltaAbsY() >= this.deltaAbsZ()){ 560 | if (this.deltaY() < 0) { 561 | this.facing = BACKWARDY; 562 | } else if (this.deltaY() > 0) { 563 | this.facing = FORWARDY; 564 | } 565 | } else { 566 | if (this.deltaZ() < 0) { 567 | this.facing = DOWN; 568 | } else if (this.deltaZ() > 0) { 569 | this.facing = UP; 570 | } 571 | } 572 | 573 | if (this.moves) { 574 | this._dx = this.deltaX(); 575 | this._dy = this.deltaY(); 576 | this._dz = this.deltaZ(); 577 | 578 | if (this.deltaMax.x !== 0 && this._dx !== 0) { 579 | if (this._dx < 0 && this._dx < -this.deltaMax.x) { 580 | this._dx = -this.deltaMax.x; 581 | } else if (this._dx > 0 && this._dx > this.deltaMax.x) { 582 | this._dx = this.deltaMax.x; 583 | } 584 | } 585 | 586 | if (this.deltaMax.y !== 0 && this._dy !== 0) { 587 | if (this._dy < 0 && this._dy < -this.deltaMax.y) { 588 | this._dy = -this.deltaMax.y; 589 | } else if (this._dy > 0 && this._dy > this.deltaMax.y) { 590 | this._dy = this.deltaMax.y; 591 | } 592 | } 593 | 594 | if (this.deltaMax.z !== 0 && this._dz !== 0) { 595 | if (this._dz < 0 && this._dz < -this.deltaMax.z) { 596 | this._dz = -this.deltaMax.z; 597 | } else if (this._dz > 0 && this._dz > this.deltaMax.z) { 598 | this._dz = this.deltaMax.z; 599 | } 600 | } 601 | 602 | this.sprite.isoX += this._dx; 603 | this.sprite.isoY += this._dy; 604 | this.sprite.isoZ += this._dz; 605 | } 606 | 607 | this.center.setTo(this.position.x + this.halfWidthX, this.position.y + this.halfWidthY, this.position.z + this.halfHeight); 608 | 609 | if (this.allowRotation) { 610 | this.sprite.angle += this.deltaR(); 611 | } 612 | 613 | this.prev.x = this.position.x; 614 | this.prev.y = this.position.y; 615 | this.prev.z = this.position.z; 616 | 617 | this._reset = false; 618 | } 619 | 620 | /** 621 | * Removes this body's reference to its parent sprite, freeing it up for gc. 622 | * 623 | * @method Body#destroy 624 | */ 625 | destroy() { 626 | this.sprite = null; 627 | } 628 | 629 | /** 630 | * Internal method. 631 | * 632 | * @method Body#checkWorldBounds 633 | * @protected 634 | */ 635 | checkWorldBounds() { 636 | const pluginKey = this.scene.sys.settings.map.isoPhysics; 637 | const { world } = this.scene[pluginKey]; 638 | 639 | if (this.position.x < world.bounds.x && world.checkCollision.backX) { 640 | this.position.x = world.bounds.x; 641 | this.velocity.x *= -this.bounce.x; 642 | this.blocked.backX = true; 643 | } else if (this.frontX > world.bounds.frontX && world.checkCollision.frontX) { 644 | this.position.x = world.bounds.frontX - this.widthX; 645 | this.velocity.x *= -this.bounce.x; 646 | this.blocked.frontX = true; 647 | } 648 | 649 | if (this.position.y < world.bounds.y && world.checkCollision.backY) { 650 | this.position.y = world.bounds.y; 651 | this.velocity.y *= -this.bounce.y; 652 | this.blocked.backY = true; 653 | } else if (this.frontY > world.bounds.frontY && world.checkCollision.frontY) { 654 | this.position.y = world.bounds.frontY - this.widthY; 655 | this.velocity.y *= -this.bounce.y; 656 | this.blocked.frontY = true; 657 | } 658 | 659 | if (this.position.z < world.bounds.z && world.checkCollision.down) { 660 | this.position.z = world.bounds.z; 661 | this.velocity.z *= -this.bounce.z; 662 | this.blocked.down = true; 663 | } else if (this.top > world.bounds.top && world.checkCollision.up) { 664 | this.position.z = world.bounds.top - this.height; 665 | this.velocity.z *= -this.bounce.z; 666 | this.blocked.up = true; 667 | } 668 | } 669 | 670 | /** 671 | * You can modify the size of the physics Body to be any dimension you need. 672 | * So it could be smaller or larger than the parent Sprite. You can also control the x, y and z offset, which 673 | * is the position of the Body relative to the center of the Sprite. 674 | * 675 | * @method Body#setSize 676 | * @param {number} widthX - The X width (breadth) of the Body. 677 | * @param {number} widthY - The Y width (depth) of the Body. 678 | * @param {number} height - The height of the Body. 679 | * @param {number} [offsetX] - The X offset of the Body from the Sprite position. 680 | * @param {number} [offsetY] - The Y offset of the Body from the Sprite position. 681 | * @param {number} [offsetZ] - The Z offset of the Body from the Sprite position. 682 | */ 683 | setSize(widthX, widthY, height, offsetX, offsetY, offsetZ) { 684 | if (typeof offsetX === 'undefined') { 685 | offsetX = this.offset.x; 686 | } 687 | if (typeof offsetY === 'undefined') { 688 | offsetY = this.offset.y; 689 | } 690 | if (typeof offsetZ === 'undefined') { 691 | offsetZ = this.offset.z; 692 | } 693 | 694 | this.sourceWidthX = widthX; 695 | this.sourceWidthY = widthY; 696 | this.sourceHeight = height; 697 | this.widthX = (this.sourceWidthX) * this._sx; 698 | this.widthY = (this.sourceWidthY) * this._sx; 699 | this.height = (this.sourceHeight) * this._sy; 700 | this.halfWidthX = Math.floor(this.widthX * 0.5); 701 | this.halfWidthY = Math.floor(this.widthY * 0.5); 702 | this.halfHeight = Math.floor(this.height * 0.5); 703 | this.offset.setTo(offsetX, offsetY, offsetZ); 704 | 705 | this.center.setTo(this.position.x + this.halfWidthX, this.position.y + this.halfWidthY, this.position.z + this.halfHeight); 706 | } 707 | 708 | /** 709 | * Resets all Body values (velocity, acceleration, rotation, etc) 710 | * 711 | * @method Body#reset 712 | * @param {number} x - The new x position of the Body. 713 | * @param {number} y - The new y position of the Body. 714 | * @param {number} z - The new z position of the Body. 715 | */ 716 | reset(x, y, z) { 717 | this.velocity.set(0); 718 | this.acceleration.set(0); 719 | 720 | this.angularVelocity = 0; 721 | this.angularAcceleration = 0; 722 | 723 | this.position.x = x + ((this.widthX * -this.sprite.anchor.x) + this.widthX * 0.5) + this.offset.x; 724 | this.position.y = y + ((this.widthY * this.sprite.anchor.x) - this.widthY * 0.5) + this.offset.y; 725 | this.position.z = z - (Math.abs(this.sprite.height) * (1 - this.sprite.anchor.y)) + (Math.abs(this.sprite.width * 0.5)) + this.offset.z; 726 | 727 | this.prev.x = this.position.x; 728 | this.prev.y = this.position.y; 729 | this.prev.z = this.position.z; 730 | 731 | this.rotation = this.sprite.angle; 732 | this.preRotation = this.rotation; 733 | 734 | this._sx = this.sprite.scaleX; 735 | this._sy = this.sprite.scaleY; 736 | 737 | this.center.setTo(this.position.x + this.halfWidthX, this.position.y + this.halfWidthY, this.position.z + this.halfHeight); 738 | 739 | this.sprite._isoPositionChanged = true; 740 | } 741 | 742 | /** 743 | * Tests if a world point lies within this Body. 744 | * 745 | * @method Body#hitTest 746 | * @param {number} x - The world x coordinate to test. 747 | * @param {number} y - The world y coordinate to test. 748 | * @param {number} z - The world z coordinate to test. 749 | * @return {boolean} True if the given coordinates are inside this Body, otherwise false. 750 | */ 751 | hitTest(x, y, z) { 752 | return Cube.contains(this, x, y, z); 753 | } 754 | 755 | /** 756 | * Returns true if the bottom of this Body is in contact with either the world bounds. 757 | * 758 | * @method Body#onFloor 759 | * @return {boolean} True if in contact with either the world bounds. 760 | */ 761 | onFloor() { 762 | return this.blocked.down; 763 | } 764 | 765 | /** 766 | * Returns true if either side of this Body is in contact with either the world bounds. 767 | * 768 | * @method Body#onWall 769 | * @return {boolean} True if in contact with world bounds. 770 | */ 771 | onWall() { 772 | return (this.blocked.frontX || this.blocked.frontY || this.blocked.backX || this.blocked.backY); 773 | } 774 | 775 | /** 776 | * Returns the absolute delta x value. 777 | * 778 | * @method Body#deltaAbsX 779 | * @return {number} The absolute delta value. 780 | */ 781 | deltaAbsX() { 782 | return (this.deltaX() > 0 ? this.deltaX() : -this.deltaX()); 783 | } 784 | 785 | /** 786 | * Returns the absolute delta y value. 787 | * 788 | * @method Body#deltaAbsY 789 | * @return {number} The absolute delta value. 790 | */ 791 | deltaAbsY() { 792 | return (this.deltaY() > 0 ? this.deltaY() : -this.deltaY()); 793 | } 794 | 795 | /** 796 | * Returns the absolute delta z value. 797 | * 798 | * @method Body#deltaAbsZ 799 | * @return {number} The absolute delta value. 800 | */ 801 | deltaAbsZ() { 802 | return (this.deltaZ() > 0 ? this.deltaZ() : -this.deltaZ()); 803 | } 804 | 805 | /** 806 | * Returns the delta x value. The difference between Body.x now and in the previous step. 807 | * 808 | * @method Body#deltaX 809 | * @return {number} The delta value. Positive if the motion was to the right, negative if to the left. 810 | */ 811 | deltaX() { 812 | return this.position.x - this.prev.x; 813 | } 814 | 815 | /** 816 | * Returns the delta y value. The difference between Body.y now and in the previous step. 817 | * 818 | * @method Body#deltaY 819 | * @return {number} The delta value. Positive if the motion was downwards, negative if upwards. 820 | */ 821 | deltaY() { 822 | return this.position.y - this.prev.y; 823 | } 824 | 825 | /** 826 | * Returns the delta z value. The difference between Body.z now and in the previous step. 827 | * 828 | * @method Body#deltaZ 829 | * @return {number} The delta value. Positive if the motion was downwards, negative if upwards. 830 | */ 831 | deltaZ() { 832 | return this.position.z - this.prev.z; 833 | } 834 | 835 | /** 836 | * Returns the delta r value. The difference between Body.rotation now and in the previous step. 837 | * 838 | * @method Body#deltaR 839 | * @return {number} The delta value. Positive if the motion was clockwise, negative if anti-clockwise. 840 | */ 841 | deltaR() { 842 | return this.rotation - this.preRotation; 843 | } 844 | 845 | /** 846 | * Returns the 8 corners that make up the body's bounding cube. 847 | * 848 | * @method Body#getCorners 849 | * @return {Array.} An array of Point3 values specifying each corner co-ordinate. 850 | */ 851 | getCorners() { 852 | this._corners[0].setTo(this.x, this.y, this.z); 853 | this._corners[1].setTo(this.x, this.y, this.z + this.height); 854 | this._corners[2].setTo(this.x, this.y + this.widthY, this.z); 855 | this._corners[3].setTo(this.x, this.y + this.widthY, this.z + this.height); 856 | this._corners[4].setTo(this.x + this.widthX, this.y, this.z); 857 | this._corners[5].setTo(this.x + this.widthX, this.y, this.z + this.height); 858 | this._corners[6].setTo(this.x + this.widthX, this.y + this.widthY, this.z); 859 | this._corners[7].setTo(this.x + this.widthX, this.y + this.widthY, this.z + this.height); 860 | 861 | return this._corners; 862 | } 863 | 864 | /** 865 | * @name Body#top 866 | * @property {number} bottom - The top value of this Body (same as Body.z + Body.height) 867 | * @readonly 868 | */ 869 | get top() { 870 | return this.position.z + this.height; 871 | } 872 | 873 | /** 874 | * @name Body#frontX 875 | * @property {number} right - The front X value of this Body (same as Body.x + Body.widthX) 876 | * @readonly 877 | */ 878 | 879 | get frontX() { 880 | return this.position.x + this.widthX; 881 | } 882 | 883 | /** 884 | * @name Body#right 885 | * @property {number} right - The front X value of this Body (same as Body.x + Body.widthX) - alias used for QuadTree 886 | * @readonly 887 | */ 888 | 889 | get right() { 890 | return this.position.x + this.widthX; 891 | } 892 | 893 | /** 894 | * @name Body#frontY 895 | * @property {number} right - The front Y value of this Body (same as Body.y + Body.widthY) 896 | * @readonly 897 | */ 898 | 899 | get frontY() { 900 | return this.position.y + this.widthY; 901 | } 902 | 903 | /** 904 | * @name Body#bottom 905 | * @property {number} right - The front Y value of this Body (same as Body.y + Body.widthY) - alias used for QuadTree 906 | * @readonly 907 | */ 908 | 909 | get bottom() { 910 | return this.position.y + this.widthY; 911 | } 912 | 913 | /** 914 | * @name Body#x 915 | * @property {number} x - The x position. 916 | */ 917 | 918 | get x() { 919 | return this.position.x; 920 | } 921 | 922 | set x(value) { 923 | this.position.x = value; 924 | } 925 | 926 | /** 927 | * @name Body#y 928 | * @property {number} y - The y position. 929 | */ 930 | 931 | get y() { 932 | return this.position.y; 933 | } 934 | 935 | set y(value) { 936 | this.position.y = value; 937 | } 938 | 939 | /** 940 | * @name Body#z 941 | * @property {number} z - The z position. 942 | */ 943 | 944 | get z() { 945 | return this.position.z; 946 | } 947 | 948 | set z(value) { 949 | this.position.z = value; 950 | } 951 | 952 | debugRender(context, color = 'rgba(0,255,0,0.4)', filled = true) { 953 | var points = []; 954 | var corners = this.getCorners(); 955 | 956 | var posX = -this.scene.cameras.main.x; 957 | var posY = -this.scene.cameras.main.y; 958 | 959 | const pluginKey = this.scene.sys.settings.map.isoPlugin; 960 | const projector = this.scene[pluginKey].projector; 961 | 962 | if (filled) { 963 | points = [corners[1], corners[3], corners[2], corners[6], corners[4], corners[5], corners[1]]; 964 | 965 | points = points.map(p => { 966 | var newPos = projector.project(p); 967 | newPos.x += posX; 968 | newPos.y += posY; 969 | return newPos; 970 | }); 971 | 972 | context.beginPath(); 973 | context.fillStyle = color; 974 | context.moveTo(points[0].x, points[0].y); 975 | 976 | for (var i = 1; i < points.length; i++) { 977 | context.lineTo(points[i].x, points[i].y); 978 | } 979 | 980 | context.fill(); 981 | } else { 982 | points = corners.slice(0, corners.length); 983 | points = points.map(p => { 984 | var newPos = projector.project(p); 985 | newPos.x += posX; 986 | newPos.y += posY; 987 | return newPos; 988 | }); 989 | 990 | context.moveTo(points[0].x, points[0].y); 991 | context.beginPath(); 992 | context.strokeStyle = color; 993 | 994 | context.lineTo(points[1].x, points[1].y); 995 | context.lineTo(points[3].x, points[3].y); 996 | context.lineTo(points[2].x, points[2].y); 997 | context.lineTo(points[6].x, points[6].y); 998 | context.lineTo(points[4].x, points[4].y); 999 | context.lineTo(points[5].x, points[5].y); 1000 | context.lineTo(points[1].x, points[1].y); 1001 | context.lineTo(points[0].x, points[0].y); 1002 | context.lineTo(points[4].x, points[4].y); 1003 | context.moveTo(points[0].x, points[0].y); 1004 | context.lineTo(points[2].x, points[2].y); 1005 | context.moveTo(points[3].x, points[3].y); 1006 | context.lineTo(points[7].x, points[7].y); 1007 | context.lineTo(points[6].x, points[6].y); 1008 | context.moveTo(points[7].x, points[7].y); 1009 | context.lineTo(points[5].x, points[5].y); 1010 | context.stroke(); 1011 | context.closePath(); 1012 | } 1013 | } 1014 | } 1015 | 1016 | /** 1017 | * Render IsoSprite Body Physics Data as text. 1018 | * 1019 | * @method Body#renderBodyInfo 1020 | * @param {Body} body - The Body to render the info of. 1021 | * @param {number} x - X position of the debug info to be rendered. 1022 | * @param {number} y - Y position of the debug info to be rendered. 1023 | * @param {string} [color='rgb(255,255,255)'] - color of the debug info to be rendered. (format is css color string). 1024 | */ 1025 | // Body.renderBodyInfo = function (debug, body) { 1026 | // 1027 | // debug.line('x: ' + body.x.toFixed(2), 'y: ' + body.y.toFixed(2), 'z: ' + body.z.toFixed(2), 'widthX: ' + body.widthX, 'widthY: ' + body.widthY, 'height: ' + body.height); 1028 | // debug.line('velocity x: ' + body.velocity.x.toFixed(2), 'y: ' + body.velocity.y.toFixed(2), 'z: ' + body.velocity.z.toFixed(2), 'deltaX: ' + body._dx.toFixed(2), 'deltaY: ' + body._dy.toFixed(2), 'deltaZ: ' + body._dz.toFixed(2)); 1029 | // debug.line('acceleration x: ' + body.acceleration.x.toFixed(2), 'y: ' + body.acceleration.y.toFixed(2), 'z: ' + body.acceleration.z.toFixed(2), 'speed: ' + body.speed.toFixed(2), 'angle: ' + body.angle.toFixed(2)); 1030 | // debug.line('gravity x: ' + body.gravity.x, 'y: ' + body.gravity.y, 'z: ' + body.gravity.z); 1031 | // debug.line('bounce x: ' + body.bounce.x.toFixed(2), 'y: ' + body.bounce.y.toFixed(2), 'z: ' + body.bounce.z.toFixed(2)); 1032 | // debug.line('touching: ', 'frontX: ' + (body.touching.frontX ? 1 : 0) + ' frontY: ' + (body.touching.frontY ? 1 : 0) + ' backX: ' + (body.touching.backX ? 1 : 0) + ' backY: ' + (body.touching.backY ? 1 : 0) + ' up: ' + (body.touching.up ? 1 : 0) + ' down: ' + (body.touching.down ? 1 : 0)); 1033 | // debug.line('blocked: ', 'frontX: ' + (body.blocked.frontX ? 1 : 0) + ' frontY: ' + (body.blocked.frontY ? 1 : 0) + ' backX: ' + (body.blocked.backX ? 1 : 0) + ' backY: ' + (body.blocked.backY ? 1 : 0) + ' up: ' + (body.blocked.up ? 1 : 0) + ' down: ' + (body.blocked.down ? 1 : 0)); 1034 | // 1035 | // }; 1036 | 1037 | // Phaser.Utils.Debug.prototype.body = (function (_super) { 1038 | // 1039 | // return function (sprite, color, filled, depth) { 1040 | // if (sprite.body && sprite.body.type === ISOPHYSICS) { 1041 | // this.start(); 1042 | // Body.render(this.context, sprite.body, color, filled); 1043 | // if (depth) { 1044 | // this.text(sprite.depth.toFixed(2), sprite.x, sprite.y, color, '12px Courier'); 1045 | // } 1046 | // this.stop(); 1047 | // } 1048 | // 1049 | // return _super.call(this, sprite, color, filled); 1050 | // }; 1051 | // 1052 | // })(Phaser.Utils.Debug.prototype.body); 1053 | 1054 | // Phaser.Utils.Debug.prototype.bodyInfo = (function (_super) { 1055 | // 1056 | // return function (sprite, x, y, color) { 1057 | // if (sprite.body && sprite.body.type === ISOPHYSICS) { 1058 | // this.start(x, y, color, 210); 1059 | // Body.renderBodyInfo(this, sprite.body); 1060 | // this.stop(); 1061 | // } 1062 | // 1063 | // return _super.call(this, sprite, x, y, color); 1064 | // }; 1065 | // 1066 | // })(Phaser.Utils.Debug.prototype.bodyInfo); 1067 | -------------------------------------------------------------------------------- /src/physics/World.js: -------------------------------------------------------------------------------- 1 | import Body from './Body'; 2 | import Point3 from '../Point3'; 3 | import Cube from '../Cube'; 4 | import Octree from '../Octree'; 5 | 6 | import { ISOSPRITE } from '../IsoSprite'; 7 | 8 | const { GameObjects, Structs } = Phaser; 9 | 10 | export default class World { 11 | constructor(scene) { 12 | 13 | /** 14 | * Bodies 15 | * 16 | * @name World#bodies 17 | * @type {Phaser.Structs.Set.} 18 | * @since 3.0.0 19 | */ 20 | this.bodies = new Structs.Set(); 21 | 22 | /** 23 | * @property {Cube} bounds - The bounds inside of which the physics world exists. Defaults to match the world bounds relatively closely given the isometric projection. 24 | */ 25 | const { width, height } = scene.sys.game.config; 26 | this.bounds = new Cube(0, 0, 0, width * 0.5, width * 0.5, height); 27 | 28 | /** 29 | * @property {Point3} gravity - The World gravity setting. Defaults to x: 0, y: 0, z: 0 or no gravity. 30 | */ 31 | this.gravity = new Point3(); 32 | 33 | /** 34 | * Set the checkCollision properties to control for which bounds collision is processed. 35 | * For example checkCollision.down = false means Bodies cannot collide with the World.bounds.bottom. 36 | * @property {object} checkCollision - An object containing allowed collision flags. 37 | */ 38 | this.checkCollision = { 39 | up: true, 40 | down: true, 41 | frontX: true, 42 | frontY: true, 43 | backX: true, 44 | backY: true 45 | }; 46 | 47 | /** 48 | * @property {number} OVERLAP_BIAS - A value added to the delta values during collision checks. 49 | */ 50 | this.OVERLAP_BIAS = 4; 51 | 52 | /** 53 | * @property {boolean} forceX - If true World.separate will always separate on the X and Y axes before Z. Otherwise it will check gravity totals first. 54 | */ 55 | this.forceXY = false; 56 | 57 | /** 58 | * @property {boolean} skipTree - If true an Octree will never be used for any collision. Handy for tightly packed games. See also Body.skipTree. 59 | */ 60 | this.skipTree = false; 61 | 62 | /** 63 | * @property {number} maxObjects - Used by the Octree to set the maximum number of objects per quad. 64 | */ 65 | this.maxObjects = 10; 66 | 67 | /** 68 | * @property {number} maxLevels - Used by the Octree to set the maximum number of iteration levels. 69 | */ 70 | this.maxLevels = 4; 71 | 72 | /** 73 | * @property {Octree} octree - The world Octree. 74 | */ 75 | this.octree = new Octree( 76 | this.bounds.x, 77 | this.bounds.y, 78 | this.bounds.z, 79 | this.bounds.widthX, 80 | this.bounds.widthY, 81 | this.bounds.height, 82 | this.maxObjects, 83 | this.maxLevels 84 | ); 85 | 86 | // Avoid gc spikes by caching these values for re-use 87 | 88 | /** 89 | * @property {number} _overlap - Internal cache var. 90 | * @private 91 | */ 92 | this._overlap = 0; 93 | 94 | /** 95 | * @property {number} _maxOverlap - Internal cache var. 96 | * @private 97 | */ 98 | this._maxOverlap = 0; 99 | 100 | /** 101 | * @property {number} _velocity1 - Internal cache var. 102 | * @private 103 | */ 104 | this._velocity1 = 0; 105 | 106 | /** 107 | * @property {number} _velocity2 - Internal cache var. 108 | * @private 109 | */ 110 | this._velocity2 = 0; 111 | 112 | /** 113 | * @property {number} _newVelocity1 - Internal cache var. 114 | * @private 115 | */ 116 | this._newVelocity1 = 0; 117 | 118 | /** 119 | * @property {number} _newVelocity2 - Internal cache var. 120 | * @private 121 | */ 122 | this._newVelocity2 = 0; 123 | 124 | /** 125 | * @property {number} _average - Internal cache var. 126 | * @private 127 | */ 128 | this._average = 0; 129 | 130 | /** 131 | * @property {Array} _mapData - Internal cache var. 132 | * @private 133 | */ 134 | this._mapData = []; 135 | 136 | /** 137 | * @property {boolean} _result - Internal cache var. 138 | * @private 139 | */ 140 | this._result = false; 141 | 142 | /** 143 | * @property {number} _total - Internal cache var. 144 | * @private 145 | */ 146 | this._total = 0; 147 | 148 | /** 149 | * @property {number} _angle - Internal cache var. 150 | * @private 151 | */ 152 | this._angle = 0; 153 | 154 | /** 155 | * @property {number} _dx - Internal cache var. 156 | * @private 157 | */ 158 | this._dx = 0; 159 | 160 | /** 161 | * @property {number} _dy - Internal cache var. 162 | * @private 163 | */ 164 | this._dy = 0; 165 | 166 | /** 167 | * @property {number} _dz - Internal cache var. 168 | * @private 169 | */ 170 | this._dz = 0; 171 | } 172 | 173 | /** 174 | * This will create an IsoPhysics Physics body on the given game object or array of game objects. 175 | * A game object can only have 1 physics body active at any one time, and it can't be changed until the object is destroyed. 176 | * 177 | * @method IsoPhysics#enable 178 | * @param {object|array|Phaser.Group} object - The game object to create the physics body on. Can also be an array or Group of objects, a body will be created on every child that has a `body` property. 179 | * @param {boolean} [children=true] - Should a body be created on all children of this object? If true it will recurse down the display list as far as it can go. 180 | */ 181 | enable(object, children = true) { 182 | var i = 1; 183 | 184 | if (Array.isArray(object)) { 185 | i = object.length; 186 | 187 | while (i--) { 188 | if (object[i] instanceof GameObjects.Group) { 189 | // If it's a Group then we do it on the children regardless 190 | this.enable(object[i].children, children); 191 | } else { 192 | this.enableBody(object[i]); 193 | 194 | if (children && object[i].hasOwnProperty('children') && object[i].children.length > 0) { 195 | this.enable(object[i], true); 196 | } 197 | } 198 | } 199 | } else if (object instanceof GameObjects.Group) { 200 | // If it's a Group then we do it on the children regardless 201 | this.enable(object.children, children); 202 | } else { 203 | this.enableBody(object); 204 | 205 | if (children && object.hasOwnProperty('children') && object.children.length > 0) { 206 | this.enable(object.children, true); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Creates an IsoPhysics Physics body on the given game object. 213 | * A game object can only have 1 physics body active at any one time, and it can't be changed until the body is nulled. 214 | * 215 | * @method IsoPhysics#enableBody 216 | * @param {object} object - The game object to create the physics body on. A body will only be created if this object has a null `body` property. 217 | */ 218 | enableBody(object) { 219 | if (object.body === null) { 220 | object.body = new Body(object); 221 | this.bodies.set(object.body); 222 | } 223 | 224 | return object; 225 | } 226 | 227 | /** 228 | * Updates the size of this physics world. 229 | * 230 | * @method IsoPhysics#setBounds 231 | * @param {number} x - Bottom rear most corner of the world. 232 | * @param {number} y - Bottom rear most corner of the world. 233 | * @param {number} z - Bottom rear most corner of the world. 234 | * @param {number} widthX - New X width (breadth) of the world. Can never be smaller than the Game.width. 235 | * @param {number} widthY - New Y width (depth) of the world. Can never be smaller than the Game.width. 236 | * @param {number} height - New height of the world. Can never be smaller than the Game.height. 237 | */ 238 | setBounds(x, y, z, widthX, widthY, height) { 239 | this.bounds.setTo(x, y, z, widthX, widthY, height); 240 | } 241 | 242 | /** 243 | * Updates the size of this physics world to match the size of the game world. 244 | * 245 | * @method IsoPhysics#setBoundsToWorld 246 | */ 247 | setBoundsToWorld() { 248 | const { width, height } = this.scene.sys.game.config; 249 | this.bounds.setTo(0, 0, 0, width * 0.5, width * 0.5, height); 250 | } 251 | 252 | /** 253 | * A tween-like function that takes a starting velocity and some other factors and returns an altered velocity. 254 | * Based on a function in Flixel by @ADAMATOMIC 255 | * 256 | * @method World#computeVelocity 257 | * @param {number} axis - 0 for nothing, 1 for X-axis, 2 for Y-axis, 3 for vertical (Z-axis). 258 | * @param {Body} body - The Body object to be updated. 259 | * @param {number} velocity - Any component of velocity (e.g. 20). 260 | * @param {number} acceleration - Rate at which the velocity is changing. 261 | * @param {number} drag - Really kind of a deceleration, this is how much the velocity changes if Acceleration is not set. 262 | * @param {number} [max=10000] - An absolute value cap for the velocity. 263 | * @return {number} The altered Velocity value. 264 | */ 265 | computeVelocity(axis, body, velocity, acceleration, drag, max, delta) { 266 | max = max || 10000; 267 | 268 | if (axis === 1 && body.allowGravity) { 269 | velocity += (this.gravity.x + body.gravity.x) * delta; 270 | } else if (axis === 2 && body.allowGravity) { 271 | velocity += (this.gravity.y + body.gravity.y) * delta; 272 | } else if (axis === 3 && body.allowGravity) { 273 | velocity += (this.gravity.z + body.gravity.z) * delta; 274 | } 275 | 276 | if (acceleration) { 277 | velocity += acceleration * delta; 278 | } else if (drag) { 279 | this._drag = drag * delta; 280 | 281 | if (velocity - this._drag > 0) { 282 | velocity -= this._drag; 283 | } else if (velocity + this._drag < 0) { 284 | velocity += this._drag; 285 | } else { 286 | velocity = 0; 287 | } 288 | } 289 | 290 | if (velocity > max) { 291 | velocity = max; 292 | } else if (velocity < -max) { 293 | velocity = -max; 294 | } 295 | 296 | return velocity; 297 | } 298 | 299 | /** 300 | * The core separation function to separate two physics bodies. 301 | * 302 | * @private 303 | * @method IsoPhysics#separate 304 | * @param {Body} body1 - The first Body object to separate. 305 | * @param {Body} body2 - The second Body object to separate. 306 | * @param {function} [processCallback=null] - A callback function that lets you perform additional checks against the two objects if they overlap. If this function is set then the sprites will only be collided if it returns true. 307 | * @param {object} [callbackContext] - The context in which to run the process callback. 308 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 309 | * @return {boolean} Returns true if the bodies collided, otherwise false. 310 | */ 311 | separate(body1, body2, processCallback, callbackContext, overlapOnly) { 312 | if (!body1.enable || !body2.enable || !this.intersects(body1, body2)) { 313 | return false; 314 | } 315 | 316 | // They overlap. Is there a custom process callback? If it returns true then we can carry on, otherwise we should abort. 317 | if (processCallback && processCallback.call(callbackContext, body1.sprite, body2.sprite) === false) { 318 | return false; 319 | } 320 | 321 | if (overlapOnly) { 322 | // We already know they intersect from the check above, and we don't need separation, so ... 323 | return true; 324 | } 325 | 326 | // Do we separate on X and Y first? 327 | // If we weren't having to carry around so much legacy baggage with us, we could do this properly. But alas ... 328 | if (this.forceXY || Math.abs(this.gravity.z + body1.gravity.z) < Math.abs(this.gravity.x + body1.gravity.x) || Math.abs(this.gravity.z + body1.gravity.z) < Math.abs(this.gravity.y + body1.gravity.y)) { 329 | this._result = (this.separateX(body1, body2, overlapOnly) || this.separateY(body1, body2, overlapOnly) || this.separateZ(body1, body2, overlapOnly)); 330 | } else { 331 | this._result = (this.separateZ(body1, body2, overlapOnly) || this.separateX(body1, body2, overlapOnly) || this.separateY(body1, body2, overlapOnly)); 332 | } 333 | 334 | return this._result; 335 | } 336 | 337 | /** 338 | * Check for intersection against two bodies. 339 | * 340 | * @method IsoPhysics#intersects 341 | * @param {Body} body1 - The Body object to check. 342 | * @param {Body} body2 - The Body object to check. 343 | * @return {boolean} True if they intersect, otherwise false. 344 | */ 345 | intersects(body1, body2) { 346 | if (body1.frontX <= body2.x) { 347 | return false; 348 | } 349 | 350 | if (body1.frontY <= body2.y) { 351 | return false; 352 | } 353 | 354 | if (body1.x >= body2.frontX) { 355 | return false; 356 | } 357 | 358 | if (body1.y >= body2.frontY) { 359 | return false; 360 | } 361 | 362 | if (body1.top <= body2.z) { 363 | return false; 364 | } 365 | 366 | if (body1.z >= body2.top) { 367 | return false; 368 | } 369 | 370 | return true; 371 | } 372 | 373 | /** 374 | * The core separation function to separate two physics bodies on the x axis. 375 | * 376 | * @private 377 | * @method IsoPhysics#separateX 378 | * @param {Body} body1 - The Body object to separate. 379 | * @param {Body} body2 - The Body object to separate. 380 | * @param {boolean} overlapOnly - If true the bodies will only have their overlap data set, no separation or exchange of velocity will take place. 381 | * @return {boolean} Returns true if the bodies were separated, otherwise false. 382 | */ 383 | separateX(body1, body2, overlapOnly) { 384 | // Can't separate two immovable bodies 385 | if (body1.immovable && body2.immovable) { 386 | return false; 387 | } 388 | 389 | this._overlap = 0; 390 | 391 | this._maxOverlap = body1.deltaAbsX() + body2.deltaAbsX() + this.OVERLAP_BIAS; 392 | 393 | if (body1.deltaX() === 0 && body2.deltaX() === 0) { 394 | // They overlap but neither of them are moving 395 | body1.embedded = true; 396 | body2.embedded = true; 397 | } else if (body1.deltaX() > body2.deltaX()) { 398 | // Body1 is moving forward and/or Body2 is moving back 399 | this._overlap = body1.frontX - body2.x; 400 | 401 | if ((this._overlap > this._maxOverlap) || body1.checkCollision.frontX === false || body2.checkCollision.backX === false) { 402 | this._overlap = 0; 403 | } else { 404 | body1.touching.none = false; 405 | body1.touching.frontX = true; 406 | body2.touching.none = false; 407 | body2.touching.backX = true; 408 | } 409 | } else if (body1.deltaX() < body2.deltaX()) { 410 | // Body1 is moving back and/or Body2 is moving forward 411 | this._overlap = body1.x - body2.widthX - body2.x; 412 | 413 | if ((-this._overlap > this._maxOverlap) || body1.checkCollision.backX === false || body2.checkCollision.frontX === false) { 414 | this._overlap = 0; 415 | } else { 416 | body1.touching.none = false; 417 | body1.touching.backX = true; 418 | body2.touching.none = false; 419 | body2.touching.frontX = true; 420 | } 421 | } 422 | 423 | // Then adjust their positions and velocities accordingly (if there was any overlap) 424 | if (this._overlap !== 0) { 425 | body1.overlapX = this._overlap; 426 | body2.overlapX = this._overlap; 427 | 428 | if (overlapOnly || body1.customSeparateX || body2.customSeparateX) { 429 | return true; 430 | } 431 | 432 | this._velocity1 = body1.velocity.x; 433 | this._velocity2 = body2.velocity.x; 434 | 435 | if (!body1.immovable && !body2.immovable) { 436 | this._overlap *= 0.5; 437 | 438 | body1.x = body1.x - this._overlap; 439 | body2.x += this._overlap; 440 | 441 | this._newVelocity1 = Math.sqrt((this._velocity2 * this._velocity2 * body2.mass) / body1.mass) * ((this._velocity2 > 0) ? 1 : -1); 442 | this._newVelocity2 = Math.sqrt((this._velocity1 * this._velocity1 * body1.mass) / body2.mass) * ((this._velocity1 > 0) ? 1 : -1); 443 | this._average = (this._newVelocity1 + this._newVelocity2) * 0.5; 444 | this._newVelocity1 -= this._average; 445 | this._newVelocity2 -= this._average; 446 | 447 | body1.velocity.x = this._average + this._newVelocity1 * body1.bounce.x; 448 | body2.velocity.x = this._average + this._newVelocity2 * body2.bounce.x; 449 | } else if (!body1.immovable) { 450 | body1.x = body1.x - this._overlap; 451 | body1.velocity.x = this._velocity2 - this._velocity1 * body1.bounce.x; 452 | } else if (!body2.immovable) { 453 | body2.x += this._overlap; 454 | body2.velocity.x = this._velocity1 - this._velocity2 * body2.bounce.x; 455 | } 456 | 457 | return true; 458 | } 459 | 460 | return false; 461 | } 462 | 463 | /** 464 | * The core separation function to separate two physics bodies on the x axis. 465 | * 466 | * @private 467 | * @method IsoPhysics#separateY 468 | * @param {Body} body1 - The Body object to separate. 469 | * @param {Body} body2 - The Body object to separate. 470 | * @param {boolean} overlapOnly - If true the bodies will only have their overlap data set, no separation or exchange of velocity will take place. 471 | * @return {boolean} Returns true if the bodies were separated, otherwise false. 472 | */ 473 | separateY(body1, body2, overlapOnly) { 474 | // Can't separate two immovable bodies 475 | if (body1.immovable && body2.immovable) { 476 | return false; 477 | } 478 | 479 | this._overlap = 0; 480 | 481 | this._maxOverlap = body1.deltaAbsY() + body2.deltaAbsY() + this.OVERLAP_BIAS; 482 | 483 | if (body1.deltaY() === 0 && body2.deltaY() === 0) { 484 | // They overlap but neither of them are moving 485 | body1.embedded = true; 486 | body2.embedded = true; 487 | } else if (body1.deltaY() > body2.deltaY()) { 488 | // Body1 is moving forward and/or Body2 is moving back 489 | this._overlap = body1.frontY - body2.y; 490 | 491 | if ((this._overlap > this._maxOverlap) || body1.checkCollision.frontY === false || body2.checkCollision.backY === false) { 492 | this._overlap = 0; 493 | } else { 494 | body1.touching.none = false; 495 | body1.touching.frontY = true; 496 | body2.touching.none = false; 497 | body2.touching.backY = true; 498 | } 499 | } else if (body1.deltaY() < body2.deltaY()) { 500 | // Body1 is moving back and/or Body2 is moving forward 501 | this._overlap = body1.y - body2.widthY - body2.y; 502 | 503 | if ((-this._overlap > this._maxOverlap) || body1.checkCollision.backY === false || body2.checkCollision.frontY === false) { 504 | this._overlap = 0; 505 | } else { 506 | body1.touching.none = false; 507 | body1.touching.backY = true; 508 | body2.touching.none = false; 509 | body2.touching.frontY = true; 510 | } 511 | } 512 | 513 | // Then adjust their positions and velocities accordingly (if there was any overlap) 514 | if (this._overlap !== 0) { 515 | body1.overlapY = this._overlap; 516 | body2.overlapY = this._overlap; 517 | 518 | if (overlapOnly || body1.customSeparateY || body2.customSeparateY) { 519 | return true; 520 | } 521 | 522 | this._velocity1 = body1.velocity.y; 523 | this._velocity2 = body2.velocity.y; 524 | 525 | if (!body1.immovable && !body2.immovable) { 526 | this._overlap *= 0.5; 527 | 528 | body1.y = body1.y - this._overlap; 529 | body2.y += this._overlap; 530 | 531 | this._newVelocity1 = Math.sqrt((this._velocity2 * this._velocity2 * body2.mass) / body1.mass) * ((this._velocity2 > 0) ? 1 : -1); 532 | this._newVelocity2 = Math.sqrt((this._velocity1 * this._velocity1 * body1.mass) / body2.mass) * ((this._velocity1 > 0) ? 1 : -1); 533 | this._average = (this._newVelocity1 + this._newVelocity2) * 0.5; 534 | this._newVelocity1 -= this._average; 535 | this._newVelocity2 -= this._average; 536 | 537 | body1.velocity.y = this._average + this._newVelocity1 * body1.bounce.y; 538 | body2.velocity.y = this._average + this._newVelocity2 * body2.bounce.y; 539 | } else if (!body1.immovable) { 540 | body1.y = body1.y - this._overlap; 541 | body1.velocity.y = this._velocity2 - this._velocity1 * body1.bounce.y; 542 | } else if (!body2.immovable) { 543 | body2.y += this._overlap; 544 | body2.velocity.y = this._velocity1 - this._velocity2 * body2.bounce.y; 545 | } 546 | 547 | return true; 548 | } 549 | 550 | return false; 551 | } 552 | 553 | /** 554 | * The core separation function to separate two physics bodies on the z axis. 555 | * 556 | * @private 557 | * @method IsoPhysics#separateZ 558 | * @param {Body} body1 - The Body object to separate. 559 | * @param {Body} body2 - The Body object to separate. 560 | * @param {boolean} overlapOnly - If true the bodies will only have their overlap data set, no separation or exchange of velocity will take place. 561 | * @return {boolean} Returns true if the bodies were separated, otherwise false. 562 | */ 563 | separateZ(body1, body2, overlapOnly) { 564 | // Can't separate two immovable or non-existing bodys 565 | if (body1.immovable && body2.immovable) { 566 | return false; 567 | } 568 | 569 | this._overlap = 0; 570 | 571 | this._maxOverlap = body1.deltaAbsZ() + body2.deltaAbsZ() + this.OVERLAP_BIAS; 572 | 573 | if (body1.deltaZ() === 0 && body2.deltaZ() === 0) { 574 | // They overlap but neither of them are moving 575 | body1.embedded = true; 576 | body2.embedded = true; 577 | } else if (body1.deltaZ() > body2.deltaZ()) { 578 | // Body1 is moving down and/or Body2 is moving up 579 | this._overlap = body1.top - body2.z; 580 | 581 | if ((this._overlap > this._maxOverlap) || body1.checkCollision.down === false || body2.checkCollision.up === false) { 582 | this._overlap = 0; 583 | } else { 584 | body1.touching.none = false; 585 | body1.touching.down = true; 586 | body2.touching.none = false; 587 | body2.touching.up = true; 588 | } 589 | } else if (body1.deltaZ() < body2.deltaZ()) { 590 | // Body1 is moving up and/or Body2 is moving down 591 | this._overlap = body1.z - body2.top; 592 | 593 | if ((-this._overlap > this._maxOverlap) || body1.checkCollision.up === false || body2.checkCollision.down === false) { 594 | this._overlap = 0; 595 | } else { 596 | body1.touching.none = false; 597 | body1.touching.up = true; 598 | body2.touching.none = false; 599 | body2.touching.down = true; 600 | } 601 | } 602 | 603 | // Then adjust their positions and velocities accordingly (if there was any overlap) 604 | if (this._overlap !== 0) { 605 | body1.overlapZ = this._overlap; 606 | body2.overlapZ = this._overlap; 607 | 608 | if (overlapOnly || body1.customSeparateY || body2.customSeparateZ) { 609 | return true; 610 | } 611 | 612 | this._velocity1 = body1.velocity.z; 613 | this._velocity2 = body2.velocity.z; 614 | 615 | if (!body1.immovable && !body2.immovable) { 616 | this._overlap *= 0.5; 617 | 618 | body1.z = body1.z - this._overlap; 619 | body2.z += this._overlap; 620 | 621 | this._newVelocity1 = Math.sqrt((this._velocity2 * this._velocity2 * body2.mass) / body1.mass) * ((this._velocity2 > 0) ? 1 : -1); 622 | this._newVelocity2 = Math.sqrt((this._velocity1 * this._velocity1 * body1.mass) / body2.mass) * ((this._velocity1 > 0) ? 1 : -1); 623 | this._average = (this._newVelocity1 + this._newVelocity2) * 0.5; 624 | this._newVelocity1 -= this._average; 625 | this._newVelocity2 -= this._average; 626 | 627 | body1.velocity.z = this._average + this._newVelocity1 * body1.bounce.z; 628 | body2.velocity.z = this._average + this._newVelocity2 * body2.bounce.z; 629 | } else if (!body1.immovable) { 630 | body1.z = body1.z - this._overlap; 631 | body1.velocity.z = this._velocity2 - this._velocity1 * body1.bounce.z; 632 | 633 | // This is special case code that handles things like moving platforms you can ride 634 | if (body2.moves) { 635 | body1.x += body2.x - body2.prev.x; 636 | body1.y += body2.y - body2.prev.y; 637 | } 638 | } else if (!body2.immovable) { 639 | body2.z += this._overlap; 640 | body2.velocity.z = this._velocity1 - this._velocity2 * body2.bounce.z; 641 | 642 | // This is special case code that handles things like moving platforms you can ride 643 | if (body1.moves) { 644 | body2.x += body1.x - body1.prev.x; 645 | body2.y += body1.y - body1.prev.y; 646 | } 647 | } 648 | 649 | return true; 650 | } 651 | 652 | return false; 653 | } 654 | 655 | /** 656 | * Checks for overlaps between two game objects. The objects can be IsoSprites or Groups. 657 | * You can perform IsoSprite vs. IsoSprite, IsoSprite vs. Group and Group vs. Group overlap checks. 658 | * Unlike collide the objects are NOT automatically separated or have any physics applied, they merely test for overlap results. 659 | * The second parameter can be an array of objects, of differing types. 660 | * NOTE: This function is not recursive, and will not test against children of objects passed (i.e. Groups within Groups). 661 | * 662 | * @method IsoPhysics#overlap 663 | * @param {IsoSprite|Phaser.Group} object1 - The first object to check. Can be an instance of IsoSprite or Phaser.Group. 664 | * @param {IsoSprite|Phaser.Group|array} object2 - The second object or array of objects to check. Can be IsoSprite or Phaser.Group. 665 | * @param {function} [overlapCallback=null] - An optional callback function that is called if the objects overlap. The two objects will be passed to this function in the same order in which you specified them. 666 | * @param {function} [processCallback=null] - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then overlapCallback will only be called if processCallback returns true. 667 | * @param {object} [callbackContext] - The context in which to run the callbacks. 668 | * @return {boolean} True if an overlap occured otherwise false. 669 | */ 670 | overlap(object1, object2, overlapCallback = null, processCallback = null, callbackContext) { 671 | callbackContext = callbackContext || overlapCallback; 672 | 673 | this._result = false; 674 | this._total = 0; 675 | 676 | if (Array.isArray(object2)) { 677 | for (var i = 0, len = object2.length; i < len; i++) { 678 | this.collideHandler(object1, object2[i], overlapCallback, processCallback, callbackContext, true); 679 | } 680 | } else { 681 | this.collideHandler(object1, object2, overlapCallback, processCallback, callbackContext, true); 682 | } 683 | 684 | return (this._total > 0); 685 | } 686 | 687 | /** 688 | * Checks for collision between two game objects. You can perform IsoSprite vs. IsoSprite, IsoSprite vs. Group or Group vs. Group collisions. 689 | * The second parameter can be an array of objects, of differing types. 690 | * The objects are also automatically separated. If you don't require separation then use IsoPhysics.overlap instead. 691 | * An optional processCallback can be provided. If given this function will be called when two sprites are found to be colliding. It is called before any separation takes place, 692 | * giving you the chance to perform additional checks. If the function returns true then the collision and separation is carried out. If it returns false it is skipped. 693 | * The collideCallback is an optional function that is only called if two sprites collide. If a processCallback has been set then it needs to return true for collideCallback to be called. 694 | * NOTE: This function is not recursive, and will not test against children of objects passed (i.e. Groups within Groups). 695 | * 696 | * @method IsoPhysics#collide 697 | * @param {IsoSprite|Phaser.Group} object1 - The first object to check. Can be an instance of IsoSprite or Phaser.Group. 698 | * @param {IsoSprite|Phaser.Group|array} object2 - The second object or array of objects to check. Can be IsoSprite or Phaser.Group. 699 | * @param {function} [collideCallback=null] - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them, unless you are colliding Group vs. Sprite, in which case Sprite will always be the first parameter. 700 | * @param {function} [processCallback=null] - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 701 | * @param {object} [callbackContext] - The context in which to run the callbacks. 702 | * @return {boolean} True if a collision occured otherwise false. 703 | */ 704 | collide(object1, object2, collideCallback = null, processCallback = null, callbackContext) { 705 | callbackContext = callbackContext || collideCallback; 706 | 707 | this._result = false; 708 | this._total = 0; 709 | 710 | if (Array.isArray(object2)) { 711 | for (var i = 0, len = object2.length; i < len; i++) { 712 | this.collideHandler(object1, object2[i], collideCallback, processCallback, callbackContext, false); 713 | } 714 | } 715 | else { 716 | this.collideHandler(object1, object2, collideCallback, processCallback, callbackContext, false); 717 | } 718 | 719 | return (this._total > 0); 720 | } 721 | 722 | /** 723 | * Internal collision handler. 724 | * 725 | * @method IsoPhysics#collideHandler 726 | * @private 727 | * @param {IsoSprite|Phaser.Group} object1 - The first object to check. Can be an instance of IsoSprite or Phaser.Group. 728 | * @param {IsoSprite|Phaser.Group} object2 - The second object to check. Can be an instance of IsoSprite or Phaser.Group. Can also be an array of objects to check. 729 | * @param {function} collideCallback - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them. 730 | * @param {function} processCallback - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 731 | * @param {object} callbackContext - The context in which to run the callbacks. 732 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 733 | */ 734 | collideHandler(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly) { 735 | // Only collide valid objects 736 | if (!object2 && object1.type === Phaser.GROUP) { 737 | this.collideGroupVsSelf(object1, collideCallback, processCallback, callbackContext, overlapOnly); 738 | return; 739 | } 740 | 741 | if (object1 && object2) { 742 | // ISOSPRITES 743 | if (object1.type === ISOSPRITE) { 744 | if (object2.type === ISOSPRITE) { 745 | this.collideSpriteVsSprite(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly); 746 | } else if (object2.type === Phaser.GROUP) { 747 | this.collideSpriteVsGroup(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly); 748 | } 749 | } 750 | // GROUPS 751 | else if (object1.type === Phaser.GROUP) { 752 | if (object2.type === ISOSPRITE) { 753 | this.collideSpriteVsGroup(object2, object1, collideCallback, processCallback, callbackContext, overlapOnly); 754 | } else if (object2.type === Phaser.GROUP) { 755 | this.collideGroupVsGroup(object1, object2, collideCallback, processCallback, callbackContext, overlapOnly); 756 | } 757 | } 758 | } 759 | } 760 | 761 | /** 762 | * An internal function. Use IsoPhysics.collide instead. 763 | * 764 | * @method IsoPhysics#collideSpriteVsSprite 765 | * @private 766 | * @param {IsoSprite} sprite1 - The first sprite to check. 767 | * @param {IsoSprite} sprite2 - The second sprite to check. 768 | * @param {function} collideCallback - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them. 769 | * @param {function} processCallback - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 770 | * @param {object} callbackContext - The context in which to run the callbacks. 771 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 772 | * @return {boolean} True if there was a collision, otherwise false. 773 | */ 774 | collideSpriteVsSprite(sprite1, sprite2, collideCallback, processCallback, callbackContext, overlapOnly) { 775 | if (!sprite1.body || !sprite2.body) { return false; } 776 | 777 | if (this.separate(sprite1.body, sprite2.body, processCallback, callbackContext, overlapOnly)) { 778 | if (collideCallback) { 779 | collideCallback.call(callbackContext, sprite1, sprite2); 780 | } 781 | 782 | this._total++; 783 | } 784 | 785 | return true; 786 | } 787 | 788 | /** 789 | * An internal function. Use IsoPhysics.collide instead. 790 | * 791 | * @method IsoPhysics#collideSpriteVsGroup 792 | * @private 793 | * @param {IsoSprite} sprite - The sprite to check. 794 | * @param {Phaser.Group} group - The Group to check. 795 | * @param {function} collideCallback - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them. 796 | * @param {function} processCallback - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 797 | * @param {object} callbackContext - The context in which to run the callbacks. 798 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 799 | */ 800 | collideSpriteVsGroup(sprite, group, collideCallback, processCallback, callbackContext, overlapOnly) { 801 | var i, len; 802 | 803 | if (group.children.size === 0 || !sprite.body) { return; } 804 | 805 | if (sprite.body.skipTree || this.skipTree) { 806 | for (i = 0, len = group.children.size; i < len; i++) { 807 | const child = group.children.entries[i]; 808 | if (child) { 809 | this.collideSpriteVsSprite(sprite, child, collideCallback, processCallback, callbackContext, overlapOnly); 810 | } 811 | } 812 | } else { 813 | // What is the sprite colliding with in the octree? 814 | this.octree.clear(); 815 | 816 | this.octree.reset(this.bounds.x, this.bounds.y, this.bounds.z, this.bounds.widthX, this.bounds.widthY, this.bounds.height, this.maxObjects, this.maxLevels); 817 | 818 | this.octree.populate(group); 819 | 820 | this._potentials = this.octree.retrieve(sprite); 821 | 822 | for (i = 0, len = this._potentials.length; i < len; i++) { 823 | // We have our potential suspects, are they in this group? 824 | if (this.separate(sprite.body, this._potentials[i], processCallback, callbackContext, overlapOnly)) { 825 | if (collideCallback) { 826 | collideCallback.call(callbackContext, sprite, this._potentials[i].sprite); 827 | } 828 | 829 | this._total++; 830 | } 831 | } 832 | } 833 | } 834 | 835 | /** 836 | * An internal function. Use IsoPhysics.collide instead. 837 | * 838 | * @method IsoPhysics#collideGroupVsSelf 839 | * @private 840 | * @param {Phaser.Group} group - The Group to check. 841 | * @param {function} collideCallback - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them. 842 | * @param {function} processCallback - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 843 | * @param {object} callbackContext - The context in which to run the callbacks. 844 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 845 | * @return {boolean} True if there was a collision, otherwise false. 846 | */ 847 | collideGroupVsSelf(group, collideCallback, processCallback, callbackContext, overlapOnly) { 848 | if (group.children.size === 0) { return; } 849 | 850 | var len = group.children.size; 851 | 852 | for (var i = 0; i < len; i++) { 853 | for (var j = i + 1; j <= len; j++) { 854 | const entries = group.children.entries; 855 | const spriteOne = entries[i]; 856 | const spriteTwo = entries[j]; 857 | 858 | if (spriteOne && spriteTwo) { 859 | this.collideSpriteVsSprite( 860 | spriteOne, 861 | spriteTwo, 862 | collideCallback, 863 | processCallback, 864 | callbackContext, 865 | overlapOnly 866 | ); 867 | } 868 | } 869 | } 870 | } 871 | 872 | /** 873 | * An internal function. Use IsoPhysics.collide instead. 874 | * 875 | * @method IsoPhysics#collideGroupVsGroup 876 | * @private 877 | * @param {Phaser.Group} group1 - The first Group to check. 878 | * @param {Phaser.Group} group2 - The second Group to check. 879 | * @param {function} collideCallback - An optional callback function that is called if the objects collide. The two objects will be passed to this function in the same order in which you specified them. 880 | * @param {function} processCallback - A callback function that lets you perform additional checks against the two objects if they overlap. If this is set then collision will only happen if processCallback returns true. The two objects will be passed to this function in the same order in which you specified them. 881 | * @param {object} callbackContext - The context in which to run the callbacks. 882 | * @param {boolean} overlapOnly - Just run an overlap or a full collision. 883 | */ 884 | collideGroupVsGroup(group1, group2, collideCallback, processCallback, callbackContext, overlapOnly) { 885 | if (group1.children.size === 0 || group2.children.size === 0) { return; } 886 | 887 | for (var i = 0, len = group1.children.size; i < len; i++) { 888 | this.collideSpriteVsGroup(group1.children.entries[i], group2, collideCallback, processCallback, callbackContext, overlapOnly); 889 | } 890 | } 891 | 892 | /** 893 | * Called automatically by a Physics body, it updates all motion related values on the Body. 894 | * 895 | * @method IsoPhysics#updateMotion 896 | * @param {Body} body - The Body object to be updated. 897 | */ 898 | updateMotion(body, delta) { 899 | this._velocityDelta = this.computeVelocity(0, body, body.angularVelocity, body.angularAcceleration, body.angularDrag, body.maxAngular) - body.angularVelocity; 900 | body.angularVelocity += this._velocityDelta; 901 | body.rotation += (body.angularVelocity * delta); 902 | 903 | body.velocity.x = this.computeVelocity(1, body, body.velocity.x, body.acceleration.x, body.drag.x, body.maxVelocity.x, delta); 904 | body.velocity.y = this.computeVelocity(2, body, body.velocity.y, body.acceleration.y, body.drag.y, body.maxVelocity.y, delta); 905 | body.velocity.z = this.computeVelocity(3, body, body.velocity.z, body.acceleration.z, body.drag.z, body.maxVelocity.z, delta); 906 | } 907 | 908 | update(time, delta) { 909 | const bodies = this.bodies.entries; 910 | const len = bodies.length; 911 | let i; 912 | 913 | for (i = 0; i < len; i++) { 914 | const body = bodies[i]; 915 | if (body.enable) { 916 | bodies[i].update(time, delta); 917 | } 918 | } 919 | } 920 | 921 | postUpdate() { 922 | const bodies = this.bodies.entries; 923 | const len = bodies.length; 924 | let i; 925 | 926 | for (i = 0; i < len; i++) { 927 | const body = bodies[i]; 928 | if (body.enable) { 929 | body.postUpdate(); 930 | } 931 | } 932 | } 933 | } 934 | --------------------------------------------------------------------------------