Class: Quake::Game::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/quake/game/engine.rb

Overview

Encapsulates the entire game state and per-frame loop, allowing both interactive (bin/quake) and scripted (bin/quake-debug) execution.

Defined Under Namespace

Classes: DynamicLight, Projectile, TemporaryBeam, TemporaryBeamSegment, TemporaryDynamicLight, WeaponSound

Constant Summary collapse

POWERUP_EXPIRING_EVENTS =
{
  ring: :ring_expiring,
  pentagram: :pentagram_expiring,
  biosuit: :biosuit_expiring,
  quad: :quad_expiring
}.freeze
POWERUP_EXPIRING_MESSAGES =

CheckPowerups sprints (client.qc) shown when a powerup has less than three seconds left.

{
  ring: "Ring of Shadows magic is fading",
  pentagram: "Protection is almost burned out",
  biosuit: "Air supply in Biosuit expiring",
  quad: "Quad Damage is wearing off"
}.freeze
CENTERPRINT_TIME =

scr_centertime (screen.c) and con_notifytime (console.c) defaults.

2.0
CON_NOTIFYTIME =
3.0
MAX_NOTIFY_LINES =

NUM_CON_TIMES notify lines shown at once (console.c Con_DrawNotify).

4
START_ITEM_CLASSNAMES =
%w[
  item_health item_armor1 item_armor2 item_armorInv
  weapon_supershotgun weapon_nailgun weapon_supernailgun
  weapon_grenadelauncher weapon_rocketlauncher weapon_lightning
  item_shells item_spikes item_rockets item_cells item_weapon
  item_key1 item_key2 item_sigil
  item_artifact_invulnerability item_artifact_envirosuit
  item_artifact_invisibility item_artifact_super_damage
].freeze
ITEM_WIDE_BOUNDS_CLASSNAMES =
%w[
  item_health item_shells item_spikes item_rockets item_cells item_weapon
].freeze
ITEM_TALL_BOUNDS_CLASSNAMES =
%w[
  item_armor1 item_armor2 item_armorInv
  weapon_supershotgun weapon_nailgun weapon_supernailgun
  weapon_grenadelauncher weapon_rocketlauncher weapon_lightning
].freeze
QUAKEWORLD_REMOVED_MONSTER_CLASSNAMES =
%w[
  monster_ogre monster_demon1 monster_shambler monster_knight
  monster_army monster_wizard monster_dog monster_zombie
  monster_boss monster_tarbaby monster_hell_knight monster_fish
  monster_shalrath monster_enforcer monster_oldone
].freeze
PLAYER_DEATH_FRAMES =
{
  axe: (41..49).to_a,
  a: (50..60).to_a,
  b: (61..69).to_a,
  c: (70..84).to_a,
  d: (85..93).to_a,
  e: (94..102).to_a
}.freeze
PLAYER_DEATHA11_FRAME =
60
PLAYER_PAIN_FRAMES =
{
  axe: (29..34).to_a,
  normal: (35..40).to_a
}.freeze
PLAYER_STAND_FRAMES =
{
  axe: (17..28).to_a,
  normal: (12..16).to_a
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pak_path:, window: nil, window_visible: true, window_width: nil, window_height: nil, enable_sound: true, enable_render: true, registered: false) ⇒ Engine

Returns a new instance of Engine.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
142
143
144
145
146
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
175
# File 'lib/quake/game/engine.rb', line 90

