├── public ├── favicon.ico ├── sprite │ ├── kp_run.png │ ├── kp_out_of_breath.png │ ├── kp_run.json │ └── kp_out_of_breath.json ├── manifest.json └── index.html ├── README.md ├── src ├── pixi │ ├── constants.js │ ├── loader.js │ ├── state.js │ ├── Render.js │ └── Controller.js ├── index.js ├── index.css └── App.js ├── .gitignore ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── scripts ├── test.js ├── start.js └── build.js └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiunhau/rx-pixi/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #rx-pixi 2 | 3 | A game engine experiment with Rxjs as controller, Pixijs as renderer. 4 | -------------------------------------------------------------------------------- /public/sprite/kp_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiunhau/rx-pixi/HEAD/public/sprite/kp_run.png -------------------------------------------------------------------------------- /src/pixi/constants.js: -------------------------------------------------------------------------------- 1 | export const KEYCODE = { 2 | left: 37, 3 | up: 38, 4 | right: 39, 5 | down: 40 6 | } 7 | -------------------------------------------------------------------------------- /public/sprite/kp_out_of_breath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiunhau/rx-pixi/HEAD/public/sprite/kp_out_of_breath.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/pixi/loader.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | const loader = PIXI.loader; 4 | 5 | loader 6 | .add('/sprite/kp_run.json') 7 | .add('/sprite/kp_out_of_breath.json') 8 | 9 | export default loader; 10 | -------------------------------------------------------------------------------- /src/pixi/state.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | kp: { 3 | x: window.innerWidth / 2, 4 | y: window.innerHeight * 0.8, 5 | vx: 0, 6 | vy: 0, 7 | side: 'RIGHT', 8 | ani: 'STAND' 9 | } 10 | } 11 | 12 | export default state; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | padding: 0; 5 | margin: 0; 6 | overflow: hidden; 7 | } 8 | 9 | canvas { 10 | image-rendering: optimizeSpeed; 11 | image-rendering: -moz-crisp-edges; 12 | image-rendering: -o-crisp-edges; 13 | image-rendering: -webkit-optimize-contrast; 14 | image-rendering: optimize-contrast; 15 | image-rendering: crisp-edges; 16 | image-rendering: pixelated; 17 | -ms-interpolation-mode: nearest-neighbor; 18 | } 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import * as PIXI from 'pixi.js'; 3 | import Controller from './pixi/Controller'; 4 | import loader from './pixi/loader'; 5 | 6 | class App extends Component { 7 | componentDidMount() { 8 | const renderer = new PIXI.autoDetectRenderer({ 9 | width: window.innerWidth, 10 | height: window.innerHeight, 11 | backgroundColor: 0x1c3192, 12 | roundPixels: true 13 | }); 14 | 15 | document.getElementById('pixi').appendChild(renderer.view); 16 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 17 | 18 | loader.load(() => { 19 | new Controller(renderer) 20 | }) 21 | } 22 | render() { 23 | return ( 24 |
25 |
26 |
27 | ); 28 | } 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /public/sprite/kp_run.json: -------------------------------------------------------------------------------- 1 | {"frames":{"kp_run0.png":{"frame":{"x":0,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_run1.png":{"frame":{"x":50,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_run2.png":{"frame":{"x":100,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_run3.png":{"frame":{"x":150,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_run4.png":{"frame":{"x":200,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_run5.png":{"frame":{"x":250,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}}},"meta":{"app":"https://github.com/juliandescottes/piskel/","version":"1.0","image":"kp_run.png","format":"RGBA8888","size":{"w":300,"h":50}}} 2 | -------------------------------------------------------------------------------- /public/sprite/kp_out_of_breath.json: -------------------------------------------------------------------------------- 1 | {"frames":{"kp_out_of_breath0.png":{"frame":{"x":0,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath1.png":{"frame":{"x":50,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath2.png":{"frame":{"x":100,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath3.png":{"frame":{"x":150,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath4.png":{"frame":{"x":200,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath5.png":{"frame":{"x":250,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}},"kp_out_of_breath6.png":{"frame":{"x":300,"y":0,"w":50,"h":50},"rotated":false,"trimmed":false,"spriteSourceSize":{"x":0,"y":0,"w":50,"h":50},"sourceSize":{"w":50,"h":50}}},"meta":{"app":"https://github.com/juliandescottes/piskel/","version":"1.0","image":"kp_out_of_breath.png","format":"RGBA8888","size":{"w":350,"h":50}}} 2 | -------------------------------------------------------------------------------- /src/pixi/Render.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | class Render { 4 | constructor(state, renderer) { 5 | this.kp = new PIXI.extras.AnimatedSprite(getAnimatedTextures('kp_out_of_breath', 7)); 6 | this.kp.animationSpeed = 0.18; 7 | this.kp.anis = []; 8 | this.kp.currentAni = 'STAND'; 9 | this.kp.anis['STAND'] = getAnimatedTextures('kp_out_of_breath', 7); 10 | this.kp.anis['RUN'] = getAnimatedTextures('kp_run', 6); 11 | this.kp.anchor.set(0.5, 0.5); 12 | this.kp.scale.set(2); 13 | this.kp.x = state.kp.x; 14 | this.kp.y = state.kp.y; 15 | this.kp.play(); 16 | 17 | this.stage = new PIXI.Container(); 18 | this.stage.addChild(this.kp); 19 | renderer.render(this.stage); 20 | } 21 | 22 | render(state, renderer) { 23 | this.kp.x += state.kp.vx; 24 | this.kp.y += state.kp.vy; 25 | if (state.kp.ani !== this.kp.currentAni) { 26 | this.kp.currentAni = state.kp.ani; 27 | this.kp.textures = this.kp.anis[this.kp.currentAni]; 28 | this.kp.play(); 29 | } 30 | this.kp.scale.x = state.kp.side === 'LEFT' ? -2 : 2; 31 | 32 | renderer.render(this.stage); 33 | } 34 | } 35 | 36 | function getAnimatedTextures(ref, frames) { 37 | let textures = []; 38 | for(let i = 0; i < frames; i ++) { 39 | textures.push(PIXI.Texture.fromFrame(`${ref}${i}.png`)) 40 | } 41 | 42 | return textures; 43 | } 44 | 45 | export default Render; 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/pixi/Controller.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import initalState from './state'; 3 | import Render from './Render'; 4 | import { KEYCODE } from './constants'; 5 | 6 | class Controller { 7 | constructor(renderer) { 8 | let pixiRender = new Render(initalState, renderer) 9 | 10 | const keydown$ = Rx.Observable 11 | .fromEvent(document, 'keydown', e => { 12 | switch (e.keyCode) { 13 | case KEYCODE.left: 14 | return -1 15 | case KEYCODE.right: 16 | return 1 17 | default: 18 | } 19 | }); 20 | 21 | const keyup$ = Rx.Observable 22 | .fromEvent(document, 'keyup', e => { 23 | switch (e.keyCode) { 24 | case KEYCODE.left: 25 | return -1 26 | case KEYCODE.right: 27 | return 1 28 | default: 29 | } 30 | }) 31 | .withLatestFrom(keydown$) 32 | .filter(([keyup, keydown]) => { 33 | return keyup === keydown 34 | }) 35 | .map(x => 0) 36 | 37 | const input$ = Rx.Observable 38 | .merge(keydown$, keyup$) 39 | .startWith(0) 40 | .distinctUntilChanged(); 41 | 42 | const ticker$ = Rx.Observable 43 | .interval(0, Rx.Scheduler.requestAnimationFrame) 44 | 45 | const state$ = ticker$ 46 | .withLatestFrom(input$) 47 | .scan(({kp}, [ticker, input]) => { 48 | switch (input) { 49 | case -1: 50 | kp.vx = -1; 51 | kp.side = 'LEFT'; 52 | kp.ani = 'RUN'; 53 | break; 54 | case 1: 55 | kp.vx = 1; 56 | kp.side = 'RIGHT'; 57 | kp.ani = 'RUN'; 58 | break; 59 | case 0: 60 | kp.vx = 0; 61 | kp.ani = 'STAND'; 62 | break; 63 | default: 64 | } 65 | 66 | return {kp} 67 | }, initalState) 68 | 69 | ticker$ 70 | .withLatestFrom(state$) 71 | .subscribe(([ticker, state]) => { 72 | pixiRender.render(state, renderer) 73 | }) 74 | } 75 | } 76 | 77 | export default Controller; 78 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right