Phaser IO Update dynamically created text on callback - javascript

I'm currently creating a game using Phaser IO and SignalR (+jQuery),
I get a list of players from server (for now containing an ID and Name), for each player I create a text field, which I later want to manipulate (with the amount of votes the specific player has), however I have no clue on how to reference the dynamically text object.
I'm open to new ideas as well
var game = new Phaser.Game($window.innerWidth, $window.innerHeight, Phaser.Auto, 'gameCanvas');
var dayState = {
preload: function () {
// Preloaded stuff
},
create: function () {
var world = game.world;
// Players alive in game
var players = // Call to Server, retrieves list of players
// Add player
for (var i = 0; i < players.length; i++) {
var currentPlayer = players[i];
// Player name
game.add.text(world.width - 225, y, currentPlayer.Name);
// I WANT TO UPDATE THIS UPON CALLBACK
game.add.text(world.width - 175, y, 0)
// Vote button
game.add.button(world.width - 50, y + 2, //preloaded texture for button, voteFunction, currentPlayer.Id , 2, 1, 0);
}
}
};
game.state.add('DayState', dayState);
game.state.start('DayState');
function voteFunction() {
// Posts vote to server
};
function voteReturnedFromServer(amount){
// Server calls this function (SignalR)
// This is where I want to update text element created above with data from SignalR
// Update text with callback data "amount"
};

You can go ahead and define a variable at the same level as game (for ease), and then set a variable equal to the text you add to the game.
var voteText;
// ...
voteText = game.add.text('world.width - 175, y, 0);
Then simply update the text, if voteText is defined.
voteText.text = 'data returned from the server'

Issue was finding the text after it has been created. I ended up creating an array outside game states and then push the texts into that array.
Then when I needed to edit the text, I'd search the array using grep (since Im already using jQuery)
var game = new Phaser.Game($window.innerWidth, $window.innerHeight, Phaser.Auto, 'gameCanvas');
// This is where I'll push my texts
var voteTexts = [];
var dayState = {
preload: function () {
// Preloaded stuff
},
create: function () {
var world = game.world;
// Players alive in game
var players = // Call to Server, retrieves list of players
// Add player
for (var i = 0; i < players.length; i++) {
var currentPlayer = players[i];
// Player name
game.add.text(world.width - 225, y, currentPlayer.Name);
// I WANT TO UPDATE THIS UPON CALLBACK
var vote = game.add.text(world.width - 175, y, 0)
vote.id = currentPlayer.Id;
voteTexts.push(vote);
// Vote button
game.add.button(world.width - 50, y + 2, //preloaded texture for button, voteFunction, currentPlayer.Id , 2, 1, 0);
}
}
};
game.state.add('DayState', dayState);
game.state.start('DayState');
function voteFunction() {
// Posts vote to server
};
function voteReturnedFromServer(amount){
var textToUpdate = $.grep(voteTexts, function (e) {
return e.id === votes.TargetId;
});
// Since I know there'll be a result and only one, then I use [0]
textToUpdate[0].text = votes.Count;
};

Use built-in method setText()
text.setText(amount);
https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Text.html

Related

Using a while loop to check in a for loop if an array includes a string from another source

Trying to program a flags trivia game. A flag image appears, along with 3 different answers (with only one answer being the correct flag).
I am using the getFlag() function which creates image elements for each gCountries flag, as well as an array (gFlagsProperties) containing an object for each flag which each contains:
A unique id
The src of the image
2 random other flag options and the actual flag option (3 overall options for the user to pick one in the game)
A string of the flag name
I'm trying to use the function render() to ensure that the proper flag image appears on the page - meaning one which one of the options key of the flag object in the gFlagsProperties contains. The while loop infinitely loops and I don't know why.
var gFlagsProperties = [];
var gCountries = ['albania', 'chad', 'colombia', 'cyprus', 'denmark', 'gabon', 'hungary', 'iceland', 'latvia', 'madagascar', 'romania', 'serbia'];
var gElFlagsDiv;
function init() {
render();
}
function getFlag() {
var options = generateOptions();
gElFlagsDiv = document.querySelector('.flags');
for (var i = 1; i <= gCountries.length; i++) {
var flagImg = document.createElement('img');
flagImg.src = 'img/' + i + '-' + gCountries[i - 1] + '.gif';
flagImg.setAttribute('class', 'flag');
gElFlagsDiv.appendChild(flagImg);
var flagImgSrc = flagImg.attributes.src.nodeValue;
var flagImgSrcDash = flagImgSrc.indexOf('-');
var flagStr = flagImgSrc.slice(flagImgSrcDash + 1, flagImgSrc.length - 4);
while (!options.includes(flagStr)) {
options = generateOptions();
continue;
}
gFlagsProperties.push({id: i, image: flagImg, opts: options, correctOpt: flagStr});
return gFlagsProperties;
}
function render() {
getFlag();
var gElOptions = document.querySelector('.options');
for (var i = 0; i < 3; i++) {
var elOptionBtn = document.createElement('button');
elOptionBtn.innerText = gFlagsProperties[i].opts[i];
gFlagsProperties[i].image.style.display = 'block';
gElOptions.appendChild(elOptionBtn);
elOptionBtn.setAttribute('class', `option-${i}`);
var flagImgSrc = gFlagsProperties[i].image.attributes.src.nodeValue;
var flagImgSrcDash = flagImgSrc.indexOf('-');
var flagStr = flagImgSrc.slice(flagImgSrcDash + 1, flagImgSrc.length - 4);
}
while (!gFlagsProperties[i].opts.includes(flagStr)) {
init();
}
}
function generateOptions() {
var tempRandomizedOptions = gCountries.join();
var randomizedOptions = tempRandomizedOptions.split(',');
shuffle(randomizedOptions);
for (var i = 0; i < 3; i++) {
randomizedOptions.splice(0, randomizedOptions.length - 3);
}
return randomizedOptions;
}
<div class="flags">
</div>
<div class="options">
</div>
Been lost in this for hours. Please help me figure it out.
So, I got a little carried away with this.
So, the key thing about your code is that both of your while loops weren't working properly so in this example I've tried to simplify things a little bit.
I've restructured the code a little because I didn't have access to your flag images so I used a flag API instead. This meant that I had to convert your array of countries into an array of objects with name and code properties. A forEach then iterates over the array and gets the flag src information from the API and adds it to the object.
I refer to questions made up of flag data rather than flags, as that made it easier for me to understand.
All the actual rendering is now done in the renderQuestion function. What was getFlag and is now called getQuestionData only returns data for the main flag, and its options.
I added a checkAnswer function to ensure the code actually worked properly.
// Cache the elements
const container = document.querySelector('.container');
const flags = document.querySelector('.flags');
const options = document.querySelector('.options');
const button = document.querySelector('button');
const result = document.querySelector('.result');
// Add a listener to the button
button.addEventListener('click', checkAnswer, false);
// The new array of country objects
const countries = [{ name: 'albania', code: 'AL' }, { name: 'chad', code: 'TD'}, { name: 'colombia', code: 'CO' }, { name: 'cyprus', code: 'CY' }, { name: 'denmark', code: 'DK' }, { name: 'gabon', code: 'GA' }, { name: 'hungary', code: 'HU' }, { name: 'iceland', code: 'IS' }, { name: 'latvia', code: 'LV' }, { name: 'madagascar', code: 'MG' }, { name: 'romania', code: 'RO' }, { name: 'serbia', code: 'RS' }];
// Iterate over the countries array, and for
// each country add the API link for the flag
// as the country image source
countries.forEach(country => {
country.src = `https://flagcdn.com/24x18/${country.code.toLowerCase()}.png`;
});
// Grab a random flag from the array
function randomFlag() {
const rnd = Math.floor(Math.random() * ((countries.length - 1) - 0) + 0);
return countries[rnd];
}
function getQuestionData() {
// Make a copy of the flag so we don't
// overwrite the data in the countries array
const flag = {...randomFlag()};
// Create the options array, and add the
// main flag to it
flag.options = [];
flag.options.push(flag)
// We obviously don't want to add the main
// flag to the options again so we use while
// to create a random flag. If its code doesn't
// match the main flag code add it to options
// otherwise loop again until there are three flags
// in the array
while (flag.options.length < 3) {
const option = randomFlag();
if (option.code !== flag.code) {
flag.options.push(option);
}
}
return flag;
}
function renderQuestion(question) {
// Add the main flag to the flags element
const img = document.createElement('img');
img.src = question.src;
img.className = 'flag';
flags.appendChild(img);
// Create some HTML by mapping over the question
// options and using a template string to add the
// relevant option data to it
const html = question.options.map(option => {
return `
<span class="name">${option.name}</span>
<input
type="radio"
name="option"
value=${option.code}
/>`;
});
// Add the HTML to the options element
options.innerHTML = html.join('<br />');
}
function checkAnswer() {
// Get the checked input from the options element
const input = options.querySelector('input:checked');
// Add a message to the result element depending
// on whether the image value matches the main flag code
// in the question
if (input.value === question.code) {
result.textContent = 'Correct!';
} else {
result.textContent = 'Incorrect';
}
}
// Call `renderQuestion` with a new question.
const question = getQuestionData();
renderQuestion(question);
.result, .options, button { margin-top: 1em; }
.name { text-transform: capitalize; }
<div class="container">
<div class="flags"></div>
<div class="options"></div>
<button>Check answer</button>
<div class="result"></div>
</div>
Where is i being incremented? in
while (!gFlagsProperties[i].opts.includes(flagStr)) {
init();
}
And also, init directly calls render, which calls init, so even if you do break out of the while loop somehow, you're going to end up right back where you were.
I think you meant for that while loop to be inside the for, but that's not going to solve the infinite call loop from init -> render -> init

Javascript: Infinite loop in webworker [duplicate]

I want to sort an array, using Web Workers. But this array might receive new values over time, while the worker is still performing the sort function.
So my question is, how can I "stop" the sorting computation on the worker after receiving the new item, so it can perform the sort on the array with that item, while still keeping the sorting that was already made?
Example:
let worker = new Worker('worker.js');
let list = [10,1,5,2,14,3];
worker.postMessage({ list });
setInterval(() => worker.postMessage({ num: SOME_RANDOM_NUM, list }), 100);
worker.onmessage = event => {
list = event.data.list;
}
So lets say that, I've passed 50, the worker made some progress in the sorting before that and now I have something like this:
[1, 2, 3, 10, 5, 14, 50]. Which means the sorting stopped at index 3. So I pass this new array back to the worker, so it can continue the sorting from position 3.
How can I accomplish that, since there is no way to pause/resume a web worker?
Even though the Worker works on an other thread than the one of your main page, and can thus run continuously without blocking the UI, it still runs on a single thread.
This means that until your sort algorithm has finished, the Worker will delay the execution of the message event handler; it is as blocked as would be the main thread.
Even if you made use of an other Worker from inside this worker, the problem would be the same.
The only solution would be to use a kind of generator function as the sorter, and to yield it every now and then so that the events can get executed.
But doing this will drastically slow down your sorting algorithm.
To make it better, you could try to hook to each Event Loop, thanks to a MessageChannel object: you talk in one port and receive the message in the next Event loop. If you talk again to the other port, then you have your own hook to each Event loop.
Now, the best would be to run a good batch in every of these Event loop, but for demo, I'll call only one instance of our generator function (that I borrowed from this Q/A)
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let sorter = bubbleSort(list);
let done = false;
/* inner messaging channel */
const msg_channel = new MessageChannel();
// Hook to every Event loop
msg_channel.port2.onmessage = e => {
// procede next step in sorting algo
// could be a few thousands in a loop
const state = sorter.next();
// while running
if(!state.done) {
msg_channel.port1.postMessage('');
done = false;
}
else {
done = true;
}
}
msg_channel.port1.postMessage("");
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
sorter = bubbleSort(list);
msg_channel.port1.postMessage('');
}
}
};
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
Note that the same can be achieved with an async function, which may be more practical in some cases:
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let done = false;
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
There are two decent options.
Option 1: Worker.terminate()
The first is just to kill your existing web worker and start a new one. For that you can use Worker.terminate().
The terminate() method of the Worker interface immediately terminates the Worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
The only downsides of this approach are:
You lose all worker state. If you had to copy a load of data into it for the request you have to do it all again.
It involves thread creation and destruction, which isn't as slow as most people think but if you terminate web workers a lot it might cause issues.
If neither of those are an issue it is probably the easiest option.
In my case I have lots of state. My worker is rendering part of an image, and when the user pans to a different area I want it to stop what it is doing and start rendering the new area. But the data needed to render the image is pretty huge.
In your case you have the state of your (presumably huge) list that you don't want to use.
Option 2: Yielding
The second option is basically to do cooperative multitasking. You run your computation as normal, but every now and then you pause (yield) and say "should I stop?", like this (this is for some nonsense calculation, not sorting).
let requestId = 0;
onmessage = event => {
++requestId;
sortAndSendData(requestId, event.data);
}
function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This won't work though because sortAndSendData() runs to completion and blocks the web worker's event loop. We need some way to yield just before thisRequestId !== requestId. Unfortunately Javascript doesn't quite have a yield method. It does have async/await so we might try this:
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await Promise.resolve();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
Unfortunately it doesn't work. I think it's because async/await executes things eagerly using "microtasks", which get executed before pending "macrotasks" (our web worker message) if possible.
We need to force our await to become a macrotask, which you can do using setTimeout(0):
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
function yieldToMacrotasks() {
return new Promise((resolve) => setTimeout(resolve));
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await yieldToMacrotasks();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This works! However it is extremely slow. await yieldToMacrotasks() takes approximately 4 ms on my machine with Chrome! This is because browsers set a minimum timeout on setTimeout(0) of something like 1 or 4 ms (the actual minimum seems to be complicated).
Fortunately another user pointed me to a quicker way. Basically sending a message on another MessageChannel also yields to the event loop, but isn't subject to the minimum delay like setTimeout(0) is. This code works and each loop only takes ~0.04 ms which should be fine.
let currentTask = {
cancelled: false,
}
onmessage = event => {
currentTask.cancelled = true;
currentTask = {
cancelled: false,
};
performComputation(currentTask, event.data);
}
async function performComputation(task, data) {
let total = 0;
let promiseResolver;
const channel = new MessageChannel();
channel.port2.onmessage = event => {
promiseResolver();
};
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Yield to the event loop.
const promise = new Promise(resolve => {
promiseResolver = resolve;
});
channel.port1.postMessage(null);
await promise;
// Check if this task has been superceded by another one.
if (task.cancelled) {
return;
}
}
// Return the result.
postMessage(total);
}
I'm not totally happy about it - it relies on postMessage() events being processed in FIFO order, which I doubt is guaranteed. I suspect you could rewrite the code to make it work even if that isn't true.
You can do it with some trick – with the help of setTimeout function interrupting. For example it is not possible without an addition thread to execute 2 functions parallel, but with setTimeout function interrupting trick we can do it like follows:
Example of parallel execution of functions
var count_0 = 0,
count_1 = 0;
function func_0()
{
if(count_0 < 3)
setTimeout(func_0, 0);//the same: setTimeout(func_0);
console.log('count_0 = '+count_0);
count_0++
}
function func_1()
{
if(count_1 < 3)
setTimeout(func_1, 0);
console.log('count_1 = '+count_1)
count_1++
}
func_0();
func_1();
You will get this output:
count_0 = 0
count_1 = 0
count_0 = 1
count_1 = 1
count_0 = 2
count_1 = 2
count_0 = 3
count_1 = 3
Why is it possible? Because the setTimeout function needs some time to be executed. And this time is even enought for the execution of some part from your following code.
Solution for you
For this case you have to write your own array sort function (or you can also use the following function from me) because we can not interrupt the native sort function. And in this your own function you have to use this setTimeout function interrupting trick. And you can receive your message event notification.
In the following example I have the interrupting in the half length of my array, and you can change it if you want.
Example with custom sort function interrupting
var numbers = [4, 2, 1, 3, 5];
// this is my bubble sort function with interruption
/**
* Sorting an array. You will get the same, but sorted array.
* #param {array[]} arr – array to sort
* #param {number} dir – if dir = -1 you will get an array like [5,4,3,2,1]
* and if dir = 1 in opposite direction like [1,2,3,4,5]
* #param {number} passCount – it is used only for setTimeout interrupting trick.
*/
function sortNumbersWithInterruption(arr, dir, passCount)
{
var passes = passCount || arr.length,
halfOfArrayLength = (arr.length / 2) | 0; // for ex. 2.5 | 0 = 2
// Why we need while loop: some values are on
// the end of array and we have to change their
// positions until they move to the first place of array.
while(passes--)
{
if(!passCount && passes == halfOfArrayLength)
{
// if you want you can also not write the following line for full break of sorting
setTimeout(function(){sortNumbersWithInterruption(arr, dir, passes)}, 0);
/*
You can do here all what you want. Place 1
*/
break
}
for(var i = 0; i < arr.length - 1; i++)
{
var a = arr[i],
b = arr[i+1];
if((a - b) * dir > 0)
{
arr[i] = b;
arr[i+1] = a;
}
}
console.log('array is: ' + arr.join());
}
if(passCount)
console.log('END sring is: ' + arr.join());
}
sortNumbersWithInterruption(numbers, -1); //without passCount parameter
/*
You can do here all what you want. Place 2
*/
console.log('The execution is here now!');
You will get this output:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
You can do it with insertion sort (kind of).
Here is the idea:
Start your worker with an internal empty array (empty array is sorted obviously)
Your worker receives only elements not the entire array
Your worker insert any received element right in correct position into the array
Every n seconds, the worker raises a message with the current array if it has changed after the last event. (If you prefer, you can send the array on every insertion, but is more efficient to buffer somehow)
Eventually, you get the entire array, if any item is added, you will receive the updated array to.
NOTE: Because your array is always sorted, you can insert in correct position using binary search. This is very efficient.
I think the case comes down to careful management of postMessage calls and amount of data passed to be processed at a time. Was dealing with problem of this kind - think about not sending all new data into the function at once but rather creating your own queue and when small enough portion of the task has been acomplished by webworker thread send a message back to the main thread and decide to send the next portion, wait or quit.
In Your case, e.g. one time You get 9000 new items, next 100k - maybe create a queue/buffer that adds next 10k new elements each time webworker is done processing last data change.
const someWorker = new Worker('abc.js');
var processingLock = false;
var queue = [];
function newDataAction(arr = null) {
if (arr != null) {
queue = queue.concat(arr);
}
if (!processingLock) {
processingLock = true;
var data = [];
for (let i = 0; i < 10000 && queue.length > 0; i++) {
data.push(queue.pop());
}
worker.postMessage(data);
}
}
someWorker.addEventListener('message', function(e) {
if (e.data == 'finished-last-task') {
processingLock = false;
if (queue.length > 0) {
newDataAction();
}
}
});
Worked through many sorting algorithms and I don't see how sending new data into an sorting algorithm with partially sorted array makes much difference in terms of compuation time from sorting them both sequentially and performing a merge.

JavaScript - Issues recovering a map in an object after being saved in localStorage

I've been dealing with this for some time. I've a list of sections in which the user checks some checkboxes and that is sent to the server via AJAX. However, since the user can return to previous sections, I'm using some objects of mine to store some things the user has done (if he/she already finished working in that section, which checkboxes checked, etc). I'm doing this to not overload the database and only send new requests to store information if the user effectively changes a previous checkbox, not if he just starts clicking "Save" randomly. I'm using objects to see the sections of the page, and storing the previous state of the checkboxes in a Map. Here's my "supervisor":
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
var children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children().length;
for (var i = 0; i < children; i++) {
if (i % 2 == 0) {
var checkbox = $("#ContentPlaceHolder1_checkboxes_div_" + id).children()[i];
var idCheck = checkbox.id.split("_")[2];
this.selections.set(idCheck, false);
}
}
console.log("Length " + this.selections.size);
this.change = false;
}
The console.log gives me the expected output, so I assume my Map is created and initialized correctly. Since the session of the user can expire before he finishes his work, or he can close his browser by accident, I'm storing this object using local storage, so I can change the page accordingly to what he has done should anything happen. Here are my functions:
function setObj(id, supervisor) {
localStorage.setItem(id, JSON.stringify(supervisor));
}
function getObj(key) {
var supervisor = JSON.parse(localStorage.getItem(key));
return supervisor;
}
So, I'm trying to add to the record whenever an user clicks in a checkbox. And this is where the problem happens. Here's the function:
function checkboxClicked(idCbx) {
var idSection = $("#ContentPlaceHolder1_hdnActualField").val();
var supervisor = getObj(idSection);
console.log(typeof (supervisor)); //Returns object, everythings fine
console.log(typeof (supervisor.change)); //Returns boolean
supervisor.change = true;
var idCheck = idCbx.split("_")[2]; //I just want a part of the name
console.log(typeof(supervisor.selections)); //Prints object
console.log("Length " + supervisor.selections.size); //Undefined!
supervisor.selections.set(idCheck, true); //Error! Note: The true is just for testing purposes
setObj(idSection, supervisor);
}
What am I doing wrong? Thanks!
Please look at this example, I removed the jquery id discovery for clarity. You'll need to adapt this to meet your needs but it should get you mostly there.
const mapToJSON = (map) => [...map];
const mapFromJSON = (json) => new Map(json);
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
this.change = false;
this.selections.set('blah', 'hello');
}
Supervisor.from = function (data) {
const id = data.id;
const supervisor = new Supervisor(id);
supervisor.verif = data.verif;
supervisor.selections = new Map(data.selections);
return supervisor;
};
Supervisor.prototype.toJSON = function() {
return {
id: this.id,
verif: this.verif,
selections: mapToJSON(this.selections)
}
}
const expected = new Supervisor(1);
console.log(expected);
const json = JSON.stringify(expected);
const actual = Supervisor.from(JSON.parse(json));
console.log(actual);
If you cant use the spread operation in 'mapToJSON' you could loop and push.
const mapToJSON = (map) => {
const result = [];
for (let entry of map.entries()) {
result.push(entry);
}
return result;
}
Really the only thing id change is have the constructor do less, just accept values, assign with minimal fiddling, and have a factory query the dom and populate the constructor with values. Maybe something like fromDOM() or something. This will make Supervisor more flexible and easier to test.
function Supervisor(options) {
this.id = options.id;
this.verif = null;
this.selections = options.selections || new Map();
this.change = false;
}
Supervisor.fromDOM = function(id) {
const selections = new Map();
const children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children();
for (var i = 0; i < children.length; i++) {
if (i % 2 == 0) {
var checkbox = children[i];
var idCheck = checkbox.id.split("_")[2];
selections.set(idCheck, false);
}
}
return new Supervisor({ id: id, selections: selections });
};
console.log(Supervisor.fromDOM(2));
You can keep going and have another method that tries to parse a Supervisor from localStorageand default to the dom based factory if the localStorage one returns null.

