Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a gameloop with requestAnimationFrame across multiple React Redux components?

Struggling to think of the best way to go about it. I can use a recursive call with requestAnimationFrame to have a game loop:

export interface Props {
    name: string;
    points: number;
    onIncrement?: () => void;
    onDecrement?: () => void;
}

class Hello extends React.Component<Props, object> {

    constructor(props: Props) {
        super(props);
    }

    render() {
        const { name, points, onIncrement, onDecrement } = this.props;

        return (
            <div className="hello">
                <div className="greeting">
                    Hello {name + points}
                </div>
                <button onClick={onDecrement}>-</button>
                <button onClick={onIncrement}>+</button>
            </div>
        );
    }

    componentDidMount() {
        this.tick();
    }

    tick = () => {
        this.props.onIncrement();
        requestAnimationFrame(this.tick)
    }

}

But what if I want on each frame:

  • Component1 to do X
  • Component2 to do Y
  • Component3 to do Z

I could just have another loop in each component, however my understanding is it is bad practice to have multiple requestAnimationFrame loops going and it is a significant performance hit.

So I'm at a lost here. How can I have another component use the same loop? (If that even is the best way to go about it!)

like image 1000
AlwaysNeedingHelp Avatar asked Jan 06 '19 23:01

AlwaysNeedingHelp


2 Answers

You need a parent component that calls requestAnimationFrame and iterates over an array of refs of the children components that need to be updated on every cycle, calling its update (or however you want to call it) method:

class ProgressBar extends React.Component {

  constructor(props) {
    super(props);
    
    this.state = {
      progress: 0,
    };
  }
  
  update() {
    this.setState((state) => ({
      progress: (state.progress + 0.5) % 100,
    }));
  }  

  render() {
    const { color } = this.props;
    const { progress } = this.state;
    
    const style = {
      background: color,
      width: `${ progress }%`,
    };
    
    return(
      <div className="progressBarWrapper">
        <div className="progressBarProgress" style={ style }></div>
      </div>
    );  
  }
}

class Main extends React.Component {

  constructor(props) {
    super(props);
    
    const progress1 = this.progress1 = React.createRef();
    const progress2 = this.progress2 = React.createRef();
    const progress3 = this.progress3 = React.createRef();
    
    this.componentsToUpdate = [progress1, progress2, progress3];
    this.animationID = null;    
  }
  
  componentDidMount() {  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  componentWillUnmount() {
    window.cancelAnimationFrame(this.animationID);
  }
  
  update() {
    this.componentsToUpdate.map(component => component.current.update());
  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  render() {
    return(
      <div>
        <ProgressBar ref={ this.progress1 } color="magenta" />
        <ProgressBar ref={ this.progress2 } color="blue" />     
        <ProgressBar ref={ this.progress3 } color="yellow" />       
      </div>
    );
  }
}

ReactDOM.render(<Main />, document.getElementById('app'));
body {
  margin: 0;
  padding: 16px;
}

.progressBarWrapper {
  position: relative;
  width: 100%;
  border: 3px solid black;
  height: 32px;
  box-sizing: border-box;
  margin-bottom: 16px;
}

.progressBarProgress {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="app"></div>

Keep in mind, though, that if you are trying to do something too complex and want to hit 60 fps, React might not be the right tool to use.

Also, setState is asynchronous, so when calling it you are just pushing an update to a queue that React will process at some point, which might actually happen in the next frame or later.

I profiled this simple example to see if that was the case and it's not, actually. The updates are added to the queue (enqueueSetState), but the work is done straight away:

Example app profiling result

However, I suspect that in a real application where React has more updates to handle or in future versions of React with features like time slicing, async updates with priorities... the rendering might actually happen in a different frame.

like image 141
Danziger Avatar answered Nov 12 '22 15:11

Danziger


One solution would be to define an array such as callbacks as part of your state. At the beginning of each component's lifecycle, add a function to this array which does what you want each loop. Then call each function in your rAF loop like this:

update( performance.now())

// Update loop
function update( timestamp ) {
    // Execute callback
    state.callbacks.forEach( cb => cb( ...args )) // Pass frame delta, etc.

    requestAnimationFrame( update )
  }

With some work you can adjust this simple example to provide a way to remove functions from callbacks to allow dynamic adding/subtracting routines from the game loop by name or signature.

You could also pass an object wrapping the function which also contains an integer that you could use to sort the callbacks by priority.

like image 1
soulofmischief Avatar answered Nov 12 '22 13:11

soulofmischief