onejs-react 0.1.3 → 0.1.4
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/hooks.ts +216 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syncs a value from C# (or any external source) to React state, checking every frame.
|
|
5
|
+
*
|
|
6
|
+
* This hook eliminates the need for C# events or codegen - just read any property
|
|
7
|
+
* and React will automatically update when it changes.
|
|
8
|
+
*
|
|
9
|
+
* @param getter - Function that returns the current value (called every frame)
|
|
10
|
+
* @param deps - Optional dependency array (if getter depends on changing references)
|
|
11
|
+
* @returns The current value, updated each frame if changed
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Sync a C# property to React
|
|
15
|
+
* const health = useFrameSync(() => player.health)
|
|
16
|
+
* const position = useFrameSync(() => transform.position)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With dependencies (if the object reference can change)
|
|
20
|
+
* const health = useFrameSync(() => currentPlayer.health, [currentPlayer])
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Derived values work too
|
|
24
|
+
* const healthPercent = useFrameSync(() => player.health / player.maxHealth * 100)
|
|
25
|
+
*/
|
|
26
|
+
export function useFrameSync<T>(getter: () => T, deps: readonly unknown[] = []): T {
|
|
27
|
+
// Safely get initial value
|
|
28
|
+
const getInitialValue = (): T => {
|
|
29
|
+
try {
|
|
30
|
+
return getter()
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined as T
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
37
|
+
const lastValueRef = useRef<T>(value)
|
|
38
|
+
const getterRef = useRef(getter)
|
|
39
|
+
const runningRef = useRef(false)
|
|
40
|
+
|
|
41
|
+
// Keep getter ref updated
|
|
42
|
+
getterRef.current = getter
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
// Re-initialize when deps change
|
|
46
|
+
try {
|
|
47
|
+
const initial = getterRef.current()
|
|
48
|
+
lastValueRef.current = initial
|
|
49
|
+
setValue(initial)
|
|
50
|
+
} catch {
|
|
51
|
+
// Getter failed, keep current value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
runningRef.current = true
|
|
55
|
+
|
|
56
|
+
const check = () => {
|
|
57
|
+
if (!runningRef.current) return
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const current = getterRef.current()
|
|
61
|
+
if (!Object.is(current, lastValueRef.current)) {
|
|
62
|
+
lastValueRef.current = current
|
|
63
|
+
setValue(current)
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Getter might fail if object was destroyed - that's ok
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (runningRef.current) {
|
|
70
|
+
requestAnimationFrame(check)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
requestAnimationFrame(check)
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
runningRef.current = false
|
|
78
|
+
}
|
|
79
|
+
}, deps)
|
|
80
|
+
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Similar to useFrameSync but with a custom equality function.
|
|
86
|
+
* Useful for objects/structs where reference equality isn't sufficient.
|
|
87
|
+
*
|
|
88
|
+
* @param getter - Function that returns the current value
|
|
89
|
+
* @param isEqual - Custom equality function
|
|
90
|
+
* @param deps - Optional dependency array
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Sync a Vector3, comparing by value not reference
|
|
94
|
+
* const pos = useFrameSyncWith(
|
|
95
|
+
* () => transform.position,
|
|
96
|
+
* (a, b) => a.x === b.x && a.y === b.y && a.z === b.z
|
|
97
|
+
* )
|
|
98
|
+
*/
|
|
99
|
+
export function useFrameSyncWith<T>(
|
|
100
|
+
getter: () => T,
|
|
101
|
+
isEqual: (a: T, b: T) => boolean,
|
|
102
|
+
deps: readonly unknown[] = []
|
|
103
|
+
): T {
|
|
104
|
+
const getInitialValue = (): T => {
|
|
105
|
+
try {
|
|
106
|
+
return getter()
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined as T
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
113
|
+
const lastValueRef = useRef<T>(value)
|
|
114
|
+
const getterRef = useRef(getter)
|
|
115
|
+
const isEqualRef = useRef(isEqual)
|
|
116
|
+
const runningRef = useRef(false)
|
|
117
|
+
|
|
118
|
+
getterRef.current = getter
|
|
119
|
+
isEqualRef.current = isEqual
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
try {
|
|
123
|
+
const initial = getterRef.current()
|
|
124
|
+
lastValueRef.current = initial
|
|
125
|
+
setValue(initial)
|
|
126
|
+
} catch {
|
|
127
|
+
// Getter failed, keep current value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
runningRef.current = true
|
|
131
|
+
|
|
132
|
+
const check = () => {
|
|
133
|
+
if (!runningRef.current) return
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const current = getterRef.current()
|
|
137
|
+
if (!isEqualRef.current(current, lastValueRef.current)) {
|
|
138
|
+
lastValueRef.current = current
|
|
139
|
+
setValue(current)
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Getter might fail if object was destroyed
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (runningRef.current) {
|
|
146
|
+
requestAnimationFrame(check)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
requestAnimationFrame(check)
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
runningRef.current = false
|
|
154
|
+
}
|
|
155
|
+
}, deps)
|
|
156
|
+
|
|
157
|
+
return value
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Throttled version of useFrameSync that only checks at a specified interval.
|
|
162
|
+
* Useful when you don't need per-frame updates and want to reduce overhead.
|
|
163
|
+
*
|
|
164
|
+
* @param getter - Function that returns the current value
|
|
165
|
+
* @param intervalMs - How often to check for changes (in milliseconds)
|
|
166
|
+
* @param deps - Optional dependency array
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // Check every 100ms instead of every frame
|
|
170
|
+
* const score = useThrottledSync(() => gameState.score, 100)
|
|
171
|
+
*/
|
|
172
|
+
export function useThrottledSync<T>(
|
|
173
|
+
getter: () => T,
|
|
174
|
+
intervalMs: number,
|
|
175
|
+
deps: readonly unknown[] = []
|
|
176
|
+
): T {
|
|
177
|
+
const getInitialValue = (): T => {
|
|
178
|
+
try {
|
|
179
|
+
return getter()
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined as T
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [value, setValue] = useState<T>(getInitialValue)
|
|
186
|
+
const lastValueRef = useRef<T>(value)
|
|
187
|
+
const getterRef = useRef(getter)
|
|
188
|
+
|
|
189
|
+
getterRef.current = getter
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
try {
|
|
193
|
+
const initial = getterRef.current()
|
|
194
|
+
lastValueRef.current = initial
|
|
195
|
+
setValue(initial)
|
|
196
|
+
} catch {
|
|
197
|
+
// Getter failed, keep current value
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const id = setInterval(() => {
|
|
201
|
+
try {
|
|
202
|
+
const current = getterRef.current()
|
|
203
|
+
if (!Object.is(current, lastValueRef.current)) {
|
|
204
|
+
lastValueRef.current = current
|
|
205
|
+
setValue(current)
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Getter might fail if object was destroyed
|
|
209
|
+
}
|
|
210
|
+
}, intervalMs)
|
|
211
|
+
|
|
212
|
+
return () => clearInterval(id)
|
|
213
|
+
}, [...deps, intervalMs])
|
|
214
|
+
|
|
215
|
+
return value
|
|
216
|
+
}
|
package/src/index.ts
CHANGED