Введение

Все мы помним старые игры, в которых впервые появилось трехмерное измерение.

Основоположником 3д игр стала игра Wolfenstein 3D, выпущенная в 1992 году

Игра Wolfenstein 3D (1992 год)
Игра Wolfenstein 3D (1992 год)

а за ней и Doom 1993 года.

Игра DOOM 1993 (1993)
Игра DOOM 1993 (1993)

Эти две игры разработала одна компания: «id Software»

Она создала свой движок специально для этой игры, и в итоге получилась 3д игра, что считалось практически невозможным на те времена.

Но что будет если я скажу что это не 3д игра, а всего лишь симуляция и игра выглядит на самом деле примерно вот так?

Игра Wolfenstein 3D изнутри
Игра Wolfenstein 3D изнутри

На самом деле здесь используется технология Ray Casting, третьего измерения тут просто не существует.

Что же такое этот самый RayCasting, который даже в наши времена актуален, но уже используется не для игр, а для технологии трассировки лучей в современных играх.

Если переводить на русский, то:

Метод бросания лучей(Ray Casting) - один из методов рендеринга в компьютерной графике, при котором сцена строится на основе замеров пересечения лучей с визуализируемой поверхностью.

Мне стало интересно на сколько это сложно реализовать.

И я принялся за написание технологии RayCasting.

Буду делать его на связке python + pygame

Pygame позволяет рисовать на плоскости простые 2D фигуры, и путем танцами с бубном вокруг них я и буду делать 3D иллюзию

Реализация Ray Casting

Для начала создаем простейшую карту с помощью символов, чтобы разделять при отрисовке где блок а где пустое место.

Карта и игрок на ней (под капотом)"." - пустое место, где может ходить игрок"1" - блок
Карта и игрок на ней (под капотом)
"." - пустое место, где может ходить игрок
"1" - блок

Рисуем карту в 2D, и игрока с возможностью управления и расчетом точки взгляда.

player.delta = delta_time()
player.move(enableMoving)

display.fill((0, 0, 0))

pg.draw.circle(display, pg.Color("yellow"), (player.x, player.y), 0)

drawing.world(player)
class Drawing:
    def __init__(self, surf, surf_map):
        self.surf = surf
        self.surf_map = surf_map
        self.font = pg.font.SysFont('Arial', 25, bold=True)

        
    def world(self, player):
        rayCasting(self.surf, player)
