EDIT: I have now isolated the problem into this little gist and this jsfiddle.
I would like to use Redux for state management in a three.js-based real-time 3D game. I've created a small prototype for testing purposes, but even this very simple toy application shows some serious performance issues.
My idea was to build a render loop that dispatches an UPDATE action every frame, using the requestAnimationFrame() callback:
index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import * as THREE from 'three'
import rootReducer from './reducers/rootReducer'
import App from './components/App'
import { getThreeInitialState, getThreeRenderer } from './threeApp/threeApp'
const threeInitialState = getThreeInitialState();
const initialState = {
running: false,
...threeInitialState
}
const store = createStore(rootReducer, initialState)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
const threeRenderer = getThreeRenderer()
const updateThreeApp = () => {
threeRenderer.render(store.getState().scene, store.getState().camera)
const timestamp = Date.now()
requestAnimationFrame(() => store.dispatch({ type: 'UPDATE', timestamp: timestamp }))
}
store.subscribe(updateThreeApp)
store.dispatch({ type: 'UPDATE' })
document.addEventListener('click', () => store.dispatch({ type: 'CHANGE_MATERIAL' }));
The event listener in the last line dispatches an event that changes the material of the cube which gets rendered for testig purposes. Of course in the game there will be some more listeners for processing keyboard and mouse events.
My reducer looks like this:
rootReducer.js
const changeMaterial = (state) => {
const newState = { ...state }
newState.scene.getObjectByName('box').material.wireframe = !newState.scene.getObjectByName('box').material.wireframe
return newState
}
const rotate = (state) => {
const newState = { ...state }
newState.scene.getObjectByName('box').position.y = 2 * Math.sin(Date.now() / 1000)
return newState
}
const rootReducer = (state, action) => {
switch (action.type) {
case 'CHANGE_MATERIAL':
return state.running ? changeMaterial(state) : state
case 'RUN':
return {
...state,
running: true,
}
case 'UPDATE':
return rotate(state)
default:
return state
}
}
export default rootReducer;
The two functions on the top are used for changing the material and position of the rendered cube.
Updating the three.js scene every frame and moving the cube works perfectly fine. However, every time I fire the click event to change the material, the framerate goes down a bit and stays like this. So after randomly clicking for a few seconds it goes from 60 FPS down to about 20 FPS and doesn't go up anymore, although the action is finished. Looking at the Chrome performance profile, there is a significant increase in CPU usage every time the click event get's fired. CPU usage also stays on this higher level after the event.
I also use ReactDOM.render() for rendering things like the starting page, loading screens etc. but I would like to keep the three.js render loop outside of the React part to separate things and to avoid unneccesary DOM updates. The only connection between the three.js-part and React is that the container DIV for the three.js renderer is inside one of the React components.
The full code is uploaded in this repository and I have also created a small gist showing how my redux rendering loop is supposed to work.
This is the first time I am using Redux and React so I probably just overlooked something. Or is it a rather bad Idea to use this kind of architecture for a 3D game? Any help would be very appreciated!
newState.scene.getObjectByName('box').material.wireframe = !newState.scene.getObjectByName('box').material.wireframea good thing? I know redux likes diffing stuff and all that, but searching the entire scene graph on both sides of this comparison, and every frame seems pretty intense?wireframe: trueorwireframe: falseinside the state tree and then apply the state or state changes to the scene graph every update. I'm thinking about something likemapStateToProps()in react-redux, I'm just not sure yet how to solve this...