Skip to content

Commit

Permalink
fix: hot module replacement (#46)
Browse files Browse the repository at this point in the history
* fix: add empty dependency array to useEffect in ComponentRenderer

* fix: hot module replacement

* fix: self-revie
  • Loading branch information
alvrs committed Jun 30, 2022
1 parent 9e4a8c6 commit 6fedc88
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 87 deletions.
5 changes: 4 additions & 1 deletion packages/network/src/createSyncWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export function createSyncWorker<Cm extends Components>() {

// Pass in a "config stream", receive a stream of ECS events
const subscription = fromWorker<SyncWorkerConfig<Cm>, Output<Cm>>(worker, config$).subscribe(ecsEvent$);
const dispose = () => subscription?.unsubscribe();
const dispose = () => {
worker.terminate();
subscription?.unsubscribe();
};

return {
ecsEvent$,
Expand Down
1 change: 1 addition & 0 deletions packages/network/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface NetworkConfig {
clock: ClockConfig;
provider: ProviderConfig;
checkpointServiceUrl?: string;
initialBlockNumber?: number;
}

export interface ClockConfig {
Expand Down
6 changes: 4 additions & 2 deletions packages/network/src/workers/Sync.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,13 @@ async function getCheckpoint(
export type Output<Cm extends Components> = NetworkComponentUpdate<Cm>;

export class SyncWorker<Cm extends Components> implements DoWork<SyncWorkerConfig<Cm>, Output<Cm>> {
private config = observable.box<SyncWorkerConfig<Cm>>() as IObservableValue<SyncWorkerConfig<Cm>>;
private config = observable.box() as IObservableValue<SyncWorkerConfig<Cm>>;
private clientBlockNumber = 0;
private decoders: { [key: string]: Promise<(data: BytesLike) => unknown> | ((data: BytesLike) => unknown) } = {};
private componentIdToAddress: { [key: string]: Promise<string> } = {};
private toOutput$ = new Subject<Output<Cm>>();
private schemaCache = initCache<{ ComponentSchemas: [string[], number[]] }>("Global", ["ComponentSchemas"]);
private cacheWorker?: Worker;

constructor() {
this.init();
Expand Down Expand Up @@ -160,9 +161,10 @@ export class SyncWorker<Cm extends Components> implements DoWork<SyncWorkerConfi
toCacheAndOutput$.subscribe(this.toOutput$);

// 2. stream ECS events to the Cache worker to store them to IndexDB
this.cacheWorker = new Worker(new URL("./Cache.worker.ts", import.meta.url), { type: "module" });
if (!config.disableCache) {
fromWorker<Input<Cm>, boolean>(
new Worker(new URL("./Cache.worker.ts", import.meta.url), { type: "module" }),
this.cacheWorker,
combineLatest([
toCacheAndOutput$.pipe(startWith(undefined)),
of(config.worldContract.address),
Expand Down
22 changes: 16 additions & 6 deletions packages/phaserx/src/createInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,26 @@ export function createInput(inputPlugin: Phaser.Input.InputPlugin) {

const pointermove$ = fromEvent(document, "mousemove").pipe(
filter(() => enabled.current),
map(() => inputPlugin.activePointer)
map(() => {
return inputPlugin.manager?.activePointer;
}),
filterNullish()
);

const pointerdown$ = fromEvent(document, "mousedown").pipe(
filter(() => enabled.current),
map(() => inputPlugin.activePointer)
map(() => {
return inputPlugin.manager?.activePointer;
}),
filterNullish()
);

const pointerup$ = fromEvent(document, "mouseup").pipe(
filter(() => enabled.current),
map(() => inputPlugin.activePointer)
map(() => {
return inputPlugin.manager?.activePointer;
}),
filterNullish()
);

// Double click stream
Expand All @@ -51,7 +60,8 @@ export function createInput(inputPlugin: Phaser.Input.InputPlugin) {
map<Phaser.Input.Pointer, [boolean, number]>((pointer) => [pointer.leftButtonDown(), Date.now()]), // Map events to whether the left button is down and the current timestamp
bufferCount(2, 1), // Store the last two timestamps
filter(([prev, now]) => prev[0] && !now[0] && now[1] - prev[1] < 250), // Only care if button was pressed before and is not anymore and it happened within 500ms
map(() => inputPlugin.activePointer) // Return the current pointer
map(() => inputPlugin.manager?.activePointer), // Return the current pointer
filterNullish()
);

// Double click stream
Expand All @@ -61,7 +71,8 @@ export function createInput(inputPlugin: Phaser.Input.InputPlugin) {
bufferCount(2, 1), // Store the last two timestamps
filter(([prev, now]) => now - prev < 500), // Filter clicks with more than 500ms distance
throttleTime(500), // A third click within 500ms is not counted as another double click
map(() => inputPlugin.activePointer) // Return the current pointer
map(() => inputPlugin.manager?.activePointer), // Return the current pointer
filterNullish()
);

// Drag stream
Expand Down Expand Up @@ -156,7 +167,6 @@ export function createInput(inputPlugin: Phaser.Input.InputPlugin) {
drag$,
pressedKeys,
dispose,
phaserInputPlugin: inputPlugin,
disableInput,
enableInput,
enabled,
Expand Down
5 changes: 2 additions & 3 deletions packages/phaserx/src/createPhaserEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { createChunks } from "./createChunks";
import { createCamera } from "./createCamera";
import { createCulling } from "./createCulling";
import { createObjectPool } from "./createObjectPool";
import { createAnimatedTilemap } from "./tilemap/createAnimatedTilemap";
import { generateFrames } from "./utils/generateFrames";
import { createAnimatedTilemap } from "./tilemap";
import { generateFrames } from "./utils";
import { createInput } from "./createInput";

export async function createPhaserEngine<S extends ScenesConfig>(options: PhaserEngineConfig<S>) {
Expand Down Expand Up @@ -121,7 +121,6 @@ export async function createPhaserEngine<S extends ScenesConfig>(options: Phaser
scenes,
dispose: () => {
game.destroy(true);

for (const key of Object.keys(scenes)) {
const scene = scenes[key];
scene.camera.dispose();
Expand Down
1 change: 1 addition & 0 deletions packages/phaserx/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export {
export { load } from "./load";
export { getChunksInArea } from "./chunks";
export { getObjectsInArea } from "./area";
export { generateFrames } from "./generateFrames";
43 changes: 30 additions & 13 deletions packages/recs/src/Query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { filterNullish } from "@latticexyz/utils";
import { observable, ObservableSet } from "mobx";
import { filter, map, merge, Observable } from "rxjs";
import { concat, filter, from, map, merge, Observable } from "rxjs";
import {
componentValueEquals,
getComponentEntities,
Expand All @@ -26,6 +26,7 @@ import {
Schema,
SettingQueryFragment,
} from "./types";
import { toUpdateStream } from "./utils";

export function Has<T extends Schema>(component: Component<T>): HasQueryFragment<T> {
return { type: QueryFragmentType.Has, component };
Expand Down Expand Up @@ -228,15 +229,19 @@ export function runQuery(fragments: QueryFragment[], initialSet?: Set<EntityInde
* @returns Stream of updates for entities that are matching the query or used to match and now stopped matching the query
* Note: runOnInit was removed in V2. Make sure your queries are defined before any component update events arrive.
*/
export function defineQuery(fragments: EntityQueryFragment[]): {
export function defineQuery(
fragments: EntityQueryFragment[],
options?: { runOnInit?: boolean }
): {
update$: Observable<ComponentUpdate & { type: UpdateType }>;
matching: ObservableSet<EntityIndex>;
} {
const matching = observable(new Set<EntityIndex>());
const matching = observable(options?.runOnInit ? runQuery(fragments) : new Set<EntityIndex>());
const initial$ = from(matching).pipe(toUpdateStream(fragments[0].component));

return {
matching,
update$: merge(...fragments.map((f) => f.component.update$)) // Combine all component update streams accessed accessed in this query
const update$ = concat(
initial$,
merge(...fragments.map((f) => f.component.update$)) // Combine all component update streams accessed accessed in this query
.pipe(
map((update) => {
// If this entity matched the query before, check if it still matches it
Expand Down Expand Up @@ -264,7 +269,12 @@ export function defineQuery(fragments: EntityQueryFragment[]): {
}
}),
filterNullish()
),
)
);

return {
matching,
update$,
};
}

Expand All @@ -273,23 +283,30 @@ export function defineQuery(fragments: EntityQueryFragment[]): {
* @returns Stream of component updates of entities that had already matched the query
*/
export function defineUpdateQuery(
fragments: EntityQueryFragment[]
fragments: EntityQueryFragment[],
options?: { runOnInit?: boolean }
): Observable<ComponentUpdate & { type: UpdateType }> {
return defineQuery(fragments).update$.pipe(filter((e) => e.type === UpdateType.Update));
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Update));
}

/**
* @param fragments Query fragments
* @returns Stream of component updates of entities matching the query for the first time
*/
export function defineEnterQuery(fragments: EntityQueryFragment[]): Observable<ComponentUpdate> {
return defineQuery(fragments).update$.pipe(filter((e) => e.type === UpdateType.Enter));
export function defineEnterQuery(
fragments: EntityQueryFragment[],
options?: { runOnInit?: boolean }
): Observable<ComponentUpdate> {
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Enter));
}

/**
* @param fragments Query fragments
* @returns Stream of component updates of entities not matching the query anymore for the first time
*/
export function defineExitQuery(fragments: EntityQueryFragment[]): Observable<ComponentUpdate> {
return defineQuery(fragments).update$.pipe(filter((e) => e.type === UpdateType.Exit));
export function defineExitQuery(
fragments: EntityQueryFragment[],
options?: { runOnInit?: boolean }
): Observable<ComponentUpdate> {
return defineQuery(fragments, options).update$.pipe(filter((e) => e.type === UpdateType.Exit));
}
48 changes: 30 additions & 18 deletions packages/recs/src/System.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Observable } from "rxjs";
import { removeComponent, setComponent } from "./Component";
import { concat, EMPTY, from, Observable } from "rxjs";
import { getComponentEntities, removeComponent, setComponent } from "./Component";
import { UpdateType } from "./constants";
import { defineEnterQuery, defineExitQuery, defineQuery, defineUpdateQuery } from "./Query";
import { Component, ComponentUpdate, ComponentValue, EntityIndex, EntityQueryFragment, Schema, World } from "./types";
import { toUpdateStream } from "./utils";

export function defineRxSystem<T>(world: World, observable$: Observable<T>, system: (event: T) => void) {
const subscription = observable$.subscribe(system);
Expand All @@ -12,41 +13,47 @@ export function defineRxSystem<T>(world: World, observable$: Observable<T>, syst
export function defineUpdateSystem(
world: World,
query: EntityQueryFragment[],
system: (update: ComponentUpdate) => void
system: (update: ComponentUpdate) => void,
options?: { runOnInit?: boolean }
) {
defineRxSystem(world, defineUpdateQuery(query), system);
defineRxSystem(world, defineUpdateQuery(query, options), system);
}

export function defineEnterSystem(
world: World,
query: EntityQueryFragment[],
system: (update: ComponentUpdate) => void
system: (update: ComponentUpdate) => void,
options?: { runOnInit?: boolean }
) {
defineRxSystem(world, defineEnterQuery(query), system);
defineRxSystem(world, defineEnterQuery(query, options), system);
}

export function defineExitSystem(
world: World,
query: EntityQueryFragment[],
system: (update: ComponentUpdate) => void
system: (update: ComponentUpdate) => void,
options?: { runOnInit?: boolean }
) {
defineRxSystem(world, defineExitQuery(query), system);
defineRxSystem(world, defineExitQuery(query, options), system);
}

export function defineSystem(
world: World,
query: EntityQueryFragment[],
system: (update: ComponentUpdate & { type: UpdateType }) => void
system: (update: ComponentUpdate & { type: UpdateType }) => void,
options?: { runOnInit?: boolean }
) {
defineRxSystem(world, defineQuery(query).update$, system);
defineRxSystem(world, defineQuery(query, options).update$, system);
}

export function defineComponentSystem<S extends Schema>(
world: World,
component: Component<S>,
system: (update: ComponentUpdate<S>) => void
system: (update: ComponentUpdate<S>) => void,
options?: { runOnInit?: boolean }
) {
defineRxSystem(world, component.update$, system);
const initial$ = options?.runOnInit ? from(getComponentEntities(component)).pipe(toUpdateStream(component)) : EMPTY;
defineRxSystem(world, concat(initial$, component.update$), system);
}

/**
Expand All @@ -61,11 +68,16 @@ export function defineSyncSystem<T extends Schema>(
query: EntityQueryFragment[],
component: (entity: EntityIndex) => Component<T>,
value: (entity: EntityIndex) => ComponentValue<T>,
options?: { update: boolean }
options?: { update?: boolean; runOnInit?: boolean }
) {
defineSystem(world, query, ({ entity, type }) => {
if (type === UpdateType.Enter) setComponent(component(entity), entity, value(entity));
if (type === UpdateType.Exit) removeComponent(component(entity), entity);
if (options?.update && type === UpdateType.Update) setComponent(component(entity), entity, value(entity));
});
defineSystem(
world,
query,
({ entity, type }) => {
if (type === UpdateType.Enter) setComponent(component(entity), entity, value(entity));
if (type === UpdateType.Exit) removeComponent(component(entity), entity);
if (options?.update && type === UpdateType.Update) setComponent(component(entity), entity, value(entity));
},
options
);
}
22 changes: 15 additions & 7 deletions packages/recs/src/World.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function createWorld() {
const entityToIndex = new Map<EntityID, EntityIndex>();
const entities: EntityID[] = [];
const components: Component[] = [];
let disposers: (() => void)[] = [];
let disposers: [string, () => void][] = [];

function getEntityIndexStrict(entity: EntityID): EntityIndex {
const index = entityToIndex.get(entity);
Expand All @@ -24,15 +24,15 @@ export function createWorld() {
components.push(component);
}

function dispose() {
for (let i = 0; i < disposers.length; i++) {
disposers[i]();
function dispose(namespace?: string) {
for (const [, disposer] of disposers.filter((d) => !namespace || d[0] === namespace)) {
disposer();
}
disposers = [];
disposers = disposers.filter((d) => namespace && d[0] !== namespace);
}

function registerDisposer(disposer: () => void) {
disposers.push(disposer);
function registerDisposer(disposer: () => void, namespace = "") {
disposers.push([namespace, disposer]);
}

function hasEntity(entity: EntityID): boolean {
Expand All @@ -52,6 +52,14 @@ export function createWorld() {
};
}

export function namespaceWorld(world: ReturnType<typeof createWorld>, namespace: string) {
return {
...world,
registerDisposer: (disposer: () => void) => world.registerDisposer(disposer, namespace),
dispose: () => world.dispose(namespace),
};
}

// Design decision: don't store a list of components for each entity but compute it dynamically when needed
// because there are less components than entities and maintaining a list of components per entity is a large overhead
export function getEntityComponents(world: World, entity: EntityIndex): Component[] {
Expand Down
16 changes: 15 additions & 1 deletion packages/recs/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { Component, ComponentUpdate, Schema } from "./types";
import { map, pipe } from "rxjs";
import { getComponentValueStrict } from "./Component";
import { UpdateType } from "./constants";
import { Component, ComponentUpdate, EntityIndex, Schema } from "./types";

export function isComponentUpdate<S extends Schema>(
update: ComponentUpdate,
component: Component<S>
): update is ComponentUpdate<S> {
return update.component === component;
}

export function toUpdateStream<S extends Schema>(component: Component<S>) {
return pipe(
map((entity: EntityIndex) => {
const value = getComponentValueStrict(component, entity);
return { entity, component, value: [value, undefined], type: UpdateType.Enter } as ComponentUpdate<S> & {
type: UpdateType;
};
})
);
}
Loading

0 comments on commit 6fedc88

Please sign in to comment.