React+Pixi 开启web游戏开发之路

2,245 阅读4分钟

一、背景介绍

很长时间没更新了,我这里也是发现,用Dom元素对页面进行游戏操作,是非常消耗性能的,所以也随之抛弃了原来的写法,转行一些比较成熟的框架。

经过一系列的H5游戏框架背景调研,发现pixi是对于一个react前端开发工程师最友好的游戏开发工具。

  • 性能好
  • 社区健全
  • demo简单明了
  • 开发方式最接近前端业务开发
框架pixievatinycocosegretunity
背景敏捷开发敏捷开发,淘宝开发的游戏引擎,基于pixi敏捷开发,支付宝开发的游戏引擎,基于pixi引擎开发引擎开发引擎开发
官网pixijs.com/eva-engine.gitee.io/tinyjs.net/www.cocos.com/egret.com/aboutunity.cn/
web工程师上手难度简单简单简单中等
2d性能🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟
3d性能🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟
支持物理🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟
支持语言jsjs、raxjsc、c#、python、js等jsc、c#、python、js等
社区🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟

上述评分仅个人调研结果,如对评分有疑问、或者更好的建议,非常欢迎提出来一起探讨。

最终决定使用pixi进行游戏开发,接下来的游戏文章,也将都是基于react+pixi进行分享,直到发现更好的web前端游戏框架。

二、前置准备

首先用create-react-app快速搭建一个react项目

0. 安装create-react-app

$ [sudo] cnpm i create-react-app -g

我这里采用了typescript模版,不会用ts的小伙伴,先听我一句劝强烈建议用上ts,实在不能接受可以去掉后面的--template typescript

1. 新建一个名为my-react-pixi-demo1的项目

$ create-react-app my-react-pixi-demo1 --template typescript

2. 下一步 进入项目,安装依赖 安装pixi.js

$ cd my-react-pixi-demo1

$ cnpm i

$ cnpm i pixi.js --save

3. 启动项目

$ npm run start

image.png

当项目启动 出现这个像原子核的小图标,就说明基本项目已经搭建成功了

三、快速开始 coding!

1. 进入App.tsx,删掉原来的代码,写下以下代码,画布就出现了

import { useEffect, useRef } from 'react';
import { Application, ICanvas } from 'pixi.js';
import './App.css';

function App() {
  const myRef = useRef<HTMLDivElement>(null)
  useEffect(() => {
    // 新建一个画布
    const app = new Application({ background: '#1099bb' });
    // @ts-ignore 此处app.view的类型是ICanvas,pixi官方未将其定义为React原生Node的拓展,引发TS报错,可忽略
    myRef?.current?.appendChild<ICanvas>(app.view);
  }, [])

  return (
    <div className="App" >
      <div ref={myRef} />
    </div>
  );
}

export default App;

image.png

2. 给画布添加一些元素

import { useEffect, useRef } from 'react';
import { Application, ICanvas, Sprite, Container, Texture } from 'pixi.js';
import logo from './logo.svg';
import './App.css';

function App() {
  const myRef = useRef<HTMLDivElement>(null)
  useEffect(() => {
    const app = new Application({ background: '#1099bb' });

    // @ts-ignore 此处app.view: ICanvas未被定义为Node,引发TS报错,可忽略
    myRef?.current?.appendChild<ICanvas>(app.view);

    const container = new Container();

    app.stage.addChild(container);

    // Create a new texture
    const texture = Texture.from(logo);

    const bunny = new Sprite(texture);
    bunny.anchor.set(0.5);
    container.addChild(bunny);

    // Move container to the center
    container.x = app.screen.width / 2;
    container.y = app.screen.height / 2;

    // Center bunny sprite in local container coordinates
    container.pivot.x = container.width / 2;
    container.pivot.y = container.height / 2;

    // Listen for animate update
    app.ticker.add((delta) => {
      // rotate the container!
      // use delta to create frame-independent transform
      container.rotation -= 0.01 * delta;
    });
  }, [])

  return (
    <div className="App" >
      <div ref={myRef} />
    </div>
  );
}

export default App;

上面这段代码来自pixi官方的第一个demo:pixijs.io/examples/#/… 。我小改了一下,让它可以在react项目中运行。官方有很多有意思的demo,同学们都可以把代码原封不动的复制过来玩玩。看看效果吧~

image.png

四、进阶:代码优化

1. 游戏画布hooks

上面的代码,只能说是可以运行,但并不是我们理想中的“好代码”,我们需要把创建画布和动画代码分开,让代码逻辑更加清晰,也更好去复用。

我定义了一个叫做useGame的hooks

image.png

import { useEffect, useState } from "react";
import { Application } from 'pixi.js'
import type { IApplicationOptions, ICanvas } from 'pixi.js'

