├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── demo ├── assets │ ├── 8BITWONDERNominal.eot │ ├── 8BITWONDERNominal.ttf │ ├── 8BITWONDERNominal.woff │ ├── background.wav │ ├── boardwalktile.png │ ├── buildings.png │ ├── character-gif.gif │ ├── character-sprite-grid.png │ ├── character-sprite.png │ ├── intro.png │ ├── jt.wav │ ├── jump.wav │ ├── music.wav │ ├── start.wav │ └── tiles.png ├── code-samples │ ├── loop-use.example │ ├── loop.example │ ├── physics-body-update.example │ ├── physics-body.example │ ├── physics-mobx-update.example │ ├── physics-simple.example │ ├── physics-store.example │ ├── physics-style.example │ ├── physics-update.example │ ├── physics-world-init.example │ ├── physics-world.example │ ├── raf.example │ ├── sprite-manual.example │ ├── sprite-style.example │ ├── sprite.example │ ├── stage-blurry.example │ ├── stage-size.example │ ├── stage-use.example │ ├── stage.example │ ├── tilemap-buildings.example │ ├── tilemap-custom.example │ ├── tilemap-manual.example │ ├── tilemap-map.example │ ├── tilemap-render.example │ └── tilemap.example ├── game │ ├── character.js │ ├── fade.js │ ├── index.js │ ├── level.js │ └── stores │ │ └── game-store.js ├── index.css ├── index.html ├── index.js ├── intro.js ├── presentation.js └── slides │ ├── basics.js │ ├── index.js │ ├── loop.js │ ├── physics.js │ ├── scaling.js │ ├── slide.js │ ├── sprites.js │ └── tilemaps.js ├── native.js ├── package.json ├── src ├── components │ ├── body.js │ ├── loop.js │ ├── sprite.js │ ├── stage.js │ ├── tile-map.js │ └── world.js ├── index.js ├── native │ ├── components │ │ ├── body.js │ │ ├── loop.js │ │ ├── sprite.js │ │ ├── stage.js │ │ ├── tile-map.js │ │ └── world.js │ └── utils │ │ └── game-loop.js └── utils │ ├── audio-player.js │ ├── game-loop.js │ └── key-listener.js ├── webpack.config.dev.js ├── webpack.config.js └── webpack.config.umd.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-flow-strip-types", "transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /demo/** 2 | /dist/** 3 | /interfaces/** 4 | /lib/** 5 | /node_modules/** 6 | /public/** 7 | /umd/** -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | "extends": "formidable/configurations/es6-react" 2 | "parser": "babel-eslint" 3 | "env": 4 | "browser": true 5 | "rules": 6 | "no-magic-numbers": 0 7 | "no-invalid-this": 0 8 | "react/sort-comp": 0 9 | "prefer-const": 0 10 | "comma-dangle": [2, "always-multiline"] 11 | "jsx-quotes": [2, "prefer-double"] 12 | "quotes": [2, "single",{"allowTemplateLiterals": true}] 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | interfaces 3 | 4 | [ignore] 5 | .*/dist/.* 6 | .*/lib/.* 7 | .*/umd/.* 8 | .*/demo/.* 9 | .*/node_modules/flow-bin/.* 10 | 11 | [include] 12 | src 13 | 14 | [options] 15 | esproposal.class_static_fields=enable 16 | esproposal.class_instance_fields=enable 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lib 4 | umd 5 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | dist -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

preact-game-kit

2 | 3 |

4 | Make games with Preact! 5 |

