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

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

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

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

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

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