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

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

Новая файловая структура для сниппетов

2024-03-15 20:35

В рамках эксперимента по "разработке от сниппетов" понял, что нужно поменять файловую структуру, которая позволит положить рядом два сниппета, если они как-то связаны по смыслу. Использвоть просто числа в именах файлов нет смысла, потому что не получится вставить файл между p_1 и p_2, а использовать спецсимволы в именах скриптов на питоне тоже не очень удобно. Поэтоу растащил файлы по директориям.

Теперь это выглядит так:

files

И если мне нужно будет добавить сниппет между 1 и 2, то это будет выглядеть так:

files

Перемещать "игрока" в сторону мыши

2024-03-15 20:30

Сделал сниппет, в котором шарик бегает за мышкой. Особенность этого сниппета в том, что шарик поворачивается в сторону мышки не сразу, а с заданной угловой скоростью. За счёт этого добавляется эффект инерции при перемещении по экрану.

Красная линия показывает направление вектора скорости.

import pygame as pg

from math import copysign
from pygame import QUIT, KEYDOWN, K_ESCAPE
from pygame.math import Vector2
from pygame.time import Clock


UPDATE_RATE = 1 / 120

MAX_UPDATE_RATE = 1 / 20


class Follower:
    def __init__(self, surface, position, speed):
        self.surface = surface
        self.position = Vector2(position)
        self.speed = speed

        self.direction = Vector2(0, 1)
        self.angle_speed = 90 # Градусов в секунду
        self.color = (44, 44, 44)
        self.line_color = (244, 22, 22)
        self.radius = 20

    def update(self, delta_time):
        mouse_position = Vector2(pg.mouse.get_pos())


        direction_to_mouse = (mouse_position - self.position).normalize()
        if mouse_position.distance_to(self.position) > self.radius:
            angle = self.direction.angle_to(direction_to_mouse)
            if abs(angle) > 180:
                angle = -copysign((360 - abs(angle)), angle)
            self.direction = self.direction.rotate(copysign(min(abs(angle), self.angle_speed * delta_time), angle))
        else:
            self.direction = direction_to_mouse

        self.position += (self.direction * self.speed) * delta_time

    def render(self):
        pg.draw.circle(self.surface, self.color, self.position, self.radius)
        pg.draw.line(
            self.surface,
            self.line_color,
            self.position,
            self.position + self.direction * (self.radius + 10),
            5
        )


class Game:
    def __init__(self, surface):
        self.surface = surface
        self.bg_color = (220, 220, 220)
        self.follower = Follower(surface, (surface.get_width() / 2, 100), 200)
        self.clock = Clock()
        self.time_bucket = 0
        self.working = True

    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

    def update(self, delta_time):
        self.time_bucket += delta_time
        while self.time_bucket >= UPDATE_RATE:
            self.time_bucket -= UPDATE_RATE

            self.follower.update(UPDATE_RATE)

    def draw_direction(self):
        pg.draw.line(self.surface, (44, 200, 44), self.follower.position, pg.mouse.get_pos())

    def render(self):
        self.surface.fill(self.bg_color)

        self.follower.render()
        self.draw_direction()

        pg.display.update()

    def run(self):
        delta_time = 0
        self.clock.tick()
        while self.working:
            self.handle_events()
            self.update(min(delta_time, MAX_UPDATE_RATE))
            self.render()
            delta_time = self.clock.tick() / 1000

def main():
    pg.init()
    Game(pg.display.set_mode((1920, 1080), flags=pg.FULLSCREEN)).run()
    pg.quit()


if __name__ == "__main__":
    main()

8/follow_mouse.py

