Building a Cross‑Platform English Quiz Mini‑Game with React & Pixi.js

This article details how to create a cross‑platform English‑learning quiz PK mini‑game for both a main app (H5) and WeChat mini‑games, covering technology selection, project structure, webpack configuration, virtual routing, component development with react‑pixi‑fiber, animation techniques, platform adapters, and performance optimizations.

Liulishuo Tech Team
Liulishuo Tech Team
Liulishuo Tech Team
Building a Cross‑Platform English Quiz Mini‑Game with React & Pixi.js

Introduction

The business requirement was an English‑learning quiz PK mini‑game that runs both as an H5 page inside the main app and as a WeChat mini‑game. The game consists of a few scenes with simple animations, so the goal was to use familiar tools, keep development cost low, and maintain code portability across platforms.

Technology selection

Both WeChat mini‑games and H5 use a canvas for rendering, so a canvas‑based approach was chosen. Among 2D rendering libraries, Pixi.js was selected because the react‑pixi‑fiber integration lets developers write UI with React components while Pixi.js handles the actual drawing. React therefore manages state, routing and logic, and Pixi.js focuses on graphics.

Project structure

├─build                 // scripts
│  ├─webpack.config.js  // webpack configuration
├─public                // H5 resources
│  ├─index.html         // HTML template
├─src                   // source code
│  ├─assets             // assets
│  ├─components         // reusable components
│  ├─pages              // scene pages
│  │    ├─entry         // scene 1
│  │    └─flow          // scene 2
│  │    └─...           // other scenes
│  ├─app.tsx            // game entry point
│  └─routes.ts          // routing configuration
├─project.config.json   // mini‑game project config
├─game.json             // mini‑game entry JSON
├─game.js               // mini‑game bootstrap

Shared logic resides in src because the entry points for H5 and the mini‑program differ.

Build and bundling

Webpack is used to transpile JSX and other React features. In development mode the devServer writes files to disk so the mini‑program can read updates in real time.

// webpack.config.js (excerpt)
module.exports = {
  devServer: {
    writeToDisk: true,
    // other devServer options
  },
  // other configuration
};

Build scripts switch the target via an environment variable:

// package.json scripts
{
  "scripts": {
    "build:h5": "BUILD_TARGET=h5 webpack --config build/webpack.config.js",
    "build:minigame": "BUILD_TARGET=minigame webpack --config build/webpack.config.js"
  }
}
// target handling in webpack.config.js
const target = process.env.BUILD_TARGET || 'minigame';
module.exports = {
  output: {
    filename: target === 'minigame' ? '[name].js' : '[name].[contenthash:8].js',
  },
  plugins: [
    target === 'minigame' ? null : new HTMLWebpackPlugin({})
  ].filter(Boolean)
};

Virtual routing design

The mini‑game has a single entry point and no page concept; scenes are used instead. A “virtual router” is introduced: all launches first land on a landing page that reads launch parameters (via wx.getLaunchOptionsSync on mini‑programs or URL query on H5) and then navigates to the appropriate scene using an in‑memory history.

// App.jsx (simplified)
import React from 'react';
import { Route, Router } from 'react-router';
import { createMemoryHistory } from 'history';

const history = createMemoryHistory();

function App() {
  return (
    <Router history={history}>
      <Route exact path="/" component={SceneLoad} />
      <Route exact path="/scene-home" component={SceneHome} />
      {/* other scenes */}
    </Router>
  );
}

Stage initialization

// stage.tsx
import React from 'react';
import { render } from 'react-pixi-fiber';
import * as PIXI from 'pixi.js-legacy';

const { Application } = PIXI;
PIXI.settings.ROUND_PIXELS = true;

const DPR = window.devicePixelRatio;
const app = new Application({
  view: canvas,               // platform‑specific canvas element
  width: 375 * DPR,
  height: 667 * DPR,
  antialias: true,
  autoDensity: true,
});

render(<App />, app.stage);

Component development

Example: a progress‑bar component implemented with raw Pixi.js (OOP style) and with JSX via react‑pixi‑fiber.

// native Pixi.js (OOP)
import { DPR } from '@/utils/styles';
import * as PIXI from 'pixi.js-legacy';

const wrapper = new PIXI.Container();
const bgSprite = new PIXI.Sprite(PIXI.Texture.WHITE);
bgSprite.width = 335 * DPR;
bgSprite.height = 16 * DPR;
bgSprite.tint = 0x1e127f;
bgSprite.alpha = 0.13;
wrapper.addChild(bgSprite);
// add inner bar, etc.
// JSX version with react-pixi-fiber
import React from 'react';
import { Sprite, Container } from 'react-pixi-fiber';
import { Texture, Point } from 'pixi.js-legacy';

export default function ProgressBar({ width = 335 * DPR, height = 16 * DPR, progress, color }) {
  const progressWidth = React.useMemo(() => progress * width, [progress, width]);
  return (
    <Container width={width} height={height}>
      <Sprite width={width} height={height} texture={Texture.WHITE} tint={0x1e127f} alpha={0.13} />
      <Container width={progressWidth} height={height}>
        <Sprite width={progressWidth} height={height} texture={Texture.WHITE} tint={color[0]} />
        <Sprite height={(375 * height) / 16} width={height} texture={loadingBackground} angle={90} anchor={new Point(0, 1)} />
      </Container>
    </Container>
  );
}

