Class: Doom::Game::PlayerPhysics
- Inherits:
-
Object
- Object
- Doom::Game::PlayerPhysics
- Defined in:
- lib/doom/game/player_physics.rb
Overview
Player movement and vertical physics, extracted from the Gosu window so it can be unit-tested without a graphics stack.
Tracks the player's feet position (floor_z) and vertical velocity (momz) in DOOM map units, matching Chocolate Doom's P_ZMovement semantics: GRAVITY=1 unit/tic^2, falls clamp to floor, viewheight squat on landing.
Constant Summary collapse
- PLAYER_RADIUS =
16.0- MAX_STEP_UP =
24- MIN_HEADROOM =
56- GRAVITY =
1.0- INITIAL_FALL_MOMZ =
First-tic kick when momz==0 (P_ZMovement uses -GRAVITY*2)
-2.0 # First-tic kick when momz==0 (P_ZMovement uses -GRAVITY*2)
- FALL_IMPACT_THRESHOLD =
momz < -GRAVITY*8 triggers viewheight squat
-8.0 # momz < -GRAVITY*8 triggers viewheight squat
- TICRATE =
35.0- FRICTION =
Horizontal movement runs per-tic, not per-frame. A frame-rate-dependent integration can never be deterministic, and lockstep needs every peer to get the same result from the same ticcmd.
FRICTION is DOOM's exact per-tic value (ORIG_FRICTION 0xE800/65536). Momentum is carried in units/sec, so THRUST_PER_TIC is chosen to hold the same terminal speed the old continuous model had:
v_terminal = thrust * FRICTION / (1 - FRICTION) 0.90625
- TERMINAL_SPEED =
units/sec
264.0- THRUST_PER_TIC =
~27.3
TERMINAL_SPEED * (1.0 - FRICTION) / FRICTION
- SIDE_THRUST_SCALE =
DOOM strafes slower than it walks
0.8
- STOPSPEED =
Snap-to-zero threshold (units/sec)
0.5
- SOLID_THING_RADIUS =
Solid thing types with their collision radii (from mobjinfo MF_SOLID).
{ 9 => 20, 65 => 20, 66 => 20, 67 => 20, 68 => 20, # Shotgun Guy variants 3004 => 20, 84 => 20, # Zombieman 3001 => 20, # Imp 3002 => 30, 58 => 30, # Demon, Spectre 3003 => 24, 69 => 24, # Baron, Hell Knight 3006 => 16, # Lost Soul 3005 => 31, # Cacodemon 16 => 40, # Cyberdemon 7 => 128, # Spider Mastermind 64 => 20, # Archvile 71 => 31, # Pain Elemental 2035 => 10, # Barrel 2028 => 16, # Tall lamp 48 => 16, 30 => 16, 32 => 16, # Tech column, green/red pillars 31 => 16, 33 => 16, 36 => 16, # Short pillars 41 => 16, 43 => 16, # Evil eye, burnt tree 54 => 32, # Brown tree 44 => 16, 45 => 16, 46 => 16, # Tall torches 55 => 16, 56 => 16, 57 => 16, # Short torches 47 => 16, 70 => 16, # Stubs 85 => 16, 86 => 16, # Tall tech lamps 2046 => 16 # Burning barrel }.freeze
Instance Attribute Summary collapse
-
#combat ⇒ Object
writeonly
Sets the attribute combat.
-
#floor_z ⇒ Object
readonly
Returns the value of attribute floor_z.
-
#item_pickup ⇒ Object
writeonly
Sets the attribute item_pickup.
-
#momz ⇒ Object
readonly
Returns the value of attribute momz.
-
#skill_hidden ⇒ Object
writeonly
Sets the attribute skill_hidden.
Instance Method Summary collapse
-
#compute_slide(px, py, dx, dy) ⇒ Object
When valid_move? fails, find the nearest blocking wall and return a slide vector projected along it.
-
#eye_z ⇒ Object
Eye position used by the renderer: feet + viewheight + view-bob.
-
#initialize(map, player_state) ⇒ PlayerPhysics
constructor
A new instance of PlayerPhysics.
-
#move(player, dx, dy) ⇒ Object
Move a player by (dx, dy) with collision and wall sliding, settling it onto the floor wherever it ends up.
- #reset ⇒ Object
-
#run_tic(player, cmd) ⇒ Object
Advance one player by one tic from its ticcmd: turn, thrust, friction, then move with wall sliding.
-
#settle(player, x, y) ⇒ Object
Settle onto the floor and push the resulting eye height to the player.
-
#settle_at(x, y) ⇒ Object
Bring the player onto the floor at (x, y) after a horizontal move.
-
#step(x, y) ⇒ Object
Per-tic vertical movement.
-
#valid_move?(old_x, old_y, new_x, new_y) ⇒ Boolean
True if the player can occupy (new_x, new_y).
Constructor Details
#initialize(map, player_state) ⇒ PlayerPhysics
Returns a new instance of PlayerPhysics.
66 67 68 69 70 71 72 73 74 |
# File 'lib/doom/game/player_physics.rb', line 66 def initialize(map, player_state) @map = map @player_state = player_state @floor_z = nil @momz = 0.0 @skill_hidden = {} @item_pickup = nil @combat = nil end |
Instance Attribute Details
#combat=(value) ⇒ Object (writeonly)
Sets the attribute combat
64 65 66 |
# File 'lib/doom/game/player_physics.rb', line 64 def combat=(value) @combat = value end |
#floor_z ⇒ Object (readonly)
Returns the value of attribute floor_z.
63 64 65 |
# File 'lib/doom/game/player_physics.rb', line 63 def floor_z @floor_z end |
#item_pickup=(value) ⇒ Object (writeonly)
Sets the attribute item_pickup
64 65 66 |
# File 'lib/doom/game/player_physics.rb', line 64 def item_pickup=(value) @item_pickup = value end |
#momz ⇒ Object (readonly)
Returns the value of attribute momz.
63 64 65 |
# File 'lib/doom/game/player_physics.rb', line 63 def momz @momz end |
#skill_hidden=(value) ⇒ Object (writeonly)
Sets the attribute skill_hidden
64 65 66 |
# File 'lib/doom/game/player_physics.rb', line 64 def skill_hidden=(value) @skill_hidden = value end |
Instance Method Details
#compute_slide(px, py, dx, dy) ⇒ Object
When valid_move? fails, find the nearest blocking wall and return a slide vector projected along it. Returns nil if no wall blocks.
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/doom/game/player_physics.rb', line 229 def (px, py, dx, dy) best_wall = nil best_dist = Float::INFINITY each_nearby_linedef(px, py, px + dx, py + dy) do |linedef| v1 = @map.vertices[linedef.v1] v2 = @map.vertices[linedef.v2] next unless Geometry.line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, px + dx, py + dy, PLAYER_RADIUS) next unless linedef_blocks?(linedef, px, py, px + dx, py + dy) || crosses_blocking_linedef?(px, py, px + dx, py + dy, linedef) dist = Geometry.point_to_segment_distance(px, py, v1.x, v1.y, v2.x, v2.y) if dist < best_dist best_dist = dist best_wall = linedef end end return nil unless best_wall v1 = @map.vertices[best_wall.v1] v2 = @map.vertices[best_wall.v2] wall_dx = (v2.x - v1.x).to_f wall_dy = (v2.y - v1.y).to_f wall_len = Math.sqrt((wall_dx * wall_dx) + (wall_dy * wall_dy)) return nil if wall_len == 0 wall_dx /= wall_len wall_dy /= wall_len dot = (dx * wall_dx) + (dy * wall_dy) [dot * wall_dx, dot * wall_dy] end |
#eye_z ⇒ Object
Eye position used by the renderer: feet + viewheight + view-bob. Returns nil before the first settle_at.
83 84 85 86 87 88 89 90 91 |
# File 'lib/doom/game/player_physics.rb', line 83 def eye_z return nil unless @floor_z if @player_state @floor_z + @player_state.viewheight + @player_state.view_bob_offset else @floor_z + PlayerState::VIEWHEIGHT end end |
#move(player, dx, dy) ⇒ Object
Move a player by (dx, dy) with collision and wall sliding, settling it onto the floor wherever it ends up.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/doom/game/player_physics.rb', line 178 def move(player, dx, dy) old_x = player.x old_y = player.y new_x = old_x + dx new_y = old_y + dy if valid_move?(old_x, old_y, new_x, new_y) player.move_to(new_x, new_y) settle(player, new_x, new_y) return end # Wall sliding: project movement along the blocking wall , = (old_x, old_y, dx, dy) if && ( != 0.0 || != 0.0) sx = old_x + sy = old_y + if valid_move?(old_x, old_y, sx, sy) player.move_to(sx, sy) settle(player, sx, sy) scale = [dx.abs, dy.abs].max.nonzero? || 1 player.momx = / scale * player.momx.abs player.momy = / scale * player.momy.abs return end end # Fallback: try axis-aligned sliding if dx != 0.0 && valid_move?(old_x, old_y, new_x, old_y) player.move_to(new_x, old_y) settle(player, new_x, old_y) player.momy = 0.0 elsif dy != 0.0 && valid_move?(old_x, old_y, old_x, new_y) player.move_to(old_x, new_y) settle(player, old_x, new_y) player.momx = 0.0 else player.momx = 0.0 player.momy = 0.0 end end |
#reset ⇒ Object
76 77 78 79 |
# File 'lib/doom/game/player_physics.rb', line 76 def reset @floor_z = nil @momz = 0.0 end |
#run_tic(player, cmd) ⇒ Object
Advance one player by one tic from its ticcmd: turn, thrust, friction, then move with wall sliding. This is the whole horizontal simulation, and it is a pure function of (player, cmd, map) -- no wall clock, no frame rate, no Kernel#rand. That is what makes lockstep possible.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/doom/game/player_physics.rb', line 147 def run_tic(player, cmd) player.turn(cmd.angleturn) if cmd.angleturn != 0.0 if cmd.forwardmove != 0.0 thrust = cmd.forwardmove * THRUST_PER_TIC player.momx += player.cos_angle * thrust player.momy += player.sin_angle * thrust end if cmd.sidemove != 0.0 thrust = cmd.sidemove * THRUST_PER_TIC * SIDE_THRUST_SCALE # Strafe right is 90 degrees clockwise from facing. player.momx += player.sin_angle * thrust player.momy -= player.cos_angle * thrust end if !cmd.moving? && player.momx.abs < STOPSPEED && player.momy.abs < STOPSPEED player.momx = 0.0 player.momy = 0.0 else player.momx *= FRICTION player.momy *= FRICTION end return unless player.momx.abs > STOPSPEED || player.momy.abs > STOPSPEED move(player, player.momx / TICRATE, player.momy / TICRATE) end |
#settle(player, x, y) ⇒ Object
Settle onto the floor and push the resulting eye height to the player.
221 222 223 224 225 |
# File 'lib/doom/game/player_physics.rb', line 221 def settle(player, x, y) settle_at(x, y) z = eye_z player.z = z if z end |
#settle_at(x, y) ⇒ Object
Bring the player onto the floor at (x, y) after a horizontal move. Snaps up for step-up (gated to <= MAX_STEP_UP by valid_move?), leaves
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/doom/game/player_physics.rb', line 96 def settle_at(x, y) sector = @map.sector_at(x, y) return unless sector new_floor = sector.floor_height if @player_state @floor_z ||= new_floor if new_floor > @floor_z step = new_floor - @floor_z @player_state.notify_step(step) if step.abs <= MAX_STEP_UP @floor_z = new_floor @momz = 0.0 end else @floor_z = new_floor end end |
#step(x, y) ⇒ Object
Per-tic vertical movement. Mutates @floor_z and @momz; calls player_state.notify_step / apply_fall_impact as needed.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/doom/game/player_physics.rb', line 117 def step(x, y) return unless @player_state && @floor_z sector = @map.sector_at(x, y) return unless sector ground = sector.floor_height if @floor_z > ground @momz = (@momz == 0.0 ? INITIAL_FALL_MOMZ : @momz - GRAVITY) @floor_z += @momz if @floor_z <= ground impact_momz = @momz @floor_z = ground @momz = 0.0 @player_state.apply_fall_impact(impact_momz) if impact_momz < FALL_IMPACT_THRESHOLD end elsif @floor_z < ground # Floor rose under player (lift, raising sector) step_amount = ground - @floor_z @player_state.notify_step(step_amount) if step_amount.abs <= MAX_STEP_UP @floor_z = ground @momz = 0.0 end end |
#valid_move?(old_x, old_y, new_x, new_y) ⇒ Boolean
True if the player can occupy (new_x, new_y). Step-up is capped at MAX_STEP_UP from the player's current floor; step-down is unlimited.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/doom/game/player_physics.rb', line 265 def valid_move?(old_x, old_y, new_x, new_y) sector = @map.sector_at(new_x, new_y) return false unless sector current_floor = @floor_z || sector.floor_height return false if sector.floor_height - current_floor > MAX_STEP_UP each_nearby_linedef(old_x, old_y, new_x, new_y) do |linedef| return false if linedef_blocks?(linedef, old_x, old_y, new_x, new_y) return false if crosses_blocking_linedef?(old_x, old_y, new_x, new_y, linedef) end return false if collides_with_solid_thing?(new_x, new_y) true end |