Last active
October 29, 2017 10:05
-
-
Save rossimo/2e6b5d345d2cdd549eff9a3524bc32fe 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)); |
To animate something, you would fire off a saga, such as:
sagas.run(function*() {
yield call(tweenNumberSaga, 0, 360, 5000, tween.linear, function*(value) {
yield put(updateSprite('ship', {rotation: value})
})
}
This would rotation the ship 360 degrees over 5 seconds.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@rossimo/react-pixi
is a fork ofreact-pixi
that allows control of when the pixi.js stage re-renders.