why doesn't JavaScript's setTimeout slow down keydown rate? - javascript

Why doesn't the following keydown event slow down by 3000 mil's, when I am continuously pressing the keydown event (letter k)? If I keep my finger down, the count rapidly adds up as through there is no setTimeout on mcount. why is that? There should be a delay between each count but I can't get it to work...
var mcount = 0;
function playershoot() {
if(!game.playerHit){
$(document).keydown(function(e){
switch(e.keyCode){
case 75:
clearTimeout();
setTimeout(console.log(mcount++), 3000);
break;
}
});
}
}
playershoot();
Any advice will be appreciated!
Thanks

1.: setTimeout() returns a timeoutId which can be cleared with clearTimeout(timeoutId). You're not doing that... so, after your 3 second delay, all those timeouts are called back-to-back.
2.: your console.log is executed immediately because you didn't wrap it in a function like so:
setTimeout(function() { console.log(mcount++) }, 3000);

setTimeout does not cause a delay, it starts a timer that fires an event after the specified amount of time.
You cannot "sleep" in Javascript, you need to refactor your code so it can work with events. For your code, it looks like you will need to set a flag at first keypress. Then return, and only allow new keypresses (ie. only respond to), when the flag is cleared. The flag can then be cleared automatically after a time with setTimeout.

To go with what #Norguard said, here's an implementation: http://jsfiddle.net/apu3P/
this.fire = function(){
var cFire = new Date();
if ((cFire - lastFire) / 1000 > 1/me.fireRate){
// code to fire the projectile
lastFire = cFire;
}
};
I have fireRate set up as an integer indicating how many times per second the player can fire.
In the demo, I set up 3 players each with different fire rates. If you hold the spacebar down, you can see this in action.

