*Вы можете найти ссылки на предыдущие части в нижней части этого руководства.

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

ЧТО ВЫ УЗНАЕТЕ В ЭТОЙ ЧАСТИ:

  • Как работать с узлами таймера.
  • Как создать движущуюся платформу.
  • Как работать со слоями карты листов.

ПОСЛЕДНИЕ ШТРИХИ УРОВНЯ 1

Прямо сейчас, когда мы запускаем наш уровень, у нас все еще есть серый фон по умолчанию в задней части нашей карты. Мы изменим этот фон на тайловый с помощью слоев Tilemap. Слои позволяют нам отличать плитки переднего плана от плиток фона для лучшей организации.

По умолчанию узел TileMap автоматически имеет один готовый слой. В вашей основной сцене выберите узел «Уровень» Tilemap и перейдите к свойству «Слои» на панели «Инспектор».

Вы увидите, что существующий слой существует, но у него нет имени. В настоящее время все плитки, которые мы уже нарисовали на нашей Tilemap, существуют на этом слое. Мы хотим переименовать этот слой в наш слой «Передний план». Вы заметите, что ваше свойство Layers на панели Tilemap ниже теперь отражает это изменение.

Мы хотим, чтобы наш фон отображался за нашим слоем переднего плана, поэтому давайте добавим новый элемент в свойство «Слои» и назовем его «Фон». Теперь вы сможете переключаться между слоями.

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

Теперь в опции «Слои» на панели Tilemap убедитесь, что фоновый слой выбран. Если вы выберете слой, все плитки, существующие на других слоях, должны быть затемнены.

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

Кроме того, убедитесь, что вы не закрашиваете свои лестницы. Мы добавим еще один узел Tilemap, чтобы заполнить пространство за лестницами. Альтернативным решением этой проблемы является перетаскивание узла Ladders над узлом Tilemap в дереве основной сцены, но мне не нравится, как лестницы нависают над картой!

Восполним этот пробел. Дублируйте узел Tilemap в основной сцене и перетащите его за узел Ladders. Мы можем переименовать этот дублированный узел в «Фон».

Удалите слой «Передний план» этой Tilemap и просто нарисуйте остальную часть фона, чтобы избавиться от этих неудобных серых пробелов! (Или просто удалите существующий фон из узла вашего уровня и просто нарисуйте его здесь).

У вас должно получиться что-то вроде этого:

Теперь давайте добавим в нашу сцену несколько окон в качестве декораций! В узле Tilemap «Уровень» добавьте новый слой под названием «Украшения».

В вашей панели TileSet ниже нам нужно создать новый лист тайлов для наших украшений. Перейдите к «res://Assets/Kings and Pigs/Sprites/14-TileSets/Decorations (32x32).png» и перетащите изображение в свойство Tiles.

На панели Tilemap нарисуйте окна на слое «Украшения». Вы также можете добавить другие украшения — например, полки или свечи. Вы можете найти больше предметов декора в ресурсах, представленных в каталоге «res://Assets/Kings and Pigs/Sprites/7-Objects/».

В итоге я создал что-то вроде этого:

УРОВЕНЬ 2 + ДВИЖУЩАЯСЯ ПЛАТФОРМА

Теперь, когда мы настроили наш первый уровень, мы можем продолжить и создать наш второй уровень. Если вы хотите — и у вас есть время — вы также можете создать уровни 3 и 4. Сейчас мы создадим только уровень 2. Для этого мы можем просто продублировать нашу главную сцену. Мы можем назвать это «Main_2».

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

Вот план расположения нашего второго уровня:

  • У нас будет спаунер бочки наверху. Вместо бочек наш спаунер будет спавнить бомбы, которые будут катиться по определенному пути.
  • У нас также будут маленькие создатели препятствий по бокам, которые будут бросать ящики в нашего игрока, чтобы усложнить игру. Эти ящики также будут следовать по определенному пути.
  • У нас также будут случайным образом разбросаны на нашем уровне подбираемые предметы, повышающие атаку и очки.
  • У нас будут стены, которые двигаются вверх и вниз, блокируя проход игрока.
  • Наконец, у нас будет начальная точка появления игрока и конечная точка, которая завершит уровень.

Вот пример того, как может выглядеть этот уровень:

