mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 10:35:44 +00:00
Add multiplayer player (#148)
* Add player index * Add second player * Add player index slider * add multiplayer * WIP * Clean up * Add multiplayer play
This commit is contained in:
parent
76515a00ce
commit
55815cb9ef
16 changed files with 210 additions and 25 deletions
|
|
@ -50,7 +50,7 @@ import "C"
|
|||
type naEmulator struct {
|
||||
imageChannel chan<- *image.RGBA
|
||||
audioChannel chan<- []int16
|
||||
inputChannel <-chan int
|
||||
inputChannel <-chan InputEvent
|
||||
|
||||
meta config.EmulatorMeta
|
||||
gamePath string
|
||||
|
|
@ -65,11 +65,16 @@ type naEmulator struct {
|
|||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
type InputEvent struct {
|
||||
KeyState int
|
||||
PlayerIdx int
|
||||
}
|
||||
|
||||
var NAEmulator *naEmulator
|
||||
var outputImg *image.RGBA
|
||||
|
||||
// NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch)
|
||||
func NewNAEmulator(etype string, roomID string, inputChannel <-chan int) (*naEmulator, chan *image.RGBA, chan []int16) {
|
||||
func NewNAEmulator(etype string, roomID string, inputChannel <-chan InputEvent) (*naEmulator, chan *image.RGBA, chan []int16) {
|
||||
meta := config.EmulatorConfig[etype]
|
||||
imageChannel := make(chan *image.RGBA, 30)
|
||||
audioChannel := make(chan []int16, 30)
|
||||
|
|
@ -79,7 +84,7 @@ func NewNAEmulator(etype string, roomID string, inputChannel <-chan int) (*naEmu
|
|||
imageChannel: imageChannel,
|
||||
audioChannel: audioChannel,
|
||||
inputChannel: inputChannel,
|
||||
keys: make([]bool, joypadNumKeys),
|
||||
keys: make([]bool, joypadNumKeys*4),
|
||||
roomID: roomID,
|
||||
done: make(chan struct{}, 1),
|
||||
lock: &sync.Mutex{},
|
||||
|
|
@ -87,7 +92,7 @@ func NewNAEmulator(etype string, roomID string, inputChannel <-chan int) (*naEmu
|
|||
}
|
||||
|
||||
// Init initialize new RetroArch cloud emulator
|
||||
func Init(etype string, roomID string, inputChannel <-chan int) (*naEmulator, chan *image.RGBA, chan []int16) {
|
||||
func Init(etype string, roomID string, inputChannel <-chan InputEvent) (*naEmulator, chan *image.RGBA, chan []int16) {
|
||||
emulator, imageChannel, audioChannel := NewNAEmulator(etype, roomID, inputChannel)
|
||||
// Set to global NAEmulator
|
||||
NAEmulator = emulator
|
||||
|
|
@ -99,7 +104,9 @@ func Init(etype string, roomID string, inputChannel <-chan int) (*naEmulator, ch
|
|||
func (na *naEmulator) listenInput() {
|
||||
// input from javascript follows bitmap. Ex: 00110101
|
||||
// we decode the bitmap and send to channel
|
||||
for inpBitmap := range NAEmulator.inputChannel {
|
||||
for inpEvent := range NAEmulator.inputChannel {
|
||||
inpBitmap := inpEvent.KeyState
|
||||
|
||||
for k := 0; k < len(bindRetroKeys); k++ {
|
||||
key, ok := bindRetroKeys[k]
|
||||
if ok == false {
|
||||
|
|
@ -107,9 +114,9 @@ func (na *naEmulator) listenInput() {
|
|||
}
|
||||
|
||||
if (inpBitmap & 1) == 1 {
|
||||
na.keys[key] = true
|
||||
na.keys[key*4+inpEvent.PlayerIdx] = true
|
||||
} else {
|
||||
na.keys[key] = false
|
||||
na.keys[key*4+inpEvent.PlayerIdx] = false
|
||||
}
|
||||
inpBitmap >>= 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,11 +118,11 @@ func coreInputPoll() {
|
|||
|
||||
//export coreInputState
|
||||
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
|
||||
if port > 0 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
|
||||
if id >= 255 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
|
||||
return 0
|
||||
}
|
||||
|
||||
if id < 255 && NAEmulator.keys[id] {
|
||||
if NAEmulator.keys[id*4+port] {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -123,6 +123,23 @@ func (s *Session) RouteBrowser() {
|
|||
|
||||
return resp
|
||||
})
|
||||
|
||||
browserClient.Receive("playerIdx", func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
log.Println("Overlord: Received update player index request from a browser")
|
||||
log.Println("Overlord: Relay update player index request from a browser to worker")
|
||||
// TODO: Async
|
||||
resp.SessionID = s.ID
|
||||
resp.RoomID = s.RoomID
|
||||
wc, ok := s.handler.workerClients[s.ServerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(
|
||||
resp,
|
||||
)
|
||||
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
|
|||
// zone param is to pick worker in that zone only
|
||||
// if there is no zone param, we can pic
|
||||
userZone := r.URL.Query().Get("zone")
|
||||
|
||||
log.Printf("Get Room %s Zone %s From URL %v", roomID, userZone, r.URL)
|
||||
|
||||
if roomID != "" {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,14 @@ type WebRTC struct {
|
|||
curFPS int
|
||||
|
||||
RoomID string
|
||||
|
||||
// store thing related to game
|
||||
GameMeta GameMeta
|
||||
}
|
||||
|
||||
// Game Meta
|
||||
type GameMeta struct {
|
||||
PlayerIndex int
|
||||
}
|
||||
|
||||
// StartClient start webrtc
|
||||
|
|
|
|||
|
|
@ -136,6 +136,16 @@ func (h *Handler) getRoom(roomID string) *room.Room {
|
|||
return room
|
||||
}
|
||||
|
||||
// getRoom returns session from sessionID
|
||||
func (h *Handler) getSession(sessionID string) *Session {
|
||||
session, ok := h.sessions[sessionID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// detachRoom detach room from Handler
|
||||
func (h *Handler) detachRoom(roomID string) {
|
||||
delete(h.rooms, roomID)
|
||||
|
|
@ -143,7 +153,7 @@ func (h *Handler) detachRoom(roomID string) {
|
|||
|
||||
// createNewRoom creates a new room
|
||||
// Return nil in case of room is existed
|
||||
func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int, videoEncoderType string) *room.Room {
|
||||
func (h *Handler) createNewRoom(gameName string, roomID string, videoEncoderType string) *room.Room {
|
||||
// If the roomID is empty,
|
||||
// or the roomID doesn't have any running sessions (room was closed)
|
||||
// we spawn a new room
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package worker
|
|||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/giongto35/cloud-game/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/pkg/util"
|
||||
|
|
@ -175,6 +176,27 @@ func (h *Handler) RouteOverlord() {
|
|||
return req
|
||||
})
|
||||
|
||||
oClient.Receive(
|
||||
"playerIdx",
|
||||
func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
log.Println("Received an update player index event from overlord")
|
||||
req.ID = "playerIdx"
|
||||
|
||||
room := h.getRoom(resp.RoomID)
|
||||
session := h.getSession(resp.SessionID)
|
||||
idx, err := strconv.Atoi(resp.Data)
|
||||
log.Printf("Got session %v and room %v", session, room)
|
||||
|
||||
if room != nil && session != nil && err == nil {
|
||||
room.UpdatePlayerIndex(session.peerconnection, idx)
|
||||
req.Data = strconv.Itoa(idx)
|
||||
} else {
|
||||
req.Data = "error"
|
||||
}
|
||||
|
||||
return req
|
||||
})
|
||||
|
||||
oClient.Receive(
|
||||
"icecandidate",
|
||||
func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
|
|
@ -224,8 +246,10 @@ func (h *Handler) startGameHandler(gameName, existedRoomID string, playerIndex i
|
|||
// If room is not running
|
||||
if room == nil {
|
||||
log.Println("Got Room from local ", room, " ID: ", existedRoomID)
|
||||
// Create new room
|
||||
room = h.createNewRoom(gameName, existedRoomID, playerIndex, videoEncoderType)
|
||||
// Create new room and update player index
|
||||
room = h.createNewRoom(gameName, existedRoomID, videoEncoderType)
|
||||
room.UpdatePlayerIndex(peerconnection, playerIndex)
|
||||
|
||||
// Wait for done signal from room
|
||||
go func() {
|
||||
<-room.Done
|
||||
|
|
@ -242,7 +266,7 @@ func (h *Handler) startGameHandler(gameName, existedRoomID string, playerIndex i
|
|||
log.Println("Is PC in room", room.IsPCInRoom(peerconnection))
|
||||
if !room.IsPCInRoom(peerconnection) {
|
||||
h.detachPeerConn(peerconnection)
|
||||
room.AddConnectionToRoom(peerconnection, playerIndex)
|
||||
room.AddConnectionToRoom(peerconnection)
|
||||
}
|
||||
|
||||
// Register room to overlord if we are connecting to overlord
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ type Room struct {
|
|||
imageChannel <-chan *image.RGBA
|
||||
// audioChannel is audio stream received from director
|
||||
audioChannel <-chan []int16
|
||||
// inputChannel is input stream from websocket send to room
|
||||
inputChannel chan<- int
|
||||
// inputChannel is input stream send to director. This inputChannel is combined
|
||||
// input from webRTC + connection info (player indexc)
|
||||
inputChannel chan<- nanoarch.InputEvent
|
||||
// State of room
|
||||
IsRunning bool
|
||||
// Done channel is to fire exit event when room is closed
|
||||
|
|
@ -70,7 +71,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
|
|||
gameInfo := gamelist.GetGameInfoFromName(gameName)
|
||||
|
||||
log.Println("Init new room: ", roomID, gameName, gameInfo)
|
||||
inputChannel := make(chan int, 100)
|
||||
inputChannel := make(chan nanoarch.InputEvent, 100)
|
||||
|
||||
room := &Room{
|
||||
ID: roomID,
|
||||
|
|
@ -185,14 +186,19 @@ func (r *Room) isGameOnLocal(savepath string) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
func (r *Room) AddConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) {
|
||||
func (r *Room) AddConnectionToRoom(peerconnection *webrtc.WebRTC) {
|
||||
peerconnection.AttachRoomID(r.ID)
|
||||
r.rtcSessions = append(r.rtcSessions, peerconnection)
|
||||
|
||||
go r.startWebRTCSession(peerconnection, playerIndex)
|
||||
go r.startWebRTCSession(peerconnection)
|
||||
}
|
||||
|
||||
func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int) {
|
||||
func (r *Room) UpdatePlayerIndex(peerconnection *webrtc.WebRTC, playerIndex int) {
|
||||
log.Println("Updated player Index to: ", playerIndex)
|
||||
peerconnection.GameMeta.PlayerIndex = playerIndex
|
||||
}
|
||||
|
||||
func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Warn: Recovered when sent to close inputChannel")
|
||||
|
|
@ -210,9 +216,8 @@ func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int
|
|||
// the first 10 bits belong to player 1
|
||||
// the next 10 belongs to player 2 ...
|
||||
// We standardize and put it to inputChannel (20 bits)
|
||||
input = input << ((uint(playerIndex) - 1) * config.NumKeys)
|
||||
select {
|
||||
case r.inputChannel <- input:
|
||||
case r.inputChannel <- nanoarch.InputEvent{KeyState: input, PlayerIdx: peerconnection.GameMeta.PlayerIndex}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
web/css/main.css
vendored
50
web/css/main.css
vendored
|
|
@ -535,6 +535,56 @@ body {
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
#slider-playeridx {
|
||||
display: block;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
position: absolute;
|
||||
top: 90px;
|
||||
left: 20px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
/* The slider itself */
|
||||
.slider {
|
||||
-webkit-appearance: none; /* Override default CSS styles */
|
||||
appearance: none;
|
||||
width: 100%; /* Full-width */
|
||||
height: 25px; /* Specified height */
|
||||
background: #d3d3d3; /* Grey background */
|
||||
outline: none; /* Remove outline */
|
||||
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
|
||||
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
|
||||
transition: opacity .2s;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Mouse-over effects */
|
||||
.slider:hover {
|
||||
opacity: 1; /* Fully shown on mouse-over */
|
||||
}
|
||||
|
||||
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; /* Override default look */
|
||||
appearance: none;
|
||||
width: 25px; /* Set a specific slider handle width */
|
||||
height: 25px; /* Slider handle height */
|
||||
background: #4CAF50; /* Green background */
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 25px; /* Set a specific slider handle width */
|
||||
height: 25px; /* Slider handle height */
|
||||
background: #4CAF50; /* Green background */
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
|
|
|||
5
web/game.html
vendored
5
web/game.html
vendored
|
|
@ -48,6 +48,11 @@
|
|||
<div id="btn-load" unselectable="on" class="btn big unselectable" value="load">load</div>
|
||||
<div id="btn-save" unselectable="on" class="btn big unselectable" value="save">save</div>
|
||||
<div id="btn-join" unselectable="on" class="btn big unselectable" value="join">play</div>
|
||||
<div id="slider-playeridx" class="slidecontainer">
|
||||
player index
|
||||
<input type="range" min="1" max="4" value="1" class="slider" id="playeridx" onkeydown="event.preventDefault()">
|
||||
</div>
|
||||
|
||||
<div id="btn-quit" unselectable="on" class="btn big unselectable" value="quit">quit</div>
|
||||
<div id="btn-select" unselectable="on" class="btn big unselectable" value="select">Select</div>
|
||||
<div id="btn-start" unselectable="on" class="btn big unselectable" value="start">Start</div>
|
||||
|
|
|
|||
36
web/js/controller.js
vendored
36
web/js/controller.js
vendored
|
|
@ -121,6 +121,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
//const el = document.createElement('textarea');
|
||||
const playeridx = parseInt($('#playeridx').val(), 10) - 1
|
||||
|
||||
log.info('[control] starting game screen');
|
||||
|
||||
setState(app.state.game);
|
||||
|
|
@ -142,7 +145,7 @@
|
|||
// currently it's a game with the index 1
|
||||
// on the server this game is ignored and the actual game will be extracted from the share link
|
||||
// so there's no point in doing this and this' really confusing
|
||||
socket.startGame(gameList.getCurrentGame(), env.isMobileDevice(), room.getId(), 1);
|
||||
socket.startGame(gameList.getCurrentGame(), env.isMobileDevice(), room.getId(), playeridx);
|
||||
|
||||
// clear menu screen
|
||||
input.poll().disable();
|
||||
|
|
@ -183,6 +186,13 @@
|
|||
state.keyRelease(data.key);
|
||||
};
|
||||
|
||||
const updatePlayerIndex = (idx) => {
|
||||
var slider = document.getElementById('playeridx');
|
||||
slider.value = idx + 1;
|
||||
socket.updatePlayerIndex(idx);
|
||||
};
|
||||
|
||||
|
||||
const app = {
|
||||
state: {
|
||||
eden: {
|
||||
|
|
@ -264,8 +274,10 @@
|
|||
input.setKeyState(key, false);
|
||||
|
||||
switch (key) {
|
||||
// nani? why join / copy switch, it's confusing
|
||||
case KEY.JOIN:
|
||||
// nani? why join / copy switch, it's confusing. Me: It's because of the original design to update label only :-s.
|
||||
case KEY.JOIN: // or SHARE
|
||||
// save when click share
|
||||
event.pub(KEY_PRESSED, {key: KEY.SAVE})
|
||||
room.copyToClipboard();
|
||||
popup('Copy link to clipboard!');
|
||||
break;
|
||||
|
|
@ -278,6 +290,22 @@
|
|||
case KEY.FULL:
|
||||
env.display().toggleFullscreen(gameScreen.height() !== window.innerHeight, gameScreen[0]);
|
||||
break;
|
||||
|
||||
// update player index
|
||||
case KEY.PAD1:
|
||||
updatePlayerIndex(0);
|
||||
break;
|
||||
case KEY.PAD2:
|
||||
updatePlayerIndex(1);
|
||||
break;
|
||||
case KEY.PAD3:
|
||||
updatePlayerIndex(2);
|
||||
break;
|
||||
case KEY.PAD4:
|
||||
updatePlayerIndex(3);
|
||||
break;
|
||||
|
||||
// quit
|
||||
case KEY.QUIT:
|
||||
input.poll().disable();
|
||||
|
||||
|
|
@ -302,6 +330,8 @@
|
|||
event.sub(GAME_ROOM_AVAILABLE, onGameRoomAvailable, 2);
|
||||
event.sub(GAME_SAVED, () => popup('Saved'));
|
||||
event.sub(GAME_LOADED, () => popup('Loaded'));
|
||||
event.sub(GAME_PLAYER_IDX, (idx) => popup(parseInt(idx)+1));
|
||||
|
||||
event.sub(MEDIA_STREAM_INITIALIZED, (data) => {
|
||||
rtcp.start(data.stunturn);
|
||||
gameList.set(data.games);
|
||||
|
|
|
|||
1
web/js/event/event.js
vendored
1
web/js/event/event.js
vendored
|
|
@ -60,6 +60,7 @@ const LATENCY_CHECK_REQUESTED = 'latencyCheckRequested';
|
|||
const GAME_ROOM_AVAILABLE = 'gameRoomAvailable';
|
||||
const GAME_SAVED = 'gameSaved';
|
||||
const GAME_LOADED = 'gameLoaded';
|
||||
const GAME_PLAYER_IDX = 'gamePlayerIndex';
|
||||
|
||||
const CONNECTION_READY = 'connectionReady';
|
||||
const CONNECTION_CLOSED = 'connectionClosed';
|
||||
|
|
|
|||
4
web/js/input/keyboard.js
vendored
4
web/js/input/keyboard.js
vendored
|
|
@ -21,6 +21,10 @@ const keyboard = (() => {
|
|||
83: KEY.SAVE, // s
|
||||
87: KEY.JOIN, // w
|
||||
65: KEY.LOAD, // a
|
||||
49: KEY.PAD1, // 1
|
||||
50: KEY.PAD2, // 2
|
||||
51: KEY.PAD3, // 3
|
||||
52: KEY.PAD4, // 4
|
||||
70: KEY.FULL, // f
|
||||
72: KEY.HELP, // h
|
||||
};
|
||||
|
|
|
|||
4
web/js/input/keys.js
vendored
4
web/js/input/keys.js
vendored
|
|
@ -16,5 +16,9 @@ const KEY = (() => {
|
|||
DOWN: 'down',
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
PAD1: 'pad1',
|
||||
PAD2: 'pad2',
|
||||
PAD3: 'pad3',
|
||||
PAD4: 'pad4',
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
15
web/js/input/touch.js
vendored
15
web/js/input/touch.js
vendored
|
|
@ -20,6 +20,7 @@ const touch = (() => {
|
|||
|
||||
const window_ = $(window);
|
||||
const buttons = $(".btn");
|
||||
const playerSlider = $("#playeridx")
|
||||
const dpad = $(".dpad");
|
||||
|
||||
function resetVpadState() {
|
||||
|
|
@ -124,6 +125,15 @@ const touch = (() => {
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
Player index slider
|
||||
*/
|
||||
|
||||
function handlePlayerSlider() {
|
||||
socket.updatePlayerIndex($(this).val() - 1);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Touch menu
|
||||
*/
|
||||
|
|
@ -226,6 +236,11 @@ const touch = (() => {
|
|||
vpadHolder.on('touchstart', handleVpadJoystickDown);
|
||||
vpadHolder.on('touchend', handleVpadJoystickUp);
|
||||
|
||||
// touch/mouse events for player slider.
|
||||
playerSlider.on('oninput', handlePlayerSlider);
|
||||
playerSlider.on('onchange', handlePlayerSlider);
|
||||
playerSlider.on('mouseup', handlePlayerSlider);
|
||||
|
||||
// Bind events for menu
|
||||
// TODO change this flow
|
||||
event.pub(MENU_HANDLER_ATTACHED, {event: 'mousedown', handler: handleMenuDown});
|
||||
|
|
|
|||
6
web/js/network/socket.js
vendored
6
web/js/network/socket.js
vendored
|
|
@ -25,6 +25,7 @@ const socket = (() => {
|
|||
};
|
||||
conn.onerror = error => log.error(`[ws] ${error}`);
|
||||
conn.onclose = () => log.info('[ws] closed');
|
||||
// Message received from server
|
||||
conn.onmessage = response => {
|
||||
const data = JSON.parse(response.data);
|
||||
const message = data.id;
|
||||
|
|
@ -59,6 +60,9 @@ const socket = (() => {
|
|||
case 'load':
|
||||
event.pub(GAME_LOADED);
|
||||
break;
|
||||
case 'playerIdx':
|
||||
event.pub(GAME_PLAYER_IDX, data.data);
|
||||
break;
|
||||
case 'checkLatency':
|
||||
curPacketId = data.packet_id;
|
||||
const addresses = data.data.split(',');
|
||||
|
|
@ -77,6 +81,7 @@ const socket = (() => {
|
|||
});
|
||||
const saveGame = () => send({"id": "save", "data": ""});
|
||||
const loadGame = () => send({"id": "load", "data": ""});
|
||||
const updatePlayerIndex = (idx) => send({"id": "playerIdx", "data": idx.toString()});
|
||||
const startGame = (gameName, isMobile, roomId, playerIndex) => send({
|
||||
"id": "start",
|
||||
"data": JSON.stringify({
|
||||
|
|
@ -94,6 +99,7 @@ const socket = (() => {
|
|||
latency: latency,
|
||||
saveGame: saveGame,
|
||||
loadGame: loadGame,
|
||||
updatePlayerIndex: updatePlayerIndex,
|
||||
startGame: startGame,
|
||||
quitGame: quitGame
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue