Исправление смещения гексагональной карты в 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:
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 не поддерживает эту ориентацию нативно и требует ручной корректировки координат.
Содержание
- Понимание проблемы
- Распространенные проблемы с шестиугольными картами
- Решение 1: Улучшенное преобразование координат
- Решение 2: Пользовательский рендеринг шестиугольников
- Решение 3: Подход с предварительной обработкой
- Шаги отладки
- Лучшие практики
- Полный рабочий пример
Понимание проблемы
Основная проблема заключается в том, как Phaser 3 обрабатывает координаты тайлов по сравнению с тем, как Tiled хранит их для шестиугольных карт. При использовании staggered: true с staggerAxis: 'y', Tiled использует другую систему координат, чем традиционные изометрические или ортогональные карты в Phaser.
Ваш текущий код пытается скорректировать значение pixelY, вычитая hexSideLength / 2, но это решает лишь часть проблемы. Преобразование системы координат должно происходить как на уровне позиционирования тайлов, так и на уровне рендеринга.
Распространенные проблемы с шестиугольными картами
- Несоответствие систем координат: Phaser ожидает традиционные координаты сетки, но система шестиугольных координат Tiled использует смещенные координаты
- Ошибки позиционирования в пикселях: Тайлы не выравниваются правильно из-за неправильного преобразования пиксель-в-тайл
- Отсутствие крайних тайлов: Верхние и крайние ряды часто не отображаются из-за проблем с границами координат
- Проблемы с границами камеры: Неправильный расчет границ карты для шестиугольных компоновок
Решение 1: Улучшенное преобразование координат
Измените вашу функцию загрузки для правильной обработки преобразования координат шестиугольника:
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: Пользовательский рендеринг шестиугольников
Для лучшего контроля рассмотрите подход с пользовательским рендерингом:
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:
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;
}
Шаги отладки
-
Проверьте свойства карты: Убедитесь, что все свойства карты соответствуют между Tiled и вашим кодом:
typescriptconsole.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 }); -
Изучите отдельные тайлы: Отладьте позиционирование конкретных тайлов:
typescriptlayer.forEachTile((tile) => { console.log(`Тайл в (${tile.x}, ${tile.y}):`, { pixelX: tile.pixelX, pixelY: tile.pixelY, index: tile.index }); }); -
Визуальная отладка: Добавьте визуализацию для отладки:
typescriptscene.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); }
Лучшие практики
-
Используйте согласованные единицы измерения: Убедитесь, что все измерения используют одну и ту же систему единиц (пиксели против тайлов)
-
Кэшируйте вычисления: Храните часто используемые вычисления, чтобы избежать повторных расчетов:
typescriptconst 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; } -
Обрабатывайте разные типы смещения: Учитывайте как четное, так и нечетное индексирование смещения:
typescriptfunction 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)); } -
Оптимизируйте производительность: Используйте объектный пул для часто создаваемых/уничтожаемых тайлов
Полный рабочий пример
Вот полное решение, которое объединяет все подходы:
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 - понимание различий в системах координат и реализация правильной логики преобразования. Начните с подхода улучшенного преобразования координат и систематически отлаживайте, если проблемы сохраняются. Подход с пользовательским рендерингом дает больше контроля, но требует большего объема обслуживания кода.