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

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

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

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

Загрузка шрифта из файла

2024-03-06 23:10

Сниппет, в котором я использовал ttf-шрифт для отображения последней нажатой клавиши. При этом кнопки, которые не дают какое-то строковое представление, игнорируются. И чтобы это было чуть динамичнее, я сделал так, чтобы текст бегал за курсором мышки.

Шрифт взял там - https://opengameart.org/content/boxy-bold-truetype-font

Font screenshot

import pygame

from pygame.math import Vector2


def main():
    pygame.init()

    surface = pygame.display.set_mode((800, 450))

    font = pygame.font.Font('Boxy-Bold.ttf', 24)
    rendered = font.render('Ok?', False, (50, 50, 50))

    working = True
    while working:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                working = False
            elif e.type == pygame.KEYDOWN:
                if e.key == pygame.K_ESCAPE:
                    working = False
                else:
                    if e.unicode.strip():
                        rendered = font.render(e.unicode.strip(), False, (50, 50, 50))

        surface.fill((200, 200, 200))
        surface.blit(rendered, Vector2(pygame.mouse.get_pos()) + Vector2(20, 20))

        pygame.display.update()

    pygame.quit()


if __name__ == "__main__":
    main()

4/use_font.py

Кнопка с пререндеренным Surface

2024-03-03 23:00

Сниппет, в котором для кнопки создаются несколько объектов Surface. Рисуются они один раз, а потом блитятся (blit) на родительский Surface

Prerendered button

import pygame as pg

from pygame.surface import Surface
from pygame.font import Font


class Button:
    def __init__(self, parent_surface: Surface, position, size):
        self.parent_surface = parent_surface
        self.position = position
        self.size = size
        self.border_width = 3

        self.normal = self.make_normal_button()
        self.hover = self.make_hover_button()
        self.pressed = self.make_pressed_button()

        self.is_hover = False
        self.is_pressed = False

    def make_normal_button(self):
        surface = Surface(self.size, pg.SRCALPHA)

        bg_color = (255, 255, 255)
        border_color = (40, 40, 40)
        shadow_height = 5
        surface.fill(bg_color)
        pg.draw.rect(
            surface,
            border_color,
            pg.rect.Rect(0, 0, self.size[0], self.size[1]),
            self.border_width
        )
        pg.draw.rect(
            surface,
            border_color,
            pg.rect.Rect(0, self.size[1] - shadow_height, self.size[0], shadow_height)
        )

        return surface

    def make_hover_button(self):
        surface = Surface(self.size, pg.SRCALPHA)

        bg_color = (255, 255, 255)
        border_color = (80, 80, 80)
        shadow_height = 5
        surface.fill(bg_color)
        pg.draw.rect(
            surface,
            border_color,
            pg.rect.Rect(0, 0, self.size[0], self.size[1]),
            self.border_width
        )
        pg.draw.rect(
            surface,
            border_color,
            pg.rect.Rect(0, self.size[1] - shadow_height, self.size[0], shadow_height)
        )
        return surface

    def make_pressed_button(self):
        surface = Surface(self.size, pg.SRCALPHA)

        bg_color = (255, 255, 255)
        border_color = (80, 80, 80)
        surface.fill(bg_color)
        pg.draw.rect(
            surface,
            border_color,
            pg.rect.Rect(0, 0, self.size[0], self.size[1]),
            self.border_width + 3
        )
        return surface

    def render(self):
        surface = self.normal

        if self.is_hover:
            surface = self.hover
            if self.is_pressed:
                surface = self.pressed

        self.parent_surface.blit(surface, self.position)


class Game:
    def __init__(self, display_surface: Surface):
        self.display_surface = display_surface

        button_size = (200, 60)
        self.button = Button(
            self.display_surface,
            (
                (self.display_surface.get_width() - button_size[0]) // 2,
                (self.display_surface.get_height() - button_size[1]) // 2,
            ),
            button_size
        )

        self.working = True

    def process_events(self):
        for e in pg.event.get():
            if e.type == pg.QUIT or (e.type == pg.KEYDOWN and e.key == pg.K_ESCAPE):
                self.working = False

        mouse_pos = pg.mouse.get_pos()
        button_rect = pg.rect.Rect(
            self.button.position[0],
            self.button.position[1],
            self.button.size[0],
            self.button.size[1]
        )
        self.button.is_hover = button_rect.collidepoint(mouse_pos)
        self.button.is_pressed = pg.mouse.get_pressed()[0]

    def render(self):
        self.display_surface.fill((220, 220, 220))
        self.button.render()
        pg.display.update()

    def run(self):
        while self.working:
            self.process_events()
            self.render()


