Ggpacman
A `ggplot2` and `gganimate` Version of Pac-Man
Install / Use
/learn @mcanouil/GgpacmanREADME
A ggplot2 and gganimate Version of Pac-Man <img src="man/figures/ggpacman.gif" align="right" width="120" />
<!-- badges: start -->
<!-- badges: end -->
The goal of ggpacman is to …
Make a GIF of the game Pac-Man (not to develop an R version of Pac-Man
…).
Installation
# Install ggpacman from CRAN:
install.packages("ggpacman")
# Or the the development version from GitHub:
# install.packages("remotes")
remotes::install_github("mcanouil/ggpacman")
Pac-Man in action
library(ggpacman)
animate_pacman(
pacman = pacman,
ghosts = list(blinky, pinky, inky, clyde),
font_family = "xkcd"
)
<!-- -->
The Story of ggpacman
It started on a Saturday evening …
It was the 21<sup>st</sup> 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/IMDbRating) 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.
<blockquote class="twitter-tweet"> <p lang="en" dir="ltr"> Maybe I went too far with <a href="https://twitter.com/hashtag/ggplot2?src=hash&ref_src=twsrc%5Etfw">\#ggplot2</a> and <a href="https://twitter.com/hashtag/gganimate?src=hash&ref_src=twsrc%5Etfw">\#gganimate</a> …😅<br>What do you think <a href="https://twitter.com/hadleywickham?ref_src=twsrc%5Etfw">@hadleywickham</a> & <a href="https://twitter.com/thomasp85?ref_src=twsrc%5Etfw">@thomasp85</a> , did I go too far or not enough ? (I am planning to add the ghosts 😎) <a href="https://t.co/nkfbti1Etd">pic.twitter.com/nkfbti1Etd</a> </p> — Mickaël CANOUIL (@mickaelcanouil) <a href="https://twitter.com/mickaelcanouil/status/1241760925499170824?ref_src=twsrc%5Etfw">March 22, 2020</a> </blockquote>- The packages
- The maze layer
- Pac-Man character
- The Ghosts characters
- How Pac-Man interacts with the maze?
- Plot time
The packages
library("stats")
library("utils")
library("rlang")
library("magrittr")
library("dplyr")
library("tidyr")
library("purrr")
library("ggplot2")
library("ggforce")
library("gganimate")
library("ggtext")
The maze layer
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.
base_layer <- ggplot() +
theme_void() +
theme(
legend.position = "none",
plot.background = element_rect(fill = "black", colour = "black"),
panel.background = element_rect(fill = "black", colour = "black"),
) +
coord_fixed(xlim = c(0, 20), ylim = c(0, 26))
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"
)
base_layer <- base_layer +
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)
<!-- -->
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?!
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.
right_segments <- mutate(
.data = left_segments,
x = abs(x - 20),
xend = abs(xend - 20)
)
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.
maze_walls <- bind_rows(
left_segments,
centre_vertical_segments,
right_segments
) %>%
mutate(type = "wall") %>%
bind_rows(door_segment)
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
