При обучении рекомендуется активно применять технологии ИИ, например Qwen
lesson_04_physics/project/ в Godotscenes/sandbox.tscn — физическая песочница со стенамиscenes/ball.tscn — шаблон мяча (RigidBody2D)scenes/box.tscn — шаблон ящика (RigidBody2D)scenes/trigger_zone.tscn — зона-триггер (Area2D)scripts/ со скелетным кодомЦель: понять поведение физических тел.
Откройте scenes/sandbox.tscn — вы увидите прямоугольное поле со стенами (StaticBody2D)
Добавьте в сцену несколько экземпляров ball.tscn:
Запустите (F6) — мячи должны упасть и отскочить от стен
Эксперименты (меняйте в инспекторе для разных мячей):
mass (1, 5, 20) — наблюдайте разницуgravity_scale (0, 0.5, 2.0)bounce = 0.9 (резиновый)bounce = 0.1 (тяжёлый)friction = 0.0 (скользкий)Добавьте несколько box.tscn — посмотрите, как ящики взаимодействуют с мячами
Цель: реализовать перетаскивание физических объектов — ключевая механика головоломок.
scripts/draggable_body.gdextends RigidBody2D
@export var grid_size: int = 64
@export var is_fixed: bool = false
var dragging: bool = false
var drag_offset: Vector2 = Vector2.ZERO
func _ready() -> void:
# Если объект фиксированный — замораживаем его сразу
if is_fixed:
freeze = true
func _input(event: InputEvent) -> void:
if is_fixed:
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
# TODO 1: Проверьте, что мышь над объектом (_is_mouse_over())
# Если да:
# - dragging = true
# - freeze = true (остановить физику)
# - drag_offset = global_position - event.position
pass
else:
if dragging:
# TODO 2: Завершить перетаскивание
# - dragging = false
# - Вызвать _snap_to_grid()
pass
if event is InputEventMouseMotion and dragging:
# TODO 3: Обновить позицию
# global_position = event.position + drag_offset
pass
func _snap_to_grid() -> void:
# TODO 4: Привязать позицию к сетке
# position.x = round(position.x / grid_size) * grid_size
# position.y = round(position.y / grid_size) * grid_size
pass
func _is_mouse_over() -> bool:
var mouse_pos := get_global_mouse_position()
var shape := $CollisionShape2D.shape
if shape is CircleShape2D:
return global_position.distance_to(mouse_pos) < shape.radius
elif shape is RectangleShape2D:
var rect := Rect2(global_position - shape.size / 2, shape.size)
return rect.has_point(mouse_pos)
return false
ball.tscn и box.tscn. Откройте их и убедитесь, что он подключёнЦель: построить физическую цепочку — прообраз уровня головоломки.
В sandbox.tscn постройте цепочку:
Шар (сверху)
→ падает на наклонную платформу
→ скатывается
→ попадает в Area2D (триггер)
→ триггер выводит "Победа!"
Добавьте наклонную платформу:
StaticBody2D + CollisionShape2D (RectangleShape2D)Настройте триггер:
scripts/trigger_zone.gdextends Area2D
signal triggered
var is_activated: bool = false
func _ready() -> void:
# TODO 1: Подключите сигнал body_entered
# body_entered.connect(_on_body_entered)
pass
func _on_body_entered(body: Node2D) -> void:
if is_activated:
return
# TODO 2: Отметить зону как активированную
# is_activated = true
# TODO 3: Изменить цвет (визуальная обратная связь)
# $Sprite2D.modulate = Color.GREEN
# TODO 4: Отправить сигнал triggered
# triggered.emit()
# TODO 5: Вывести сообщение
# print("Триггер активирован объектом: ", body.name)
pass
Запустите и проверьте цепочку.
Усложните: добавьте второй триггер и условие «оба должны быть активированы».
Цель: создать прототип одного элемента из вашего кейса.
Создайте источник колебаний:
extends Node2D
@export var amplitude: float = 50.0
@export var frequency: float = 2.0
@onready var line := $Line2D
var time: float = 0.0
func _process(delta: float) -> void:
time += delta
var points: PackedVector2Array = []
for i in range(200):
var x := float(i) * 3.0
var y := amplitude * sin(2.0 * PI * frequency * (time - x / 300.0))
points.append(Vector2(x, y))
line.points = points
Добавьте Node2D + Line2D в сцену, назначьте скрипт, запустите.
Создайте провод между двумя точками:
extends Node2D
@export var point_a: Vector2 = Vector2(100, 300)
@export var point_b: Vector2 = Vector2(400, 300)
@export var is_powered: bool = false
@onready var line := $Line2D
func _process(_delta: float) -> void:
line.points = [point_a, point_b]
line.default_color = Color.YELLOW if is_powered else Color.GRAY
line.width = 4.0 if is_powered else 2.0
Создайте луч света с отражением:
extends Node2D
@onready var ray := $RayCast2D
@onready var line := $Line2D
func _physics_process(_delta: float) -> void:
ray.force_raycast_update()
if ray.is_colliding():
var hit := ray.get_collision_point()
line.points = [Vector2.ZERO, to_local(hit)]
else:
line.points = [Vector2.ZERO, ray.target_position]
line.default_color = Color.YELLOW
Добавьте Node2D + RayCast2D (target = (500, 0)) + Line2D, назначьте скрипт.
Создайте магнит, притягивающий RigidBody2D:
extends StaticBody2D
@export var strength: float = 50000.0
@export var attract: bool = true
var affected: Array[RigidBody2D] = []
func _ready() -> void:
$Area2D.body_entered.connect(func(b): if b is RigidBody2D: affected.append(b))
$Area2D.body_exited.connect(func(b): affected.erase(b))
func _physics_process(_delta: float) -> void:
for body in affected:
var dir := global_position - body.global_position
var dist := dir.length()
if dist < 10.0:
continue
var force := dir.normalized() * strength / (dist * dist)
if not attract:
force = -force
body.apply_central_force(force)