Shniper
Shniper

Reputation: 854

Using setTimeout to force delays between creating new elements

I'm trying to make a p then have a delay, then make another p my battle function works until I try to add setTimeout. I've tried using setTimeout just about everywhere to no effect. Sometimes it creates an infinite loop, and in some places it can make a delay before it creates every p at the same time.

The effect should be similar to this Click Attack Button

I tried using the same method as this codepen's creator but I don't understand how his code works.

Here is the Demo

function timeout() {
  setTimeout(function() {

  }, 500);
}

var battle = function() {

  while (monsterHP > 0) {

    var playerDam = Math.floor(Math.random() * ((playerAtk - monsterAtk) + 2));

    var newP = $('#battle').append("<p>You have hit the monster for " + playerDam + " damage. The monster has " + (monsterHP - playerDam) + "HP left</p>");
    monsterHP -= playerDam;
    timeout();
    if (monsterHP <= 0) {
      $('#battle').append("<p>You have defeated the monster</p>");

    }
  }

}


$('#battleButton').click(function() {
  battle();
});

Upvotes: 0

Views: 91

Answers (4)

Michael Geary
Michael Geary

Reputation: 28870

The accepted solution starts a number of setTimeout() calls in parallel. In your case it will tend to be less than a dozen concurrent timers - although there's no guarantee how many timers it will start because of the random numbers.

In most cases it's better to have each timeout start the next one, instead of firing them up all at once like this. For example, imagine that you need to report periodic progress on a much longer running process than this, or a process that keeps running until the program exits? You would need a very large number of concurrent timeouts.

Here's a version of your code with minimal changes from your original and uses only a single setTimeout() at any moment, to avoid this problem.

The way it works is simple: it doesn't use a loop at all, and instead battle() calls timeout() if it needs another run. timeout() in turn passes battle as the parameter to setTimeout(), so battle() will be called when the time elapses.

I marked the changes with //** for you:

var playerAtk = 5;
var playerDef = 5;
var playerHP = 10;
var monsterAtk = 4;
var monsterDef = 4;
var monsterHP = 8;

function timeout() {
  //** Your original setTimeout call didn't do anything at all.
  //** setTimeout isn't a "delay and then return when the delay is done".
  //** Instead, it returns immediately, and the function you pass into it
  //** will be run after the time elapses.
  //** Here we pass battle, so that function is called after the timeout.
  setTimeout(battle, 500);
}

var battle = function() {

  //** This is now just an if instead of a while. It doesn't loop at all here.
  //** Instead, when we want to run another iteration, the setTimeout() call
  //** takes care of that by calling battle again.
  //** In fact, if monsterHP always starts positive, you don't need this
  //** if statement at all.
  if (monsterHP > 0) {

    var playerDam = Math.floor(Math.random() * ((playerAtk - monsterAtk) + 2));

    var newP = $('#battle').append("<p>You have hit the monster for " + playerDam + " damage. The monster has " + (monsterHP - playerDam) + "HP left</p>");
    monsterHP -= playerDam;
    if (monsterHP <= 0) {
      $('#battle').append("<p>You have defeated the monster</p>");
    } else {
      //** The final change: if monsterHP is still > 0, we call timeout()
      //** here to start the next battle call after the interval elapses.
      timeout();
    }
  }

}


$('#battleButton').click(function() {
  battle();
});

Working fiddle

Upvotes: 0

Fizz
Fizz

Reputation: 3487

You could alter your battle function a bit and instead of using a while-loop, simply let the setTimeout() call loop it for you.

The following is an example of my modifications to your linked fiddle:

var playerAtk = 5;
var playerDef = 5;
var playerHP = 10;
var monsterAtk = 4;
var monsterDef = 4;
var monsterHP = 8;

var battle = function() {
  if (monsterHP > 0)
  {
    var playerDam = Math.floor(Math.random() * ((playerAtk - monsterAtk) + 2));
    var newP = $('#battle').append("<p>You have hit the monster for " + playerDam + " damage. The monster has " + (monsterHP - playerDam) + "HP left</p>");
    monsterHP -= playerDam;
    setTimeout(function() {
      battle();
    }, 1000);
  }
  else {
    $('#battle').append("<p>You have defeated the monster</p>");
  }
}


$('#battleButton').click(function() {
  battle();
});

You can see my JSFiddle here

Upvotes: 1

Damon
Damon

Reputation: 4346

The problem you are having is with your implementation of timeout. As tymeJV mentioned in the comments, setTimeout is async. Your code will not stop what it is doing to complete the timeout. Instead you have to pass a callback to be completed upon the time being elapsed (in your case appending the element).

Try this code snippet in your console to better illustrate this:

setTimeout(function() {
  console.log('hello from callback');
}, 3000)

console.log('hello from outside');

As you can see the later code gets run first even though it is after the timeout.

Upvotes: 0

dandavis
dandavis

Reputation: 16726

i tried explaining in a comment, but it won't fit because there's a couple things going on here. it's easier just to fix it and talk about the new code:

var battle = function() {
  var i=0;
  while (monsterHP > 0) {
    (function(hp){

    var playerDam = Math.floor(Math.random() * ((playerAtk - monsterAtk) + 2));
    monsterHP-= playerDam;

   hp= monsterHP; // this might or might not be needed.

   setTimeout(function(){
    var newP = $('#battle').append("<p>You have hit the monster for " + playerDam + " damage. The monster has " + (hp - playerDam) + "HP left</p>");
    if (hp<= 0)$('#battle').append("<p>You have defeated the monster</p>");
   }, i++ * 500);


   }(monsterHP));
  }
}

here i've moved the computations to the top of the loop, then defered the visible action of the computations behind a setTimeout for 1/2 second.

you needed private copies of the vars altered by the loop, and you needed a timeout that could use those values.

Upvotes: 1

Related Questions