Механизм обработки времени в игровом цикле (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

Загрузка изображения и вращение вокруг своего центра

2024-03-11 22:30

В этом сниппете я сделал загрузку изображения с диска и вращение вокруг центра экрана. Если нажать на какую-нибудь клавишу, то направление вращения меняется на противоположное.

Ключевой момент в этом сниппете в том, что выводить на экран нужно не просто картинку в заданных координатах, а Rect, который получается с заданным центром. Если этого не сделать, а просто вывести картинку так:

self.surface.blit(self.tank, self.center)

то центр картинки будет всё время смещаться.

Для вращения сделал вот такой танк:

Tank

Внешне результат работы сниппета выглядит так:

Shoot letters preview

import pygame as pg 

from math import copysign
from pygame.time import Clock


class Game:
    def __init__(self, surface):
        self.surface = surface

        self.background = (121, 103, 85)

        self.origin = pg.image.load('tank.png')
        self.center = (
            (self.surface.get_width() - self.origin.get_width()) / 2,
            (self.surface.get_height() - self.origin.get_height()) / 2
        )
        self.angle = 0
        self.angle_speed = 3
        self.tank = pg.transform.rotate(self.origin, self.angle)

        self.clock = Clock()
        self.fps = 60

        self.working = True

    def handle_events(self):
        for e in pg.event.get():
            if e.type == pg.QUIT:
                self.working = False
            elif e.type == pg.KEYDOWN:
                if e.key == pg.K_ESCAPE:
                    self.working = False
                else:
                    # change direction
                    self.angle_speed = -self.angle_speed

    def update(self):
        self.angle += self.angle_speed
        self.angle = copysign(abs(self.angle) % 360, self.angle)
        self.tank = pg.transform.rotate(self.origin, self.angle)

    def render(self):
        self.surface.fill(self.background)
        self.surface.blit(self.tank, self.tank.get_rect(center=self.center))
        pg.display.update()


    def run(self):
        while self.working:
            self.handle_events()
            self.update()
            self.render()

            self.clock.tick(self.fps)

def main():

    pg.init()

    size = (800, 450)
    display_surface = pg.display.set_mode(size)
    Game(display_surface).run()

    pg.quit()


if __name__ == "__main__":
    main()

6/load_and_rotate_image.py

"Стрелялка" буквами

2024-03-10 22:30

За основу взял пост, в котором загружал шрифт, и сделал так, чтобы каждая нажатая буква появлялась внизу окна и поднималась вверх.

Рендеринг букв не стал выносить в метод класса, а оставил внутри main, но для хранения данных о букве сделал датакласс Letter.

Shoot letters preview

import pygame as pg

from dataclasses import dataclass
from pygame import QUIT, KEYDOWN, K_ESCAPE
from pygame.font import Font
from pygame.math import Vector2
from pygame.surface import Surface
from pygame.time import Clock
from random import randint


WINDOW_SIZE = (800, 450)

BG_COLOR = (200, 200, 200)

SPEED = Vector2(0, -3)

FPS = 60


@dataclass()
class Letter:
    position: Vector2
    surface: Surface


def next_position(surf):
    return Vector2(
        randint(0, WINDOW_SIZE[0] - surf.get_width()),
        WINDOW_SIZE[1] + surf.get_height()
    )


def main():
    pg.init()

    surface = pg.display.set_mode(WINDOW_SIZE)
    font = Font('Boxy-Bold.ttf', 24)

    items = []
    clock = Clock()

    working = True
    while working:
        # Handle events
        for e in pg.event.get():
            if e.type == QUIT:
                working = False
                continue

            if e.type != KEYDOWN:
                continue

            if e.key == K_ESCAPE:
                working = False

            char = e.unicode.strip()
            if char:
                surf = font.render(char, False, (50, 50, 50))
                items.append(Letter(next_position(surf), surf))

        # Update
        for i in items:
            i.position += SPEED
        items = [i for i in items if i.position.y > -i.surface.get_height()]

        # Render
        surface.fill(BG_COLOR)
        for i in items:
            surface.blit(i.surface, i.position)
        pg.display.update()

        clock.tick(FPS)

    pg.quit()


if __name__ == "__main__":
    main()    

5/shoot_letters.py