На этом уровне мы добавим больше генераторов бомб по бокам, чтобы сделать его более сложным, мы также создадим движущуюся стену, которая будет блокировать проход нашего игрока каждые 3 секунды или около того. Теперь, прежде чем мы приступим к созданию нашего уровня, давайте сначала создадим нашу подвижную стену или платформу. Для этого нам нужно создать новую сцену с узлом Area2D в качестве корня. Вы также можете использовать узел CharacterBody2D, если хотите — нам просто нужен корневой узел, который может обрабатывать столкновения и движения.

Переименуйте этот узел в «Платформа» и сохраните сцену в папке «Сцены».

Назначьте этой новой сцене узел CollisionShape2D с формой типа RectangleShape2D. Позже мы исправим ширину и высоту этой фигуры.

Теперь скопируйте и вставьте карту Level TileMap из основной сцены в сцену Platform. Мы скопировали этот узел, потому что нам нужны автотайлы (территории), которые мы создали ранее. Удалите все добавленные нами слои (фон, передний план и украшения), чтобы у нас был чистый лист для рисования.

Нарисуйте стену шириной 2 и высотой 5 клеток. Кроме того, исправьте форму столкновения, чтобы очертить ее.

В качестве подсказки на будущее убедитесь, что ваша Tilemap переименована во что-то отличное от «Стена» или «Уровень», так как это приведет к взрыву бомбы при взаимодействии с этой платформой позже.

Теперь в вашей сцене платформы подключите к ней новый скрипт и сохраните этот скрипт в папке Scripts.

Нам также нужно добавить в нашу сцену последний узел — узел Таймер. Узел Timer создает таймер обратного отсчета, который отсчитывает указанный интервал и подает сигнал при достижении 0. Мы хотим, чтобы этот таймер изменил состояние движения нашей стены через x секунд.

Мы также хотим связать сигнал timeout() нашего узла Timer с нашим скриптом. Этот сигнал будет излучаться каждый раз, когда наш таймер достигает 0. Таким образом, наша стена будет двигаться, отсчитывать до 0, а затем снова двигаться. Этот таймер вызовет изменение этих состояний движения. На панели «Сигналы» узла «Таймер» подключите сигнал timeout() к вашему сценарию.

Это создаст функцию func _on_timer_timeout(): в вашем скрипте. В этой функции мы изменим состояние движения наших платформ. Прежде чем мы это сделаем, нам сначала нужно определить наши состояния движения. Наша платформа будет двигаться вертикально, ожидая внизу, затем поднимаясь, ожидая наверху и снова двигаясь вниз по петле.

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

### Platform.gd 

extends Area2D

#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}

Затем в нашей функции _on_timer_timeout(): мы можем изменить наши состояния. Если платформа находится в состоянии WAIT_AT_TOP, она должна изменить свое состояние на MOVING_DOWN. Если платформа находится в состоянии WAIT_AT_BOTTOM, она должна изменить свое состояние на MOVING_UP. Нам нужно создать переменную, которая фиксирует состояние нашей платформы при запуске игры, а также при изменении этого состояния. Сначала мы установим его на WAIT_AT_BOTTOM.

### Platform.gd 

extends Area2D

#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}

#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM

#platform direction changes on timer timeout      
func _on_timer_timeout():
    if current_state == State.WAIT_AT_TOP:
        switch_state(State.MOVING_DOWN)
    
    if current_state == State.WAIT_AT_BOTTOM:
        switch_state(State.MOVING_UP)

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

### Platform.gd 

extends Area2D

#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}

#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM

#movement position 
var initial_position

Мы установим значение этой переменной во встроенной функции ready(), потому что эта функция вызывается при добавлении узла на сцену. В этой функции мы установим для переменной initial_position текущую позицию платформы y и установим ее состояние MOVING_UP, поскольку наше текущее_состояние равно WAIT_AT_BOTTOM, и мы хотим, чтобы наше состояние изменялось как как только игра загрузится. Мы можем найти положение узла, обратившись к свойству position узла.

### Platform.gd 

#older code

#sets platforms y position on game start and switches the state
func _ready():
    initial_position = position.y
    switch_state(State.MOVING_UP)

