perf: damage

This commit is contained in:
cha0s 2024-07-31 10:35:43 -05:00
parent 2997370e3b
commit cb3d9ad8c6
3 changed files with 126 additions and 169 deletions

View File

@ -1,6 +1,7 @@
import {memo, useCallback, useRef} from 'react'; import {memo, useCallback, useRef} from 'react';
import {DamageTypes} from '@/ecs/components/vulnerable.js'; import {DamageTypes} from '@/ecs/components/vulnerable.js';
import {useEcsTick} from '@/react/context/ecs.js';
import useAnimationFrame from '@/react/hooks/use-animation-frame.js'; import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js'; import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
@ -25,142 +26,143 @@ function damageHue(type) {
return hue; return hue;
} }
function createDamageNode() { class Damage {
const damage = document.createElement('div'); elapsed = 0;
damage.classList.add(styles.damage); hue = [0, 30];
damage.appendChild(document.createElement('p')); offsetX = 0;
return damage; offsetY = 0;
step = 0;
constructor() {
const element = document.createElement('div');
element.classList.add(styles.damage);
element.appendChild(document.createElement('p'));
this.element = element;
}
reset(key, scale, {amount, position, type}) {
this.elapsed = 0;
[this.hueStart, this.hueEnd] = damageHue(type);
this.offsetX = 20 * scale * 1 * (Math.random() - 0.5);
this.offsetY = -1 * (10 + Math.random() * 45 * scale);
this.step = 0;
const {element} = this;
element.style.setProperty('rotate', `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`);
element.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount)))));
element.style.setProperty('--positionX', `${position.x * scale}px`);
element.style.setProperty('--positionY', `${position.y * scale}px`);
element.style.setProperty('zIndex', key);
const p = element.querySelector('p');
p.style.scale = scale / 2;
p.innerText = Math.abs(amount);
}
tick(elapsed, stepSize) {
this.elapsed += elapsed;
this.step += elapsed;
if (this.step > stepSize) {
this.step = this.step % stepSize;
// offset
let offsetX = 0, offsetY = 0;
if (this.elapsed <= 0.5) {
offsetX = easeOutQuad(this.elapsed, 0, this.offsetX, 0.5);
offsetY = easeOutQuad(this.elapsed, 0, this.offsetY, 0.5);
}
else if (this.elapsed > 1.375) {
offsetX = easeOutQuad(this.elapsed - 1.375, this.offsetX, -this.offsetX, 0.125);
offsetY = easeOutQuad(this.elapsed - 1.375, this.offsetY, -this.offsetY, 0.125);
}
else {
offsetX = this.offsetX;
offsetY = this.offsetY;
}
this.element.style.setProperty('--offsetX', offsetX);
this.element.style.setProperty('--offsetY', offsetY);
// scale
let scale = 0.35;
if (this.elapsed <= 0.5) {
scale = easeOutQuad(this.elapsed, 0, 1, 0.5);
}
else if (this.elapsed > 0.5 && this.elapsed < 0.6) {
scale = linear(this.elapsed - 0.5, 1, 0.5, 0.1);
}
else if (this.elapsed > 0.6 && this.elapsed < 0.675) {
scale = easeInQuint(this.elapsed - 0.6, 1, 0.25, 0.075);
}
else if (this.elapsed > 0.675 && this.elapsed < 1.375) {
scale = 1;
}
else if (this.elapsed > 1.375) {
scale = easeOutQuad(this.elapsed - 1.375, 1, -1, 0.125);
}
this.element.style.setProperty('--scale', scale);
// opacity
let opacity = 0.75;
if (this.elapsed <= 0.375) {
opacity = linear(this.elapsed, 0.75, 0.25, 0.375);
}
else if (this.elapsed > 0.375 && this.elapsed < 1.375) {
opacity = 1;
}
else if (this.elapsed > 1.375) {
opacity = linear(this.elapsed - 1.375, 1, -1, 0.125);
}
this.element.style.setProperty('--opacity', opacity);
// hue
this.element.style.setProperty(
'--hue',
easeInOutExpo(
Math.abs((this.elapsed % 0.3) - 0.15) / 0.15,
this.hueStart,
this.hueEnd,
1,
),
);
}
}
} }
function Damages({damages, scale}) { function Damages({scale}) {
const animations = useRef({}); const damages = useRef({});
const pool = useRef([]);
const damagesRef = useRef(); const damagesRef = useRef();
const pool = useRef([]);
const onEcsTick = useCallback((payload) => {
for (const id in payload) {
const update = payload[id];
if (false === update || !update.Vulnerable?.damage) {
continue;
}
const {damage: damageUpdate} = update.Vulnerable;
for (const key in damageUpdate) {
const damage = pool.current.length > 0 ? pool.current.pop() : new Damage();
damage.reset(key, scale, damageUpdate[key]);
damages.current[key] = damage;
damagesRef.current.appendChild(damage.element);
}
}
}, [scale]);
useEcsTick(onEcsTick);
const frame = useCallback((elapsed) => { const frame = useCallback((elapsed) => {
if (!damagesRef.current) { if (!damagesRef.current) {
return; return;
} }
if (0 === pool.current.length) { const keys = Object.keys(damages.current);
for (let i = 0; i < 512; ++i) { if (0 === keys.length && 0 === pool.current.length) {
const damage = createDamageNode(); for (let i = 0; i < 500; ++i) {
damagesRef.current.appendChild(damage); pool.current.push(new Damage());
pool.current.push(damage);
} }
} }
const keys = Object.keys(animations.current); const stepSize = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed;
for (const key of keys) { for (const key of keys) {
const animation = animations.current[key]; const damage = damages.current[key];
if (!damages[key]) { damage.tick(elapsed, stepSize);
if (animation.element) { if (damage.elapsed > 1.5) {
if (pool.current.length < 512) { damagesRef.current.removeChild(damage.element);
pool.current.push(animation.element); delete damages.current[key];
} if (pool.current.length < 1000) {
animation.element = undefined; pool.current.push(damage);
} }
delete animations.current[key];
continue;
}
if (!animation.element) {
const {amount, position} = damages[key];
let damage;
if (pool.current.length > 0) {
damage = pool.current.pop();
}
else {
damage = createDamageNode();
damagesRef.current.appendChild(damage);
}
const p = damage.querySelector('p');
p.style.scale = scale / 2;
p.innerText = Math.abs(amount);
damage.style.setProperty('--randomnessX', animation['--randomnessX']);
damage.style.setProperty('--randomnessY', animation['--randomnessY']);
damage.style.setProperty('rotate', animation['rotate']);
damage.style.setProperty('rotate', animation['rotate']);
damage.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount)))));
damage.style.setProperty('--positionX', `${position.x * scale}px`);
damage.style.setProperty('--positionY', `${position.y * scale}px`);
damage.style.setProperty('zIndex', key);
animation.element = damage;
}
animation.elapsed += elapsed;
animation.step += elapsed;
const offsetX = 20 * scale * animation['--randomnessX'];
const offsetY = -1 * (10 + animation['--randomnessY'] * 45 * scale);
// offset
if (animation.elapsed <= 0.5) {
animation['--offsetX'] = easeOutQuad(animation.elapsed, 0, offsetX, 0.5);
animation['--offsetY'] = easeOutQuad(animation.elapsed, 0, offsetY, 0.5);
}
else if (animation.elapsed > 1.375) {
animation['--offsetX'] = offsetX - easeOutQuad(animation.elapsed - 1.375, 0, offsetX, 0.125);
animation['--offsetY'] = offsetY - easeOutQuad(animation.elapsed - 1.375, 0, offsetY, 0.125);
}
// scale
if (animation.elapsed <= 0.5) {
animation['--scale'] = easeOutQuad(animation.elapsed, 0, 1, 0.5);
}
else if (animation.elapsed > 0.5 && animation.elapsed < 0.6) {
animation['--scale'] = linear(animation.elapsed - 0.5, 1, 0.5, 0.1);
}
else if (animation.elapsed > 0.6 && animation.elapsed < 0.675) {
animation['--scale'] = 1.5 - easeInQuint(animation.elapsed - 0.6, 1, 0.5, 0.075);
}
else if (animation.elapsed > 0.675 && animation.elapsed < 1.375) {
animation['--scale'] = 1;
}
else if (animation.elapsed > 1.375) {
animation['--scale'] = 1 - easeOutQuad(animation.elapsed - 1.375, 0, 1, 0.125);
}
// fade
if (animation.elapsed <= 0.375) {
animation['--opacity'] = linear(animation.elapsed, 0.75, 0.25, 0.375);
}
else if (animation.elapsed > 0.375 && animation.elapsed < 1.375) {
animation['--opacity'] = 1;
}
else if (animation.elapsed > 1.375) {
animation['--opacity'] = 1 - linear(animation.elapsed - 1.375, 0, 1, 0.125);
}
// hue
const h = Math.abs((animation.elapsed % 0.3) - 0.15) / 0.15;
animation['--hue'] = easeInOutExpo(h, animation.hue[0], animation.hue[1], 1);
const step = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed;
if (animation.step > step) {
animation.step = animation.step % step;
animation.element.style.setProperty('--hue', animation['--hue']);
animation.element.style.setProperty('--opacity', animation['--opacity']);
animation.element.style.setProperty('--offsetX', animation['--offsetX']);
animation.element.style.setProperty('--offsetY', animation['--offsetY']);
animation.element.style.setProperty('--scale', animation['--scale']);
}
if (animation.elapsed > 1.5) {
if (pool.current.length < 512) {
pool.current.push(animation.element);
}
animation.element.style.setProperty('--opacity', 0);
animation.element = undefined;
damages[key].onClose();
} }
} }
}, [damages, scale]); }, []);
useAnimationFrame(frame); useAnimationFrame(frame);
for (const key in damages) {
if (!animations.current[key]) {
animations.current[key] = {
elapsed: 0,
hue: damageHue(damages[key].type),
rotate: `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`,
step: 0,
'--scale': 0.35,
'--opacity': 0.75,
'--offsetX': 0,
'--offsetY': 0,
'--randomnessX': 1 * (Math.random() - 0.5),
'--randomnessY': Math.random(),
};
}
}
return ( return (
<div <div
className={styles.damages} className={styles.damages}

View File

@ -23,22 +23,10 @@
inherits: false; inherits: false;
syntax: '<number>'; syntax: '<number>';
} }
@property --randomnessX {
initial-value: 0;
inherits: false;
syntax: '<number>';
}
@property --randomnessY {
initial-value: 0;
inherits: false;
syntax: '<number>';
}
.damage { .damage {
--hue: 0; --hue: 0;
--opacity: 0.75; --opacity: 0.75;
--randomnessX: 0;
--randomnessY: 0;
--scale: 0.35; --scale: 0.35;
--background: hsl(var(--hue) 100% 12.5%); --background: hsl(var(--hue) 100% 12.5%);
--foreground: hsl(var(--hue) 100% 50%); --foreground: hsl(var(--hue) 100% 50%);

View File

@ -1,4 +1,4 @@
import {useCallback, useRef, useState} from 'react'; import {useCallback, useState} from 'react';
import {usePacket} from '@/react/context/client.js'; import {usePacket} from '@/react/context/client.js';
import {useEcsTick} from '@/react/context/ecs.js'; import {useEcsTick} from '@/react/context/ecs.js';
@ -14,8 +14,6 @@ export default function Entities({
setMonopolizers, setMonopolizers,
}) { }) {
const [entities, setEntities] = useState({}); const [entities, setEntities] = useState({});
const [damages, setDamages] = useState({});
const {current: pendingDamage} = useRef({accumulated: [], handle: undefined});
usePacket('EcsChange', async () => { usePacket('EcsChange', async () => {
setEntities({}); setEntities({});
}); });
@ -87,36 +85,6 @@ export default function Entities({
}; };
} }
} }
const {damage} = update.Vulnerable || {};
if (damage) {
for (const key in damage) {
damage[key].onClose = () => {
setDamages((damages) => {
const {[key]: _, ...rest} = damages; // eslint-disable-line no-unused-vars
return rest;
})
};
}
pendingDamage.accumulated.push(damage);
if (!pendingDamage.handle) {
pendingDamage.handle = setTimeout(() => {
const update = {};
for (const damage of pendingDamage.accumulated) {
for (const key in damage) {
update[key] = damage[key];
}
}
pendingDamage.accumulated.length = 0;
setDamages((damages) => {
return {
...damages,
...update,
};
});
pendingDamage.handle = undefined;
}, 50);
}
}
} }
setEntities((entities) => { setEntities((entities) => {
for (const id in deleting) { for (const id in deleting) {
@ -127,7 +95,7 @@ export default function Entities({
...updating, ...updating,
}; };
}); });
}, [pendingDamage, setChatMessages, setMonopolizers]); }, [setChatMessages, setMonopolizers]);
useEcsTick(onEcsTick); useEcsTick(onEcsTick);
const renderables = []; const renderables = [];
for (const id in entities) { for (const id in entities) {
@ -151,7 +119,6 @@ export default function Entities({
> >
{renderables} {renderables}
<Damages <Damages
damages={damages}
scale={scale} scale={scale}
/> />
</div> </div>