def initialize(pak_path:, window: nil, window_visible: true,
               window_width: nil, window_height: nil,
               enable_sound: true, enable_render: true,
               registered: false)
  @enable_render = enable_render
  @registered = registered

  # PAK + assets
  @pak = Pak::Reader.new(pak_path)
  @palette = Palette.new(@pak.read("gfx/palette.lmp"))

  wad_data = @pak.read("gfx.wad")
  @wad = Wad::Reader.new(wad_data) if wad_data

  # Sound (optional)
  if enable_sound
    @sound_mixer = Sound::Mixer.new(@pak)
    @sound_mixer.open
  end
  @sound_events = Sound::Events.new(@sound_mixer)

  # Window
  if window
    @window = window
    @owns_window = false
  else
    opts = { visible: window_visible }
    opts[:width] = window_width if window_width
    opts[:height] = window_height if window_height
    @window = Window.new(**opts)
    @owns_window = true
  end

  @keys = {}
  @attack_pressed = false
  @attack_finished = 0.0
  @pending_changelevel = nil
  @pending_changelevel_time = nil
  @samelevel = 0
  @deathmatch = 0
  @teamplay = 0
  @rj = 1.0
  @axe = 0
  @temp1 = 0
  @timelimit = 0
  @fraglimit = 0
  @current_map = nil
  @killed_monsters = 0
  @total_monsters = 0
  @level_name = nil
  @intermission_running = false
  @intermission_start_time = nil
  @gravity = GRAVITY
  @projectiles = []
  @temporary_dynamic_lights = []
  @temporary_beams = []
  @environment_damage_time = 0.0
  @air_finished = 12.0
  @drown_damage = 2.0
  @drown_pain_finished = 0.0
  @pain_sound_finished = 0.0
  @invincible_sound_finished = 0.0
  @swim_sound_finished = 0.0
  @quad_attack_sound_finished = 0.0
  @ring_idle_sound_finished = 0.0
  @powerup_warning_finished = {}
  @bonus_cshift_percent = 0.0
  @damage_cshift = Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0.0)
  @damage_kick_time = 0.0
  @damage_kick_roll = 0.0
  @damage_kick_pitch = 0.0
  @weapon_punch_angle = Math::Vec3::ORIGIN
  @view_oldz = 0.0
  @view_frame_dt = 0.0
  @view_idle_scale = 0.0
  @view_idealpitch = 0.0
  @view_pitch_velocity = 0.0
  @view_pitch_nodrift = true
  @view_pitch_driftmove = 0.0
  @view_pitch_laststop = 0.0
  @jump_flag = 0.0
  @game_time = 0.0
  @notify_lines = []
  @centerprint = nil
  @centerprint_expire = nil
end

Instance Attribute Details

#attack_pressed=(value) ⇒ Object (writeonly)

Sets the attribute attack_pressed

Parameters:

  • value

    the value to set the attribute attack_pressed to.



88
89
90
# File 'lib/quake/game/engine.rb', line 88

def attack_pressed=(value)
  @attack_pressed = value
end

#axeObject

Returns the value of attribute axe.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def axe
  @axe
end

#brush_gameObject (readonly)

Returns the value of attribute brush_game.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def brush_game
  @brush_game
end

#cameraObject (readonly)

Returns the value of attribute camera.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def camera
  @camera
end

#deathmatchObject

Returns the value of attribute deathmatch.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def deathmatch
  @deathmatch
end

#dynamic_lightsObject (readonly)

Returns the value of attribute dynamic_lights.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def dynamic_lights
  @dynamic_lights
end

#entitiesObject (readonly)

Returns the value of attribute entities.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def entities
  @entities
end

#fraglimitObject

Returns the value of attribute fraglimit.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def fraglimit
  @fraglimit
end

#game_timeObject

Returns the value of attribute game_time.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def game_time
  @game_time
end

#hudObject (readonly)

Returns the value of attribute hud.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def hud
  @hud
end

#item_pickupsObject (readonly)

Returns the value of attribute item_pickups.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def item_pickups
  @item_pickups
end

#levelObject (readonly)

Returns the value of attribute level.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def level
  @level
end

#pakObject (readonly)

Returns the value of attribute pak.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def pak
  @pak
end

#paletteObject (readonly)

Returns the value of attribute palette.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def palette
  @palette
end

#particlesObject (readonly)

Returns the value of attribute particles.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def particles
  @particles
end

#playerObject (readonly)

Returns the value of attribute player.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def player
  @player
end

#player_stateObject (readonly)

Returns the value of attribute player_state.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def player_state
  @player_state
end

#rjObject

Returns the value of attribute rj.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def rj
  @rj
end

#samelevelObject

Returns the value of attribute samelevel.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def samelevel
  @samelevel
end

#sound_eventsObject (readonly)

Returns the value of attribute sound_events.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def sound_events
  @sound_events
end

#teamplayObject

Returns the value of attribute teamplay.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def teamplay
  @teamplay
end

#temp1Object

Returns the value of attribute temp1.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def temp1
  @temp1
end

#timelimitObject

Returns the value of attribute timelimit.



86
87
88
# File 'lib/quake/game/engine.rb', line 86

def timelimit
  @timelimit
end

#viewmodelObject (readonly)

Returns the value of attribute viewmodel.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def viewmodel
  @viewmodel
end

#wadObject (readonly)

Returns the value of attribute wad.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def wad
  @wad
end

#windowObject (readonly)

Returns the value of attribute window.



82
83
84
# File 'lib/quake/game/engine.rb', line 82

def window
  @window
end

Instance Method Details

