Перейти к содержимому

Уроки · 29

Свои функции

Мы уже давно вызываем чужие функции: draw_circle, mouse_position, is_key_down. Они делают работу за нас, а мы только подставляем нужные числа. Теперь научимся писать такие же — свои.

В уроке «Турель стреляет» внутри loop снова и снова повторялся один и тот же кусок: из двух точек посчитать dx, dy, расстояние и разделить — получить направление, куда смотрит глазик. Тот же рецепт встречался и раньше — в уроках «Погоня за мышью», «Точная остановка» — но сейчас упростим именно ту программу с турелью: когда один и тот же код нужен много раз, ему удобно дать имя и вызывать по этому имени.

Объявление похоже на то, что мы уже видели у main, только у нашей функции есть вход и выход:

// Своя функция: по двум точкам считает направление от первой ко второй.
fn direction(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> (f32, f32) {
let dx = to_x - from_x;
let dy = to_y - from_y;
let distance = (dx * dx + dy * dy).sqrt();
if distance > 0.0 {
(dx / distance, dy / distance)
} else {
(1.0, 0.0)
}
}

Разберём по кусочкам. fn (сокращение от английского function) — тоже ключевое слово, как let: Rust понимает его как «дальше будет функция», и это слово не наше, а зарезервированное самим языком. После него — имя функции, здесь direction, а дальше в скобках — параметры: у каждого сначала имя, потом через двоеточие тип, а если параметров несколько — через запятую, как from_x: f32, from_y: f32 и так далее. После скобок -> — стрелка, отделяющая параметры от того, что функция возвращает: здесь (f32, f32), пара чисел. А само тело функции — в фигурных скобках, таких же, как у main и у loop.

Функция — это как станок: на входе сырьё (параметры), внутри — обработка, на выходе — готовая деталь. Эта готовая деталь и есть то, что функция возвращает. Мы вызываем direction(...) не ради самого вызова, а ради значения, которое он произведёт и отдаст нам обратно — именно его мы и кладём в direction_x, direction_y.

Кстати, .sqrt() в той же строке — тоже вызов функции, просто записанный по-другому: значение, потом точка, потом имя, а не имя и скобки сразу. Такую запись уже доводилось видеть — например, SPEED.min(distance) в уроке «Точная остановка» — и она значит то же самое, что min(SPEED, distance), просто SPEED написан перед точкой, а не в скобках. Такую функцию называют методом; как писать свои методы и откуда вообще берётся эта запись, разберём отдельно в уроке «Методы» — сейчас достаточно узнавать её на вид.

  • fn direction(from_x: f32, from_y: f32, to_x: f32, to_y: f32) — на вход не два значения, а четыре, и каждое — просто число (f32), без пометки «это точка». Первые два, from_x и from_y, — координаты первой точки, «откуда». Вторые два, to_x и to_y, — координаты второй точки, «куда». Одна точка — это всегда пара чисел, поэтому две точки на входе — это ровно четыре параметра.
  • -> (f32, f32) — та же самая пара чисел, что возвращала mouse_position(); здесь в неё попадают direction_x и direction_y.
  • В Rust есть специальное слово return — оно явно говорит: «здесь готовая деталь, отдаём её и на этом всё, дальше код в этой функции не выполняется». Но у него есть более короткий, неявный вариант: если последнее выражение в теле функции написано без точки с запятой в конце, оно само становится тем, что возвращается — как если бы перед ним стояло return, просто без лишнего слова.
  • Наша функция заканчивается не одной строкой, а целым if-else: у него не один конец, а два — тело if и тело else, и в каждом из них по одному выражению без точки с запятой. Какая бы ветка ни сработала — distance > 0.0 или нет — именно её значение и есть то, что вернёт direction: либо (dx / distance, dy / distance), либо (1.0, 0.0).
  • Внутри — та же математика, что в уроке «Направление»: вектор, длина, деление. Снаружи её больше не видно.
  • Если точки совпали, расстояния нет и направление не посчитать. Тогда просто берём направление по умолчанию — вправо (1.0, 0.0).

Как и константу в уроке «Константы», свою функцию объявляем вне main: ей не нужно ждать запуска игры, это просто рецепт, лежащий рядом с программой. В loop вместо длинного блока с sqrt остаётся вызов — как в уроке «Турель стреляет», только короче:

// Куда смотрит турель — направление от центра к курсору.
let (direction_x, direction_y) = direction(center_x, center_y, mouse_x, mouse_y);

В уроке «Турель стреляет» внутри loop было много строк подряд: центр экрана, курсор, dx и dy, расстояние, проверка «не ноль ли», деление — и только потом глазик, клик и пуля. Логика выстрела и математика направления были перемешаны в одном месте, и каждый кадр приходилось продираться через оба слоя сразу.

После выноса в direction в main остаётся сценарий кадра: где турель, где курсор, куда смотрит глазик, был ли клик, куда летит пуля, что нарисовать. В loop больше нет sqrt и деления — видно только, что делает игра прямо сейчас. Как считается направление, спрятано в функции с понятным именем: нужна математика — открываешь direction, нужен только ход программы — читаешь main сверху вниз.

Та же турель, что в уроке «Турель стреляет», но направление считает своя функция direction. Подвигай мышь, кликни для выстрела — скорость пули задаётся полем ниже:

src/main.rs
use macroquad::prelude::*;
// Скорость одна и та же на каждом кадре, поэтому это константа
// (как в уроке «Константы»), а не переменная внутри loop.
const SPEED: f32 = 10.0;
// Своя функция: по двум точкам считает направление от первой ко второй.
fn direction(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> (f32, f32) {
// Вектор от начальной точки к конечной.
let dx = to_x - from_x;
let dy = to_y - from_y;
// Длина этого вектора — расстояние между точками.
let distance = (dx * dx + dy * dy).sqrt();
// Точки не совпадают — можно посчитать настоящее направление.
if distance > 0.0 {
// Делим на длину — получаем направление длиной ровно 1.
(dx / distance, dy / distance)
} else {
// Точки совпали — направления нет, берём по умолчанию вправо.
(1.0, 0.0)
}
}
#[macroquad::main("Свои функции")]
async fn main() {
// bullet_x, bullet_y — положение пули в игровом мире. Пока не было
// ни одного клика, она стоит далеко за пределами экрана, и её
// просто не видно.
let mut bullet_x = -1000.0;
let mut bullet_y = -1000.0;
// bullet_direction_x, bullet_direction_y — направление, в котором
// движется пуля. Сначала оно нулевое: пуля никуда не летит.
let mut bullet_direction_x = 0.0;
let mut bullet_direction_y = 0.0;
loop {
clear_background(BLACK);
// Турель стоит в центре экрана.
let center_x = screen_width() / 2.0;
let center_y = screen_height() / 2.0;
// Координаты курсора.
let (mouse_x, mouse_y) = mouse_position();
// Направление от центра к курсору: туда смотрит турель и туда же
// полетит пуля, если сейчас выстрелить.
let (direction_x, direction_y) = direction(center_x, center_y, mouse_x, mouse_y);
// Глазик — точка плюс вектор (как в уроке «Точка плюс вектор»):
// центр турели плюс направление, растянутое на 25 пикселей.
let eye_x = center_x + direction_x * 25.0;
let eye_y = center_y + direction_y * 25.0;
// Кнопка мыши только что нажата — новый выстрел.
if is_mouse_button_pressed(MouseButton::Left) {
// Переносим пулю в положение глазика.
bullet_x = eye_x;
bullet_y = eye_y;
// Устанавливаем её направление в то, что вычислили выше.
bullet_direction_x = direction_x;
bullet_direction_y = direction_y;
}
// Пуля летит своим зафиксированным направлением, не подстраиваясь
// под курсор — в отличие от урока «Погоня за мышью».
bullet_x += bullet_direction_x * SPEED;
bullet_y += bullet_direction_y * SPEED;
// Тело турели.
draw_circle(center_x, center_y, 40.0, BLUE);
// Глазик — смотрит на курсор.
draw_circle(eye_x, eye_y, 10.0, WHITE);
// Пуля — рисуется всегда, просто до первого клика она далеко
// за экраном и её не видно.
draw_circle(bullet_x, bullet_y, 8.0, YELLOW);
next_frame().await;
}
}
  • fn direction(...) — наша функция: принимает четыре числа (координаты двух точек), возвращает направление от первой точки ко второй длиной ровно 1.
  • dx, dy и distance живут только внутри функции. В main их больше нет — снаружи нужно только готовое направление, и сам loop стал короче: меньше деталей на каждом кадре, проще увидеть порядок действий.
  • Если distance равна нулю, функция возвращает (1.0, 0.0): мы просто решили, что когда направление не посчитать, будет направление по умолчанию — вправо. Поэтому в loop отдельная проверка «курсор не в центре» больше не нужна: она уже внутри функции.
  • bullet_x, bullet_y — положение пули в игровом мире. Пока не было клика, пуля стоит далеко за экраном (-1000.0), и её не видно.
  • bullet_direction_x, bullet_direction_y — направление, в котором движется пуля. Сначала оно нулевое, потому что пуля никуда не летит.
  • direction_x, direction_y — то, что вернула direction. Это не только то, куда смотрит турель, но и то, куда полетит пуля при выстреле.
  • При клике мышью (is_mouse_button_pressed) мы обновляем пулю: ставим её в глазик и копируем в bullet_direction_x, bullet_direction_y текущее направление. Делаем это именно при клике, потому что выстрел случается один раз, и дальше пуля должна лететь по прямой, а не доворачивать за курсором — так же, как в уроке «Турель стреляет».
  • SPEED по-прежнему константа вне main, как в уроке «Константы».