Разработка на коленке

"тут должна быть красивая цитата о программировании"

Механизм обработки времени в игровом цикле (Game loop)

2024-03-12 22:45

На идею сделать нормальный игровой цикла меня натолкнула предыдущая заметка - Загрузка изображения и вращение вокруг своего центра.

В этом сниппете я сделал реализацию двух вариантов обработки времени в игровом цикле (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()

7/fixed_time_game_loop.py

  • 1 / 1