524 lines
20 KiB
Text
524 lines
20 KiB
Text
== Lesson 24: Physics
|
|
|
|
image::learn/24-physics.png[The finished lesson, 480]
|
|
|
|
Up to now every moving thing in your games has moved because you did the
|
|
arithmetic. A number went up by two each frame; if it went past an edge you
|
|
turned it round. That is the honest way to make a game move, and most games
|
|
are made of it.
|
|
|
|
This lesson hands the arithmetic to somebody else. A *physics engine*
|
|
simulates the world -- gravity, falling, sliding, stacking, bouncing,
|
|
rolling, and things hitting each other -- and moves your nodes for you. By
|
|
the end you will have a tray of crates and a cannon that knocks them over.
|
|
The last section is about when not to do any of this, and it is the part to
|
|
read twice.
|
|
|
|
=== What It Does, and What It Does Not
|
|
|
|
Give a node a *body* and the simulation takes charge of it. From then on it
|
|
falls under gravity, lands on whatever is beneath it, tips when it is hit off
|
|
center, rests in a stack, slides to a stop, and the node follows wherever the
|
|
simulation puts it. You stop setting its position. That is the deal: you
|
|
hand over the position and rotation of that node, and in exchange you get
|
|
behavior you did not have to write.
|
|
|
|
What it does not do is anything about your game. It does not know what a
|
|
score is, or that the red crate is the one worth points, or that falling off
|
|
the edge means a life. It does not animate a character walking, it does not
|
|
know how a jump should feel, and it will not make an object move in a way you
|
|
can predict exactly. Physics is a machine for producing plausible mess.
|
|
|
|
Units are meters, kilograms, and seconds. Gravity is `0, -9.81, 0` unless you
|
|
say otherwise, which is Earth's, downward, in meters per second squared. A
|
|
crate that is one unit wide is one meter wide, and it falls like one. If your
|
|
scene is built at some other scale, everything will look like it is falling
|
|
through syrup or through a cartoon, and the fix is to build at meters.
|
|
|
|
=== A Floor and One Crate
|
|
|
|
Make a folder, make `crates.singe`, and type this in. It is a scene, a slab,
|
|
a crate above the slab, and three lines of physics.
|
|
|
|
[source,lua]
|
|
----
|
|
sceneEnable(true)
|
|
sceneSetBackground(16, 18, 26)
|
|
sceneSetAmbient(40, 42, 50)
|
|
|
|
local stone = materialNew()
|
|
materialSetColor(stone, 110, 110, 120)
|
|
materialSetRoughness(stone, 0.9)
|
|
|
|
local wood = materialNew()
|
|
materialSetColor(wood, 190, 140, 80)
|
|
materialSetRoughness(wood, 0.8)
|
|
|
|
local floor = nodeNew()
|
|
nodeSetMesh(floor, meshBox(16, 0.4, 16), stone)
|
|
nodeSetPosition(floor, 0, -0.2, 0)
|
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
|
|
|
local crate = nodeNew()
|
|
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood)
|
|
nodeSetPosition(crate, 0.3, 4, 0)
|
|
nodeSetRotation(crate, 20, 0, 15)
|
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
|
bodySetMass(crate, 4)
|
|
|
|
local sun = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(sun, -5, 9, 6)
|
|
nodeLookAt(sun, 0, 0, 0)
|
|
lightSetIntensity(sun, 1.7)
|
|
lightSetShadow(sun, true)
|
|
|
|
local camera = nodeNew()
|
|
nodeSetPosition(camera, 0, 4, 11)
|
|
nodeLookAt(camera, 0, 1, 0)
|
|
cameraSet(camera)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Run it with `Singe -R crates`. The crate falls from four meters up,
|
|
lands on a corner because you tilted it, rocks over onto a face, and stops.
|
|
Press *F5* to run it again and watch it once more.
|
|
|
|
Nothing in `onOverlayUpdate` moves anything. The two `bodyNew` calls did all
|
|
of it.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
|
----
|
|
|
|
`bodyNew` gives a node a body. It takes the node, what kind of body it is,
|
|
what shape the simulation should use for it, and the sizes of that shape.
|
|
|
|
There are three kinds, and choosing the right one is most of the skill.
|
|
|
|
* `BODY_STATIC` never moves. It is placed where the node is when you make the
|
|
body, and there it stays forever. Floors, walls, the ground, the cabinet,
|
|
the level. Moving its node afterwards does not move the body, which is a
|
|
trap worth remembering: static means static.
|
|
* `BODY_DYNAMIC` is moved by the simulation. It falls, bounces, slides, gets
|
|
pushed, and drags its node along with it. This is a crate, a barrel, a ball,
|
|
a piece of debris.
|
|
* `BODY_KINEMATIC` goes where *you* put its node, and shoves dynamic bodies
|
|
out of its way without ever being pushed itself. A moving platform, a
|
|
paddle, a door on a track, a character's swinging fist. It is the bridge
|
|
between your arithmetic and the simulation: you drive it, it drives them.
|
|
|
|
The shape is not the mesh. It is a rough stand-in the simulation uses because
|
|
it can answer questions about a box far faster than about nine hundred
|
|
triangles. `SHAPE_BOX` takes a width, a height, and a depth; `SHAPE_SPHERE`
|
|
takes a radius; `SHAPE_CAPSULE` and `SHAPE_CYLINDER` take a radius and a
|
|
height. There is `SHAPE_HULL`, which shrink-wraps whatever mesh is under the
|
|
node -- the usual answer for a model -- and `SHAPE_MESH`, the exact triangles,
|
|
which static scenery may use and a falling object may not.
|
|
|
|
The sizes are yours to get right. Nothing checks that the body you described
|
|
matches the mesh you drew, and a crate drawn at `0.8` with a body made at `2`
|
|
will float above the floor and knock things over from a distance. When
|
|
something behaves as if it were a different size, it is.
|
|
|
|
[source,lua]
|
|
----
|
|
bodySetMass(crate, 4)
|
|
----
|
|
|
|
A dynamic body's mass comes from its shape at the density of water, which
|
|
makes things much heavier than you expect: a box the size of a door weighs
|
|
about a hundred and forty kilograms, and nothing you can throw at it will
|
|
move it. Set the mass of anything meant to feel light. Four kilograms is a
|
|
crate you could pick up.
|
|
|
|
Mass matters because forces are shared out by it. The same shove moves a
|
|
light thing further, and an impulse of mass times speed is what it takes to
|
|
set something moving at that speed. Tune the mass first and the forces
|
|
afterwards, or you will chase your own tail.
|
|
|
|
=== Making Things Move
|
|
|
|
Three calls push a dynamic body around, and they are not interchangeable.
|
|
|
|
`bodySetVelocity(node, x, y, z)` says "you are now traveling at this speed",
|
|
replacing whatever it was doing. This is a serve, a launch, a spawn with some
|
|
speed already on it.
|
|
|
|
`bodyApplyImpulse(node, ix, iy, iz)` is a hit: a kick, an explosion, a bat
|
|
meeting a ball. It adds to what the body was already doing. Give it a point
|
|
in the world as three more numbers and it hits *there* instead of at the
|
|
middle, so a crate struck near the top tips over rather than sliding.
|
|
|
|
`bodyApplyForce(node, fx, fy, fz)` is a push that lasts: a rocket, a fan, a
|
|
conveyor. A force applied for one frame does almost nothing, which surprises
|
|
people who expected an impulse.
|
|
|
|
Gravity itself is `physicsSetGravity(x, y, z)` and it is world-wide. The moon
|
|
is `physicsSetGravity(0, -1.62, 0)`, space is all zeroes, and a game with
|
|
gravity sideways is one line away.
|
|
|
|
Two more settings finish the feel of an object. `bodySetBounce(node, amount)`
|
|
runs from `0`, which stops dead, to `1`, which comes back with everything it
|
|
arrived with; the default is a barely-there `0.1`. `bodySetFriction(node,
|
|
amount)` is `0` for ice and `0.5` by default. Both combine between the two
|
|
things touching, so an icy floor needs the low number on the floor, not on
|
|
everything that walks across it.
|
|
|
|
=== Knowing When Things Touch
|
|
|
|
[source,lua]
|
|
----
|
|
function onCollision(nodeA, nodeB, x, y, z, speed)
|
|
if speed > 4 then
|
|
knocks = knocks + 1
|
|
end
|
|
end
|
|
----
|
|
|
|
`knocks` there is a variable of your own, waiting at the top of the file with
|
|
`local knocks = 0`, and the toy below prints it.
|
|
|
|
`onCollision` is a callback, like `onOverlayUpdate`: you write it, the engine
|
|
calls it. It arrives once for each new contact -- two bodies that stay
|
|
touching do not report again -- with both nodes, the point in the world where
|
|
they met, and how fast they met. The speed is what lets a bump and a crash
|
|
sound different, and it is why the test above ignores the gentle settling of a
|
|
stack.
|
|
|
|
The other half of the pair is `onTrigger`. Make a body a trigger with
|
|
`bodySetTrigger(node, true)` and it stops pushing things: it becomes a region
|
|
that reports what enters and leaves it through `onTrigger(trigger, other,
|
|
entered)`. A finish line, a doorway, a pit, a pickup. It is the 3D
|
|
descendant of the collision checks from lesson eleven, and for anything
|
|
shaped like "did the player reach here" it is the call you want rather than a
|
|
solid body.
|
|
|
|
=== The Toy
|
|
|
|
Now the whole thing. Start from the script above and change it into this: a
|
|
tray with four walls, six crates in a pyramid, and a steel ball you can aim
|
|
and fire. The pieces are all ones you have met.
|
|
|
|
Everything the new code remembers goes at the top of the file, above the
|
|
scene:
|
|
|
|
[source,lua]
|
|
----
|
|
local crates = {}
|
|
local balls = {}
|
|
local aimX = 0
|
|
local aimLeft = false
|
|
local aimRight = false
|
|
local knocks = 0
|
|
local showShapes = false
|
|
----
|
|
|
|
Two more shared handles go with the materials: a polished metal for the ball
|
|
and the aiming marker, and one crate mesh for all six crates.
|
|
|
|
[source,lua]
|
|
----
|
|
local steel = materialNew()
|
|
materialSetColor(steel, 200, 205, 215)
|
|
materialSetMetallic(steel, 1)
|
|
materialSetRoughness(steel, 0.25)
|
|
|
|
local crateMesh = meshBox(0.8, 0.8, 0.8)
|
|
----
|
|
|
|
The tray's walls are four static boxes, made in a loop from a table of
|
|
placements, which is lesson six doing honest work:
|
|
|
|
[source,lua]
|
|
----
|
|
local wallMesh = meshBox(16, 1, 0.4)
|
|
|
|
for _, side in ipairs({ { 0, -8, 0 }, { 0, 8, 0 }, { -8, 0, 90 }, { 8, 0, 90 } }) do
|
|
local wall = nodeNew()
|
|
nodeSetMesh(wall, wallMesh, stone)
|
|
nodeSetPosition(wall, side[1], 0.5, side[2])
|
|
nodeSetRotation(wall, 0, side[3], 0)
|
|
bodyNew(wall, BODY_STATIC, SHAPE_BOX, 16, 1, 0.4)
|
|
end
|
|
----
|
|
|
|
Each entry is an X, a Z, and a turn about Y. Two walls lie across the back
|
|
and front; two are turned ninety degrees to make the sides. One mesh does for
|
|
all four, and the body is made after the node is placed and turned, because a
|
|
static body takes the node's position and rotation at the moment it is made.
|
|
|
|
The crates come from a function, so that six of them is six lines:
|
|
|
|
[source,lua]
|
|
----
|
|
local function crateAt(x, y)
|
|
local crate = nodeNew()
|
|
|
|
nodeSetMesh(crate, crateMesh, wood)
|
|
nodeSetPosition(crate, x, y, -2)
|
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
|
bodySetMass(crate, 4)
|
|
crates[#crates + 1] = crate
|
|
end
|
|
----
|
|
|
|
It keeps every crate it makes in the `crates` table, which is how the drawing
|
|
callback can ask all of them a question later. Six calls build the pyramid,
|
|
three on the floor, two on those, and one on top:
|
|
|
|
[source,lua]
|
|
----
|
|
crateAt(-0.9, 0.4)
|
|
crateAt(0, 0.4)
|
|
crateAt(0.9, 0.4)
|
|
crateAt(-0.45, 1.25)
|
|
crateAt(0.45, 1.25)
|
|
crateAt(0, 2.1)
|
|
----
|
|
|
|
The upper rows sit a finger's width above the ones below and drop into place
|
|
the moment the game starts. Leaving a small gap is easier than stacking
|
|
exactly, and it is kinder to the simulation than handing it a pile that is
|
|
already pressed together.
|
|
|
|
Firing makes a new ball each time:
|
|
|
|
[source,lua]
|
|
----
|
|
local function fire()
|
|
local ball = nodeNew()
|
|
|
|
nodeSetMesh(ball, meshSphere(0.35, 24), steel)
|
|
nodeSetPosition(ball, aimX, 1.2, 6)
|
|
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
|
|
bodySetMass(ball, 3)
|
|
bodySetBounce(ball, 0.3)
|
|
bodySetVelocity(ball, 0, 2, -14)
|
|
balls[#balls + 1] = ball
|
|
if #balls > 5 then
|
|
nodeDelete(balls[1])
|
|
table.remove(balls, 1)
|
|
end
|
|
end
|
|
----
|
|
|
|
Look at the order. The node is made, given a shape to draw, and *placed*, and
|
|
only then given a body. A dynamic body starts where its node is, and after
|
|
that the traffic runs the other way: the body moves the node. Setting the
|
|
position of a dynamic node yourself afterwards fights the simulation and the
|
|
simulation wins. When something must be moved, give it a velocity, hit it
|
|
with an impulse, or do what this does -- make a new one and let the old one
|
|
go.
|
|
|
|
Letting the old one go is `nodeDelete`, which frees the node, everything
|
|
under it, and the body that was riding on it. Without those three lines every
|
|
shot would stay in the world forever, and a few hundred balls later the
|
|
simulation would be doing a great deal of work on things nobody can see.
|
|
Cleaning up is part of using physics.
|
|
|
|
The controls and the display are the rest:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
fire()
|
|
elseif what == SWITCH_BUTTON3 then
|
|
showShapes = not showShapes
|
|
if showShapes then
|
|
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
|
|
else
|
|
physicsSetDebug(DEBUG_NONE)
|
|
end
|
|
elseif what == SWITCH_LEFT then
|
|
aimLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
aimRight = true
|
|
end
|
|
end
|
|
----
|
|
|
|
Space fires, shift turns the physics view on and off, and the arrows hold
|
|
down to slide the launch point. A flat metal plate with no body on it, moved
|
|
each frame to `aimX`, shows where the next ball will come from. The full
|
|
script, with the release callback and the rest of the setup, is
|
|
`24-physics.singe` in the `learn` folder.
|
|
|
|
`physicsSetDebug` is the most useful call in this lesson. It draws what the
|
|
simulation actually holds -- the real shapes in wireframe, and a red cross
|
|
wherever two bodies touched this step -- over the top of your scene. The
|
|
first time a crate hovers a hand's width above the floor, or falls through it,
|
|
or knocks something over from two meters away, turn this on and the answer
|
|
will be on the screen. Add `DEBUG_STATIC` to see the floor and walls as well.
|
|
It costs nothing while it is off.
|
|
|
|
Finally, the counting:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
local over = 0
|
|
local settled = true
|
|
|
|
if aimLeft then
|
|
aimX = math.max(aimX - 0.08, -5)
|
|
end
|
|
if aimRight then
|
|
aimX = math.min(aimX + 0.08, 5)
|
|
end
|
|
nodeSetPosition(marker, aimX, 0.05, 6)
|
|
|
|
for _, crate in ipairs(crates) do
|
|
local pitch, _, roll = nodeGetRotation(crate)
|
|
|
|
if math.abs(pitch) > 30 or math.abs(roll) > 30 then
|
|
over = over + 1
|
|
end
|
|
if not bodyIsResting(crate) then
|
|
settled = false
|
|
end
|
|
end
|
|
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Arrows aim. Space fires. Shift shows the shapes.")
|
|
overlayPrint(2, 4, "knocked over " .. over .. " of " .. #crates .. " hard hits " .. knocks)
|
|
if settled then
|
|
overlayPrint(2, 6, "everything has settled")
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
A crate is counted as knocked over when it is leaning more than thirty
|
|
degrees, which is a game rule written in one line of arithmetic on top of
|
|
whatever the simulation did. `nodeGetRotation` gives three angles and this
|
|
wants the first and the third, so the middle one is caught in a variable
|
|
called `_`. That underscore is an ordinary name with nothing special about
|
|
it, used by convention to say "I am ignoring this one".
|
|
|
|
`bodyIsResting` says whether the simulation has put a body to sleep because
|
|
it stopped moving. That is how you know a shot is over: not a timer, not a
|
|
guess, but every crate asleep. A bowling game scores its frame exactly there.
|
|
|
|
=== When Not to Use It
|
|
|
|
Physics is the most tempting tool in any engine and the one that wastes the
|
|
most weekends. Three things are true about it, and none of them are the
|
|
engine's fault.
|
|
|
|
*It is expensive.* Every dynamic body is work every step, sixty times a
|
|
second, whether anybody is looking at it or not. Forty crates is fine. Four
|
|
thousand is not, and on a small machine four hundred is not.
|
|
|
|
*It is unpredictable.* Run your toy twice and the crates land differently.
|
|
That is what makes it look alive, and it is also why a game cannot rely on it
|
|
for anything that must come out the same each time. A door that must open, a
|
|
platform that must arrive, a jump the player must be able to make: those are
|
|
arithmetic, or a tween, or a kinematic body on a path you wrote. Never leave
|
|
a thing the player must do to a simulation that can drop it behind a crate.
|
|
|
|
*It is hard to tune.* The mass, the friction, the bounce, the shape, and the
|
|
size all pull on each other, and "it feels wrong" rarely has one cause. Every
|
|
hour you spend making a physics crate feel good is an hour you did not spend
|
|
on the game.
|
|
|
|
So reach for it when the mess *is* the point: things toppling, debris flying,
|
|
a pile settling, a ball rattling round a room, a vehicle rolling over rough
|
|
ground, a ragdoll flopping down a staircase. Nobody can write that by hand
|
|
and nobody can tell you exactly how it should look.
|
|
|
|
Do not reach for it when you know what should happen. A bullet that flies
|
|
straight is two lines from lesson two. A platform that goes up and down is
|
|
`math.sin`. A player that walks and jumps the way players expect wants a
|
|
character controller (`playerNew`, in the manual), because a person is not a
|
|
box and never behaves like one. An enemy that patrols wants a path. A menu
|
|
that slides in wants a tween.
|
|
|
|
The rule that serves best: use physics for the things the player watches, and
|
|
arithmetic for the things the player does.
|
|
|
|
=== Try It
|
|
|
|
. *Change the world.* Add `physicsSetGravity(0, -1.62, 0)` near the top and
|
|
fire again. Then try `0, -30, 0`. The same crates, a different planet, one
|
|
line.
|
|
. *Change the ball.* Make the ball ten times heavier with `bodySetMass(ball,
|
|
30)`, then ten times lighter with `0.3`, keeping the speed the same. Then
|
|
put the mass back and change the `-14` instead. Two ways to hit harder, and
|
|
they do not feel the same.
|
|
. *Make the crates ice.* Add `bodySetFriction(crate, 0.02)` in `crateAt`, and
|
|
add a line giving the floor a low friction too. Watch the pile refuse to
|
|
stand up in the first place.
|
|
. *Wrong the shape on purpose.* In `crateAt`, change the body's sizes to
|
|
`2, 2, 2` while the mesh stays `0.8`. Run it, see the crates hover and
|
|
shove each other from nowhere, then press shift and watch the debug view
|
|
explain it in one picture.
|
|
. *Score it properly.* Use `bodyIsResting` on every crate to notice when the
|
|
shot has finished, and only then count how many are over and add that to a
|
|
score. That is the scoring half of a bowling game.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Add a line under the floor's `bodyNew` making the floor heavy, which is a
|
|
reasonable thing to think you want:
|
|
|
|
[source,lua]
|
|
----
|
|
bodySetMass(floor, 200)
|
|
----
|
|
|
|
Singe stops the game with something like:
|
|
|
|
----
|
|
32:bodySetMass: Node 1 is not a dynamic body, or the mass is not positive.
|
|
----
|
|
|
|
The first number is the line in your own file, so yours will differ.
|
|
|
|
The number after `Node` is the node's handle, the number `nodeNew` handed
|
|
back. The complaint is exact: mass belongs to things the simulation moves,
|
|
and a static body is not moved by anything, so it has no mass to set. Asking
|
|
is a sign that the body is the wrong type for what you had in mind -- and if
|
|
you truly want a heavy floor that can be shifted, what you want is a dynamic
|
|
body with a large mass, or a kinematic one you move yourself.
|
|
|
|
The same message appears for a mass of `0`, which people write meaning
|
|
"weightless". Weightless is not a mass. It is a world with no gravity, or
|
|
`playerSetGravityScale` on a character controller.
|
|
|
|
=== What You Learned
|
|
|
|
* A body attaches to a node, and from then on the node and the body are one
|
|
thing.
|
|
* Static never moves, dynamic is moved by the simulation, and kinematic is
|
|
moved by you and pushes everything else aside.
|
|
* The collision shape is a stand-in for the mesh, and you are responsible for
|
|
its sizes matching what you drew.
|
|
* Mass defaults to the shape's volume at the density of water, so set it on
|
|
anything meant to feel light.
|
|
* `bodySetVelocity` replaces a body's motion, `bodyApplyImpulse` is a hit, and
|
|
`bodyApplyForce` is a push that lasts.
|
|
* Do not set the position of a dynamic node; delete it and make a new one
|
|
when you must start it over.
|
|
* `onCollision` reports each new contact with its point and its speed, and a
|
|
trigger body reports arrivals and departures through `onTrigger`.
|
|
* `bodyIsResting` tells you when everything has stopped, which is usually when
|
|
a turn is over.
|
|
* `physicsSetDebug` draws the simulation's own view, and it answers most
|
|
physics questions faster than reading code does.
|
|
* Physics is expensive, unrepeatable, and fiddly. Use it for the mess, not for
|
|
the things the game depends on.
|
|
|
|
=== Next Time
|
|
|
|
Crates falling is one kind of spectacle. The other kind is made of a thousand
|
|
tiny things that live for half a second: sparks, smoke, fire, rain, and dust.
|
|
Next lesson is emitters, and it ends with an explosion. That is lesson
|
|
twenty-five.
|