Movement
Key Points:
[ ]
What does "game feel" mean and what are its key attributes?[ ]
Does every game have "game feel"?[ ]
What is an ADSR envelope and what does it have to do with "game feel"?[ ]
How do I make a character move a certain way?[ ]
What is an explicit state machine and how does it differ from using a bunch of state-tracking bools?[ ]
Why is Mario's jump so complex?
Game Feel
Imagine we made a game based on our interactive drawing code. We could easily move rectangles around the screen with the arrow keys—but we probably don't want to only make games about sliding rectangles. There are two main reasons this would feel slidey:
- Its physical movement would probably not be very responsive, but more importantly:
- It wouldn't communicate its physical movement clearly.
No matter how much time we spend on (1), if we can't tell whether the rectangle is skidding to a halt or speeding up at a glance, we won't be able to play effectively and we won't have the subjective experience of controlling a physical object: game feel. In Swink's book, a game has game feel to the extent that it combines simulated space, real-time control, and polish. It's your bridge to feeling like you are Super Mario in a very real sense—even flinching when you land on spikes (I mean, when Mario lands on spikes).
Not every game needs game feel, but because it puts pressure on so many bits of a game engine—from input to physics and animation—we'll dig into it this week.
Envelopes: ADSR
From the physical simulation side of things, we can discuss a character's handling in terms of responsiveness to changes in inputs. One useful tool here is the attack/decay/sustain/release envelope borrowed from signal processing. We can compare and contrast the differences between a character who has a very sharp (or instantaneous) acceleration up to its top speed (and a similarly sharp braking deceleration when stopping) and one who has a smoother ramp up to their top speed, or one who puts on an initial burst but then settles into a steady sustained speed. That initial burst itself can become the subject of emergent game dynamics, and players will quickly learn how to manage their movement to be bursting as often as possible.
It's possible to examine individual game characters' movement (usually in a single dimension at a time) using this formalism, and we can even use it to guide our design decisions: how long is the warmup period of this move, what maximum speed does it get to, and how long does it take to return to zero?
From a programming standpoint, we can characterize the character's controls in terms of a bunch of parameters (initial velocity boost, attack acceleration, maximum velocity, decay rate if any, sustain level, release acceleration) and apply those either to the character's net velocity or to one component, e.g. x or y velocity (or another, non-velocity variable!). For this example, we'll use a simpler notion of an initial velocity change, an acceleration, a maximum velocity, and a braking acceleration when the button is released.
pub struct Movement { // The units are I guess "pixels per frame" pub attack_initial:f32, pub attack_rate:f32, pub attack_peak:f32, pub release_rate:f32 }
We can also interpret a pair of button inputs as defining values along an axis, and use that to control the direction of movement. Note that we're careful here not to compare floating point numbers directly using equality.
let x_axis = if key_down(VirtualKeyCode::Left) { -1 } else { 0 } + if key_down(VirtualKeyCode::Right) { 1 } else { 0 }; if x_axis != 0 { let x_axis = x_axis as f32; if vx.abs() < 0.001 { vx = x_axis * xmovement.attack_initial; } vx = (vx + x_axis * xmovement.attack_rate).clamp(-xmovement.attack_peak, xmovement.attack_peak); } else { if vx.abs() < xmovement.release_rate { vx= 0.0; } else { vx -= vx.signum() * xmovement.release_rate.min(vx.abs()); } } x += vx;
The key idea here is that we've turned some aspect of the character's physical behavior into data rather than code.
By choosing different values for the fields of Movement
we can make a variety of different-feeling characters appropriate for different types of games.
We could handle movement in two axes (e.g. in a top-down game) either by using one Movement
for both of the components of motion, or use the Movement
to determine just the speed while the direction is given by the axes.
We could also implement mechanics like jumping by using Movement
parameters, though we'd want something a bit more elaborate to handle a jump like that of Super Mario.
Activity: Feeling out Feel
Set up the interactive drawing example with a colored rectangle whose position is determined by two variables (x and y position floats, which you'll cast to usize when drawing). Also set up an x-velocity variable for this rectangle. Take the movement code above and set it up to control your rectangle.
Try two different Movement
structs for xmovement
and compare the results. What kinds of games would each be a better or worse fit for?
let xmovement1 = Movement{ attack_initial: 0.0, attack_rate: 0.01, attack_peak: 0.5, release_rate: 0.005 }; let xmovement2 = Movement { attack_initial: 0.5, attack_rate: 0.0, attack_peak: 0.5, release_rate: 0.5 }; let xmovement = xmovement1;
State Machines
Characters are often partially under the control of the animation system and under the control of the player's direct input; moreover, which inputs are available also tend to depend on the character's state. We can also give multiple inputs to a character simultaneously: for example, to run and then to jump while ducking (but you can't for instance walk while running!). It's helpful to organize all this complexity using formalisms based on finite state machines.
Finite state machines have a formal CS definition related to regular languages, but we'll ignore that here in favor of their more flexible if a bit scruffier and less well specified cousins. In games, we often think of characters as being in different states at different times (and sometimes at the same time—our first departure from the formal theory). The FSM is defined by the set of possible states and the permissible transitions between them (and the preconditions and effects of those transitions).
Super Mario
For example, Mario has distinct standing, walking, running, and falling states (among others). But Mario also has powerup states—small or big or fire—and whether he's invincible or not. Fire acts mostly like big, but you can only have exactly one of those three states simultaneously. If you're hit while you're Fire Mario, you turn into Super (big) Mario; if you're hit while big you turn small; and if you're hit while small you die. Likewise, you can transition from walking to falling or jumping to falling, but not directly from falling to jumping.
We can use this representation in lots of ways! We could identify cyclic animations with state machine states and one-shot animations with transitions, we could set physical properties like acceleration due to gravity based on the current state, we could implement things like hit-stun/temporary invulnerability by using states with timed exit transitions, we could turn colliders off and on depending on the current state, and more.
Mario in particular can be defined by several simple state machines in a kind of stack or by a more complex formalism like concurrent hierarchical state machines (e.g. a powerup state machine concurrent with a ground/air state machine, each of whose states is itself a state machine handling horizontal and possibly vertical movement appropriately).
Of course it's also possible to model Mario with a bunch of booleans or timer variables: on_ground
, big
, fire
, rising_jump
, falling
, ducking
, etc.
But this introduces the possibility of bugs where there are transitions we might not have anticipated—what if small Mario gets a fire flower and we set fire
but forget to set big
?
Moreover, most of these flags will be ignored most of the time, but they'll keep sitting there taking up RAM anyway.
Plus, we'd have to write a lot of repetitive code to glue together character state and animations!
Let's show the portion of Mario's state machine that's relevant to movement. We'll abstract away ducking, left vs right movement, and horizontal control while in the air.
Mario's Jump
Let's dig in to Mario's jump for a minute—why does it use so many states? The short answer is "because that's what feels best", and the long answer gives an object lesson in the lengths to which game designers go to achieve the feel they want. The goal is to have an expressive but predictable jump where the player can decide how high to jump each time, but the designer and player both can anticipate where the jump will fall within a reasonable range of heights and distances—all while feeling natural, despite the fact that it's highly improbable that a person can decide how high to jump after leaving the ground!
Mario starts out on the ground. He can freely transition between standing, walking, and running. He can also press the jump button to initiate a jump. This gives him some initial vertical velocity (based on his horizontal speed) and also starts a timer. The designers didn't want the player to be able to feather the jump button for just a moment to get a very (and unpredictably) tiny jump, so they put a floor on the minimum air time to ensure that there was a steady shortest jump. By the same token, they needed to be able to predict the highest possible jump, so they put a limit on how long the button can be held. So this gives a range of possible jumps determined during the controlled period. In the uncontrolled rise period, Mario continues to accelerate due to gravity (at a rate half that of the falling state), gradually losing his initial upwards velocity. When it crosses zero, Mario switches to falling mode and its standard gravity. Whenever Mario is in the air, he also accelerates horizontally due to directional input at about half the normal rate.
All this together gives Mario a slow, controlled rise followed by a precise and rapid fall to the ground. It's not a bad starting point for designing a jump, but other options are possible!
Mario's World
Note that Mario's movement is defined with relatively slow acceleration curves and a jump which starts very "floaty" and lands pretty hard. Compare with Samus Aran from Metroid, whose horizontal movement is extremely precise with instantaneous acceleration and just one speed, and whose jump is a bit like riding an elevator: while holding the button you keep going up until you get to the top (or release the button—although, as in Mario, there is a minimum jump duration; in this case 10 frames). This makes sense when you compare levels from their respective games:
Activity: Implementing Jumping
Try to implement a Mario-style jumping state machine.
The key thing to model here is the distinct states, so you'll want to reach for an enum
.
By tweaking the parameters, you could get both Mario's and Samus's jump with the exact same actual code!
Link
Link from The Legend of Zelda is a simple character who doesn't move by accelerating around—but he still has some interesting states!
In particular, Link's got a variety of states influencing his movement—he can't turn or move while attacking, knockback forcibly moves him backwards (but he is able to move again shortly), and he has a hitstun state where he can't take additional damage but can move and attack normally (Mario has this too, but I skipped it before). These combine to create real penalties for getting hit without putting the player into impossible situations like being juggled between two enemies and killed or being able to walk through a large enemy within the hitstun window.
Activity: State Machines
Pick a character from a video game you know well. List out their distinct movement and animation states and the transitions between them (you can use the formalism I've been showing or your own). Describe their physical behaviors in each state, maybe with envelopes if you feel like it.