Skip to content

Repository files navigation

๐Ÿ—บ๏ธ ํ˜„์‹ค ์ง€๋„ ๊ธฐ๋ฐ˜ 3D ๊ฒŒ์ž„ - ์™„์ „ ๊ฐ€์ด๋“œ

๐Ÿ“š ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

Mapbox ์‹ค์‹œ๊ฐ„ ์ง€๋„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Three.js 3D ์บ๋ฆญํ„ฐ๊ฐ€ ๋„๋กœ๋ฅผ ๋”ฐ๋ผ ์›€์ง์ด๋Š” ๋„ค๋น„๊ฒŒ์ด์…˜ ์Šคํƒ€์ผ์˜ 3D ๊ฒŒ์ž„์ž…๋‹ˆ๋‹ค.

โœจ ์ฃผ์š” ํŠน์ง•

  • โœ… Mapbox GL JS + Three.js ์™„์ „ ํ†ตํ•ฉ
  • โœ… GPS ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์บ๋ฆญํ„ฐ ์ด๋™
  • โœ… ๋„ค๋น„๊ฒŒ์ด์…˜ ์นด๋ฉ”๋ผ (์นด์นด์˜ค๋งต/๊ตฌ๊ธ€ ๋„ค๋น„ ์Šคํƒ€์ผ)
  • โœ… Mapbox Directions API ๊ฒฝ๋กœ ๊ณ„์‚ฐ
  • โœ… ๋ฌผ๋ฆฌ ์—”์ง„ ๊ธฐ๋ฐ˜ ์ด๋™ (Rapier.js)
  • โœ… glTF ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์Šคํ…œ
  • โœ… ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”
  • โœ… TypeScript + React ๊ธฐ๋ฐ˜

๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘

1๏ธโƒฃ ์„ค์น˜

# ์ €์žฅ์†Œ ํด๋ก 
git clone <repository-url>
cd mapbox-3d-game

# ์˜์กด์„ฑ ์„ค์น˜
npm install

# ๋˜๋Š”
yarn install

2๏ธโƒฃ Mapbox Token ์„ค์ •

# .env ํŒŒ์ผ ์ƒ์„ฑ
cp .env.example .env

# .env ํŒŒ์ผ ํŽธ์ง‘
REACT_APP_MAPBOX_TOKEN=your_token_here

Mapbox Account์—์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์œผ์„ธ์š”.

3๏ธโƒฃ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰

npm run dev

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:3000 ์ ‘์†

4๏ธโƒฃ ๋นŒ๋“œ

npm run build
npm run preview

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ 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

๐ŸŽฎ ํ•ต์‹ฌ ๊ฐœ๋…

1. ์ขŒํ‘œ ๋ณ€ํ™˜

  • 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);

2. ๊ฒŒ์ž„ ๋ฃจํ”„

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);
}

3. ๋„ค๋น„๊ฒŒ์ด์…˜ ์นด๋ฉ”๋ผ

์นด๋ฉ”๋ผ๋Š” ํ•ญ์ƒ ์บ๋ฆญํ„ฐ ๋’ค์— ์œ„์น˜ํ•˜๋ฉฐ ์•ž ๋„๋กœ๋ฅผ ๋ฐ”๋ผ๋ด…๋‹ˆ๋‹ค:

์นด๋ฉ”๋ผ ์œ„์น˜ = ์บ๋ฆญํ„ฐ ์œ„์น˜ + {
  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);
}

