Class: Doom::Game::PlayerPhysics

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

Parameters:

  • value

    the value to set the attribute combat to.



64
65
66
# File 'lib/doom/game/player_physics.rb', line 64

def combat=(value)
  @combat = value
end

#floor_zObject (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

Parameters:

  • value

    the value to set the attribute item_pickup to.



64
65
66
# File 'lib/doom/game/player_physics.rb', line 64

def item_pickup=(value)
  @item_pickup = value
end

#momzObject (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

Parameters:

  • value

    the value to set the attribute skill_hidden to.



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 compute_slide(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_zObject

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
  slide_x, slide_y = compute_slide(old_x, old_y, dx, dy)
  if slide_x && (slide_x != 0.0 || slide_y != 0.0)
    sx = old_x + slide_x
    sy = old_y + slide_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 = slide_x / scale * player.momx.abs
      player.momy = slide_y / 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

#resetObject



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.

Returns:

  • (Boolean)


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