Turbocharged Tutorials: Building a React Native Car Riding Game from Scratch

Creating a car riding game in React Native can be an exciting project, blending the realms of app development and game design. In this article, we’ll walk through the process of building a basic car riding app using React Native and the React Native Game Engine, illustrating the process with a functional example code that runs both on iOS and Android platforms.

Prerequisites

Before diving into the development, ensure you have the following:

  • A basic understanding of React Native and its components.
  • Node.js and npm installed on your machine.
  • React Native CLI installed, or use Expo if you prefer.
  • An IDE or code editor of your choice (e.g., Visual Studio Code).

Check below mentioned Article for tools and environments required to configure:

Setting Up the Project

Start by creating a new React Native project. You can do this by running the following command in your terminal:

npx react-native init CarRidingApp

Navigate into your project directory:

cd CarRidingApp

Install the required dependencies, including the react-native-game-engine for the game logic, and other libraries for gesture handling and animations:

npm install react-native-game-engine react-native-gesture-handler react-native-reanimated

Structuring the App

The app’s structure is crucial for maintainability and scalability. The provided code is organized as follows:

  • App.tsx: The root component that sets up the game environment.
  • components/: Contains the game engine and the road background components.
  • entities/: Includes the car and obstacles components.
  • styles/: Holds the styling for the game, road, lanes, and car.
  • systems/: Contains the game logic systems like movement, collision, and touch controls.
  • utils/: Includes constants and utility functions for the game.

Building the Game Engine Component

The GameEngineApp component in components/GameEngine.tsx utilizes the GameEngine from react-native-game-engine. It manages the game’s state, including the car entity, obstacles, and the time elapsed. The game’s logic is handled by systems passed to the GameEngine component, including movement, touch control, and collision detection.

Handling Movements and Collisions

The movement and collision systems in the systems/ directory define how entities interact within the game. The MoveSystem updates the position of the car and obstacles based on their velocity, while the CollisionSystem checks for collisions between the car and obstacles, updating the game state accordingly.

Styling the Game

The styles/GameStyles.ts file defines the visual aspects of the game, including the road, lanes, and car. Using StyleSheet.create, it ensures a consistent and performant styling approach.

Running the App

With the code in place, you can run the app on iOS or Android simulators (or actual devices) using the React Native CLI commands:

npx react-native run-ios

or

npx react-native run-android

Ensure you have the appropriate development environment set up for iOS or Android development.

Coding of the App

We will delve into the specifics of each file in the car riding app created with React Native, breaking down the purpose and functionality of the code within.

App.tsx:

This is the entry point of the app, where the main component is defined. It sets up a basic React Native layout with a StatusBar and a SafeAreaView, ensuring the app content is displayed correctly across different devices. The GameEngineApp component, which contains the game’s logic and UI, is rendered inside the SafeAreaView.

import React from 'react';
import { SafeAreaView, StatusBar } from 'react-native';
import GameEngineApp from './components/GameEngine';

const App: React.FC = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView style={{ flex: 1 }}>
        <GameEngineApp />
      </SafeAreaView>
    </>
  );
};

export default App;

utils/Constants.ts:

This file defines several constants used throughout the app, such as screen dimensions, car settings, and game duration. These constants help maintain consistency and make it easier to adjust game settings.

import { Dimensions } from 'react-native';

export const SCREEN_WIDTH = Dimensions.get('window').width;
export const SCREEN_HEIGHT = Dimensions.get('window').height;

export const CAR_START_POSITION = {
  x: SCREEN_WIDTH / 2 - 25,
  y: SCREEN_HEIGHT - 100,
};

export const CAR_VELOCITY = { x: 0, y: -0.1 };

export const RIDE_DURATION = 7000; // Duration in milliseconds

utils/GameUtils.ts:

This utility file includes a function to generate obstacles at random positions on the screen. Each obstacle is represented as an object with a unique key, position, size, and color.

import Obstacle from '../entities/Obstacle';
import { SCREEN_WIDTH, SCREEN_HEIGHT } from './Constants';

export const generateObstacles = (numberOfObstacles: number) => {
  let obstacles = {};
  for (let i = 0; i < numberOfObstacles; i++) {
    const position = {
      x: Math.random() * (SCREEN_WIDTH - 50),
      y: Math.random() * SCREEN_HEIGHT,
    };
    obstacles[`obstacle_${i}`] = Obstacle(position, { width: 50, height: 50 }, 'red');
  }
  return obstacles;
};

styles/GameStyles.ts:

This file contains the styles for the game components, including the game container, road, lanes, and car design elements. It uses StyleSheet.create for better performance and maintainability.

