Skip to content

Instantly share code, notes, and snippets.

@rossimo
Last active October 29, 2017 10:05
Show Gist options
  • Save rossimo/2e6b5d345d2cdd549eff9a3524bc32fe to your computer and use it in GitHub Desktop.
Save rossimo/2e6b5d345d2cdd549eff9a3524bc32fe to your computer and use it in GitHub Desktop.
Design for React Games
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;
}
}
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);
}
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));
@rossimo
Copy link
Author

rossimo commented Sep 27, 2017

@rossimo/react-pixi is a fork of react-pixi that allows control of when the pixi.js stage re-renders.

@rossimo
Copy link
Author

rossimo commented Sep 27, 2017

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