Skip to content

Commit

Permalink
feat(@mud/phaserx): add @mud/phaserx
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs committed May 13, 2022
1 parent aaf6d0f commit cbefb71
Show file tree
Hide file tree
Showing 46 changed files with 2,339 additions and 8 deletions.
2 changes: 2 additions & 0 deletions packages/phaserx/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
10 changes: 10 additions & 0 deletions packages/phaserx/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
2 changes: 2 additions & 0 deletions packages/phaserx/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
1 change: 1 addition & 0 deletions packages/phaserx/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
6 changes: 6 additions & 0 deletions packages/phaserx/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"printWidth": 120,
"semi": true,
"tabWidth": 2,
"useTabs": false
}
3 changes: 3 additions & 0 deletions packages/phaserx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @mud/phaserx

A highly scalable engine building on top of phaser
7 changes: 7 additions & 0 deletions packages/phaserx/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["tests"],
};
41 changes: 41 additions & 0 deletions packages/phaserx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@mud/phaserx",
"license": "MIT",
"version": "0.0.1",
"main": "src/index.ts",
"scripts": {
"lint": "eslint . --ext .ts",
"build": "rimraf dist && rollup -c rollup.config.js",
"test": "jest"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-typescript": "^8.3.1",
"@types/jest": "^27.4.1",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"eslint": "^8.10.0",
"jest": "^27.5.1",
"mobx": "^6.4.2",
"phaser": "3.60.0-beta.4",
"prettier": "^2.6.0",
"rimraf": "^3.0.2",
"rollup": "^2.70.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rxjs": "^7.5.5",
"ts-jest": "^27.1.3",
"tslib": "^2.3.1",
"typescript": "^4.6.2",
"@mud/utils": "0.0.1"
},
"peerDependencies": {
"mobx": "^6.5.0",
"phaser": "3.60.0-beta.4",
"rxjs": "^7.5.5",
"@mud/utils": "0.0.1"
},
"dependencies": {
"@use-gesture/vanilla": "^10.2.9"
}
}
16 changes: 16 additions & 0 deletions packages/phaserx/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import typescript from "@rollup/plugin-typescript";
import commonjs from "@rollup/plugin-commonjs";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import { nodeResolve } from "@rollup/plugin-node-resolve";

import { defineConfig } from "rollup";

export default defineConfig({
input: "./src/index.ts",
treeshake: true,
output: {
dir: "dist",
sourcemap: true,
},
plugins: [nodeResolve(), commonjs(), typescript(), peerDepsExternal()],
});
10 changes: 10 additions & 0 deletions packages/phaserx/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum AssetType {
Image,
SpriteSheet,
MultiAtlas,
}

export const GameObjectClasses = {
Sprite: Phaser.GameObjects.Sprite,
Rectangle: Phaser.GameObjects.Rectangle,
};
75 changes: 75 additions & 0 deletions packages/phaserx/src/createCamera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Gesture } from "@use-gesture/vanilla";
import { BehaviorSubject, map, sampleTime, scan, Subject, throttleTime } from "rxjs";
import { Camera, CameraConfig, GestureState, ObjectPool } from "./types";

export function createCamera(phaserCamera: Phaser.Cameras.Scene2D.Camera, options: CameraConfig): Camera {
const phaserGame = document.getElementById(options.phaserSelector);
if (!phaserGame) {
throw new Error("Could not connect camera input. No element with id " + options.phaserSelector);
}

document.addEventListener("gesturestart", (e) => e.preventDefault());
document.addEventListener("gesturechange", (e) => e.preventDefault());

const worldView$ = new BehaviorSubject<Phaser.Cameras.Scene2D.Camera["worldView"]>(phaserCamera.worldView);
const zoom$ = new BehaviorSubject<number>(phaserCamera.zoom);
const wheelStream$ = new Subject<GestureState<"onWheel">>();
const pinchStream$ = new Subject<GestureState<"onPinch">>();

const gesture = new Gesture(
phaserGame,
{
onPinch: (state: any) => pinchStream$.next(state),
onWheel: (state: any) => wheelStream$.next(state),
},
{}
);

// function getNearestLevel(currentZoom: number): number {
// return Math.pow(2, Math.floor(Math.log(currentZoom * 2) / Math.log(2))) / 2;
// }

const pinchSub = pinchStream$
.pipe(
throttleTime(10),
map((state) => state.offset[0]), // Compute pinch speed
map((zoom) => Math.min(Math.max(zoom, options.minZoom), options.maxZoom)), // Limit zoom values
scan((acc, curr) => [acc[1], curr], [1, 1]) // keep track of the last value to offset the map position (not implemented yet)
)
.subscribe(([, zoom]) => {
// Set the gesture zoom state to the current zoom value to avoid zooming beyond the max values
if (gesture._ctrl.state.pinch) gesture._ctrl.state.pinch.offset[0] = zoom;
phaserCamera.setZoom(zoom);
worldView$.next(phaserCamera.worldView);
zoom$.next(zoom);
});

const wheelSub = wheelStream$
.pipe(
sampleTime(10),
map((state) => state.delta.map((x) => x * options.wheelSpeed)), // Compute wheel speed
map((movement) => movement.map((m) => m / phaserCamera.zoom)), // Adjust for current zoom value
map((movement) => [phaserCamera.scrollX + movement[0], phaserCamera.scrollY + movement[1]]) // Compute new pinch
)
.subscribe(([x, y]) => {
phaserCamera.setScroll(x, y);
worldView$.next(phaserCamera.worldView);
});

function ignore(objectPool: ObjectPool, ignore: boolean) {
objectPool.ignoreCamera(phaserCamera.id, ignore);
}

return {
phaserCamera,
worldView$,
zoom$,
ignore,
dispose: () => {
pinchSub.unsubscribe();
wheelSub.unsubscribe();
gesture.destroy();
},
};
}
31 changes: 31 additions & 0 deletions packages/phaserx/src/createChunks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { map, Observable, Subject } from "rxjs";
import { Area, ChunkCoord } from "./types";
import { CoordMap, getChunksInArea, subtract } from "./utils";

