1 The Story of ggpacman
It started on a Saturday evening …
It was the 21st of March (for the sake of precision), around 10 pm CET (also for the sake of precision and mostly because it is not relevant). I was playing around with my data on ‘all’ the movies I have seen so far (mcanouil/imdb-ratings) and looking on possibly new ideas of visualisation on twitter using #ggplot2
and #gganimate
(by the way the first time I played with gganimate
was at useR-2018 (Brisbane, Australia), just before and when @thomasp85 released the actual framework). The only thing on the feed was “contaminated/deaths and covid-19” curves made with ggplot2
and a few with gganimate
… Let’s say, it was not as funny and interesting as I was hoping for … Then, I’ve got an idea, what if I can do something funny and not expected with ggplot2
and gganimate
? My first thought, was let’s draw and animate Pac-Man, that should not be that hard!
Well, it was not that easy after-all … But, I am going to go through my code here (you might be interested to actually look at the commits history.
2 The R packages
3 The maze layer
3.1 The base layer
First thing first, I needed to set-up the base layer, meaning, the maze from Pac-Man. I did start by setting the coordinates of the maze.
For later use, I defined some scales (actually those scales, where defined way after chronologically speaking). I am using those to define sizes and colours for all the geometries I am going to use to achieve the Pac-Man GIF.
map_colours <- c(
"READY!" = "goldenrod1",
"wall" = "dodgerblue3", "door" = "dodgerblue3",
"normal" = "goldenrod1", "big" = "goldenrod1", "eaten" = "black",
"Pac-Man" = "yellow",
"eye" = "white", "iris" = "black",
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
"Pinky" = "pink", "Pinky_weak" = "blue", "Pinky_eaten" = "transparent",
"Inky" = "cyan", "Inky_weak" = "blue", "Inky_eaten" = "transparent",
"Clyde" = "orange", "Clyde_weak" = "blue", "Clyde_eaten" = "transparent"
)
My base_layer
here is not really helpful, so I temporarily added some elements to help me draw everything on it. Note: I won’t use it in the following.
base_layer +
scale_x_continuous(breaks = 0:21, sec.axis = dup_axis()) +
scale_y_continuous(breaks = 0:26, sec.axis = dup_axis()) +
theme(
panel.grid.major = element_line(colour = "white"),
axis.text = element_text(colour = "white")
) +
annotate("rect", xmin = 0, xmax = 21, ymin = 0, ymax = 26, fill = NA)
Quite better, isn’t it?!
3.2 The grid layer
Here, I am calling “grid”, the walls of the maze. For this grid, I started drawing the vertical lines on the left side of the maze (as you may have noticed, the first level is symmetrical).
left_vertical_segments <- tribble(
~x, ~y, ~xend, ~yend,
0, 0, 0, 9,
0, 17, 0, 26,
2, 4, 2, 5,
2, 19, 2, 20,
2, 22, 2, 24,
4, 4, 4, 7,
4, 9, 4, 12,
4, 14, 4, 17,
4, 19, 4, 20,
4, 22, 4, 24,
6, 2, 6, 5,
6, 9, 6, 12,
6, 14, 6, 20,
6, 22, 6, 24,
8, 4, 8, 5,
8, 9, 8, 10,
8, 12, 8, 15,
8, 19, 8, 20,
8, 22, 8, 24
)
base_layer +
geom_segment(
data = left_vertical_segments,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
Then, I added the horizontal lines (still only on the left side of the maze)!
left_horizontal_segments <- tribble(
~x, ~y, ~xend, ~yend,
0, 0, 10, 0,
2, 2, 8, 2,
0, 4, 2, 4,
8, 4, 10, 4,
0, 5, 2, 5,
8, 5, 10, 5,
2, 7, 4, 7,
6, 7, 8, 7,
0, 9, 4, 9,
8, 9, 10, 9,
8, 10, 10, 10,
0, 12, 4, 12,
8, 12, 10, 12,
0, 14, 4, 14,
8, 15, 9, 15,
0, 17, 4, 17,
6, 17, 8, 17,
2, 19, 4, 19,
8, 19, 10, 19,
2, 20, 4, 20,
8, 20, 10, 20,
2, 22, 4, 22,
6, 22, 8, 22,
2, 24, 4, 24,
6, 24, 8, 24,
0, 26, 10, 26
)
left_segments <- bind_rows(left_vertical_segments, left_horizontal_segments)
base_layer +
geom_segment(
data = left_segments,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
The maze is slowly appearing, but surely. As I wrote earlier, the first level is symmetrical, so I used my left lines left_segments
to compute all the lines on the right right_segments
.
base_layer +
geom_segment(
data = bind_rows(left_segments, right_segments),
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
The middle vertical lines were missing, i.e., I did not want to plot them twice, which would have happen, if I added these in left_segments
. Also, the “door” of the ghost spawn area is missing. I added the door and the missing vertical walls in the end.
centre_vertical_segments <- tribble(
~x, ~y, ~xend, ~yend,
10, 2, 10, 4,
10, 7, 10, 9,
10, 17, 10, 19,
10, 22, 10, 26
)
door_segment <- tibble(x = 9, y = 15, xend = 11, yend = 15, type = "door")
Finally, I combined all the segments and drew them all.
base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend),
lineend = "round",
inherit.aes = FALSE,
colour = "white"
)
The maze is now complete, but no-one can actually see the door, since it appears the same way as the walls. You may have noticed, I added a column named type
. type
can currently hold two values: "wall"
and "door"
. I am going to use type
as values for two aesthetics, you may already have guessed which ones. The answer is the colour
and size
aesthetics.
base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, colour = type, size = type),
lineend = "round",
inherit.aes = FALSE
)
Note: maze_walls
is a dataset of ggpacman
(data("maze_walls", package = "ggpacman")
).
3.3 The bonus points layer
The strategy was quite the same as for the grid layer:
- Setting up the point coordinates for the left side and the middle.
- Compute the coordinates for the right side.
- Use a column
type
for the two types of bonus points, i.e.,"normal"
and"big"
(the one who weaken the ghosts).
bonus_points_coord <- function() {
left_bonus_points <- tribble(
~x, ~y, ~type,
1, c(1:3, 7:8, 18:22, 24:25), "normal",
1, c(6, 23), "big",
2, c(1, 3, 6, 8, 18, 21, 25), "normal",
3, c(1, 3:6, 8, 18, 21, 25), "normal",
4, c(1, 3, 8, 18, 21, 25), "normal",
5, c(1, 3:25), "normal",
6, c(1, 6, 8, 21, 25), "normal",
7, c(1, 3:6, 8, 18:21, 25), "normal",
8, c(1, 3, 6, 8, 18, 21, 25), "normal",
9, c(1:3, 6:8, 18, 21:25), "normal"
)
bind_rows(
left_bonus_points,
tribble(
~x, ~y, ~type,
10, c(1, 21), "normal"
),
mutate(left_bonus_points, x = abs(x - 20))
) %>%
unnest("y")
}
maze_points <- bonus_points_coord()
maze_layer <- base_layer +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, colour = type, size = type),
lineend = "round",
inherit.aes = FALSE
) +
geom_point(
data = maze_points,
mapping = aes(x = x, y = y, size = type, colour = type),
inherit.aes = FALSE
)
Note: maze_points
is a dataset of ggpacman
(data("maze_points", package = "ggpacman")
).
4 Pac-Man character
It is now time to draw the main character. To draw Pac-Man, I needed few things:
-
The Pac-Man moves, i.e., all the coordinates where Pac-Man is supposed to be at every
step
.data("pacman", package = "ggpacman") unnest(pacman, c("x", "y")) #> # A tibble: 150 × 3 #> x y colour #> <dbl> <dbl> <chr> #> 1 10 6 Pac-Man #> 2 10 6 Pac-Man #> 3 10 6 Pac-Man #> 4 10 6 Pac-Man #> 5 10 6 Pac-Man #> 6 10 6 Pac-Man #> 7 10 6 Pac-Man #> 8 10 6 Pac-Man #> 9 10 6 Pac-Man #> 10 10 6 Pac-Man #> # … with 140 more rows
maze_layer + geom_point( data = unnest(pacman, c("x", "y")), mapping = aes(x = x, y = y, colour = colour), size = 4 )
-
The Pac-Man shape (open and closed mouth). Since, Pac-Man is not a complete circle shape, I used
geom_arc_bar()
(fromggforce
), and defined the properties of each state of Pac-Man based on the aesthetics required by this function. Note: At first, I wanted a smooth animation/transition ofPac-Man opening and closing its mouth, this is why there arefour"close_"
states.pacman_state <- tribble( ~state, ~start, ~end, "open_right", 14 / 6 * pi, 4 / 6 * pi, "close_right", 15 / 6 * pi, 3 / 6 * pi, "open_up", 11 / 6 * pi, 1 / 6 * pi, "close_up", 12 / 3 * pi, 0 / 6 * pi, "open_left", 8 / 6 * pi, - 2 / 6 * pi, "close_left", 9 / 6 * pi, - 3 / 6 * pi, "open_down", 5 / 6 * pi, - 5 / 6 * pi, "close_down", pi, - pi )
ggplot() + geom_arc_bar( data = pacman_state, mapping = aes(x0 = 0, y0 = 0, r0 = 0, r = 0.5, start =start, end = end), fill = "yellow", inherit.aes = FALSE ) + facet_wrap(vars(state), ncol = 4)
Once those things available, how to make Pac-Man look where he is headed? Short answer, I just computed the differences between two successive positions of Pac-Man and added both open/close state to a new column state
.
pacman %>%
unnest(c("x", "y")) %>%
mutate(
state_x = sign(x - lag(x)),
state_y = sign(y - lag(y)),
state = case_when(
(is.na(state_x) | state_x %in% 0) & (is.na(state_y) | state_y %in% 0) ~ list(c("open_right", "close_right")),
state_x == 1 & state_y == 0 ~ list(c("open_right", "close_right")),
state_x == -1 & state_y == 0 ~ list(c("open_left", "close_left")),
state_x == 0 & state_y == -1 ~ list(c("open_down", "close_down")),
state_x == 0 & state_y == 1 ~ list(c("open_up", "close_up"))
)
) %>%
unnest("state")
#> # A tibble: 300 × 6
#> x y colour state_x state_y state
#> <dbl> <dbl> <chr> <dbl> <dbl> <chr>
#> 1 10 6 Pac-Man NA NA open_right
#> 2 10 6 Pac-Man NA NA close_right
#> 3 10 6 Pac-Man 0 0 open_right
#> 4 10 6 Pac-Man 0 0 close_right
#> 5 10 6 Pac-Man 0 0 open_right
#> 6 10 6 Pac-Man 0 0 close_right
#> 7 10 6 Pac-Man 0 0 open_right
#> 8 10 6 Pac-Man 0 0 close_right
#> 9 10 6 Pac-Man 0 0 open_right
#> 10 10 6 Pac-Man 0 0 close_right
#> # … with 290 more rows
Here, in preparation for gganimate
, I also added a column step
before merging the new upgraded pacman
(i.e., with the Pac-Man state
column) with the pacman_state
defined earlier.
pacman_moves <- ggpacman::compute_pacman_coord(pacman)
#> # A tibble: 300 × 9
#> x y colour state_x state_y state step start end
#> <dbl> <dbl> <chr> <dbl> <dbl> <chr> <int> <dbl> <dbl>
#> 1 10 6 Pac-Man NA NA open_right 1 7.33 2.09
#> 2 10 6 Pac-Man NA NA close_right 2 7.85 1.57
#> 3 10 6 Pac-Man 0 0 open_right 3 7.33 2.09
#> 4 10 6 Pac-Man 0 0 close_right 4 7.85 1.57
#> 5 10 6 Pac-Man 0 0 open_right 5 7.33 2.09
#> 6 10 6 Pac-Man 0 0 close_right 6 7.85 1.57
#> 7 10 6 Pac-Man 0 0 open_right 7 7.33 2.09
#> 8 10 6 Pac-Man 0 0 close_right 8 7.85 1.57
#> 9 10 6 Pac-Man 0 0 open_right 9 7.33 2.09
#> 10 10 6 Pac-Man 0 0 close_right 10 7.85 1.57
#> # … with 290 more rows
maze_layer +
geom_arc_bar(
data = pacman_moves,
mapping = aes(x0 = x, y0 = y, r0 = 0, r = 0.5, start = start, end = end, colour = colour, fill = colour, group = step),
inherit.aes = FALSE
)
You can’t see much?! Ok, perhaps it’s time to use gganimate
. I am going to animate Pac-Man based on the column step
, which is, if you looked at the code above, just the line number of pacman_moves
.
animated_pacman <- maze_layer +
geom_arc_bar(
data = pacman_moves,
mapping = aes(x0 = x, y0 = y, r0 = 0, r = 0.5, start = start, end = end, colour = colour, fill = colour, group = step),
inherit.aes = FALSE
) +
transition_manual(step)
Note: pacman
is a dataset of ggpacman
(data("pacman", package = "ggpacman")
).
5 The Ghosts characters
Time to draw the ghosts, namely: Blinky, Pinky, Inky and Clyde.
5.1 Body
I started with the body, especially the top and the bottom part of the ghost which are half circle (or at least I chose this) and use again geom_arc_bar()
.
ghost_arc <- tribble(
~x0, ~y0, ~r, ~start, ~end, ~part,
0, 0, 0.5, - 1 * pi / 2, 1 * pi / 2, "top",
-0.5, -0.5 + 1/6, 1 / 6, pi / 2, 2 * pi / 2, "bottom",
-1/6, -0.5 + 1/6, 1 / 6, pi / 2, 3 * pi / 2, "bottom",
1/6, -0.5 + 1/6, 1 / 6, pi / 2, 3 * pi / 2, "bottom",
0.5, -0.5 + 1/6, 1 / 6, 3 * pi / 2, 2 * pi / 2, "bottom"
)
I retrieved the coordinates of the created polygon, using ggplot_build()
.
top_polygon <- ggplot_build(top)$data[[1]][, c("x", "y")]
And I proceeded the same way for the bottom part of the ghost.
bottom_polygon <- ggplot_build(bottom)$data[[1]][, c("x", "y")]
Then, I just added one point to “properly” link the top and the bottom part.
ghost_body <- dplyr::bind_rows(
top_polygon,
dplyr::tribble(
~x, ~y,
0.5, 0,
0.5, -0.5 + 1/6
),
bottom_polygon,
dplyr::tribble(
~x, ~y,
-0.5, -0.5 + 1/6,
-0.5, 0
)
)
I finally got the whole ghost shape I was looking for.
ggplot() +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1)) +
geom_polygon(
data = ghost_body,
mapping = aes(x = x, y = y),
inherit.aes = FALSE
)
Note: ghost_body
is a dataset of ggpacman
(data("ghost_body", package = "ggpacman")
).
Note: ghost_body
definitely needs some code refactoring.
5.2 Eyes
The eyes are quite easy to draw, they are just circles, but … As for Pac-Man before, I wanted the ghosts to look where they are headed. This implies moving the iris one way or the other, and so I defined five states for the iris: right, down, left, up and middle.
ghost_eyes <- tribble(
~x0, ~y0, ~r, ~part, ~direction,
1/5, 1/8, 1/8, "eye", c("up", "down", "right", "left", "middle"),
-1/5, 1/8, 1/8, "eye", c("up", "down", "right", "left", "middle"),
5/20, 1/8, 1/20, "iris", "right",
-3/20, 1/8, 1/20, "iris", "right",
1/5, 1/16, 1/20, "iris", "down",
-1/5, 1/16, 1/20, "iris", "down",
3/20, 1/8, 1/20, "iris", "left",
-5/20, 1/8, 1/20, "iris", "left",
1/5, 3/16, 1/20, "iris", "up",
-1/5, 3/16, 1/20, "iris", "up",
1/5, 1/8, 1/20, "iris", "middle",
-1/5, 1/8, 1/20, "iris", "middle"
) %>%
unnest("direction")
map_eyes <- c("eye" = "white", "iris" = "black")
ggplot() +
coord_fixed(xlim = c(-0.5, 0.5), ylim = c(-0.5, 0.5)) +
scale_fill_manual(breaks = names(map_eyes), values = map_eyes) +
scale_colour_manual(breaks = names(map_eyes), values = map_eyes) +
geom_circle(
data = ghost_eyes,
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part),
inherit.aes = FALSE,
show.legend = FALSE
) +
facet_wrap(vars(direction), ncol = 3)
Note: ghost_eyes
is a dataset of ggpacman
(data("ghost_eyes", package = "ggpacman")
).
5.3 Ghost shape
I had the whole ghost shape and the eyes.
ggplot() +
coord_fixed(xlim = c(-1, 1), ylim = c(-1, 1)) +
scale_fill_manual(breaks = names(map_colours), values = map_colours) +
scale_colour_manual(breaks = names(map_colours), values = map_colours) +
geom_polygon(
data = get(data("ghost_body", package = "ggpacman")),
mapping = aes(x = x, y = y),
inherit.aes = FALSE
) +
geom_circle(
data = get(data("ghost_eyes", package = "ggpacman")),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part),
inherit.aes = FALSE,
show.legend = FALSE
) +
facet_wrap(vars(direction), ncol = 3)
Again, same as for Pac-Man, in order to know where the ghosts are supposed to look, I computed the differences of each successive positions of the ghosts and I added the corresponding directions.
blinky_ghost <- tibble(x = c(0, 1, 1, 0, 0), y = c(0, 0, 1, 1, 0), colour = "Blinky") %>%
unnest(c("x", "y")) %>%
mutate(
X0 = x,
Y0 = y,
state_x = sign(round(x) - lag(round(x))),
state_y = sign(round(y) - lag(round(y))),
direction = case_when(
(is.na(state_x) | state_x %in% 0) & (is.na(state_y) | state_y %in% 0) ~ "middle",
state_x == 1 & state_y == 0 ~ "right",
state_x == -1 & state_y == 0 ~ "left",
state_x == 0 & state_y == -1 ~ "down",
state_x == 0 & state_y == 1 ~ "up"
)
) %>%
unnest("direction")
#> # A tibble: 5 × 8
#> x y colour X0 Y0 state_x state_y direction
#> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 0 0 Blinky 0 0 NA NA middle
#> 2 1 0 Blinky 1 0 1 0 right
#> 3 1 1 Blinky 1 1 0 1 up
#> 4 0 1 Blinky 0 1 -1 0 left
#> 5 0 0 Blinky 0 0 0 -1 down
I also added some noise around the position, i.e., four noised position at each actual position of a ghost.
#> # A tibble: 20 × 12
#> x y colour X0 Y0 state_x state_y direction state step noise_x
#> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <int> <int> <dbl>
#> 1 0 0 Blinky 0 0 NA NA middle 1 1 0.0586
#> 2 0 0 Blinky 0 0 NA NA middle 2 2 0.0619
#> 3 0 0 Blinky 0 0 NA NA middle 3 3 -0.0552
#> 4 0 0 Blinky 0 0 NA NA middle 4 4 0.0196
#> 5 1 0 Blinky 1 0 1 0 right 1 5 0.0342
#> 6 1 0 Blinky 1 0 1 0 right 2 6 -0.0173
#> 7 1 0 Blinky 1 0 1 0 right 3 7 0.0616
#> 8 1 0 Blinky 1 0 1 0 right 4 8 -0.0979
#> 9 1 1 Blinky 1 1 0 1 up 1 9 0.0481
#> 10 1 1 Blinky 1 1 0 1 up 2 10 0.0540
#> 11 1 1 Blinky 1 1 0 1 up 3 11 0.0445
#> 12 1 1 Blinky 1 1 0 1 up 4 12 0.0641
#> 13 0 1 Blinky 0 1 -1 0 left 1 13 -0.0431
#> 14 0 1 Blinky 0 1 -1 0 left 2 14 0.00634
#> 15 0 1 Blinky 0 1 -1 0 left 3 15 -0.0333
#> 16 0 1 Blinky 0 1 -1 0 left 4 16 -0.0391
#> 17 0 0 Blinky 0 0 0 -1 down 1 17 0.0164
#> 18 0 0 Blinky 0 0 0 -1 down 2 18 -0.00357
#> 19 0 0 Blinky 0 0 0 -1 down 3 19 -0.0166
#> 20 0 0 Blinky 0 0 0 -1 down 4 20 -0.0298
#> # … with 1 more variable: noise_y <dbl>
Then, I added (in a weird way I might say) the polygons coordinates for the body and the eyes.
blinky_ghost <- blinky_ghost %>%
mutate(
body = pmap(
.l = list(x, y, noise_x, noise_y),
.f = function(.x, .y, .noise_x, .noise_y) {
mutate(
.data = get(data("ghost_body")),
x = x + .x + .noise_x,
y = y + .y + .noise_y
)
}
),
eyes = pmap(
.l = list(x, y, noise_x, noise_y, direction),
.f = function(.x, .y, .noise_x, .noise_y, .direction) {
mutate(
.data = filter(get(data("ghost_eyes")), direction == .direction),
x0 = x0 + .x + .noise_x,
y0 = y0 + .y + .noise_y,
direction = NULL
)
}
),
x = NULL,
y = NULL
)
#> # A tibble: 20 × 12
#> colour X0 Y0 state_x state_y direction state step noise_x noise_y
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <int> <int> <dbl> <dbl>
#> 1 Blinky 0 0 NA NA middle 1 1 0.0586 -0.0504
#> 2 Blinky 0 0 NA NA middle 2 2 0.0619 -0.00298
#> 3 Blinky 0 0 NA NA middle 3 3 -0.0552 0.0317
#> 4 Blinky 0 0 NA NA middle 4 4 0.0196 -0.104
#> 5 Blinky 1 0 1 0 right 1 5 0.0342 0.0781
#> 6 Blinky 1 0 1 0 right 2 6 -0.0173 0.00306
#> 7 Blinky 1 0 1 0 right 3 7 0.0616 0.0107
#> 8 Blinky 1 0 1 0 right 4 8 -0.0979 -0.0575
#> 9 Blinky 1 1 0 1 up 1 9 0.0481 -0.0265
#> 10 Blinky 1 1 0 1 up 2 10 0.0540 -0.0886
#> 11 Blinky 1 1 0 1 up 3 11 0.0445 -0.0244
#> 12 Blinky 1 1 0 1 up 4 12 0.0641 -0.0399
#> 13 Blinky 0 1 -1 0 left 1 13 -0.0431 0.0715
#> 14 Blinky 0 1 -1 0 left 2 14 0.00634 0.0267
#> 15 Blinky 0 1 -1 0 left 3 15 -0.0333 -0.0453
#> 16 Blinky 0 1 -1 0 left 4 16 -0.0391 0.0148
#> 17 Blinky 0 0 0 -1 down 1 17 0.0164 0.0367
#> 18 Blinky 0 0 0 -1 down 2 18 -0.00357 0.0986
#> 19 Blinky 0 0 0 -1 down 3 19 -0.0166 -0.0322
#> 20 Blinky 0 0 0 -1 down 4 20 -0.0298 0.0287
#> # … with 2 more variables: body <list>, eyes <list>
For ease, it is now a call to one function directly on the position matrix of a ghost.
blinky_plot <- base_layer +
coord_fixed(xlim = c(-1, 2), ylim = c(-1, 2)) +
geom_polygon(
data = unnest(blinky_moves, "body"),
mapping = aes(x = x, y = y, fill = colour, colour = colour, group = step),
inherit.aes = FALSE
) +
geom_circle(
data = unnest(blinky_moves, "eyes"),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part, group = step),
inherit.aes = FALSE
)
Again, it is better with an animated GIF.
animated_blinky <- blinky_plot + transition_manual(step)
6 How Pac-Man interacts with the maze?
6.1 Bonus points
For ease, I am using some functions I defined to go quickly to the results of the first part of this readme. The idea here is to look at all the position in common between Pac-Man (pacman_moves
) and the bonus points (maze_points
). Each time Pac-Man was at the same place as a bonus point, I defined a status "eaten"
for all values of step
after. I ended up with a big table with position and the state of the bonus points.
pacman_moves <- ggpacman::compute_pacman_coord(get(data("pacman", package = "ggpacman")))
right_join(get(data("maze_points")), pacman_moves, by = c("x", "y")) %>%
distinct(step, x, y, type) %>%
mutate(
step = map2(step, max(step), ~ seq(.x, .y, 1)),
colour = "eaten"
) %>%
unnest("step")
#> # A tibble: 45,150 × 5
#> x y type step colour
#> <dbl> <dbl> <chr> <dbl> <chr>
#> 1 1 1 normal 61 eaten
#> 2 1 1 normal 62 eaten
#> 3 1 1 normal 63 eaten
#> 4 1 1 normal 64 eaten
#> 5 1 1 normal 65 eaten
#> 6 1 1 normal 66 eaten
#> 7 1 1 normal 67 eaten
#> 8 1 1 normal 68 eaten
#> 9 1 1 normal 69 eaten
#> 10 1 1 normal 70 eaten
#> # … with 45,140 more rows
Again, for ease, I am using a function I defined to compute everything.
If you don’t recall, maze_layer
already includes a geometry with the bonus points.
I could have change this geometry (i.e., geom_point()
), but I did not, and draw a new geometry on top of the previous ones. Do you remember the values of the scale for the size aesthetic?
scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3))
maze_layer_points <- maze_layer +
geom_point(
data = bonus_points_eaten,
mapping = aes(x = x, y = y, colour = colour, size = colour, group = step),
inherit.aes = FALSE
)
A new animation to see, how the new geometry is overlapping the previous one as step
increases.
animated_points <- maze_layer_points + transition_manual(step)
6.2 Ghost "weak"
and "eaten"
states
The ghosts were more tricky (I know, they are ghosts …).
I first retrieved all the positions where a "big"
bonus point was eaten by Pac-Man.
#> # A tibble: 93 × 3
#> step_init step vulnerability
#> <dbl> <dbl> <lgl>
#> 1 79 79 TRUE
#> 2 79 80 TRUE
#> 3 79 81 TRUE
#> 4 79 82 TRUE
#> 5 79 83 TRUE
#> 6 79 84 TRUE
#> 7 79 85 TRUE
#> 8 79 86 TRUE
#> 9 79 87 TRUE
#> 10 79 88 TRUE
#> # … with 83 more rows
This is part of a bigger function (I won’t dive too deep into it).
ggpacman::compute_ghost_status
#> function (ghost, pacman_moves, bonus_points_eaten)
#> {
#> ghosts_vulnerability <- bonus_points_eaten %>% dplyr::filter(.data[["type"]] ==
#> "big") %>% dplyr::group_by(.data[["x"]], .data[["y"]]) %>%
#> dplyr::summarise(step_init = min(.data[["step"]])) %>%
#> dplyr::ungroup() %>% dplyr::mutate(step = purrr::map(.data[["step_init"]],
#> ~seq(.x, .x + 30, 1)), vulnerability = TRUE, x = NULL,
#> y = NULL) %>% tidyr::unnest("step")
#> ghost_out <- dplyr::left_join(x = compute_ghost_coord(ghost),
#> y = pacman_moves %>% dplyr::mutate(ghost_eaten = TRUE) %>%
#> dplyr::select(c(X0 = "x", Y0 = "y", "step", "ghost_eaten")),
#> by = c("X0", "Y0", "step")) %>% dplyr::left_join(y = ghosts_vulnerability,
#> by = "step") %>% dplyr::mutate(vulnerability = tidyr::replace_na(.data[["vulnerability"]],
#> FALSE), ghost_name = .data[["colour"]], ghost_eaten = .data[["ghost_eaten"]] &
#> .data[["vulnerability"]], colour = ifelse(.data[["vulnerability"]],
#> paste0(.data[["ghost_name"]], "_weak"), .data[["colour"]]))
#> pos_eaten_start <- which(ghost_out[["ghost_eaten"]])
#> ghosts_home <- which(ghost_out[["X0"]] == 10 & ghost_out[["Y0"]] ==
#> 14)
#> for (ipos in pos_eaten_start) {
#> pos_eaten_end <- min(ghosts_home[ghosts_home >= ipos])
#> ghost_out[["colour"]][ipos:pos_eaten_end] <- paste0(unique(ghost_out[["ghost_name"]]),
#> "_eaten")
#> }
#> dplyr::left_join(x = ghost_out, y = ghost_out %>% dplyr::filter(.data[["step"]] ==
#> .data[["step_init"]] & grepl("eaten", .data[["colour"]])) %>%
#> dplyr::mutate(already_eaten = TRUE) %>% dplyr::select(c("step_init",
#> "already_eaten")), by = "step_init") %>% dplyr::mutate(colour = dplyr::case_when(.data[["already_eaten"]] &
#> .data[["X0"]] == 10 & .data[["Y0"]] == 14 ~ paste0(.data[["ghost_name"]],
#> "_eaten"), grepl("weak", .data[["colour"]]) & .data[["already_eaten"]] ~
#> .data[["ghost_name"]], TRUE ~ .data[["colour"]]))
#> }
#> <bytecode: 0x10c690fb0>
#> <environment: namespace:ggpacman>
The goal of this function, is to compute the different states of a ghost, according to the bonus points eaten and, of course, the current Pac-Man position at a determined step
.
pacman_moves <- ggpacman::compute_pacman_coord(get(data("pacman", package = "ggpacman")))
bonus_points_eaten <- ggpacman::compute_points_eaten(get(data("maze_points")), pacman_moves)
ghost_moves <- ggpacman::compute_ghost_status(
ghost = get(data("blinky", package = "ggpacman")),
pacman_moves = pacman_moves,
bonus_points_eaten = bonus_points_eaten
)
ghost_moves %>%
filter(state == 1) %>%
distinct(step, direction, colour, vulnerability) %>%
as.data.frame()
#> colour direction step vulnerability
#> 1 Blinky middle 1 FALSE
#> 2 Blinky middle 5 FALSE
#> 3 Blinky middle 9 FALSE
#> 4 Blinky middle 13 FALSE
#> 5 Blinky middle 17 FALSE
#> 6 Blinky middle 21 FALSE
#> 7 Blinky middle 25 FALSE
#> 8 Blinky middle 29 FALSE
#> 9 Blinky middle 33 FALSE
#> 10 Blinky left 37 FALSE
#> 11 Blinky left 41 FALSE
#> 12 Blinky left 45 FALSE
#> 13 Blinky down 49 FALSE
#> 14 Blinky down 53 FALSE
#> 15 Blinky down 57 FALSE
#> 16 Blinky left 61 FALSE
#> 17 Blinky left 65 FALSE
#> 18 Blinky down 69 FALSE
#> 19 Blinky down 73 FALSE
#> 20 Blinky down 77 FALSE
#> 21 Blinky_weak down 81 TRUE
#> 22 Blinky_weak down 85 TRUE
#> 23 Blinky_eaten left 89 TRUE
#> 24 Blinky_eaten right 93 TRUE
#> 25 Blinky_eaten middle 97 TRUE
#> 26 Blinky_eaten middle 101 TRUE
#> 27 Blinky_eaten right 105 TRUE
#> 28 Blinky_eaten up 109 TRUE
#> 29 Blinky_eaten right 113 FALSE
#> 30 Blinky_eaten up 117 FALSE
#> 31 Blinky_eaten right 121 FALSE
#> 32 Blinky_eaten up 125 FALSE
#> 33 Blinky_eaten right 129 FALSE
#> 34 Blinky_eaten up 133 FALSE
#> 35 Blinky_eaten right 137 FALSE
#> 36 Blinky_eaten up 141 TRUE
#> 37 Blinky_eaten up 145 TRUE
#> 38 Blinky_eaten middle 149 TRUE
#> 39 Blinky_eaten middle 153 TRUE
#> 40 Blinky_eaten middle 157 TRUE
#> 41 Blinky up 161 TRUE
#> 42 Blinky up 165 TRUE
#> 43 Blinky right 169 TRUE
#> 44 Blinky right 173 FALSE
#> 45 Blinky right 177 FALSE
#> 46 Blinky down 181 FALSE
#> 47 Blinky down 185 FALSE
#> 48 Blinky down 189 FALSE
#> 49 Blinky down 193 FALSE
#> 50 Blinky down 197 FALSE
#> 51 Blinky down 201 FALSE
#> 52 Blinky down 205 FALSE
#> 53 Blinky down 209 FALSE
#> 54 Blinky left 213 FALSE
#> 55 Blinky_weak left 217 TRUE
#> 56 Blinky_weak down 221 TRUE
#> 57 Blinky_weak down 225 TRUE
#> 58 Blinky_weak right 229 TRUE
#> 59 Blinky_weak right 233 TRUE
#> 60 Blinky_weak right 237 TRUE
#> 61 Blinky_weak right 241 TRUE
#> 62 Blinky_weak middle 245 TRUE
#> 63 Blinky down 249 FALSE
#> 64 Blinky down 253 FALSE
#> 65 Blinky down 257 FALSE
#> 66 Blinky right 261 FALSE
#> 67 Blinky right 265 FALSE
#> 68 Blinky up 269 FALSE
#> 69 Blinky up 273 FALSE
#> 70 Blinky up 277 FALSE
#> 71 Blinky middle 281 FALSE
#> 72 Blinky right 285 FALSE
#> 73 Blinky right 289 FALSE
#> 74 Blinky up 293 FALSE
#> 75 Blinky up 297 FALSE
To simplify a little, below a small example of a ghost moving in one direction with predetermined states.
blinky_ghost <- bind_rows(
tibble(x = 1:4, y = 0, colour = "Blinky"),
tibble(x = 5:8, y = 0, colour = "Blinky_weak"),
tibble(x = 9:12, y = 0, colour = "Blinky_eaten")
)
blinky_moves <- ggpacman::compute_ghost_coord(blinky_ghost)
#> # A tibble: 48 × 12
#> colour X0 Y0 state_x state_y direc…¹ state step noise_x noise_y body
#> <chr> <int> <dbl> <dbl> <dbl> <chr> <int> <int> <dbl> <dbl> <lis>
#> 1 Blinky 1 0 NA NA middle 1 1 0.0381 -1.02e-2 <df>
#> 2 Blinky 1 0 NA NA middle 2 2 0.0138 8.09e-2 <df>
#> 3 Blinky 1 0 NA NA middle 3 3 -0.0236 -7.36e-2 <df>
#> 4 Blinky 1 0 NA NA middle 4 4 0.0463 -1.09e-1 <df>
#> 5 Blinky 2 0 1 0 right 1 5 0.0443 -5.53e-2 <df>
#> 6 Blinky 2 0 1 0 right 2 6 -0.0241 6.92e-2 <df>
#> 7 Blinky 2 0 1 0 right 3 7 -0.0517 2.81e-2 <df>
#> 8 Blinky 2 0 1 0 right 4 8 0.0766 1.86e-2 <df>
#> 9 Blinky 3 0 1 0 right 1 9 0.0182 1.15e-2 <df>
#> 10 Blinky 3 0 1 0 right 2 10 -0.0943 6.72e-4 <df>
#> # … with 38 more rows, 1 more variable: eyes <list>, and abbreviated variable
#> # name ¹direction
blinky_plot <- base_layer +
coord_fixed(xlim = c(0, 13), ylim = c(-1, 1)) +
geom_polygon(
data = unnest(blinky_moves, "body"),
mapping = aes(x = x, y = y, fill = colour, colour = colour, group = step),
inherit.aes = FALSE
) +
geom_circle(
data = unnest(blinky_moves, "eyes"),
mapping = aes(x0 = x0, y0 = y0, r = r, colour = part, fill = part, group = step),
inherit.aes = FALSE
)
I am sure, you remember all the colours and their mapped values from the beginning, so you probably won’t need the following to understand of the ghost disappeared.
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
Note: yes, "transparent"
is a colour and a very handy one.
A new animation to see our little Blinky in action?
animated_blinky <- blinky_plot + transition_manual(step)
7 Plot time
In the current version, nearly everything is either a dataset or a function and could be used like this.
7.1 Load and compute the data
data("pacman", package = "ggpacman")
data("maze_points", package = "ggpacman")
data("maze_walls", package = "ggpacman")
data("blinky", package = "ggpacman")
data("pinky", package = "ggpacman")
data("inky", package = "ggpacman")
data("clyde", package = "ggpacman")
ghosts <- list(blinky, pinky, inky, clyde)
pacman_moves <- ggpacman::compute_pacman_coord(pacman)
bonus_points_eaten <- ggpacman::compute_points_eaten(maze_points, pacman_moves)
map_colours <- c(
"READY!" = "goldenrod1",
"wall" = "dodgerblue3", "door" = "dodgerblue3",
"normal" = "goldenrod1", "big" = "goldenrod1", "eaten" = "black",
"Pac-Man" = "yellow",
"eye" = "white", "iris" = "black",
"Blinky" = "red", "Blinky_weak" = "blue", "Blinky_eaten" = "transparent",
"Pinky" = "pink", "Pinky_weak" = "blue", "Pinky_eaten" = "transparent",
"Inky" = "cyan", "Inky_weak" = "blue", "Inky_eaten" = "transparent",
"Clyde" = "orange", "Clyde_weak" = "blue", "Clyde_eaten" = "transparent"
)
7.2 Build the base layer with the maze
base_grid <- ggplot() +
theme_void() +
theme(
legend.position = "none",
plot.background = element_rect(fill = "black", colour = "black"),
panel.background = element_rect(fill = "black", colour = "black")
) +
scale_size_manual(values = c("wall" = 2.5, "door" = 1, "big" = 2.5, "normal" = 0.5, "eaten" = 3)) +
scale_fill_manual(breaks = names(map_colours), values = map_colours) +
scale_colour_manual(breaks = names(map_colours), values = map_colours) +
coord_fixed(xlim = c(0, 20), ylim = c(0, 26)) +
geom_segment(
data = maze_walls,
mapping = aes(x = x, y = y, xend = xend, yend = yend, size = type, colour = type),
lineend = "round",
inherit.aes = FALSE
) +
geom_point(
data = maze_points,
mapping = aes(x = x, y = y, size = type, colour = type),
inherit.aes = FALSE
) +
geom_text(
data = tibble(x = 10, y = 11, label = "READY!", step = 1:20),
mapping = aes(x = x, y = y, label = label, colour = label, group = step),
size = 6
)
base_grid
7.3 Draw the "eaten"
bonus points geometry
p_points <- list(
geom_point(
data = bonus_points_eaten,
mapping = aes(x = x, y = y, colour = colour, size = colour, group = step),
inherit.aes = FALSE
)
)
base_grid + p_points
7.4 Draw the main character (I am talking about Pac-Man …)
p_pacman <- list(
geom_arc_bar(
data = pacman_moves,
mapping = aes(
x0 = x, y0 = y,
r0 = 0, r = 0.5,
start = start, end = end,
colour = colour, fill = colour,
group = step
),
inherit.aes = FALSE
)
)
base_grid + p_pacman
7.5 Draw the ghosts, using the trick that +
works also on a list of geometries
p_ghosts <- map(.x = ghosts, .f = function(data) {
ghost_moves <- compute_ghost_status(
ghost = data,
pacman_moves = pacman_moves,
bonus_points_eaten = bonus_points_eaten
)
list(
geom_polygon(
data = unnest(ghost_moves, "body"),
mapping = aes(
x = x, y = y,
fill = colour, colour = colour,
group = step
),
inherit.aes = FALSE
),
geom_circle(
data = unnest(ghost_moves, "eyes"),
mapping = aes(
x0 = x0, y0 = y0,
r = r,
colour = part, fill = part,
group = step
),
inherit.aes = FALSE
)
)
})
base_grid + p_ghosts
7.6 Draw everything
base_grid + p_points + p_pacman + p_ghosts
7.7 Animate everything
PacMan <- base_grid + p_points + p_pacman + p_ghosts + transition_manual(step)