这段太长别看: 在这系列教程中, 你将学会如何使用 React 和 Redux 去控制一堆 SVG 元素来制作一个游戏. 这一个系列所带给你的知识也可以让你使用 React 和 Redux 去制作其他的动画和特效, 并不仅限于游戏. 你可以在这里找到第一部分的全部代码: Aliens Go Home - Part 1 https://github.com/auth0-blog/aliens-go-home-part-1
React 游戏 : 外星人, 滚回家!
在本系列开发的游戏名为外星人, 滚回家!(Aliens , Go Home !). 这个游戏很简单: 你用一个加农炮, 来消灭试图入侵地球的飞碟. 你必须通过准确点击 SVG 元素来发射加农炮. 如果你忍不住好奇心, 可以先去看看可以试玩的最终版本(链接已经挂了, 不知道作者什么时候恢复, 你可以 clone 第三部分的代码自己运行试玩) http://bang-bang.digituz.com.br/ . 但是别玩太久! 你还有正事要做呢!
第三部分的最终代码 https://github.com/auth0-blog/aliens-go-home-part-3
知识储备
学习本系列之前, 你需要一些知识储备:
Web 开发基本知识, 主要是 JavaScript.
有 node 环境.
会用 Node 包管理工具 npm.
你并不需要十分精通 JavaScript,React 和 SVG. 当然, 如果你玩的很 6, 你学起来会很轻松, 并且能很快抓住重点(译者: 建议还是先学点儿 React 和 Redux 吧, 不然能做出来但是看不懂的).
- # 使用 npx 将会下载
- # create-react-app 并且执行它
- npx create-react-app aliens-go-home
- # 进入项目目录
- cd aliens-go-home
- |- node_modules
- |- public
- |- favicon.ico
- |- index.html
- |- manifest.json
- |- src
- |- App.css
- |- App.js
- |- App.test.js
- |- index.css
- |- index.js
- |- logo.svg
- |- registerServiceWorker.js
- |- .gitignore
- |- package.json
- |- package-lock.json
- |- README.md
- render() {
- return (
- <div className="App">
- </div>
- );
- }
- const initialState = {
- message: `It's easy to integrate React and Redux, isn't it?`,
- };
- function reducer(state = initialState) {
- return state;
- }
- export default reducer;
- import React, {Component} from 'react';
- import PropTypes from 'prop-types';
- class App extends Component {
- render() {
- return (
- <div className="App">
- <h1>{this.props.message}</h1>
- </div>
- );
- }
- }
- App.propTypes = {
- message: PropTypes.string.isRequired,
- };
- export default App;
- import { connect } from 'react-redux';
- import App from '../App';
- const mapStateToProps = state => ({
- message: state.message,
- });
- const Game = connect(
- mapStateToProps,
- )(App);
- export default Game;
- import React from 'react';
- import ReactDOM from 'react-dom';
- import { Provider } from 'react-redux';
- import { createStore } from 'redux';
- import './index.css';
- import Game from './containers/Game';
- import reducer from './reducers';
- import registerServiceWorker from './registerServiceWorker';
- /* eslint-disable no-underscore-dangle */
- const store = createStore(
- reducer, /* preloadedState, */
- window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
- //__REDUX_DEVTOOLS_EXTENSION__是一个调试扩展工具, 不传也没关系
- );
- /* eslint-enable */
- ReactDOM.render(
- <Provider store={store}>
- <Game />
- </Provider>,
- document.getElementById('root'),
- );
- registerServiceWorker();
- import React from 'react';
- const Canvas = () => {
- const style = {
- border: '1px solid black',
- };
- return (
- <svg
- id="aliens-go-home-canvas"
- preserveAspectRatio="xMaxYMax none"
- style={style}
- >
- <circle cx={0} cy={0} r={50} />
- </svg>
- );
- };
- export default Canvas;
- import React, {Component} from 'react';
- import Canvas from './components/Canvas';
- class App extends Component {
- render() {
- return (
- <Canvas />
- );
- }
- }
- export default App;
- import React from 'react';
- const Canvas = () => {
- const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
- return (
- <svg
- id="aliens-go-home-canvas"
- preserveAspectRatio="xMaxYMax none"
- viewBox={viewBox}
- >
- <circle cx={0} cy={0} r={50} />
- </svg>
- );
- };
- export default Canvas;
- html, body {
- overflow: hidden;
- height: 100%;
- }
- import React from 'react';
- const Sky = () => {
- const skyStyle = {
- fill: '#30abef',
- };
- const skyWidth = 5000;
- const gameHeight = 1200;
- return (
- <rect
- style={skyStyle}
- x={skyWidth / -2}
- y={100 - gameHeight}
- width={skyWidth}
- height={gameHeight}
- />
- );
- };
- export default Sky;
- import React from 'react';
- import Sky from './Sky';
- const Canvas = () => {
- const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
- return (
- <svg
- id="aliens-go-home-canvas"
- preserveAspectRatio="xMaxYMax none"
- viewBox={viewBox}
- >
- <Sky />
- <circle cx={0} cy={0} r={50} />
- </svg>
- );
- };
- export default Canvas;
- import React from 'react';
- const Ground = () => {
- const groundStyle = {
- fill: '#59a941',
- };
- const division = {
- stroke: '#458232',
- strokeWidth: '3px',
- };
- const groundWidth = 5000;
- return (
- <g id="ground">
- <rect
- id="ground-2"
- data-name="ground"
- style={groundStyle}
- x={groundWidth / -2}
- y={0}
- width={groundWidth}
- height={100}
- />
- <line
- x1={groundWidth / -2}
- y1={0}
- x2={groundWidth / 2}
- y2={0}
- style={division}
- />
- </g>
- );
- };
- export default Ground;
- // very wide to provide as full screen feeling
- export const skyAndGroundWidth = 5000;
- export const pathFromBezierCurve = (cubicBezierCurve) => {
- const {
- } = cubicBezierCurve;
- return `
- M${initialAxis.x} ${initialAxis.y}
- c ${initialControlPoint.x} ${initialControlPoint.y}
- ${endingControlPoint.x} ${endingControlPoint.y}
- ${endingAxis.x} ${endingAxis.y}
- `;
- };
- import React from 'react';
- import { pathFromBezierCurve } from '../utils/formulas';
- const CannonBase = (props) => {
- const cannonBaseStyle = {
- fill: '#a16012',
- stroke: '#75450e',
- strokeWidth: '2px',
- };
- const baseWith = 80;
- const halfBase = 40;
- const height = 60;
- const negativeHeight = height * -1;
- const cubicBezierCurve = {
- initialAxis: {
- x: -halfBase,
- y: height,
- },
- initialControlPoint: {
- x: 20,
- y: negativeHeight,
- },
- endingControlPoint: {
- x: 60,
- y: negativeHeight,
- },
- endingAxis: {
- x: baseWith,
- y: 0,
- },
- };
- return (
- <g>
- <path
- style={cannonBaseStyle}
- d={pathFromBezierCurve(cubicBezierCurve)}
- />
- <line
- x1={-halfBase}
- y1={height}
- x2={halfBase}
- y2={height}
- style={cannonBaseStyle}
- />
- </g>
- );
- };
- export default CannonBase;
- import React from 'react';
- import PropTypes from 'prop-types';
- import { pathFromBezierCurve } from '../utils/formulas';
- const CannonPipe = (props) => {
- const cannonPipeStyle = {
- fill: '#999',
- stroke: '#666',
- strokeWidth: '2px',
- };
- const transform = `rotate(${props.rotation}, 0, 0)`;
- const muzzleWidth = 40;
- const halfMuzzle = 20;
- const height = 100;
- const yBasis = 70;
- const cubicBezierCurve = {
- initialAxis: {
- x: -halfMuzzle,
- y: -yBasis,
- },
- initialControlPoint: {
- x: -40,
- y: height * 1.7,
- },
- endingControlPoint: {
- x: 80,
- y: height * 1.7,
- },
- endingAxis: {
- x: muzzleWidth,
- y: 0,
- },
- };
- return (
- <g transform={transform}>
- <path
- style={cannonPipeStyle}
- d={pathFromBezierCurve(cubicBezierCurve)}
- />
- <line
- x1={-halfMuzzle}
- y1={-yBasis}
- x2={halfMuzzle}
- y2={-yBasis}
- style={cannonPipeStyle}
- />
- </g>
- );
- };
- CannonPipe.propTypes = {
- rotation: PropTypes.number.isRequired,
- };
- export default CannonPipe;
- import React from 'react';
- import Sky from './Sky';
- import Ground from './Ground';
- import CannonBase from './CannonBase';
- import CannonPipe from './CannonPipe';
- const Canvas = () => {
- const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
- return (
- <svg
- id="aliens-go-home-canvas"
- preserveAspectRatio="xMaxYMax none"
- viewBox={viewBox}
- >
- <Sky />
- <Ground />
- <CannonPipe rotation={45} />
- <CannonBase />
- </svg>
- );
- };
- export default Canvas;
- export const MOVE_OBJECTS = 'MOVE_OBJECTS';
- export const moveObjects = mousePosition => ({
- type: MOVE_OBJECTS,
- mousePosition,
- });
- import { MOVE_OBJECTS } from '../actions';
- import moveObjects from './moveObjects';
- const initialState = {
- angle: 45,
- };
- function reducer(state = initialState, action) {
- switch (action.type) {
- case MOVE_OBJECTS:
- return moveObjects(state, action);
- default:
- return state;
- }
- }
- export default reducer;
- import { calculateAngle } from '../utils/formulas';
- function moveObjects(state, action) {
- if (!action.mousePosition) return state;
- const { x, y } = action.mousePosition;
- const angle = calculateAngle(0, 0, x, y);
- return {
- ...state,
- angle,
- };
- }
- export default moveObjects;
- export const radiansToDegrees = radians => ((radians * 180) / Math.PI);
- // https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees
- if (x2>= 0 && y2>= 0) {
- return 90;
- } else if (x2 <0 && y2>= 0) {
- return -90;
- }
- const dividend = x2 - x1;
- const divisor = y2 - y1;
- const quotient = dividend / divisor;
- return radiansToDegrees(Math.atan(quotient)) * -1;
- };
- import { connect } from 'react-redux';
- import App from '../App';
- import { moveObjects } from '../actions/index';
- const mapStateToProps = state => ({
- angle: state.angle,
- });
- const mapDispatchToProps = dispatch => ({
- moveObjects: (mousePosition) => {
- dispatch(moveObjects(mousePosition));
- },
- });
- const Game = connect(
- mapStateToProps,
- mapDispatchToProps,
- )(App);
- export default Game;
- import React, {Component} from 'react';
- import PropTypes from 'prop-types';
- import { getCanvasPosition } from './utils/formulas';
- import Canvas from './components/Canvas';
- class App extends Component {
- componentDidMount() {
- const self = this;
- setInterval(() => {
- self.props.moveObjects(self.canvasMousePosition);
- }, 10);
- }
- trackMouse(event) {
- this.canvasMousePosition = getCanvasPosition(event);
- }
- render() {
- return (
- <Canvas
- angle={this.props.angle}
- trackMouse={event => (this.trackMouse(event))}
- />
- );
- }
- }
- App.propTypes = {
- angle: PropTypes.number.isRequired,
- moveObjects: PropTypes.func.isRequired,
- };
- export default App;
- export const getCanvasPosition = (event) => {
- // mouse position on auto-scaling canvas
- // https://stackoverflow.com/a/10298843/1232793
- const svg = document.getElementById('aliens-go-home-canvas');
- const point = svg.createSVGPoint();
- point.x = event.clientX;
- point.y = event.clientY;
- const { x, y } = point.matrixTransform(svg.getScreenCTM().inverse());
- return {x, y};
- };
- import React from 'react';
- import PropTypes from 'prop-types';
- import Sky from './Sky';
- import Ground from './Ground';
- import CannonBase from './CannonBase';
- import CannonPipe from './CannonPipe';
- const Canvas = (props) => {
- const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
- return (
- <svg
- id="aliens-go-home-canvas"
- preserveAspectRatio="xMaxYMax none"
- onMouseMove={props.trackMouse}
- viewBox={viewBox}
- >
- <Sky />
- <Ground />
- <CannonPipe rotation={props.angle} />
- <CannonBase />
- </svg>
- );
- };
- Canvas.propTypes = {
- angle: PropTypes.number.isRequired,
- trackMouse: PropTypes.func.isRequired,
- };
- export default Canvas;
来源: https://juejin.im/post/5b0a648df265da0dd424d6f8