На идею сделать нормальный игровой цикла меня натолкнула предыдущая заметка - Загрузка изображения и вращение вокруг своего центра.
В этом сниппете я сделал реализацию двух вариантов обработки времени в игровом цикле (GameLoop).
Если зажать кнопку мыши, то имитируется подвисание игры (пока кнопка зажата, не вызывается обновление). На экране в этот время вокруг курсора рисуется зелёный круг.
У приложения есть два режима: "медленный" и "быстрый". В медленном режиме время между вызовами update
передаётся в метод "как есть" и используется для вычисления следующей позиции по формуле "скорость * время":
def move_ball(self, delta_time):
self.ball.position += self.ball.speed * delta_time
И при таком варианте шарик может пролетать через стену (в медленном режиме на видео чёрный фон).
Для "быстрого" режима есть минимально возможное значение delta_time
. Для примера я выставил в одну секунду, но в реальном приложении поставил бы 1 / 20 секунды. И помимо этого, вызовы update
идут не на всё время, а на отдельные кусочки UPDATE_RATE
.
Исходник
import pygame as pg
from pygame import QUIT, KEYDOWN, K_ESCAPE, K_SPACE
from pygame.math import Vector2
from pygame.rect import Rect
from pygame.time import Clock
UPDATE_RATE = 1 / 120
MIN_UPDATE_RATE = 1
class Ball:
def __init__(self, surface, position, speed, radius):
self.surface = surface
self.position = position
self.speed = speed
self.radius = radius
self.color = (100, 100, 200)
def render(self):
pg.draw.circle(self.surface, self.color, self.position, self.radius)
class Game:
def __init__(self, surface):
self.surface = surface
self.clock = Clock()
self.accumulator = 0
self.normal_bg_color = (200, 200, 200)
self.slow_bg_color = (20, 20, 20)
self.bg_color = self.normal_bg_color
radius = 10
self.ball = Ball(
self.surface,
Vector2(radius, self.surface.get_height() / 2),
Vector2(200, 0),
radius
)
self.wall = self.create_wall()
self.wall_color = (200, 10, 10)
self.slow = False
self.working = True
def create_wall(self) -> Rect:
wall_width = 10
wall_height = 200
x = (self.surface.get_width() - wall_width) / 2
y = (self.surface.get_height() - wall_height) / 2
return Rect(x, y, wall_width, wall_height)
def handle_events(self):
for e in pg.event.get():
if e.type == QUIT or (e.type == KEYDOWN and e.key == K_ESCAPE):
self.working = False
if e.type == KEYDOWN and e.key == K_SPACE:
self.slow = not self.slow
self.bg_color = self.slow_bg_color if self.slow else self.normal_bg_color
def move_ball(self, delta_time):
self.ball.position += self.ball.speed * delta_time
def collide_screen(self):
if self.ball.position.x < self.ball.radius:
self.ball.position.x = self.ball.radius
self.ball.speed = -self.ball.speed
elif self.ball.position.x > (self.surface.get_width() - self.ball.radius):
self.ball.position.x = self.surface.get_width() - self.ball.radius
self.ball.speed = -self.ball.speed
def collide_wall(self):
if self.ball.position.x + self.ball.radius < self.wall.x or self.ball.position.x - self.ball.radius > self.wall.x + self.wall.width:
return
self.ball.speed = -self.ball.speed
def update(self, delta_time):
self.move_ball(delta_time)
self.collide_screen()
self.collide_wall()
def render(self):
self.surface.fill(self.bg_color)
pg.draw.rect(self.surface, self.wall_color, self.wall)
self.ball.render()
if pg.mouse.get_pressed()[0]:
pg.draw.circle(self.surface, (10, 150, 10), pg.mouse.get_pos(), 10)
pg.display.update()
def run(self):
delta_time = 0
self.clock.tick()
while self.working:
skip = pg.mouse.get_pressed()[0]
self.handle_events()
if self.slow:
if not skip:
self.update(delta_time)
else:
if not skip:
delta_time = min(delta_time, MIN_UPDATE_RATE)
self.accumulator += delta_time
while self.accumulator >= UPDATE_RATE:
self.accumulator -= UPDATE_RATE
self.update(UPDATE_RATE)
self.render()
if not skip:
delta_time = self.clock.tick() / 1000
def main():
pg.init()
Game(pg.display.set_mode((800, 450))).run()
pg.quit()
if __name__ == "__main__":
main()