I’m working on a game called “Charisma The Chameleon” which is developed using Three.js, React and WebGL. This is an introduction to using the React -three- Renderer (R3R for short) in conjunction with these frameworks.
SitePoint has an introduction to React and WebGL. Visit WebGL Primer and React + JSX to get started. These two articles and the accompanying source code use ES6 syntax.
How to start
A while back, Pete Hunt told a joke about React development in IRC # reactJS:
I bet we can develop first person shooters with React!
The enemy has “” and so on.
He and I both laughed, and everyone was happy. “Who on earth would do that?” I wonder.
A year later, that’s what I’m doing.
Charmed Chameleon is a game that shrinks itself to solve endless fractal mazes by collecting “power supplies”. As a React developer with many years of experience, I was curious if there was a good way to use three.js in React. That’s when THE R3R caught my eye.
Why React?
I know what you’re thinking: Why? Let me be funny for a moment. Here are a few reasons to consider using React to drive 3D scenes:
-
The “declaration” view allows you to clearly separate the scene rendering from the game logic.
- Components are easy to design, such as
.
, and so on. -
Hot loading of game resources. Update textures and models in the scene in real time!
-
Use a browser tool (such as Chrome Inspector) to examine and debug 3D scenes like markup.
-
Use Webpack dependencies to manage game resources, for example
Let’s build a scenario to understand how they work.
Recommended course
Wes Bos is a step-by-step training course that allows you to build real websites and applications using React. Js + Firebase in just a few afternoons. Use the promo code ‘SITEPOINT’ when paying to get 25% off.
React and WebGL
Along with this article, I built a GitHub sample repository. Clone the repository, run the code following the instructions in readme.md and follow along. It launches the SitePointy 3D robot!
Warning: R3R is still in beta, its API is not stable and may change in the future; Currently only a subset of three.js is processed. In my opinion it is sufficient to develop a full game, but it may vary from person to person.
Organizing view code
One of the biggest benefits of using React to drive WebGL is the ability to decouple our view code from the game logic, which means that the entities we present can be small components that can be easily exported.
R3R exposes a declared API by wrapping three.js. For example, we could write:
<scene>
<perspectiveCamera
position={ new THREE.Vector3( 1, 1, 1 )
/>
</scene>
Copy the code
Now we have an empty 3D scene and camera. Adding a grid to a scene is as simple as introducing Component, and give it, and ‘ ‘.
The < scene >... <mesh> <boxGeometry width={ 1 } height={ 1 } depth={ 1 } /> <meshBasicMaterial color={ 0x00ff00 } /> </mesh>Copy the code
Underneath the code, it creates a three. Scene and automatically adds a grid via Three. BoxGeometry. R3R handles changes to the scene. If you add a grid to the scene, the original grid will not be rebuilt. Just like the regular React DOM, 3D scenes are only updated where there are differences.
Because we developed with React, we were able to separate the entire game into component files. The rotor.js file in the sample repository demonstrates how to represent the protagonist using pure React view code. It is a “stateless component”, meaning it has no state of its own to manage:
const Robot = ({ position, rotation }) => <group
position={ position }
rotation={ rotation }
>
<mesh rotation={ localRotation }>
<geometryResource
resourceId="robotGeometry"
/>
<materialResource
resourceId="robotTexture"
/>
</mesh>
</group>;
Copy the code
Now let’s load the 3D scene!
The < scene >... The < mesh >... < / mesh > < Robot position = {... } rotation = {... } /> </scene>Copy the code
You can see more examples of the API in the R3R GitHub repository, or see the full sample code in the companion project.
Organization code logic
The second part of the problem is dealing with the game logic. Let’s add some simple animations to the SitePointy robot.
How do traditional games work? They take user input, analyze the existing “state of the game world,” and return the new state for rendering. For convenience, let’s save the “game state” object into the component. In more mature projects, it is recommended to put the game state in Redux or Flux’s store.
We use the browser’s requestAnimationFrame API callback as the driver of the game loop and run it in gamecontainer.js. To get the robot moving, we calculate the new position based on the timestamp passed to the requestAnimationFrame, and then save the new position to the state.
/ /... GameLoop (time) {this.setState({robotPosition: new three.sin (time * 0.01), 0, 0)}); }Copy the code
A call to setState() triggers a child component redraw, and the 3D scene is updated. We pass state from the container component to the presentation component:
render() {
const { robotPosition } = this.state;
return <Game
robotPosition={ robotPosition }
/>;
}
Copy the code
We can use a very useful pattern to help organize this code. Updating the robot’s position is a simple time-based calculation, and in the future, it will be considered to record the previous robot’s position in the previous game state. A function that receives data, processes it, and then returns new data is usually called a reducer. We can abstract the move position code into the Reducer function!
Now we can write a clean, simple game loop with only function calls inside:
import robotMovementReducer from './game-reducers/robotMovementReducer.js'; / /... gameLoop() { const oldState = this.state; const newState = robotMovementReducer( oldState ); this.setState( newState ); }Copy the code
To add more logic to the game loop (such as dealing with physics), create another reducer function and pass its results to the previous one:
`const newState = physicsReducer( robotMovementReducer( oldState ) ); `Copy the code
As game engines continue to grow in complexity, organizing game logic into separate functions is critical. This organization would be simple in the Reducer mode.
Resource management
This is still an evolving part of R3R. The texture component needs to specify the URL attribute on the JSX tag. Using Webpack, you can import images directly from the local path:
`<texture url={ require( '.. /local/image/path.png' ) } />`Copy the code
In this way, if you modify the local image, your 3D scene will be hot updated! This is very helpful for rapidly iterating game design and content.
For other resources, such as 3D models, you may still need to use components built into Three.js to handle them; For example, JSONLoader. I tried to use a custom WebPack loader to load 3D model files, but ended up doing a lot of work that didn’t pay off. It is easier to process models as binary data and load them using file-loader, which also provides hot updates to the model data. You can see this in the sample code.
debugging
R3R supports the React developer tool extension that supports both Chrome and Firefox. If the scene is a normal DOM element, you can check it! Display the bounding box of the scene by moving the mouse over the component of the inspector. You can also move the mouse over the definition of a texture to see which object in the scene uses the texture.
You can also join the React-three-Renderer Gitter chat room for help with application debugging.
Performance Considerations
By building charmed Chameleon, I ran into some workflow-related performance issues:
- My Webpack hot reload time is up to 30 seconds! This is because a large number of resources are rewritten at the time of reloading. The solution is to implement Webpack’s DLLPlugin, which can reduce the reload time to less than 5 seconds.
- Ideally, the scene should only be called once per frame during rendering
setState()
. By analyzing my game, React itself is the main bottleneck, called every framesetState()
More than one will result in double rendering and degrade performance. - R3R performs worse than normal three.js code when there are more than a certain number of objects. In my project, it’s about 1,000 objects. You can compare the differences between R3R and three.js by using the “Benchmarks” sample code.
Timeline in Chrome DevTools is a great tool for debugging performance. It makes it easy and intuitive to check your game loop, and is much more readable than the Profile feature.
That’s it!
Check out the charmed Chameleon to see the implementation of this project. Even though the tool is still young, I found React and R3R to be perfectly clean in organizing game code. You can also check out the growing R3R sample page to see organized code examples.
This article was moderated by Mark Brown and Kev Zettler. Thanks to all the SitePoint reviewers for making SitePoint content even better!
Understanding of the author
Hello! I’m a software engineer from Bay Area.