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

Серьезно. Это 100% ржавчина!

Предыдущая статья: Компиляция в iOS

Критерии

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

0. Создайте сетку квадратов

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

Для тех, кому любопытно, ядро ​​сетки сводится к пакету спрайтов, который я инкапсулировал как «TileBundle». На данный момент каждая плитка имеет произвольный цвет и позицию.

sprite: SpriteBundle {
    sprite: Sprite {
        color: settings.color,
        ..default()
    },
    ..default()
},
position: settings.position,
..default()

1. Создайте систему для обработки событий указателя

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

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

.add_system(tap_capture_system)
.add_system(click_capture_system)
.add_event::<MyPointerEvent>()
pub struct MyPointerEvent {
    pub position: Vec2,
}

2. Получить координату окна

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

fn click_capture_system(
    windows: Res<Windows>,
    mut tap_event: EventWriter<PointerEvent>,
    q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>
    // todo: add mouse button or tap
) {

    // assuming there is exactly one main camera entity, so query::single() is OK
    let (camera, camera_transform) = q_camera.single();

    // get the window that the camera is displaying to (or the primary window)
    let wnd = if let RenderTarget::Window(id) = camera.target       
    {
        windows.get(id).unwrap()
    } else {
        windows.get_primary().unwrap()
    };
}

3. Отправка общего события

Теперь мне нужно прослушивать события щелчка и касания в окне. Когда происходит какое-либо событие, я отправлю одно и то же событие с именем PointerEvent.

fn click_capture_system(
    // ...
    btn: Res<Input<MouseButton>>,
) {
    // check if the cursor is inside the window and get its position
    if let Some(screen_pos) = wnd.cursor_position() {
        if btn.just_released(MouseButton::Left) {
            debug!("hello click {}", screen_pos);
            tap_event.send(PointerEvent {
                position: screen_pos
            });
        }
    }
}

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

fn tap_capture_system(
    // ...
    touches: Res<Touches>,
) {
   for touch in touches.iter_just_released() {
        if touches.just_released(touch.id()) {
            debug!("hello tap {}", touch.position());
            tap_event.send(PointerEvent {
                position: touch.position()
            });
        }
    }
}

4. Прослушивание пользовательского события

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

fn handle_tile_pointer_events(
    mut events: EventReader<PointerEvent>,
) {
    for pointer_event in events.iter() {
       // do something
    }
}

5. Преобразование координат экрана в координаты игрового мира

В моем существующем коде есть проблема, которую мне потребовалось некоторое время, чтобы понять. Проблема в том, что приходится учитывать несколько систем координат. Окно/мобильное устройство имеет 2D-систему координат, а игра имеет 3D/2D-систему координат.

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

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

// get the size of the window
let window_size = Vec2::new(wnd.width() as f32, wnd.height() as f32);

// convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates)
let ndc = (INSERT_SCREEN_POSITION_HERE / window_size) * 2.0 - Vec2::ONE;

// matrix for undoing the projection and camera transform
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();

// use it to convert ndc to world-space coordinates
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));

// reduce it to a 2D value
let world_pos: Vec2 = world_pos.truncate();

6. Измените мировую позицию для iOS.

Еще одна проблема во время тестирования. Экран обнаруживал нажатия, но на противоположной стороне экрана.

Не уверен, что это изменится в будущем, но в настоящее время Беви создает перевернутую координату. Чтобы решить эту проблему, я перевернул мировую позицию по оси Y.

let world_pos: Vec2 = world_pos.truncate()
    // flip y axis so touches line up with screen
    * Vec2::new(1.0, -1.0);

7. Используйте многоразовое событие указателя

Я хочу изменить плитку, на которую нажали / нажали, на случайный цвет.

Все, что я делаю, это наблюдаю, пересекается ли щелчок/касание со спрайтом тайла. Чтобы быстро выполнить эту работу, мне пришлось вручную добавить nalgebra и parry2d в мой проект.

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

for e in events.iter() {
    for (mut tile, mut sprite, global, transform) in q.iter_mut() {
        tile.update();

        let pointer = point!(e.position.x, e.position.y);

        let pos = global.translation();

        let size = transform.scale;
        let bl = pos - (size / 2.0);
        let tr = pos + (size / 2.0);

        let square = [
            point!(bl.x, tr.y),
            point!(tr.x, tr.y),
            point!(tr.x, bl.y),
            point!(bl.x, bl.y),
        ];

        if point_in_poly2d(&pointer, &square) {
            sprite.color = random_color();
        }
    }
}

Заключение

Rust может обрабатывать собственные события касания iOS, а также события кликов. Самая сложная часть — это нормализация координат и отработка перегибов.

Продолжение следует

Пожалуйста, подумайте о том, чтобы оставить хлопки и оставить комментарий ниже. Это помогает мне спланировать следующую статью.

Кроме того, комментарии о том, над чем вы работаете, могут помочь или вдохновить других. Давайте сделаем разработку на Rust для iOS чем-то особенным!

Предыдущая статья: Компиляция в iOS