تعارف
@p2play-js/p2p-game برائوزر پر مبنی P2P (WebRTC) ملٹی پلیئر گیمز بنانے کے لیے ایک ماڈولر TypeScript لائبریری ہے۔ یہ فراہم کرتی ہے حالت کی ہم آہنگی (مکمل/ڈیلٹا)، مطابقت کی حکمت عملیاں (ٹائم اسٹیمپ/اتھورٹیرین)، کم سے کم WebSocket سگنلنگ اڈاپٹر، موومنٹ ہیلپرز، ہوسٹ انتخاب/منتقلی، اور ایک پنگ اوورلے۔
فوری شروعات
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) => {/* رینڈر */});
ڈیموز
-
مکمل منی‑گیم: examples/complete
- بنیادی سگنلنگ ٹیسٹر: examples/basic
فن تعمیر
- لائبریری WebSocket سگنلنگ سرور استعمال کرتی ہے تاکہ کمرے سنبھالے، کھلاڑیوں کے شناختی نمبرز کی فہرست رکھے، اور مخصوص پیئرز تک SDP/ICE پیغامات پہنچائے۔
- پیئرز فل‑میش بناتے ہیں: ہر جوڑی میں وہ پیئر جو
playerId
کے لغوی ترتیب میں چھوٹا ہو، WebRTC آفّر بناتا ہے — ڈبل آفّر ٹکراؤ سے بچاؤ۔ - DataChannel قائم ہو جانے کے بعد گیم کے پیغامات پیئر‑ٹو‑پیئر چلتے ہیں؛ سگنلنگ سرور اب ایپ ٹریفک ریلے نہیں کرتا۔
- ہوسٹ کا انتخاب متعین ہے: سب سے چھوٹا
playerId
ہوسٹ بنتا ہے۔ جب ہوسٹ نکلے تو اگلا چھوٹا منتخب ہوتا ہے اور نیا مکمل اسنیپ شاٹ بھیجتا ہے۔
سگنلنگ ترتیب
فل‑میش ٹوپولوجی
حالت کی ہم آہنگی
- مکمل اسنیپ شاٹس: شامل ہونے/منتقلی پر، اصلاحی ری‑سِنک۔
- ڈیلٹا اپ ڈیٹس: راستہ‑بنیاد ہدفی تبدیلیاں (عملی طور پر عموماً ہائبرڈ طریقہ)۔
مطابقت
- ٹائم اسٹیمپ (طے شدہ): فی‑بھیجنے والے سلسلے پر Last‑Writer‑Wins (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²)
) — چھوٹے کمروں میں ٹھیک؛ بھیڑ میں گرڈ/کواڈ ٹری جیسے ڈھانچے شامل کریں۔
فلو: موومنٹ اسٹیپ
نیٹ ورکنگ کی تفصیل
- بیک پریشر حکمت عملیاں: لبریز چینلز کے لیے مرج/ڈراپس۔
- گنجائش:
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 آبجیکٹ کیز ہیں (ارے انڈیکس معاون نہیں)۔
- ہدفی اپ ڈیٹس کے لیے ساخت کو کم گہرا اور keyed رکھیں (جیسے
objects.chest.42
)؛ گہرے ارریز سے گریز کریں۔
// اچھا: آبجیکٹ میپ
{ 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
گیم پلے APIs
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
پے لوڈ APIs
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; // کمرے میں شامل ہوں اور روستر پائیں
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
: قطار میں پرانا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);
تسلسل
- حکمت عملیاں:
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 دکھائیں؛ واپسی جانچنے کو روستر پر انحصار کریں۔
- مائیگریشن کے بعد ہوسٹ نیا
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 DataChannels سپورٹ کرتے ہیں؛ Safari کو پروڈکشن میں HTTPS/WSS درکار۔
- ادارہ/ہوٹل نیٹ ورکس کے لیے TURN تعینات کریں؛ ریلے پر تاخیر زیادہ۔
مسائل کا حل
WebRTC کنکشن قائم نہیں ہوتا
- مخلوط مواد: یقینی بنائیں صفحہ اور سگنلنگ دونوں HTTPS/WSS ہیں (براؤزر HTTPS صفحے سے WS روکتے ہیں)۔
- TURN کی کمی: ادارہ/ہوٹل نیٹ ورکس میں براہ راست P2P مسدود؛
iceServers
میں TURN اسناد دیں۔ - CORS/فائر وال: سگنلنگ اینڈ پوائنٹ کو اصل (origin) قبول کرنا چاہیے؛ ریورس پراکسی قواعد اور کھلے پورٹس (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; فی‑بھیجنے والا سلسلہ جس میں تازہ ترین اپ ڈیٹ غالب۔
لنکس
- GitHub: aguiran/p2play-js
- npm: @p2play-js/p2p-game