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
Maintainers
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:
AssetManagerfor images/spritesheets/textures and audio (viaexpo-asset/expo-av);SpriteAnimatorfor sprite animations - 2D Math: Comprehensive vector operations
- TypeScript: Fully typed for an improved DX
📦 Installation
npm install expo-game-supportRequired 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):
GLRendererand render typesIRenderer,TextureInfo,DrawOptions,Rect as RenderRect - Types:
GameEngineConfig,GameObjectConfig,PhysicsBody,GameTouchEvent,GameLoopConfig,CollisionEvent, and assets types likeAssetManifest,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(seeuploadTextureFromAssetNativeplaceholder). Consider integrating native-assisted decoding or use Expo utilities to obtain a pixel buffer.
React Native setup:
- Ensure you have
react-native-gesture-handlerandreact-native-reanimatedproperly configured per their docs. - For
expo-gl, useGLViewand callgl.endFrameEXP()after each frame. - Prefer platform-specific files when needed (e.g.
*.web.tsvs*.native.ts) to separate implementations.
- Ensure you have
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 enginestart(): Start gamepause(): Pause gameresume(): Resume gamestop(): Stop game
Object management
addGameObject(gameObject: GameObject): Add an objectremoveGameObject(id: string): Remove an objectgetGameObject(id: string): Get by IDgetAllGameObjects(): Get all objects
Callbacks
onUpdate(callback: (deltaTime: number) => void): Update callbackonRender(callback: (interpolation: number) => void): Render callbackonTouch(id: string, callback: (event: TouchEvent) => void): Touch callbackonGesture(id: string, callback: (gesture: GestureEvent) => void): Gesture callbackonCollisionStart(cb),onCollisionEnd(cb): Physics collision eventsonTriggerEnter(cb),onTriggerExit(cb): Trigger events
GameObject
Constructor
new GameObject(config: GameObjectConfig)Properties
id: string: Unique identifierposition: Vector2D: World positionsize: Vector2D: Object sizerotation: number: Radiansphysics?: PhysicsBody: Optional rigid body
Methods
update(deltaTime: number): Per-frame updateapplyForce(force: Vector2D): Apply forceapplyImpulse(impulse: Vector2D): Apply impulsecontainsPoint(point: Vector2D): Point testdestroy(): 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
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push the branch (
git push origin feature/AmazingFeature) - 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.
