I wanted to try out creating a proof-of-concept of actors that have their own independent loops, outside of the main loop - I created something like that, but I'd like to know if there are some glaring issues, or if I am doing it completely wrong.
Basically I'd like to know if the right way to handle the "internal" loop would be using this, or if there is a better way of doing it (inside the live() function):
setTimeout(() => {this.live()}, 100);
Second question would be to know the best way of destroying an instantiated class, withing the class, with something like "this.destroy()" - right now I am just removing the connection from the container to the object
Example here: https://codepen.io/tommica/pen/qmNXYL
I'll paste the code itself too:
<ul id="simple-log"></ul>
<script>
// We will store out actors as "id" => "actor"
let actors = {};
// Custom logging functionality for a simple ul list
const sLog = (text) => {
let li = document.createElement('li');
li.innerHTML = text;
document.getElementById('simple-log').appendChild(li);
};
const randomNum = (min,max) => { return Math.floor(Math.random() * max) + min; }
// Actor definition
class Actor {
constructor(name, id) {
this.id = id;
this.name = name;
this.gender = randomNum(1,2) === 1 ? 'male' : 'female'; // Random gender
this.lastTime = null;
}
live() {
// Get the current time, and log every 5 seconds
let now = Date.now();
let difference = now - this.lastTime;
if(difference > 5000) {
sLog(`Actor "${this.name}" Log - Difference: ${difference}`);
this.lastTime = now;
}
// Determine if the actor died of a tragic accident
if(randomNum(1,100000) < 5) {
// Something tragic happened, that caused this actor to die
this.die();
} else {
// I'm pretty sure that this is not the best way, but for some reason just
// setTimeout(this.live, 1); does not work
setTimeout(() => {this.live()}, 100);
}
}
die() {
// Log the death
sLog(`Actor "${this.name}" died`);
// This seems really a wrong way to do this, should not need to rely on an element outside of the scope - something else should do this, but how?
delete actors[this.id];
}
}
// Function to spawn a new actor
let spawnActor = () => {
let id = 'a'+randomNum(1,9000000);
sLog('Spawning an actor');
let actorInstance = new Actor(id, id); // Rejoice!
actorInstance.live();
actors[id] = actorInstance;
}
// Simple loop that simulates the rendering of the screen
let lastTimeAnimated = null;
let mainAnimationLoop = () => {
// Logs every 5 seconds to the log
let now = Date.now();
let difference = now - lastTimeAnimated;
if(difference > 5000) {
sLog(`Main Animation Loop Log - Difference: ${difference}`);
lastTimeAnimated = now;
}
window.requestAnimationFrame(mainAnimationLoop);
}
// Simple loop that simulates a normal game main loop
let lastTime = null;
let mainLoop = () => {
// Mainloop logs every 5 seconds to the log
let now = Date.now();
let difference = now - lastTime;
if(difference > 5000) {
sLog(`Main Loop Log - Difference: ${difference}`);
lastTime = now;
}
// Random actor spawner
if(randomNum(1,10000) < 5) {
spawnActor(); // It truly is a blessed day!
}
setTimeout(mainLoop, 1);
}
// Should be obvious
let init = () => {
mainAnimationLoop();
mainLoop();
}
// Let's get started!
init();
</script>
Basically I'd like to know if the right way to handle the "internal" loop would be using this, or if there is a better way of doing it (inside the live() function): setTimeout(() => {this.live()}, 100);
There are many other ways to do this (some of them even involving a real while loop), but none of them is "the one right way".
I'm pretty sure that this is not the best way, but for some reason just
setTimeout(this.live, 1); does not work
See How to access the correct this / context inside a callback? for the why.
Second question would be to know the best way of destroying an instantiated class, withing the class, with something like "this.destroy()" - right now I am just removing the connection from the container to the object:
delete actors[this.id];
This seems really a wrong way to do this, should not need to rely on an element outside of the scope - something else should do this, but how?
You cannot "destroy" anything in javascript. If you want an instance to get garbage-collected, you need to remove all references to it. The right way to let an actor die is to just let it stop living - i.e. don't call .live() any more, and/or remove all timeouts that are scheduled to call it.
You don't need that container for anything (and in the code you've shown, you're not even using it). For some reason, spawnActor did store the instances, so it is its job to collect the dead. If you really don't need that collection, just omit it; if you use if for something then each actor should announce its death by setting an own property or by sending a message to the main actor, so that it can be removed from the collection as appropriate.
Related
Say I have a queue class that's executing a series of functions I've already declared:
class DrawQueue{
constructor(interval){
this.sequence = [];
this.interval=interval?interval:50;
}
addFunction=(fn)=>{
this.sequence.push(fn);
//throw exception here if not a function
};
execFunctions = ()=>{
let intvl = setInterval(
()=>{
const fn = this.sequence.shift();
//clear interval & return here if not a function
fn.call();
},
this.interval
)
}
}
Now I want to pass it a series of functions that have some values calculated inside them:
//I have a single count variable here but the code I'm running is being generated by a user who might have any number of variables that are being updated
let count = 0;
let counterDiv = document.querySelector('#counter')
let dq = new DrawQueue(1000);
function startCount(){ //call when window's loaded
let countFn=(()=>
{
let innerFn= function(){
let str = (function(){
return count.toString()
})();
counterDiv.innerHTML=str;
}
//imagine that any number of count variables might be being updated somewhere in the function
count++;
dq.addFunction(innerFn);
})
while(count<10){
countFn();
}
dq.execFunctions();
}
Right now this immediately sets the counter div to 10, and then keeps setting it to 10 ten more times. But I want to assign the str variable's value before I pass the functions. So the first function I pass sets the counter to 1, then 2 and so on.
I was trying to set the let str= function(... using an iife, but that didn't work.
One solution that I know would work is to make the whole function a string and then run it with eval but I really do not want to use eval unless I absolutely have to.
Is there any other way to pass these functions with certain variables already "locked in", meaning they're assigned before the function is placed in the queue?
UPDATE: To clarify, this is just a simplified version of a more complex example. In the actual example, the code is dynamically generated by another user, so in addition to 'count' any number of other values might need to be evaluated. So passing the count variable, as several good answers have suggested, is not going to work.
FURTHER CLARIFICATION: What I'm saying above is that because the user could be generating any number of variables that will be updated as the code runs, I can't pass those variables as arguments. (imagine there might be a count2, count3...countn and I don't know how many or where they'll be used or updated in the code.
FURTHER UPDATE: so a commenter wants to see the code in which this applies so here goes. It is an application using Blockly and P5 play, where users will be making code with blocks to move a sprite. So the code for the blocks might be something like this (yes this code is really ugly because it's just a test, but you asked to see it):
function moveForward(sprite){
let dir = ship.rotation* (Math.PI / 180);
let deltaX = Math.cos(dir)*5;
let deltaY = Math.sin(dir)*5;
let newX = ship.position.x + deltaX;
let newY = ship.position.y + deltaY;
ship.position.x=newX;
ship.position.y=newY;
redraw();
}
function moveBackward(sprite){
let dir = ship.rotation* (Math.PI / 180);
let deltaX = Math.cos(dir)*5;
let deltaY = Math.sin(dir)*5;
let newX = ship.position.x - deltaX;
let newY = ship.position.y - deltaY;
ship.position.x=newX;
ship.position.y=newY;
redraw();
}
function turnLeft(sprite){
let newDir=ship.rotation-90;
ship.rotation=newDir;
redraw();
}
function turnRight(sprite){
let newDir=ship.rotation+90;
ship.rotation=newDir;
redraw();
}
There will be any number of other sprites, each with 20 or so properties that could be updated.
Now if I just put all these functions in a row, the sprite will just immediately jump to where the code would put it. Because, you know, normally we want computers to do things as fast as they can.
But since this is made for teaching, I want the user to see the canvas updating step by step, with a delay between each redraw. That means every sprite will have its x and y coordinates, along with color and rotation and a bunch of other things, change slowly.
So the purpose of the DrawQueue to execute the drawing update steps slowly with a setInterval and update the canvas at any interval I want. I can't just run every single command with a setInterval because there could be logic or loops in there. The only thing I want to go in the interval is the updates to the canvas, anything else can happen as fast as it wants.
So imagine the four functions I provided above, along with any number of other functions and modifications to the properties of any number of other sprites or properties of the canvas.
The problem you have is the value is not stored at the time you make the function. It is just a reference to a variable that you are updating. So when it calls, it is reading that variable.
You would need to pass it into the method so you can store the state of the variable at that moment in time.
let count = 0;
let counterDiv = document.querySelector('#counter')
let dq = new DrawQueue(1000);
function startCount(){ //call when window's loaded
let countFn=((count)=> . // <-- reference it here
{
let innerFn= function(){
let str = (function(){
return count.toString()
})();
counterDiv.innerHTML=str;
}
dq.addFunction(innerFn);
})
while(count<10){
countFn(count++); // <-- update it here
}
dq.execFunctions();
}
By the time the innerFn is actually called, the count variable has already increased to its final value.
To give each innerFn instance its own value for count, you could bind it as function argument:
let innerFn = function(count) { //<--- argument
let str = (function(){
return count.toString()
})();
counterDiv.innerHTML=str;
}.bind(null, count); // pass the global count into a bound argument
NB: make sure to check in your class that fn is defined (as the array will become empty at some point).
class DrawQueue{
constructor(interval){
this.sequence = [];
this.interval=interval?interval:50;
}
addFunction(fn){
this.sequence.push(fn);
//throw exception here if not a function
};
execFunctions(){
let intvl = setInterval(
()=>{
const fn = this.sequence.shift();
//clear interval & return here if not a function
if (fn) fn.call();
},
this.interval
)
}
}
let count = 0;
let counterDiv = document.querySelector('#counter')
let dq = new DrawQueue(1000);
function startCount(){ //call when window's loaded
let countFn=(()=>
{
let innerFn= function(count){
let str = (function(){
return count.toString()
})();
counterDiv.innerHTML=str;
}.bind(null, count);
count++;
dq.addFunction(innerFn);
})
while(count<10){
countFn();
}
dq.execFunctions();
}
window.onload = startCount;
<div id="counter"></div>
Even better would be to avoid a reference to a global variable, and pass count to the countFn function as parameter:
let counterDiv = document.querySelector('#counter')
let dq = new DrawQueue(1000);
function startCount(){ //call when window's loaded
let countFn=((count)=> // local variable
{
let innerFn= function(){
let str = (function(){
return count.toString()
})();
counterDiv.innerHTML=str;
}
dq.addFunction(innerFn);
})
for(let count = 0; count<10; count++){ // local variable
countFn(count); // pass it
}
dq.execFunctions();
}
Addendum
In your question's update you speak of more variables. In that case, pass an object around, which can have many properties, possibly even managed completely by the user-provided code:
let counterDiv = document.querySelector('#counter')
let dq = new DrawQueue(1000);
function startCount(){ //call when window's loaded
let countFn=((state)=> // local variable with count property
{
let innerFn= function(){
let str = (function(){
return state.count.toString()
})();
counterDiv.innerHTML=str;
}
dq.addFunction(innerFn);
})
for(let count = 0; count<10; count++){ // local variable
const state = {};
state.count = count;
countFn(state); // pass it
}
dq.execFunctions();
}
Depending on your expectations you should either use the same state object or create new state variables (within the loop, or even deeper in the execution context). This all depends on how you want the system to behave.
so I'm having a problem with scope in javascript. I'm currently writing a little js app to allow me to create console-based(or looking) games on my website super quickly and have most of my utility and specific console-application functions stored within variables.
The problem occurs when I wanna add a "setTimeout" or Interval function and want to use my variable functions. I know about proxy but theres gotta be a better way than calling $.proxy every time I wanna refer to one of my functions, and calling proxy for everything im referring to WITHIN those functions.
jQuery(document).ready(function(){
let gameStart = $.proxy(game.start, game);
setTimeout(gameStart, 1000);
});
let options = {
"consoleOutputDiv":"#console-output",
"thisIsHowIFormatText":"something"
};
let utils = {
formattxt: function(str){
let formatted = str;
let toReplace = options.thisIsHowIFormatText;
//I need to refer to options.thisIsHowIFormatText now and thats not possible.
//format text here!
return formatted;
}
}
let consolApp = {
log: function(str){
let stringToBeLogged = str;
//ok so now I need to refer to formattxt which refers to options.thisIsHowIFormatText
let format = $.proxy(utils.formattxt, utils, str);
stringToBeLogged = format();
//print stringToBeLogged to my console div.
}
}
let game = {
start: function() {
let consol = $.proxy(consolApp.log, consolApp, 'please log me!');
consol();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='console-output'></div>
I just think there's gotta be a better way! That gets tedious and just looks gross to me constantly calling $.proxy everywhere to allow for my functions to work.
Here's a small OOP suggestion for your code, perhaps just enough to give you an idea of how this kind of app might be structured:
When the window is loaded a new game is created, after a 1 second timeout.
The game creates a console when it is started.
The console uses a static utils method.
class Utils {
static format(str){
let formatted = str + "!!!"
return formatted;
}
}
class Console {
log(str){
let stringToBeLogged = Utils.format(str);
console.log(stringToBeLogged)
}
}
class Game {
start() {
let consol = new Console()
consol.log('please log me...');
}
}
window.addEventListener("load", () => {
setTimeout(()=>{
let g = new Game()
g.start()
}, 1000)
})
I have created the following click count factory for adding the click count to the event information already present in a regular click event. This function creates a clickCountObj to track the number of clicks, as well as a new function for catching a click event on the given element parameter and reporting it back to the listener parameter along with the click count.
Originally, I wanted to do this as a class, rather than a factory... Way back when I was working in Java, I would have done it with a façade class, so that's what I was thinking. But I've concluded that it is not possible in Javascript, because the same function you'd use to create the object would be the one called in response to the click, and I can't see a way around this.
The purpose of this question is simply to improve my understanding and using of JavaScript. Please let me know if I am wrong in my conclusion stated above, or if there are any other alternatives to doing this a better way?
function clickCount(element, listener) {
let clickCountObj = {};
clickCountObj.clickCount = 0;
clickCountObj.clickDelay = 500;
clickCountObj.element = element;
clickCountObj.lastClickTime = 0;
let clickCountListener = function (e) {
// alert("last click time: " + clickCountObj.clickDelay);
if ((e.timeStamp - clickCountObj.clickDelay) < clickCountObj.lastClickTime) {
clickCountObj.clickCount = clickCountObj.clickCount + 1;
// alert("click count up: " + clickCountObj.clickCount);
}
else {
clickCountObj.clickCount = 1;
}
clickCountObj.lastClickTime = e.timeStamp;
listener.call(element, clickCountObj.clickCount, e);
};
if (!element) throw "No element to listener to";
element.addEventListener("click", clickCountListener);
return clickCountListener;
}
For sure you can also use a class:
class ClickCounter {
constructor(element, onClick, delay = 500) {
this.element = element;
this.onClick = onClick;
this.counter = 0;
this.delay = delay;
this.lastClicked = 0;
element.addEventListener("click", () => this.click(), false);
}
click() {
if(Date.now() < this.lastClicked + this.delay)
return;
this.lastClicked = Date.now();
this.onClick.call(this.element, this.counter++);
}
}
new ClickCounter(document.body, count => {
alert(count);
});
[is] doing this a better way?
No, not really. Using a class is not really useful here as you don't want to expose properties and you also don't need inheritance. A factory seems to be a good approach here.
Small sidenote: Instead of
return clickCountListener;
it would make more sense to
return clickCountObj;
as it would expose the settings and the count which might be useful.
warning: unserious content below
Way back when I was working in Java ...
... you took over that senseless naming scheme (clickCountObj.clickCount). I guess you won't loose any necessary information with just settings.count ...
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.
Writing some code, and when creating an instance of a class, something strange happens with an integer variable I have:
function Mat(x, y, spawner) {
this.x = x;
this.y = y;
this.val = 1;
this._spawner = spawner;
this.newborn = true;
this.bornTime = 0;
this.spawnTimer = setInterval("this.bornTime++; console.log(this.bornTime);", 1000);
}
Pretty cut and clear code; every second after an instance of the variable is created, it should increment the bornTime variable by 1 and log it.
Mat.prototype.update = function() {
if (this.bornTime >= 5) {
this.bornTime = null;
clearInterval(this.spawnTimer);
this.newborn = false;
console.log("Grown!");
}
}
This additional code would cause this instance to be "grown" after 5 seconds, however when I check the console, it reads that bornTime is not a number(NaN).
Why is this, and is there a solution that I am not seeing?
this inside the setTimeout code is not the same as outside (more info on MDN), so your code is actually calculating undefined++, which is NaN.
You have to create another variable, and pass a function to setTimeout instead of letting it eval a string (by the way, passing a function is supposed to be faster, and looks better):
var that = this;
this.spawnTimer = setInterval(function(){
that.bornTime++;
console.log(that.bornTime);
}, 1000);
I know this is 5 years old question but its 2018 and heres an Es6 syntax solution to avoid extra step of binding key word this.
this.spawnTimer = setInterval(() => {
this.bornTime++;
console.log(this.bornTime);
}, 1000);