mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-07-20 16:54:25 +00:00
Merge branch 'master' of github.com:giongto35/cloud-game
This commit is contained in:
commit
d9a91c4876
11 changed files with 80 additions and 68 deletions
6
Makefile
vendored
6
Makefile
vendored
|
|
@ -73,13 +73,11 @@ dev.build-local:
|
|||
|
||||
dev.run: dev.build-local
|
||||
./bin/overlord --v=5 &
|
||||
./bin/overworker --overlordhost ws://localhost:8000/wso
|
||||
|
||||
|
||||
./bin/overworker --overlordhost localhost:8000
|
||||
|
||||
dev.run-docker:
|
||||
docker build . -t cloud-game-local
|
||||
docker stop cloud-game-local || true
|
||||
docker rm cloud-game-local || true
|
||||
# Overlord and worker should be run separately.
|
||||
docker run --privileged -v $PWD/games:/cloud-game/games -d --name cloud-game-local -p 8000:8000 -p 9000:9000 cloud-game-local bash -c "overlord --v=5 & overworker --overlordhost ws://localhost:8000/wso"
|
||||
docker run --privileged -v $PWD/games:/cloud-game/games -d --name cloud-game-local -p 8000:8000 -p 9000:9000 cloud-game-local bash -c "overlord --v=5 & overworker --overlordhost localhost:8000"
|
||||
|
|
|
|||
2
README.md
vendored
2
README.md
vendored
|
|
@ -25,7 +25,7 @@ Crowd play: **[Pokemon Emerald](http://cloudretro.io/?id=652e45d78d2b91cd%7CPoke
|
|||
2. Cross-platform compatibility: The game is run on web browser, the most universal built-in app. No console, plugin, external app or devices are needed. Chrome with the latest version and fully WebRTC support is recommended for the game.
|
||||
3. Emulator agnostic: The game can be played directly without any extra effort to set up the gaming emulator or platform.
|
||||
4. Collaborate gameplay: Follow the idea of crowdplay([TwitchPlaysPokemon](https://en.wikipedia.org/wiki/Twitch_Plays_Pok%C3%A9mon)), multiple players can play the same game together by addressing the same deeplink. The game experience is powered by cloud-gaming, so the game is much smoother. [Check CrowdPlay section](#crowd-play-play-game-together)
|
||||
5. Vertically scaled: The infrastructure is designed to be able to scale under high traffic by adding more instances.
|
||||
5. Horizontally scaled: The infrastructure is designed to be able to scale under high traffic by adding more instances.
|
||||
6. Cloud storage: Game state is storing on online storage, so you can come back and continue playing your incomplete game later.
|
||||
7. Online multiplayer: 2nd, 3rd player can join the same game in the same room.
|
||||
|
||||
|
|
|
|||
BIN
docs/img/overlord.png
vendored
BIN
docs/img/overlord.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 53 KiB |
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const defaultoverlord = "ws://localhost:9000/wso"
|
||||
const DefaultSTUNTURN = `[{"urls":"stun:stun-turn.webgame2d.com:3478"},{"urls":"turn:stun-turn.webgame2d.com:3478","username":"root","credential":"root"}]`
|
||||
const CODEC_VP8 = "VP8"
|
||||
const CODEC_H264 = "H264"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type Config struct {
|
|||
EnableAspectRatio bool
|
||||
Width int
|
||||
Height int
|
||||
Zone string
|
||||
|
||||
MonitoringConfig monitoring.ServerMonitoringConfig
|
||||
}
|
||||
|
|
@ -21,11 +22,12 @@ type Config struct {
|
|||
func NewDefaultConfig() Config {
|
||||
return Config{
|
||||
Port: 8800,
|
||||
OverlordAddress: "ws://localhost:8000/wso",
|
||||
OverlordAddress: "localhost:8000",
|
||||
Scale: 1,
|
||||
EnableAspectRatio: false,
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
Zone: "",
|
||||
MonitoringConfig: monitoring.ServerMonitoringConfig{
|
||||
Port: 6601,
|
||||
URLPrefix: "/worker",
|
||||
|
|
@ -37,6 +39,7 @@ func NewDefaultConfig() Config {
|
|||
func (c *Config) AddFlags(fs *pflag.FlagSet) *Config {
|
||||
fs.IntVarP(&c.Port, "port", "", 8800, "OverWorker server port")
|
||||
fs.StringVarP(&c.OverlordAddress, "overlordhost", "", c.OverlordAddress, "OverWorker URL to connect")
|
||||
fs.StringVarP(&c.Zone, "zone", "z", c.Zone, "Zone of the worker")
|
||||
|
||||
fs.IntVarP(&c.Scale, "scale", "s", c.Scale, "Set output viewport scale factor")
|
||||
fs.BoolVarP(&c.EnableAspectRatio, "ar", "", c.EnableAspectRatio, "Enable Aspect Ratio")
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"html/template"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
|
@ -25,9 +24,9 @@ const (
|
|||
|
||||
type Server struct {
|
||||
cfg Config
|
||||
// roomToServer map roomID to workerID
|
||||
roomToServer map[string]string
|
||||
// workerClients are the map serverID to worker Client
|
||||
// roomToWorker map roomID to workerID
|
||||
roomToWorker map[string]string
|
||||
// workerClients are the map workerID to worker Client
|
||||
workerClients map[string]*WorkerClient
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +39,7 @@ func NewServer(cfg Config) *Server {
|
|||
// Mapping serverID to client
|
||||
workerClients: map[string]*WorkerClient{},
|
||||
// Mapping roomID to server
|
||||
roomToServer: map[string]string{},
|
||||
roomToWorker: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,8 +79,11 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Register to workersClients map the client connection
|
||||
address := util.GetRemoteAddress(c)
|
||||
fmt.Println("Is public", util.IsPublicIP(address))
|
||||
fmt.Println("Mode: ", *config.Mode, config.ProdEnv)
|
||||
// Zone of the worker
|
||||
zone := r.URL.Query().Get("zone")
|
||||
|
||||
fmt.Printf("Is public: %v zone: %v\n", util.IsPublicIP(address), zone)
|
||||
|
||||
if !util.IsPublicIP(address) && *config.Mode == config.ProdEnv {
|
||||
// Don't accept private IP for worker's address in prod mode
|
||||
// However, if the worker in the same host with overlord, we can get public IP of worker
|
||||
|
|
@ -93,7 +95,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
client := NewWorkerClient(c, serverID, address, fmt.Sprintf(config.StunTurnTemplate, address, address))
|
||||
client := NewWorkerClient(c, serverID, address, fmt.Sprintf(config.StunTurnTemplate, address, address), zone)
|
||||
o.workerClients[serverID] = client
|
||||
defer o.cleanConnection(client, serverID)
|
||||
|
||||
|
|
@ -110,9 +112,9 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
|
|||
client.Listen()
|
||||
}
|
||||
|
||||
// WSO handles all connections from frontend to overlord
|
||||
// WSO handles all connections from user/frontend to overlord
|
||||
func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Browser connected to overlord")
|
||||
log.Println("A user connected to overlord ", r.URL)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Warn: Something wrong. Recovered in ", r)
|
||||
|
|
@ -134,18 +136,28 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
|
|||
// get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
|
||||
// TODO: Update link to the wiki
|
||||
roomID := r.URL.Query().Get("room_id")
|
||||
// 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")
|
||||
|
||||
if roomID != "" {
|
||||
log.Printf("Detected roomID %v from URL", roomID)
|
||||
if workerID, ok := o.roomToServer[roomID]; ok {
|
||||
if workerID, ok := o.roomToWorker[roomID]; ok {
|
||||
workerClient = o.workerClients[workerID]
|
||||
log.Printf("Found running server with id=%v client=%v", workerID, workerClient)
|
||||
if userZone != "" && workerClient.Zone != userZone {
|
||||
// if there is zone param, we need to ensure ther worker in that zone
|
||||
// if not we consider the room is missing
|
||||
workerClient = nil
|
||||
} else {
|
||||
log.Printf("Found running server with id=%v client=%v", workerID, workerClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no existing server to connect to, we find the best possible worker for the frontend
|
||||
if workerClient == nil {
|
||||
// Get best server for frontend to connect to
|
||||
workerClient, err = o.getBestWorkerClient(client)
|
||||
workerClient, err = o.getBestWorkerClient(client, userZone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -187,7 +199,7 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
|
|||
wssession.WorkerClient.IsAvailable = true
|
||||
}
|
||||
|
||||
func (o *Server) getBestWorkerClient(client *BrowserClient) (*WorkerClient, error) {
|
||||
func (o *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
|
||||
if o.cfg.DebugHost != "" {
|
||||
log.Println("Connecting to debug host instead prod servers", o.cfg.DebugHost)
|
||||
wc := o.getWorkerFromAddress(o.cfg.DebugHost)
|
||||
|
|
@ -200,14 +212,7 @@ func (o *Server) getBestWorkerClient(client *BrowserClient) (*WorkerClient, erro
|
|||
|
||||
workerClients := o.getAvailableWorkers()
|
||||
|
||||
var serverID string
|
||||
var err error
|
||||
if config.MatchWorkerRandom {
|
||||
serverID, err = findBestServerRandom(workerClients)
|
||||
} else {
|
||||
serverID, err = findBestServerFromBrowser(workerClients, client)
|
||||
}
|
||||
|
||||
serverID, err := findBestServerFromBrowser(workerClients, client, zone)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
|
|
@ -239,27 +244,9 @@ func (o *Server) getWorkerFromAddress(address string) *WorkerClient {
|
|||
return nil
|
||||
}
|
||||
|
||||
// findBestServer returns the best server for a session
|
||||
func findBestServerRandom(workerClients map[string]*WorkerClient) (string, error) {
|
||||
// TODO: Find best Server by latency, currently return by ping
|
||||
if len(workerClients) == 0 {
|
||||
return "", errors.New("No server found")
|
||||
}
|
||||
|
||||
r := rand.Intn(len(workerClients))
|
||||
for k := range workerClients {
|
||||
if r == 0 {
|
||||
return k, nil
|
||||
}
|
||||
r--
|
||||
}
|
||||
|
||||
return "", errors.New("No server found")
|
||||
}
|
||||
|
||||
// findBestServerFromBrowser returns the best server for a session
|
||||
// All workers addresses are sent to user and user will ping to get latency
|
||||
func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) (string, error) {
|
||||
func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
|
||||
// TODO: Find best Server by latency, currently return by ping
|
||||
if len(workerClients) == 0 {
|
||||
return "", errors.New("No server found")
|
||||
|
|
@ -277,6 +264,11 @@ func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *B
|
|||
|
||||
// get the worker with lowest latency to user
|
||||
for wc, l := range latencies {
|
||||
if zone != "" && wc.Zone != zone {
|
||||
// skip worker not in the zone if zone param is given
|
||||
continue
|
||||
}
|
||||
|
||||
if l < minLatency {
|
||||
bestWorker = wc
|
||||
minLatency = l
|
||||
|
|
@ -328,9 +320,9 @@ func (o *Server) cleanConnection(client *WorkerClient, serverID string) {
|
|||
// Remove serverID from servers
|
||||
delete(o.workerClients, serverID)
|
||||
// Clean all rooms connecting to that server
|
||||
for roomID, roomServer := range o.roomToServer {
|
||||
for roomID, roomServer := range o.roomToWorker {
|
||||
if roomServer == serverID {
|
||||
delete(o.roomToServer, roomID)
|
||||
delete(o.roomToWorker, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type WorkerClient struct {
|
|||
Address string
|
||||
StunTurnServer string
|
||||
IsAvailable bool
|
||||
Zone string
|
||||
}
|
||||
|
||||
// RouteWorker are all routes server received from worker
|
||||
|
|
@ -21,8 +22,8 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
|
|||
// RoomID is global so it is managed by overlord.
|
||||
workerClient.Receive("registerRoom", func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Overlord: Received registerRoom room %s from worker %s", resp.Data, workerClient.ServerID)
|
||||
o.roomToServer[resp.Data] = workerClient.ServerID
|
||||
log.Printf("Overlord: Current room list is: %+v", o.roomToServer)
|
||||
o.roomToWorker[resp.Data] = workerClient.ServerID
|
||||
log.Printf("Overlord: Current room list is: %+v", o.roomToWorker)
|
||||
|
||||
return cws.WSPacket{
|
||||
ID: "registerRoom",
|
||||
|
|
@ -32,8 +33,8 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
|
|||
// closeRoom event from a worker, when worker close a room
|
||||
workerClient.Receive("closeRoom", func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Overlord: Received closeRoom room %s from worker %s", resp.Data, workerClient.ServerID)
|
||||
delete(o.roomToServer, resp.Data)
|
||||
log.Printf("Overlord: Current room list is: %+v", o.roomToServer)
|
||||
delete(o.roomToWorker, resp.Data)
|
||||
log.Printf("Overlord: Current room list is: %+v", o.roomToWorker)
|
||||
|
||||
return cws.WSPacket{
|
||||
ID: "closeRoom",
|
||||
|
|
@ -43,10 +44,10 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
|
|||
// getRoom returns the server ID based on requested roomID.
|
||||
workerClient.Receive("getRoom", func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Println("Overlord: Received a getroom request")
|
||||
log.Println("Result: ", o.roomToServer[resp.Data])
|
||||
log.Println("Result: ", o.roomToWorker[resp.Data])
|
||||
return cws.WSPacket{
|
||||
ID: "getRoom",
|
||||
Data: o.roomToServer[resp.Data],
|
||||
Data: o.roomToWorker[resp.Data],
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -56,12 +57,13 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
|
|||
}
|
||||
|
||||
// NewWorkerClient returns a client connecting to worker. This connection exchanges information between workers and server
|
||||
func NewWorkerClient(c *websocket.Conn, serverID string, address string, stunturn string) *WorkerClient {
|
||||
func NewWorkerClient(c *websocket.Conn, serverID string, address string, stunturn string, zone string) *WorkerClient {
|
||||
return &WorkerClient{
|
||||
Client: cws.NewClient(c),
|
||||
ServerID: serverID,
|
||||
Address: address,
|
||||
StunTurnServer: stunturn,
|
||||
IsAvailable: true,
|
||||
Zone: zone,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package worker
|
|||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
|
@ -58,7 +59,7 @@ func NewHandler(cfg worker.Config) *Handler {
|
|||
// Run starts a Handler running logic
|
||||
func (h *Handler) Run() {
|
||||
for {
|
||||
oClient, err := setupOverlordConnection(h.overlordHost)
|
||||
oClient, err := setupOverlordConnection(h.overlordHost, h.cfg.Zone)
|
||||
if err != nil {
|
||||
log.Println("Cannot connect to overlord. Retrying...")
|
||||
time.Sleep(time.Second)
|
||||
|
|
@ -74,8 +75,16 @@ func (h *Handler) Run() {
|
|||
}
|
||||
}
|
||||
|
||||
func setupOverlordConnection(ohost string) (*OverlordClient, error) {
|
||||
conn, err := createOverlordConnection(ohost)
|
||||
func setupOverlordConnection(ohost string, zone string) (*OverlordClient, error) {
|
||||
overlordURL := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: ohost,
|
||||
Path: "/wso",
|
||||
RawQuery: "zone=" + zone,
|
||||
}
|
||||
log.Println("Worker connecting to overlord:", overlordURL.String())
|
||||
|
||||
conn, err := createOverlordConnection(overlordURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
4
web/js/init.js
vendored
4
web/js/init.js
vendored
|
|
@ -7,7 +7,7 @@ $(document).ready(() => {
|
|||
joystick.init();
|
||||
touch.init();
|
||||
|
||||
const roomId = room.loadMaybe();
|
||||
[roomId, zone] = room.loadMaybe();
|
||||
// if from URL -> start game immediately!
|
||||
socket.init(roomId);
|
||||
socket.init(roomId, zone);
|
||||
});
|
||||
|
|
|
|||
6
web/js/network/socket.js
vendored
6
web/js/network/socket.js
vendored
|
|
@ -11,8 +11,10 @@ const socket = (() => {
|
|||
let conn;
|
||||
let curPacketId = '';
|
||||
|
||||
const init = (roomId) => {
|
||||
conn = new WebSocket(`ws://${location.host}/ws${roomId ? `?room_id=${roomId}` : ''}`);
|
||||
const init = (roomId, zone) => {
|
||||
const paramString = new URLSearchParams({roomId: roomId, zone: zone})
|
||||
|
||||
conn = new WebSocket(`ws://${location.host}/ws?${paramString.toString()}`);
|
||||
|
||||
// Clear old roomID
|
||||
conn.onopen = () => {
|
||||
|
|
|
|||
15
web/js/room.js
vendored
15
web/js/room.js
vendored
|
|
@ -11,7 +11,11 @@ const room = (() => {
|
|||
// !to rewrite
|
||||
const parseURLForRoom = () => {
|
||||
let queryDict = {};
|
||||
let regex = /^\/?([A-Za-z]*)\/?/g;
|
||||
var zone = regex.exec(location.pathname)[1]
|
||||
var room = null
|
||||
|
||||
// get room from URL
|
||||
location.search.substr(1)
|
||||
.split('&')
|
||||
.forEach((item) => {
|
||||
|
|
@ -19,10 +23,10 @@ const room = (() => {
|
|||
});
|
||||
|
||||
if (typeof queryDict.id === 'string') {
|
||||
return decodeURIComponent(queryDict.id);
|
||||
room = decodeURIComponent(queryDict.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
return [room, zone];
|
||||
};
|
||||
|
||||
event.sub(GAME_ROOM_AVAILABLE, data => {
|
||||
|
|
@ -50,12 +54,15 @@ const room = (() => {
|
|||
//roomID = loadRoomID();
|
||||
|
||||
// Shared URL second
|
||||
const parsedId = parseURLForRoom();
|
||||
const [parsedId, czone] = parseURLForRoom();
|
||||
if (parsedId !== null) {
|
||||
id = parsedId;
|
||||
}
|
||||
if (czone !== null) {
|
||||
zone = czone;
|
||||
}
|
||||
|
||||
return id;
|
||||
return [id, zone];
|
||||
},
|
||||
copyToClipboard: () => {
|
||||
const el = document.createElement('textarea');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue