如何在多个React Redux组件中使用requestAnimationFrame实现游戏循环?

7

苦恼于如何最好地处理它。我可以使用requestAnimationFrame的递归调用来实现游戏循环:

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)
    }

}

但是如果我想在每个帧上执行以下操作:

  • Component1 执行 X 操作
  • Component2 执行 Y 操作
  • Component3 执行 Z 操作

我可以在每个组件中添加另一个循环,但是我的理解是同时运行多个 requestAnimationFrame 循环是不好的实践,并且会对性能造成重大影响。

所以我有些困惑。如何使另一个组件使用相同的循环?(如果这甚至是最好的方法!)

3个回答

8
你需要一个父组件调用requestAnimationFrame,并迭代一个refs数组,其中包含需要在每个周期更新的子组件,调用它的update(或者你想如何称呼它)方法:

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>

请记住,如果您试图做一些过于复杂的事情并想达到60fps,React可能不是使用的正确工具。此外,setState是异步的,因此在调用它时,您只是将更新推送到React将在某个时刻处理的队列中,这可能实际上会在下一帧或以后发生。我对此简单的示例进行了分析,以查看是否是这种情况,实际上并不是。更新被添加到队列(enqueueSetState),但工作立即完成。

Example app profiling result

然而,在真正的应用程序中,当React需要处理更多更新或者在未来版本中添加了时间分片、优先级异步更新等功能时,渲染实际上可能会发生在不同的帧中。


1
我同意,由于各种原因,React并不适合游戏开发。我唯一能在游戏开发中使用React的方式是将我的游戏包装在一个单独的组件中,该组件仅渲染一次,并使用单个requestAnimationFrame。在这种情况下,React仅用于在其他视图中导航,而没有游戏循环。 - Mosè Raguzzini
@Danziger 你能看一下这个问题吗?https://dev59.com/e7vpa4cB1Zd3GeqPAumU - Nevin Madhukar K

1
一种解决方案是将类似于callbacks的数组定义为状态的一部分。在每个组件生命周期的开始,向该数组添加一个函数,该函数执行您想要的每个循环操作。然后像这样在rAF循环中调用每个函数:
update( performance.now())

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

    requestAnimationFrame( update )
  }

通过一些工作,您可以调整这个简单的示例,提供一种方法来从回调中删除函数,以允许按名称或签名动态添加/减少游戏循环中的例程。

您还可以传递一个包含该函数的对象,该对象还包含一个整数,您可以使用该整数按优先级对回调进行排序。


0
你应该创建一个父组件来运行循环,然后将其传递给其他组件,它应该长这样:
<Loop>
    <ComponentX loop={loop} />
    <ComponentY loop={loop} />
    <ComponentZ loop={loop} />
</Loop>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接