npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

expo-game-support

v0.0.21

Published

Game support library for Expo/React Native with physics, robust game loop, and optimized touch input.

Downloads

112

Readme

Expo Game Support

A complete game-development library for Expo/React Native that adds advanced physics, a robust game loop, and optimized touch input handling.

🚀 Features

  • Physics Engine: Gravity, collisions, forces, impulses, sleeping
  • Optimized Game Loop: Fixed or variable time step, FPS control
  • Advanced Touch Input: Optimized gestures (tap, swipe, long press, double tap)
  • Collision Detection: AABB and circle with basic resolution
  • Collision Events: Collision start/end and trigger enter/exit callbacks
  • Rendering (optional): WebGL renderer via Expo GL (GLRenderer) and typed render interfaces (IRenderer, TextureInfo, DrawOptions)
  • Assets & Audio: AssetManager for images/spritesheets/textures and audio (via expo-asset/expo-av); SpriteAnimator for sprite animations
  • 2D Math: Comprehensive vector operations
  • TypeScript: Fully typed for an improved DX

📦 Installation

npm install expo-game-support

Required peer dependencies:

npm install expo react react-native \
  expo-asset expo-av expo-gl \
  react-native-gesture-handler react-native-reanimated

🎮 Basic Usage

Initial setup (Web)

import { GameEngine, GameObject, Vector2D } from 'expo-game-support';

// Configure the game engine
const gameEngine = new GameEngine({
  width: 400,
  height: 600,
  gravity: new Vector2D(0, 981), // Gravity pointing down
  gameLoop: {
    targetFPS: 60,
    maxDeltaTime: 0.05,
    enableFixedTimeStep: true
  }
});

// Initialize and start
gameEngine.initialize();
gameEngine.start();

Setup for React Native/Expo

import React, { useEffect, useRef } from 'react';
import { View, PanResponder } from 'react-native';
import { GameEngine, GameObject, Vector2D } from 'expo-game-support';

export default function GameComponent() {
  const gameEngineRef = useRef<GameEngine | null>(null);

  useEffect(() => {
    const gameEngine = new GameEngine({
      width: 400,
      height: 600,
      gravity: new Vector2D(0, 981),
      gameLoop: {
        targetFPS: 60,
        maxDeltaTime: 0.016,
        enableFixedTimeStep: true
      }
    });

    gameEngineRef.current = gameEngine;
    gameEngine.initialize();
    gameEngine.start();

    return () => gameEngine.stop();
  }, []);

  // Wire PanResponder to touch input
  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderGrant: (evt) => {
      gameEngineRef.current?.handleTouchStart(evt.nativeEvent);
    },
    onPanResponderMove: (evt) => {
      gameEngineRef.current?.handleTouchMove(evt.nativeEvent);
    },
    onPanResponderRelease: (evt) => {
      gameEngineRef.current?.handleTouchEnd(evt.nativeEvent);
    },
  });

  return (
    <View style={{ flex: 1 }} {...panResponder.panHandlers}>
      {/* Your game UI here */}
    </View>
  );
}

Create game objects

// Create a ball with physics
const ball = new GameObject({
  id: 'ball',
  position: new Vector2D(200, 100),
  size: new Vector2D(40, 40),
  physics: {
    mass: 1,
    velocity: new Vector2D(0, 0),
    acceleration: new Vector2D(0, 0),
    friction: 0.1,
    restitution: 0.8, // Bounciness
    isStatic: false
  }
});

// Add to the engine
gameEngine.addGameObject(ball);

Handle touch input

// Touch events
gameEngine.onTouch('player-input', (touchEvent) => {
  if (touchEvent.type === 'start') {
    console.log('Touch started at:', touchEvent.position);
  }
});

// Gestures
gameEngine.onGesture('player-gestures', (gesture) => {
  switch (gesture.type) {
    case 'tap':
      console.log('Tap at:', gesture.position);
      break;
    case 'swipe':
      console.log('Swipe direction:', gesture.direction);
      break;
  }
});

Update loop

gameEngine.onUpdate((deltaTime) => {
  // Game logic per frame
  const ball = gameEngine.getGameObject('ball');
  if (ball) {
    // Apply forces, check conditions, etc.
  }
});

gameEngine.onRender((interpolation) => {
  // Rendering (hook into your renderer of choice)
});

🖼️ Rendering with GLRenderer (Expo GL)

This library ships an optional WebGL renderer built on top of expo-gl. You can use the typed interfaces exported from render/IRenderer and the concrete GLRenderer:

