Add help overlay, Fix joystick, Update comment (#45)
* add help overlay, fix joystick * Remove icecandidate in connectionState * UPgrade turn server * Update README * Update README * Update readme
4
README.md
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# POGO, Web-based Cloud Gaming Service
|
||||
**SEA**: [http://cloud.webgame2d.com](http://cloud.webgame2d.com) | **US West**: (not hosted)| **US East**: (not hosted)| **Europe**: (not hosted)
|
||||
**SEA**: [http://cloud.webgame2d.com](http://cloud.webgame2d.com) | **US West**: [http://usw.cloud.webgame2d.com](http://usw.cloud.webgame2d.com) | **US East**: [http://use.cloud.webgame2d.com](http://use.cloud.webgame2d.com) | **Europe**: [http://eu.cloud.webgame2d.com](http://eu.cloud.webgame2d.com) | **Instruction**: [Instruction](document/instruction/)
|
||||
For the best gaming experience, please select the closest region to you.
|
||||
|
||||
**Video demo**: https://www.youtube.com/watch?v=koqWB1VKflo
|
||||
|
|
@ -49,7 +49,7 @@ And run
|
|||
* The scripts includes build the binary using Go module
|
||||
|
||||
## Documentation
|
||||
[Design Doc](document/)
|
||||
[Design Doc](document/designdoc/)
|
||||
[Implentation Doc](document/implementation/)
|
||||
|
||||
## Follow up
|
||||
|
|
|
|||
4
document/designdoc/README.md
vendored
|
|
@ -7,14 +7,14 @@ Web-based Cloud Gaming Service contains multiple workers for gaming streaming an
|
|||
Worker is responsible for streaming game to frontend
|
||||

|
||||
|
||||
- After Coordinator found the most appropriate server for the user, webRTC peer-to-peer handshake will be conducted. Coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
|
||||
- After Coordinator matches most appropriate server to the user, webRTC peer-to-peer handshake will be conducted. Coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
|
||||
- On worker, each user session will spawn a new room running a gaming emulator. Image stream and audio stream from emulator is captured and encoded to WebRTC streaming format. We applied Vp8 for Video compression and Opus for audio compression to ensure the smoothest experience. After finish encoded, these stream is then piped out to user and observers joining that room.
|
||||
- On the other hand, input from users is sent to workers over WebRTC DataChannel. Game logic on the emulator will be updated based on the input stream.
|
||||
- Game state is stored in cloud storage, so all workers can collaborate and keep the same understanding with each other. It allows user can continue from the saved state in the next time.
|
||||
|
||||
## Overlord
|
||||
|
||||
Overlord is in charge of picking the most suitable workers for a user. Every time a user connects to Overlord, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Overlord will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
|
||||
Overlord is loadbalancer and coordinator, which is in charge of picking the most suitable workers for a user. Every time a user connects to Overlord, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Overlord will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
28
document/instruction/README.md
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Web-based Cloud Gaming Service Game Instruction
|
||||
|
||||
The game can be played on Desktop, Mobile (Android only). You can plug joystick to play with the game.
|
||||
|
||||
Click question mark on the top left to see game instruction.
|
||||
|
||||
## Key map on Desktop
|
||||
Game keymap follows
|
||||
Arrow keys to move
|
||||
H -> Show help
|
||||
C -> Start
|
||||
V -> Select
|
||||
Z -> A
|
||||
X -> B
|
||||
S -> Save (Save state)
|
||||
A -> Load (Load previous saved state)
|
||||
W -> Share your running game to other or you can keep it to continue playing the next time. Multiple people can access the same game for multiplayer or observation.
|
||||
F -> Full screen
|
||||
Q -> Quit the current game and go to menu screen.**NOTE**: we are facing some issue with quit, so it's better to refresh the page.
|
||||
|
||||
## Mobile play
|
||||
You can play the game on Android device. Make sure your Android has the version that support WebRTC. IOS doesn't support WebRTC streaming now.
|
||||
|
||||
The keys map are equivalent to Desktop. Press the button to fire input.
|
||||
|
||||
## Joystick
|
||||
The game also accepts joystick, so you can try plug in one and experience. It will be very fun!
|
||||
|
||||
BIN
pogo
vendored
44
static/css/main.css
vendored
|
|
@ -1,9 +1,10 @@
|
|||
body {
|
||||
background-image: url('/static/img/500_F_241143639_RW6VdHbqYRM0yfNpDxDlWpj0uXWglAC2.jpg');
|
||||
background-image: url('/static/img/background.jpg');
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
#gamebody {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
width: 556px;
|
||||
height: 278px;
|
||||
|
|
@ -21,6 +22,33 @@ body {
|
|||
box-shadow: inset 0px 0px 2px 2px rgba(219, 222, 222, 1);
|
||||
}
|
||||
|
||||
#help-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#help-overlay-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
background-color: gray;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#help-overlay-detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
background-image: url('/static/img/help_overlay.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
|
||||
#circle-pad-holder {
|
||||
display: block;
|
||||
|
|
@ -168,6 +196,18 @@ body {
|
|||
box-shadow: 0px 0px 2px 2px rgba(219, 222, 222, 1);
|
||||
}
|
||||
|
||||
#btn-help {
|
||||
padding-top: 5px;
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
font-size: 10px;
|
||||
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
|
||||
background-color: lightskyblue;
|
||||
}
|
||||
|
||||
#btn-load {
|
||||
top: 30px;
|
||||
left: 435px;
|
||||
|
|
@ -352,7 +392,7 @@ body {
|
|||
height: 240px;
|
||||
|
||||
/* background-color: gray; */
|
||||
background-image: url('/static/img/pixel_waterfall_bg__by_isohei-d4xntof.gif');
|
||||
background-image: url('/static/img/screen_background1.gif');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
|
|
|||
9
static/game.html
vendored
|
|
@ -20,7 +20,6 @@
|
|||
|
||||
<body>
|
||||
<div id="gamebody">
|
||||
|
||||
<div id="circle-pad-holder">
|
||||
<div id="btn-up" class="dpad" value="up"></div>
|
||||
<div id="btn-down" class="dpad" value="down"></div>
|
||||
|
|
@ -30,8 +29,7 @@
|
|||
</div>
|
||||
|
||||
<div id="bottom-screen">
|
||||
<video id="game-screen" autoplay=true
|
||||
poster="/static/img/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif"></video>
|
||||
<video id="game-screen" autoplay=true poster="/static/img/screen_loading.gif"></video>
|
||||
|
||||
<div id="menu-screen">
|
||||
<div id="menu-container">
|
||||
|
|
@ -66,6 +64,11 @@
|
|||
|
||||
<div id="noti-box" unselectable="on" class="unselectable">Oh my god</div>
|
||||
|
||||
<div id="help-overlay">
|
||||
<div id="help-overlay-background"></div>
|
||||
<div id="help-overlay-detail"></div>
|
||||
</div>
|
||||
<div id="btn-help" unselectable="on" class="btn unselectable" value="help">?</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
BIN
static/img/help_overlay.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 886 KiB After Width: | Height: | Size: 886 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
70
static/js/controller.js
vendored
|
|
@ -2,6 +2,36 @@
|
|||
Menu Controller
|
||||
*/
|
||||
|
||||
function showHelpScreen() {
|
||||
// show btn-save, btn-load
|
||||
if (screenState === "menu") {
|
||||
$("#btn-save").show();
|
||||
$("#btn-load").show();
|
||||
|
||||
$("#menu-screen").hide();
|
||||
} else {
|
||||
$("#game-screen").hide();
|
||||
}
|
||||
|
||||
// show help overlay
|
||||
$("#help-overlay").show();
|
||||
}
|
||||
|
||||
function hideHelpScreen() {
|
||||
//
|
||||
if (screenState === "menu") {
|
||||
$("#btn-save").hide();
|
||||
$("#btn-load").hide();
|
||||
|
||||
$("#menu-screen").show();
|
||||
} else {
|
||||
$("#game-screen").show();
|
||||
}
|
||||
|
||||
// show help overlay
|
||||
$("#help-overlay").hide();
|
||||
}
|
||||
|
||||
function reloadGameMenu() {
|
||||
log("Load game menu");
|
||||
|
||||
|
|
@ -143,6 +173,10 @@ function doButtonDown(name) {
|
|||
} else if (screenState === "game") {
|
||||
setKeyState(name, true);
|
||||
}
|
||||
|
||||
if (name === "help") {
|
||||
showHelpScreen();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -150,11 +184,32 @@ function doButtonUp(name) {
|
|||
$(`#btn-${name}`).removeClass("pressed");
|
||||
|
||||
if (screenState === "menu") {
|
||||
if (name === "up" || name === "down") {
|
||||
stopGamePickerTimer();
|
||||
} else if (name === "join" || name === "a" || name === "b" || name === "start" || name === "select") {
|
||||
startGame();
|
||||
//log("select game");
|
||||
switch (name) {
|
||||
case "up":
|
||||
case "down":
|
||||
stopGamePickerTimer();
|
||||
break;
|
||||
|
||||
case "join":
|
||||
case "a":
|
||||
case "b":
|
||||
case "start":
|
||||
case "select":
|
||||
startGame();
|
||||
break;
|
||||
|
||||
case "quit":
|
||||
popup("You are already in menu screen!");
|
||||
break;
|
||||
|
||||
case "load":
|
||||
popup("Lets play to load game!");
|
||||
break;
|
||||
|
||||
case "save":
|
||||
popup("Lets play to save game!");
|
||||
break;
|
||||
|
||||
}
|
||||
} else if (screenState === "game") {
|
||||
setKeyState(name, false);
|
||||
|
|
@ -184,6 +239,7 @@ function doButtonUp(name) {
|
|||
openFullscreen(screen);
|
||||
}
|
||||
break;
|
||||
|
||||
case "quit":
|
||||
stopGameInputTimer();
|
||||
showMenuScreen();
|
||||
|
|
@ -196,5 +252,9 @@ function doButtonUp(name) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name === "help") {
|
||||
hideHelpScreen();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
23
static/js/gesture_joystick.js
vendored
|
|
@ -15,6 +15,10 @@
|
|||
axis 0, 1 <--> second dpad
|
||||
*/
|
||||
|
||||
/*
|
||||
change full to help (temporary)
|
||||
*/
|
||||
|
||||
let joystickMap;
|
||||
let joystickState;
|
||||
let joystickIdx;
|
||||
|
|
@ -28,7 +32,7 @@ function checkJoystickAxisState(name, state) {
|
|||
if (state === true) {
|
||||
doButtonDown(name);
|
||||
} else {
|
||||
doButtonUp(button);
|
||||
doButtonUp(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +74,7 @@ function checkJoystickState() {
|
|||
|
||||
|
||||
// we only capture the last plugged joystick
|
||||
$(window).on("gamepadconnected", function (event) {
|
||||
window.addEventListener("gamepadconnected", function (event) {
|
||||
var gamepad = event.gamepad;
|
||||
log(`Gamepad connected at index ${gamepad.index}: ${gamepad.id}. ${gamepad.buttons.length} buttons, ${gamepad.axes.length} axes.`);
|
||||
|
||||
|
|
@ -83,28 +87,29 @@ $(window).on("gamepadconnected", function (event) {
|
|||
|
||||
if (os === "android") {
|
||||
// default of android is KeyMap1
|
||||
joystickMap = { 2: "a", 0: "b", 3: "start", 4: "select", 10: "load", 11: "save", 8: "full", 9: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
joystickMap = { 2: "a", 0: "b", 3: "start", 4: "select", 10: "load", 11: "save", 8: "help", 9: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
} else {
|
||||
// default of other OS is KeyMap2
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "full", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
}
|
||||
|
||||
if (os === "android" && (browser === "firefox" || browser === "uc")) { //KeyMap2
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "full", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
}
|
||||
|
||||
if (os === "win" && browser === "firefox") { //KeyMap3
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "full", 7: "quit" };
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit" };
|
||||
}
|
||||
|
||||
if (os === "mac" && browser === "safari") { //KeyMap4
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "full", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
}
|
||||
|
||||
if (os === "mac" && browser === "firefox") { //KeyMap5
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "full", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
}
|
||||
|
||||
|
||||
// reset state
|
||||
joystickState = {
|
||||
left: false,
|
||||
|
|
@ -128,7 +133,7 @@ $(window).on("gamepadconnected", function (event) {
|
|||
|
||||
|
||||
// disconnected event is triggered
|
||||
$(window).on("gamepaddisconnected", (event) => {
|
||||
window.addEventListener("gamepaddisconnected", (event) => {
|
||||
clearInterval(joystickTimer);
|
||||
log(`Gamepad disconnected at index ${e.gamepad.index}`);
|
||||
});
|
||||
|
|
|
|||
1
static/js/gesture_keyboard.js
vendored
|
|
@ -19,6 +19,7 @@ const KEYBOARD_MAP = {
|
|||
87: "join", // w
|
||||
65: "load", // a
|
||||
70: "full", // f
|
||||
72: "help", // h
|
||||
}
|
||||
|
||||
$("body").on("keyup", function (event) {
|
||||
|
|
|
|||
11
static/js/init.js
vendored
|
|
@ -1,8 +1,15 @@
|
|||
|
||||
// Window rerender / rotate screen if needed
|
||||
function fixScreenLayout() {
|
||||
var targetWidth = $(document).width() * 0.8;
|
||||
var targetHeight = $(document).height() * 0.8;
|
||||
|
||||
var targetWidth = $(document).width() * 0.9;
|
||||
var targetHeight = $(document).height() * 0.9;
|
||||
|
||||
// mobile == full screen
|
||||
if (getOS() === "android") {
|
||||
var targetWidth = $(document).width();
|
||||
var targetHeight = $(document).height();
|
||||
}
|
||||
|
||||
// Should have maximum box for desktop?
|
||||
// targetWidth = 800; targetHeight = 600; // test on desktop
|
||||
|
|
|
|||
1
static/js/utils.js
vendored
|
|
@ -42,6 +42,7 @@ function getOS() {
|
|||
var OSName = "unknown";
|
||||
if (navigator.appVersion.indexOf("Win") !== -1) OSName = "win";
|
||||
else if (navigator.appVersion.indexOf("Mac") !== -1) OSName = "mac";
|
||||
else if (navigator.userAgent.indexOf("Linux") !== -1) OSName = "linux";
|
||||
else if (navigator.userAgent.indexOf("Android") !== -1) OSName = "android";
|
||||
return OSName;
|
||||
}
|
||||
|
|
|
|||
11
static/js/ws.js
vendored
|
|
@ -97,11 +97,11 @@ function sendPing() {
|
|||
function startWebRTC() {
|
||||
// webrtc
|
||||
pc = new RTCPeerConnection({iceServers: [{
|
||||
urls: 'stun:159.65.141.209:3478',
|
||||
urls: 'stun:stun-turn.webgame2d.com:3478',
|
||||
username: "root",
|
||||
credential: "root"
|
||||
}, {
|
||||
urls: "turn:159.65.141.209:3478",
|
||||
urls: "turn:stun-turn.webgame2d.com:3478",
|
||||
username: "root",
|
||||
credential: "root"
|
||||
}]})
|
||||
|
|
@ -181,7 +181,6 @@ function startWebRTC() {
|
|||
if (pc.iceConnectionState === "connected") {
|
||||
gameReady = true
|
||||
iceSuccess = true
|
||||
conn.send(JSON.stringify({"id": "icecandidate", "data": e.candidate}));
|
||||
}
|
||||
else if (pc.iceConnectionState === "failed") {
|
||||
gameReady = false
|
||||
|
|
@ -231,11 +230,13 @@ function startWebRTC() {
|
|||
|
||||
function startGame() {
|
||||
if (!iceSuccess) {
|
||||
popup("Game cannot load. Please refresh")
|
||||
popup("Game cannot load. Please refresh");
|
||||
return;
|
||||
}
|
||||
// TODO: Add while loop
|
||||
if (!gameReady) {
|
||||
popup("Game is not ready yet. Please wait")
|
||||
popup("Game is not ready yet. Please wait");
|
||||
return;
|
||||
}
|
||||
log("Starting game screen");
|
||||
screenState = "game";
|
||||
|
|
|
|||