An Orientation Hook for Mobile Games in React Native / Expo

October 25, 2024

Mobile gaming requires careful handling of device orientation to provide the best user experience. Today, we'll explore a custom React Native hook that manages device orientation intelligently, particularly focusing on gaming scenarios. This hook seamlessly handles orientation changes, locks screen orientation when needed, and integrates with Redux for state management.

Understanding the Requirements

In mobile games, we often need to:

  1. Lock the screen to portrait mode during regular app usage
  2. Allow free rotation during gameplay
  3. Track device orientation changes in real-time
  4. Update the app state based on orientation changes

The Implementation

Let's break down our useDeviceOrientation hook:

type DeviceOrientationT =
  | 'portrait'
  | 'landscape-right'
  | 'landscape-left'
  | 'upside-down';

const orientationMap: Record<DeviceMotionOrientation, DeviceOrientationT> = {
  0: 'portrait',
  90: 'landscape-right',
  180: 'upside-down',
  [-90]: 'landscape-left',
};

First, we define our orientation types and create a mapping between device motion values and human-readable orientations. This mapping makes the code more maintainable and self-documenting.

Core Functionality

The hook's main logic resides in a single useEffect:

useEffect(() => {
  const lockOrientation = async () => {
    try {
      await ScreenOrientation.lockAsync(
        gameViewActive
          ? ScreenOrientation.OrientationLock.DEFAULT
          : ScreenOrientation.OrientationLock.PORTRAIT_UP
      );
    } catch (error) {
      console.warn('Error locking screen orientation:', error);
    }
  };

  lockOrientation();

  if (gameViewActive) {
    // Motion handling setup...
  }
  return;
}, [dispatch, gameViewActive]);

Key features:

  1. Async orientation locking based on game state
  2. Error handling for orientation lock failures
  3. Conditional motion tracking setup

Motion Tracking

When the game view is active, we set up device motion tracking:

if (gameViewActive) {
  const handleMotion = (motionData: DeviceMotionMeasurement) => {
    const orientation = orientationMap[motionData.orientation];
    dispatch(setAppOrientation(orientation));
  };

  DeviceMotion.setUpdateInterval(500);
  const subscription = DeviceMotion.addListener(handleMotion);

  return () => {
    subscription.remove();
    DeviceMotion.setUpdateInterval(0);
  };
}

Notable aspects:

  • 500ms update interval balances responsiveness and performance
  • Clean subscription management
  • Automatic cleanup on component unmount

Integration with Redux

The hook integrates with Redux for state management:

const dispatch = useAppDispatch();
const gameViewActive = useAppSelector(
  (state: RootState) => state.app.gameViewActive
);

This allows:

  • Centralized state management
  • Reactive orientation updates
  • Consistent state across the application

Performance Optimization

Several performance considerations are built in:

configureReanimatedLogger({
  level: ReanimatedLogLevel.warn,
  strict: false,
});
  1. Reanimated logger configuration for debugging
  2. Optimal motion update interval
  3. Efficient cleanup of resources

Use Cases and Benefits

This hook is particularly useful for:

// Game Component
function GameView() {
  useDeviceOrientation();

  return <View>{/* Game content */}</View>;
}

// App Navigation
function AppNavigator() {
  useDeviceOrientation();

  return <Navigator>{/* App routes */}</Navigator>;
}

Benefits include:

  1. Automatic Orientation Management

    • Locks to portrait during normal use
    • Enables rotation during gameplay
    • Handles transitions smoothly
  2. Enhanced User Experience

    • Responsive orientation changes
    • Predictable behavior
    • Smooth transitions
  3. Developer Friendly

    • Simple integration
    • Clear error handling
    • Automatic cleanup

Error Handling

The hook includes robust error handling:

try {
  await ScreenOrientation.lockAsync(/* ... */);
} catch (error) {
  console.warn('Error locking screen orientation:', error);
}

This ensures:

  • Graceful failure handling
  • Debugging information
  • App stability

Real-World Considerations

When implementing this hook, consider:

  1. Battery Impact

    • Motion updates affect battery life
    • 500ms interval is a good balance
    • Cleanup when not needed
  2. Device Compatibility

    • Different devices handle orientation differently
    • Some devices may not support all orientations
    • Fallback handling may be needed
  3. State Management

    • Redux integration for global state
    • Consistent orientation across app
    • Predictable behavior

Future Improvements

Possible enhancements could include:

  1. Customizable Update Interval

    const useDeviceOrientation = (updateInterval = 500) => {
      // ... existing code
      DeviceMotion.setUpdateInterval(updateInterval);
    };
  2. Orientation Change Callbacks

    const useDeviceOrientation = (
      onOrientationChange?: (orientation: DeviceOrientationT) => void
    ) => {
      // ... handle orientation changes
    };
  3. Device-Specific Optimizations

    • Different update intervals per device
    • Platform-specific behavior
    • Performance profiling

Conclusion

The useDeviceOrientation hook demonstrates how to create a robust solution for handling device orientation in React Native games. By combining orientation locking, motion tracking, and state management, we've created a powerful tool that:

  • Enhances user experience
  • Simplifies orientation management
  • Provides consistent behavior
  • Optimizes performance

Whether you're building a mobile game or any app that needs orientation management, this pattern provides a solid foundation for handling device orientation in React Native applications.

Remember to test thoroughly on various devices and platforms, as orientation behavior can vary significantly between different hardware and operating systems.