Advanced
Advanced topics and techniques for using the React 3rd Person Character library.
Custom State Machines
Extending the character state machine with custom states and transitions:
import { createMachine } from 'xstate';
// Custom character state machine
const customCharacterMachine = createMachine({
id: 'customCharacter',
initial: 'idle',
context: {
velocity: new THREE.Vector3(),
rotation: new THREE.Euler(),
isOnGround: true,
health: 100,
stamina: 100
},
states: {
idle: {
on: {
MOVE: 'walking',
JUMP: 'jumping',
ATTACK: 'attacking'
}
},
walking: {
on: {
STOP: 'idle',
RUN: 'running',
JUMP: 'jumping'
}
},
running: {
on: {
STOP: 'idle',
WALK: 'walking',
JUMP: 'jumping'
}
},
jumping: {
on: {
LAND: 'idle',
FALL: 'falling'
}
},
falling: {
on: {
LAND: 'idle'
}
},
attacking: {
on: {
ATTACK_END: 'idle'
}
}
}
});
// Use custom machine in character config
const customCharacterConfig = new CharacterBuilder({ fps: 60 })
.withName('Custom Character')
.withStateMachine(customCharacterMachine)
.withCameraSettings({ radius: 5 })
.withMovement({ movementSpeed: 5 })
.with3DModels('/models/player', PlayerModel, Scenario3D)
.build();
Custom Physics Integration
Integrating custom physics systems:
import { useRapier } from '@react-three/rapier';
function CustomPhysicsCharacter() {
const { rapier, world } = useRapier();
const { characterState } = useActorContext();
// Custom physics body
const rigidBodyRef = useRef();
useFrame(() => {
if (!rigidBodyRef.current) return;
const rigidBody = rigidBodyRef.current;
const velocity = rigidBody.linvel();
// Custom physics logic
if (characterState.matches('walking')) {
const targetVelocity = new THREE.Vector3(0, velocity.y, 5);
rigidBody.setLinvel(targetVelocity, true);
}
// Custom collision handling
const collider = rigidBody.collider(0);
if (collider) {
const contacts = world.contactPairs(rigidBody, collider);
// Handle custom collision logic
}
});
return (
<RAPIER.RigidBody ref={rigidBodyRef}>
<RAPIER.CapsuleCollider args={[0.5, 0.5]} />
<PlayerModel />
</RAPIER.RigidBody>
);
}
Custom Animation Systems
Building custom animation systems:
import { useFrame } from '@react-three/fiber';
function CustomAnimatedCharacter() {
const meshRef = useRef();
const { characterState } = useActorContext();
// Custom animation state
const [animationState, setAnimationState] = useState({
walkCycle: 0,
runCycle: 0,
jumpHeight: 0
});
useFrame((state, delta) => {
if (!meshRef.current) return;
const mesh = meshRef.current;
// Custom walking animation
if (characterState.matches('walking')) {
animationState.walkCycle += delta * 5;
mesh.rotation.y = Math.sin(animationState.walkCycle) * 0.1;
mesh.position.y = Math.abs(Math.sin(animationState.walkCycle * 2)) * 0.1;
}
// Custom running animation
if (characterState.matches('running')) {
animationState.runCycle += delta * 8;
mesh.rotation.y = Math.sin(animationState.runCycle) * 0.15;
mesh.position.y = Math.abs(Math.sin(animationState.runCycle * 3)) * 0.15;
}
// Custom jumping animation
if (characterState.matches('jumping')) {
animationState.jumpHeight = Math.min(animationState.jumpHeight + delta * 10, 2);
mesh.position.y = animationState.jumpHeight;
} else {
animationState.jumpHeight = Math.max(animationState.jumpHeight - delta * 10, 0);
mesh.position.y = animationState.jumpHeight;
}
});
return (
<mesh ref={meshRef}>
<capsuleGeometry args={[0.5, 1, 4, 8]} />
<meshStandardMaterial color="blue" />
</mesh>
);
}
Custom Camera Systems
Implementing custom camera behaviors:
import { useThree } from '@react-three/fiber';
function CustomCamera() {
const { camera } = useThree();
const { characterState } = useActorContext();
const { character } = useActorContext();
// Custom camera logic
useFrame(() => {
const targetPosition = new THREE.Vector3();
const characterPosition = new THREE.Vector3(0, 1, 0); // Get from character
// Calculate camera position based on character state
let cameraDistance = character.camera.radius;
if (characterState.matches('running')) {
cameraDistance *= 1.2; // Further camera when running
}
if (characterState.matches('jumping')) {
cameraDistance *= 0.8; // Closer camera when jumping
}
// Calculate camera position
targetPosition.copy(characterPosition);
targetPosition.y += character.camera.cameraOffsetY;
targetPosition.z += cameraDistance;
// Smooth camera movement
camera.position.lerp(targetPosition, 0.1);
camera.lookAt(characterPosition);
});
return null;
}
// Use custom camera
function App() {
return (
<CharacterConfigProvider character={characterConfig}>
<FIBER.Canvas>
<ExperienceWrapper>
<CustomCamera />
<Scenario />
<Player />
</ExperienceWrapper>
</FIBER.Canvas>
</CharacterConfigProvider>
);
}
Custom Input Systems
Building custom input handling:
import { useEffect, useState } from 'react';
function CustomInputSystem() {
const [customInputs, setCustomInputs] = useState({
mouseX: 0,
mouseY: 0,
mousePressed: false,
gamepad: null
});
// Custom mouse input
useEffect(() => {
const handleMouseMove = (event) => {
setCustomInputs(prev => ({
...prev,
mouseX: (event.clientX / window.innerWidth) * 2 - 1,
mouseY: -(event.clientY / window.innerHeight) * 2 + 1
}));
};
const handleMouseDown = () => {
setCustomInputs(prev => ({ ...prev, mousePressed: true }));
};
const handleMouseUp = () => {
setCustomInputs(prev => ({ ...prev, mousePressed: false }));
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
// Custom gamepad input
useEffect(() => {
const handleGamepadConnected = (event) => {
setCustomInputs(prev => ({ ...prev, gamepad: event.gamepad }));
};
const handleGamepadDisconnected = () => {
setCustomInputs(prev => ({ ...prev, gamepad: null }));
};
window.addEventListener('gamepadconnected', handleGamepadConnected);
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);
return () => {
window.removeEventListener('gamepadconnected', handleGamepadConnected);
window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected);
};
}, []);
// Process gamepad input
useFrame(() => {
if (customInputs.gamepad) {
const gamepad = navigator.getGamepads()[customInputs.gamepad.index];
if (gamepad) {
// Process gamepad axes and buttons
const leftStickX = gamepad.axes[0];
const leftStickY = gamepad.axes[1];
const rightStickX = gamepad.axes[2];
const rightStickY = gamepad.axes[3];
// Apply gamepad input to character
// This would integrate with your character movement system
}
}
});
return null;
}
Performance Optimization Techniques
Advanced performance optimization:
import { Suspense, useMemo } from 'react';
function OptimizedCharacter() {
// Memoize expensive calculations
const characterConfig = useMemo(() => {
return new CharacterBuilder({ fps: 60 })
.withName('Optimized Player')
.withCameraSettings({ radius: 5 })
.withMovement({ movementSpeed: 5 })
.with3DModels('/models/player', PlayerModel, Scenario3D)
.build();
}, []);
// Lazy load components
const LazyPlayerModel = useMemo(() => {
return React.lazy(() => import('./PlayerModel'));
}, []);
return (
<CharacterConfigProvider character={characterConfig}>
<FIBER.Canvas
gl={{
powerPreference: "high-performance",
antialias: false,
stencil: false,
depth: false
}}
shadows={false}
dpr={[1, 2]} // Responsive pixel ratio
>
<Suspense fallback={<LoadingSpinner />}>
<ExperienceWrapper>
<Scenario />
<Player />
</ExperienceWrapper>
</Suspense>
</FIBER.Canvas>
</CharacterConfigProvider>
);
}
// Optimized model loading
function OptimizedPlayerModel() {
const { scene } = useGLTF('/models/player.glb');
// Optimize geometry
useEffect(() => {
if (scene) {
scene.traverse((child) => {
if (child.isMesh) {
child.geometry.computeBoundingSphere();
child.geometry.computeBoundingBox();
child.frustumCulled = true;
}
});
}
}, [scene]);
return <primitive object={scene} />;
}
Custom Shaders and Materials
Implementing custom visual effects:
import { shaderMaterial } from '@react-three/drei';
// Custom shader material
const CustomCharacterMaterial = shaderMaterial(
{
time: 0,
color: new THREE.Color(0.2, 0.0, 0.1),
scale: 1.0
},
// Vertex shader
`
uniform float time;
uniform float scale;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vUv = uv;
vPosition = position;
vec3 pos = position;
pos.y += sin(time + position.x * 10.0) * 0.1 * scale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
// Fragment shader
`
uniform float time;
uniform vec3 color;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vec3 finalColor = color;
finalColor += sin(time + vUv.x * 10.0) * 0.1;
gl_FragColor = vec4(finalColor, 1.0);
}
`
);
// Extend Three.js with custom material
extend({ CustomCharacterMaterial });
function CustomShadedCharacter() {
const materialRef = useRef();
useFrame((state) => {
if (materialRef.current) {
materialRef.current.time = state.clock.elapsedTime;
}
});
return (
<mesh>
<capsuleGeometry args={[0.5, 1, 4, 8]} />
<customCharacterMaterial ref={materialRef} />
</mesh>
);
}
Network Integration
Integrating with multiplayer systems:
import { useEffect, useRef } from 'react';
function NetworkedCharacter() {
const { characterState } = useActorContext();
const socketRef = useRef();
const lastUpdateRef = useRef(0);
// Connect to network
useEffect(() => {
socketRef.current = new WebSocket('ws://localhost:3001');
socketRef.current.onopen = () => {
console.log('Connected to game server');
};
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle incoming character updates
if (data.type === 'characterUpdate') {
// Update character position/state from network
}
};
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
// Send character updates
useFrame((state) => {
if (socketRef.current && state.clock.elapsedTime - lastUpdateRef.current > 0.05) {
const characterData = {
type: 'characterUpdate',
state: characterState.value,
position: [0, 1, 0], // Get from character
rotation: [0, 0, 0] // Get from character
};
socketRef.current.send(JSON.stringify(characterData));
lastUpdateRef.current = state.clock.elapsedTime;
}
});
return null;
}
Debugging and Development Tools
Development utilities:
import { useThree } from '@react-three/fiber';
function DebugTools() {
const { scene, camera } = useThree();
const { characterState } = useActorContext();
// Debug UI
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log('Character State:', characterState.value);
console.log('Scene Objects:', scene.children.length);
console.log('Camera Position:', camera.position);
}
}, [characterState, scene, camera]);
// Debug helpers
return (
<group>
{/* Grid helper */}
<gridHelper args={[20, 20]} />
{/* Axes helper */}
<axesHelper args={[5]} />
{/* Camera helper */}
<cameraHelper args={[camera]} />
</group>
);
}
// Performance monitoring
function PerformanceMonitor() {
const [fps, setFps] = useState(0);
const frameCountRef = useRef(0);
const lastTimeRef = useRef(0);
useFrame((state) => {
frameCountRef.current++;
if (state.clock.elapsedTime - lastTimeRef.current >= 1) {
setFps(frameCountRef.current);
frameCountRef.current = 0;
lastTimeRef.current = state.clock.elapsedTime;
}
});
return (
<div className="performance-monitor">
<div>FPS: {fps}</div>
<div>Draw Calls: {performance.memory?.usedJSHeapSize || 'N/A'}</div>
</div>
);
}
These advanced techniques allow you to extend the library's functionality and create more complex character systems tailored to your specific needs.