#active_centerprintObject



430
431
432
433
434
435
# File 'lib/quake/game/engine.rb', line 430

def active_centerprint
  return nil unless @centerprint
  return nil if @centerprint_expire.to_f <= @game_time

  @centerprint
end

#active_notify_linesObject



437
438
439
440
# File 'lib/quake/game/engine.rb', line 437

def active_notify_lines
  (@notify_lines || []).select { |line| line[:expire] > @game_time }
                       .map { |line| line[:text] }
end

#adjust_viewsize(delta) ⇒ Object

viewsize like Quake's +/- keys: 120 hides the bar, 110 shows the status bar only (default), 100 adds the inventory bar. Sub-100 sizes (shrunk 3D view) are not supported.



445
446
447
448
449
# File 'lib/quake/game/engine.rb', line 445

def adjust_viewsize(delta)
  @viewsize = ((@viewsize || DEFAULT_VIEWSIZE) + delta).clamp(100, 120)
  puts "viewsize #{@viewsize}"
  @viewsize
end

#clear_keysObject



473
474
475
# File 'lib/quake/game/engine.rb', line 473

def clear_keys
  @keys = {}
end

#dump_stateObject



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/quake/game/engine.rb', line 501

def dump_state
  {
    time: @game_time,
    map: @current_map,
    player: {
      position: vec_to_a(@player.position),
      velocity: vec_to_a(@player.velocity),
      yaw: @player.yaw,
      pitch: @player.pitch,
      on_ground: @player.on_ground,
      water_level: @player.water_level,
      noclip: @player.noclip
    },
    stats: {
      health: @player_state.health,
      armor: @player_state.armor,
      armor_type: @player_state.armor_type,
      current_weapon: @player_state.current_weapon,
      ammo: @player_state.ammo.dup,
      attack_finished: @attack_finished
    },
    brush_entities: brush_entity_summaries,
    particles: @particles ? @particles.particle_count : 0
  }
end

#hud_statsObject

Level statistics for the HUD (Quake's cl.stats STAT_MONSTERS / STAT_SECRETS etc. plus intermission state, sbar.c).



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/quake/game/engine.rb', line 388

def hud_stats
  {
    monsters: @killed_monsters.to_i,
    total_monsters: @total_monsters.to_i,
    secrets: @brush_game&.found_secrets.to_i,
    total_secrets: @brush_game&.total_secrets.to_i,
    time: @game_time,
    level_name: @level_name,
    intermission: !!@intermission_running,
    completed_time: @intermission_start_time,
    sb_lines: sb_lines,
    centerprint: active_centerprint,
    notify_lines: active_notify_lines
  }
end

#keysObject



477
478
479
# File 'lib/quake/game/engine.rb', line 477

def keys
  @keys
end

#load_map(map_name) ⇒ Object

Load a BSP map and (re)build all per-level state.



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
219
220
221
222
223
224
225
226
227
228
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
# File 'lib/quake/game/engine.rb', line 178

def load_map(map_name)
  bsp_data = @pak.read(map_name)
  raise "Map not found: #{map_name}" unless bsp_data

  @level = Bsp::Reader.new(bsp_data).parse
  @current_map = map_name
  configure_world_gravity(map_name)
  @entities = EntityParser.parse(@level.entities)
  @target_map = EntityParser.build_target_map(@entities)
  initialize_body_queue
  @projectiles = []
  @temporary_dynamic_lights = []
  @temporary_beams = []
  @environment_damage_time = 0.0
  @air_finished = @game_time + 12.0
  @drown_damage = 2.0
  @drown_pain_finished = 0.0
  @player_in_water = false
  @bonus_cshift_percent = 0.0
  @damage_cshift = Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0.0)
  @damage_kick_time = 0.0
  @damage_kick_roll = 0.0
  @damage_kick_pitch = 0.0
  @weapon_punch_angle = Math::Vec3::ORIGIN
  @view_oldz = 0.0
  @view_frame_dt = 0.0
  @view_idle_scale = 0.0
  @view_idealpitch = 0.0
  @view_pitch_velocity = 0.0
  @view_pitch_nodrift = true
  @view_pitch_driftmove = 0.0
  @view_pitch_laststop = 0.0
  @attack_finished = @game_time
  @jump_flag = 0.0
  @notify_lines = []
  @centerprint = nil
  @centerprint_expire = nil
  setup_misc_entities
  setup_ambient_sounds

  # Level stats for the HUD scoreboard/intermission (Quake's
  # total_monsters / killed_monsters, sbar.c STAT_* values).
  # Monsters count only entities that actually spawned (the
  # QuakeWorld-style monster stubs remove themselves at spawn).
  @killed_monsters = 0
  @total_monsters = @entities.count do |ent|
    ent.classname.to_s.start_with?("monster_") && !ent.removed?
  end
  message = world_info_value("message")
  @level_name = message && !message.empty? ? message : File.basename(map_name, ".bsp")
  @intermission_start_time = nil

  set_player_spawn_point(select_spawn_point)

  # Load MDL models referenced by entities
  @mdl_cache ||= {}
  @entities.each { |ent| load_mdl_model(ent["model"]) unless ent.removed? }
  TEMP_ENTITY_MODELS.each { |model_path| load_mdl_model(model_path) }

  # Persistent player state across level loads
  @player_state ||= PlayerState.new
  @player_state.reset_spawn_runtime_fields
  apply_start_map_parms
  @item_pickups = ItemPickups.new(@entities, deathmatch: deathmatch)
  @brush_game = BrushEntities.new(
    @entities, @level, @target_map,
    sound_events: @sound_events,
    registered: @registered,
    serverflags: @player_state.serverflags,
    worldtype: current_worldtype
  )

  # Player + camera (reset position to start)
  @player = Physics::Player.new(position: player_spawn_origin, yaw: @player_yaw)
  @player.gravity = current_gravity
  set_player_spawn_entity_state
  apply_put_client_in_server_spawn_effects
  @camera = Camera.new(position: player_eye_position, yaw: @player_yaw)

  build_renderers if @enable_render