Reduce lags on a nodejs game

I have created a nodejs game. i have a setInterval function on the server which sends all the objects near the player. but it doesnt seem to be as smooth as i get when i run it locally. the data package has about 60 objects. here's the server code for sending objects and receiving them on the client. any way to compress the package or reduce the lags? pinka.herokuapp.com
server:
setInterval(function() {
server.update();
for(var key in sockets) {
var socket = sockets[key];
var player = server.players.filter(function(p) {
return p.id == socket.id
})[0];
var package = [];
var blobs = server.getNodesInRange(player.centerX, player.centerY);
for(var i = 0; i < blobs.length; i++) {
var b = blobs[i];
package.push({
x: b.x,
y: b.y,
nick: b.nick,
size: Math.sqrt(b._mass) * 10,
hue: b.hue
});
};
socket.emit("update blobs", package);
socket.emit("leaders", server.getLeaders());
if(player.blobs.length == 0) {
socket.emit("dead");
continue;
}
var translateX = player._centerX * player._drawZoom - player.screenWidth / 2;
var translateY = player._centerY * player._drawZoom - player.screenHeight / 2;
socket.emit("center and zoom", {
centerX: translateX,
centerY: translateY,
zoom: player._drawZoom
});
}
}, 1000/60);
client:
socket.on("update blobs", function(data) {
blobs = data;
});
this is the whole communication part.
As Jonas W. said, the problem is in the server-client communication.
To be efficient a realtime system with socket.io should be based on events and not in interval checks.
I'd suggest you to have something like this:
On the client, emit a 'user:move' event when the user moves. Prevent too many events to relief the server with unnecessary updates.
On the server, react to a specific 'player:move' event. If the events needs to be broadcasted to the other players, a filter with the ones that can actually "see" the action will avoid unnecessary information for the client too.
An example with pseudo code:
Client
let updating = false;
let timeout = 0;
// Assuming this function is triggered everytime the user moves (i.e. presses a key)
onUserMove (data) {
if ('same press as before' && updating) {
// ignore move if it's the same that has just been done
return;
} else {
socket.emit('user:move', data);
// 'Block' the emit for same movement for 20ms
updating = true;
clearTimeout(timeout);
timeout = setTimeout(() => {
updating = false;
}, 20);
}
}
Server
socket.on('user:move', (data) => {
const dataForTheUser = processingYouAreAlreadyDoing(data);
socket.emit('data:for:user', dataForTheUser);
// In case there's information to be sent to every user
const dataToBroadcast = getDataToBroadcast(data);
const usersToBroadcast = getCloseUsers(data);
for (let user in usersToBroadcast) {
user.socket.emit('whatever:event', dataToBroadcast);
}
})