While everyone here is right, what they're missing is that you need to put a delay on the firing, not on the event being called...
Inside of your keydown event, set a timestamp, have a previous-time and a current-time for the event.
Inside of the function, have a time_limit.
So when you press the key (or it fires repeatedly), check for:
current_time - last_fired >= rate_limit;
If the current time is more than 3000ms since the last shot, then set the last_fired timestamp to the current time, and fire your weapon.
EDIT
Consider this trivial example:
var Keyboard = {};
var player = (function () {
var gun = {
charging : false,
lastFired : 0,
rateLimit : 3000
},
controls = { shoot : 75 },
isHit = false,
public_interface;
function shoot () {
var currentTime = Date.now();
if (gun.rateLimit > currentTime - gun.lastFired) { return; }
/* make bullet, et cetera */
gun.lastFired = currentTime;
}
function update () {
if (Keyboard[controls.shoot] || gun.charging) { this.shoot(); }
// if key was released before this update, then the key is gone...
// but if the gun was charging, that means that it's ready to be fired
// do other updates
}
function draw (ctx) { /* draw player */ }
public_interface = {
shoot : shoot,
damage : function (amt) { isHurt = true; /* rest of your logic */ }
draw : draw,
update : update
};
return public_interface;
}());
document.addEventListener("keydown", function (e) {
// if key is already down, exit
if (!!Keyboard[e.keyCode]) { return; }
// else, set the key to the time the key was pressed
// (think of "charging-up" guns, based on how long you've held the button down)
Keyboard[e.keyCode] = e.timeStamp;
});
document.addEventListener("keyup", function (e) { delete Keyboard[e.keyCode]; });
Inside of your gameloop, you're now going to do things a little differently:
Your player is going to update itself.
Inside of that update, it's asking the Keyboard if it's got the shoot key pressed down.
If it is, then it will call the shoot method.
This still isn't 100% correct, as Player shouldn't care about or know about Keyboard.
It should be handled through a service of some kind, rather than asking for window.Keyboard.
Regardless...
Your controls are now wrapped inside of the player -- so you can define what those controls are, rather than asking by keyCode all over the place.
Your events are now doing what they should: setting the key and going away.
In your current iteration, every time the browser fires keydown, which might be 300x/sec, if it wanted to, that event ALSO has to call all of your player logic... 300x/sec...
In larger games, you could then take this a step further, and make components out of Controls and Health, each having all of the properties and all of the methods that they need to do their own job, and nothing else.
Breaking the code up this way would also make it dirt-simple to have different guns.
Imagine an Inventory component:
The inventory contains different guns.
Each gun has its own rateLimit, has its own lastFired, has its own bulletCount, does its own damage, and fires its own bulletType.
So then you'd call player.shoot();, and inside, it would call inventory.equipped.shoot();.
That inner function would take care of all of the logic for firing the equipped gun (because you'd inventory.add(Gun); to your guns, and inventory.equip(id); the gun you want.

You have to pass returned value of setTimeout to clearTimeout . to cancel it.
var mcount = 0,timeout;
function playershoot() {
if(!game.playerHit){
$(document).keydown(function(e){
switch(e.keyCode){
case 75:
clearTimeout(timeout );
timeout = setTimeout(function(){
console.log(mcount++);
}, 3000);
break;
}
});
}
}
playershoot();

Related

Why are old variables reused when using event handler functions with Javascript?

I'm trying to create a Simon game (project from a Udemy course) where you have to memorize a pattern sequence and click the right buttons. At each level, the pattern sequence increases by one. You have to repeat the entire sequence each time. If you lose, you have to press a key to restart the game. Here is an example:
https://londonappbrewery.github.io/Simon-Game/
However, when my game is restarted with my current code, everything falls apart. I don't know if old variables from a previous game continue into the new one, or maybe my code is just written really poorly.
I've implemented the entire game within a keypress event handler. Doing this initiates all the functions and variables of the game. The sequence is stored within an array, and each subsequent click on a box compares its colour value against the array. If it doesn't match - its game over. If it does match, it will either wait for you to finish the sequence correctly, or go to the next level.
Can anyone give me pointers on what I might be doing wrong? And whether it's at all possible to make this work the way I have it set up?
$(document).on("keypress", function(){
let counter = 0;
let level = 1;
let colours = ["green","red","yellow","blue"];
let nodeList = [];
function nextNode(){
randomNumberGen = Math.floor(Math.random()*4);
currentNode = colours[randomNumberGen];
nodeList.push(currentNode);
$("."+currentNode).fadeOut(200).fadeIn(200);
}
$("h1").text("Level "+level);
setTimeout(function() {
nextNode();
},500);
$(document).on("click", ".box", function(){
selectedNode = $(this).attr("id");
$("#"+selectedNode).addClass("pressed");
setTimeout(function(){
$("#"+selectedNode).removeClass("pressed");
},100);
if (nodeList[counter] != selectedNode){
gameSounds("wrong");
$("body").addClass("game-over");
setTimeout(function(){
$("body").removeClass("game-over");
},100);
$("h1").text("Game Over, Press Any Key to Restart");
}
else{
gameSounds(selectedNode);
counter++;
if (counter>= nodeList.length){
level++;
setTimeout(function(){
$("h1").text("Level "+(level));
}, 1000);
counter = 0;
setTimeout(function() {
nextNode();
},1000);
}
}
});
});
function gameSounds(key){
var soundPlay = new Audio("sounds/"+key+".mp3");
soundPlay.play();
}

How to trigger a var after x actions

I have the following code. I want to trigger the action in function activityDetected(eventName) only after 100 click. How to do this ?
I know I have to put let a = 1; ++a but not sure where...
https://pastebin.com/SMsJsikE
const intervalTimeout = 2000;
//here is where code should be added. let a = 1; ++a...
function activityDetected(eventName) {
console.log(`Activity detected with the event name: ${eventName}!`);
clearInterval(activityTimeout);
activityTimeout = setInterval(recordNoActivity, intervalTimeout);
}
document.addEventListener('click', _ => {
activityDetected('click');
});
You need to declare a counter outside the function and up it by 1 when the eventName is 'click'. After that check for a % 100 and put whatever action you want to call every 100 clicks in there.
Look at the code example:
// For ease, change this to a smaller value to detect more often, higher to detect less often!
const intervalTimeout = 2000;
let a = 0;
// Here's our interval, setting up the initial capture of no activity
let activityTimeout = setInterval(recordNoActivity, intervalTimeout);
// A single function to handle the events we're listening to.
// clears the interval and restarts it, also tells us which event has cleared the interval!
//here is where code should be added. let a = 1; ++a...
function activityDetected(eventName) {
if(eventName == 'click'){
a++;
if(a%100 == 0){
// Trigger whatever you want to trigger after every 100 clicks
}
}
console.log(`Activity detected with the event name: ${eventName}!`);
clearInterval(activityTimeout);
activityTimeout = setInterval(recordNoActivity, intervalTimeout);
}
// Set listening events
document.addEventListener('keydown', _ => {
activityDetected('keydown');
});
document.addEventListener('click', _ => {
activityDetected('click');
});
As correctly pointed by others on this thread the variable a should be declared and defined outside the function but the reason why this approach would work is because of Closure
So when the function is getting invoked an execution context is created which contains
scopeChain - it contains variableObject + all parent execution context's variableObject
variableObject - it contains function arguments / parameters, inner variable and function declarations
this - the this context.
Thus the variable a values would be saved before invoking the function and the variable will keep incrementing.

How to be frugal with JS DOM calls for onscroll functions

What I'm trying to achieve:
If user has scrolled more than 24px from the top (origin), do something once.
If the user scrolls back within 24px of the top (origin), reverse that something once.
I have this code:
$("#box").scroll(function(){
var ofs = $(".title").offset().top;
if (ofs <= 24)
// Do something
else
// Reverse that something
})
As I understand it, this function runs every time the user scrolls, which can result in hundreds of calls to the DOM.
This isn't every resource efficient - Is there a more passive approach to this?
Both conditions are triggered repeatedly even for small scroll amounts - any way to execute the code just once and not repeat if the same condition is true?
What you are looking to do is either throttling the requests or something called "debounce". Throttling only allows a certain number of calls to whatever in a period of time, debounce only calls the function once a certain time after action has stopped.
This is a good link explaining it: https://css-tricks.com/the-difference-between-throttling-and-debouncing/
There are several libraries out there that will do this for you like Underscore and Lodash. You can roll your own as well and the premise is basically the following for debounce:
var timer;
$('#box').scroll(function(){
//cancel and overwrite timer if it exists already
// set timer to execute doWork after x ms
})
function doWork(){
//do stuff
}
You can also look into using requestAnimationFrame depending on browser support. requestAnimationFrame example and it looks like it's supported in most modern browsers and IE >= 10
In the code below, everytime the user scrolls above or below that 25px threshold, one of the conditions in the if ($boxAboveBelow) if-statement will be called.
var $box = $('#box');
var $boxAboveBelow = true; // true above, false below
$box.on('scroll', function() { // Throttle this function if needed
var newAboveBelow = $box.scrollTop() < 25;
if (newAboveBelow !== $boxAboveBelow) {
$boxAboveBelow = newAboveBelow;
if ($boxAboveBelow) {
// If the user scrolls back within 24px of the top (origin), reverse that something once.
} else {
// If user has scrolled more than 24px from the top (origin), do something once.
}
}
})
If you need those to only be called once ever, you can set Boolean variables to record if those conditions have ever been called.
var aboveCalled = false;
var belowCalled = false;
var $box = $('#box');
var $boxAboveBelow = true; // true above, false below
$box.on('scroll', function() { // Throttle this function if needed
var newAboveBelow = $box.scrollTop() < 25;
if (newAboveBelow !== $boxAboveBelow) {
$boxAboveBelow = newAboveBelow;
if ($boxAboveBelow) {
!aboveCalled && doScrollAboveStuff();
aboveCalled = true;
} else {
!belowCalled && doScrollBelowStuff();
belowCalled = true;
}
if (aboveCalled && belowCalled) {
$box.off('scroll'); // No need to keep listening, since both called
}
});

How to allow rewind but disable fast-forwarding in html5-video

I have the following code (using videojs)
this.on("timeupdate", function(event) {
previousTime = this.currentTime();
});
this.on("seeking", function(){
if (this.currentTime() > previousTime){
this.currentTime = previousTime;
}
});
Problem is that, once you try to fast-forward one time, it no longers keeps track of the timeupdate on the progress bar. I can not longer seek on the bar - not even rewind the video. What am I missing here?
First off, these are not standard HTMLMediaElement methods and properties, so I'm assuming you're using some kind of framework, perhaps Popcorn.js? And I'm assuming this refers to the object wrapping your video element.
The problem is that you're overwriting the currentTime method. If you were to reference the video element directly, then you would seek the way you're doing it, by setting the currentTime property, like: videoElement.currentTime = previousTime. But since you're using the framework, this.currentTime is supposed to be a function until you change it to a number.
To demonstrate, modify your code as follows:
this.on("seeking", function(){
console.log(typeof this.currentTime); // 'function'
if (this.currentTime() > previousTime){
this.currentTime = previousTime;
console.log(typeof this.currentTime); // 'number'
}
});
The next time you run the "timeupdate" handler, the call to this.currentTime() will throw an error because it is no longer a function.
You can probably fix it like this (assuming you're using Popcorn or something that works similarly):
this.on("seeking", function(){
if (this.currentTime() > previousTime){
this.currentTime(previousTime);
}
});
You'll also want to make sure that you don't set previousTime while seeking, since "timeupdate" may fire before "seeking" does. I'm not sure how to do that with your framework, so here's a link to a working example using the native HTMLVideoElement API:
http://jsbin.com/kilemoto/1/edit
Just had to implement this, seems to be working nicely
var elem = document.querySelector("video");
let previousTime = 0;
elem.ontimeupdate = function() {
setTimeout(() => {
previousTime = elem.currentTime;
}, 1000)
}
elem.onseeking = function() {
if (elem.currentTime > previousTime) {
elem.currentTime = previousTime;
}
}

Why is clearTimeout not working for this code?

I call the following code on mouse click:
clearTimeouts();
var inMotion = true, x = 0;
var shuffleTimer = setTimeout(function(){inMotion = false}, 3500);
var shuffleStart = setTimeout(oneShuffle, x);
function oneShuffle(){
x+=5;
if(inMotion === true){
console.log('Shuffling again');
//shuffle again
shuffleStart = setTimeout(oneShuffle, x);
} else {
//increment spins
spins++;
//reset spins if loadOrder been exhausted
spins === loadOrder.length ? spins = 0 : 0;
console.log(spins);
}
}
function clearTimeouts(){
console.log('Clearing timeouts')
clearTimeout(shuffleTimer);
clearTimeout(shuffleStart);
}
What should be happening is if I click the elment while inMotion is true the two timeouts should reset and my spins counter should not increase for the previous click. However, what happens instead is that spins gets incremented for the previous timeouts anyway. Why?
What timers should reset? If the code you posted is in a click handler, then each click produces new timers.
The lines:
var shuffleTimer = setTimeout(function(){inMotion = false}, 3500);
var shuffleStart = setTimeout(oneShuffle, x);
create new timers each time, so the first line (clearTimeouts();) makes no sense since the timers don't exist until next two lines.
You should put both timers outside the scope of a click handler, so all click invocations would reference same timers. Also all state (inMotin, spins, etc.) should exist outside the function scope, otherwise each click produces new, unrelated variables.
You should put clearTimeouts() inside your if, like this:
if(inMotion === true){
clearTimeouts();
console.log('Shuffling again');
//shuffle again
shuffleStart = setTimeout(oneShuffle, x);
}

Categories

Resources