Теперь нам также нужно дать нашей платформе несколько других переменных, которые будут определять ее скорость движения (насколько быстро она движется), диапазон движения (как далеко она движется вверх и вниз), прогресс (закончила ли она движение или нет), и верхнее и нижнее время ожидания (как долго оно ждет, прежде чем двигаться вверх и вниз). Мы экспортируем переменные, которые мы хотим изменить в панели Inspector. Это позволит нам индивидуально изменять значения движения каждой инстансированной платформы — т. е. некоторые платформы будут двигаться быстрее, ждать дольше или подниматься выше, чем другие.

### Platform.gd

extends Area2D

#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}

#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM

#movement position and movement progress value
var initial_position
var progress = 0.0

#platforms movement speed and range
@export var movement_speed = 50.0
@export var movement_range = 50

#wait times
@export  var wait_time_at_top = 3.0 # Time in seconds to wait at top
@export var wait_time_at_bottom = 3.0 # Time in seconds to wait at top

Нам нужно создать функцию, которая будет постоянно переключать состояние платформы. В зависимости от нового состояния он должен сбросить прогресс движения платформы, установить таймер и его время ожидания (которое представляет собой секунды, необходимые для обратного отсчета), а также изменить переменную current_state. Мы будем использовать оператор match для проверки и изменения состояний нашей платформы. Оператор match используется для ветвления выполнения программы. Это эквивалент оператора switch, который можно найти во многих других языках, но он предлагает некоторые дополнительные функции.

### Platform.gd 

#older code

#changes the platforms movement states
func switch_state(new_state):
    current_state = new_state
    match new_state:
        #if state is moving up, reset progress
        State.MOVING_UP:
            progress = 0.0
        
        #if state is waiting at top, start the timer to change the state
        State.WAIT_AT_TOP:
            $Timer.wait_time = wait_time_at_top #will wait x seconds before moving
            $Timer.start()
            
        #if state is waiting at bottom, start the timer to change the state
        State.WAIT_AT_BOTTOM:
            $Timer.wait_time = wait_time_at_bottom #will wait x seconds before moving
            $Timer.start()
            
        #if state is moving down, move the platform via the speed and range defined
        State.MOVING_DOWN:
            progress = movement_range / movement_speed

Наконец, мы можем вызвать нашу вновь созданную функцию switch_state() в нашей функции physics_process(). Эта функция называется каждым физическим кадром. Мы будем использовать его для проверки текущего состояния платформы и перемещения или ожидания в зависимости от состояния. Функция Lerp используется для плавной интерполяции между нижней и верхней позициями.

### Platform.gd 

#older code

#moves our platform
func _physics_process(delta):
    match current_state:
        #if its moving up
        State.MOVING_UP:
            progress += delta
            #change its position
            position.y = lerp(initial_position, initial_position - movement_range,   progress / (movement_range / movement_range))
            if progress >= (movement_range / movement_speed):
                switch_state(State.WAIT_AT_TOP)
        
        #if its moving down
        State.MOVING_DOWN:
            progress -= delta
            #change its position
            position.y = lerp(initial_position, initial_position - movement_range, progress / (movement_range / movement_speed))
            if progress <= 0:
                switch_state(State.WAIT_AT_BOTTOM)

В сценарии мы перемещаем нашу платформу по вертикали между двумя точками со временем ожидания вверху и внизу. Движение плавное благодаря использованию линейной интерполяции (lerp), а состояния обеспечивают правильную обработку различных фаз движения.

Ваш код должен выглядеть как это.

Если вы вставите свою платформу в сцену Main_2, вы увидите, что можете изменить ее значения на панели инспектора. Убедитесь, что вы положили его на пол и установили диапазон его движения на остановку, когда он достигает этажа над ним — вы хотите, чтобы края вашей платформы соединялись с этажом выше и ниже. Вы можете проверить эти значения, нажав F6, чтобы проверить текущую сцену.

Он должен быть размещен следующим образом:

И его диапазон движения должен быть изменен так, чтобы он останавливался вот так:

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

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

Вот что я создал для своего второго уровня:

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

Устранение неполадок: устранение проблемы с прыжками игрока

