-
-
Save nastanford/34260fbc29e822ca2fa43c050f960be5 to your computer and use it in GitHub Desktop.
Design for React Games
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Rx from 'rxjs/Rx'; | |
import * as tween from 'tween-functions' | |
import * as update from 'immutability-helper' | |
import { call, take, Func1, CallEffectFn } from 'redux-saga/effects' | |
import { eventChannel, END } from 'redux-saga' | |
import * as _ from 'lodash' | |
export let tick = new Rx.Subject(); | |
export let draw = new Rx.Subject<State>(); | |
Rx.Observable.of(0, Rx.Scheduler.animationFrame).repeat().subscribe(() => { | |
tick.next(); | |
draw.next(); | |
}) | |
export let tweenNumbersSaga = function* (start: number[], end: number[], durationMs: number, easings: Function[], callback: CallEffectFn<Func1<number[]>>) { | |
let channel = eventChannel(emitter => { | |
let values = easings.map((easing, i) => value => easing(value, start[i], end[i], 1)) | |
let startTime = Date.now() | |
let subscription = tick | |
.takeUntil(Rx.Observable.timer(durationMs)) | |
.subscribe(() => emitter(values.map(value => value((Date.now() - startTime) / durationMs)))) | |
subscription.add(() => emitter(END)) | |
return () => subscription.unsubscribe() | |
}) | |
while (true) { | |
let values = yield take(channel); | |
if (values == END) break; | |
yield call(callback, values) | |
} | |
} | |
export let tweenNumberSaga = function* (start: number, end: number, duration: number, easing: Function, callback: CallEffectFn<Func1<number>>) { | |
yield call(tweenNumbersSaga, [start], [end], duration, [easing], function* ([value]) { | |
yield call(callback, value) | |
}) | |
} | |
export function* takeOneAtATime(type: string, saga: CallEffectFn<any>) { | |
while (true) { | |
let { payload } = yield take(type); | |
yield call(saga, payload); | |
} | |
} | |
export interface Sprite { | |
x?: number | |
y?: number | |
image?: string | |
alpha?: number | |
rotation?: number | |
scale?: number | |
offsetX?: number | |
offsetY?: number | |
} | |
export interface State { | |
sprites: { [id: string]: Sprite } | |
} | |
export let initialState: State = { | |
sprites: {} | |
} | |
export let updateSprite = (id: string, update: Sprite) => ({ | |
type: 'UPDATE_SPRITE', | |
payload: { id, update } | |
}) | |
export let removeSprite = (id: string) => ({ | |
type: 'REMOVE_SPRITE', | |
payload: { id } | |
}) | |
export let reducer = (state: State = initialState, { type, payload }): State => { | |
switch (type) { | |
case 'REMOVE_SPRITE': { | |
let { id } = payload; | |
return update(state, { sprites: { $unset: [id] } }) | |
} | |
case 'UPDATE_SPRITE': { | |
let sprites = {}, { id } = payload | |
sprites[id] = update(state.sprites[id] || {}, { $merge: payload.update }) | |
return update(state, { sprites: { $merge: sprites } }) | |
} | |
default: | |
return state; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react' | |
import * as PIXI from 'pixi.js' | |
import * as ReactPIXI from '@rossimo/react-pixi' | |
import { Store } from 'redux' | |
import * as update from 'immutability-helper' | |
import * as _ from 'lodash' | |
import { store, State, draw, updateSprite } from './store' | |
interface In { | |
store: Store<State> | |
} | |
class Game extends React.Component<In, State> { | |
private stage: ReactPIXI.Stage; | |
constructor(props) { | |
super(props) | |
this.state = store.getState() | |
} | |
componentDidMount() { | |
let { store } = this.props; | |
let latest: State; | |
draw.map(() => store.getState()).subscribe(state => { | |
if (latest != state) { | |
latest = state | |
this.setState(update(this.state, { $merge: latest })) | |
} else { | |
(this.stage as any).renderStage() | |
} | |
}) | |
} | |
render() { | |
let { sprites } = this.state; | |
return <ReactPIXI.Stage ref={ref => this.stage = ref} | |
width={800} height={600} | |
backgroundColor={0} | |
disableAutoRender={true}> | |
{_.toPairs(sprites).map(([id, sprite], index) => | |
<ReactPIXI.Sprite key={id} | |
image={`/resources/${sprite.image}.png`} | |
x={sprite.x + _.get(sprite, 'offsetX', 0)} | |
y={sprite.y + _.get(sprite, 'offsetY', 0)} | |
alpha={_.get(sprite, 'alpha', 1)} | |
scale={_.get(sprite, 'scale', 1)} /> | |
)} | |
</ReactPIXI.Stage> | |
} | |
} | |
window.onload = () => { | |
store.dispatch(updateSprite('ship', { | |
image: 'ship', x: 0, y: 0 | |
})) | |
let root = document.getElementById('root'); | |
ReactPIXI.render(<Game store={store} />, root); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as _ from 'lodash' | |
import { createStore, applyMiddleware, compose, Store, Action } from 'redux' | |
import createSagaMiddleware from 'redux-saga' | |
import { reducer, State, Sprite, initialState } from './animate' | |
export * from './animate' | |
let sagas = createSagaMiddleware() | |
export let store = createStore<State>(reducer, applyMiddleware(sagas)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment