├── logo.png ├── .babelrc ├── .npmignore ├── src ├── index.js ├── DefaultRenderer.js ├── DefaultTimer.js ├── GameLoop.js └── GameEngine.js ├── webpack.config.js ├── LICENSE ├── .gitignore ├── package.json └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine/HEAD/logo.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-object-rest-spread", "transform-react-jsx"] 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | .eslintrc 3 | .babelrc 4 | .gitignore 5 | src/ 6 | 7 | # Images and other content 8 | # 9 | logo.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import GameLoop from "./GameLoop"; 2 | import GameEngine from "./GameEngine"; 3 | import DefaultRenderer from "./DefaultRenderer"; 4 | import DefaultTimer from "./DefaultTimer"; 5 | 6 | export { 7 | GameLoop, 8 | GameEngine, 9 | DefaultRenderer, 10 | DefaultRenderer as Renderer, 11 | DefaultTimer, 12 | DefaultTimer as Timer 13 | }; -------------------------------------------------------------------------------- /src/DefaultRenderer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default (entities, window) => { 4 | if (!entities || !window) return null; 5 | 6 | return Object.keys(entities) 7 | .filter(key => entities[key].renderer) 8 | .map(key => { 9 | let entity = entities[key]; 10 | if (typeof entity.renderer === "object") 11 | return ( 12 | 17 | ); 18 | else if (typeof entity.renderer === "function") 19 | return ( 20 | 21 | ); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = { 3 | entry: './src/index.js', 4 | output: { 5 | path: path.resolve(__dirname, 'build'), 6 | filename: 'index.js', 7 | libraryTarget: 'commonjs2' 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | include: path.resolve(__dirname, 'src'), 14 | exclude: /(node_modules|bower_components|build)/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['env'], 19 | plugins: ['transform-class-properties'] 20 | } 21 | } 22 | } 23 | ] 24 | }, 25 | externals: { 26 | 'react': 'commonjs react' 27 | } 28 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Boris Berak 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-game-engine", 3 | "version": "1.2.0", 4 | "description": "A lightweight game engine for React projects", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "webpack", 9 | "prepublish": "rm -rf ./build && npm run build" 10 | }, 11 | "jest": { 12 | "testURL": "http://localhost/" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/bberak/react-game-engine.git" 17 | }, 18 | "keywords": [ 19 | "React", 20 | "JS", 21 | "Game", 22 | "Engine", 23 | "2D", 24 | "3D" 25 | ], 26 | "author": "Boris Berak", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/bberak/react-game-engine/issues" 30 | }, 31 | "homepage": "https://github.com/bberak/react-game-engine", 32 | "peerDependencies": { 33 | "react": ">= 16.3.0" 34 | }, 35 | "devDependencies": { 36 | "babel-jest": "^22.4.1", 37 | "babel-loader": "^7.0.0", 38 | "babel-plugin-transform-class-properties": "^6.24.1", 39 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 40 | "babel-plugin-transform-react-jsx": "^6.24.1", 41 | "babel-preset-env": "^1.6.1", 42 | "babel-preset-react": "^6.24.1", 43 | "jest": "^22.4.2", 44 | "react": "*", 45 | "react-test-renderer": "^16.2.0", 46 | "webpack": "^2.6.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DefaultTimer.js: -------------------------------------------------------------------------------- 1 | /* 2 | With thanks, https://github.com/FormidableLabs/react-game-kit/blob/master/src/native/utils/game-loop.js 3 | */ 4 | 5 | /* 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2013 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | */ 16 | 17 | export default class DefaultTimer { 18 | constructor() { 19 | this.subscribers = []; 20 | this.loopId = null; 21 | } 22 | 23 | loop = time => { 24 | if (this.loopId) { 25 | this.subscribers.forEach(callback => { 26 | callback(time); 27 | }); 28 | } 29 | 30 | this.loopId = requestAnimationFrame(this.loop); 31 | }; 32 | 33 | start() { 34 | if (!this.loopId) { 35 | this.loop(); 36 | } 37 | } 38 | 39 | stop() { 40 | if (this.loopId) { 41 | cancelAnimationFrame(this.loopId); 42 | this.loopId = null; 43 | } 44 | } 45 | 46 | subscribe(callback) { 47 | if (this.subscribers.indexOf(callback) === -1) 48 | this.subscribers.push(callback); 49 | } 50 | 51 | unsubscribe(callback) { 52 | this.subscribers = this.subscribers.filter(s => s !== callback) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/GameLoop.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import DefaultTimer from "./DefaultTimer"; 3 | 4 | const events = `onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onWheel onTouchCancel onTouchEnd onTouchMove onTouchStart onKeyDown onKeyPress onKeyUp`; 5 | 6 | export default class GameLoop extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.timer = props.timer || new DefaultTimer(); 10 | this.input = []; 11 | this.previousTime = null; 12 | this.previousDelta = null; 13 | this.container = React.createRef(); 14 | } 15 | 16 | componentDidMount() { 17 | this.timer.subscribe(this.updateHandler); 18 | 19 | if (this.props.running) this.start(); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.stop(); 24 | this.timer.unsubscribe(this.updateHandler); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | if (prevProps.running !== this.props.running) { 29 | if (this.props.running) this.start(); 30 | else this.stop(); 31 | } 32 | } 33 | 34 | start = () => { 35 | this.input.length = 0; 36 | this.previousTime = null; 37 | this.previousDelta = null; 38 | this.timer.start(); 39 | this.container.current.focus(); 40 | }; 41 | 42 | stop = () => { 43 | this.timer.stop(); 44 | }; 45 | 46 | updateHandler = currentTime => { 47 | let args = { 48 | input: this.input, 49 | window: window, 50 | time: { 51 | current: currentTime, 52 | previous: this.previousTime, 53 | delta: currentTime - (this.previousTime || currentTime), 54 | previousDelta: this.previousDelta 55 | } 56 | }; 57 | 58 | if (this.props.onUpdate) this.props.onUpdate(args); 59 | 60 | this.input.length = 0; 61 | this.previousTime = currentTime; 62 | this.previousDelta = args.time.delta; 63 | }; 64 | 65 | inputHandlers = events 66 | .split(" ") 67 | .map(name => ({ 68 | name, 69 | handler: payload => { 70 | payload.persist(); 71 | this.input.push({ name, payload }); 72 | } 73 | })) 74 | .reduce((acc, val) => { 75 | acc[val.name] = val.handler; 76 | return acc; 77 | }, {}); 78 | 79 | render() { 80 | return ( 81 |
88 | {this.props.children} 89 |
90 | ); 91 | } 92 | } 93 | 94 | GameLoop.defaultProps = { 95 | running: true 96 | }; 97 | 98 | const css = { 99 | container: { 100 | flex: 1, 101 | outline: "none" 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/GameEngine.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import DefaultTimer from "./DefaultTimer"; 3 | import DefaultRenderer from "./DefaultRenderer"; 4 | 5 | const getEntitiesFromProps = props => 6 | props.initState || 7 | props.initialState || 8 | props.state || 9 | props.initEntities || 10 | props.initialEntities || 11 | props.entities; 12 | 13 | const isPromise = obj => { 14 | return !!( 15 | obj && 16 | obj.then && 17 | obj.then.constructor && 18 | obj.then.call && 19 | obj.then.apply 20 | ); 21 | }; 22 | 23 | const events = `onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onWheel onTouchCancel onTouchEnd onTouchMove onTouchStart onKeyDown onKeyPress onKeyUp`; 24 | 25 | export default class GameEngine extends Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | entities: null 30 | }; 31 | this.timer = props.timer || new DefaultTimer(); 32 | this.input = []; 33 | this.previousTime = null; 34 | this.previousDelta = null; 35 | this.events = []; 36 | this.container = React.createRef(); 37 | } 38 | 39 | async componentDidMount() { 40 | this.timer.subscribe(this.updateHandler); 41 | 42 | let entities = getEntitiesFromProps(this.props); 43 | 44 | if (isPromise(entities)) entities = await entities; 45 | 46 | this.setState( 47 | { 48 | entities: entities || {} 49 | }, 50 | () => { 51 | if (this.props.running) this.start(); 52 | } 53 | ); 54 | } 55 | 56 | componentWillUnmount() { 57 | this.stop(); 58 | this.timer.unsubscribe(this.updateHandler); 59 | } 60 | 61 | componentDidUpdate(prevProps) { 62 | if (prevProps.running !== this.props.running) { 63 | if (this.props.running) this.start(); 64 | else this.stop(); 65 | } 66 | } 67 | 68 | clear = () => { 69 | this.input.length = 0; 70 | this.events.length = 0; 71 | this.previousTime = null; 72 | this.previousDelta = null; 73 | }; 74 | 75 | start = () => { 76 | this.clear(); 77 | this.timer.start(); 78 | this.dispatch({ type: "started" }); 79 | this.container.current.focus(); 80 | }; 81 | 82 | stop = () => { 83 | this.timer.stop(); 84 | this.dispatch({ type: "stopped" }); 85 | }; 86 | 87 | swap = async newEntities => { 88 | if (isPromise(newEntities)) newEntities = await newEntities; 89 | 90 | this.setState({ entities: newEntities || {} }, () => { 91 | this.clear(); 92 | this.dispatch({ type: "swapped" }); 93 | }); 94 | }; 95 | 96 | defer = e => { 97 | this.dispatch(e); 98 | }; 99 | 100 | dispatch = e => { 101 | setTimeout(() => { 102 | this.events.push(e); 103 | if (this.props.onEvent) this.props.onEvent(e); 104 | }, 0); 105 | }; 106 | 107 | updateHandler = currentTime => { 108 | let args = { 109 | input: this.input, 110 | window: window, 111 | events: this.events, 112 | dispatch: this.dispatch, 113 | defer: this.defer, 114 | time: { 115 | current: currentTime, 116 | previous: this.previousTime, 117 | delta: currentTime - (this.previousTime || currentTime), 118 | previousDelta: this.previousDelta 119 | } 120 | }; 121 | 122 | this.setState(prevState => { 123 | let newEntities = this.props.systems.reduce( 124 | (state, sys) => sys(state, args), 125 | prevState.entities 126 | ); 127 | this.input.length = 0; 128 | this.events.length = 0; 129 | this.previousTime = currentTime; 130 | this.previousDelta = args.time.delta; 131 | return { entities: newEntities}; 132 | }); 133 | }; 134 | 135 | inputHandlers = events 136 | .split(" ") 137 | .map(name => ({ 138 | name, 139 | handler: payload => { 140 | payload.persist(); 141 | this.input.push({ name, payload }); 142 | } 143 | })) 144 | .reduce((acc, val) => { 145 | acc[val.name] = val.handler; 146 | return acc; 147 | }, {}); 148 | 149 | render() { 150 | return ( 151 |
158 | {this.props.renderer(this.state.entities, window)} 159 | {this.props.children} 160 |
161 | ); 162 | } 163 | } 164 | 165 | GameEngine.defaultProps = { 166 | systems: [], 167 | entities: {}, 168 | renderer: DefaultRenderer, 169 | running: true 170 | }; 171 | 172 | const css = { 173 | container: { 174 | flex: 1, 175 | outline: "none" 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | React Game Engine 3 |

4 | 5 | # React Game Engine · [![npm version](https://badge.fury.io/js/react-game-engine.svg)](https://badge.fury.io/js/react-game-engine) [![mit license](https://img.shields.io/badge/license-MIT-50CB22.svg)](https://opensource.org/licenses/MIT) 6 | 7 | Some components that make it easier to construct dynamic and interactive scenes on the **web** using React. 8 | 9 | If you are looking for the **React Native** version of this library, go to [react-native-game-engine](https://github.com/bberak/react-native-game-engine). 10 | 11 | ## Differences between React Native Game Engine 12 | 13 | The core APIs are exactly the same. I've made some simplifications and improvements where it made sense. Please note that this library is still a bit more **experimental** than RNGE, but definitely in a usable state. 14 | 15 | The main changes are: 16 | 17 | ### Rx Dependency Removed 18 | 19 | I've removed the Rx dependency, and therefore the ability to pass in your own touch processor. For most developers, they won't even notice this change. The number and variety of events that you can receive from a DOM element is vastly bigger than on mobile. The burden of normalizing these events into streams that are consumable by the game developer in every possible way is quite daunting and would undoubtedly lead to developers pulling out their hair. 20 | 21 | ### Touches Replaced By Input 22 | 23 | Taking the above into account - the `touches` array has been replaced by a generic `input`. Every system will receive an `input` array that contain the name and payload of the following events: `onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onWheel onTouchCancel onTouchEnd onTouchMove onTouchStart onKeyDown onKeyPress onKeyUp` 24 | 25 | Here's a system that would handle these events: 26 | 27 | ```javascript 28 | const sys1 = (entities, { input }) => { 29 | const { payload } = input.find(x => x.name === "onMouseDown") || {}; 30 | 31 | if (payload) { 32 | //-- Do something here 33 | } 34 | 35 | return entities; 36 | }; 37 | ``` 38 | 39 | ## Table of Contents 40 | 41 | - [Quick Start](#quick-start) 42 | - [GameEngine Properties](#gameengine-properties) 43 | - [GameEngine Methods](#gameengine-methods) 44 | - [FAQ](#faq) 45 | - [Introduction](#introduction) 46 | - [Managing Complexity with Component Entity Systems](#managing-complexity-with-component-entity-systems) 47 | - [Additional CES Reading Material](#additional-ces-reading-material) 48 | - [Get in Touch](#get-in-touch) 49 | - [License](#license) 50 | 51 | 52 | ## Quick Start 53 | 54 | If you've used **react-game-engine** before and understand the core concepts, take a look at [react-game-engine-template](https://github.com/bberak/react-game-engine-template). It's a sort of game kickstarter project that allows you to prototype ideas quickly and comes preloaded with a bunch of stuff like: 55 | 56 | - A 3D renderer 57 | - Physics 58 | - Particle system 59 | - Crude sound API 60 | - Sprite support with animations 61 | - Etc 62 | 63 | Otherwise, continue reading the quick start guide below. 64 | 65 |
66 | 67 | Firstly, install the package to your project: 68 | 69 | ```npm install --save react-game-engine``` 70 | 71 | Then import the GameEngine component: 72 | 73 | ```javascript 74 | import { GameEngine } from "react-game-engine"; 75 | ``` 76 | 77 | To start with, let's create some components that can be rendered by React. Create a file called ```renderers.js```: 78 | 79 | ```javascript 80 | import React, { PureComponent } from "react"; 81 | 82 | class Box extends PureComponent { 83 | render() { 84 | const size = 100; 85 | const x = this.props.x - size / 2; 86 | const y = this.props.y - size / 2; 87 | return ( 88 |
89 | ); 90 | } 91 | } 92 | 93 | export { Box }; 94 | ``` 95 | 96 | Next, let's code our logic in a file called ```systems.js```: 97 | 98 | ```javascript 99 | const MoveBox = (entities, { input }) => { 100 | //-- I'm choosing to update the game state (entities) directly for the sake of brevity and simplicity. 101 | //-- There's nothing stopping you from treating the game state as immutable and returning a copy.. 102 | //-- Example: return { ...entities, t.id: { UPDATED COMPONENTS }}; 103 | //-- That said, it's probably worth considering performance implications in either case. 104 | 105 | const { payload } = input.find(x => x.name === "onMouseDown") || {}; 106 | 107 | if (payload) { 108 | const box1 = entities["box1"]; 109 | 110 | box1.x = payload.pageX; 111 | box1.y = payload.pageY; 112 | } 113 | 114 | return entities; 115 | }; 116 | 117 | export { MoveBox }; 118 | ``` 119 | 120 | Finally let's bring it all together in our `index.js`: 121 | 122 | ```javascript 123 | import React, { PureComponent } from "react"; 124 | import { GameEngine } from "react-game-engine"; 125 | import { Box } from "./renderers"; 126 | import { MoveBox } from "./systems" 127 | 128 | export default class SimpleGame extends PureComponent { 129 | render() { 130 | return ( 131 | } 139 | }}> 140 | 141 | 142 | ); 143 | } 144 | } 145 | ``` 146 | 147 | Build and run. Each entity is a **"box"**. Every time you click on the screen, the first entity will move to the clicked coordinate. 148 | 149 | If you're curious, our ```GameEngine``` component is a loose implementation of the [Component-Entity-System](#managing-complexity-with-component-entity-systems) pattern - we've written up a quick intro [here](#managing-complexity-with-component-entity-systems). 150 | 151 | ## GameEngine Properties 152 | 153 | | Prop | Description | Default | 154 | |---|---|---| 155 | |**`systems`**|An array of functions to be called on every tick. |`[]`| 156 | |**`entities`**|An object containing your game's initial entities. This can also be a Promise that resolves to an object containing your entities. This is useful when you need to asynchronously load a texture or other assets during the creation of your entities or level. |`{} or Promise`| 157 | |**`renderer`**|A function that receives the entities and needs to render them on every tick. ```(entities,screen) => { /* DRAW ENTITIES */ }``` |`DefaultRenderer`| 158 | |**`timer`**|An object that can be used to override the default timer behavior |`new DefaultTimer()`| 159 | |**`running`**|A boolean that can be used to control whether the game loop is running or not |`true`| 160 | |**`onEvent`**|A callback for being notified when events are dispatched |`undefined`| 161 | |**`style`**|An object containing styles for the root container |`undefined`| 162 | |**`className`**|A className for applying styles for the root container |`undefined`| 163 | |**`children`**|React components that will be rendered after the entities |`undefined`| 164 | 165 | ## GameEngine Methods 166 | 167 | | Method | Description | Arg1, Arg2, ArgN | 168 | |---|---|---| 169 | |**`stop`**|Stop the game loop |`NA`| 170 | |**`start`**|Start the game loop. |`NA`| 171 | |**`swap`**|A method that can be called to update your game with new entities. Can be useful for level switching etc. You can also pass a Promise that resolves to an entities object into this method. |`{} or Promise`| 172 | |**`dispatch`**|A method that can be called to fire events after the currenty frame completed. The event will be received by **ALL** the systems and any `onEvent` callbacks |`event`| 173 | 174 | ## FAQ 175 | 176 | ### Is React Game Engine suitable for production quality games? 177 | 178 | > This depends on your definition of production quality. You're not going to make a AAA title with RGE. You could however create some more basic games (doesn't mean they can't be fun games), or even jazz up your existing business applications with some interactive eye candy. 179 | 180 | ### How do I manage physics? 181 | 182 | > RGE does not come with an out-of-the-box physics engine. We felt that this would be an area where the game designers should be given greater liberty. There are lots of JS-based physics engines out there, each with their pros and cons. Check out [Matter JS](https://github.com/liabru/matter-js) if you're stuck. 183 | 184 | ### Do I have a choice of renderers? 185 | 186 | > How you render your entities is up to you. You can use the stand web components (div, img, canvas) or using [ThreeJS](https://threejs.org) for 3D games. 187 | 188 | ## Introduction 189 | 190 | This package contains only two components: 191 | 192 | - ```GameLoop``` 193 | - ```GameEngine``` 194 | 195 | Both are standalone components. The ```GameLoop``` is a subset of the ```GameEngine``` and gives you access to an ```onUpdate``` callback that fires every **16ms** (or roughly 60 fps). On top of this, the ```GameLoop``` will supply a reference to the screen (via ```Dimensions.get("window"))```, touch events for multiple fingers (start, end, press, long-press, move) and time + deltas. The ```GameLoop``` is useful for simple interactive scenes, and pretty much stays out of your way. 196 | 197 | The ```GameEngine``` is more opinionated and is a react-friendly implementation of the [Component-Entity-Systems pattern](#managing-complexity-with-component-entity-systems). It provides the same features out of the box as the ```GameLoop``` but also includes a crude event/signaling pipeline for communication between your game and your other React components. You probably want to use the ```GameEngine``` to implement slightly more complex games and interactive scenes. 198 | 199 | ## Managing Complexity with Component Entity Systems 200 | 201 | Typically, game developers have used OOP to implement complex game objects and scenes. Each game object is instantiated from a class, and polymorphism allows code re-use and behaviors to be extended through inheritance. As class hierarchies grow, it becomes increasingly difficult to create new types of game entities without duplicating code or seriously re-thinking the entire class hierarchy. 202 | 203 | ``` 204 | [GameEntity] 205 | | 206 | | 207 | [Vehicle] 208 | / | \ 209 | / | \ 210 | / | \ 211 | / | \ 212 | [Terrestrial] [Marine] [Airborne] 213 | | | | 214 | | | | 215 | [Tank] [Boat] [Jet] 216 | ``` 217 | > How do we insert a new terrestrial and marine-based vehicle - say a Hovercraft - into the class hierarchy? 218 | 219 | One way to address these problems is to favor composition over inheritance. With this approach, we break out the attributes and behaviours of our various game entities into decoupled, encapsulated and atomic components. This allows us to be really imaginative with the sorts of game entities we create because we can easily compose them with components from disparate domains and concerns. 220 | 221 | Component entity systems are one way to organize your game entities in a composable manner. To start with, we take the common attributes (data) of our game entities and move them into siloed components. These don't have to be concrete classes, simple hash maps (or equivalent) and scalars will do - but this depends on the data you're storing. 222 | 223 | - ***Position:** { x: 0, y: 0 }* 224 | - ***Velocity:** { x: 0, y: 0 }* 225 | - ***Acceleration:** { x: 0, y: 0 }* 226 | - ***Mass:** 1.0* 227 | - ***Health:** 100* 228 | - ***Physics:** Body b* 229 | - ***Controls:** { jump: 'w', left: 'a', crouch: 's', right: 'd' }* 230 | 231 | > Examples of different types of components in a hypothetical programming language. 232 | 233 | Your game entities will be reduced to lists/arrays of components and labeled with a unique identifier. An entity's components are by no means static - you're free to update components and even add or remove them on the fly. If our favourite Italian plumber ingests a mushroom, we simple double his velocity. If our character turns into a ghost - we remove his physics component and let him walk through walls. 234 | 235 | - ***Player#1:** [Position, Velocity, Health, Sprite, Physics, Controls]* 236 | - ***Enemy#1:** [Position, Velocity, Health, Sprite, Physics, AI]* 237 | - ***Platform#1:** [Position, Sprite, Physics]* 238 | - ***Platform#2:** [Position, Sprite, Physics, Velocity] // <-- Moving platform!* 239 | 240 | > All entities are assigned a unique id. 241 | 242 | Since our entities are simple data holders now, we must move all our game logic into our systems. At its core, a system is a function that processes related groups of components and is called on each iteration of the game loop. The system will extract entities that contain the necessary components it requires to run, update those entities as necessary, and wait for the next cycle. For example, we could code a "Gravity" component that calculates the force of gravity and applies it to all entities that have an acceleration AND velocity AND mass component. Entities that do not contain these components will not be affected by gravity. 243 | 244 | - ***Gravity:** (Acceleration, Velocity, Mass) => { // Update all matching entities // }* 245 | - ***Render:** (Sprite, Position) => { }* 246 | - ***Movement:** (Position, Velocity, Controls) => { }* 247 | - ***Damage:** (Health) => { }* 248 | - ***Bot:** (Position, Velocity, AI) => { }* 249 | 250 | > The logic in a system is inherently reusable because it can be applied to all entities that meet the system's criteria. 251 | 252 | How exactly you choose to define your components, entities and systems is up to you. You'll probably find that coming up with well-defined components and systems will take some practice - but the general pattern is conducive to refactoring and the long term benefits will outweigh the learning curve. 253 | 254 | ### Additional CES Reading Material 255 | 256 | - [Gamedev.net article](https://www.gamedev.net/articles/programming/general-and-gameplay-programming/understanding-component-entity-systems-r3013/) 257 | - [Intro to Entity Systems](https://github.com/junkdog/artemis-odb/wiki/Introduction-to-Entity-Systems) 258 | - [Intro to CES from A-Frame](https://aframe.io/docs/0.7.0/introduction/entity-component-system.html) 259 | 260 | ## License 261 | 262 | MIT License 263 | 264 | Copyright (c) 2018 Boris Berak 265 | 266 | Permission is hereby granted, free of charge, to any person obtaining a copy 267 | of this software and associated documentation files (the "Software"), to deal 268 | in the Software without restriction, including without limitation the rights 269 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 270 | copies of the Software, and to permit persons to whom the Software is 271 | furnished to do so, subject to the following conditions: 272 | 273 | The above copyright notice and this permission notice shall be included in all 274 | copies or substantial portions of the Software. 275 | 276 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 277 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 278 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 279 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 280 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 281 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 282 | SOFTWARE. 283 | --------------------------------------------------------------------------------