UI事件

昨天的UI无法点击暂时通过给上层Node添加PickingBehavior::IGNORE,来解决。 现在我们给UI加上点击实现, 实现方式很简单,在生成的地方,再加一个Observe用来发生事件

p.spawn((  
    UiImage::new(asset_server.load("icons/lamp.png")),  
    Node {  
        height: Val::Px(40.),  
        margin: UiRect::axes(Val::Px(0.), Val::Px(5.)),  
        ..default()  
    },  
    IdeaButton,  
))  
.observe(  
    |_trigger: Trigger<Pointer<Click>>, mut commands: Commands| {  
        commands.send_event(TogglePuzzleHint);  
    },  
);

全屏/取消全屏

在点击时, 将window的模式设置为全屏

.observe(  
    |_trigger: Trigger<Pointer<Click>>, mut window: Single<&mut Window>| {  
        window.mode = WindowMode::Fullscreen(MonitorSelection::Current);  
    },  
);

按下ESC取消全屏

fn exit_fullscreen_on_esc(mut window: Single<&mut Window>, input: Res<ButtonInput<KeyCode>>) {  
    if !window.focused {  
        return;  
    }  
  
    if input.just_pressed(KeyCode::Escape) {  
        window.mode = WindowMode::Windowed;  
    }  
}

重构随机打乱

将随机打乱分为两种, 一种是全窗口内的随机打乱, 一种是将拼图块贴着窗口边

  
#[derive(Event)]  
pub enum Shuffle {  
    Random,  
    Edge,  
}  
  
fn shuffle_pieces(  
    mut shuffle_events: EventReader<Shuffle>,  
    mut query: Query<(&Piece, &mut Transform)>,  
    window: Single<&Window>,  
    camera: Single<&OrthographicProjection, With<Camera2d>>,  
) {  
    for event in shuffle_events.read() {  
        match event {  
            Shuffle::Random => {  
                for (piece, mut transform) in &mut query.iter_mut() {  
                    let random_pos =  
                        random_position(&piece, window.resolution.size(), camera.scale);  
                    transform.translation = random_pos.extend(piece.index as f32);  
                }  
            }  
            Shuffle::Edge => {  
                for (piece, mut transform) in &mut query.iter_mut() {  
                    let edge_pos = edge_position(&piece, window.resolution.size(), camera.scale);  
                    transform.translation = edge_pos.extend(piece.index as f32);  
                }  
            }  
        }  
    }  
}

效果如下:

提示匹配拼图

将未合成的相邻两块拼图置为选中状态

  
#[derive(Event)]  
pub struct TogglePuzzleHint;  
  
fn handle_toggle_puzzle_hint(  
    mut event: EventReader<TogglePuzzleHint>,  
    selected_query: Query<Entity, With<Selected>>,  
    piece_query: Query<(Entity, &Piece, &MoveTogether), Without<Selected>>,  
    mut commands: Commands,  
) {  
    for _ in event.read() {  
        for entity in selected_query.iter() {  
            commands.entity(entity).remove::<Selected>();  
        }  
        let mut first_piece = None;  
        let mut first_entity = None;  
        let mut second_entity = None;  
        'f1: for (entity, piece, move_together) in piece_query.iter() {  
            if move_together.len() > 0 {  
                continue 'f1;  
            }  
            first_piece = Some(piece);  
            first_entity = Some(entity);  
            break 'f1;  
        }  
        if let Some(first_piece) = first_piece {  
            'f2: for (entity, piece, move_together) in piece_query.iter() {  
                if move_together.len() > 0 {  
                    continue 'f2;  
                }  
                if first_piece.beside(&piece) {  
                    second_entity = Some(entity);  
                    break 'f2;  
                }  
            }  
        }  
        if let (Some(first_entity), Some(second_entity)) = (first_entity, second_entity) {  
            commands.entity(first_entity).insert(Selected);  
            commands.entity(second_entity).insert(Selected);  
        }  
    }  
}

找出第一个单个的拼图, 然后找到它任意一个旁边的拼图,将他们都加入Selected组件

切换显示全部拼图和只有边缘的拼图

  
#[derive(Event)]  
pub struct ToggleEdgeHint;   
fn handle_puzzle_hint(  
    mut event: EventReader<ToggleEdgeHint>,  
    selected_query: Query<Entity, With<Selected>>,  
    mut piece_query: Query<(&Piece, &mut Visibility), Without<Selected>>,  
    mut commands: Commands,  
    mut show_all: Local<bool>,  
) {  
    for _ in event.read() {  
        for entity in selected_query.iter() {  
            commands.entity(entity).remove::<Selected>();  
        }  
        *show_all = !*show_all;  
  
        if *show_all {  
            for (_, mut visibility) in piece_query.iter_mut() {  
                *visibility = Visibility::Visible;  
            }  
        } else {  
            for (piece, mut visibility) in piece_query.iter_mut() {  
                if piece.is_edge() {  
                    *visibility = Visibility::Visible;  
                } else {  
                    *visibility = Visibility::Hidden;  
                }  
            }  
        }  
    }  
}

拆分系统

为了更好的游玩体验, 将系统状态拆分为 Menu(菜单), GamePlay(游玩)。 后续在Gameplay里还要拆分游戏状态。

/// 定义应用状态
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]  
pub enum AppState {  
    #[default]  
    Splash,  
    MainMenu,  
    Gameplay,  
}

Menu和Gameplay部分可以参考官方项目示例代码

fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {  
    let font = asset_server.load("fonts/MinecraftEvenings.ttf");  
    let text_font = TextFont {  
        font: font.clone(),  
        font_size: 50.0,  
        ..default()  
    };  
    let text_justification = JustifyText::Center;  
    // Display the logo  
    commands  
        .spawn((  
            Node {  
                align_items: AlignItems::Center,  
                justify_content: JustifyContent::Center,  
                width: Val::Percent(100.0),  
                height: Val::Percent(100.0),  
                ..default()  
            },  
            OnSplashScreen,  
        ))  
        .with_children(|parent| {  
            parent.spawn((  
                UiImage::new(asset_server.load("images/puzzle.jpg")),  
                Node {  
                    width: Val::Percent(100.0),  
                    ..default()  
                },  
            ));  
  
            parent.spawn((  
                Text::new("Jigsaw Puzzle"),  
                text_font.clone(),  
                TextLayout::new_with_justify(text_justification),  
                TextColor(BLACK.into()),  
                Node {  
                    position_type: PositionType::Absolute,  
                    left: Val::Px(100.0),  
                    bottom: Val::Px(100.0),  
                    ..default()  
                },  
            ));  
        });  
  
    // Insert the timer as a resource  
    commands.insert_resource(SplashTimer(Timer::from_seconds(3.0, TimerMode::Once)));  
}

我们读取一张背景图片铺在窗口里,然后在左下角显示游戏名称, 注意这里文字位置用了绝对定位。 显示效果