Pong online multiplayer - phaser js + WebRTC

Odpowiedz Nowy wątek
2018-12-10 21:58
lhp
4

Hej,

pong

moja pierwsza prosta gra sieciowa oparta na komunikacji WebRTC,
można pograć z żywym przeciwnikiem pod warunkiem, że obie przeglądarki wspierają WebRTC.
Gra korzysta z biblioteki phaser.js
Zasady proste, aż do bólu :)

link do gry - www.pong-multiplayer.eu


Pozostało 580 znaków

2018-12-11 01:51
1

Fajnie byłoby przeczytać jakiś artykuł w stylu "post mortem" (Taka refleksja jak na Gamasutra.com), który by analizował co poszło dobrze w developerce, co poszło źle, z czym były problemy, czy WebRTC jest już gotowe do używania itp.

To, przynajmniej dla mnie, byłoby bardziej ciekawe niż samo granie w grę typu pong.


((0b10*0b11*(0b10**0b101-0b10)**0b10+0b110)**0b10+(100-1)**0b10+0x10-1).toString(0b10**0b101+0b100);

Pozostało 580 znaków

2018-12-11 10:18
lhp
2

Ok, więc do rzeczy :)

Po pierwsze nie jest trudno się w wgryźć w temat, bo WebRTC jest dobrze opisana i udokumentowana:
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API

Na dodatek z implementacją WebRTC na potrzeby takiej małej aplikacji nie ma większych problemów.
Mamy tylko jeden strumień (dane) pomiędzy użytkownikami, którego stan obserwujemy.
Co innego w bardziej złożonych aplikacjach gdzie mamy strumienie video, audio, danych, połączenia punkt<->wielopunkt, ... ale to nie ten przypadek.

Wiadomo trudnością w aplikacjach sieciowych jest potrzeba reagowania na potencjalne problemy z połączeniem, a te mogą się pojawić spontanicznie.
WebRTC jest w stanie się "naprawić" tzn wejść w stan "disconnected" i po rozwiązaniu problemów wrócić do stanu "connected".
Ciężko jest jednak takie zachowanie wymusić, w sieci nic nie znalazłem jak symulować problemy z połączeniem via webrtc,
a odłączanie co chwila laptopa od wifi jest męczące, niepewne i w ogóle do d... :)

Dlatego jeśli strumień danych zostanie nawet chwilowo "uszkodzony", traktuje to jako całkowitą utratę połączenia.
Zadbałem o czytelne informowanie uczestników gry o tym fakcie oraz o możliwość ponownego dołączenia do tej samej rozgrywki.
Gdyby udało mi się znaleźć odpowiednie narzędzie do symulowania problemów z połączeniami to chętnie bym dodał aplikacji więcej "sprytu" :)
Może ktoś z Was mógłby coś podpowiedzieć w tej materii.

poza tym korzystanie z webrtc wymaga:

  • postawienia serwera STUN/TURN na jakiejś maszynie w sieci, do translacji NAT
  • posiadanie kanału komunikacji do początkowej negocjacji połączenia WebRTC, np serwera websocket

Minusem jest na pewno brak wsparcia przez wszystkie przeglądarki, działa bez problemu w FF, CH, O.

Trudno też testować całość. Testowanie ma sens jeśli uruchamia się aplikacje z dwóch różnych sieci.
W sieci lokalnej nie ma translacji NAT, połączenia jest zestawiane błyskawicznie, a komunikacja praktycznie bez opóźnień.
W warunkach domowych mogłem potestować jedynie na szybkim połączeniu stacjonarnym (100 / 10 MB), mobilnym (1 / 0.25 MB) i komórkowym lte.
Działało, a opóźnienia były na tyle małe, że nie psuły przyjemności z gry :)

Tyle w skrócie z moich przemyśleń :D


edytowany 1x, ostatnio: lhp, 2018-12-11 10:21
co do testowania opóźnień, to Chrome w Dev Toolsach, w zakładce Network ma opcję symulacji wolnego połączenia. - LukeJL 2018-12-12 03:54
niestety to narzędzie najwyraźniej nie spowalnia samej transmisji danych połączeniem zestawionym via webrtc, nawet ekstremalnie niskie ustawienia 1kb/1kb/10000ms "nie szkodzą" grze - lhp 2018-12-12 10:15

