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 |
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 |
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 |
--------------------------------------------------------------------------------