-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@mud/phaserx): add @mud/phaserx
- Loading branch information
Showing
46 changed files
with
2,339 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"printWidth": 120, | ||
"semi": true, | ||
"tabWidth": 2, | ||
"useTabs": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}, | ||
}; | ||
} |
Oops, something went wrong.