import { GameEngine } from 'expo-game-support';
import { GLView } from 'expo-gl';
import { GLRenderer } from 'expo-game-support';

export default function GameWithGL() {
  return (
    <GLView
      style={{ flex: 1 }}
      onContextCreate={(gl) => {
        const renderer = new GLRenderer(gl);

        const engine = new GameEngine({
          width: gl.drawingBufferWidth,
          height: gl.drawingBufferHeight,
          gameLoop: { targetFPS: 60, maxDeltaTime: 0.033, enableFixedTimeStep: true },
        });

        // Hook engine render to the renderer
        engine.onRender(() => {
          renderer.beginFrame();
          // renderer.drawRect({...}) or draw your textures/sprites here
          renderer.endFrame();
          gl.endFrameEXP?.();
        });

        engine.initialize();
        engine.start();
      }}
    />
  );
}

Types you can import:

import type { IRenderer, TextureInfo, DrawOptions, Rect as RenderRect } from 'expo-game-support';

📤 Exports Overview

From expo-game-support root entry:

  • Core: GameEngine, GameLoop, GameObject
  • Physics: PhysicsEngine, CollisionDetector
  • Input: TouchInputManager (web), TouchInputManagerRN (React Native)
  • Utils: BoundaryChecker, ObjectCleaner, ScoreZone, ScoreManager, ObjectSpawner
  • Assets: AssetManager, SpriteAnimator
  • Rendering (optional): GLRenderer and render types IRenderer, TextureInfo, DrawOptions, Rect as RenderRect
  • Types: GameEngineConfig, GameObjectConfig, PhysicsBody, GameTouchEvent, GameLoopConfig, CollisionEvent, and assets types like AssetManifest, AssetId, ImageAsset, TextureAsset, SoundAsset, LoadedTexture, LoadedSpriteSheet, SoundHandle

Check src/index.ts for the authoritative export list.

🧭 Platform Notes

  • Web-only helpers:

    • uploadTextureFromImage(image: HTMLImageElement | ImageBitmap) is intended for web contexts using DOM-compatible image sources.
    • On iOS/Android, you must decode the asset to raw RGBA pixels before calling gl.texImage2D (see uploadTextureFromAssetNative placeholder). Consider integrating native-assisted decoding or use Expo utilities to obtain a pixel buffer.
  • React Native setup:

    • Ensure you have react-native-gesture-handler and react-native-reanimated properly configured per their docs.
    • For expo-gl, use GLView and call gl.endFrameEXP() after each frame.
    • Prefer platform-specific files when needed (e.g. *.web.ts vs *.native.ts) to separate implementations.
  • Tree-shaking:

    • The API is designed to be modular. Import only what you use to keep bundle sizes small.

🎯 Advanced Examples

Simple Pong

import { GameEngine, GameObject, Vector2D } from 'expo-game-support';

class PongGame {
  private gameEngine: GameEngine;
  private ball: GameObject;
  private paddle: GameObject;

  constructor() {
    this.gameEngine = new GameEngine({
      width: 400,
      height: 600,
      gravity: new Vector2D(0, 0), // No gravity for Pong
      gameLoop: {
        targetFPS: 60,
        maxDeltaTime: 0.016,
        enableFixedTimeStep: true
      }
    });

    this.setupGame();
  }

  private setupGame() {
    // Crear pelota
    this.ball = new GameObject({
      id: 'ball',
      position: new Vector2D(200, 300),
      size: new Vector2D(20, 20),
      physics: {
        mass: 1,
        velocity: new Vector2D(200, 150),
        acceleration: new Vector2D(0, 0),
        friction: 0,
        restitution: 1,
        isStatic: false
      }
    });

    // Crear paddle
    this.paddle = new GameObject({
      id: 'paddle',
      position: new Vector2D(200, 550),
      size: new Vector2D(80, 20),
      physics: {
        mass: 10,
        velocity: new Vector2D(0, 0),
        acceleration: new Vector2D(0, 0),
        friction: 0.9,
        restitution: 0,
        isStatic: false
      }
    });

    this.gameEngine.addGameObject(this.ball);
    this.gameEngine.addGameObject(this.paddle);

    // Handle input to move the paddle
    this.gameEngine.onTouch('paddle-control', (touch) => {
      if (touch.type === 'move') {
        this.paddle.position.x = touch.position.x;
      }
    });

    // Game logic
    this.gameEngine.onUpdate((deltaTime) => {
      this.updateGame(deltaTime);
    });
  }

