├── .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 | 
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 |
--------------------------------------------------------------------------------