Understanding JavaScript setTimeout and setInterval - javascript

I need a bit of help understanding and learning how to control these functions to do what I intend for them to do
So basically I'm coming from a Java background and diving into JavaScript with a "Pong game" project. I have managed to get the game running with setInteval calling my main game loop every 20ms, so that's all ok. However I'm trying to implement a "countdown-to-begin-round" type of feature that basically makes a hidden div visible between rounds, sets it's innerHTML = "3" // then "2" then "1" then "GO!".
I initially attempted to do this by putting setTimeout in a 4-iteration for-loop (3,2,1,go) but always only displayed the last iteration. I tried tinkering for a bit but I keep coming back to the feeling that I'm missing a fundamental concept about how the control flows.
I'll post the relevant code from my program, and my question would be basically how is it that I'm writing my code wrong, and what do I need to know about setTimeout and setInterval to be able to fix it up to execute the way I intend it to. I'm interested in learning how to understand and master these calls, so although code examples would be awesome to help me understand and are obviously not unwelcome, but I just want to make it clear that I'm NOT looking for you to just "fix my code". Also, please no jQuery.
The whole program would be a big wall of code, so I'll try to keep it trimmed and relevant:
//this function is called from the html via onclick="initGame();"
function initGame(){
usrScore = 0;
compScore = 0;
isInPlay = true;
//in code not shown here, these objects all have tracking variables
//(xPos, yPos, upperBound, etc) to update the CSS
board = new Board("board");
ball = new Ball("ball");
lPaddle = new LPaddle("lPaddle");
rPaddle = new RPaddle("rPaddle");
renderRate = setInterval(function(){play();}, 20);
}
.
function initNewRound(){
/*
* a bunch of code to reset the pieces and their tracking variables(xPos, etc)
*/
//make my hidden div pop into visibility to display countdown (in center of board)
count = document.getElementById("countdown");
count.style.visibility = "visible";
//*****!!!! Here's my issue !!!!*****//
//somehow i ends up as -1 and that's what is displayed on screen
//nothing else gets displayed except -1
for(var i = 3; i >= 0; i--){
setInterval(function(){transition(i);}, 1000);
}
}
.
//takes initNewRound() for-loop var i and is intended to display 3, 2, 1, GO!
function transition(i){
count.innerHTML = (i === 0) ? "Go" : i;
}
.
//and lastly my main game loop "play()" just for context
function play(){
if(usrScore < 5 && compScore < 5){
isInPlay = true;
checkCollision();
moveBall();
moveRPaddle();
if(goalScored()){
isInPlay = false;
initNewRound();
}
}
}
Thanks a bunch for your advise, I'm pretty new to JavaScript so I really appreciate it.

Expanding on cookie monster's comment, when you use setInterval in a loop, you are queueing up method executions that will run after the base code flow has completed. Rather than queue up multiple setInterval executions, you can queue up a single execution and use a variable closure or global counter to track the current count. In the example below, I used a global variable:
var i = 3 // global counter;
var counterInterval = null; // this will be the id of the interval so we can stop it
function initNewRound() {
// do reset stuff
counterInterval = setInterval(function () { transition() }, 1000); // set interval returns a ID number
}
// we don't need to worry about passing i, because it is global
function transition() {
if (i > 0) {
count.innerHTML = i;
}
else if (i === 0) {
count.innerHTML = "Go!";
}
else {
i = 4; // set it to 4, so we can do i-- as one line
clearInterval(counterInterval); // this stops execution of the interval; we have to specify the id, so you don't kill the main game loop
}
i--;
}
Here is a Fiddle Demo

