vimp-engine 0.35.2 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/SoundManager.js +18 -3
- package/src/client/lib/applyCamera.js +20 -1
- package/src/client/lib/hotTick.js +66 -0
- package/src/client/main.js +27 -27
package/package.json
CHANGED
|
@@ -716,8 +716,11 @@ export default class SoundManager {
|
|
|
716
716
|
|
|
717
717
|
// отсечка в мировых координатах: она обязана совпадать с maxDistance
|
|
718
718
|
// самого PannerNode, поэтому зумом НЕ масштабируется — иначе движок
|
|
719
|
-
// считал бы источник слышимым там, где узел уже отдал
|
|
720
|
-
|
|
719
|
+
// считал бы источник слышимым там, где узел уже отдал тишину.
|
|
720
|
+
// «не ближе maxDistance», а не «дальше»: NaN-дистанция (координата
|
|
721
|
+
// источника или слушателя не число) иначе считалась бы слышимой на
|
|
722
|
+
// полной громкости
|
|
723
|
+
if (!(Math.hypot(dx, dy) < this._spatial.maxDistance)) {
|
|
721
724
|
sound.volume(0, soundId);
|
|
722
725
|
|
|
723
726
|
return false;
|
|
@@ -787,7 +790,8 @@ export default class SoundManager {
|
|
|
787
790
|
* @private Пишет позицию в паннер, если она изменилась заметнее
|
|
788
791
|
* POSITION_EPSILON. Единственная точка записи: у неподвижного источника
|
|
789
792
|
* позиция уже в узле, и повторная запись — только лишняя автоматизация,
|
|
790
|
-
* а именно её поток WebKit и не переносит.
|
|
793
|
+
* а именно её поток WebKit и не переносит. Неконечная позиция
|
|
794
|
+
* пропускается: не пишется и не запоминается.
|
|
791
795
|
* @param {Howl} sound - Экземпляр Howl.
|
|
792
796
|
* @param {number} soundId - ID конкретного проигрываемого экземпляра.
|
|
793
797
|
* @param {number} px - Координата X в осях Web Audio.
|
|
@@ -795,6 +799,17 @@ export default class SoundManager {
|
|
|
795
799
|
* @param {number} pz - Координата Z в осях Web Audio.
|
|
796
800
|
*/
|
|
797
801
|
_writePos(sound, soundId, px, py, pz) {
|
|
802
|
+
// Неконечная координата в узел не пишется: Web Audio бросает на ней
|
|
803
|
+
// TypeError из setValueAtTime, исключение уходит из рендер-тика в тикер
|
|
804
|
+
// Pixi, и тот больше не запрашивает кадр — игра замирает целиком.
|
|
805
|
+
// Позиции источников задаёт и игра (registerSound/updateSoundData),
|
|
806
|
+
// поэтому одного фильтра камеры (lib/applyCamera.js) здесь мало.
|
|
807
|
+
// Запомненная позиция не трогается: следующую конечную сравним с
|
|
808
|
+
// настоящей
|
|
809
|
+
if (!Number.isFinite(px) || !Number.isFinite(py) || !Number.isFinite(pz)) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
798
813
|
const written = this._pannerPos.get(soundId);
|
|
799
814
|
|
|
800
815
|
if (written !== undefined) {
|
|
@@ -14,12 +14,31 @@
|
|
|
14
14
|
* @param {object} soundManager - Экземпляр SoundManager.
|
|
15
15
|
* @param {Array | number} camera - `[x, y, cameraReset, shakeData]` из
|
|
16
16
|
* горячего буфера ядра; `0` или пустое значение — кадра камеры нет.
|
|
17
|
+
* @param {Function} [onInvalid] - Зовётся с кадром, у которого x или y — не
|
|
18
|
+
* конечное число (NaN, Infinity, undefined, null); такой кадр не применяется.
|
|
17
19
|
*/
|
|
18
|
-
export default function applyCamera(
|
|
20
|
+
export default function applyCamera(
|
|
21
|
+
canvasManager,
|
|
22
|
+
soundManager,
|
|
23
|
+
camera,
|
|
24
|
+
onInvalid,
|
|
25
|
+
) {
|
|
19
26
|
if (!camera || camera === 0) {
|
|
20
27
|
return;
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
// Неконечная координата не применяется ни к полотну, ни к слушателю.
|
|
31
|
+
// Полотну один NaN вредит надолго: CanvasManagerModel сглаживает камеру
|
|
32
|
+
// через lerp, NaN оседает в его накопителях до cameraReset — масштаб
|
|
33
|
+
// сцены NaN, картинки нет. Слушателю — сразу: NaN уходит в Howl.pos(),
|
|
34
|
+
// тот бросает, и тикер Pixi останавливается. null отсекается тоже: serde
|
|
35
|
+
// пишет неконечное число JSON-кадра как null, а в арифметике null молча
|
|
36
|
+
// стал бы нулём — прыжок камеры в начало координат
|
|
37
|
+
if (!Number.isFinite(camera[0]) || !Number.isFinite(camera[1])) {
|
|
38
|
+
onInvalid?.(camera);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
canvasManager.updateCoords(camera);
|
|
24
43
|
soundManager.setListenerPosition(
|
|
25
44
|
camera[0],
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HOT_FLAGS } from '../../config/opcodes.js';
|
|
2
|
+
import { reconstructHot } from '../../lib/reconstructHot.js';
|
|
3
|
+
|
|
4
|
+
// Рендер-тик клиентского ядра: сэмпл, событийные кадры, горячие данные,
|
|
5
|
+
// камера. Вынесено из main.js ради одной вещи — порядка ЧТЕНИЯ, который
|
|
6
|
+
// ничем больше не защищён: main.js целиком в тесте не поднять.
|
|
7
|
+
//
|
|
8
|
+
// Hot-буфер читается view поверх памяти WASM (zero-copy), а view живёт
|
|
9
|
+
// ровно до следующего роста этой памяти: `memory.grow` отцепляет старый
|
|
10
|
+
// ArrayBuffer, длина view становится 0, hot[i] — undefined. Растить память
|
|
11
|
+
// может любой аллоцирующий вызов ядра: take_frames() собирает строку JSON
|
|
12
|
+
// в куче WASM, парты при разборе кадров сами зовут ядро. Поэтому всё нужное
|
|
13
|
+
// из буфера снимается копией СРАЗУ, а применяется в прежнем порядке.
|
|
14
|
+
// Раньше камера читалась после take_frames(): undefined в ней становился
|
|
15
|
+
// NaN, NaN навсегда оседал в сглаживании камеры (картинка пропадала до
|
|
16
|
+
// cameraReset), а слушатель звука с NaN ронял Howl.pos() — и тикер Pixi.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Один рендер-тик клиентского ядра.
|
|
20
|
+
* @param {Object} deps
|
|
21
|
+
* @param {Object} deps.core - ClientCore: sample, hot_ptr, take_frames.
|
|
22
|
+
* @param {WebAssembly.Memory} deps.memory - Память WASM клиентского ядра.
|
|
23
|
+
* @param {Object} deps.snapshotKeysById - Результат buildSnapshotKeysById.
|
|
24
|
+
* @param {number} deps.now - Время рендера (performance.now()).
|
|
25
|
+
* @param {Function} deps.applyShot - (game, camera): событийный кадр.
|
|
26
|
+
* @param {Function} deps.applyGameData - (game): горячие данные сущностей.
|
|
27
|
+
* @param {Function} deps.applyCamera - ([x, y]): камера тика.
|
|
28
|
+
*/
|
|
29
|
+
export default function runHotTick({
|
|
30
|
+
core,
|
|
31
|
+
memory,
|
|
32
|
+
snapshotKeysById,
|
|
33
|
+
now,
|
|
34
|
+
applyShot,
|
|
35
|
+
applyGameData,
|
|
36
|
+
applyCamera,
|
|
37
|
+
}) {
|
|
38
|
+
const len = core.sample(now);
|
|
39
|
+
// указатель — раньше memory.buffer: аргументы `new Float32Array(...)`
|
|
40
|
+
// вычисляются слева направо, и buffer, взятый до вызова ядра, мог бы уже
|
|
41
|
+
// оказаться отцепленным
|
|
42
|
+
const ptr = core.hot_ptr();
|
|
43
|
+
const hot = new Float32Array(memory.buffer, ptr, len);
|
|
44
|
+
const flags = hot[0];
|
|
45
|
+
// reconstructHot копирует поля (Array.from): после разбора view не нужен
|
|
46
|
+
const game =
|
|
47
|
+
flags & (HOT_FLAGS.GAME | HOT_FLAGS.PREDICTED)
|
|
48
|
+
? reconstructHot(hot, snapshotKeysById)
|
|
49
|
+
: null;
|
|
50
|
+
const camera = flags & HOT_FLAGS.CAMERA ? [hot[1], hot[2]] : null;
|
|
51
|
+
|
|
52
|
+
if (flags & HOT_FLAGS.FRAMES) {
|
|
53
|
+
JSON.parse(core.take_frames()).forEach(frame => {
|
|
54
|
+
applyShot(frame.game, frame.camera);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (game) {
|
|
59
|
+
applyGameData(game);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// камера уже разрешена ядром: предсказанная позиция либо интерполированная
|
|
63
|
+
if (camera) {
|
|
64
|
+
applyCamera(camera);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/client/main.js
CHANGED
|
@@ -52,10 +52,7 @@ import { readCoreAbi, dispatchCoreOp, ABI_UNKNOWN } from '../lib/coreAbi.js';
|
|
|
52
52
|
import { ABI_OP_DEBUG_JSON } from '../config/abiOps.js';
|
|
53
53
|
import { createDebugApi, debugLog, DEBUG_PREFIX } from './debug.js';
|
|
54
54
|
import { buildClientCoreConfig } from '../lib/clientCoreConfig.js';
|
|
55
|
-
import {
|
|
56
|
-
buildSnapshotKeysById,
|
|
57
|
-
reconstructHot,
|
|
58
|
-
} from '../lib/reconstructHot.js';
|
|
55
|
+
import { buildSnapshotKeysById } from '../lib/reconstructHot.js';
|
|
59
56
|
import Factory from '../lib/factory.js';
|
|
60
57
|
import { formatMessage } from '../lib/formatters.js';
|
|
61
58
|
import { sanitizeMessage } from '../lib/sanitizers.js';
|
|
@@ -87,7 +84,6 @@ import ClientReportsView from './components/view/ClientReports.js';
|
|
|
87
84
|
import ClientReportsCtrl from './components/controller/ClientReports.js';
|
|
88
85
|
import BakingProvider from './providers/BakingProvider.js';
|
|
89
86
|
import DependencyProvider from './providers/DependencyProvider.js';
|
|
90
|
-
import { HOT_FLAGS } from '../config/opcodes.js';
|
|
91
87
|
import wsports from '../config/wsports.js';
|
|
92
88
|
import GAME_CODES from '../config/gameCodes.js';
|
|
93
89
|
import {
|
|
@@ -99,6 +95,7 @@ import lobbyConfig from '../config/lobby.js';
|
|
|
99
95
|
import authClientConfig from '../config/authClient.js';
|
|
100
96
|
import clientDefaults from '../config/clientDefaults.js';
|
|
101
97
|
import applyCamera from './lib/applyCamera.js';
|
|
98
|
+
import runHotTick from './lib/hotTick.js';
|
|
102
99
|
|
|
103
100
|
// Динамическая загрузка игры по каталогу мастера (Этап 6.3): ClientPlugin
|
|
104
101
|
// (parts, bakers, игровой CSS, хуки ядра) грузится по entries.client манифеста
|
|
@@ -951,15 +948,28 @@ function applyGameData(game) {
|
|
|
951
948
|
});
|
|
952
949
|
}
|
|
953
950
|
|
|
951
|
+
// Камеру с NaN/undefined applyCamera не применяет (lib/applyCamera.js), но
|
|
952
|
+
// молчать о ней нельзя: это симптом сбоя выше по течению (ядро, чтение
|
|
953
|
+
// hot-буфера), и журнал — единственный способ узнать о нём с прода.
|
|
954
|
+
// String(): JSON.stringify превратил бы NaN в null, а undefined потерял бы
|
|
955
|
+
function reportBadCamera(camera) {
|
|
956
|
+
diagnostics.warn(
|
|
957
|
+
'engine.camera.non-finite',
|
|
958
|
+
{ x: String(camera[0]), y: String(camera[1]) },
|
|
959
|
+
{ source: 'client' },
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
954
963
|
// применяет кадр целиком (первый кадр и дискретные кадры интерполяции)
|
|
955
964
|
function applyShot(game, camera) {
|
|
956
965
|
applyGameData(game);
|
|
957
|
-
applyCamera(modules.canvasManager, soundManager, camera);
|
|
966
|
+
applyCamera(modules.canvasManager, soundManager, camera, reportBadCamera);
|
|
958
967
|
}
|
|
959
968
|
|
|
960
969
|
// рендер-тик: ядро выдаёт пересечённые кадры (события, создания/удаления)
|
|
961
970
|
// JSON-очередью, а горячие позиции (танки/динамика/камера + предсказанный
|
|
962
|
-
// свой танк) — плоским Float32-буфером zero-copy из памяти WASM
|
|
971
|
+
// свой танк) — плоским Float32-буфером zero-copy из памяти WASM; порядок
|
|
972
|
+
// чтения буфера — в lib/hotTick.js
|
|
963
973
|
function renderTick() {
|
|
964
974
|
if (!clientCore) {
|
|
965
975
|
return;
|
|
@@ -974,26 +984,16 @@ function renderTick() {
|
|
|
974
984
|
task();
|
|
975
985
|
}
|
|
976
986
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (flags & (HOT_FLAGS.GAME | HOT_FLAGS.PREDICTED)) {
|
|
990
|
-
applyGameData(reconstructHot(hot, snapshotKeysById));
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (flags & HOT_FLAGS.CAMERA) {
|
|
994
|
-
// камера уже разрешена ядром: предсказанная позиция либо интерполированная
|
|
995
|
-
applyCamera(modules.canvasManager, soundManager, [hot[1], hot[2]]);
|
|
996
|
-
}
|
|
987
|
+
runHotTick({
|
|
988
|
+
core: clientCore,
|
|
989
|
+
memory: wasm.memory,
|
|
990
|
+
snapshotKeysById,
|
|
991
|
+
now: performance.now(),
|
|
992
|
+
applyShot,
|
|
993
|
+
applyGameData,
|
|
994
|
+
applyCamera: camera =>
|
|
995
|
+
applyCamera(modules.canvasManager, soundManager, camera, reportBadCamera),
|
|
996
|
+
});
|
|
997
997
|
|
|
998
998
|
soundManager.processAudibility();
|
|
999
999
|
soundManager.updateActiveSounds();
|