def rayCasting(display, player):
    inBlockPos = {'left': player.x - player.x // blockSize * blockSize,
                  'right': blockSize - (player.x - player.x // blockSize * blockSize),
                  'top': player.y - player.y // blockSize * blockSize,
                  'bottom': blockSize - (player.y - player.y // blockSize * blockSize)}

    for ray in range(numRays):
        cur_angle = player.angle - halfFOV + deltaRays * ray
        cos_a, sin_a = cos(cur_angle), sin(cur_angle)
        vl, hl = 0, 0

Движение будет осуществляться путем сложения косинуса угла зрения по горизонтали и синуса угла зрения по вертикали

class Player: 
  def init(self)
    self.x = 0
    self.y = 0
    self.angle = 0
    self.delta = 0
    self.speed = 100
    self.mouse_sense = settings.mouse_sensivity

  def move(self, active):
    self.rect.center = self.x, self.y
    key = pygame.key.get_pressed()
    key2 = pygame.key.get_pressed()
    cos_a, sin_a = cos(self.angle), sin(self.angle)

    if key2[pygame.K_LSHIFT]:
        self.speed += 5
        if self.speed >= 200:
            self.speed = 200
    else:
        self.speed = 100

    if key[pygame.K_w]:
        dx = cos_a * self.delta * self.speed
        dy = sin_a * self.delta * self.speed
    if key[pygame.K_s]:
        dx = cos_a * self.delta * -self.speed
        dy = sin_a * self.delta * -self.speed
    if key[pygame.K_a]:
        dx = sin_a * self.delta * self.speed
        dy = cos_a * self.delta * -self.speed
    if key[pygame.K_d]:
        dx = sin_a * self.delta * -self.speed
        dy = cos_a * self.delta * self.speed
Визуализация расчета движения игрока
Визуализация расчета движения игрока

Получаем такой результат:

Промежуточный результат запуска игры
Промежуточный результат запуска игры

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

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

Пересечение луча и линий на сетке
Пересечение луча и линий на сетке

Так же нужно рассчитать расстояние до вертикальных и горизонтальных линий с которыми пересекся луч.

Вспоминаем школьную тригонометрию и рассмотрим это на примере вертикальных линий

Расстояние до вертикальных и горизонтальных линий с которыми пересекся луч
Расстояние до вертикальных и горизонтальных линий с которыми пересекся луч

Нам известна сторона k – это расстояние игрока до блока

a – это угол каждого луча

Далее просто добавляем длину, так как мы знаем размер нашего блока сетки.

И когда луч врежется в стену цикл остановиться.

Потом применяем это ко всем осям с небольшими изменениями

Для горизонтальных линий тоже самое только с синусом.

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

Добавляем пару переменных высоты, глубины, размера которые высчитываются из достаточно простых формул

def rayCasting(display, player):
  inBlockPos = {'left': player.x - player.x // blockSize * blockSize,
                'right': blockSize - (player.x - player.x // blockSize * blockSize),
                'top': player.y - player.y // blockSize * blockSize,
                'bottom': blockSize - (player.y - player.y // blockSize * blockSize)}

  for ray in range(numRays):
      cur_angle = player.angle - halfFOV + deltaRays * ray
      cos_a, sin_a = cos(cur_angle), sin(cur_angle)
      vl, hl = 0, 0

      #Вертикали
      for k in range(mapWidth):
          if cos_a > 0:
              vl = inBlockPos['right'] / cos_a + blockSize / cos_a * k + 1
          elif cos_a < 0:
              vl = inBlockPos['left'] / -cos_a + blockSize / -cos_a * k + 1

          xw, yw = vl * cos_a + player.x, vl * sin_a + player.y
          fixed = xw // blockSize * blockSize, yw // blockSize * blockSize
          if fixed in blockMap:
              textureV = blockMapTextures[fixed]
              break

      #Горизонтали
      for k in range(mapHeight):
          if sin_a > 0:
              hl = inBlockPos['bottom'] / sin_a + blockSize / sin_a * k + 1
          elif sin_a < 0:
              hl = inBlockPos['top'] / -sin_a + blockSize / -sin_a * k + 1

          xh, yh = hl * cos_a + player.x, hl * sin_a + player.y
          fixed = xh // blockSize * blockSize, yh // blockSize * blockSize
          if fixed in blockMap:
              textureH = blockMapTextures[fixed]
              break

      ray_size = min(vl, hl) * depthCoef
      toX, toY = ray_size * cos(cur_angle) + player.x, ray_size * sin(cur_angle) + player.y
      pg.draw.line(display, pg.Color("yellow"), (player.x, player.y), (toX, toY))

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

#def rayCasting

ray_size += cos(player.angle - cur_angle)
height_c = coef / (ray_size + 0.0001)
c = 255 / (1 + ray_size ** 2 * 0.0000005)
color = (c, c, c)
block = pg.draw.rect(display, color, (ray * scale, half_height - height_c // 2, scale, height_c))

И вот получается уже какая-никакая иллюзия 3D измерения.

Иллюзия 3D измерения
Иллюзия 3D измерения

Текстуры

1 блок имеет 4 стороны и каждую бы должны покрыть текстурой.

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

Наложение полосок текстуры на блок
Наложение полосок текстуры на блок

Так ширина будет варьироваться в зависимости от удаленности стороны блока. А положение полоски рассчитывается путем умножения отступа на размер текстуры.

Если луч падает на вертикаль то отступ высчитываем от верхней точки, если на горизонталь то от левой точки.

Расчет отступа
Расчет отступа
#def rayCasting

if hl > vl:
    ray_size = vl
    mr = yw
    textNum = textureV
else:
    ray_size = hl
    mr = xh
    textNum = textureH

mr = int(mr) % blockSize

textures[textNum].set_alpha(c)
wallLine = textures[textNum].subsurface(mr * textureScale, 0, textureScale, textureSize)
wallLine = pg.transform.scale(wallLine, (scale, int(height_c))).convert_alpha()
display.blit(wallLine, (ray * scale, half_height - height_c // 2))

Добавляем еще возможность отрисовки нескольких текстур на одной карте путем добавления на карту специальных знаков, каждому будет присваиваться своя текстура.

Список моих знаков для создания уровней
Список моих знаков для создания уровней

Вот пример как выглядит 2-ой уровень в игре в виде кода:

textMaplvl2 = [
            "111111111111111111111111",
            "1111................1111",
            "11.........1....11...111",
            "11....151..1....31...111",
            "1111............331...11",
            "11111.....115..........1",
            "1111.....11111....1113.1",
            "115.......111......333.1",
            "15....11.......11......1",
            "11....11.......11..11111",
            "111...................51",
            "111........1......115551",
            "11111...11111...11111111",
            "11111%<@1111111111111111",
]

В итоге получаем адекватное отображение текстур:

Коллизия

Где же такое видано что мы можем проходить через блоки…

Добавляем коллизию. К каждой позиция блока добавляем так называемый коллайдер и такой же коллайдер добавляем игроку. Если он продолжит идти так как шел и такими темпами на следующем кадре по предсказанию зайдет в блок, то мы просто зануляем ускорение по нужной оси.

Столкновение блока и игрока
Столкновение блока и игрока

Для этого чуть допишем класс Player. Я решил еще сразу добавить управление камерой с помощью мыши. Вот как по итогу стал выглядеть этот класс:

class Player:
    def __init__(self):
        self.x = 0
        self.y = 0

        self.angle = 0
        self.delta = 0
        self.speed = 100
        self.mouse_sense = settings.mouse_sensivity

        #collision
        self.side = 50
        self.rect = pygame.Rect(*(self.x, self.y), self.side, self.side)

    def detect_collision_wall(self, dx, dy):
        next_rect = self.rect.copy()
        next_rect.move_ip(dx, dy)
        hit_indexes = next_rect.collidelistall(collision_walls)

        if len(hit_indexes):
            delta_x, delta_y = 0, 0
            for hit_index in hit_indexes:
                hit_rect = collision_walls[hit_index]
                if dx > 0:
                    delta_x += next_rect.right - hit_rect.left
                else:
                    delta_x += hit_rect.right - next_rect.left
                if dy > 0:
                    delta_y += next_rect.bottom - hit_rect.top
                else:
                    delta_y += hit_rect.bottom - next_rect.top
            if abs(delta_x - delta_y) < 50:
                dx, dy = 0, 0
            elif delta_x > delta_y:
                dy = 0
            elif delta_y > delta_x:
                dx = 0

        self.x += dx
        self.y += dy

    def move(self, active):
        self.rect.center = self.x, self.y
        key = pygame.key.get_pressed()
        key2 = pygame.key.get_pressed()
        cos_a, sin_a = cos(self.angle), sin(self.angle)

        if key2[pygame.K_LSHIFT]:
            self.speed += 5
            if self.speed >= 200:
                self.speed = 200
        else:
            self.speed = 100

        self.mouse_control(active=active)

        if key[pygame.K_w]:
            dx = cos_a * self.delta * self.speed
            dy = sin_a * self.delta * self.speed
            self.detect_collision_wall(dx, dy)
        if key[pygame.K_s]:
            dx = cos_a * self.delta * -self.speed
            dy = sin_a * self.delta * -self.speed
            self.detect_collision_wall(dx, dy)
        if key[pygame.K_a]:
            dx = sin_a * self.delta * self.speed
            dy = cos_a * self.delta * -self.speed
            self.detect_collision_wall(dx, dy)
        if key[pygame.K_d]:
            dx = sin_a * self.delta * -self.speed
            dy = cos_a * self.delta * self.speed
            self.detect_collision_wall(dx, dy)

    def mouse_control(self, active):
        if active:
            if pygame.mouse.get_focused():
                diff = pygame.mouse.get_pos()[0] - half_width
                pygame.mouse.set_pos((half_width, half_height))
                self.angle += diff * self.delta * self.mouse_sense

Геймплей

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

for blockNow in blockMapTextures:
        questBlock = False
        if (blockNow[0] - blockSize // 2 < player.x < blockNow[0] + blockSize * 1.5 and blockNow[1] < player.y < blockNow[1] + blockSize) or \
        (blockNow[1] - blockSize // 2 < player.y < blockNow[1] + blockSize * 1.5 and blockNow[0] < player.x < blockNow[0] + blockSize):
            if countOfDraw < len(blocksActive) and doubleDrawOff:
                display.blit(
                    pg.transform.scale(ui['mouse2'], (ui['mouse2'].get_width() // 2, ui['mouse2'].get_height() // 2)),
                    (130, 750))
                if event.type == pg.MOUSEBUTTONDOWN and pg.mouse.get_pressed()[2]:
                    if blockMapTextures[blockNow] == '<':
                        questBlock = True
                    if questBlock == False:
                        try:
                            tempbackup_color.clear()
                            tempbackup.clear()
                            coloredBlocks.clear()
                            block_in_bag.pop(-1) 
                            tempbackup.append(blockMapTextures[blockNow])
                            tempbackup_color.append(blocks_draw_avaliable[list(blocks_draw_avaliable.keys())[-1]])
                            print('tempbackup_color : ', tempbackup_color)
                            blockMapTextures[blockNow] = blocks_draw_avaliable[list(blocks_draw_avaliable.keys())[-1]]
                            coloredBlocks.append(blockNow)
                            blocks_draw_avaliable.pop(list(blocks_draw_avaliable.keys())[-1])
                            countOfDraw += 1         
                            doubleDrawOff = False
                            doubleBack = False
                        except:
                            print('Error in color drawing')

Грубо говоря, мы условно увеличиваем диапазон координат которые захватывает один блок, и постоянно смотрим, заходит ли игрок в эти координаты. У каждого блока, получается, есть некая область вокруг(без углов) размером в несколько десятков пикселей, и при заходе в нее, считается что ты рядом с определенным блоком.

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

Далее реализуем систему квестов и смену уровней в зависимости от того выполнен квест или нет. А так же переключатель уровней, с картинкой для сюжета в начале каждого уровня.

def lvlSwitch():
    settings.textMap = levels.levelsList[str(settings.numOfLvl)]
    with open("game/settings/settings.json", 'w') as f:
        settings.sett['numL'] = settings.numOfLvl
        js.dump(settings.sett, f)
    print(settings.numOfLvl)
    main.tempbackup.clear()
    main.coloredBlocks.clear()
    main.blocksActive.clear()
    main.tempbackup_color.clear()
    main.block_in_bag.clear()
    main.blocks_draw_avaliable.clear()
    main.countOfDraw = 0
    main.blockClickAvaliable = 0
    
def switcher():  
    global lvlSwitches 
    main.display.blit(ui[f'lvl{settings.numOfLvl+1}'], (0,0))
    main.timer = False
    if pg.key.get_pressed()[pg.K_SPACE]:
        level5_quest.clear()
        main.doubleQuest = True 
        settings.numOfLvl += 1 
        lvlSwitch()
        main.timer = True
        level5_quest.clear()
        lvlSwitches = False
    

def quest(lvl):
    global lvlSwitches
    tmp = []
    for blockNeed in blockQuest:
        if blockQuest[blockNeed] == '@':
            if blockMapTextures[blockNeed] == '3':
                tmp.append(1)
                if settings.numOfLvl == 5:
                    level5_quest.add(1)
        if blockQuest[blockNeed] == '!':
            if blockMapTextures[blockNeed] == '2':
                tmp.append(2)
                if settings.numOfLvl == 5:
                    level5_quest.add(2)
                    
        if blockQuest[blockNeed] == '$':
            if blockMapTextures[blockNeed] == '4':
                tmp.append(3)
                if settings.numOfLvl == 5:
                    level5_quest.add(3)
        if blockQuest[blockNeed] == '%':
            if blockMapTextures[blockNeed] == '5':
                tmp.append(4)
                if settings.numOfLvl == 5:
                    level5_quest.add(4)

Реализуем пару механик:

Первая механика – банально поставить нужный цвет в нужную ячейку. Объяснений не требуется.

Вторая механика – телепортация создается новая карта в виде листа и блоки в ней раз в какое то время перемешиваются, создается ощущения телепортаций цветов.

def randomColorBlockMap(textMap):
    timer = t.perf_counter()
    text = textMap
    newTextMap = []
    generatedMap = []
    for row in text:
        roww = []
        for column in row:
            roww.append(column)
        newTextMap.append(roww)
    textsForShuffle = []
    for row in text:
        for column in row:
            if column != '.' and column != '<' and column != '$' and column != '%' and column != '@' and column != '!':
                textsForShuffle.append(column)
    xy_original = []
    for y, row in enumerate(text):
        for x, column in enumerate(row):
            if column != '.' and column != '<' and column != '$' and column != '%' and column != '@' and column != '!':
                if (x*blockSize, y*blockSize) not in list(settings.blockQuest.keys()):
                    xy_original.append([x,y])
    xy_tmp = xy_original
    for y, row in enumerate(newTextMap):       
        for x, column in enumerate(row):
            if column != '.' and column != '<' and column != '$' and column != '%' and column != '@' and column != '!':
                if (x*blockSize, y*blockSize) not in list(settings.blockQuest.keys()):  
                    ch = rn.choice(textsForShuffle)
                    newTextMap[y][x] = ch
                    textsForShuffle.remove(ch)
                
    for row in newTextMap:
        generatedMap.append(''.join(row))

    initMap(generatedMap)

Третья механика – добавляем ЧБ фильтр на каждую текстуру…

def toBlack():
    settings.textures['2'] = pygame.image.load('textures/colorYellowWallBlack.png').convert()
    settings.textures['3'] =  pygame.image.load('textures/colorBlueWallBlack.png').convert()
    settings.textures['4'] =  pygame.image.load('textures/colorRedWallBlack.png').convert()
    settings.textures['5'] =  pygame.image.load('textures/colorGreenWallBlack.png').convert()
    settings.textures['<'] =  pygame.image.load('textures/robotBlack.png').convert()
    ui['3'] = pygame.image.load("textures/blue_uiBlack.png")
    ui['2'] = pygame.image.load("textures/yellow_uiBlack.png")
    ui['4'] = pygame.image.load("textures/red_uiBlack.png")
    ui['5'] = pygame.image.load("textures/green_uiBlack.png")

Дальше я сделал меню в виде класса, чтобы удобно добавлять опции когда это будет нужно.

class Menu:
    def __init__(self):
        self.option_surface = []
        self.callbacks = []
        self.current_option_index = 0

    def add_option(self, option, callback):
        self.option_surface.append(f1.render(option, True, (255, 255, 255)))
        self.callbacks.append(callback)

    def switch(self, direction):
        self.current_option_index = max(0, min(self.current_option_index + direction, len(self.option_surface) - 1))

    def select(self):
        self.callbacks[self.current_option_index]()

    def draw(self, surf, x, y, option_y):
        for i, option in enumerate(self.option_surface):
            option_rect = option.get_rect()
            option_rect.topleft = (x, y + i * option_y)
            if i == self.current_option_index:
                pg.draw.rect(surf, (0, 100, 0), option_rect)
            b = surf.blit(option, option_rect)
            pos = pygame.mouse.get_pos()
            if b.collidepoint(pos):
                self.current_option_index = i
                for event in pg.event.get():
                    if pg.mouse.get_pressed()[0]:
                        self.select()

Реализуем сохранения:

try:
    with open("game/settings/settings.json", 'r') as f:
        sett = js.load(f)
except:
    with open("game/settings/settings.json", 'w') as f:
        sett = {
            'FOV' : pi / 2,
            'numRays' : 400,
            'MAPSCALE' : 10,
            'numL' : 1,
            'mouse_sensivity' : 0.15
        }
        js.dump(sett, f)

numOfLvl = sett['numL']
textMap = levels.levelsList[str(numOfLvl)]

mouse_sensivity = sett['mouse_sensivity']

И в заключении мини философскую историю с глубоким смыслом и неожиданную концовку.

Заключение

Вот и получается игра с 2.5D измерением, сотнями лучей, маленьким FPS и незамысловатым геймплеем, на которую потребовалось всего 4 библиотеки, 68 текстур, и  1018 строчек кода.

Также вы всегда можете ознакомиться с полным кодом этого проекта или скачать игру у меня на GitHub.

Надеюсь этой статьей я вам чем то помог и вы нашли данную информацию в какой-то степени полезной. Спасибо за внимание <3

Комментарии (7)


  1. HemulGM
    23.07.2023 06:14
    +2

    Отличный, кстати, пример того, как питон плохо справляется с работой, когда простые вычисления приходится делать ему самому, а не полагаться на C/C++ библиотеки.


  1. da-nie
    23.07.2023 06:14
    +5

    На самом деле здесь используется технология Ray Casting, третьего измерения тут просто не существует.

    Так. Сначала показан Wolf-3D, а потом DooM. Это утверждение, полагаю, относится и к DooM? В таком случае, разочарую — в DooM используется BSP-дерево, и третье измерение, внезапно, существует и в нём тоже можно перемещаться сколько угодно! :) Это уже вторая статья про Ray Casting за последние пару месяцев, и опять автор не удосужился почитать, как же сделан DooM (у меня есть подозрение, что вы родилисль сильно позже, чем этот DooM был уже почти забыт)! Ну пожалуйста, не надо фантазий. Да поиграйте ж вы в этот DooM, наконец. Сравните со своим движком и задайтесь вопросом, как же это у вас такие сцены, как в DooM, не получаются физически?


    Мне стало интересно на сколько это сложно реализовать.

    Несложно. Но лучше реализуйте экранный портал. Он тоже несложный, но 3D у вас будет уровня DooM и Duke Nukem 3D.


    Вот и получается игра с 2.5D измерением, сотнями лучей, маленьким FPS

    А вы на I5 запускали? :) Шутка. А сравните ваш код и подход с кодом из книги Шикин, Боресков "Компьютерная графика. Полигональные модели". Там этот метод разобран детально. А то что-то у вас как-то FPS даже для питона не очень.
    Сколько вы, кстати, лучей-то запускаете? А то у меня нехорошее предчувствие появилось, что их число у вас не равно разрешению по X экрана.


    Кстати, как дела со скольжением при столкновении с блоком (когда игрок упирается в блок под некоторым углом)? :) Помнится, правильно сделать это скольжение была весёлая задачка. :)


    В итоге получаем адекватное отображение текстур:

    А где же текстуры? Вот эти цветные вертикальные линии? Ну нет, это не серьёзно...


    1. Vasyutka
      23.07.2023 06:14
      +2

      как-то слишком много сурового русского фидбека. :) А книжка - ну понятно что автор не из тех, кто может закрыть глаза и вспомнить как выглядели отдельные страницы Шикина и Борескова, да еще и как пахли. И всю историю эволюции wolf3d->doom->и затем уже полноценные полигональные 3д движки (с вставкой спрайтов иногда от бедности). А чтобы поностальгировать, для сведения олдскул - сама книжка (хотя мне кажется, что это и правда очень хорошее историческое чтиво для кого угодно кто немного занимается компьютерной графикой):

      Hidden text


      1. da-nie
        23.07.2023 06:14
        +1

        Да ну бросьте, где же тут суровость? :) Скорее, сарказм.


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

        Конечно, нет. Автор, думаю, только-только узнал, что может сделать Ray Casting и получить хоть какое-то 3D и спешит поделиться этой технологией. :)
        Но я ему завидую — у него всё впереди. :) Даже плюсиков наставил ему, вот.


    1. benvito Автор
      23.07.2023 06:14

      разочарую — в DooM используется BSP-дерево, и третье измерение, внезапно, существует и в нём тоже можно перемещаться сколько угодно!

      Правильно подметили, спасибо! Действительно, даже не удосужился почитать про эту игру побольше, просто визуально уж больно похож он на Ray Casting) Впредь постараюсь поглубже вникать в тему.

      А то что-то у вас как-то FPS даже для питона не очень.

      Эту всю штуку можно оптимизировать, в том числе и с помощью любого JIT компилятора, но проблема в том, что на пайтоне они уж очень не дружат с ООП и с некоторыми видами данных, а чинить всю эту тему мне не очень то и хотелось. Как-нибудь на досуге ознакомлюсь с книгой.

      Сколько вы, кстати, лучей-то запускаете? А то у меня нехорошее предчувствие появилось, что их число у вас не равно разрешению по X экрана.

      Это все можно настроить, конечно. А так, разрешение экрана 1600 по ширине, запускаю в основном 400 лучей, 4 к 1 получается, путем проб и ошибок выяснилось, что лучше запускать число лучей такое, чтобы это число имело общий знаменатель с шириной экрана, как то так.

      Кстати, как дела со скольжением при столкновении с блоком (когда игрок упирается в блок под некоторым углом)?

      О да, в точку! Но уже честно не помню как, но это проблему я как то сгладил, конечно присутствуют иногда глитчи, но в основном игрок именно скользит по стенам.

      А где же текстуры? Вот эти цветные вертикальные линии?

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

      Спасибо за такую полную критику, а также за то, что верно подмечали ошибки и недочеты!


  1. playermet
    23.07.2023 06:14

    Про рейкастинг на хабре (да и вообще в сети) уже много статей было, и во всех примерно одно и то же, только языки отличаются. Хотелось бы увидеть какое-то отклонение от "нормы" - округлые стены, полупрозрачность, освещение, или еще что-нибудь интересное придумать. Алгоритм то позволяет делать игры на железе 30-летней давности, в его чистом виде мощности современных ПК почти не задействованы. Интересно посмотреть, какие прикольные штуки можно получить если заменить рейкастинг на ray marching.


  1. Zara6502
    23.07.2023 06:14

    по теме смотрел этот канал на ютубе