Animation

Simple UI animations can be driven by JavaScript. For richer effects, react‑spring is used to animate a button’s scale on press.

// button with spring animation
import React from 'react';
import { animated } from 'react-spring';
import { Container } from 'react-pixi-fiber';

const AnimatedContainer = animated(Container);

function Button({ children, onPointerDown, onPointerUp, pointerDown }) {
  const { scale } = useSpring({
    config: { duration: 50 },
    scale: pointerDown ? 0.8 : 1,
  });
  return (
    <AnimatedContainer
      interactive
      buttonMode
      onPointerDown={onPointerDown}
      onPointerUp={onPointerUp}
      scale={scale.interpolate(t => new Point(t, t))}
    >
      {children}
    </AnimatedContainer>
  );
}

Lottie animations are rendered onto an off‑screen canvas and then used as a Pixi Sprite texture.

// Lottie view component
import React from 'react';
import { Sprite } from 'react-pixi-fiber';
import { Texture } from 'pixi.js-legacy';

function Canvas(props, ref) {
  const canvasRef = React.useRef(null);
  const spriteRef = React.useRef(null);

  React.useImperativeHandle(ref, () => {
    canvasRef.current = wx.createCanvas();
    return canvasRef.current;
  }, []);

  return (
    <Sprite
      {...props}
      texture={canvasRef.current && Texture.from(canvasRef.current)}
      ref={spriteRef}
    />
  );
}

const LottieView = React.forwardRef(Canvas);

Cross‑platform adaptation

Pixi.js expects browser APIs (e.g., document, audio). Adapter modules map these to WeChat’s wx namespace.

// canvas adapter (mini‑game)
const canvas = wx.createCanvas();

// audio adapter (mini‑game)
const audio = wx.createInnerAudioContext();
audio.src = 'foo.mp3';
audio.play();

Request adapters for H5 and mini‑game are placed under adaptor/ and referenced via a Webpack alias $adaptor that resolves to the appropriate implementation based on BUILD_TARGET.

// adaptor/h5/request.js
export const request = (url, params) => {
  const options = { ...params, method: (params.method || 'GET').toUpperCase() };
  if (params.data && options.method === 'POST') {
    options.body = JSON.stringify(params.data);
  }
  return window.fetch(url, options);
};

// adaptor/minigame/request.js
export const request = (url, params) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url,
      method: (params.method || 'GET').toUpperCase(),
      header: params.headers || {},
      success: resolve,
      fail: reject,
    });
  });
};

// webpack alias snippet
module.exports = {
  resolve: {
    alias: {
      $adaptor: resolve(src, `adaptor/${process.env.BUILD_TARGET}`),
    },
  },
};

Performance and experience optimizations

To keep the initial package small, static assets are loaded over HTTP after the landing page. Resource caching uses wx.downloadFile with a custom cache‑lookup routine.

export const downloadFile = async (path, save = true) => {
  const cached = await new Promise(resolve => {
    wx.getFileSystemManager().access({
      path: 'your cache path',
      success: () => resolve(true),
      fail: () => resolve(false),
    });
  });
  if (cached) return { tempFilePath: path, filePath: path };
  return new Promise((resolve, reject) => {
    wx.downloadFile({
      url: path,
      success: async data => {
        if (data.statusCode !== 200) return reject(new Error(data.errMsg));
        const { tempFilePath } = data;
        const filePath = save ? await saveFile(tempFilePath, path) : tempFilePath;
        resolve({ filePath, tempFilePath });
      },
      fail: reject,
    });
  });
};

Graphics performance is improved by updating only changed properties. For example, the applyProps hook now checks whether alpha actually changed before assigning it.

// before
instance.alpha = alpha;
// after
if (alpha !== oldProps.alpha) {
  instance.alpha = alpha;
}

Frequently changing text (e.g., countdown timers) uses BitmapText instead of Text to reduce rendering cost.

const Countdown = ({ time }) => (
  <Container>
    <BitmapText
      text={time + 's'}
      anchor={new Point(0.5, 0.5)}
      position={new Point(0, px(12))}
      style={{ fontWeight: 'bolder', fill: 0xffffff }}
    />
  </Container>
);

Interactive flags are set only on elements that need user interaction, minimizing the load on Pixi’s interaction manager.

Conclusion

Combining React with Pixi.js provides a productive workflow for 2D mini‑games. React offers declarative UI, state management and routing, while Pixi.js handles efficient canvas rendering. By abstracting platform differences through adapters and sharing a common canvas API, the same codebase can be deployed to both H5 and WeChat mini‑games, significantly reducing multi‑platform development and maintenance costs.

PerformanceCross-PlatformReActWebpackH5WeChat Mini Gamepixi.js
Liulishuo Tech Team
Written by

Liulishuo Tech Team

Help everyone become a global citizen!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.