Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
[
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
"plugin:prettier/recommended",
],
rules: {
"quotes": [2, "double"],
"indent": ["warn", 2, { "SwitchCase": 1 }],
}
rules:
{ "quotes": [2, "double"], "indent": ["warn", 2, { "SwitchCase": 1 }] },
}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
## 🎮 Simple 2D Game Framework for [PixiJS](https://pixijs.com) using [Vite⚡](https://vitejs.dev/)

<a href="https://pixi-framework.onrender.com/" target="_blank">Demo</a>
<a href="https://pixi-framework.onrender.com/" target="_blank">Demo (old)</a>

### Highlights 🌟

- Latest PixiJS version (8)
- Typescript
- <a href="https://c.tenor.com/Hw0aKasI6B4AAAAC/fast-blazing-fast.gif" target="_blank">Blazing fast</a> builds and HMR through Vite
- Scene management
- Automagic asset loading per scene (sounds, spritesheets, textures, spine)
- Keyboard input handling
- Spine!
- Spine! (Waiting for PixiJS 8 support)

## Usage 🛠️

Expand Down
670 changes: 63 additions & 607 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"vite": "^4.0.4"
},
"dependencies": {
"@pixi/sound": "^5.2.0",
"@pixi/sound": "^6.0.0",
"gsap": "^3.11.4",
"pixi-spine": "^4.0.4",
"pixi.js": "^7.2.4"
"pixi.js": "^8.0.2",
"ts-key-enum": "^2.0.12"
}
}
File renamed without changes
Binary file removed public/Game/images/clouds_2.png
Binary file not shown.
Binary file removed public/Game/images/ground_1.png
Binary file not shown.
Binary file removed public/Game/images/ground_2.png
Binary file not shown.
Binary file removed public/Game/images/ground_3.png
Binary file not shown.
Binary file removed public/Game/images/plant.png
Binary file not shown.
Binary file removed public/Game/images/rocks.png
Binary file not shown.
Binary file removed public/Game/images/sky.png
Binary file not shown.
Binary file added public/Game/images/sky_island.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/Game/sounds/dash.ogg
Binary file not shown.
Binary file added public/Game/sounds/jump.mp3
Binary file not shown.
Binary file removed public/Game/sounds/jump2.wav
Binary file not shown.
6 changes: 0 additions & 6 deletions public/Game/spine/vine-pro.atlas

This file was deleted.

Binary file removed public/Game/spine/vine-pro.png
Binary file not shown.
Binary file removed public/Game/spine/vine-pro.skel
Binary file not shown.
Binary file removed public/Loading/images/bgNight.webp
Binary file not shown.
Binary file added public/Loading/images/loading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 0 additions & 23 deletions src/config.ts

This file was deleted.

13 changes: 9 additions & 4 deletions src/core/AssetLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Assets } from "pixi.js";
import { Debug } from "../utils/debug";
import "pixi-spine";

