├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── AmmoLib.js ├── Component.js ├── DebugDrawer.js ├── DebugShapes.js ├── Entity.js ├── EntityManager.js ├── FiniteStateMachine.js ├── Input.js ├── assets │ ├── ammo │ │ ├── AmmoBox.fbx │ │ ├── AmmoBox_AO.tga.png │ │ ├── AmmoBox_D.tga.png │ │ ├── AmmoBox_M.tga.png │ │ ├── AmmoBox_N.tga.png │ │ └── AmmoBox_R.tga.png │ ├── animations │ │ ├── mutant breathing idle.fbx │ │ ├── mutant dying.fbx │ │ ├── mutant punch.fbx │ │ ├── mutant run.fbx │ │ ├── mutant walking.fbx │ │ └── mutant.fbx │ ├── decals │ │ ├── decal_a.jpg │ │ ├── decal_c.jpg │ │ └── decal_n.jpg │ ├── guns │ │ └── ak47 │ │ │ ├── T_INS_Body_a.tga.png │ │ │ ├── T_INS_Skin_a.tga.png │ │ │ ├── ak47.glb │ │ │ ├── weapon_ak47_D.tga.png │ │ │ └── weapon_ak47_N_S.tga.png │ ├── level.glb │ ├── muzzle_flash.glb │ ├── navmesh.obj │ ├── sky.jpg │ └── sounds │ │ └── ak47_shot.wav ├── entities │ ├── AmmoBox │ │ └── AmmoBox.js │ ├── Level │ │ ├── BulletDecals.js │ │ ├── LevelSetup.js │ │ └── Navmesh.js │ ├── NPC │ │ ├── AttackTrigger.js │ │ ├── CharacterCollision.js │ │ ├── CharacterController.js │ │ ├── CharacterFSM.js │ │ └── DirectionDebug.js │ ├── Player │ │ ├── PlayerControls.js │ │ ├── PlayerHealth.js │ │ ├── PlayerPhysics.js │ │ ├── Weapon.js │ │ └── WeaponFSM.js │ ├── Sky │ │ ├── Sky.js │ │ └── Sky2.js │ └── UI │ │ └── UIManager.js ├── entry.js ├── index.html └── ui │ └── crosshair.png ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-proposal-class-properties" 6 | ] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,json}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | *.log* 4 | .DS_Store 5 | *.code-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Mohsen Heydari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Three FPS Demo 2 | 3 | Three.js FPS game using ammo.js and three-pathfinding with ES6 and Webpack. 4 | 5 | The project features an entity/component system, FPS controller using ammo.js rigidbody, NPC with root-motion animations and a basic AI. 6 | 7 | Please note that the project is still under development. 8 | 9 | [Online Demo](http://venolabs.com/three-fps-demo/) 10 | 11 | ## Install 12 | Before you begin, make sure you are comfortable with terminal commands and have [Node and NPM installed](https://www.npmjs.com/get-npm). Then either install via a download or with Git. 13 | 14 | ### Install via Download 15 | First download the [zip of the project](https://github.com/mohsenheydari/three-fps/archive/master.zip) and extract it. Then in terminal at that folder type `npm install` to set things up. To get going run: `npm start`. 16 | 17 | ### Install with Git 18 | In terminal clone the project into a directory of your choice then delete the git folder to start fresh. 19 | 20 | ```bash 21 | git clone --depth=1 https://github.com/mohsenheydari/three-fps.git three-fps 22 | cd three-fps 23 | rm -rf .git 24 | npm install 25 | ``` 26 | 27 | ## Running the development server 28 | To see the changes you make to the project go to the project's folder in terminal and type... 29 | 30 | ```bash 31 | npm start 32 | ``` 33 | 34 | This command will bundle the project code and start a development server at [http://localhost:8080/](http://localhost:8080/). Visit this in your web browser. 35 | 36 | ## Editing the code 37 | The first file you should open is `./src/entry.js`. In it you will find the main application class. This class is reponsible for initializing the libraries and loading art assets, it also handles the main game loop. 38 | 39 | ## Building the project for the web 40 | Running `npm run build` in terminal will bundle your project into the folder `./build/`. You can upload this directory to a web server. For more complex results read [this guide](https://webpack.js.org/guides/production/). 41 | 42 | ## About the models 43 | Art assets used in this project: 44 | 45 | * [Ak47](https://skfb.ly/6UEL9) by [kursat_sokmen](https://sketchfab.com/kursat_sokmen) is licensed under CC BY 4.0 46 | * [Metal Ammo Box](https://skfb.ly/6UAQY) by [TheoClarke](https://sketchfab.com/TheoClarke) is licensed under CC BY 4.0 47 | * [Mutant](https://mixamo.com) by [mixamo.com](https://mixamo.com) 48 | * [Veld Fire](https://hdrihaven.com/hdri/?h=veld_fire) by [Greg Zaal](https://hdrihaven.com/hdris/?a=Greg%20Zaal) is licensed under CC0 49 | 50 | ## Thanks to 51 | * [Three Seed](https://github.com/edwinwebb/three-seed) 52 | * [ammo.js](https://github.com/kripken/ammo.js/) 53 | * [three-pathfinding](https://github.com/donmccurdy/three-pathfinding) 54 | 55 | ## License 56 | [MIT](https://github.com/mohsenheydari/three-fps/blob/master/LICENSE) 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-fps-demo", 3 | "version": "0.9.0", 4 | "description": "Three.js FPS Demo", 5 | "keywords": [ 6 | "webgl", 7 | "boilerplate", 8 | "three.js", 9 | "es6", 10 | "webpack" 11 | ], 12 | "scripts": { 13 | "start": "webpack serve", 14 | "prebuild": "npx rimraf ./build && npx mkdirp ./build", 15 | "build": "webpack --mode production" 16 | }, 17 | "dependencies": { 18 | "@types/ammo.js": "github:osman-turan/ammo.js-typings", 19 | "ammo.js": "kripken/ammo.js", 20 | "three": "^0.127.0", 21 | "three-pathfinding": "^0.14.1", 22 | "webpack-cli": "^4.3.1" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.12.10", 26 | "@babel/preset-env": "^7.12.11", 27 | "babel-loader": "^8.2.2", 28 | "file-loader": "^6.2.0", 29 | "html-webpack-plugin": "^4.5.1", 30 | "raw-loader": "^4.0.2", 31 | "webpack": "^5.14.0", 32 | "webpack-dev-server": "^3.11.2" 33 | }, 34 | "engines": { 35 | "node": ">=8.0.0" 36 | }, 37 | "browserslist": [ 38 | "since 2017-06" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/AmmoLib.js: -------------------------------------------------------------------------------- 1 | import * as _Ammo from "ammo.js" 2 | import * as THREE from 'three' 3 | import {ConvexHull} from '../node_modules/three/examples/jsm/math/ConvexHull' 4 | 5 | let Ammo = null; 6 | let rayOrigin = null; 7 | let rayDest = null; 8 | let closestRayResultCallback = null; 9 | 10 | const CollisionFlags = { CF_NO_CONTACT_RESPONSE: 4 } 11 | const CollisionFilterGroups = { 12 | DefaultFilter: 1, 13 | StaticFilter: 2, 14 | KinematicFilter: 4, 15 | DebrisFilter: 8, 16 | SensorTrigger: 16, 17 | CharacterFilter: 32, 18 | AllFilter: -1 //all bits sets: DefaultFilter | StaticFilter | KinematicFilter | DebrisFilter | SensorTrigger 19 | }; 20 | 21 | function createConvexHullShape(object) { 22 | const geometry = createConvexGeom(object); 23 | let coords = geometry.attributes.position.array; 24 | let tempVec = new Ammo.btVector3(0, 0, 0); 25 | let shape = new Ammo.btConvexHullShape(); 26 | for (let i = 0, il = coords.length; i < il; i+= 3) { 27 | tempVec.setValue(coords[i], coords[i + 1], coords[i + 2]); 28 | let lastOne = (i >= (il - 3)); 29 | shape.addPoint(tempVec, lastOne); 30 | } 31 | return shape; 32 | } 33 | 34 | function createConvexGeom (object) { 35 | // Compute the 3D convex hull. 36 | let hull = new ConvexHull().setFromObject(object); 37 | let faces = hull.faces; 38 | let vertices = []; 39 | let normals = []; 40 | 41 | for ( var i = 0; i < faces.length; i ++ ) { 42 | var face = faces[ i ]; 43 | var edge = face.edge; 44 | do { 45 | var point = edge.head().point; 46 | vertices.push( point.x, point.y, point.z); 47 | normals.push( face.normal.x, face.normal.y, face.normal.z ); 48 | edge = edge.next; 49 | } while ( edge !== face.edge ); 50 | } 51 | 52 | const geom = new THREE.BufferGeometry(); 53 | geom.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) ); 54 | geom.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); 55 | 56 | return geom; 57 | } 58 | 59 | class AmmoHelper{ 60 | 61 | static Init(callback = ()=>{}){ 62 | _Ammo().then((ammo)=>{ 63 | Ammo = ammo; 64 | callback(); 65 | }); 66 | } 67 | 68 | static CreateTrigger(shape, position, rotation){ 69 | const transform = new Ammo.btTransform(); 70 | transform.setIdentity(); 71 | position && transform.setOrigin(new Ammo.btVector3(position.x, position.y, position.z)); 72 | rotation && transform.setRotation(new Ammo.btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w)); 73 | 74 | const ghostObj = new Ammo.btPairCachingGhostObject(); 75 | ghostObj.setCollisionShape(shape); 76 | ghostObj.setCollisionFlags(CollisionFlags.CF_NO_CONTACT_RESPONSE); 77 | ghostObj.setWorldTransform(transform); 78 | 79 | return ghostObj; 80 | } 81 | 82 | static IsTriggerOverlapping(ghostObj, rigidBody){ 83 | for(let i = 0; i < ghostObj.getNumOverlappingObjects(); i++) 84 | { 85 | const body = Ammo.castObject( ghostObj.getOverlappingObject(i), Ammo.btRigidBody ); 86 | if(body == rigidBody){ 87 | return true; 88 | } 89 | } 90 | 91 | return false; 92 | } 93 | 94 | static CastRay(world, origin, dest, result={}, collisionFilterMask=CollisionFilterGroups.AllFilter){ 95 | if(!rayOrigin){ 96 | rayOrigin = new Ammo.btVector3(); 97 | rayDest = new Ammo.btVector3(); 98 | closestRayResultCallback = new Ammo.ClosestRayResultCallback( rayOrigin, rayDest ); 99 | } 100 | 101 | // Reset closestRayResultCallback to reuse it 102 | const rayCallBack = Ammo.castObject( closestRayResultCallback, Ammo.RayResultCallback ); 103 | rayCallBack.set_m_closestHitFraction( 1 ); 104 | rayCallBack.set_m_collisionObject( null ); 105 | 106 | rayCallBack.m_collisionFilterMask = collisionFilterMask; 107 | 108 | // Set closestRayResultCallback origin and dest 109 | rayOrigin.setValue( origin.x, origin.y, origin.z ); 110 | rayDest.setValue( dest.x, dest.y, dest.z ); 111 | closestRayResultCallback.get_m_rayFromWorld().setValue( origin.x, origin.y, origin.z ); 112 | closestRayResultCallback.get_m_rayToWorld().setValue( dest.x, dest.y, dest.z ); 113 | 114 | // Perform ray test 115 | world.rayTest( rayOrigin, rayDest, closestRayResultCallback ); 116 | 117 | if ( closestRayResultCallback.hasHit() ) { 118 | 119 | if(result.intersectionPoint){ 120 | const point = closestRayResultCallback.get_m_hitPointWorld(); 121 | result.intersectionPoint.set( point.x(), point.y(), point.z() ); 122 | } 123 | 124 | if (result.intersectionNormal) { 125 | const normal = closestRayResultCallback.get_m_hitNormalWorld(); 126 | result.intersectionNormal.set( normal.x(), normal.y(), normal.z() ); 127 | } 128 | 129 | result.collisionObject = rayCallBack.get_m_collisionObject(); 130 | return true; 131 | } 132 | else { 133 | return false; 134 | } 135 | } 136 | 137 | } 138 | 139 | export {AmmoHelper, Ammo, createConvexHullShape, CollisionFlags, CollisionFilterGroups} -------------------------------------------------------------------------------- /src/Component.js: -------------------------------------------------------------------------------- 1 | export default class Component{ 2 | constructor(){ 3 | this.parent = null; 4 | } 5 | 6 | Initialize(){} 7 | 8 | SetParent(parent){ 9 | this.parent = parent; 10 | } 11 | 12 | GetComponent(name) { 13 | return this.parent.GetComponent(name); 14 | } 15 | 16 | FindEntity(name) { 17 | return this.parent.FindEntity(name); 18 | } 19 | 20 | Broadcast(msg){ 21 | this.parent.Broadcast(msg); 22 | } 23 | 24 | Update(_) {} 25 | 26 | PhysicsUpdate(_){} 27 | } -------------------------------------------------------------------------------- /src/DebugDrawer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yannick Deubel (https://github.com/yandeu) 3 | * @copyright Copyright (c) 2020 Yannick Deubel; Project Url: https://github.com/enable3d/enable3d 4 | * @description This is a modified version of the original code from Kevin Lee 5 | */ 6 | 7 | /** 8 | * @author Kevin Lee (https://github.com/InfiniteLee) 9 | * @copyright Copyright (c) 2019 Kevin Lee; Project Url: https://github.com/InfiniteLee/ammo-debug-drawer 10 | * @license {@link https://github.com/InfiniteLee/ammo-debug-drawer/blob/master/LICENSE|MPL-2.0} 11 | */ 12 | 13 | import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Scene, StaticDrawUsage } from 'three' 14 | 15 | const AmmoDebugConstants = { 16 | NoDebug: 0, 17 | DrawWireframe: 1, 18 | DrawAabb: 2, 19 | DrawFeaturesText: 4, 20 | DrawContactPoints: 8, 21 | NoDeactivation: 16, 22 | NoHelpText: 32, 23 | DrawText: 64, 24 | ProfileTimings: 128, 25 | EnableSatComparison: 256, 26 | DisableBulletLCP: 512, 27 | EnableCCD: 1024, 28 | DrawConstraints: 1 << 11, //2048 29 | DrawConstraintLimits: 1 << 12, //4096 30 | FastWireframe: 1 << 13, //8192 31 | DrawNormals: 1 << 14, //16384 32 | DrawOnTop: 1 << 15, //32768 33 | MAX_DEBUG_DRAW_MODE: 0xffffffff 34 | } 35 | 36 | /** 37 | * An implementation of the btIDebugDraw interface in Ammo.js, for debug rendering of Ammo shapes 38 | */ 39 | class DebugDrawer { 40 | 41 | constructor(scene, world, options = {}) { 42 | this.scene = scene; 43 | this.world = world; 44 | this.options = options; 45 | this.debugDrawMode = options.debugDrawMode || AmmoDebugConstants.DrawWireframe; 46 | const drawOnTop = this.debugDrawMode & AmmoDebugConstants.DrawOnTop || false; 47 | const maxBufferSize = options.maxBufferSize || 1000000; 48 | 49 | this.geometry = new BufferGeometry() 50 | const vertices = new Float32Array(maxBufferSize * 3) 51 | const colors = new Float32Array(maxBufferSize * 3) 52 | 53 | /* 54 | I do not know the difference, just using the first one. 55 | export const StaticDrawUsage: Usage; 56 | export const DynamicDrawUsage: Usage; 57 | export const StreamDrawUsage: Usage; 58 | export const StaticReadUsage: Usage; 59 | export const DynamicReadUsage: Usage; 60 | export const StreamReadUsage: Usage; 61 | export const StaticCopyUsage: Usage; 62 | export const DynamicCopyUsage: Usage; 63 | export const StreamCopyUsage: Usage; 64 | */ 65 | this.geometry.setAttribute('position', new BufferAttribute(vertices, 3).setUsage(StaticDrawUsage)) 66 | this.geometry.setAttribute('color', new BufferAttribute(colors, 3).setUsage(StaticDrawUsage)) 67 | 68 | this.index = 0 69 | 70 | const material = new LineBasicMaterial({ 71 | vertexColors: true, 72 | depthTest: !drawOnTop 73 | }); 74 | 75 | this.mesh = new LineSegments(this.geometry, material); 76 | if (drawOnTop) this.mesh.renderOrder = 999; 77 | this.mesh.frustumCulled = false; 78 | 79 | this.enabled = false; 80 | 81 | this.debugDrawer = new Ammo.DebugDrawer(); 82 | this.debugDrawer.drawLine = this.drawLine.bind(this); 83 | this.debugDrawer.drawContactPoint = this.drawContactPoint.bind(this); 84 | this.debugDrawer.reportErrorWarning = this.reportErrorWarning.bind(this); 85 | this.debugDrawer.draw3dText = this.draw3dText.bind(this); 86 | this.debugDrawer.setDebugMode = this.setDebugMode.bind(this); 87 | this.debugDrawer.getDebugMode = this.getDebugMode.bind(this); 88 | 89 | this.world.setDebugDrawer(this.debugDrawer); 90 | } 91 | 92 | enable() { 93 | this.enabled = true; 94 | this.scene.add(this.mesh); 95 | } 96 | 97 | disable() { 98 | this.enabled = false; 99 | this.scene.remove(this.mesh); 100 | } 101 | 102 | update() { 103 | if (!this.enabled) { 104 | return; 105 | } 106 | 107 | if (this.index != 0) { 108 | // @ts-ignore 109 | this.geometry.attributes.position.needsUpdate = true; 110 | // @ts-ignore 111 | this.geometry.attributes.color.needsUpdate = true; 112 | } 113 | 114 | this.index = 0; 115 | this.world.debugDrawWorld(); 116 | this.geometry.setDrawRange(0, this.index); 117 | } 118 | 119 | drawLine(from, to, color) { 120 | // @ts-ignore 121 | const heap = Ammo.HEAPF32; 122 | const r = heap[(color + 0) / 4]; 123 | const g = heap[(color + 4) / 4]; 124 | const b = heap[(color + 8) / 4]; 125 | 126 | const fromX = heap[(from + 0) / 4]; 127 | const fromY = heap[(from + 4) / 4]; 128 | const fromZ = heap[(from + 8) / 4]; 129 | this.geometry.attributes.position.setXYZ(this.index, fromX, fromY, fromZ); 130 | this.geometry.attributes.color.setXYZ(this.index++, r, g, b); 131 | 132 | const toX = heap[(to + 0) / 4]; 133 | const toY = heap[(to + 4) / 4]; 134 | const toZ = heap[(to + 8) / 4]; 135 | this.geometry.attributes.position.setXYZ(this.index, toX, toY, toZ); 136 | this.geometry.attributes.color.setXYZ(this.index++, r, g, b); 137 | } 138 | 139 | //TODO: figure out how to make lifeTime work 140 | drawContactPoint(pointOnB, normalOnB, distance, _lifeTime, color) { 141 | // @ts-ignore 142 | const heap = Ammo.HEAPF32; 143 | const r = heap[(color + 0) / 4]; 144 | const g = heap[(color + 4) / 4]; 145 | const b = heap[(color + 8) / 4]; 146 | 147 | const x = heap[(pointOnB + 0) / 4]; 148 | const y = heap[(pointOnB + 4) / 4]; 149 | const z = heap[(pointOnB + 8) / 4]; 150 | this.geometry.attributes.position.setXYZ(this.index, x, y, z); 151 | this.geometry.attributes.color.setXYZ(this.index++, r, g, b); 152 | 153 | const dx = heap[(normalOnB + 0) / 4] * distance; 154 | const dy = heap[(normalOnB + 4) / 4] * distance; 155 | const dz = heap[(normalOnB + 8) / 4] * distance; 156 | this.geometry.attributes.position.setXYZ(this.index, x + dx, y + dy, z + dz); 157 | this.geometry.attributes.color.setXYZ(this.index++, r, g, b); 158 | } 159 | 160 | reportErrorWarning(warningString) { 161 | // eslint-disable-next-line no-prototype-builtins 162 | if (Ammo.hasOwnProperty('Pointer_stringify')) { 163 | // @ts-ignore 164 | console.warn(Ammo.Pointer_stringify(warningString)); 165 | } else if (!this.warnedOnce) { 166 | this.warnedOnce = true; 167 | console.warn("Cannot print warningString, please rebuild Ammo.js using 'debug' flag"); 168 | } 169 | } 170 | 171 | draw3dText(_location, _textString) { 172 | //TODO 173 | console.warn('TODO: draw3dText'); 174 | } 175 | 176 | setDebugMode(debugMode) { 177 | this.debugDrawMode = debugMode; 178 | } 179 | 180 | getDebugMode() { 181 | return this.debugDrawMode; 182 | } 183 | } 184 | 185 | export default DebugDrawer -------------------------------------------------------------------------------- /src/DebugShapes.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export default class DebugShapes{ 4 | constructor(scene){ 5 | this.scene = scene; 6 | this.meshes = []; 7 | this.pointGeom = new THREE.SphereGeometry( 0.3, 8, 8 ); 8 | } 9 | 10 | AddPoint(position, color){ 11 | const material = new THREE.MeshBasicMaterial( {color, wireframe: true} ); 12 | const sphere = new THREE.Mesh( this.pointGeom, material ); 13 | sphere.position.copy(position); 14 | this.scene.add( sphere ); 15 | this.meshes.push(sphere); 16 | } 17 | 18 | Clear(){ 19 | for(const mesh of this.meshes){ 20 | this.scene.remove(mesh); 21 | } 22 | 23 | this.meshes.length = 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Entity.js: -------------------------------------------------------------------------------- 1 | import { Vector3, Quaternion } from 'three'; 2 | 3 | export default class Entity{ 4 | constructor(){ 5 | this.name = null; 6 | this.components = {}; 7 | this.position = new Vector3(); 8 | this.rotation = new Quaternion(); 9 | this.parent = null; 10 | this.eventHandlers = {}; 11 | } 12 | 13 | AddComponent(component){ 14 | component.SetParent(this); 15 | this.components[component.name] = component; 16 | } 17 | 18 | SetParent(parent){ 19 | this.parent = parent; 20 | } 21 | 22 | SetName(name){ 23 | this.name = name; 24 | } 25 | 26 | get Name(){ 27 | return this.name; 28 | } 29 | 30 | GetComponent(name){ 31 | return this.components[name]; 32 | } 33 | 34 | SetPosition(position){ 35 | this.position.copy(position); 36 | } 37 | 38 | get Position(){ 39 | return this.position; 40 | } 41 | 42 | SetRotation(rotation){ 43 | this.rotation.copy(rotation); 44 | } 45 | 46 | get Rotation(){ 47 | return this.rotation; 48 | } 49 | 50 | FindEntity(name) { 51 | return this.parent.Get(name); 52 | } 53 | 54 | RegisterEventHandler(handler, topic){ 55 | if(!this.eventHandlers.hasOwnProperty(topic)){ 56 | this.eventHandlers[topic] = []; 57 | } 58 | 59 | this.eventHandlers[topic].push(handler); 60 | } 61 | 62 | Broadcast(msg){ 63 | if(!this.eventHandlers.hasOwnProperty(msg.topic)){ 64 | return; 65 | } 66 | 67 | for(const handler of this.eventHandlers[msg.topic]){ 68 | handler(msg); 69 | } 70 | } 71 | 72 | PhysicsUpdate(world, timeStep){ 73 | for (let k in this.components) { 74 | this.components[k].PhysicsUpdate(world, timeStep); 75 | } 76 | } 77 | 78 | Update(timeElapsed) { 79 | for (let k in this.components) { 80 | this.components[k].Update(timeElapsed); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/EntityManager.js: -------------------------------------------------------------------------------- 1 | export default class EntityManager{ 2 | constructor(){ 3 | this.ids = 0; 4 | this.entities = []; 5 | } 6 | 7 | Get(name){ 8 | return this.entities.find(el=>el.Name===name); 9 | } 10 | 11 | Add(entity){ 12 | if(!entity.Name){ 13 | entity.SetName(this.ids); 14 | } 15 | entity.id = this.ids; 16 | this.ids++; 17 | entity.SetParent(this); 18 | this.entities.push(entity); 19 | } 20 | 21 | EndSetup(){ 22 | for(const ent of this.entities){ 23 | for(const key in ent.components){ 24 | ent.components[key].Initialize(); 25 | } 26 | } 27 | } 28 | 29 | PhysicsUpdate(world, timeStep){ 30 | for (const entity of this.entities) { 31 | entity.PhysicsUpdate(world, timeStep); 32 | } 33 | } 34 | 35 | Update(timeElapsed){ 36 | for (const entity of this.entities) { 37 | entity.Update(timeElapsed); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/FiniteStateMachine.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class FiniteStateMachine { 4 | constructor() { 5 | this.states = {}; 6 | this.currentState = null; 7 | } 8 | 9 | AddState(name, instance) { 10 | this.states[name] = instance; 11 | } 12 | 13 | SetState(name) { 14 | const prevState = this.currentState; 15 | 16 | if (prevState) { 17 | if (prevState.Name == name) { 18 | return; 19 | } 20 | prevState.Exit(); 21 | } 22 | 23 | this.currentState = this.states[name]; 24 | this.currentState.Enter(prevState); 25 | } 26 | 27 | Update(timeElapsed) { 28 | this.currentState && this.currentState.Update(timeElapsed); 29 | } 30 | }; 31 | 32 | class State { 33 | constructor(parent) { 34 | this.parent = parent; 35 | } 36 | 37 | Enter() {} 38 | Exit() {} 39 | Update() {} 40 | }; 41 | 42 | export {State, FiniteStateMachine} -------------------------------------------------------------------------------- /src/Input.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Input{ 4 | constructor(){ 5 | this._keyMap = {}; 6 | this.events = []; 7 | 8 | this.AddKeyDownListner(this._onKeyDown); 9 | this.AddKeyUpListner(this._onKeyUp); 10 | } 11 | 12 | _addEventListner(element, type, callback){ 13 | element.addEventListener(type, callback); 14 | this.events.push({element, type, callback}); 15 | } 16 | 17 | AddKeyDownListner(callback){ 18 | this._addEventListner(document, 'keydown', callback); 19 | } 20 | 21 | AddKeyUpListner(callback){ 22 | this._addEventListner(document, 'keyup', callback); 23 | } 24 | 25 | AddMouseMoveListner(callback){ 26 | this._addEventListner(document, 'mousemove', callback); 27 | } 28 | 29 | AddClickListner(callback){ 30 | this._addEventListner(document.body, 'click', callback); 31 | } 32 | 33 | AddMouseDownListner(callback){ 34 | this._addEventListner(document.body, 'mousedown', callback); 35 | } 36 | 37 | AddMouseUpListner(callback){ 38 | this._addEventListner(document.body, 'mouseup', callback); 39 | } 40 | 41 | _onKeyDown = (event) => { 42 | this._keyMap[event.code] = 1; 43 | } 44 | 45 | _onKeyUp = (event) => { 46 | this._keyMap[event.code] = 0; 47 | } 48 | 49 | GetKeyDown(code){ 50 | return this._keyMap[code] === undefined ? 0 : this._keyMap[code]; 51 | } 52 | 53 | ClearEventListners(){ 54 | this.events.forEach(e=>{ 55 | e.element.removeEventListener(e.type, e.callback); 56 | }); 57 | 58 | this.events = []; 59 | this.AddKeyDownListner(this._onKeyDown); 60 | this.AddKeyUpListner(this._onKeyUp); 61 | } 62 | } 63 | 64 | const inputInstance = new Input(); 65 | export default inputInstance; -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox.fbx -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox_AO.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox_AO.tga.png -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox_D.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox_D.tga.png -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox_M.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox_M.tga.png -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox_N.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox_N.tga.png -------------------------------------------------------------------------------- /src/assets/ammo/AmmoBox_R.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/ammo/AmmoBox_R.tga.png -------------------------------------------------------------------------------- /src/assets/animations/mutant breathing idle.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant breathing idle.fbx -------------------------------------------------------------------------------- /src/assets/animations/mutant dying.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant dying.fbx -------------------------------------------------------------------------------- /src/assets/animations/mutant punch.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant punch.fbx -------------------------------------------------------------------------------- /src/assets/animations/mutant run.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant run.fbx -------------------------------------------------------------------------------- /src/assets/animations/mutant walking.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant walking.fbx -------------------------------------------------------------------------------- /src/assets/animations/mutant.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/animations/mutant.fbx -------------------------------------------------------------------------------- /src/assets/decals/decal_a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/decals/decal_a.jpg -------------------------------------------------------------------------------- /src/assets/decals/decal_c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/decals/decal_c.jpg -------------------------------------------------------------------------------- /src/assets/decals/decal_n.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/decals/decal_n.jpg -------------------------------------------------------------------------------- /src/assets/guns/ak47/T_INS_Body_a.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/guns/ak47/T_INS_Body_a.tga.png -------------------------------------------------------------------------------- /src/assets/guns/ak47/T_INS_Skin_a.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/guns/ak47/T_INS_Skin_a.tga.png -------------------------------------------------------------------------------- /src/assets/guns/ak47/ak47.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/guns/ak47/ak47.glb -------------------------------------------------------------------------------- /src/assets/guns/ak47/weapon_ak47_D.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/guns/ak47/weapon_ak47_D.tga.png -------------------------------------------------------------------------------- /src/assets/guns/ak47/weapon_ak47_N_S.tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/guns/ak47/weapon_ak47_N_S.tga.png -------------------------------------------------------------------------------- /src/assets/level.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/level.glb -------------------------------------------------------------------------------- /src/assets/muzzle_flash.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/muzzle_flash.glb -------------------------------------------------------------------------------- /src/assets/navmesh.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.79 (sub 0) OBJ File: '' 2 | # www.blender.org 3 | o Navmesh_Navmesh.001 4 | v -5.769411 0.199995 41.820972 5 | v -5.769411 0.199995 39.120972 6 | v -0.969411 0.199995 39.120972 7 | v -0.969411 0.199995 41.520973 8 | v 5.930590 0.199995 41.520973 9 | v 3.230590 0.199995 23.520973 10 | v 18.230591 0.199995 21.720972 11 | v 23.030590 0.199995 22.020971 12 | v 23.030590 0.199995 26.820972 13 | v 25.130592 0.199995 27.120972 14 | v 25.130592 0.199995 37.620972 15 | v 22.430592 0.199995 37.620972 16 | v 22.130589 0.199995 40.020973 17 | v 16.730591 0.199995 40.020973 18 | v 16.430592 0.199995 37.620972 19 | v 8.630589 0.199995 37.620972 20 | v 8.330589 0.199995 40.020973 21 | v 7.130589 0.199995 40.020973 22 | v 6.830589 0.199995 37.620972 23 | v 0.530590 0.199995 37.620972 24 | v 0.530590 0.199995 31.320972 25 | v 3.230590 0.199995 31.020973 26 | v -6.069411 5.199996 37.620972 27 | v -6.069411 5.199996 36.420975 28 | v -0.969411 5.199996 36.420975 29 | v -0.969411 5.199996 37.620972 30 | v 33.830593 0.199995 27.120972 31 | v 34.130592 0.199995 25.920973 32 | v 35.330593 0.199995 26.220972 33 | v 35.330593 0.199995 33.120972 34 | v 32.630592 0.199995 34.920975 35 | v 29.330593 0.199995 34.920975 36 | v 29.030590 0.199995 37.620972 37 | v 35.630592 0.199995 33.720974 38 | v 39.530590 0.199995 33.420975 39 | v 39.530590 0.199995 27.720972 40 | v 41.030590 0.199995 27.420973 41 | v 41.030590 0.199995 37.620972 42 | v 32.930592 0.199995 37.620972 43 | v 33.230591 0.199995 35.220974 44 | v -6.069411 2.599996 35.220974 45 | v -6.069411 2.599996 31.320972 46 | v -0.969411 2.599996 31.320972 47 | v -0.969411 2.599996 35.220974 48 | v -5.769411 0.199995 29.820972 49 | v -5.769411 0.199995 23.520973 50 | v -5.769411 0.199995 16.920973 51 | v -5.769411 0.199995 10.620972 52 | v -5.769411 0.199995 4.020973 53 | v -0.669411 0.199995 4.020973 54 | v -0.669411 0.199995 9.120972 55 | v -3.369411 0.199995 9.420971 56 | v -3.369411 0.199995 19.620972 57 | v -3.369411 0.199995 29.820972 58 | v -1.869411 5.199996 29.820972 59 | v -1.869411 5.199996 20.220972 60 | v -1.869411 5.199996 10.620972 61 | v -0.969411 5.199996 10.620972 62 | v -0.969411 5.199996 17.220972 63 | v -0.669411 5.199996 23.520973 64 | v -0.969411 5.199996 29.820972 65 | v 0.530590 2.599996 29.820972 66 | v 0.530590 2.599996 24.420973 67 | v 1.730590 2.599996 24.420973 68 | v 1.730590 2.599996 29.820972 69 | v 34.130592 0.199995 19.020971 70 | v 33.530590 0.199995 18.720972 71 | v 34.430592 0.199995 17.520971 72 | v 41.030590 0.199995 17.820972 73 | v 39.230591 0.199995 25.320972 74 | v 35.630592 0.199995 25.320972 75 | v 24.530590 2.599996 25.620972 76 | v 24.530590 2.599996 24.720972 77 | v 31.130592 2.599996 24.720972 78 | v 31.430592 2.599996 20.220972 79 | v 32.630592 2.599996 20.220972 80 | v 32.630592 2.599996 25.620972 81 | v 42.530594 5.199996 24.120972 82 | v 42.530594 5.199996 22.920973 83 | v 47.630592 5.199996 22.920973 84 | v 47.630592 5.199996 24.120972 85 | v 2.930590 0.199995 22.920973 86 | v 0.530590 0.199995 22.920973 87 | v 0.530590 0.199995 10.920971 88 | v 5.630589 0.199995 10.920971 89 | v 5.930590 0.199995 6.420971 90 | v 11.930591 0.199995 6.720970 91 | v 18.230591 0.199995 6.420971 92 | v 18.230591 0.199995 14.220970 93 | v 42.530594 2.599996 21.420973 94 | v 42.530594 2.599996 17.820972 95 | v 47.630592 2.599996 17.820972 96 | v 47.630592 2.599996 21.420973 97 | v 24.530590 0.199995 7.620972 98 | v 31.730591 0.199995 7.620972 99 | v 31.730591 0.199995 15.120972 100 | v 34.430592 0.199995 15.420971 101 | v 30.230591 0.199995 18.720972 102 | v 29.930592 0.199995 20.820972 103 | v 24.530590 0.199995 20.820972 104 | v 24.530590 0.199995 14.220970 105 | v 19.730591 5.199996 13.620972 106 | v 19.730591 5.199996 8.520973 107 | v 23.030590 5.199996 8.520973 108 | v 23.030590 5.199996 20.520971 109 | v 22.130589 5.199996 20.520971 110 | v 22.130589 5.199996 14.220970 111 | v 21.530590 5.199996 13.620972 112 | v 38.330593 0.199995 2.220970 113 | v 42.830593 0.199995 1.920971 114 | v 42.830593 0.199995 8.220970 115 | v 42.830593 0.199995 14.220970 116 | v 35.930592 0.199995 13.920971 117 | v 35.930592 0.199995 8.520973 118 | v 38.330593 0.199995 8.220970 119 | v 3.230590 2.599996 9.420971 120 | v 3.230590 2.599996 4.020973 121 | v 11.330590 2.599996 4.020973 122 | v 11.330590 2.599996 4.920971 123 | v 5.330589 2.599996 4.920971 124 | v 4.430590 2.599996 5.820972 125 | v 4.430590 2.599996 9.420971 126 | v 21.830589 0.199995 0.120972 127 | v 22.730591 0.199995 -1.079029 128 | v 26.330593 0.199995 -1.379028 129 | v 26.330593 0.199995 -5.279030 130 | v 32.030590 0.199995 -5.279030 131 | v 31.730591 0.199995 1.320972 132 | v 34.430592 0.199995 1.620972 133 | v 34.430592 0.199995 7.020973 134 | v 32.030590 0.199995 7.020973 135 | v 24.230591 0.199995 7.020973 136 | v 22.130589 0.199995 7.020973 137 | v 18.530590 0.199995 0.120972 138 | v 18.230591 0.199995 2.520973 139 | v 9.230590 0.199995 2.520973 140 | v 0.530590 0.199995 2.520973 141 | v 0.530590 0.199995 -5.279030 142 | v 11.330590 0.199995 -5.279030 143 | v 22.430592 0.199995 -5.279030 144 | v 38.030590 0.199995 0.120972 145 | v 35.930592 0.199995 0.120972 146 | v 35.630592 0.199995 -6.779030 147 | v 42.830593 0.199995 -6.779030 148 | v -5.769411 0.199995 0.120972 149 | v -5.769411 0.199995 -6.779030 150 | v -3.069411 0.199995 -6.779030 151 | v -3.369411 0.199995 0.120972 152 | v 0.830589 2.599996 -6.779030 153 | v 0.830589 2.599996 -11.879028 154 | v 1.730590 2.599996 -11.879028 155 | v 1.730590 2.599996 -8.279030 156 | v 2.330589 2.599996 -7.679028 157 | v 8.630589 2.599996 -7.679028 158 | v 8.630589 2.599996 -6.779030 159 | v 10.130589 5.199996 -6.779030 160 | v 10.130589 5.199996 -7.679028 161 | v 16.130589 5.199996 -7.679028 162 | v 22.430592 5.199996 -7.679028 163 | v 22.430592 5.199996 -6.779030 164 | v 16.130589 5.199996 -6.779030 165 | vn 0.0000 1.0000 0.0000 166 | s off 167 | f 1//1 5//1 4//1 168 | f 1//1 4//1 2//1 169 | f 2//1 4//1 3//1 170 | f 19//1 18//1 16//1 171 | f 16//1 18//1 17//1 172 | f 22//1 19//1 16//1 173 | f 9//1 15//1 12//1 174 | f 12//1 15//1 13//1 175 | f 13//1 15//1 14//1 176 | f 9//1 7//1 15//1 177 | f 15//1 7//1 16//1 178 | f 16//1 7//1 22//1 179 | f 22//1 7//1 6//1 180 | f 9//1 8//1 7//1 181 | f 19//1 22//1 20//1 182 | f 20//1 22//1 21//1 183 | f 9//1 12//1 10//1 184 | f 10//1 12//1 11//1 185 | f 26//1 25//1 23//1 186 | f 23//1 25//1 24//1 187 | f 30//1 29//1 27//1 188 | f 27//1 29//1 28//1 189 | f 10//1 11//1 32//1 190 | f 32//1 11//1 33//1 191 | f 27//1 10//1 30//1 192 | f 30//1 10//1 31//1 193 | f 31//1 10//1 32//1 194 | f 34//1 30//1 40//1 195 | f 40//1 30//1 31//1 196 | f 38//1 37//1 35//1 197 | f 35//1 37//1 36//1 198 | f 38//1 35//1 39//1 199 | f 39//1 35//1 34//1 200 | f 39//1 34//1 40//1 201 | f 44//1 43//1 41//1 202 | f 41//1 43//1 42//1 203 | f 49//1 52//1 50//1 204 | f 50//1 52//1 51//1 205 | f 47//1 46//1 53//1 206 | f 52//1 49//1 48//1 207 | f 53//1 46//1 54//1 208 | f 54//1 46//1 45//1 209 | f 53//1 52//1 47//1 210 | f 47//1 52//1 48//1 211 | f 59//1 56//1 60//1 212 | f 60//1 56//1 61//1 213 | f 61//1 56//1 55//1 214 | f 56//1 59//1 57//1 215 | f 57//1 59//1 58//1 216 | f 65//1 64//1 62//1 217 | f 62//1 64//1 63//1 218 | f 68//1 67//1 66//1 219 | f 66//1 28//1 71//1 220 | f 71//1 28//1 29//1 221 | f 69//1 70//1 37//1 222 | f 37//1 70//1 36//1 223 | f 71//1 70//1 66//1 224 | f 66//1 70//1 68//1 225 | f 68//1 70//1 69//1 226 | f 77//1 76//1 74//1 227 | f 74//1 76//1 75//1 228 | f 77//1 74//1 72//1 229 | f 72//1 74//1 73//1 230 | f 81//1 80//1 78//1 231 | f 78//1 80//1 79//1 232 | f 87//1 86//1 85//1 233 | f 89//1 88//1 87//1 234 | f 85//1 84//1 82//1 235 | f 82//1 84//1 83//1 236 | f 7//1 89//1 6//1 237 | f 6//1 89//1 82//1 238 | f 82//1 89//1 85//1 239 | f 85//1 89//1 87//1 240 | f 93//1 92//1 90//1 241 | f 90//1 92//1 91//1 242 | f 96//1 98//1 97//1 243 | f 97//1 98//1 68//1 244 | f 68//1 98//1 67//1 245 | f 96//1 101//1 98//1 246 | f 98//1 101//1 100//1 247 | f 94//1 101//1 95//1 248 | f 95//1 101//1 96//1 249 | f 100//1 99//1 98//1 250 | f 104//1 103//1 108//1 251 | f 108//1 103//1 102//1 252 | f 104//1 107//1 105//1 253 | f 105//1 107//1 106//1 254 | f 104//1 108//1 107//1 255 | f 112//1 111//1 113//1 256 | f 113//1 111//1 115//1 257 | f 113//1 115//1 114//1 258 | f 115//1 111//1 109//1 259 | f 109//1 111//1 110//1 260 | f 117//1 116//1 121//1 261 | f 121//1 116//1 122//1 262 | f 117//1 120//1 118//1 263 | f 118//1 120//1 119//1 264 | f 117//1 121//1 120//1 265 | f 125//1 124//1 123//1 266 | f 128//1 131//1 129//1 267 | f 129//1 131//1 130//1 268 | f 123//1 133//1 132//1 269 | f 95//1 131//1 128//1 270 | f 128//1 127//1 125//1 271 | f 125//1 127//1 126//1 272 | f 128//1 125//1 95//1 273 | f 95//1 125//1 94//1 274 | f 94//1 125//1 132//1 275 | f 132//1 125//1 123//1 276 | f 140//1 139//1 124//1 277 | f 124//1 139//1 123//1 278 | f 123//1 139//1 134//1 279 | f 139//1 136//1 134//1 280 | f 134//1 136//1 135//1 281 | f 139//1 138//1 136//1 282 | f 136//1 138//1 137//1 283 | f 141//1 109//1 110//1 284 | f 143//1 142//1 141//1 285 | f 110//1 144//1 141//1 286 | f 141//1 144//1 143//1 287 | f 145//1 148//1 146//1 288 | f 146//1 148//1 147//1 289 | f 149//1 152//1 150//1 290 | f 150//1 152//1 151//1 291 | f 149//1 155//1 153//1 292 | f 153//1 155//1 154//1 293 | f 153//1 152//1 149//1 294 | f 161//1 158//1 156//1 295 | f 156//1 158//1 157//1 296 | f 161//1 160//1 158//1 297 | f 158//1 160//1 159//1 298 | -------------------------------------------------------------------------------- /src/assets/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/sky.jpg -------------------------------------------------------------------------------- /src/assets/sounds/ak47_shot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/assets/sounds/ak47_shot.wav -------------------------------------------------------------------------------- /src/entities/AmmoBox/AmmoBox.js: -------------------------------------------------------------------------------- 1 | import Component from '../../Component' 2 | import {Ammo, AmmoHelper, CollisionFilterGroups} from '../../AmmoLib' 3 | 4 | 5 | export default class AmmoBox extends Component{ 6 | constructor(scene, model, shape, physicsWorld){ 7 | super(); 8 | this.name = 'AmmoBox'; 9 | this.model = model; 10 | this.shape = shape; 11 | this.scene = scene; 12 | this.world = physicsWorld; 13 | 14 | this.quat = new Ammo.btQuaternion(); 15 | this.update = true; 16 | } 17 | 18 | Initialize(){ 19 | this.player = this.FindEntity('Player'); 20 | this.playerPhysics = this.player.GetComponent('PlayerPhysics'); 21 | 22 | this.trigger = AmmoHelper.CreateTrigger(this.shape); 23 | 24 | this.world.addCollisionObject(this.trigger, CollisionFilterGroups.SensorTrigger); 25 | this.scene.add(this.model); 26 | } 27 | 28 | Disable(){ 29 | this.update = false; 30 | this.scene.remove(this.model); 31 | this.world.removeCollisionObject(this.trigger); 32 | } 33 | 34 | Update(t){ 35 | if(!this.update){ 36 | return; 37 | } 38 | 39 | const entityPos = this.parent.position; 40 | const entityRot = this.parent.rotation; 41 | 42 | this.model.position.copy(entityPos); 43 | this.model.quaternion.copy(entityRot); 44 | 45 | const transform = this.trigger.getWorldTransform(); 46 | 47 | this.quat.setValue(entityRot.x, entityRot.y, entityRot.z, entityRot.w); 48 | transform.setRotation(this.quat); 49 | transform.getOrigin().setValue(entityPos.x, entityPos.y, entityPos.z); 50 | 51 | if(AmmoHelper.IsTriggerOverlapping(this.trigger, this.playerPhysics.body)){ 52 | this.player.Broadcast({topic: 'AmmoPickup'}); 53 | this.Disable(); 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/entities/Level/BulletDecals.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { Ammo } from "../../AmmoLib"; 3 | import Component from "../../Component"; 4 | import { DecalGeometry } from 'three/examples/jsm/geometries/DecalGeometry' 5 | 6 | 7 | export default class LevelBulletDecals extends Component{ 8 | constructor(scene, colorMap, normalMap, alphaMap){ 9 | super(); 10 | this.name = "LevelBulletDecals"; 11 | this.scene = scene; 12 | 13 | 14 | this.rot = new THREE.Euler(); 15 | this.mat4 = new THREE.Matrix4(); 16 | this.position = new THREE.Vector3(0,0,0); 17 | this.up = new THREE.Vector3(0,1,0); 18 | this.scale = new THREE.Vector3(1,1,1); 19 | this.material = new THREE.MeshStandardMaterial( { 20 | depthTest: true, 21 | depthWrite: false, 22 | polygonOffset: true, 23 | polygonOffsetFactor: - 4, 24 | alphaMap, 25 | normalMap, 26 | map: colorMap, 27 | transparent: true, 28 | } ); 29 | } 30 | 31 | Hit = e => { 32 | this.mat4.lookAt(this.position, e.hitResult.intersectionNormal, this.up); 33 | this.rot.setFromRotationMatrix(this.mat4); 34 | 35 | const size = Math.random() * 0.3 + 0.2; 36 | this.scale.set(size, size, 1.0); 37 | 38 | const rigidBody = Ammo.castObject( e.hitResult.collisionObject, Ammo.btRigidBody ); 39 | const mesh = rigidBody.mesh; 40 | 41 | const m = new THREE.Mesh( new DecalGeometry( mesh, e.hitResult.intersectionPoint, this.rot, this.scale ), this.material ); 42 | this.scene.add(m); 43 | } 44 | 45 | Initialize(){ 46 | this.parent.RegisterEventHandler(this.Hit, "hit"); 47 | } 48 | } -------------------------------------------------------------------------------- /src/entities/Level/LevelSetup.js: -------------------------------------------------------------------------------- 1 | import Component from '../../Component' 2 | import * as THREE from 'three' 3 | import {Ammo, createConvexHullShape} from '../../AmmoLib' 4 | 5 | export default class LevelSetup extends Component{ 6 | constructor(mesh, scene, physicsWorld){ 7 | super(); 8 | this.scene = scene; 9 | this.physicsWorld = physicsWorld; 10 | this.name = 'LevelSetup'; 11 | this.mesh = mesh; 12 | } 13 | 14 | LoadScene(){ 15 | 16 | this.mesh.traverse( ( node ) => { 17 | if ( node.isMesh || node.isLight ) { node.castShadow = true; } 18 | if(node.isMesh){ 19 | node.receiveShadow = true; 20 | //node.material.wireframe = true; 21 | this.SetStaticCollider(node); 22 | } 23 | 24 | if(node.isLight){ 25 | node.intensity = 3; 26 | const shadow = node.shadow; 27 | const lightCam = shadow.camera; 28 | 29 | shadow.mapSize.width = 1024 * 3; 30 | shadow.mapSize.height = 1024 * 3; 31 | shadow.bias = -0.00007; 32 | 33 | const dH = 35, dV = 35; 34 | lightCam.left = -dH; 35 | lightCam.right = dH; 36 | lightCam.top = dV; 37 | lightCam.bottom = -dV; 38 | 39 | //const cameraHelper = new THREE.CameraHelper(lightCam); 40 | //this.scene.add(cameraHelper); 41 | } 42 | }); 43 | 44 | this.scene.add( this.mesh ); 45 | } 46 | 47 | 48 | SetStaticCollider(mesh){ 49 | const shape = createConvexHullShape(mesh); 50 | const mass = 0; 51 | const transform = new Ammo.btTransform(); 52 | transform.setIdentity(); 53 | const motionState = new Ammo.btDefaultMotionState(transform); 54 | 55 | const localInertia = new Ammo.btVector3(0,0,0); 56 | const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia); 57 | const object = new Ammo.btRigidBody(rbInfo); 58 | object.parentEntity = this.parent; 59 | object.mesh = mesh; 60 | 61 | this.physicsWorld.addRigidBody(object); 62 | } 63 | 64 | Initialize(){ 65 | this.LoadScene(); 66 | } 67 | } -------------------------------------------------------------------------------- /src/entities/Level/Navmesh.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Component from '../../Component' 3 | 4 | import {Pathfinding} from 'three-pathfinding' 5 | 6 | 7 | export default class Navmesh extends Component{ 8 | constructor(scene, mesh){ 9 | super(); 10 | this.scene = scene; 11 | this.name = "Navmesh"; 12 | this.zone = "level1"; 13 | this.mesh = mesh; 14 | } 15 | 16 | Initialize(){ 17 | this.pathfinding = new Pathfinding(); 18 | 19 | this.mesh.traverse( ( node ) => { 20 | if(node.isMesh){ 21 | this.pathfinding.setZoneData(this.zone, Pathfinding.createZone(node.geometry)); 22 | } 23 | }); 24 | } 25 | 26 | GetRandomNode(p, range){ 27 | const groupID = this.pathfinding.getGroup(this.zone, p); 28 | return this.pathfinding.getRandomNode(this.zone, groupID, p, range); 29 | } 30 | 31 | FindPath(a, b){ 32 | const groupID = this.pathfinding.getGroup(this.zone, a); 33 | return this.pathfinding.findPath(a, b, this.zone, groupID); 34 | } 35 | } -------------------------------------------------------------------------------- /src/entities/NPC/AttackTrigger.js: -------------------------------------------------------------------------------- 1 | import Component from '../../Component' 2 | import {Ammo, AmmoHelper, CollisionFilterGroups} from '../../AmmoLib' 3 | 4 | export default class AttackTrigger extends Component{ 5 | constructor(physicsWorld){ 6 | super(); 7 | this.name = 'AttackTrigger'; 8 | this.physicsWorld = physicsWorld; 9 | 10 | //Relative to parent 11 | this.localTransform = new Ammo.btTransform(); 12 | this.localTransform.setIdentity(); 13 | this.localTransform.getOrigin().setValue(0.0, 1.0, 1.0); 14 | 15 | this.quat = new Ammo.btQuaternion(); 16 | 17 | this.overlapping = false; 18 | } 19 | 20 | SetupTrigger(){ 21 | const shape = new Ammo.btSphereShape(0.4); 22 | this.ghostObj = AmmoHelper.CreateTrigger(shape); 23 | 24 | this.physicsWorld.addCollisionObject(this.ghostObj, CollisionFilterGroups.SensorTrigger); 25 | } 26 | 27 | Initialize(){ 28 | this.playerPhysics = this.FindEntity('Player').GetComponent('PlayerPhysics'); 29 | this.SetupTrigger(); 30 | } 31 | 32 | PhysicsUpdate(world, t){ 33 | this.overlapping = AmmoHelper.IsTriggerOverlapping(this.ghostObj, this.playerPhysics.body); 34 | } 35 | 36 | Update(t){ 37 | const entityPos = this.parent.position; 38 | const entityRot = this.parent.rotation; 39 | const transform = this.ghostObj.getWorldTransform(); 40 | 41 | this.quat.setValue(entityRot.x, entityRot.y, entityRot.z, entityRot.w); 42 | transform.setRotation(this.quat); 43 | transform.getOrigin().setValue(entityPos.x, entityPos.y, entityPos.z); 44 | transform.op_mul(this.localTransform); 45 | } 46 | } -------------------------------------------------------------------------------- /src/entities/NPC/CharacterCollision.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Component from '../../Component' 3 | import {Ammo, AmmoHelper} from '../../AmmoLib' 4 | 5 | export default class CharacterCollision extends Component{ 6 | constructor(physicsWorld){ 7 | super(); 8 | this.world = physicsWorld; 9 | this.bonePos = new THREE.Vector3(); 10 | this.boneRot = new THREE.Quaternion(); 11 | this.globalRot = new Ammo.btQuaternion(); 12 | 13 | this.collisions = { 14 | 'MutantLeftArm':{ 15 | rotation: {x: -0.1, y: 0.0, z: Math.PI * 0.5}, 16 | position: {x: 0.13, y: -0.04, z: 0.0}, 17 | radius: 0.13, 18 | height: 0.13 19 | }, 20 | 'MutantLeftForeArm':{ 21 | rotation: {x: -0.1, y: 0.0, z: Math.PI * 0.5}, 22 | position: {x: 0.3, y: 0.0, z: -0.05}, 23 | radius: 0.2, 24 | height: 0.3 25 | }, 26 | 'MutantRightArm':{ 27 | rotation: {x: 0.1, y: 0.0, z: Math.PI * 0.5}, 28 | position: {x: -0.13, y: -0.04, z: 0.0}, 29 | radius: 0.13, 30 | height: 0.13 31 | }, 32 | 'MutantRightForeArm':{ 33 | rotation: {x: 0.1, y: 0.0, z: Math.PI * 0.5}, 34 | position: {x: -0.3, y: 0.0, z: -0.05}, 35 | radius: 0.2, 36 | height: 0.3 37 | }, 38 | 'MutantSpine':{ 39 | rotation: {x: 0.0, y: 0.0, z: 0.0}, 40 | position: {x: 0.0, y: 0.25, z: 0.0}, 41 | radius: 0.25, 42 | height: 0.5 43 | }, 44 | 'MutantLeftUpLeg':{ 45 | rotation: {x: -0.1, y: 0.0, z: 0.1}, 46 | position: {x: -0.02, y: -0.12, z: 0.0}, 47 | radius: 0.16, 48 | height: 0.24 49 | }, 50 | 'MutantRightUpLeg':{ 51 | rotation: {x: -0.1, y: 0.0, z: -0.1}, 52 | position: {x: 0.02, y: -0.12, z: 0.0}, 53 | radius: 0.16, 54 | height: 0.24 55 | }, 56 | 'MutantLeftLeg':{ 57 | rotation: {x: 0.13, y: 0.0, z: 0.0}, 58 | position: {x: 0.02, y: -0.12, z: 0.0}, 59 | radius: 0.14, 60 | height: 0.24 61 | }, 62 | 'MutantRightLeg':{ 63 | rotation: {x: 0.13, y: 0.0, z: 0.0}, 64 | position: {x: -0.02, y: -0.12, z: 0.0}, 65 | radius: 0.14, 66 | height: 0.24 67 | }, 68 | }; 69 | } 70 | 71 | Initialize(){ 72 | this.controller = this.GetComponent('CharacterController'); 73 | 74 | this.controller.model.traverse(child =>{ 75 | if ( !child.isSkinnedMesh ) { 76 | return; 77 | } 78 | 79 | this.mesh = child; 80 | }); 81 | 82 | Object.keys(this.collisions).forEach(key=>{ 83 | const collision = this.collisions[key]; 84 | 85 | collision.bone = this.mesh.skeleton.bones.find(bone => bone.name == key); 86 | 87 | const shape = new Ammo.btCapsuleShape(collision.radius, collision.height); 88 | collision.object = AmmoHelper.CreateTrigger(shape); 89 | collision.object.parentEntity = this.parent; 90 | 91 | const localRot = new Ammo.btQuaternion(); 92 | localRot.setEulerZYX(collision.rotation.z,collision.rotation.y, collision.rotation.x); 93 | collision.localTransform = new Ammo.btTransform(); 94 | collision.localTransform.setIdentity(); 95 | collision.localTransform.setRotation(localRot); 96 | collision.localTransform.getOrigin().setValue(collision.position.x, collision.position.y, collision.position.z); 97 | 98 | this.world.addCollisionObject(collision.object); 99 | }); 100 | 101 | } 102 | 103 | Update(t){ 104 | Object.keys(this.collisions).forEach(key=>{ 105 | const collision = this.collisions[key]; 106 | 107 | const transform = collision.object.getWorldTransform(); 108 | 109 | collision.bone.getWorldPosition(this.bonePos); 110 | collision.bone.getWorldQuaternion(this.boneRot); 111 | 112 | this.globalRot.setValue(this.boneRot.x, this.boneRot.y, this.boneRot.z, this.boneRot.w); 113 | transform.getOrigin().setValue(this.bonePos.x, this.bonePos.y, this.bonePos.z); 114 | transform.setRotation(this.globalRot); 115 | 116 | transform.op_mul(collision.localTransform); 117 | }); 118 | 119 | } 120 | } -------------------------------------------------------------------------------- /src/entities/NPC/CharacterController.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Component from '../../Component' 3 | import {Ammo, AmmoHelper, CollisionFilterGroups} from '../../AmmoLib' 4 | import CharacterFSM from './CharacterFSM' 5 | 6 | import DebugShapes from '../../DebugShapes' 7 | 8 | 9 | export default class CharacterController extends Component{ 10 | constructor(model, clips, scene, physicsWorld){ 11 | super(); 12 | this.name = 'CharacterController'; 13 | this.physicsWorld = physicsWorld; 14 | this.scene = scene; 15 | this.mixer = null; 16 | this.clips = clips; 17 | this.animations = {}; 18 | this.model = model; 19 | this.dir = new THREE.Vector3(); 20 | this.forwardVec = new THREE.Vector3(0,0,1); 21 | this.pathDebug = new DebugShapes(scene); 22 | this.path = []; 23 | this.tempRot = new THREE.Quaternion(); 24 | 25 | this.viewAngle = Math.cos(Math.PI / 4.0); 26 | this.maxViewDistance = 20.0 * 20.0; 27 | this.tempVec = new THREE.Vector3(); 28 | this.attackDistance = 2.2; 29 | 30 | this.canMove = true; 31 | this.health = 100; 32 | } 33 | 34 | SetAnim(name, clip){ 35 | const action = this.mixer.clipAction(clip); 36 | this.animations[name] = {clip, action}; 37 | } 38 | 39 | SetupAnimations(){ 40 | Object.keys(this.clips).forEach(key=>{this.SetAnim(key, this.clips[key])}); 41 | } 42 | 43 | Initialize(){ 44 | this.stateMachine = new CharacterFSM(this); 45 | this.navmesh = this.FindEntity('Level').GetComponent('Navmesh'); 46 | this.hitbox = this.GetComponent('AttackTrigger'); 47 | this.player = this.FindEntity("Player"); 48 | 49 | this.parent.RegisterEventHandler(this.TakeHit, 'hit'); 50 | 51 | const scene = this.model; 52 | 53 | scene.scale.setScalar(0.01); 54 | scene.position.copy(this.parent.position); 55 | 56 | this.mixer = new THREE.AnimationMixer( scene ); 57 | 58 | scene.traverse(child => { 59 | if ( !child.isSkinnedMesh ) { 60 | return; 61 | } 62 | 63 | child.frustumCulled = false; 64 | child.castShadow = true; 65 | child.receiveShadow = true; 66 | this.skinnedmesh = child; 67 | this.rootBone = child.skeleton.bones.find(bone => bone.name == 'MutantHips'); 68 | this.rootBone.refPos = this.rootBone.position.clone(); 69 | this.lastPos = this.rootBone.position.clone(); 70 | }); 71 | 72 | this.SetupAnimations(); 73 | 74 | this.scene.add(scene); 75 | this.stateMachine.SetState('idle'); 76 | } 77 | 78 | UpdateDirection(){ 79 | this.dir.copy(this.forwardVec); 80 | this.dir.applyQuaternion(this.parent.rotation); 81 | } 82 | 83 | CanSeeThePlayer(){ 84 | const playerPos = this.player.Position.clone(); 85 | const modelPos = this.model.position.clone(); 86 | modelPos.y += 1.35; 87 | const charToPlayer = playerPos.sub(modelPos); 88 | 89 | if(playerPos.lengthSq() > this.maxViewDistance){ 90 | return; 91 | } 92 | 93 | charToPlayer.normalize(); 94 | const angle = charToPlayer.dot(this.dir); 95 | 96 | if(angle < this.viewAngle){ 97 | return false; 98 | } 99 | 100 | const rayInfo = {}; 101 | const collisionMask = CollisionFilterGroups.AllFilter & ~CollisionFilterGroups.SensorTrigger; 102 | 103 | if(AmmoHelper.CastRay(this.physicsWorld, modelPos, this.player.Position, rayInfo, collisionMask)){ 104 | const body = Ammo.castObject( rayInfo.collisionObject, Ammo.btRigidBody ); 105 | 106 | if(body == this.player.GetComponent('PlayerPhysics').body){ 107 | return true; 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | 114 | NavigateToRandomPoint(){ 115 | const node = this.navmesh.GetRandomNode(this.model.position, 50); 116 | this.path = this.navmesh.FindPath(this.model.position, node); 117 | } 118 | 119 | NavigateToPlayer(){ 120 | this.tempVec.copy(this.player.Position); 121 | this.tempVec.y = 0.5; 122 | this.path = this.navmesh.FindPath(this.model.position, this.tempVec); 123 | 124 | /* 125 | if(this.path){ 126 | this.pathDebug.Clear(); 127 | for(const point of this.path){ 128 | this.pathDebug.AddPoint(point, "blue"); 129 | } 130 | } 131 | */ 132 | } 133 | 134 | FacePlayer(t, rate = 3.0){ 135 | this.tempVec.copy(this.player.Position).sub(this.model.position); 136 | this.tempVec.y = 0.0; 137 | this.tempVec.normalize(); 138 | 139 | this.tempRot.setFromUnitVectors(this.forwardVec, this.tempVec); 140 | this.model.quaternion.rotateTowards(this.tempRot, rate * t); 141 | } 142 | 143 | get IsCloseToPlayer(){ 144 | this.tempVec.copy(this.player.Position).sub(this.model.position); 145 | 146 | if(this.tempVec.lengthSq() <= this.attackDistance * this.attackDistance){ 147 | return true; 148 | } 149 | 150 | return false; 151 | } 152 | 153 | get IsPlayerInHitbox(){ 154 | return this.hitbox.overlapping; 155 | } 156 | 157 | HitPlayer(){ 158 | this.player.Broadcast({topic: 'hit'}); 159 | } 160 | 161 | TakeHit = msg => { 162 | this.health = Math.max(0, this.health - msg.amount); 163 | 164 | if(this.health == 0){ 165 | this.stateMachine.SetState('dead'); 166 | }else{ 167 | const stateName = this.stateMachine.currentState.Name; 168 | if(stateName == 'idle' || stateName == 'patrol'){ 169 | this.stateMachine.SetState('chase'); 170 | } 171 | } 172 | } 173 | 174 | MoveAlongPath(t){ 175 | if(!this.path?.length) return; 176 | 177 | const target = this.path[0].clone().sub( this.model.position ); 178 | target.y = 0.0; 179 | 180 | if (target.lengthSq() > 0.1 * 0.1) { 181 | target.normalize(); 182 | this.tempRot.setFromUnitVectors(this.forwardVec, target); 183 | this.model.quaternion.slerp(this.tempRot,4.0 * t); 184 | } else { 185 | // Remove node from the path we calculated 186 | this.path.shift(); 187 | 188 | if(this.path.length===0){ 189 | this.Broadcast({topic: 'nav.end', agent: this}); 190 | } 191 | } 192 | } 193 | 194 | ClearPath(){ 195 | if(this.path){ 196 | this.path.length = 0; 197 | } 198 | } 199 | 200 | ApplyRootMotion(){ 201 | if(this.canMove){ 202 | const vel = this.rootBone.position.clone(); 203 | vel.sub(this.lastPos).multiplyScalar(0.01); 204 | vel.y = 0; 205 | 206 | vel.applyQuaternion(this.model.quaternion); 207 | 208 | if(vel.lengthSq() < 0.1 * 0.1){ 209 | this.model.position.add(vel); 210 | } 211 | } 212 | 213 | //Reset the root bone horizontal position 214 | this.lastPos.copy(this.rootBone.position); 215 | this.rootBone.position.z = this.rootBone.refPos.z; 216 | this.rootBone.position.x = this.rootBone.refPos.x; 217 | } 218 | 219 | Update(t){ 220 | this.mixer && this.mixer.update(t); 221 | this.ApplyRootMotion(); 222 | 223 | this.UpdateDirection(); 224 | this.MoveAlongPath(t); 225 | this.stateMachine.Update(t); 226 | 227 | this.parent.SetRotation(this.model.quaternion); 228 | this.parent.SetPosition(this.model.position); 229 | } 230 | } -------------------------------------------------------------------------------- /src/entities/NPC/CharacterFSM.js: -------------------------------------------------------------------------------- 1 | import {FiniteStateMachine, State} from '../../FiniteStateMachine' 2 | import * as THREE from 'three' 3 | 4 | export default class CharacterFSM extends FiniteStateMachine{ 5 | constructor(proxy){ 6 | super(); 7 | this.proxy = proxy; 8 | this.Init(); 9 | } 10 | 11 | Init(){ 12 | this.AddState('idle', new IdleState(this)); 13 | this.AddState('patrol', new PatrolState(this)); 14 | this.AddState('chase', new ChaseState(this)); 15 | this.AddState('attack', new AttackState(this)); 16 | this.AddState('dead', new DeadState(this)); 17 | } 18 | } 19 | 20 | class IdleState extends State{ 21 | constructor(parent){ 22 | super(parent); 23 | this.maxWaitTime = 5.0; 24 | this.minWaitTime = 1.0; 25 | this.waitTime = 0.0; 26 | } 27 | 28 | get Name(){return 'idle'} 29 | get Animation(){return this.parent.proxy.animations['idle']; } 30 | 31 | Enter(prevState){ 32 | this.parent.proxy.canMove = false; 33 | const action = this.Animation.action; 34 | 35 | if(prevState){ 36 | action.time = 0.0; 37 | action.enabled = true; 38 | action.crossFadeFrom(prevState.Animation.action, 0.5, true); 39 | } 40 | 41 | action.play(); 42 | 43 | this.waitTime = Math.random() * (this.maxWaitTime - this.minWaitTime) + this.minWaitTime; 44 | } 45 | 46 | Update(t){ 47 | if(this.waitTime <= 0.0){ 48 | this.parent.SetState('patrol'); 49 | return; 50 | } 51 | 52 | this.waitTime -= t; 53 | 54 | if(this.parent.proxy.CanSeeThePlayer()){ 55 | this.parent.SetState('chase'); 56 | } 57 | } 58 | } 59 | 60 | class PatrolState extends State{ 61 | constructor(parent){ 62 | super(parent); 63 | } 64 | 65 | get Name(){return 'patrol'} 66 | get Animation(){return this.parent.proxy.animations['walk']; } 67 | 68 | PatrolEnd = ()=>{ 69 | this.parent.SetState('idle'); 70 | } 71 | 72 | Enter(prevState){ 73 | this.parent.proxy.canMove = true; 74 | const action = this.Animation.action; 75 | 76 | if(prevState){ 77 | action.time = 0.0; 78 | action.enabled = true; 79 | action.crossFadeFrom(prevState.Animation.action, 0.5, true); 80 | } 81 | 82 | action.play(); 83 | 84 | this.parent.proxy.NavigateToRandomPoint(); 85 | } 86 | 87 | Update(t){ 88 | if(this.parent.proxy.CanSeeThePlayer()){ 89 | this.parent.SetState('chase'); 90 | }else if(this.parent.proxy.path && this.parent.proxy.path.length == 0){ 91 | this.parent.SetState('idle'); 92 | } 93 | } 94 | } 95 | 96 | class ChaseState extends State{ 97 | constructor(parent){ 98 | super(parent); 99 | this.updateFrequency = 0.5; 100 | this.updateTimer = 0.0; 101 | this.attackDistance = 2.0; 102 | this.shouldRotate = false; 103 | this.switchDelay = 0.2; 104 | } 105 | 106 | get Name(){return 'chase'} 107 | get Animation(){return this.parent.proxy.animations['run']; } 108 | 109 | RunToPlayer(prevState){ 110 | this.parent.proxy.canMove = true; 111 | const action = this.Animation.action; 112 | this.updateTimer = 0.0; 113 | 114 | if(prevState){ 115 | action.time = 0.0; 116 | action.enabled = true; 117 | action.setEffectiveTimeScale(1.0); 118 | action.setEffectiveWeight(1.0); 119 | action.crossFadeFrom(prevState.Animation.action, 0.2, true); 120 | } 121 | 122 | action.timeScale = 1.5; 123 | action.play(); 124 | } 125 | 126 | Enter(prevState){ 127 | this.RunToPlayer(prevState); 128 | } 129 | 130 | Update(t){ 131 | if(this.updateTimer <= 0.0){ 132 | this.parent.proxy.NavigateToPlayer(); 133 | this.updateTimer = this.updateFrequency; 134 | } 135 | 136 | if(this.parent.proxy.IsCloseToPlayer){ 137 | if(this.switchDelay <= 0.0){ 138 | this.parent.SetState('attack'); 139 | } 140 | 141 | this.parent.proxy.ClearPath(); 142 | this.switchDelay -= t; 143 | }else{ 144 | this.switchDelay = 0.1; 145 | } 146 | 147 | this.updateTimer -= t; 148 | } 149 | } 150 | 151 | class AttackState extends State{ 152 | constructor(parent){ 153 | super(parent); 154 | this.attackTime = 0.0; 155 | this.canHit = true; 156 | } 157 | 158 | get Name(){return 'attack'} 159 | get Animation(){return this.parent.proxy.animations['attack']; } 160 | 161 | Enter(prevState){ 162 | this.parent.proxy.canMove = false; 163 | const action = this.Animation.action; 164 | this.attackTime = this.Animation.clip.duration; 165 | this.attackEvent = this.attackTime * 0.85; 166 | 167 | if(prevState){ 168 | action.time = 0.0; 169 | action.enabled = true; 170 | action.crossFadeFrom(prevState.Animation.action, 0.1, true); 171 | } 172 | 173 | action.play(); 174 | } 175 | 176 | Update(t){ 177 | this.parent.proxy.FacePlayer(t); 178 | 179 | if(!this.parent.proxy.IsCloseToPlayer && this.attackTime <= 0.0){ 180 | this.parent.SetState('chase'); 181 | return; 182 | } 183 | 184 | if(this.canHit && this.attackTime <= this.attackEvent && this.parent.proxy.IsPlayerInHitbox){ 185 | this.parent.proxy.HitPlayer(); 186 | this.canHit = false; 187 | } 188 | 189 | if(this.attackTime <= 0.0){ 190 | this.attackTime = this.Animation.clip.duration; 191 | this.canHit = true; 192 | } 193 | 194 | this.attackTime -= t; 195 | } 196 | } 197 | 198 | class DeadState extends State{ 199 | constructor(parent){ 200 | super(parent); 201 | } 202 | 203 | get Name(){return 'dead'} 204 | get Animation(){return this.parent.proxy.animations['die']; } 205 | 206 | Enter(prevState){ 207 | const action = this.Animation.action; 208 | action.setLoop(THREE.LoopOnce, 1); 209 | action.clampWhenFinished = true; 210 | 211 | if(prevState){ 212 | action.time = 0.0; 213 | action.enabled = true; 214 | action.crossFadeFrom(prevState.Animation.action, 0.1, true); 215 | } 216 | 217 | action.play(); 218 | } 219 | 220 | Update(t){} 221 | } -------------------------------------------------------------------------------- /src/entities/NPC/DirectionDebug.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Component from '../../Component' 3 | 4 | 5 | export default class DirectionDebug extends Component{ 6 | constructor(scene){ 7 | super(); 8 | this.name = 'DirectionDebug'; 9 | this.scene = scene; 10 | 11 | this.dir = new THREE.Vector3(); 12 | this.forwardVec = new THREE.Vector3(0,0,1); 13 | } 14 | 15 | Initialize(){ 16 | this.arrowHelper = new THREE.ArrowHelper(); 17 | this.scene.add( this.arrowHelper ); 18 | } 19 | 20 | Update(t){ 21 | this.dir.copy(this.forwardVec); 22 | this.dir.applyQuaternion(this.parent.rotation); 23 | this.arrowHelper.position.copy(this.parent.position); 24 | this.arrowHelper.position.y += 1; 25 | this.arrowHelper.setDirection(this.dir); 26 | this.arrowHelper.setLength(1); 27 | this.arrowHelper.setColor(0xffff00); 28 | } 29 | } -------------------------------------------------------------------------------- /src/entities/Player/PlayerControls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Component from '../../Component' 3 | import Input from '../../Input' 4 | import {Ammo} from '../../AmmoLib' 5 | 6 | import DebugShapes from '../../DebugShapes' 7 | 8 | 9 | export default class PlayerControls extends Component{ 10 | constructor(camera){ 11 | super(); 12 | this.name = 'PlayerControls'; 13 | this.camera = camera; 14 | 15 | this.timeZeroToMax = 0.08; 16 | 17 | this.maxSpeed = 7.0; 18 | this.speed = new THREE.Vector3(); 19 | this.acceleration = this.maxSpeed / this.timeZeroToMax; 20 | this.decceleration = -7.0; 21 | 22 | this.mouseSpeed = 0.002; 23 | this.physicsComponent = null; 24 | this.isLocked = false; 25 | 26 | this.angles = new THREE.Euler(); 27 | this.pitch = new THREE.Quaternion(); 28 | this.yaw = new THREE.Quaternion(); 29 | 30 | this.jumpVelocity = 5; 31 | this.yOffset = 0.5; 32 | this.tempVec = new THREE.Vector3(); 33 | this.moveDir = new THREE.Vector3(); 34 | this.xAxis = new THREE.Vector3(1.0, 0.0, 0.0); 35 | this.yAxis = new THREE.Vector3(0.0, 1.0, 0.0); 36 | } 37 | 38 | Initialize(){ 39 | this.physicsComponent = this.GetComponent("PlayerPhysics"); 40 | this.physicsBody = this.physicsComponent.body; 41 | this.transform = new Ammo.btTransform(); 42 | this.zeroVec = new Ammo.btVector3(0.0, 0.0, 0.0); 43 | this.angles.setFromQuaternion(this.parent.Rotation); 44 | this.UpdateRotation(); 45 | 46 | Input.AddMouseMoveListner(this.OnMouseMove); 47 | 48 | document.addEventListener('pointerlockchange', this.OnPointerlockChange) 49 | 50 | Input.AddClickListner( () => { 51 | if(!this.isLocked){ 52 | document.body.requestPointerLock(); 53 | } 54 | }); 55 | } 56 | 57 | OnPointerlockChange = () => { 58 | if (document.pointerLockElement) { 59 | this.isLocked = true; 60 | return; 61 | } 62 | 63 | this.isLocked = false; 64 | } 65 | 66 | OnMouseMove = (event) => { 67 | if (!this.isLocked) { 68 | return; 69 | } 70 | 71 | const { movementX, movementY } = event 72 | 73 | this.angles.y -= movementX * this.mouseSpeed; 74 | this.angles.x -= movementY * this.mouseSpeed; 75 | 76 | this.angles.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.angles.x)); 77 | 78 | this.UpdateRotation(); 79 | } 80 | 81 | UpdateRotation(){ 82 | this.pitch.setFromAxisAngle(this.xAxis, this.angles.x); 83 | this.yaw.setFromAxisAngle(this.yAxis, this.angles.y); 84 | 85 | this.parent.Rotation.multiplyQuaternions(this.yaw, this.pitch).normalize(); 86 | 87 | this.camera.quaternion.copy(this.parent.Rotation); 88 | } 89 | 90 | Accelarate = (direction, t) => { 91 | const accel = this.tempVec.copy(direction).multiplyScalar(this.acceleration * t); 92 | this.speed.add(accel); 93 | this.speed.clampLength(0.0, this.maxSpeed); 94 | } 95 | 96 | Deccelerate = (t) => { 97 | const frameDeccel = this.tempVec.copy(this.speed).multiplyScalar(this.decceleration * t); 98 | this.speed.add(frameDeccel); 99 | } 100 | 101 | Update(t){ 102 | const forwardFactor = Input.GetKeyDown("KeyS") - Input.GetKeyDown("KeyW"); 103 | const rightFactor = Input.GetKeyDown("KeyD") - Input.GetKeyDown("KeyA"); 104 | const direction = this.moveDir.set(rightFactor, 0.0, forwardFactor).normalize(); 105 | 106 | const velocity = this.physicsBody.getLinearVelocity(); 107 | 108 | if(Input.GetKeyDown('Space') && this.physicsComponent.canJump){ 109 | velocity.setY(this.jumpVelocity); 110 | this.physicsComponent.canJump = false; 111 | } 112 | 113 | this.Deccelerate(t); 114 | this.Accelarate(direction, t); 115 | 116 | const moveVector = this.tempVec.copy(this.speed); 117 | moveVector.applyQuaternion(this.yaw); 118 | 119 | velocity.setX(moveVector.x); 120 | velocity.setZ(moveVector.z); 121 | 122 | this.physicsBody.setLinearVelocity(velocity); 123 | this.physicsBody.setAngularVelocity(this.zeroVec); 124 | 125 | const ms = this.physicsBody.getMotionState(); 126 | if(ms){ 127 | ms.getWorldTransform(this.transform); 128 | const p = this.transform.getOrigin(); 129 | this.camera.position.set(p.x(), p.y() + this.yOffset, p.z()); 130 | this.parent.SetPosition(this.camera.position); 131 | } 132 | 133 | } 134 | } -------------------------------------------------------------------------------- /src/entities/Player/PlayerHealth.js: -------------------------------------------------------------------------------- 1 | import Component from "../../Component"; 2 | 3 | export default class PlayerHealth extends Component{ 4 | constructor(){ 5 | super(); 6 | 7 | this.health = 100; 8 | } 9 | 10 | TakeHit = e =>{ 11 | this.health = Math.max(0, this.health - 10); 12 | this.uimanager.SetHealth(this.health); 13 | } 14 | 15 | Initialize(){ 16 | this.uimanager = this.FindEntity("UIManager").GetComponent("UIManager"); 17 | this.parent.RegisterEventHandler(this.TakeHit, "hit"); 18 | this.uimanager.SetHealth(this.health); 19 | } 20 | } -------------------------------------------------------------------------------- /src/entities/Player/PlayerPhysics.js: -------------------------------------------------------------------------------- 1 | import Component from '../../Component' 2 | import {Ammo} from '../../AmmoLib' 3 | 4 | //Bullet enums 5 | const CF_KINEMATIC_OBJECT = 2; 6 | const DISABLE_DEACTIVATION = 4; 7 | 8 | export default class PlayerPhysics extends Component{ 9 | constructor(world){ 10 | super(); 11 | this.world = world; 12 | this.body = null; 13 | this.name = "PlayerPhysics"; 14 | this.canJump = false; 15 | this.up = new Ammo.btVector3(0,1,0); 16 | this.tempVec = new Ammo.btVector3(); 17 | } 18 | 19 | Initialize(){ 20 | const height = 1.3, 21 | radius = 0.3, 22 | mass = 5; 23 | 24 | const transform = new Ammo.btTransform(); 25 | transform.setIdentity(); 26 | const pos = this.parent.Position; 27 | transform.setOrigin(new Ammo.btVector3(pos.x,pos.y,pos.z)); 28 | const motionState = new Ammo.btDefaultMotionState(transform); 29 | 30 | const shape = new Ammo.btCapsuleShape(radius, height); 31 | const localInertia = new Ammo.btVector3(0,0,0); 32 | const bodyInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia); 33 | this.body = new Ammo.btRigidBody(bodyInfo); 34 | this.body.setFriction(0); 35 | 36 | //this.body.setCollisionFlags(this.body.getCollisionFlags() | CF_KINEMATIC_OBJECT); 37 | this.body.setActivationState(DISABLE_DEACTIVATION); 38 | 39 | this.world.addRigidBody(this.body); 40 | } 41 | 42 | QueryJump(){ 43 | const dispatcher = this.world.getDispatcher(); 44 | const numManifolds = dispatcher.getNumManifolds(); 45 | 46 | for ( let i = 0; i < numManifolds; i++ ) { 47 | const contactManifold = dispatcher.getManifoldByIndexInternal( i ); 48 | const rb0 = Ammo.castObject( contactManifold.getBody0(), Ammo.btRigidBody ); 49 | const rb1 = Ammo.castObject( contactManifold.getBody1(), Ammo.btRigidBody ); 50 | 51 | if(rb0 != this.body && rb1 != this.body){ 52 | continue; 53 | } 54 | 55 | const numContacts = contactManifold.getNumContacts(); 56 | 57 | for ( let j = 0; j < numContacts; j++ ) { 58 | const contactPoint = contactManifold.getContactPoint( j ); 59 | 60 | const normal = contactPoint.get_m_normalWorldOnB(); 61 | this.tempVec.setValue(normal.x(),normal.y(),normal.z()); 62 | 63 | if(rb1 == this.body){ 64 | this.tempVec.setValue(-this.tempVec.x(),-this.tempVec.y(),-this.tempVec.z()); 65 | } 66 | 67 | const angle = this.tempVec.dot(this.up); 68 | this.canJump = angle > 0.5; 69 | 70 | if(this.canJump){ 71 | return; 72 | } 73 | } 74 | } 75 | } 76 | 77 | PhysicsUpdate(){ 78 | this.QueryJump(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/entities/Player/Weapon.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import Component from '../../Component' 3 | import Input from '../../Input' 4 | import {Ammo, AmmoHelper, CollisionFilterGroups} from '../../AmmoLib' 5 | 6 | import WeaponFSM from './WeaponFSM'; 7 | 8 | 9 | export default class Weapon extends Component{ 10 | constructor(camera, model, flash, world, shotSoundBuffer, listner){ 11 | super(); 12 | this.name = 'Weapon'; 13 | this.camera = camera; 14 | this.world = world; 15 | this.model = model; 16 | this.flash = flash; 17 | this.animations = {}; 18 | this.shoot = false; 19 | this.fireRate = 0.1; 20 | this.shootTimer = 0.0; 21 | 22 | this.shotSoundBuffer = shotSoundBuffer; 23 | this.audioListner = listner; 24 | 25 | this.magAmmo = 30; 26 | this.ammoPerMag = 30; 27 | this.ammo = 100; 28 | this.damage = 2; 29 | this.uimanager = null; 30 | this.reloading = false; 31 | this.hitResult = {intersectionPoint: new THREE.Vector3(), intersectionNormal: new THREE.Vector3()}; 32 | 33 | } 34 | 35 | SetAnim(name, clip){ 36 | const action = this.mixer.clipAction(clip); 37 | this.animations[name] = {clip, action}; 38 | } 39 | 40 | SetAnimations(){ 41 | this.mixer = new THREE.AnimationMixer( this.model ); 42 | this.SetAnim('idle', this.model.animations[1]); 43 | this.SetAnim('reload', this.model.animations[2]); 44 | this.SetAnim('shoot', this.model.animations[0]); 45 | } 46 | 47 | SetMuzzleFlash(){ 48 | this.flash.position.set(-0.3, -0.5, 8.3); 49 | this.flash.rotateY(Math.PI); 50 | this.model.add(this.flash); 51 | this.flash.life = 0.0; 52 | 53 | this.flash.children[0].material.blending = THREE.AdditiveBlending; 54 | } 55 | 56 | SetSoundEffect(){ 57 | this.shotSound = new THREE.Audio(this.audioListner); 58 | this.shotSound.setBuffer(this.shotSoundBuffer); 59 | this.shotSound.setLoop(false); 60 | } 61 | 62 | AmmoPickup = (e) => { 63 | this.ammo += 30; 64 | this.uimanager.SetAmmo(this.magAmmo, this.ammo); 65 | } 66 | 67 | Initialize(){ 68 | const scene = this.model; 69 | scene.scale.set(0.05, 0.05, 0.05); 70 | scene.position.set(0.04, -0.02, 0.0); 71 | scene.setRotationFromEuler(new THREE.Euler(THREE.MathUtils.degToRad(5), THREE.MathUtils.degToRad(185), 0)); 72 | 73 | scene.traverse(child=>{ 74 | if(!child.isSkinnedMesh){ 75 | return; 76 | } 77 | 78 | child.receiveShadow = true; 79 | }); 80 | 81 | this.camera.add(scene); 82 | 83 | this.SetAnimations(); 84 | this.SetMuzzleFlash(); 85 | this.SetSoundEffect(); 86 | 87 | this.stateMachine = new WeaponFSM(this); 88 | this.stateMachine.SetState('idle'); 89 | 90 | this.uimanager = this.FindEntity("UIManager").GetComponent("UIManager"); 91 | this.uimanager.SetAmmo(this.magAmmo, this.ammo); 92 | 93 | this.SetupInput(); 94 | 95 | //Listen to ammo pickup event 96 | this.parent.RegisterEventHandler(this.AmmoPickup, "AmmoPickup"); 97 | } 98 | 99 | SetupInput(){ 100 | Input.AddMouseDownListner( e => { 101 | if(e.button != 0 || this.reloading){ 102 | return; 103 | } 104 | 105 | this.shoot = true; 106 | this.shootTimer = 0.0; 107 | }); 108 | 109 | Input.AddMouseUpListner( e => { 110 | if(e.button != 0){ 111 | return; 112 | } 113 | 114 | this.shoot = false; 115 | }); 116 | 117 | Input.AddKeyDownListner(e => { 118 | if(e.repeat) return; 119 | 120 | if(e.code == "KeyR"){ 121 | this.Reload(); 122 | } 123 | }); 124 | } 125 | 126 | Reload(){ 127 | if(this.reloading || this.magAmmo == this.ammoPerMag || this.ammo == 0){ 128 | return; 129 | } 130 | 131 | this.reloading = true; 132 | this.stateMachine.SetState('reload'); 133 | } 134 | 135 | ReloadDone(){ 136 | this.reloading = false; 137 | const bulletsNeeded = this.ammoPerMag - this.magAmmo; 138 | this.magAmmo = Math.min(this.ammo + this.magAmmo, this.ammoPerMag); 139 | this.ammo = Math.max(0, this.ammo - bulletsNeeded); 140 | this.uimanager.SetAmmo(this.magAmmo, this.ammo); 141 | } 142 | 143 | Raycast(){ 144 | const start = new THREE.Vector3(0.0, 0.0, -1.0); 145 | start.unproject(this.camera); 146 | const end = new THREE.Vector3(0.0, 0.0, 1.0); 147 | end.unproject(this.camera); 148 | 149 | const collisionMask = CollisionFilterGroups.AllFilter & ~CollisionFilterGroups.SensorTrigger; 150 | 151 | if(AmmoHelper.CastRay(this.world, start, end, this.hitResult, collisionMask)){ 152 | const ghostBody = Ammo.castObject( this.hitResult.collisionObject, Ammo.btPairCachingGhostObject ); 153 | const rigidBody = Ammo.castObject( this.hitResult.collisionObject, Ammo.btRigidBody ); 154 | const entity = ghostBody.parentEntity || rigidBody.parentEntity; 155 | 156 | entity && entity.Broadcast({'topic': 'hit', from: this.parent, amount: this.damage, hitResult: this.hitResult}); 157 | } 158 | } 159 | 160 | Shoot(t){ 161 | if(!this.shoot){ 162 | return; 163 | } 164 | 165 | if(!this.magAmmo){ 166 | //Reload automatically 167 | this.Reload(); 168 | return; 169 | } 170 | 171 | if(this.shootTimer <= 0.0 ){ 172 | //Shoot 173 | this.flash.life = this.fireRate; 174 | this.flash.rotateZ(Math.PI * Math.random()); 175 | const scale = Math.random() * (1.5 - 0.8) + 0.8; 176 | this.flash.scale.set(scale, 1, 1); 177 | this.shootTimer = this.fireRate; 178 | this.magAmmo = Math.max(0, this.magAmmo - 1); 179 | this.uimanager.SetAmmo(this.magAmmo, this.ammo); 180 | 181 | this.Raycast(); 182 | this.Broadcast({topic: 'ak47_shot'}); 183 | 184 | this.shotSound.isPlaying && this.shotSound.stop(); 185 | this.shotSound.play(); 186 | } 187 | 188 | this.shootTimer = Math.max(0.0, this.shootTimer - t); 189 | } 190 | 191 | AnimateMuzzle(t){ 192 | const mat = this.flash.children[0].material; 193 | const ratio = this.flash.life / this.fireRate; 194 | mat.opacity = ratio; 195 | this.flash.life = Math.max(0.0, this.flash.life - t); 196 | } 197 | 198 | Update(t){ 199 | this.mixer.update(t); 200 | this.stateMachine.Update(t); 201 | this.Shoot(t); 202 | this.AnimateMuzzle(t); 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /src/entities/Player/WeaponFSM.js: -------------------------------------------------------------------------------- 1 | import {FiniteStateMachine, State} from '../../FiniteStateMachine' 2 | import * as THREE from 'three' 3 | 4 | export default class WeaponFSM extends FiniteStateMachine{ 5 | constructor(proxy){ 6 | super(); 7 | this.proxy = proxy; 8 | this.Init(); 9 | } 10 | 11 | Init(){ 12 | this.AddState('idle', new IdleState(this)); 13 | this.AddState('shoot', new ShootState(this)); 14 | this.AddState('reload', new ReloadState(this)); 15 | } 16 | } 17 | 18 | class IdleState extends State{ 19 | constructor(parent){ 20 | super(parent); 21 | } 22 | 23 | get Name(){return 'idle'} 24 | get Animation(){return this.parent.proxy.animations['idle']; } 25 | 26 | Enter(prevState){ 27 | const action = this.Animation.action; 28 | 29 | if(prevState){ 30 | action.time = 0.0; 31 | action.enabled = true; 32 | action.setEffectiveTimeScale(1.0); 33 | action.crossFadeFrom(prevState.Animation.action, 0.1, true); 34 | } 35 | 36 | action.play(); 37 | } 38 | 39 | Update(t){ 40 | if(this.parent.proxy.shoot && this.parent.proxy.magAmmo > 0){ 41 | this.parent.SetState('shoot'); 42 | } 43 | } 44 | } 45 | 46 | class ShootState extends State{ 47 | constructor(parent){ 48 | super(parent); 49 | } 50 | 51 | get Name(){return 'shoot'} 52 | get Animation(){return this.parent.proxy.animations['shoot']; } 53 | 54 | Enter(prevState){ 55 | const action = this.Animation.action; 56 | 57 | if(prevState){ 58 | action.time = 0.0; 59 | action.enabled = true; 60 | action.setEffectiveTimeScale(1.0); 61 | action.crossFadeFrom(prevState.Animation.action, 0.1, true); 62 | } 63 | 64 | action.timeScale = 3.0; 65 | action.play(); 66 | } 67 | 68 | Update(t){ 69 | if(!this.parent.proxy.shoot || this.parent.proxy.magAmmo == 0){ 70 | this.parent.SetState('idle'); 71 | } 72 | } 73 | } 74 | 75 | class ReloadState extends State{ 76 | constructor(parent){ 77 | super(parent); 78 | 79 | this.parent.proxy.mixer.addEventListener( 'finished', this.AnimationFinished); 80 | } 81 | 82 | get Name(){ return 'reload'; } 83 | get Animation(){ return this.parent.proxy.animations['reload']; } 84 | 85 | AnimationFinished = e => { 86 | if(e.action != this.Animation.action){ 87 | return; 88 | } 89 | 90 | this.parent.proxy.ReloadDone(); 91 | this.parent.SetState('idle'); 92 | } 93 | 94 | Enter(prevState){ 95 | const action = this.Animation.action; 96 | action.loop = THREE.LoopOnce; 97 | 98 | if(prevState){ 99 | action.time = 0.0; 100 | action.enabled = true; 101 | action.setEffectiveTimeScale(1.0); 102 | action.crossFadeFrom(prevState.Animation.action, 0.1, true); 103 | } 104 | 105 | action.play(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/entities/Sky/Sky.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Component from '../../Component' 3 | 4 | const _VS = ` 5 | varying vec3 vWorldPosition; 6 | void main() { 7 | vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); 8 | vWorldPosition = worldPosition.xyz; 9 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 10 | }`; 11 | 12 | const _FS = ` 13 | uniform vec3 topColor; 14 | uniform vec3 bottomColor; 15 | uniform float offset; 16 | uniform float exponent; 17 | varying vec3 vWorldPosition; 18 | void main() { 19 | float h = normalize( vWorldPosition + offset ).y; 20 | gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 ); 21 | }`; 22 | 23 | 24 | export default class Sky extends Component{ 25 | constructor(scene){ 26 | super(); 27 | this.scene = scene; 28 | } 29 | 30 | Initialize(){ 31 | const hemiLight = new THREE.HemisphereLight(0xFFFFFF, 0xFFFFFFF, 0.6); 32 | hemiLight.color.setHSL(0.6, 1, 0.6); 33 | hemiLight.groundColor.setHSL(0.095, 1, 0.75); 34 | this.scene.add(hemiLight); 35 | 36 | const uniforms = { 37 | "topColor": { value: new THREE.Color(0x0077ff) }, 38 | "bottomColor": { value: new THREE.Color(0xffffff) }, 39 | "offset": { value: 33 }, 40 | "exponent": { value: 0.6 } 41 | }; 42 | uniforms["topColor"].value.copy(hemiLight.color); 43 | 44 | this.scene.fog = new THREE.FogExp2(0x89b2eb, 0.002); 45 | this.scene.fog.color.copy(uniforms["bottomColor"].value); 46 | 47 | const skyGeo = new THREE.SphereBufferGeometry(1000, 32, 15); 48 | const skyMat = new THREE.ShaderMaterial({ 49 | uniforms: uniforms, 50 | vertexShader: _VS, 51 | fragmentShader: _FS, 52 | side: THREE.BackSide 53 | }); 54 | 55 | const sky = new THREE.Mesh(skyGeo, skyMat); 56 | this.scene.add(sky); 57 | } 58 | } -------------------------------------------------------------------------------- /src/entities/Sky/Sky2.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Component from '../../Component' 3 | 4 | 5 | export default class Sky extends Component{ 6 | constructor(scene, skyTexture){ 7 | super(); 8 | this.scene = scene; 9 | this.name = 'Sky'; 10 | this.texture = skyTexture; 11 | } 12 | 13 | Initialize(){ 14 | const hemiLight = new THREE.HemisphereLight(0xFFFFFF, 0xFFFFFFF, 1); 15 | hemiLight.color.setHSL(0.6, 1, 0.6); 16 | hemiLight.groundColor.setHSL(0.095, 1, 0.75); 17 | this.scene.add(hemiLight); 18 | 19 | const skyGeo = new THREE.SphereGeometry(1000, 25, 25); 20 | const skyMat = new THREE.MeshBasicMaterial({ 21 | map: this.texture, 22 | side: THREE.BackSide, 23 | depthWrite: false, 24 | toneMapped: false 25 | }); 26 | const sky = new THREE.Mesh(skyGeo, skyMat); 27 | sky.rotateY(THREE.MathUtils.degToRad(-60)); 28 | this.scene.add(sky); 29 | } 30 | } -------------------------------------------------------------------------------- /src/entities/UI/UIManager.js: -------------------------------------------------------------------------------- 1 | import Component from '../../Component' 2 | 3 | export default class UIManager extends Component{ 4 | constructor(){ 5 | super(); 6 | this.name = 'UIManager'; 7 | } 8 | 9 | SetAmmo(mag, rest){ 10 | document.getElementById("current_ammo").innerText = mag; 11 | document.getElementById("max_ammo").innerText = rest; 12 | } 13 | 14 | SetHealth(health){ 15 | document.getElementById("health_progress").style.width = `${health}%`; 16 | } 17 | 18 | Initialize(){ 19 | document.getElementById("game_hud").style.visibility = 'visible'; 20 | } 21 | } -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * entry.js 3 | * 4 | * This is the first file loaded. It sets up the Renderer, 5 | * Scene, Physics and Entities. It also starts the render loop and 6 | * handles window resizes. 7 | * 8 | */ 9 | 10 | import * as THREE from 'three' 11 | import {AmmoHelper, Ammo, createConvexHullShape} from './AmmoLib' 12 | import EntityManager from './EntityManager' 13 | import Entity from './Entity' 14 | import Sky from './entities/Sky/Sky2' 15 | import LevelSetup from './entities/Level/LevelSetup' 16 | import PlayerControls from './entities/Player/PlayerControls' 17 | import PlayerPhysics from './entities/Player/PlayerPhysics' 18 | import Stats from 'three/examples/jsm/libs/stats.module' 19 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' 20 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' 21 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' 22 | import { SkeletonUtils } from 'three/examples/jsm/utils/SkeletonUtils' 23 | import NpcCharacterController from './entities/NPC/CharacterController' 24 | import Input from './Input' 25 | 26 | import level from './assets/level.glb' 27 | import navmesh from './assets/navmesh.obj' 28 | 29 | import mutant from './assets/animations/mutant.fbx' 30 | import idleAnim from './assets/animations/mutant breathing idle.fbx' 31 | import attackAnim from './assets/animations/Mutant Punch.fbx' 32 | import walkAnim from './assets/animations/mutant walking.fbx' 33 | import runAnim from './assets/animations/mutant run.fbx' 34 | import dieAnim from './assets/animations/mutant dying.fbx' 35 | 36 | //AK47 Model and textures 37 | import ak47 from './assets/guns/ak47/ak47.glb' 38 | import muzzleFlash from './assets/muzzle_flash.glb' 39 | //Shot sound 40 | import ak47Shot from './assets/sounds/ak47_shot.wav' 41 | 42 | //Ammo box 43 | import ammobox from './assets/ammo/AmmoBox.fbx' 44 | import ammoboxTexD from './assets/ammo/AmmoBox_D.tga.png' 45 | import ammoboxTexN from './assets/ammo/AmmoBox_N.tga.png' 46 | import ammoboxTexM from './assets/ammo/AmmoBox_M.tga.png' 47 | import ammoboxTexR from './assets/ammo/AmmoBox_R.tga.png' 48 | import ammoboxTexAO from './assets/ammo/AmmoBox_AO.tga.png' 49 | 50 | //Bullet Decal 51 | import decalColor from './assets/decals/decal_c.jpg' 52 | import decalNormal from './assets/decals/decal_n.jpg' 53 | import decalAlpha from './assets/decals/decal_a.jpg' 54 | 55 | //Sky 56 | import skyTex from './assets/sky.jpg' 57 | 58 | import DebugDrawer from './DebugDrawer' 59 | import Navmesh from './entities/Level/Navmesh' 60 | import AttackTrigger from './entities/NPC/AttackTrigger' 61 | import DirectionDebug from './entities/NPC/DirectionDebug' 62 | import CharacterCollision from './entities/NPC/CharacterCollision' 63 | import Weapon from './entities/Player/Weapon' 64 | import UIManager from './entities/UI/UIManager' 65 | import AmmoBox from './entities/AmmoBox/AmmoBox' 66 | import LevelBulletDecals from './entities/Level/BulletDecals' 67 | import PlayerHealth from './entities/Player/PlayerHealth' 68 | 69 | class FPSGameApp{ 70 | 71 | constructor(){ 72 | this.lastFrameTime = null; 73 | this.assets = {}; 74 | this.animFrameId = 0; 75 | 76 | AmmoHelper.Init(()=>{this.Init();}); 77 | } 78 | 79 | Init(){ 80 | this.LoadAssets(); 81 | this.SetupGraphics(); 82 | this.SetupStartButton(); 83 | } 84 | 85 | SetupGraphics(){ 86 | this.scene = new THREE.Scene(); 87 | this.renderer = new THREE.WebGLRenderer({antialias: true}); 88 | this.renderer.shadowMap.enabled = true; 89 | this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; 90 | 91 | this.renderer.toneMapping = THREE.ReinhardToneMapping; 92 | this.renderer.toneMappingExposure = 1; 93 | this.renderer.outputEncoding = THREE.sRGBEncoding; 94 | 95 | this.camera = new THREE.PerspectiveCamera(); 96 | this.camera.near = 0.01; 97 | 98 | // create an AudioListener and add it to the camera 99 | this.listener = new THREE.AudioListener(); 100 | this.camera.add( this.listener ); 101 | 102 | // renderer 103 | this.renderer.setPixelRatio(window.devicePixelRatio); 104 | 105 | this.WindowResizeHanlder(); 106 | window.addEventListener('resize', this.WindowResizeHanlder); 107 | 108 | document.body.appendChild( this.renderer.domElement ); 109 | 110 | // Stats.js 111 | this.stats = new Stats(); 112 | document.body.appendChild(this.stats.dom); 113 | } 114 | 115 | SetupPhysics() { 116 | // Physics configuration 117 | const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); 118 | const dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration ); 119 | const broadphase = new Ammo.btDbvtBroadphase(); 120 | const solver = new Ammo.btSequentialImpulseConstraintSolver(); 121 | this.physicsWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration ); 122 | this.physicsWorld.setGravity( new Ammo.btVector3( 0.0, -9.81, 0.0 ) ); 123 | const fp = Ammo.addFunction(this.PhysicsUpdate); 124 | this.physicsWorld.setInternalTickCallback(fp); 125 | this.physicsWorld.getBroadphase().getOverlappingPairCache().setInternalGhostPairCallback(new Ammo.btGhostPairCallback()); 126 | 127 | //Physics debug drawer 128 | //this.debugDrawer = new DebugDrawer(this.scene, this.physicsWorld); 129 | //this.debugDrawer.enable(); 130 | } 131 | 132 | SetAnim(name, obj){ 133 | const clip = obj.animations[0]; 134 | this.mutantAnims[name] = clip; 135 | } 136 | 137 | PromiseProgress(proms, progress_cb){ 138 | let d = 0; 139 | progress_cb(0); 140 | for (const p of proms) { 141 | p.then(()=> { 142 | d++; 143 | progress_cb( (d / proms.length) * 100 ); 144 | }); 145 | } 146 | return Promise.all(proms); 147 | } 148 | 149 | AddAsset(asset, loader, name){ 150 | return loader.loadAsync(asset).then( result =>{ 151 | this.assets[name] = result; 152 | }); 153 | } 154 | 155 | OnProgress(p){ 156 | const progressbar = document.getElementById('progress'); 157 | progressbar.style.width = `${p}%`; 158 | } 159 | 160 | HideProgress(){ 161 | this.OnProgress(0); 162 | } 163 | 164 | SetupStartButton(){ 165 | document.getElementById('start_game').addEventListener('click', this.StartGame); 166 | } 167 | 168 | ShowMenu(visible=true){ 169 | document.getElementById('menu').style.visibility = visible?'visible':'hidden'; 170 | } 171 | 172 | async LoadAssets(){ 173 | const gltfLoader = new GLTFLoader(); 174 | const fbxLoader = new FBXLoader(); 175 | const objLoader = new OBJLoader(); 176 | const audioLoader = new THREE.AudioLoader(); 177 | const texLoader = new THREE.TextureLoader(); 178 | const promises = []; 179 | 180 | //Level 181 | promises.push(this.AddAsset(level, gltfLoader, "level")); 182 | promises.push(this.AddAsset(navmesh, objLoader, "navmesh")); 183 | //Mutant 184 | promises.push(this.AddAsset(mutant, fbxLoader, "mutant")); 185 | promises.push(this.AddAsset(idleAnim, fbxLoader, "idleAnim")); 186 | promises.push(this.AddAsset(walkAnim, fbxLoader, "walkAnim")); 187 | promises.push(this.AddAsset(runAnim, fbxLoader, "runAnim")); 188 | promises.push(this.AddAsset(attackAnim, fbxLoader, "attackAnim")); 189 | promises.push(this.AddAsset(dieAnim, fbxLoader, "dieAnim")); 190 | //AK47 191 | promises.push(this.AddAsset(ak47, gltfLoader, "ak47")); 192 | promises.push(this.AddAsset(muzzleFlash, gltfLoader, "muzzleFlash")); 193 | promises.push(this.AddAsset(ak47Shot, audioLoader, "ak47Shot")); 194 | //Ammo box 195 | promises.push(this.AddAsset(ammobox, fbxLoader, "ammobox")); 196 | promises.push(this.AddAsset(ammoboxTexD, texLoader, "ammoboxTexD")); 197 | promises.push(this.AddAsset(ammoboxTexN, texLoader, "ammoboxTexN")); 198 | promises.push(this.AddAsset(ammoboxTexM, texLoader, "ammoboxTexM")); 199 | promises.push(this.AddAsset(ammoboxTexR, texLoader, "ammoboxTexR")); 200 | promises.push(this.AddAsset(ammoboxTexAO, texLoader, "ammoboxTexAO")); 201 | //Decal 202 | promises.push(this.AddAsset(decalColor, texLoader, "decalColor")); 203 | promises.push(this.AddAsset(decalNormal, texLoader, "decalNormal")); 204 | promises.push(this.AddAsset(decalAlpha, texLoader, "decalAlpha")); 205 | 206 | promises.push(this.AddAsset(skyTex, texLoader, "skyTex")); 207 | 208 | await this.PromiseProgress(promises, this.OnProgress); 209 | 210 | this.assets['level'] = this.assets['level'].scene; 211 | this.assets['muzzleFlash'] = this.assets['muzzleFlash'].scene; 212 | 213 | //Extract mutant anims 214 | this.mutantAnims = {}; 215 | this.SetAnim('idle', this.assets['idleAnim']); 216 | this.SetAnim('walk', this.assets['walkAnim']); 217 | this.SetAnim('run', this.assets['runAnim']); 218 | this.SetAnim('attack', this.assets['attackAnim']); 219 | this.SetAnim('die', this.assets['dieAnim']); 220 | 221 | this.assets['ak47'].scene.animations = this.assets['ak47'].animations; 222 | 223 | //Set ammo box textures and other props 224 | this.assets['ammobox'].scale.set(0.01, 0.01, 0.01); 225 | this.assets['ammobox'].traverse(child =>{ 226 | child.castShadow = true; 227 | child.receiveShadow = true; 228 | 229 | child.material = new THREE.MeshStandardMaterial({ 230 | map: this.assets['ammoboxTexD'], 231 | aoMap: this.assets['ammoboxTexAO'], 232 | normalMap: this.assets['ammoboxTexN'], 233 | metalness: 1, 234 | metalnessMap: this.assets['ammoboxTexM'], 235 | roughnessMap: this.assets['ammoboxTexR'], 236 | color: new THREE.Color(0.4, 0.4, 0.4) 237 | }); 238 | 239 | }); 240 | 241 | this.assets['ammoboxShape'] = createConvexHullShape(this.assets['ammobox']); 242 | 243 | this.HideProgress(); 244 | this.ShowMenu(); 245 | } 246 | 247 | EntitySetup(){ 248 | this.entityManager = new EntityManager(); 249 | 250 | const levelEntity = new Entity(); 251 | levelEntity.SetName('Level'); 252 | levelEntity.AddComponent(new LevelSetup(this.assets['level'], this.scene, this.physicsWorld)); 253 | levelEntity.AddComponent(new Navmesh(this.scene, this.assets['navmesh'])); 254 | levelEntity.AddComponent(new LevelBulletDecals(this.scene, this.assets['decalColor'], this.assets['decalNormal'], this.assets['decalAlpha'])); 255 | this.entityManager.Add(levelEntity); 256 | 257 | const skyEntity = new Entity(); 258 | skyEntity.SetName("Sky"); 259 | skyEntity.AddComponent(new Sky(this.scene, this.assets['skyTex'])); 260 | this.entityManager.Add(skyEntity); 261 | 262 | const playerEntity = new Entity(); 263 | playerEntity.SetName("Player"); 264 | playerEntity.AddComponent(new PlayerPhysics(this.physicsWorld, Ammo)); 265 | playerEntity.AddComponent(new PlayerControls(this.camera, this.scene)); 266 | playerEntity.AddComponent(new Weapon(this.camera, this.assets['ak47'].scene, this.assets['muzzleFlash'], this.physicsWorld, this.assets['ak47Shot'], this.listener )); 267 | playerEntity.AddComponent(new PlayerHealth()); 268 | playerEntity.SetPosition(new THREE.Vector3(2.14, 1.48, -1.36)); 269 | playerEntity.SetRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), -Math.PI * 0.5)); 270 | this.entityManager.Add(playerEntity); 271 | 272 | const npcLocations = [ 273 | [10.8, 0.0, 22.0], 274 | ]; 275 | 276 | npcLocations.forEach((v,i)=>{ 277 | const npcEntity = new Entity(); 278 | npcEntity.SetPosition(new THREE.Vector3(v[0], v[1], v[2])); 279 | npcEntity.SetName(`Mutant${i}`); 280 | npcEntity.AddComponent(new NpcCharacterController(SkeletonUtils.clone(this.assets['mutant']), this.mutantAnims, this.scene, this.physicsWorld)); 281 | npcEntity.AddComponent(new AttackTrigger(this.physicsWorld)); 282 | npcEntity.AddComponent(new CharacterCollision(this.physicsWorld)); 283 | npcEntity.AddComponent(new DirectionDebug(this.scene)); 284 | this.entityManager.Add(npcEntity); 285 | }); 286 | 287 | const uimanagerEntity = new Entity(); 288 | uimanagerEntity.SetName("UIManager"); 289 | uimanagerEntity.AddComponent(new UIManager()); 290 | this.entityManager.Add(uimanagerEntity); 291 | 292 | const ammoLocations = [ 293 | [14.37, 0.0, 10.45], 294 | [32.77, 0.0, 33.84], 295 | ]; 296 | 297 | ammoLocations.forEach((loc, i) => { 298 | const box = new Entity(); 299 | box.SetName(`AmmoBox${i}`); 300 | box.AddComponent(new AmmoBox(this.scene, this.assets['ammobox'].clone(), this.assets['ammoboxShape'], this.physicsWorld)); 301 | box.SetPosition(new THREE.Vector3(loc[0], loc[1], loc[2])); 302 | this.entityManager.Add(box); 303 | }); 304 | 305 | this.entityManager.EndSetup(); 306 | 307 | this.scene.add(this.camera); 308 | this.animFrameId = window.requestAnimationFrame(this.OnAnimationFrameHandler); 309 | } 310 | 311 | StartGame = ()=>{ 312 | window.cancelAnimationFrame(this.animFrameId); 313 | Input.ClearEventListners(); 314 | 315 | //Create entities and physics 316 | this.scene.clear(); 317 | this.SetupPhysics(); 318 | this.EntitySetup(); 319 | this.ShowMenu(false); 320 | } 321 | 322 | // resize 323 | WindowResizeHanlder = () => { 324 | const { innerHeight, innerWidth } = window; 325 | this.renderer.setSize(innerWidth, innerHeight); 326 | this.camera.aspect = innerWidth / innerHeight; 327 | this.camera.updateProjectionMatrix(); 328 | } 329 | 330 | // render loop 331 | OnAnimationFrameHandler = (t) => { 332 | if(this.lastFrameTime===null){ 333 | this.lastFrameTime = t; 334 | } 335 | 336 | const delta = t-this.lastFrameTime; 337 | let timeElapsed = Math.min(1.0 / 30.0, delta * 0.001); 338 | this.Step(timeElapsed); 339 | this.lastFrameTime = t; 340 | 341 | this.animFrameId = window.requestAnimationFrame(this.OnAnimationFrameHandler); 342 | } 343 | 344 | PhysicsUpdate = (world, timeStep)=>{ 345 | this.entityManager.PhysicsUpdate(world, timeStep); 346 | } 347 | 348 | Step(elapsedTime){ 349 | this.physicsWorld.stepSimulation( elapsedTime, 10 ); 350 | //this.debugDrawer.update(); 351 | 352 | this.entityManager.Update(elapsedTime); 353 | 354 | this.renderer.render(this.scene, this.camera); 355 | this.stats.update(); 356 | } 357 | 358 | } 359 | 360 | let _APP = null; 361 | window.addEventListener('DOMContentLoaded', () => { 362 | _APP = new FPSGameApp(); 363 | }); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 84 | 85 | 86 | 87 |
88 | 92 |
93 | 94 |
95 | 0 96 | | 97 | 0 98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /src/ui/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsenheydari/three-fps/625a18c2240687796a0440190cfe3dd7f3ce45a2/src/ui/crosshair.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const buildPath = './build/'; 5 | 6 | module.exports = { 7 | entry: ['./src/entry.js'], 8 | output: { 9 | path: path.join(__dirname, buildPath), 10 | filename: '[name].[fullhash].js', 11 | publicPath: '', 12 | }, 13 | mode: 'development', 14 | target: 'web', 15 | devtool: 'source-map', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | use: 'babel-loader', 21 | exclude: path.resolve(__dirname, './node_modules/') 22 | }, 23 | { 24 | test: /\.(jpe?g|png|gif|svg|tga|glb|gltf|bin|fbx|babylon|mtl|pcb|pcd|prwm|obj|mat|mp3|ogg|wav)$/i, 25 | use: { 26 | loader: 'file-loader', 27 | options: { 28 | esModule: false 29 | } 30 | }, 31 | exclude: path.resolve(__dirname, './node_modules/') 32 | }, 33 | ] 34 | }, 35 | resolve: { 36 | alias: { 37 | vendor: path.resolve(__dirname, 'vendor') 38 | }, 39 | fallback: { 40 | 'fs': false, 41 | 'path': false, // ammo.js seems to also use path 42 | } 43 | }, 44 | plugins: [ 45 | new HtmlWebpackPlugin({'title': 'Three.js FPS Demo | Venolabs', template: './src/index.html'}) 46 | ] 47 | } 48 | --------------------------------------------------------------------------------