React Native

Master React Native Three: 3D Player & Camera Control 2025

Unlock immersive 3D experiences in your mobile apps! Learn to master player movement and dynamic camera controls in React Native using React Three Fiber in 2025.

A

Alejandro Vargas

Creative Technologist specializing in immersive mobile experiences with React Native and Three.js.

8 min read12 views

Level Up Your Mobile Apps: From Flat UIs to Immersive 3D Worlds

For years, mobile app development has been a world of flat lists, clean buttons, and predictable navigation. It’s a world we’ve mastered. But the horizon is shifting. Users crave more engaging, memorable experiences, and the technology to deliver them on mobile has finally come of age. Welcome to the era of 3D in React Native.

Forget the steep learning curves and performance nightmares of the past. With the power of React Three Fiber (R3F), creating stunning, interactive 3D scenes inside your React Native apps is not just possible—it’s intuitive and fun. It leverages your existing React knowledge to build complex scenes declaratively.

In this guide, we're not just dipping our toes in the water. We’re diving deep. By the end, you will have built a foundational 3D experience from scratch: a controllable player character with a smooth, dynamic third-person camera. This is the cornerstone of countless 3D games and interactive experiences. Let's build something amazing.

The Foundation: Setting Up Your 3D World

Before we can run, we must walk. And before we walk, we must build the ground beneath our feet. Our setup will use a few key libraries from the Poimandres ecosystem, the brilliant minds behind R3F.

Our Toolkit for 2025:

  • @react-three/fiber: The React renderer for Three.js. This is our core engine.
  • @react-three/drei: A treasure trove of helpers and abstractions for R3F. We’ll use it for shapes and camera utilities.
  • @react-three/rapier: A powerful physics engine wrapper for R3F. This will give our player and world a sense of weight and collision.

First, get these installed in your Expo or React Native CLI project:

npm install three @react-three/fiber @react-three/drei @react-three/rapier

With our dependencies ready, let's create the main canvas for our 3D world. We'll set up a simple scene with a floor and some basic lighting.

import { Canvas } from '@react-three/fiber/native';
import { Physics } from '@react-three/rapier';

const Floor = () => {
  return (
    
      
      
    
  );
};

export default function App() {
  return (
    
      
      
      
        
        {/* Our Player will go here soon! */}
      
    
  );
}

What’s happening here? The <Canvas> component creates a full-screen WebGL context. Inside, we add some ambient light for general illumination and a directional light to simulate the sun, casting some shadows. We wrap our scene elements in the <Physics> component from Rapier, which enables collision detection and physical simulation for everything inside it.

Bringing Your Player to Life

An empty world is no fun. Let's create our protagonist. We won't worry about a fancy 3D model just yet; a simple geometric shape is perfect for nailing down the physics and controls.

A Body and a Soul (Physics)

To make our player interact with the world (i.e., not fall through the floor), it needs a physical body. We'll use Rapier's RigidBody and CapsuleCollider components. A capsule is a fantastic choice for player characters because its rounded bottom prevents it from getting snagged on small bumps in the geometry.

Let's create a Player component.

import { RigidBody, CapsuleCollider } from '@react-three/rapier';

const Player = () => {
  return (
    
      
        
        
      
      
    
  );
};

Let's break down the RigidBody props:

Advertisement
  • position={[0, 5, 0]}: We spawn the player 5 units up so it has a moment to fall and settle onto the floor.
  • colliders={false}: We tell the RigidBody not to auto-generate a collider from the mesh's geometry.
  • enabledRotations={[false, false, false]}: This is crucial! It locks the player's rotation, so it doesn't tip over like a domino when it moves.

We then manually define a more efficient <CapsuleCollider>. Now, if you add <Player /> inside your <Physics> component, you'll see a blue capsule fall from the sky and land squarely on your green floor. We have life!

The Heart of the Game: Implementing Player Controls

A static character isn't a player. It's a statue. To give the player agency, we need to translate their touch on the screen into movement in the 3D world. We'll simulate a simple virtual joystick.

Listening to the User

We'll use React's state to keep track of the user's input. Imagine a joystick on the left side of the screen. When the user drags their finger, we'll store the direction and magnitude of that drag.

For simplicity, we'll manage this logic directly in our main `App` component and pass the input down to the `Player`. We can use React Native's `PanResponder` for this, but to keep the code focused, let's represent the input with a simple state object.

In a real app, you'd use `PanResponder` or a library like `react-native-gesture-handler` to update this state based on touch events.

From Input to Impulse

Now, how do we make the player move? We'll use R3F's `useFrame` hook. This hook executes a function on every single rendered frame, making it the perfect place for game logic.

Inside `useFrame`, we'll check our input state and apply a force to the player's `RigidBody`. To do this, we need a reference to the body's API.

Let's update our `Player` component:

import { useRef, useEffect } from 'react';
import { useFrame } from '@react-three/fiber/native';
import { RigidBody, CapsuleCollider } from '@react-three/rapier';
import * as THREE from 'three';

