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

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

Изменение рабочей директории

2024-03-22 21:35

Этот снипетт полезен именно в качестве дополнения для разработки кода в рамках Цеттелкастена.

Для переключения между разными снипеттами или готовыми программами в редакторе кода нужно находиться в родительской директории. Но чтобы запускать, приходиться переходить в директорию со скриптом. Поэтому я написал такой сниппет:

def chdir():
    path = os.path.dirname(os.path.realpath(__file__))
    print('Change dir to: <{}>'.format(path))
    os.chdir(path)

9/change_dir_to_base.py

Первая игра, опубликованная из снипеттов

2024-03-22 20:35

Эксперимент стартовал с идеи про Цеттелькастен, котрым пользовался Луман, а я узнал из книги Аренса Зонке. Цель такого ящика: упрощать создание каких-то письменных работ, в моём случае - программ.

Я попробовал оформить что-то из тех снипеттов, которые уже успел сделать. Получилась заготовка под игру про космический кораблик, стреляющий самонаводящимися ракетами.

Это сборка из снипеттов:

Пожалуй, в качестве персонажа лучше бы подошёл какой-нибудь человек с гранатомётом, охотящийся на тараканов-мутантов, но я пока не стал ничего такого рисовать.

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

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