Skip to content

Commit

Permalink
Merge pull request #140 from kasper573/optimization
Browse files Browse the repository at this point in the history
Optimization
  • Loading branch information
kasper573 authored Feb 2, 2025
2 parents 92f0e22 + 7d479ad commit 9d0c43d
Show file tree
Hide file tree
Showing 27 changed files with 255 additions and 218 deletions.
3 changes: 2 additions & 1 deletion apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"@mp/time": "workspace:*",
"@solidjs/router": "0.14.7",
"@tanstack/solid-query": "5.56.2",
"solid-js": "1.9.3"
"solid-js": "1.9.3",
"uniqolor": "1.1.1"
},
"devDependencies": {
"@mp/server": "workspace:*",
Expand Down
14 changes: 4 additions & 10 deletions apps/client/src/integrations/sync.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Character, NPCInstance, WorldState } from "@mp/server";
import type { Character, WorldState } from "@mp/server";
import { type CharacterId } from "@mp/server";
import { SyncClient } from "@mp/sync/client";
import {
Expand All @@ -23,20 +23,14 @@ export function createSyncClient(auth: AuthClient) {
const sync = new SyncClient<WorldState>(env.wsUrl, () => ({
token: auth.identity()?.token,
}));
const worldState = createMutable<WorldState>({ characters: {}, npcs: {} });
const worldState = createMutable<WorldState>({ actors: {} });
const [characterId, setCharacterId] = createSignal<CharacterId | undefined>();
const character = createMemo(
() => worldState.characters[characterId()!] as Character | undefined,
() => worldState.actors[characterId()!] as Character | undefined,
);
const areaId = createMemo(() => character()?.areaId);
const [readyState, setReadyState] = createSignal(sync.getReadyState());
const actors = createMemo(
(): Array<Character | NPCInstance> =>
Object.values({
...worldState.characters,
...worldState.npcs,
}),
);
const actors = createMemo(() => Object.values(worldState.actors));
const actorsInArea = createMemo(() =>
actors().filter((actor) => actor.areaId === areaId()),
);
Expand Down
8 changes: 3 additions & 5 deletions apps/client/src/pages/game/Actor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import type { Vector } from "@mp/math";
import { Pixi } from "@mp/solid-pixi";
import type { TiledResource } from "@mp/data";
import { createEffect, Show } from "solid-js";
import type { MovementTrait, AppearanceTrait } from "@mp/server";
import type { AppearanceTrait, Actor } from "@mp/server";
import type { Pixel, Tile } from "@mp/std";
import { useAnimatedCoords } from "../../state/useAnimatedCoords";

export type ActorTrait = MovementTrait & AppearanceTrait;

export function Actor(props: { tiled: TiledResource; actor: ActorTrait }) {
export function Actor(props: { tiled: TiledResource; actor: Actor }) {
const coords = useAnimatedCoords(
() => props.actor.coords,
() => props.actor.path,
Expand All @@ -20,7 +18,7 @@ export function Actor(props: { tiled: TiledResource; actor: ActorTrait }) {
<ActorGraphics
tileSize={props.tiled.tileSize}
color={props.actor.color}
position={props.tiled.tileCoordToWorld(coords)}
position={props.tiled.tileCoordToWorld(coords())}
name={props.actor.name}
/>
);
Expand Down
30 changes: 19 additions & 11 deletions apps/client/src/pages/game/AreaDebugUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {
} from "solid-js";
import { Pixi } from "@mp/solid-pixi";
import { EngineContext } from "@mp/engine";
import type { Character } from "@mp/server";
import type { Actor, Character } from "@mp/server";
import type { TimeSpan } from "@mp/time";
import type { Pixel, Tile } from "@mp/std";
import uniqolor from "uniqolor";
import { env } from "../../env";
import { SyncClientContext } from "../../integrations/sync";
import { Select } from "../../ui/Select";
Expand All @@ -31,13 +32,13 @@ type VisibleGraphType = (typeof visibleGraphTypes)[number];

export function AreaDebugUI(props: {
area: AreaResource;
pathsToDraw: Path<Tile>[];
drawPathsForActors: Actor[];
}) {
const trpc = useTRPC();

const isTickEnabled = trpc.system.isTickEnabled.createQuery();
const setTickEnabled = trpc.system.setTickEnabled.createMutation();
const spawnNPC = trpc.npc.spawnProblematicNPC.createMutation(() => ({
const spawnNPC = trpc.npc.spawnRandomNPC.createMutation(() => ({
meta: { invalidateCache: false },
}));
const [visibleGraphType, setVisibleGraphType] =
Expand All @@ -46,8 +47,16 @@ export function AreaDebugUI(props: {
return (
<Pixi label="AreaDebugUI" isRenderGroup>
<DebugGraph area={props.area} visible={visibleGraphType} />
<For each={props.pathsToDraw}>
{(path) => <DebugPath tiled={props.area.tiled} path={path} />}
<For each={props.drawPathsForActors}>
{(actor) =>
actor.path ? (
<DebugPath
tiled={props.area.tiled}
path={actor.path}
color={uniqolor(actor.id).color}
/>
) : null
}
</For>
<div class={styles.debugMenu}>
<div on:pointerdown={(e) => e.stopPropagation()}>
Expand All @@ -71,9 +80,7 @@ export function AreaDebugUI(props: {
/>
</div>
<div>
<Button on:click={() => spawnNPC.mutate()}>
Spawn problematic NPC
</Button>
<Button on:click={() => spawnNPC.mutate()}>Spawn random NPC</Button>
</div>
</div>
<DebugText tiled={props.area.tiled} />
Expand Down Expand Up @@ -123,13 +130,14 @@ function DebugGraph(props: {
function DebugPath(props: {
tiled: TiledResource;
path: Path<Tile> | undefined;
color: string;
}) {
const gfx = new Graphics();

createEffect(() => {
gfx.clear();
if (props.path?.length) {
drawPath(gfx, props.path.map(props.tiled.tileCoordToWorld));
drawPath(gfx, props.path.map(props.tiled.tileCoordToWorld), props.color);
}
});

Expand Down Expand Up @@ -191,12 +199,12 @@ function drawGraphNode(
);
}

function drawPath(ctx: Graphics, path: Iterable<Vector<Pixel>>) {
function drawPath(ctx: Graphics, path: Iterable<Vector<Pixel>>, color: string) {
const [start, ...rest] = Array.from(path);

ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.strokeStyle = { width: 1, color: "purple" };
ctx.strokeStyle = { width: 1, color };
for (const { x, y } of rest) {
ctx.lineTo(x, y);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/pages/game/AreaScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function AreaScene(props: ParentProps<{ area: AreaResource }>) {
);

const myWorldPos = createMemo(() =>
props.area.tiled.tileCoordToWorld(myCoords),
props.area.tiled.tileCoordToWorld(myCoords()),
);

const cameraPos = useSpring(
Expand Down
13 changes: 9 additions & 4 deletions apps/client/src/pages/game/GamePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ export default function GamePage() {
return (
<SyncClientContext.Provider value={world}>
<Switch>
<Match when={world.readyState() !== "open" || area.isLoading}>
<Match
when={
world.readyState() !== "open" ||
area.isLoading ||
// AreaId would be null if the initial world state hasn't been received yet
!world.areaId()
}
>
{/** TODO replace with specialized loading screen for loading areas */}
<LoadingSpinner />
</Match>
Expand All @@ -46,9 +53,7 @@ export default function GamePage() {
<Show when={debug()}>
<AreaDebugUI
area={data}
pathsToDraw={world
.actorsInArea()
.flatMap((actor) => (actor.path ? [actor.path] : []))}
drawPathsForActors={world.actorsInArea()}
/>
</Show>
</AreaScene>
Expand Down
44 changes: 23 additions & 21 deletions apps/client/src/state/useAnimatedCoords.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { moveAlongPath } from "@mp/data";
import { EngineContext } from "@mp/engine";
import type { Path } from "@mp/math";
import { vec_copy, vec_distance, type Vector } from "@mp/math";
import { path_copy, vec_distance, type Vector } from "@mp/math";
import type { TimeSpan } from "@mp/time";
import {
type Accessor,
createEffect,
onCleanup,
useContext,
createMemo,
batch,
createSignal,
} from "solid-js";
import { createMutable } from "solid-js/store";

/**
* Creates a vector signal that lerps each frame along the current path.
Expand All @@ -21,44 +22,45 @@ export function useAnimatedCoords<T extends number>(
externalPath: Accessor<Path<T> | undefined>,
speed: Accessor<NoInfer<T>>,
snapDistance?: Accessor<NoInfer<T>>,
): Vector<NoInfer<T>> {
): Accessor<Vector<NoInfer<T>>> {
const engine = useContext(EngineContext);
const local = createMutable({
coords: vec_copy(externalCoords()),
path: externalPath()?.map(vec_copy),
});
const isMoving = createMemo(() => !!local.path);
const [localCoords, setLocalCoords] = createSignal(externalCoords());
const [localPath, setLocalPath] = createSignal(path_copy(externalPath()));
const isMoving = createMemo(() => !!localPath());

createEffect(() => {
if (isMoving()) {
onCleanup(engine.addFrameCallback(onFrame));
}
});

createEffect(() => {
// eslint-disable-next-line solid/reactivity
local.path = externalPath()?.map(vec_copy);
});
createEffect(() => setLocalPath(path_copy(externalPath())));

if (snapDistance) {
createEffect(() => {
const coords = externalCoords();
// If the distance between real and animated coords is too large, snap to real coords
if (vec_distance(coords, local.coords) >= snapDistance()) {
Object.assign(local.coords, coords);
if (vec_distance(coords, localCoords()) >= snapDistance()) {
setLocalCoords(coords);
}
});
}

function onFrame(deltaTime: TimeSpan) {
if (local.path) {
moveAlongPath(local.coords, local.path, speed(), deltaTime);
if (local.path.length === 0) {
delete local.path;
}
const path = localPath();
if (path) {
const [newCoords, newPath] = moveAlongPath(
localCoords(),
path,
speed(),
deltaTime,
);
batch(() => {
setLocalCoords(newCoords);
setLocalPath(newPath.length > 0 ? newPath : undefined);
});
}
}

// eslint-disable-next-line solid/reactivity
return local.coords;
return localCoords;
}
14 changes: 3 additions & 11 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const syncServer: WorldSyncServer = new SyncServer({
logger,
httpServer,
path: opt.wsEndpointPath,
initialState: { characters: {}, npcs: {} } satisfies WorldState,
initialState: { actors: {} } satisfies WorldState,
logSyncPatches: opt.logSyncPatches,
async handshake(_, { token }) {
const result = await auth.verifyToken(token as AuthToken);
Expand Down Expand Up @@ -140,17 +140,9 @@ collectUserMetrics(metrics, clients, syncServer);
collectPathFindingMetrics(metrics);

updateTicker.subscribe(npcAIBehavior(syncServer.access, areas));
updateTicker.subscribe(
movementBehavior(
syncServer.access,
(state) => [
...Object.values(state.characters),
...Object.values(state.npcs),
],
areas,
),
);
updateTicker.subscribe(movementBehavior(syncServer.access, areas));
updateTicker.subscribe(npcSpawnBehavior(syncServer.access, npcService, areas));
updateTicker.subscribe(syncServer.flush);
characterRemoveBehavior(clients, syncServer.access, logger, 5000);

clients.on(({ type, clientId, userId }) =>
Expand Down
13 changes: 11 additions & 2 deletions apps/server/src/metrics/collectUserMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MetricsRegistry } from "@mp/telemetry/prom";
import { MetricsGague } from "@mp/telemetry/prom";
import { recordValues } from "@mp/std";
import type { WorldSyncServer } from "../modules/world/WorldState";
import type { ClientRegistry } from "../ClientRegistry";

Expand All @@ -25,7 +26,11 @@ export function collectUserMetrics(
worldState.access(
"collectUserMetrics.active_character_count",
(state) => {
this.set(Object.values(state.characters).length);
this.set(
recordValues(state.actors)
.filter((actor) => actor.type === "character")
.toArray().length,
);
},
);
},
Expand All @@ -37,7 +42,11 @@ export function collectUserMetrics(
registers: [registry],
collect() {
worldState.access("collectUserMetrics.active_npc_count", (state) => {
this.set(Object.values(state.npcs).length);
this.set(
recordValues(state.actors)
.filter((actor) => actor.type === "npc")
.toArray().length,
);
});
},
});
Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/modules/character/characterRemoveBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { UserId } from "@mp/auth";
import type { StateAccess } from "@mp/sync/server";
import type { Logger } from "@mp/logger";
import { recordValues } from "@mp/std";
import type { ClientRegistry } from "../../ClientRegistry";
import type { WorldState } from "../world/WorldState";

Expand Down Expand Up @@ -35,10 +36,12 @@ export function characterRemoveBehavior(

function removeCharacter(userId: UserId) {
accessState("removeCharacter", (state) => {
for (const char of Object.values(state.characters)) {
for (const char of recordValues(state.actors).filter(
(actor) => actor.type === "character",
)) {
if (char.userId === userId) {
logger.info("Removing character", char.id);
delete state.characters[char.id];
delete state.actors[char.id];
}
}
});
Expand Down
Loading

0 comments on commit 9d0c43d

Please sign in to comment.