perf: huge entity gainz

This commit is contained in:
cha0s 2024-07-31 21:07:17 -05:00
parent b53d7e3d35
commit a4949bd7a0
12 changed files with 335 additions and 321 deletions

View File

@ -145,7 +145,7 @@ export default class Component {
toJSON() { toJSON() {
return Component.constructor.filterDefaults(this); return Component.constructor.filterDefaults(this);
} }
update(values) { async update(values) {
for (const key in values) { for (const key in values) {
if (concrete.properties[key]) { if (concrete.properties[key]) {
this[`$$${key}`] = values[key]; this[`$$${key}`] = values[key];
@ -214,10 +214,12 @@ export default class Component {
} }
async updateMany(entities) { async updateMany(entities) {
const promises = [];
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const [entityId, values] = entities[i]; const [entityId, values] = entities[i];
this.get(entityId).update(values); promises.push(this.get(entityId).update(values));
} }
return Promise.all(promises);
} }
} }

View File

@ -14,12 +14,14 @@ export default class Sprite extends Component {
} }
set anchorX(anchorX) { set anchorX(anchorX) {
this.$$anchor = {x: anchorX, y: this.anchorY}; this.$$anchor = {x: anchorX, y: this.anchorY};
super.anchorX = anchorX;
} }
get anchorY() { get anchorY() {
return this.$$anchor.y; return this.$$anchor.y;
} }
set anchorY(anchorY) { set anchorY(anchorY) {
this.$$anchor = {x: this.anchorX, y: anchorY}; this.$$anchor = {x: this.anchorX, y: anchorY};
super.anchorY = anchorY;
} }
get animation() { get animation() {
return super.animation; return super.animation;
@ -77,12 +79,14 @@ export default class Sprite extends Component {
} }
set scaleX(scaleX) { set scaleX(scaleX) {
this.$$scale = {x: scaleX, y: this.scaleY}; this.$$scale = {x: scaleX, y: this.scaleY};
super.scaleX = scaleX;
} }
get scaleY() { get scaleY() {
return this.$$scale.y; return this.$$scale.y;
} }
set scaleY(scaleY) { set scaleY(scaleY) {
this.$$scale = {x: this.scaleX, y: scaleY}; this.$$scale = {x: this.scaleX, y: scaleY};
super.scaleY = scaleY;
} }
get size() { get size() {
if (!this.$$sourceJson.frames) { if (!this.$$sourceJson.frames) {
@ -98,6 +102,32 @@ export default class Sprite extends Component {
const {elapsed, ...rest} = super.toNet(recipient, data); const {elapsed, ...rest} = super.toNet(recipient, data);
return rest; return rest;
} }
update(values) {
for (const key in values) {
switch (key) {
case 'anchorX': {
this.$$anchor.x = values[key];
break;
}
case 'anchorY': {
this.$$anchor.y = values[key];
break;
}
case 'scaleX': {
this.$$scale.x = values[key];
break;
}
case 'scaleY': {
this.$$scale.y = values[key];
break;
}
default: {
continue;
}
}
}
return super.update(values);
}
}; };
} }
async load(instance) { async load(instance) {
@ -123,6 +153,6 @@ export default class Sprite extends Component {
scaleY: {defaultValue: 1, type: 'float32'}, scaleY: {defaultValue: 1, type: 'float32'},
source: {type: 'string'}, source: {type: 'string'},
speed: {type: 'float32'}, speed: {type: 'float32'},
tint: {type: 'uint32'}, tint: {defaultValue: 0xffffff, type: 'uint32'},
}; };
} }

View File

@ -0,0 +1,18 @@
import {Graphics} from '@pixi/graphics';
export default class Aabb extends Graphics {
constructor({color, width = 0.5}) {
super();
this.$$color = color;
this.$$width = width;
}
redraw({x0, y0, x1, y1}) {
this.clear();
this.lineStyle(this.$$width, this.$$color);
this.moveTo(x0, y0);
this.lineTo(x1, y0);
this.lineTo(x1, y1);
this.lineTo(x0, y1);
this.lineTo(x0, y0);
}
}

