डॉक्स
🌐 भाषा:
स्थानीय स्टेटिक HTML @p2play-js/p2p-game थीम
⚠️ चेतावनी: यह दस्तावेज़ीकरण अंग्रेजी से स्वचालित रूप से अनुवादित है। त्रुटियां हो सकती हैं। मूल अंग्रेजी संस्करण

परिचय

@p2play-js/p2p-game ब्राउज़र-आधारित P2P (WebRTC) मल्टीप्लेयर गेम बनाने के लिए एक मॉड्यूलर TypeScript लाइब्रेरी है। यह प्रदान करती है राज्य सिंक्रोनाइज़ेशन (पूर्ण/डेल्टा), संगति रणनीतियां (टाइमस्टैम्प/प्राधिकरण), एक न्यूनतम WebSocket सिग्नलिंग एडाप्टर, मूवमेंट हेल्पर्स, होस्ट चुनाव/माइग्रेशन, और एक पिंग ओवरले।

डेमो

आर्किटेक्चर

  • लाइब्रेरी WebSocket सिग्नलिंग सर्वर का उपयोग कमरे प्रबंधन, प्लेयर आईडी सूची और SDP/ICE संदेशों को लक्षित साथियों तक रूट करने के लिए करती है।
  • पीयर पूर्ण‑जाल (full‑mesh) बनाते हैं: हर जोड़ी में, जिसका playerId शब्दकोश क्रम में छोटा है, वही WebRTC offer बनाता है — डबल ऑफर टकराव से बचाव।
  • DataChannel बनने के बाद गेम संदेश सीधे पीयर‑टू‑पीयर बहते हैं; सिग्नलिंग सर्वर अब एप्लीकेशन ट्रैफिक रिले नहीं करता।
  • होस्ट चुनाव नियतात्मक है: सबसे छोटा playerId होस्ट बनता है। होस्ट के निकलने पर अगला छोटा चुना जाता है और वह नया पूर्ण स्नैपशॉट भेजता है।
सिग्नलिंग क्या है?
ब्राउज़र WebRTC कनेक्शन खोले बिना पहले मेटाडेटा (SDP offers/answers, ICE candidates) को किसी बाह्य चैनल से नहीं बदल सकते। सिग्नलिंग सर्वर केवल यही आदान‑प्रदान करता है और कमरे की सूची रखता है; DataChannel खुलने के बाद यह गेमप्ले ट्रैफिक नहीं भेजता।

सिग्नलिंग सीक्वेंस

sequenceDiagram participant A as क्लाइंट A participant S as WS सिग्नलिंग participant B as क्लाइंट B A->>S: register {roomId, from, announce} B->>S: register {roomId, from, announce} S-->>A: sys: roster [A,B] S-->>B: sys: roster [A,B] note over A,B: सबसे छोटा playerId offer शुरू करता है A->>S: kind: desc (offer), to: B S->>B: kind: desc (offer) from A B->>S: kind: desc (answer), to: A S->>A: kind: desc (answer) from B A->>S: kind: ice, to: B B->>S: kind: ice, to: A note over A,B: DataChannel खुलता है → गेमप्ले P2P हो जाता है

फुल‑मेश टोपोलॉजी

graph LR A[खिलाड़ी A] --- B[खिलाड़ी B] A --- C[खिलाड़ी C] B --- C classDef host fill:#2b79c2,stroke:#2a3150,color:#fff; class A host;

राज्य सिंक्रोनाइज़ेशन

  • पूर्ण स्नैपशॉट: जुड़ने/माइग्रेशन पर, सुधारात्मक री‑सिंक।
  • डेल्टा अपडेट: मार्ग‑विशिष्ट लक्षित परिवर्तन (व्यवहार में अक्सर मिश्रित दृष्टिकोण)।
फुल बनाम डेल्टा
पूर्ण स्नैपशॉट सरल और भरोसेमंद हैं पर भारी; डेल्टा छोटे और कुशल हैं पर स्थिर स्टेट स्कीमा चाहिए। अभ्यास में, ज़्यादातर समय डेल्टा भेजें, और साथियों के जुड़ने या होस्ट माइग्रेशन के बाद पूर्ण स्नैपशॉट।

संगति

  • टाइमस्टैम्प (डिफ़ॉल्ट): प्रति‑प्रेषक अनुक्रम पर Last‑Writer‑Wins (LWW)।
  • प्राधिकृत (authoritative): केवल अधिकार (होस्ट/नियत आईडी) से क्रियाएं स्वीकारें।
गेम्स में संगति
टाइमस्टैम्प मोड में, प्रेषक‑प्रति नवीनतम क्रिया LWW नियम से स्वीकारें: जिसका 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²)) — छोटे कमरों में ठीक, भीड़ में ग्रिड/क्वाडट्री जैसे विभाजन जोड़ें।

फ़्लो: मूवमेंट स्टेप