// Assume `input` is a state like { x: 0, y: 0 } passed as a prop
const Player = ({ input }) => {
  const playerRef = useRef();
  const moveSpeed = 5;
  const moveDirection = new THREE.Vector3();

  useFrame((state, delta) => {
    if (!playerRef.current) return;

    // Calculate movement direction based on input
    moveDirection.set(input.x, 0, -input.y).normalize();

    // Get current velocity
    const currentVelocity = playerRef.current.linvel();

    // Calculate new velocity
    const newVelocity = {
      x: moveDirection.x * moveSpeed,
      y: currentVelocity.y, // Preserve gravity's effect
      z: moveDirection.z * moveSpeed,
    };

    // Apply new velocity
    playerRef.current.setLinvel(newVelocity, true);
  });

  return (
    
      {/* ...mesh and collider */}
    
  );
};

Here's the magic: We use `useRef` to get a direct handle on the `RigidBody`. In `useFrame`, we create a `Vector3` representing our desired movement direction from the `input` prop. We then set the player's linear velocity (`linvel`) directly. We're careful to preserve the existing `y` velocity so that gravity still works correctly. Now, by updating the `input` state in our parent component, our player will glide across the floor!

Seeing the Action: Camera Control

Our player can move, but our camera is stuck in one place. A great camera system is what separates a clunky prototype from a fluid, professional experience. Our goal is a smooth, third-person camera that follows the player.

The Classic Third-Person Follow Cam

The camera shouldn't be rigidly attached to the player; that can feel nauseating. Instead, it should smoothly *follow* the player, like a drone operator tracking a moving subject. We'll achieve this by interpolating the camera's position over time using a technique called `lerping` (linear interpolation).

Let's create a dedicated `CameraManager` component.

import { useFrame, useThree } from '@react-three/fiber/native';
import * as THREE from 'three';

// playerRef is the ref to the player's RigidBody
const CameraManager = ({ playerRef }) => {
  const { camera } = useThree();
  const cameraOffset = new THREE.Vector3(0, 5, 10);
  const targetPosition = new THREE.Vector3();
  const smoothedPosition = new THREE.Vector3();

  useFrame((state, delta) => {
    if (!playerRef.current) return;

    // 1. Calculate the ideal camera position
    const playerPosition = playerRef.current.translation();
    targetPosition.set(playerPosition.x, playerPosition.y, playerPosition.z).add(cameraOffset);

    // 2. Smoothly interpolate the camera's current position to the target
    smoothedPosition.lerp(targetPosition, 0.1); // The 0.1 is the 'lag' factor
    camera.position.copy(smoothedPosition);

    // 3. Make the camera look at the player
    camera.lookAt(playerPosition.x, playerPosition.y + 1, playerPosition.z);
  });

  return null; // This component doesn't render anything itself
};

This component is pure logic. In `useFrame`, it does three things:

  1. It gets the player's current position from the physics body.
  2. It calculates the ideal camera position by adding an `offset` (behind and above the player).
  3. It uses `lerp` to move the camera's position a small fraction (10% in this case) of the way towards the target on each frame. This creates that beautiful, smooth lag.
  4. Finally, it ensures the camera is always pointing at the player.

Add this component to your main scene, passing it the `playerRef`, and watch as your camera gracefully follows your every move.

A Quick Look at Other Perspectives

While our follow-cam is perfect for a third-person game, it's good to know the alternatives.

Camera TypeImplementationBest For
First-PersonParent the camera directly to the player object. The camera moves and rotates exactly as the player does.Immersive FPS-style games where you see through the character's eyes.
Orbit ControlsUse <OrbitControls /> from @react-three/drei.Debugging, model viewers, or strategy games where the user needs free control to inspect the scene.
Third-Person FollowOur custom lerp-based solution in useFrame.Action-adventure games, platformers; provides situational awareness while feeling dynamic.

Putting It All Together

Now, let's see how all our components work in harmony in our main `App.js`.

import { Canvas } from '@react-three/fiber/native';
import { Physics } from '@react-three/rapier';
import { useRef, useState } from 'react';
// ...import your Player, Floor, and CameraManager components

export default function App() {
  const playerRef = useRef();
  // In a real app, this state would be updated by a virtual joystick
  const [input, setInput] = useState({ x: 0, y: 0 });

  return (
    <> 
      {/* Your UI for the virtual joystick would go here */}
      
        
        
         {/* Use 'debug' prop to see colliders! */} 
          
          
        
        
      
    
  );
}

We've assembled our world. The `Canvas` holds everything. `Physics` governs the rules. The `Floor` provides the ground. The `Player` moves based on `input`. And the `CameraManager` ensures we always have the best view of the action. Notice the `debug` prop on the `Physics` component—this is an invaluable tool that draws wireframes of all your colliders.

Your Adventure Begins

You’ve done it. You’ve built the core of an interactive 3D application in React Native. You have a physics-enabled character that responds to input and a dynamic, cinematic camera system that follows the action. This is no small feat.

From here, the possibilities are endless. You can swap the simple capsule for an animated 3D model using `gltf-jsx`. You can build out more complex environments, add enemies, or implement game mechanics. You have the foundational skills to start building the immersive mobile experiences of the future.

The world of 3D on mobile is your canvas. Go create.

Tags

You May Also Like