5.2 Physics Feel
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
You turn a bare scene into a playable game one behaviour at a time — gravity, a ground-guarded jump, bounce, friction, a win condition — running each change before adding the next. That is the implement-and-test half of the outcome; the structured half arrives with classes in §5.3.
Learning Objectives
By the end of this section, you will be able to:
- Adjust gravity to create different game feels (earth-like, top-down, upside-down).
- Read and write a sprite's
.velvector to control movement. - Implement a jump that only works when the player is on the ground.
- Tune
.bouncinessand.frictionto change how a sprite reacts to a collision. - Push a sprite continuously with
applyForce, and explain how it differs from setting.veldirectly. - Remove a sprite from the world with
.delete()and explain what that does and does not clean up.
This section picks up exactly where §5.1 left off: a player, a ground, a gold goal, and arrow-key movement. By the end, that scene will be a small complete game — the player can jump, bounce off walls, get a wind boost, and win by reaching the goal.
5.2.1 Tuning Gravity
world.gravity.y pulls every dynamic sprite downward. 10 feels roughly earth-like; set it to 0 for top-down games, or try negative values for "upside-down" worlds.
A force that pulls dynamic sprites in a direction. In moSHion, world.gravity.y controls vertical gravity. Positive values pull downward; zero means no gravity; negative values pull upward.
Notice that world.gravity.y is a number you pick, not a constant you look up. Nothing in this section asks what gravity is — every property here (gravity, bounciness, friction, and the force in §5.2.4) is a dial, and the section's real subject is what turning each one does to the way a game feels to play. That is why every Try It Now asks you to try several values rather than the one correct value. There isn't one.
Here's the scene from the end of §5.1. Change world.gravity.y to each of these values and run the sketch: 10, 0, -5, 20. Describe what happens to the player in each case — remember it also has arrow-key movement now.
▶ Press Run to see the output…
Solution
10: Player falls at a normal speed and lands on the ground, same as §5.1.0: Player floats at its starting height — arrow keys still work, but nothing pulls it down.-5: Player drifts upward off the top of the canvas.20: Player falls fast enough that it's easy to overshoot the ground on the first frame or two.
Set it back to 10 before moving on — the rest of this section assumes earth-like gravity.
5.2.2 Adding a Jump
A jump needs two things: (1) detect the space bar press, and (2) check that the player is on the ground so they can't jump in mid-air. player.colliding(ground) returns true when the player sprite is touching the ground sprite.
Setting player.vel.y = -8 gives an upward burst. Gravity pulls the player back down, creating an arc.
The ground check (player.colliding(ground)) prevents double-jumping. kb.presses is edge-triggered, so holding space does nothing on its own — but without the ground check every separate press would fire, mid-air ones included, and tapping space repeatedly would let the player climb straight off the top of the canvas. With the check, a jump can only start from a surface.
Add a jump to your scene: pressing space should launch the player upward, but only while it's touching ground.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
You can now walk toward the gold goal and hop over it if you overshoot. The jump only works when the player is touching the ground — try it in mid-air and nothing happens.
5.2.3 Bounciness and Friction
Drop a sprite on the ground in moSHion and it lands with a thud and stays there — both .bounciness and .friction default to 0. .bounciness (also called restitution) is how much speed a sprite keeps after a collision: 0 means it sticks, 1 means it bounces back at full speed. .friction resists sliding along a surface: 0 is ice, higher values drag motion to a stop.
Both are per-sprite properties. When two sprites touch, the physics engine combines their values: bounciness uses the higher of the two, friction uses a geometric mean.
Definition: Bounciness and Friction
.bounciness (restitution) is how much speed a sprite keeps after a collision, from 0 (sticks) to 1 (bounces at full speed). .friction resists sliding along a surface, from 0 (ice) to higher values that drag motion to a stop. When two colliding sprites disagree, bounciness uses the higher value and friction uses a geometric mean of the two.
Give your player a .bounciness of 0.6 and your ground a .friction of 0.5. Add walls on the left and right edges of the canvas (static sprites) so the player bounces off them instead of leaving the screen. Jump into a wall and watch what happens.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
The player now noticeably rebounds off a wall it jumps into, instead of just stopping — that's .bounciness on the player doing the work, since the walls themselves have none set. On the ground, .friction slows any horizontal slide left over from a bounce.
5.2.4 Applying Forces
sprite.applyForce(fx, fy) pushes a sprite with a force, in Newtons — the raw physics-engine unit, not pixels — along the x and y axes. Call it every frame for continuous thrust, or once for a single shove. Forces respect mass and add to whatever motion the sprite already has — this is different from setting .vel directly, which overrides physics entirely rather than nudging it.
Definition: applyForce
sprite.applyForce(fx, fy) pushes a sprite with a continuous or one-time force, measured in Newtons rather than pixels, along the x and y axes. Unlike setting .vel directly, applyForce respects the sprite's mass and adds to existing motion rather than replacing it.
.vel and applyForce solve different problems, and their units don't match — .vel is pixels per frame, applyForce is Newtons, with no fixed conversion between the two. Setting player.vel.x = 4 every frame — what §5.1.6 already does — is right for direct player control, because you're telling the engine exactly how fast to move, full stop. applyForce is right when you want the physics engine itself to work out the resulting motion from a push, layered on top of whatever the player is already doing. Expect to try a number, watch what it does, and adjust by a large factor rather than a small one.
Add a "wind zone" to your scene: while the player is above y = 200 (the upper half of the canvas), apply an upward force of (0, -3) every frame in addition to normal gravity and movement. Watch how it changes the feel of a jump made inside that zone versus one made near the ground.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
A jump that reaches the upper half of the canvas gets an extra push upward from the wind zone, hanging in the air noticeably longer than a jump that stays low. Because applyForce adds to existing motion instead of replacing it, the wind layers on top of the jump arc rather than overriding it.
5.2.5 Reaching the Goal
sprite.delete() removes a sprite from the world. After deleting, it stops drawing, stops updating, and its physics body is gone. There is no undo — if you only want to hide a sprite temporarily, set .visible = false instead of deleting it.
Definition: delete()
sprite.delete() permanently removes a sprite from the world: it stops drawing, stops updating, and its physics body is gone. There is no undo. To hide a sprite without permanently removing it, set .visible = false instead.
Chapter 6 introduces overlaps(), the proper tool for detecting when two sprites touch. For now, a simple distance check is enough to finish your game: compare the player's position to the goal's position, and treat "close enough" as a win.
Finish the game: when the player gets within 25 pixels of the goal (compare player.x/player.y to goal.x/goal.y), delete the goal and print "You win!" to the console — exactly once, not every frame.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Guide the player to the gold square and it vanishes with a single "You win!" in the console — never a repeat, because goal becomes null the instant it's deleted, and the whole check is wrapped in if (goal) so it stops running once there's nothing left to check against. This is the complete game §5.3 will refactor into classes.
5.2.6 Challenge: Extend It Yourself
No starter code this time — you build it. Take the finished game from this section (player, ground, goal, walls, jump, wind zone, win condition) and add a second, harder-to-reach goal: place it above the wind zone line, worth finding as a bonus. Track how many goals have been collected in a variable, and print a different message — "All goals collected!" — only once both are gone.
Hint (try the Challenge yourself first!)
Give both goals the same distance-check treatment from §5.2.5, but write it as a small function you call twice (once per goal) instead of copy-pasting the whole block — you'll meet a cleaner way to do this with overlaps() and Groups at the start of Chapter 6.
Problem Set 5.2
Problem 1. What does world.gravity.y = 0 do? What kind of game would use this setting?
Solution
Step 1 — What the property controls:
world.gravity.y is the downward pull applied to every dynamic sprite. Setting it to 0 removes that pull entirely, so a sprite stays exactly where its velocity puts it and never drifts toward the bottom of the canvas.
Step 2 — What that feels like to play: With no vertical pull, "up" and "down" stop being special — the canvas reads as a floor seen from above rather than a wall seen from the side.
Answer: It turns gravity off, so dynamic sprites no longer fall. Top-down games use this setting: a dungeon crawler, a racing game seen from above, or a puzzle game where pieces stay put until the player moves them.
Problem 2. In the jump code, why do we check player.colliding(ground) before setting player.vel.y = -8? What would happen without this check?
Solution
Step 1 — What the check asks:
player.colliding(ground) is true only while the player sprite is actually touching the ground sprite. Guarding the jump with it means "only launch from a surface."
Step 2 — What happens without it:
The jump would fire on any space-bar press, including mid-air ones. Each press would reset player.vel.y to -8 before gravity had finished pulling the player back down, so repeated presses would carry the player upward indefinitely.
Answer: The check makes the ground a precondition for jumping. Without it the player could jump in mid-air, and repeated presses would let them climb off the top of the canvas instead of arcing back down.
Problem 3. What does .vel stand for? What units is it measured in?
Solution
Step 1 — The name:
.vel is short for velocity. It is a vector with two components, .vel.x and .vel.y, describing how fast the sprite is moving and in which direction.
Step 2 — The units:
Velocity in moSHion is measured in pixels per frame — not per second. player.vel.x = 4 moves the sprite 4 pixels to the right on each of roughly 60 frames a second.
Answer: .vel stands for velocity, measured in pixels per frame.
Problem 4. What happens to a sprite's vertical velocity after you set player.vel.y = -8 and the sprite is in the air? Why doesn't it keep going up forever?
Solution
Step 1 — What the assignment does:
Setting player.vel.y = -8 replaces the sprite's vertical velocity outright with 8 pixels per frame upward (negative y is up).
Step 2 — What gravity does next:
world.gravity.y keeps acting every frame after that. It adds a small downward amount to .vel.y on each frame, so -8 becomes -7, then -6, and so on.
Step 3 — The turning point:
Once .vel.y passes through 0 the sprite is momentarily at the top of its arc; gravity keeps adding, .vel.y goes positive, and the player falls.
Answer: Vertical velocity shrinks toward zero, crosses it, and then grows downward. The player does not keep rising because the jump sets velocity once, while gravity changes it every frame.
Problem 5. Modify the jump example so the player jumps higher. What single value would you change?
Solution
Step 1 — Locate the value that sizes the jump:
The whole jump is one line: player.vel.y = -8;. That -8 is the launch speed, and nothing else in the jump code affects how high the player goes.
Step 2 — Change it in the right direction:
Negative y is up, so a more negative number is a faster launch and a higher arc — -12 instead of -8.
Answer: Change -8 to a more negative number, e.g. player.vel.y = -12;. (Lowering world.gravity.y would also produce a higher jump, but it changes how every sprite falls, not just this one.)
Problem 6. What would happen if you set player.vel.y = -8 every frame inside draw (without any key check)?
Solution
Step 1 — How often draw runs:
draw is called about 60 times a second, so an unguarded line in it runs on every frame.
Step 2 — What that does to velocity:
Gravity nudges .vel.y downward each frame, but the assignment immediately overwrites it back to -8 before that nudge can accumulate. Vertical velocity is pinned at -8 forever.
Answer: The player would rise at a constant 8 pixels per frame and never come down — straight off the top of the canvas. Gravity is still being applied, but the assignment discards its effect every frame, so the arc never forms.
Problem 7. What is the difference between .bounciness and .friction? Give a real-world material that would have a high value for each.
Solution
Step 1 — .bounciness:
Also called restitution, it is how much speed a sprite keeps after a collision — 0 sticks on impact, 1 rebounds at full speed.
Step 2 — .friction:
It resists sliding along a surface rather than bouncing off it — 0 is frictionless, higher values drag horizontal motion to a stop.
Step 3 — The distinction: Bounciness acts on the impact itself; friction acts on the contact that follows it.
Answer: .bounciness governs how much speed survives a collision; .friction governs how much a sprite resists sliding along a surface. A rubber ball has high bounciness; sandpaper (or a rough concrete floor) has high friction.
Problem 8. Two sprites collide: one has bounciness: 0.2, the other bounciness: 0.8. Which value does the engine use for the collision? What about if their .friction values were 0.2 and 0.8 instead?
Solution
Step 1 — Bounciness combines by taking the higher value:
Between 0.2 and 0.8, the engine uses 0.8. One bouncy surface is enough to produce a bouncy collision.
Step 2 — Friction combines by geometric mean: Friction instead uses the geometric mean of the two values: \(\sqrt{0.2 \times 0.8} = \sqrt{0.16} = 0.4\).
Step 3 — Why they differ: Bounciness describes a rebound either surface can supply on its own, so the greater value wins. Friction describes how two surfaces drag against each other, so both contribute.
Answer: The collision uses bounciness 0.8 (the higher of the two). With those same numbers as friction it would use 0.4, the geometric mean of 0.2 and 0.8.
Problem 9. Explain the difference between setting player.vel.x = 4 and calling player.applyForce(4, 0). When would you use each one?
Solution
Step 1 — player.vel.x = 4 commands the motion:
It sets velocity directly, in pixels per frame, replacing whatever the sprite was doing horizontally. The result is exact and immediate, and it ignores the sprite's mass.
Step 2 — player.applyForce(4, 0) requests the motion:
It applies a push measured in Newtons and lets the physics engine work out the resulting velocity from the sprite's mass and current motion. It adds to existing movement rather than replacing it.
Step 3 — Note the units do not match:
Pixels per frame and Newtons have no fixed conversion, so 4 in one is not 4 in the other. Expect to tune a force by a large factor, not a small one.
Answer: .vel overrides physics with an exact speed — right for direct player control, where the input should feel immediate and predictable. applyForce layers a push on top of existing motion and respects mass — right for environmental effects like wind, explosions, or thrust.
Problem 10. In the wind-zone code, why does the effect layer on top of a jump instead of replacing it?
Solution
Step 1 — What the wind code does:
player.applyForce(0, -3) runs every frame the player is above y = 200. It adds an upward push to whatever velocity the player already has.
Step 2 — Why the jump survives it:
Because applyForce adds rather than assigns, the jump's upward velocity is still there underneath. Setting .vel.y instead would discard the jump arc and pin the player to one wind-driven speed.
Answer: applyForce adds to existing motion instead of replacing it, so the wind's push accumulates on top of the jump's velocity. A jump made inside the zone hangs noticeably longer, which is the effect layering rather than overwriting.
Problem 11. What does sprite.delete() do that setting sprite.visible = false does not?
Solution
Step 1 — visible = false hides the sprite:
Drawing stops, but the sprite is still in the world. It keeps updating, and its physics body still collides — an invisible wall the player can walk into.
Step 2 — delete() removes it:
Drawing stops, updating stops, and the physics body is gone. Nothing is left to collide with, and there is no undo.
Answer: delete() removes the sprite's physics body and stops it updating; .visible = false only stops it being drawn, leaving an invisible but fully solid sprite in the world. Use .visible when you want the sprite back later, delete() when you do not.
Problem 12. In the win-condition code, what would happen if the if (goal) check were removed, and the player stayed sitting on top of the goal's old position after it was deleted?
Solution
Step 1 — What the win check reads:
The block computes a distance from goal.x and goal.y. Once goal.delete() has run, the code also sets goal = null — the variable no longer holds a sprite.
Step 2 — Reading a property off null:
Without the if (goal) guard, the next frame would evaluate goal.x on null and throw a TypeError, stopping draw from that frame onward.
Step 3 — Why sitting on the old position matters: It guarantees the failure. The player is still within 25 pixels of where the goal used to be, so the check runs again on the very next frame instead of the player wandering out of range first.
Answer: draw would crash with a TypeError — reading .x of null — on the frame after the win, and the game would stop rendering. The if (goal) guard exists so the distance check stops running once there is no goal left to measure against.
Key Terms
| Term | Definition |
|---|---|
| Gravity | A force that pulls dynamic sprites in a direction; controlled by world.gravity.y |
| Velocity | A vector (x and y components) describing how fast and in what direction a sprite moves, in pixels per frame |
| Bounciness | How much speed a sprite keeps after a collision, from 0 (sticks) to 1 (full-speed bounce) |
| Friction | How much a sprite resists sliding along a surface, from 0 (ice) to higher values that drag it to a stop |
| applyForce | A method that pushes a sprite with a continuous or one-time force, respecting its mass and adding to existing motion |
| delete() | A method that permanently removes a sprite from the world, with no undo |