Не могу корректно интегрировать Phaser игру на React страницу
Проблема заключается в том что при открытии страницы, игра написанная на Phaser перекрывает все возможные модули которые должны отображаться. И занимает весь экран, не взирая на стили указанные для блока .
Паралельно в консоль выдавая такую ошибку
Phaser v3.85.2 (WebGL | Web Audio) https://phaser.io/v385/ phaser.js:16679:20
TypeError: (void 0) is undefined
fh GameWindow.js:569
React 3
T scheduler.production.min.js:13
M scheduler.production.min.js:14
122 scheduler.production.min.js:14
Webpack 12
react-dom.production.min.js:188:119
Uncaught TypeError: (void 0) is undefined
fh GameWindow.js:569
React 3
T scheduler.production.min.js:13
M scheduler.production.min.js:14
122 scheduler.production.min.js:14
Webpack 12
GameWindow.js:569:8
WebGL warning: texImage: Alpha-premult and y-flip are deprecated for non-DOM-Element uploads.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
WebGL warning: generateMipmap: Tex image TEXTURE_2D level 0 is incurring lazy initialization.
Вот как это выглядит
Логика поведения должна быть такова
- Подгружаем информацию о пользовател
- Если информация о пользователе получена проверяем кол-во энергии
- Если у пользователя более или равно 700 энергии ты мы отнимаем 700 энергии и даём пользователю право играть
- Окно игры занимает 80% от высоты экрана, нижние оставшиеся 20% процентов занимает меню навигации(Оно отображается автоматически, это прописано в файле App.js)
- По окончанию игры пополняем баланс пользователя на количество заработанных очков
К сожалению у меня нет возможности прикрепить укороченную, исполняемую версию кода. Потому что я столкнулся с этой проблемой на позднем этапе разработки. И я не совсем понимаю как сделать это корректно, чтобы передать проблему. Но я надеюсь оставленные комментарии, обилие консоль логов поможет вам понять мой код правильно, и помочь мне....
Вот мой код.
import React, { useContext, useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import {Context} from "../../../index";
import Phaser from 'phaser';
import { updateUser, check } from "../../../http/userApi";
import coin1 from '../../../img/game/1.svg';
import coin2 from '../../../img/game/2.svg';
import coin3 from '../../../img/game/3.svg';
import coin4 from '../../../img/game/4.svg';
import coin5 from '../../../img/game/5.svg';
import coin6 from '../../../img/game/6.svg';
import coin7 from '../../../img/game/7.svg';
import coin8 from '../../../img/game/8.svg';
import coin9 from '../../../img/game/9.svg';
import coin10 from '../../../img/game/10.svg';
import coin11 from '../../../img/game/11.svg';
import "./GameWindowStyle.css";
const GameWindow = observer(() => {
const [userInfo, setUserInfo] = useState(null);
const [isUserInfoLoaded, setIsUserInfoLoaded] = useState(false);
const [gamePass, setGamePass] = useState(false);
// Первый useEffect: получение данных и обновление состояния
useEffect(() => {
const fetchData = async () => {
try {
const data = await check(); // Замени check() на свою функцию получения данных
setUserInfo(data);
setIsUserInfoLoaded(true); // Устанавливаем флаг, что данные загружены
} catch (error) {
console.error("Ошибка при получении данных:", error);
}
};
fetchData();
}, []); // Пустой массив зависимостей для запуска эффекта только один раз при монтировании компонента
useEffect(() => {
if (isUserInfoLoaded) {
const gameStart = async () =>{
if(userInfo.energy >= 700){
let newData;
newData = await updateUser(userInfo.telegramID, userInfo.username, userInfo.role, userInfo.balance, userInfo.level, userInfo.energy - 700);
setGamePass(true);
}
}
gameStart()
}
}, [isUserInfoLoaded])
useEffect( () => {
if (isUserInfoLoaded) {
if(gamePass){
const config = {
type: Phaser.AUTO,
width: window.innerWidth < 480 ? window.innerWidth : 480,
height: window.innerHeight < 854 ? window.innerHeight : 854,
parent: 'gameContainer', // Используем id div-а для встраивания игры
physics: {
default: 'matter',
matter: {
gravity: { y: 1 },
debug: {
showBody: false,
showStaticBody: false,
color: 0xff0000,
alpha: .5
}
}
},
fps: { min: 60, target: 60 },
scene: { preload, create, update }
};
const game = new Phaser.Game(config);
let coins = [];
let score = 0;
let scoreText;
let nextCoinsText;
let nextCoins = [];
let activeCoin = null;
let isCoinFollowing = true;
let isCoinSpawning = false; // Флаг, указывающий на процесс появления монеты
let gameActive = true; // Флаг, указывающий, активна ли игра
let idleTimer; // Таймер простоя
const lineY = 110 + 0.25 * config.height; // высота
// Базовый процентный размер монеты (например, 25% от ширины экрана)
const baseSizePercentage = 0.15;
let crossingTimer = 0; // Таймер для отслеживания времени пересечения
const CROSSING_TIMEOUT = 5000; // Время, после которого игра заканчивается, если монета пересекает линию
let crossingActive = false; // Флаг, указывающий на пересечение линией
const IDLE_TIMEOUT = 20000; // Время простоя в миллисекундах
// Массив ассетов монет для каждого уровня
const coinAssets = [
'coin1', 'coin2', 'coin3', 'coin4', 'coin5',
'coin6', 'coin7', 'coin8', 'coin9', 'coin10', 'coin11'
];
// Базовый размер монеты
const baseSize = 60; // Увеличенный размер монеты
function preload() {
this.load.svg("coin1", coin1);
this.load.svg("coin2", coin2);
this.load.svg("coin3", coin3);
this.load.svg("coin4", coin4);
this.load.svg("coin5", coin5);
this.load.svg("coin6", coin6);
this.load.svg("coin7", coin7);
this.load.svg("coin8", coin8);
this.load.svg("coin9", coin9);
this.load.svg("coin10", coin10);
this.load.svg("coin11", coin11);
}
async function endGame() {
if (!gameActive) return; // Игнорируем, если игра уже завершена
gameActive = false; // Устанавливаем флаг в false
console.log('Game ended.'); // Логирование окончания игры
// Удаляем все монеты, счетчик очков и табло
if (activeCoin) {
activeCoin.destroy();
activeCoin = null; // Сбрасываем ссылку на активную монету
console.log('Active coin destroyed.'); // Логирование удаления активной монеты
}
// Удаляем все ранее упавшие монеты
coins.forEach(coin => coin.destroy());
coins = []; // Очищаем массив монет
nextCoinsText.removeAll(true); // Удаляем табло с будущими монетами
scoreText.destroy(); // Удаляем счетчик очков
// Отображаем сообщение о завершении игры
const finalMessage = this.add.text(config.width / 2, config.height / 2, `Good job,\nyou got ${score} coins`, {
fontSize: '5vw',
fontFamily: 'Atkinson Hyperlegible',
fontWeight: 'bold',
fill: '#fff'
}).setOrigin(0.5);
let newData;
newData = await updateUser(userInfo.telegramID, userInfo.username, userInfo.role, userInfo.balance + score, userInfo.level, userInfo.energy);
console.log(`Final score: ${score}`); // Логирование итогового счета
}
function create() {
// Устанавливаем цвет фона
this.cameras.main.setBackgroundColor('#1C1C1C');
const groundHeight = 40;
const groundY = config.height - (config.height * 0.05) - groundHeight;
// Пол
const groundColor = 0x444444; // Чуть темнее
this.add.rectangle(config.width / 2, groundY, config.width, groundHeight, groundColor);
this.matter.add.rectangle(config.width / 2, groundY, config.width, groundHeight, { isStatic: true });
// Создаем текст для следующих монет
nextCoinsText = this.add.container(config.width / 2, 60); // Панель для следующих монет
initializeNextCoins.call(this); // Инициализация
updateNextCoins.call(this); // Генерация монет на табло сразу при запуске игры
// Создаем текст счетчика очков
scoreText = this.add.text(config.width / 2, 110, 'SCORE: 0', {
fontSize: '5vw', // Адаптивный размер шрифта
fontFamily: 'Atkinson Hyperlegible',
fontWeight: 'bold',
fill: '#fff'
}).setOrigin(0.5);
// Добавляем пунктирную линию на 25% ниже счетчика очков
const dashLength = 10; // Длина штриха
const spaceLength = 5; // Длина пробела
for (let i = 0; i < config.width; i += dashLength + spaceLength) {
this.add.line(0, lineY, i, 0, i + dashLength, 0, 0xffffff, 1); // Создаём отдельные линии
}
// Стены
const wallColor = 0x444444; // Чуть темнее
const wallThickness = 20;
// Создаем визуальные стены
this.add.rectangle(wallThickness / 2, config.height / 2, wallThickness, config.height, wallColor); // Левое
this.add.rectangle(config.width - wallThickness / 2, config.height / 2, wallThickness, config.height, wallColor); // Правое
// Добавляем физические стены
this.matter.add.rectangle(wallThickness / 2, config.height / 2, wallThickness, config.height, { isStatic: true });
this.matter.add.rectangle(config.width - wallThickness / 2, config.height / 2, wallThickness, config.height, { isStatic: true });
// Блок с возможными монетами, касающийся игрового пола
const allCoinsContainer = this.add.container(config.width / 2, groundY + groundHeight * 1.25); // Поднимаем блок на половину высоты пола
updateAllCoins.call(this, allCoinsContainer); // Инициализация блока с монетами
spawnCoin.call(this); // Спавн первой монеты
resetIdleTimer.call(this);
let isCoinSpawning = false; // Флаг, указывающий на процесс появления монеты
// Флаг для отслеживания состояния активной монеты
let activeCoinIsReady = false;
this.input.on('pointerdown', (pointer) => {
// Проверяем, не идет ли процесс спавна и не готова ли монета
if (pointer.button === 0 && gameActive && !isCoinSpawning) {
resetIdleTimer.call(this); // Сбрасываем таймер при клике
if (activeCoin) {
isCoinFollowing = false; // Прекращаем следование указателя
const targetX = pointer.x; // Целевая позиция по X
// Учитываем ширину стен и радиус монеты
const coinRadius = activeCoin.displayWidth / 2;
const leftBoundary = 20 + coinRadius; // Левая граница с учетом радиуса
const rightBoundary = config.width - 20 - coinRadius; // Правая граница с учетом радиуса
// Ограничиваем целевую позицию
const constrainedTargetX = Phaser.Math.Clamp(targetX, leftBoundary, rightBoundary);
const speedFactor = 0.1; // Фактор скорости перемещения
// Плавное перемещение монеты к целевой позиции
const moveToTarget = () => {
activeCoin.x += (constrainedTargetX - activeCoin.x) * speedFactor;
// Проверяем, достигла ли монета целевой точки
if (Math.abs(activeCoin.x - constrainedTargetX) > 1) {
requestAnimationFrame(moveToTarget); // Продолжаем движение
} else {
// Создаем физическую монету, когда целевая позиция достигнута
createPhysicalCoin.call(this, activeCoin.x);
}
};
moveToTarget(); // Запускаем движение к цели
}
}
});
function resetIdleTimer() {
clearTimeout(idleTimer); // Очищаем предыдущий таймер
idleTimer = setTimeout(() => {
endGame.call(this); // Завершаем игру по истечении времени
}, IDLE_TIMEOUT);
}
this.matter.world.on('collisionstart', (event) => {
if (!gameActive) return; // Игнорируем столкновения, если игра завершена
event.pairs.forEach((pair) => {
const bodyA = pair.bodyA;
const bodyB = pair.bodyB;
// Проверяем, что оба объекта являются монетами
if (bodyA.gameObject && bodyB.gameObject) {
// Проверяем, что монеты одного уровня
if (bodyA.gameObject.level === bodyB.gameObject.level && bodyA.gameObject !== bodyB.gameObject) {
mergeCoins.call(this, bodyA.gameObject, bodyB.gameObject);
}
}
});
});
}
// Функция для расчета размера монеты на основе размеров экрана
function getCoinSize(level) {
const baseSize = config.width * baseSizePercentage; // Базовый размер монеты
return baseSize * (1 + (level - 1) * 0.15); // Увеличиваем размер в зависимости от уровня монеты
}
function update() {
if (!gameActive || coins.length === 0) return; // Игнорируем обновление, если игра завершена или нет монет
scoreText.setText(`SCORE: ${score}`);
// Если активная монета должна следовать за курсором
if (activeCoin && isCoinFollowing) {
const pointerX = this.input.activePointer.x;
const speedFactor = 0.1; // Чем меньше значение, тем медленнее будет следование
const leftBoundary = 20 + activeCoin.displayWidth / 2; // Левая граница
const rightBoundary = config.width - 20 - activeCoin.displayWidth / 2; // Правая граница
// Плавное перемещение по оси X
activeCoin.x = Phaser.Math.Clamp(activeCoin.x + (pointerX - activeCoin.x) * speedFactor, leftBoundary, rightBoundary);
// Рассчитываем угол наклона
const maxTilt = Phaser.Math.DegToRad(25); // Максимальный угол наклона
const tiltAmount = Phaser.Math.Clamp((pointerX - activeCoin.x) * 0.01, -maxTilt, maxTilt); // Наклон
activeCoin.setRotation(tiltAmount); // Устанавливаем наклон
}
// Проверка пересечения с пунктирной линией для всех монет
let isAnyCoinCrossing = false;
coins.forEach((coin) => {
const coinRadius = coin.displayWidth / 2; // Радиус монеты
const isCoinOnLine = (coin.y + coinRadius >= lineY && coin.y - coinRadius <= lineY); // Проверка пересечения линии
if (isCoinOnLine) {
console.log(`Coin with level ${coin.level} is on the dashed line. X: ${coin.x}, Y: ${coin.y}`); // Логирование
isAnyCoinCrossing = true;
}
});
if (isAnyCoinCrossing) {
if (!crossingActive) {
crossingActive = true;
crossingTimer = 0;
console.log('A coin is on the dashed line. Starting the crossing timer.');
}
} else {
if (crossingActive) {
console.log('No coins are on the dashed line. Resetting the crossing timer.');
crossingActive = false;
crossingTimer = 0;
}
}
// Увеличиваем таймер пересечения, если активен
if (crossingActive) {
crossingTimer += this.game.loop.delta; // Увеличиваем таймер
console.log(`Crossing timer: ${crossingTimer.toFixed(2)} ms`);
if (crossingTimer >= CROSSING_TIMEOUT) {
endGame.call(this); // Завершаем игру при превышении таймаута
}
}
}
const COIN_SPAWN_INTERVAL = 500; // Время респавна монеты в миллисекундах
function createPhysicalCoin(x) {
const coinSize = getCoinSize(activeCoin.level); // Рассчитываем размер монеты
const newCoin = this.matter.add.image(x, activeCoin.y, coinAssets[activeCoin.level - 1]);
newCoin.setCircle();
newCoin.setDisplaySize(coinSize, coinSize); // Устанавливаем адаптивный размер монеты
newCoin.setBounce(0.6);
newCoin.level = activeCoin.level;
newCoin.setIgnoreGravity(false);
newCoin.setVelocityY(5); // Даем толчок вниз
coins.push(newCoin); // Добавляем монету в массив coins
activeCoin.destroy(); // Уничтожаем активную монету
activeCoin = null;
// Таймер перед созданием нового указателя
isCoinSpawning = true;
setTimeout(() => {
if (!gameActive) return;
spawnCoin.call(this);
isCoinSpawning = false;
}, COIN_SPAWN_INTERVAL); // Таймер в 500 миллисекунд
}
// Функция для проверки, пересекает ли монета пунктирную линию
function isCrossingDashedLine(coin) {
const lineY = 110 + 0.25 * config.height; // Высота пунктирной линии
return coin.y >= lineY; // Проверяем, ниже ли монета линии
}
// Функция для обработки пересечения
function handleCrossing() {
if (!crossingActive) {
crossingActive = true; // Активируем отслеживание пересечения
crossingTimer = 0; // Сбрасываем таймер
console.log('Coin is crossing the dashed line. Timer started.'); // Логирование
} else {
console.log('Coin continues to cross the dashed line.'); // Логирование продолжающегося пересечения
}
}
// Обновление состояния пересечения при выходе из области пересечения
this.matter.world.on('collisionend', (event) => {
event.pairs.forEach((pair) => {
const bodyA = pair.bodyA;
const bodyB = pair.bodyB;
if (bodyA.gameObject && bodyB.gameObject) {
if (isCrossingDashedLine(bodyA.gameObject) || isCrossingDashedLine(bodyB.gameObject)) {
console.log('Coin stopped crossing the dashed line. Resetting timer.'); // Логирование
crossingActive = false; // Отключаем отслеживание
crossingTimer = 0; // Сбрасываем таймер
}
}
});
});
function initializeNextCoins() {
// Инициализируем массив с тремя случайными уровнями монет
nextCoins = [Phaser.Math.Between(1, 3), Phaser.Math.Between(1, 3), Phaser.Math.Between(1, 3)];
}
function addRandomCoin() {
// Добавляем новую случайную монету в конец массива
const randomLevel = Phaser.Math.Between(1, 3); // Уровень от 1 до 3
nextCoins.push(randomLevel);
}
function spawnCoin() {
const coinY = 0.25 * config.height; // Высота спавна монеты
const nextCoinLevel = nextCoins[0]; // Берем первый уровень монеты из массива
const asset = coinAssets[nextCoinLevel - 1];
// Спавним активную монету по центру
const coinX = config.width / 2;
activeCoin = this.add.image(coinX, coinY, asset); // Создаем обычное изображение монеты, без физики
// Устанавливаем адаптивный размер монеты
let coinSize = getCoinSize(nextCoinLevel);
activeCoin.setDisplaySize(coinSize, coinSize);
// Устанавливаем уровень монеты
activeCoin.level = nextCoinLevel;
console.log(`Spawned coin with level: ${activeCoin.level}`); // Логирование уровня монеты
isCoinFollowing = true; // Монета будет следовать за указателем
// Удаляем первую монету из массива и добавляем новую случайную монету
nextCoins.shift();
addRandomCoin.call(this);
updateNextCoins.call(this);
}
function updateNextCoins() {
// Удаляем старые монеты из контейнера
nextCoinsText.removeAll(true);
// Обновляем текст с монетами
nextCoins.forEach((level, index) => {
const asset = coinAssets[level - 1];
const coinImage = this.add.image(0, 0, asset);
coinImage.setDisplaySize(50, 50); // Уменьшенный размер для монет в табло
// Позиционируем монеты в контейнере с небольшим расстоянием между ними
coinImage.x = (index - 1) * 70; // Устанавливаем расстояние в 70 пикселей между монетами
nextCoinsText.add(coinImage);
});
}
function updateAllCoins(container) {
const wallThickness = 20; // Толщина стен
const groundHeight = 40; // Высота пола
const groundY = config.height - (config.height * 0.05) - groundHeight; // Положение пола
// Определяем доступную ширину контейнера (между боковыми стенами)
const availableWidth = config.width - 2 * wallThickness;
// Количество монет
const totalCoins = coinAssets.length;
// Рассчитываем адаптивный размер монеты
const maxCoinSize = availableWidth * 0.9 / totalCoins;
const coinSize = Math.min(maxCoinSize, 60); // Максимальный размер монеты 60px, адаптивный - меньше
// Очищаем контейнер от предыдущих монет
container.removeAll(true);
// Вычисление отступов между монетами
const spacing = (availableWidth - totalCoins * coinSize) / (totalCoins - 1);
// Добавляем монеты в контейнер
coinAssets.forEach((asset, index) => {
const xPosition = -availableWidth / 2 + index * (coinSize + spacing) + coinSize / 2;
const coinImage = this.add.image(xPosition, 0, asset);
coinImage.setDisplaySize(coinSize, coinSize);
container.add(coinImage);
});
// Рассчитываем отступ от нижней границы игрового поля на радиус монеты
const containerY = config.height - groundHeight / 1.25; // Учитываем радиус монеты
// Устанавливаем позицию контейнера
container.setPosition(config.width / 2, containerY);
}
function mergeCoins(coin1, coin2) {
console.log(`Attempting to merge coins: ${coin1.level} + ${coin2.level}`); // Логируем попытку объединения
if (!coin1 || !coin2 || !coin1.body || !coin2.body) {
console.error('Attempt to merge with undefined coin objects or one of the coins is destroyed.', coin1, coin2);
return null;
}
if (coin1.level < 11) {
const newLevel = coin1.level + 1;
const x = (coin1.x + coin2.x) / 2;
const y = (coin1.y + coin2.y) / 2;
// Создаем новую монету
const newCoin = this.matter.add.image(x, y, coinAssets[newLevel - 1]);
const newCoinSize = getCoinSize(newLevel); // Вычисляем адаптивный размер новой монеты
newCoin.setCircle();
newCoin.setDisplaySize(newCoinSize, newCoinSize);
newCoin.setBounce(0.6);
newCoin.level = newLevel;
// Увеличиваем счетчик очков
score += 10;
console.log(`Merged coins into new coin with level: ${newLevel}. Score: ${score}`); // Логирование
// Удаляем старые монеты из массива
coins = coins.filter(c => c !== coin1 && c !== coin2);
// Уничтожаем старые монеты
coin1.destroy();
coin2.destroy();
coins.push(newCoin); // Добавляем новую монету в массив coins
return newCoin;
}
console.log(`Merge failed: one or both coins at max level.`);
return null;
}
}}
}, [gamePass])
return (
<div className="GameWindow">
< div id = "root" > </ div >
< div id = "phaser-container" > </ div >
<script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.js"></script>
</div>
);
});
export default GameWindow;
Заранее спасибо