GPS Coordinate Conversion System

실제 GPS 좌표를 Unity 가상 세계로 변환하는 수학

2025.12 | 개발 기간 2주

기술 스택

UnityC#Google Maps APIMercator ProjectionGPSTile System

프로젝트 개요

Pokémon GO, Ingress 같은 위치 기반 게임의 핵심 기술인 GPS 좌표를 Unity World 좌표로 변환하고, Google Maps 타일 시스템을 구현한 프로젝트입니다.

실제 서울 강남역(37.4979°N, 127.0276°E)을 Unity 상의 (0, 0, 0) 위치로 변환하고, 플레이어가 실제로 이동하면 가상 세계에서도 동일하게 이동하는 시스템입니다. 지구는 둥글지만 화면은 평평하기 때문에, Mercator Projection이라는 수학적 변환이 필수적입니다.


해결해야 할 문제

문제 1: 지구는 둥글고, 화면은 평평하다

GPS는 구면 좌표계(Latitude, Longitude)를 사용하지만, Unity는 평면 좌표계(X, Y, Z)를 사용합니다. 직접 변환하면 심각한 왜곡이 발생합니다.

  • 극지방으로 갈수록 가로 길이가 심하게 왜곡됨
  • 1도의 실제 거리가 위도에 따라 달라짐
  • 위도 1° ≈ 111km (일정), 경도 1° ≈ 111km × cos(위도) (변함)

문제 2: Google Maps는 타일 기반

Google Maps는 전 세계를 256×256px 타일로 나눕니다. Zoom Level에 따라 타일 개수가 지수적으로 증가합니다.

  • Zoom 0: 1개 타일 (전 세계)
  • Zoom 15: 1,073,741,824개 타일
  • Zoom 20: 1,099,511,627,776개 타일

어떻게 효율적으로 로드할 것인가? 3×3 무한 스크롤 시스템이 필요합니다.

문제 3: 실시간 GPS는 부정확하고 조작 가능하다

GPS는 다양한 이유로 부정확하며, 사용자가 위치를 조작할 수도 있습니다.

  • GPS 오차: ±5~10m
  • GPS Spoofing (위치 조작) 가능
  • 네트워크 딜레이
  • 실내에서 GPS 작동 불안정

어뷰징을 방지하고 안정적으로 작동시키는 시스템이 필요합니다.


좌표계 기초 지식

GPS 좌표계 (WGS84)

GPS는 WGS84 (World Geodetic System 1984) 좌표계를 사용합니다.

text
Latitude (위도):  -90° ~ +90° (남극 ~ 북극)
Longitude (경도): -180° ~ +180° (서 ~ 동)

예시:
- 서울 강남역: 37.4979°N, 127.0276°E
- 뉴욕:        40.7128°N, -74.0060°W
- 도쿄:        35.6762°N, 139.6503°E

문제점:

  • 1° 위도 ≈ 111km (일정)
  • 1° 경도 ≈ 111km × cos(위도) (위도에 따라 변함!)
  • 적도에서는 1° 경도 = 111km, 북극에서는 1° 경도 = 0km

Mercator Projection (메르카토르 도법)

구면을 평면으로 펼치는 수학적 변환입니다. Google Maps가 사용하는 방식입니다.

장점:

  • ✅ 각도가 보존됨 (항해에 유용)
  • ✅ 계산이 간단함
  • ✅ Google Maps 표준

단점:

  • ❌ 극지방이 심하게 왜곡됨
  • ❌ 면적이 보존 안 됨
  • ❌ 그린란드가 아프리카보다 커 보임

Google Maps 타일 좌표계

Google Maps는 전 세계를 2^zoom × 2^zoom 개의 타일로 분할합니다.

text
Zoom 15 (서울 도심 수준):
- 전 세계: 32,768 × 32,768 타일
- 타일 1개: 약 4.89km × 4.89km
- 256×256 픽셀/타일

좌표 범위:
- X (가로): 0 ~ 2^zoom - 1
- Y (세로): 0 ~ 2^zoom - 1

예시: Zoom 15에서 서울 강남역
- Tile X: 28062
- Tile Y: 12793

핵심 알고리즘 3가지

알고리즘 1: GPS → 타일 좌표 변환

GPS 좌표(Latitude, Longitude)를 Google Maps 타일 좌표로 변환합니다. Mercator Projection 공식을 사용합니다.