  private updateGame(deltaTime: number) {
    // Wall bounces
    if (this.ball.position.x <= 10 || this.ball.position.x >= 390) {
      this.ball.physics!.velocity.x *= -1;
    }
    if (this.ball.position.y <= 10) {
      this.ball.physics!.velocity.y *= -1;
    }

    // Reset if the ball leaves screen bottom
    if (this.ball.position.y > 610) {
      this.resetBall();
    }
  }

  private resetBall() {
    this.ball.position = new Vector2D(200, 300);
    this.ball.physics!.velocity = new Vector2D(200, 150);
  }

  start() {
    this.gameEngine.initialize();
    this.gameEngine.start();
  }
}

Particle System

class ParticleSystem {
  private gameEngine: GameEngine;
  private particles: GameObject[] = [];

  constructor(gameEngine: GameEngine) {
    this.gameEngine = gameEngine;
  }

  createExplosion(position: Vector2D, particleCount: number = 20) {
    for (let i = 0; i < particleCount; i++) {
      const angle = (Math.PI * 2 * i) / particleCount;
      const speed = 100 + Math.random() * 200;
      
      const particle = new GameObject({
        id: `particle_${Date.now()}_${i}`,
        position: position.clone(),
        size: new Vector2D(4, 4),
        physics: {
          mass: 0.1,
          velocity: new Vector2D(
            Math.cos(angle) * speed,
            Math.sin(angle) * speed
          ),
          acceleration: new Vector2D(0, 0),
          friction: 0.02,
          restitution: 0.3,
          isStatic: false
        }
      });

      this.particles.push(particle);
      this.gameEngine.addGameObject(particle);

      // Destroy particle after 3 seconds
      setTimeout(() => {
        particle.destroy();
        this.particles = this.particles.filter(p => p !== particle);
      }, 3000);
    }
  }
}

📚 API Reference

GameEngine

Constructor

new GameEngine(config: GameEngineConfig)

Main methods

  • initialize(): Initialize engine
  • start(): Start game
  • pause(): Pause game
  • resume(): Resume game
  • stop(): Stop game

Object management

  • addGameObject(gameObject: GameObject): Add an object
  • removeGameObject(id: string): Remove an object
  • getGameObject(id: string): Get by ID
  • getAllGameObjects(): Get all objects

Callbacks

  • onUpdate(callback: (deltaTime: number) => void): Update callback
  • onRender(callback: (interpolation: number) => void): Render callback
  • onTouch(id: string, callback: (event: TouchEvent) => void): Touch callback
  • onGesture(id: string, callback: (gesture: GestureEvent) => void): Gesture callback
  • onCollisionStart(cb), onCollisionEnd(cb): Physics collision events
  • onTriggerEnter(cb), onTriggerExit(cb): Trigger events

GameObject

Constructor

new GameObject(config: GameObjectConfig)

Properties

  • id: string: Unique identifier
  • position: Vector2D: World position
  • size: Vector2D: Object size
  • rotation: number: Radians
  • physics?: PhysicsBody: Optional rigid body

Methods

  • update(deltaTime: number): Per-frame update
  • applyForce(force: Vector2D): Apply force
  • applyImpulse(impulse: Vector2D): Apply impulse
  • containsPoint(point: Vector2D): Point test
  • destroy(): Destroy object

Vector2D

Constructor

new Vector2D(x: number = 0, y: number = 0)

Operations

  • add(vector: Vector2D)
  • subtract(vector: Vector2D)
  • multiply(scalar: number)
  • divide(scalar: number)
  • magnitude()
  • normalize()
  • dot(vector: Vector2D)
  • distance(vector: Vector2D)

🔧 Advanced Configuration

Performance optimization

// Configure for performance
const gameEngine = new GameEngine({
  width: 400,
  height: 600,
  gravity: new Vector2D(0, 981),
  gameLoop: {
    targetFPS: 30, // Reduce FPS on slower devices
    maxDeltaTime: 0.033, // Limit time jumps
    enableFixedTimeStep: false // Variable timestep for performance
  }
});

// Touch input config
gameEngine.touchInputManager.updateConfig({
  deadZone: 10, // Larger dead zone
  maxTouchPoints: 2, // Limit touch points
  touchSensitivity: 0.8 // Lower sensitivity
});

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

📄 License

MIT License - see LICENSE for details.

🙏 Acknowledgements

  • Inspired by engines like Phaser and Matter.js
  • Tuned specifically for the Expo/React Native ecosystem

Note: To minimize tunneling and improve stability, we recommend a fixed time step (enableFixedTimeStep: true) with targetFPS 60 for physics-heavy scenes. Collision and trigger events are available via GameEngine.onCollisionStart/End and onTriggerEnter/Exit.