perf: reduce mobile rendering and cull offscreen characters and building tiles
This commit is contained in:
@@ -51,7 +51,8 @@ test('downloaded footprints are deduplicated, closed, and generate finite merged
|
||||
const city=createBuildings(data,terrain);
|
||||
assert.equal(Object.values(city.counts).reduce((a,b)=>a+b,0),data.features.length);
|
||||
assert.ok(city.counts.tagged>0 && city.counts.levels>0 && city.counts.fallback>0);
|
||||
assert.ok(city.group.children.length<=3);
|
||||
assert.ok(city.group.children.length > 3, 'separate city districts can be culled independently');
|
||||
assert.ok(city.group.children.length < 100, 'batch buildings instead of one draw call per building');
|
||||
for(const mesh of city.group.children as Mesh[]) assert.ok(Array.from(mesh.geometry.attributes.position.array).every(Number.isFinite));
|
||||
city.dispose();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createFrameClock } from '../src/scene/frameClock.ts';
|
||||
|
||||
test('30 fps pacing retains elapsed simulation time on 60 and 120 Hz displays', () => {
|
||||
for (const hz of [60, 120]) {
|
||||
const clock = createFrameClock(30);
|
||||
let frames = 0, elapsed = 0;
|
||||
for (let i = 0; i <= hz * 10; i++) {
|
||||
const delta = clock.tick(i * 1000 / hz);
|
||||
if (delta !== undefined) { frames++; elapsed += delta; }
|
||||
}
|
||||
assert.equal(frames, 301);
|
||||
assert.ok(Math.abs(elapsed - 10) < 1e-8);
|
||||
}
|
||||
});
|
||||
|
||||
test('resume resets elapsed time instead of simulating the hidden interval', () => {
|
||||
const clock = createFrameClock(30);
|
||||
clock.tick(0); clock.tick(100);
|
||||
clock.reset();
|
||||
assert.equal(clock.tick(60000), 0);
|
||||
assert.equal(clock.tick(60001), undefined);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three';
|
||||
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, PerspectiveCamera, Raycaster, Vector3 } from 'three';
|
||||
import { createServerClock } from '../src/multiplayer/clock.ts';
|
||||
import { createRemotePlayers } from '../src/multiplayer/remotePlayers.ts';
|
||||
import type { Sheep } from '../src/scene/sheep.ts';
|
||||
@@ -16,6 +16,27 @@ test('server clock uses round trip midpoint and monotonic elapsed time', () => {
|
||||
assert.equal(clock.now(), 1001020);
|
||||
});
|
||||
|
||||
test('offscreen players skip posing but keep movement current and reappear when the camera turns', () => {
|
||||
const scene = new Group(), camera = new PerspectiveCamera(60, 1, .1, 100);
|
||||
let poses = 0;
|
||||
const template = { group: new Group(), clone() { return { group: new Group(), setEquipment() {},
|
||||
pose() { poses++; return true; }, dispose() {} } as unknown as Sheep; } } as Sheep;
|
||||
const remote = createRemotePlayers(template, scene, () => 0);
|
||||
const player: PlayerState = { id: 'other', nickname: '친구', x: 0, z: 10, yaw: 0, moving: false,
|
||||
emote: 'idle', equipment: { umbrella: false, raincoat: false, hood: false }, updatedAt: 1000, emoteStartedAt: 1000 };
|
||||
remote.synchronize([player], 'self');
|
||||
for (let i = 0; i < 300; i++) remote.update(1 / 30, 1000 + i * 1000 / 30, camera);
|
||||
assert.equal(poses, 0);
|
||||
assert.equal(remote.get('other')!.visible, false);
|
||||
remote.synchronize([{ ...player, x: 2, updatedAt: 11000 }], 'self');
|
||||
remote.update(.2, 11200, camera);
|
||||
assert.equal(remote.get('other')!.position.x, 2);
|
||||
camera.lookAt(2, 0, 10);
|
||||
assert.equal(remote.update(.03, 11230, camera), true);
|
||||
assert.equal(poses, 1);
|
||||
assert.equal(remote.get('other')!.visible, true);
|
||||
});
|
||||
|
||||
test('companion selection picks visible players and chooses the nearest reachable unoccupied side', () => {
|
||||
const scene = new Group();
|
||||
const geometry = new BoxGeometry(1, 2, 1), material = new MeshBasicMaterial();
|
||||
@@ -51,7 +72,8 @@ test('remote snapshot reconciles membership, equipment and interpolated motion w
|
||||
let disposed = 0;
|
||||
const template = { clone() { return { group: new Group(), setEquipment: (...v: boolean[]) => equipment.push(v),
|
||||
pose: () => false, dispose() { this.group.removeFromParent(); disposed++; } } as unknown as Sheep; } } as Sheep;
|
||||
const remote = createRemotePlayers(template, scene, () => 3);
|
||||
let heightSamples = 0;
|
||||
const remote = createRemotePlayers(template, scene, () => { heightSamples++; return 3; });
|
||||
const player: PlayerState = { id: 'other', nickname: '방문자', x: 0, z: 0, yaw: 0, moving: false,
|
||||
emote: 'idle', equipment: { umbrella: true, raincoat: true, hood: false, flashlight: true }, updatedAt: 1000, emoteStartedAt: 1000 };
|
||||
remote.synchronize([player, { ...player, id: 'self' }], 'self');
|
||||
@@ -63,6 +85,9 @@ test('remote snapshot reconciles membership, equipment and interpolated motion w
|
||||
remote.update(.2, 1300);
|
||||
assert.equal(scene.children[0].position.x, 1);
|
||||
assert.equal(scene.children[0].position.y, 3);
|
||||
const samplesAfterMoving = heightSamples;
|
||||
for (let i = 0; i < 300; i++) remote.update(1 / 30, 1300 + i * 1000 / 30);
|
||||
assert.equal(heightSamples, samplesAfterMoving, 'stationary players reuse their terrain height');
|
||||
remote.synchronize([], 'self');
|
||||
assert.equal(scene.children.length, 0);
|
||||
assert.equal(disposed, 1);
|
||||
|
||||
Reference in New Issue
Block a user