Location-based игры и приложения — одна из самых технически насыщенных областей мобильной разработки. Здесь сходятся точность GPS, пространственные базы данных, real-time синхронизация и необходимость защиты от читеров. Разберём каждый уровень.
Слои точности GPS
GPS на смартфоне — это не одна технология, а стек источников геолокации с разной точностью и потреблением энергии.
| Источник | Точность | Время до первого фикса | Потребление | |---|---|---|---| | GPS (спутники) | 3–10 м | 30–90 с (cold start) | Высокое | | AGPS (assisted GPS) | 3–10 м | 1–5 с | Высокое | | Wi-Fi триангуляция | 10–50 м | <1 с | Низкое | | Cell tower | 100–1000 м | <1 с | Очень низкое | | IP-геолокация | 1–50 км | Мгновенно | Нет |
На практике операционная система сначала отдаёт грубую позицию (Cell/Wi-Fi), затем уточняет через GPS. Важно обрабатывать onLocationChanged событие правильно: не игнорировать первые неточные позиции, но и не принимать их за истину.
Фильтрация GPS-шума
Сырые GPS-данные содержат шум — небольшие прыжки координат даже при стоящем на месте устройстве. Для сглаживания применяют фильтр Калмана:
class KalmanFilter {
constructor(processNoise = 3, measurementNoise = 1) {
this.processNoise = processNoise;
this.measurementNoise = measurementNoise;
this.estimatedError = 1;
this.lastEstimate = null;
}
filter(measurement) {
if (this.lastEstimate === null) {
this.lastEstimate = measurement;
return measurement;
}
const kalmanGain = this.estimatedError /
(this.estimatedError + this.measurementNoise);
const estimate = this.lastEstimate + kalmanGain * (measurement - this.lastEstimate);
this.estimatedError = (1 - kalmanGain) * this.estimatedError +
Math.abs(this.lastEstimate - estimate) * this.processNoise;
this.lastEstimate = estimate;
return estimate;
}
}
const latFilter = new KalmanFilter();
const lngFilter = new KalmanFilter();
navigator.geolocation.watchPosition((pos) => {
const smoothLat = latFilter.filter(pos.coords.latitude);
const smoothLng = lngFilter.filter(pos.coords.longitude);
// используем smoothLat, smoothLng
});
Haversine vs PostGIS ST_DWithin
Формула Haversine (на клиенте и простых серверах)
Haversine вычисляет расстояние между двумя точками на сфере:
function haversineDistance(lat1, lng1, lat2, lng2) {
const R = 6371000; // радиус Земли в метрах
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(Δφ/2) ** 2 +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // метры
}
Haversine подходит для проверки на клиенте и простых серверных вычислений, но не масштабируется для поиска ближайших объектов по базе в 100 тысяч+ записей.
PostGIS ST_DWithin (на сервере)
Для геопространственных запросов к базе данных используйте PostGIS. Подробнее об индексах и типах данных читайте в статье PostGIS: пространственные запросы и индексы.
-- Найти все игровые объекты в радиусе 500 м от игрока
SELECT
id,
name,
type,
ST_Distance(location::geography,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography) AS distance_m
FROM game_objects
WHERE ST_DWithin(
location::geography,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
500 -- метры (geography тип автоматически работает в метрах)
)
ORDER BY distance_m
LIMIT 50;
Индекс для быстрого поиска:
CREATE INDEX game_objects_location_gist ON game_objects
USING GIST (location);
Геофенсинг
Геофенс — определение момента, когда пользователь входит/выходит из зоны. Реализация на сервере надёжнее клиентской (нельзя подделать).
// Node.js: проверка точки внутри полигона через Turf.js
const turf = require('@turf/turf');
function isInsideZone(userLat, userLng, zonePolygon) {
const point = turf.point([userLng, userLat]);
const polygon = turf.polygon(zonePolygon.coordinates);
return turf.booleanPointInPolygon(point, polygon);
}
// Зона событий (GeoJSON Polygon)
const eventZone = {
type: 'Polygon',
coordinates: [[
[37.615, 55.755],
[37.620, 55.755],
[37.620, 55.750],
[37.615, 55.750],
[37.615, 55.755]
]]
};
// При получении обновления от игрока
async function handlePlayerLocation(playerId, lat, lng) {
const wasInside = await redis.get(`player:${playerId}:in_zone`);
const isInside = isInsideZone(lat, lng, eventZone);
if (!wasInside && isInside) {
await emitEvent('zone_enter', playerId);
await redis.set(`player:${playerId}:in_zone`, '1', 'EX', 3600);
} else if (wasInside && !isInside) {
await emitEvent('zone_exit', playerId);
await redis.del(`player:${playerId}:in_zone`);
}
}
Кластеризация точек: Supercluster
При отображении тысяч игровых объектов на карте нужна кластеризация. Supercluster — де-факто стандарт для быстрой кластеризации на клиенте.
import Supercluster from 'supercluster';
const cluster = new Supercluster({
radius: 60, // радиус кластеризации в пикселях
maxZoom: 17, // максимальный уровень зума для кластеризации
minPoints: 3, // минимум точек для создания кластера
});
// Загрузка точек (GeoJSON Features)
cluster.load(gameObjects.map(obj => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [obj.lng, obj.lat] },
properties: { id: obj.id, type: obj.type, name: obj.name }
})));
// Получение кластеров для текущего viewport
function getClustersForViewport(bounds, zoom) {
const [west, south, east, north] = bounds;
return cluster.getClusters([west, south, east, north], Math.floor(zoom));
}
// Пример результата — кластер
// {
// type: 'Feature',
// geometry: { type: 'Point', coordinates: [37.617, 55.752] },
// properties: { cluster: true, cluster_id: 42, point_count: 38 }
// }
Для серверной кластеризации (когда данных слишком много для клиента) используйте PostGIS ST_ClusterDBSCAN или ST_ClusterKMeans.
Античит: защита от фейковых координат
Подделка GPS — главная проблема геоигр. Злоумышленники используют mock location приложения или GPS-спуферы. Несколько уровней защиты:
Уровень 1: Детектирование mock location на клиенте
// React Native: проверка mock location
import { Platform, NativeModules } from 'react-native';
async function isMockLocation(position) {
if (Platform.OS === 'android') {
// Android API предоставляет isMock с API 31
return position.coords.isMock === true;
}
return false; // iOS не поддерживает mock detection нативно
}
// Дополнительно: проверка через нативный модуль
// isMockLocationEnabled() проверяет режим разработчика
Уровень 2: Серверная проверка скорости перемещения
async function validatePlayerMove(playerId, newLat, newLng, timestamp) {
const lastPos = await redis.hgetall(`player:${playerId}:last_pos`);
if (lastPos.lat && lastPos.lng) {
const distance = haversineDistance(
parseFloat(lastPos.lat), parseFloat(lastPos.lng),
newLat, newLng
);
const timeDelta = (timestamp - parseInt(lastPos.ts)) / 1000; // секунды
const speedMs = distance / timeDelta; // метры/секунда
const MAX_SPEED_MS = 15; // ~54 км/ч — быстрее бегущего человека
if (speedMs > MAX_SPEED_MS) {
await flagPlayerSuspicious(playerId, {
reason: 'speed_violation',
speed: speedMs,
distance,
timeDelta
});
return { valid: false, reason: 'speed_too_high' };
}
}
await redis.hset(`player:${playerId}:last_pos`, {
lat: newLat,
lng: newLng,
ts: timestamp
});
return { valid: true };
}
Уровень 3: Статистический анализ паттернов
| Паттерн | Признак читерства |
|---|---|
| Идеально прямолинейное движение | Телепортация по GPS-треку |
| Координаты всегда на целых градусах | Ручной ввод координат |
| Нулевая погрешность (accuracy: 0) | Программная эмуляция |
| Движение сквозь здания/воду | GPS-спуфинг без учёта карты |
| Нет изменения высоты при ходьбе | Отсутствие барометрических данных |
Оптимизация батареи
GPS — главный пожиратель батареи. Стратегии оптимизации:
// React Native: адаптивный режим обновления GPS
import Geolocation from '@react-native-community/geolocation';
class AdaptiveLocationTracker {
constructor() {
this.watchId = null;
this.lastUpdate = null;
this.isMoving = false;
}
start() {
this.watchId = Geolocation.watchPosition(
this.handlePosition.bind(this),
this.handleError.bind(this),
{
enableHighAccuracy: this.isMoving, // точность по ситуации
distanceFilter: this.isMoving ? 5 : 50, // порог обновления в метрах
interval: this.isMoving ? 3000 : 30000, // интервал (Android)
fastestInterval: 1000,
forceRequestLocation: false,
}
);
}
handlePosition(position) {
const speed = position.coords.speed || 0;
const wasMoving = this.isMoving;
this.isMoving = speed > 0.5; // > 0.5 м/с — движение
if (wasMoving !== this.isMoving) {
// Пересоздаём watcher с новыми параметрами
Geolocation.clearWatch(this.watchId);
this.start();
}
// ... обработка позиции
}
}
Дополнительные советы по батарее:
- Используйте
Significant Location ChangeAPI (iOS) для фоновых обновлений — потребляет в 10× меньше. - Отключайте GPS когда приложение в фоне, если это допустимо по логике игры.
- Батчуйте отправку координат на сервер (каждые 5–10 позиций, а не каждую).
Real-time синхронизация через WebSocket
Для многопользовательских геоигр нужна real-time синхронизация позиций:
// Сервер: Socket.io с геопространственными комнатами
io.on('connection', (socket) => {
socket.on('update_position', async (data) => {
const { lat, lng, playerId } = data;
// Валидация
const validation = await validatePlayerMove(playerId, lat, lng, Date.now());
if (!validation.valid) return;
// Обновляем в Redis Geo
await redis.geoadd('online_players', lng, lat, playerId);
// Рассылаем только игрокам в радиусе 500м
const nearbyPlayers = await redis.georadius(
'online_players', lng, lat, 500, 'm', 'WITHCOORD', 'COUNT', 50
);
nearbyPlayers.forEach(([nearPlayerId]) => {
if (nearPlayerId !== playerId) {
const nearSocket = playerSockets.get(nearPlayerId);
nearSocket?.emit('player_moved', { playerId, lat, lng });
}
});
});
});
Подробнее об архитектуре WebSocket и масштабировании читайте в статье WebSocket и real-time в веб-приложениях.
Итог
Location-based разработка требует понимания нескольких слоёв: точность источников геолокации, пространственные запросы в базе данных, умная кластеризация для UI и многоуровневая защита от читеров. Комбинация клиентской фильтрации, серверной валидации скорости и статистического анализа закрывает большинство сценариев мошенничества.

