How do I give this spaceship acceleration?
Asked Answered
C

3

3

Have a very small snippet of an asteroids-like game I'm working on using only the DOM without Canvas. I have the "ship" moving pretty smoothly when arrow keys are pressed but how would I go about making the ship accelerate ( in speed and rotation ) when an arrow key is held down for a longer length of time?

window.onkeyup = function( e ) {
  var kc = e.keyCode;
  e.preventDefault();

  if ( kc === 37 ) Keys.left = false;
  else if ( kc === 38 ) Keys.up = false;
  else if ( kc === 39 ) Keys.right = false;
  else if ( kc === 40 ) Keys.down = false;
};

function update() {
  if ( Keys.up ) {
    document.querySelector( 'div' ).style.transform += 'translateY( -1px )';
  }
  else if ( Keys.down ) {
    document.querySelector( 'div' ).style.transform += 'translateY( 1px )';
  }

  if ( Keys.left ) {
    document.querySelector( 'div' ).style.transform += 'rotate( -1deg )';
  }
  else if ( Keys.right ) { 
    document.querySelector( 'div' ).style.transform += 'rotate( 1deg )';
  }
  
  requestAnimationFrame( update );
}
requestAnimationFrame( update );
@import url( "https://fonts.googleapis.com/css?family=Nunito" );

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: "Nunito", sans-serif;
  font-size: 2rem;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
}

b {
  display: block;
  transform: rotate( 180deg );
}
<div>
  <b>v</b>
</div>

<script>
  var Keys = {
    up: false,
    down: false,
    left: false,
    right: false
  }

  window.onkeydown = function( e ) {
    var kc = e.keyCode;
    e.preventDefault();

    if ( kc === 37 ) Keys.left = true;
    else if ( kc === 38 ) Keys.up = true;
    else if ( kc === 39 ) Keys.right = true;
    else if ( kc === 40 ) Keys.down = true;
  };
</script>

Use the arrow keys to control snippet.

Certain answered 2/5, 2017 at 5:7 Comment(0)
C
8

Updated. As another similar question had a answer based on this previous version answer I have changed the answer to the better answer.

Transformations, Acceleration, Drag, and Rocket Ships.

There are so many ways to apply movement to a asteroids type game. This answer shows the most basic method and then gives an example showing variations on the basic methods to produce different feels. This answer also give a brief overview of how to set the CSS transformation using matrix (2D)

The basics

