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.
@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.”