Pozostało 580 znaków

2018-12-11 10:40
0

@lhp: wrzucales gdzies na guthuba/bitbucketa ?

Pozostało 580 znaków

2018-12-11 13:02
lhp
2
WhiteLightning napisał(a):

@lhp: wrzucales gdzies na guthuba/bitbucketa ?

nie upubliczniałem całego kodu, czuje w nim potencjał komercyjny ;)

ale klasę opakowującą RTC mogę zapodać:

import {Observable, Subscriber, Subject, ReplaySubject, Subscription } from 'rxjs';

export const enum WebRTCPeerConnectionStateTypes {
    Connecting = "[RTC] Connecting",
    Connected = "[RTC] Connected",
//    Exit = "[RTC] Exit",
    Failed = "[RTC] Failed",
//    Closed = "[RTC] Closed"
}

interface DataChannel {
    readyState:string; //experimental
    send:(data:any) => void;
    onopen:(ev:Event) => void; //experimental
    onmessage:(ev:Event) => void; //experimental
    onclose:(ev:Event) => void; //experimental
    close:() => void;
}

export class WebRTCPeerConnectionState {
    constructor(
        public state: WebRTCPeerConnectionStateTypes,
        public failedReason: string = null
    ) {}
}

export class WebRTCPeerConnection {

    private rtcCfg:object = {
        iceServers:[
            {urls:["stun:xx.xx.xxx.xx:xxxx"]},
            {urls:["turn:xx.xx.xxx.xx:xxxx"], username:"xxxxxx", credential:"xxxxxxxx"}
        ]
    };

    private initialized:boolean = false;
    private webRTCSupported:boolean;

    private rtcPeerConnection:RTCPeerConnection;
    private connectionState:WebRTCPeerConnectionState;
    private dataChannel:DataChannel;

    private stateObserver:Subscriber<WebRTCPeerConnectionState>;
    private state$:Subject<WebRTCPeerConnectionState>;
    private stateSub:Subscription;

    private dataObserver:Subscriber<object>;
    private data$:Subject<object>;
    private dataSub:Subscription;

    constructor(
        private connectionMaster:boolean,
        private sendMessageToRemote:Function,
        private debugMode:boolean
    ) {

        this.state$ = new ReplaySubject();
        this.stateSub = (new Observable(observer => {
            this.stateObserver = observer;
        })).subscribe(this.state$);

        this.data$ = new Subject();
        this.dataSub = (new Observable(observer => {
            this.dataObserver = observer;
        })).subscribe(this.data$);

    }

    ini() {

        if(this.initialized) {
            throw new Error("Instance already initialized.");
        }

        this.initialized = true;

        this.setStateConnecting();

        if (WebRTCPeerConnection.browserSupportWebRTC()) {
            this.webRTCSupported = true;
            this.iniWebRTC();
        } else {
            this.webRTCSupported = false;
            this.setStateFailed("WebRTC not supported.");
        }

    }

    private iniWebRTC() {

        this.rtcPeerConnection = new RTCPeerConnection(this.rtcCfg);   //experimental

        this.rtcPeerConnection.oniceconnectionstatechange = 
            this.onIceConnectionStateChange.bind(this);  //experimental

        this.rtcPeerConnection.onicecandidate = 
            this.onIceCandidate.bind(this); //experimental

        if(this.connectionMaster) {

            this.dataChannel = this.rtcPeerConnection["createDataChannel"]("dataChannel");  //experimental
            this.dataChannel.onopen //experimental
                = (ev:Event) => {
                    this.setStateConnected();
                    this.logInternalStates();
                }
            this.dataChannel.onmessage //experimental
                = (ev:Event) => {
//                    console.log("channel data message", ev["data"]);
                    try { this.dataObserver.next(JSON.parse(ev["data"])); } catch (ex) {}
                };    
            this.dataChannel.onclose //experimental
                = (ev:Event) => {
                    this.setStateFailed("Data channel suddenly closed");
                };    

            this.createOffer();

        } else {

            this.rtcPeerConnection["ondatachannel"] //experimental
                = (ev:Event) => {
                    this.dataChannel = ev["channel"];
                    this.dataChannel.onopen //experimental
                        = (ev:Event) => {
                            this.setStateConnected();
                            this.logInternalStates();
                        }
                    this.dataChannel.onmessage //experimental
                        = (ev:Event) => {
//                            console.log("channel data message", ev["data"]);
                            try { this.dataObserver.next(JSON.parse(ev["data"])); } catch (ex) {}
                        };    
                    this.dataChannel.onclose //experimental
                        = (ev:Event) => {
                            this.setStateFailed("Data channel suddenly closed");
                        };   
                }

        }

        this.logInternalStates();

    }