At the most basic you have a number that represents some component of the position or rotation. To move you add a constant x += 1; move in x one unit at a time when you let go of the control you don`t add and you stop.

But things don't move like that, they accelerate. So you create a second value that holds the speed (formerly the one from x += 1) and call it dx (delta X). When you get input you increase the speed by some small amount dx += 0.01 so that in time the speed increases gradually.

But the problem is the longer you hold the controls the faster you go, and when you let go of the controls the ship keeps going (which is all normal for space but a pain in a game) so you need to limit the speed and bring it gradually back down to zero. You can do that by applying a scale to the delta X value each frame. dx *= 0.99;

Thus you have the basic acceleration, drag, and speed limited value

x += dx;
dx *= 0.99;
if(input){ dx += 0.01);

Do that for both the x, y and angle. Where input is directional you need to use vectors for x, y as follows.

x += dx;
y += dy;
angle += dAngle;
dx *= 0.99;
dy *= 0.99;
dAngle *= 0.99;
if(turnLeft){
     dAngle += 0.01;
}
if(turnRight){
     dAngle -= 0.01;
}
if(inputMove){ 
    dx += Math.cos(angle) * 0.01;
    dy += Math.sin(angle) * 0.01;
}

That is the most basic space game movement.

Setting the CSS transform.

Setting the CSS transform is easiest apply via the matrix command. eg setting default transform element.style.transform = "matrix(1,0,0,1,0,0)";

The six values often named a,b,c,d,e 'matrix(a,b,c,d,e,f)' or m11, m12, m21, m22, m31, m32 or horizontal scaling, horizontal skewing, vertical skewing, vertical scaling, horizontal moving, vertical moving and represent a shortened version of the 3 by 3 2D matrix.

I find that the majority of confusion about how this matrix works and why it is not often used is in part due to the naming of the variables. I prefer the description as x axis x, x axis y, y axis x, y axis y, origin x, origin y and simply describe the direction and scale of the x and y axis and the position of the origin all in CSS pixel coordinates.

The following image illustrates the matrix. The red box is a element that has been rotated 45 deg (Math.PI / 4 radians) and its origin moved to CSS pixel coordinated 16,16.

Grid shows CSS pixels. The right grid shows a zoomed view of the matrix showing the X Axis vector (a,b) = (cos(45), sin(45)), Y Axis vector (c,d) = (cos(45 + 90), sin(45 + 90)) and the Origin (e,f) = (16, 16)

Image Grid shows CSS pixels. The right grid shows a zoomed view of the matrix showing the X Axis vector (a,b) = (cos(45), sin(45)), Y Axis vector (c,d) = (cos(45 + 90), sin(45 + 90)) and the Origin (e,f) = (16, 16)

Thus is I have the values for an element in terms of angle, position (x,y), scale (x,y). We then create the matrix as follows

var scale = { x : 1, y : 1 };
var pos = {x : 16, y : 16 };
var angle = Math.PI / 4; // 45 deg
var a,b,c,d,e,f; // the matrix arguments
// the x axis and x scale
a = Math.cos(angle) * scale.x;
b = Math.sin(angle) * scale.x;
// the y axis which is at 90 degree clockwise of the x axis
// and the y scale
c = -Math.sin(angle) * scale.y;
d = Math.cos(angle) * scale.y;
// and the origin
e = pos.x;
f = pos.y;

element.style.transform = "matrix("+[a,b,c,d,e,f].join(",")+")";

As most of the time we dont skew the transform and use a uniform scale we can shorten the code. I prefer to use a predefined array to help keep GC hits low.

const preDefinedMatrix = [1,0,0,1,0,0]; // define at start

// element is the element to set the CSS transform on.
// x,y the position of the elements local origin
// scale the scale of the element
// angle the angle in radians
function setElementTransform (element, x, y, scale, angle) {
    var m = preDefinedMatrix;
    m[3] = m[0] = Math.cos(angle) * scale;
    m[2] = -(m[1] = Math.sin(angle) * scale);
    m[4] = x;
    m[5] = y;
    element.style.transform = "matrix("+m.join(",")+")";
}

I use a slightly different function in the demo. ship.updatePos and uses the ship.pos and ship.displayAngle to set the transformation relative to the containing elements origin (top,left)

Note that the 3D matrix though a little more complex (includes the projection) is very similar to the 2D matrix, it describes the x,y, and z axis as 3 vectors each with 3 scalars (x,y,z) with the y axis at 90 deg to the x axis and the z axis at 90 deg to both the x and y axis and can be found with the cross product of the x dot y axis. The length of each axis is the scale and the origin is a point coordinate (x,y,z).


Demo:

The demo shows 4 5 variants. Use the keyboard 1,2,3,4,5 to select a ship (it will turn red) and use the arrow keys to fly. Basic up arrow you go, left right you turn.

The maths for each ship is in the object ship.controls

requestAnimationFrame(mainLoop);
const keys = {
    ArrowUp : false,
    ArrowLeft : false,
    ArrowRight : false,
    Digit1 : false,
    Digit2 : false,
    Digit3 : false,
    Digit4 : false,
    Digit5 : false,
    event(e){ 
        if(keys[e.code] !== undefined){ 
            keys[e.code] = event.type === "keydown" ;
            e.preventDefault();
        } 
    },
}
addEventListener("keyup",keys.event);
addEventListener("keydown",keys.event);
focus();
const ships = {
    items : [],
    controling : 0,
    add(ship){ this.items.push(ship) },
    update(){
        var i;
        
        for(i = 0; i < this.items.length; i++){
            if(keys["Digit" + (i+1)]){
                if(this.controling !== -1){
                    this.items[this.controling].element.style.color = "green";
                    this.items[this.controling].hasControl = false;
                }
                this.controling = i;
                this.items[i].element.style.color = "red";
                this.items[i].hasControl = true;
            }
            this.items[i].updateUserIO();
            this.items[i].updatePos();
        }
    }
    
}
const ship = {
    element : null,
    hasControl : false,
    speed : 0,
    speedC : 0,  // chase value for speed limit mode
    speedR : 0,  // real value (real as in actual speed)
    angle : 0,
    angleC : 0,  // as above
    angleR : 0,
    engSpeed : 0,
    engSpeedC : 0,
    engSpeedR : 0,
    displayAngle : 0, // the display angle
    deltaAngle : 0,
    matrix : null,  // matrix to create when instantiated 
    pos : null,     // position of ship to create when instantiated 
    delta : null,   // movement of ship to create when instantiated 
    checkInView(){
        var bounds = this.element.getBoundingClientRect();
        if(Math.max(bounds.right,bounds.left) < 0 && this.delta.x < 0){
            this.pos.x = innerWidth;
        }else if(Math.min(bounds.right,bounds.left) > innerWidth  && this.delta.x > 0){
            this.pos.x = 0;
        }
        if(Math.max(bounds.top,bounds.bottom) < 0  && this.delta.y < 0){
            this.pos.y = innerHeight;
        }else if( Math.min(bounds.top,bounds.bottom) > innerHeight  && this.delta.y > 0){
            this.pos.y = 0;
        }
        
    },
    controls : {
        oldSchool(){
            if(this.hasControl){
                if(keys.ArrowUp){
                    this.delta.x += Math.cos(this.angle) * 0.1;
                    this.delta.y += Math.sin(this.angle) * 0.1;
                }
                if(keys.ArrowLeft){
                    this.deltaAngle -= 0.001;
                }
                if(keys.ArrowRight){
                    this.deltaAngle += 0.001;
                }
            }
            this.pos.x += this.delta.x;
            this.pos.y += this.delta.y;
            this.angle += this.deltaAngle;
            this.displayAngle = this.angle;
            this.delta.x *= 0.995;
            this.delta.y *= 0.995;
            this.deltaAngle *= 0.995;            
        },
        oldSchoolDrag(){
            if(this.hasControl){
                if(keys.ArrowUp){
                    this.delta.x += Math.cos(this.angle) * 0.5;
                    this.delta.y += Math.sin(this.angle) * 0.5;
                }
                if(keys.ArrowLeft){
                    this.deltaAngle -= 0.01;
                }
                if(keys.ArrowRight){
                    this.deltaAngle += 0.01;
                }
            }
            this.pos.x += this.delta.x;
            this.pos.y += this.delta.y;
            this.angle += this.deltaAngle;
            this.delta.x *= 0.95;
            this.delta.y *= 0.95;
            this.deltaAngle *= 0.9;
            this.displayAngle = this.angle;
        },
        speedster(){
            if(this.hasControl){
                
                if(keys.ArrowUp){
                    this.speed += 0.02;
                }
                if(keys.ArrowLeft){
                    this.deltaAngle -= 0.01;
                }
                if(keys.ArrowRight){
                    this.deltaAngle += 0.01;
                }
            }
            this.speed *= 0.99;
            this.deltaAngle *= 0.9;
            this.angle += this.deltaAngle;
            this.delta.x += Math.cos(this.angle) * this.speed;
            this.delta.y += Math.sin(this.angle) * this.speed;
            this.delta.x *= 0.95;
            this.delta.y *= 0.95;
            this.pos.x += this.delta.x;
            this.pos.y += this.delta.y;
            this.displayAngle = this.angle;
        },
        engineRev(){  // this one has a 3 control. Engine speed then affects acceleration. 
            if(this.hasControl){
                if(keys.ArrowUp){
                    this.engSpeed = 3
                }else{
                    this.engSpeed *= 0.9;
                }
                if(keys.ArrowLeft){
                    this.angle -= 0.1;
                }
                if(keys.ArrowRight){
                    this.angle += 0.1;
                }
            }else{
                this.engSpeed *= 0.9;
            }
            this.engSpeedC += (this.engSpeed- this.engSpeedR) * 0.05;
            this.engSpeedC *= 0.1;
            this.engSpeedR += this.engSpeedC;
            this.speedC += (this.engSpeedR - this.speedR) * 0.1;
            this.speedC *= 0.4;
            this.speedR += this.speedC;
            this.angleC += (this.angle - this.angleR) * 0.1;
            this.angleC *= 0.4;
            this.angleR += this.angleC;
            this.delta.x += Math.cos(this.angleR) * this.speedR * 0.1; // 0.1 reducing this as easier to manage speeds when values near pixel size and not 0.00umpteen0001
            this.delta.y += Math.sin(this.angleR) * this.speedR * 0.1;
            this.delta.x *= 0.99;
            this.delta.y *= 0.99;
            this.pos.x += this.delta.x;
            this.pos.y += this.delta.y;
            this.displayAngle = this.angleR;
        },
        speedLimiter(){
            if(this.hasControl){
    
                if(keys.ArrowUp){
                    this.speed = 15;
                }else{
                    this.speed = 0;
                }
                if(keys.ArrowLeft){
                    this.angle -= 0.1;
                }
                if(keys.ArrowRight){
                    this.angle += 0.1;
                }
            }else{
                this.speed = 0;
            }
            this.speedC += (this.speed - this.speedR) * 0.1;
            this.speedC *= 0.4;
            this.speedR += this.speedC;
            this.angleC += (this.angle - this.angleR) * 0.1;
            this.angleC *= 0.4;
            this.angleR += this.angleC;
            this.delta.x = Math.cos(this.angleR) * this.speedR;
            this.delta.y = Math.sin(this.angleR) * this.speedR;
            this.pos.x += this.delta.x;
            this.pos.y += this.delta.y;
            this.displayAngle = this.angleR;
        }
    },
    updateUserIO(){
    },
    updatePos(){
        this.checkInView();
        var m = this.matrix;
        m[3] = m[0] = Math.cos(this.displayAngle);
        m[2] = -(m[1] = Math.sin(this.displayAngle));
        m[4] = this.pos.x;
        m[5] = this.pos.y;
        this.element.style.transform = `matrix(${m.join(",")})`;
    },
    create(shape,container,xOff,yourRide){  // shape is a string
        this.element = document.createElement("div")
        this.element.style.position = "absolute";
        this.element.style.top = this.element.style.left = "0px";
        this.element.style.fontSize = "24px";
        this.element.textContent = shape;
        this.element.style.color  = "green";
        this.element.style.zIndex  = 100;

        container.appendChild(this.element);
        this.matrix = [1,0,0,1,0,0];
        this.pos = { x : innerWidth / 2 + innerWidth * xOff, y : innerHeight / 2 };
        this.delta = { x : 0, y : 0};
        this.updateUserIO = this.controls[yourRide];
        return this;
    }
}
var contain = document.createElement("div");
contain.style.position = "absolute";
contain.style.top = contain.style.left = "0px";
contain.style.width = contain.style.height = "100%";
contain.style.overflow = "hidden";
document.body.appendChild(contain);
window.focus();




ships.add(Object.assign({},ship).create("=Scl>",contain,-0.4,"oldSchool"));
ships.add(Object.assign({},ship).create("=Drg>",contain,-0.25,"oldSchoolDrag"));
ships.add(Object.assign({},ship).create("=Fast>",contain,-0.1,"speedster"));
ships.add(Object.assign({},ship).create("=Nimble>",contain,0.05,"speedLimiter"));
ships.add(Object.assign({},ship).create("=Rev>",contain,0.2,"engineRev"));
function mainLoop(){
    ships.update();
    requestAnimationFrame(mainLoop);
}
body {
  font-family : verdana;
  background : black;
  color : #0F0;
 }
   Click to focus then keys 1, 2, 3, 4, 5 selects a ship. Arrow keys to fly. Best full page.

A zillion variants

There are many other variants and ways. I like the using a second derivative (first derivative dx/dt (dt is time) from x += dx, second de/dt for engine power) that simulates an engine ramping up power and winding down which can give a very nice feel. Basicly its

 x += dx;
 dx += de;
 dx *= 0.999;
 de *= 0.99;
 if(input){ de += 0.01 }

What is suited for your game is up to you, you don't have to follow the rules so try out different values and methods till you are happy.

Colourable answered 2/5, 2017 at 17:36 Comment(1)
I love those ships, grate answer.Incidental
S
1

Very basic idea how to implement acceleration: Create speed variable and multiply them as you want.

Example of acceleration only for "UP" key

var speed = 1.0;
window.onkeyup = function( e ) {
  var kc = e.keyCode;
  e.preventDefault();

  if ( kc === 37 ) Keys.left = false;
  else if ( kc === 38 ) Keys.up = false;
  else if ( kc === 39 ) Keys.right = false;
  else if ( kc === 40 ) Keys.down = false;
};

function update() {
  if ( Keys.up ) {
    speed = speed *1.01;
    document.querySelector( 'div' ).style.transform += 'translateY( -'+Math.round(speed )+'px )';
  }
  else if ( Keys.down ) {
   if (speed>1)
   {   speed = speed *0.9;
       document.querySelector( 'div' ).style.transform += 'translateY( -'+Math.round(speed )+'px )';
   }
else {
    speed = 1;
    document.querySelector( 'div' ).style.transform += 'translateY( 1px )';
}
  }

  if ( Keys.left ) {
    document.querySelector( 'div' ).style.transform += 'rotate( -1deg )';
  }
  else if ( Keys.right ) { 
    document.querySelector( 'div' ).style.transform += 'rotate( 1deg )';
  }
  
    requestAnimationFrame( update );
}
requestAnimationFrame( update );
@import url( "https://fonts.googleapis.com/css?family=Nunito" );

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: "Nunito", sans-serif;
  font-size: 2rem;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
}

b {
  display: block;
  transform: rotate( 180deg );
}
<div>
  <b>v</b>
</div>

<script>
  var Keys = {
    up: false,
    down: false,
    left: false,
    right: false
  }

  window.onkeydown = function( e ) {
    var kc = e.keyCode;
    e.preventDefault();

    if ( kc === 37 ) Keys.left = true;
    else if ( kc === 38 ) Keys.up = true;
    else if ( kc === 39 ) Keys.right = true;
    else if ( kc === 40 ) Keys.down = true;
  };
</script>
Silvanus answered 2/5, 2017 at 5:15 Comment(6)
This is awesome!! Now any ideas on inertia or is that a separate question? lolCertain
You mean, when you will press 'down' button, it shouldn't stop immediately. Right?Silvanus
Yeah. Like it keeps travelling at whatever velocity you let go of the arrow key at. At the same speed and same direction. Is that a simple or an intense undertaking?Certain
Hmm. All that seems to change is when I press the up arrow it goes back slightly first. Maybe revert this back to original answer. That was the main question anyway. I'll accept now.Certain
If you want better physics, I want to suggest you use some game frameworks. For example phaser.ioSilvanus
Yeah, it's not perfect, there many ways how to make physics more 'real', but it will be big job without game frameworksSilvanus
R
1
 window.onkeydown = function( e ) {
    var kc = e.keyCode;
    e.preventDefault();

    if ( kc === 37 ) Keys.left++;
    else if ( kc === 38 ) Keys.up++;
    else if ( kc === 39 ) Keys.right++;
    else if ( kc === 40 ) Keys.down++;
  };
window.onkeyup = function(e)
{
  var kc = e.keyCode;
    e.preventDefault();

    if ( kc === 37 ) {Keys.left = 0;}
    else if ( kc === 38 ) Keys.up = 0;
    else if ( kc === 39 ) Keys.right = 0;
    else if ( kc === 40 ) Keys.down = 0;
}
function update() {
  if ( Keys.up ) {
    document.querySelector( 'div' ).style.transform += 'translateY( -'+Keys.up+'px )';
  }
  else if ( Keys.down ) {
    document.querySelector( 'div' ).style.transform += 'translateY( '+Keys.down+'px )';
  }

  if ( Keys.left ) {
    document.querySelector( 'div' ).style.transform += 'rotate( -'+Keys.left+'deg )';
  }
  else if ( Keys.right ) { 
    document.querySelector( 'div' ).style.transform += 'rotate( '+Keys.right+'deg )';
  }

    requestAnimationFrame( update );
}
requestAnimationFrame( update );
Roxana answered 2/5, 2017 at 5:23 Comment(1)
Interesting, I like this version too. We're trying to figure out momentum as well, math wasn't my strongest subject in school.Certain

© 2022 - 2024 — McMap. All rights reserved.