-
+
- Change Scene
+ changeScene()}>
+ Change Scene
+
- Toggle Movement
+ moveSprite()}
+ >
+ Toggle Movement
+
-
Sprite Position:
+
+ Sprite Position:
{`{\n x: ${spritePosition.x}\n y: ${spritePosition.y}\n}`}
- Add New Sprite
+ addSprite()}>
+ Add New Sprite
+
- )
+ );
}
-export default App
+export default App;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..bf5e3b3
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,31 @@
+import '@/styles/globals.css';
+import { Inter } from 'next/font/google';
+import type { Metadata, Viewport } from 'next';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export default function RootLayout({
+ // Layouts must accept a children prop.
+ // This will be populated with nested layouts or pages
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export const metadata: Metadata = {
+ title: 'Phaser Nextjs Template',
+ description:
+ 'A Phaser 3 Next.js project template that demonstrates Next.js with React communication and uses Vite for bundling.',
+ icons: { icon: '/favicon.png' },
+};
+
+export const viewport: Viewport = {
+ width: 'device-width',
+ initialScale: 1,
+};
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..a2cb9f4
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,9 @@
+import App from '@/App';
+
+export default async function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/src/game/EventBus.ts b/src/game/EventBus.ts
index 154ecbc..c7be37c 100644
--- a/src/game/EventBus.ts
+++ b/src/game/EventBus.ts
@@ -2,4 +2,4 @@ import { Events } from 'phaser';
// Used to emit events between React components and Phaser scenes
// https://newdocs.phaser.io/docs/3.70.0/Phaser.Events.EventEmitter
-export const EventBus = new Events.EventEmitter();
\ No newline at end of file
+export const EventBus = new Events.EventEmitter();
diff --git a/src/game/PhaserGame.tsx b/src/game/PhaserGame.tsx
index ec2b1a2..efcf624 100644
--- a/src/game/PhaserGame.tsx
+++ b/src/game/PhaserGame.tsx
@@ -1,86 +1,50 @@
-import { forwardRef, useEffect, useLayoutEffect, useRef } from 'react';
+import { RefObject, useEffect, useLayoutEffect } from 'react';
import StartGame from './main';
import { EventBus } from './EventBus';
-export interface IRefPhaserGame
-{
+interface IRefPhaserGame {
game: Phaser.Game | null;
scene: Phaser.Scene | null;
}
-interface IProps
-{
- currentActiveScene?: (scene_instance: Phaser.Scene) => void
+interface IPropsPhaserGame {
+ ref: RefObject
;
+ onChangeScene: () => void;
}
-export const PhaserGame = forwardRef(function PhaserGame({ currentActiveScene }, ref)
-{
- const game = useRef(null!);
-
- useLayoutEffect(() =>
- {
- if (game.current === null)
- {
-
- game.current = StartGame("game-container");
-
- if (typeof ref === 'function')
- {
- ref({ game: game.current, scene: null });
- } else if (ref)
- {
- ref.current = { game: game.current, scene: null };
- }
-
+const PhaserGame = ({ ref, onChangeScene }: IPropsPhaserGame) => {
+ useLayoutEffect(() => {
+ if (ref.current === null) {
+ const game = StartGame('game-container');
+ ref.current = { game: game, scene: null };
}
- return () =>
- {
- if (game.current)
- {
- game.current.destroy(true);
- if (game.current !== null)
- {
- game.current = null;
+ return () => {
+ if (ref.current?.game) {
+ const game = ref.current.game;
+ game.destroy(true);
+ if (ref.current.game !== null) {
+ ref.current.game = null;
}
+ ref.current = null;
}
- }
+ };
}, [ref]);
- useEffect(() =>
- {
- EventBus.on('current-scene-ready', (scene_instance: Phaser.Scene) =>
- {
- if (currentActiveScene && typeof currentActiveScene === 'function')
- {
-
- currentActiveScene(scene_instance);
+ useEffect(() => {
+ EventBus.on('current-scene-ready', (scene_instance: Phaser.Scene) => {
+ const prevRef = ref.current as IRefPhaserGame;
- }
-
- if (typeof ref === 'function')
- {
-
- ref({ game: game.current, scene: scene_instance });
-
- } else if (ref)
- {
-
- ref.current = { game: game.current, scene: scene_instance };
+ ref.current = Object.assign(prevRef, { scene: scene_instance });
- }
-
+ onChangeScene();
});
- return () =>
- {
-
+ return () => {
EventBus.removeListener('current-scene-ready');
-
- }
- }, [currentActiveScene, ref]);
+ };
+ }, [ref, onChangeScene]);
- return (
-
- );
+ return
;
+};
-});
+export { PhaserGame as default, type IRefPhaserGame, type IPropsPhaserGame };
diff --git a/src/game/main.ts b/src/game/main.ts
index 1ca80d7..84b0e49 100644
--- a/src/game/main.ts
+++ b/src/game/main.ts
@@ -1,31 +1,24 @@
-import { Boot } from './scenes/Boot';
-import { GameOver } from './scenes/GameOver';
-import { Game as MainGame } from './scenes/Game';
+import { Boot } from './scenes/loaders/Boot';
+import { Preloader } from './scenes/loaders/Preloader';
import { MainMenu } from './scenes/MainMenu';
+import { Game as MainGame } from './scenes/Game';
+import { GameOver } from './scenes/GameOver';
import { AUTO, Game } from 'phaser';
-import { Preloader } from './scenes/Preloader';
+import CONFIG from './utils/config';
// Find out more information about the Game Config at:
// https://newdocs.phaser.io/docs/3.70.0/Phaser.Types.Core.GameConfig
const config: Phaser.Types.Core.GameConfig = {
type: AUTO,
- width: 1024,
- height: 768,
+ width: CONFIG.WIDTH,
+ height: CONFIG.HEIGHT,
parent: 'game-container',
backgroundColor: '#028af8',
- scene: [
- Boot,
- Preloader,
- MainMenu,
- MainGame,
- GameOver
- ]
+ scene: [Boot, Preloader, MainMenu, MainGame, GameOver],
};
const StartGame = (parent: string) => {
-
return new Game({ ...config, parent });
-
-}
+};
export default StartGame;
diff --git a/src/game/scenes/Game.ts b/src/game/scenes/Game.ts
index 19db45c..9eb1ed5 100644
--- a/src/game/scenes/Game.ts
+++ b/src/game/scenes/Game.ts
@@ -1,36 +1,35 @@
-import { EventBus } from '../EventBus';
-import { Scene } from 'phaser';
+import { GameScene } from './common/GameScene';
-export class Game extends Scene
-{
- camera: Phaser.Cameras.Scene2D.Camera;
- background: Phaser.GameObjects.Image;
- gameText: Phaser.GameObjects.Text;
-
- constructor ()
- {
+export class Game extends GameScene {
+ constructor() {
super('Game');
}
- create ()
- {
- this.camera = this.cameras.main;
+ create() {
this.camera.setBackgroundColor(0x00ff00);
-
- this.background = this.add.image(512, 384, 'background');
this.background.setAlpha(0.5);
- this.gameText = this.add.text(512, 384, 'Make something fun!\nand share it with us:\nsupport@phaser.io', {
- fontFamily: 'Arial Black', fontSize: 38, color: '#ffffff',
- stroke: '#000000', strokeThickness: 8,
- align: 'center'
- }).setOrigin(0.5).setDepth(100);
+ this.sceneText = this.add
+ .text(
+ 512,
+ 384,
+ 'Make something fun!\nand share it with us:\nsupport@phaser.io',
+ {
+ fontFamily: 'Arial Black',
+ fontSize: 38,
+ color: '#ffffff',
+ stroke: '#000000',
+ strokeThickness: 8,
+ align: 'center',
+ }
+ )
+ .setOrigin(0.5)
+ .setDepth(100);
- EventBus.emit('current-scene-ready', this);
+ this.eventBus.emit('current-scene-ready', this);
}
- changeScene ()
- {
+ changeScene() {
this.scene.start('GameOver');
}
}
diff --git a/src/game/scenes/GameOver.ts b/src/game/scenes/GameOver.ts
index 34cd62b..ba3e89c 100644
--- a/src/game/scenes/GameOver.ts
+++ b/src/game/scenes/GameOver.ts
@@ -1,36 +1,30 @@
-import { EventBus } from '../EventBus';
-import { Scene } from 'phaser';
+import { GameScene } from './common/GameScene';
-export class GameOver extends Scene
-{
- camera: Phaser.Cameras.Scene2D.Camera;
- background: Phaser.GameObjects.Image;
- gameOverText : Phaser.GameObjects.Text;
-
- constructor ()
- {
+export class GameOver extends GameScene {
+ constructor() {
super('GameOver');
}
- create ()
- {
- this.camera = this.cameras.main
+ create() {
this.camera.setBackgroundColor(0xff0000);
-
- this.background = this.add.image(512, 384, 'background');
this.background.setAlpha(0.5);
- this.gameOverText = this.add.text(512, 384, 'Game Over', {
- fontFamily: 'Arial Black', fontSize: 64, color: '#ffffff',
- stroke: '#000000', strokeThickness: 8,
- align: 'center'
- }).setOrigin(0.5).setDepth(100);
-
- EventBus.emit('current-scene-ready', this);
+ this.sceneText = this.add
+ .text(512, 384, 'Game Over', {
+ fontFamily: 'Arial Black',
+ fontSize: 64,
+ color: '#ffffff',
+ stroke: '#000000',
+ strokeThickness: 8,
+ align: 'center',
+ })
+ .setOrigin(0.5)
+ .setDepth(100);
+
+ this.eventBus.emit('current-scene-ready', this);
}
- changeScene ()
- {
+ changeScene() {
this.scene.start('MainMenu');
}
}
diff --git a/src/game/scenes/MainMenu.ts b/src/game/scenes/MainMenu.ts
index fa90cca..ccc6f94 100644
--- a/src/game/scenes/MainMenu.ts
+++ b/src/game/scenes/MainMenu.ts
@@ -1,60 +1,48 @@
-import { GameObjects, Scene } from 'phaser';
+import { GameScene } from './common/GameScene';
+import { GameObjects } from 'phaser';
-import { EventBus } from '../EventBus';
+export class MainMenu extends GameScene {
+ logo: GameObjects.Image | undefined;
+ logoTween: Phaser.Tweens.Tween | undefined;
-export class MainMenu extends Scene
-{
- background: GameObjects.Image;
- logo: GameObjects.Image;
- title: GameObjects.Text;
- logoTween: Phaser.Tweens.Tween | null;
-
- constructor ()
- {
+ constructor() {
super('MainMenu');
}
- create ()
- {
- this.background = this.add.image(512, 384, 'background');
-
+ create() {
this.logo = this.add.image(512, 300, 'logo').setDepth(100);
- this.title = this.add.text(512, 460, 'Main Menu', {
- fontFamily: 'Arial Black', fontSize: 38, color: '#ffffff',
- stroke: '#000000', strokeThickness: 8,
- align: 'center'
- }).setOrigin(0.5).setDepth(100);
+ this.sceneText = this.add
+ .text(512, 460, 'Main Menu', {
+ fontFamily: 'Arial Black',
+ fontSize: 38,
+ color: '#ffffff',
+ stroke: '#000000',
+ strokeThickness: 8,
+ align: 'center',
+ })
+ .setOrigin(0.5)
+ .setDepth(100);
- EventBus.emit('current-scene-ready', this);
+ this.eventBus.emit('current-scene-ready', this);
}
-
- changeScene ()
- {
- if (this.logoTween)
- {
+
+ changeScene() {
+ if (this.logoTween) {
this.logoTween.stop();
- this.logoTween = null;
+ this.logoTween = undefined;
}
-
this.scene.start('Game');
}
- moveLogo (reactCallback: ({ x, y }: { x: number, y: number }) => void)
- {
- if (this.logoTween)
- {
- if (this.logoTween.isPlaying())
- {
+ moveLogo(reactCallback: ({ x, y }: { x: number; y: number }) => void) {
+ if (this.logoTween) {
+ if (this.logoTween.isPlaying()) {
this.logoTween.pause();
- }
- else
- {
+ } else {
this.logoTween.play();
}
- }
- else
- {
+ } else {
this.logoTween = this.tweens.add({
targets: this.logo,
x: { value: 750, duration: 3000, ease: 'Back.easeInOut' },
@@ -62,14 +50,14 @@ export class MainMenu extends Scene
yoyo: true,
repeat: -1,
onUpdate: () => {
- if (reactCallback)
- {
+ if (reactCallback && this.logo) {
reactCallback({
+ // why not use the tween args?
x: Math.floor(this.logo.x),
- y: Math.floor(this.logo.y)
+ y: Math.floor(this.logo.y),
});
}
- }
+ },
});
}
}
diff --git a/src/game/scenes/common/GameScene.ts b/src/game/scenes/common/GameScene.ts
new file mode 100644
index 0000000..dd3154e
--- /dev/null
+++ b/src/game/scenes/common/GameScene.ts
@@ -0,0 +1,24 @@
+import { Scene } from 'phaser';
+import { EventBus } from '@/game/EventBus';
+
+export abstract class GameScene extends Scene {
+ eventBus: Phaser.Events.EventEmitter;
+ // @ts-expect-error see init()
+ background: Phaser.GameObjects.Image;
+ // @ts-expect-error see init()
+ camera: Phaser.Cameras.Scene2D.Camera;
+ sceneText: Phaser.GameObjects.Text | undefined;
+ abstract changeScene(): void;
+
+ constructor(key: string) {
+ super(key);
+ this.eventBus = EventBus;
+ }
+
+ // https://github.com/phaserjs/phaser/pull/7039
+ init() {
+ // all scenes use this same background
+ this.background = this.add.image(512, 384, 'background');
+ this.camera = this.cameras.main;
+ }
+}
diff --git a/src/game/scenes/Boot.ts b/src/game/scenes/loaders/Boot.ts
similarity index 80%
rename from src/game/scenes/Boot.ts
rename to src/game/scenes/loaders/Boot.ts
index 404ea09..67e48f9 100644
--- a/src/game/scenes/Boot.ts
+++ b/src/game/scenes/loaders/Boot.ts
@@ -1,22 +1,18 @@
import { Scene } from 'phaser';
-export class Boot extends Scene
-{
- constructor ()
- {
+export class Boot extends Scene {
+ constructor() {
super('Boot');
}
- preload ()
- {
+ preload() {
// The Boot Scene is typically used to load in any assets you require for your Preloader, such as a game logo or background.
// The smaller the file size of the assets, the better, as the Boot Scene itself has no preloader.
this.load.image('background', 'assets/bg.png');
}
- create ()
- {
+ create() {
this.scene.start('Preloader');
}
}
diff --git a/src/game/scenes/Preloader.ts b/src/game/scenes/loaders/Preloader.ts
similarity index 84%
rename from src/game/scenes/Preloader.ts
rename to src/game/scenes/loaders/Preloader.ts
index ca77176..936893b 100644
--- a/src/game/scenes/Preloader.ts
+++ b/src/game/scenes/loaders/Preloader.ts
@@ -1,14 +1,11 @@
import { Scene } from 'phaser';
-export class Preloader extends Scene
-{
- constructor ()
- {
+export class Preloader extends Scene {
+ constructor() {
super('Preloader');
}
- init ()
- {
+ init() {
// We loaded this image in our Boot Scene, so we can display it here
this.add.image(512, 384, 'background');
@@ -16,19 +13,16 @@ export class Preloader extends Scene
this.add.rectangle(512, 384, 468, 32).setStrokeStyle(1, 0xffffff);
// This is the progress bar itself. It will increase in size from the left based on the % of progress.
- const bar = this.add.rectangle(512-230, 384, 4, 28, 0xffffff);
+ const bar = this.add.rectangle(512 - 230, 384, 4, 28, 0xffffff);
// Use the 'progress' event emitted by the LoaderPlugin to update the loading bar
this.load.on('progress', (progress: number) => {
-
// Update the progress bar (our bar is 464px wide, so 100% = 464px)
- bar.width = 4 + (460 * progress);
-
+ bar.width = 4 + 460 * progress;
});
}
- preload ()
- {
+ preload() {
// Load the assets for the game - Replace with your own assets
this.load.setPath('assets');
@@ -36,8 +30,7 @@ export class Preloader extends Scene
this.load.image('star', 'star.png');
}
- create ()
- {
+ create() {
// When all the assets have loaded, it's often worth creating global objects here that the rest of the game can use.
// For example, you can define global animations here, so we can use them in other scenes.
diff --git a/src/game/utils/config.ts b/src/game/utils/config.ts
new file mode 100644
index 0000000..cbfbbd2
--- /dev/null
+++ b/src/game/utils/config.ts
@@ -0,0 +1,6 @@
+const CONFIG = {
+ WIDTH: 1024,
+ HEIGHT: 768,
+};
+
+export default CONFIG;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
deleted file mode 100644
index a7a790f..0000000
--- a/src/pages/_app.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import "@/styles/globals.css";
-import type { AppProps } from "next/app";
-
-export default function App({ Component, pageProps }: AppProps) {
- return ;
-}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
deleted file mode 100644
index 2f4bae4..0000000
--- a/src/pages/_document.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
deleted file mode 100644
index a303a14..0000000
--- a/src/pages/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import Head from "next/head";
-import { Inter } from "next/font/google";
-import styles from "@/styles/Home.module.css";
-import dynamic from "next/dynamic";
-
-const inter = Inter({ subsets: ["latin"] });
-
-const AppWithoutSSR = dynamic(() => import("@/App"), { ssr: false });
-
-export default function Home() {
- return (
- <>
-
- Phaser Nextjs Template
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/src/styles/Home.module.css b/src/styles/Home.module.css
deleted file mode 100644
index e69de29..0000000
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 3c67ca6..7bc03ce 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -2,7 +2,7 @@ body {
margin: 0;
padding: 0;
color: rgba(255, 255, 255, 0.87);
- background-color: #000000;
+ background-color: #000000;
font-family: Arial, Helvetica, sans-serif;
}
@@ -45,4 +45,4 @@ body {
border: 1px solid rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.3);
}
-}
\ No newline at end of file
+}
diff --git a/tsconfig.json b/tsconfig.json
index cff5a3a..73693cb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,22 +1,36 @@
{
- "compilerOptions": {
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "strictPropertyInitialization": false,
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"],
+ "@game/*": ["./src/game/*"],
+ "@scenes*": ["./src/game/scenes/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
}