Reputation: 1530
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.
Upvotes: 1
Views: 1927
Reputation: 54026
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.
You can pull down the velocity by defining a max over speed and a over speed drag coefficient
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.
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
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
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() {
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>
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.
Upvotes: 1
Reputation: 4283
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.
Upvotes: -1
Reputation: 4073
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).
Upvotes: 1