View File

@ -1,19 +0,0 @@
import {Graphics} from '@pixi/react';
import {memo, useCallback} from 'react';
function Aabb({color, width = 0.5, x0, y0, x1, y1}) {
const draw = useCallback((g) => {
g.clear();
g.lineStyle(width, color);
g.moveTo(x0, y0);
g.lineTo(x1, y0);
g.lineTo(x1, y1);
g.lineTo(x0, y1);
g.lineTo(x0, y0);
}, [color, width, x0, x1, y0, y1]);
return (
<Graphics draw={draw} />
);
}
export default memo(Aabb);

View File

@ -0,0 +1,21 @@
import {Graphics} from '@pixi/graphics';
export default function crosshair() {
const g = new Graphics();
g.clear();
g.lineStyle(1, 0x000000);
g.moveTo(-5, 0);
g.lineTo(5, 0);
g.moveTo(0, -5);
g.lineTo(0, 5);
g.lineStyle(0.5, 0xffffff);
g.moveTo(-5, 0);
g.lineTo(5, 0);
g.moveTo(0, -5);
g.lineTo(0, 5);
g.lineStyle(1, 0x000000);
g.drawCircle(0, 0, 3);
g.lineStyle(0.5, 0xffffff);
g.drawCircle(0, 0, 3);
return g;
}

View File

@ -1,27 +0,0 @@
import {Graphics} from '@pixi/react';
import {memo, useCallback} from 'react';
function Crosshair() {
const draw = useCallback((g) => {
g.clear();
g.lineStyle(1, 0x000000);
g.moveTo(-5, 0);
g.lineTo(5, 0);
g.moveTo(0, -5);
g.lineTo(0, 5);
g.lineStyle(0.5, 0xffffff);
g.moveTo(-5, 0);
g.lineTo(5, 0);
g.moveTo(0, -5);
g.lineTo(0, 5);
g.lineStyle(1, 0x000000);
g.drawCircle(0, 0, 3);
g.lineStyle(0.5, 0xffffff);
g.drawCircle(0, 0, 3);
}, []);
return (
<Graphics draw={draw} />
);
}
export default memo(Crosshair);

View File