The problem is in this code:
for(var i = 3; i >= 0; i--){
setInterval(function(){transition(i);}, 1000);
}
When the code runs, it creates a new function 3 times, once for each loop, and then passes that function to setInterval. Each of these new functions refers to the variable i.
When the first new function runs it first looks for a local variable (in it's own scope) called i. When it does not find it, it looks in the enclosing scope, and finds i has the value -1.
In Javascript, variables are lexically scoped; an inner function may access the variables defined in the scope enclosing it. This concept is also known as "closure". This is probably the most confusing aspect of the language to learn, but is incredibly powerful once you understand it.
There is no need to resort to global variables, as you can keep i safely inside the enclosing scope:
function initNewRound(){
var i = 3;
var count = document.getElementById("countdown");
count.style.visibility = "visible";
var interval = setInterval(function(){
//this function can see variables declared by the function that created it
count.innerHTML = i || "Go"; //another good trick
i-=1;
i || clearInterval(interval); //stop the interval when i is 0
},1000);
}
Each call to this function will create a new i, count and interval.

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 make private or local variables?

So I checked a thread on here about global and local variables but didn't really find a solution to my problem. I just want a private or local variable to increment so that a function only fires once. I'll paste what I'm trying to achieve here any help would be much appreciate also please go easy on me I'm brand new to JavaScript. This code works but the variable I seems to be shared between functions.
function phonefun(){
i++;
console.log(i);
wage = wage - phone;
console.log(wage);
display();
document.getElementById('phone').style.backgroundColor = "darkgrey";
}
function waterfun(){
i++;
console.log(i);
wage = wage - water;
console.log(wage);
display();
document.getElementById('water-aid').style.backgroundColor = "darkgrey";
}
...the function is called on the click of a button, I want it so you
can only press the button
I think what you want to do is have your event handler unbind from the button after if fires. Thas is much better solution than counting how many times it's been clicked. Check out this link for how to bind and unbind event handlers using "vanilla" JS: https://plainjs.com/javascript/events/binding-and-unbinding-of-event-handlers-12/
In reference to your earlier questions...
A variable created inside of a function is said to be "scoped" to that function, which means that nothing outside of that function can access the variable. However, by initializing your variable without using the var or let keyword (the latter is ES6 syntax), you created an implicit global. This means that you inadvertently made it a global variable when you wanted it to be function-scoped.
Declaring a variable does not automatically assign a value of zero. If you do not assign a value, the value will be undefined.
If you had declared / assigned the variable thusly,var i = 0; or let i = 0; you would have had a properly scoped variable with an initial value of 0. The problem is, each time that function executed, the value would be reset to zero. To get the value to "stick" you would have to create state. You could do that by creating an object with getter and setter methods or by using a closure. However, the unbind solution seems to be the best way to go for what you want to do here.
I hope this helps.
To do what you want, you need a variable with a higher scope than the function so that the value can persist between function calls. A local variable will be garbage collected as the function returns and so, your counter would be lost.
var counter = 0; // This variable exists in a higher scope than the function
function loanfun(){
counter++;
if (counter == 1) {
console.log("function has run " + counter + " times.");
}
}
loanfun(); // Will run
loanfun(); // Won't run
you can make a class
function myStuff(){
this.i = 0,
this.loanfun = function(){
this.i++;
if (this.i == 1) {
wage = wage - loan;
console.log(wage);
display();
document.getElementById('loan').style.backgroundColor = "darkgrey";
}
}
}
var s = new myStuff();
s.loanfun();
s.loanfun();
You could try namespacing within an object:
var PageModule = {
count: 0,
loadfun: function (wage, loan) {
PageModule.count += 1;
if (PageModule.count === 1) {
console.log('execute!');
wage = wage - loan;
console.log(wage);
display();
document.getElementById('loan').style.backgroundColor = "darkgrey";
}
}
};
PageModule.loadfun();
PageModule.loadfun();
PageModule.loadfun();
// if you want to attach the method to a button
document.getElementById('my-btn-id').addEventListener('click', PageModule.loadfun);
Alternatively, you could use the following approach:
function myclickhandler () {
// do whatever you want here ...
//remove handler from button, so that the next button clicks will not do anything
document.getElementById('my-btn-id').removeEventListener('click', myclickhandler);
}
// attach the method to a button
document.getElementById('my-btn-id').addEventListener('click', myclickhandler);
Hope that this is what you want to do.But if you want simply to call(invoke) you function once just call and it will be executed only one time.
wage = 10;
loan = 5;
i=0; //this is the global variable
function loanfun(){
let j = i +1; //j is local variable
if (j === 1) {
wage = wage - loan;
console.log(wage);
//display();
document.getElementById('loan').style.backgroundColor = "darkgrey";
}
}
loanfun(); //invoke the function here
<div id="loan">
hi I am here working as expected
</div>

"Class" continues working without being referenced globally

I made an image slider "class" and originally instantiated it as:
var foo = new Slider(document.getElementById("featuredSlider"), 900);
I tried removing var foo = and it continues to work which was surprising to me. What makes it continue working? Is it a bad idea to not reference it globally?
On a side note, "featuredSlider" is the id of a <div> tag. It contains some number of <a> tags and each contain <img> tags.
function Slider(inElement, inStep) {
if (!inElement) return;
this.element = inElement;
this.start = 0;
this.end = 0;
var self = this;
var limit = inElement.getElementsByTagName("a").length*inStep;
setInterval(function() {
self.start = self.end;
self.end = (self.end+inStep)%limit;
self.animate(this.start < this.end ? 1 : -1);
}, 3000);
}
Slider.prototype.animate = function(inZeno) {
this.start += ((this.end-this.start)>>2)+inZeno;
this.element.style.left = this.start+"px";
if (this.start !== this.end) {
var self = this;
setTimeout(function() {
self.animate(self.start < self.end ? 1 : -1);
}, 33);
}
}
//new Slider(document.getElementById("featuredSlider"), 900);
var foo = new Slider(document.getElementById("featuredSlider"), 900);
I tried removing var foo = and it continues to work which was surprising to me. What makes it continue working?
Because references to it are kept by the timer system, because it's set up callbacks to functions (closures) that reference it via setInterval and setTimeout. So references to the necessary parts of it are kept around as long as those timers are still held.
The function it's giving to setInterval here:
setInterval(function() {
self.start = self.end;
self.end = (self.end+inStep)%limit;
self.animate(this.start < this.end ? 1 : -1);
}, 3000);
is a closure over the call to Slider. So all of the things in scope within the closure are kept around as long as the closure (function) is around. Since that timer is never cancelled, the timer system keeps those references alive. (If "closure" is unfamiliar or only slightly familiar, don't worry: Closures are not complicated.)
Is it a bad idea to not reference it globally?
No, that's fine. If it keeps working and you don't need the reference, there's no need to store a reference to it. This was quite a common pattern back when a lot of people used script.aculo.us, which used new for about half of its effects.

JavaScript - Function Not Modifying Global Variable

I'm having trouble modifying a variable within a text adventure game I'm writing code for in JavaScript. I want to change the value of a global variable using a function within a function (no, not closure).
/*This is just example code.*/
var health = 100;
var exp = 0;
function refreshStats() {
health -= 10;
exp += 1;
}
function foo(flag) {
if (flag == DONOTHING) {
refreshStats();
}
if (health <= 0) {
say("You died, bwahaha.");
}
if ((exp/10) == Math.floor(exp/10)) {
health += 10;
say("You leveled up!");
}
}
How the game works is that each function (defined as actions or areas within the game) will be called by buttons and forms placed by JavaScript that the user can click or write in, respectively. I need refreshStats() to update the health and exp variables so foo() can use them correctly; the function doesn't seem to be updating the variables until after foo() runs. I do wonder if it's a browser compatibility issue, which really would tick me off, but I'm hoping it isn't that.
Any suggestions?
Store your values into your window object, so it will be available in any scope of that window:
//take care to not overwrite native properties of the window
window.health = 100;
window.exp = 0;
function refreshStats() {
health -= 10;
exp += 1;
}
It looks like it's working for me. Perhaps it's a scoping issue - instead of example code, could you post your actual relevant code?

How to Add Event Handler with Arguments to an Array of Elements in Javascript?

I have a three-step process that is entirely reliant upon JavaScript and Ajax to load data and animate the process from one step to the next. To further complicate matters, the transition (forward and backward) between steps is animated :-(. As user's progress through the process anchor's appear showing the current step and previous steps. If they click on a previous step, then it takes them back to the previous step.
Right now, the entire process (forward and backward) works correctly, if you begin at step 1, but if you jump straight to step 3 then the anchors for step 1 and step 2 also perform the same action as step 3.
This is the portion of the code that loops through all of the steps up to the current step that the user would be on and displays each anchor in turn and assigns the appropriate function to the click event:
for (var i = 0; i < profile.current + 1; i++) {
if ($('step_anchor_' + i).innerHTML.empty()) {
var action = profile.steps[i].action;
var dao_id = profile.steps[i].dao_id;
$('step_anchor_' + i).innerHTML = profile.steps[i].anchor;
$('step_anchor_' + i).observe('click', function(){
pm.loadData(action, dao_id, true);
});
Effect.Appear('step_anchor_' + i, {
duration: 1,
delay: (down_delay++)
});
}
}
I know that problem lies in the way that the action and dao_id parameters are being passed in. I've also tried passing profile.steps[i].action and profile.steps[i].dao_id but in that case both profile and i or at least i are out scope.
How do I make it so that I can assign the parameters for action and dao_id correctly for each step? (If it makes any difference we are using Prototype and Scriptaculous)
Your closure scope chain is causing your problems. By declaring the handler function inline, you've created a closure. Obviously you did this to take advantage of the loop.
However, since you have created a closure, you're playing by closure scoping rules. Those rules state that the local variables within the parent function remain active and available as long as the closure exists.
You are trying to pass and then use "action" and "dao_id" to your closure, but you are passing references here, not values. So when your closures (handlers) are called they use the value that the reference was last assigned. In your case, the Step 3 handler.
Closure scoping rules are confusing enough, but you may also be confused by the fact that "action" and "dao_id" are still alive even though the loop block has finished executing. Well, in JavaScript there is no such thing as block scope. Once you declare a variable it is available until the end of the function or until is it deleted. Whichever comes first.
All that said, you need to break the scope chain. Here are two ways to do that:
Try this:
for (var i = 0; i < profile.current + 1; i++) {
if ($('step_anchor_' + i).innerHTML.empty()) {
var action = profile.steps[i].action;
var dao_id = profile.steps[i].dao_id;
$('step_anchor_' + i).innerHTML = profile.steps[i].anchor;
$('step_anchor_' + i).observe('click', function(a, b){
return function(){pm.loadData(a, b, true)};
}(action, dao_id));
Effect.Appear('step_anchor_' + i, {
duration: 1,
delay: (down_delay++)
});
}
}
Or this:
function createHandler(action, dao_id) {
return function(){pm.loadData(action, dao_id, true);};
}
/* snip - inside some other function */
for (var i = 0; i < profile.current + 1; i++) {
if ($('step_anchor_' + i).innerHTML.empty()) {
var action = profile.steps[i].action;
var dao_id = profile.steps[i].dao_id;
$('step_anchor_' + i).innerHTML = profile.steps[i].anchor;
$('step_anchor_' + i).observe('click', createHandler(action, dao_id));
Effect.Appear('step_anchor_' + i, {
duration: 1,
delay: (down_delay++)
});
}
}
First, remember your execution scope in the click event. The this keyword in that context refers to the element being clicked on. Is there any way you can determine the dao_id from the element that is clicked on?

Categories

Resources