    destroy() {

        if(!this.initialized) {
            throw new Error("Instance not initialized yet. Call method ini().");
        }

        if(this.rtcPeerConnection) {
            this.rtcPeerConnection.close();
            this.rtcPeerConnection.oniceconnectionstatechange = null;
            this.rtcPeerConnection.onicecandidate = null;
            this.rtcPeerConnection["ondatachannel"] = null;
            this.rtcPeerConnection = null;
        }

        if(this.dataChannel) {
            this.dataChannel.onopen = null;
            this.dataChannel.onmessage = null;
            this.dataChannel.onclose = null;
            this.dataChannel = null;
        }

        this.stateSub.unsubscribe();
        this.state$.unsubscribe();
        this.dataSub.unsubscribe();
        this.data$.unsubscribe();

        this.logStatus("!!!WRAPPER TOTAL DESTROYED!!!");

    }

    subscribeState(observer:{next:(state:WebRTCPeerConnectionState)=>void}) {

        if(this.initialized) {
            throw new Error("Instance already initialized.");
        }

        return this.state$.subscribe(observer);

    }

    //wysyla dane do zdalnego klienta
    sendData(data:string) {
        if(this.connectionState.state !== WebRTCPeerConnectionStateTypes.Connected) {
            throw new Error("Data channel is unavailable.");
        }

        try {
            "open" === this.dataChannel.readyState && this.dataChannel.send(data);

        //for chrome bug fix, mimo ze readyState ma wartosc open send rzuca 
        //wyjatek DOMException "Failed to execute 'send' on 'RTCDataChannel'    
        } catch (ex) {} 

    }

    //subskrybuje dane od zdalnego klienta
    subscribeData(observer:{next:(data:object)=>void}) {
        return this.data$.subscribe(observer);
    }

