Другое

Исправление смещения гексагональной карты в Phaser 3: Полное руководство

Узнайте, как исправить проблемы со смещением и неправильным выравниванием гексагональных тайловых карт в Phaser 3. Полное руководство с примерами кода для ступенчатых гексагональных карт из Tiled.

Как исправить проблемы с смещением и неправильным выравниванием гексагональной тайловой карты в Phaser 3?

Я работаю над игрой на Phaser 3, которая использует гексагональную (ступенчатую) карту, созданную в Tiled. Когда я загружаю карту в Phaser, расположение тайлов выглядит со смещением, отсутствуют некоторые координаты, тайлы неправильно выровнены, а верхние строки вообще не отображаются.

Настройки карты в Tiled:

  • Ориентация: Гексагональная (ступенчатая)
  • Ось ступенчатости: Y
  • Индекс ступенчатости: Чётный
  • Ширина/высота тайла: 64
  • Длина стороны гекса: 32
  • Ширина: 16, Высота: 10

Я проверил:

  • JSON загружается правильно (map.width и map.height имеют правильные значения)
  • Ширина/высота тайла и длина стороны гекса соответствуют настройкам Tiled
  • Ориентация карты и ось ступенчатости соответствуют ожиданиям

Не могли бы вы определить ошибку или предложить альтернативный подход к загрузке гексагональных карт в Phaser 3?

Текущий код загрузки в Phaser 3:

typescript
export async function setupMap(scene: Phaser.Scene) {
    const map = scene.make.tilemap({ key: "hexmap" });

    // Динамическое добавление всех тайлсетов из карты
    const layers: Record<string, Phaser.Tilemaps.TilemapLayer> = {};
    map.tilesets.forEach((ts) => {
        const tileset = map.addTilesetImage(ts.name, ts.name);
        if (!tileset) throw new Error(`Tileset not found: ${ts.name}`);
    });

    // Автоматическое создание слоёв
    map.layers.forEach((layerData) => {
        const layer = map.createLayer(layerData.name, map.tilesets, 0, 0);
        if (!layer)
            throw new Error(`Failed to create layer: ${layerData.name}`);
        layers[layerData.name] = layer;
    });

    map.layers.forEach((layerData) => {
        const layer = layers[layerData.name];
        layer.forEachTile((tile) => {
            tile.pixelY -= map.hexSideLength / 2;
        });
    });

    scene.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);

    return { map, layers };
}

Проблемы загрузки шестиугольных карт в Phaser 3

Проблемы загрузки шестиугольных карт в Phaser 3 обычно возникают из-за несоответствий систем координат между Tiled и конвейером рендеринга Phaser. Проблемы с отступами и несоответствием, с которыми вы сталкиваетесь, являются распространенными при работе с шестиугольными картами в шахматном порядке, поскольку Phaser не поддерживает эту ориентацию нативно и требует ручной корректировки координат.

Содержание

Понимание проблемы

Основная проблема заключается в том, как Phaser 3 обрабатывает координаты тайлов по сравнению с тем, как Tiled хранит их для шестиугольных карт. При использовании staggered: true с staggerAxis: 'y', Tiled использует другую систему координат, чем традиционные изометрические или ортогональные карты в Phaser.

Ваш текущий код пытается скорректировать значение pixelY, вычитая hexSideLength / 2, но это решает лишь часть проблемы. Преобразование системы координат должно происходить как на уровне позиционирования тайлов, так и на уровне рендеринга.

Распространенные проблемы с шестиугольными картами

  • Несоответствие систем координат: Phaser ожидает традиционные координаты сетки, но система шестиугольных координат Tiled использует смещенные координаты
  • Ошибки позиционирования в пикселях: Тайлы не выравниваются правильно из-за неправильного преобразования пиксель-в-тайл
  • Отсутствие крайних тайлов: Верхние и крайние ряды часто не отображаются из-за проблем с границами координат
  • Проблемы с границами камеры: Неправильный расчет границ карты для шестиугольных компоновок

