Mapbox ์ค์๊ฐ ์ง๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Three.js 3D ์บ๋ฆญํฐ๊ฐ ๋๋ก๋ฅผ ๋ฐ๋ผ ์์ง์ด๋ ๋ค๋น๊ฒ์ด์ ์คํ์ผ์ 3D ๊ฒ์์ ๋๋ค.
- โ Mapbox GL JS + Three.js ์์ ํตํฉ
- โ GPS ๊ธฐ๋ฐ ์ค์๊ฐ ์บ๋ฆญํฐ ์ด๋
- โ ๋ค๋น๊ฒ์ด์ ์นด๋ฉ๋ผ (์นด์นด์ค๋งต/๊ตฌ๊ธ ๋ค๋น ์คํ์ผ)
- โ Mapbox Directions API ๊ฒฝ๋ก ๊ณ์ฐ
- โ ๋ฌผ๋ฆฌ ์์ง ๊ธฐ๋ฐ ์ด๋ (Rapier.js)
- โ glTF ์ ๋๋ฉ์ด์ ์์คํ
- โ ๋ชจ๋ฐ์ผ ์ต์ ํ
- โ TypeScript + React ๊ธฐ๋ฐ
# ์ ์ฅ์ ํด๋ก
git clone <repository-url>
cd mapbox-3d-game
# ์์กด์ฑ ์ค์น
npm install
# ๋๋
yarn install# .env ํ์ผ ์์ฑ
cp .env.example .env
# .env ํ์ผ ํธ์ง
REACT_APP_MAPBOX_TOKEN=your_token_hereMapbox Account์์ ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ผ์ธ์.
npm run dev๋ธ๋ผ์ฐ์ ์์ http://localhost:3000 ์ ์
npm run build
npm run previewsrc/
โโโ core/ # ํต์ฌ ์์ง
โ โโโ map/
โ โ โโโ MapboxManager.ts # Mapbox ๊ด๋ฆฌ
โ โโโ three/
โ โ โโโ ThreeManager.ts # Three.js ๋ ๋๋ฌ
โ โโโ physics/
โ โโโ PhysicsManager.ts # ๋ฌผ๋ฆฌ ์์ง
โ
โโโ systems/ # ๊ฒ์ ์์คํ
โ โโโ camera/
โ โ โโโ CameraSystem.ts # ๋ค๋น๊ฒ์ด์
์นด๋ฉ๋ผ
โ โโโ route/
โ โ โโโ RouteSystem.ts # ๊ฒฝ๋ก ์์คํ
โ โโโ player/
โ โ โโโ PlayerSystem.ts # ์บ๋ฆญํฐ ์์คํ
โ โโโ animation/
โ โโโ AnimationSystem.ts # ์ ๋๋ฉ์ด์
โ
โโโ ui/ # React ์ปดํฌ๋ํธ
โ โโโ GameManager.tsx # ๋ฉ์ธ ๊ฒ์ ๋ฃจํ
โ โโโ HUD.tsx # UI ์์
โ
โโโ utils/ # ์ ํธ๋ฆฌํฐ
โ โโโ coordinates.ts # ์ขํ ๋ณํ
โ โโโ math.ts # ์ํ ํจ์
โ โโโ config.ts # ์ค์
โ
โโโ types/
โ โโโ index.ts # TypeScript ์ธํฐํ์ด์ค
โ
โโโ main.tsx # Entry point
- LngLat: [๊ฒฝ๋, ์๋] - GPS ์ขํ
- Mercator: Mapbox ์ ๊ทํ ์ขํ (0~1)
- World: Three.js 3D ์ขํ (๋ฏธํฐ)
// ๋ณํ ์์
const lngLat: LngLat = [127.1086, 37.4050];
const coordinator = mapboxManager.getCoordinateConverter();
const worldPos: Vector3 = coordinator.lngLatToWorld(lngLat);requestAnimationFrame(gameLoop);
function gameLoop(time) {
deltaTime = (time - lastTime) / 1000;
// 1. GPS ์
๋ ฅ
playerSystem.update(gpsPosition, deltaTime);
// 2. ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
physicsWorld.step(deltaTime);
// 3. ์นด๋ฉ๋ผ ์
๋ฐ์ดํธ
cameraSystem.update(playerPos, playerHeading, deltaTime);
// 4. ๋ ๋๋ง
renderer.render(scene, camera);
}์นด๋ฉ๋ผ๋ ํญ์ ์บ๋ฆญํฐ ๋ค์ ์์นํ๋ฉฐ ์ ๋๋ก๋ฅผ ๋ฐ๋ผ๋ด ๋๋ค:
์นด๋ฉ๋ผ ์์น = ์บ๋ฆญํฐ ์์น + {
x: -sin(heading) * distance (๋ค์ชฝ)
y: height (์์ชฝ)
z: -cos(heading) * distance (๋ค์ชฝ)
}
LookAt ๋์ = ์บ๋ฆญํฐ ์์น + {
x: sin(heading) * lookahead (์)
y: 1.5 (๋ ๋์ด)
z: cos(heading) * lookahead (์)
}
const routeSystem = new RouteSystem(coordinator, mapboxToken);
// ๊ฒฝ๋ก ๋ก๋
const route = await routeSystem.loadRoute(
[127.1086, 37.4050], // ์์
[127.1100, 37.4040] // ๋์ฐฉ
);
// 3D ์๊ฐํ
const routeMesh = routeSystem.visualizeRoute(route);
scene.add(routeMesh);
// ๋ค์ ์จ์ดํฌ์ธํธ ๊ณ์ฐ
const nextWaypoint = routeSystem.getNextWaypoint(
playerWorldPos,
route,
10 // 10m ์๋ณด๊ธฐ
);const playerSystem = new PlayerSystem(coordinator);
// ๋ชจ๋ธ ๋ก๋
const characterModel = await playerSystem.initialize(
'models/character.gltf'
);
// ๊ฒ์ ๋ฃจํ์์ ์
๋ฐ์ดํธ
playerSystem.update(gpsPosition, deltaTime);
// ์ํ ํ์ธ
const state = playerSystem.getState();
console.log(state.animationState); // 'idle' | 'walk' | 'run'const camera = threeManager.getCamera();
const cameraSystem = new CameraSystem(camera);
// ๊ฒ์ ๋ฃจํ์์
cameraSystem.update(playerWorldPos, playerHeading, deltaTime);
// ์นด๋ฉ๋ผ ์ค์ ์กฐ์
cameraSystem.setCameraOffset({
distance: 5.0, // ๋ ๋ฉ๋ฆฌ
height: 2.5, // ๋ ๋์ด
lookAheadDistance: 15.0 // ๋ ์๋ณด๊ธฐ
});import { getMobileOptimizedConfig } from './utils/config';
if (isMobileDevice()) {
const config = getMobileOptimizedConfig();
threeManager.initialize(container, config.render);
}- Frustum Culling: ํ๋ฉด ๋ฐ ์ค๋ธ์ ํธ ๋ ๋๋ง ์คํต
- Instancing: ๊ฐ์ ํํ ์ค๋ธ์ ํธ ๋ฐฐ์น ๋ ๋๋ง
- ๋ฌผ๋ฆฌ Tick ์กฐ์ : 30fps๋ก ๋ฌผ๋ฆฌ ๊ณ์ฐ (๋ ๋๋ 60fps)
- ์ขํ ์บ์ฑ: ๋ฐ๋ณต ๋ณํ ์บ์
- ํด์๋ ๋์ ์กฐ์ : FPS์ ๋ฐ๋ผ ํ๋ฉด ํด์๋ ์กฐ์
์์ธํ ์ต์ ํ ๊ฐ์ด๋๋ OPTIMIZATION.md๋ฅผ ์ฐธ๊ณ ํ์ธ์.
// ์ค์ GPS ๋ฐ์ดํฐ ์ฌ์ฉ
navigator.geolocation.watchPosition(position => {
const {latitude, longitude} = position.coords;
playerSystem.update([longitude, latitude], deltaTime);
// ์ง๋ ์ค์ฌ ์ถ์
mapboxManager.setCenter([longitude, latitude], true);
});class GPSFilter {
private kalmanX = new KalmanFilter();
private kalmanY = new KalmanFilter();
filter(lngLat: LngLat): LngLat {
return [
this.kalmanX.update(lngLat[0]),
this.kalmanY.update(lngLat[1])
];
}
}// config.ts์์
export const DEBUG_CONFIG = {
enabled: true,
showCoordinateAxis: true,
showCameraCone: true,
showPhysicsDebug: true,
showPathDebug: true,
logPerformanceMetrics: true
};const fps = threeManager.getFPS();
console.log(`FPS: ${fps}`);
const cacheSize = coordinator.getCacheSize();
console.log(`์ขํ ์บ์: ${cacheSize}`);<!-- index.html -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="
width=device-width,
initial-scale=1.0,
user-scalable=no,
viewport-fit=cover
" />// Passive event listeners
container.addEventListener('touchmove', onTouchMove, {passive: true});
// ๋ฐฑ๊ทธ๋ผ์ด๋ ์ผ์ ์ค์ง
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopGameLoop();
} else {
startGameLoop();
}
});class MapboxManager {
initialize(container: HTMLElement): Promise<void>
getCoordinateConverter(): CoordinateConverter
getCenter(): LngLat
setCenter(lngLat: LngLat, animate?: boolean): void
setZoom(zoom: number, animate?: boolean): void
dispose(): void
}class ThreeManager {
initialize(container: HTMLElement): void
getScene(): THREE.Scene
getRenderer(): THREE.WebGLRenderer
getCamera(): THREE.PerspectiveCamera
render(): void
getFPS(): number
dispose(): void
}class PlayerSystem {
initialize(gltfPath: string): Promise<CharacterModel>
update(gpsPosition: LngLat, deltaTime: number): void
getState(): PlayerState
getMesh(): THREE.Group | null
getWorldPosition(): Vector3
getHeading(): number
}class CameraSystem {
update(playerPos: Vector3, playerHeading: number, deltaTime: number): void
setCameraOffset(config: Partial<CameraConfig>): void
getCamera(): THREE.PerspectiveCamera
}class RouteSystem {
loadRoute(start: LngLat, end: LngLat): Promise<Route>
visualizeRoute(route: Route): THREE.Mesh
findClosestPointOnRoute(pos: Vector3, route: Route): {point, index}
getNextWaypoint(pos: Vector3, route: Route, lookahead: number): Vector3
isRouteComplete(pos: Vector3, route: Route): boolean
}# .env
VITE_MAPBOX_TOKEN=pk.eyJ1IjoieW91ciIsImEiOiJjbXBmNHpld...// ์ง์ํ๋ ํ๋กํ
'driving' // ์๋์ฐจ
'walking' // ๋ณดํ์
'cycling' // ์์ ๊ฑฐ
'driving-traffic' // ๊ตํต ์ ๋ณด ํฌํจconst characterModelPath = 'models/my-character.gltf';
const gameManager = <GameManager
characterModelPath={characterModelPath}
// ...
/>;const mapboxConfig = {
...DEFAULT_MAPBOX_CONFIG,
style: 'mapbox://styles/mapbox/satellite-v9' // ์์ฑ์ง๋
};const DEFAULT_CAMERA_CONFIG = {
distance: 6.0, // ๋ ๋ฉ๋ฆฌ (๊ธฐ๋ณธ: 4.0)
height: 3.0, // ๋ ๋์ด (๊ธฐ๋ณธ: 2.0)
lookAheadDistance: 20.0
};- ์ ์ฒด ์ค๊ณ ๊ฐ์ด๋
- ์ขํ ๋ณํ ๊ณต์
- ๋ ๋๋ง ๋ฐฉ์ ๋ถ์
- ์ต์ ํ ์ ๋ต
- Three.js ๋ฌธ์
- Mapbox GL JS ๋ฌธ์
๋ฒ๊ทธ ๋ฆฌํฌํธ๋ ๊ธฐ๋ฅ ์ ์์ Issue๋ฅผ ํตํด ์ ์ถํด์ฃผ์ธ์.
MIT License
- NPC ์์คํ
- ์งํ ์ฝ๋ผ์ด๋
- 3D ์ํฅ
- ๋ ์จ ์์คํ
- ํ๋ฃจ/๋ฐค ์ฌ์ดํด
- ๋ฉํฐํ๋ ์ด์ด (WebSocket)
- ์์ ์์คํ
- ๋ฏธ์ /ํ์คํธ
- ์์ดํ ์์ง
๋ ์ด์์ ๋์์ด ํ์ํ๋ฉด GitHub Discussions์์ ์ง๋ฌธํด์ฃผ์ธ์!
Happy Coding! ๐