action-engine-js 1.0.0
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/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
# ActionNet - Networking for ActionEngine
|
|
2
|
+
|
|
3
|
+
A complete multiplayer networking solution for ActionEngine games, providing:
|
|
4
|
+
|
|
5
|
+
- **ActionNetManager**: Client-side WebSocket manager with room/lobby system
|
|
6
|
+
- **ActionNetManagerP2P**: Peer-to-peer networking with DHT discovery and WebRTC
|
|
7
|
+
- **ActionNetManagerGUI**: Unified GUI supporting both WebSocket and P2P modes
|
|
8
|
+
- **ActionNetServerUtils**: Server-side utilities for client and room management
|
|
9
|
+
- **SyncSystem**: Generic state synchronization for client-to-client data sharing
|
|
10
|
+
|
|
11
|
+
## Client Identity System
|
|
12
|
+
|
|
13
|
+
ActionNet provides a clean client identity system with three key concepts:
|
|
14
|
+
|
|
15
|
+
### 1. **Client ID** (Unique Identifier)
|
|
16
|
+
- Auto-generated unique identifier for internal tracking
|
|
17
|
+
- Format: `client_1234567890` (WebSocket) or `peer_abc123def` (P2P)
|
|
18
|
+
- Never changes during connection
|
|
19
|
+
- Used for data lookups and internal logic
|
|
20
|
+
|
|
21
|
+
### 2. **Username** (User-Provided Name)
|
|
22
|
+
- Human-readable name provided by the user
|
|
23
|
+
- What users type when connecting (e.g., "Alice", "Player123")
|
|
24
|
+
- Can be changed during session with `setUsername()`
|
|
25
|
+
- May not be unique (multiple users can request same name)
|
|
26
|
+
|
|
27
|
+
### 3. **Display Name** (Server-Generated Unique Name)
|
|
28
|
+
- Auto-generated by server/peer to ensure uniqueness
|
|
29
|
+
- Based on username but made unique with suffixes
|
|
30
|
+
- Examples:
|
|
31
|
+
- First "Player" → `"Player"`
|
|
32
|
+
- Second "Player" → `"Player (1)"`
|
|
33
|
+
- Third "Player" → `"Player (2)"`
|
|
34
|
+
- **This is what you should show in your UI**
|
|
35
|
+
- Automatically updated in user lists
|
|
36
|
+
|
|
37
|
+
## Quick Start - WebSocket Mode
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// Create and connect
|
|
41
|
+
const net = new ActionNetManager({
|
|
42
|
+
url: 'ws://yourserver.com:3000',
|
|
43
|
+
reconnect: true,
|
|
44
|
+
debug: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
net.on('connected', () => console.log('Connected!'));
|
|
48
|
+
net.on('roomList', (rooms) => console.log('Available rooms:', rooms));
|
|
49
|
+
|
|
50
|
+
// Connect with username
|
|
51
|
+
await net.connectToServer({ username: 'Player123' });
|
|
52
|
+
|
|
53
|
+
// Join a room
|
|
54
|
+
await net.joinRoom('lobby-1');
|
|
55
|
+
|
|
56
|
+
// Listen for other players
|
|
57
|
+
net.on('userList', (users) => {
|
|
58
|
+
users.forEach(user => {
|
|
59
|
+
console.log(`${user.displayName} (ID: ${user.id})`);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Send messages
|
|
64
|
+
net.send({ type: 'chat', text: 'Hello!' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start - P2P Mode (No Server Required)
|
|
68
|
+
|
|
69
|
+
P2P mode uses bittorrent trackers for peer discovery - no central server needed.
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Initialize GUI with P2P mode - handles lobby UI
|
|
73
|
+
const gui = new ActionNetManagerGUI(canvases, input, audio, { mode: 'p2p' });
|
|
74
|
+
|
|
75
|
+
// Listen for when user creates/joins a room
|
|
76
|
+
gui.on('joinedRoom', (roomName) => {
|
|
77
|
+
console.log('Joined room:', roomName);
|
|
78
|
+
// Get the network manager and data channel
|
|
79
|
+
const net = gui.getNetManager();
|
|
80
|
+
const dataChannel = net.getDataChannel();
|
|
81
|
+
// Your game session starts here
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
gui.on('leftRoom', () => {
|
|
85
|
+
console.log('Left room');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Update GUI each frame
|
|
89
|
+
gui.action_update(deltaTime);
|
|
90
|
+
gui.action_draw();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or integrate P2P directly:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const net = new ActionNetManagerP2P({ gameId: 'tetris-1v1', debug: true });
|
|
97
|
+
|
|
98
|
+
// Join DHT network and search for rooms
|
|
99
|
+
await net.joinGame('tetris-1v1', 'Player123');
|
|
100
|
+
|
|
101
|
+
// Listen for discovered rooms
|
|
102
|
+
net.on('roomList', (rooms) => {
|
|
103
|
+
rooms.forEach(room => {
|
|
104
|
+
console.log(`${room.username}'s room: ${room.currentPlayers}/${room.maxPlayers} players`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Create your own room (become host)
|
|
109
|
+
net.createRoom();
|
|
110
|
+
net.on('joinedRoom', ({ peerId, dataChannel }) => {
|
|
111
|
+
console.log('Room created, waiting for players...');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Or join someone else's room
|
|
115
|
+
await net.joinRoom(hostPeerId);
|
|
116
|
+
net.on('joinedRoom', ({ peerId, dataChannel }) => {
|
|
117
|
+
console.log('Joined room! Connected via WebRTC');
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### P2P vs WebSocket Mode
|
|
122
|
+
|
|
123
|
+
| Feature | P2P | WebSocket |
|
|
124
|
+
|---------|-----|-----------|
|
|
125
|
+
| Server Required | ❌ No | ✅ Yes |
|
|
126
|
+
| Discovery | DHT (distributed) | Server list |
|
|
127
|
+
| Connection | WebRTC (direct) | WebSocket relay |
|
|
128
|
+
| Latency | Low (direct) | Medium (relay) |
|
|
129
|
+
| Scalability | Unlimited | Server-limited |
|
|
130
|
+
| Setup Complexity | Medium (browser APIs) | Low (server required) |
|
|
131
|
+
|
|
132
|
+
## SyncSystem - State Synchronization
|
|
133
|
+
|
|
134
|
+
`SyncSystem` provides a simple way to synchronize state between clients without writing custom sync logic.
|
|
135
|
+
|
|
136
|
+
### Basic Usage (WebSocket)
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const net = new ActionNetManager({ url: 'ws://localhost:3000' });
|
|
140
|
+
|
|
141
|
+
// Create sync system
|
|
142
|
+
const sync = new SyncSystem({
|
|
143
|
+
send: (msg) => net.send(msg),
|
|
144
|
+
broadcastInterval: 16, // Broadcast every 16ms (~60fps)
|
|
145
|
+
staleThreshold: 200 // Consider remote stale after 200ms
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Register sync sources
|
|
149
|
+
sync.register('player', {
|
|
150
|
+
getFields: () => ({
|
|
151
|
+
score: player.score,
|
|
152
|
+
level: player.level,
|
|
153
|
+
alive: !player.gameOver
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
sync.register('match', {
|
|
158
|
+
getFields: () => ({
|
|
159
|
+
state: matchStateMachine.getState(),
|
|
160
|
+
ready: isReady
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Listen for remote updates
|
|
165
|
+
sync.on('remoteUpdated', (allRemoteData) => {
|
|
166
|
+
updateOpponentDisplay(allRemoteData.player);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
sync.on('remoteStale', () => {
|
|
170
|
+
showDisconnectedWarning();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
sync.on('remoteFresh', () => {
|
|
174
|
+
hideDisconnectedWarning();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Start syncing
|
|
178
|
+
sync.start();
|
|
179
|
+
|
|
180
|
+
// Hook up incoming messages
|
|
181
|
+
net.on('syncUpdate', (msg) => {
|
|
182
|
+
sync.handleSyncUpdate(msg);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Query remote data
|
|
186
|
+
const remotePlayer = sync.getRemote('player');
|
|
187
|
+
if (remotePlayer) {
|
|
188
|
+
opponentScore.text = remotePlayer.score;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### SyncSystem with P2P
|
|
193
|
+
|
|
194
|
+
For P2P, pass the dataChannel's send function:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
const net = new ActionNetManagerP2P({ gameId: 'tetris-1v1' });
|
|
198
|
+
|
|
199
|
+
const sync = new SyncSystem({
|
|
200
|
+
send: (msg) => {
|
|
201
|
+
const channel = net.getDataChannel();
|
|
202
|
+
if (channel && channel.readyState === 'open') {
|
|
203
|
+
channel.send(JSON.stringify(msg));
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
broadcastInterval: 16,
|
|
207
|
+
staleThreshold: 200
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Same registration and event handling as above
|
|
211
|
+
sync.register('player', { getFields: () => ({...}) });
|
|
212
|
+
sync.start();
|
|
213
|
+
|
|
214
|
+
// For P2P, listen to messages on the data channel directly
|
|
215
|
+
const channel = net.getDataChannel();
|
|
216
|
+
channel.onmessage = (event) => {
|
|
217
|
+
try {
|
|
218
|
+
const msg = JSON.parse(event.data);
|
|
219
|
+
if (msg.type === 'syncUpdate') {
|
|
220
|
+
sync.handleSyncUpdate(msg);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error('Failed to parse message:', e);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### SyncSystem API
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// Register a sync source
|
|
232
|
+
sync.register('sourceId', {
|
|
233
|
+
getFields: () => ({ field1: value1, field2: value2 })
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Query remote data
|
|
237
|
+
sync.getRemote('sourceId') // Returns all fields
|
|
238
|
+
sync.getRemoteField('sourceId', 'field1') // Returns single field
|
|
239
|
+
|
|
240
|
+
// Check connection health
|
|
241
|
+
sync.isRemoteStale() // Boolean
|
|
242
|
+
sync.hasRemoteData() // Boolean
|
|
243
|
+
sync.getTimeSinceLastUpdate() // Milliseconds
|
|
244
|
+
|
|
245
|
+
// Manual control
|
|
246
|
+
sync.forceBroadcast() // Send immediately
|
|
247
|
+
sync.clearRemoteData() // Clear remote state
|
|
248
|
+
sync.start() // Begin syncing
|
|
249
|
+
sync.stop() // Stop syncing
|
|
250
|
+
|
|
251
|
+
// Events
|
|
252
|
+
sync.on('remoteUpdated', (data) => {})
|
|
253
|
+
sync.on('remoteStale', () => {})
|
|
254
|
+
sync.on('remoteFresh', () => {})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Ping and RTT Tracking (WebSocket Only)
|
|
258
|
+
|
|
259
|
+
ActionNetManager includes built-in ping/pong for latency tracking.
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
const net = new ActionNetManager({
|
|
263
|
+
url: 'ws://localhost:3000',
|
|
264
|
+
pingInterval: 30000, // Ping every 30 seconds
|
|
265
|
+
pongTimeout: 5000 // Expect pong within 5 seconds
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Get current round-trip time
|
|
269
|
+
const rtt = net.getRTT();
|
|
270
|
+
console.log(`Ping: ${rtt}ms`);
|
|
271
|
+
|
|
272
|
+
// Listen for RTT updates
|
|
273
|
+
net.on('rtt', (rtt) => {
|
|
274
|
+
pingDisplay.text = `${rtt}ms`;
|
|
275
|
+
if (rtt > 200) {
|
|
276
|
+
showLagWarning();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Listen for timeout (pong not received)
|
|
281
|
+
net.on('timeout', () => {
|
|
282
|
+
console.warn('Connection timeout - may be disconnected');
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Auto-Reconnection (WebSocket Only)
|
|
287
|
+
|
|
288
|
+
ActionNetManager supports automatic reconnection with exponential backoff.
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
const net = new ActionNetManager({
|
|
292
|
+
url: 'ws://localhost:3000',
|
|
293
|
+
reconnect: true, // Enable auto-reconnect
|
|
294
|
+
reconnectDelay: 1000, // Start with 1 second delay
|
|
295
|
+
maxReconnectDelay: 30000, // Cap at 30 seconds
|
|
296
|
+
reconnectAttempts: -1 // -1 = infinite attempts
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Listen for reconnection events
|
|
300
|
+
net.on('reconnecting', ({ attempt, delay }) => {
|
|
301
|
+
console.log(`Reconnecting... attempt ${attempt} in ${delay}ms`);
|
|
302
|
+
showReconnectingMessage(attempt);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
net.on('connected', () => {
|
|
306
|
+
console.log('Reconnected!');
|
|
307
|
+
hideReconnectingMessage();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
net.on('reconnectFailed', () => {
|
|
311
|
+
console.log('Max reconnect attempts reached');
|
|
312
|
+
showConnectionFailedMessage();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Get current reconnect attempt count
|
|
316
|
+
const attempts = net.getReconnectAttempts();
|
|
317
|
+
console.log(`Reconnect attempts: ${attempts}`);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Exponential Backoff
|
|
321
|
+
|
|
322
|
+
Reconnect delays increase exponentially:
|
|
323
|
+
- Attempt 1: 1 second
|
|
324
|
+
- Attempt 2: 2 seconds
|
|
325
|
+
- Attempt 3: 4 seconds
|
|
326
|
+
- Attempt 4: 8 seconds
|
|
327
|
+
- Attempt 5: 16 seconds
|
|
328
|
+
- Attempt 6+: 30 seconds (capped at maxReconnectDelay)
|
|
329
|
+
|
|
330
|
+
## Host System
|
|
331
|
+
|
|
332
|
+
The first person to join a room becomes the host. This is useful for peer selection, game flow control, or special privileges.
|
|
333
|
+
|
|
334
|
+
### Client API
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// Check if current user is the host
|
|
338
|
+
if (net.isCurrentUserHost()) {
|
|
339
|
+
console.log('You are the host - you can start the game!');
|
|
340
|
+
showStartButton();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Get the host's info
|
|
344
|
+
const host = net.getHost();
|
|
345
|
+
if (host) {
|
|
346
|
+
console.log('Host:', host.displayName);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Listen for host leaving (only guests receive this)
|
|
350
|
+
net.on('hostLeft', (msg) => {
|
|
351
|
+
console.log('Host left - returning to lobby');
|
|
352
|
+
showLobbyScreen();
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Server API
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
const utils = new ActionNetServerUtils(wss);
|
|
360
|
+
|
|
361
|
+
// Check if a client is the host
|
|
362
|
+
if (utils.isHost(ws)) {
|
|
363
|
+
console.log('This client is the host!');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Get host info for a room
|
|
367
|
+
const host = utils.getHostOfRoom('game-room-1');
|
|
368
|
+
if (host) {
|
|
369
|
+
console.log('Host:', host.displayName);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Best Practices
|
|
374
|
+
|
|
375
|
+
### ✅ DO:
|
|
376
|
+
```javascript
|
|
377
|
+
// Use displayName for anything users see
|
|
378
|
+
chatMessage.text = `${user.displayName}: ${text}`;
|
|
379
|
+
userListItem.text = user.displayName;
|
|
380
|
+
scoreboard.add(user.displayName, score);
|
|
381
|
+
|
|
382
|
+
// Use client ID for internal tracking
|
|
383
|
+
playerData[user.id] = { score: 100 };
|
|
384
|
+
entityMap.set(user.id, entity);
|
|
385
|
+
|
|
386
|
+
// Check host status for game logic
|
|
387
|
+
if (net.isCurrentUserHost()) {
|
|
388
|
+
// Only host can start the game
|
|
389
|
+
showStartButton();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Use SyncSystem for continuous state
|
|
393
|
+
sync.register('position', {
|
|
394
|
+
getFields: () => ({ x: player.x, y: player.y })
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Use custom messages for one-shot events
|
|
398
|
+
net.send({ type: 'attack', damage: 10 });
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### ❌ DON'T:
|
|
402
|
+
```javascript
|
|
403
|
+
// Don't show client IDs to users
|
|
404
|
+
chat.addMessage(client.id, text); // Shows "client_1234567890" - confusing!
|
|
405
|
+
|
|
406
|
+
// Don't use usernames as unique keys
|
|
407
|
+
playerData[client.username] = data; // Can conflict if names aren't unique!
|
|
408
|
+
|
|
409
|
+
// Don't confuse username and displayName
|
|
410
|
+
chat.text = client.username; // Use displayName instead!
|
|
411
|
+
|
|
412
|
+
// Don't sync everything constantly
|
|
413
|
+
sync.register('everything', {
|
|
414
|
+
getFields: () => game.entireState // Too much data!
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Server Setup
|
|
419
|
+
|
|
420
|
+
### WebSocket Server (Node.js)
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
const WebSocket = require('ws');
|
|
424
|
+
const ActionNetServerUtils = require('./ActionNetServerUtils');
|
|
425
|
+
|
|
426
|
+
const wss = new WebSocket.Server({ port: 3000 });
|
|
427
|
+
const utils = new ActionNetServerUtils(wss);
|
|
428
|
+
|
|
429
|
+
wss.on('connection', (ws) => {
|
|
430
|
+
ws.on('message', (data) => {
|
|
431
|
+
const msg = JSON.parse(data.toString());
|
|
432
|
+
|
|
433
|
+
if (msg.type === 'connect') {
|
|
434
|
+
// Register client - automatically generates unique displayName
|
|
435
|
+
utils.registerClient(ws, msg);
|
|
436
|
+
const client = utils.getClient(ws);
|
|
437
|
+
|
|
438
|
+
// Send confirmation
|
|
439
|
+
ws.send(JSON.stringify({
|
|
440
|
+
type: 'connectSuccess',
|
|
441
|
+
clientId: client.id,
|
|
442
|
+
displayName: client.displayName
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
// Broadcast room list
|
|
446
|
+
utils.broadcastToAllClients({
|
|
447
|
+
type: 'roomList',
|
|
448
|
+
rooms: utils.getAvailableRooms()
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (msg.type === 'joinRoom') {
|
|
453
|
+
utils.addToRoom(ws, msg.roomName);
|
|
454
|
+
const client = utils.getClient(ws);
|
|
455
|
+
|
|
456
|
+
ws.send(JSON.stringify({
|
|
457
|
+
type: 'joinSuccess',
|
|
458
|
+
roomName: msg.roomName
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
// Notify room members
|
|
462
|
+
utils.broadcastToRoom(msg.roomName, {
|
|
463
|
+
type: 'userJoined',
|
|
464
|
+
id: client.id,
|
|
465
|
+
displayName: client.displayName,
|
|
466
|
+
isHost: utils.isHost(ws)
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Send user list to joiner
|
|
470
|
+
const users = utils.getClientsInRoom(msg.roomName).map(c => ({
|
|
471
|
+
id: c.id,
|
|
472
|
+
displayName: c.displayName,
|
|
473
|
+
isHost: utils.isHost(ws) && utils.getHostOfRoom(msg.roomName) === ws
|
|
474
|
+
}));
|
|
475
|
+
|
|
476
|
+
ws.send(JSON.stringify({
|
|
477
|
+
type: 'userList',
|
|
478
|
+
users: users
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (msg.type === 'leaveRoom') {
|
|
483
|
+
const client = utils.getClient(ws);
|
|
484
|
+
const wasHost = utils.isHost(ws);
|
|
485
|
+
|
|
486
|
+
utils.removeFromRoom(ws);
|
|
487
|
+
|
|
488
|
+
if (wasHost) {
|
|
489
|
+
// Host left - close room
|
|
490
|
+
utils.broadcastToRoom(client.roomName, {
|
|
491
|
+
type: 'hostLeft',
|
|
492
|
+
displayName: client.displayName
|
|
493
|
+
});
|
|
494
|
+
utils.closeRoom(client.roomName);
|
|
495
|
+
} else {
|
|
496
|
+
// Guest left - notify room
|
|
497
|
+
utils.broadcastToRoom(client.roomName, {
|
|
498
|
+
type: 'userLeft',
|
|
499
|
+
id: client.id,
|
|
500
|
+
displayName: client.displayName
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
ws.on('close', () => {
|
|
507
|
+
const client = utils.getClient(ws);
|
|
508
|
+
if (client) {
|
|
509
|
+
const wasHost = utils.isHost(ws);
|
|
510
|
+
utils.unregisterClient(ws);
|
|
511
|
+
|
|
512
|
+
if (wasHost && client.roomName) {
|
|
513
|
+
// Host disconnected - close room
|
|
514
|
+
utils.broadcastToRoom(client.roomName, {
|
|
515
|
+
type: 'hostLeft',
|
|
516
|
+
displayName: client.displayName
|
|
517
|
+
});
|
|
518
|
+
utils.closeRoom(client.roomName);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## API Reference
|
|
526
|
+
|
|
527
|
+
### ActionNetManager (WebSocket Client)
|
|
528
|
+
|
|
529
|
+
#### Connection
|
|
530
|
+
```javascript
|
|
531
|
+
net.connectToServer({ username: 'Bob' }) // Returns Promise
|
|
532
|
+
net.disconnect()
|
|
533
|
+
net.isConnected() // Returns Boolean
|
|
534
|
+
net.connectionFailed() // Returns Boolean
|
|
535
|
+
net.testServerConnection() // Returns Promise<{available, error}>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Room Management
|
|
539
|
+
```javascript
|
|
540
|
+
net.joinRoom('lobby') // Returns Promise
|
|
541
|
+
net.leaveRoom()
|
|
542
|
+
net.isInRoom() // Returns Boolean
|
|
543
|
+
net.getCurrentRoomName() // Returns String|null
|
|
544
|
+
net.getAvailableRooms() // Returns Array<String>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Identity
|
|
548
|
+
```javascript
|
|
549
|
+
net.getClientId() // Returns String (unique ID)
|
|
550
|
+
net.getUsername() // Returns String (user-provided)
|
|
551
|
+
net.setUsername(name) // Returns Promise
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
#### Host System
|
|
555
|
+
```javascript
|
|
556
|
+
net.isCurrentUserHost() // Returns Boolean
|
|
557
|
+
net.getHost() // Returns {id, displayName, isHost}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### Messaging
|
|
561
|
+
```javascript
|
|
562
|
+
net.send(message) // Returns Boolean
|
|
563
|
+
net.getNewMessages() // Returns Array (polling pattern)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
#### Latency (WebSocket only)
|
|
567
|
+
```javascript
|
|
568
|
+
net.getRTT() // Returns Number (milliseconds)
|
|
569
|
+
net.getReconnectAttempts() // Returns Number
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### Connected Users
|
|
573
|
+
```javascript
|
|
574
|
+
net.getConnectedUsers() // Returns Array<{id, displayName, isHost}>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### ActionNetManagerP2P (P2P Client)
|
|
578
|
+
|
|
579
|
+
#### Game Management
|
|
580
|
+
```javascript
|
|
581
|
+
net.joinGame(gameId, username) // Returns Promise
|
|
582
|
+
net.createRoom()
|
|
583
|
+
net.joinRoom(hostPeerId) // Returns Promise
|
|
584
|
+
net.leaveRoom()
|
|
585
|
+
net.disconnect() // Returns Promise
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
#### State
|
|
589
|
+
```javascript
|
|
590
|
+
net.isConnected() // Returns Boolean
|
|
591
|
+
net.isInRoom() // Returns Boolean
|
|
592
|
+
net.isCurrentUserHost() // Returns Boolean
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### Identity
|
|
596
|
+
```javascript
|
|
597
|
+
net.getUsername() // Returns String
|
|
598
|
+
net.setUsername(name) // Returns Promise
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### Data
|
|
602
|
+
```javascript
|
|
603
|
+
net.getDataChannel() // Returns RTCDataChannel|null
|
|
604
|
+
net.getAvailableRooms() // Returns Array<{peerId, username, displayName, currentPlayers, maxPlayers, slots}>
|
|
605
|
+
net.getConnectedUsers() // Returns Array<{id, displayName, isHost}>
|
|
606
|
+
net.getConnectedPeerCount() // Returns Number (direct connections)
|
|
607
|
+
net.getDiscoveredPeerCount() // Returns Number (DHT peers)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
#### Host-specific (Host only)
|
|
611
|
+
```javascript
|
|
612
|
+
net.acceptJoin(peerId) // Accept pending join request
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### ActionNetManagerGUI (UI Bridge)
|
|
616
|
+
|
|
617
|
+
#### Setup
|
|
618
|
+
```javascript
|
|
619
|
+
// WebSocket mode
|
|
620
|
+
const gui = new ActionNetManagerGUI(canvases, input, audio, 8000);
|
|
621
|
+
|
|
622
|
+
// P2P mode
|
|
623
|
+
const gui = new ActionNetManagerGUI(canvases, input, audio, { mode: 'p2p' });
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### Update & Render
|
|
627
|
+
```javascript
|
|
628
|
+
gui.action_update(deltaTime)
|
|
629
|
+
gui.action_draw()
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Access
|
|
633
|
+
```javascript
|
|
634
|
+
gui.getNetManager() // Returns ActionNetManager or ActionNetManagerP2P
|
|
635
|
+
gui.getUsername() // Returns String
|
|
636
|
+
gui.isConnected() // Returns Boolean
|
|
637
|
+
gui.isInRoom() // Returns Boolean
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
#### SyncSystem
|
|
641
|
+
```javascript
|
|
642
|
+
gui.syncSystem // Access SyncSystem instance
|
|
643
|
+
gui.activateSyncForRoom() // Start syncing when room joined
|
|
644
|
+
gui.deactivateSyncForRoom() // Stop syncing when room left
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
#### Custom Messages
|
|
648
|
+
```javascript
|
|
649
|
+
gui.registerMessageHandler('myEvent', (msg) => {
|
|
650
|
+
// Handle custom message
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
gui.unregisterMessageHandler('myEvent')
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
#### Events
|
|
657
|
+
```javascript
|
|
658
|
+
gui.on('joinedRoom', (roomName) => {})
|
|
659
|
+
gui.on('leftRoom', (roomName) => {})
|
|
660
|
+
gui.on('buttonPressed', () => {})
|
|
661
|
+
gui.on('back', () => {})
|
|
662
|
+
gui.on('selectionChanged', ({oldIndex, newIndex}) => {})
|
|
663
|
+
gui.on('disconnected', () => {})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### ActionNetServerUtils (Server Utilities)
|
|
667
|
+
|
|
668
|
+
#### Client Management
|
|
669
|
+
```javascript
|
|
670
|
+
utils.registerClient(ws, {username, clientId, ...metadata})
|
|
671
|
+
utils.unregisterClient(ws)
|
|
672
|
+
utils.getClient(ws) // Returns {id, username, displayName, roomName}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### Room Management
|
|
676
|
+
```javascript
|
|
677
|
+
utils.addToRoom(ws, roomName) // Returns Boolean
|
|
678
|
+
utils.removeFromRoom(ws)
|
|
679
|
+
utils.closeRoom(roomName)
|
|
680
|
+
utils.getClientsInRoom(roomName) // Returns Array of client objects
|
|
681
|
+
utils.getAvailableRooms() // Returns Array<String>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
#### Broadcasting
|
|
685
|
+
```javascript
|
|
686
|
+
utils.broadcastToRoom(roomName, message) // Send to room members
|
|
687
|
+
utils.broadcastToAllClients(message) // Send to everyone
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
#### Host System
|
|
691
|
+
```javascript
|
|
692
|
+
utils.isHost(ws) // Returns Boolean
|
|
693
|
+
utils.getHostOfRoom(roomName) // Returns client object or null
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
#### Display Names
|
|
697
|
+
```javascript
|
|
698
|
+
utils.generateUniqueDisplayName(username, excludeId) // Returns String
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### SyncSystem
|
|
702
|
+
|
|
703
|
+
#### Registration
|
|
704
|
+
```javascript
|
|
705
|
+
sync.register(sourceId, {
|
|
706
|
+
getFields: () => ({ field1, field2 })
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
sync.unregister(sourceId)
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### Query
|
|
713
|
+
```javascript
|
|
714
|
+
sync.getRemote(sourceId) // Returns Object|null
|
|
715
|
+
sync.getRemoteField(sourceId, field) // Returns Any|null
|
|
716
|
+
sync.getAllRemote() // Returns Object
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
#### State Checks
|
|
720
|
+
```javascript
|
|
721
|
+
sync.isRemoteStale() // Returns Boolean
|
|
722
|
+
sync.hasRemoteData() // Returns Boolean
|
|
723
|
+
sync.getTimeSinceLastUpdate() // Returns Number (ms)
|
|
724
|
+
sync.getRegisteredSources() // Returns Array<String>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### Manual Control
|
|
728
|
+
```javascript
|
|
729
|
+
sync.forceBroadcast() // Broadcast immediately
|
|
730
|
+
sync.clearRemoteData() // Clear remote state
|
|
731
|
+
sync.handleSyncUpdate(message) // Process incoming sync
|
|
732
|
+
sync.start() // Begin syncing
|
|
733
|
+
sync.stop() // Stop syncing
|
|
734
|
+
sync.setSendFunction(fn) // Change transport
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
#### Events
|
|
738
|
+
```javascript
|
|
739
|
+
sync.on('remoteUpdated', (allRemoteData) => {})
|
|
740
|
+
sync.on('remoteStale', () => {})
|
|
741
|
+
sync.on('remoteFresh', () => {})
|
|
742
|
+
sync.on('broadcast', (localData) => {})
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## Events Reference
|
|
746
|
+
|
|
747
|
+
### ActionNetManager Events
|
|
748
|
+
|
|
749
|
+
- `connected`: `() => {}` - Connected to server
|
|
750
|
+
- `disconnected`: `() => {}` - Disconnected from server
|
|
751
|
+
- `error`: `(error) => {}` - Connection or server error
|
|
752
|
+
- `message`: `(msg) => {}` - Any message received
|
|
753
|
+
- `roomList`: `(rooms) => {}` - Available rooms updated
|
|
754
|
+
- `userList`: `(users) => {}` - Users in room updated
|
|
755
|
+
- `joinedRoom`: `(roomName) => {}` - Successfully joined room
|
|
756
|
+
- `leftRoom`: `(roomName) => {}` - Left room
|
|
757
|
+
- `userJoined`: `(user) => {}` - Someone joined your room
|
|
758
|
+
- `userLeft`: `(user) => {}` - Someone left your room
|
|
759
|
+
- `hostLeft`: `(msg) => {}` - Host left, room closing
|
|
760
|
+
- `usernameChanged`: `({oldUsername, newUsername, displayName}) => {}` - Username changed
|
|
761
|
+
- `chat`: `(msg) => {}` - Chat message received
|
|
762
|
+
- `system`: `(msg) => {}` - System message received
|
|
763
|
+
- `rtt`: `(milliseconds) => {}` - Round-trip time updated
|
|
764
|
+
- `timeout`: `() => {}` - Pong not received (connection issue)
|
|
765
|
+
- `reconnecting`: `({attempt, delay}) => {}` - Attempting to reconnect
|
|
766
|
+
- `reconnectFailed`: `() => {}` - Max reconnect attempts reached
|
|
767
|
+
- Custom events based on `message.type`
|
|
768
|
+
|
|
769
|
+
### ActionNetManagerP2P Events
|
|
770
|
+
|
|
771
|
+
- `connected`: `() => {}` - Joined DHT network
|
|
772
|
+
- `disconnected`: `() => {}` - Disconnected from DHT
|
|
773
|
+
- `error`: `(error) => {}` - Connection error
|
|
774
|
+
- `roomList`: `(rooms) => {}` - Available rooms updated
|
|
775
|
+
- `userList`: `(users) => {}` - Users in room updated
|
|
776
|
+
- `joinedRoom`: `({peerId, dataChannel}) => {}` - Joined/created room
|
|
777
|
+
- `leftRoom`: `(peerId) => {}` - Left room
|
|
778
|
+
- `userJoined`: `(user) => {}` - Someone joined your room
|
|
779
|
+
- `userLeft`: `(user) => {}` - Someone left your room
|
|
780
|
+
- `hostLeft`: `({peerId}) => {}` - Host left (guests only)
|
|
781
|
+
- `guestLeft`: `({peerId}) => {}` - Guest left (host only)
|
|
782
|
+
- `joinRequest`: `({peerId, username}) => {}` - Join request received (host only)
|
|
783
|
+
- `joinAccepted`: `({peerId, users}) => {}` - Join accepted by host (guest only)
|
|
784
|
+
- `joinRejected`: `({peerId, reason}) => {}` - Join rejected by host (guest only)
|
|
785
|
+
- `usernameChanged`: `({oldUsername, newUsername, displayName}) => {}` - Username changed
|
|
786
|
+
- `peerHandshook`: `({peerId, username}) => {}` - Initial handshake completed
|
|
787
|
+
|
|
788
|
+
### ActionNetManagerGUI Events
|
|
789
|
+
|
|
790
|
+
- `joinedRoom`: `(roomName) => {}` - Joined room
|
|
791
|
+
- `leftRoom`: `(roomName) => {}` - Left room
|
|
792
|
+
- `buttonPressed`: `() => {}` - Button pressed (for sound effects)
|
|
793
|
+
- `back`: `() => {}` - Back button pressed
|
|
794
|
+
- `backToLogin`: `() => {}` - Back to login screen
|
|
795
|
+
- `selectionChanged`: `({oldIndex, newIndex}) => {}` - Selection changed
|
|
796
|
+
- `disconnected`: `() => {}` - Network disconnected
|
|
797
|
+
- `message:{type}`: `(msg) => {}` - Custom message (emitted for unhandled types)
|
|
798
|
+
|
|
799
|
+
### SyncSystem Events
|
|
800
|
+
|
|
801
|
+
- `remoteUpdated`: `(remoteData) => {}` - Remote data received
|
|
802
|
+
- `remoteStale`: `() => {}` - Remote stopped sending updates
|
|
803
|
+
- `remoteFresh`: `() => {}` - Remote resumed after being stale
|
|
804
|
+
- `broadcast`: `(localData) => {}` - We broadcasted data
|
|
805
|
+
|
|
806
|
+
## See Also
|
|
807
|
+
|
|
808
|
+
- Server example: `game/server/ActionNetServer.js`
|
|
809
|
+
- Server README: `game/server/README.md`
|
|
810
|
+
- Run server: `game/server/start.bat` (Windows) or `game/server/start.sh` (Mac/Linux)
|