Решение 1: Улучшенное преобразование координат

Измените вашу функцию загрузки для правильной обработки преобразования координат шестиугольника:

typescript
export async function setupMap(scene: Phaser.Scene) {
    const map = scene.make.tilemap({ key: "hexmap" });
    
    // Добавьте тайлсеты
    const layers: Record<string, Phaser.Tilemaps.TilemapLayer> = {};
    map.tilesets.forEach((ts) => {
        const tileset = map.addTilesetImage(ts.name, ts.name);
        if (!tileset) throw new Error(`Тайлсет не найден: ${ts.name}`);
    });

    // Создайте слои с правильным позиционированием
    map.layers.forEach((layerData) => {
        const layer = map.createLayer(layerData.name, map.tilesets, 0, 0);
        if (!layer) throw new Error(`Не удалось создать слой: ${layerData.name}`);
        
        // Примените преобразование шестиугольных координат
        applyHexagonalTransform(layer, map);
        layers[layerData.name] = layer;
    });

    // Установите границы камеры с учетом шестиугольной компоновки
    const bounds = calculateHexagonalBounds(map);
    scene.cameras.main.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);

    return { map, layers };
}

function applyHexagonalTransform(layer: Phaser.Tilemaps.TilemapLayer, map: Phaser.Tilemaps.Tilemap) {
    const hexSideLength = map.hexSideLength || 32;
    const tileWidth = map.tileWidth;
    const tileHeight = map.tileHeight;
    const staggerIndex = map.staggerIndex || 'even';
    const staggerAxis = map.staggerAxis || 'y';

    layer.forEachTile((tile) => {
        if (tile.index === -1) return; // Пропустите пустые тайлы

        // Преобразуйте из смещенных в пиксельные координаты
        let pixelX = tile.x * tileWidth;
        let pixelY = tile.y * tileHeight;

        // Примените смещение на основе оси и индекса
        if (staggerAxis === 'y') {
            if ((staggerIndex === 'even' && tile.x % 2 === 0) || 
                (staggerIndex === 'odd' && tile.x % 2 === 1)) {
                pixelY -= hexSideLength / 2;
            }
        } else {
            if ((staggerIndex === 'even' && tile.y % 2 === 0) || 
                (staggerIndex === 'odd' && tile.y % 2 === 1)) {
                pixelX -= hexSideLength / 2;
            }
        }

        // Примените преобразованную позицию
        tile.pixelX = pixelX;
        tile.pixelY = pixelY;
    });
}

function calculateHexagonalBounds(map: Phaser.Tilemaps.Tilemap) {
    const tileWidth = map.tileWidth;
    const tileHeight = map.tileHeight;
    const hexSideLength = map.hexSideLength || 32;
    const mapWidth = map.width;
    const mapHeight = map.height;
    
    // Рассчитайте общую ширину и высоту с учетом шестиугольной компоновки
    const totalWidth = mapWidth * tileWidth;
    const totalHeight = mapHeight * tileHeight + (hexSideLength / 2);
    
    return {
        x: 0,
        y: 0,
        width: totalWidth,
        height: totalHeight
    };
}

Решение 2: Пользовательский рендеринг шестиугольников

Для лучшего контроля рассмотрите подход с пользовательским рендерингом:

typescript
export async function setupHexagonalMap(scene: Phaser.Scene) {
    const map = scene.make.tilemap({ key: "hexmap" });
    
    // Добавьте тайлсеты
    const tilesets: Record<string, Phaser.Tilemaps.Tileset> = {};
    map.tilesets.forEach((ts) => {
        const tileset = map.addTilesetImage(ts.name, ts.name);
        if (!tileset) throw new Error(`Тайлсет не найден: ${ts.name}`);
        tilesets[ts.name] = tileset;
    });

    // Создайте пользовательский шестиугольный слой
    const hexLayer = scene.add.container(0, 0);
    const layers: Record<string, any> = {};

    map.layers.forEach((layerData) => {
        const layerContainer = scene.add.container(0, 0);
        const tiles: Phaser.GameObjects.GameObject[][] = [];

        // Преобразуйте данные слоя в шестиугольные тайлы
        for (let y = 0; y < map.height; y++) {
            tiles[y] = [];
            for (let x = 0; x < map.width; x++) {
                const tileIndex = map.getTileAt(x, y, false, layerData.name);
                if (tileIndex && tileIndex.index !== -1) {
                    const tileset = tilesets[tileIndex.tileset.name];
                    const tile = scene.add.tilemap(
                        tileset,
                        tileIndex.index,
                        0, 0,
                        map.tileWidth,
                        map.tileHeight
                    );
                    
                    // Рассчитайте шестиугольную позицию
                    const pos = getHexagonalPosition(x, y, map);
                    tile.setPosition(pos.x, pos.y);
                    
                    layerContainer.add(tile);
                    tiles[y][x] = tile;
                }
            }
        }

        hexLayer.add(layerContainer);
        layers[layerData.name] = {
            container: layerContainer,
            tiles: tiles
        };
    });

    scene.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
    
    return { map, layers, hexLayer };
}

function getHexagonalPosition(x: number, y: number, map: Phaser.Tilemaps.Tilemap) {
    const tileWidth = map.tileWidth;
    const tileHeight = map.tileHeight;
    const hexSideLength = map.hexSideLength || 32;
    const staggerIndex = map.staggerIndex || 'even';
    const staggerAxis = map.staggerAxis || 'y';

    let pixelX = x * tileWidth;
    let pixelY = y * tileHeight;

    // Примените шестиугольное смещение
    if (staggerAxis === 'y') {
        if ((staggerIndex === 'even' && x % 2 === 0) || 
            (staggerIndex === 'odd' && x % 2 === 1)) {
            pixelY -= hexSideLength / 2;
        }
    } else {
        if ((staggerIndex === 'even' && y % 2 === 0) || 
            (staggerIndex === 'odd' && y % 2 === 1)) {
            pixelX -= hexSideLength / 2;
        }
    }

    return { x: pixelX, y: pixelY };
}

Решение 3: Подход с предварительной обработкой

Преобразуйте ваши данные карты Tiled перед загрузкой их в Phaser:

typescript
function preprocessHexagonalMapData(mapData: any) {
    const processedData = { ...mapData };
    
    if (mapData.orientation === 'hexagonal') {
        processedData.layers = mapData.layers.map((layer: any) => {
            const processedLayer = { ...layer };
            processedLayer.data = layer.data.map((tileIndex: number, index: number) => {
                const x = index % mapData.width;
                const y = Math.floor(index / mapData.width);
                
                // Примените преобразование шестиугольных координат
                const adjustedIndex = adjustTileIndexForHexagon(tileIndex, x, y, mapData);
                return adjustedIndex;
            });
            return processedLayer;
        });
    }
    
    return processedData;
}

function adjustTileIndexForHexagon(tileIndex: number, x: number, y: number, mapData: any) {
    // Пропустите, если тайл пустой
    if (tileIndex === 0) return 0;
    
    // Примените шестиугольную логику на основе настроек смещения
    const staggerIndex = mapData.staggerIndex || 'even';
    const staggerAxis = mapData.staggerAxis || 'y';
    
    // скорректируйте позицию тайла на основе шестиугольного смещения
    if (staggerAxis === 'y') {
        if ((staggerIndex === 'even' && x % 2 === 0) || 
            (staggerIndex === 'odd' && x % 2 === 1)) {
            // Скорректируйте тайл для шестиугольного смещения
            return tileIndex + (mapData.height * mapData.width);
        }
    }
    
    return tileIndex;
}

