Created
May 7, 2026 13:01
-
-
Save Hammer2900/7140fe26365f4ca2b10cef516e107498 to your computer and use it in GitHub Desktop.
интерполяция рендера
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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