From ff48c63637c0899fd8a0546a62aefb755fdcd8c1 Mon Sep 17 00:00:00 2001 From: LiprikON2 <44316968+LiprikON2@users.noreply.github.com> Date: Sun, 2 Jul 2023 18:13:19 +0300 Subject: [PATCH] feat: add loading screen using custom event emitter --- client/package.json | 2 + .../scripts/core/{game.ts => gameConfig.ts} | 42 +--------- client/src/game/scripts/core/gameExtended.ts | 9 +++ client/src/game/scripts/core/gameManager.ts | 76 +++++++++++++++++++ client/src/game/scripts/core/index.ts | 3 + client/src/game/scripts/index.ts | 7 +- client/src/game/scripts/scenes/mainScene.ts | 2 + .../src/game/scripts/scenes/preloadScene.ts | 32 ++++++-- client/src/ui/App.tsx | 2 +- client/src/ui/Root.tsx | 7 ++ client/src/ui/hooks/useGame.ts | 27 +++---- .../src/ui/scenes/BottomRight/BottomRight.tsx | 10 ++- client/src/ui/scenes/Center/Center.tsx | 30 ++------ .../scenes/Center/scenes/Loading/Loading.tsx | 22 ++++++ .../Center/scenes/Loading/hooks/index.ts | 1 + .../scenes/Loading/hooks/useGameLoading.tsx | 17 +++++ .../ui/scenes/Center/scenes/Loading/index.ts | 1 + .../Center/scenes/MainMenu/MainMenu.tsx | 30 ++++++++ .../ui/scenes/Center/scenes/MainMenu/index.ts | 1 + client/src/ui/scenes/TopLeft/TopLeft.tsx | 1 - client/tsconfig.json | 2 +- 21 files changed, 229 insertions(+), 95 deletions(-) rename client/src/game/scripts/core/{game.ts => gameConfig.ts} (69%) create mode 100644 client/src/game/scripts/core/gameExtended.ts create mode 100644 client/src/game/scripts/core/gameManager.ts create mode 100644 client/src/game/scripts/core/index.ts create mode 100644 client/src/ui/scenes/Center/scenes/Loading/Loading.tsx create mode 100644 client/src/ui/scenes/Center/scenes/Loading/hooks/index.ts create mode 100644 client/src/ui/scenes/Center/scenes/Loading/hooks/useGameLoading.tsx create mode 100644 client/src/ui/scenes/Center/scenes/Loading/index.ts create mode 100644 client/src/ui/scenes/Center/scenes/MainMenu/MainMenu.tsx create mode 100644 client/src/ui/scenes/Center/scenes/MainMenu/index.ts diff --git a/client/package.json b/client/package.json index 26fb2a2..27a762b 100644 --- a/client/package.json +++ b/client/package.json @@ -39,6 +39,7 @@ "@capacitor/device": "^5.0.4", "@dnd-kit/core": "^6.0.8", "@emotion/styled": "^11.11.0", + "@geckos.io/client": "^2.3.1", "@mantine/core": "^6.0.14", "@mantine/form": "^6.0.14", "@mantine/hooks": "^6.0.14", @@ -46,6 +47,7 @@ "core-js": "^3.31.0", "immer": "^10.0.2", "lodash": "^4.17.21", + "nanoevents": "^8.0.0", "navmesh": "^2.3.1", "phaser": "3.55.2", "phaser-navmesh": "^2.3.1", diff --git a/client/src/game/scripts/core/game.ts b/client/src/game/scripts/core/gameConfig.ts similarity index 69% rename from client/src/game/scripts/core/game.ts rename to client/src/game/scripts/core/gameConfig.ts index 3607109..92d197e 100644 --- a/client/src/game/scripts/core/game.ts +++ b/client/src/game/scripts/core/gameConfig.ts @@ -1,4 +1,3 @@ -import "phaser"; import Phaser from "phaser"; import MouseWheelScrollerPlugin from "phaser3-rex-plugins/plugins/mousewheelscroller-plugin.js"; import RotateToPlugin from "phaser3-rex-plugins/plugins/rotateto-plugin.js"; @@ -8,7 +7,7 @@ import VirtualJoystickPlugin from "phaser3-rex-plugins/plugins/virtualjoystick-p import ButtonPlugin from "phaser3-rex-plugins/plugins/button-plugin.js"; import { MainScene, PreloadScene } from "~/scenes"; -import type { Spaceship } from "~/objects"; + const DEFAULT_WIDTH = 1920; const DEFAULT_HEIGHT = 1080; // const DEFAULT_WIDTH = 920; @@ -80,42 +79,3 @@ export const gameConfig: Phaser.Types.Core.GameConfig = { gamepad: true, }, }; - -export class Game { - config; - game: Phaser.Game; - - constructor(config) { - this.config = config; - } - - init = async (settings) => { - const whenIsBooted = new Promise((resolve) => { - this.game = new Phaser.Game({ - ...this.config, - callbacks: { postBoot: () => resolve(true) }, - }); - this.game["settings"] = settings; - }); - await whenIsBooted; - - const whenSceneCreated = new Promise((resolve) => { - const MainScene = this.game.scene.keys.MainScene as MainScene; - MainScene.events.on("create", resolve); - }); - await whenSceneCreated; - - return this; - }; - get scene(): MainScene | null { - return (this.game?.scene?.keys?.MainScene as MainScene) ?? null; - } - get player(): Spaceship | null { - return this.scene?.player ?? null; - } - destroy = () => { - this.game.destroy(false); - }; -} - -export const game = new Game(gameConfig); diff --git a/client/src/game/scripts/core/gameExtended.ts b/client/src/game/scripts/core/gameExtended.ts new file mode 100644 index 0000000..344e546 --- /dev/null +++ b/client/src/game/scripts/core/gameExtended.ts @@ -0,0 +1,9 @@ +export class GameExtended extends Phaser.Game { + settings; + outEmitter; + constructor(GameConfig?: Phaser.Types.Core.GameConfig, settings = {}, outEmitter = null) { + super(GameConfig); + this.settings = settings; + this.outEmitter = outEmitter; + } +} diff --git a/client/src/game/scripts/core/gameManager.ts b/client/src/game/scripts/core/gameManager.ts new file mode 100644 index 0000000..f7f84f1 --- /dev/null +++ b/client/src/game/scripts/core/gameManager.ts @@ -0,0 +1,76 @@ +import Phaser from "phaser"; +import { createNanoEvents } from "nanoevents"; +import type { Emitter } from "nanoevents"; + +import { MainScene } from "~/scenes"; +import type { Spaceship } from "~/objects"; +import { GameExtended } from "."; +import { gameConfig } from "."; + +interface Events { + loading: (report: { name: string; progress: number }) => void; +} + +export class GameManager { + config: Phaser.Types.Core.GameConfig; + game: GameExtended; + emitter: Emitter; + + constructor(config) { + this.config = config; + this.emitter = createNanoEvents(); + } + + on = (event, callback) => { + return this.emitter.on(event, callback); + }; + + init = async (settings) => { + console.log("Booting"); + const whenIsBooted = new Promise((resolve) => { + this.game = new GameExtended( + { + ...this.config, + callbacks: { postBoot: () => resolve(true) }, + }, + settings, + this.emitter + ); + }); + await whenIsBooted; + console.log("Booted"); + console.log("Creating"); + + const whenSceneCreated = new Promise((resolve) => { + const MainScene = this.game.scene.keys.MainScene as MainScene; + MainScene.events.on("create", resolve); + }); + await whenSceneCreated; + + console.log("Created"); + + return this; + }; + initMultiplayer = async (settings) => {}; + + // TODO use this when ui modals are opened + lockInput = () => { + this.game.input.keyboard.enabled = false; + }; + unlockInput = () => { + this.game.input.keyboard.enabled = false; + }; + + get scene(): MainScene | null { + const mainScene = this.game?.scene?.keys?.MainScene as MainScene; + return mainScene ?? null; + } + get player(): Spaceship | null { + return this.scene?.player ?? null; + } + destroy = () => { + this.game.destroy(false); + }; +} + +export const gameManager = new GameManager(gameConfig); diff --git a/client/src/game/scripts/core/index.ts b/client/src/game/scripts/core/index.ts new file mode 100644 index 0000000..34f3cd6 --- /dev/null +++ b/client/src/game/scripts/core/index.ts @@ -0,0 +1,3 @@ +export * from "./gameConfig"; +export * from "./gameExtended"; +export * from "./gameManager"; diff --git a/client/src/game/scripts/index.ts b/client/src/game/scripts/index.ts index fc2eea7..1d43e54 100644 --- a/client/src/game/scripts/index.ts +++ b/client/src/game/scripts/index.ts @@ -1,3 +1,4 @@ -import { game, gameConfig } from "./core/game"; - -export { game, gameConfig }; +export * from "./core"; +export * from "./managers"; +export * from "./objects"; +export * from "./scenes"; diff --git a/client/src/game/scripts/scenes/mainScene.ts b/client/src/game/scripts/scenes/mainScene.ts index c781aeb..c37799a 100644 --- a/client/src/game/scripts/scenes/mainScene.ts +++ b/client/src/game/scripts/scenes/mainScene.ts @@ -1,7 +1,9 @@ import { InputManager, MobManager, SoundManager } from "~/managers"; import { Spaceship, GenericText } from "~/objects"; +import type { GameExtended } from "~/game/core"; export default class MainScene extends Phaser.Scene { + game: GameExtended; inputManager; soundManager; mobManager; diff --git a/client/src/game/scripts/scenes/preloadScene.ts b/client/src/game/scripts/scenes/preloadScene.ts index 3f4424e..e678b13 100644 --- a/client/src/game/scripts/scenes/preloadScene.ts +++ b/client/src/game/scripts/scenes/preloadScene.ts @@ -1,31 +1,41 @@ +import type { GameExtended } from "~/game/core"; + export default class PreloadScene extends Phaser.Scene { + game: GameExtended; constructor() { super({ key: "PreloadScene" }); } preload() { // Maps + this.game.outEmitter.emit("loading", { name: "Maps", progress: 1 }); this.load.atlas("map_1-1", "assets/maps/map_1-1.jpg", "assets/maps/map_1-1.json"); this.load.atlas("map_1-2", "assets/maps/map_1-2.webp", "assets/maps/map_1-2.json"); // Ships + this.game.outEmitter.emit("loading", { name: "Ships", progress: 2 }); this.load.atlas({ key: "F5S4", textureURL: "assets/ships/F5S4.png", normalMap: "assets/ships/F5S4N.png", atlasURL: "assets/ships/F5S4.json", }); + // Weapons + this.game.outEmitter.emit("loading", { name: "Weapons", progress: 3 }); this.load.spritesheet("laser", "assets/weapons/lasers/spr_bullet_strip02.png", { frameWidth: 95, frameHeight: 68, }); this.load.image("gatling", "assets/weapons/gatling/projectile.webp"); + // Modules + this.game.outEmitter.emit("loading", { name: "Modules", progress: 4 }); this.load.image("shield", "assets/ships/shield_Edit.png"); // Effects - // TODO is it better to use power of 2? + // TODO is it better to use powers of 2? + this.game.outEmitter.emit("loading", { name: "Effects", progress: 7 }); this.load.spritesheet("particles", "assets/effects/particles_1080x1080.png", { frameWidth: 1080, frameHeight: 1080, @@ -49,10 +59,12 @@ export default class PreloadScene extends Phaser.Scene { }); // UI + this.game.outEmitter.emit("loading", { name: "UI", progress: 10 }); this.load.image("joystick_1", "assets/ui/joystick_1.svg"); this.load.image("joystick_2", "assets/ui/joystick_2.svg"); // Sound Effects + this.game.outEmitter.emit("loading", { name: "Sound Effects", progress: 15 }); this.load.audio("laser_sound_1", "assets/weapons/lasers/laser1_short.mp3"); this.load.audio("laser_sound_2", "assets/weapons/lasers/laser2_short.mp3"); this.load.audio("laser_sound_3", "assets/weapons/lasers/laser3_short.mp3"); @@ -65,15 +77,22 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio("shield_down_sound_1", "assets/ships/shield_powerdown.mp3"); // Music + this.game.outEmitter.emit("loading", { name: "Music", progress: 20 }); this.load.audio("track_1", "assets/music/SMP1_THEME_Cargoship.mp3"); this.load.audio("track_2", "assets/music/SMP1_THEME_Gliese 1214b.mp3"); this.load.audio("track_3", "assets/music/SMP1_THEME_Space caravan.mp3"); this.load.audio("track_4", "assets/music/SMP1_THEME_Voyager.mp3"); + + this.game.outEmitter.emit("loading", { + name: "Preload Scene", + progress: 80, + }); } create() { // Scenes this.scene.start("MainScene"); + this.game.outEmitter.emit("loading", { name: "Main Scene", progress: 90 }); // Animations this.anims.create({ @@ -104,6 +123,7 @@ export default class PreloadScene extends Phaser.Scene { repeat: 0, hideOnComplete: true, }); + this.game.outEmitter.emit("loading", { name: "Animations", progress: 100 }); /** * This is how you would dynamically import the mainScene class (with code splitting), @@ -112,11 +132,11 @@ export default class PreloadScene extends Phaser.Scene { * The name of the chunk would be 'mainScene.chunk.js * Find more about code splitting here: https://webpack.js.org/guides/code-splitting/ */ - // let someCondition = true + // let someCondition = true; // if (someCondition) - // import(/* webpackChunkName: "mainScene" */ './mainScene').then(mainScene => { - // this.scene.add('MainScene', mainScene.default, true) - // }) - // else console.log('The mainScene class will not even be loaded by the browser') + // import(/* webpackChunkName: "mainScene" */ "./mainScene").then((mainScene) => { + // this.scene.add("MainScene", mainScene.default, true); + // }); + // else console.log("The mainScene class will not even be loaded by the browser"); } } diff --git a/client/src/ui/App.tsx b/client/src/ui/App.tsx index 0bd9f13..b28df0f 100644 --- a/client/src/ui/App.tsx +++ b/client/src/ui/App.tsx @@ -92,7 +92,7 @@ export const App = () => { {isLoaded && } {isLoaded && } - {mode === "mainMenu" &&
} + {!isLoaded &&
} {isLoaded && } diff --git a/client/src/ui/Root.tsx b/client/src/ui/Root.tsx index eaa9adb..a345d08 100644 --- a/client/src/ui/Root.tsx +++ b/client/src/ui/Root.tsx @@ -41,6 +41,13 @@ const theme: Partial = { p: "xl", }, }, + Progress: { + defaultProps: { + color: "cyan", + size: "xl", + style: { width: "100%" }, + }, + }, }, // TODO // headings: { diff --git a/client/src/ui/hooks/useGame.ts b/client/src/ui/hooks/useGame.ts index 7c08495..74d798e 100644 --- a/client/src/ui/hooks/useGame.ts +++ b/client/src/ui/hooks/useGame.ts @@ -3,11 +3,11 @@ import type { Spaceship } from "~/objects"; import { produce } from "immer"; import { create } from "zustand"; -import { game } from "~/game/core/game"; -import type { Game } from "~/game/core/game"; +import { gameManager } from "~/game/core/gameManager"; +import type { GameManager } from "~/game/core/gameManager"; interface GameStore { - game: Game | null; + gameManager: GameManager | null; mode: "mainMenu" | "singleplayer" | "multiplayer"; computed: { isLoading: boolean; @@ -21,7 +21,7 @@ interface GameStore { } export const useGame = create((set, get) => ({ - game: null, + gameManager: null, mode: "mainMenu", // https://github.com/pmndrs/zustand/issues/132 @@ -30,14 +30,14 @@ export const useGame = create((set, get) => ({ return get().mode !== "mainMenu" && !get().computed.isLoaded; }, get isLoaded() { - return !!get().game?.player; + return !!get().gameManager?.player; }, get player() { - return get().game?.player; + return get().gameManager?.player; }, get scene() { - return get().game?.scene; + return get().gameManager?.scene; }, }, @@ -47,11 +47,11 @@ export const useGame = create((set, get) => ({ state.mode = "singleplayer"; }) ); - const singleplayerGame = await game.init(settings); + const singleplayerGame = await gameManager.init(settings); set( produce((state) => { - state.game = singleplayerGame; + state.gameManager = singleplayerGame; }) ); }, @@ -61,18 +61,19 @@ export const useGame = create((set, get) => ({ state.mode = "multiplayer"; }) ); - const multiplayerGame = await game.init(settings); + const multiplayerGame = await gameManager.init(settings); + set( produce((state) => { - state.game = multiplayerGame; + state.gameManager = multiplayerGame; }) ); }, loadMainMenu: () => { - get().game.destroy(); + get().gameManager.destroy(); set( produce((state) => { - state.game = null; + state.gameManager = null; state.mode = "mainMenu"; }) ); diff --git a/client/src/ui/scenes/BottomRight/BottomRight.tsx b/client/src/ui/scenes/BottomRight/BottomRight.tsx index fc1c4a3..fa66e1c 100644 --- a/client/src/ui/scenes/BottomRight/BottomRight.tsx +++ b/client/src/ui/scenes/BottomRight/BottomRight.tsx @@ -21,16 +21,18 @@ export const BottomRight = ({ GroupComponent }) => { } }; - const handleLoadMainMenu = () => { + const handleMainMenu = () => { loadMainMenu(); }; return ( <> - + {isLoaded && ( + + )} diff --git a/client/src/ui/scenes/Center/Center.tsx b/client/src/ui/scenes/Center/Center.tsx index 06548f2..aba8c1b 100644 --- a/client/src/ui/scenes/Center/Center.tsx +++ b/client/src/ui/scenes/Center/Center.tsx @@ -1,31 +1,11 @@ import React from "react"; -import { Button } from "~/ui/components"; -import { useGame, useSettings } from "~/ui/hooks"; +import { useGame } from "~/ui/hooks"; +import { MainMenu } from "./scenes/MainMenu"; +import { Loading } from "./scenes/Loading"; export const Center = ({ GroupComponent }) => { - const { settings } = useSettings(); - const { mode, loadSingleplayer, loadMultiplayer } = useGame(); + const { mode } = useGame(); - const handleSingleplayer = () => { - if (mode === "mainMenu") { - loadSingleplayer(settings); - } - }; - const handleMultiplayer = () => { - if (mode === "mainMenu") { - loadMultiplayer(settings); - } - }; - - return ( - - - - - ); + return {mode === "mainMenu" ? : }; }; diff --git a/client/src/ui/scenes/Center/scenes/Loading/Loading.tsx b/client/src/ui/scenes/Center/scenes/Loading/Loading.tsx new file mode 100644 index 0000000..23d2e43 --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/Loading/Loading.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Progress, Title, Text, Group, Loader } from "@mantine/core"; + +import { useGameLoading } from "./hooks"; + +export const Loading = () => { + const { status } = useGameLoading(); + + return ( + <> + Loading... + + + + {status.name} + + + ); +}; diff --git a/client/src/ui/scenes/Center/scenes/Loading/hooks/index.ts b/client/src/ui/scenes/Center/scenes/Loading/hooks/index.ts new file mode 100644 index 0000000..4b15a39 --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/Loading/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useGameLoading"; diff --git a/client/src/ui/scenes/Center/scenes/Loading/hooks/useGameLoading.tsx b/client/src/ui/scenes/Center/scenes/Loading/hooks/useGameLoading.tsx new file mode 100644 index 0000000..a8fba49 --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/Loading/hooks/useGameLoading.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +import { gameManager } from "~/game"; + +export const useGameLoading = () => { + const [status, setStatus] = useState({ name: "Initializing", progress: 0 }); + + useEffect(() => { + const unbind = gameManager.on("loading", (loadingStatus) => { + setStatus(loadingStatus); + }); + + return unbind; + }, []); + + return { status }; +}; diff --git a/client/src/ui/scenes/Center/scenes/Loading/index.ts b/client/src/ui/scenes/Center/scenes/Loading/index.ts new file mode 100644 index 0000000..618e384 --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/Loading/index.ts @@ -0,0 +1 @@ +export * from "./Loading"; diff --git a/client/src/ui/scenes/Center/scenes/MainMenu/MainMenu.tsx b/client/src/ui/scenes/Center/scenes/MainMenu/MainMenu.tsx new file mode 100644 index 0000000..f34ace8 --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/MainMenu/MainMenu.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +import { Button } from "~/ui/components"; +import { useGame, useSettings } from "~/ui/hooks"; + +export const MainMenu = () => { + const { mode, loadSingleplayer, loadMultiplayer } = useGame(); + const { settings } = useSettings(); + + const handleSingleplayer = () => { + if (mode === "mainMenu") { + loadSingleplayer(settings); + } + }; + const handleMultiplayer = () => { + if (mode === "mainMenu") { + loadMultiplayer(settings); + } + }; + return ( + <> + + + + ); +}; diff --git a/client/src/ui/scenes/Center/scenes/MainMenu/index.ts b/client/src/ui/scenes/Center/scenes/MainMenu/index.ts new file mode 100644 index 0000000..2ad4cff --- /dev/null +++ b/client/src/ui/scenes/Center/scenes/MainMenu/index.ts @@ -0,0 +1 @@ +export * from "./MainMenu"; diff --git a/client/src/ui/scenes/TopLeft/TopLeft.tsx b/client/src/ui/scenes/TopLeft/TopLeft.tsx index 2765940..bac7f59 100644 --- a/client/src/ui/scenes/TopLeft/TopLeft.tsx +++ b/client/src/ui/scenes/TopLeft/TopLeft.tsx @@ -2,7 +2,6 @@ import React from "react"; import { useToggle } from "@mantine/hooks"; import { Settings } from "tabler-icons-react"; -import { game } from "~/game"; import { Button } from "~/ui/components"; import { SettingsModal } from "./scenes/SettingsModal"; import { EffectsMuteBtn } from "./scenes/EffectsMuteBtn"; diff --git a/client/tsconfig.json b/client/tsconfig.json index 5246fa4..00ba1e4 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "jsx": "react", "target": "es6", - "module": "es6", + "module": "esnext", "strict": true, "esModuleInterop": true,