Thrust / applyForce

A question for any of you game-designers out there. If I have a little free-floating, rotating ship (a la Asteroids), what is the best way to give it thrust? (The ship is a physics.body polygon, by the way.) Is the best way to use applyForce? If so, could anyone explain how? I’ve been messing with that approach, but can’t seem to get that back-of-the-ship thrust effect. Alternatively, I have seen one Asteroids-ish example that first computed the direction of the ship’s nose, then moved the ship “manually,” so to speak, by changing the x and y positions. Seems complicated, but is that my best hope? Any advice, suggestions or opinions would be most welcome. Thanks, all!

applyForce is fine for something like this. Here’s an example of how you could implement it.

-- compute thrust direction
if thrustOn then
  dir = vec2(math.cos(shipBody.angle), math.sin(shipBody.angle))
  thrustPower = 100 -- tweak to get the right feel in the game
  shipBody:applyForce(dir * thrustPower) -- apply force from center of mass
end

maxSpeed = 200 -- maximum linear velocity of ship
speed = shipBody.linearVelocity:len()
if velocity > maxSpeed then
    shipBody.linearVelocity = shipBody.linearVelocity:normalize() * maxSpeed
end

Does a force have to be applied each frame, or does it persist between frames until set to zero?

If applied each frame, is impulse calculated from the framerate?

The force needs to be applied each frame. f = ma, so if you apply the force 10 * shipBody.mass, you will get an acceleration of 10 meters per second per second. The reason that the units are in meters is because forces are in newtons. You can use physics.pixelToMeterRatio to calculate the exact amount of force to get the acceleration you want in pixels

acc = 32 -- acceleration in pixels per second per second
body:applyForce(acc / physics.pixelToMeterRatio * body.mass) 

@John, thanks for this! I gave it a try, but the movement I get is odd. My ship initially moves in a straight line to the right, but then, as the ship bounces around (I have it surrounded by walls), I can’t quite tell what the thrust is doing. Something I’ve done wrong here? Maybe a problem somewhere else in my code?

If you post the code I can have a look at it for you

@John, I was afraid you’d say that! Alright, here’s my code – I’m just Frankensteining bits and pieces together to learn how things work, so it’s pretty gruesome-looking. But your offer to give it a read is very generous. Many thanks. Here goes:

-- Main

-- Use this function to perform your initial setup
function setup()

    iparameter("Bricks", 1, 5)
    theBricks ={}
    brickIndex=0
    
    -- Define cage
    cage = physics.body(CHAIN, 4, vec2(0,0), vec2(WIDTH, 0),
                    vec2(WIDTH, HEIGHT), vec2(0, HEIGHT))
    cage.type = STATIC
    
    -- Define hero
    hero = Ship()
    hero:setUp(WIDTH/2,HEIGHT/2, 0.5) -- x pos, y pos, restitution
    
    physics.gravity(0,0)
    
    -- Define thrust touch area
    -- thrust = vec2(WIDTH -100,HEIGHT/2)

end


function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    
checkParameter()  
    
local t
for t = 1, Bricks do
    theBricks[t]:Update()
end
    
    -- Draw cage
    stroke(0, 255, 0, 255)
    noFill()
    makeCage()

    -- Check for thrust
    hero:touched()
    
    -- Draw hero
    hero:Update()
    
end -- end draw()
    

function makeCage()
    
    local points = cage.points
    local a, b
    for j = 1,#points do
        a = points[j]
        b = points[(j % #points)+1]
        line(a.x, a.y, b.x, b.y)
    end
end

function makeBrick()
    theBricks[Bricks]= Brick()
    theBricks[Bricks]:setUp(300+(Bricks*30),200+(Bricks*30),120,30,40,1.1)
end

    -- Increase or decrease bricks in table
function checkParameter()
    if Bricks > brickIndex then
        table.insert(theBricks, Bricks)
        makeBrick()
    elseif Bricks < brickIndex then
        theBricks[brickIndex] = nil
    end
        brickIndex = Bricks    
end

-- Brick class

Brick = class()

function Brick:setUp(x,y,w,h,r,bounce)
    
    -- Set self.variables for accepted position, dimensions and qualities
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    self.r = r
    
    self.body = physics.body(POLYGON,
                vec2(0,0), vec2(w,0), vec2(w,h), vec2(0,h))
    
    self.body.x = x
    self.body.y = y
    
    self.body.angle = r
    
    self.body.restitution = bounce

end

function Brick:Update()

    self.x = self.body.x -- Now the position of the rectangle
    self.y = self.body.y -- equals the position of the physics.body (I think).
    
    self:draw()
    
end


function Brick:draw()
    -- Codea does not automatically call this method
    -- Draw the brick
    
    pushMatrix()
    stroke(255, 0, 0, 255)
    fill(255, 0, 0, 255)

    translate(self.body.x, self.body.y)
    rotate(self.body.angle)

    rect(0,0,self.w,self.h)
    popMatrix()
    
end

-- Ship class

Ship = class()

function Ship:setUp(x,y,b)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.b = b
       
    self.body = physics.body(POLYGON, vec2(0,0), vec2(30,0), vec2(15,40))
    
    self.body.x = x
    self.body.y = y

    self.body.restitution = b
    
end

function Ship:Update()

    self.x = self.body.x -- Now the position of the visible ship
    self.y = self.body.y -- equals the position of the physics.body (I think)
    
    self:draw()
    
end

function Ship:draw()
    -- Codea does not automatically call this method
    stroke(255, 255, 255, 255)
    lineCapMode(ROUND)
    
    translate(self.body.x, self.body.y)
    rotate(self.body.angle)
    
    local points = self.body.points
    local a, b
    for j = 1,#points do
        a = points[j]
        b = points[(j % #points)+1]
        line(a.x, a.y, b.x, b.y)
    end
end

function Ship:touched(touch)
    -- Codea does not automatically call this method
    if CurrentTouch.state == BEGAN or 
        CurrentTouch.state == MOVING then
        if CurrentTouch.x > (WIDTH-100) then
            thrustOn = 1
            self:thrust()
        else thrustOn = nil
        end
    end
end


--@John's applyForce code, adapted by me
-- compute thrust direction
function Ship:thrust()

if thrustOn then
  dir = vec2(math.cos(self.body.angle), math.sin(self.body.angle))
  thrustPower = 100 -- tweak to get the right feel in the game
    self.body:applyForce(dir * thrustPower) -- apply force from center of mass
end

maxSpeed = 200 -- maximum linear velocity of ship
speed = self.body.linearVelocity:len()
-- Maybe he means speed, not "velocity," below?
if speed > maxSpeed then
    self.body.linearVelocity = self.body.linearVelocity:normalize() * maxSpeed
end

end

Jeez, I didn’t even do that right…

The best way to get code to render is to “fence it in”. Put a line with ~~~ before the code and one again afterwards (I’ve put them in in your post now).

Man, I just hunted that down on the FAQ! Thank you for putting the tildes in, and apologies for the rookie move…

Ok basically the issue is that body.rotation is in degrees where as sin and cos take radians, which causes the thrust direction to change rapidly. I made some changes to your code tofix this and make the ship turn towards the touch location

-- Main

-- Use this function to perform your initial setup
function setup()

    iparameter("Bricks", 1, 5)
    theBricks ={}
    brickIndex=0
    touching = false    
    -- Define cage
    cage = physics.body(CHAIN, 4, vec2(0,0), vec2(WIDTH, 0),
                    vec2(WIDTH, HEIGHT), vec2(0, HEIGHT))
    cage.type = STATIC
    
    -- Define hero
    hero = Ship()
    hero:setUp(WIDTH/2,HEIGHT/2, 0.5) -- x pos, y pos, restitution
    
    physics.gravity(0,0)
    
    -- Define thrust touch area
    -- thrust = vec2(WIDTH -100,HEIGHT/2)

end

function touched(touch)
    hero:touched(touch)
end

function collide(contact)
end

function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    
    checkParameter()  
        
    local t
    for t = 1, Bricks do
        theBricks[t]:Update()
    end
    
    -- Draw cage
    stroke(0, 255, 0, 255)
    noFill()
    makeCage()

    -- Check for thrust
    
    -- Draw hero
    hero:Update()
    
end -- end draw()
    

function makeCage()
    
    local points = cage.points
    local a, b
    for j = 1,#points do
        a = points[j]
        b = points[(j % #points)+1]
        line(a.x, a.y, b.x, b.y)
    end
end

function makeBrick()
    theBricks[Bricks]= Brick()
    theBricks[Bricks]:setUp(300+(Bricks*30),200+(Bricks*30),120,30,40,1.1)
end

    -- Increase or decrease bricks in table
function checkParameter()
    if Bricks > brickIndex then
        table.insert(theBricks, Bricks)
        makeBrick()
    elseif Bricks < brickIndex then
        theBricks[brickIndex] = nil
    end
        brickIndex = Bricks    
end

-- Brick class

Brick = class()

function Brick:setUp(x,y,w,h,r,bounce)
    
    -- Set self.variables for accepted position, dimensions and qualities
    self.x = x
    self.y = y
    self.w = w
    self.h = h
    self.r = r
    
    self.body = physics.body(POLYGON,
                vec2(0,0), vec2(w,0), vec2(w,h), vec2(0,h))
    
    self.body.x = x
    self.body.y = y
    
    self.body.angle = r
    
    self.body.restitution = bounce

end

function Brick:Update()

    self.x = self.body.x -- Now the position of the rectangle
    self.y = self.body.y -- equals the position of the physics.body (I think).
    
    self:draw()
    
end


function Brick:draw()
    -- Codea does not automatically call this method
    -- Draw the brick
    
    pushMatrix()
    stroke(255, 0, 0, 255)
    fill(255, 0, 0, 255)

    translate(self.body.x, self.body.y)
    rotate(self.body.angle)

    rect(0,0,self.w,self.h)
    popMatrix()
    
end

-- Ship class

Ship = class()

function Ship:setUp(x,y,b)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.b = b
       
    self.body = physics.body(POLYGON, vec2(0,0), vec2(30,0), vec2(15,40))
    
    self.body.x = x
    self.body.y = y

    self.body.restitution = b
    
end

function Ship:Update()
    self.x = self.body.x -- Now the position of the visible ship
    self.y = self.body.y -- equals the position of the physics.body (I think)
    
    if touching then
        self:thrust()
    end
      
    self:draw()    
end

function Ship:draw()
    -- Codea does not automatically call this method
    stroke(255, 255, 255, 255)
    lineCapMode(ROUND)
    
    translate(self.body.x, self.body.y)
    rotate(self.body.angle)
    
    local points = self.body.points
    local a, b
    for j = 1,#points do
        a = points[j]
        b = points[(j % #points)+1]
        line(a.x, a.y, b.x, b.y)
    end
end

function Ship:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == BEGAN or touch.state == MOVING then
        touching = true
        target = vec2(touch.x, touch.y)
    elseif touch.state == ENDED then
        touching = false    
    end
end


--@John's applyForce code, adapted by me
-- compute thrust direction
function Ship:thrust()
    local ang = math.rad(self.body.angle) + math.pi/2
    
    dir = vec2(math.cos(ang), math.sin(ang))
    thrustPower = 100 -- tweak to get the right feel in the game
    self.body:applyForce(dir * thrustPower) -- apply force from center of mass
    
    maxSpeed = 200 -- maximum linear velocity of ship
    speed = self.body.linearVelocity:len()
    -- Maybe he means speed, not "velocity," below?
    if speed > maxSpeed then
        self.body.linearVelocity = self.body.linearVelocity:normalize() * maxSpeed
    end
    
    if touching then
        local diff = target - self.body.position
        diff = diff:normalize()
        ang = math.deg(math.atan2(diff.y, diff.x))
        t = self.body.angle + 90 - ang
        while t > 180 do t = t - 360 end
        while t < -180 do t = t + 360 end      
        self.body:applyTorque(-t * 0.04 - self.body.angularVelocity * 0.01)
    end

end

@John, thanks again for your help. This would have taken me a while to figure out on my own. Like, two or three years.

@John: I still get odd movement with this code, with the ship moving in a kind of unpredictable arc. I’ll keep tinkering, but may I trouble you for one short explanation? How does applyForce work, exactly? What I see in the documentation seems different than what you used. Am I right in thinking that the force must come from a particular point, like a “place” in the overall “area?” Thanks again for all your help so far.

By default applyForce will apply a force to the object from the center of mass. F = ma, so essentially applyForce is accelerating the object proportional to its mass, that is all it does.
This will normally just move the object, however if there is any kind of angular velocity present the object will not go exactly straight because we are applying force in the direction that the ship is pointed at. You would probably need to apply some stabilising torque in order to get it to head straight.
The code I provided you with (if you just copy and paste) will move the ship towards where you touch on the screen in a completely predictable way. I suggest you look into how the torque is applied if you want more stable behaviour from the ship.

Thanks again @John – I’ll keep at it! If I make any breakthroughs I’ll let you know…

@John, just fyi, I did find a formula that worked to create the Asteroids-style thrust I wanted: from the “back” (actually the center) of the ship, in the direction the ship is pointing. It is:

dir = vec2(-math.sin(ang), math.cos(ang))

I found it in a Java version of Asteroids, but I don’t have the math background to know why it works. So today I swallowed my pride and actually purchased a copy of “Trigonometry for Dummies.”