์ฃผ์š” ์ตœ์ ํ™” ์ „๋žต

  1. Frustum Culling: ํ™”๋ฉด ๋ฐ– ์˜ค๋ธŒ์ ํŠธ ๋ Œ๋”๋ง ์Šคํ‚ต
  2. Instancing: ๊ฐ™์€ ํ˜•ํƒœ ์˜ค๋ธŒ์ ํŠธ ๋ฐฐ์น˜ ๋ Œ๋”๋ง
  3. ๋ฌผ๋ฆฌ Tick ์กฐ์ ˆ: 30fps๋กœ ๋ฌผ๋ฆฌ ๊ณ„์‚ฐ (๋ Œ๋”๋Š” 60fps)
  4. ์ขŒํ‘œ ์บ์‹ฑ: ๋ฐ˜๋ณต ๋ณ€ํ™˜ ์บ์‹œ
  5. ํ•ด์ƒ๋„ ๋™์  ์กฐ์ ˆ: FPS์— ๋”ฐ๋ผ ํ™”๋ฉด ํ•ด์ƒ๋„ ์กฐ์ •

์ž์„ธํ•œ ์ตœ์ ํ™” ๊ฐ€์ด๋“œ๋Š” OPTIMIZATION.md๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.


๐ŸŽฌ ์‹ค์‹œ๊ฐ„ GPS ํ†ตํ•ฉ

GPS ์ž…๋ ฅ ์˜ˆ์‹œ

// ์‹ค์ œ GPS ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
navigator.geolocation.watchPosition(position => {
  const {latitude, longitude} = position.coords;
  
  playerSystem.update([longitude, latitude], deltaTime);
  
  // ์ง€๋„ ์ค‘์‹ฌ ์ถ”์ 
  mapboxManager.setCenter([longitude, latitude], true);
});

Kalman Filter๋กœ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ (๊ถŒ์žฅ)

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}`);

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ง€์›

iOS/Android ํ˜ธํ™˜์„ฑ

<!-- 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();
  }
});

๐Ÿ”ง API ๋ฌธ์„œ

MapboxManager

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
}

ThreeManager

class ThreeManager {
  initialize(container: HTMLElement): void
  getScene(): THREE.Scene
  getRenderer(): THREE.WebGLRenderer
  getCamera(): THREE.PerspectiveCamera
  render(): void
  getFPS(): number
  dispose(): void
}

PlayerSystem

class PlayerSystem {
  initialize(gltfPath: string): Promise<CharacterModel>
  update(gpsPosition: LngLat, deltaTime: number): void
  getState(): PlayerState
  getMesh(): THREE.Group | null
  getWorldPosition(): Vector3
  getHeading(): number
}

CameraSystem

class CameraSystem {
  update(playerPos: Vector3, playerHeading: number, deltaTime: number): void
  setCameraOffset(config: Partial<CameraConfig>): void
  getCamera(): THREE.PerspectiveCamera
}

RouteSystem

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
}

๐ŸŒ Mapbox Directions API

API ํ‚ค ์„ค์ •

# .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
};

๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ


๐Ÿค ๊ธฐ์—ฌํ•˜๊ธฐ

๋ฒ„๊ทธ ๋ฆฌํฌํŠธ๋‚˜ ๊ธฐ๋Šฅ ์ œ์•ˆ์€ Issue๋ฅผ ํ†ตํ•ด ์ œ์ถœํ•ด์ฃผ์„ธ์š”.


๐Ÿ“„ ๋ผ์ด์„ ์Šค

MIT License


๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„

Phase 2 ๊ฐœ์„  ์‚ฌํ•ญ

  • NPC ์‹œ์Šคํ…œ
  • ์ง€ํ˜• ์ฝœ๋ผ์ด๋”
  • 3D ์Œํ–ฅ
  • ๋‚ ์”จ ์‹œ์Šคํ…œ
  • ํ•˜๋ฃจ/๋ฐค ์‚ฌ์ดํด

Phase 3 ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

  • ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด (WebSocket)
  • ์ƒ์  ์‹œ์Šคํ…œ
  • ๋ฏธ์…˜/ํ€˜์ŠคํŠธ
  • ์•„์ดํ…œ ์ˆ˜์ง‘

๐Ÿ’ฌ ์งˆ๋ฌธ?

๋” ์ด์ƒ์˜ ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด GitHub Discussions์—์„œ ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”!

Happy Coding! ๐Ÿš€

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages