Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created May 7, 2026 13:01
Show Gist options
  • Select an option

  • Save Hammer2900/7140fe26365f4ca2b10cef516e107498 to your computer and use it in GitHub Desktop.

Select an option

Save Hammer2900/7140fe26365f4ca2b10cef516e107498 to your computer and use it in GitHub Desktop.
интерполяция рендера
import pygame
import sys
import math
import random
import time
from collections import deque
pygame.init()
W, H = 1200, 780
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption('Game Loop Timestep — все 5 режимов')
clock = pygame.time.Clock()
# ── Цвета ──────────────────────────────────────────────────────────────────
BG = (18, 18, 16)
PANEL_BG = (26, 26, 24)
CARD_BG = (34, 34, 32)
BORDER = (60, 60, 55)
WHITE = (220, 218, 210)
MUTED = (120, 118, 110)
ACCENT = [
(216, 90, 48), # 0 coral – Variable
(55, 138, 221), # 1 blue – Fixed+acc
(29, 158, 117), # 2 teal – Semi-Fixed
(186, 117, 23), # 3 amber – Fixed+Interp
(83, 74, 183), # 4 purple – Lockstep
]
ACCENT_DIM = [(max(0, r - 80), max(0, g - 80), max(0, b - 80)) for r, g, b in ACCENT]
WARN = (220, 160, 40)
GRID_COL = (40, 40, 36)
# ── Шрифты ─────────────────────────────────────────────────────────────────
try:
F_SM = pygame.font.SysFont('DejaVuSans,Arial,sans-serif', 13)
F_MD = pygame.font.SysFont('DejaVuSans,Arial,sans-serif', 15)
F_LG = pygame.font.SysFont('DejaVuSans,Arial,sans-serif', 18, bold=True)
F_XL = pygame.font.SysFont('DejaVuSans,Arial,sans-serif', 22, bold=True)
F_MONO = pygame.font.SysFont('DejaVuSansMono,Courier,monospace', 13)
except:
F_SM = F_MD = F_LG = F_XL = F_MONO = pygame.font.Font(None, 16)
# ── Helpers ─────────────────────────────────────────────────────────────────
def draw_text(surf, text, font, color, pos, anchor='topleft'):
s = font.render(str(text), True, color)
r = s.get_rect(**{anchor: pos})
surf.blit(s, r)
return r
def lerp(a, b, t):
return a + (b - a) * t
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def draw_rounded_rect(surf, color, rect, radius=8, border=0, border_color=None):
pygame.draw.rect(surf, color, rect, border_radius=radius)
if border and border_color:
pygame.draw.rect(surf, border_color, rect, border, border_radius=radius)
# ── Slider ──────────────────────────────────────────────────────────────────
class Slider:
def __init__(self, x, y, w, lo, hi, val, label, fmt='{:.0f}', unit=''):
self.rect = pygame.Rect(x, y, w, 6)
self.lo, self.hi = lo, hi
self.val = val
self.label = label
self.fmt = fmt
self.unit = unit
self.dragging = False
self.handle_r = 9
@property
def norm(self):
return (self.val - self.lo) / (self.hi - self.lo)
def handle_pos(self):
return (self.rect.x + int(self.norm * self.rect.w), self.rect.centery)
def draw(self, surf):
# track bg
pygame.draw.rect(surf, BORDER, self.rect, border_radius=3)
# filled part
filled = pygame.Rect(self.rect.x, self.rect.y, int(self.norm * self.rect.w), self.rect.h)
pygame.draw.rect(surf, (80, 140, 200), filled, border_radius=3)
# handle
hx, hy = self.handle_pos()
pygame.draw.circle(surf, WHITE, (hx, hy), self.handle_r)
pygame.draw.circle(surf, (80, 140, 200), (hx, hy), self.handle_r, 2)
# label left
draw_text(surf, self.label, F_SM, MUTED, (self.rect.x, self.rect.y - 18))
# value right
val_str = self.fmt.format(self.val) + self.unit
draw_text(surf, val_str, F_SM, WHITE, (self.rect.right, self.rect.y - 18), 'topright')
def handle_event(self, event):
hx, hy = self.handle_pos()
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if math.hypot(event.pos[0] - hx, event.pos[1] - hy) <= self.handle_r + 4:
self.dragging = True
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
self.dragging = False
elif event.type == pygame.MOUSEMOTION and self.dragging:
t = clamp((event.pos[0] - self.rect.x) / self.rect.w, 0, 1)
self.val = self.lo + t * (self.hi - self.lo)
# ── Graph (ring buffer of floats) ───────────────────────────────────────────
class Graph:
def __init__(self, x, y, w, h, capacity=200, color=WHITE, label=''):
self.rect = pygame.Rect(x, y, w, h)
self.buf = deque([0.0] * capacity, maxlen=capacity)
self.color = color
self.label = label
self.cap = capacity
def push(self, v):
self.buf.append(float(v))
def draw(self, surf, lo=None, hi=None, h_lines=None, unit=''):
pygame.draw.rect(surf, CARD_BG, self.rect, border_radius=6)
pygame.draw.rect(surf, BORDER, self.rect, 1, border_radius=6)
data = list(self.buf)
if lo is None:
lo = 0
if hi is None:
hi = max(max(data), 1)
span = hi - lo if hi != lo else 1
# grid lines
for gl in h_lines or []:
gy = self.rect.bottom - int((gl - lo) / span * self.rect.h)
if self.rect.top < gy < self.rect.bottom:
pygame.draw.line(surf, GRID_COL, (self.rect.left, gy), (self.rect.right, gy))
draw_text(surf, f'{gl:.0f}{unit}', F_SM, MUTED, (self.rect.left + 4, gy - 14))
# curve
pts = []
for i, v in enumerate(data):
x = self.rect.left + int(i / (self.cap - 1) * self.rect.w)
y = self.rect.bottom - int(clamp((v - lo) / span, 0, 1) * self.rect.h)
pts.append((x, y))
if len(pts) > 1:
pygame.draw.lines(surf, self.color, False, pts, 2)
# label + current value
draw_text(surf, self.label, F_SM, self.color, (self.rect.left + 6, self.rect.top + 6))
draw_text(surf, f'{data[-1]:.1f}{unit}', F_SM, WHITE, (self.rect.right - 6, self.rect.top + 6), 'topright')
# ── Ball physics state ────────────────────────────────────────────────────
GRAV = 900.0 # px/s²
BOUNCE = 0.72
BALL_R = 10
class Ball:
def __init__(self, x, y, vx=120, color=WHITE):
self.x = x
self.y = y
self.px = x
self.py = y
self.vx = vx
self.vy = 0
self.color = color
self.trail = deque(maxlen=18)
def step(self, dt, floor_y, grav=GRAV):
self.px, self.py = self.x, self.y
self.vy += grav * dt
self.x += self.vx * dt
self.y += self.vy * dt
if self.y >= floor_y:
self.y = floor_y
self.vy *= -BOUNCE
if self.x <= BALL_R:
self.x = BALL_R
self.vx *= -1
if self.x >= self.xlim - BALL_R:
self.x = self.xlim - BALL_R
self.vx *= -1
self.trail.append((self.x, self.y))
def draw(self, surf, ox, oy, rx=None, ry=None, scale=1.0):
# trail
trail = list(self.trail)
for i, (tx, ty) in enumerate(trail):
a = int(80 * i / len(trail))
r = max(2, int(BALL_R * 0.6 * scale * i / len(trail)))
c = (*self.color, a)
s = pygame.Surface((r * 2, r * 2), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (r, r), r)
surf.blit(s, (ox + int(tx * scale) - r, oy + int(ty * scale) - r))
# ball
bx = ox + int(self.x * scale)
by = oy + int(self.y * scale)
pygame.draw.circle(surf, self.color, (bx, by), int(BALL_R * scale))
# interp ball
if rx is not None and ry is not None:
ix = ox + int(rx * scale)
iy = oy + int(ry * scale)
s = pygame.Surface((BALL_R * 2, BALL_R * 2), pygame.SRCALPHA)
pygame.draw.circle(s, (*ACCENT[3], 200), (BALL_R, BALL_R), BALL_R)
surf.blit(s, (ix - BALL_R, iy - BALL_R))
pygame.draw.line(surf, (*ACCENT[3], 120), (bx, by), (ix, iy), 1)
# ══════════════════════════════════════════════════════════════════════════════
# Панели (по одной на режим)
# ══════════════════════════════════════════════════════════════════════════════
SIM_W = 320 # ширина зоны симуляции
GRAPH_W = 300 # ширина каждого графика
class BasePanel:
def __init__(self, idx, title, desc):
self.idx = idx
self.title = title
self.desc = desc
self.sliders: list[Slider] = []
self.graphs: list[Graph] = []
self.color = ACCENT[idx]
def update(self, real_dt):
pass
def draw(self, surf, px, py, pw, ph):
pass
def handle_event(self, e):
for sl in self.sliders:
sl.handle_event(e)
# helpers
def draw_sim_box(self, surf, rx, ry, rw, rh):
pygame.draw.rect(surf, (22, 22, 20), (rx, ry, rw, rh), border_radius=6)
pygame.draw.rect(surf, BORDER, (rx, ry, rw, rh), 1, border_radius=6)
# floor
pygame.draw.line(surf, BORDER, (rx + 4, ry + rh - 14), (rx + rw - 4, ry + rh - 14))
def floor_y(self, box_h):
return box_h - 14 - BALL_R
# ─────────────────────────────────────────────────────────────────
# 0 · Variable Timestep
# ─────────────────────────────────────────────────────────────────
class PanelVariable(BasePanel):
def __init__(self):
super().__init__(0, 'Variable Timestep', 'dt = реальное время кадра. При лаге шарик делает огромный скачок.')
self.sl_lag = Slider(0, 0, 200, 0, 200, 0, 'Симуляция лага', unit=' мс')
self.sl_grav = Slider(0, 0, 200, 100, 2000, 900, 'Гравитация', unit=' px/s²')
self.sliders = [self.sl_lag, self.sl_grav]
self.g_dt = Graph(0, 0, GRAPH_W, 90, color=ACCENT[0], label='dt (мс)')
self.g_vy = Graph(0, 0, GRAPH_W, 90, color=(200, 160, 60), label='скорость Y')
self.graphs = [self.g_dt, self.g_vy]
self.ball = Ball(60, 60, vx=130, color=ACCENT[0])
self.ball.xlim = SIM_W - 8
self.dt_history = deque([0.0] * 80, maxlen=80)
def update(self, real_dt):
lag = self.sl_lag.val / 1000.0
grav = self.sl_grav.val
dt = real_dt + lag
self.ball.step(dt, self.floor_y(160), grav)
self.g_dt.push(dt * 1000)
self.g_vy.push(abs(self.ball.vy))
def draw(self, surf, px, py, pw, ph):
bx, by, bw, bh = px + 8, py + 8, SIM_W, 160
self.draw_sim_box(surf, bx, by, bw, bh)
self.ball.draw(surf, bx + 4, by + 4, scale=(bw - 8) / SIM_W)
lag = self.sl_lag.val
if lag > 0:
warn = F_SM.render(f'ЛАГ +{lag:.0f} мс → dt={lag + 16:.0f} мс', True, WARN)
surf.blit(warn, (bx + 6, by + 6))
# sliders
for i, sl in enumerate(self.sliders):
sl.rect.x = px + 8
sl.rect.y = py + 185 + i * 42
sl.rect.w = pw - 16
sl.draw(surf)
# graphs
gx = px + 8
gy = py + 270
self.g_dt.rect = pygame.Rect(gx, gy, pw // 2 - 12, 100)
self.g_dt.draw(surf, lo=0, hi=250, h_lines=[16, 50, 100, 200], unit=' мс')
self.g_vy.rect = pygame.Rect(gx + pw // 2, gy, pw // 2 - 8, 100)
self.g_vy.draw(surf, lo=0, unit=' px/s')
# ─────────────────────────────────────────────────────────────────
# 1 · Fixed + Accumulator
# ─────────────────────────────────────────────────────────────────
class PanelFixed(BasePanel):
def __init__(self):
super().__init__(1, 'Fixed Timestep + аккумулятор', 'physUpdate(fixedDt) пока accumulator >= fixedDt.')
self.sl_lag = Slider(0, 0, 200, 0, 300, 0, 'Лаг кадра', unit=' мс')
self.sl_fdt = Slider(0, 0, 200, 4, 64, 16, 'Fixed dt', unit=' мс')
self.sliders = [self.sl_lag, self.sl_fdt]
self.g_acc = Graph(0, 0, GRAPH_W, 90, color=ACCENT[1], label='accumulator (мс)')
self.g_ticks = Graph(0, 0, GRAPH_W, 90, color=(180, 220, 100), label='тиков/кадр')
self.graphs = [self.g_acc, self.g_ticks]
self.ball = Ball(60, 60, color=ACCENT[1])
self.ball.xlim = SIM_W - 8
self.acc = 0.0
self.ticks = 0
self.spiral = False
def update(self, real_dt):
lag = self.sl_lag.val / 1000.0
fdt = self.sl_fdt.val / 1000.0
frame = real_dt + lag
self.acc += frame
self.ticks = 0
while self.acc >= fdt and self.ticks < 30:
self.ball.step(fdt, self.floor_y(160))
self.acc -= fdt
self.ticks += 1
self.spiral = self.acc > fdt * 10
self.g_acc.push(self.acc * 1000)
self.g_ticks.push(self.ticks)
def draw(self, surf, px, py, pw, ph):
bx, by, bw, bh = px + 8, py + 8, SIM_W, 160
self.draw_sim_box(surf, bx, by, bw, bh)
self.ball.draw(surf, bx + 4, by + 4, scale=(bw - 8) / SIM_W)
if self.ticks > 1:
txt = F_SM.render(f'{self.ticks} тиков за 1 кадр', True, ACCENT[1])
surf.blit(txt, (bx + 6, by + 6))
if self.spiral:
pygame.draw.rect(surf, (60, 20, 10), (bx, by, bw, bh), border_radius=6)
warn = F_MD.render('СПИРАЛЬ СМЕРТИ', True, WARN)
surf.blit(warn, (bx + bw // 2 - warn.get_width() // 2, by + bh // 2 - 10))
for i, sl in enumerate(self.sliders):
sl.rect.x = px + 8
sl.rect.y = py + 185 + i * 42
sl.rect.w = pw - 16
sl.draw(surf)
gx = px + 8
gy = py + 270
self.g_acc.rect = pygame.Rect(gx, gy, pw // 2 - 12, 100)
self.g_acc.draw(surf, lo=0, hi=200, h_lines=[16, 50, 100], unit=' мс')
self.g_ticks.rect = pygame.Rect(gx + pw // 2, gy, pw // 2 - 8, 100)
self.g_ticks.draw(surf, lo=0, hi=12, h_lines=[1, 4, 8])
# ─────────────────────────────────────────────────────────────────
# 2 · Semi-Fixed
# ─────────────────────────────────────────────────────────────────
class PanelSemiFixed(BasePanel):
def __init__(self):
super().__init__(2, 'Semi-Fixed Timestep', 'frameTime = min(frameTime, MAX_CLAMP). Спираль невозможна.')
self.sl_lag = Slider(0, 0, 200, 0, 400, 0, 'Лаг кадра', unit=' мс')
self.sl_fdt = Slider(0, 0, 200, 4, 64, 16, 'Fixed dt', unit=' мс')
self.sl_clamp = Slider(0, 0, 200, 16, 250, 100, 'Max clamp', unit=' мс')
self.sliders = [self.sl_lag, self.sl_fdt, self.sl_clamp]
self.g_frame = Graph(0, 0, GRAPH_W, 90, color=ACCENT[2], label='frame clamped (мс)')
self.g_ticks = Graph(0, 0, GRAPH_W, 90, color=(100, 200, 200), label='тиков/кадр')
self.graphs = [self.g_frame, self.g_ticks]
self.ball = Ball(60, 60, color=ACCENT[2])
self.ball.xlim = SIM_W - 8
self.acc = 0.0
self.ticks = 0
self.clamped = False
self.frame_raw = 0.0
self.frame_clamped = 0.0
def update(self, real_dt):
lag = self.sl_lag.val / 1000.0
fdt = self.sl_fdt.val / 1000.0
clamp_v = self.sl_clamp.val / 1000.0
self.frame_raw = real_dt + lag
self.frame_clamped = min(self.frame_raw, clamp_v)
self.clamped = self.frame_raw > clamp_v
self.acc += self.frame_clamped
self.ticks = 0
while self.acc >= fdt and self.ticks < 30:
self.ball.step(fdt, self.floor_y(160))
self.acc -= fdt
self.ticks += 1
self.g_frame.push(self.frame_clamped * 1000)
self.g_ticks.push(self.ticks)
def draw(self, surf, px, py, pw, ph):
bx, by, bw, bh = px + 8, py + 8, SIM_W, 160
self.draw_sim_box(surf, bx, by, bw, bh)
self.ball.draw(surf, bx + 4, by + 4, scale=(bw - 8) / SIM_W)
if self.clamped:
msg = f'Clamp! {self.frame_raw * 1000:.0f} мс → {self.frame_clamped * 1000:.0f} мс'
surf.blit(F_SM.render(msg, True, ACCENT[2]), (bx + 6, by + 6))
for i, sl in enumerate(self.sliders):
sl.rect.x = px + 8
sl.rect.y = py + 185 + i * 42
sl.rect.w = pw - 16
sl.draw(surf)
gx = px + 8
gy = py + 315
self.g_frame.rect = pygame.Rect(gx, gy, pw // 2 - 12, 90)
self.g_frame.draw(surf, lo=0, hi=220, h_lines=[16, 50, 100], unit=' мс')
self.g_ticks.rect = pygame.Rect(gx + pw // 2, gy, pw // 2 - 8, 90)
self.g_ticks.draw(surf, lo=0, hi=12, h_lines=[1, 4, 8])
# ─────────────────────────────────────────────────────────────────
# 3 · Fixed + Interpolation
# ─────────────────────────────────────────────────────────────────
class PanelInterp(BasePanel):
def __init__(self):
super().__init__(
3,
'Fixed + интерполяция рендера',
'alpha = acc/fixedDt. Рендер lerp(prev, cur, alpha). Плавно при любом FPS.',
)
self.sl_lag = Slider(0, 0, 200, 0, 200, 0, 'Лаг кадра', unit=' мс')
self.sl_fdt = Slider(0, 0, 200, 8, 80, 20, 'Fixed dt', unit=' мс')
self.sliders = [self.sl_lag, self.sl_fdt]
self.g_alpha = Graph(0, 0, GRAPH_W, 90, color=ACCENT[3], label='alpha (0–1)')
self.g_gap = Graph(0, 0, GRAPH_W, 90, color=(200, 100, 60), label='|phys – render| px')
self.graphs = [self.g_alpha, self.g_gap]
self.ball = Ball(60, 60, color=ACCENT[1])
self.ball.xlim = SIM_W - 8
self.prev_x = 60.0
self.prev_y = 60.0
self.rx = 60.0
self.ry = 60.0
self.acc = 0.0
self.alpha = 0.0
def update(self, real_dt):
lag = self.sl_lag.val / 1000.0
fdt = self.sl_fdt.val / 1000.0
self.acc += real_dt + lag
while self.acc >= fdt:
self.prev_x, self.prev_y = self.ball.x, self.ball.y
self.ball.step(fdt, self.floor_y(160))
self.acc -= fdt
self.alpha = clamp(self.acc / fdt, 0, 1)
self.rx = lerp(self.prev_x, self.ball.x, self.alpha)
self.ry = lerp(self.prev_y, self.ball.y, self.alpha)
self.g_alpha.push(self.alpha)
self.g_gap.push(math.hypot(self.ball.x - self.rx, self.ball.y - self.ry))
def draw(self, surf, px, py, pw, ph):
bx, by, bw, bh = px + 8, py + 8, SIM_W, 160
self.draw_sim_box(surf, bx, by, bw, bh)
sc = (bw - 8) / SIM_W
# physics ball (blue)
self.ball.draw(surf, bx + 4, by + 4, rx=self.rx, ry=self.ry, scale=sc)
# labels
surf.blit(F_SM.render('синий = физика', True, ACCENT[1]), (bx + 6, by + 6))
surf.blit(F_SM.render(f'жёлтый = рендер (α={self.alpha:.2f})', True, ACCENT[3]), (bx + 6, by + 22))
for i, sl in enumerate(self.sliders):
sl.rect.x = px + 8
sl.rect.y = py + 185 + i * 42
sl.rect.w = pw - 16
sl.draw(surf)
gx = px + 8
gy = py + 270
self.g_alpha.rect = pygame.Rect(gx, gy, pw // 2 - 12, 100)
self.g_alpha.draw(surf, lo=0, hi=1, h_lines=[0.25, 0.5, 0.75])
self.g_gap.rect = pygame.Rect(gx + pw // 2, gy, pw // 2 - 8, 100)
self.g_gap.draw(surf, lo=0, unit=' px')
# ─────────────────────────────────────────────────────────────────
# 4 · Lockstep
# ─────────────────────────────────────────────────────────────────
class PanelLockstep(BasePanel):
def __init__(self):
super().__init__(
4, 'Lockstep (детерминированный)', 'Все клиенты тикают синхронно. Лаг одного — стоп для всех. Разрыв = 0.'
)
self.sl_lag = Slider(0, 0, 200, 0, 250, 0, 'Лаг клиента 2', unit=' мс')
self.sl_tick = Slider(0, 0, 200, 16, 100, 33, 'Tick rate', unit=' мс')
self.sliders = [self.sl_lag, self.sl_tick]
self.g_wait = Graph(0, 0, GRAPH_W, 90, color=WARN, label='ожидание (мс)')
self.g_tick = Graph(0, 0, GRAPH_W, 90, color=ACCENT[4], label='тик №')
self.graphs = [self.g_wait, self.g_tick]
self.b1 = Ball(50, 50, vx=110, color=ACCENT[4])
self.b2 = Ball(50, 50, vx=110, color=(160, 140, 240))
for b in (self.b1, self.b2):
b.xlim = SIM_W // 2 - 12
self.tick_n = 0
self.timer1 = 0.0
self.timer2 = 0.0
self.waiting = False
self.wait_ms = 0.0
self.lag_debt = 0.0
def update(self, real_dt):
tick_s = self.sl_tick.val / 1000.0
lag_ms = self.sl_lag.val
self.timer1 += real_dt
# client2 occasionally delayed
extra = 0.0
if lag_ms > 0 and random.random() < real_dt * (lag_ms / 800.0):
extra = lag_ms / 1000.0 * random.uniform(0.5, 1.5)
self.lag_debt = max(0, self.lag_debt - real_dt)
self.lag_debt += extra
effective2 = max(0, real_dt - self.lag_debt)
self.timer2 += effective2
can1 = self.timer1 >= tick_s
can2 = self.timer2 >= tick_s
self.waiting = can1 and not can2
self.wait_ms = max(0, (tick_s - self.timer2) * 1000) if self.waiting else 0
if can1 and can2:
self.b1.step(tick_s, self.floor_y(160))
self.b2.step(tick_s, self.floor_y(160))
self.timer1 -= tick_s
self.timer2 -= tick_s
self.tick_n += 1
self.g_wait.push(self.wait_ms)
self.g_tick.push(self.tick_n % 60)
def draw(self, surf, px, py, pw, ph):
half = (pw - 24) // 2
# client 1
bx1, by1 = px + 8, py + 8
pygame.draw.rect(surf, (22, 22, 20), (bx1, by1, half, 170), border_radius=6)
pygame.draw.rect(surf, BORDER, (bx1, by1, half, 170), 1, border_radius=6)
draw_text(surf, 'Клиент 1', F_SM, MUTED, (bx1 + 6, by1 + 6))
pygame.draw.line(surf, BORDER, (bx1 + 4, by1 + 156), (bx1 + half - 4, by1 + 156))
self.b1.draw(surf, bx1 + 4, by1 + 20, scale=(half - 8) / (SIM_W // 2))
# client 2
bx2, by2 = px + 16 + half, py + 8
c2_bg = (50, 30, 10) if self.waiting else (22, 22, 20)
pygame.draw.rect(surf, c2_bg, (bx2, by2, half, 170), border_radius=6)
pygame.draw.rect(surf, BORDER, (bx2, by2, half, 170), 1, border_radius=6)
lbl2 = 'Клиент 2 — ЖДЁМ' if self.waiting else 'Клиент 2'
col2 = WARN if self.waiting else MUTED
draw_text(surf, lbl2, F_SM, col2, (bx2 + 6, by2 + 6))
pygame.draw.line(surf, BORDER, (bx2 + 4, by2 + 156), (bx2 + half - 4, by2 + 156))
self.b2.draw(surf, bx2 + 4, by2 + 20, scale=(half - 8) / (SIM_W // 2))
# tick badge
badge = F_LG.render(f'тик {self.tick_n}', True, ACCENT[4])
surf.blit(badge, (px + pw // 2 - badge.get_width() // 2, py + 185))
for i, sl in enumerate(self.sliders):
sl.rect.x = px + 8
sl.rect.y = py + 215 + i * 42
sl.rect.w = pw - 16
sl.draw(surf)
gx = px + 8
gy = py + 305
self.g_wait.rect = pygame.Rect(gx, gy, pw // 2 - 12, 90)
self.g_wait.draw(surf, lo=0, hi=200, h_lines=[33, 100], unit=' мс')
self.g_tick.rect = pygame.Rect(gx + pw // 2, gy, pw // 2 - 8, 90)
self.g_tick.draw(surf, lo=0, hi=60)
# ══════════════════════════════════════════════════════════════════════════════
# Главный цикл
# ══════════════════════════════════════════════════════════════════════════════
PANELS = [
PanelVariable(),
PanelFixed(),
PanelSemiFixed(),
PanelInterp(),
PanelLockstep(),
]
TABS = [
'1 · Variable',
'2 · Fixed+акк.',
'3 · Semi-Fixed',
'4 · Fixed+Interp',
'5 · Lockstep',
]
active = 0
TAB_H = 46
HEADER_H = 56
PANEL_X = 0
PANEL_Y = HEADER_H + TAB_H
PANEL_W = W
PANEL_H = H - PANEL_Y
def draw_tabs(surf, active_idx):
n = len(TABS)
tw = W // n
for i, label in enumerate(TABS):
r = pygame.Rect(i * tw, HEADER_H, tw - 2, TAB_H - 2)
color = ACCENT[i] if i == active_idx else CARD_BG
border = ACCENT[i] if i == active_idx else BORDER
draw_rounded_rect(surf, color, r, radius=6, border=1, border_color=border)
txt_col = (240, 238, 230) if i == active_idx else MUTED
draw_text(surf, label, F_MD, txt_col, r.center, 'center')
def draw_header(surf, panel):
pygame.draw.rect(surf, PANEL_BG, (0, 0, W, HEADER_H))
draw_text(surf, panel.title, F_XL, panel.color, (16, 10))
draw_text(surf, panel.desc, F_SM, MUTED, (16, 34))
def draw_panel_bg(surf, px, py, pw, ph):
pygame.draw.rect(surf, PANEL_BG, (px, py, pw, ph))
def tab_hit(mx, my):
if HEADER_H <= my <= HEADER_H + TAB_H:
return min(mx // (W // len(TABS)), len(TABS) - 1)
return -1
last_time = time.perf_counter()
running = True
while running:
now = time.perf_counter()
real_dt = min(now - last_time, 0.1)
last_time = now
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
hi = tab_hit(*event.pos)
if hi >= 0:
active = hi
PANELS[active].handle_event(event)
PANELS[active].update(real_dt)
screen.fill(BG)
draw_panel_bg(screen, PANEL_X, PANEL_Y, PANEL_W, PANEL_H)
PANELS[active].draw(screen, PANEL_X, PANEL_Y, PANEL_W, PANEL_H)
draw_tabs(screen, active)
draw_header(screen, PANELS[active])
# FPS counter
fps_txt = F_SM.render(f'FPS: {clock.get_fps():.0f}', True, MUTED)
screen.blit(fps_txt, (W - fps_txt.get_width() - 10, 10))
pygame.display.flip()
clock.tick(120)
pygame.quit()
sys.exit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment