diff --git a/package.json b/package.json
index b038cdb..11cc3bb 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"dexie": "^3.0.3",
"err-code": "^2.0.3",
"fake-indexeddb": "^3.1.2",
+ "fast-deep-equal": "^3.1.3",
"fuse.js": "^6.4.1",
"interactjs": "^1.9.7",
"konva": "^7.1.8",
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
index 1e6b749..a55e3c1 100644
--- a/src/components/map/MapInteraction.js
+++ b/src/components/map/MapInteraction.js
@@ -18,6 +18,8 @@ import MapStageContext, {
import AuthContext from "../../contexts/AuthContext";
import SettingsContext from "../../contexts/SettingsContext";
import KeyboardContext from "../../contexts/KeyboardContext";
+import { PlayerUpdaterContext } from "../../contexts/PlayerContext";
+import PartyContext from "../../contexts/PartyContext";
function MapInteraction({
map,
@@ -35,6 +37,7 @@ function MapInteraction({
useEffect(() => {
if (
!map ||
+ !mapState ||
(map.type === "file" && !map.file && !map.resolutions) ||
mapState.mapId !== map.id
) {
@@ -177,6 +180,8 @@ function MapInteraction({
const auth = useContext(AuthContext);
const settings = useContext(SettingsContext);
+ const player = useContext(PlayerUpdaterContext);
+ const party = useContext(PartyContext);
const mapInteraction = {
stageScale,
@@ -218,15 +223,19 @@ function MapInteraction({
/>
{/* Forward auth context to konva elements */}
-
-
-
-
- {mapLoaded && children}
-
-
-
-
+
+
+
+
+
+
+ {mapLoaded && children}
+
+
+
+
+
+
diff --git a/src/components/party/Party.js b/src/components/party/Party.js
index 2acb24b..45eee01 100644
--- a/src/components/party/Party.js
+++ b/src/components/party/Party.js
@@ -14,10 +14,16 @@ import DiceTrayButton from "./DiceTrayButton";
import useSetting from "../../helpers/useSetting";
-import PlayerContext from "../../contexts/PlayerContext";
+import PartyContext from "../../contexts/PartyContext";
+import {
+ PlayerUpdaterContext,
+ PlayerStateWithoutPointerContext,
+} from "../../contexts/PlayerContext";
function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) {
- const { playerState, setPlayerState, partyState } = useContext(PlayerContext);
+ const setPlayerState = useContext(PlayerUpdaterContext);
+ const playerState = useContext(PlayerStateWithoutPointerContext);
+ const partyState = useContext(PartyContext);
const [fullScreen] = useSetting("map.fullScreen");
const [shareDice, setShareDice] = useSetting("dice.shareDice");
diff --git a/src/contexts/PartyContext.js b/src/contexts/PartyContext.js
new file mode 100644
index 0000000..37c3e32
--- /dev/null
+++ b/src/contexts/PartyContext.js
@@ -0,0 +1,34 @@
+import React, { useState, useEffect } from "react";
+
+const PartyContext = React.createContext();
+
+export function PartyProvider({ session, children }) {
+ const [partyState, setPartyState] = useState({});
+
+ useEffect(() => {
+ function handleSocketPartyState(partyState) {
+ if (partyState) {
+ const { [session.id]: _, ...otherMembersState } = partyState;
+ setPartyState(otherMembersState);
+ } else {
+ setPartyState({});
+ }
+ }
+
+ if (session.socket) {
+ session.socket.on("party_state", handleSocketPartyState);
+ }
+
+ return () => {
+ if (session.socket) {
+ session.socket.off("party_state", handleSocketPartyState);
+ }
+ };
+ });
+
+ return (
+ {children}
+ );
+}
+
+export default PartyContext;
diff --git a/src/contexts/PlayerContext.js b/src/contexts/PlayerContext.js
index e70a163..4a756da 100644
--- a/src/contexts/PlayerContext.js
+++ b/src/contexts/PlayerContext.js
@@ -1,4 +1,5 @@
-import React, { useState, useEffect, useContext } from "react";
+import React, { useEffect, useContext, useState } from "react";
+import compare from "fast-deep-equal";
import useNetworkedState from "../helpers/useNetworkedState";
import DatabaseContext from "./DatabaseContext";
@@ -6,7 +7,12 @@ import AuthContext from "./AuthContext";
import { getRandomMonster } from "../helpers/monsters";
-const PlayerContext = React.createContext();
+export const PlayerStateContext = React.createContext();
+export const PlayerUpdaterContext = React.createContext(() => {});
+/**
+ * Store the player state without the pointer data to prevent unnecessary updates
+ */
+export const PlayerStateWithoutPointerContext = React.createContext();
export function PlayerProvider({ session, children }) {
const { userId } = useContext(AuthContext);
@@ -17,14 +23,13 @@ export function PlayerProvider({ session, children }) {
nickname: "",
timer: null,
dice: { share: false, rolls: [] },
- pointer: {},
+ pointer: { position: { x: 0, y: 0 }, visible: false },
sessionId: null,
userId,
},
session,
"player_state"
);
- const [partyState, setPartyState] = useState({});
useEffect(() => {
if (!database || databaseStatus === "loading") {
@@ -45,7 +50,7 @@ export function PlayerProvider({ session, children }) {
}
loadNickname();
- }, [database, databaseStatus]);
+ }, [database, databaseStatus, setPlayerState]);
useEffect(() => {
if (
@@ -64,7 +69,7 @@ export function PlayerProvider({ session, children }) {
...prevState,
userId,
}));
- }, [userId]);
+ }, [userId, setPlayerState]);
useEffect(() => {
function handleSocketConnect() {
@@ -72,21 +77,11 @@ export function PlayerProvider({ session, children }) {
setPlayerState({ ...playerState, sessionId: session.id });
}
- function handleSocketPartyState(partyState) {
- if (partyState) {
- const { [session.id]: _, ...otherMembersState } = partyState;
- setPartyState(otherMembersState);
- } else {
- setPartyState({});
- }
- }
-
session.on("connected", handleSocketConnect);
if (session.socket) {
session.socket.on("connect", handleSocketConnect);
session.socket.on("reconnect", handleSocketConnect);
- session.socket.on("party_state", handleSocketPartyState);
}
return () => {
@@ -95,19 +90,32 @@ export function PlayerProvider({ session, children }) {
if (session.socket) {
session.socket.off("connect", handleSocketConnect);
session.socket.off("reconnect", handleSocketConnect);
- session.socket.off("party_state", handleSocketPartyState);
}
};
});
- const value = {
- playerState,
- setPlayerState,
- partyState,
- };
+ const [playerStateWithoutPointer, setPlayerStateWithoutPointer] = useState(
+ playerState
+ );
+ useEffect(() => {
+ const { pointer, ...state } = playerState;
+ if (
+ !playerStateWithoutPointer ||
+ !compare(playerStateWithoutPointer, state)
+ ) {
+ setPlayerStateWithoutPointer(state);
+ }
+ }, [playerState, playerStateWithoutPointer]);
+
return (
- {children}
+
+
+
+ {children}
+
+
+
);
}
-
-export default PlayerContext;
diff --git a/src/helpers/useNetworkedState.js b/src/helpers/useNetworkedState.js
index b421a5e..25741ce 100644
--- a/src/helpers/useNetworkedState.js
+++ b/src/helpers/useNetworkedState.js
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState, useCallback } from "react";
function useNetworkedState(defaultState, session, eventName) {
const [state, _setState] = useState(defaultState);
@@ -6,10 +6,10 @@ function useNetworkedState(defaultState, session, eventName) {
const [dirty, setDirty] = useState(false);
// Update dirty at the same time as state
- function setState(update, sync = true) {
+ const setState = useCallback((update, sync = true) => {
_setState(update);
setDirty(sync);
- }
+ }, []);
useEffect(() => {
if (session.socket && dirty) {
@@ -31,7 +31,7 @@ function useNetworkedState(defaultState, session, eventName) {
session.socket.off(eventName, handleSocketEvent);
}
};
- }, [session.socket]);
+ }, [session.socket, eventName]);
return [state, setState];
}
diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js
index db521aa..b5c61c1 100644
--- a/src/network/NetworkedMapAndTokens.js
+++ b/src/network/NetworkedMapAndTokens.js
@@ -5,7 +5,7 @@ import MapDataContext from "../contexts/MapDataContext";
import MapLoadingContext from "../contexts/MapLoadingContext";
import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
-import PlayerContext from "../contexts/PlayerContext";
+import PartyContext from "../contexts/PartyContext";
import { omit } from "../helpers/shared";
import useDebounce from "../helpers/useDebounce";
@@ -27,7 +27,7 @@ import Tokens from "../components/token/Tokens";
*/
function NetworkedMapAndTokens({ session }) {
const { userId } = useContext(AuthContext);
- const { partyState } = useContext(PlayerContext);
+ const partyState = useContext(PartyContext);
const {
assetLoadStart,
assetLoadFinish,
diff --git a/src/network/NetworkedMapPointer.js b/src/network/NetworkedMapPointer.js
index 4347f19..afa4374 100644
--- a/src/network/NetworkedMapPointer.js
+++ b/src/network/NetworkedMapPointer.js
@@ -2,29 +2,31 @@ import React, { useState, useContext, useEffect, useRef } from "react";
import { Group } from "react-konva";
import AuthContext from "../contexts/AuthContext";
+import PartyContext from "../contexts/PartyContext";
+import { PlayerUpdaterContext } from "../contexts/PlayerContext";
import MapPointer from "../components/map/MapPointer";
import { isEmpty } from "../helpers/shared";
-import { lerp } from "../helpers/vector2";
+import { lerp, compare } from "../helpers/vector2";
// Send pointer updates every 33ms
-const sendTickRate = 33;
+const sendTickRate = 100;
-function NetworkedMapPointer({ session, active, gridSize }) {
+let t = 0;
+
+function NetworkedMapPointer({ active, gridSize }) {
const { userId } = useContext(AuthContext);
- const [pointerState, setPointerState] = useState({});
+ const setPlayerState = useContext(PlayerUpdaterContext);
+ const partyState = useContext(PartyContext);
+ const [localPointerState, setLocalPointerState] = useState({});
+
useEffect(() => {
- if (userId && !(userId in pointerState)) {
- setPointerState({
+ if (userId && !(userId in localPointerState)) {
+ setLocalPointerState({
[userId]: { position: { x: 0, y: 0 }, visible: false, id: userId },
});
}
- }, [userId, pointerState]);
-
- const sessionRef = useRef(session);
- useEffect(() => {
- sessionRef.current = session;
- }, [session]);
+ }, [userId, localPointerState]);
// Send pointer updates every sendTickRate to peers to save on bandwidth
// We use requestAnimationFrame as setInterval was being blocked during
@@ -42,8 +44,14 @@ function NetworkedMapPointer({ session, active, gridSize }) {
if (counter > sendTickRate) {
counter -= sendTickRate;
- if (ownPointerUpdateRef.current && sessionRef.current) {
- sessionRef.current.send("pointer", ownPointerUpdateRef.current);
+ if (ownPointerUpdateRef.current) {
+ const { position, visible } = ownPointerUpdateRef.current;
+ console.log("send time", performance.now() - t);
+ t = performance.now();
+ setPlayerState((prev) => ({
+ ...prev,
+ pointer: { position, visible },
+ }));
ownPointerUpdateRef.current = null;
}
}
@@ -55,7 +63,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
}, []);
function updateOwnPointerState(position, visible) {
- setPointerState((prev) => ({
+ setLocalPointerState((prev) => ({
...prev,
[userId]: { position, visible, id: userId },
}));
@@ -75,39 +83,43 @@ function NetworkedMapPointer({ session, active, gridSize }) {
}
// Handle pointer data receive
- const syncedPointerStateRef = useRef({});
+ const interpolationsRef = useRef({});
useEffect(() => {
- function handlePeerData({ id, data }) {
- if (id === "pointer") {
- // Setup an interpolation to the current pointer data when receiving a pointer event
- if (syncedPointerStateRef.current[data.id]) {
- const from = syncedPointerStateRef.current[data.id].to;
- syncedPointerStateRef.current[data.id] = {
- id: data.id,
- from: {
- ...from,
- time: performance.now(),
- },
- to: {
- ...data,
- time: performance.now() + sendTickRate,
- },
- };
- } else {
- syncedPointerStateRef.current[data.id] = {
- from: null,
- to: { ...data, time: performance.now() + sendTickRate },
- };
- }
+ // TODO: Handle player disconnect while pointer visible
+ const interpolations = interpolationsRef.current;
+ for (let player of Object.values(partyState)) {
+ const id = player.userId;
+ const pointer = player.pointer;
+ if (!id) {
+ continue;
+ }
+ if (!(id in interpolations)) {
+ interpolations[id] = {
+ id,
+ from: null,
+ to: { ...pointer, time: performance.now() + sendTickRate },
+ };
+ } else if (
+ !compare(interpolations[id].to.position, pointer.position, 0.0001) ||
+ interpolations[id].to.visible !== pointer.visible
+ ) {
+ console.log("receive time", performance.now() - t, pointer.position);
+ t = performance.now();
+ const from = interpolations[id].to;
+ interpolations[id] = {
+ id,
+ from: {
+ ...from,
+ time: performance.now(),
+ },
+ to: {
+ ...pointer,
+ time: performance.now() + sendTickRate,
+ },
+ };
}
}
-
- session.on("peerData", handlePeerData);
-
- return () => {
- session.off("peerData", handlePeerData);
- };
- });
+ }, [partyState]);
// Animate to the peer pointer positions
useEffect(() => {
@@ -116,36 +128,32 @@ function NetworkedMapPointer({ session, active, gridSize }) {
function animate(time) {
request = requestAnimationFrame(animate);
let interpolatedPointerState = {};
- for (let syncState of Object.values(syncedPointerStateRef.current)) {
- if (!syncState.from || !syncState.to) {
+ for (let interp of Object.values(interpolationsRef.current)) {
+ if (!interp.from || !interp.to) {
continue;
}
- const totalInterpTime = syncState.to.time - syncState.from.time;
- const currentInterpTime = time - syncState.from.time;
+ const totalInterpTime = interp.to.time - interp.from.time;
+ const currentInterpTime = time - interp.from.time;
const alpha = currentInterpTime / totalInterpTime;
if (alpha >= 0 && alpha <= 1) {
- interpolatedPointerState[syncState.id] = {
- id: syncState.to.id,
- visible: syncState.from.visible,
- position: lerp(
- syncState.from.position,
- syncState.to.position,
- alpha
- ),
+ interpolatedPointerState[interp.id] = {
+ id: interp.id,
+ visible: interp.from.visible,
+ position: lerp(interp.from.position, interp.to.position, alpha),
};
}
- if (alpha > 1 && !syncState.to.visible) {
- interpolatedPointerState[syncState.id] = {
- id: syncState.id,
- visible: syncState.to.visible,
- position: syncState.to.position,
+ if (alpha > 1 && !interp.to.visible) {
+ interpolatedPointerState[interp.id] = {
+ id: interp.id,
+ visible: interp.to.visible,
+ position: interp.to.position,
};
- delete syncedPointerStateRef.current[syncState.to.id];
+ delete interpolationsRef.current[interp.id];
}
}
if (!isEmpty(interpolatedPointerState)) {
- setPointerState((prev) => ({
+ setLocalPointerState((prev) => ({
...prev,
...interpolatedPointerState,
}));
@@ -159,7 +167,7 @@ function NetworkedMapPointer({ session, active, gridSize }) {
return (
- {Object.values(pointerState).map((pointer) => (
+ {Object.values(localPointerState).map((pointer) => (
-
-
-
-
-
+
+
+
+
+
+
+
-
- setPeerError(null)}>
-
-
- {peerError} See FAQ for more
- information.
-
-
-
- {}} allowClose={false}>
-
-
- Unable to connect to game, refresh to reconnect.
-
-
-
- {}}
- allowClose={false}
- >
-
-
- Disconnected. Attempting to reconnect...
-
-
-
-
- {authenticationStatus === "unknown" && !offline && }
-
-
+ setPeerError(null)}
+ >
+
+
+ {peerError} See FAQ for more
+ information.
+
+
+
+ {}} allowClose={false}>
+
+
+ Unable to connect to game, refresh to reconnect.
+
+
+
+ {}}
+ allowClose={false}
+ >
+
+
+ Disconnected. Attempting to reconnect...
+
+
+
+
+ {authenticationStatus === "unknown" && !offline && }
+
+
+
);
}
diff --git a/yarn.lock b/yarn.lock
index ffd0ea8..9347d08 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5451,7 +5451,7 @@ fake-indexeddb@^3.1.2:
realistic-structured-clone "^2.0.1"
setimmediate "^1.0.5"
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==