import { StyleSheet } from 'react-native';

export const gameStyles = StyleSheet.create({
  gameContainer: {
    flex: 1,
    backgroundColor: '#fff',
  },
  road: {
    position: 'absolute',
    width: '100%',
    height: '100%',
    backgroundColor: '#333',
    justifyContent: 'center',
    alignItems: 'center',
  },
  lane: {
    height: '100%',
    width: 5,
    backgroundColor: '#fff',
    opacity: 0.5,
  },
});

export const carStyles = StyleSheet.create({
  carBody: {
    width: 50,
    height: 100,
    backgroundColor: 'blue',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  carTop: {
    width: 35,
    height: 25,
    backgroundColor: 'darkblue',
    borderTopLeftRadius: 5,
    borderTopRightRadius: 5,
  },
  carBottom: {
    width: 50,
    height: 50,
    backgroundColor: 'blue',
  },
  wheel: {
    width: 15,
    height: 15,
    borderRadius: 7.5,
    backgroundColor: 'black',
  },
  wheelContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '100%',
    paddingHorizontal: 5,
  },
});

components/GameEngine.tsx:

This component sets up the React Native Game Engine, defines the initial state of the game, and updates the game state based on time and game events. It also initializes the game entities, such as the car and obstacles.

import React, { useState, useEffect } from 'react';
import { View } from 'react-native';
import { GameEngine } from 'react-native-game-engine';
import Car from '../entities/Car';
import MoveSystem from '../systems/MoveSystem';
import TouchControlSystem from '../systems/TouchControlSystem';
import CollisionSystem from '../systems/CollisionSystem';
import { SCREEN_WIDTH, SCREEN_HEIGHT, RIDE_DURATION, CAR_START_POSITION, CAR_VELOCITY } from '../utils/Constants';
import { generateObstacles } from '../utils/GameUtils';
import RoadBackground from './RoadBackground';
import { gameStyles } from '../styles/GameStyles';

const GameEngineApp: React.FC = () => {
  const [timeElapsed, setTimeElapsed] = useState(0);
  const [entities, setEntities] = useState({
    car: Car(CAR_START_POSITION, { width: 50, height: 100 }, 'blue'),
    ...generateObstacles(5),
  });

  useEffect(() => {
    const timer = setInterval(() => {
      setTimeElapsed(prevTime => prevTime + 1000);
    }, 1000);

    if (timeElapsed >= RIDE_DURATION) {
      setTimeElapsed(0);
      setEntities(prevEntities => ({
        ...prevEntities,
        car: Car(CAR_START_POSITION, { width: 50, height: 100 }, 'blue'),
      }));
    }

    return () => clearInterval(timer);
  }, [timeElapsed]);

  return (
    <View style={gameStyles.gameContainer}>
      <RoadBackground />
      <GameEngine
        systems={[MoveSystem(generateObstacles), TouchControlSystem, CollisionSystem]}
        entities={entities}>
        {/* Game view goes here */}
      </GameEngine>
    </View>
  );
};

export default GameEngineApp;

components/RoadBackground.tsx:

This component renders the road background for the game, including the lanes. It uses styles defined in styles/GameStyles.ts.

import React from 'react';
import { View } from 'react-native';
import { gameStyles } from '../styles/GameStyles';

const RoadBackground: React.FC = () => {
  return (
    <View style={gameStyles.road}>
      <View style={gameStyles.lane} />
      <View style={[gameStyles.lane, { marginHorizontal: 20 }]} />
      <View style={gameStyles.lane} />
    </View>
  );
};

export default RoadBackground;

entities/Car.tsx:

This file defines the Car component, including its initial position, velocity, and rendering. The CarDesign component is responsible for rendering the car’s visual appearance based on its position and size.

import React from 'react';
import { View } from 'react-native';
import { CAR_START_POSITION, CAR_VELOCITY } from '../utils/Constants';
import { carStyles } from '../styles/GameStyles';

const Car = () => {
  const carProps = {
    position: CAR_START_POSITION,
    velocity: CAR_VELOCITY,
    size: { width: 50, height: 100 },
  };

  return {
    ...carProps,
    renderer: <CarDesign {...carProps} />,
  };
};

const CarDesign: React.FC = ({ position }) => {
  return (
    <View style={[carStyles.carBody, { left: position.x, top: position.y }]}>
      <View style={carStyles.carTop} />
      <View style={carStyles.wheelContainer}>
        <View style={carStyles.wheel} />
        <View style={carStyles.wheel} />
      </View>
      <View style={carStyles.carBottom} />
      <View style={[carStyles.wheelContainer, { top: 0 }]}>
        <View style={carStyles.wheel} />
        <View style={carStyles.wheel} />
      </View>
    </View>
  );
};