flowchart TD A[नवीनतम ज्ञात स्थिति] --> B{क्या नया नेटवर्क अपडेट?} B -- हाँ --> C[टाइमस्टैम्प रीसेट; इंटरपोलेशन लागू] B -- नहीं --> D{क्या एक्सट्रापोलेशन बजट बचा है?} D -- हाँ --> E[अंतिम वेग * smoothing से प्रोजेक्ट] D -- नहीं --> F[स्थिति बनाए रखें] C --> G{विश्व सीमाएँ?} E --> G G -- सीमित करें --> H[सीमाएँ लागू] G -- ओपन वर्ल्ड --> I[सीमाएँ छोड़ें] H --> J[टक्कर हल करें] I --> J J --> K[रेंडर]

टक्कर समाधान (वृत्त/गोला)

flowchart TD A(प्रारम्भ) --> B(A और B के बीच दूरी निकालें) B --> C{क्या दूरी 2*r से कम है?} C -- नहीं --> Z(ओवरलैप नहीं) C -- हाँ --> D(A से B तक नार्मलाइज़्ड वेक्टर निकालें) D --> E(ओवरलैप = 2*r - दूरी) E --> F(A को -नॉर्मल * ओवरलैप/2 से पीछे करें) E --> G(B को +नॉर्मल * ओवरलैप/2 से आगे करें) F --> H(समाप्त) G --> H

नेटवर्किंग विवरण

  • बैकप्रेशर रणनीतियाँ: संतृप्त चैनलों पर मर्ज/ड्रॉप।
  • क्षमता: maxPlayers लागू करना + maxCapacityReached इवेंट।
  • STUN/TURN: कड़े नेटवर्क के लिए TURN; सिग्नलिंग के लिए WSS।
NAT और TURN
कई एंटरप्राइज़/होटल नेटवर्क सीधे P2P को ब्लॉक करते हैं। TURN सर्वर ट्रैफ़िक रिले कर कनेक्शन संभव बनाता है, पर विलंब/बैंडविड्थ की कीमत पर। प्रोडक्शन में विश्वसनीयता हेतु TURN क्रेडेंशियल दें।

बैकप्रेशर

बैकप्रेशर DataChannel को ओवरलोड से बचाता है। जब RTCDataChannel.bufferedAmount थ्रेसहोल्ड से ऊपर बढ़े, तो अस्थायी रूप से भेजना रोकें, कम‑मूल्य संदेश छोड़ें, या कई अपडेट को नवीनतम में संक्षेपित करें।

कैसे काम करता है
हर send() bufferedAmount बढ़ाता है जब तक ब्राउज़र नेटवर्क पर फ़्लश न करे। यदि भेजना डिलीवरी से तेज़ रहा तो विलंब बढ़ेगा और ऐप अटकने लगेगा।

रणनीतियाँ

  • off: कोई सुरक्षा नहीं — बहुत छोटे/अनियमित संदेशों हेतु।
  • drop-moves: थ्रेसहोल्ड से ऊपर नए move छोड़ें।
  • coalesce-moves: प्रत्येक पीयर का केवल नवीनतम move रखें।
const multiP2PGame = new P2PGameLibrary({
    signaling,
    backpressure: { strategy: 'coalesce-moves', thresholdBytes: 256 * 1024 }
  });
  
अनुशंसित थ्रेसहोल्ड
256–512 KB से शुरू करें; बार‑बार सीमा पार हो तो आवृत्ति/आकार घटाएँ (डेल्टा, binary‑min, वेक्टर क्वांटाइज़)।

इवेंट्स और 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 }

देखें examples/server/ws-server.mjs

PeerManager (आंतरिक)

  • हर पीयर के लिए एक RTCPeerConnection और एक RTCDataChannel बनाए रखता है तथा आवश्यक कॉलबैक वायर करता है।
  • हर जोड़ी में छोटा playerId ऑफर बनाकर कनेक्शन शुरू करता है; दूसरा उत्तर देता है — एक‑साथ ऑफर से बचाव।
  • peerJoin, peerLeave, hostChange, ping इवेंट्स निकालता है; डिकोडेड नेटवर्क संदेशों को netMessage के रूप में अग्रसारित करता है।
  • बैकप्रेशर:
    • off: चैनल खुला हो तो हमेशा भेजें।
    • drop-moves: bufferedAmount सीमा पार होने पर नए move छोड़ें।
    • coalesce-moves: पुराने queued move को नवीनतम से बदलें।
  • क्षमता: 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);
चार्ट पढ़ना
हर रंगीन रेखा एक पीयर है। सपाट‑कम मान अच्छे; आरी‑दाँत या अचानक उछाल भीड़/रिले (TURN) का संकेत। कोई पीयर स्थायी रूप से उच्च हो तो भूमिकाएँ पुनः संतुलित करें (उदा., उसे होस्ट न बनाएँ)।

सीरियलाइज़ेशन

  • रणनीतियाँ: 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) => {/* रेंडर */});