type Asset = {
name: string;
Expand All @@ -25,14 +24,20 @@ export default class AssetLoader {
return Object.keys(assetFiles);
}

async loadAssetsGroup(group: string) {
async loadAssetsGroup(
group: string,
onProgress?: (progress: number) => void
) {
const sceneAssets = this.manifest.filter((asset) => asset.group === group);

for (const asset of sceneAssets) {
Assets.add(asset.name, asset.url);
Assets.add({ alias: asset.name, src: asset.url });
}

const resources = await Assets.load(sceneAssets.map((asset) => asset.name));
const resources = await Assets.load(
sceneAssets.map((asset) => asset.name),
onProgress
);

Debug.log("✅ Loaded assets group", group, resources);

Expand Down
93 changes: 31 additions & 62 deletions src/core/Keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,17 @@
import { utils } from "pixi.js";
export type KeyCallback = (data: { state: "up" | "down" }) => void;

export default class Keyboard extends utils.EventEmitter {
export default class Keyboard {
private static instance: Keyboard;

static states = {
ACTION: "ACTION",
};

static actions = {
UP: "UP",
DOWN: "DOWN",
LEFT: "LEFT",
RIGHT: "RIGHT",
JUMP: "JUMP",
SHIFT: "SHIFT",
} as const;

static actionKeyMap = {
[Keyboard.actions.UP]: "KeyW",
[Keyboard.actions.DOWN]: "KeyS",
[Keyboard.actions.LEFT]: "KeyA",
[Keyboard.actions.RIGHT]: "KeyD",
[Keyboard.actions.JUMP]: "Space",
[Keyboard.actions.SHIFT]: "ShiftLeft",
} as const;

static allKeys = Object.values(Keyboard.actionKeyMap);

static keyActionMap = Object.entries(Keyboard.actionKeyMap).reduce(
(acc, [key, action]) => {
acc[action] = key as keyof typeof Keyboard.actions;

return acc;
},
{} as Record<string, keyof typeof Keyboard.actionKeyMap>
);

private keyMap = new Map<string, boolean>();
private callbacks = new Map<string, KeyCallback[]>();

private constructor() {
super();

this.listenToKeyEvents();
}

private listenToKeyEvents() {
document.addEventListener("keydown", (e) => this.onKeyPress(e.code));
document.addEventListener("keyup", (e) => this.onKeyRelease(e.code));
document.addEventListener("keydown", (e) => this.onKeyDown(e.code));
document.addEventListener("keyup", (e) => this.onKeyUp(e.code));
}

public static getInstance(): Keyboard {
Expand All @@ -57,39 +22,43 @@ export default class Keyboard extends utils.EventEmitter {
return Keyboard.instance;
}

public getAction(action: keyof typeof Keyboard.actions): boolean {
return this.isKeyDown(Keyboard.actionKeyMap[action]);
public registerKey(key: string, callback?: KeyCallback): void {
if (!this.callbacks.has(key)) {
this.callbacks.set(key, []);
}

this.callbacks.get(key).push(callback);
}

public onAction(
callback: (e: {
action: keyof typeof Keyboard.actions;
buttonState: "pressed" | "released";
}) => void
): void {
this.on(Keyboard.states.ACTION, callback);
public unregisterKey(key: string, callback?: KeyCallback): void {
if (!this.callbacks.has(key)) return;

const callbacks = this.callbacks.get(key);
const index = callbacks.indexOf(callback);

if (index !== -1) {
callbacks.splice(index, 1);
} else {
callbacks.length = 0;
}
}

private onKeyPress(key: string): void {
if (this.isKeyDown(key) || !(key in Keyboard.keyActionMap)) return;
private onKeyDown(key: string): void {
if (this.isKeyDown(key)) return;

this.keyMap.set(key, true);

this.emit(Keyboard.states.ACTION, {
action: Keyboard.keyActionMap[key],
buttonState: "pressed",
});
}
if (!this.callbacks.has(key)) return;

private onKeyRelease(key: string): void {
if (!(key in Keyboard.keyActionMap)) return;
this.callbacks.get(key).forEach((callback) => callback({ state: "down" }));
}

private onKeyUp(key: string): void {
this.keyMap.set(key, false);

this.emit(Keyboard.states.ACTION, {
action: Keyboard.keyActionMap[key],
buttonState: "released",
});
if (!this.callbacks.has(key)) return;

this.callbacks.get(key).forEach((callback) => callback({ state: "up" }));
}

public isKeyDown(key: string): boolean {
Expand Down
4 changes: 1 addition & 3 deletions src/core/Scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ export interface Scene {
unload?(): void | Promise<void>;
start?(): void | Promise<void>;
onResize?(width: number, height: number): void;
update?(delta: number): void;
onUpdate?(delta: number): void;
}

export abstract class Scene extends Container {
abstract name: string;

constructor(protected utils: SceneUtils) {
super();
}
Expand Down
71 changes: 37 additions & 34 deletions src/core/SceneManager.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import { Application } from "pixi.js";
import { Application, ApplicationOptions } from "pixi.js";
import Scene from "./Scene";
import AssetLoader from "./AssetLoader";
import { Debug } from "../utils/debug";

export interface SceneUtils {
assetLoader: AssetLoader;
}

export default class SceneManager {
private sceneConstructors = this.importScenes();
type SceneConstructor = ConstructorType<typeof Scene>;

export default class SceneManager {
app: Application;
sceneInstances = new Map<string, Scene>();
sceneInstances = new Map<SceneConstructor, Scene>();
currentScene?: Scene;

constructor() {
this.app = new Application({
view: document.querySelector("#app") as HTMLCanvasElement,
this.app = new Application();
}

async init(options: Partial<ApplicationOptions> = {}) {
await this.app.init({
canvas: document.querySelector("#app") as HTMLCanvasElement,
autoDensity: true,
resizeTo: window,
powerPreference: "high-performance",
backgroundColor: 0x23272a,
...options,
});

// @ts-expect-error Set PIXI app to global window object for the PIXI Inspector
Debug.log(`🎨 Rendering context: ${this.app.renderer.name}`);

window.__PIXI_APP__ = this.app;

window.addEventListener("resize", (ev: UIEvent) => {
Expand All @@ -32,35 +38,24 @@ export default class SceneManager {
});

this.app.ticker.add(() => {
this.currentScene?.update?.(this.app.ticker.elapsedMS);
this.currentScene?.onUpdate?.(this.app.ticker.elapsedMS);
});
}

importScenes() {
const sceneModules = import.meta.glob("/src/scenes/*.ts", {
eager: true,
}) as Record<string, { default: ConstructorType<typeof Scene> }>;

return Object.entries(sceneModules).reduce((acc, [path, module]) => {
const fileName = path.split("/").pop()?.split(".")[0];

if (!fileName) throw new Error("Error while parsing filename");

acc[fileName] = module.default;

return acc;
}, {} as Record<string, ConstructorType<typeof Scene>>);
}

async switchScene(sceneName: string, deletePrevious = true): Promise<Scene> {
async switchScene(
scene: SceneConstructor,
deletePrevious = true
): Promise<Scene> {
await this.removeScene(deletePrevious);

this.currentScene = this.sceneInstances.get(sceneName);
Debug.log(`🔀 Switching to scene ${scene.name}`);

this.currentScene = this.sceneInstances.get(scene);

if (!this.currentScene) this.currentScene = await this.initScene(sceneName);
if (!this.currentScene) this.currentScene = await this.initScene(scene);

if (!this.currentScene)
throw new Error(`Failed to initialize scene: ${sceneName}`);
throw new Error(`Failed to initialize scene: ${scene}`);

this.app.stage.addChild(this.currentScene);

Expand All @@ -72,10 +67,18 @@ export default class SceneManager {
private async removeScene(destroyScene: boolean) {
if (!this.currentScene) return;

if (destroyScene) {
this.sceneInstances.delete(this.currentScene.name);
Debug.log(
`🔀 Removing scene ${this.currentScene.constructor.name} ${
destroyScene && "and destroying it"
}`
);

if (destroyScene) {
this.currentScene.destroy({ children: true });

this.sceneInstances.delete(
this.currentScene.constructor as SceneConstructor
);
} else {
this.app.stage.removeChild(this.currentScene);
}
Expand All @@ -85,14 +88,14 @@ export default class SceneManager {
this.currentScene = undefined;
}

private async initScene(sceneName: string) {
private async initScene(sceneConstructor: SceneConstructor) {
const sceneUtils = {
assetLoader: new AssetLoader(),
};

const scene = new this.sceneConstructors[sceneName](sceneUtils);
const scene = new sceneConstructor(sceneUtils);

this.sceneInstances.set(sceneName, scene);
this.sceneInstances.set(sceneConstructor, scene);

if (scene.load) await scene.load();

Expand Down
Loading