Шаги отладки

  1. Проверьте свойства карты: Убедитесь, что все свойства карты соответствуют между Tiled и вашим кодом:

    typescript
    console.log('Свойства карты:', {
        orientation: map.orientation,
        staggerAxis: map.staggerAxis,
        staggerIndex: map.staggerIndex,
        tileWidth: map.tileWidth,
        tileHeight: map.tileHeight,
        hexSideLength: map.hexSideLength,
        width: map.width,
        height: map.height
    });
    
  2. Изучите отдельные тайлы: Отладьте позиционирование конкретных тайлов:

    typescript
    layer.forEachTile((tile) => {
        console.log(`Тайл в (${tile.x}, ${tile.y}):`, {
            pixelX: tile.pixelX,
            pixelY: tile.pixelY,
            index: tile.index
        });
    });
    
  3. Визуальная отладка: Добавьте визуализацию для отладки:

    typescript
    scene.add.graphics()
        .lineStyle(2, 0xff0000)
        .strokeRect(0, 0, map.widthInPixels, map.heightInPixels);
    
    // Нарисуйте линии сетки для отладки
    for (let x = 0; x <= map.width; x++) {
        scene.add.line(x * map.tileWidth, 0, x * map.tileWidth, map.heightInPixels, 0x00ff00);
    }
    

Лучшие практики

  1. Используйте согласованные единицы измерения: Убедитесь, что все измерения используют одну и ту же систему единиц (пиксели против тайлов)

  2. Кэшируйте вычисления: Храните часто используемые вычисления, чтобы избежать повторных расчетов:

    typescript
    const hexCache = new Map<string, {x: number, y: number}>();
    
    function getCachedHexPosition(x: number, y: number, map: Phaser.Tilemaps.Tilemap) {
        const key = `${x},${y}`;
        if (hexCache.has(key)) {
            return hexCache.get(key)!;
        }
        
        const pos = getHexagonalPosition(x, y, map);
        hexCache.set(key, pos);
        return pos;
    }
    
  3. Обрабатывайте разные типы смещения: Учитывайте как четное, так и нечетное индексирование смещения:

    typescript
    function isEvenStaggered(x: number, y: number, map: Phaser.Tilemaps.Tilemap) {
        return map.staggerIndex === 'even' && 
               ((map.staggerAxis === 'y' && x % 2 === 0) || 
                (map.staggerAxis === 'x' && y % 2 === 0));
    }
    
  4. Оптимизируйте производительность: Используйте объектный пул для часто создаваемых/уничтожаемых тайлов

Полный рабочий пример

Вот полное решение, которое объединяет все подходы:

typescript
export class HexagonalMapPlugin extends Phaser.Plugins.ScenePlugin {
    constructor(scene: Phaser.Scene, pluginManager: Phaser.Plugins.PluginManager) {
        super(scene, pluginManager, 'hexagonalMap');
    }

    async loadMap(key: string) {
        const map = this.scene.make.tilemap({ key });
        const layers: Record<string, Phaser.Tilemaps.TilemapLayer> = {};
        
        // Добавьте тайлсеты
        map.tilesets.forEach((ts) => {
            const tileset = map.addTilesetImage(ts.name, ts.name);
            if (!tileset) throw new Error(`Тайлсет не найден: ${ts.name}`);
        });

        // Обработайте каждый слой
        map.layers.forEach((layerData) => {
            const layer = map.createLayer(layerData.name, map.tilesets, 0, 0);
            if (!layer) throw new Error(`Не удалось создать слой: ${layerData.name}`);
            
            this.applyHexagonalCorrection(layer, map);
            layers[layerData.name] = layer;
        });

        // Установите границы камеры
        const bounds = this.calculateHexagonalBounds(map);
        this.scene.cameras.main.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);

