1
votes

I have a 2.5D space shooter game in progress that uses Unity's built-in physics. Everything happens in 2D space but all the models are 3D.

The player (a space ship) can rotate using a controller axis and can accelerate when a button is held down (e.g. xbox controller's A button).

There is a limit on how fast the player can move (maxEngineSpeed) and I clamp the magnitude of the RigidBody's velocity in FixedUpdate as follows:

if (rb.velocity.magnitude > maxEngineSpeed)
{
    rb.velocity = Vector2.ClampMagnitude(rb.velocity, maxEngineSpeed);
}

Now the problem is that this prevents the veclocity from ever reaching a value higher than maxEngineSpeed .

I want a behaviour that only limits the velocity when the player is accelerating. If the player somehow gains more speed from a coillision or from a bullet hit, then the velocity should not be limited. We can think it like the space ship not having enough power in its engines to go any faster. It's like linear drag but only when accelerating (when not accelerating, the ship doesn't decelerate at all). I have power-ups that grant the player more maximum speed, so it's important.

How would this be implemented? I've tried to only limit the velocity when the player is accelerating, but then it clamps it immediately to specified value and it looks unnatural. Would a couroutine work that would slowly reduce the magnitude when accelerating? But then it would have to take account the direction of the player and current velocity?

EDIT: Clarification: in practise what I would like to have is to ask a RigidBody "if I apply this force to you while you're moving faster than maxEngineSpeed, would it increase your speed? If it would, don't apply the force, if it would decrease your speed, then apply it".

EDIT: changed the maxSpeed variable name to maxEngineSpeed for more clarity.

3

3 Answers

1
votes

Remove the clamping in FixedUpdate. Instead, add a check where you add Velocity (where you detect Xbox Controllers 'A' is pressed).

Something like:

if(Input.GetButton("Xbox-A"))
{
    if(rb.velocity.magnitude < scaledMaxSpeed)
    {
        rb.addForce(...);
    }
}

So if you are faster than your max-speed, the ship cannot accelerate more (by own power).

1
votes

Pull down & Drag

There are so many ways to achieve what you want. Below I show two possible methods with a working demo to allow you to get a bit of a feel for how the perform and differ. Also link at bottom to another demo.

Pull down

You can pull down the velocity by defining a max over speed and a over speed drag coefficient

The pull down method

Define settings

 float pullDown = 0.1f;  // dimensionless > 0, < 1 
 float maxOverSpeed = 5.0f;
 float maxSpeed = 4.0f
 float acceleration = 0.1f;

Per frame

 if (accelerate && speed < maxSpeed) { speed += acceleration }

 // clamp max over speed
 speed = speed > maxOverSpeed ? maxOverSpeed : speed;

 float speedAdjust = speed - maxSpeed;

 // pull speed down if needed
 speed -= speedAdjust > 0.0f ? speedAdjust * pullDown : 0.0f;

 // set the velocity magnitude to the new  speed

Personally I don't like this method as it is a coasting model, ship gets to speed an holds it, there is no deceleration, but it does give finer control over velocity.

Drag

My preferred method is to use a simple drag coefficient. Slight modification to add extra draw when over speed

However this makes is difficult to know what the max speed will be given some acceleration. There is a formula that will give you a drag coefficient to match a max speed for acceleration, or acceleration to match a max speed for a drag coefficient, but off the top of my head I can not remember it as its been years since I found I needed to use it.

I wing it and define an approximation, test it, and refine till I get what feels right. In reality if ask what is the max speed of the player? All i know is not too fast and not too slow. :P

The drag method

Define

  float acceleration = 0.1f;
  float drag = 1.0f - 0.021f;
  float overSpeedDrag = 1.0f - 0.026f;
  float maxSpeed = 4;

Per frame

  // apply drag depending on speed
  speed *= speed > maxSpeed ? overSpeedDrag : drag;
  if (accelerate) { speed += acceleration }
            
  // set the velocity magnitude to the new current speed

 

Example

These methods as code do not give much of a feel for the actual results so the following snippet implements both methods so you can see and feel how they work.

