I'm making a simple JavaScript game (Space Invaders style) (Top-down space shooter) and I'm trying to make my character shoot a bullet per each 'space' key press. How can I do that?
I have tried multiple approaches, setting a flag, using onkeypress instead of keydown, Google searches (have also encountered this similar question yet it didn't help: Javascript onkeydown event fire only once?)
Below is an example of one solution I have tried.
document.onkeydown = function(e)
{
if(e.keyCode == 32 && space == false)
{
space = true;
}
}
document.onkeyup = function(e)
{
if(e.keyCode == 32) space = false;
}
window.requestAnimationFrame(gameLoop);
function gameLoop(timeStamp)
{ if(space === true)
{
p.shoot();
}
window.requestAnimationFrame(gameLoop);
}
Expected results: Key being fired only once.
Actual results: Key is being fired multiple times.
There's really 3 states to a bullet - and, taking into consideration that in classic Space Invaders the player can only have one bullet in flight at a time, this makes things relatively simple
The bullet can be
NONE - doesn't exist
FIRED - i.e. need to create one
EXISTS - it's in flight
The code to handle it is relatively simple too
var BulletState = {
NONE: 0,
FIRED: 1,
EXISTS: 2
};
var bullet = BulletState.NONE;
document.onkeydown = function(e) {
if (e.keyCode == 32 && bullet === BulletState.NONE) {
bullet = BulletState.FIRED;
}
}
window.requestAnimationFrame(gameLoop);
function gameLoop(timeStamp) {
if (bullet === BulletState.FIRED) {
bullet = BulletState.EXISTS;
p.shoot(); // p.shoot needs to set bullet = BulletState.NONE when the bullet expires
}
window.requestAnimationFrame(gameLoop);
}
document.onkeyup = function(e)
{
// not required at all
}
edit: To fire a bullet every press of spacebar (allowing multiple bullets)
var space = false;
var fireBullet = 0;
document.onkeydown = function(e) {
if (e.keyCode == 32 && !e.repeat && !space) {
fireBullet++;
space = true;
}
}
window.requestAnimationFrame(gameLoop);
function gameLoop(timeStamp) {
while (fireBullet) {
p.shoot();
fireBullet--;
}
window.requestAnimationFrame(gameLoop);
}
document.onkeyup = function(e)
{
space = false;
}
I use fireBullet++ and fireBullet-- because I guess it's plausible that someone could press and release and press the spacebar within 16ms (single frame) :p
You could also do
function gameLoop(timeStamp) {
if (fireBullet) {
p.shoot();
fireBullet--;
}
window.requestAnimationFrame(gameLoop);
}
that way you still handle multiple bullets OK, but only fire off one per frame - really depends on how you want to handle someone with a very fast trigger finger :p
As #JaromandaX suggested, space = false; after p.shoot(); will give you the desired result:
Reproducing the issue:
var space = false;
var element = document.querySelector('input');
element.onkeydown = function(e) {
if (!space && e.keyCode == 32) {
space = true;
console.log('event fired');
}
};
element.onkeyup = function(e) {
if (e.keyCode == 32)
space = false;
}
window.requestAnimationFrame(gameLoop);
function gameLoop(timeStamp) {
if (space == true) {
console.log('shoot');
}
window.requestAnimationFrame(gameLoop);
}
<input />
EDIT: Added another isShot variable to keep track of shot fired and space key is down in requestAnimationFrame event:
var space = false;
var element = document.querySelector('input');
var isShot = false;
element.onkeydown = function(e) {
if (!space && e.keyCode == 32) {
space = true;
console.log('event fired');
}
};
element.onkeyup = function(e) {
if (e.keyCode == 32) {
space = isShot = false;
}
}
window.requestAnimationFrame(gameLoop);
function gameLoop(timeStamp) {
if (space && !isShot) {
console.log('shoot');
isShot = true;
}
window.requestAnimationFrame(gameLoop);
}
<input />
Here you have two event, on each key press three are three even that get's fired
1. keydown (when user press the key but not yet release the key)
2. keypress (its fired after the keydown event)
3. keyup (When user release the key)
If you want to fire only once for each keypress
implement on of those if implement all of the three all three methods will fired.
Additionally: for safety you can write e.preventDefault().
I was working on Babylon.js when I wanted to make a jump. I found a website and copied it jump code and it works perfectly! But I want to make a real game and I don't want people spamming spacebar to go flying. How could I make it have a few second delay so no one can keep on spamming spacebar and go flying?
function jump(){
camera.cameraDirection.y = 2;
}
document.body.onkeyup = function(e){
if(e.keyCode == 32){
//your code
console.log("jump");
setTimeout(jump(), 1000);
}
}
Link to my game
https://playground.babylonjs.com/#JCE1G3
One option would be to create a persistent boolean variable such as justJumped, and only jump if justJumped is false. When jumping, set justJumped to true, and create a timeout that resets it back to false after the duration of the jump, which looks to be a bit less than half a second:
let justJumped = false;
document.body.onkeyup = function(e) {
if (e.keyCode == 32 && !justJumped) {
justJumped = true;
setTimeout(() => justJumped = false, 400)
console.log("jump");
jump();
}
}
Also note that setTimeout(jump(), 1000); probably isn't doing what you're thinking it does - it invokes jump immediately. If you wanted to call the jump function after 1000ms, just pass the function name itself:
let justJumped = false;
document.body.onkeyup = function(e) {
if (e.keyCode == 32 && !justJumped) {
justJumped = true;
setTimeout(() => justJumped = false, 1400)
console.log("jump");
setTimeout(jump, 1000);
}
}
You can also use other library to achieve it. My favorite one is lodash. You can use _.throttle
var throttled = _.throttle(jump, 1000);
document.body.onkeyup = function(e) {
if (e.keyCode == 32) {
throttled()
}
}
if you don't want to jump immediately after the first key up. you can add option trailing: false
var throttled = _.throttle(jump, 1000, { 'trailing': false });
window.addEventListener('keydown', function(event) {
onKeyDownHandler(event);
}, false);
function onKeyDownHandler(e)
{
var focus_id = e.target.id;
switch (e.keyCode) {
case 13: // enter
if(focus_id == "Text1")
{
alert("function 1");
}else if(focus_id == "Text2")
{
alert("function 2");
}else if(focus_id == "Text3")
{
alert("function 3");
}
return;
}
}
is there anyway i can delay or make sure user dont spam by clicking the enter , how do i set keypress delay on my enter button ? which is the best way set delay timer or remove EventListener?
You can use the jQuery throttle/debounce plugin to only handle call your function when there is a pause in keyDown events.
You can prevent the default action for a period of time after the last Enter keypress:
window.addEventListener('keydown', onKeyDownHandler, false);
var lastEnter = null;
function onKeyDownHandler(e) {
var focus_id = e.target.id;
switch (e.which || e.keyCode) { // Note the e.which, for x-browser compat
case 13:
if (lastEnter && Date.now() - lastEnter < 5000) {
e.preventDefault();
return;
}
lastEnter = Date.now();
// Enter key processing...
break;
// ...other keys...
}
}
Or using jQuery (you've tagged your question jquery, but don't appear to be using jQuery in your code):
$(window).on("keydown", function(e) {
onKeyDownHandler(e);
});
var lastEnter = null;
function onKeyDownHandler(e) {
var focus_id = e.target.id;
switch (e.which) { // jQuery normalizes this for you
case 13:
if (lastEnter && Date.now() - lastEnter < 5000) {
e.preventDefault();
return;
}
lastEnter = Date.now();
// Enter key processing...
break;
// ...other keys...
}
}
Side notes:
Since the return value of an addEventListener callback is completely ignored and addEventListener calls the handler with just a single argument, if you're not using this within the handler (as you appear not to be), there's no need to wrap a function around onKeyDownHandler; just use it directly.
Some browsers use which for the keycode, others use keyCode, which is why I used e.which || e.keyCode in the switch. JavaScript's curiously-powerful || operator will use e.which if it's not falsey, e.keyCode otherwise.
You can create a timeout on enter press, and on another enter press, overwrite that previous timeout with the new one. That means that if you for example press enter again before the first timeout has ended, that first timeout will be overwritten by a new one, so that you get a new x amount of time before the actual timeout is executed. This works until infinity.
Example:
var keyup_timeout;
var timeout_delay_in_ms = 500;
element.on('keyup', function(e) {
e.preventDefault(); // Prevent default enter press action.
var enter_pressed;
if (e.which === 13) {
enter_pressed = true; // Just an example to illustrate what you could do.
}
if (enter_pressed) {
clearTimeout(keyup_timeout); // Clear the previous timeout so that it won't be executed any more. It will be overwritten by a new one below.
keyup_timeout = setTimeout(function() {
// Perform your magic here.
}, timeout_delay_in_ms);
}
});
I'm trying to insert delays in a sequence of stimuli that are presented on the keypress event.
The code below shows how I'd tried to insert a 1000ms delay between stimuli (i.e. after the key was pressed, it should take 1000ms before the next stimulus appears). Yet there seems to be no delay. Where am I wrong (is it better to do this with setTimeout)?
And: how could I set the maximum 'display-time' of each stimulus to, say, 2000ms, so that it automatically presents another stimulus even if no key is pressed?
var stim = [
{name:"WORD1", path:".../pic1.jpg"},
{name:"WORD2", path:".../pic2.jpg"},
{name:"WORD3", path:".../pic3.jpg"},
]
$(function(){
$(document).keypress(function(e){
if ($(e.target).is('input, textarea')) {
return;
};
if (e.which === 97 || e.which === 108) {
if(Math.random() < 0.5) {
var new_word = stim[Math.floor((Math.random()*stim.length)+1)].name;
$("#abc").delay(1000).text(new_word);
} else {
var new_img = stim[Math.floor((Math.random()*stim.length)+1)].path;
$("#abc").delay(1000).empty();
var prox_img = $('<img id="abcimg" height="300px" width="300px">');
prox_img.attr('src', new_img);
prox_img.appendTo('#abc');
}
};
});
});
According to the documentation for delay: https://api.jquery.com/delay/
The .delay() method is best for delaying between queued jQuery
effects.
You should be using setTimeout for this since it also allows you to cancel it. That is really important for your need to have a maximum timeout.
So, first I would break your function up. You could have a function that handles the displaying, which will get called when timeout occurs or when a key is pressed.
$(function(){
var to = 0; //this is used to clear your timeout when the user clicks a button
function showNext() {
if(Math.random() < 0.5) {
var new_word = stim[Math.floor((Math.random()*stim.length)+1)].name;
$("#abc").delay(1000).text(new_word);
} else {
var new_img = stim[Math.floor((Math.random()*stim.length)+1)].path;
$("#abc").delay(1000).empty();
var prox_img = $('<img id="abcimg" height="300px" width="300px">');
prox_img.attr('src', new_img);
prox_img.appendTo('#abc');
}
to = setTimeout(function(){showNext()}, 2000);
}
$(document).keypress(function(e){
if ($(e.target).is('input, textarea')) {
return;
};
clearTimeout(to);
if (e.which === 97 || e.which === 108) {
setTimeout(function(){showNext();}, 1000);
}
});
});
I'm handling both the click and dblclick event on a DOM element. Each one carries out a different command, but I find that when double clicking on the element, in addition to firing the double click event, the click event is also fired twice. What is the best approach for preventing this behavior?
In case anyone else stumbles on this (as I did) looking for an answer, the absolute best solution that I could come up with is the following:
$node.on('click',function(e){
if(e.originalEvent.detail > 1){
return;
/* if you are returning a value from this
function then return false or cancel
the event some other way */
}
});
Done. If there is more than one click back to back, the second, third,etc. will not fire. I definitely prefer this to using any sort of timers.
I got myself pointed in this direction by reading this.
Incidentally: I was first researching this problem because I accidentally double clicked a paginated link, and the event fired and finished twice before the callback could happen.
Before coming up with the code above, I had
if e.originalEvent.detail === 2 //return
however, I was able to click on the link 3 times (a triple click), and though the second click didn't fire, the third did
In a comment, you said,
I delay the click handler by 300 ms (a noticeable and annoying delay) and even ...
So it sounds like what you want is that when you click then the DOM should geneate a click event immediately, except not if the click is the first click of a double-click.
To implement this feature, when you click, the DOM would need to be able to predict whether this is the final click or whether it's the first of a double-click (however I don't think is possible in general for the DOM to predict whether the user is about to click again).
What are the two distinct actions which you're trying to take on click and double-click? IMO, in a normal application you might want both events: e.g. single-click to focus on an element and then double-click to activate it.
When you must separate the events, some applications use something other than double-click: for example, they use right-click, or control-click.
You can use UIEvent.detail if you want to detect how many times the element was clicked and fire events based on that.
A simple example:
element.addEventListener("click", function (e) {
if (e.detail === 1) {
// do something if the element was clicked once.
} else if (e.detail === 2) {
// do something else if the element was clicked twice
}
});
In this case, it is best to delay the execution of the single click event slightly. Have your double click handler set a variable that the single click event will check. If that variable has a particular value, could be boolDoubleClick == true, then don't fire/handle the single click.
Thanks to all the other answers here as the combination of them seems to provide a reasonable solution for me when the interaction requires both, but mutually exclusive:
var pendingClick = 0;
function xorClick(e) {
// kill any pending single clicks
if (pendingClick) {
clearTimeout(pendingClick);
pendingClick = 0;
}
switch (e.detail) {
case 1:
pendingClick = setTimeout(function() {
console.log('single click action here');
}, 500);// should match OS multi-click speed
break;
case 2:
console.log('double click action here');
break;
default:
console.log('higher multi-click actions can be added as needed');
break;
}
}
myElem.addEventListener('click', xorClick, false);
Update: I added a generalized version of this approach along with a click polyfill for touch devices to this Github repo with examples:
https://github.com/mckamey/doubleTap.js
AFAIK DOM Level 2 Events makes no specification for double-click.
It doesn't work for me on IE7 (there's a shock), but FF and Opera have no problem managing the spec, where I can attach all actions to the click event, but for double-click just wait till the "detail" attribute of the event object is 2. From the docs: "If multiple clicks occur at the same screen location, the sequence repeats with the detail attribute incrementing with each repetition."
Here is what I did to distinguish within a module
node.on('click', function(e) {
//Prepare for double click, continue to clickHandler doesn't come soon enough
console.log("cleared timeout in click",_this.clickTimeout);
clearTimeout(_this.clickTimeout);
_this.clickTimeout = setTimeout(function(){
console.log("handling click");
_this.onClick(e);
},200);
console.log(_this.clickTimeout);
});
node.on('dblclick', function (e) {
console.log("cleared timeout in dblclick",_this.clickTimeout);
clearTimeout(_this.clickTimeout);
// Rest of the handler function
I use this solution for my project to prevent click event action, if I had dblclick event that should do different thing.
Note: this solution is just for click and dblclick and not any other thing like tripleclick or etc.
To see proper time between click and double click see this
sorry for my bad English.
I hope it helps :)
var button, isDblclick, timeoutTiming;
var clickTimeout, dblclickTimeout;
//-----
button = $('#button');
isDblclick = false;
/*
the proper time between click and dblclick is not standardized,
and is cutsomizable by user apparently (but this is windows standard I guess!)
*/
timeoutTiming = 500;
//-----
button.on('dblclick', function () {
isDblclick = true;
clearTimeout(dblclickTimeout);
dblclickTimeout = setTimeout(function () {
isDblclick = false;
}, timeoutTiming);
//-----
// here goes your dblclick codes
console.log('double clicked! not click.');
}).on('click', function () {
clearTimeout(clickTimeout);
clickTimeout = setTimeout(function () {
if(!isDblclick) {
// here goes your click codes
console.log('a simple click.');
}
}, timeoutTiming);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button type="button" id="button">
click/dblclick on this to see the result
</button>
It can be achieved via following code
var clickHandler = function(e) { /* put click event handling code here */ };
var doubleclickHandler = function(e) { /* put doubleclick event handling code here */ }
const maxMsBetweenClicks = 300;
var clickTimeoutId = null;
document.addEventListener("dblclick", handleDoubleClick);
document.addEventListener("click", handleSingleClick);
function handleSingleClick(e){
clearTimeout(clickTimeoutId);
clickTimeoutId = setTimeout( function() { clickHandler(e);}, maxMsBetweenClicks);
}
function handleDoubleClick(e){
clearTimeout(clickTimeoutId);
doubleclickHandler(e);
}
I know this is old as heck, but thought I'd post anyhow since I just ran into the same problem. Here's how I resolved it.
$('#alerts-display, #object-display').on('click', ['.item-data-summary', '.item-marker'], function(e) {
e.preventDefault();
var id;
id = setTimeout(() => {
// code to run here
return false;
}, 150);
timeoutIDForDoubleClick.push(id);
});
$('.panel-items-set-marker-view').on('dblclick', ['.summary', '.marker'], function(e) {
for (let i = 0; i < timeoutIDForDoubleClick.length; i++) {
clearTimeout(timeoutIDForDoubleClick[i]);
}
// code to run on double click
e.preventDefault();
});
Here is my simple solution to prevent the second click. Of course, I could restart the timeout when a double click detected, but in reality I never need it.
clickTimeoutId = null;
onClick(e) {
if (clickTimeoutId !== null) {
// Double click, do nothing
return;
}
// Single click
// TODO smth
clickTimeoutId = setTimeout(() => {
clearTimeout(clickTimeoutId);
clickTimeoutId = null;
}, 300);
}
Summarizing, to recognize the simpleClick and doubleClick events on the same element, just treat the onClick event with this method:
var EVENT_DOUBLE_CLICK_DELAY = 220; // Adjust max delay btw two clicks (ms)
var eventClickPending = 0;
function onClick(e){
if ((e.detail == 2 ) && (eventClickPending!= 0)) {
// console.log('double click action here ' + e.detail);
clearTimeout(eventClickPending);
eventClickPending = 0;
// call your double click method
fncEventDblclick(e);
} else if ((e.detail === 1 ) && (eventClickPending== 0)){
// console.log('sigle click action here 1');
eventClickPending= setTimeout(function() {
// console.log('Executing sigle click');
eventClickPending = 0
// call your single click method
fncEventClick(e);
}, EVENT_DOUBLE_CLICK_DELAY);
// } else { // do nothing
// console.log('more than two clicks action here ' + e.detail);
}
}
You can use debounce to free the single click handler from detecting the double/multiple clicks
Test at: https://jsfiddle.net/L3sajybp/
HTML
<div id='toDetect'>
Click or double-click me
</div>
<hr/>
<ol id='info'>
</ol>
JS
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this,
args = arguments;
const later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
function debounceSingleClickOnly(func, timeout = 500) {
function eventHandler (event) {
const { detail } = event;
if (detail > 1) {
console.log('no double click for you '+ func.name);
console.log('');
return;
}
func.apply(this, arguments);
}
return debounce(eventHandler, timeout);
}
window.toDetect.addEventListener('click', debounceSingleClickOnly(handleSingleClick));
window.toDetect.addEventListener('dblclick', handleDoubleClick);
function handleS() {
console.log('S func');
console.log(this.id);
}
function handleSingleClick(event) {
console.log('single click');
const divText = document.createElement('li');
divText.appendChild(document.createTextNode('single click'));
window.info.appendChild(divText)
console.group();
console.log('this element was single-clicked: ' + event.target.id);
console.log(this.id);
console.log('');
console.groupEnd();
}
function handleDoubleClick(event) {
console.log('double click');
const divText = document.createElement('li');
divText.appendChild(document.createTextNode('double click'));
window.info.appendChild(divText);
console.group();
console.log('this element was double-clicked: ' + event.target.id);
console.log(this.id);
console.log('');
console.groupEnd();
}
Output:
const toggle = () => {
watchDouble += 1;
setTimeout(()=>{
if (watchDouble === 2) {
console.log('double' + watchDouble)
} else if (watchDouble === 1) {
console.log("signle" + watchDouble)
}
watchDouble = 0
},200);
}