Найти в Дзене

Разработка игр в Godot с использованием Python-подобного GDScript

Godot — это мощный открытый игровой движок, который предлагает гибкую систему разработки и простой в изучении язык сценариев GDScript. Хотя GDScript синтаксически похож на Python, это отдельный язык, созданный специально для Godot. В этой статье мы подробно разберем основы работы с Godot и GDScript с практическими примерами. Прежде чем начать, скачайте Godot с официального сайта (https://godotengine.org/). Версия 4.x предоставляет самые современные функции. После запуска Godot создайте новый проект: 1. Выберите "New Project" 2. Укажите имя проекта и путь 3. Выберите рендерер (Vulkan или OpenGL) 4. Нажмите "Create & Edit" GDScript имеет синтаксис, похожий на Python, но с некоторыми особенностями. Рассмотрим основные элементы: # Объявление переменных var number = 10 # Тип определяется автоматически var name: String = "Player" # Явное указание типа var health := 100 # Тип выводится из значения # Константы const MAX_SPEED = 200 # Перечисление (enum) enum Direction {NORTH, SOUTH, EAST,
Оглавление

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

Установка и настройка Godot

Прежде чем начать, скачайте Godot с официального сайта (https://godotengine.org/). Версия 4.x предоставляет самые современные функции.

После запуска Godot создайте новый проект:

1. Выберите "New Project"

2. Укажите имя проекта и путь

3. Выберите рендерер (Vulkan или OpenGL)

4. Нажмите "Create & Edit"

Основы GDScript

GDScript имеет синтаксис, похожий на Python, но с некоторыми особенностями. Рассмотрим основные элементы:

Переменные и типы данных

# Объявление переменных
var number = 10 # Тип определяется автоматически
var name: String = "Player" # Явное указание типа
var health := 100 # Тип выводится из значения
# Константы
const MAX_SPEED = 200
# Перечисление (enum)
enum Direction {NORTH, SOUTH, EAST, WEST}
var current_direction = Direction.NORTH
# Массивы
var items = ["sword", "potion", "shield"]
var numbers: Array[int] = [1, 2, 3]
# Словари
var player_stats = {"health": 100, "mana": 50, "level": 1}

Функции

# Определение функции
func calculate_damage(base_damage: float, multiplier: float = 1.0) -> float:
....var total_damage = base_damage * multiplier
....return total_damage
# Функция с несколькими параметрами
func move_character(x: float, y: float, speed: float) -> Vector2:
....return Vector2(x, y) * speed
# Переопределение встроенных функций
func _ready():
....# Вызывается при инициализации объекта
....print("Объект готов!")
....# Вызов собственной функции
....var damage = calculate_damage(25.0, 1.5)
....print("Нанесен урон: ", damage)

Создание первого персонажа

Давайте создадим простого 2D-персонажа, который может перемещаться по сцене.

Настройка сцены

1. Создайте новую 2D-сцену (Scene → New Scene → 2D Scene)

2. Сохраните её как "Player.tscn"

3. Добавьте Node2D как корневой узел

4. Добавьте дочерний Sprite2D и назначьте ему текстуру

5. Добавьте CollisionShape2D для обработки столкновений

Скрипт персонажа

Присоедините к корневому узлу скрипт:

extends Node2D
# Экспортируемые переменные (видны в редакторе)
@export var speed: float = 300.0
@export var jump_force: float = 500.0
@export var gravity: float = 980.0
# Переменные персонажа
var velocity: Vector2 = Vector2.ZERO
var is_on_floor: bool = false
# Ссылки на дочерние узлы
@onready var sprite = $Sprite2D
@onready var collision_shape = $CollisionShape2D
func _ready():
....print("Персонаж загружен!")
....# Инициализация может быть здесь
func _process(delta):
....# Вызывается каждый кадр
....handle_input()
....apply_gravity(delta)
....move_character(delta)
func handle_input():
....# Обработка горизонтального движения
....var horizontal_input = Input.get_axis("move_left", "move_right")
....velocity.x = horizontal_input * speed
....# Обработка прыжка
....if Input.is_action_just_pressed("jump") and is_on_floor:
........velocity.y = -jump_force
........is_on_floor = false
func apply_gravity(delta):
....if not is_on_floor:
........velocity.y += gravity * delta
func move_character(delta):
....# Move_and_slide обрабатывает столкновения автоматически
....# Для этого нужно использовать CharacterBody2D или аналогичный
....position += velocity * delta
....# Ограничение скорости падения
....if velocity.y > 1000:
........velocity.y = 1000
# Обработка столкновений
func _on_body_entered(body):
....if body.is_in_group("floor"):
........is_on_floor = true
........velocity.y = 0

Создание игрового уровня

Теперь создадим простой уровень с платформами.

Настройка тайлсета

1. Создайте новый TileMap node

2. Создайте или импортируйте tileset (набор тайлов)

3. Настройте collision shapes для тайлов

Скрипт для управления уровнем

extends Node2D
@onready var player = $Player
@onready var camera = $Camera2D
@onready var tile_map = $TileMap
func _ready():
....# Генерация уровня
....generate_level()
....# Настройка камеры для следования за игроком
....if camera:
........camera.make_current()
func generate_level():
....# Простая процедурная генерация платформ
....var platform_length = 20
....var height_variation = 100
....for i in range(platform_length):
........# Основание уровня
........tile_map.set_cell(0, Vector2i(i, 10), 0, Vector2i(0, 0))
........# Случайные платформы
........if randf() > 0.7:
........var height = randi() % 3 + 1
........for j in range(height):
............tile_map.set_cell(0, Vector2i(i, 9 - j), 0, Vector2i(1, 0))
func _process(delta):
....# Обновление камеры для следования за игроком
....if camera and player:
........camera.position = player.position

Создание врагов и ИИ

Добавим простых врагов, которые перемещаются по платформам.

Скрипт врага

extends CharacterBody2D
@export var patrol_speed: float = 100.0
@export var chase_speed: float = 200.0
@export var detection_range: float = 300.0
enum State {PATROL, CHASE, ATTACK}
var current_state: State = State.PATROL
var patrol_direction: Vector2 = Vector2.RIGHT
var player_ref: Node2D = null
@onready var ray_cast = $RayCast2D
@onready var animation_player = $AnimationPlayer
func _ready():
....# Начинаем с патрулирования
....set_state(State.PATROL)
func _physics_process(delta):
....match current_state:
........State.PATROL:
............patrol(delta)
........State.CHASE:
............chase(delta)
........State.ATTACK:
............attack(delta)
....move_and_slide()
func patrol(delta):
....velocity = patrol_direction * patrol_speed
....# Проверяем, нужно ли развернуться
....if not ray_cast.is_colliding():
........patrol_direction *= -1
........scale.x *= -1 # Отражаем спрайт
....# Проверяем, виден ли игрок
....detect_player()
func detect_player():
....var space_state = get_world_2d().direct_space_state
....var query = PhysicsRayQueryParameters2D.create(
........global_position,
........get_tree().get_first_node_in_group("player").global_position
....)
....query.exclude = [self]
....var result = space_state.intersect_ray(query)
....if result:
........if result.collider.is_in_group("player"):
............var distance = global_position.distance_to(result.collider.global_position)
............if distance <= detection_range:
................player_ref = result.collider
................set_state(State.CHASE)
func chase(delta):
....if not player_ref:
........set_state(State.PATROL)
........return
....var direction = (player_ref.global_position - global_position).normalized()
....velocity = direction * chase_speed
....# Поворачиваем спрайт в направлении движения
....if direction.x > 0:
........scale.x = abs(scale.x)
....elif direction.x < 0:
........scale.x = -abs(scale.x)
....# Если близко к игроку - атакуем
....if global_position.distance_to(player_ref.global_position) < 50:
........set_state(State.ATTACK)
....# Если игрок ушел далеко - возвращаемся к патрулированию
....if global_position.distance_to(player_ref.global_position) > detection_range * 1.5:
........set_state(State.PATROL)
func attack(delta):
....velocity = Vector2.ZERO
....if player_ref and global_position.distance_to(player_ref.global_position) > 70:
........set_state(State.CHASE)
....elif not player_ref:
........set_state(State.PATROL)
func set_state(new_state: State):
....current_state = new_state
....match current_state:
........State.PATROL:
............animation_player.play("walk")
........State.CHASE:
............animation_player.play("run")
........State.ATTACK:
............animation_player.play("attack")

Система здоровья и урона

Реализуем систему здоровья для игрока и врагов.

Базовый класс для всех живых существ

# HealthComponent.gd
class_name HealthComponent
extends Node
signal health_changed(old_value, new_value)
signal health_depleted
@export var max_health: float = 100.0
var current_health: float
func _ready():
....current_health = max_health
func take_damage(amount: float):
....var old_health = current_health
....current_health = max(0, current_health - amount)
....emit_signal("health_changed", old_health, current_health)
....if current_health <= 0:
........emit_signal("health_depleted")
func heal(amount: float):
....var old_health = current_health
....current_health = min(max_health, current_health + amount)
....emit_signal("health_changed", old_health, current_health)
func get_health_percentage() -> float:
....return current_health / max_health

Модифицированный скрипт игрока с поддержкой здоровья

extends CharacterBody2D
# ... предыдущий код ...
var health_component: HealthComponent
func _ready():
....# Инициализация компонента здоровья
....health_component = HealthComponent.new()
....health_component.max_health = 100
....add_child(health_component)
....# Подключаем сигналы
....health_component.health_changed.connect(_on_health_changed)
....health_component.health_depleted.connect(_on_health_depleted)
....print("Персонаж загружен!")
func _on_health_changed(old_value, new_value):
....print("Здоровье изменилось: ", old_value, " -> ", new_value)
....# Здесь можно обновить UI здоровья
func _on_health_depleted():
....print("Игрок умер!")
....# Воспроизвести анимацию смерти
....# Перезапустить уровень или показать экран Game Over
func take_damage(amount: float, source: Node = null):
....health_component.take_damage(amount)
....# Эффект получения урона (мигание спрайта)
....var tween = create_tween()
....tween.tween_property(sprite, "modulate", Color.RED, 0.1)
....tween.tween_property(sprite, "modulate", Color.WHITE, 0.1)

Пользовательский интерфейс

Создадим простой UI для отображения здоровья и счета.

Скрипт UI

extends CanvasLayer
@onready var health_bar = $Control/HealthBar
@onready var score_label = $Control/ScoreLabel
var player: Node2D
var score: int = 0
func _ready():
....# Ищем игрока в дереве сцены
....player = get_tree().get_first_node_in_group("player")
....if player and player.has_node("HealthComponent"):
........var health_component = player.get_node("HealthComponent")
........health_component.health_changed.connect(update_health_bar)
........# Инициализируем полосу здоровья
........health_bar.max_value = health_component.max_health
........health_bar.value = health_component.current_health
....update_score(0)
func update_health_bar(old_value, new_value):
....health_bar.value = new_value
....# Анимация изменения здоровья
....var tween = create_tween()
....tween.tween_property(health_bar, "value", new_value, 0.3)
func update_score(value: int):
....score += value
....score_label.text = "Счет: %d" % score
....# Анимация изменения счета
....var tween = create_tween()
....tween.tween_property(score_label, "scale", Vector2(1.2, 1.2), 0.1)
....tween.tween_property(score_label, "scale", Vector2(1.0, 1.0), 0.1)

Сохранение и загрузка игры

Реализуем систему сохранения прогресса.

# SaveSystem.gd
class_name SaveSystem
extends Node
const SAVE_PATH = "user://savegame.dat"
func save_game():
....var save_data = {
........"player": {
........"position": {
............"x": get_tree().get_first_node_in_group("player").global_position.x,
............."y": get_tree().get_first_node_in_group("player").global_position.y
........},
........"health": ............get_tree().get_first_node_in_group("player").get_node("HealthComponent").current_health,
........"score": get_tree().get_first_node_in_group("ui").score
........},
........"level": get_tree().current_scene.name,
........"timestamp": Time.get_unix_time_from_system()
....}
....var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
....if file:
........file.store_var(save_data)
........file.close()
........print("Игра сохранена!")
....else:
........push_error("Ошибка сохранения игры: ", FileAccess.get_open_error())
func load_game():
....if not FileAccess.file_exists(SAVE_PATH):
........push_error("Файл сохранения не существует!")
........return false
....var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
....if file:
........var save_data = file.get_var()
........file.close()
....# Загружаем данные
....if save_data.has("level"):
........# Переходим на нужный уровень
........get_tree().change_scene_to_file("res://Levels/%s.tscn" % save_data["level"])
........# Ждем завершения загрузки уровня
........await get_tree().process_frame
........# Восстанавливаем состояние игрока
........var player = get_tree().get_first_node_in_group("player")
........if player and save_data["player"].has("position"):
............player.global_position = Vector2(
................save_data["player"]["position"]["x"],
................save_data["player"]["position"]["y"]
........)
........if player and player.has_node("HealthComponent") and ............save_data["player"].has("health"):
............player.get_node("HealthComponent").current_health = save_data["player"]["health"]
............if save_data["player"].has("score"):
................get_tree().get_first_node_in_group("ui").score = save_data["player"]["score"]
................get_tree().get_first_node_in_group("ui").update_score(0)
........print("Игра загружена!")
........return true
....else:
........push_error("Ошибка загрузки игры: ", FileAccess.get_open_error())
........return false

Заключение

В этой статье мы рассмотрели основные аспекты разработки игр в Godot с использованием GDScript. Мы изучили:

1. Основы синтаксиса GDScript

2. Создание и управление персонажем

3. Построение игрового уровня

4. Реализацию врагов с простым ИИ

5. Систему здоровья и урона

6. Создание пользовательского интерфейса

7. Систему сохранения и загрузки игры

Godot предлагает мощные инструменты для разработки игр, а GDScript, благодаря своему Python-подобному синтаксису, достаточно прост для изучения. Этот движок отлично подходит как для начинающих, так и для опытных разработчиков.

Не бойтесь экспериментировать и изучать документацию Godot — она содержит множество полезных примеров и руководств. Удачи в разработке ваших игр!