Google Visualizations: Where does JS execution begin again after drawing charts?

A small project using Google Visualizations, https://jsfiddle.net/brisray/qsgewt2d/ - works as far as I have it, but I have a question about it.
Once the graphs are drawn, I assumed that control would pass back to "mainFunction". This is a function that loops through the object array I made creating the queries, then calls other functions that draws the tables and charts. I was hoping to add more code to that function to call other functions for other things I want to do.
What I find is that it doesn't work that way. A simple JS alert AFTER the loop shows none of the drawing is done until the alert is acknowledged.
Am I missing something? Perhaps an event handler that's triggered after the last of the array is processed and the last of the initial graphs drawn? Something to do with the asynchronous natures of the drawing.
Is there something fundamentally wrong with the code and the way I've written it?
Whatever is happening, I cannot see it, so would appreciate some help.
Ray
var SCOB_metrics = (function() {
// Create the object array that is global to the namespace
var graphArray = [];
// Create objects and push them to the array
// Object properties are: ID, sheet, GID, datatable, graphtype, rotated
createObject('MSDC', '1RCZiWWsEKPs6-1ULXeHjWmaXUEHCaRPtKT9U_6FzCJ4', '1835225366', 'MSDC_data', 'Column', false);
createObject('StudentPop', '1RCZiWWsEKPs6-1ULXeHjWmaXUEHCaRPtKT9U_6FzCJ4', '3256521', 'StudentPop_data', 'Column', false);
createObject('EnrolTrends', '1RCZiWWsEKPs6-1ULXeHjWmaXUEHCaRPtKT9U_6FzCJ4', '1037635451', 'EnrolTrends_data', 'Column', false);
google.charts.load('current');
google.charts.setOnLoadCallback(mainFunction);
function mainFunction() {
for (i = 0; i < graphArray.length; i++) {
makeQuery(graphArray[i]);
}
// Now everthing is drawn, set up the listener for the drawingArea div
// so that the graphs can be updated if needed
var theParent = document.querySelector("#drawingArea");
theParent.addEventListener("change", whichDrop, false);
}
function makeQuery(myObject) {
// Create the querystring and send it
var queryStr = "https://docs.google.com/spreadsheets/d/" + myObject.sheet + "/gviz/tq?gid=" + myObject.GID + "&headers=1";
var query = new google.visualization.Query(queryStr);
/* You can't send a variable though the normal query.send method
query.send(handleQueryResponse);
so do this */
query.send(function(response) {
visuals(response, myObject);
});
}
function whichDrop(e) {
// Find which dropdown was changed, get it's value, find the index of the graphArray.ID it belongs to and redraw the graph
if (e.target !== e.currentTarget) {
var changedItem = e.target.id;
}
e.stopPropagation();
var findID = changedItem.substr(0, changedItem.length - 4);
arrayIndex = graphArray.findIndex(x => x.ID == findID);
var e = document.getElementById(changedItem);
var chosenGraph = e.options[e.selectedIndex].value;
graphArray[arrayIndex].graphtype = chosenGraph;
drawGraphs(graphArray[arrayIndex]);
}
function visuals(response, myObject) {
// Create the data table and draw both the table and graph
myObject.datatable = response.getDataTable();
drawTables(myObject);
drawGraphs(myObject);
}
function drawTables(myObject) {
// Draw the table
var tableArea = myObject.ID + "_table_div";
var cssClassNames = {
'headerRow': 'header-css',
'headerCell': 'border-css'
};
theTables = new google.visualization.ChartWrapper({
chartType: 'Table',
containerId: tableArea,
dataTable: myObject.datatable,
options: {
'allowHtml': true,
'cssClassNames': cssClassNames
}
});
theTables.draw();
}
function drawGraphs(myObject) {
// Draw the graph
var graphArea = myObject.ID + "_graph_div";
var chartType = myObject.graphtype + "Chart";
theGraphs = new google.visualization.ChartWrapper({
chartType: chartType,
containerId: graphArea,
dataTable: myObject.datatable,
// May have to use IF or SWITCH statements depending on chartType
options: {
height: 400,
hAxis: {
maxTextLines: 4, // maximum number of lines to wrap to
maxAlternation: 4, // maximum layers of labels (setting this higher than 1 allows labels to stack over/under each other)
minTextSpacing: 1, // minimum space in pixels between adjacent labels
},
textStyle: {
fontSize: 9
}
}
});
theGraphs.draw();
}
function transposeDataTable(myObject) {
// Transpose the datatable
dataTable = myObject.datatable;
// Toggle rotated boolean
myObject.rotated = !myObject.rotated;
// Rotate the datatable
var rows = []; //the row tip becomes the column header and the rest become
for (var rowIdx = 0; rowIdx < dataTable.getNumberOfRows(); rowIdx++) {
var rowData = [];
for (var colIdx = 0; colIdx < dataTable.getNumberOfColumns(); colIdx++) {
rowData.push(dataTable.getValue(rowIdx, colIdx));
}
rows.push(rowData);
}
var newTB = new google.visualization.DataTable();
newTB.addColumn('string', dataTable.getColumnLabel(0));
newTB.addRows(dataTable.getNumberOfColumns() - 1);
var colIdx = 1;
for (var idx = 0; idx < (dataTable.getNumberOfColumns() - 1); idx++) {
var colLabel = dataTable.getColumnLabel(colIdx);
newTB.setValue(idx, 0, colLabel);
colIdx++;
}
for (var i = 0; i < rows.length; i++) {
var rowData = rows[i];
console.log(rowData[0]);
newTB.addColumn('number', rowData[0]); //assuming the first one is always a header
var localRowIdx = 0;
for (var j = 1; j < rowData.length; j++) {
newTB.setValue(localRowIdx, (i + 1), rowData[j]);
localRowIdx++;
}
}
return newTB;
}
function createObject(ID, sheet, GID, datatable, graphtype, rotated) {
// Create the data objects and push them to the graphArray array
graphArray.push({
ID: ID,
sheet: sheet,
GID: GID,
datatable: datatable,
graphtype: graphtype,
rotated: rotated,
});
}
})();
I think that you are looking for 'ready' event. https://developers.google.com/chart/interactive/docs/events#the-ready-event
I abandoned what I was trying to do, which was to step through a list of various chart types to see what would be the best for what I wanted to display.
One of the things I wanted to look at was to create PNGs of the various charts. But, I ran foul of the "ready" event handlers again.
Just before I drew the charts I used
google.visualization.events.addListener(theGraphs, 'ready', function() {
document.getElementById(imgLink).innerHTML = 'Printable Graph';
});
but I was getting "Cannot read property 'getImageURI' of null" errors.
Presumably because of the loop the drawing of these graphs is in, it was running too fast for the event handler to catch?
I tried removing the event handler after the drawing of the charts using
google.visualization.events.removeListener(theGraphs);
and even
google.visualization.events.removeAllListeners(theGraphs);
I had thought of doing something like trying to count the number of times the event was triggered and comparing that to the number of times the function was called but went for a simpler, but probably not the best method, of adding a setTimeout function.
setTimeout(function() {
document.getElementById(imgLink).innerHTML = 'Printable Graph';
}, 100);
theGraphs.draw();
Probably not the most elegant solution, but the delay of just 1/10 of a second made the problems I've been having go away.
https://jsfiddle.net/brisray/z49jw264/706/
setTimeout(function() {
document.getElementById(imgLink).innerHTML = '<a target="_blank" href="' + theGraphs.getChart().getImageURI() + '">Printable Graph</a>';
}, 100);
theGraphs.draw();

Categories

Resources