csharp
// Mercator Projection 공식
float n = Mathf.Pow(2, zoomLevel);

// Longitude → X (간단한 선형 변환)
float tileX = (longitude + 180.0f) / 360.0f * n;

// Latitude → Y (복잡한 Mercator 변환)
float latRad = latitude * Mathf.Deg2Rad;
float tileY = (1 - Mathf.Log(
    Mathf.Tan(latRad) + 1 / Mathf.Cos(latRad)
) / Mathf.PI) / 2 * n;

왜 이렇게 복잡한가?

  • • Longitude는 선형 변환으로 충분 (동서 방향은 단순)
  • • Latitude는 Mercator 공식 필요 (남북 방향은 왜곡 보정)
  • • Log, Tan, Cos: 구면을 평면으로 펼치는 수학적 왜곡 보정

알고리즘 2: 타일 좌표 → Unity World 좌표

타일 좌표를 Unity의 3D 공간 좌표로 변환합니다. 기준점(Origin Tile) 대비 상대 좌표로 계산하여 float 정밀도 문제를 해결합니다.

csharp
// 타일 좌표 차이 계산
int deltaX = currentTileX - originTileX;
int deltaY = currentTileY - originTileY;

// Unity 좌표로 변환 (1타일 = 100 Unity Unit)
float unityX = deltaX * TILE_SIZE;
float unityZ = -deltaY * TILE_SIZE; // Y는 반대 방향

return new Vector3(unityX, 0, unityZ);

왜 originTile이 필요한가?

  • • Unity는 float 정밀도 한계 (7자리)
  • • 타일 번호가 28062처럼 크면 정밀도 문제 발생
  • • 기준점(예: 강남역) 설정 후 상대 좌표로 계산
  • • 이렇게 해야 mm 단위까지 정확한 표현 가능

알고리즘 3: 3×3 타일 무한 스크롤

플레이어 주변 9개 타일만 로드하여 메모리와 API 비용을 절약합니다. 플레이어가 이동하면 필요한 타일만 동적으로 로드/언로드합니다.

csharp
// 현재 플레이어가 있는 타일
(int centerX, int centerY) = GetPlayerTile();

// 주변 8개 + 중앙 1개 = 9개 타일
for (int dx = -1; dx <= 1; dx++) {
    for (int dy = -1; dy <= 1; dy++) {
        int tileX = centerX + dx;
        int tileY = centerY + dy;
        
        // 아직 로드되지 않은 타일만 로드
        if (!loadedTiles.ContainsKey((tileX, tileY))) {
            LoadTile(tileX, tileY); // Google API 호출
        }
    }
}

// 플레이어가 다른 타일로 이동하면
if (PlayerChangedTile()) {
    UnloadFarTiles(); // 멀어진 타일 언로드
}

최적화 포인트

  • • Dictionary<(int, int), Tile> 캐싱 → O(1) 검색
  • • 타일 변경 시에만 로드/언로드 → API 호출 최소화
  • • 비동기 로드 → 프레임 드랍 방지
  • • 메모리: 9타일 × 256KB ≈ 2.3MB (매우 가벼움)

어뷰징 방지 시스템

GPS Spoofing 감지

사용자가 GPS 위치를 조작하여 순간이동하는 것을 감지하고 차단합니다.

csharp
// 이전 위치와 현재 위치의 타일 차이 계산
int tileDelta = Mathf.Abs(newTileX - oldTileX) 
              + Mathf.Abs(newTileY - oldTileY);

// 2타일 이상 순간이동 = 의심
if (tileDelta >= 2 && timeDelta < 1.0f) {
    Debug.LogError("GPS Spoofing 감지!");
    Application.Quit(); // 앱 강제 종료
}

왜 2타일인가?

  • • Zoom 15 기준: 1타일 ≈ 5km
  • • 사람이 1초에 5km 이동 = 불가능 (18,000 km/h)
  • • 자동차도 5km/s는 불가능
  • • 2타일 = 10km ≈ 확실한 치팅

전체 변환 파이프라인

csharp
public class GPSConverter
{
    private const int ZOOM_LEVEL = 15;
    private const float TILE_SIZE = 100f; // Unity Unit
    
    private Vector2Int _originTile; // 기준점 타일 (강남역)
    
