0

So i am trying to implement simple touch controls on a javascript game. I have the following answer from a search:

Snake Game with Controller Buttons for Mobile Use **UPDATED**

However I was trying to change this jquery into javascript so that it would work with my game

Jquery:

    $(document).on('click', '.button-pad > button', function(e) {
            if ($(this).hasClass('left-btn')) {
                e = 37;
            }

Javascript:

     var contoller = document.getElementById("button-pad").on('click', 
     '.button-pad > button', function(e) {
                if ('.button-pad > button'(this).hasClass('btn-left')) {
                     e = 37;
                 }

I thought I had it sorted but it is not working at all

Codepen here:

https://codepen.io/MrVincentRyan/pen/VqpMrJ?editors=1010

  • 3
    There is no native version of `on()` for Elements. Have you done any web searches for native javascript event delegation? – Taplar Dec 27 '18 at 17:53
  • 3
    Also `if ('.button-pad > button'(this).hasClass('btn-left')) {` is completely syntatically broken – Taplar Dec 27 '18 at 17:54
  • Ref. https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation – Taplar Dec 27 '18 at 18:00
  • `.on` is jquery . Use globaleventhandlers instead https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers – NVRM Dec 27 '18 at 18:16
  • 1
    @Cryptopat While `onXyz` element properties do work, their use is widely discouraged as they don't provide a robust way to managed events. Instead. always use `.addEventListener()`. – Scott Marcus Dec 27 '18 at 18:18
  • I agree (propagation, bubbling), but for a one time action. i use it often like so as example. It's good to learn to me `onXyz = (function(e){ ... })` Then it's possible to use `dispatchevent` to bubble if need (From other es6 modules too, avoid conflicts, i like it) – NVRM Dec 27 '18 at 18:21
  • @Cryptopat It has nothing to do with propagation or bubbling. Simply, you can never register more than one callback to any given event. As I said, while event properties "work", they are generally discouraged. – Scott Marcus Dec 27 '18 at 18:27
  • @ScottMarcus sure you can register multiple callbacks to a single event http://jsfiddle.net/8nk2c6tj/ – Taplar Dec 27 '18 at 18:33
  • @Taplar I think it was what he meant. Totally got it. Good to learn! It's not a bug, it's a feature! – NVRM Dec 27 '18 at 18:45
  • @Taplar You are showing `.addEventListener()`, which is what I'm saying to use. When I said *you can never register more than one callback to any given event* it was in response to Cryptopat advocating `onXyz` event properties. – Scott Marcus Dec 27 '18 at 18:46
  • Ahhh, you're talking about inline bindings. herp derp – Taplar Dec 27 '18 at 18:47
  • @Taplar No, I was talking about DOM event properties, not HTML inline event attributes. But, both are discouraged. – Scott Marcus Dec 27 '18 at 18:50
  • Doesn't the `` map to the Element.onclick ? @ScottMarcus I associate them as the same thing. http://jsfiddle.net/qk3eo8n2/ – Taplar Dec 27 '18 at 18:53
  • @Taplar No, element attributes do have DOM object property counterparts, but with events, the object event properties don't have some of the side-effects that the HTML element attributes do. Event attributes create anonymous global wrapper functions that wrap the value of the event attribute (that's why you include the `()` in event attributes (i.e. `onclick=foo()`) and you don't with event properties (i.e. `onclick = foo`). These global wrappers modify `this` bindings in the actual callback. – Scott Marcus Dec 27 '18 at 18:56
  • 1
    Interesting. New things to think about (and continue to avoid all together), heh. @ScottMarcus – Taplar Dec 27 '18 at 19:05
  • @Taplar FYI: https://jsfiddle.net/f0unvpk6/5/ – Scott Marcus Dec 27 '18 at 19:14

3 Answers3

3

Your existing code has some problems with it, but it was close enough where I could translate it. However, your current code seems to want to reassign the event argument being passed to the click handler (e) to 37. This makes no sense. Most likely you just want another variable set to 37 and that's what I've done below:

spaceInvader(window, document.getElementById('space-invader'));
window.focus();

let game = null;
let ship = null;

function spaceInvader (window, canvas) {

    canvas.focus();
    var context = canvas.getContext('2d');

    /* GAME */

    function Game () {
        this.message = '';
        this.rebel = [];
        this.republic = [];
        this.other = [];
        this.size = {x: canvas.width, y: canvas.height};
        this.wave = 0;

        this.refresh = function () {
            this.update();
            this.draw();
            requestAnimationFrame(this.refresh);
        }.bind(this);

        this.init();
    }
    Game.MESSAGE_DURATION = 1500;
    Game.prototype.init = function () {
        this.ship = new Ship(this);
        this.addRebel(this.ship);
        this.refresh();
    };
    Game.prototype.update = function () {
        this.handleCollisions();
        this.computeElements();
        this.elements.forEach(Element.update);
        if (!this.rebel.length) {
            this.showText('Gatwick closed', true);
            return;
        }
        if (!this.republic.length) this.createWave();
    };
    Game.prototype.draw = function () {
        context.clearRect(0, 0, this.size.x, this.size.y);
        this.elements.forEach(Element.draw);
        Alien.drawLife(this.republic);
        if (this.message) {
            context.save();
            context.font = '30px Arial';
            context.textAlign='center';
            context.fillStyle = '#FFFFFF';
            context.fillText(this.message, canvas.width / 2, canvas.height / 2);
            context.restore();
        }
    };
    Game.prototype.computeElements = function () {
        this.elements = this.other.concat(this.republic, this.rebel);
    };
    Game.prototype.addRebel = function (element) {
        this.rebel.push(element);
    };
    Game.prototype.addRepublic = function (element) {
        this.republic.push(element);
    };
    Game.prototype.addOther = function (element) {
        this.other.push(element);
    };
    Game.prototype.handleCollisions = function () {
        this.rebel.forEach(function(elementA) {
            this.republic.forEach(function (elementB) {
                if (!Element.colliding(elementA, elementB)) return;
                elementA.life--;
                elementB.life--;
                var sizeA = elementA.size.x * elementA.size.y;
                var sizeB = elementB.size.x * elementB.size.y;
                this.addOther(new Explosion(this, sizeA > sizeB ? elementA.pos : elementB.pos));
            }, this);
        }, this);
        this.republic = this.republic.filter(Element.isAlive);
        this.rebel = this.rebel.filter(Element.isAlive);
        this.other = this.other.filter(Element.isAlive);
        this.republic = this.republic.filter(this.elementInGame, this);
        this.rebel = this.rebel.filter(this.elementInGame, this);
    };
    Game.prototype.elementInGame = function (element) {
        return !(element instanceof Bullet) || (
            element.pos.x + element.halfWidth > 0 &&
            element.pos.x - element.halfWidth < this.size.x &&
            element.pos.y + element.halfHeight > 0 &&
            element.pos.y - element.halfHeight < this.size.x
        );
    };
    Game.prototype.createWave = function () {
        this.ship.life = Ship.MAX_LIFE;
        this.ship.fireRate = Math.max(50, Ship.FIRE_RATE - 50 * this.wave);
        this.wave++;
        this.showText('Wave: ' + this.wave);
        var waveSpeed = Math.ceil(this.wave / 2);
        var waveProb = (999 - this.wave * 2) / 1000;
        var margin = {x: Alien.SIZE.x + 10, y: Alien.SIZE.y + 10};
        for (var i = 0; i < 2; i++) {
            var x = margin.x + (i % 8) * margin.x;
            var y = -200 + (i % 3) * margin.y;
            this.addRepublic(new Alien(this, {x: x, y: y}, waveSpeed, waveProb));
        }
    };
    Game.prototype.showText = function (message, final) {
        this.message = message;
        if (!final) setTimeout(this.showText.bind(this, '', true), Game.MESSAGE_DURATION);
    };

    /* GENERIC ELEMENT */

    function Element (game, pos, size) {
        this.game = game;
        this.pos = pos;
        this.size = size;
        this.halfWidth = Math.floor(this.size.x / 2);
        this.halfHeight = Math.floor(this.size.y / 2);
    }
    Element.update = function (element) {
        element.update();
    };
    Element.draw = function (element) {
        element.draw();
    };
    Element.isAlive = function (element) {
        return element.life > 0;
    };
    Element.colliding = function (elementA, elementB) {
        return !(
            elementA === elementB ||
            elementA.pos.x + elementA.halfWidth < elementB.pos.x - elementB.halfWidth ||
            elementA.pos.y + elementA.halfHeight < elementB.pos.y - elementB.halfHeight ||
            elementA.pos.x - elementA.halfWidth > elementB.pos.x + elementB.halfWidth ||
            elementA.pos.y - elementA.halfHeight > elementB.pos.y + elementB.halfHeight
        );
    };

    /* SHIP */

    function Ship(game) {
        var pos = {
            x: Math.floor(game.size.x / 2) - Math.floor(Ship.SIZE.x / 2),
            y: game.size.y - Math.floor(Ship.SIZE.y / 2)
        };
        Element.call(this, game, pos, Ship.SIZE);
        this.kb = new KeyBoard();
        this.speed = Ship.SPEED;
        this.allowShooting = true;
        this.life = Ship.MAX_LIFE;
        this.fireRate = Ship.FIRE_RATE;
    }
    Ship.SIZE = {x: 67, y: 100};
    Ship.SPEED = 8;
    Ship.MAX_LIFE = 5;
    Ship.FIRE_RATE = 200;
    Ship.prototype.update = function () {
      if (this.kb.isDown(KeyBoard.KEYS.LEFT) && this.pos.x - this.halfWidth > 0) {
        this.pos.x -= this.speed;
      } else if (this.kb.isDown(KeyBoard.KEYS.RIGHT) && this.pos.x + this.halfWidth < this.game.size.x) {
        this.pos.x += this.speed;
      }
    
      if (this.allowShooting && this.kb.isDown(KeyBoard.KEYS.SPACE)) {
        var bullet = new Bullet(
          this.game,
          {x: this.pos.x, y: this.pos.y - this.halfHeight },
          { x: 0, y: -Bullet.SPEED },
          true
        );
        this.game.addRebel(bullet);
        this.toogleShooting();
      }
    };
    
    Ship.prototype.draw = function () {
        var img = document.getElementById('ship');
        context.save();
        context.translate(this.pos.x - this.halfWidth, this.pos.y - this.halfHeight);
        context.drawImage(img, 0, 0);
        context.restore();
        this.drawLife();
    };
    
    Ship.prototype.drawLife = function () {
        context.save();
        context.fillStyle = 'white';
        context.fillRect(this.game.size.x -112, 10, 102, 12);
        context.fillStyle = 'red';
        context.fillRect(this.game.size.x -111, 11, this.life * 100 / Ship.MAX_LIFE, 10);
        context.restore();
    };
    
    Ship.prototype.toogleShooting = function (final) {
        this.allowShooting = !this.allowShooting;
        if (!final) setTimeout(this.toogleShooting.bind(this, true), this.fireRate);
    };

    /* ALIENS */

    function Alien(game, pos, speed, shootProb) {
        Element.call(this, game, pos, Alien.SIZE);
        this.speed = speed;
        this.shootProb = shootProb;
        this.life = 3;
        this.direction = {x: 1, y: 1};
    }
    Alien.SIZE = {x: 51, y: 60};
    Alien.MAX_RANGE = 350;
    Alien.CHDIR_PRO = 0.990;
    Alien.drawLife = function (array) {
        array = array.filter(function (element) {
            return element instanceof Alien;
        });
        context.save();
        context.fillStyle = 'white';
        context.fillRect(10, 10, 10 * array.length + 2, 12);
        array.forEach(function (alien, idx) {
            switch (alien.life) {
                case 3:
                    context.fillStyle = 'green';
                    break;
                case 2:
                    context.fillStyle = 'yellow';
                    break;
                case 1:
                    context.fillStyle = 'red';
                    break;
            }
            context.fillRect(10 * idx + 11, 11, 10, 10);
        });
        context.restore();
    };
    Alien.prototype.update = function () {
        if (this.pos.x - this.halfWidth <= 0) {
            this.direction.x = 1;
        } else if (this.pos.x + this.halfWidth >= this.game.size.x) {
            this.direction.x = -1;
        } else if (Math.random() > Alien.CHDIR_PRO) {
            this.direction.x = -this.direction.x;
        }
        if (this.pos.y - this.halfHeight <= 0) {
            this.direction.y = 1;
        } else if (this.pos.y + this.halfHeight >= Alien.MAX_RANGE) {
            this.direction.y = -1;
        } else if (Math.random() > Alien.CHDIR_PRO) {
            this.direction.y = -this.direction.y;
        }
        this.pos.x += this.speed * this.direction.x;
        this.pos.y += this.speed * this.direction.y;

        if (Math.random() > this.shootProb) {
            var bullet = new Bullet(
                this.game,
                {x: this.pos.x, y: this.pos.y + this.halfHeight },
                { x: Math.random() - 0.5, y: Bullet.SPEED },
                false
            );
            this.game.addRepublic(bullet);
      }
    };
    Alien.prototype.draw = function () {
        var img = document.getElementById('fighter');
        context.save();
        context.translate(this.pos.x + this.halfWidth, this.pos.y + this.halfHeight);
        context.rotate(Math.PI);
        context.drawImage(img, 0, 0);
        context.restore();
    };

    /* BULLET */

    function Bullet(game, pos, direction, isRebel) {
        Element.call(this, game, pos, Bullet.SIZE);
        this.direction = direction;
        this.isRebel = isRebel;
        this.life = 1;

        try {
            var sound = document.getElementById('sound-raygun');
            sound.load();
            sound.play().then(function () {}, function () {});
        }
        catch (e) {
            // only a sound issue
        }
    }
    Bullet.SIZE = {x: 6, y: 20};
    Bullet.SPEED = 3;
    Bullet.prototype.update = function () {
        this.pos.x += this.direction.x;
        this.pos.y += this.direction.y;
    };
    Bullet.prototype.draw = function () {
        context.save();
        var img;
        if (this.isRebel) {
            context.translate(this.pos.x - this.halfWidth, this.pos.y - this.halfHeight);
            img = document.getElementById('rebel-bullet');
        }
        else {
            context.translate(this.pos.x + this.halfWidth, this.pos.y + this.halfHeight);
            img = document.getElementById('republic-bullet');
            context.rotate(Math.PI);
        }
        context.drawImage(img, 0, 0);
        context.restore();
    };

    /* EXPLOSION */

    function Explosion(game, pos) {
        Element.call(this, game, pos, Explosion.SIZE);
        this.life = 1;
        this.date = new Date();

        try {
            var sound = document.getElementById('sound-explosion');
            sound.load();
            sound.play().then(function () {}, function () {});
        }
        catch (e) {
            // only a sound issue
        }
    }
    Explosion.SIZE = {x: 115, y: 100};
    Explosion.DURATION = 150;
    Explosion.prototype.update = function () {
        if (new Date() - this.date > Explosion.DURATION) this.life = 0;
    };
    Explosion.prototype.draw = function () {
        var img = document.getElementById('explosion');
        context.save();
        context.translate(this.pos.x - this.halfWidth, this.pos.y - this.halfHeight);
        context.drawImage(img, 0, 0);
        context.restore();
    };

    /* KEYBOARD HANDLING */

    function KeyBoard() {
        var state = {};
        window.addEventListener('keydown', function(e) {
            state[e.keyCode] = true;
        });
        window.addEventListener('keyup', function(e) {
            state[e.keyCode] = false;
        });
        this.isDown = function (key) {
            return state[key];
        };
    }
    KeyBoard.KEYS = {
        LEFT: 37,
        RIGHT: 39,
        SPACE: 32
    };
   
   
    window.addEventListener('load', function() {
        game = new Game();

    });
    
   
// Get all the button elements that are children of elements that have 
// the .button-pad class and convert the resulting node list into an Array
let elements = 
  Array.prototype.slice.call(document.querySelectorAll('.button-pad button'));

// Loop over the array    
elements.forEach(function(el){

  el.textContent = "XXXX";
  // Set up a click event handler for the current element being iterated:
  el.addEventListener('click', function(e) {

    // When the element is clicked, check to see if it uses the left-btn class
    if(this.classList.contains('left-btn')) {

      // Perform whatever actions you need to:
      ship.update();
    }
  });
});  
   
}
<h1>Gatwick invaders</h1>
<p>Press <b>left arrow</b> to go left, <b>right arrow</b> to go right, and <b>space</b> to shoot...</p>
<canvas id="space-invader" width="640" height="500" tabindex="0"></canvas>
<img id="fighter" src="https://raw.githubusercontent.com/MrVIncentRyan/assets/master/drone1.png" />
<img id="ship" src="https://raw.githubusercontent.com/MrVIncentRyan/assets/master/cop1.png" />
<img id="rebel-bullet" src="https://raw.githubusercontent.com/OlivierB-OB/starwars-invader/master/rebelBullet.png" />
<img id="republic-bullet" src="https://raw.githubusercontent.com/OlivierB-OB/starwars-invader/master/republicBullet.png" />
<img id="explosion" src="https://raw.githubusercontent.com/OlivierB-OB/starwars-invader/master/explosion.png" />
<audio id="sound-explosion" src="https://raw.githubusercontent.com/OlivierB-OB/starwars-invader/master/explosion.mp3"></audio>
<audio id="sound-raygun" src="https://raw.githubusercontent.com/OlivierB-OB/starwars-invader/master/raygun.mp3"></audio>

 </div>

 <div class="button-pad">

  <div class="btn-up">
   <button type="submit" class="up">
       <img src="http://aaronblomberg.com/sites/ez/images/btn-up.png" />
      </button>
  </div>
  
  <div class="btn-right">
   <button type="submit" class="right">
       <img src="http://aaronblomberg.com/sites/ez/images/btn-right.png" />
      </button>
  </div>
  
  <div class="btn-down">
   <button type="submit" class="down">
       <img src="http://aaronblomberg.com/sites/ez/images/btn-down.png" />
      </button>
  </div>
  
  <div class="btn-left">
   <button type="submit" class="left">
       <img src="http://aaronblomberg.com/sites/ez/images/btn-left.png" />
      </button>
  </div>

 </div>
Scott Marcus
  • 57,085
  • 6
  • 34
  • 54
  • 37 is the button code for the left arrow on the keyboard. I am trying to use an onscreen button to have the same reaction as hitting left arrow. – Vincent Ryan Dec 27 '18 at 18:33
  • @VincentRyan What would you expect the on screen action to be in this case? Instead of trying to trigger keystroke `37`, we just perform the action that the keystroke would have done. – Scott Marcus Dec 27 '18 at 18:43
  • This is the action that would have been triggered. I thought it would be simpler to try trigger the key stroke: Ship.prototype.update = function () { if (this.kb.isDown(KeyBoard.KEYS.LEFT) && this.pos.x - this.halfWidth > 0) { this.pos.x -= this.speed; – Vincent Ryan Dec 27 '18 at 18:54
  • @VincentRyan We can't trigger keystrokes in JavaScript, so we just perform the action(s) that the keystroke would have performed. See my updated answer that includes your code. – Scott Marcus Dec 27 '18 at 18:59
  • if(this.classList.contains('left-btn') { Unexpected token error. – Vincent Ryan Dec 27 '18 at 19:05
  • @VincentRyan Oops. Just missing a closing paren. I'll update. – Scott Marcus Dec 27 '18 at 19:06
  • will keyboard controls still work when this is implemented. Thanks by the way – Vincent Ryan Dec 27 '18 at 19:10
  • Hmm still nothing – Vincent Ryan Dec 27 '18 at 19:13
  • @VincentRyan Yes, keyboard actions continue to work as normal. As for nothing, it most likely has to do with that bit of `Ship` code you told me about. I can't say what that does or if it's correct. – Scott Marcus Dec 27 '18 at 19:15
  • @VincentRyan That code appears to simply define the `update` function for a `Ship`, but it doesn't call the `update` function, so nothing happens. I would think that you'd want that code somewhere else and have it run all the time early on. Then, in the location where it currently is, you'd want something like `myShip.update()` (substituting your actual `Ship` instance name for `myShip`. – Scott Marcus Dec 27 '18 at 19:18
  • The whole codebase is here https://codepen.io/MrVincentRyan/pen/VqpMrJ?editors=1010 . This seems an unnecessarily complex way to achieve a simple action. – Vincent Ryan Dec 27 '18 at 19:25
  • I'm still struggling to implement it. Sorry. – Vincent Ryan Dec 27 '18 at 19:57
  • @VincentRyan The issue isn't with the code I gave you. The issue is that your `Ship.upate` needs to be called from the clicking of the buttons, but the `Ship` function requires an instance of `Ship` and an instance of `Game` be passed to it and I don't see where you are doing any of that. This is well beyond your original question. – Scott Marcus Dec 27 '18 at 19:57
  • Your solution rather changed how I was approaching the problem. I don't think it is as simple as calling an update Ship fuction. – Vincent Ryan Dec 27 '18 at 20:07
  • @VincentRyan The click event of the "left" and "right" buttons need to simply do whatever the action(s) associated with the pressing of the "left" and "right" arrow keys are. It really is that simple. The problem is that you seem to be working with code that you don't fully understand and that code has some issues that need to be resolved first. – Scott Marcus Dec 27 '18 at 20:12
  • The code works fine it is just the way that it implements the direction controls that seems a bit complicated. As it takes points and then redraws the ship. That is why I was trying to just mirror the use of the keys. – Vincent Ryan Dec 27 '18 at 20:33
  • Thanks for this. I think I can bash the rest of it out. – Vincent Ryan Dec 27 '18 at 21:40
  • If you feel like doing a fully worked example feel free – Vincent Ryan Dec 27 '18 at 22:29
1

A custom solution for emulating keypresses on mobile in both vanilla Javascript as well as jQuery!

// jQuery (edge), for use with ES2015-19
/*

$(document).on("click", ".example-btn", e => { // Click event handler
    if($(this).hasClass("example-btn")) { // Verifying that element has class
    e = 37
    jQuery.event.trigger({type: "keypress", which: character.charCodeAt(e)}) // Simulating keystroke

    // The following is simply for debugging, remove if needed
    alert("Button validation confirmed!")
    console.log("E: ", e)
  }
})

*/



// Pure Javascript (ECMA Standard)

document.querySelector(".example-btn").addEventListener("click", function(e) { // Click event handler
    if(this.classList.contains("example-btn")) { // Verifying that element has class
    e = 37

    if(document.createEventObject) {
        var eventObj = document.createEventObject();
      eventObj.keyCode = e;
      document.querySelector(".example-btn").fireEvent("onkeydown", eventObj);
    } else if(document.createEvent) {
        var eventObj2 = document.createEvent("Events");
      eventObj2.initEvent("keydown", true, true);
      eventObj2.which = e;
      document.querySelector(".example-btn").dispatchEvent(eventObj2);
    }

    // The following is simply for debugging, remove if needed
    alert("Button validation confirmed!");
    console.log("E: ", e);
  }
});



// ---------------------------------------------------------------------------------------------------
/* 
You can not use the "this" statement when referring to an embedded element. In your previous code "this" would refer to ".button-container > .example-btn" which the compiler will interpret as only the parent element, being .button-container (.button-pad in your code) not the child element in which you want. Also there is no such thing as returning a character code and expecting it to automatically know what to do with it. I assume you are doing this to emulate a keystroke on a mobile device and I assure you that this design works although it might be flawed. Give it a try and I hope it does something to at least help if not solve your problem. 
*/
// ---------------------------------------------------------------------------------------------------
  • Hey man thanks for this. I am indeed trying to emulate the button. This looks very promising. It is definitely doing something. However the ship element is still not moving. I'll work through it a little more – Vincent Ryan Dec 29 '18 at 15:15
  • I get the confirmation "button validation confirmed!" but even when I remove that code it still doesn't make the ship move! – Vincent Ryan Dec 29 '18 at 15:20
  • I think the issue is with e. So now if I have it trigger the same button. It triggers all functions to do with e so I get the ship flying to the right and firing continually although I have triggered go left....! – Vincent Ryan Dec 29 '18 at 17:29
  • Hmmm... I'm not sure why it wouldn't move. Have you perhaps tried replacing the keydown keyword with keypress or keyup? –  Jan 05 '19 at 10:07
0

When an event listener is attached to an element, that listener is not unique for the element, but it propagates to its children. This functionality is enabled in jQuery by adding a parameter on an event listener a parameter that targets the element that we want. This is not case in vanillaJS, but using e.target we can inspect in which elements the event is executed.

Probably your are looking something like this. However, I would prefer to add an id in the button so you can more easily work with it.

document.addEventListener('click', function(e){
                if(e.target.tagName === 'BUTTON' && e.target.classList.value.includes('btn-left')){
                    // execute your code
                }
        });
Marios Simou
  • 159
  • 2
  • 7