export interface IUseGameOptions extends IApplicationOptions {
    myRef: React.RefObject<HTMLDivElement>;
}

const useGame = ({ myRef, ...rest }: IUseGameOptions) => {
    const [app, setApp] = useState<Application<ICanvas>>()

    useEffect(() => {
        // 获取容器的宽高
        const width = rest?.width || myRef?.current?.clientWidth;
        const height = rest?.height || myRef?.current?.clientHeight;
        const myApp = new Application({ ...rest, width, height });
        setApp(myApp)
        // @ts-ignore 此处app.view: ICanvas未被定义为Node,引发TS报错,可忽略
        myRef?.current?.appendChild(myApp.view);
    }, [])

    return {
        app,
    }
}

export default useGame;

useGame该如何使用?

  1. App.tsx
import { useEffect, useRef } from 'react';
import useGame from './hooks/useGame';
import './App.css';

function App() {
  const myRef = useRef<HTMLDivElement>(null)
  const { app } = useGame({ myRef: myRef, background: '#1099bb' })

  useEffect(() => {
    // 此处可以用app编写游戏代码
  }, [app])

  return (
    <div className="App" >
      <div className='game1' ref={myRef} />
    </div>
  );
}

export default App;
  1. App.css
.App {
  display: flex;
  justify-content: center;
  padding: 20px;
}

.game1 {
  width: 300px;
  height: 300px;
}

  1. 很轻松的定义了一块画布,画布的位置和大小随css而定,非常方便。

image.png

  1. 添加内容,责任分明后,代码更加清晰了,也非常好懂
import { useEffect, useRef } from 'react';
import { Container, Sprite, Texture } from 'pixi.js';
import useGame from './hooks/useGame';
import logo from './logo.svg';
import './App.css';

function App() {
  const myRef = useRef<HTMLDivElement>(null)
  const { app } = useGame({ myRef: myRef, background: '#1099bb' })

  const addContainer = () => {
    if (!app) {
      return;
    }
    const container = new Container();

    app.stage.addChild(container);

    // Create a new texture
    const texture = Texture.from(logo);

    const bunny = new Sprite(texture);
    bunny.anchor.set(0.5);
    container.addChild(bunny);

    // Move container to the center
    container.x = app.screen.width / 2;
    container.y = app.screen.height / 2;

    // Center bunny sprite in local container coordinates
    container.pivot.x = container.width / 2;
    container.pivot.y = container.height / 2;

    // Listen for animate update
    app.ticker.add((delta) => {
      // rotate the container!
      // use delta to create frame-independent transform
      container.rotation -= 0.01 * delta;
    });
  }

  useEffect(() => {
    addContainer()
  }, [app])

  return (
    <div className="App" >
      <div className='game1' ref={myRef} />
    </div>
  );
}

export default App;

image.png

2. 定义游戏元素

一个游戏、无非就是若干个元素的组合,只要提前定义好游戏元素,想用的时候拿来用就好了。

import { useEffect, useRef } from 'react';
import { Application, Container, ICanvas, Sprite, Texture } from 'pixi.js';
import useGame from './hooks/useGame';
import logo from './logo.svg';
import './App.css';

// 定义一个游戏元素
const gameNode = ({ app, x, y }: { app: Application<ICanvas>, x?: number, y?: number }) => {
  const container = new Container();
  // Create a new texture
  const texture = Texture.from(logo);
  const bunny = new Sprite(texture);
  bunny.anchor.set(0.5);
  container.addChild(bunny);
  container.x = x || 0;
  container.y = y || 0;
  let speedX = 1, speedY = 1

  app.ticker.add((delta) => {
    // rotate the container!
    // use delta to create frame-independent transform
    container.rotation -= 0.01 * delta;
    if (container.x < 0 || container.x > app.screen.width) {
      speedX = -speedX
    }
    if (container.y < 0 || container.y > app.screen.width) {
      speedY = -speedY
    }
    container.x = container.x + speedX
    container.y = container.y + speedY
  });
  return container
}

function App() {
  const myRef = useRef<HTMLDivElement>(null)
  const { app } = useGame({ myRef: myRef, background: '#1099bb' })

  useEffect(() => {
    if (!app) {
      return;
    }
    // 定义不同的位置
    [[50, 50], [50, 250], [250, 250], [250, 50]].map((item) => {
      app.stage.addChild(gameNode({ app: app, x: item[0], y: item[1] }));
    })

  }, [app])

  return (
    <div className="App" >
      <div className='game1' ref={myRef} />
    </div>
  );
}

export default App;

看看效果吧

游戏元素.gif

五、后续更新

后续会继续更新pixi.js小游戏的demo