I have spent the last four days studying promises, coroutines, fibers, continuations, etc.
I am still unable to see how to resolve my multiplayer turn-based card game moves, in which the starting player is effectively the game 'controller' of up to five, either AI or human players.
The code below works but has one problem:-
it cannot detect human oppo's card moves and therefore continues playing without them, which, of course makes a mess.
Can anyone please suggest either a change to my overall concept or a way to use promise or one of any other 'synchronising' constructs?
Here are the four key areas of my code:
function oppoPlays () {
// can only go through here if game starter
if (joiner!="") {return;}
for (pp=1; pp<numberofplayers; pp++) {
if (oppoType[pp] == "AI") {
// an AI player moves
.
.
} else {
// non-AI player
var yourTurnmsg="It's "+playerNames[pp]+"'s turn";
// tell human player that it's their turn
$("#text_message").val(yourTurnmsg).trigger(jQuery.Event('keypress', { keyCode: 13, which: 13 }));
// how to detect human oppo's card moved?
}
}
}
// chat functionality
$("#text_message").on("keypress", function(e) {
if (e.keyCode == 13){
payload = new Object();
payload.action = 'chat_text';
payload.chat_text = tmsg; // It's michael29's turn
payload.user_id = playerNames[pp];
payload.game_no = game_no;
socket.send(JSON.stringify(payload));
}
});
// socket gets oppo's response
function checkJson(res, sttr_id, game_no) {
if(res.action=="game_move"){
// find player
var pp=playerNames.indexOf(res.user_id);
cpos=res.cardno;
playCard_oppo(pp, cpos);
}
}
// turn an oppo's card face up and update scores
function playCard_oppo(pp, cardno) {
// and move it to the stack
topoc= parseInt($("#oppo_card" + cardno).css('top'));
leftoc=parseInt($("#oppo_card" + cardno).css('left'));
$("#oppo_card" + cardno).css({ top: topoc, left: leftoc, opacity: "50%" });
.
.
if (joiner=="") {
// tell oppoPlays fn that the card has moved
}
}
The game is similar to uno in concept but with a scoring component
(aimed to help children with basic arithmetic).
Consider having a board state that is a global and player/AI moves modify it. Then, when it is time for the AI opponent to make a move, it consults the current board state and decides the move.
If your board state is just represented by elements on the page you'll need a way to scan it and calculate a useful in-memory representation of the board state. Without details of your implementation it's hard to be more specific.
Maybe start thinking in terms of a play-cycle, something like this :
(host) Broadcasts game state.
(host) Awaits confirmation from all clients that broadcast was received.
(clients) Render game state.
(host) Receives confirmations then informs next client (and hence its player) that it is his/her/its turn.
(host) Awaits player's move
(client) Unlocks UI allowing player to make move.
(player) Makes move.
(client) Sends move command and re-locks UI.
(host) Receives move command and modifies game state accordingly.
Then back to 1.
Also, think of AI players as just a special case of human players. If you can get it right for humans, then (re)introducing AI should be fairly simple.
The solution hinged around two things:-
separating the AI player code from the human player code;
adding and removing a window event that's triggered after a human's move is detected.
The condensed code now looks like this:-
// if this is game starter give each player a turn
if (joiner == "") {
// there's always at least one
pp = 1;
if (oppoType[pp] == "AI") { AIplays(); } else { humanPlays(); }
}
function humanPlays () {
// tell human player that it's their turn
var yourTurnmsg="It's "+playerNames[pp]+"'s turn"
$("#text_message").val(yourTurnmsg).trigger(jQuery.Event('keypress', { keyCode: 13, which: 13 }));
//window.addEventListener("humanPlayed", function(evnt) {
$(window).on("humanPlayed", function(evnt) {
endOfTurn();
});
}
function endOfTurn () {
if (!(winner)) {
if (pp++ != numberofplayers) {
if (oppoType[pp] == "AI") {
setTimeout(function (){ $("#clickForNextPlayer").show(); }, 1000);
} else {
$("#clickForNextPlayer").trigger('click');
}
}
}
}
// click for next player
$("#clickForNextPlayer").on('click', function() {
$("#clickForNextPlayer").hide();
$(window).off("humanPlayed");
if (pp == numberofplayers) {
// uncover outStack for game starter to play
$("#outStackcover").hide();
return;
}
if (oppoType[pp] == "AI") { AIplays(); } else { humanPlays(); }
});
function AIplays () {
AIcardno = chooseCard(pp, diffLevel);
.
.
if ($("#chatWindow").is(":visible")) {
payload = new Object();
payload.action="game_move";
payload.game_no=gamestarted;
payload.user_id=playerNames[pp];
payload.cardno=AIcardno;
socket.send(JSON.stringify(payload));
}
$("#oppo_card" + cc).css('background-image', "url(JerseyTeam" + playerNumbers[(pp == numberofplayers ? 1 : pp)] + ".gif)");
outStackturn();
endOfTurn();
}
Related
Im creating a multiplayer game using Phaser 3 in combination with Socket.io, it is a Mario party style game with multiple minigames that are separated by quiz rounds where questions are asked.
I intend for the host of the game to be able to pick the minigame and then the players will play it, then the same minigame could be replayed if the host picks it again, however after a minigame has been played once when it is loaded again multiple versions of the scene are created.
It is difficult to explain the issue so I will try to show it visually by showing how the scenes are loaded:
Host Client Scene order:
Main page (This is the page where a client chooses to be host or player)
Host Page (This is where the host waits for players to join)
Minigame Selector (Where the host picks a minigame)
Minigame 1 Host side (For this example a minigame called minigame 1 is picked)
Minigame Selector (The minigame selector is loaded again)
Minigame 1 Host side (Minigame 1 is picked again)
Minigame Selector
Player Client Scene order
Main Page
Player Page (This is where a player waits in the lobby until the host starts the game)
Intermediate Player Scene (This is where a player waits until a minigame is chosen)
Minigame 1 Player (once host picks minigame 1 all players connected to the host lobby will play the minigame)
Intermediate Player Scene
Minigame 1 Player x2 (After the minigame is launched again 2 versions of it are loaded simultaneously causing scores to be messed up)
Intermediate Player Scene x2 (At this point the error is exponential, if the minigame is loaded again then scores become even more skewed and more versions of the same scene are loaded)
Im pretty sure it is nothing to do with multiple socket events being emitted because I tried just launching the scenes on the player side with no socket interactions and the same error occurred.
Things I have tried:
Just using .launch() and .stop() to start and stop scenes
Using if statements and variables to prevent multiple launches
Clearing Timer interval at beginning of scene
Using .remove() to completely destroy it, then .add() to add it back to manager
Using this.events.once() to ensure it can only happen once
Code:
Host Minigame Scene:
class minigame1Host extends Phaser.Scene
{
constructor() {
super('mg1Host');
}
create()
{
clearInterval(this.countdown)
this.socket = this.registry.get("socket")
this.val = this.registry.get("pin")
let { width, height } = this.sys.game.canvas;
this.timeLimit = 15
this.doneCheck = null
this.timeText = this.add.text(width/2,height/2-200,this.timeLimit).setScale(2)
this.countdown = setInterval(() => {
this.timeLimit = this.timeLimit - 1
this.timeText.text = String(this.timeLimit)
if (this.timeLimit == 0) {
if(this.doneCheck != true)
{
this.doneCheck = true
clearInterval(this.countdown)
this.scene.launch("selector").stop()
}
}
}, 1000);
}
update()
{
//some code to generate a random value for enemy spawning
}
}
Player Minigame Scene:
class minigame1Player extends Phaser.Scene
{
constructor() {
super('mg1Player');
}
create()
{
clearInterval(this.countdown)
this.socket = this.registry.get("socket")
this.val = this.registry.get("pin")
let { width, height } = this.sys.game.canvas;
this.timeLimit = 15
this.score = 0
//create groups for scorers (a scorer is something a player can click to get points)
this.goodScorers = this.add.group()
this.badScorers = this.add.group()
this.timeText = this.add.text(width/2,height/2-460,this.timeLimit).setScale(2)
this.scoreText = this.add.text(width/2-200,height/2-100,this.score).setScale(2)
this.doneCheck = false
this.countdown = setInterval(() => {
this.timeLimit = this.timeLimit - 1
this.timeText.text = String(this.timeLimit)
if (this.timeLimit == 0) {
if(this.doneCheck != true)
{
this.goodScorers.destroy()
this.badScorers.destroy()
this.doneCheck = true
clearInterval(this.countdown)
clearTimeout(this.deleteBadAfter)
clearTimeout(this.deleteGoodAfter)
score = score + this.score
this.scene.launch("tempPlayer").stop()
}
}
}, 1000);
this.socket.on("createScorer" ,(values) =>
{
//code that creates scorers
})
}
}
Minigame Selector:
class pickMinigameHost extends Phaser.Scene
{
constructor() {
super('selector');
}
create()
{
this.socket = this.registry.get("socket")
this.val = this.registry.get("pin")
let { width, height } = this.sys.game.canvas;
this.add.text(width/2, height/2, "Pick a minigame:")
this.mg1But = this.add.image(width/2,height/2-300,"dissButton").setInteractive().setScale(0.5)
this.mouseCheck = false
this.mg1But.on('pointerdown', () =>
{
if(this.mouseCheck == false)
{
this.mouseCheck = true
this.socket.emit("startMG1", [this.val]);
this.scene.launch("mg1Host").stop()
}
})
}
}
Temporary Player Scene:
class temporaryPlayer extends Phaser.Scene
{
constructor() {
super('tempPlayer');
}
create()
{
clearInterval(this.countdown)
this.socket = this.registry.get("socket")
let { width, height } = this.sys.game.canvas;
this.add.text(width/2, height/2, "A Minigame is being selected")
this.socket.on("startMg1Comp" ,() =>
{
this.scene.launch("mg1Player").stop()
})
}
}
Note:
All of the provided code is client side as I dont think server is the issue, btw sorry for my awful code and if it is a really easy fix.
Your code is pretty complicated , with all the setTimeout and setInterval function calls, and Alot of "Scene-hopping", what makes following the flow very difficult.
That said, your assumtion is incorrect, as far as I know there can't be two scene running with the same name. You can check it calling console.info(this.scene.manager.keys) this will show all scenes.
I can just asume that the problem has to do with the socket call, in the create function, for example:
this.socket.on("createScorer", (values) => {
//code that creates scorers
})
Since you are binding on each launch function a new listener to the same socket. Possible solutions, all with there own drawbacks:
remove listener on stopping the scene, (
something like: this.events.on('shutdown', () => socket.off('createScorer'));)
attach the listener in the constructor (a scene constructor is only called once)
create a master/background Scene that handels all socket actions (I have no good example at hand, but you could create a hierachy something like in this answer)
P.s: I would recommend createng CustomScenes with you extend to keep code duplication to a minimum.
For example:
(extracting some logic/data into the custom class)
class MySocketScene extends Phaser.Scene {
constructor(name) {
super(name);
}
create() {
this.socket = this.registry.get("socket");
this.val = this.registry.get("pin");
this.width = this.sys.game.canvas.width;
this.height = this.sys.game.canvas.height;
// ... Other repeating code
}
}
// btw.: I would Name My Classes in PascalCase
class minigame1Host extends MySocketScene {
constructor() {
super('mg1Host');
}
create() {
super.create();
let { width, height } = this;
//...
}
//...
}
I am attempting to create an app to detect a ball and another object while taking a video, say a racket, and these objects must be in FOV of the camera all the time to activate a function that then takes the average speed of the ball. I have established a site that does request permission to access webcam and displays it on screen, while making a recording button and all. However, my issues start turning up in my Javascript.EDIT:Start Recording contains id="togglerecordingmodeEl" onclick="toggleRecording()"
HTML:
Start Recording
Preview
JavaScript:
let video = null; // video element
let detector = null; // detector object
let detections = []; // store detection result
let videoVisibility = true;
let detecting = false;
const toggleRecordingEl = document.getElementById('toggleRecordingEl');
function toggleRecording() {
if (!video || !detector) return;
if (!detecting) {
detect();
toggleDetectingEl.innerText = 'Stop Detecting';
} else {
toggleDetectingEl.innerText = 'Start Detecting';
}
detecting = !detecting;
}
function detect() {
// instruct "detector" object to start detect object from video element
// and "onDetected" function is called when object is detected
detector.detect(video, onDetected);
}
// callback function. it is called when object is detected
function onDetected(error, results) {
if (error) {
console.error(error);
}
detections = results;
}
In summary, I want it to first recognise two objects in the FOV, and only if these two objects are in the FOV, then I can work on a new function. I have used https://medium.com/the-web-tub/object-detection-with-javascript-the-easy-way-74fbe98741cf & https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API/Recording_a_media_element to construct this app so far.
I have a function that checks if bullet hits a player, and if so it should remove the bullet and the player that got hit.
I have check_if_bullet_hit_player(bulletRef) function that gets a bullet ref, and the id of the player got shot.
I also have a shoot() function that calls the check_if_bullet_hit_player(bulletRef) for every bullet's movement.
Currently when a bullet hits a player, the bullet disappear, but I can't make the player got hit to disappear also.
Attempt - 1:
I tried the following shoot function:
function shoot(speed=0.5, distance=5, targetX, targetY){
var shoot = setInterval(function() {
check_if_bullet_hit_player(bulletRef).then(player_got_shot_id => {
if (player_got_shot_id != ""){
clearInterval(shoot);
bulletRef.remove();
hitPlayerRef = firebase.database().ref(`players/${player_got_shot_id}`);
hitPlayerRef.once("value").then(function(){
alert("hi");
hitPlayerRef.remove();
})
}
})
})
}
I get alert hi when a player gets hit, and the player that gets hit disappear for a moment, then re-created (which is not what is wanted). I think that the player get re-created because of async, so I tried different methods.
Attempt - 2:
I tried as the following stack overflow's answer, which waits for the ref to return:
function shoot(speed=0.5, distance=5, targetX, targetY){
var shoot = setInterval(function() {
check_if_bullet_hit_player(bulletRef).then(player_got_shot_id => {
if (player_got_shot_id != ""){
clearInterval(shoot);
bulletRef.remove();
await firebase.database().ref(`players/${player_got_shot_id}`).remove();
}
})
})
}
and I get:
Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
Attempt - 3:
I tried to wait with then:
function shoot(speed=0.5, distance=5, targetX, targetY){
var shoot = setInterval(function() {
check_if_bullet_hit_player(bulletRef).then(player_got_shot_id => {
if (player_got_shot_id != ""){
clearInterval(shoot);
bulletRef.remove();
hitPlayerRef = firebase.database().ref(`players/${player_got_shot_id}`);
return hitPlayerRef.get(); // HERE we chain the promise
}
}).then(hola => {
hola.remove();
})
})
}
But nothing happens.
How can I make the player got hit to disappear?
I have some JS cobbled together with the ToneJS library to toggle some sounds on/off.
function togglePlay() {
const status = Tone.Transport.state; // Is either 'started', 'stopped' or 'paused'
const element = document.getElementById("play-stop-toggle");
if (status == "started") {
element.innerText = "PLAY";
Tone.Transport.stop()
} else {
element.innerText = "STOP";
Tone.Transport.start()
}
document.querySelector('#status').textContent = status;
}
var filter = new Tone.Filter({
type: 'lowpass',
Q: 12
}).toMaster()
var fmOsc = new Tone.AMOscillator("Ab3", "sine", "square").toMaster()
var fmOsc2 = new Tone.AMOscillator("Cb3", "sine", "square").toMaster()
var fmOsc3 = new Tone.AMOscillator("Eb3", "sine", "square").toMaster()
var synth = new Tone.MembraneSynth().toMaster()
//create a loop
var loop = new Tone.Loop(function(time){
// synth.triggerAttackRelease("A1", "8n", time)
// fmOsc.start()
fmOsc2.start()
fmOsc3.start();
}, "4n")
//play the loop between 0-2m on the transport
loop.start(0)
// loop.start(0).stop('2m')
Inside the loop I have a drum beat that I have commented out currently. When it was commented in, the togglePlay() function would start and stop it as expected.
However, the fmOsc2 and fmOsc3 functions will start when I toggle start, but do not terminate when I toggle stop.
For reference, here is what the HTML side looks like: <div class="trackPlay" id="play-stop-toggle" onclick="togglePlay()">PLAY</div>
How can I get the fmOsc2 and fmOsc3 functions to toggle with the button state?
The Tone Transport is not meant to be used in this way, either for starting or stopping oscillators (it can start/stop oscillators, of course, but I don't think that it's doing what you want it to do) :-)
If you add a console.log within the function that you're passing to Tone.Loop, you will see that it's being called repeatedly i.e., you're calling fmOsc.start() over and over again. (It does make sense to do the repeating drum beat in that Loop though)
Calling Tone.Transport.stop() does not stop the oscillators -- it only stops the Loop / Transport function (i.e., the "time keeping"). The drum beat makes it obvious -- pressing Stop in your UI kills the drums, but the oscillators keep oscillating.
The easiest / most direct (and only?) way of stopping the oscillators is to call .stop() on them:
if (status == "started") {
element.innerText = "PLAY";
Tone.Transport.stop()
fmOsc2.stop()
fmOsc3.stop()
} else {
// ... [snip] ...
I don't know why, but I can't figure out why this does not work. I've been sitting for hours trying different methods, reading and watching videos on the subject, but I still can not figure it out.
Q: Why does a "new" audio file start playing, overlapping the previous one, every time I click the element? Should my if statement not take care of that?
document.getElementById("chatWrapper").addEventListener("click", playSound);
function playSound() {
//alert("Success!");
var sEE0 = new Audio('../sound/00-menu-music.mp3');
sEE0.volume = 0.2;
if (sEE0.duration > 0) {
return;
} else {
sEE0.play();
}
}
How do I solve this?
Thanks in advance!
You seem to be creating a new audio element each time you click.
Try to initialize it only once: (attn, quick'n'dirty!)
document.getElementById("chatWrapper").addEventListener("click", playSound);
var sEE0 = new Audio('../sound/00-menu-music.mp3');
function playSound() {
//alert("Success!");
sEE0.volume = 0.2;
if (sEE0.duration > 0) {
return;
} else {
sEE0.play();
}
}
like #frontend_dev said, one of your problem is the variable scoping, other is you are checking using sEE0.duration, but that would give you the length of the audio file, which is bound to be bigger than 0, may be that's why it is not playing, what you are looking for is currentTime, the if condition should be if (sEE0.duration > 0) { ,
but I would prefer checking if the audio is playing or paused, rather than checking till what part the audio has played( unless the usecase requires that)
I would re-write the code as:
document.getElementById("chatWrapper").addEventListener("click", playSound);
var sEE0 = new Audio('../sound/00-menu-music.mp3');
sEE0.volume = 0.2;
function playSound() {
//alert("Success!");
if (sEE0.paused) {
sEE0.play();
}
}