6 | 7 | #### [Live Demo:](https://preactnext.surge.sh) 8 | 9 | [![Live Demo](http://i.imgur.com/om66NeH.gif)](https://preactnext.surge.sh) 10 | 11 | This is a fork of the awesome [react-game-kit](https://github.com/FormidableLabs/react-game-kit), where React has just been aliased out for [Preact](https://github.com/developit/preact). Aside from simplifying the HMR setup (couldn't resist), no code was changed. 12 | 13 | The bundle size after aliasing is **59kb**, and everything works identically! 14 | -------------------------------------------------------------------------------- /demo/assets/8BITWONDERNominal.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/8BITWONDERNominal.eot -------------------------------------------------------------------------------- /demo/assets/8BITWONDERNominal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/8BITWONDERNominal.ttf -------------------------------------------------------------------------------- /demo/assets/8BITWONDERNominal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/8BITWONDERNominal.woff -------------------------------------------------------------------------------- /demo/assets/background.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/background.wav -------------------------------------------------------------------------------- /demo/assets/boardwalktile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/boardwalktile.png -------------------------------------------------------------------------------- /demo/assets/buildings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/buildings.png -------------------------------------------------------------------------------- /demo/assets/character-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/character-gif.gif -------------------------------------------------------------------------------- /demo/assets/character-sprite-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/character-sprite-grid.png -------------------------------------------------------------------------------- /demo/assets/character-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/character-sprite.png -------------------------------------------------------------------------------- /demo/assets/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/intro.png -------------------------------------------------------------------------------- /demo/assets/jt.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/jt.wav -------------------------------------------------------------------------------- /demo/assets/jump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/jump.wav -------------------------------------------------------------------------------- /demo/assets/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/music.wav -------------------------------------------------------------------------------- /demo/assets/start.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/start.wav -------------------------------------------------------------------------------- /demo/assets/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/preact-game-kit/ef191ca8edd915e2687504951770f612876d3872/demo/assets/tiles.png -------------------------------------------------------------------------------- /demo/code-samples/loop-use.example: -------------------------------------------------------------------------------- 1 | class Example extends Component { 2 | static contextTypes = { 3 | loop: PropTypes.object, 4 | }; 5 | 6 | loop = () => { 7 | //Do stuff here 8 | }; 9 | 10 | componentDidMount() { 11 | this.loopID = this.context.loop.subscribe(this.loop); 12 | } 13 | 14 | componentWillUnmount() { 15 | this.context.loop.unsubscribe(this.loopID); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/code-samples/loop.example: -------------------------------------------------------------------------------- 1 | class Game extends Component { 2 | render() { 3 | return ( 4 | 5 | // Child components get this.context.loop 6 | 7 | ) 8 | } 9 | } -------------------------------------------------------------------------------- /demo/code-samples/physics-body-update.example: -------------------------------------------------------------------------------- 1 | class WorldChild extends Component { 2 | move = (body, x) => { 3 | Matter.Body.setVelocity(body, { x, y: 0 }); 4 | }; 5 | 6 | update = () => { 7 | const { body } = this.body; 8 | if (keys.isDown(keys.LEFT)) { 9 | this.move(body, -5); 10 | } else if (keys.isDown(keys.RIGHT)) { 11 | this.move(body, 5); 12 | } 13 | }; 14 | //... 15 | } 16 | -------------------------------------------------------------------------------- /demo/code-samples/physics-body.example: -------------------------------------------------------------------------------- 1 | class WorldChild extends Component { 2 | //... 3 | render() { 4 | return ( 5 | { this.body = b; }} 8 | > 9 | 10 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/code-samples/physics-mobx-update.example: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react'; 2 | 3 | @observer 4 | class WorldChild extends Component { 5 | move = (body, x) => { 6 | Matter.Body.setVelocity(body, { x, y: 0 }); 7 | }; 8 | 9 | update = () => { 10 | const { body } = this.body; 11 | if (keys.isDown(keys.LEFT)) { 12 | this.move(body, -5); 13 | } else if (keys.isDown(keys.RIGHT)) { 14 | this.move(body, 5); 15 | } 16 | store.characterPosition = body.position; 17 | }; 18 | //... 19 | } 20 | -------------------------------------------------------------------------------- /demo/code-samples/physics-simple.example: -------------------------------------------------------------------------------- 1 | // Update loop 2 | const update = () => { 3 | if (rightKeyPressed) { 4 | character.x += 1; 5 | } 6 | if (leftKeyPressed) { 7 | character.x -= 1; 8 | } 9 | }; -------------------------------------------------------------------------------- /demo/code-samples/physics-store.example: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | class GameStore { 4 | @observable characterPosition = { x: 0, y: 0 }; 5 | } 6 | 7 | export default new GameStore(); 8 | -------------------------------------------------------------------------------- /demo/code-samples/physics-style.example: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react'; 2 | 3 | @observer 4 | class Enemy extends Component { 5 | getStyle() { 6 | const {x, y} = store.characterPosition; 7 | 8 | return { 9 | position: 'absolute', 10 | transform: `translate(-${x}px, -${y}px)`, 11 | } 12 | } 13 | 14 | render() { 15 | return
16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/code-samples/physics-update.example: -------------------------------------------------------------------------------- 1 | class WorldChild extends Component { 2 | update = () => { 3 | //Logic goes here 4 | } 5 | 6 | componentDidMount() { 7 | Matter.Events.on(this.context.engine, 'afterUpdate', this.update); 8 | } 9 | 10 | componentWillUnmount() { 11 | Matter.Events.off(this.context.engine, 'afterUpdate', this.update); 12 | } 13 | } -------------------------------------------------------------------------------- /demo/code-samples/physics-world-init.example: -------------------------------------------------------------------------------- 1 | class WorldExample extends Component { 2 | physicsInit = (engine) => { 3 | const ground = Matter.Bodies.rectangle( 4 | 512, 448, 5 | 1024, 64, 6 | { 7 | isStatic: true, 8 | }, 9 | ); 10 | 11 | Matter.World.addBody(engine.world, ground); 12 | } 13 | render() { 14 | return ; 15 | } 16 | } -------------------------------------------------------------------------------- /demo/code-samples/physics-world.example: -------------------------------------------------------------------------------- 1 | class Game extends Component { 2 | render() { 3 | return ( 4 | 5 | 6 | 7 | // Children get this.context.engine 8 | 9 | 10 | 11 | ) 12 | } 13 | } -------------------------------------------------------------------------------- /demo/code-samples/raf.example: -------------------------------------------------------------------------------- 1 | let animationFrame; 2 | 3 | const loop = () => { 4 | // Update logic 5 | animationFrame = requestAnimationFrame(loop); 6 | } 7 | 8 | animationFrame = requestAnimationFrame(loop); -------------------------------------------------------------------------------- /demo/code-samples/sprite-manual.example: -------------------------------------------------------------------------------- 1 | class Sprite extends Component { 2 | render() { 3 | return ( 4 |
10 | 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /demo/code-samples/sprite-style.example: -------------------------------------------------------------------------------- 1 | getImageStyles() { 2 | const left = this.state.step * tileWidth; 3 | const top = this.state.state * tileHeight; 4 | 5 | return { 6 | position: 'absolute', 7 | transform: `translate(-${left}px, -${top}px)`, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /demo/code-samples/sprite.example: -------------------------------------------------------------------------------- 1 | return ( 2 | 9 | ); -------------------------------------------------------------------------------- /demo/code-samples/stage-blurry.example: -------------------------------------------------------------------------------- 1 | getImageStyles() { 2 | const scaledWidth = Math.round(this.props.width * this.context.scale); 3 | 4 | return { 5 | width: scaledWidth, 6 | imageRendering: 'pixelated' 7 | }; 8 | } -------------------------------------------------------------------------------- /demo/code-samples/stage-size.example: -------------------------------------------------------------------------------- 1 | class Game extends Component { 2 | render() { 3 | return ( 4 | 5 | 6 | // Child components get this.context.scale 7 | 8 | 9 | ) 10 | } 11 | } -------------------------------------------------------------------------------- /demo/code-samples/stage-use.example: -------------------------------------------------------------------------------- 1 | getWrapperStyles() { 2 | const x = Math.round(this.state.x * this.context.scale); 3 | 4 | return { 5 | position: 'absolute', 6 | transform: `translate(${x}px, 0px) translateZ(0)`, 7 | transformOrigin: 'top left', 8 | }; 9 | } -------------------------------------------------------------------------------- /demo/code-samples/stage.example: -------------------------------------------------------------------------------- 1 | class Game extends Component { 2 | render() { 3 | return ( 4 | 5 | 6 | // Child components get this.context.scale 7 | 8 | 9 | ) 10 | } 11 | } -------------------------------------------------------------------------------- /demo/code-samples/tilemap-buildings.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/code-samples/tilemap-custom.example: -------------------------------------------------------------------------------- 1 | { 6 | if (tile.index === 2) { 7 | return ; 8 | } 9 | return ; 10 | )} 11 | layers={[ 12 | [ 13 | 0, 0, 2, 0, 14 | 2, 0, 1, 1, 15 | ] 16 | ]} 17 | /> -------------------------------------------------------------------------------- /demo/code-samples/tilemap-manual.example: -------------------------------------------------------------------------------- 1 | layers.forEach((l, index) => { 2 | const layer = []; 3 | for (let r = 0; r < rows; r++) { // Loop over rows 4 | for (let c = 0; c < columns; c++) { // Loop over columns 5 | const gridIndex = (r * columns) + c; // Get index in grid 6 | if (layer[gridIndex] !== 0) { // If it isn't 0 7 | layer.push({ 8 | row: r, 9 | column: c, 10 | tileIndex: layer[gridIndex] 11 | }) 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /demo/code-samples/tilemap-map.example: -------------------------------------------------------------------------------- 1 | const tileMap = { 2 | rows: 4, 3 | columns: 8, 4 | layers: [ 5 | [ 6 | 0, 0, 0, 0, 0, 0, 0, 0, 7 | 0, 0, 0, 0, 0, 0, 0, 0, 8 | 0, 0, 0, 0, 0, 0, 0, 0, 9 | 1, 1, 1, 1, 1, 1, 1, 1, 10 | ], 11 | ], 12 | }; -------------------------------------------------------------------------------- /demo/code-samples/tilemap-render.example: -------------------------------------------------------------------------------- 1 | getTileStyles(column, row, size) { 2 | const left = column * size; 3 | const top = row * size; 4 | 5 | return { 6 | height: size, 7 | width: size, 8 | overflow: 'hidden', 9 | position: 'absolute', 10 | transform: `translate(${left}px, ${top}px)`, 11 | }; 12 | } -------------------------------------------------------------------------------- /demo/code-samples/tilemap.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/game/character.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Matter from 'matter-js'; 4 | 5 | import { 6 | AudioPlayer, 7 | Body, 8 | Sprite, 9 | } from '../../src'; 10 | 11 | @observer 12 | export default class Character extends Component { 13 | 14 | static propTypes = { 15 | keys: PropTypes.object, 16 | onEnterBuilding: PropTypes.func, 17 | store: PropTypes.object, 18 | }; 19 | 20 | static contextTypes = { 21 | engine: PropTypes.object, 22 | scale: PropTypes.number, 23 | }; 24 | 25 | handlePlayStateChanged = (state) => { 26 | this.setState({ 27 | spritePlaying: state ? true : false, 28 | }); 29 | }; 30 | 31 | move = (body, x) => { 32 | Matter.Body.setVelocity(body, { x, y: 0 }); 33 | }; 34 | 35 | jump = (body) => { 36 | this.jumpNoise.play(); 37 | this.isJumping = true; 38 | Matter.Body.applyForce( 39 | body, 40 | { x: 0, y: 0 }, 41 | { x: 0, y: -0.15 }, 42 | ); 43 | Matter.Body.set(body, 'friction', 0.0001); 44 | }; 45 | 46 | punch = () => { 47 | this.isPunching = true; 48 | this.setState({ 49 | characterState: 4, 50 | repeat: false, 51 | }); 52 | } 53 | 54 | getDoorIndex = (body) => { 55 | let doorIndex = null; 56 | 57 | const doorPositions = [...Array(6).keys()].map((a) => { 58 | return [(512 * a) + 208, (512 * a) + 272]; 59 | }); 60 | 61 | doorPositions.forEach((dp, di) => { 62 | if (body.position.x + 64 > dp[0] && body.position.x + 64 < dp[1]) { 63 | doorIndex = di; 64 | } 65 | }); 66 | 67 | return doorIndex; 68 | } 69 | 70 | enterBuilding = (body) => { 71 | const doorIndex = this.getDoorIndex(body); 72 | 73 | if (doorIndex !== null) { 74 | this.setState({ 75 | characterState: 3, 76 | }); 77 | this.isLeaving = true; 78 | this.props.onEnterBuilding(doorIndex); 79 | } 80 | }; 81 | 82 | checkKeys = (shouldMoveStageLeft, shouldMoveStageRight) => { 83 | const { keys, store } = this.props; 84 | const { body } = this.body; 85 | 86 | let characterState = 2; 87 | 88 | if (keys.isDown(65)) { 89 | return this.punch(); 90 | } 91 | 92 | if (keys.isDown(keys.SPACE)) { 93 | this.jump(body); 94 | } 95 | 96 | if (keys.isDown(keys.UP)) { 97 | return this.enterBuilding(body); 98 | } 99 | 100 | if (keys.isDown(keys.LEFT)) { 101 | if (shouldMoveStageLeft) { 102 | store.setStageX(store.stageX + 5); 103 | } 104 | 105 | this.move(body, -5); 106 | characterState = 1; 107 | } else if (keys.isDown(keys.RIGHT)) { 108 | if (shouldMoveStageRight) { 109 | store.setStageX(store.stageX - 5); 110 | } 111 | 112 | this.move(body, 5); 113 | characterState = 0; 114 | } 115 | 116 | this.setState({ 117 | characterState, 118 | repeat: characterState < 2, 119 | }); 120 | } 121 | 122 | update = () => { 123 | const { store } = this.props; 124 | const { body } = this.body; 125 | 126 | const midPoint = Math.abs(store.stageX) + 448; 127 | 128 | const shouldMoveStageLeft = body.position.x < midPoint && store.stageX < 0; 129 | const shouldMoveStageRight = body.position.x > midPoint && store.stageX > -2048; 130 | 131 | const velY = parseFloat(body.velocity.y.toFixed(10)); 132 | 133 | if (velY === 0) { 134 | this.isJumping = false; 135 | Matter.Body.set(body, 'friction', 0.9999); 136 | } 137 | 138 | if (!this.isJumping && !this.isPunching && !this.isLeaving) { 139 | this.checkKeys(shouldMoveStageLeft, shouldMoveStageRight); 140 | 141 | store.setCharacterPosition(body.position); 142 | } else { 143 | if (this.isPunching && this.state.spritePlaying === false) { 144 | this.isPunching = false; 145 | } 146 | 147 | const targetX = store.stageX + (this.lastX - body.position.x); 148 | if (shouldMoveStageLeft || shouldMoveStageRight) { 149 | store.setStageX(targetX); 150 | } 151 | } 152 | 153 | this.lastX = body.position.x; 154 | }; 155 | 156 | constructor(props) { 157 | super(props); 158 | 159 | this.loopID = null; 160 | this.isJumping = false; 161 | this.isPunching = false; 162 | this.isLeaving = false; 163 | this.lastX = 0; 164 | 165 | this.state = { 166 | characterState: 2, 167 | loop: false, 168 | spritePlaying: true, 169 | }; 170 | } 171 | 172 | componentDidMount() { 173 | this.jumpNoise = new AudioPlayer('/assets/jump.wav'); 174 | Matter.Events.on(this.context.engine, 'afterUpdate', this.update); 175 | } 176 | 177 | componentWillUnmount() { 178 | Matter.Events.off(this.context.engine, 'afterUpdate', this.update); 179 | } 180 | 181 | getWrapperStyles() { 182 | const { characterPosition, stageX } = this.props.store; 183 | const { scale } = this.context; 184 | const { x, y } = characterPosition; 185 | const targetX = x + stageX; 186 | 187 | return { 188 | position: 'absolute', 189 | transform: `translate(${targetX * scale}px, ${y * scale}px)`, 190 | transformOrigin: 'left top', 191 | }; 192 | } 193 | 194 | render() { 195 | const x = this.props.store.characterPosition.x; 196 | 197 | return ( 198 |
199 | { this.body = b; }} 203 | > 204 | 212 | 213 |
214 | ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /demo/game/fade.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Fade = (props) => ( 4 |
7 | ); 8 | 9 | Fade.propTypes = { 10 | visible: PropTypes.bool, 11 | }; 12 | 13 | Fade.defaultProps = { 14 | visible: true, 15 | }; 16 | 17 | export default Fade; 18 | -------------------------------------------------------------------------------- /demo/game/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Matter from 'matter-js'; 3 | 4 | import { 5 | AudioPlayer, 6 | Loop, 7 | Stage, 8 | KeyListener, 9 | World, 10 | } from '../../src'; 11 | 12 | import Character from './character'; 13 | import Level from './level'; 14 | import Fade from './fade'; 15 | 16 | import GameStore from './stores/game-store'; 17 | 18 | export default class Game extends Component { 19 | 20 | static propTypes = { 21 | onLeave: PropTypes.func, 22 | }; 23 | 24 | physicsInit = (engine) => { 25 | const ground = Matter.Bodies.rectangle( 26 | 512 * 3, 448, 27 | 1024 * 3, 64, 28 | { 29 | isStatic: true, 30 | }, 31 | ); 32 | 33 | const leftWall = Matter.Bodies.rectangle( 34 | -64, 288, 35 | 64, 576, 36 | { 37 | isStatic: true, 38 | }, 39 | ); 40 | 41 | const rightWall = Matter.Bodies.rectangle( 42 | 3008, 288, 43 | 64, 576, 44 | { 45 | isStatic: true, 46 | }, 47 | ); 48 | 49 | Matter.World.addBody(engine.world, ground); 50 | Matter.World.addBody(engine.world, leftWall); 51 | Matter.World.addBody(engine.world, rightWall); 52 | } 53 | 54 | handleEnterBuilding = (index) => { 55 | this.setState({ 56 | fade: true, 57 | }); 58 | setTimeout(() => { 59 | this.props.onLeave(index); 60 | }, 500); 61 | } 62 | 63 | constructor(props) { 64 | super(props); 65 | 66 | this.state = { 67 | fade: true, 68 | }; 69 | this.keyListener = new KeyListener(); 70 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 71 | window.context = window.context || new AudioContext(); 72 | } 73 | 74 | componentDidMount() { 75 | this.player = new AudioPlayer('/assets/music.wav', () => { 76 | this.stopMusic = this.player.play({ loop: true, offset: 1, volume: 0.35 }); 77 | }); 78 | 79 | this.setState({ 80 | fade: false, 81 | }); 82 | 83 | this.keyListener.subscribe([ 84 | this.keyListener.LEFT, 85 | this.keyListener.RIGHT, 86 | this.keyListener.UP, 87 | this.keyListener.SPACE, 88 | 65, 89 | ]); 90 | } 91 | 92 | componentWillUnmount() { 93 | this.stopMusic(); 94 | this.keyListener.unsubscribe(); 95 | } 96 | 97 | render() { 98 | return ( 99 | 100 | 101 | 104 | 107 | 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /demo/game/level.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { autorun } from 'mobx'; 3 | 4 | import { 5 | TileMap, 6 | } from '../../src'; 7 | 8 | import GameStore from './stores/game-store'; 9 | 10 | export default class Level extends Component { 11 | 12 | static contextTypes = { 13 | scale: PropTypes.number, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | stageX: 0, 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | this.cameraWatcher = autorun(() => { 26 | const targetX = Math.round(GameStore.stageX * this.context.scale); 27 | this.setState({ 28 | stageX: targetX, 29 | }); 30 | }); 31 | } 32 | 33 | componentWillReceiveProps(nextProps, nextContext) { 34 | const targetX = Math.round(GameStore.stageX * nextContext.scale); 35 | this.setState({ 36 | stageX: targetX, 37 | }); 38 | } 39 | 40 | componentWillUnmount() { 41 | this.cameraWatcher(); 42 | } 43 | 44 | getWrapperStyles() { 45 | return { 46 | position: 'absolute', 47 | transform: `translate(${this.state.stageX}px, 0px) translateZ(0)`, 48 | transformOrigin: 'top left', 49 | }; 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 | 70 | 80 |
81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /demo/game/stores/game-store.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | class GameStore { 4 | @observable characterPosition = { x: 0, y: 0 }; 5 | 6 | @observable stageX = 0; 7 | 8 | setCharacterPosition(position) { 9 | this.characterPosition = position; 10 | } 11 | 12 | setStageX(x) { 13 | if (x > 0) { 14 | this.stageX = 0; 15 | } else if (x < -2048) { 16 | this.stageX = -2048; 17 | } else { 18 | this.stageX = x; 19 | } 20 | } 21 | } 22 | 23 | export default new GameStore(); 24 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: '8BIT WONDER'; 3 | src: url('assets/8BITWONDERNominal.eot'); 4 | src: url('assets/8BITWONDERNominal.eot?#iefix') format('embedded-opentype'), 5 | url('assets/8BITWONDERNominal.woff') format('woff'), 6 | url('assets/8BITWONDERNominal.ttf') format('truetype'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | html, body, #root { 12 | background: black; 13 | height: 100%; 14 | width: 100%; 15 | margin: 0; 16 | padding: 0; 17 | color: white; 18 | font-family: 'Helvetica Neue'; 19 | font-size: 3vw; 20 | } 21 | 22 | .yellow { 23 | color: #f1c40f; 24 | } 25 | 26 | del { 27 | color: #e74c3c; 28 | } 29 | 30 | p { 31 | font-size: 1.2em; 32 | line-height: 1.5; 33 | } 34 | 35 | li { 36 | font-size: 1.3em; 37 | line-height: 1.5; 38 | } 39 | 40 | pre { 41 | font-size: 1.5vw; 42 | max-height: 100%; 43 | overflow: hidden; 44 | } 45 | 46 | .intro { 47 | margin: auto; 48 | width: 100%; 49 | max-width: 1024px; 50 | display: block; 51 | } 52 | 53 | .start { 54 | font-family: '8BIT WONDER'; 55 | display: block; 56 | width: 400px; 57 | font-size: 24px; 58 | margin: -1% auto 0px; 59 | text-align: center; 60 | color: white; 61 | } 62 | 63 | .fade { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | bottom: 0; 68 | right: 0; 69 | background: black; 70 | -webkit-transition: 500ms opacity linear; 71 | transition: 1s opacity linear; 72 | opacity: 0; 73 | } 74 | 75 | .fade.active { 76 | opacity: 1; 77 | } 78 | 79 | * { 80 | box-sizing: border-box; 81 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Game Kit 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | function init() { 5 | const Presentation = require('./presentation').default; 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | } 11 | 12 | init(); 13 | 14 | if (module.hot) module.hot.accept('./presentation', init); 15 | -------------------------------------------------------------------------------- /demo/intro.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { AudioPlayer } from '../src'; 3 | 4 | export default class Intro extends Component { 5 | static propTypes = { 6 | onStart: PropTypes.func, 7 | }; 8 | 9 | startUpdate = () => { 10 | this.animationFrame = requestAnimationFrame(this.startUpdate); 11 | } 12 | 13 | handleKeyPress = (e) => { 14 | if (e.keyCode === 13) { 15 | this.startNoise.play(); 16 | this.props.onStart(); 17 | } 18 | } 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | blink: false, 25 | }; 26 | } 27 | 28 | componentDidMount() { 29 | this.startNoise = new AudioPlayer('/assets/start.wav'); 30 | window.addEventListener('keypress', this.handleKeyPress); 31 | this.animationFrame = requestAnimationFrame(this.startUpdate); 32 | this.interval = setInterval(() => { 33 | this.setState({ 34 | blink: !this.state.blink, 35 | }); 36 | }, 500); 37 | } 38 | 39 | componentWillUnmount() { 40 | window.removeEventListener('keypress', this.handleKeyPress); 41 | cancelAnimationFrame(this.animationFrame); 42 | clearInterval(this.interval); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 49 |

53 | Press Start 54 |

55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demo/presentation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Intro from './intro'; 4 | import Game from './game'; 5 | import Slides from './slides'; 6 | 7 | export default class Presentation extends Component { 8 | 9 | handleStart = () => { 10 | this.setState({ 11 | gameState: 1, 12 | }); 13 | }; 14 | 15 | handleDone = () => { 16 | this.setState({ 17 | gameState: 1, 18 | }); 19 | }; 20 | 21 | handleLeave = (index) => { 22 | this.setState({ 23 | gameState: 2, 24 | slideIndex: index, 25 | }); 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | gameState: 0, 33 | slideIndex: 0, 34 | }; 35 | } 36 | render() { 37 | this.gameStates = [ 38 | , 39 | , 40 | , 41 | ]; 42 | return this.gameStates[this.state.gameState]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/slides/basics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | 4 | import Slide from './slide'; 5 | 6 | export default { 7 | slides: [ 8 | 9 |

Disclaimer:

10 |

I'm not a game dev. I just build games for fun.

11 |
, 12 | 13 |

Should you build a game with React?

14 |
, 15 | 16 |

Can Should you build a game with React?

17 |
, 18 | 19 |

You sure can!

20 |
, 21 | 22 |

Why would you build a game with React?

23 |
, 24 | 25 |
    26 |
  • The same game code can work on the web, iOS & Android
  • 27 |
  • You primarily write React code
  • 28 |
  • You dont feel like learning Unity
  • 29 |
  • You can hot reload game logic
  • 30 |
31 |
, 32 | 33 |

What is a game?

34 |
, 35 | 36 |

"A form of play or sport, especially a competitive one played according to rules and decided by skill, strength, or luck."

37 |
, 38 | 39 |

Today we are going to learn how to make a 2d platformer game with ReactJS

40 |
, 41 | 42 |

Basic Concepts

43 |
, 44 | 45 |

Game Loop

46 |

A programmatic loop that gets input, updates game state and draws the game.

47 |
, 48 | 49 |

Tick

50 |

Each step of the loop.

51 |
, 52 | 53 |

Update Function

54 |

A function called on each tick where game logic is checked.

55 |
, 56 | 57 |

Stage

58 |

The main game container to which game entities are added.

59 |
, 60 | 61 |

Sprite

62 |

An often animated bitmap graphic derived from a larger tiled image of states and steps.

63 |
, 64 | 65 |

TileMap

66 |

A large graphic created by rendering a matrix of position indexes derived from a smaller set of common tiles.

67 |
, 68 | 69 |

Physics Engine

70 |

A class that simulates physical systems.

71 |
, 72 | 73 |

Rigid Body Physics Engine

74 |

A physics engine that assumes that physical bodies are not elastic or fluid.

75 |
, 76 | 77 |

Physics World

78 |

A class that provides a set of conditions that the simulation abides by.

79 |
, 80 | 81 |

Physics Body

82 |

A class that acts as an entity inside the physics world.

83 |
, 84 | 85 |

This sounds hard.

86 |

But it doesn't have to be!

87 |
, 88 | 89 |

Introducing:

90 |

react-game-kit

91 |
, 92 | 93 |

94 | A collection of ReactJS components and utilities that help you make awesome games. 95 |

96 |
, 97 | 98 |

99 | It's pretty fun. In fact, this entire presentation is built in it. 100 |

101 |
, 102 | 103 |

104 | Oh, and it works on React Native too! 105 |

106 |
, 107 | ], 108 | }; 109 | -------------------------------------------------------------------------------- /demo/slides/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Basics from './basics'; 4 | import Loop from './loop'; 5 | import Scaling from './scaling'; 6 | import Sprites from './sprites'; 7 | import TileMaps from './tilemaps'; 8 | import Physics from './physics'; 9 | 10 | const slides = [Basics, Loop, Scaling, Sprites, TileMaps, Physics]; 11 | 12 | export default class Slides extends Component { 13 | 14 | static propTypes = { 15 | index: PropTypes.number, 16 | onDone: PropTypes.func, 17 | }; 18 | 19 | restartLoop = () => { 20 | setTimeout(() => { 21 | this.startUpdate(); 22 | }, 300); 23 | }; 24 | 25 | highlight = () => { 26 | if (window.Prism) { 27 | window.Prism.highlightAll(); 28 | } 29 | }; 30 | 31 | startUpdate = () => { 32 | this.animationFrame = requestAnimationFrame(this.startUpdate); 33 | }; 34 | 35 | handleKeyPress = (e) => { 36 | if (e.keyCode === 27) { 37 | this.props.onDone(); 38 | } 39 | 40 | if (e.keyCode === 37) { 41 | this.handlePrev(); 42 | } 43 | 44 | if (e.keyCode === 39) { 45 | this.handleNext(); 46 | } 47 | }; 48 | 49 | handleNext(restartLoop) { 50 | const { currentSlide } = this.state; 51 | const { index } = this.props; 52 | 53 | if (currentSlide + 1 === slides[index].slides.length) { 54 | this.props.onDone(); 55 | } else { 56 | this.setState({ 57 | currentSlide: currentSlide + 1, 58 | }, () => { 59 | if (restartLoop) { 60 | this.restartLoop(); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | handlePrev(restartLoop) { 67 | const { currentSlide } = this.state; 68 | 69 | if (currentSlide !== 0) { 70 | this.setState({ 71 | currentSlide: currentSlide - 1, 72 | }, () => { 73 | if (restartLoop) { 74 | this.restartLoop(); 75 | } 76 | }); 77 | } else if (restartLoop) { 78 | this.restartLoop(); 79 | } 80 | } 81 | 82 | constructor(props) { 83 | super(props); 84 | 85 | this.state = { 86 | currentSlide: 0, 87 | }; 88 | } 89 | 90 | componentDidMount() { 91 | this.highlight(); 92 | window.addEventListener('keyup', this.handleKeyPress); 93 | window.addEventListener('keypress', this.handleKeyPress); 94 | this.animationFrame = requestAnimationFrame(this.startUpdate); 95 | } 96 | 97 | componentWillUnmount() { 98 | window.removeEventListener('keyup', this.handleKeyPress); 99 | window.removeEventListener('keypress', this.handleKeyPress); 100 | cancelAnimationFrame(this.animationFrame); 101 | } 102 | 103 | componentDidUpdate() { 104 | this.highlight(); 105 | } 106 | 107 | getWrapperStyles() { 108 | return { 109 | height: '100%', 110 | width: '100%', 111 | display: 'flex', 112 | alignItems: 'stretch', 113 | justifyContent: 'center', 114 | }; 115 | } 116 | 117 | render() { 118 | return ( 119 |
120 | {slides[this.props.index].slides[this.state.currentSlide]} 121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /demo/slides/loop.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | import Slide from './slide'; 4 | 5 | export default { 6 | slides: [ 7 | 8 |

How does the loop work?

9 |
, 10 | 11 |

requestAnimationFrame

12 |
, 13 | 14 |
15 |         
16 |           {require('raw-loader!../code-samples/raf.example')}
17 |         
18 |       
19 |
, 20 | 21 |

How can I implement this in React with react-game-kit?

22 |
, 23 | 24 |
25 |         
26 |           {require('raw-loader!../code-samples/loop.example')}
27 |         
28 |       
29 |
, 30 | 31 |

Wait, how does context work?

32 |
, 33 | 34 |
35 |         
36 |           {require('raw-loader!../code-samples/loop-use.example')}
37 |         
38 |       
39 |
, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /demo/slides/physics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | 4 | import Slide from './slide'; 5 | 6 | export default { 7 | slides: [ 8 | 9 |

You probably don't need a physics engine

10 |
, 11 | 12 |
13 |         
14 |           {require('raw-loader!../code-samples/physics-simple.example')}
15 |         
16 |       
17 |
, 18 | 19 |

But lets say you do want physics

20 |
, 21 | 22 |

react-game-kit provides physics helpers provided by matter-js

23 |
, 24 | 25 |
26 |         
27 |           {require('raw-loader!../code-samples/physics-world.example')}
28 |         
29 |       
30 |
, 31 | 32 |
33 |         
34 |           {require('raw-loader!../code-samples/physics-world-init.example')}
35 |         
36 |       
37 |
, 38 | 39 |

When using matter-js physics, it's important to do physics updates after the world has updated.

40 |
, 41 | 42 |
43 |         
44 |           {require('raw-loader!../code-samples/physics-update.example')}
45 |         
46 |       
47 |
, 48 | 49 |

Using physics bodies

50 |
, 51 | 52 |
53 |         
54 |           {require('raw-loader!../code-samples/physics-body.example')}
55 |         
56 |       
57 |
, 58 | 59 |
60 |         
61 |           {require('raw-loader!../code-samples/physics-body-update.example')}
62 |         
63 |       
64 |
, 65 | 66 |

Performant use of physics data for positioning

67 |
, 68 | 69 |

mobx

70 |
, 71 | 72 |
73 |         
74 |           {require('raw-loader!../code-samples/physics-store.example')}
75 |         
76 |       
77 |
, 78 | 79 |
80 |         
81 |           {require('raw-loader!../code-samples/physics-mobx-update.example')}
82 |         
83 |       
84 |
, 85 | 86 |
87 |         
88 |           {require('raw-loader!../code-samples/physics-style.example')}
89 |         
90 |       
91 |
, 92 | ], 93 | }; 94 | -------------------------------------------------------------------------------- /demo/slides/scaling.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | 4 | import Slide from './slide'; 5 | 6 | export default { 7 | slides: [ 8 | 9 |

How can we size and scale our game?

10 |
, 11 | 12 |

transform: scale()

13 |
, 14 | 15 |

transform: scale()

16 |
, 17 | 18 |

Rounding errors on subpixel floats mean we have to manually round & scale.

19 |
, 20 | 21 |

react-game-kit provides a Stage component to help with this

22 |
, 23 | 24 |
25 |         
26 |           {require('raw-loader!../code-samples/stage.example')}
27 |         
28 |       
29 |
, 30 | 31 |

Most screens you are targeting will have a 16:9 aspect ratio

32 |
, 33 | 34 |
35 |         
36 |           {require('raw-loader!../code-samples/stage-size.example')}
37 |         
38 |       
39 |
, 40 | 41 |
42 |         
43 |           {require('raw-loader!../code-samples/stage-use.example')}
44 |         
45 |       
46 |
, 47 | 48 |

That's cool, but won't my images be blurry?

49 |
, 50 | 51 |
52 |         
53 |           {require('raw-loader!../code-samples/stage-blurry.example')}
54 |         
55 |       
56 |
, 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /demo/slides/slide.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const slideStyles = { 4 | display: 'flex', 5 | flex: '1 1 0', 6 | alignItems: 'center', 7 | justifyContent: 'flex-start', 8 | maxWidth: '166vh', 9 | padding: 20, 10 | }; 11 | 12 | const Slide = (props) => ( 13 |
14 |
15 | {props.children} 16 |
17 |
18 | ); 19 | 20 | export default Slide; 21 | -------------------------------------------------------------------------------- /demo/slides/sprites.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | 4 | import Slide from './slide'; 5 | 6 | export default { 7 | slides: [ 8 | 9 |

So how do sprites work?

10 |
, 11 | 12 |
13 | 14 |
15 |
, 16 | 17 |
18 | 19 |
20 |
, 21 | 22 |
23 | 24 |
25 |
, 26 | 27 |
28 |         
29 |           {require('raw-loader!../code-samples/sprite-manual.example')}
30 |         
31 |       
32 |
, 33 | 34 |
35 |         
36 |           {require('raw-loader!../code-samples/sprite-style.example')}
37 |         
38 |       
39 |
, 40 | 41 |

react-game-kit provides a Sprite component to simplify this process.

42 |
, 43 | 44 |
45 |         
46 |           {require('raw-loader!../code-samples/sprite.example')}
47 |         
48 |       
49 |
, 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /demo/slides/tilemaps.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-key, max-len */ 2 | import React from 'react'; 3 | 4 | import Slide from './slide'; 5 | 6 | export default { 7 | slides: [ 8 | 9 |

What in the world is a tilemap?

10 |
, 11 | 12 |

Tile maps use a tile atlas to use a few graphics to create a "level"

13 |
, 14 | 15 |

Tile maps have tiles and layers

16 |
, 17 | 18 | 19 | , 20 | 21 | 22 | , 23 | 24 |

Ok, so what does the map look like?

25 |
, 26 | 27 |
28 |         
29 |           {require('raw-loader!../code-samples/tilemap-map.example')}
30 |         
31 |       
32 |
, 33 | 34 |

Parsing a tile map

35 |
, 36 | 37 |
38 |         
39 |           {require('raw-loader!../code-samples/tilemap-manual.example')}
40 |         
41 |       
42 |
, 43 | 44 |
45 |         
46 |           {require('raw-loader!../code-samples/tilemap-render.example')}
47 |         
48 |       
49 |
, 50 | 51 |

react-game-kit provides a TileMap component to simplify this process.

52 |
, 53 | 54 |
55 |         
56 |           {require('raw-loader!../code-samples/tilemap.example')}
57 |         
58 |       
59 |
, 60 | 61 |
62 |         
63 |           {require('raw-loader!../code-samples/tilemap-buildings.example')}
64 |         
65 |       
66 |
, 67 | 68 |

Why not just make it one big image?

69 |
, 70 | 71 |
72 |         
73 |           {require('raw-loader!../code-samples/tilemap-custom.example')}
74 |         
75 |       
76 |
, 77 | ], 78 | }; 79 | -------------------------------------------------------------------------------- /native.js: -------------------------------------------------------------------------------- 1 | import Body from './lib/native/components/body.js'; 2 | import Loop from './lib/native/components/loop.js'; 3 | import Sprite from './lib/native/components/sprite.js'; 4 | import Stage from './lib/native/components/stage.js'; 5 | import TileMap from './lib/native/components/tile-map.js'; 6 | import World from './lib/native/components/world.js'; 7 | 8 | export { 9 | Body, 10 | Loop, 11 | Sprite, 12 | Stage, 13 | TileMap, 14 | World, 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-game-kit", 3 | "version": "0.0.1", 4 | "description": "Make games with react", 5 | "main": "lib", 6 | "files": [ 7 | "native.js", 8 | "lib", 9 | "umd" 10 | ], 11 | "scripts": { 12 | "start": "webpack-dev-server --hot --inline --port 3000 --config webpack.config.dev.js --content-base demo/", 13 | "build": "babel src -d lib --copy-files", 14 | "clean": "rimraf dist", 15 | "clean-umd": "rimraf umd", 16 | "copy-assets": "cp -a demo/assets/. dist/assets", 17 | "copy-html-css": "cp -a demo/index.html dist/index.html && cp -a demo/index.css dist/index.css", 18 | "dist": "npm run clean && webpack -p && npm run copy-assets && npm run copy-html-css", 19 | "lint": "eslint src demo --fix", 20 | "umd": "npm run clean-umd && webpack --config webpack.config.umd.js" 21 | }, 22 | "author": "Ken Wheeler", 23 | "license": "MIT", 24 | "repository": "https://github.com/FormidableLabs/react-game-kit", 25 | "dependencies": { 26 | "matter-js": "^0.10.0", 27 | "preact": "^7.2.0", 28 | "preact-compat": "^3.1.0" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.10.1", 32 | "babel-core": "^6.10.4", 33 | "babel-eslint": "^6.1.2", 34 | "babel-loader": "^6.2.4", 35 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 36 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 37 | "babel-preset-es2015": "^6.9.0", 38 | "babel-preset-react": "^6.11.1", 39 | "babel-preset-stage-0": "^6.5.0", 40 | "css-loader": "^0.23.1", 41 | "eslint": "^3.3.1", 42 | "eslint-config-formidable": "^1.0.1", 43 | "eslint-plugin-filenames": "^1.1.0", 44 | "eslint-plugin-import": "^1.14.0", 45 | "eslint-plugin-jsx-a11y": "^2.1.0", 46 | "eslint-plugin-react": "^6.1.2", 47 | "json-loader": "^0.5.4", 48 | "mobx": "^2.5.0", 49 | "mobx-react": "^3.5.5", 50 | "postcss-loader": "^0.10.1", 51 | "raw-loader": "^0.5.1", 52 | "rimraf": "^2.5.4", 53 | "style-loader": "^0.13.1", 54 | "webpack": "^1.13.1", 55 | "webpack-dev-server": "^1.15.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/body.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | 3 | import Matter, { World, Bodies } from 'matter-js'; 4 | 5 | export default class Body extends Component { 6 | 7 | static propTypes = { 8 | angle: PropTypes.number, 9 | area: PropTypes.string, 10 | args: PropTypes.array, 11 | axes: PropTypes.shape({ 12 | x: PropTypes.number, 13 | y: PropTypes.number, 14 | }), 15 | bounds: PropTypes.shape({ 16 | min: PropTypes.shape({ 17 | x: PropTypes.number, 18 | y: PropTypes.number, 19 | }), 20 | max: PropTypes.shape({ 21 | x: PropTypes.number, 22 | y: PropTypes.number, 23 | }), 24 | }), 25 | children: PropTypes.any, 26 | collisionFilter: PropTypes.shape({ 27 | category: PropTypes.number, 28 | group: PropTypes.number, 29 | mask: PropTypes.number, 30 | }), 31 | density: PropTypes.number, 32 | force: PropTypes.shape({ 33 | x: PropTypes.number, 34 | y: PropTypes.number, 35 | }), 36 | friction: PropTypes.number, 37 | frictionAir: PropTypes.number, 38 | frictionStatic: PropTypes.number, 39 | id: PropTypes.number, 40 | inertia: PropTypes.number, 41 | inverseInertia: PropTypes.number, 42 | inverseMass: PropTypes.number, 43 | isSensor: PropTypes.bool, 44 | isSleeping: PropTypes.bool, 45 | isStatic: PropTypes.bool, 46 | label: PropTypes.string, 47 | mass: PropTypes.number, 48 | position: PropTypes.shape({ 49 | x: PropTypes.number, 50 | y: PropTypes.number, 51 | }), 52 | restitution: PropTypes.number, 53 | shape: PropTypes.string, 54 | sleepThreshold: PropTypes.number, 55 | slop: PropTypes.number, 56 | slope: PropTypes.number, 57 | timeScale: PropTypes.number, 58 | torque: PropTypes.number, 59 | vertices: PropTypes.array, 60 | }; 61 | 62 | static defaultProps = { 63 | args: [0, 0, 100, 100], 64 | restitution: 0, 65 | friction: 1, 66 | frictionStatic: 0, 67 | shape: 'rectangle', 68 | }; 69 | 70 | static contextTypes = { 71 | engine: PropTypes.object, 72 | }; 73 | 74 | static childContextTypes = { 75 | body: PropTypes.object, 76 | }; 77 | 78 | constructor(props, context) { 79 | super(props); 80 | 81 | const { args, children, shape, ...options } = props; 82 | 83 | this.body = Bodies[shape](...args, options); 84 | World.addBody(context.engine.world, this.body); 85 | } 86 | 87 | componentWillReceiveProps(nextProps) { 88 | const { args, children, shape, ...options } = nextProps; 89 | 90 | Object.keys(options).forEach((option) => { 91 | if (option in this.body && this.props[option] !== nextProps[option]) { 92 | Matter.Body.set(this.body, option, options[option]); 93 | } 94 | }); 95 | } 96 | 97 | componentWillUnmount() { 98 | World.remove(this.context.engine.world, this.body); 99 | } 100 | 101 | getChildContext() { 102 | return { 103 | body: this.body, 104 | }; 105 | } 106 | 107 | render() { 108 | return this.props.children; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/components/loop.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import GameLoop from '../utils/game-loop'; 4 | 5 | export default class Loop extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.any, 9 | style: PropTypes.object, 10 | }; 11 | 12 | static childContextTypes = { 13 | loop: PropTypes.object, 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.loop = new GameLoop(); 20 | } 21 | 22 | componentDidMount() { 23 | this.loop.start(); 24 | } 25 | 26 | componentWillUnmount() { 27 | this.loop.stop(); 28 | } 29 | 30 | getChildContext() { 31 | return { 32 | loop: this.loop, 33 | }; 34 | } 35 | 36 | render() { 37 | const defaultStyles = { 38 | height: '100%', 39 | width: '100%', 40 | }; 41 | const styles = { ...defaultStyles, ...this.props.style }; 42 | return ( 43 |
44 | {this.props.children} 45 |
46 | ); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/components/sprite.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Sprite extends Component { 4 | 5 | static propTypes = { 6 | offset: PropTypes.array, 7 | onPlayStateChanged: PropTypes.func, 8 | repeat: PropTypes.bool, 9 | scale: PropTypes.number, 10 | src: PropTypes.string, 11 | state: PropTypes.number, 12 | steps: PropTypes.array, 13 | style: PropTypes.object, 14 | ticksPerFrame: PropTypes.number, 15 | tileHeight: PropTypes.number, 16 | tileWidth: PropTypes.number, 17 | }; 18 | 19 | static defaultProps = { 20 | offset: [0, 0], 21 | onPlayStateChanged: () => {}, 22 | repeat: true, 23 | src: '', 24 | state: 0, 25 | steps: [], 26 | ticksPerFrame: 4, 27 | tileHeight: 64, 28 | tileWidth: 64, 29 | }; 30 | 31 | static contextTypes = { 32 | loop: PropTypes.object, 33 | scale: PropTypes.number, 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | this.loopID = null; 40 | this.tickCount = 0; 41 | this.finished = false; 42 | 43 | this.state = { 44 | currentStep: 0, 45 | }; 46 | } 47 | 48 | componentDidMount() { 49 | this.props.onPlayStateChanged(1); 50 | const animate = this.animate.bind(this, this.props); 51 | this.loopID = this.context.loop.subscribe(animate); 52 | } 53 | 54 | componentWillReceiveProps(nextProps) { 55 | if (nextProps.state !== this.props.state) { 56 | this.finished = false; 57 | this.props.onPlayStateChanged(1); 58 | this.context.loop.unsubscribe(this.loopID); 59 | this.tickCount = 0; 60 | 61 | this.setState({ 62 | currentStep: 0, 63 | }, () => { 64 | const animate = this.animate.bind(this, nextProps); 65 | this.loopID = this.context.loop.subscribe(animate); 66 | }); 67 | } 68 | } 69 | 70 | componentWillUnmount() { 71 | this.context.loop.unsubscribe(this.loopID); 72 | } 73 | 74 | animate(props) { 75 | const { repeat, ticksPerFrame, state, steps } = props; 76 | 77 | if (this.tickCount === ticksPerFrame && !this.finished) { 78 | if (steps[state] !== 0) { 79 | const { currentStep } = this.state; 80 | const lastStep = steps[state]; 81 | const nextStep = currentStep === lastStep ? 0 : currentStep + 1; 82 | 83 | this.setState({ 84 | currentStep: nextStep, 85 | }); 86 | 87 | if (currentStep === lastStep && repeat === false) { 88 | this.finished = true; 89 | this.props.onPlayStateChanged(0); 90 | } 91 | } 92 | 93 | this.tickCount = 0; 94 | } else { 95 | this.tickCount++; 96 | } 97 | 98 | } 99 | 100 | getImageStyles() { 101 | const { currentStep } = this.state; 102 | const { state, tileWidth, tileHeight } = this.props; 103 | 104 | const left = this.props.offset[0] + (currentStep * tileWidth); 105 | const top = this.props.offset[1] + (state * tileHeight); 106 | 107 | return { 108 | position: 'absolute', 109 | transform: `translate(-${left}px, -${top}px)`, 110 | }; 111 | } 112 | 113 | getWrapperStyles() { 114 | return { 115 | height: this.props.tileHeight, 116 | width: this.props.tileWidth, 117 | overflow: 'hidden', 118 | position: 'relative', 119 | transform: `scale(${this.props.scale || this.context.scale})`, 120 | transformOrigin: 'top left', 121 | imageRendering: 'pixelated', 122 | }; 123 | } 124 | 125 | render() { 126 | return ( 127 |
128 | 132 |
133 | ); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/components/stage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Stage extends Component { 4 | 5 | static propTypes = { 6 | children: PropTypes.any, 7 | height: PropTypes.number, 8 | style: PropTypes.object, 9 | width: PropTypes.number, 10 | }; 11 | 12 | static defaultProps = { 13 | width: 1024, 14 | height: 576, 15 | }; 16 | 17 | static contextTypes = { 18 | loop: PropTypes.object, 19 | } 20 | 21 | static childContextTypes = { 22 | loop: PropTypes.object, 23 | scale: PropTypes.number, 24 | }; 25 | 26 | setDimensions = () => { 27 | this.setState({ 28 | dimensions: [ 29 | this.container.offsetWidth, 30 | this.container.offsetHeight, 31 | ], 32 | }); 33 | } 34 | 35 | constructor(props) { 36 | super(props); 37 | 38 | this.container = null; 39 | 40 | this.state = { 41 | dimensions: [0, 0], 42 | }; 43 | } 44 | 45 | componentDidMount() { 46 | window.addEventListener('resize', this.setDimensions); 47 | this.setDimensions(); 48 | } 49 | 50 | componentWillUnmount() { 51 | window.removeEventListener('resize', this.setDimensions); 52 | } 53 | 54 | getChildContext() { 55 | return { 56 | scale: this.getScale().scale, 57 | loop: this.context.loop, 58 | }; 59 | } 60 | 61 | getScale() { 62 | const [vwidth, vheight] = this.state.dimensions; 63 | const { height, width } = this.props; 64 | 65 | let targetWidth; 66 | let targetHeight; 67 | let targetScale; 68 | 69 | if (height / width > vheight / vwidth) { 70 | targetHeight = vheight; 71 | targetWidth = targetHeight * width / height; 72 | targetScale = vheight / height; 73 | } else { 74 | targetWidth = vwidth; 75 | targetHeight = targetWidth * height / width; 76 | targetScale = vwidth / width; 77 | } 78 | 79 | if (!this.container) { 80 | return { 81 | height, 82 | width, 83 | scale: 1, 84 | }; 85 | } else { 86 | return { 87 | height: targetHeight, 88 | width: targetWidth, 89 | scale: targetScale, 90 | }; 91 | } 92 | } 93 | 94 | getWrapperStyles() { 95 | return { 96 | height: '100%', 97 | width: '100%', 98 | position: 'relative', 99 | }; 100 | } 101 | 102 | getInnerStyles() { 103 | const scale = this.getScale(); 104 | const xOffset = Math.floor((this.state.dimensions[0] - scale.width) / 2); 105 | const yOffset = Math.floor((this.state.dimensions[1] - scale.height) / 2); 106 | 107 | return { 108 | height: Math.floor(scale.height), 109 | width: Math.floor(scale.width), 110 | position: 'absolute', 111 | overflow: 'hidden', 112 | transform: `translate(${xOffset}px, ${yOffset}px)`, 113 | }; 114 | } 115 | 116 | render() { 117 | return ( 118 |
{ this.container = c; }}> 119 |
120 | {this.props.children} 121 |
122 |
123 | ); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/components/tile-map.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | 4 | export default class TileMap extends Component { 5 | 6 | static propTypes = { 7 | columns: PropTypes.number, 8 | layers: PropTypes.array, 9 | renderTile: PropTypes.func, 10 | rows: PropTypes.number, 11 | scale: PropTypes.number, 12 | src: PropTypes.string, 13 | style: PropTypes.object, 14 | tileSize: PropTypes.number, 15 | }; 16 | 17 | static defaultProps = { 18 | columns: 16, 19 | layers: [], 20 | renderTile: (tile, src, styles) => ( 21 | 25 | ), 26 | rows: 9, 27 | src: '', 28 | tileSize: 64, 29 | }; 30 | 31 | static contextTypes = { 32 | scale: PropTypes.number, 33 | }; 34 | 35 | shouldComponentUpdate(nextProps, nextState, nextContext) { 36 | return this.context.scale !== nextContext.scale; 37 | } 38 | 39 | generateMap() { 40 | const { columns, layers, rows } = this.props; 41 | 42 | const mappedLayers = []; 43 | 44 | layers.forEach((l, index) => { 45 | const layer = []; 46 | for (let r = 0; r < rows; r++) { 47 | for (let c = 0; c < columns; c++) { 48 | const gridIndex = (r * columns) + c; 49 | if (l[gridIndex] !== 0) { 50 | layer.push( 51 |
55 | {this.props.renderTile( 56 | this.getTileData(r, c, l[gridIndex]), 57 | this.props.src, 58 | this.getImageStyles(l[gridIndex]), 59 | )} 60 |
61 | ); 62 | } 63 | } 64 | } 65 | mappedLayers.push(layer); 66 | }); 67 | 68 | return mappedLayers; 69 | } 70 | 71 | getTileData(row, column, index) { 72 | const { tileSize } = this.props; 73 | 74 | const size = tileSize; 75 | const left = column * size; 76 | const top = row * size; 77 | 78 | return { 79 | index, 80 | size: tileSize, 81 | left, 82 | top, 83 | }; 84 | } 85 | 86 | getImageStyles(imageIndex) { 87 | const { scale } = this.context; 88 | const { tileSize } = this.props; 89 | 90 | const size = Math.round(scale * tileSize); 91 | const left = (imageIndex - 1) * size; 92 | 93 | return { 94 | position: 'absolute', 95 | imageRendering: 'pixelated', 96 | display: 'block', 97 | height: '100%', 98 | transform: `translate(-${left}px, 0px)`, 99 | }; 100 | } 101 | 102 | getImageWrapperStyles(row, column) { 103 | const { scale } = this.context; 104 | const { tileSize } = this.props; 105 | 106 | const size = Math.round(scale * tileSize); 107 | const left = column * size; 108 | const top = row * size; 109 | 110 | return { 111 | height: size, 112 | width: size, 113 | overflow: 'hidden', 114 | position: 'absolute', 115 | transform: `translate(${left}px, ${top}px)`, 116 | }; 117 | } 118 | 119 | getLayerStyles() { 120 | return { 121 | position: 'absolute', 122 | top: 0, 123 | left: 0, 124 | }; 125 | } 126 | 127 | getWrapperStyles() { 128 | return { 129 | position: 'absolute', 130 | top: 0, 131 | left: 0, 132 | }; 133 | } 134 | 135 | render() { 136 | const layers = this.generateMap(); 137 | return ( 138 |
139 | { layers.map((layer, index) => { 140 | return ( 141 |
142 | {layer} 143 |
144 | ); 145 | })} 146 |
147 | ); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/components/world.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Matter, { Engine, Events } from 'matter-js'; 4 | 5 | export default class World extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.any, 9 | gravity: PropTypes.shape({ 10 | x: PropTypes.number, 11 | y: PropTypes.number, 12 | scale: PropTypes.number, 13 | }), 14 | onCollision: PropTypes.func, 15 | onInit: PropTypes.func, 16 | onUpdate: PropTypes.func, 17 | }; 18 | 19 | static defaultProps = { 20 | gravity: { 21 | x: 0, 22 | y: 1, 23 | scale: 0.001, 24 | }, 25 | onCollision: () => {}, 26 | onInit: () => {}, 27 | onUpdate: () => {}, 28 | }; 29 | 30 | static contextTypes = { 31 | scale: PropTypes.number, 32 | loop: PropTypes.object, 33 | }; 34 | 35 | static childContextTypes = { 36 | engine: PropTypes.object, 37 | }; 38 | 39 | loop = () => { 40 | const currTime = 0.001 * Date.now(); 41 | Engine.update(this.engine, 1000 / 60, this.lastTime ? currTime / this.lastTime : 1); 42 | this.lastTime = currTime; 43 | }; 44 | 45 | constructor(props) { 46 | super(props); 47 | 48 | this.loopID = null; 49 | this.lastTime = null; 50 | 51 | const world = Matter.World.create({ gravity: props.gravity }); 52 | 53 | this.engine = Engine.create({ 54 | world, 55 | }); 56 | } 57 | 58 | componentWillReceiveProps(nextProps) { 59 | const { gravity } = nextProps; 60 | 61 | if (gravity !== this.props.gravity) { 62 | this.engine.world.gravity = gravity; 63 | } 64 | } 65 | 66 | componentDidMount() { 67 | this.loopID = this.context.loop.subscribe(this.loop); 68 | this.props.onInit(this.engine); 69 | Events.on(this.engine, 'afterUpdate', this.props.onUpdate); 70 | Events.on(this.engine, 'collisionStart', this.props.onCollision); 71 | } 72 | 73 | componentWillUnmount() { 74 | this.context.loop.unsubscribe(this.loopID); 75 | Events.off(this.engine, 'afterUpdate', this.props.onUpdate); 76 | Events.off(this.engine, 'collisionStart', this.props.onCollision); 77 | } 78 | 79 | getChildContext() { 80 | return { 81 | engine: this.engine, 82 | }; 83 | } 84 | 85 | render() { 86 | const defaultStyles = { 87 | position: 'absolute', 88 | top: 0, 89 | left: 0, 90 | height: '100%', 91 | width: '100%', 92 | }; 93 | 94 | return ( 95 |
96 | {this.props.children} 97 |
98 | ); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import AudioPlayer from './utils/audio-player.js'; 2 | import Body from './components/body.js'; 3 | import Loop from './components/loop.js'; 4 | import KeyListener from './utils/key-listener.js'; 5 | import Sprite from './components/sprite.js'; 6 | import Stage from './components/stage.js'; 7 | import TileMap from './components/tile-map.js'; 8 | import World from './components/world.js'; 9 | 10 | export { 11 | AudioPlayer, 12 | Body, 13 | Loop, 14 | KeyListener, 15 | Sprite, 16 | Stage, 17 | TileMap, 18 | World, 19 | }; 20 | -------------------------------------------------------------------------------- /src/native/components/body.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | 3 | import Matter, { World, Bodies } from 'matter-js'; 4 | 5 | export default class Body extends Component { 6 | 7 | static propTypes = { 8 | angle: PropTypes.number, 9 | area: PropTypes.string, 10 | args: PropTypes.array, 11 | axes: PropTypes.shape({ 12 | x: PropTypes.number, 13 | y: PropTypes.number, 14 | }), 15 | bounds: PropTypes.shape({ 16 | min: PropTypes.shape({ 17 | x: PropTypes.number, 18 | y: PropTypes.number, 19 | }), 20 | max: PropTypes.shape({ 21 | x: PropTypes.number, 22 | y: PropTypes.number, 23 | }), 24 | }), 25 | children: PropTypes.any, 26 | collisionFilter: PropTypes.shape({ 27 | category: PropTypes.number, 28 | group: PropTypes.number, 29 | mask: PropTypes.number, 30 | }), 31 | density: PropTypes.number, 32 | force: PropTypes.shape({ 33 | x: PropTypes.number, 34 | y: PropTypes.number, 35 | }), 36 | friction: PropTypes.number, 37 | frictionAir: PropTypes.number, 38 | frictionStatic: PropTypes.number, 39 | id: PropTypes.number, 40 | inertia: PropTypes.number, 41 | inverseInertia: PropTypes.number, 42 | inverseMass: PropTypes.number, 43 | isSensor: PropTypes.bool, 44 | isSleeping: PropTypes.bool, 45 | isStatic: PropTypes.bool, 46 | label: PropTypes.string, 47 | mass: PropTypes.number, 48 | position: PropTypes.shape({ 49 | x: PropTypes.number, 50 | y: PropTypes.number, 51 | }), 52 | restitution: PropTypes.number, 53 | shape: PropTypes.string, 54 | sleepThreshold: PropTypes.number, 55 | slop: PropTypes.number, 56 | slope: PropTypes.number, 57 | timeScale: PropTypes.number, 58 | torque: PropTypes.number, 59 | vertices: PropTypes.array, 60 | }; 61 | 62 | static defaultProps = { 63 | args: [0, 0, 100, 100], 64 | restitution: 0, 65 | friction: 1, 66 | frictionStatic: 0, 67 | shape: 'rectangle', 68 | }; 69 | 70 | static contextTypes = { 71 | engine: PropTypes.object, 72 | }; 73 | 74 | static childContextTypes = { 75 | body: PropTypes.object, 76 | }; 77 | 78 | constructor(props, context) { 79 | super(props); 80 | 81 | const { args, children, shape, ...options } = props; 82 | 83 | this.body = Bodies[shape](...args, options); 84 | World.addBody(context.engine.world, this.body); 85 | } 86 | 87 | componentWillReceiveProps(nextProps) { 88 | const { args, children, shape, ...options } = nextProps; 89 | 90 | Object.keys(options).forEach((option) => { 91 | if (option in this.body && this.props[option] !== nextProps[option]) { 92 | Matter.Body.set(this.body, option, options[option]); 93 | } 94 | }); 95 | } 96 | 97 | componentWillUnmount() { 98 | World.remove(this.context.engine.world, this.body); 99 | } 100 | 101 | getChildContext() { 102 | return { 103 | body: this.body, 104 | }; 105 | } 106 | 107 | render() { 108 | return this.props.children; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/native/components/loop.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { 4 | View, 5 | } from 'react-native'; 6 | 7 | import GameLoop from '../utils/game-loop'; 8 | 9 | export default class Loop extends Component { 10 | 11 | static propTypes = { 12 | children: PropTypes.any, 13 | style: PropTypes.object, 14 | }; 15 | 16 | static childContextTypes = { 17 | loop: PropTypes.object, 18 | }; 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.loop = new GameLoop(); 24 | } 25 | 26 | componentDidMount() { 27 | this.loop.start(); 28 | } 29 | 30 | componentWillUnmount() { 31 | this.loop.stop(); 32 | } 33 | 34 | getChildContext() { 35 | return { 36 | loop: this.loop, 37 | }; 38 | } 39 | 40 | render() { 41 | const defaultStyles = { 42 | flex: 1 43 | }; 44 | const styles = { ...defaultStyles, ...this.props.style }; 45 | return ( 46 | 47 | {this.props.children} 48 | 49 | ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/native/components/sprite.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { View, Image } from 'react-native'; 4 | 5 | export default class Sprite extends Component { 6 | 7 | static propTypes = { 8 | offset: PropTypes.array, 9 | onPlayStateChanged: PropTypes.func, 10 | repeat: PropTypes.bool, 11 | scale: PropTypes.number, 12 | src: PropTypes.number, 13 | state: PropTypes.number, 14 | steps: PropTypes.array, 15 | style: PropTypes.object, 16 | ticksPerFrame: PropTypes.number, 17 | tileHeight: PropTypes.number, 18 | tileWidth: PropTypes.number, 19 | }; 20 | 21 | static defaultProps = { 22 | offset: [0, 0], 23 | onPlayStateChanged: () => {}, 24 | repeat: true, 25 | src: '', 26 | state: 0, 27 | steps: [], 28 | ticksPerFrame: 4, 29 | tileHeight: 64, 30 | tileWidth: 64, 31 | }; 32 | 33 | static contextTypes = { 34 | loop: PropTypes.object, 35 | scale: PropTypes.number, 36 | }; 37 | 38 | constructor(props) { 39 | super(props); 40 | 41 | this.loopID = null; 42 | this.tickCount = 0; 43 | this.finished = false; 44 | 45 | this.state = { 46 | currentStep: 0, 47 | }; 48 | } 49 | 50 | componentDidMount() { 51 | this.props.onPlayStateChanged(1); 52 | const animate = this.animate.bind(this, this.props); 53 | this.loopID = this.context.loop.subscribe(animate); 54 | } 55 | 56 | componentWillReceiveProps(nextProps) { 57 | if (nextProps.state !== this.props.state) { 58 | this.finished = false; 59 | this.props.onPlayStateChanged(1); 60 | this.context.loop.unsubscribe(this.loopID); 61 | this.tickCount = 0; 62 | 63 | this.setState({ 64 | currentStep: 0, 65 | }, () => { 66 | const animate = this.animate.bind(this, nextProps); 67 | this.loopID = this.context.loop.subscribe(animate); 68 | }); 69 | } 70 | } 71 | 72 | componentWillUnmount() { 73 | this.context.loop.unsubscribe(this.loopID); 74 | } 75 | 76 | animate(props) { 77 | const { repeat, ticksPerFrame, state, steps } = props; 78 | 79 | if (this.tickCount === ticksPerFrame && !this.finished) { 80 | if (steps[state] !== 0) { 81 | const { currentStep } = this.state; 82 | const lastStep = steps[state]; 83 | const nextStep = currentStep === lastStep ? 0 : currentStep + 1; 84 | 85 | this.setState({ 86 | currentStep: nextStep, 87 | }); 88 | 89 | if (currentStep === lastStep && repeat === false) { 90 | this.finished = true; 91 | this.props.onPlayStateChanged(0); 92 | } 93 | } 94 | 95 | this.tickCount = 0; 96 | } else { 97 | this.tickCount++; 98 | } 99 | 100 | } 101 | 102 | getImageStyles() { 103 | const { currentStep } = this.state; 104 | const { state, tileWidth, tileHeight } = this.props; 105 | 106 | const left = this.props.offset[0] + (currentStep * tileWidth); 107 | const top = this.props.offset[1] + (state * tileHeight); 108 | 109 | return { 110 | position: 'absolute', 111 | transform: [ 112 | { translateX: left * -1 }, 113 | { translateY: top * -1 } 114 | ] 115 | }; 116 | } 117 | 118 | getWrapperStyles() { 119 | const scale = this.props.scale || this.context.scale; 120 | return { 121 | height: this.props.tileHeight, 122 | width: this.props.tileWidth, 123 | overflow: 'hidden', 124 | position: 'relative', 125 | transform: [{scale: scale}] 126 | }; 127 | } 128 | 129 | render() { 130 | return ( 131 | 132 | 136 | 137 | ); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/native/components/stage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { View, Dimensions } from 'react-native'; 4 | 5 | export default class Stage extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.any, 9 | height: PropTypes.number, 10 | style: PropTypes.object, 11 | width: PropTypes.number, 12 | }; 13 | 14 | static defaultProps = { 15 | width: 1024, 16 | height: 576, 17 | }; 18 | 19 | static contextTypes = { 20 | loop: PropTypes.object, 21 | } 22 | 23 | static childContextTypes = { 24 | loop: PropTypes.object, 25 | scale: PropTypes.number, 26 | }; 27 | 28 | constructor(props) { 29 | super(props); 30 | 31 | const { height, width } = Dimensions.get('window'); 32 | 33 | this.state = { 34 | dimensions: [height, width ], 35 | }; 36 | } 37 | 38 | getChildContext() { 39 | return { 40 | scale: this.getScale().scale, 41 | loop: this.context.loop, 42 | }; 43 | } 44 | 45 | getScale() { 46 | const [vheight, vwidth] = this.state.dimensions; 47 | const { height, width } = this.props; 48 | 49 | let targetWidth; 50 | let targetHeight; 51 | let targetScale; 52 | 53 | if (height / width > vheight / vwidth) { 54 | targetHeight = vheight; 55 | targetWidth = targetHeight * width / height; 56 | targetScale = vheight / height; 57 | } else { 58 | targetWidth = vwidth; 59 | targetHeight = targetWidth * height / width; 60 | targetScale = vwidth / width; 61 | } 62 | 63 | return { 64 | height: targetHeight, 65 | width: targetWidth, 66 | scale: targetScale, 67 | }; 68 | } 69 | 70 | getWrapperStyles() { 71 | return { 72 | flex: 1 73 | }; 74 | } 75 | 76 | getInnerStyles() { 77 | const scale = this.getScale(); 78 | const xOffset = Math.floor((this.state.dimensions[1] - scale.width) / 2); 79 | const yOffset = Math.floor((this.state.dimensions[0] - scale.height) / 2); 80 | 81 | return { 82 | height: Math.floor(scale.height), 83 | width: Math.floor(scale.width), 84 | position: 'absolute', 85 | overflow: 'hidden', 86 | left: xOffset, 87 | top: yOffset, 88 | }; 89 | } 90 | 91 | render() { 92 | return ( 93 | 94 | 95 | {this.props.children} 96 | 97 | 98 | ); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/native/components/tile-map.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { View, Image } from 'react-native'; 4 | 5 | export default class TileMap extends Component { 6 | 7 | static propTypes = { 8 | columns: PropTypes.number, 9 | layers: PropTypes.array, 10 | sourceWidth: PropTypes.number.isRequired, 11 | renderTile: PropTypes.func, 12 | rows: PropTypes.number, 13 | scale: PropTypes.number, 14 | src: PropTypes.number, 15 | style: PropTypes.object, 16 | tileSize: PropTypes.number, 17 | }; 18 | 19 | static defaultProps = { 20 | columns: 16, 21 | layers: [], 22 | renderTile: (tile, src, styles) => ( 23 | 28 | ), 29 | rows: 9, 30 | src: '', 31 | tileSize: 64, 32 | }; 33 | 34 | static contextTypes = { 35 | scale: PropTypes.number, 36 | }; 37 | 38 | shouldComponentUpdate(nextProps, nextState, nextContext) { 39 | return this.context.scale !== nextContext.scale; 40 | } 41 | 42 | generateMap() { 43 | const { columns, layers, rows } = this.props; 44 | 45 | const mappedLayers = []; 46 | 47 | layers.forEach((l, index) => { 48 | const layer = []; 49 | for (let r = 0; r < rows; r++) { 50 | for (let c = 0; c < columns; c++) { 51 | const gridIndex = (r * columns) + c; 52 | if (l[gridIndex] !== 0) { 53 | layer.push( 54 | 58 | {this.props.renderTile( 59 | this.getTileData(r, c, l[gridIndex]), 60 | this.props.src, 61 | this.getImageStyles(l[gridIndex]), 62 | )} 63 | 64 | ); 65 | } 66 | } 67 | } 68 | mappedLayers.push(layer); 69 | }); 70 | 71 | return mappedLayers; 72 | } 73 | 74 | getTileData(row, column, index) { 75 | const { tileSize } = this.props; 76 | 77 | const size = tileSize; 78 | const left = column * size; 79 | const top = row * size; 80 | 81 | return { 82 | index, 83 | size: tileSize, 84 | left, 85 | top, 86 | }; 87 | } 88 | 89 | getImageStyles(imageIndex) { 90 | const { scale } = this.context; 91 | const { tileSize, sourceWidth } = this.props; 92 | 93 | const size = scale * tileSize; 94 | const left = (imageIndex - 1) * size; 95 | 96 | return { 97 | position: 'absolute', 98 | height: size, 99 | width: sourceWidth * scale, 100 | top: 0, 101 | left: left * -1, 102 | }; 103 | } 104 | 105 | getImageWrapperStyles(row, column) { 106 | const { scale } = this.context; 107 | const { tileSize } = this.props; 108 | 109 | const size = scale * tileSize; 110 | const left = column * size; 111 | const top = row * size; 112 | 113 | return { 114 | height: size, 115 | width: size, 116 | overflow: 'hidden', 117 | position: 'absolute', 118 | top, 119 | left: left, 120 | }; 121 | } 122 | 123 | getLayerStyles() { 124 | return { 125 | position: 'absolute', 126 | top: 0, 127 | left: 0, 128 | }; 129 | } 130 | 131 | getWrapperStyles() { 132 | return { 133 | position: 'absolute', 134 | top: 0, 135 | left: 0, 136 | }; 137 | } 138 | 139 | render() { 140 | const layers = this.generateMap(); 141 | return ( 142 | 143 | { layers.map((layer, index) => { 144 | return ( 145 | 146 | {layer} 147 | 148 | ); 149 | })} 150 | 151 | ); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/native/components/world.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { View } from 'react-native'; 4 | 5 | import Matter, { Engine, Events } from 'matter-js'; 6 | 7 | export default class World extends Component { 8 | 9 | static propTypes = { 10 | children: PropTypes.any, 11 | gravity: PropTypes.shape({ 12 | x: PropTypes.number, 13 | y: PropTypes.number, 14 | scale: PropTypes.number, 15 | }), 16 | onCollision: PropTypes.func, 17 | onInit: PropTypes.func, 18 | onUpdate: PropTypes.func, 19 | }; 20 | 21 | static defaultProps = { 22 | gravity: { 23 | x: 0, 24 | y: 1, 25 | scale: 0.001, 26 | }, 27 | onCollision: () => {}, 28 | onInit: () => {}, 29 | onUpdate: () => {}, 30 | }; 31 | 32 | static contextTypes = { 33 | scale: PropTypes.number, 34 | loop: PropTypes.object, 35 | }; 36 | 37 | static childContextTypes = { 38 | engine: PropTypes.object, 39 | }; 40 | 41 | loop = () => { 42 | const currTime = 0.001 * Date.now(); 43 | Engine.update(this.engine, 1000 / 60, this.lastTime ? currTime / this.lastTime : 1); 44 | this.lastTime = currTime; 45 | }; 46 | 47 | constructor(props) { 48 | super(props); 49 | 50 | this.loopID = null; 51 | this.lastTime = null; 52 | 53 | const world = Matter.World.create({ gravity: props.gravity }); 54 | 55 | this.engine = Engine.create({ 56 | world, 57 | }); 58 | } 59 | 60 | componentWillReceiveProps(nextProps) { 61 | const { gravity } = nextProps; 62 | 63 | if (gravity !== this.props.gravity) { 64 | this.engine.world.gravity = gravity; 65 | } 66 | } 67 | 68 | componentDidMount() { 69 | this.loopID = this.context.loop.subscribe(this.loop); 70 | this.props.onInit(this.engine); 71 | Events.on(this.engine, 'afterUpdate', this.props.onUpdate); 72 | Events.on(this.engine, 'collisionStart', this.props.onCollision); 73 | } 74 | 75 | componentWillUnmount() { 76 | this.context.loop.unsubscribe(this.loopID); 77 | Events.off(this.engine, 'afterUpdate', this.props.onUpdate); 78 | Events.off(this.engine, 'collisionStart', this.props.onCollision); 79 | } 80 | 81 | getChildContext() { 82 | return { 83 | engine: this.engine, 84 | }; 85 | } 86 | 87 | render() { 88 | const defaultStyles = { 89 | flex: 1, 90 | }; 91 | 92 | return ( 93 | 94 | {this.props.children} 95 | 96 | ); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/native/utils/game-loop.js: -------------------------------------------------------------------------------- 1 | export default class GameLoop { 2 | loop = () => { 3 | this.subscribers.forEach((callback) => { 4 | callback.call(); 5 | }); 6 | 7 | this.loopID = window.requestAnimationFrame(this.loop); 8 | } 9 | constructor() { 10 | this.subscribers = []; 11 | this.loopID = null; 12 | } 13 | start() { 14 | if (!this.loopID) { 15 | this.loop(); 16 | } 17 | } 18 | stop() { 19 | window.cancelAnimationFrame(this.loop); 20 | } 21 | subscribe(callback) { 22 | return this.subscribers.push(callback); 23 | } 24 | unsubscribe(id) { 25 | delete this.subscribers[id - 1]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/audio-player.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | export default class AudioPlayer { 3 | constructor(url, callback) { 4 | this.url = url || null; 5 | this.callback = callback || function () {}; 6 | 7 | this.buffer = null; 8 | 9 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 10 | this.context = window.context || new AudioContext(); 11 | 12 | this.loadBuffer(); 13 | } 14 | 15 | play = (options) => { 16 | const volume = options && options.volume; 17 | const offset = options && options.offset; 18 | const loop = options && options.loop; 19 | 20 | const source = this.context.createBufferSource(); 21 | const gainNode = this.context.createGain(); 22 | gainNode.gain.value = volume || 0.5; 23 | 24 | gainNode.connect(this.context.destination); 25 | source.connect(gainNode); 26 | 27 | source.buffer = this.buffer; 28 | source.start(offset ? this.context.currentTime + offset : 0); 29 | source.loop = loop || false; 30 | return source.stop.bind(source); 31 | } 32 | 33 | loadBuffer = () => { 34 | const request = new XMLHttpRequest(); 35 | request.open('GET', this.url, true); 36 | request.responseType = 'arraybuffer'; 37 | 38 | request.onload = () => { 39 | this.context.decodeAudioData( 40 | request.response, 41 | (buffer) => { 42 | if (!buffer) { 43 | console.error(`error decoding file data: ${this.url}`); 44 | return; 45 | } 46 | this.buffer = buffer; 47 | this.callback(); 48 | }, 49 | (error) => { 50 | console.error('decodeAudioData error', error); 51 | } 52 | ); 53 | }; 54 | 55 | request.onerror = function onError() { 56 | console.error('BufferLoader: XHR error'); 57 | }; 58 | 59 | request.send(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/game-loop.js: -------------------------------------------------------------------------------- 1 | export default class GameLoop { 2 | loop = () => { 3 | this.subscribers.forEach((callback) => { 4 | callback.call(); 5 | }); 6 | 7 | this.loopID = window.requestAnimationFrame(this.loop); 8 | } 9 | constructor() { 10 | this.subscribers = []; 11 | this.loopID = null; 12 | } 13 | start() { 14 | if (!this.loopID) { 15 | this.loop(); 16 | } 17 | } 18 | stop() { 19 | window.cancelAnimationFrame(this.loopID); 20 | } 21 | subscribe(callback) { 22 | return this.subscribers.push(callback); 23 | } 24 | unsubscribe(id) { 25 | delete this.subscribers[id - 1]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/key-listener.js: -------------------------------------------------------------------------------- 1 | export default class KeyListener { 2 | 3 | LEFT = 37; 4 | RIGHT = 39; 5 | UP = 38; 6 | DOWN = 40; 7 | SPACE = 32; 8 | 9 | down = (event) => { 10 | if (event.keyCode in this.keys) { 11 | event.preventDefault(); 12 | this.keys[event.keyCode] = true; 13 | } 14 | }; 15 | 16 | up = (event) => { 17 | if (event.keyCode in this.keys) { 18 | event.preventDefault(); 19 | this.keys[event.keyCode] = false; 20 | } 21 | }; 22 | 23 | isDown = (keyCode) => { 24 | return this.keys[keyCode] || false; 25 | } 26 | 27 | subscribe = (keys) => { 28 | window.addEventListener('keydown', this.down); 29 | window.addEventListener('keyup', this.up); 30 | 31 | keys.forEach((key) => { 32 | this.keys[key] = false; 33 | }); 34 | } 35 | 36 | unsubscribe = () => { 37 | window.removeEventListener('keydown', this.down); 38 | window.removeEventListener('keyup', this.up); 39 | this.keys = {}; 40 | } 41 | 42 | constructor() { 43 | this.keys = {}; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | // 'react-hot-loader/patch', 7 | './demo/index', 8 | ], 9 | output: { 10 | path: __dirname, 11 | filename: 'bundle.js', 12 | publicPath: '/', 13 | }, 14 | resolve: { 15 | alias: { 16 | 'react': 'preact-compat', 17 | 'react-dom': 'preact-compat', 18 | }, 19 | }, 20 | plugins: [ 21 | new webpack.NoErrorsPlugin(), 22 | new webpack.DefinePlugin({ 23 | 'process.env': { 24 | NODE_ENV: JSON.stringify('production'), 25 | }, 26 | }), 27 | ], 28 | module: { 29 | loaders: [{ 30 | test: /\.js$/, 31 | loaders: ['babel'], 32 | include: [ 33 | path.join(__dirname, 'src'), 34 | path.join(__dirname, 'demo'), 35 | ], 36 | }, { 37 | test: /\.json$/, 38 | loaders: ['json'], 39 | }, { 40 | test: /\.css$/, 41 | include: [ 42 | path.join(__dirname, 'src'), 43 | path.join(__dirname, 'demo'), 44 | ], 45 | loader: 'style!css!postcss', 46 | }], 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | './demo/index', 7 | ], 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'bundle.js', 11 | publicPath: '/dist/', 12 | }, 13 | resolve: { 14 | alias: { 15 | 'react': 'preact-compat', 16 | 'react-dom': 'preact-compat' 17 | } 18 | }, 19 | plugins: [ 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | NODE_ENV: JSON.stringify('production'), 24 | }, 25 | }), 26 | ], 27 | module: { 28 | loaders: [{ 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | loader: 'babel', 32 | }, { 33 | test: /\.css$/, 34 | loader: 'style!css!postcss', 35 | }], 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.config.umd.js: -------------------------------------------------------------------------------- 1 | /* globals __dirname */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.join(__dirname, 'src/index.js'), 7 | externals: [ 8 | 'preact', 9 | 'preact-compat' 10 | ], 11 | resolve: { 12 | alias: { 13 | 'react': 'preact-compat', 14 | 'react-dom': 'preact-compat' 15 | } 16 | }, 17 | output: { 18 | library: 'ReactGameKit', 19 | libraryTarget: 'umd', 20 | filename: 'react-game-kit.min.js', 21 | path: path.join(__dirname, 'umd'), 22 | }, 23 | module: { 24 | loaders: [ 25 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 26 | ], 27 | }, 28 | plugins: [ 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.UglifyJsPlugin({ 31 | compress: { 32 | warnings: false, 33 | }, 34 | }), 35 | new webpack.DefinePlugin({ 36 | 'process.env.NODE_ENV': JSON.stringify('production'), 37 | }), 38 | new webpack.SourceMapDevToolPlugin('[file].map'), 39 | ], 40 | }; 41 | --------------------------------------------------------------------------------