Уроки · 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);main читать проще
Заголовок раздела «main читать проще»В уроке «Турель стреляет» внутри loop было много строк подряд:
центр экрана, курсор, dx и dy, расстояние, проверка «не ноль ли»,
деление — и только потом глазик, клик и пуля. Логика выстрела и
математика направления были перемешаны в одном месте, и каждый кадр
приходилось продираться через оба слоя сразу.
После выноса в direction в main остаётся сценарий кадра: где
турель, где курсор, куда смотрит глазик, был ли клик, куда летит
пуля, что нарисовать. В loop больше нет sqrt и деления — видно
только, что делает игра прямо сейчас. Как считается
направление, спрятано в функции с понятным именем: нужна математика —
открываешь direction, нужен только ход программы — читаешь main
сверху вниз.
Попробуй сам
Заголовок раздела «Попробуй сам»Та же турель, что в уроке «Турель стреляет», но направление
считает своя функция direction. Подвигай мышь, кликни
для выстрела — скорость пули задаётся полем ниже:
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, как в уроке «Константы».