@ -4,65 +4,85 @@ import {Container} from '@pixi/react';
import {useCallback, useEffect, useRef, useState} from 'react'; import {useCallback, useEffect, useRef, useState} from 'react';
import {usePacket} from '@/react/context/client.js'; import {usePacket} from '@/react/context/client.js';
import {useDebug} from '@/react/context/debug.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/react/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import {useRadians} from '@/react/context/radians.js'; import {useRadians} from '@/react/context/radians.js';
import Entity from './entity.jsx'; import Entity from './entity.js';
export default function Entities({monopolizers, particleWorker}) { export default function Entities({monopolizers, particleWorker}) {
const [debug] = useDebug();
const [ecs] = useEcs(); const [ecs] = useEcs();
const containerRef = useRef();
const entities = useRef({}); const entities = useRef({});
const pool = useRef([]);
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
const radians = useRadians(); const radians = useRadians();
const [willInteractWith, setWillInteractWith] = useState(0); const willInteractWith = useRef(0);
const [interactionFilters] = useState([ const [interactionFilters] = useState([
new AdjustmentFilter(), new AdjustmentFilter(),
new GlowFilter({color: 0x0}), new GlowFilter({color: 0x0}),
]); ]);
const [, forceRender] = useState(); const pulse = (Math.cos(radians / 4) + 1) * 0.5;
interactionFilters[0].brightness = (pulse * 0.75) + 1;
interactionFilters[1].outerStrength = pulse * 0.5;
const updateEntities = useCallback((payload) => {
if (0 === Object.keys(entities.current).length) {
for (let i = 0; i < 1000; ++i) {
pool.current.push(new Entity());
}
}
for (const id in payload) {
if (!payload[id]) {
entities.current[id].removeFromContainer();
pool.current.push(entities.current[id]);
delete entities.current[id];
continue;
}
const entity = ecs.get(id);
if (!entity.Position) {
continue;
}
if (!entities.current[id]) {
entities.current[id] = pool.current.length > 0 ? pool.current.pop() : new Entity();
entities.current[id].reset(entity, debug);
}
if (mainEntity === id) {
entities.current[id].setMainEntity();
}
entities.current[id].update(payload[id]);
entities.current[id].addToContainer(containerRef.current);
}
}, [debug, ecs, mainEntity])
useEffect(() => {
for (const key in entities.current) {
entities.current[key].setDebug(debug);
}
}, [debug]);
usePacket('EcsChange', async () => {
for (const id in entities.current) {
entities.current[id].removeFromContainer();
}
entities.current = {};
});
const onEcsTickEntities = useCallback((payload) => {
updateEntities(payload);
}, [updateEntities]);
useEcsTick(onEcsTickEntities);
useEffect(() => { useEffect(() => {
if (!ecs || !particleWorker) { if (!ecs || !particleWorker) {
return; return;
} }
async function onMessage(diff) { async function onMessage(diff) {
await ecs.apply(diff.data); await ecs.apply(diff.data);
for (const id in diff.data) { updateEntities(diff.data);
if (!diff.data[id]) {
delete entities.current[id]
}
else {
entities.current[id] = ecs.get(id);
}
}
forceRender(Math.random());
} }
particleWorker.addEventListener('message', onMessage); particleWorker.addEventListener('message', onMessage);
return () => { return () => {
particleWorker.removeEventListener('message', onMessage); particleWorker.removeEventListener('message', onMessage);
}; };
}, [ecs, particleWorker]); }, [ecs, particleWorker, updateEntities]);
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
interactionFilters[0].brightness = (pulse * 0.75) + 1;
interactionFilters[1].outerStrength = pulse * 0.5;
usePacket('EcsChange', async () => {
entities.current = {};
});
const onEcsTickEntities = useCallback((payload, ecs) => {
for (const id in payload) {
if ('1' === id) {
continue;
}
const update = payload[id];
if (false === update) {
delete entities.current[id];
continue;
}
entities.current[id] = ecs.get(id);
}
forceRender(Math.random());
}, []);
useEcsTick(onEcsTickEntities);
const onEcsTickParticles = useCallback((payload) => { const onEcsTickParticles = useCallback((payload) => {
for (const id in payload) { for (const id in payload) {
const update = payload[id]; const update = payload[id];
@ -77,26 +97,25 @@ export default function Entities({monopolizers, particleWorker}) {
const onEcsTickInteractions = useCallback((payload, ecs) => { const onEcsTickInteractions = useCallback((payload, ecs) => {
const main = ecs.get(mainEntity); const main = ecs.get(mainEntity);
if (main) { if (main) {
setWillInteractWith(main.Interacts.willInteractWith); if (willInteractWith.current !== main.Interacts.willInteractWith) {
if (entities.current[willInteractWith.current]) {
entities.current[willInteractWith.current].diffuse.filters = [];
}
willInteractWith.current = main.Interacts.willInteractWith;
}
const interacting = entities.current[main.Interacts.willInteractWith];
if (interacting) {
interacting.diffuse.filters = 0 === monopolizers.length
? interactionFilters
: [];
}
} }
}, [mainEntity]); }, [interactionFilters, mainEntity, monopolizers]);
useEcsTick(onEcsTickInteractions); useEcsTick(onEcsTickInteractions);
const renderables = [];
for (const id in entities.current) {
const isHighlightedInteraction = 0 === monopolizers.length && id == willInteractWith;
renderables.push(
<Entity
filters={isHighlightedInteraction ? interactionFilters : null}
entity={entities.current[id]}
key={id}
/>
);
}
return ( return (
<Container <Container
ref={containerRef}
sortableChildren sortableChildren
> />
{renderables}
</Container>
); );
} }

View File

@ -0,0 +1,191 @@
import {Assets} from '@pixi/assets';
import {Container as PixiContainer} from '@pixi/display';
import {Sprite} from '@pixi/sprite';
import Aabb from './aabb.js';
import createCrosshair from './crosshair.js';
import {deferredLighting, PointLight} from './lights.js';
export default class Entity {
static assets = {};
attached = false;
colliderAabb = new Aabb({color: 0xffffff});
container = new PixiContainer();
crosshair = createCrosshair();
debug = new PixiContainer();
diffuse = new Sprite();
isMainEntity = false;
normals = new Sprite();
point;
sprite = new PixiContainer();
visibleAabb = new Aabb({color: 0xff00ff});
constructor() {
this.diffuse.parentGroup = deferredLighting.diffuseGroup;
this.normals.parentGroup = deferredLighting.normalGroup;
this.container.addChild(this.diffuse);
this.container.addChild(this.normals);
this.container.addChild(this.crosshair);
this.debug.addChild(this.colliderAabb);
this.debug.addChild(this.visibleAabb);
}
addToContainer(container) {
if (this.attached || !container) {
return;
}
container.addChild(this.container);
container.addChild(this.debug);
this.attached = true;
}
static async loadAsset(source = '') {
if (!this.assets['']) {
const {Texture} = await import('@pixi/core');
this.assets[''] = {data: {meta: {}}, textures: {'': Texture.WHITE}};
}
if (!this.assets[source]) {
this.assets[source] = Assets.load(source);
}
return this.assets[source];
}
removeFromContainer() {
this.container.parent.removeChild(this.container);
this.debug.parent.removeChild(this.debug);
this.attached = false;
}
reset(entity, debug) {
this.entity = entity;
if (this.light) {
this.container.removeChild(this.light);
this.light = undefined;
}
this.setDebug(debug);
this.isMainEntity = false;
if (this.interactionAabb) {
this.debug.removeChild(this.interactionAabb);
}
this.interactionAabb = undefined;
}
setDebug(isDebugging) {
if (isDebugging) {
this.crosshair.alpha = 1;
this.debug.alpha = 1;
}
else {
this.crosshair.alpha = 0;
this.debug.alpha = 0;
}
}
setMainEntity() {
if (this.isMainEntity) {
return;
}
this.isMainEntity = true;
this.interactionAabb = new Aabb({color: 0x00ff00});
this.debug.addChild(this.interactionAabb);
}
static textureFromAsset(asset, animation, frame) {
if (!asset) {
return undefined;
}
let texture;
if (asset.data.animations) {
if (!animation) {
return undefined;
}
texture = asset.animations[animation][frame];
}
else {
texture = asset.textures[''];
}
return texture;
}
update({Direction, Light, Position, Sprite, VisibleAabb}) {
if (Light) {
if (!this.light) {
this.light = new PointLight(
0xffffff - 0x2244cc,
0,
);
}
this.light.brightness = Light.brightness;
}
if (Position) {
const {x, y} = this.entity.Position;
this.container.x = x;
this.container.y = y;
this.container.zIndex = y;
}
if (Sprite) {
const {diffuse, normals} = this;
if (!this.attached || 'alpha' in Sprite) {
diffuse.alpha = normals.alpha = this.entity.Sprite.alpha;
}
if (!this.attached || 'anchorX' in Sprite) {
diffuse.anchor.x = normals.anchor.x = this.entity.Sprite.anchorX;
}
if (!this.attached || 'anchorY' in Sprite) {
diffuse.anchor.y = normals.anchor.y = this.entity.Sprite.anchorY;
}
if (!this.attached || 'scaleX' in Sprite) {
diffuse.scale.x = normals.scale.x = this.entity.Sprite.scaleX;
}
if (!this.attached || 'scaleY' in Sprite) {
diffuse.scale.y = normals.scale.y = this.entity.Sprite.scaleY;
}
if (!this.attached || 'tint' in Sprite) {
diffuse.tint = normals.tint = this.entity.Sprite.tint;
}
if (
!this.attached
|| ('source' in Sprite)
|| ('animation' in Sprite)
|| ('frame' in Sprite)
) {
this.constructor.loadAsset(this.entity.Sprite.source).then(async (asset) => {
const texture = await this.constructor.textureFromAsset(
asset,
this.entity.Sprite.animation,
this.entity.Sprite.frame,
);
diffuse.texture = texture;
if (asset.data.meta.normals) {
const {pathname} = new URL(
Sprite.source
.split('/')
.slice(0, -1)
.concat(asset.data.meta.normals)
.join('/'),
'http://example.org',
);
this.constructor.loadAsset(pathname).then(async (asset) => {
const texture = await this.constructor.textureFromAsset(
asset,
this.entity.Sprite.animation,
this.entity.Sprite.frame,
);
normals.texture = texture;
});
}
});
}
}
if (Direction) {
const {diffuse, normals} = this;
if ('direction' in Direction) {
if (this.entity.Sprite.rotates) {
const rotation = Direction.direction + this.entity.Sprite.rotation;
diffuse.rotation = rotation;
normals.rotation = rotation;
}
}
}
if (this.entity.Collider) {
this.colliderAabb.redraw(this.entity.Collider.aabb);
}
if (VisibleAabb) {
this.visibleAabb.redraw(this.entity.VisibleAabb);
}
if (this.isMainEntity) {
this.interactionAabb.redraw(this.entity.Interacts.aabb());
}
}
}

View File

@ -1,84 +0,0 @@
import {Container} from '@pixi/react';
import {memo} from 'react';
import {useDebug} from '@/react/context/debug.js';
import {useMainEntity} from '@/react/context/main-entity.js';
import Aabb from './aabb.jsx';
import Crosshair from './crosshair.jsx';
import Light from './light.jsx';
import SpriteComponent from './sprite.jsx';
function Entity({entity, ...rest}) {
const [debug] = useDebug();
const [mainEntity] = useMainEntity();
if (!entity) {
return false;
}
const {Direction, id, Sprite} = entity;
return (
<>
<Container
x={entity.Position.x}
y={entity.Position.y}
zIndex={entity.Position?.y || 0}
>
{entity.Sprite && (
<SpriteComponent
alpha={Sprite.alpha}
anchor={Sprite.anchor}
animation={Sprite.animation}
direction={Direction?.direction}
frame={Sprite.frame}
id={id}
scale={Sprite.scale}
rotates={Sprite.rotates}
rotation={Sprite.rotation}
source={Sprite.source}
tint={Sprite.tint}
{...rest}
/>
)}
{entity.Light && (
<Light
brightness={entity.Light.brightness}
/>
)}
{debug && entity.Position && (
<Crosshair />
)}
</Container>
{debug && (
<Aabb
color={0xff00ff}
{...entity.VisibleAabb}
/>
)}
{debug && entity.Collider && (
<>
<Aabb
color={0xffffff}
width={0.5}
{...entity.Collider.aabb}
/>
{entity.Collider.aabbs.map((aabb, i) => (
<Aabb
color={0xffff00}
width={0.25}
key={i}
{...aabb}
/>
))}
</>
)}
{debug && mainEntity == entity.id && (
<Aabb
color={0x00ff00}
{...entity.Interacts.aabb()}
/>
)}
</>
);
}
export default memo(Entity);

View File

@ -1,34 +0,0 @@
import {PixiComponent} from '@pixi/react';
import {PointLight} from './lights.js';
const LightInternal = PixiComponent('Light', {
create({brightness}) {
const light = new PointLight(
0xffffff - 0x2244cc,
brightness,
);
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
// 'float D = length(lightVector)',
// 'float D = length(lightVector) / 1.0',
// );
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
// 'intensity = diffuse * attenuation',
// 'intensity = diffuse * (attenuation * 2.0)',
// );
// light.falloff = [0.5, 5, 50];
// light.falloff = light.falloff.map((n, i) => n / (2 + i));
// light.parentGroup = deferredLighting.lightGroup;
// delete light.parentGroup;
return light;
},
});
export default function Light({brightness}) {
return (
<LightInternal
brightness={brightness}
/>
)
}

View File

@ -1,102 +0,0 @@
import {Sprite as PixiSprite} from '@pixi/react';
import {memo, useEffect, useState} from 'react';
import {useAsset} from '@/react/context/assets.js';
import {deferredLighting} from './lights.js';
function textureFromAsset(asset, animation, frame) {
if (!asset) {
return undefined;
}
let texture;
if (asset.data.animations) {
if (!animation) {
return undefined;
}
texture = asset.animations[animation][frame];
}
else {
texture = asset.textures[''];
}
return texture;
}
function Sprite(props) {
const {
alpha,
anchor,
animation,
direction,
frame,
scale,
rotates,
rotation,
source,
tint,
...rest
} = props;
const [mounted, setMounted] = useState();
const [normals, setNormals] = useState();
const [normalsMounted, setNormalsMounted] = useState();
const asset = useAsset(source);
const normalsAsset = useAsset(normals);
useEffect(() => {
if (!asset) {
return;
}
const {normals} = asset.data.meta;
if (normals) {
const {pathname} = new URL(
source.split('/').slice(0, -1).concat(normals).join('/'),
'http://example.org',
);
setNormals(pathname);
}
}, [asset, source]);
const texture = textureFromAsset(
asset,
animation,
frame,
);
const normalsTexture = textureFromAsset(
normalsAsset,
animation,
frame,
);
if (mounted) {
mounted.parentGroup = deferredLighting.diffuseGroup;
}
if (normalsMounted) {
normalsMounted.parentGroup = deferredLighting.normalGroup;
}
return (
<>
{texture && (
<PixiSprite
alpha={alpha}
anchor={anchor}
ref={setMounted}
{...(rotates ? {rotation: direction + rotation} : {})}
scale={scale}
texture={texture}
{...(0 !== tint ? {tint} : {})}
{...rest}
/>
)}
{normalsTexture && (
<PixiSprite
alpha={alpha}
anchor={anchor}
ref={setNormalsMounted}
{...(rotates ? {rotation: direction + rotation} : {})}
scale={scale}
texture={normalsTexture}
{...rest}
/>
)}
</>
);
}
export default memo(Sprite);

View File

@ -10,13 +10,11 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
create: () => { create: () => {
// Solid target square. // Solid target square.
const target = new Graphics(); const target = new Graphics();
target.alpha = 0.7;
target.lineStyle(1, 0xffffff); target.lineStyle(1, 0xffffff);
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y); target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2}; target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
// Inner spinny part. // Inner spinny part.
const targetInner = new Graphics(); const targetInner = new Graphics();
targetInner.alpha = 0.3;
targetInner.lineStyle(3, 0x333333); targetInner.lineStyle(3, 0x333333);
targetInner.beginFill(0xdddddd); targetInner.beginFill(0xdddddd);
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2}; targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
@ -25,6 +23,7 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
targetInner.position.y = 0.5; targetInner.position.y = 0.5;
// ... // ...
const container = new Container(); const container = new Container();
container.alpha = 0.4;
container.addChild(target, targetInner); container.addChild(target, targetInner);
return container; return container;
}, },