def main():
    pg.init()
    display_surface = pg.display.set_mode((800, 450))
    Game(display_surface).run()
    pg.quit()


if __name__ == "__main__":
    main()

3/prerendered_button_surface.py

Код, чтобы "стрелять" шариками

2024-03-01 23:00

Небольшой кусок кода, который позволяет "стрелять" шариками. Для этого нужно нажать на кнопку мышки, отвести курсор в другое место и отпустить. Линия покажет направление движения шарика. А длина линии станет визуализацией скорости.

import pygame as pg

from pygame.math import Vector2 as vec2
from pygame.time import Clock
from queue import Queue
from random import randint


class Ball:
    RADIUS = 10

    def __init__(self, surface, position, speed, color):
        self.surface = surface
        self.position = position
        self.speed = speed

        self.color = color

    def render(self):
        pg.draw.circle(self.surface, self.color, self.position, self.RADIUS)


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

        self.width = self.surface.get_width()
        self.height = self.surface.get_height()

        self.bg_color = (30, 40, 60)
        self.balls = []
        self.maxsize = 5

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

        self.mouse_down_pos = None
        self.current_mouse_pos = (0, 0)

        self.ball_color = self.get_next_ball_color()

        self.working = True

    @staticmethod
    def get_next_ball_color():
        return (randint(100, 255), randint(100, 255), randint(0, 100))

    def add_ball(self, position, diff: vec2):
        length = max(0.3, (diff / 10).length())
        length = min(length, 15)

        speed = diff.normalize() * length

        self.balls.append(Ball(self.surface, position, speed, self.ball_color))
        self.ball_color = self.get_next_ball_color()

        while len(self.balls) > self.maxsize:
            self.balls.pop(0)

    def process_events(self):
        for e in pg.event.get():
            if e.type == pg.QUIT or (e.type == pg.KEYDOWN and e.key == pg.K_ESCAPE):
                self.working = False

        self.current_mouse_pos = pg.mouse.get_pos()

        pressed = pg.mouse.get_pressed()[0]
        if pressed and self.mouse_down_pos is None:
            self.mouse_down_pos = self.current_mouse_pos

        if not pressed and self.mouse_down_pos is not None:
            if self.mouse_down_pos != self.current_mouse_pos:
                self.add_ball(
                    vec2(self.mouse_down_pos),
                    vec2(self.mouse_down_pos) - vec2(self.current_mouse_pos))
            self.mouse_down_pos = None

    def update(self):
        for b in self.balls:
            b.position += b.speed

            if b.position.x < Ball.RADIUS or b.position.x > self.width - Ball.RADIUS:
                b.speed = b.speed.reflect(vec2(1, 0))
                if b.position.x < Ball.RADIUS:
                    b.position.x = Ball.RADIUS
                else:
                    b.position.x = self.width - Ball.RADIUS

            if b.position.y < Ball.RADIUS or b.position.y > self.height - Ball.RADIUS:
                b.speed = b.speed.reflect(vec2(0, 1))
                if b.position.y < Ball.RADIUS:
                    b.position.y = Ball.RADIUS
                else:
                    b.position.y = self.height - Ball.RADIUS

    def render(self):
        self.surface.fill(self.bg_color)
        for b in self.balls:
            b.render()

        if self.mouse_down_pos is not None:
            pg.draw.circle(self.surface, self.ball_color, self.mouse_down_pos, Ball.RADIUS)
            pg.draw.line(self.surface, self.ball_color, self.mouse_down_pos, self.current_mouse_pos)

        pg.display.update()

    def run(self):
        while self.working:
            self.process_events()
            self.update()
            self.render()
            self.clock.tick(self.fps)


def main():
    pg.init()

    display_surface = pg.display.set_mode((1920, 1080), pg.FULLSCREEN)
    Game(display_surface).run()

    pg.quit()


if __name__ == "__main__":
    main()

2/pull_and_fire.py