परिचय
@p2play-js/p2p-game ब्राउज़र-आधारित P2P (WebRTC) मल्टीप्लेयर गेम बनाने के लिए एक मॉड्यूलर TypeScript लाइब्रेरी है। यह प्रदान करती है राज्य सिंक्रोनाइज़ेशन (पूर्ण/डेल्टा), संगति रणनीतियां (टाइमस्टैम्प/प्राधिकरण), एक न्यूनतम WebSocket सिग्नलिंग एडाप्टर, मूवमेंट हेल्पर्स, होस्ट चुनाव/माइग्रेशन, और एक पिंग ओवरले।
डेमो
-
पूर्ण मिनी‑गेम: examples/complete
- बेसिक सिग्नलिंग टेस्टर: examples/basic
आर्किटेक्चर
- लाइब्रेरी WebSocket सिग्नलिंग सर्वर का उपयोग कमरे प्रबंधन, प्लेयर आईडी सूची और SDP/ICE संदेशों को लक्षित साथियों तक रूट करने के लिए करती है।
- पीयर पूर्ण‑जाल (full‑mesh) बनाते हैं: हर जोड़ी में, जिसका
playerId
शब्दकोश क्रम में छोटा है, वही WebRTC offer बनाता है — डबल ऑफर टकराव से बचाव। - DataChannel बनने के बाद गेम संदेश सीधे पीयर‑टू‑पीयर बहते हैं; सिग्नलिंग सर्वर अब एप्लीकेशन ट्रैफिक रिले नहीं करता।
- होस्ट चुनाव नियतात्मक है: सबसे छोटा
playerId
होस्ट बनता है। होस्ट के निकलने पर अगला छोटा चुना जाता है और वह नया पूर्ण स्नैपशॉट भेजता है।
सिग्नलिंग सीक्वेंस
फुल‑मेश टोपोलॉजी
राज्य सिंक्रोनाइज़ेशन
- पूर्ण स्नैपशॉट: जुड़ने/माइग्रेशन पर, सुधारात्मक री‑सिंक।
- डेल्टा अपडेट: मार्ग‑विशिष्ट लक्षित परिवर्तन (व्यवहार में अक्सर मिश्रित दृष्टिकोण)।
संगति
- टाइमस्टैम्प (डिफ़ॉल्ट): प्रति‑प्रेषक अनुक्रम पर Last‑Writer‑Wins (LWW)।
- प्राधिकृत (authoritative): केवल अधिकार (होस्ट/नियत आईडी) से क्रियाएं स्वीकारें।
seq
अंतिम देखे से कम है, उसे छोड़ दें। प्राधिकृत मोड में एक पीयर (अक्सर विश्वसनीय होस्ट) सभी क्रियाएं लागू करता है; अन्य केवल इरादे भेजते और सुधार स्वीकारते हैं।
मूवमेंट
लक्ष्य है नेटवर्क जिटर में भी स्मूद पर पूर्वानुमेय गति। इसके लिए इंटरपोलेशन और सीमित एक्सट्रापोलेशन (छोटी भविष्यवाणी खिड़कियां) का संयोजन किया जाता है ताकि देर से आए अपडेट छिपें और सच्चाई से दूर न जाए।
इंटरपोलेशन
नया रिमोट पोजीशन आते ही स्नैप नहीं करते; हर फ्रेम शेष दूरी का एक अंश चलते हैं। smoothing
उस अंश (0..1) को नियंत्रित करता है — बड़ा मान लैग घटाता, पर "फ्लोटी" लग सकता है।
// Pseudocode
const clampedVx = clamp(velocity.x, -maxSpeed, maxSpeed);
const clampedVy = clamp(velocity.y, -maxSpeed, maxSpeed);
// allowedDtSec एक्सट्रापोलेशन कैप को ध्यान में रखता है
position.x += clampedVx * allowedDtSec * smoothing;
position.y += clampedVy * allowedDtSec * smoothing;
// वैकल्पिक Z अक्ष
0.2–0.3
से शुरू करें। यदि गति इनपुट से पीछे है तो बढ़ाएँ; यदि दोलन/ओवरशूट दिखे तो घटाएँ।
एक्सट्रापोलेशन (सीमा सहित)
जब फ्रेम में नया अपडेट न हो, तो अस्थायी रूप से अंतिम ज्ञात वेग से आगे प्रोजेक्ट करें; extrapolationMs
(जैसे 120–140ms) से विंडो सीमित रखें — बजट पार होते ही रुकें और अगले प्राधिकृत अपडेट की प्रतीक्षा करें।
2D बनाम 3D
डिफ़ॉल्ट 2D; सरल 3D हेतु z
जोड़ें। यदि worldBounds.depth
दिया है तो Z पर भी कैम्पिंग लागू होती है।
विश्व सीमाएं बनाम ओपन‑वर्ल्ड
worldBounds
के साथ, पोजीशन [0..width]
और [0..height]
(और Z के लिए [0..depth]
) तक सीमित। ओपन‑वर्ल्ड के लिए ignoreWorldBounds: true
करें (केवल खिलाड़ी‑बनाम‑खिलाड़ी टक्करें रहती हैं)।
टक्कर (वृत्त/गोला)
समान त्रिज्या के वृत्त/गोलों को सममित रूप से अलग करके टकराव हल होते हैं। केंद्रों के बीच नार्मलाइज़्ड वेक्टर निकालें और दोनों को ओवरलैप/2 से धकेलें — सरल और स्थिर।
// दो खिलाड़ी A,B, त्रिज्या r
const dx = B.x - A.x, dy = B.y - A.y, dz = (B.z||0) - (A.z||0);
const dist = Math.max(1e-6, Math.hypot(dx, dy, dz));
const overlap = Math.max(0, 2*r - dist) / 2;
const nx = dx / dist, ny = dy / dist, nz = dz / dist;
A.x -= nx * overlap; A.y -= ny * overlap; A.z = (A.z||0) - nz * overlap;
B.x += nx * overlap; B.y += ny * overlap; B.z = (B.z||0) + nz * overlap;
O(n²)
) — छोटे कमरों में ठीक, भीड़ में ग्रिड/क्वाडट्री जैसे विभाजन जोड़ें।
फ़्लो: मूवमेंट स्टेप
टक्कर समाधान (वृत्त/गोला)
नेटवर्किंग विवरण
- बैकप्रेशर रणनीतियाँ: संतृप्त चैनलों पर मर्ज/ड्रॉप।
- क्षमता:
maxPlayers
लागू करना +maxCapacityReached
इवेंट। - STUN/TURN: कड़े नेटवर्क के लिए TURN; सिग्नलिंग के लिए WSS।
बैकप्रेशर
बैकप्रेशर DataChannel को ओवरलोड से बचाता है। जब RTCDataChannel.bufferedAmount
थ्रेसहोल्ड से ऊपर बढ़े,
तो अस्थायी रूप से भेजना रोकें, कम‑मूल्य संदेश छोड़ें, या कई अपडेट को नवीनतम में संक्षेपित करें।
send()
bufferedAmount
बढ़ाता है जब तक ब्राउज़र नेटवर्क पर फ़्लश न करे। यदि भेजना डिलीवरी से तेज़ रहा तो विलंब बढ़ेगा और ऐप अटकने लगेगा।
रणनीतियाँ
- off: कोई सुरक्षा नहीं — बहुत छोटे/अनियमित संदेशों हेतु।
- drop-moves: थ्रेसहोल्ड से ऊपर नए
move
छोड़ें। - coalesce-moves: प्रत्येक पीयर का केवल नवीनतम
move
रखें।
const multiP2PGame = new P2PGameLibrary({
signaling,
backpressure: { strategy: 'coalesce-moves', thresholdBytes: 256 * 1024 }
});
इवेंट्स और API (चयन)
on('playerMove')
,on('inventoryUpdate')
,on('objectTransfer')
on('stateSync')
,on('stateDelta')
,on('hostChange')
,on('ping')
broadcastMove()
,updateInventory()
,transferItem()
broadcastPayload()
,sendPayload()
setStateAndBroadcast()
,announcePresence()
,getHostId()
इवेंट्स ओवरव्यू
इवेंट | सिग्नेचर | विवरण |
---|---|---|
playerMove | (playerId, position) | मूवमेंट लागू |
inventoryUpdate | (playerId, items) | इन्वेंट्री बदली |
objectTransfer | (from, to, item) | वस्तु हस्तांतरित |
sharedPayload | (from, payload, channel?) | जनरल पेलोड प्राप्त |
stateSync | (state) | पूर्ण स्नैपशॉट मिला |
stateDelta | (delta) | स्टेट डेल्टा मिला |
peerJoin | (playerId) | पीयर जुड़ा |
peerLeave | (playerId) | पीयर निकला |
hostChange | (hostId) | नया होस्ट |
ping | (playerId, ms) | RTT |
maxCapacityReached | (maxPlayers) | क्षमता पूरी; नए कनेक्शन अस्वीकृत |
लाइफसाइकल और उपस्थिति
- उपस्थिति:
announcePresence(playerId)
जल्दी कॉल करें ताकि साथियों को तुरंत प्लेयर दिखे। - peerJoin/peerLeave: UI एंटिटीज़ दिखा/छुपा सकता है; होस्ट‑साइड क्लीनअप हेतु
cleanupOnPeerLeave: true
सक्षम करें — होस्ट प्रविष्टियाँ हटाकर डेल्टा प्रसारित करता है। - क्षमता सीमा:
maxPlayers
सेट करें; सीमा पर नई शुरुआत निषिद्ध और अतिरिक्त offers को अनदेखा किया जाएगा;maxCapacityReached
इवेंट UI को सूचित करता है।
प्रकार संदर्भ
type SerializationStrategy = "json" | "binary-min";
type SyncStrategy = "full" | "delta"; // सलाह: 'hybrid' मोड स्विच नहीं
type ConflictResolution = "timestamp" | "authoritative";
interface BackpressureOptions {
strategy?: "off" | "drop-moves" | "coalesce-moves";
thresholdBytes?: number; // डिफ़ॉल्ट ~256KB
}
interface DebugOptions {
enabled?: boolean;
onSend?: (info: {
type: "broadcast" | "send";
to: string | "all";
payloadBytes: number;
delivered: number;
queued: number;
serialization: SerializationStrategy;
timestamp: number;
}) => void;
}
interface MovementOptions {
maxSpeed?: number;
smoothing?: number; // 0..1
extrapolationMs?: number;
worldBounds?: { width: number; height: number; depth?: number };
ignoreWorldBounds?: boolean;
playerRadius?: number;
}
interface GameLibOptions {
maxPlayers?: number;
syncStrategy?: SyncStrategy; // सलाह: आप तय करें कब full बनाम delta भेजना है
conflictResolution?: ConflictResolution;
authoritativeClientId?: string;
serialization?: SerializationStrategy;
iceServers?: RTCIceServer[];
cleanupOnPeerLeave?: boolean;
debug?: DebugOptions;
backpressure?: BackpressureOptions;
pingOverlay?: { enabled?: boolean; position?: "top-left"|"top-right"|"bottom-left"|"bottom-right"; canvas?: HTMLCanvasElement | null };
movement?: MovementOptions;
}
type EventMap = {
playerMove: (playerId: string, position: { x:number; y:number; z?:number }) => void;
inventoryUpdate: (playerId: string, items: Array<{ id:string; type:string; quantity:number }>) => void;
objectTransfer: (fromId: string, toId: string, item: { id:string; type:string; quantity:number }) => void;
stateSync: (state: GlobalGameState) => void;
stateDelta: (delta: StateDelta) => void;
peerJoin: (playerId: string) => void;
peerLeave: (playerId: string) => void;
hostChange: (hostId: string) => void;
ping: (playerId: string, ms: number) => void;
sharedPayload: (from: string, payload: unknown, channel?: string) => void;
maxCapacityReached: (maxPlayers: number) => void;
};
interface GlobalGameState {
players: Record;
inventories: Record>;
objects: Record }>;
tick: number;
}
interface StateDelta { tick:number; changes: Array<{ path:string; value:unknown }> }
डेल्टा पथ नियम
- पथ dot‑separated ऑब्जेक्ट कीज़ हैं (array index समर्थित नहीं)।
- लक्षित अपडेट हेतु संरचना सपाट/कीड रखें (जैसे
objects.chest.42
) — गहरे arrays से बचें।
// अच्छा: ऑब्जेक्ट मैप
{ path: 'objects.chest.42', value: { id:'chest.42', kind:'chest', data:{ opened:true } } }
// समर्थित नहीं: 'objects[3]' या 'players.list.0' जैसे इंडेक्स पथ
P2PGameLibrary
कन्स्ट्रक्टर
new P2PGameLibrary(options: GameLibOptions & { signaling: WebSocketSignaling | SignalingAdapter })
लाइफसाइकल
await start(): Promise
on(name: N, handler: EventMap[N]): () => void
getState(): GlobalGameState
getHostId(): string | undefined
setPingOverlayEnabled(enabled: boolean): void
tick(now?: number): void // प्रति फ्रेम एक बार इंटरपोलेशन/टक्कर लागू
स्टेट उपयोगिताएँ
setStateAndBroadcast(selfId: string, changes: Array<{ path:string; value:unknown }>): string[]
broadcastFullState(selfId: string): void
broadcastDelta(selfId: string, paths: string[]): void
गेमप्ले API
announcePresence(selfId: string, position = { x:0, y:0 }): void
broadcastMove(selfId: string, position: {x:number;y:number;z?:number}, velocity?: {x:number;y:number;z?:number}): void
updateInventory(selfId: string, items: Array<{ id:string; type:string; quantity:number }>): void
transferItem(selfId: string, to: string, item: { id:string; type:string; quantity:number }): void
पेलोड API
broadcastPayload(selfId: string, payload: unknown, channel?: string): void
sendPayload(selfId: string, to: string, payload: unknown, channel?: string): void
संदेश (ट्रांसपोर्ट)
// NetMessage union (चयन)
type NetMessage =
| { t:"move"; from:string; ts:number; seq?:number; position:{x:number;y:number;z?:number}; velocity?:{x:number;y:number;z?:number} }
| { t:"inventory"; from:string; ts:number; seq?:number; items:Array<{id:string;type:string;quantity:number}> }
| { t:"transfer"; from:string; ts:number; seq?:number; to:string; item:{id:string;type:string;quantity:number} }
| { t:"state_full"; from:string; ts:number; seq?:number; state: GlobalGameState }
| { t:"state_delta"; from:string; ts:number; seq?:number; delta: StateDelta }
| { t:"payload"; from:string; ts:number; seq?:number; payload: unknown; channel?: string };
// Serialization
// strategy: "json" (string frames) या "binary-min" (ArrayBuffer UTF-8 JSON)
सिग्नलिंग एडाप्टर
लाइब्रेरी किसी भी बैकएंड (WebSocket, REST, आदि) के माध्यम से SDP/ICE बदलने के लिए इस एब्स्ट्रैक्शन का प्रयोग करती है।
interface SignalingAdapter {
localId: string;
roomId?: string;
register(): Promise; // कमरे में जुड़ें और roster प्राप्त करें
announce(desc: RTCSessionDescriptionInit, to?: string): Promise;
onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void): void;
onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void): void;
onRoster(cb: (roster: string[]) => void): void;
sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise;
}
उदाहरण: न्यूनतम कस्टम एडाप्टर (WebSocket)
साधारण WebSocket सिग्नलिंग सर्वर का उपयोग करता हुआ छोटा इम्प्लीमेंटेशन।
class SimpleWsSignaling implements SignalingAdapter {
constructor(public localId: string, public roomId: string, private url: string) {
this.ws = new WebSocket(this.url);
}
private ws: WebSocket;
private rosterCb?: (list: string[]) => void;
private descCb?: (d: RTCSessionDescriptionInit, from: string) => void;
private iceCb?: (c: RTCIceCandidateInit, from: string) => void;
async register(): Promise {
await new Promise((resolve) => {
this.ws.addEventListener('open', () => {
this.ws.send(JSON.stringify({ kind:'register', roomId: this.roomId, from: this.localId, announce:true }));
resolve();
});
});
this.ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data);
if (msg.sys === 'roster' && this.rosterCb) this.rosterCb(msg.roster);
if (msg.kind === 'desc' && this.descCb) this.descCb(msg.payload, msg.from);
if (msg.kind === 'ice' && this.iceCb) this.iceCb(msg.payload, msg.from);
});
}
onRoster(cb: (roster: string[]) => void){ this.rosterCb = cb; }
onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void){ this.descCb = cb; }
onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void){ this.iceCb = cb; }
async announce(desc: RTCSessionDescriptionInit, to?: string): Promise {
this.ws.send(JSON.stringify({ kind:'desc', roomId:this.roomId, from:this.localId, to, payload: desc }));
}
async sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise {
this.ws.send(JSON.stringify({ kind:'ice', roomId:this.roomId, from:this.localId, to, payload: candidate }));
}
}
// लाइब्रेरी के साथ उपयोग
const signaling = new SimpleWsSignaling('alice', 'room-1', 'wss://your-signal.example');
await signaling.register();
const multiP2PGame = new P2PGameLibrary({ signaling });
await multiP2PGame.start();
उदाहरण: REST + लंबी‑पोलिंग एडाप्टर
जहाँ WebSocket उपलब्ध न हो, वहाँ HTTP एंडपॉइंट्स और पोलिंग लूप से संदेश प्राप्त करें।
class RestPollingSignaling implements SignalingAdapter {
constructor(public localId: string, public roomId: string, private baseUrl: string) {}
private rosterCb?: (list: string[]) => void;
private descCb?: (d: RTCSessionDescriptionInit, from: string) => void;
private iceCb?: (c: RTCIceCandidateInit, from: string) => void;
private polling = false;
async register(): Promise {
await fetch(`${this.baseUrl}/register`, { method:'POST', headers:{ 'content-type':'application/json' }, body: JSON.stringify({ roomId:this.roomId, from:this.localId, announce:true }) });
this.polling = true;
void this.poll();
}
private async poll(): Promise {
while (this.polling) {
try {
const res = await fetch(`${this.baseUrl}/poll?roomId=${encodeURIComponent(this.roomId)}&from=${encodeURIComponent(this.localId)}`);
if (!res.ok) { await new Promise(r=>setTimeout(r,1000)); continue; }
const msgs = await res.json();
for (const msg of msgs) {
if (msg.sys === 'roster' && this.rosterCb) this.rosterCb(msg.roster);
if (msg.kind === 'desc' && this.descCb) this.descCb(msg.payload, msg.from);
if (msg.kind === 'ice' && this.iceCb) this.iceCb(msg.payload, msg.from);
}
} catch {
await new Promise(r=>setTimeout(r,1000));
}
}
}
onRoster(cb: (roster: string[]) => void){ this.rosterCb = cb; }
onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void){ this.descCb = cb; }
onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void){ this.iceCb = cb; }
async announce(desc: RTCSessionDescriptionInit, to?: string): Promise {
await fetch(`${this.baseUrl}/send`, { method:'POST', headers:{ 'content-type':'application/json' }, body: JSON.stringify({ kind:'desc', roomId:this.roomId, from:this.localId, to, payload: desc }) });
}
async sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise {
await fetch(`${this.baseUrl}/send`, { method:'POST', headers:{ 'content-type':'application/json' }, body: JSON.stringify({ kind:'ice', roomId:this.roomId, from:this.localId, to, payload: candidate }) });
}
}
// उपयोग
const restSignaling = new RestPollingSignaling('alice','room-1','https://your-signal.example');
await restSignaling.register();
const multiP2PGame2 = new P2PGameLibrary({ signaling: restSignaling });
await multiP2PGame2.start();
WebSocketSignaling
उदाहरणों में प्रयुक्त रेफरेंस इम्प्लीमेंटेशन; प्रोटोकॉल:
{ sys:'roster', roster:string[] }
ब्रॉडकास्ट; लक्षित संदेश to
से।
new WebSocketSignaling(localId: string, roomId: string, serverUrl: string)
await signaling.register();
signaling.onRoster((list) => {/* UI अपडेट */});
signaling.onRemoteDescription((desc, from) => {/* PeerManager को दें */});
signaling.onIceCandidate((cand, from) => {/* PeerManager को दें */});
संदेश स्वरूप
// क्लाइंट → सर्वर (register)
{ roomId: string, from: string, announce: true, kind: 'register' }
// सर्वर → क्लाइंट (roster broadcast)
{ sys: 'roster', roomId: string, roster: string[] }
// क्लाइंट → सर्वर (SDP/ICE, लक्षित या कमरे में ब्रॉडकास्ट)
{ kind: 'desc'|'ice', roomId: string, from: string, to?: string, payload: any, announce?: true }
PeerManager (आंतरिक)
- हर पीयर के लिए एक RTCPeerConnection और एक RTCDataChannel बनाए रखता है तथा आवश्यक कॉलबैक वायर करता है।
- हर जोड़ी में छोटा
playerId
ऑफर बनाकर कनेक्शन शुरू करता है; दूसरा उत्तर देता है — एक‑साथ ऑफर से बचाव। peerJoin
,peerLeave
,hostChange
,ping
इवेंट्स निकालता है; डिकोडेड नेटवर्क संदेशों कोnetMessage
के रूप में अग्रसारित करता है।- बैकप्रेशर:
off
: चैनल खुला हो तो हमेशा भेजें।drop-moves
:bufferedAmount
सीमा पार होने पर नएmove
छोड़ें।coalesce-moves
: पुराने queuedmove
को नवीनतम से बदलें।
- क्षमता:
maxPlayers
लागू; अतिरिक्त ऑफर अनदेखा;maxCapacityReached(maxPlayers)
ट्रिगर।
EventBus (आंतरिक)
class EventBus {
on(name: N, fn: EventMap[N]): () => void
off(name: N, fn: EventMap[N]): void
emit(name: N, ...args: Parameters): void
}
आमतौर पर आप P2PGameLibrary.on()
के माध्यम से सब्सक्राइब करते हैं जो आंतरिक बस को डेलीगेट करता है।
PingOverlay
यह ओवरले पेज पर एक छोटा डैशबोर्ड बनाता है जो जुड़े साथियों तक RTT (राउंड‑ट्रिप टाइम) ट्रैक करता है और छोटी हिस्ट्री (~60 सैंपल) रखता है। डेवलपमेंट में स्पाइक्स पकड़ने, TURN उपयोग जाँचने और साथियों की तुलना हेतु उपयोगी।
विकल्प
{
enabled?: boolean; // डिफ़ॉल्ट false
position?: 'top-left'|'top-right'|'bottom-left'|'bottom-right'; // डिफ़ॉल्ट 'top-right'
canvas?: HTMLCanvasElement | null; // अपना canvas दें या ओवरले को बनाने दें
}
उपयोग
const multiP2PGame = new P2PGameLibrary({ signaling, pingOverlay: { enabled: true, position: 'top-right' } });
// रनटाइम पर टॉगल
multiP2PGame.setPingOverlayEnabled(false);
सीरियलाइज़ेशन
- रणनीतियाँ:
json
(string frames) याbinary-min
(ArrayBuffer UTF‑8 JSON)। - असमर्थित रणनीति पर त्रुटि फेंकी जाती है।
interface Serializer {
encode(msg: NetMessage): string | ArrayBuffer;
decode(data: string | ArrayBuffer): NetMessage;
}
function createSerializer(strategy: 'json'|'binary-min' = 'json'): Serializer
उदाहरण
प्राधिकृत होस्ट इरादे लागू करता है
const isHost = () => multiP2PGame.getHostId() === localId;
multiP2PGame.on("sharedPayload", (from, payload, channel) => {
if (!isHost()) return;
if (channel === "move-intent" && typeof payload === "object") {
const p = payload as { pos:{x:number;y:number}; vel?:{x:number;y:number} };
multiP2PGame.broadcastMove(multiP2PGame.getHostId()!, p.pos, p.vel);
}
});
क्षणिक पेलोड को साझा स्टेट में सहेजना
multiP2PGame.on("sharedPayload", (from, payload, channel) => {
if (channel !== "status") return;
if (payload && typeof payload === "object" && "hp" in (payload as any)) {
multiP2PGame.setStateAndBroadcast(multiP2PGame.getHostId()!, [
{ path: `objects.playerStatus.${from}`, value: { id:`playerStatus.${from}`, kind:"playerStatus", data:{ hp:(payload as any).hp } } }
]);
}
});
चयनात्मक डेल्टा अपडेट
const paths = multiP2PGame.setStateAndBroadcast(localId, [
{ path:"objects.chest.42", value:{ id:"chest.42", kind:"chest", data:{ opened:true } } }
]);
// paths == ["objects.chest.42"]
इवेंट्स संदर्भ
playerMove
game.on('playerMove', (playerId, position) => {
drawAvatar(playerId, position);
});
inventoryUpdate
game.on('inventoryUpdate', (playerId, items) => {
ui.updateInventory(playerId, items);
});
objectTransfer
game.on('objectTransfer', (from, to, item) => {
ui.toast(`${from} ने ${item.id} दिया ${to} को`);
});
sharedPayload
game.on('sharedPayload', (from, payload, channel) => {
if (channel === 'chat') chat.add(from, (payload as any).text);
});
stateSync
game.on('stateSync', (state) => {
world.hydrate(state);
});
stateDelta
game.on('stateDelta', (delta) => {
world.applyDelta(delta);
});
peerJoin / peerLeave
game.on('peerJoin', (id) => ui.addPeer(id));
game.on('peerLeave', (id) => ui.removePeer(id));
hostChange
game.on('hostChange', (hostId) => ui.setHost(hostId));
ping
game.on('ping', (id, ms) => ui.setPing(id, ms));
maxCapacityReached
game.on('maxCapacityReached', (max) => ui.alert(`कमरा भर चुका है (${max})`));
उत्पादन नोट्स
- ICE (TURN) और सुरक्षित सिग्नलिंग (WSS) उपलब्ध कराएँ।
- निष्पक्षता हेतु विश्वसनीय/हेडलैस होस्ट के साथ authoritative मोड पर विचार करें।
RTCDataChannel.bufferedAmount
मॉनिटर करें और बैकप्रेशर ट्यून करें।
रीकनेक्ट और UX चेकलिस्ट
- पीयर्स ड्रॉप हों तो "reconnecting" UI दिखाएँ; वापसी पता करने हेतु roster का सहारा लें।
- माइग्रेशन के बाद होस्ट नया
state_full
भेजकर क्लाइंट्स को लाइन में लाता है। - वैकल्पिक तौर पर
cleanupOnPeerLeave
सक्षम करें ताकि होस्ट छोड़ने पर स्टेट छांटे।
डीबगिंग
const game = new P2PGameLibrary({
signaling,
debug: {
enabled: true,
onSend(info){
console.log('[send]', info.type, 'to', info.to, 'bytes=', info.payloadBytes, 'queued=', info.queued);
}
}
});
ब्राउज़र संगतता
- नए Chrome/Firefox/Edge/Safari DataChannel सपोर्ट करते हैं; Safari को प्रोडक्शन में HTTPS/WSS चाहिए।
- एंटरप्राइज़/होटल नेटवर्क के लिए TURN लगाएँ; रिले पर विलंब अधिक रहेगा।
समस्या निवारण
WebRTC कनेक्शन बन नहीं रहा
- मिक्स्ड कंटेंट: पेज और सिग्नलिंग दोनों HTTPS/WSS हों (ब्राउज़र HTTPS पेज से WS ब्लॉक करते हैं)।
- TURN गायब: एंटरप्राइज़/होटल नेटवर्क पर सीधे P2P ब्लॉक;
iceServers
में TURN क्रेडेंशियल दें। - CORS/फ़ायरवॉल: सिग्नलिंग एन्डपॉइंट को ओरिजिन स्वीकारना चाहिए; रिवर्स‑प्रॉक्सी नियम/पोर्ट (TLS 443) जाँचें।
DataChannel रुक‑रुक कर (उच्च विलंब, इनपुट लेट)
- बैकप्रेशर:
coalesce-moves
याdrop-moves
सक्षम करें औरthresholdBytes
ट्यून करें (256–512 KB से शुरू)। - मैसेज आकार घटाएँ: डेल्टा अपनाएँ; binary‑min; वेक्टर क्वांटाइज़।
- भेजने की दर घटाएँ: मूवमेंट ब्रॉडकास्ट (30–60 Hz) थ्रॉटल करें और इंटरपोलेशन पर भरोसा करें।
होस्ट बदलने के बाद असंगति
- सुनिश्चित करें नया होस्ट
state_full
ब्रॉडकास्ट करे (लाइब्रेरी स्वतः ट्रिगर करती है)। - क्लाइंट्स पूर्ण स्नैपशॉट लागू करें और लोकल कैश साफ़ करें (कुछ फ्रेम्स इंटरपोलेशन स्थिर होने दें)।
Safari विशेष
- localhost के बाहर HTTPS/WSS आवश्यक।
- यदि आपके रिले को चाहिए तो STUN/TURN URLs में
?transport=udp
जैसे ट्रांसपोर्ट पैरामीटर जाँचें।
गेम वर्कफ़्लो
विभिन्न शैलियों के लिए नेटवर्किंग, संगति और स्टेट को जोड़ने वाले एंड‑टू‑एंड पैटर्न।
1) रीयल‑टाइम एरीना (एक्शन/शूटर)
- संगति:
timestamp
से शुरू; विश्वसनीय होस्ट हो तोauthoritative
। - सिंक: स्थिर अवस्था में डेल्टा; होस्ट माइग्रेशन पर कभी‑कभार पूरा स्नैपशॉट।
- बैकप्रेशर:
coalesce-moves
— केवल नवीनतम मूवमेंट रखें।
// सेटअप
const multiP2PGame = new P2PGameLibrary({ signaling, conflictResolution: 'timestamp', backpressure: { strategy: 'coalesce-moves' }, movement: { smoothing: 0.2, extrapolationMs: 120 } });
await multiP2PGame.start();
// लोकल इनपुट → ब्रॉडकास्ट मूवमेंट (क्लाइंट प्रेडिक्शन आपका रेंडरर संभाले)
function onInput(vec){
const pos = getPredictedPosition(vec);
multiP2PGame.broadcastMove(localId, pos, vec);
}
multiP2PGame.on('playerMove', (id, pos) => renderPlayer(id, pos));
2) को‑ऑप RPG (इन्वेंट्री, होस्ट इरादे लागू करता है)
- संगति:
authoritative
(होस्ट आइटम उपयोग/दरवाज़े/चेस्ट सत्यापित करता है)। - प्रोटोकॉल: क्लाइंट इरादे (पेलोड) भेजते; होस्ट स्टेट बदलकर डेल्टा प्रसारित करता है।
// क्लाइंट केवल होस्ट को इरादे भेजता
function usePotion(){
multiP2PGame.sendPayload(localId, multiP2PGame.getHostId()!, { action: 'use-item', itemId: 'potion' }, 'intent');
}
// होस्ट इरादे संभालकर स्टेट बदलता
const isHost = () => multiP2PGame.getHostId() === localId;
multiP2PGame.on('sharedPayload', (from, payload, channel) => {
if (!isHost() || channel !== 'intent') return;
if ((payload as any).action === 'use-item') {
const inv = getInventoryAfterUse(from, (payload as any).itemId);
multiP2PGame.setStateAndBroadcast(localId, [ { path: `inventories.${from}`, value: inv } ]);
}
});
3) टर्न‑आधारित टैक्टिक्स (निर्धारक, पूर्ण स्नैपशॉट)
- संगति: प्राधिकृत होस्ट नियम और टर्न क्रम लागू करता है।
- सिंक: प्रति चाल छोटा डेल्टा; सुरक्षा हेतु हर N चाल पर पूर्ण स्नैपशॉट।
interface TurnMove { unitId:string; to:{x:number;y:number} }
multiP2PGame.on('sharedPayload', (from, payload, channel) => {
if (channel !== 'turn-move' || multiP2PGame.getHostId() !== localId) return;
const mv = payload as TurnMove;
const ok = validateMove(currentState, from, mv);
if (!ok) return; // अवैध चाल अस्वीकार
applyMove(currentState, mv);
multiP2PGame.setStateAndBroadcast(localId, [ { path: `players.${from}.lastMove`, value: mv } ]);
});
// हर 10 चाल पर पूर्ण स्नैपशॉट
if (currentState.tick % 10 === 0) multiP2PGame.broadcastFullState(localId);
4) पार्टी गेम + लॉबी (क्षमता और माइग्रेशन)
maxPlayers
सेट करें;maxCapacityReached
हैंडल कर उपयोगकर्ता को सूचित करें।- रोस्टर से लॉबी दिखाएँ; निकलने पर होस्ट स्वतः माइग्रेट।
const multiP2PGame = new P2PGameLibrary({ signaling, maxPlayers: 8 });
multiP2PGame.on('maxCapacityReached', (max) => showToast(`Room is full (${max})`));
multiP2PGame.on('hostChange', (host) => updateLobbyHost(host));
5) ओपन‑वर्ल्ड सैंडबॉक्स (नो बाउंड्स, Z अक्ष)
- दुनिया की सीमाएँ निष्क्रिय; केवल खिलाड़ी‑खिलाड़ी टक्करें।
- यदि आप बार‑बार अपडेट भेजते हैं तो
binary-min
से पेलोड बचत।
const multiP2PGame = new P2PGameLibrary({
signaling,
serialization: 'binary-min',
movement: { ignoreWorldBounds: true, playerRadius: 20, smoothing: 0.25, extrapolationMs: 140 }
});
शब्दकोश
- SDP: Session Description Protocol; WebRTC के मीडिया/डेटा सत्र पैरामीटर का वर्णन।
- ICE: Interactive Connectivity Establishment; साथियों के बीच नेटवर्क मार्ग (STUN/TURN से) ढूँढता है।
- STUN: सर्वर जो क्लाइंट को उसका सार्वजनिक पता सीखने में मदद करता है; NAT ट्रैवर्सल के लिए।
- TURN: रिले सर्वर जो सीधे P2P संभव न होने पर ट्रैफ़िक अग्रसारित करता है।
- DataChannel: WebRTC द्वि‑दिशात्मक डेटा ट्रांसपोर्ट, गेम संदेशों के लिए।
- LWW: Last‑Writer‑Wins; प्रति‑प्रेषक अनुक्रम पर नवीनतम अपडेट विजेता।
त्वरित शुरुआत
npm install @p2play-js/p2p-game
import { P2PGameLibrary, WebSocketSignaling } from "@p2play-js/p2p-game";
const signaling = new WebSocketSignaling("खिलाड़ीA", "कमरा-42", "wss://आपका-ws.उदाहरण");
const multiP2PGame = new P2PGameLibrary({
signaling,
maxPlayers: 4,
syncStrategy: "delta",
conflictResolution: "timestamp",
});
await multiP2PGame.start();
multiP2PGame.on("playerMove", (id, pos) => {/* रेंडर */});
लिंक
- GitHub: aguiran/p2play-js
- npm: @p2play-js/p2p-game