basic/
basic.rs

1use bevy::{
2    input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ButtonState},
3    math::Vec3Swizzles,
4    prelude::*,
5    utils::HashMap,
6};
7use bevy::window::PrimaryWindow;
8use bevy_match3::prelude::*;
9
10const GEM_SIDE_LENGTH: f32 = 50.0;
11
12fn main() {
13    App::new()
14        .add_plugins(
15            DefaultPlugins
16                .set(WindowPlugin {
17                    primary_window: Some(Window {
18                        resizable: false,
19                        title: "bevy_match3 basic example".to_string(),
20                        ..default()
21                    }),
22                    ..default()
23                }),
24        )
25        .insert_resource(Selection::default())
26        .add_plugins(Match3Plugin)
27        .add_systems(Startup, setup_graphics)
28        .add_systems(Update, (
29            move_to,
30            consume_events,
31            input,
32            visualize_selection,
33            control,
34            animate_once,
35            shuffle))
36        .run();
37}
38
39#[derive(Component, Clone)]
40struct VisibleBoard(HashMap<UVec2, Entity>);
41
42#[derive(Component)]
43struct MainCamera;
44
45fn setup_graphics(mut commands: Commands, board: Res<Board>, asset_server: Res<AssetServer>) {
46    let board_side_length = GEM_SIDE_LENGTH * 10.0;
47    let centered_offset_x = board_side_length / 2.0 - GEM_SIDE_LENGTH / 2.0;
48    let centered_offset_y = board_side_length / 2.0 - GEM_SIDE_LENGTH / 2.0;
49
50    let mut camera = Camera2dBundle::default();
51    camera.transform = Transform::from_xyz(
52        centered_offset_x,
53        0.0 - centered_offset_y,
54        camera.transform.translation.z,
55    );
56    commands.spawn(camera).insert(MainCamera);
57
58    let mut gems = HashMap::default();
59
60    let vis_board = commands.spawn(SpatialBundle::default()).id();
61
62    board.iter().for_each(|(position, typ)| {
63        let transform = Transform::from_xyz(
64            position.x as f32 * GEM_SIDE_LENGTH,
65            position.y as f32 * -GEM_SIDE_LENGTH,
66            0.0,
67        );
68
69        let child = commands
70            .spawn(SpriteBundle {
71                sprite: Sprite {
72                    custom_size: Some(Vec2::new(GEM_SIDE_LENGTH, GEM_SIDE_LENGTH)),
73                    ..Sprite::default()
74                },
75                transform,
76                texture: asset_server.load(&map_type_to_path(*typ)),
77                ..SpriteBundle::default()
78            })
79            .insert(Name::new(format!("{};{}", position.x, position.y)))
80            .id();
81        gems.insert(*position, child);
82        commands.entity(vis_board).add_child(child);
83    });
84
85
86    let board = VisibleBoard(gems);
87
88    commands.entity(vis_board).insert(board);
89}
90
91fn map_type_to_path(typ: u32) -> String {
92    format!("{typ}.png")
93}
94
95#[derive(Component)]
96struct MoveTo(Vec2);
97
98fn move_to(
99    mut commands: Commands,
100    time: Res<Time>,
101    mut moves: Query<(Entity, &mut Transform, &MoveTo)>,
102) {
103    for (entity, mut transform, MoveTo(move_to)) in moves.iter_mut() {
104        if transform.translation == Vec3::new(move_to.x, move_to.y, transform.translation.z) {
105            commands.entity(entity).remove::<MoveTo>();
106        } else {
107            let mut movement = *move_to - transform.translation.xy();
108            movement = // Multiplying the move by GEM_SIDE_LENGTH as well as delta seconds means the animation takes exactly 1 second
109                (movement.normalize() * time.delta_seconds() * GEM_SIDE_LENGTH * 5.0).clamp_length_max(movement.length());
110            let movement = movement.extend(transform.translation.z);
111            transform.translation += movement;
112        }
113    }
114}
115
116fn consume_events(
117    mut commands: Commands,
118    mut events: ResMut<BoardEvents>,
119    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
120    mut board_commands: ResMut<BoardCommands>,
121    ass: Res<AssetServer>,
122    mut board: Query<(Entity, &mut VisibleBoard)>,
123    animations: Query<(), With<MoveTo>>,
124) {
125    if animations.iter().count() == 0 {
126        if let Ok(event) = events.pop() {
127            let (board_entity, mut board) = board.single_mut();
128            match event {
129                BoardEvent::Swapped(pos1, pos2) => {
130                    let gem1 = board.0.get(&pos1).copied().unwrap();
131                    let gem2 = board.0.get(&pos2).copied().unwrap();
132                    commands
133                        .entity(gem1)
134                        .insert(MoveTo(board_pos_to_world_pos(&pos2)));
135
136                    commands
137                        .entity(gem2)
138                        .insert(MoveTo(board_pos_to_world_pos(&pos1)));
139
140                    board.0.insert(pos2, gem1);
141                    board.0.insert(pos1, gem2);
142                }
143                BoardEvent::Popped(pos) => {
144                    let gem = board.0.get(&pos).copied().unwrap();
145                    board.0.remove(&pos);
146                    commands.entity(gem).despawn_recursive();
147                    spawn_explosion(
148                        &ass,
149                        &mut texture_atlases,
150                        &mut commands,
151                        &board_pos_to_world_pos(&pos),
152                    );
153                }
154                BoardEvent::Matched(matches) => {
155                    board_commands
156                        .push(BoardCommand::Pop(
157                            matches.without_duplicates().iter().copied().collect(),
158                        ))
159                        .unwrap();
160                }
161                BoardEvent::Dropped(drops) => {
162                    // Need to keep a buffered board clone because we read and write at the same time
163                    let mut new_board = board.clone();
164                    for Drop { from, to } in drops {
165                        let gem = board.0.get(&from).copied().unwrap();
166                        new_board.0.insert(to, gem);
167                        new_board.0.remove(&from);
168                        commands
169                            .entity(gem)
170                            .insert(MoveTo(board_pos_to_world_pos(&to)));
171                    }
172                    // And copy the buffer to the resource
173                    *board = new_board;
174                }
175                BoardEvent::Spawned(spawns) => {
176                    let mut new_board = board.clone();
177
178                    for (pos, typ) in spawns {
179                        let world_pos = board_pos_to_world_pos(&pos);
180                        let gem = commands
181                            .spawn(SpriteBundle {
182                                texture: ass.load(&map_type_to_path(typ)),
183                                transform: Transform::from_xyz(world_pos.x, 200.0, 0.0),
184                                sprite: Sprite {
185                                    custom_size: Some([50.0, 50.0].into()),
186                                    ..Sprite::default()
187                                },
188                                ..SpriteBundle::default()
189                            })
190                            .insert(MoveTo(world_pos))
191                            .id();
192                        new_board.0.insert(pos, gem);
193                        commands.entity(board_entity).add_child(gem);
194                    }
195                    *board = new_board;
196                }
197                BoardEvent::Shuffled(moves) => {
198                    let mut temp_board = board.clone();
199                    for (from, to) in moves {
200                        let gem = board.0.get(&from).copied().unwrap();
201
202                        commands
203                            .entity(gem)
204                            .insert(MoveTo(board_pos_to_world_pos(&to)));
205
206                        temp_board.0.insert(to, gem);
207                    }
208                    *board = temp_board;
209                }
210                _ => {
211                    println!("Received unimplemented event");
212                }
213            }
214        }
215    }
216}
217
218fn board_pos_to_world_pos(pos: &UVec2) -> Vec2 {
219    Vec2::new(
220        pos.x as f32 * GEM_SIDE_LENGTH,
221        -(pos.y as f32) * GEM_SIDE_LENGTH,
222    )
223}
224
225#[derive(Default, Clone, Copy, Resource)]
226struct Selection(Option<Entity>);
227
228fn input(
229    window_query: Query<&Window, With<PrimaryWindow>>,
230    mut selection: ResMut<Selection>,
231    mut button_events: EventReader<MouseButtonInput>,
232    camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
233    board: Query<&VisibleBoard>,
234) {
235    for event in button_events.read() {
236        if let MouseButtonInput {
237            button: MouseButton::Left,
238            state: ButtonState::Pressed,
239            ..
240        } = event
241        {
242            let window = window_query.single();
243            let (camera, camera_transform) = camera.single();
244            if let Some(world_position) = window.cursor_position()
245                .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
246                .map(|ray| ray.origin.truncate())
247            {
248                // round down to the gem coordinate
249                let coordinates: IVec2 = (
250                    ((world_position.x + GEM_SIDE_LENGTH / 2.0) / GEM_SIDE_LENGTH) as i32,
251                    ((GEM_SIDE_LENGTH / 2.0 - world_position.y) / GEM_SIDE_LENGTH) as i32,
252                )
253                    .into();
254
255                if coordinates.x >= 0 && coordinates.y >= 0 {
256                    selection.0 = board
257                        .single()
258                        .0
259                        .get::<UVec2>(&UVec2::new(coordinates.x as u32, coordinates.y as u32))
260                        .copied();
261                }
262            }
263        }
264    }
265}
266
267#[derive(Component)]
268struct SelectionRectangle;
269
270fn visualize_selection(
271    mut commands: Commands,
272    selection: Res<Selection>,
273    ass: Res<AssetServer>,
274    g_transforms: Query<&GlobalTransform>,
275    mut rectangle: Query<(Entity, &mut Transform), With<SelectionRectangle>>,
276) {
277    if selection.is_changed() {
278        if let Some(selected_gem) = selection.0 {
279            let transform = g_transforms.get(selected_gem).unwrap();
280            if let Ok((_, mut old_transform)) = rectangle.get_single_mut() {
281                *old_transform = (*transform).into();
282            } else {
283                commands
284                    .spawn(SpriteBundle {
285                        texture: ass.load("rectangle.png"),
286                        sprite: Sprite {
287                            custom_size: Some([50.0, 50.0].into()),
288                            ..Sprite::default()
289                        },
290                        transform: (*transform).into(),
291                        ..SpriteBundle::default()
292                    })
293                    .insert(SelectionRectangle);
294            }
295        } else if let Ok((entity, _)) = rectangle.get_single_mut() {
296            commands.entity(entity).despawn();
297        }
298    }
299}
300
301fn control(
302    mut board_commands: ResMut<BoardCommands>,
303    mut selection: ResMut<Selection>,
304    mut last_selection: Local<Selection>,
305    transforms: Query<&Transform>,
306) {
307    if selection.is_changed() {
308        if let Some(selected_gem) = selection.0 {
309            if let Some(last_selected_gem) = last_selection.0 {
310                let selected_pos = transforms.get(selected_gem).unwrap().translation.xy() / 50.0;
311                let last_selected_pos =
312                    transforms.get(last_selected_gem as Entity).unwrap().translation.xy() / 50.0;
313
314                board_commands
315                    .push(BoardCommand::Swap(
316                        [selected_pos.x as u32, -selected_pos.y as u32].into(),
317                        [last_selected_pos.x as u32, -last_selected_pos.y as u32].into(),
318                    ))
319                    .map_err(|err| println!("{err}"))
320                    .unwrap();
321                selection.0 = None;
322                last_selection.0 = None;
323            } else {
324                *last_selection = *selection;
325            }
326        } else {
327            last_selection.0 = None
328        }
329    }
330}
331
332#[derive(Component)]
333struct AnimationTimer(Timer);
334
335fn animate_once(
336    mut commands: Commands,
337    time: Res<Time>,
338    texture_atlases: Res<Assets<TextureAtlas>>,
339    mut timers: Query<(
340        Entity,
341        &mut AnimationTimer,
342        &mut TextureAtlasSprite,
343        &Handle<TextureAtlas>,
344    )>,
345) {
346    for (entity, mut timer, mut sprite, texture_atlas_handle) in timers.iter_mut() {
347        timer.0.tick(time.delta());
348        if timer.0.just_finished() {
349            let texture_atlas = texture_atlases.get(texture_atlas_handle).unwrap();
350            if sprite.index == 3 {
351                commands.entity(entity).despawn_recursive();
352            } else {
353                sprite.index = (sprite.index + 1) % texture_atlas.textures.len();
354            }
355        }
356    }
357}
358
359fn spawn_explosion(
360    ass: &AssetServer,
361    texture_atlases: &mut Assets<TextureAtlas>,
362    commands: &mut Commands,
363    pos: &Vec2,
364) {
365    let texture_handle = ass.load("explosion.png");
366    let texture_atlas =
367        TextureAtlas::from_grid(texture_handle, Vec2::new(49.0, 50.0), 4, 1, None, None);
368    let texture_atlas_handle = texture_atlases.add(texture_atlas);
369    commands
370        .spawn(SpriteSheetBundle {
371            texture_atlas: texture_atlas_handle,
372            transform: Transform::from_translation(pos.extend(0.0)),
373            ..SpriteSheetBundle::default()
374        })
375        .insert(AnimationTimer(Timer::from_seconds(
376            0.1,
377            TimerMode::Repeating,
378        )));
379}
380
381fn shuffle(
382    mut board_commands: ResMut<BoardCommands>,
383    mut key_event: EventReader<KeyboardInput>,
384    animations: Query<(), With<MoveTo>>,
385) {
386    if animations.iter().count() == 0 {
387        for event in key_event.read() {
388            if let KeyboardInput {
389                key_code: Some(KeyCode::S),
390                state: ButtonState::Pressed,
391                ..
392            } = event
393            {
394                board_commands.push(BoardCommand::Shuffle).unwrap();
395            }
396        }
397    }
398}