export function createChunks(worldView$: Observable<Area>, chunkSize: number) {
const visibleChunks = { current: new CoordMap<boolean>() };

const addedChunks$ = new Subject<ChunkCoord>();
const removedChunks$ = new Subject<ChunkCoord>();

const visibleChunkStream = worldView$.pipe(
map((area) => getChunksInArea(area, chunkSize)) // Calculate current chunks from the world view
);

visibleChunkStream.subscribe((newVisibleChunks) => {
const added = subtract(newVisibleChunks, visibleChunks.current); // Chunks that are visible not but not before
for (const coord of added.coords()) addedChunks$.next(coord);

const removed = subtract(visibleChunks.current, newVisibleChunks); // Chunks that were visible before but not now
for (const coord of removed.coords()) removedChunks$.next(coord);

visibleChunks.current = newVisibleChunks;
});

return {
addedChunks$: addedChunks$.asObservable(),
removedChunks$: removedChunks$.asObservable(),
chunkSize,
visibleChunks,
};
}
105 changes: 105 additions & 0 deletions packages/phaserx/src/createCulling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { computed, observe, reaction } from "mobx";
import { from, map, mergeMap, pipe } from "rxjs";
import { filterNullish } from "@mud/utils";
import { Camera, ChunkCoord, Chunks, Coord, EmbodiedEntity, ObjectPool } from "./types";
import { CoordMap, pixelToChunkCoord, coordEq } from "./utils";

function createRegistry() {
const coordToIds = new CoordMap<Set<string>>();
const idToCoord = new Map<string, Coord>();

function get(coord: Coord): Set<string> {
let set = coordToIds.get(coord);
if (!set) {
set = new Set<string>();
coordToIds.set(coord, set);
}
return set;
}

function set(id: string, coord: Coord) {
// Remove from prev set
const prevCoord = idToCoord.get(id);
const idsAtPrevCoord = prevCoord && get(prevCoord);
idsAtPrevCoord?.delete(id);

// Add to new set
const idsAtNewCoord = get(coord);
idsAtNewCoord.add(id);

// Set new idToCoord mapping
idToCoord.set(id, coord);
}

function remove(id: string) {
const prevCoord = idToCoord.get(id);
const idsAtCoord = prevCoord && get(prevCoord);
idsAtCoord?.delete(id);
idToCoord.delete(id);
}

return { set, remove, get };
}

export function createCulling(objectPool: ObjectPool, camera: Camera, chunks: Chunks) {
const chunkRegistry = createRegistry();
const disposer = new Map<string, () => void>();

const chunkToEntity = pipe(
map((chunk: ChunkCoord) => from(chunkRegistry.get(chunk))), // Map to streams of entityIds
mergeMap((entities) => entities), // Flatten the stream of entities
map((entityId) => objectPool.get(entityId, "Existing")), // Map entityId to embodiedEntity
filterNullish()
);

// Spawn entities when their chunk appears in the viewport
const addedChunkSub = chunks.addedChunks$.pipe(chunkToEntity).subscribe((entity) => entity.spawn());

// Despawn entites when their chunk disappears from the viewport
const removedChunkSub = chunks.removedChunks$.pipe(chunkToEntity).subscribe((entity) => entity.despawn());

// Keep track of entity's chunk
function trackEntity(entity: EmbodiedEntity<never>) {
if (disposer.get(entity.id)) console.error("Entity is being tracked multiple times", entity);
const chunk = computed(() => pixelToChunkCoord(entity.position, chunks.chunkSize), { equals: coordEq });
const dispose = reaction(
() => chunk.get(),
(newChunk) => {
// Register the new chunk position
chunkRegistry.set(entity.id, newChunk);

// Check whether entity is in the viewport if it switched chunks
const visible = chunks.visibleChunks.current.get(newChunk);
if (visible) {
entity.spawn();
} else {
entity.despawn();
}
},
{ fireImmediately: true }
);
disposer.set(entity.id, dispose);
}

// Setup tracking of entity chunks
const disposeObjectPoolObserver = observe(objectPool.objects, (change) => {
if (change.type === "add") {
trackEntity(change.newValue as EmbodiedEntity<never>);
}
if (change.type === "delete") {
chunkRegistry.remove(change.oldValue.id);
const dispose = disposer.get(change.oldValue.id);
if (dispose) dispose();
disposer.delete(change.oldValue.id);
}
});

return {
dispose: () => {
for (const d of disposer.values()) d();
disposeObjectPoolObserver();
addedChunkSub.unsubscribe();
removedChunkSub.unsubscribe();
},
};
}
Loading

0 comments on commit cbefb71

Please sign in to comment.