end

#renderObject



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/quake/game/engine.rb', line 344

def render
  build_dynamic_lights
  @lightmap&.update(@game_time, lightstyles: current_lightstyles, dynamic_lights: @dynamic_lights)
  @world_renderer.render(@camera, @window.aspect_ratio, time: @game_time)
  @brush_renderer&.render(@entities, view_origin: @camera.position, time: @game_time,
                          frustum: @camera.frustum_planes(@window.aspect_ratio))

  @entities.each do |ent|
    next if ent.removed?
    next if @item_pickups.picked_up?(ent)

    emit_entity_effect_particles(ent)
    model_path = ent["model"]
    next unless model_path&.end_with?(".mdl")

    gl_model = @mdl_renderers[model_path]
    next unless gl_model
    ambient_light, shade_light = alias_entity_lighting(ent.position)

    gl_model.render(
      frame_index: ent.frame,
      lerp: 0.0,
      position: ent.position,
      yaw: alias_entity_yaw(ent, model_path),
      pitch: -ent.angles.x,
      roll: ent.angles.z,
      skin_index: ent.skin,
      time: @game_time + entity_sync_base(ent, gl_model),
      ambient_light: ambient_light,
      shade_light: shade_light
    )
  end

  render_projectiles
  render_temp_beams
  @particles&.render(camera: @camera)
  render_viewmodel
  @world_renderer.render_water
  @view_blend&.render(current_view_blend)
  @hud&.render(@player_state, time: @game_time, stats: hud_stats)
end

#sb_linesObject



451
452
453
454
455
456
457
# File 'lib/quake/game/engine.rb', line 451

def sb_lines
  size = @viewsize || DEFAULT_VIEWSIZE
  return 0 if size >= 120
  return 24 if size >= 110

  40
end

#screenshot(filename) ⇒ Object

---------------------- debug output -----------------------



497
498
499
# File 'lib/quake/game/engine.rb', line 497

def screenshot(filename)
  Debug::Screenshot.save(filename, @window.width, @window.height)
end

#set_attack(pressed) ⇒ Object



481
482
483
# File 'lib/quake/game/engine.rb', line 481

def set_attack(pressed)
  @attack_pressed = pressed
end

#set_key(scancode, pressed) ⇒ Object

---------------------- input control ----------------------



465
466
467
# File 'lib/quake/game/engine.rb', line 465

def set_key(scancode, pressed)
  @keys[scancode] = pressed
end

#set_keys(keys_hash) ⇒ Object



469
470
471
# File 'lib/quake/game/engine.rb', line 469

def set_keys(keys_hash)
  @keys = keys_hash.dup
end

#shutdownObject



527
528
529
530
531
532
533
# File 'lib/quake/game/engine.rb', line 527

def shutdown
  @sound_mixer&.close
  if @owns_window
    @window&.close
  end
  @pak.close
end

#sprint(text) ⇒ Object

