It looks like you're new here. If you want to get involved, click one of these buttons!
Looking at top-down 2D car physics code and tutorials, a lot of them cite Marco Monster's article (archived here, a port of the code here). Here's a quick Codea port I made, just the car/wheel physics (not the engine, gears etc), using modelMatrix
to handle the rotations. The basic principle is that you translate into the local space of the car, then you can separate the lateral and longitudinal forces acting on the body and the wheels.
I don't understand how all the variables in the simulation interact, but you can make the car drift quite easily.
You could also implement this in Box2D, by cancelling out most of the lateral forces on the wheels, eg top example here.
Tilt to steer, touch right side to accelerate, left side to brake/ reverse.
Friction doesn't seem to bring car to a complete halt, there seems to be lingering velocity.
Steering doesn't work when vehicle is going backwards
--# Main
-- 2D Car Physics
supportedOrientations(LANDSCAPE_ANY)
displayMode(OVERLAY)
function setup()
centre=vec2(WIDTH,HEIGHT)/2
parameter.watch("you.pos")
parameter.watch("you.angleVel")
parameter.watch("you.vel")
parameter.watch("you.steerAngle")
you = Vehicle{pos=vec2(WIDTH,HEIGHT)/2, size=vec2(50,30), mass=700, inertia=700, steering = Vehicle.tiltSteering, gas = Vehicle.touchGas}
end
function draw()
background(40, 40, 50)
you:update()
end
function touched(t)
if t.state == ENDED then
touching = false
else
if t.x<centre.x then
touching = 1
else
touching = 2
end
end
end
--# Body
Body = class()
function Body:init(t)
self.size=t.size
local w, h = t.size.x/2, t.size.y/2
self.radius = vec2(w,h)
self.points={vec2(-w,-h), vec2(-w,h), vec2(w,h), vec2(w,-h)}
-- linear properties
self.pos = t.pos
self.vel = t.vel or vec2(0,0)
self.forces = vec2(0,0) --accumulated forces. Zeroed every frame
self.mass = t.mass or 1 --kg
--angular properties
self.angle = t.angle or 0
self.angleVel = 0 --in rad/s
self.torque = 0 --accumulated torque. zeroed every frame
self.inertia = t.inertia or 150 --kg.m
--total transform
self.matrix=matrix()
end
function Body:constrain()
self.vel = outOfBounds(self.vel, self.pos-self.radius, self.pos+self.radius)
end
function Body:integrate()
local dt = DeltaTime
--integrate physics
--linear
local accel = self.forces /self.mass
self.vel = self.vel + dt * accel
--angular
local angAcc = self.torque /self.inertia
self.angleVel = self.angleVel + dt * angAcc
--constrain
-- self:constrain()
--update
self.pos = self.pos + dt *self.vel
self.angle = self.angle + dt * self.angleVel
--reset accumulators
self.forces = vec2(0,0)
self.torque = 0
end
function Body:getWorldPoint(v)
return self.matrix * v
end
function Body:getLocalPoint(v)
local inv = self.matrix:inverse()
return inv * v
end
function Body:applyForce(force, worldPoint)
self.forces = self.forces + force
--[[
if worldPoint then
self:applyTorque( math.rad( worldPoint-self.pos:cross(force)) ) --world offset (torque arm) crossed with world force. Why does this have to be converted to radians though?
end
]]
end
function Body:applyTorque(ang)
self.torque = self.torque + ang
end
--# Vehicle
Vehicle = class(Body) --based on Marco Monster's model
--X Forwards.
--added weight distribution formula
--issues: steering doesnt work in reverse
--friction doesnt quite bring car to complete stop
local drag = 20 --4.257 --air resistance, proportional to vel squared
local resistance = drag * 30 --rolling resistance, proportional to vel
local cornerStiffRear = -5.2
local cornerStiffFront = -5
local maxGrip= 4 --increase this for a tighter turning circle, less sliding?
local engineAccel = 180
local engineBreak = engineAccel * 10
local sensitivity = 1 --steering sensitivity
function Vehicle:init(t)
Body.init(self, t)
self.wheelRadius = 10
self.length = t.size.x - (self.wheelRadius*2) --wheelbase
self.halfLength = self.length/2
--weight distribution variables
local height=2 --vertical distance from centre of wheels to centre of geometry (keep low, otherwise vehicle really slides on breaking)
self.heightRatio = 2/ self.length -- height as a proportion if wheelbase, used to calculate shifting weight distribution
self.weightDiff = 0
self.accelRate = 0 -- this is stored to calculate distribution of weight between axles
--inputs
self.steering = t.steering
self.steerTarget=0
self.gas = t.gas
self.revs = 0
self.maxRevs = 64000 --completely made-up number
end
function Vehicle:update()
-- inputs
self:gas()
self:steering()
--physics
self:doPhysics()
self:constrain()
self:integrate()
self:draw()
end
function Vehicle:doPhysics()
--transform world velocity to local 2D space
local vel = vecRotMat(self.vel, self.matrix:inverse()) --
-- local vel = vec2(cos * self.vel.y + sin * self.vel.x, -sin * self.vel.y + cos * self.vel.x)
-- local vel=returnVec2(self:getLocalPoint(self.vel))
--lateral force on wheels. r * vel in radians/second
local yawSpeed = self.halfLength * self.angleVel
rotAngle = math.atan(yawSpeed,vel.x)
--sideslip angle of car (beta). ie angle between orientation and velocity
sideSlip = math.atan(vel.y, vel.x)
--calculate slip angles for front and rear wheels (alpha)
local slipAngleFront = sideSlip + rotAngle - self.steerAngle
local slipAngleRear = sideSlip - rotAngle
--weight per axle = half car mass times 1gee (9.8 m/s^2)
local weight = self.mass * 4.9
weightDiff = self.heightRatio * self.mass * self.accelRate --weight distribution between axles (stored to animate body)
local weightFront = weight - weightDiff
local weightRear = weight + weightDiff
--vary this according to acceleration
--lateral force on front wheels (corner stiffness * slip angle, clamped to friction circle * load), front slips
local latForceFront = vec2(0,clamp(cornerStiffFront * slipAngleFront, -maxGrip, maxGrip)*weightFront)
--lateral force on rear wheels
local latForceRear = vec2(0, clamp(cornerStiffRear * slipAngleRear, -maxGrip, maxGrip)* weightRear)
--longitudinal force on rear wheels = traction. 100 * (self.revs - self.brake * sign(vel.x))
local traction = vec2(100 * self.revs, 0 )
--drag and rolling resistance
local friction = -vel * (resistance + drag * vel:len())
--sum forces
local cornering = (latForceFront):rotate(self.steerAngle) + latForceRear
local force = traction + cornering + friction
--APPLY FORCE AND TORQUE
--sum torque from lat forces
self:applyTorque(latForceFront.y - latForceRear.y)
--convert force back to 3D world space
acceleration = vecRotMat(force, self.matrix) -- self:getWorldPoint(accel)
-- acceleration = vecAddZ(vec2(cos * accel.y + sin * accel.x, -sin * accel.y + cos * accel.x), 0)
self:applyForce(acceleration)
self.accelRate = DeltaTime * force.x / self.mass --store acceleration rate to calculate weight distribution next frame
end
function Vehicle:draw()
pushMatrix()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.angle))
self.matrix = modelMatrix()
sprite("Platformer Art:Block Grass", 0,0,self.size.x, self.size.y)
self:drawWheel(1, self.wheelRadius)
self:drawWheel(2, self.wheelRadius)
self:drawWheel(3, -self.wheelRadius, self.steerAngle)
self:drawWheel(4, -self.wheelRadius, self.steerAngle)
popMatrix()
end
function Vehicle:drawWheel(n, offset, turn)
local turn = turn or 0
pushMatrix()
translate(self.points[n].x+offset, self.points[n].y)
rotate(math.deg(turn))
sprite("Platformer Art:Block Special", 0,0,20,5)
popMatrix()
end
function Vehicle:tiltSteering()
self.steerAngle = clamp(-Gravity.x * sensitivity, -0.40, 0.40)
end
function Vehicle:touchGas()
if touching==2 then
self.revs = math.min(self.revs + engineAccel, self.maxRevs)
elseif touching==1 then
self.revs= math.max(self.revs - engineAccel, -self.maxRevs * 0.1)
else
self.revs= math.max(self.revs - engineBreak, 0)
end
end
--# Helper
function outOfBounds(vel, a, b, bb, aa) --if box ab on heading vel is outside of boundary aabb, returns a corrective vel, plus how much out of bounds the box is
local b=b or a
local aa=aa or vec2(0,0)
local bb=bb or vec2(WIDTH,HEIGHT)
local x,y=0,0
if a.x<aa.x and vel.x<0 then x=a.x-aa.x vel.x=-vel.x
elseif b.x>bb.x and vel.x>0 then x=b.x-bb.x vel.x=-vel.x
end
if a.y<aa.y and vel.y<0 then y=a.y-aa.y vel.y=-vel.y
elseif b.y>bb.y and vel.y>0 then y=b.y-bb.y vel.y=-vel.y
end
return vel, x, y
end
function AABB(v,aa,bb)
if v.x>aa.x and v.x<bb.x and v.y>aa.y and v.y<bb.y then return true end
end
function sign(x) --returns -1 if x<0, else 1
return (x<0 and -1) or 1
end
function clamp(v,low,high)
return math.min(math.max(v, low), high)
end
function vecRotMat(v, m) --apply rotation part (top 2x2) of a mat4 to a vec2
return vec2(
m[1]*v.x + m[5]*v.y,
m[2]*v.x + m[6]*v.y)
end
Comments
Here's a partial fix for the weird friction problem. Turning the wheel creates tiny amounts of lateral force, so that the car still twitches a little when it is at rest. This uses an
edge
algorithm to kill the lateral force when the velocity drops down too low. It's a bit of a hack though...Change these lines in
Vehicle:doPhysics()
, and then add theedge
function somewhere:I have no idea why the steering doesn't work in reverse though. Perhaps someone who understands the trig better than me can work out what's going on there?
These issues aside, I quite like the feel of this, it reminds me of top-down racers like the Reckless Racing games. I'm sure with some tweaking of the variables you could get various drift behaviours like "donuts" etc
@Yojimbo2000 - I suspect the code writer didn't attempt reverse because that is more difficult
It seems to me the whole thing could be done more simply without all this effort, because there is no need for such complexity on a flat even surface. Even if you drive on bumpy terrain, I'm sure it's easier to just fake it.
This seems to be your default response, "it's too complex, why bother!" :P
It depends on what your aims are I suppose. I initially started with the assumption "the car can only move in the direction the wheels are pointing". But then the vehicle handles as if it is on rails. That assumption is probably true at slow speeds, but it's unrealistic, and just not very satisfying at high speeds.
Then I tried simulating the car as an unrestrained physics body, with its own mass and inertia, being propelled in the direction the wheels are pointing. But then it handles like a shopping trolley (because the wheels move equally well in all directions).
I didn't find either of the above approaches particularly satisfying.
In order to get a somewhat satisfying feeling that we are steering a car at speed we need a combination of the above two approaches. The wheels want to move longitudinally (ie they roll in the direction they face), but particularly at high speeds they are also subject to lateral forces (slip). So we need some kind of system that converts a certain amount of the lateral force into longitudinal force.
This is the case regardless of whether we are working in 2D or 3D.
I think Marco's code probably can be simplified, but the fundamental principle — of translating into the local space of the car, so that longitudinal and lateral forces can be mapped onto the X and Y axis — is a good one. It also lends itself well to 3D (or 2.5D) simulations. The video below adapts the code above to a 3D simulation. All of the calculations of slip and drift occur in a local 2D plane, but that plane undulates as the vehicle moves over the bumpy terrain (I've read that this is how a lot of the more arcadey 3D racers work).
I've also animated the body so that you can see the weight shifting forward when the vehicle brakes, which helps the vehicle drift.
Don't get me wrong, I admire all the technical wizardry B-), and I think it's great to try it out.
I'm simply repeating what I've read about in the history of video gaming, that faking stuff realistically is much easier (and faster) than trying to copy the real physics, which is horrendously complex.
Watching the video, I think simple terrain following code would look effectively identical, except for the drift, which can be faked, and I'm sure the steering round corners at speed can be mimicked too. Perhaps the physics would show to greater advantage if the vehicle got more air.
(Nice terrain, by the way!).
I would argue that Marco's code is "faking it" though. For one thing, it's only modelling two wheels (front and back), and using them to represent the four wheels of the car. Things like the
maxGrip
variable is a fairly arbitrary number that is representing far more complex things (I think it's the friction circle * load, whatever that means)And mapping the 2D torque and sliding onto whatever 3D plane the vehicle is in is definitely faking it.
Here's another tutorial on doing the same thing in Box2D, by cancelling out some of the lateral forces: http://www.iforce2d.net/b2dtut/top-down-car
Here is a simple example of using forces to steer the car round corners.
The car will try to follow your finger on the screen, and the black line shows the wheel direction (I haven't put realistic limits on how far wheels can turn!). The cars momentum keeps it travelling in its original direction initially, so the car would appear to slide if I bothered drawing wheels.
That's cool! It reminds me a little of the path-following vehicle in Nature Of Code. It's really interesting I think that you've done this without any explicit angles (just vectors), and almost no trig (just normalize). But the car can only point in the direction it's travelling, have I got that right (so no drifting or lateral movement)?
Could I ask you a favour, would you be able to add some comments to your code above? e.g. the formula for calculating
carDir
andmomentum
are a little hard to follow (eg I don't understand why you dividecarDir
bymomentum+speed
if you're then normalizing it?). It would be really helpful for me (and I'm sure others) if you could unpack those parts a little.Lol, the Nature of Code is pretty much what I'm channeling.
It should be possible to drift the car by delaying the change in direction until after the car is drawn, ie draw the car, then adjust direction and move the car.
I added some comments in the code.
Awesome, thanks for commenting the code. It's an interesting approach. So rather than damping the lateral motion (like in the box2d tutorial I linked to previously), it adds extra longitudinal motion depending on the dot product of the wheel and car direction.
You'd need to separate car direction (ie which way it is facing) from car velocity (which direction it is moving in) to get sideways drift though.
Agreed, so it does seem to be possible to get similar effects with this approach.
Vector-based steering seems to work very nicely, for tabletop racing.
Here's a version of your code that adds velocity and friction, so the car can have lateral motion (drift). With some tweaking, this could actually be pretty good. The momentum calculation is really interesting, it's absolutely key. The vehicle section of Nature of Code uses dot products for its steering, but it didn't use them quite like this, AFAIR. Did you come up with that yourself?
I could say it was my idea, but it's more likely I got it from somewhere :P
Here's a way that might interest you. Just move your finger in a figure 8 or any other way you want. Also, after you turn a corner, lift your finger off the screen. I didn't put a whole lot into this because I don't know if this is usable or not. If you loose where the car is, just restart the program. You can try changing the friction, or the 1.05 in draw.
@dave1707 I think it should be
linearDamping
in these kinds of top-down sims, notfriction
, as damping is in effect at all times (ie simulating an XY ground plane that box2D is otherwise unaware of), whereas friction only applies when two box2D bodies come into contact. Personally, I wouldn't use a joint for a steering action like this, it's too hard to control. I'd apply a force in the direction of the touch. EDIT: or rather, the object doesn't feel very car-like with this kind of offset control, you don't get a sense of steering towards something, more like it is directly translating your touch, but with a rubber-banding effect.@yojimbo2000 I'm not sure why I didn't try the linearDamping. Maybe the linearVelocity calculation was working how I wanted and I didn't look for anything else. This was reminding me of a front wheel drive pickup truck driving around in a snow covered parking lot.