├── .babelrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── public └── index.html ├── screenshot.png ├── src ├── App.scss ├── Types.ts ├── components │ ├── Defeat.tsx │ ├── Display.tsx │ └── Overlay.tsx ├── constants │ ├── ActionType.ts │ └── index.ts ├── functions │ ├── addOverlay.tsx │ ├── addWindowEvents.ts │ ├── createShader.ts │ └── tick.ts ├── img │ └── money.png ├── index.d.ts ├── index.ts ├── reducers │ └── gameState.ts └── shaders │ ├── fog.frag │ ├── index.ts │ ├── scanlines.frag │ └── vignette.frag ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | npm-error.log 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": true, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2020, Mat Sz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted (subject to the limitations in the disclaimer 6 | below) provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 20 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 23 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 24 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 28 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flight 2 | 3 |

4 | Screenshot 5 |

6 | 7 | A simple game made so I can test a few things out. 8 | 9 | three.js, TypeScript, React, Redux and GLSL shaders at once. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flight", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "scripts": { 6 | "start": "NODE_ENV=development webpack-dev-server --mode development --open --hot --host 0.0.0.0", 7 | "build": "NODE_ENV=production webpack --mode production" 8 | }, 9 | "husky": { 10 | "hooks": { 11 | "pre-commit": "lint-staged" 12 | } 13 | }, 14 | "lint-staged": { 15 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 16 | "prettier --write" 17 | ], 18 | "__tests__/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 19 | "prettier --write" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.6.4", 24 | "@babel/plugin-proposal-class-properties": "^7.5.5", 25 | "@babel/preset-env": "^7.6.3", 26 | "@babel/preset-react": "^7.7.4", 27 | "babel-loader": "^8.0.6", 28 | "css-loader": "^3.2.0", 29 | "file-loader": "^5.0.2", 30 | "html-webpack-plugin": "^3.2.0", 31 | "husky": "^4.2.5", 32 | "image-webpack-loader": "^6.0.0", 33 | "lint-staged": "^10.2.11", 34 | "node-sass": "^4.13.0", 35 | "prettier": "^2.0.5", 36 | "raw-loader": "^4.0.0", 37 | "sass-loader": "^8.0.0", 38 | "style-loader": "^1.0.0", 39 | "ts-loader": "^6.2.1", 40 | "typescript": "^3.7.2", 41 | "webpack": "^4.41.2", 42 | "webpack-cli": "^3.3.9", 43 | "webpack-dev-server": "^3.9.0" 44 | }, 45 | "dependencies": { 46 | "@types/classnames": "^2.2.9", 47 | "@types/react": "^16.9.15", 48 | "@types/react-dom": "^16.9.4", 49 | "@types/react-redux": "^7.1.5", 50 | "@types/redux": "^3.6.0", 51 | "classnames": "^2.2.6", 52 | "react": "^16.12.0", 53 | "react-device-detect": "^1.11.14", 54 | "react-dom": "^16.12.0", 55 | "react-redux": "^7.1.3", 56 | "redux": "^4.0.4", 57 | "three": "^0.111.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flight 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mat-sz/flight/83dc25110f0167fc6df7f9c7ff0f96eb8ff0934c/screenshot.png -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | overflow: hidden; 4 | } 5 | 6 | body { 7 | position: fixed; 8 | color: white; 9 | font-family: monospace; 10 | 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | flex-direction: column; 15 | width: 100%; 16 | margin: 0; 17 | } 18 | 19 | button { 20 | background: rgba(255, 255, 255, 0.75); 21 | border: none; 22 | color: black; 23 | font-size: 20px; 24 | padding: 10px 15px; 25 | cursor: pointer; 26 | transition: 0.2s ease-in-out all; 27 | 28 | &:hover { 29 | background: rgba(255, 255, 255, 1); 30 | } 31 | } 32 | 33 | .github-corner { 34 | z-index: 500; 35 | 36 | &:hover .octo-arm { 37 | animation: octocat-wave 560ms ease-in-out; 38 | } 39 | } 40 | 41 | @keyframes octocat-wave { 42 | 0%, 43 | 100% { 44 | transform: rotate(0); 45 | } 46 | 47 | 20%, 48 | 60% { 49 | transform: rotate(-25deg); 50 | } 51 | 52 | 40%, 53 | 80% { 54 | transform: rotate(10deg); 55 | } 56 | } 57 | 58 | .display { 59 | background: rgba(255, 255, 255, 0.05); 60 | color: white; 61 | margin: 10px; 62 | padding: 10px; 63 | width: 150px; 64 | 65 | &__title { 66 | color: #999; 67 | font-size: 12px; 68 | } 69 | 70 | &__value { 71 | font-size: 24px; 72 | will-change: contents; 73 | } 74 | } 75 | 76 | .overlay { 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | right: 0; 81 | bottom: 0; 82 | 83 | .score { 84 | position: absolute; 85 | left: 5%; 86 | top: 5%; 87 | font-size: 20px; 88 | } 89 | 90 | .money { 91 | position: absolute; 92 | top: 5%; 93 | right: 5%; 94 | font-size: 20px; 95 | 96 | display: flex; 97 | justify-content: flex-end; 98 | 99 | img { 100 | margin-bottom: -1px; 101 | max-height: 20px; 102 | } 103 | } 104 | 105 | .help { 106 | position: absolute; 107 | bottom: 5%; 108 | font-size: 16px; 109 | left: 5%; 110 | right: 5%; 111 | text-align: center; 112 | } 113 | 114 | .defeat.hidden { 115 | opacity: 0; 116 | pointer-events: none; 117 | } 118 | 119 | .defeat { 120 | color: white; 121 | position: absolute; 122 | top: 0; 123 | left: 0; 124 | right: 0; 125 | bottom: 0; 126 | 127 | display: flex; 128 | justify-content: center; 129 | align-items: center; 130 | flex-direction: column; 131 | 132 | background: rgba(255, 255, 255, 0.05); 133 | transition: 0.2s ease-in-out all; 134 | 135 | .text { 136 | mix-blend-mode: difference; 137 | font-weight: bold; 138 | font-size: 90px; 139 | } 140 | } 141 | } 142 | 143 | @media screen and (min-width: 1000px) { 144 | .overlay { 145 | .score { 146 | left: 10%; 147 | top: 10%; 148 | } 149 | 150 | .money { 151 | top: 10%; 152 | right: 10%; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | import ActionType from './constants/ActionType'; 2 | 3 | export interface Action { 4 | type: ActionType; 5 | value?: number | boolean; 6 | } 7 | 8 | export interface GameState { 9 | score: number; 10 | highScore: number; 11 | lane: number; 12 | money: number; 13 | defeat: boolean; 14 | } 15 | 16 | export interface SavedState { 17 | highScore: number; 18 | money: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Defeat.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | 5 | import { GameState } from '../Types'; 6 | 7 | const Defeat = ({ onReset }: { onReset: () => void }) => { 8 | const defeat = useSelector((state: GameState) => state.defeat); 9 | 10 | return ( 11 |
16 |
Defeat
17 |
18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Defeat; 25 | -------------------------------------------------------------------------------- /src/components/Display.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Display = ({ title, value }: { title: string; value: any }) => { 4 | return ( 5 |
6 |
{title}:
7 |
{value}
8 |
9 | ); 10 | }; 11 | 12 | export default Display; 13 | -------------------------------------------------------------------------------- /src/components/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { MobileView, BrowserView } from 'react-device-detect'; 4 | 5 | import moneyImage from '../img/money.png'; 6 | 7 | import { GameState } from '../Types'; 8 | import Defeat from './Defeat'; 9 | import Display from './Display'; 10 | 11 | const Overlay = ({ onReset }: { onReset: () => void }) => { 12 | const score = useSelector((state: GameState) => state.score); 13 | const highScore = useSelector((state: GameState) => state.highScore); 14 | const money = useSelector((state: GameState) => state.money); 15 | const defeat = useSelector((state: GameState) => state.defeat); 16 | 17 | return ( 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | 29 | {Math.round(money)} Money 30 | 31 | } 32 | /> 33 |
34 |
35 | 36 | Tap on left and right sides of the screen to move. 37 | 38 | 39 | {defeat 40 | ? 'Press Space to restart.' 41 | : 'Use left and right arrow keys to move.'} 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Overlay; 49 | -------------------------------------------------------------------------------- /src/constants/ActionType.ts: -------------------------------------------------------------------------------- 1 | enum ActionType { 2 | SET_SCORE, 3 | SET_MONEY, 4 | SET_DEFEAT, 5 | SET_LANE, 6 | ADD_SCORE, 7 | ADD_MONEY, 8 | } 9 | 10 | export default ActionType; 11 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const laneWidth = 0.4; 2 | -------------------------------------------------------------------------------- /src/functions/addOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import Overlay from '../components/Overlay'; 6 | 7 | export default function addOverlay(store: any, onReset: () => void) { 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('overlay') 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/functions/addWindowEvents.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, WebGLRenderer } from 'three'; 2 | import { GameState } from '../Types'; 3 | import { Store, Action } from 'redux'; 4 | import ActionType from '../constants/ActionType'; 5 | 6 | export default function addWindowEvents( 7 | camera: PerspectiveCamera, 8 | renderer: WebGLRenderer, 9 | store: Store, 10 | onReset: () => void 11 | ) { 12 | window.addEventListener('resize', () => { 13 | camera.aspect = window.innerWidth / window.innerHeight; 14 | camera.updateProjectionMatrix(); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | }); 17 | 18 | window.addEventListener('keydown', event => { 19 | const state = store.getState(); 20 | switch (event.key) { 21 | case 'ArrowLeft': 22 | if (state.lane > -1) { 23 | store.dispatch({ type: ActionType.SET_LANE, value: state.lane - 1 }); 24 | } 25 | break; 26 | case 'ArrowRight': 27 | if (state.lane < 1) { 28 | store.dispatch({ type: ActionType.SET_LANE, value: state.lane + 1 }); 29 | } 30 | break; 31 | case ' ': 32 | if (state.defeat) { 33 | onReset(); 34 | } 35 | break; 36 | } 37 | }); 38 | 39 | window.addEventListener('touchstart', event => { 40 | const planeLane = store.getState().lane; 41 | const touch = event.touches[0]; 42 | 43 | if (touch.pageX < window.innerWidth / 2) { 44 | if (planeLane > -1) 45 | store.dispatch({ type: ActionType.SET_LANE, value: planeLane - 1 }); 46 | } else { 47 | if (planeLane < 1) 48 | store.dispatch({ type: ActionType.SET_LANE, value: planeLane + 1 }); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/functions/createShader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | WebGLRenderTarget, 4 | Vector2, 5 | Vector3, 6 | OrthographicCamera, 7 | ShaderMaterial, 8 | Mesh, 9 | PlaneBufferGeometry, 10 | Scene, 11 | Uniform, 12 | } from 'three'; 13 | 14 | /** 15 | * Creates a function that allows shaders to be easily applied. 16 | * @param renderer 17 | * @param fragmentShader 18 | * @param vertexShader 19 | */ 20 | export default function createShader( 21 | renderer: WebGLRenderer, 22 | fragmentShader?: string, 23 | vertexShader?: string, 24 | customUniforms?: { [key: string]: Uniform } 25 | ) { 26 | const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1); 27 | const scene = new Scene(); 28 | 29 | let uniforms = { 30 | iTime: new Uniform(0), 31 | iResolution: new Uniform(new Vector3()), 32 | iTexture: new Uniform(null), 33 | ...customUniforms, 34 | }; 35 | 36 | const shaderMaterial: ShaderMaterial = new ShaderMaterial({ 37 | uniforms, 38 | fragmentShader, 39 | vertexShader, 40 | }); 41 | 42 | const shaderMesh = new Mesh(new PlaneBufferGeometry(2, 2), shaderMaterial); 43 | shaderMesh.frustumCulled = false; 44 | const outputBuffer: WebGLRenderTarget = new WebGLRenderTarget(1, 1); 45 | 46 | scene.add(shaderMesh); 47 | 48 | return (inputBuffer: WebGLRenderTarget, time: number, finalPass = false) => { 49 | uniforms.iResolution.value.set( 50 | renderer.domElement.width, 51 | renderer.domElement.height, 52 | 1 53 | ); 54 | uniforms.iTime.value = time * 0.001; 55 | uniforms.iTexture.value = inputBuffer.texture; 56 | 57 | const size = renderer.getDrawingBufferSize(new Vector2()); 58 | outputBuffer.setSize(size.width, size.height); 59 | const output = finalPass ? null : outputBuffer; 60 | 61 | renderer.setRenderTarget(output); 62 | renderer.render(scene, camera); 63 | 64 | return output; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/functions/tick.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PerspectiveCamera, 3 | Scene, 4 | Mesh, 5 | IcosahedronGeometry, 6 | BoxGeometry, 7 | MeshNormalMaterial, 8 | Vector3, 9 | Geometry, 10 | } from 'three'; 11 | import { Store, Action } from 'redux'; 12 | 13 | import { laneWidth } from '../constants'; 14 | import ActionType from '../constants/ActionType'; 15 | import { GameState } from '../Types'; 16 | 17 | let boxMeshes: Mesh[] = []; 18 | let pickupMeshes: Mesh[] = []; 19 | 20 | let lastX = 0; 21 | let speed = 0.02; 22 | 23 | let spawnCycle = 0; 24 | let spawnMode = 0; 25 | 26 | function spawnGeometry( 27 | geometry: Geometry, 28 | trackingArray: Mesh[], 29 | camera: PerspectiveCamera, 30 | scene: Scene, 31 | lane: number 32 | ) { 33 | const material = new MeshNormalMaterial(); 34 | let mesh = new Mesh(geometry, material); 35 | mesh.position.x = Math.floor(camera.position.x / 0.3) * 0.3 + 6; 36 | mesh.position.y = 0; 37 | mesh.position.z = lane * laneWidth; 38 | 39 | trackingArray.push(mesh); 40 | 41 | scene.add(mesh); 42 | } 43 | 44 | function spawnPickup(camera: PerspectiveCamera, scene: Scene, lane: number) { 45 | const geometry = new IcosahedronGeometry(0.1, 0); 46 | spawnGeometry(geometry, pickupMeshes, camera, scene, lane); 47 | } 48 | 49 | function spawnBox(camera: PerspectiveCamera, scene: Scene, lane: number) { 50 | const geometry = new BoxGeometry(0.2, 0.2, 0.2); 51 | spawnGeometry(geometry, boxMeshes, camera, scene, lane); 52 | } 53 | 54 | function spawn(camera: PerspectiveCamera, scene: Scene) { 55 | spawnCycle++; 56 | 57 | switch (spawnMode) { 58 | case 0: 59 | if (spawnCycle < 4) { 60 | spawnBox(camera, scene, 0); 61 | spawnBox(camera, scene, 1); 62 | } 63 | if (spawnCycle == 4) spawnPickup(camera, scene, 0); 64 | break; 65 | case 1: 66 | if (spawnCycle < 4) { 67 | spawnBox(camera, scene, -1); 68 | spawnBox(camera, scene, 1); 69 | } 70 | if (spawnCycle == 4) spawnPickup(camera, scene, 0); 71 | break; 72 | case 2: 73 | if (spawnCycle % 3 != 2) 74 | spawnBox(camera, scene, Math.round(Math.random() * 2) - 1); 75 | break; 76 | case 3: 77 | if (spawnCycle % 3 != 2) spawnBox(camera, scene, (spawnCycle % 2) - 1); 78 | break; 79 | } 80 | 81 | // Get rid of the ones we don't see anyway. 82 | boxMeshes = boxMeshes.filter(mesh => { 83 | if (mesh.position.x < camera.position.x) { 84 | scene.remove(mesh); 85 | return false; 86 | } 87 | 88 | return true; 89 | }); 90 | 91 | pickupMeshes = pickupMeshes.filter(mesh => { 92 | if (mesh.position.x < camera.position.x) { 93 | scene.remove(mesh); 94 | return false; 95 | } 96 | 97 | return true; 98 | }); 99 | 100 | if (spawnCycle == 5) { 101 | spawnCycle = 0; 102 | spawnMode = Math.round(Math.random() * 3); 103 | } 104 | } 105 | 106 | export function reset( 107 | camera: PerspectiveCamera, 108 | scene: Scene, 109 | planeMesh: Mesh, 110 | gameStateStore: Store 111 | ) { 112 | gameStateStore.dispatch({ type: ActionType.SET_SCORE, value: 0 }); 113 | camera.position.x = 0; 114 | planeMesh.position.x = 1; 115 | camera.lookAt(new Vector3(camera.position.x + 1, 1, 0)); 116 | 117 | lastX = 0; 118 | speed = 0.02; 119 | spawnCycle = 0; 120 | spawnMode = 0; 121 | 122 | boxMeshes = boxMeshes.filter(mesh => { 123 | scene.remove(mesh); 124 | return false; 125 | }); 126 | 127 | pickupMeshes = pickupMeshes.filter(mesh => { 128 | scene.remove(mesh); 129 | return false; 130 | }); 131 | 132 | gameStateStore.dispatch({ type: ActionType.SET_DEFEAT, value: false }); 133 | } 134 | 135 | export function spawnTick(camera: PerspectiveCamera, scene: Scene) { 136 | const currentX = Math.floor(camera.position.x / 0.3); 137 | if (currentX > lastX) { 138 | if (lastX !== 0) spawn(camera, scene); 139 | 140 | lastX = currentX; 141 | speed += 0.0001; 142 | } 143 | } 144 | 145 | export default function tick( 146 | camera: PerspectiveCamera, 147 | scene: Scene, 148 | planeMesh: Mesh, 149 | gameStateStore: Store, 150 | timeDifference: number 151 | ) { 152 | const state = gameStateStore.getState(); 153 | if (state.defeat) return; 154 | 155 | // 16.667 = 60 FPS tick target 156 | camera.position.x += speed * (timeDifference / 16.667); 157 | camera.lookAt(new Vector3(camera.position.x + 1, 1, 0)); 158 | 159 | const targetZ = state.lane * laneWidth; 160 | planeMesh.position.x = camera.position.x + 1; 161 | 162 | if (planeMesh.position.z != targetZ) { 163 | if (targetZ > planeMesh.position.z) { 164 | planeMesh.position.z += 0.04; 165 | } else { 166 | planeMesh.position.z -= 0.04; 167 | } 168 | 169 | if (Math.abs(planeMesh.position.z - targetZ) < 0.04) { 170 | planeMesh.position.z = targetZ; 171 | } 172 | } 173 | 174 | for (let pickupMesh of pickupMeshes) { 175 | pickupMesh.rotation.x += 0.01; 176 | pickupMesh.rotation.z -= 0.02; 177 | } 178 | 179 | pickupMeshes = pickupMeshes.filter(mesh => { 180 | if ( 181 | Math.abs(planeMesh.position.x - mesh.position.x) < 0.15 && 182 | Math.abs(planeMesh.position.z - mesh.position.z) < 0.2 183 | ) { 184 | // Dumb collision detection. 185 | gameStateStore.dispatch({ type: ActionType.ADD_MONEY, value: 1 }); 186 | scene.remove(mesh); 187 | return false; 188 | } else { 189 | return true; 190 | } 191 | }); 192 | 193 | for (let mesh of boxMeshes) { 194 | if ( 195 | Math.abs(planeMesh.position.x - mesh.position.x) < 0.15 && 196 | Math.abs(planeMesh.position.z - mesh.position.z) < 0.2 197 | ) { 198 | gameStateStore.dispatch({ type: ActionType.SET_DEFEAT, value: true }); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/img/money.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mat-sz/flight/83dc25110f0167fc6df7f9c7ff0f96eb8ff0934c/src/img/money.png -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.frag'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PerspectiveCamera, 3 | Scene, 4 | WebGLRenderer, 5 | Mesh, 6 | ConeGeometry, 7 | MeshNormalMaterial, 8 | Vector2, 9 | WebGLRenderTarget, 10 | } from 'three'; 11 | import { createStore } from 'redux'; 12 | 13 | import './App.scss'; 14 | 15 | import gameState from './reducers/gameState'; 16 | import addOverlay from './functions/addOverlay'; 17 | import addWindowEvents from './functions/addWindowEvents'; 18 | import tick, { reset, spawnTick } from './functions/tick'; 19 | import createShaders from './shaders'; 20 | import ActionType from './constants/ActionType'; 21 | 22 | const gameStateStore = createStore(gameState); 23 | 24 | const camera: PerspectiveCamera = new PerspectiveCamera( 25 | 70, 26 | window.innerWidth / window.innerHeight, 27 | 0.01, 28 | 10 29 | ); 30 | camera.position.x = 1; 31 | camera.position.z = 0; 32 | camera.position.y = 2; 33 | 34 | const scene: Scene = new Scene(); 35 | const renderer: WebGLRenderer = new WebGLRenderer({ antialias: true }); 36 | 37 | // The cone. 38 | const material = new MeshNormalMaterial(); 39 | const geometry = new ConeGeometry(0.15, 0.3, 8); 40 | const planeMesh = new Mesh(geometry, material); 41 | planeMesh.rotateZ(-Math.PI / 2); 42 | scene.add(planeMesh); 43 | 44 | const shaders = createShaders(renderer); 45 | 46 | // ...and here we start rendering things. 47 | renderer.setSize(window.innerWidth, window.innerHeight); 48 | document.body.appendChild(renderer.domElement); 49 | 50 | const outputBuffer: WebGLRenderTarget = new WebGLRenderTarget(1, 1); 51 | outputBuffer.texture.generateMipmaps = false; 52 | 53 | let timeLast = 0; 54 | render(0); 55 | 56 | function render(time: number) { 57 | const timeDifference = time - timeLast; 58 | timeLast = time; 59 | 60 | tick(camera, scene, planeMesh, gameStateStore, timeDifference); 61 | 62 | const size = renderer.getDrawingBufferSize(new Vector2()); 63 | outputBuffer.setSize(size.width, size.height); 64 | 65 | renderer.setRenderTarget(outputBuffer); 66 | renderer.render(scene, camera); 67 | 68 | shaders.reduce( 69 | (buffer, current, i) => current(buffer, time, i === shaders.length - 1), 70 | outputBuffer 71 | ); 72 | 73 | requestAnimationFrame(render); 74 | } 75 | 76 | setInterval(() => { 77 | gameStateStore.dispatch({ 78 | type: ActionType.SET_SCORE, 79 | value: camera.position.x, 80 | }); 81 | }, 250); 82 | 83 | setInterval(() => { 84 | spawnTick(camera, scene); 85 | }, 100); 86 | 87 | const onReset = () => { 88 | reset(camera, scene, planeMesh, gameStateStore); 89 | }; 90 | 91 | addWindowEvents(camera, renderer, gameStateStore, onReset); 92 | addOverlay(gameStateStore, onReset); 93 | -------------------------------------------------------------------------------- /src/reducers/gameState.ts: -------------------------------------------------------------------------------- 1 | import ActionType from '../constants/ActionType'; 2 | import { Action, GameState, SavedState } from '../Types'; 3 | 4 | let savedState: SavedState = { 5 | highScore: 0, 6 | money: 0, 7 | }; 8 | 9 | const savedStateSerialized = localStorage.getItem('savedState'); 10 | if (savedStateSerialized) { 11 | try { 12 | savedState = JSON.parse(savedStateSerialized) as SavedState; 13 | } catch {} 14 | } 15 | 16 | const initialState: GameState = { 17 | score: 0, 18 | highScore: savedState.highScore, 19 | defeat: false, 20 | lane: 0, 21 | money: savedState.money, 22 | }; 23 | 24 | export default function gameState(state = initialState, action: Action) { 25 | const newState = { ...state }; 26 | switch (action.type) { 27 | case ActionType.SET_DEFEAT: 28 | newState.defeat = action.value as boolean; 29 | 30 | savedState.highScore = newState.highScore; 31 | savedState.money = newState.money; 32 | 33 | localStorage.setItem('savedState', JSON.stringify(savedState)); 34 | break; 35 | case ActionType.SET_SCORE: 36 | newState.score = action.value as number; 37 | break; 38 | case ActionType.SET_LANE: 39 | newState.lane = action.value as number; 40 | break; 41 | case ActionType.SET_MONEY: 42 | newState.money = action.value as number; 43 | break; 44 | case ActionType.ADD_SCORE: 45 | newState.score += action.value as number; 46 | break; 47 | case ActionType.ADD_MONEY: 48 | newState.money += action.value as number; 49 | break; 50 | default: 51 | return state; 52 | } 53 | 54 | if (newState.score > newState.highScore) { 55 | newState.highScore = newState.score; 56 | } 57 | 58 | return newState; 59 | } 60 | -------------------------------------------------------------------------------- /src/shaders/fog.frag: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | uniform sampler2D iTexture; 4 | uniform vec3 iResolution; 5 | 6 | vec4 fog(vec2 coord, vec4 screen) 7 | { 8 | float dy = coord.y; 9 | return screen * (0.75 - dy * dy); 10 | } 11 | 12 | void main() 13 | { 14 | vec2 p = gl_FragCoord.xy / iResolution.xy; 15 | gl_FragColor = texture2D(iTexture, p); 16 | gl_FragColor = fog(p, gl_FragColor); 17 | } -------------------------------------------------------------------------------- /src/shaders/index.ts: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer } from 'three'; 2 | 3 | import scanlines from './scanlines.frag'; 4 | import vignette from './vignette.frag'; 5 | import fog from './fog.frag'; 6 | 7 | import createShader from '../functions/createShader'; 8 | 9 | export const createShaders = (renderer: WebGLRenderer) => [ 10 | createShader(renderer, fog), 11 | createShader(renderer, scanlines), 12 | createShader(renderer, vignette), 13 | ]; 14 | 15 | export default createShaders; 16 | -------------------------------------------------------------------------------- /src/shaders/scanlines.frag: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | uniform sampler2D iTexture; 4 | uniform vec3 iResolution; 5 | uniform float iTime; 6 | 7 | vec2 fisheye(vec2 coord, float str) 8 | { 9 | vec2 neg1to1 = coord; 10 | neg1to1 = (neg1to1 - 0.5) * 2.0; 11 | 12 | vec2 offset; 13 | offset.x = ( pow(neg1to1.y,2.0)) * str * (neg1to1.x); 14 | offset.y = ( pow(neg1to1.x,2.0)) * str * (neg1to1.y); 15 | 16 | return coord + offset; 17 | } 18 | 19 | vec4 scanline(vec2 coord, vec4 screen) 20 | { 21 | const float scale = .0025; 22 | const float amt = 0.01; 23 | const float spd = 1.0; 24 | 25 | screen.rgb += sin((coord.y / scale - (iTime * spd * 6.28))) * amt; 26 | return screen; 27 | } 28 | 29 | void main() 30 | { 31 | vec2 p = gl_FragCoord.xy / iResolution.xy; 32 | p = fisheye(p, 0.1); 33 | gl_FragColor = texture2D(iTexture, p); 34 | 35 | gl_FragColor = scanline(p, gl_FragColor); 36 | gl_FragColor.a = 0.5; 37 | } -------------------------------------------------------------------------------- /src/shaders/vignette.frag: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | uniform sampler2D iTexture; 4 | uniform vec3 iResolution; 5 | 6 | vec4 vignette(vec2 coord, vec4 screen) 7 | { 8 | float dx = 1.3 * abs(coord.x - .5); 9 | float dy = 1.3 * abs(coord.y - .5); 10 | return screen * (1.0 - dx * dx - dy * dy); 11 | } 12 | 13 | void main() 14 | { 15 | vec2 p = gl_FragCoord.xy / iResolution.xy; 16 | gl_FragColor = texture2D(iTexture, p); 17 | gl_FragColor = vignette(p, gl_FragColor); 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "outDir": "./dist/", 9 | "noImplicitAny": true, 10 | "module": "es6", 11 | "target": "es6", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "moduleResolution": "node", 15 | "jsx": "react", 16 | }, 17 | "include": ["src/index.d.ts"] 18 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: "./src/index.ts", 7 | output: { 8 | path: path.join(__dirname, "/dist"), 9 | filename: "index-bundle.js" 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | exclude: /node_modules/, 16 | use: ['ts-loader'], 17 | }, 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: ['babel-loader'] 22 | }, 23 | { 24 | test: /\.css$/i, 25 | use: ['style-loader', 'css-loader'] 26 | }, 27 | { 28 | test: /\.scss$/i, 29 | use: ['style-loader', 'css-loader', 'sass-loader'] 30 | }, 31 | { 32 | test: /\.(frag|vert)$/i, 33 | use: ['raw-loader'], 34 | }, 35 | { 36 | test: /\.(gif|png|jpe?g|svg)$/i, 37 | use: [ 38 | 'file-loader', 39 | { 40 | loader: 'image-webpack-loader', 41 | options: { 42 | bypassOnDebug: true, // webpack@1.x 43 | disable: true, // webpack@2.x and newer 44 | }, 45 | }, 46 | ], 47 | } 48 | ] 49 | }, 50 | resolve: { 51 | extensions: [ '.tsx', '.ts', '.js' ], 52 | }, 53 | plugins: [ 54 | new HtmlWebpackPlugin({ 55 | template: './public/index.html', 56 | }), 57 | new webpack.DefinePlugin({ 58 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 59 | }), 60 | ] 61 | }; --------------------------------------------------------------------------------