    setReceivedMessage(msg:string) {

        if(!this.initialized) {
            throw new Error("Instance not initialized yet. Call method ini().");
        }

        if(this.webRTCSupported) {

            let data:object;
            try { data = JSON.parse(msg); } catch (ex) {}

            if(data && "offer" === data["type"] && data["sdp"]) {

                this.logStatus("received offer");

//                this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer", sdp:"foo"})) //SYMULOWANE WYWOLANIE BLEDU
                this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(data))
                    .then(
                        () => this.createAnswer(),
                        (err:object) => {
                            this.setStateFailed(err.toString())
                        }
                    );

            } else if(data && "answer" === data["type"] && data["sdp"]) {        

                this.logStatus("received answer");

//                this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription({type:"offer", sdp:"foo"})) //SYMULOWANE WYWOLANIE BLEDU
                this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(data))
                    .then(
                        () => {},
                        (err:object) => {
                            this.setStateFailed(err.toString())
                        }
                    );

            } else if(data && data["candidate"]) {

                this.logStatus("received candidate");

//                this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate({candidate:null})) //SYMULOWANE WYWOLANIE BLEDU
                this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(data))
                    .catch(
                        (err:object) => {
                            this.setStateFailed(err.toString())
                        }
                    );    

            } else {
                this.logStatus("received unknow data");
            }

        }

    }

    manualInterruptConnection(sig:number) {
        this.logStatus(`Requested manual interrupt connection for sig=${sig}.`);
        switch(sig) {
            case 1:
                if(!this.rtcPeerConnection) throw new Error("Rtc peer conneciton not exists.");
                this.rtcPeerConnection.close();
                break;
            case 2:
                if(!this.dataChannel) throw new Error("Data channel not exists.");
                this.dataChannel.close();
                break;
        }
    }

    static browserSupportWebRTC():boolean {

//        return false; //SYMULOWANE WYWOLANIE BLEDU

        if(!!~window.navigator.userAgent.indexOf("Edge")) return false;

        var isWebRTCSupported:boolean = !!(
            navigator["webkitGetUserMedia"] ||
            navigator["mozGetUserMedia"] ||
            navigator["msGetUserMedia"] ||
            window["RTCPeerConnection"]
        );

        return isWebRTCSupported;
    }

    private createOffer() {

//        this.rtcPeerConnection.createOffer(null, null, {}).then( //SYMULOWANE WYWOLANIE BLEDU
        this.rtcPeerConnection.createOffer().then(
            (offer: RTCSessionDescription) => {
                this.rtcPeerConnection.setLocalDescription(offer); //experimental
                this.sendMessageToRemote(JSON.stringify(offer));
            },
            (err:object) => {
                this.setStateFailed(err.toString())
            }
        );
    }

    private createAnswer() {

//        this.rtcPeerConnection.createAnswer(null, null).then( //SYMULOWANE WYWOLANIE BLEDU
        this.rtcPeerConnection.createAnswer().then( //experimental
            (answer:RTCSessionDescription) => {
                this.rtcPeerConnection.setLocalDescription(answer); //experimental
                this.sendMessageToRemote(JSON.stringify(answer));
            },
            (err:object) => {
                this.setStateFailed(err.toString())
            }
        );
    }

    private onIceCandidate(ev:RTCPeerConnectionIceEvent) {
        if(ev.candidate) {
            this.sendMessageToRemote(JSON.stringify(ev.candidate.toJSON()));
        }
    }

    private onIceConnectionStateChange(ev:Event) {

        switch(this.rtcPeerConnection.iceConnectionState) { //experimental

            /*
             * nie uzywamy tych stanow, dlatego sa zakomentowane 
             */
//            case "new":
//            case "checking":
//                this.setStateConnecting();
//                break;
//            
//            case "connected":
//            case "completed":
//                this.setStateConnected();
//                break;

            case "failed":
                this.setStateFailed("ice connection state " + this.rtcPeerConnection.iceConnectionState);
                break; 

//            case "disconnected":
//            case "closed":
//                this.setStateFailed("ice connection state " + this.rtcPeerConnection.iceConnectionState);
//                break; 

        }

        this.logInternalStates();

    }

    private setStateConnecting() {
        this.connectionState = new WebRTCPeerConnectionState(
            WebRTCPeerConnectionStateTypes.Connecting
        );
        this.stateObserver.next(this.connectionState);
    }

    private setStateConnected() {
        this.connectionState = new WebRTCPeerConnectionState(
            WebRTCPeerConnectionStateTypes.Connected
        );
        this.stateObserver.next(this.connectionState);
    }

    private setStateFailed(reason:string) {
        this.connectionState = new WebRTCPeerConnectionState(
            WebRTCPeerConnectionStateTypes.Failed,
            reason
        );
        this.stateObserver.next(this.connectionState);
        this.logFailed(reason);
    }

    private logInternalStates() {
        this.logStatus(
            "SIGNALING_STATE:" + (this.rtcPeerConnection ? this.rtcPeerConnection.signalingState : "rtcPeerConnection not exists") + " " +
            "ICE_CONNECTION_STATE:" + (this.rtcPeerConnection ? this.rtcPeerConnection.iceConnectionState : "rtcPeerConnection not exists") + " " +
            "MY_STATE.STATE:" + this.connectionState.state + " " +
            "MY_STATE.FAILED_MSG:" + this.connectionState.failedReason + " " +
            "CHANNEL_DATA:" + (this.dataChannel ? this.dataChannel.readyState : "dataChannel not exists")
        );
    }

    private logStatus(status:string) {
        this.debugMode && console.log("WebRTC LOG STATUS >>", status);
    }

    private logFailed(reason:any) {
        this.debugMode && console.log("WebRTC LOG FAILED >>", reason);
    }

}

edytowany 1x, ostatnio: lhp, 2018-12-11 13:02

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0