    // GPS → Unity World Position
    public Vector3 GPSToUnity(double lat, double lon)
    {
        // 1. GPS → 타일 좌표
        Vector2Int tile = GPSToTileCoord(lat, lon);
        
        // 2. 타일 좌표 → Unity 좌표
        return TileToUnity(tile);
    }
    
    private Vector2Int GPSToTileCoord(double lat, double lon)
    {
        float n = Mathf.Pow(2, ZOOM_LEVEL);
        
        // Longitude → X (선형 변환)
        int x = (int)((lon + 180.0) / 360.0 * n);
        
        // Latitude → Y (Mercator Projection)
        double latRad = lat * Mathf.Deg2Rad;
        double mercN = Math.Log(Math.Tan(latRad) + 1 / Math.Cos(latRad));
        int y = (int)((1 - mercN / Math.PI) / 2 * n);
        
        return new Vector2Int(x, y);
    }
    
    private Vector3 TileToUnity(Vector2Int tile)
    {
        int deltaX = tile.x - _originTile.x;
        int deltaY = tile.y - _originTile.y;
        
        return new Vector3(
            deltaX * TILE_SIZE,
            0,
            -deltaY * TILE_SIZE
        );
    }
    
    // 3x3 타일 로드 시스템
    public void UpdateTiles(Vector2Int playerTile)
    {
        for (int dx = -1; dx <= 1; dx++) {
            for (int dy = -1; dy <= 1; dy++) {
                Vector2Int tileCoord = new Vector2Int(
                    playerTile.x + dx,
                    playerTile.y + dy
                );
                
                if (!_loadedTiles.ContainsKey(tileCoord)) {
                    StartCoroutine(LoadTileAsync(tileCoord));
                }
            }
        }
        
        UnloadDistantTiles(playerTile);
    }
}

성능 & 정확도

성능 지표

  • ✅ GPS 업데이트: 60 FPS (1초에 1번 폴링)
  • ✅ 타일 로드: 비동기 (프레임 드랍 없음)
  • ✅ 메모리: 9타일 × 256KB = 2.3MB
  • ✅ API 호출: 타일 변경 시에만

정확도

  • GPS 오차: ±5m
  • 타일 크기: ~5000m
  • 상대 오차: 0.1% 미만
  • Unity 정밀도: mm 단위까지 표현 가능

알고리즘 복잡도

작업시간 복잡도공간 복잡도
GPS → 타일 좌표O(1)O(1)
타일 좌표 → UnityO(1)O(1)
타일 로드 확인O(1)O(n)
3×3 타일 업데이트O(9) = O(1)O(9) = O(1)

배운 점

수학의 중요성

Mercator Projection이 왜 필요한지, 구면 좌표계와 평면 좌표계의 본질적 차이를 이해했습니다. 수학 없이는 GPS 게임 개발이 불가능하다는 것을 깨달았습니다. Log, Tan, Cos 같은 삼각함수가 실제 게임 개발에서 어떻게 사용되는지 직접 경험했습니다.

최적화의 기술

전 세계를 로드하는 대신 3×3 타일만 로드하여 메모리와 API 비용을 99.99% 절감했습니다. 상대 좌표를 사용하여 float 정밀도 문제를 해결하고, Dictionary 캐싱으로 O(1) 검색 성능을 달성했습니다.

보안의 중요성

GPS Spoofing은 생각보다 쉽게 가능하지만, 타일 기반 검사로 간단하고 효과적으로 방어할 수 있습니다. 서버 검증 없이도 클라이언트에서 기본적인 치팅 방어가 가능하다는 것을 배웠습니다.

좌표계 변환의 깊이

Unity의 Transform 시스템을 깊이 이해하게 되었습니다. 단순히 GPS 좌표를 받아서 쓰는 것이 아니라, 여러 좌표계 간의 변환 과정을 거쳐야 정확하고 효율적인 시스템을 만들 수 있다는 것을 깨달았습니다.


향후 계획

  • Geohash 도입: 타일보다 정밀한 위치 표현을 위해 Geohash 알고리즘 적용 예정
  • 서버 기반 위치 검증: PlayFab Integration을 통한 서버 측 GPS 검증 시스템 구축
  • 실내 위치 추적: Bluetooth Beacon, WiFi Triangulation으로 실내에서도 위치 추적 가능하게
  • 다중 Zoom Level 지원: 사용자의 이동 속도에 따라 동적으로 Zoom Level 조정

© 2026 Trout. Built with Next.js & TypeScript.