├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html ├── models │ ├── envSetting.glb │ ├── lantern.glb │ └── player.glb ├── sounds │ ├── 187024__lloydevans09__jump2.wav │ ├── 194081__potentjello__woosh-noise-1.wav │ ├── Christmassynths.ogg │ ├── Christmassynths.wav │ ├── Concrete 2.wav │ ├── Eye of the Storm.mp3 │ ├── Retro Event UI 13.wav │ ├── Retro Magic Protection 25.wav │ ├── Retro Water Drop 01.wav │ ├── Rise03.mp3 │ ├── Rise04.mp3 │ ├── Snowland.wav │ ├── copycat(revised).mp3 │ ├── fw_03.ogg │ ├── fw_03.wav │ ├── fw_05.ogg │ ├── fw_05.wav │ └── vgmenuselect.wav ├── sprites │ ├── aBtn.png │ ├── arrowBtn.png │ ├── bBtn.png │ ├── beginning_anim.png │ ├── bg_anim_text_dialogue.png │ ├── controls.jpeg │ ├── dropoff_anim.png │ ├── lanternbutton.jpeg │ ├── leaving_anim.png │ ├── lose.jpeg │ ├── pause.jpeg │ ├── pauseBtn.png │ ├── reading_anim.png │ ├── rotate.png │ ├── spark.png │ ├── sparkLife.png │ ├── start.jpeg │ ├── text_dialogue.png │ ├── tutorial.jpeg │ ├── tutorialMobile.jpeg │ ├── watermelon_anim.png │ └── working_anim.png ├── styles.css └── textures │ ├── envtext.env │ ├── flare.png │ ├── flwr.png │ ├── litLantern.png │ └── solidStar.png ├── src ├── app.ts ├── characterController.ts ├── environment.ts ├── inputController.ts ├── lantern.ts └── ui.ts ├── tsconfig.json ├── tutorial ├── characterMove1 │ ├── app.ts │ ├── characterController.ts │ └── inputController.ts ├── characterMove2 │ ├── app.ts │ ├── characterController.ts │ └── inputController.ts ├── collisionsTriggers │ ├── characterController.ts │ └── environment.ts ├── gui │ ├── app.ts │ └── ui.ts ├── importMeshes │ ├── app.ts │ ├── characterController.ts │ └── environment.ts ├── lanterns │ ├── app.ts │ ├── characterController.ts │ ├── environment.ts │ └── lantern.ts ├── oldLantern.ts ├── oldUpdateGround.txt ├── simpleGameState │ ├── app.ts │ ├── characterController.ts │ └── environment.ts └── stateMachine │ └── sampleApp.ts └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Hanabi (Chrome)", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "localhost:8080/", 9 | "webRoot": "${workspaceRoot}/", 10 | "sourceMaps": true, 11 | "preLaunchTask": "start" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "start", 8 | "type": "npm", 9 | "script": "start:dev", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "isBackground": true, 15 | "problemMatcher": { 16 | "pattern": [ 17 | { 18 | "regexp": "dummy", 19 | "file": 1, 20 | "location": 2, 21 | "message": 3 22 | } 23 | ], 24 | "background": { 25 | "activeOnStart": true, 26 | "beginsPattern": "hanabi@1.0.0 start", 27 | "endsPattern": "Compiled successfully." 28 | } 29 | }, 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SummerFestival 2 | Source code for game tutorial written by capucat 3 | 4 | Includes: game files, assets, tutorial related files 5 | 6 | [Documentation](https://doc.babylonjs.com/guidedLearning/createAGame) 7 | 8 | [Game](https://babylonjs.github.io/SummerFestival/) 9 | 10 | # How To's: 11 | Install dependencies via: 12 | `npm install` 13 | 14 | Build typescript and open web page via 15 | `npm run start` 16 | then connect to localhost:8080 from a web browser. 17 | 18 | # Controls: 19 | SPACE: jump 20 | 21 | SHIFT(mid-air): dash 22 | 23 | Left/Right/Up/Down Arrows: movement 24 | 25 | # Licensing 26 | ## Art Assets 27 | - [models](https://github.com/capucat/hanabi/tree/master/public/models) 28 | - [sprites](https://github.com/capucat/hanabi/tree/master/public/sprites) 29 | - [texture](https://github.com/capucat/hanabi/blob/master/public/textures/litLantern.png) 30 | 31 | Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. 32 | Art by Bianca Guerrero (capucat) 33 | 34 | ## Music 35 | ["copycat"](https://opengameart.org/content/copycat) by syncopika is licensed under [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/) 36 | ["Snowland Town"](https://opengameart.org/content/snowland-town) by Matthew Pablo is licensed under [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/) 37 | http://www.matthewpablo.com 38 | ["Jump2"](https://freesound.org/people/LloydEvans09/sounds/187024/) by LloydEvans09 is licensed under [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/) 39 | 40 | ## Other Assets 41 | Unless specified, all other assets are licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hanabi", 3 | "version": "1.0.0", 4 | "description": "3D babylon game", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack serve", 9 | "start:dev": "webpack serve --mode=development" 10 | }, 11 | "author": "capucat", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babylonjs/core": "^8.0.1", 15 | "@babylonjs/gui": "^8.0.1", 16 | "@babylonjs/inspector": "^8.0.1", 17 | "@babylonjs/loaders": "^8.0.1", 18 | "@types/react": "^17.0.1", 19 | "@types/react-dom": "^17.0.0", 20 | "clean-webpack-plugin": "^4.0.0", 21 | "copy-webpack-plugin": "^11.0.0", 22 | "html-loader": "^4.2.0", 23 | "html-webpack-plugin": "^5.5.0", 24 | "ts-loader": "^9.4.1", 25 | "typescript": "^4.8.4", 26 | "webpack": "^5.74.0", 27 | "webpack-cli": "^4.10.0", 28 | "webpack-dev-server": "^4.11.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hanabi 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/models/envSetting.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/models/envSetting.glb -------------------------------------------------------------------------------- /public/models/lantern.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/models/lantern.glb -------------------------------------------------------------------------------- /public/models/player.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/models/player.glb -------------------------------------------------------------------------------- /public/sounds/187024__lloydevans09__jump2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/187024__lloydevans09__jump2.wav -------------------------------------------------------------------------------- /public/sounds/194081__potentjello__woosh-noise-1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/194081__potentjello__woosh-noise-1.wav -------------------------------------------------------------------------------- /public/sounds/Christmassynths.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Christmassynths.ogg -------------------------------------------------------------------------------- /public/sounds/Christmassynths.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Christmassynths.wav -------------------------------------------------------------------------------- /public/sounds/Concrete 2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Concrete 2.wav -------------------------------------------------------------------------------- /public/sounds/Eye of the Storm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Eye of the Storm.mp3 -------------------------------------------------------------------------------- /public/sounds/Retro Event UI 13.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Retro Event UI 13.wav -------------------------------------------------------------------------------- /public/sounds/Retro Magic Protection 25.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Retro Magic Protection 25.wav -------------------------------------------------------------------------------- /public/sounds/Retro Water Drop 01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Retro Water Drop 01.wav -------------------------------------------------------------------------------- /public/sounds/Rise03.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Rise03.mp3 -------------------------------------------------------------------------------- /public/sounds/Rise04.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Rise04.mp3 -------------------------------------------------------------------------------- /public/sounds/Snowland.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/Snowland.wav -------------------------------------------------------------------------------- /public/sounds/copycat(revised).mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/copycat(revised).mp3 -------------------------------------------------------------------------------- /public/sounds/fw_03.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/fw_03.ogg -------------------------------------------------------------------------------- /public/sounds/fw_03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/fw_03.wav -------------------------------------------------------------------------------- /public/sounds/fw_05.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/fw_05.ogg -------------------------------------------------------------------------------- /public/sounds/fw_05.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/fw_05.wav -------------------------------------------------------------------------------- /public/sounds/vgmenuselect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sounds/vgmenuselect.wav -------------------------------------------------------------------------------- /public/sprites/aBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/aBtn.png -------------------------------------------------------------------------------- /public/sprites/arrowBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/arrowBtn.png -------------------------------------------------------------------------------- /public/sprites/bBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/bBtn.png -------------------------------------------------------------------------------- /public/sprites/beginning_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/beginning_anim.png -------------------------------------------------------------------------------- /public/sprites/bg_anim_text_dialogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/bg_anim_text_dialogue.png -------------------------------------------------------------------------------- /public/sprites/controls.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/controls.jpeg -------------------------------------------------------------------------------- /public/sprites/dropoff_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/dropoff_anim.png -------------------------------------------------------------------------------- /public/sprites/lanternbutton.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/lanternbutton.jpeg -------------------------------------------------------------------------------- /public/sprites/leaving_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/leaving_anim.png -------------------------------------------------------------------------------- /public/sprites/lose.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/lose.jpeg -------------------------------------------------------------------------------- /public/sprites/pause.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/pause.jpeg -------------------------------------------------------------------------------- /public/sprites/pauseBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/pauseBtn.png -------------------------------------------------------------------------------- /public/sprites/reading_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/reading_anim.png -------------------------------------------------------------------------------- /public/sprites/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/rotate.png -------------------------------------------------------------------------------- /public/sprites/spark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/spark.png -------------------------------------------------------------------------------- /public/sprites/sparkLife.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/sparkLife.png -------------------------------------------------------------------------------- /public/sprites/start.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/start.jpeg -------------------------------------------------------------------------------- /public/sprites/text_dialogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/text_dialogue.png -------------------------------------------------------------------------------- /public/sprites/tutorial.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/tutorial.jpeg -------------------------------------------------------------------------------- /public/sprites/tutorialMobile.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/tutorialMobile.jpeg -------------------------------------------------------------------------------- /public/sprites/watermelon_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/watermelon_anim.png -------------------------------------------------------------------------------- /public/sprites/working_anim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/sprites/working_anim.png -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | .disable-selection { 2 | -moz-user-select: none; 3 | -ms-user-select: none; 4 | -khtml-user-select: none; 5 | -webkit-user-select: none; 6 | -webkit-touch-callout: none; 7 | user-select: none; 8 | outline: none; 9 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */ 10 | } -------------------------------------------------------------------------------- /public/textures/envtext.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/textures/envtext.env -------------------------------------------------------------------------------- /public/textures/flare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/textures/flare.png -------------------------------------------------------------------------------- /public/textures/flwr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/textures/flwr.png -------------------------------------------------------------------------------- /public/textures/litLantern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/textures/litLantern.png -------------------------------------------------------------------------------- /public/textures/solidStar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BabylonJS/SummerFestival/0e870bc73fd8ffa1e767c466b1d2ee69852d4b95/public/textures/solidStar.png -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, Vector3, Color3, TransformNode, SceneLoader, ParticleSystem, Color4, Texture, PBRMetallicRoughnessMaterial, VertexBuffer, AnimationGroup, Sound, ExecuteCodeAction, ActionManager, Tags } from "@babylonjs/core"; 2 | import { Lantern } from "./lantern"; 3 | import { Player } from "./characterController"; 4 | 5 | export class Environment { 6 | private _scene: Scene; 7 | 8 | //Meshes 9 | private _lanternObjs: Array; //array of lanterns that need to be lit 10 | private _lightmtl: PBRMetallicRoughnessMaterial; // emissive texture for when lanterns are lit 11 | 12 | //fireworks 13 | private _fireworkObjs = []; 14 | private _startFireworks: boolean = false; 15 | 16 | constructor(scene: Scene) { 17 | this._scene = scene; 18 | this._lanternObjs = []; 19 | 20 | //create emissive material for when lantern is lit 21 | const lightmtl = new PBRMetallicRoughnessMaterial("lantern mesh light", this._scene); 22 | lightmtl.emissiveTexture = new Texture("/textures/litLantern.png", this._scene, true, false); 23 | lightmtl.emissiveColor = new Color3(0.8784313725490196, 0.7568627450980392, 0.6235294117647059); 24 | this._lightmtl = lightmtl; 25 | } 26 | //What we do once the environment assets have been imported 27 | //handles setting the necessary flags for collision and trigger meshes, 28 | //sets up the lantern objects 29 | //creates the firework particle systems for end-game 30 | public async load() { 31 | 32 | const assets = await this._loadAsset(); 33 | //Loop through all environment meshes that were imported 34 | assets.allMeshes.forEach(m => { 35 | m.receiveShadows = true; 36 | m.checkCollisions = true; 37 | 38 | if (m.name == "ground") { //dont check for collisions, dont allow for raycasting to detect it(cant land on it) 39 | m.checkCollisions = false; 40 | m.isPickable = false; 41 | } 42 | 43 | //areas that will use box collisions 44 | if (m.name.includes("stairs") || m.name == "cityentranceground" || m.name == "fishingground.001" || m.name.includes("lilyflwr")) { 45 | m.checkCollisions = false; 46 | m.isPickable = false; 47 | } 48 | //collision meshes 49 | if (m.name.includes("collision")) { 50 | m.isVisible = false; 51 | m.isPickable = true; 52 | } 53 | //trigger meshes 54 | if (m.name.includes("Trigger")) { 55 | m.isVisible = false; 56 | m.isPickable = false; 57 | m.checkCollisions = false; 58 | } 59 | }); 60 | 61 | //--LANTERNS-- 62 | assets.lantern.isVisible = false; //original mesh is not visible 63 | //transform node to hold all lanterns 64 | const lanternHolder = new TransformNode("lanternHolder", this._scene); 65 | for (let i = 0; i < 22; i++) { 66 | //Mesh Cloning 67 | let lanternInstance = assets.lantern.clone("lantern" + i); //bring in imported lantern mesh & make clones 68 | lanternInstance.isVisible = true; 69 | lanternInstance.setParent(lanternHolder); 70 | 71 | //Animation cloning 72 | let animGroupClone = new AnimationGroup("lanternAnimGroup " + i); 73 | animGroupClone.addTargetedAnimation(assets.animationGroups.targetedAnimations[0].animation, lanternInstance); 74 | 75 | //Create the new lantern object 76 | let newLantern = new Lantern(this._lightmtl, lanternInstance, this._scene, assets.env.getChildTransformNodes(false).find(m => m.name === "lantern " + i).getAbsolutePosition(), animGroupClone); 77 | this._lanternObjs.push(newLantern); 78 | } 79 | //dispose of original mesh and animation group that were cloned 80 | assets.lantern.dispose(); 81 | assets.animationGroups.dispose(); 82 | 83 | //--FIREWORKS-- 84 | for (let i = 0; i < 20; i++) { 85 | this._fireworkObjs.push(new Firework(this._scene, i)); 86 | } 87 | //before the scene renders, check to see if the fireworks have started 88 | //if they have, trigger the firework sequence 89 | this._scene.onBeforeRenderObservable.add(() => { 90 | this._fireworkObjs.forEach(f => { 91 | if (this._startFireworks) { 92 | f._startFirework(); 93 | } 94 | }) 95 | }) 96 | } 97 | 98 | 99 | //Load all necessary meshes for the environment 100 | public async _loadAsset() { 101 | //loads game environment 102 | const result = await SceneLoader.ImportMeshAsync(null, "./models/", "envSetting.glb", this._scene); 103 | 104 | let env = result.meshes[0]; 105 | let allMeshes = env.getChildMeshes(); 106 | 107 | //loads lantern mesh 108 | const res = await SceneLoader.ImportMeshAsync("", "./models/", "lantern.glb", this._scene); 109 | 110 | //extract the actual lantern mesh from the root of the mesh that's imported, dispose of the root 111 | let lantern = res.meshes[0].getChildren()[0]; 112 | lantern.parent = null; 113 | res.meshes[0].dispose(); 114 | 115 | //--ANIMATION-- 116 | //extract animation from lantern (following demystifying animation groups video) 117 | const importedAnims = res.animationGroups; 118 | let animation = []; 119 | animation.push(importedAnims[0].targetedAnimations[0].animation); 120 | importedAnims[0].dispose(); 121 | //create a new animation group and target the mesh to its animation 122 | let animGroup = new AnimationGroup("lanternAnimGroup"); 123 | animGroup.addTargetedAnimation(animation[0], res.meshes[1]); 124 | 125 | return { 126 | env: env, 127 | allMeshes: allMeshes, 128 | lantern: lantern as Mesh, 129 | animationGroups: animGroup 130 | } 131 | } 132 | 133 | public checkLanterns(player: Player) { 134 | if (!this._lanternObjs[0].isLit) { 135 | this._lanternObjs[0].setEmissiveTexture(); 136 | } 137 | this._lanternObjs.forEach(lantern => { 138 | player.mesh.actionManager.registerAction( 139 | new ExecuteCodeAction( 140 | { 141 | trigger: ActionManager.OnIntersectionEnterTrigger, 142 | parameter: lantern.mesh 143 | }, 144 | () => { 145 | //if the lantern is not lit, light it up & reset sparkler timer 146 | if (!lantern.isLit && player.sparkLit) { 147 | player.lanternsLit += 1; 148 | lantern.setEmissiveTexture(); 149 | player.sparkReset = true; 150 | player.sparkLit = true; 151 | 152 | //SFX 153 | player.lightSfx.play(); 154 | } 155 | //if the lantern is lit already, reset the sparkler timer 156 | else if (lantern.isLit) { 157 | player.sparkReset = true; 158 | player.sparkLit = true; 159 | 160 | //SFX 161 | player.sparkResetSfx.play(); 162 | } 163 | } 164 | ) 165 | ); 166 | }); 167 | } 168 | } 169 | 170 | class Firework { 171 | private _scene:Scene; 172 | 173 | //variables used by environment 174 | private _emitter: Mesh; 175 | private _rocket: ParticleSystem; 176 | private _exploded: boolean = false; 177 | private _height: number; 178 | private _delay: number; 179 | private _started: boolean; 180 | 181 | //sounds 182 | private _explosionSfx: Sound; 183 | private _rocketSfx: Sound; 184 | 185 | constructor(scene: Scene, i: number) { 186 | this._scene = scene; 187 | //Emitter for rocket of firework 188 | const sphere = Mesh.CreateSphere("rocket", 4, 1, scene); 189 | sphere.isVisible = false; 190 | //the origin spawn point for all fireworks is determined by a TransformNode called "fireworks", this was placed in blender 191 | let randPos = Math.random() * 10; 192 | sphere.position = (new Vector3(scene.getTransformNodeByName("fireworks").getAbsolutePosition().x + randPos * -1, scene.getTransformNodeByName("fireworks").getAbsolutePosition().y, scene.getTransformNodeByName("fireworks").getAbsolutePosition().z)); 193 | this._emitter = sphere; 194 | 195 | //Rocket particle system 196 | let rocket = new ParticleSystem("rocket", 350, scene); 197 | rocket.particleTexture = new Texture("./textures/flare.png", scene); 198 | rocket.emitter = sphere; 199 | rocket.emitRate = 20; 200 | rocket.minEmitBox = new Vector3(0, 0, 0); 201 | rocket.maxEmitBox = new Vector3(0, 0, 0); 202 | rocket.color1 = new Color4(0.49, 0.57, 0.76); 203 | rocket.color2 = new Color4(0.29, 0.29, 0.66); 204 | rocket.colorDead = new Color4(0, 0, 0.2, 0.5); 205 | rocket.minSize = 1; 206 | rocket.maxSize = 1; 207 | rocket.addSizeGradient(0, 1); 208 | rocket.addSizeGradient(1, 0.01); 209 | this._rocket = rocket; 210 | 211 | //set how high the rocket will travel before exploding and how long it'll take before shooting the rocket 212 | this._height = sphere.position.y + Math.random() * (15 + 4) + 4; 213 | this._delay = (Math.random() * i + 1) * 60; //frame based 214 | 215 | this._loadSounds(); 216 | } 217 | 218 | private _explosions(position: Vector3): void { 219 | //mesh that gets split into vertices 220 | const explosion = Mesh.CreateSphere("explosion", 4, 1, this._scene); 221 | explosion.isVisible = false; 222 | explosion.position = position; 223 | 224 | let emitter = explosion; 225 | emitter.useVertexColors = true; 226 | let vertPos = emitter.getVerticesData(VertexBuffer.PositionKind); 227 | let vertNorms = emitter.getVerticesData(VertexBuffer.NormalKind); 228 | let vertColors = []; 229 | 230 | //for each vertex, create a particle system 231 | for (let i = 0; i < vertPos.length; i += 3) { 232 | let vertPosition = new Vector3( 233 | vertPos[i], vertPos[i + 1], vertPos[i + 2] 234 | ) 235 | let vertNormal = new Vector3( 236 | vertNorms[i], vertNorms[i + 1], vertNorms[i + 2] 237 | ) 238 | let r = Math.random(); 239 | let g = Math.random(); 240 | let b = Math.random(); 241 | let alpha = 1.0; 242 | let color = new Color4(r, g, b, alpha); 243 | vertColors.push(r); 244 | vertColors.push(g); 245 | vertColors.push(b); 246 | vertColors.push(alpha); 247 | 248 | //emitter for the particle system 249 | let gizmo = Mesh.CreateBox("gizmo", 0.001, this._scene); 250 | gizmo.position = vertPosition; 251 | gizmo.parent = emitter; 252 | let direction = vertNormal.normalize().scale(1); // move in the direction of the normal 253 | 254 | //actual particle system for each exploding piece 255 | const particleSys = new ParticleSystem("particles", 500, this._scene); 256 | particleSys.particleTexture = new Texture("textures/flare.png", this._scene); 257 | particleSys.emitter = gizmo; 258 | particleSys.minEmitBox = new Vector3(1, 0, 0); 259 | particleSys.maxEmitBox = new Vector3(1, 0, 0); 260 | particleSys.minSize = .1; 261 | particleSys.maxSize = .1; 262 | particleSys.color1 = color; 263 | particleSys.color2 = color; 264 | particleSys.colorDead = new Color4(0, 0, 0, 0.0); 265 | particleSys.minLifeTime = 1; 266 | particleSys.maxLifeTime = 2; 267 | particleSys.emitRate = 500; 268 | particleSys.gravity = new Vector3(0, -9.8, 0); 269 | particleSys.direction1 = direction; 270 | particleSys.direction2 = direction; 271 | particleSys.minEmitPower = 10; 272 | particleSys.maxEmitPower = 13; 273 | particleSys.updateSpeed = 0.01; 274 | particleSys.targetStopDuration = 0.2; 275 | particleSys.disposeOnStop = true; 276 | particleSys.start(); 277 | } 278 | 279 | emitter.setVerticesData(VertexBuffer.ColorKind, vertColors); 280 | } 281 | 282 | private _startFirework(): void { 283 | 284 | if(this._started) { //if it's started, rocket flies up to height & then explodes 285 | if (this._emitter.position.y >= this._height && !this._exploded) { 286 | //--sounds-- 287 | this._explosionSfx.play(); 288 | //transition to the explosion particle system 289 | this._exploded = !this._exploded; // don't allow for it to explode again 290 | this._explosions(this._emitter.position); 291 | this._emitter.dispose(); 292 | this._rocket.stop(); 293 | } else { 294 | //move the rocket up 295 | this._emitter.position.y += .2; 296 | } 297 | } else { 298 | //use its delay to know when to shoot the firework 299 | if(this._delay <= 0){ 300 | this._started = true; 301 | //--sounds-- 302 | this._rocketSfx.play(); 303 | //start particle system 304 | this._rocket.start(); 305 | } else { 306 | this._delay--; 307 | } 308 | } 309 | } 310 | 311 | private _loadSounds(): void { 312 | this._rocketSfx = new Sound("selection", "./sounds/fw_05.wav", this._scene, function () { 313 | }, { 314 | volume: 0.5, 315 | }); 316 | 317 | this._explosionSfx = new Sound("selection", "./sounds/fw_03.wav", this._scene, function () { 318 | }, { 319 | volume: 0.5, 320 | }); 321 | } 322 | } -------------------------------------------------------------------------------- /src/inputController.ts: -------------------------------------------------------------------------------- 1 | import { Scene, ActionManager, ExecuteCodeAction, Observer, Scalar } from '@babylonjs/core'; 2 | import { Hud } from './ui'; 3 | 4 | export class PlayerInput { 5 | 6 | public inputMap: any; 7 | private _scene: Scene; 8 | 9 | //simple movement 10 | public horizontal: number = 0; 11 | public vertical: number = 0; 12 | //tracks whether or not there is movement in that axis 13 | public horizontalAxis: number = 0; 14 | public verticalAxis: number = 0; 15 | 16 | //jumping and dashing 17 | public jumpKeyDown: boolean = false; 18 | public dashing: boolean = false; 19 | 20 | //Mobile Input trackers 21 | private _ui: Hud; 22 | public mobileLeft: boolean; 23 | public mobileRight: boolean; 24 | public mobileUp: boolean; 25 | public mobileDown: boolean; 26 | private _mobileJump: boolean; 27 | private _mobileDash: boolean; 28 | 29 | constructor(scene: Scene, ui: Hud) { 30 | 31 | this._scene = scene; 32 | this._ui = ui; 33 | 34 | //scene action manager to detect inputs 35 | this._scene.actionManager = new ActionManager(this._scene); 36 | 37 | this.inputMap = {}; 38 | this._scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, (evt) => { 39 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 40 | })); 41 | this._scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, (evt) => { 42 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 43 | })); 44 | 45 | //add to the scene an observable that calls updateFromKeyboard before rendering 46 | scene.onBeforeRenderObservable.add(() => { 47 | this._updateFromKeyboard(); 48 | }); 49 | 50 | // Set up Mobile Controls if on mobile device 51 | if (this._ui.isMobile) { 52 | this._setUpMobile(); 53 | } 54 | } 55 | 56 | // Keyboard controls & Mobile controls 57 | //handles what is done when keys are pressed or if on mobile, when buttons are pressed 58 | private _updateFromKeyboard(): void { 59 | 60 | //forward - backwards movement 61 | if ((this.inputMap["ArrowUp"] || this.mobileUp) && !this._ui.gamePaused) { 62 | this.verticalAxis = 1; 63 | this.vertical = Scalar.Lerp(this.vertical, 1, 0.2); 64 | 65 | } else if ((this.inputMap["ArrowDown"] || this.mobileDown) && !this._ui.gamePaused) { 66 | this.vertical = Scalar.Lerp(this.vertical, -1, 0.2); 67 | this.verticalAxis = -1; 68 | } else { 69 | this.vertical = 0; 70 | this.verticalAxis = 0; 71 | } 72 | 73 | //left - right movement 74 | if ((this.inputMap["ArrowLeft"] || this.mobileLeft) && !this._ui.gamePaused) { 75 | //lerp will create a scalar linearly interpolated amt between start and end scalar 76 | //taking current horizontal and how long you hold, will go up to -1(all the way left) 77 | this.horizontal = Scalar.Lerp(this.horizontal, -1, 0.2); 78 | this.horizontalAxis = -1; 79 | 80 | } else if ((this.inputMap["ArrowRight"] || this.mobileRight) && !this._ui.gamePaused) { 81 | this.horizontal = Scalar.Lerp(this.horizontal, 1, 0.2); 82 | this.horizontalAxis = 1; 83 | } 84 | else { 85 | this.horizontal = 0; 86 | this.horizontalAxis = 0; 87 | } 88 | 89 | //dash 90 | if ((this.inputMap["Shift"] || this._mobileDash) && !this._ui.gamePaused) { 91 | this.dashing = true; 92 | } else { 93 | this.dashing = false; 94 | } 95 | 96 | //Jump Checks (SPACE) 97 | if ((this.inputMap[" "] || this._mobileJump) && !this._ui.gamePaused) { 98 | this.jumpKeyDown = true; 99 | } else { 100 | this.jumpKeyDown = false; 101 | } 102 | } 103 | 104 | // Mobile controls 105 | private _setUpMobile(): void { 106 | //Jump Button 107 | this._ui.jumpBtn.onPointerDownObservable.add(() => { 108 | this._mobileJump = true; 109 | }); 110 | this._ui.jumpBtn.onPointerUpObservable.add(() => { 111 | this._mobileJump = false; 112 | }); 113 | 114 | //Dash Button 115 | this._ui.dashBtn.onPointerDownObservable.add(() => { 116 | this._mobileDash = true; 117 | }); 118 | this._ui.dashBtn.onPointerUpObservable.add(() => { 119 | this._mobileDash = false; 120 | }); 121 | 122 | //Arrow Keys 123 | this._ui.leftBtn.onPointerDownObservable.add(() => { 124 | this.mobileLeft = true; 125 | }); 126 | this._ui.leftBtn.onPointerUpObservable.add(() => { 127 | this.mobileLeft = false; 128 | }); 129 | 130 | this._ui.rightBtn.onPointerDownObservable.add(() => { 131 | this.mobileRight = true; 132 | }); 133 | this._ui.rightBtn.onPointerUpObservable.add(() => { 134 | this.mobileRight = false; 135 | }); 136 | 137 | this._ui.upBtn.onPointerDownObservable.add(() => { 138 | this.mobileUp = true; 139 | }); 140 | this._ui.upBtn.onPointerUpObservable.add(() => { 141 | this.mobileUp = false; 142 | }); 143 | 144 | this._ui.downBtn.onPointerDownObservable.add(() => { 145 | this.mobileDown = true; 146 | }); 147 | this._ui.downBtn.onPointerUpObservable.add(() => { 148 | this.mobileDown = false; 149 | }); 150 | 151 | 152 | } 153 | } -------------------------------------------------------------------------------- /src/lantern.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Color3, Mesh, Vector3, PointLight, Texture, Color4, ParticleSystem, AnimationGroup, PBRMetallicRoughnessMaterial } from "@babylonjs/core"; 2 | 3 | export class Lantern { 4 | public _scene: Scene; 5 | 6 | public mesh: Mesh; 7 | public isLit: boolean = false; 8 | private _lightmtl: PBRMetallicRoughnessMaterial; 9 | private _light: PointLight; 10 | 11 | //Lantern animations 12 | private _spinAnim: AnimationGroup; 13 | 14 | //Particle System 15 | private _stars: ParticleSystem; 16 | 17 | constructor(lightmtl: PBRMetallicRoughnessMaterial, mesh: Mesh, scene: Scene, position: Vector3, animationGroups: AnimationGroup) { 18 | this._scene = scene; 19 | this._lightmtl = lightmtl; 20 | 21 | //load the lantern mesh 22 | this._loadLantern(mesh, position); 23 | 24 | //load particle system 25 | this._loadStars(); 26 | 27 | //set animations 28 | this._spinAnim = animationGroups; 29 | 30 | //create light source for the lanterns 31 | const light = new PointLight("lantern light", this.mesh.getAbsolutePosition(), this._scene); 32 | light.intensity = 0; 33 | light.radius = 2; 34 | light.diffuse = new Color3(0.45, 0.56, 0.80); 35 | this._light = light; 36 | //only allow light to affect meshes near it 37 | this._findNearestMeshes(light); 38 | } 39 | 40 | private _loadLantern(mesh: Mesh, position: Vector3): void { 41 | this.mesh = mesh; 42 | this.mesh.scaling = new Vector3(.8, .8, .8); 43 | this.mesh.setAbsolutePosition(position); 44 | this.mesh.isPickable = false; 45 | } 46 | 47 | public setEmissiveTexture(): void { 48 | this.isLit = true; 49 | 50 | //play animation and particle system 51 | this._spinAnim.play(); 52 | this._stars.start(); 53 | //swap texture 54 | this.mesh.material = this._lightmtl; 55 | this._light.intensity = 30; 56 | } 57 | 58 | //when the light is created, only include the meshes specified 59 | private _findNearestMeshes(light: PointLight): void { 60 | if(this.mesh.name.includes("14") || this.mesh.name.includes("15")) { 61 | light.includedOnlyMeshes.push(this._scene.getMeshByName("festivalPlatform1")); 62 | } else if(this.mesh.name.includes("16") || this.mesh.name.includes("17")) { 63 | light.includedOnlyMeshes.push(this._scene.getMeshByName("festivalPlatform2")); 64 | } else if (this.mesh.name.includes("18") || this.mesh.name.includes("19")) { 65 | light.includedOnlyMeshes.push(this._scene.getMeshByName("festivalPlatform3")); 66 | } else if (this.mesh.name.includes("20") || this.mesh.name.includes("21")) { 67 | light.includedOnlyMeshes.push(this._scene.getMeshByName("festivalPlatform4")); 68 | } 69 | //grab the corresponding transform node that holds all of the meshes affected by this lantern's light 70 | this._scene.getTransformNodeByName(this.mesh.name + "lights").getChildMeshes().forEach(m => { 71 | light.includedOnlyMeshes.push(m); 72 | }) 73 | } 74 | 75 | private _loadStars(): void { 76 | const particleSystem = new ParticleSystem("stars", 1000, this._scene); 77 | 78 | particleSystem.particleTexture = new Texture("textures/solidStar.png", this._scene); 79 | particleSystem.emitter = new Vector3(this.mesh.position.x, this.mesh.position.y + 1.5, this.mesh.position.z); 80 | particleSystem.createPointEmitter(new Vector3(0.6, 1, 0), new Vector3(0, 1, 0)); 81 | particleSystem.color1 = new Color4(1, 1, 1); 82 | particleSystem.color2 = new Color4(1, 1, 1); 83 | particleSystem.colorDead = new Color4(1, 1, 1, 1); 84 | particleSystem.emitRate = 12; 85 | particleSystem.minEmitPower = 14; 86 | particleSystem.maxEmitPower = 14; 87 | particleSystem.addStartSizeGradient(0, 2); 88 | particleSystem.addStartSizeGradient(1, 0.8); 89 | particleSystem.minAngularSpeed = 0; 90 | particleSystem.maxAngularSpeed = 2; 91 | particleSystem.addDragGradient(0, 0.7, 0.7); 92 | particleSystem.targetStopDuration = .25; 93 | 94 | this._stars = particleSystem; 95 | } 96 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "noResolve": false, 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "preserveConstEnums":true, 9 | "lib": [ 10 | "dom", 11 | "es6" 12 | ], 13 | // "outDir": "./dist" 14 | "rootDir": "src" 15 | }, 16 | "exclude": [ //dont include the tutorial files in the build, they're just there for references in documentation 17 | "tutorial" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tutorial/characterMove1/app.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4, StandardMaterial, Color3, PointLight, ShadowGenerator, Quaternion, Matrix } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | import { Environment } from "./environment"; 7 | import { Player } from "./characterController"; 8 | import { PlayerInput } from "./inputController"; 9 | 10 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 11 | 12 | class App { 13 | // General Entire Application 14 | private _scene: Scene; 15 | private _canvas: HTMLCanvasElement; 16 | private _engine: Engine; 17 | 18 | //Game State Related 19 | public assets; 20 | private _input: PlayerInput; 21 | private _environment; 22 | private _player: Player; 23 | 24 | 25 | //Scene - related 26 | private _state: number = 0; 27 | private _gamescene: Scene; 28 | private _cutScene: Scene; 29 | 30 | constructor() { 31 | this._canvas = this._createCanvas(); 32 | 33 | // initialize babylon scene and engine 34 | this._engine = new Engine(this._canvas, true); 35 | this._scene = new Scene(this._engine); 36 | 37 | // hide/show the Inspector 38 | window.addEventListener("keydown", (ev) => { 39 | // Shift+Ctrl+Alt+I 40 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 41 | if (this._scene.debugLayer.isVisible()) { 42 | this._scene.debugLayer.hide(); 43 | } else { 44 | this._scene.debugLayer.show(); 45 | } 46 | } 47 | }); 48 | 49 | // run the main render loop 50 | this._main(); 51 | } 52 | 53 | private _createCanvas(): HTMLCanvasElement { 54 | 55 | //Commented out for development 56 | // document.documentElement.style["overflow"] = "hidden"; 57 | // document.documentElement.style.overflow = "hidden"; 58 | // document.documentElement.style.width = "100%"; 59 | // document.documentElement.style.height = "100%"; 60 | // document.documentElement.style.margin = "0"; 61 | // document.documentElement.style.padding = "0"; 62 | // document.body.style.overflow = "hidden"; 63 | // document.body.style.width = "100%"; 64 | // document.body.style.height = "100%"; 65 | // document.body.style.margin = "0"; 66 | // document.body.style.padding = "0"; 67 | 68 | //create the canvas html element and attach it to the webpage 69 | this._canvas = document.createElement("canvas"); 70 | this._canvas.style.width = "100%"; 71 | this._canvas.style.height = "100%"; 72 | this._canvas.id = "gameCanvas"; 73 | document.body.appendChild(this._canvas); 74 | 75 | return this._canvas; 76 | } 77 | 78 | private async _main(): Promise { 79 | await this._goToStart(); 80 | 81 | // Register a render loop to repeatedly render the scene 82 | this._engine.runRenderLoop(() => { 83 | switch (this._state) { 84 | case State.START: 85 | this._scene.render(); 86 | break; 87 | case State.CUTSCENE: 88 | this._scene.render(); 89 | break; 90 | case State.GAME: 91 | this._scene.render(); 92 | break; 93 | case State.LOSE: 94 | this._scene.render(); 95 | break; 96 | default: break; 97 | } 98 | }); 99 | 100 | //resize if the screen is resized/rotated 101 | window.addEventListener('resize', () => { 102 | this._engine.resize(); 103 | }); 104 | } 105 | private async _goToStart(){ 106 | this._engine.displayLoadingUI(); 107 | 108 | this._scene.detachControl(); 109 | let scene = new Scene(this._engine); 110 | scene.clearColor = new Color4(0,0,0,1); 111 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 112 | camera.setTarget(Vector3.Zero()); 113 | 114 | //create a fullscreen ui for all of our GUI elements 115 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 116 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 117 | 118 | //create a simple button 119 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 120 | startBtn.width = 0.2 121 | startBtn.height = "40px"; 122 | startBtn.color = "white"; 123 | startBtn.top = "-14px"; 124 | startBtn.thickness = 0; 125 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 126 | guiMenu.addControl(startBtn); 127 | 128 | //this handles interactions with the start button attached to the scene 129 | startBtn.onPointerDownObservable.add(() => { 130 | this._goToCutScene(); 131 | scene.detachControl(); //observables disabled 132 | }); 133 | 134 | //--SCENE FINISHED LOADING-- 135 | await scene.whenReadyAsync(); 136 | this._engine.hideLoadingUI(); 137 | //lastly set the current state to the start state and set the scene to the start scene 138 | this._scene.dispose(); 139 | this._scene = scene; 140 | this._state = State.START; 141 | } 142 | 143 | private async _goToCutScene(): Promise { 144 | this._engine.displayLoadingUI(); 145 | //--SETUP SCENE-- 146 | //dont detect any inputs from this ui while the game is loading 147 | this._scene.detachControl(); 148 | this._cutScene = new Scene(this._engine); 149 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 150 | camera.setTarget(Vector3.Zero()); 151 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 152 | 153 | //--GUI-- 154 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 155 | 156 | //--PROGRESS DIALOGUE-- 157 | const next = Button.CreateSimpleButton("next", "NEXT"); 158 | next.color = "white"; 159 | next.thickness = 0; 160 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 161 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 162 | next.width = "64px"; 163 | next.height = "64px"; 164 | next.top = "-3%"; 165 | next.left = "-12%"; 166 | cutScene.addControl(next); 167 | 168 | next.onPointerUpObservable.add(() => { 169 | this._goToGame(); 170 | }) 171 | 172 | //--WHEN SCENE IS FINISHED LOADING-- 173 | await this._cutScene.whenReadyAsync(); 174 | this._engine.hideLoadingUI(); 175 | this._scene.dispose(); 176 | this._state = State.CUTSCENE; 177 | this._scene = this._cutScene; 178 | 179 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 180 | var finishedLoading = false; 181 | await this._setUpGame().then(res =>{ 182 | finishedLoading = true; 183 | }); 184 | } 185 | 186 | private async _setUpGame() { 187 | let scene = new Scene(this._engine); 188 | this._gamescene = scene; 189 | 190 | //--CREATE ENVIRONMENT-- 191 | const environment = new Environment(scene); 192 | this._environment = environment; 193 | await this._environment.load(); //environment 194 | await this._loadCharacterAssets(scene); 195 | } 196 | 197 | private async _loadCharacterAssets(scene){ 198 | 199 | async function loadCharacter(){ 200 | //collision mesh 201 | const outer = MeshBuilder.CreateBox("outer", { width: 2, depth: 1, height: 3 }, scene); 202 | outer.isVisible = false; 203 | outer.isPickable = false; 204 | outer.checkCollisions = true; 205 | 206 | //move origin of box collider to the bottom of the mesh (to match player mesh) 207 | outer.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)) 208 | 209 | //for collisions 210 | // outer.ellipsoid = new Vector3(1, 1.5, 1); 211 | // outer.ellipsoidOffset = new Vector3(0, 1.5, 0); 212 | 213 | outer.rotationQuaternion = new Quaternion(0, 1, 0, 0); // rotate the player mesh 180 since we want to see the back of the player 214 | 215 | var box = MeshBuilder.CreateBox("Small1", { width: 0.5, depth: 0.5, height: 0.25, faceColors: [new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1),new Color4(0,0,0,1), new Color4(0,0,0,1)] }, scene); 216 | box.position.y = 1.5; 217 | box.position.z = 1; 218 | 219 | var body = Mesh.CreateCylinder("body", 3, 2,2,0,0,scene); 220 | var bodymtl = new StandardMaterial("red",scene); 221 | bodymtl.diffuseColor = new Color3(.8,.5,.5); 222 | body.material = bodymtl; 223 | body.isPickable = false; 224 | body.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)); // simulates the imported mesh's origin 225 | 226 | //parent the meshes 227 | box.parent = body; 228 | body.parent = outer; 229 | 230 | return { 231 | mesh: outer as Mesh 232 | } 233 | } 234 | return loadCharacter().then(assets=> { 235 | this.assets = assets; 236 | }) 237 | 238 | } 239 | 240 | private async _initializeGameAsync(scene): Promise { 241 | //temporary light to light the entire scene 242 | var light0 = new HemisphericLight("HemiLight", new Vector3(0, 1, 0), scene); 243 | 244 | const light = new PointLight("sparklight", new Vector3(0, 0, 0), scene); 245 | light.diffuse = new Color3(0.08627450980392157, 0.10980392156862745, 0.15294117647058825); 246 | light.intensity = 35; 247 | light.radius = 1; 248 | 249 | const shadowGenerator = new ShadowGenerator(1024, light); 250 | shadowGenerator.darkness = 0.4; 251 | 252 | //Create the player 253 | this._player = new Player(this.assets, scene, shadowGenerator, this._input); 254 | const camera = this._player.activatePlayerCamera(); 255 | } 256 | 257 | private async _goToGame(){ 258 | //--SETUP SCENE-- 259 | this._scene.detachControl(); 260 | let scene = this._gamescene; 261 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 262 | 263 | //--GUI-- 264 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 265 | //dont detect any inputs from this ui while the game is loading 266 | scene.detachControl(); 267 | 268 | //create a simple button 269 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 270 | loseBtn.width = 0.2 271 | loseBtn.height = "40px"; 272 | loseBtn.color = "white"; 273 | loseBtn.top = "-14px"; 274 | loseBtn.thickness = 0; 275 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 276 | playerUI.addControl(loseBtn); 277 | 278 | //this handles interactions with the start button attached to the scene 279 | loseBtn.onPointerDownObservable.add(() => { 280 | this._goToLose(); 281 | scene.detachControl(); //observables disabled 282 | }); 283 | 284 | //--INPUT-- 285 | this._input = new PlayerInput(scene); //detect keyboard/mobile inputs 286 | 287 | //primitive character and setting 288 | await this._initializeGameAsync(scene); 289 | 290 | //--WHEN SCENE FINISHED LOADING-- 291 | await scene.whenReadyAsync(); 292 | scene.getMeshByName("outer").position = new Vector3(0,3,0); 293 | //get rid of start scene, switch to gamescene and change states 294 | this._scene.dispose(); 295 | this._state = State.GAME; 296 | this._scene = scene; 297 | this._engine.hideLoadingUI(); 298 | //the game is ready, attach control back 299 | this._scene.attachControl(); 300 | } 301 | 302 | private async _goToLose(): Promise { 303 | this._engine.displayLoadingUI(); 304 | 305 | //--SCENE SETUP-- 306 | this._scene.detachControl(); 307 | let scene = new Scene(this._engine); 308 | scene.clearColor = new Color4(0, 0, 0, 1); 309 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 310 | camera.setTarget(Vector3.Zero()); 311 | 312 | //--GUI-- 313 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 314 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 315 | mainBtn.width = 0.2; 316 | mainBtn.height = "40px"; 317 | mainBtn.color = "white"; 318 | guiMenu.addControl(mainBtn); 319 | //this handles interactions with the start button attached to the scene 320 | mainBtn.onPointerUpObservable.add(() => { 321 | this._goToStart(); 322 | }); 323 | 324 | //--SCENE FINISHED LOADING-- 325 | await scene.whenReadyAsync(); 326 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 327 | //lastly set the current state to the lose state and set the scene to the lose scene 328 | this._scene.dispose(); 329 | this._scene = scene; 330 | this._state = State.LOSE; 331 | } 332 | } 333 | new App(); -------------------------------------------------------------------------------- /tutorial/characterMove1/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3, Quaternion, Ray } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | //Camera 12 | private _camRoot: TransformNode; 13 | private _yTilt: TransformNode; 14 | 15 | //const values 16 | private static readonly PLAYER_SPEED: number = 0.45; 17 | private static readonly JUMP_FORCE: number = 0.80; 18 | private static readonly GRAVITY: number = -2.8; 19 | private static readonly ORIGINAL_TILT: Vector3 = new Vector3(0.5934119456780721, 0, 0); 20 | 21 | //player movement vars 22 | private _deltaTime: number = 0; 23 | private _h: number; 24 | private _v: number; 25 | 26 | private _moveDirection: Vector3 = new Vector3(); 27 | private _inputAmt: number; 28 | 29 | //gravity, ground detection, jumping 30 | private _gravity: Vector3 = new Vector3(); 31 | private _lastGroundPos: Vector3 = Vector3.Zero(); // keep track of the last grounded position 32 | private _grounded: boolean; 33 | 34 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 35 | super("player", scene); 36 | this.scene = scene; 37 | this._setupPlayerCamera(); 38 | 39 | this.mesh = assets.mesh; 40 | this.mesh.parent = this; 41 | 42 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 43 | 44 | this._input = input; 45 | } 46 | 47 | private _updateFromControls(): void { 48 | this._deltaTime = this.scene.getEngine().getDeltaTime() / 1000.0; 49 | 50 | this._moveDirection = Vector3.Zero(); // vector that holds movement information 51 | this._h = this._input.horizontal; //x-axis 52 | this._v = this._input.vertical; //z-axis 53 | 54 | //--MOVEMENTS BASED ON CAMERA (as it rotates)-- 55 | let fwd = this._camRoot.forward; 56 | let right = this._camRoot.right; 57 | let correctedVertical = fwd.scaleInPlace(this._v); 58 | let correctedHorizontal = right.scaleInPlace(this._h); 59 | 60 | //movement based off of camera's view 61 | let move = correctedHorizontal.addInPlace(correctedVertical); 62 | 63 | //clear y so that the character doesnt fly up, normalize for next step 64 | this._moveDirection = new Vector3((move).normalize().x, 0, (move).normalize().z); 65 | 66 | //clamp the input value so that diagonal movement isn't twice as fast 67 | let inputMag = Math.abs(this._h) + Math.abs(this._v); 68 | if (inputMag < 0) { 69 | this._inputAmt = 0; 70 | } else if (inputMag > 1) { 71 | this._inputAmt = 1; 72 | } else { 73 | this._inputAmt = inputMag; 74 | } 75 | 76 | //final movement that takes into consideration the inputs 77 | this._moveDirection = this._moveDirection.scaleInPlace(this._inputAmt * Player.PLAYER_SPEED); 78 | 79 | //Rotations 80 | //check if there is movement to determine if rotation is needed 81 | let input = new Vector3(this._input.horizontalAxis, 0, this._input.verticalAxis); //along which axis is the direction 82 | if (input.length() == 0) {//if there's no input detected, prevent rotation and keep player in same rotation 83 | return; 84 | } 85 | //rotation based on input & the camera angle 86 | let angle = Math.atan2(this._input.horizontalAxis, this._input.verticalAxis); 87 | angle += this._camRoot.rotation.y; 88 | let targ = Quaternion.FromEulerAngles(0, angle, 0); 89 | this.mesh.rotationQuaternion = Quaternion.Slerp(this.mesh.rotationQuaternion, targ, 10 * this._deltaTime); 90 | } 91 | 92 | private _floorRaycast(offsetx: number, offsetz: number, raycastlen: number): Vector3 { 93 | let raycastFloorPos = new Vector3(this.mesh.position.x + offsetx, this.mesh.position.y + 0.5, this.mesh.position.z + offsetz); 94 | let ray = new Ray(raycastFloorPos, Vector3.Up().scale(-1), raycastlen); 95 | 96 | let predicate = function (mesh) { 97 | return mesh.isPickable && mesh.isEnabled(); 98 | } 99 | let pick = this.scene.pickWithRay(ray, predicate); 100 | 101 | if (pick.hit) { 102 | return pick.pickedPoint; 103 | } else { 104 | return Vector3.Zero(); 105 | } 106 | } 107 | 108 | private _isGrounded(): boolean { 109 | if (this._floorRaycast(0, 0, 0.6).equals(Vector3.Zero())) { 110 | return false; 111 | } else { 112 | return true; 113 | } 114 | } 115 | 116 | private _updateGroundDetection(): void { 117 | if (!this._isGrounded()) { 118 | this._gravity = this._gravity.addInPlace(Vector3.Up().scale(this._deltaTime * Player.GRAVITY)); 119 | this._grounded = false; 120 | } 121 | //limit the speed of gravity to the negative of the jump power 122 | if (this._gravity.y < -Player.JUMP_FORCE) { 123 | this._gravity.y = -Player.JUMP_FORCE; 124 | } 125 | this.mesh.moveWithCollisions(this._moveDirection.addInPlace(this._gravity)); 126 | 127 | if (this._isGrounded()) { 128 | this._gravity.y = 0; 129 | this._grounded = true; 130 | this._lastGroundPos.copyFrom(this.mesh.position); 131 | } 132 | } 133 | 134 | private _beforeRenderUpdate(): void { 135 | this._updateFromControls(); 136 | this._updateGroundDetection(); 137 | } 138 | 139 | public activatePlayerCamera(): UniversalCamera { 140 | this.scene.registerBeforeRender(() => { 141 | 142 | this._beforeRenderUpdate(); 143 | this._updateCamera(); 144 | 145 | }) 146 | return this.camera; 147 | } 148 | 149 | private _updateCamera(): void { 150 | let centerPlayer = this.mesh.position.y + 2; 151 | this._camRoot.position = Vector3.Lerp(this._camRoot.position, new Vector3(this.mesh.position.x, centerPlayer, this.mesh.position.z), 0.4); 152 | } 153 | 154 | private _setupPlayerCamera() { 155 | //root camera parent that handles positioning of the camera to follow the player 156 | this._camRoot = new TransformNode("root"); 157 | this._camRoot.position = new Vector3(0, 0, 0); //initialized at (0,0,0) 158 | //to face the player from behind (180 degrees) 159 | this._camRoot.rotation = new Vector3(0, Math.PI, 0); 160 | 161 | //rotations along the x-axis (up/down tilting) 162 | let yTilt = new TransformNode("ytilt"); 163 | //adjustments to camera view to point down at our player 164 | yTilt.rotation = Player.ORIGINAL_TILT; 165 | this._yTilt = yTilt; 166 | yTilt.parent = this._camRoot; 167 | 168 | //our actual camera that's pointing at our root's position 169 | this.camera = new UniversalCamera("cam", new Vector3(0, 0, -30), this.scene); 170 | this.camera.lockedTarget = this._camRoot.position; 171 | this.camera.fov = 0.47350045992678597; 172 | this.camera.parent = yTilt; 173 | 174 | this.scene.activeCamera = this.camera; 175 | return this.camera; 176 | } 177 | } -------------------------------------------------------------------------------- /tutorial/characterMove1/inputController.ts: -------------------------------------------------------------------------------- 1 | import { Scene, ActionManager, ExecuteCodeAction, Scalar } from "@babylonjs/core"; 2 | 3 | export class PlayerInput { 4 | public inputMap: any; 5 | 6 | //simple movement 7 | public horizontal: number = 0; 8 | public vertical: number = 0; 9 | //tracks whether or not there is movement in that axis 10 | public horizontalAxis: number = 0; 11 | public verticalAxis: number = 0; 12 | 13 | constructor(scene: Scene) { 14 | scene.actionManager = new ActionManager(scene); 15 | 16 | this.inputMap = {}; 17 | scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, (evt) => { 18 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 19 | })); 20 | scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, (evt) => { 21 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 22 | })); 23 | 24 | scene.onBeforeRenderObservable.add(() => { 25 | this._updateFromKeyboard(); 26 | }); 27 | } 28 | 29 | private _updateFromKeyboard(): void { 30 | if (this.inputMap["ArrowUp"]) { 31 | this.vertical = Scalar.Lerp(this.vertical, 1, 0.2); 32 | this.verticalAxis = 1; 33 | 34 | } else if (this.inputMap["ArrowDown"]) { 35 | this.vertical = Scalar.Lerp(this.vertical, -1, 0.2); 36 | this.verticalAxis = -1; 37 | } else { 38 | this.vertical = 0; 39 | this.verticalAxis = 0; 40 | } 41 | 42 | if (this.inputMap["ArrowLeft"]) { 43 | this.horizontal = Scalar.Lerp(this.horizontal, -1, 0.2); 44 | this.horizontalAxis = -1; 45 | 46 | } else if (this.inputMap["ArrowRight"]) { 47 | this.horizontal = Scalar.Lerp(this.horizontal, 1, 0.2); 48 | this.horizontalAxis = 1; 49 | } 50 | else { 51 | this.horizontal = 0; 52 | this.horizontalAxis = 0; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /tutorial/characterMove2/app.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4, StandardMaterial, Color3, PointLight, ShadowGenerator, Quaternion, Matrix } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | import { Environment } from "./environment"; 7 | import { Player } from "./characterController"; 8 | import { PlayerInput } from "./inputController"; 9 | 10 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 11 | 12 | class App { 13 | // General Entire Application 14 | private _scene: Scene; 15 | private _canvas: HTMLCanvasElement; 16 | private _engine: Engine; 17 | 18 | //Game State Related 19 | public assets; 20 | private _input: PlayerInput; 21 | private _environment; 22 | private _player: Player; 23 | 24 | 25 | //Scene - related 26 | private _state: number = 0; 27 | private _gamescene: Scene; 28 | private _cutScene: Scene; 29 | 30 | constructor() { 31 | this._canvas = this._createCanvas(); 32 | 33 | // initialize babylon scene and engine 34 | this._engine = new Engine(this._canvas, true); 35 | this._scene = new Scene(this._engine); 36 | 37 | // hide/show the Inspector 38 | window.addEventListener("keydown", (ev) => { 39 | // Shift+Ctrl+Alt+I 40 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 41 | if (this._scene.debugLayer.isVisible()) { 42 | this._scene.debugLayer.hide(); 43 | } else { 44 | this._scene.debugLayer.show(); 45 | } 46 | } 47 | }); 48 | 49 | // run the main render loop 50 | this._main(); 51 | } 52 | 53 | private _createCanvas(): HTMLCanvasElement { 54 | 55 | //Commented out for development 56 | // document.documentElement.style["overflow"] = "hidden"; 57 | // document.documentElement.style.overflow = "hidden"; 58 | // document.documentElement.style.width = "100%"; 59 | // document.documentElement.style.height = "100%"; 60 | // document.documentElement.style.margin = "0"; 61 | // document.documentElement.style.padding = "0"; 62 | // document.body.style.overflow = "hidden"; 63 | // document.body.style.width = "100%"; 64 | // document.body.style.height = "100%"; 65 | // document.body.style.margin = "0"; 66 | // document.body.style.padding = "0"; 67 | 68 | //create the canvas html element and attach it to the webpage 69 | this._canvas = document.createElement("canvas"); 70 | this._canvas.style.width = "100%"; 71 | this._canvas.style.height = "100%"; 72 | this._canvas.id = "gameCanvas"; 73 | document.body.appendChild(this._canvas); 74 | 75 | return this._canvas; 76 | } 77 | 78 | private async _main(): Promise { 79 | await this._goToStart(); 80 | 81 | // Register a render loop to repeatedly render the scene 82 | this._engine.runRenderLoop(() => { 83 | switch (this._state) { 84 | case State.START: 85 | this._scene.render(); 86 | break; 87 | case State.CUTSCENE: 88 | this._scene.render(); 89 | break; 90 | case State.GAME: 91 | this._scene.render(); 92 | break; 93 | case State.LOSE: 94 | this._scene.render(); 95 | break; 96 | default: break; 97 | } 98 | }); 99 | 100 | //resize if the screen is resized/rotated 101 | window.addEventListener('resize', () => { 102 | this._engine.resize(); 103 | }); 104 | } 105 | private async _goToStart(){ 106 | this._engine.displayLoadingUI(); 107 | 108 | this._scene.detachControl(); 109 | let scene = new Scene(this._engine); 110 | scene.clearColor = new Color4(0,0,0,1); 111 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 112 | camera.setTarget(Vector3.Zero()); 113 | 114 | //create a fullscreen ui for all of our GUI elements 115 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 116 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 117 | 118 | //create a simple button 119 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 120 | startBtn.width = 0.2 121 | startBtn.height = "40px"; 122 | startBtn.color = "white"; 123 | startBtn.top = "-14px"; 124 | startBtn.thickness = 0; 125 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 126 | guiMenu.addControl(startBtn); 127 | 128 | //this handles interactions with the start button attached to the scene 129 | startBtn.onPointerDownObservable.add(() => { 130 | this._goToCutScene(); 131 | scene.detachControl(); //observables disabled 132 | }); 133 | 134 | //--SCENE FINISHED LOADING-- 135 | await scene.whenReadyAsync(); 136 | this._engine.hideLoadingUI(); 137 | //lastly set the current state to the start state and set the scene to the start scene 138 | this._scene.dispose(); 139 | this._scene = scene; 140 | this._state = State.START; 141 | } 142 | 143 | private async _goToCutScene(): Promise { 144 | this._engine.displayLoadingUI(); 145 | //--SETUP SCENE-- 146 | //dont detect any inputs from this ui while the game is loading 147 | this._scene.detachControl(); 148 | this._cutScene = new Scene(this._engine); 149 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 150 | camera.setTarget(Vector3.Zero()); 151 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 152 | 153 | //--GUI-- 154 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 155 | 156 | //--PROGRESS DIALOGUE-- 157 | const next = Button.CreateSimpleButton("next", "NEXT"); 158 | next.color = "white"; 159 | next.thickness = 0; 160 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 161 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 162 | next.width = "64px"; 163 | next.height = "64px"; 164 | next.top = "-3%"; 165 | next.left = "-12%"; 166 | cutScene.addControl(next); 167 | 168 | next.onPointerUpObservable.add(() => { 169 | this._goToGame(); 170 | }) 171 | 172 | //--WHEN SCENE IS FINISHED LOADING-- 173 | await this._cutScene.whenReadyAsync(); 174 | this._engine.hideLoadingUI(); 175 | this._scene.dispose(); 176 | this._state = State.CUTSCENE; 177 | this._scene = this._cutScene; 178 | 179 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 180 | var finishedLoading = false; 181 | await this._setUpGame().then(res =>{ 182 | finishedLoading = true; 183 | }); 184 | } 185 | 186 | private async _setUpGame() { 187 | let scene = new Scene(this._engine); 188 | this._gamescene = scene; 189 | 190 | //--CREATE ENVIRONMENT-- 191 | const environment = new Environment(scene); 192 | this._environment = environment; 193 | await this._environment.load(); //environment 194 | await this._loadCharacterAssets(scene); 195 | } 196 | 197 | private async _loadCharacterAssets(scene){ 198 | 199 | async function loadCharacter(){ 200 | //collision mesh 201 | const outer = MeshBuilder.CreateBox("outer", { width: 2, depth: 1, height: 3 }, scene); 202 | outer.isVisible = false; 203 | outer.isPickable = false; 204 | outer.checkCollisions = true; 205 | 206 | //move origin of box collider to the bottom of the mesh (to match player mesh) 207 | outer.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)) 208 | 209 | //for collisions 210 | // outer.ellipsoid = new Vector3(1, 1.5, 1); 211 | // outer.ellipsoidOffset = new Vector3(0, 1.5, 0); 212 | 213 | outer.rotationQuaternion = new Quaternion(0, 1, 0, 0); // rotate the player mesh 180 since we want to see the back of the player 214 | 215 | var box = MeshBuilder.CreateBox("Small1", { width: 0.5, depth: 0.5, height: 0.25, faceColors: [new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1),new Color4(0,0,0,1), new Color4(0,0,0,1)] }, scene); 216 | box.position.y = 1.5; 217 | box.position.z = 1; 218 | 219 | var body = Mesh.CreateCylinder("body", 3, 2,2,0,0,scene); 220 | var bodymtl = new StandardMaterial("red",scene); 221 | bodymtl.diffuseColor = new Color3(.8,.5,.5); 222 | body.material = bodymtl; 223 | body.isPickable = false; 224 | body.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)); // simulates the imported mesh's origin 225 | 226 | //parent the meshes 227 | box.parent = body; 228 | body.parent = outer; 229 | 230 | return { 231 | mesh: outer as Mesh 232 | } 233 | } 234 | return loadCharacter().then(assets=> { 235 | this.assets = assets; 236 | }) 237 | 238 | } 239 | 240 | private async _initializeGameAsync(scene): Promise { 241 | //temporary light to light the entire scene 242 | var light0 = new HemisphericLight("HemiLight", new Vector3(0, 1, 0), scene); 243 | 244 | const light = new PointLight("sparklight", new Vector3(0, 0, 0), scene); 245 | light.diffuse = new Color3(0.08627450980392157, 0.10980392156862745, 0.15294117647058825); 246 | light.intensity = 35; 247 | light.radius = 1; 248 | 249 | const shadowGenerator = new ShadowGenerator(1024, light); 250 | shadowGenerator.darkness = 0.4; 251 | 252 | //Create the player 253 | this._player = new Player(this.assets, scene, shadowGenerator, this._input); 254 | const camera = this._player.activatePlayerCamera(); 255 | } 256 | 257 | private async _goToGame(){ 258 | //--SETUP SCENE-- 259 | this._scene.detachControl(); 260 | let scene = this._gamescene; 261 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 262 | 263 | //--GUI-- 264 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 265 | //dont detect any inputs from this ui while the game is loading 266 | scene.detachControl(); 267 | 268 | //create a simple button 269 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 270 | loseBtn.width = 0.2 271 | loseBtn.height = "40px"; 272 | loseBtn.color = "white"; 273 | loseBtn.top = "-14px"; 274 | loseBtn.thickness = 0; 275 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 276 | playerUI.addControl(loseBtn); 277 | 278 | //this handles interactions with the start button attached to the scene 279 | loseBtn.onPointerDownObservable.add(() => { 280 | this._goToLose(); 281 | scene.detachControl(); //observables disabled 282 | }); 283 | 284 | //--INPUT-- 285 | this._input = new PlayerInput(scene); //detect keyboard/mobile inputs 286 | 287 | //primitive character and setting 288 | await this._initializeGameAsync(scene); 289 | 290 | //--WHEN SCENE FINISHED LOADING-- 291 | await scene.whenReadyAsync(); 292 | scene.getMeshByName("outer").position = new Vector3(0,3,0); 293 | //get rid of start scene, switch to gamescene and change states 294 | this._scene.dispose(); 295 | this._state = State.GAME; 296 | this._scene = scene; 297 | this._engine.hideLoadingUI(); 298 | //the game is ready, attach control back 299 | this._scene.attachControl(); 300 | } 301 | 302 | private async _goToLose(): Promise { 303 | this._engine.displayLoadingUI(); 304 | 305 | //--SCENE SETUP-- 306 | this._scene.detachControl(); 307 | let scene = new Scene(this._engine); 308 | scene.clearColor = new Color4(0, 0, 0, 1); 309 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 310 | camera.setTarget(Vector3.Zero()); 311 | 312 | //--GUI-- 313 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 314 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 315 | mainBtn.width = 0.2; 316 | mainBtn.height = "40px"; 317 | mainBtn.color = "white"; 318 | guiMenu.addControl(mainBtn); 319 | //this handles interactions with the start button attached to the scene 320 | mainBtn.onPointerUpObservable.add(() => { 321 | this._goToStart(); 322 | }); 323 | 324 | //--SCENE FINISHED LOADING-- 325 | await scene.whenReadyAsync(); 326 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 327 | //lastly set the current state to the lose state and set the scene to the lose scene 328 | this._scene.dispose(); 329 | this._scene = scene; 330 | this._state = State.LOSE; 331 | } 332 | } 333 | new App(); -------------------------------------------------------------------------------- /tutorial/characterMove2/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3, Quaternion, Ray } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | //Camera 12 | private _camRoot: TransformNode; 13 | private _yTilt: TransformNode; 14 | 15 | //const values 16 | private static readonly PLAYER_SPEED: number = 0.45; 17 | private static readonly JUMP_FORCE: number = 0.80; 18 | private static readonly GRAVITY: number = -2.8; 19 | private static readonly DASH_FACTOR: number = 2.5; 20 | private static readonly DASH_TIME: number = 10; //how many frames the dash lasts 21 | private static readonly ORIGINAL_TILT: Vector3 = new Vector3(0.5934119456780721, 0, 0); 22 | public dashTime: number = 0; 23 | 24 | //player movement vars 25 | private _deltaTime: number = 0; 26 | private _h: number; 27 | private _v: number; 28 | 29 | private _moveDirection: Vector3 = new Vector3(); 30 | private _inputAmt: number; 31 | 32 | //dashing 33 | private _dashPressed: boolean; 34 | private _canDash: boolean = true; 35 | 36 | //gravity, ground detection, jumping 37 | private _gravity: Vector3 = new Vector3(); 38 | private _lastGroundPos: Vector3 = Vector3.Zero(); // keep track of the last grounded position 39 | private _grounded: boolean; 40 | private _jumpCount: number = 1; 41 | 42 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 43 | super("player", scene); 44 | this.scene = scene; 45 | this._setupPlayerCamera(); 46 | 47 | this.mesh = assets.mesh; 48 | this.mesh.parent = this; 49 | 50 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 51 | 52 | this._input = input; 53 | } 54 | 55 | private _updateFromControls(): void { 56 | this._deltaTime = this.scene.getEngine().getDeltaTime() / 1000.0; 57 | 58 | this._moveDirection = Vector3.Zero(); // vector that holds movement information 59 | this._h = this._input.horizontal; //x-axis 60 | this._v = this._input.vertical; //z-axis 61 | 62 | if (this._input.dashing && !this._dashPressed && this._canDash && !this._grounded) { 63 | this._canDash = false; //we've started a dash, do not allow another 64 | this._dashPressed = true; //start the dash sequence 65 | } 66 | 67 | let dashFactor = 1; 68 | //if you're dashing, scale movement 69 | if (this._dashPressed) { 70 | if (this.dashTime > Player.DASH_TIME) { 71 | this.dashTime = 0; 72 | this._dashPressed = false; 73 | } else { 74 | dashFactor = Player.DASH_FACTOR; 75 | } 76 | this.dashTime++; 77 | } 78 | 79 | //--MOVEMENTS BASED ON CAMERA (as it rotates)-- 80 | let fwd = this._camRoot.forward; 81 | let right = this._camRoot.right; 82 | let correctedVertical = fwd.scaleInPlace(this._v); 83 | let correctedHorizontal = right.scaleInPlace(this._h); 84 | 85 | //movement based off of camera's view 86 | let move = correctedHorizontal.addInPlace(correctedVertical); 87 | 88 | //clear y so that the character doesnt fly up, normalize for next step, taking into account whether we've DASHED or not 89 | this._moveDirection = new Vector3((move).normalize().x * dashFactor, 0, (move).normalize().z * dashFactor); 90 | 91 | //clamp the input value so that diagonal movement isn't twice as fast 92 | let inputMag = Math.abs(this._h) + Math.abs(this._v); 93 | if (inputMag < 0) { 94 | this._inputAmt = 0; 95 | } else if (inputMag > 1) { 96 | this._inputAmt = 1; 97 | } else { 98 | this._inputAmt = inputMag; 99 | } 100 | 101 | //final movement that takes into consideration the inputs 102 | this._moveDirection = this._moveDirection.scaleInPlace(this._inputAmt * Player.PLAYER_SPEED); 103 | 104 | //Rotations 105 | //check if there is movement to determine if rotation is needed 106 | let input = new Vector3(this._input.horizontalAxis, 0, this._input.verticalAxis); //along which axis is the direction 107 | if (input.length() == 0) {//if there's no input detected, prevent rotation and keep player in same rotation 108 | return; 109 | } 110 | //rotation based on input & the camera angle 111 | let angle = Math.atan2(this._input.horizontalAxis, this._input.verticalAxis); 112 | angle += this._camRoot.rotation.y; 113 | let targ = Quaternion.FromEulerAngles(0, angle, 0); 114 | this.mesh.rotationQuaternion = Quaternion.Slerp(this.mesh.rotationQuaternion, targ, 10 * this._deltaTime); 115 | } 116 | 117 | private _floorRaycast(offsetx: number, offsetz: number, raycastlen: number): Vector3 { 118 | let raycastFloorPos = new Vector3(this.mesh.position.x + offsetx, this.mesh.position.y + 0.5, this.mesh.position.z + offsetz); 119 | let ray = new Ray(raycastFloorPos, Vector3.Up().scale(-1), raycastlen); 120 | 121 | let predicate = function (mesh) { 122 | return mesh.isPickable && mesh.isEnabled(); 123 | } 124 | let pick = this.scene.pickWithRay(ray, predicate); 125 | 126 | if (pick.hit) { 127 | return pick.pickedPoint; 128 | } else { 129 | return Vector3.Zero(); 130 | } 131 | } 132 | 133 | private _isGrounded(): boolean { 134 | if (this._floorRaycast(0, 0, 0.6).equals(Vector3.Zero())) { 135 | return false; 136 | } else { 137 | return true; 138 | } 139 | } 140 | 141 | private _checkSlope(): boolean { 142 | 143 | //only check meshes that are pickable and enabled (specific for collision meshes that are invisible) 144 | let predicate = function (mesh) { 145 | return mesh.isPickable && mesh.isEnabled(); 146 | } 147 | 148 | //4 raycasts outward from center 149 | let raycast = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z + .25); 150 | let ray = new Ray(raycast, Vector3.Up().scale(-1), 1.5); 151 | let pick = this.scene.pickWithRay(ray, predicate); 152 | 153 | let raycast2 = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z - .25); 154 | let ray2 = new Ray(raycast2, Vector3.Up().scale(-1), 1.5); 155 | let pick2 = this.scene.pickWithRay(ray2, predicate); 156 | 157 | let raycast3 = new Vector3(this.mesh.position.x + .25, this.mesh.position.y + 0.5, this.mesh.position.z); 158 | let ray3 = new Ray(raycast3, Vector3.Up().scale(-1), 1.5); 159 | let pick3 = this.scene.pickWithRay(ray3, predicate); 160 | 161 | let raycast4 = new Vector3(this.mesh.position.x - .25, this.mesh.position.y + 0.5, this.mesh.position.z); 162 | let ray4 = new Ray(raycast4, Vector3.Up().scale(-1), 1.5); 163 | let pick4 = this.scene.pickWithRay(ray4, predicate); 164 | 165 | if (pick.hit && !pick.getNormal().equals(Vector3.Up())) { 166 | if(pick.pickedMesh.name.includes("stair")) { 167 | return true; 168 | } 169 | } else if (pick2.hit && !pick2.getNormal().equals(Vector3.Up())) { 170 | if(pick2.pickedMesh.name.includes("stair")) { 171 | return true; 172 | } 173 | } 174 | else if (pick3.hit && !pick3.getNormal().equals(Vector3.Up())) { 175 | if(pick3.pickedMesh.name.includes("stair")) { 176 | return true; 177 | } 178 | } 179 | else if (pick4.hit && !pick4.getNormal().equals(Vector3.Up())) { 180 | if(pick4.pickedMesh.name.includes("stair")) { 181 | return true; 182 | } 183 | } 184 | return false; 185 | } 186 | 187 | private _updateGroundDetection(): void { 188 | if (!this._isGrounded()) { 189 | //if the body isnt grounded, check if it's on a slope and was either falling or walking onto it 190 | if (this._checkSlope() && this._gravity.y <= 0) { 191 | //if you are considered on a slope, you're able to jump and gravity wont affect you 192 | this._gravity.y = 0; 193 | this._jumpCount = 1; 194 | this._grounded = true; 195 | } else { 196 | //keep applying gravity 197 | this._gravity = this._gravity.addInPlace(Vector3.Up().scale(this._deltaTime * Player.GRAVITY)); 198 | this._grounded = false; 199 | } 200 | } 201 | //limit the speed of gravity to the negative of the jump power 202 | if (this._gravity.y < -Player.JUMP_FORCE) { 203 | this._gravity.y = -Player.JUMP_FORCE; 204 | } 205 | this.mesh.moveWithCollisions(this._moveDirection.addInPlace(this._gravity)); 206 | 207 | if (this._isGrounded()) { 208 | this._gravity.y = 0; 209 | this._grounded = true; 210 | this._lastGroundPos.copyFrom(this.mesh.position); 211 | 212 | this._jumpCount = 1; //allow for jumping 213 | //dashing reset 214 | this._canDash = true; //the ability to dash 215 | //reset sequence(needed if we collide with the ground BEFORE actually completing the dash duration) 216 | this.dashTime = 0; 217 | this._dashPressed = false; 218 | } 219 | 220 | //Jump detection 221 | if (this._input.jumpKeyDown && this._jumpCount > 0) { 222 | this._gravity.y = Player.JUMP_FORCE; 223 | this._jumpCount--; 224 | } 225 | 226 | 227 | } 228 | 229 | private _beforeRenderUpdate(): void { 230 | this._updateFromControls(); 231 | this._updateGroundDetection(); 232 | } 233 | 234 | public activatePlayerCamera(): UniversalCamera { 235 | this.scene.registerBeforeRender(() => { 236 | 237 | this._beforeRenderUpdate(); 238 | this._updateCamera(); 239 | 240 | }) 241 | return this.camera; 242 | } 243 | 244 | private _updateCamera(): void { 245 | let centerPlayer = this.mesh.position.y + 2; 246 | this._camRoot.position = Vector3.Lerp(this._camRoot.position, new Vector3(this.mesh.position.x, centerPlayer, this.mesh.position.z), 0.4); 247 | } 248 | 249 | private _setupPlayerCamera() { 250 | //root camera parent that handles positioning of the camera to follow the player 251 | this._camRoot = new TransformNode("root"); 252 | this._camRoot.position = new Vector3(0, 0, 0); //initialized at (0,0,0) 253 | //to face the player from behind (180 degrees) 254 | this._camRoot.rotation = new Vector3(0, Math.PI, 0); 255 | 256 | //rotations along the x-axis (up/down tilting) 257 | let yTilt = new TransformNode("ytilt"); 258 | //adjustments to camera view to point down at our player 259 | yTilt.rotation = Player.ORIGINAL_TILT; 260 | this._yTilt = yTilt; 261 | yTilt.parent = this._camRoot; 262 | 263 | //our actual camera that's pointing at our root's position 264 | this.camera = new UniversalCamera("cam", new Vector3(0, 0, -30), this.scene); 265 | this.camera.lockedTarget = this._camRoot.position; 266 | this.camera.fov = 0.47350045992678597; 267 | this.camera.parent = yTilt; 268 | 269 | this.scene.activeCamera = this.camera; 270 | return this.camera; 271 | } 272 | } -------------------------------------------------------------------------------- /tutorial/characterMove2/inputController.ts: -------------------------------------------------------------------------------- 1 | import { Scene, ActionManager, ExecuteCodeAction, Scalar } from "@babylonjs/core"; 2 | 3 | export class PlayerInput { 4 | public inputMap: any; 5 | 6 | //simple movement 7 | public horizontal: number = 0; 8 | public vertical: number = 0; 9 | //tracks whether or not there is movement in that axis 10 | public horizontalAxis: number = 0; 11 | public verticalAxis: number = 0; 12 | 13 | //jumping and dashing 14 | public jumpKeyDown: boolean = false; 15 | public dashing: boolean = false; 16 | 17 | constructor(scene: Scene) { 18 | scene.actionManager = new ActionManager(scene); 19 | 20 | this.inputMap = {}; 21 | scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, (evt) => { 22 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 23 | })); 24 | scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, (evt) => { 25 | this.inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown"; 26 | })); 27 | 28 | scene.onBeforeRenderObservable.add(() => { 29 | this._updateFromKeyboard(); 30 | }); 31 | } 32 | 33 | private _updateFromKeyboard(): void { 34 | if (this.inputMap["ArrowUp"]) { 35 | this.vertical = Scalar.Lerp(this.vertical, 1, 0.2); 36 | this.verticalAxis = 1; 37 | 38 | } else if (this.inputMap["ArrowDown"]) { 39 | this.vertical = Scalar.Lerp(this.vertical, -1, 0.2); 40 | this.verticalAxis = -1; 41 | } else { 42 | this.vertical = 0; 43 | this.verticalAxis = 0; 44 | } 45 | 46 | if (this.inputMap["ArrowLeft"]) { 47 | this.horizontal = Scalar.Lerp(this.horizontal, -1, 0.2); 48 | this.horizontalAxis = -1; 49 | 50 | } else if (this.inputMap["ArrowRight"]) { 51 | this.horizontal = Scalar.Lerp(this.horizontal, 1, 0.2); 52 | this.horizontalAxis = 1; 53 | } 54 | else { 55 | this.horizontal = 0; 56 | this.horizontalAxis = 0; 57 | } 58 | 59 | //dash 60 | if (this.inputMap["Shift"]) { 61 | this.dashing = true; 62 | } else { 63 | this.dashing = false; 64 | } 65 | 66 | //Jump Checks (SPACE) 67 | if (this.inputMap[" "]) { 68 | this.jumpKeyDown = true; 69 | } else { 70 | this.jumpKeyDown = false; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /tutorial/collisionsTriggers/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3, Quaternion, Ray, ParticleSystem, ActionManager, ExecuteCodeAction } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | //Camera 12 | private _camRoot: TransformNode; 13 | private _yTilt: TransformNode; 14 | 15 | //const values 16 | private static readonly PLAYER_SPEED: number = 0.45; 17 | private static readonly JUMP_FORCE: number = 0.80; 18 | private static readonly GRAVITY: number = -2.8; 19 | private static readonly DASH_FACTOR: number = 2.5; 20 | private static readonly DASH_TIME: number = 10; //how many frames the dash lasts 21 | private static readonly DOWN_TILT: Vector3 = new Vector3(0.8290313946973066, 0, 0); 22 | private static readonly ORIGINAL_TILT: Vector3 = new Vector3(0.5934119456780721, 0, 0); 23 | public dashTime: number = 0; 24 | 25 | //player movement vars 26 | private _deltaTime: number = 0; 27 | private _h: number; 28 | private _v: number; 29 | 30 | private _moveDirection: Vector3 = new Vector3(); 31 | private _inputAmt: number; 32 | 33 | //dashing 34 | private _dashPressed: boolean; 35 | private _canDash: boolean = true; 36 | 37 | //gravity, ground detection, jumping 38 | private _gravity: Vector3 = new Vector3(); 39 | private _lastGroundPos: Vector3 = Vector3.Zero(); // keep track of the last grounded position 40 | private _grounded: boolean; 41 | private _jumpCount: number = 1; 42 | 43 | //player variables 44 | public lanternsLit: number = 1; //num lanterns lit 45 | public totalLanterns: number; 46 | public win: boolean = false; //whether the game is won 47 | 48 | //sparkler 49 | public sparkler: ParticleSystem; // sparkler particle system 50 | public sparkLit: boolean = true; 51 | public sparkReset: boolean = false; 52 | 53 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 54 | super("player", scene); 55 | this.scene = scene; 56 | this._setupPlayerCamera(); 57 | 58 | this.mesh = assets.mesh; 59 | this.mesh.parent = this; 60 | 61 | this.scene.getLightByName("sparklight").parent = this.scene.getTransformNodeByName("Empty"); 62 | 63 | //--COLLISIONS-- 64 | this.mesh.actionManager = new ActionManager(this.scene); 65 | //Platform destination 66 | this.mesh.actionManager.registerAction( 67 | new ExecuteCodeAction( 68 | { 69 | trigger: ActionManager.OnIntersectionEnterTrigger, 70 | parameter: this.scene.getMeshByName("destination") 71 | }, 72 | () => { 73 | if(this.lanternsLit == 22){ 74 | this.win = true; 75 | //tilt camera to look at where the fireworks will be displayed 76 | this._yTilt.rotation = new Vector3(5.689773361501514, 0.23736477827122882, 0); 77 | this._yTilt.position = new Vector3(0, 6, 0); 78 | this.camera.position.y = 17; 79 | } 80 | } 81 | ) 82 | ); 83 | 84 | //World ground detection 85 | //if player falls through "world", reset the position to the last safe grounded position 86 | this.mesh.actionManager.registerAction( 87 | new ExecuteCodeAction({ 88 | trigger: ActionManager.OnIntersectionEnterTrigger, 89 | parameter: this.scene.getMeshByName("ground") 90 | }, 91 | () => { 92 | this.mesh.position.copyFrom(this._lastGroundPos); // need to use copy or else they will be both pointing at the same thing & update together 93 | } 94 | ) 95 | ); 96 | 97 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 98 | 99 | this._input = input; 100 | } 101 | 102 | private _updateFromControls(): void { 103 | this._deltaTime = this.scene.getEngine().getDeltaTime() / 1000.0; 104 | 105 | this._moveDirection = Vector3.Zero(); // vector that holds movement information 106 | this._h = this._input.horizontal; //x-axis 107 | this._v = this._input.vertical; //z-axis 108 | 109 | if (this._input.dashing && !this._dashPressed && this._canDash && !this._grounded) { 110 | this._canDash = false; //we've started a dash, do not allow another 111 | this._dashPressed = true; //start the dash sequence 112 | } 113 | 114 | let dashFactor = 1; 115 | //if you're dashing, scale movement 116 | if (this._dashPressed) { 117 | if (this.dashTime > Player.DASH_TIME) { 118 | this.dashTime = 0; 119 | this._dashPressed = false; 120 | } else { 121 | dashFactor = Player.DASH_FACTOR; 122 | } 123 | this.dashTime++; 124 | } 125 | 126 | //--MOVEMENTS BASED ON CAMERA (as it rotates)-- 127 | let fwd = this._camRoot.forward; 128 | let right = this._camRoot.right; 129 | let correctedVertical = fwd.scaleInPlace(this._v); 130 | let correctedHorizontal = right.scaleInPlace(this._h); 131 | 132 | //movement based off of camera's view 133 | let move = correctedHorizontal.addInPlace(correctedVertical); 134 | 135 | //clear y so that the character doesnt fly up, normalize for next step, taking into account whether we've DASHED or not 136 | this._moveDirection = new Vector3((move).normalize().x * dashFactor, 0, (move).normalize().z * dashFactor); 137 | 138 | //clamp the input value so that diagonal movement isn't twice as fast 139 | let inputMag = Math.abs(this._h) + Math.abs(this._v); 140 | if (inputMag < 0) { 141 | this._inputAmt = 0; 142 | } else if (inputMag > 1) { 143 | this._inputAmt = 1; 144 | } else { 145 | this._inputAmt = inputMag; 146 | } 147 | 148 | //final movement that takes into consideration the inputs 149 | this._moveDirection = this._moveDirection.scaleInPlace(this._inputAmt * Player.PLAYER_SPEED); 150 | 151 | //Rotations 152 | //check if there is movement to determine if rotation is needed 153 | let input = new Vector3(this._input.horizontalAxis, 0, this._input.verticalAxis); //along which axis is the direction 154 | if (input.length() == 0) {//if there's no input detected, prevent rotation and keep player in same rotation 155 | return; 156 | } 157 | //rotation based on input & the camera angle 158 | let angle = Math.atan2(this._input.horizontalAxis, this._input.verticalAxis); 159 | angle += this._camRoot.rotation.y; 160 | let targ = Quaternion.FromEulerAngles(0, angle, 0); 161 | this.mesh.rotationQuaternion = Quaternion.Slerp(this.mesh.rotationQuaternion, targ, 10 * this._deltaTime); 162 | } 163 | 164 | private _floorRaycast(offsetx: number, offsetz: number, raycastlen: number): Vector3 { 165 | let raycastFloorPos = new Vector3(this.mesh.position.x + offsetx, this.mesh.position.y + 0.5, this.mesh.position.z + offsetz); 166 | let ray = new Ray(raycastFloorPos, Vector3.Up().scale(-1), raycastlen); 167 | 168 | let predicate = function (mesh) { 169 | return mesh.isPickable && mesh.isEnabled(); 170 | } 171 | let pick = this.scene.pickWithRay(ray, predicate); 172 | 173 | if (pick.hit) { 174 | return pick.pickedPoint; 175 | } else { 176 | return Vector3.Zero(); 177 | } 178 | } 179 | 180 | private _isGrounded(): boolean { 181 | if (this._floorRaycast(0, 0, 0.6).equals(Vector3.Zero())) { 182 | return false; 183 | } else { 184 | return true; 185 | } 186 | } 187 | 188 | private _checkSlope(): boolean { 189 | 190 | //only check meshes that are pickable and enabled (specific for collision meshes that are invisible) 191 | let predicate = function (mesh) { 192 | return mesh.isPickable && mesh.isEnabled(); 193 | } 194 | 195 | //4 raycasts outward from center 196 | let raycast = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z + .25); 197 | let ray = new Ray(raycast, Vector3.Up().scale(-1), 1.5); 198 | let pick = this.scene.pickWithRay(ray, predicate); 199 | 200 | let raycast2 = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z - .25); 201 | let ray2 = new Ray(raycast2, Vector3.Up().scale(-1), 1.5); 202 | let pick2 = this.scene.pickWithRay(ray2, predicate); 203 | 204 | let raycast3 = new Vector3(this.mesh.position.x + .25, this.mesh.position.y + 0.5, this.mesh.position.z); 205 | let ray3 = new Ray(raycast3, Vector3.Up().scale(-1), 1.5); 206 | let pick3 = this.scene.pickWithRay(ray3, predicate); 207 | 208 | let raycast4 = new Vector3(this.mesh.position.x - .25, this.mesh.position.y + 0.5, this.mesh.position.z); 209 | let ray4 = new Ray(raycast4, Vector3.Up().scale(-1), 1.5); 210 | let pick4 = this.scene.pickWithRay(ray4, predicate); 211 | 212 | if (pick.hit && !pick.getNormal().equals(Vector3.Up())) { 213 | if(pick.pickedMesh.name.includes("stair")) { 214 | return true; 215 | } 216 | } else if (pick2.hit && !pick2.getNormal().equals(Vector3.Up())) { 217 | if(pick2.pickedMesh.name.includes("stair")) { 218 | return true; 219 | } 220 | } 221 | else if (pick3.hit && !pick3.getNormal().equals(Vector3.Up())) { 222 | if(pick3.pickedMesh.name.includes("stair")) { 223 | return true; 224 | } 225 | } 226 | else if (pick4.hit && !pick4.getNormal().equals(Vector3.Up())) { 227 | if(pick4.pickedMesh.name.includes("stair")) { 228 | return true; 229 | } 230 | } 231 | return false; 232 | } 233 | 234 | private _updateGroundDetection(): void { 235 | if (!this._isGrounded()) { 236 | //if the body isnt grounded, check if it's on a slope and was either falling or walking onto it 237 | if (this._checkSlope() && this._gravity.y <= 0) { 238 | //if you are considered on a slope, you're able to jump and gravity wont affect you 239 | this._gravity.y = 0; 240 | this._jumpCount = 1; 241 | this._grounded = true; 242 | } else { 243 | //keep applying gravity 244 | this._gravity = this._gravity.addInPlace(Vector3.Up().scale(this._deltaTime * Player.GRAVITY)); 245 | this._grounded = false; 246 | } 247 | } 248 | //limit the speed of gravity to the negative of the jump power 249 | if (this._gravity.y < -Player.JUMP_FORCE) { 250 | this._gravity.y = -Player.JUMP_FORCE; 251 | } 252 | this.mesh.moveWithCollisions(this._moveDirection.addInPlace(this._gravity)); 253 | 254 | if (this._isGrounded()) { 255 | this._gravity.y = 0; 256 | this._grounded = true; 257 | this._lastGroundPos.copyFrom(this.mesh.position); 258 | 259 | this._jumpCount = 1; //allow for jumping 260 | //dashing reset 261 | this._canDash = true; //the ability to dash 262 | //reset sequence(needed if we collide with the ground BEFORE actually completing the dash duration) 263 | this.dashTime = 0; 264 | this._dashPressed = false; 265 | } 266 | 267 | //Jump detection 268 | if (this._input.jumpKeyDown && this._jumpCount > 0) { 269 | this._gravity.y = Player.JUMP_FORCE; 270 | this._jumpCount--; 271 | } 272 | 273 | 274 | } 275 | 276 | private _beforeRenderUpdate(): void { 277 | this._updateFromControls(); 278 | this._updateGroundDetection(); 279 | } 280 | 281 | public activatePlayerCamera(): UniversalCamera { 282 | this.scene.registerBeforeRender(() => { 283 | 284 | this._beforeRenderUpdate(); 285 | this._updateCamera(); 286 | 287 | }) 288 | return this.camera; 289 | } 290 | 291 | private _updateCamera(): void { 292 | //trigger areas for rotating camera view 293 | if (this.mesh.intersectsMesh(this.scene.getMeshByName("cornerTrigger"))) { 294 | if (this._input.horizontalAxis > 0) { //rotates to the right 295 | this._camRoot.rotation = Vector3.Lerp(this._camRoot.rotation, new Vector3(this._camRoot.rotation.x, Math.PI / 2, this._camRoot.rotation.z), 0.4); 296 | } else if (this._input.horizontalAxis < 0) { //rotates to the left 297 | this._camRoot.rotation = Vector3.Lerp(this._camRoot.rotation, new Vector3(this._camRoot.rotation.x, Math.PI, this._camRoot.rotation.z), 0.4); 298 | } 299 | } 300 | 301 | //rotates the camera to point down at the player when they enter the area, and returns it back to normal when they exit 302 | if (this.mesh.intersectsMesh(this.scene.getMeshByName("festivalTrigger"))) { 303 | if (this._input.verticalAxis > 0) { 304 | this._yTilt.rotation = Vector3.Lerp(this._yTilt.rotation, Player.DOWN_TILT, 0.4); 305 | } else if (this._input.verticalAxis < 0) { 306 | this._yTilt.rotation = Vector3.Lerp(this._yTilt.rotation, Player.ORIGINAL_TILT, 0.4); 307 | } 308 | } 309 | //once you've reached the destination area, return back to the original orientation, if they leave rotate it to the previous orientation 310 | if (this.mesh.intersectsMesh(this.scene.getMeshByName("destinationTrigger"))) { 311 | if (this._input.verticalAxis > 0) { 312 | this._yTilt.rotation = Vector3.Lerp(this._yTilt.rotation, Player.ORIGINAL_TILT, 0.4); 313 | } else if (this._input.verticalAxis < 0) { 314 | this._yTilt.rotation = Vector3.Lerp(this._yTilt.rotation, Player.DOWN_TILT, 0.4); 315 | } 316 | } 317 | 318 | let centerPlayer = this.mesh.position.y + 2; 319 | this._camRoot.position = Vector3.Lerp(this._camRoot.position, new Vector3(this.mesh.position.x, centerPlayer, this.mesh.position.z), 0.4); 320 | } 321 | 322 | private _setupPlayerCamera() { 323 | //root camera parent that handles positioning of the camera to follow the player 324 | this._camRoot = new TransformNode("root"); 325 | this._camRoot.position = new Vector3(0, 0, 0); //initialized at (0,0,0) 326 | //to face the player from behind (180 degrees) 327 | this._camRoot.rotation = new Vector3(0, Math.PI, 0); 328 | 329 | //rotations along the x-axis (up/down tilting) 330 | let yTilt = new TransformNode("ytilt"); 331 | //adjustments to camera view to point down at our player 332 | yTilt.rotation = Player.ORIGINAL_TILT; 333 | this._yTilt = yTilt; 334 | yTilt.parent = this._camRoot; 335 | 336 | //our actual camera that's pointing at our root's position 337 | this.camera = new UniversalCamera("cam", new Vector3(0, 0, -30), this.scene); 338 | this.camera.lockedTarget = this._camRoot.position; 339 | this.camera.fov = 0.47350045992678597; 340 | this.camera.parent = yTilt; 341 | 342 | this.scene.activeCamera = this.camera; 343 | return this.camera; 344 | } 345 | } -------------------------------------------------------------------------------- /tutorial/collisionsTriggers/environment.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, Vector3, SceneLoader, TransformNode, PBRMetallicRoughnessMaterial, ExecuteCodeAction, ActionManager, Texture, Color3 } from "@babylonjs/core"; 2 | import { Lantern } from "./lantern"; 3 | import { Player } from "./characterController"; 4 | 5 | export class Environment { 6 | private _scene: Scene; 7 | 8 | //Meshes 9 | private _lanternObjs: Array; //array of lanterns that need to be lit 10 | private _lightmtl: PBRMetallicRoughnessMaterial; // emissive texture for when lanterns are lit 11 | 12 | constructor(scene: Scene) { 13 | this._scene = scene; 14 | 15 | this._lanternObjs = []; 16 | //create emissive material for when lantern is lit 17 | const lightmtl = new PBRMetallicRoughnessMaterial("lantern mesh light", this._scene); 18 | lightmtl.emissiveTexture = new Texture("/textures/litLantern.png", this._scene, true, false); 19 | lightmtl.emissiveColor = new Color3(0.8784313725490196, 0.7568627450980392, 0.6235294117647059); 20 | this._lightmtl = lightmtl; 21 | } 22 | 23 | public async load() { 24 | // var ground = Mesh.CreateBox("ground", 24, this._scene); 25 | // ground.scaling = new Vector3(1,.02,1); 26 | 27 | const assets = await this._loadAsset(); 28 | //Loop through all environment meshes that were imported 29 | assets.allMeshes.forEach(m => { 30 | m.receiveShadows = true; 31 | m.checkCollisions = true; 32 | 33 | if (m.name == "ground") { //dont check for collisions, dont allow for raycasting to detect it(cant land on it) 34 | m.checkCollisions = false; 35 | m.isPickable = false; 36 | } 37 | //areas that will use box collisions 38 | if (m.name.includes("stairs") || m.name == "cityentranceground" || m.name == "fishingground.001" || m.name.includes("lilyflwr")) { 39 | m.checkCollisions = false; 40 | m.isPickable = false; 41 | } 42 | //collision meshes 43 | if (m.name.includes("collision")) { 44 | m.isVisible = false; 45 | m.isPickable = true; 46 | } 47 | //trigger meshes 48 | if (m.name.includes("Trigger")) { 49 | m.isVisible = false; 50 | m.isPickable = false; 51 | m.checkCollisions = false; 52 | } 53 | }); 54 | 55 | //--LANTERNS-- 56 | assets.lantern.isVisible = false; //original mesh is not visible 57 | //transform node to hold all lanterns 58 | const lanternHolder = new TransformNode("lanternHolder", this._scene); 59 | for (let i = 0; i < 22; i++) { 60 | //Mesh Cloning 61 | let lanternInstance = assets.lantern.clone("lantern" + i); //bring in imported lantern mesh & make clones 62 | lanternInstance.isVisible = true; 63 | lanternInstance.setParent(lanternHolder); 64 | 65 | //Create the new lantern object 66 | let newLantern = new Lantern(this._lightmtl, lanternInstance, this._scene, assets.env.getChildTransformNodes(false).find(m => m.name === "lantern " + i).getAbsolutePosition()); 67 | this._lanternObjs.push(newLantern); 68 | } 69 | //dispose of original mesh and animation group that were cloned 70 | assets.lantern.dispose(); 71 | } 72 | 73 | //Load all necessary meshes for the environment 74 | public async _loadAsset() { 75 | const result = await SceneLoader.ImportMeshAsync(null, "./models/", "envSetting.glb", this._scene); 76 | 77 | let env = result.meshes[0]; 78 | let allMeshes = env.getChildMeshes(); 79 | 80 | //loads lantern mesh 81 | const res = await SceneLoader.ImportMeshAsync("", "./models/", "lantern.glb", this._scene); 82 | 83 | //extract the actual lantern mesh from the root of the mesh that's imported, dispose of the root 84 | let lantern = res.meshes[0].getChildren()[0]; 85 | lantern.parent = null; 86 | res.meshes[0].dispose(); 87 | 88 | return { 89 | env: env, //reference to our entire imported glb (meshes and transform nodes) 90 | allMeshes: allMeshes, // all of the meshes that are in the environment 91 | lantern: lantern as Mesh 92 | } 93 | } 94 | 95 | public checkLanterns(player: Player) { 96 | if (!this._lanternObjs[0].isLit) { 97 | this._lanternObjs[0].setEmissiveTexture(); 98 | } 99 | 100 | this._lanternObjs.forEach(lantern => { 101 | player.mesh.actionManager.registerAction( 102 | new ExecuteCodeAction( 103 | { 104 | trigger: ActionManager.OnIntersectionEnterTrigger, 105 | parameter: lantern.mesh 106 | }, 107 | () => { 108 | //if the lantern is not lit, light it up & reset sparkler timer 109 | if (!lantern.isLit && player.sparkLit) { 110 | player.lanternsLit += 1; //increment the lantern count 111 | lantern.setEmissiveTexture(); //"light up" the lantern 112 | //reset the sparkler 113 | player.sparkReset = true; 114 | player.sparkLit = true; 115 | } 116 | //if the lantern is lit already, reset the sparkler 117 | else if (lantern.isLit) { 118 | player.sparkReset = true; 119 | player.sparkLit = true; 120 | } 121 | } 122 | ) 123 | ); 124 | }); 125 | } 126 | } -------------------------------------------------------------------------------- /tutorial/gui/ui.ts: -------------------------------------------------------------------------------- 1 | import { TextBlock, StackPanel, AdvancedDynamicTexture, Image, Button, Rectangle, Control, Grid } from "@babylonjs/gui"; 2 | import { Scene, Sound, ParticleSystem, PostProcess, Effect, SceneSerializer } from "@babylonjs/core"; 3 | 4 | export class Hud { 5 | private _scene: Scene; 6 | 7 | //Game Timer 8 | public time: number; //keep track to signal end game REAL TIME 9 | private _prevTime: number = 0; 10 | private _clockTime: TextBlock = null; //GAME TIME 11 | private _startTime: number; 12 | private _stopTimer: boolean; 13 | private _sString = "00"; 14 | private _mString = 11; 15 | private _lanternCnt: TextBlock; 16 | 17 | //Animated UI sprites 18 | private _sparklerLife: Image; 19 | private _spark: Image; 20 | 21 | //Timer handlers 22 | public stopSpark: boolean; 23 | private _handle; 24 | private _sparkhandle; 25 | 26 | //Pause toggle 27 | public gamePaused: boolean; 28 | 29 | //Quit game 30 | public quit: boolean; 31 | public transition: boolean = false; 32 | 33 | //UI Elements 34 | public pauseBtn: Button; 35 | public fadeLevel: number; 36 | private _playerUI; 37 | private _pauseMenu; 38 | private _controls; 39 | 40 | constructor(scene: Scene) { 41 | 42 | this._scene = scene; 43 | 44 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 45 | this._playerUI = playerUI; 46 | this._playerUI.idealHeight = 720; 47 | 48 | const lanternCnt = new TextBlock(); 49 | lanternCnt.name = "lantern count"; 50 | lanternCnt.textVerticalAlignment = TextBlock.VERTICAL_ALIGNMENT_CENTER; 51 | lanternCnt.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 52 | lanternCnt.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; 53 | lanternCnt.fontSize = "22px"; 54 | lanternCnt.color = "white"; 55 | lanternCnt.text = "Lanterns: 1 / 22"; 56 | lanternCnt.top = "32px"; 57 | lanternCnt.left = "-64px"; 58 | lanternCnt.width = "25%"; 59 | lanternCnt.fontFamily = "Viga"; 60 | lanternCnt.resizeToFit = true; 61 | playerUI.addControl(lanternCnt); 62 | this._lanternCnt = lanternCnt; 63 | 64 | const stackPanel = new StackPanel(); 65 | stackPanel.height = "100%"; 66 | stackPanel.width = "100%"; 67 | stackPanel.top = "14px"; 68 | stackPanel.verticalAlignment = 0; 69 | playerUI.addControl(stackPanel); 70 | 71 | //Game timer text 72 | const clockTime = new TextBlock(); 73 | clockTime.name = "clock"; 74 | clockTime.textHorizontalAlignment = TextBlock.HORIZONTAL_ALIGNMENT_CENTER; 75 | clockTime.fontSize = "48px"; 76 | clockTime.color = "white"; 77 | clockTime.text = "11:00"; 78 | clockTime.resizeToFit = true; 79 | clockTime.height = "96px"; 80 | clockTime.width = "220px"; 81 | clockTime.fontFamily = "Viga"; 82 | stackPanel.addControl(clockTime); 83 | this._clockTime = clockTime; 84 | 85 | //sparkler bar animation 86 | const sparklerLife = new Image("sparkLife", "./sprites/sparkLife.png"); 87 | sparklerLife.width = "54px"; 88 | sparklerLife.height = "162px"; 89 | sparklerLife.cellId = 0; 90 | sparklerLife.cellHeight = 108; 91 | sparklerLife.cellWidth = 36; 92 | sparklerLife.sourceWidth = 36; 93 | sparklerLife.sourceHeight = 108; 94 | sparklerLife.horizontalAlignment = 0; 95 | sparklerLife.verticalAlignment = 0; 96 | sparklerLife.left = "14px"; 97 | sparklerLife.top = "14px"; 98 | playerUI.addControl(sparklerLife); 99 | this._sparklerLife = sparklerLife; 100 | 101 | const spark = new Image("spark", "./sprites/spark.png"); 102 | spark.width = "40px"; 103 | spark.height = "40px"; 104 | spark.cellId = 0; 105 | spark.cellHeight = 20; 106 | spark.cellWidth = 20; 107 | spark.sourceWidth = 20; 108 | spark.sourceHeight = 20; 109 | spark.horizontalAlignment = 0; 110 | spark.verticalAlignment = 0; 111 | spark.left = "21px"; 112 | spark.top = "20px"; 113 | playerUI.addControl(spark); 114 | this._spark = spark; 115 | 116 | const pauseBtn = Button.CreateImageOnlyButton("pauseBtn", "./sprites/pauseBtn.png"); 117 | pauseBtn.width = "48px"; 118 | pauseBtn.height = "86px"; 119 | pauseBtn.thickness = 0; 120 | pauseBtn.verticalAlignment = 0; 121 | pauseBtn.horizontalAlignment = 1; 122 | pauseBtn.top = "-16px"; 123 | playerUI.addControl(pauseBtn); 124 | pauseBtn.zIndex = 10; 125 | this.pauseBtn = pauseBtn; 126 | //when the button is down, make pause menu visable and add control to it 127 | pauseBtn.onPointerDownObservable.add(() => { 128 | this._pauseMenu.isVisible = true; 129 | playerUI.addControl(this._pauseMenu); 130 | this.pauseBtn.isHitTestVisible = false; 131 | 132 | //when game is paused, make sure that the next start time is the time it was when paused 133 | this.gamePaused = true; 134 | this._prevTime = this.time; 135 | }); 136 | 137 | this._createPauseMenu(); 138 | this._createControlsMenu(); 139 | } 140 | 141 | public updateHud(): void { 142 | if (!this._stopTimer && this._startTime != null) { 143 | let curTime = Math.floor((new Date().getTime() - this._startTime) / 1000) + this._prevTime; // divide by 1000 to get seconds 144 | 145 | this.time = curTime; //keeps track of the total time elapsed in seconds 146 | this._clockTime.text = this._formatTime(curTime); 147 | } 148 | } 149 | 150 | public updateLanternCount(numLanterns: number): void { 151 | this._lanternCnt.text = "Lanterns: " + numLanterns + " / 22"; 152 | } 153 | //---- Game Timer ---- 154 | public startTimer(): void { 155 | this._startTime = new Date().getTime(); 156 | this._stopTimer = false; 157 | } 158 | public stopTimer(): void { 159 | this._stopTimer = true; 160 | } 161 | 162 | //format the time so that it is relative to 11:00 -- game time 163 | private _formatTime(time: number): string { 164 | let minsPassed = Math.floor(time / 60); //seconds in a min 165 | let secPassed = time % 240; // goes back to 0 after 4mins/240sec 166 | //gameclock works like: 4 mins = 1 hr 167 | // 4sec = 1/15 = 1min game time 168 | if (secPassed % 4 == 0) { 169 | this._mString = Math.floor(minsPassed / 4) + 11; 170 | this._sString = (secPassed / 4 < 10 ? "0" : "") + secPassed / 4; 171 | } 172 | let day = (this._mString == 11 ? " PM" : " AM"); 173 | return (this._mString + ":" + this._sString + day); 174 | } 175 | 176 | //---- Sparkler Timers ---- 177 | //start and restart sparkler, handles setting the texture and animation frame 178 | public startSparklerTimer(): void { 179 | //reset the sparkler timers & animation frames 180 | this.stopSpark = false; 181 | this._sparklerLife.cellId = 0; 182 | this._spark.cellId = 0; 183 | if (this._handle) { 184 | clearInterval(this._handle); 185 | } 186 | if (this._sparkhandle) { 187 | clearInterval(this._sparkhandle); 188 | } 189 | 190 | this._scene.getLightByName("sparklight").intensity = 35; 191 | 192 | //sparkler animation, every 2 seconds update for 10 bars of sparklife 193 | this._handle = setInterval(() => { 194 | if (!this.gamePaused) { 195 | if (this._sparklerLife.cellId < 10) { 196 | this._sparklerLife.cellId++; 197 | } 198 | if (this._sparklerLife.cellId == 10) { 199 | this.stopSpark = true; 200 | clearInterval(this._handle); 201 | 202 | } 203 | } 204 | }, 2000); 205 | 206 | this._sparkhandle = setInterval(() => { 207 | if (!this.gamePaused) { 208 | if (this._sparklerLife.cellId < 10 && this._spark.cellId < 5) { 209 | this._spark.cellId++; 210 | } else if (this._sparklerLife.cellId < 10 && this._spark.cellId >= 5) { 211 | this._spark.cellId = 0; 212 | } 213 | else { 214 | this._spark.cellId = 0; 215 | clearInterval(this._sparkhandle); 216 | } 217 | } 218 | }, 185); 219 | } 220 | 221 | //stop the sparkler, resets the texture 222 | public stopSparklerTimer(): void { 223 | this.stopSpark = true; 224 | this._scene.getLightByName("sparklight").intensity = 0; 225 | } 226 | 227 | //---- Pause Menu Popup ---- 228 | private _createPauseMenu(): void { 229 | this.gamePaused = false; 230 | 231 | const pauseMenu = new Rectangle(); 232 | pauseMenu.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; 233 | pauseMenu.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; 234 | pauseMenu.height = 0.8; 235 | pauseMenu.width = 0.5; 236 | pauseMenu.thickness = 0; 237 | pauseMenu.isVisible = false; 238 | 239 | //background image 240 | const image = new Image("pause", "sprites/pause.jpeg"); 241 | pauseMenu.addControl(image); 242 | 243 | //stack panel for the buttons 244 | const stackPanel = new StackPanel(); 245 | stackPanel.width = .83; 246 | pauseMenu.addControl(stackPanel); 247 | 248 | const resumeBtn = Button.CreateSimpleButton("resume", "RESUME"); 249 | resumeBtn.width = 0.18; 250 | resumeBtn.height = "44px"; 251 | resumeBtn.color = "white"; 252 | resumeBtn.fontFamily = "Viga"; 253 | resumeBtn.paddingBottom = "14px"; 254 | resumeBtn.cornerRadius = 14; 255 | resumeBtn.fontSize = "12px"; 256 | resumeBtn.textBlock.resizeToFit = true; 257 | resumeBtn.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; 258 | resumeBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; 259 | stackPanel.addControl(resumeBtn); 260 | 261 | this._pauseMenu = pauseMenu; 262 | 263 | //when the button is down, make menu invisable and remove control of the menu 264 | resumeBtn.onPointerDownObservable.add(() => { 265 | this._pauseMenu.isVisible = false; 266 | this._playerUI.removeControl(pauseMenu); 267 | this.pauseBtn.isHitTestVisible = true; 268 | 269 | //game unpaused, our time is now reset 270 | this.gamePaused = false; 271 | this._startTime = new Date().getTime(); 272 | }); 273 | 274 | const controlsBtn = Button.CreateSimpleButton("controls", "CONTROLS"); 275 | controlsBtn.width = 0.18; 276 | controlsBtn.height = "44px"; 277 | controlsBtn.color = "white"; 278 | controlsBtn.fontFamily = "Viga"; 279 | controlsBtn.paddingBottom = "14px"; 280 | controlsBtn.cornerRadius = 14; 281 | controlsBtn.fontSize = "12px"; 282 | resumeBtn.textBlock.resizeToFit = true; 283 | controlsBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; 284 | controlsBtn.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; 285 | 286 | stackPanel.addControl(controlsBtn); 287 | 288 | //when the button is down, make menu invisable and remove control of the menu 289 | controlsBtn.onPointerDownObservable.add(() => { 290 | //open controls screen 291 | this._controls.isVisible = true; 292 | this._pauseMenu.isVisible = false; 293 | }); 294 | 295 | const quitBtn = Button.CreateSimpleButton("quit", "QUIT"); 296 | quitBtn.width = 0.18; 297 | quitBtn.height = "44px"; 298 | quitBtn.color = "white"; 299 | quitBtn.fontFamily = "Viga"; 300 | quitBtn.paddingBottom = "12px"; 301 | quitBtn.cornerRadius = 14; 302 | quitBtn.fontSize = "12px"; 303 | resumeBtn.textBlock.resizeToFit = true; 304 | quitBtn.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; 305 | quitBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; 306 | stackPanel.addControl(quitBtn); 307 | 308 | //set up transition effect 309 | Effect.RegisterShader("fade", 310 | "precision highp float;" + 311 | "varying vec2 vUV;" + 312 | "uniform sampler2D textureSampler; " + 313 | "uniform float fadeLevel; " + 314 | "void main(void){" + 315 | "vec4 baseColor = texture2D(textureSampler, vUV) * fadeLevel;" + 316 | "baseColor.a = 1.0;" + 317 | "gl_FragColor = baseColor;" + 318 | "}"); 319 | this.fadeLevel = 1.0; 320 | 321 | quitBtn.onPointerDownObservable.add(() => { 322 | const postProcess = new PostProcess("Fade", "fade", ["fadeLevel"], null, 1.0, this._scene.getCameraByName("cam")); 323 | postProcess.onApply = (effect) => { 324 | effect.setFloat("fadeLevel", this.fadeLevel); 325 | }; 326 | this.transition = true; 327 | }) 328 | } 329 | 330 | //---- Controls Menu Popup ---- 331 | private _createControlsMenu(): void { 332 | const controls = new Rectangle(); 333 | controls.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; 334 | controls.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; 335 | controls.height = 0.8; 336 | controls.width = 0.5; 337 | controls.thickness = 0; 338 | controls.color = "white"; 339 | controls.isVisible = false; 340 | this._playerUI.addControl(controls); 341 | this._controls = controls; 342 | 343 | //background image 344 | const image = new Image("controls", "sprites/controls.jpeg"); 345 | controls.addControl(image); 346 | 347 | const title = new TextBlock("title", "CONTROLS"); 348 | title.resizeToFit = true; 349 | title.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; 350 | title.fontFamily = "Viga"; 351 | title.fontSize = "32px"; 352 | title.top = "14px"; 353 | controls.addControl(title); 354 | 355 | const backBtn = Button.CreateImageOnlyButton("back", "./sprites/lanternbutton.jpeg"); 356 | backBtn.width = "40px"; 357 | backBtn.height = "40px"; 358 | backBtn.top = "14px"; 359 | backBtn.thickness = 0; 360 | backBtn.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 361 | backBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; 362 | controls.addControl(backBtn); 363 | 364 | //when the button is down, make menu invisable and remove control of the menu 365 | backBtn.onPointerDownObservable.add(() => { 366 | this._pauseMenu.isVisible = true; 367 | this._controls.isVisible = false; 368 | }); 369 | } 370 | } -------------------------------------------------------------------------------- /tutorial/importMeshes/app.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4, StandardMaterial, Color3, PointLight, ShadowGenerator, Quaternion, Matrix, SceneLoader } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | import { Environment } from "./environment"; 7 | import { Player } from "./characterController"; 8 | import { PlayerInput } from "./inputController"; 9 | 10 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 11 | 12 | class App { 13 | // General Entire Application 14 | private _scene: Scene; 15 | private _canvas: HTMLCanvasElement; 16 | private _engine: Engine; 17 | 18 | //Game State Related 19 | public assets; 20 | private _input: PlayerInput; 21 | private _environment; 22 | private _player: Player; 23 | 24 | 25 | //Scene - related 26 | private _state: number = 0; 27 | private _gamescene: Scene; 28 | private _cutScene: Scene; 29 | 30 | constructor() { 31 | this._canvas = this._createCanvas(); 32 | 33 | // initialize babylon scene and engine 34 | this._engine = new Engine(this._canvas, true); 35 | this._scene = new Scene(this._engine); 36 | 37 | // hide/show the Inspector 38 | window.addEventListener("keydown", (ev) => { 39 | // Shift+Ctrl+Alt+I 40 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 41 | if (this._scene.debugLayer.isVisible()) { 42 | this._scene.debugLayer.hide(); 43 | } else { 44 | this._scene.debugLayer.show(); 45 | } 46 | } 47 | }); 48 | 49 | // run the main render loop 50 | this._main(); 51 | } 52 | 53 | private _createCanvas(): HTMLCanvasElement { 54 | 55 | //Commented out for development 56 | // document.documentElement.style["overflow"] = "hidden"; 57 | // document.documentElement.style.overflow = "hidden"; 58 | // document.documentElement.style.width = "100%"; 59 | // document.documentElement.style.height = "100%"; 60 | // document.documentElement.style.margin = "0"; 61 | // document.documentElement.style.padding = "0"; 62 | // document.body.style.overflow = "hidden"; 63 | // document.body.style.width = "100%"; 64 | // document.body.style.height = "100%"; 65 | // document.body.style.margin = "0"; 66 | // document.body.style.padding = "0"; 67 | 68 | //create the canvas html element and attach it to the webpage 69 | this._canvas = document.createElement("canvas"); 70 | this._canvas.style.width = "100%"; 71 | this._canvas.style.height = "100%"; 72 | this._canvas.id = "gameCanvas"; 73 | document.body.appendChild(this._canvas); 74 | 75 | return this._canvas; 76 | } 77 | 78 | private async _main(): Promise { 79 | await this._goToStart(); 80 | 81 | // Register a render loop to repeatedly render the scene 82 | this._engine.runRenderLoop(() => { 83 | switch (this._state) { 84 | case State.START: 85 | this._scene.render(); 86 | break; 87 | case State.CUTSCENE: 88 | this._scene.render(); 89 | break; 90 | case State.GAME: 91 | this._scene.render(); 92 | break; 93 | case State.LOSE: 94 | this._scene.render(); 95 | break; 96 | default: break; 97 | } 98 | }); 99 | 100 | //resize if the screen is resized/rotated 101 | window.addEventListener('resize', () => { 102 | this._engine.resize(); 103 | }); 104 | } 105 | private async _goToStart(){ 106 | this._engine.displayLoadingUI(); 107 | 108 | this._scene.detachControl(); 109 | let scene = new Scene(this._engine); 110 | scene.clearColor = new Color4(0,0,0,1); 111 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 112 | camera.setTarget(Vector3.Zero()); 113 | 114 | //create a fullscreen ui for all of our GUI elements 115 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 116 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 117 | 118 | //create a simple button 119 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 120 | startBtn.width = 0.2 121 | startBtn.height = "40px"; 122 | startBtn.color = "white"; 123 | startBtn.top = "-14px"; 124 | startBtn.thickness = 0; 125 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 126 | guiMenu.addControl(startBtn); 127 | 128 | //this handles interactions with the start button attached to the scene 129 | startBtn.onPointerDownObservable.add(() => { 130 | this._goToCutScene(); 131 | scene.detachControl(); //observables disabled 132 | }); 133 | 134 | //--SCENE FINISHED LOADING-- 135 | await scene.whenReadyAsync(); 136 | this._engine.hideLoadingUI(); 137 | //lastly set the current state to the start state and set the scene to the start scene 138 | this._scene.dispose(); 139 | this._scene = scene; 140 | this._state = State.START; 141 | } 142 | 143 | private async _goToCutScene(): Promise { 144 | this._engine.displayLoadingUI(); 145 | //--SETUP SCENE-- 146 | //dont detect any inputs from this ui while the game is loading 147 | this._scene.detachControl(); 148 | this._cutScene = new Scene(this._engine); 149 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 150 | camera.setTarget(Vector3.Zero()); 151 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 152 | 153 | //--GUI-- 154 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 155 | 156 | //--PROGRESS DIALOGUE-- 157 | const next = Button.CreateSimpleButton("next", "NEXT"); 158 | next.color = "white"; 159 | next.thickness = 0; 160 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 161 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 162 | next.width = "64px"; 163 | next.height = "64px"; 164 | next.top = "-3%"; 165 | next.left = "-12%"; 166 | cutScene.addControl(next); 167 | 168 | next.onPointerUpObservable.add(() => { 169 | // this._goToGame(); 170 | }) 171 | 172 | //--WHEN SCENE IS FINISHED LOADING-- 173 | await this._cutScene.whenReadyAsync(); 174 | this._engine.hideLoadingUI(); 175 | this._scene.dispose(); 176 | this._state = State.CUTSCENE; 177 | this._scene = this._cutScene; 178 | 179 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 180 | var finishedLoading = false; 181 | await this._setUpGame().then(res =>{ 182 | finishedLoading = true; 183 | this._goToGame(); 184 | }); 185 | } 186 | 187 | private async _setUpGame() { 188 | let scene = new Scene(this._engine); 189 | this._gamescene = scene; 190 | 191 | //--CREATE ENVIRONMENT-- 192 | const environment = new Environment(scene); 193 | this._environment = environment; 194 | await this._environment.load(); //environment 195 | await this._loadCharacterAssets(scene); 196 | } 197 | 198 | private async _loadCharacterAssets(scene){ 199 | 200 | async function loadCharacter(){ 201 | //collision mesh 202 | const outer = MeshBuilder.CreateBox("outer", { width: 2, depth: 1, height: 3 }, scene); 203 | outer.isVisible = false; 204 | outer.isPickable = false; 205 | outer.checkCollisions = true; 206 | 207 | //move origin of box collider to the bottom of the mesh (to match player mesh) 208 | outer.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)) 209 | 210 | //for collisions 211 | outer.ellipsoid = new Vector3(1, 1.5, 1); 212 | outer.ellipsoidOffset = new Vector3(0, 1.5, 0); 213 | 214 | outer.rotationQuaternion = new Quaternion(0, 1, 0, 0); // rotate the player mesh 180 since we want to see the back of the player 215 | 216 | return SceneLoader.ImportMeshAsync(null, "./models/", "player.glb", scene).then((result) =>{ 217 | const root = result.meshes[0]; 218 | //body is our actual player mesh 219 | const body = root; 220 | body.parent = outer; 221 | body.isPickable = false; //so our raycasts dont hit ourself 222 | body.getChildMeshes().forEach(m => { 223 | m.isPickable = false; 224 | }) 225 | 226 | return { 227 | mesh: outer as Mesh, 228 | } 229 | }); 230 | } 231 | return loadCharacter().then(assets=> { 232 | this.assets = assets; 233 | }) 234 | 235 | } 236 | 237 | private async _initializeGameAsync(scene): Promise { 238 | //temporary light to light the entire scene 239 | var light0 = new HemisphericLight("HemiLight", new Vector3(0, 1, 0), scene); 240 | 241 | const light = new PointLight("sparklight", new Vector3(0, 0, 0), scene); 242 | light.diffuse = new Color3(0.08627450980392157, 0.10980392156862745, 0.15294117647058825); 243 | light.intensity = 35; 244 | light.radius = 1; 245 | 246 | const shadowGenerator = new ShadowGenerator(1024, light); 247 | shadowGenerator.darkness = 0.4; 248 | 249 | //Create the player 250 | this._player = new Player(this.assets, scene, shadowGenerator, this._input); 251 | const camera = this._player.activatePlayerCamera(); 252 | } 253 | 254 | private async _goToGame(){ 255 | //--SETUP SCENE-- 256 | this._scene.detachControl(); 257 | let scene = this._gamescene; 258 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 259 | 260 | //--GUI-- 261 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 262 | //dont detect any inputs from this ui while the game is loading 263 | scene.detachControl(); 264 | 265 | //create a simple button 266 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 267 | loseBtn.width = 0.2 268 | loseBtn.height = "40px"; 269 | loseBtn.color = "white"; 270 | loseBtn.top = "-14px"; 271 | loseBtn.thickness = 0; 272 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 273 | playerUI.addControl(loseBtn); 274 | 275 | //this handles interactions with the start button attached to the scene 276 | loseBtn.onPointerDownObservable.add(() => { 277 | this._goToLose(); 278 | scene.detachControl(); //observables disabled 279 | }); 280 | 281 | //--INPUT-- 282 | this._input = new PlayerInput(scene); //detect keyboard/mobile inputs 283 | 284 | //primitive character and setting 285 | await this._initializeGameAsync(scene); 286 | 287 | //--WHEN SCENE FINISHED LOADING-- 288 | await scene.whenReadyAsync(); 289 | scene.getMeshByName("outer").position = scene.getTransformNodeByName("startPosition").getAbsolutePosition(); //move the player to the start position 290 | //get rid of start scene, switch to gamescene and change states 291 | this._scene.dispose(); 292 | this._state = State.GAME; 293 | this._scene = scene; 294 | this._engine.hideLoadingUI(); 295 | //the game is ready, attach control back 296 | this._scene.attachControl(); 297 | } 298 | 299 | private async _goToLose(): Promise { 300 | this._engine.displayLoadingUI(); 301 | 302 | //--SCENE SETUP-- 303 | this._scene.detachControl(); 304 | let scene = new Scene(this._engine); 305 | scene.clearColor = new Color4(0, 0, 0, 1); 306 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 307 | camera.setTarget(Vector3.Zero()); 308 | 309 | //--GUI-- 310 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 311 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 312 | mainBtn.width = 0.2; 313 | mainBtn.height = "40px"; 314 | mainBtn.color = "white"; 315 | guiMenu.addControl(mainBtn); 316 | //this handles interactions with the start button attached to the scene 317 | mainBtn.onPointerUpObservable.add(() => { 318 | this._goToStart(); 319 | }); 320 | 321 | //--SCENE FINISHED LOADING-- 322 | await scene.whenReadyAsync(); 323 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 324 | //lastly set the current state to the lose state and set the scene to the lose scene 325 | this._scene.dispose(); 326 | this._scene = scene; 327 | this._state = State.LOSE; 328 | } 329 | } 330 | new App(); -------------------------------------------------------------------------------- /tutorial/importMeshes/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3, Quaternion, Ray } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | //Camera 12 | private _camRoot: TransformNode; 13 | private _yTilt: TransformNode; 14 | 15 | //const values 16 | private static readonly PLAYER_SPEED: number = 0.45; 17 | private static readonly JUMP_FORCE: number = 0.80; 18 | private static readonly GRAVITY: number = -2.8; 19 | private static readonly DASH_FACTOR: number = 2.5; 20 | private static readonly DASH_TIME: number = 10; //how many frames the dash lasts 21 | private static readonly ORIGINAL_TILT: Vector3 = new Vector3(0.5934119456780721, 0, 0); 22 | public dashTime: number = 0; 23 | 24 | //player movement vars 25 | private _deltaTime: number = 0; 26 | private _h: number; 27 | private _v: number; 28 | 29 | private _moveDirection: Vector3 = new Vector3(); 30 | private _inputAmt: number; 31 | 32 | //dashing 33 | private _dashPressed: boolean; 34 | private _canDash: boolean = true; 35 | 36 | //gravity, ground detection, jumping 37 | private _gravity: Vector3 = new Vector3(); 38 | private _lastGroundPos: Vector3 = Vector3.Zero(); // keep track of the last grounded position 39 | private _grounded: boolean; 40 | private _jumpCount: number = 1; 41 | 42 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 43 | super("player", scene); 44 | this.scene = scene; 45 | this._setupPlayerCamera(); 46 | 47 | this.mesh = assets.mesh; 48 | this.mesh.parent = this; 49 | 50 | this.scene.getLightByName("sparklight").parent = this.scene.getTransformNodeByName("Empty"); 51 | 52 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 53 | 54 | this._input = input; 55 | } 56 | 57 | private _updateFromControls(): void { 58 | this._deltaTime = this.scene.getEngine().getDeltaTime() / 1000.0; 59 | 60 | this._moveDirection = Vector3.Zero(); // vector that holds movement information 61 | this._h = this._input.horizontal; //x-axis 62 | this._v = this._input.vertical; //z-axis 63 | 64 | if (this._input.dashing && !this._dashPressed && this._canDash && !this._grounded) { 65 | this._canDash = false; //we've started a dash, do not allow another 66 | this._dashPressed = true; //start the dash sequence 67 | } 68 | 69 | let dashFactor = 1; 70 | //if you're dashing, scale movement 71 | if (this._dashPressed) { 72 | if (this.dashTime > Player.DASH_TIME) { 73 | this.dashTime = 0; 74 | this._dashPressed = false; 75 | } else { 76 | dashFactor = Player.DASH_FACTOR; 77 | } 78 | this.dashTime++; 79 | } 80 | 81 | //--MOVEMENTS BASED ON CAMERA (as it rotates)-- 82 | let fwd = this._camRoot.forward; 83 | let right = this._camRoot.right; 84 | let correctedVertical = fwd.scaleInPlace(this._v); 85 | let correctedHorizontal = right.scaleInPlace(this._h); 86 | 87 | //movement based off of camera's view 88 | let move = correctedHorizontal.addInPlace(correctedVertical); 89 | 90 | //clear y so that the character doesnt fly up, normalize for next step, taking into account whether we've DASHED or not 91 | this._moveDirection = new Vector3((move).normalize().x * dashFactor, 0, (move).normalize().z * dashFactor); 92 | 93 | //clamp the input value so that diagonal movement isn't twice as fast 94 | let inputMag = Math.abs(this._h) + Math.abs(this._v); 95 | if (inputMag < 0) { 96 | this._inputAmt = 0; 97 | } else if (inputMag > 1) { 98 | this._inputAmt = 1; 99 | } else { 100 | this._inputAmt = inputMag; 101 | } 102 | 103 | //final movement that takes into consideration the inputs 104 | this._moveDirection = this._moveDirection.scaleInPlace(this._inputAmt * Player.PLAYER_SPEED); 105 | 106 | //Rotations 107 | //check if there is movement to determine if rotation is needed 108 | let input = new Vector3(this._input.horizontalAxis, 0, this._input.verticalAxis); //along which axis is the direction 109 | if (input.length() == 0) {//if there's no input detected, prevent rotation and keep player in same rotation 110 | return; 111 | } 112 | //rotation based on input & the camera angle 113 | let angle = Math.atan2(this._input.horizontalAxis, this._input.verticalAxis); 114 | angle += this._camRoot.rotation.y; 115 | let targ = Quaternion.FromEulerAngles(0, angle, 0); 116 | this.mesh.rotationQuaternion = Quaternion.Slerp(this.mesh.rotationQuaternion, targ, 10 * this._deltaTime); 117 | } 118 | 119 | private _floorRaycast(offsetx: number, offsetz: number, raycastlen: number): Vector3 { 120 | let raycastFloorPos = new Vector3(this.mesh.position.x + offsetx, this.mesh.position.y + 0.5, this.mesh.position.z + offsetz); 121 | let ray = new Ray(raycastFloorPos, Vector3.Up().scale(-1), raycastlen); 122 | 123 | let predicate = function (mesh) { 124 | return mesh.isPickable && mesh.isEnabled(); 125 | } 126 | let pick = this.scene.pickWithRay(ray, predicate); 127 | 128 | if (pick.hit) { 129 | return pick.pickedPoint; 130 | } else { 131 | return Vector3.Zero(); 132 | } 133 | } 134 | 135 | private _isGrounded(): boolean { 136 | if (this._floorRaycast(0, 0, 0.6).equals(Vector3.Zero())) { 137 | return false; 138 | } else { 139 | return true; 140 | } 141 | } 142 | 143 | private _checkSlope(): boolean { 144 | 145 | //only check meshes that are pickable and enabled (specific for collision meshes that are invisible) 146 | let predicate = function (mesh) { 147 | return mesh.isPickable && mesh.isEnabled(); 148 | } 149 | 150 | //4 raycasts outward from center 151 | let raycast = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z + .25); 152 | let ray = new Ray(raycast, Vector3.Up().scale(-1), 1.5); 153 | let pick = this.scene.pickWithRay(ray, predicate); 154 | 155 | let raycast2 = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z - .25); 156 | let ray2 = new Ray(raycast2, Vector3.Up().scale(-1), 1.5); 157 | let pick2 = this.scene.pickWithRay(ray2, predicate); 158 | 159 | let raycast3 = new Vector3(this.mesh.position.x + .25, this.mesh.position.y + 0.5, this.mesh.position.z); 160 | let ray3 = new Ray(raycast3, Vector3.Up().scale(-1), 1.5); 161 | let pick3 = this.scene.pickWithRay(ray3, predicate); 162 | 163 | let raycast4 = new Vector3(this.mesh.position.x - .25, this.mesh.position.y + 0.5, this.mesh.position.z); 164 | let ray4 = new Ray(raycast4, Vector3.Up().scale(-1), 1.5); 165 | let pick4 = this.scene.pickWithRay(ray4, predicate); 166 | 167 | if (pick.hit && !pick.getNormal().equals(Vector3.Up())) { 168 | if(pick.pickedMesh.name.includes("stair")) { 169 | return true; 170 | } 171 | } else if (pick2.hit && !pick2.getNormal().equals(Vector3.Up())) { 172 | if(pick2.pickedMesh.name.includes("stair")) { 173 | return true; 174 | } 175 | } 176 | else if (pick3.hit && !pick3.getNormal().equals(Vector3.Up())) { 177 | if(pick3.pickedMesh.name.includes("stair")) { 178 | return true; 179 | } 180 | } 181 | else if (pick4.hit && !pick4.getNormal().equals(Vector3.Up())) { 182 | if(pick4.pickedMesh.name.includes("stair")) { 183 | return true; 184 | } 185 | } 186 | return false; 187 | } 188 | 189 | private _updateGroundDetection(): void { 190 | if (!this._isGrounded()) { 191 | //if the body isnt grounded, check if it's on a slope and was either falling or walking onto it 192 | if (this._checkSlope() && this._gravity.y <= 0) { 193 | //if you are considered on a slope, you're able to jump and gravity wont affect you 194 | this._gravity.y = 0; 195 | this._jumpCount = 1; 196 | this._grounded = true; 197 | } else { 198 | //keep applying gravity 199 | this._gravity = this._gravity.addInPlace(Vector3.Up().scale(this._deltaTime * Player.GRAVITY)); 200 | this._grounded = false; 201 | } 202 | } 203 | //limit the speed of gravity to the negative of the jump power 204 | if (this._gravity.y < -Player.JUMP_FORCE) { 205 | this._gravity.y = -Player.JUMP_FORCE; 206 | } 207 | this.mesh.moveWithCollisions(this._moveDirection.addInPlace(this._gravity)); 208 | 209 | if (this._isGrounded()) { 210 | this._gravity.y = 0; 211 | this._grounded = true; 212 | this._lastGroundPos.copyFrom(this.mesh.position); 213 | 214 | this._jumpCount = 1; //allow for jumping 215 | //dashing reset 216 | this._canDash = true; //the ability to dash 217 | //reset sequence(needed if we collide with the ground BEFORE actually completing the dash duration) 218 | this.dashTime = 0; 219 | this._dashPressed = false; 220 | } 221 | 222 | //Jump detection 223 | if (this._input.jumpKeyDown && this._jumpCount > 0) { 224 | this._gravity.y = Player.JUMP_FORCE; 225 | this._jumpCount--; 226 | } 227 | 228 | 229 | } 230 | 231 | private _beforeRenderUpdate(): void { 232 | this._updateFromControls(); 233 | this._updateGroundDetection(); 234 | } 235 | 236 | public activatePlayerCamera(): UniversalCamera { 237 | this.scene.registerBeforeRender(() => { 238 | 239 | this._beforeRenderUpdate(); 240 | this._updateCamera(); 241 | 242 | }) 243 | return this.camera; 244 | } 245 | 246 | private _updateCamera(): void { 247 | let centerPlayer = this.mesh.position.y + 2; 248 | this._camRoot.position = Vector3.Lerp(this._camRoot.position, new Vector3(this.mesh.position.x, centerPlayer, this.mesh.position.z), 0.4); 249 | } 250 | 251 | private _setupPlayerCamera() { 252 | //root camera parent that handles positioning of the camera to follow the player 253 | this._camRoot = new TransformNode("root"); 254 | this._camRoot.position = new Vector3(0, 0, 0); //initialized at (0,0,0) 255 | //to face the player from behind (180 degrees) 256 | this._camRoot.rotation = new Vector3(0, Math.PI, 0); 257 | 258 | //rotations along the x-axis (up/down tilting) 259 | let yTilt = new TransformNode("ytilt"); 260 | //adjustments to camera view to point down at our player 261 | yTilt.rotation = Player.ORIGINAL_TILT; 262 | this._yTilt = yTilt; 263 | yTilt.parent = this._camRoot; 264 | 265 | //our actual camera that's pointing at our root's position 266 | this.camera = new UniversalCamera("cam", new Vector3(0, 0, -30), this.scene); 267 | this.camera.lockedTarget = this._camRoot.position; 268 | this.camera.fov = 0.47350045992678597; 269 | this.camera.parent = yTilt; 270 | 271 | this.scene.activeCamera = this.camera; 272 | return this.camera; 273 | } 274 | } -------------------------------------------------------------------------------- /tutorial/importMeshes/environment.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, Vector3, SceneLoader } from "@babylonjs/core"; 2 | 3 | export class Environment { 4 | private _scene: Scene; 5 | 6 | constructor(scene: Scene) { 7 | this._scene = scene; 8 | } 9 | 10 | public async load() { 11 | // var ground = Mesh.CreateBox("ground", 24, this._scene); 12 | // ground.scaling = new Vector3(1,.02,1); 13 | 14 | const assets = await this._loadAsset(); 15 | //Loop through all environment meshes that were imported 16 | assets.allMeshes.forEach(m => { 17 | m.receiveShadows = true; 18 | m.checkCollisions = true; 19 | }); 20 | } 21 | 22 | //Load all necessary meshes for the environment 23 | public async _loadAsset() { 24 | const result = await SceneLoader.ImportMeshAsync(null, "./models/", "envSetting.glb", this._scene); 25 | 26 | let env = result.meshes[0]; 27 | let allMeshes = env.getChildMeshes(); 28 | 29 | return { 30 | env: env, //reference to our entire imported glb (meshes and transform nodes) 31 | allMeshes: allMeshes // all of the meshes that are in the environment 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /tutorial/lanterns/app.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4, StandardMaterial, Color3, PointLight, ShadowGenerator, Quaternion, Matrix, SceneLoader } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | import { Environment } from "./environment"; 7 | import { Player } from "./characterController"; 8 | import { PlayerInput } from "./inputController"; 9 | 10 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 11 | 12 | class App { 13 | // General Entire Application 14 | private _scene: Scene; 15 | private _canvas: HTMLCanvasElement; 16 | private _engine: Engine; 17 | 18 | //Game State Related 19 | public assets; 20 | private _input: PlayerInput; 21 | private _environment; 22 | private _player: Player; 23 | 24 | 25 | //Scene - related 26 | private _state: number = 0; 27 | private _gamescene: Scene; 28 | private _cutScene: Scene; 29 | 30 | constructor() { 31 | this._canvas = this._createCanvas(); 32 | 33 | // initialize babylon scene and engine 34 | this._engine = new Engine(this._canvas, true); 35 | this._scene = new Scene(this._engine); 36 | 37 | // hide/show the Inspector 38 | window.addEventListener("keydown", (ev) => { 39 | // Shift+Ctrl+Alt+I 40 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 41 | if (this._scene.debugLayer.isVisible()) { 42 | this._scene.debugLayer.hide(); 43 | } else { 44 | this._scene.debugLayer.show(); 45 | } 46 | } 47 | }); 48 | 49 | // run the main render loop 50 | this._main(); 51 | } 52 | 53 | private _createCanvas(): HTMLCanvasElement { 54 | 55 | //Commented out for development 56 | // document.documentElement.style["overflow"] = "hidden"; 57 | // document.documentElement.style.overflow = "hidden"; 58 | // document.documentElement.style.width = "100%"; 59 | // document.documentElement.style.height = "100%"; 60 | // document.documentElement.style.margin = "0"; 61 | // document.documentElement.style.padding = "0"; 62 | // document.body.style.overflow = "hidden"; 63 | // document.body.style.width = "100%"; 64 | // document.body.style.height = "100%"; 65 | // document.body.style.margin = "0"; 66 | // document.body.style.padding = "0"; 67 | 68 | //create the canvas html element and attach it to the webpage 69 | this._canvas = document.createElement("canvas"); 70 | this._canvas.style.width = "100%"; 71 | this._canvas.style.height = "100%"; 72 | this._canvas.id = "gameCanvas"; 73 | document.body.appendChild(this._canvas); 74 | 75 | return this._canvas; 76 | } 77 | 78 | private async _main(): Promise { 79 | await this._goToStart(); 80 | 81 | // Register a render loop to repeatedly render the scene 82 | this._engine.runRenderLoop(() => { 83 | switch (this._state) { 84 | case State.START: 85 | this._scene.render(); 86 | break; 87 | case State.CUTSCENE: 88 | this._scene.render(); 89 | break; 90 | case State.GAME: 91 | this._scene.render(); 92 | break; 93 | case State.LOSE: 94 | this._scene.render(); 95 | break; 96 | default: break; 97 | } 98 | }); 99 | 100 | //resize if the screen is resized/rotated 101 | window.addEventListener('resize', () => { 102 | this._engine.resize(); 103 | }); 104 | } 105 | private async _goToStart(){ 106 | this._engine.displayLoadingUI(); 107 | 108 | this._scene.detachControl(); 109 | let scene = new Scene(this._engine); 110 | scene.clearColor = new Color4(0,0,0,1); 111 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 112 | camera.setTarget(Vector3.Zero()); 113 | 114 | //create a fullscreen ui for all of our GUI elements 115 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 116 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 117 | 118 | //create a simple button 119 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 120 | startBtn.width = 0.2 121 | startBtn.height = "40px"; 122 | startBtn.color = "white"; 123 | startBtn.top = "-14px"; 124 | startBtn.thickness = 0; 125 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 126 | guiMenu.addControl(startBtn); 127 | 128 | //this handles interactions with the start button attached to the scene 129 | startBtn.onPointerDownObservable.add(() => { 130 | this._goToCutScene(); 131 | scene.detachControl(); //observables disabled 132 | }); 133 | 134 | //--SCENE FINISHED LOADING-- 135 | await scene.whenReadyAsync(); 136 | this._engine.hideLoadingUI(); 137 | //lastly set the current state to the start state and set the scene to the start scene 138 | this._scene.dispose(); 139 | this._scene = scene; 140 | this._state = State.START; 141 | } 142 | 143 | private async _goToCutScene(): Promise { 144 | this._engine.displayLoadingUI(); 145 | //--SETUP SCENE-- 146 | //dont detect any inputs from this ui while the game is loading 147 | this._scene.detachControl(); 148 | this._cutScene = new Scene(this._engine); 149 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 150 | camera.setTarget(Vector3.Zero()); 151 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 152 | 153 | //--GUI-- 154 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 155 | 156 | //--PROGRESS DIALOGUE-- 157 | const next = Button.CreateSimpleButton("next", "NEXT"); 158 | next.color = "white"; 159 | next.thickness = 0; 160 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 161 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 162 | next.width = "64px"; 163 | next.height = "64px"; 164 | next.top = "-3%"; 165 | next.left = "-12%"; 166 | cutScene.addControl(next); 167 | 168 | next.onPointerUpObservable.add(() => { 169 | // this._goToGame(); 170 | }) 171 | 172 | //--WHEN SCENE IS FINISHED LOADING-- 173 | await this._cutScene.whenReadyAsync(); 174 | this._engine.hideLoadingUI(); 175 | this._scene.dispose(); 176 | this._state = State.CUTSCENE; 177 | this._scene = this._cutScene; 178 | 179 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 180 | var finishedLoading = false; 181 | await this._setUpGame().then(res =>{ 182 | finishedLoading = true; 183 | this._goToGame(); 184 | }); 185 | } 186 | 187 | private async _setUpGame() { 188 | let scene = new Scene(this._engine); 189 | this._gamescene = scene; 190 | 191 | //--CREATE ENVIRONMENT-- 192 | const environment = new Environment(scene); 193 | this._environment = environment; 194 | await this._environment.load(); //environment 195 | await this._loadCharacterAssets(scene); 196 | } 197 | 198 | private async _loadCharacterAssets(scene){ 199 | 200 | async function loadCharacter(){ 201 | //collision mesh 202 | const outer = MeshBuilder.CreateBox("outer", { width: 2, depth: 1, height: 3 }, scene); 203 | outer.isVisible = false; 204 | outer.isPickable = false; 205 | outer.checkCollisions = true; 206 | 207 | //move origin of box collider to the bottom of the mesh (to match player mesh) 208 | outer.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)) 209 | 210 | //for collisions 211 | outer.ellipsoid = new Vector3(1, 1.5, 1); 212 | outer.ellipsoidOffset = new Vector3(0, 1.5, 0); 213 | 214 | outer.rotationQuaternion = new Quaternion(0, 1, 0, 0); // rotate the player mesh 180 since we want to see the back of the player 215 | 216 | return SceneLoader.ImportMeshAsync(null, "./models/", "player.glb", scene).then((result) =>{ 217 | const root = result.meshes[0]; 218 | //body is our actual player mesh 219 | const body = root; 220 | body.parent = outer; 221 | body.isPickable = false; //so our raycasts dont hit ourself 222 | body.getChildMeshes().forEach(m => { 223 | m.isPickable = false; 224 | }) 225 | 226 | return { 227 | mesh: outer as Mesh, 228 | } 229 | }); 230 | } 231 | return loadCharacter().then(assets=> { 232 | console.log("load char assets") 233 | this.assets = assets; 234 | }) 235 | 236 | } 237 | 238 | private async _initializeGameAsync(scene): Promise { 239 | //temporary light to light the entire scene 240 | var light0 = new HemisphericLight("HemiLight", new Vector3(0, 1, 0), scene); 241 | 242 | const light = new PointLight("sparklight", new Vector3(0, 0, 0), scene); 243 | light.diffuse = new Color3(0.08627450980392157, 0.10980392156862745, 0.15294117647058825); 244 | light.intensity = 35; 245 | light.radius = 1; 246 | 247 | const shadowGenerator = new ShadowGenerator(1024, light); 248 | shadowGenerator.darkness = 0.4; 249 | 250 | //Create the player 251 | this._player = new Player(this.assets, scene, shadowGenerator, this._input); 252 | const camera = this._player.activatePlayerCamera(); 253 | 254 | //set up lantern collision checks 255 | this._environment.checkLanterns(this._player); 256 | } 257 | 258 | private async _goToGame(){ 259 | //--SETUP SCENE-- 260 | this._scene.detachControl(); 261 | let scene = this._gamescene; 262 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 263 | 264 | //--GUI-- 265 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 266 | //dont detect any inputs from this ui while the game is loading 267 | scene.detachControl(); 268 | 269 | //create a simple button 270 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 271 | loseBtn.width = 0.2 272 | loseBtn.height = "40px"; 273 | loseBtn.color = "white"; 274 | loseBtn.top = "-14px"; 275 | loseBtn.thickness = 0; 276 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 277 | playerUI.addControl(loseBtn); 278 | 279 | //this handles interactions with the start button attached to the scene 280 | loseBtn.onPointerDownObservable.add(() => { 281 | this._goToLose(); 282 | scene.detachControl(); //observables disabled 283 | }); 284 | 285 | //--INPUT-- 286 | this._input = new PlayerInput(scene); //detect keyboard/mobile inputs 287 | 288 | //primitive character and setting 289 | await this._initializeGameAsync(scene); 290 | 291 | //--WHEN SCENE FINISHED LOADING-- 292 | await scene.whenReadyAsync(); 293 | scene.getMeshByName("outer").position = scene.getTransformNodeByName("startPosition").getAbsolutePosition(); //move the player to the start position 294 | //get rid of start scene, switch to gamescene and change states 295 | this._scene.dispose(); 296 | this._state = State.GAME; 297 | this._scene = scene; 298 | this._engine.hideLoadingUI(); 299 | //the game is ready, attach control back 300 | this._scene.attachControl(); 301 | } 302 | 303 | private async _goToLose(): Promise { 304 | this._engine.displayLoadingUI(); 305 | 306 | //--SCENE SETUP-- 307 | this._scene.detachControl(); 308 | let scene = new Scene(this._engine); 309 | scene.clearColor = new Color4(0, 0, 0, 1); 310 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 311 | camera.setTarget(Vector3.Zero()); 312 | 313 | //--GUI-- 314 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 315 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 316 | mainBtn.width = 0.2; 317 | mainBtn.height = "40px"; 318 | mainBtn.color = "white"; 319 | guiMenu.addControl(mainBtn); 320 | //this handles interactions with the start button attached to the scene 321 | mainBtn.onPointerUpObservable.add(() => { 322 | this._goToStart(); 323 | }); 324 | 325 | //--SCENE FINISHED LOADING-- 326 | await scene.whenReadyAsync(); 327 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 328 | //lastly set the current state to the lose state and set the scene to the lose scene 329 | this._scene.dispose(); 330 | this._scene = scene; 331 | this._state = State.LOSE; 332 | } 333 | } 334 | new App(); -------------------------------------------------------------------------------- /tutorial/lanterns/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3, Quaternion, Ray, ParticleSystem, ActionManager } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | //Camera 12 | private _camRoot: TransformNode; 13 | private _yTilt: TransformNode; 14 | 15 | //const values 16 | private static readonly PLAYER_SPEED: number = 0.45; 17 | private static readonly JUMP_FORCE: number = 0.80; 18 | private static readonly GRAVITY: number = -2.8; 19 | private static readonly DASH_FACTOR: number = 2.5; 20 | private static readonly DASH_TIME: number = 10; //how many frames the dash lasts 21 | private static readonly ORIGINAL_TILT: Vector3 = new Vector3(0.5934119456780721, 0, 0); 22 | public dashTime: number = 0; 23 | 24 | //player movement vars 25 | private _deltaTime: number = 0; 26 | private _h: number; 27 | private _v: number; 28 | 29 | private _moveDirection: Vector3 = new Vector3(); 30 | private _inputAmt: number; 31 | 32 | //dashing 33 | private _dashPressed: boolean; 34 | private _canDash: boolean = true; 35 | 36 | //gravity, ground detection, jumping 37 | private _gravity: Vector3 = new Vector3(); 38 | private _lastGroundPos: Vector3 = Vector3.Zero(); // keep track of the last grounded position 39 | private _grounded: boolean; 40 | private _jumpCount: number = 1; 41 | 42 | //player variables 43 | public lanternsLit: number = 1; //num lanterns lit 44 | public totalLanterns: number; 45 | public win: boolean = false; //whether the game is won 46 | 47 | //sparkler 48 | public sparkler: ParticleSystem; // sparkler particle system 49 | public sparkLit: boolean = true; 50 | public sparkReset: boolean = false; 51 | 52 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 53 | super("player", scene); 54 | this.scene = scene; 55 | this._setupPlayerCamera(); 56 | 57 | this.mesh = assets.mesh; 58 | this.mesh.parent = this; 59 | 60 | //--COLLISIONS-- 61 | this.mesh.actionManager = new ActionManager(this.scene); 62 | 63 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 64 | 65 | this._input = input; 66 | } 67 | 68 | private _updateFromControls(): void { 69 | this._deltaTime = this.scene.getEngine().getDeltaTime() / 1000.0; 70 | 71 | this._moveDirection = Vector3.Zero(); // vector that holds movement information 72 | this._h = this._input.horizontal; //x-axis 73 | this._v = this._input.vertical; //z-axis 74 | 75 | if (this._input.dashing && !this._dashPressed && this._canDash && !this._grounded) { 76 | this._canDash = false; //we've started a dash, do not allow another 77 | this._dashPressed = true; //start the dash sequence 78 | } 79 | 80 | let dashFactor = 1; 81 | //if you're dashing, scale movement 82 | if (this._dashPressed) { 83 | if (this.dashTime > Player.DASH_TIME) { 84 | this.dashTime = 0; 85 | this._dashPressed = false; 86 | } else { 87 | dashFactor = Player.DASH_FACTOR; 88 | } 89 | this.dashTime++; 90 | } 91 | 92 | //--MOVEMENTS BASED ON CAMERA (as it rotates)-- 93 | let fwd = this._camRoot.forward; 94 | let right = this._camRoot.right; 95 | let correctedVertical = fwd.scaleInPlace(this._v); 96 | let correctedHorizontal = right.scaleInPlace(this._h); 97 | 98 | //movement based off of camera's view 99 | let move = correctedHorizontal.addInPlace(correctedVertical); 100 | 101 | //clear y so that the character doesnt fly up, normalize for next step, taking into account whether we've DASHED or not 102 | this._moveDirection = new Vector3((move).normalize().x * dashFactor, 0, (move).normalize().z * dashFactor); 103 | 104 | //clamp the input value so that diagonal movement isn't twice as fast 105 | let inputMag = Math.abs(this._h) + Math.abs(this._v); 106 | if (inputMag < 0) { 107 | this._inputAmt = 0; 108 | } else if (inputMag > 1) { 109 | this._inputAmt = 1; 110 | } else { 111 | this._inputAmt = inputMag; 112 | } 113 | 114 | //final movement that takes into consideration the inputs 115 | this._moveDirection = this._moveDirection.scaleInPlace(this._inputAmt * Player.PLAYER_SPEED); 116 | 117 | //Rotations 118 | //check if there is movement to determine if rotation is needed 119 | let input = new Vector3(this._input.horizontalAxis, 0, this._input.verticalAxis); //along which axis is the direction 120 | if (input.length() == 0) {//if there's no input detected, prevent rotation and keep player in same rotation 121 | return; 122 | } 123 | //rotation based on input & the camera angle 124 | let angle = Math.atan2(this._input.horizontalAxis, this._input.verticalAxis); 125 | angle += this._camRoot.rotation.y; 126 | let targ = Quaternion.FromEulerAngles(0, angle, 0); 127 | this.mesh.rotationQuaternion = Quaternion.Slerp(this.mesh.rotationQuaternion, targ, 10 * this._deltaTime); 128 | } 129 | 130 | private _floorRaycast(offsetx: number, offsetz: number, raycastlen: number): Vector3 { 131 | let raycastFloorPos = new Vector3(this.mesh.position.x + offsetx, this.mesh.position.y + 0.5, this.mesh.position.z + offsetz); 132 | let ray = new Ray(raycastFloorPos, Vector3.Up().scale(-1), raycastlen); 133 | 134 | let predicate = function (mesh) { 135 | return mesh.isPickable && mesh.isEnabled(); 136 | } 137 | let pick = this.scene.pickWithRay(ray, predicate); 138 | 139 | if (pick.hit) { 140 | return pick.pickedPoint; 141 | } else { 142 | return Vector3.Zero(); 143 | } 144 | } 145 | 146 | private _isGrounded(): boolean { 147 | if (this._floorRaycast(0, 0, 0.6).equals(Vector3.Zero())) { 148 | return false; 149 | } else { 150 | return true; 151 | } 152 | } 153 | 154 | private _checkSlope(): boolean { 155 | 156 | //only check meshes that are pickable and enabled (specific for collision meshes that are invisible) 157 | let predicate = function (mesh) { 158 | return mesh.isPickable && mesh.isEnabled(); 159 | } 160 | 161 | //4 raycasts outward from center 162 | let raycast = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z + .25); 163 | let ray = new Ray(raycast, Vector3.Up().scale(-1), 1.5); 164 | let pick = this.scene.pickWithRay(ray, predicate); 165 | 166 | let raycast2 = new Vector3(this.mesh.position.x, this.mesh.position.y + 0.5, this.mesh.position.z - .25); 167 | let ray2 = new Ray(raycast2, Vector3.Up().scale(-1), 1.5); 168 | let pick2 = this.scene.pickWithRay(ray2, predicate); 169 | 170 | let raycast3 = new Vector3(this.mesh.position.x + .25, this.mesh.position.y + 0.5, this.mesh.position.z); 171 | let ray3 = new Ray(raycast3, Vector3.Up().scale(-1), 1.5); 172 | let pick3 = this.scene.pickWithRay(ray3, predicate); 173 | 174 | let raycast4 = new Vector3(this.mesh.position.x - .25, this.mesh.position.y + 0.5, this.mesh.position.z); 175 | let ray4 = new Ray(raycast4, Vector3.Up().scale(-1), 1.5); 176 | let pick4 = this.scene.pickWithRay(ray4, predicate); 177 | 178 | if (pick.hit && !pick.getNormal().equals(Vector3.Up())) { 179 | if(pick.pickedMesh.name.includes("stair")) { 180 | return true; 181 | } 182 | } else if (pick2.hit && !pick2.getNormal().equals(Vector3.Up())) { 183 | if(pick2.pickedMesh.name.includes("stair")) { 184 | return true; 185 | } 186 | } 187 | else if (pick3.hit && !pick3.getNormal().equals(Vector3.Up())) { 188 | if(pick3.pickedMesh.name.includes("stair")) { 189 | return true; 190 | } 191 | } 192 | else if (pick4.hit && !pick4.getNormal().equals(Vector3.Up())) { 193 | if(pick4.pickedMesh.name.includes("stair")) { 194 | return true; 195 | } 196 | } 197 | return false; 198 | } 199 | 200 | private _updateGroundDetection(): void { 201 | if (!this._isGrounded()) { 202 | //if the body isnt grounded, check if it's on a slope and was either falling or walking onto it 203 | if (this._checkSlope() && this._gravity.y <= 0) { 204 | //if you are considered on a slope, you're able to jump and gravity wont affect you 205 | this._gravity.y = 0; 206 | this._jumpCount = 1; 207 | this._grounded = true; 208 | } else { 209 | //keep applying gravity 210 | this._gravity = this._gravity.addInPlace(Vector3.Up().scale(this._deltaTime * Player.GRAVITY)); 211 | this._grounded = false; 212 | } 213 | } 214 | //limit the speed of gravity to the negative of the jump power 215 | if (this._gravity.y < -Player.JUMP_FORCE) { 216 | this._gravity.y = -Player.JUMP_FORCE; 217 | } 218 | this.mesh.moveWithCollisions(this._moveDirection.addInPlace(this._gravity)); 219 | 220 | if (this._isGrounded()) { 221 | this._gravity.y = 0; 222 | this._grounded = true; 223 | this._lastGroundPos.copyFrom(this.mesh.position); 224 | 225 | this._jumpCount = 1; //allow for jumping 226 | //dashing reset 227 | this._canDash = true; //the ability to dash 228 | //reset sequence(needed if we collide with the ground BEFORE actually completing the dash duration) 229 | this.dashTime = 0; 230 | this._dashPressed = false; 231 | } 232 | 233 | //Jump detection 234 | if (this._input.jumpKeyDown && this._jumpCount > 0) { 235 | this._gravity.y = Player.JUMP_FORCE; 236 | this._jumpCount--; 237 | } 238 | 239 | 240 | } 241 | 242 | private _beforeRenderUpdate(): void { 243 | this._updateFromControls(); 244 | this._updateGroundDetection(); 245 | } 246 | 247 | public activatePlayerCamera(): UniversalCamera { 248 | this.scene.registerBeforeRender(() => { 249 | 250 | this._beforeRenderUpdate(); 251 | this._updateCamera(); 252 | 253 | }) 254 | return this.camera; 255 | } 256 | 257 | private _updateCamera(): void { 258 | let centerPlayer = this.mesh.position.y + 2; 259 | this._camRoot.position = Vector3.Lerp(this._camRoot.position, new Vector3(this.mesh.position.x, centerPlayer, this.mesh.position.z), 0.4); 260 | } 261 | 262 | private _setupPlayerCamera() { 263 | //root camera parent that handles positioning of the camera to follow the player 264 | this._camRoot = new TransformNode("root"); 265 | this._camRoot.position = new Vector3(0, 0, 0); //initialized at (0,0,0) 266 | //to face the player from behind (180 degrees) 267 | this._camRoot.rotation = new Vector3(0, Math.PI, 0); 268 | 269 | //rotations along the x-axis (up/down tilting) 270 | let yTilt = new TransformNode("ytilt"); 271 | //adjustments to camera view to point down at our player 272 | yTilt.rotation = Player.ORIGINAL_TILT; 273 | this._yTilt = yTilt; 274 | yTilt.parent = this._camRoot; 275 | 276 | //our actual camera that's pointing at our root's position 277 | this.camera = new UniversalCamera("cam", new Vector3(0, 0, -30), this.scene); 278 | this.camera.lockedTarget = this._camRoot.position; 279 | this.camera.fov = 0.47350045992678597; 280 | this.camera.parent = yTilt; 281 | 282 | this.scene.activeCamera = this.camera; 283 | return this.camera; 284 | } 285 | } -------------------------------------------------------------------------------- /tutorial/lanterns/environment.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, Vector3, SceneLoader, TransformNode, PBRMetallicRoughnessMaterial, ExecuteCodeAction, ActionManager, Texture, Color3 } from "@babylonjs/core"; 2 | import { Lantern } from "./lantern"; 3 | import { Player } from "./characterController"; 4 | 5 | export class Environment { 6 | private _scene: Scene; 7 | 8 | //Meshes 9 | private _lanternObjs: Array; //array of lanterns that need to be lit 10 | private _lightmtl: PBRMetallicRoughnessMaterial; // emissive texture for when lanterns are lit 11 | 12 | constructor(scene: Scene) { 13 | this._scene = scene; 14 | 15 | this._lanternObjs = []; 16 | //create emissive material for when lantern is lit 17 | const lightmtl = new PBRMetallicRoughnessMaterial("lantern mesh light", this._scene); 18 | lightmtl.emissiveTexture = new Texture("/textures/litLantern.png", this._scene, true, false); 19 | lightmtl.emissiveColor = new Color3(0.8784313725490196, 0.7568627450980392, 0.6235294117647059); 20 | this._lightmtl = lightmtl; 21 | } 22 | 23 | public async load() { 24 | // var ground = Mesh.CreateBox("ground", 24, this._scene); 25 | // ground.scaling = new Vector3(1,.02,1); 26 | 27 | const assets = await this._loadAsset(); 28 | //Loop through all environment meshes that were imported 29 | assets.allMeshes.forEach(m => { 30 | m.receiveShadows = true; 31 | m.checkCollisions = true; 32 | }); 33 | 34 | //--LANTERNS-- 35 | assets.lantern.isVisible = false; //original mesh is not visible 36 | //transform node to hold all lanterns 37 | const lanternHolder = new TransformNode("lanternHolder", this._scene); 38 | for (let i = 0; i < 22; i++) { 39 | //Mesh Cloning 40 | let lanternInstance = assets.lantern.clone("lantern" + i); //bring in imported lantern mesh & make clones 41 | lanternInstance.isVisible = true; 42 | lanternInstance.setParent(lanternHolder); 43 | 44 | //Create the new lantern object 45 | let newLantern = new Lantern(this._lightmtl, lanternInstance, this._scene, assets.env.getChildTransformNodes(false).find(m => m.name === "lantern " + i).getAbsolutePosition()); 46 | this._lanternObjs.push(newLantern); 47 | } 48 | //dispose of original mesh and animation group that were cloned 49 | assets.lantern.dispose(); 50 | } 51 | 52 | //Load all necessary meshes for the environment 53 | public async _loadAsset() { 54 | const result = await SceneLoader.ImportMeshAsync(null, "./models/", "envSetting.glb", this._scene); 55 | 56 | let env = result.meshes[0]; 57 | let allMeshes = env.getChildMeshes(); 58 | 59 | //loads lantern mesh 60 | const res = await SceneLoader.ImportMeshAsync("", "./models/", "lantern.glb", this._scene); 61 | 62 | //extract the actual lantern mesh from the root of the mesh that's imported, dispose of the root 63 | let lantern = res.meshes[0].getChildren()[0]; 64 | lantern.parent = null; 65 | res.meshes[0].dispose(); 66 | 67 | return { 68 | env: env, //reference to our entire imported glb (meshes and transform nodes) 69 | allMeshes: allMeshes, // all of the meshes that are in the environment 70 | lantern: lantern as Mesh 71 | } 72 | } 73 | 74 | public checkLanterns(player: Player) { 75 | if (!this._lanternObjs[0].isLit) { 76 | this._lanternObjs[0].setEmissiveTexture(); 77 | } 78 | 79 | this._lanternObjs.forEach(lantern => { 80 | player.mesh.actionManager.registerAction( 81 | new ExecuteCodeAction( 82 | { 83 | trigger: ActionManager.OnIntersectionEnterTrigger, 84 | parameter: lantern.mesh 85 | }, 86 | () => { 87 | //if the lantern is not lit, light it up & reset sparkler timer 88 | if (!lantern.isLit && player.sparkLit) { 89 | player.lanternsLit += 1; //increment the lantern count 90 | lantern.setEmissiveTexture(); //"light up" the lantern 91 | //reset the sparkler 92 | player.sparkReset = true; 93 | player.sparkLit = true; 94 | } 95 | //if the lantern is lit already, reset the sparkler 96 | else if (lantern.isLit) { 97 | player.sparkReset = true; 98 | player.sparkLit = true; 99 | } 100 | } 101 | ) 102 | ); 103 | }); 104 | } 105 | } -------------------------------------------------------------------------------- /tutorial/lanterns/lantern.ts: -------------------------------------------------------------------------------- 1 | import { PBRMetallicRoughnessMaterial, Mesh, Scene, Vector3, AnimationGroup, PointLight, Color3 } from "@babylonjs/core"; 2 | 3 | export class Lantern { 4 | public _scene: Scene; 5 | 6 | public mesh: Mesh; 7 | public isLit: boolean = false; 8 | private _lightSphere: Mesh; 9 | private _lightmtl: PBRMetallicRoughnessMaterial; 10 | 11 | constructor(lightmtl: PBRMetallicRoughnessMaterial, mesh: Mesh, scene: Scene, position: Vector3, animationGroups?: AnimationGroup) { 12 | this._scene = scene; 13 | this._lightmtl = lightmtl; 14 | 15 | //create the lantern's sphere of illumination 16 | const lightSphere = Mesh.CreateSphere("illum", 4, 20, this._scene); 17 | lightSphere.scaling.y = 2; 18 | lightSphere.setAbsolutePosition(position); 19 | lightSphere.parent = this.mesh; 20 | lightSphere.isVisible = false; 21 | lightSphere.isPickable = false; 22 | this._lightSphere = lightSphere; 23 | 24 | //load the lantern mesh 25 | this._loadLantern(mesh, position); 26 | } 27 | 28 | private _loadLantern(mesh: Mesh, position: Vector3): void { 29 | this.mesh = mesh; 30 | this.mesh.scaling = new Vector3(.8, .8, .8); 31 | this.mesh.setAbsolutePosition(position); 32 | this.mesh.isPickable = false; 33 | } 34 | 35 | public setEmissiveTexture(): void { 36 | this.isLit = true; 37 | 38 | //swap texture 39 | this.mesh.material = this._lightmtl; 40 | 41 | //create light source for the lanterns 42 | const light = new PointLight("lantern light", this.mesh.getAbsolutePosition(), this._scene); 43 | light.intensity = 30; 44 | light.radius = 2; 45 | light.diffuse = new Color3(0.45, 0.56, 0.80); 46 | this._findNearestMeshes(light); 47 | } 48 | 49 | //when the light is created, only include the meshes that are within the sphere of illumination 50 | private _findNearestMeshes(light: PointLight): void { 51 | this._scene.getMeshByName("__root__").getChildMeshes().forEach(m => { 52 | if (this._lightSphere.intersectsMesh(m)) { 53 | light.includedOnlyMeshes.push(m); 54 | } 55 | }); 56 | 57 | //get rid of the sphere 58 | this._lightSphere.dispose(); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /tutorial/oldLantern.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Scene, Color3, Mesh, Vector3, PointLight, Texture, Color4, ParticleSystem, AnimationGroup, PBRMetallicRoughnessMaterial } from "@babylonjs/core"; 3 | 4 | export class Lantern { 5 | public _scene: Scene; 6 | 7 | public mesh: Mesh; 8 | public isLit: boolean = false; 9 | private _lightSphere: Mesh; 10 | private _lightmtl: PBRMetallicRoughnessMaterial; 11 | 12 | //Lantern animations 13 | private _spinAnim: AnimationGroup; 14 | 15 | //Particle System 16 | private _stars: ParticleSystem; 17 | 18 | constructor(lightmtl: PBRMetallicRoughnessMaterial, mesh: Mesh, scene: Scene, position: Vector3, animationGroups: AnimationGroup) { 19 | this._scene = scene; 20 | this._lightmtl = lightmtl; 21 | 22 | //create the lantern's sphere of illumination 23 | const lightSphere = Mesh.CreateSphere("illum", 4, 20, this._scene); 24 | lightSphere.scaling.y = 2; 25 | lightSphere.setAbsolutePosition(position); 26 | lightSphere.parent = this.mesh; 27 | lightSphere.isVisible = false; 28 | lightSphere.isPickable = false; 29 | this._lightSphere = lightSphere; 30 | 31 | //load the lantern mesh 32 | this._loadLantern(mesh, position); 33 | 34 | //load particle system 35 | this._loadStars(); 36 | 37 | //set animations 38 | this._spinAnim = animationGroups; 39 | } 40 | 41 | private _loadLantern(mesh: Mesh, position: Vector3): void { 42 | this.mesh = mesh; 43 | this.mesh.scaling = new Vector3(.8, .8, .8); 44 | this.mesh.setAbsolutePosition(position); 45 | this.mesh.isPickable = false; 46 | } 47 | 48 | public setEmissiveTexture(): void { 49 | this.isLit = true; 50 | 51 | //play animation and particle system 52 | this._spinAnim.play(); 53 | this._stars.start(); 54 | //swap texture 55 | this.mesh.material = this._lightmtl; 56 | 57 | //create light source for the lanterns 58 | const light = new PointLight("lantern light", this.mesh.getAbsolutePosition(), this._scene); 59 | light.intensity = 30; 60 | light.radius = 2; 61 | light.diffuse = new Color3(0.45, 0.56, 0.80); 62 | 63 | this._findNearestMeshes(light); 64 | } 65 | 66 | //when the light is created, only include the meshes that are within the sphere of illumination 67 | private _findNearestMeshes(light: PointLight): void { 68 | this._scene.getMeshByName("__root__").getChildMeshes().forEach(m => { 69 | if (this._lightSphere.intersectsMesh(m)) { 70 | light.includedOnlyMeshes.push(m); 71 | } 72 | }); 73 | 74 | //get rid of the sphere 75 | this._lightSphere.dispose(); 76 | } 77 | 78 | private _loadStars(): void { 79 | const particleSystem = new ParticleSystem("stars", 1000, this._scene); 80 | 81 | particleSystem.particleTexture = new Texture("textures/solidStar.png", this._scene); 82 | particleSystem.emitter = new Vector3(this.mesh.position.x, this.mesh.position.y + 1.5, this.mesh.position.z); 83 | particleSystem.createPointEmitter(new Vector3(0.6, 1, 0), new Vector3(0, 1, 0)); 84 | particleSystem.color1 = new Color4(1, 1, 1); 85 | particleSystem.color2 = new Color4(1, 1, 1); 86 | particleSystem.colorDead = new Color4(1, 1, 1, 1); 87 | particleSystem.emitRate = 12; 88 | particleSystem.minEmitPower = 14; 89 | particleSystem.maxEmitPower = 14; 90 | particleSystem.addStartSizeGradient(0, 2); 91 | particleSystem.addStartSizeGradient(1, 0.8); 92 | particleSystem.minAngularSpeed = 0; 93 | particleSystem.maxAngularSpeed = 2; 94 | particleSystem.addDragGradient(0, 0.7, 0.7); 95 | particleSystem.targetStopDuration = .25; 96 | 97 | this._stars = particleSystem; 98 | } 99 | } -------------------------------------------------------------------------------- /tutorial/oldUpdateGround.txt: -------------------------------------------------------------------------------- 1 | //update ground detection 2 | private updateGroundDetection(): void { 3 | 4 | //raycasting for gravity & ground collision 5 | let onObject = false; 6 | 7 | //jump check 8 | var delta = this._scene.getEngine().getDeltaTime()/1000.0; 9 | var pick1; 10 | var pick2; 11 | var pick3; 12 | var pick4; 13 | 14 | 15 | this.velocityfalling.y = this.velocityfalling.y + (Player.GRAVITY*delta*Player.GRAVITY_SCALE); 16 | 17 | if(this.velocityfalling.y < -Player.JUMP_FORCE){ 18 | this.velocityfalling.y = -Player.JUMP_FORCE; 19 | } 20 | if(this.velocityfalling.y < 0){ 21 | 22 | //anim controllers 23 | this._isFalling = true; 24 | if(this._anims != null){ 25 | this._currentAnim = this._land; 26 | } 27 | this._jumped = false; 28 | 29 | //use the outer mesh(invisible) 30 | var corners = [ 31 | new Vector3(this._mesh.position.x-1, this._mesh.position.y+1.5, this._mesh.position.z+1), 32 | new Vector3(this._mesh.position.x+1, this._mesh.position.y+1.5, this._mesh.position.z+1), 33 | new Vector3(this._mesh.position.x-1, this._mesh.position.y+1.5, this._mesh.position.z-1), 34 | new Vector3(this._mesh.position.x+1, this._mesh.position.y+1.5, this._mesh.position.z-1) 35 | ]; 36 | var ray = new Ray(corners[0],new Vector3(0,1,0).scale(-1), 2); 37 | var ray2 =new Ray(corners[1],new Vector3(0,1,0).scale(-1), 2); 38 | var ray3 =new Ray(corners[2],new Vector3(0,1,0).scale(-1), 2); 39 | var ray4 =new Ray(corners[3],new Vector3(0,1,0).scale(-1), 2); 40 | 41 | pick1 = this._scene.pickWithRay(ray); 42 | pick2 = this._scene.pickWithRay(ray2); 43 | pick3 = this._scene.pickWithRay(ray3); 44 | pick4 = this._scene.pickWithRay(ray4); 45 | 46 | 47 | if(pick1.hit || pick2.hit || pick3.hit || pick4.hit ){ 48 | onObject = true; 49 | //difference keeps track of how far the pickpoint of the ray was from the bottom player's y position 50 | var diff = 0; 51 | if(pick1.hit){ 52 | // console.log(pick1.pickedMesh.name); 53 | diff = pick1.pickedPoint.y - this._mesh.position.y; 54 | } 55 | if(pick2.hit) { 56 | diff = pick2.pickedPoint.y - this._mesh.position.y; 57 | } 58 | if(pick3.hit) { 59 | diff = pick3.pickedPoint.y - this._mesh.position.y; 60 | } 61 | if(pick4.hit){ 62 | diff = pick4.pickedPoint.y - this._mesh.position.y; 63 | } 64 | 65 | //in progress: hit dist is less than where expected to fall next 66 | if(diff > this.velocityfalling.y + (Player.GRAVITY*delta*Player.GRAVITY_SCALE*(3.5-1))) { 67 | this.velocityfalling.y = 0; 68 | this._mesh.position.addInPlace(new Vector3(0,diff,0)); //add the difference so that player is on the ground now 69 | 70 | } 71 | 72 | // if the pickpoint fell into the player, readjust 73 | // if(diff>-1.99) { 74 | // //should no longer be falling 75 | // this.velocityfalling.y = 0; 76 | // this._mesh.position.addInPlace(new Vector3(0,1.99+diff,0)); //add the difference so that player is on the ground now 77 | // } 78 | } 79 | } 80 | if (onObject) { 81 | this.velocityfalling.y = Math.max(0, this.velocityfalling.y); 82 | this._isFalling = false; 83 | this._groundY = this._mesh.position.y; 84 | this._lastGroundPos.copyFrom(this._mesh.position); 85 | } 86 | 87 | if (this._input.jumpKeyDown && onObject) { 88 | this._jumped = true; 89 | this.velocityfalling.y = Player.JUMP_FORCE; 90 | onObject = false; 91 | } 92 | } 93 | 94 | private beforeRenderUpdate(): void { 95 | this.updateFromControls(); 96 | //movement based on camera 97 | this._velocity = new Vector3(this._h, 0, this._v).scale(this._deltaTime * Player.PLAYER_SPEED); 98 | 99 | this.updateGroundDetection(); 100 | 101 | this.animatePlayer(); 102 | 103 | //limit dash to once per ground/platform touch 104 | if(this._input.dashing){ 105 | if(this.dashTime <= 0){ 106 | this._input.dashDxn = 0; 107 | this.dashTime = Player.DASH_START_TIME; 108 | this._input.dashing = false; 109 | this.dashvelv.y = 0; 110 | this.dashvelh.x = 0; 111 | } else { 112 | this.dashTime -= this._scene.getEngine().getDeltaTime()/3000; 113 | 114 | if(this._input.dashDxn == 1){ // fwd 115 | this.dashvelv.z = .5; 116 | 117 | this._velocity.z +=.8; 118 | } else if(this._input.dashDxn == 2) { 119 | this.dashvelv.z = -.5; 120 | 121 | this._velocity.z -=.8; 122 | } else if(this._input.dashDxn == 3){ // left 123 | this.dashvelh.x= -.5; 124 | 125 | this._velocity.x -=.8; 126 | } else if(this._input.dashDxn == 4){ //right 127 | this.dashvelh.x= .5; 128 | 129 | this._velocity.x +=.8; 130 | 131 | 132 | } 133 | } 134 | } 135 | 136 | //MOVEMENTS BASED ON CAMERA (as it rotates) 137 | var fwd = this._camRoot.forward; 138 | var right = this._camRoot.right; 139 | fwd.y =0; 140 | right.y=0; 141 | fwd.normalize(); 142 | right.normalize(); 143 | //movement to check for collisions in x/z 144 | var temp = this._velocity; 145 | temp.y = 0 146 | fwd.scaleInPlace(temp.z).addInPlace(right.scaleInPlace(temp.x)); 147 | this._mesh.moveWithCollisions(fwd); 148 | 149 | //movement to have gravity act 150 | this._mesh.position.addInPlace(new Vector3(0,this.velocityfalling.y,0)); 151 | } -------------------------------------------------------------------------------- /tutorial/simpleGameState/app.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4, StandardMaterial, Color3, PointLight, ShadowGenerator, Quaternion, Matrix } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | import { Environment } from "./environment"; 7 | import { Player } from "./characterController"; 8 | 9 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 10 | 11 | class App { 12 | // General Entire Application 13 | private _scene: Scene; 14 | private _canvas: HTMLCanvasElement; 15 | private _engine: Engine; 16 | 17 | //Game State Related 18 | public assets; 19 | private _environment; 20 | private _player: Player; 21 | 22 | 23 | //Scene - related 24 | private _state: number = 0; 25 | private _gamescene: Scene; 26 | private _cutScene: Scene; 27 | 28 | constructor() { 29 | this._canvas = this._createCanvas(); 30 | 31 | // initialize babylon scene and engine 32 | this._engine = new Engine(this._canvas, true); 33 | this._scene = new Scene(this._engine); 34 | 35 | // hide/show the Inspector 36 | window.addEventListener("keydown", (ev) => { 37 | // Shift+Ctrl+Alt+I 38 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 39 | if (this._scene.debugLayer.isVisible()) { 40 | this._scene.debugLayer.hide(); 41 | } else { 42 | this._scene.debugLayer.show(); 43 | } 44 | } 45 | }); 46 | 47 | // run the main render loop 48 | this._main(); 49 | } 50 | 51 | private _createCanvas(): HTMLCanvasElement { 52 | 53 | //Commented out for development 54 | // document.documentElement.style["overflow"] = "hidden"; 55 | // document.documentElement.style.overflow = "hidden"; 56 | // document.documentElement.style.width = "100%"; 57 | // document.documentElement.style.height = "100%"; 58 | // document.documentElement.style.margin = "0"; 59 | // document.documentElement.style.padding = "0"; 60 | // document.body.style.overflow = "hidden"; 61 | // document.body.style.width = "100%"; 62 | // document.body.style.height = "100%"; 63 | // document.body.style.margin = "0"; 64 | // document.body.style.padding = "0"; 65 | 66 | //create the canvas html element and attach it to the webpage 67 | this._canvas = document.createElement("canvas"); 68 | this._canvas.style.width = "100%"; 69 | this._canvas.style.height = "100%"; 70 | this._canvas.id = "gameCanvas"; 71 | document.body.appendChild(this._canvas); 72 | 73 | return this._canvas; 74 | } 75 | 76 | private async _main(): Promise { 77 | await this._goToStart(); 78 | 79 | // Register a render loop to repeatedly render the scene 80 | this._engine.runRenderLoop(() => { 81 | switch (this._state) { 82 | case State.START: 83 | this._scene.render(); 84 | break; 85 | case State.CUTSCENE: 86 | this._scene.render(); 87 | break; 88 | case State.GAME: 89 | this._scene.render(); 90 | break; 91 | case State.LOSE: 92 | this._scene.render(); 93 | break; 94 | default: break; 95 | } 96 | }); 97 | 98 | //resize if the screen is resized/rotated 99 | window.addEventListener('resize', () => { 100 | this._engine.resize(); 101 | }); 102 | } 103 | private async _goToStart(){ 104 | this._engine.displayLoadingUI(); 105 | 106 | this._scene.detachControl(); 107 | let scene = new Scene(this._engine); 108 | scene.clearColor = new Color4(0,0,0,1); 109 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 110 | camera.setTarget(Vector3.Zero()); 111 | 112 | //create a fullscreen ui for all of our GUI elements 113 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 114 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 115 | 116 | //create a simple button 117 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 118 | startBtn.width = 0.2 119 | startBtn.height = "40px"; 120 | startBtn.color = "white"; 121 | startBtn.top = "-14px"; 122 | startBtn.thickness = 0; 123 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 124 | guiMenu.addControl(startBtn); 125 | 126 | //this handles interactions with the start button attached to the scene 127 | startBtn.onPointerDownObservable.add(() => { 128 | this._goToCutScene(); 129 | scene.detachControl(); //observables disabled 130 | }); 131 | 132 | //--SCENE FINISHED LOADING-- 133 | await scene.whenReadyAsync(); 134 | this._engine.hideLoadingUI(); 135 | //lastly set the current state to the start state and set the scene to the start scene 136 | this._scene.dispose(); 137 | this._scene = scene; 138 | this._state = State.START; 139 | } 140 | 141 | private async _goToCutScene(): Promise { 142 | this._engine.displayLoadingUI(); 143 | //--SETUP SCENE-- 144 | //dont detect any inputs from this ui while the game is loading 145 | this._scene.detachControl(); 146 | this._cutScene = new Scene(this._engine); 147 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 148 | camera.setTarget(Vector3.Zero()); 149 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 150 | 151 | //--GUI-- 152 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 153 | 154 | //--PROGRESS DIALOGUE-- 155 | const next = Button.CreateSimpleButton("next", "NEXT"); 156 | next.color = "white"; 157 | next.thickness = 0; 158 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 159 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 160 | next.width = "64px"; 161 | next.height = "64px"; 162 | next.top = "-3%"; 163 | next.left = "-12%"; 164 | cutScene.addControl(next); 165 | 166 | next.onPointerUpObservable.add(() => { 167 | this._goToGame(); 168 | }) 169 | 170 | //--WHEN SCENE IS FINISHED LOADING-- 171 | await this._cutScene.whenReadyAsync(); 172 | this._engine.hideLoadingUI(); 173 | this._scene.dispose(); 174 | this._state = State.CUTSCENE; 175 | this._scene = this._cutScene; 176 | 177 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 178 | var finishedLoading = false; 179 | await this._setUpGame().then(res =>{ 180 | finishedLoading = true; 181 | }); 182 | } 183 | 184 | private async _setUpGame() { 185 | let scene = new Scene(this._engine); 186 | this._gamescene = scene; 187 | 188 | //--CREATE ENVIRONMENT-- 189 | const environment = new Environment(scene); 190 | this._environment = environment; 191 | await this._environment.load(); //environment 192 | await this._loadCharacterAssets(scene); 193 | } 194 | 195 | private async _loadCharacterAssets(scene){ 196 | 197 | async function loadCharacter(){ 198 | //collision mesh 199 | const outer = MeshBuilder.CreateBox("outer", { width: 2, depth: 1, height: 3 }, scene); 200 | outer.isVisible = false; 201 | outer.isPickable = false; 202 | outer.checkCollisions = true; 203 | 204 | //move origin of box collider to the bottom of the mesh (to match player mesh) 205 | outer.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)) 206 | 207 | //for collisions 208 | outer.ellipsoid = new Vector3(1, 1.5, 1); 209 | outer.ellipsoidOffset = new Vector3(0, 1.5, 0); 210 | 211 | outer.rotationQuaternion = new Quaternion(0, 1, 0, 0); // rotate the player mesh 180 since we want to see the back of the player 212 | 213 | var box = MeshBuilder.CreateBox("Small1", { width: 0.5, depth: 0.5, height: 0.25, faceColors: [new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1), new Color4(0,0,0,1),new Color4(0,0,0,1), new Color4(0,0,0,1)] }, scene); 214 | box.position.y = 1.5; 215 | box.position.z = 1; 216 | 217 | var body = Mesh.CreateCylinder("body", 3, 2,2,0,0,scene); 218 | var bodymtl = new StandardMaterial("red",scene); 219 | bodymtl.diffuseColor = new Color3(.8,.5,.5); 220 | body.material = bodymtl; 221 | body.isPickable = false; 222 | body.bakeTransformIntoVertices(Matrix.Translation(0, 1.5, 0)); // simulates the imported mesh's origin 223 | 224 | //parent the meshes 225 | box.parent = body; 226 | body.parent = outer; 227 | 228 | return { 229 | mesh: outer as Mesh 230 | } 231 | } 232 | return loadCharacter().then(assets=> { 233 | this.assets = assets; 234 | }) 235 | 236 | } 237 | 238 | private async _initializeGameAsync(scene): Promise { 239 | //temporary light to light the entire scene 240 | var light0 = new HemisphericLight("HemiLight", new Vector3(0, 1, 0), scene); 241 | 242 | const light = new PointLight("sparklight", new Vector3(0, 0, 0), scene); 243 | light.diffuse = new Color3(0.08627450980392157, 0.10980392156862745, 0.15294117647058825); 244 | light.intensity = 35; 245 | light.radius = 1; 246 | 247 | const shadowGenerator = new ShadowGenerator(1024, light); 248 | shadowGenerator.darkness = 0.4; 249 | 250 | //Create the player 251 | this._player = new Player(this.assets, scene, shadowGenerator); //dont have inputs yet so we dont need to pass it in 252 | } 253 | 254 | private async _goToGame(){ 255 | //--SETUP SCENE-- 256 | this._scene.detachControl(); 257 | let scene = this._gamescene; 258 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 259 | 260 | //--GUI-- 261 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 262 | //dont detect any inputs from this ui while the game is loading 263 | scene.detachControl(); 264 | 265 | //create a simple button 266 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 267 | loseBtn.width = 0.2 268 | loseBtn.height = "40px"; 269 | loseBtn.color = "white"; 270 | loseBtn.top = "-14px"; 271 | loseBtn.thickness = 0; 272 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 273 | playerUI.addControl(loseBtn); 274 | 275 | //this handles interactions with the start button attached to the scene 276 | loseBtn.onPointerDownObservable.add(() => { 277 | this._goToLose(); 278 | scene.detachControl(); //observables disabled 279 | }); 280 | 281 | //primitive character and setting 282 | await this._initializeGameAsync(scene); 283 | 284 | //--WHEN SCENE FINISHED LOADING-- 285 | await scene.whenReadyAsync(); 286 | scene.getMeshByName("outer").position = new Vector3(0,3,0); 287 | //get rid of start scene, switch to gamescene and change states 288 | this._scene.dispose(); 289 | this._state = State.GAME; 290 | this._scene = scene; 291 | this._engine.hideLoadingUI(); 292 | //the game is ready, attach control back 293 | this._scene.attachControl(); 294 | } 295 | 296 | private async _goToLose(): Promise { 297 | this._engine.displayLoadingUI(); 298 | 299 | //--SCENE SETUP-- 300 | this._scene.detachControl(); 301 | let scene = new Scene(this._engine); 302 | scene.clearColor = new Color4(0, 0, 0, 1); 303 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 304 | camera.setTarget(Vector3.Zero()); 305 | 306 | //--GUI-- 307 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 308 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 309 | mainBtn.width = 0.2; 310 | mainBtn.height = "40px"; 311 | mainBtn.color = "white"; 312 | guiMenu.addControl(mainBtn); 313 | //this handles interactions with the start button attached to the scene 314 | mainBtn.onPointerUpObservable.add(() => { 315 | this._goToStart(); 316 | }); 317 | 318 | //--SCENE FINISHED LOADING-- 319 | await scene.whenReadyAsync(); 320 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 321 | //lastly set the current state to the lose state and set the scene to the lose scene 322 | this._scene.dispose(); 323 | this._scene = scene; 324 | this._state = State.LOSE; 325 | } 326 | } 327 | new App(); -------------------------------------------------------------------------------- /tutorial/simpleGameState/characterController.ts: -------------------------------------------------------------------------------- 1 | import { TransformNode, ShadowGenerator, Scene, Mesh, UniversalCamera, ArcRotateCamera, Vector3 } from "@babylonjs/core"; 2 | 3 | export class Player extends TransformNode { 4 | public camera; 5 | public scene: Scene; 6 | private _input; 7 | 8 | //Player 9 | public mesh: Mesh; //outer collisionbox of player 10 | 11 | constructor(assets, scene: Scene, shadowGenerator: ShadowGenerator, input?) { 12 | super("player", scene); 13 | this.scene = scene; 14 | this._setupPlayerCamera(); 15 | 16 | this.mesh = assets.mesh; 17 | this.mesh.parent = this; 18 | 19 | shadowGenerator.addShadowCaster(assets.mesh); //the player mesh will cast shadows 20 | 21 | this._input = input; //inputs we will get from inputController.ts 22 | } 23 | 24 | private _setupPlayerCamera() { 25 | var camera4 = new ArcRotateCamera("arc", -Math.PI/2, Math.PI/2, 40, new Vector3(0,3,0), this.scene); 26 | } 27 | } -------------------------------------------------------------------------------- /tutorial/simpleGameState/environment.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Mesh, Vector3 } from "@babylonjs/core"; 2 | 3 | export class Environment { 4 | private _scene: Scene; 5 | 6 | constructor(scene: Scene) { 7 | this._scene = scene; 8 | } 9 | 10 | public async load() { 11 | var ground = Mesh.CreateBox("ground", 24, this._scene); 12 | ground.scaling = new Vector3(1,.02,1); 13 | } 14 | } -------------------------------------------------------------------------------- /tutorial/stateMachine/sampleApp.ts: -------------------------------------------------------------------------------- 1 | import "@babylonjs/core/Debug/debugLayer"; 2 | import "@babylonjs/inspector"; 3 | import "@babylonjs/loaders/glTF"; 4 | import { Engine, Scene, ArcRotateCamera, Vector3, HemisphericLight, Mesh, MeshBuilder, FreeCamera, Color4 } from "@babylonjs/core"; 5 | import { AdvancedDynamicTexture, Button, Control } from "@babylonjs/gui"; 6 | 7 | enum State { START = 0, GAME = 1, LOSE = 2, CUTSCENE = 3 } 8 | 9 | class App { 10 | // General Entire Application 11 | private _scene: Scene; 12 | private _canvas: HTMLCanvasElement; 13 | private _engine: Engine; 14 | 15 | //Scene - related 16 | private _state: number = 0; 17 | private _gamescene: Scene; 18 | private _cutScene: Scene; 19 | 20 | constructor() { 21 | this._canvas = this._createCanvas(); 22 | 23 | // initialize babylon scene and engine 24 | this._engine = new Engine(this._canvas, true); 25 | this._scene = new Scene(this._engine); 26 | 27 | // hide/show the Inspector 28 | window.addEventListener("keydown", (ev) => { 29 | // Shift+Ctrl+Alt+I 30 | if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { 31 | if (this._scene.debugLayer.isVisible()) { 32 | this._scene.debugLayer.hide(); 33 | } else { 34 | this._scene.debugLayer.show(); 35 | } 36 | } 37 | }); 38 | 39 | // run the main render loop 40 | this._main(); 41 | } 42 | 43 | private _createCanvas(): HTMLCanvasElement { 44 | 45 | //Commented out for development 46 | document.documentElement.style["overflow"] = "hidden"; 47 | document.documentElement.style.overflow = "hidden"; 48 | document.documentElement.style.width = "100%"; 49 | document.documentElement.style.height = "100%"; 50 | document.documentElement.style.margin = "0"; 51 | document.documentElement.style.padding = "0"; 52 | document.body.style.overflow = "hidden"; 53 | document.body.style.width = "100%"; 54 | document.body.style.height = "100%"; 55 | document.body.style.margin = "0"; 56 | document.body.style.padding = "0"; 57 | 58 | //create the canvas html element and attach it to the webpage 59 | this._canvas = document.createElement("canvas"); 60 | this._canvas.style.width = "100%"; 61 | this._canvas.style.height = "100%"; 62 | this._canvas.id = "gameCanvas"; 63 | document.body.appendChild(this._canvas); 64 | 65 | return this._canvas; 66 | } 67 | 68 | private async _main(): Promise { 69 | await this._goToStart(); 70 | 71 | // Register a render loop to repeatedly render the scene 72 | this._engine.runRenderLoop(() => { 73 | switch (this._state) { 74 | case State.START: 75 | this._scene.render(); 76 | break; 77 | case State.CUTSCENE: 78 | this._scene.render(); 79 | break; 80 | case State.GAME: 81 | this._scene.render(); 82 | break; 83 | case State.LOSE: 84 | this._scene.render(); 85 | break; 86 | default: break; 87 | } 88 | }); 89 | 90 | //resize if the screen is resized/rotated 91 | window.addEventListener('resize', () => { 92 | this._engine.resize(); 93 | }); 94 | } 95 | private async _goToStart(){ 96 | this._engine.displayLoadingUI(); 97 | 98 | this._scene.detachControl(); 99 | let scene = new Scene(this._engine); 100 | scene.clearColor = new Color4(0,0,0,1); 101 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 102 | camera.setTarget(Vector3.Zero()); 103 | 104 | //create a fullscreen ui for all of our GUI elements 105 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 106 | guiMenu.idealHeight = 720; //fit our fullscreen ui to this height 107 | 108 | //create a simple button 109 | const startBtn = Button.CreateSimpleButton("start", "PLAY"); 110 | startBtn.width = 0.2 111 | startBtn.height = "40px"; 112 | startBtn.color = "white"; 113 | startBtn.top = "-14px"; 114 | startBtn.thickness = 0; 115 | startBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 116 | guiMenu.addControl(startBtn); 117 | 118 | //this handles interactions with the start button attached to the scene 119 | startBtn.onPointerDownObservable.add(() => { 120 | this._goToCutScene(); 121 | scene.detachControl(); //observables disabled 122 | }); 123 | 124 | //--SCENE FINISHED LOADING-- 125 | await scene.whenReadyAsync(); 126 | this._engine.hideLoadingUI(); 127 | //lastly set the current state to the start state and set the scene to the start scene 128 | this._scene.dispose(); 129 | this._scene = scene; 130 | this._state = State.START; 131 | } 132 | 133 | private async _goToCutScene(): Promise { 134 | this._engine.displayLoadingUI(); 135 | //--SETUP SCENE-- 136 | //dont detect any inputs from this ui while the game is loading 137 | this._scene.detachControl(); 138 | this._cutScene = new Scene(this._engine); 139 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), this._cutScene); 140 | camera.setTarget(Vector3.Zero()); 141 | this._cutScene.clearColor = new Color4(0, 0, 0, 1); 142 | 143 | //--GUI-- 144 | const cutScene = AdvancedDynamicTexture.CreateFullscreenUI("cutscene"); 145 | 146 | //--PROGRESS DIALOGUE-- 147 | const next = Button.CreateSimpleButton("next", "NEXT"); 148 | next.color = "white"; 149 | next.thickness = 0; 150 | next.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 151 | next.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT; 152 | next.width = "64px"; 153 | next.height = "64px"; 154 | next.top = "-3%"; 155 | next.left = "-12%"; 156 | cutScene.addControl(next); 157 | 158 | next.onPointerUpObservable.add(() => { 159 | this._goToGame(); 160 | }) 161 | 162 | //--WHEN SCENE IS FINISHED LOADING-- 163 | await this._cutScene.whenReadyAsync(); 164 | this._engine.hideLoadingUI(); 165 | this._scene.dispose(); 166 | this._state = State.CUTSCENE; 167 | this._scene = this._cutScene; 168 | 169 | //--START LOADING AND SETTING UP THE GAME DURING THIS SCENE-- 170 | var finishedLoading = false; 171 | await this._setUpGame().then(res =>{ 172 | finishedLoading = true; 173 | }); 174 | } 175 | 176 | private async _setUpGame() { 177 | let scene = new Scene(this._engine); 178 | this._gamescene = scene; 179 | 180 | //...load assets 181 | } 182 | 183 | private async _goToGame(){ 184 | //--SETUP SCENE-- 185 | this._scene.detachControl(); 186 | let scene = this._gamescene; 187 | scene.clearColor = new Color4(0.01568627450980392, 0.01568627450980392, 0.20392156862745098); // a color that fit the overall color scheme better 188 | let camera: ArcRotateCamera = new ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2, 2, Vector3.Zero(), scene); 189 | camera.setTarget(Vector3.Zero()); 190 | 191 | //--GUI-- 192 | const playerUI = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 193 | //dont detect any inputs from this ui while the game is loading 194 | scene.detachControl(); 195 | 196 | //create a simple button 197 | const loseBtn = Button.CreateSimpleButton("lose", "LOSE"); 198 | loseBtn.width = 0.2 199 | loseBtn.height = "40px"; 200 | loseBtn.color = "white"; 201 | loseBtn.top = "-14px"; 202 | loseBtn.thickness = 0; 203 | loseBtn.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM; 204 | playerUI.addControl(loseBtn); 205 | 206 | //this handles interactions with the start button attached to the scene 207 | loseBtn.onPointerDownObservable.add(() => { 208 | this._goToLose(); 209 | scene.detachControl(); //observables disabled 210 | }); 211 | 212 | //temporary scene objects 213 | var light1: HemisphericLight = new HemisphericLight("light1", new Vector3(1, 1, 0), scene); 214 | var sphere: Mesh = MeshBuilder.CreateSphere("sphere", { diameter: 1 }, scene); 215 | 216 | //--WHEN SCENE FINISHED LOADING-- 217 | await scene.whenReadyAsync(); 218 | //get rid of start scene, switch to gamescene and change states 219 | this._scene.dispose(); 220 | this._state = State.GAME; 221 | this._scene = scene; 222 | this._engine.hideLoadingUI(); 223 | //the game is ready, attach control back 224 | this._scene.attachControl(); 225 | } 226 | 227 | private async _goToLose(): Promise { 228 | this._engine.displayLoadingUI(); 229 | 230 | //--SCENE SETUP-- 231 | this._scene.detachControl(); 232 | let scene = new Scene(this._engine); 233 | scene.clearColor = new Color4(0, 0, 0, 1); 234 | let camera = new FreeCamera("camera1", new Vector3(0, 0, 0), scene); 235 | camera.setTarget(Vector3.Zero()); 236 | 237 | //--GUI-- 238 | const guiMenu = AdvancedDynamicTexture.CreateFullscreenUI("UI"); 239 | const mainBtn = Button.CreateSimpleButton("mainmenu", "MAIN MENU"); 240 | mainBtn.width = 0.2; 241 | mainBtn.height = "40px"; 242 | mainBtn.color = "white"; 243 | guiMenu.addControl(mainBtn); 244 | //this handles interactions with the start button attached to the scene 245 | mainBtn.onPointerUpObservable.add(() => { 246 | this._goToStart(); 247 | }); 248 | 249 | //--SCENE FINISHED LOADING-- 250 | await scene.whenReadyAsync(); 251 | this._engine.hideLoadingUI(); //when the scene is ready, hide loading 252 | //lastly set the current state to the lose state and set the scene to the lose scene 253 | this._scene.dispose(); 254 | this._scene = scene; 255 | this._state = State.LOSE; 256 | } 257 | } 258 | new App(); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | const appDirectory = fs.realpathSync(process.cwd()); 7 | 8 | module.exports = { 9 | entry: path.resolve(appDirectory, "src/app.ts"), 10 | output: { 11 | path: path.resolve(appDirectory, "dist"), 12 | //name for the js file that is created/compiled in memory 13 | filename: "js/hanabiBundle.js", 14 | }, 15 | resolve: { 16 | // extensions: [".ts"] 17 | extensions: [".tsx", ".ts", ".js"], 18 | }, 19 | devServer: { 20 | host: "0.0.0.0", 21 | port: 8080, 22 | static: path.resolve(appDirectory, "public"), //tells webpack to serve from the public folder 23 | // publicPath: '/', 24 | hot: true, 25 | }, 26 | module: { 27 | rules: [ 28 | // {test: /\.tsx?$/, 29 | // loader: "ts-loader"} 30 | { 31 | test: /\.tsx?$/, 32 | use: "ts-loader", 33 | exclude: /node_modules/, 34 | }, 35 | ], 36 | }, 37 | plugins: [ 38 | new CopyPlugin({ 39 | patterns: [ 40 | { 41 | from: "public", 42 | globOptions: { 43 | dot: true, 44 | gitignore: true, 45 | ignore: ["**/index.html"], 46 | }, 47 | }, 48 | ], 49 | }), 50 | new HtmlWebpackPlugin({ 51 | inject: true, 52 | template: path.resolve(appDirectory, "public/index.html"), 53 | }), 54 | new CleanWebpackPlugin(), 55 | ], 56 | mode: "development", 57 | }; 58 | --------------------------------------------------------------------------------