export default Car;

entities/Obstacle.tsx:

This file defines the Obstacle component, including its position, size, and color. The Rectangle component is used for rendering each obstacle.

import React from 'react';
import { View } from 'react-native';

const Obstacle = (position, size = { width: 50, height: 50 }, color = 'red') => {
  return {
    position,
    size,
    color,
    renderer: <Rectangle position={position} size={size} color={color} />,
  };
};

const Rectangle: React.FC = ({ position, size, color }) => {
  return (
    <View
      style={{
        position: 'absolute',
        left: position.x,
        top: position.y,
        width: size.width,
        height: size.height,
        backgroundColor: color,
      }}
    />
  );
};

export default Obstacle;

systems/CollisionSystem.ts:

This system checks for collisions between the car and obstacles. If a collision is detected, the obstacle’s color is changed to black, indicating a hit.

const CollisionSystem = (entities) => {
  const { car } = entities;

  Object.keys(entities).forEach(key => {
    const entity = entities[key];

    if (key.startsWith('obstacle_')) {
      const carWidth = car.size?.width ?? 0;
      const carHeight = car.size?.height ?? 0;
      const entityWidth = entity.size?.width ?? 0;
      const entityHeight = entity.size?.height ?? 0;

      if (car.position.x < entity.position.x + entityWidth &&
          car.position.x + carWidth > entity.position.x &&
          car.position.y < entity.position.y + entityHeight &&
          car.position.y + carHeight > entity.position.y) {
        entity.color = 'black';
      }
    }
  });

  return entities;
};

export default CollisionSystem;

systems/MoveSystem.ts:

This system updates the position of the car and obstacles based on their velocity. It also handles the logic for regenerating obstacles when the car reaches the top of the screen.

import { updatePosition } from './Physics';
import { SCREEN_WIDTH, SCREEN_HEIGHT } from '../utils/Constants';
import { generateObstacles } from '../utils/GameUtils';

const MoveSystem = (generateObstacles) => (entities, { time }) => {
  const { delta } = time;

  Object.keys(entities).forEach(key => {
    const entity = entities[key];

    if (entity.position && entity.velocity) {
      const newPosition = updatePosition(entity.position, entity.velocity, delta);

      if (key === 'car') {
        if (newPosition.y <= 0) {
          entity.position.y = SCREEN_HEIGHT - entity.size.height;
          const newObstacles = generateObstacles(5);
          Object.keys(newObstacles).forEach(obstacleKey => {
            entities[obstacleKey] = newObstacles[obstacleKey];
          });
        } else {
          entity.position.y = newPosition.y;
        }

        if (newPosition.x >= 0 && newPosition.x <= SCREEN_WIDTH - entity.size.width) {
          entity.position.x = newPosition.x;
        }
      }
    }
  });

  return entities;
};

export default MoveSystem;

systems/Physics.ts:

This file includes physics-related functions, such as updating an entity’s position based on its velocity and the time elapsed since the last update.

interface Position {
  x: number;
  y: number;
}

interface Velocity {
  x: number;
  y: number;
}

export const updatePosition = (position: Position, velocity: Velocity, deltaTime: number): Position => {
  return {
    x: position.x + velocity.x * deltaTime,
    y: position.y + velocity.y * deltaTime,
  };
};

systems/TouchControlSystem.ts

This system handles touch inputs, allowing the player to control the car’s horizontal movement. When the player touches the screen, the car’s velocity is updated based on the touch position relative to the screen’s center.

import { SCREEN_WIDTH } from '../utils/Constants';

const TouchControlSystem = (entities, { touches }) => {
  touches.filter(t => t.type === "press").forEach(t => {
    const { pageX } = t.event;
    const center = SCREEN_WIDTH / 2;
    const direction = pageX < center ? -0.05 : 0.05;
    entities.car.velocity.x = direction;
  });

  return entities;
};

export default TouchControlSystem;

By understanding the functionality and purpose of each of these files, you can gain insights into how the car riding app works and explore modifications or enhancements to tailor the game to your preferences.

Conclusion

Creating a car riding app in React Native is a great way to learn game development on mobile platforms. By leveraging React Native’s capabilities and the react-native-game-engine, you can create engaging games with efficient performance. Remember to continually test on both iOS and Android platforms to ensure compatibility and optimize the user experience.

For those interested in exploring the complete code and trying out the app, visit the GitHub repository at https://github.com/mesepith/CarSimulator. This example serves as a foundation, encouraging further exploration and customization to create more complex and engaging games.

Leave a Reply

Your email address will not be published. Required fields are marked *