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

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

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

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

Перемещать объект в место последнего клика

2024-02-27 22:00

Когда я писал аркадные игрушки, например, Арканоид, то у объекта была либо своя постоянная скорость, и менялось только направление, либо же объект двигался вслед за курсором (пальцем на телефоне).

В этом сниппете я сделал перемещение к конкретной точке, которая ставится кликом мышки.



import pygame as pg


from pygame.math import Vector2
from pygame.time import Clock


class Deck:
    def __init__(self, surface, x, y, velocity=4, size=(140, 30), color=(10, 100, 10)):
        self.surface = surface
        self.position = Vector2(x, y)
        self.velocity = velocity
        self.size = size
        self.color = color

        self.center_x = x + size[0] / 2

        self.rect = pg.rect.Rect(x, y, size[0], size[1])

    def move(self, x):
        self.center_x = x + self.size[0] // 2
        self.position.x = x
        self.rect.x = x

    def render(self):
        pg.draw.rect(self.surface, self.color, self.rect, border_radius=3)


class Mark:
    def __init__(self, surface, x, y, color=(200, 200, 150)):
        self.surface = surface
        self.x = x
        self.y = y
        self.color = color

        self.inner_radius = 6
        self.outer_radius = 16
        self.dash_len = 4
        self.dash_padding = 13

    def render(self):
        pg.draw.circle(
            self.surface,
            self.color,
            (self.x, self.y),
            self.outer_radius,
            1
        )

        pg.draw.circle(
            self.surface,
            self.color,
            (self.x, self.y),
            self.inner_radius
        )

        y = 0
        while y < self.surface.get_height():
            pg.draw.line(
                self.surface, 
                self.color,
                (self.x, y),
                (self.x, y + self.dash_len)
            )
            y += (self.dash_len + self.dash_padding)

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

        self.bg_color = (0, 0, 0)

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

        self.mouse_down_x = None
        self.mouse_offset = 10

        y = self.surface.get_height() - 80
        self.deck = Deck(self.surface, 10, y)
        self.mark = Mark(self.surface, surface.get_width() // 2, y + self.deck.size[1] //2)

        self.working = True

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

        if pg.mouse.get_pressed()[0] and self.mouse_down_x is None:
            self.mouse_down_x = pg.mouse.get_pos()[0]

        if not pg.mouse.get_pressed()[0] and self.mouse_down_x is not None:
            x = pg.mouse.get_pos()[0]
            if abs(x - self.mouse_down_x) <= self.mouse_offset:
                self.mark.x = self.mouse_down_x
                if self.mark.x >= self.deck.center_x:
                    self.deck.velocity = abs(self.deck.velocity)
                else:
                    self.deck.velocity = -abs(self.deck.velocity)
            self.mouse_down_x = None

    def update(self):
        if abs(self.deck.center_x - self.mark.x) >= abs(self.deck.velocity):
            self.deck.move(self.deck.position.x + self.deck.velocity)
        else:
            self.deck.move(self.mark.x - self.deck.size[0] // 2)

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

        self.deck.render()
        self.mark.render()

        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()

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

    pg.quit()


if __name__ == "__main__":
    main()

1/move_object_to_click.py