        return { map, layers };
    }

    private applyHexagonalCorrection(layer: Phaser.Tilemaps.TilemapLayer, map: Phaser.Tilemaps.Tilemap) {
        const hexSideLength = map.hexSideLength || 32;
        const tileWidth = map.tileWidth;
        const tileHeight = map.tileHeight;
        const staggerIndex = map.staggerIndex || 'even';
        const staggerAxis = map.staggerAxis || 'y';

        layer.forEachTile((tile) => {
            if (tile.index === -1) return;

            // Рассчитайте правильную пиксельную позицию
            let pixelX = tile.x * tileWidth;
            let pixelY = tile.y * tileHeight;

            // Примените шестиугольное смещение
            if (staggerAxis === 'y') {
                const shouldOffset = (staggerIndex === 'even' && tile.x % 2 === 0) ||
                                   (staggerIndex === 'odd' && tile.x % 2 === 1);
                if (shouldOffset) {
                    pixelY -= hexSideLength / 2;
                }
            } else {
                const shouldOffset = (staggerIndex === 'even' && tile.y % 2 === 0) ||
                                   (staggerIndex === 'odd' && tile.y % 2 === 1);
                if (shouldOffset) {
                    pixelX -= hexSideLength / 2;
                }
            }

            // Примените скорректированную позицию
            tile.pixelX = pixelX;
            tile.pixelY = pixelY;
        });

        // Обновите слой для применения изменений
        layer.dirty = true;
        layer.layer.data = layer.layer.data.slice();
    }

    private calculateHexagonalBounds(map: Phaser.Tilemaps.Tilemap) {
        const tileWidth = map.tileWidth;
        const tileHeight = map.tileHeight;
        const hexSideLength = map.hexSideLength || 32;
        const mapWidth = map.width;
        const mapHeight = map.height;

        // Рассчитайте общие размеры с учетом шестиугольной компоновки
        const totalWidth = mapWidth * tileWidth;
        const totalHeight = mapHeight * tileHeight + (hexSideLength / 2);

        return {
            x: 0,
            y: 0,
            width: totalWidth,
            height: totalHeight
        };
    }
}

// Использование в вашей сцене:
export class GameScene extends Phaser.Scene {
    private hexMapPlugin!: HexagonalMapPlugin;

    preload() {
        // Загрузите вашу карту из Tiled
        this.load.tilemapTiledJSON('hexmap', 'assets/maps/hexagonal-map.json');
        this.load.image('tileset', 'assets/tilesets/hex-tileset.png');
    }

    async create() {
        // Инициализируйте плагин
        this.hexMapPlugin = new HexagonalMapPlugin(this, this.plugins);
        
        // Загрузите карту с корректировкой шестиугольника
        const { map, layers } = await this.hexMapPlugin.loadMap('hexmap');
        
        // Добавьте любую дополнительную настройку
        this.setupCamera(map);
        this.setupInput(map, layers);
    }

    private setupCamera(map: Phaser.Tilemaps.Tilemap) {
        const bounds = this.hexMapPlugin.calculateHexagonalBounds(map);
        this.cameras.main.setBounds(bounds.x, bounds.y, bounds.width, bounds.height);
        this.cameras.main.setZoom(1);
    }

    private setupInput(map: Phaser.Tilemaps.Tilemap, layers: Record<string, Phaser.Tilemaps.TilemapLayer>) {
        // Преобразуйте мышь в координаты тайлов
        this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
            const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
            const tile = map.getTileAtWorldXY(worldPoint.x, worldPoint.y, false, undefined, layers['ground']);
            
            if (tile) {
                console.log('Нажат тайл в:', {
                    gridX: tile.x,
                    gridY: tile.y,
                    pixelX: tile.pixelX,
                    pixelY: tile.pixelY
                });
            }
        });
    }
}

Ключ к решению проблем с шестиугольными картами в Phaser 3 - понимание различий в системах координат и реализация правильной логики преобразования. Начните с подхода улучшенного преобразования координат и систематически отлаживайте, если проблемы сохраняются. Подход с пользовательским рендерингом дает больше контроля, но требует большего объема обслуживания кода.

Авторы
Проверено модерацией
Модерация