Если вы тестировали свою игру, вы, вероятно, заметили небольшой сбой, который возникает, когда ваш игрок прыгает и меняет направление. Игрок прыгает, но если вы нажимаете кнопки для поворота влево или вправо, он немного меняет направление, прежде чем продолжить прыгать. Мы хотим, чтобы игрок сначала закончил анимацию прыжка, а затем побежал в новом направлении, а не менял направление во время прыжка. Для этого нам нужно исправить нашу функцию player_animations(), чтобы она запускалась только в том случае, если игрок не прыгает. Прежде чем мы сможем это сделать, давайте создадим новую переменную в нашем глобальном скрипте, чтобы отслеживать состояние нашего прыжка.

### Global.gd

extends Node

#movement states
var is_attacking = false
var is_climbing = false
var is_jumping = false

Затем в нашей функции player_animations() мы хотим запускать анимацию бега только в том случае, если наш игрок не прыгает.

### Player.gd

#older code

#animations
func player_animations():
    #on left (add is_action_just_released so you continue running after jumping)
    if Input.is_action_pressed("ui_left") && Global.is_jumping == false:
        $AnimatedSprite2D.flip_h = true
        $AnimatedSprite2D.play("run")
        $CollisionShape2D.position.x = 7
        
    #on right (add is_action_just_released so you continue running after jumping)
    if Input.is_action_pressed("ui_right") && Global.is_jumping == false:
        $AnimatedSprite2D.flip_h = false
        $AnimatedSprite2D.play("run")
        $CollisionShape2D.position.x = -7
    
    #on idle if nothing is being pressed
    if !Input.is_anything_pressed():
        $AnimatedSprite2D.play("idle")

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

### Player.gd

#older code

#singular input captures
func _input(event):
    #on attack
    if event.is_action_pressed("ui_attack"):
        Global.is_attacking = true
        $AnimatedSprite2D.play("attack")        

    #on jump
    if event.is_action_pressed("ui_jump") and is_on_floor():
        velocity.y = jump_height
        $AnimatedSprite2D.play("jump")
    
    #on climbing ladders
    if Global.is_climbing == true:
        if Input.is_action_pressed("ui_up"):
            $AnimatedSprite2D.play("climb") 
            gravity = 100
            velocity.y = -160
            Global.is_jumping = true
            
    #reset gravity
    else:
        gravity = 200
        Global.is_climbing = false  
        Global.is_jumping = false

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

Поздравляем с созданием второго уровня с движущимися платформами! Вы также научились работать со слоями и, надеюсь, у вас получилась крутая карта! Вы можете пойти дальше и создать 3-й и 4-й уровень с большим количеством платформ и большим количеством мест для врагов, если хотите. Говоря о врагах, мы создадим нашу Бомбу и Генератор Бомб в следующей части. Этот генератор бомб создаст бомбу, которая будет двигаться по определенному пути и взорвется, если достигнет конца или столкнется с игроком.

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

Следующая часть серии руководств

Учебная серия состоит из 24 глав. Я буду публиковать все главы в ежедневных разделах в течение следующих нескольких недель. Вы можете найти обновленный список ссылок на учебники для всех 24 частей этой серии на моем GitBook. Если вы еще не видите ссылку, добавленную к части, это означает, что она еще не опубликована. Кроме того, если будут какие-либо будущие обновления серии, мой GitBook будет местом, где вы сможете быть в курсе всего!

Поддержите серию и получите ранний доступ!

Если вам нравится эта серия и вы хотите поддержать меня, вы можете пожертвовать любую сумму моему магазину KoFi или купить оффлайн PDF, в котором вся серия собрана в одном буклете, который можно взять с собой!

Брошюра дает вам пожизненный доступ к полной автономной версии брошюры «Изучайте Godot 4, создавая двухмерный платформер» в формате PDF. Это 451-страничный документ, содержащий все учебные пособия из этой серии в последовательном формате, а также вы получите от меня специальную помощь, если вы когда-нибудь застрянете или вам понадобится совет. Это означает, что вам не нужно ждать, пока я опубликую следующую часть серии руководств на Dev.to или Medium. Вы можете просто двигаться дальше и продолжать обучение в своем собственном темпе — в любое время и в любом месте!

Эта книга будет постоянно обновляться для исправления недавно обнаруженных ошибок или устранения проблем совместимости с более новыми версиями Godot 4.