The code is at the top (in JavaScript) the two different methods are flagged PULL_DOWN, DRAG in the function update() {

  • Ship speeds are in pixels per second (Pps)
  • Both ships have same acceleration constant, however ship B (drag method) does not accelerate at a constant rate.
  • Ship A will coast, ship B will always come to a stop.
  • Click bump to kick the ship's velocity.

const accelFunction = {
    get vel() { return new Vec2(0, 0) },
    speed: 0,
    acceleration: 0.1,
    maxSpeed: 4,
    
    // drag constants
    drag: 1 - 0.0241,
    overSpeedDrag: 1 - 0.0291,

    // pulldown constants;
    overSpeed: 5,
    pullDown: 0.1,

    update() {
        if (this.method === DRAG) { // Drag method
            this.speed *= this.speed > this.maxSpeed ? this.overSpeedDrag: this.drag;
            if (this.accelerate) { this.speed += this.acceleration }


        } else {  // Pull down method
            if (this.accelerate && this.speed < this.maxSpeed) { this.speed += this.acceleration }
            this.speed = this.speed > this.maxOverSpeed ? this.maxOverSpeed : this.speed;
            var speedAdjust = this.speed - this.maxSpeed;
            this.speed -= speedAdjust > 0 ? speedAdjust * this.pullDown : 0;
        }

        // move ship
        this.vel.length = this.speed;
        this.pos.add(this.vel);
    },
}







/* rest of code unrelated to anwser */




















requestAnimationFrame(start);
const ctx = canvas.getContext("2d");
const PULL_DOWN = 0;
const DRAG = 1;
var shipA, shipB;
var bgPos;
function Ship(method, control, controlBump) {  // creates a Player ship

    control.addEventListener("mousedown",() => API.go());
    control.addEventListener("mouseup",() => API.coast());
    control.addEventListener("mouseout",() => API.coast());
    controlBump.addEventListener("click",() => API.bump());
    const API = {    
        ...accelFunction,
        pos: new Vec2(100, 50 + method * 50),
        method, // 0 drag method, 1 pulldown
        draw() {
            ctx.setTransform(1,0,0,1,this.pos.x - bgPos.x, this.pos.y)
            ctx.strokeStyle = "#FFF";
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.lineTo(20, 0);
            ctx.lineTo(-20, -20);
            ctx.lineTo(-20,  20);
            ctx.closePath();
            ctx.stroke();
            ctx.fillText(this.method ? "B" : "A", -11, 3);     
            ctx.fillText((this.speed * 60 | 0) + "Pps", 80, 3);
            if (this.accelerate) {
                ctx.strokeStyle = "#FF0";
                ctx.beginPath();
                ctx.lineTo(-20, -10);
                ctx.lineTo(-30 - Math.rand(0,10), 0);
                ctx.lineTo(-20,  10);
                ctx.stroke();
            }
                
        
        },
        focus: false,
        reset() {
            this.focus = false;
            this.vel.zero();
            this.pos.init(100, 50 + this.method * 50);
            this.speed = 0;
            this.accelerate = false;
        },
        go() {
            this.accelerate = true;
            this.focus  = true;
            if (this.method === 1) { shipA.reset() }
            else { shipB.reset() }
        },
        coast() {
            this.accelerate = false;
        },    
        bump() {
            this.speed += 1;
        },

    };
    return API;
}
function start() {
   init();
   requestAnimationFrame(mainLoop);
}

function mainLoop() {
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,500,170);
 
    shipA.update();
    shipB.update();
    
    bgPos.x = shipA.focus ? shipA.pos.x - 50 :  shipB.pos.x - 50 ;
    drawBG(bgPos);
    
    shipA.draw();
    shipB.draw();
    requestAnimationFrame(mainLoop);

}

function drawBG(bgPos) {
    ctx.fillStyle = "#FFF";
    ctx.beginPath();
    const bgX = -bgPos.x + 100000;
    for (const p of background) {
        x = (p.x + bgX) % 504 - 2;
        ctx.rect(x, p.y, 2, 2);
    } 
    ctx.fill();
}


const BG_COUNT = 200;
const background = [];
function init() {
    ctx.font = "16px arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    bgPos = new Vec2();
    shipA = Ship(PULL_DOWN, goA, bumpA);
    shipB = Ship(DRAG, goB, bumpB);
    var i = BG_COUNT;
    while (i--) {
        background.push(new Vec2(Math.rand(0, 10000), Math.rand(-1, 170)));    
    }
}






/* Math LIB Vec2 and math extensions */
Math.rand = (m, M) => Math.random() * (M - m) + m;
function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) { this.x = x; this.y = y }
Vec2.prototype = {
    init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, 
    zero() { this.x = this.y = 0; return this },
    add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res },
    scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res },
    get length() { return this.lengthSqr ** 0.5 },
    set length(l) { 
        const len = this.lengthSqr;
        len > 0 ? this.scale(l / len ** 0.5) : (this.x = l);
    },
    get lengthSqr() { return this.x * this.x + this.y * this.y },
};
canvas {background: #347;}
div {
   position: absolute;
   top: 150px;
   left: 20px;
}
span { color: white; font-family: arial }
<canvas id="canvas" width="500" height="170"></canvas>
<div>
<button id="goA">Go A</button>
<button id="bumpA">Bump A</button>
<button id="goB">Go B</button>
<button id="bumpB">Bump B</button>
<span> Methods: A = Pull down B = Drag </span>
</div>

No limit

There are many variations on these methods, and the are many example on SO (I have written many answers in the subject. eg See demo snippet (bottom of answer) for example of drag method modification) .

Which method you use is very dependent on how you want the interaction to feel, there is no right or wrong method as game physics will is very different than real physics.

0
votes

Knowing that acceleration (a) is the change in velocity (Δv) over the change in time (Δt), I'll check that.

Maybe with something like (pseudo):

float lastVelocity = 0;
bool isAccelerating = false;
Update()
{
    float currentVelocity = rb.velocity;
    if(currentVelocity > lastVelocity)
    {
        isAccelerating = true;
        lastVelocity = currentVelocity;
    }
    else
    {
        isAccelerating = false;
    }
}

Now you know when your "ship" is speedingUp, the only way to decrease the speed is caused by external forces (like gravity, or friction), depending of your setup, I'll deactivate those forces, or change the physicalMaterial that is causing the friction.