I'm creating a simple asteroids-like game in CSS and JS using the DOM over canvas for...experimentation purposes.
My code is pretty small in this example to make it easy to see what's going on below. The ultimate goal: Let arrow keys smoothly rotate and translate the spaceship around the window without creating an infinite amount of transforms. I think I'm 90% there:
Use the arrow keys to control the snippet below.
'use strict';
function defineDistances() {
var distance = {};
distance.up = -1;
distance.right = 1;
distance.down = 1;
distance.left = -1;
return distance;
}
function defineKeys() {
var keys = {};
keys.up = 38;
keys.right = 39;
keys.down = 40;
keys.left = 37;
return keys;
}
function checkKeys( e ) {
var triBx = document.getElementById( 'v-wrp' ),
keys = defineKeys(),
distance = defineDistances();
switch( e.keyCode ) {
case keys.up:
triBx.style.transform += 'translateY(' + distance.up + 'px)';
break;
case keys.right:
triBx.style.transform += 'rotate(' + distance.right + 'deg)';
break;
case keys.down:
triBx.style.transform += 'translateY(' + distance.down + 'px)';
break;
case keys.left:
triBx.style.transform += 'rotate(' + distance.left + 'deg)';
break;
}
}
function detectMovement( e ) {
setInterval (
function() {
checkKeys( e );
},
1000/24
);
}
function start() {
window.addEventListener( 'keydown', detectMovement );
preventBrowserWindowScroll()
}
start();
@import url( "https://fonts.googleapis.com/css?family=Nunito" );
html {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-family: "Nunito", sans-serif;
font-size: 2rem;
}
.v {
display: block;
transform: rotate( 180deg );
}
<div id="v-wrp" class="v-wrp">
<b class="v">V</b>
</div>
<script>
function preventBrowserWindowScroll() {
window.addEventListener( 'keydown', function( e ) {
// space and arrow keys
if([32, 37, 38, 39, 40].indexOf( e.keyCode ) > -1 ) {
e.preventDefault();
}
}, false )
}
</script>
If you inspect the v-wrp
element in the browser you can see the transforms get added endlessly.
The reason I use +=
to add transforms is to avoid this problem: Reset CSS transform origin after translation / rotation
( The transform-origin
doesn't move with the element as it moves, causing undesired effects unless all transforms are added in addition to the previous ones... )
So how do I overcome these challenges? I suspect the snippet is so choppy because of the endless transforms being added. How do I get this working similarly to the way it is now without all the memory loss/ choppiness/ bugginess / endless transforms?
Edit: Another major problem is how the ship will travel in the same directions continuously once the keys are pressed, even going in a circular like pattern if you hit the correct keys. I want it to drift like in space but not turn once the keys are let go. The trajectory should stay straight as it "floats" What am I doing wrong?.
For more info and a demo of the answer below see https://stackoverflow.com/a/43744006/3877726
Using CSS transform : matrix function
If given a object position, scale and rotation the quickest way to set the transform is to do it as a single matrix element.style.transform = "matrix(a,b,c,d,e,f)";
The 6 values represent the direction and scale of the X axis (a,b), Y axis (c,d) , and the local origin (e,f)
As most of the time you don't want to skew and the scale is uniform (x and y scale the same) the function to create and set the transform is quick. All you do is pass the position, scale and rotation.
const setElementTransform = (function(){
const matrix = [1,0,0,1,0,0]; // predefine the array (helps ease the GC load
const m = matrix; // alias for code readability.
return function(element, x, y, scale, rotation);
m[3] = m[0] = Math.cos(rotation) * scale; // set rotation and scale
m[2] = -(m[1] = Math.sin(rotation) * scale); // set rotation and scale
m[4] = x;
m[5] = y;
element.style.transform = `matrix(${m.join(",")})`;
}
}());
Don't use keyboardEvent.keyCode
it has depreciated.
Rather than use the old (and obscure key values) keyCode
property to read the keys you should use the code
property that has a string representing which key is down or up.
const keys = {
ArrowLeft : false, // add only the named keys you want to listen to.
ArrowRight: false,
ArrowUp : false,
ArrowDown : false,
stopKeyListener : (function(){ // adds a listener and returns function to stop key listener if needed.
function keyEvent(e){
if(keys[e.code] !== undefined){ // is the key on the named list
keys[e.code] = e.type === "keydown"; // set true if down else false
e.preventDefault(); // prevent the default Browser action for this key.
}
addEventListener("keydown",keyEvent);
addEventListener("keyup",keyEvent);
return function(){
removeEventListener("keydown",keyEvent);
removeEventListener("keyup",keyEvent);
}
}()) //
}
Now at any time you can just check if a key is down with if(keys.ArrowLeft){
Updating the DOM regularly? use requestAnimationFrame
If you are making many changes to the DOM at regular intervals you should use requestAnimationFrame
and it tells the browser your intention and will cause all DOM changes made from within the callback to sync with the display hardware and the DOM's own compositing and rendering.
requestAnimationFrame(mainLoop); // will start the animation once code below has been parse and executed.
var player = { // the player
x : 0,
y : 0,
scale : 1,
rotate : 0,
speed : 0,
element : document.getElementById("thePlayer")
}
function mainLoop(time){ // requestAnimationFrame adds the time as the first argument for the callback
if(keys.ArrowLeft){ player.rotate -= 1 }
if(keys.ArrowRight){ player.rotate += 1 }
if(keys.ArrowUp){ player.speed += 1 }
if(keys.ArrowRight){ player.speed -= 1 }
player.x += Math.cos(player.rotate) * player.speed;
player.y += Math.sin(player.rotate) * player.speed;
setElementTransform(
player.element,
player.x, player.y,
player.scale,
player.rotate
);
requestAnimationFrame(mainLoop);
}
For a demo https://stackoverflow.com/a/43744006/3877726 (same link as at top of answer)
I have done a rapid answer - probably there are some aspect to smooth, but you'll get the idea: (ES6 code)
'use strict'
class Ship {
constructor (elem) {
this.posX = 0;
this.posY = 0;
this.deg = 0;
this.rad = 0;
this.speed = 0;
}
update (event) {
switch( event.key ) {
case "ArrowUp":
this.speed += 5;
break;
case "ArrowDown":
this.speed -= 5;
if (this.speed < 0) this.speed = 0;
break;
case "ArrowRight":
this.deg += 3;
break;
case "ArrowLeft":
this.deg -= 3;
break;
}
this.rad = (this.deg + 90) * Math.PI / 180;
}
move () {
this.posX += this.speed * Math.cos(this.rad);
this.posY += this.speed * Math.sin(this.rad);
if (this.speed > 0) {
this.speed -= 0.1;
}
if (this.elem == undefined) {
this.elem = document.getElementById('ship');
}
var translation = 'translate(' + this.posX +'px, ' + this.posY + 'px) ';
var rotation = 'rotate(' + this.deg + 'deg)';
this.elem.style.transform = translation + rotation;
}
}
var ship = new Ship
function update( e ) {
ship.update(e);
return false;
}
function start() {
window.addEventListener( 'keydown', update );
setInterval (
function() {
ship.move();
},
1000 / 24
);
}
start();
#ship {
position: absolute;
left: 50%;
top: 50%;
}
<div id="ship">V</div>
I think the problem is the detectMovement is calling the checkKeys again and again in infinite loop with same event e.
I tried adding listeners for keyup, keydown, keyleft and keyright so that checkkeys is called only when these keys are pressed.
Please comment if I have understood wrongly
'use strict';
function defineDistances() {
var distance = {};
distance.up = -1;
distance.right = 1;
distance.down = 1;
distance.left = -1;
return distance;
}
function defineKeys() {
var keys = {};
keys.up = 38;
keys.right = 39;
keys.down = 40;
keys.left = 37;
return keys;
}
function checkKeys( e ) {
e.preventDefault();
var triBx = document.getElementById( 'v-wrp' ),
keys = defineKeys(),
distance = defineDistances();
switch( e.keyCode ) {
case keys.up:
triBx.style.transform += 'translateY(' + distance.up + 'px)';
break;
case keys.right:
triBx.style.transform += 'rotate(' + distance.right + 'deg)';
break;
case keys.down:
triBx.style.transform += 'translateY(' + distance.down + 'px)';
break;
case keys.left:
triBx.style.transform += 'rotate(' + distance.left + 'deg)';
break;
}
}
function detectMovement( e ) {
setInterval (
function() {
checkKeys( e );
},
1000/24
);
}
function start() {
window.addEventListener( 'keydown', checkKeys );
window.addEventListener( 'keyup', checkKeys );
window.addEventListener( 'keyright', checkKeys );
window.addEventListener( 'keyleft', checkKeys );
}
start();
@import url( "https://fonts.googleapis.com/css?family=Nunito" );
html {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-family: "Nunito", sans-serif;
font-size: 2rem;
}
.v {
display: block;
transform: rotate( 180deg );
}
<div id="v-wrp" class="v-wrp">
<b class="v">V</b>
</div>
<script>
function preventBrowserWindowScroll() {
window.addEventListener( 'keydown', function( e ) {
// space and arrow keys
if([32, 37, 38, 39, 40].indexOf( e.keyCode ) > -1 ) {
e.preventDefault();
}
}, false )
}
</script>