QuakeC sprint(): queue a console notify line (top-left text, console.c Con_Print + Con_DrawNotify). Multi-line messages become one notify line each; only the last MAX_NOTIFY_LINES are kept.



407
408
409
410
411
412
413
414
415
# File 'lib/quake/game/engine.rb', line 407

def sprint(text)
  @notify_lines ||= []
  text.to_s.split("\n").each do |line|
    next if line.empty?

    @notify_lines << { text: line, expire: @game_time + CON_NOTIFYTIME }
    @notify_lines.shift while @notify_lines.length > MAX_NOTIFY_LINES
  end
end

#swap_buffersObject



459
460
461
# File 'lib/quake/game/engine.rb', line 459

def swap_buffers
  @window.swap
end

#teleport(x, y, z, yaw: nil, pitch: nil) ⇒ Object

---------------------- player control ---------------------



487
488
489
490
491
492
493
# File 'lib/quake/game/engine.rb', line 487

def teleport(x, y, z, yaw: nil, pitch: nil)
  @player.position = Math::Vec3.new(x.to_f, y.to_f, z.to_f)
  @player.velocity = Math::Vec3::ORIGIN
  @player.instance_variable_set(:@yaw, yaw.to_f) if yaw
  @player.instance_variable_set(:@pitch, pitch.to_f) if pitch
  sync_camera
end

#tick(dt) ⇒ Object

Update + render one frame.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/quake/game/engine.rb', line 261

def tick(dt)
  @game_time += dt

  if @intermission_running
    intermission_think
    sync_camera
    return unless @enable_render

    render
    return
  end

  # Brush entities first (so collision uses current positions)
  if @brush_game
    @brush_game.update(dt, @player.position, player_state: @player_state)
    @brush_game.check_triggers(@player.position, player_forward: @player.forward_flat) do |trigger|
      handle_trigger(trigger)
    end

    snapped = @brush_game.snap_to_platform(@player.position)
    if snapped
      @player.position = snapped
      @player.on_ground = true
      @player.velocity = Math::Vec3.new(@player.velocity.x, @player.velocity.y, 0.0)
    end
  end

  solid_brush = @entities.select(&:brush_entity?)
  update_player_control(dt, solid_brush)
  @player_state.update_megahealth(@game_time) unless @item_pickups&.owns_megahealth_rot_for?(@player_state)
  update_start_items
  if @item_pickups
    @item_pickups.update(@game_time, player_state: @player_state).each do |evt|
      @sound_events&.on_pickup(evt)
    end
  end

  # Item pickups
  events = @item_pickups.check_pickups(
    @player.position, @player_state, game_time: @game_time, water_level: @player.water_level
  )
  events.each do |evt|
    process_item_pickup_feedback(evt)
    process_item_pickup_targets(evt)
  end

  if player_postthink_active?
    check_powerup_warnings
    refresh_biosuit_air
    update_player_powerup_visual_state
    update_player_powerups
    check_no_ammo_weapon_switch
    fire_weapon if @attack_pressed && @game_time >= @attack_finished
  end
  update_triggered_misc_entities
  update_projectiles(dt)
  update_temporary_entities
  update_player_pain_animation
  update_player_death_animation
  check_rules if player_postthink_active?
  if @pending_changelevel && @game_time + 0.000001 >= @pending_changelevel_time.to_f
    run_pending_changelevel
  end

  # Viewmodel bob (before camera so bob is applied to camera too)
  if @viewmodel
    speed = ::Math.sqrt(@player.velocity.x**2 + @player.velocity.y**2)
    @viewmodel.update(dt, speed)
  end

  @view_frame_dt = dt
  sync_camera
  update_screen_messages

  return unless @enable_render

  @world_renderer.update(dt) if @world_renderer.respond_to?(:update)
  @particles&.update(dt)
  update_view_cshifts(dt)

  render
end

#update_screen_messagesObject

Latch centerprints written to the player (QuakeC centerprint()) and drop expired on-screen text. Runs once per tick.



419
420
421
422
423
424
425
426
427
428
# File 'lib/quake/game/engine.rb', line 419

def update_screen_messages
  message = @player_state&.centerprint
  if message && !message.to_s.empty?
    @centerprint = message
    @centerprint_expire = @game_time + CENTERPRINT_TIME
    @player_state.centerprint = nil
  end
  @centerprint = nil if @centerprint && @centerprint_expire.to_f <= @game_time
  @notify_lines&.reject! { |line| line[:expire] <= @game_time }
end